reference: 早先那版 C 写的装甲板固件(ESP-IDF 工程)

`armor/` 是它的 Rust 改写版,这一份留着当参考:ADC 阈值的 2000 就是从它标定出来的
(`.env.example` 里那句 "same as the reference firmware" 指的就是它),而且它的 README
记着实测设备与 MAC,换硬件时是有用的现场记录。

只收了 20 个源文件。它自带的 `.gitignore` 已经排掉 `build/`(211 MB 的 ESP-IDF 构建产物)
与生成的 `sdkconfig`,所以仓库里不会多出这些东西。

两处顺带处理:
- `reference/armor/.git` 是一个**空的**仓库壳(`git init` 过,零提交、零 remote、零 ref),
  而外层 git 正因为它的存在而拒绝索引 `reference/` 下的任何文件
  (`error: 'reference/armor/' does not have a commit checked out`)。它被挪到
  `desktop/.dev/backup/reference-armor-git`(可逆,且该目录已被忽略),不是删除。
- 它的 README 指着 `../armor-platformio-backup/`,那份备份并不存在。改成明说"没有进仓库",
  免得以后有人照着找一个不存在的目录。

另外把 `/docs/` 加进 .gitignore:那是工作区里另起的一个 Astro 文档站,与本仓库无关。
This commit is contained in:
2026-10-14 19:45:15 +08:00
parent a30eb3200a
commit fd71b83e3a
22 changed files with 1886 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
CompileFlags:
Remove: [-f*, -m*]
+106
View File
@@ -0,0 +1,106 @@
# =============================================================================
# armor — 文本/二进制归属与行尾(CRLF/LF)策略
# -----------------------------------------------------------------------------
# 规则:仓库里一律存 LF;工作区只在 Windows 原生脚本上使用 CRLF。
# 目的:
# * 避免不同机器(core.autocrlf 开着/关着)之间产生"整文件假改动";
# * 让 diff / blame / 合并只在真正改动的行上出现;
# * .ps1/.bat/.cmd 保持 CRLF,兼容 cmd.exe 与习惯 Windows 的工具链。
#
# 注意:.gitattributes 的 text/eol 属性优先级高于 core.autocrlf。
# 本文件用 "* text=auto eol=lf" 覆盖了所有路径,所以无论别人本机
# core.autocrlf 是 true / false / 未设置,入库结果都完全一致(已实测)。
# 若想改成"工作区也全用 LF"(含 .ps1),只需把第 1 节的
# eol=crlf 全部改成 eol=lf,其余规则不用动。
#
# 核对命令:
# git check-attr -a -- <路径> 查看某路径最终生效的属性
# git ls-files --eol 查看已入库文件的 i/(索引) w/(工作区) 行尾
# =============================================================================
# 默认:内容自动判定是否为文本;文本一律在索引与工作区使用 LF
* text=auto eol=lf
# =============================================================================
# 1. Windows 原生脚本 —— 工作区保持 CRLF
# =============================================================================
# cmd.exe 对纯 LF 的 .bat/.cmd 处理有坑(尤其是带 label/goto 的脚本),
# 必须 CRLF;.ps1/.psm1 按 Windows 惯例也保持 CRLF。
*.ps1 text eol=crlf
*.psm1 text eol=crlf
*.bat text eol=crlf
*.cmd text eol=crlf
# =============================================================================
# 2. 源码与文本配置 —— 显式声明为文本,工作区 LF
# =============================================================================
# 固件源码(main/ 下的 C/C++ 与官方 RMT 编码器)
*.c text eol=lf
*.h text eol=lf
*.cpp text eol=lf
*.hpp text eol=lf
*.cc text eol=lf
*.hh text eol=lf
# 构建系统
CMakeLists.txt text eol=lf
*.cmake text eol=lf
Kconfig text eol=lf
Kconfig.projbuild text eol=lf
# ESP-IDF 配置:sdkconfig 由工具生成(已忽略),defaults 是入库的那份
sdkconfig text eol=lf
sdkconfig.old text eol=lf
sdkconfig.defaults text eol=lf
sdkconfig.ci* text eol=lf
# 文档与数据
*.md text eol=lf
*.rst text eol=lf
*.txt text eol=lf
*.csv text eol=lf
*.json text eol=lf
*.yml text eol=lf
*.yaml text eol=lf
# 本仓库自身的元文件与工程级工具配置
.gitignore text eol=lf
.gitattributes text eol=lf
.clangd text eol=lf
# =============================================================================
# 3. 二进制 —— 禁止任何行尾转换与文本合并
# =============================================================================
# 固件/构建产物(已被 .gitignore 排除,这里再兜一层,防止被强制入库时被破坏)
*.bin binary
*.elf binary
*.hex binary
*.a binary
*.o binary
# 图片与压缩包
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.bmp binary
*.svg binary
*.zip binary
*.gz binary
*.tar binary
*.7z binary
# 密钥与证书:绝不能因为行尾转换而改变字节
*.pem binary
*.key binary
*.der binary
*.crt binary
*.p12 binary
*.pfx binary
+209
View File
@@ -0,0 +1,209 @@
# =============================================================================
# armor — ESP32-C3 / ESP-IDF v6.1 工程专用 .gitignore
# -----------------------------------------------------------------------------
# 取舍原则
# 1) 只忽略「本机生成物 + 本机专有配置 + 编辑器/系统杂物」;
# 源码、构建脚本、分区表、可复现配置一律入库。
# 2) 能用 "/" 锚定就用锚定模式,避免将来新增同名目录时被误伤。
# 3) 每段都写明理由,后人可据此判断能否删改。
# 4) 语法提醒:.gitignore 的 "#" 注释必须独占一行,不能写在模式后面;
# 行尾空格会被静默丢弃(或被当成模式的一部分),也不要加。
#
# 自检
# git status --short # 期望只列出应该入库的源文件
# git check-ignore -v <路径> # 查看某路径被哪条规则命中
# =============================================================================
# =============================================================================
# 1. ESP-IDF 构建输出(体积最大,本工作区约 17 MB,必须忽略)
# =============================================================================
# idf.py 默认在工程根目录生成 build/:CMake/Ninja 缓存、.o、静态库、固件
# 镜像、flasher_args.json、compile_commands.json 全在里面。
# 本工程路径是纯 ASCII,不会触发 idf.ps1 的「非 ASCII 路径 -> 外置构建目录」
# 逻辑,所以 build/ 就落在工程内。
/build/
# 其它常见构建目录命名:多配置构建、CMake 预设、CLion
/build-*/
/cmake-build-*/
# 仅当 IDF_BUILD_DIR 被指向工程内部时才会出现
# (idf.ps1 的默认兜底是 <盘符>:\esp-idf-build\armor,位于仓库之外)
/esp-idf-build/
# clangd --background-index 的索引缓存(.vscode 里启用了该选项)
/.cache/
# 根目录下零散的 CMake/Ninja 缓存(正常应在 build/ 内,保险起见)
/.ninja_deps
/.ninja_log
/CMakeCache.txt
/CMakeFiles/
# =============================================================================
# 2. ESP-IDF 生成的本机配置
# =============================================================================
# sdkconfig : menuconfig/构建时自动写出,代表「本机当前状态」,每台机器都可能不同
# sdkconfig.old : 上一次配置的备份
# 可复现的那份是 sdkconfig.defaults(已生成,见第 9 节),所以这两个都忽略。
/sdkconfig
/sdkconfig.old
# 提示:ESP-IDF 工具在 Windows 上写 sdkconfig / sdkconfig.defaults 用的是 CRLF,
# 这是工具行为、无需干预 —— 入库行尾由 .gitattributes 统一成 LF,
# 本地暂时出现 CRLF 不会让 git status 变脏(已实测)。
# =============================================================================
# 3. IDF Component Manager(引入 idf_component.yml 后才会出现)
# =============================================================================
# managed_components/ 是下载到本地的第三方组件副本,官方文档明确说它
# 「通常不纳入版本控制」;dependencies.lock 由工具自动求解生成、禁止手改。
# 若团队要锁定依赖版本,可注释掉 dependencies.lock 并把它提交入库。
/managed_components/
/dependencies.lock
# 工具导出的编译数据库副本(真正那份在 build/ 里,已随 build/ 忽略)
/compile_commands.json
# =============================================================================
# 4. 固件产物与运行日志
# =============================================================================
# 只锚定根目录,避免误伤将来放进 assets/ 之类的素材
/*.bin
/*.elf
/*.map
/*.hex
# README 里 monitor 打印的 CSV,属于采集到的运行数据,不入库
/logs/
*.log
# =============================================================================
# 5. 密钥类文件 —— 提交即泄密,且很难从历史里挽回
# =============================================================================
# 安全启动签名私钥 / Flash 加密密钥 / 证书私钥。
# 若仓库确实需要存放公开证书,请把下面的通配收窄成具体文件名。
*.pem
*.key
*.p12
*.pfx
# =============================================================================
# 6. 编辑器 / IDE(本机专有,往往含绝对路径)
# =============================================================================
# VS Code + ESP-IDF 扩展自动生成。当前 .vscode/settings.json 里是
# C:\esp\v6.1\esp-idf、d:\宣筹\...\build、COM10 这类本机绝对路径,
# 提交上去只会误导别人。
/.vscode/*
# 想共享「推荐扩展列表」时,取消下一行注释(上一行的 /.vscode/* 必须保留)
!/.vscode/extensions.json
/.idea/
/.vs/
*.code-workspace
*.sublime-project
*.sublime-workspace
# 编辑器临时/交换文件
*.swp
*.swo
*~
.#*
\#*#
# =============================================================================
# 7. Python 与测试/覆盖率工具缓存(ESP-IDF 工具链脚本产物)
# =============================================================================
__pycache__/
*.py[cod]
.venv/
venv/
/.mypy_cache/
/.pytest_cache/
/.ruff_cache/
# pytest-embedded、单元测试与尺寸报告(ESP-IDF 自身仓库也忽略这些)
pytest-embedded/
pytest_embedded_log/
TEST_LOGS/
XUNIT_RESULT*.xml
build_summary_*.xml
app_info_*.txt
size_info*.txt
.manifest_sha
# 覆盖率
*.gcda
*.gcno
*.gcov
coverage.info
coverage_report/
# =============================================================================
# 8. 操作系统杂物
# =============================================================================
Thumbs.db
ehthumbs.db
desktop.ini
$RECYCLE.BIN/
.DS_Store
.AppleDouble
._*
*.lnk
# =============================================================================
# 9. 必须入库的文件(切勿加入忽略列表)
# =============================================================================
# CMakeLists.txt / main/CMakeLists.txt
# main/*.cpp main/*.hpp main/*.c main/*.h
# idf.ps1 构建入口(含非 ASCII 路径 workaround)
# partitions.csv 自定义分区表
# README.md 工程文档(移植要点与踩坑记录)
# .clangd 工程级 clangd 配置
# sdkconfig.defaults 可复现配置来源(由 idf.py save-defconfig 生成)
# .gitattributes 行尾(CRLF/LF)与文本/二进制归属策略
#
# 提醒:ESP-IDF 自身仓库的 .gitignore 会忽略 .clangd 和 .vscode/;
# 本工程故意保留 .clangd(它是为 clangd 去掉 -f*/-m* 的项目配置),
# 不要照抄 IDF 仓库的规则。
# =============================================================================
# 10. sdkconfig.defaults 与 sdkconfig 的关系(2026-10-14 核对并修复)
# =============================================================================
# 忽略 sdkconfig 的前提是 sdkconfig.defaults 存在且正确。核对时发现:
# * README「目录结构」里列的 sdkconfig.defaults 当时并不存在;
# * 当时的 sdkconfig(以及 build/config/sdkconfig.h)是
# CONFIG_PARTITION_TABLE_SINGLE_APP=y、CONFIG_ESPTOOLPY_FLASHSIZE_2MB=y,
# 即用的是默认 single-app 分区表,partitions.csv 根本没有生效;
# * 而 README「实测验证结果」的 boot log 却是 4 MB flash + 2 MB factory 分区。
# 已按文档修正(4 MB flash + CONFIG_PARTITION_TABLE_CUSTOM=y)重新配置,
# 并用 `idf.py save-defconfig` 生成 sdkconfig.defaults,内容就是最小差异:
# CONFIG_IDF_TARGET="esp32c3"
# CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
# CONFIG_PARTITION_TABLE_CUSTOM=y
# (CONFIG_PARTITION_TABLE_CUSTOM_FILENAME 的 Kconfig 默认值本来就是
# "partitions.csv",所以 save-defconfig 不会重复写入。)
#
# 维护规则:
# * 改配置请改 sdkconfig.defaults(或 menuconfig 后重跑 save-defconfig),
# 不要把 sdkconfig 提交进来;
# * 想确认「相对 IDF 默认值改了什么」:重跑 idf.py save-defconfig 再看 diff;
# * 构建日志里分区表应为 nvs 0x9000 / phy_init 0xf000 / factory 0x10000 2M,
# 否则说明 defaults 没生效或 partitions.csv 没被采用。
# =============================================================================
+8
View File
@@ -0,0 +1,8 @@
# ESP-IDF project for the "armor" hit-detection node (ESP32-C3).
#
# Converted from a PlatformIO / Arduino project; see README.md for the mapping
# between the original Arduino APIs and the ESP-IDF ones used here.
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(armor)
+205
View File
@@ -0,0 +1,205 @@
# armor (ESP32-C3) — ESP-IDF 工程
本目录原为一个 PlatformIO / Arduino 工程,现已改造为 ESP-IDF 工程。
原始 PlatformIO 工程曾备份在同级的 `../armor-platformio-backup/`——**那份备份没有进仓库**,
这里的 `reference/armor/` 只保留改造后的 ESP-IDF 工程。
- 目标芯片:**ESP32-C3**(实测设备 `ESP32-C3 (QFN32) rev v0.4`,USB-Serial/JTAG,MAC `98:a3:16:4c:67:c8`)
- ESP-IDF 版本:**v6.1.0**
- 状态:**已构建并烧录成功,实测稳定运行**
## 目录结构
```
armor/
├── CMakeLists.txt # ESP-IDF 顶层工程文件
├── sdkconfig.defaults # 默认配置(替代 platformio.ini 的 board/build_flags)
│ # 由 idf.py save-defconfig 生成:IDF_TARGET / 4MB flash / 自定义分区表
├── partitions.csv # 自定义分区表(2 MB factory 应用分区)
├── idf.ps1 # 便捷脚本:自动激活 ESP-IDF 后转发给 idf.py
├── .gitignore # 忽略 build/、sdkconfig、IDE 与本机生成物
├── .gitattributes # 行尾策略:仓库统一 LF,.ps1/.bat/.cmd 在工作区用 CRLF
└── main/
├── CMakeLists.txt
├── armor_main.cpp # app_main:NVS / 串口 / 按键 / 业务任务(原 main.cpp)
├── air.hpp / air.cpp # ESP-NOW 对拷链路
├── hit.hpp / hit.cpp # 压电片击打检测 + WS2812
├── button.hpp/.cpp # OneButton 2.6.1 状态机移植
├── storage.hpp/.cpp # NVS 封装(替代 Arduino Preferences)
├── armor_time.hpp # millis()
└── led_strip_encoder.* # 官方 WS2812 RMT 编码器(取自 IDF 示例)
```
## 与 PlatformIO 工程的对应关系
| PlatformIO / Arduino | ESP-IDF |
| --- | --- |
| `platform = espressif32`, `framework = arduino` | ESP-IDF v6.1 原生工程 |
| `board = esp32-c3-devkitm-1` | `CONFIG_IDF_TARGET="esp32c3"` |
| `lib_deps = Adafruit NeoPixel` | `led_strip_encoder.c` + RMT TX 驱动 |
| `lib_deps = OneButton` | `main/button.cpp`(逐行移植其状态机) |
| `Preferences` | `storage.cpp`(NVS) |
| `ARDUINO_USB_CDC_ON_BOOT=1`(`Serial`) | USB Serial/JTAG 控制台 + `usb_serial_jtag_read_bytes()` |
| `Serial0`(GPIO20/21) | `uart_driver_install(UART_NUM_0)`,TX=21 / RX=20 |
| `setup()` / `loop()` / `loop2()` | `app_main()` / `hit_task` / `loop2_task` |
| `gen_build.cmd`, `upload.cmd` | `idf.ps1`(见下) |
## 移植要点
### 1. ADC:旧驱动在新版 IDF 中已被移除
原代码使用 `driver/adc.h` 的 legacy DMA 接口(`adc_digi_initialize` /
`adc_digi_controller_configure` / `adc_digi_read_bytes`),该驱动在 ESP-IDF v6
中已删除。现改用 `esp_adc/adc_continuous.h`,采样参数保持一致:
总采样率 40 kHz、4 通道、12 bit、`ADC_ATTEN_DB_0`,每帧 10 ms(1600 字节)。
解析方式仍是直接读取 `adc_digi_output_data_t::type2`(C3 的 12 bit 布局),
因此击打判定阈值 `HIT_THRESHOLD = 2000` 的语义不变。
### 2. WS2812:软件位翻转 → RMT 硬件驱动
Adafruit_NeoPixel 是软件位翻转驱动,这里换成 RMT + 官方 `led_strip_encoder`。
字节序保持 Adafruit 的 `NEO_GRB`(GRB + MSB first)。
**ESP32-C3 的 RMT 资源限制(踩坑记录)**:C3 的 RMT 只有一个 block,总计
**96 个 symbol 的通道内存**,按 `SOC_RMT_MEM_WORDS_PER_CHANNEL = 48` 为单位分配给
两个 TX 通道。因此:
- `mem_block_symbols = 64` 会被向上取整到 96,即**一个通道吃光全部内存**,
导致第二个 `rmt_new_tx_channel()` 失败:
`E rmt: rmt_tx_register_to_group(168): no free tx channels` →
`ESP_ERROR_CHECK` 触发 `abort()` → 芯片反复重启。
- 4 条灯带在固件中始终显示同一颜色,所以这里只用**一个** RMT TX 通道,
令其独占全部 96 symbol(足够一次装下 3 颗灯珠的整帧:3×24 bit + reset = 73
symbol),再用 `rmt_tx_switch_gpio()` 依次把该通道切到 4 个 GPIO 上发送。
每条灯带约 140 µs,10 ms 周期内开销可忽略。
### 3. 按键
按 OneButton 2.6.1 的 `_fsm()` / `debounce()` 逐行移植,默认值与调用方式
(`attachClick` / `attachLongPressStart` / `setClickMs(50)` / `setPressMs(500)` /
每 10 ms `tick()`)完全一致。未使用的双击、多击、idle 等功能没有移植。
### 4. 控制台
控制台走 USB Serial/JTAG,等价于原来的 `ARDUINO_USB_CDC_ON_BOOT=1`,
同时让 UART0(GPIO20/21)腾出来给 DBUS 数据,与原 `Serial0` 一致。
USB CDC 的 10 字节 DBUS 帧由独立的 `cdc_task` 接收。
### 5. NVS
命名空间仍是 `armor`,键名 `color`、`peer_mac` 保持原样。
### 与原始固件的差异(有意为之)
- `Air::onRecv()` 现在直接用 `esp_now_recv_info_t::rx_ctrl->rssi` 记录信号强度。
原代码依赖混杂模式回调“恰好先于” ESP-NOW 回调执行,存在竞态;新写法在配对
比较时能拿到当前包的准确 RSSI。
- `my_packet.dbus` 的写入/清零加了临界区保护(原代码在 CDC 回调与业务线程之间
无保护)。
- ESP-NOW 负载长度由 `alignas(1)` 改为 `__attribute__((packed))`,
两者都让 `sizeof(packet_t) == 19`,但后者是可移植写法(C++ 标准不允许
`alignas` 比成员自然对齐更宽松)。
- `hit_cnt++` 改为 `hit_cnt = hit_cnt + 1`:C++20 起对 `volatile` 使用 `++` 已废弃,
而 IDF 默认开启 `-Werror`。
- 分区表使用 2 MB 的 factory 分区(默认的 1 MB 对 WiFi + ESP-NOW 固件偏紧;
当前固件 0xcfbc0 ≈ 852 KB)。
其余业务逻辑(颜色切换、配对流程、串口协议、超时清零)与原实现逐条对应。
## ⚠️ 非 ASCII 路径问题(本工程必须注意)
本工程所在路径包含中文(`D:\宣筹\校内赛\...`)。**ESP-IDF v6.1 在 Windows 上无法
在非 ASCII 路径下编译**,原因是 `components/esp_libc/project_include.cmake` 会把
编译器的 `-specs=<build>/specs/picolibc.specs` 参数写进
`build/toolchain/cflags`,而读写该文件用的 `tools/cmake/toolchain_flags.cmake`
走的是 CMake `file(STRINGS)`;未指定 `ENCODING` 时它按 ASCII 解析,会**静默丢弃
所有非 ASCII 字节**,于是 `-specs=` 指向的文件名被破坏,每个编译单元都报:
```
riscv32-esp-elf-gcc.exe: fatal error: cannot read spec file
'D:/\x0a/\x0a/2026/SmallMaster-master/code/armor/build/specs/picolibc.specs': Invalid argument
```
实测字节级证据(`file(STRINGS)` 往返):
| | 字节 |
| --- | --- |
| 输入 `D:/宣筹/校内赛/...` | `... 2F, E5,AE,A3, E7,AD,B9, 2F, E6,A0,A1, E5,86,85, E8,B5,9B, 2F ...` |
| `file(READ)` 直读(正常) | 同上 |
| `file(STRINGS)` 往返后 | `... 2F, 0D,0A, 2F, 0D,0A, 2F ...` |
**解决办法**:把 build 目录放到纯 ASCII 路径下(源码可以留在中文路径),
即 `idf.py -B <ASCII 目录>`。本仓库的 `idf.ps1` 已自动处理:
```
检测到工程路径含非 ASCII 字符时,自动改用 <盘符>:\esp-idf-build\armor 作为 build 目录
```
可用环境变量 `IDF_BUILD_DIR` 覆盖该位置;若想改回在工程内构建,
把工程移到纯 ASCII 路径即可(此时脚本不会加 `-B`)。
> 注意:用目录联接(junction)指向中文路径**无效**——PowerShell 的
> `Set-Location` 会把联接解析回真实路径,CMake 仍然看到中文路径。
## 构建与烧录
```powershell
# 构建(自动处理非 ASCII 路径)
.\idf.ps1 build
# 烧录
.\idf.ps1 -p COM9 flash
# 烧录并监视(Ctrl+] 退出)
.\idf.ps1 -p COM9 flash monitor
# 擦除整片 flash
.\idf.ps1 erase-flash
```
`idf.ps1` 会在需要时通过 `eim_idf.json` 自动找到并激活本机的 ESP-IDF。
如果你已经激活过 ESP-IDF(`idf.py` 可用),也可以直接:
```powershell
idf.py -B <ASCII 路径> build
idf.py -B <ASCII 路径> -p COM9 flash monitor
```
串口参数与原先一致:UART0 = 115200 8N1(DBUS),USB CDC 输出
`color,hit_cnt,peer_rssi,my_rssi,latency_ms` 的 CSV 行。
## 实测验证结果(COM9,ESP32-C3 rev v0.4)
烧录后 boot log:
```
I (24) boot: ESP-IDF v6.1 2nd stage bootloader
I (25) boot.esp32c3: SPI Speed : 80MHz / Mode : DIO / Flash Size : 4MB
I (26) boot: 2 factory factory app 00 00 00010000 00200000
I (175) boot: Loaded app from partition at offset 0x10000
I (185) app_init: Project name: armor
I (185) app_init: ESP-IDF: v6.1
I (190) main_task: Calling app_main()
I (245) wifi:mode : sta (98:a3:16:4c:67:c8)
I (247) wifi:(2.4G)set country: cc=JP schan=1 nchan=14 policy=1
I (250) wifi:ic_enable_sniffer <- 混杂模式
I (250) ESPNOW: espnow [version: 2.0] init
I (251) armor: armor node started
I (252) main_task: Returned from app_main()
16711680,0,0,0,65
16711680,0,0,0,75
...
```
- 无重启、无 panic、无 abort;连续 6 秒稳定输出 555 行 CSV(约 11 ms 一条,符合 10 ms 循环)。
- `color = 16711680 = 0xFF0000`,即 NVS 为空时的默认红色,与 `Hit::RED` 一致。
- WiFi 国家码 `JP`、信道 1–14、混杂模式、ESP-NOW v2.0 全部按原逻辑生效。
## 硬件引脚(未改动)
| 功能 | GPIO |
| --- | --- |
| 按键(BOOT,低有效) | 9 |
| WS2812 前 / 后 / 左 / 右 | 10 / 5 / 6 / 7 |
| 压电片 ADC 前 / 后 / 左 / 右(ADC1_CH0/4/3/1) | 0 / 4 / 3 / 1 |
| DBUS UART0 TX / RX | 21 / 20 |
+83
View File
@@ -0,0 +1,83 @@
<#
.SYNOPSIS
Run idf.py for this project with a local ESP-IDF environment activated.
.DESCRIPTION
Activates ESP-IDF (located through the Espressif Installation Manager's
eim_idf.json, unless IDF_PATH is already set) and forwards every remaining
argument to idf.py.
Non-ASCII project path workaround
---------------------------------
ESP-IDF v6.1 rewrites the compiler's "-specs=..." flag from
components/esp_libc/project_include.cmake through CMake's file(STRINGS),
which silently drops every non-ASCII byte. When this project lives under a
path containing non-ASCII characters (e.g. "D:\宣筹\..."), the rewritten
flag points at a nonexistent file and every compile fails with:
riscv32-esp-elf-gcc.exe: fatal error: cannot read spec file ...
This script detects that case and builds out-of-tree into an ASCII build
directory instead, which keeps the flag (and every generated path) ASCII.
Override the location with the IDF_BUILD_DIR environment variable.
.EXAMPLE
.\idf.ps1 build
.\idf.ps1 menuconfig
.\idf.ps1 -p COM9 flash monitor
.\idf.ps1 erase-flash
#>
# NOTE: deliberately no param() block. An advanced/declared parameter list makes
# PowerShell try to bind idf.py's own short options to its common parameters
# ("-p" is ambiguous with -ProgressAction / -PipelineVariable). A simple script
# passes every unbound token through $args verbatim.
$IdfArgs = @($args)
$ErrorActionPreference = 'Stop'
function Find-IdfActivation {
$candidates = @(
(Join-Path $env:USERPROFILE '.espressif\eim_idf.json'),
'C:\Espressif\tools\eim_idf.json',
(Join-Path $env:LOCALAPPDATA 'Espressif\eim_idf.json')
)
foreach ($path in $candidates) {
if (-not (Test-Path $path)) { continue }
$cfg = Get-Content $path -Raw | ConvertFrom-Json
$installed = $cfg.idfInstalled | Where-Object { $_.id -eq $cfg.idfSelectedId } | Select-Object -First 1
if (-not $installed) { $installed = $cfg.idfInstalled | Select-Object -First 1 }
if ($installed -and (Test-Path $installed.activationScript)) {
return $installed.activationScript
}
}
return $null
}
if (-not $env:IDF_PATH) {
$activation = Find-IdfActivation
if (-not $activation) {
throw 'ESP-IDF not found. Activate your ESP-IDF environment first (export.ps1 / eim).'
}
. $activation *> $null
}
if (-not $IdfArgs -or $IdfArgs.Count -eq 0) { $IdfArgs = @('build') }
# --- non-ASCII project path workaround -------------------------------------
$projectDir = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path }
$userPickedBuildDir = $IdfArgs -contains '-B' -or $IdfArgs -contains '--build-dir'
if ($projectDir -notmatch '^[\x20-\x7E]+$' -and -not $userPickedBuildDir) {
if ($env:IDF_BUILD_DIR) {
$buildDir = $env:IDF_BUILD_DIR
} else {
$qualifier = Split-Path -Qualifier $projectDir # e.g. "D:"
$buildDir = Join-Path $qualifier 'esp-idf-build\armor'
}
Write-Host "Non-ASCII project path detected; building out-of-tree in '$buildDir'." -ForegroundColor Yellow
Write-Host 'Set IDF_BUILD_DIR to use a different build directory.' -ForegroundColor Yellow
$IdfArgs = @('-B', $buildDir) + $IdfArgs
}
& idf.py @IdfArgs
exit $LASTEXITCODE
+21
View File
@@ -0,0 +1,21 @@
idf_component_register(
SRCS
"armor_main.cpp"
"air.cpp"
"hit.cpp"
"button.cpp"
"storage.cpp"
"led_strip_encoder.c"
INCLUDE_DIRS "."
REQUIRES
esp_wifi
esp_netif
esp_event
esp_timer
nvs_flash
esp_adc
esp_driver_gpio
esp_driver_rmt
esp_driver_uart
esp_driver_usb_serial_jtag
)
+222
View File
@@ -0,0 +1,222 @@
#include "air.hpp"
#include <cstring>
#include "armor_time.hpp"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "storage.hpp"
namespace {
constexpr const char *TAG = "air";
portMUX_TYPE s_dbus_lock = portMUX_INITIALIZER_UNLOCKED;
// ESP-NOW payloads are 802.11 frames; the promiscuous callback is used to keep
// track of the peer's signal strength.
typedef struct {
unsigned frame_ctrl : 16;
unsigned duration_id : 16;
uint8_t addr1[6]; /* receiver address */
uint8_t addr2[6]; /* sender address */
uint8_t addr3[6]; /* filtering address */
unsigned sequence_ctrl : 16;
uint8_t addr4[6]; /* optional */
} wifi_ieee80211_mac_hdr_t;
typedef struct {
wifi_ieee80211_mac_hdr_t hdr;
uint8_t payload[0]; /* network data ended with 4 bytes csum (CRC32) */
} wifi_ieee80211_packet_t;
} // namespace
// public
Air::packet_t Air::my_packet, Air::peer_packet;
volatile uint32_t Air::last_air_ms = 0;
volatile bool Air::is_pairing = false;
volatile bool Air::pairing_found = false;
// private
const uint8_t Air::empty_mac[6] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
const uint8_t Air::pairing_data[4] = {0x11, 0x45, 0x14, 0x00};
esp_now_peer_info_t Air::broadcast_peer;
esp_now_peer_info_t Air::peer;
volatile int8_t Air::pairing_best_rssi = -128;
void Air::begin() {
// ESP-NOW needs the WiFi stack up on the station interface.
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
wifi_init_config_t wifi_cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&wifi_cfg));
// The peer MAC lives in our own NVS namespace; don't let the WiFi driver
// write extra calibration data it does not need here.
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_start());
// 使用 WiFi 14 信道
wifi_country_t country = {};
memcpy(country.cc, "JP", sizeof(country.cc));
country.schan = 1;
country.nchan = 14;
country.policy = WIFI_COUNTRY_POLICY_MANUAL;
ESP_ERROR_CHECK(esp_wifi_set_country(&country));
ESP_ERROR_CHECK(esp_wifi_set_channel(14, WIFI_SECOND_CHAN_NONE));
// 20dBm 功率(84 = 21dBm;C3 上限为 20dBm,失败时退回 80)
esp_err_t power_err = esp_wifi_set_max_tx_power(84);
if (power_err != ESP_OK) {
ESP_LOGW(TAG, "set_max_tx_power(84) failed: %s, retrying with 80",
esp_err_to_name(power_err));
power_err = esp_wifi_set_max_tx_power(80);
if (power_err != ESP_OK) {
ESP_LOGW(TAG, "set_max_tx_power(80) failed: %s", esp_err_to_name(power_err));
}
}
// 启用混杂模式,以获取发送方 rssi
wifi_promiscuous_filter_t filter = {};
filter.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT;
ESP_ERROR_CHECK(esp_wifi_set_promiscuous_filter(&filter));
ESP_ERROR_CHECK(esp_wifi_set_promiscuous_rx_cb(promiscuous_rx_cb));
ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true));
// 初始化 ESP-NOW
ESP_ERROR_CHECK(esp_now_init());
ESP_ERROR_CHECK(esp_now_register_recv_cb(onRecv));
// 添加广播 peer
memset(&broadcast_peer, 0, sizeof(broadcast_peer));
memset(broadcast_peer.peer_addr, 0xFF, sizeof(broadcast_peer.peer_addr));
broadcast_peer.channel = 0;
broadcast_peer.ifidx = WIFI_IF_STA;
broadcast_peer.encrypt = false;
ESP_ERROR_CHECK(esp_now_add_peer(&broadcast_peer));
memset(&peer, 0, sizeof(peer));
peer.channel = 0;
peer.ifidx = WIFI_IF_STA;
peer.encrypt = false;
if (Storage::hasKey("peer_mac")) {
Storage::getBytes("peer_mac", peer.peer_addr, sizeof(peer.peer_addr));
if (memcmp(peer.peer_addr, empty_mac, sizeof(empty_mac)) == 0) {
ESP_LOGI(TAG, "stored peer MAC is empty, staying unpaired");
} else {
esp_err_t err = esp_now_add_peer(&peer);
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_now_add_peer(stored) failed: %s", esp_err_to_name(err));
}
}
} else {
memset(peer.peer_addr, 0x00, sizeof(peer.peer_addr));
}
}
void Air::entryPairing() {
// 清除之前的配对
esp_now_del_peer(peer.peer_addr);
is_pairing = true;
pairing_found = false;
// 用于只配对最强的信号
my_packet.rssi = -128;
pairing_best_rssi = -128;
}
void Air::savePairing() {
if (pairing_found) {
// 添加 peer
esp_err_t err = esp_now_add_peer(&peer);
if (err != ESP_OK) {
ESP_LOGW(TAG, "esp_now_add_peer(pairing) failed: %s", esp_err_to_name(err));
}
} else {
// 配对失败,清空设备 mac
memset(peer.peer_addr, 0x00, sizeof(peer.peer_addr));
}
// 写入 flash
Storage::putBytes("peer_mac", peer.peer_addr, sizeof(peer.peer_addr));
is_pairing = false;
}
void Air::onLoop() {
if (is_pairing) {
// 发送配对请求
esp_now_send(broadcast_peer.peer_addr, pairing_data, sizeof(pairing_data));
} else if (memcmp(peer.peer_addr, empty_mac, sizeof(empty_mac)) != 0) {
// 已有配对设备
esp_now_send(peer.peer_addr, reinterpret_cast<const uint8_t *>(&my_packet), sizeof(packet_t));
}
}
void Air::dbusSet(const uint8_t *src, size_t len) {
portENTER_CRITICAL(&s_dbus_lock);
memcpy(my_packet.dbus, src, len);
portEXIT_CRITICAL(&s_dbus_lock);
}
void Air::dbusClear() {
portENTER_CRITICAL(&s_dbus_lock);
memset(my_packet.dbus, 0, sizeof(my_packet.dbus));
portEXIT_CRITICAL(&s_dbus_lock);
}
void Air::onRecv(const esp_now_recv_info_t *info, const uint8_t *data, int data_len) {
if (info == nullptr || info->src_addr == nullptr || data == nullptr) {
return;
}
// The original relied on the promiscuous callback to have recorded the RSSI
// of this frame beforehand; rx_ctrl carries the same value synchronously
// with the packet we are handling.
if (info->rx_ctrl != nullptr) {
my_packet.rssi = info->rx_ctrl->rssi;
}
if (is_pairing) {
// 配对 magic 正确
if (data_len == static_cast<int>(sizeof(pairing_data)) &&
memcmp(pairing_data, data, sizeof(pairing_data)) == 0) {
// 只配对最强的信号
if (my_packet.rssi > pairing_best_rssi) {
pairing_best_rssi = my_packet.rssi;
memcpy(peer.peer_addr, info->src_addr, sizeof(peer.peer_addr));
pairing_found = true;
}
}
} else if (memcmp(peer.peer_addr, info->src_addr, sizeof(peer.peer_addr)) == 0) {
// 是从配对设备发过来的,且数据长度正确
if (data_len == static_cast<int>(sizeof(packet_t))) {
memcpy(&peer_packet, data, sizeof(packet_t));
last_air_ms = millis();
}
}
}
void Air::promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type) {
if (type != WIFI_PKT_MGMT) {
return;
}
const auto *ppkt = static_cast<const wifi_promiscuous_pkt_t *>(buf);
const auto *ipkt = reinterpret_cast<const wifi_ieee80211_packet_t *>(ppkt->payload);
const wifi_ieee80211_mac_hdr_t *hdr = &ipkt->hdr;
if (is_pairing) {
// 配对模式
my_packet.rssi = ppkt->rx_ctrl.rssi;
} else if (memcmp(hdr->addr2, peer.peer_addr, sizeof(peer.peer_addr)) == 0) {
my_packet.rssi = ppkt->rx_ctrl.rssi;
}
}
+57
View File
@@ -0,0 +1,57 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include "esp_now.h"
#include "esp_wifi.h"
/**
* @brief ESP-NOW peer link (ported from the Arduino/ESP-NOW version).
*
* Runs ESP-NOW on the station interface of channel 14 with a broadcast peer
* used for pairing requests, plus a stored unicast peer used for the normal
* game traffic.
*/
class Air {
public:
/**
* Over-the-air packet. The original declared it `alignas(1)`; `packed`
* expresses the same intent (sizeof == 19, no trailing padding) in a way
* that is portable C++.
*/
struct __attribute__((packed)) packet_t {
uint32_t color = 0;
uint32_t hit_cnt = 0;
uint8_t dbus[10] = {0};
int8_t rssi = 0;
};
static packet_t my_packet, peer_packet;
static volatile uint32_t last_air_ms;
static volatile bool is_pairing;
static volatile bool pairing_found;
static void begin();
static void entryPairing();
static void savePairing();
static void onLoop();
// The USB-CDC task writes my_packet.dbus while the application task reads
// it, so the copy/clear are serialised here instead of being done inline.
static void dbusSet(const uint8_t *src, size_t len);
static void dbusClear();
private:
static const uint8_t empty_mac[6];
static const uint8_t pairing_data[4];
static esp_now_peer_info_t broadcast_peer;
static esp_now_peer_info_t peer;
static volatile int8_t pairing_best_rssi;
static void onRecv(const esp_now_recv_info_t *info, const uint8_t *data, int data_len);
static void promiscuous_rx_cb(void *buf, wifi_promiscuous_pkt_type_t type);
};
+215
View File
@@ -0,0 +1,215 @@
/*
* ESP-IDF entry point for the "armor" hit-detection node (ESP32-C3).
*
* This is the ESP-IDF counterpart of the original Arduino `main.cpp`:
* setup() -> app_main()
* loop() -> hit_task() (only ran Hit::onLoop())
* loop2() -> loop2_task() (button, ESP-NOW and serial bridging)
* Serial -> USB Serial/JTAG console + driver (CDC protocol)
* Serial0 -> UART0 on GPIO20/21 (DBUS bridge)
*/
#include <cstdio>
#include <cstring>
#include "driver/uart.h"
#include "driver/usb_serial_jtag.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nvs_flash.h"
#include "air.hpp"
#include "armor_time.hpp"
#include "button.hpp"
#include "hit.hpp"
#include "storage.hpp"
namespace {
constexpr const char *TAG = "armor";
constexpr uint32_t TIMEOUT_MS = 100;
// GPIO9 is the on-board BOOT button of the ESP32-C3 DevKitM-1, active low.
// Original: `OneButton button(9, true);`
Button button(9, true);
volatile bool g_click = false;
volatile bool g_long_click = false;
volatile uint32_t g_last_cdc_receive_ms = 0;
void handleClick() {
g_click = true;
}
void handleLongPressStart() {
g_long_click = true;
}
// --------------------------------------------------------------- UART0 -----
// Original: `Serial0.begin(115200, SERIAL_8N1, 20, 21);` (rx=20, tx=21)
constexpr uart_port_t DBUS_UART = UART_NUM_0;
constexpr int DBUS_UART_RX = 20;
constexpr int DBUS_UART_TX = 21;
void serial0_begin() {
uart_config_t cfg = {};
cfg.baud_rate = 115200;
cfg.data_bits = UART_DATA_8_BITS;
cfg.parity = UART_PARITY_DISABLE;
cfg.stop_bits = UART_STOP_BITS_1;
cfg.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
cfg.source_clk = UART_SCLK_DEFAULT;
ESP_ERROR_CHECK(uart_driver_install(DBUS_UART, 256, 256, 0, nullptr, 0));
ESP_ERROR_CHECK(uart_param_config(DBUS_UART, &cfg));
ESP_ERROR_CHECK(uart_set_pin(DBUS_UART, DBUS_UART_TX, DBUS_UART_RX, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
}
// ----------------------------------------------------------- USB CDC -------
// Replaces Arduino's `serialEvent()`: 10-byte DBUS frames arrive over the USB
// CDC port, and the original truncated/committed them in 10-byte units.
void cdc_task(void *) {
uint8_t chunk[32];
uint8_t frame[10];
size_t frame_len = 0;
while (true) {
const int n = usb_serial_jtag_read_bytes(chunk, sizeof(chunk), pdMS_TO_TICKS(20));
for (int i = 0; i < n; i++) {
frame[frame_len++] = chunk[i];
if (frame_len == sizeof(frame)) {
Air::dbusSet(frame, sizeof(frame));
g_last_cdc_receive_ms = millis();
frame_len = 0;
}
}
}
}
// ------------------------------------------------------------ business -----
void handle_air() {
static Air::packet_t peer_packet_last;
// 处理收到的数据包
// 颜色变化
if (Air::peer_packet.color != peer_packet_last.color && Air::peer_packet.color != 0) {
Hit::color = Air::peer_packet.color;
Storage::putUInt("color", Hit::color);
}
// 击打次数变化
if (Air::peer_packet.hit_cnt != peer_packet_last.hit_cnt) {
Hit::last_hit_ms = millis();
}
peer_packet_last = Air::peer_packet;
// 发送自己的数据包
Air::my_packet.color = Hit::color;
Air::my_packet.hit_cnt = Hit::hit_cnt;
if (millis() - g_last_cdc_receive_ms > TIMEOUT_MS) { // 超时数据清零
Air::dbusClear();
}
}
void handle_serial() {
// 串口发送 DBUS 数据
if (millis() - Air::last_air_ms > TIMEOUT_MS) { // 超时数据清零
memset(Air::peer_packet.dbus, 0, sizeof(Air::peer_packet.dbus));
}
uart_write_bytes(DBUS_UART, Air::peer_packet.dbus, sizeof(Air::peer_packet.dbus));
uart_wait_tx_done(DBUS_UART, pdMS_TO_TICKS(20)); // Serial0.flush()
// CDC发送:颜色、击打次数、信号强度(tx, rx)、距离最后一次收到消息的毫秒数
const uint32_t latency_ms = millis() - Air::last_air_ms;
printf("%d,%d,%d,%d,%d\n",
static_cast<int>(Air::my_packet.color),
static_cast<int>(Air::peer_packet.hit_cnt),
static_cast<int>(Air::peer_packet.rssi),
static_cast<int>(Air::my_packet.rssi),
static_cast<int>(latency_ms));
}
// ------------------------------------------------------------- tasks -------
// Equivalent of the original Arduino `loop2()`.
void loop2_task(void *) {
while (true) {
button.tick();
Air::onLoop();
if (!Air::is_pairing) { // 不在配对中
if (g_long_click) { // 长按配对
Air::entryPairing();
Hit::detect_hit = false;
} else if (g_click) { // 短按切换颜色
Hit::color = (Hit::color == Hit::RED) ? Hit::BLUE : Hit::RED;
Storage::putUInt("color", Hit::color);
} else { // 正常运行业务逻辑
handle_air();
handle_serial();
}
} else { // 在配对中
if (g_click) { // 短按保存配对
Air::savePairing();
Hit::color = Hit::RED;
Storage::putUInt("color", Hit::color);
Hit::detect_hit = true;
} else {
Hit::color = Air::pairing_found ? Hit::GREEN : Hit::YELLOW;
}
}
g_click = false;
g_long_click = false;
vTaskDelay(pdMS_TO_TICKS(10));
}
}
// Equivalent of the original Arduino `loop()`.
void hit_task(void *) {
while (true) {
Hit::onLoop();
}
}
} // namespace
extern "C" void app_main(void) {
// 初始化 NVS(原 Preferences 底层就是 NVS)
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_ERROR_CHECK(err);
ESP_ERROR_CHECK(Storage::begin("armor"));
// With CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG the console initialisation has
// already installed the USB Serial/JTAG driver, so only install it when
// that did not happen; reading raw CDC bytes then works either way.
if (!usb_serial_jtag_is_driver_installed()) {
usb_serial_jtag_driver_config_t usb_cfg = USB_SERIAL_JTAG_DRIVER_CONFIG_DEFAULT();
usb_cfg.rx_buffer_size = 256;
if (usb_serial_jtag_driver_install(&usb_cfg) != ESP_OK) {
ESP_LOGW(TAG, "USB Serial/JTAG driver unavailable, CDC protocol disabled");
}
}
serial0_begin();
// 绑定回调函数
button.attachClick(handleClick);
button.attachLongPressStart(handleLongPressStart);
button.setClickMs(50); // 短按时间(毫秒)
button.setPressMs(500); // 长按时间(毫秒)
Hit::begin();
Air::begin();
ESP_LOGI(TAG, "armor node started");
xTaskCreate(cdc_task, "cdc", 3072, nullptr, 5, nullptr);
xTaskCreate(loop2_task, "loop2", 8192, nullptr, 5, nullptr);
xTaskCreate(hit_task, "hit", 4096, nullptr, 5, nullptr);
}
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <cstdint>
#include "esp_timer.h"
/**
* @brief Arduino-compatible millisecond counter.
*
* The original firmware used the Arduino `millis()` helper; ESP-IDF offers the
* same value through esp_timer. The result is truncated to 32 bits on purpose
* so that the "elapsed = now - then" idiom keeps working across the wrap.
*/
inline uint32_t millis() {
return static_cast<uint32_t>(esp_timer_get_time() / 1000);
}
+126
View File
@@ -0,0 +1,126 @@
#include "button.hpp"
#include <cstdlib>
#include "armor_time.hpp"
#include "driver/gpio.h"
Button::Button(int gpio, bool active_low) : _pin(gpio), _active_low(active_low) {
gpio_config_t cfg = {};
cfg.pin_bit_mask = 1ULL << gpio;
cfg.mode = GPIO_MODE_INPUT;
cfg.pull_up_en = active_low ? GPIO_PULLUP_ENABLE : GPIO_PULLUP_DISABLE;
cfg.pull_down_en = active_low ? GPIO_PULLDOWN_DISABLE : GPIO_PULLDOWN_ENABLE;
cfg.intr_type = GPIO_INTR_DISABLE;
gpio_config(&cfg);
}
void Button::attachClick(callback_t cb) {
_clickFunc = cb;
}
void Button::attachLongPressStart(callback_t cb) {
_longPressStartFunc = cb;
}
void Button::setClickMs(uint32_t ms) {
_click_ms = ms;
}
void Button::setPressMs(uint32_t ms) {
_press_ms = ms;
}
// Port of OneButton::debounce(): a level only becomes the debounced level once
// it has been stable for _debounce_ms.
bool Button::debounce(bool value) {
now = millis();
if (_lastDebounceLevel == value) {
if (now - _lastDebounceTime >= static_cast<unsigned long>(std::abs(_debounce_ms))) {
debouncedLevel = value;
}
} else {
_lastDebounceTime = now;
_lastDebounceLevel = value;
}
return debouncedLevel;
}
void Button::tick() {
if (_pin < 0) {
return;
}
const int level = gpio_get_level(static_cast<gpio_num_t>(_pin));
const bool active_level = _active_low ? (level == 0) : (level != 0);
fsm(debounce(active_level));
}
void Button::reset() {
_state = OCS_INIT;
_nClicks = 0;
_startTime = millis();
}
// Port of OneButton::_fsm() (single click + long press start only).
void Button::fsm(bool active_level) {
const unsigned long waitTime = now - _startTime;
switch (_state) {
case OCS_INIT:
// Waiting for the level to become active.
if (active_level) {
_state = OCS_DOWN;
_startTime = now;
_nClicks = 0;
}
break;
case OCS_DOWN:
// Waiting for the level to become inactive again... or a long press.
if (!active_level) {
_state = OCS_UP;
_startTime = now;
} else if (waitTime > _press_ms) {
if (_longPressStartFunc) {
_longPressStartFunc();
}
_state = OCS_PRESS;
}
break;
case OCS_UP:
// Level is inactive: count as a short button down.
_nClicks++;
_state = OCS_COUNT;
break;
case OCS_COUNT:
if (active_level) {
// Button is down again.
_state = OCS_DOWN;
_startTime = now;
} else if ((waitTime >= _click_ms) || (_nClicks == _maxClicks)) {
if (_nClicks == 1 && _clickFunc) {
_clickFunc();
}
reset();
}
break;
case OCS_PRESS:
// Waiting for the pin to be released after a long press.
if (!active_level) {
_state = OCS_PRESSEND;
}
break;
case OCS_PRESSEND:
reset();
break;
default:
_state = OCS_INIT;
break;
}
}
+78
View File
@@ -0,0 +1,78 @@
#pragma once
#include <cstdint>
/**
* @brief Button gesture detector, ported from OneButton 2.6.1.
*
* Only the parts the firmware actually uses are kept (single click and long
* press start); the state machine, the debounce rule and the default timings
* are a line-by-line port of `OneButton::_fsm()` from
* https://github.com/mathertel/OneButton (BSD-3-Clause) so gesture behaviour
* matches the original Arduino build exactly.
*
* The unused OneButton features (double click, multi click, press, long press
* stop/during, idle) were never attached in the original firmware and are
* therefore not reproduced here.
*/
class Button {
public:
using callback_t = void (*)();
/**
* @param gpio GPIO the button is attached to.
* @param active_low true when pressing connects the pin to GND
* (the original used `OneButton button(9, true)`).
*/
Button(int gpio, bool active_low);
void attachClick(callback_t cb);
void attachLongPressStart(callback_t cb);
/** Milliseconds before a click is considered stable (OneButton setClickMs). */
void setClickMs(uint32_t ms);
/** Milliseconds before the press counts as a long press (setPressMs). */
void setPressMs(uint32_t ms);
/** Poll the pin and advance the state machine. Call periodically (10 ms). */
void tick();
private:
enum state_t {
OCS_INIT = 0,
OCS_DOWN = 1,
OCS_UP = 2,
OCS_COUNT = 3,
OCS_PRESS = 6,
OCS_PRESSEND = 7,
};
bool debounce(bool value);
void fsm(bool active_level);
void reset();
int _pin;
bool _active_low;
state_t _state = OCS_INIT;
int _nClicks = 0;
// Debounce state (OneButton: _debounce_ms defaults to 50 ms).
int _debounce_ms = 50;
bool debouncedLevel = false;
bool _lastDebounceLevel = false;
unsigned long _lastDebounceTime = 0;
unsigned long now = 0;
unsigned long _startTime = 0;
// `setClickMs(50)` / `setPressMs(500)` are applied by the application;
// these are the OneButton defaults.
unsigned int _click_ms = 400;
unsigned int _press_ms = 800;
int _maxClicks = 1;
callback_t _clickFunc = nullptr;
callback_t _longPressStartFunc = nullptr;
};
+192
View File
@@ -0,0 +1,192 @@
#include "hit.hpp"
#include <cstring>
#include "armor_time.hpp"
#include "driver/gpio.h"
#include "driver/rmt_tx.h"
#include "esp_adc/adc_continuous.h"
#include "esp_log.h"
#include "hal/adc_types.h"
#include "led_strip_encoder.h"
#include "soc/soc_caps.h"
#include "storage.hpp"
namespace {
constexpr const char *TAG = "hit";
// 这是总轮询采样率,每个通道的采样率只有 1/4
constexpr uint32_t SAMPLE_FREQ_HZ = 40000;
// 10ms 的采样数据(字节数,每个采样点 4 字节)
constexpr uint32_t SAMPLE_LEN = static_cast<uint32_t>(10e-3 * SAMPLE_FREQ_HZ * 4);
// 压电片 ADC 输入:front, back, left, right -> GPIO0 / GPIO4 / GPIO3 / GPIO1
constexpr int ADC_CHANNEL_COUNT = 4;
constexpr adc_channel_t ADC_CHANNELS[ADC_CHANNEL_COUNT] = {ADC_CHANNEL_0, ADC_CHANNEL_4, ADC_CHANNEL_3,
ADC_CHANNEL_1};
// WS2812 数据脚:front, back, left, right
constexpr int STRIP_COUNT = 4;
constexpr int STRIP_LEDS = 3;
constexpr gpio_num_t STRIP_GPIOS[STRIP_COUNT] = {GPIO_NUM_10, GPIO_NUM_5, GPIO_NUM_6, GPIO_NUM_7};
// WS2812 bit timing needs a fine resolution: 10MHz -> 1 tick = 0.1us
constexpr uint32_t RMT_RESOLUTION_HZ = 10000000;
// The ESP32-C3 RMT block owns 96 symbols of channel memory in total, shared by
// its two TX channels (each channel's memory is allocated in multiples of
// SOC_RMT_MEM_WORDS_PER_CHANNEL = 48 symbols). A 64-symbol block size would
// round up to 96 and therefore consume the whole block for a single channel,
// making the second `rmt_new_tx_channel()` fail with
// "rmt_tx_register_to_group: no free tx channels".
//
// All four strips always display the same colour, so a single channel that owns
// the full 96-symbol memory is enough: it holds a complete 3-LED frame
// (3 * 24 bits + reset = 73 symbols) in one shot and is parked on a different
// GPIO for each strip.
constexpr int RMT_CHANNEL_COUNT = 1;
constexpr uint32_t RMT_MEM_BLOCK_SYMBOLS = 96;
adc_continuous_handle_t s_adc = nullptr;
rmt_channel_handle_t s_rmt_channels[RMT_CHANNEL_COUNT] = {};
rmt_encoder_handle_t s_rmt_encoders[RMT_CHANNEL_COUNT] = {};
// Raw ADC DMA frame (10 ms worth of samples). 4-byte aligned so the result
// records can be read through `adc_digi_output_data_t`.
alignas(4) uint8_t s_adc_buf[SAMPLE_LEN];
// GRB pixel buffer; all four strips always display the same colour, so a single
// buffer is reused for each of them.
uint8_t s_pixels[STRIP_LEDS * 3];
void continuous_adc_init() {
// ADC 配置
adc_continuous_handle_cfg_t adc_dma_config = {};
adc_dma_config.max_store_buf_size = SAMPLE_LEN * 2; // 两倍缓冲区
adc_dma_config.conv_frame_size = SAMPLE_LEN;
ESP_ERROR_CHECK(adc_continuous_new_handle(&adc_dma_config, &s_adc));
// 通道配置
adc_digi_pattern_config_t adc_pattern[ADC_CHANNEL_COUNT] = {};
for (int i = 0; i < ADC_CHANNEL_COUNT; i++) {
adc_pattern[i].atten = ADC_ATTEN_DB_0;
adc_pattern[i].channel = ADC_CHANNELS[i] & 0x7;
adc_pattern[i].unit = 0;
adc_pattern[i].bit_width = SOC_ADC_DIGI_MAX_BITWIDTH;
}
adc_continuous_config_t dig_cfg = {};
dig_cfg.pattern_num = ADC_CHANNEL_COUNT;
dig_cfg.adc_pattern = adc_pattern;
dig_cfg.sample_freq_hz = SAMPLE_FREQ_HZ;
dig_cfg.conv_mode = ADC_CONV_SINGLE_UNIT_1;
ESP_ERROR_CHECK(adc_continuous_config(s_adc, &dig_cfg));
}
void rmt_led_init() {
for (int i = 0; i < RMT_CHANNEL_COUNT; i++) {
rmt_tx_channel_config_t tx_chan_config = {};
tx_chan_config.gpio_num = STRIP_GPIOS[i];
tx_chan_config.clk_src = RMT_CLK_SRC_DEFAULT;
tx_chan_config.resolution_hz = RMT_RESOLUTION_HZ;
tx_chan_config.mem_block_symbols = RMT_MEM_BLOCK_SYMBOLS;
tx_chan_config.trans_queue_depth = 4;
ESP_ERROR_CHECK(rmt_new_tx_channel(&tx_chan_config, &s_rmt_channels[i]));
led_strip_encoder_config_t encoder_config = {};
encoder_config.resolution = RMT_RESOLUTION_HZ;
ESP_ERROR_CHECK(rmt_new_led_strip_encoder(&encoder_config, &s_rmt_encoders[i]));
ESP_ERROR_CHECK(rmt_enable(s_rmt_channels[i]));
}
}
// Push the shared pixel buffer to one strip. A single RMT TX channel can only
// drive one pin at a time, and rmt_tx_switch_gpio() only works on a disabled
// channel, so the channel is briefly parked on the target GPIO before each frame.
void strip_show(int strip) {
const int index = strip % RMT_CHANNEL_COUNT;
rmt_channel_handle_t chan = s_rmt_channels[index];
esp_err_t err = rmt_disable(chan);
if (err == ESP_OK) {
err = rmt_tx_switch_gpio(chan, STRIP_GPIOS[strip], false);
}
if (err == ESP_OK) {
err = rmt_enable(chan);
}
rmt_transmit_config_t tx_config = {};
tx_config.loop_count = 0; // no transfer loop
if (err == ESP_OK) {
err = rmt_transmit(chan, s_rmt_encoders[index], s_pixels, sizeof(s_pixels), &tx_config);
}
if (err == ESP_OK) {
err = rmt_tx_wait_all_done(chan, 1000);
}
if (err != ESP_OK) {
ESP_LOGW(TAG, "strip %d refresh failed: %s", strip, esp_err_to_name(err));
}
}
void refresh_strips() {
const bool blink = Hit::detect_hit && (millis() - Hit::last_hit_ms < Hit::HIT_BLINK_MS);
if (blink) {
memset(s_pixels, 0, sizeof(s_pixels));
} else {
// WS2812 wants GRB byte order, MSB first, matching Adafruit_NeoPixel's
// NEO_GRB setting.
const uint32_t rgb = Hit::color;
for (int i = 0; i < STRIP_LEDS; i++) {
s_pixels[i * 3 + 0] = static_cast<uint8_t>((rgb >> 8) & 0xFF); // G
s_pixels[i * 3 + 1] = static_cast<uint8_t>((rgb >> 16) & 0xFF); // R
s_pixels[i * 3 + 2] = static_cast<uint8_t>((rgb >> 0) & 0xFF); // B
}
}
for (int i = 0; i < STRIP_COUNT; i++) {
strip_show(i);
}
}
} // namespace
// public
volatile uint32_t Hit::color = 0;
volatile bool Hit::detect_hit = true;
volatile uint32_t Hit::hit_cnt = 0;
volatile uint32_t Hit::last_hit_ms = 0;
void Hit::begin() {
// 从 flash 读取颜色
color = Storage::getUInt("color", RED);
rmt_led_init();
continuous_adc_init();
ESP_ERROR_CHECK(adc_continuous_start(s_adc));
}
void Hit::onLoop() {
// 击打检测
uint32_t out_length = 0;
esp_err_t err = adc_continuous_read(s_adc, s_adc_buf, SAMPLE_LEN, &out_length, 1000);
if (err == ESP_OK) {
for (uint32_t i = 0; i + SOC_ADC_DIGI_RESULT_BYTES <= out_length; i += SOC_ADC_DIGI_RESULT_BYTES) {
const auto *p = reinterpret_cast<const adc_digi_output_data_t *>(&s_adc_buf[i]);
const uint32_t data = p->type2.data;
if (data > HIT_THRESHOLD) { // 检测到击打
if (detect_hit && (millis() - last_hit_ms) > HIT_BLINK_MS) { // 不在上一次击打冷却期内,新的击打
// Note: '++' on a volatile is deprecated since C++20 (and IDF
// builds with -Werror), so use an explicit read-modify-write.
hit_cnt = hit_cnt + 1;
last_hit_ms = millis();
}
}
}
} else if (err != ESP_ERR_TIMEOUT) {
ESP_LOGW(TAG, "adc_continuous_read failed: %s", esp_err_to_name(err));
}
refresh_strips();
}
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include <cstdint>
/**
* @brief Piezo hit detection + WS2812 status LEDs.
*
* Ported from the Arduino version:
* * the legacy ADC DMA driver (`driver/adc.h`, removed in ESP-IDF v6) is
* replaced by the `adc_continuous` driver with the same 40 kHz aggregate
* sampling rate, 0 dB attenuation and 12-bit results;
* * Adafruit_NeoPixel (bit-banged) is replaced by an RMT WS2812 encoder.
*/
class Hit {
public:
// Same values as Adafruit_NeoPixel::Color(r, g, b) -> 0x00RRGGBB.
static constexpr uint32_t RED = 0xFF0000;
static constexpr uint32_t BLUE = 0x0000FF;
static constexpr uint32_t GREEN = 0x00FF00;
static constexpr uint32_t YELLOW = 0xFFFF00;
/** Blink duration after a hit, and the cooldown that suppresses repeats. */
static constexpr uint32_t HIT_BLINK_MS = 50;
/** Raw 12-bit ADC level above which a sample counts as a hit. */
static constexpr uint32_t HIT_THRESHOLD = 2000;
static volatile uint32_t color;
static volatile bool detect_hit;
static volatile uint32_t hit_cnt;
static volatile uint32_t last_hit_ms;
static void begin();
static void onLoop();
};
+126
View File
@@ -0,0 +1,126 @@
/*
* SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "esp_check.h"
#include "led_strip_encoder.h"
static const char *TAG = "led_encoder";
typedef struct {
rmt_encoder_t base;
rmt_encoder_t *bytes_encoder;
rmt_encoder_t *copy_encoder;
int state;
rmt_symbol_word_t reset_code;
} rmt_led_strip_encoder_t;
RMT_ENCODER_FUNC_ATTR
static size_t rmt_encode_led_strip(rmt_encoder_t *encoder, rmt_channel_handle_t channel, const void *primary_data, size_t data_size, rmt_encode_state_t *ret_state)
{
rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
rmt_encoder_handle_t bytes_encoder = led_encoder->bytes_encoder;
rmt_encoder_handle_t copy_encoder = led_encoder->copy_encoder;
rmt_encode_state_t session_state = RMT_ENCODING_RESET;
rmt_encode_state_t state = RMT_ENCODING_RESET;
size_t encoded_symbols = 0;
switch (led_encoder->state) {
case 0: // send RGB data
encoded_symbols += bytes_encoder->encode(bytes_encoder, channel, primary_data, data_size, &session_state);
if (session_state & RMT_ENCODING_COMPLETE) {
led_encoder->state = 1; // switch to next state when current encoding session finished
}
if (session_state & RMT_ENCODING_MEM_FULL) {
state |= RMT_ENCODING_MEM_FULL;
goto out; // yield if there's no free space for encoding artifacts
}
// fall-through
case 1: // send reset code
encoded_symbols += copy_encoder->encode(copy_encoder, channel, &led_encoder->reset_code,
sizeof(led_encoder->reset_code), &session_state);
if (session_state & RMT_ENCODING_COMPLETE) {
led_encoder->state = RMT_ENCODING_RESET; // back to the initial encoding session
state |= RMT_ENCODING_COMPLETE;
}
if (session_state & RMT_ENCODING_MEM_FULL) {
state |= RMT_ENCODING_MEM_FULL;
goto out; // yield if there's no free space for encoding artifacts
}
}
out:
*ret_state = state;
return encoded_symbols;
}
static esp_err_t rmt_del_led_strip_encoder(rmt_encoder_t *encoder)
{
rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
rmt_del_encoder(led_encoder->bytes_encoder);
rmt_del_encoder(led_encoder->copy_encoder);
free(led_encoder);
return ESP_OK;
}
RMT_ENCODER_FUNC_ATTR
static esp_err_t rmt_led_strip_encoder_reset(rmt_encoder_t *encoder)
{
rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base);
rmt_encoder_reset(led_encoder->bytes_encoder);
rmt_encoder_reset(led_encoder->copy_encoder);
led_encoder->state = RMT_ENCODING_RESET;
return ESP_OK;
}
esp_err_t rmt_new_led_strip_encoder(const led_strip_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder)
{
esp_err_t ret = ESP_OK;
rmt_led_strip_encoder_t *led_encoder = NULL;
ESP_GOTO_ON_FALSE(config && ret_encoder, ESP_ERR_INVALID_ARG, err, TAG, "invalid argument");
led_encoder = rmt_alloc_encoder_mem(sizeof(rmt_led_strip_encoder_t));
ESP_GOTO_ON_FALSE(led_encoder, ESP_ERR_NO_MEM, err, TAG, "no mem for led strip encoder");
led_encoder->base.encode = rmt_encode_led_strip;
led_encoder->base.del = rmt_del_led_strip_encoder;
led_encoder->base.reset = rmt_led_strip_encoder_reset;
// different led strip might have its own timing requirements, following parameter is for WS2812
rmt_bytes_encoder_config_t bytes_encoder_config = {
.bit0 = {
.level0 = 1,
.duration0 = 0.3 * config->resolution / 1000000, // T0H=0.3us
.level1 = 0,
.duration1 = 0.9 * config->resolution / 1000000, // T0L=0.9us
},
.bit1 = {
.level0 = 1,
.duration0 = 0.9 * config->resolution / 1000000, // T1H=0.9us
.level1 = 0,
.duration1 = 0.3 * config->resolution / 1000000, // T1L=0.3us
},
.flags.msb_first = 1 // WS2812 transfer bit order: G7...G0R7...R0B7...B0
};
ESP_GOTO_ON_ERROR(rmt_new_bytes_encoder(&bytes_encoder_config, &led_encoder->bytes_encoder), err, TAG, "create bytes encoder failed");
rmt_copy_encoder_config_t copy_encoder_config = {};
ESP_GOTO_ON_ERROR(rmt_new_copy_encoder(&copy_encoder_config, &led_encoder->copy_encoder), err, TAG, "create copy encoder failed");
uint32_t reset_ticks = config->resolution / 1000000 * 50 / 2; // reset code duration defaults to 50us
led_encoder->reset_code = (rmt_symbol_word_t) {
.level0 = 0,
.duration0 = reset_ticks,
.level1 = 0,
.duration1 = reset_ticks,
};
*ret_encoder = &led_encoder->base;
return ESP_OK;
err:
if (led_encoder) {
if (led_encoder->bytes_encoder) {
rmt_del_encoder(led_encoder->bytes_encoder);
}
if (led_encoder->copy_encoder) {
rmt_del_encoder(led_encoder->copy_encoder);
}
free(led_encoder);
}
return ret;
}
+36
View File
@@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Apache-2.0
*/
#pragma once
#include <stdint.h>
#include "driver/rmt_encoder.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Type of led strip encoder configuration
*/
typedef struct {
uint32_t resolution; /*!< Encoder resolution, in Hz */
} led_strip_encoder_config_t;
/**
* @brief Create RMT encoder for encoding LED strip pixels into RMT symbols
*
* @param[in] config Encoder configuration
* @param[out] ret_encoder Returned encoder handle
* @return
* - ESP_ERR_INVALID_ARG for any invalid arguments
* - ESP_ERR_NO_MEM out of memory when creating led strip encoder
* - ESP_OK if creating encoder successfully
*/
esp_err_t rmt_new_led_strip_encoder(const led_strip_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder);
#ifdef __cplusplus
}
#endif
+92
View File
@@ -0,0 +1,92 @@
#include "storage.hpp"
#include <cstring>
#include "esp_log.h"
#include "nvs.h"
namespace {
constexpr const char *TAG = "storage";
nvs_handle_t s_handle = 0;
bool s_open = false;
} // namespace
namespace Storage {
esp_err_t begin(const char *name_space) {
esp_err_t err = nvs_open(name_space, NVS_READWRITE, &s_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "nvs_open(%s) failed: %s", name_space, esp_err_to_name(err));
return err;
}
s_open = true;
return ESP_OK;
}
bool hasKey(const char *key) {
if (!s_open) {
return false;
}
// nvs_get_u32() requires a real output pointer, so probe with a scratch value.
uint32_t scratch = 0;
if (nvs_get_u32(s_handle, key, &scratch) == ESP_OK) {
return true;
}
size_t len = 0;
return nvs_get_blob(s_handle, key, nullptr, &len) == ESP_OK;
}
uint32_t getUInt(const char *key, uint32_t default_value) {
if (!s_open) {
return default_value;
}
uint32_t value = default_value;
if (nvs_get_u32(s_handle, key, &value) != ESP_OK) {
return default_value;
}
return value;
}
size_t getBytes(const char *key, void *out, size_t max_len) {
if (!s_open || out == nullptr) {
return 0;
}
size_t len = max_len;
if (nvs_get_blob(s_handle, key, out, &len) != ESP_OK) {
return 0;
}
return len;
}
esp_err_t putUInt(const char *key, uint32_t value) {
if (!s_open) {
return ESP_ERR_INVALID_STATE;
}
esp_err_t err = nvs_set_u32(s_handle, key, value);
if (err == ESP_OK) {
err = nvs_commit(s_handle);
}
if (err != ESP_OK) {
ESP_LOGE(TAG, "putUInt(%s) failed: %s", key, esp_err_to_name(err));
}
return err;
}
esp_err_t putBytes(const char *key, const void *data, size_t len) {
if (!s_open) {
return ESP_ERR_INVALID_STATE;
}
esp_err_t err = nvs_set_blob(s_handle, key, data, len);
if (err == ESP_OK) {
err = nvs_commit(s_handle);
}
if (err != ESP_OK) {
ESP_LOGE(TAG, "putBytes(%s) failed: %s", key, esp_err_to_name(err));
}
return err;
}
} // namespace Storage
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include "esp_err.h"
/**
* @brief Small NVS wrapper standing in for the Arduino `Preferences` class.
*
* The original code used `preferences.begin("armor", false)`, `putUInt`,
* `getUInt`, `isKey`, `putBytes` and `getBytes`. Those map directly onto the
* ESP-IDF NVS API; the handle is kept open for the lifetime of the program
* exactly like `Preferences` did.
*/
namespace Storage {
/** Open (creating if needed) the NVS namespace used by this application. */
esp_err_t begin(const char *name_space);
/** True when @p key exists as either a u32 or a blob. */
bool hasKey(const char *key);
/** Read a u32, returning @p default_value when the key is absent. */
uint32_t getUInt(const char *key, uint32_t default_value);
/** Read a blob, returning the number of bytes read (0 when absent). */
size_t getBytes(const char *key, void *out, size_t max_len);
/** Store a u32 and commit it. */
esp_err_t putUInt(const char *key, uint32_t value);
/** Store a blob and commit it. */
esp_err_t putBytes(const char *key, const void *data, size_t len);
} // namespace Storage
+11
View File
@@ -0,0 +1,11 @@
# Custom partition table for the armor node (4 MB flash).
#
# nvs : application settings (color, peer MAC) + WiFi calibration
# phy_init : RF calibration data
# factory : the single application image (2 MB headroom; the WiFi + ESP-NOW
# build is well under 1 MB but the default 1 MB single-app slot is
# uncomfortably tight)
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
phy_init, data, phy, 0xf000, 0x1000,
factory, app, factory, 0x10000, 0x200000,
1 # Custom partition table for the armor node (4 MB flash).
2 #
3 # nvs : application settings (color, peer MAC) + WiFi calibration
4 # phy_init : RF calibration data
5 # factory : the single application image (2 MB headroom; the WiFi + ESP-NOW
6 # build is well under 1 MB but the default 1 MB single-app slot is
7 # uncomfortably tight)
8 # Name, Type, SubType, Offset, Size, Flags
9 nvs, data, nvs, 0x9000, 0x6000,
10 phy_init, data, phy, 0xf000, 0x1000,
11 factory, app, factory, 0x10000, 0x200000,
+7
View File
@@ -0,0 +1,7 @@
# This file was generated using idf.py save-defconfig or menuconfig [D] key. It can be edited manually.
# Espressif IoT Development Framework (ESP-IDF) 6.1.0 Project Minimal Configuration
#
# default:
CONFIG_IDF_TARGET="esp32c3"
CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y
CONFIG_PARTITION_TABLE_CUSTOM=y