(注意: 書きかけです。順序回路の前まで)

SFLチュートリアル(NSLにも対応)

リポジトリ

サンプルなどは GitHub リポジトリに置いてあります( https://github.com/metanest/sfl_tutorial )。

1 SFLとは何か

1.1 SFLとは何か

SFL(Structured Function description Language)は、VHDLやVerilog HDLといったようなハードウェア記述言語の一種ですが、それらと違い同期設計に記述対象を特化することで、設計用として見通しの良い言語です。なお、似たような意図のHDLであるChiselは "hardware construction language" としています。

1.2 NSLについて

SFLのもともとの処理系はPARTHENON(パルテノン、Parallel Architecture Refiner Theorized by NTT Original Concept)に含まれており、現在もパルテノン研究会に問い合わせれば、入手およびライセンスの継続が可能ですが、ASICのパターンまで一気に合成する前提のためFPGA用に向かない、等の問題がありました。

NSL(New Synthesis Language)は、近年のFPGA用ベンダ製ツールに合わせ合成ターゲットをVerilog HDLに特化したsfl2vlとして、清水尚彦先生により開発されていた処理系を発展させたもので、トークン(字句)の記法をVerilog HDLに合わせるような変更や、いくつかの機能強化と使わない機能の整理などが行われた言語です。

このチュートリアルでは、基本的にはSFLで解説しますが、適宜NSLの場合についての補足説明も行います。

1.3 ハードウェア記述言語とプログラミング言語

プログラミング言語はコンピュータプログラムを記述するための言語です。それに対してハードウェア記述言語は(論理)回路を記述するための言語です。「ある入力からある出力を得るためのモノ」を記述する言語、と捉えればある程度の同質性はありますが、記述対象のモデルが異なっており、違うものである、と筆者は考えています。Verilog HDLやVHDLは、論理回路における「信号」を、プログラミング言語風にあたかも変数のように扱っていますが、その結果(特にVerilog HDLで)いわゆる「意図しないラッチ」やシミュレーションと実機で(タイミング等の問題ではなく)論理的な動作が異なる、といった問題を発生させていると筆者は考えます。

1.4 SFLの歴史

背景理解のために紙幅を割きますが、2節に飛んでいただいても構いません。

PARTHENONのページのこちらの(http://www.parthenon-society.com/archive/NTT/html/pre_body.htm)「1.開発の背景・動機」も参考にしてください。

まず「DIPS」という背景があるわけですが、詳細はウィキペディア(http://ja.wikipedia.org/wiki/DIPS_(コンピュータ))を見てください。SFLの視点から見て重要なことは、DIPSプロジェクトにおいて、製造部門を持たない電電公社が「設計通り製造して問題のないコンピュータの設計」をメーカ各社に示す必要があった、という点です。

「1.開発の背景・動機」では「方式」という言葉が使われていますが、これはいわゆるアーキテクチャのことで、国内コンピュータメーカの古い資料を見ると(あるいは現代でも時々)使われています。フレデリック・ブルックスが定義した、命令セットアーキテクチャ(ISA)のような抽象度の高い意味のこともあれば、いわゆるマイクロアーキテクチャのような論理詳細設計を含む場合もあります。キャッシュメモリの選択などについて議論されていますから、ここではマイクロアーキテクチャを指しています。また「方式DA」の「DA」はDesign Automationで、今日ではElectronicを付けて、EDAという語の一部で使われていることが多いでしょう。

ともかくそういった理由から、物理的な詳細(LSIの製造プロセス)からは独立で、かつ論理的にはあいまいさを残さない所まで詰めた設計を行うための言語として設計されたSFLは、論理設計のための言語として過不足ないモデルと機能と備えている、と筆者は評価しています。

1.5 CSPとSFL

SFLはプロセス代数のCSP(Communicating Sequential Processes)がベースにある、と宣伝されていますが、チャネルに相当するような機能は無いので、プロセス代数的なことをするには設計者が作り込む必要があります。"par" や "alt" というキーワードは、CSPベースの並行言語Occamからと思われます。

2 SFLの仕様と処理系

2.1 SFLの仕様の入手

パルテノン研究会のクレジットのある「SFL仕様書」が http://www.algopro.co.jp/sfl/index.htm で配布されていますが、これは基本的には次の解説にある構文規則を構文図化したものです。以前に確認をいただいた際のコメントでは、解説にある構文規則を本則とする、ということのようでした。

以前にCQ出版からPARTHENONが書籍流通のパッケージソフトウェアとして商品化された際の添付冊子「はじめてのPARTHENON」の3章にあったSFLの解説を http://www.parthenon-society.com/archive/NTT/hajimete/3shou.htm で見ることができます。単行本『ULSIの効果的な設計法』にはより深い解説もありますが、SFLの言語仕様などはこちらのほうが比較的新しいので、この 3shou.htm をリファレンスマニュアル代わりにするのが良いでしょう。

筆者によるメモも参考にしてください。

2.2 SFLの処理系の入手

前述のようにパルテノン研究会からPARTHENONを入手する方法もありますが、このチュートリアルではNSL処理系のnslcoreを使います。nslcoreには入力をNSLからSFLに切り替えるオプションがあり、それを使います。

元々のSFLからの変更点や制限点などもあるのですが、それらについては逐次その場で説明します。

nslcoreは http://www.overtone.co.jp/binaries/ から、最新のベータ版 and・or 少し前の安定版をダウンロードできます。バリエーションは "i386 command line" がWindows用でコマンドだけが入ったもの、"i386-win32" がCygwin用でいくつかのコマンド用スクリプトも入ったもの、"i386-linux" と "x86_64-linux" はそれぞれのLinux用です。筆者の手元のFreeBSDではi386-linux版がFreeBSDのLinuxバイナリ実行機能で使えています。このチュートリアルではその他の環境も含め、64ビット(x86-64)か32ビット(IA-32)のLinuxを使うこととします(筆者の手元ではFreeBSDを使っていますが、問題は無いように配慮します)。

上記からの入手は不可能になりました。対応プラットフォーム等は不明ですが、こちら http://www.overtone.co.jp/support/downloads/ からダウンロードできる「NSL Coreインストーラープログラム(zip)」を使うか、http://www.ip-arch.jp/ の「もしくは、TRIAL SITEから、直接、コンパイルできます。」とある所にあるフォームを利用してください。

(そのまま使うと「教育用ライセンス」状態で動作します。教育用の範囲を越えるライセンスについては http://www.overtone.co.jp/support/license/ をご参照ください)

ホームディレクトリに適当にディレクトリを作り、その中に展開しておいてください。

2.3 シミュレータについて

できればSFLレベルで論理シミュレーションが行えると良いのですが、PARTHENON付属のシミュレータSECONDSは慣れるまで少々とっつきづらく、nslcoreにはシミュレータが含まれていません。このチュートリアルでは、nslcoreによりVerilog HDLに変換した後、Icarus Verilogでシミュレーションを実行することにします。

2.4 nslcoreのプログラムとオプション

nslcoreには、プリプロセッサnslpp.exeと、コンパイラ(合成系)nslc.exeという2個のプログラムが入っています。

nslppのオプションは -h オプションで確認できます(以下、どちらも i386-linux-20140921 版)。

> $NSL_CORE/nslpp.exe -h
usage: nslpp {options} filename or -
option: 
	-nest_comment: comment nesting mode 
	-v: verbose mode 
	-d: debug mode 

コメントのネストはSFLの仕様では許されていますが、デフォルトでは無効になっているようにオプションを付けないと使えませんし、おすすめしません。

/*
 * コメント
 * /* ネストしたコメント */ ← ネストしない場合は、ここでコメント終了
 * コメント
 */ ← ネストが許される場合はここまでコメント

このようにコメントのネストが許されると、まとまったコードをコメントアウトする時、その内側にコメントがある、という場合に便利なのですが、nslppでは // で一行コメントが書けますので、そちらを積極的に使うようにすれば良いでしょう。

またnslppでは、コメントの部分は全く無かったことになるという(標準化以前のC言語処理系のcppによく見られたのと同じ)仕様のため foo/* */bar のようにコメントの前後を詰めて書くと foobar のように一つのトークンになってしまいますので注意してください(マクロで複数の引数を連結する裏技にも使えます)。これは、構文図が描かれているPDFのほうの「SFL仕様書」に書かれているコメントの仕様とは異なっており、HTMLの 3shou_15.htm に書かれている仕様と一致しています。

他の2つのオプションも使う必要はまず無いでしょう。nslppへの入力はコマンドライン引数でパスを指定するか、引数に無ければ標準入力を読み込みます。プリプロセス処理の結果は標準出力に出力されます。その他のメッセージやエラーメッセージは標準エラー出力に出力されます。nslppは、SFLとNSLの両方のプリプロセッサ指令に対応しています。

プリプロセッサについての詳細は、必要な都度それぞれ説明します。

nslcのオプションは -v オプションで確認できます。

> $NSL_CORE/nslc.exe -v
OPTIONS:
  INPUT CONTROLS
	-sfl	Input file is SFL format
	-sfl_only	Input file is SFL format
	-f input_file_name	Designate input file
	-canreadoutput	Allow to read output signal in source code
  OUTPUT CONTROLS
	-o output_file_name	Designate output file or directory
	-split	Make a separate file for each module.
	-vhdl	Output VHDL file
	-vasy	Alliance Vasy format VHDL
	-ieee	Output VHDL only with std_logic_1164 and numeric_std.
	-systemc	Output SystemC file
	-verisim	Output VerilogHDL simulation skelton
	-verisim2	Output VerilogHDL simulation skelton with VerilogHDL
	-scsim	Output SystemC simulation skelton
	-scsim2	Output SystemC simulation skelton with SystemC
	-sc_ext_cpp	Use .cpp as SystemC extension(split only)
	-sc_bool	SystemC use bool for 1bit wire
	-sc_split_header	SystemC split header file as .sh
	-sc_trace	SystemC sc_trace turn on
	-sc_trace_depth	SystemC sc_trace traverse level
	-sc_output_lv	Use sc_lv/sc_logic for output terminals
	-target target_name	Simulation target module
	-test_bench test_bench_name	Simulation test bench module
	-default_nettype type_name	Simulation default type
	-p	Add prefix to signal names
	-clock_name clock_name	Change generated clock name
	-reset_name reset_name	Change generated clock name
	-psddly	Add psudo delay for VerilogHDL ASSIGN statement
	-neg_clk	Use negative edge clock
	-neg_res	Use negative edge reset
	-sync_res	Use syncronous reset
	-und {0|1|u|x|z}	Designate signal value for undriven signal
	-start_init num	Designate start clock of _init block
  OPTIMIZE CONTROLS
	-sim	Simulation support mode
	-O0	Turn off optimize and Simulation mode
	-O1	Turn on opt_sel and opt_reg
	-O2	Turn on opt_sel,opt_reg,opt_vhdl,gray,scond
	-O	Same as O2
	-opt_sel	Generate AND/OR type selector
	-opt_reg	Generate AND/OR type selecting register
	-opt_vhdl	Use AND/OR even for VHDL output
	-opt_out_dontcare	output don't care optimize
	-gray	Use Gray code for state encoding
	-scond	Simplify condition signals
	-verbose	Check for more warnings
	-check_hiz	Check for internal use of Hi-Z signal
	-regreport	Report used regs and mem
	-xref	Report associated line information

入力にSFLを指定するオプションは -sfl_only です。-sfl オプションとの違いについての詳細は不明です(問い合わせましたが、教えていただけませんでした)。

-f と -o オプションで入力と出力を指定できますが、指定しない場合はデフォルトで標準入出力になります。

その他のオプションは必要になった際に説明します。

3 SFLひとめぐり

SFLの基本的な機能を、ごく簡単な回路記述、少し複雑な組合せ論理回路、基礎的な順序論理回路の順で、一通りざっと見てゆきます。

3.1 シェルスクリプトとMakefile

作業の効率化のため、コンパイラドライバ(C言語のcc)に相当するシェルスクリプト nc.sh と make を使います。

また、このチュートリアルでは使いませんが、command line版以外のnslcoreにはnsl2vlといったような名前の既製スクリプトが入っていますので、それらも参考にすると良いでしょう。

シェルスクリプト nc.sh と Makefile は、このチュートリアルのGitHubプロジェクトの中のutilsディレクトリに置いてあります( https://github.com/metanest/sfl_tutorial/tree/master/utils )。また、チュートリアルの各サンプルのディレクトリに、それぞれのサンプル用として置いておきます。どちらも分量はそう少なくもありませんが( nc.sh が約100行、Makefile が約50行)そんなに大袈裟なことやトリッキーなことはしていませんので、順々に見てゆけば何をやっているかは分かると思います。

3.2 Icarus VerilogとGTKWave

このチュートリアルではIcarus Verilogでシミュレーションの生成と実行を行い、GTKWaveでシミュレーション結果を確認しますので、それらについても準備してください。それぞれの環境にパッケージで入れれば良いでしょう。

3.3 初めてのSFL記述

いよいよ最初のSFL記述です。ディレクトリ sample_1_1 の中の andor.sfh と andor.sfl を見てください。

==> andor.sfh <==
// vim:set ft=sfl:
declare andor {
    input i1, i2, i3;
    output o1, o2;
}

==> andor.sfl <==
// vim:set ft=sfl:
%i "andor.sfh"
module andor {
    input i1, i2, i3;
    output o1, o2;

    par {
        o1 = i1 & i2;
        o2 = i2 | i3;
    }
}

シミュレーション記述の都合で、ファイルを宣言用と定義用に分けています。宣言のファイル(ヘッダファイル)の拡張子を .sfh とする慣用は定着したものではありませんが、NSL の公式サンプルに見られる .nsh からの類推です。SFLの古いサンプルには .h をつかっているものが見られますが、C言語用ヘッダファイルと被るので避けました。%i はSFLでインクルードを指示するプリプロセッサ指令です。

また、先頭行にvimのmodelineでファイルタイプの指定がありますが、これは筆者の環境で誤認が起きることがあるので、それを防ぐために入れたものです。

以下、サンプルのモジュールの宣言(declaration)とモジュールの定義を詳細に見てゆきます。

declare モジュール名 { ... } のようにしてモジュールを宣言します。SFLでは定義の前、あるいはサブモジュールとして使う(後述)前に宣言が必要です。

モジュールの宣言では、入出力の端子を記述します。ここでは、データ入力端子 i1, i2, i3 を input i1, i2, i3; で、データ出力端子 o1, o2 をoutput o1, o2; のようにして記述しています。端子名には、任意の、名前として使える識別子が使えます。input などの、言語に使うキーワードや、"p_reset" などの予約されている名前(合成後の回路で特別の意味を持ちます)は、ユーザーの記述中の名前として使えません。ビット幅の指定が付いていないので、端子の(信号の)ビット幅は 1 ビットになります。ここではサンプルなので単純で抽象的な名前を付けていますが、実際の設計では入出力には具体的なもっと長い名前を付けたほうが良いでしょう。

データ入力・データ出力の他に、双方向端子と制御端子がありますが、双方向端子についてはこのチュートリアルではあまり扱いません。制御端子はこの後で述べます。

続いて定義を見てゆきます。module モジュール名 { ... } のようにしてモジュールを定義します。Javaなど最近のプログラミング言語では、いちいち事前に宣言を必要としませんが、SFLは基本設計が古く、構文解析を簡単にするために多くの要素に事前の宣言が必要となっています。

モジュールの定義の最初に、入出力端子の記述があります。モジュールの宣言にあるものと全く同じで全く冗長ですが、SFLの仕様では必要ということになっているので、ここでは記述しました

NSLではこのような冗長な記述は不要になっています(nslcoreの最近のバージョンでは記述すると構文エラーになります)。nslcoreではSFLでも不要なので、このチュートリアルでも次からは省略します。

par { ... } が、動作の記述です。NSLでは「動作」ではなく「アクション」と呼んでいます。ここで記述しているのは、(モジュールの)「共通動作」というもので、モジュールにおいて常に有効になっている動作を記述したものです。SFLでは単一の動作と、複数の動作を並列・選択するものとを明示する言語設計のため、ここでは複数(2つ)の動作を par(parallel、並列の意)という記述でまとめています。この "par" という名前はOccamという並行言語からのものと思われます。NSLではparというキーワードは無くなり、並列の記述は波カッコだけになりました。また共通動作のような場所では明示的に波カッコを付けなくても、NSLでは複数の記述があれば並列動作になります。

具体的には o1 = i1 & i2;o2 = i2 | i3; という記述で、それぞれ論理積と論理和の式の値を出力端子に出力する、という動作です。つまり、次のような組合せ論理回路を記述しています。

SFLやNSLのインデントやスペーシングのスタイルには、まだ確立したコンベンションは無いようです。このチュートリアルのサンプルファイルでは、C言語のK&Rスタイルに似た空白の入れ方をし、インデントは空白4個を使っています。同じ規則にする必要はありませんが、スタイルは一貫させるようにしてください。

さて、この論理回路の記述から合成を行ってシミュレーションを実行したいわけですが、SFLではそういったことの記述は言語の外の環境で行うという考えになっています。NSLにはシミュレーションのための記述もあるので、ここではNSLからSFLのモジュールを使うようにして、シミュレーションベンチを作成してあります。

ckt.nsl を見てください(cktはcircuitの意)。

// vim:set ft=nsl:
#include "andor.sfh"
declare ckt simulation {
    output o1, o2;
}
module ckt {
    reg count[6] = 0o00;
    reg running = 0b0;

    andor andor1;

    _init {
        running := 0b1;
    }

    any {
        (running & ~count[5]) : {
            count := count + 0o01;

            andor1.i1 = count[0];
            andor1.i2 = count[1];
            andor1.i3 = count[2];
            o1 = andor1.o1;
            o2 = andor1.o2;
        }
        (running & count[5]) : { _finish("simulation finished"); }
    }
}

詳細にはここでは踏込みませんが、SFLとだいたいは似ているので、i1, i2, i3 と o1, o2 を設定している、主要な部分はなんとなくつかめると思います。_init_finish がシミュレーション関係の記述で、シミュレーション開始時に 1 回だけ有効になる記述と、シミュレーションを終了させるための記述です。_init が有効になるタイミングは Makefile 中の nslc へのオプション -start_init 100 で制御しています。

makeを実行すれば、SFLからの合成・シミュレーションの生成・シミュレーションの実行が全て実行されるはずです。

次のように表示されれば、全て問題なく実行されています(細かい所は違うかもしれません)。もしどこか変なようであれば、筆者まで連絡してください。なお、標準出力と標準エラー出力が混じっています。

$ make
./nc.sh -sfl_only -f andor.sfl -sim > andor.v || ( stat=$? ; /bin/rm andor.v ; exit $stat )
NSL CORE (version=20140921)
 Copyright (c) 2002-2014 IP ARCH, Inc. Naohiko Shimizu
 All rights reserved.
No License file found
Licensed to evaluation user without license file.
You are allowed to compile up to 2000 lines or statements
 You are limited EVALUATION USER


./nc.sh -f ckt.nsl -verisim2 -target ckt -start_init 100 -sim > ckt.v || ( stat=$? ; /bin/rm ckt.v ; exit $stat )
NSL CORE (version=20140921)
 Copyright (c) 2002-2014 IP ARCH, Inc. Naohiko Shimizu
 All rights reserved.
No License file found
Licensed to evaluation user without license file.
You are allowed to compile up to 2000 lines or statements
 You are limited EVALUATION USER


iverilog -o ckt.vvp andor.v ckt.v
vvp ckt.vvp
VCD info: dumpfile ckt.vcd opened for output.
simulation finished

やり直したい場合はmake cleanで中間ファイルなどは全て削除されます。

シミュレーション結果は ckt.vcd というファイルに保存されていますので、それをGTKWaveで開きます。

$ gtkwave ckt.vcd

まず左上部分のツリー表示で分岐の部分をクリックして、回路のツリーを展開します(もしツリー表示自体が見えていない場合は、「SST」と書かれている部分の左の三角をクリックしてください)。

上(↑)の画像は全て展開したところです。ここで、作成したモジュールのインスタンス名である "andor1" が表示されている所をクリックし選択状態にすると、モジュールに存在する端子名が表示されますので、それをクリック操作(コントロールを押しながらで複数選択できますが、シフトを押しながらクリックを使うといっぺんに選択できます)で全部選択します。

上(↑)の画像は全て選択したところです。ここでマウスカーソルを置いてあるAppendボタン、またはInsertボタンをクリックすると、右の信号の内容を表示する部分に選択した端子の信号が追加されます。

上(↑)の画像は信号が追加されて表示されたところです(皆さんの場合の見え方は違うかもしれません)。左上の赤く丸で囲んだ部分のズーム操作ボタンや、波形が表示されている部分の下のスクロールバーなどを操作して、見たい部分が見やすく表示されるように表示を変更してください。-start_init 100 という設定のために、約 200ns(ナノ秒) の所から動作が始まっています。波形の上に表示されている経過時間を参考にしてください。

上(↑)の画像は動作がよくわかるように表示させたところです。o1 が i1 と i2 の論理積、o2 が i2 と i3 の論理和になっているのがよくわかると思います。

以降このチュートリアルでは注意が必要と思われる点以外は、合成やシミュレーションやGTKWaveの操作については、細かい説明は行いません。またGTKWaveについては、オシロスコープと同様な動作確認に必携のツールですから、いろいろ操作してみて必ず慣れるようにしてください。

3.4 少し複雑な組合せ論理回路

次に、少し複雑な組合せ論理回路を記述してみます。数値の2進表現の信号から「日」の字型の7セグメント表示器を駆動する信号を作るためのデコーダは、積和標準型で設計していわゆる論理の圧縮(簡単化)を行う練習問題の題材に良く使われますが、ここではそのまま動作を全部記述してしまい、最適化は合成系に任せてしまうことにします。

(画像はWikimedia Commonsの "File:7 Segment LED.jpg" から)

ある製品のデータシートで、次のように上のセグメントから時計回りに a〜g の記号を付けていましたので、ここでもセグメントにこのように名前を付けることにします。

SFL記述を見てみましょう。ディレクトリ sample_1_2 の中の decoder.sfh と decoder.sfl を見てください。

==> decoder.sfh <==
// vim:set ft=sfl:
declare decoder {
#ifdef HDLANG_SFL
    input hex<4>;
    output led7<7>;
#endif
#ifdef HDLANG_NSL
    input hex[4];
    output led7[7];
#endif
}

==> decoder.sfl <==
// vim:set ft=sfl:
%d HDLANG_SFL
%i "decoder.sfh"
module decoder {
    any {                  // abcdefg
        hex == 0x0 : led7 = 0b1111110;
        hex == 0x1 : led7 = 0b0110000;
        hex == 0x2 : led7 = 0b1101101;
        hex == 0x3 : led7 = 0b1111001;
        hex == 0x4 : led7 = 0b0110011;
        hex == 0x5 : led7 = 0b1011011;
        hex == 0x6 : led7 = 0b1011111;
        hex == 0x7 : led7 = 0b1110000;
        hex == 0x8 : led7 = 0b1111111;
        hex == 0x9 : led7 = 0b1111011;
        hex == 0xa : led7 = 0b1110111;
        hex == 0xb : led7 = 0b0011111;
        hex == 0xc : led7 = 0b0001101;
        hex == 0xd : led7 = 0b0111101;
        hex == 0xe : led7 = 0b1001111;
        hex == 0xf : led7 = 0b1000111;
    }
}

端子のビット幅の表記法が、SFLでは <幅> 、NSLでは [幅] といったように違うため、プリプロセッサ指令を使って記述を分けています。%d はSFLのマクロ定義のプリプロセッサ指令です。NSLのプリプロセッサ指令では、C言語のそれとだいたい同じ指令が使えます。nslcoreではNSLの記法でSFLとしても通るのですが、これについてはSFLの記法とNSLの記法を使い分けます(NSLのマニュアルには「束線」(タバセン、と読むようです)という表現がありますが、ローカルな表現のようです)。

続いて本体ですが、any { ... } という動作の記述があります。any では par のように単に並列動作ではなく、「条件」が付いている「条件付き動作」を記述します。条件付き動作はコロン( ":" )で区切られていて、先頭側であるコロンの左側にある「式」が条件、コロンの右側にある「動作」が条件が真の場合(条件の部分の式による信号の値が真の場合)に有効になる動作です。いくつかのプログラミング言語にある条件の記述「ガード」と同様のものです。ここでは各の動作は 1 個のため、par { ... } で囲ってはいません。

複数の条件が同時に 1 になっている場合は、any ではその全てが並列に動作しますが、この場合のように全ての条件が排他的でもかまいません。また、この例では全ての場合をカバーしていますが、必ずしもそうしなくても構いません。どの条件も 1 にならなかった場合は、単に何の動作も起きないだけです。

論理圧縮の演習などでは、10以降の場合をdon't care(どんな値になってもよい)として扱うことがありますが、SFLにはそのような不定値(Verilog HDLの 'x' のような値)を明示的に表現する方法が無いので、ここでは16進で F までを表示するように設計しました。

(SFLでの不定値について)SFLでは、何らかの動作によって駆動されていない状態のデータ端子は不定値になっています。nslcoreではオプション -und の説明で「-und {0|1|u|x|z} Designate signal value for undriven signal」となっているように、合成時のオプションで、合成先の言語におけるどの表現にマッピングするかを指定できます。このチュートリアルの次で説明する制御端子は不定値になることはありません。

シミュレーションベンチの ckt.nsl は次のようになります。

// vim:set ft=nsl:
#define HDLANG_NSL
#include "decoder.sfh"
declare ckt simulation {
    output led7[7];
}
module ckt {
    reg count[5] = 0b00000;
    reg running = 0b0;

    decoder decoder1;

    _init {
        running := 0b1;
    }

    any {
        (running & ~count[4]) : {
            count := count + 0b00001;

            decoder1.hex = count[3:0];
            led7 = decoder1.led7;
        }
        (running & count[4]) : { _finish("simulation finished"); }
    }
}

シミュレーション結果は次のようになります。

(幅のある信号は波形の左の信号名の所をダブルクリックすると、1 ビット毎の表現に展開されます)

1の所では2個しか点灯しないことや8の所で全部点灯していることなどはすぐわかると思います。

3.5 簡単な順序論理回路

状態のある回路というとLEDを明滅させるのが電子工作の定番ですが、HDLではちょっと面白くありません。ここではこの節の最後の例として、モールス符号の A のパターンを生成する回路を作ってみます。

モールス符号で A は「トン ツー」つまり短点1個と長点1個です。長点の長さは短点3個ぶん、点と点の間隔は短点1個ぶん、符号と符号の間も短点3個ぶん、と決まっていますから、

___-_---___-_---___-_---___

というような信号を作ればいいわけです。

sample_1_3 の morse.sfh と morse.sfl を見てみましょう。

==> morse.sfh <==
// vim:set ft=sfl:
declare morse {
    output o;
#ifdef HDLANG_SFL
    instrin enable;
    instrout oe;
#endif
#ifdef HDLANG_NSL
    func_in enable;
    func_out oe;
#endif
}

==> morse.sfl <==
// vim:set ft=sfl:
%d HDLANG_SFL
%i "morse.sfh"
module morse {
    reg_wr signal;
    reg_ws s1;
    reg_wr s2, s3, s4, s5;
    reg_wr s6, s7, s8;

    any {
        0b1 : o = signal;

        ^enable : par {
            s1 := 0b1;
            s2 := 0b0; s3 := 0b0; s4 := 0b0; s5 := 0b0;
            s6 := 0b0; s7 := 0b0; s8 := 0b0;
        }
    }

    instruct enable any {
        0b1 : oe();
        s1 : par { signal := 0b1; s1 := 0b0; s2 := 0b1; }
        s2 : par { signal := 0b0; s2 := 0b0; s3 := 0b1; }
        s3 : par { signal := 0b1; s3 := 0b0; s4 := 0b1; }
        s4 : par { s4 := 0b0; s5 := 0b1; }
        s5 : par { s5 := 0b0; s6 := 0b1; }
        s6 : par { signal := 0b0; s6 := 0b0; s7 := 0b1; }
        s7 : par { s7 := 0b0; s8 := 0b1; }
        s8 : par { s8 := 0b0; s1 := 0b1; }
    }
}

まず、宣言の instrin と instrout は、制御入力端子と制御出力端子です。制御端子というのは、データではなく制御のための信号の端子、という考え方で、信号をデータと制御の2種類に分けているのはSFLの大きな特徴です。

制御端子には次のような特徴があります。

また、NSLでは func_in と func_out というキーワードですので、ヘッダファイルでは書き分けをしてあります。

続いて定義の中を見てゆきます。reg というのが、状態を持つようなレジスタの定義で、要領は端子などと同じです。reg_wr で定義したレジスタは回路のリセット時に 0 にリセットされ、reg_ws で定義したレジスタは回路のリセット時に 1 にセットされます。

ここでは初期状態の s1 レジスタだけを reg_ws で定義し、他は reg_wr で定義しています。

続いてモジュールの共通動作です。any を使っていますが、その中で最初に 0b1 : のようにして無条件で動くような動作を記述しています。これは par { ... any { ... }} のようにネストを深くしないためのイディオムです。

signal というレジスタを o という出力に無条件で直結するような記述になっています。これは、SFLにおいて、複数クロックにわたって安定な出力を得るためのイディオムです。SFLでは、クロック同期設計が前提なので、いわゆるハザード(次の図を参照)の発生を防ぐことが困難なため、このようにします。

ハザードの一例です(他にもいろいろな種類のハザードがあります)。このような回路は、論理的には常に 0 を出力しますが、現実的には遅れ(遅延)によりヒゲのような信号が発生します。この図では配線遅延は無視できるものとして、NOTゲートでΔのゲート遅延が起きるものとしたため、幅Δの短いパルス(ハザード)が発生しています。

非同期設計ではこのようなハザードによる誤動作が起きないよう対策を凝らすわけですが、SFLではクロック同期設計を前提として、ハザード等は出るかもしれないが、そういった過渡的な現象が全て収まって安定した後に、フリップフロップにおける信号の取り込みが行われる、という回路が合成されることになっています。そのため、論理の組合せでハザードが発生しないような回路を設計者が明示的に記述することは困難です。ですので前述のようなイディオムにより、レジスタを無条件で出力に直結するようにして、複数クロックにわたって安定な出力を得るようにします。

次の ^enable が条件になっている記述ですが、これは制御入力 enable がオフの時の記述で、イネーブルでない時にはシーケンサの状態レジスタである s1〜s8 を初期状態にする、というものです。"^" はビットの反転の演算子でC言語などと違ってますので注意してください。端子への出力には "=" を使いましたが、レジスタへの書き込みは ":=" を使います。端子の値は直ちに変わりますが、レジスタへの書き込みがレジスタの値に反映するのは次のクロックからです。

以上でモジュールの共通動作は終わりです。

次に、instruct enable any { ... } という記述があります。これは「制御端子による動作」の記述で、制御端子が起動されている場合の動作を記述する部分です(any だけでなく、任意の動作を記述できます)。

最初の無条件の oe(); は制御出力端子の起動で、このように名前の後に "()" を付けて記述します。カッコの中に信号を渡す端子を指定することもできますが、この節ではそれには触れません。その後、s1 から s8 まで、それぞれのレジスタが 1 の時の動作を記述していますが、必要な状態の数だけレジスタを用意し、そのうちのどれか 1 つだけのレジスタが 1 になる、いわゆる「ワンホット型シーケンサ」を記述しています。1 行に詰め込んでしまっていますが、このような短い記述であればこのように書いたほうが見通しが良くなるでしょう。

シミュレーションベンチ ckt.nsl は、以前と大きく違うところはありませんから省略します。Icarus Verilogの実行時に、

warning: Found both default and `timescale based delays. Use
         -Wtimescale to find the module(s) with no `timescale.

という警告が出ます(出るかもしれません)。これは、

`timescale	1ns / 1ns

という記述が ckt.v にはあって morse.v には無いためのようですが、nslcoreがどういう場合にこれを出力したりしなかったりするのがよくわかっていないため、現状では対策していません。結果を見る限りでは問題はないようなので、無視しています。

シミュレーションの結果は次のようになります。

ワンホット型シーケンサの動作がよくわかると思います。

4 SFLの設計モデル

SFLはハードウェア記述言語であり、論理回路(論理回路として働く電子回路)を記述する言語ですが、その記述対象をどのようにモデル化して記述しているのかを述べます。

4.1 離散時間

静的な(時間による変化を考慮しない)論理回路もありますが、一般には時間が経過して何らかの変化があるような働きをする回路が多いでしょう。

Verilog HDLやVHDLは、任意の時間経過を表現できる仕様になっています。実ハードウェアのゲートや配線の遅延を正確にモデリングするにはそうでなければなりませんが、論理設計に対象を絞るならばオーバースペックとも言えます。

SFL(およびNSL)は、クロック同期式設計・完全同期式設計を前提として、時間は t, t+1, t+2, といったように、離散的なモデルとなっています(システムリセットによる全リセットのみ非同期もありうる)。非同期式の回路ともし同居させたい場合は、SFL以外で記述した別モジュールとしなければならないのが不利な点ですが、SFLで設計する部分は論理設計に集中できます。

実装を単一クロックとするかマスタースレーブラッチとするか、といったようなことは合成系に任せることになります。単一クロック設計においては配線遅延によるクロックスキューによるホールド時間違反に注意する必要がありますが、SFLではそういった過渡的な現象は扱わない(理想的な動作をする前提とする)ので、ホールド時間を別のクロック信号で保証しているマスタースレーブラッチをモデルとして考えている、と言ってもよいでしょう(たいていの場合は単一クロックの回路を合成するものと思いますが)。

時間モデルについては、あとでレジスタについて解説する時、また触れます。

4.2 論理回路

SFLの記述対象はディジタルな論理回路です。時間について離散的であると前節で述べましたが、入出力などの端子の状態も、基本的には0か1、偽か真で(0が偽、1が真)アナログな信号は扱いません。電気的なLowとHighをどちらにあてはめるか(正論理・負論理)については、SFL内ではなく外部で決めるものとしています(ですので、このチュートリアル中でも、HighとLowという表現はできるかぎり避けます)。

電気的にはバスなどには、HighとLowの他にハイインピーダンス状態(しばしば "z" で表現される)があり、他に設計上は、どんな値になっても良い(unspecified)、あるいはどんな値でも気にしない(don't care)という値(しばしば "u" で表現される)がありますが、これらもSFLでは陽に扱うことはありません。出力されていない状態のデータ端子はこのどちらかになりますが、どちらになるかは決められていませんし、そのような状態の端子の値を式中で参照してはいけません(nslcでは -und オプションで指定)。なお、本来の現行のSFLでは sel(または sel_v )と bus(または bus_v )としてデータ内部端子にこれらの区別がありますが、これはNTTにおいて設計対象だったLSIの都合のなごりだそうです。古いSFLでは term と tmp(この 2 つの違いは、合成時に観測可能な端子として残すか否か)、NSLでは wire という一種類だけになっています。このチュートリアルは基本的にはSFLで記述しますが、データ内部端子はNSL流に wire で定義します。

4.3 組合せ回路と順序回路

論理回路には一般に、組合せ回路(組合せ論理回路)と順序回路(順序論理回路)があります。組合せ回路は、その瞬間における入力のみから出力が決定される論理回路で、ループを含みません。順序回路はフリップフロップのようなループするプリミティブを含んでいて、以前の状態が出力に影響します。

SFLでループするような接続を記述して、ループを含む(その動作が順序回路のようになり得る)回路を作ることはできません。「禁止されている」という意味に考えてください。PARTHENONでは処理系がスタックオーバーフローを起こして異常終了するようです。nslcではチェックされてはねられるようですが、-verbose オプションが必要かもしれません。

順序回路を記述するには、プリミティブである reg で定義するレジスタ(あるいはメモリ)を利用します。その他の数式のような記述は全て、組合せ回路の記述となります。

4.4 ムーア機械とミーリ機械

オートマトン理論に、現在の状態のみから出力が決定するシステムを指すムーア機械(Moore machine)と、現在の状態と入力により出力が決定する機械を指すミーリ機械(Mealy machine、どちらも人名が由来ですので先頭は大文字に)という言葉がありますが、同期式で順序回路を含む論理回路のモデルを考える上でも、これらは有用ですので、簡単に説明します。

(以下の図は『VHDLデジタル回路設計 標準講座』の図5.2を参考にしました)

普通は状態遷移図のようなもっと抽象的なモデルで議論しますが、ここではそれぞれの論理回路による実装がどのようになるかを示しました。

ここで、組合せ論理の出力は、入力の変化に応じて即時に変化します。一方、レジスタの出力は、クロックに合わせて変化します。クロックによって区切られる時刻を順番に T-1, T, T+1 とすると、時刻 T の時にレジスタが出力しているのは、時刻 T-1 の時にレジスタの入力にあった値です。時刻 T の時にレジスタの入力にある値は、時刻 T+1 の時にレジスタから出力されます。SFL では、組合せ論理の内部における端子への出力には " = " を、レジスタへの出力には " := " を、と、明確に書き分けますが、これは、前者はその値がそのクロックのうちに伝搬し、後者は次のクロックで出力が変化する、という区別を反映したものです。

実際の論理回路の設計では、必ずしもこのようにレジスタに出力する組合せ論理 (next) と、外部に出力する組合せ論理 (out) というように分離する必然性はありませんが、どんな論理回路でも単純化するとこのようなモデルとしてとらえることができるということは片隅に置いておいてください(余談: ムーア機械とミーリ機械という言葉は、オートマトン理論や論理設計の基礎として教えられることが多いですが、授業の時間の都合などで省略する先生もあるようです。筆者が論理回路を教わったA先生は省略されていました)。

5 モジュール

ここから、SFLの具体的な記述の説明に入ります。

論理回路の設計に限りませんが、一般に設計というものにおいては、対象をある程度のまとまりに区分けして設計を行います。モジュール化と言いますが、SFLにおけるモジュール化の単位は「モジュール」です。C言語における関数プロトタイプ宣言と関数の定義のように、SFLのモジュールにはモジュールの宣言とモジュールの定義、それからサブモジュールの定義があります。

5.1 モジュールの宣言

モジュールの宣言とモジュールの定義は、SFLファイルのトップレベルに記述します。「モジュールの宣言」では、そのモジュールの入力や出力といったインタフェースを定義します。ファイル中でモジュールを定義あるいは利用するよりも前に、宣言が必要です。モジュールの宣言の例を示します。

// モジュールの宣言
declare adder_4 {
    input a<4>, b<4>;
    input c_i;
    output o<4>;
    output c_o;
}

モジュールの宣言は、キーワード declare(宣言の意)、モジュール名、"{" 外部(入出力)端子の宣言... "}" という構文です。名前の先頭には任意の英文字、2文字目以降には数字と下線( "_" )も使えますが、下線を2文字以上続けることはできません。なお、下線で始まる名前はVerilog HDLのシステムタスクなどに相当する機能をNSLで記述するために予約されています。"input" などのキーワードと同じ綴りは名前として使えませんが、その他にもいくつか予約されている名前があり、そういった名前は避ける必要があります。(なお、古いSFLサンプルで "if" という、キーワードと同じ綴りを名前に使っているものがありますが、現在はそういったことはできません。TODO: 別ファイルにまとめて、リンク。)

input でデータ入力端子、output でデータ出力端子を定義します。"<4>" といったような記述を付加して複数ビットの端子を示します。この値は十進法で書いてください(SFLでは、(プレフィックスのない)十進法による整数の記述は「数」、"0b" などで始まる 2or8or16 進法による整数の記述は「定数」という、別扱いのモノになります)。1を指定してもかまいませんが、普通は単に省略すれば1ビット幅になります。外部端子にはデータ端子の他に制御端子もありますが、制御端子についてはあとでまとめて扱います。

端子名(および幅)を、コンマで区切って複数記述することもできます。最後にはコンマを付けず、セミコロンを付けます。

bidirect でデータ双方向端子を定義することもでき、その端子はトライステートバッファ(その端子に向けた出力が無い時にはハイインピーダンス状態になる端子)になりますが、SFL・NSLでデータ双方向端子を利用するのはおすすめしません。FPGAなどでは、メーカーが提供しているツールのVerilog HDLで、メーカーが例示しているような記述により入力や出力などのインタフェースは記述するようにし、その内部モジュールをSFLやNSLで記述するようにするのが良いでしょう。

NSLでは、Verilog HDL に合わせた変更で、幅は <4> ではなく [4] のように書きます。また双方向端子のキーワードは bidirect ではなく inout ですが、qwerty 配列のキーボードでは input と inout はミスタイプしやすいので注意してください。

NSLでの機能拡張として、モジュール名の後・波カッコの前に、interface あるいは simulation という修飾を付けることができます。interface は、主にVerilog HDLなどで記述したモジュールとの接続のための機能で、この修飾が付いたモジュールではリセットとクロックである p_reset と m_clock 端子の扱いが、暗黙のうちに生成されるのが抑止されます。Verilog HDLで記述したモジュールをNSLから利用する場合に必要になります。また、interface で宣言したモジュールをNSLで定義する場合は、クロックとモジュールを明示的に記述します。( p_reset と m_clock という名前は nslc のオプションで変更できます)

simulation という修飾は、Verilog HDLにおけるシステムタスクに相当するシミュレーションのための記述を、モジュールの定義の中で行うことを示すもので、宣言自体には特に影響しませんが定義ではなく宣言の側に付けることになっています。なお、interface と simulation は意味的には直交しているはずですが、両方付けることはなぜかできないようです。

また昔のSFLでは、declare ではなく submod_type や submod_class というキーワードを使っていたようで、PARTHENONのバージョンによっては後方互換性があるようですが、現在まず使うことはないでしょう(サンプルなどでたまに見かけます)。一応予約語と同様に名前として使うのは避ける、ぐらいで良いでしょう。同じく昔のSFLでは、モジュールの定義の中に(ネストして)宣言が書けたようですが(PARTHENON付属のSFL2VHDLなどの説明でそれらしきことが少し触れられている)現在はできません。もし古いサンプルを処理系に掛けてエラーになる原因がそれだった場合は、単に外に出してしまえば解決します。

5.2 モジュールの定義

モジュールの論理回路としての動作を定義するのが、「モジュールの定義」です。あるモジュールの定義がファイル中に現れるよりも前に、そのモジュールは宣言されている必要があります。モジュールの定義の例を示します。

// モジュールの定義
module adder_4 {
    // 外部端子の定義 …… nslcoreでは省略できるので省略する

    // 内部端子(構成要素)の定義
    wire temp<5>;

    // 共通動作の定義
    par {
        temp = ((0b0 || a) + (0b0 || b)) + (0b0000 || c_i);
        o = temp<3:0>
        c_o = temp<4>
    }

    // その他の動作の定義
    ...etc...
}

モジュールの定義は、キーワード module、モジュール名、"{" 中身... "}" という構文です。nslcoreでは省略できるので省略することを強くおすすめしますが、最初に外部端子の定義を書く場合は宣言のそれと全く同じにすることが必要です。もし記述する場合はミスなどが無いよう、コピペ等で全く同じになるようにしましょう。

中身の記述についてはあとで説明しますが、内部端子などの構成要素、モジュールにおいて常に動作する部分である「共通動作」、その他の定義、という内容が大枠になります。

SFLの仕様では、回路の合成ができない(シミュレーションのみの)高水準の記述が許される circuit というキーワードを使う「機能回路の定義」というものもありますが、nslcore では使いません(同様の記述が全て普通のモジュールの定義で許されています)。また、古い仕様では circuit ではなく circuit_type あるいは circuit_class というキーワードで、機能回路については宣言と定義を兼ねていました(宣言のキーワード declare に対し、一般的な言語感覚では定義のキーワードは define となるはずですが、そうなっていないのはこのような歴史的事情によります)。

5.3 サブモジュールの定義

あるモジュールの中で、別のモジュールを使う場合、その「使われる」モジュールのことをSFLではサブモジュールと呼びます。Verilog HDLなどではインスタンシエート等と呼ばれているものと同じです。

たとえば alu というモジュールの定義の中で、サブモジュールとして、ここまでの例で使っている adder_4 を adder という名前で使う例は、次のようになります。

declare adder_4 {
    ……(略)……
}  // サブモジュールとして使う前に必ず宣言が必要

declare alu {
    ……(略)……
}  // 定義の前に必ず宣言が必要

module alu {
    // 外部端子の定義 …… nslcoreでは省略できるので省略する

    // 内部端子(構成要素)の定義
    wire temp<5>;
    adder_4 adder;  // 構成要素として、サブモジュールを定義

    // 共通動作の定義
    par {
        ……(略)……
    }

    // その他の動作の定義
    ...etc...
}

サブモジュールとして使う前にも必ず宣言が必要なので、それぞれのモジュールの定義を別のファイルに分ける場合は、宣言の記述が分散しないよう宣言はヘッダファイルに分けて、プリプロセッサ指令 %i で取り込むようにしたほうが良いでしょう。

なお、古いSFLでは、submod というキーワードが頭に付くという仕様でした。そのような文法のほうが構文解析は少し簡単です。古いサンプルなどでたまに見掛けます。

6 組合せ論理回路

順序回路(レジスタ)を含まない回路の実例で、具体的なSFLの記述を説明してゆきます。

6.1 例: 7セグメントデコーダ

まず、3.4節で紹介した7セグメントデコーダを、中身についてもう少し詳細に設計してみます。

モジュールの宣言は3節のものと同じです。

// vim:set ft=sfl:
declare decoder {
#ifdef HDLANG_SFL
    input hex<4>;
    output led7<7>;
#endif
#ifdef HDLANG_NSL
    input hex[4];
    output led7[7];
#endif
}

シミュレーションの記述のためにNSLからも使うため、両方に対応するようプリプロセッサ指令で振り分けています。端子の幅は、16進ひとケタぶんの入力が4ビット、出力は7セグメント表示ですので7ビットです。

ここでは、3節で紹介したよりも、もう少し詳細に内容を設計(記述)してみます。7セグメントデコーダの動作を、次の2段で実装することにします。

モジュールの定義の、まず前半を示します。

// vim:set ft=sfl:
%d HDLANG_SFL
%i "decoder.sfh"
module decoder {
    wire temp<16>;

    par {
        temp =
            (hex<3> & hex<2> & hex<1> & hex<0>) ||  // 15
            (hex<3> & hex<2> & hex<1> & ^hex<0>) ||  // 14
            (hex<3> & hex<2> & ^hex<1> & hex<0>) ||  // 13
            (hex<3> & hex<2> & ^hex<1> & ^hex<0>) ||  // 12
            (hex<3> & ^hex<2> & hex<1> & hex<0>) ||  // 11
            (hex<3> & ^hex<2> & hex<1> & ^hex<0>) ||  // 10
            (hex<3> & ^hex<2> & ^hex<1> & hex<0>) ||  // 9
            (hex<3> & ^hex<2> & ^hex<1> & ^hex<0>) ||  // 8
            (^hex<3> & hex<2> & hex<1> & hex<0>) ||  // 7
            (^hex<3> & hex<2> & hex<1> & ^hex<0>) ||  // 6
            (^hex<3> & hex<2> & ^hex<1> & hex<0>) ||  // 5
            (^hex<3> & hex<2> & ^hex<1> & ^hex<0>) ||  // 4
            (^hex<3> & ^hex<2> & hex<1> & hex<0>) ||  // 3
            (^hex<3> & ^hex<2> & hex<1> & ^hex<0>) ||  // 2
            (^hex<3> & ^hex<2> & ^hex<1> & hex<0>) ||  // 1
            (^hex<3> & ^hex<2> & ^hex<1> & ^hex<0>);  // 0

        ……続く……

%d と %i はプリプロセッサ指令です。SFLとNSLの振り分けのためのマクロを定義してから、ヘッダファイルにある宣言をインクルードしています。

続いてモジュールの定義の最初で wire temp<16>; という記述により、16ビットの中間結果のための端子を定義しています。ここで、wire というキーワードの綴りはVerilog HDL由来ですが(と思われますが)、Verilog HDLの場合と違い、ラッチ(レジスタ)が合成されることはSFLでは絶対にありません。その端子に向けて出力が行われていない時には、単に不定値かハイインピーダンス状態になります。

par { で始まっているのがモジュールの共通動作で、この回路の場合のように場合分けなどもなく全部接続してしまうような回路では、この部分の par の中に全て記述してしまいます。Verilog HDLでのassignでの記述に相当します。

ここでは1本1本別の端子にするのではなく、ビット幅のある端子としたので、連結の演算子 "||" でビット連結をしています(C言語の論理和ではないので注意してください)。

幅のある端子をビット毎に分解するには "<ビット位置>" としてアクセスします。LSBの方から順に 0, 1, 2,... ですので、MSBが (ビット幅 - 1) となることなどに注意してください。なお、出力の "=" の左辺側でビットの切り出しはできません。ですので、ビットの一部にだけ値を出力したいなどの場合は、別々の端子としてください。また "<x>" といったようにして信号によってどれかのビットを取り出す、といったこともSFLではできません(nslcoreでは、変換先のVerilog HDLで出来ることは出来るようにしているようなので、SFLではできないような記述もある程度は通るようですが)。

その他演算子の一覧は http://www.parthenon-society.com/archive/NTT/hajimete/3shou_12.htm にありますので、そちらを参照してください。一覧表が表3.4ですが、演算子に細かい優先順位がなく、3種類の優先度しかないことに注意してください。特にandとorが同じ優先度ですので、積和標準形の式などは必ず積をカッコで囲んで、たとえば (a & b)|(c & d)|(e & f) といったように書く必要があります。

ビット幅の一致については、ある程度ルーズなようでも通るようですが、基本的に必ず一致させるようにしたほうが良いでしょう。特に、ここでは出てきていませんが 0xff のような「定数」は、普通のプログラミング言語では一律32ビットなどなんらかの固定幅(intの幅)の整数ですが、SFLではその記述自身がたとえば 0xff は8ビット幅、0o123 は12ビット幅、といったように幅の情報も持っているということに注意してください(nslcoreでは実はある程度使えますが、SFLでは十進法は信号の値としては使えません)。

コンピュータプログラムと違い、このようにある程度大きな記述を一気に書くことも多いので、改行の入れかたなどは適宜工夫すると良いでしょう(ここでは演算子の直後で改行していますが、演算子の直前でも良いでしょう。SFLでは // による一行コメントなど以外は、改行の影響を受けません)。コンピュータプログラムならループ等ですっきりと記述したいようなところを、SFLでは力任せに記述しなければならないことも多いです(NSLでは「構造展開」という機能で、ある程度そういった記述ができます)。このチュートリアルでは扱いませんが m4 のようなマクロプロセッサの併用も検討すると良いかもしれません。

続いて後半です。表示したい7セグLEDには次のようにa〜gの記号が振ってあるものとします。aをビット6、gをビット0とします(逆のようですが、左から順にa〜gにするとこうなる)。

        ……承前……

        led7 =
            (temp<0> | temp<2> | temp<3> | temp<5> | temp<6> | temp<7> |
             temp<8> | temp<9> | temp<10> | temp<12> | temp<14> | temp<15>) ||  // a
            (temp<0> | temp<1> | temp<2> | temp<3> | temp<4> |
             temp<7> | temp<8> | temp<9> | temp<10> | temp<13>) ||  // b
            (temp<0> | temp<1> | temp<3> | temp<4> | temp<5> | temp<6> |
             temp<7> | temp<8> | temp<9> | temp<10> | temp<11> | temp<13>) ||  // c
            (temp<0> | temp<2> | temp<3> | temp<5> | temp<6> |
             temp<8> | temp<9> | temp<11> | temp<12> | temp<13> | temp<14>) ||  // d
            (temp<0> | temp<2> | temp<6> | temp<8> | temp<10> |
             temp<11> | temp<12> | temp<13> | temp<14> | temp<15>) ||  // e
            (temp<0> | temp<4> | temp<5> | temp<6> | temp<8> |
             temp<9> | temp<10> | temp<11> | temp<12> | temp<14> | temp<15>) ||  // f
            (temp<2> | temp<3> | temp<4> | temp<5> | temp<6> | temp<8> |
             temp<9> | temp<10> | temp<11> | temp<13> | temp<14> | temp<15>);  // g
    }  // par end
}  // module end

論理設計の演習などでは、カルノー地図(Karnaugh map)を書いたりクワイン-マクラスキー法(Quine-McCluskey method)のプログラムに掛けたりして、簡単化を行うわけですが、ここでは合成系による最適化に任せてしまうことにします。

3節のサンプルと同じようにまとめたものが、ディレクトリ sample_6_1 の中にありますので、試してみてください。

これまでのサンプルでは、データの流れの順に記述していますが、必ずしもデータの流れの順に記述する必要はありません。par の波カッコの中に羅列した「動作」は、全てパラレル(並列)に動作します。共通動作と、制御端子に対応した動作のように、ソースコード上で出現順が前後するかもしれませんが、影響のしかた(されかた)はどちらにある動作も対等です。ですが、論理的にループするような記述は無いはずですから(できない)、一つの par の中などであまりに直感的でない順序で記述するのはやめておきましょう。

他の参考例として、NSLですが、筆者の作成した74181 ALUにリンクしておきます http://github.com/metanest/npc74181

6.2 any

場合分けのような動作について説明します。

これまでのサンプルでも使っている par は、羅列した動作が、並列に無条件で全て動作するものでした。any と alt は条件として式を付けることで、その条件が真の時にのみ動作する、というような動作になります。関数型や論理型、あるいは並行型のプログラミング言語にある「ガード」と(ほぼ)同じものです。

まず、any から説明します。any の構文は、

any {
    条件1 : 動作1
    条件2 : 動作2
      :
      :
    [else : 動作n]
}

のように、par の場合の各動作の前に「条件 : 」を付けたようなものになります。ここで、条件には幅が 1 ビットになる任意の式を書きます(比較のような条件を表現する式でなければならないわけではありません。たとえば a<4> と b<4> というような幅のある端子があるとすると "a == b" という式は、全ビットが等しければ真、そうでなければ偽という 1 ビットの真偽値になり、その 1 ビットの値が評価されます。式については少し後で詳説します)。また、NSLでは(SFLではできません)、"a < b" のような比較演算も書けますが(そのままVerilog HDLなどの比較式に変換される)、多ビットを数値的に比較するような規模の大きい回路が共用されずに合成されるでしょうから、控えめにしましょう。

any では、前置した条件が真になっている動作が複数ある場合、それらは全て動きます。真になる条件がひとつもなかった場合は、else を前置した動作があればそれが動きます。Verilog HDLの場合は意図しないラッチが合成されないように、必ず else を記述するよう注意する必要があったりましたが、SFLでは動作が無い場合は単に何の動作も起きないだけですから、基本的にはそういった注意は必要ありません。

any を使った簡単な例を示します。

// セレクタ
any {
    sel == 0b00 : o = i<0>;
    sel == 0b01 : o = i<1>;
    sel == 0b10 : o = i<2>;
    sel == 0b11 : o = i<3>;
}

入力 sel の内容 0〜3 に応じて、4ビット中から1ビットを選択して出力する回路です。この例の場合では、それぞれの条件は排他的ですので、どれかひとつが動作することになります。

// 条件は独立
any {
    i<0> : r0 := ^r0;
    i<1> : r1 := ^r1;
    i<2> : r2 := ^r2;
    i<3> : r3 := ^r3;
    else : r4 := ^r4;
}

入力において立っているビットに応じて、レジスタが反転します。それぞれの条件は独立ですので、複数が同時(並列)に動作すすることもあります。どのビットも立っていない場合は else の行の r4 := ^r4 が動作します。

// 定数の条件
any {
    0b1 : o1 = i1;
    a : o2 = i2;
}

ごく単純な動作だけのために、par-alt と入れ子を深くすると記述が煩雑になりますので、このように定数式を使うとよいでしょう。

// 空文とelse
any {
    a : o = i1;
    b : ;  // 空文
    else : o = i2;
}

SFLには基本的には「文」という名前のものはありませんが、その例外として何の動作も表さない「空文」という特別な動作があり、コメント示した行にあるように単にセミコロンのみを書きます。この例では、elseにまとめられる「その他の場合」に含まれない場合のために使っています。(このチュートリアルではSFLの仕様に合わせた用語を使うようにしていますが、これについては「空動作」あるいは「無動作」という語を使ったほうがいいかもしれません)

6.3 alt

ステートマシンの遷移など、優先順位を付けてどれかひとつ、という選択をしたい場合、any で書くと、

// anyで優先順位付き選択
any {
    a : ...
    else : any {
        b : ...
        else : any {
            ...
        }
    }
}

のように煩雑になり、ネストも深くなります。このような場合を簡単に表現できるのが alt です。

alt {
    条件1 : 動作1
    条件2 : 動作2
      :
      :
    [else : 動作n]
}

any と見た目は同じですが、上から順に見て、最初に真だった条件に対応する動作のみが動きます。典型例であるプライオリティエンコーダの例を示します。

// プライオリティエンコーダ
alt {
    i<2> : o = 0b11;  // i == 0b1??
    i<1> : o = 0b10;  // i == 0b01?
    i<0> : o = 0b01;  // i == 0b001
    else : o = 0b00;  // i == 0b000
}

alt の場合は最後の else は定数 0b1 でもかまいません。意味がプログラミング言語の多分岐に近いのはこちらですが、条件がもともと排他的で優先順位が必要なければ alt ではなく any を使うべきです(優先順位を付けるために、回路規模が大きくなります)。

6.4 if

条件が1個の場合専用の、一種のシンタクティックシュガーのようなものとして if もあります。このチュートリアルでは説明は省略しますので、仕様などで確認してください。

6.5 端子への値の出力

ここまでで説明している組合せ論理回路における単位動作は、ここまでの例に既に何度も出てきていますが、端子への値の出力です。データ出力端子(output)やデータ内部端子(wire)に対して値を出力する動作は、その端子の名前を t とすると次のように記述します。

t = 式 ; 

あるいはサブモジュール m のデータ入力端子 i に対して値を出力する場合は、

m.i = 式 ; 

のようになります。左辺の端子と右辺の信号のビット幅は一致していなければなりません。また、左辺には単に端子名のみしか指定できず、t<3> のようにビット切出しにすることはできません。

データ端子に値が出力されていない時には、データ端子の状態は不定値(u)かハイインピーダンス(z)になると考えてください(SFLの言語仕様としては、双方向端子がハイインピーダンスになること以外は規定していない、と考えてください)。

SFLには出力の他にも、制御端子や順序回路に関する単位動作がありますがそれらはこのあと制御端子や順序回路について説明する時に説明します。式については、次で説明します。

6.6 式

「式」は、「動作」の一部ですが、「動作」そのものではありません、動作が「何らかの振舞い」であるのに対し、式は「何らかの値を求めるもの」です。SFLの「式」の演算子などには低・中・高の3種類の優先順位があり、構文規則では順に「式」「単項式」「要素」が対応しています。これらの構文規則を順に見ていきます。

式:
      単項式 "|"  式  // ビット論理和
    | 単項式 "@"  式  // ビット排他的論理和
    | 単項式 "&"  式  // ビット論理積
    | 単項式 "||" 式  // 連結
    | 単項式 "+"  式  // 加算
    | 単項式 ">>" 式  // 右シフト
    | 単項式 "<<" 式  // 左シフト
    | 単項式 "==" 式  // 一致
    | 単項式

いくつかの記号がC言語などと、あるいはVerilog HDLとも異なるので注意してください。以下で述べる例外を除いて、演算子の左右の式のビット幅は同じでなければならず演算の結果は左右の式の幅と同じ幅になります。例外は、連結は任意の幅を連結でき、結果の幅はそれぞれの幅の合計になります。シフトの右辺は任意の幅で、結果は左辺と同じ幅になります。一致は、左右の幅は同じである必要があり、結果は1ビットになります。

加算のキャリィアウトは、あらかじめMSB側を1ビット広げておくことで得ます。キャリィインは定数1(幅に注意)を足します。シフトのはみ出るビットについても同様です。シフトで空いたビットには0が入ります。いわゆる算術(右)シフトの演算子は無いので、後述する符号拡張演算子と組み合わせて自分で記述する必要があります。

ここで使っているnslcoreでは減算もできますが、SFLの仕様には減算は無いので、仕様通りのSFLでは2の補数を使ってこれも自分で記述する必要があります。

また、一般的な算術の優先順位と違い、右結合であることと、論理和と論理積の優先順位が同じということに注意してください。たとえば次のようになります。

o = a | b & c; // a | (b & c) の意味
o = a & b | c; // a & (b | c) の意味

あまりに入り組んだ式をいっぺんに書くのはやめておいたほうが良いでしょう。

単項式:
      "^" 単項式   // ビット否定
    | "/|" 単項式  // 桁方向のビット論理和
    | "/@" 単項式  // 桁方向のビット排他的論理和
    | "/&" 単項式  // 桁方向のビット論理積
    | 結果の桁数 "#" 単項式  // 符号拡張
    | 要素 "<" (最上位)桁位置 [ ":" 最下位桁位置 ] ">"  // ビット切出し
    | 要素

桁方向の〜、は、それぞれの種類の(Verilog HDLで言う)リダクション演算で、たとえば幅が4ビットの a があったとすると /|a は a<3> | a<2> | a<1> | a<0> の意味になり結果の幅は1です。

符号拡張ではMSBを上位側に複製して目的の桁数にします。

ビット切出しで桁位置を1個だけ指定すると、その桁1ビットだけを切出します。桁位置の大小を逆にすると、逆順にすることができます(nslcoreではできますが、前身のsfl2vlではできません)。SFLの仕様では、元のビット幅より大きい位置を指定すると、ゼロ拡張になると規定されていますが、nslcoreではそのような指定はできません。

SFLの仕様では桁位置には「数」しか置けませんので、コンパイル時に固定した桁位置の切出ししかできませんが、nslcoreでは1ビットの切出しであれば桁位置には任意の式が置けます。

(SFLの仕様にはデコードとエンコードの単項演算子もありますが、nslcoreでは使えないので省略しました)

算術右シフトは次のようにして記述できます。16ビットの例です。

// 算術右シフト
declare asr {
    input i<16>, w<4>;
    output o<16>;
}
module asr {
    o = (32#i >> w)<15:0>;
}

最後は「要素」です。

要素:
      "(" 式 ")"
    | 定数
    | レジスタ名
    | メモリ名 "[" 式 "]"
    | 端子名
    | サブモジュール名 "." 端子名

特に説明が必要なものはないでしょう。レジスタやメモリについては後で扱います。定数は次で説明します。

6.7 定数

仕様で「定数」という語を使っているので、本稿でも「定数」で統一していますが、何らかの名前(識別子)で常に同じ何らかの値を参照できるといういわゆる定数ではなく、式の値を直接書いた式である、いわゆる「リテラル」のことを、SFLでは「定数」と呼んでいます。

C言語などでは整数は基本的に同じ幅であるのと違い、SFLの定数には幅があります。たとえば、

0x0  // 4ビット
0x003  // 12ビット
0x0000_1234  // 32ビット

慣れないうちは、特に 0 や 1 など小さい値でうっかり幅が足りない、あるいはプレフィックスを忘れないよう注意が必要です。

32ビットの例のようにnslcoreでは(仕様としてはSFLではなくNSLのものですが)途中に "_" を挟むことができるので、可読性のために適宜使いましょう。

16進の 0x の他、2進の 0b、8進の 0o というプレフィックスがあります。

(SFLの仕様では定数の途中で進法を変えることができますが、nslcoreではNSL言語設計者による「バグの元だ」という判断により使えません。8086の命令コードのようなものを記述する時には、2進記法と前述の "_" をうまく使いましょう)

(TODO、ここまでの数節のそれぞれ最後のほうNSLについてのフォローも入れる)

6.8 制御端子

離散時間モデルと並ぶ、SFLのもう1つの大きな特徴が制御端子です。ここで制御端子について説明します。

典型的にはコンピュータの論理回路などの場合、その回路を流れる信号は、処理対象の数値などを表す「データ信号」と、命令などに由来する、処理そのものを表す「制御信号」に大別できます。

コンピュータの例を続けますが、アーキテクチャによっては、ハーバードアーキテクチャのように構造上もハッキリ分かれている場合もあれば、いわゆるノイマンアーキテクチャのように一緒に扱う場所があるものもあるように、明確に線引きができるものでもないことを注意しておきます。

SFLでは、以上のような分類におけるデータなどを「動作の客体」と呼び、制御などを行うものを「動作の主体」と呼び、多くの点で扱いが異なります。

SFLにおける、「動作の主体」の一種類である制御端子には、以下のような細分類があります。

制御端子の特徴、特にデータ端子と違う主な点は次の通りです。

機能ICによくある端子に、動作を許可するイネーブルという端子があります。少し前に出たプライオリティエンコーダにイネーブルを付けると、次のようになるでしょう。ここでは出力が有効なことを示す oe という端子も付けています。

(古いサンプルには do という名前が使われていることがありますが、nslcoreでは出力として選べる言語のキーワードと同じ綴りは予約されていて使えなかったりするため、SystemC などで使われている do などは避けたほうが良いでしょう)

// プライオリティエンコーダ、ie・oe 付き
declare priority_encoder {
    input i<3>;
    output o<2>
    instrin ie;
    instrout oe;
}
module priority_encoder {
    instruct ie par {
        oe();
        alt {
            i<2> : o = 0b11;
            i<1> : o = 0b10;
            i<0> : o = 0b01;
            else : o = 0b00;
        }
    }
}

ここで、モジュールの定義は次のように書いても全く同じです。

// 別の書き方
module priority_encoder {
    any {  // 共通動作
        ie : par {
            oe();
            alt {
                i<2> : o = 0b11;
                i<1> : o = 0b10;
                i<0> : o = 0b01;
                else : o = 0b00;
            }
        }
    }
}

制御端子には、キーワード instruct を使った「その端子が起動された時の動作」を定義するための構文が用意されていて、それを使ったのが先に示した方です。この例のように単純な場合は専用の記法を使ったほうがネストが深くならず見通しが良いですが、起動に複数の条件が関与するなどといった場合など、後者のように共通動作などで any を使って記述する方法も併用する必要があるでしょう。

制御端子をアクティブ(値を真に・1に)する(「制御端子の起動」)には、この例の「oe();」のように、端子名に () を付けたものを記述します。他のモジュールの制御入力端子の場合には、「サブモジュール名.」を前に付けます。たとえば、このプライオリティエンコーダを使うモジュールの記述は次のようになるでしょう。

module ... {
    reg r<2>;
    priority_encoder pe;

    ...
        par {
            pe.ie();
            pe.i = 入力データ;
        }
        ...

    instruct pe.oe r := pe.o;
}

SFLの構文として、モジュールの定義の中には、共通動作の定義が先に、制御端子(の起動)による動作の定義が後に現れなければなりません。逆にすると構文エラーになりますが、エラーメッセージがちょっとわかりにくいかもしれません。

instr_arg というキーワードを使って引き数を定義し、制御端子の起動の際にプログラミング言語における関数呼出しのように記述できる方法もありますが、このチュートリアルでは扱いません。記法などにこだわるよりも、あくまで前述したような特性を持つ、端子の一種として扱うべきと筆者は考えています(参考: SFLと比較対象になる言語としてBluespec System Verilog(BSV)があると思いますが、BSVは「メソッド」として、オブジェクト指向プログラミング言語風の記法をおおいに使っています)。

またSFLの言語内からは、Lowがアクティブを意味する、いわゆる負論理を指定する方法はありません。

7 順序回路

4節でSFLにおける時間モデルについて説明したように、

(以下TBD)