资讯详情

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

📅 2026/9/10 7:28:56 | 华诺云谱 👁 阅读
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_FREQ和BAUD_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配115200BAUD_DIV234会让实际波特率偏高0.16%这个量级不会导致通信失败但不要在这个误差基础上再叠加多次取整。提示如果你把两个模块分别用BAUD_DIV一半的计数器去产生脉冲就会变成在两个半位处各触发一次状态机会多出一拍。最稳妥的做法是只做一个位宽计数器在计数满时拉一个一周期宽的bit_tick脉冲所有状态机都用这个脉冲驱动。2.3 接收端为什么不能只在数据线上采样刚写UART的人最容易犯的错是看到RX线有跳变就立刻锁存数据。RX线上的毛刺、按键抖动、电平转换芯片的边沿噪声都可能被当成起始位。接收端必须做两级同步和边沿检测先消除跨时钟域的亚稳态风险再识别真正的下降沿。GPIO输入的信号先打两拍同步到系统时钟域然后用rx_d1 !rx_d2检测下降沿。起始位确认后也不是马上采样而要等半个位时间再取一次电平。如果半个位时间后的电平仍然是低说明刚才确实是起始位随后在每个数据位的正中间采样。这就是中点采样它能最大程度避开码间串扰和边沿抖动的区间。3. Verilog HDL发送通路用状态机把UART数据线拉出来3.1 发送状态机的五个状态发送方向比接收简单状态机只需要在正确的时刻把TX引脚置成对应电平。定义五个状态IDLE、START、DATA、STOP、DONE。空闲时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 3d0, START 3d1, DATA 3d2, STOP 3d3; 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 1b1; tx_ready 1b1; cnt 16d0; bit_cnt 4d0; end else begin case (state) IDLE: begin tx 1b1; if (tx_valid) begin shift_reg tx_data; state START; tx_ready 1b0; cnt 16d0; end end START: begin tx 1b0; if (bit_tick) begin state DATA; bit_cnt 4d0; cnt 16d0; end else cnt cnt 1b1; end DATA: begin tx shift_reg[0]; if (bit_tick) begin shift_reg {1b0, shift_reg[DATA_WIDTH-1:1]}; bit_cnt bit_cnt 1b1; cnt 16d0; if (bit_cnt DATA_WIDTH - 1) state STOP; end else cnt cnt 1b1; end STOP: begin tx 1b1; if (bit_tick) begin state IDLE; tx_ready 1b1; cnt 16d0; end else cnt cnt 1b1; 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 3d0, START_CONFIRM 3d1, DATA 3d2, STOP 3d3; 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 3b111; state IDLE; cnt 16d0; bit_cnt 4d0; rx_valid 1b0; end else begin state_rx {state_rx[1:0], rx}; rx_valid 1b0; case (state) IDLE: begin cnt 16d0; if (rx_negedge) begin state START_CONFIRM; cnt 16d0; end end START_CONFIRM: begin if (cnt BAUD_DIV / 2 - 1) begin if (!rx_sync2) begin state DATA; cnt 16d0; bit_cnt 4d0; end else state IDLE; end else cnt cnt 1b1; 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 1b1; cnt 16d0; if (bit_cnt DATA_WIDTH - 1) begin state STOP; rx_data shift_reg; rx_valid 1b1; end end else cnt cnt 1b1; 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_left、sample_mid、sample_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 1b1; #(BIT_TIME); rx 1b0; // 起始位 #(BIT_TIME); for (i 0; i 8; i i 1) begin rx data[i]; // LSB first #(BIT_TIME); end rx 1b1; // 停止位 #(BIT_TIME); end endtaskBIT_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_MODEUP;约束本身不复杂但有一个坑默认情况下FPGA的IO不上拉UART空闲高电平要求RX引脚必须有上拉。很多板卡已经在外部加了上拉电阻如果原理图上没有就必须在约束里加PULL_MODEUP否则接收端会一直检测到低电平把噪声当成起始位。GW2A-LV18PG256C8的内核电压是1.2VIO 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_DIV9600正常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 1b0; measure_cnt 16d0; baud_div_measured 16d0; end else if (rx_negedge) begin measuring 1b1; measure_cnt 16d0; end else if (measuring) begin if (rx_posedge || rx_negedge) begin // 校准字节的任一跳变 baud_div_measured measure_cnt 1b1; measuring 1b0; end else measure_cnt measure_cnt 1b1; end end逻辑说明起始位下降沿作为测量起点之后遇到第一个跳变可能是LSB的上升沿或下降沿就停止计数器的值就是一个位周期。baud_div_measured可以直接作为后续和发送模块的BAUD_DIV动态输入。需要注意校准字节只在链路建立时发送一次测量完成后状态机切换到正常数据接收模式。rx_posedge和rx_negedge分别由同步后的RX信号边沿检测产生不能直接使用原始引脚电平否则会因毛刺产生错误测量。用这个方法同一套Verilog HDL代码可以自适应300到921600波特率前提是系统时钟能提供足够的计数分辨率位周期至少要有几十个时钟周期。时钟频率越低测量误差越大比如27MHz下921600波特率每个位周期只有约29个时钟周期测量值误差一个周期相当于3.4%勉强可用115200及以下则非常从容。自适应波特率模块做好后配合前面收发模块的握手和电平匹配GW2A-LV18PG256C8上的串口通信就不再依赖人工改参数直接把二进制码流和上位机对齐。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。