VHDLでシリアル信号入力

# はじめに
VHDLでのUART受信回路です
回路規模、動作周波数のための最適化は行わずに可読性を意識して記述します[^1]

[^1]: 不要な条件式がかなりあるのでVunitでリグレッションテストを回しながらリファクタリングしてみると楽しいかもしれません

# 開発環境
* Windows10 Pro 64bit
* Python 3.7.0
* vunit-hdl 4.0.8
* colorama 0.4.1
* GHDL 0.35

# 仕様
## 通信仕様
* 通信フォーマット
    * LSBファースト
    * データ長 : 8bit
    * パリティ : 1bit, 奇数パリティ
    * ストップ : 1bit
* 通信速度
    * 115.2kbps
* クロック
    * 50MHz

## 入力仕様

* クロック : CLK
    * 50MHz
* リセット : RST
    * 内部ステートマシンのリセット信号
    * テスト対象外
* 受信信号 : i_RX
    * シリアル受信信号

## 出力仕様

* イネーブル : o_EN
    * 受信終了時にアサート
    * 1クロック後にネゲート
* エラー : o_ERR
    * 受信終了時にイネーブルと共に出力
    * イネーブル時以外はネゲート
    * 検出エラー
        * パリティエラー
        * フレーミングエラー
* データ : o_DATA
    * 受信終了時にイネーブルと共に出力
    * イネーブル時以外は0x00を出力
    * 受信エラーの場合0x00を出力

## 実装仕様
* シリアル信号の各ビットの中央で値を読む

# 実装

## 概要

* 立下り検出回路でスタートビットを検出しステートマシンを開始する
* 各ステートで処理を実施する
    * 待機状態 : idle
    * スタートビット受信 : receive_start
    * データビット受信 : receive_data
    * パリティビット受信 : receive_odd_parity
    * ストップビット受信 : receive_stop
* ストップビット受信後に出力を確定する

## コード

