第四十四章:基于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,解析模块才知道"可以开始收下一包了"——这就是包与包之间的握手。

程序架构:六模块流水线

图44.4.2 UART数据收发实验系统框图

  • 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)

跳转逻辑的关键点:

  1. st_head:uart_rx_done 有效时判断收到的是不是 8'h55,不是则输出 parse_result=8'hE0 直接跳到 st_rx_end;
  2. st_cmd:识别四种命令;注意 8'h03(查询命令)没有数据字段,直接跳到 st_check,其余命令进 st_len;
  3. st_len:收到 8'd0 说明没有数据,报 8'hE2;
  4. st_data:用 data_cnt 按 data_len 计数收数据,同时把数据累加进 checksum;
  5. st_check:接收的校验字节与计算的 checksum 相等则输出 parse_done,否则报 8'hE3;
  6. 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.5.4 上位机发送命令包与接收返回信息

图片对照

  • 硬件:图 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(收发结果)。

初学者容易踩的坑

  1. 校验和忘了累加包头和数据长度:只加数据字节算出的校验必然对不上,规则是"包头+命令+长度+数据全加取低 8 位"。
  2. 查询命令也去收数据:8'h03 命令后面没有数据字段,状态机必须从 st_cmd 直接跳 st_check,否则会傻等数据导致整包解析卡死。
  3. 用 ASCII 模式发送:串口助手必须勾选"十六进制发送/接收",发"55000101 57"字符串是没用的。
  4. 忘记 packet_tx_done 握手:应答发完不拉高这个信号,解析模块永远停在 st_rx_end,第二包就收不到了。
  5. 呼吸灯频率参数设为 0:cnt_us_max = breath_fre × 25 = 0 会导致计数条件永假,呼吸灯卡死。协议层面应限制数据2 的值在 1~5。
  6. 一次发多包:应答机制是"一问一答",上位机连发多包时开发板逐包应答会互相干扰,调试时先手动单包验证。

小结 & 下一步

本章完成了从"字节通信"到"协议通信"的跨越:五字段包格式、累加校验和、六状态解析状态机、错误码反馈、一问一答握手。packet_decode 的状态机框架可以直接改造成任何二进制协议的解析器。通用模块(uart_rx/uart_tx)的复用、命令执行与协议解析的解耦,也是工程化思维的示范。

下一章是全书最后的技术实验:音频环回。我们将驱动 ES8388 音频编解码芯片,通过 I2S 总线实现"输入什么音乐、输出什么音乐"的实时回放。