Compare commits

..

No commits in common. "feat/add_mcuboot" and "main" have entirely different histories.

25 changed files with 15 additions and 2327 deletions

View File

@ -22,10 +22,5 @@ else()
src/temp.cpp src/temp.cpp
src/watdog.cpp src/watdog.cpp
src/com.cpp src/com.cpp
src/uid.cpp
)
zephyr_sources_ifdef(CONFIG_APP_DFU
src/dfu.cpp
) )
endif() endif()

View File

@ -6,11 +6,3 @@ config SAMPLE_HELLOWORLD
bool "Hello World sample" bool "Hello World sample"
config SAMPLE_NTC config SAMPLE_NTC
bool "NTC sample" bool "NTC sample"
config APP_DFU
default y
bool "Enable DFU"
config APP_DEBUG_LED_STRIP
default n
bool "Enable debug LED strip"

View File

@ -1,5 +0,0 @@
VERSION_MAJOR = 0
VERSION_MINOR = 0
PATCHLEVEL = 19
VERSION_TWEAK = 0
EXTRAVERSION =

View File

@ -1,156 +0,0 @@
# 按摩头 UID 编码规则
## 1. 设备分类
| 类别 | 设备类型 | 说明 |
|---|---|---|
| 内置式 | 冲击波、光磁 | 由主板直接驱动,按摩头内无独立控制模块 |
| 外置式 | DMS、负压、捶打、揉捏、热敷、EMS 等 | 按摩头自带驱动模块,通过数据线与主板通信 |
> 通用设计所有按摩头均可插入设备的任意端口Port 0 或 Port 1端口号不影响 UID 编码。
---
## 2. UID 基本规则
每个按摩头拥有一个唯一的身份标识码UID用于识别设备类型和追溯生产信息。
### UID 组成(共 10 字节)
```
┌──────────┬──────────┬────────────┬─────────────────────┐
│ 设备类型 │ 产品代次 │ 生产年月 │ 唯一序列号 │
│ (1字节) │ (1字节) │ (4字节) │ (4字节) │
└──────────┴──────────┴────────────┴─────────────────────┘
字节: [0] [1] [2..6) [6..10)
```
### 字段说明
| 字段 | 说明 | 示例 |
|---|---|---|
| 设备类型 | 区分按摩头的产品系列 | 冲击波、光磁、外置按摩头等 |
| 产品代次 | 同一系列内的产品迭代版本(硬件版本号) | 第一代、第二代、第三代 |
| 生产年月 | 同一代次的不同批次修订版本 | 第一批、第二批 |
| 唯一序列号 | 生产时分配的唯一编号 | 用于追溯具体产品 |
---
## 3. 设备类型编码
### 冲击波系列(类型 0x01
| 代次 | 设备名称 | UID 示例 |
|---|---|---|
| 第一代 | 标准冲击波手柄 | 01-01-XXXX-1234 |
| 第二代 | 高能型冲击波手柄 | 01-02-XXXX-1235 |
### 光磁/热疗系列(类型 0x02
| 代次 | 设备名称 | UID 示例 |
|---|---|---|
| 第一代 | 光磁手柄 | 02-01-XXXX-XXXX |
| 第二代 | 热疗手柄 | 02-02-XXXX-XXXX |
| 第三代 | 增强型光磁手柄 | 02-03-XXXX-XXXX |
### 外置按摩头系列(类型 0x03
| 代次 | 设备名称 | UID 示例 |
|---|---|---|
| 第一代 | 捶打按摩头 | 03-01-XXXX-XXXX |
| 第一代 | 揉捏按摩头 | 03-02-XXXX-XXXX |
| 第一代 | 热敷按摩头 | 03-03-XXXX-XXXX |
| 第一代 | 电磁按摩头EMS | 03-04-XXXX-XXXX |
| 第一代 | DMS 深层肌肉刺激仪 | 03-05-XXXX-XXXX |
| 第一代 | 负压吸力按摩头 | 03-06-XXXX-XXXX |
| — | 通用型(未分类) | 03-F0-XXXX-XXXX |
| — | 未知设备(自动探测) | 03-FF-XXXX-XXXX |
---
## 4. 编码示例
### 示例 1冲击波第一代
- 设备类型冲击波系列0x01
- 产品代次第一代0x01
- 生产年月26082026 年 8 月BCD 风格)
- 序列号00001234
```
设备类型 │ 产品代次 │ 生产年月 │ 唯一序列号
─────────┼─────────┼──────────────┼────────────────────────
0x01 │ 0x01 │ 26 08 00 00 │ 0x00 0x00 0x12 0x34
```
UIDhex10 字节):`01 01 26 08 00 00 00 00 12 34`
### 示例 2光磁第三代
- 设备类型:光磁/热疗系列0x02
- 产品代次第三代0x03
- 生产年月2608
- 序列号00005678
```
设备类型 │ 产品代次 │ 生产年月 │ 唯一序列号
─────────┼─────────┼──────────────┼────────────────────────
0x02 │ 0x03 │ 26 08 00 00 │ 0x00 0x00 0x56 0x78
```
UIDhex10 字节):`02 03 26 08 00 00 00 00 56 78`
### 示例 3DMS 第一代
- 设备类型外置按摩头系列0x03
- 产品代次第一代0x01
- 生产年月2608
- 序列号AABBCCDD
```
设备类型 │ 产品代次 │ 生产年月 │ 唯一序列号
─────────┼─────────┼──────────────┼────────────────────────
0x03 │ 0x01 │ 26 08 00 00 │ 0xAA 0xBB 0xCC 0xDD
```
UIDhex10 字节):`03 01 26 08 00 00 AA BB CC DD`
> 注:生产年月 4 字节的具体子字段分配BCD/扩展信息)由生产系统定义,本工具按原始字节透传、透读。
---
## 5. 数据存储方式
UID 信息存储在设备 storage 分区0x7000064KB起始处记录格式
```
[magic "UID1" 4B][ver 1B][uid 10B][crc16 2B][pad 7B] = 24 字节
```
- 含 CRC16-Modbus 校验;记录损坏/未写入时自动回退 MCU 芯片 UID12 字节)。
- 写入命令:`0x07` 指令,见《通讯协议.md》。
---
## 6. 端口识别说明
按摩头插入端口后,设备通过以下方式识别:
- **UID**:识别按摩头的类型和追溯信息
- **端口号**:决定控制电路的分配(与 UID 无关)
按摩头可自由插入任意端口UID 不会因端口切换而改变。
---
## 7. 扩展预留
| 类型范围 | 用途 |
|---|---|
| 0x01 | 冲击波系列(已定义) |
| 0x02 | 光磁/热疗系列(已定义) |
| 0x03 | 外置按摩头系列(已定义) |
| 0x04 - 0x0F | 未来新增设备系列 |
| 0x10 - 0xEF | 客户定制设备 |
| 0xF0 - 0xFE | 工厂测试工具 |
| 0xFF | 无效/未编程(保留) |

View File

@ -1,307 +0,0 @@
# 光磁头固件升级操作指南
## 1. 环境准备
### 1.1 安装依赖
```bash
# GUI 工具必需:串口库
pip install pyserial
# 固件签名工具(imgtool 随 Zephyr/MCUboot 自带,也可单独安装)
pip install imgtool
# 可选:命令行升级(smpmgr)——GUI 已内置 SMP 客户端,不装也能升级
pip install smpmgr
```
### 1.2 生成签名密钥(量产阶段)
```bash
# 在应用目录下生成密钥对
cd /home/issac-zys/code/zephyr_prj_template/app/app_photomagnetic
mkdir -p keys
imgtool keygen -k keys/mykey.pem -t ecdsa-p256
# 更新 sysbuild.conf 中的密钥路径与签名类型
# SB_CONFIG_BOOT_SIGNATURE_TYPE_ECDSA_P256=y
# SB_CONFIG_BOOT_SIGNATURE_KEY_FILE="${APP_DIR}/keys/mykey.pem"
```
> 开发阶段可跳过此步,使用 MCUboot 自带的开发密钥 `root-ec-p256.pem`
> ⚠️ **密钥算法必须与签名类型一致**:
> - `-t ecdsa-p256` 对应 `SB_CONFIG_BOOT_SIGNATURE_TYPE_ECDSA_P256=y`
> - `-t rsa-2048` 对应 `SB_CONFIG_BOOT_SIGNATURE_TYPE_RSA=y`
> 不匹配会报链接错误(如 `undefined reference to 'ecdsa_pub_key'`)。
> 本项目 boot 分区只有 48KB,建议用 ECDSA-P256(RSA 会把 MCUboot 撑到接近极限)。
> 更换密钥后必须**完整重建**(删 build 目录),否则签名/验签用的还是旧公钥。
---
## 2. 首次构建与烧录(需要调试器)
### 2.1 构建 MCUboot + 应用
```bash
cd /home/issac-zys/code/zephyr_prj_template
# 构建(sysbuild 模式,自动构建 MCUboot + 应用)
west build -p auto -b dr2501a_g0b0ce/stm32g0b0xx \
--sysbuild app/app_photomagnetic \
-d build
```
构建产物:
- `build/mcuboot/zephyr/zephyr.hex` / `.bin` — MCUboot bootloader(0x08000000)
- `build/app_photomagnetic/zephyr/zephyr.signed.hex` / `.bin` — 签名后的应用固件(0x0800C000)
> ⚠️ 注意:顶层 `west flash -d build` 只会烧应用镜像,不会烧 MCUboot!MCUboot 必须单独烧录。
### 2.2 烧录(一次性,需要调试器)
```bash
# ① 先烧 MCUboot 到 0x08000000(覆盖出厂 godtek bootloader,只需一次)
west flash -d build/mcuboot
# ② 再烧签名应用到 slot0 (0x0800C000)
west flash -d build
```
> 两条命令顺序不能反:没有 MCUboot 时,0x0800C000 的镜像头无法被引导,设备会完全无反应。
### 2.3 验证首次启动
串口调试口(usart3)应输出:
```
*** Booting MCUboot build ...
*** Booting Zephyr OS build ...
[dfu] init: upgrade command enabled (0xFF)
[com] init: rx table=4, id sent
[main] init on dr2501a_g0b0ce
```
协议口(usart1)应输出 ID 帧: `[7E E7][01][03][XX XX XX][CRC]`
---
## 3. 后续升级(通过 UART,不需要调试器)
### 3.1 升级流程图
```
┌─────────────────────────────────────────────────────────────┐
│ 主板 / 测试工具 │
│ 1. 发送升级命令 [7E E7][FF][01][01][CRC] │
│ 2. 等待 300ms(小板重启) │
│ 3. 通过 smpmgr 上传固件(3 秒内) │
│ 4. 标记待测试 + 复位 │
│ 5. 等待新固件启动 │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 光磁头(小板) │
│ 1. 收到升级命令 → 直接重启 │
│ 2. MCUboot 启动 → 等待 SMP 命令(3 秒) │
│ 3. 收到 SMP 命令 → 进入 serial recovery │
│ 4. 接收固件 → 直接写入 slot0(主槽,串口恢复固定写主槽) │
│ 5. 复位 → MCUboot 校验签名 → 启动新固件 │
└─────────────────────────────────────────────────────────────┘
```
> ⚠️ **重要:串口恢复(serial recovery)是把镜像直接写入 slot0(主槽)**,
> 不是写 slot1 再 swap(那是应用内 mcumgr DFU 的做法)。因此:
> 1. **升级过程中绝不能抢占/打开串口**(比如用其他串口工具、或让本工具重新 Connect),
> 否则上传中断会把 slot0 擦到一半,设备无法启动;
> 2. 上传中断后设备不会变砖——MCUboot 仍在运行并停留在 serial recovery 等待区,
> 直接用 smpmgr 重新上传即可恢复(或 `west flash -d build` 重烧应用);
> 3. 设备异常停在 bootloader 时,再点一次升级按钮也能恢复(0xFF 命令对 MCUboot 无害,
> smpmgr 会自动连上当前等待中的 serial recovery)。
### 3.2 使用 smpmgr CLI 升级
#### 步骤 1: 发送升级命令
使用串口工具或 Python 发送升级命令:
```python
import serial, struct, time
def send_upgrade(port='/dev/ttyUSB0'):
cmd = bytes([0x7E, 0xE7, 0xFF, 0x01, 0x01])
crc = 0xFFFF
for b in cmd[2:]:
crc ^= b
for _ in range(8):
crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
cmd += struct.pack('<H', crc)
with serial.Serial(port, 115200, timeout=1) as ser:
ser.write(cmd)
print(f"已发送升级命令: {cmd.hex()}")
time.sleep(0.3) # 等待重启
send_upgrade('/dev/ttyUSB0')
```
#### 步骤 2: 使用 smpmgr 上传固件
```bash
# 一键升级(串口恢复模式直接写 slot0 主槽,上传完自动复位,无需 test/confirm)
# 必须在小板重启后 3 秒内执行;--timeout 10 给擦除/慢速上传留足超时
smpmgr --port /dev/ttyUSB0 --timeout 10 upgrade \
build/app_photomagnetic/zephyr/zephyr.signed.bin
```
也可以手动分步:
```bash
# 上传固件到 slot0(主槽)
smpmgr --port /dev/ttyUSB0 image upload build/app_photomagnetic/zephyr/zephyr.signed.bin
# 复位,让 MCUboot 校验并启动新固件
smpmgr --port /dev/ttyUSB0 os reset
```
#### 步骤 3: 验证新固件
```bash
# 查看串口输出,确认新固件版本(协议口应输出新固件的 ID 帧)
# 串口恢复直接写主槽、无 swap,固件已生效,无需 confirm。
```
### 3.3 使用 GUI 工具升级(推荐)
已集成到 `scripts/test_gui.py` 中,升级功能**进程内封装了 SMP 客户端**(`smp_client.py`),
不再依赖外部 smpmgr/mcumgr 命令:
1. 打开 GUI 工具: `python scripts/test_gui.py`
2. 连接串口
3. 点击 **"Upgrade Firmware"** 按钮
4. 选择 `.signed.bin` 固件文件
5. 等待升级完成(GUI 会显示上传进度,自动复位并重连)
升级期间 GUI 会锁定 Connect 按钮/端口选择,防止串口被抢占导致上传中断。
> 可选:仍可用命令行一键升级(需已安装 smpmgr):
> `smpmgr --port /dev/ttyUSB0 --timeout 10 upgrade build/app_photomagnetic/zephyr/zephyr.signed.bin`
### 3.4 自动化升级脚本
```bash
#!/bin/bash
# upgrade.sh - 光磁头固件升级脚本
# 用法: ./upgrade.sh /dev/ttyUSB0 zephyr.signed.bin
PORT=${1:-/dev/ttyUSB0}
FIRMWARE=${2:-build/app_photomagnetic/zephyr/zephyr.signed.bin}
echo "=== 光磁头固件升级 ==="
echo "串口: $PORT"
echo "固件: $FIRMWARE"
# 检查固件文件
if [ ! -f "$FIRMWARE" ]; then
echo "错误: 固件文件不存在"
exit 1
fi
# 发送升级命令
echo "1. 发送升级命令..."
python3 -c "
import serial, struct, time
with serial.Serial('$PORT', 115200, timeout=1) as ser:
cmd = bytes([0x7E, 0xE7, 0xFF, 0x01, 0x01])
crc = 0xFFFF
for b in cmd[2:]:
crc ^= b
for _ in range(8):
crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
cmd += struct.pack('<H', crc)
ser.write(cmd)
print(f' 已发送: {cmd.hex()}')
"
# 等待重启
echo "2. 等待小板重启..."
sleep 0.3
# 上传固件(串口恢复直接写 slot0 主槽,上传完自动复位)
echo "3. 上传固件..."
smpmgr --port "$PORT" --timeout 10 upgrade "$FIRMWARE"
if [ $? -ne 0 ]; then
echo "错误: 固件上传失败"
echo "提示: 上传中断会擦掉 slot0 一半,但设备停在 bootloader 中不会变砖,"
echo " 重新执行本脚本即可恢复(或 west flash -d build 重烧应用)。"
exit 1
fi
echo "=== 升级完成,等待新固件启动 ==="
```
---
## 4. 恢复(串口恢复模式无 swap,没有回滚机制)
> 串口恢复(serial recovery)是直接把镜像写入 slot0 主槽,固件即刻生效、
> 不存在“待测试/回滚”的概念。旧版文档里的 swap/回滚描述来自应用内 mcumgr DFU,
> 本方案不适用。
### 4.1 上传中断/镜像损坏后的恢复
上传中断只会擦掉 slot0 一部分,MCUboot 仍然在运行并停留在 serial recovery 等待区,
**不需要调试器**即可恢复:
```bash
# 直接重传即可(设备已在 bootloader 等待,无需再发升级命令)
smpmgr --port /dev/ttyUSB0 --timeout 10 upgrade \
build/app_photomagnetic/zephyr/zephyr.signed.bin
```
或使用 GUI:再次点击 **Upgrade Firmware**(0xFF 命令对 MCUboot 无害,smpmgr 会自动连上)。
### 4.2 强制恢复(需要调试器)
如果连 MCUboot 都进不去(比如误烧了 boot 分区):
```bash
# 重新烧录 MCUboot + 应用
west flash -d build/mcuboot
west flash -d build
```
---
## 5. 故障排查
| 现象 | 原因 | 解决方案 |
|---|---|---|
| smpmgr 超时 | 上传未在 3 秒窗口内开始 | 先确认设备已进 bootloader(串口有 MCUboot 日志)再上传 |
| 上传中断 | 串口被其他工具/GUI 占用 | 关闭其他串口工具,重新上传(需从头传,不支持断点续传) |
| 上传后无法启动 | 上传中断擦坏了 slot0 | 设备停在 bootloader,直接重传或 `west flash -d build` 重烧应用 |
| 签名验证失败 | 密钥不匹配 | 用构建时相同的密钥(开发期 `root-ec-p256.pem`)签名 |
| smpmgr 连接失败 | 端口错误/被占用 | 确认端口号,关闭占用程序后再试 |
---
## 6. 注意事项
1. **3 秒窗口**:小板重启后,MCUboot 等待 SMP 命令 3 秒;超时则正常启动应用
2. **波特率**:应用层协议和 smpmgr 都使用 115200
3. **签名**:开发阶段使用默认密钥 `root-ec-p256.pem`;量产必须替换(且必须用 ECDSA-P256,RSA 装不进 48KB 分区)
4. **升级期间勿占用串口**:上传过程中不要打开其他串口工具、不要让 GUI 重新 Connect,否则上传中断会擦坏 slot0
5. **调试器**:首次烧录 MCUboot 需要;后续升级不需要
6. **断电保护**:升级中断电 → MCUboot 仍在运行,停在 serial recovery 等待区,重新上传即可恢复,设备不会变砖
---
## 7. 工具对比
| 工具 | 安装方式 | 优点 |
|---|---|---|
| **GUI 工具(推荐)** | 集成在 `scripts/test_gui.py`,内置 SMP 客户端 | 图形界面,一键升级,无需额外依赖(smpmgr/mcumgr 均可不要) |
| **smpmgr** | `pip install smpmgr` | 命令行,支持 Serial/BLE/UDP,适合脚本化 |
| **mcumgr CLI** | Go 编译/下载二进制 | Zephyr 官方工具,功能完整 |

