スピーカアレイ駆動システム VHDLコード (FIFO無し版)

FPGA

システムの構成

多数のスピーカを並べた「スピーカアレイ」を駆動するシステムを紹介します。元々は,「多数のスピーカやマイクロフォンをびっしり並べたものを使うと,面白いことができそうだ」ということでスタートした一連の研究のために必要になりました。

この研究をハードウェアを作って実験的に進める場合,数十個,数百個のスピーカの1つ1つに信号を供給し,増幅回路を介して駆動することになります。アナログの駆動回路ではコスト的に大変です。そこで,1ビット信号で駆動する方式を採用しています。原理などの詳しい説明は別稿に譲ります。

注意:

この投稿は,未完成のシステムの開発過程の備忘録として書かれたものです。紹介されているコードにはバグや冗長な部分が含まれています。このコードを参考にする場合は注意をお願いします。また,解説の文章などに誤りや意味不明の箇所が含まれていることにも配慮してください。

[/show_more]

信号発生部分は,図のように3つの部分で構成されています。

アレイスピーカ駆動システム
  • PC
    音声データを2次元ΔΣ変調により1ビット信号群に変換する。このデータをUSBインタフェースを介して送信する。現在,1ビット駆動信号群の生成はMATLABコードにより,データ送信はC++コードで作成したソフトウェアで実行している。
  • USBインタフェース
    データは,FTDI社のUSBインタフェースIC,FT232Hにより受信される。FT232HはAdafruit社の FT232H Breakout基板に搭載されている。
  • FPGA
    FT232Hとのインタフェース,駆動信号のタイミングの調整,データのスピーカへの割り当てなどを実行するハードウェアをFPGA上に構成する。ハードウェア実行による高速の処理を期待してFPGAを採用している。

動作の概要

試作中のシステムは64個のスピーカをサンプリング周波数3MHzで駆動する。このシステムの動作は以下のようになっている。

  • PCからのデータ送信
    サンプリング周波数3MHz,1回のサンプル当たり64ビットの1ビット信号群を,8ビット単位でUSBインタフェースを介して送信する。
  • FT232Hによる受信
    データはFT232Hで受信され,容量1kバイトの内蔵FIFOバッファに入力される。
  • FT232HとFPGAによる受信と出力
    FPGAはFT232HのFIFOバッファから60MHzのクロック信号に同期してデータを読み出す(Synchronous FIFOモード)。読みだされたデータは60MHzのクロック信号を分周して作った3MHzのオーディオ用サンプリングクロックに同期する64個の1ビット信号群として出力される。

FIFOバッファから取り出されたデータは,8ビット(1バイト)単位の時系列データになっている。このデータはレジスタに順次格納され,8バイトまとまった時点で3MHzのクロックに同期して出力される。

<続きを読む>

システムを作るとき注意すべき点は,USBインタフェースを介してFPGAに送られるデータはタイミングが一定ではない,ということです。そもそもPC内部の計算のタイミングもUSBの通信も,OSや並行して動いている他のソフトウェアの動作の影響を受けています。このため,FPGAが受け取るデータは,一定のサンプリング間隔では到達しません。

一方,スピーカを駆動する信号は,一定の時間間隔でサンプルされていることを前提としています。そこでFPGAは,受け取ったデータを正確なクロック信号に同期するようにタイミングを合わせて出力します。

また,現在のシステムは,通信エラーによるデータの欠落や「ずれ」への対策は施されていません。通信エラーは必ず起こると考えるべきなので,いずれエラー対応の機能を組み込む必要があると考えています。

<閉じる>

FPGAの構成

 FPGAについて解説します。

  • 使用するFPGA基板
    Lattice社MachXO2 Breakout Board
  • 開発ソフトウェア
    Lattice Diamond
  • ハードウェア記述言語
    VHDL

では,FPGAのハードウェアを記述するVHDLコードについて解説していきます。システムを構築するためには生成したハードウェアの入出力ピンをFPGAのIOピンに割り当てるための作業も必要ですが,それについては別稿で解説する予定です。

VHDLコード

LIBRARY ieee;
USE ieee.std_logic_1164.all;
USE ieee.std_logic_arith.all;
USE ieee.std_logic_unsigned.all;
USE work.ARRAY_PKG.all;	-- 自作のパッケージは“work.”で指定

