今天给大侠带来基于FPGA的 UART 控制器设计(VHDL)(下),由于篇幅较长,分三篇。今天带来第三篇,下篇,使用 FPGA 实现 UART。话不多说,上货。
之前有关于 Veriliog HDL 实现的 UART 控制器设计,这里放上超链接,仅供各位大侠参考。
FPGA零基础学习:UART协议驱动设计
源码系列:基于FPGA的串口UART设计(附源工程)
第三篇内容摘要:本篇会介绍使用 FPGA 实现 UART,包括UART设计框架、UART 工作流程、信号监测器模块的实现、波特率发生器模块的实现、移位寄存器模块的实现、奇偶校验器模块的实现、总线选择器模块的实现、计数器模块的实现、UART 内核模块的实现、UART 顶层模块的实现 、测试平台的编写和仿真等相关内容。
三、使用 FPGA 实现 UART
3.1 UART 设计框架
UART 主要由 UART 内核、信号监测器、移位寄存器、波特率发生器、计数器、总线选择器和奇偶校验器总共 7 个模块组成,如图 5 所示。
图 5 UART 实现框架图
UART 各个模块的功能如下。
1)UART 内核模块
UART 内核模块是整个设计的核心。在数据接收时,UART 内核模块负责控制波特率发生器和移位寄存器,使得移位寄存器在波特率时钟的驱动下同步地接收并且保存 RS-232 接收端口上的串行数据。在数据发送时,UART 内核模块首先根据待发送的数据和奇偶校验位的设置产生完整的发送序列(包括起始位、数据位、奇偶校验位和停止位),之后控制移位寄存器将序列加载到移位寄存器的内部寄存器里,最后再控制波特率发生器驱动移位寄存器将数据串行输出。
2)信号监测器模块
信号监测器用于对 RS-232 的输入信号进行实时监测,一旦发现新的数据则立即通知 UART内核。
注意:这里所说的 RS-232 输入、输出信号都指的是经过电平转换后的逻辑信号,而不是 RS-232 总线上的信号。绝对不能直接将 RS-232 总线的信号连接到 FPGA 管脚上,否则很容易造成 FPGA芯片的损坏。
3)移位寄存器模块
移位寄存器的作用是存储输入或者输出的数据。当 UART 接收 RS-232 输入时,移位寄存器在波特率模式下采集 RS-232 输入信号,并且保存结果;当 UART 进行 RS-232 输出时,UART 内核首先将数据加载到移位寄存器内,再使移位寄存器在波特率模式下将数据输出到 RS-232 输出端口上。
注意:波特率模式指的是模块的输入时钟是符合 RS-232 传输波特率的时钟,与波特率模式对应的就是系统时钟模式,即模块是工作在系统时钟下。
4)波特率发生器模块
由于 RS-232 传输必定是工作在某种波特率下,比如 9600,为了便于和 RS-232 总线进行同步,需要产生符合 RS-232 传输波特率的时钟,这就是波特率发生器的功能。
5)奇偶校验器模块
奇偶校验器的功能是根据奇偶校验的设置和输入数据计算出相应的奇偶校验位,它是通过纯组合逻辑实现的。
6)总线选择模块
总线选择模块用于选择奇偶校验器的输入是数据发送总线还是数据接收总线。在接收数据时,总线选择模块将数据接收总线连接到奇偶校验器的输入端,来检查已接收数据的奇偶校验位是否正确;而在发送数据时,总线选择模块将数据发送总线连接到奇偶校验器的输入端,UART内核模块就能够获取并且保存待发送序列所需的奇偶校验位了。
7)计数器模块
计数器模块的功能是记录串行数据发送或者接收的数目,在计数到某数值时通知 UART 内核模块。
3.2 UART 工作流程
UART 的工作流程可以分为接收过程和发送过程两部分。
接收过程指的是 UART 监测到 RS-232 总线上的数据,顺序读取串行数据并且将其输出给CPU 的过程。当信号监测器监测到新的数据(RS-232 输入逻辑变为 0,即 RS-232 传输协议的起始位)就会触发接收过程,其流程图如图 6 所示。
图 6 UART 接收数据流程图
首先 UART 内核会重置波特率发生器和移位寄存器,并且设置移位寄存器的工作模式为波特率模式,以准备接收数据。其次,移位寄存器在波特率时钟的驱动下工作,不断读取 RS-232 串行总线的输入数据,并且将数据保存在内部的寄存器内。接收完成后,UART 内核会对已接收的数据进行奇偶校验并且输出校验结果。最后,UART 内核会重置信号监测器,以准备进行下一次数据接收。
发送过程是由加载和发送两个步骤组成,如图 7 所示。加载步骤是 UART 内核按 RS-232串行发送的顺序将起始位、数据位、奇偶校验位和停止位加载到移位寄存器内,这个过程工作在系统时钟下,相对于 RS-232 的传输速度来说非常快。完成加载步骤后,UART 内核会重置波特率发生器,并且设置移位寄存器工作在波特率模式下,于是移位寄存器便在波特率时钟的驱动下依次将加载的数据发送到 RS-232 的发送端 TxD,这样便产生了 RS-232 的数据发送时序。
图 7 UART 发送数据流程图
下面依次介绍一下 UART 各个模块的实现方法。
3.3 信号监测器模块的实现
信号监测器模块的功能是监测 RS-232 输入端的信号,当有新的数据传输时通知 UART 内核开始接收数据,其端口定义如表 5 所示。
表 5 信号监测器端口定义表
在监测到传输的起始位后,信号监测器需要将自己锁定,即不对输入信号进行监测,直到UART 内核将其复位。信号监测器的实现代码如下:
-- 库声明
library IEEE;
use IEEE.STD_LOGIC_1164.all;
use WORK.UART_PACKAGE.ALL;
-- 实体声明
entity detector is
port (
clk : in std_logic;
reset_n : in std_logic;
RxD : in std_logic;
new_data : out std_logic );
end detector;
--}} End of automatically maintained section
-- 结构体
architecture detector of detector is
-- 信号监测器状态机
signal state : dt_state;
begin
-- enter your statements here --
-- 主过程
main : process(reset_n, clk)
begin
-- 复位信号
if reset_n = '0' then
state <= dt_unlock;
new_data <= '0';
elsif rising_edge(clk) then
-- 检查输入信号和状态,当输入为低并且不在锁定状态时,输出 new_data 信号
if state = dt_unlock and RxD = '0' then
new_data <= '1';
state <= dt_lock;
else
new_data <= '0';
end if;
end if;
end process;
end detector;
代码中的状态机 dt_state 是在 UART_PACKAGE 包中定义的,如下:
-- 信号监测器状态
type dt_state is (
dt_unlock, -- 未锁定状态
dt_lock -- 锁定状态
);
为了验证信号监测器模块的实现,需要编写一个测试平台测试其功能。测试平台的代码请参考 UART 工程源代码中的 detector_tb.vhd 文件,测试的结果如图 8 所示。
图 8 信号监测器仿真时序图
其中,RxD 第一次变为低时,new_data 信号产生输出;之后,RxD 又变为低,但由于信号监测器处于锁定状态,所以 new_data 并没有输出;最后,由于 reset_n 信号将信号监测器复位了,RxD 再次变为低时,new_data 上又有输出了。可见,信号监测器的实现完全正确,其功能完全符合设计的要求。
3.4 波特率发生器模块的实现
波特率发生器的功能是产生和 RS-232 通信所采用的波特率同步的时钟,这样才能方便地按照 RS-232 串行通信的时序要求进行数据接收或者发送。图 9 表示了波特率时钟和 RS-232接收端信号 RxD 之间的时序关系,波特率时钟的频率就是波特率。比如,波特率为 9600,即每秒传输 9600 位数据,则同步的波特率时钟频率为 9600,周期为 1/9600=0.10417 毫秒。
图 9 波特率时钟与 RxD 时序图
实现上述的波特率时钟的基本思路就是设计一个计数器,该计数器工作在速度很高的系统时钟下,当计数到某数值时将输出置高,再计数一定数值后再将输出置低,如此反复便能够得到所需的波特率时钟。假如 FPGA 的系统时钟为 50MHz,RS-232 通信的波特率为 9600,则波特率时钟的每个周期相当于个系统时钟的周期。假如要得到占空比为 50%的波特率时钟,只要使得计数器在计数到时将输出置高,之后在计数到 5208 时将输出置低并且重新计数,就能够实现和 9600 波特率同步的时钟,原理图如图 10 所示。
图 10 波特率时钟实现原理
波特率发生器的端口定义如表 6 所示。
表 6 波特率发生器端口定义表
波特率发生器在复位后,将内部计数器置为“0”,如果使能信号有效,则在每个系统时钟的上升沿工作,将计数器计数增加一。当输出一个完整的波特率时钟脉冲后,波特率发生器会自动将内部计数器置为零,同时开始进行下一个脉冲的计数。还有一个 indicator 信号,每产生一个完整的波特率时钟周期,indicator 信号会输出一个宽度的高电平。indicator 信号用于表示产生了一个完整的波特率时钟周期,UART 通过此信号来了解波特率发生器已输出的脉冲个数。
波特率发生器的 实现代码如下,其中在实体声明中声明了两个类属参数,FULL_PULSE_COUNT 表示一个波特率时钟完整的周期所对应的计数器计数, RISE_PULSE_COUNT表示波特率时钟信号上升时刻所对应的计数器计数,这样波特率时钟的占空比可以表示为:
-- 库声明
library IEEE;
use IEEE.STD_LOGIC_1164.all;
use WORK.UART_PACKAGE.ALL;
-- 实体声明
entity baudrate_generator is
-- 类属参数声明
generic (
FULL_PULSE_COUNT : BD_COUNT := BD9600_FPC;
RISE_PULSE_COUNT : BD_COUNT := BD9600_HPC );
-- 端口声明
port (
clk : in std_logic;
reset_n : in std_logic;
ce : in std_logic;
bd_out : out std_logic;
indicator : out std_logic );
end baudrate_generator;
--}} End of automatically maintained section
-- 结构体
architecture baudrate_generator of baudrate_generator is
begin
-- enter your statements here --
-- 主过程
-- main process
main : process( clk, reset_n )
variable clk_count : BD_COUNT;
begin
-- 判断复位信号
if reset_n = '0' then
bd_out <= '0';
indicator <= '0';
clk_count := 0;
-- 在时钟信号的上升沿动作
elsif rising_edge(clk) then
-- 判断使能信号
if ce = '1' then
-- 经过了 RISE_PULSE_COUNT 个计数,数脉冲上升
if clk_count = RISE_PULSE_COUNT-1 then -- pulse rise
bd_out <= '1';
clk_count := clk_count+1;
-- 经过了 FULL_PULSE_COUNT 个计数,数脉冲下降
elsif clk_count = FULL_PULSE_COUNT-1 then -- indicator output and pulse fall
-- 输出提示信号,使其为高
indicator <= '1';
bd_out <= '0';
-- 重置计数器计数为 0
clk_count := 0;
-- 恢复提示信号为低
elsif clk_count = 0 then
indicator <= '0';
clk_count := clk_count+1;
else
clk_count := clk_count+1;
end if;
end if;
end if;
end process;
end baudrate_generator;
以上代码中的 BD_COUNT 是在 UART_PACKAGE 库中定义的,它代表范围从 0~65535 的整数(即16 位整数);BD9600_FPC 代表波特率时钟完整周期对应的计数,而 BD9600_HFC 代表的是波特率时钟半周期对应的计数,它们也是在 UART_PACKAGE 库中定义的,如下所示:
-- 计数器计数范围
type BD_COUNT is range 65535 downto 0;
-- 9600 波特率对应参数
constant BD9600_FPC : BD_COUNT := 5208;
constant BD9600_HPC : BD_COUNT := 2604;
下面介绍一下波特率发生器的仿真测试过程。由于 9600 波特率对应的参数数值比较大,所 以 为 了 便 于 观 察 仿 真 的 波 形 , 可 以 首 先 选 用 数 值 较 小 的 测 试 数 据 。比 如 , 可 以 在UART_PACKAGE 库中定义完整波特率时钟周期对应计数为 10,半周期对应计数为 5,代码如下:
-- 波特率测试参数
constant BDTEST_FPC : BD_COUNT := 10;
constant BDTEST_HPC : BD_COUNT := 5;
在测试平台文件中,声明波特率发生器实例时应该将其类属参数设置为测试参数,如下所示:
-- 波特率发生器实例
UUT : baudrate_generator
generic map (
FULL_PULSE_COUNT => BDTEST_FPC,
RISE_PULSE_COUNT => BDTEST_HPC
)
port map (
clk => clk,
reset_n => reset_n,
ce => ce,
bd_out => bd_out,
indicator => indicator
);
完整的波特率发生器测试平台请参考 UART 工程源代码中的 baudrate_generator_tb.vhd文件,使用测试参数仿真得到的波形如图 11 所示。观察波形可以看到波特率发生器每经过10 个时钟周期输出一个完整的波特率时钟周期,占空比为 1/2,并且在每次输出完整脉冲后输出一个系统时钟脉宽的提示信号,可见波特率发生器的工作完全满足设计的要求。
图 11 波特率时钟实现原理
使用测试参数仿真正常后,可以使用实际的参数进行测试。
3.5 移位寄存器模块的实现
移位寄存器在整个设计中非常关键,无论是数据接收还是数据发送都需要使用到移位寄存器。移位寄存器的基本工作原理是在触发信号的驱动下将内部寄存器序列的最高位输出,将次高位到最低位向高位移一位,并且读取输入端的数据保存到最低位。图 12 所示为移位寄存器的工作原理。
图 12 移位寄存器工作原理图
由于 RS-232 通信在不同的传输设置下(比如奇偶校验位、停止位)总的数据位不同,所以为了能够灵活的配置移位寄存器,可以在声明移位寄存器实体的时候添加一个表示寄存器序列总长度的类属参数,代码如下:
entity shift_register is
-- generic
generic (
TOTAL_BIT : integer := 10 );
-- port
port (
clk : in std_logic;
reset_n : in std_logic;
din : in std_logic;
regs : out std_logic_vector(TOTAL_BIT-1 downto 0);
dout : out std_logic
);
end shift_register;
以上代码中的 TOTAL_BIT 表示的就是寄存器序列的长度,默认为 10,对应的传输设置是 8位数据位、奇偶校验位、1 位停止位。移位寄存器的端口定义如表 7 所示。
表 7 移位寄存器端口定义表
完整的移位寄存器实现代码如下:
-- 库声明
library IEEE;
use IEEE.STD_LOGIC_1164.all;
-- 实体声明
entity shift_register is
-- 类属参数
generic (
TOTAL_BIT : integer := 10 );
-- 端口
port (
clk : in std_logic;
reset_n : in std_logic;
din : in std_logic;
regs : out std_logic_vector(TOTAL_BIT-1 downto 0);
dout : out std_logic );
end shift_register;
--}} End of automatically maintained section
-- 结构体
architecture shift_register of shift_register is
-- 内部寄存器序列
signal shift_regs : std_logic_vector(TOTAL_BIT-1 downto 0) := (others => '1');
begin
-- 寄存器输出
regs <= shift_regs;
-- 主过程
main : process(reset_n, clk)
begin
-- 检查复位信号
if reset_n = '0' then
dout <= '1';
-- 在时钟上升沿动作
elsif rising_edge(clk) then
-- 将最高位输出到 dout
dout <= shift_regs(TOTAL_BIT-1);
-- 次高位到最低位都向高位移一位
shift_regs(TOTAL_BIT-1 downto 1) <= shift_regs(TOTAL_BIT-2 downto 0);
-- 读取输入端口信号并且保存到寄存器序列的最低位
shift_regs(0) <= din;
end if;
end process;
end shift_register
编写一个测试平台对上述的代码进行测试,得到的仿真波形如图 13 所示。移位寄存器复位后在每个时钟的上升沿工作,输出寄存器最高位,将寄存器移位并且将输入端保存到寄存器最低位。由于输入信号 din 以时钟周期为 “0”和“1”之间交替变化,所以移位寄存器每次保存到最低位的数据也是“0”、“1”交替变化,最后其内部寄存器也会呈现“0”、“1”交替的情况。所以上述的代码符合设计的要求。
图 13 移位寄存器仿真时序图
3.6 奇偶校验器模块的实现
奇偶校验器根据奇偶校验的设置和输入数据计算出奇偶校验位,所以在定义其实体时需要添加两个类属参数 DATA_LENGTH 和 PARITY_RULE,分别表示校验数据的长度和校验规则,代码如下:
entity parity_verifier is
generic (
DATA_LENGTH : integer := 8;
PARITY_RULE : PARITY := NONE );
port (
source : in std_logic_vector(DATA_LENGTH-1 downto 0);
parity : out std_logic
);
end parity_verifier;
其中,PARITY 是在 UART_PACKAGE 库中定义的,代码如下:
-- 奇偶校验规则定义
type PARITY is(
NONE, -- 无奇偶校验
ODD, -- 奇校验
EVEN -- 偶校验
);
奇偶校验器的端口定义如表 8 所示。
表 8 奇偶校验器端口定义表
奇偶校验器的实现代码如下:
-- 库声明
library IEEE;
use IEEE.STD_LOGIC_1164.all;
use WORK.UART_PACKAGE.ALL;
-- 实体声明
entity parity_verifier is
-- 类属参数
generic (
DATA_LENGTH : integer := 8;
PARITY_RULE : PARITY := NONE );
-- 端口
port (
source : in std_logic_vector(DATA_LENGTH-1 downto 0);
parity : out std_logic );
end parity_verifier;
--}} End of automatically maintained section
-- 结构体
architecture parity_verifier of parity_verifier is
begin
-- enter your statements here --
-- 按照校验规则计算校验位
with PARITY_RULE select
parity <= MultiXOR(source) when ODD, -- 奇校验
( not MultiXOR(source) ) when EVEN, -- 偶校验
'1' when others;
end parity_verifier;
上述奇偶校验器实现代码中的 MultiXOR 函数也是在 UART_PACKAGE 库中定义的,其功能是对输入序列进行异或计算。MultiXOR 的函数声明如下:
function MultiXOR(
din : in std_logic_vector )
return std_logic;
MultiXOR 的函数实现如下:
function MultiXOR(
din : in std_logic_vector )
return std_logic is
variable check : std_logic;
begin
check := din(din'LOW);
for i in 1 to (din'HIGH) loop
check := check xor din(i);
end loop;
return check;
end MultiXOR;
对上述奇偶校验器进行仿真测试,在偶校验设置下,仿真得到的波形如图 14 所示。
图 14 奇偶校验器仿真波形
注意:偶校验和奇校验的计算有一个技巧。如果使用偶校验,在待校验序列中有偶数个“1”,则得到的结果为“1”,反之为“0”;如果使用奇校验,在待校验序列中有奇数个“1”,则得到的结果为“1”,反之为“0”。
3.7 总线选择器模块的实现
总线选择器模块的功能是通过一个选择信号控制两个输入信号,将其输出,也就是一个二选一的选择器。图 15 是总线选择模块的框图,当选择信号 sel 为低时,将会把输入总线一din1 输出,否则在选择信号 sel 为高时,将会把输入总线二 din2 输出。
图 15 总线选择器框图
为了能够使总线选择器适应不同宽度的总线,在声明其实体的时候可以添加一个BUS_WIDTH 的类属参数,它表示总线的宽度,代码如下:
entity switcher_bus is
-- 类属参数
generic (
BUS_WIDTH : integer := 8 ); -- 总线宽度
port (
din1 : in std_logic_vector(BUS_WIDTH-1 downto 0); -- 输入总线一
din2 : in std_logic_vector(BUS_WIDTH-1 downto 0); -- 输入总线二
sel : in std_logic; -- 选择信号
dout : out std_logic_vector(BUS_WIDTH-1 downto 0) ); -- 输出总线
end switcher_bus;
总线选择器的端口定义如表 9 所示。
表 9 总线选择器端口定义表
总线选择器的实现代码如下:
-- 库声明
library IEEE;
use IEEE.STD_LOGIC_1164.all;
-- 实体声明
entity switcher_bus is
-- 类属参数
generic (
BUS_WIDTH : integer := 8 ); -- 总线宽度
port (
din1 : in std_logic_vector(BUS_WIDTH-1 downto 0); -- 输入总线一
din2 : in std_logic_vector(BUS_WIDTH-1 downto 0); -- 输入总线二
sel : in std_logic; -- 选择信号
dout : out std_logic_vector(BUS_WIDTH-1 downto 0) ); -- 输出总线
end switcher_bus;
--}} End of automatically maintained section
-- 结构体
architecture switcher_bus of switcher_bus is
begin
-- 使用 select 语句
with sel select
dout <= din1 when '0',
din2 when others;
end switcher_bus;
编写一个测试平台对上述代码进行测试,使得输入总线一上的数据为“11110000”,输入总线二上的数据为“00001111”,再分别使选择信号为“0”和“1”。得到的仿真波形如图 16所示,输出总线在选择信号为“0”时是“11110000”,即输入总线一的数据,而当选择信号变为“1”后,输出总线上的数据也变为输入总线二的数据“00001111”,可见上述代码完全实现了设计所需的功能。
图 16 总线选择器仿真波形
3.8 计数器模块的实现
计数器模块的功能是可控地在输入时钟驱动下进行计数,当到达计数上阈时给 UART 内核一个提示信号。在不同的工作状态下,计数器模块的输入时钟是不同的。UART 在数据发送之前需要进行数据加载(即将串行序列保存到移位寄存器内),在此过程中计数器模块的输入时钟就是系统时钟,因为此时移位寄存器也是工作在系统时钟下的。除了数据加载,另外两个需要计数器模块的过程是数据接收和数据发送,由于这两个过程中移位寄存器是工作在波特率时钟下的,所以计数器模块的时钟就是和波特率时钟同步的波特率发生器提示信号 indicator,这样每输出一个完整的波特率时钟周期计数器就能增加一。
计数器计数的上阈是在实体声明中定义的,代码如下。代码中的 MAX_COUNT 类属参数就是计数上阈。
entity counter is
generic (
MAX_COUNT : integer := 10 );
port (
clk : in std_logic;
reset_n : in std_logic;
ce : in std_logic;
overflow : out std_logic );
end counter;
计数器模块的端口定义如表 10 所示。
表 10 计数器端口定义表
计数器模块的实现代码如下:
-- 库声明
library IEEE;
use IEEE.STD_LOGIC_1164.all;
-- 实体声明
entity counter is
generic (
MAX_COUNT : integer := 10 );
port (
clk : in std_logic;
reset_n : in std_logic;
ce : in std_logic;
overflow : out std_logic );
end counter;
--}} End of automatically maintained section
-- 结构体
architecture counter of counter is
signal count : integer;
begin
-- enter your statements here --
-- 主过程
main: process( clk, reset_n )
begin
-- 判断复位信号
if reset_n = '0' then
count <= 0;
overflow <= '0';
-- 时钟信号的上升沿动作
elsif rising_edge(clk) and ce = '1' then
-- 在计数上阈时候输出提示信号 overflow
if count = MAX_COUNT-1 then
count <= 0;
overflow <= '1';
-- 恢复提示信号 overflow 为低
elsif count = 0 then
count <= count+1;
overflow <= '0';
else
count <= count+1;
end if;
end if;
end process;
end counter
对上述代码进行仿真测试,得到的仿真波形如图 17 所示。计数器在复位后并且 ce 有效时开始计数,并且在第 10 个时钟周期输出提示信号 overflow。
图 17 计数器仿真波形
3.9 UART 内核模块的实现
UART 内核模块是整个设计的核心,所以它也是整个设计中最为复杂的模块。由于 UART 内核模块的整体结构比较复杂,下面的内容将从模块接口、状态机设计和实现代码 3 方面介绍UART 内核的实现方法。
1)UART 内核模块的接口
(1)CPUUART 内核模块提供的 CPU 接口就是 UART 模块的 CPU 接口,如图 18 虚线框中所示。
图 18 UART 内核模块的 CPU 接口
这些端口又可以分为两组:第一组是与发送相关的,包括 send、send_bus 和 send_over,其中 send 信号是发送控制的信号,send_bus 是待发送数据的总线,send_over 是发送完成的提示信号;第二组是与接收相关的,包括 recv、recv_bus 和 error,其中 recv 信号表示有新的数据被接收,recv_bus 是接收数据的总线,error 信号表示数据接收产生错误。CPU 接口的端口定义如表 11 所示。
表 11 CPU 接口端口定义表
其中,DATA_BIT 是 UART 内核模块实体声明中定义的类属参数,表示数据位的长度。
(2)奇偶校验器UART
内核模块通过总线选择模块和奇偶校验器模块实现奇偶校验,它们之间的连接方式如图 19 所示。
图 19 奇偶校验相关模块连接示意图
总线选择器的两个输入端分别连接到数据发送总线 send_bus 和数据接收总线 recv_bus上,它的输出连接到奇偶校验器的输入端,最后奇偶校验器的校验结果输出连接到 UART 的一个端口 parity 上。这样的连接方式有一个好处,就是在发送和接收的不同过程中,只要通过一个总线选择信号 sel_pv 就能够选择不同的奇偶校验内容,sel_pv 信号是由 UART 内核的一个端口连接到总线选择器的选择信号端口上。
由上述内容可知, UART 使用的端口除了上面已经介绍的数据发送总线和数据接收总线外,就是总线选择信号 sel_pv 和奇偶校验信号 parity 了。它们的定义如表 12 所示。
表 12 奇偶校验端口定义表
(3)计数器模块
计数器模块的功能是在输入时钟的驱动下进行计数,当到达计数上阈时给 UART 内核一个提示信号,它们两者之间的连接方法如图 20 所示。
图 20 UART 内核模块和计数器模块连接示意图
在数据接收、数据加载和数据发送过程中,都需要使用到计数器,并且不同的过程中提供给计数器的时钟信号是不一样的。要选择计数器时钟信号,可以通过一个二选一选择器实现。二选一选择器的两个输入端分别连接到波特率发生器的 indicator 提示信号和系统时钟信号,同时其信号选择端口连接 UART 内核的一个 sel_clk 端口,这样便可以通过控制 sel_clk 端口实现对计数器模块时钟信号的控制。另一方面,要正确使用计数器需要对其进行复位和使能,所以 UART 内核还提供了两个端口 reset_parts 和 ce_parts,作为子模块复位信号和使能信号的端口。以上介绍的 4 个端口定义如表 5-13 所示。
表 13 奇偶校验端口定义表
(4)移位寄存器UART
内核和移位寄存器之间的接口主要可以分为 3 个作用:第一,UART 内核需要控制数据加载过程,所以具有向移位寄存器发送串行数据的接口;第二,已接收的数据是保存在移位寄存器内部的,所以移位寄存器具有提供内部寄存器数据的接口;第三,在不同的工作流程中,移位寄存器的工作时钟也不同,可能是波特率时钟,也可能是系统时钟,所以 UART 内核还需要有控制移位寄存器输入时钟的信号接口。图 21 是 UART 内核和移位寄存器之间的连接示意图。
图 21 UART 内核与移位寄存器连接示意图
UART 内核对数据加载过程的控制是通过 send_si 信号、sel_si 信号和一个二选一选择器实现的。图 21 中左侧的二选一选择器的作用即是串行数据选择,它的输入端分别接到 UART内核的串行数据发送端口 send_si 和 RS-232 的数据接收端口 RxD,信号选择端口则和 UART 内核的 sel_si 端口相连,输出端口连接到移位寄存器的数据输入端口。这样,在进行数据加时,UART 内核可以通过 sel_si 信号控制 UART 内核的串行数据输入端口作为移位寄存器的输入;当进行数据接收时,UART 内核又可以将 RS-232 的接收端口 RxD 选择为移位寄存器的输入。
UART 内核对移位寄存器输入时钟的控制方法和对计数器的控制方法一样,也是利用了一个二选一选择器,再通过选择信号控制。图 5-21 中右侧的二选一选择器的作用便是实现对移位寄存器输入时钟的选择,它的两个输入信号分别是波特率时钟 bd_clk 和系统时钟,选择信号连接到 UART 内核的一个端口 sel_clk 上,输出和移位寄存器的输入时钟端口相连。
移位寄存器内部寄存器的数据是通过一个 regs 端口发送给 UART 内核的,regs 端口是多位信号,其宽度就是 RS-232 串行通信的总位数(起始位、数据位、奇偶校验位和停止位)。表5-14 所示为 UART 内核和移位寄存器之间的端口。
表 14 UART 内核与移位寄存器间端口定义表
除了表 14 所列的端口,UART 内核的 reset_parts 端口还和移位寄存器的复位端口相连,作为它的复位信号。
(5)波特率发生器
UART 内核和波特率发生器之间的接口比较简单,只有复位和使能两个信号,即图 22 所示中的 reset_parts 和 ce_parts 信号。
图 22 UART 内核与波特率发生器连接示意图
波特率发生器的复位、使能信号与计数器的相同,其端口定义参考表 13 所示。
(6)信号监测器
UART 内核不但需要接收信号监测器的指示信号,同时还需要在完成数据接收后控制信号监测器复位。所以,UART 内核和信号监测器之间有两个接口,第一个是监测到数据传输的提示信号接口 new_data,另一个是用于复位信号监测器的 reset_dt 信号。图 23 所示是 UART内核与信号监测器连接示意图。
图 23 UART 内核与信号监测器连接示意图
UART 内核和信号监测器的端口定义如表 15 所示。
表 15 UART 内核与信号监测器间端口定义表
(7)RS-232 串行发送端口
移位寄存器在进行移位的时候,会将最高位输出,但是只有在发送数据的时候才需要将移位寄存器的数据串行输出,所以移位寄存器的输出端不能直接连到 RS-232 串行发送端口上,它们之间需要添加一个二选一选择器,如图 24 所示。
图 24 RS-232 串行发送端口连接示意图
图 24 中的二选一选择器的输入信号分别是高电平 VCC(即逻辑“1”)和移位寄存器输出 dout,选择信号连接到 UART 内核的一个端口 set_out,输出连接到 RS-232 串行发送端口TxD 上。这样,UART 内核就可以通过 sel_out 信号选择向 TxD 发送的数据,在发送过程中将移位寄存器输出 dout 送到 TxD 上,在其他的过程中则将高电平送到 TxD 上。
2)UART 内核模块的状态机设计
UART 内核模块的功能是控制数据接收、数据加载和数据发送的过程,这可以用状态机来实现。下面就按接收和发送的过程来介绍 UART 内核模块状态机的实现。
(1)数据接收过程
数据接收过程的流程图如图 6 所示,可以定义 3 个状态——空闲、接收和接收完成,其状态变换图如图 25 所示。
UART 内核模块在复位后进入空闲状态。如果信号监测器监测到数据传输,会给 UART 内核发送一个提示信号,UART 内核监测到此信号就会进入接收状态。在 UART 内核由空闲状态转为接收状态过程中,需要进行一系列的接收预备操作,包括将子模块复位、选择移位寄存器串行输入数据、选择移位寄存器时钟等。
进入接收状态后,波特率发生器开始工作,其输出波特率时钟驱动移位寄存器同步地存储 RS-232 接收端口上的数据,并且其提示信号驱动计数器进行计数。当所有数据接收完成,计数器也达到了其计数的上阈,它会给 UART 内核发送一个信号,使得 UART 内核进入接收完成的状态。UART 内核进入接收完成状态的同时,会检查奇偶校验的结果,同时使得子模块使能信号无效以停止各个子模块。UART 内核的接收完成状态仅仅保持一个时钟周期,设置这个状态的作用是借用一个时钟周期复位信号监测器,准备接收下次数据传输。
图 25 UART 内核数据接收状态转换图
(2)数据加载和发送过程
数据加载和发送的过程都是为了发送数据而设定的,所以将它们放在一起进行介绍。可以用 4 个状态来实现上述的过程,即空闲、加载、发送和发送完成,其中的空闲状态就是 UART内核复位后的空闲状态,和上面介绍的数据接收过程的空闲状态一致。数据加载和发送过程的状态转换图如图 26 所示。
图 26 UART 内核数据加载和发送状态转换图
数据加载过程在数据发送过程之前进行。UART 内核复位后进入空闲状态,当探测到发送控制信号有效时,便会进入加载状态开始数据加载。在进入加载状态的同时,UART 内核会将移位寄存器、计数器复位,并且通过选择信号使得移位寄存器的输入为 UART 内核产生的串行数据序列,使得移位存器和计数器的工作时钟为系统时钟。进入加载状态后,UART 内核会将完整的待发送序列加载到移位寄存器的数据输入端,发送的序列是和系统时钟同步的,移位寄存器则在系统时钟的驱动下不断读入输入端数据并且保存在内部寄存器内。在移位寄存器加载数据的同时,计数器也在时钟的驱动下进行计数,由于都是工作在系统时钟下,所以当所有数据被加载时,计数器也达到了计数的上阈(即串行数据的总量),它会产生一个提示信号使得UART 内核进入发送状态。
UART 内核进入发送状态的同时会改变几个选择信号,比如将移位寄存器时钟设为波特率时钟,将计数器时钟设为波特率的提示信号,最重要的是将输出信号送到 RS-232 的发送端口TxD 上。发送的过程和接收类似,移位寄存器在波特率时钟的驱动下内部寄存器的数据串行的发送出去,同时计数器在波特率发生器的提示信号驱动下进行计数。UART 内核在计数器到达计数上阈后便进入发送完成模式,并且输出发送完成信号。
3)UART 内核模块的实现代码
由于 UART 内核控制着所有的处理过程,并且还要跟大部分模块进行通信,所以它的实现代码比较复杂。为了能够便于读者理解,下面将分 5 部分对其进行介绍。
(1)实体声明
上面的内容已经介绍了 UART 内核和其他模块之间的接口,在实体声明中,需要将所有的接口都包括进去。表 16 所示总结了所有的 UART 内核接口。
表 16 UART 内核端口定义表
除了上述的端口,UART 内核模块的声明中还需要声明 3 个类属参数,分别是 DATA_BIT、TOTAL_BIT 和 PARITY_RULE,分别表示数据位个数、总数据个数、奇偶校验规则。
UART 内核的实体声明代码如下:
entity uart_core is
generic (
-- 数据位个数
DATA_BIT : integer := 8;
-- 总数据个数
TOTAL_BIT : integer := 10;
-- 奇偶校验规则
PARITY_RULE : PARITY := NONE );
port (
-- 时钟和复位信号
clk : in std_logic;
reset_n : in std_logic;
-- 和信号监测器的接口信号
new_data : in std_logic;
reset_dt : out std_logic;
-- 复位、使能子模块的信号
reset_parts : out std_logic;
ce_parts : out std_logic;
-- 和移位寄存器的接口信号
send_si : out std_logic;
sel_si : out std_logic;
regs : in std_logic_vector(TOTAL_BIT-1 downto 0);
-- 计数器时钟选择信号和计数器计数到达上阈的指示信号
sel_clk : out std_logic;
overflow : in std_logic;
-- 和奇偶校验器的接口信号
sel_pv : out std_logic;
parity : in std_logic;
-- 输出选择信号
sel_out : out std_logic;
-- 提供给 CPU 的接口信号
send : in std_logic;
send_bus : in std_logic_vector(DATA_BIT-1 downto 0);
send_over : out std_logic;
recv : out std_logic;
recv_bus : out std_logic_vector(DATA_BIT-1 downto 0);
error : out std_logic );
end uart_core;
(2)内部信号定义
在 UART 内核模块内部需要定义 3 个信号,如下:
signal state : UART_STATE := UART_IDLE;
signal send_buf : std_logic_vector(TOTAL_BIT-1 downto 0);
signal si_count : integer range 0 to 15 := 0;
其中 state 信号是状态机状态信号;send_buf 表示待发送串行序列的缓冲寄存器;si_count 是发送序列的索引信号,在生成加载的串行发送序列时候需要使用到。
(3)串行加载序列的生成方法
串行加载序列的生成有两个步骤,第一个步骤是将起始位、数据位、奇偶校验的结果等存储到待发送串行序列的缓存寄存器内。这是通过一个过程来实现的,过程的触发信号是数据发送总线和奇偶校验输入信号,代码如下。此过程的功能除了存储奇偶校验结果外,还包括存储起始位的功能。
-- 生成串行加载序列
send_buffer: process(send_bus, parity)
begin
-- 存储起始位
send_buf(0) <= '0';
-- 存储数据位
send_buf(DATA_BIT downto 1) <= send_bus(DATA_BIT-1 downto 0);
-- 存储奇偶校验位和停止位
if PARITY_RULE = ODD or PARITY_RULE = EVEN then
send_buf(DATA_BIT+1) <= parity;
send_buf(TOTAL_BIT-1 downto DATA_BIT+2) <= (others => '1');
else
send_buf(TOTAL_BIT-1 downto DATA_BIT+1) <= (others => '1');
end if;
end process;
第二个步骤是将 send_buf 寄存器序列中的数据发送到 send_si 端口上,发送的时序应该和系统时钟同步。此步骤也是利用一个过程实现的,代码如下。其中 si_count 是加载串行序列的索引,UART 内核在加载过程中,每经过一个时钟就会将 si_count 增加 1。
-- serial input switch
si_switch: process(reset_n, si_count)
begin
-- 复位
if reset_n = '0' then
send_si <= '1';
else
-- 将 send_buf 里面的数据送到 send_si 端口上
send_si <= send_buf(si_count);
end if;
end process;
(4)复位处理
UART 内核模块是由 reset_n 信号控制复位的,此信号为低即表示复位有效。复位的处理是在一个 UART 内核的主过程中实现的,代码如下:
-- 主过程
main: process(clk, reset_n)
begin
if reset_n = '0' then
-- 信号监测器复位信号
reset_dt <= '1';
-- 其他模块的复位和使能信号
reset_parts <= '0';
ce_parts <= '0';
-- 移位寄存器输入
sel_si <= '0';
-- 波特率发生器和计数器的时钟选择信号
sel_clk <= '0';
-- 奇偶校验器的输入
sel_pv <= '0';
-- 选择 TxD 输出
sel_out <= '0';
-- 与 CPU 之间的接口信号
send_over <= '0';
recv <= '0';
error <= '0';
-- 状态机
state <= UART_IDLE;
-- 串行加载的计数
si_count <= 0;
elsif rising_edge(clk) then
-- 状态机实现
end if
end process;
(5)UART 内核模块的状态机实现
UART 内核的主过程除了处理复位信号外,还控制了数据发送和数据接收的状态转换,即实现了状态机,代码如下:
-- 主过程
main: process(clk, reset_n)
begin
if reset_n = '0' then
-- 复位处理
elsif rising_edge(clk) then
case state is
-- 空闲状态
when UART_IDLE =>
-- 当信号监测器监测到数据时,new_data 变为‘1’
if new_data = '1' then
-- 复位子模块
reset_parts <= '0';
-- 子模块使能无效
ce_parts <= '0';
-- 选择移位寄存器串行输入为 RxD
sel_si <= '1';
-- 选择移位寄存器的时钟为波特率始终
-- 选择计数器的时钟为波特率发生器的指示信号
sel_clk <= '0';
-- 使得输出保持为’1’
sel_out <= '0';
-- 设置奇偶校验的数据源为数据发送总线
sel_pv <= '1';
-- 改变状态为接收
state <= UART_RECV;
-- 当 send 信号变为‘1’,表示 CPU 要求发送数据
elsif send = '1' then
-- 复位子模块
reset_parts <= '0';
-- 子模块使能无效
ce_parts <= '0';
-- 选择移位寄存器串行输入为串行加载序列
sel_si <= '0';
-- 选择移位寄存器的时钟为波特率始终
-- 选择计数器的时钟为波特率发生器的指示信号
sel_clk <= '0';
-- 使得输出保持为‘1’
sel_out <= '0';
-- 设置奇偶校验的数据源为数据发送总线
sel_pv <= '0';
-- 初始化串行加载序列的索引变量
si_count <= TOTAL_BIT-1;
-- 改变状态为加载
state <= UART_LOAD;
else
-- 停止对信号监测器的复位
reset_dt <= '1';
end if;
-------- 数据加载和发送状态--------
-- 加载状态
when UART_LOAD =>
-- 如果 overflow 信号为‘1’,表示数据加载完成
if overflow = '1' then
-- 复位子模块
reset_parts <= '0';
-- 子模块使能信号无效
ce_parts <= '0';
-- 选择移位寄存器串行输入为串行加载序列
sel_si <= '0';
-- 选择移位寄存器的时钟为波特率始终
-- 选择计数器的时钟为波特率发生器的指示信号
sel_clk <= '0';
-- 使得输出保持为‘1’
sel_out <= '0';
-- 设置奇偶校验的数据源为数据发送总线
sel_pv <= '0';
-- 改变状态为发送
state <= UART_SEND;
else
-- 选择移位寄存器的时钟为系统时钟
-- 选择计数器的时钟为系统时钟
sel_clk <= '1';
-- 通过增加 si_count,生成串行加载序列
if not(si_count = TOTAL_BIT-1) then
si_count <= si_count+1;
else
si_count <= 0;
end if;
-- 子模块复位信号无效
reset_parts <= '1';
-- 子模块使能信号有效
ce_parts <= '1';
end if;
-- 发送状态
when UART_SEND =>
-- 如果 overflow 为‘1’,表示发送完成
if overflow = '1' then
-- 输出发送完成的指示信号
send_over <= '1';
-- 改变状态为发送完成
state <= UART_END_SEND;
else
-- 子模块复位信号无效
reset_parts <= '1';
-- 子模块使能信号有效
ce_parts <= '1';
end if;
-- 发送完成状态
when UART_END_SEND =>
-- 子模块使能信号无效
ce_parts <= '0';
-- 复位信号监测器
reset_dt <= '0';
-- 恢复发送完成指示信号
send_over <= '0';
-- 改变状态为空闲
state <= UART_IDLE;
-------- 数据接收状态--------
-- 接收状态
when UART_RECV =>
-- 如果 overflow 变为“1”,表示接收完成
if overflow = '1' then
-- 输出接收指示信号
recv <= '1';
-- 改变状态为接收完成
state <= UART_END_RECV;
else
-- 子模块复位信号无效
reset_parts <= '1';
-- 子模块使能信号有效
ce_parts <= '1';
end if;
-- 接收完成状态
when UART_END_RECV =>
-- 进行奇偶校验
if not(regs(0) = parity) then
error <= '1';
end if;
-- 子模块使能信号无效
ce_parts <= '0';
-- 复位信号监测器
reset_dt <= '0';
-- 恢复接收完成指示信号
recv <= '0';
-- 改变状态为空闲
state <= UART_IDLE;
-- 如果产生未知状态,输出错误信息
when others =>
error <= '1';
-- 恢复到空闲状态
state <= UART_IDLE;
end case;
end if;
end process;
3.10 UART 顶层模块的实现
上面介绍了 UART 各个模块的基本原理和实现方法,要实现 UART 还需要将所有的模块连接起来,即需要编写一个顶层模块。顶层模块实现了所有 UART 和外部器件之间的通信接口,端口定义如表 17 所示。
表 17 UART 顶层模块端口定义表
由于 UART 顶层模块包括了所有的子模块,所以其实现代码也比较复杂,为了便于读者理解,下面分 3 个部分进行介绍。
1)实体声明
UART 顶层模块的实体声明中除了端口的声明外,还需要声明所有子模块需要使用的类属参数,包括 DATA_BIT(数据位个数)、TOTAL_BIT(总数据个数)、PARITY_RULE(奇偶校验规则)、FULL_PULSE_COUNT(完整波特率时钟对应的计数)和 RISE_PULSE_COUNT(波特率时钟上升沿对应的计数)。
实体声明部分的代码如下:
library IEEE;
use IEEE.std_logic_1164.all;
use WORK.UART_PACKAGE.all;
entity UART is
generic(
-- 数据位个数
DATA_BIT : integer := 8;
-- 总数据个数
TOTAL_BIT : integer := 10;
-- 奇偶校验规则
PARITY_RULE : PARITY := NONE;
--完整波特率时钟对应的计数
FULL_PULSE_COUNT : BD_COUNT := BD9600_FPC;
--波特率时钟上升沿对应的计数
RISE_PULSE_COUNT : BD_COUNT := BD9600_HPC
);
port(
-- 时钟信号
clk : in STD_LOGIC;
-- 复位信号
reset_n : in STD_LOGIC;
-- 发送控制信号
send : in STD_LOGIC;
-- 数据发送总线
send_bus : in STD_LOGIC_VECTOR(7 downto 0);
-- 发送完成信号
send_over : out STD_LOGIC;
-- 错误提示信号
error : out STD_LOGIC;
-- 接收提示信号
recv : out STD_LOGIC;
-- 数据接收总线
recv_buf : out STD_LOGIC_VECTOR(7 downto 0);
-- RS-232 数据接收端口
RxD : in STD_LOGIC;
-- RS-232 数据发送端口
TxD : out STD_LOGIC;
);
end UART;
2)子模块和内部信号声明
子模块声明就是将各个子模块实体端口、类属参数的定义方式按照组件的格式声明一遍。声明组件的格式和声明实体完全一致,惟一的差别在于实体声明使用 entity 和 end entity,而组件声明使用 component 和 end component,所以实际编写过程中没必要完整地书写一遍声明内容,只需要将实体声明的代码拷贝过来并将 entity 修改为 component 即可。
UART 顶层模块中实体声明的代码如下:
-- 波特率发生器组件声明
component baudrate_generator
generic(
FULL_PULSE_COUNT : BD_COUNT := BD9600_FPC;
RISE_PULSE_COUNT : BD_COUNT := BD9600_HPC
);
port (
ce : in STD_LOGIC;
clk : in STD_LOGIC;
reset_n : in STD_LOGIC;
bg_out : out STD_LOGIC;
indicator : out STD_LOGIC
);
end component;
-- 计数器组件声明
component counter
generic(
MAX_COUNT : INTEGER := 10
);
port (
ce : in STD_LOGIC;
clk : in STD_LOGIC;
reset_n : in STD_LOGIC;
overflow : out STD_LOGIC
);
end component;
-- 信号监测器
component detector
port (
RxD : in STD_LOGIC;
clk : in STD_LOGIC;
reset_n : in STD_LOGIC;
new_data : out STD_LOGIC
);
end component;
-- 奇偶校验器
component parity_verifier
generic(
DATA_LENGTH : INTEGER := 8;
PARITY_RULE : PARITY := NONE
);
port (
source : in STD_LOGIC_VECTOR(DATA_LENGTH-1 downto 0);
parity : out STD_LOGIC
);
end component;
-- 移位寄存器
component shift_register
generic(
TOTAL_BIT : INTEGER := 10
);
port (
clk : in STD_LOGIC;
din : in STD_LOGIC;
reset_n : in STD_LOGIC;
dout : out STD_LOGIC;
regs : out STD_LOGIC_VECTOR(TOTAL_BIT-1 downto 0)
);
end component;
-- 二选一选择器
component switch
port (
din1 : in STD_LOGIC;
din2 : in STD_LOGIC;
sel : in STD_LOGIC;
dout : out STD_LOGIC
);
end component;
-- 总线选择器
component switch_bus
generic(
BUS_WIDTH : INTEGER := 8
);
port (
din1 : in STD_LOGIC_VECTOR(BUS_WIDTH-1 downto 0);
din2 : in STD_LOGIC_VECTOR(BUS_WIDTH-1 downto 0);
sel : in STD_LOGIC;
dout : out STD_LOGIC_VECTOR(BUS_WIDTH-1 downto 0)
);
end component;
-- UART 内核
component uart_core
generic(
DATA_BIT : INTEGER := 8;
PARITY_RULE : PARITY := NONE;
TOTAL_BIT : INTEGER := 10
);
port (
clk : in STD_LOGIC;
new_data : in STD_LOGIC;
overflow : in STD_LOGIC;
parity : in STD_LOGIC;
regs : in STD_LOGIC_VECTOR(TOTAL_BIT-1 downto 0);
reset_n : in STD_LOGIC;
send : in STD_LOGIC;
send_bus : in STD_LOGIC_VECTOR(DATA_BIT-1 downto 0);
ce_parts : out STD_LOGIC;
error : out STD_LOGIC;
recv : out STD_LOGIC;
recv_bus : out STD_LOGIC_VECTOR(DATA_BIT-1 downto 0);
reset_dt : out STD_LOGIC;
reset_parts : out STD_LOGIC;
sel_clk : out STD_LOGIC;
sel_out : out STD_LOGIC;
sel_pv : out STD_LOGIC;
sel_si : out STD_LOGIC;
send_over : out STD_LOGIC;
send_si : out STD_LOGIC
);
end component;
完成组件声明后,需要对内部信号进行声明。内部信号的主要作用有两种,第一种是作为各个模块(组件)之间的连接信号,第二种是作为寄存器使用。在 UART 顶层模块中的内部信号主要用于连接各个组件(模块),即作为连接信号使用。
内部信号声明的代码如下:
---- 常数 -----
constant VCC_CONSTANT : STD_LOGIC := '1';
---- 内部信号声明 ----
signal bg_clk : STD_LOGIC;
signal bg_out : STD_LOGIC;
signal ce_parts : STD_LOGIC;
signal clk_inv : STD_LOGIC;
signal counter_clk : STD_LOGIC;
signal indicator : STD_LOGIC;
signal new_data : STD_LOGIC;
signal overflow : STD_LOGIC;
signal parity : STD_LOGIC;
signal reset_dt : STD_LOGIC;
signal reset_parts : STD_LOGIC;
signal sel_clk : STD_LOGIC;
signal sel_out : STD_LOGIC;
signal sel_pv : STD_LOGIC;
signal sel_si : STD_LOGIC;
signal send_si : STD_LOGIC;
signal sr_in : STD_LOGIC;
signal sr_out : STD_LOGIC;
signal VCC : STD_LOGIC;
signal pv_source : STD_LOGIC_VECTOR (DATA_BIT-1 downto 0);
signal recv_parity_source : STD_LOGIC_VECTOR (DATA_BIT-1 downto 0);
signal regs : STD_LOGIC_VECTOR (TOTAL_BIT-1 downto 0);
signal send_parity_source : STD_LOGIC_VECTOR (DATA_BIT-1 downto 0);
3)子模块实例化
子模块实例化表示的就是根据子模块(组件)的声明定义一个子模块实例,同时定义此实例的信号连接方式以及类属参数等。
UART 顶层模块的子模块实例化代码如下:
-- 波特率发生器实例
U_BG : baudrate_generator
port map(
bg_out => bg_out,
ce => ce_parts,
clk => clk,
indicator => indicator,
reset_n => reset_parts
);
-- 总线选择器实例
U_BusSwitch : switch_bus
port map(
din1 => send_parity_source( DATA_BIT-1 downto 0 ),
din2 => recv_parity_source( DATA_BIT-1 downto 0 ),
dout => pv_source( DATA_BIT-1 downto 0 ),
sel => sel_pv
);
-- UART 内核实例
U_Core : uart_core
port map(
ce_parts => ce_parts,
clk => clk,
error => error,
new_data => new_data,
overflow => overflow,
parity => parity,
recv => recv,
recv_bus => recv_parity_source( DATA_BIT-1 downto 0 ),
regs => regs( TOTAL_BIT-1 downto 0 ),
reset_dt => reset_dt,
reset_n => reset_n,
reset_parts => reset_parts,
sel_clk => sel_clk,
sel_out => sel_out,
sel_pv => sel_pv,
sel_si => sel_si,
send => send,
send_bus => send_parity_source( DATA_BIT-1 downto 0 ),
send_over => send_over,
send_si => send_si
);
-- 计数器实例
U_Counter : counter
port map(
ce => ce_parts,
clk => counter_clk,
overflow => overflow,
reset_n => reset_parts
);
-- 计数器时钟源选择器
U_CounterClkSwitch : switch
port map(
din1 => indicator,
din2 => clk_inv,
dout => counter_clk,
sel => sel_clk
);
-- 信号监测器
U_Detector : detector
port map(
RxD => RxD,
clk => clk,
new_data => new_data,
reset_n => reset_dt
);
-- 奇偶校验器
U_ParityVerifier : parity_verifier
port map(
parity => parity,
source => pv_source( DATA_BIT-1 downto 0 )
);
-- 移位寄存器输入源选择器实例
U_SISwitch : switch
port map(
din1 => send_si,
din2 => RxD,
dout => sr_in,
sel => sel_si
);
-- 移位寄存器实例
U_SR : shift_register
port map(
clk => bg_clk,
din => sr_in,
dout => sr_out,
regs => regs( TOTAL_BIT-1 downto 0 ),
reset_n => reset_parts
);
-- 移位寄存器时钟源选择器实例
U_SRClkSwitch : switch
port map(
din1 => bg_out,
din2 => clk_inv,
dout => bg_clk,
sel => sel_clk
);
-- 输出选择器实例
U_TXDSwitch : switch
port map(
din1 => VCC,
din2 => sr_out,
dout => TxD,
sel => sel_out
);
以上便是 UART 顶层模块的实现方法, UART 顶层模块就是将 UART 内核和其他模块连接起来组成一个完成的模块。
3.11 测试平台的编写和仿真
为了验证 UART 实现的正确性,需要设计一个仿真平台对 UART 顶层模块进行仿真,下面就介绍一下 UART 仿真平台的编写方法和仿真结果的分析。仿真平台是一个 VHDL 文件,其本身也是一个实体(entity)。仿真平台除了包含了实体声明(entity)和结构体(architecture)以外,还需要有一个配置(configuration)。例如,针对 UART 顶层模块 uart_top.vhd 编写的测试平台就有如下的结构:
-- 库声明
library ieee;
use work.uart_package.all;
use ieee.std_logic_1164.all;
-- 实体声明
entity uart_top_tb is
-- 实体声明内容(略)
end uart_top_tb;
-- 结构体
architecture TB_ARCHITECTURE of uart_top_tb is
-- 结构体内容(略)
end TB_ARCHITECTURE;
-- 配置
configuration TESTBENCH_FOR_uart_top of uart_top_tb is
for TB_ARCHITECTURE
for UUT : uart_top
use entity work.uart_top(uart_top);
end for;
end for;
end TESTBENCH_FOR_uart_top;
从上面的代码可以看出,配置的作用就是为测试对象指定一个结构体,下面从 3 个方面介绍测试平台的实现代码。
1)实体声明
一般来说,测试平台的实体声明中不会有输入/输出信号,仅包括其测试对象所需要的类属参数。UART 测试平台的实体声明如下:
entity uart_top_tb is
-- 定义类属参数
generic(
DATA_BIT : INTEGER := 8;
TOTAL_BIT : INTEGER := 10;
PARITY_RULE : PARITY := none;
FULL_PULSE_COUNT : BD_COUNT := 5208;
RISE_PULSE_COUNT : BD_COUNT := 2604
);
end uart_top_tb;
2)组件和信号声明
组件声明就是对测试对象的声明。在测试平台中,测试对象是作为一个组件来呈现的。比如 UART 测试平台中对 UART 顶层模块的组件声明如下:
-- UART 顶层模块组件声明
component uart_top
generic(
DATA_BIT : INTEGER := 8;
TOTAL_BIT : INTEGER := 10;
PARITY_RULE : PARITY := none;
FULL_PULSE_COUNT : BD_COUNT := 5208;
RISE_PULSE_COUNT : BD_COUNT := 2604
);
port(
RxD : in std_logic;
clk : in std_logic;
reset_n : in std_logic;
send : in std_logic;
send_bus : in std_logic_vector(7 downto 0);
TxD : out std_logic;
error : out std_logic;
recv : out std_logic;
send_over : out std_logic;
recv_buf : out std_logic_vector(7 downto 0)
);
end component;
测试对象肯定有一些输入/输出信号,它们在测试平台中是定义为内部信号的,可以直接对这些内部信号进行赋值来控制测试对象的输入信号。实际上,一般来说测试平台的内部信号都是和测试对象的输入/输出信号一一对应的,代码如下:
-- 内部信号
signal RxD : std_logic := '1';
signal clk : std_logic := '0';
signal reset_n : std_logic := '0';
signal send : std_logic := '0';
signal send_bus : std_logic_vector(7 downto 0) := (others => '0');
signal TxD : std_logic := '1';
signal error : std_logic := '0';
signal recv : std_logic := '0';
signal send_over : std_logic := '0';
signal recv_buf : std_logic_vector(7 downto 0) := (others => '0');
3)测试流程的控制
编写测试流程的第一个步骤是对测试对象的实例化,即将 UART 顶层模块实例化,实现代码如下:
-- 测试对象实例化
UUT : uart_top
generic map (
DATA_BIT => DATA_BIT,
TOTAL_BIT => TOTAL_BIT,
PARITY_RULE => PARITY_RULE,
FULL_PULSE_COUNT => FULL_PULSE_COUNT,
RISE_PULSE_COUNT => RISE_PULSE_COUNT
);
port map (
RxD => RxD,
clk => clk,
reset_n => reset_n,
send => send,
send_bus => send_bus,
TxD => TxD,
error => error,
recv => recv,
send_over => send_over,
recv_buf => recv_buf
)
第二个步骤是产生时钟信号,由于时钟信号比较有规律,所以可以用一个过程(process)来实现,代码如下:
-- 产生时钟信号
clk_gen : process
begin
clk <= not clk;
wait for 10 ns;
end process;
最后一个步骤就是实现测试的主流程,一般是在一个过程(Process)中实现。对于 UART的测试,主要的内容就是数据发送的测试和数据接收的测试,测试主流程的流程图如图 27所示。
图 27 UART 测试流程图
测试主流程的实现代码如下:
-- 测试主流程
main: process
begin
-- 复位
reset_n <= '0';
wait for 100 ns;
-- 结束复位
reset_n <= '1';
wait for 100 ns;
-- 测试数据发送
wait for 10 ns;
-- 发送数据为 01010101
send_bus <= "01010101";
-- send 为高激活数据发送
send <= '1';
wait for 20 ns;
send <= '0';
-- 测试数据接收
-- 使用测试用波特率
if FULL_PULSE_COUNT = BDTEST_FPC then
wait for 2500 ns;
-- 仿真 RS-232 输入信号 RxD
for i in 0 to 9 loop
RxD <= test_si_none(i);
-- 测试波特率为 10,所以输入间隔 10 个时钟,总共 200ns
wait for 200 ns;
end loop;
-- 使用实际波特率 9600
elsif FULL_PULSE_COUNT = BD9600_FPC then
wait for 1.2 ms;
-- 仿真 RS-232 输入信号 RxD
for i in 0 to 9 loop
RxD <= test_si_none(i);
-- 测试波特率为 9600,所以输入间隔 9600 个时钟,总共 104.17μs
wait for 104.17 us;
end loop;
end if;
wait ;
end process;
上面代码中的 test_si_none 是在 UART_PACKAGE 库中定义的输入测试数据串行序列(无奇偶教研),此外还定义了奇校验和偶校验对应的序列,代码如下:
-- 类型声明
type test_vectors is array (0 to 10) of std_logic;
-- 无奇偶校验测试序列
constant test_si_none : test_vectors :=
('0', '1', '0', '1', '0', '1', '0', '1', '0', others => '1');
-- 奇校验测试序列
constant test_si_odd : test_vectors :=
('0', '1', '0', '1', '0', '1', '1', '1', '0', '1', others => '1');
-- 偶校验测试序列
constant test_si_even : test_vectors :=
('0', '1', '0', '1', '0', '1', '0', '1', '0', '0', others => '1');
在波特率为 9600 情况下利用上述测试平台对 UART 进行仿真,得到数据发送的仿真结果分别如图 28 所示。
图 5-28 UART 数据发送仿真结果
从图 28 可以看出,待发送的数据是 0x55(十六进制,即 send_bus 总线上的数据),由send 信号触发后,RS-232 的 TxD 端输出为序列 001010101(二进制),其中第一位是起始位,中间的八位正是待发送的数据 0xFF,最后再发送完成后输出提示信号 send_over。可见,发送的结果符合 RS-232 的时序要求,UART 的发送功能完全正确。
同样测试条件下数据接收的仿真结果如图 29 所示。首先,RxD 上的数据序列为0101010010(二进制),表示起始位 0,之后数据位是 10101010(二进制),所以待接收的数据是 0xAA(十六进制)。recv_buf 是数据接收总线,可以看到其最终得到的数据正是 0xAA(十六进制),并且,在接收完成后 recv 信号会输出一个脉宽的高电平作为提示。由上述可知,数据接收的过程也完全正确。
图 29 UART 数据接收仿真结果
本篇到此结束,各位大侠,有缘再见!
END
后续会持续更新,带来Vivado、 ISE、Quartus II 、candence等安装相关设计教程,学习资源、项目资源、好文推荐等,希望大侠持续关注。
大侠们,江湖偌大,继续闯荡,愿一切安好,有缘再见!
精彩推荐
FPGA 高级设计:时序分析和收敛
基于FPGA的单目内窥镜定位系统设计(下)
基于FPGA的扩频系统设计(下)
FPGA工程师就业班,9月份开课!