从板级到云端:空调嵌入式通信协议选型与调试指南 做空调产品开发真正需要花大量时间调通的往往不是压缩机控制或者风机 PWM而是“通信”。从主板上读一个温湿度传感器是 I²C 还是 SPI室内机和室外机之间走什么总线MCU 跟 Wi-Fi 模组之间用什么接口数据到了云端之后用 MQTT 还是 HTTP 上报状态这些问题如果不在方案前期想清楚后面每个联调节点都会反复返工。这篇内容不做代码大全而是把空调开发里从板级到云端经常遇到的嵌入式通信协议按层次理一遍重点给出选型依据、帧格式设计、典型代码示例和排查方法。读完以后你能拿这张协议地图去套自己手上的产品方案先判断当前节点该用哪种总线再看协议帧怎么解析最后知道在云端怎么跟设备来回对齐数据。文章适合四类读者刚进入暖通空调或家电嵌入式方向的学生从通用 MCU 开发转做智能空调的工程师负责室内机、室外机、线控器联调的系统工程师以及要对接云端或者 App 的物联网开发人员。开发层级常见通信方式主要解决问题板级外设通信I²C、SPI、UARTMCU 读传感器、写 EEPROM、驱动显示或 Flash调试与日志UART输出日志、抓取协议帧、打印关键变量设备间通信RS485/Modbus、CAN室内机与室外机、多联机、集中控制器之间交换状态本地模组对接UART/SPI/SDIO、AT 指令MCU 与 Wi-Fi、蓝牙、4G 模组建立连接云端通信MQTT/TLS、HTTPS设备上云、远程控制、状态上报、OTA 升级这条链路并不是每个空调产品都必须走全。有些小功率空调只有遥控和主板不上云有些工程机走 Modbus 接楼宇自控有些家用空调用 Wi-Fi 模组走 App。先识别自己产品属于哪一类再决定协议栈怎么搭。1. 空调开发通信全景从板级到云端看哪些协议先给一张全景图。空调嵌入式开发里的通信可以按“板内—板间—设备间—上云”四个维度来看。板内通信是主控 MCU 与周围芯片、传感器、存储器之间的通信典型的就是距离很短、传输速率不高的 I²C、SPI、UART。比如主板上常用的温湿度传感器是 I²C 接口段码 LCD 或 LED 显示驱动用 I²C 或 SPI字库 Flash 或语音芯片用 SPI 更多。板间和设备间通信稍微复杂。挂壁机室内机与室外机之间很多方案会走一条异步串行通信线多联机系统里的内外机或者集中控制器与多台内机之间往往用 RS485、CAN 甚至私有总线。这里的关键不是协议本身多高深而是通信距离变长、干扰变大以后帧格式、校验、重传和故障隔离都需要纳入设计。本地近端通信则是指空调主板和 Wi-Fi 模组、蓝牙模组之间的连接。绝大多数商用 Wi-Fi 方案会把 TCP/IP、TLS、MQTT 栈放在模组内部跑MCU 只需要通过 UART 发送 AT 指令或 JSON 控制串这样主芯片的 Flash 和 RAM 压力会小很多。云端通信是链路最后一段。设备侧 MCU 把运行状态通过模组协议栈发布到云平台云平台再把控制指令下发到设备。这里要关注的不只是“能不能连上”更重要的是物模型怎么定义、上下行消息如何对齐、离线时怎么处理、OTA 升级怎么做。一张表把典型场景和协议对号入座场景典型参与者常见协议一次传递的数据量空调主板读温湿度MCU 与传感器/EEPROMI²C几个字节高速传输画面/日志MCU 与 Flash/显示SPI几十到几百字节调试日志/线控器UART自定义帧/Modbus RTU几十字节室内外机通信两块独立控制板RS485、私有串行几十字节楼宇集中控制空调与 BAS/能源管理Modbus RTU/BACnet几十字节车载空调控制空调控与整车域控CAN/LIN8 字节左右主控联网MCU 与 Wi-Fi/BLE 模组UARTAT/MQTT几百字节 JSON上云远控模组与云端MQTT/TLSJSON 几百到上千字节空调开发中常见的误区是一张原理图上既有 I²C 又有 SPI就把它们混为一谈。实际上三种板级总线各有优缺点适合的场景非常不一样。2. 板级外设通信选型I²C、SPI、UART 对比很多刚入行的工程师在原理图出来以后才开始看通信协议这个顺序应该反过来。原理图定下来之前优先决定每个外设挂在哪条总线上。维度I²CSPIUART信号线SCL、SDA2 根SCLK、MOSI、MISO、CS4 根起TX、RX2 根时钟由主机产生从机不产生由主机产生从机不产生双方约定波特率无独立时钟线传输方向半双工分时收发全双工可同时收发全双工可同时收发挂载设备数理论上多个地址设备挂一条总线每个设备独立 CS扩展靠 GPIO常规是一对一或靠软件做多路典型速率100kHz/400kHz 常见可到几十 MHz9600/115200/460800 常见空调上常见用途温湿度、EEPROM、ADCFlash、LCD、高速数据日志、调试、无线模组数据口调试难度中需要看 SCL/SDA 时序中需确认时钟极性与相位低逻辑分析仪直接看 RX/TXI²C 的核心优势是引脚少、可挂多设备。空调主板上一个 I²C 总线可以接温湿度传感器、存储校准参数的 EEPROM、实时时钟芯片只要地址不冲突就行。缺点是总线上所有设备共享一根 SDA任何一端把电平拉死整条总线都会卡住。SPI 的优势是速度和可靠性更强。如果空调主板需要驱动较大的 Flash 缓存日志或者带较大屏幕SPI 往往比 I²C 稳。缺点是多一个 CS 片选信号设备一多就要占用更多 IO。空调主控开发中常用的是 SPI Flash用于存放字库、图标资源或者运行日志。UART 则是最灵活的兜底方案。几乎每块空调板都会留一个调试串口方便抓日志看状态机。如果板子上接了 Wi-Fi 模组MCU 和模组之间大概率也是 UART 连接。UART 没有独立时钟线所以通信双方必须约定一致波特率、数据位、停止位、校验位。选型的时候记住一句话传感器、小容量存储、需要总线上挂多个低速设备优先 I²C需要高速读写大块数据优先 SPI要跟外部模块对话、调试输出、协议不定制优先 UART。3. I²C 实例空调主板读取温湿度传感器空调主板读取环境温度的常规做法是由 MCU 作为 I²C 主机定时读取温湿度传感器。以一颗支持标准 I²C 接口的传感器为例典型的读取流程是主机发起起始条件。发送从机地址加写位addr 1 | 0。发送需要读取的寄存器地址。重新发起起始条件或直接进入读模式。发送从机地址加读位addr 1 | 1。读取数据并在最后一字节回 NACK。发送停止条件。不同厂商传感器的寄存器地址和数据格式不同但操作套路基本一致。下面给出一段可移植的 I²C 寄存读封装实际接口名要根据 MCU 的 SDK 替换/* i2c_read_reg.c * 说明hal_i2c_master_write/read 是硬件抽象接口不同芯片名称不同。 * dev_addr 是 7 位器件地址reg_addr 为寄存器地址。 */ #include stdint.h #include stddef.h int hal_i2c_master_write(uint8_t dev_addr, const uint8_t *data, uint16_t len); int hal_i2c_master_read(uint8_t dev_addr, uint8_t *buf, uint16_t len); int i2c_read_reg(uint8_t dev_addr, uint8_t reg_addr, uint8_t *buf, uint16_t len) { uint8_t reg_cmd reg_addr; int ret; ret hal_i2c_master_write(dev_addr, reg_cmd, 1); if (ret ! 0) { return -1; } ret hal_i2c_master_read(dev_addr, buf, len); if (ret ! 0) { return -2; } return 0; }实际空调主板上I²C 温湿度芯片通常会给一个“温度寄存器组”一次可以连续读取。比如#define TEMP_HUM_SENSOR_ADDR 0x44 #define TEMP_REG_HIGH 0x00 void read_temperature_demo(void) { uint8_t raw[6] {0}; uint16_t raw_temp; float temp_c; if (i2c_read_reg(TEMP_HUM_SENSOR_ADDR, TEMP_REG_HIGH, raw, sizeof(raw)) 0) { raw_temp ((uint16_t)raw[0] 8) | raw[1]; /* 大多数温湿度芯片的温度值换算公式是 raw * 0.01 * 具体换算必须按所用芯片数据手册执行。 */ temp_c (float)raw_temp * 0.01f; /* 这里将 temp_c 写入空调控制状态结构体 */ set_indoor_temperature(temp_c); } }注意传感器地址和设备数据格式必须以数据手册为准不要照搬其他项目。判断一条 I²C 通信是否正常的直观手段是看返回值返回成功读到的是合理温度范围比如 10.0 到 45.0 摄氏度之间。返回0xFF或不变值先查器件地址、上拉电阻和接线。反复 NACK再查总线上是否有地址冲突或者设备是否处于复位状态。如果手上有示波器或逻辑分析仪可以抓 SCL 和 SDA 波形SCL 高电平中间 SDA 不能随意跳变地址字节后面应有 ACK 低电平。这个判断方法比反复改代码更高效。4. UART 实例从调试口到业务帧的状态机设计空调设备上最常见也最容易被写坏的就是 UART 协议。调试口要打印日志Wi-Fi 模组要收发 MQTT 数据部分线控器与内机之间也可能走 UART。很多人图省事在接收中断里直接拼数组再用memcmp找帧头这种做法在数据量大或干扰多时很容易出问题。一个可靠的做法是使用“字节状态机”。每收到一字节按照当前状态决定是找帧头、收长度、收数据还是校验。下面用一段简单的 C 语言状态机演示/* uart_frame_parser.c */ #include stdint.h #include stdbool.h #define FRAME_HEADER 0xA5 #define FRAME_MAX_SIZE 128 #define PARSER_WAIT_HEAD 0 #define PARSER_WAIT_LEN 1 #define PARSER_WAIT_DATA 2 #define PARSER_WAIT_CHK 3 typedef struct { uint8_t state; uint8_t len; uint8_t idx; uint8_t checksum; uint8_t buf[FRAME_MAX_SIZE]; } frame_parser_t; /* 一帧协议 * HEAD LEN CMD Payload... CHECK * len 表示 CMD Payload CHECK 的长度 */ bool frame_parser_push(frame_parser_t *p, uint8_t byte) { bool complete false; switch (p-state) { case PARSER_WAIT_HEAD: if (byte FRAME_HEADER) { p-state PARSER_WAIT_LEN; } break; case PARSER_WAIT_LEN: if (byte FRAME_MAX_SIZE) { p-state PARSER_WAIT_HEAD; } else { p-len byte; p-idx 0; p-checksum 0; p-buf[0] byte; p-state (byte 0) ? PARSER_WAIT_DATA : PARSER_WAIT_CHK; } break; case PARSER_WAIT_DATA: p-idx; p-checksum ^ byte; if (p-idx p-len - 1) { p-state PARSER_WAIT_CHK; } break; case PARSER_WAIT_CHK: if ((p-checksum ^ byte) 0) { complete true; } p-state PARSER_WAIT_HEAD; break; default: p-state PARSER_WAIT_HEAD; break; } return complete; }实际工程里接收函数会把逐字节调用这个状态机void uart_isr_receive_byte(uint8_t byte) { static frame_parser_t parser; if (frame_parser_push(parser, byte)) { handle_aircon_frame(parser); /* 清空当前帧状态等待下一帧 */ } }用状态机的好处是不需要依赖一包数据一次读完。无论 UART 中断是一次来一个字节还是 DMA 一次收几十个字节状态机总能正确地把完整帧从字节流里提取出来。空调协议的帧校验要尽量覆盖命令、数据区和序列号。很多人只对“数据中心”做校验结果在强干扰环境中出现帧头发错、命令对不上、响应错乱的问题。5. 中长距离策略RS485/Modbus RTU 在暖通场景的落点空调开发一旦进入多节点场景I²C 和单点 UART 就不够用了。多联机中一台室外机带多台室内机或者空调要接入楼宇自控系统时最常用的硬件层是 RS485应用层则可能是 Modbus RTU 或厂商私有协议。RS485 的优势在于差分传输。两根线 AB 之间靠电平差表示逻辑 0/1抗共模干扰能力强传输距离可以覆盖一个楼层甚至一栋楼。实际空调工程里RS485 总线一般是一主多从主机轮询从机应答。Modbus RTU 的帧结构非常经典如果不确定空调外接协议怎么设计可以先用 Modbus RTU 的思路做参考。RTU 帧的基本组成是从机地址(1字节) 功能码(1字节) 数据区(N字节) CRC16(2字节)CRC16 是整个 Modbus 帧通信可靠性的关键。这里给出常用的 Modbus CRC16 计算函数/* modbus_crc.c * 输入数据指针和长度 * 输出CRC16 值发送时先发低字节再发高字节 */ #include stdint.h uint16_t modbus_crc16(const uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; uint16_t i; uint8_t bit; for (i 0; i len; i) { crc ^ data[i]; for (bit 0; bit 8; bit) { if (crc 0x0001) { crc (crc 1) ^ 0xA001; } else { crc 1; } } } return crc; }一个典型的 Modbus RTU 读取保持寄存器请求包是从机地址 功能码 起始寄存器 读取数量 CRC16 0x01 0x03 0x0000 0x0001 CRC_LO CRC_HI在楼宇自控项目里空调设备如果作为 Modbus 从机主机只要下发这样的命令就能读取空调开关机状态、设定温度、室内温度、故障码等数据。不过空调设备并不全部直接采用标准 Modbus很多厂商会在 Modbus 寄存器表上叠一层私有地址映射。开发时最容易踩的坑不是 CRC 算错而是寄存器地址是按 0 还是按 1 偏移。16 位数据是大端还是小端。温度值带一位小数还是两位小数比如实际 26.5 度存成 265 还是 2650。故障码是单个寄存器位映射还是多位组合。所以接入任何第三方系统前一定要先拿到对方的寄存器地址表和数据定义不要只拿着 Modbus 协议规范就开始写。6. 多联机和车载空调CAN 总线需要掌握到哪一步空调开发中另一个容易遇到的总线是 CAN。CAN 在整车空调控制器和部分大型多联机系统里比较常见。CAN 和 RS485 最大的差别是帧结构自带标识符和优先级仲裁。多个节点可以同时往总线上发数据通过标识符优先级仲裁谁先发不需要像 RS485 那样严格一主多从轮询。CAN 标准数据帧的载荷最多为 8 字节适合传控制状态比如压缩机转速、开停机命令、故障码等短消息。如果做家用壁挂机可能不需要深入了解 CAN如果做多联机、中央空调集中控制或者做车载空调控制器那么就需要掌握以下内容CAN 收发器硬件电路、终端电阻配置。MCU 内部 CAN 控制器的位时序、波特率计算。标准帧 11 位 ID 和扩展帧 29 位 ID 的区别。报文滤波和接收邮箱配置。错误帧、总线关闭状态的处理。很多芯片的 CAN 底层驱动已经由 SDK 提供算法工程师只需要关心应用层报文 ID。但硬件联调时还是要会用 CAN 分析仪。推荐先抓“总线负载率”和“错误帧计数”如果错误帧持续增长说明节点波特率不一致或电平有问题。需要注意商用空调的 CAN 应用层协议很多并不公开芯片底层的 CAN 只是传输通道真正的报文 ID 和字节含义都属于厂商私有资料。做二次开发或数据接入时必须确认自己有没有授权不要擅自逆向或绕过厂商保护机制。7. 主控与联网模块Wi-Fi/BLE 模组接入设计智能空调现在基本都会带 Wi-Fi 模组有些还会加蓝牙配网。MCU 和 Wi-Fi/BLE 模组之间的通信大多不是 MCU 自己跑 TCP/IP 协议栈而是通过串口或 SPI 与模组交互。很多模组厂商提供 AT 指令固件。MCU 通过 UART 发指令给模组让模组完成 Wi-Fi 连接、MQTT 订阅发布等操作。下面给出一段命令交互流程的抽象示意实际指令请以模组厂商手册为准MCU - 模组: ATNETJOINSSID,PASSWORD 模组 - MCU: OK MCU - 模组: ATMQTTCONNiot.example.com,8883 模组 - MCU: OK MCU - 模组: ATMQTTPUB/dev/AC001/property,26.5 模组 - MCU: OK这里的关键问题是UART 是逐字节传输而 MQTT 的消息内容是 JSON 字符串。如果 MCU 发送一个几百字节的 JSON底层串口缓冲区不够大就会把数据截断导致模组收到半包。 解决办法有两个在 MCU 侧先把完整 JSON 拼进发送缓冲区再一次性交给串口 DMA。或者在应用层定义一个简单的“长度数据”封装把 JSON 包进帧里再发给模组。Wi-Fi 模组联调时还要重点观察供电。Wi-Fi 发射瞬间电流会明显抬升如果主板电源走线太细或电容不够模组会出现连不上、频繁重启、传输失败等奇怪问题。先给模组单独测量 3.3V 或 5V 供电电压再看 UART 日志排错顺序千万别反。蓝牙模组的对接逻辑类似但多数蓝牙方案会让 MCU 作为 GATT Server/Client。如果你只做主控侧至少要知道以下几个方面服务 UUID、特征值 UUID、通知开关、MTU 大小。空调 App 远程控制功能实际走的路径多数是App - 云端 - Wi-Fi 模组 - UART - MCU - 空调执行器MCU 收到的指令已经经过了模组和云端的两次转换所以上层下发的字段和 MCU 内部状态机的字段可能完全不一致。合理的做法是在 MCU 内部增加一个“协议转换层”把云端的控制命令先翻译成室内机控制状态机能识别的数据而不是让应用代码到处判断字符串。8. 云端通信MQTT、TLS、物模型与 OTA模组做完 Wi-Fi 连接后真正承担云端通信的是 MQTT 这类物联网协议。MQTT 是发布/订阅模式跟传统 HTTP 请求响应不同设备端可以主动上报云端也可以主动下发。典型的智能空调上下行链路可以抽象成下面这套 topic上行属性上报 /dev/{deviceId}/property/post 下行属性设置 /dev/{deviceId}/property/set 下行控制命令 /dev/{deviceId}/command 上行命令应答 /dev/{deviceId}/command/reply这里只是通用例子不同平台的 topic 规则差异很大。真实开发时一定要先去云平台控制台查看“产品—设备—Topic 列表”里的定义不要按网上教程硬套。一次空调属性上报 JSON 可以是{ ts: 1710000000000, deviceId: AC-001, data: { power: 1, workMode: 0, targetTemp: 26, indoorTemp: 28.5, fanSpeed: 2 } }如果 MCU 侧无法直接构造 JSON可以让 Wi-Fi 模组使用模组 SDK 的 JSON 接口MCU 只传递结构化整形数据。下面用 Python 演示一个模拟设备通过 MQTT 发布属性的流程实际设备端验证时可以用同样的思路import paho.mqtt.client as mqtt broker iot.example.com port 8883 client_id AC-001 device_topic /dev/AC-001/property/post # 实际项目必须配置 TLS、证书和鉴权信息 client mqtt.Client(client_idclient_id) client.username_pw_set(product_key, device_secret) payload {ts: 1710000000000, deviceId: AC-001, data: {power: 1, targetTemp: 26}} client.connect(broker, port, 60) client.publish(device_topic, payload, qos1) client.disconnect()上面的示例省去了 TLS 证书配置实际生产环境绝不能这样裸连。空调属于长期在线、涉及远程开关操作的设备至少要做到设备端启用 TLS 加密。每个设备使用唯一的密钥或证书。topic 中避免出现可猜测的设备权限字段。控制指令必须带序列号或时间戳防止重放。MQTT QoS 选择也值得认真想。QoS 0 可能丢消息不适合远程开关QoS 2 会带来更多确认开销很多家电平台默认建议用 QoS 1即“至少一次”。如果设备端处理重复消息能力不强就要在控制指令里带上消息序号收到重复序号直接丢弃。OTA 升级是云端链路里最不能省的一环。空调控制固件更新一旦中途断电可能导致设备无法启动。规范做法是主固件分 A/B 两个分区。下载升级包前先校验固件大小和版本号。下载完成后校验 MD5/SHA256不带验证的固件不写入。写入完成后设置 boot 标记重启测试新固件。新固件运行异常或上报失败时自动回退到旧分区。OTA 测试不能用刚改完的代码直接批量推给线上设备先拿一两台测试设备验证新固件能正常启动、能上云、能执行控制、能再次升级再逐步扩大升级范围。9. 开发环境、工具链与空调通信问题排查清单空调嵌入式通信开发不复杂但对工具和日志要求很高。很多问题其实不是代码逻辑错而是观察手段不够。前期先把这几类工具准备好能省一半联调时间工具用途使用建议逻辑分析仪看 I²C、SPI、UART 时序抓 SCL/SDA、RX/TX 波形确认电平翻转示波器查电源毛刺、信号质量、波特率偏差重点看模组供电、RS485 A/B 差分信号串口调试助手抓 MCU 日志、模拟模组回复波特率、换行符、十六进制显示都要调好USB 转 485 工具模拟 Modbus 主机先与设备单点通信再接入总线MQTT 客户端验证云端上下行订阅设备 topic观察属性上报和命令下发实际排错时可以按下面的表格逐项排查。问题现象可能原因排查方式解决方案I²C 读取全是 0xFF器件地址错、地址线不通、缺少上拉电阻用逻辑分析仪看 SCL/SDA 是否有 ACK核对地址补上拉电阻换器件重新扫描I²C 总线卡死SDA 被某设备拉低断电后量 SDA 电平逐个断开从机找到拉低设备检查是否进入异常状态需要复位UART 输出乱码波特率不一致、共地问题、二进制格式错用示波器量 RX/TX 波形估算波特率统一波特率确认两侧 GND 共地UART 收到正常但解析失败帧长度字段不对、Crc 校验算法不对用串口助手抓原始 hex 数据按帧结构逐字节核对用模拟数据复算 CRCRS485 通讯超时从机地址错、终端电阻缺、波特率错抓 AB 波形确认电平极性补接 120 欧终端电阻对地址表Wi-Fi 模组频繁断连供电不足、AP 信号弱、模组天线匹配差量模组供电电压看模组 log加强电源滤波检查天线焊接MCU 能上云但属性上报不更新Topic 拼错、payload 结构和物模型不一致用 MQTT 客户端订阅 topic对照云平台产品物模型修正 JSON 字段控制指令偶发不执行MQTT 重复消息或指令时序没处理好看日志中指令 seq 是否重复支持 seq 去重状态机加实时控制条件OTA 升级后设备起不来固件校验不通过、分区表溢出查看 bootloader 日志和分区表增加 A/B 分区回退重新规划 flash 分区从板级到云端最容易忽视的两个问题一个是“时间戳”一个是“状态一致性”。板级通信传感器数据延时几毫秒通常无所谓但上云之后控制指令从 App 到设备要经过云端、模组、UART每一跳都可能产生不确定时延。如果设备侧不记录指令到达时间不判断当前运行模式是否允许该指令很容易出现“用户点了关机压缩机又运行了几十秒才停”的情况。因此空调通信代码里至少要有一个统一的“当前状态结构体”把开关机、模式、设定温度、室内温度、风机档位、故障码放在一起。无论是传感器读取结果、室内外机通信帧、Wi-Fi 模组下发还是云端远程指令最终都更新到同一个结构体。其他业务逻辑只从结构体读状态不直接读某个全局变量。这样联调时只需要看状态结构体变化就能快速判断通信数据有没有真正生效。还有一个常被忽略的问题空调通信涉及强电和弱电混合区域通信线、传感器线、电源线走线如果不规范很容易导致脉冲干扰或通信误码。这一点在 PCB 阶段就要提前考虑。通信端子附近不要紧贴继电器、压缩机驱动或开关电源的强干扰环路RS485 或通信线尽量采用双绞线并做好屏蔽接地。最后强调边界意识。空调产品涉及用户居家安全、用电安全和隐私数据。调试通信协议时不要用真实 App 账号去大量抓包不要在未授权情况下抓取竞品设备数据不要尝试绕过设备厂商的鉴权和加密机制。你要做的是把自己产品的板级到云端的链路打通、调稳、测透确保用户远程开机、故障上报和 OTA 升级都安全可控。下一次拿到空调主控需求时先别急着写代码建议先花半天把这张协议链路图画清楚传感器走哪条总线、MCU 和室内外机怎么通信、Wi-Fi 模组挂在哪个串口、云端 topic 和字段怎么定。链路图画清楚了后面的移植和联调都会顺很多。