一张图看懂设备端架构:MCU → 协议栈 → 云平台的完整数据链路
专栏第 2 篇|上一篇讲了为什么 MQTT、OTA、低功耗缺一不可。这一篇把「全局图」画出来——在你动手写任何一行代码之前,先搞清楚数据是怎么从传感器一路走到云端数据库的。这张图会贯穿整个专栏。

先问一个问题
你有没有遇到过这种情况:功能单个测试都没问题,一集成到项目里就出各种奇怪的 Bug?
原因通常不是代码写错了,而是对整体数据链路没有清晰的认知,不知道自己的代码在整个系统里处于哪个位置,不知道数据进来之前经过了什么处理,出去之后又到了哪里。
这篇文章的目的,就是把这张全局图画清楚。
架构总览:三层模型
物联网设备端的架构,可以清晰地分成三层:

设备层是你能直接触摸到的硬件:MCU 运行你的固件,传感器采集物理世界的数据,通信模组负责把数据发出去。
协议与安全层是这个专栏的核心——MQTT 决定数据怎么传,TLS 决定传输过程是否安全,OTA 通道决定固件怎么更新。这一层是纯软件,跑在 MCU 上,却决定了整个系统的可靠性上限。
云平台层是数据的终点。MQTT Broker 负责接收和转发消息,业务服务做规则处理和告警,最终数据写入时序数据库,通过 App 或大屏呈现给用户。
深入每一层
设备层:三个角色,各司其职
MCU 主控是大脑。它运行 RTOS(通常是 FreeRTOS 或 RT-Thread),上面跑着你的应用逻辑——什么时候采集数据、什么时候上报、收到云端指令怎么执行。MCU 的软件通常分三层:应用逻辑层、RTOS 调度层、硬件驱动层(HAL)。你写的业务代码在最上层,驱动层负责和硬件打交道。
传感器是眼睛和耳朵。温湿度、光照、加速度、气体浓度,传感器把物理世界的信号变成数字信号,通过 I2C 或 SPI 总线传给 MCU。这里有一个常被忽视的问题:传感器数据是原始 ADC 值还是已经换算过的物理量?很多传感器需要你自己做标定和换算,SHT30 温湿度传感器就是如此——读出来的是 16 位原始值,需要用官方公式换算成摄氏度。
通信模组是嘴巴。ESP32 内置 Wi-Fi 和 BLE,SIM7600 提供 4G 连接,BC660 提供 NB-IoT 连接,SX1276 提供 LoRa 连接。模组和 MCU 之间通常通过 AT 命令或 SPI/UART 接口通信。选对模组非常关键——Wi-Fi 功耗高但速度快,NB-IoT 功耗极低但带宽窄,选错了后期改起来代价巨大。
协议与安全层:被严重低估的复杂度
这一层看起来是「中间件」,实际上是最容易出工程问题的地方。
MQTT 协议栈在嵌入式上通常用 Paho MQTT 嵌入式 C 库或 MQTT-C 库。这些库提供了 connect、publish、subscribe 等接口,但如何在网络抖动、设备重启、Broker 过载等情况下保持正确状态,完全是你的责任。后续文章会专门讲这块。
TLS 安全层在 MCU 上的实现通常用 mbedTLS。它负责在 TCP 之上建立加密通道,防止数据被窃听和篡改。TLS 握手会消耗相当的 CPU 时间和内存(mbedTLS 默认配置需要约 60KB RAM),在资源受限的 MCU 上需要仔细裁剪配置。
OTA 通道本质上是一条文件传输通道——云端把固件包通过 MQTT 或 HTTP 推给设备,设备写入备用分区,验证通过后切换启动。这个过程的可靠性设计是后续专题文章的重点。
云平台层:你的代码终点在这里
MQTT Broker 是云端的消息中枢。所有设备连接到 Broker,向指定 Topic 发布消息,Broker 负责路由到订阅了该 Topic 的服务。常用的开源 Broker 有 EMQX 和 Mosquitto,商业托管的有阿里云 IoT、腾讯云 IoT Hub、AWS IoT Core。
业务服务消费 Broker 转发的消息,做规则判断(温度超过阈值就告警)、设备影子维护(记录设备最新上报状态)、控制指令下发(反向通过 MQTT 推消息给设备)。
时序数据库存储设备上报的历史数据。InfluxDB 和 TDengine 是常见选择,比普通关系型数据库在时间序列查询和压缩上有巨大优势。
追踪一条数据的完整旅程
光看架构图还不够直观。我们追踪一个具体的数据点,看它从产生到落库经历了什么。
场景:一台工业传感器节点每 30 秒上报一次温湿度数据,芯片是 ESP32,传感器是 SHT30,云平台用阿里云 IoT。
第一步:采集(MCU 端,< 1ms)
FreeRTOS 的采集任务被定时器唤醒,通过 I2C 读取 SHT30 寄存器:
// 发送测量命令
i2c_write(SHT30_ADDR, 0x2C, 0x06);
vTaskDelay(pdMS_TO_TICKS(15)); // 等待测量完成
// 读取 6 字节原始数据
uint8_t buf[6];
i2c_read(SHT30_ADDR, buf, 6);
// 换算温度和湿度
float temp = –45.0f + 175.0f * ((buf[0] << 8 | buf[1]) / 65535.0f);
float hum = 100.0f * ((buf[3] << 8 | buf[4]) / 65535.0f);
注意第 7 行:SHT30 的换算公式来自数据手册,不是你随便写的。很多工程师照着网上的代码抄,但网上代码未必和你用的传感器型号匹配。养成习惯:传感器驱动必须对照数据手册。
第二步:序列化(MCU 端,< 1ms)
把采集到的数值组装成 JSON,加上设备 ID 和时间戳:
char payload[128];
snprintf(payload, sizeof(payload),
"{\\"device_id\\":\\"%s\\",\\"ts\\":%llu,\\"temp\\":%.1f,\\"hum\\":%.1f,\\"batt\\":%d}",
device_id, // 从 NVS/Flash 读取,绝对不能硬编码
ntp_timestamp(), // NTP 校准后的 Unix 时间戳,不是 RTC 本地时间
temp,
hum,
battery_percent()
);
这里有两个坑,几乎每个新手都会踩:
坑 1:device_id 硬编码。很多人为了省事,把 device_id 写死在代码里,比如 "node_001"。量产 1000 台,全都上报同一个 ID,云端完全无法区分。正确做法是从 Flash(NVS)里读,出厂时烧录,或者用芯片唯一 ID 生成。ESP32 内置了 esp_efuse_mac_get_default() 可以读 6 字节 MAC 地址,可以用它作为设备 ID 的一部分。
坑 2:时间戳用本地 RTC。如果设备没有 NTP 同步,RTC 时间通常不准——上电默认从 2000 年 1 月 1 日开始跑,或者经历了断电漂移。数据进了云端,时序全乱,画出来的曲线是错的。正确做法是连上网之后立刻做 NTP 同步,同步成功之前不上报数据(或者上报时带一个 "ts_valid": false 标记)。
第三步:MQTT 发布(MCU 端,< 5ms)
MQTTMessage msg = {
.qos = QOS1,
.payload = payload,
.payloadlen = strlen(payload)
};
char topic[64];
snprintf(topic, sizeof(topic), "/%s/%s/data", product_key, device_id);
MQTTPublish(&client, topic, &msg);
QoS 1 意味着消息至少送达一次。Broker 会回复 PUBACK,如果在超时时间内没收到 PUBACK,客户端会重发。这个机制保证了数据不丢,但也意味着网络抖动时可能收到重复消息——云端的业务服务需要做幂等处理(用时间戳 + 设备 ID 去重)。
第四步:TLS 加密传输(网络层,握手约 50ms,后续 < 5ms)
这一步对应用层是透明的——你调用 MQTTPublish,底层的 mbedTLS 自动完成加密。但你需要知道它发生了什么:
首次连接时,TLS 握手包括证书交换、密钥协商,大约需要 50~100ms,消耗约 20KB 栈空间和若干 KB 堆内存。握手完成后,后续消息的加解密开销很小(几毫秒量级)。
在低功耗场景,TLS 握手是一个显著的功耗峰值——每次重连都要来一次。这也是为什么「MQTT 保持连接」比「每次上报都重新建连」在功耗上友好得多:前者只握手一次,后者每次都握手。
第五步:Broker 路由(云端,< 1ms)
消息到达 EMQX 或阿里云 IoT 的 Broker,Broker 解析 Topic,找到所有订阅了 /{product_key}/{device_id}/data 模式的订阅者,把消息转发过去。
这里有一个设计决策:Topic 该怎么设计? 上面用的 /{product_key}/{device_id}/data 是阿里云 IoT 的规范格式。如果你自建 Broker,Topic 设计有很大自由度,但要考虑:
- 层级不要超过 4 级(/a/b/c/d),否则规则匹配性能下降
- 设备 ID 放在固定位置,方便通配符订阅(/+/+/data 订阅所有设备的数据)
- 不同类型的消息用不同后缀(/data、/event、/ota、/cmd),便于做权限控制
第六步:业务处理 → 落库(云端,< 10ms)
业务服务拿到消息后:
全程耗时:正常网络环境下,从传感器采集到数据落库,整个链路约 80~200ms。NB-IoT 因为空口延迟大,通常在 500ms~2s 之间。
下行链路:云端指令如何到达设备
上面讲的是「上行」——设备数据上云。反过来,云端如何控制设备?
云端下发一条控制指令(比如修改上报频率为 60 秒),走的是同一条 MQTT 通道,方向相反:
云端业务服务 → MQTT Broker → 设备订阅的 Topic → MCU 解析执行
设备需要在启动时订阅控制 Topic:
// 订阅云端下行指令
MQTTSubscribe(&client, "/{product_key}/{device_id}/cmd", QOS1, cmd_callback);
// 指令处理回调
void cmd_callback(MessageData *data) {
// 解析 JSON 指令
cJSON *root = cJSON_Parse(data->message->payload);
const char *cmd = cJSON_GetObjectItem(root, "cmd")->valuestring;
if (strcmp(cmd, "set_interval") == 0) {
int interval = cJSON_GetObjectItem(root, "value")->valueint;
report_interval_ms = interval * 1000;
nvs_set_i32("report_interval", interval); // 持久化,重启后生效
}
cJSON_Delete(root);
}
这里有一个重要的工程原则:任何云端下发的配置变更,必须持久化到 Flash。否则设备重启后配置丢失,又变回默认值。我见过不止一个项目,运维人员在云端改了设备参数,以为生效了,结果设备因为功耗问题重启了一次,参数全回到默认,运维完全不知道。
OTA 链路:固件升级的数据路径
OTA 走的是独立的数据路径,但触发信号同样经过 MQTT:
云端触发(MQTT 下发升级指令)
↓
设备收到升级指令,提取固件包 URL
↓
设备发起 HTTPS GET 请求,拉取固件包
↓
边下载边写入备用 Flash 分区
↓
写完后校验 SHA256 / 验签
↓
通过 Bootloader 切换启动分区
↓
重启,运行新固件
↓
新固件回报升级成功,更新 OTA 状态
注意固件包通常走 HTTPS 而不是 MQTT 传输——原因是 MQTT 设计上不适合传输大文件(几十到几百 KB 的固件包),而 HTTP 的断点续传、分块传输天然支持大文件。MQTT 只用来传「开始升级」这条指令和「升级成功/失败」的回报。
把架构图「装进脑子」里

