拓冰建站拓冰建站
首页 / 资讯中心 / 正文

FPGA串口接收模块Verilog实现:UART状态机与上板调试全解析

简介西科大FPGA实验四串口接收模块是面向西安科技大学FPGA课程设计的一份实践代码包适合正在学习串口通信与数字系统设计的本科生及自学者。资源围绕串口接收功能展开涵盖顶层模块、接收控制、波特率生成、LED输出等核心子模块帮助读者掌握波特率、起始位、停止位等串口协议要素以及基于Verilog的模块划分与仿真验证思路。压缩包共84个文件以.v源文件、.sdb综合数据、.html报告、.log日志及.cmd命令脚本为主整体仅168KB轻量小巧。其中包含综合、仿真、工程配置等多种类型文件便于对照学习Xilinx ISE工具链的完整设计流程。已有439人学习浏览适合FPGA入门者参考代码注释并逐步完善自己的串口接收设计。1. 串口接收在 FPGA 上的落地这个工程到底解决了什么很多人在 FPGA 入门阶段会遇到一个坎数码管和流水灯都跑通了但一到串口就卡住。不是代码写不出来而是对「异步采样」这件事缺乏直观认识——发送端和接收端没有共享时钟你凭什么知道每一位从哪里开始、在哪里采样西科大这个 FPGA 实验四串口接收模块正好把这个问题拆开揉碎了摆在桌面上。它用五个 Verilog 文件从波特率生成、起始位检测、数据位采样到停止位校验完整实现了一个 8 位 UART 接收链路再通过 LED 把收到的数据直接显示出来。你不仅能跑通仿真还能用串口助手往板子上发数据、在 LED 上看到真实的二进制结果。这个工程的第二个价值在于它的产物完整性。压缩包里不只是源码还有 ISE 14.7 的完整工程文件 chuankou.gise、综合报告 top_module_summary.html、仿真日志 isim.log、可执行的测试平台 top_tb_isim_beh.exe。换句话说这是一份「从工程创建到上板调试的全程留痕」你能从报告里看到资源占用、时序约束结果从日志里看到综合和仿真的每一个步骤。对正在做课程设计的学生或者刚转 FPGA 开发的工程师来说这种带完整工程链路的参考项目比孤立贴一段代码有用得多——它告诉你一个真实项目的文件组织长什么样、哪些产物是中间文件可以忽略、哪些文件才是核心逻辑。2. 拆解工程文件从 chuankou.gise 到顶层模块的映射关系2.1 ISE 工程里这些文件到底谁是谁拿到压缩包先别急着打开代码。整个工程留下的文件大致分四类工程描述文件、HDL 源码、综合仿真中间产物、报告文档。搞清楚每类文件的作用你以后排查问题会快很多。以 chuankou.gise 为入口。这个文件记录的是工程的完整配置目标芯片型号、综合工具版本、引脚约束、时钟频率等信息。在 ISE 里双击它就能恢复整个工程Verilog 源码和约束文件会自动加载回项目树。工程目录下的 top_module.xst 是综合选项配置文件里面定义了目标器件系列、优化策略、层次结构保留选项top_module.syr 是综合策略记录这两者决定了综合引擎的行为。删掉它们工程还能重新生成但里面的自定义选项也会丢失团队协作时这两个文件要一并提交到版本库。_xmsgs和xst.xmsgs是 Xilinx 的消息文件记录综合过程中的警告和错误信息。很多时候你的代码明明能仿真但上板后行为不对回头翻 xst.xmsgs 会看到类似 Signal is incomplete 这样的警告这往往意味着某个信号没有在所有分支中被赋值综合器推断出了锁存器。养成综合后翻消息文件的习惯可以提前暴露大部分时序隐患。2.2 核心源码文件的模块划分工程里有六个 Verilog 文件按照数据流方向整理如下文件模块职责输入要点输出要点rx_module.v顶层集成系统时钟、复位、rx 串行输入8 位并行数据、状态指示rx_bps_module.v波特率时钟生成系统时钟、使能信号一位宽度的采样脉冲rx_control_module.vUART 接收控制核心波特率脉冲、串行输入移位后的并行数据detect_module.v起始位下降沿检测串行输入、系统时钟接收启动标志led_out.v数据输出显示并行数据LED 位输出从文件组织方式能看出这是典型的分层设计思路。rx_module.v 作为顶层只做实例化不写任何逻辑细节rx_bps_module 和 rx_control_module 把「计时」和「采数」彻底分开这样改波特率时只需要动一个模块接收逻辑完全不受影响。2.3 顶层模块的端口契约与实例化方式打开 rx_module.v结构应该是这样的module rx_module( input clk, // 系统时钟 50MHz input rst_n, // 低电平复位 input rx, // 串行数据输入 output [7:0] data_out, // 接收到的并行数据 output rx_done // 一帧接收完成指示 ); wire bps_pulse; wire rx_start; wire [7:0] rx_data; // 起始位检测 detect_module u_detect( .clk(clk), .rst_n(rst_n), .rx(rx), .start_flag(rx_start) ); // 波特率脉冲生成 rx_bps_module u_bps( .clk(clk), .rst_n(rst_n), .en(rx_start), .bps_pulse(bps_pulse) ); // 接收控制核心 rx_control_module u_rx_ctrl( .clk(clk), .rst_n(rst_n), .rx(rx), .bps_pulse(bps_pulse), .data_out(rx_data), .rx_done(rx_done) ); // LED 显示 led_out u_led( .data_in(rx_data), .led(data_out) ); endmodule顶层模块的设计有一个值得注意的细节detect_module 的 start_flag 同时送给 rx_bps_module 和 rx_control_module。前者用它开启波特率计数后者用它把状态机从 IDLE 迁移到采样状态。这种「一个标志位驱动两个子模块」的协调方式避免了两个模块各自判断起始位导致的状态竞争。实际测试中如果把 start_flag 分发做成组合逻辑而不是寄存器输出时序分析时很容易出现建立时间违例。3. 波特率生成rx_bps_module 的计数原理与边界参数3.1 为什么需要独立的波特率脉冲UART 接收端的核心挑战在于接收器和发送器各自运行在不同的时钟域。发送端按自己的时钟把数据一位一位发出来接收端不知道每一位的边界在哪里、宽度是多少。解决办法是约定一个共同的速率参数——波特率即每秒传输的比特数。接收端用这个速率对串行线反复采样从数据流中恢复出原始信息。但 FPGA 的系统时钟通常远高于波特率。以 50MHz 系统时钟、9600 波特率为例一个数据位的时长约为 10416 个时钟周期。你不能直接把系统时钟分频成 9600Hz 的方波去采样因为一个数据位要持续 10416 个周期方波的上升沿只能提供一个采样点而你可能想要在每一位的中间位置采样。更常见的方法是生成一个「位脉冲」——每隔 10416 个时钟周期产生一个周期为一个系统时钟宽度的脉冲这个脉冲指示「此刻是某一位的中心采样时刻」。3.2 计数器的参数计算与代码实现rx_bps_module.v 的核心逻辑如下module rx_bps_module( input clk, input rst_n, input en, output reg bps_pulse ); parameter CLK_FREQ 50_000_000; // 系统时钟 50MHz parameter BAUD_RATE 9600; // 波特率 localparam CNT_MAX CLK_FREQ / BAUD_RATE - 1; // 5207 reg [12:0] cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cnt 13d0; bps_pulse 1b0; end else if (en) begin if (cnt CNT_MAX) begin cnt 13d0; bps_pulse 1b1; // 输出一个周期宽度的脉冲 end else begin cnt cnt 1b1; bps_pulse 1b0; end end else begin cnt 13d0; bps_pulse 1b0; end end endmodule参数说明CNT_MAX CLK_FREQ / BAUD_RATE - 1计数到 5207 时恰好经过 5208 个时钟周期即 104.16 微秒对应 9600 波特率的一个数据位宽度。en信号控制计数器的启动与停止。接收空闲时计数器归零检测到起始位后才开始计数这样能确保采样点从起始位下降沿之后精确对齐。bps_pulse是一个单时钟周期宽度的脉冲输出频率等于波特率。它不是方波不能直接驱动任何外设只作为接收状态机的采样节拍信号。修改波特率时只需要改变BAUD_RATE参数的值。115200 波特率时CNT_MAX变为 433但要注意50000000 / 115200 434.027取整后的误差约为 0.006%这个误差在十几帧数据内累积下来不会导致采样点漂移到位边界附近工程上可以接受。3.3 计数器位宽选择的陷阱reg [12:0] cnt这一行看似随意其实是计算过的。CNT_MAX最大需要表示 5207二进制需要 13 位。如果你粗心写成了 12 位计数器会在溢出后归零导致波特率变成实际设定值的一半接收端采到的数据全乱。常见的做法是在模块里加一行仿真断言initial begin if (CNT_MAX (1 13) - 1) $display(ERROR: counter width too small); end这行代码在仿真时立即检查参数是否越界避免综合后才发现问题。上板调试时如果收不到正确数据先用$display或者在线逻辑分析仪看看 bps_pulse 的频率是否符合预期这往往能直接定位问题。4. 接收状态机起始位检测、数据位采样与停止位校验的硬件实现4.1 接收时序的四个阶段UART 的一帧数据由 1 位起始位、8 位数据位、1 位停止位组成。空闲状态时串行线保持高电平。接收端持续监视 rx 线一旦检测到由高到低的跳变就认为发送端开始发送起始位。之后每隔一个波特率周期采样一次数据线连续采样 8 次得到数据位最后验证停止位是否为高电平。起始位检测看起来简单实际有坑。如果 rx 线上有毛刺或者数据位中的某个 0 被误判为起始位后续采样就会全部错位。detect_module.v 里通常的做法是检测到下降沿后不立即确认而是延迟半个位周期再采一次样。如果此时 rx 确实是低电平才确认是真正的起始位否则视为毛刺丢弃。这个「两次确认」机制牺牲了一定的响应速度换来了抗干扰能力。4.2 rx_control_module 的完整状态机实现module rx_control_module( input clk, input rst_n, input rx, input bps_pulse, output reg [7:0] data_out, output reg rx_done ); localparam IDLE 3d0; localparam START 3d1; localparam DATA 3d2; localparam STOP 3d3; reg [2:0] state; reg [3:0] bit_cnt; reg [7:0] shift_reg; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; bit_cnt 4d0; shift_reg 8d0; data_out 8d0; rx_done 1b0; end else begin rx_done 1b0; // 默认拉低保证 rx_done 只有一个周期高电平 case (state) IDLE: begin if (!rx) begin state START; // 检测到下降沿进入起始位确认 end end START: begin if (bps_pulse) begin state DATA; bit_cnt 4d0; end end DATA: begin if (bps_pulse) begin // 在每一位的中心采样 shift_reg[bit_cnt] rx; bit_cnt bit_cnt 1b1; if (bit_cnt 4d7) begin state STOP; bit_cnt 4d0; end end end STOP: begin if (bps_pulse) begin if (rx 1b1) begin // 停止位正确输出数据 data_out shift_reg; rx_done 1b1; state IDLE; end else begin // 停止位错误帧错误标志这里简化为回到 IDLE state IDLE; end end end default: state IDLE; endcase end end endmodule状态机的跳转逻辑基于 bps_pulse 进行保证了每个状态停留的时间恰好是一个数据位宽度。有几个细节值得展开采样点的选取。在 DATA 状态中shift_reg[bit_cnt] rx在 bps_pulse 的上升沿执行。由于 bps_pulse 在每个数据位的中心时刻产生采样点正好位于数据位的中央理论上距离前后两个位边界各 50% 的位宽对抗信号上升沿的抖动和线缆延迟有足够的余量。如果采样点靠近位边界很小的相位偏移就会导致采样错误。位计数器溢出条件。bit_cnt从 0 计数到 7在第 7 个脉冲到来时完成最后一个数据位的采样同时跳转到 STOP 状态不等到 bit_cnt 溢出。这里用4d7作为比较值而不是4d8是因为计数器的值从 0 开始采完第 8 位数据bit_cnt 7后立即做状态切换下一次的 bps_pulse 出现在停止位中心恰好可以采样停止位。如果把比较条件写成bit_cnt 4d8会导致在 DATA 状态多停留一位时间时序整体延后停止位采样错位。rx_done 脉冲宽度。rx_done 1b0;放在 always 块的开头是一种常见的脉冲生成方式。它保证 rx_done 在绝大多数时钟周期保持低电平仅当停止位校验通过的时钟沿拉高一个周期。接收完成标志被外部模块用做数据锁存信号时这种单周期脉冲恰好满足触发条件。4.3 外设模块的配合逻辑led_out.v 的作用是把接收到的并行数据直接映射到 LED 引脚。它内部逻辑很简单module led_out( input [7:0] data_in, output reg [7:0] led ); always (*) begin led ~data_in; // 低电平点亮取反使逻辑 1 对应 LED 亮 end endmodule~data_in这个非阻塞赋值其实可以去掉直接连线但保留它意味着可以在这里加入驱动增强或电平转换。不同开发板的 LED 点亮电平不一样有的高电平点亮有的低电平点亮严格来说这个模块应该加一个参数控制极性不过实验场景下用取反操作适配大多数板子已经够用。4.4 帧错误处理的工程考量上面代码里停止位为低电平时只是简单地回到 IDLE没有对外暴露错误标志。实际工程中帧错误往往意味着发送端和接收端的波特率不匹配或者线路干扰严重统计帧错误率能帮助判断链路状态。常见做法是增加一个frame_err输出信号在 STOP 状态检测到停止位错误时拉高一个周期配合 rx_done 一起上报给上层模块。5. 仿真验证到上板调试从 ISIM 波形到 LED 的完整链路检查5.1 从测试平台看仿真环境的搭建方式压缩包里的 top_tb.v 是测试平台文件top_tb_isim_beh.exe 是已经编译好的仿真可执行文件。如果你从零开始搭仿真环境测试平台的黄金法则是不要对被测模块的内部信号做任何初始化。你只负责提供时钟和复位激励信号要模拟真实外部条件。一个能用的测试平台大概长这样module top_tb; reg clk; reg rst_n; reg rx; wire [7:0] data_out; wire rx_done; // 生成 50MHz 时钟 initial begin clk 0; forever #10 clk ~clk; end // 激励序列模拟发送数据 0xA5 initial begin rst_n 0; rx 1; #100; rst_n 1; #100; // 起始位拉低一个位时间9600 波特率 ≈ 104160ns rx 0; #104160; // 数据位 LSB first0xA5 1010_0101 rx 1; #104160; rx 0; #104160; rx 1; #104160; rx 0; #104160; rx 0; #104160; rx 1; #104160; rx 0; #104160; rx 1; #104160; // 停止位 rx 1; #104160; #1000; $finish; end endmodule测试平台里的时间参数和 rx_bps_module 里的波特率参数是同一组数字的两次书写。如果波特率参数调整了而测试平台的延时没跟上仿真结果会出错而且错误表现得很隐晦——数据位错位、bit_cnt 与期望不符。所以写测试平台时建议用宏定义统一管理时间参数define BIT_TIME 104_160 // 9600 波特率下每一位的纳秒数直接在测试平台里用#(BIT_TIME) 写延时修改起来不会漏。5.2 验证停止位校验和 rx_done 时序的方法一个容易被忽略的仿真场景是停止位错误。发送端在停止位阶段拉低 rx相当于制造帧错误接收端应该不产生 rx_done数据保持上一次的值。在测试平台里补一段激励// 发送一帧停止位错误的数据 rx 0; #(BIT_TIME); // 起始位 rx 1; #(BIT_TIME); // bit0 rx 0; #(BIT_TIME); // bit1 // ... 其余数据位 ... rx 0; #(BIT_TIME); // 停止位错误 // 预期rx_done 保持低电平data_out 不更新对比波形图里 rx_done 和 data_out 的变化可以一次性验证接收状态机的完整行为。ISE 的 ISIM 仿真器里在波形窗口中展开 rx_control 模块的 state 信号把显示格式改成字符串映射可以更直观地看到状态机的跳转过程。5.3 上板调试的串口回环验证法仿真通过后上板测试建议分两步走。第一步把数据从 FPGA 原样发回主机。在顶层再实例化一个发送模块每当接收完成一次就立即把收到的数据原封不动发送回去。在 PC 端打开串口助手发送十六进制字节如果回显内容和发送内容一致说明接收链路的数据采样、位同步、字节输出全部正常问题不可能在接收方向。第二步验证数据的持续接收能力。让 PC 以 1ms 间隔连续发送一组递增数据检查 FPGA 侧是否有数据丢失。丢失通常有两种原因一是接收缓冲没有做满标志处理上一帧数据还没来得及被外部逻辑读走就被覆盖二是 rx_done 作为锁存信号存在竞争冒险导致取数时刻不稳定。常见做法是增加一个 FIFO 或者简单的握手寄存器让 rx_done 信号触发读操作而不是靠电平保持。5.4 常见问题对照表故障现象可能原因排查手段接收数据全部为 0xFFrx 输入悬空或方向错误检查顶层端口映射和引脚约束文件数据偶发错位波特率参数不匹配用示波器或逻辑分析仪测量 bps_pulse 频率第一帧正确后续全错起始位使能信号未及时复位检查 detect_module 的 start_flag 是否在帧结束后拉低LED 亮度异常驱动电流不足或极性反了对照开发板原理图检查 led_out.v 的取反逻辑仿真正常但上板不工作输入引脚没有添加 IBUF 约束或时序约束不足检查 UCF 文件中的 IOSTANDARD 和 PERIOD 约束第三个问题最容易踩。detect_module 检测到起始位后拉高 start_flag 用于开启波特率计数但如果这一帧数据接收完成后 start_flag 仍然保持高电平rx_bps_module 的计数器不会停止下一帧数据到来时采样点整体提前了一段不确定的时间导致后续所有数据位采样偏移。确保 start_flag 在 STOP 状态结束后被清零可以在接收状态机的 IDLE 分支中显式将 start_flag 拉低或者用 start_flag 和 bps_pulse 的组合逻辑生成计数使能而不是直接用 start_flag。5.5 用 ISIM 命令行脚本复现仿真结果压缩包里的 isim.cmd 是 ISIM 仿真器的命令脚本通过它可以在命令行批量化运行仿真适合回归测试。脚本内容大致是run 1 ms quit在 ISE 的 Tcl 控制台里执行source isim.cmd或者直接在命令行输入fuse top_tb -o top_tb_isim_beh.exe ./top_tb_isim_beh.exe就能重新编译并运行仿真。这个方式比每次打开 ISIM 图形界面要快得多尤其在修改波特率参数后连续跑多组配置对比时很有用。ISIM 生成的 isim_usage_statistics.html 里记录着仿真运行时长、内存占用和波形数据量这些数据可以帮助判断仿真环境是否正常——如果你发现仿真耗时异常增长先检查测试平台里是不是有死循环或者过长的forever语句。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门