3.2 实验 2
3.2 实验 2
3.2 实验 2
重要文件: lab2 实验手册
代码解析
这个实验相较于上一个实验较复杂,涉及了6个Verilog文件,我们先进行一个层级分类。
uart_led.vmeta_harden.vuart_rx.vmeta_harden.vuart_baud_gen.vuart_rx_ctl.v
led_ctl.v
这就是这个lab的设计层级结果,可以理解成每个v文件调用了别的v文件中的模块,最终形成了一个完整的项目。
我们从上往下分析,首先是 uart_led.v 文件,该顶层文件主要是调用模块和传递参数,把uart和led拼接起来。具体可以自行查看代码
led_ctl.v 文件定义了一个led模块,并且可以将一个8位的数据传递给led模块,led会进显示。
主要实现的代码部分如下:
always @(posedge clk_rx)
begin
if (rst_clk_rx)
begin
old_rx_data_rdy <= 1'b0;
char_data <= 8'b0;
led_o <= 8'b0;
end
else
begin
// Capture the value of rx_data_rdy for edge detection
old_rx_data_rdy <= rx_data_rdy;
// If rising edge of rx_data_rdy, capture rx_data
if (rx_data_rdy && !old_rx_data_rdy)
begin
char_data <= rx_data;
end
// Output the normal data or the data with high and low swapped
if (btn_clk_rx)
led_o <= {char_data[3:0],char_data[7:4]};
else
led_o <= char_data;
end // if !rst
end // always
其中always @(posedge clk_rx)是每当clk_rx上升沿时就会进入该模块,首先利用if (rst_clk_rx)判断是否处于复位状态,如果是则将old_rx_data_rdy、char_data、led_o都置为0(即重置所有状态),否则就会进入else部分
当不处于复位状态(进入else)时,首先将rx_data_rdy这个串口数据已经准备好的参数<=给old_rx_data_rdy
需要注意的是这里的<=是非阻塞赋值,该操作会在所有always模块执行完后才进行更新!!!
接下来进行if (rx_data_rdy && !old_rx_data_rdy)判断。
该判定逻辑是如果当前时钟周期的rdy是1(表示有rx数据)同时上一个时钟周期的rdy是0(表示上一个周期没有数据),则说明这是一个新的数据,此时更新char_data为rx_data(即串口数据)
需要注意的是由于非阻塞赋值的情况存在,old_rx_data_rdy在if判断的时候还未更新,所以是上一个时钟周期的值。同时后续操char_data的更新最终也发生在always模块执行之后。
最后是led显示部分,首先检测btn_clk_rx(外部输入按钮)的值是否为1,如果是则将led_o的低4位和高4位交换,否则直接显示char_data
接下来说明一下meta_harden.v与uart_baud_gen.v的作用
meta_harden.v主要实现了双重同步的功能,目的是将一个异步信号同步到目标时钟域
简单来说,假设这个异步信号是按键,按键被触发并不是同步于FPGA时钟信号的,当触发的时候刚好在时钟信号的上升沿时候,就会出现这个按键电平变得不稳定(亚稳态)导致后续读取问题。这是异步信号同步会发生的问题,所以需要一个双重同步的过程
实现代码:
always @(posedge clk_dst)
begin
if (rst_dst)
begin
signal_meta <= 1'b0;
signal_dst <= 1'b0;
end
else // if !rst_dst
begin
signal_meta <= signal_src;
signal_dst <= signal_meta;
end // if rst
end // always
双重同步实现起来并不复杂,首先要理解前面提到的非阻塞赋值概念,即<=操作会在所有always模块执行完后才进行更新。所以在这个模块中,首先将signal_src赋值给signal_meta,下个周期才能再将signal_meta赋值给signal_dst
当然根据这个原理你也可以实现n重同步,多写几个<=赋值即可,具体取决于项目需求
uart_baud_gen.v主要是用来产生波特率,该部分重点是实现过采样
我们先首先要理解他的逻辑部分,稍后我们会进行分析如何得到这个频率计数数值。
逻辑代码如下:
assign internal_count_m_1 = internal_count - 1'b1;
always @(posedge clk)
begin
if (rst)
begin
internal_count <= OVERSAMPLE_VALUE;
baud_x16_en_reg <= 1'b0;
end
else
begin
// Assert baud_x16_en_reg in the next clock when internal_count will be
// zero in that clock (thus when internal_count_m_1 is 0).
baud_x16_en_reg <= (internal_count_m_1 == {CNT_WID{1'b0}});
// Count from OVERSAMPLE_VALUE down to 0 repeatedly
if (internal_count == {CNT_WID{1'b0}})
begin
internal_count <= OVERSAMPLE_VALUE;
end
else // internal_count is not 0
begin
internal_count <= internal_count_m_1;
end
end // if rst
end // always
首先我们得大概了解一下assign internal_count_m_1 = internal_count - 1'b1;的作用,这是一个连续赋值的语句,物理上是通过硬件组合逻辑实现,当internal_count变化时,internal_count_m_1也会马上变化,不需要等待时钟同步。
该模块always @(posedge clk)是在每一个时钟上升沿进入,当复位时候赋值internal_count为OVERSAMPLE_VALUE,同时baud_x16_en_reg为0
上述逻辑部分实现了,每多少个时钟周期触发一次baud_x16_en_reg,假设时钟周期是1Khz,每10次触发一次就是进行100Hz的采样,如果波特率是10Hz那么就进行了10倍的过采样
实现代码:
parameter BAUD_RATE = 57_600; // Baud rate
parameter CLOCK_RATE = 50_000_000;
// The OVERSAMPLE_RATE is the BAUD_RATE times 16
localparam OVERSAMPLE_RATE = BAUD_RATE * 16;
// The divider is the CLOCK_RATE / OVERSAMPLE_RATE - rounded up
// (so add 1/2 of the OVERSAMPLE_RATE before the integer division)
localparam DIVIDER = (CLOCK_RATE+OVERSAMPLE_RATE/2) / OVERSAMPLE_RATE;
// The value to reload the counter is DIVIDER-1;
localparam OVERSAMPLE_VALUE = DIVIDER - 1;
// The required width of the counter is the ceiling of the base 2 logarithm
// of the DIVIDER
localparam CNT_WID = clogb2(DIVIDER);
该部分就是实现了计算OVERSAMPLE_VALUE的值,也就是上述提到的每多少次触发一次baud_x16_en_reg的值
减去1是因为计数器是从0开始的
clogb2(DIVIDER)的作用是计算DIVIDER的二进制位数,这样就可以得到一个合适的计数器宽度,可以节省资源
实现代码:
function integer clogb2;
input [31:0] value;
reg [31:0] my_value;
begin
my_value = value - 1;
for (clogb2 = 0; my_value > 0; clogb2 = clogb2 + 1)
my_value = my_value >> 1;
end
endfunction
主要逻辑就是一直将value右移,直到为0,同时每一次右移的时候clogb2+1,最终得到的clogb2就是value的二进制位数
最后是uart_rx.v与uart_rx_ctl.v的作用
uart_rx.v主要是调用与传递参数,类似于uart_led.v
重点部分是uart_rx_ctl.v,该部分主要是实现了一个串口接收控制器,主要是接收串口数据并且进行解码
这部分的代码比较长,里面有详细的注释,这里大概说明一下作用,详细可以对照代码查看
总共有五个always同步进行的逻辑部分,后续简称模块X
模块1实现的是状态机功能,在每一个时钟周期上升沿进行状态更新
模块2实现的是在判断到有数据时候,在第八个过采样读取当前bit的值,后续从起始位中间开始每16个过采样读取一次bit,也就是每次读取过采样的中间值,防止出现数据不稳定等情况
模块3实现的是记录当前读取的bit数
模块4实现的是判断bit位数是否正确产生rx_data_rdy信号
模块5是用于停止位校验,正常情况下停止位应该是1,如果不是则表示数据错误,通过frm_err传递结果
需要注意的是这些always是同步进行的,也就是在时钟沿上升的时候同步开始执行所有部分(读取的是上一个周期的数据)
实验结果
以下图片和动画展示了实验的运行结果
优化前电路图

设备视图

功率报告

项目概要

优化后电路图

时序分析报告(存在时序违规)

检查点文件

最终测试
