Skip to content

LV045-IAP串口接收与帧处理 ​

一、 总体设计 ​

1. 三条约束 ​

收帧逻辑同时被三件事约束,最终形态基本是被它们逼出来的:

  • 中断里不能干重活:115200 下每 87µs 就可能来一个字节,128KB 载荷的 CRC32 需要一百多毫秒,放中断必然丢字节;
  • 内部 RAM 装不下:64KB 内部 SRAM 放不下 128KB 的 flash-app 镜像,缓冲只能落外部 SRAM;
  • 两种镜像共用一套协议:flash-app 与 sram-app 的帧格式相同,只有头部地址字段不同,落盘目标由地址决定。

2. 五个阶段、两个上下文 ​

IAP 收帧状态机

状态机只有五个状态,前四个跑在中断里,最后一个跑在主循环:

阶段上下文做的事
IAP_ST_SCAN中断8 字节滑窗匹配魔数,前面混进杂字节也能对齐
IAP_ST_HEADER中断再收 12 字节:长度、CRC32、目标地址
IAP_ST_PAYLOAD中断逐字节写入外部 SRAM 的载荷缓冲
IAP_ST_TRAILER中断逐字节比对 IAPEOF02
IAP_ST_VERIFY主循环算 CRC32、上报事件、由业务层落盘

中断与主循环之间没有锁,只有一个"待校验"标志 verify_req:中断把它置 1,主循环看到后执行校验并把状态机推回 SCAN。这样两边永远只碰自己那一半状态。

3. 载荷缓冲为什么钉在外部 SRAM ​

c
// atk-f103/Drivers/BSP/Iap/iap.c
static uint8_t s_payload[IAP_PAYLOAD_MAX] __attribute__((at(externalSRAM_BASE)));

IAP_PAYLOAD_MAX 是 128KB,内部 SRAM 只有 64KB,只能用 __attribute__((at())) 把数组钉在 FSMC 的 0x68000000。代价是一条硬前提:MX_FSMC_Init() 必须先执行,否则任何一次读写都落在没使能的 FSMC 上,直接 HardFault。Bootloader 的初始化顺序里,FSMC 排在所有 IAP 操作之前,正是因为这个。

二、 帧的类型与"收完"的判定 ​

1. 同一条串口上的两类数据 ​

串口上跑着两种完全不同的东西,它们共用字节流,却用两套语法:

数据类型识别方式结束标志处理者
文本命令首单词整词匹配命令表,大小写不敏感\r\nusart1_cmd_poll()
二进制镜像帧8 字节魔数 IAPBIN02长度 + 尾部魔数 + CRC32iap_rx_poll()

两者互斥的规则只有一条:匹配到魔数时挂起命令识别(usart1_cmd_set_enabled(0)),帧结束或失败后恢复。这样载荷里即使凑出 go 字样也不会被当成命令。

镜像帧内部还分两种"子类型",但帧里没有类型字段——类型由目标地址隐含:

目标加载地址帧的类型落盘方式
0x08020000FLASH APP就地编程
0x20007000SRAM APP写暂存区,等 go sram
其它非法直接判 IAP_ERR_ADDR

这样做的好处是协议最小:上位机不需要额外告诉设备"这是哪种镜像",只要把链接地址填进头部即可;坏处是地址填错时只能靠设备端校验兜住,所以地址判定必须严格——只认两个基址,不做"就近对齐"这种猜测。

2. 四个字段各自管什么 ​

字段回答的问题缺了会怎样
魔数 IAPBIN02这段字节是不是一帧镜像?无法从任意字节流里自同步,文本里出现数字就会被误判
长度 N载荷有多长?何时该看尾部?只能靠"静默 3 秒"猜结束,慢链路上必然误判
目标地址装到哪一块?容量上限多少?无法区分 Flash 版与 SRAM 版,也不知道该按 128KB 还是 24KB 校验
CRC32内容有没有被改坏?链路误码、发错文件都发现不了,坏镜像会直接落盘
尾部 IAPEOF02是不是被截断了?长度对但少发 8 字节时,会被当成合法帧

