はじめに
VHDLでのUART受信回路です
回路規模、動作周波数のための最適化は行わずに可読性を意識して記述します1
開発環境
- 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
- ストップビット受信後に出力を確定する
コード
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であることを確認する
コード
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;
パッケージ
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
-
不要な条件式がかなりあるのでVunitでリグレッションテストを回しながらリファクタリングしてみると楽しいかもしれません ↩