基于bi站正点原子讲解视频:
系统框图(基于串口的数据回环)如下:
以下,是串口接收端的波形图,系统时钟和波特率时钟不同,为异步时钟,,需要先延时两拍,将时钟同步过来,取到start_flag信号,由start_flag信号结合clk_cnt、bps_cnt两个计数器取到rx_flag信号,随后在rx_flag高电平时计算clk_cnt以及bps_cnt两个信号。最后两个信号uart_done、uart_data则在串口发送模块有所体现。
实际上,uart_done在串口发送模块中也就是uart_en信号,而uart_data也就是发送模块中的uart_din信号。
`timescale 1ns / 1ps
// Create Date: 2025/01/06 09:38:08
// Design Name:
// Module Name: uart_recv
module uart_recv(input sys_clk , //50Mhz系统时钟input sys_rst_n ,input uart_rxd , //接收到的数据output reg [7:0] uart_data , //输出的并行数据output reg uart_done //一帧信号接收完成);parameter sys_freq = 50_000_000;parameter uart_bps = 115_200;parameter bps_cnt = sys_freq/uart_bps - 1;//从0开始计算reg uart_rxd_d0;reg uart_rxd_d1;wire start_flag;reg rx_flag;reg [15:0] clk_cnt;reg [3:0] rx_cnt;reg [7:0] rx_data;//中间变量存储提取到的每个位的数据来实现串口端的串并转换//由高电平向低电平的跳变(下降沿),相当于d1延时2个时钟周期;d0延时1个时钟周期//因此判断d1是否为高且d0是否为低即可。assign start_flag = uart_rxd_d1 & (~uart_rxd_d0);//异步时钟同步化处理always@(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)beginuart_rxd_d0 <= 1'b1;uart_rxd_d1 <= 1'b1;endelse beginuart_rxd_d0 <= uart_rxd;uart_rxd_d1 <= uart_rxd_d0;endend//rx_flagalways@(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)rx_flag <= 1'b0;else if(start_flag == 1'b1)rx_flag <= 1'b1;else if((rx_cnt == 4'd9) && (clk_cnt == bps_cnt/2 - 1'b1)) //为监测到下一帧数据的起始位留半个周期的时间rx_flag <= 1'b0;else rx_flag <= 1'b1;end//clk_cntalways@(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)clk_cnt <= 16'd0;else if(rx_flag == 1'b1)
// begin //效果相同否?
// if(clk_cnt == uart_bps)
// clk_cnt <= 16'd0;
// else
// clk_cnt <= clk_cnt + 16'd1;
// endbegin if(clk_cnt < uart_bps - 16'd1) clk_cnt <= clk_cnt + 16'd1; else clk_cnt <= 16'd0; end else clk_cnt <= 16'd0; end//rx_cntalways@(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)rx_cnt <= 4'd0;else if(rx_flag == 1'b1)beginif(clk_cnt == uart_bps - 16'd1)rx_cnt <= rx_cnt + 4'd1;else rx_cnt <= rx_cnt;endelserx_cnt <= 4'd0;end//rx_dataalways@(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)rx_data <= 8'd0;else if((rx_flag == 1'b1)&&(clk_cnt == uart_bps/2)) begincase(rx_cnt)//数据的串转并_uart_rxd是异步信号//所以要需要两拍之后的信号uart_rxd_d1。4'd1: rx_data[0] <= uart_rxd_d1;4'd2: rx_data[1] <= uart_rxd_d1;4'd3: rx_data[2] <= uart_rxd_d1;4'd4: rx_data[3] <= uart_rxd_d1;4'd5: rx_data[4] <= uart_rxd_d1;4'd6: rx_data[5] <= uart_rxd_d1;4'd7: rx_data[6] <= uart_rxd_d1;4'd8: rx_data[7] <= uart_rxd_d1;endcaseendendalways@(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)beginuart_data <= 8'd0;uart_done <= 1'd0;endelse if(rx_cnt == 4'd9)begin uart_data <= rx_data; uart_done <= 1'd1; end elsebegin uart_data <= 8'd0; uart_done <= 1'd0; end end
endmodule
以下:如何在程序中捕获信号的高低电平:(可实现异步时钟的同步处理以及信号的边沿检测)
d0 <= 1'b1;
d1 <= 1'b1;
d0 <= uart_rxd;
d1 <= d0;
也就是说,在初始状态时将d0、d1拉高,通过两次打拍将d0延时1个时钟周期,d1延时2个时钟周期,从而根据d0和d1的状态,设置wire型的标志位flag来捕获需要的高低电平。
以下,是是串口发送端的波形图,整体过程和接收端基本类似,只需要进行某些变量名称的修改即可,但需要注意,接收到的数据实际上是串口接收端的发送的数据(uart_data);uart_done信号也在发送端检测边沿电平过程中以uart_en的形式完成了两次打拍延迟。
`timescale 1ns / 1ps
// Create Date: 2025/01/06 14:50:27
// Design Name:
// Module Name: uart_sendmodule uart_send(input sys_clk , //50Mhz系统时钟 input sys_rst_n , output reg uart_txd , //准备发送的数据 input uart_en , input [7:0] uart_din //从发送模块接收到的数据 );parameter sys_freq = 50_000_000;parameter uart_bps = 115_200;parameter bps_cnt = sys_freq/uart_bps - 1;//从0开始计算reg uart_en_d0;reg uart_en_d1; reg tx_flag;reg [15:0] clk_cnt;reg [3:0] tx_cnt; reg [7:0] tx_data;//中间变量存储提取到的每个位的数据来实现串口端的串并转换wire en_flag;//由高电平向低电平的跳变(下降沿),相当于d1延时2个时钟周期;d0延时1个时钟周期//因此判断d1是否为高且d0是否为低即可。assign en_flag = (~uart_en_d1) & uart_en_d0;//边沿信号检测,检测上升沿always@(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)beginuart_en_d0 <= 1'b1;uart_en_d1 <= 1'b1;endelse beginuart_en_d0 <= uart_en;uart_en_d1 <= uart_en_d0;endend//tx_dataalways@(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)tx_data <= 8'd0; else if(en_flag == 1'b1) tx_data <= uart_din;else tx_data <= tx_data;end//tx_flagalways@(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)tx_flag <= 1'b0;else if(en_flag == 1'b1)tx_flag <= 1'b1;else if((tx_cnt == 4'd9) && (clk_cnt == bps_cnt/2 - 1))tx_flag <= 1'b0;else //tx_flag <= tx_flag; //这两句话在这里作用相同否?tx_flag <= 1'b1;end//clk_cntalways@(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)clk_cnt <= 16'd0;else if(tx_flag == 1'b1)
// begin
// if(clk_cnt == uart_bps)
// clk_cnt <= 16'd0;
// else
// clk_cnt <= clk_cnt + 16'd1;
// endbegin if(clk_cnt < uart_bps - 16'd1) clk_cnt <= clk_cnt + 16'd1; else clk_cnt <= 16'd0; end else clk_cnt <= 16'd0; end//tx_cntalways@(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)tx_cnt <= 4'd0;else if(tx_flag == 1'b1)beginif(clk_cnt == uart_bps - 16'd1)tx_cnt <= tx_cnt + 4'd1;else tx_cnt <= tx_cnt;endelserx_cnt <= 4'd0;end//uart_txdalways@(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)uart_txd <= 1'b1;else if((tx_flag == 1'b1)&&(clk_cnt == 16'd0)) begincase(tx_cnt)//数据并转串,首先判断起始位,赋值低电平;//随后将tx_data一次赋值给输出端口uart_txd;//最后注意一帧数据的停止位,赋值高电平。4'd0: uart_txd <= 1'b0;4'd1: uart_txd <= tx_data[0];4'd2: uart_txd <= tx_data[1];4'd3: uart_txd <= tx_data[2];4'd4: uart_txd <= tx_data[3];4'd5: uart_txd <= tx_data[4];4'd6: uart_txd <= tx_data[5];4'd7: uart_txd <= tx_data[6];4'd8: uart_txd <= tx_data[7];4'd9: uart_txd <= 1'b1;endcaseendendendmodule
以下是顶层文件,分别将串口发送端和接收端两部分程序例化。
`timescale 1ns / 1ps
// Create Date: 2025/01/06 15:49:15
// Design Name:
// Module Name: top_uartmodule top_uart(input sys_clk , //50Mhz系统input sys_rst_n , input uart_rxd , //接收到的数据output uart_txd //准备发送的数据 );wire [7:0] uart_data;wire uart_done;//串口接收模块的例化:uart_recv uart_recv_u(.sys_clk (sys_clk ) , //50Mhz系统时钟.sys_rst_n (sys_rst_n ) ,.uart_rxd (uart_rxd ) , //接收到的数据.uart_data (uart_data ) , //输出的并行数据.uart_done (uart_done ) //一帧信号接收完成);// wire uart_en ;
// wire [7:0] uart_din;//串口发送模块的例化;uart_send uart_send_u(.sys_clk (sys_clk ) , //50Mhz系统时钟 .sys_rst_n (sys_rst_n) , .uart_txd (uart_txd ) , //准备发送的数据 .uart_en (uart_done ) , .uart_din (uart_data ) //从发送模块接收到的数据 );endmodule
以下是对应程序的RTL视图:
另注意1:
打两拍的异步时钟的同步处理,打一拍是为了将异步时钟转为同步时钟,如上本例子是将波特率时钟(115200bps)转换为系统时钟50Mhz,打两拍则是为了消除亚稳态(0 1之间的状态)。
参考连接:【Chips】跨时钟域的亚稳态处理、为什么要打两拍不是打一拍、为什么打两拍能有效?_跨时钟域为什么打两拍-CSDN博客
另注意2:
为什么在程序中最后1位的停止位需要延时半个周期而不是一个完整的周期?也就是发送模块的如下程序:
always@(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)tx_flag <= 1'b0;else if(en_flag == 1'b1)tx_flag <= 1'b1;else if((tx_cnt == 4'd9) && (clk_cnt == bps_cnt/2 - 1))//停止位只有半个波特率周期tx_flag <= 1'b0;else //tx_flag <= tx_flag; //tx_flag <= 1'b1;end
首先延时半个周期是为了给下一帧数据接收时检测起始位留够时间,在数据回环过程中不会出现太大问题,但是如果单独调用串口的发送模块(或者接收模块)时就会出现问题,在单独调用发送模块时,因为停止位只有半个周期,所以发送1帧数据过程总会先于上位机正常接收而提前结束,造成错误。
所以选择将停止位周期设置为1个时钟周期,但是实际上位机的波特率和实际计算的波特率仍有微小误差,在将停止位设置为1个时钟周期时,上位机(串口助手)的波特率实际会和自己设置的波特率(如9600,115200等)存在或多或少的误差,因此需要将停止位周期设置为1/2--1之间,和上位机什么时候检测到1帧数据的停止位有关。
另附加上视频中给出得数据环回模块的程序:
`timescale 1ns / 1ps
// Create Date: 2025/01/07 10:50:37
// Intrduction: 将串口接收到的数据进行缓存,消除上位机和串口模块之间的时间误差;
// Module Name: uart_loopmodule uart_loop(input sys_clk , //50Mhz系统时input sys_rst_n , input recv_done , //接收完成信号 input [7:0] recv_data , //接收到的数据 input tx_busy ,output reg send_en , output reg [7:0] send_data //待发送数据);reg recv_done_d0 ;reg recv_done_d1 ;reg tx_ready ;wire recv_done_flag; //捕获上升沿assign recv_done_flag = (~recv_done_d0) & recv_done_d1;//打两拍实现边沿检测always@(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)beginrecv_done_d0 <= 1'b0;recv_done_d1 <= 1'b0;endelse begin recv_done_d0 <= recv_done;recv_done_d1 <= recv_done_d0;end end//判断接收完成信号,并在串口发送模块空闲时发送是使能信号always@(posedge sys_clk or negedge sys_rst_n)beginif(!sys_rst_n)begintx_ready <= 1'b0;send_en <= 1'b0; send_data <= 8'd0;endelsebeginif(recv_done_flag) begintx_ready <= 1'b1; send_en <= 1'b0; send_data <= recv_data; end else if(tx_ready&&(~tx_busy))begintx_ready <= 1'b0;send_en <= 1'b1;endendend endmodule