帧头刻意没有"命令字""序号""时间戳""镜像类型"这些字段:设备端只需要回答"装哪、装多少、装坏没有",其余留给业务语义(比如 go sram 这条文本命令)。版本信息塞在魔数末两位里,既省字段,又让旧固件在比对魔数时自然失败。

3. "收完"是三道证据,不是一个超时 ​

一帧数据的四道判定

接收结束这件事由三道独立证据共同确认,任何一道缺失都会留下漏洞:

  • 长度告诉状态机"何时从载荷切到尾部"——它是唯一的时间点依据;
  • 尾部魔数确认"确实收完了、没多也没少"——长度字段自己也可能写错;
  • CRC32 确认"内容没被改坏"——这是唯一覆盖载荷全部字节的检查。

帧内超时(3000ms)只做兜底,不参与"成功"的判定:它的职责是把卡在半帧的设备救回扫描态,而不是判断这一帧收没收到。

换个角度理解:如果只靠超时判帧尾,会遇到两个具体问题。一是 115200 下正常字节间隔只有 87µs,但主机偶尔会因为缓冲区调度停顿几十毫秒,静默阈值定小了会误判、定大了会让每次传输都白等;二是 128KB 传完之后还要再等一个阈值才认为结束,白白多耗 3 秒。有了长度字段,收满即切换,超时只用于异常。

这里的"三道"专指判断这一帧收完没有;再加上"目标地址是否合法",就是下一节展开的四道判定——前者决定"能不能用",后者决定"往哪放、放不放得下"。

4. 判定的顺序为什么不能调 ​

四道判定是有依赖关系的,顺序换了要么不安全,要么白花钱:

顺序判定位置为什么必须在这
①长度范围中断(HEADER 解析后)决定后续写缓冲的长度,越界会写穿 s_payload
②目标地址中断(同上)决定落盘目标与容量上限,非法就不该继续收
③尾部魔数中断(收满 N 字节后)先排除截断,避免为不完整的数据白算 CRC
④载荷 CRC32主循环(iap_rx_poll)最贵(128KB 约一百多毫秒),只有通过才允许落盘

顺序总结成一句话:先用便宜的检查挡住不该发生的写入,再用最贵的检查守住落盘那一步。

三、 中断侧实现 ​

1. SCAN:8 字节滑窗 ​

匹配魔数不能假设"帧从第一个字节开始":串口上可能先收到几行日志回显、TCP 上可能先收到一条文本命令。做法是维护一个 8 字节窗口,先填满,之后每来一个字节整体左移一位,窗口内始终是最近 8 个字节,与 IAPBIN02 逐字节比较。

滑窗匹配魔数的过程

实现的成本很低:中断里只需要维护 win_len(窗口已填几个字节)与一个 8 字节数组,每来一个字节做一次 memmove 式的左移和一次最多 8 字节的比较,最坏耗时几微秒,完全放得进 86.8 µs 的字节间隔。

这样做的收益是"自同步":主机中途重启、丢掉半帧、发错文件,接收方都能自己重新对齐,不需要额外的"同步字"协议。代价是滑窗恒为 8 字节比较,比"从固定偏移开始比较"多花一点 CPU,但在这个速率下可以忽略。

2. HEADER:收满 12 字节再解析 ​

魔数命中后并不立刻解析,而是继续收满 12 字节(头部长 20 - 魔数 8)再一次性读出三个字段。原因很实际:长度、CRC、地址都是小端多字节,边收边解析容易写出"高字节还没到就用了"的错误,攒齐再解析只多耗 12 字节的时间。

攒齐的 12 字节放在 s_header 里,与命中的 8 字节魔数拼成完整的 20 字节头部备份。它的用途是排查:失败时不必重放数据,直接看这份头部就知道是长度不对、地址不对还是版本不对。

s_header 下标对应头部偏移字段解析方式
0 ~ 70 ~ 7魔数 IAPBIN02命中时已存入
8 ~ 118 ~ 11载荷长度 N小端拼成 uint32
12 ~ 1512 ~ 15载荷 CRC32小端拼成 uint32
16 ~ 1916 ~ 19目标加载地址小端拼成 uint32

