news 2026/9/13 5:44:36

FPGA串口通信实战:DE2-70上UART设计、分频与回环验证

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 = 2'd0; localparam START = 2'd1; localparam DATA = 2'd2; localparam STOP = 2'd3; 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 <= 4'd0; clk_cnt <= 16'd0; txd <= 1'b1; end else begin case (state) IDLE: begin txd <= 1'b1; if (tx_start) begin clk_cnt <= 16'd0; state <= START; end end START: begin txd <= 1'b0; // 起始位拉低 if (clk_cnt == DIV - 1) begin clk_cnt <= 16'd0; state <= DATA; bit_cnt <= 4'd0; end else begin clk_cnt <= clk_cnt + 1'b1; end end DATA: begin txd <= tx_data[bit_cnt]; // LSB first if (clk_cnt == DIV - 1) begin clk_cnt <= 16'd0; if (bit_cnt == DATA_BITS - 1) begin state <= STOP; end else begin bit_cnt <= bit_cnt + 1'b1; end end else begin clk_cnt <= clk_cnt + 1'b1; end end STOP: begin txd <= 1'b1; // 停止位拉高 if (clk_cnt == DIV - 1) begin clk_cnt <= 16'd0; state <= IDLE; end else begin clk_cnt <= clk_cnt + 1'b1; end end default: begin txd <= 1'b1; state <= IDLE; end endcase end end endmodule

这段代码的关键点在于txd的赋值时机。START 状态内txd <= 1'b0在进入 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 接 TX,RXD 悬空,收发当然都看不到数据。

连接方式对 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_TXDUART_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 发送勾上,发送55A5。如果 FPGA 的接收状态机能正确解析,并且发送状态机把收到的字节回传,调试助手会收到同样的数据。若先自发自收 USB 转串口本身没有问题,再排查 FPGA 侧。

3.4 回环没反应时的排查顺序

按下面这个顺序检查,比盲目重烧 bitstream 有效得多:

  1. 设备管理器里确认 COM 口存在,并且没有被蓝牙虚拟串口占掉。
  2. 用 USB 转串口的 TX/RX 短接做自发自收,排除转换器故障。
  3. 用示波器或逻辑分析仪看 DE2-70 的 UART_RXD 引脚是否有下降沿。
  4. 检查 TXD/RXD 是否交叉,GND 是否连接,RS-232 电平是否反相。
  5. 如果烧写失败提示在 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 <= 3'b111; 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 <= 1'b0; 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 + 1'b1) == rd_ptr); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin wr_ptr <= 4'd0; end else if (rx_byte_en && !fifo_full) begin fifo_mem[wr_ptr] <= rx_byte_data; wr_ptr <= wr_ptr + 1'b1; end end

rx_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", baudrate=9600, timeout=0.1, ) ser.set_buffer_size(rx_size=65536, tx_size=65536) # 一次读走缓冲区内所有数据,而不是 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, timeout=0.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(f"mismatch: got {len(echo)} bytes")

这里先发 256 字节,再一次性读回。如果预计回包超过 256,就把ser.read的字节数设为len(payload),并用timeout避免阻塞;如果出现长度正确但数据错乱,优先检查 FPGA 侧 FIFO 的读指针是否在 FIFO 空时被连续读取,导致重复搬运上一帧数据。DE2-70 上跑这种回环压力测试,比单纯在调试助手里点几下更能暴露状态机边界问题,尤其是连续两帧之间是否存在额外等待周期。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 5:44:27

Codex+Zotero文献自动化联动实战指南

1. 项目概述&#xff1a;让文献管理真正“活”起来&#xff0c;而不是堆在硬盘里吃灰你有没有过这样的经历&#xff1a;花一整个下午下载了27篇PDF&#xff0c;用Zotero挨个拖进去、手动补元数据、调格式、打标签&#xff1b;结果写论文时想查某篇关于“钙钛矿界面钝化”的文献…

作者头像 李华
网站建设 2026/9/13 5:44:00

MindSpore API全解析:从核心模块到实战技巧

1. MindSpore API全景解析&#xff1a;从入门到实战 作为华为自研的全场景AI计算框架&#xff0c;MindSpore凭借其"一次开发&#xff0c;全场景部署"的特性&#xff0c;正在成为国产AI框架的中坚力量。我在华为实习期间深度使用了MindSpore的各类API&#xff0c;发现…

作者头像 李华
网站建设 2026/9/13 5:43:41

别再只换路由器,光猫才是千兆宽带的最大瓶颈

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 5:42:00

SSM员工培训考试系统实战:MyBatis Plus与Vue全栈解析

简介&#xff1a;面向企业人事培训与考核场景的员工知识培训考试系统源码&#xff0c;是一套基于SSM&#xff08;SpringSpringMVCMyBatisPlus&#xff09;与Vue的前后端分离实现。项目涵盖题库管理、在线考试、自动评分、成绩统计、员工信息管理等核心功能&#xff0c;同时区分…

作者头像 李华
网站建设 2026/9/13 5:41:33

企业经营分析五大误区与实战解决方案

1. 经营分析常见误区解析作为从业十年的商业分析师&#xff0c;我见过太多企业在经营分析过程中踩坑。今天就来聊聊最常见的5个误区&#xff0c;这些坑我都亲身踩过&#xff0c;希望能帮你少走弯路。文末还准备了实用的分析模板和工具包&#xff0c;都是我们团队在实际项目中验…

作者头像 李华