```vhdl:UartReceiver.vhd
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity UartReceiver is
    generic(
        CLKS_PER_BIT : natural := 434; -- (1 / 115.2kbps) / (1 / 50MHz)
        DATA_BITS : natural := 8
    );
    port(
        CLK : in std_logic;
        RST : in std_logic;
        i_RX : in std_logic;
        o_EN : out std_logic;
        o_ERR : out std_logic;
        o_DATA : out std_logic_vector(DATA_BITS - 1 downto 0)
    );
end UartReceiver;

architecture Behavioral of UartReceiver is

    signal rx_register : std_logic_vector(1 downto 0);
    signal start_trigger : std_logic;

    type type_state is (
        idle,
        receive_start,
        receive_data,
        receive_odd_parity,
        receive_stop
    );
    signal state : type_state;

    signal time_count : natural range 0 to CLKS_PER_BIT - 1;
    signal receive_trigger : std_logic;

    signal bit_count : natural range 0 to DATA_BITS - 1;
    signal is_data_end : std_logic;

    signal serial_data : std_logic_vector(DATA_BITS - 1 downto 0);
    signal serial_odd_parity : std_logic;
    signal serial_stop : std_logic;

    signal odd_parity_calc : std_logic;

    signal parity_error : std_logic;
    signal framing_error : std_logic;
    signal com_error : std_logic;

    signal data_hold : std_logic;

begin

    -- detect start_bit falling edge
    rx_register(0) <= i_RX;

    process(CLK)
    begin
        if rising_edge(CLK) then
            rx_register(1) <= rx_register(0);
        end if;
    end process;

    start_trigger <= '1' when rx_register = "10" else '0';

    -- statemachine
    statemachine : process(CLK)
    begin
        if rising_edge(CLK) then
            if RST ='1' then
                state <= idle;
            else
                case state is
                    when idle =>
                        if start_trigger = '1' then
                            state <= receive_start;
                        end if;
                    when receive_start =>
                        if receive_trigger = '1' then
                            state <= receive_data;
                        end if;
                    when receive_data =>
                        if receive_trigger = '1' and is_data_end = '1' then
                            state <= receive_odd_parity;
                        end if;
                    when receive_odd_parity =>
                        if receive_trigger = '1' then
                            state <= receive_stop;
                        end if;
                    when receive_stop =>
                        if receive_trigger = '1' then
                            state <= idle;
                        end if;
                    when others =>
                        state <= idle;
                end case;
            end if;
        end if;
    end process;

    -- generate receive_trigger
    process(CLK)
    begin
        if rising_edge(CLK) then
            case state is
                when idle =>
                    time_count <= CLKS_PER_BIT / 2 - 1;
                when others =>
                    if time_count = 0 then
                        time_count <= CLKS_PER_BIT - 1;
                    else
                        time_count <= time_count - 1;
                    end if;
            end case;
        end if;
    end process;

    receive_trigger <= '1' when time_count = 0 else '0';

    -- count bit
    process(CLK)
    begin
        if rising_edge(CLK) then
            case state is
                when idle =>
                    bit_count <= DATA_BITS - 1;
                when receive_data =>
                    if receive_trigger = '1' then
                        if bit_count = 0 then
                            bit_count <= DATA_BITS - 1;
                        else
                            bit_count <= bit_count - 1;
                        end if;
                    end if;
                when others =>
                    null;
            end case;
        end if;
    end process;

    is_data_end <= '1' when bit_count = 0 else '0';

    -- serial to pararell (data)
    process(CLK)
    begin
        if rising_edge(CLK) then
            case state is
                when receive_data =>
                    if receive_trigger = '1' then
                        serial_data <= i_RX & serial_data(DATA_BITS - 1 downto 1);
                    end if;
                when others =>
                    null;
            end case;
        end if;
    end process;

    -- calculate odd_parity
    process(CLK)
    begin
        if rising_edge(CLK) then
            case state is
                when receive_start =>
                    odd_parity_calc <= '1';
                when receive_data =>
                    if receive_trigger = '1' then
                        odd_parity_calc <= odd_parity_calc xor i_RX;
                    end if;
                when others =>
                    null;
            end case;
        end if;
    end process;

    -- serial to pararell (odd_parity)
    process(CLK)
    begin
        if rising_edge(CLK) then
            if receive_trigger = '1' then
                case state is
                    when receive_odd_parity =>
                        if receive_trigger = '1' then
                            serial_odd_parity <= i_RX;
                        end if;
                    when others =>
                        null;
                end case;
            end if;
        end if;
    end process;

    -- framing error
    process(CLK)
    begin
        if rising_edge(CLK) then
            case state is
                when receive_stop =>
                    if receive_trigger = '1' then
                        if i_RX = '0' then
                            framing_error <= '1';
                        else
                            framing_error <= '0';
                        end if;
                    end if;
                when others =>
                    null;
            end case;
        end if;
    end process;

    -- odd_parity error
    process(CLK)
    begin
        if rising_edge(CLK) then
            case state is
                when receive_stop =>
                    if receive_trigger = '1' then
                        if serial_odd_parity /= odd_parity_calc then
                            parity_error <= '1';
                        else
                            parity_error <= '0';
                        end if;
                    end if;
                when others =>
                    null;
            end case;
        end if;
    end process;

    -- error
    com_error <= framing_error or parity_error;

    -- hold data
    process(CLK)
    begin
        if rising_edge(CLK) then
            case state is
                when receive_stop =>
                    if receive_trigger = '1' then
                        data_hold <= '1';
                    else
                        data_hold <= '0';
                    end if;
                when others =>
                    data_hold <= '0';
            end case;
        end if;
    end process;

    process(CLK)
    begin
        if rising_edge(CLK) then
            if data_hold = '1' then
                if com_error = '0' then
                    o_EN <= '1';
                    o_ERR <= '0';
                    o_DATA <= serial_data;
                else
                    o_EN <= '1';
                    o_ERR <= '1';
                    o_DATA <= (others => '0');
                end if;
            else
                o_EN <= '0';
                o_ERR <= '0';
                o_DATA <= (others => '0');
            end if;
        end if;
    end process;

end Behavioral;

```

# テスト

テストはVunitをもちいて行います

## テスト入力
* 奇数パリティが1の場合と0の場合の2種類の値をテストする
    * X"AC"
    * X"13"

## テスト項目

### 正常系

#### イネーブルテスト : o_EN

正常なシリアル信号を受信した場合のo_ENの動作を確認する

* o_ENが1クロックのみアサートされることを確認する
* o_ENが1度アサートされることを確認する

#### 受信テスト : normal

正常なシリアル信号を受信した場合のo_ERR, o_DATAの動作を確認する

* o_ENが'1'のとき
    * o_ERRが'0'であることを確認する
    * o_DATAが受信データであることを確認する
* o_ENが'0'のとき
    * o_ERRが'0'であることを確認する
    * o_DATAが0x00であることを確認する

### 準正常系

#### パリティエラーテスト : parity error

パリティエラーのシリアル信号を受信した場合のo_ERR, o_DATAの動作を確認する
エラーの代表としてパリティビットが反転した信号を入力とする

