1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

FPGAとRaspberryPi picoWでSPI通信(その②FPGAのSPI受信回路)

Last updated at Posted at 2025-01-16

概要

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サイクルだけアサートします

タイミング.png

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

RTL図

同期化FFとエッジ検出についてはFPGA入力信号のエッジ検出の記事で紹介した、SYNCRO_DFF.vhdをインスタンスして使っています。

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

RTL.png

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

リードアクセス

リード.png

余談

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

おわりに

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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?