STM32F103+LWIP实战:从零搭建嵌入式Web服务器与编译避坑指南
2026/9/19 12:22:18 网站建设 项目流程

1. 项目缘起与整体设计思路

嵌入式网络通信一直是STM32开发中门槛较高的部分。很多朋友能熟练操作GPIO、配置定时器、跑通串口中断,但一碰到以太网协议栈就卡壳,尤其是LWIP这种体量不小的开源协议栈,配置项多、依赖关系复杂,稍有不慎就是一堆编译错误或者ping不通。我这次做的项目目标很明确:用STM32F103系列芯片配合CubeMX,在最短时间内搭起一个能响应HTTP请求的嵌入式Web服务器,让板子通过网口对外提供网页访问能力,同时把过程中踩到的编译坑一次性讲清楚。

这个项目的核心价值在于“可复现”。网上不少教程要么基于老旧的标准库,要么跳过了CubeMX的配置细节直接扔一堆代码,初学者照着做往往卡在第一步。我选择STM32F103C8T6加ENC28J60这类SPI接口以太网模块的组合,原因是这套方案成本低、资料多、对引脚要求不苛刻,非常适合入门和中小型项目。如果你用的是STM32H723这类带内置MAC的高端型号,配置逻辑是相通的,只是PHY芯片和时钟树部分需要额外注意。

整个方案的设计思路分三层:底层是CubeMX生成的HAL库初始化代码,负责时钟、GPIO、SPI和中断;中间层是LWIP协议栈的移植与网卡驱动对接,这是最关键也最容易出问题的地方;上层是HTTP服务器应用,基于LWIP的raw API实现,不依赖操作系统,裸机就能跑。选择raw API而不是netconn或socket API,是因为裸机环境下raw API没有RTOS依赖,代码量小、响应快,适合资源受限的F103。如果你后续要上FreeRTOS,再切换到netconn API也不迟,但入门阶段先把raw API吃透,对理解协议栈的运行机制帮助极大。

提示:本文所有操作基于STM32CubeMX 6.x版本和STM32CubeF1固件包,Keil MDK 5作为编译环境。不同版本的CubeMX界面略有差异,但核心配置项位置基本一致。

2. 环境准备与CubeMX关键配置

2.1 软件安装与工程创建

工欲善其事,必先利其器。环境搭建这一步看似简单,但版本不匹配导致的编译错误占了新手问题的一半以上。我的建议是:CubeMX用6.6以上的版本,Keil MDK用5.36以上,STM32CubeF1固件包用1.8.x。这三个版本组合经过我多次实测,兼容性最稳。

安装CubeMX时有个细节要注意:安装路径不要带中文和空格,否则生成工程时可能报路径错误。安装完成后第一次打开,它会提示你下载固件包,这里直接选STM32CubeF1的最新版即可。Keil那边需要单独安装STM32F1的Device Family Pack,很多人编译时报“cannot open source input file stm32f1xx_hal.h”就是因为芯片包没装或者版本不对。

新建工程的流程:打开CubeMX,选择“New Project”,在芯片选择框里输入STM32F103C8,双击选中。这里有个小技巧,如果你用的是其他F103型号,比如RCT6或者ZET6,直接选对应型号就行,配置逻辑完全一样,只是引脚数量和外设资源有差异。

2.2 时钟树与调试接口配置

进入工程后第一件事是配时钟。F103的外部晶振通常是8MHz,在RCC配置里把HSE设为Crystal/Ceramic Resonator。然后切到Clock Configuration标签页,把PLL Source选为HSE,PLL Mul调到9倍频,这样系统时钟就是72MHz。AHB不分频,APB1分频系数设为2得到36MHz,APB2不分频保持72MHz。这个时钟配置是F103的经典跑法,稳定且性能足够。

调试接口方面,强烈建议在SYS里把Debug设为Serial Wire。我见过太多人因为没配这个,下载一次程序后芯片就锁了,只能靠复位时序救回来。Serial Wire占用PA13和PA14两个引脚,不影响其他功能,留着它后续调试方便得多。

2.3 SPI接口与网卡引脚分配

ENC28J60通过SPI与STM32通信,我用的是SPI1。在Connectivity里选SPI1,Mode设为Full-Duplex Master,Hardware NSS Signal选Disable。参数配置上,Prescaler先设成16,Baud Rate大概4.5Mbits/s,这个速度对ENC28J60来说足够且稳定。Clock Polarity和Phase都设为Low,这是ENC28J60的SPI模式要求,设错了数据读写全是乱的。

