前两天帮朋友调一个机房环境监控的小项目,需求很简单:机房角落里放几台设备,想实时盯着温度和湿度,但又不想装一堆客户端软件,更不想把数据往第三方云平台送。我直接给方案定了“以太网温湿度传感器 + 内置Web Server”这条路子,浏览器打开就能看,局域网里任何一台电脑手机都能访问,完全不需要装额外的东西。这套方案在嵌入式物联网场景里特别实用,而且在浏览器、Web Server、以太网这几个关键词背后,其实是整个硬件和网络协议的配合过程。这篇就把我在实际做这个项目时的完整思路、硬件选型、固件实现和填坑记录都写出来,给正准备做类似“传感器上云/上内网”项目的朋友做个参考。
1. 方案定位与设计思路拆解
1.1 一个能开网页的传感器,解决的到底是什么问题
先说需求。机房、仓库、温室大棚、实验室,这些场景普遍存在一个共同痛点:需要监测温湿度,但数据必须在现场看,或者只在内网流转。以前常见的做法是拿个串口屏或者USB转串口接电脑,再装个上位机软件,数据只能在那一台电脑上看。后来流行过一阵WiFi模块+MQTT的方案,数据往云平台推,但很多工业现场和内部机房对数据出网有严格限制,云平台那条路根本走不通。
这时候“传感器直接内置Web Server”的思路就很香了。我做的这个设备本质上就是一块板子:MCU负责读传感器,SPI接一个以太网芯片,再通过RJ45网线连交换机或路由器。设备通电后自动获得一个内网IP,你用浏览器输入这个IP,直接就能看到当前温度、湿度和采集时间。整个过程不依赖任何上位机软件、不依赖云平台、不依赖操作系统,单片机自己把HTTP协议处理了。多台设备就是多几个IP,浏览器标签页一开,所有数据一目了然。
这个方案的杀手锏是跨平台。Windows、macOS、Linux、Android、iOS,只要有浏览器就能看,这在团队协作场景里非常爽。设备维护人员不用在被监测电脑上装软件,也不用学习上位机的操作逻辑,打开网址看数据这个动作谁都会。
1.2 同场竞技:以太网Web Server对比串口、WiFi、蓝牙方案
选型阶段我列过一张对比表,把主流的几种传感器数据传输方案横向拉了一下。这里直接放出来,是我当时做决策的核心依据。
| 方案 | 传输距离 | 布线成本 | 客户端要求 | 数据上云/上内网 | 可靠性 | 适用场景 |
|---|---|---|---|---|---|---|
| 串口(RS232/USB) | 短(15m内) | 低 | 需上位机软件 | 需主机中转 | 中 | 台式机本地调试 |
| 蓝牙BLE | 极短(10m) | 无 | 需配网/App | 需手机中转 | 中 | 个人近距离查看 |
| WiFi(ESP8266等) | 受路由覆盖影响 | 无 | 浏览器/App | 方便,但信号易受干扰 | 中 | 家庭/办公环境 |
| 以太网+Web Server | 100m(标准UTP) | 网线部署 | 仅浏览器 | 直接内网可达 | 高 | 机房/工业现场/长期监测 |
串口方案最大的问题是“数据被绑定在一台机器上”,而且上位机软件通常要买授权或者自己写,工程现场最烦的就是软件环境兼容性。蓝牙方案其实是近距离调试用的,真拿去机房部署,手机连哪个设备还得配对,多设备监测体验很差。WiFi方案看似灵活,但在实际项目中碰到过不少坑:AP信号不稳、设备掉线重连慢、路由节能模式把连接睡了,而且很多工业现场根本不部署无线网络。
以太网方案在这些场景里是最稳的。网线供电和传输一体,100米的覆盖半径在不大的机房和仓库里完全够用。更重要的是以太网通信本身是成熟到不能再成熟的技术,没有无线信号干扰问题,设备该通就通,不该通大概率就是网线没插好或者IP配错了,排查路径非常清晰。内置Web Server之后,前面说的客户端软件成本、跨平台问题全部消失。
1.3 为什么选择硬件协议栈方案:W5500的取舍逻辑
以太网通信在嵌入式上做,其实有两条技术路线。一条是用MCU自带MAC或者裸的PHY芯片,比如STM32F407的MAC+LAN8720,然后在固件里跑LwIP这样的软件协议栈。另一条就是我用W5500这种自带硬件TCP/IP协议栈的芯片,MCU只需要通过SPI往寄存器里读写数据,TCP、UDP、ICMP这些协议全部由芯片自己完成。
W5500方案对单片机来说压力小得让人感动。我用的主控是那颗经典的STM32F103C8T6,也就是大家常说的“蓝丸”芯片,主频72MHz,Flash只有64KB。如果用LwIP,光协议栈代码和缓冲就得吃掉几十KB的Flash和大量RAM,F103C8的资源配置起来非常紧张,还得折腾内存池和零拷贝这些细节。而W5500把TCP/IP协议栈做死在芯片内部,MCU这边只需要维护SPI通信和简单的Socket状态机,Flash空间主要留给业务逻辑就行。
当然这个选择也有代价。W5500内部有独立的TX/RX Buffer,总共32KB,分成8个Socket,每个Socket最大能分16KB收发缓冲区。这个配置做HTTP Server其实绰绰有余,因为我传的数据就是几KB的HTML页面和温湿度数值,一个客户端连接根本不占多少资源。但如果你要做的是一台高并发的Web服务器,那就别指望这点缓存了,那本来就是Linux和Nginx的地盘。工业级传感器Web Server这个特定场景,W5500就是性价比和开发效率的最优解。
2. 硬件选型与电路连接要点
2.1 传感器怎么挑:DHT11、DHT22还是SHT3x
温湿度传感器在项目里是最容易被低估但又最容易翻车的一环。市面上最常见的三款是DHT11、DHT22(AM2302)和SHT3x系列。我第一版用DHT11做的,后面发现精度不够,又换了DHT22,最后批量做的时候用了SHT30。
DHT11的价格最低,但精度说实话比较感人:温度精度正负2摄氏度,湿度精度正负5%RH,而且采样周期要求不低于1秒。它内部用的是电阻式测湿元件和NTC测温元件,响应速度很慢。如果你只是看个大概环境变化趋势,DHT11够用,但如果要用来做设备运行环境合规监测,这个精度又有点说不过去。另外DHT11的单总线时序对延时精度很敏感,我之前在FreeRTOS环境下读DHT11,任务调度偶尔会造成时序漂移,读出来的数据校验就报错。
DHT22把精度提升到温度正负0.5摄氏度、湿度正负2%RH,价格也就贵几块钱,性价比很高。它和DHT11用同样的单总线协议,但数据位定义不一样,DHT22是16位湿度、16位温度,最高位是符号位。要注意的是DHT22的湿度分辨率是0.1%RH,在0到100%RH范围内线性度比DHT11好很多。我最后在样机阶段用了DHT22,总体表现稳定。
SHT3x是I2C接口的数字传感器,精度最高,温度能做到正负0.2摄氏度,湿度正负1.5%RH。它的好处不仅仅是精度,更重要的是I2C数字通信不受时序抖动影响,固件端读取非常稳定。工业场景如果预算允许,建议直接上这一档。我在后面的批量版本里就换成了SHT30,代码改动也不大,因为上层都是一层“读取传感器返回温湿度结构体”的抽象,底层换驱动就行。
2.2 以太网芯片:W5500、ENC28J60、LAN8720怎么选
以太网接口这块,除了W5500,市场上还经常见到ENC28J60和LAN8720。这三者很容易让人晕,我直接说结论。
W5500是自带TCP/IP协议栈的MAC+PHY集成芯片,SPI接口,核心优势是开发简单。主机MCU不需要知道TCP握手、数据包分片这些细节,直接把Socket打开,写数据,读数据就行。它的SPI通信速率最高到80MHz左右,实际跑下来一个HTTP请求的响应速度毫秒级,对传感器这种低频小数据量场景绰绰有余。
ENC28J60是老前辈了,但它严格来说只包含MAC+PHY,没有硬件TCP/IP协议栈,你必须自己在MCU端跑uIP或LwIP。而且它只有8KB的收发缓冲,大一点的IP包就得做分片处理,固件复杂度直接上一个档次。我在老项目上被它折腾过,掉包、缓冲溢出各种问题,所以新项目从来不用。
LAN8720则是纯PHY芯片,连MAC都要MCU自带的以太网控制器来配合,还必须上LwIP软件协议栈。它的性能上限最高,100Mbps,但开发难度也最高,需要配置MAC地址、MDIO管理接口、RMII时序。除非你追求100Mbps的吞吐,否则拿它做传感器Web Server有点杀鸡用牛刀。
我的最终选择是W5500最经典的版本,外加板载网络变压器和RJ45座。选带网络变压器的模块可以避免自己在PCB上画变压器的麻烦,而且不少模块直接集成了12MHz晶振和必要的滤波电容,外围电路基本为零。模块如图书页大小的蓝色小板,出线直接连SPI和电源就能用。
2.3 硬件连接与供电经验
W5500模块和STM32的连接方式已经非常成熟了。标准接法是SPI四根线加两根控制线,我用的引脚分配如下,方便你直接抄。
| 功能 | W5500模块 | STM32F103C8T6引脚 |
|---|---|---|
| SPI时钟 | SCLK | PA5(SPI1_SCK) |
| SPI主机输出/从机输入 | MOSI | PA7(SPI1_MOSI) |
| SPI主机输入/从机输出 | MISO | PA6(SPI1_MISO) |
| SPI片选 | CS | PA4(软件控制) |
| 复位 | RSTN | PA3(软件控制) |
| 中断 | INTN | PA2(外部中断,可选) |
W5500芯片的IO电平兼容3.3V,但片内对5V输入也做了容忍,从5V单片机引脚直连问题不大。STM32F103本来就是3.3V系统,直接连就行,不用加电平转换。供电方面要特别注意,W5500模块上的网络变压器中心抽头需要3.3V供电,如果电源纹波比较大,会直接影响网络信号质量。我实测过用同一个LDO给MCU和W5500供电,当板的系统总电流超过150mA时,网络传输会出现偶发丢包。建议W5500的3.3V单独用一个低噪声LDO,或者至少在主LDO输出端加一个10uF陶瓷电容和一个100uF钽电容做去耦。
DHT22传感器接在PB12上,外部加上一个4.7kΩ上拉电阻到3.3V。单总线协议要求设备空闲时总线保持高电平,没有上拉电阻会直接导致通信失败。SHT30如果要用I2C版本就接PB6(I2C1_SCL)和PB7(I2C1_SDA),地址线ADDR接低电平,从设备地址是0x44。
还有个小细节:电源滤波电容要尽量靠近W5500模块的电源引脚,最好控制在5mm以内。我在第一版PCB上就是因为电容放得远,导致高频噪声滤不掉,Web Server偶发卡顿,查了一天才定位到是电源问题。
3. Web Server固件实现:从底层HTTP到数据页面
3.1 浏览器访问一个IP时,发生了什么
固件层面最核心的其实是搞懂HTTP协议的最小模型。很多人一听“在单片机上写Web Server”就觉得很难,其实拆开看就几个动作。
浏览器在地址栏输入设备的IP,比如192.168.1.100,它会往这个IP的80端口发起一个TCP连接。TCP三次握手完成之后,浏览器会发送一个HTTP请求报文。这个报文长这样:
GET / HTTP/1.1 Host: 192.168.1.100 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Accept: text/html Connection: keep-alive重点是第一行:“GET / HTTP/1.1”是请求行,表示用GET方法请求根路径;后面的是请求头,浏览器会带一堆信息,但对我们来说大部分都是废的。Web Server真正需要关心的就是请求行里的方法(GET/POST)和URL路径(/、/data、/favicon.ico这种)。
单片机端Web Server收到这个请求后,需要回一个HTTP响应报文。响应也分三部分:状态行、响应头、响应体。最小可用的响应是这样:
HTTP/1.1 200 OK Content-Type: text/html; charset=utf-8 Content-Length: 345 Connection: close <html>...页面内容...</html>状态行里的200表示成功,404就是找不到资源。Content-Type告诉浏览器这是HTML文档,charset=utf-8是防止中文乱码的关键,很多人页面中文变乱码就是漏了这行。Content-Length必须填对,浏览器按这个长度读取响应体,填多了浏览器会等超时,填少了数据被截断。
在固件里实现HTTP响应,核心就是拼字符串。MCU收到请求后判断是根路径,就把一个HTML页面字符串通过TCP Socket发送出去,发送完毕后主动关闭TCP连接。整个流程对W5500来说就是几个寄存器的操作:打开Socket、监听端口、接收数据、写入发送缓冲区、发送、关闭。
3.2 固件主循环与TCP Socket状态机
W5500库的写法很多,官方有ioLibrary,但直接拿来做HTTP Server还是要理清状态机。我的主循环架构非常简单清晰,就是“轮询+中断标志”。
while (1) { // 1. 周期读取温湿度传感器,建议最低间隔2秒 if (read_sensor_flag) { temp = read_temperature(); humi = read_humidity(); read_sensor_flag = 0; } // 2. 处理W5500的Socket事件 w5500_loop(); // 3. 其他低优先级任务 systick_handler(); } void w5500_loop(void) { uint16_t len; uint8_t buf[512]; switch (getSn_SR(SOCK_TCPS)) { case SOCK_CLOSED: socket(SOCK_TCPS, Sn_MR_TCP, 80, 0x00); break; case SOCK_INIT: listen(SOCK_TCPS); break; case SOCK_ESTABLISHED: len = getSn_RX_RSR(SOCK_TCPS); if (len > 0) { len = recv(SOCK_TCPS, buf, sizeof(buf)); parse_http_request(buf, len); send_http_response(SOCK_TCPS); close(SOCK_TCPS); } break; default: break; } }这个循环的关键是W5500的Socket状态迁移。Socket刚上电是CLOSED,调用socket()函数创建TCP Socket后变成INIT,调用listen()进入监听状态,有浏览器连上来并完成三次握手后变成ESTABLISHED。此时接收缓冲区里就有HTTP请求数据了,用recv()读出来,解析一下URL路径,然后发送HTTP响应,最后调用close()把连接关掉。
这里有一个很多新手容易踩的坑:close()之后W5500的Socket会进入FIN_WAIT状态,底层TCP会完成四次挥手。如果你马上重新调socket()创建新Socket,可能会因为旧连接还没完全释放而出错。我的做法是close之后延时100ms再做下一步,给W5500一点时间把状态机转完。后来排查到这个问题是因为客户反馈“刷新页面偶尔会失败”,抓包发现是Socket被快速重开时资源还没释放完。
3.3 读取温湿度:单总线时序与数据解析
DHT22读取在固件端也是个经典环节。虽然表面上是两根线,但时序要求非常严格。我用的是标准流程:主机先把总线拉低至少18ms,之后释放并拉高20到40微秒,然后等待DHT22响应。
DHT22响应是一个80微秒的低电平再加一个80微秒的高电平,之后连续输出40位数据。每一位数据的开始都是一个50微秒的低电平,接着是高电平的时间长短区分0和1:高电平持续26到28微秒是0,持续70微秒是1。单片机端要精确测量这个高电平的持续时间,在72MHz的STM32上可以用定时器输入捕获或者简单的Delay循环配合读取GPIO电平。
uint8_t dht22_read_data(float *temperature, float *humidity) { uint8_t data[5] = {0}; uint8_t i, j; // 发送起始信号 DHT_GPIO_MODE_OUT(); DHT_LOW(); delay_ms(20); DHT_HIGH(); delay_us(30); DHT_GPIO_MODE_IN(); // 等待响应 while (GPIO_ReadInputDataBit(DHT_PORT, DHT_PIN)); // 等待低电平 while (!GPIO_ReadInputDataBit(DHT_PORT, DHT_PIN)); // 等待高电平 while (GPIO_ReadInputDataBit(DHT_PORT, DHT_PIN)); // 等待数据起始 // 读取40位数据 for (i = 0; i < 5; i++) { for (j = 0; j < 8; j++) { while (!GPIO_ReadInputDataBit(DHT_PORT, DHT_PIN)); // 等待低电平结束 delay_us(40); if (GPIO_ReadInputDataBit(DHT_PORT, DHT_PIN)) { data[i] |= (0x80 >> j); } while (GPIO_ReadInputDataBit(DHT_PORT, DHT_PIN)); // 等待高电平结束 } } // 校验 (前4字节之和取低8位等于第5字节) if ((uint8_t)(data[0] + data[1] + data[2] + data[3]) != data[4]) { return 1; // 校验失败 } *humidity = ((data[0] << 8) | data[1]) / 10.0f; *temperature = ((data[2] << 8) | data[3]) / 10.0f; return 0; }这个代码里最敏感的地方是delay_us(40)。如果用的是HAL库的HAL_Delay,最小精度是1ms,根本做不了微秒级延时。我当时用的是正点原子风格的Delay函数,用SysTick做微秒延时,实际误差在2微秒以内,满足DHT22的时序要求。如果你在RTOS环境里跑这个代码,记得在这段函数里加临界区保护,否则任务抢占会让时序完全乱掉。DHT22读取失败的时候,返回一个错误标记,Web Server端就显示“--”而不是一个错误的数值,这个习惯能省掉很多排查时间。
3.4 页面设计与数据自动刷新
温湿度数据显示页面不用花哨,但要做到两个点:自动刷新、一眼看懂。我第一版用的是最古老但最稳的meta refresh方式,在HTML的head里加一行:
<meta http-equiv="refresh" content="5">这样浏览器每5秒自动重新加载一次页面,MCU端每次收到GET请求时,现读取一次温湿度传感器,动态拼到HTML里返回。这个方案实现成本最低,但对嵌入式设备来说有一个问题:每次刷新都是全量页面,一个页面大约1.5KB,每5秒一次,长时间运行下来MAC层虽然没问题,但浏览器端的加载感比较明显,而且如果无线网络环境差一点,整页刷新反而容易白屏。不过在有线以太网W5500的场景下,这个缺点其实不明显,我在项目里实际用的就是这个方案,稳定跑了一周都没掉过链子。
第二版我实现了更优雅的方式:页面框架静态加载一次,之后用JavaScript的XMLHttpRequest定时去请求一个“/data”接口,这个接口返回纯文本温湿度值。这样做的好处是页面框架不重新加载,只有数据部分每10秒轮询一次,用户体验更好,而且数据请求的HTTP响应体更小。单片机端需要解析不同的路径:GET / 返回完整HTML页面,GET /data返回“25.3, 60.2”这样的字符串。JavaScript那边解析字符串后直接更新DOM元素的innerText,比正则解析JSON省事多了。
function refreshData() { var xhr = new XMLHttpRequest(); xhr.open("GET", "/data", true); xhr.timeout = 2000; xhr.onload = function() { if (xhr.readyState === 4 && xhr.status === 200) { var parts = xhr.responseText.split(","); document.getElementById("temp").innerText = parts[0] + " ℃"; document.getElementById("humi").innerText = parts[1] + " %RH"; } }; xhr.send(); } setInterval(refreshData, 10000);这里要特别提醒一个W5500并发处理的问题。当浏览器发起/data请求时,如果上一次的页面请求连接还没完全关闭,W5500的8个Socket够不够用?放心,够。一次只开一个Socket监听80端口,收到请求后可以用同一个Socket响应并关闭。一个浏览器同时段只会维持一两个TCP连接到同一台设备,8个Socket理论上同时支持7个客户端并发都没问题。但要注意同一时刻只能有一个Socket在listen()状态,否则W5500会报Socket冲突,这个坑我在多Socket配置时踩过一次。
4. 实际操作流程与配置步骤
4.1 准备清单:从硬件到软件的完整列表
在实际动手之前,我把整个操作流程整理成了一份清单,照着准备就不会漏东西。
硬件方面我需要:STM32F103C8T6最小系统板一块、W5500以太网模块一块、DHT22温湿度传感器一个(或DHT11/SHT30)、标准网线一根、路由器或交换机一台、5V/1A以上的USB电源一个。软件方面则需要STM32的编译工具链(我用的是Keil MDK)、W5500的驱动库(官方ioLibrary或者自己写的简化版)、任意一款现代浏览器(Chrome、Edge或者Firefox都行)。如果你打算直连电脑不经过路由器,还需要手动配置电脑的IP地址为同一网段,比如设备设192.168.1.100,电脑就设192.168.1.50。
这块特别注意一点:W5500的官方库里面有很多针对不同型号的宏定义,用F103的时候记得把WIZCHIP和WIZCHIP_IO_MODE这些宏配置正确,我用ST的HAL库做SPI底层对接时,把Spi读写函数封装成WIZCHIP_READ_BUF和WIZCHIP_WRITE_BUF,再和官方库挂接上就完事了。如果连官方库都懒得理,只写一个精简驱动也完全够用,毕竟Web Server只需要Socket的open/listen/recv/send/close这几个核心API。
4.2 网络参数设置:静态IP和DHCP的取舍
网络配置这块我的建议很简单:固定IP。HTTP Server设备在网络里是一个服务端身份,IP地址应该保持稳定。如果你让设备用DHCP自动获取地址,路由器一重启,设备可能就换IP了,原来浏览器收藏夹里的地址就失效了,这对运维来说非常痛苦。我做的设备支持两种模式,但默认是静态IP,比如192.168.1.100、网关192.168.1.1、子网掩码255.255.255.0。
静态IP的初始化配置在固件里就三行代码:
// 配置W5500为静态IP uint8_t ip[4] = {192, 168, 1, 100}; uint8_t gw[4] = {192, 168, 1, 1}; uint8_t mask[4] = {255, 255, 255, 0}; setSHAR(mac); // 设置MAC地址 setSIPR(ip); // 设置IP地址 setGAR(gw); // 设置网关 setSUBR(mask); // 设置子网掩码MAC地址需要自定,但要注意不能和局域网内其他设备的MAC冲突,也不要乱选到广播组播地址段。我习惯用00:08:dc:xx:xx:xx这种前缀,WIZnet官方模块的MAC前缀也是00:08:dc。自己批量做设备时,建议在程序里保存一个基于序列号的MAC尾号,避免每台设备MAC雷同。虽然同一个局域网里两台设备MAC相同会导致路由器ARP表抖动、网络时断时续,这种问题排查起来比配置问题难多了。
DHCP模式在我这个项目里作为备选实现。W5500 ioLibrary里带DHCP客户端,跑起来之后会自动从路由器拿IP。但有个坑:DHCP客户端需要周期性地发送租约续期请求,否则IP会被路由器收回。如果主循环里长时间卡在HTTP请求处理上,DHCP续期延时了,IP过期后路由器的ARP表会清掉对应条目,设备就“失联”了。这个场景在长时间运行的设备上尤其明显,我有一台测试机就是跑到大概24小时左右突然失联,日志查了才发现是DHCP租约没续上。后来直接把该模式设为非默认选项,只在诊断时用。
4.3 浏览器访问和验证效果
配置完固件并烧录进STM32后,整个系统的验证流程一共就四步。
第一步,给设备供电,插上网线。看W5500模块上的Link指示灯是否亮起,这个灯亮表示物理链路是通的,网线或交换机端口已经协商成功。第二步,在浏览器里输入设备IP地址,比如http://192.168.1.100,然后回车。如果页面正常显示温度和湿度,说明Web Server和传感器读取链路都是通的。第三步,打开电脑的命令行,使用ping工具对设备IP执行连通性测试。丢包率应该是0,如果出现明显丢包,优先检查网络变压器附近的电源滤波,其次检查网线质量。第四步,用浏览器开发者工具(按F12)的Network面板查看请求列表,确认页面加载请求和/data请求都返回200状态码。这一步属于进阶验证,能帮你排除很多“页面看着正常但数据不更新”的隐形问题。
浏览器这块我还想多说一句,项目上线以后最好留一个固定浏览器作为“标准客户端”。我之前遇到过用某个安全浏览器访问设备页面时,页面被“智能拦截”提示风险,原因是设备HTTP响应里没有X-Frame-Options头,被一些浏览器误判为潜在不安全页面。后来我干脆在设备页面里加入了X-Frame-Options: DENY响应头,情况才好转。这个问题的核心不是设备本身不安全,而是浏览器策略越来越严格。
4.4 数据对外提供API:给后续监控平台留接口
做Web Server不只是给人看,更多时候是为了给别的系统提供数据。我在设备上额外实现了一个/api/temperature和/api/humidity的GET接口,返回格式是纯文本数值。这样内网监控平台通过HTTP请求就能把数据抓到自己的数据库,比如用Python的requests库每10秒拉一次,存到InfluxDB或者MySQL里做历史曲线。
API接口的实现就是前面说的URL路径解析,在parse_http_request函数里用strncmp判断路径:
if (strncmp(req_path, "/api/temperature", 16) == 0) { snprintf(payload, sizeof(payload), "%.1f", temperature); send_http_response(200, "text/plain", payload); } else if (strncmp(req_path, "/api/humidity", 13) == 0) { snprintf(payload, sizeof(payload), "%.1f", humidity); send_http_response(200, "text/plain", payload); } else { send_http_response(404, "text/plain", "Not Found"); }很多人觉得在单片机上做API接口很麻烦,其实HTTP协议本身就是文本协议,只要做好字符串解析,API和网页本质上没有区别。设备端的固件里甚至不需要保存任何历史数据,所有历史记录都由更上层的监控平台去存。这种设计有一个好处:设备固件保持简单,设备和平台解耦,以后哪怕把监控平台换了,设备端代码一行都不用改。这个思路对长期维护的系统特别重要,传感器设备本身的职责就是把实时数据准确送出去,至于数据怎么存、怎么展示,那是平台层的事。
5. 常见问题与排查技巧实录
5.1 高频故障对照表
做这类项目最容易遇到的问题其实不多,我把实际工程里出现的和高频网络讨论里的典型问题汇总成了一张排查表,后面遇到相同情况直接照着查就行。
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 浏览器打不开页面 | 设备IP不在同一网段 | 命令行看本机IP,对比设备IP | 修改本机IP或设备IP到同一网段 |
| 浏览器一直转圈不响应 | TCP连接建立不了,Socket状态卡死 | 查看设备日志或串口输出Socket状态 | 给close()后加延时,确保Socket彻底释放 |
| 页面打开但温湿度显示-- | 传感器读取失败 | 代码里打印传感器错误码 | 检查上拉电阻、接线、时序参数 |
| 数据能显示但一旦刷新就掉线 | W5500快速开关Socket导致资源未释放 | 连续多次刷新页面观察Socket状态 | close后加100ms延时再重新listen |
| 中文乱码 | 响应头缺少charset=utf-8 | F12看响应头信息 | 在Content-Type中补充charset=utf-8 |
| ping不通设备,但Link灯正常 | IP/MAC配置错误或ARP缓存问题 | 电脑上ping完后查arp -a表 | 确认MAC和IP配置正确,清ARP缓存重试 |
| 设备运行一天后失联 | DHCP租约过期(如果开了DHCP) | 查路由器的DHCP客户端列表 | 改用静态IP |
| 网页偶尔加载很慢 | W5500电源噪声导致丢包重传 | 观察交换机端口错误计数 | 加强电源滤波,LDO单独给W5500供电 |
这份表格里我标出来的第一条和第六条是现场最容易遇到又不能互相替代的。第一种是纯粹的网络配置不对,电脑和设备不在同一网段,访问当然失败。第六种是物理链路通但逻辑不通,可能性就多了:设备MAC冲突、IP冲突、ARP缓存污染,逐一排查耗时比较长。我自己的排查习惯是,先ping,ping不通就查ARP表,ARP表看不到设备MAC就查物理链路,物理链路没问题再查设备侧配置,按照这个顺序很少有人能难倒你。
5.2 那些不写在文档里的避坑经验
最后分享一些这个项目真正磨人的地方,这些经验是我在调试过程中一点一点攒出来的,常规教程里基本不会提到。
第一个坑是关于TCP连接关闭方式的。HTTP/1.1默认是keep-alive长连接,浏览器打开页面后会保持连接,然后询问服务器是否需要复用。如果设备端简单粗暴地close(),浏览器可能会报连接被重置的错误。后来我直接把响应头里的Connection: close加上,明确告诉浏览器“我给你发完响应就关连接”,这个报错就消失了。看似很小的一个头字段,其实是服务器端主动管理连接生命周期的规范做法。
第二个坑是W5500的中断引脚问题。W5500的INTN引脚在收到数据、Socket状态变化时都会拉低。我最早把它接到单片机的EXTI中断上,每次中断都要读W5500的中断源寄存器、Socket中断寄存器来确认到底是谁触发的,代码逻辑绕来绕去,反而容易漏处理。后来我改成在主循环里定时轮询Socket的接收数据寄存器大小,虽然“浪费”了几毫秒,但逻辑清晰了很多,也没有丢过数据。对低速传感器场景,轮询明显比中断更抗造。
第三个坑关于页面转义。如果你要把设备型号、固件版本这些字符串拼到HTML页面里返回,记得先做HTML转义。有一个已知场景是设备命名时带了“&”字符,结果HTML解析把它当成实体引用的起始符号,导致页面显示异常。这个坑在Web领域很经典,但嵌入式开发者容易忽略,毕竟大家平时处理的是二进制数据,哪会想到文本还要转义。
第四个坑是供电。我给设备做过一次最简单的长时间稳定性测试,用手机充电器5V供电,结果运行半小时后设备开始间歇性失联,抓包发现是有CRC错误的废包出现。换了实验室稳压电源之后一切正常。这个项目告诉我:嵌入式网络设备的电源设计必须留足余量,至少要有20%以上的电流余量,并且输出端要加足够容量的滤波电容。W5500这类带网络变压器的PHY芯片,瞬态电流需求比静态高很多,电源响应不够快就掉包。
最后分享一个再实用不过的经验:遇到界面卡住先别改代码,开浏览器F12看Network面板的请求状态,十有八九能一步定位问题。HTTP请求是明文的,什么时候发出去、什么时候收到响应、响应是什么状态码,都清清楚楚写在浏览器里。搞嵌入式网络开发,进度一大半在抓包工具里,剩下的才是单片机代码的问题。