尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

FPGA串口通信设计:从协议原理到Verilog实现与调试

FPGA串口通信设计:从协议原理到Verilog实现与调试 1. 项目概述为什么要在FPGA上实现串口通信在嵌入式系统和数字逻辑设计领域串口通信UART堪称是“数字世界的老黄牛”。它结构简单、协议透明是MCU、传感器、调试终端之间最基础、最可靠的通信方式之一。那么当我们的主角换成FPGA时这件事就变得有趣了。你可能会问明明有现成的UART IP核或者专用芯片为什么还要用FPGA从头设计一个串口这恰恰是理解FPGA设计精髓的绝佳切入点。首先自主可控与深度定制。使用现成IP核固然快捷但其内部状态机、缓冲区大小、错误处理机制对你而言可能是个黑盒。当你需要实现一些非标准波特率如自定义的115200*1.5、添加特定的数据包帧头帧尾比如网络热词中提到的分隔符0x0D 0x0A、或者集成复杂的流控逻辑时自己动手设计才能做到心手合一。其次FPGA的并行优势。一个FPGA可以轻松实现几十路甚至上百路独立的UART通道且互不干扰这对于需要多串口汇聚如热词中“多串口接收合并到单串口发送”的网关设备来说是MCU难以企及的性能。最后学习与验证的价值。UART设计涵盖了FPGA开发的核心技能同步时序设计、状态机FSM应用、跨时钟域处理、以及Testbench验证。搞定一个稳定的UART就等于打通了FPGA数字系统设计的任督二脉。本文将带你从零开始用Verilog HDL在FPGA上实现一个功能完整、稳定可靠的UART收发器。我们会深入每个模块的细节解释每一个参数和状态转移背后的原因并分享在实际工程中调试UART时那些手册上不会写的“坑”和技巧。无论你是刚接触FPGA的新手还是想深化对通信接口理解的老手这篇内容都能提供可直接“抄作业”的实践指南。2. 核心设计思路与架构拆解2.1 UART协议的精髓与关键参数在动手写代码之前我们必须吃透协议。UART是一种异步串行通信协议其通信帧格式是固定的1个起始位低电平、5-9个数据位、可选的奇偶校验位、1个或2个停止位高电平。通信双方完全依靠预先约定好的波特率Baud Rate来同步每一位的采样时刻。这里有一个至关重要的概念波特率时钟Baud Clock。它不是直接用于驱动发送或接收数据的时钟而是用来产生采样点。通常为了在数据位中心获得最稳定的采样值我们会使用一个比波特率高N倍的本地时钟如16倍通过计数器产生一个波特率使能信号。例如系统时钟为50MHz目标波特率为115200那么每个位周期的系统时钟周期数为50,000,000 / 115200 ≈ 434。如果我们采用16倍过采样则计数器模值应为434 / 16 ≈ 27。这个计算过程直接决定了通信的准确性。注意波特率误差是UART通信失败的首要元凶。误差必须控制在允许范围内通常3%。计算分频系数时应使用取整而非四舍五入并尽可能选择高频的系统时钟来降低量化误差。例如用50MHz时钟产生115200波特率实际波特率为50,000,000 / (27*16) ≈ 115740误差约为0.47%完全可接受。2.2 整体系统架构设计一个完整的FPGA UART模块通常划分为以下几个核心部分这种划分也体现了模块化设计思想波特率发生器Baud Rate Generator核心是一个分频计数器根据系统时钟和配置的波特率产生一个周期等于位宽时间的脉冲信号baud_tick以及用于接收过采样的16倍频脉冲sample_tick。发送器UART Transmitter, TX一个有限状态机FSM。当收到发送请求后FSM依次输出起始位、数据位、校验位如有和停止位。每个位的持续时间由baud_tick控制。接收器UART Receiver, RX这是设计的难点和重点。同样是一个FSM但它需要持续监听RX线路。检测到起始位下降沿后FSM启动并在每个数据位的中心点利用sample_tick计数采样RX引脚状态组装成数据。它必须能处理毛刺和噪声。顶层封装与接口Top Module Interface将TX和RX模块、波特率发生器以及可能的FIFO先入先出缓冲区封装起来对外提供简洁的并行数据接口如data_to_send,send_en,data_received,data_valid和串行物理接口uart_tx,uart_rx。为何要采用状态机因为UART的发送和接收过程本质上是一系列按严格时间顺序执行的动作这正是状态机的用武之地。状态清晰逻辑明确调试时也容易定位问题所在。3. 核心模块的Verilog实现与细节剖析3.1 波特率发生器的精确实现波特率发生器是全局的“心跳”。我们不直接生成波特率时钟而是生成周期性的使能脉冲这更符合同步设计规范避免产生难以处理的毛刺时钟。module baud_gen #( parameter CLK_FREQ 50_000_000, // 输入时钟频率单位Hz parameter BAUD_RATE 115200 // 目标波特率 )( input wire clk, input wire rst_n, output reg baud_tick, // 波特率使能脉冲位周期 output reg sample_tick // 采样使能脉冲16倍波特率 ); // 计算分频系数 localparam BIT_CYCLE CLK_FREQ / BAUD_RATE; // 每个位周期的时钟数 localparam SAMPLE_CYCLE BIT_CYCLE / 16; // 每个采样周期的时钟数 localparam CNT_WIDTH $clog2(BIT_CYCLE); reg [CNT_WIDTH-1:0] bit_cnt; reg [3:0] sample_cnt; // 0-15计数器用于产生16倍采样 always (posedge clk or negedge rst_n) begin if (!rst_n) begin bit_cnt 0; sample_cnt 0; baud_tick 1b0; sample_tick 1b0; end else begin // 默认拉低脉冲信号 baud_tick 1b0; sample_tick 1b0; // 位周期计数器 if (bit_cnt BIT_CYCLE - 1) begin bit_cnt 0; baud_tick 1b1; // 一个位周期结束产生脉冲 end else begin bit_cnt bit_cnt 1; end // 采样周期计数器16倍频 if (sample_cnt 15) begin sample_cnt 0; sample_tick 1b1; // 每16个采样周期产生一个脉冲对应位周期的开始 // 错这里需要对齐。 end else begin sample_cnt sample_cnt 1; end end end endmodule上面代码有一个典型的逻辑错误sample_tick的产生没有与baud_tick同步。在接收端我们需要在数据位的中心点采样这意味着第一个采样点应该出现在起始位开始后的第8个sample_tick假设16倍过采样中心点是第7.5个取整为第8个。因此更稳健的做法是只用一个主计数器然后通过比较来产生两个使能信号。修正后的核心逻辑// 在always块内 reg [CNT_WIDTH-1:0] main_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin main_cnt 0; baud_tick 0; sample_tick 0; end else begin baud_tick 0; sample_tick 0; if (main_cnt BIT_CYCLE - 1) begin main_cnt 0; baud_tick 1‘b1; // 每个位周期结束时产生一个脉冲 end else begin main_cnt main_cnt 1; end // 当主计数器计数值是SAMPLE_CYCLE的整数倍时产生采样脉冲 // 并且为了对准位中心我们可以从计数到 SAMPLE_CYCLE/2 开始第一个有效采样 if (main_cnt % SAMPLE_CYCLE (SAMPLE_CYCLE/2)) begin sample_tick 1‘b1; end end end这个设计确保了sample_tick精确地出现在每个“虚拟”采样窗口的中心为接收器提供稳定的采样时机。3.2 发送器TX状态机设计发送器是一个典型的状态机状态转移由baud_tick驱动。状态定义如下IDLE空闲状态输出线为高电平停止位电平。START_BIT发送起始位低电平持续一个位周期。DATA_BITS依次发送8个数据位从最低位LSB开始。PARITY_BIT可选发送奇偶校验位。STOP_BIT发送停止位高电平至少一个位周期。module uart_tx #( parameter DATA_WIDTH 8 )( input wire clk, input wire rst_n, input wire [DATA_WIDTH-1:0] data_in, input wire send_en, input wire baud_tick, // 来自波特率发生器 output reg tx_out, output reg tx_busy ); // 状态定义 localparam S_IDLE 0; localparam S_START 1; localparam S_DATA 2; localparam S_STOP 3; reg [1:0] state, next_state; reg [2:0] bit_index; // 用于计数已发送的数据位 reg [DATA_WIDTH-1:0] data_reg; // 状态寄存器更新 always (posedge clk or negedge rst_n) begin if (!rst_n) state S_IDLE; else state next_state; end // 状态转移逻辑与输出逻辑组合逻辑部分也可拆分为两个always块 always (*) begin next_state state; tx_out 1‘b1; // 默认输出高电平 tx_busy 1‘b1; // 默认忙 case (state) S_IDLE: begin tx_out 1‘b1; tx_busy 1‘b0; if (send_en) begin next_state S_START; end end S_START: begin tx_out 1‘b0; // 起始位 if (baud_tick) begin next_state S_DATA; end end S_DATA: begin tx_out data_reg[bit_index]; // 发送当前位 if (baud_tick) begin if (bit_index DATA_WIDTH-1) begin next_state S_STOP; end // bit_index 在时钟驱动下更新见下方时序逻辑 end end S_STOP: begin tx_out 1‘b1; // 停止位 if (baud_tick) begin next_state S_IDLE; end end default: next_state S_IDLE; endcase end // 数据寄存与位索引更新时序逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin data_reg 0; bit_index 0; end else begin case (state) S_IDLE: begin if (send_en) begin data_reg data_in; // 锁存待发送数据 end bit_index 0; end S_DATA: begin if (baud_tick) begin if (bit_index DATA_WIDTH-1) begin bit_index bit_index 1; end else begin bit_index 0; end end end default: begin // 其他状态保持 end endcase end end endmodule实操心得发送器的设计相对直接关键点在于精确的时序控制。baud_tick必须干净无毛刺且状态转移一定要在baud_tick有效的那个时钟沿发生。tx_busy信号非常有用可以反馈给上游逻辑如CPU或另一个状态机防止在新数据尚未发送完时被覆盖。3.3 接收器RX状态机与抗干扰设计接收器是UART设计的灵魂也是调试中最容易出问题的地方。其核心挑战是在异步信号中可靠地检测起始位并同步采样。状态设计IDLE监控RX线等待下降沿起始位。START_DET检测到下降沿后等待半个位周期即8个sample_tick后再次采样确认是否为有效的起始位依然是低电平。这一步是抗干扰的关键可以滤除短于半个位周期的毛刺。RECV_DATA在随后的每个位周期中心点即每16个sample_tick采样一次采样RX线并存入移位寄存器。STOP_BIT采样停止位验证其为高电平。如果停止位为低则可能发生帧错误。module uart_rx #( parameter DATA_WIDTH 8, parameter OVERSAMPLE 16 // 过采样倍数 )( input wire clk, input wire rst_n, input wire rx_in, input wire sample_tick, // 16倍波特率的采样脉冲 output reg [DATA_WIDTH-1:0] data_out, output reg data_valid, output reg frame_error ); // 状态定义 localparam S_IDLE 0; localparam S_START_DET 1; localparam S_RECV_DATA 2; localparam S_STOP_BIT 3; reg [1:0] state, next_state; reg [3:0] sample_cnt; // 0-15计数器 reg [2:0] bit_cnt; // 0-7数据位计数器 reg [DATA_WIDTH-1:0] shift_reg; reg rx_sync1, rx_sync2; // 两级同步器用于跨时钟域 // 对异步的rx_in进行同步化处理消除亚稳态 always (posedge clk or negedge rst_n) begin if (!rst_n) begin rx_sync1 1‘b1; rx_sync2 1‘b1; end else begin rx_sync1 rx_in; rx_sync2 rx_sync1; end end // 状态寄存器 always (posedge clk or negedge rst_n) begin if (!rst_n) state S_IDLE; else state next_state; end // 状态转移与计数器逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin next_state S_IDLE; sample_cnt 0; bit_cnt 0; shift_reg 0; data_out 0; data_valid 1‘b0; frame_error 1‘b0; end else begin data_valid 1‘b0; // 默认拉低脉冲有效 frame_error 1‘b0; case (state) S_IDLE: begin sample_cnt 0; bit_cnt 0; if (rx_sync2 1‘b0) begin // 检测到下降沿 next_state S_START_DET; end end S_START_DET: begin if (sample_tick) begin if (sample_cnt (OVERSAMPLE/2 - 1)) begin // 到达起始位中心点再次采样确认 if (rx_sync2 1‘b0) begin // 确认是有效的起始位 next_state S_RECV_DATA; end else begin // 是毛刺回到空闲 next_state S_IDLE; end sample_cnt 0; end else begin sample_cnt sample_cnt 1; end end end S_RECV_DATA: begin if (sample_tick) begin if (sample_cnt (OVERSAMPLE - 1)) begin // 到达一个数据位的采样点中心点 shift_reg {rx_sync2, shift_reg[DATA_WIDTH-1:1]}; // 右移LSB先入 sample_cnt 0; if (bit_cnt DATA_WIDTH-1) begin // 所有数据位接收完毕 next_state S_STOP_BIT; bit_cnt 0; end else begin bit_cnt bit_cnt 1; end end else begin sample_cnt sample_cnt 1; end end end S_STOP_BIT: begin if (sample_tick) begin if (sample_cnt (OVERSAMPLE - 1)) begin // 采样停止位 if (rx_sync2 1‘b1) begin // 停止位正确 data_out shift_reg; // 输出数据 data_valid 1‘b1; end else begin // 帧错误停止位为低 frame_error 1‘b1; end next_state S_IDLE; sample_cnt 0; end else begin sample_cnt sample_cnt 1; end end end default: next_state S_IDLE; endcase end end endmodule核心技巧接收器设计中两级同步器rx_sync1, rx_sync2是必须的因为rx_in是来自FPGA引脚的外部异步信号直接使用会产生亚稳态风险。其次在起始位中心点进行二次验证是区分真实起始位和噪声毛刺的经典方法能极大提升通信可靠性。4. 系统集成、仿真验证与板级调试4.1 顶层模块集成与FIFO缓冲将上述模块与一个FIFO先入先出存储器集成可以构建一个更实用的UART控制器。FIFO用于缓冲收发数据解决上游数据处理速度与串口速度不匹配的问题。module uart_top #( parameter CLK_FREQ 50_000_000, parameter BAUD_RATE 115200, parameter DATA_WIDTH 8, parameter FIFO_DEPTH 16 )( input wire clk, input wire rst_n, // 并行用户接口 input wire [DATA_WIDTH-1:0] tx_data_i, input wire tx_wr_en_i, output wire tx_full_o, output wire tx_busy_o, output wire [DATA_WIDTH-1:0] rx_data_o, output wire rx_rd_en_i, output wire rx_empty_o, output wire rx_valid_o, // 串行物理接口 output wire uart_tx, input wire uart_rx ); // 内部信号声明 wire baud_tick, sample_tick; wire tx_fifo_empty, tx_fifo_full; wire [DATA_WIDTH-1:0] tx_fifo_data; wire rx_fifo_full; wire [DATA_WIDTH-1:0] rx_fifo_data; // 实例化模块 baud_gen #(.CLK_FREQ(CLK_FREQ), .BAUD_RATE(BAUD_RATE)) u_baud_gen(.clk(clk), .rst_n(rst_n), .baud_tick(baud_tick), .sample_tick(sample_tick)); // TX FIFO (例如使用FPGA内置的Block RAM IP核或自己编写的小容量FIFO) fifo #(.WIDTH(DATA_WIDTH), .DEPTH(FIFO_DEPTH)) u_tx_fifo(.clk(clk), .rst_n(rst_n), .wr_en(tx_wr_en_i), .wr_data(tx_data_i), .full(tx_full_o), .rd_en(!tx_fifo_empty !tx_busy_o baud_tick), // 当发送器空闲且FIFO不空时在波特率节拍下读数据 .rd_data(tx_fifo_data), .empty(tx_fifo_empty)); uart_tx #(.DATA_WIDTH(DATA_WIDTH)) u_tx(.clk(clk), .rst_n(rst_n), .data_in(tx_fifo_data), .send_en(!tx_fifo_empty), // FIFO不空即启动发送 .baud_tick(baud_tick), .tx_out(uart_tx), .tx_busy(tx_busy_o)); uart_rx #(.DATA_WIDTH(DATA_WIDTH), .OVERSAMPLE(16)) u_rx(.clk(clk), .rst_n(rst_n), .rx_in(uart_rx), .sample_tick(sample_tick), .data_out(rx_fifo_data), .data_valid(rx_valid_wire), // 内部连线 .frame_error()); // RX FIFO fifo #(.WIDTH(DATA_WIDTH), .DEPTH(FIFO_DEPTH)) u_rx_fifo(.clk(clk), .rst_n(rst_n), .wr_en(rx_valid_wire), .wr_data(rx_fifo_data), .full(rx_fifo_full), .rd_en(rx_rd_en_i), .rd_data(rx_data_o), .empty(rx_empty_o)); assign rx_valid_o !rx_empty_o; // 当RX FIFO非空时数据有效 endmodule这个顶层模块提供了一个友好的并行数据接口。用户只需向tx_data_i写数据并拉高tx_wr_en_i前提是tx_full_o为低数据就会进入FIFO并自动发送。接收到的数据则被存入RX FIFO用户通过读取rx_data_o并拉高rx_rd_en_i来获取。4.2 Testbench仿真验证仿真Simulation是FPGA设计流程中保证逻辑正确性的关键一步。我们需要编写一个Testbench来模拟上位机与FPGA UART的交互。timescale 1ns / 1ps module tb_uart_top(); reg clk; reg rst_n; reg [7:0] tx_data_i; reg tx_wr_en_i; wire tx_full_o; wire tx_busy_o; wire [7:0] rx_data_o; reg rx_rd_en_i; wire rx_empty_o; wire uart_tx; reg uart_rx; // 实例化被测单元 uart_top #(.CLK_FREQ(50_000_000), .BAUD_RATE(115200)) uut (.*); // 使用 .* 连接所有同名信号 // 生成50MHz时钟 initial begin clk 0; forever #10 clk ~clk; // 20ns周期即50MHz end // 模拟上位机发送数据到FPGA的RX task uart_send_byte; input [7:0] data; integer i; begin uart_rx 1‘b1; // 空闲高电平 #8680; // 等待一个波特率周期 (1/115200 ≈ 8.68us) uart_rx 1‘b0; // 起始位 #8680; for (i0; i8; ii1) begin // 发送8个数据位LSB first uart_rx data[i]; #8680; end uart_rx 1‘b1; // 停止位 #8680; end endtask // 主测试流程 initial begin // 初始化 rst_n 0; tx_data_i 0; tx_wr_en_i 0; rx_rd_en_i 0; uart_rx 1‘b1; #100; rst_n 1; #1000; // 测试1FPGA发送数据 $display([Test1] FPGA TX Test); tx_data_i 8‘h55; // 发送数据 0x55 (01010101) tx_wr_en_i 1‘b1; #20; tx_wr_en_i 1‘b0; // 等待发送完成可以通过监控tx_busy_o或简单延时 wait(tx_busy_o 1‘b0); #10000; // 测试2FPGA接收数据 $display([Test2] FPGA RX Test); uart_send_byte(8‘hAA); // 模拟上位机发送 0xAA #20000; // 等待接收处理完成 // 从RX FIFO读取数据 if (!rx_empty_o) begin rx_rd_en_i 1‘b1; #20; rx_rd_en_i 1‘b0; $display(Received data: 0x%h, rx_data_o); if (rx_data_o 8‘hAA) $display(RX Test PASSED!); else $display(RX Test FAILED!); end else begin $display(RX FIFO is empty, Test FAILED!); end // 测试3连续收发测试 $display([Test3] Loopback Test); // 将TX和RX短接在Testbench中直接赋值 assign uart_rx uart_tx; // 回环 // 发送一串数据 repeat(5) begin tx_data_i $random; tx_wr_en_i 1‘b1; #20; tx_wr_en_i 1‘b0; wait(tx_busy_o 1‘b0); #10000; // 等待回环接收 if (!rx_empty_o) begin rx_rd_en_i 1‘b1; #20; rx_rd_en_i 1‘b0; $display(Sent: 0x%h, Received: 0x%h, tx_data_i, rx_data_o); end end #10000; $finish; end endmodule通过仿真波形我们可以清晰地观察起始位、数据位、停止位的时序以及FIFO的读写指针变化确保逻辑符合预期。4.3 板级调试实战与问题排查将设计综合、实现并下载到FPGA开发板后真正的挑战才开始。以下是几个最常见的调试场景和解决方法问题1完全无通信TX线无波形。排查首先用示波器或逻辑分析仪测量uart_tx引脚。如果一直为高检查复位信号确保rst_n已正确释放为高电平。时钟信号用示波器测量输入到FPGA的系统时钟是否正常。波特率使能在代码中添加一个LED用baud_tick信号驱动它闪烁频率很低肉眼可见。如果LED不闪说明波特率发生器未工作检查分频计算和计数器逻辑。发送使能确认send_en或tx_wr_en_i信号是否被正确触发。问题2能发送但接收不到数据或接收数据乱码。排查这是最典型的问题90%以上源于波特率不匹配或起始位检测失败。示波器测量测量FPGA发出的TX波形计算实际位宽时间反推实际波特率与预设值对比。检查系统时钟频率配置是否正确CLK_FREQ参数。起始位检测在接收模块中将状态机状态state信号引出到LED或IO口观察是否从IDLE进入了START_DET和RECV_DATA。如果没有可能是RX引脚电平不对默认应为高或者起始位检测逻辑过于敏感/迟钝。可以尝试调整起始位确认的采样点如从第8个sample_tick改为第7或第9个。采样点偏移在RECV_DATA状态确保采样点正好在数据位的中心。由于线路延迟和上升/下降时间有时中心点需要微调。可以在代码中增加一个可配置的sample_offset参数进行补偿。问题3通信不稳定偶尔出错。排查信号质量用示波器观察RX/TX波形看是否有过冲、振铃或毛刺。过长或未端接的导线可能引起信号完整性问题。必要时在FPGA引脚附近添加串联匹配电阻如22欧姆或33欧姆。跨时钟域CDC如果用户接口时钟如AXI总线时钟与UART内部时钟不同源那么用户侧的tx_wr_en_i和rx_rd_en_i信号必须经过同步器处理才能接入UART模块否则会导致FIFO指针错误和数据丢失。FIFO溢出监控tx_full_o和rx_fifo_full信号。如果频繁变高说明数据产生或消费速度不匹配需要增加FIFO深度或优化流控。调试金律“让硬件说话”。不要只依赖仿真和想象一定要用示波器、逻辑分析仪LA等工具抓取真实信号。FPGA开发中将内部关键信号如状态机状态、计数器值、使能信号分配到空闲IO口用LA抓取是定位复杂时序问题的终极手段。5. 高级应用与扩展思路一个基础的UART实现只是起点。基于此我们可以进行多种功能扩展这也是FPGA灵活性的体现硬件流控RTS/CTS在高速或大数据量传输时增加请求发送RTS和清除发送CTS信号线实现硬件流控防止数据丢失。自动波特率检测设计一个算法通过测量第一个起始位的低电平持续时间自动计算并匹配对方的波特率。这对于需要自适应不同设备的应用非常有用。多协议支持在状态机中增加对数据位7/8/9、停止位1/1.5/2、奇偶校验奇/偶/无的可配置支持。DMA集成将UART的FIFO与FPGA内部的DMA控制器连接实现数据在串口和外部存储器如DDR3参考热词之间的大批量、零CPU开销搬运。多通道聚合如热词所述实现“多串口接收合并到单串口发送”。可以设计一个仲裁调度器轮询多个RX通道将数据打包后通过一个高速TX通道发送出去常用于数据采集网关。与软核处理器协同将UART作为外设挂载到如NIOS II、MicroBlaze或RISC-V等软核CPU的总线上通过驱动程序进行访问构建更复杂的片上系统SoC。实现这些扩展功能本质上是对状态机的进一步丰富和对数据通路的精心设计。每一次扩展都是对数字逻辑设计能力的深化。从最简单的UART出发你最终可以构建出像“FPGA万兆网例程”或复杂工业通信协议如Modbus控制器那样强大的系统。这条路就从你手上这个稳定可靠的UART内核开始。
返回列表