1. 项目概述:STM32+ENC28J60构建轻量级Web服务器
在嵌入式物联网开发中,如何让资源受限的MCU设备具备网络接入能力一直是个经典课题。今天要分享的是基于STM32F103ZET6和ENC28J60以太网模块的轻量级Web服务器实现方案,这个组合特别适合需要远程监控但预算有限的应用场景。
整个系统的核心价值在于:
- 硬件成本极低:STM32F103ZET6(俗称"大蓝板")和ENC28J60模块合计成本不超过50元
- 资源占用少:UIP协议栈仅需5KB RAM,让Cortex-M3这类低端MCU也能跑TCP/IP协议
- 开发周期短:从硬件连接到网页访问,完整实现不超过3天工作量
- 可扩展性强:基础框架搭建好后,可快速接入传感器数据或增加控制功能
我曾用这套方案为某农业大棚项目实现了环境监测系统,稳定运行两年多未出现异常。下面将从硬件选型到代码实现,完整还原这个方案的构建过程。
2. 硬件设计与连接方案
2.1 核心器件选型解析
STM32F103ZET6特性挖掘
这款72MHz主频的Cortex-M3芯片虽然现在看起来性能普通,但其外设资源对网络应用非常友好:
- SPI接口:最高18MHz时钟,满足ENC28J60的通信需求
- GPIO中断:用于处理网络模块的中断信号
- 64KB RAM:足够承载UIP协议栈和应用程序
- SWD调试接口:方便问题排查
实际使用中发现其GPIO驱动能力较强,直接驱动LED指示灯无需额外三极管,这点在紧凑型设计中很实用。
ENC28J60模块的实战考量
选择这个老款以太网控制器主要基于:
- SPI接口:节省IO资源(相比并行接口的DM9000)
- 3.3V供电:与STM32电平匹配
- 内置MAC+PHY:单芯片解决方案
- 市场存量:虽然性能一般(10Mbps),但货源充足价格稳定
注意要选择带网络变压器的模块版本(如下图),否则需要外接HR911105A这类网络接口模块。
关键提示:市场上有些ENC28J60模块省略了25MHz晶振,使用时会出现链路不稳定的情况,务必确认模块上有这个晶振!
2.2 硬件连接细节与避坑指南
引脚连接方案
plaintext复制STM32F103ZET6 <--> ENC28J60
PA5(SPI1_SCK) <--> SCK
PA6(SPI1_MISO) <--> SO(MISO)
PA7(SPI1_MOSI) <--> SI(MOSI)
PA4(普通GPIO) <--> CS
PB0(EXTI0) <--> INT
3.3V <--> VCC
GND <--> GND
容易出错的连接点
- SPI时钟相位:ENC28J60要求CPOL=0, CPHA=0模式
- 中断引脚:必须配置为下降沿触发,模块默认低电平有效
- 片选信号:普通GPIO模拟即可,但响应速度要快
- 电源滤波:建议在模块VCC附近加100nF+10μF电容组合
实测中发现,如果SPI时钟超过8MHz,通信失败率会明显上升。建议初始化时先设置为4MHz,稳定后再尝试提升。
3. 软件开发环境搭建
3.1 工具链配置全流程
Keil MDK关键配置
- 安装STM32F1 Device Family Pack
- 在Options for Target中:
- Target页:勾选"Use MicroLIB"(节省资源)
- C/C++页:添加UIP源码路径
- Debug页:选择ST-Link调试器
- 推荐开启"Optimization Level 2"平衡性能与代码大小
STM32CubeMX生成代码
配置步骤:
- 选择STM32F103ZE型号
- 开启SPI1(Full-Duplex Master模式)
- 配置PA4为GPIO_Output
- 配置PB0为EXTI0中断
- 生成代码时选择"Generate peripheral initialization as a pair of .c/.h files"
经验分享:CubeMX生成的SPI初始化代码可能需要手动修改:
c复制hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; // CPOL=0 hspi1.Init.CLKPha = SPI_PHASE_1EDGE; // CPHA=0
3.2 UIP协议栈移植要点
从GitHub获取官方uip源码后,需要重点关注以下文件:
uip/uip.c- 协议栈核心uip/uip_arp.c- ARP协议处理uip/uipopt.h- 关键参数配置
必须修改的配置项:
c复制#define UIP_CONF_MAX_CONNECTIONS 2 // 最大连接数
#define UIP_CONF_BUFFER_SIZE 600 // 缓冲区大小
#define UIP_CONF_BYTE_ORDER LITTLE_ENDIAN
#define UIP_CONF_LOGGING 0 // 关闭日志节省资源
4. 核心代码实现解析
4.1 ENC28J60驱动开发
底层SPI通信封装
c复制void ENC28J60_WriteBuffer(uint8_t* buf, uint16_t len) {
SPI1_CS_LOW();
SPI1_ReadWriteByte(ENC28J60_WRITE_BUF_MEM);
while(len--) {
SPI1_ReadWriteByte(*buf++);
}
SPI1_CS_HIGH();
}
这个函数体现了三个优化点:
- 使用指针传递减少内存拷贝
- 单次片选操作完成多字节写入
- 内联汇编实现高速SPI传输(在STM32F1上可达8MHz)
中断处理优化
c复制void EXTI0_IRQHandler(void) {
if(EXTI_GetITStatus(EXTI_Line0) != RESET) {
ENC28J60_InterruptFlag = 1; // 设置标志位
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
在主循环中检查标志位而非直接处理中断,避免复杂操作导致中断阻塞。
4.2 UIP协议栈集成
定时器配置
c复制void TIM2_IRQHandler(void) {
if(TIM_GetITStatus(TIM2, TIM_IT_Update)) {
uip_periodic(); // 处理周期性任务
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
使用TIM2每100ms触发一次uip_periodic()调用,用于ARP缓存更新等后台任务。
IP地址设置技巧
c复制uip_ipaddr_t ipaddr;
uip_ipaddr(ipaddr, 192, 168, 1, 100);
uip_sethostaddr(ipaddr);
uip_ipaddr(ipaddr, 192, 168, 1, 1);
uip_setdraddr(ipaddr); // 默认网关
uip_ipaddr(ipaddr, 255, 255, 255, 0);
uip_setnetmask(ipaddr); // 子网掩码
建议将IP配置放在单独的头文件中,方便不同环境切换。
4.3 HTTP服务器实现
请求解析优化
c复制if(strncmp((char*)uip_appdata, "GET ", 4) == 0) {
char* url = (char*)uip_appdata + 4;
url = strtok(url, " "); // 提取URL路径
if(strcmp(url, "/") == 0) {
send_html(index_html); // 首页
} else if(strcmp(url, "/data") == 0) {
send_json(sensor_data); // 传感器数据API
}
}
这种简单的路由实现足够应对基础需求,且内存占用极低。
响应头生成技巧
c复制void send_http_header(uint16_t status_code) {
char header[64];
snprintf(header, sizeof(header),
"HTTP/1.0 %d OK\r\n"
"Connection: close\r\n"
"Content-Type: text/html\r\n\r\n",
status_code);
uip_send(header, strlen(header));
}
注意Connection设为close可避免保持连接占用资源。
5. 系统部署与调试
5.1 烧录配置要点
调试接口配置
plaintext复制ST-Link引脚连接:
SWCLK -> PA14
SWDIO -> PA13
GND -> GND
VCC -> 3.3V (可选)
建议在Keil的Debug选项中:
- 勾选"Reset and Run"
- 设置"Download Function"为"Erase Sectors"
- 启用"Reset after programming"
5.2 网络测试方法
基础连通性测试
-
Ping测试:
bash复制
ping 192.168.1.100 -t持续观察丢包率应低于1%
-
ARP缓存验证:
bash复制
arp -a确认能正确显示STM32的MAC地址
高级抓包分析
使用Wireshark过滤条件:
plaintext复制eth.addr == 00:04:a3:12:34:56 || ip.addr == 192.168.1.100
重点关注TCP三次握手过程和HTTP请求/响应时序。
6. 典型问题解决方案
6.1 网络连接失败排查流程
-
检查物理层:
- 示波器查看SPI时钟信号
- 测量INT引脚电平变化
- 确认RJ45接口指示灯状态
-
验证驱动层:
c复制uint8_t revid = ENC28J60_Read(EREVID); if(revid != 0x04) { // 正确版本号 printf("ENC28J60 init failed!\n"); } -
协议栈调试:
在uip_arp.c中添加调试输出,观察ARP请求/响应过程。
6.2 网页访问异常的常见原因
-
TCP端口未正确监听:
c复制uip_listen(HTONS(80)); // 必须在连接建立后调用 -
HTTP响应格式错误:
必须严格遵循格式:http复制HTTP/1.0 200 OK\r\n Content-Type: text/html\r\n \r\n <html>... -
缓冲区溢出:
检查uip_buf数组大小是否足够容纳以太网帧(通常需要600字节以上)。
7. 性能优化与扩展
7.1 内存优化技巧
-
使用const修饰符:
c复制const char http_header[] = "HTTP/1.0 200 OK\r\n";确保常量数据存放在Flash而非RAM中。
-
合并字符串常量:
c复制#define HTML_START "<html><body>" #define HTML_END "</body></html>"
7.2 功能扩展方向
-
AJAX动态更新:
在页面中添加JavaScript定时请求数据:javascript复制setInterval(function(){ fetch("/data").then(r=>r.json()).then(updateUI); }, 1000); -
GPIO远程控制:
增加POST请求处理:c复制if(strncmp(req, "POST /led", 9) == 0) { GPIO_WriteBit(GPIOC, GPIO_Pin_13, (strstr(req, "on") != NULL) ? Bit_SET : Bit_RESET); } -
多连接支持:
修改uipopt.h配置:c复制#define UIP_CONF_MAX_CONNECTIONS 4
这套方案经过多个项目验证,在智能家居、工业监测等场景下表现稳定。最大的优势是成本低廉且完全自主可控,特别适合中小批量物联网终端产品开发。