-- TOP Level Hardware
ENTITY array_speaker_driver IS
	PORT(
			clk_60MHz	: in 		STD_LOGIC;	-- Clock signal from FT232H
			data_in		: in 		STD_LOGIC_VECTOR(N_BYTE*8-1 downto 0);
												--Input from FT232H
			WR			: buffer	STD_LOGIC; 	-- ホストにデータ転送のとき1に
			TXE			: in		STD_LOGIC; 	-- Transmit Bufffer Enough 
												-- 0を確認してバッファに書込み
			RD			: buffer	STD_LOGIC;	-- ホストからデータ転送のとき1に
			RXF			: in		STD_LOGIC;	-- Receiver Buffer Full
												-- 0を確認してバッファから読出し
			OE			: buffer	STD_LOGIC;	-- Output Enable
												-- FIFOからのデータ出力する際1に
			RESET_SW  	: in  		STD_LOGIC; 	-- Connect to tact switch1
			START_SW	: in  		STD_LOGIC; 	-- Connect to tact switch2
			drive		: buffer 	SIG_ARRAY;	-- Output to Loudspeaker Array
			drive_clk	: buffer 	STD_LOGIC; 	-- Clock for loudspeaker array
			LED			: out 		STD_LOGIC_VECTOR(7 downto 0)-- LED Indicator 
	     );
END array_speaker_driver;

ARCHITECTURE behavior OF array_speaker_driver IS
	signal out_data	: SIG_ARRAY;	-- アレイスピーカへの出力
	signal ch_data 	: SIG_ARRAY; 	-- チャンネルセレクタの出力	
	-- タイミング用信号:
	signal d_clk 	: STD_LOGIC; 	-- driver clock 駆動出力の外部同期用
	signal cnt_d	: STD_LOGIC_VECTOR(7 downto 0); -- d_clk用カウンタ
	signal d_clk_D 	: STD_LOGIC;	-- d_clkを60MHzクロックの1周期だけ遅らせたもの
	signal reset 	: STD_LOGIC;	-- システム全体のリセット信号
    signal Ch_cnt	: STD_LOGIC_VECTOR(4 downto 0); --チャンネルの番号を指定
	signal ch_n		: STD_LOGIC_VECTOR(4 downto 0); --チャンネル数を指定
	signal RD_busy	: STD_LOGIC := '1';		-- Read Busy
	signal RD_busy_D: STD_LOGIC := '1';
	signal OE_busy	: STD_LOGIC := '0'; 	-- Output Enable Busy
	signal RXF_in 	: STD_LOGIC := '1';
	-- 動作のスタートなどに関連する信号:
	signal START_D 	: STD_LOGIC := '1';		-- START_SWを1クロック遅延させた信号
	signal START 	: STD_LOGIC := '1'; 	-- スタート状態を表す
	signal RUN 		: STD_LOGIC := '0'; 	-- 実行状態(RUN)を示す
	-- 動作確認用の信号
	signal STATUS	: STD_LOGIC_VECTOR(7 downto 0);
	CONSTANT ND : integer := 19;
	
BEGIN
drive_clk	<= d_clk;	-- 60MHzを分周して作ったd_clkを外部同期用に出力
reset 	<= RESET_SW; 	-- システムリセット信号をRESET用スイッチにより生成
Ch_n	<= CONV_std_logic_vector(N_CH,5); 	-- N_CHの値をCh_nに設定

