简介:本资源是一套完整的物联网设备接入实战工程,面向嵌入式开发初学者与STM32项目实践者,解决单片机通过以太网直连云平台并实现双向通信的核心问题。项目基于STM32F103C8T6主控与W5500以太网芯片(SPI接口),采用标准HTTP协议对接阿里云IoT物联网平台,支持本地温湿度数据主动上报、继电器远程控制指令接收与状态反馈,具备WEB端可视化交互基础能力。压缩包共218个文件,含50个头文件(.h)定义硬件抽象与协议结构、48个源文件(.c)覆盖底层驱动(如usart、i2c、tim、rcc)、网络栈封装及阿里云HTTP交互逻辑,辅以编译中间文件(.o/.d/.crf)和KEIL工程配置(.uvprojx/.uvoptx/.hex),整体大小为6.97MB。已有858人学习下载,提供可直接烧录运行的完整KEIL工程,含系统时钟配置、W5500初始化、传感器读取、JSON格式组包、HTTP请求构造与响应解析等关键模块代码,适配F103全系列芯片,仅需微调KEIL目标型号与Flash容量即可迁移。
1. 项目概述与需求分析
1.1 为什么选STM32+W5500这个组合来做物联网设备
先把这个项目的核心价值说清楚。这套东西解决的是一个很典型的物联网场景:设备端需要上网、需要被远程控制、需要上报传感器数据,同时不想引入WiFi模块的复杂协议栈和功耗问题。STM32负责逻辑控制,W5500负责网络通信,HTTP协议负责跟云端对话,最终用户通过浏览器就能操作继电器、看到温湿度数据。
先说为什么不是ESP8266或者ESP32 WiFi方案。这两类WiFi方案确实开发快,但有一个现实问题:工业场景或弱网环境下的稳定性不够理想,TCP/IP协议栈在模块内部实现,遇到异常连接、DNS解析失败、长时间运行的情况,经常出现需要复位模块才能恢复的尴尬。W5500是硬件协议栈芯片,TCP/IP协议栈全部在芯片内部用硬件逻辑实现,不占用MCU资源,也不依赖操作系统和协议栈代码质量,连接状态稳定得多。而且它用的是SPI接口,几乎任何单片机都能挂,不锁定平台。
再说为什么用HTTP协议而不是MQTT。阿里云物联网平台官方推荐MQTT,但如果你的项目对实时性要求没那么高,控制频率低、数据上报间隔以秒甚至分钟计,HTTP协议完全可以胜任,而且调试简单得多——浏览器里直接能看到返回的JSON数据,不装任何客户端工具。更关键的是,HTTP接入方式意味着你可以在阿里云上直接创建一个普通Web服务器,或者用物联网平台的HTTP上行接口,业务逻辑直接从设备侧发请求到云端即可,不用WebSocket也不用心跳保活,适合快速落地、后期维护人员也能看懂的方案。
这个项目的完整链路是:STM32读取DHT11温湿度数据,同时接收来自云端的HTTP请求,解析指令后控制继电器开关,再把温湿度数据和继电器状态回传给云端。用户侧通过阿里云Web应用或者直接通过公网IP访问设备(如果设备有公网IP的话)完成控制。我在部署时用的方案是阿里云物联网平台提供的设备上报属性和服务调用接口,配合一个简单的Web控制页面,实际跑下来效果非常稳定。
1.2 项目适合什么样的人来学习
如果你满足下面任何一个条件,这个项目就值得你做一遍:
- 你刚入门嵌入式物联网,想搞清楚单片机到底怎么上网、怎么跟云平台交互;
- 你之前用过ESP8266,但对纯MCU+以太网协议栈芯片的方案感兴趣,想学习稳定型方案;
- 你需要做一个远程控制类的产品原型,比如远程开关、智能插座、温控器,不想被WiFi模块的兼容性和信号问题折磨;
- 你想搞懂HTTP协议在嵌入式设备上到底长什么样,报头怎么拼、JSON怎么解析、返回值怎么处理。
我记得第一次做完这个项目时最大的体会是:以前觉得物联网高不可攀,其实核心就三块——设备端的数据采集与执行、网络协议栈的通信封装、云端的规则配置。这套代码走通之后,换云平台也好,换传感器也好,都属于增量工作了。
2. 整体方案设计与硬核拆解
2.1 为什么选阿里云平台的接入方式
市面上的物联网云平台很多,阿里云、腾讯云、百度天工、OneNET都行。我用阿里云的主要原因其实很实际:它的物联网平台文档最快找到、教程最多,而且免费额度对个人开发足够。你需要的核心能力无非就两个方向:设备上行数据(温湿度上报)、云端下行指令(控制继电器)。阿里云物联网平台的高级版提供了物模型机制,能直接在控制台定义温湿度属性和继电器开关属性,生成一套标准的JSON数据格式,设备端和云端都不需要自己发明通信协议。
这套方案的接入逻辑并不复杂。设备端通过HTTP POST请求,往阿里云物联网平台指定的HTTPS或HTTP接口上报属性数据;云端下发控制指令则通过HTTP GET或POST方式把参数拼在请求里,设备端定期去拉取待执行指令,或者阿里云支持直接给设备的公网地址回推数据(这需要设备端有端口映射或使用HTTP长轮询)。我们实际项目中使用的是最简单的模型:设备作为HTTP客户端,主动上报数据,同时每隔1秒监听一个本地HTTP端口,云端服务通过预设的API网关把控制指令转发到设备监听的端口上。
可能会有朋友问:那我直接用TCP Socket连服务器不行吗?当然可以,但你要自己去处理数据粘包、半包、连接超时重连这些问题。W5500虽然硬件协议栈帮你处理了TCP底层细节,但HTTP协议的人工解析依然要写代码。用W5500最舒服的一点是,它连HTTP协议都不关心,给你的是原始数据流,你在W5500之上自己封HTTP层,主动权完全在自己手里。你不用像使用AT指令的WiFi模块那样,折腾各种AT指令版本差异,也不用担心模块固件更新导致指令集变化。
2.2 硬件方案选型:主控、网络芯片与传感器的搭配
这个项目的硬件选型,我最终圈定的是STM32F103C8T6最小系统板、W5500以太网模块、DHT11温湿度传感器、5V继电器模块。整套东西在某宝上买齐也就几十块钱,非常适合学习验证。
STM32F103系列是目前资料最多、社区最活跃的单片机,没有之一。它的SPI外设支持DMA,方便和W5500进行高速数据交换;3.3V逻辑电平与W5500兼容;定时器、GPIO、串口资源充足;即使你用的是C8T6这种48引脚的小芯片,也足够完成SPI驱动、定时器延时、串口调试、GPIO控制继电器这几件事。
W5500是全硬件TCP/IP协议栈芯片,内置10/100M以太网MAC和PHY,支持TCP、UDP、IPv4、ICMP、ARP、IGMP、PPPoE协议。对于我们的需求来说,它能够独立维护TCP连接状态,MCU只需要通过SPI读写它的寄存器缓冲区即可,不需要额外跑lwIP协议栈。这一点特别重要,因为嵌入式开发中协议栈占用资源、Bug难排查的问题,用W5500直接绕过去了。
DHT11的精度不算高,湿度误差±5%RH、温度误差±2℃,但作为学习和原型验证完全够用。如果用在农业大棚或仓库环境监测里,建议后续换DHT22或者SHT30,精度提升明显。继电器用5V低电平触发的模块,和STM32的GPIO直连需要做电平转换或者用光耦隔离,这个我在后文会详细说。
2.3 网络接入架构的绘制与数据流走向
我们把整套系统的数据链路画在脑子里:
用户浏览器访问Web控制页面 → Web页面发送HTTP请求到阿里云物联网平台(应用端API) → 平台记录指令并等待设备拉取 → 设备端W5500定时向平台发送GET请求,获取待执行指令 → STM32解析指令JSON → GPIO控制继电器 → 同时STM32读取DHT11数据 → 设备端通过HTTP POST上报温湿度到平台 → Web页面轮询平台API获取最新数据 → 浏览器渲染显示。
值得注意的是,这种架构下设备不需要有公网IP,阿里云物联网平台作为中转站,设备主动发起连接即可。这解决了家庭宽带没有公网IP的痛点,也避免了在路由器上做端口映射的麻烦。
如果只是本地局域网测试(比如不带阿里云),想让手机浏览器直接控制设备,那就是另一个分支了:STM32的W5500模块直接监听80端口,手机在同一局域网内通过设备IP访问一个简单HTML页面,页面上有按钮,点击后向设备的80端口发POST请求,设备解析后控制继电器。这两种模式我都实现了,代码里通过一个宏定义切换云端模式或本地模式,调试起来非常方便。
3. 环境准备与软件工具链
3.1 开发环境搭建:STM32CubeMX + Keil MDK
我的开发环境是STM32CubeMX生成初始化代码,Keil MDK5编写业务逻辑。建议你也用这套组合,因为CubeMX生成的HAL库代码结构清晰,外设初始化不用自己手写寄存器,对W5500这种多寄存器交互的芯片来说能减少很多低级错误。
具体版本参考:STM32CubeMX 6.x,F1固件包1.8.x,Keil MDK 5.33以上,ARM Compiler 5或6都行。ST-Link驱动按官方装好。
新建工程时,选择STM32F103C8T6芯片,时钟配置为72MHz(外部8MHz晶振,PLL倍频9倍)。SPI1配置为主模式,速率设置为18Mbit/s或9Mbit/s(W5500最高支持SPI时钟频率为33.3MHz)。SPI的时钟极性CPOL=0、相位CPHA=1,这是W5500手册要求的模式,务必不要配错,否则读出来全是0xFF或者状态寄存器错误。
GPIO方面,W5500的RST(复位)引脚随便接一个普通GPIO,比如PB0;SCS(片选)引脚接SPI1的NSS(我习惯用PB12作为软件片选,或者直接用硬件NSS);另外W5500还有一个INT引脚,可以接STM32的外部中断,用于通知MCU有数据到达,本项目的HTTP轮询模式不依赖INT引脚,可以不接。
USART1配置为115200-8-N-1,用于打印调试信息,排查问题全靠它了。继电器控制引脚选用PA0,DHT11数据引脚选用PA1。
CubeMX生成工程后,在MDK中加入W5500的驱动源码。W5500的官方驱动函数库可以从WIZnet官网下载,或者用我下面推荐的标准库版本的W5500.c和W5500.h。驱动代码的核心包括SPI读写函数、寄存器读写函数、Socket创建与关闭函数、数据发送与接收函数。
3.2 阿里云物联网平台的准备工作
在阿里云控制台搜索“物联网平台”,进入控制台后按以下步骤操作:
创建产品。产品名随意(比如SmartDevice),联网方式选“以太网”,数据格式选“JSON”,认证方式选“设备密钥”。我建议在“产品定义”的“功能定义”里添加自定义物模型属性:温度(float,单位℃)、湿度(float,单位%RH)、继电器状态(bool,开关型)。如果不想定义物模型,也可以直接用“透传数据”功能,但为了网页端可视化方便,还是推荐定义物模型。
注册设备。在产品下添加一个设备,获取设备名称(DeviceName)、产品Key(ProductKey)、设备密钥(DeviceSecret)。这三个参数就是设备端的“身份证”,后面HTTP请求里要用到。
创建“HTTP接入”的应用。阿里云物联网平台支持设备直接通过HTTPS接入上报数据,它的HTTP报文里要携带签名信息,签名算法是HMAC-SHA256,以DeviceSecret作为密钥,对一段字符串签名。完整的签名计算过程官方文档有,我在这里直接把常用代码结构放出来。
开通“服务端订阅”或使用“物联网平台的API”。如果Web页面要读取设备最新的温湿度数据,可以通过调用阿里云物联网平台开放API实现,需要主账号AccessKey的鉴权。如果嫌接口签名麻烦,还可以使用物联网平台自带的“基础通信Topic”,用MQTT的Java SDK做个简单服务端中转,不过这就有点偏离HTTP接入的主题了,我不展开。
我自己的方案是更简单的:在阿里云物联网平台侧配置一个Rule引擎规则,将设备上报的数据转发到一个HTTP服务(比如部署在轻量服务器上的一个接口),这个接口再把数据写入云数据库或直接显示在网页上。这样做的好处是Web开发完全不依赖设备端,设备端只负责上报,云端逻辑随便怎么设计。
3.3 代码框架总览
程序的整体结构大致如下:
- main.c:初始化HAL库、时钟、GPIO、SPI,初始化W5500,初始化DHT11和继电器,进入主循环。
- w5500.c / w5500.h:W5500驱动,包括SPI读写、寄存器操作、Socket API。
- http_client.c:HTTP协议封装,构造HTTP请求头、解析响应数据。
- http_server.c(可选):本地Web控制模式时,在W5500上创建TCP监听Socket,解析HTTP请求。
- dht11.c:DHT11单总线数据读取。
- relay.c:继电器控制封装。
- aliyun_iot.c:阿里云HTTP接入的签名生成、数据上报、指令监听。
这套代码框架是模块化的,每个模块的耦合度很低,便于移植。
4. W5500驱动与HTTP协议核心实现
4.1 W5500初始化流程,注意这些细节
W5500上电之后,第一个要做的是硬件复位。复位时序很简单:RST引脚拉低至少500微秒,再拉高,然后等待芯片内部的PHY启动完成,这个等待时间建议500毫秒左右。接着通过SPI写寄存器配置:设置网关地址(GAR)、子网掩码(SUBR)、本机MAC地址(SHAR)、本机IP地址(SIPR)。这四个寄存器必须全部配置正确,否则后面Socket通信永远失败。
W5500的SPI读写有两种模式:可变数据长度模式和固定数据长度模式。我们最常用的是固定数据长度模式的块读写,读取一个寄存器的流程是:先发送16位地址+2位控制位+8位数据,但必须在同一帧CS片选内完成。W5500的控制位表示操作类型(读/写)和寄存器块区域,具体定义如下:
- 位[15:11] = 寄存器地址
- 位[10:8] = 块选择(0=Common寄存器,1=Socket 0寄存器,2=Socket 1寄存器,以此类推)
- 位[2] = 读/写控制(1读,0写)
- 位[1:0] = 操作模式(00=可变长度,01=固定长度,10=读只读,11=无效)
实际调试中,最容易出问题的是SPI时钟极性和相位。我用逻辑分析仪抓过波形,发现W5500要求数据在SCLK下降沿被采样,也就是说SPI模式3(CPOL=1,CPHA=1)或者模式0(CPOL=0,CPHA=1)?标准库里用的是SPI_MODE_0还是SPI_MODE_3?我踩过坑,最后确认W5500的数据手册明确写了支持SPI模式0和模式3,但ST的HAL库驱动时要注意设置CPHA为1Edge还是2Edge。我用的是SPI_MODE3,也就是CPOL=1、CPHA=1,实测能稳定工作。如果你的代码出现W5500读ID失败,优先检查SPI模式。
初始化完成之后,读取W5500的版本寄存器(VERSIONR,地址0x0039),正常值应该是0x04。如果读到0x00或0xFF,说明SPI通信有问题,基本不用往下排查了,先解决SPI时序再继续。
4.2 Socket API怎么用:TCP客户端与服务器的实现
W5500内部有8个独立的Socket,每个Socket独立维护TCP状态机。我们用Socket 0做阿里云上报的TCP客户端;Socket 1做本地HTTP监听的TCP服务器。官方驱动库提供了如下API:
- WIZCHIP_Init():初始化芯片,设置基础寄存器;
- socket(sn, protocol, port, flag):创建Socket,返回socket号;
- connect(sn, addr, port):TCP客户端连接远程服务器;
- listen(sn):TCP服务器监听端口;
- send(sn, buf, len):发送数据;
- recv(sn, buf, len):接收数据;
- close(sn):关闭Socket;
- getSn_SR(sn):获取Socket状态。
作为TCP客户端连接阿里云时,我建议使用短连接方案:每次上报数据前执行 connect → send → recv → close。不要保持长连接,因为HTTP请求本身就自带Keep-Alive的问题,长连接容易导致云端资源占用;而且短连接逻辑简单,即使某次连接失败也不会影响后续上报。实测下来短连接的延迟大约在50~200毫秒,对于物联网数据上报场景完全够用。
TCP服务器监听本地80端口时,需要设置端口为80,然后在主循环中不断检查Socket状态。当监听到新的连接请求时,调用recv读取HTTP请求报文,解析出用户点击的按钮参数,然后执行继电器控制,最后构造一个HTTP响应头并send给浏览器。这个响应头要包含Content-Length字段,否则某些浏览器会一直转圈。
4.3 HTTP协议在单片机上的精简实现
HTTP协议本身不复杂,一个最简单的POST请求报文长这样:
POST /api/device/upload HTTP/1.1 Host: iot.cn-shanghai.aliyuncs.com Content-Type: application/json Content-Length: 123 {"temperature":25.5,"humidity":60.1}构造这个报文时,最关键的坑是Content-Length必须和你实际发送的JSON字符串长度完全一致,多一个空格或少一个空格都会导致服务器接收失败。我的做法是在代码里先定义JSON缓冲区,然后通过snprintf生成JSON字符串,再用strlen获取实际长度,最后填入HTTP头的Content-Length字段。千万不要手写固定长度,因为温湿度小数点位数变化,长度就变化了。
HTTP响应解析时通常不需要完整解析HTTP头,只需要找到“\r\n\r\n”这个空行,空行之后的内容就是响应体(通常是JSON)。我们用strstr函数查找特定字段,比如“errorCode”或“success”,就可以判断设备上报是否成功。需要注意的是,有些服务器会返回分块传输编码(Transfer-Encoding: chunked),这时候响应体里会有十六进制的长度说明,需要额外处理。阿里云物联网平台的HTTP接口我实测返回的是普通Content-Length格式,没有chunked问题,但如果自己搭服务器做中转,就要留意这一点。
4.4 HTTP请求阿里云的签名算法实现
阿里云物联网平台设备HTTP接入的完整URL和头部要求可以参考官方文档《HTTP接入说明》。它的核心验证逻辑是:
- 构建请求参数:包括ProductKey、DeviceName、时间戳timestamp(毫秒)、签名方法hmacsha256。
- 签名内容(StringToSign)是将所有参与签名的参数按字典序排序,然后用&连接起来。
- 以DeviceSecret为密钥,对StringToSign计算HMAC-SHA256。
- 将签名结果作为sign参数追加到请求里。
要在一个Cortex-M3内核的单片机上实现HMAC-SHA256,纯手写算法代码不算难但耗代码空间,我直接移植了开源的小型SHA256实现。完整的签名函数参考如下:
// 生成ArrayToSign:将所有参数key=value按字典序排序并用&拼接 // 例如:deviceName=MyDevice&productKey=a1abc×tamp=1700000000000 // sign = HMAC_SHA256(deviceSecret, StringToSign) // 最终请求URL: // https://iot.cn-shanghai.aliyuncs.com/auth/device/${ProductKey}/${DeviceName}?sign=${sign}&...如果你不想移植全套的加密算法,另一种取巧的方法是使用阿里云物联网平台的“一机一密直连”模式里的设备证书三元组,其中部分HTTP接入接口支持Basic Auth方式,User-Agent里直接带设备证书信息。但这种方式的可靠性不如官方签名验证,我建议还是按签名算法来,毕竟一次移植,以后换腾讯云、换华为云都类似。
我画过一张阿里云HTTP接入的请求流程图(但这里不能用mermaid,我用文字描述):设备发起HTTP请求 → 携带签名参数 → 阿里云网关校验签名 → 校验通过则上报数据并返回200 OK;校验失败则返回401或403错误 → 设备根据返回码重试或放弃。
5. 实战配置:阿里云平台侧完整操作步骤
5.1 创建产品与设备,获取三元组
登录阿里云物联网平台控制台,进入“公共实例”。如果没有实例,先开通一个。
在“产品”页面,点击“创建产品”。产品名称填“STM32_Relay_Device”,所属分类选择“智能生活/电工照明”或“自定义”。联网方式选择“以太网”,认证方式选择“设备密钥”。数据格式选择“JSON”。点击确认。
接下来在“设备”页面添加设备。设备名称填“Device001”。设备添加后,页面会给出ProductKey、DeviceName、DeviceSecret;ProductKey以“a1”或“a2”开头,DeviceSecret是一串32位字符串。这三元组要写进固件里。
进入产品的“功能定义”页面,添加以下自定义功能,作为物模型属性:
| 功能标识符 | 名称 | 数据类型 | 读写类型 |
|---|---|---|---|
| Temperature | 温度 | float(-40~80,单位℃) | 只读 |
| Humidity | 湿度 | float(0~100,单位%) | 只读 |
| RelaySwitch | 继电器开关 | bool | 读写 |
“读写类型”为“读写”意味着云端可以下发该属性的设置指令,非常适合控制继电器。如果只做上报,选“只读”就行。
5.2 配置Web应用与API,打通云端控制链路
控制设备有多种方式,最简单的Web控制页面可以直接通过阿里云物联网平台的“在线调试”功能实现:在设备详情页,点击“在线调试”,可以发送设置属性指令,比如设置RelaySwitch为true,设备端就会收到这个指令。但正式给别人用,还是需要一个独立Web页面。有两个可选方案:
方案A:用阿里云物联网平台提供的“应用开发”或“IoT Studio”,拖拽式设计Web页面和API绑定。这个适合不想写前端代码的朋友,但灵活性一般,适合快速Demo。
方案B:自己部署一个轻量的Web服务(比如用Python Flask或者Node.js),服务器通过调用阿里云物联网平台开放API和设备互动。这个适合有自己的云服务器或轻量应用服务器的朋友。服务器调用设备下发指令的API需要AccessKey_ID和AccessKey_Secret,对AccessKey的权限控制在RAM里做,最小化授权。然后Web页面通过HTTP请求到你的Web服务,Web服务再去调阿里云API。
我实际采用的是方案B。部署在阿里云轻量服务器上,Web页面通过Ajax发起控制请求。Python服务大致长这样:
import requests from aliyunsdkcore.client import AcsClient from aliyunsdkiot.request.v20180120.PubRequest import PubRequest client = AcsClient('<AccessKeyId>', '<AccessKeySecret>', 'cn-shanghai') def publish(command): request = PubRequest() request.set_ProductKey('a1XXX') request.set_MessageContent(base64.b64encode(json.dumps({"RelaySwitch": command}).encode()).decode()) request.set_TopicFullName('/a1XXX/Device001/user/relay_control') response = client.do_action_with_exception(request)注意这里我用的是MQTT的Topic下发指令,因为阿里云设备的属性设置本质上走的是Topic机制。设备端虽然用HTTP上报,但也可以同时维护一个MQTT订阅主题来接收指令;如果你不想引入MQTT协议栈,可以让设备端通过HTTP轮询“设备影子”中的状态,或者每次上报时附带一个“获取指令”标志。轮询方案的延迟通常在几秒到十几秒之间,对继电器控制这个场景可以接受。
大家可能会疑惑:既然可以用MQTT下发指令,那为什么还折腾HTTP?因为设备端W5500要实现MQTT协议和KeepAlive,代码量和复杂度会明显上升,对于只想控制一个继电器、秒级延迟无所谓的产品来说,HTTP轮询就是最简单可靠的方式。设备每上报一次数据,在响应里就能拿到云端待执行的指令,或者单独用一个Topic获取,本质上都是短轮询。
5.3 云端下发指令时,设备端如何解析执行
我们设备端主循环每隔2秒执行一次:
- 读取DHT11,获取温度和湿度。
- 构造JSON上报报文,调用aliyun_iot_upload()将数据POST到阿里云。
- 紧接着调用aliyun_iot_get_command(),该函数会向云端发送一个GET请求,路径类似于/device/command/query?deviceName=Device001&productKey=a1XXX×tamp=...,云端如果存在未消费的指令(比如继电器设置),会在响应中返回。
- 如果响应中包含“RelaySwitch”:1,则GPIO置高,闭合继电器;包含0则断开。
- 将继电器当前状态作为下一次上报的一部分。
这种轮询方式不是阿里的标准物模型交互方式,更像是一种自定义的“指令队列”机制。为了让云端的设置属性指令能被设备拿到,我在云端的“规则引擎-消息流转”里定义了一条规则,将设备属性设置的指令转发到一个自定义HTTP服务,由这个服务存储到Redis里,然后设备端轮询这个HTTP服务。如果你不想自己搭服务,也可以使用阿里云的“设备影子”,设备上报数据后网关自动更新影子,设备也可以通过HTTP获取影子内容,不需要额外写服务端代码。实际项目中,我个人更倾向设备影子方案,因为省去运维一个Redis的成本。
6. 主控端关键代码剖析
6.1 STM32初始化、SPI与定时器配置
CubeMX生成工程后,我只修改几处关键配置:
SPI1的初始化:
hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH; // CPOL=1 hspi1.Init.CLKPhase = SPI_PHASE_2EDGE; // CPHA=1, 即Mode3 hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // 72MHz/8=9MHz hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; HAL_SPI_Init(&hspi1);W5500的SPI速率我建议不要超过18MHz,PCB走线差的话9MHz更稳。如果你的系统时钟不是72MHz,记得重新计算分频系数。
串口重定向fputc:
int fputc(int ch, FILE *f) { HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF); return ch; }这里注意Keil里要勾选“Use MicroLIB”,否则printf会占用大量ROM空间。
定时器方面,我用TIM2做毫秒级时间基准:
HAL_TIM_Base_Start_IT(&htim2); volatile uint32_t g_tick_ms; void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim->Instance == TIM2) { g_tick_ms++; } }W5500内部有超时寄存器,也建议设置一下,特别是TCP重发时间,我用默认值,实测局域网内没问题,广域网环境下遇到超时可以适当调大重试次数。
6.2 W5500驱动移植流程,官方库裁剪
从WIZnet官网下载W5500驱动源代码,里面的文件包括wizchip_conf.c、wizchip_conf.h、socket.c、socket.h、w5500.c、w5500.h。在STM32工程中,只需要把socket.c和w5500.c加入编译,然后在wizchip_conf.h里修改与STM32相关的底层SPI接口函数。
官方库要求实现以下几个回调函数:
void reg_wizchip_cs_cbfunc(void (*cs_sel)(void), void (*cs_desel)(void)); void reg_wizchip_spi_cbfunc(uint8_t (*read_byte)(void), void (*write_byte)(uint8_t byte)); void reg_wizchip_cris_cbfunc(void (*cris_enable)(void), void (*cris_disable)(void));分别表示片选选中/取消、SPI读一个字节/写一个字节、临界区保护。由于我们软件控制片选,cris_enable里我直接关中断,cris_disable里开中断。
以STM32 HAL库实现为例:
void W5500_CS_Select(void) { HAL_GPIO_WritePin(W5500_CS_GPIO_Port, W5500_CS_Pin, GPIO_PIN_RESET); } void W5500_CS_Release(void) { HAL_GPIO_WritePin(W5500_CS_GPIO_Port, W5500_CS_Pin, GPIO_PIN_SET); } uint8_t W5500_SPI_ReadByte(void) { uint8_t byte = 0; HAL_SPI_Receive(&hspi1, &byte, 1, 100); return byte; } void W5500_SPI_WriteByte(uint8_t byte) { HAL_SPI_Transmit(&hspi1, &byte, 1, 100); }在初始化函数中注册:
reg_wizchip_cs_cbfunc(W5500_CS_Select, W5500_CS_Release); reg_wizchip_spi_cbfunc(W5500_SPI_ReadByte, W5500_SPI_WriteByte); reg_wizchip_cris_cbfunc(W5500_EnterCritical, W5500_ExitCritical);然后调用ctlwizchip(CW_INIT_WIZCHIP, (void *)netInfo)完成网络初始化。netInfo结构体里填写网关、MAC、IP等信息。这里有个小坑:W5500的MAC地址必须是全球唯一的,但局域网测试时随便填一个不冲突的都可以;如果多块板子同时接入同一个路由器,MAC地址重复会导致随机掉线,一定要修改。
6.3 HTTP客户端封装的完整代码框架
我写一个精简版的http_client.c,核心函数如下:
uint8_t http_post(const char *host, uint16_t port, const char *path, const char *content_type, const char *body, char *response, uint16_t response_size) { int8_t sock = socket(0, Sn_MR_TCP, 0, 0); if (sock < 0) return 0; // 解析域名或使用固定IP uint8_t ip[4] = {0}; // 阿里云服务器域名需要解析,可以使用DNS Socket 1来解析,也可以直接配置静态IP if (resolve_domain(host, ip) != 1) { close(sock); return 0; } if (connect(sock, ip, port) != SOCK_OK) { close(sock); return 0; } // 构造HTTP请求 char txbuf[512]; uint16_t body_len = strlen(body); sprintf(txbuf, "POST %s HTTP/1.1\r\n" "Host: %s\r\n" "Content-Type: %s\r\n" "Content-Length: %d\r\n" "Connection: close\r\n" "\r\n" "%s", path, host, content_type, body_len, body); send(sock, (uint8_t *)txbuf, strlen(txbuf), 0); // 等待响应 memset(response, 0, response_size); int32_t recvlen = recv(sock, (uint8_t *)response, response_size - 1, 0); close(sock); if (recvlen > 0) { // 跳过HTTP头 char *body_start = strstr(response, "\r\n\r\n"); if (body_start) { memmove(response, body_start + 4, strlen(body_start + 4) + 1); return 1; } } return 0; }这个函数里的域名解析是最麻烦的。W5500自带DNS功能,官方socket库里提供了DNS_init和DNS_run函数,但需要配置一个DNS服务器地址,比如114.114.114.114或223.5.5.5。由于我们的设备在路由器内网,网关地址通常是192.168.1.1,DNS可以通过DHCP自动获取,但W5500本身没有DHCP客户端(官方库里有个DHCP库,我没用),所以我直接在netInfo里写静态IP和DNS。如果你只是连阿里云,也可以直接查一下阿里云服务器IP然后写死,可以省掉DNS代码,但域名IP变更后维护麻烦,不推荐生产环境写死。
6.4 阿里云签名和JSON构造注意事项
HMAC-SHA256是这段代码最绕的部分,我踩过不少坑,这里说几个要点。
首先,时间戳必须是13位毫秒数,不能是10位秒数。阿里云的签名校验会检查时间戳的偏差,如果设备RTC不准,可能被拒绝。我直接用g_tick_ms加一个固定偏移模拟,但长期运行会有累计误差。更好的方案是设备通过NTP服务器校时,但实现NTP会增加代码,我在批量生产时会让设备开机后通过HTTP请求一个时间接口获取标准时间。如果你只是测试,时间戳偏差在5分钟内是可以接受的。
签名字节序和大小写问题。HMAC-SHA256的结果是32字节二进制数据,阿里云要求转成十六进制字符串的小写形式。很多初学者直接拿二进制数据做URL参数,导致请求一直403。我在代码里专门写了一个to_hex函数转换。
最后,阿里云HTTP接入的URL中,签名参数和普通查询参数一样需要URL编码。不过设备上报使用的参数都是简单的字母数字和下划线,不用额外编码。
JSON构造时,浮点数用%.1f格式化为一位小数,避免出现25.500000这种长度不稳定的字符串。博文里如果出现snprintf拼接浮点,要确保缓冲区足够大,我一般分配128字节给JSON缓冲区。
6.5 DHT11与继电器控制的并发处理
STM32主循环如果长时间阻塞在DHT11读取或HTTP等待响应上,会导致继电器控制响应迟缓。我采用的方案是:将DHT11读取和HTTP上报放在一个2ms的软件定时器中,继电器控制则实时响应外部指令。不过要注意,W5500的SPI操作不能被打断,否则SPI数据错误会引发校验失败,所以在SPI读写期间要关闭定时器中断,或者用临界区保护。
DHT11读取时序对比很多,最容易犯的错误是在读取完毕之后没有释放总线,导致下一次读超时。正确做法是主机发送起始信号后,把总线拉高释放,然后等待从机响应。为了保险,我每次读取前都会等待DHT11空闲,最多重试3次,如果失败就返回上次成功的数据,而不是每次都上报0或错误值,这样云端看到的数据不会跳变,Web页面上更平滑。
继电器控制逻辑更简单:PA0接NPN三极管驱动继电器线圈,或者使用光耦继电器模块。STM32的GPIO推挽输出高电平时,继电器模块的信号端如果设为低电平触发,注意逻辑取反。我在代码里定义RELAY_ON_LEVEL = GPIO_PIN_SET,如果是低电平触发的继电器改为GPIO_PIN_RESET即可。务必增加一句禁忌提示:不要用一个GPIO直接驱动5V继电器(没有三极管/光耦)的情况下直接高电平触发,会拉低MCU IO电压甚至烧毁引脚。
7. 本地Web控制模式:不依赖云端的调试玩法
7.1 在W5500上实现简易HTTP服务器
在开发阶段,如果不想每次都跟云端联调,可以在设备上直接跑一个本地HTTP服务器,电脑浏览器访问设备局域网IP,就能看到控制页面。这个功能非常实用,演示起来也直观。
代码实现思路:创建Socket 1,调用listen(1, 80, 0)监听80端口。主循环中调用getSn_SR(1)检查状态。当状态为SOCK_ESTABLISHED时,表示有客户端连接,调用recv接收HTTP请求。请求内容类似:
GET /?relay=on HTTP/1.1 Host: 192.168.1.100在解析请求时,重点看GET /后面的查询字符串。relay=on则置继电器开,relay=off则关。处理完成后,构造HTTP响应:
HTTP/1.1 200 OK Content-Type: text/html Content-Length: 123 Connection: close <!DOCTYPE html> <html>...响应体就是一张简单的HTML页面,可以在页面上用JavaScript定时刷新当前状态,不过本地模式只是调试用,不必写得太复杂。
注意,W5500是硬件TCP栈,多个TCP连接同时访问时会占用多个Socket。作为设备端,通常只需支持单连接即可。如果浏览器开了多个Tab页,可能导致端口占用,需要等待旧连接超时关闭。最简单的方式是让HTTP响应中带上Connection: close,告诉浏览器响应后断开连接,避免Keep-Alive占用Socket。
7.2 局域网调试的便利与局限
本地Web控制模式大大提升了开发效率。我经常在调试时先在本地模式验证继电器逻辑和DHT11采集是否正确,确认没问题之后再切到阿里云模式。两个模式用宏定义切换:
#define CLOUD_MODE 1 #if CLOUD_MODE // 阿里云上报、指令拉取 #else // 本地HTTP server #endif调试时把CLOUD_MODE改为0,运行完全独立于云平台。需要联网时改为1。这个做法让我避免了很多次因为云端配置没改好,设备反复重启的蠢问题。
当然本地模式也有局限:手机在4G网络下无法访问设备,只能用局域网;控制页面无法作为最终产品交付。但对于刚上手的人,这个模式是理解HTTP服务器机理最好的途径。
8. 常见问题与排查技巧实录
8.1 W5500初始化失败、读ID为0的排查方法
这个问题几乎每个初做W5500的人都会遇到。我总结排查顺序如下:
一是SPI接线。检查SCLK、MOSI、MISO、SCS四根线是否与STM32对应引脚一致,特别是MISO/MOSI方向,有朋友因为杜邦线插反,连续几天找不到原因。用万用表量一下SPI时钟引脚是否有脉冲输出,如果CubeMX配置的SPI没有使能,测试代码里要调用HAL_SPI_Init。
二是SPI模式。前面提过,我使用的是SPI Mode3(CPOL=1,CPHA=1)。如果你用的是Mode0,虽然W5500也支持,但有些驱动库写死了Mode3,不匹配会导致读取控制寄存器时字节错位。建议强制统一为Mode3。
三是片选信号。W5500的SCS片选时序要求:每次读写操作片选都要完整拉低到拉高,中间不能用软件GCON PIO模式。使用软件片选时,务必在CS拉高之前完成所有字节收发。我在处理W5500块读写时,SPI发送地址和控制位之后,立刻读取数据,整个过程CS全程拉低。
四是确认W5500芯片供电。W5500需要3.3V供电,且它的I/O电压域为3.3V。如果模块上有1117稳压芯片,检查是否输出正常。
最后是版本寄存器地址。0x0039是Common寄存器块的地址,读取时控制位要写0x0019吗?不,W5500的寄存器地址和控制位拼接方式不是我们直觉上那样的。强烈建议直接用官方驱动库:getVERSIONR()函数,不要自己拼地址。如果还是0,看看代码里有没有把WIZCHIP_READ(value)的返回值当成了地址而不是内容。
8.2 阿里云HTTP返回401、403和404的排查
设备端上报阿里云,常见错误有:
| 返回码 | 可能原因 | 排查方法 |
|---|---|---|
| 401 | 签名错误、设备不存在 | 检查ProductKey/DeviceName/DeviceSecret是否匹配;检查签名算法是否使用了小写hex;检查时间戳偏差是否过大 |
| 403 | 设备被禁用、权限不足 | 在物联网平台确认设备状态为“已激活”;检查是否有IP白名单限制 |
| 404 | 路径不对 | 确认URL路径中/device/xxx是否与官方文档一致;确认租户实例是public还是专属 |
| 429 | 流量被限流 | 降低上报频率,HTTP短连接不要每秒狂刷 |
印象最深的一次是我把签名参数中参数列表排序了,但拼进字符串时没有按ASCII排序,查了两天才发现。这里必须强调:参与签名的所有参数必须用ASCII码从小到大排序,顺序错了,签名永远不对。
另一个隐蔽问题是设备第一次接入后,物联网平台的设备状态显示“未激活”,即使上报成功也不更新状态。这是因为阿里云要求设备必须通过“设备证书”进行一机一密的认证,而我们用HTTP方式接入必须遵循专用接入流程。如果使用标准的HTTP上报接口,设备第一次上报就会激活。如果显示未激活,检查设备端上报的URL和Topic格式,通常是因为路径不是认证地址。
8.3 Web页面一直显示“设备离线”或“数据未更新”
我以前遇到过Web页面显示设备一直离线,但串口日志显示上报是成功的。原因有两点:一是阿里云物联网的设备在线状态是通过MQTT连接保持来判断的,HTTP上报接口本身不改变设备状态,设备在平台一直是“未激活”或“离线”;二是Web页面读取的数据不是从设备影子获取,而是从另一个接口查询,接口查询失败就显示旧数据。
解决方法是:如果你的Web控制页面是由自己搭的Python服务从设备影子拉取数据,那么需要设备端在HTTP上报成功后额外调用云平台的“更新设备影子”接口,或者在物模型上报接口里响应结果中直接带出最新属性。这个逻辑绕了一圈,其实直接用物模型的标准上报接口,平台会自动更新影子,设备状态也会变为在线。所以最终建议:务必走物模型的标准HTTP上报路径,不要自己造轮子。
8.4 继电器抖动、误动作的处理
继电器控制有时候会出现误动作,尤其在上电瞬间或者云端轮询间隔时。排查中发现两个元凶:
一是GPIO默认状态。STM32复位时所有GPIO浮空输入,继电器模块如果内部上拉到低电平触发,可能导致上电瞬间继电器吸合。解决办法是在GPIO初始化时先将继电器引脚设置为推挽输出并输出高电平(对应不动作),再配置时钟,或者外部加上PNP管控制。
二是HTTP响应解析出错,把字符串中的“1”误判为继电器开命令。我原来用strstr(response, "\"RelaySwitch\":1")判断,但有时响应中包含"RelaySwitch":10之类扩展字段,会错误匹配。正确做法是解析出完整的键值对,并分隔出数字部分,严格等于1再执行。
另外继电器本身电气特性会产生火花干扰,如果和W5500或STM32共用电源,会导致系统重启。建议继电器线圈电源独立供电,或者用光耦隔离模块;至少也要在继电器供电引脚并联一个续流二极管。这个坑我在做第一个版本时遇到过,每次继电器动作,W5500就断线重连,后来才发现是共地干扰。
9. 实操心得与我的几点建议
9.1 这套方案的体验如何,谈谈我的感受
在STM32F103+W5500+HTTP+阿里云这套组合上,我完成过两个类似项目,一个是远程控制水泵,一个是环境监测报警器。最大感受是“确定性”。HTTP协议无状态,每次请求得到响应就结束,逻辑链路短,出了问题能快速定位是网络问题、还是云端问题、还是设备问题。相比MQTT长连接断线重连的心跳机制,HTTP方案直观得多。
当然它也有明显短板:实时性做不到毫秒级。我的轮询周期最短做到500毫秒,这已经是极限了,因为要照顾HTTP连接的开销和云端API的限流。如果你要做的是“按钮按下去灯马上灭”这种交互,这个方案不建议;但如果控制的是电机开关、水泵启停,秒级响应完全没问题。
功耗方面,W5500作为有线以太网芯片,功耗比WiFi模块高,不适合电池供电,适合有稳压电源的工业现场或智能家居弱电箱。这是它的固有属性,选型时要心里有数。
9.2 我给新手的3个起步建议
第一,先把本地模式调通。不要一上来就折腾阿里云,本地Web控制能成功,再切云端。本地模式代码量小,串口打印状态直观,能快速建立信心。
第二,调通阿里云前先在电脑上用Postman模拟HTTP请求验证签名算法。把设备端要发送的报文在Postman里原样发一遍,如果能收到正确响应,说明签名和URL路径没问题,剩下的就是嵌入式端移植代码的功夫了。我每次开发新项目都是先这样验证,能省掉至少两天时间。
第三,学会看串口日志。我把所有关键节点都打了日志,包括W5500初始化、TCP连接状态、HTTP响应码、JSON解析结果,一条条对照,问题一目了然。调试嵌入式设备,没有日志就跟闭着眼走路一样。
9.3 基于此方案的扩展方向
做完这个基础项目,后续扩展空间很大。你可以把DHT11换成SHT30、BMP280等多路传感器;把继电器换成PWM控制的可调光、调速设备;把阿里云换成腾讯云、OneNET;把控制端从Web改成微信小程序或手机App。核心的W5500驱动和HTTP逻辑基本不用改,改的是云平台的API和上层业务代码。
如果你想把设备做成大规模出货的产品,还需要考虑加OTA远程升级、TLS加密通信等。W5500本身不支持TLS,需要MCU侧做加密或换用支持TLS的芯片,这就进入另一个复杂度级别了。但学习的角度,先把这个项目完整做出来,你就已经掌握了物联网嵌入式开发中最核心的通信链路,后面的一切都是添砖加瓦。
最后分享一个调试小技巧:我在W5500的HTTP发送函数中,特意留了一个DEBUG_PRINT宏定义,打开它后,单片机通过串口打印完整的HTTP请求报文和响应报文。你可以把它和Postman里的报文对比,逐字节比对,任何不一致的地方就是Bug所在。这个方法我用了很久,针对HTTP这类文本协议,比对报文比看逻辑代码高效得多。
本文还有配套的精品资源,点击获取