ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

ESP32 应用平台:像装 App 一样装脚本,免编译烧录

ESP32 应用平台:像装 App 一样装脚本,免编译烧录 ESP32 能不能像手机一样安装应用你别说还真能只是这个“应用”不是 APK也不是 IPA而是一份脚本加配置文件。我最近在 ESP32 上做了一套小型应用平台改功能不再需要反复拔线重烧直接在网页里上传脚本就能“装应用”重启后还能自动运行。这篇文章会把整套思路、架构、代码和我在实操中踩过的坑都摊开讲一遍如果你也在纠结“固件烧写到什么时候是个头”那这篇内容值得看完。1. 为什么要在 ESP32 上做“应用平台”1.1 传统开发方式真正让人难受的地方这几年玩 ESP32 的人越来越多环境从 Arduino 到 ESP-IDF 都有但无论走哪条路只要还保持“改一行代码就编译一次、烧录一次”的节奏时间一长一定会暴躁。我自己的经历是给家里做一个小型桌面终端显示温湿度、控制灯带、偶尔读几个传感器。刚开始挺开心写了几版后开始变味改一个阈值要重编译调一个页面样式也要重编译板子装在亚克力盒子里每次烧录都要拆壳接线。这些痛点的本质是你的“固件”和“你的业务逻辑”没有解耦。设备烧进什么就是什么想变只能整体更换二进制。手机系统之所以舒服是因为操作系统只负责基础服务和运行环境真正的功能来自“应用”应用可以被单独安装、升级、卸载。ESP32 虽然小但它的 Flash、RAM、网络能力完全可以支撑一套简化版的应用管理机制。1.2 这里说的“安装应用”到底在装什么不用把“安装应用”这四个字想得太高大上。拆开看手机安装一个应用其实做了三件事把代码和资源放到系统指定的目录写入一条注册记录告诉系统“这个应用存在、入口在哪”然后由系统的启动器或用户去触发运行。ESP32 上做同样的事就是三步脚本文件放到一个固定目录比如/apps/。维护一份清单apps.json记录应用名称、版本、入口文件、是否开机自启。用一段固定的服务代码去读取这份清单并提供 HTTP 接口让用户在网页上执行安装、卸载、启动。由于 ESP32 并不是一个完整的操作系统跑的不是 Linux所以这份“平台”是搭在一个运行时的上面的。我这里采用的是 MicroPython 方案应用就是.py脚本。解释器本身先作为固件烧进去之后换功能只动脚本不再动底层固件。这套思路搬到 ESP-IDF 上同样成立只是你需要一个脚本引擎或者一个能动态加载二进制镜像的 loader。2. 整体架构与方案选型2.1 三条可行路线对比要做“应用安装”这件事方案不止一种我梳理了三种比较主流的路线各有各的适用场景。方案实现思路优点缺点适合谁解释器脚本方案固件烧入 MicroPython 或 Lua 运行时应用为脚本文件开发效率高改脚本即生效排查方便运行效率比原生二进制低RAM 占用固定成本较高DIY 爱好者、小批量产品原型、快速迭代场景OTA 双分区方案将不同功能固件打包为多个分区镜像通过 OTA 下载切换代码执行效率高可复用原生 SDK 生态更像“系统升级”不是独立应用多应用共存复杂对性能敏感、功能相对固定的产品原生二进制动态加载使用 IDA/链接脚本将独立 app 编译为位置无关代码由 loader 加载性能最好接近手机上本地应用门槛高资料少排错难度大有时间啃底层、需要极致性能的人多数人不是在做操作系统内核而是想让开发过程舒服一点所以我选了第一种。脚本应用在当前 ESP32 的算力下做控制逻辑、采集逻辑、简单的 HTML 服务端页面都足够真到了性能瓶颈再考虑把某个应用改写成原生模块也不迟。2.2 平台的五层结构这套微型应用平台架构上分成五层每一层各管各的事硬件驱动层GPIO、I2C、SPI、UART、ADC、网络外设的驱动。运行时层MicroPython 解释器负责执行脚本屏蔽底层寄存器和驱动细节。文件系统层基于 Flash 的 LittleFS用来存放应用脚本和配置。服务管理层核心服务负责扫描应用清单、提供 HTTP 上传/列表/运行接口、开机启动调度。应用层用户开发的具体功能脚本比如 LED 控制应用、传感器展示应用。手机会动不动弹“要不要允许这个应用访问传感器”本质是权限系统。在单片机平台上我先不做这么复杂的权限控制但通过这种分层已经能保证“平台代码”和“应用代码”不相干应用崩了甚至文件删了平台主体还是好的。这一点在后面的容错设计里我会专门讲。2.3 硬件准备与 Flash 地址规划我这套用的是一块最普通的 ESP32 DevKitCFlash 大小 4MB。别小看 4MB对脚本应用来说空间非常奢侈。MicroPython 官方固件默认会烧写 bootloader、分区表、MicroPython 固件然后在末尾自动划出一个文件系统分区。查看当前文件系统大小可以进 REPL 执行import os print(os.getvfs(/).info())打印出的信息里能看到文件系统总容量和剩余空间。如果嫌官方默认分区给文件系统的空间太小可以自己改分区表。ESP-IDF 的分区表是一个 CSV 文件我这边用一个典型的分区设定示例# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 0x200000, storage, data, fat, 0x300000,0x100000,分区表里factory是主固件区如果启用了 OTA 机制可以分出ota_0和ota_1来。storage分区就是留给文件系统的。在设计这套“安装应用”的平台时一个很重要的原则是固件区是运行基础文件系统区才是应用安装的位置两者物理隔离才不怕应用写坏系统。3. 实操过程从零搭建微型应用平台3.1 烧录 MicroPython 基础固件整个过程的第一步是给 ESP32 烧一个带解释器的基础固件。去 MicroPython 官网下载适用于 ESP32 的.bin文件就行选带SPIRAM或者不带要看你的板子我手头这块没有 PSRAM就用标准版。烧采用 esptool先擦除整片 Flash再写入固件esptool.py --port COM3 erase_flash esptool.py --port COM3 --baud 460800 write_flash -z 0x1000 ESP32_GENERIC-20240602-v1.23.0.bin提示0x1000是 ESP32 出厂规定的 bootloader 起始偏移不要乱改。如果看到A fatal error occurred: Failed to connect to Espressif device大概率是芯片没有进入下载模式按住板子上的 BOOT 键再点烧录即可。烧录完用 PuTTY 或 Thonny 打开串口复位一下应该能看到的 Python 提示符。到这一步你的 ESP32 已经从一台“需要整体编译烧录的设备”变成了一个“具备脚本运行能力的设备”。后续再改业务逻辑不需要回到编译器直接在 Thonny 里编辑脚本上传就行。3.2 网络接入给平台装上“下载通道”要实现网页里“安装应用”网络是必须的。ESP32 的优势是自带 Wi-Fi代码很短。我通常在main.py里固定写一个连接 Wi-Fi 并输出 IP 地址的模块import network import time def connect_wifi(ssid, password, timeout15): wlan network.WLAN(network.STA_IF) wlan.active(True) wlan.connect(ssid, password) start time.time() while not wlan.isconnected(): if time.time() - start timeout: raise RuntimeError(Wi-Fi connect timeout) time.sleep(0.5) print(IP:, wlan.ifconfig()[0]) return wlan.ifconfig()[0]如果你的设备使用了 LAN8720 以太网模块那需要走network.LAN接口这个我在后面“常见问题”里专门说因为坑很多。网络通了之后“应用平台”的服务端就可以绑定在0.0.0.0:80上这样手机、电脑在同一个局域网里打开浏览器输入 ESP32 的 IP就能访问应用管理界面。3.3 编写核心服务应用管理器这个应用管理器是整个平台的心脏我用 MicroPython 的socket模块手写了一个精简 HTTP 服务不依赖任何第三方网络库方便你在同样环境下直接复现。核心逻辑如下import os import json import socket import gc APPS_DIR /apps MANIFEST /apps/apps.json def load_manifest(): try: with open(MANIFEST) as f: return json.loads(f.read()) except OSError: return {installed: []} def save_manifest(manifest): with open(MANIFEST, w) as f: f.write(json.dumps(manifest)) def install_app(filename, code): if not os.path.isdir(APPS_DIR): os.mkdir(APPS_DIR) path APPS_DIR / filename with open(path, w) as f: f.write(code) manifest load_manifest() for app in manifest[installed]: if app[file] filename: app[version] 1.0 save_manifest(manifest) return update manifest[installed].append({ name: filename.replace(.py, ), file: filename, version: 1.0, autorun: False }) save_manifest(manifest) return install def list_apps(): manifest load_manifest() html htmlheadtitleESP32 App Store/title/headbody html h1ESP32 应用管理/h1 html h3已安装应用/h3ul for app in manifest[installed]: html fli{app[name]} - v{app[version]} html f[a href/run?app{app[file]}运行/a] html f[a href/uninstall?app{app[file]}卸载/a]/li html /ul html h3上传安装/h3 html form methodPOST action/upload html input typetext namefilename placeholderapp_name.py html textarea namecode rows8 cols60 placeholder粘贴脚本代码/textarea html input typesubmit value安装 html /form/body/html return html这里我没有直接把 HTTP 解析写满因为你看到的是骨架实际操作时还需要在do_GET和do_POST里解析请求行和请求体取到/upload表单里的filename和code字段再交给install_app。为了保证 RAM 不被大请求撑爆上传的代码不要太大几百行以内的脚本都没问题。如果将来想上传更复杂的应用建议改成按块写入或压缩包方式。3.4 演示应用通过网页远程开关 LED平台有了还要真正“装上”一个应用验证流程。我写了一个最简单的 LED 控制应用它的功能是提供一个小网页点按钮就翻转 GPIO2 的电平。这个应用本身就是一段独立脚本放到/apps/app_led.pyimport machine import socket import network led machine.Pin(2, machine.Pin.OUT) def page(): state OFF if led.value() 0 else ON return fhtmlbodyh1LED 控制/h1 fp当前状态: {state}/p fa href/toggle点我翻转/a/body/html def run_server(): s socket.socket() s.bind((0.0.0.0, 8080)) s.listen(2) while True: conn, addr s.accept() req conn.recv(1024).decode() if /toggle in req: led.value(1 - led.value()) conn.send(HTTP/1.1 200 OK\r\nContent-Type:text/html\r\n\r\n page()) conn.close()安装这个应用的时候通过管理平台的网页表单填文件名app_led.py粘贴上述代码点提交。之后浏览器访问http://esp32-ip:8080就能看到这个应用的独立控制界面。这个例子的意义在于应用和平台之间的关系就像手机装上了一个独立 App。用户不会感受到底层“Flash 里有几个文件”他们只知道访问网页就能控制 LED。3.5 开机自启动与“应用入口”的调度手机开机之后你可以手动点开某个 App也可以设置自启动。我在平台里也做了类似机制main.py里启动网络、启动管理服务、然后扫描清单里autorun为True的应用。from app_manager import load_manifest, start_http_server def main(): connect_wifi(your_ssid, your_password) start_http_server() manifest load_manifest() for app in manifest[installed]: if app.get(autorun): print(autorun, app[file]) # 通过 importlib 或动态执行机制加载应用 if __name__ __main__: main()动态执行应用有很多种方式比如importlib.import_module或者直接exec(open(path).read())。我建议是每个应用本身也做成一个函数入口由管理器在子线程中执行这样某个应用阻塞了也不影响整个管理平台继续对外服务。这里有个容易被忽略的点MicroPython 的线程支持比较基础多线程时要注意 GIL 和栈空间限制所以在exec那种粗暴方式里要格外小心。4. 常见问题与排查技巧实录4.1 LAN8720 以太网模块的三个高频坑如果你的设备是用网线接入局域网的就会用到 LAN8720。这个模块本身不贵但接 ESP32 时非常容易翻车。我在这里把最有代表性的三个问题讲透。第一个坑PHY 芯片的复位时序。LAN8720 有个nRST引脚如果它一直处于低电平芯片就永远处于复位状态网络自然起不来。最常见的错误是把nRST直接接到 ESP32 的 EN 引脚这样 ESP32 复位后 PHY 也跟着复位但 ESP32 主控初始化网卡时 PHY 可能还没准备好问题就随机出现了。解决办法把nRST接到 ESP32 的一个普通 GPIO比如 GPIO18然后在代码里手动延时并拉高reset_pin machine.Pin(18, machine.Pin.OUT) reset_pin.value(0) time.sleep_ms(100) reset_pin.value(1) time.sleep_ms(150)接线就按 GPIO18 到 LAN8720 的 nRST中间不用加电阻电平兼容性没问题。第二个坑REF_CLK 时钟源。LAN8720 支持从外部晶振或从主控输入 50MHz 参考时钟。很多模块板载了 50MHz 晶振有的却没有而 ESP32 的 RMII 接口要求ETH_CLOCK_GPIO0_IN或ETH_CLOCK_GPIO17_OUT二选一。如果是板载晶振软件配置为输入模式如果是外部输出要在 GPIO17 引 50MHz。配置错了现象就是一直连不上错误日志里全是TIMEOUT。第三个坑供电。LAN8720 是 3.3V 供电但一些模块在启动瞬间电流不小。如果从 ESP32 开发板的 3V3 引脚直接取电而旁边又带着屏幕、传感器电压容易跌落网络会时好时坏。我第一次遇到就很玄学刚开机时能连上一操作外设就掉线。最后查下来是供电问题建议单独用一片 AMS1117 降压模块从 5V 取电给 LAN8720。下面是我调试通过后的接线参考LAN8720 引脚ESP32 引脚VCC3V3或独立 3.3V 电源GNDGNDMDCGPIO23MDIOGPIO18RXD0GPIO19RXD1GPIO21CRS_DVGPIO22TX_ENGPIO25TXD0GPIO26TXD1GPIO27nRSTGPIO18注意与 MDIO 区分建议换一个空闲脚REF_CLK按模块晶振情况接 GPIO0 或 GPIO17建议真正布线时把 MDIO 和 nRST 分到两个不同 GPIO我之前图省事把它们重合导致 PHY 复位后 MDIO 总线一直异常。4.2 烧录与文件系统典型故障上传代码失败、文件写入提示 OSError检查路径是否存在MicroPython 的/apps/目录如果要手动创建用os.mkdir(/apps)。有些固件默认文件系统是只读挂载需要先卸载再重新挂载或者干脆重刷固件。文件系统损坏最直接的表现是启动后import os; os.listdir()报OSError: 19。解决方法是擦除 Flash 后重刷然后重新上传脚本。不要尝试在设备上做太多救援操作浪费的时间足够重刷第二遍。应用平台被自己的应用影响这一点很关键。我踩过一次很深的坑在做一个睡眠主题应用时脚本里写了machine.deepsleep()结果平台管理服务也被拉下去了。后面我把应用执行统一放在独立线程里并且在启动应用前做了gc.collect()保证内存回收及时。4.3 硬件资源限制与运行时的“隐藏规则”ESP32 的 RAM 只有 320KB 左右实际可用到 MicroPython 堆的也就 100KB 上下。这个体量在“应用平台”层面够用但你不能指望跑重型计算。我在实际测试中一个控制页面加几个传感器读数的应用堆内存占用大约 15KB还能接受。如果你要跑图像处理或者复杂的加密逻辑脚本方案就吃力了。另外一定要提 ESP32 的 ADC 问题。用 ADC2 的通道采集时如果 Wi-Fi 同时在工作采集结果可能一直不对因为 ADC2 和 Wi-Fi 是共用硬件的。我建议所有模拟量采集尽量走 ADC1GPIO32-GPIO39同时做好电压分压校准毕竟 ESP32 ADC 在中段比较线性靠近两端会有明显非线性你需要查原始曲线或者自己多点校准。4.4 快速排查速查表现象可能原因解决办法点击安装后没有出现在列表表单字段名不一致导致安装函数没拿到参数确认 POST 字段为filename和code访问管理页面一直转圈服务端线程阻塞或卡在某个应用执行上重启设备启动平台后再手动触发问题应用上传大脚本后设备重启接收请求时recv缓冲区太大导致 RAM 不足限制上传大小或改用分块接收Wi-Fi 连接不稳定供电不足或信号弱独立供电调整天线方向减少高功率外设平台服务能起来但应用自动启动失败应用脚本有语法错误或缺少依赖在 REPL 里单独执行该脚本确认报错信息5. 这个平台还能用在哪些场景5.1 小批量设备上的“在线增装”如果你做的是小批量的设备比如帮朋友做十来个环境监测终端过去每改一次需求都要把所有设备拆下来烧录。有了这套应用平台设备只需要第一次烧好基础固件后面全部通过网页更新脚本远程运维的体验完全不一样。我身边有人用这个思路做桌面终端设备放在客厅人坐在工位上打开浏览器就能往终端上装一个“客流统计”应用或者“白噪音播放”应用这就是“ESP32 终端”该有的自由度。5.2 边缘采集与异常检测工业场景里的轴承故障检测、电机振动分析搞起来不便宜但 ESP32 加传感器做轻量级预筛选完全可行。你可以把传感器驱动、特征计算、告警上传这些模块拆成独立应用基础平台只负责采集原始数据装一个“FFT 频谱分析”应用就做频域分析装一个“阈值报警”应用就只发告警。应用之间互不影响升级某个算法也不用整机停摆。这种模块化能力在产品原型验证阶段特别有用。5.3 后续扩展方向这套平台目前只实现了“文件即应用、清单即注册”的雏形但扩展空间足够大。你可以考虑增加以下能力用 Zip 格式打包应用包含代码、网页静态资源、应用图标上传后自动解压更像手机应用包。增加简单鉴权 Token避免局域网内任意设备都能改应用设置。应用间通信基于ujson或msgpack通过统一消息总线传递数据。给文件系统外接 SD 卡让应用区容量从几百 KB 扩展到几 GB。极端性能需求下改造部分应用为原生二进制并通过 loader 动态加载把解释型应用和原生应用混合管理。我之前也试过直接在 ESP-IDF 里写一个真正的动态加载器思路和上面的原生二进制路线一致但开发节奏确实慢很多目前更稳定的组合还是 MicroPython 为主。建议先从脚本应用开始把平台用起来再逐步按需升级。最后再分享一个小技巧平台入口脚本main.py里尽量给所有可能出错的调用都加上try/except让应用级别的问题最多只打印错误日志不中断整个平台进程。我最初就是因为没加保护一个应用语法错误直接让整个 ESP32 陷入反复重启排查过程比写代码本身还痛苦。把平台基础稳定住后面再“安装”多少应用都不慌。
返回列表