· 

SPIハードをFPGAで(1)

たいていのマイコンのSPI機能で送れるビット数は8bit固定です。可変のものでも5~9bitです。このためSWDには使えない。よって、FPGAで作っちゃう。構成を考えると、マイコンまで組み込んだほうがはんだ付けが楽なので、MAX10FPGAでNIOSIIを組み込むところまでやってみる予定。が、いきなりQuartusPrimeでやるのはいろいろイラっとするので、GHDLとGtkWaveでやる。GHDLのインストールやらGtkWaveのインストールやらは別のサイトに説明があるに違いない。当方、WSLでやってるので、Synapticでインストールしちゃいました。(Win11はLinuxのGUIソフトも何も気を使うことなく動くのでいいぞー。)

ざっくりこんな感じで行きます。しょーじき、得意じゃないので、後でいろいろ不都合が出て変えるかもしれません。

ClockGenerator

INITの立下りエッジで、設定情報を取り込んで、RUNの立下りエッジで動作を開始する。動作中はBUSYをHで出力。NCLKDIVはCLKINを分周して出す設定。NSENDはクロックを吐き出す回数(これで、SPIのビット数を設定する)。

 

TrgGen

INITからINITとRUNを生成してClockGeneratorに入れる。ブロックで書いてあるけど部品化はしない。

 

Writer

INITでWDATAを取り込んで、CLKの立下りエッジで書き出しと内部データの左シフトをする。

 

Reader

INITでRDATAを初期化して、CLKの立ち上がりで内部データの左シフトとINPの取り込みをする。

 

で、さっそく、

(Google Code Prettifyがイマイチすぎるけどしょうがない)

clkgen.vhdl

LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
USE ieee.std_logic_unsigned.ALL;

ENTITY ClockGenerator IS
    PORT (
        NSEND : IN STD_LOGIC_VECTOR(2 DOWNTO 0);
        NCLKDIV : IN STD_LOGIC_VECTOR(2 DOWNTO 0);
        CLKIN : IN STD_LOGIC;
        CLKOUT : OUT STD_LOGIC;
        CLKSYS : IN STD_LOGIC;
        BUSY : OUT STD_LOGIC;
        RUN : IN STD_LOGIC;
        INIT : IN STD_LOGIC
    );
END ClockGenerator;

ARCHITECTURE ppl_type OF ClockGenerator IS
    TYPE t_state IS (STATE_IDLE, STATE_INIT, STATE_RUN, STATE_DONE);
    SIGNAL state : t_state := STATE_IDLE;
    SIGNAL next_state : t_state := STATE_IDLE;
    SIGNAL state_num : INTEGER RANGE 0 TO 7 := 0;

    SIGNAL reqinit : STD_LOGIC := '0';
    SIGNAL reqrun : STD_LOGIC := '0';
    SIGNAL reqdone : STD_LOGIC := '0';
    SIGNAL reqidle : STD_LOGIC := '0';

    SIGNAL in_nsend : INTEGER RANGE 0 TO 8;
    SIGNAL in_nclkdiv : INTEGER RANGE 0 TO 8;
    SIGNAL clkcntr : INTEGER RANGE 0 TO 8;
    SIGNAL clkdivcntr : INTEGER RANGE 0 TO 8;
    SIGNAL out_clk : STD_LOGIC := '1';
    SIGNAL internal_oclk : STD_LOGIC := '1';
    SIGNAL internal_clkdivcntr : INTEGER RANGE 0 TO 8;

    SIGNAL CLKIN_p : STD_LOGIC := '0';
    SIGNAL out_clk_p : STD_LOGIC := '0';

BEGIN
    PROCESS (CLKSYS, INIT, state) BEGIN
        IF INIT'event AND INIT = '0' THEN
            IF state = STATE_IDLE OR state = STATE_RUN OR state = STATE_DONE THEN
                reqinit <= '1';
            END IF;
        END IF;
        IF state = STATE_INIT THEN
            reqinit <= '0';
        END IF;
    END PROCESS;
    PROCESS (CLKSYS, RUN, state) BEGIN
        IF RUN'event AND RUN = '0' THEN
            IF state = STATE_INIT THEN
                reqrun <= '1';
            END IF;
        END IF;
        IF state = STATE_RUN THEN
            reqrun <= '0';
        END IF;
    END PROCESS;
    PROCESS (CLKSYS, clkcntr, state) BEGIN
        IF clkcntr = in_nsend THEN
            IF state = STATE_RUN THEN
                reqdone <= '1';
            END IF;
        END IF;
        IF state = STATE_DONE THEN
            reqdone <= '0';
        END IF;
    END PROCESS;
    PROCESS (internal_oclk, state) BEGIN
        IF internal_oclk'event AND internal_oclk = '0' THEN
            IF state = STATE_DONE THEN
                reqidle <= '1';
            END IF;
        END IF;
        IF state = STATE_IDLE THEN
            reqidle <= '0';
        END IF;
    END PROCESS;
    PROCESS (state, reqinit, reqrun, reqdone, reqidle) BEGIN
        CASE state IS
            WHEN STATE_IDLE =>
                IF reqinit = '1' THEN
                    next_state <= STATE_INIT;
                ELSE
                    next_state <= STATE_IDLE;
                END IF;
            WHEN STATE_INIT =>
                IF reqrun = '1' THEN
                    next_state <= STATE_RUN;
                ELSE
                    next_state <= STATE_INIT;
                END IF;
            WHEN STATE_RUN =>
                IF reqdone = '1' THEN
                    next_state <= STATE_DONE;
                ELSE
                    next_state <= STATE_RUN;
                END IF;
            WHEN STATE_DONE =>
                IF reqidle = '1' THEN
                    next_state <= STATE_IDLE;
                ELSE
                    next_state <= STATE_DONE;
                END IF;
        END CASE;
    END PROCESS;
    PROCESS (CLKSYS) BEGIN
        IF CLKSYS'event AND CLKSYS = '1' THEN
            state <= next_state;
        END IF;
    END PROCESS;

    BUSY <= '1' WHEN state = STATE_INIT ELSE
        '1' WHEN state = STATE_RUN ELSE
        '1' WHEN state = STATE_DONE ELSE
        '0';
    state_num <= 1 WHEN state = STATE_INIT ELSE
        2 WHEN state = STATE_RUN ELSE
        3 WHEN state = STATE_DONE ELSE
        0;
    PROCESS (CLKSYS, state) BEGIN
        IF CLKSYS'event AND CLKSYS = '1' THEN
            IF state = STATE_INIT THEN
                in_nsend <= CONV_INTEGER(NSEND) + 1;
                in_nclkdiv <= CONV_INTEGER(NCLKDIV);
            END IF;
        END IF;
    END PROCESS;

    PROCESS (CLKSYS, CLKIN, state) BEGIN
        IF CLKSYS'event AND CLKSYS = '1' THEN
            IF state = STATE_INIT THEN
                clkdivcntr <= 0;
                out_clk <= '1';
            END IF;
            IF state = STATE_IDLE THEN
                out_clk <= '1';
            END IF;

            IF CLKIN_p = '0' AND CLKIN = '1' THEN
                IF state = STATE_RUN THEN
                    IF clkdivcntr < in_nclkdiv THEN
                        clkdivcntr <= clkdivcntr + 1;
                    END IF;
                    IF clkdivcntr = in_nclkdiv THEN
                        clkdivcntr <= 0;
                        out_clk <= out_clk XOR '1';
                    END IF;
                END IF;
            END IF;
        END IF;
    END PROCESS;

    PROCESS (CLKSYS, CLKIN, state) BEGIN
        IF CLKSYS'event AND CLKSYS = '1' THEN
            IF state = STATE_INIT THEN
                internal_clkdivcntr <= 0;
                internal_oclk <= '1';
            END IF;
            IF state = STATE_IDLE THEN
                internal_oclk <= '1';
            END IF;
            IF CLKIN_p = '0' AND CLKIN = '1' THEN
                IF state = STATE_RUN OR state = STATE_DONE THEN
                    IF internal_clkdivcntr < in_nclkdiv THEN
                        internal_clkdivcntr <= internal_clkdivcntr + 1;
                    END IF;
                    IF internal_clkdivcntr = in_nclkdiv THEN
                        internal_clkdivcntr <= 0;
                        internal_oclk <= internal_oclk XOR '1';
                    END IF;
                END IF;
            END IF;
        END IF;
    END PROCESS;

    PROCESS (CLKSYS, CLKIN, state) BEGIN
        IF CLKSYS'event AND CLKSYS = '1' THEN
            CLKIN_p <= CLKIN;
        END IF;
    END PROCESS;

    PROCESS (CLKSYS, out_clk, state) BEGIN
        IF CLKSYS'event AND CLKSYS = '1' THEN
            out_clk_p <= out_clk;
            IF state = STATE_INIT THEN
                clkcntr <= 0;
            END IF;

            IF out_clk_p = '0' AND out_clk = '1' THEN
                IF state = STATE_RUN THEN
                    IF clkcntr < in_nsend THEN
                        clkcntr <= clkcntr + 1;
                    END IF;
                END IF;
            END IF;
        END IF;
    END PROCESS;

    CLKOUT <= out_clk;