View File

@ -1,251 +0,0 @@
# 光磁头固件在线升级方案 — 更改说明
## 1. 方案概述
采用 **MCUboot Serial Recovery** 方案,复用 usart1 协议口实现固件升级。
- 应用层正常运行时,usart1 由 0x7EE7 协议占用
- 收到升级命令后,应用层直接重启
- MCUboot 接管 usart1,等待 3 秒看是否有 mcumgr 命令
- 主板在 3 秒内发送 mcumgr 命令 → 进入 serial recovery 模式
- 主板上传签名固件 → MCUboot 验证签名 → swap → 重启运行新固件
**关键特性**:
- 零额外硬件(复用 usart1)
- 应用层不运行 mcumgr,与 0x7EE7 协议零冲突
- MCUboot 内置 RSA 签名验证
- 升级中断电可回滚,设备不会变砖
---
## 2. 涉及的文件
### 2.1 新建文件
| 文件 | 用途 |
|---|---|
| `sysbuild.conf` | 启用 MCUboot bootloader + 签名密钥路径 |
| `sysbuild/mcuboot.conf` | MCUboot 配置:serial recovery、签名、喂狗 |
| `sysbuild/mcuboot.overlay` | MCUboot 设备树覆盖(当前为空,保留扩展) |
| `src/dfu.cpp` | DFU 模块初始化确认打印 |
| `doc/固件升级方案.md` | 详细升级方案文档 |
### 2.2 修改文件
| 文件 | 改动 |
|---|---|
| `include/com.hpp` | 新增 `DfuCommand` 回调(条件编译 `CONFIG_APP_DFU`),处理 0xFF 指令 |
| `CMakeLists.txt` | 已有 `zephyr_sources_ifdef(CONFIG_APP_DFU src/dfu.cpp)` 控制 |
| `Kconfig` | 已有 `config APP_DFU default y` |
### 2.3 未修改文件
| 文件 | 说明 |
|---|---|
| `boards/dr2501a/dr2501a_g0b0ce.dts` | flash 分区已就绪(boot=48KB, slot0/1=200KB, storage=64KB) |
| `prj.conf` | 无需额外配置(应用层不运行 mcumgr) |
| `boards/use_ms.overlay` | 传感器配置,无改动 |
---
## 3. 配置说明
### 3.1 sysbuild.conf
```
SB_CONFIG_BOOTLOADER_MCUBOOT=y
SB_CONFIG_BOOT_SIGNATURE_KEY_FILE="${ZEPHYR_BASE}/../bootloader/mcuboot/root-rsa-2048.pem"
```
- 启用 MCUboot bootloader
- 开发阶段使用 MCUboot 自带的 RSA-2048 密钥
- 量产时替换为自生成密钥
### 3.2 sysbuild/mcuboot.conf
```
CONFIG_MCUBOOT_SERIAL=y # 启用 serial recovery
CONFIG_BOOT_SERIAL_UART=y # 使用 UART 接口
CONFIG_BOOT_SERIAL_ENTRANCE_GPIO=n # 禁用 GPIO 按钮检测
CONFIG_BOOT_SERIAL_WAIT_FOR_DFU=y # 等待 mcumgr 命令触发
CONFIG_BOOT_SERIAL_WAIT_FOR_DFU_TIMEOUT=3000 # 等待 3 秒
CONFIG_BOOT_WATCHDOG_FEED=y # 喂狗(避免升级过程中看门狗复位)
CONFIG_BOOT_SIGNATURE_TYPE_RSA=y # RSA 签名验证
CONFIG_BOOT_ERASE_PROGRESSIVELY=y # 渐进擦除(优化升级速度)
```
### 3.3 条件编译
DFU 功能通过 `CONFIG_APP_DFU` 控制:
- `y`(默认):编译 dfu.cpp,com.hpp 中包含 0xFF 指令处理
- `n`:完全移除 DFU 功能(零开销)
---
## 4. 协议扩展
### 4.1 新增指令:0xFF 升级命令
| 子命令 | 方向 | 行为 |
|---|---|---|
| `0x01` | 主板→小板 | 进入升级模式(直接重启) |
| `0x02` | 主板→小板 | 查询升级状态 |
| `0x02` | 小板→主板 | 回复 0x00 = 应用层正常运行 |
### 4.2 帧格式
```
进入升级模式:
主板 → 小板: [7E E7] [FF] [01] [01] [CRC_L] [CRC_H]
小板: 无回复(直接重启)
查询升级状态:
主板 → 小板: [7E E7] [FF] [02] [00] [CRC_L] [CRC_H]
小板 → 主板: [7E E7] [FF] [02] [01] [00] [CRC_L] [CRC_H]
```
---
## 5. 升级流程
### 5.1 首次构建与烧录(需要调试器)
```bash
# 构建(MCUboot + 应用)
west build -p auto -b dr2501a_g0b0ce/stm32g0b0xx \
--sysbuild app/app_photomagnetic \
-d build
# 烧录(一次性)
west flash -d build
```
### 5.2 后续升级(通过 UART,不需要调试器)
```
步骤1: 主板发送升级命令
主板 → 小板: [7E E7] [FF] [01] [01] [CRC_L] [CRC_H]
步骤2: 小板重启,MCUboot 接管 usart1
MCUboot 等待 3 秒看是否有 mcumgr 命令
步骤3: 主板切换到 mcumgr 模式(3 秒内)
mcumgr --conntype=serial --connstring='dev=/dev/ttyUSBx,baud=115200' \
image upload -e zephyr.signed.bin
步骤4: 标记待测试
mcumgr image test <hash>
步骤5: 复位触发 swap
mcumgr reset
步骤6: MCUboot 验证签名 → swap → 重启运行新固件
```
---
## 6. 签名密钥管理
### 6.1 开发阶段
使用 MCUboot 自带的开发密钥:
```
${ZEPHYR_BASE}/../bootloader/mcuboot/root-rsa-2048.pem
```
sysbuild.conf 中已配置,构建时自动签名。
### 6.2 量产阶段
```bash
# 生成自定义密钥对
pip install imgtool
imgtool keygen -k keys/my-signing-key.pem -t rsa-2048
# 更新 sysbuild.conf
SB_CONFIG_BOOT_SIGNATURE_KEY_FILE="${APP_DIR}/keys/my-signing-key.pem"
```
> ⚠️ 私钥必须安全保管,丢失后无法签名新固件。
### 6.3 签名验证流程
```
构建时:imgtool 用私钥签名 → zephyr.signed.bin
运行时:MCUboot 用公钥验证 → 验证通过才 swap
```
---
## 7. Flash 分区布局
| 分区 | 偏移 | 大小 | 用途 |
|---|---|---|---|
| boot_partition | 0x00000 | 48KB | MCUboot bootloader |
| slot0_partition | 0x0C000 | 200KB | 应用镜像(当前运行) |
| slot1_partition | 0x3E000 | 200KB | 升级镜像(待 swap) |
| storage_partition | 0x70000 | 64KB | NVS 存储 |
分区已在 `dr2501a_g0b0ce.dts` 中定义,无需修改。
---
## 8. 安全机制
| 机制 | 说明 |
|---|---|
| 镜像签名 | RSA-2048 签名验证,公钥烘焙进 bootloader |
| 未确认自动回滚 | test 模式下重启未确认 → MCUboot 自动回滚到原固件 |
| 升级中断保护 | 升级中断电 → MCUboot 回滚,设备不会变砖 |
| 等待超时 | 3 秒内无 mcumgr 命令 → 正常启动应用 |
---
## 9. 主板端配合
### 9.1 升级时序
```
t=0ms: 主板发送升级命令 [7E E7][FF][01][01][CRC]
t=100ms: 小板重启
t=200ms: MCUboot 启动,开始等待 mcumgr 命令
t=200ms~3200ms: 主板发送 mcumgr 命令(必须在 3 秒内)
t=3200ms: MCUboot 超时,正常启动应用(如果没收到 mcumgr)
```
### 9.2 关键约束
- 主板必须在小板重启后 **3 秒内** 发送 mcumgr 命令
- 波特率:115200(与应用层协议一致)
- 升级完成后,小板自动重启运行新固件
- 主板需检测小板重启(等待新固件的 ID 帧),然后切回 0x7EE7 协议模式
---
## 10. 注意事项
1. **首次烧录需要调试器**:MCUboot + 应用首次烧录需要 J-Link/OpenOCD;后续升级全部通过 UART
2. **3 秒窗口**:主板必须在小板重启后 3 秒内发送 mcumgr 命令,否则 MCUboot 会正常启动应用
3. **签名密钥**:开发阶段使用默认密钥(不安全);量产必须替换为自生成密钥
4. **降级保护**:开发阶段建议关闭(`CONFIG_BOOT_DOWNGRADE_PREVENTION=n`),方便回退
5. **usart1 共享**:应用层协议和 mcumgr 在不同阶段使用同一 UART,不会冲突
6. **DFU 开关**:通过 `CONFIG_APP_DFU`(Kconfig)控制,默认启用;设为 n 可完全移除 DFU 功能
---
## 11. 文件清单
```
app/app_photomagnetic/
├── sysbuild.conf # [新建] 启用 MCUboot
├── sysbuild/
│ ├── mcuboot.conf # [新建] MCUboot 配置
│ └── mcuboot.overlay # [新建] MCUboot 设备树(当前为空)
├── include/
│ └── com.hpp # [修改] 新增 0xFF 升级命令处理
├── src/
│ └── dfu.cpp # [新建] DFU 模块初始化
└── doc/
├── 固件升级方案.md # [新建] 详细方案文档
└── 升级更改说明.md # [新建] 本文档
```