到这里,这张全局图应该已经在你脑子里成型了。用一句话概括:
传感器采集数据 → MCU 序列化 → MQTT 发布 → TLS 加密穿越互联网 → Broker 路由 → 业务处理 → 落库存储;云端指令反向走同一条通道下行;固件更新走 MQTT 触发 + HTTPS 传输的独立路径。
这张图的价值,不只是帮你理解数据流向,更重要的是:当系统出问题时,你知道去哪里查。
- 数据不到云端?先查设备侧 MQTT 是否成功 Publish(日志);再查网络侧是否有 TLS 握手错误(抓包);再查 Broker 侧是否有接收记录(Broker 日志)
- 云端下发指令设备没响应?先查设备是否订阅了正确的 Topic;再查 QoS 设置是否匹配;再查设备当时是否在线
- OTA 升级失败?先看固件包 URL 是否可访问;再查 Flash 写入是否报错;再查校验是否通过
这套排查逻辑,需要你对整条链路都有清晰认知。架构图不只是「画给别人看的」,是你自己调试的地图。
专栏接下来怎么走
从下一篇开始,我们进入 MQTT 系列的深水区:
- 第 3 篇:选型避坑:ESP32 vs STM32+模组 vs NB-IoT,不同场景怎么选
- 第 4 篇:MQTT 协议精讲——QoS 0/1/2 背后的工程权衡,不是文档翻译
- 第 5 篇:ESP32 + MQTT 完整实战——从 Wi-Fi 连接到第一条消息上云,附完整可运行代码
- 第 6 篇:断线重连的正确姿势——指数退避 + Last Will + 会话恢复
每一篇都会回到这张架构图,告诉你当前讲的内容处于哪个位置。全局认知是局部深挖的前提。
下一篇:[选型避坑:ESP32 vs STM32+模组 vs NB-IoT,不同场景怎么选] 上一篇:[物联网设备端工程师的核心能力地图:MQTT、OTA、低功耗为什么缺一不可]
作者:15 年嵌入式软件工程师,专注物联网设备端开发 专栏:嵌入式物联网工程实战:从连接到上云 觉得有收获?点赞收藏支持一下,让更多嵌入式工程师看到这个系列。

![[开源]基于STM32单片机WiFi智能停车场系统 Onene车位管理设计 云平台停车场车位检测 APP智慧停车场HK-010-171主机测评](https://www.171host.com/wp-content/uploads/2026/09/20260921150408-6ab1476821e49-220x150.jpg)

