ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32F103+ENC28J60实现低成本嵌入式Web服务器实战

STM32F103+ENC28J60实现低成本嵌入式Web服务器实战 简介面向STM32F103开发人员该资源整合ENC28J60以太网控制器用极少量代码实现了基于网页的IP、子网掩码和默认网关远程配置固件ROM占用仅6.11KBRW数据2.59KB适用于资源受限的嵌入式网络设备或快速原型验证。压缩包共91个文件以36个C源文件和38个头文件为主辅以启动汇编、工程配置、说明文本和清理脚本整体321KB目录按APP、RTOS、TCPIP、SPI、LED等模块拆分方便按功能查找驱动、协议栈和用户代码。目前已有328人学习下载适合具备一定STM32编码经验、希望学习ENC28J60驱动或轻量级Webserver实现方式的开发者。资料内含可直接烧录的HEX文件及完整MDK工程可对照研究HTTP请求解析、网页表单提交和参数存储流程也能借助readme快速了解各文件用途为后续移植或扩展网络配置功能提供清晰范例。 前阵子朋友问我手里一堆STM32F103最小系统板能不能不换芯片就联网做个Web服务器。我第一反应是能但别指望它像Linux板卡那样跑Nginx。STM32F103没有以太网MAC控制器必须外接一颗以太网芯片。我最早用的就是ENC28J60SPI接口、8KB收发缓冲区、外部25MHz晶振配合uIP协议栈硬是在20KB内存的单片机里塞下了一个能用的HTTP服务器。这篇文章记录从选型、硬件连接、驱动移植到Web应用实现的完整过程也会把调试时踩过的坑和实测数据一并放出来给想低成本做嵌入式Web的同学一个参考。1. 选型逻辑STM32F103为何要外挂ENC28J601.1 芯片本身没有以太网外设STM32F103系列虽然外设丰富但并没有内置以太网MAC更不用说PHY了。也就是说如果你不想换芯片想在F103上实现以太网只能外接一个MAC或MACPHY芯片。ENC28J60就是一颗集成MAC和PHY的SPI转以太网控制器由Microchip推出成本低外围简单非常适合8位/32位MCU。它把链路层和物理层的活都干了MCU只需要通过SPI读写它的内部寄存器、收发缓冲区上层TCP/IP协议栈由你自己用软件实现。有人会说“市面上不是有W5500模块吗干嘛非要折腾ENC28J60”这话问得有道理。W5500确实更省心因为它把TCP/IP协议栈做进了芯片硬件里MCU只负责读写Socket。但从学习和成本角度ENC28J60的优势在于模块便宜、开源资料多而且它逼着你去理解TCP/IP怎么跑起来这对嵌入式开发来说是很值的一段经验。1.2 为什么不是W5500或其他方案这里放一张我当时做对比的表也是后来选型的主要依据方案硬件TCP/IP接口价格参考适合场景ENC28J60无SPI10-20元模块学习网络协议栈、低成本联网W5500有SPI20-40元模块不想碰协议栈、快速集成DM9000无并口少见更高吞吐但接线复杂内置MAC的F207/F4xx系列部分MII/RMII芯片贵不少需要较高性能和带宽ENC28J60最大的优势是便宜、资料多缺点也很明显只有8KB缓冲10Mbps半双工协议栈必须自己跑。W5500因为硬件协议栈省了MCU不少负担但价格高一些而且遇到问题时如果不了解内部机制反而不好排查。我选择ENC28J60更多是想把TCP/IP底层看个明白。如果你项目周期紧直接上W5500也不会错。1.3 资源账20KB RAM跑Web够不够STM32F103C8T6有64KB Flash、20KB RAM这个资源听起来寒酸但跑一个简单的Web服务器是完全够的。ENC28J60驱动本身占不了多少uIP协议栈加HTTP应用总内存占用可以控制在5KB以内Flash占用10KB左右。这和现代操作系统里的Web服务器完全不是一个概念需要的是精简再精简。如果不做DHCP、不做加密、不做多并发只提供几个静态页面和轮流刷新数据完全可行。当时我还特意用标准库3.5版本写驱动而不是HAL库。原因很简单标准库体积更小启动更快编译出来的代码更紧凑对Flash紧张的项目更友好。当然现在用CubeMX生成工程也很方便只要把SPI速度、中断优先级配置正确移植思路一模一样。2. 硬件连接里最容易翻车的几个细节2.1 引脚分配建议我用的是SPI1时钟线PA5MISO PA6MOSI PA7片选PA4软件控制外接中断引脚PA8可配置为EXTI8。这里专门说一下PA8PA8默认功能是MCO用来输出时钟信号但也可以做普通GPIO或外部中断。很多人照抄例程不检查板子上PA8刚好被其他外设占用导致中断进不来。我建议在接线前还是看一眼原理图确认没有冲突。比如你如果想把PA8引时钟出来接别的芯片就不要再占它做中断。硬件接线表如下ENC28J60模块引脚STM32F103引脚VCC3.3V需确认模块是否带稳压GNDGNDSCKPA5MISOPA6MOSIPA7CSPA4INTPA8外部中断RSTPB12可选软件复位2.2 供电和晶振的两个坑ENC28J60功耗比一般传感器要高启动瞬间和发射时电流可以达到150mA以上。如果用最小系统板上的LDO给整个系统供电线一插网线就可能复位。我第一次就是这么干的网页怎么都调不通最后发现是供电电压被拉低到2.8V芯片自动复位。后来给ENC28J60单独接了一个低压差线性稳压器滤波电容加大到220uF问题才消失。另一个坑是晶振。ENC28J60需要25MHz的外部晶振负载电容通常选18pF或22pF焊接时要尽量靠近芯片。模块上一般已经焊好但如果你自己画板务必确认晶振起振否则CLKOUT没有信号MAC寄存器读不到正确ID。我见过凌晨三点还在查SPI而最后发现晶振没焊牢的项目真的会让人怀疑人生。2.3 为什么我建议先用现成模块自己画板遇到问题排查很痛苦。我第一次自己做板子时栽在RJ45网络变压器上网口指示灯全灭SPI读芯片ID正常但物理链接始终建立不起来。后来换成现成的ENC28J60模块问题立刻解决。所以给新手的建议是先用模块跑通功能驱动和应用层都验证完再考虑画板集成。这样才能把变量控制在最少的范围内。3. 让芯片“动”起来SPI时序与收发缓冲区模型3.1 SPI初始化参数怎么定ENC28J60支持SPI Mode 0,0或1,1绝大多数驱动都用Mode 0即CPOL0、CPHA0。初始化时先把分频系数设大一点比如SPI1分频到2MHz保证通信稳定后面再逐步提高。我最终稳定在9MHz左右超过这个速率丢包率明显上升。原因很简单模块的PCB布局、杜邦线长度、连接器质量都会影响信号完整性不要一上来就开满速。标准库下的关键初始化如下SPI_InitTypeDef SPI_InitStructure; SPI_InitStructure.SPI_Direction SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode SPI_Mode_Master; SPI_InitStructure.SPI_DataSize SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL SPI_CPOL_Low; // CPOL0 SPI_InitStructure.SPI_CPHA SPI_CPHA_1Edge; // CPHA0 SPI_InitStructure.SPI_NSS SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_8; // 先低速 SPI_InitStructure.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, SPI_InitStructure);要注意STM32标准库版本3.5和旧版的SPI结构体成员名可能不同编译不过时先查头文件别急着改代码。这是个很基础但经常卡住新人的点。3.2 寄存器访问不要被“读回0”迷惑ENC28J60的寄存器分多个bank访问前需要切换到对应bank。控制寄存器的指令是0x40|寄存器地址。读MAC和PHY寄存器有特殊流程先通过MIREGADR选择地址然后写MICMD.MIIRD开始读再轮询MISTAT.BUSY清掉标志后从MIRD读出两字节。如果跳过这个流程读PHY寄存器会一直返回0但读MAC寄存器正常这种怪象特别容易骗人。我建议写一个简单的reg_read/reg_write工具函数并输出调试信息。第一步先验证芯片ID确保SPI通路没问题。如果ID读出来不对先别碰协议栈回头看硬件连接和SPI时钟极性。把这一步走稳后面就顺了。3.3 内存缓冲区分配策略这是ENC28J60最需要理解的模型芯片内部只有8KB RAM既放发送缓冲也放接收缓冲。代码里常见配置是收发各4KB但实测中HTTP响应要发的内容往往比收的多TCP回包一多TX缓冲就容易溢出。更合理的做法是接收缓冲留大一点比如6KB发送缓冲留2KB。发送缓冲大并不会提高吞吐量反而占空间。一个参考配置#define ENC_RX_BUF_START 0x0000 #define ENC_RX_BUF_END 0x17FF // 6KB #define ENC_TX_BUF_START 0x1800 #define ENC_TX_BUF_END 0x1FFF // 2KB收包流程收到PKTIF中断后读ERXHEAD记录包的长度和状态再从接收缓冲区环形队列里把包搬进MCU然后修改ERXHEAD指针并置位ECON2.PKTDEC。这个顺序不能改否则数据包会重复读取表现就是网页内容时而正常时而乱码。3.4 中断引脚怎么接省事使用外部中断EXTI8来触发收包和链路状态变化最省事。注意配置为下降沿触发因为ENC28J60的INT引脚默认高事件发生时拉低。收到中断后在中断服务函数里置标志位不要在中断里做SPI读写因为SPI操作耗时很长会拖垮其他中断。主循环里轮询标志位再处理这样既不会丢事件也不会让系统卡顿。4. 协议栈选择资源和可维护性之间的权衡4.1 uIP和lwIP的差距在哪里uIP是专为8/16位处理器设计的极简TCP/IP协议栈代码量很小一次只处理一个包TCP连接状态有限lwIP功能全支持多线程、内存池、raw API和netconn API但即使裁剪得再狠内存占用也通常是5~15KB以上加上应用层很容易逼近F103的20KB RAM上限。所以STM32F103ENC28J60这种组合最稳的做法是用uIP。我并不是说lwIP不能用而是在这个资源级别下uIP的性价比明显更高。打个比方lwIP像一个功能齐全的厨房锅碗瓢盆样样有但厨房只有两平方米转身都难uIP则是一个小巧的移动餐车只保留一套必要的设备虽然不能做大餐但出餐稳、占地少。我需要的就是这个级别的Web服务器。4.2 底层对接的四个关键时机uIP移植到STM32上核心是处理好四个时机网卡收到一个包调用uip_input()uIP内部解析ARP或IP包如果产生应答会写入全局缓冲区uip_buf并让发送函数把uip_len长度的数据发出去。周期定时器每100ms调用一次uip_periodic(conn_id)检查超时重传、ARP老化、TCP定时器如果过期连接需要处理也会填入uip_buf同样要发送。ARP查询在发IP包前先查ARP表必要时调用uip_arp_out()它会填充ARP请求或填入MAC地址。初始化设置本机IP、子网掩码、网关、MAC地址然后调用uip_init()。典型的主循环while (1) { if (net_packet_len 0) { memcpy(uip_buf, net_packet_buf, net_packet_len); uip_len net_packet_len; uip_input(); net_send_if_required(); } if (tick_100ms) { timer_clear(); for (int i 0; i UIP_CONNS; i) { uip_periodic(i); net_send_if_required(); } } }uIP要求上层把包放在uip_buf并设置uip_len由底层发送到网卡。刚开始觉得这个机制很绕等调过一遍ping和网页后你就懂得它为什么这样设计了。4.3 DHCP还是静态IPuIP自带dhcpc源码但依赖定时器和状态机如果不想折腾就先用静态IP。我在调试阶段一直用静态IP省掉很多不确定因素。设置方法uip_ipaddr_t ipaddr, netmask, gw; uip_ipaddr(ipaddr, 192,168,1,10); uip_ipaddr(netmask, 255,255,255,0); uip_ipaddr(gw, 192,168,1,1); uip_sethostaddr(ipaddr); uip_setnetmask(netmask); uip_setdraddr(gw);我遇到过一个诡异问题设置了静态IP但浏览器打不开网页Ping也超时最后发现是MAC地址初始化得全是0。ENC28J60的MAC地址寄存器默认不是有效值必须从外部EEPROM或代码里写入。MAC前三个字节建议用合法OUI后三个字节随便编但别和其他设备冲突。4.4 如果一定要用lwIP很多模块厂商提供的Demo是lwIP版本但那是针对大内存芯片的。真要移植到F103重点调整三个地方MEM_SIZE降到10KB以内、PBUF池数量减少、禁用TCP窗口缩放和选择性确认并用raw API写应用避免开RTOS。我没有长期使用这个方案因为牺牲太多功能后还不如uIP易维护。如果你有足够的Flash和RAM可以考虑直接上F429这类带MAC的芯片连lwIP都不用裁剪得这么狠。5. Web服务器代码结构静态页面、动态数据和AJAX刷新5.1 HTTP响应怎么拼最简单的Web服务器只需要做到当收到TCP端口80的连接和数据时解析请求行根据URL返回不同内容。响应格式必须按HTTP/1.1或0.9否则现代浏览器可能不认。一个最小的响应头const char http_ok_header[] HTTP/1.1 200 OK\r\n Server: STM32F103-ENC28J60\r\n Content-Type: text/html\r\n Connection: close\r\n \r\n;注意末尾必须有一个空行。然后紧跟HTML内容。请求行解析可以用strstr匹配比如判断GET /或GET /data不需要引入复杂的解析库。5.2 动态页面避免在RAM里攒整个HTML新手最容易犯的错是把所有HTML拼接成一个很大的字符串再一次性uip_send。F103的RAM不允许这样做。我的做法是分段发送uip_send((char *)http_ok_header, strlen(http_ok_header)); net_send_with_retry(); uip_send((char *)page_start, strlen(page_start)); net_send_with_retry(); char value_buf[16]; sprintf(value_buf, pADC:%d/p, adc_value); uip_send(value_buf, strlen(value_buf)); net_send_with_retry(); uip_send((char *)page_end, strlen(page_end)); net_send_with_retry();这里的net_send_with_retry是对uIP发送流程的封装调用uip_send后uIP会把数据放入发送缓冲区再由底层发送API真正发出。如果数据超过TCP MSS常见1460字节但uIP里受缓冲限制必须拆包。实测中一次HTTP响应拆成3~5个TCP分片很正常所以不要写一个超大send然后期待一次发完。5.3 用状态机控制分页发送有些页面数据长度超过一个包光靠分段发送还不够。uIP不缓存整个TCP流它是在应用层每次收到ACK或窗口更新时重新调用上层应用函数生成数据。因此一个经验是用状态机记录当前发送到第几段每次发送一个段直到发完。具体做法if (is_http_page_active) { if (page_part 1) { uip_send(...); page_part 2; } else if (page_part 2) { uip_send(...); page_part 3; } // ... else { page_part 0; close_conn 1; } }不维护这个状态机网页可能会卡在一半浏览器一直转圈。这个坑特别隐蔽因为串口日志里看不到错误TCP连接也正常但页面就是不完整。5.4 AJAX轮询数据接口要让页面实时显示数据不用每秒钟整页刷新更好的办法是JS里定时请求一个纯文本接口。服务器端判断请求路径为/data时返回if (strncmp(url, /data, 5) 0) { sprintf(buf, ADC%d;TEMP%d;HUMI%d\r\n, adc, temp, humi); uip_send(buf, strlen(buf)); }浏览器端JSsetInterval(function(){ fetch(/data).then(r r.text()).then(s { // 解析 ADCxxx;TEMPxxx;HUMIxxx }); }, 1000);注意STM32的Web服务器不适合频繁并发请求轮询间隔建议1秒以上。如果你需要在响应的同时发送多个变量用这种keyvalue文本格式比JSON更省内存和带宽。也可以在C里直接拼一个简易JSON但字符串转义处理起来比较啰嗦我就不推荐了。5.5 连接资源管理uIP最多同时支持几个TCP连接由UIP_CONNS宏决定我设置成4。浏览器访问一个页面往往同时发起多个连接HTML、CSS、Favicon如果连接数太少会导致部分请求排队超时后白屏。设置成4比较合理但每个连接都会占用RAM实际能开的连接数取决于剩余内存。响应结束后记得调用uip_abort或标记关闭连接否则连接一直被占用第二次刷新就打不开了。6. 实测性能与调试陷阱6.1 一组可以参考的实测数据我在STM32F103C8T6最小系统板现成ENC28J60模块专用3.3V供电的测试环境下SPI时钟9MHz系统时钟72MHzuIP协议栈编译时不开优化得到这些数据测试项结果Ping平均延迟10~20ms单次HTTP请求响应时间50~100msAJAX轮询间隔1s时无丢失最大并发TCP连接4持续运行12小时未掉线内存稳定需要说明这些数据只代表我的硬件和软件环境不同模块、不同PCB布局差距可能很大。如果你的板子得出来的结果比我差很多优先检查供电和SPI信号质量。6.2 三个最常见的问题第一个是页面刷几次就挂。排查思路先查串口日志里TCP断开是否正常再查uIP连接数是否被占满最后看发送缓冲是否溢出。我遇到的是发送缓冲太小长网页没能完整发完加上没有状态机连接被掐断。改大TX缓冲区域并在发送完关闭连接后解决。第二个问题是ARP解析不了。表现是首次ping不通过几十秒后才通。原因是周期定时器没有调用uip_arp_timer()或者ARP表老化后被错误填充。uIP要求每10秒调用一次uip_arp_timer()来清理ARP缓存别把它忘了。第三个问题是SPI偶发错误导致收包CRC错误。经过逻辑分析仪抓包发现CS释放时SCK还在跳这是SPI收发时序问题后来在片选拉高前加了一个等待SPI不忙的延时解决。另外杜邦线过长时速率超过10MHz特别容易出错。我用的线约15cm9MHz是稳定临界点。6.3 调试手段串口和.map文件配合调试网络协议栈最忌讳靠猜。我的做法是串口打印所有关键事件网卡初始化完成、ARP收到、TCP连接建立、HTTP请求URL、发送字节数。串口重定向printf会占用不少Flash和RAM可以在正式版里关掉。我用Keil编译后查看.map文件发现printf相关代码占用了将近2KB Flash对64KB的芯片来说不是小数目。后来把调试输出换成简易的逐字节发送函数体积立刻降下来了。如果你也想检查资源占用编译后在.map文件里搜索uip、enc28j60、http这些模块名就能看到各个文件占用的Code和Data量。这个习惯有助于你做裁剪决策。6.4 这块平台还能往哪扩展这套框架稳定后很容易扩展出CAN到以太网网关。STM32F103本身带CAN控制器加一颗TJA1050收发器uIP的TCP服务端接收远程指令再通过CAN发送到现场设备就能实现远程控制。CAN的同步跳转宽度、采样点这些参数也可以单独调不会影响以太网部分。如果想记录数据可以用F103的定时器和ADC采集模拟量再通过Web页面展示。你只需要在HTTP应用里加一个case处理新的URL其他协议栈部分完全不用动。最后分享一点个人体会STM32F103ENC28J60这个组合已经有些年头了但它依然是我给入门的工程师推荐的网络入门方案。芯片手册、寄存器说明、协议栈源码全都是公开的踩坑成本低一旦搞明白后面再接触W5500甚至带MAC的高性能MCU都会轻松很多。我的建议是不要只抄Demo先把ENC28J60的寄存器模型和uIP的输入输出时机理清楚再跑Web应用。卡住了把我上面提到的几个关键点逐项对照一遍基本能解决大部分问题。也欢迎你分享自己的实测数据给后面做类似项目的人留个参考。本文还有配套的精品资源点击获取
返回列表