View File

@ -1,382 +0,0 @@
# 光磁头在线固件升级方案
> 方案:MCUboot Serial Recovery + MCUmgr SMP over UART
> 约束:复用 usart1 协议口(0x7EE7 帧),应用层与 bootloader 阶段互斥使用,零冲突
---
## 1. 方案选型
### 为什么选 MCUboot Serial Recovery
| 对比项 | 应用层 SMP Server | MCUboot Serial Recovery ✅ |
|---|---|---|
| 与 0x7EE7 协议 | ⚠️ 冲突 — `uart_mcumgr` 接管 usart1 RX 中断 | ✅ 不冲突 — 两个阶段互斥使用 |
| 应用层改动 | 需要加 mcumgr 模块 + 写 UART 分发层 | 只加一个"进入升级"命令(~20 行) |
| 签名验证 | ✅ | ✅ MCUboot 内置,swap 前强制校验 |
| 需要重启 | ❌ | ✅ 升级场景可接受 |
| 回滚保护 | 依赖应用层实现 | ✅ MCUboot 内置(test 模式未确认自动回滚) |
**结论**:应用层不运行 mcumgr,避免 UART 冲突;升级时重启进入 MCUboot serial recovery,usart1 由 MCUboot 独占运行 mcumgr 协议。
---
## 2. 升级流程
```mermaid
sequenceDiagram
participant MB as 主板
participant PH as 光磁头(应用层)
participant BOOT as MCUboot(serial recovery)
participant CLI as mcumgr CLI
Note over PH: 正常运行(usart1 = 0x7EE7 协议)
MB->>PH: 升级命令 [7E E7][FF][01][01][CRC]
PH->>PH: bootmode_set(BOOTLOADER)
PH->>PH: sys_reboot()
Note over PH: 复位 → MCUboot 接管 usart1
BOOT-->>MB: MCUboot serial recovery 就绪
MB->>CLI: 切换到 mcumgr 客户端模式
CLI->>BOOT: mcumgr image upload zephyr.signed.bin
Note over BOOT: MCUboot 写入 slot1 + 验证签名
CLI->>BOOT: mcumgr image test <hash>
CLI->>BOOT: mcumgr reset
Note over BOOT: MCUboot 校验签名 → swap slot0 ↔ slot1
Note over PH: 重启 → 应用层运行新固件
PH-->>MB: 上电发 ID 帧(新固件版本)
```
---
## 3. 协议扩展(嵌入现有 0x7EE7 帧)
### 新增指令:升级命令 `0xFF`
| 指令 | 方向 | 数据 | 行为 |
|---|---|---|---|
| `0xFF` 子命令 `0x01` | 主板→小板 | — | 进入升级模式(复位进 bootloader) |
| `0xFF` 子命令 `0x02` | 主板→小板 | — | 查询升级状态 |
| `0xFF` 子命令 `0x02` | 小板→主板 | 1 字节 | 0x00=应用层正常运行 |
### 帧格式
```
进入升级模式:
主板 → 小板: [7E E7] [FF] [01] [01] [CRC_L] [CRC_H]
小板: 无回复(直接复位)
查询升级状态:
主板 → 小板: [7E E7] [FF] [02] [00] [CRC_L] [CRC_H]
小板 → 主板: [7E E7] [FF] [02] [01] [00] [CRC_L] [CRC_H]
```
---
## 4. 实现步骤
### 4.1 Flash 分区(已就绪,无需修改)
`boards/dr2501a/dr2501a_g0b0ce.dts` 中已有分区:
| 分区 | 偏移 | 大小 | 用途 |
|---|---|---|---|
| boot_partition | 0x00000 | 48KB | MCUboot bootloader |
| slot0_partition | 0x0C000 | 200KB | 应用镜像(当前运行) |
| slot1_partition | 0x3E000 | 200KB | 升级镜像(待 swap) |
| storage_partition | 0x70000 | 64KB | NVS 存储 |
> slot0 和 slot1 大小相同(200KB),满足 MCUboot swap 模式要求。
### 4.2 Sysbuild 配置
**`app/app_photomagnetic/sysbuild.conf`**(新建):
```
# 启用 MCUboot bootloader
SB_CONFIG_BOOTLOADER_MCUBOOT=y
# 固件签名密钥(开发阶段用 MCUboot 自带密钥,量产替换为自生成密钥)
# 路径相对于应用目录(${APP_DIR}),sysbuild 会自动传播到 MCUboot 和应用
SB_CONFIG_BOOT_SIGNATURE_KEY_FILE="${ZEPHYR_BASE}/../bootloader/mcuboot/root-rsa-2048.pem"
```
> **注意**:`SB_CONFIG_BOOT_SIGNATURE_KEY_FILE` 会自动传播到 MCUboot(`CONFIG_BOOT_SIGNATURE_KEY_FILE`)和应用(`CONFIG_MCUBOOT_SIGNATURE_KEY_FILE`),无需在 mcuboot.conf 和 prj.conf 中重复配置。
### 4.3 MCUboot 配置
**`app/app_photomagnetic/sysbuild/mcuboot.conf`**(新建):
```
# ── Serial Recovery 配置 ──
CONFIG_MCUBOOT_SERIAL=y
CONFIG_BOOT_SERIAL_UART=y
# 通过 retention boot mode 触发(serial recovery 不依赖 GPIO 按钮)
CONFIG_BOOT_SERIAL_BOOT_MODE=y
# MCUboot 等待 DFU 命令超时(ms),作为备用触发方式
CONFIG_BOOT_SERIAL_WAIT_FOR_DFU=y
CONFIG_BOOT_SERIAL_WAIT_FOR_DFU_TIMEOUT=3000
# 喂狗(避免升级过程中看门狗复位)
CONFIG_BOOT_WATCHDOG_FEED=y
# ── 签名验证 ──
CONFIG_BOOT_SIGNATURE_TYPE_RSA=y
# ── 优化 ──
CONFIG_BOOT_ERASE_PROGRESSIVELY=y
# ── 防降级(可选,量产启用) ──
# CONFIG_BOOT_DOWNGRADE_PREVENTION=y
# ── 日志 ──
CONFIG_MCUBOOT_LOG_LEVEL_WRN=y
```
> **关键配置说明**:
> - `CONFIG_BOOT_SERIAL_BOOT_MODE=y`:MCUboot 检查 retention 中的 boot mode 标志,若为 `BOOTLOADER` 则进入 serial recovery
> - `CONFIG_BOOT_SERIAL_WAIT_FOR_DFU=y`:MCUboot 启动后等待 3 秒看是否有 DFU 命令,作为备用触发方式
> - `CONFIG_BOOT_WATCHDOG_FEED=y`:MCUboot 在擦除/写入 flash 时喂狗,避免看门狗复位
**`app/app_photomagnetic/sysbuild/mcuboot.overlay`**(新建,指定 serial recovery 使用 usart1):
```dts
/ {
chosen {
/* MCUboot serial recovery 使用 usart1(与应用层协议口同一物理 UART) */
zephyr,console = <&usart1>;
};
};
```
> **注意**:不需要删除 usart1 上的 `uart-com` 节点 — MCUboot 构建时只使用 `zephyr,console` 指定的 UART,不会加载应用层的协议节点。
### 4.4 应用层 Kconfig
**`app/app_photomagnetic/prj.conf`**(追加):
```
# Boot mode retention(应用层触发进入 bootloader)
CONFIG_RETENTION_BOOT_MODE=y
CONFIG_RETENTION=y
```
### 4.5 应用层设备树
**`app/app_photomagnetic/boards/use_ms.overlay`**(追加 boot mode 节点):
```dts
/* Boot mode retention:复位后 MCUboot 检查此标志决定是否进入 serial recovery */
/ {
sram@2003FFFF {
compatible = "zephyr,memory-region", "mmio-sram";
reg = <0x2003FFFF 0x1>;
zephyr,memory-region = "RetainedMem";
status = "okay";
retainedmem {
compatible = "zephyr,retained-ram";
status = "okay";
#address-cells = <1>;
#size-cells = <1>;
retention0: retention@0 {
compatible = "zephyr,retention";
status = "okay";
reg = <0x0 0x1>;
};
};
};
chosen {
zephyr,boot-mode = <&retention0>;
};
};
```
> **注意**:需根据 STM32G0B0 实际 SRAM 大小调整 `sram0``reg`,确保 retention 节点地址在 sram0 范围末尾且不被 `.bss` 清零覆盖。
### 4.6 应用层代码变更
**`src/com.cpp`**:新增升级命令回调
```cpp
#include <zephyr/retention/bootmode.h>
#include <zephyr/sys/reboot.h>
namespace {
/// 升级命令处理(0xFF)
void OnUpgradeCommand(uart_com::DataType data) {
if (data.size() < 1) return;
switch (data[0]) {
case 0x01: // 进入升级模式
printk("[com] entering upgrade mode, rebooting...\n");
k_msleep(100); // 等待最后一帧发送完成
if (bootmode_set(BOOT_MODE_TYPE_BOOTLOADER) == 0) {
sys_reboot(SYS_REBOOT_COLD);
}
break;
case 0x02: // 查询升级状态
{
uint8_t status = 0x00; // 0x00 = 应用层正常运行
Proto().Send(0xFF, uart_com::DataType(&status, 1));
}
break;
}
}
} // namespace
```
**回调表**:增加 `0xFF` 条目
```cpp
constexpr std::pair<const uint8_t, uart_com::WriteCallback> kRxTable[] = {
{static_cast<uint8_t>(Cmd::kTemp), OnTempQuery},
{static_cast<uint8_t>(Cmd::kId), OnGetId},
{static_cast<uint8_t>(Cmd::kRunningState), OnRunningState},
{0xFF, OnUpgradeCommand}, // 新增:升级命令
};
```
---
## 5. 签名密钥管理
### 5.1 开发阶段
使用 MCUboot 自带的开发密钥(不安全,仅用于开发):
```
${ZEPHYR_BASE}/../bootloader/mcuboot/root-rsa-2048.pem
```
sysbuild.conf 中已配置,无需额外操作。构建时 sysbuild 会自动:
- 用私钥签名应用镜像 → `zephyr.signed.bin`
- 将公钥烘焙进 MCUboot bootloader
### 5.2 量产阶段
生成自定义密钥对:
```bash
# 生成 RSA-2048 密钥对
pip install imgtool
imgtool keygen -k keys/my-signing-key.pem -t rsa-2048
# 更新 sysbuild.conf 中的密钥路径
# SB_CONFIG_BOOT_SIGNATURE_KEY_FILE="${APP_DIR}/keys/my-signing-key.pem"
```
> ⚠️ **私钥必须安全保管**,丢失后无法签名新固件,设备将无法升级。
### 5.3 签名验证流程
```
构建时:imgtool 用私钥签名 zephyr.bin → zephyr.signed.bin
运行时:MCUboot 用烘焙进 bootloader 的公钥验证签名
- 验证通过 → swap
- 验证失败 → 拒绝 swap,继续运行原固件
```
---
## 6. 构建与烧录
### 6.1 首次构建(MCUboot + 应用)
```bash
west build -p auto -b dr2501a_g0b0ce/stm32g0b0xx \
--sysbuild app/app_photomagnetic \
-d build \
-DOVERLAY_CONFIG=boards/use_ms.overlay
```
构建产物:
- `build/mcuboot/zephyr/zephyr.bin` — MCUboot bootloader
- `build/app_photomagnetic/zephyr/zephyr.signed.bin` — 签名后的应用固件(用于 mcumgr 上传)
### 6.2 首次烧录(需要调试器)
```bash
# 烧录 MCUboot + 应用(一次性)
west flash -d build
```
或手动烧录 merged.hex(MCUboot + 应用合并镜像)。
### 6.3 后续升级(通过 UART,不需要调试器)
```bash
# 主板发送升级命令后,切换到 mcumgr 模式
mcumgr --conntype=serial --connstring='dev=/dev/ttyUSBx,baud=115200' \
image upload -e build/app_photomagnetic/zephyr/zephyr.signed.bin
# 查看镜像列表
mcumgr --conntype=serial --connstring='dev=/dev/ttyUSBx,baud=115200' \
image list
# 标记待测试
mcumgr --conntype=serial --connstring='dev=/dev/ttyUSBx,baud=115200' \
image test <hash>
# 复位触发 swap
mcumgr --conntype=serial --connstring='dev=/dev/ttyUSBx,baud=115200' \
reset
# 确认新固件(可选,test 模式未确认会自动回滚)
mcumgr --conntype=serial --connstring='dev=/dev/ttyUSBx,baud=115200' \
image confirm <hash>
```
---
## 7. 安全机制
| 机制 | 说明 |
|---|---|
| **镜像签名** | RSA-2048 签名验证,公钥烘焙进 bootloader,防止未授权固件 |
| **回滚保护** | `CONFIG_BOOT_DOWNGRADE_PREVENTION=y`(量产启用),禁止降级 |
| **未确认自动回滚** | test 模式下重启未确认 → MCUboot 自动回滚到原固件 |
| **升级中断保护** | 升级中断电 → MCUboot 回滚,设备不会变砖 |
| **Boot mode flag** | SRAM retention,复位后 MCUboot 检查标志进入 serial recovery |
---
## 8. 主板端配合
主板在发送"进入升级模式"命令后:
1. 等待 200ms(小板复位时间)
2. 切换到 mcumgr 客户端模式(波特率不变,仍是 115200)
3. 使用 mcumgr 协议上传固件
4. 等待新固件启动(小板上电发 ID 帧)
5. 切回 0x7EE7 协议模式
---
## 9. 工作量评估
| 任务 | 工作量 |
|---|---|
| sysbuild.conf + mcuboot.conf + mcuboot.overlay | 配置文件,~30 分钟 |
| boot mode retention 设备树节点 | overlay 追加,~15 分钟 |
| com.cpp 新增升级命令回调 | ~20 行代码,~30 分钟 |
| 首次构建验证 MCUboot + 应用 | 调试+验证,~2 小时 |
| mcumgr CLI 升级流程端到端测试 | ~1 小时 |
| 主板端配合(切换模式逻辑) | 主板侧工作,单独评估 |
**总计**:应用层侧约半天工作量(含调试);主板侧需额外配合。
---
## 10. 注意事项
1. **SRAM retention**:STM32G0B0 的 SRAM 复位后默认不清零(retained),boot mode flag 利用此特性;需确保 retention 地址不被 `.bss` 清零覆盖
2. **MCUboot 大小**:48KB 对 MCUboot 启用 RSA 签名偏紧,如溢出需调整 boot 分区(可能需要重划分 flash)
3. **首次烧录**:需要调试器烧录 MCUboot + 应用(一次性);后续升级全部通过 UART
4. **usart1 波特率**:应用层协议 115200,MCUboot serial recovery 也是 115200,无需切换
5. **降级保护**:开发阶段建议关闭(`CONFIG_BOOT_DOWNGRADE_PREVENTION=n`),方便回退;量产启用
6. **签名密钥路径**:sysbuild.conf 中的 `SB_CONFIG_BOOT_SIGNATURE_KEY_FILE` 会自动传播到 MCUboot 和应用,无需在 mcuboot.conf 和 prj.conf 中重复配置