解析后立刻做两道防线:

  • 长度为 0 或超过 IAP_PAYLOAD_MAX → IAP_ERR_TOO_LARGE,不进载荷阶段;
  • 地址必须是两个容器基址之一、且载荷放得下 → 否则 IAP_ERR_ADDR。

这两道防线在中断里做,成本只是几次比较,但能保证后面的 s_payload[index] 永远不会越界——这是中断里唯一值得做"重"判断的地方。

3. PAYLOAD:直写外部 SRAM ​

载荷阶段只有一个数组赋值和一个下标自增,收满 len 字节就切到尾部阶段。这里没有任何"边收边校验",因为 CRC 要等最后一段才算得完。

写入期间外设总线(FSMC)一直在被访问,速度远低于 CPU,但因为每字节只写一次、且写入地址连续,实测不构成瓶颈。

4. TRAILER:逐字节比对 ​

尾部 8 字节只做形式校验:逐字节与 IAPEOF02 比较,任一字节不符就 IAP_ERR_TRAILER。它的价值在于区分"长度字段对了但数据被截断"和"整帧完好"——如果只靠长度,发送方少发 8 字节会被当成合法帧。

5. 超时与回退 ​

半帧卡死是现场最常见的情况(主机取消、串口拔线)。判据放在 iap_rx_poll() 里,只对 HEADER/PAYLOAD/TRAILER 三个阶段生效:

c
// atk-f103/Drivers/BSP/Iap/iap.c
if ((HAL_GetTick() - s_rx.last_tick) > IAP_RX_TIMEOUT_MS)
{
    iap_fail(IAP_ERR_TIMEOUT);
    return IAP_EVT_FRAME_ERR;
}

超时后调 iap_back_to_scan():状态、下标、窗口长度全部归零,并恢复文本命令识别(usart1_cmd_set_enabled(1))。这一步很关键,否则一次失败的传输会让设备再也收不到 go/reboot,只能硬件复位。

四、 主循环侧处理 ​

1. 三种事件 ​

iap_rx_poll() 每 1ms 被调用一次,上报三类事件,业务层按事件做事:

事件触发时机业务层动作
IAP_EVT_HEADER头部解析完成、开始收载荷打印"开始接收",可用来估算耗时
IAP_EVT_FRAME_OKCRC32 校验通过读载荷、按目标地址落盘
IAP_EVT_FRAME_ERR超时、尾部错、CRC 错、地址非法打印具体错误,等待重发

事件的语义是"已经发生",不是"请你去做":FRAME_OK 上报时状态机已经回到 SCAN,载荷缓冲的内容保持不变,等业务层取走。

2. CRC32 为什么必须在主循环 ​

128KB 逐位 CRC 在 72MHz 上约需一百多毫秒。如果放在中断里,这段时间的串口字节会全部错过(ORE),传输必然失败。放到主循环后,中断继续服务后续字节——虽然此时已经在等下次传输,但至少不会因为一次校验把整个接收器搞死。

代价是主循环必须够快地响应 verify_req:Bootloader 主循环是 1ms 一拍,iap_service_task() 每拍都会调 iap_rx_poll(),延迟可以忽略。

3. FLASH APP:写完就地可运行 ​

帧校验通过后的落盘分支

帧头地址是 0x08020000 时,落盘目标就是运行目标:镜像直接写进 Flash 第 2 块,写完不搬动,go flash 只是把控制权交过去。

c
// atk-f103/Drivers/BSP/Iap/iap_service.c
iap_wr_res_t wr = iap_install_image(base, p, iap_rx_payload_len());

if (wr == IAP_WR_OK)
{
    s_last_image_base = base;                    /* 'go' 不带参数时跳这里 */

    printf("IAP: flash image installed at 0x%08X, send 'go flash' to run it\r\n",
           (unsigned int)base);
    iap_tcp_notify("FLASH OK\r\n");              /* 网络回执 */
}

处理要点:

  • 擦除按"载荷覆盖到的页数"算,64 页不会全擦;
  • 写完之后对 Flash 里的副本算一次 CRC32,与外部 SRAM 里的源数据比对,只信回读结果;
  • 成功只打印"已安装",不自动跳转——跳转是 go flash 的职责,避免"刚发完就被切走、看不到回执";
  • 网络通道另外回执 FLASH OK,上位机据此判断可以发下一条命令。

