#前置き
D/A Converter: DACは音響機器自作が好きな方から組み込みシステム設計者まで幅広く親しまれている機器かと思います。本記事は最近流行しているFPGAを使用してDACを制御することを目的とします。
FPGAでの設計経験があれば,DACを使用するにあたり何のトラブルもありません。DAC ICを使用して回路を設計し,通信プロトコルに則って通信機能を実装するだけでDA変換を行うことが可能だからです。しかしながら,これからシステム自作を始めてみたい方,ハードウェア設計に興味があるソフトウェア開発者の方からすれば,FPGA設計フローは何から始めれば良いのか戸惑うことが多いと思います。本記事にはシステムの最小構成を示しますので,FPGA導入の足がかりになれば幸いです。
#必要なもの
- FPGA搭載ボード
- Basys, Zyboなどの評価ボードが便利
- FPGA設計ツール
- VivadoかQuartus (無償版だとVivadoの方ができることが多いとか)
- DAC IC搭載回路
- 自作してみてください (本記事に回路構成を書いてます)
※FPGAは所詮0と1を出力するだけのものです。DAC ICの性能をフルで引き出したい場合には回路設計(電源や配線)が重要になります。
#なぜFPGAを使うのか
FPGAは並列処理を高速に行うことを得意とするハードウェアで,ハードリアルタイムアプリケーションに適しています。スピーカやロボットを制御する際には多チャンネルのDACを使用しますが,少ないクロックジッタで高速に動作させた方が良い性能を引き出すことができるのは想像に難くないと思います。
詳しく知りたい方はこちらを読んでみてください。
そろそろプログラマーもFPGAを触ってみよう!
#回路を設計しよう
ソフトウェア開発者にとって回路設計は経験の少ない部分ではないでしょうか。FPGA搭載システムでは高周波信号を取り扱うため,ノイズが大きな問題になります。ただ電気が通じていれば機能が実現するものではありません。また,FPGAからDAC ICに出力参照値を渡す方法についても,メモリアクセス等とは異なるものがあります。
FPGAからDAC等のICを制御するために,SPIやI2Cといったデジタル通信規格が整備されています。ICの仕様書を確認して,タイムチャートを解読して通信器を実装することになります(後述します)。そのため,回路にはFPGAと通信するためのポートを設置する必要があります。SPIやI2Cでは4線あれば通信ができます。
この4線のデジタル信号を回路に伝達するために,絶縁パルストランスを使用します。FPGAは高速でスイッチングを行なっていますので,DACやADCがFPGAの近くにいる場合やグランド(回路の基準電圧点)を共有する場合にはノイズの原因となります。また,FPGAを用いて大電力系統を制御する場合には,系統からの電力が流れ込んだ場合にFPGAが破損する恐れがあります。そのため,古くはフォトカプラを用いてデジタルとアナログを分離していました。電源も絶縁型電源を使用します。近年ではAnalog DevicesのiCoupler技術による高速なデジタルアイソレータが登場しており,高速なパルストランスが実現しています。
デジタル信号をDAC ICに渡すことができれば,ICからアナログ信号が出力されます。ただし,DACは構造上の制約から大きな電力を扱うことはできません。また,代表的なDA変換方式であるR-2Rラダー抵抗型では,分圧によって階段状電圧を発生させているため,出力インピーダンスを持っています。そこで,インピーダンス変換の意味も含めて電力増幅器を出力段に設置する必要があります。
信号の流れは次の様になります。
※FPGA上で取り扱う数値形式によってエンコーダを設計する必要があります。
※FPGAが高速に動作していても,DAC ICの応答が遅ければ意味がありません。今回の用途におけるFPGAはあくまで周辺回路の性能を引き出すためのデバイスなので,システムの性能を引き上げたければ回路設計が非常に重要になります。
別件ですが,自作R-2Rラダー抵抗型DACを作成してみると,面白い現象が見受けられます。電圧参照値変更時に,ヒゲが発生します。これはグリッチと呼ばれる現象なのですが,信号の僅かな伝送遅れによって生じるものです(製品化されているDAC ICはグリッジ調整が行われています)。高周波信号の取り扱いでは僅かなジッタでも問題になることを念頭において設計を行う必要があります。
最後に,私が設計に使用しているICを紹介します。
機能 | IC | ベンダー |
---|---|---|
絶縁型DC/DC電源 | MCWI03-12D12 | Minmax Technology |
DAC | AD5542A | Analog Devices |
絶縁パルストランス | ADUM140D | Analog Devices |
電力増幅器 | ADA4898-2 | Analog Devices |
※電力増幅用にオペアンプADA4898を使用していますが,ゲイン遮断周波数が非常に高いため,負帰還回路の配線が長いと発振します。
#FPGA上に通信器を実装しよう
FPGAとDACの通信をサポートするためのドライバを作ります。コードを書く前に,ライブラリを呼び出しておきます。
library IEEE;
use ieee.std_logic_1164.all;
use ieee.std_logic_signed.all;
use ieee.std_logic_arith.all;
Linuxではアプリケーションとハードウェアの通信のためにカーネルが仕事をしますが,同様の機能を実装する必要があります。FPGAではカーネルのデバイスドライバにあたる部分をスレーブモジュールといった形で実装できます。まずはFPGAの上位モジュールの指令を受けて動作するように宣言します。
entity ad5542a is
port(
clk: in std_logic;
sw_clr: in std_logic;
cnv_st: in std_logic;
dref: in std_logic_vector(15 downto 0);
--To AD5542
ncs: out std_logic;
sclk: out std_logic;
dout: out std_logic;
nclr: out std_logic
);
end ad5542a;
※このモジュールはエンコードされた16bit信号を受け取って動作します。そのため,エンコーダを別途設計する必要があります。
通信器に関しては,ステートマシンを使用して実装しています。ハードウェア記述とソフトウェア記述の大きな違いは,コードが上から実行されないことです。それぞれの役割を持った回路が協調して機能を実現します。回路記述においては各ステートにおける動作と例外処理をきっちりと書く必要があります。今回使用しているVHDLはverilogと比較して厳密記述が必要なため,FPGA入門には適した言語だと思います(慣れると記述が長く感じます)。
下の記述ではいくつかの回路ブロックが確認できます。それぞれはSCLK生成,ステートマシンの遷移,各ステートにおける動作を記述した回路になります。
Architecture RTL of ad5542a is
signal ncs_reg: std_logic :='0';
signal nclr_reg: std_logic :='0';
signal sclk_reg: std_logic :='0';
signal pclk: std_logic :='0';
constant pclk_cnt_bit: integer := 4;
constant pclk_cnt_max: std_logic_vector(pclk_cnt_bit -1 downto 0) :=x"4";
signal pclk_cnt: std_logic_vector(pclk_cnt_bit -1 downto 0) :=(others=>'0');
constant dac_bit: integer :=16;
constant dac_bit_bin: std_logic_vector(4 downto 0) := "10000"; --16
constant dac_cnt_zero: std_logic_vector(4 downto 0) := "00000"; --0
signal dac_data: std_logic_vector(dac_bit-1 downto 0) :=(others=>'0');
signal dac_stream: std_logic_vector(dac_bit-1 downto 0) :=(others=>'0');
signal dac_streamcnt: std_logic_vector(4 downto 0) := (others=>'0');
signal dac_streamend: std_logic :='0';
signal dac_start: std_logic :='0';
signal dac_loadcnt: std_logic :='0';
signal dac_loadend: std_logic :='0';
--state machine
signal dac_state: std_logic_vector(1 downto 0) :=(others=>'0');
--state 0: wait;
--state 1: change a clock source from main one to divided one
--state 2: data input;
--state 3: data load;
begin
--to ad5542a
ncs <= ncs_reg;
sclk <= sclk_reg;
dout <= dac_stream(dac_bit-1);
nclr <= nclr_reg;
--From master system
dac_start <= cnv_st;
dac_data <= dref;
nclr_reg <= sw_clr;
--Clock divider
process(clk) begin
if clk'event and clk='1' then
--Timing manager for clock generator
if pclk_cnt=pclk_cnt_max then pclk_cnt <= (others=>'0');
else pclk_cnt <= pclk_cnt+'1'; end if;
--Clock generator
if pclk_cnt=pclk_cnt_max then pclk <= not pclk;
else pclk <= pclk; end if;
end if;
end process;
--State machine
process(clk) begin
if clk'event and clk='1' then
case dac_state is
when "00" =>
if dac_start='1' then dac_state <="01";
else dac_state <= dac_state; end if;
when "01" =>
if pclk_cnt=pclk_cnt_max and pclk='1' then dac_state <= "10";
else dac_state <= dac_state; end if;
when "10" =>
if dac_streamend='1' then dac_state <="11";
else dac_state <= dac_state; end if;
when "11" =>
if dac_loadend='1' then dac_state <="00";
else dac_state <= dac_state; end if;
when others =>
dac_state <= dac_state;
end case;
end if;
end process;
--Communicator
process(clk) begin
if clk'event and clk='1' then
if pclk_cnt=pclk_cnt_max and pclk='1' then
--Data streaming
if dac_state="10" then
--Stream Data
if dac_streamcnt=dac_cnt_zero then dac_stream <= dac_data;
else dac_stream <= dac_stream(dac_bit-2 downto 0) & '0'; end if;
--Stream Count
if dac_streamcnt=dac_bit_bin then dac_streamcnt<=(others=>'0');
else dac_streamcnt <= dac_streamcnt+'1'; end if;
--Stream End Flag
if dac_streamcnt=dac_bit_bin then dac_streamend<='1';
else dac_streamend <= '0'; end if;
else
dac_stream <= (others=>'0');
dac_streamcnt <= (others=>'0');
dac_streamend <= '0';
end if;
--Load data
if dac_state="11" then
if dac_loadcnt='0' then dac_loadcnt <= '1';
else dac_loadcnt <= dac_loadcnt; end if;
if dac_loadcnt='0' then ncs_reg <= '1'; --Load the data
else ncs_reg <= '0'; end if;
if dac_loadcnt='1' then dac_loadend <= '1';
else dac_loadend <= '0'; end if;
else
ncs_reg <= '0';
dac_loadcnt <= '0';
dac_loadend <= '0';
end if;
end if;
end if;
end process;
sclk_reg <= pclk when dac_state="10" else '0';
end RTL;
module ad5542a(clk,ena,cnv_st,dref,nsc,sclk,dout,nclr);
input clk,ena,cnv_st;
input [15:0] dref;
output nsc,sclk,dout,nclr;
reg pclk; initial pclk=0;
reg [2:0] pcnt; initial pcnt=3'b0;
parameter pmax=3'b100;
always@(posedge clk)
begin
pcnt<=(pcnt==pmax)? 3'b0:pcnt+3'b1;
pclk<=(pcnt==pmax)? ~pclk:pclk;
end
reg [1:0] state; initial state=2'b0;
reg [15:0] stdat; initial stdat=16'b0;
reg [4:0] stcnt; initial stcnt=5'b0;
reg stend; initial stend=1'b0;
parameter [4:0] stmax=5'b10000;
reg [1:0] ldcnt; initial ldcnt=2'b0;
reg ldend; initial ldend=1'b0;
reg ldsig; initial ldsig=1'b0;
parameter [1:0] ldmax=2'b01;
assign nsc=ldsig;
assign sclk=(state==2'b10)? pclk:1'b0;
assign dout=stdat[15];
assign nclr=ena;
always@(posedge clk)
begin
case(state)
2'b00: state<=(ena==1'b1&cnv_st==1'b1)? state+2'b1:state;
2'b01: state<=(pcnt==pmax&pclk==1'b1)? state+2'b1:state;
2'b10: state<=(stend==1'b1)? state+2'b1:state;
2'b11: state<=(ldend==1'b1)? state+2'b1:state;
default: state=2'b00;
endcase
end
always@(posedge clk)
begin
if(pcnt==pmax&pclk==1'b1)
begin
if(state==2'b10)
begin
stcnt<=(stcnt==stmax)? stmax:stcnt+5'b1;
stend<=(stcnt==stmax)? 1'b1:1'b0;
stdat<=(stcnt==5'b0)? dref:{stdat[14:0],1'b0};
end
else begin stcnt<=5'b0; stdat<=16'b0; stend<=1'b0; end
if(state==2'b11)
begin
ldcnt<=(ldcnt==ldmax)? ldmax:ldcnt+2'b1;
ldsig<=(ldcnt!=ldmax)? 1'b1:1'b0;
ldend<=(ldcnt==ldmax)? 1'b1:1'b0;
end
else begin ldcnt<=2'b0; ldsig<=1'b0; ldend<=1'b0; end
end
end
endmodule
以上の回路によりDACの制御が可能になります。ZynqやCyclone Vなどを使ってソフトウェアアプリケーションから高速動作するDACへのアクセスをすると,アプリケーションの幅が広がるのではないでしょうか。