STM32+W5500+OneNet单路状态上传及控制方案详解

STM32+W5500+OneNet单路状态上传及控制方案详解 简介这是一份面向 STM32 与 W5500 以太网开发者的物联网实战代码包演示如何让单片机作为 TCP 客户端接入 OneNet 物联网平台实现单路状态上传及远程控制。例程基于 STM32F103通过 SPI 接口连接 W5500完整涵盖 DHCP 动态获取 IP、连接服务端、TCP 数据通讯与关闭连接等流程KEIL 工程可直接编译运行于 STM32F103C8T6其他 F103 型号稍作芯片与 Flash 容量调整即可适配。包内共 195 个文件约 6.46MB以 C 源码、H 头文件及 KEIL 工程文件为主并包含 hex、axf、map 等编译输出与可烧录文件便于对照工程结构学习或直接烧录验证。已有 1034 人学习适合正在做物联网项目实战或需要快速搭建 OneNet 上云链路的开发者参考。读者可从中获得完整的 W5500 驱动思路、TCP 客户端编程框架以及基于 STM32 标准库的应用层组织方式省去从零排查网络协议栈与移植适配的时间。1. 为什么固定点位上报STM32W5500OneNet是更省心的选择固定点位、又有网线可走的现场用W5500上云比用4G模组更稳也更可控。W5500的特别之处在于它把TCP/IP协议栈做进了硬件ARP、IP、TCP握手、重传这些事全由芯片内部完成STM32不需要为网络协议栈耗尽Flash和内存。标题里的“单路状态上传及控制”落地就是一台设备定时上报一个开关量比如继电器状态同时接收OneNet平台上发下来的控制命令去翻转这个开关。本方案使用STM32F103C8T6 W5500以太网模块 带网络变压器的RJ45座平台侧使用OneNet的HTTP数据点接口全部链路用开源驱动和标准HTTP报文就能打通。适合刚入手以太网上云的工程师也适合拿来做毕业设计框架甚至可以直接改成商业设备的有线局域网版本。2. 硬件链路先闭环W5500原理图要点、RJ45接口定义与读版本号验证2.1 为什么选W5500而不是ENC28J60、DM9000STM32F103系列没有内置以太网MAC更别提PHY了。要让它上网常见外部方案有ENC28J60、DM9000、CH395和W5500。选型时看四个维度MCU负担、接线复杂度、协议栈是否内置、调试成本。我一般会先看TCP/IP协议栈在哪跑因为这一条直接决定主循环能不能按时跑完。方案接口TCP/IP协议栈MCU负载适用场景ENC28J60SPI带MAC/PHY软件栈uIP/lwIP高F103内存紧张数据量小、不要求实时性的场景DM9000并行总线软件栈高占用IO多带FSMC的芯片跑Linux才常见W5500SPI带MAC/PHY/硬协议栈硬件栈8个Socket低MCU只收发数据裸机或RTOS上报、TCP客户端CH395SPI/UART硬件栈但生态小低对成本敏感、资料少也能接受W5500把MAC、PHY和TCP/IP协议栈集成在一颗芯片里MCU侧只需要通过SPI读写寄存器。更关键的是W5500的TCP重传、超时管理在硬件里完成不占用MCU中断和定时器。对于这个项目主循环里既要做5秒周期上报又要轮询控制命令用W5500不会因为网络异常卡死整个程序。2.2 W5500应用电路要点与RJ45座子母头针脚图如果是直接买W5500以太网模块接线非常少但最好还是能看懂模块上的电路。W5500应用电路中核心是3.3V供电、25MHz晶振、复位和差分信号输出。供电部分W5500内置LDO产生内核需要的1.8V外部只需要在电源引脚附近加0.1uF退耦电容。晶振两个引脚到XI/XO之间每脚对地接22pF左右的负载电容具体值以晶振规格书为准。RJ45部分要注意是否“带网络变压器”。模块上常见的HR911105A座子内部已集成变压器和指示灯电阻直接接W5500差分对即可如果是分离式的RJ45座必须中间经过网络变压器再接W5500的TXP/TXN/RXP/RXN不能直连。RJ45母头针脚定义在TIA-568B标准下1脚TX、2脚TX-、3脚RX、6脚RX-4/5、7/8一对空着。焊接座子或排查网口灯不亮时用万用表通断档按这几对关系测量最直接。STM32F103C8T6与W5500模块的引脚对照如下STM32引脚W5500模块信号方向说明PA5SCK输出SPI时钟PA6MISO输入W5500输出数据PA7MOSI输出W5500输入数据PA4SCS输出片选低有效PB0RST输出硬件复位低复位PB1INT输入中断通知可悬空用轮询MISO/MOSI接反是常见错误。模块端子丝印经常标成SDO/SDISDO对应MCU的MISOSDI对应MCU的MOSI对不上就读不到数据。SCS不能直接接地必须用GPIO控制否则W5500无法区分每次SPI帧的起点和终点。2.3 上电先读版本寄存器0x39验证SPI链路硬件焊接完别急着写业务逻辑先写一个最简读寄存器函数确认MCU和W5500之间SPI通信正常。W5500有个版本寄存器VERSIONR地址0x0039正常返回值是0x04。能稳定读到0x04说明供电、晶振、SPI接线全部正常。static void w5500_cs_low(void) { GPIO_ResetBits(GPIOA, GPIO_Pin_4); } static void w5500_cs_high(void) { GPIO_SetBits(GPIOA, GPIO_Pin_4); } uint8_t w5500_read_version(void) { uint8_t ver 0; w5500_cs_low(); spi_read_write_byte(0x00); // 地址高字节VERSIONR 0x0039 spi_read_write_byte(0x39); // 地址低字节 spi_read_write_byte(0x00); // 控制字节读操作、固定数据长度 ver spi_read_write_byte(0xFF); // 读回版本号 w5500_cs_high(); return ver; }代码逻辑不复杂先拉低SCS启动一次SPI帧依次发送16位寄存器地址和控制字节然后发送一个假字节把W5500返回的数据移出来最后拉高SCS结束帧。如果读到的值一直为0xFF或0x00先把SPI时钟降到1MHz到2MHz排除时钟太快导致的采样错误再检查PA6/PA7有没有接反最后确认W5500模块的RST引脚不是悬空状态很多模块RST没有上拉必须由MCU拉高才能正常工作。3. OneNet接入准备创建产品、生成APIKey并用curl验证数据流3.1 平台创建产品和设备协议选HTTPOneNet平台注册登录后进入开发者中心在“多协议接入”里创建产品。接入协议选HTTP联网方式选以太网。创建完成后进入设备列表添加一台设备记下两样东西设备ID和APIKey。设备ID是一串数字APIKey在设备详情页查看一长串十六进制字符。这里有个容易走弯路的地方OneNet支持HTTP、MQTT、TCP透传等多种接入方式。标题场景是W5500以太网最简单可靠的是HTTP协议接入因为W5500本身只处理TCP连接和收发数据HTTP报文格式由STM32拼字符串即可。如果选MQTT还要在MCU上移植MQTT客户端库对仅有单路上报和控制的场景来说收益不大。APIKey的生成和权限建议按设备分配。每台设备独立APIKey便于后续批量管理时单独吊销某个设备的上报权限。产品的产品级APIKey也可以用来上报任意设备数据但一把钥匙开多把锁联调时容易搞混是哪个设备在发数据。3.2 数据流设计上行状态与下行控制分离OneNet的数据流是时间序列的数据点集合。上报状态和控制命令可以共用一条数据流但从工程可维护性上讲我习惯拆成两条数据流ID方向取值用途power上行0或1设备当前开关状态ctrl下行0或1平台希望设备执行的期望状态Two-stream的设计有个明显好处上报数据在平台画折线图时power曲线是真实状态ctrl曲线是命令输入两者对照就能看出命令有没有被执行。控制链路采用“平台写入ctrl设备查询ctrl”的方式不需要维持一条随时在线的长连接。HTTP协议长连接会被中间交换机和运营商设备空闲断开轮询反而更稳定。3.3 先用curl把HTTP接口跑通再动MCU代码不要一上来就写STM32代码先在PC上用curl验证APIKey、设备ID、数据流JSON格式全部正确。上报数据点接口是POST方式路径为/devices/{设备ID}/datapointsDEVICE_ID123456789 API_KEYOxxxxxxxxxxxxxxxxxxxxxxxxxxxx curl -X POST \ http://api.heclouds.com/devices/${DEVICE_ID}/datapoints \ -H api-key: ${API_KEY} \ -H Content-Type: application/json \ -d {datastreams:[{id:power,datapoints:[{value:1}]}]}请求头里的api-key是重点OneNet不认Authorization这种标准头必须叫api-key。返回结果里出现{errno:0,error:succ}代表写入成功errno不为0时核对设备ID、APIKey和JSON里的字段名是否正确。查询控制命令用GET方式路径带上datastream_id和limit参数curl -X GET \ http://api.heclouds.com/devices/${DEVICE_ID}/datapoints?datastream_idctrllimit1 \ -H api-key: ${API_KEY}返回JSON中data部分是最新的ctrl数据点。curl验证通过后把上报和查询两个请求的URL、Header、Body结构固定下来STM32端只需要按同样结构拼字符串。还需要注意W5500里填的服务器IP是api.heclouds.com解析出来的IP但HTTP报文里的Host字段必须保留域名服务端靠Host区分虚拟主机。4. STM32代码实现SPI初始化、W5500建连、HTTP上报与ctrl命令轮询4.1 SPI初始化与W5500硬件复位STM32F103C8T6使用SPI1引脚按第2章的对照表接好。初始化代码使用标准外设库配置PA5、PA7为复用推挽输出PA6为浮空输入PA4为普通推挽输出。void spi1_gpio_init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); gpio.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); gpio.GPIO_Pin GPIO_Pin_6; gpio.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, gpio); gpio.GPIO_Pin GPIO_Pin_4; gpio.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOA, gpio); GPIO_SetBits(GPIOA, GPIO_Pin_4); // SCS默认拉高 }SPI外设配置为主模式、8位数据、CPOL为低、CPHA第一个边沿采样分频器先设大一点保证通信稳定。void spi1_init(void) { SPI_InitTypeDef spi; spi.SPI_Direction SPI_Direction_2Lines_FullDuplex; spi.SPI_Mode SPI_Mode_Master; spi.SPI_DataSize SPI_DataSize_8b; spi.SPI_CPOL SPI_CPOL_Low; spi.SPI_CPHA SPI_CPHA_1Edge; spi.SPI_NSS SPI_NSS_Soft; spi.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_8; // 9MHz/8 spi.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, spi); SPI_Cmd(SPI1, ENABLE); }复位时序上W5500的RST引脚要先拉低10ms以上再拉高然后等待PHY启动200ms比较稳妥。复位后立刻读版本寄存器返回值应为0x04uint8_t w5500_hw_reset(void) { GPIO_ResetBits(GPIOB, GPIO_Pin_0); delay_ms(10); GPIO_SetBits(GPIOB, GPIO_Pin_0); delay_ms(200); return w5500_read_version(); }版本寄存器返回0x04说明SPI链路和W5500内部逻辑都没问题。如果返回0xFF重点查PA4片选时序和SPI分频返回0x00查W5500供电和RST引脚是否被拉死。4.2 创建TCP Socket并连接OneNet服务器W5500驱动里最基础的是Socket操作。本方案只用Socket0参考WIZnet ioLibrary的寄存器宏封装。连接服务器的流程是设置Sn_MR为TCP模式发OPEN命令等待Sn_SR变为SOCK_INIT写远程IP和端口发CONNECT命令等到SOCK_ESTABLISHED。void w5500_socket_connect(uint8_t *server_ip, uint16_t port) { setSn_MR(0, Sn_MR_TCP); // Socket0设为TCP模式 setSn_CR(0, Sn_CR_OPEN); // 打开Socket0 while (getSn_SR(0) ! SOCK_INIT); // 0x13等待OPEN完成 setSn_DIPR(0, server_ip); // 目的IP setSn_DPORT(0, port); // 目的端口80 setSn_CR(0, Sn_CR_CONNECT); // 发起TCP连接 uint32_t timeout HAL_GetTick() 5000; while (getSn_SR(0) ! SOCK_ESTABLISHED) { // 0x17 if (HAL_GetTick() timeout) { printf([W5500] connect timeout\r\n); return; } } }这段代码里setSn_MR、setSn_CR这类函数是网络驱动库的寄存器读写宏在WIZnet官方ioLibrary里都能找到。SOCK_INIT和SOCK_ESTABLISHED这两个状态值需要记住后面查问题要看它们停在SOCK_INIT说明CONNECT命令没生效或者服务器IP不可达停在SOCK_CLOSED说明目标网络不通检查RJ45网口灯和路由器。服务器端口固定80W5500只负责把TCP包发到80端口HTTP协议解析由上层代码完成。4.3 构造HTTP上报报文与JSON数据点OneNet HTTP上报的本质是向服务器发送一个格式严格的HTTP POST请求。报文包括请求行、请求头、空行和Body四部分Body是JSON格式的数据点。这里最关键的是Content-Length必须和JSON字符串实际字节数完全一致多一个少一个都会导致服务器解析失败。static uint8_t http_buf[512]; void w5500_report_power(uint8_t power) { char json[96]; int json_len snprintf(json, sizeof(json), {\datastreams\:[{\id\:\power\,\datapoints\:[{\value\:%d}]}]}, power); int req_len snprintf((char *)http_buf, sizeof(http_buf), POST /devices/%s/datapoints HTTP/1.1\r\n Host: api.heclouds.com\r\n api-key: %s\r\n Content-Length: %d\r\n Content-Type: application/json\r\n Connection: close\r\n \r\n %s, DEVICE_ID, API_KEY, json_len, json); w5500_send_tcp(http_buf, req_len); // 发送到W5500发送缓冲区 w5500_recv_tcp(http_buf, 512); // 等待服务器返回 }发送函数w5500_send_tcp内部会把数据写入Sn_TX_FIFO写完后发SEND命令W5500硬件负责把TCP报文发出去。接收函数阻塞等待Sn_RX_RSR寄存器收到数据后从接收缓冲区读出完整HTTP响应。正常情况下响应Body里有errno:0串口打印出来做联调日志。Connection: close这个头可以简化收包逻辑服务器返回完响应就会关闭TCP连接设备侧不用处理粘包和keep-alive下一轮上报时重新建连即可。代价是每次上报多一次TCP握手但对5秒周期的心跳上报来说完全可以接受。4.4 轮询ctrl数据流并驱动GPIO上报完成后再发一次GET请求查询ctrl数据流的最新值。返回JSON里找到value字段后的数字就是平台下发的期望状态。每次查询结果和本地保存的last_ctrl比较有变化才执行GPIO翻转避免重复执行相同命令。uint8_t parse_value_from_body(const char *body) { const char *p strstr(body, \value\); if (p NULL) return 0xFF; p 7; // 跳过value while (*p ! \0 (*p 0 || *p 9)) p; if (*p \0) return 0xFF; return (uint8_t)(*p - 0); } void w5500_poll_ctrl(void) { static uint8_t last_ctrl 0xFF; int req_len snprintf((char *)http_buf, sizeof(http_buf), GET /devices/%s/datapoints?datastream_idctrllimit1 HTTP/1.1\r\n Host: api.heclouds.com\r\n api-key: %s\r\n Connection: close\r\n \r\n, DEVICE_ID, API_KEY); w5500_send_tcp(http_buf, req_len); memset(http_buf, 0, sizeof(http_buf)); w5500_recv_tcp(http_buf, 512); uint8_t ctrl parse_value_from_body((char *)http_buf); if (ctrl ! 0xFF ctrl ! last_ctrl) { GPIO_WriteBit(GPIOC, GPIO_Pin_13, ctrl ? Bit_SET : Bit_RESET); last_ctrl ctrl; printf([CTRL] %d\r\n, ctrl); } }主循环就是“上报状态、查询命令、睡眠”三部曲单路状态上传及控制的完整逻辑全部在里面int main(void) { delay_init(); usart1_init(115200); spi1_gpio_init(); spi1_init(); if (w5500_hw_reset() ! 0x04) { printf([W5500] hw error\r\n); while (1); } uint8_t server_ip[4] {0, 0, 0, 0}; // 填api.heclouds.com解析出的IP while (1) { w5500_socket_connect(server_ip, 80); w5500_report_power(get_power_pin()); w5500_poll_ctrl(); delay_ms(5000); } }实际工程里还要在每轮循环前判断Socket状态如果上一轮连接被服务器关闭下一轮要重新走OPEN/CONNECT流程。另外状态采集建议做成“变化即上报不变走心跳”也就是GPIO电平变化时立刻上报一次平时按5秒周期上报心跳这样平台侧数据粒度更好网络流量也小。5. 联调排错三板斧Wireshark抓以太网帧、调试器报错排查、OneNet折线图验证5.1 Wireshark确认HTTP报文真的发到了线上MCU代码写完连不上OneNet时最有效的排查工具是Wireshark。把电脑接到同一台交换机交换机不带镜像功能就串一个普通Hub电脑网卡开启混杂模式抓包。过滤条件用tcp.port 80 || http然后给设备重新上电。正常流程里能看到三种帧设备发出ARP请求查询网关MAC、设备向OneNet服务器发TCP SYN、服务器回SYNACK。如果只看到SYN不断重传说明服务器端口不可达或出口被防火墙拦截连SYN都没有问题在W5500的IP配置或交换机端口的VLAN隔离。抓包抓到HTTP层时可以直接看到POST请求行、api-key和JSON内容用这几样东西和平台返回结果对比能区分是链路问题还是数据格式问题。以太网帧格式本身不用过多关注W5500发出的Ethernet II帧符合标准Wireshark能正常解析就说明物理层没问题。5.2 两个高频硬件坑调试器报错和RST悬空STM32烧录时最烦人的报错是Error: No STM32 target found! If your product embeds debug authentication, please ...。这个报错和W5500无关但会卡住整个开发流程。排查顺序建议固定先量STM32的3.3V供电再看SWDIO和SWCLK两条线有没有接反然后确认BOOT0已经接地最后把下载器速度从高速降到1MHz。W5500模块的RST引脚如果悬空上电后芯片可能处于复位状态表现为版本寄存器读不到0x04、RJ45网口灯不亮务必由MCU引脚控制。5.3 OneNet平台折线图验证完整闭环OneNet的数据流自带时间序列存储不用写任何后台代码就能画折线图。进入产品下的“应用管理”新建一个应用添加“折线图”控件数据流绑定power。绑定后在设备上电状态下每5秒会出现一个数据点平台展示的历史曲线可以直接看到状态变化的时间点。控制链路验证可以在应用里再加一个“开关”控件触发该控件向ctrl数据流写入0或1同时观察STM32串口打印的[CTRL] 1确认设备端收到了命令并翻转了GPIO。联调中最后一个小技巧last_ctrl初始值是0xFF设备第一次上电查询ctrl时一定会触发一次GPIO动作。如果不想让设备上电就“乱动”把初始值改成平台当前ctrl值或者在主循环里先做一次同步置位再进入正常比较逻辑这样设备掉电重启后状态也能与平台保持一致。本文还有配套的精品资源点击获取