ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32F103+W5500接入OneNet:MQTT数据上报与继电器控制实战

STM32F103+W5500接入OneNet:MQTT数据上报与继电器控制实战 简介这是一套基于STM32F103与W5500以太网模块的物联网项目源码面向嵌入式开发者、物联网爱好者以及需要快速实现设备上云的工程师。工程通过SPI驱动W5500网卡以MQTT协议连接中移OneNet云平台实现温湿度数据主动上报、平台控制指令实时接收与继电器动作响应适用于智能家居、环境监测等小型联网设备开发。压缩包共218个文件以C源文件与头文件为核心涵盖标准外设库、W5500底层驱动、MQTT通信逻辑及应用层业务代码同时附带KEIL工程配置、hex固件、map映射和调试信息文件整体约6.97MB方便直接导入、编译与烧录。目前已有525人浏览学习适合正在研究STM32以太网接入、MQTT协议应用或OneNet平台开发的读者作为参考工程。1. 为什么说 STM32F103 W5500 有线接入 OneNet 是省心选择做设备物联最怕的不是协议不会写而是通信链路在没人看着的时候悄悄掉线。Wi-Fi 模块配网麻烦断网后重连要自己维护状态机4G 模块又有流量和资费成本。我自己调过几版之后反而回到 STM32F103 W5500 以太网这种“笨办法”网线插上DHCP 拿到地址MQTT 一条 TCP 长连接稳定挂在 OneNet 上。这套代码把温湿度采集、继电器状态上报和平台下行控制都打通了开发环境是 Keil默认跑在 STM32F103C8T6 上换同系列其他 F103 芯片只需要改型号和 Flash 容量。适合有单片机程序开发经验、想把设备快速接入中移 OneNet 的工程师也适合课程设计和样机二次开发直接抄作业。2. STM32 SPI 驱动 W5500寄存器帧格式与网络初始化2.1 W5500 为什么能帮 MCU 省掉 TCP/IP 协议栈W5500 是一款硬件 TCP/IP 协议栈芯片ARP、IP、ICMP、TCP 和 UDP 都在芯片内部完成MCU 不参与协议栈运算。STM32F103 只需要通过 SPI 读写 W5500 的寄存器和 Socket 缓冲区就能维护一条完整的 TCP 连接。对资源紧张的 F103 来说这比移植 LwIP 然后自己在链路层做以太网帧收发要省事得多。W5500 的应用电路也不复杂一个 25MHz 晶振、一个网络变压器加 RJ45 座就能工作。常见 W5500 原理图里SCLK、MISO、MOSI 三根线接 SPI1SCS 接普通 GPIORSTn 再接一个 GPIO 控制复位。接口对应关系如下表按这个接就不会和 STM32F103C8T6 的默认 JTAG/SWD 引脚打架。STM32F103C8T6 引脚SPI1 功能接 W5500说明PA5SPI1_SCKSCLK时钟一般 18MHz 以内PA6SPI1_MISOMISOW5500 输出STM32 输入PA7SPI1_MOSIMOSISTM32 输出W5500 输入PA4普通 GPIO 推挽输出SCS片选低有效PA3普通 GPIO 推挽输出RSTn硬件复位低电平复位官方 ioLibrary 读一个寄存器时会发出一段 SPI 帧16 bit 寄存器地址、8 bit 控制字节然后是数据字节。控制字节里的块选择决定了当前访问的是公共寄存器、Socket 寄存器、发送缓冲区还是接收缓冲区。实际工程里建议直接用 WIZnet 官方 ioLibrary不要自己从零抠控制字节位域否则很容易在读写缓冲区时踩地址错位的坑。void w5500_spi_init(void) { MX_SPI1_Init(); /* CubeMX 生成的 SPI1Full-Duplex Master */ w5500_hard_reset(); /* 复位脚拉低 100ms 再拉高 */ } void w5500_hard_reset(void) { HAL_GPIO_WritePin(W5500_RST_GPIO_Port, W5500_RST_Pin, GPIO_PIN_RESET); HAL_Delay(100); HAL_GPIO_WritePin(W5500_RST_GPIO_Port, W5500_RST_Pin, GPIO_PIN_SET); HAL_Delay(200); } uint8_t w5500_check(void) { uint8_t ver 0; ver getVERSIONR(); /* W5500 版本寄存器地址 0x0039正常返回 0x04 */ if (ver 0x04) { return 1; } return 0; }MX_SPI1_Init()是 CubeMX 帮我们生成的 SPI 外设初始化函数这里只是为了保证工程里不管从哪里入口进入外设时钟和引脚复用都是打开的。w5500_hard_reset()的作用是让 W5500 上电后回到确定状态如果直接读寄存器容易读到还没就绪的随机值。w5500_check()里的getVERSIONR()是 ioLibrary 提供的寄存器读宏相当于一次最小化的 SPI 回环验证。返回值不是0x04时优先检查 SPI 引脚配置和 W5500 供电。2.2 网络参数设置与 Socket 0 打开W5500 拿到 IP 信息后才能参与 TCP 通信。工程里两种方式都常见静态 IP 或者 DHCP。开发调试阶段建议静态 IP抓包方便产品阶段再改 DHCP。下面的代码把 MAC、IP、子网掩码、网关和 DNS 一次性写进 W5500 公共寄存器。wiz_NetInfo net_info { .mac {0x00, 0x08, 0xDC, 0x12, 0x34, 0x56}, .ip {192, 168, 1, 100}, .sn {255, 255, 255, 0}, .gw {192, 168, 1, 1}, .dns {8, 8, 8, 8}, .dhcp NETINFO_STATIC }; ctlnetwork(CN_SET_NETINFO, (void *)net_info); uint8_t sn 0; socket(sn, Sn_MR_TCP, 0, 0);这里mac是 W5500 的全球唯一 MAC买模块时如果厂家没有预烧就自己随便编一个只要在局域网内不冲突即可。ip是设备本机地址sn是子网掩码gw是网关dns在连接 MQTT 域名时才会用到这里直接填公共 DNS 也能通过。socket()的第二个参数Sn_MR_TCP表示把 Socket 0 设为 TCP 模式第三个参数是本地端口0 表示让协议栈自动分配。到这里STM32 和 W5500 的硬件链路就算通了。后面要想让数据上云还需要把 Socket 0 连到 OneNet 的 MQTT broker 上并且在 TCP 之上完成 MQTT 协议握手。3. MQTT 接入 OneNetAPIKey 鉴权、数据上报与继电器命令3.1 OneNet 控制台要生成哪些参数在 OneNet 控制台创建产品后会得到一个产品 ID再在产品下添加设备会得到设备 ID。设备鉴权信息里可以生成 APIKey。老版 OneNet 多协议接入 MQTT 时broker 地址常见的是183.230.40.39:6002但不同活动区域可能不一样以你控制台设备列表页显示为准。这些参数和 MQTT CONNECT 报文的映射关系如下表。OneNet 控制台字段MQTT 报文字段示例值产品 IDUsername366123设备 IDClient ID5201314APIKeyPasswordabcdefg1234567接入地址Broker183.230.40.39:6002数据上报主题Publish Topicdp命令下发主题Subscribe Topiccmd注意这里说的主题是 OneNet 老版 MQTT 多协议接入方式。如果用的是新版 OneNet Studio主题名会变成$sys/{产品ID}/{设备名}/thing/property/post这类形式但 MQTT CONNECT 和 PUBLISH 的报文结构完全一样改一下主题宏就能继续用。工程里默认按老版主题封装方便在平台旧项目里快速验证。3.2 先建立 TCP Socket再组装 MQTT CONNECT 报文W5500 的 Socket 是一个隐藏的通道使用前要先用socket()打开再调用connect()连到远端。连接 MQTT broker 的 IP 和端口可以在初始化时用常见做法固定写死如果平台域名不稳定也可以先 DNS 解析出 IP 再填进去。int8_t mqtt_tcp_connect(uint8_t sn) { uint8_t server_ip[4] {183, 230, 40, 39}; uint16_t server_port 6002; socket(sn, Sn_MR_TCP, 0, 0); while (connect(sn, server_ip, server_port) 0) { HAL_Delay(1000); } return 1; }TCP 连接建立后MQTT 客户端要做的第一件事是发送 CONNECT 报文。下面的函数是简化的 MQTT CONNECT 报文组装完整版在工程里会有 Remaining Length 多字节编码这里为了看清结构按长度小于 128 的常用场景处理。uint16_t mqtt_build_connect(uint8_t *buf, const char *client_id, const char *username, const char *password) { uint16_t pos 0; uint16_t clen strlen(client_id); uint16_t ulen strlen(username); uint16_t plen strlen(password); uint16_t remain 10 2 clen 2 ulen 2 plen; buf[pos] 0x10; /* MQTT Message Type: CONNECT */ buf[pos] remain 0x7F; /* Remaining Length */ buf[pos] 0x00; buf[pos] 0x04; memcpy(buf[pos], MQTT, 4); /* Protocol Name */ pos 4; buf[pos] 0x04; /* Protocol Level MQTT 4 */ buf[pos] 0xC2; /* Username Password Clean Session */ buf[pos] 0x00; buf[pos] 0x3C; /* Keep Alive 60s */ buf[pos] clen 8; /* Client Identifier 长度 */ buf[pos] clen 0xFF; memcpy(buf[pos], client_id, clen); pos clen; buf[pos] ulen 8; /* Username 长度 */ buf[pos] ulen 0xFF; memcpy(buf[pos], username, ulen); pos ulen; buf[pos] plen 8; /* Password 长度 */ buf[pos] plen 0xFF; memcpy(buf[pos], password, plen); pos plen; return pos; }0xC2是 Connect Flagsbit 7 和 bit 6 分别代表 Username Flag 和 Password Flagbit 1 是 Clean Session。OneNet 老版 MQTT 接入必须让 Username、Password 都参与鉴权所以这个标志位不要去掉。Keep Alive 设置 60 秒意思是 60 秒内如果没有任何 MQTT 控制报文设备要主动发一个 PINGREQ平台才会继续认为链路在线。如果 W5500 TCP 层断开connect()会返回错误这时候不能只是重新connect()还要重新走一遍 CONNECT 鉴权。3.3 温湿度上报和继电器下行指令数据上报其实是把温湿度和继电器状态拼成 JSON然后作为 PUBLISH 报文的内容发到dp主题。OneNet 老版数据格式要求这样写{datastreams:[{id:temp,datapoints:[{value:25.3}]},{id:humi,datapoints:[{value:60.1}]},{id:relay,datapoints:[{value:1}]}]}工程里可以直接倒腾一个缓冲区用sprintf把它拼出来然后调用封装好的 MQTT 发布接口。一个可靠的做法是在主循环里定期采样采样结束后统一发布不要在中断服务函数里拼 JSON否则底层的printf重定向和 W5500 SPI 时序容易互相干扰。static char pub_buf[192]; sprintf(pub_buf, {\datastreams\:[{\id\:\temp\,\datapoints\:[{\value\:%.1f}]}, {\id\:\humi\,\datapoints\:[{\value\:%.1f}]}, {\id\:\relay\,\datapoints\:[{\value\:%d}]}]}, temp_value, humi_value, relay_state); mqtt_publish(sn, ONENET_PUB_TOPIC, pub_buf, strlen(pub_buf));mqtt_publish()是工程里已经封装好的函数底层会构造 PUBLISH 报文再通过 W5500 Socket 的send()发送。订阅下行命令时让设备主动订阅cmd主题OneNet 那边一旦有 APP 或者网页按钮按下平台就会向这个主题推一条 JSON。设备端在mqtt_on_message()回调里解析relay字段然后操作继电器引脚。static void mqtt_on_message(const char *topic, uint8_t *payload, uint16_t len) { if (strstr((char *)payload, \relay\:1) ! NULL) { relay_on(); } else if (strstr((char *)payload, \relay\:0) ! NULL) { relay_off(); } }注意MQTT 回调收到的payload是 PUBLISH 报文的载荷部分不是完整报文所以可以直接在这里做字符串匹配。实际产品建议用cJSON解析一是避免relay:1和relay:10出现误匹配二是当平台下发多个字段时能更规范地处理。4. Keil 工程适配芯片型号、Flash 容量与 ST-Link/J-Link 烧录排错4.1 从 STM32F103C8T6 换到其他 F103 的改动点这套代码基于标准外设库工程里默认设备是 STM32F103C8T6。如果你手里是STM32F103RCT6或者STM32F103ZET6原理上都能直接跑只要改 Keil 里的芯片型号和 Flash 容量即可。用 Keil 打开工程后在Options for Target的Device页选择具体型号在Target页里手动调整 ROM 起始地址和大小。芯片型号Flash 容量Keil Device工程宏定义IROM1 设置STM32F103C8T664KBSTM32F103C8STM32F10X_MD0x80000000x10000STM32F103RCT6256KBSTM32F103RCSTM32F10X_HD0x80000000x40000STM32F103ZET6512KBSTM32F103ZESTM32F10X_HD0x80000000x80000这里的宏定义是给标准外设库用的。中密度和高密度芯片外设中断向量、启动文件不同忘记改宏编译时也可能通过但程序会在启动阶段跑飞。换芯片后还要顺手把启动文件换成对应密度的startup_stm32f10x_xx.s。如果 Keil 里找不到型号先到 Pack Installer 里安装 STM32F1xx 系列芯片包安装完重新打开工程。4.2 J-Link 与 ST-Link 的选择逻辑下载和调试时工程里需要区分 J-Link 和 ST-Link。在Options for Target - Debug页面右侧下拉框选J-LINK/J-TRACE Cortex或者ST-Link Debugger。选错下载器常见表现是 Keil 能识别到驱动但连接时报RDDI-DAP Error或者No STM32 Target Found。另外 J-Link 和 ST-Link 的接线都要注意长线问题。SWDIO 和 SWCLK 两根线超过 20cm 时下载频率超过 1MHz 就可能握手失败。我一般先把 Download 频率降到 500kHz确认能连上后再恢复到 4MHz。手里只有 ST-Link 时也可以用 STM32 ST-LINK Utility 做全片擦除然后再回 Keil 烧录这样能清掉之前工程里的读保护位。4.3 “No STM32 Target Found” 最常见是哪几个原因这个报错在stm32 st-link utility和 Keil 里都经常出现不是代码问题而是调试链路没建立。优先查以下四步SWDIO 和 SWCLK 是否接反这是最隐蔽的硬件错误。BOOT0 是否被外部电路拉高。BOOT0 悬空时容易受干扰把它通过 10k 电阻接地。目标板供电是否稳定。W5500 模组如果和 STM32 共用一个 3.3V LDO电流不够时 W5500 会在上电瞬间把电压拉低导致 STM32 无法被调试器连接。复位脚被复用。电路里如果接了看门狗或一键下载电路调试器在握手时会被复位打断。用命令行工具排查时可以这样快速探测STM32_Programmer_CLI.exe -c portSWD modeURmodeUR表示 Under Reset。加上这个参数以后调试器会先拉低复位脚再发起 SWD 握手能绕过很多软件复位失效的情况。如果这里还是连接失败就该拿示波器看 SWCLK 波形了。5. 上云验证、MQTT 保活与继电器状态回读5.1 先看数据流再点 APP 按钮程序烧进去以后第一步不是直接连 APP而是打开 OneNet 控制台看设备是否在线。设备在线后把温湿度传感器用手焐热控制台对应数据流temp和humi应该能看到数值变化。确认上行没问题后再在 OneNet 网页端下发一条relay指令。注意区分“收到指令”和“物理继电器动作”可以串口打印日志也可以在继电器驱动引脚上接 LED避免 APP 显示成功但继电器根本没动。5.2 MQTT 保活和 W5500 Socket 重连OneNet 平台会在一定时间内收不到 MQTT 报文就断开这条 TCP 连接。如果设备上报频率低于 60 秒一次保活周期不要设太长。工程里我一般把 Keep Alive 设为 60 秒主循环里每 30 秒主动发一次 PINGREQ这样即便数据流不频繁平台也能感知设备活着。保活的同时还要监控 W5500 的 Socket 状态TCP 断开后自动重连。static uint32_t last_keepalive; void mqtt_loop(uint8_t sn) { if (HAL_GetTick() - last_keepalive 30 * 1000) { mqtt_ping(sn); last_keepalive HAL_GetTick(); } if (getSn_SR(sn) ! SOCK_ESTABLISHED) { close(sn); socket(sn, Sn_MR_TCP, 0, 0); if (connect(sn, server_ip, 6002) 0) { mqtt_connect(sn, device_id, product_id, api_key); mqtt_subscribe(sn, ONENET_SUB_TOPIC); } } }getSn_SR()读的是 Socket n 的状态寄存器SOCK_ESTABLISHED表示 TCP 连接已经建立。当网线被拔掉或平台主动断开时这个状态会切回SOCK_CLOSED这时候要先close()再重新socket()不能对同一个 Socket 直接connect()。重连完成后必须重新订阅下行主题否则平台下发的cmd不会投递到设备。5.3 继电器状态回读避免平台显示和实际不一致Many failures are caused by only reporting the set value. If APP下发relay1, but the relay didnt turn on due to coil failure or driving tube damage, the platform still shows1. In product debugging, add a feedback pin connected to the relay output-contact side. Instead of directly using the command value, read the feedback pin after a 200ms delay, then report the actual level.if (HAL_GPIO_ReadPin(RELAY_FB_Pin) ! relay_state) { relay_state HAL_GPIO_ReadPin(RELAY_FB_Pin); mqtt_publish_relay(relay_state); }这里RELAY_FB_Pin接的是继电器的常开触点侧继电器吸合后引脚会被拉到确定电平。relay_state是当前要上报的布尔值。只有反馈引脚和软件里的状态不一致时才重新上报既能纠正平台侧的错误展示也能避免每 30 秒重复上报同一份数据。对于5年以上的量产产品整流二极管、三极管基极限流电阻和继电器线圈反向电动势都要重新算一遍单靠软件保护是不现实的。本文还有配套的精品资源点击获取
返回列表