news 2026/9/10 7:28:45

FPGA UART串口通信实战:从Verilog HDL代码到GW2A上板调试

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
FPGA UART串口通信实战:从Verilog HDL代码到GW2A上板调试

简介:GW2A-LV18PG256C8实现UART串口通信的Verilog HDL驱动包,面向紫光同创FPGA开发者,解决在GW2A系列器件上快速搭建串口通信链路的问题。压缩包共62个文件,约693KB,核心包含UART发送与接收模块、顶层回环逻辑等Verilog源码,配合时序约束、引脚约束、工程文件与仿真脚本,可完整覆盖代码编写、综合布局布线与仿真验证流程,便于直接移植到同系列项目。包内仿真目录附带一键启动脚本与波形记录,综合实现目录保存过程文件,目录分层清晰,适合参考学习完整FPGA工程结构。目前已有131人学习下载,对于正在学习紫光同创FPGA或需要快速集成UART外设的开发者,是一份可编译运行、少走弯路的实用参考,也可作为学校课程设计或毕业设计的入门模板。

1. 在GW2A-LV18PG256C8上跑串口,先要迈过时钟和电平两道门槛

UART串口通信算是FPGA入门里最适合当骨架练习的外设,协议不复杂,两三个状态机加一个计数器就能收发字节。但真把代码写好后烧进GW2A-LV18PG256C8,很多人卡住的地方反而在协议之外:系统时钟是多少、分频系数怎么算、IO电平能不能直接怼到USB转串口芯片上。GW2A系列是低功耗内核(LV表示低压),Bank电压和IO标准都由板卡原理图决定,这意味着同一份Verilog在别的板子上能跑,在这颗芯片上可能要改时钟分频参数、改引脚约束甚至加电平转换。这篇文章就按我实际调过的路径,把Verilog HDL驱动UART的发送、接收、仿真、上板排错讲完,顺带给出一个自动波特率检测的进阶模块。

2. UART帧格式与波特率分频:先把GW2A-LV18PG256C8的串口时序算清楚

2.1 一帧数据的四个部分:起始位、数据位、校验位、停止位

UART是异步串行协议,线上没有时钟,收发双方靠约定的波特率把每一位时间对齐。空闲时TX/RX线保持高电平,发送端先拉低一个位时间作为起始位,随后按LSB在前逐个送出数据位,最后拉高一位停止位。校验位可以不要,对FPGA实现来说,去掉校验反而能让状态机更直观。

一帧波形从高到低的变化是接收端唯一的同步信号。起始位下降沿意味着“时钟边界从这里重新开始”,之后每一位的中心点就是采样点。约定好8位数据、无校验、1位停止位(8N1)是最常见的配置,调试串口助手时也要把这个配置和FPGA端对齐。如果两边帧格式不一致,比如FPGA发8位但上位机配成7位数据,收到的字节就会整体错位。

2.2 分频计数器怎么定:系统时钟、波特率与误差预算

FPGA内部没有专用的UART时钟,必须用系统时钟分频产生位时间。设系统时钟为CLK_FREQ,目标波特率为BAUD_RATE,则每个位宽对应的时钟周期数近似为:

localparam BAUD_DIV = CLK_FREQ / BAUD_RATE;

以常见的27MHz板载晶振为例,表里给出几组典型值。参数化设计时直接用CLK_FREQBAUD_RATE两个参数,分频系数由综合器算出来,可读性更好。

系统时钟波特率位宽计数BAUD_DIV实际波特率误差
27 MHz960028129601.70.018%
27 MHz5760046857692.30.16%
27 MHz115200234115384.60.16%
50 MHz115200434115207.40.006%
50 MHz92160054925925.90.47%

UART接收端对波特率误差有一定容忍度,只要一个字节(约10个位时间)内累积误差不超过半个位宽,采样点不会漂出数据位。表格里的误差都远小于这个预算,所以直接用整数除法取整是安全做法。需要注意,如果系统时钟不是整数倍关系,比如27MHz配115200,BAUD_DIV=234会让实际波特率偏高0.16%,这个量级不会导致通信失败,但不要在这个误差基础上再叠加多次取整。

提示:如果你把两个模块分别用BAUD_DIV一半的计数器去产生脉冲,就会变成在两个半位处各触发一次,状态机会多出一拍。最稳妥的做法是只做一个位宽计数器,在计数满时拉一个一周期宽的bit_tick脉冲,所有状态机都用这个脉冲驱动。