View File

@ -19,28 +19,6 @@
- 0x01运行中 - 0x01运行中
- 0x02暂停中 - 0x02暂停中
- 0x03故障中 - 0x03故障中
- 写UID0x07指令生产烧录自定义UID替代MCU芯片UID
- 主板发 0x[7ee7][07][0a][uid 10字节][crc16]
- 将10字节UID写入小板storage分区flash掉电不丢失
- 写入后0x01读ID指令返回的是该UID
- UID编码规则见《UID编码规则.md》
`[设备类型 1B][产品代次 1B][生产年月 4B][序列号 4B]`
- 主板发 0x[7ee7][07][00][crc16]
- 清除自定义UID恢复使用MCU芯片UID
- 小板回 0x[7ee7][07][01][状态][crc16]
- 状态值定义:
- 0x00成功
- 0x01写入失败flash擦除/写入/校验错误)
- 0x02长度错误UID必须为10字节或0字节=清除)
- 说明UID在storage分区0x7000064KB起始处记录格式
`[magic "UID1" 4B][ver 1B][uid 10B][crc16 2B][pad 7B]`
含CRC16-Modbus校验损坏/未写入时自动回退芯片UID
- 灯带调色(调试)0x08指令仅调试固件 CONFIG_APP_DEBUG_LED_STRIP=y 时存在,
生产固件不编译此命令)
- 主板发 0x[7ee7][08][03][R][G][B][crc16]
- 灯带整体设为该 RGB 颜色R/G/B 各 0~255
- [0,0,0] 即熄灭
- 用于开发/产线检查灯光颜色是否满足需求
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
@ -67,9 +45,4 @@ Note over S: 系统上电
S -->> M: 回复 温度 数据 S -->> M: 回复 温度 数据
end end
Note over M, S: 生产烧录 UID (0x07 指令)
M ->> S: 写入 12 字节 UID (0x07 指令帧)
S -->> M: 回复状态 (0x00 成功)
M ->> S: 请求 ID 验证 (0x01 指令帧)
S -->> M: 回复写入后的 UID
``` ```

View File