END;

writer.vhdl

LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
USE ieee.std_logic_unsigned.ALL;

ENTITY Writer IS
        PORT (
                CLKSYS : IN STD_LOGIC;
                CLK : IN STD_LOGIC;
                INIT : IN STD_LOGIC;
                WDATA : IN STD_LOGIC_VECTOR(7 DOWNTO 0);
                OUTP : OUT STD_LOGIC
        );
END Writer;

ARCHITECTURE ppl_type OF Writer IS
        SIGNAL in_wdata : STD_LOGIC_VECTOR(7 DOWNTO 0) := "00000000";
        SIGNAL out_outp : STD_LOGIC := '1';
        SIGNAL CLK_p : STD_LOGIC := '0';
        SIGNAL INIT_p : STD_LOGIC := '0';
BEGIN
        PROCESS (CLKSYS, INIT, CLK) BEGIN
                IF CLKSYS'event AND CLKSYS = '1' THEN
                        CLK_p <= CLK;
                        INIT_p <= INIT;
                END IF;
        END PROCESS;
        PROCESS (CLKSYS, INIT, CLK) BEGIN
                IF CLKSYS'event AND CLKSYS = '1' THEN
                        IF INIT_p = '1' AND INIT = '0' THEN
                                in_wdata <= WDATA;
                        END IF;
                        IF CLK_p = '1' AND CLK = '0' THEN
                                out_outp <= in_wdata(7);
                                in_wdata <= in_wdata(6 DOWNTO 0) & '0';
                        END IF;
                END IF;
        END PROCESS;
        OUTP <= out_outp;
        
END;

reader.vhdl

LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
USE ieee.std_logic_unsigned.ALL;

ENTITY Reader IS
        PORT (
                CLKSYS : IN STD_LOGIC;
                CLK : IN STD_LOGIC;
                INIT : IN STD_LOGIC;
                RDATA : OUT STD_LOGIC_VECTOR(7 DOWNTO 0);
                INP : IN STD_LOGIC
        );
END Reader;

ARCHITECTURE ppl_type OF Reader IS
        SIGNAL out_rdata : STD_LOGIC_VECTOR(7 DOWNTO 0) := "00000000";
        SIGNAL in_inp : STD_LOGIC := '1';
        SIGNAL CLK_p : STD_LOGIC := '0';
        SIGNAL INIT_p : STD_LOGIC := '0';

BEGIN
        PROCESS (CLKSYS, INIT, CLK) BEGIN
                IF CLKSYS'event AND CLKSYS = '1' THEN
                        CLK_p <= CLK;
                        INIT_p <= INIT;
                END IF;
        END PROCESS;

        PROCESS (CLKSYS, INIT, CLK) BEGIN
                IF CLKSYS'event AND CLKSYS = '1' THEN
                        IF INIT_p = '1' AND INIT = '0' THEN
                                out_rdata <= "00000000";
                        END IF;
                        IF CLK_p = '0' AND CLK = '1' THEN
                                out_rdata <= out_rdata(6 DOWNTO 0) & in_inp;
                        END IF;
                END IF;
        END PROCESS;
        in_inp <= INP;
        RDATA <= out_rdata;
END;

myspi.vhdl

LIBRARY ieee;
USE ieee.std_logic_1164.ALL;
USE ieee.std_logic_unsigned.ALL;

ENTITY MySPI IS
    PORT (
--        run : out std_logic;
        NSEND : IN STD_LOGIC_VECTOR(2 DOWNTO 0);
        NCLKDIV : IN STD_LOGIC_VECTOR(2 DOWNTO 0);
        CLKIN : IN STD_LOGIC;
        CLKOUT : OUT STD_LOGIC;--SCK
        CLKSYS : IN STD_LOGIC;
        BUSY : OUT STD_LOGIC;
        INIT : IN STD_LOGIC;
        WDATA : IN STD_LOGIC_VECTOR(7 DOWNTO 0);
        RDATA : OUT STD_LOGIC_VECTOR(7 DOWNTO 0);
        OUTP : OUT STD_LOGIC;--MOSI
        INP : IN STD_LOGIC; --MISO
        EN : IN STD_LOGIC
    );
END MySPI;

