FPGAの構成
FPGAについて解説します。
- 使用するFPGA基板
Lattice社MachXO2 Breakout Board - 開発ソフトウェア
Lattice Diamond - ハードウェア記述言語
VHDL
MachXO2 Breakout Boardはプログラムと電源供給用のUSBポートを備えています。PCに接続するだけで内部ハードウェアの構成のプログラムと動作確認ができます。(残念なことに,せっかくのUSBポートもデータ通信に使えないので,別途USBインタフェース基板が必要となります)開発ソフトのLattice Diamondは無償で使えます。
それではFPGAのハードウェアを記述するVHDLコードについて解説していきます。システムを構築するためには生成したハードウェアの入出力ピンをFPGAのIOピンに割り当てるための作業も必要ですが,それについては別稿で解説する予定です。
VHDLコード
VHDLコード全体は,概略,以下のような構成になっています。
ライブラリ宣言
トップレベル記述のEntity宣言
Architecture宣言
(信号と部品の宣言)
中身の仕組み・動作を記述
トップレベルARRAY_MIC.vhdの構成

ファイルの構成
コードは全部で3つのファイルで構成されています。
- ARRAY_MIC.vhd
トップレベルの回路を記述するファイルです。トップレベルというのは回路を外からみたとき1番外側になっている部分のことです。このファイルで記述された入出力ポートがFPGAの外部とデータをやりとりすることになります。
- FT232H_FIFOwrite.vhd
トップレベルの回路内部で使われる部品(component)を記述するファイルです。この部品は,FT232HのFIFOバッファへのデータの書き込みを担当します。
componentはCPUの動作を記述するC言語などのプログラミング言語の関数と似た役割になります。ただし,プログラミング言語の関数とは異なり,メインのプログラムから呼び出されるのではないことに注意してください。componentは,トップレベル記述の中で部品として実体化(instantiation)され,ずーっと存在することになります。 - ARRAY_PKG.vhd
自作のパッケージファイルです。パッケージというのは,共有化できる信号の型や演算子などをまとめたものです。C言語のヘッダファイルのようなものです。
今回のコードでは部品への分割化が十分でなく,トップレベルで多くの機能を記述する形になってしまいました。コードの可読性やメンテナンスを考えると,部品ファイルをもっと増やした記述が望ましいのです。小さな記述から出発し機能の付け足しを繰り返してできたファイルを「動いているからこのままでいいや」とそのまま使い続けています。
トップレベルの回路記述
トップレベルの回路記述,ARRAY_MIC.vhdを見ていきましょう。web画面では一目で把握するのは難しいので,頭から少しずつ解説していきましょう。
ライブラリ宣言
USE ieee.std_logic_1164.all;
USE ieee.std_logic_arith.all;
USE ieee.std_logic_unsigned.all;
USE work.ARRAY_PKG.all;
VHDLコードで使うライブラリとパッケージを指定します。
- USE ieee.std_logic_arith.all;
算術演算を使うためのパッケージ。 - USE ieee.std_logic_unsigned.all;
符号なし整数を使うためのパッケージ。 - USE work.ARRAY_PKG.all;
自作のパッケージ。チャンネル数や,64ビット幅の信号を使うための変数型を宣言している。Lattice Diamondの処理系ではユーザ作成のパッケージは,workというパッケージライブラリの中に作られるので,ライブラリ宣言ではwork.ARRAY_PKG.allのように指定する。
自作パッケージ
別のファイルになってしまいますが,ARRAY_PKG.vhdの中を見てみましょう。
LIBRARY ieee;
USE ieee.std_logic_1164.all;
package ARRAY_PKG is
constant N_CH : integer := 8;
constant N_BYTE : integer := 1;
subtype SIG is std_logic_vector (N_BYTE*8-1 downto 0);
type SIG_ARRAY is array (0 to N_CH-1) of SIG;
--std_logic_vector (N_BYTE*8-1 downto 0);
end ARRAY_PKG;
ここでは,以下のような変数型を定義しています。
- constant N_CH : integer := 8;
“チャンネル数”を表す定数。“チャンネル”というのは,FPGAにデータを転送するデータの単位のことを指す。FPGAとUSBインタフェースのFT232Hは,このチャンネルを単位としてデータをやり取りする。チャンネル数N_CHは定数として8という値に固定されている。 - constant N_BYTE : integer := 1;
N_BYTEはチャンネルを構成するバイト数を表す定数。N_BYTEバイトで構成されるチャンネルをN_CH個使って,1回の時間サンプルで得られるデータを表す。つまり,N_CH×N_Byte×8ビットがFT232Hの入力ポートに入力される。 - subtype SIG is std_logic_vector (N_BYTE*8-1 downto 0);
サブタイプSIGを定義している。N_BYTE=1の場合は,std_logic_vector(7 downto 0)という8ビット幅のstd_logic_vector型になる。 - type SIG_ARRAY is array (0 to N_CH-1) of SIG;
FPGAから出力される信号を,N_BYTE*8ビットのstd_logic_vector型データの配列で表する。配列は0からN_CH-1までの昇順のインデックスで指定される。N_CH = 8,N_BYTE = 1なので,SIG_ARRAY型は,std_logic_vector(7 downto 0)型のデータの寸法N_CHの配列になる。
トップレベル回路のEntity宣言
-- TOP Level Hardware
ENTITY recording_mic IS
PORT(
clk_60MHz : in STD_LOGIC; -- Clock from FT232H USB interface
mic_clk : buffer STD_LOGIC; -- Clock for MEMS Mic
mic_in : in SIG_ARRAY; -- Connect to Mic Output
-- SIG_ARRAYの寸法はARRAY_PKG.vhdで設定
RESET_SW : in STD_LOGIC; -- Connect to tact switch
START_SW : in STD_LOGIC; -- Connect to tact switch2
word_data : out STD_LOGIC_VECTOR(N_BYTE*8-1 downto 0);
-- send to USB UART/FIFO IC (FT232H)
-- Controll Signals
WR : out STD_LOGIC;
TXE : in STD_LOGIC;
RD : out STD_LOGIC;
RXF : in STD_LOGIC;
LED : out STD_LOGIC_VECTOR(7 downto 0)
);
END recording_mic;
Entityとは「実体」という意味です。VHDLでのEntity宣言は,ある回路を外から見たときに見える入力や出力を宣言する構文です。回路の内部構成は別にarchitecture文の中で記述されます。
「トップレベル」というのは,階層化されたハードウェア階層(レベル)の最上位,最も外側に見える回路ということです。トップレベル記述のEntity宣言により,FPGAの外から見える入力と出力を定義しています。
この宣言で,recording_micという名前の回路の入出力が宣言されます。主な入出力信号は以下のようになります。
- clk_60MHz : in STD_LOGIC;
FT232Hに内蔵された発振器から出力される公称値60MHzのクロック信号。FPGAは,このクロック信号に同期して動作する。clk_60MHz はFT232Hの電源ONの直後には出力されない。FT232Hを制御するC++コードなどで同期FIFOの動作モードが設定された後で出力される。 - mic_clk : buffer STD_LOGIC;
MEMSマイクに供給される周波数3MHzのサンプリングクロック。入出力の属性である“buffer”は,出力として取り出した信号をFPGA内部で他の信号の生成に使う場合に宣言する。 - RESET_SW,START_SW
FPGAのリセットと動作開始をさせるためのタクトスイッチからの信号。将来的には,自動的なリセットや動作開始信号に置き換える予定。 - word_data : out STD_LOGIC_VECTOR(N_BYTE*8-1 downto 0);
FT232Hへの出力。FT232Hは1バイト単位での入出力になるので,N_BYTE=1である。2バイト,4バイトなどで入出力をするデバイスを将来使用する場合は,N_BYTEの値を変更すればよい。 - WR : out STD_LOGIC;
FPGAからFT232Hに送信する書き込み制御信号。WR = ‘0’のとき,FT232HはFPGAの出力を内部バッファに書き込む。本システムでは,FPGAからFT232Hにデータを送信するので,このWR信号を使う。 - TXE : in STD_LOGIC;
FT232Hの内部FIFOの状態を表すフラグ信号。TXEが0のときはFIFOに空きがありデータを書き込むことができる。 - RD : buffer STD_LOGIC;
FPGAからFT232Hに送信する読み込み制御信号。RD = ‘0’のとき,FT232Hはデータを出力する。本システムでは使用しない。 - RXF : in STD_LOGIC;
Read Buffer Full。FT232HのFIFOバッファにデータが残っていることを示すフラグ。0の時データが残っていることを示す。FPGAはRXF = ‘0’を確認してRD = ‘0’として,データを読み込む。
本システムでは使用しない。 - LED : out STD_LOGIC_VECTOR(7 downto 0)
デバッグ用のLED出力。FPGA内部の信号を目視でモニタする場合に使う。VHDLコードを書き換えて目的とする信号を割り当てる。
Architecture宣言(信号と部品の宣言)
Entity宣言では「外側から見た回路」を定義しました。Architecture宣言では,回路の中身の仕組みは動作を記述していきます。
ARCHITECTURE behavior OF recording_mic IS
signal clk_3MHz : STD_LOGIC; -- Clock
signal mclk_D : STD_LOGIC; -- Clock delayed 1 clock cycle
signal mclk : STD_LOGIC; -- 確認用
signal reset : STD_LOGIC;
signal Ch_cnt : STD_LOGIC_VECTOR(7 downto 0); -- マイクのカウント用
signal Ch_n : STD_LOGIC_VECTOR(7 downto 0); -- チャンネル数設定用
signal WR_busy : STD_LOGIC; --書き込み状態で1
signal Samp_cnt : STD_LOGIC_VECTOR(15 downto 0) := "0000000000000000";
-- 時間サンプル数のカウント用
signal Samp_n : STD_LOGIC_VECTOR(15 downto 0); --時間サンプル数の上限設定
signal data_cnt : STD_LOGIC_VECTOR(7 downto 0) := (others => '0');
signal START_D : STD_LOGIC; -- START生成用
signal START : STD_LOGIC; -- START状態を示す
signal cnt_3M : STD_LOGIC_VECTOR(7 downto 0) := (others => '0');
signal RUN : STD_LOGIC := '0'; -- 動作状態のとき1
signal in_data_buffer : SIG_ARRAY; -- 入力バッファ
signal mic_data : STD_LOGIC_VECTOR(N_BYTE*8-1 downto 0);
COMPONENT FT232H_FIFO_write
PORT(
--write_word : out STD_LOGIC_VECTOR(N_BYTE*8-1 downto 0);
write_word : out SIG;
in_data : in SIG_ARRAY;
reset : in STD_LOGIC;
Ch_cnt : in STD_LOGIC_VECTOR(7 downto 0);
clk : in STD_LOGIC;
clk_60MHz : in STD_LOGIC;
--WR : in STD_LOGIC;
WR_busy : in STD_LOGIC;
TXE : in STD_LOGIC
);
END COMPONENT;
ARCHITECTURE behavior OF recording_mic IS
から回路recording_micの仕組み(architecture)の記述を開始します。
architecture宣言のはじめの部分で,回路内部で使う信号をsignal宣言で定義します。また,あらかじめ構造を定義した部品(component)も,この部分で,どのような入出力を持っているかを宣言します。
Architecture宣言(中身の仕組み・動作を記述)
signal宣言とcomponent宣言の後のBEGINから最終行のEND behavior;までが,signal宣言で定義された信号,Entity宣言で定義された信号,部品を使って,回路の動作や構造を記述していきます。以下のコードは,BEGINの直後から15行目までを示しています。
BEGIN
USB:FT232H_FIFO_write
PORT MAP(
write_word => word_data,
in_data => in_data_buffer,
clk => mclk,
clk_60MHz => clk_60MHz,
Ch_cnt => Ch_cnt,
reset => reset,
WR_busy => WR_busy,
TXE => TXE
);
mic_clk <= clk_3MHz;
mclk <= clk_3MHz;
PROCESS(mclk) -- MEMS Mic 1bit output (Synchronized with mclk)
BEGIN
IF(mclk'EVENT AND mclk='1') THEN
in_data_buffer <= mic_in;
END IF;
END PROCESS;
reset <= RESET_SW;
Ch_n <= CONV_std_logic_vector(N_CH,8);
Samp_n <= CONV_std_logic_vector(8192,16);
Architecture宣言内の回路記述は,「同時処理文」を並べたものになります。同時処理文は,回路内のゲートやレジスタなどの要素の働きや,相互の接続を記述します。なぜ「同時」と呼ぶかというと,回路内の複数の要素は並列に存在し同時に動作するもので,それを記述するための文だからです。
ですから,これらの同時処理文はVHDLコードの中で順番を変えても,表している回路は変わらないことに注意してください。
一方,C/C++言語などでは,コード内の式文は並べた順に処理されていく点が大きく異なります。VHDLやVerilogなどのハードウェア記述言語の初学者が最も苦労するのは,この点ではないかと考えています。
- USB:FT232H_FIFO_write
PORT MAP(
・・・・
TXE => TXE
);
このコードブロックは,コンポーネントインスタンス文と呼ばれます。Architecture宣言の初めの部分のComponent宣言で,“外から見た部品”つまり部品の入出力を定義しました。この部品を使う場合は,回路内の他の信号や外部からの入出力,さらに他の部品の入出力を,部品の入出力ポートに接続することになります。
このように,定義された部品を,実際の回路の中に生成(instantiation)するので,コンポーネントインスタンス文と呼ばれます。 - mic_clk <= clk_3MHz;
は「代入文」と呼ばれています(私は,“<=”のことを“やじるしイコール”と呼んでいます)。代入というよりは,「clk_3MHzをmic_clkに接続する」という記述と考えた方が良いことが多いです。
以下,延々と同時処理文が続きます。ここでコードを書いていく上で問題となることがあります。順番を気にしなくてよいので,つい,いろいろな箇所に同時処理文を挿入してしまいます。すると,1つの機能,例えばFT232Hとのインタフェースの機能を実現するための要素が,コード内の離れた場所に分散してしまう可能性が出てきます。
この問題は,回路設計の初期の段階で,機能に合わせて回路を部品に分割して記述する,つまり逐次処理記述型のプログラム言語のfunctionのようにすることで解決します。しかし,今回は,部品はFT232H_FIFO_write1つだけなので,同じ回路機能を実現するための同時処理文がコード内で離れて配置されて読みにくくなっている可能性があります。
マイク出力の取り込み
PROCESS(mclk) -- MEMS Mic 1bit output (Synchronized with mclk)
BEGIN
IF(mclk'EVENT AND mclk='1') THEN
in_data_buffer <= mic_in;
END IF;
END PROCESS;
サンプリングクロックmclkの立上りエッジのタイミングでMEMSマイクの出力信号を取り込む動作を記述しています。
この記述は全体で1つのProcess文を構成しています。Process文の全体で,1つの同時処理文,つまり,1つの回路要素をあらわしています。
一方,Process文の中身は,「逐次処理文」で構成されています。これは手続き処理型のプログラミング言語と同じように,回路の動作を「コードの上から順番に実行される処理として」記述するものです。(順番に実行される要素で回路を生成する,ということではなく,順番に並べた文で回路の動作を記述する,ということです。)
- PROCESS(mclk)
プロセス文は,PROCESS(“センシティビティリスト”)という式文から始まる。プロセス文では,回路動作を「センシティビティリストに記される複数の信号が変化したときに,プロセス文のBEGINの下に書かれている動作が上から順番に実行される」形で記述する。
このプロセス文のセンシティビティリストにはmclkだけが記されている。mclkが変化したときにBEGINとEND PROCESSの間の記述が実行される,と考える。 - IF(mclk’EVENT AND mclk=’1′) THEN
この形の記述は,同期回路の記述では非常に頻繁に目にする。mclk’EVENTという論理式は信号mclkが変化したときに真値(’1’)になる。つまり,mclk’EVENT AND mclk=’1’は,mclkの立上りの時に真となる。 - in_data_buffer <= mic_in_b;
mclkの立上りのタイミングで,mic_in_bがin_data_bufferに取り込まれる。
<続きを読む>
回路の動作は同時並行的です。そして回路は相互接続されたゲートやレジスタで表すことができます。したがって,全てをゲートやレジスタの相互接続を記述する同時処理文で表すことは可能です。しかし,そのような記述は,見ても何をしているか全くわからないものになってしまいます。
逐次処理文による記述は,「こういう条件のときは,こう動作する,別の条件のときは,こう動作する」とか,「まず,このようにして,次はこのようにして・・・」と1ステップずつ考えて回路の動作を書くものです。それにより,記述される回路の動作が,わかりやすくなります。
<閉じる>
分周によりサンプリングクロックを生成
PROCESS(clk_60MHz,reset) --60MHz → 3MHz
BEGIN
IF(reset ='0')THEN
cnt_3M <= (others => '0');
clk_3MHz <='0';
ELSIF(clk_60MHz'EVENT AND clk_60MHz='1')THEN
IF(cnt_3M < 19)THEN
cnt_3M <= cnt_3M + 1;
IF(cnt_3M < 10)THEN
clk_3MHz <= '0';
ELSE
clk_3MHz <= '1';
END IF;
ELSE
cnt_3M <= (others => '0');
END IF;
END IF;
END PROCESS;
SyncFIFOモードのFT232Hが出力する60MHzのクロック信号を分周してMEMSマイクに供給する3MHzのクロック信号を生成する部分です。また,このクロック信号に同期してMEMSマイクの出力をFPGAに取り込みます。
- PROCESS(clk_60MHz,reset)
センシティビティリストにはclk_60MHzとリセット用にresetという2つの信号を列挙している。 - IF(reset =’0′)THEN
cnt_3M <= (others => ‘0’);
clk_3MHz <=’0′;
reset=’0’のとき,カウンタ用信号cnt_3Mの全ビットを’0’,3MHzのクロック信号clk_3MHzを’0’にする。この動作は60MHzのクロックに値に関わらず実行されるので,“非同期リセット”になる。 - ELSIF(clk_60MHz’EVENT AND clk_60MHz=’1′)THEN
clk_60MHzの立上りエッジで動作します。 - IF(cnt_3M < 19)THEN
cnt_3M <= cnt_3M + 1;
カウンタ信号cnt_3Mの値が19未満のときは,カウントアップしていく。 - IF(cnt_3M < 10)THEN
clk_3MHz <= ‘0’;
ELSE
clk_3MHz <= ‘1’;
END IF;
cnt_3MHzが10未満のときclk_3MHz <= ‘0’;,それ以外のときはclk_3MHz <= ‘1’;とする。
この回路記述により生成される回路は,カウンタ信号cnt_3Mがclk_60MHzの立上りエッジに同期して0, 1, 2, ・・・,19という変化を繰り返します。つまり,繰り返し周波数は60MHzを20分周した3MHzになります。その中でcnt_3M < 10のとき,つまりcnt_3Mの値が0, 1, 2, ・・・,9のときはclk_3MHz = ‘0’,10, 11, 12, ・・・, 19のときはclk_3MHz = ‘0’となります。したがって,0になっている長さと1になっている長さが等しい(Duty比が50%の)3MHのクロック信号を生成することになります。
チャンネル選択信号と出力レジスタ
64個のMEMSマイクからの入力信号をチャンネル単位でまとめて処理します。チャンネルの寸法はN_BYTEバイトつまりN_BYTE×8ビットになります。入力信号はチャンネル単位でレジスタに保持され,部品FT232H_FIFO_writeでチャンネル番号順に選ばれてFT232Hに転送されます。チャンネルカウンタは,チャンネルの番号を60MHzのクロックに同期して増加させます。このチャンネルの番号はFT232H_FIFOに送られます。
チャンネルカウンタ
PROCESS(clk_60MHz,reset) --Ch counter
BEGIN
IF(reset='0')THEN
Ch_cnt <= (others => '0');
ELSIF(clk_60MHz'EVENT AND clk_60MHz='1' AND WR_busy ='1')THEN
IF(Ch_cnt = Ch_n-1)THEN
Ch_cnt <= (others => '0');
ELSE
Ch_cnt <= Ch_cnt + 1;
END IF;
END IF;
END PROCESS;
MEMSマイクのデータは,チャンネル0,チャンネル1,チャンネル2,・・・という順番でFT232Hに送信されます。チャンネルカウンタは,チャンネル番号を指定する信号を生成します。
- PROCESS(clk_60MHz,reset) –Ch counter
BEGIN
IF(reset=’0′)THEN
Ch_cnt <= (others => ‘0’);
PROCESS文でチャンネルカウンタ信号Ch_cntを生成する回路を記述する。センシティビティリストは60MHzクロックclk_60MHzとリセット信号resetになっている。リセット信号が0になると,clk_60MHzに関係なく直ちに信号Ch_cntの全ビットを0にする。 - ELSIF(clk_60MHz’EVENT AND clk_60MHz=’1′ AND
WR_busy =’1′)THEN
IF(Ch_cnt = Ch_n-1)THEN
Ch_cnt <= (others => ‘0’);
ELSE
Ch_cnt <= Ch_cnt + 1;
END IF;
resetが0でない場合は,カウンタとしての動作になる。クロックの立上りの際,Ch_cntが指定されたチャンネル数Ch_n未満の場合はCh_cntの値を1増加し,Ch_nに達していればCh_cntの全ビットを0にする。
サンプル数のカウンタ
サンプル数をカウントする回路を記述します。
PROCESS(clk_60MHz, reset) -- サンプル回数のカウンタ
BEGIN
IF(reset='0')THEN
Samp_cnt <= (others => '0');
ELSIF(clk_60MHz'EVENT AND clk_60MHz='1' AND Ch_cnt =Ch_n-1)THEN
IF(Samp_cnt = Samp_n)THEN
Samp_cnt <= Samp_n;
ELSE
Samp_cnt <= Samp_cnt + 1;
END IF;
END IF;
END PROCESS;
データ転送の動作を開始してからマイクロフォンの信号をサンプルした回数を数えています。このサンプル回数を表す信号Sam_cntは,Samp_nで指定されるサンプル数に達すると,その値を保持し続けます。当初,Samp_cntは一定のサンプル数に達したときに動作を終了する目的で作られたものでした。しかし,現在のコードでは何も機能していません。
- IF(Samp_cnt = Samp_n)THEN
Samp_cnt <= Samp_n;
Samp_cntの値がSamp_nと等しくなった後は,Samp_cntの値はSamp_nの値を保持し続けることになります。
コメント