最上位階層について
VHDLやVerilogコードを論理合成する際、多くの場合はコードに階層があります。
最上位階層は0と1の論理的な世界とリアルな世界の境界になるため、下位階層のモジュールとはちょっと違ったことに注意する必要があります。
私が考える最上位階層で気をつける点は以下になります。
- リアル世界との接続
- 非同期対策
- デバイス依存のプリミティブの対応
#####1.リアル世界との接続
回路図との対応やピンアサインが分かりやすいようにしておくと便利だと思います。
ピンアサインやILAなどのデバッグ時に少し分かりやすくなると思います。
#####2.非同期対策
FFの動作クロックと違うクロックが入力されるとメタステーブルという状態が生じて狙い通り動かないことがあります。メタステーブルが起きると信号の取りこぼしや余分に数クロック多く信号を拾ってしまうなどの現象が生じます。
前回記事のコメントで教えていただいたリンクを紹介します。
#####3.デバイス依存のプリミティブの対応
プリミティブとはデバイスの構成部品のことと思ってもらってよいです。
たとえば、XilinxでいうところのI/Oバッファ(IOBUF、BUFGなど)やPLLなど、デバイスやメーカーごとに特殊な記述が必要なものです。
この辺の記述を最上位階層でまとめておけば、下位階層のモジュールはデバイスやメーカーに依存することがなくなります。
※LUTやFFなどもプリミティブですが論理合成で解釈されるようなものはこの件では例外です。
とはいえ、あくまで理想であって実際にはモジュール内で割り算器やメーカーIPのFIFOなどを使ったほうが楽な場面もあります。そういった際、私の場合はデバイスやメーカーに依存していることが分かるようにモジュール名などを工夫して注意喚起を行うようにしています。
実際の例
前項を踏まえて実際に記述したコードを見ていきます。
ソースコードはこれです。
https://github.com/x7700jp/x7700jp_codes/blob/master/rc_robot/TE0726_TOP.vhd
###■ リアル世界との接続について
Zynqberry(TE0726)という既存のSoCボードを利用していてentity文はなるべく回路図どおりの名称としてます。iBUTTON,iRCINはGPIOを振り替えてます。この辺はコメントがあったほうが良かったかな。
entity TE0726_TOP is
port (
-- bank34
iBUTTON : in std_logic;
iRCIN : in std_logic_vector( 1 downto 0);
GPIO : inout std_logic_vector(25 downto 3);
PUDC : inout std_logic;
CSI_C_P : out std_logic;
CSI_C_N : out std_logic;
CSI_D_P : out std_logic_vector( 1 downto 0);
CSI_D_N : out std_logic_vector( 1 downto 0);
CSI_D0_R_P : out std_logic;
CSI_D0_R_N : out std_logic;
PWM_L : inout std_logic;
PWM_R : inout std_logic;
CEC_B : inout std_logic;
HDMI_TXC_P : out std_logic;
HDMI_TXC_N : out std_logic;
HDMI_TX_P : out std_logic_vector( 2 downto 0);
HDMI_TX_N : out std_logic_vector( 2 downto 0);
--bank35
DSI_C_R_P : out std_logic;
DSI_C_R_N : out std_logic;
DSI_D_R_P : out std_logic_vector( 1 downto 0);
DSI_D_R_N : out std_logic_vector( 1 downto 0);
DSI_XA : out std_logic;
DSI_XB : out std_logic;
--PS
MIO : inout std_logic_vector(53 downto 0);
PS_CLK : inout std_logic;
PS_SRSTB : inout std_logic;
PS_PORB : inout std_logic;
-- PS_DDR
DDR_VRP : inout std_logic;
DDR_VRN : inout std_logic;
DDR_WEB : inout std_logic;
DDR_RAS_n : inout std_logic;
DDR_ODT : inout std_logic;
DDR_DRSTB : inout std_logic;
DDR_DQS : inout std_logic_vector( 1 downto 0);
DDR_DQS_n : inout std_logic_vector( 1 downto 0);
DDR_DQ : inout std_logic_vector(15 downto 0);
DDR_DM : inout std_logic_vector( 1 downto 0);
DDR_CS_n : inout std_logic;
DDR_CKE : inout std_logic;
DDR_Clk : inout std_logic;
DDR_Clk_n : inout std_logic;
DDR_CAS_n : inout std_logic;
DDR_BankAddr : inout std_logic_vector(2 downto 0);
DDR_Addr : inout std_logic_vector(14 downto 0)
);
end TE0726_TOP;
...
コーディングスタイルというかクセで内部の信号とI/Oの接続はなるべく文末にまとめて書くようにしてます。以下はその抜粋です。
...
-- I/O
-- bank34
sLED(0) <= sGPIO20M( 4);
sLED(1) <= sGPIO20M( 5);
sLED(2) <= sGPIO20M( 6);
sLED(3) <= sGPIO20M( 7);
sLED(4) <= sGPIO20M( 8);
sLED(5) <= sGPIO20M( 9);
sLED(6) <= sGPIO20M(10);
sLED(7) <= sGPI(0);
-- GPIO( 2) <= sLED(0);
GPIO( 3) <= sLED(1);
GPIO( 4) <= sLED(2);
GPIO( 5) <= rSERVO( 6); --
GPIO( 6) <= rSERVO( 7); --
GPIO( 7) <= rSERVO( 5); --
GPIO( 8) <= rSERVO( 4); --
GPIO( 9) <= rSERVO( 1); --
GPIO(10) <= rSERVO( 0); --
GPIO(11) <= rSERVO( 3); sGPI(1) <= '0';--
GPIO(12) <= rSERVO( 8); --
GPIO(13) <= rSERVO( 9); --
GPIO(14) <= sLED(3);
GPIO(15) <= sLED(4);
GPIO(16) <= rSERVO(11); sGPI(2) <= '0';--
GPIO(17) <= sLED(5);
GPIO(18) <= rSERVO(14);
GPIO(19) <= rSERVO(10); --
GPIO(20) <= rSERVO(13); --
GPIO(21) <= rSERVO(15); --
GPIO(22) <= sLED(7);
GPIO(23) <= rSERVO( 2); --
GPIO(24) <= rSERVO(12); --
GPIO(25) <= sLED(6);
sGPI(0) <= rBUTTON(1);
PUDC <= 'Z';
PR_CSI_C : OBUFTDS port map(O => CSI_C_P , OB => CSI_C_N , I => '0' , T => '1');
PR_CSI_D0 : OBUFTDS port map(O => CSI_D_P(0) , OB => CSI_D_N(0) , I => '0' , T => '1');
PR_CSI_D1 : OBUFTDS port map(O => CSI_D_P(1) , OB => CSI_D_N(1) , I => '0' , T => '1');
CSI_D0_R_P <= 'Z'; CSI_D0_R_N <= 'Z';
PWM_L <= 'Z';
PWM_R <= 'Z';
CEC_B <= 'Z';
PR_HDMI_TX1 : OBUFTDS port map(O => HDMI_TX_P(1) , OB => HDMI_TX_N(1) , I => '0' , T => '1');
PR_HDMI_TX2 : OBUFTDS port map(O => HDMI_TX_P(2) , OB => HDMI_TX_N(2) , I => '0' , T => '1');
HDMI_TXC_P <= 'Z'; HDMI_TXC_N <= 'Z';
HDMI_TX_P(0) <= 'Z'; HDMI_TX_N(0) <= 'Z';
--bank35
DSI_C_R_P <= 'Z'; DSI_C_R_N <= 'Z';
DSI_D_R_P(0) <= 'Z'; DSI_D_R_N(0) <= 'Z';
DSI_D_R_P(1) <= 'Z'; DSI_D_R_N(1) <= 'Z';
DSI_XA <= 'Z';
DSI_XB <= 'Z';
...
・・・あらためて自分のコードをレビューすると大変お粗末。
GPIO(x)ではボードのピンヘッダの入出力ピンを割り当ててます。未使用ポートをハイインピーダンスにしてます。
ちなみにOBUFTDSはZynq-7000のプリミティブでトライステートの差動出力バッファです。
###■ 非同期対策(メタステーブル)
メタステーブル対策が必要な信号はiRCIN(受信RC信号)です。
iRCINの信号を前回のTKRCVで使います。TKRCVはsCLK20Mで動くのでsCLK20Mで動作するFF2段で対策した信号を前回のTKRCVに入力します。
また、バス信号の非同期の問題は特定のbitだけ遅れるなどの問題があります。バス信号が壊れるとRCサーボが誤動作しかねないので対策が必要です。
対策としては非同期FIFOでバッファすること。CLK_CHGの中身はVivadoのFIFO Generatorで作った非同期FIFOです。sPS_CLKのほうがsCLK20Mよりレートが早いのでバッファオーバーしますが、取りこぼしは問題にならないので許容できます。
-- signal宣言部
type tRCIN_ARRY is array (0 to 1) of std_logic_vector(1 downto 0);
constant cRCIN : tRCIN_ARRY := (others =>(others => '0'));
signal rRCIN : tRCIN_ARRY := cRCIN;
...
P_LT : process(sCLK20M) begin
if (sCLK20M'event and sCLK20M = '1') then
if (gRST = '1') then
...
rRCIN <= cRCIN;
else
...
rRCIN <= rRCIN(0) & iRCIN; -- 非同期対策
end if;
end if;
end process;
...
-- クロック乗り換え
U_IP1 : CLK_CHG
port map(
rst => gRST ,
wr_clk => sCLK_PS ,
rd_clk => sCLK20M ,
din => sGPIO ,
wr_en => '1' ,
rd_en => '1' ,
dout => sGPIO20M ,
full => open ,
empty => open
);
...
###■ デバイス依存のプリミティブの対応
今回の例でデバイス依存があるプリミティブはクロック生成、前述している差動出力バッファくらいです。
CLK_GEN20MはXilinxのClocking Wizardです。
...
-- クロック生成
U_IP2 : CLK_GEN20M
port map(
clk_in1 => sCLK_PS , -- Clock in ports
clk_out1 => sCLK20M , -- Clock out ports
reset => gRST , -- Status and control signals
locked => sLOCKED
);
...
クロック関係はデバイス固有であることが多いので分離しておいたほう良いでしょう。
ちなみに今回使っているTKSERVO、TKRCVはLatticeデバイスで使うために作ったものを流用したもので、変更無く使いまわせています。
###■ IP Integratorラッパーのインスタンシェート
Vivado IP Integratorで済めばラッパーを最上位として使えますが(Xilinxはそれを推奨している?)自作したコードがAXIならともかく、そうでないとかえって面倒です。なのでラッパーをインスタンシェートしてしまいました。
MIO(x)が歯抜けなのはEntityを回路図にあわせた結果です。
...
-- DUT
U_DUT : zsys_wrapper
port map(
DDR_addr => DDR_Addr ,
DDR_ba => DDR_BankAddr ,
DDR_cas_n => DDR_CAS_n ,
DDR_ck_n => DDR_Clk_n ,
DDR_ck_p => DDR_Clk ,
DDR_cke => DDR_CKE ,
DDR_cs_n => DDR_CS_n ,
DDR_dm => DDR_DM ,
DDR_dq => DDR_DQ ,
DDR_dqs_n => DDR_DQS_n ,
DDR_dqs_p => DDR_DQS ,
DDR_odt => DDR_ODT ,
DDR_ras_n => DDR_RAS_n ,
DDR_reset_n => DDR_DRSTB ,
DDR_we_n => DDR_WEB ,
FIXED_IO_ddr_vrn => DDR_VRN ,
FIXED_IO_ddr_vrp => DDR_VRP ,
FIXED_IO_mio(15 downto 0) => MIO(15 downto 0) ,
FIXED_IO_mio(27 downto 16) => MIO(39 downto 28) ,
FIXED_IO_mio(29 downto 28) => MIO(49 downto 48) ,
FIXED_IO_mio(31 downto 30) => MIO(53 downto 52) ,
FIXED_IO_ps_clk => PS_CLK ,
FIXED_IO_ps_porb => PS_PORB ,
FIXED_IO_ps_srstb => PS_SRSTB ,
oAXI_GPIO0_TRI_O => sGPIO ,
iAXI_GPIO1_TRI_I => sGPI ,
iAXI_GPIO2_TRI_I => sRCV4 ,
oCLK => sCLK_PS ,
oxRST(0) => sxRST_PS
);
...
ラッパーを改造するという方法もありますがやめたほうがいいでしょう。IP Integratorを更新する都度、改造部を手作業で直す必要がでてくるので。
#雑記
3回に分けてZYNQでロボットを動かした際に用いたVHDL関係の紹介をしました。
公開用にコードをきれいにしたりとかそういうの一切なしの素のコードです。
ぶっちゃけ指摘されるまで非同期対策も施していないような代物でした。
(実動作的にはPSからのポーリングは頻度が低いので非同期の悪影響はないのですが、かっこ悪いので直しました)
Webを情報源としているとVHDLの情報については理論的なものはあるのですが、実際例というのがあまり見当たらないと思い稚拙であることを自覚しつつも僭越ながら公開することとしました。
早速コメントをいただけ公開してよかったと思います。
今後はXilinx以上に情報の少ないLatticeのMachXO2を使って作ったRCアンプについて大胆公開しようかなと思います。
秋月でもMachXO2-256の販売始まったし、アマチュア向けにも良いと思うのですがいまいち盛り上がってないのが気がかりですから。
では。