简介:西科大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.v | UART 接收控制核心 | 波特率脉冲、串行输入 | 移位后的并行数据 |
| 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 <= 13'd0; bps_pulse <= 1'b0; end else if (en) begin if (cnt == CNT_MAX) begin cnt <= 13'd0; bps_pulse <= 1'b1; // 输出一个周期宽度的脉冲 end else begin cnt <= cnt + 1'b1; bps_pulse <= 1'b0; end end else begin cnt <= 13'd0; bps_pulse <= 1'b0; 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 = 3'd0; localparam START = 3'd1; localparam DATA = 3'd2; localparam STOP = 3'd3; 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 <= 4'd0; shift_reg <= 8'd0; data_out <= 8'd0; rx_done <= 1'b0; end else begin rx_done <= 1'b0; // 默认拉低,保证 rx_done 只有一个周期高电平 case (state) IDLE: begin if (!rx) begin state <= START; // 检测到下降沿,进入起始位确认 end end START: begin if (bps_pulse) begin state <= DATA; bit_cnt <= 4'd0; end end DATA: begin if (bps_pulse) begin // 在每一位的中心采样 shift_reg[bit_cnt] <= rx; bit_cnt <= bit_cnt + 1'b1; if (bit_cnt == 4'd7) begin state <= STOP; bit_cnt <= 4'd0; end end end STOP: begin if (bps_pulse) begin if (rx == 1'b1) begin // 停止位正确,输出数据 data_out <= shift_reg; rx_done <= 1'b1; 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 溢出。这里用4'd7作为比较值而不是4'd8,是因为计数器的值从 0 开始,采完第 8 位数据(bit_cnt = 7)后立即做状态切换,下一次的 bps_pulse 出现在停止位中心,恰好可以采样停止位。如果把比较条件写成bit_cnt == 4'd8,会导致在 DATA 状态多停留一位时间,时序整体延后,停止位采样错位。
rx_done 脉冲宽度。rx_done <= 1'b0;放在 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 first:0xA5 = 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 常见问题对照表
| 故障现象 | 可能原因 | 排查手段 |
|---|---|---|
| 接收数据全部为 0xFF | rx 输入悬空或方向错误 | 检查顶层端口映射和引脚约束文件 |
| 数据偶发错位 | 波特率参数不匹配 | 用示波器或逻辑分析仪测量 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语句。
本文还有配套的精品资源,点击获取