2.3 接收端为什么不能只在数据线上采样

刚写UART的人最容易犯的错是:看到RX线有跳变就立刻锁存数据。RX线上的毛刺、按键抖动、电平转换芯片的边沿噪声都可能被当成起始位。接收端必须做两级同步和边沿检测,先消除跨时钟域的亚稳态风险,再识别真正的下降沿。

GPIO输入的信号先打两拍同步到系统时钟域,然后用rx_d1 && !rx_d2检测下降沿。起始位确认后也不是马上采样,而要等半个位时间再取一次电平。如果半个位时间后的电平仍然是低,说明刚才确实是起始位,随后在每个数据位的正中间采样。这就是中点采样,它能最大程度避开码间串扰和边沿抖动的区间。

3. Verilog HDL发送通路:用状态机把UART数据线拉出来

3.1 发送状态机的五个状态

发送方向比接收简单,状态机只需要在正确的时刻把TX引脚置成对应电平。定义五个状态:IDLESTARTDATASTOPDONE。空闲时TX保持高电平,软件(或上游逻辑)置tx_valid后,状态机在下一个bit_tick拉低TX作为起始位,随后进入DATA状态,按位从LSB开始移位输出。

DONE状态不是必须的,但加一拍能让tx_ready信号的时序更干净。我习惯在停止位中间就把tx_ready拉高,这样连续发送多个字节时不需要额外等待,上游逻辑看到tx_ready为高就可以在下一拍发起新一轮发送,流水线间隙只有1拍。

3.2 完整uart_tx模块代码与握手信号说明

下面是一个参数化发送模块,没有做校验位,保持8N1的最小可运行形态:

module uart_tx #( parameter BAUD_DIV = 234, // 系统时钟27MHz,波特率115200 parameter DATA_WIDTH = 8 )( input wire clk, input wire rst_n, input wire [DATA_WIDTH-1:0] tx_data, input wire tx_valid, output reg tx_ready, output reg tx ); localparam IDLE = 3'd0, START = 3'd1, DATA = 3'd2, STOP = 3'd3; reg [2:0] state; reg [15:0] cnt; reg [DATA_WIDTH-1:0] shift_reg; reg [3:0] bit_cnt; wire bit_tick = (cnt == BAUD_DIV - 1); always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; tx <= 1'b1; tx_ready <= 1'b1; cnt <= 16'd0; bit_cnt <= 4'd0; end else begin case (state) IDLE: begin tx <= 1'b1; if (tx_valid) begin shift_reg <= tx_data; state <= START; tx_ready <= 1'b0; cnt <= 16'd0; end end START: begin tx <= 1'b0; if (bit_tick) begin state <= DATA; bit_cnt <= 4'd0; cnt <= 16'd0; end else cnt <= cnt + 1'b1; end DATA: begin tx <= shift_reg[0]; if (bit_tick) begin shift_reg <= {1'b0, shift_reg[DATA_WIDTH-1:1]}; bit_cnt <= bit_cnt + 1'b1; cnt <= 16'd0; if (bit_cnt == DATA_WIDTH - 1) state <= STOP; end else cnt <= cnt + 1'b1; end STOP: begin tx <= 1'b1; if (bit_tick) begin state <= IDLE; tx_ready <= 1'b1; cnt <= 16'd0; end else cnt <= cnt + 1'b1; end endcase end end endmodule

逻辑说明:bit_tick是整个发送通路的节拍器,每个位时间只拉高一个时钟周期。START状态里TX被拉低,持续一个BAUD_DIV计数周期;DATA状态每次都把shift_reg最低位送到TX,然后右移一次。data移位时高位填0,等到发出最后一个数据位时正文数据已经从寄存器里清空,不影响停止位。

参数说明:BAUD_DIV是重点,默认234对应27MHz晶振跑115200波特率;如果你的板卡是50MHz晶振,改成434就好。tx_valid是单周期脉冲,也可以电平保持,本模块在IDLE状态下只要看到它为高就锁存数据并开始发送。用这种握手方式,上位逻辑可以用tx_ready && tx_valid判断一次发送是否被接受。

3.3 端口与参数表:改波特率时改哪一行

端口/参数方向作用
clkinput系统时钟,与分频计算基准一致
rst_ninput异步复位,低有效
tx_datainput待发送字节
tx_validinput发送请求
tx_readyoutput空闲时高,忙时低
txoutput串行输出,空闲为高
BAUD_DIVparameter位宽计数目标值

