概要
FPGAとラズピコをSPI通信で連携して、ラズピコからFPGAのレジスタ設定を書き換えます。
FPGAとRaspberryPi picoWでSPI通信(その①ラズピコのSPIマスター)
の記事でラズピコのSPI親分側を用意しましたので、FPGAでSPIの受け回路を作成します。
条件
- FPGAクロックは50MHzです(SPIクロック1MHzより十分速い)
- VHDLでコーディングします
- FPGAのグローバルクロックに同期を原則としていますが、例外としてMISO出力だけSPIクロックの立下りエッジ同期のFFを最終段に1つだけ入れています
- MOSI出力FFは非同期渡しにはなりますが、SPIクロックの立下り時点でFF入力は確実に安定なので2段受けの必要はありません
IOタイミング
- SPIのデータフォーマットの考え方については前の記事で触れたものとします
- FPGA内部インタフェースは、アドレス8bit、データ16bitの単純なバスとします
- リード時はアドレスとリードイネーブルを出力し、リードイネーブルの最終サイクルで受け取ったデータをSPIに返します
- ライト時は、SPI受信データをラッチした次のサイクルでライトイネーブルを1サイクルだけアサートします
※ Wishboneバスにした方がIPとの親和性が高くなりますが、ここでWishboneバスにすると初心者殺しになるので、単純ばバスリードライトにしています。「Wishboneバスにしたいよ!」という場合でも、本モジュールの後段にWishboneバスマスター変換モジュールを接続する方法もとれますので、本モジュールをそのまま使えます。(たぶん)
RTL図
同期化FFとエッジ検出についてはFPGA入力信号のエッジ検出の記事で紹介した、SYNCRO_DFF.vhdをインスタンスして使っています。
クロックの立ち上がりエッジパルスでシリアルデータ入力をラッチすると共にカウンタを動作させます。
所定のカウンタ値でアドレス、コマンド、書き込みデータをシリパラ用FFからラッチし、コマンドに応じてリードないしはライト動作を行います。
VHDLコード
library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_arith.all;
use ieee.std_logic_unsigned.all;
entity SPI_IF is
generic(
SPI_CMD_WRITE: std_logic_vector(3 downto 0) := x"6"; --書き込みコマンド
SPI_CMD_READ : std_logic_vector(3 downto 0) := x"9" --読み出しコマンド
);
Port(
i_CLK :in std_logic;
i_RST_p :in std_logic;
i_SPI_SCK :in std_logic;
i_SPI_MOSI :in std_logic;
i_SPI_CSn :in std_logic;
o_SPI_MISO :out std_logic;
o_WR_ENA_p :out std_logic;
o_RD_ENA_p :out std_logic;
o_ADDRESS :out std_logic_vector(7 downto 0);
o_WR_DATA :out std_logic_vector(15 downto 0);
i_ACK_DATA :in std_logic_vector(15 downto 0)
);
end entity;
architecture RTL of SPI_IF is
signal spi_sck_rise :std_logic;
signal spi_sck_fall :std_logic;
signal spi_csn_sync :std_logic;
signal spi_dat_sync :std_logic;
signal sck_rise_dly :std_logic;
signal parallel_mosi:std_logic_vector(15 downto 0);
constant COUNT_END :integer :=32;
signal count :integer range 0 to COUNT_END := 0;
signal get_address :std_logic;
signal get_command :std_logic;
signal get_rx_data :std_logic;
signal reg_adr :std_logic_vector( 7 downto 0);
signal reg_cmd :std_logic_vector( 3 downto 0);
signal reg_dat :std_logic_vector(15 downto 0);
signal reg_cmd_ena :std_logic;
signal reg_dat_ena :std_logic;
signal cmd_fix_wr :std_logic;
signal cmd_fix_rd :std_logic;
signal reserve_wr :std_logic;
signal wr_ena :std_logic;
signal rd_ena :std_logic;
signal send_start :std_logic;
signal send_finish :std_logic;
signal send_ena :std_logic;
signal shift_out :std_logic_vector(15 downto 0);
signal miso :std_logic;
begin
--SPIクロック同期化
u_SYNCRO_DFF_SCK:entity work.SYNCRO_DFF
port map
(i_CLK =>i_CLK
,i_RST_p =>i_RST_p
,i_PORT =>i_SPI_SCK
,o_SYNCRO =>open
,o_RISE_p =>spi_sck_rise
,o_FALL_p =>spi_sck_fall
);
--SPIイネーブル同期化
u_SYNCRO_DFF_CSN:entity work.SYNCRO_DFF
port map
(i_CLK =>i_CLK
,i_RST_p =>i_RST_p
,i_PORT =>i_SPI_CSn
,o_SYNCRO =>spi_csn_sync
,o_RISE_p =>open
,o_FALL_p =>open
);
--SPIデータ同期化
u_SYNCRO_DFF_DAT:entity work.SYNCRO_DFF
port map
(i_CLK =>i_CLK
,i_RST_p =>i_RST_p
,i_PORT =>i_SPI_MOSI
,o_SYNCRO =>spi_dat_sync
,o_RISE_p =>open
,o_FALL_p =>open
);
--SPI_MOSIをSPIクロックの立ち上がりでシリパラする
process(i_CLK,i_RST_p)begin
if(i_RST_p='1')then
parallel_mosi <=(others=>'0');
elsif(rising_edge(i_CLK))then
if(spi_sck_rise='1')then
parallel_mosi(parallel_mosi'high downto 1) <= parallel_mosi(parallel_mosi'high-1 downto 0);
parallel_mosi(0) <=spi_dat_sync;
end if;
end if;
end process;
--SPIクロックエッジパルス(シリパラの更新タイミング合わせるD-FF)
process(i_CLK,i_RST_p)begin
if(i_RST_p='1')then
sck_rise_dly <='0';
elsif(rising_edge(i_CLK))then
sck_rise_dly <=spi_sck_rise;
end if;
end process;
--受信したBit数を把握するカウンタ
process(i_CLK,i_RST_p)begin
if(i_RST_p='1')then
count <=0;
elsif(rising_edge(i_CLK))then
if(spi_csn_sync='1')then
count <=0;--チップセレクトネゲートでクリア
elsif(spi_sck_rise='1')then
if(count=COUNT_END)then
count <= count;--ラップアラウンド防止
else
count <= count+1;
end if;
end if;
end if;
end process;
--SPIクロックエッジに合わせてシリパラデータをラッチするトリガ
get_address <= '1' when((count= 8)and(sck_rise_dly='1'))else '0';--アドレスラッチするトリガ
get_command <= '1' when((count=12)and(sck_rise_dly='1'))else '0';--コマンドラッチするトリガ
get_rx_data <= '1' when((count=32)and(sck_rise_dly='1'))else '0';--書込みデータラッチするトリガ
--SPIで受信したアドレス、コマンド、データをラッチ
process(i_CLK,i_RST_p)begin
if(i_RST_p='1')then
reg_adr <=(others=>'0');
reg_cmd <=(others=>'0');
reg_dat <=(others=>'0');
reg_cmd_ena <='0';
reg_dat_ena <='0';
elsif(rising_edge(i_CLK))then
if(get_address='1')then
reg_adr <= parallel_mosi(7 downto 0);
end if;
if(get_command='1')then
reg_cmd <= parallel_mosi(3 downto 0);
end if;
if(get_rx_data='1')then
reg_dat <= parallel_mosi(15 downto 0);
end if;
reg_cmd_ena <=get_command;
reg_dat_ena <=get_rx_data;
end if;
end process;
--ラッチしたコマンドが定義と一致している判定
cmd_fix_wr<= '1' when(reg_cmd=SPI_CMD_WRITE)else '0';
cmd_fix_rd<= '1' when(reg_cmd=SPI_CMD_READ )else '0';
-----------------------------------------------------------------------------------
--リード処理
-----------------------------------------------------------------------------------
--SPIリード応答データを送りす期間のイネーブルをセットクリアするトリガ
send_start <= '1' when((spi_sck_fall='1')and(count=16)and(cmd_fix_rd='1'))else '0';
send_finish <= '1' when((spi_sck_fall='1')and(count=31))else '0';
--リードイネーブルと応答データの送信パラシリイネーブル
process(i_CLK,i_RST_p)begin
if(i_RST_p='1')then
rd_ena <='0';
send_ena <='0';
shift_out <=(others=>'0');
elsif(rising_edge(i_CLK))then
--リードイネーブル
if(reg_cmd_ena='1')then
rd_ena<=cmd_fix_rd;--コマンド確定タイミングでリードコマンドと一致していたらHになる
elsif(send_start='1')then
rd_ena<='0';--送信開始したらネゲート
end if;
--送信パラシリシフトレジスタの動作イネーブル
if(send_start='1')then
send_ena <='1';
elsif(send_finish='1')then
send_ena <='0';
end if;
--送信パラシリシフトレジスタ
if(rd_ena='1')then
shift_out <= i_ACK_DATA;
elsif((send_ena='1')and(spi_sck_rise='1'))then
shift_out <= shift_out(14 downto 0) & '0';
end if;
end if;
end process;
-----------------------------------------------------------------------------------
--ライト処理
-----------------------------------------------------------------------------------
process(i_CLK,i_RST_p)begin
if(i_RST_p='1')then
reserve_wr<='0';
wr_ena<='0';
elsif(rising_edge(i_CLK))then
--ライト予約:コマンド確定タイミングでアドレス一致をラッチしてwr_enaでクリア
if(reg_cmd_ena='1')then
reserve_wr<=cmd_fix_wr;
elsif(wr_ena='1')then
reserve_wr<='0';
end if;
--ライトイネーブル:データ受信完了時にリザーブを出す
if(reg_dat_ena='1')then
wr_ena<=reserve_wr;
else
wr_ena<='0';
end if;
end if;
end process;
--------------------------------
--SPI応答はSPIクロックの立下り同期で出力
process(i_SPI_SCK,i_RST_p)begin
if(i_RST_p='1')then
miso <='0';
elsif(falling_edge(i_SPI_SCK))then
miso <=shift_out(15);
end if;
end process;
--出力ポート接続
o_SPI_MISO <=miso;
o_ADDRESS <=reg_adr;
o_WR_DATA <=reg_dat;
o_WR_ENA_p <=wr_ena;
o_RD_ENA_p <=rd_ena;
end architecture;
シミュレーション波形
SPIクロック周波数は、実際に接続するラズピコでは1MHzということにしていますが、FPGA設計的にはもっと速くてもOKなので、シミュレーションではクロック周波数を上げています。
ライトアクセス
リードアクセス
余談
ステートマシンを使ってコーディンしてしまった方が、コードだけ見た際の可読性は高いです。
今回は「RTL図を描いて検討して、VHDLに落とし込んだらこんな感じ」ということで紹介しました。
筆者も最近ではRTL図をわざわざ書くことは少ないので、ステートマシンで簡単に書いてしまっています。
おわりに
SPI受信してレジスタに読み書きする信号へ変換できましたので、次回はレジスタの実装例を紹介できればと思います。