10、UART串口通信:UART协议解析、波特率发生器设计、发送模块实现、接收模块实现、回环测试
UART串口,说白了就是嵌入式世界里最常用的“对讲机”。
我刚开始做FPGA那会儿,第一个调通的模块就是UART。为什么?因为它简单、直观,而且调试起来特别有成就感——你写个“A”,电脑上就能收到“A”,这种即时反馈对新手来说太重要了。
这一章,我们就来把UART串口通信彻底吃透。从协议解析到模块实现,再到回环测试,一步到位。
10.1 UART协议解析:一帧数据的“长相”
UART全称是Universal Asynchronous Receiver/Transmitter,通用异步收发器。注意“异步”这两个字——收发双方没有独立的时钟线,全靠约定好的波特率来同步。
一帧UART数据长什么样?我画个草图给你看:
空闲位(高电平) | 起始位(低) | 数据位(LSB先发) | 校验位(可选) | 停止位(高)
具体来说:
- 空闲位:总线空闲时,TX线保持高电平
- 起始位:一个低电平脉冲,告诉接收方“我要开始发了”
- 数据位:5~8位,通常用8位。注意是LSB(最低有效位)先发
- 校验位:奇校验或偶校验,可选。我一般不用,除非项目要求
- 停止位:1位、1.5位或2位的高电平,表示帧结束
关键点:UART是“异步”的,所以收发双方的波特率必须一致。差一点没关系,但差太多就会丢数据。我曾经遇到过波特率偏差2%就导致通信不稳定的情况,嗯,那是个血泪教训。
10.2 波特率发生器设计:给UART一个“心跳”
波特率发生器,说白了就是一个分频器。系统时钟50MHz,我们要产生9600bps的波特率,怎么算?
公式很简单:
波特率分频值 = 系统时钟频率 / (目标波特率 × 采样倍数)
采样倍数通常取16。为什么是16?因为接收模块需要在每个数据位中间采样,16倍采样可以保证采样点落在位中间附近,容错性更好。
举个例子:50MHz时钟,目标波特率9600,采样倍数16:
分频值 = 50_000_000 / (9600 × 16) ≈ 325.52
取整:325
实际波特率 = 50_000_000 / (325 × 16) ≈ 9615bps,误差约0.16%,完全OK。
代码实现如下:
module baud_gen (
input wire clk,
input wire rst_n,
output reg baud_clk_en // 波特率时钟使能信号
);
parameter CLK_FREQ = 50_000_000;
parameter BAUD_RATE = 9600;
localparam DIV_CNT = CLK_FREQ / (BAUD_RATE * 16) - 1;
reg [15:0] cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 16'd0;
baud_clk_en <= 1'b0;
end else if (cnt == DIV_CNT) begin
cnt <= 16'd0;
baud_clk_en <= 1'b1;
end else begin
cnt <= cnt + 1'b1;
baud_clk_en <= 1'b0;
end
end
endmodule
个人习惯:我通常把波特率参数化,这样在顶层模块里可以随时切换。比如调试时用115200,量产时用9600,改一个参数就行,不用动代码。
10.3 发送模块实现:把并行数据“串”出去
发送模块的任务很简单:把CPU给的8位并行数据,按照UART协议一比特一比特地发出去。
状态机设计:
- IDLE:空闲状态,TX线拉高。检测到发送请求,跳转到START
- START:发送起始位(低电平),持续1个位时间
- DATA:发送8个数据位,从LSB开始
- STOP:发送停止位(高电平),持续1个位时间
核心代码片段:
module uart_tx (
input wire clk,
input wire rst_n,
input wire baud_clk_en,
input wire [7:0] data_in,
input wire send_en,
output reg tx,
output reg tx_busy
);
localparam IDLE = 2'd0;
localparam START = 2'd1;
localparam DATA = 2'd2;
localparam STOP = 2'd3;
reg [1:0] state;
reg [3:0] bit_cnt;
reg [7:0] data_reg;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
tx <= 1'b1;
tx_busy <= 1'b0;
bit_cnt <= 4'd0;
end else if (baud_clk_en) begin
case (state)
IDLE: begin
tx <= 1'b1;
tx_busy <= 1'b0;
if (send_en) begin
data_reg <= data_in;
state <= START;
tx_busy <= 1'b1;
end
end
START: begin
tx <= 1'b0;
state <= DATA;
bit_cnt <= 4'd0;
end
DATA: begin
tx <= data_reg[bit_cnt];
if (bit_cnt == 4'd7) begin
state <= STOP;
end else begin
bit_cnt <= bit_cnt + 1'b1;
end
end
STOP: begin
tx <= 1'b1;
state <= IDLE;
tx_busy <= 1'b0;
end
endcase
end
end
endmodule
注意:发送使能信号send_en必须保持至少一个波特率时钟周期,否则可能漏触发。我习惯在外部做一个边沿检测,把脉冲展宽一下。
10.4 接收模块实现:从“串”里恢复并行数据
接收比发送稍微复杂一点。为什么?因为接收方不知道数据什么时候来,需要实时检测起始位。
接收的核心思路:
- 用16倍波特率时钟采样RX线
- 检测到下降沿(起始位开始)
- 在起始位中间(第8个采样点)确认确实是低电平
- 之后每隔16个采样点采集一个数据位
状态机设计:
- IDLE:等待起始位。检测到RX下降沿,跳转到START
- START:在起始位中间采样,确认是低电平。如果检测到高电平,认为是毛刺,回到IDLE
- DATA:采集8个数据位,每个位在中间点采样
- STOP:采集停止位,确认是高电平。完成后输出数据
核心代码片段:
module uart_rx (
input wire clk,
input wire rst_n,
input wire baud_clk_en,
input wire rx,
output reg [7:0] data_out,
output reg data_valid
);
localparam IDLE = 2'd0;
localparam START = 2'd1;
localparam DATA = 2'd2;
localparam STOP = 2'd3;
reg [1:0] state;
reg [3:0] sample_cnt;
reg [3:0] bit_cnt;
reg [7:0] data_reg;
reg rx_sync, rx_prev;
// 同步和边沿检测
always @(posedge clk) begin
rx_sync <= rx;
rx_prev <= rx_sync;
end
wire rx_fall = (rx_prev && !rx_sync);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
data_out <= 8'd0;
data_valid <= 1'b0;
sample_cnt <= 4'd0;
bit_cnt <= 4'd0;
end else if (baud_clk_en) begin
case (state)
IDLE: begin
data_valid <= 1'b0;
if (rx_fall) begin
state <= START;
sample_cnt <= 4'd0;
end
end
START: begin
if (sample_cnt == 4'd7) begin
if (!rx_sync) begin
state <= DATA;
sample_cnt <= 4'd0;
bit_cnt <= 4'd0;
end else begin
state <= IDLE; // 毛刺,放弃
end
end else begin
sample_cnt <= sample_cnt + 1'b1;
end
end
DATA: begin
if (sample_cnt == 4'd15) begin
data_reg[bit_cnt] <= rx_sync;
sample_cnt <= 4'd0;
if (bit_cnt == 4'd7) begin
state <= STOP;
end else begin
bit_cnt <= bit_cnt + 1'b1;
end
end else begin
sample_cnt <= sample_cnt + 1'b1;
end
end
STOP: begin
if (sample_cnt == 4'd15) begin
if (rx_sync) begin
data_out <= data_reg;
data_valid <= 1'b1;
end
state <= IDLE;
end else begin
sample_cnt <= sample_cnt + 1'b1;
end
end
endcase
end
end
endmodule
避坑指南:我曾经在接收模块里忘记做同步处理,直接把外部RX信号接到状态机里。结果因为亚稳态问题,偶尔会收到错误数据。后来老老实实加了两级同步寄存器,问题就解决了。记住:跨时钟域的信号一定要同步!
10.5 回环测试:验证收发是否正常
回环测试,就是把TX和RX短接,发送一个数据,看能不能正确收回来。这是验证UART模块最直接的方法。
测试思路:
- 顶层例化一个发送模块和一个接收模块
- 把发送模块的TX输出接到接收模块的RX输入
- 发送一个字节,等待接收完成
- 比较发送数据和接收数据是否一致
顶层模块示意:
module uart_loopback (
input wire clk,
input wire rst_n,
input wire send_en,
input wire [7:0] data_in,
output wire [7:0] data_out,
output wire data_valid,
output wire tx_busy
);
wire baud_clk_en;
wire tx_wire;
baud_gen #(
.CLK_FREQ(50_000_000),
.BAUD_RATE(9600)
) u_baud (
.clk(clk),
.rst_n(rst_n),
.baud_clk_en(baud_clk_en)
);
uart_tx u_tx (
.clk(clk),
.rst_n(rst_n),
.baud_clk_en(baud_clk_en),
.data_in(data_in),
.send_en(send_en),
.tx(tx_wire),
.tx_busy(tx_busy)
);
uart_rx u_rx (
.clk(clk),
.rst_n(rst_n),
.baud_clk_en(baud_clk_en),
.rx(tx_wire),
.data_out(data_out),
.data_valid(data_valid)
);
endmodule
仿真时,你只需要给一个send_en脉冲,然后等data_valid拉高,检查data_out是否等于data_in。如果相等,恭喜你,UART通信调通了!
调试技巧:如果回环测试不通过,先检查波特率发生器。用示波器或者逻辑分析仪看TX脚的波形,确认每个位的宽度是否正确。我遇到过好几次都是分频值算错了,导致波特率偏差太大。
嗯,UART串口通信这一块,说难不难,说简单也不简单。关键是把协议吃透,把状态机写稳。你想想看,只要掌握了UART,后面学SPI、I2C这些串行协议,思路都是相通的。
下一章,我们会把UART和之前学的按键、LED结合起来,做一个完整的串口控制实验。到时候你就知道,FPGA和PC通信有多爽了。