1. FPGA串口通信:RS232协议与Verilog实现
在嵌入式系统和数字电路设计中,串口通信是最基础也最常用的通信方式之一。作为FPGA开发者,掌握UART(通用异步收发传输器)的实现原理至关重要。今天我要分享的是基于RS232协议的UART发送模块(TX)的Verilog实现细节,这是我在多个工业项目中验证过的稳定方案。
RS232作为经典的串行通信标准,虽然速度不高,但因其简单可靠,至今仍广泛应用于设备调试、传感器数据采集等场景。FPGA通过Verilog实现的UART核心优势在于:1)完全可定制化波特率和数据格式;2)无需额外芯片即可实现通信功能;3)时序控制精确到纳秒级。下面我将从波形设计到代码实现,详细拆解这个发送模块的每个技术细节。
2. UART发送模块设计解析
2.1 核心功能与信号定义
UART发送模块的核心任务是将8位并行数据转换为符合RS232标准的串行数据流。根据提供的波形图和分析,我们需要重点关注以下关键信号:
- Pi_data[7:0]:输入的8位并行数据,来自接收模块或其他数据源
- Pi_flag:数据有效标志,上升沿表示Pi_data已经稳定可用
- tx:最终输出的串行数据线,按照起始位(0)+数据位(LSB first)+停止位(1)的格式发送
模块工作时序可分为三个阶段:
- 空闲状态:tx线保持高电平
- 起始位检测:当Pi_flag有效时,下一个波特周期开始发送起始位(0)
- 数据发送:依次发送8位数据,最后跟停止位(1)
2.2 波特率生成原理
RS232是异步通信协议,收发双方必须严格遵循相同的波特率。在50MHz系统时钟下实现9600波特率的计算公式为:
code复制CNT_BAUD_MAX = SYS_CLK_FREQ / UART_BAUD
= 50,000,000 / 9,600 ≈ 5208
这意味着每个比特周期需要计数5208个系统时钟。代码中使用13位计数器(cnt_Baud)实现这个功能,实际项目中有三个关键注意点:
- 计数器位宽应预留余量,建议使用16位寄存器以适应更高波特率
- 实际应用中建议加入波特率容错机制,通常允许±2%的偏差
- 对于高精度需求,可以使用小数分频技术(如DDS)
注意:在FPGA实现时,建议将CNT_BAUD_MAX定义为参数(parameter),这样只需修改一处即可调整整个设计的波特率,提高代码复用性。
3. Verilog代码深度解析
3.1 状态控制逻辑
发送模块的状态由work_en信号控制,其状态转换逻辑如下:
verilog复制always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
work_en <= 1'b0;
else if (Pi_flag == 1'b1) // 检测到数据有效标志
work_en <= 1'b1;
else if( (assign_en == 1'b1) && (cnt_assign_en == 'd9) ) // 发送完停止位
work_en <= 1'b0;
else
work_en <= work_en;
end
这个状态机有三个关键设计考量:
- 采用同步复位设计,确保在时钟边沿稳定切换状态
- Pi_flag作为触发信号,必须至少保持一个时钟周期有效
- 发送完成后自动回到空闲状态,避免重复发送
3.2 数据移位输出机制
数据发送的核心在于cnt_assign_en计数器,它控制着10个发送阶段(1起始+8数据+1停止):
verilog复制always @(posedge sys_clk or negedge rst_n) begin
if(!rst_n)
tx <= 'b1; // 空闲状态为高电平
else if(assign_en == 1'b1 )
case (cnt_assign_en)
0: tx <= 1'b0; // 起始位
1: tx <= Pi_data[0]; // LSB first
2: tx <= Pi_data[1];
// ...中间6位省略...
8: tx <= Pi_data[7]; // MSB
9: tx <= 1'b1; // 停止位
default:tx <= 1'b1;
endcase
else
tx <= tx;
end
这里有几个值得注意的实现细节:
- RS232协议规定数据线空闲时为高电平,起始位为低电平
- 采用LSB(最低有效位)优先的发送顺序,这是UART的标准约定
- case语句中的default分支是良好的防御性编程习惯
3.3 测试平台设计要点
提供的测试平台(tb)模拟了数据0-7的发送过程,每个数据间隔52082010个时间单位。在验证UART模块时,建议补充以下测试场景:
- 连续发送测试:验证FIFO缓冲能力
- 异常复位测试:在发送过程中突然复位
- 波特率容错测试:±5%的时钟偏差
- 边界值测试:0x00和0xFF等特殊数据
一个更完善的测试激励可以这样编写:
verilog复制initial begin
// 基础测试数据
test_single_byte(8'h55); // 01010101
test_single_byte(8'hAA); // 10101010
test_single_byte(8'h00);
test_single_byte(8'hFF);
// 连续发送测试
repeat(10) begin
test_single_byte($random);
#(5208*20); // 间隔1个比特周期
end
end
task test_single_byte;
input [7:0] data;
begin
Pi_data <= data;
Pi_flag <= 1'b1;
#20 Pi_flag <= 1'b0;
#(5208*20*10); // 等待1帧发送完成
end
endtask
4. 工程实践中的经验总结
4.1 常见问题与解决方案
在实际项目中,UART发送模块容易遇到以下典型问题:
-
数据错位:
- 现象:接收端数据位序错乱
- 原因:LSB/MSB顺序与接收端不一致
- 解决:统一收发双方的位序约定,通常采用LSB first
-
停止位错误:
- 现象:接收端报告帧错误
- 原因:停止位持续时间不足或电平错误
- 解决:确保停止位为高电平且保持完整波特周期
-
波特率偏差:
- 现象:高速通信时数据丢失
- 原因:系统时钟精度不足或分频计算错误
- 解决:使用PLL生成精确时钟,重新校验分频系数
4.2 性能优化技巧
-
流水线设计:
当需要更高发送速率时,可以采用两级流水线:- 第一级:准备下一个要发送的数据
- 第二级:当前正在发送的数据
这样可以减少数据准备时间,提高最大吞吐量
-
参数化设计:
将关键参数如波特率、数据位宽、停止位数等设计为模块参数:verilog复制module UART_TX #( parameter DATA_WIDTH = 8, parameter STOP_BITS = 1, parameter PARITY = "NONE" // "NONE","ODD","EVEN" )( // 端口定义 ); -
时钟域交叉处理:
当发送数据来自不同时钟域时,必须添加双缓冲同步器:verilog复制reg [7:0] data_buf0, data_buf1; always @(posedge clk_src) data_buf0 <= data_in; always @(posedge sys_clk) data_buf1 <= data_buf0;
4.3 扩展应用方向
基础UART发送模块可以扩展以下功能:
-
硬件流控:
添加RTS/CTS信号线实现流量控制,防止数据丢失 -
软件FIFO:
增加16/32/64字节的发送缓冲,提高数据吞吐效率 -
多协议支持:
通过参数配置支持不同数据格式(如7位数据+奇偶校验) -
自适应波特率:
通过测量起始位宽度自动检测波特率
5. 模块集成与系统级考量
在实际FPGA工程中,UART发送模块通常需要与其他模块协同工作。以下是几个关键集成要点:
-
与接收模块的配合:
- 共享相同的波特率参数
- 统一使用同一时钟源
- 考虑添加回环测试模式
-
与上层逻辑的接口:
- 建议采用AXI-Stream接口标准
- 添加"发送忙"状态信号
- 设计超时重传机制
-
低功耗设计:
- 空闲时关闭波特率时钟
- 使用时钟门控技术
- 添加自动睡眠/唤醒功能
一个典型的系���级连接方式如下:
verilog复制UART_TX #(
.UART_BAUD(115200),
.SYS_CLK_FREQ(50_000_000)
) u_uart_tx (
.sys_clk(sys_clk),
.rst_n(rst_n),
.Pi_data(tx_fifo_data),
.Pi_flag(!tx_fifo_empty),
.tx(uart_tx_pin),
.busy(tx_busy)
);
FIFO #(
.DEPTH(64),
.WIDTH(8)
) u_tx_fifo (
.wr_clk(user_clk),
.wr_data(user_data),
.wr_en(user_valid),
.rd_clk(sys_clk),
.rd_data(tx_fifo_data),
.rd_en(!tx_busy && !tx_fifo_empty)
);
在实现过程中,我发现几个值得分享的经验:第一,在高速通信时(>1Mbps),建议使用ODDR原语直接驱动TX引脚,避免时序问题;第二,对于长距离传输,可以在输出端添加RS232电平转换芯片(如MAX3232)增强信号驱动能力;第三,在多个UART实例的场景下,考虑使用共享的波特率生成模块以减少资源占用。
