尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32+W5500+MQTT上云实战:继电器与温湿度控制闭环

STM32+W5500+MQTT上云实战:继电器与温湿度控制闭环 简介面向物联网开发与嵌入式学习者的STM32W5500实战工程基于MQTT协议完整演示了接入百度天工云平台的数据收发流程。工程以STM32F103为主控通过SPI驱动W5500以太网模块既能主动上报温度、湿度及继电器状态也能接收云端下发的控制指令并驱动执行适合具备一定单片机基础的开发者参考。压缩包共207个文件约6.37MB以C源码和Keil工程文件为核心包含标准外设库、编译产物及调试辅助文件可直观对照源码与生成结果。验证代码基于STM32F103C8T6运行同系列其他型号只需调整芯片型号与Flash容量即可复用下载时根据实际调试器选择J-Link或ST-Link能减少环境配置问题。目前已有884人学习适合快速搭建物联网平台接入原型也有助于理解MQTT指令交互、以太网协议栈移植以及板级外设协同的完整工程结构。1. STM32 网口上云为什么是 W5500 MQTT 这条路给一块 STM32 加网口方案其实不少LAN8720 跑 LwIP、串口转网口模块、或者干脆用带以太网控制器的 MCU。但工业现场和毕设里最常见的组合是 STM32 加 W5500 这颗硬件协议栈芯片。W5500 内部把 MAC、PHY 和 TCP/IP 状态机一次做掉MCU 只负责通过 SPI 收发 Socket 数据固件里不需要 LwIP 那套内存池对只有 20KB RAM 的 F103 也非常友好。这篇短文顺着标题里的“继电器 温湿度”展开以百度云物联网平台作为 MQTT broker写清楚从 W5500 初始化到 MQTT 收发闭环的完整测试程序怎么落地。适合手里已经有 STM32 开发板想在一个下午之内把数据发到云端并收到远程命令的工程师。2. W5500 电路图与寄存器初始化让 STM32 先打通 TCP 链路2.1 W5500 电路图与引脚要点SPI 四线加复位、中断W5500 不是普通的 PHY 芯片它把 MAC、PHY、TCP/IP 协议栈和 32KB 收发缓存都封装进了单颗芯片。跟车载以太网那种动辄 TSN、SOME/IP 的复杂度相比W5500 面向的是传统 10/100M 工业以太网接线和驱动都简单得多。MCU 侧只需要 SPI 接口协议栈里的连接、断开、重传、超时全部由芯片硬件处理这决定了后续写 MQTT 代码时我们根本不用关心 TCP 滑动窗口和重传定时器。典型的 W5500 电路图里信号线基本就是下面六根。网上很多参考设计会给它配一个带网络变压器的 RJ45 座好处是省掉独立变压器布局的麻烦差分对布线也更省心。信号W5500 引脚STM32 引脚说明SCLKSCLKPA5SPI1_SCK建议分频后不超过 18MHzMISOMISOPA6SPI1_MISOW5500 输出MOSIMOSIPA7SPI1_MOSI写入寄存器和数据SCSSCSnPA4片选软件控制低电平有效RSTRSTnPB0硬件复位低电平复位INTINTnPB1中断输出下降沿有效可选复位时序是个容易翻车的点。RST 拉低至少保持 500us再拉高之后等 150ms 再访问寄存器否则 PHY 还没准备好读出来的寄存器全是 0xFF。电源部分用 3.3V 给 VCC芯片内部还会产生 1.8V 内核电压注意在 VCC 引脚附近放 100nF 和 10uF 的退耦电容。差分线对 TD/TD- 和 RD/RD- 要成对走线长度差控制在 5~10mil 以内别为了抄近路把差分线拉得一条长一条短。2.2 用 VERSION 寄存器验证 SPI 引脚接线W5500 的 SPI 访问帧由三部分组成16 位寄存器地址、8 位控制字节、数据段。控制字节里高两位是操作模式 OMbit5 是读/写位 RWB低 5 位是块选择 BSB。块选择从 0x00 到 0x080x00 对应公共寄存器区0x01 到 0x08 对应 8 个 Socket。读操作时 RWB 置 1所以控制字节就是0x20 | bsb。先写一个最基础的读寄存器函数用它读 VERSION 寄存器验证 SPI 接线是否正确。VERSION 是公共寄存器区地址 0x0039复位后固定返回 0x04。static uint8_t w5500_read_reg(uint8_t bsb, uint16_t addr) { uint8_t frame[3]; uint8_t value 0; frame[0] (uint8_t)(addr 8); // 地址高字节 frame[1] (uint8_t)(addr 0xFF); // 地址低字节 frame[2] 0x20 | (bsb 0x07); // RWB1 读 HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_Transmit(hspi1, frame, 3, 100); HAL_SPI_Receive(hspi1, value, 1, 100); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); return value; } uint8_t w5500_read_version(void) { return w5500_read_reg(0x00, 0x0039); // BSB0x00 公共寄存器 }代码里 CS 在整个读操作期间保持低电平HAL_SPI_Transmit 和 HAL_SPI_Receive 虽然是两次独立调用但片选没有释放W5500 会把它当成同一次寄存器访问。第三个参数 100 是 SPI 超时时间单位毫秒调试时如果总线时钟配错这里会返回 HAL_TIMEOUT。上电后如果读到 0x04说明 SPI 引脚、时钟和片选逻辑都没问题再往下调 Socket 才有意义如果读到 0x00 或 0xFF先查复位引脚和 SPI 分频。2.3 Socket 寄存器状态机从 OPEN 到 ESTABLISHED 的最小代码网络初始化要写的内容不多依次是软件复位、网关、子网掩码、MAC 地址和本机 IP。公共寄存器区的地址比较规律MR 是 0x0000GAR 是 0x0001SUBR 是 0x0005SHAR 是 0x0009SIPR 是 0x000F。写寄存器时注意不同寄存器的长度GAR、SUBR、SIPR 都是 4 字节SHAR 是 6 字节。void w5500_net_init(const uint8_t *ip, const uint8_t *mask, const uint8_t *gw, const uint8_t *mac) { w5500_write_reg(0x00, 0x0000, 0x80); // MR: RST 软复位 HAL_Delay(10); w5500_write_buf(0x00, 0x0001, gw, 4); // GAR 网关地址 w5500_write_buf(0x00, 0x0005, mask, 4); // SUBR 子网掩码 w5500_write_buf(0x00, 0x0009, mac, 6); // SHAR MAC 地址 w5500_write_buf(0x00, 0x000F, ip, 4); // SIPR 本地 IP }TCP 连接的建立流程是固定的先设置 Sn_MR 为 TCP 模式写 Sn_CR 的 OPEN 命令等到 Sn_SR 变成 SOCK_INIT 即 0x13再写 CONNECT 命令最后等 Sn_SR 变成 SOCK_ESTABLISHED 即 0x17。这里对应 Socket0BSB 就是 0x01Socket 寄存器的地址偏移从 0x0000 开始。寄存器相对地址作用Sn_MR0x0000协议模式0x01 表示 TCPSn_CR0x0001命令寄存器写入触发动作Sn_SR0x0003状态寄存器命令结果反馈void w5500_socket_connect(uint8_t *server_ip, uint16_t port) { w5500_write_reg(0x01, 0x0000, 0x01); // Sn_MR TCP w5500_write_reg(0x01, 0x000F, 0x10); // Sn_DIPR 对端 IP w5500_write_reg(0x01, 0x0013, (port 8) 0xFF); // Sn_DPORT w5500_write_reg(0x01, 0x0014, port 0xFF); w5500_write_reg(0x01, 0x0001, 0x01); // Sn_CR OPEN while (w5500_read_reg(0x01, 0x0003) ! 0x13); // 等 SOCK_INIT w5500_write_reg(0x01, 0x0001, 0x04); // Sn_CR CONNECT while (w5500_read_reg(0x01, 0x0003) ! 0x17); // 等 ESTABLISHED }这段代码里的 Sn_DIPR 是目的 IP 寄存器相对地址 0x000F占 4 字节Sn_DPORT 是目的端口0x0013 高字节、0x0014 低字节。CONNECT 命令发出后TCP 三报文握手由 W5500 硬件完成不需要 MCU 干预。如果状态长时间停在 0x15 也就是 SYN_SENT说明对端不可达先检查 IP 配置和网关如果停在 0x13 不动检查目的端口是不是被防火墙挡了。3. 在 STM32 上组 MQTT 报文温湿度上报与继电器控制的收发闭环3.1 MQTT 协议详解里的关键报文与 TCP 状态配合W5500 把 TCP 链路打通之后MQTT 就是纯粹的应用层协议这一步不需要移植完整的 MQTT 库。MQTT 3.1.1 的基础报文一共就十几种收发测试程序里真正用到的只有 6 种。报文的第一个字节高四位是报文类型低四位是标志位解析时直接对 0xF0 取掩码就能分辨。报文固定头首字节方向用途CONNECT0x10设备到云端发起 MQTT 连接CONNACK0x20云端到设备返回连接结果PUBLISH0x30双向发布消息SUBSCRIBE0x82设备到云端订阅主题SUBACK0x90云端到设备订阅确认PINGREQ / PINGRESP0xC0 / 0xD0双向保活心跳QoS 等级在固定头的标志位里体现。PUBLISH 用 QoS0 时首字节是 0x30QoS1 是 0x32。温湿度上报这种数据丢一次可以接受用 QoS0 能省掉 PUBACK 往返继电器指令必须可靠送达建议把下行 Topic 的 QoS 设为 1否则远程开合闸丢一条消息会造成误判。代码里订阅时怎么声明 QoS发布时就必须匹配这个等级不能发布端写 0、订阅端期望 1。3.2 CONNECT 报文组包与剩余长度编码CONNECT 报文的可变头固定是 10 字节协议名“MQTT”占用 6 字节、协议级别 1 字节、连接标志 1 字节、Keep Alive 2 字节。后面的 Payload 依次是 Client ID、Username、Password每段前都有 2 字节长度前缀。一个常见的坑是剩余长度字段小数据包确实可以单字节表示但只要 Client ID 和密码一长剩余长度超过 127就必须用 MQTT 的多字节编码规则。uint8_t encode_rem_len(uint32_t len, uint8_t *out) { uint8_t cnt 0; do { uint8_t b len % 128; len / 128; if (len) b | 0x80; // 后面还有字节 out[cnt] b; } while (len); return cnt; }这段编码逻辑就是每次取低 7 位最高位置 1 表示继续。解码时反过来累加即可。真正组 CONNECT 包时先把可变头和 Payload 填进一个缓冲区最后把剩余长度编码后插到固定头后面这样的顺序不容易算错长度。uint16_t mqtt_build_connect(uint8_t *out, const char *client_id, const char *username, const char *password) { uint16_t rem_len; uint16_t payload_len 0; uint8_t *p out; payload_len 2 strlen(client_id); // Client ID payload_len 2 strlen(username); // Username payload_len 2 strlen(password); // Password rem_len 10 payload_len; // 可变头 10 字节 *p 0x10; // CONNECT p encode_rem_len(rem_len, p); *p 0x00; *p 0x04; // 协议名长度 MQTT *p M; *p Q; *p T; *p T; *p 0x04; // 协议级别 4 *p 0xC2; // UsernamePasswordCleanSession *p 0x00; *p 0x3C; // Keep Alive 60 秒 *p 0x00; *p strlen(client_id); memcpy(p, client_id, strlen(client_id)); p strlen(client_id); *p 0x00; *p strlen(username); memcpy(p, username, strlen(username)); p strlen(username); *p 0x00; *p strlen(password); memcpy(p, password, strlen(password)); p strlen(password); return (uint16_t)(p - out); }连接标志 0xC2 二进制是 1100 0010bit7 置 1 表示有 Usernamebit6 置 1 表示有 Passwordbit1 置 1 表示 Clean Session。如果你的设备管理后台不允许两个连接同时在线这个标志就不能改成 0。Keep Alive 填写 60 秒设备必须在 60 秒内发任意报文通常做法是发 PINGREQ。组合出来的这一包数据就是 MQTT Client 的核心W5500 把它当成普通的 TCP 发送数据通过 Sn_CR 的 SEND 命令发出去即可。3.3 把继电器和温度 Topic 约定好让收发测试闭环跑起来温湿度采集和继电器控制是两个独立的功能模块但都挂在 MQTT 收发循环上。市场上大量成品的“以太网温湿度传感器”也是这个套路采集端定时发布数据控制端订阅命令 Topic本质上就是一套发布订阅模型。测试代码里不需要上 cJSON用自描述的短文本协议就能跑通闭环。功能TopicQoSPayload 示例温湿度上报user/{productId}/{deviceName}/telemetry0temperature26.5,humidity48.2继电器命令user/{productId}/{deviceName}/relay1RELAY:ON继电器应答user/{productId}/{deviceName}/relay_reply0relayON温湿度传感器用 DHT11 最省事GPIO 初始化成开漏输出加 4.7k 上拉读取前先拉低 18ms 再释放传感器返回 40bit 数据。DHT11 的时序对中断很敏感读取期间最好关全局中断否则偶发一位错位温度和湿度会出现整段跳变。校验位通过后sprintf 拼成上面的字符串调用 mqtt_publish 发送。float temperature 0.0f, humidity 0.0f; char payload[64]; if (dht11_read(temperature, humidity) 0) { snprintf(payload, sizeof(payload), temperature%.1f,humidity%.1f, temperature, humidity); mqtt_publish(TOPIC_TELEMETRY, payload, 0); }继电器控制的命令解析也放在主循环里收到 MQTT 消息后先判断这是不是自己订阅的 Topic再对 Payload 做字符串匹配。继电器模块如果是低电平触发GPIO 初始化时就要注意默认电平避免上电瞬间继电器误动作。if (strcmp(topic, TOPIC_RELAY) 0) { if (strstr(payload, RELAY:ON)) { HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, GPIO_PIN_SET); mqtt_publish(TOPIC_RELAY_REPLY, relayON, 0); } else if (strstr(payload, RELAY:OFF)) { HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, GPIO_PIN_RESET); mqtt_publish(TOPIC_RELAY_REPLY, relayOFF, 0); } }这段逻辑里 strstr 做匹配简单直接但如果有两个指令分别是 RELAY:ON 和 RELAY:ONCE短字符串匹配就会误触发。测试代码用 RELAY:ON 和 RELAY:OFF 这种互斥指令问题不大正式产品建议改成精确比较或者直接解析 JSON。继电器的线圈端必须并续流二极管MCU 控制端如果有光耦隔离更好不然继电器吸合瞬间的浪涌会通过地线干扰 W5500 的以太网收发。4. 百度云物联网平台接入三元组、HMAC-SHA256 签名与 MQTT 连接参数4.1 在控制台建产品、定义物模型拿到设备三元组百度云物联网平台实际上帮你把 MQTT 服务器搭建这件事托管了不需要自己维护 Broker。登录物联网核心套件后先建产品再在产品下定义物模型把温度、湿度、继电器开关这三种属性列出来系统会自动生成对应的数据模板。接着注册设备拿到产品 ID、设备名、设备密钥这三个值固件里所有鉴权都依赖这三元组。物模型的属性定义会直接影响后续 Topic 的结构但测试代码里其实不一定用物模型自带的 Topic。我一般习惯在设备详情页把平台生成的接入信息复制到本地重点关注 Broker 地址、端口和用户名密码样例。百度云的 Broker 地址属于当前创建区域的资源通常在控制台概览页显示格式类似{产品ID}.mqtt.iot.{区域}.baidubce.com端口默认 1883TLS 用 8883。4.2 用 deviceSecret 生成 passwordHMAC-SHA256 签名在 STM32 上的落地百度云 MQTT 接入的 password 不是设备密钥本身而是用设备密钥对一组请求因子做 HMAC-SHA256 签名再转成十六进制字符串。签名因子通常包含版本号、时间戳、Topic 等因素具体拼接顺序以控制台“生成密码”工具给出的样例为准。调这个环节最容易出的问题是因子顺序写反导致云端验签失败。我在 PC 上会先用 OpenSSL 验证一遍签名结果再搬到固件里。echo -n source_string | openssl dgst -sha256 -hmac deviceSecret固件端用 mbedTLS 的 MD 模块实现同样的计算STM32CubeMX 里勾选 mbedTLS 软件包后就能直接调用。完整计算 HMAC-SHA256 只需要四个步骤初始化上下文、指定 SHA256 算法、喂入密钥、喂入消息。void calc_password(const char *secret, const char *source, uint8_t out[32]) { mbedtls_md_context_t ctx; mbedtls_md_init(ctx); mbedtls_md_setup(ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1); mbedtls_md_hmac_starts(ctx, (const uint8_t *)secret, strlen(secret)); mbedtls_md_hmac_update(ctx, (const uint8_t *)source, strlen(source)); mbedtls_md_hmac_finish(ctx, out); mbedtls_md_free(ctx); }计算出来的 out 是 32 字节原始摘要MQTT 连接参数里需要的是十六进制字符串还需要一个 hex 转换函数把它变成 64 个字符的小写字符串。转换时注意 sprintf 每次调用开销不小测试代码里可以预先分配 64 字节的缓冲用查表法转换别在每次重连时反复格式化。签名里如果带时间戳设备还需要有一个准确的时间源最简单的办法是在固件里固定一个较长的有效期或者连上平台后先从服务器同步一次时间。MQTT 连接参数的整理可以做成一张表方便和平台控制台对比MQTT 参数取值说明Broker 地址控制台概览页展示区域资源地址TCP 端口1883TLS 加密连接用 8883Client ID设备唯一标识同一设备重复连接会互踢Username产品 ID 加设备名的组合按平台样例拼接PasswordHMAC-SHA256 签名后的 hex用 deviceSecret 生成4.3 上线、订阅、发布的时序与 W5500 状态机配合从 W5500 的角度看MQTT 的上线过程分两层。先让 TCP Socket 进入 ESTABLISHED再在应用层发 MQTT CONNECT。TCP 层连接失败和 MQTT 鉴权失败是两类完全不同的错误调试时不要被“上不了线”一句话混在一起排查。正确顺序是W5500 的 Sn_SR 变成 0x17 后把 mqtt_build_connect 生成的数组通过 Socket SEND 发出去然后等待接收缓冲区出现 0x20 开头的报文。收到 CONNACK 后0x20 0x02 这两个字节是固定的第三个字节不用管第四个字节才是返回码。返回码为 0 表示连接成功接着发 SUBSCRIBE 报文订阅继电器 Topic。收 SUBACK 后再进入主循环定时发布温湿度数据。平台侧在设备列表中能看到设备上线物模型数据里会出现温度和湿度属性这就是最直接的验证方式。百度云控制台的“在线调试”功能可以直接向设备下发命令测试时用平台调试工具往 relay Topic 发一条 RELAY:ON看 STM32 的调试串口输出和继电器动作能省掉反复改固件的时间。如果平台日志里能查到上行消息但设备端没反应优先查 Topic 是否完全一致多一个斜杠或者少一个层级都不行。5. 收发测试排错用 Wireshark、CONNACK 返回码和 Socket 重连把链路调好5.1 用 Wireshark 把 TCP 层和 MQTT 层分开看抓包是这个方案里效率最高的排错手段。电脑直接用网线连设备的 RJ45 口Wireshark 选择对应网卡先过滤目标端口再逐步缩小范围。TCP 层只看三次握手有没有完成MQTT 层看协议交互过程两者混在一起很难定位。tcp.port 1883如果只看到 SYN 重传说明 TCP 层就没通问题在网线、IP 配置或防火墙跟 MQTT 无关。TCP 握手正常之后再加过滤器mqtt看 CONNECT 发出后有没有 CONNACK 回来。电脑端如果同时跑着其它 MQTT 调试客户端过滤器要加 ip.addr 设备IP排除干扰。5.2 CONNACK 返回码与签名相关报错CONNACK 返回码是最直接的错误线索。返回码不是 0 时平台会在这一条报文里明确告诉设备端问题出在哪。返回码含义处理方向0连接接受正常1协议版本不支持检查协议级别是否为 42Client ID 不合法检查标识符字符集3服务不可用检查平台配额和产品状态4用户名或密码错误检查 Username 格式5未授权重点检查 HMAC 签名因子顺序返回码 5 是出现频率最高的问题基本集中在 password 生成环节。先在 PC 上用 OpenSSL 把签名因子算一遍再和固件里算出的 hex 字符串逐字符对比。时间戳因子的位数和有效期也要一致如果平台要求秒级时间戳固件里填了毫秒级验签必然失败。5.3 断线重连的 Socket 恢复顺序设备长时间运行后云端把连接断开或者网线被拔掉再插回去测试代码里最容易出现一个错误W5500 的 Socket 还停留在旧状态就直接重发 CONNECT。TCP 连接已经不存在Sn_SR 不会自动回到 CLOSED正确做法是先显式关闭 Socket等状态归零后重新走一遍 OPEN 和 CONNECT 流程。void w5500_socket_reopen(void) { w5500_write_reg(0x01, 0x0001, 0x10); // Sn_CR CLOSE while (w5500_read_reg(0x01, 0x0003) ! 0x00); // 等 CLOSED w5500_write_reg(0x01, 0x0001, 0x01); // Sn_CR OPEN /* 重新设置目的 IP 和端口再走 CONNECT */ }CLOSE 命令执行后Socket 的中断标志位会置位如果启用了 INT 引脚重连前要把 Sn_IR 对应位清掉否则 W5500 的 INT 引脚一直拉低主循环会把残留的中断当成新事件反复处理表现出来就是重连后第一包数据经常丢失。Keep Alive 期间设备没有数据要发主动发 PINGREQ 维持链路如果发出 PINGREQ 后长时间收不到 PINGRESP才判定链路失效走上面的关闭流程。通过这种方式测试代码才能在局域网断电、平台重启、网线松脱这些场景里保持可预期地工作。本文还有配套的精品资源点击获取
返回列表