WSL2 Ubuntu 22.04 LTS ESP-IDF 5.5 Micropython 1.28 固件编译指南

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.vhdxVHDX 动态盘,首次建议 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/nimblecomponents/openthreadcomponents/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.12https://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.cmakeMICROPY_PY_BLUETOOTH=OFF,见 §7.1 CMake Error: Cannot find source file .../ble_svc_cte.cmodbluetooth_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.cmakeMICROPY_PY_BTREE=OFFmpconfigboard.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 installmanifestfile.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.shchmod +xwsl -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 esp32This chip is ESP32-S3 not ESP32 板级 mpconfigboard.cmake 必须 set(IDF_TARGET esp32s3)(否则默认 esp32);同时 mpconfigboard.hMICROPY_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_*_internal wrapper 与 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_BLUETOOTHMicroPython 的宏 ,和 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 不加保护 = 两种情况都正确:

  1. 真实 esp_netif_ppp.c 编译 → 强定义覆盖 weak,运行真正实现
  2. 真实 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.pysys.path.insert(0, '/modbus') + from uModBusTCPSlave import ...(依赖 LittleFS 里有 /modbus 目录)
  • 规则引擎 exec(open('/softplc/rules_engine_full.py').read())(依赖文件系统里有 .py 源)
  • rules_engine_full.pyjson.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.pyload_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):

bash 复制代码
sudo 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 4MBVFS 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 mainImportError: 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.cmakeMICROPY_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 内容已编译进固件

如果日志里没有 makemanifestGEN 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_setupImportError: 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.pylib/micropython-lib/python-stdlib/webrepl/ 不存在
配置工具 webrepl_setup Python ports/esp32/modules/webrepl_setup.py 不存在

MicroPython 1.28 将 webrepl.pyports/esp32/modules/ 移到了 micropython-lib 子模块。但我们的项目没有初始化 micropython-lib 子模块(git submodule status lib/micropython-lib 显示前缀 -),所以 webrepl.py 完全缺失。

network_manager.pyimport webrepl 失败 → WEBREPL_AVAILABLE = Falsestart_webrepl() 输出 "webrepl模块不可用"。

13.2 修复:手动创建 webrepl.pywebrepl_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.pyimport 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*.shstep99-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 mainImportError → frozen 模块未编译进固件,参见 §12 排查 mpconfigboard.cmakeMICROPY_FROZEN_MANIFEST 路径。

如果 import webreplImportError → WebREPL 高层模块缺失,参见 §13 创建 webrepl.py 并 freeze。

C.5 重新进入主程序入口

frozen 固件开机自动执行 boot.pymain.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() 是唯一安全方式。


祝编译顺利! 🚀

相关推荐
琥珀色糖1 小时前
Linux GDB调试
linux·运维·服务器·gdb·调试
问天_观心1 小时前
虚拟环境WSL之Ubuntu的安装
人工智能·ubuntu
闲云野鹤在人间2 小时前
项目实战:LNMP-电商平台-ECshop
linux·运维·nginx·apache·lvs
李小白662 小时前
1-Shell编程和网络服务介绍
运维·云计算
吹什么轩2 小时前
linux网络:UDP套接字的业务实现:字典
linux·运维·udp
zzzll11112 小时前
Loop Engineering:循环工程的原理、实践与应用
java·数据库·python
啊啊啊啊啊!!!!2 小时前
【Linux】初始Linux操作系统
linux·运维·服务器
刘新洲3 小时前
别再只做会聊天的 Agent:我用 1 天把工具调用做成了可验证、可评测的工程系统
人工智能·python·openai