第四十四章:基于UART的数据包收发实验
本文对应原书第1145页起,第五篇「实战篇」。
本章在讲什么?(先看这个)
第三十一章的串口通信只能一次收发一个字节,实际系统里我们要传的是"命令+参数"这样的结构化信息。本章在 UART 之上定义了一套数据包协议,实现上位机(串口调试助手)与开发板的"对话式"通信:
- 上位机发送控制包 → 开发板解析执行 → 返回应答包;
- 支持控制 LED、蜂鸣器、呼吸灯,以及查询按键状态;
- 解析出错时返回错误码,上位机可以据此修正重发。
这个"协议收发"模式是所有通信系统(Modbus、蓝牙 AT 指令、网络协议栈)的雏形,学完本章你就能设计自己的应用层协议了。
核心概念拆解
协议格式:五字段数据包
控制类数据包由五部分组成:
| 包头(1B) | 命令(1B) | 数据长度N(1B) | 数据(N字节) | 校验(1B) |
- 包头:固定 8'h55,用于字节流中定位一包的开始;
- 命令:8'h00 控制 LED、8'h01 控制蜂鸣器、8'h02 控制呼吸灯、8'h03 查询按键;
- 数据长度:后面数据字段的字节数;
- 校验和(Checksum):包头 + 命令 + 数据长度 + 所有数据累加,取低 8 位。
校验和规则举例:点亮 LED0 的包是 55 00 01 01 57——0x55+0x00+0x01+0x01 = 0x57,正好是最后那个校验字节。接收方用同样的累加对比,不一致就判为坏包。
查询类与控制类的区别
控制类包携带控制信息(数据字段),查询类包不带数据,只有 包头+命令+校验 三字节(如查询按键就是 55 03 58)。但查询类的应答包要带数据:开发板返回 55 03 01 数据 校验,数据字节里用 bit4 表示 KEY0、bit0 表示 KEY1(如 8'h10=KEY0 按下、8'h01=KEY1 按下、8'h11=无按键、8'h00=两个都按下)。
错误码与反馈机制
一个可靠的协议必须有"回话"机制。开发板解析出错时返回 包头+错误码+校验:
| 错误码 | 含义 | 应答包 |
|---|---|---|
| 8'hE0 | 包头不是 8'h55 | 55 E0 35 |
| 8'hE1 | 未定义的命令 | 55 E1 36 |
| 8'hE2 | 数据长度为 0 | 55 E2 37 |
| 8'hE3 | 校验和错误 | 55 E3 38 |
解析正确时返回 包头+命令+校验(如控制 LED 正确应答 55 00 55)。发送完成后拉高 packet_tx_done,解析模块才知道"可以开始收下一包了"——这就是包与包之间的握手。
程序架构:六模块流水线