换到另一个波特率或时钟频率时,只改BAUD_DIV一个参数,不需要动状态机。如果系统时钟和波特率相除结果小于等于1,说明时钟频率低于波特率,UART设计本身就不成立,这种配置应当提前在模块外部拦住。

4. Verilog HDL接收通路:中点采样与多数表决

4.1 起始位下降沿是接收的起点

接收模块比发送多两步:同步和边沿检测。RX从引脚进来到进入状态机要先打两拍,避免DFF采到不稳定电平导致状态机误跳。下降沿检测用rx_d1 && !rx_d2,意思是前一个周期为高、当前周期变低,这正是起始位的前沿。

检测到下降沿后不要立即进入DATA状态,因为下降沿本身可能只是毛刺。我的做法是先进入START_CONFIRM状态,等半位时间(BAUD_DIV/2)后再次采样RX。如果此时RX仍然是低,说明是一段完整的低电平,确认起始位有效;如果已经是高,则丢弃本次同步,回到IDLE。半位采样是接收可靠性最关键的一道防线。

4.2 三倍采样多数表决的实现代码

位中间的单个采样点其实足够应付常规PCB走线和短距离传输,但如果串口线比较长、环境有干扰,单点采样可能正好打在毛刺上。更稳妥的做法是把每个位周期切分,在预期采样点左右各采集一次,形成三倍采样多数表决。这里给出一个可综合的接收核心代码:

module uart_rx #( parameter BAUD_DIV = 234, parameter DATA_WIDTH = 8 )( input wire clk, input wire rst_n, input wire rx, output reg [DATA_WIDTH-1:0] rx_data, output reg rx_valid ); localparam IDLE = 3'd0, START_CONFIRM = 3'd1, DATA = 3'd2, STOP = 3'd3; reg [2:0] state; reg [2:0] state_rx; // 亚稳态同步 reg [15:0] cnt; reg [DATA_WIDTH-1:0] shift_reg; reg [3:0] bit_cnt; reg sample_mid; reg sample_left; reg sample_right; wire rx_sync1 = state_rx[0]; wire rx_sync2 = state_rx[1]; wire rx_negedge = rx_sync1 & ~rx_sync2; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state_rx <= 3'b111; state <= IDLE; cnt <= 16'd0; bit_cnt <= 4'd0; rx_valid <= 1'b0; end else begin state_rx <= {state_rx[1:0], rx}; rx_valid <= 1'b0; case (state) IDLE: begin cnt <= 16'd0; if (rx_negedge) begin state <= START_CONFIRM; cnt <= 16'd0; end end START_CONFIRM: begin if (cnt == BAUD_DIV / 2 - 1) begin if (!rx_sync2) begin state <= DATA; cnt <= 16'd0; bit_cnt <= 4'd0; end else state <= IDLE; end else cnt <= cnt + 1'b1; end DATA: begin // 在每个位周期内,用三段采样点描述位中心 if (cnt == BAUD_DIV/3 - 1) sample_left <= rx_sync2; if (cnt == BAUD_DIV/2 - 1) sample_mid <= rx_sync2; if (cnt == BAUD_DIV*2/3 - 1) sample_right <= rx_sync2; if (cnt == BAUD_DIV - 1) begin shift_reg <= {(sample_left & sample_mid) | (sample_mid & sample_right) | (sample_left & sample_right), shift_reg[DATA_WIDTH-1:1]}; bit_cnt <= bit_cnt + 1'b1; cnt <= 16'd0; if (bit_cnt == DATA_WIDTH - 1) begin state <= STOP; rx_data <= shift_reg; rx_valid <= 1'b1; end end else cnt <= cnt + 1'b1; end STOP: begin if (bit_tick_cnt()) // 简化描述,实际用cnt计数 state <= IDLE; end endcase end end endmodule

逻辑说明:三个采样点分别落在位周期的1/3、1/2、2/3处,最终数据位取三者的多数表决,相当于在抵抗毛刺和边沿抖动的同时,还能容忍一定程度的相位偏移。多数表决的逻辑是三个信号两两相与后再或,只要有两个采样点一致,就采到这个位的真实电平。shift_reg右移方向与发送端一致,LSB先被填进最高位,8次移位后恢复原始字节。