-- FT232からの60MHz信号を分周してd_clkを生成
PROCESS(clk_60MHz,reset) -- 60MHzからdrive_clkを作る
CONSTANT WIDTH : integer := 8;	-- '1'の幅
--CONSTANT ND : integer := 19;	-- 分周比 
BEGIN
	IF(reset ='0')THEN
		cnt_d  <= (others => '0');
		d_clk <='0';
	ELSIF(clk_60MHz'EVENT AND clk_60MHz='1')THEN
		IF(cnt_d < ND)THEN
			cnt_d <= cnt_d + 1;
			IF(cnt_d < WIDTH)THEN
				d_clk <= '1';
			ELSE
				d_clk <= '0';
			END IF;
		ELSE
			cnt_d  <= (others => '0');
		END IF;
	END IF;
END PROCESS;

-- チャンネルセレクタ用カウンタ
PROCESS(clk_60MHz,reset) --Ch counter
BEGIN
	IF(reset='0')THEN
		Ch_cnt <= (others => '0');
	ELSIF(clk_60MHz'EVENT AND clk_60MHz='1') THEN
		--IF(RUN ='1' AND cnt_d <N_CH)THEN
		IF(RD_busy ='1')THEN
			IF(Ch_cnt = N_CH-1)THEN
				Ch_cnt <= (others => '0');
				--Ch_cnt <= Ch_n-1;
			ELSE
				Ch_cnt <= Ch_cnt + 1;
			END IF;
		END IF;
	END IF;
END PROCESS;

-- Channel Selector チャンネルセレクタ
PROCESS(clk_60MHz, reset)
BEGIN
	IF(reset = '0')THEN
		ch_data <= (others => (others => '0'));
		ELSIF(clk_60MHz'EVENT AND clk_60MHz='1')THEN
			--IF(RUN ='1' AND ch_cnt < N_CH AND cnt_d <N_CH)THEN
			IF(RD_busy ='1')THEN
				ch_data(CONV_INTEGER(ch_cnt)) <= data_in;
		END IF;
	END IF;
END PROCESS;

-- Driver Output 出力用最終段のレジスタ
PROCESS(clk_60MHz, reset)
BEGIN
	IF(reset = '0')THEN
		out_data <= (others => (others => '0'));
		ELSIF(clk_60MHz'EVENT AND clk_60MHz='1')THEN
			IF(cnt_d = 0)THEN
				out_data <= ch_data;
			END IF;
	END IF;
END PROCESS;
drive <= out_data;
			
-- 外部スイッチからクロックに同期したSTART信号を生成
PROCESS(clk_60MHz, reset) 
BEGIN
	IF(reset='0')THEN
		START_D <= '1';			-- システムリセットでSTART_Dを1にセット
	ELSIF(clk_60MHz'EVENT AND clk_60MHz='1')THEN
		START_D <= START_SW;	-- START_Dを外部のSTART_SW信号から生成
	END IF;
END PROCESS;
-- “論理差分”により長い信号からクロック幅のパルスを生成
PROCESS(clk_60MHz, reset)
BEGIN
	IF(reset='0')THEN
		d_clk_D <= '0';		-- システムリセットでd_clk_Dを0にセット
	ELSIF(clk_60MHz'EVENT AND clk_60MHz='1')THEN
		d_clk_D <= d_clk;	-- 1クロック遅れた信号
	END IF;
END PROCESS;
--START <= NOT(d_clk AND NOT(d_clk_D) AND RUN);
START <= '0' WHEN cnt_d = 0 AND RUN = '1' ELSE
         '1';

-- 外部スイッチで動作信号RUNを生成
PROCESS(clk_60MHz,reset)
BEGIN
	IF(reset='0')THEN
		RUN <= '0';
	ELSIF(clk_60MHz'EVENT AND clk_60MHz = '1')THEN
		IF(START_SW = '0')THEN
			RUN <= '1';
		END IF;
	END IF;
END PROCESS;

--State transition for RD_busy,RD信号を作るためのステートマシン
PROCESS(clk_60MHz,reset)
BEGIN
	IF(reset='0')THEN
		RD_busy <= '0';
	ELSIF(clk_60MHz'EVENT AND clk_60MHz='1')THEN
		CASE RD_busy is
			WHEN '0' =>
				IF(START = '0' AND RXF = '0')THEN
					RD_busy <= '1';
				ELSE
					RD_busy <= '0';
				END IF;
			WHEN '1' => 
				IF(Ch_cnt = Ch_n-1)THEN
					RD_busy <= '0';
				ELSE
					RD_busy <= '1';
				END IF;
			WHEN OTHERS =>
					RD_busy <='0';
		END CASE;
	END IF;
END PROCESS;

PROCESS(clk_60MHz,reset)
BEGIN
	IF(reset='0')THEN
		RD_busy_D <= '1';
	--ELSIF(clk_60MHz'EVENT AND clk_60MHz='0')THEN
	  ELSIF(clk_60MHz'EVENT AND clk_60MHz='1')THEN
		RD_busy_D <= NOT(RD_busy);
	END IF;
END PROCESS;
-- RD control: RXFとRD_busyからRDを生成
RD <= NOT(NOT(RXF) AND RD_busy)AND RD_busy_D;
--RD <= NOT(NOT(RXF_in) AND RD_busy)AND RD_busy_D;

--RD <= NOT(NOT(RXF) AND RD_busy);
		-- RXF=0 のときバッファに読むデータが存在する
		
OE <=  	NOT RUN; 	-- FT232Hの出力許可信号
--OE <= '0';
WR <= '1';			-- FT232HへのWR,とりあえず常に1に
LED <= NOT STATUS;	-- LEDはLowで点灯するので反転

PROCESS(clk_60MHz)
BEGIN
	IF(clk_60MHz'EVENT AND clk_60MHz ='0')THEN
		RXF_in <= RXF;
	END IF;
END PROCESS;

-- STATUS LED表示用の信号
PROCESS(clk_60MHz,reset)
CONSTANT K : integer := 0;
BEGIN
	IF(reset ='0')THEN
		STATUS <= (others =>'0');
		ELSIF(clk_60MHz'EVENT AND clk_60MHz = '1')THEN
			-- 表示が不要な信号はコメントアウト
			--STATUS(7)	<= TXE; 	STATUS(6) <= NOT (WR);
			--STATUS(5)	<= RXF; 	STATUS(4) <= NOT (RD);
			--STATUS(3)	<= OE; 		  
			--STATUS(0)	<= RUN; 
			STATUS(7) <= drive(K)(7); STATUS(6) <= drive(K)(6);
			STATUS(5) <= drive(K)(5); STATUS(4) <= drive(K)(4);
			STATUS(3) <= drive(K)(3); STATUS(2) <= drive(K)(2);
			STATUS(1) <= drive(K)(1); STATUS(0) <= drive(K)(0);	
			--STATUS <= data_in;
	END IF;  
END PROCESS;

END behavior;

ライブラリ宣言

LIBRARY ieee;
USE ieee.std_logic_1164.all;
USE ieee.std_logic_arith.all;
USE ieee.std_logic_unsigned.all;
USE work.ARRAY_PKG.all;	-- 自作のパッケージは“work.”で指定

コードで使うライブラリとパッケージを指定します。

  • USE ieee.std_logic_arith.all;
    算術演算を使うためのパッケージ。
  • USE ieee.std_logic_unsigned.all;
    符号なし整数を使うためのパッケージ。
  • USE work.ARRAY_PKG.all;
    自作のパッケージ。チャンネル数や,64ビット幅の信号を使うための変数型を宣言している。

自作パッケージ

-- 自作のパッケージ
LIBRARY ieee;
USE ieee.std_logic_1164.all;
package ARRAY_PKG is
  constant N_CH : integer := 8;	-- チャンネル数
  constant N_BYTE : integer := 1;	-- チャンネル当たりのバイト数
  -- 信号はチャンネル単位で伝送される
  -- チャンネル数とチャンネル当たりのバイト数はホストPCと接続に使うUSBチップ
  -- により異なり、上のconstant文で変更する
  -- USBインタフェースチップにFT232を使う場合,	N_BYTE = 1
  --               FT601を使う場合,	N_BYTE = 4
  type SIG_ARRAY is array (0 to N_CH-1) of 
       std_logic_vector (N_BYTE*8-1 downto 0);
	   -- SIG_ARRAY型の信号Xは,X(0)~X(N_CH-1)で指定され
	   -- それぞれがN_BYTE×8ビット幅になる
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ビットがFPGAの出力ポートから出力される。
  • type SIG_ARRAY is array (0 to N_CH-1) of std_logic_vector (N_BYTE*8-1 downto 0);
    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宣言

ENTITY array_speaker_driver IS
	PORT(
			clk_60MHz	: in 		STD_LOGIC;	-- Clock signal from FT232H
			data_in		: in 		STD_LOGIC_VECTOR(N_BYTE*8-1 downto 0);
												--Input from FT232H
			WR			: buffer	STD_LOGIC; 	-- ホストにデータ転送のとき1に
			TXE			: in		STD_LOGIC; 	-- Transmit Bufffer Enough 
												-- 0を確認してバッファに書込み
			RD			: buffer	STD_LOGIC;	-- ホストからデータ転送のとき1に
			RXF			: in		STD_LOGIC;	-- Receiver Buffer Full
												-- 0を確認してバッファから読出し
			OE			: buffer	STD_LOGIC;	-- Output Enable
												-- FIFOからのデータ出力する際1に
			RESET_SW  	: in  		STD_LOGIC; 	-- Connect to tact switch1
			START_SW	: in  		STD_LOGIC; 	-- Connect to tact switch2
			drive		: buffer 	SIG_ARRAY;	-- Output to Loudspeaker Array
			drive_clk	: buffer 	STD_LOGIC; 	-- Clock for loudspeaker array
			LED			: out 		STD_LOGIC_VECTOR(7 downto 0)-- LED Indicator 
	     );
END array_speaker_driver;

Entityとは「実体」という意味です。VHDLでのEntity宣言は,ある回路を外から見たときに見える入力や出力を宣言する構文です。回路の内部構成は別にarchitecture文の中で記述されます。

「トップレベル」というのは,階層化されたハードウェア階層(レベル)の最上位ということです。つまり,トップレベルのEntity宣言により,FPGAの外に現れる入力と出力を定義しています。

この宣言で,array_speaker_driverという名前の回路の入出力が宣言されます。主な入出力信号は以下のようになります。

  • clk_60MHz : in STD_LOGIC;
    FT232Hに内蔵された発振器から出力される公称値60MHzのクロック信号。FPGAは,このクロック信号に同期して動作する。clk_60MHz はFT232Hの電源ONの直後には出力されない。FT232Hを制御するC++コードなどで,同期FIFOの動作モードが設定された後で出力される。
  • data_in : in STD_LOGIC_VECTOR(N_BYTE*8-1 downto 0);
    USBインタフェースICからの入力。FT232Hの場合,N_BYTE = 1なので,STD_LOGIC_VECTOR(7 downto 0);という8ビット幅のデータになる。
  • RD : buffer STD_LOGIC;
    FPGAからFT232Hに送信する読み込み制御信号。RD = ‘0’のとき,FT232Hはデータを出力する。
  • RXF : in STD_LOGIC;
    Read Buffer Full。FT232HのFIFOバッファにデータが残っていることを示すフラグ。0の時データが残っていることを示す。
    FPGAはRXF = ‘0’を確認してRD = ‘0’として,データを読み込む。
  • OE : buffer STD_LOGIC;
    Output Enable。FT232Hの入出力ポートからの出力を許可する制御信号。
  • RESET_SW,START_SW
    FPGAのリセットと動作開始をさせるためのタクトスイッチからの信号。将来的には,自動的なリセットや動作開始信号に置き換える予定。
  • drive : buffer SIG_ARRAY;
    スピーカ駆動回路への出力。
  • drive_clk : buffer STD_LOGIC;
    drive信号のクロックをFPGA外部に出力している。
  • LED : out STD_LOGIC_VECTOR(7 downto 0)
    デバッグ用のLED出力。FPGA内部の信号を目視でモニタする場合は,VHDLコードを書き換えて目的とする信号を割り当てる。

Architecture宣言(信号の宣言)

ARCHITECTURE behavior OF array_speaker_driver IS
	signal out_data	: SIG_ARRAY;	-- アレイスピーカへの出力
	signal ch_data 	: SIG_ARRAY; 	-- チャンネルセレクタの出力	
	-- タイミング用信号:
	signal d_clk 	: STD_LOGIC; 	-- driver clock 駆動出力の外部同期用
	signal cnt_d	: STD_LOGIC_VECTOR(7 downto 0); -- d_clk用カウンタ
	signal d_clk_D 	: STD_LOGIC;	-- d_clkを60MHzクロックの1周期だけ遅らせたもの
	signal reset 	: STD_LOGIC;	-- システム全体のリセット信号
    signal Ch_cnt	: STD_LOGIC_VECTOR(4 downto 0); --チャンネルの番号を指定
	signal ch_n		: STD_LOGIC_VECTOR(4 downto 0); --チャンネル数を指定
	signal RD_busy	: STD_LOGIC := '1';		-- Read Busy
	signal RD_busy_D: STD_LOGIC := '1';
	signal OE_busy	: STD_LOGIC := '0'; 	-- Output Enable Busy
	signal RXF_in 	: STD_LOGIC := '1';
	-- 動作のスタートなどに関連する信号:
	signal START_D 	: STD_LOGIC := '1';		-- START_SWを1クロック遅延させた信号
	signal START 	: STD_LOGIC := '1'; 	-- スタート状態を表す
	signal RUN 		: STD_LOGIC := '0'; 	-- 実行状態(RUN)を示す
	-- 動作確認用の信号
	signal STATUS	: STD_LOGIC_VECTOR(7 downto 0);
	CONSTANT ND : integer := 19;

Entity宣言では「外側から見た回路」を定義しました。Architecture宣言では,回路の中身の構造を記述していきます。

ARCHITECTURE behavior OF array_speaker_driver IS
から回路array_speaker_driverの構造(architecture)の記述を開始します。

architecture宣言のはじめの部分で,回路内部で使う信号をsignal宣言で定義します。signal宣言の後のBEGINから最終行のEND behavior;までが,signal宣言で定義された信号とEntity宣言で定義された信号を使って,回路の動作や構造を記述していきます。以下のコードは,BEGINの直後の3行目までを示しています。

BEGIN
drive_clk	<= d_clk;	-- 60MHzを分周して作ったd_clkを外部同期用に出力
reset 	<= RESET_SW; 	-- システムリセット信号をRESET用スイッチにより生成
Ch_n	<= CONV_std_logic_vector(N_CH,5); 	-- N_CHの値をCh_nに設定

これらの回路記述は,「同時処理文」を並べたものになります。同時処理文は,回路内のゲートやレジスタなどの要素の働きや,相互の接続を記述します。なぜ「同時」と呼ぶかというと,回路内の複数の要素は並列に存在し同時に動作するもので,それを記述するための文だからです。

ですから,これらの同時処理文はVHDLコードの中で順番を変えても,表している回路は変わらないことに注意してください。

一方,C/C++言語などでは,コード内の式文は並べた順に処理されていく点が大きく異なります。VHDLやVerilogなどのハードウェア記述言語の初学者が最も苦労するのは,この点ではないかと考えています。

drive_clk <= d_clk;
は「代入文」と呼ばれています。しかし,「d_clkをdrive_clkに接続する」という記述と考えた方が良いでしょう。そして,
reset <= RESET_SW;
Ch_n <= CONV_std_logic_vector(N_CH,5);
も同様に代入文です。Ch_n <= CONV_std_logic_vector(N_CH,5); は,N_CHというデータを5ビットの信号に変換してからCh_nに代入する(あるいは,信号Ch_nにN_CHを5ビットに変換した値を割り当てる)ことを意味している。

以下,延々と同時処理文が続きます。ここでコードを書いていく上で問題となることがあります。順番を気にしなくてよいので,つい,いろいろな箇所に同時処理文を挿入してしまいます。すると,1つの機能,例えばFT232Hとのインタフェースの機能を実現するための要素が,コード内の離れた場所に分散してしまう可能性が出てきます。

この問題は,回路設計の初期の段階で,機能に合わせて回路を分割して記述する,つまり逐次処理記述型のプログラム言語のfunctionのようにすることで解決します。しかし,今回は,トップレベルの回路記述のみで構成しているので,同じ回路機能を実現するための同時処理文がコード内で離れて配置されて読みにくくなっている可能性があります。

分周によりサンプリングクロックを生成

-- FT232からの60MHz信号を分周してd_clkを生成
PROCESS(clk_60MHz,reset) -- 60MHzからdrive_clkを作る
CONSTANT WIDTH : integer := 8;	-- '1'の幅
--CONSTANT ND : integer := 19;	-- 分周比 
BEGIN
	IF(reset ='0')THEN
		cnt_d  &lt;= (others => '0');
		d_clk &lt;='0';
	ELSIF(clk_60MHz'EVENT AND clk_60MHz='1')THEN
		IF(cnt_d &lt; ND)THEN
			cnt_d &lt;= cnt_d + 1;
			IF(cnt_d &lt; WIDTH)THEN
				d_clk &lt;= '1';
			ELSE
				d_clk &lt;= '0';
			END IF;
		ELSE
			cnt_d  &lt;= (others => '0');
		END IF;
	END IF;
END PROCESS;

コメントにあるように,SyncFIFOモードのFT232Hが出力する60MHzのクロック信号を分周して3MHzのクロック信号を生成する部分です。このクロック信号に同期してデータを読み出すことで,スピーカを駆動する信号を発生します。

<続きを読む>

この記述は全体で1つのProcess文を構成しています。Process文の全体で,1つの同時処理文,つまり,1つの回路要素をあらわしています。

一方,Process文の中身は,「逐次処理文」で構成されています。これは手続き処理型のプログラミング言語と同じように,回路の動作を「コードの上から順番に実行される処理として」記述するものです。(順番に実行される要素で回路を生成する,ということではなく,順番に並べた文で回路の動作を記述する,ということです。)

回路の動作は同時並行的です。そして回路は相互接続されたゲートやレジスタで表すことができます。したがって,全てをゲートやレジスタの相互接続を記述する同時処理文で表すことは可能です。しかし,そのような記述は,見ても何をしているか全くわからないものになってしまいます。

逐次処理文による記述は,「こういう条件のときは,こう動作する,別の条件のときは,こう動作する」とか,「まず,このようにして,次はこのようにして・・・」と1ステップずつ考えて回路の動作を書くものです。それにより,記述される回路の動作が,わかりやすくなります。

<閉じる>

チャンネルセレクタと出力レジスタ

チャンネルセレクタ

-- チャンネルセレクタ用カウンタ
PROCESS(clk_60MHz,reset) --Ch counter
BEGIN
	IF(reset='0')THEN
		Ch_cnt &lt;= (others => '0');
	ELSIF(clk_60MHz'EVENT AND clk_60MHz='1') THEN
		--IF(RUN ='1' AND cnt_d &lt;N_CH)THEN
		IF(RD_busy ='1')THEN
			IF(Ch_cnt = N_CH-1)THEN
				Ch_cnt &lt;= (others => '0');
				--Ch_cnt &lt;= Ch_n-1;
			ELSE
				Ch_cnt &lt;= Ch_cnt + 1;
			END IF;
		END IF;
	END IF;
END PROCESS;

-- Channel Selector チャンネルセレクタ
PROCESS(clk_60MHz, reset)
BEGIN
	IF(reset = '0')THEN
		ch_data &lt;= (others => (others => '0'));
		ELSIF(clk_60MHz'EVENT AND clk_60MHz='1')THEN
			--IF(RUN ='1' AND ch_cnt &lt; N_CH AND cnt_d &lt;N_CH)THEN
			IF(RD_busy ='1')THEN
				ch_data(CONV_INTEGER(ch_cnt)) &lt;= data_in;
		END IF;
	END IF;
END PROCESS;

「チャンネルセレクタ」というのは,FT232Hから受け取ったデータをチャンネルに割り当てる回路です。データは,チャンネル0,チャンネル1,チャンネル2,・・・という順番で受信されます。チャンネルセレクタは,このデータをそれぞれのチャンネルに対応した出力レジスタに格納する働きをします。

まず,チャンネルセレクタ用カウンタ信号Ch_cntを生成するProcess文があります。このProcess文の中に出てくる信号ch_dataはSIG_ARRAY型の信号で,ch_data(i)はi番目のチャンネル信号を表します(i = 0~7)。

  • ch_data(CONV_INTEGER(ch_cnt)) <= data_in;
    clk_60MHzの立上りのときRD_busy =’1’であれば,そのときの入力信号data_inがch_dataのch_cnt番目に割り振られる。

出力レジスタ

-- Driver Output 出力用最終段のレジスタ
PROCESS(clk_60MHz, reset)
BEGIN
	IF(reset = '0')THEN
		out_data &lt;= (others => (others => '0'));
		ELSIF(clk_60MHz'EVENT AND clk_60MHz='1')THEN
			IF(cnt_d = 0)THEN
				out_data &lt;= ch_data;
			END IF;
	END IF;
END PROCESS;
drive &lt;= out_data;
  • IF(cnt_d = 0)THEN
    out_data <= ch_data;
    cnt_dは,駆動用クロックを生成するときに使っているカウント信号。このIF構文では,60MHzクロックの立ち上がりのときにcnt_d=0であればout_dataにch_dataが割り当てられる回路が生成される。
  • drive <= out_data;
    out_dataが出力driveに接続される。この式文は同時処理文なので,VHDLコード内のどこに書いてもよいが,見やすくするためout_dataを生成するProcess文の直後に配置してある。

制御信号の発生回路

このシステムでは,FPGAに接続したスイッチにより動作の開始やリセットを制御しています。またFT232Hとデータをやり取りするための制御信号を生成します。これらの制御信号の発生回路について説明します。

スイッチによる制御信号の発生

-- 外部スイッチからクロックに同期したSTART信号を生成
PROCESS(clk_60MHz, reset) 
BEGIN
	IF(reset='0')THEN
		START_D &lt;= '1';			-- システムリセットでSTART_Dを1にセット
	ELSIF(clk_60MHz'EVENT AND clk_60MHz='1')THEN
		START_D &lt;= START_SW;	-- START_Dを外部のSTART_SW信号から生成
	END IF;
END PROCESS;
-- “論理差分”により長い信号からクロック幅のパルスを生成
PROCESS(clk_60MHz, reset)
BEGIN
	IF(reset='0')THEN
		d_clk_D &lt;= '0';		-- システムリセットでd_clk_Dを0にセット
	ELSIF(clk_60MHz'EVENT AND clk_60MHz='1')THEN
		d_clk_D &lt;= d_clk;	-- 1クロック遅れた信号
	END IF;
END PROCESS;
--START &lt;= NOT(d_clk AND NOT(d_clk_D) AND RUN);
START &lt;= '0' WHEN cnt_d = 0 AND RUN = '1' ELSE
         '1';

-- 外部スイッチで動作信号RUNを生成
PROCESS(clk_60MHz,reset)
BEGIN
	IF(reset='0')THEN
		RUN &lt;= '0';
	ELSIF(clk_60MHz'EVENT AND clk_60MHz = '1')THEN
		IF(START_SW = '0')THEN
			RUN &lt;= '1';
		END IF;
	END IF;
END PROCESS;

回路のリセットや動作スタートなどを制御する信号をFPGA外部のスイッチで生成する部分です。

  • START_D <= START_SW;
    クロック立ち上がりで入力信号START_SWを内部信号START_Dに取り込む。信号START_SWはFPGA外部のタクトスイッチを使って発生する。タクトスイッチは,2つの端子が通常は開放つまり切れている状態で,ボタンを押すと短絡される。スイッチを押すことでSTART_Dは0になる。
  • START <= ‘0’ WHEN cnt_d = 0 AND RUN = ‘1’ ELSE ‘1’;
    信号STARTを生成する回路を条件付き代入文で記述している。動作状態(RUN = ‘1’)で駆動用クロックのカウンタcnt_dの値が0になったときのみSTART信号を0にする。つまり,START信号は3MHzの駆動用クロックの周期で0を発生することになる。
  • IF(START_SW = ‘0’)THEN
    START_SWに接続されているタクトスイッチを押すと動作状態RUNが1になる。RUNは次にリセット信号に接続されたタクトスイッチが押されるまで1を取り続ける。

読み出し制御信号RDの発生

FPGAはデータを読み取る際に,読み出し制御信号RDをFT232Hに送信します。読み出し可能な状態を表す信号RD_busyをステートマシンにより生成し,これとFT232Hの内部FIFOの状態を表すフラグ信号RXFからRDを生成します。

ステートマシン
--State transition for RD_busy,RD信号を作るためのステートマシン
PROCESS(clk_60MHz,reset)
BEGIN
	IF(reset='0')THEN
		RD_busy &lt;= '0';
	ELSIF(clk_60MHz'EVENT AND clk_60MHz='1')THEN
		CASE RD_busy is
			WHEN '0' =>
				IF(START = '0' AND RXF = '0')THEN
					RD_busy &lt;= '1';
				ELSE
					RD_busy &lt;= '0';
				END IF;
			WHEN '1' => 
				IF(Ch_cnt = Ch_n-1)THEN
					RD_busy &lt;= '0';
				ELSE
					RD_busy &lt;= '1';
				END IF;
			WHEN OTHERS =>
					RD_busy &lt;='0';
		END CASE;
	END IF;
END PROCESS;

FT232Hからデータを読み込む際に使われるのが,FPGAからFT232Hに送信する制御信号RDとFT232Hが出力するフラグ信号RXFです。

  • RXF
    Read Buffer Fullフラグ。FT232HにはFIFOバッファが内蔵されていて,USBを介してPCが送信するデータは,このFIFOバッファに蓄積される。RXFフラグは,FIFO内に読み出すデータが残っているときに0になるフラグ信号でFT232Hが出力する。
  • RD
    FT232Hからデータを読み出すとき0にするアクティブローの制御信号。FPGAはRXF=0であることを確認したら,RD=0にし,60MHzのクロックに同期してデータを読み込む。

このProcess文は,読み出し中であることを示すRD_Busyを作る回路を生成します。

RD信号の生成
PROCESS(clk_60MHz,reset)
BEGIN
	IF(reset='0')THEN
		RD_busy_D &lt;= '1';
	--ELSIF(clk_60MHz'EVENT AND clk_60MHz='0')THEN
	  ELSIF(clk_60MHz'EVENT AND clk_60MHz='1')THEN
		RD_busy_D &lt;= NOT(RD_busy);
	END IF;
END PROCESS;
-- RD control: RXFとRD_busyからRDを生成
RD &lt;= NOT(NOT(RXF) AND RD_busy)AND RD_busy_D;

RXFとRD_busyから制御信号RDを発生する回路の記述です。RD_busyを1クロック遅らせた信号RD_busy_Dを使っています。これは,データとRD信号のタイミングを合わせるために使っています。動かしてみたときにデータがずれるため,タイミング調整のため,AND RD_busy_Dの部分を追加しました。理論的に考えての調整ではなく,「結果オーライ」で良しとしている部分です。

その他の信号発生回路

OE &lt;=  	NOT RUN; 	-- FT232Hの出力許可信号
--OE &lt;= '0';
WR &lt;= '1';			-- FT232HへのWR,とりあえず常に1に
LED &lt;= NOT STATUS;	-- LEDはLowで点灯するので反転
  • OE <= NOT RUN;
    Output Eneble。FT232Hの出力を許可するアクティブローの制御信号。RUN=1つまり動作状態のとき0にしてFT232Hの出力を許可する。
  • WR <= ‘1’;
    WR(Write)はFPGAがFT232HのFIFOバッファにデータを書き込むときに使うアクティブローの制御信号。今回設計しているシステムではデータはFT232Hから出力されるだけなので,WRは常に1になるようにしている。
  • LED <= NOT STATUS;
    STATUS信号をFPGA基板上の8個のLEDで表示するための記述。MachXO2BB基板上のLEDは接続された信号が‘0’のときに発光する。モニタしたい信号の値が’1’のときにLEDが点灯する方が直感的には望ましいと考えたので,反転して信号LEDに代入している。

STATUSへの内部信号の割り当て

-- STATUS LED表示用の信号
PROCESS(clk_60MHz,reset)
CONSTANT K : integer := 0;
BEGIN
	IF(reset ='0')THEN
		STATUS &lt;= (others =>'0');
		ELSIF(clk_60MHz'EVENT AND clk_60MHz = '1')THEN
			-- 表示が不要な信号はコメントアウト
			--STATUS(7)	&lt;= TXE; 	STATUS(6) &lt;= NOT (WR);
			--STATUS(5)	&lt;= RXF; 	STATUS(4) &lt;= NOT (RD);
			--STATUS(3)	&lt;= OE; 		  
			--STATUS(0)	&lt;= RUN; 
			STATUS(7) &lt;= drive(K)(7); STATUS(6) &lt;= drive(K)(6);
			STATUS(5) &lt;= drive(K)(5); STATUS(4) &lt;= drive(K)(4);
			STATUS(3) &lt;= drive(K)(3); STATUS(2) &lt;= drive(K)(2);
			STATUS(1) &lt;= drive(K)(1); STATUS(0) &lt;= drive(K)(0);	
			--STATUS &lt;= data_in;
	END IF;  

信号STATUSにLEDで表示したい内部信号を割り当てる回路です。ここでは,整数型変数kを指定すると,出力のkチャンネル目のバイトがSTATUSに代入されます。clk_60MHzの立上りに同期して変化するようにしています。単なる代入文だけでも目的は達成するのですが,開発の初期にクロック信号が生成することを確認したかったため,このようにしています。

例えば入力信号の1つをSTATUSの1つのビットに割り当てておきます。その入力端子に取り付けたリード線のもう一方の端子を電源に接触させると対応するLEDが点灯,GNDに接触させたときはLEDは消灯します。リード線の電圧を切り替えてもLEDの点灯状態が変化しない場合はクロックが入力されていないことがわかります。

コメント