* o_ENが'1'のとき
    * o_ERRが'1'であることを確認する
    * o_DATAが0x00であることを確認する
* o_ENが'0'のとき
    * o_ERRが'0'であることを確認する
    * o_DATAが0x00であることを確認する

#### フレーミングエラーテスト : framing error

フレーミングエラーのシリアル信号を受信した場合のo_ERR, o_DATAの動作を確認する

* o_ENが'1'のとき
    * o_ERRが'1'であることを確認する
    * o_DATAが0x00であることを確認する
* o_ENが'0'のとき
    * o_ERRが'0'であることを確認する
    * o_DATAが0x00であることを確認する

## コード

```vhdl:tb_UartReceiver_0.vhd
library vunit_lib;
context vunit_lib.vunit_context;
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
library work;
use work.pkg_uart.all;

entity tb_UartReceiver_0 is
    generic (
        runner_cfg : string := runner_cfg_default
    );
end tb_UartReceiver_0;

architecture behavior of tb_UartReceiver_0 is

    -- Parameters
    constant CLKS_PER_BIT : natural := 434;
    constant DATA_BITS : natural := 8;

    constant BITS_PER_FRAME : natural := DATA_BITS + 3; -- start(1bit) + data(DATA_BITS bit) + parity(1bit) + stop(1bit)

    constant TEST_FRAMES_NUM : natural := 2;
    type type_serial_data_array is array(TEST_FRAMES_NUM - 1 downto 0) of std_logic_vector(BITS_PER_FRAME - 1 downto 0);
    constant normal_serial_data : type_serial_data_array := (
        "1" & "1" & X"AC" & "0",
        "1" & "0" & X"13" & "0"
    );

    -- Constants
    constant ADDR_START : natural := 0;
    constant ADDR_PARITY : natural := BITS_PER_FRAME - 2;
    constant ADDR_STOP : natural := BITS_PER_FRAME - 1;
    constant START_BIT : std_logic := '0';
    constant STOP_BIT : std_logic := '1';
    constant unused_data : std_logic_vector(DATA_BITS - 1 downto 0) := (others => '0');

    -- Inputs
    signal CLK : std_logic := '0';
    signal RST : std_logic := '0';
    signal i_RX : std_logic := '1';

    -- Outputs
    signal o_EN : std_logic;
    signal o_ERR : std_logic;
    signal o_DATA : std_logic_vector(DATA_BITS - 1 downto 0);

    -- Clock period definitions
    constant CLK_period : time := 20 ns;

begin

    UartReceiver_inst : UartReceiver
    generic map(
        CLKS_PER_BIT => CLKS_PER_BIT,
        DATA_BITS => DATA_BITS
    )
    port map(
        CLK => CLK,
        RST => RST,
        i_RX => i_RX,
        o_EN => o_EN,
        o_ERR => o_ERR,
        o_DATA => o_DATA
    );

    -- Clock process definitions
    CLK_process : process
    begin
        CLK <= '0';
        wait for CLK_period / 2;
        CLK <= '1';
        wait for CLK_period / 2;
    end process;

    test_bench : process
        variable test_serial_data : std_logic_vector(BITS_PER_FRAME - 1 downto 0);
        variable output_enbale_flag : std_logic;
    begin
        test_runner_setup(runner, runner_cfg);
        while test_suite loop

            RST <= '1';
            wait for CLK_period;
            RST <= '0';
            wait for CLK_period;

            if run("o_EN") then

                for frame_i in TEST_FRAMES_NUM - 1 downto 0 loop
                    test_serial_data := normal_serial_data(frame_i);
                    output_enbale_flag := '0';

                    for bit_i in 0 to BITS_PER_FRAME - 1 loop
                        for t in 0 to CLKS_PER_BIT - 1 loop
                            i_RX <= test_serial_data(bit_i);
                            wait for CLK_period;
                            if output_enbale_flag = '1' then
                                check_equal(o_EN, '0', "o_EN is not 1 clock when " & "frame = " & to_string(frame_i) & ", t = " & to_string(t));
                            end if;
                            output_enbale_flag := output_enbale_flag or o_EN;
                        end loop;
                    end loop;

                    check_equal(output_enbale_flag, '1', "o_EN doesn't work when " & "frame = " & to_string(frame_i));

                end loop;

                wait for CLK_period;

            elsif run("normal") then

                for frame_i in TEST_FRAMES_NUM - 1 downto 0 loop
                    test_serial_data := normal_serial_data(frame_i);

                    for bit_i in 0 to BITS_PER_FRAME - 1 loop
                        for t in 0 to CLKS_PER_BIT - 1 loop
                            i_RX <= test_serial_data(bit_i);
                            wait for CLK_period;
                            if o_EN = '1' then
                                check_equal(o_ERR, '0', "o_ERR when o_EN = '1', " & "frame = " & to_string(frame_i) & ", t = " & to_string(t));
                                check_equal(o_DATA, normal_serial_data(frame_i)(BITS_PER_FRAME - 3 downto 1), "o_DATA when o_EN = '1', " & "frame = " & to_string(frame_i) & ", t = " & to_string(t));
                            else
                                check_equal(o_ERR, '0', "o_ERR when o_EN = '0', " & "frame = " & to_string(frame_i) & ", t = " & to_string(t));
                                check_equal(o_DATA, unused_data, "o_DATA when o_EN = '0', " & "frame = " & to_string(frame_i) & ", t = " & to_string(t));
                            end if;
                        end loop;
                    end loop;

                end loop;

                wait for CLK_period;

            elsif run("parity error") then

                for frame_i in TEST_FRAMES_NUM - 1 downto 0 loop

                    test_serial_data := normal_serial_data(frame_i);
                    test_serial_data(ADDR_PARITY) := not test_serial_data(ADDR_PARITY);

                    for bit_i in 0 to BITS_PER_FRAME - 1 loop
                        for t in 0 to CLKS_PER_BIT - 1 loop
                            i_RX <= test_serial_data(bit_i);
                            wait for CLK_period;
                            if o_EN = '1' then
                                check_equal(o_ERR, '1', "o_ERR when o_EN = '1', " & "frame = " & to_string(frame_i) & ", t = " & to_string(t));
                                check_equal(o_DATA, unused_data, "o_DATA when o_EN = '1', " & "frame = " & to_string(frame_i) & ", t = " & to_string(t));
                            else
                                check_equal(o_ERR, '0', "o_ERR when o_EN = '0', " & "frame = " & to_string(frame_i) & ", t = " & to_string(t));
                                check_equal(o_DATA, unused_data, "o_DATA when o_EN = '0', " & "frame = " & to_string(frame_i) & ", t = " & to_string(t));
                            end if;
                        end loop;
                    end loop;

                end loop;

                wait for CLK_period;

            elsif run("framing error") then

                for frame_i in TEST_FRAMES_NUM - 1 downto 0 loop

                    test_serial_data := normal_serial_data(frame_i);
                    test_serial_data(ADDR_STOP) := not test_serial_data(ADDR_STOP);

                    for bit_i in 0 to BITS_PER_FRAME - 1 loop
                        for t in 0 to CLKS_PER_BIT - 1 loop
                            i_RX <= test_serial_data(bit_i);
                            wait for CLK_period;
                            if o_EN = '1' then
                                check_equal(o_ERR, '1', "o_ERR when o_EN = '1', " & "frame = " & to_string(frame_i) & ", t = " & to_string(t));
                                check_equal(o_DATA, unused_data, "o_DATA when o_EN = '1', " & "frame = " & to_string(frame_i) & ", t = " & to_string(t));
                            else
                                check_equal(o_ERR, '0', "o_ERR when o_EN = '0', " & "frame = " & to_string(frame_i) & ", t = " & to_string(t));
                                check_equal(o_DATA, unused_data, "o_DATA when o_EN = '0', " & "frame = " & to_string(frame_i) & ", t = " & to_string(t));
                            end if;
                        end loop;
                    end loop;

                end loop;

                wait for CLK_period;

            end if;
        end loop;
        test_runner_cleanup(runner);
    end process;

end;

```

## パッケージ

```vhdl:pkg_uart.vhd
library ieee;
use ieee.std_logic_1164.all;

package pkg_uart is

    component UartReceiver
        generic(
            CLKS_PER_BIT : natural;
            DATA_BITS : natural
        );
        port(
            CLK : in std_logic;
            RST : in std_logic;
            i_RX : in std_logic;
            o_EN : out std_logic;
            o_ERR : out std_logic;
            o_DATA : out std_logic_vector(DATA_BITS - 1 downto 0)
        );
    end component;

end pkg_uart;

```

# 備考
テストパラメータで`constant CLKS_PER_BIT : natural := 2;`とすると実装は問題ないがテストがエラーとなる
実用上使用しないパラメータでありテストが複雑化するため対応しない

# 参考

* https://qiita.com/cyebu1103/items/b72eed2e0f50b843ae21
* https://hero1000.blog.fc2.com/blog-entry-8.html