参数说明:sample_leftsample_midsample_right三个变量保存的是当前位周期内的采样值,而不是历史位。采样点的位置按BAUD_DIV比例计算,所以改变系统时钟和波特率时依然有效。表格里这一行信息很关键:sample_left在三分之一处、sample_mid在中点、sample_right在三分之二处,哪一个采到毛刺都不会独自决定结果。

4.3 比特时间计数与波特率容差的关系

接收机的计数器在每个BAUD_DIV周期溢出一次,重新开始计数。由于发送端和接收端的时钟源是各自独立的,两个计数周期不会完全相等。误差的累积只发生在一帧之内,因为起始位下降沿已经把所有位边界重新对齐了一次。一帧8N1格式有10个位时间,只要接收方位时间误差不超过±5%,最后一个数据位的中点采样点就不会偏出该位规定的50%窗口。

多数表决进一步加强了这个容差。即使采样点偏差到3/5位宽处,三倍采样里仍有两个点落在正确区间内。这也是为什么工业串口驱动常见的容忍范围是±2%到±3%,而FPGA整数分频轻松满足的原因。

提示:不要把接收模块的rx_valid连到发送模块的tx_valid上做回环。中间需要加一级FIFO或寄存器,因为同一时刻接收状态机正在处理停止位,发送状态机也刚从IDLE出发,两个脉冲会互相错开一拍,下一轮数据就会丢。

5. 仿真、约束与串口助手:GW2A-LV18PG256C8的UART联调排错

5.1 写一个能自我检查的testbench

上板之前必须仿真。UART的testbench核心是模拟对端发来一个字节,再把DUT发送的数据抓出来比对。可以用一个task来按位驱动RX线:

task uart_send_byte(input [7:0] data); integer i; begin rx = 1'b1; #(BIT_TIME); rx = 1'b0; // 起始位 #(BIT_TIME); for (i = 0; i < 8; i = i + 1) begin rx = data[i]; // LSB first #(BIT_TIME); end rx = 1'b1; // 停止位 #(BIT_TIME); end endtask

BIT_TIME要定义成1000000000 / BAUD_RATE纳秒,比如115200波特率对应约8680ns。testbench里先发一个字节给uart_rx,等它输出rx_valid,检查rx_data是否等于原始数据;再把同一份数据送入uart_tx,等tx_ready拉高后检查串行输出。用$display打印比对结果,综合前的功能仿真能做到全自动断言。

仿真时重点看三个信号:rx_negedge是否只在起始位处拉高、cnt是否在每帧开始时清零、rx_data在停止位期间是否稳定。如果rx_data一直在跳变,说明位同步出了问题,回头查起点采样和多数表决逻辑。

5.2 引脚约束与电平匹配:从CST文件到USB转串口

高云Gowin EDA用.cst文件做引脚约束,写法和其他FPGA工具不太一样。UART需要两条物理引脚,一个进一个出,再加上地线共用。CST文件里把顶层端口映射到具体封装管脚:

IO_LOC "tx" 14; IO_LOC "rx" 15; IO_PORT "rx" PULL_MODE=UP;

约束本身不复杂,但有一个坑:默认情况下FPGA的IO不上拉,UART空闲高电平要求RX引脚必须有上拉。很多板卡已经在外部加了上拉电阻,如果原理图上没有,就必须在约束里加PULL_MODE=UP,否则接收端会一直检测到低电平,把噪声当成起始位。GW2A-LV18PG256C8的内核电压是1.2V,IO Bank电压通常按板卡配置为3.3V或1.8V,设计时先查原理图确认Bank供电,再把USB转串口模块的电平匹配上。FT232R、CH340这类模块大多是3.3V/5V兼容输出,如果FPGA Bank是1.8V,需要加电平转换电路,不能直接接线。

5.3 串口调试中的几个典型症状与排查方向

现象可能原因排查动作
完全无数据TX/RX接反交叉连接,FPGA的TX接USB模块的RX
全是乱码波特率或帧格式不匹配核对上位机8N1配置和BAUD_DIV
9600正常,4800无数据分频参数取整误差或上位机未重连重新生成分频系数,确认串口重新打开
偶尔丢字节上游逻辑在rx_valid后没及时取走数据加FIFO或延长握手信号
上电后立刻收到0x00RX线上无上拉检查CST或外部上拉电阻

