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:
@@ -0,0 +1,2 @@
|
||||
CompileFlags:
|
||||
Remove: [-f*, -m*]
|
||||
@@ -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
|
||||
@@ -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 没被采用。
|
||||
# =============================================================================
|
||||
@@ -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)
|
||||
@@ -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 |
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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(©_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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
@@ -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
|
||||
Reference in New Issue
Block a user