4. SRAM APP:先落暂存区,等 go sram ​

帧头地址是 0x20007000 时,落盘目标不是运行目标:镜像写进 Flash 第 3 块暂存区(0x08040000),等 go sram 再搬进内部 SRAM。

c
// atk-f103/Drivers/BSP/Iap/iap_service.c(同一处分支的另一半)
if (base == IAP_SRAM_APP_BASE)
{
    printf("IAP: sram image stored to flash staging 0x%08X (%u bytes), "
           "send 'go sram' to load it into SRAM and run\r\n",
           (unsigned int)IAP_SRAM_STAGE_BASE, (unsigned int)iap_rx_payload_len());
    iap_tcp_notify("SRAM OK\r\n");
}

处理要点:

  • 容量判据按 SRAM 容器 24KB 算,不是暂存区的 128KB——iap_target_ok() 按容器判,pack_iap.py 打包时也按容器拦;
  • 落盘后 s_last_image_base 也更新成 0x20007000,此时裸发 go 会走 SRAM 路径;
  • 真正运行需要第二次交互(go sram),中间可以断电重启:暂存区在 Flash 里,重启后一条命令就能装载;
  • 失败分支打印 sram staging write failed ... stay in bootloader,网络回执 SRAM ERR。

5. 两条处理路径的对比 ​

对比项FLASH APPSRAM APP
帧头地址0x080200000x20007000
落盘位置就地 0x08020000暂存区 0x08040000
容量判据128KB(第 2 块)24KB(SRAM 容器)
成功提示flash image installed ... 'go flash'stored to flash staging ... 'go sram'
网络回执FLASH OKSRAM OK
是否自动跳转否否
让它跑起来go flashgo sram(先搬运再跳)
掉电重启后直接 go flash 即可暂存区仍在,go sram 即可
写在哪个函数里iap_install_image() 的直写分支同函数改写到 IAP_SRAM_STAGE_BASE

两条分支的擦除、编程、回读校验实现完全共用,差别只在"写到哪个地址、提示哪句话"。回读校验这一道是必须的:它验的是介质,不是链路。

五、 常量与错误定位 ​

1. 关键常量 ​

常量位置取值说明
IAP_PAYLOAD_MAXiap.h128KB单帧载荷上限,也是外部 SRAM 缓冲大小
IAP_HEADER_LENiap.h20头部总长(含 8 字节魔数)
IAP_TRAILER_LENiap.h8尾部魔数长度
IAP_RX_TIMEOUT_MSiap.c3000帧内字节间超时
IAP_MAGIC_LENiap.h8滑窗长度

2. 错误提示与含义 ​

提示错误码通常原因
magic prefix matched but version mismatchIAP_ERR_VERSION固件与上位机帧版本不一致(01 与 02)
payload length invalid or too largeIAP_ERR_TOO_LARGE长度字段为 0 或超过 128KB
target address invalid or image too large for itIAP_ERR_ADDR地址不是两个容器基址,或载荷超出容器
trailer magic mismatchIAP_ERR_TRAILER尾部被截断或多发了字节
CRC32 check failedIAP_ERR_CRC链路误码、发的是别的文件、发送工具没按二进制模式发
flash/sram staging write failed at 0x...IAP_WR_*擦除或编程失败、回读 CRC 与源数据不一致
长时间无反应后恢复提示IAP_ERR_TIMEOUT主机中途取消,超时后自动回到扫描态

3. 串口与 TCP 的差异 ​

维度串口通道TCP 通道
字节入口USART1 中断调 iap_rx_byte()tcp_recv 回调调 iap_rx_byte()
上下文中断主循环(lwip_comm_poll 驱动)
文本命令同一中断并行喂 usart1_cmd_feed()同一回调并行喂 usart1_cmd_feed()
半帧超时3 秒3 秒(另外还有 TCP 连接超时兜底)
切走时机不涉及accept 时摘掉串口消费者,断开时恢复

帧解析代码完全共用,两条通道的差别只在"谁把字节递进来"。


本文档由 markdowncli 技能辅助生成