1. FPGA 串口发送实验详解
在 FPGA 开发中,UART(通用异步收发传输器)是最基础也最常用的通信接口之一。这个实验展示了如何在 FPGA 的可编程逻辑(PL)部分实现一个灵活的 UART 发送模块,通过按键控制交替发送 6 字节和 8 字节数据。
1.1 硬件平台与需求
本实验基于 Tang FPGA 开发板实现,核心需求如下:
- 使用 27MHz 系统时钟
- 实现 115200bps 的 UART 发送
- 通过按键触发发送
- 交替发送 6 字节和 8 字节数据包
硬件连接非常简单:
- CLOCK_XTAL_27MHz:27MHz 晶振输入
- RESET:复位信号
- KEY1:触发按键
- TXD:UART 发送线
1.2 系统架构设计
整个系统采用分层设计,分为三个主要模块:
- 顶层模块(TANG_FPGA_Demo_Top):处理按键检测、数据缓存和发送控制
- UART发送控制器(uart_sender):管理数据包发送流程
- UART TX IP核(uart_tx):实际生成UART信号的底层驱动
这种分层设计使得每个模块职责清晰,便于维护和复用。特别是将UART TX功能独立为IP核,可以在不同项目中直接调用。
2. 核心模块实现解析
2.1 顶层模块设计
顶层模块主要完成三项工作:
verilog复制module TANG_FPGA_Demo_Top #(
parameter P_CLK_FREQ = 27000000,
parameter P_UART_BPS = 115200,
parameter P_MAX_LEN = 8
)(
input wire CLOCK_XTAL_27MHz,
input wire RESET,
input wire KEY1,
output wire TXD
);
2.1.1 按键检测电路
按键检测采用经典的边沿检测电路,避免按键抖动带来的多次触发:
verilog复制// 按键下降沿检测
reg r_key_dly;
wire w_key_fall;
always @(posedge CLOCK_XTAL_27MHz or negedge RESET) begin
if (!RESET)
r_key_dly <= 1'b1;
else
r_key_dly <= KEY1;
end
assign w_key_fall = (r_key_dly == 1'b1) && (KEY1 == 1'b0);
实际项目中,建议增加防抖处理(如计时器或滤波器),确保每次按键只触发一次操作。
2.1.2 数据缓存设计
数据缓存采用参数化设计,支持最大8字节存储:
verilog复制reg [P_MAX_LEN*8-1:0] r_data_buf;
initial begin
r_data_buf = {
8'h88,
8'h77,
8'h66,
8'h55,
8'h44,
8'h33,
8'h22,
8'h11
};
end
这种初始化方式便于调试,实际应用中可以通过其他接口(如SPI、I2C)动态更新数据。
2.1.3 发送长度控制
通过一个简单的触发器实现6/8字节交替发送:
verilog复制reg r_toggle;
reg r_start;
always @(posedge CLOCK_XTAL_27MHz or negedge RESET) begin
if (!RESET) begin
r_toggle <= 1'b0;
r_start <= 1'b0;
end else begin
r_start <= 1'b0; // 默认拉低(单周期脉冲)
if (w_key_fall && !w_busy) begin
r_toggle <= ~r_toggle;
r_start <= 1'b1;
end
end
end
wire [7:0] w_len = (r_toggle == 1'b0) ? 8'd6 : 8'd8;
2.2 UART发送控制器
uart_sender模块是系统的核心调度器,主要特点:
- AXI-Stream接口:采用标准流接口设计,便于集成
- 状态机控制:清晰管理发送流程
- 忙闲指示:提供busy/done信号,方便上层控制
2.2.1 状态机设计
状态机仅需两个状态,简洁高效:
verilog复制localparam S_IDLE = 2'b00;
localparam S_SEND = 2'b01;
reg [1:0] r_state;
状态转移逻辑:
- IDLE:等待启动信号
- SEND:逐个字节发送数据
2.2.2 数据发送流程
发送过程采用握手协议(tvalid/tready):
verilog复制if (r_tvalid && w_tready) begin
if (r_tlast) begin
r_state <= S_IDLE;
r_tvalid <= 1'b0;
r_tlast <= 1'b0;
o_done <= 1'b1;
o_busy <= 1'b0;
end else begin
r_cnt <= r_cnt + 1'b1;
r_tvalid <= 1'b0;
end
end
这种设计确保每个字节都能可靠发送,避免数据丢失。
2.3 UART TX IP核
uart_tx模块是实际的物理层驱动,关键特性:
- 精确的波特率控制:支持任意时钟频率
- 可配置参数:数据位、停止位、校验位灵活配置
- FIFO缓冲:可选FIFO深度,应对突发数据
2.3.1 波特率计算
精确计算每个bit的时钟周期数:
verilog复制localparam L_BAUD_CYCLES = ((P_CLK_FREQ*10*2 + P_UART_BPS) / (P_UART_BPS*2)) / 10;
localparam L_BAUD_CYCLES_FRAC = ((P_CLK_FREQ*10*2 + P_UART_BPS) / (P_UART_BPS*2)) % 10;
这种计算方式考虑了时钟频率可能不是波特率的整数倍的情况,通过分数部分补偿,确保波特率精度。
2.3.2 数据帧生成
支持多种帧格式配置:
verilog复制localparam [31:0] L_TOTAL_BITS =
(P_STOP_BITS >= ('hFFFFFFFF-9-L_PARITY_BITS)) ?
'hFFFFFFFF : (L_PARITY_BITS + P_STOP_BITS + 9);
可以根据需要配置:
- 5-9位数据位
- 奇偶校验(奇/偶/无)
- 1-2位停止位
3. 仿真验证与调试
3.1 测试平台搭建
使用SystemVerilog搭建测试平台,主要功能:
- 时钟和复位生成
- 测试数据准备
- 自动发送控制
- 状态监控
systemverilog复制task automatic send_byte(input int len);
begin
// 等待空闲
while (o_busy) @(posedge i_clk);
i_len <= len;
i_start <= 0;
@(posedge i_clk);
i_start <= 1;
@(posedge i_clk);
i_start <= 0;
wait(busy_rise);
wait(done_pulse);
end
endtask
3.2 测试用例设计
测试流程模拟实际使用场景:
systemverilog复制initial begin
int toggle = 0;
@(posedge i_rst_n);
repeat (10) begin
send_byte((toggle == 0) ? 6 : 8);
toggle = ~toggle;
end
#1000;
$stop;
end
这种测试方式验证了:
- 正常发送流程
- 忙闲状态转换
- 数据包交替发送
- 系统稳定性
3.3 实际测试结果
通过串口调试助手观察输出,符合预期:
code复制11 22 33 44 55 66 77 88
11 22 33 44 55 66
11 22 33 44 55 66 77 88
11 22 33 44 55 66
11 22 33 44 55 66 77 88
4. 工程实现细节
4.1 约束文件配置
硬件引脚约束确保信号正确映射:
tcl复制IO_LOC "CLOCK_XTAL_27MHz" 47;
IO_PORT "CLOCK_XTAL_27MHz" IO_TYPE=LVCMOS33 PULL_MODE=UP;
IO_LOC "RESET" 13;
IO_PORT "RESET" IO_TYPE=LVCMOS33 PULL_MODE=UP;
IO_LOC "KEY1" 44;
IO_PORT "KEY1" IO_TYPE=LVCMOS33 PULL_MODE=UP;
IO_LOC "TXD" 34;
IO_PORT "TXD" IO_TYPE=LVCMOS33 PULL_MODE=UP DRIVE=8;
时钟约束保证时序分析准确:
tcl复制create_clock -name CLOCK_XTAL_27MHz -period 37.037 -waveform {0 18.518} [get_ports {CLOCK_XTAL_27MHz}]
4.2 文件组织
清晰的目录结构便于管理:
code复制src/
├── uart/
│ ├── uart_tx.v // UART TX IP核
│ ├── uart_sender.v // 发送控制器
│ └── tb.sv // 测试平台
constraints/
├── vio_uart_prj.cst // 引脚约束
└── vio_uart_prj.sdc // 时序约束
4.3 参数化设计
关键参数全部参数化,提高复用性:
verilog复制module uart_sender #(
parameter P_CLK_FREQ = 27000000,
parameter P_UART_BPS = 115200,
parameter P_MAX_LEN = 8
)(
// 端口定义
);
这种设计使得模块可以轻松适配不同项目需求,只需修改参数值即可支持不同的时钟频率、波特率和数据长度。
5. 常见问题与调试技巧
5.1 波特率不准确
现象:接收端数据错误或无法识别
排查步骤:
- 检查系统时钟频率设置是否正确
- 确认波特率计算参数
- 使用逻辑分析仪测量实际波特率
解决方案:
verilog复制// 确保波特率计算准确
localparam L_BAUD_CYCLES = ((P_CLK_FREQ*10*2 + P_UART_BPS) / (P_UART_BPS*2)) / 10;
5.2 数据包不完整
现象:接收到的数据字节数不对
排查步骤:
- 检查busy/done信号是否正常
- 验证数据长度参数传递
- 仿真观察状态机转换
解决方案:
verilog复制// 确保长度参数正确传递
wire [7:0] w_len = (r_toggle == 1'b0) ? 8'd6 : 8'd8;
5.3 按键多次触发
现象:按一次键发送多次数据
排查步骤:
- 检查按键硬件消抖
- 验证边沿检测电路
- 添加软件防重复触发机制
解决方案:
verilog复制// 添加busy状态判断,避免重复触发
if (w_key_fall && !w_busy) begin
r_toggle <= ~r_toggle;
r_start <= 1'b1;
end
5.4 时序违规
现象:综合后出现时序警告
排查步骤:
- 检查时钟约束是否正确定义
- 分析关键路径
- 优化状态机设计
解决方案:
tcl复制# 确保时钟约束正确定义
create_clock -name CLOCK_XTAL_27MHz -period 37.037 -waveform {0 18.518} [get_ports {CLOCK_XTAL_27MHz}]
6. 性能优化与扩展
6.1 FIFO深度优化
对于高速或大数据量场景,可以增加FIFO深度:
verilog复制parameter P_FIFO_EA = 2, // 默认深度4
根据实际需求调整,平衡资源和性能。
6.2 多字节并行处理
当前设计逐字节发送,可以扩展为支持多字节并行:
verilog复制parameter P_BYTE_WIDTH = 2, // 支持2字节并行
需要相应修改数据路径和状态机。
6.3 动态配置
增加配置接口,支持运行时修改参数:
verilog复制input wire [31:0] i_baud_rate,
input wire [7:0] i_data_bits,
input wire [1:0] i_stop_bits
6.4 错误检测
增加错误检测机制:
- 奇偶校验错误
- 帧错误
- 溢出错误
verilog复制output wire o_parity_error,
output wire o_frame_error,
output wire o_overflow_error
7. 实际应用建议
- 时钟域处理:如果UART接口与其他模块时钟不同,需要添加跨时钟域同步
- ESD保护:UART接口添加适当的ESD保护器件
- 电平转换:根据连接设备电平,可能需要电平转换电路(如3.3V转5V)
- 电缆长度:长距离传输时考虑添加终端匹配电阻
- 功耗管理:空闲时降低功耗,如关闭时钟或进入低功耗模式
在项目中使用这个UART发送模块时,建议先进行充分的仿真验证,再上板测试。可以先降低波特率测试基本功能,再逐步提高波特率到目标值。同时,建议添加足够的调试信息输出,如通过LED指示状态,便于问题定位。