app_main 启动 GPTIMER 后即 return,FreeRTOS 仅剩空闲壳;
移植源码零 FreeRTOS 头/API,新增 GPIO 驱动框架并完成真机自检。真机证据与诚实边界见
第 11 节。
esp32s3 → build/,其余 → build_<target>/。目录选错会
烧写成功、rc=0、打 [OK],但烧的是上一次的旧产物。本仓的
tools/idf_flash.py 专门为此加了两条拒绝烧写的检查(见第 5 节)。frame-source=SYNTH 或干脆停发),而不是拿合成值顶上。
所以看到「没有数据」先分两件事:串口有没有开对?器件/接线在不在?0=成功、
1=失败、3=没跑到(缺硬件/缺依赖/口没打开)。
把 3 当成绿是这套体系里最严重的错误 —— 它会让「没验证」伪装成「验证过」。| 件 | 型号 / 规格 | 作用 | 接入 |
|---|---|---|---|
| 主控 A | ESP32-S3-DevKitC,N16R8(16MB Flash / 8MB Octal PSRAM) | MPU6050 六轴 + 温度节点;同时是板载 RMT 逻辑分析仪的载体 | USB → COM3 |
| 主控 B | ESP32-C6-DevKitC,N8(8MB Flash) | AM2302 温湿度节点(测温应用的真帧源) | USB → COM4 |
| 传感器 A | MPU6050(I2C,从机地址 0x68) |
加速度 / 陀螺 / 片上温度 | SDA=GPIO8,SCL=GPIO9 |
| 传感器 B | AM2302 / DHT22(单总线) | 温度 + 湿度 | DATA=GPIO4 |
| 上拉电阻 | 4.7kΩ–10kΩ(建议备 2 个) | I2C 与单总线都需要。固件打开的是芯片内部上拉(标称约 45kΩ),比数据手册口径弱一个数量级 | — |
| 屏(可选) | MPI7002,7″ 1024×600,HDMI 输入 | 上位机 APP 的大屏输出 | 接 PC / 树莓派,不是接 ESP32 |
| 软件 | 本机实测位置 | 说明 |
|---|---|---|
| ESP-IDF v5.5 | D:/Espressif/v5.5/esp-idf | 构建 S3 / C6 固件 |
| IDF 工具链 | C:/Espressif/tools | 由 eim 安装;IDF_TOOLS_PATH 指向这里 |
| Python(带 pyserial + tkinter) | C:/Espressif/tools/python/v5.5/venv/Scripts/python.exe | 必须用这个:系统 PATH 上的 python 没有 pyserial,会
ModuleNotFoundError: serial |
| Rust(可选) | cargo | 只在跑 Gateway 交叉验证时需要(第 7 节) |
下面所有命令都假定:仓库根目录为工作目录,且 python 指向上表那个 venv 解释器。
为省事可以先设一个别名:
# Git-Bash
alias py='C:/Espressif/tools/python/v5.5/venv/Scripts/python.exe'
# 之后本文所有 python 命令都写作 py
git clone http://gitlab.ptteng.com/openclaw-bot/elang-eos.git
cd elang-eos
git checkout mos/bootstrap
这两个是隐性知识,不设会以各种奇怪的方式失败:
export IDF_TOOLS_PATH=C:/Espressif
export ESP_ROM_ELF_DIR=C:/Espressif/tools/esp-rom-elfs/20241011
py verify.py --offline
期望结尾出现 ALL THREE LINES GREEN 且退出码 0。
若这里就红了,先别接硬件 —— 环境问题与硬件问题混在一起查,成本翻十倍。
0。若看到 3(SKIP/INCOMPLETE),
说明某项没跑(通常是缺编译器或缺依赖),而不是「过了」。
详细后果分析、面包板落位、逐线接错会怎样,见本站 《MOS 硬件接线指导》。这里只给最小连线表。
| MPU6050 | ESP32-S3 | 备注 |
|---|---|---|
| VCC | 3V3 | 只上 3.3V |
| GND | GND | 必须共地 |
| SDA | GPIO8 | 建议 4.7k 上拉到 3V3 |
| SCL | GPIO9 | 建议 4.7k 上拉到 3V3 |
| AD0 | GND | 决定地址:接 GND → 0x68;接 3V3 → 0x69 |
| AM2302 | ESP32-C6 | 备注 |
|---|---|---|
| VCC | 3V3 | 只上 3.3V;给 5V 会把数据线电平抬到 5V,可能打坏 C6 |
| GND | GND | 必须共地 |
| DATA | GPIO4 | 单总线,需上拉(强烈建议外挂 4.7k–10k) |
| NC | 悬空 | 第三脚不接 |
用本仓的 tools/idf_build.py,不要直接敲 idf.py:
ESP-IDF 会拒绝 MSYS/Mingw 环境(MSYSTEM 在每次启动进程时被重新注入,
在 bash 里 unset 是没用的)。该工具在 Python 里构造干净环境再启动 cmake,
把这条注入链切断。
py tools/idf_build.py --target esp32s3 --project firmware/esp_frame_tx
py tools/idf_build.py --target esp32c6 --project firmware/esp_thermo
构建目录分别是 firmware/esp_frame_tx/build/ 与
firmware/esp_thermo/build_esp32c6/。这条规则第 5 节还要用。
-Werror。典型坑是
printf("%u", x) 而 x 是 uint32_t(即
long unsigned)→ 格式串不匹配直接编译失败。修法是强转:
printf("%u", (unsigned)x)。
# S3(默认 target=esp32s3、project=firmware/esp_frame_tx)
py tools/idf_flash.py --port COM3
# C6 测温固件
py tools/idf_flash.py --port COM4 --target esp32c6 --project firmware/esp_thermo
先干跑一遍看它打算烧什么(不碰硬件):
py tools/idf_flash.py --port COM3 --dry-run
--target 不符 → 拒。
否则会「烧写成功、rc=0、[OK],但烧的是 20 分钟前那个目录里的旧产物」。--allow-stale 可越过,但要在结论里写明)。
「构建返回 0」不等于「bin 是新的」。0x0、分区表 0x8000、
app 0x10000。写错不会报错,只会启动空白。
工具只接受「从 build/ 里自己找」,不接受手填偏移。
退出码:0 成功;1 找不到产物/目标不符/产物过旧;
2 打开串口或烧写失败;3 未执行(缺 esptool)。
烧完先看板子自己说什么,再谈上位机。复位后抓一段串口:
py tools/serial_capture.py COM4 22 --reset --out c6.txt
#thermo frame-source=AM2302_LIVE ...
#thermo seq=0 temp=2780 hum=5260 ...
#thermo seq=1 temp=2790 hum=5270 ...
...
frame-source=AM2302_LIVE → 数据取自真实器件。这是唯一算「读通」的标记。frame-source=SYNTH → 器件没应答,固件如实声明在用合成值。这不丢人,
但不能当成元器件验证。#thermo 行 → 读不到就停发(本仓刻意不用合成值兜底)。#frame_tx frame-source=MPU6050_LIVE who=0x68 note=hum_c100 恒为 0(MPU6050 无湿度源)
who=0x68 → E 写的 I2C 时序逻辑从真实 MPU6050 读回了器件 ID。ts_ms 是名义时间。py tools/la_align_decode.py spec/evidence/la_i2c_mpu6050.txt
这会独立解码一份入库的双通道 RMT 波形,期望输出
0xD0 0x75 0xD1 0x68(MPU6050 的 WHO_AM_I),退出码 0。
它不依赖固件自报,是第三方视角的复核。
只想看温湿度,可以跳过本节直接做第 8 节。本节是「把真帧接进网关做交叉验证」的路径。
py tools/serial_frame_bridge.py --port COM3 --seconds 30 --out frames.bin
它做三层校验,逐层加强:L1 结构(magic 'ME' + ver==1)、
L2 自洽(帧内 CRC 与第三方 zlib.crc32 一致)、L3 内容(整帧 40 字节与
按 seq 独立算出的期望帧逐字节相同)。
cd gateway
cargo run -- file:../frames.bin --boot-grace 0
# 或实时监听:先 --serve-tcp 15040,再 cargo run -- tcp:127.0.0.1:9100
scan_expired_leases() 恒返回空 ——
那是「没有派发过」,不是「回收正常」。别拿它当验收依据。
测温应用 = tools/thermo_display.py。它只读 40 字节定长帧,按
firmware/frame-spec.md 的偏移表解析;CRC 不符即丢弃并重扫,绝不放行。
py tools/thermo_display.py --port COM4 --node am2302 --ui tk
py tools/thermo_display.py --port COM4 --node am2302 --ui tk --geom 1024x600+1920+0 --kiosk
--kiosk 在面板屏上必须加:
600 高的屏要减掉标题栏与任务栏,不加的话最下面两条会被压在任务栏底下。退出按 Esc。
py tools/thermo_display.py --port COM4 --node am2302 --ui none --seconds 10
py tools/thermo_display.py --port COM3 --node mpu6050 --ui tk
MPU6050 没有湿度传感器,UI 上湿度显示为
—(无源),不是 0.0。「没有来源」与「测得 0」是两件事。
| 参数 | 默认 | 说明 |
|---|---|---|
| --port | auto | 串口;auto 自动查找 |
| --node | am2302 | am2302(C6,温+湿) / mpu6050(S3,只有温度) |
| --baud | 115200 | 与固件 UART0 一致 |
| --ui | tk | tk=窗口,none=只看终端 |
| --geom | — | 窗口区域,如 1024x600+1920+0(缩放按该区域算) |
| --kiosk | off | 与 --geom 合用:去标题栏并置顶,正好铺满 |
| --seconds | 0 | 跑 N 秒后收尾并打印统计;0=一直跑 |
| --mqtt | — | 可选,上行到 MQTT broker 主机 |
| --selftest | — | 离线自检:不接硬件,跑协议解析 + 排版判据 |
py tools/thermo_display.py --selftest
py tools/thermo_display.py --ui-smoke
每一级都有可判定的结果。含糊的「看起来出来了」不算。
| 级别 | 看什么 | 成功 | 未执行(不是成功) |
|---|---|---|---|
| L1 环境 | verify.py --offline |
ALL THREE LINES GREEN,rc=0 | rc=3(有项 SKIP) |
| L2 构建 | idf_build.py |
rc=0 且产物 mtime 新于源码 | 用错目录(旧产物) |
| L3 烧录 | idf_flash.py |
rc=0,且工具未报 target 不符/产物过旧 | rc=3(缺 esptool) |
| L4 器件 | 串口 banner | frame-source=…_LIVE 且 seq 递增 |
只有 SYNTH,或完全没有帧行 |
| L5 链路 | thermo_display.py |
屏上有数且持续更新 | 大数字变灰显示 NO DATA |
| L6 第三方 | la_align_decode.py |
rc=0,解出 0xD0 0x75 0xD1 0x68 |
rc≠0 |
STALE_S 秒没有有效帧,
界面会把大字变灰并显示 NO DATA,不保留旧值假装还在更新。
看到 NO DATA 说明「链路断了」,而不是「界面坏了」。
| 现象 | 最可能的原因 | 怎么处置 |
|---|---|---|
ModuleNotFoundError: serial |
用了 PATH 上的系统 python(没装 pyserial) | 改用 IDF venv 那个解释器(第 1.2 节) |
MSys/Mingw is not supported |
直接敲了 idf.py |
改用 tools/idf_build.py |
| 烧完板子毫无反应、串口安静 | 偏移错 / 烧的是别的 target 的产物 | 跑 idf_flash.py --dry-run 看三个产物路径 |
| 界面显示 NO DATA | 串口没开对 / 板子没发帧 / 器件没接 | 先用 serial_capture.py 单独看板子说什么 |
| 字符串格式 warning 导致编译失败 | -Werror + %u 配 uint32_t |
强转 (unsigned) |
| 屏上有字但互相重叠 | 主机侧排版(字号单位:Tk 正数=点,会放大 4/3) | 跑 --selftest,它的几何判据会指出越界/相交 |
vTaskDelay 轮询;现版用 ESP32 硬件 GPTIMER 定时器中断,在 ISR 里直接调用 mos_sched_tick()。app_main 启动 GPTIMER 后 return,FreeRTOS 只剩空闲壳。firmware/mos_esp32/main/mos_esp32_port.c 不再 #include "freertos/...",无任何 FreeRTOS API 调用(门禁 T34 G6 源码级零依赖判据)。mos_drivers.c/.h,提供输出/输入配置、set/get、selfcheck,作为后续 ADC / PWM(LEDC) / SPI / I2C 驱动的接入骨架;真机自检 [PASS] GPIO2 set/readback consistent。完整记录见仓库 spec/evidence/mos_esp32_s3_full.txt。关键行:
=== MOS kernel bring-up on ESP32-S3 (FreeRTOS-INDEPENDENT) ===
arena bytes = 4096
... 六组测试 A-F ...
=== ALL PASS (failures=0) ===
I (293) main_task: Returned from app_main() # FreeRTOS 仅空闲壳
=== GPIO driver self-check (MOS drives real registers) ===
[PASS] GPIO2 set/readback consistent
# MAAAA/BBBB 轮转流从 tick0 起(GPTIMER ISR 驱动)
首版自检是 [FAIL] GPIO2 readback mismatch:ESP32-S3 的 GPIO_MODE_OUTPUT 是纯输出、输入缓冲被关,gpio_get_level() 读输入寄存器恒回 0。改成 GPIO_MODE_INPUT_OUTPUT(开输入缓冲,pad 电平反映驱动值)后复烧 → PASS。这说明自检不是绿灯表演,它挡住了一处"看起来能写、实际读不回"的假成功。
继 GPIO 之后,MOS 驱动框架接入第二个真机可验证驱动——I2C。自检在板载 MPU6050(SDA8/SCL9)上读取 WHO_AM_I 寄存器(地址 0x75),应回 0x68。这是对真实硅片的 探测,证明 MOS 真的把数据送上了 I2C 总线并与真实器件成功通信,而不是"看起来能写"。
mos_i2c_init(sda, scl, clk_hz) —— 配置 I2C 主机(ESP32-S3 旧版 master API,IDF v5.5 仍提供)。mos_i2c_write_reg(addr, reg, data, len) / mos_i2c_read_reg(addr, reg, buf, len) —— 寄存器级读写。mos_i2c_selfcheck(sda, scl) —— 探测 MPU6050,返回 WHO_AM_I==0x68。=== I2C driver self-check (probe MPU6050 on SDA8/SCL9) === [PASS] I2C MPU6050 WHO_AM_I=0x68 detected
MOS 驱动框架接入第三个真机可验证驱动——ADC。自检在 ADC1/CH4(GPIO5)上读 4 次原始值, 断言每次都落在 12 位合法区间 [0,4095]。这证明 MOS 真的驱动了 ADC 外设并完成一次转换。
mos_adc_selfcheck(void) —— 配置 ADC1 单元 + 通道,读 4 次原始值,验证均落 12 位合法区间。=== ADC driver self-check (ADC1 CH4/GPIO5) === [PASS] ADC raw reads in [0..4095] x4
MOS 驱动框架接入第四个真机可验证驱动——PWM(LEDC)。自检配置 LEDC 定时器+通道并设 50% 占空比;
ESP-IDF v5.5 无 ledc_get_duty,故以"timer+channel 配置 + set_duty/update_duty 均 ESP_OK"
证明 MOS 确实把参数写进了 LEDC 寄存器。
mos_pwm_selfcheck(pin) —— 配置 LEDC_TIMER_0(13 位、1kHz、LOW_SPEED)+ LEDC_CHANNEL_0 于 pin,设 50% 占空比。=== PWM(LEDC) driver self-check (GPIO18) === [PASS] LEDC timer+channel configured, 50% duty set
MOS 驱动框架接入第五个真机可验证驱动——SPI 主机。自检初始化 SPI2 总线+设备并发一帧哑数据; 以"bus init + device add + transmit 均 ESP_OK"证明 MOS 真的把数据送进了 SPI 状态机。
mos_spi_selfcheck(mosi, miso, scl) —— 初始化 SPI2 总线(mosi/miso/scl)+ 设备,发一帧哑数据。=== SPI master driver self-check (GPIO15/16/17) === [PASS] SPI bus+device init OK, dummy transmit ESP_OK