先讲个常见的场景。
做物联网设备的老哥们应该都遇到过这种情况:产品经理丢过来一句“我们要支持远程升级”,然后你打开需求文档一看,里面同时出现了 Ymodem、HTTP、MQTT、DFU 这四个词。第一次接触的人很容易懵——这四个东西到底谁依赖谁?是不是选一个用就行?为什么有的项目折腾半天,最后还是得把 Ymodem 捞回来?
我早年做 STM32 设备的 IAP(在应用编程)时也在这个问题上绕了不少弯路。当时以为 HTTP 比 Ymodem“高级”,一门心思要搞全远程升级,结果真上线了才发现,本地串口升级这扇后门要是没有,光售后就能把人逼疯。这篇文章就把这四个概念掰开揉碎了讲清楚:它们各自是什么、在固件升级这条链路里各自扮演什么角色、为什么成熟的方案里它们经常同时出现。
1. 先说清楚四件事各自是什么
1.1 Ymodem:串口世界的老牌文件搬运工
Ymodem 是一个基于串口的文件传输协议,诞生于上世纪八十年代,属于 Xmodem 的改进版本。它最核心的价值就是两个字:可靠。
只要你在两个设备之间拉了一根串口线(或者虚拟串口),Ymodem 就能把文件从一端搬到另一端。它的工作方式是:发送端把文件切成一个个数据块,每个数据块默认 128 字节(也可以协商成 1024 字节),接收端每收到一块都要回一个 ACK 确认帧,发送端只有在收到确认之后才发下一个数据块。块里面还带序号、数据、CRC16 校验码,任何一个包出错,双方都能立刻发现并重传。
这套机制怎么说呢,放在今天看确实不够花哨,但它把“点对点、一对一、不经过任何中转”这件事做到了极致。裸机上只要有一个 UART 外设、一点 Flash 空间,就能把 Ymodem 跑起来。不需要操作系统,不需要 TCP/IP 协议栈,不需要任何云端的配合。
在嵌入式固件升级的语境里,Ymodem 最常见的落点是 IAP Bootloader 里的串口下载功能。比如你在 STM32 上做了一个 Bootloader,上电后通过串口和 PC 端 SecureCRT 或 Ymodem 工具握手,把新的 App 固件传进芯片指定的 Flash 分区里。很多小批量设备、样机调试、产线烧录,至今还在用这套方案,因为它几乎没有失败的可能,出问题也好排查。
1.2 HTTP:互联网上最通用的“去门口取快递”
HTTP 是应用层协议里的大众脸,大家每天刷网页、调接口都在跟它打交道。它的模型是典型的客户端-服务器:客户端发一个请求,服务器返回一个响应。在固件升级场景下,设备作为客户端,向服务器请求“我要下载固件包”,服务器返回固件文件的内容,就这么简单。
HTTP 之所以在 OTA 升级里被广泛使用,核心原因是它的基础设施太成熟了。CDN 加速、Range 断点续传、HTTPS 加密、静态文件服务,这些能力都是现成的,不需要你自己从零造轮子。固件文件一般少则几十 KB,多则几十 MB,用 HTTP 下载是最顺手的路径。
但它有一个典型的问题:HTTP 是“拉”的模式。设备必须主动去问服务器“有没有新固件”,如果服务器想通知十万台设备“你们该升级了”,它没法直接推给设备,只能等设备自己来轮询。所以你会发现,在真正落地的 OTA 系统里,HTTP 很少单独承担所有工作,它只是负责“取货”的那一段。
另外要说一句,HTTP 不是只有 GET 下载固件这一种用途。设备上报日志、上报版本号、查询升级策略,都可以走 HTTP 接口。它本身是通用协议,只不过在升级链路里最常干的是传输固件二进制流这个活。
1.3 MQTT:为弱网而生的“消息交换机”
MQTT 是专为物联网设计的轻量级消息协议,它的核心模型是发布/订阅(Pub/Sub),不是 HTTP 那种一问一答。设备可以连接到一个 MQTT Broker(消息代理服务器),订阅某个主题,然后任何客户端往这个主题发消息,订阅了它的设备都能收到。
这个模型带来了两个关键特性:
第一是异步推送。服务器可以主动向设备下发消息,不用设备反复轮询。你在云端发一条“开始升级”的指令,设备秒级收到,这对升级体验的提升非常大。
第二是协议开销极低。MQTT 的报文头非常紧凑,一个控制报文可能只有几个字节,非常适合 NB-IoT、2G/3G、卫星通信这类带宽小、不稳定、流量贵的网络。
MQTT 里还有个容易被人忽略的细节是 QoS(服务质量)等级:
- QoS 0:发了就不管,最多一次,效率最高但可能丢
- QoS 1:至少一次,保证送达但可能重复
- QoS 2:恰好一次,最可靠但开销最大
在升级指令下发这个场景里,大多数人会选 QoS 1。为什么?因为指令本身很小,就算重复收到一次,设备端做一个幂等判断就行,但绝不能丢。丢了就意味着设备永远不知道有新固件,用户体验直接崩坏。
不过要说清楚,MQTT 的设计目标从来不是传大文件。虽然理论上可以通过 MQTT 把固件包切成小块发出去,但实际项目中极少有人这么干。因为 MQTT Broker 对单条消息大小通常有限制,而且消息经过 Broker 转发,没有 CDN 那种下载加速能力,传一个几 MB 的固件又慢又占资源,纯属自找麻烦。
1.4 DFU:固件更新的“总导演”而非某种具体协议
DFU 全称是 Device Firmware Update,直译就是设备固件升级。它不是一个像 Ymodem、HTTP、MQTT 那样可以立刻说出“它在哪一层、用什么端口跑”的协议,而是一整套机制的总称。
比如说,你听到“STM32 的 DFU 模式”,那指的是芯片出厂 BootROM 里的一段程序,通过 USB 或者串口接收固件并写入 Flash;你听到“iOS 的 DFU 模式”,指的是 iPhone 在系统无法启动时进入的一种恢复状态;你听到“OTA 升级方案”,那也是一套 DFU 流程。
所以更准确的理解方式是:DFU 是一个目标或者框架,它规定了“固件从哪里来、暂存在哪里、怎么校验、怎么写入、失败怎么回滚”,而 Ymodem、HTTP、MQTT 这些是具体实现这个框架的工具和通道。
一个完整的 DFU 机制通常包含这几个环节:
- 升级通知:设备怎么知道有新固件可用
- 固件获取:新固件的二进制数据通过什么通道进入设备
- 固件存储:数据先放到哪里(通常是独立的下载分区)
- 固件校验:完整性、合法性检查(CRC、签名、版本号)
- 固件激活:从旧版本切换到新版本(可能是全量替换,也可能是 A/B 双备份)
- 失败回滚:升级失败后能不能自动退回旧版本
记住 DFU 是这整个流程的总称,你就不会再把 Ymodem 和 DFU 当成二选一的选项来考虑了。
2. 这四个东西到底怎么串成一条线
2.1 为什么没人只靠一个协议走天下
很多人会问:既然 HTTP 能下载固件,那设备定期去服务器拉取不就行了?既然 MQTT 能推送消息,那直接用 MQTT 推送升级指令不好吗?为什么要把四个概念全塞进来?
答案是:通信这件事,在工程上从来不是“一种协议包打天下”,而是“每个环节选最合适的工具”。
用生活里的例子类比一下。你在一家公司上班,公司通知你“明天去新办公楼开会”用的是内部通讯软件(这就好比 MQTT 下发的升级指令);你到了楼下,坐电梯上 18 层(这就好比 HTTP 从服务器拉取固件);如果大楼停电了,你还可以走应急楼梯(这就好比 Ymodem 通过串口兜底);而整个“让你从旧办公地点迁移到新办公地点”的过程,就是一套搬迁方案(这就是 DFU)。每个环节用的工具完全不同,但少了任何一个,体验都可能出问题。
嵌入式设备固件升级也是一样。MQTT 干不了大文件传输的活,HTTP 干不了服务端主动推送的活,Ymodem 干不了远程的活,而 DFU 这个框架把前三者的角色都安排得明明白白。
2.2 一条真实设备上的升级消息流
我以一个典型的联网设备为例:STM32 主控 + ESP8266 WiFi 模组(或者 4G 模组都可以),设备已经接入了云端 MQTT Broker,固件包放在 HTTP 文件服务器上。
正常的远程升级流程是这样的:
设备上电后,先正常连接 WiFi,然后启动 MQTT 客户端连接 Broker,订阅一个自己的专属主题,比如device/{device_id}/ota/command。云端如果要给这台设备升级,就往这个主题发布一条 JSON 指令:
{ "cmd": "start_ota", "version": "2.1.0", "url": "https://ota.example.com/firmware/device_2.1.0.bin", "md5": "5d41402abc4b2a76b9719d911017c592", "size": 1048576 }设备收到这条指令后,先做几个判断:这个版本号是不是比当前版本新?这个设备型号是不是匹配?如果都满足,就开始走 DFU 流程的第一步——获取固件。设备解析出 url 字段,然后用 HTTP 客户端去下载这个 bin 文件,下载过程中校验大小和 MD5。
下载完成后,固件先写入 Flash 的下载分区(不是直接覆盖当前正在运行的 App),全部写完校验通过后,置一个“待升级”标志位,然后执行软复位。Bootloader 在启动时发现这个标志位,就把新固件从下载分区拷贝到 App 分区,再做一次 CRC 校验,最后跳转执行新固件。如果校验失败,Bootloader 就回滚到旧 App 继续跑。
整个流程里,MQTT 负责的是“通知”,HTTP 负责的是“搬运”,DFU 负责的是“落地执行”,三个角色缺一不可。
2.3 Ymodem在这个架构里还有什么存在意义
这时候你可能要问:既然远程链路已经通了,Ymodem 是不是可以退休了?
我的答案是:不能。至少在产品生命周期的多个阶段,Ymodem 依然是救命稻草。
首先是产线烧录阶段。你总不能要求产线的工人在烧录时也搭一套 MQTT+HTTP 环境吧?板子刚从 SMT 贴片出来,连 WiFi 天线都没焊,这时候最快的烧录方式就是电脑通过 Ymodem 走串口把 Bootloader + App 一次性灌进去。
然后是售后和现场调试阶段。设备出厂后如果因为网络配置错误连不上网,或者升级过程中断电导致两个分区都坏了,远程通道已经失效,唯一的恢复路径就是通过串口 Ymodem 重新烧录。很多有经验的公司会在设备上预留一个调试串口座,壳子都不封死,就是这个原因。
所以在成熟的工程方案里,Ymodem 不是被 HTTP 或 MQTT 替代掉的,它是作为本地兜底通道和远程升级通道长期共存。DFU 框架里应该同时兼容“本地升级”和“远程升级”两条路径,按需触发。
3. 从零搭一套可落地的多通道 DFU 方案
3.1 明确三个分区和一个标志位
DFU 落地的时候,首先要设计 Flash 分区。以常见的 1MB Flash 单片机为例,我一般这样切:
| 分区 | 起始地址 | 大小 | 作用 |
|---|---|---|---|
| Bootloader | 0x08000000 | 64KB | 启动、校验、升级入口 |
| App | 0x08010000 | 768KB | 主应用固件 |
| Download | 0x080D0000 | 128KB | 远程下载的固件缓存 |
| Flag | 0x080FF000 | 4KB | 升级标志与状态记录 |
分区划分的原则是:Bootloader 和 App 必须完全独立,Download 分区要能容纳最大固件包,Flag 区最好单独占一个扇区,方便独立擦写,避免频繁操作影响 App 代码区。
标志位里通常记录这几类信息:当前 App 版本号、待升级版本号、下载状态(0=空闲,1=下载完成待校验,2=校验通过待激活)、升级失败次数。这些状态在 Bootloader 和 App 之间需要共享,通常是定义成一个结构体,在编译时固定放入 Flag 分区地址,Bootloader 也能直接访问这个地址。
3.2 Bootloader 里同时实现 Ymodem 和 App 跳转逻辑
Bootloader 是 DFU 的核心执行者。最简单的一种设计是:上电后 Bootloader 先检查升级标志,如果标志为“待激活”,就校验 Download 分区里的固件;校验通过则拷贝到 App 分区,跳转执行;校验失败则清除标志,直接跳转当前 App。
如果升级标志为空,Bootloader 还要做一件很重要的事:短暂的窗口等待,用来接收本地 Ymodem 升级命令。这个等待窗口通常设置为几百毫秒到 2 秒。为什么要等?因为产线或者售后工程师可能要在设备上电瞬间通过串口主动触发升级。如果不等,每次上电都瞬间跳转 App,本地烧录就没机会介入了。
Ymodem 的实现一般嵌入在 Bootloader 里。接收端初始化两块缓冲区,一块用于接收 128 字节的小包,一块用于接收 1024 字节的大包。实际项目中,我建议直接支持 1024 字节包,因为大包传输时 CRC 校验次数少,整体速度比 128 字节模式快不少,特别是在几十 KB 固件的时候,体感差异很明显。
关键细节是:Ymodem 的起始协商阶段,发送方会先发送一个文件名的信息包(128 字节),里面带着文件名和文件大小(ASCII 字符串),接收端不要忽略它,解析出文件大小可以提前判断固件是否放得下,避免传输到一半才报 Flash 不足。
3.3 App 里同时集成 MQTT 与 HTTP 客户端
App 端的任务相对更复杂一点。它既要维护 MQTT 连接接收指令,又要在需要时通过 HTTP 拉取固件。
MQTT 客户端这里有一个经验:不要把它和业务逻辑耦合得太死。专门开一个任务线程处理 MQTT 收发,收到指令后解析 JSON,把所有消息统一放到一个队列里,主业务逻辑从队列里读。这样即使 Broker 偶尔抖动重连,也不会把业务线程卡死。
订阅主题的设计我推荐分成两条:
- 设备专属指令:
device/{device_id}/ota/cmd - 设备状态上报:
device/{device_id}/ota/status
前者是云端往设备推指令,后者是设备主动上报“下载中”“下载完成”“升级成功”“升级失败”等状态。MQTT 的遗嘱消息(LWT)也建议顺手配置一下,设成offline,这样云端可以及时感知设备掉线。
HTTP 下载固件时,最好实现 Range 断点续传。比如下载到 60% 时网络断了,重连后不要从头开始,而是发一个带Range: bytes=629145-的请求,从断点继续下。别觉得 MCU 上跑 HTTP 不用管这些,4G 模组在移动网络下下载大文件,断开重连是常态,这个功能能省下大量流量和时间。
下载到 Download 分区的过程中,建议边下载边算 MD5。全量算完再校验当然可以,但如果边下载边累加计算,最后直接比对报文里的 md5 字段,整个过程会流畅很多,不需要额外的时间。
3.4 激活与回滚的几种策略
下载完成之后,是不是立刻复位激活?这里有个讲究。如果你的设备正在执行关键业务(比如正在处理一个写 Flash 的敏感操作,或者正在上传一批需要实时的数据),贸然复位会造成数据丢失。
稳妥的做法是:收到升级指令后,先下载固件,然后置“待激活”标志,等设备进入空闲状态(比如晚上低峰期,或者业务线程主动报告空闲),再执行复位升级。如果设备连的是电池供电的低功耗设备,还得检查当前电量,电量过低时应该拒绝复位激活,防止升级到一半断电变砖。
回滚策略方面,如果你 Flash 容量够大,强烈建议用 A/B 双备份方案:App 分区切成 A 和 B 两份,当前运行 A,新固件写入 B,下次启动直接从 B 启动,如果运行失败(看门狗长时间没人喂)自动切回 A。如果 Flash 有限,就只能用“Download 缓存 + 拷贝 + 校验”这种简化方案,但这种方案升级失败后旧固件可能已经被覆盖,只能靠 Ymodem 重新烧录兜底。
4. 选型对比:什么时候用哪个
| 维度 | Ymodem | HTTP | MQTT | DFU |
|---|---|---|---|---|
| 通信模型 | 点对点串口 | 请求/响应 | 发布/订阅 | 流程框架 |
| 传输对象 | 本地文件 | 任意数据/文件 | 小消息 | 固件更新全过程 |
| 典型网络 | 串口/USB虚拟串口 | TCP/IP 互联网 | TCP/弱网 | 不限定 |
| 可靠性机制 | ACK+CRC 重传 | TCP + 可选断点续传 | QoS 0/1/2 | 分区校验/回滚 |
| 带宽开销 | 低(串口速度决定) | 高(HTTP 头部较大) | 极低(头部极小) | 取决于所选通道 |
| 嵌入式资源要求 | 极低,裸机可跑 | 需要 TCP/IP 协议栈 | 需要 TCP/IP 协议栈 | 依赖 Flash 规划 |
| 典型使用时机 | 产线烧录、售后恢复 | 远程下载固件文件 | 下发升级指令、状态上报 | 所有环节的总调度 |
这张表你只看三行就够了:Ymodem 用在本地点对点,HTTP 用在大文件远程下载,MQTT 用在小指令远程下发,而 DFU 是整合这三者的作战计划。
有一种组合是很多团队早期会走弯路的,就是想全部用 MQTT 完成。设备订阅主题,服务器直接把固件分片推过来,听着挺优雅,但实际部署时问题很多:一是 Broker 负载压力大,十万设备同时推分片,服务器很容易被打崩;二是断点续传做得再细,也不如 HTTP 的 Range 机制成熟;三是 MQTT 报文多用于即时消息,固件二进制块容易被 Broker 当作普通消息处理,产生额外的序列化开销。所以我个人的建议是,如果没有非常特殊的原因,远程大文件传输一律走 HTTP,MQTT 专注做指令和状态。
5. 实践里踩过的坑和排查思路
5.1 串口传输到一半卡死
Ymodem 传文件时最常见的故障是传了几十个包之后发送端或接收端就互相干等,进度条不动了。排查思路很固定:先确认两边波特率是否一致,很多人用了 115200 但有一端配了 9600,看起来能握手,实际传几块就错乱;然后检查流控,如果用了 RTS/CTS 硬件流控,但线没接对,也会出现类似问题;最后看接收端的缓冲区—如果接收端是单缓冲区,处理速度跟不上串口接收速度,丢包重传几次可能就死锁了。
个人习惯是 Ymodem 接收端用双缓冲加定时器超时机制,任何一包超过 1 秒没收到就主动发送取消帧,重新进入等待状态,而不是闷头死等。
5.2 MQTT 指令到了但设备没反应
这种问题十有八九不是协议问题,而是主题或者 QoS 的问题。先确认设备订阅的主题和云端发布的主题是否完全一致,MQTT 主题是区分大小写的,Device/123/OTA和device/123/ota完全是两个主题。再看 QoS,如果云端用了 QoS 0 发布,而设备当时正好网络抖动,消息丢了也就丢了,所以在升级指令场景必须用 QoS 1。
另外还要排查设备端 MQTT 的重连机制。我见过很多项目,设备连接断开后没有自动重连逻辑,或者重连后没有重新订阅主题,导致云端发布指令时设备虽然在线,但它根本没在听。每次重连成功后重新订阅是必须做的一步。
5.3 HTTP 下载校验失败
HTTP 下载固件后 MD5 对不上,最常见的原因有两个。一是在下载过程中 Find 到 Flash 写入的拆分逻辑出了问题,比如写入偏移算错了,导致中间少了一块数据,这种问题通常不是每次必现,可能与网络分包错位有关。二是服务器返回了错误页面而不是固件二进制,比如 URL 过期、服务器返回了 403,而设备端没有检查 HTTP 状态码,直接把 HTML 错误页当作固件写入 Flash 了。
所以 HTTP 下载客户端在写入 Flash 前,一定要先确认三点:HTTP 状态码是 200(或者 206 表示断点续传)、Content-Length 和指令下发的 size 一致、下载完成后的 MD5 一致。任何一步不满足,都不要置“待激活”标志,直接删掉缓存区重新来。
5.4 升级成功之后反复重启
如果新固件启动后立马崩溃,看门狗一直复位,设备就会在“Bootloader 尝试启动新固件 → 新固件崩溃 → 看门狗复位”之间无限循环。排查方法是在 Bootloader 里加一个启动计数:每次启动新固件前,把计数器加一并写入 Flag 区,如果新固件运行正常超过 1 分钟(或者主动上报心跳成功),就清零计数器。一旦下次启动发现计数器已经超过 3 次而新固件还没稳定运行,就自动回滚旧固件。
这个机制实现成本很低,但能大幅减少远程升级造成的返修,强烈建议做进去。
最后再分享一个小技巧。你在把 HTTPS、MQTT、Ymodem 这些能力全部塞进固件时,编译产物体积会膨胀得很厉害,Flash 规划时要留足余量。我见过不少团队把 Bootloader 功能做得很丰满,结果 Bootloader 本身占了 128KB,App 分区缩水严重,新功能都塞不进去。所以划分分区前,先用编译后的 map 文件看一眼各部分体积,再结合目标固件的增长预期来定大小,别拍脑袋决定。
四者说到底是分工协作的关系,Ymodem 稳、HTTP 快、MQTT 灵、DFU 统,搞明白了每个工具适合干什么,再回头去看你那套 OTA 需求,心里自然就有底了。