- uart_rx / uart_tx:直接复用串口通信实验的通用模块;
- packet_decode(数据包解析):核心模块;
- cmd_exe(命令执行):执行命令控制外设;
- packet_code(数据包封装):打包应答并逐字节发送;
- 顶层 top_uart_packet 例化以上模块。
数据包解析:六状态机
解析是按字段逐个进行的,天然适合状态机。六个状态:
st_head(6'b00_0001) → st_cmd(00_0010) → st_len(00_0100) → st_data(00_1000) → st_check(01_0000) → st_rx_end(10_0000)
跳转逻辑的关键点:
- st_head:uart_rx_done 有效时判断收到的是不是 8'h55,不是则输出 parse_result=8'hE0 直接跳到 st_rx_end;
- st_cmd:识别四种命令;注意 8'h03(查询命令)没有数据字段,直接跳到 st_check,其余命令进 st_len;
- st_len:收到 8'd0 说明没有数据,报 8'hE2;
- st_data:用 data_cnt 按 data_len 计数收数据,同时把数据累加进 checksum;
- st_check:接收的校验字节与计算的 checksum 相等则输出 parse_done,否则报 8'hE3;
- st_rx_end:等 packet_tx_done 拉高后回到 st_head,开始下一包。
st_check: begin // 校验
if (uart_rx_done) begin
if (checksum == uart_rx_data) begin
skip_en <= 1'b1;
parse_done <= 1'b1; // 校验正确,接收完成
end
else begin
parse_result <= ERR_CHECKSUM; // 校验错误
parse_done <= 1'b1;
end
end
end
解析成功后按命令分发:LED 命令把 rec_data 赋给 led_data(led_data <= {rec_data[4], rec_data[0]}),蜂鸣器取 rec_data[0],呼吸灯取数据1(开关)和数据2(频率)。
命令执行:cmd_exe 模块
LED 和蜂鸣器就是两行 assign 直连;呼吸灯复用前面实验的 breath_led 模块,只做了两处修改:
- 增加 breath_sw 开关输入,与复位信号相与:
assign rst_n = sys_rst_n & breath_sw;(开关关掉即整模块复位停止); - 呼吸频率参数化:
assign cnt_us_max = breath_fre * 25;——breath_fre 单位是秒,换算成每个亮度阶梯的时长(建议设置 1~5 秒效果最佳)。
数据包封装:packet_code 模块
把应答打包成 40 位寄存器 uart_packet_data(最多 5 字节),逐字节发送:
- 包长判断:查询按键的应答是 5 字节,其余是 3 字节;
- 校验和在打包时现算:
uart_packet_data[39:32] <= PACKET_HEAD + parse_cmd + 8'h01 + 按键数据; - 逐字节发送的节拍:检测 uart_tx_busy 的下降沿(neg_tx_busy)表示一个字节发完,data_cnt 加一,直到计满 packet_len 后拉高 packet_tx_done。
assign neg_tx_busy = tx_busy_d1 & (~tx_busy_d0); // 发送忙信号下降沿 = 一字节发完
下载验证
引脚约束:sys_clk=U18、复位=N16、uart_rxd=T19、uart_txd=J15、LED0/1=H15/L15、蜂鸣器=M14、呼吸灯=J16、按键=L14/K16。上板时用跳线帽连接 P5 排针,串口助手设置 115200-8-N-1 并勾选十六进制收发,发送 55 00 01 01 57,应看到 PL_LED0 点亮且收到应答 55 00 55。

图片对照
- 硬件:图 44.2.1(硬件实物图),以及与串口实验共用的 USB 转串口电路图
/images/zynq/ch27/ch27_p0636_00702_65d0dfdb99c93b3b.png。 - 架构:图 44.4.1(系统架构图)、图 44.4.2(系统框图)。
- 解析模块:图 44.4.3(框图)、图 44.4.4(状态跳转图,六个状态怎么转全靠它)、图 44.4.5(解析波形图,以点亮 LED0 的包为例逐步对照)。
- 执行与封装:图 44.4.6(命令执行框图)、图 44.4.7(封装框图)、图 44.4.8(封装模块波形)。
- 仿真:图 44.4.10(控制 LED0 仿真)、图 44.4.11/44.4.12(呼吸灯仿真及代码修改)、图 44.4.13(查询按键)、图 44.4.14(校验错误 8'hE3 的仿真)。
- 上板:图 44.5.1(IO Planning)、图 44.5.2(串口与 P5 跳线帽连接)、图 44.5.3(串口设置)、图 44.5.4(收发结果)。
初学者容易踩的坑
- 校验和忘了累加包头和数据长度:只加数据字节算出的校验必然对不上,规则是"包头+命令+长度+数据全加取低 8 位"。
- 查询命令也去收数据:8'h03 命令后面没有数据字段,状态机必须从 st_cmd 直接跳 st_check,否则会傻等数据导致整包解析卡死。
- 用 ASCII 模式发送:串口助手必须勾选"十六进制发送/接收",发"55000101 57"字符串是没用的。
- 忘记 packet_tx_done 握手:应答发完不拉高这个信号,解析模块永远停在 st_rx_end,第二包就收不到了。
- 呼吸灯频率参数设为 0:cnt_us_max = breath_fre × 25 = 0 会导致计数条件永假,呼吸灯卡死。协议层面应限制数据2 的值在 1~5。
- 一次发多包:应答机制是"一问一答",上位机连发多包时开发板逐包应答会互相干扰,调试时先手动单包验证。
小结 & 下一步
本章完成了从"字节通信"到"协议通信"的跨越:五字段包格式、累加校验和、六状态解析状态机、错误码反馈、一问一答握手。packet_decode 的状态机框架可以直接改造成任何二进制协议的解析器。通用模块(uart_rx/uart_tx)的复用、命令执行与协议解析的解耦,也是工程化思维的示范。
下一章是全书最后的技术实验:音频环回。我们将驱动 ES8388 音频编解码芯片,通过 I2S 总线实现"输入什么音乐、输出什么音乐"的实时回放。