引脚分配如下:PA5是SPI1_SCK,PA6是SPI1_MISO,PA7是SPI1_MOSI。片选引脚我单独用PA4做GPIO输出,复位引脚用PA3,中断引脚用PA2配置为外部中断下降沿触发。这里注意,片选虽然可以用硬件NSS,但ENC28J60的时序对片选控制比较敏感,用软件GPIO手动拉低拉高更可靠。

注意:PA4作为片选,初始电平必须设为High,否则上电瞬间网卡可能被误选中导致初始化失败。在GPIO配置里把PA4的初始输出电平设为High,速度设为High。

2.4 LWIP协议栈的启用与参数调整

在Middleware里选LWIP,Mode设为Enabled。这里CubeMX会自动勾选一些依赖项,比如ETH或者网卡驱动,但因为我们用的是SPI接口的ENC28J60,CubeMX原生并不直接支持,所以需要在后续手动添加驱动文件。不过LWIP本身的配置可以先在这里做好。

General Settings里,LWIP_DHCP我建议先关掉,用静态IP调试更方便。IP地址设成192.168.1.100,子网掩码255.255.255.0,网关192.168.1.1。这几个值根据你实际路由器网段调整,但要注意板子和电脑必须在同一网段。MEM_SIZE默认是1600,对于HTTP服务器来说偏小,我改成4096。PBUF_POOL_SIZE改成8,TCP_SND_BUF和TCP_WND都设成2920,这样单个TCP包能承载更多数据,网页加载更快。

Key Options里,LWIP_NETCONN和LWIP_SOCKET都可以关掉,我们用raw API不需要它们。LWIP_HTTPD要打开,这是HTTP服务器功能的核心开关。打开后下面会出现HTTPD相关配置,比如LWIP_HTTPD_SSI和LWIP_HTTPD_CGI,这两个先关掉,等基础功能跑通再按需开启。

3. 网卡驱动移植与核心代码实现

3.1 ENC28J60驱动文件的结构与集成

CubeMX生成的工程里没有ENC28J60的驱动,需要自己添加。驱动文件通常包含enc28j60.c、enc28j60.h以及一个网卡接口文件ethernetif.c。ethernetif.c是LWIP与硬件之间的桥梁,它实现了low_level_init、low_level_output和low_level_input三个关键函数。

把这三个文件放到工程的合适目录下,比如Middlewares/Third_Party/LWIP/target。然后在Keil里把它们加入工程组。这里有个容易忽略的点:ethernetif.c里会引用lwip/netif.h和lwip/timeouts.h等头文件,确保Include Paths里包含了LWIP的include目录,否则编译时找不到头文件。

3.2 low_level_init函数的实现要点

low_level_init负责初始化网卡硬件并设置netif结构体的参数。核心步骤是:先调用ENC28J60_Init初始化SPI和网卡寄存器,然后设置netif的hwaddr_len为6,hwaddr填入MAC地址。MAC地址可以随便设,但建议避开广播地址和多播地址,比如用02:00:00:00:00:01这种本地管理地址。

接下来设置netif的mtu为1500,flags置为NETIF_FLAG_BROADCAST | NETIF_FLAG_ETHARP | NETIF_FLAG_LINK_UP。最后调用netif_set_link_up和netif_set_up把网卡状态置为可用。这里有个细节:netif_set_up必须在netif_add之后调用,否则LWIP不会处理这个网卡的数据包。

3.3 数据包收发流程解析

发送流程:上层应用调用tcp_write把数据写入发送缓冲区,LWIP的ip_output和etharp_output逐层封装后,最终调用netif->linkoutput,也就是low_level_output。这个函数里,我们需要把pbuf链中的数据拷贝到一个连续的发送缓冲区,然后调用ENC28J60_PacketSend发送出去。注意pbuf可能是链式结构,要遍历整个链把数据拼起来再发。

接收流程:ENC28J60收到数据包后触发外部中断,在中断服务函数里置一个标志位。主循环里检测到这个标志位后,调用ENC28J60_PacketReceive读取数据到缓冲区,然后用pbuf_alloc分配一个PBUF_POOL类型的pbuf,把数据拷贝进去,最后调用netif->input把pbuf交给LWIP处理。这里要注意中断里不要做耗时操作,读包和交给协议栈处理都放到主循环里做。

提示:ENC28J60的接收缓冲区只有8KB,如果主循环处理不及时,新来的包会覆盖旧包导致丢包。实测下来,主循环里其他任务不要太耗时,或者把LWIP的处理优先级提高。

3.4 HTTP服务器raw API回调实现

LWIP的HTTP服务器基于httpd.c实现,我们需要做的是提供文件系统和回调函数。最简单的做法是使用LWIP_HTTPD_SSI和LWIP_HTTPD_CGI,但入门阶段可以先做一个固定的响应页面。

