docs: 整理项目文档并统一界面表述
将项目说明按主题集中到 docs,补齐根目录 README 和 AGENTS,移除弃用方案并保留有效测试记录与未实现提案。同步文档索引、引用及改动日志生成路径,README 增加 GitHub Markdown 导航和 Mermaid 图。 - fix(client): 统一状态与操作文案,录制和截图成功后再显示完成提示 - test(client): 按文件名核对媒体类型,避免依赖时间戳排序 验证:23 份项目文档、144 处本地引用、27 条外链、2 张 Mermaid 图;前端构建和 12 项测试;截图与录制的 4 条异步反馈路径;Windows Rust 编译检查和后端测试。后端 40 项返回成功,其中 3 项 ffmpeg 集成用例因本机缺少工具而未实际执行。
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
# 装甲板(ESP32-C3 / Rust / ESP-IDF)—— 一主三从,与 RMIC::2026 桌面端联动
|
||||
|
||||
每车有前、后、左、右四块装甲板,只有主板含 MCU。其余三块从板各有一路 WS2812 和一路压电片,直接接线到主板。主板负责四路受击检测、四路灯带和整车血量,并向 RMIC::2026 桌面端广播。
|
||||
|
||||
固件使用 Rust + ESP-IDF,目标芯片为 ESP32-C3(RISC-V)。以下构建、烧录与探测命令从仓库根目录执行;主机测试按示例切换目录。
|
||||
|
||||
```text
|
||||
armor/
|
||||
├── Cargo.toml workspace:两个 crate,各自独立构建
|
||||
├── armor-protocol/ 规则层 + 线路格式(纯计算,主机上 22 个单元测试)
|
||||
├── armor-firmware/ 固件:ADC / RMT / NVS / 按键 / WiFi+UDP
|
||||
│ ├── src/leds.rs 四路 WS2812(单 RMT 通道轮流切 GPIO)
|
||||
│ ├── src/piezo.rs 四路压电片(40 kHz ADC 连续采样)
|
||||
│ ├── src/net.rs UDP 广播 ArmorHealth
|
||||
│ ├── src/settings.rs NVS:队伍与颜色
|
||||
│ └── sdkconfig.defaults / partitions.csv
|
||||
└── .cargo/config.toml riscv32imc-esp-espidf + MCU=esp32c3
|
||||
```
|
||||
|
||||
`armor-protocol` 负责受击判定、扣血、灯色和按键规则,可通过 `Duration` 模拟时间来测试。压电片尖峰难以按需复现,因此把规则与外设操作分开,固件侧负责连接外设。
|
||||
|
||||
## 接线
|
||||
|
||||
| 功能 | GPIO | 说明 |
|
||||
| --- | --- | --- |
|
||||
| WS2812 前 | IO10 | 每条灯带 3 颗灯珠,GRB、MSB first |
|
||||
| WS2812 后 | IO5 | |
|
||||
| WS2812 左 | IO6 | |
|
||||
| WS2812 右 | IO7 | |
|
||||
| 压电片 前 | GPIO0(ADC1_CH0) | 0 dB 衰减、12 bit |
|
||||
| 压电片 后 | GPIO4(ADC1_CH4) | |
|
||||
| 压电片 左 | GPIO3(ADC1_CH3) | |
|
||||
| 压电片 右 | GPIO1(ADC1_CH1) | |
|
||||
| BOOT 键 | GPIO9 | 低有效,内部上拉 |
|
||||
| 调试串口 UART0 TXD | GPIO21 | 115200 8N1,日志从这里出来(主控制台) |
|
||||
| 调试串口 UART0 RXD | GPIO20 | 目前空闲,留给以后的命令通道 |
|
||||
|
||||
引脚与参考实现一致(灯:前 10 / 后 5 / 左 6 / 右 7;压电:前 0 / 后 4 / 左 3 / 右 1)。UART0 作为主控制台,日志从丝印 RXD/TXD 对应的调试口输出。
|
||||
|
||||
`sdkconfig.defaults` 还将 USB Serial/JTAG 设为次控制台(`CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG=y`)。未连接 USB-TTL 时,也能从板上原生 USB 口读取应用日志;仅配置 UART0 则只能在原生 USB 上看到 ROM 启动信息。
|
||||
|
||||
## 规则
|
||||
|
||||
| | 值 | 在哪 |
|
||||
| --- | --- | --- |
|
||||
| 整车血量 | 100 | `Rules::max_hp` |
|
||||
| 每次受击扣血 | 3 | `Rules::damage_per_hit` |
|
||||
| 受击后变暗 | **只有被打的那一路**全黑 50 ms,其余三路不动 | `Rules::hit_blink` + `Armor::strip` |
|
||||
| 同一板位重复判定抑制 | 50 ms | `Rules::hit_cooldown` |
|
||||
| 受击阈值 | 原始 12 bit ADC > 2000(`ARMOR_THRESHOLD` 可改) | `Rules::threshold` |
|
||||
| 血量归零 | 四路全黑,直到复位 | `Armor::strip` |
|
||||
| 正常颜色 | 队伍一红(`0xFF0000`)、队伍二蓝(`0x0000FF`) | `Rgb::for_team` |
|
||||
| 复位(新一局) | 长按 BOOT ≥500 ms → 满血、清计数 | `Button` + `Armor::reset` |
|
||||
| 打印状态 | 短按 BOOT → 立刻打一行状态并重发上报 | `ButtonEvent::Click` |
|
||||
|
||||
**整车一条血量。** 板子算账、裁判端只显示:四条上报的 `hp` 相同,`index` 分别是 0..3,
|
||||
界面按 `(team, index)` 收齐四条后显示同一个数值。
|
||||
|
||||
参考实现使用全局 50 ms 冷却,一路受击会抑制其余路在该窗口内的击打。当前四路分别冷却,由 `cooldown_is_per_plate_not_global` 测试验证。
|
||||
|
||||
## 上报(UDP)
|
||||
|
||||
主板每 200 ms 广播四条 `protocol::ArmorHealth` 到 `255.255.255.255:5601`:
|
||||
|
||||
```text
|
||||
magic "MVC1"(4) | type(1)=5 | version(2) | sender_id(4) | ... | team(1) | index(1) | hp(2)
|
||||
```
|
||||
|
||||
- **血量一变立刻发**,没变就按 200 ms 心跳重发。裁判端靠心跳判断板子还在线,一次
|
||||
丢包不该让界面退回"未接入"。
|
||||
- `sender_id` 用 MAC 派生的板子 id(`protocol::id_from_mac`),不是 0 —— 0 是裁判
|
||||
终端的保留 id,抓包时一块装甲板发的包不该长得像裁判端发的。
|
||||
- 四条 `index` 的 `hp` 相同(整车一条血),裁判端按 `(team, index)` 收齐四条才显示。
|
||||
- 广播而不是单播:桌面端电脑的 IP 是 DHCP 给的,会变。图传板用 Hello 广播绕开这件事,
|
||||
装甲板沿用同一个约定。
|
||||
|
||||
### 桌面端怎么收
|
||||
|
||||
`desktop/core/src/armor.rs` 里的 `ArmorHealthStore`(两种身份都启动):
|
||||
|
||||
- 绑 `0.0.0.0:5601` 收广播,按 `(team, index)` 存最后一次读数。
|
||||
- **四条都在 1.5 s 内收到**才交给界面显示;否则回退占位符。心跳 200 ms,1.5 s 是连丢
|
||||
7 个包,偶发丢包不会让读数来回跳。
|
||||
- 用户端也要显示双方血条,所以两端都会占 5601。
|
||||
- 血量变化会打日志(`armor: board 0x… team 0 hp 100 -> 97`):窗口是给别人看的,
|
||||
抓日志的人也得能看见掉血。
|
||||
|
||||
**界面显示血量数字。** `TeamHealthDto` 带 `hp` 与 `critical`:数值就在队伍名旁边,低于
|
||||
30 或归零时它与血条一起转红。归零**不写"已击毁"**——那一刻图传中央会给出
|
||||
“胜利 / 落败”的结论(见 [桌面客户端](../desktop/client.md))。
|
||||
|
||||
## 构建
|
||||
|
||||
凭据与队伍号走 `.env`(与图传固件共用同一个文件):
|
||||
|
||||
```bash
|
||||
# .env
|
||||
WIFI_SSID=your-ssid
|
||||
WIFI_PASS=your-password
|
||||
ARMOR_TEAM=0 # 0 = 队伍一(红),1 = 队伍二(蓝)
|
||||
ARMOR_THRESHOLD=2000 # 受击阈值(原始 12 bit ADC),看日志里的 peak 来定
|
||||
|
||||
scripts/build_armor.sh # 编译
|
||||
ARMOR_TEAM=1 scripts/build_armor.sh # 临时当队伍二编译
|
||||
ARMOR_THRESHOLD=1200 scripts/build_armor.sh # 换受击阈值(看日志里的 peak 来定)
|
||||
```
|
||||
|
||||
产物:`armor/target/riscv32imc-esp-espidf/release/armor`。
|
||||
|
||||
`ARMOR_TEAM` 是**编译期**注入的:换队伍重刷固件即可,不用先擦 NVS。NVS 里另外记着
|
||||
`team` 与 `color`(命名空间 `armor`,与参考实现同名),两者不一致时以 `ARMOR_TEAM`
|
||||
为准并覆盖,日志里会说一声。
|
||||
|
||||
本机已有的 ESP-IDF 与工具链通过 `armor/.cargo/config.toml` 指定(`IDF_PATH` /
|
||||
`ESP_IDF_TOOLS_INSTALL_DIR`),不需要先 `activate` ESP-IDF。删掉那两行就交给
|
||||
esp-idf-sys 自己下载它钉住的版本。
|
||||
|
||||
### 用的是 ESP-IDF v6.0.1,不是图传板那套 v5.5.5
|
||||
|
||||
这台机器上有两套 ESP-IDF:
|
||||
|
||||
| | 路径 | 工具链 |
|
||||
| --- | --- | --- |
|
||||
| v6.0.1(eim 装的) | `/Users/peng/.espressif/v6.0.1/esp-idf` | Xtensa + **RISC-V(`esp-15.2.0_20251204`)** |
|
||||
| v5.5.5(图传板在用) | `/Users/peng/.espressif/esp-idf/v5.5.5` | 只有 Xtensa |
|
||||
|
||||
C3 是 **RISC-V**,而 v5.5.5 那套缺 `riscv32-esp-elf`(它要的是 `esp-14.2.0_20260121`,
|
||||
本机没有,而 GitHub release 又下不动)。所以装甲板固件指向 v6.0.1 —— 它的工具链齐全,
|
||||
且 `esp-idf-sys` 0.38.1 支持的版本范围是 4.4.4+、5.0–5.5、**6.0–6.2**。
|
||||
|
||||
两套 IDF 共存没有冲突:各自 `.cargo/config.toml` 里的 `IDF_PATH` 是每套 crate 自己钉的。
|
||||
|
||||
`ESP_IDF_TOOLS_INSTALL_DIR` 默认 `global`(即 `~/.espressif`,工具所在处),但**没有**加
|
||||
`force`:沙箱或 CI 里可以用 `ESP_IDF_TOOLS_INSTALL_DIR=custom:<可写目录>` 覆盖(工具目录
|
||||
不可写时把 `tools/` 软链到已装好的工具即可,Python venv 会建在那个可写目录里)。
|
||||
|
||||
### 主机侧测试
|
||||
|
||||
规则层在 PC 上跑,不需要板子:
|
||||
|
||||
```bash
|
||||
cd armor/armor-protocol && cargo test # 22 个测试
|
||||
cd protocol && cargo test # 线路格式往返(含 ArmorHealth)
|
||||
```
|
||||
|
||||
(`armor-protocol/.cargo/config.toml` 把 target 覆盖回 host:它坐在 `armor/` 目录树
|
||||
里,否则会继承 RISC-V 目标,链接必然失败。这与 `protocol/`、`desktop/` 的做法一致。
|
||||
换到其他宿主平台时,配置里钉住的 `aarch64-apple-darwin` 不适用,按
|
||||
[开发与验证](../development/firmware.md) 用 `--target <宿主三元组> --config 'unstable.build-std=[]'` 覆盖。)
|
||||
|
||||
## 烧录与实测
|
||||
|
||||
```bash
|
||||
scripts/flash_armor.sh # 自动找串口,烧完看 20 秒日志
|
||||
ESP_PORT=/dev/cu.usbmodem1101 scripts/flash_armor.sh
|
||||
scripts/flash_armor.sh --monitor 60 # 看 60 秒
|
||||
scripts/flash_armor.sh --no-monitor # 只烧不抓
|
||||
```
|
||||
|
||||
烧的是 ELF(`espflash` 自己从 ELF 里读分区表与 bootloader),芯片 `esp32c3`。
|
||||
|
||||
不打开裁判端界面,也可以单独确认板子在广播、血量对不对:
|
||||
|
||||
```bash
|
||||
python3 scripts/armor_probe.py 15
|
||||
```
|
||||
|
||||
它手写协议常量(不依赖 Rust 侧代码),可用于独立检查线路格式,发现固件编码与脚本解析不一致的情况。
|
||||
|
||||
### 串口日志长什么样(实机抓的)
|
||||
|
||||
```text
|
||||
I (321) armor: pins: ws2812 front=IO10 rear=IO5 left=IO6 right=IO7
|
||||
I (321) armor: pins: piezo front=GPIO0 rear=GPIO4 left=GPIO3 right=GPIO1
|
||||
I (4864) esp_netif_handlers: sta ip: 192.168.1.117, mask: 255.255.255.0, gw: 192.168.1.1
|
||||
I (5034) armor: joined "SR-602-2G4" on 192.168.1.117
|
||||
I (5034) armor::net: reporting to 255.255.255.255:5601 over UDP
|
||||
I (5034) armor::net: this board's id is 0xA820D321, from MAC [98, A3, 16, 4C, 67, C8]
|
||||
I (6044) armor: status: hp=100 hits=0 dead=false plates[front=0 rear=0 left=0 right=0]
|
||||
peak[front=490 rear=395 left=456 right=475] thr=2000
|
||||
strips[front=#FF0000 rear=#FF0000 left=#FF0000 right=#FF0000]
|
||||
ap=SR-602-2G4 rssi=-36 ch=6 ip=192.168.1.117 reports=5 errs=0
|
||||
I (63104) armor: hit plate=front raw=4095 hp=1 (hits=33)
|
||||
I (63314) armor: hit plate=front raw=4095 hp=0 (hits=34)
|
||||
W (63314) armor: hp reached 0: all four strips stay dark until the next reset
|
||||
W (306504) armor: BOOT long press: hp reset to 100 (new round)
|
||||
```
|
||||
|
||||
(日志里的长行是这里折行显示的,实机是一行。)
|
||||
|
||||
## 实测(2026-10-14,ESP32-C3 devkit,MAC `98:a3:16:4c:67:c8`)
|
||||
|
||||
| 项 | 结果 |
|
||||
| --- | --- |
|
||||
| 编译 | `cargo build --release` 通过;text 1,081,093 B,app 镜像 **1,082,848 B**,占 4 MB flash 的 26% |
|
||||
| 启动 | 无 panic/重启;状态行每秒一条 |
|
||||
| WiFi | 关联 `SR-602-2G4`(rssi −36,ch 6),DHCP 拿到 `192.168.1.117` |
|
||||
| 上报 | `scripts/armor_probe.py` 收到队伍一四条、约 5 包/秒、`sender_id 0xA820D321`、四格同值 |
|
||||
| 裁判端 | 旧客户端 `cam-client --role referee` 日志 `team 0 plate 0..3 is online (hp 100) via 192.168.1.117`,界面显示血量数字 |
|
||||
| 受击 | 敲前装甲板:`hit plate=front raw=2663..4095`,每次 −3(100→97→94…),只有 front 计数 |
|
||||
| 变暗 | 敲击瞬间**只有前那一路**全黑 50 ms,其余不变(现场确认) |
|
||||
| 死亡 | 34 击到 0;状态行 `dead=true strips[front=off rear=off left=off right=off]`,四路全黑 |
|
||||
| 复位 | BOOT 长按 → 板子 `reset to 100`,裁判端日志 `armor: team 0 hp 0 -> 100` |
|
||||
| 底噪 | 未敲击时四路 peak 约 380–510,阈值 2000 有明显余量 |
|
||||
| 未接线 | 后/左/右三路压电片与灯带本次未接(用户说明),代码按四路齐全写 |
|
||||
|
||||
### 旧客户端的血量显示问题
|
||||
|
||||
旧客户端已收到正确的 `hps`,但界面只显示绿/灰方块,没有绘制血量数值,归零后仍为绿色。修复后,队伍名旁显示血量数字,日志记录 `armor: board 0x… team 0 hp 100 -> 97`。
|
||||
|
||||
## 实现要点
|
||||
|
||||
### 1. ESP32-C3 的 RMT 只有 96 个 symbol 的通道内存
|
||||
|
||||
C3 的 RMT 只有一个 block,总计 96 个 symbol,按 48 为单位分给两个 TX 通道。`mem_block_symbols = 64`
|
||||
会被向上取整到 96,也就是一个通道吃光全部内存,第二个 `rmt_new_tx_channel()` 就会失败:
|
||||
|
||||
```text
|
||||
E rmt: rmt_tx_register_to_group(168): no free tx channels
|
||||
```
|
||||
|
||||
C3 只有 2 个 TX 通道,四路灯带无法各占一个。因此只创建一个通道,使用全部 96 个 symbol,再通过 `rmt_tx_switch_gpio()` 依次切换四个 GPIO。每帧为 3 珠 × 24 bit + reset,共 73 个 symbol,每路发送约 90 µs。
|
||||
|
||||
**为什么不用 `esp-idf-hal` 的 `TxChannelDriver`。** 它把 `rmt_channel_handle_t` 藏在
|
||||
结构体里(`as_ptr()` 是 `pub(crate)`),拿不到句柄就调不了 `rmt_tx_switch_gpio()`。
|
||||
所以 `leds.rs` 直接用 sys 绑定:通道、官方 copy encoder、transmit 都是 IDF 原生的那
|
||||
几个函数,用法与参考实现的 `led_strip_encoder.c` 一一对应。
|
||||
|
||||
参考实现每 10 ms 刷新四路同色灯带。这里四路颜色独立,只有内容变化时才执行 `disable → switch → enable → transmit`;每秒另做一次全量刷新,避免灯带因掉电、静电等丢帧后一直保持错误状态。
|
||||
|
||||
### 2. 压电片必须连续(DMA)采样
|
||||
|
||||
一次击打的波形只有几十微秒宽,oneshot 轮询容易错过尖峰,造成轻敲漏判。采样方式与参考实现一致:四通道合计 40 kHz,每通道 10 kHz;每帧为 10 ms、共 400 个采样点,主循环按帧处理。
|
||||
|
||||
`esp-idf-hal` 0.47 的 `adc::continuous` 多通道入口 `EmptyAdcChannels::chain` 的约束是
|
||||
`A: Adc` 且 `O: AdcChannels<AdcUnit = A>`:`ADC1` 满足前者、`ADCU1` 满足后者,同一个
|
||||
类型参数凑不齐,四路 ADC1 通道拼不出来。所以 `piezo.rs` 同样直接用 sys 绑定,与参考
|
||||
实现逐行对应。
|
||||
|
||||
采样点的通道号由硬件结果自带(`type2.channel()`),映射不依赖"结果按 pattern 顺序
|
||||
轮转"这个假设。
|
||||
|
||||
### 3. 一次击打会被算成几十次
|
||||
|
||||
40 kHz 采样时,一次击打可使连续几十个采样点超过阈值。`Armor::on_sample` 在规则层用冷却窗口合并为一次受击,主机测试 `repeat_samples_inside_the_cooldown_are_one_hit` 覆盖这一行为。
|
||||
|
||||
### 4. 控制台放在 UART0
|
||||
|
||||
`sdkconfig.defaults` 里显式写了 `CONFIG_ESP_CONSOLE_UART_DEFAULT=y`:C3 的 ROM 默认
|
||||
控制台就是 UART0,引脚正好是 RXD=GPIO20 / TXD=GPIO21,与要求一致。换成 USB
|
||||
Serial/JTAG 输出只要把这一行换成 `CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG`。
|
||||
|
||||
### 5. 应用分区给 2 MB
|
||||
|
||||
默认的 1 MB 单 app 槽对带 WiFi 的 Rust 固件太紧,`partitions.csv` 按参考实现给
|
||||
factory 划了 2 MB。
|
||||
|
||||
### 6. WiFi 省电关掉
|
||||
|
||||
省电模式可能让上报多等一个信标间隔,达到上百毫秒。装甲板使用车载供电,因此关闭省电以减少上报等待。主循环每 5 s 检查连接,断开后重连。
|
||||
|
||||
### 7. 判断"连上没有"要问 IDF,不要信 esp-idf-svc 的事件缓存
|
||||
|
||||
`BlockingWifi::is_connected()/is_up()/wait_netif_up()` 读的是 esp-idf-svc 自己维护的
|
||||
WiFi 事件状态。实测在 **IDF v6.0.1 + esp-idf-svc 0.53** 上它会停在"已连接":日志说
|
||||
一切正常、`get_ip_info()` 却是 `0.0.0.0`,而 IDF 的 `esp_wifi_sta_get_ap_info()` 明明
|
||||
说没关联。后果是 `join()` 假装连上了、看门狗永远不会重试,板子静默失联。
|
||||
|
||||
所以固件里一律用 IDF 的 API 判断:
|
||||
|
||||
- 关联:`esp_wifi_sta_get_ap_info()`
|
||||
- 地址:`esp_netif_get_handle_from_ifkey("WIFI_STA_DEF")` + `esp_netif_get_ip_info()`
|
||||
|
||||
`join()` 失败只记录警告,不退出,断网时仍保持本地检测与灯带功能。看门狗每 5 s 检查关联与有效 IP,不满足时断开重连;重连等待 IP 最多 3 s,避免长时间阻塞受击处理。
|
||||
|
||||
### 8. 日志里"有没有连上"要说清楚
|
||||
|
||||
状态行长这样:
|
||||
|
||||
```text
|
||||
status: hp=0 hits=37 dead=true plates[front=37 rear=0 left=0 right=0]
|
||||
peak[front=504 rear=379 left=459 right=486] thr=2000
|
||||
strips[front=off rear=off left=off right=off]
|
||||
ap=SR-602-2G4 rssi=-29 ch=6 ip=192.168.1.117 reports=1745 errs=0
|
||||
```
|
||||
|
||||
- `peak[...]` 是**这一秒内每路 ADC 的峰值**,用来校准阈值:没敲时的底噪要明显低于
|
||||
阈值,正常敲击要明显高于它(实测底噪 ~400–500,敲击 2000–4095)。只报"有没有过阈值"
|
||||
没法判断阈值该调到哪。
|
||||
- `thr=` 是当前阈值,`ARMOR_THRESHOLD=1200 scripts/build_armor.sh` 可以改。
|
||||
- `ap=` / `ip=` / `reports=` 用于区分未联网和已发送但对端未显示的情况。
|
||||
|
||||
### 9. USB 控制台看不到复位后的头两秒
|
||||
|
||||
主控制台是 UART0,另外把 USB Serial/JTAG 配成了次控制台,所以插原生 USB 也能看日志。
|
||||
但复位时 USB 会重新枚举,**复位后最初约 1–2 秒的日志会丢**(我们实测错过过启动横幅)。
|
||||
需要完整启动日志时,通过 UART0 连接 USB-TTL。
|
||||
|
||||
## 与参考实现(`reference/armor`)的差异
|
||||
|
||||
| | 参考实现 | 这里 | 为什么 |
|
||||
| --- | --- | --- | --- |
|
||||
| 主从链路 | ESP-NOW 配对,两块板对拷 | 无:三块从板只是直接接线的灯与传感器 | 按实际硬件(只有主板一颗 MCU) |
|
||||
| DBUS 透传 | UART0 收发 10 字节 DBUS,USB CDC 打 CSV | 不做 | 这套硬件没有 DBUS 需求;UART0 让给日志 |
|
||||
| 受击变暗 | 四条灯带一起全黑 50 ms | 只暗被打的那一路 | 每面独立灯带的意义就在这里 |
|
||||
| 冷却窗口 | 全局 50 ms | 每板独立 50 ms | 别块板的击打不该被吃掉 |
|
||||
| 灯带刷新 | 每 10 ms 无条件刷四遍 | 只在内容变化时刷 + 每秒强制全刷 | 四路独立控制后,省掉四轮 RMT 开销 |
|
||||
| 血量 | 只有击打计数,无血量 | 整车 100 血、每击 −3、0 血全黑,按 `ArmorHealth` 上报 | 与裁判端界面的血量显示对齐 |
|
||||
| 颜色切换 | BOOT 短按切红/蓝,存 NVS | 颜色由 `ARMOR_TEAM` 决定(红/蓝),短按打状态 | 队别不该靠现场按键切错 |
|
||||
| 按键 | 移植 OneButton 状态机(短按配对,长按切色) | 本地 FSM:长按复位血量、短按打状态 | 只有两种动作,且要能在主机上测 |
|
||||
| 上报 sender_id | —(不走 UDP) | MAC 派生 id | 0 是裁判终端保留 id |
|
||||
|
||||
## 还没做的
|
||||
|
||||
- **另外三路(后/左/右)的压电片与灯带还没接**(2026-10-14 实机只有前一路)。
|
||||
代码支持四路,接线后无需修改;四路同时受击与各路独立变暗仍未完成实板验证。
|
||||
- **UART0 RX(GPIO20)上的命令通道**:日志方向已经能用;反向的 `status` / `reset`
|
||||
命令还没接(当前复位走 BOOT 长按)。
|
||||
- **从板加 MCU 的方案**:如果以后从板也装单片机(走 UART 或无线回传),
|
||||
`armor-protocol` 里已经预留了板位枚举与上报格式,加一个 `slave` bin 即可。
|
||||
- **双击 / 多击**:`Button` 只实现短按与长按,参考实现里未使用的 OneButton 功能没有移植。
|
||||
- **下发血量**:当前板子本地算账(裁判端只显示)。若以后要裁判端权威(下发血量/复位),
|
||||
需要新增一条下行消息并在 `desktop` 里发。
|
||||
@@ -0,0 +1,77 @@
|
||||
# 图传板硬件说明
|
||||
|
||||
对应 Freenove ESP32-S3-WROOM CAM、ESP32-S3-WROOM-1-N16R8 与 OV5640。构建和烧录入口见 [固件入门](../setup/firmware.md)。
|
||||
|
||||
## 摄像头引脚
|
||||
|
||||
配置以 `src/camera.rs` 的 `pins` 模块为准。DATA 按 D0–D7(传感器 Y2–Y9)排列。
|
||||
|
||||
| 信号 | GPIO |
|
||||
| --- | --- |
|
||||
| RESET | 0 |
|
||||
| XCLK | 15 |
|
||||
| PCLK | 13 |
|
||||
| VSYNC | 6 |
|
||||
| HREF | 7 |
|
||||
| SIOD / SDA | 4 |
|
||||
| SIOC / SCL | 5 |
|
||||
| D0–D7 | 11, 9, 8, 10, 12, 18, 17, 16 |
|
||||
| PWDN | 未连接(`-1`) |
|
||||
|
||||
当前 XCLK 为 20 MHz。不要直接套用 AI-Thinker、XIAO 等其他板卡的相机引脚表。
|
||||
|
||||
本模组需避开的资源包括 Flash / PSRAM 总线(GPIO 26–37)、USB(19 / 20)、串口(43 / 44)及模组内部引脚;具体以原理图为最终依据。占用 Flash / PSRAM 引脚可能造成传输期间 CPU 挂死、串口静默。
|
||||
|
||||
## 内存与任务配置
|
||||
|
||||
参数位于根目录 `sdkconfig.defaults`:
|
||||
|
||||
| 参数 | 当前值 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `CONFIG_SPIRAM_MODE_OCT` | `y` | 8 MB Octal PSRAM |
|
||||
| `CONFIG_SPIRAM_SPEED_80M` | `y` | PSRAM 时钟 |
|
||||
| `CONFIG_CAMERA_PSRAM_DMA` | `n` | DMA 经内部 RAM 中转后复制到 PSRAM |
|
||||
| `CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP` | `n` | Wi-Fi / lwIP 缓冲保留在内部 RAM |
|
||||
| `CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240` | `y` | CPU 240 MHz |
|
||||
| `CONFIG_CAMERA_CORE1` | `y` | 显式选择 CPU1,与 Wi-Fi 分开(只禁用 CORE0 不足以改变 choice 默认值) |
|
||||
| `CONFIG_FREERTOS_HZ` | `1000` | 发送拥塞时以 1 ms tick 让出调度 |
|
||||
| `CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM` | `64` | 发送缓冲数量,使用内部 RAM |
|
||||
| `CONFIG_ESP_WIFI_TX_BA_WIN` | `32` | 保留 AMPDU 发送聚合 |
|
||||
|
||||
相机使用 3 个 PSRAM 帧缓冲及 `GRAB_LATEST`,优先取较新的帧。JPEG 编码由传感器完成,S3 主要负责采集和发送。
|
||||
|
||||
原测试板启用 PSRAM DMA 后曾出现 `NO-EOI`:JPEG 结束标记位于驱动末尾 DMA 搜索窗口之外,随后停止交付帧。因此默认关闭该配置;更换驱动后应单独验证持续采集。
|
||||
|
||||
## Flash 与分区
|
||||
|
||||
默认 **16 MB、DIO、40 MHz**,分区来自 `partitions.csv`:
|
||||
|
||||
| 分区 | 偏移 | 大小 |
|
||||
| --- | --- | --- |
|
||||
| NVS | `0x9000` | `0x6000` |
|
||||
| PHY init | `0xF000` | `0x1000` |
|
||||
| Factory app | 以 `partitions.csv` 为准,坏板重编时可能调整 | `0x120000`(1152 KiB) |
|
||||
|
||||
分区表位于 `0x8000`,S3 Bootloader 从 `0x0` 开始。App 写入分区表声明的偏移,不能直接套用 `0x10000`。修改分区或 Flash 模式后需重新构建,成套更新 Bootloader、分区表和 App。
|
||||
|
||||
健康硬件可以考虑把 App 偏移恢复到 `0x10000`,或改用更快的 Flash 配置,但需重新验证。
|
||||
|
||||
## 损坏 Flash 模组的兼容处理
|
||||
|
||||
早期测试模组在绝对地址 **`0xFADE7`** 存在写入异常,bit 5–7 无法正确写入。这是特定模组的故障。当前 App 偏移来自该板的开发过程,不能保证新镜像仍避开坏字节。
|
||||
|
||||
2026-10-18 的整段回读还发现 **`0x10863`** 的 bit 6 异常读回为 1:期望 `0x2e`,读回 `0x6e`,该轮 App 只有这一处字节不同,尚未单独擦写复核是否为永久故障。该模组的 App 偏移需至少为 `0x20000`,并继续检查 `0xFADE7`;烧录工具的校验失败必须先排除,不能当作网络或相机故障。
|
||||
|
||||
`scripts/flash.sh` 保留的流程是:编译、导出 App、搜索 64 KiB 对齐的可写偏移、修改分区、重新构建和验证,再写入三段镜像。重新构建可能移动镜像内容,因此第二次验证不可省略。
|
||||
|
||||
脚本依赖 macOS 串口名、Unix `termios`、本地 ESP-IDF Python 目录及旧版工具参数,不适合作为通用烧录入口。坏板需核对工具版本及坏地址字节;普通板卡使用 README 流程。更换故障模组比长期依赖镜像位置更可靠。
|
||||
|
||||
## 本地相机组件
|
||||
|
||||
`components/esp32-camera/` 包含上游 2.1.7 及本地兼容补丁,`components/cam_wrapper/` 提供 Rust FFI 封装。
|
||||
|
||||
本地增加 `esp_camera_fb_get_latest` / `cam_take_latest`:等待第一张有效帧后,以有限次数零等待取出更新帧,并归还替换的缓冲。`cam_take(0)` 表示单次非阻塞尝试,不在空队列、坏 JPEG 或 DMA 复位哨兵上继续等待。升级驱动时需保留或移植这些补丁,并运行缓冲所有权测试;不能用“先检查有帧、再调用可能阻塞 4 秒的 get”替代。
|
||||
|
||||
S3 的 `ll_cam.c` 中有 `CAM_IO_MUX_REG()` 本地补丁,用于 SCCB / I2C 引脚连接。升级驱动后需检查补丁并验证传感器探测和采集。
|
||||
|
||||
`components/esp32-camera/target/` 是按芯片划分的**源代码**,清理缓存时不能删除,也不能忽略所有层级的 `target/`。
|
||||
Reference in New Issue
Block a user