ARCHITECTURE ppl_type OF MySPI IS
    COMPONENT ClockGenerator IS
        PORT (
            NSEND : IN STD_LOGIC_VECTOR(2 DOWNTO 0);
            NCLKDIV : IN STD_LOGIC_VECTOR(2 DOWNTO 0);
            CLKIN : IN STD_LOGIC;
            CLKOUT : OUT STD_LOGIC;
            CLKSYS : IN STD_LOGIC;
            BUSY : OUT STD_LOGIC;
            RUN : IN STD_LOGIC;
            INIT : IN STD_LOGIC
        );
    END COMPONENT;
    COMPONENT Writer IS
        PORT (
            CLKSYS : IN STD_LOGIC;
            CLK : IN STD_LOGIC;
            INIT : IN STD_LOGIC;
            WDATA : IN STD_LOGIC_VECTOR(7 DOWNTO 0);
            OUTP : OUT STD_LOGIC
        );
    END COMPONENT;
    COMPONENT Reader IS
        PORT (
            CLKSYS : IN STD_LOGIC;
            CLK : IN STD_LOGIC;
            INIT : IN STD_LOGIC;
            RDATA : OUT STD_LOGIC_VECTOR(7 DOWNTO 0);
            INP : IN STD_LOGIC
        );
    END COMPONENT;

    SIGNAL out_clkout : STD_LOGIC;
    SIGNAL out_outp : STD_LOGIC;
    SIGNAL out_busy : STD_LOGIC;
    SIGNAL internal_init : STD_LOGIC := '1';
    SIGNAL internal_run : STD_LOGIC := '1';

    TYPE t_state IS (STATE_IDLE, STATE_TRIGGERED);
    SIGNAL state : t_state := STATE_IDLE;
    SIGNAL next_state : t_state := STATE_IDLE;

    SIGNAL reqinit : STD_LOGIC := '0';
BEGIN
--      run<=internal_run;
    ClockGenerator_inst : COMPONENT ClockGenerator PORT MAP(
        NSEND => NSEND,
        NCLKDIV => NCLKDIV,
        CLKIN => CLKIN,
        CLKOUT => out_clkout,
        CLKSYS => CLKSYS,
        BUSY => out_busy,
        RUN => internal_run,
        INIT => internal_init
    );
    Writer_inst : COMPONENT Writer PORT MAP(
        CLKSYS => CLKSYS,
        CLK => out_clkout,
        INIT => internal_init,
        WDATA => WDATA,
        OUTP => out_outp
    );
    Reader_inst : COMPONENT Reader PORT MAP(
        CLKSYS => CLKSYS,
        CLK => out_clkout,
        INIT => internal_init,
        RDATA => RDATA,
        INP => INP
    );
    CLKOUT <= out_clkout WHEN EN = '1' ELSE
        'Z';
    internal_init <= INIT;
    OUTP <= out_outp WHEN (out_busy = '1' AND EN = '1') ELSE
        '1' WHEN (out_busy = '0' AND EN = '1') ELSE
        'Z';
    --OUTP<='1' WHEN out_busy='0' ELSE out_outp;
    BUSY <= out_busy;

    PROCESS (CLKSYS, INIT) BEGIN
        IF INIT'event AND INIT = '0' THEN
            IF state = STATE_IDLE THEN
                reqinit <= '1';
            END IF;
        END IF;
        IF state = STATE_TRIGGERED THEN
            reqinit <= '0';
        END IF;
    END PROCESS;

    PROCESS (state, reqinit) BEGIN
        CASE state IS
            WHEN STATE_IDLE =>
                IF reqinit = '1' THEN
                    next_state <= STATE_TRIGGERED;
                END IF;
            WHEN STATE_TRIGGERED =>
                next_state <= STATE_IDLE;
        END CASE;
    END PROCESS;

    PROCESS (CLKSYS) BEGIN
        IF CLKSYS'event AND CLKSYS = '1' THEN
            state <= next_state;
        END IF;
    END PROCESS;

    PROCESS (CLKSYS) BEGIN
        IF CLKSYS'event AND CLKSYS = '1' THEN
            IF state = STATE_TRIGGERED THEN
                internal_run <= '0';
            ELSE
                internal_run <= '1';
            END IF;
        END IF;
    END PROCESS;

END;

そして、テストベンチ。MOSIをMISOとつなげている。

myspi_tb.vhdl

library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_unsigned.all;

entity MySPI_TB is
end MySPI_TB;