@ -5,17 +5,11 @@
#include <etl/delegate.h> #include <etl/delegate.h>
#include <etl/signal.h> #include <etl/signal.h>
#include <led_strip_indicator/led_strip_indicator.hpp> #include <led_strip_indicator/led_strip_indicator.hpp>
#include <stdio.h>
#include <uart_com/simple_protocal.hpp> #include <uart_com/simple_protocal.hpp>
#include <uid.hpp>
#include <zephyr/app_version.h>
#include <zephyr/drivers/hwinfo.h> #include <zephyr/drivers/hwinfo.h>
#include <zephyr/drivers/sensor.h> #include <zephyr/drivers/sensor.h>
#include <zpp/driver.hpp> #include <zpp/driver.hpp>
#include <zpp/error.hpp> #include <zpp/error.hpp>
#ifdef CONFIG_APP_DFU
#include <zephyr/sys/reboot.h>
#endif
namespace ther { namespace ther {
class Com { class Com {
@ -24,9 +18,8 @@ public:
using RunningStateSignal = etl::signal<void(HostStatus), 10>; using RunningStateSignal = etl::signal<void(HostStatus), 10>;
static auto Init() -> zpp::error { static auto Init() -> zpp::error {
s_proto->SetRxCallbackTable(kRxCallbackTable); s_proto->SetRxCallbackTable(kRxCallbackTable);
// 优先使用生产写入的自定义 UID,否则回退芯片 UID auto size = hwinfo_get_device_id(buff, sizeof(buff));
Uid::Init(); s_id_buff = etl::span<uint8_t>(buff, size);
s_id_buff = Uid::Get();
return zpp::ok(); return zpp::ok();
} }
static auto SendTemp(sensor_value val) -> void { static auto SendTemp(sensor_value val) -> void {
@ -36,28 +29,14 @@ public:
static auto AddRunningState(RunningStateSignal::slot_type slot) -> bool { static auto AddRunningState(RunningStateSignal::slot_type slot) -> bool {
return s_running_state_sig.connect(slot); return s_running_state_sig.connect(slot);
} }
#ifdef CONFIG_APP_DEBUG_LED_STRIP
/// 调试灯带控制(0x08):仅调试固件(CONFIG_APP_DEBUG_LED_STRIP=y)支持,
/// 生产固件不编译此命令。订阅后收到命令回调 (r, g, b)。
using LedColorSignal = etl::signal<void(uint8_t, uint8_t, uint8_t), 4>;
static auto AddLedColor(LedColorSignal::slot_type slot) -> bool {
return s_led_color_sig.connect(slot);
}
#endif
private: private:
enum Addr : uint8_t { enum Addr : uint8_t {
R_TEMP = 0, R_TEMP = 0,
R_GET_ID, R_GET_ID,
W_RUNNING_STATE, W_RUNNING_STATE,
W_UID = 0x07,
R_VERSION = 0x06,
W_LED = 0x08,
}; };
inline static RunningStateSignal s_running_state_sig{}; inline static RunningStateSignal s_running_state_sig{};
#ifdef CONFIG_APP_DEBUG_LED_STRIP
inline static LedColorSignal s_led_color_sig{};
#endif
static auto RunningState(uart_com::DataType data) -> void { static auto RunningState(uart_com::DataType data) -> void {
if (data.size() != 1) { if (data.size() != 1) {
return; return;
@ -68,83 +47,19 @@ private:
printk("handle get id"); printk("handle get id");
s_proto->Send(R_GET_ID, uart_com::DataType(s_id_buff)); s_proto->Send(R_GET_ID, uart_com::DataType(s_id_buff));
}; };
static auto GetVersion(uart_com::DataType data) -> void {
// Send version string: major.minor.patchlevel
static char ver_buf[16];
snprintf(ver_buf, sizeof(ver_buf), "%d.%d.%d", APP_VERSION_MAJOR,
APP_VERSION_MINOR, APP_PATCHLEVEL);
printk("[com] GetVersion: %s\n", ver_buf);
s_proto->Send(R_VERSION, uart_com::DataType((const uint8_t *)ver_buf,
strlen(ver_buf)));
};
/// 写 UID(0x07):data 为 10 字节生产 UID → 写入 flash;data 为空 →
/// 清除记录(恢复芯片 UID)。 回 ACK:0x00=成功 0x01=写入失败 0x02=长度错误
static auto WriteUid(uart_com::DataType data) -> void {
uint8_t status = 0x02;
if (data.size() == Uid::kUidLen) {
status = (Uid::Write(data) == 0) ? 0x00 : 0x01;
} else if (data.empty()) {
status = (Uid::Write({}) == 0) ? 0x00 : 0x01;
}
if (status == 0x00) {
// 立即生效:后续 GET_ID(0x01)返回新 UID,无需等重启
s_id_buff = Uid::Get();
}
printk("[com] WriteUid len=%zu status=0x%02x\n", data.size(), status);
s_proto->Send(W_UID, uart_com::DataType(&status, 1));
};
#ifdef CONFIG_APP_DEBUG_LED_STRIP
/// 调试灯带(0x08):data = [R][G][B] → 灯带整体改色,用于开发/产线检查
/// 灯光颜色是否满足需求。(0,0,0) 即熄灭。生产固件不含此命令。
static auto SetLed(uart_com::DataType data) -> void {
if (data.size() != 3) {
printk("[com] SetLed: expect 3 bytes (R G B), got %zu\n", data.size());
return;
}
printk("[com] SetLed R=%u G=%u B=%u\n", data[0], data[1], data[2]);
s_led_color_sig(data[0], data[1], data[2]);
}
#endif
#ifdef CONFIG_APP_DFU
/// 升级命令(0xFF):data[0]=0x01 进入升级模式,data[0]=0x02 查询状态
static auto DfuCommand(uart_com::DataType data) -> void {
if (data.size() < 1)
return;
switch (data[0]) {
case 0x01:
printk("[com] entering DFU mode, rebooting...\n");
k_msleep(100);
sys_reboot(SYS_REBOOT_COLD);
break;
case 0x02: {
uint8_t status = 0x00;
s_proto->Send(0xFF, uart_com::DataType(&status, 1));
break;
}
}
}
#endif
constexpr static std::pair<const uint8_t, constexpr static std::pair<const uint8_t,
uart_com::SimpleProtocal::CallbackType> uart_com::SimpleProtocal::CallbackType>
kRxCallbackTable[] = { kRxCallbackTable[] = {
{R_GET_ID, GetId}, {W_RUNNING_STATE, RunningState}, {R_GET_ID, GetId},
{R_VERSION, GetVersion}, {W_UID, WriteUid}, {W_RUNNING_STATE, RunningState},
#ifdef CONFIG_APP_DEBUG_LED_STRIP
{W_LED, SetLed},
#endif
#ifdef CONFIG_APP_DFU
{0xFF, DfuCommand},
#endif
}; };
inline static auto s_proto = (uart_com::SimpleProtocal *)DEVICE_DT_GET( inline static auto s_proto = (uart_com::SimpleProtocal *)DEVICE_DT_GET(
DT_COMPAT_GET_ANY_STATUS_OKAY(uart_com_simple_protocal)); DT_COMPAT_GET_ANY_STATUS_OKAY(uart_com_simple_protocal));
inline static etl::span<const uint8_t> s_id_buff; inline static uint8_t buff[20];
inline static auto s_heating_state = false; inline static auto s_heating_state = false;
inline static etl::span<uint8_t> s_id_buff;
}; };
} // namespace ther } // namespace ther

View File

@ -1,56 +0,0 @@
#ifndef __THER_UID_HPP__
#define __THER_UID_HPP__
#include <etl/span.h>
#include <stddef.h>
#include <stdint.h>
namespace ther {
/// 生产 UID 存储:优先使用写入 storage 分区的自定义 UID,否则回退 MCU 芯片 UID。
///
/// UID 共 10 字节(生产编码,见 doc/UID编码规则.md)
/// [0] 设备类型
/// [1] 产品代次
/// [2..6) 生产年月(4 字节)
/// [6..10) 唯一序列号(4 字节)
///
/// 存储记录(24 字节,位于 storage 分区起始,与 8 字节写对齐)
/// [0..4] magic "UID1"
/// [4] ver 0x02
/// [5..15) uid 10 字节
/// [15..17) crc16 CRC16-Modbus([0..15)),LSB 在前
/// [17..24) pad 0xFF
class Uid {
public:
static constexpr size_t kUidLen = 10; ///< 生产 UID 长度
static constexpr size_t kChipUidLen =
12; ///< MCU 芯片 UID 长度(STM32G0 96bit)
static constexpr size_t kRecordLen = 24;
/// 上电读取:存在有效存储记录则使用之,否则回退芯片 UID。
static auto Init() -> void;
/// 当前生效的 UID(10 字节)。
static auto Get() -> etl::span<const uint8_t>;
/// 是否使用自定义存储 UID(否则为芯片 UID)。
static auto IsCustom() -> bool;
/// 写入自定义 UID(必须 10 字节);空 span 表示清除记录(恢复芯片 UID)。
/// 返回 0 成功,负数为错误码。
static auto Write(etl::span<const uint8_t> uid) -> int;
private:
static auto LoadRecord() -> void;
static auto RecordValid(const uint8_t *rec) -> bool;
static auto EraseRecord() -> int;
static auto SaveRecord(const uint8_t *rec) -> int;
inline static uint8_t s_chip_uid[kChipUidLen]{};
inline static size_t s_chip_uid_len = 0;
inline static uint8_t s_custom_uid[kUidLen]{};
inline static etl::span<const uint8_t> s_active{};
inline static bool s_custom{false};
};
} // namespace ther
#endif // __THER_UID_HPP__

View File

@ -1,5 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgwlUCVXuz/+irsEXd
xU6IwCQCxnA42StQaD3Hd04QvnyhRANCAAQGpcUOBI1p35AGLNctUd50QL1OmeE3
2fPWMOf9byIXeQFnRTVAJkSo067+GMP5onCHOvx76lREg5IxLRZilfxR
-----END PRIVATE KEY-----

View File

@ -22,19 +22,9 @@ CONFIG_ADC_MCP320X_ACQUISITION_THREAD_STACK_SIZE=2048
CONFIG_REBOOT=y CONFIG_REBOOT=y
CONFIG_WATCHDOG=y CONFIG_WATCHDOG=y
CONFIG_PWM=y CONFIG_PWM=y
# # Debug logging # # Debug logging
# CONFIG_LOG=y # CONFIG_LOG=y
# CONFIG_LOG_MODE_IMMEDIATE=y # CONFIG_LOG_MODE_IMMEDIATE=y
# CONFIG_ADC_LOG_LEVEL_DBG=y # CONFIG_ADC_LOG_LEVEL_DBG=y
# CONFIG_SENSOR_LOG_LEVEL_DBG=y # CONFIG_SENSOR_LOG_LEVEL_DBG=y
# CONFIG_SPI_LOG_LEVEL_DBG=y # CONFIG_SPI_LOG_LEVEL_DBG=y
# ── MCUboot 引导支持(关键!)──
CONFIG_BOOTLOADER_MCUBOOT=y
# ── 镜像管理(补全依赖链,Nordic 教程 Step 5.2)──
CONFIG_FLASH=y
CONFIG_FLASH_MAP=y
CONFIG_STREAM_FLASH=y
CONFIG_IMG_MANAGER=y

View File

@ -1 +0,0 @@
pyserial==3.5

View File

@ -1,403 +0,0 @@
"""
SMP serial client for MCUboot serial recovery (in-process, no external tools).
Implements the mcumgr-over-UART transport used by MCUboot's boot_serial:
- frame: [0x06 0x09 | base64(payload) | 0x0a] (first frame)
[0x04 0x14 | base64(payload) | 0x0a] (continuation frames)
- payload: [len:2 BE][SMP packet:8B hdr + CBOR][crc16-ccitt:2 BE]
- CRC16-CCITT (poly 0x1021, init 0), appended BIG-endian (matches MCUboot,
which uses htons + crc16_ccitt(0, ...) and checks for a zero result).
MCUboot serial recovery always writes uploaded images to slot 0 (primary),
so no test/confirm marking is needed - upload then reset is the whole flow.
"""
import struct
import base64
import hashlib
import time
import sys
import serial
# mcumgr over UART framing constants (match MCUboot boot_serial)
SMP_HEADER = b"\x06\x09"
SMP_FRAG_HEADER = b"\x04\x14"
SMP_NEWLINE = b"\x0a"
FRAME_MTU = 124 # MCUboot BOOT_SERIAL_FRAME_MTU: 127 - 2 - 1
class SMPError(Exception):
"""SMP communication error."""
def crc16_ccitt(data: bytes, crc: int = 0) -> int:
"""CRC16-CCITT (poly 0x1021, init 0), same as Zephyr crc16_ccitt."""
for byte in data:
crc ^= byte << 8
for _ in range(8):
if crc & 0x8000:
crc = ((crc << 1) ^ 0x1021) & 0xFFFF
else:
crc = (crc << 1) & 0xFFFF
return crc
# ── CBOR encoding (requests) ───────────────────────────────────
def cbor_uint(val: int) -> bytes:
if val < 24:
return bytes([val])
elif val < 256:
return bytes([0x18, val])
elif val < 65536:
return bytes([0x19, val >> 8, val & 0xFF])
else:
return bytes([0x1a, (val >> 24) & 0xFF, (val >> 16) & 0xFF,
(val >> 8) & 0xFF, val & 0xFF])
def cbor_bytes(data: bytes) -> bytes:
n = len(data)
if n < 24:
return bytes([0x40 + n]) + data
elif n < 256:
return bytes([0x58, n]) + data
else:
return bytes([0x59, n >> 8, n & 0xFF]) + data
def cbor_str(s: str) -> bytes:
b = s.encode('utf-8')
n = len(b)
if n < 24:
return bytes([0x60 + n]) + b
elif n < 256:
return bytes([0x78, n]) + b
else:
return bytes([0x79, n >> 8, n & 0xFF]) + b
# ── CBOR decoding (responses) ──────────────────────────────────
def _cbor_decode(data: bytes, off: int = 0):
"""Decode one CBOR item. Returns (value, new_offset).
Supports unsigned ints, byte/str strings, maps and arrays - enough
for MCUboot boot_serial responses ({rc, off} maps and image lists).
"""
if off >= len(data):
raise SMPError("CBOR: truncated data")
b = data[off]
off += 1
if b <= 0x17:
return b, off
if b == 0x18:
return data[off], off + 1
if b == 0x19:
return struct.unpack('>H', data[off:off + 2])[0], off + 2
if b == 0x1a:
return struct.unpack('>I', data[off:off + 4])[0], off + 4
if 0x40 <= b <= 0x5b: # byte string
if b <= 0x57:
n = b - 0x40
elif b == 0x58:
n = data[off]; off += 1
elif b == 0x59:
n = struct.unpack('>H', data[off:off + 2])[0]; off += 2
else:
n = struct.unpack('>I', data[off:off + 4])[0]; off += 4
return data[off:off + n], off + n
if 0x60 <= b <= 0x7b: # text string
if b <= 0x77:
n = b - 0x60
elif b == 0x78:
n = data[off]; off += 1
elif b == 0x79:
n = struct.unpack('>H', data[off:off + 2])[0]; off += 2
else:
n = struct.unpack('>I', data[off:off + 4])[0]; off += 4
return data[off:off + n].decode('utf-8', 'replace'), off + n
if 0x80 <= b <= 0x9b: # array
if b <= 0x97:
n = b - 0x80
elif b == 0x98:
n = data[off]; off += 1
elif b == 0x99:
n = struct.unpack('>H', data[off:off + 2])[0]; off += 2
else:
n = struct.unpack('>I', data[off:off + 4])[0]; off += 4
out = []
for _ in range(n):
v, off = _cbor_decode(data, off)
out.append(v)
return out, off
if 0xa0 <= b <= 0xbb: # map
if b <= 0xb7:
n = b - 0xa0
elif b == 0xb8:
n = data[off]; off += 1
elif b == 0xb9:
n = struct.unpack('>H', data[off:off + 2])[0]; off += 2
else:
n = struct.unpack('>I', data[off:off + 4])[0]; off += 4
out = {}
for _ in range(n):
k, off = _cbor_decode(data, off)
v, off = _cbor_decode(data, off)
out[k] = v
return out, off
raise SMPError(f"CBOR: unsupported type byte 0x{b:02x}")
class SMPClient:
"""Minimal, verified SMP serial client for MCUboot serial recovery."""
OP_READ = 0
OP_READ_RSP = 1
OP_WRITE = 2
OP_WRITE_RSP = 3
GROUP_OS = 0
GROUP_IMAGE = 1
IMAGE_STATE_READ = 0
IMAGE_UPLOAD = 1
IMAGE_STATE_WRITE = 2
OS_RESET = 5
def __init__(self, port: str, baudrate: int = 115200, log=None):
self.port = port
self.baudrate = baudrate
self.ser: serial.Serial | None = None
self._log = log or (lambda m: print(f"[SMP] {m}", file=sys.stderr))
# ── connection ──────────────────────────────────────────
def open(self):
self.ser = serial.Serial(self.port, self.baudrate, timeout=0.1)
# 清掉设备刚重启时可能残留的字节
time.sleep(0.2)
self.ser.reset_input_buffer()
self._log(f"opened {self.port} @ {self.baudrate}")
def close(self):
if self.ser:
try:
self.ser.close()
except Exception:
pass
self.ser = None
# ── packet / frame building ─────────────────────────────
def _build_smp_packet(self, op: int, group: int, cmd: int,
payload: bytes = b"", seq: int = 0) -> bytes:
"""SMP packet: 8B header + CBOR payload + 2B BE CRC, wrapped in
[len:2 BE] transport prefix."""
hdr = bytearray(8)
hdr[0] = op
hdr[1] = 0 # flags
hdr[2:4] = struct.pack('>H', len(payload) + 8)
hdr[4:6] = struct.pack('>H', group)
hdr[6] = seq
hdr[7] = cmd
packet = bytes(hdr) + payload
crc = crc16_ccitt(packet)
return struct.pack('>H', len(packet) + 2) + packet + struct.pack('>H', crc)
def _send_frame(self, data: bytes):
"""Base64-encode and send as mcumgr UART frame(s) with fragmentation."""
encoded = base64.b64encode(data)
offset = 0
first = True
while offset < len(encoded):
chunk = encoded[offset:offset + FRAME_MTU]
header = SMP_HEADER if first else SMP_FRAG_HEADER
first = False
self.ser.write(header + chunk + SMP_NEWLINE)
offset += len(chunk)
self.ser.flush()
# ── response receiving ──────────────────────────────────
def _recv_packet(self, timeout: float = 10.0) -> bytes:
"""Read one complete decoded SMP packet (transport payload without
the 2-byte length prefix / 2-byte CRC)."""
start = time.time()
buf = bytearray()
while time.time() - start < timeout:
if self.ser and self.ser.in_waiting:
buf.extend(self.ser.read(self.ser.in_waiting))
while b'\n' in buf:
nl = buf.index(b'\n')
line = bytes(buf[:nl])
del buf[:nl + 1]
if not line:
continue
if line[:2] not in (SMP_HEADER, SMP_FRAG_HEADER):
self._log(f"skip non-SMP line: {line[:16].hex()}")
continue
encoded = line[2:]
try:
decoded = base64.b64decode(encoded, validate=True)
except Exception as e:
self._log(f"base64 decode failed: {e}")
continue
if len(decoded) < 12: # 2 len + 8 hdr + 2 crc
self._log("decoded too short")
continue
total_len = struct.unpack('>H', decoded[:2])[0]
if len(decoded) < 2 + total_len:
self._log(f"short packet: {len(decoded)} < {2 + total_len}")
continue
packet = decoded[2:2 + total_len - 2]
wire_crc = struct.unpack('>H', decoded[2 + total_len - 2:2 + total_len])[0]
if crc16_ccitt(packet) != wire_crc:
self._log("CRC mismatch")
continue
return packet
time.sleep(0.01)
raise SMPError(f"timeout waiting for SMP response ({timeout}s)")
def _request(self, op: int, group: int, cmd: int, payload: bytes = b"",
seq: int = 0, timeout: float = 10.0):
"""Send a request and return the response header + payload."""
self._send_frame(self._build_smp_packet(op, group, cmd, payload, seq))
resp = self._recv_packet(timeout)
if len(resp) < 8:
raise SMPError("response too short")
r_op = resp[0]
r_group = struct.unpack('>H', resp[4:6])[0]
r_seq = resp[6]
r_cmd = resp[7]
r_payload = resp[8:]
if r_op != (self.OP_READ_RSP if op == self.OP_READ else self.OP_WRITE_RSP):
self._log(f"unexpected op {r_op} for request op {op}")
return r_group, r_op, r_seq, r_cmd, r_payload
@staticmethod
def _rc(payload: bytes) -> int:
"""Extract 'rc' from a CBOR map response; missing means 0 (OK)."""
if not payload:
return 0
try:
val, _ = _cbor_decode(payload)
if isinstance(val, dict):
return int(val.get('rc', 0))
except SMPError:
pass
return 0
@staticmethod
def _off(payload: bytes):
"""Extract 'off' from a CBOR map response, or None."""
if not payload:
return None
try:
val, _ = _cbor_decode(payload)
if isinstance(val, dict) and 'off' in val:
return int(val['off'])
except SMPError:
pass
return None
# ── image upload (serial recovery writes slot 0 directly) ──
def upload_image(self, filepath: str, chunk_size: int = 512,
progress_callback=None) -> bool:
"""Upload a signed image to the device. MCUboot serial recovery writes
it to slot 0 (primary); after this call the caller should reset().
chunk_size MCUboot BOOT_SERIAL_MAX_RECEIVE_SIZE(1024)限制:
base64 行长不能超过 1024(本配置),所以块大小取 512 留足余量
progress_callback(offset, total) is called in the calling thread.
"""
with open(filepath, 'rb') as f:
image = f.read()
image_size = len(image)
sha = hashlib.sha256(image).digest()
offset = 0
seq = 0
while offset < image_size:
chunk = image[offset:offset + chunk_size]
payload = bytearray()
payload.append(0xa3) # map: off, data, len
payload.extend(cbor_str("off"))
payload.extend(cbor_uint(offset))
payload.extend(cbor_str("data"))
payload.extend(cbor_bytes(chunk))
payload.extend(cbor_str("len"))
payload.extend(cbor_uint(image_size))
if offset == 0:
payload[0] = 0xa4 # add sha
payload.extend(cbor_str("sha"))
payload.extend(cbor_bytes(sha))
_, _, _, _, r_payload = self._request(
self.OP_WRITE, self.GROUP_IMAGE, self.IMAGE_UPLOAD,
bytes(payload), seq=seq, timeout=20.0)
rc = self._rc(r_payload)
if rc != 0:
self._log(f"upload rejected at offset {offset}: rc={rc}")
return False
resp_off = self._off(r_payload)
if resp_off is None:
resp_off = offset + len(chunk)
if resp_off <= offset:
# MCUboot wants a retransmission from resp_off; go back.
offset = resp_off
else:
offset = resp_off
seq = (seq + 1) & 0xFF
if progress_callback:
progress_callback(min(offset, image_size), image_size)
return offset >= image_size
def image_state_write(self, hash_hex: str, confirm: bool = False) -> bool:
"""Mark an image for test/confirm swap (only needed for slot1 uploads;
serial recovery writes slot0 directly so this is normally unused)."""
payload = bytearray()
payload.append(0xa2)
payload.extend(cbor_str("hash"))
payload.extend(cbor_bytes(bytes.fromhex(hash_hex)))
payload.extend(cbor_str("confirm"))
payload.extend(cbor_uint(1 if confirm else 0))
_, _, _, _, r_payload = self._request(
self.OP_WRITE, self.GROUP_IMAGE, self.IMAGE_STATE_WRITE,
bytes(payload), timeout=10.0)
return self._rc(r_payload) == 0
def image_state_read(self) -> list:
"""Read image list. Returns list of dicts (slot, version, flags, hash)."""
_, _, _, _, r_payload = self._request(
self.OP_READ, self.GROUP_IMAGE, self.IMAGE_STATE_READ, timeout=10.0)
if self._rc(r_payload) != 0:
raise SMPError(f"image state read failed rc={self._rc(r_payload)}")
val, _ = _cbor_decode(r_payload)
images = []
if isinstance(val, dict) and 'images' in val and isinstance(val['images'], list):
for entry in val['images']:
if not isinstance(entry, dict):
continue
images.append({
'slot': int(entry.get('slot', -1)),
'version': entry.get('version', '?'),
'flags': entry.get('flags', 0),
'hash': entry.get('hash', b''),
})
return images
def reset(self, timeout: float = 5.0) -> bool:
"""Reset the device (MCUboot replies, then reboots)."""
self._send_frame(self._build_smp_packet(
self.OP_WRITE, self.GROUP_OS, self.OS_RESET))
# MCUboot bs_reset replies with {rc:0} then reboots; read it.
try:
resp = self._recv_packet(timeout)
r_payload = resp[8:] if len(resp) >= 8 else b""
return self._rc(r_payload) == 0
except SMPError:
# Some builds reset without replying; treat as success.
return True

View File

@ -9,15 +9,8 @@ from tkinter import ttk, scrolledtext, messagebox
import threading import threading
import time import time
import sys import sys
import os
import struct
import base64
from enum import IntEnum from enum import IntEnum
# Add scripts directory to path for smp_client import
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from smp_client import SMPClient
try: try:
import serial import serial
import serial.tools.list_ports import serial.tools.list_ports
@ -35,39 +28,12 @@ class Cmd(IntEnum):
RUNNING_STATE = 0x02 RUNNING_STATE = 0x02
W_HEATING = 0x03 W_HEATING = 0x03
R_HEATING = 0x04 R_HEATING = 0x04
READ_VERSION = 0x06
WRITE_UID = 0x07
SET_LED = 0x08 # 调试用:灯带 RGB(仅调试固件支持)
READ_POLE_NTC = 0x64 READ_POLE_NTC = 0x64
READ_HEATING_NTC = 0x65 READ_HEATING_NTC = 0x65
HEATING_STATE_NAMES = {0: "OFF", 1: "ON"} HEATING_STATE_NAMES = {0: "OFF", 1: "ON"}
STATE_NAMES = {0: "Standby", 1: "Running", 2: "Pause", 3: "Error"} STATE_NAMES = {0: "Standby", 1: "Running", 2: "Pause", 3: "Error"}
# UID 编码规则(与 doc/UID编码规则.md 一致)
UID_TYPE_NAMES = {
0x01: "冲击波",
0x02: "光磁/热疗",
0x03: "外置按摩头",
0xF0: "通用(未分类)",
0xFF: "未知",
}
UID_DEV_NAMES = {
(0x01, 0x01): "标准冲击波手柄",
(0x01, 0x02): "高能型冲击波手柄",
(0x02, 0x01): "光磁手柄",
(0x02, 0x02): "热疗手柄",
(0x02, 0x03): "增强型光磁手柄",
(0x03, 0x01): "捶打按摩头",
(0x03, 0x02): "揉捏按摩头",
(0x03, 0x03): "热敷按摩头",
(0x03, 0x04): "EMS 电磁按摩头",
(0x03, 0x05): "DMS 深层肌肉刺激仪",
(0x03, 0x06): "负压吸力按摩头",
(0x03, 0xF0): "通用按摩头(未分类)",
(0x03, 0xFF): "未知设备(自动探测)",
}
# ── Protocol helpers (from test.py) ─────────────────────────── # ── Protocol helpers (from test.py) ───────────────────────────
def crc16_modbus(data: bytes) -> int: def crc16_modbus(data: bytes) -> int:
@ -225,52 +191,10 @@ class SerialWorker:
# ── GUI Application ─────────────────────────────────────────── # ── GUI Application ───────────────────────────────────────────
def read_version_file(filepath):
"""Read Zephyr VERSION file and return version dict"""
version = {'major': 0, 'minor': 0, 'patchlevel': 0, 'tweak': 0, 'extraversion': ''}
try:
with open(filepath, 'r') as f:
for line in f:
line = line.strip()
if '=' not in line or line.startswith('#'):
continue
key, val = line.split('=', 1)
key = key.strip()
val = val.strip().strip('"').strip("'")
if key == 'VERSION_MAJOR':
version['major'] = int(val) if val else 0
elif key == 'VERSION_MINOR':
version['minor'] = int(val) if val else 0
elif key == 'PATCHLEVEL':
version['patchlevel'] = int(val) if val else 0
elif key == 'VERSION_TWEAK':
version['tweak'] = int(val) if val else 0
elif key == 'EXTRAVERSION':
version['extraversion'] = val
except (FileNotFoundError, ValueError):
pass
return version
def format_version(version):
"""Format version dict to string like '0.0.9'"""
v = f"{version['major']}.{version['minor']}.{version['patchlevel']}"
if version['tweak']:
v += f"+{version['tweak']}"
if version['extraversion']:
v += f"-{version['extraversion']}"
return v
class PhotomagneticGUI: class PhotomagneticGUI:
def __init__(self): def __init__(self):
# Read version from VERSION file
version_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'VERSION')
self._version = read_version_file(version_path)
self._version_str = format_version(self._version)
self.root = tk.Tk() self.root = tk.Tk()
self.root.title(f"Photomagnetic Communication Tool v{self._version_str}") self.root.title("Photomagnetic Communication Tool")
self.root.geometry("850x700") self.root.geometry("850x700")
self.root.minsize(700, 550) self.root.minsize(700, 550)
@ -291,17 +215,16 @@ class PhotomagneticGUI:
self._port_var = tk.StringVar() self._port_var = tk.StringVar()
self._port_combo = ttk.Combobox(conn_frame, textvariable=self._port_var, width=25) self._port_combo = ttk.Combobox(conn_frame, textvariable=self._port_var, width=25)
self._port_combo.grid(row=0, column=1, padx=4, sticky=tk.W) self._port_combo.grid(row=0, column=1, padx=4, sticky=tk.W)
self._scan_btn = ttk.Button(conn_frame, text="", width=3, ttk.Button(conn_frame, text="", width=3,
command=self._scan_ports) command=self._scan_ports).grid(row=0, column=2, padx=2)
self._scan_btn.grid(row=0, column=2, padx=2)
ttk.Label(conn_frame, text="Baud:").grid(row=0, column=3, padx=(12, 0), sticky=tk.W) ttk.Label(conn_frame, text="Baud:").grid(row=0, column=3, padx=(12, 0), sticky=tk.W)
self._baud_var = tk.StringVar(value="115200") self._baud_var = tk.StringVar(value="115200")
self._baud_combo = ttk.Combobox(conn_frame, textvariable=self._baud_var, baud_combo = ttk.Combobox(conn_frame, textvariable=self._baud_var,
values=("9600", "19200", "38400", "57600", values=("9600", "19200", "38400", "57600",
"115200", "230400", "460800"), "115200", "230400", "460800"),
width=10) width=10)
self._baud_combo.grid(row=0, column=4, padx=4, sticky=tk.W) baud_combo.grid(row=0, column=4, padx=4, sticky=tk.W)
self._connect_btn = ttk.Button(conn_frame, text="Connect", command=self._toggle_connect) self._connect_btn = ttk.Button(conn_frame, text="Connect", command=self._toggle_connect)
self._connect_btn.grid(row=0, column=5, padx=(12, 0)) self._connect_btn.grid(row=0, column=5, padx=(12, 0))
@ -311,19 +234,6 @@ class PhotomagneticGUI:
conn_frame.columnconfigure(6, weight=1) conn_frame.columnconfigure(6, weight=1)
# Version display
version_frame = ttk.Frame(self.root)
version_frame.pack(fill=tk.X, padx=8, pady=0)
ttk.Label(version_frame, text="Tool Version:",
font=("Consolas", 9), foreground="#666").pack(side=tk.LEFT)
ttk.Label(version_frame, text=f"v{self._version_str}",
font=("Consolas", 9, "bold"), foreground="#333").pack(side=tk.LEFT, padx=(2, 12))
ttk.Label(version_frame, text="Firmware Version:",
font=("Consolas", 9), foreground="#666").pack(side=tk.LEFT)
self._fw_version_lbl = ttk.Label(version_frame, text="N/A",
font=("Consolas", 9, "bold"), foreground="#333")
self._fw_version_lbl.pack(side=tk.LEFT, padx=2)
# ── Main content: left (controls) + right (display) ── # ── Main content: left (controls) + right (display) ──
main_frame = ttk.Frame(self.root) main_frame = ttk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=2) main_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=2)
@ -338,13 +248,11 @@ class PhotomagneticGUI:
cmd_frame = ttk.LabelFrame(left, text="Commands", padding=8) cmd_frame = ttk.LabelFrame(left, text="Commands", padding=8)
cmd_frame.pack(fill=tk.X) cmd_frame.pack(fill=tk.X)
# Row 1: GET_ID + Version + State # Row 1: GET_ID + State
r1 = ttk.Frame(cmd_frame) r1 = ttk.Frame(cmd_frame)
r1.pack(fill=tk.X, pady=2) r1.pack(fill=tk.X, pady=2)
ttk.Button(r1, text="Get Device ID", width=14, ttk.Button(r1, text="Get Device ID", width=14,
command=self._cmd_get_id).pack(side=tk.LEFT) command=self._cmd_get_id).pack(side=tk.LEFT)
ttk.Button(r1, text="Read Version", width=12,
command=self._cmd_read_version).pack(side=tk.LEFT, padx=4)
ttk.Label(r1, text="State:").pack(side=tk.LEFT, padx=(10, 2)) ttk.Label(r1, text="State:").pack(side=tk.LEFT, padx=(10, 2))
self._state_combo = ttk.Combobox(r1, values=["Standby", "Running", "Pause", "Error"], self._state_combo = ttk.Combobox(r1, values=["Standby", "Running", "Pause", "Error"],
state="readonly", width=10) state="readonly", width=10)
@ -398,44 +306,6 @@ class PhotomagneticGUI:
self._loop_btn = ttk.Button(r5, text="▶ Loop GET_ID", width=14, self._loop_btn = ttk.Button(r5, text="▶ Loop GET_ID", width=14,
command=self._toggle_loop) command=self._toggle_loop)
self._loop_btn.pack(side=tk.LEFT) self._loop_btn.pack(side=tk.LEFT)
ttk.Separator(r5, orient=tk.VERTICAL).pack(side=tk.LEFT, fill=tk.Y, padx=8)
self._upgrade_btn = ttk.Button(r5, text="⬆ Upgrade Firmware", width=18,
command=self._cmd_upgrade)
self._upgrade_btn.pack(side=tk.LEFT, padx=4)
# Row 6: UID 写入(生产烧录自定义 UID,10 字节编码)
r6 = ttk.Frame(cmd_frame)
r6.pack(fill=tk.X, pady=2)
ttk.Label(r6, text="UID(10B):").pack(side=tk.LEFT)
self._uid_var = tk.StringVar()
uid_entry = ttk.Entry(r6, textvariable=self._uid_var, width=24)
uid_entry.pack(side=tk.LEFT, padx=2)
ttk.Button(r6, text="Write UID", width=10,
command=self._cmd_write_uid).pack(side=tk.LEFT, padx=2)
ttk.Button(r6, text="Use Chip UID", width=12,
command=self._cmd_clear_uid).pack(side=tk.LEFT, padx=2)
# Row 7: LED 调色调试(需调试固件 CONFIG_APP_DEBUG_LED_STRIP=y,
# 生产固件无 0x08 命令)
r7 = ttk.Frame(cmd_frame)
r7.pack(fill=tk.X, pady=2)
ttk.Label(r7, text="LED:").pack(side=tk.LEFT)
self._led_r_var = tk.IntVar(value=255)
self._led_g_var = tk.IntVar(value=255)
self._led_b_var = tk.IntVar(value=255)
# 允许键盘直接输入(0~255),上下箭头仍可用
vcmd = (self.root.register(self._validate_led_channel), "%P")
for var in (self._led_r_var, self._led_g_var, self._led_b_var):
tk.Spinbox(r7, from_=0, to=255, textvariable=var, width=4,
validate="key", validatecommand=vcmd).pack(
side=tk.LEFT, padx=1)
self._led_swatch = tk.Label(r7, text=" ", bg="#FFFFFF", width=3,
relief=tk.SUNKEN)
self._led_swatch.pack(side=tk.LEFT, padx=4)
ttk.Button(r7, text="Set", width=4,
command=self._cmd_set_led).pack(side=tk.LEFT, padx=2)
ttk.Button(r7, text="Off", width=4,
command=self._cmd_led_off).pack(side=tk.LEFT, padx=2)
# ── Right top: Live display ── # ── Right top: Live display ──
disp_frame = ttk.LabelFrame(right, text="Live Values", padding=6) disp_frame = ttk.LabelFrame(right, text="Live Values", padding=6)
@ -539,14 +409,6 @@ class PhotomagneticGUI:
self.worker.send(Cmd.GET_ID) self.worker.send(Cmd.GET_ID)
self.on_log("→ GET_ID sent") self.on_log("→ GET_ID sent")
def _cmd_read_version(self):
"""Request firmware version from device"""
if not self._check_connected():
return
self.worker.send(Cmd.READ_VERSION)
self.on_log("→ VERSION request sent (0x06)")
print(f"[DEBUG] VERSION request sent: cmd=0x{Cmd.READ_VERSION:02X}", flush=True)
def _cmd_set_state(self): def _cmd_set_state(self):
if not self._check_connected(): if not self._check_connected():
return return
@ -600,168 +462,7 @@ class PhotomagneticGUI:
self.on_log(f"→ Raw: CMD=0x{cmd:02X} data={data.hex()}") self.on_log(f"→ Raw: CMD=0x{cmd:02X} data={data.hex()}")
self._raw_var.set("") self._raw_var.set("")
# ── UID ────────────────────────────────────────────────
def _cmd_write_uid(self):
"""写生产 UID(10 字节 = 20 个 hex 字符,编码见 doc/UID编码规则.md)"""
if not self._check_connected():
return
hex_str = self._uid_var.get().strip().replace(" ", "")
if not hex_str:
self.on_log("✗ UID 为空")
return
try:
uid = bytes.fromhex(hex_str)
except ValueError:
self.on_log("✗ UID 不是合法 hex(20 个 hex 字符 = 10 字节)")
return
if len(uid) != 10:
self.on_log(f"✗ UID 长度错误: {len(uid)} 字节,应为 10 字节")
return
self.worker.send(Cmd.WRITE_UID, uid)
self.on_log(f"→ Write UID: {uid.hex(' ').upper()} (10B)")
def _cmd_clear_uid(self):
"""清除自定义 UID,恢复芯片 UID"""
if not self._check_connected():
return
self.worker.send(Cmd.WRITE_UID, b"")
self.on_log("→ Clear UID (restore chip UID)")
self._uid_var.set("")
# ── LED 调色调试 ────────────────────────────────────────
def _validate_led_channel(self, new_text: str) -> bool:
"""RGB 输入框键盘校验:允许空串(编辑中)或 0~255 的整数"""
if new_text == "":
return True
return (new_text.isascii() and new_text.isdigit()
and int(new_text) <= 255)
def _cmd_set_led(self):
"""调试用:灯带整体设为 RGB 颜色(0x08,需 CONFIG_APP_DEBUG_LED_STRIP=y) """
if not self._check_connected():
return
try:
r, g, b = (self._led_r_var.get(), self._led_g_var.get(),
self._led_b_var.get())
except tk.TclError:
r = g = b = 0 # 输入框为空(编辑中)时按 0 处理
r, g, b = max(0, min(255, r)), max(0, min(255, g)), max(0, min(255, b))
self.worker.send(Cmd.SET_LED, bytes([r, g, b]))
self._led_swatch.configure(bg=f"#{r:02X}{g:02X}{b:02X}")
self.on_log(f"→ Set LED RGB({r},{g},{b}) #{r:02X}{g:02X}{b:02X}")
def _cmd_led_off(self):
"""调试用:熄灭灯带"""
if not self._check_connected():
return
self.worker.send(Cmd.SET_LED, b"\x00\x00\x00")
self._led_swatch.configure(bg="#000000")
self.on_log("→ LED off")
# ── Monitor ────────────────────────────────────────── # ── Monitor ──────────────────────────────────────────
def _cmd_upgrade(self):
"""Upgrade firmware: send upgrade cmd -> wait reboot -> SMP upload in-process"""
if not self.worker.is_open:
self._append_log("Please connect serial port first")
return
from tkinter import filedialog
firmware = filedialog.askopenfilename(
title="Select firmware file",
filetypes=[("Signed Binary", "*.signed.bin"), ("Binary", "*.bin"), ("All", "*.*")],
initialdir=os.path.join(os.getcwd(), "build", "app_photomagnetic", "zephyr")
)
if not firmware:
return
self._append_log(f"Upgrade start: {os.path.basename(firmware)}")
self._upgrade_btn.configure(state=tk.DISABLED)
# 升级期间锁定连接相关控件:禁止用户中途 Connect/换端口,否则会抢占
# 串口导致 smpmgr 上传中断、slot0 被擦除一半 → 设备变砖。
for w in (self._connect_btn, self._port_combo, self._baud_combo, self._scan_btn):
w.configure(state=tk.DISABLED)
def upgrade_thread():
try:
port = self._port_var.get()
# Step 1: Close GUI serial connection first(释放协议串口)
self._append_log("Preparing serial port...")
self._disconnect()
# Step 2: Send upgrade command using separate serial connection
self._append_log("-> Sending upgrade command...")
body = bytes([0xFF, 0x01, 0x01])
crc = crc16_modbus(body)
upgrade_frame = HEADER + body + struct.pack('<H', crc)
try:
with serial.Serial(port, 115200, timeout=0.1) as s:
s.write(upgrade_frame)
self._append_log(f" Sent: {upgrade_frame.hex()}")
except Exception as e:
self._append_log(f"Failed to send upgrade command: {e}")
return
# Step 3: Wait for device to reboot into MCUboot
self._append_log("Waiting for device to enter bootloader...")
time.sleep(1.2)
# Step 4: 进程内 SMP 上传(不再调用外部 smpmgr 命令)
# MCUboot serial recovery 固定把镜像写到 slot0(主槽),
# 上传完 reset 即可,无需 test/confirm。
from smp_client import SMPClient
client = SMPClient(port, 115200, log=lambda m: None)
try:
client.open()
self._append_log("Connected to bootloader, uploading...")
self._upgrade_last_pct = -1
ok = client.upload_image(firmware,
progress_callback=self._upgrade_progress)
if not ok:
self._append_log(
"⚠ 上传失败。设备可能已停在 bootloader,重新点升级即可重传;"
"或 west flash -d build 重烧应用")
return
self._append_log("Upload complete (100%)")
self._append_log("Resetting device...")
client.reset()
time.sleep(1)
finally:
client.close()
self._append_log("Upgrade complete! Device will reboot with new firmware")
# Step 6: Wait for device to boot new firmware
self._append_log("Waiting for device to boot...")
time.sleep(4)
# Step 7: Reconnect
self._connect()
except Exception as e:
self._append_log(f"Upgrade failed: {e}")
finally:
def _restore():
self._upgrade_btn.configure(state=tk.NORMAL)
for w in (self._connect_btn, self._port_combo,
self._baud_combo, self._scan_btn):
w.configure(state=tk.NORMAL)
self.root.after(0, _restore)
threading.Thread(target=upgrade_thread, daemon=True).start()
def _upgrade_progress(self, offset: int, total: int):
"""SMP upload progress callback (runs in upgrade thread)."""
pct = offset * 100 // total if total else 0
if pct // 10 != getattr(self, '_upgrade_last_pct', -1) // 10 or pct >= 100:
self._upgrade_last_pct = pct
self.root.after(0, self._append_log, f" Uploading... {offset}/{total} ({pct}%)")
def _toggle_monitor(self): def _toggle_monitor(self):
if self.worker.monitoring: if self.worker.monitoring:
self._stop_monitor() self._stop_monitor()
@ -823,7 +524,6 @@ class PhotomagneticGUI:
# ── Frame handling (runs in main thread via after) ─── # ── Frame handling (runs in main thread via after) ───
def _handle_frame(self, cmd: int, data: bytes): def _handle_frame(self, cmd: int, data: bytes):
raw_hex = data.hex(" ") raw_hex = data.hex(" ")
print(f"[DEBUG] Frame received: cmd=0x{cmd:02X}, data={raw_hex}", flush=True)
# ── 0x00 TEMP: 设备主动推送, 只更新 live value, 不输出 log ── # ── 0x00 TEMP: 设备主动推送, 只更新 live value, 不输出 log ──
if cmd == Cmd.TEMP: if cmd == Cmd.TEMP:
@ -841,26 +541,8 @@ class PhotomagneticGUI:
id_str = data.hex(" ") id_str = data.hex(" ")
id_show = id_str[:47] + "..." if len(id_str) > 50 else id_str id_show = id_str[:47] + "..." if len(id_str) > 50 else id_str
self._disp_labels["dev_id"].configure(text=id_show) self._disp_labels["dev_id"].configure(text=id_show)
if len(data) == 10:
# 生产 UID:[设备类型][产品代次][生产年月 4B][序列号 4B]
dev_type, gen = data[0], data[1]
ym = data[2:6].hex()
serial = data[6:10].hex()
tname = UID_TYPE_NAMES.get(dev_type, f"0x{dev_type:02X}")
dev = UID_DEV_NAMES.get((dev_type, gen))
desc = f"{tname}·代{gen}" + (f" ({dev})" if dev else "")
self._append_log(
f"🆔 Device ID (10B UID): {raw_hex} | {desc} | "
f"年月={ym} 序列号={serial}")
else:
self._append_log(f"🆔 Device ID ({len(data)}B): {raw_hex}") self._append_log(f"🆔 Device ID ({len(data)}B): {raw_hex}")
elif cmd == Cmd.READ_VERSION:
# 固件回复: ASCII 字符串,如 b"0.0.14"
ver = data.decode("utf-8", "replace").strip("\x00 \r\n")
self._fw_version_lbl.configure(text=ver if ver else "?")
self._append_log(f" Firmware version: {ver if ver else '(empty)'}")
elif cmd == Cmd.RUNNING_STATE: elif cmd == Cmd.RUNNING_STATE:
state = data[0] if data else -1 state = data[0] if data else -1
name = STATE_NAMES.get(state, f"Unknown({state})") name = STATE_NAMES.get(state, f"Unknown({state})")
@ -895,19 +577,6 @@ class PhotomagneticGUI:
t = temp_from_data(data) t = temp_from_data(data)
self._append_log(f"🔥 Pad NTC: {t:.2f}°C ({raw_hex})") self._append_log(f"🔥 Pad NTC: {t:.2f}°C ({raw_hex})")
elif cmd == Cmd.WRITE_UID:
status = data[0] if data else 0xFF
if status == 0x00:
self._append_log("✓ UID 写入成功")
# 刷新 ID 显示
self.worker.send(Cmd.GET_ID)
elif status == 0x01:
self._append_log("✗ UID 写入失败(flash 错误)")
elif status == 0x02:
self._append_log("✗ UID 长度错误(需 12 字节或空=清除)")
else:
self._append_log(f"📦 UID ACK: status=0x{status:02X}")
else: else:
self._append_log(f"📦 CMD=0x{cmd:02X} data={raw_hex}") self._append_log(f"📦 CMD=0x{cmd:02X} data={raw_hex}")

View File

@ -1,19 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
/*
* DFU
*
* com.hpp CONFIG_APP_DFU :
* - 0xFF 0x01:(bootmode_set + sys_reboot)
* - 0xFF 0x02:( 0x00 = )
*
* DFU
*/
#include <zephyr/init.h>
#include <zephyr/sys/printk.h>
static auto DfuInit() -> int {
printk("[dfu] init: upgrade command enabled (0xFF)\n");
return 0;
}
SYS_INIT(DfuInit, APPLICATION, 51);

View File

@ -3,9 +3,6 @@
#include <led.hpp> #include <led.hpp>
#include <led_strip_indicator/led_strip_indicator.hpp> #include <led_strip_indicator/led_strip_indicator.hpp>
#include <zephyr/init.h> #include <zephyr/init.h>
#ifdef CONFIG_APP_DEBUG_LED_STRIP
#include <zephyr/drivers/led_strip.h> // struct led_rgb
#endif
using namespace ther; using namespace ther;
namespace { namespace {
using McuState = Led<LED_DT_SPEC_GET(DT_NODELABEL(led_mcu_state))>; using McuState = Led<LED_DT_SPEC_GET(DT_NODELABEL(led_mcu_state))>;
@ -23,20 +20,6 @@ static auto Init() -> int {
Inf::Off(); Inf::Off();
} }
}); });
#ifdef CONFIG_APP_DEBUG_LED_STRIP
// 调试灯带控制:生产固件不编译此段
Com::AddLedColor([](uint8_t r, uint8_t g, uint8_t b) {
led_rgb color{};
color.r = r;
color.g = g;
color.b = b;
const auto ret = indicator->ChangeColor(color);
if (!ret) {
printk("[%s] ChangeColor failed: %d\n", MODULE, (int)ret.code_id());
}
});
#endif
indicator->Status(Com::HostStatus::STANDBY);
using namespace std::chrono_literals; using namespace std::chrono_literals;
McuState::Flash(1s); McuState::Flash(1s);
printk("[%s] Init: MCU state LED flashing at 1s interval\n", MODULE); printk("[%s] Init: MCU state LED flashing at 1s interval\n", MODULE);

