STM32+ENC28J60+LWIP+AJAX:从零搭建嵌入式Web控制台
2026/9/7 9:30:06 网站建设 项目流程

简介:基于STM32、ENC28J60与LWIP协议栈的嵌入式智能家居项目,源自本科毕业设计,面向物联网应用开发、STM32网络通信及嵌入式Web技术学习者。系统利用AJAX实现浏览器与单片机的轻量级数据交互,网页控制板载LED开关,并实时刷新STM32的时间和温度传感器数值;HTML页面用记事本编写后与图片一并转码存储于单片机内部,完整呈现了资源受限设备上搭建动态网页服务器的可行方案。压缩包共567个文件,约10.88MB,其中126个h头文件与112个c源码文件构成工程主体,覆盖STM32标准外设驱动、LWIP协议栈移植和CGI处理逻辑;另有uvproj工程文件、hex固件、网页html及jpg/gif图片素材、readme说明等,编译中间文件也一并保留,便于直接打开工程查看或重新烧录。目前已有1165人学习下载,适合希望从零复现类似智能家居、或研究小内存设备如何用AJAX局部刷新替代整页刷新的开发者。资料内还含有makefsdata转换工具及bat批处理脚本,可帮助理解网页转数组的过程,提升动手调试效率。 从STM32入门物联网,很多人第一反应就是上ESP8266走WiFi,图省事。但如果你想让设备稳定跑在局域网里、顺便把TCP/IP协议栈的底层逻辑吃透,那STM32+ENC28J60+LWIP这套组合,绝对是一个绕不开的经典方案。再加上AJAX做网页动态交互,用浏览器就能直接控制继电器、读温湿度,整屋设备在一个页面上搞定,体验非常接近手机App。这篇博文我会把整个项目的选型思路、移植过程、HTTP服务搭建和AJAX联调细节一次讲透,适合正在做毕设、想做智能家居网关或者单纯想啃LWIP的开发者参考。

1. 选型复盘:为什么这四样东西凑在一起反而很能打

1.1 不是没考虑过W5500和ESP8266,但ENC28J60有它的位置

我做这个项目之前,先对比了市面上几种常见联网方案,列表如下。

方案接口协议栈实现速率成本适合场景
ENC28J60SPI软件协议栈(LWIP/uIP)10Mbps最低学习、低成本有线网关
W5500SPI硬件TCP/IP协议栈10/100Mbps快速开发、稳定性要求高
ESP8266UART/SPI内置协议栈WiFi无线传感器、遥控开关

最终选了ENC28J60,原因很直接:它只提供MAC+PHY,TCP/IP全要跑在MCU里,这样一来你会被迫去接触LWIP的RAW API、pbuf、netif这些东西。虽然折腾,但弄懂以后再看其他网络方案,基本是降维打击。W5500确实省心,但所有协议都在芯片内部,你学到的东西很有限。ESP8266则更适合做无线终端,作为以太网网关来用反而不太合适。

成本上,ENC28J60模块十几块钱,比W5500便宜将近一半,对DIY来说性价比很高。ST官方甚至都没有给F103出以太网MAC外设,所以ENC28J60几乎是低成本有线入网的最优解。

1.2 裸机+RAW API还是上RTOS?资源说了算

LWIP有RAW API、Netconn API和Socket API三种编程接口,很多人第一次移植就卡在这。我的建议是:如果你用的是F103C8T6这种只有20KB RAM的芯片,老老实实用裸机+RAW API,别上RTOS。

RAW API是事件驱动模式,TCP连接建立、数据到达都是通过回调函数通知你,不需要操作系统的线程调度,内存开销也最小。而Netconn和Socket API虽然写起来像PC上的socket,但需要操作系统的信号量、邮箱等机制支撑,对MCU资源要求高一个量级。我实测过F103C8T6跑LWIP裸机版本,RAM占用大约在15KB左右,已经比较紧张了,再塞一个RTOS和Netconn层,很容易爆内存。

1.3 网页交互选AJAX,而不是传统的CGI整页刷新

嵌入式Web服务器常见的交互方式有两种:一种是每次请求都由服务器生成完整HTML页面,浏览器整页刷新,参考老式CGI思路;另一种就是用AJAX,页面首次加载完静态HTML+JS,后续只通过XMLHttpRequest向后台请求数据,局部更新页面内容。

