一、UART 串口基本概念
-
什么是 UART?
UART(Universal Asynchronous Receiver/Transmitter)是一种“异步”串行通信协议,用来在两个设备间逐位发送数据。 -
数据帧结构
每发送一个“字节”(8 位数据),会在它前后加上固定的控制位,常见框架叫做 “8N1”:-
起始位:始终是
0
,告诉接收端“马上有数据了”。 -
数据位:实际要传输的 8 个二进制位。
-
校验位:可选,用于简单检测数据是否出错(本例中用了“奇校验”)。
-
停止位:始终是
1
,告诉接收端“这一帧结束了,恢复空闲”。
-
二、接收模块 uart_rx
(uart_rx)
1. 同步与消抖
-
三级寄存器(
rx_reg1
、rx_reg2
、rx_reg3
)把外部的异步串口信号rx
先在 FPGA 时钟域里稳定下来,避免亚稳态。 -
然后用
rx_reg2
和rx_reg3
比较,检测到“高→低”的下降沿(即起始位到来),把start_nedge
拉高一个时钟周期,开启接收流程。
2. 波特率(BAUD)产生
-
先根据系统时钟(50 MHz)和目标波特率(115200 bps)计算出一个计数上限:
-
用
baud_cnt
计数,让它从 0 数到BAUD_CNT_MAX – 1
,每数一次就相当于过了一个“串口比特时间”。 -
当计数到一半(
BAUD_CNT_MAX/2
)时,把bit_flag
拉高,表示“现在是采样时刻,数据最稳定”,这时读一次当前电平。
3. 数据移位与计数
-
bit_cnt
记录当前是第几位:-
0
:起始位 -
1~8
:8 位数据 -
9
:校验位 -
10
:停止位(本例中接收到第 9 次采样就结束,因此 stop 在计数到 9 时结束)
-
-
在
bit_flag
为高的时候:-
对于数据位(
1~8
),把采到的rx_reg3
依次移入rx_data
(移位寄存器)。 -
在第 9 位采样时,把它当作
parity_bit
(奇校验位)保存。
-
-
当
bit_cnt
数满(接收完起始、8 数据、1 校验、1 停止)后,拉高rx_flag
一个时钟脉冲,表示“这一字节收完了,可以输出了”。
4. 输出与错误检测
-
并行输出:在
rx_flag
之后一个时钟周期,把rx_data
送到po_data
,同时po_flag
拉高一个时钟,告诉外部“新数据来了”。 -
奇校验错误:收到完整字节后,用归约异或
^rx_data ^ parity_bit
检查“1 的个数是否为奇数”,若不符则parity_error
拉高。
三、发送模块 uart_tx
(uart_tx)
1. 发送使能
-
当外部把一个新字节(
pi_data
)及其有效信号pi_flag
拉高时,模块把work_en
置1
,开始发送。 -
发送完一个完整帧(1 起始 + 8 数据 + 1 校验 + 1 停止=共 11 比特的输出)后,
work_en
置0
,恢复空闲。
2. 波特率与节拍
-
同样用
baud_cnt
按目标波特率计时,每当计数到1
(即一个比特周期到来),拉高bit_flag
,让下一位数据准备好输出。 -
bit_cnt
跟踪当前发送到哪一比特:0 是起始位,1~8 是数据位,9 是校验位,10 是停止位。
3. 校验位计算
-
使用“奇校验”规则:先对 8 位数据做归约异或
^pi_data
,再取反~^pi_data
,得到要发送的parity_bit
。
4. 一位一位发送
在每个 bit_flag
到来时,根据 bit_cnt
来决定 tx
信号应该输出什么:
四、总结与核心要点
-
“异步”意思:没有单独的时钟线,靠双方事先约定同一波特率,用起始位和停止位来区分帧。
-
接收模块:
-
先把异步信号“同步”到本地时钟域;
-
检测到起始位;
-
按波特率节拍,依次采样数据、校验、停止;
-
移位、输出并做奇校验。
-
-
发送模块:
-
外部拉起发送使能;
-
按波特率节拍,一位一位输出:起始→数据→校验→停止;
-
结束后恢复空闲。
-
-
奇校验:保证“1 的总数为奇数”,简单检测“哪怕有一位翻转”也能发现错误。
最后贴上源码:
`timescale 1ns/1ps
module uart_rx
#(
parameter UART_BPS = 'd115200, //串口波特率
parameter CLK_FREQ = 'd50_000_000 //时钟频率
)
(
input wire sys_clk , //系统时钟50MHz
input wire sys_rst_n , //全局复位
input wire rx , //串口接收数据
output reg [7:0] po_data , //串转并后的8bit数据
output reg po_flag , //串转并后的数据有效标志信号
output reg parity_error // 奇校验错误标志
);
//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//
//localparam define
localparam BAUD_CNT_MAX = CLK_FREQ/UART_BPS ;
//reg define
reg rx_reg1 ;
reg rx_reg2 ;
reg rx_reg3 ;
reg start_nedge ;
reg work_en ;
reg [12:0] baud_cnt ;
reg bit_flag ;
reg [3:0] bit_cnt ;
reg [7:0] rx_data ;
reg rx_flag ;
reg parity_bit ; // 新增:保存接收到的校验位
//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//
//插入两级寄存器进行数据同步,用来消除亚稳态
//rx_reg1:第一级寄存器,寄存器空闲状态复位为1
always@(posedge sys_clk)
if(sys_rst_n == 1'b1)
rx_reg1 <= 1'b1;
else
rx_reg1 <= rx;
//rx_reg2:第二级寄存器,寄存器空闲状态复位为1
always@(posedge sys_clk)
if(sys_rst_n == 1'b1)
rx_reg2 <= 1'b1;
else
rx_reg2 <= rx_reg1;
//rx_reg3:第三级寄存器和第二级寄存器共同构成下降沿检测
always@(posedge sys_clk)
if(sys_rst_n == 1'b1)
rx_reg3 <= 1'b1;
else
rx_reg3 <= rx_reg2;
//start_nedge:检测到下降沿时start_nedge产生一个时钟的高电平
always@(posedge sys_clk)
if(sys_rst_n == 1'b1)
start_nedge <= 1'b0;
else if((~rx_reg2) && (rx_reg3))
start_nedge <= 1'b1;
else
start_nedge <= 1'b0;
//work_en:接收数据工作使能信号
always@(posedge sys_clk)
if(sys_rst_n == 1'b1)
work_en <= 1'b0;
else if(start_nedge == 1'b1)
work_en <= 1'b1;
else if((bit_cnt == 4'd9) && (bit_flag == 1'b1))
work_en <= 1'b0;
//baud_cnt:波特率计数器计数,从0计数到BAUD_CNT_MAX - 1
always@(posedge sys_clk)
if(sys_rst_n == 1'b1)
baud_cnt <= 13'b0;
else if((baud_cnt == BAUD_CNT_MAX - 1) || (work_en == 1'b0))
baud_cnt <= 13'b0;
else if(work_en == 1'b1)
baud_cnt <= baud_cnt + 1'b1;
//bit_flag:当baud_cnt计数器计数到中间数时采样的数据最稳定,
//此时拉高一个标志信号表示数据可以被取走
always@(posedge sys_clk)
if(sys_rst_n == 1'b1)
bit_flag <= 1'b0;
else if(baud_cnt == BAUD_CNT_MAX/2 - 1)
bit_flag <= 1'b1;
else
bit_flag <= 1'b0;
//bit_cnt:有效数据个数计数器,当8个有效数据(不含起始位和停止位)
//都接收完成后计数器清零
always@(posedge sys_clk)
if(sys_rst_n == 1'b1)
bit_cnt <= 4'b0;
else if((bit_cnt == 4'd9) && (bit_flag == 1'b1)) // 计数到9
bit_cnt <= 4'b0;
else if(bit_flag ==1'b1)
bit_cnt <= bit_cnt + 1'b1;
//rx_data:输入数据进行移位
always@(posedge sys_clk)
if(sys_rst_n == 1'b1)
rx_data <= 8'b0;
else if((bit_cnt >= 4'd1)&&(bit_cnt <= 4'd8)&&(bit_flag == 1'b1))
rx_data <= {rx_reg3, rx_data[7:1]};
//parity_bit:接收第9位校验位
always@(posedge sys_clk)
if(sys_rst_n == 1'b1)
parity_bit <= 1'b0;
else if((bit_cnt == 4'd9)&&(bit_flag == 1'b1))
parity_bit <= rx_reg3;
//rx_flag:输入数据移位完成时rx_flag拉高一个时钟的高电平
always@(posedge sys_clk)
if(sys_rst_n == 1'b1)
rx_flag <= 1'b0;
else if((bit_cnt == 4'd9) && (bit_flag == 1'b1)) // 9位后拉高
rx_flag <= 1'b1;
else
rx_flag <= 1'b0;
//po_data:输出完整的8位有效数据
always@(posedge sys_clk)
if(sys_rst_n == 1'b1)
po_data <= 8'b0;
else if(rx_flag == 1'b1)
po_data <= rx_data;
//po_flag:输出数据有效标志(比rx_flag延后一个时钟周期,为了和po_data同步)
always@(posedge sys_clk)
if(sys_rst_n == 1'b1)
po_flag <= 1'b0;
else
po_flag <= rx_flag;
//parity_error: 奇校验错误标志
always@(posedge sys_clk)
if(sys_rst_n == 1'b1)
parity_error <= 1'b0;
else if(rx_flag == 1'b1) begin
// 奇校验:数据位+校验位总1的个数应为奇数
if(^rx_data ^ parity_bit) // ^为奇偶校验归约异或
parity_error <= 1'b0; // 正确
else
parity_error <= 1'b1; // 错误
end
endmodule
`timescale 1ns/1ps
module uart_tx
#(
parameter UART_BPS = 'd115200, //串口波特率
parameter CLK_FREQ = 'd50_000_000 //时钟频率
)
(
input wire sys_clk , //系统时钟50MHz
input wire sys_rst_n , //全局复位
input wire [7:0] pi_data , //模块输入的8bit数据
input wire pi_flag , //并行数据有效标志信号
output reg tx //串转并后的1bit数据
);
//********************************************************************//
//****************** Parameter and Internal Signal *******************//
//********************************************************************//
//localparam define
localparam BAUD_CNT_MAX = CLK_FREQ/UART_BPS ;
//reg define
reg [12:0] baud_cnt;
reg bit_flag;
reg [3:0] bit_cnt ;
reg work_en ;
reg parity_bit; // 新增:奇校验位
//********************************************************************//
//***************************** Main Code ****************************//
//********************************************************************//
//work_en:接收数据工作使能信号
always@(posedge sys_clk)
if(sys_rst_n == 1'b1)
work_en <= 1'b0;
else if(pi_flag == 1'b1)
work_en <= 1'b1;
else if((bit_flag == 1'b1) && (bit_cnt == 4'd10)) // 计数到10
work_en <= 1'b0;
//baud_cnt:波特率计数器计数,从0计数到BAUD_CNT_MAX - 1
always@(posedge sys_clk)
if(sys_rst_n == 1'b1)
baud_cnt <= 13'b0;
else if((baud_cnt == BAUD_CNT_MAX - 1) || (work_en == 1'b0))
baud_cnt <= 13'b0;
else if(work_en == 1'b1)
baud_cnt <= baud_cnt + 1'b1;
//bit_flag:当baud_cnt计数器计数到1时让bit_flag拉高一个时钟的高电平
always@(posedge sys_clk)
if(sys_rst_n == 1'b1)
bit_flag <= 1'b0;
else if(baud_cnt == 13'd1)
bit_flag <= 1'b1;
else
bit_flag <= 1'b0;
//bit_cnt:数据位数个数计数,10个有效数据(含起始位和停止位)到来后计数器清零
always@(posedge sys_clk)
if(sys_rst_n == 1'b1)
bit_cnt <= 4'b0;
else if((bit_flag == 1'b1) && (bit_cnt == 4'd10)) // 计数到10
bit_cnt <= 4'b0;
else if((bit_flag == 1'b1) && (work_en == 1'b1))
bit_cnt <= bit_cnt + 1'b1;
//parity_bit:计算奇校验位
always@(posedge sys_clk)
if(sys_rst_n == 1'b1)
parity_bit <= 1'b0;
else if(pi_flag == 1'b1)
parity_bit <= ~^pi_data; // 奇校验:数据位异或后取反
//tx:输出数据在满足rs422协议(起始位为0,停止位为1)的情况下一位一位输出
always@(posedge sys_clk)
if(sys_rst_n == 1'b1)
tx <= 1'b1; //空闲状态时为高电平
else if(bit_flag == 1'b1)
case(bit_cnt)
0 : tx <= 1'b0; // 起始位
1 : tx <= pi_data[0];
2 : tx <= pi_data[1];
3 : tx <= pi_data[2];
4 : tx <= pi_data[3];
5 : tx <= pi_data[4];
6 : tx <= pi_data[5];
7 : tx <= pi_data[6];
8 : tx <= pi_data[7];
9 : tx <= parity_bit; // 奇校验位
10 : tx <= 1'b1; // 停止位
default : tx <= 1'b1;
endcase
endmodule