View File

@ -1,10 +1,8 @@
#include <zephyr/app_version.h>
#include <zephyr/device.h> #include <zephyr/device.h>
#include <zephyr/kernel.h> #include <zephyr/kernel.h>
auto main(void) -> int { auto main(void) -> int {
printk("[main] init on %s\n", CONFIG_BOARD); printk("[main] init on %s\n", CONFIG_BOARD);
printk("[main] firmware version: %d.%d.%d\n", APP_VERSION_MAJOR, APP_VERSION_MINOR, APP_PATCHLEVEL);
while (1) { while (1) {
k_sleep(K_SECONDS(1)); k_sleep(K_SECONDS(1));
} }

View File

@ -1,165 +0,0 @@
#include <uid.hpp>
#include <string.h>
#include <zephyr/drivers/flash.h>
#include <zephyr/drivers/hwinfo.h>
#include <zephyr/storage/flash_map.h>
#include <zephyr/sys/printk.h>
namespace ther {
namespace {
constexpr uint8_t kMagic[4] = {'U', 'I', 'D', '1'};
constexpr uint8_t kVersion = 0x02;
constexpr size_t kCrcOffset = 15; // CRC16 覆盖 [0, 15) = magic+ver+uid(10B)
auto Crc16Modbus(const uint8_t *data, size_t len) -> uint16_t {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; ++i) {
crc ^= data[i];
for (int b = 0; b < 8; ++b) {
crc = (crc & 1) ? (crc >> 1) ^ 0xA001 : crc >> 1;
}
}
return crc;
}
/// 打开 storage 分区;失败返回 nullptr。
auto OpenArea(const flash_area **area) -> bool {
const int id = PARTITION_ID(storage_partition);
if (flash_area_open(id, area) != 0) {
printk("[uid] open storage partition failed\n");
return false;
}
if (!device_is_ready(flash_area_get_device(*area))) {
printk("[uid] flash device not ready\n");
flash_area_close(*area);
return false;
}
return true;
}
/// 分区起始(offset 0)所在 flash 页的大小;erase 需整页对齐。
///
/// 注意:不要用 flash_area_get_sectors(..., count=1) 来取——count 的语义是
/// “数组容量”,分区有多个页时会返回 -ENOMEM(见 flash_map_layout.c)。
auto FirstPageSize(const flash_area *area) -> uint32_t {
struct flash_pages_info info{};
if (flash_get_page_info_by_offs(flash_area_get_device(area), area->fa_off,
&info) != 0) {
return 0;
}
return info.size;
}
} // namespace
auto Uid::Init() -> void {
// 芯片 UID 作为兜底(保持 12 字节,不做截断)
s_chip_uid_len = hwinfo_get_device_id(s_chip_uid, sizeof(s_chip_uid));
s_active = etl::span<const uint8_t>(s_chip_uid, s_chip_uid_len);
s_custom = false;
const flash_area *area = nullptr;
if (!OpenArea(&area)) {
return;
}
uint8_t rec[kRecordLen];
const int rc = flash_area_read(area, 0, rec, sizeof(rec));
flash_area_close(area);
if (rc != 0) {
printk("[uid] read failed, use chip uid\n");
return;
}
if (RecordValid(rec)) {
memcpy(s_custom_uid, &rec[5], kUidLen);
s_active = etl::span<const uint8_t>(s_custom_uid, kUidLen);
s_custom = true;
printk("[uid] custom uid active\n");
} else {
printk("[uid] no valid record, use chip uid\n");
}
}
auto Uid::Get() -> etl::span<const uint8_t> { return s_active; }
auto Uid::IsCustom() -> bool { return s_custom; }
auto Uid::Write(etl::span<const uint8_t> uid) -> int {
if (!uid.empty() && uid.size() != kUidLen) {
return -EINVAL;
}
// 组装记录;空 span 表示清除(记录全 0xFF = 擦除态)
uint8_t rec[kRecordLen];
memset(rec, 0xFF, sizeof(rec));
if (!uid.empty()) {
memcpy(rec, kMagic, sizeof(kMagic));
rec[4] = kVersion;
memcpy(&rec[5], uid.data(), kUidLen);
const uint16_t crc = Crc16Modbus(rec, kCrcOffset);
rec[kCrcOffset] = static_cast<uint8_t>(crc & 0xFF);
rec[kCrcOffset + 1] = static_cast<uint8_t>(crc >> 8);
}
const flash_area *area = nullptr;
if (!OpenArea(&area)) {
return -ENODEV;
}
// 记录位于分区起始,只需擦除 offset 0 所在的 flash 页(整页对齐)
const uint32_t page_size = FirstPageSize(area);
if (page_size == 0) {
flash_area_close(area);
return -EIO;
}
int rc = flash_area_erase(area, 0, page_size);
if (rc != 0) {
printk("[uid] erase failed rc=%d\n", rc);
flash_area_close(area);
return rc;
}
// kRecordLen 与 STM32G0 写粒度(8B)对齐
rc = flash_area_write(area, 0, rec, kRecordLen);
if (rc != 0) {
printk("[uid] write failed rc=%d\n", rc);
flash_area_close(area);
return rc;
}
// 回读校验
uint8_t rd[kRecordLen];
if (flash_area_read(area, 0, rd, sizeof(rd)) != 0 ||
memcmp(rd, rec, sizeof(rd)) != 0) {
printk("[uid] verify failed\n");
flash_area_close(area);
return -EIO;
}
flash_area_close(area);
// 更新生效 UID
if (!uid.empty()) {
memcpy(s_custom_uid, uid.data(), kUidLen);
s_active = etl::span<const uint8_t>(s_custom_uid, kUidLen);
s_custom = true;
printk("[uid] custom uid written\n");
} else {
s_active = etl::span<const uint8_t>(s_chip_uid, s_chip_uid_len);
s_custom = false;
printk("[uid] custom uid cleared, use chip uid\n");
}
return 0;
}
auto Uid::RecordValid(const uint8_t *rec) -> bool {
if (memcmp(rec, kMagic, sizeof(kMagic)) != 0) {
return false;
}
if (rec[4] != kVersion) {
return false;
}
const uint16_t crc =
static_cast<uint16_t>(rec[kCrcOffset] | (rec[kCrcOffset + 1] << 8));
return Crc16Modbus(rec, kCrcOffset) == crc;
}
} // namespace ther

View File

@ -1,14 +0,0 @@
# 启用 MCUboot bootloader
SB_CONFIG_BOOTLOADER_MCUBOOT=y
# 把 MCUboot + 应用合并成一个 hex(方便一次性烧录/产线),
# 产物: build/merged_<board>.hex(如 merged_dr2501a_g0b0ce_stm32g0b0xx.hex)
SB_CONFIG_MERGED_HEX_FILES=y
# 固件签名密钥(开发阶段使用默认密钥)
# 注意:用 ECDSA-P256 而不是 RSA-2048 —— RSA 会把 MCUboot 撑到 ~57KB,
# 超过 48KB 的 boot 分区;ECDSA 验证代码小很多,MCUboot 能装进 48KB。
# 签名类型必须在这里用 SB_CONFIG_BOOT_SIGNATURE_TYPE_* 指定,
# 写 mcuboot.conf 里的 CONFIG_BOOT_SIGNATURE_TYPE_* 会被 sysbuild 强制覆盖。
SB_CONFIG_BOOT_SIGNATURE_TYPE_ECDSA_P256=y
SB_CONFIG_BOOT_SIGNATURE_KEY_FILE="${APP_DIR}/keys/mykey.pem"

View File

@ -1,50 +0,0 @@
# ── MCUboot 日志 ──
# 裁掉多余驱动后有 ~14KB 余量,恢复 INF 日志方便排障(usart3 调试口可见)
CONFIG_LOG=y
CONFIG_MCUBOOT_LOG_LEVEL_INF=y
# ── Serial Recovery 配置(Nordic 教程 Step 3.1)──
CONFIG_MCUBOOT_SERIAL=y
CONFIG_BOOT_SERIAL_UART=y
# 禁用 UART console,避免与 Serial Recovery 争用 UART(Nordic 教程 Step 3.2)
# 注意:教程里 console 和 serial recovery 共用同一个 UART 才需要禁。
# 本板 console 在 usart3(调试口)、serial recovery 在 usart1(协议口),
# 两个 UART 互不冲突,打开 console 才能看到 MCUboot 的启动日志,便于排障。
CONFIG_UART_CONSOLE=y
# 禁用 GPIO 按钮检测(我们通过升级命令触发,不需要按钮)
CONFIG_BOOT_SERIAL_ENTRANCE_GPIO=n
# 等待 mcumgr 命令触发进入 serial recovery
CONFIG_BOOT_SERIAL_WAIT_FOR_DFU=y
CONFIG_BOOT_SERIAL_WAIT_FOR_DFU_TIMEOUT=3000
# 喂狗(避免升级过程中看门狗复位)
CONFIG_BOOT_WATCHDOG_FEED=y
# ── 签名验证 ──
# 签名类型由 sysbuild.conf 的 SB_CONFIG_BOOT_SIGNATURE_TYPE_ECDSA_P256 决定
# (写在这里的 CONFIG_BOOT_SIGNATURE_TYPE_* 会被 sysbuild 强制覆盖,无效)
# ── 优化 ──
CONFIG_BOOT_ERASE_PROGRESSIVELY=y
# ── 裁掉板级 dts 引入、但 MCUboot 用不到的外设驱动 ──
# MCUboot 只需要 UART(serial recovery)+ GPIO(pinctrl)+ FLASH + WATCHDOG(喂狗),
# 其余驱动(ADC/SPI/PWM/传感器/灯带/协议栈等)全部关掉,把体积压进 48KB boot 分区。
CONFIG_ADC=n
CONFIG_SPI=n
CONFIG_PWM=n
CONFIG_SENSOR=n
CONFIG_LED=n
CONFIG_LED_STRIP=n
CONFIG_LED_STRIP_INDICATOR=n
CONFIG_RTC=n
CONFIG_HWINFO=n
CONFIG_INPUT=n
CONFIG_GODTEK_TEMP_UART=n
CONFIG_UART_COM_PROTOCAL=n
CONFIG_UART_COM_SIMPLE_PROTOCAL=n
CONFIG_UART_COM_NORMAL_PROTOCAL=n
CONFIG_UART_COM_MODBUS_BRIDGE_PROTOCAL=n

View File

@ -1,9 +0,0 @@
/ {
chosen {
/* MCUboot 从 boot_partition 启动(Nordic 教程 Step 2.5)
* Serial Recovery 使用板级 dts 中的 zephyr,uart-mcumgr = &usart1
* console 使用 zephyr,console = &usart3(调试口)
*/
zephyr,code-partition = <&boot_partition>;
};
};

View File

@ -5,10 +5,6 @@ manifest:
- cmsis_6 - cmsis_6
- hal_stm32 - hal_stm32
- hal_ti - hal_ti
- mcuboot
- zcbor
- mbedtls
- tf-psa-crypto
path-prefix: extern path-prefix: extern
name: zephyr name: zephyr
path: zephyr path: zephyr
@ -41,7 +37,7 @@ manifest:
- name: ch9438 - name: ch9438
path: modules/ch9438 path: modules/ch9438
remote: robotstorm remote: robotstorm
revision: dev revision: main
# - name: heading_pad # - name: heading_pad
# path: modules/heading_pad # path: modules/heading_pad
# revision: main # revision: main