在httpd_init之后,LWIP会自动监听80端口。当有客户端连接时,httpd会调用我们注册的CGI处理函数或者直接返回fsdata里的文件内容。fsdata.c是一个由makefsdata工具生成的C文件,它把网页文件转换成C数组。你可以自己写一个简单的HTML页面,然后用工具转换,或者直接手写一个最小的HTTP响应。

我手写了一个最简单的响应:当收到GET请求时,返回一个包含“Hello from STM32”的HTML页面。核心代码是在httpd.c的http_accept函数里,根据uri判断请求路径,然后调用tcp_write发送响应头和响应体。这种方式虽然简陋,但能最快验证整个链路是否通畅。

4. 编译错误排查与常见问题实录

4.1 头文件找不到与路径配置错误

这是最高频的编译错误,报错信息通常是“cannot open source input file xxx.h”。原因无非两个:要么文件没加入工程,要么Include Paths没配全。LWIP的头文件分布在多个目录,包括src/include、src/include/lwip、src/include/lwip/arch、src/include/ipv4等。我的做法是在Keil的Options for Target里,把LWIP相关的所有include目录都加进去,宁可多加不要少加。

还有一个隐蔽的坑:CubeMX生成的工程里,LWIP的include路径可能只加了顶层目录,子目录没加。这时候需要手动补上。另外,如果同时装了多个版本的CubeF1固件包,Keil可能引用了旧版本的路径,检查一下Options里的路径是否指向当前使用的固件包版本。

4.2 重复定义与符号冲突

报错信息类似“multiply defined symbol xxx”。这种情况通常是因为LWIP的某些源文件被重复添加,或者CubeMX自动生成的代码和我们手动添加的驱动有冲突。比如ethernetif.c,CubeMX在启用LWIP时可能会自动生成一个,我们又手动加了一个,两个文件里的函数名一样,链接时就冲突了。

解决办法是检查工程里的文件列表,确保每个源文件只出现一次。如果CubeMX自动生成了ethernetif.c,就在它的基础上修改,不要另外新建。另外,LWIP的opt.h和lwipopts.h也可能冲突,CubeMX生成的lwipopts.h优先级更高,确保只保留一个。

4.3 内存分配失败与堆栈溢出

编译通过但运行异常,比如ping不通或者HTTP请求无响应,很多时候是内存问题。LWIP需要一块内存池,MEM_SIZE设得太小会导致pbuf分配失败。我一开始用默认的1600,结果网页稍微大一点就返回不全。改成4096后正常。另外,启动文件里的堆栈大小也要检查,Stack_Size建议至少0x800,Heap_Size至少0x400。

还有一个容易忽略的点:ENC28J60的接收缓冲区是8KB,如果PBUF_POOL_SIZE设得太大,每个pbuf占用的内存加起来可能超过STM32的RAM容量。F103C8T6只有20KB RAM,要精打细算。我的配置是PBUF_POOL_SIZE为8,每个pbuf大小默认,加上MEM_SIZE的4KB,总共占用约10KB,剩下的留给栈和其他变量,刚好够用。

4.4 网卡初始化失败与ping不通排查

网卡初始化失败的表现是ENC28J60_Init返回错误,或者netif_set_up后link状态始终为down。排查步骤:先用万用表量一下ENC28J60的供电是否正常,3.3V和GND都要确认。然后检查SPI通信是否正常,可以写一个简单的SPI回环测试,发一个字节看能不能收到正确的响应。

如果SPI正常但网卡还是不通,检查复位时序。ENC28J60的复位引脚需要拉低至少1ms再拉高,然后等待至少1ms才能访问寄存器。我遇到过因为复位延时不够导致初始化失败的情况,后来在复位后加了5ms延时就好了。

ping不通但网卡初始化成功的情况,先确认IP地址和电脑是否在同一网段,然后检查防火墙是否屏蔽了ICMP。Windows防火墙默认可能阻止ping请求,临时关掉防火墙测试一下。如果还是不通,用Wireshark抓包看板子有没有发出ARP请求,有请求没响应说明接收通路有问题,没请求说明发送通路有问题。

4.5 常见问题速查表

问题现象可能原因排查方法解决方案
编译报头文件找不到Include路径缺失检查Options里的C/C++ Include Paths补全LWIP所有include目录
链接报重复定义源文件重复添加查看工程文件列表删除重复的ethernetif.c或opt.h
ping不通IP不在同一网段核对板子和电脑IP改为同一网段
HTTP请求无响应MEM_SIZE太小增大MEM_SIZE后测试改为4096或更大
网卡初始化失败复位延时不够示波器看复位引脚波形复位后加5ms延时
数据包丢失主循环处理太慢减少主循环其他任务提高LWIP处理优先级

5. 实操验证与性能调优经验

5.1 从零到ping通的完整验证流程

代码编译通过只是第一步,真正的验证要从ping开始。把板子通过网线连到路由器或者直接连电脑,给板子和电脑配好同网段的静态IP。打开命令行,ping 192.168.1.100,如果能看到回复,说明底层链路和ARP已经通了。

ping通之后,打开浏览器输入板子的IP地址,如果能看到我们写的HTML页面,说明TCP和HTTP层也正常了。这个过程我建议分步验证:先ping通,再用telnet连80端口看能不能建立TCP连接,最后才用浏览器访问。这样出问题时能快速定位是哪一层的问题。

注意:有些路由器开启了AP隔离,会阻止同一网段内设备互相通信。如果ping不通,先确认路由器没有开这个功能,或者直接用网线直连电脑和板子。

5.2 响应速度优化与并发连接处理

基础功能跑通后,可以做一些优化。首先是响应速度,LWIP的TCP_SND_BUF和TCP_WND设大一些能减少分包,网页加载更快。我把这两个值从默认的1024改到2920后,一个约2KB的网页从原来的3秒加载缩短到1秒以内。

并发连接方面,LWIP默认的MEMP_NUM_TCP_PCB是5,意味着最多同时处理5个TCP连接。对于嵌入式Web服务器来说够用了,但如果你的页面包含多个图片或CSS文件,浏览器会同时发起多个连接,这时候可以适当增加到8或10。不过要注意,每个TCP PCB都占内存,F103的RAM有限,不要设太大。

5.3 长时间运行的稳定性观察

嵌入式设备经常需要7x24小时运行,稳定性很重要。我连续跑了48小时,每隔一段时间ping一次并访问网页,观察是否有丢包或响应变慢。实测下来,ENC28J60在长时间运行后偶尔会出现接收缓冲区溢出,导致短暂无响应,但几秒后自动恢复。

为了减少这种情况,我在主循环里加了看门狗喂狗和网卡状态检测。如果连续多次ping不通,就重新初始化网卡。这个逻辑虽然简单,但能有效应对偶发的网卡死锁。另外,LWIP的tcp_fasttmr和tcp_slowtmr要确保被定期调用,否则TCP的重传和超时机制不工作,连接异常后无法恢复。

5.4 从F103到H723的移植注意事项

如果你用的是STM32H723这类带内置以太网MAC的高端芯片,整体思路一样,但有几个关键差异。H723的ETH外设需要配置PHY芯片,通常是LAN8720或者DP83848,通过RMII接口连接。CubeMX里要启用ETH并配置RMII引脚,时钟树也要调整,H723的ETH需要50MHz的参考时钟,通常由外部晶振或者PLL提供。

LWIP的配置基本一致,但H723的RAM大得多,MEM_SIZE和PBUF_POOL_SIZE可以设得更大,性能也更好。另外,H723支持硬件校验和卸载,在ETH配置里可以开启,能减轻CPU负担。不过这些高级功能建议在基础功能跑通后再逐步开启,避免引入新的变量。

6. 个人实操心得与后续扩展方向

这个项目我从开始到ping通大概花了两个晚上,其中大部分时间耗在编译错误和网卡初始化上。回头看,如果一开始就把Include路径配全、复位延时加够,能省下至少一半时间。嵌入式开发就是这样,硬件相关的坑往往比软件逻辑更磨人,但一旦跑通,那种成就感也是纯软件给不了的。

后续扩展方面,可以做的事情很多。比如加上SSI功能,让网页能动态显示STM32的ADC采样值或者GPIO状态;或者加上CGI,通过网页控制板子上的LED。再进一步,可以移植一个轻量级的文件系统,把网页文件存在外部Flash里,通过HTTP服务器提供更丰富的页面。如果项目需要远程升级,还可以基于HTTP实现OTA功能,通过网页上传固件到板子的外部Flash,然后跳转执行。

我个人在实际操作中的体会是,LWIP的配置项虽然多,但常用的就那么十几个,把MEM_SIZE、PBUF_POOL_SIZE、TCP_SND_BUF、TCP_WND这几个调对了,大部分问题都能解决。另外,调试网络问题时Wireshark是神器,能看到每一层的数据包,比盲目改代码高效得多。最后再分享一个小技巧:在httpd.c里加一个计数器,记录收到的HTTP请求数量,通过串口打印出来,能直观地看到服务器的工作状态,排查问题时很有用。

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

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

立即咨询