第三章 UART接口IP设计
UART,说白了就是串口通信。这玩意儿在嵌入式系统里太常见了。我刚开始做芯片设计那会儿,第一个独立完成的IP就是UART。记得当时调试了好久,最后发现是波特率没算对——嗯,这个坑我后面会细说。
3.1 UART协议详解
UART全称是Universal Asynchronous Receiver/Transmitter。异步,意味着没有时钟线。发送方和接收方各用自己的时钟,靠约定好的波特率来同步。
一个标准的UART帧长这样:
起始位(1bit) + 数据位(5~8bit) + 校验位(0/1bit) + 停止位(1/1.5/2bit)
我习惯用8位数据、无校验、1位停止位,也就是常说的8N1。为什么?因为大部分MCU和传感器都支持这个配置,兼容性最好。
关键时序点:
- 空闲时,TX线保持高电平
- 起始位:拉低1个波特率周期
- 数据位:LSB先发,MSB最后
- 停止位:拉高至少1个周期
你想想看,如果接收方采样到了起始位的下降沿,它就开始数时钟周期。每过一个波特率周期,采样一次数据位。这就是异步通信的精髓——靠边沿触发,靠周期采样。
3.2 波特率发生器设计
波特率发生器,本质上就是一个分频器。系统时钟除以你想要的波特率,得到分频系数。
公式很简单:
baud_div = sys_clk_freq / (target_baud_rate * oversample_factor)
这里有个坑——过采样。我个人习惯用16倍过采样。为什么是16?因为这样可以在一个bit周期内采16个点,取中间3个点做多数判决,抗干扰能力强。
实战经验:
我曾经在一个项目中,系统时钟是50MHz,目标波特率115200。算出来分频系数是27.12。取整27,实际波特率变成了115740,误差0.47%。这在UART协议里是允许的(一般要求误差<2%)。但如果你的系统时钟是33MHz,算115200就尴尬了——分频系数17.9,取整18,误差4.3%,通信就会丢包。
Verilog代码实现:
module baud_gen (
input clk,
input rst_n,
input [15:0] baud_div,
output reg baud_pulse
);
reg [15:0] cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
cnt <= 16'd0;
baud_pulse <= 1'b0;
end else begin
if (cnt == baud_div - 1) begin
cnt <= 16'd0;
baud_pulse <= 1'b1;
end else begin
cnt <= cnt + 1'b1;
baud_pulse <= 1'b0;
end
end
end
endmodule
注意,这里输出的是脉冲信号,不是方波。每个波特率周期只拉高一个时钟周期,用来驱动FSM的状态跳转。
3.3 发送器FSM设计
发送器的状态机,我一般设计成4个状态:
IDLE -> START -> DATA -> STOP
每个状态做什么?
- IDLE:TX线拉高,等待发送使能信号
- START:TX拉低,持续1个波特率周期
- DATA:逐位发送数据,LSB first,持续8个周期
- STOP:TX拉高,持续1个周期,然后回到IDLE
注意:
我曾经犯过一个低级错误——在DATA状态里忘了加bit计数器。结果数据位发完了还在发,接收方那边全乱了。后来我加了一个3位的bit_cnt,每发完一位加1,到8就跳转到STOP。
发送器FSM的Verilog核心代码:
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= IDLE;
tx_reg <= 1'b1;
bit_cnt <= 3'd0;
end else begin
case (state)
IDLE: begin
tx_reg <= 1'b1;
if (tx_start) begin
state <= START;
data_reg <= tx_data; // 锁存待发送数据
end
end
START: begin
tx_reg <= 1'b0;
if (baud_pulse) state <= DATA;
end
DATA: begin
tx_reg <= data_reg[bit_cnt]; // LSB first
if (baud_pulse) begin
if (bit_cnt == 3'd7) begin
bit_cnt <= 3'd0;
state <= STOP;
end else begin
bit_cnt <= bit_cnt + 1'b1;
end
end
end
STOP: begin
tx_reg <= 1'b1;
if (baud_pulse) state <= IDLE;
end
endcase
end
end
3.4 接收器FSM设计
接收器比发送器麻烦一点。因为你要在噪声中准确找到起始位,还要在正确的时刻采样数据。
我设计的接收器状态机:
IDLE -> START_DETECT -> SAMPLE -> DATA -> STOP_CHECK
这里有个技巧——起始位检测。在IDLE状态下,如果检测到TX线从高变低,不要立刻认为就是起始位。我习惯先等半个波特率周期,再采一次。如果还是低,才确认是真正的起始位。这样可以滤掉毛刺。
避坑指南:
我曾经在一个电磁干扰很强的项目里,UART接收老是误触发。后来加了起始位确认机制——检测到下降沿后,等8个过采样时钟(也就是半个bit周期)再采一次。如果还是低,才进入SAMPLE状态。从此再也没误触发过。
接收器的采样点选择也很关键。16倍过采样下,我一般取第7、8、9个采样点做多数判决。为什么是这三个?因为它们在bit周期的正中间,抖动最小。
// 多数判决逻辑
always @(*) begin
case ({sample_7, sample_8, sample_9})
3'b000: rx_bit = 1'b0;
3'b111: rx_bit = 1'b1;
default: rx_bit = sample_8; // 中间值优先
endcase
end
接收器还有一个关键点——停止位检查。如果停止位不是高电平,说明这帧数据有问题,应该置位帧错误标志。我习惯在STOP_CHECK状态里做这个检查。
3.5 验证环境搭建
验证UART IP,我一般搭三个层次的测试:
- 单元测试:单独测波特率发生器、发送FSM、接收FSM
- 环路测试:把发送器和接收器连起来,自发自收
- 系统测试:用BFM模拟外部设备,测各种异常情况
环路测试的testbench结构:
// 顶层测试模块
module uart_tb;
reg clk, rst_n;
reg [7:0] tx_data;
reg tx_start;
wire tx_line;
wire [7:0] rx_data;
wire rx_done;
// 实例化DUT
uart_top u_dut (
.clk(clk),
.rst_n(rst_n),
.tx_data(tx_data),
.tx_start(tx_start),
.tx(tx_line),
.rx(tx_line), // 环路连接
.rx_data(rx_data),
.rx_done(rx_done)
);
// 测试序列
initial begin
// 初始化
rst_n = 0;
#100 rst_n = 1;
// 发送0x55 (01010101)
tx_data = 8'h55;
tx_start = 1;
#20 tx_start = 0;
// 等待接收完成
@(posedge rx_done);
// 检查结果
if (rx_data == 8'h55)
$display("Test PASS: 0x55 loopback OK");
else
$display("Test FAIL: expected 0x55, got 0x%h", rx_data);
// 更多测试...
#1000 $finish;
end
endmodule
验证要点:
- 边界值测试:全0、全1、交替模式(0x55、0xAA)
- 异常测试:波特率偏差、毛刺干扰、帧错误
- 压力测试:连续发送10000帧,检查有无丢帧
我个人习惯在验证环境里加一个错误注入模块。它可以随机在TX线上插入毛刺、拉长或缩短bit周期。这样能充分验证接收器的鲁棒性。你想想看,如果连这种恶劣环境都能通过,那实际项目里基本不会出问题。
嗯,UART接口IP设计就讲到这里。下一章我们会讲SPI接口,那个比UART稍微复杂一点,但原理是相通的。