1. UART串口通信基础与项目概述
在嵌入式系统和FPGA开发中,UART(Universal Asynchronous Receiver/Transmitter)是最基础也最常用的通信接口之一。这个项目实现了完整的RS232串口收发功能,使用Verilog HDL语言编写,已在Xilinx和Altera FPGA平台上验证通过。核心功能是接收PC端串口调试助手发送的数据,并将数据原样回传显示,形成"数据回显"效果。
UART通信有几个关键特性需要理解:
- 异步通信:不需要时钟线,依靠双方约定的波特率进行同步
- 帧结构:1位起始位(低电平) + 8位数据位(LSB在前) + 1位停止位(高电平)
- 空闲状态:保持高电平(即停止位状态)
- 典型波特率:9600、115200等,本项目采用115200bps
注意:RS232电平与FPGA的TTL电平不兼容,实际硬件连接时需要MAX3232等电平转换芯片。虽然部分开发板已集成电平转换电路,但自行设计电路时务必注意这一点。
2. 系统架构设计解析
2.1 整体模块划分
系统采用经典的模块化设计,主要分为三个功能模块:
- 波特率发生器(baud_generator) - 将系统时钟分频为UART通信所需的波特率时钟
- 接收模块(uart_rx) - 实现串行数据的接收和并行化
- 发送模块(uart_tx) - 实现并行数据的串行化发送
顶层模块负责实例化这三个子模块并协调它们的工作。特别需要注意的是收发切换控制,这是避免数据冲突的关键。
2.2 时钟域处理
由于UART是异步通信,系统涉及两个时钟域:
- 系统主时钟(如50MHz)
- 波特率时钟(如115200Hz)
在FPGA设计中,跨时钟域的信号需要特殊处理。本项目采用了一个巧妙的设计:让波特率时钟和系统时钟保持同步关系(通过分频产生),从而避免了复杂的跨时钟域同步问题。
3. 核心模块实现详解
3.1 波特率发生器设计
波特率发生器的本质是一个时钟分频器。以50MHz系统时钟生成115200波特率为例:
verilog复制module baud_generator(
input clk_50m,
output reg baud_clk
);
// 分频计数器
reg [15:0] cnt;
parameter CLK_DIV = 50_000_000 / 115200 / 2;
always @(posedge clk_50m) begin
if(cnt == CLK_DIV-1) begin // 注意-1修正
baud_clk <= ~baud_clk; // 翻转生成方波
cnt <= 0;
end else begin
cnt <= cnt + 1'b1;
end
end
endmodule
关键点解析:
- 分频系数计算:50MHz/(115200*2)=217
- 乘2是因为要在波特率时钟的上升沿和下降沿都采样,提高抗干扰能力
- 实际测试发现需要-1修正才能精确匹配PC端波特率
- 输出是50%占空比的方波,确保每个数据位都能在稳定电平期间被采样
调试技巧:可以用示波器测量baud_clk输出的实际频率,应该非常接近115200Hz。如果偏差较大,会导致通信失败。
3.2 接收模块实现
接收模块是UART实现中最复杂的部分,采用有限状态机(FSM)设计:
verilog复制module uart_rx(
input rx_pin,
input baud_clk,
output reg [7:0] data_out,
output reg data_valid
);
// 状态编码
localparam IDLE = 2'b00;
localparam START = 2'b01;
localparam DATA = 2'b10;
localparam STOP = 2'b11;
reg [2:0] state;
reg [3:0] bit_cnt;
always @(posedge baud_clk) begin
case(state)
IDLE:
if(!rx_pin) begin // 检测起始位下降沿
state <= START;
bit_cnt <= 0;
end
START:
state <= DATA; // 等待到起始位中间点
DATA:
begin
data_out[bit_cnt] <= rx_pin; // 低位优先存储
if(bit_cnt == 7) begin
state <= STOP;
end
bit_cnt <= bit_cnt + 1;
end
STOP:
begin
data_valid <= 1'b1; // 有效信号持续1周期
state <= IDLE;
end
endcase
end
endmodule
状态机工作流程:
- IDLE状态:监测RX线,等待起始位下降沿
- START状态:对齐到起始位中间点(提高采样准确性)
- DATA状态:依次采样8个数据位(LSB first)
- STOP状态:产生数据有效信号,返回IDLE状态
关键设计考量:
- 在START状态停留一个周期,确保采样点位于数据位中央
- data_valid信号仅持续一个波特率周期,便于上层模块捕获
- 采用低位优先(LSB first)的存储顺序,符合UART标准
3.3 发送模块实现
发送模块同样采用状态机设计,但相对接收模块更简单:
verilog复制module uart_tx(
input [7:0] data_in,
input send_en,
output reg tx_pin,
output reg ready
);
reg [3:0] state;
reg [7:0] shift_reg;
always @(posedge baud_clk) begin
case(state)
0: if(send_en) begin
tx_pin <= 0; // 起始位
shift_reg <= data_in;
state <= 1;
ready <= 0;
end
1: begin
tx_pin <= shift_reg[0]; // 发送LSB
shift_reg <= shift_reg >> 1;
if(state == 8) begin // 发送完8位数据
state <= 9;
end else begin
state <= state + 1;
end
end
9: begin
tx_pin <= 1; // 停止位
ready <= 1;
state <= 0;
end
endcase
end
endmodule
发送过程分解:
- 状态0:等待发送使能信号(send_en)
- 状态1-8:依次发送8个数据位(通过移位寄存器实现)
- 状态9:发送停止位,恢复ready信号
注意点:发送完成后必须将TX线拉高(停止位),否则会影响下一次通信。ready信号指示模块可以接收新数据。
4. 系统集成与调试技巧
4.1 顶层模块设计
顶层模块需要协调收发模块的工作,主要解决两个问题:
- 防止发送模块占用总线时影响接收
- 实现数据回显功能(接收到的数据立即发送)
典型实现方案:
verilog复制module uart_top(
input clk_50m,
input rx_pin,
output tx_pin
);
wire baud_clk;
wire [7:0] rx_data;
wire rx_valid;
wire tx_ready;
baud_generator baud_gen(.clk_50m(clk_50m), .baud_clk(baud_clk));
uart_rx receiver(
.rx_pin(rx_pin),
.baud_clk(baud_clk),
.data_out(rx_data),
.data_valid(rx_valid)
);
uart_tx transmitter(
.data_in(rx_data),
.send_en(rx_valid),
.tx_pin(tx_pin),
.ready(tx_ready)
);
endmodule
4.2 仿真验证方法
完善的仿真环境是确保设计正确的关键。建议采用分层验证策略:
-
模块级仿真:单独验证每个子模块
- 波特率发生器:检查输出频率是否正确
- 接收模块:模拟各种数据输入场景
- 发送模块:检查输出波形是否符合UART标准
-
系统级仿真:验证整体功能
- 模拟PC端发送数据,检查回显功能
- 测试边界情况(连续发送、异常数据等)
示例测试用例:
verilog复制initial begin
// 发送0x55 (二进制01010101)
#100 rx_pin = 0; // 起始位
#8680 rx_pin = 1; // bit0
#8680 rx_pin = 0; // bit1
#8680 rx_pin = 1; // bit2
#8680 rx_pin = 0; // bit3
#8680 rx_pin = 1; // bit4
#8680 rx_pin = 0; // bit5
#8680 rx_pin = 1; // bit6
#8680 rx_pin = 0; // bit7
#8680 rx_pin = 1; // 停止位
end
4.3 硬件调试经验
在实际硬件调试中,常见问题及解决方案:
-
通信不稳定或数据错误
- 检查波特率是否准确(用示波器测量)
- 确认电平转换电路工作正常
- 检查PCB布线,避免信号干扰
-
自发自收问题(回环数据被自己锁存)
- 确保发送完成后及时释放总线
- 在顶层模块中添加适当的控制逻辑
-
抗干扰能力差
- 在RX/TX线上添加适当的上拉电阻
- 必要时添加RC滤波电路
实用技巧:在串口调试助手中使用16进制显示模式,可以更直观地观察数据内容。发送"0x55"(二进制01010101)这类交替变化的测试数据,特别适合检测位序和同步问题。
5. 性能优化与扩展
5.1 波特率自适应
基础版本使用固定波特率,实际应用中可以考虑实现波特率自动检测。常见方法:
- 测量起始位宽度,计算实际波特率
- 预置多种波特率,通过尝试匹配找到正确的
5.2 添加硬件流控
对于高速或可靠性要求高的场景,可以添加RTS/CTS硬件流控信号:
- RTS(Request To Send):指示本端准备好接收数据
- CTS(Clear To Send):指示对端允许发送数据
5.3 FIFO缓冲设计
为避免数据丢失,可以在收发模块添加FIFO缓冲区:
- 接收FIFO:暂存接收到的数据,等待主控读取
- 发送FIFO:缓存待发送数据,实现连续发送
实现示例:
verilog复制module uart_fifo #(
parameter DEPTH = 8
)(
input clk,
input rst,
input [7:0] data_in,
input wr_en,
input rd_en,
output [7:0] data_out,
output full,
output empty
);
reg [7:0] mem [0:DEPTH-1];
reg [3:0] wptr, rptr;
always @(posedge clk or posedge rst) begin
if(rst) begin
wptr <= 0;
rptr <= 0;
end else begin
if(wr_en && !full) begin
mem[wptr] <= data_in;
wptr <= wptr + 1;
end
if(rd_en && !empty) begin
rptr <= rptr + 1;
end
end
end
assign full = (wptr == rptr + DEPTH);
assign empty = (wptr == rptr);
assign data_out = mem[rptr];
endmodule
5.4 多字节协议处理
在简单回显基础上,可以定义更复杂的通信协议:
- 添加帧头、帧尾标识
- 包含长度字段和校验字段
- 支持命令解析和响应
例如定义一个简单的协议帧格式:
code复制[0xAA][0x55][LEN][DATA...][CHECKSUM]
其中:
- 0xAA 0x55:帧头标识
- LEN:数据长度
- DATA:有效载荷
- CHECKSUM:校验和(如累加和)
在FPGA设计中实现这样的协议处理,可以大大提高通信的可靠性。