architecture SIM of MySPI_TB is
    component MySPI is
        port
        (
            NSEND : in std_logic_vector(2 downto 0);
            NCLKDIV : in std_logic_vector(2 downto 0);
            CLKIN : in std_logic;
            CLKOUT : out std_logic;
            CLKSYS : in std_logic;
            BUSY : out std_logic;
            INIT : in std_logic;
            WDATA : in std_logic_vector(7 downto 0);
            RDATA : out std_logic_vector(7 downto 0);
            OUTP : out std_logic;
            INP : in std_logic;
            EN : in std_logic
        );
    end component;

    signal nsend : std_logic_vector(2 downto 0):="111";
    signal nclkdiv : std_logic_vector(2 downto 0):="101";
    signal clkbase : std_logic;
    signal clkout : std_logic;
    signal clksys : std_logic;
    signal busy : std_logic;
    signal init : std_logic:='1';
    signal wdata : std_logic_vector(7 downto 0):="11010101";
    signal rdata : std_logic_vector(7 downto 0);
    signal outp : std_logic;
    signal inp : std_logic;
    signal en : std_logic:='1';
    constant STEPSYS:time:=125 ns;
    constant STEPBASE:time:=500 ns;
begin
    MySPI_inst : component MySPI port map(
        NSEND=>nsend,
        NCLKDIV=>nclkdiv,
        CLKIN=>clkbase,
        CLKOUT=>clkout,
        CLKSYS=>clksys,
        BUSY=>busy,
        INIT=>init,
        WDATA=>wdata,
        RDATA=>rdata,
        OUTP=>outp,
        INP=>inp,
        EN=>en
    );
    inp<=outp;
    process begin
        clksys<='1';
        wait for STEPSYS/2;
        clksys<='0';
        wait for STEPSYS/2;
    end process;
    process begin
        clkbase<='1';
        wait for STEPBASE/2;
        clkbase<='0';
        wait for STEPBASE/2;
    end process;

    process begin
        wait for 1 us;
        init<='0';
        wait for STEPSYS;
        init<='1';
        wait for 60 us;

        wdata<="01101010";
        nclkdiv<="001";
        wait for 1 us;
        init<='0';
        wait for STEPSYS;
        init<='1';
        wait for 20 us;

        wdata<="00110101";
        nclkdiv<="000";
        wait for 1 us;
        init<='0';
        wait for STEPSYS;
        init<='1';
        wait for 10 us;

        wdata<="01010101";
        nsend<="000";
        nclkdiv<="000";
        wait for 1 us;
        init<='0';
        wait for STEPSYS;
        init<='1';
        wait for 10 us;

        wdata<="01110100";
        nsend<="111";
        nclkdiv<="000";
        wait for 1 us;
        init<='0';
        wait for STEPSYS;
        init<='1';
        wait for 10 us;

        en<='0';
        wait;
    end process; 
end;

VHDLでも相変わらずのコメント皆無。wikipediaによると、

米国国防総省は、業者の納品する機器で含むASICの動作の文書記述のためにVHDLを開発した。すなわち、分厚く複雑になりがちな紙のマニュアルの代替を目指したのが始まりである。

ということなので、VHDLそのものが仕様を示しており、それにコメントを書くのはナンセンスなので、できるだけ書かないようにしよう(、、、曲解)。

 

ちょっとだけラクするために、Makefile

VHDLC=/usr/bin/ghdl
SRC=clkgen.vhdl writer.vhdl reader.vhdl myspi.vhdl
TBSRC=myspi_tb.vhdl
VCD=myspi.vcd
SIMRTL=MySPI_TB

all :
        @make comp
        @make tb
        @make tb_e
        @make vcd

comp :
        $(VHDLC) -a --ieee=synopsys $(SRC)

tb :
        $(VHDLC) -a --ieee=synopsys $(TBSRC)

tb_e :
        $(VHDLC) -e --ieee=synopsys $(SIMRTL)

vcd :
        $(VHDLC) -r --ieee=synopsys $(SIMRTL) --vcd=$(VCD)

ちなみに、テストベンチに終わりを指示していないので放っておくとずっとやるみたい。適当なところでCtrl+Cでとめちゃう。

そして、GtkWaveで開くとこうなる。

それらしくできているんじゃないかと思う。初めて使ったGHDL。早くていい。ロジックの検討ならメーカーの開発環境は使わずにこっちでやったほうがラク。今までなんで使ってなかったのか。

次回はQuartusPrimeでもやってみます、、、道のりは長い。道のりが長いネタをやっているときに、事件が起こると、休みがなくなって放置してしまう。そして忘れる、、、その繰り返し。やり切ってないネタをいつかやり遂げたいが、過去を振り返るのは大きなストレスを感じる。

 

VHDLもVerilogHDLもちょっと前はまぁまぁすんなり書けていたのに、もうすっかり忘れてしまってたわ。さらに加えて、論理回路の考え方がどうも苦手なので、素人感出てるんやろなーと思う。まぁ素人ですからっ!