Writing your own VCD File with Common Lisp
FPGACommon Lisp
2023-6-15 10:51 JST

VCD ファイルを出力するような Lisp のプログラムを書いてみた。

VCD の仕様

VCD(Value Change Dump) は Verilog HDL の仕様の一部で波形をダンプする際のASCII ベースの形式です。ASCII なので中身を覗くことが出来ます。iverilog が生成する VCD ファイルを見ると簡単そうです。自前の回路シミュレータにVCD を出力する機能をつけてみました。

VCD の構成

1364-2005 Figure 18-1 によると VCD File は次の3部構成になっています。

Node Information は階層化出来るようですが、今回は簡易版なのでどフラットに情報を出力します。Value Changes は終わり(を示すマーク)がありません。これはどうも次々と Value Changes が生成され、それをリアルタイムで表示するということも念頭にあるようです。

Header Information

まずは Header を出力します。format でかなり無理矢理な感じで出力します。date のフォーマットはどうでも良さそうだったので month が数字のままです。

vcd.lisp
(defun vcd-show-header (vcd-obj)
  (let ((str (vcd-stream vcd-obj)))
    (format str "$date~%")
    (multiple-value-bind
        (second minute hour day month year day-of-week dst-p tz)
        (get-decoded-time)
      (format str "    ~a ~a ~a ~a:~a:~a (GMT~@d) ~a~%"
      (nth day-of-week *day-names*)
      month day hour minute second (- tz) year
         ))
    (format str "$end~%")
    (format str "$version~%")
    (format str "    ~a~%" *vcd-version*)
    (format str "$end~%")
    (format str "$timescale~%")
    (let* ((timescale (vcd-timescale vcd-obj))
           (timescale-num (car timescale))
           (timescale-unit (string-downcase (cadr timescale))))
      (format str "    ~a~a~%" timescale-num timescale-unit))
    (format str "$end~%")))

Node Information

次に Node Information を出力します。本来は Node というだけあって、階層構造を表現できるのですが、簡易版なのでトップだけを出力します。ここでは vars に Variable の定義が入っているものとして、そのすべての情報を出力しています。そして、最後に $enddefinitions を出力して定義の終わりを示します。

vcd.lisp
(defun vcd-show-module-tb (vcd-obj)
  (let ((str (vcd-stream vcd-obj))
        (vars (vcd-vars vcd-obj)))
    (format str "$scope module tb $end~%")
    (format str "$upscope $end~%")
    (dolist (var-obj vars)
      (format str "$var ~a ~a ~a ~a $end~%"
        (vcd-var-type var-obj)
        (vcd-var-size var-obj)
        (vcd-var-id-code var-obj)
        (vcd-var-reference-name var-obj)))
    (format str "$enddefinitions $end~%")))

Value Changes

あとは延々と Value Changes をかけばいいだけです。vcd-probe という関数を定義して現在時刻と値とIDを書くようにしました。この関数は値が変わるたびに呼ばれます(回路シミュレータがそのように設計されている)。

vcd.lisp
(defun vcd-probe (vcd-obj id-code xsig)
  (let ((_id-code id-code)
        (_vcd-obj vcd-obj))
     (add-action
       xsig
       (lambda (sig)
         (format (vcd-stream vcd-obj) "#~a~%~a~a~%" (get-current-time the-agenda) 
                 (get-xsignal sig) _id-code )))))

使ってみる

実際に使ってみた。と言いたいところですが、package 化する前のテストプログラムだからちょっと古い。雰囲気だけ。clk も add-action で作ってるけど、今は make-clock という関数を用意しています。

vcd.lisp
(setf vcd0 (make-vcd))

(setf clk (make-xsignal :value 0))
(setf i0 (make-xsignal :value 0))
(setf o0 (make-xsignal :value 1))
(setf i1 (make-xsignal :value 1))
(setf o1 (make-xsignal :value 0))
(clk-ff-gate i0 clk o0)
(clk-ff-gate i1 clk o1)
(add-action clk
  (lambda (sig)
    (let ((now-value (xsignal-value sig)))
      (after-delay
        5
        (lambda ()
          (set-xsignal sig (- 1 now-value)))))))

; 本当は make-xsignal の type に type と size が入っていて
; そこを参照するようにしたい
; そうなれば (vcd-add-vars vcd0 clk) だけでいい

(vcd-add-vars vcd0 clk "reg" 1 nil "clk")
(vcd-add-vars vcd0 i0 "reg" 1 nil "i0")
(vcd-add-vars vcd0 o0 "reg" 1 nil "o0")
(vcd-add-vars vcd0 i1 "reg" 1 nil "i1")
(vcd-add-vars vcd0 o1 "reg" 1 nil "o1")

(with-open-file (str "test3.vcd" :direction :output :if-exists :overwrite :if-does-not-exist :create)
  (setf (vcd-stream vcd0) str)
  (vcd-show-header vcd0)
  (vcd-show-module-tb vcd0)
  (vcd-show-start-dumpvars vcd0)

  (propagate 17)
  (set-xsignal i0 (xsignal-value o1))
  (set-xsignal i1 (xsignal-value o0))
  (propagate 13)
  )

GTKWave で結果を見る

結果は GTKWave で見ることが出来ます。

GTKWave っていろいろできるのね

GTKWave の ユーザズガイドを見ると面白い機能があります。時間があったら使ってみようと思います。

リンク集