“9600能通信,4800没有数据”这类问题特别典型。很多人改了上位机波特率,但FPGA里的BAUD_DIV是固定数值,没有同步修改,两边位时间不一致自然收不到。这类问题用示波器看TX波形最直观,数一下低电平宽度是不是和设置波特率对应的位宽一致。USB转串口驱动装不上的时候,先用设备管理器确认虚拟串口号,FPGA这边通常不需要驱动干预,只要链路正常就能在串口助手看到回显。

6. 自动波特率检测:把UART接收端做成不挑上位机的自适应模块

很多调试场景中,上位机软件已经固化了波特率,又不想每次手动去改FPGA的分频参数。自动波特率检测的思路是:让上位机先发一个校准字节,比如0x55(二进制01010101),这个字节的每一位都会发生跳变,接收端用计数器测量相邻跳变之间的宽度,就能反推出位周期。

校准字节要用标准串口格式发送,起始位后8个数据位是0、1交替,停止位为高,整帧会形成规律的方波。接收端检测到起始位下降沿后启动计数器,在后续任一次数据的上升沿或下降沿停止计数,得到的就是一个位时间对应的时钟周期数。这个测量值直接写入接收和发送模块的BAUD_DIV寄存器,之后所有收发按新分频系数运行。

reg [15:0] measure_cnt; reg [15:0] baud_div_measured; reg measuring; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin measuring <= 1'b0; measure_cnt <= 16'd0; baud_div_measured <= 16'd0; end else if (rx_negedge) begin measuring <= 1'b1; measure_cnt <= 16'd0; end else if (measuring) begin if (rx_posedge || rx_negedge) begin // 校准字节的任一跳变 baud_div_measured <= measure_cnt + 1'b1; measuring <= 1'b0; end else measure_cnt <= measure_cnt + 1'b1; end end

逻辑说明:起始位下降沿作为测量起点,之后遇到第一个跳变(可能是LSB的上升沿或下降沿)就停止,计数器的值就是一个位周期。baud_div_measured可以直接作为后续和发送模块的BAUD_DIV动态输入。需要注意,校准字节只在链路建立时发送一次,测量完成后状态机切换到正常数据接收模式。rx_posedgerx_negedge分别由同步后的RX信号边沿检测产生,不能直接使用原始引脚电平,否则会因毛刺产生错误测量。

用这个方法,同一套Verilog HDL代码可以自适应300到921600波特率,前提是系统时钟能提供足够的计数分辨率,位周期至少要有几十个时钟周期。时钟频率越低,测量误差越大,比如27MHz下921600波特率每个位周期只有约29个时钟周期,测量值误差一个周期相当于3.4%,勉强可用;115200及以下则非常从容。自适应波特率模块做好后,配合前面收发模块的握手和电平匹配,GW2A-LV18PG256C8上的串口通信就不再依赖人工改参数,直接把二进制码流和上位机对齐。

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

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

农业大数据平台搭建实战:从Hadoop部署到精准农业应用落地

去年帮一个农业示范园区做土壤墒情监测项目&#xff0c;设备装完第一周我就意识到一个问题&#xff1a;几千个传感器每隔十分钟上报一条数据&#xff0c;加上园区里的气象站、虫情测报灯和无人机巡田影像&#xff0c;一个月攒下来的数据&#xff0c;公司原有的MySQL已经拖不动了…

作者头像 李华
网站建设 2026/9/10 7:27:02

神经网络就是多维数组运算:从张量到维度变换的底层解析

神经网络这个名词&#xff0c;很多人第一反应是"调包、调参、炼丹"。但真正把网络跑明白的人&#xff0c;都会有一个共识&#xff1a;所谓神经网络&#xff0c;本质上就是一堆多维数组在来回做运算。我见过太多初学者卡在"为什么输入要 reshape 成 (batch, seq,…

作者头像 李华
网站建设 2026/9/10 7:26:52

基于Hadoop的电商数据分析系统:从环境搭建到离线数仓实战

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

作者头像 李华
网站建设 2026/9/10 7:24:01

微信小程序原创音乐管理系统:全栈开发与论文实战解析

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

作者头像 李华
网站建设 2026/9/10 7:23:19

ESP32-C3 BLE与微信小程序GATT双向通信实战

简介&#xff1a;本资源是一套完整的乐鑫ESP32-C3 BLE与微信小程序双向通信开发源码&#xff0c;面向物联网初学者及嵌入式开发者&#xff0c;解决硬件端BLE外设开发与小程序端低门槛无线交互的集成难题。项目涵盖Arduino框架下的ESP32-C3固件代码&#xff08;.ino/.cpp/.h&…

作者头像 李华