我选AJAX的原因很实际。MCU的计算资源本来就紧张,整页刷新意味着每次要动态拼接大量HTML字符串,网络传输的数据量也大。换成AJAX之后,服务器只需要返回几十字节的JSON数据,比如当前温度、继电器状态,前端JS拿到的JSON解析后更新页面元素就行。设备状态还可以用定时器每秒自动拉取,交互体验和手机App几乎没有差别。

2. 硬件连接与LWIP配置:几个关键设计细节

2.1 ENC28J60接线看起来简单,但细节比想象的多

我用的是STM32F103C8T6最小系统板加蓝板ENC28J60模块,接线方式如下:

  • PA5 → SCK(SPI1时钟)
  • PA6 → MISO(SPI1主机输入)
  • PA7 → MOSI(SPI1主机输出)
  • PA4 → CS(片选,也可以用任意普通GPIO模拟)
  • PB0 → INT(ENC28J60中断输出,必须接,否则收包靠轮询会非常难受)
  • 3.3V和GND,注意模块必须3.3V供电,接5V大概率烧芯片

有几点必须提醒:第一,晶振必须是25MHz,且模块上要有网络变压器和RJ45座,常见的HR911105A模块已经集成好了;第二,SPI通信速率刚开始别拉太高,先用2MHz调通逻辑,再逐步提升到8MHz、10MHz;第三,初始化之后一定要读一下ENC28J60的版本寄存器(地址0x01),正常情况返回0x00或者0x06,如果你读回来是0xFF,基本可以断定SPI通信有问题,先查接线和供电。

2.2 LWIP内存配置:三个宏决定你稳不稳

LWIP运行需要自己管理内存,所有关键参数集中在lwipopts.h里。我实际调稳定的配置是这样:

#define MEM_ALIGNMENT 4 #define MEM_SIZE (10 * 1024) #define MEMP_NUM_PBUF 10 #define MEMP_NUM_TCP_SEG 10 #define MEMP_NUM_TCP_PCB 5 #define PBUF_POOL_SIZE 20 #define TCP_MSS 1460 #define TCP_WND (2 * TCP_MSS) #define TCP_SND_BUF (2 * TCP_MSS) #define LWIP_DHCP 0

这里最影响稳定性的三个参数是PBUF_POOL_SIZE、TCP_WND和TCP_SND_BUF。PBUF_POOL_SIZE负责底层收包缓冲池,小于10的话一旦网络数据稍多就会丢包;TCP_WND是接收窗口,TCP_SND_BUF是发送缓冲,对F103这种小内存芯片来说,这两个值不能贪大,否则内存瞬间告急,表现在现象上就是系统莫名其妙死机。我建议总内存占用不要超过16KB,留点余量给应用层。

另外直接关掉DHCP,用静态IP。

#define LWIP_DHCP 0

嵌入式设备固定IP能省掉DHCP协议的开销,也避免启动时等待分配IP。我的开发板固定为192.168.1.200,子网掩码255.255.255.0,网关192.168.1.1。

2.3 LWIP移植时最容易被忽略的定时任务

LWIP裸机版有一个核心机制:它内部很多协议状态机是靠周期性的超时函数推进的。你在主循环里必须反复调用这个函数:

while (1) { enc28j60_poll(); // 轮询网络接口,有数据就交给LWIP sys_check_timeouts(); // LWIP定时调度,必需! application_task(); // 你自身的业务逻辑 }

我遇到过有人移植后TCP连接建立不了,折腾半天才发现是sys_check_timeouts没调用。LWIP里头TCP的三次握手、重传计时、ARP表老化全都要依赖这个函数定期驱动,漏了它等于整个协议栈时间停止了。

2.4 ENC28J60底层驱动与LWIP对接的核心逻辑

LWIP本身不直接操作硬件,它通过netif结构体调底层的函数,你要实现三个关键函数:

err_t low_level_init(struct netif *netif); err_t low_level_output(struct netif *netif, struct pbuf *p); void low_level_input(struct netif *netif);

low_level_init负责初始化ENC28J60的MAC地址、打开接收过滤器,并将netif->state指向设备句柄;low_level_output把LWIP传来的pbuf链表里的数据,通过SPI写入ENC28J60的发送缓冲区并触发发送;low_level_input则在中断或轮询发现收到以太网帧后,从ENC28J60的接收缓冲区读出数据,封装成pbuf,最后调用netif->input把数据喂给协议栈。

这里最容易写错的地方在pbuf的理解。LWIP处理网络数据都是用pbuf,不一定是一整块连续内存,可能是一个链表。底层输出时一定要遍历整个pbuf链表,逐段通过SPI发送,不能只发第一个节点;底层接收的时候,也要注意pbuf的ref计数,用完之后调用pbuf_free释放。

3. 从SPI点亮到AJAX联动:实操过程全记录

3.1 第一步:先用SPI把ENC28J60点亮

无论后端设计多少功能,硬件通信必须先打通。用STM32CubeMX生成SPI1初始化代码,配置主机模式、8位数据、MSB先行,速率先选2Mbps。然后写一个最基础的初始化与ID读取函数。

uint8_t enc28j60_read_ver(void) { uint8_t cmd = ENC28J60_READ_CTRL_REG | 0x01; spi_cs_low(); spi_read_write_byte(cmd); uint8_t ver = spi_read_write_byte(0xFF); spi_cs_high(); return ver; }

正常识别后,串口打印版本号。我调试时见过0x00和0x06两种返回值,都是正常的,0xFF才说明读失败。这一步通了,后面LWIP移植才有意义。

3.2 第二步:移植LWIP到裸机工程

LWIP源码建议直接拉官方稳定版(我用的2.1.x)。你需要把src目录下的core、netif、port加入工程,然后新建一个你的网卡驱动文件enc28j60_glue.c,实现上面说的三个低层函数。接着在main.c里完成netif注册:

struct netif g_netif; ip4_addr_t ip, mask, gw; IP4_ADDR(&gw, 192, 168, 1, 1); IP4_ADDR(&ip, 192, 168, 1, 200); IP4_ADDR(&mask, 255, 255, 255, 0); netif_add(&g_netif, &ip, &mask, &gw, NULL, low_level_init, tcpip_input); netif_set_default(&g_netif); netif_set_up(&g_netif);

这里注意,裸机RAW API模式下,最后一个参数可以是tcpip_input也可以是你自己的转发函数。我用的是直接netif->input方式,收发都在主循环完成,不加线程。正确移植后,插上网线、设置电脑IP同网段,立刻能ping通板子。

3.3 第三步:手写一个极简HTTP服务器

LWIP自带httpd功能,但一般需要netconn API配合。我既然用RAW API,就自己写了一个很精简的HTTP解析器。核心思路是:在TCP回调中接收数据、解析请求行、按URL分发处理。

先创建一个TCP监听PCB:

struct tcp_pcb *pcb = tcp_new(); tcp_bind(pcb, IP_ADDR_ANY, 80); pcb = tcp_listen(pcb); tcp_accept(pcb, http_accept_cb);

然后在http_recv_cb回调里处理收到的HTTP请求:

err_t http_recv_cb(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) { if (p == NULL) { tcp_close(tpcb); return ERR_OK; } // 将收到的数据拷贝到应用缓冲区 tcp_recved(tpcb, p->tot_len); http_parse_request(tpcb, p); pbuf_free(p); return ERR_OK; }

http_parse_request内部读取缓冲区里的第一行,判断是GET还是POST。我这边全部用GET,方便调试。拿到类似“GET /api/status HTTP/1.1”的内容后,用字符串匹配判断路径,执行对应的逻辑。

在这里提醒一个关键点:Ajax请求头里可能会带Keep-Alive,但嵌入式服务器最好每次响应都带Connection: close,处理完一个请求直接关闭TCP连接。否则一个连接占着PCB资源不释放,多刷新几次页面,连接就被占满了,新请求进不来。

3.4 第四步:设计AJAX接口,返回JSON不拼HTML

我的API设计得很简单,就三个端点:

  • GET / → 返回整个控制页面(HTML+JS)
  • GET /api/status → 返回温湿度和灯光状态
  • GET /api/led_ctrl?state=1 → 控制继电器,返回执行结果

C端处理/api/status时,把DHT11读出来的温度湿度、GPIO电平状态拼成JSON:

char resp[128]; snprintf(resp, sizeof(resp), "HTTP/1.1 200 OK\r\n" "Content-Type: application/json\r\n" "Connection: close\r\n" "Content-Length: %d\r\n" "\r\n" "{\"temp\":%d.%d,\"hum\":%d.%d,\"led\":%d}", len, temp_int, temp_dec, hum_int, hum_dec, led_state);

注意Content-Length必须精确等于JSON的实际长度,否则浏览器会一直等后续数据,导致请求挂起。我踩过这个坑,页面白屏,控制台显示pending状态,就是Content-Length算错了。

网页端的AJAX请求,写起来也很简洁:

<script> function loadStatus() { fetch('/api/status') .then(res => res.json()) .then(data => { document.getElementById('temp').innerText = data.temp; document.getElementById('hum').innerText = data.hum; document.getElementById('led').innerText = data.led; }); } function setLed(state) { fetch('/api/led_ctrl?state=' + state) .then(res => res.json()) .then(() => loadStatus()); } setInterval(loadStatus, 1000); </script>

页面文件我用一个小工具打包成C语言数组,编译进固件。浏览器访问开发板IP时,HTTP服务器把这个数组原样返回,浏览器解析成HTML和JS,之后所有数据交换都走AJAX。

3.5 第五步:局域网内实际验证

把开发板、路由器、电脑联网后,浏览器打开http://192.168.1.200,页面加载出来以后,每秒会自动刷新一次温度和湿度数值。点页面上的按钮,继电器嗒的一声吸合,LED状态马上从0变1,整个过程不需要刷新页面。这就算完成了最基本的智能家居控制流程。

4. 调试实录:五个最常见的翻车现场与解决办法

4.1 ENC28J60读版本号全0xFF,SPI通信不通

曾遇到过模块怎么初始化都读不到数据,排查下来是杜邦线太长,加上SPI速率太高信号变形。解决方法是把SPI时钟降到1MHz,换成短线,重新读取版本号就正常了。所以只要遇到通信问题,第一件事先降SPI速率排查。

4.2 复位引脚电平不对,模块上电不工作

还有一次是ENC28J60完全不响应,后来发现复位引脚一直被拉低,查看原理图发现模块上的复位电路没焊,我直接飞线接到STM32的PB1,初始化时拉低再拉高,问题解决。使用模块时先看原理图,确认复位引脚是悬空还是需要外部驱动。

4.3 ST-Link连不上芯片,报target not found

这个报错我遇到过好几次,原因包括芯片SWD引脚被重新配置成普通GPIO、芯片进入了低功耗停止模式、SWD线太长干扰。解决办法是按住板子复位键,然后点击IDE下载,在开始擦除瞬间松开复位。如果还不行,检查BOOT0引脚,把它拉高强制从系统存储器启动,再用ST-Link Utility擦除整个Flash,恢复SWD功能。

4.4 浏览器请求挂起、AJAX一直pending

八成是HTTP响应格式问题。除了Content-Length必须精确外,还要注意响应头结尾的\r\n\r\n不能丢。我一开始手写响应时漏了一个换行,浏览器认为头部没有结束,一直等待,页面就卡住了。建议用串口打印实际发送的HTTP响应,对照HTTP协议检查。

4.5 设备跑几个小时就死机,必须断电重启

这个问题最隐蔽,也最折磨人。后来排查发现是我在定时器中断里直接调用了tcp_write,而主循环同时在执行sys_check_timeouts,两个执行流同时对TCP控制块操作,偶尔会踩踏内存。LWIP的API函数都不是中断安全的,全部要在主循环上下文调用。加上看门狗以后,就算程序跑飞也能自动恢复,至少不用每次都手动断电了。

我用这套方案完成了家里客厅的灯光控制,又加了DHT11温湿度监测和一路门磁传感器。整个项目做下来,最大的体会是底层的选型不能只看性能参数,更要看你的目标是什么。我在这套方案里把SPI、以太网帧、TCP握手、HTTP协议整个链路都摸了一遍,之后再去看那些用现成模组的项目,已经能理解它们背后在做什么了。如果你也在做类似的物联网项目,建议按这个顺序来:先点亮SPI通信,再移植LWIP跑通ping,然后写HTTP服务器,最后加AJAX页面,一步一层,翻车的概率会小很多。最后分享一个复盘时发现的扩展方向,这套HTTP+AJAX的框架加上POST方法和简单的JSON解析,就能做成一个支持远程设置参数的设备管理后台,相比现在只能手动改代码烧固件要方便得多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询