FPGAとRaspberryPi picoWでSPI通信（その②FPGAのSPI受信回路）

# 概要
FPGAとラズピコをSPI通信で連携して、ラズピコからFPGAのレジスタ設定を書き換えます。
[FPGAとRaspberryPi picoWでSPI通信（その①ラズピコのSPIマスター）
](https://qiita.com/takeru0x5569/items/be7e4590c8e0e901d113)の記事でラズピコのSPI親分側を用意しましたので、FPGAでSPIの受け回路を作成します。

# 条件
* FPGAクロックは50MHzです(SPIクロック1MHzより十分速い)
* VHDLでコーディングします
* FPGAのグローバルクロックに同期を原則としていますが、例外としてMISO出力だけSPIクロックの立下りエッジ同期のFFを最終段に1つだけ入れています
* MOSI出力FFは非同期渡しにはなりますが、SPIクロックの立下り時点でFF入力は確実に安定なので２段受けの必要はありません

# IOタイミング
* SPIのデータフォーマットの考え方については[前の記事](https://qiita.com/takeru0x5569/items/be7e4590c8e0e901d113)で触れたものとします
* FPGA内部インタフェースは、アドレス８bit、データ16bitの単純なバスとします
* リード時はアドレスとリードイネーブルを出力し、リードイネーブルの最終サイクルで受け取ったデータをSPIに返します
* ライト時は、SPI受信データをラッチした次のサイクルでライトイネーブルを１サイクルだけアサートします

![タイミング.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204268/a9f0d216-7bd4-3d74-140b-f4d2fd4bc320.png)


※ Wishboneバスにした方がIPとの親和性が高くなりますが、ここでWishboneバスにすると初心者殺しになるので、単純ばバスリードライトにしています。「Wishboneバスにしたいよ！」という場合でも、本モジュールの後段にWishboneバスマスター変換モジュールを接続する方法もとれますので、本モジュールをそのまま使えます。（たぶん）

# RTL図
同期化FFとエッジ検出については[FPGA入力信号のエッジ検出](https://qiita.com/takeru0x5569/items/56cf4d1da39b38b75521)の記事で紹介した、SYNCRO_DFF.vhdをインスタンスして使っています。

クロックの立ち上がりエッジパルスでシリアルデータ入力をラッチすると共にカウンタを動作させます。
所定のカウンタ値でアドレス、コマンド、書き込みデータをシリパラ用FFからラッチし、コマンドに応じてリードないしはライト動作を行います。

![RTL.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204268/6fa123a6-e2c2-6e03-8786-d30a71d8ae1c.png)


# VHDLコード

```vhdl:SPI_IF.vhd
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なので、シミュレーションではクロック周波数を上げています。

## ライトアクセス
![ライト.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204268/585781a6-db7a-3c9e-eaa3-c563f9c4bfd0.png)

## リードアクセス
![リード.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204268/8a4646b9-6ab8-9f80-b3c5-c9ed86077d91.png)



# 余談
ステートマシンを使ってコーディンしてしまった方が、コードだけ見た際の可読性は高いです。
今回は「RTL図を描いて検討して、VHDLに落とし込んだらこんな感じ」ということで紹介しました。
筆者も最近ではRTL図をわざわざ書くことは少ないので、ステートマシンで簡単に書いてしまっています。

# おわりに
SPI受信してレジスタに読み書きする信号へ変換できましたので、次回はレジスタの実装例を紹介できればと思います。