ModelSim 仿真记录:基于地址帧 UART 的 FPGA 多外设控制系统
1. 项目概述
本设计完成了一个基于 FPGA 的多外设控制系统,核心功能是通过 UART 串口协议接收控制命令,再根据命令控制开发板上的 LED、蜂鸣器、数码管、电机、8×8 点阵以及 LCD1602 本地菜单模块。
本文主要记录 ModelSim 仿真过程,包括仿真工程搭建、波形观察方法、UART 帧解析方法,以及各模块关键信号的观察思路。Quartus 综合、引脚分配和下载到开发板的部分放在后续文章中。
系统采用地址帧通信方式,串口接收的数据不再是单字节直接控制,而是按照固定格式组成一帧完整命令:
帧头 + 地址 + 命令 + 校验例如:
66 01 11 10其中:
66:帧头,用于标识一帧数据的开始;01:设备地址,用于区分不同从机;11:控制命令,例如控制 LED 进入某种显示模式;10:校验值,用于判断地址和命令是否在传输过程中出错。
2. 开发板与外设资源
本设计使用 QC-FPGA-C1 FPGA 开发板套件。开发板板载 FPGA 芯片、时钟晶振、LED、数码管、蜂鸣器、点阵、按键、LCD1602 接口以及电机驱动接口,适合完成基础数字系统和嵌入式 FPGA 综合实验。
开发板主要资源如下:
- FPGA:Cyclone 系列 FPGA,工程目标器件为
EP1C3T144; - 时钟:板载 48 MHz 有源晶振,原理图中 OSC1 输出
CLK0; - 下载器:USB-Blaster;
- 串口通信:可使用 USB 转串口线,或蓝牙透明串口模块;
- 显示模块:LED、四位七段数码管、8×8 LED 点阵、LCD1602;
- 输入模块:拨码开关、矩阵按键;
- 执行模块:无源蜂鸣器、L9110 电机驱动接口。
在仿真阶段,重点不是观察真实硬件现象,而是验证 RTL 逻辑是否正确。例如 UART 是否能正确接收帧,校验是否有效,命令是否能正确转换为各模块的控制信号。
3. ModelSim 仿真的作用
在 FPGA 开发流程中,ModelSim 主要用于 RTL 仿真。它可以在不下载到开发板的情况下,提前观察代码逻辑是否符合设计要求。
本设计中 ModelSim 主要用于检查以下内容:
- UART 接收器是否能识别起始位、数据位、校验位和停止位;
- 地址帧协议是否能正确识别
66 + 地址 + 命令 + 校验; - 校验正确时是否产生
cmd_valid有效脉冲; - LED、蜂鸣器、数码管、电机、点阵等模块是否根据命令切换模式;
- LCD1602 与按键菜单逻辑是否能独立产生本地控制命令;
- 顶层模块连接是否正确。
ModelSim 仿真通过测试文件 testbench 主动产生时钟、复位和 UART 输入数据,再观察输出信号是否符合预期。
4. 仿真文件结构
工程中通常包含两类文件:
src/ fpga_rs232_top.vhd 顶层模块 uart_rx.vhd UART 接收模块 uart_tx.vhd UART 发送模块 uart_frame_parser.vhd 地址帧解析模块 command_controller.vhd 命令控制模块 led_ctrl.vhd LED 控制模块 buzzer_ctrl.vhd 蜂鸣器控制模块 seg_ctrl.vhd 数码管控制模块 motor_ctrl.vhd 电机控制模块 dot_matrix_ctrl.vhd 点阵控制模块 lcd1602_ctrl.vhd LCD1602 显示模块 key_scan.vhd 矩阵按键扫描模块 sim/ tb_fpga_rs232_top.vhd 顶层仿真文件 run_msim_gui.do ModelSim 自动仿真脚本其中tb_fpga_rs232_top.vhd不会下载到 FPGA 中,它只用于仿真。真正烧入开发板的是src目录下的设计文件。
5. UART 地址帧协议
5.1 串口字节格式
UART 是异步串行通信方式。空闲时,串口线保持高电平;发送一个字节时,先拉低产生起始位,然后从低位到高位依次发送 8 位数据,再发送校验位,最后发送停止位。
本设计采用 8E1 格式:
1 位起始位 + 8 位数据位 + 1 位偶校验位 + 1 位停止位也就是:
Start + D0 D1 D2 D3 D4 D5 D6 D7 + Even Parity + Stop需要注意,UART 的 8 位数据是串行传输的,但接收完成后会在 FPGA 内部形成一个 8 位并行数据rx_data。因此报告中出现“8 位并行数据”并不是说 UART 线上同时传输了 8 根数据线,而是指接收模块完成串并转换后的内部数据。
5.2 地址帧格式
单个 UART 字节只能表示一个数据。为了让通信更可靠,也方便以后扩展多机通信,系统增加了帧头、地址和校验。
本设计使用的帧格式为:
66 ADDR CMD CHECK例如 LED 跑马灯命令:
66 01 11 10校验采用异或方式:
CHECK = ADDR xor CMD所以:
01 xor 11 = 10接收模块只有在帧头、地址和校验都正确时,才会输出有效命令。这样可以避免串口误码或错误数据直接控制外设。
6. ModelSim 仿真基本流程
6.1 打开 ModelSim
进入工程目录后,可以在 ModelSim 中执行.do脚本,也可以手动创建工程并添加文件。为了减少重复操作,推荐使用脚本方式。
常用脚本示例如下:
vlib work vmap work work vcom ../src/uart_rx.vhd vcom ../src/uart_tx.vhd vcom ../src/uart_frame_parser.vhd vcom ../src/command_controller.vhd vcom ../src/led_ctrl.vhd vcom ../src/buzzer_ctrl.vhd vcom ../src/seg_ctrl.vhd vcom ../src/motor_ctrl.vhd vcom ../src/dot_matrix_ctrl.vhd vcom ../src/fpga_rs232_top.vhd vcom tb_fpga_rs232_top.vhd vsim work.tb_fpga_rs232_top如果 Transcript 窗口中显示:
Errors: 0说明语法编译通过,可以继续观察波形。
6.2 添加波形信号
进入仿真后,需要把关键信号加入 Wave 窗口。常见信号如下:
add wave -divider "Clock and Reset" add wave /tb_fpga_rs232_top/clk add wave /tb_fpga_rs232_top/rst_n add wave -divider "UART" add wave /tb_fpga_rs232_top/uart_rxd add wave /tb_fpga_rs232_top/uart_txd add wave /tb_fpga_rs232_top/uut/rx_data add wave /tb_fpga_rs232_top/uut/rx_valid add wave -divider "Frame Parser" add wave /tb_fpga_rs232_top/uut/cmd_data add wave /tb_fpga_rs232_top/uut/cmd_valid add wave /tb_fpga_rs232_top/uut/resp_code add wave -divider "Outputs" add wave /tb_fpga_rs232_top/uut/led_mode add wave /tb_fpga_rs232_top/LedOut add wave /tb_fpga_rs232_top/uut/buzzer_mode add wave /tb_fpga_rs232_top/Bell add wave /tb_fpga_rs232_top/uut/seg_value add wave /tb_fpga_rs232_top/dataout add wave /tb_fpga_rs232_top/en add wave /tb_fpga_rs232_top/uut/dot_mode add wave /tb_fpga_rs232_top/dot_row add wave /tb_fpga_rs232_top/dot_col添加完成后运行:
run 4 ms如果仿真时间太短,可能只能看到 UART 接收过程,看不到 LED、蜂鸣器、点阵等慢速模块的明显变化。对于慢速显示模块,可以适当延长仿真时间,或者在 testbench 中使用加速参数。
7. Wave 窗口观察方法
7.1 波形放大与缩小
ModelSim 的 Wave 窗口中,常用观察方法如下:
- 点击放大镜
+:放大当前时间区域; - 点击放大镜
-:缩小当前时间区域; - 点击
Zoom Full:显示完整仿真时间; - 用鼠标在时间轴上拖选一段区域,再选择
Zoom Range; - 使用黄色 Cursor 定位某一时刻,观察左侧信号当前值。
UART 的每一位持续时间较短,而 LED 和数码管扫描时间相对较长,所以不能用同一个缩放比例观察所有模块。观察 UART 时要放大到微秒级,观察 LED 或蜂鸣器时可以缩小到毫秒级。
7.2 单独显示 Wave 窗口
ModelSim 默认会把 Source、Objects、Wave 等窗口放在同一个界面中。为了截图更清晰,可以把 Wave 窗口单独显示:
- 打开菜单
View -> Wave; - 找到下方或中间的
Wave标签页; - 拖动 Wave 标签到屏幕空白区域,形成独立窗口;
- 或者使用
Window -> Tile Vertically将窗口平铺; - 调整左侧变量名区域宽度,保证变量名完整显示。
截图时建议只保留当前模块相关信号,不要把所有信号全部放在一张图里。这样变量名、数值和波形变化都会更清楚。
7.3 设置信号显示进制
总线信号默认可能显示为二进制,也可能显示为十六进制。观察 UART 命令时建议设置为十六进制。
设置方法:
- 在 Wave 左侧选中信号;
- 右键选择
Radix; - UART 数据、命令码选择
Hexadecimal; - LED、点阵行列可以选择
Binary; - 计数器可以选择
Unsigned。
例如:
rx_data显示为8'h66、8'h01、8'h11;LedOut显示为3'b001、3'b010;dot_row、dot_col显示为二进制更直观。
8. UART 接收波形分析
UART 接收时,重点观察uart_rxd、rx_data和rx_valid。
当 testbench 发送命令:
66 01 11 10波形中可以看到uart_rxd先后出现 4 个字节的串行波形。每个字节都包含起始位、8 位数据位、偶校验位和停止位。
接收完成一个字节后:
rx_data更新为当前收到的字节;rx_valid产生一个时钟周期的高电平;- 帧解析模块继续判断该字节属于帧头、地址、命令还是校验。
图中需要重点观察:
uart_rxd:串口输入线,空闲为高电平;rx_data:接收完成后的 8 位数据;rx_valid:字节接收完成标志;cmd_data:解析出的命令字节;cmd_valid:完整帧校验通过后的命令有效脉冲。
如果只看rx_data,会感觉数据突然变成了8'h66或8'h11。这是正常现象,因为rx_data是串并转换后的结果;真正一位一位传输的过程要看uart_rxd。
9. 地址帧解析波形分析
帧解析模块收到 UART 字节后,会按照状态机依次判断:
等待帧头 -> 接收地址 -> 接收命令 -> 接收校验 -> 输出有效命令以命令:
66 01 11 10为例,解析过程如下:
- 收到
66,认为一帧开始; - 收到地址
01,与本机地址一致; - 收到命令
11,暂存为待执行命令; - 收到校验
10,计算结果正确; - 输出
cmd_valid = 1,同时cmd_data = 8'h11。
cmd_valid一般只保持一个系统时钟周期,它的作用是通知后级控制模块:“当前命令有效,可以执行”。
如果校验错误,cmd_valid不会拉高,外设状态也不会变化。
10. LED 控制波形分析
LED 模块根据led_mode选择不同显示方式。常见模式包括跑马灯、流水灯和呼吸灯。
例如发送:
66 01 11 10解析成功后,led_mode进入对应模式,LedOut开始按照设定节奏变化。
跑马灯模式:
流水灯模式:
呼吸灯模式:
观察 LED 波形时需要注意:
cmd_valid出现后,led_mode才会更新;LedOut不一定马上快速变化,因为 LED 显示通常经过分频;- 如果仿真时间太短,可能只能看到模式切换,看不到完整流水效果;
- 在仿真中可以缩短 LED 分频计数值,使波形更容易观察。
硬件上 48 MHz 时钟非常快,人眼无法直接分辨每个时钟周期,因此 LED 模块必须使用计数器分频。假设某模式计数到N后移动一次,则流水灯频率约为:
f_led = 48 MHz / N实际报告中应根据代码里的计数值计算具体频率。
11. 蜂鸣器控制波形分析
无源蜂鸣器不能直接写入一个固定高低电平来播放不同音调,而是需要 FPGA 输出一定频率的方波。不同频率对应不同音高。
蜂鸣器模块一般包含两类功能:
- 报警模式:输出固定频率的间歇方波;
- 电子琴模式:根据音符命令输出不同频率的方波。
例如进入电子琴模式后,再发送音符命令:
66 01 22 23 66 01 31 30其中22表示进入电子琴模式,31表示播放某个音符。
观察时重点看:
buzzer_mode是否进入对应模式;Bell是否输出周期性方波;- 不同音符命令下,
Bell的周期是否发生变化。
频率计算公式为:
f_bell = 48 MHz / (2 × half_period)其中half_period是蜂鸣器方波半周期计数值。因为一个完整方波包含高电平和低电平两个半周期,所以公式中需要乘以 2。
12. 数码管控制波形分析
四位数码管通常采用动态扫描方式。FPGA 在很短时间内轮流点亮不同位数码管,由于人眼视觉暂留,看起来像四位同时显示。
数码管相关信号一般包括:
seg_value:需要显示的数据;dataout:段选信号;en:位选信号。
数码管段码由 a、b、c、d、e、f、g、dp 等段组成。
例如显示数字0时,一般需要点亮 a、b、c、d、e、f,关闭 g;显示数字1时,只需要点亮 b、c。具体高低电平是否取反,要根据开发板原理图判断数码管是共阳还是共阴。
en是否周期性切换不同位;dataout是否随当前位显示内容改变;- 输入数字命令后,
seg_value是否正确更新。
13. 点阵控制波形分析
8×8 点阵同样采用扫描显示。FPGA 逐行或逐列刷新点阵,通过快速扫描形成完整图案。
点阵相关信号通常包括:
dot_mode:点阵显示模式;dot_row:行选择;dot_col:列数据。
点阵代码中通常会把图案写成 8 个字节,每个字节对应一行或一列。例如某一行数据为:
00111100表示该行中间 4 个 LED 点亮,两侧 LED 熄灭。
如果显示爱心、笑脸等图案,本质上就是把每一行的亮灭状态提前写入 ROM 数组,然后由扫描计数器循环输出。
14. 常见波形问题分析
14.1 为什么波形里只看到8'hA1、8'hA3之类的数据?
这些通常是系统返回码,不是乱码。
例如命令执行成功后,发送模块可能返回:
66 01 A1 A0其中A1表示 LED 命令应答,校验字节仍然由地址和返回码计算得到。
14.2 为什么rx_data看不到每一位变化?
rx_data是接收完成后的并行结果。
如果要看 UART 每一位的传输过程,需要观察uart_rxd;如果要看接收完成后的字节结果,就观察rx_data和rx_valid。
14.3 为什么时钟看起来太密集?
系统时钟为 48 MHz,周期约为:
1 / 48 MHz ≈ 20.83 ns在毫秒级波形窗口里,时钟当然会密成一整条。
观察 UART 或外设状态时,不需要一直盯着clk,只需要确认时钟存在即可。分析重点应放在rx_valid、cmd_valid、cmd_data和外设输出信号上。
14.4 为什么运行很久 LED 也没明显变化?
原因通常有两个:
- LED 分频计数较大,仿真时间不够;
- 多条 UART 命令发送间隔太短,模式刚切换又被下一条命令覆盖。
解决方法:
- 对 LED 单独写 testbench;
- 在仿真参数中缩短分频计数;
- 延长命令之间的等待时间;
- 对目标时间段使用 Wave 缩放观察。
14.5 UART 接收为什么需要很多个时钟?
UART 必须按照波特率采样。
如果系统时钟是 48 MHz,波特率是 9600,则每个 UART bit 需要的系统时钟数约为:
48,000,000 / 9600 = 5000所以“5000 个时钟”不是额外等待,而是 9600 波特率本身对应的采样周期。
如果波特率改为 115200,则每 bit 约为:
48,000,000 / 115200 ≈ 416.7因此波特率越高,每个 bit 对应的系统时钟数越少,通信速度也越快。
15. 仿真结论
通过 ModelSim 仿真可以确认:
- UART 接收模块能够正确识别起始位、数据位、偶校验位和停止位;
- 地址帧协议能够正确解析
66 + 地址 + 命令 + 校验; - 校验正确时,系统能够输出
cmd_valid有效脉冲; - LED、蜂鸣器、数码管、点阵、电机等模块能够根据命令切换工作状态;
- LCD1602 本地菜单模块可以通过按键产生本地控制逻辑;
- ModelSim 波形能够在下载开发板前提前暴露时序、协议和模块连接问题。
ModelSim 仿真的重点不是“看到和实物完全一样的效果”,而是通过波形确认每一级逻辑是否按预期工作。对于 UART 这类串行协议,重点观察起始位、数据位、校验位、停止位,以及接收完成后的rx_data和rx_valid;对于外设模块,重点观察模式寄存器和最终输出信号是否正确变化。
后续进入 Quartus 流程时,可以在 ModelSim 仿真通过的基础上继续完成综合、引脚分配、时序检查和 FPGA 下载。