FPGA串口通信实战:DE2-70上UART设计、分频与回环验证
简介基于DE2-70 FPGA开发板使用Verilog HDL实现的完整UART串行通信模块面向FPGA学习者、嵌入式开发者和电子工程相关专业学生既可作为理解UART协议与FPGA设计流程的入门参考也可用于课程设计与毕业设计。压缩包共包含2000个文件以Quartus工程文件qpf、qsf、sdc、Verilog源码.v、.h、C程序、Tcl脚本和XML配置文件为主另有大量综合仿真生成的中间文件整体约21.04MB可在DE2-70或兼容开发板上直接导入工程进行复现与调试。UART模块设计涵盖波特率发生器、帧格式控制起始位/数据位/校验位/停止位、异步收发状态机、数据缓冲与可选校验逻辑源码层次清晰便于逐模块分析串行通信时序并扩展到其他通信协议。目前已有118人学习这份资源对希望通过实际工程快速掌握FPGA串口开发的读者具有较高的参考价值。1. UART 在 DE2-70 上的坑从 50 MHz 时钟分频那一刻就开始了DE2-70 这套用 Verilog HDL 写的 UART 控制器本质上是把 PC 通过串口发来的数据收下再用发送状态机原样回传的链路层演示。实际调试时最容易卡住的并不是“起始位/停止位”怎么写而是波特率计数器算得对不对、管脚约束填在哪、USB 转串口插上后驱动有没有映射成 COM 口。如果直接把 50 MHz 主频除以 9600 的整数结果当成位时钟前几个字节可能正常跑上一段时间采样点就会飘出去出现夹字节和 0x55 变 0x57 这类现象。这篇文章从帧格式、分频计算、状态机实现到 DE2-70 上的回环验证把一套能直接综合的 UART HDL 设计掰开讲清楚也覆盖了接收端过采样、毛刺过滤和 Linux 下丢数据的处理方法适合刚接触 FPGA 串口的入门者也适合想复用这套逻辑做联调工具的工程师。2. 从 UART 帧格式到 Verilog 状态机先定参数再把发送通路搭起来2.1 帧格式是状态机的骨架UART 是异步通信收发双方没有公共时钟只能靠一帧数据里的电平变化来对齐。帧格式决定了状态机要有几个状态、每个状态持续多久以及空闲时 TX 引脚应该保持什么电平。一个最常见的 8-N-1 帧包括空闲时为高电平起始位拉低一个位宽数据位从 LSB 开始依次输出停止位拉高至少一个位宽。把这些位段翻译成状态就是发送通路的骨架。位段电平时长状态机动作空闲高无限制IDLE等待 tx_start起始位低1 个位宽START把 TXD 拉低数据位8 bit LSB 在先8 个位宽DATA按 bit_cnt 输出 tx_data停止位高1 个位宽STOP把 TXD 拉高可选校验位奇偶由数据决定1 个位宽在 DATA 后插入 PARITY 状态这里最容易犯的错误是把数据位当成整体输出而不是按位展开。Verilog 里tx_data是一个 8 bit 寄存器必须用bit_cnt索引每一位并且在每个位宽的边界推进一次计数器。校验位如果不需要就把 PARITY 状态去掉不要在状态机里留一个永远跳不进去的分支综合器会警告但不报错后期维护的人容易误以为校验逻辑是生效的。2.2 波特率分频50 MHz 主频下的 DIV 计算波特率发生器本质是一个计数器。对 50 MHz 系统时钟发送 9600 bps 时每个位宽需要50_000_000 / 9600 ≈ 5208.33个时钟周期取整数 5208。因为取整丢掉了 0.33 个周期每个位都会短约 6.4 纳秒一个字节 10 个位累积下来约 64 纳秒的偏差相对位宽 104 微秒来说可以忽略。但如果你不是用 DIV 控制位宽而是用一个频率刚好等于波特率的独立时钟去驱动状态机问题就会放大波特率时钟通常无法精确生成且状态跳变沿会和 UART 数据沿叠加出不可预期的建立时间。波特率(bps)DIV CLK_FREQ / BAUD位宽(us)理论位宽(us)单 bit 误差96005208104.160104.167约 0.006%19200260452.08052.083约 0.006%1152004348.6808.681约 0.012%这个误差在 FPGA 内部没有累积风险因为每个字节的起始位边沿都会重新校准一次。接收端尤其不能依赖这个 DIV 值做“整位采样”它只适合发送端接收端需要更高的采样密度这部分在第 4 章展开。参数表里我一般把 DIV 定义成带符号的 localparam而不是直接写在 if 判断里这样换板卡时只需要改顶层参数。2.3 发送状态机骨架一段能直接综合的 Verilog下面这段代码只包含发送通路时钟采用 50 MHz波特率和时钟频率都用 parameter 暴露出来换到 DE2-70 的其他工程不用改内部逻辑。// uart_tx_ctrl.v module uart_tx_ctrl #( parameter CLK_FREQ 50_000_000, parameter BAUD_RATE 9_600, parameter DATA_BITS 8 )( input wire clk, input wire rst_n, input wire tx_start, input wire [7:0] tx_data, output reg txd ); localparam DIV CLK_FREQ / BAUD_RATE; localparam IDLE 2d0; localparam START 2d1; localparam DATA 2d2; localparam STOP 2d3; reg [1:0] state; reg [3:0] bit_cnt; reg [15:0] clk_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; bit_cnt 4d0; clk_cnt 16d0; txd 1b1; end else begin case (state) IDLE: begin txd 1b1; if (tx_start) begin clk_cnt 16d0; state START; end end START: begin txd 1b0; // 起始位拉低 if (clk_cnt DIV - 1) begin clk_cnt 16d0; state DATA; bit_cnt 4d0; end else begin clk_cnt clk_cnt 1b1; end end DATA: begin txd tx_data[bit_cnt]; // LSB first if (clk_cnt DIV - 1) begin clk_cnt 16d0; if (bit_cnt DATA_BITS - 1) begin state STOP; end else begin bit_cnt bit_cnt 1b1; end end else begin clk_cnt clk_cnt 1b1; end end STOP: begin txd 1b1; // 停止位拉高 if (clk_cnt DIV - 1) begin clk_cnt 16d0; state IDLE; end else begin clk_cnt clk_cnt 1b1; end end default: begin txd 1b1; state IDLE; end endcase end end endmodule这段代码的关键点在于txd的赋值时机。START 状态内txd 1b0在进入 START 的下一个时钟沿生效同时clk_cnt从 0 开始计数等到clk_cnt DIV - 1时跳进 DATA此时刚才的起始位已经被保持了整整 DIV 个时钟周期。数据位用tx_data[bit_cnt]索引每经过一个 DIV 就推进一位因此每位宽度一致。参数DIV是整数除法后的 localparam综合时会被视为常量不会消耗逻辑资源。如果波特率不是 9600只需要改BAUD_RATE如果开发板时钟不是 50 MHz需要同步改CLK_FREQ否则串口助手里显示的永远是一堆乱码。还有一个容易忽略的点tx_start必须给出至少一个时钟周期的脉冲不能一直拉高否则状态机会反复启动形成连续发送。2.4 接收状态机的设计思路先立起来接收通路比发送复杂因为它无法预知数据什么时候来。常见做法是让状态机停留在 IDLE持续检测rxd的下降沿检测到下降沿后等待半个位宽再采样数据位中心点。这里“半个位宽”同样由 DIV 控制但实际验证中直接用DIV/2会引入半周期不对齐的问题更稳妥的方案是采用 16 倍过采样把每个位宽切成 16 个采样点在第 8 个采样点附近读取电平。这样即使起始沿检测慢了半个时钟采样点仍然落在位中心附近。完整的过采样状态机留到第 4 章这里先记住一句话接收端不要只在DIV边界采一次采样密度越高毛刺容忍度越好。3. 在 DE2-70 工程里跑通串口回环管脚约束、USB 转串口与调试助手3.1 DE2-70 上的串口链路DE2-70 板卡上的串口物理层通常由板载 RS-232 收发器完成FPGA 侧看到的是 3.3 V 或者 2.5 V 的 CMOS 电平板子外部则是 DB9 或者排针。PC 和开发板之间如果都用 DB9直接做交叉线连接如果板子只引出排针则需要自己把 TXD 和 RXD 交叉同时把 GND 连在一起。很多回环调不通的问题都出在这一步TX 接 TXRXD 悬空收发当然都看不到数据。连接方式对 UART HDL 验证没有影响因为 FPGA 只感知数字电平真正决定能否通信的是两端是否共地、电平标准是否兼容。建议在动手写 Quartus 工程之前先用万用表确认 DE2-70 串口接头上的 TXD 和 RXD 引脚位置再对照原理图把信号名映射到顶层模块端口。3.2 Quartus 里加 QSF 管脚约束和时序例外在 Quartus II 工程里顶层模块的信号名需要与 DE2-70 的管脚分配文件对应。DE2-70 板卡附带的DE2_70_pin_assignments.csv或.qsf里已经给出了UART_TXD、UART_RXD的位置直接复制过来不要自己凭原理图猜引脚编号。下面是一段 QSF 格式示例# 以 DE2-70 手册实际引脚为准这里只给格式示例 set_location_assignment PIN_AC23 -to uart_txd set_location_assignment PIN_AD25 -to uart_rxd set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to uart_txd set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to uart_rxd # rxd 是异步输入先打两拍再进状态机避免亚稳态 set_false_path -from [get_ports {uart_rxd}] -to [get_registers {rx_sync*|q}]第一段把 TXD/RXD 绑定到物理引脚并指定电平标准第二段是时序例外。UART 的 RXD 信号与系统时钟没有固定相位关系TimeQuest 静态时序分析如果对这个端口做 setup/hold 检查可能报出红色关键路径但那条路径在实际硬件上往往不影响功能。更合理的做法是给 RXD 加一拍或两拍同步器再对同步寄存器设置set_false_path让时序分析工具知道这组信号是异步来源不需要按系统时钟约束。QSF 文件修改后需要重新 full compile单独做 Analysis Synthesis 不会重新生成管脚分配结果。3.3 用串口调试助手与 USB 转串口做回环实测DE2-70 自带 RS-232 端口但现代 PC 基本没有 DB9 串口最常用的是 USB 转串口方案。市场上常见的芯片有 FT232R、CH340 和 PL2303 三类驱动表现差异很大调试前先确认设备管理器里出现了 COM 端口。芯片方案驱动注意点常见问题FT232RFTDI 官方 VCP 驱动买了克隆片会被识别成 FTDI 但复位异常建议换原厂线CH340CH341SER 官方驱动插在 USB 3.0 口可能不识别换 USB 2.0 口即可PL2303找旧版本驱动新版对老芯片不友好芯片型号繁杂装完提示未知设备回环测试步骤先打开串口调试助手选择 COM 号波特率设 9600数据位 8停止位 1校验无流控无。然后把调试助手的 hex 发送勾上发送55或A5。如果 FPGA 的接收状态机能正确解析并且发送状态机把收到的字节回传调试助手会收到同样的数据。若先自发自收 USB 转串口本身没有问题再排查 FPGA 侧。3.4 回环没反应时的排查顺序按下面这个顺序检查比盲目重烧 bitstream 有效得多设备管理器里确认 COM 口存在并且没有被蓝牙虚拟串口占掉。用 USB 转串口的 TX/RX 短接做自发自收排除转换器故障。用示波器或逻辑分析仪看 DE2-70 的 UART_RXD 引脚是否有下降沿。检查 TXD/RXD 是否交叉GND 是否连接RS-232 电平是否反相。如果烧写失败提示在 JTAG 链路则和 UART 无关检查 USB-Blaster 驱动和下载模式。有一种常见误判串口调试助手里能看到字符但打开或关闭 DTR/RTS 时 FPGA 复位于是数据中断。这是因为有些 USB 转串口默认把 RTS/DTR 作为供电或复位信号DE2-70 的 RS-232 电路如果把这些信号引到 FPGA就可能导致系统抖动。遇到这种情况把流控关掉并且检查原理图上 RTS/CTS 是否真正接到 UART 模块而不是接到了复位电路。4. 异步接收别按整数倍采UART RX 的过采样与毛刺过滤4.1 为什么接收端需要 16 倍过采样发送端只要保证每个位的宽度接收端却要知道“什么时候该采样”。如果接收端只在自己的时钟下定时采样两个独立时钟源的相位差会在长时间传输中累积最终采样到相邻位的边界。更可靠的做法是在检测到起始位下降沿后用高频时钟对 RXD 连续采样把每个位宽切成多个采样点然后选择中心位置的电平作为该位的最终值。过采样倍数每 bit 采样点数常见采样点抗毛刺能力8 倍8第 4 点较弱16 倍16第 8 点中等32 倍32第 16 点较强但消耗更多寄存器工程里最常用 16 倍过采样。对 50 MHz 时钟和 9600 波特率单个 bit 是 5208 个系统时钟周期过采样时钟周期就是5208 / 16 325这里同样有个 0.5 周期的误差但因为每个 bit 重新采样误差不会跨 bit 积累。更高波特率如 115200 时DIV / 16 27过采样时钟只剩下 27 个周期采样密度较低此时建议直接用 50 MHz 时钟做逐周期边沿检测再配合计数采样。4.2 毛刺过滤与边沿确认RXD 在插拔接插件、电平转换芯片切换瞬间会产生亚稳态或毛刺。如果状态机直接拿原始rxd信号做边沿检测一个 20 ns 的毛刺就可能被当成起始位。常规做法是先把rxd打两拍同步到系统时钟域再做多数投票过滤。// uart_rx_filter.v reg [2:0] rx_shift; always (posedge clk or negedge rst_n) begin if (!rst_n) rx_shift 3b111; else rx_shift {rx_shift[1:0], rxd}; end // 3 位多数投票rx_shift 中至少 2 个采样为低才认为是稳定的低电平 wire rx_low_majority (~rx_shift[0] ~rx_shift[1]) | (~rx_shift[1] ~rx_shift[2]) | (~rx_shift[0] ~rx_shift[2]); // 下降沿在多数投票结果中出现且上一拍还是高电平 reg rx_low_d; always (posedge clk or negedge rst_n) begin if (!rst_n) begin rx_low_d 1b0; end else begin rx_low_d rx_low_majority; end end wire start_edge rx_low_majority ~rx_low_d;这段逻辑里rx_shift连续缓存了三拍rxd每次时钟沿只移入一个采样点。rx_low_majority通过组合逻辑判断三个采样点里是否有至少两个低电平这样单个周期的毛刺无法翻转结果。start_edge表示刚才还是高电平现在多数投票结果变成低电平标志着起始位起点。实际使用时在检测到start_edge后还要再等 8 个过采样时钟周期到达位中心然后连续采 3 次取多数作为数据位值。这种多数投票方式能滤掉窄脉冲但会引入一个采样周期的延迟。DE2-70 上的 RS-232 速率一般只有几百 Kbps 以下这个延迟相比位宽可以忽略如果换到高速串口就要用异步 FIFO 和专用 IO 电平处理不能只靠寄存器投票。4.3 奇偶校验与“能发不能收”的注意点如果工程打开了奇偶校验发送端需要在数据位后插入一个校验位接收端再对数据位和校验位做异或比较。Verilog 里可以用^tx_data算出偶校验再取反得到奇校验// 偶校验位生成 assign tx_parity ^tx_data; // 奇校验位生成 assign tx_parity_odd ~^tx_data;这里有个容易踩的坑校验位只能在发送状态机的 DATA 之后、STOP 之前插入而不是在 DATA 状态内多算一位。如果状态机把校验位当成第 9 个数据位输出接收端会永远报校验错。调这类问题先关掉上位机软件的校验选项改成 8-N-1 帧回环确认数据通路本身没问题再打开校验位。“能发不能收”的场景通常是发送状态机正常RXD 却没有进入接收状态机。我会先看接收端是否使用了同步寄存器如果直接把rxd接到状态机组合逻辑没有打拍Quartus 综合后会出现亚稳态风险表现为时好时坏。其次检查 DE2-70 的 RS-232 收发器型号MAX3232 一类的芯片如果电荷泵电容没焊接电平转换会失败FPGA 引脚上既看不到高电平也看不到低电平问题不在 Verilog 代码而在硬件。5. 进阶用异步 FIFO 缓存 UART 接收Linux 端丢字节一起解决5.1 给接收字节加一层 FIFO 缓冲当 UART 高速接收而 FPGA 内部还有其他模块在同时处理数据时接收状态机每次只吐出 1 个字节处理模块未必能及时取走。常见做法是在 RX 与用户逻辑之间加一个小型 FIFO队列深度根据单包最大长度选择示例中定义 16 深、8 bit 宽reg [7:0] fifo_mem [0:15]; reg [3:0] wr_ptr; reg [3:0] rd_ptr; wire fifo_empty (wr_ptr rd_ptr); wire fifo_full ((wr_ptr 1b1) rd_ptr); always (posedge clk or negedge rst_n) begin if (!rst_n) begin wr_ptr 4d0; end else if (rx_byte_en !fifo_full) begin fifo_mem[wr_ptr] rx_byte_data; wr_ptr wr_ptr 1b1; end endrx_byte_en是接收状态机在完整收完一帧后送出的单周期脉冲此时rx_byte_data里是有效数据。FIFO 满时不要继续写入发送端会因此丢帧。这里没有用 Quartus 的 FIFO IP是因为 16 深的小队列用寄存器实现反而更直观软核或 Nios II 要接自定义外设时改成 Avalon-MM 接口也方便。5.2 Linux 下从串口读取容易丢字节的原因Linux 上打开/dev/ttyUSB0后如果应用层用read(fd, buf, 1)一个字节一个字节读内核串口缓冲区很快会被填满表现为“数据丢失”而不是“读不到数据”。UART 速率 9600 时大约每毫秒一个字节单字节 read 的系统调用开销远大于 1 ms瓶颈不在驱动而在应用层。import serial ser serial.Serial( port/dev/ttyUSB0, baudrate9600, timeout0.1, ) ser.set_buffer_size(rx_size65536, tx_size65536) # 一次读走缓冲区内所有数据而不是 read(1) data ser.read(ser.in_waiting or 1)ser.in_waiting返回当前内核缓冲里的字节数read(n)能一次把整批数据搬出来减少系统调用次数。如果你的上位机还需要解析帧头帧尾拆包逻辑放到应用层做不要在 read 循环里逐字节处理。Linux 下也可以先用stty -F /dev/ttyUSB0 raw -echo把 tty 设置为原始模式避免终端驱动把 0x11/0x13 当成软件流控吞掉。5.3 把 FIFO 缓冲接到一个最简回环脚本FPGA 侧 FIFO 准备好后可以在 DE2-70 上做一个“USB 转串口回环压力测试”上位机连续发送一组递增字节FPGA 接收 FIFO 存满或收到固定长度后再整包通过 TX 发回。脚本只需要几行import serial ser serial.Serial(/dev/ttyUSB0, 9600, timeout0.3) payload bytes([0x55, 0xA5, i 0xFF for i in range(256)]) ser.write(payload) echo ser.read(ser.in_waiting or 1) if echo payload: print(loopback ok) else: print(fmismatch: got {len(echo)} bytes)这里先发 256 字节再一次性读回。如果预计回包超过 256就把ser.read的字节数设为len(payload)并用timeout避免阻塞如果出现长度正确但数据错乱优先检查 FPGA 侧 FIFO 的读指针是否在 FIFO 空时被连续读取导致重复搬运上一帧数据。DE2-70 上跑这种回环压力测试比单纯在调试助手里点几下更能暴露状态机边界问题尤其是连续两帧之间是否存在额外等待周期。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →