尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

STM32F103+ENC28J60+uIP构建低成本嵌入式Web服务器实战

STM32F103+ENC28J60+uIP构建低成本嵌入式Web服务器实战 简介STM32F103ENC28J60Webserver 是一份面向嵌入式网络开发者的轻量级 Web 服务器工程基于 STM32F103 与 ENC28J60 以太网控制器实现通过极小的代码量完成网页配置 IP、子网掩码和默认网关。工程使用精简的 TCPIP 协议栈ROM 仅约 6.21KB、RAM 约 2.59KB占用资源极低可作为资源受限 MCU 场景的参考设计也适合学习以太网驱动、SPI 通信、RTOS 任务调度和 GPIO 控制。压缩包共 91 个文件以 38 个 .h 头文件与 36 个 .c 源文件为主另含 6 个 .s 启动文件、Hex 固件、Keil 工程文件和烧录批处理脚本并提供 C 语言关键字 static/extern/const/volatile 用法笔记与 readme 说明工程目录按应用层、TCPIP、SPI、LED、SysTick、RTOS、外设库等模块划分便于按需查阅和二次开发。资源包约 321KB已有 328 人学习下载。对于需要在 STM32 平台上快速搭建网页配置功能、或研究轻量级 Web 服务器实现细节的开发者这份资源能提供从底层驱动到上层网页配置的完整代码参考。 老规矩先交代背景。我做这个项目的时候手里有一块现成的STM32F103C8T6最小系统板主频72MHzRAM只有20KBFlash 64KB。产品需求是给测试台加一个Webserver功能现场人员打开浏览器输入板子的IP就能看到传感器数据还能点按钮控制继电器。主控不想换也不能加串口线最省事的办法就是挂一颗外置以太网控制器。当时手头正好有ENC28J60模块于是就有了“STM32F103ENC28J60Webserver”这套组合。整个方案做下来从画原理图到网页能正常点灯大概花了两天时间中间踩了不少坑。如果你也想在低成本MCU上搞一个网页控制页面这篇文章可以直接当参考。1. 方案概述与核心思路1.1 需求定了不加主控但要连网嵌入式设备要联网常见的路子无非几种。第一种是换一颗自带以太网MAC的MCU比如STM32F407或F429但这就意味着重新画板、重新写驱动成本和时间都上去了完全违背了“能用现有板子就绝不动硬件”的原则。第二种是外接串口转以太网模块比如USR-TCP232这种模块确实省事但一个模块几十块钱而且数据全部经过串口转延迟和吞吐都有限总觉得不够“可控”。第三种就是我现在用的方案MCU通过SPI接口外挂一颗以太网控制器由MCU自己跑TCP/IP协议栈把整块芯片变成一个真正意义上的微型Web服务器。从成本上看一颗ENC28J60散片几块钱加上网口座和变压器整个以太网部分成本能压到十几块。而且SPI接口是几乎每颗MCU都有的意味着这套方案换到其他平台也能复用。当年很多开源项目比如有名的Nanode、一些早期的Arduino以太网扩展板用的都是这个组合算是经典老搭档了。1.2 为什么是ENC28J60一个纯粹的MACPHYENC28J60是Microchip出的独立以太网控制器内部集成了MAC介质访问控制层和PHY物理层对外只暴露一个SPI接口。也就是说以太网协议里的电信号收发、帧校验、冲突检测这些脏活累活它都帮你干了MCU只需要通过SPI读写寄存器把要发的数据扔给它再把收到的数据取走就行。很多人会拿W5500来和它对比。W5500是“全硬件TCP/IP协议栈”也就是说TCP、UDP、IP这些协议栈的工作全在芯片内部硬件完成MCU甚至不需要知道什么是ACK只要通过SPI把数据写进芯片的Socket缓冲区就完事了。而ENC28J60不一样它只处理到数据链路层TCP/IP协议栈必须由MCU用软件实现这就引出了后面要讲的uIP协议栈选型问题。选型时我更偏向ENC28J60原因有两个一是价格便宜W5500虽然好用但价格是ENC28J60的两三倍二是这套方案能让你真正把TCP/IP协议栈的细节理一遍对学习网络协议底层很有帮助。如果你只是想在产品里快速上网那W5500上手更快但如果你想做的是“能学到东西、能折腾”的实验项目ENC28J60是更好的教材。2. 硬件连接与最小系统搭建2.1 STM32F103最小系统回顾很多朋友手头都有一块F103最小系统板但未必清楚最小系统里到底哪些东西是必须的。简单说一颗F103要跑起来至少需要3.3V电源纹波别太大、8MHz外部晶振HSE给PLL提供时钟源、复位电路NRST引脚接RC复位或者直接接复位芯片、BOOT0和BOOT1引脚的电平设置一般BOOT0接GND从Flash启动、SWD调试接口PA13/PA14强烈建议留出来不然程序烧不进去只能干瞪眼。这些在最小系统板上都已经做好了直接插上USB转串口或者ST-Link就能用。有一点要提醒F103虽然内部有HSI振荡器但以太网对时钟准确性要求比较高如果完全靠HSI跑时钟误差可能会导致网络通信时偶发丢包。所以尽量保证8MHz外部晶振是正常工作的不要图省事把时钟配置成HSI。2.2 ENC28J60引脚与STM32的接线表ENC28J60模块最常见的封装是28脚SOIC引脚功能其实很简单核心就是SPI四根线加一根片选和一根中断。我用的接线是SPI1STM32F103引脚功能ENC28J60引脚说明PA5SPI1_SCKSCKSPI时钟速率先用2MHz调试稳定后可提到10MHzPA6SPI1_MISOMISO芯片输出到MCU配置为浮空输入或上拉输入PA7SPI1_MOSIMOSIMCU输出到芯片PA4GPIO推挽输出CS片选低有效软件控制PA8GPIO输入/EXTIINT中断输出收到数据包时拉低可接可轮询3.3V电源VCC模块供电注意电流需求约180mAGND地GND共地关于PA8这个引脚多说一句。经常有人问“STM32F103 PA8引时钟”能不能直接给ENC28J60提供时钟因为PA8是MCO引脚可以输出时钟。答案是MCO输出不了25MHz。F103的MCO可以从HSI、HSE或PLL中选一路输出但PLL的输出频率一般配置成36MHz或72MHz没有25MHz这个档位。ENC28J60需要的是25MHz的时钟源所以老老实实用板载25MHz无源晶体不要把MCO接到ENC28J60的OSC输入上。PA8拿来接中断没问题但注意初始化的时候不要让GPIO配置成MCO复用功能否则中断信号会被时钟输出电路干扰。2.3 电源、复位、网络变压器的工程细节ENC28J60这个芯片别的都好就是对电源稳定性有点挑剔。它的模拟电源和数字电源虽然都标3.3V但如果供电纹波太大以太网PHY的信号质量会变差表现就是偶尔连不上、ping不通、甚至网口指示灯忽明忽暗。我实测下来模块供电线上一定要加一个100uF电解电容和若干个0.1uF陶瓷电容放在模块电源引脚附近最好再串一个磁珠做电源隔离。如果直接用USB转串口模块的3.3V供电电流可能不够建议单独接稳压芯片。复位电路也容易被人忽略。ENC28J60的复位引脚要确保上电后能稳定拉高。有些模块直接把RESET引脚悬空靠芯片内部上电复位实际使用中容易出现复位不彻底导致寄存器状态混乱的情况。稳妥做法是用一个10k电阻把RESET上拉到3.3V再并联一个1uF电容到地这样上电时RC充电延时能保证复位脉冲宽度足够。网口变压器这一块如果用的是带网口座的模块比如HR911105A这种集成RJ45座和变压器的直接插RJ45网线就行。如果是裸芯片设计就需要自己在PHY和RJ45座之间加网络变压器。注意变压器中心抽头的连接方式有的需要接电源、有的需要接电容到地具体要按ENC28J60数据手册里的典型应用电路接不然会出现在实验室能通、换一根线就断的诡异问题。3. 软件架构轻量TCP/IP协议栈的选型与移植3.1 uIP还是lwIP一个小项目背后的选择逻辑硬件连好了接下来是软件。ENC28J60只负责以太网物理层和数据链路层TCP/IP协议栈必须自己跑。当时我的候选有两个uIP和lwIP。uIP是Adam Dunkels写的一个极简TCP/IP协议栈整个协议栈的代码量很小RAM占用可以控制在几KB以内Cortex-M3上跑得飞快。它的设计思路是“事件驱动轮询”所有协议栈处理都发生在一个主循环里没有线程、没有信号量非常适合裸机MCU。代价是它的API比较原始TCP连接数默认只有4个而且不支持分片很多高级特性也被砍掉了。lwIP是功能完整得多的协议栈支持多线程、内存池管理、RAW API和Netconn API但完整跑起来需要至少十几KB RAM还要配RTOS才能发挥威力。F103C8T6只有20KB RAM跑lwIP有点侥幸好Mi但留给应用的空间就非常紧张了。而且lwIP移植起来比uIP复杂要先处理内存堆、信号量、网卡驱动接口一堆东西。我给这个项目的选择是uIP核心考虑就一条越简单越可靠。既然网页服务器只需要处理极少量并发访问uIP的4个TCP连接完全够用了。再加上ENC28J60驱动在网上有大量现成代码uIPENC28J60的组合几乎是“开箱即燃”的经典搭配。3.2 ENC28J60裸机驱动要点移植uIP之前你得先有一个能用的ENC28J60驱动。驱动分三层第一层是SPI底层通信。ENC28J60的SPI指令集是固定的8位命令比如读控制寄存器0x00、写控制寄存器0x40、读缓冲区0x3A、写缓冲区0x7A、置位0x80、清位0xA0。所有指令都是高位在前CS拉低后发命令然后读写数据。第二层是寄存器初始化。初始化流程大概是复位芯片写RESET命令要求SPI时钟低于某个频率老驱动里这一步会延时几十毫秒、等待芯片准备好读ESTAT寄存器的CLKRDY位、设置MAC地址MAC寄存器有6个字节、配置接收缓冲区范围ERCST和ERXND指向8KB收包缓冲、配置收包过滤ERXFCON允许接收广播帧和自己MAC地址的帧、开启接收置位ECON1的RXEN。这一套做完网卡就能收到数据包了。第三层是数据收发。发包比较简单把要发的数据通过写缓冲区指令逐字节写入发送缓冲区然后写入发送包大小置位ECON1的TXRTS开始发送发送完再清掉这个位。收包稍微绕一点芯片收到的每个以太网帧会存放在接收缓冲区里缓冲区头4字节是这个包的“下一包指针”和“接收状态”继续往下读才是完整的以太网帧。因为收包缓冲区是环形结构所以驱动里要维护一个NextPacketPtr变量每次取完包后都要更新它。注意网上流传的很多ENC28J60驱动在SPI初始化时直接把速率开到10MHz以上结果读取版本寄存器返回的全是0xFF或者乱码。稳妥的调试顺序是先降到1MHz~2MHz确认能正确读到芯片版本号寄存器地址0x29再逐步提高SPI分频系数。不要一上来就超频不然你会在“为什么读不到寄存器”这个问题上卡很久。3.3 移植uIP需要提供的几个接口uIP本身并不关心底层网卡是什么它只定义了四个和网卡相关的接口初始化时设置网卡MAC地址、从网卡读取一个包并交给上层、把一个包通过网卡发送出去、以及获取当前时间用于TCP超时重传计算。我们需要做的就是把ENC28J60驱动接到这三个口上。其中最关键的是发送回调。uIP在需要发送数据时会调用uip_linkoutput()这个函数里要调用ENC28J60的发送函数把uip_buf里的数据发出去。收包则是在主循环里不断调用ENC28J60的收包函数读到完整以太网帧后把数据拷贝到uip_buf里然后调用uip_input()交给协议栈处理。uIP处理完TCP数据后会立即调用uip_linkoutput()发回响应所以你会在主循环里看到这么一种结构while (1) { if (enc28j60_packet_receive(uip_buf, UIP_BUFSIZE) 0) { uip_len packet_len; uip_input(); // 交给uIP协议栈处理 if (uip_len 0) { // 协议栈生成了响应数据 enc28j60_packet_send(uip_buf, uip_len); } } timer_poll_count; if (timer_poll_count TCP_POLL_INTERVAL) { uip_periodic(TCP_POLL_INTERVAL); // 周期性处理各个TCP连接 if (uip_len 0) { enc28j60_packet_send(uip_buf, uip_len); } } }这里有一点很容易搞混uip_input()和uip_periodic()处理后uip_len可能变为0表示没有数据要发也可能非0表示协议栈把响应数据放进了uip_buf。所以每次都要判断uip_len再决定是否调用网卡发送函数。如果漏了这个判断就会出现协议栈要发数据但你压根没发出去的灵异问题。4. 让浏览器能访问Web服务器实现细节4.1 网页资源如何装进Flash网上不少人一开始是用PC上的网页编辑器把页面写好然后想办法塞进MCU。这个思路没错但具体实现时要特别注意不要把网页字符串定义成函数内的局部数组。F103的RAM本来就小一个几KB的HTML页面要是定义成局部变量栈瞬间就被打爆了。正确做法是把网页内容定义成全局常量存放在Flash里。方法是static const char index_html[] HTTP/1.1 200 OK\r\n Content-Type: text/html; charsetutf-8\r\n Cache-Control: no-cache\r\n Connection: close\r\n \r\n !DOCTYPE html htmlheadtitleSTM32 Web Server/title/head body h2STM32F103 ENC28J60/h2 ADC value: b%d/bbr a href\/?led1\buttonLED ON/button/a a href\/?led0\buttonLED OFF/button/a /body/html;用static const修饰后字符串会放在Flash的只读区不占RAM。而且C语言标准库的sprintf()可以用%d把ADC值动态填充进去这样页面既能显示实时数据又能带按钮控制一举两得。编译完成后打开生成的.map文件看一眼Flash占用情况如果超过60KB就要注意精简页面内容了。我习惯把网页内容单独放在一个webpage.h头文件里用宏把各个页面的数组名统一管理比如/index.html对应index_html[]/adc.html对应adc_html[]。这样要加新页面时只需要在头文件里加一个新的数组和路径映射就行。4.2 HTTP协议处理核心逻辑浏览器访问网页时实际发生的是一个HTTP GET请求。比如你在地址栏输入http://192.168.1.10/浏览器会往板子发一个类似GET / HTTP/1.1\r\nHost: 192.168.1.10\r\n\r\n的字符串。uIP协议栈会把TCP数据部分放在uip_appdata指向的缓冲区里我们需要在uIP的应用回调UIP_APPCALL里解析这段字符串然后根据请求路径返回对应内容。uIP的应用回调函数生命周期是这样的一个TCP连接建立后如果收到数据uIP会调用UIP_APPCALL()并且uip_newdata()返回非零如果要关闭连接就调用uip_close()。所以Web服务器的核心逻辑可以写在UIP_APPCALL()里void UIP_APPCALL(void) { if (uip_newdata()) { // 从uip_appdata中查找请求行 char *buf (char *)uip_appdata; // 解析出请求路径比如 / 或 /?led1 if (strncmp(buf, GET /, 5) 0) { parse_request(buf); // 处理路由和参数 } // 构造HTTP响应并发送 uip_send(response_buf, response_len); } if (uip_acked()) { // 数据被确认可以继续发送或关闭连接 } if (uip_rexmit()) { // 重传需要重新发送上次要发的数据 uip_send(response_buf, response_len); } if (uip_closed() || uip_aborted() || uip_timedout()) { // 连接关闭做清理工作 } }这里最容易被忽略的是uip_rexmit()分支。uIP在发送数据后如果超时未收到ACK会要求重传。重传时它不会保留你的应用数据而是再次回调UIP_APPCALL()并设置uip_rexmit()。如果你的代码只在uip_newdata()分支里发送响应那重传时什么都不会发客户端那边就会一直卡着直到连接超时。正确做法是把“构造响应数据”的逻辑抽成一个函数在uip_newdata()和uip_rexmit()里都调用它。4.3 带参数控制和中文显示的页面网页服务器最有意义的地方在于远程控制。我最常用的方式是“GET参数解析”。比如点页面上的“LED ON”按钮时浏览器会请求/?led1MCU解析这个参数后拉高GPIO再返回一个刷新后的页面。解析代码也不复杂char *param strstr(buf, led); if (param ! NULL) { if (param[4] 1) { GPIO_SetBits(GPIOC, GPIO_Pin_13); // 点亮LED } else { GPIO_ResetBits(GPIOC, GPIO_Pin_13); // 熄灭LED } }“led”这个子串在HTTP请求里是唯一的所以直接通过字符串查找就能拿到参数不需要引入复杂的JSON解析库。如果要传多个参数就用分隔逐个用strstr()查找。这种方式在控制类项目里非常够用。中文显示是另一个大坑。如果HTTP响应头里的Content-Type写的是text/html浏览器默认会用系统编码解析如果页面里有中文很可能出现乱码。解决办法是在响应头里显式声明字符集Content-Type: text/html; charsetutf-8。同时要确保你的源代码文件本身是UTF-8编码保存的这样index_html[]里的中文在编译后是以UTF-8字节序列存在Flash里的浏览器收到后才能正确解码。不少同学在这上面折腾半天最后发现是编辑器的“ANSI编码”惹的祸。控制PWM类输出时还有一个细节值得提一下如果网页要下发占空比参数比如设置亮度为100%你会发现很多PWM库的占空比设置函数在写最大ARR值时输出反而变成常低。这是因为有些库的计算方式是compare (arr 1) * duty / 100 - 1当duty为100时计算值会超过ARR。所以要么在应用层对100%单独处理直接把引脚置高要么在配置定时器时把ARR再放宽一格。我当初在这个问题上调了半天网页上填100%灯反而灭了最后在逻辑里加了一个判断才解决。5. 联调测试与实战排坑5.1 底层通信不上时先查这几件事网卡驱动调试是有顺序的。我习惯按“SPI通信 → 寄存器操作 → MAC收发 → TCP连接 → HTTP响应”这个顺序一层一层来每一层验证通过后再进到下一层。如果连SPI初级的寄存器读都读不到优先怀疑三点芯片供电是否稳定用万用表量3.3V看有没有明显跌落、复位引脚是否一直为低用示波器看复位波形、SPI引脚是否接对很多模块的CS引脚丝印是“CS”但实际低有效检查是不是被外部上拉电阻抗住了。还有一种很隐蔽的问题STM32的SPI1在PA5/PA6/PA7上但有些开发板把这几个引脚复用成了JTAG调试口的一部分导致初始化后引脚状态不对。解决办法是把GPIO_InitStructure.GPIO_Mode明确配置成GPIO_Mode_AF_PP并且在初始化SPI之前不要打开JTAG复用的调试功能只保留SWD。读寄存器验证方法很简单初始化后调enc28j60ReadOp(ENC28J60_READ_CTRL_REG, EREVID)如果返回值不是0xFF、也不是0x00说明SPI通路通了。我手头那片ENC28J60读出的版本号是0x02不同批次可能不同只要不是0xFF就是正常的。5.2 网络层连不通的排查路径SPI通了、驱动没问题但电脑ping不通板子这时要分流检查几个环节。第一PC和板子必须在同一个网段。板子的默认IP一般设成192.168.1.10PC的IP必须手动设成192.168.1.x掩码255.255.255.0。很多朋友直接让PC开DHCP自动获取结果PC拿到的IP是192.168.31.x跟板子的192.168.1.x不在一个网段自然ping不通。最简单的办法就是在PC网卡设置里手动配一个静态IP。第二ARP这个坎。ping之前先看ARP缓存里有没有板子的MAC地址。命令行输入arp -a如果出现了192.168.1.10 xx-xx-xx-xx-xx-xx说明二层通信已经通了问题出在TCP/IP层如果ARP表里压根没有说明收发数据链路还没通回到第5.1节继续查。第三Windows防火墙经常拦截来自未知设备的ping包。局域网调试时可以先临时关闭防火墙确认能ping通后再重新打开并配置入站规则。这个坑非常常见我一度以为代码的问题最后发现是防火墙拦了ICMP。5.3 常见问题速查表现象可能原因解决办法读ENC28J60版本寄存器返回0xFFSPI速率过高、引脚配置错误、芯片未复位成功把SPI分频降到2MHz检查复位电路和CS电平ping通但网页打不开HTTP连接数耗尽、uIP的应用回调里没发送响应检查UIP_CONNS是否默认4个、是否有连接一直不关闭网页打开很慢刷新要等几秒uIP超时重传机制触发、SPI速率太低适当提高SPI分频缩短TCP超时时间检查程序是否在uip_rexmit分支漏处理中文乱码响应头缺charset、源码文件编码不是UTF-8在Content-Type里加charsetutf-8代码文件另存为UTF-8网口指示灯亮但数据不通网络变压器接线错误、PHY信号质量差检查RJ45座与芯片之间变压器接线按数据手册调整中心抽头刷新几次后板子无响应TCP连接没正常关闭连接被占满在响应头加Connection: close处理完显式调用uip_close()这里特别要强调“Connection: close”这个头。uIP默认不会主动断开TCP连接如果你在响应头里不带这个字段浏览器会把TCP连接保持一段时间以复用。而uIP最多只能开4个连接一旦4个连接都被占住还没释放新的HTTP请求就会被丢弃。解决办法就是在响应头里明确告诉浏览器“发完这段数据就断”然后在代码里调用uip_close()主动关闭连接。5.4 一个真实问题ADC数据刷新为什么卡顿我的第一版页面里浏览器用meta http-equivrefresh content1每秒自动刷新一次用来显示ADC实时值。实测发现页面偶尔会卡住十几秒才有反应。查了一圈发现问题出在TCP连接没有完全关闭浏览器刷新时旧连接还在TIME_WAIT状态新连接请求被uIP的连接数限制挡住了。解决方法是在HTTP响应头加上Cache-Control: no-cache防止缓存同时把自动刷新间隔从1秒改成2秒给连接关闭留出余量。另外在UIP_APPCALL()里处理完响应后立即调用uip_close()避免连接被无谓地保持。改完之后刷新就稳定了。6. 进阶玩法这个Web服务器还能做什么上面的方案做出来之后其实你已经有了一套完整的“MCU网卡Web”基础架构。顺着这个结构往下扩展能做的东西非常多。一是把uIP换成lwIP配合FreeRTOS跑多线程应用。如果你之后要升级到F103RCT6或者F407RAM大了lwIP可以提供更稳定的多连接支持Web服务器也能同时处理更多客户端。二是把HTTP响应做成模板化路由。比如/control.cgi?relay3stateon这种带路径和参数的请求写一个简单的链表路由表就能扩展出继电器控制、传感器数据查询、设备状态配置多种功能。三是加上MQTT或者简单的UDP协议。uIP虽然轻量但TCP-only的Web方案对实时性要求高的场景不够灵活。可以再开一个UDP通道走简单的自定义协议上报采集数据或者接收命令这样Web页面负责人工控制UDP通道负责自动化数据交换。四是在页面里嵌入JavaScript和AJAX。因为STM32的资源有限跑不了复杂的动态网页框架但完全可以在页面里写一个setInterval()定时请求/data路径返回一段纯文本的ADC数值然后用JavaScript更新DOM。这样比整页刷新轻量得多也不会频繁占用TCP连接。我后来在这个板子上就是这么做的页面稳定多了还可以加个简单的Canvas图表显示波形。说实话STM32F103ENC28J60这套组合放到今天已经不算“高性能”了但它胜在稳定、便宜、资料多特别适合拿来理解网络协议栈的底层原理。我做完这个项目之后再去看lwIP和TCP三次握手理解深度完全不一样了。如果你也想在嵌入式里玩网络真的建议先从这个组合开始试一试。本文还有配套的精品资源点击获取
返回列表