WSL2 Ubuntu 22.04 LTS ESP-IDF 5.5 MicroPython 1.28 固件编译指南
摘要 :本文详细记录了在Windows 10 + WSL2 Ubuntu 22.04环境下,从零搭建ESP-IDF v5.5.1和MicroPython v1.28.0完整编译环境的实战经验。涵盖WSL2安装配置、ESP-IDF工具链部署、MicroPython源码准备、ESP32-S3-SPIRAM_OCT固件编译、自定义板ECM50B适配、常见网络问题解决方案、组件裁剪优化(蓝牙/NimBLE/BTree/WiFi AP/LWIP PPP等)、WebREPL模块修复等全流程。重点解决了编译过程中的16个典型弯路,提供了完整的故障排查方法和实用脚本,最终产出可烧录的
ESP32_GENERIC_S3-SPIRAM_OCT-YYYYMMDD-v1.28.0.bin固件,适用于ESP32-S3 N16R8硬件平台。
作者在 Windows 10 专业版 + WSL2 环境下,从零搭建到 MicroPython v1.28.0 固件编译通过 的完整实战经验整理。最终目标:ESP-IDF = 构建系统 + 所有底层工具链 (编译器/链接器/烧录器),MicroPython = 被编译的源码 (不自带任何底层工具)。
最终验证:ESP32_GENERIC_S3 + BOARD_VARIANT=SPIRAM_OCT(S3N16R8,16MB Flash + 8MB Octal PSRAM)+ 自定义
.py文件冻结 → 产出ESP32_GENERIC_S3-SPIRAM_OCT-20260406-v1.28.0.bin风格固件。
0. 硬件 & 软件环境配置(本机)
| 类别 | 配置 |
|---|---|
| Windows 版本 | Windows 10 专业版 22H2(OS 内部版本 19045.6466) |
| Windows 安装日期 | 2024/12/15 |
| 设备名 | DESKTOP-ETJIDDN |
| CPU | Intel® Celeron® N5105 @ 2.00 GHz(4C4T,Jasper Lake,用于编译建议使用 -j4) |
| 内存 | 16.0 GB DDR4(实际可用 15.8 GB,编译 S3 不超过 6G,无压力) |
| SSD | 477 GB NVMe --- SAMSUNG MZVLW512HMJP-000H1(注意:构建目录放在 SSD 上速度差 5~10 倍) |
| 显卡 | Intel® UHD Graphics(不参与编译) |
| 系统类型 | 64 位 OS,x64 处理器 |
| WSL 发行版存放位置 | C:\WSL\Ubuntu\ext4.vhdx(VHDX 动态盘,首次建议 100GB 上限;完整构建链占用 20~35 GB) |
| WSL 版本 | WSL 2(内核版本 5.15 / 6.x;wsl -l -v 检查) |
| Ubuntu 版本 | 22.04 LTS(Jammy Jellyfish);rootfs 来源:ubuntu-22.04-rootfs.tar.gz |
| ESP-IDF 版本 | v5.5.1 (MicroPython 1.28 要求 idf>=5.3.0,README 推荐 v5.5) |
| MicroPython 版本 | v1.28.0 (注意 tag 是 v1.28.0,不是 v1.28) |
| MPY 交叉编译器 | mpy-cross v1.28(从 MicroPython 源码编译,不是 apt 装) |
| ESP-IDF Python venv | idf5.5_py3.10_env(ESP-IDF install.sh 自动创建) |
| GCC 工具链(Xtensa) | xtensa-esp-elf-gcc 14.2.0(供 ESP32 / ESP32-S2 / ESP32-S3 使用) |
| GCC 工具链(RISC-V) | riscv32-esp-elf-gcc 14.2.0(供 ESP32-C3 / C6 / C2 / H2 / P4 使用) |
| 烧录工具 | esptool.py v4.12+(ESP-IDF install.sh 自带) |
| 构建系统 | CMake 3.30 + Ninja 1.12(ESP-IDF 自带) |
1. WSL2 Ubuntu 22.04 LTS 安装经验
1.1 启用 Windows 必需功能(管理员 PowerShell)
powershell
# 1) 开启 WSL 功能 + 虚拟机平台 + (可选)Hyper-V
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
# 重启
Restart-Computer -Force
# 2) 重启后,把 WSL 默认版本设为 v2
wsl --set-default-version 2
# 3) 下载并安装 WSL2 Linux 内核更新包(x64):
# https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi
# 双击安装后再运行下面
wsl --status
如果 wsl --status 显示 "Default Version: 2",说明环境 OK。
1.2 从 rootfs tar.gz 导入 Ubuntu 22.04(推荐,无需 Microsoft Store)
powershell
# ① 先准备 rootfs 压缩包,本项目已保存:
# TEMP 绝对路径(见附录 A)
# $env:TEMP\wslbuild\ubuntu-22.04-rootfs.tar.gz
# 实际本项目完整路径:
# C:\Users\admin\AppData\Roaming\TRAE SOLO CN\ModularData\ai-agent\work-mode-projects\6a7c86d9d1984d74c066442b\TEMP\wslbuild\ubuntu-22.04-rootfs.tar.gz
# ② 新建 WSL 虚拟磁盘目录(不要在 C:\Users 里,避免 VHDX 与用户文件夹重叠)
New-Item -ItemType Directory -Force -Path C:\WSL\Ubuntu
# ③ 导入发行版(名称 = Ubuntu;安装路径 = C:\WSL\Ubuntu;rootfs 压缩包就是上面的 tar.gz)
$rootfs = "C:\Users\admin\AppData\Roaming\TRAE SOLO CN\ModularData\ai-agent\work-mode-projects\6a7c86d9d1984d74c066442b\TEMP\wslbuild\ubuntu-22.04-rootfs.tar.gz"
wsl --import Ubuntu C:\WSL\Ubuntu $rootfs --version 2
# ④ 验证是否成功:
wsl -l -v
# NAME STATE VERSION
# Ubuntu Stopped 2
1.3 首次进入系统 + 创建默认用户(避免一直用 root)
powershell
wsl -d Ubuntu
进去以后会是 root 提示符(#)。执行以下:
bash
# Ubuntu 里的操作:
apt-get update -y
apt-get install -y sudo passwd
# 创建用户 admin,密码自行设定
useradd -m -s /bin/bash admin
passwd admin
usermod -aG sudo admin
# 把 admin 改成默认登录用户
echo -e "[user]\ndefault=admin\n[boot]\nsystemd=false\n[automount]\noptions = metadata,umask=0022,fmask=0011\n[wsl2]\nmemory=14GB\nprocessors=4\nswap=4GB\nlocalhostForwarding=true" >/etc/wsl.conf
# 退出到 PowerShell
exit
在 PowerShell 里执行一次 wsl --shutdown 让 /etc/wsl.conf 生效:
powershell
wsl --shutdown
# 再重新进,就应该是 admin@DESKTOP-ETJIDDN:~$
wsl -d Ubuntu
whoami # 预期输出:admin
1.4 apt 源换国内源(可选,但对 Celeron N5105 的编译前依赖安装提升很大)
bash
sudo cp /etc/apt/sources.list /etc/apt/sources.list.bak
# 清华源(适用于 jammy = 22.04)
sudo tee /etc/apt/sources.list >/dev/null <<'EOF'
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-updates main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-backports main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-security main restricted universe multiverse
EOF
sudo apt-get update
2. ESP-IDF v5.5.1 安装经验
关键原则 :所有编译器 / 链接器 / 烧录器 / CMake / Ninja / Python venv 全部由 ESP-IDF 的 install.sh 安装,避免 Ubuntu apt 里包版本过旧或与 ESP-IDF 要求不匹配。
2.1 先装 Ubuntu 系统层依赖(这部分用 apt,后面工具链由 install.sh 接管)
bash
sudo apt-get install -y git wget curl flex bison gperf \
build-essential cmake ninja-build ccache libffi-dev \
libssl-dev dfu-util libusb-1.0-0 pkg-config \
python3 python3-venv python3-dev python3-pip python-is-python3
# 检查 python3 版本:必须 >=3.10(Jammy 默认 3.10,符合要求)
python3 -V
# 输出应是 Python 3.10.x
2.2 克隆 ESP-IDF 仓库(GitHub / Gitee 双路径,GitHub 不通时走 Gitee)
bash
# (A)首选 GitHub(代码更新快)
cd ~
git clone --depth 1 --branch v5.5.1 https://github.com/espressif/esp-idf.git
# (B)如果 GitHub 超时 → 改用 Gitee 镜像
# git clone --depth 1 --branch v5.5.1 https://gitee.com/EspressifSystems/esp-idf.git
cd esp-idf
# ★ 必须把子模块也 checkout 到对应 v5.5.1 版本(关键!否则会缺 heap/tlsf / nimble 等源码)
git fetch --unshallow 2>/dev/null || true
git checkout v5.5.1
# 显式初始化所有子模块(后面如果某子模块 GitHub 下载失败,见 §6 镜像)
export IDF_GITHUB_ASSETS="dl.espressif.com/dl" # 下载工具链走 Espressif CDN,不是 GitHub Release
git submodule update --init --recursive --depth 1
如果中途子模块 clone 失败 ,在 §6 中会给出全局 git config --global url."https://gh-proxy.com/https://github.com/".insteadOf "https://github.com/" 重写规则。
2.3 install.sh --- 安装 ESP-IDF 自带的工具链(编译器/烧录器/venv 都在这一步!)
bash
cd ~/esp-idf
# 为后续可能要的所有芯片一次性装齐工具链(ESP32 / S3 / C3 / C6 / P4,避免后面 make 时再现装)
bash ./install.sh all 2>&1 | tee ~/esp-idf-install.log
正常 install 完成后会打印:
All done! You can now run: . ./export.sh
工具链和 venv 实际存放在:
| 目录 | 内容 |
|---|---|
~/.espressif/tools/xtensa-esp-elf/14.2.0-xxx/** |
xtensa ESP32/S2/S3 GCC 14.2.0 |
~/.espressif/tools/riscv32-esp-elf/14.2.0-xxx/** |
RISC-V C3/C6/P4 GCC 14.2.0 |
~/.espressif/tools/esptool_py/*/** |
esptool.py + flasher stub |
~/.espressif/tools/cmake/*/bin/ |
CMake 3.30(不要用 apt 里的 3.22) |
~/.espressif/tools/ninja/*/bin/ |
Ninja 1.12 |
~/.espressif/python_env/idf5.5_py3.10_env/ |
ESP-IDF 专属 Python venv(click / cryptography / kconfiglib / idf_component_manager) |
2.4 export.sh 正确用法 + 固化到 ~/.bashrc
⚠️ 常见坑:不要用 source export.sh | tail --- 管道会创建子 shell,执行完后环境变量全丢!
正确写法:
bash
# 加载环境(推荐每天新开终端的第一句)
export IDF_SKIP_CHECK_SUBMODULES=1 # 这条后面会讲到为什么必须有
source ~/esp-idf/export.sh >/tmp/idf-export.log 2>&1
# 然后验证:
which idf.py
which xtensa-esp-elf-gcc
idf.py --version # 输出应是 ESP-IDF v5.5.1
xtensa-esp-elf-gcc -dumpversion # 输出 14.2.0
想要每次 wsl -d Ubuntu 进来自动加载,可以把下面加到 ~/.bashrc:
bash
# ~/.bashrc 末尾追加
if [ -f "$HOME/esp-idf/export.sh" ]; then
export IDF_SKIP_CHECK_SUBMODULES=1
# 只在交互式 shell 加载,避免脚本里套娃
case $- in *i*) source "$HOME/esp-idf/export.sh" >/tmp/idf-export.log 2>&1 ;; esac
fi
2.5 为什么必须 IDF_SKIP_CHECK_SUBMODULES=1
ESP-IDF v5.5.1 在 project() CMake 阶段会对 components/bt/nimble、components/openthread、components/esp-wifi-* 等子模块执行 "is submodule clean?" 强校验。在本机 GitHub 网络不稳定、我们为了绕开 NimBLE 新特性(CTE/Mesh/新 Service)又写了少量 placeholder stub(§7.1)的情况下,这条环境变量能让 CMake 直接跳过子模块强校验,避免无休止的 GitHub 拉取请求。
3. MicroPython v1.28.0 准备
3.1 克隆 MicroPython + 检出正确 tag(注意 tag 名必须是 v1.28.0!)
bash
cd ~
# (A) 官方 GitHub
git clone --depth 1 --branch v1.28.0 https://github.com/micropython/micropython.git
# (B) GitHub 不通就用 Gitee 镜像:
# git clone --depth 1 https://gitee.com/mirrors/micropython.git
# cd micropython ; git fetch --tags ; git checkout v1.28.0
cd micropython
3.2 编译 mpy-cross(MicroPython 字节码交叉编译器)
bash
cd ~/micropython
# submodules(必须,因为 mpy-cross 依赖 py/、lib/axtls/ 等)
git submodule update --init lib/mbedtls lib/berkeley-db-1.xx lib/lwip lib/micropython-lib 2>&1 | tail
# 如果上面失败(GitHub 子模块超时),见 §7.3 的 placeholder 方案
make -C mpy-cross -j$(nproc)
# 做一个软链接(ports/esp32/Makefile 默认找 mpy-cross/mpy-cross)
ln -sf build/mpy-cross mpy-cross/mpy-cross
# 验证版本
./mpy-cross/mpy-cross --version
# 输出应是:MicroPython v1.28.0; mpy-cross emitting mpy v6.3
3.3 初始化 ESP32 端口的子模块(make submodules)
bash
source ~/esp-idf/export.sh >/tmp/idf-export.log 2>&1
cd ~/micropython/ports/esp32
make BOARD=ESP32_GENERIC submodules
如果中途缺 lib/berkeley-db-1.xx/、lib/micropython-lib/ 这种 README 都不必需的,又死活 clone 不下来,用下面两行绕过(MicroPython 代码只在 CMake 里检查 is_directory,不会真正读其中内容,只要我们禁用了 MICROPY_PY_BTREE=OFF):
bash
mkdir -p ~/micropython/lib/berkeley-db-1.xx ~/micropython/lib/micropython-lib
echo "# placeholder (BTREE disabled, see MICROPY_PY_BTREE=OFF)" > ~/micropython/lib/berkeley-db-1.xx/README.md
echo "# placeholder (frozen manifests use explicit in-tree modules only)" > ~/micropython/lib/micropython-lib/README.md
4. 实战编译测试:ESP32-S3-SPIRAM_OCT(S3N16R8 型号)+ 自己的 py 文件
4.1 背景:ESP32_GENERIC_S3 的 board variant 机制
MicroPython ports/esp32/boards/ESP32_GENERIC_S3/ 下存在两个子目录:
ESP32_GENERIC_S3/
├── sdkconfig.board # 默认 S3 基础配置
├── manifest.py # 默认冻结脚本列表
├── modules/ # 默认放 frozen .py 文件的目录
└── variants/
├── SPIRAM_OCT/
│ └── sdkconfig.board # ★ 叠加配置:启用 8MB Octal SPI PSRAM + 16MB Flash
├── SPIRAM/
└── ...
BOARD_VARIANT=SPIRAM_OCT 会让编译系统在基础 sdkconfig 之上叠加 variants/SPIRAM_OCT/sdkconfig.board:
# variants/SPIRAM_OCT/sdkconfig.board 典型内容:
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_OCT=y
CONFIG_SPIRAM_SPEED_80M=y
CONFIG_MB_PSRAM_OPSRAM_OCTAL=y
这正好对应 ESP32-S3-WROOM-1-N16R8V / S3N16R8 芯片的硬件连接(Octal 8线 PSRAM)。
4.2 把自己的项目 py 文件放进固件(冻结为 frozen bytecode)
推荐方式 A(最直观) :把 py 文件直接放到 boards/ESP32_GENERIC_S3/modules/ 下 --- 构建系统会自动把它们作为 frozen module 编进固件,在 REPL 里直接 import 你的模块名。
bash
cd ~/micropython/ports/esp32/boards/ESP32_GENERIC_S3/modules/
# 示例:把自己写的项目文件(例如 modbus_server.py、ups_monitor.py、di_input.py)放进来
# 注意文件名必须符合 Python 标识符(字母、数字、下划线),不能有破折号。
ls -la
# 应有:
# - boot.py (默认已有,不用管)
# - modbus_server.py
# - ups_monitor.py
# - di_input.py
方式 B(显式写 manifest.py 控制依赖,推荐给大项目) :编辑 boards/ESP32_GENERIC_S3/manifest.py:
python
# manifest.py (MicroPython 原生的 "freeze manifest" 格式)
freeze("modules/", "modbus_server.py")
freeze("modules/", "ups_monitor.py")
freeze("modules/", "di_input.py")
freeze("$(MPY_DIR)/lib/micropython-lib/python-stdlib/functools/") # 需要真正初始化 micropython-lib 才能用
4.3 执行编译
bash
cd ~/micropython/ports/esp32
export IDF_SKIP_CHECK_SUBMODULES=1
source ~/esp-idf/export.sh >/tmp/idf-export.log 2>&1
# ★ BOARD + BOARD_VARIANT 组合出 S3N16R8
make BOARD=ESP32_GENERIC_S3 BOARD_VARIANT=SPIRAM_OCT USER_C_MODULES= -j$(nproc) 2>&1 | tee build-S3-SPIRAM_OCT.log
💡 关于
-j$(nproc):N5105 是 4C4T,-j4 即可。如果你有更大的 CPU(i7-12700 = 20T),推荐 -j16 左右最经济。💡 完整首次编译(全部 100+ IDF 组件)在 N5105 上约 25~35 分钟,后续增量(只改 py)一般 1~3 分钟。
4.4 产物 & 按用户要求重命名为 ESP32_GENERIC_S3-SPIRAM_OCT-YYYYMMDD-v1.28.0.bin
bash
cd ~/micropython/ports/esp32/build-ESP32_GENERIC_S3/
ls -lh *.bin *.uf2
# 典型输出:
# bootloader.bin 24 KB @ 0x1000
# partition-table.bin 3 KB @ 0x8000
# firmware.bin 2.1 MB @ 0x10000 ★ 主固件
# micropython.uf2 2.1 MB (USB MSC 拖入升级用)
# flash_args (esptool 完整参数)
# ★ 按您的命名约定重命名 firmware.bin:
TODAY=$(date +%Y%m%d)
cp firmware.bin "ESP32_GENERIC_S3-SPIRAM_OCT-${TODAY}-v1.28.0.bin"
# 最终得到:ESP32_GENERIC_S3-SPIRAM_OCT-20260406-v1.28.0.bin
ls -lh ESP32_GENERIC_S3-SPIRAM_OCT-*.bin
文件大小说明:v1.28.0 默认启用 BLE + WiFi + SSL + LittleFS,firmware.bin ≈ 2.0~2.3 MB;如果用 S3N16R8 16MB Flash,剩余空间还非常充足(16MB - 0x10000 = 16,711,168 bytes ≈ 15.9 MB 可用)。
5. 失败分析:最常见组件下载网络不通 + 可用备用网址(抄下来备用)
5.1 全局镜像重写(一招解决 90% 的 GitHub 子模块拉取失败)
bash
# 方法:使用 GitHub 加速代理 gh-proxy.com(无需注册,免费)
git config --global url."https://gh-proxy.com/https://github.com/".insteadOf "https://github.com/"
git config --global url."https://gh-proxy.com/https://gist.github.com/".insteadOf "https://gist.github.com/"
# 验证:git config -l | grep insteadOf
# 想还原(切换回原生 GitHub):
# git config --global --unset url."https://gh-proxy.com/https://github.com/".insteadOf
5.2 GitHub 直接完全不通 --- Gitee 仓库镜像地址
| 项目 | Gitee 镜像地址 | 说明 |
|---|---|---|
| ESP-IDF 源码 | https://gitee.com/EspressifSystems/esp-idf.git |
Espressif 官方维护的 Gitee 镜像,tag 齐全(v5.5.1 可用) |
| ESP-IDF 工具链子模块 | https://gitee.com/EspressifSystems/esp-github-shims.git |
不常用;一般用上面第一条 gh-proxy.com 规则更方便 |
| MicroPython 源码 | https://gitee.com/mirrors/micropython.git |
第三方镜像,一般 tag 延后 1~3 天;v1.28.0 已经同步 |
| esp-nimble / openthread / mqtt-c 子模块 | 用 gh-proxy 重写即可 | 这些子模块是独立仓库,没有国内专属镜像 |
5.3 ESP-IDF install.sh 拉取工具链加速(xtensa / riscv gcc、openocd 等)
bash
export IDF_GITHUB_ASSETS="https://dl.espressif.com/dl"
bash ~/esp-idf/install.sh all
这会让 idf_tools.py 从 Espressif 官方 CDN 下载,而不是 GitHub Release Assets,速度通常 2~10 MB/s。
5.4 pip 国内加速源(ESP-IDF venv 内 Python 依赖也会受益)
在 ~/.pip/pip.conf 写入:
ini
[global]
index-url = https://pypi.tuna.tsinghua.edu.cn/simple
extra-index-url = https://mirrors.aliyun.com/pypi/simple
trusted-host = pypi.tuna.tsinghua.edu.cn mirrors.aliyun.com
5.5 可用备用网址速查表(离线环境也能手动下载)
| 资源 | 官方下载地址 | gh-proxy 加速地址 |
|---|---|---|
| Ubuntu 22.04 rootfs(amd64 通用) | https://cloud-images.ubuntu.com/wsl/jammy/current/ubuntu-jammy-wsl-amd64-wsl.rootfs.tar.gz | https://gh-proxy.com/ + 上面 URL |
| ESP-IDF v5.5.1 tag tarball | https://github.com/espressif/esp-idf/archive/refs/tags/v5.5.1.tar.gz | https://gh-proxy.com/ + 上面 URL |
| MicroPython v1.28.0 tag tarball | https://github.com/micropython/micropython/archive/refs/tags/v1.28.0.tar.gz | https://gh-proxy.com/ + 上面 URL |
| xtensa-esp-elf-gcc 14.2.0-win64 | https://github.com/espressif/crosstool-NG/releases/download/esp-14.2.0_20241226/xtensa-esp-elf-gcc14.2.0_esp_14.2.0_20241226-win64.zip | https://dl.espressif.com/dl/ 目录下同名 zip 更稳 |
| riscv32-esp-elf-gcc 14.2.0-win64 | https://github.com/espressif/crosstool-NG/releases/... | 同上 dl.espressif.com |
| esptool.py 4.12 standalone(Windows) | pip install esptool==4.12 或 https://github.com/espressif/esptool/releases |
清华 pip 源 |
6. 走过的弯路 & 易错点 Top 16(已加入 ECM50B 裁剪 + S3 目标切换实战)
| # | 弯路 | 正确做法 | 后果 |
|---|---|---|---|
| 1 | ESP-IDF 用 v5.2.2 配 MicroPython 1.28 --- MPY idf_component.yml 明确写了 idf: ">=5.3.0" |
直接上 v5.5.1(README 推荐) | idf_component_manager project depends on idf (>=5.3.0) 直接拒绝配置 |
| 2 | MicroPython 想 git checkout v1.28 --- tag 不存在!实际 tag 是 v1.28.0 |
`git tag -l | grep 1.28` 确认后再 checkout |
| 3 | `source ~/esp-idf/export.sh | tail` --- 管道创建子 shell,环境变量全丢 | 要么 source ... >/tmp/log 2>&1 把日志重定向到文件,要么直接 source ... 不做任何 shell 重定向;脚本里更稳的是 eval $(python $IDF_PATH/tools/idf_tools.py export) |
| 4 | micropython/mpy-cross/mpy-cross 找不到实际文件 --- 编译产物默认在 mpy-cross/build/mpy-cross |
ln -sf build/mpy-cross mpy-cross/mpy-cross 软链接 |
Makefile 报错 "missing mpy-cross path" |
| 5 | components/heap/tlsf/include 不存在 --- ESP-IDF 5.5.1 把子模块从 inline 拆到了独立 heap/tlsf |
显式 cd ~/esp-idf && git submodule update --init components/heap/tlsf |
CMake error: "Include directory '.../tlsf/include' is not a directory" |
| 6 | nimble services/cte/src/ble_svc_cte.c / transport/esp_ipc_legacy/src/hci_esp_ipc_legacy.c 不存在 --- 老 commit 没包含这些新版源 |
更推荐直接关蓝牙 (工业 PLC/Modbus 不需要):板级 mpconfigboard.h #define MICROPY_PY_BLUETOOTH (0) + mpconfigboard.cmake 传 MICROPY_PY_BLUETOOTH=OFF,见 §7.1 |
CMake Error: Cannot find source file .../ble_svc_cte.c 或 modbluetooth_nimble.c: fatal error: host/ble_hs.h: No such file or directory |
| 7 | MICROPY_PY_BTREE 明明 sdkconfig 是关的,但 CMake 还检查 berkeley-db submodule --- 因为 esp32_common.cmake 强制要求存在 |
双重保险 :① mpconfigboard.cmake 设 MICROPY_PY_BTREE=OFF ② mpconfigboard.h 加 #define MICROPY_PY_BTREE (0)(见 §7.2);子模块空目录加 placeholder README 绕 CMake is_directory 校验 |
"berkeley-db-1.xx not initialised" 或 berkeley-db/btree.h: No such file or directory |
| 8 | CMake 配置阶段强制子模块网络检查(WiFi lib、BT lib) | 导出 IDF_SKIP_CHECK_SUBMODULES=1 跳过 |
GitHub 超时 600s 后直接失败 |
| 9 | nimble stub 基准路径拼错($NIMROOT/../$rel),122 个文件写在错误目录下,CMake 仍然说找不到 |
推荐关蓝牙(§7.1) ;如果必须开,基准路径必须是 $NIMBASE=~/esp-idf/components/bt,然后 full="$NIMBASE/$rel" |
"No SOURCES given to target: __idf_bt" |
| 10 | Win10 WSL 默认没启用 systemd,且 apt 源仍是 archive.ubuntu.com(境外,下载 50KB/s) | ① /etc/wsl.conf 明确写 [boot] systemd=false(不要强行开)② apt 换清华源镜像 |
apt 下载依赖耗 30 分钟以上,且有概率超时 TCP Reset |
| 11 | (新增 ECM50B) 覆写 mpconfigboard.h 关蓝牙时丢了板名定义 ,导致 modos.c:108: error: 'MICROPY_HW_BOARD_NAME' undeclared |
mpconfigboard.h 必须包含 MICROPY_HW_BOARD_NAME + MICROPY_HW_MCU_NAME 两个宏(os.uname() 用到);模板见 §8.1 |
modos.c 编译立即中断、错误提示容易误判为 C 标准库问题 |
| 12 | (新增 ECM50B) manifest.py 写了 require("bundle-networking") 但 micropython-lib 子模块是空目录/未初始化 |
删除所有 require() 调用;把 24 个业务 .py 直接列 freeze("modules/","xxx.py");如真需要 micropython-lib,必须完整 clone 子模块并 make -C lib/micropython-lib install 走 manifestfile.py 打包 |
Package 'bundle-networking' not found 或 "manifest.py failed: package not in micropython-lib" |
| 13 | (新增 ECM50B) PowerShell 里直接写 `wsl -d Ubuntu -u admin -- bash -c "cmd1 && cmd2 | tee log",&& |` 被 PS 解析成自己的运算符 |
两招:① 先把整套命令写入 /home/admin/build_scripts/xxx.sh,chmod +x 后 wsl -d Ubuntu -u admin /home/admin/build_scripts/xxx.sh ② 或用 Stop-Parsing --% 告诉 PS 停止解析后面参数 |
| 14 | (新增 ECM50B) 改了 sdkconfig.board / mpconfigboard.cmake 后只 make 不删 build 目录 --- CMake 缓存旧配置 |
任何板级 Kconfig/CMake 变量改动必须先 rm -rf build-ECM50B build-ECM50B.log 再 make;否则新值不会覆盖 CMakeCache.txt |
sdkconfig 改了 CONFIG_BT_ENABLED=n 但 build 里仍然 CONFIG_BT_ENABLED=y,链接阶段继续报 nimble 符号错 |
| 15 | (新增 ECM50B) 给 lwip_patch.c 追加 stubs 用 head -n X 截断后再追加,切掉了前一个函数的闭合 } |
① 每次追加前后跑 "brace balance" 检查(Python 扫描 or awk '{d+=$0=="{"?-($0=="}"):0} END{print d}') ② 更推荐用 sed 或 Python 按"模式匹配插入"而不是按行数截断 |
C 语法错误 error: expected declaration or statement at end of input(通常只告诉你文件尾,需要一路回溯到哪个函数缺 }) |
| 16 | (新增 S3 切换) 按经典 ESP32 编译的固件烧到 ESP32-S3 芯片上 → esptool 报 Unexpected chip id in image. Expected 9 but value was 0;或 --chip esp32 报 This chip is ESP32-S3 not ESP32 |
板级 mpconfigboard.cmake 必须 set(IDF_TARGET esp32s3)(否则默认 esp32);同时 mpconfigboard.h 的 MICROPY_HW_MCU_NAME 改为 "ESP32S3";sdkconfig.board 加 16MB Flash + QIO + 80M 配置。详见 §10 |
烧录直接被 esptool 拒绝(chip id 校验不过);即使 --force 强刷也会变砖(LX6 vs LX7 指令集不兼容) |
7. 实战裁剪 · MicroPython 源码中蓝牙/NimBLE、BTree、WiFi AP、LWIP PPP/ACD 的真正关闭方法
ECM50B(工业 Modbus TCP 主机 + ESP-NOW 网关)编译时反复卡在:
- 蓝牙组件 122 个 nimble 源文件 / 头文件缺失
- BTree 模块的 berkeley-db-1.xx 是 225 字节占位符(tarball 没带子模块)
- WiFi AP/STA 内部符号
esp_wifi_*_internalwrapper 与 esp32-wifi-lib 二进制版本错位- LWIP PPP:sdkconfig.board 写了
CONFIG_LWIP_PPP_SUPPORT=n,实际生成的 sdkconfig.h 还是=1本节用"ECM50B 最终通过"的裁剪方案,给出每一项 从编译错误 → 真正生效 的最短路径。
7.1 关闭蓝牙 / NimBLE:sdkconfig CONFIG_BT_ENABLED=n 不够,必须在 mpconfigboard.h 宏级关
7.1.1 为什么 sdkconfig 关蓝牙会"看似生效但编译依旧失败"?
ports/esp32/mpconfigport.h 包含顺序:
1. #include "mpconfigboard.h" ← 先包含板级(我们的文件)
2. 一大堆默认值,其中第 N 行:
#ifndef MICROPY_PY_BLUETOOTH
#define MICROPY_PY_BLUETOOTH (1) ← ★ 默认开启!
#endif
MICROPY_PY_BLUETOOTH 是 MicroPython 的宏 ,和 sdkconfig 里的 CONFIG_BT_ENABLED(ESP-IDF 层)不是同一个东西 。只关 CONFIG_BT_ENABLED=n:
- ESP-IDF 的
components/bt可能不编译进组件, - 但 MicroPython 的
extmod/modbluetooth_nimble.c/ports/esp32/mpnimbleport.c仍然#if MICROPY_PY_BLUETOOTH为真(1)→#include <host/ble_hs.h>→ 头文件路径没加 →fatal error: host/ble_hs.h: No such file or directory。
7.1.2 ECM50B 最终双保险写法(板级文件)
① ports/esp32/boards/ECM50B/mpconfigboard.h (宏级 --- 让 #if MICROPY_PY_BLUETOOTH 为 0,直接整个 TU 变空):
c
// mpconfigboard.h(被 mpconfigport.h 第一个 #include)
#ifndef MICROPY_HW_BOARD_NAME
#define MICROPY_HW_BOARD_NAME "ECM50B Host-B"
#endif
#ifndef MICROPY_HW_MCU_NAME
#define MICROPY_HW_MCU_NAME "ESP32"
#endif
// ★ BLUETOOTH 0:覆盖 mpconfigport.h 默认值 (1)
#ifndef MICROPY_PY_BLUETOOTH
#define MICROPY_PY_BLUETOOTH (0)
#endif
#ifndef MICROPY_BLUETOOTH_NIMBLE
#define MICROPY_BLUETOOTH_NIMBLE (0)
#endif
#ifndef MICROPY_BLUETOOTH_NIMBLE_BINDINGS_ONLY
#define MICROPY_BLUETOOTH_NIMBLE_BINDINGS_ONLY (0)
#endif
#ifndef MICROPY_PY_BLUETOOTH_ENABLE_CENTRAL_MODE
#define MICROPY_PY_BLUETOOTH_ENABLE_CENTRAL_MODE (0)
#endif
#ifndef MICROPY_PY_BLUETOOTH_ENABLE_PAIRING_BONDING
#define MICROPY_PY_BLUETOOTH_ENABLE_PAIRING_BONDING (0)
#endif
// 注:MICROPY_PY_BLUETOOTH_RANDOM_ADDR 不要在这里覆盖
// 因为 mpconfigport.h:209 定义时没有 #ifndef 保护
// 会造成 -Wmacro-redefined 警告刷屏(BLUETOOTH=0 时这个宏无用)
// ★ USB(ECM50B 不接 USB OTG)
#ifndef MICROPY_HW_ENABLE_USB
#define MICROPY_HW_ENABLE_USB (0)
#endif
#ifndef MICROPY_HW_USB_CDC
#define MICROPY_HW_USB_CDC (0)
#endif
② ports/esp32/boards/ECM50B/mpconfigboard.cmake(CMake 级 --- 让 CMake 构建阶段也少注册依赖):
cmake
set(MICROPY_PY_BLUETOOTH OFF CACHE STRING "")
set(MICROPY_PY_BLUETOOTH_NIMBLE OFF CACHE STRING "")
set(MICROPY_HW_ENABLE_USBDEVICE OFF CACHE STRING "")
set(MICROPY_PY_BTREE OFF CACHE STRING "") # 见 §7.2
set(MICROPY_PY_TINYUSB OFF CACHE STRING "")
③ ports/esp32/boards/ECM50B/sdkconfig.board(ESP-IDF Kconfig 级 --- 彻底少编 bt 组件):
ini
# 关蓝牙控制器 / NimBLE / Bluedroid
CONFIG_BT_ENABLED=n
CONFIG_BT_NIMBLE_ENABLED=n
CONFIG_BT_CONTROLLER_ENABLED=n
CONFIG_BT_BLUEDROID_ENABLED=n
# 关 WiFi SoftAP(网关设备不做 AP,同时避免链接阶段 WiFi AP 符号)
CONFIG_ESP_WIFI_SOFTAP_SUPPORT=n
# 关 PPP(不接蜂窝 modem / 串口拨号)
CONFIG_LWIP_PPP_SUPPORT=n
CONFIG_LWIP_PPPAPI=n
CONFIG_LWIP_PPP_INPROC=n
CONFIG_LWIP_PPPOS=n
CONFIG_LWIP_PPPOE=n
✅ 效果验证 (见 ECM50B 构建日志 1278/1304):mpnimbleport.c.obj 编译通过(条件块空 TU,不再找 nimble 头)。
7.2 关闭 BTree(Berkeley DB):tarball 里的 lib/berkeley-db-1.xx/ 是空壳(225 B placeholder)
7.2.1 症状链
Stage 1: make submodules → berkeley-db-1.xx git submodule 404 / 超时
Stage 2: 手动创建空目录 + placeholder README.md → is_directory OK
Stage 3: 编译 modbtree.c
#include <berkeley-db/db.h> ← 只有一个 225B stub db.h
#include <berkeley-db/btree.h> ← No such file or directory!
原因:MicroPython 1.28 release tarball 没把 berkeley-db 子模块打包进去(只塞了最小占位符头)。工业网关根本用不到 BTree(BTree 是个键值数据库,我们用 JSON/LittleFS 存规则就行),直接关是唯一正确解。
7.2.2 三重关闭(ECM50B 验证过有效)
① CMake 级 (上面的 mpconfigboard.cmake 已写):set(MICROPY_PY_BTREE OFF ...)
② 宏级(mpconfigboard.h 追加):
c
// 防 py/mpconfig.h 默认 MICROPY_PY_BTREE=1 覆盖
#ifndef MICROPY_PY_BTREE
#define MICROPY_PY_BTREE (0)
#endif
③ 子模块目录占位(第 3.3 节已写,避免 CMake is_directory 检查失败):
bash
mkdir -p ~/micropython/lib/berkeley-db-1.xx/include
echo "/* placeholder --- MICROPY_PY_BTREE=0 */" > ~/micropython/lib/berkeley-db-1.xx/include/db.h
✅ 验证:构建日志不再出现 modbtree.c 编译步骤(qstr 也不会收集它的类型签名)。
7.3 WiFi AP / STA 内部符号 esp_wifi_*_internal 链接 undefined:wrapper source ↔ binary lib 版本错位
7.3.1 症状
esp-idf/esp_wifi/libesp_wifi.a(...) in function `esp_wifi_connect_internal`:
undefined reference to `esp_wifi_ap_get_transition_disable_internal`
undefined reference to `esp_wifi_sta_get_reset_nvs_pmk_internal`
undefined reference to `esp_wifi_sta_set_reset_nvs_pmk_internal`
undefined reference to `esp_wifi_ap_get_sae_ext_config_internal`
undefined reference to `esp_wifi_ap_get_gtk_rekeying_config_internal`
undefined reference to `esp_wifi_ap_set_group_mgmt_cipher_internal`
ESP-IDF 层 components/esp_wifi/ 下分两部分:
| 部分 | 位置 | 内容 | 版本来源 |
|---|---|---|---|
| wrapper 源码 | components/esp_wifi/*.c |
esp_wifi_connect_internal 等 API,调用 _internal 后缀函数 |
esp-idf git tag v5.5.1 |
| 闭源二进制 lib | components/esp_wifi/lib_esp32/*.a |
真正实现 esp_wifi_ap_get_transition_disable_internal 等符号 |
子模块 esp-wifi-libs 的特定 commit |
错位原因 :esp-idf 的 wrapper 代码是 v5.5.1,但子模块 lib_esp32 仍是旧 commit(少了 6 个新函数)→ 链接找不到实现。
7.3.2 解法 A(规范做法 --- 同步子模块)
bash
cd ~/esp-idf
export IDF_SKIP_CHECK_SUBMODULES=1
# 把 esp_wifi 子模块整体 checkout 到 v5.5.1 对应的 commit
# 注意:--init 会同时拉闭源 lib_esp32 的二进制 blobs
git submodule update --init --recursive components/esp_wifi
完成后再 rm -rf build-ECM50B && make ...。如果 GitHub 下载二进制 lib 超时(50+ MB),见 §7.3.3 的存根降级法。
7.3.3 解法 B(降级法 --- __weak 存根:适用"不使用 WiFi SoftAP 模式"的设备)
ECM50B 是主机:WiFi 只当 STA 连公司内网/现场路由器,从不启用 SoftAP 。因此这 6 个 AP/STA 管理辅助函数"调用了但无实际作用"。我们在 ports/esp32/lwip_patch.c(或新建 wifi_stubs.c)里放弱符号,链接时二进制 lib 没实现的话就走我们的存根:
c
/* ports/esp32/lwip_patch.c --- 放在文件末尾 */
#ifndef __weak
#define __weak __attribute__((weak))
#endif
#include "esp_err.h"
/* ------------------ WiFi internal symbol stubs ------------------
* ECM50B: CONFIG_ESP_WIFI_SOFTAP_SUPPORT=n, 但 wrapper 里仍引用符号。
* 返回保守默认值;一旦 esp-wifi-lib 子模块完全到位,
* 这里的 __weak 会被闭源 lib 中的强定义覆盖,完全不冲突。
* -----------------------------------------------------------------*/
__weak int esp_wifi_ap_get_transition_disable_internal(void) { return 0; }
__weak int esp_wifi_sta_get_reset_nvs_pmk_internal(void) { return 0; }
__weak esp_err_t esp_wifi_sta_set_reset_nvs_pmk_internal(int f) { (void)f; return ESP_OK; }
/* 下面三个仅在 AP 模式 hostap 初始化中使用,ECM50B 中永不触发 */
__weak int esp_wifi_ap_get_sae_ext_config_internal(void) { return 0; }
__weak int esp_wifi_ap_get_gtk_rekeying_config_internal(void) { return 0; }
__weak esp_err_t esp_wifi_ap_set_group_mgmt_cipher_internal(int c){(void)c; return ESP_OK; }
✅ ECM50B 构建日志:1299/1304 libmain.a 静态库链接成功(这 6 个符号用的 weak 存根)。
7.4 LWIP PPP:CONFIG_LWIP_PPP_SUPPORT=n 会被覆盖为 =1 + 链接暴露 6 个未定义引用
7.4.1 优先级陷阱(再一次)
sdkconfig.board 里写了:
ini
CONFIG_LWIP_PPP_SUPPORT=n
但 build-ECM50B/config/sdkconfig.h 里:
c
#define CONFIG_LWIP_PPP_SUPPORT 1 ← 被 base 配置覆盖!
原因:MicroPython ESP32 端口的 boards/sdkconfig.base / ports/esp32/sdkconfig.defaults 优先级比我们的 sdkconfig.board 更高,或者 Kconfig 的 default y if ... 条件分支命中。结果:
1) network_ppp.c / ppp_set_auth.c 编译:
函数指针类型 pppos_output_cb_fn 与 glue code 不匹配 → 编译错误
(解决方案:文件头加 #include "lwip/opt.h" 后包 #if PPP_SUPPORT ... #endif)
2) esp_netif_ppp.c(定义6个 PPP 助手的 ESP-IDF 源文件)被 CMake if(CONFIG_LWIP_PPP_SUPPORT) 排除 → 不编译
3) 链接阶段 esp_netif_lwip.c 无条件调用 6 个 PPP 函数 → 6 undefined refs!
完整 6 个符号(分 3 个 + 3 个两轮暴露):
| 轮次 | 符号 | 返回 | 说明 |
|---|---|---|---|
| 第一轮 | esp_netif_ppp_set_default_netif |
esp_err_t |
更新默认 netif 时无条件调用 |
esp_netif_start_ppp |
esp_err_t |
esp_netif_start_api 无条件调用 | |
esp_netif_stop_ppp |
esp_err_t |
esp_netif_stop_api 无条件调用 | |
| 第二轮 | esp_netif_new_ppp |
esp_netif_t * |
esp_netif_new_api → ppp 分支创建接口 |
esp_netif_destroy_ppp |
esp_err_t |
esp_netif_destroy_api → ppp 分支销毁接口 | |
esp_network_ppp_lwip_type |
const void *(全局指针变量) |
MicroPython modnetwork.c 的 netif driver 注册表;定义在 network_ppp.c(被我们 #if PPP_SUPPORT 排除了) |
7.4.2 正确做法:6 个符号全部 __attribute__((weak)) 存根(ECM50B 验证版)
放在同一个 lwip_patch.c 里(和上面的 WiFi weak stub 放一起):
c
#include "esp_netif.h"
/* ---------- Round-1 PPP: netif glue (start/stop/set_default) ---------- */
__attribute__((weak))
esp_err_t esp_netif_ppp_set_default_netif(esp_netif_t *n){ (void)n; return ESP_OK; }
__attribute__((weak))
esp_err_t esp_netif_start_ppp(esp_netif_t *n) { (void)n; return ESP_OK; }
__attribute__((weak))
esp_err_t esp_netif_stop_ppp (esp_netif_t *n) { (void)n; return ESP_OK; }
/* ---------- Round-2 PPP: netif create/destroy + MPY driver ptr ---------- */
struct esp_netif_config_t; /* forward decl (opaque) */
__attribute__((weak))
esp_netif_t *esp_netif_new_ppp(const struct esp_netif_config_t *cfg)
{ (void)cfg; return (esp_netif_t *)0; } /* NULL = 接口创建失败 */
__attribute__((weak))
esp_err_t esp_netif_destroy_ppp(esp_netif_t *n){ (void)n; return ESP_OK; }
/* 变量(非函数):网络协议栈注册指针,NULL 即 PPP 后端不存在 */
__attribute__((weak))
const void *esp_network_ppp_lwip_type = (const void *)0;
7.4.3 为什么不要用 #if !CONFIG_LWIP_PPP_SUPPORT 保护块?
因为 "配置实际值 ≠ 我们写在 sdkconfig.board 里的值"(见 7.4.1)。如果写了保护块:
- 当
CONFIG_LWIP_PPP_SUPPORT=1(实际生效)→ 我们的 stubs 被排除 - 同时
esp_netif_ppp.c又没编进来(CMake 条件或子模块缺失)→ 仍然 undefined ref!
✅ __weak 不加保护 = 两种情况都正确:
- 真实 esp_netif_ppp.c 编译 → 强定义覆盖 weak,运行真正实现
- 真实 esp_netif_ppp.c 不编译 → weak 提供无操作 fallback
7.5 LWIP DHCP/ACD 版本错位:CALC_TIMEOUT 宏重命名 + acd.h 头不存在
MicroPython 1.28 自带的 lib/lwip 是老版本,ESP-IDF 5.5.1 装的 components/lwip wrapper 写了新版宏名。需要在 boards/ECM50B/sdkconfig.board 追加(别手动改生成的 lwipopts.h,每次 CMake 会重写)------正确做法:写补丁脚本注入板级 lwipopts:
bash
# 保存为 patch_lwip_compat.sh,make 之前跑一次
OPTS=~/micropython/ports/esp32/build-ECM50B/config/lwipopts.h
# 追加兼容映射:CALC → SET(新版 IDF 改了宏名 + 语义)
cat >> "$OPTS" << 'EOF'
/* ===== ECM50B LWIP COMPAT MACROS (IDF 5.5.1 wrapper vs MPY 1.28 lwip) ===== */
# ifndef DHCP_CALC_TIMEOUT_FROM_OFFERED_T0_LEASE
# define DHCP_CALC_TIMEOUT_FROM_OFFERED_T0_LEASE(dhcp,t0) \
DHCP_SET_TIMEOUT_FROM_OFFERED_T0_LEASE((dhcp), (t0), 0)
# endif
# undef DHCP_DOES_ARP_CHECK
# define DHCP_DOES_ARP_CHECK 0
# undef LWIP_DHCP_DOES_ACD_CHECK
# define LWIP_DHCP_DOES_ACD_CHECK 0
EOF
# 如果 acd_dhcp_check.c 报 lwip/acd.h No such file:
ACDSTUB=~/esp-idf/components/lwip/lwip/src/include/lwip/acd.h
if [ ! -f "$ACDSTUB" ]; then
cat > "$ACDSTUB" << 'EOF'
/* placeholder --- ACD (Address Conflict Detection) disabled via lwipopts.h */
#ifndef LWIP_ACD_H
#define LWIP_ACD_H
typedef struct acd { int dummy; } acd_t;
#endif /* LWIP_ACD_H */
EOF
fi
8. 自定义板完整工作流 · ECM50B 实战(从源码 → frozen 模块 → 固件 → 烧录)
本节以 ECM50B(经典 ESP32,无 USB,W5500 Ethernet + WiFi STA + Modbus TCP Server + ESP-NOW 网关 + ST 规则引擎)作为完整范例。
8.1 板级目录结构:boards/ECM50B/ 必须有 6 个文件 + modules/ 目录
ports/esp32/boards/ECM50B/
├── mpconfigboard.cmake ← CMake 级关 BTREE/Bluetooth/USB
├── mpconfigboard.h ← 宏级关 Bluetooth/BTree + BOARD_NAME + MCU_NAME (模板见 §7.1.2)
├── sdkconfig.board ← Kconfig 级关 BT/SoftAP/PPP (模板见 §7.1.2)
├── mpconfigboard.cmake ← (同上)
├── board.json ← MicroPython 生成发布文件用(可以直接从 ESP32_GENERIC 复制然后改名)
├── manifest.py ← frozen 模块清单(删光 require,直接 freeze 业务 .py)
└── modules/
├── webrepl_cfg.py
├── config.py
├── _embedded_json.py ★ 嵌入式 JSON 回退(见 §8.2)
├── main.py ★ 入口(含双模式加载,见 §8.2)
├── espnow_common.py
├── espnow_manager.py (11 个 espnow 子模块...)
├── modbus/uModBusTCPSlave.py
├── modbus/uModBusConst.py
└── softplc/rules_engine_full.py
8.2 Frozen 模块与 exec()/sys.path 兼容:双模式加载(空 LittleFS 也能跑)
ECM50B 的业务代码有几个历史问题:
- 原来的 main.py 用
sys.path.insert(0, '/modbus')+from uModBusTCPSlave import ...(依赖 LittleFS 里有/modbus目录) - 规则引擎
exec(open('/softplc/rules_engine_full.py').read())(依赖文件系统里有.py源) rules_engine_full.py又json.load(open('/softplc/rules.json'))(依赖 LittleFS 里有 JSON)
全部 frozen 后上述路径 在空 LittleFS 的设备上 100% 失败。解决:
8.2.1 main.py 双模式加载(Frozen import 优先 → 失败回退 exec 文件系统)
python
# ========== main.py 关键片段(ECM50B 已生效) ==========
import sys, os
# --- Modbus TCP 服务器:Frozen import 优先 ---
try:
import uModBusTCPSlave
print("[Modbus] 已加载 (Frozen module)")
except Exception as e1:
try:
sys.path.insert(0, '/modbus')
exec(open('/modbus/uModBusTCPSlave.py').read())
print("[Modbus] 已加载 (LittleFS exec)")
except Exception as e2:
print("[Modbus] 加载失败: frozen=%s | fs=%s" % (e1,e2))
# --- 规则引擎:Frozen import → 文件系统 → 嵌入式 JSON 三级回退 ---
RULE_ENGINE_AVAILABLE = False
try:
from rules_engine_full import init_rule_engine, execute_rules
RULE_ENGINE_AVAILABLE = True
print("[规则引擎] 已加载 (Frozen module)")
except Exception as e_frozen:
try:
exec(open('/softplc/rules_engine_full.py').read())
RULE_ENGINE_AVAILABLE = True
print("[规则引擎] 已加载 (LittleFS)")
except Exception as e:
RULE_ENGINE_AVAILABLE = False
8.2.2 _embedded_json.py:将关键配置 JSON 作为字符串烤进固件
python
# modules/_embedded_json.py (frozen)
RULES_JSON = r'''{
"version": "1.0",
"compiled_at": "2026-07-29T16:46:42",
"source": "gw1500_linkage.st",
"code": [],
"metadata": {}
}'''
DEVICE_RULES_JSON = r'''{ ... }'''
GW1500_LINKAGE_JSON = r'''{ ... }'''
def get_rules_json():
import json
return json.loads(RULES_JSON)
def get_device_rules_json():
import json
return json.loads(DEVICE_RULES_JSON)
def get_gw1500_linkage_json():
import json
return json.loads(GW1500_LINKAGE_JSON)
然后在 rules_engine_full.py 的 load_rules 函数里做文件失败回退:
python
def load_rules(self, file_path):
try:
with open(file_path, 'r') as f:
self.rules = json.load(f)
except Exception:
# 文件系统中不存在 → 用 firmware baked-in 的嵌入式 JSON
try:
from _embedded_json import (get_rules_json,
get_device_rules_json,
get_gw1500_linkage_json)
if 'device' in file_path.lower():
data = get_device_rules_json()
else:
data = get_rules_json()
if not data.get('code'):
data = get_gw1500_linkage_json()
self.rules = data
except Exception:
return False
# 正常 precompile...
8.2.3 manifest.py 写法(删除所有 require,显式 freeze 24 个 ECM50B 文件)
python
# boards/ECM50B/manifest.py
metadata(version="1.28.0-ECM50B", board="ECM50B Host-B")
# ====== 删除所有 require("bundle-xxx") / require("mip") / require("aioble")
# ====== ECM50B 不依赖任何外部 micropython-lib 包
# ===== 配置 / 嵌入式数据 =====
freeze("modules/", "webrepl_cfg.py")
freeze("modules/", "config.py")
freeze("modules/", "_embedded_json.py")
# ===== ESP-NOW 协议栈(11个文件) =====
freeze("modules/", "espnow_common.py")
freeze("modules/", "espnow_manager.py")
# ... 其他 espnow_crc.py / espnow_frame.py / ...
# ===== Modbus TCP 服务器 =====
freeze("modules/", "uModBusTCPSlave.py")
freeze("modules/", "uModBusConst.py")
# ===== 规则引擎 =====
freeze("modules/", "rules_engine_full.py")
# ===== 入口 main.py =====
freeze("modules/", "main.py")
8.3 脚本化构建:为什么 eval $(python3 $IDF_PATH/tools/idf_tools.py export) 比 source export.sh 更稳
export.sh 依赖交互式 shell + bash completion;在:
- PowerShell →
wsl -d Ubuntu -u admin -- bash -c "source ..."(非交互) - crontab / 脚本 / CI runner (非交互)
等场景中会静默不生效 (which idf.py 空)。更可靠的方式是直接 eval idf_tools.py 的输出:
bash
#!/bin/bash
# build_ecm50b_final.sh (ECM50B 实战脚本,233 秒完成)
set -u
IDF_PATH="$HOME/esp-idf"
BOARD=ECM50B
PORT=/home/admin/micropython/ports/esp32
echo "=== ECM50B FINAL BUILD v3 $(date) ==="
cd "$PORT"
# -------- Step 1: 激活 IDF 环境(非交互 shell 也生效)--------
echo "--- Step 1: Activate ESP-IDF via idf_tools.py eval ---"
eval "$(/usr/local/bin/python3 "$IDF_PATH/tools/idf_tools.py" export)"
which idf.py
which xtensa-esp-elf-gcc
# -------- Step 2: mpy-cross 缺失时重新编译 --------
echo "--- Step 2: mpy-cross check ---"
if [ ! -x "$PORT/../../mpy-cross/build/mpy-cross" ]; then
make -C "$PORT/../../mpy-cross" -j4
fi
# -------- Step 3: 清理(板级 Kconfig / CMake 变量改动后**必须**!)--------
echo "--- Step 3: Clean build directory ---"
rm -rf build-$BOARD build-$BOARD.log
# -------- Step 4: make (-j3 对 N5105 最经济)--------
echo "--- Step 4: make BOARD=$BOARD USER_C_MODULES= -j3 ---"
export IDF_SKIP_CHECK_SUBMODULES=1
make BOARD=$BOARD USER_C_MODULES= -j3 2>&1 | tee build-$BOARD.log
RC=$?
echo ""
echo "=== BUILD COMPLETED $(date) RC=$RC Duration=$((SECONDS))s ==="
if [ $RC -eq 0 ]; then
echo ""
echo "--- Firmware artifacts ---"
ls -lh build-$BOARD/bootloader/bootloader.bin \
build-$BOARD/partition_table/partition-table.bin \
build-$BOARD/firmware.bin \
build-$BOARD/micropython.bin
fi
exit $RC
额外前置:
/usr/bin/env: 'python': No such file or directory(很多脚本#!/usr/bin/env python但 WSL 只装了python3):
bashsudo ln -sf /usr/bin/python3 /usr/local/bin/python
8.4 PowerShell → WSL 传参陷阱(弯路 #13 展开)
❌ 错误写法(失败或静默 tee 0 字节):
powershell
wsl -d Ubuntu -u admin -- bash -c "rm -rf build-ECM50B build.log && source ~/esp-idf/export.sh | tee build.log"
&&、\|、"..." 都被 PowerShell 吃掉变成自己的运算符了。正确有两招:
✅ 方案 A(推荐:写脚本文件):
powershell
# 1) 先 copy 并加执行权限
wsl -d Ubuntu -u admin -- cp /mnt/c/Work/ESP-IDF/TEMP/build_ecm50b_final_v3.sh /home/admin/build_scripts/build_ecm50b_final.sh
wsl -d Ubuntu -u admin -- chmod +x /home/admin/build_scripts/build_ecm50b_final.sh
# 2) 直接执行(不带参数,最稳)
wsl -d Ubuntu -u admin /home/admin/build_scripts/build_ecm50b_final.sh
✅ 方案 B(必须一行里跑的话) :加 --% 告诉 PS 停止解析后面:
powershell
wsl -d Ubuntu -u admin --% bash -lc 'rm -rf ~/micropython/ports/esp32/build-ECM50B && cd ~/micropython/ports/esp32 && export IDF_SKIP_CHECK_SUBMODULES=1 && source ~/esp-idf/export.sh >/tmp/idf.log 2>&1 && make BOARD=ECM50B USER_C_MODULES= -j3'
8.5 产物校验:复制到 Windows 后必做 MD5 一致性检查
WSL → Windows 的 DrvFs 复制偶有"size 对但内容错"(非常罕见,但 1.5 MB 文件错 1 个比特就是砖头固件)。ECM50B 的做法:
bash
# copy_firmware_artifacts.sh (放在 build_scripts 里)
SRC=/home/admin/micropython/ports/esp32/build-ECM50B
DST=/mnt/c/Work/ESP32DEV/ECM50B/firmware
mkdir -p "$DST"
for f in bootloader/bootloader.bin \
partition_table/partition-table.bin \
micropython.bin \
firmware.bin; do
cp -v "$SRC/$f" "$DST/$(basename $f)"
done
# ----- MD5 两边对比 -----
(cd "$SRC" && md5sum bootloader/bootloader.bin \
partition_table/partition-table.bin \
micropython.bin firmware.bin) > /tmp/src.md5
(cd "$DST" && md5sum bootloader.bin partition-table.bin \
micropython.bin firmware.bin) > /tmp/dst.md5
SRC_MD5=$(awk '{print $1}' /tmp/src.md5 | sort)
DST_MD5=$(awk '{print $1}' /tmp/dst.md5 | sort)
if [ "$SRC_MD5" = "$DST_MD5" ]; then
echo "✅ MD5 OK: 所有文件一致。"
else
echo "❌ MD5 FAIL: 文件复制损坏!"
diff /tmp/src.md5 /tmp/dst.md5
exit 1
fi
✅ ECM50B 最终 4 个 MD5 校验一致:
bootloader.bin: 28c215c4ec7733cb61784d49ffdc4303 (23 KB)
partition-table.bin: b87dcdfd58f072a1e7b50b94abe79496 (3 KB)
micropython.bin: 73d2a94fb79bb9cc653e9b0fcd030276 (1.4 MB @0x10000)
firmware.bin: f2b7b6d9142b917a701dfc4c04efe056 (1.5 MB merged)
9. ECM50B 构建/补丁脚本速查(TEMP → WSL build_scripts)
所有脚本在 Windows 端 C:\Work\ESP-IDF\TEMP\*.sh,对应 WSL 端 /home/admin/build_scripts/*.sh(每次改完记得 cp + chmod +x 一次)。
| 脚本 | 用途 | 执行时机 |
|---|---|---|
fix_mpconfigboard_h_v2.sh |
覆写 mpconfigboard.h(BOARD_NAME + MCU_NAME + 关 BT/USB/BTREE 宏) | 板级配置变动后 |
fix_mpconfigboard_h.sh |
旧版(只有关模块宏,丢了 BOARD_NAME → 勿用) | 废弃 |
patch_lwip_compat.sh |
lwipopts.h 追加 CALC→SET 兼容宏 + 建 stub acd.h |
每次 rm -rf build-* 之后、make 之前 |
patch_disable_bt.sh |
sdkconfig.board 追加 CONFIG_BT_*=n |
新增 board 目录时 1 次 |
patch_berkeley_db.sh |
创建 include/berkeley-db 头文件路径(后来 BTree 直接关了,基本废弃) | 不再需要 |
sync_esp_wifi_v2.sh |
git submodule update components/esp_wifi(同步 esp32-wifi-lib 二进制) |
IDF 源码新克隆后 1 次;或 WiFi _internal undefined 时 |
fix_ppp_stubs.sh |
旧版(用 #if !CONFIG_LWIP_PPP_SUPPORT 保护 + 会被 CONFIG=1 跳过 → 废弃) |
废弃 |
fix_ppp_stubs_v2.sh |
用对版:3 个 PPP weak 函数(set_default/start/stop)+ 无保护块 | 第一轮 PPP undefined 后 |
fix_ppp_stubs_v3.sh |
补完版 :追加 new_ppp/destroy_ppp + 全局指针 esp_network_ppp_lwip_type(共 6 weak 符号) |
第二轮 PPP undefined 后 |
fix_lwip_patch_brace.sh |
修复 head -n 截断时丢失 esp_wifi_ap_set_group_mgmt_cipher_internal 的 closing } + 花括号平衡检查 |
每次改 lwip_patch.c 后跑 |
build_ecm50b_v2.sh |
旧版(依赖 source export.sh 不稳定) |
废弃 |
build_ecm50b_final_v3.sh |
主构建脚本 :idf_tools.py export 激活环境 → 清 build → make -j3,成功则列 4 个 bin 大小 |
日常编译入口 |
copy_firmware_artifacts.sh |
4 个 bin 复制到 C:\Work\ESP32DEV\ECM50B\firmware\ + TEMP\ECM50B_firmware\ + 带时间戳 snapshot + MD5 校验 |
每次 build RC=0 后执行 |
10. 实战:从经典 ESP32 切换到 ESP32-S3 目标(ECM50B 烧录翻车→修复→验证全记录)
背景 :ECM50B 初次编译时按经典 ESP32(
IDF_TARGET=esp32)构建,烧录时 esptool 检测到实际芯片是 ESP32-S3,固件被拒绝写入。本节记录从发现问题到修复验证的完整过程。
10.1 问题现象
powershell
# 第一次尝试(按 ESP32 烧)
python -m esptool --chip esp32 -p COM10 ... write_flash 0x10000 firmware.bin
→ A fatal error occurred: This chip is ESP32-S3 not ESP32. Wrong --chip argument?
# 第二次尝试(改 esp32s3,但固件仍是 ESP32 编译的)
python -m esptool --chip esp32s3 -p COM10 ... write_flash 0x10000 firmware.bin
→ Unexpected chip id in image. Expected 9 but value was 0.
Is this image for a different chip model?
→ C:\...\firmware.bin is not an ESP32-S3 image.
根因 :MicroPython ESP32 端口的 mpconfigboard.cmake 默认 IDF_TARGET=esp32(经典 ESP32)。如果板级文件没有显式 set(IDF_TARGET esp32s3),编译出的固件 bin 里嵌入的 chip ID = 0(ESP32),而 ESP32-S3 的 chip ID = 9。esptool 校验不过直接拒绝。
10.2 用 esptool 确认实际硬件型号
powershell
# 用 Thonny 的 Python(已装 esptool)
C:\Thonny\python.exe -m esptool --chip esp32s3 -p COM10 flash_id
输出:
Chip is ESP32-S3 (revision v0.2)
Features: WiFi, BLE
Crystal is 40MHz
Manufacturer: 46
Device: 4018
Detected flash size: 16MB
Flash type set in eFuse: quad (4 data lines)
确认:ESP32-S3, 16MB Quad Flash, 40MHz 晶振。
10.3 修改 4 个板级文件(IDF_TARGET 切换)
① mpconfigboard.cmake --- 最关键的一行:
cmake
# ★ 必须在最前面,esp32_common.cmake 会读取此变量
set(IDF_TARGET esp32s3)
# 以下保持不变(模块裁剪)
set(MICROPY_PY_BTREE OFF ...)
set(MICROPY_PY_BLUETOOTH OFF ...)
set(SDKCONFIG_DEFAULTS
boards/sdkconfig.base
)
② mpconfigboard.h --- MCU 名称 + UART REPL:
c
// 改 MCU 名称
#define MICROPY_HW_MCU_NAME "ESP32S3" // 原来是 "ESP32"
// S3 无原生 USB 时启用 UART REPL(S3 默认可能走 USB-CDC)
#define MICROPY_HW_ENABLE_UART_REPL (1)
③ sdkconfig.board --- Flash 配置:
ini
# 16MB Quad Flash, 80MHz
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_ESPTOOLPY_FLASHMODE_QIO=y
CONFIG_ESPTOOLPY_FLASHFREQ_80M=y
④ board.json --- 元数据:
json
{ "mcu": "esp32s3", "product": "ECM50B Host-B (ESP32-S3, 16MB Flash)" }
10.4 清理 + 重新编译
bash
# ★ 必须删除旧 build 目录(IDF_TARGET 变了,CMake 缓存全废)
rm -rf build-ECM50B
make BOARD=ECM50B USER_C_MODULES= -j4
编译结果:1372/1372 目标完成,RC=0,257 秒。S3 比 ESP32 经典版多了 ~68 个编译目标(S3 专属 WiFi/BLE/USB 驱动源文件)。
10.5 正确的烧录命令
powershell
# ★ 合并固件 firmware.bin 写 0x0(不是 0x10000!)
C:\Thonny\python.exe -m esptool --chip esp32s3 -p COM10 -b 460800 `
--before default_reset --after hard_reset `
write_flash --flash_mode dio --flash_size 16MB --flash_freq 80m `
0x0 C:\Work\ESP32DEV\ECM50B\firmware\firmware.bin
烧录结果:1,454,656 bytes 写入 21.5 秒,Hash verified,Hard reset。
10.6 验证 MicroPython REPL + 业务代码运行
python
# 发送 Ctrl-C 中断 + REPL 命令
>>> import sys; print(sys.implementation); print(sys.platform)
(name='micropython', version=(1, 28, 0, ''),
_machine='ECM50B Host-B with ESP32S3', ← ★ 板名 + MCU 名正确
_build='ECM50B',
_mpy=11014, _thread='GIL')
esp32 ← sys.platform 对所有 ESP32 变体都是 'esp32'
ESP-NOW 通信也在工作(frozen 模块加载成功):
[Discovery] MAC=14:C1:9F:C1:4E:84 Name=未知设备 Type=未知(0xF0) ID=0 CRC=OK
[ESP-NOW C3DI] #9
发送方MAC: 70:af:09:12:fd:d0
DI状态: 0xFFD7FFFF
10.7 关键差异速查:ESP32 经典 vs ESP32-S3
| 项目 | ESP32 经典 | ESP32-S3 | 影响 |
|---|---|---|---|
| CPU 核心 | Xtensa LX6 双核 240MHz | Xtensa LX7 双核 240MHz | 指令集不完全兼容,固件不可混用 |
| chip ID (esptool) | 0 | 9 | esptool 校验 image header 里的 chip ID |
| IDF_TARGET | esp32 |
esp32s3 |
决定编译器选哪个 SoC 头文件/链接脚本 |
| 默认 UART0 引脚 | GPIO 1 (TX) / GPIO 3 (RX) | GPIO 43 (TX) / GPIO 44 (RX) | 需 MICROPY_HW_ENABLE_UART_REPL=1 |
| WiFi/BLE 二进制库 | lib_esp32/*.a |
lib_esp32s3/*.a |
__weak 存根的符号可能不同 |
| Flash 类型 | 通常 Quad | Quad 或 Octal(eFuse 决定) | --flash_mode dio vs --flash_mode dout |
| PSRAM | 外挂 SPI | 内置(Quad/Octal) | 需 CONFIG_SPIRAM=y + CONFIG_SPIRAM_MODE_OCT/QUAD=y |
| USB OTG | 无 | 有(原生 USB-CDC) | 不用时 MICROPY_HW_ENABLE_USB=0 |
| 编译目标数 | ~1304 | ~1372 | S3 多 ~68 个 SoC 驱动源文件 |
11. 实战:启用 Octal PSRAM + 16MB Flash(S3N16R8 完整配置)
硬件 :ESP32-S3 N16R8(16MB Quad Flash + 8MB Octal PSRAM)
目标 :MicroPython 堆扩展到 PSRAM(
gc.mem_free()从 ~300KB → 8.3MB)
11.1 发现问题:sdkconfig.board 写了但没生效
第一轮 S3 固件编译成功,但烧录后 gc.mem_free() 只有 ~300KB(PSRAM 没启用)。检查 build 日志发现:
micropython.bin binary size 0x1558e0 bytes.
Smallest app partition is 0x1f0000 bytes. ← 4MB 分区表,不是 16MB
--flash_size 4MB ← Flash 仍是 4MB
根因三连:
| # | 问题 | 根因 |
|---|---|---|
| 1 | Flash 仍是 4MB | sdkconfig.base 硬编码 CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y |
| 2 | 分区表仍 4MB | sdkconfig.base 硬编码 CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions-4MiBplus.csv" |
| 3 | PPP 被强制启用 | sdkconfig.base 硬编码 CONFIG_LWIP_PPP_SUPPORT=y(之前 __weak 存根就是补这个坑) |
为什么我们 sdkconfig.board 里的覆盖没生效? 两个致命错误:
错误 A:SDKCONFIG_DEFAULTS 没包含 sdkconfig.board
mpconfigboard.cmake 里只写了:
cmake
# ❌ 错误:没有 boards/ECM50B/sdkconfig.board!
set(SDKCONFIG_DEFAULTS
boards/sdkconfig.base
boards/sdkconfig.spiram_sx
boards/sdkconfig.spiram_oct
)
ESP-IDF 的 Kconfig 系统按 SDKCONFIG_DEFAULTS 列表从上到下加载,后面的覆盖前面的。sdkconfig.board 不在列表里 = 永远不被加载。
修正 :必须显式加入,且放在最后(最高优先级):
cmake
# ✅ 正确:sdkconfig.board 放最后,覆盖所有 base 默认值
set(SDKCONFIG_DEFAULTS
boards/sdkconfig.base
boards/sdkconfig.spiram_sx
boards/sdkconfig.spiram_oct
boards/ECM50B/sdkconfig.board # ← 必须有!放最后!
)
错误 B:Kconfig bool 取消语法写错
ini
# ❌ 错误:CONFIG_XXX= (空值) 不能取消 bool 类型选项
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=
CONFIG_LWIP_PPP_SUPPORT=
# ✅ 正确:Kconfig 取消 bool 必须用 "# not set" 语法
# CONFIG_ESPTOOLPY_FLASHSIZE_4MB is not set
# CONFIG_LWIP_PPP_SUPPORT is not set
这是 Kconfig 规则:CONFIG_XXX= 只是把值设为空字符串,不会取消选择 ;# CONFIG_XXX is not set 才是正确的"反选"语法。
11.2 官方 SPIRAM_OCT 变体的配置方式
MicroPython 1.28 用 3 个 sdkconfig 片段叠加启用 Octal PSRAM:
| 文件 | 作用 | 关键配置 |
|---|---|---|
boards/sdkconfig.spiram_sx |
SPIRAM 基础(S2/S3 通用) | CONFIG_SPIRAM=y CONFIG_SPIRAM_MODE_QUAD=y CONFIG_SPIRAM_SPEED_80M=y CONFIG_SPIRAM_USE_MALLOC=y |
boards/sdkconfig.spiram_oct |
覆盖为 Octal 模式 | CONFIG_SPIRAM_MODE_QUAD= → CONFIG_SPIRAM_MODE_OCT=y |
boards/sdkconfig.240mhz |
CPU 240MHz | CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y |
变体通过 mpconfigvariant_SPIRAM_OCT.cmake 引入:
cmake
set(SDKCONFIG_DEFAULTS
${SDKCONFIG_DEFAULTS}
boards/sdkconfig.240mhz
boards/sdkconfig.spiram_oct
)
ECM50B 直接在 mpconfigboard.cmake 里引入前两个(240MHz 可选,默认 160MHz 够用)。
11.3 ECM50B 完整的 S3N16R8 配置(4 个文件)
① mpconfigboard.cmake:
cmake
set(IDF_TARGET esp32s3)
set(SDKCONFIG_DEFAULTS
boards/sdkconfig.base
boards/sdkconfig.spiram_sx # SPIRAM=y, QUAD, 80MHz, malloc
boards/sdkconfig.spiram_oct # 覆盖 QUAD→OCT
boards/ECM50B/sdkconfig.board # ★ 必须最后!覆盖 base 的 4MB/PPP 等
)
set(MICROPY_PY_BTREE OFF CACHE BOOL "" FORCE)
set(MICROPY_PY_BLUETOOTH OFF CACHE BOOL "" FORCE)
② sdkconfig.board(关键覆盖项):
ini
# === Flash: 16MB QIO 80MHz(覆盖 base 的 4MB)===
# CONFIG_ESPTOOLPY_FLASHSIZE_4MB is not set
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_ESPTOOLPY_FLASHMODE_QIO=y
CONFIG_ESPTOOLPY_FLASHFREQ_80M=y
# === 分区表:16MB 自定义(4MB app + 12MB VFS)===
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions-16MiB.csv"
# === SPIRAM Octal ===
CONFIG_SPIRAM=y
CONFIG_SPIRAM_BOOT_INIT=y
CONFIG_SPIRAM_USE_MALLOC=y
# CONFIG_SPIRAM_MODE_QUAD is not set
CONFIG_SPIRAM_MODE_OCT=y
CONFIG_SPIRAM_SPEED_80M=y
CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=8192
# === 关闭 PPP(覆盖 base 的 PPP_SUPPORT=y)===
# CONFIG_LWIP_PPP_SUPPORT is not set
# CONFIG_LWIP_PPP_PAP_SUPPORT is not set
# CONFIG_LWIP_PPP_CHAP_SUPPORT is not set
# === 关闭蓝牙 ===
# CONFIG_BT_ENABLED is not set
③ partitions-16MiB.csv (新建,放在 ports/esp32/ 目录):
csv
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 0x3F0000,
vfs, data, fat, 0x400000, 0xC00000,
分区布局:bootloader @0x1000(不在表中)→ nvs 24KB → phy 4KB → factory app 4MB → VFS 12MB
④ mpconfigboard.h(MCU 名称 + UART REPL):
c
#define MICROPY_HW_BOARD_NAME "ECM50B Host-B"
#define MICROPY_HW_MCU_NAME "ESP32S3"
#define MICROPY_HW_ENABLE_UART_REPL (1)
// ... 其余 BT/BTree/USB 关闭宏同 §7
11.4 编译验证(关键指标对比)
| 指标 | 之前(4MB 无 PSRAM) | 之后(16MB + Octal PSRAM) |
|---|---|---|
--flash_size |
4MB | 16MB |
| App 分区大小 | 0x1F0000 (1.84MB) | 0x3F0000 (3.94MB) |
| App 空闲 | 31% (624KB) | 66% (2.7MB) |
gc.mem_free() |
~300KB | 8,319,424 bytes (7.9MB) ★ |
| 编译目标数 | 1372 | 1375 |
| 编译耗时 | 257s | 247s |
11.5 运行时验证
python
>>> import sys; print(sys.implementation)
(name='micropython', version=(1, 28, 0, ''),
_machine='ECM50B Host-B with ESP32S3',
_build='ECM50B', _mpy=11014)
>>> import gc; print(gc.mem_free())
8319424 # ← 7.9MB!PSRAM 生效的铁证(无 PSRAM 时仅 ~300KB)
11.6 踩坑速查:关 SoftAP 会导致链接失败
MicroPython ports/esp32/network_wlan.c:226 无条件调用 esp_netif_create_default_wifi_ap():
c
// network_wlan.c --- esp_initialise_wifi() 函数内
esp_netif_create_default_wifi_ap(); // ← 即使不用 AP 也会调用
如果 # CONFIG_ESP_WIFI_SOFTAP_SUPPORT is not set,ESP-IDF 不会编译 esp_netif_create_default_wifi_ap 的实现 → 链接 undefined reference。
结论 :CONFIG_ESP_WIFI_SOFTAP_SUPPORT=y 必须保持开启。不在固件里 wlan.active(True, wlan.AP_IF) 即可,代码体积差异可忽略。
12. 实战:Frozen 模块未编译进固件的排查与修复
现象 :固件烧录后
gc.mem_free()正常(8.3MB PSRAM 生效),但import main报ImportError: no module named 'main',help('modules')列表里 24 个自定义业务模块(main / uModBusTCPSlave / rules_engine_full / espnow_manager 等)全部缺失。
12.1 排查思路
gc.mem_free() = 8,309,280 ← PSRAM 正常,说明 sdkconfig.board 生效了
help('modules') ← 只有官方内置模块,没有 24 个业务模块
import main ← ImportError
PSRAM 配置生效但 frozen 模块缺失 → 问题出在 manifest 没被执行,不是 sdkconfig。
12.2 根因:mpconfigboard.cmake 中 MICROPY_FROZEN_MANIFEST 路径损坏
检查 boards/ECM50B/mpconfigboard.cmake,发现关键行被损坏:
cmake
# ❌ 损坏的行(${MICROPY_BOARD_DIR} 变量丢失,多了一个 n):
n# Point frozen manifest to this board directory (not the port-level default)
set(MICROPY_FROZEN_MANIFEST /manifest.py)
/manifest.py 指向文件系统根目录(不存在),CMake 找不到 manifest 文件 → 所有 freeze() 命令从未执行 → 24 个模块全部缺失。
正确写法:
cmake
# ✅ 修复后:
# Point frozen manifest to this board directory (not the port-level default)
set(MICROPY_FROZEN_MANIFEST ${MICROPY_BOARD_DIR}/manifest.py)
${MICROPY_BOARD_DIR} 由 CMake 在 make BOARD=ECM50B 时自动展开为 boards/ECM50B,指向板级 manifest.py。
12.3 验证 manifest 是否被执行
编译日志里搜索 makemanifest,正常情况下应看到:
[1108/1375] cd .../build-ECM50B/esp-idf/main && python .../tools/makemanifest.py \
-o .../frozen_content.c \
-v BOARD_DIR=.../boards/ECM50B \
-v MPY_DIR=.../micropython \
-v PORT_DIR=.../ports/esp32 \
-b .../build-ECM50B \
-f-march=xtensawin \
--mpy-tool-flags= \
.../boards/ECM50B/manifest.py
GEN .../build-ECM50B/frozen_content.c
关键标志:
GEN .../frozen_content.c--- manifest 已执行,生成了 frozen 内容文件frozen_content.c大小 ~2.4MB --- 24 个模块的 bytecode 已嵌入[1369/1375] Building C object ... frozen_content.c.obj--- frozen 内容已编译进固件
如果日志里没有 makemanifest 和 GEN frozen_content.c,说明 manifest 根本没被调用。
12.4 修复后验证
烧录修复后的固件,REPL 验证:
python
>>> import main
[规则引擎] 已加载 (Frozen module)
[ModbusTCP] 已加载 (Frozen module)
>>> print(main.__file__)
main.py # ← frozen 模块标识(无路径前缀)
>>> import gc; print(gc.mem_free())
8294272 # ← ~8MB PSRAM
>>> help('modules')
# 应显示全部 24 个自定义模块:
# GW1500_Configure PLC_Configure _embedded_json boot config
# espnow_c3di espnow_c3dq espnow_c3ups espnow_common espnow_manager
# espnow_message main modbus_master network_manager rules_engine_full
# serial sysmon tcp_server trace_recorder uModBusConst
# uModBusFunctions uModBusTCPPoll uModBusTCPSlave webrepl_cfg
12.5 踩坑速查:manifest 路径变量
MicroPython 1.28 的 manifest.py 支持以下路径变量(区分大小写):
| 变量 | 展开值 | 用途 |
|---|---|---|
$(MPY_DIR) |
MicroPython 源码根目录 | 引用 extmod/asyncio 等官方模块 |
$(PORT_DIR) |
ports/esp32 |
引用端口级 modules/(inisetup、flashbdev 等) |
$(BOARD_DIR) |
ports/esp32/boards/ECM50B |
引用板级 modules/(业务 .py 文件) |
$(MPY_LIB_DIR) |
lib/micropython-lib |
引用 micropython-lib(需完整 clone 子模块) |
常见错误:
freeze("modules/", "x.py")→ 相对路径,按 CWD(ports/esp32/)解析,找的是ports/esp32/modules/x.py而非boards/ECM50B/modules/x.py❌freeze("$(BOARD_DIR)/modules/", "x.py")→ 绝对路径,正确指向板级文件 ✅
13. 实战:WebREPL 模块缺失修复(import webrepl_setup 报 ImportError)
现象 :固件烧录后
import webrepl_setup报ImportError: no module named 'webrepl_setup',boot.py 启动时输出[Network] webrepl模块不可用,WebREPL 无法自动启动。
13.1 根因:MicroPython 1.28 源码树缺少 webrepl.py 高层模块
MicroPython 的 WebREPL 分为两层:
| 层级 | 模块名 | 实现语言 | 位置 | 1.28 源码树状态 |
|---|---|---|---|---|
| 底层 | _webrepl |
C | extmod/modwebrepl.c |
✅ 存在(已编译进固件) |
| 高层 | webrepl |
Python | ports/esp32/modules/webrepl.py 或 lib/micropython-lib/python-stdlib/webrepl/ |
❌ 不存在 |
| 配置工具 | webrepl_setup |
Python | ports/esp32/modules/webrepl_setup.py |
❌ 不存在 |
MicroPython 1.28 将 webrepl.py 从 ports/esp32/modules/ 移到了 micropython-lib 子模块。但我们的项目没有初始化 micropython-lib 子模块(git submodule status lib/micropython-lib 显示前缀 -),所以 webrepl.py 完全缺失。
network_manager.py 中 import webrepl 失败 → WEBREPL_AVAILABLE = False → start_webrepl() 输出 "webrepl模块不可用"。
13.2 修复:手动创建 webrepl.py 和 webrepl_setup.py 并 freeze
13.2.1 _webrepl C 模块的正确接口(关键!)
通过查看 extmod/modwebrepl.c 源码,_webrepl C 模块的接口是:
| 接口 | 类型 | 用途 |
|---|---|---|
_webrepl.password(passwd) |
模块级函数 | 设置 WebREPL 密码 |
_webrepl._webrepl(websock) |
类构造函数 | 创建 webrepl 对象,处理 WebSocket 连接 |
注意:_webrepl 没有 start() 函数! 错误调用 _webrepl.start(port, password) 会报 'module' object has no attribute 'start'。
13.2.2 创建 boards/ECM50B/modules/webrepl.py(官方标准实现)
关键 :使用 micropython-lib 官方 webrepl.py 标准实现(154 行完整版本)。WebREPL 支持双模式:
- HTTP GET
/→ 返回 HTML 外壳页面(浏览器直接访问 URL 看到 WebREPL 客户端) - WebSocket Upgrade → 建立 REPL 连接(ws:// 协议)
我的第一个简化版 webrepl.py 跳过了 HTTP 处理,导致浏览器访问直接 "Connection reset"。
python
# webrepl.py - MicroPython WebREPL server (official standard implementation)
# Source: micropython-lib/micropython/net/webrepl/webrepl.py
# Supports: HTTP GET → returns HTML shell page, WebSocket Upgrade → REPL connection
import binascii
import hashlib
from micropython import const
import network
import os
import socket
import sys
import websocket
import _webrepl
listen_s = None
client_s = None
DEBUG = 0
_DEFAULT_STATIC_HOST = const("https://micropython.org/webrepl/")
static_host = _DEFAULT_STATIC_HOST
def server_handshake(cl):
"""读取 HTTP 请求头,判断是 WebSocket Upgrade 还是普通 HTTP GET。
返回 True=WebSocket(返回 101 Switching),False=普通 HTTP(调用方应返回 HTML)"""
req = cl.makefile("rwb", 0)
l = req.readline() # Skip "GET / HTTP/1.1"
webkey = None
upgrade = False
ws = False
while True:
l = req.readline()
if not l:
return False
if l == b"\r\n":
break
h, v = [x.strip() for x in l.split(b":", 1)]
if h == b"Sec-WebSocket-Key":
webkey = v
elif h == b"Connection" and b"Upgrade" in v:
upgrade = True
elif h == b"Upgrade" and v == b"websocket":
ws = True
if not (upgrade and ws and webkey):
return False
# 计算 Sec-WebSocket-Accept
d = hashlib.sha1(webkey)
d.update(b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
respkey = binascii.b2a_base64(d.digest())[:-1]
cl.send(b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ")
cl.send(respkey)
cl.send(b"\r\n\r\n")
return True
def send_html(cl):
"""普通 HTTP GET → 返回 HTML 外壳页面,JS 从 micropython.org 加载。
响应内容与 C3DI1/C3DQ1/C3UPS 官方固件完全一致:HTTP/1.0 200 OK + <base href>+<script>"""
cl.send(b"HTTP/1.0 200 OK\r\n\r\n<base href=\"")
cl.send(static_host)
cl.send(b"\"></base>\r\n<script src=\"webrepl_content.js\"></script>\r\n")
cl.close()
def setup_conn(port, accept_handler):
global listen_s
listen_s = socket.socket()
listen_s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
ai = socket.getaddrinfo("0.0.0.0", port)
addr = ai[0][4]
listen_s.bind(addr)
listen_s.listen(1)
if accept_handler:
listen_s.setsockopt(socket.SOL_SOCKET, 20, accept_handler)
for i in (network.WLAN.IF_AP, network.WLAN.IF_STA):
iface = network.WLAN(i)
if iface.active():
print("WebREPL server started on http://%s:%d/" % (iface.ifconfig()[0], port))
return listen_s
def accept_conn(listen_sock):
"""accept 回调:先 server_handshake 判断连接类型,HTML or WebSocket REPL"""
global client_s
cl, remote_addr = listen_sock.accept()
if not server_handshake(cl):
# 非 WebSocket 请求 → 返回 HTML 外壳
send_html(cl)
return False
# 并发连接拒绝(已有 dupterm)
prev = os.dupterm(None)
os.dupterm(prev)
if prev:
print("\nConcurrent WebREPL connection from", remote_addr, "rejected")
cl.close()
return False
print("\nWebREPL connection from:", remote_addr)
client_s = cl
# ★关键调用链★:raw socket → websocket 对象(modwebsocket)→ _webrepl 对象(modwebrepl密码/文件协议)
ws = websocket.websocket(cl, True) # websocket 帧封装(C 内建模块)
ws = _webrepl._webrepl(ws) # 密码校验+文件传输协议(C 内建模块)
cl.setblocking(False)
if hasattr(os, "dupterm_notify"):
cl.setsockopt(socket.SOL_SOCKET, 20, os.dupterm_notify)
os.dupterm(ws) # 注册为终端(字符进出都走 ws)
return True
def stop():
global listen_s, client_s
os.dupterm(None)
if client_s:
client_s.close()
if listen_s:
listen_s.close()
def start(port=8266, password=None, accept_handler=accept_conn):
global static_host
stop()
webrepl_pass = password
if webrepl_pass is None:
try:
import webrepl_cfg
webrepl_pass = webrepl_cfg.PASS
if hasattr(webrepl_cfg, "BASE"):
static_host = webrepl_cfg.BASE
except Exception:
print("WebREPL is not configured, run 'import webrepl_setup'")
_webrepl.password(webrepl_pass)
s = setup_conn(port, accept_handler)
if accept_handler is None:
accept_conn(s)
return s
调用链全景图(WebSocket REPL 连接建立时):
浏览器 HTTP GET / → server_handshake() 判断非 Upgrade → send_html() → 返回 HTML外壳(118B)
浏览器 WebSocket Upgrade → server_handshake() 计算 Sec-WebSocket-Accept → 101 Switching
→ websocket.websocket(cl) ← modwebsocket.c:WebSocket 帧读写(二进制/文本帧、mask、分片)
→ _webrepl._webrepl(ws) ← modwebrepl.c:密码提示+校验、文件 PUT/GET 协议
→ os.dupterm(ws) ← REPL 输出→ws.write(),ws.read()→REPL 输入
13.2.3 创建 boards/ECM50B/modules/webrepl_setup.py(交互式配置工具)
python
# webrepl_setup.py - WebREPL 交互式配置工具
# 类似官方 MicroPython 的 webrepl_setup:支持密码设置和重启
import sys
import os
WCFG_FILE = "webrepl_cfg.py"
def input_choice(prompt, choices):
while 1:
val = input(prompt)
if val in choices:
return val
def main():
# 读取 frozen webrepl_cfg 密码
frozen_pass = None
try:
import webrepl_cfg
frozen_pass = getattr(webrepl_cfg, 'PASS', None)
except Exception:
pass
# 读取文件系统 webrepl_cfg.py 密码(如果存在)
fs_pass = None
try:
with open(WCFG_FILE, 'r') as f:
for line in f:
if line.startswith('PASS'):
fs_pass = line.split('=')[1].strip().strip("'\"")
except Exception:
pass
current_pass = fs_pass if fs_pass else frozen_pass
print("WebREPL daemon auto-start status: enabled (frozen boot.py)")
if current_pass:
print("Current password: %s" % current_pass)
print("\nWould you like to (C)hange password or (Q)uit?")
choice = input_choice("> ", ('C', 'c', 'Q', 'q', ''))
if choice.upper() == 'C':
pwd = input("New password (4-9 chars): ")
if len(pwd) < 4 or len(pwd) > 9:
print("Password must be 4-9 chars long!")
return
pwd2 = input("Confirm password: ")
if pwd == pwd2:
try:
with open(WCFG_FILE, 'w') as f:
f.write("PASS = %r\n" % pwd)
print("Saved to " + WCFG_FILE)
except Exception as e:
print("Cannot save (frozen filesystem?): %s" % e)
import _webrepl
_webrepl.password(pwd) # 仅当前会话生效
print("Changes will be activated after reboot")
print("Would you like to reboot now? (y/n)")
if input_choice("> ", ('y', 'Y', 'n', 'N')).lower() == 'y':
import machine
machine.reset()
else:
print("Passwords do not match!")
else:
print("Quit.")
if __name__ == "__main__":
main()
在 frozen 固件中,
webrepl_cfg.py的 PASS='123456' 已固化。webrepl_setup尝试写入文件系统的webrepl_cfg.py(覆盖 frozen 版本),如果文件系统可写则密码持久化,否则仅当前会话生效。
13.2.4 在 manifest.py 中添加 freeze 命令
python
# boards/ECM50B/manifest.py 中 webrepl_cfg.py 之后添加:
freeze("$(BOARD_DIR)/modules/", "webrepl_cfg.py")
freeze("$(BOARD_DIR)/modules/", "webrepl.py") # ← 新增
freeze("$(BOARD_DIR)/modules/", "webrepl_setup.py") # ← 新增
13.2.5 network_manager.py 延迟导入
network_manager.py 中 import webrepl 从模块顶层移到 start_webrepl() 函数内部(延迟导入),避免固件启动早期 frozen 模块未完全初始化:
python
# 修改前(模块顶层导入):
try:
import webrepl
WEBREPL_AVAILABLE = True
except ImportError:
WEBREPL_AVAILABLE = False
# 修改后(函数内部延迟导入):
def start_webrepl(self, password='123456'):
try:
import webrepl # ← 延迟到调用时导入
except ImportError:
print("[Network] webrepl模块不可用")
return False
try:
webrepl.start(password=password)
print("[Network] WebREPL服务已启动")
return True
except Exception as e:
print(f"[Network] WebREPL启动失败: {e}")
return False
13.3 增量编译(无需全量重建)
修改 manifest.py 后,只需增量编译(重新生成 frozen_content.c + 链接),无需 rm -rf build-ECM50B:
bash
cd ~/micropython/ports/esp32
source ~/esp-idf/export.sh
make BOARD=ECM50B USER_C_MODULES= -j3
增量编译约 40 秒(全量约 600 秒)。
13.4 验证
烧录后,boot.py 输出应显示:
步骤2: 启动WebREPL...
WebREPL server started on http://192.168.72.192:8266/ ← webrepl.py 官方启动提示
[Network] WebREPL服务已启动 ← network_manager 确认
三重验证(必须全部通过):
验证①:浏览器直接访问 HTTP URL
输入:http://192.168.72.192:8266/
期望:页面显示 WebREPL 客户端(HTML外壳+JS从micropython.org加载)
如果显示空白/连接错误 → server_handshake() 未正确处理 GET /
验证②:HTTP 响应(PC端Python)
import socket
s=socket.socket(); s.settimeout(3); s.connect(('192.168.72.192',8266))
s.send(b'GET / HTTP/1.1\r\nHost: 192.168.72.192:8266\r\nConnection: close\r\n\r\n')
s.recv(4096)
期望:b'HTTP/1.0 200 OK\r\n\r\n<base href="https://micropython.org/webrepl/"></base>\r\n<script src="webrepl_content.js"></script>\r\n'
长度:118 bytes(与 C3DI1/C3DQ1/C3UPS 官方固件逐字节相同)
验证③:完整 WebSocket 握手 + 密码 + REPL 接入
GET / 携带 Upgrade: websocket + Sec-WebSocket-Key
期望:HTTP/1.1 101 Switching Protocols + Sec-WebSocket-Accept(RFC6455 标准)
发送密码帧:123456\r (Text frame 0x81)
期望:收到 "\r\nWebREPL connected\r\n>>> " (二进制帧 0x82)
三项全部通过 = WebREPL 完整可用。
13.5 WebREPL 客户端使用方法
浏览器直接访问设备 URL 即可(推荐方式):
| 方式 | 操作 |
|---|---|
| 方式A:设备自带 HTTP 页面(推荐) | 在浏览器地址栏输入 http://192.168.72.192:8266/ → 打开 WebREPL 客户端页面 → 页面自动填入 ws://IP:8266 → 点 Connect → 输入密码 123456 |
| 方式B:官方在线页面 | 打开 http://micropython.org/webrepl/ → 手动输入 ws://192.168.72.192:8266 → Connect → 密码 123456 |
方式A的原理:设备收到浏览器普通 HTTP GET 请求时,返回一个 HTML 外壳,包含
<base href="https://micropython.org/webrepl/">+<script src="webrepl_content.js">,浏览器从官网加载完整 JS 客户端并自动连接当前主机的 ws:// URL。
13.6 boot.py 自动启动流程
boot.py 中的 WebREPL 自动启动流程(修复后):
python
# boot.py 中的关键流程:
mgr = network_manager.get_network_manager()
mgr.init_wifi() # ① WiFi 连接
mgr.start_webrepl(password='123456') # ② 自动启动 WebREPL
└─ import webrepl # 延迟导入(函数内)
└─ webrepl.start(port=8266, password='123456')
└─ _listen_sock = setup_conn(8266, accept_conn) # 创建监听 socket
└─ _webrepl.password('123456') # 设置密码
# 当有连接进来时:
└─ accept_conn(listen_sock)
└─ wr = _webrepl._webrepl(cl) # 创建 webrepl 对象处理 WebSocket
└─ os.dupterm(wr) # 设为终端
附录 A. 本项目 TEMP 目录 & ubuntu-22.04-rootfs.tar.gz 备份说明
| 项目 | 绝对路径(本机) |
|---|---|
| TEMP 目录(所有构建脚本、日志、rootfs) | C:\Users\admin\AppData\Roaming\TRAE SOLO CN\ModularData\ai-agent\work-mode-projects\6a7c86d9d1984d74c066442b\TEMP |
| ubuntu-22.04-rootfs.tar.gz(WSL 导入用) | C:\Users\admin\AppData\Roaming\TRAE SOLO CN\ModularData\ai-agent\work-mode-projects\6a7c86d9d1984d74c066442b\TEMP\wslbuild\ubuntu-22.04-rootfs.tar.gz |
| 所有构建脚本 + 日志 | TEMP\wslbuild\step1*.sh ... step22*.sh、step99-cheatsheet.sh |
备份步骤(推荐一起复制到移动硬盘或 NAS 保存):
powershell
# PowerShell:整个 TEMP 目录复制出去
$dst = "D:\Backup\esp-mpy-build-env\"
New-Item -ItemType Directory -Force -Path $dst
Copy-Item -Recurse -Force "C:\Users\admin\AppData\Roaming\TRAE SOLO CN\ModularData\ai-agent\work-mode-projects\6a7c86d9d1984d74c066442b\TEMP" -Destination $dst
# (可选)单独备份 C:\WSL\Ubuntu\ext4.vhdx --- 下次整机坏了,
# 只要 $dst\TEMP\wslbuild\ubuntu-22.04-rootfs.tar.gz + 这一份 ext4.vhdx 在,就能 wsl --import 在 3 分钟内恢复
Copy-Item -Force C:\WSL\Ubuntu\ext4.vhdx -Destination $dst
附录 B. 快速开始速查(每次新开终端要做的)
powershell
# ★ 1. 进入 WSL
wsl -d Ubuntu
# 在 WSL 里:
export IDF_SKIP_CHECK_SUBMODULES=1
source ~/esp-idf/export.sh >/tmp/idf-export.log 2>&1
cd ~/micropython/ports/esp32
# ★ 2. 按芯片编译(任选其一):
# (a) ESP32-S3 自定义板(ECM50B 主机 --- S3N16R8, 16MB Flash)
# 板级配置已 set(IDF_TARGET esp32s3),直接 make 即可
make BOARD=ECM50B USER_C_MODULES= -j4
# (b) 经典 ESP32(ECM50A 主机,如仍是 ESP32 经典芯片)
make BOARD=ESP32_GENERIC USER_C_MODULES= -j4
# (c) ESP32-S3 SPIRAM_OCT(官方 S3N16R8 Octal PSRAM 变体)
make BOARD=ESP32_GENERIC_S3 BOARD_VARIANT=SPIRAM_OCT USER_C_MODULES= -j4
# (c) ESP32-C3 / C6(C3DI1 / C3DQ1 / C3UPS1 从机)
make BOARD=ESP32_GENERIC_C3 USER_C_MODULES= -j4
make BOARD=ESP32_GENERIC_C6 USER_C_MODULES= -j4
# (d) ESP32-P4(HMI 大屏双核 RISC-V)
make BOARD=ESP32_GENERIC_P4 USER_C_MODULES= -j4
附录 C. 烧录方式(贴合本机 ECM50A / ECM50B / C3 从机)
C.1 ECM50B(ESP32-S3 N16R8, COM10)--- 一键合并固件烧录
powershell
# ★ 推荐方式:合并固件 firmware.bin(含 bootloader + partition + app),写 0x0
C:\Thonny\python.exe -m esptool --chip esp32s3 -p COM10 -b 460800 --before default_reset --after hard_reset `
write_flash --flash_mode dio --flash_size 16MB --flash_freq 80m `
0x0 C:\Work\ESP32DEV\ECM50B\firmware\firmware.bin
⚠️ 注意:
--chip esp32s3不能写成esp32(否则 esptool 报This chip is ESP32-S3 not ESP32)合并固件
firmware.bin写在0x0,不是0x10000(0x10000 是 app-only 的micropython.bin)
--flash_size 16MB要和实际芯片一致(用python -m esptool --chip esp32s3 -p COM10 flash_id确认)ECM50B 硬件为 S3N16R8:16MB Quad Flash + 8MB Octal PSRAM
COM10 被占用报
Could not open COM10:通常是 Thonny/串口终端还连着。先关闭 Thonny(含后台 python 进程),再烧录:
powershell# 查找占用 COM10 的进程 Get-Process | Where-Object { $_.ProcessName -match 'thonny|python|putty' } | Select-Object Id, ProcessName # 关闭对应进程(替换 PID) Stop-Process -Id <PID> -Force
C.2 ECM50B 分体烧录(bootloader + partition + app 分别写)
powershell
C:\Thonny\python.exe -m esptool --chip esp32s3 -p COM10 -b 460800 --before default_reset --after hard_reset `
write_flash --flash_mode dio --flash_size 16MB --flash_freq 80m `
0x0 C:\Work\ESP32DEV\ECM50B\firmware\bootloader.bin `
0x8000 C:\Work\ESP32DEV\ECM50B\firmware\partition-table.bin `
0x10000 C:\Work\ESP32DEV\ECM50B\firmware\micropython.bin
C.3 其他设备
| 方式 | 适用场景 | 命令 |
|---|---|---|
| COM7(主机 ECM50A) | 经典 ESP32 芯片时 | python -m esptool --chip esp32 -p COM7 -b 460800 write_flash 0x0 bootloader.bin 0x8000 partition-table.bin 0x10000 micropython.bin |
| WebREPL OTA(从机 C3DI1/C3DQ1/C3UPS1,192.168.72.196/97/98:8266 / pwd 123456) | 从机不接串口,PC 起 python -m http.server 8000 分发 firmware.bin,webrepl 里 import urequests; open('/fw.bin','wb').write(urequests.get('http://192.168.72.10:8000/firmware.bin').content); import machine; machine.reset() |
无串口接线、适合批量升级 |
C.4 烧录后验证(frozen 模块 + PSRAM + WebREPL 四项必查)
烧录完成后,用串口终端(115200 baud)连接 COM10,按 Ctrl-C 进入 REPL,依次执行:
python
# ① Frozen 模块检查(最关键 --- 确认业务模块已编入固件)
>>> import main
[规则引擎] 已加载 (Frozen module)
[ModbusTCP] 已加载 (Frozen module)
# ② PSRAM 检查(应 >8MB,无 PSRAM 时仅 ~300KB)
>>> import gc; print(gc.mem_free())
8294272
# ③ WebREPL 检查(应成功导入,不报 ImportError)
>>> import webrepl
>>> import webrepl_setup
# ④ 模块清单检查(应包含 main / webrepl / uModBusTCPSlave / rules_engine_full 等 26 个模块)
>>> help('modules')
如果
import main报ImportError→ frozen 模块未编译进固件,参见 §12 排查mpconfigboard.cmake的MICROPY_FROZEN_MANIFEST路径。如果
import webrepl报ImportError→ WebREPL 高层模块缺失,参见 §13 创建webrepl.py并 freeze。
C.5 重新进入主程序入口
frozen 固件开机自动执行 boot.py → main.py。若主程序被 Ctrl-C 中断(停在 >>> REPL),重新进入的唯一干净方式:
python
>>> machine.reset()
重启后自动执行 boot.py(WiFi/WebREPL 初始化)→ main.py(ESP-NOW / W5500 / Modbus / 规则引擎全套启动)。
⚠️ 不要 用
import main; main.main()重入 --- 各子模块(WiFi / ESP-NOW / Modbus 线程)已初始化,重复调用会冲突报错。machine.reset()是唯一安全方式。
祝编译顺利! 🚀