forked from EmbeddedTeam/app_photomagnetic
Add UID management, LED strip debug, and MCUboot signing updates
- Add UID encoding/decoding with flash storage and CRC16 validation - Add write UID (0x07) and LED strip color (0x08) protocol commands - Add debug LED strip configuration option - Switch MCUboot signing from RSA-2048 to ECDSA-P256 - Add SMP serial client for firmware upload over UART - Add firmware version output on boot - Update upgrade documentation with ECDSA key generation steps
This commit is contained in:
parent
48644610b8
commit
27ae21bbea
@ -22,6 +22,7 @@ else()
|
||||
src/temp.cpp
|
||||
src/watdog.cpp
|
||||
src/com.cpp
|
||||
src/uid.cpp
|
||||
)
|
||||
|
||||
zephyr_sources_ifdef(CONFIG_APP_DFU
|
||||
|
||||
4
Kconfig
4
Kconfig
@ -10,3 +10,7 @@ config SAMPLE_NTC
|
||||
config APP_DFU
|
||||
default y
|
||||
bool "Enable DFU"
|
||||
|
||||
config APP_DEBUG_LED_STRIP
|
||||
default n
|
||||
bool "Enable debug LED strip"
|
||||
|
||||
5
VERSION
Normal file
5
VERSION
Normal file
@ -0,0 +1,5 @@
|
||||
VERSION_MAJOR = 0
|
||||
VERSION_MINOR = 0
|
||||
PATCHLEVEL = 19
|
||||
VERSION_TWEAK = 0
|
||||
EXTRAVERSION =
|
||||
156
doc/UID编码规则.md
Normal file
156
doc/UID编码规则.md
Normal file
@ -0,0 +1,156 @@
|
||||
# 按摩头 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)
|
||||
- 生产年月:2608(2026 年 8 月,BCD 风格)
|
||||
- 序列号:00001234
|
||||
|
||||
```
|
||||
设备类型 │ 产品代次 │ 生产年月 │ 唯一序列号
|
||||
─────────┼─────────┼──────────────┼────────────────────────
|
||||
0x01 │ 0x01 │ 26 08 00 00 │ 0x00 0x00 0x12 0x34
|
||||
```
|
||||
|
||||
UID(hex,10 字节):`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
|
||||
```
|
||||
|
||||
UID(hex,10 字节):`02 03 26 08 00 00 00 00 56 78`
|
||||
|
||||
### 示例 3:DMS 第一代
|
||||
|
||||
- 设备类型:外置按摩头系列(0x03)
|
||||
- 产品代次:第一代(0x01)
|
||||
- 生产年月:2608
|
||||
- 序列号:AABBCCDD
|
||||
|
||||
```
|
||||
设备类型 │ 产品代次 │ 生产年月 │ 唯一序列号
|
||||
─────────┼─────────┼──────────────┼────────────────────────
|
||||
0x03 │ 0x01 │ 26 08 00 00 │ 0xAA 0xBB 0xCC 0xDD
|
||||
```
|
||||
|
||||
UID(hex,10 字节):`03 01 26 08 00 00 AA BB CC DD`
|
||||
|
||||
> 注:生产年月 4 字节的具体子字段分配(BCD/扩展信息)由生产系统定义,本工具按原始字节透传、透读。
|
||||
|
||||
---
|
||||
|
||||
## 5. 数据存储方式
|
||||
|
||||
UID 信息存储在设备 storage 分区(0x70000,64KB)起始处,记录格式:
|
||||
|
||||
```
|
||||
[magic "UID1" 4B][ver 1B][uid 10B][crc16 2B][pad 7B] = 24 字节
|
||||
```
|
||||
|
||||
- 含 CRC16-Modbus 校验;记录损坏/未写入时自动回退 MCU 芯片 UID(12 字节)。
|
||||
- 写入命令:`0x07` 指令,见《通讯协议.md》。
|
||||
|
||||
---
|
||||
|
||||
## 6. 端口识别说明
|
||||
|
||||
按摩头插入端口后,设备通过以下方式识别:
|
||||
|
||||
- **UID**:识别按摩头的类型和追溯信息
|
||||
- **端口号**:决定控制电路的分配(与 UID 无关)
|
||||
|
||||
按摩头可自由插入任意端口,UID 不会因端口切换而改变。
|
||||
|
||||
---
|
||||
|
||||
## 7. 扩展预留
|
||||
|
||||
| 类型范围 | 用途 |
|
||||
|---|---|
|
||||
| 0x01 | 冲击波系列(已定义) |
|
||||
| 0x02 | 光磁/热疗系列(已定义) |
|
||||
| 0x03 | 外置按摩头系列(已定义) |
|
||||
| 0x04 - 0x0F | 未来新增设备系列 |
|
||||
| 0x10 - 0xEF | 客户定制设备 |
|
||||
| 0xF0 - 0xFE | 工厂测试工具 |
|
||||
| 0xFF | 无效/未编程(保留) |
|
||||
286
doc/升级操作指南.md
286
doc/升级操作指南.md
@ -5,28 +5,36 @@
|
||||
### 1.1 安装依赖
|
||||
|
||||
```bash
|
||||
# 安装 imgtool(固件签名工具)
|
||||
# GUI 工具必需:串口库
|
||||
pip install pyserial
|
||||
|
||||
# 固件签名工具(imgtool 随 Zephyr/MCUboot 自带,也可单独安装)
|
||||
pip install imgtool
|
||||
|
||||
# 安装 mcumgr(固件上传工具)
|
||||
# Linux/macOS
|
||||
go install github.com/apache/mynewt-mcumgr-cli/mcumgr@latest
|
||||
# 或从 https://github.com/apache/mynewt-mcumgr-cli/releases 下载
|
||||
# 可选:命令行升级(smpmgr)——GUI 已内置 SMP 客户端,不装也能升级
|
||||
pip install smpmgr
|
||||
```
|
||||
|
||||
### 1.2 生成签名密钥(量产阶段)
|
||||
|
||||
```bash
|
||||
# 在项目根目录生成密钥对
|
||||
cd /home/issac-zys/code/zephyr_prj_template
|
||||
# 在应用目录下生成密钥对
|
||||
cd /home/issac-zys/code/zephyr_prj_template/app/app_photomagnetic
|
||||
mkdir -p keys
|
||||
imgtool keygen -k keys/my-signing-key.pem -t rsa-2048
|
||||
imgtool keygen -k keys/mykey.pem -t ecdsa-p256
|
||||
|
||||
# 更新 sysbuild.conf 中的密钥路径
|
||||
# SB_CONFIG_BOOT_SIGNATURE_KEY_FILE="${APP_DIR}/keys/my-signing-key.pem"
|
||||
# 更新 sysbuild.conf 中的密钥路径与签名类型
|
||||
# SB_CONFIG_BOOT_SIGNATURE_TYPE_ECDSA_P256=y
|
||||
# SB_CONFIG_BOOT_SIGNATURE_KEY_FILE="${APP_DIR}/keys/mykey.pem"
|
||||
```
|
||||
|
||||
> 开发阶段可跳过此步,使用 MCUboot 自带的开发密钥。
|
||||
> 开发阶段可跳过此步,使用 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 目录),否则签名/验签用的还是旧公钥。
|
||||
|
||||
---
|
||||
|
||||
@ -44,22 +52,23 @@ west build -p auto -b dr2501a_g0b0ce/stm32g0b0xx \
|
||||
```
|
||||
|
||||
构建产物:
|
||||
- `build/mcuboot/zephyr/zephyr.bin` — MCUboot bootloader
|
||||
- `build/app_photomagnetic/zephyr/zephyr.signed.bin` — 签名后的应用固件
|
||||
- `build/zephyr/merged.hex` — 合并镜像(MCUboot + 应用)
|
||||
- `build/mcuboot/zephyr/zephyr.hex` / `.bin` — MCUboot bootloader(0x08000000)
|
||||
- `build/app_photomagnetic/zephyr/zephyr.signed.hex` / `.bin` — 签名后的应用固件(0x0800C000)
|
||||
|
||||
### 2.2 烧录(一次性)
|
||||
> ⚠️ 注意:顶层 `west flash -d build` 只会烧应用镜像,不会烧 MCUboot!MCUboot 必须单独烧录。
|
||||
|
||||
### 2.2 烧录(一次性,需要调试器)
|
||||
|
||||
```bash
|
||||
# 方式1: 使用 west flash(推荐)
|
||||
west flash -d build
|
||||
# ① 先烧 MCUboot 到 0x08000000(覆盖出厂 godtek bootloader,只需一次)
|
||||
west flash -d build/mcuboot
|
||||
|
||||
# 方式2: 使用 J-Link 手动烧录 merged.hex
|
||||
# J-Link Commander:
|
||||
# loadfile build/zephyr/merged.hex
|
||||
# reset
|
||||
# ② 再烧签名应用到 slot0 (0x0800C000)
|
||||
west flash -d build
|
||||
```
|
||||
|
||||
> 两条命令顺序不能反:没有 MCUboot 时,0x0800C000 的镜像头无法被引导,设备会完全无反应。
|
||||
|
||||
### 2.3 验证首次启动
|
||||
|
||||
串口调试口(usart3)应输出:
|
||||
@ -82,135 +91,104 @@ west flash -d build
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 主板 │
|
||||
│ 主板 / 测试工具 │
|
||||
│ 1. 发送升级命令 [7E E7][FF][01][01][CRC] │
|
||||
│ 2. 等待 200ms(小板重启) │
|
||||
│ 3. 切换到 mcumgr 模式 │
|
||||
│ 4. 发送 mcumgr 命令(3 秒内) │
|
||||
│ 2. 等待 300ms(小板重启) │
|
||||
│ 3. 通过 smpmgr 上传固件(3 秒内) │
|
||||
│ 4. 标记待测试 + 复位 │
|
||||
│ 5. 等待新固件启动 │
|
||||
│ 6. 切回 0x7EE7 协议模式 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 光磁头(小板) │
|
||||
│ 1. 收到升级命令 → 直接重启 │
|
||||
│ 2. MCUboot 启动 → 等待 mcumgr 命令(3 秒) │
|
||||
│ 3. 收到 mcumgr 命令 → 进入 serial recovery │
|
||||
│ 4. 接收固件 → 写入 slot1 │
|
||||
│ 5. 验证签名 → swap → 重启运行新固件 │
|
||||
│ 2. MCUboot 启动 → 等待 SMP 命令(3 秒) │
|
||||
│ 3. 收到 SMP 命令 → 进入 serial recovery │
|
||||
│ 4. 接收固件 → 直接写入 slot0(主槽,串口恢复固定写主槽) │
|
||||
│ 5. 复位 → MCUboot 校验签名 → 启动新固件 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 手动升级步骤(使用 mcumgr CLI)
|
||||
> ⚠️ **重要:串口恢复(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: 发送升级命令
|
||||
|
||||
使用串口工具发送升级命令到光磁头:
|
||||
|
||||
```
|
||||
发送: 7E E7 FF 01 01 [CRC_L] [CRC_H]
|
||||
```
|
||||
|
||||
或使用 Python 脚本:
|
||||
使用串口工具或 Python 发送升级命令:
|
||||
|
||||
```python
|
||||
import serial
|
||||
import struct
|
||||
import serial, struct, time
|
||||
|
||||
def send_upgrade_command(port='/dev/ttyUSB0'):
|
||||
def send_upgrade(port='/dev/ttyUSB0'):
|
||||
cmd = bytes([0x7E, 0xE7, 0xFF, 0x01, 0x01])
|
||||
# 计算 CRC16(Modbus)
|
||||
crc = 0xFFFF
|
||||
for b in cmd[2:]: # 从 CMD 开始计算
|
||||
for b in cmd[2:]:
|
||||
crc ^= b
|
||||
for _ in range(8):
|
||||
if crc & 1:
|
||||
crc = (crc >> 1) ^ 0xA001
|
||||
else:
|
||||
crc >>= 1
|
||||
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"Sent upgrade command: {cmd.hex()}")
|
||||
print(f"已发送升级命令: {cmd.hex()}")
|
||||
|
||||
time.sleep(0.3) # 等待重启
|
||||
|
||||
send_upgrade_command()
|
||||
send_upgrade('/dev/ttyUSB0')
|
||||
```
|
||||
|
||||
#### 步骤 2: 等待小板重启
|
||||
|
||||
等待 200ms,让小板完成重启。
|
||||
|
||||
#### 步骤 3: 上传固件
|
||||
#### 步骤 2: 使用 smpmgr 上传固件
|
||||
|
||||
```bash
|
||||
# 设置 mcumgr 连接
|
||||
export MCUmgr_CONN="dev=/dev/ttyUSB0,baud=115200"
|
||||
|
||||
# 上传固件(必须在小板重启后 3 秒内执行)
|
||||
mcumgr --conntype=serial --connstring="$MCUmgr_CONN" \
|
||||
image upload -e build/app_photomagnetic/zephyr/zephyr.signed.bin
|
||||
# 一键升级(串口恢复模式直接写 slot0 主槽,上传完自动复位,无需 test/confirm)
|
||||
# 必须在小板重启后 3 秒内执行;--timeout 10 给擦除/慢速上传留足超时
|
||||
smpmgr --port /dev/ttyUSB0 --timeout 10 upgrade \
|
||||
build/app_photomagnetic/zephyr/zephyr.signed.bin
|
||||
```
|
||||
|
||||
上传过程输出:
|
||||
|
||||
```
|
||||
0 25.00 KiB [====] 100%
|
||||
Done
|
||||
```
|
||||
|
||||
#### 步骤 4: 查看镜像列表
|
||||
也可以手动分步:
|
||||
|
||||
```bash
|
||||
mcumgr --conntype=serial --connstring="$MCUmgr_CONN" image list
|
||||
# 上传固件到 slot0(主槽)
|
||||
smpmgr --port /dev/ttyUSB0 image upload build/app_photomagnetic/zephyr/zephyr.signed.bin
|
||||
|
||||
# 复位,让 MCUboot 校验并启动新固件
|
||||
smpmgr --port /dev/ttyUSB0 os reset
|
||||
```
|
||||
|
||||
输出示例:
|
||||
|
||||
```
|
||||
Split status: N/A (0)
|
||||
Image number: 1
|
||||
Slot 0: primary
|
||||
Version: 1.0.0
|
||||
Bootable: true
|
||||
Flags: active confirmed
|
||||
Hash: 5a8d...c3f2
|
||||
Slot 1: secondary
|
||||
Version: 1.1.0
|
||||
Bootable: true
|
||||
Flags:
|
||||
Hash: 9b4a...e7d1
|
||||
```
|
||||
|
||||
#### 步骤 5: 标记待测试
|
||||
#### 步骤 3: 验证新固件
|
||||
|
||||
```bash
|
||||
# 使用 slot1 的 hash 值
|
||||
mcumgr --conntype=serial --connstring="$MCUmgr_CONN" \
|
||||
image test 9b4a...e7d1
|
||||
# 查看串口输出,确认新固件版本(协议口应输出新固件的 ID 帧)
|
||||
# 串口恢复直接写主槽、无 swap,固件已生效,无需 confirm。
|
||||
```
|
||||
|
||||
#### 步骤 6: 复位触发 swap
|
||||
### 3.3 使用 GUI 工具升级(推荐)
|
||||
|
||||
```bash
|
||||
mcumgr --conntype=serial --connstring="$MCUmgr_CONN" reset
|
||||
```
|
||||
已集成到 `scripts/test_gui.py` 中,升级功能**进程内封装了 SMP 客户端**(`smp_client.py`),
|
||||
不再依赖外部 smpmgr/mcumgr 命令:
|
||||
|
||||
#### 步骤 7: 验证新固件
|
||||
1. 打开 GUI 工具: `python scripts/test_gui.py`
|
||||
2. 连接串口
|
||||
3. 点击 **"Upgrade Firmware"** 按钮
|
||||
4. 选择 `.signed.bin` 固件文件
|
||||
5. 等待升级完成(GUI 会显示上传进度,自动复位并重连)
|
||||
|
||||
MCUboot 会自动 swap 并重启。新固件启动后:
|
||||
升级期间 GUI 会锁定 Connect 按钮/端口选择,防止串口被抢占导致上传中断。
|
||||
|
||||
```bash
|
||||
# 查看串口输出,确认新固件版本
|
||||
# 协议口应输出新固件的 ID 帧
|
||||
> 可选:仍可用命令行一键升级(需已安装 smpmgr):
|
||||
> `smpmgr --port /dev/ttyUSB0 --timeout 10 upgrade build/app_photomagnetic/zephyr/zephyr.signed.bin`
|
||||
|
||||
# 确认新固件(可选,防止下次重启回滚)
|
||||
mcumgr --conntype=serial --connstring="$MCUmgr_CONN" \
|
||||
image confirm 9b4a...e7d1
|
||||
```
|
||||
|
||||
### 3.3 自动化升级脚本
|
||||
### 3.4 自动化升级脚本
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
@ -219,7 +197,6 @@ mcumgr --conntype=serial --connstring="$MCUmgr_CONN" \
|
||||
|
||||
PORT=${1:-/dev/ttyUSB0}
|
||||
FIRMWARE=${2:-build/app_photomagnetic/zephyr/zephyr.signed.bin}
|
||||
BAUD=115200
|
||||
|
||||
echo "=== 光磁头固件升级 ==="
|
||||
echo "串口: $PORT"
|
||||
@ -235,7 +212,7 @@ fi
|
||||
echo "1. 发送升级命令..."
|
||||
python3 -c "
|
||||
import serial, struct, time
|
||||
with serial.Serial('$PORT', $BAUD, timeout=1) as ser:
|
||||
with serial.Serial('$PORT', 115200, timeout=1) as ser:
|
||||
cmd = bytes([0x7E, 0xE7, 0xFF, 0x01, 0x01])
|
||||
crc = 0xFFFF
|
||||
for b in cmd[2:]:
|
||||
@ -251,70 +228,49 @@ with serial.Serial('$PORT', $BAUD, timeout=1) as ser:
|
||||
echo "2. 等待小板重启..."
|
||||
sleep 0.3
|
||||
|
||||
# 上传固件
|
||||
# 上传固件(串口恢复直接写 slot0 主槽,上传完自动复位)
|
||||
echo "3. 上传固件..."
|
||||
mcumgr --conntype=serial --connstring="dev=$PORT,baud=$BAUD" \
|
||||
image upload -e "$FIRMWARE"
|
||||
smpmgr --port "$PORT" --timeout 10 upgrade "$FIRMWARE"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "错误: 固件上传失败"
|
||||
echo "提示: 上传中断会擦掉 slot0 一半,但设备停在 bootloader 中不会变砖,"
|
||||
echo " 重新执行本脚本即可恢复(或 west flash -d build 重烧应用)。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 获取新固件 hash
|
||||
HASH=$(mcumgr --conntype=serial --connstring="dev=$PORT,baud=$BAUD" \
|
||||
image list | grep -A5 "Slot 1:" | grep "Hash:" | awk '{print $2}')
|
||||
|
||||
echo "4. 新固件 hash: $HASH"
|
||||
|
||||
# 标记待测试
|
||||
echo "5. 标记待测试..."
|
||||
mcumgr --conntype=serial --connstring="dev=$PORT,baud=$BAUD" \
|
||||
image test "$HASH"
|
||||
|
||||
# 复位
|
||||
echo "6. 复位,触发 swap..."
|
||||
mcumgr --conntype=serial --connstring="dev=$PORT,baud=$BAUD" reset
|
||||
|
||||
echo "=== 升级完成,等待新固件启动 ==="
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 回滚与恢复
|
||||
## 4. 恢复(串口恢复模式无 swap,没有回滚机制)
|
||||
|
||||
### 4.1 新固件未确认自动回滚
|
||||
> 串口恢复(serial recovery)是直接把镜像写入 slot0 主槽,固件即刻生效、
|
||||
> 不存在“待测试/回滚”的概念。旧版文档里的 swap/回滚描述来自应用内 mcumgr DFU,
|
||||
> 本方案不适用。
|
||||
|
||||
如果新固件有问题,重启后 MCUboot 会自动回滚到原固件:
|
||||
### 4.1 上传中断/镜像损坏后的恢复
|
||||
|
||||
上传中断只会擦掉 slot0 一部分,MCUboot 仍然在运行并停留在 serial recovery 等待区,
|
||||
**不需要调试器**即可恢复:
|
||||
|
||||
```bash
|
||||
# 不执行 image confirm,直接重启
|
||||
mcumgr --conntype=serial --connstring="$MCUmgr_CONN" reset
|
||||
|
||||
# MCUboot 检测到未确认 → 回滚到原固件
|
||||
# 直接重传即可(设备已在 bootloader 等待,无需再发升级命令)
|
||||
smpmgr --port /dev/ttyUSB0 --timeout 10 upgrade \
|
||||
build/app_photomagnetic/zephyr/zephyr.signed.bin
|
||||
```
|
||||
|
||||
### 4.2 手动确认新固件
|
||||
或使用 GUI:再次点击 **Upgrade Firmware**(0xFF 命令对 MCUboot 无害,smpmgr 会自动连上)。
|
||||
|
||||
如果新固件工作正常,确认后防止回滚:
|
||||
### 4.2 强制恢复(需要调试器)
|
||||
|
||||
如果连 MCUboot 都进不去(比如误烧了 boot 分区):
|
||||
|
||||
```bash
|
||||
mcumgr --conntype=serial --connstring="$MCUmgr_CONN" \
|
||||
image confirm <hash>
|
||||
```
|
||||
|
||||
### 4.3 强制恢复(需要调试器)
|
||||
|
||||
如果升级失败导致设备无法启动:
|
||||
|
||||
```bash
|
||||
# 使用调试器强制烧录
|
||||
# 重新烧录 MCUboot + 应用
|
||||
west flash -d build/mcuboot
|
||||
west flash -d build
|
||||
|
||||
# 或烧录 merged.hex
|
||||
# J-Link Commander:
|
||||
# loadfile build/zephyr/merged.hex
|
||||
# reset
|
||||
```
|
||||
|
||||
---
|
||||
@ -323,19 +279,29 @@ west flash -d build
|
||||
|
||||
| 现象 | 原因 | 解决方案 |
|
||||
|---|---|---|
|
||||
| mcumgr 超时 | 主板未在 3 秒内发送命令 | 缩短重启到上传的间隔 |
|
||||
| 签名验证失败 | 密钥不匹配 | 使用构建时的密钥签名 |
|
||||
| 上传中断 | 串口断开/超时 | 重新上传(支持断点续传) |
|
||||
| 设备无法启动 | 固件损坏 | 使用调试器恢复 |
|
||||
| mcumgr 连接失败 | 波特率错误 | 确认使用 115200 |
|
||||
| smpmgr 超时 | 上传未在 3 秒窗口内开始 | 先确认设备已进 bootloader(串口有 MCUboot 日志)再上传 |
|
||||
| 上传中断 | 串口被其他工具/GUI 占用 | 关闭其他串口工具,重新上传(需从头传,不支持断点续传) |
|
||||
| 上传后无法启动 | 上传中断擦坏了 slot0 | 设备停在 bootloader,直接重传或 `west flash -d build` 重烧应用 |
|
||||
| 签名验证失败 | 密钥不匹配 | 用构建时相同的密钥(开发期 `root-ec-p256.pem`)签名 |
|
||||
| smpmgr 连接失败 | 端口错误/被占用 | 确认端口号,关闭占用程序后再试 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 注意事项
|
||||
|
||||
1. **3 秒窗口**:主板必须在小板重启后 3 秒内发送 mcumgr 命令
|
||||
2. **波特率**:应用层协议和 mcumgr 都使用 115200
|
||||
3. **签名**:开发阶段使用默认密钥;量产必须替换
|
||||
4. **回滚**:不确认新固件 → 重启自动回滚
|
||||
5. **调试器**:首次烧录需要;后续升级不需要
|
||||
6. **断电保护**:升级中断电 → MCUboot 回滚,设备不会变砖
|
||||
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 官方工具,功能完整 |
|
||||
|
||||
27
doc/通讯协议.md
27
doc/通讯协议.md
@ -19,6 +19,28 @@
|
||||
- 0x01:运行中
|
||||
- 0x02:暂停中
|
||||
- 0x03:故障中
|
||||
- 写UID:0x07指令(生产烧录自定义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分区(0x70000,64KB)起始处,记录格式
|
||||
`[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
|
||||
sequenceDiagram
|
||||
@ -45,4 +67,9 @@ Note over S: 系统上电
|
||||
S -->> M: 回复 温度 数据
|
||||
end
|
||||
|
||||
Note over M, S: 生产烧录 UID (0x07 指令)
|
||||
M ->> S: 写入 12 字节 UID (0x07 指令帧)
|
||||
S -->> M: 回复状态 (0x00 成功)
|
||||
M ->> S: 请求 ID 验证 (0x01 指令帧)
|
||||
S -->> M: 回复写入后的 UID
|
||||
```
|
||||
|
||||
@ -5,7 +5,10 @@
|
||||
#include <etl/delegate.h>
|
||||
#include <etl/signal.h>
|
||||
#include <led_strip_indicator/led_strip_indicator.hpp>
|
||||
#include <stdio.h>
|
||||
#include <uart_com/simple_protocal.hpp>
|
||||
#include <uid.hpp>
|
||||
#include <zephyr/app_version.h>
|
||||
#include <zephyr/drivers/hwinfo.h>
|
||||
#include <zephyr/drivers/sensor.h>
|
||||
#include <zpp/driver.hpp>
|
||||
@ -21,8 +24,9 @@ public:
|
||||
using RunningStateSignal = etl::signal<void(HostStatus), 10>;
|
||||
static auto Init() -> zpp::error {
|
||||
s_proto->SetRxCallbackTable(kRxCallbackTable);
|
||||
auto size = hwinfo_get_device_id(buff, sizeof(buff));
|
||||
s_id_buff = etl::span<uint8_t>(buff, size);
|
||||
// 优先使用生产写入的自定义 UID,否则回退芯片 UID
|
||||
Uid::Init();
|
||||
s_id_buff = Uid::Get();
|
||||
return zpp::ok();
|
||||
}
|
||||
static auto SendTemp(sensor_value val) -> void {
|
||||
@ -32,14 +36,28 @@ public:
|
||||
static auto AddRunningState(RunningStateSignal::slot_type slot) -> bool {
|
||||
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:
|
||||
enum Addr : uint8_t {
|
||||
R_TEMP = 0,
|
||||
R_GET_ID,
|
||||
W_RUNNING_STATE,
|
||||
W_UID = 0x07,
|
||||
R_VERSION = 0x06,
|
||||
W_LED = 0x08,
|
||||
};
|
||||
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 {
|
||||
if (data.size() != 1) {
|
||||
return;
|
||||
@ -50,6 +68,45 @@ private:
|
||||
printk("handle get id");
|
||||
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 查询状态
|
||||
@ -74,8 +131,11 @@ private:
|
||||
constexpr static std::pair<const uint8_t,
|
||||
uart_com::SimpleProtocal::CallbackType>
|
||||
kRxCallbackTable[] = {
|
||||
{R_GET_ID, GetId},
|
||||
{W_RUNNING_STATE, RunningState},
|
||||
{R_GET_ID, GetId}, {W_RUNNING_STATE, RunningState},
|
||||
{R_VERSION, GetVersion}, {W_UID, WriteUid},
|
||||
#ifdef CONFIG_APP_DEBUG_LED_STRIP
|
||||
{W_LED, SetLed},
|
||||
#endif
|
||||
#ifdef CONFIG_APP_DFU
|
||||
{0xFF, DfuCommand},
|
||||
#endif
|
||||
@ -83,9 +143,8 @@ private:
|
||||
inline static auto s_proto = (uart_com::SimpleProtocal *)DEVICE_DT_GET(
|
||||
DT_COMPAT_GET_ANY_STATUS_OKAY(uart_com_simple_protocal));
|
||||
|
||||
inline static uint8_t buff[20];
|
||||
inline static etl::span<const uint8_t> s_id_buff;
|
||||
inline static auto s_heating_state = false;
|
||||
inline static etl::span<uint8_t> s_id_buff;
|
||||
};
|
||||
} // namespace ther
|
||||
|
||||
|
||||
56
include/uid.hpp
Normal file
56
include/uid.hpp
Normal file
@ -0,0 +1,56 @@
|
||||
#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__
|
||||
5
keys/mykey.pem
Normal file
5
keys/mykey.pem
Normal file
@ -0,0 +1,5 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgwlUCVXuz/+irsEXd
|
||||
xU6IwCQCxnA42StQaD3Hd04QvnyhRANCAAQGpcUOBI1p35AGLNctUd50QL1OmeE3
|
||||
2fPWMOf9byIXeQFnRTVAJkSo067+GMP5onCHOvx76lREg5IxLRZilfxR
|
||||
-----END PRIVATE KEY-----
|
||||
9
prj.conf
9
prj.conf
@ -29,3 +29,12 @@ CONFIG_PWM=y
|
||||
# CONFIG_ADC_LOG_LEVEL_DBG=y
|
||||
# CONFIG_SENSOR_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
|
||||
|
||||
BIN
scripts/__pycache__/smp_client.cpython-314.pyc
Normal file
BIN
scripts/__pycache__/smp_client.cpython-314.pyc
Normal file
Binary file not shown.
BIN
scripts/__pycache__/test_gui.cpython-314.pyc
Normal file
BIN
scripts/__pycache__/test_gui.cpython-314.pyc
Normal file
Binary file not shown.
1
scripts/requirements.txt
Normal file
1
scripts/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
pyserial==3.5
|
||||
403
scripts/smp_client.py
Normal file
403
scripts/smp_client.py
Normal file
@ -0,0 +1,403 @@
|
||||
"""
|
||||
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
|
||||
@ -9,8 +9,15 @@ from tkinter import ttk, scrolledtext, messagebox
|
||||
import threading
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
import struct
|
||||
import base64
|
||||
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:
|
||||
import serial
|
||||
import serial.tools.list_ports
|
||||
@ -28,12 +35,39 @@ class Cmd(IntEnum):
|
||||
RUNNING_STATE = 0x02
|
||||
W_HEATING = 0x03
|
||||
R_HEATING = 0x04
|
||||
READ_VERSION = 0x06
|
||||
WRITE_UID = 0x07
|
||||
SET_LED = 0x08 # 调试用:灯带 RGB(仅调试固件支持)
|
||||
READ_POLE_NTC = 0x64
|
||||
READ_HEATING_NTC = 0x65
|
||||
|
||||
HEATING_STATE_NAMES = {0: "OFF", 1: "ON"}
|
||||
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) ───────────────────────────
|
||||
def crc16_modbus(data: bytes) -> int:
|
||||
@ -191,10 +225,52 @@ class SerialWorker:
|
||||
|
||||
|
||||
# ── 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:
|
||||
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.title("Photomagnetic Communication Tool")
|
||||
self.root.title(f"Photomagnetic Communication Tool v{self._version_str}")
|
||||
self.root.geometry("850x700")
|
||||
self.root.minsize(700, 550)
|
||||
|
||||
@ -215,16 +291,17 @@ class PhotomagneticGUI:
|
||||
self._port_var = tk.StringVar()
|
||||
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)
|
||||
ttk.Button(conn_frame, text="↻", width=3,
|
||||
command=self._scan_ports).grid(row=0, column=2, padx=2)
|
||||
self._scan_btn = ttk.Button(conn_frame, text="↻", width=3,
|
||||
command=self._scan_ports)
|
||||
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)
|
||||
self._baud_var = tk.StringVar(value="115200")
|
||||
baud_combo = ttk.Combobox(conn_frame, textvariable=self._baud_var,
|
||||
self._baud_combo = ttk.Combobox(conn_frame, textvariable=self._baud_var,
|
||||
values=("9600", "19200", "38400", "57600",
|
||||
"115200", "230400", "460800"),
|
||||
width=10)
|
||||
baud_combo.grid(row=0, column=4, padx=4, sticky=tk.W)
|
||||
self._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.grid(row=0, column=5, padx=(12, 0))
|
||||
@ -234,6 +311,19 @@ class PhotomagneticGUI:
|
||||
|
||||
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_frame = ttk.Frame(self.root)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=2)
|
||||
@ -248,11 +338,13 @@ class PhotomagneticGUI:
|
||||
cmd_frame = ttk.LabelFrame(left, text="Commands", padding=8)
|
||||
cmd_frame.pack(fill=tk.X)
|
||||
|
||||
# Row 1: GET_ID + State
|
||||
# Row 1: GET_ID + Version + State
|
||||
r1 = ttk.Frame(cmd_frame)
|
||||
r1.pack(fill=tk.X, pady=2)
|
||||
ttk.Button(r1, text="Get Device ID", width=14,
|
||||
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))
|
||||
self._state_combo = ttk.Combobox(r1, values=["Standby", "Running", "Pause", "Error"],
|
||||
state="readonly", width=10)
|
||||
@ -306,6 +398,44 @@ class PhotomagneticGUI:
|
||||
self._loop_btn = ttk.Button(r5, text="▶ Loop GET_ID", width=14,
|
||||
command=self._toggle_loop)
|
||||
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 ──
|
||||
disp_frame = ttk.LabelFrame(right, text="Live Values", padding=6)
|
||||
@ -409,6 +539,14 @@ class PhotomagneticGUI:
|
||||
self.worker.send(Cmd.GET_ID)
|
||||
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):
|
||||
if not self._check_connected():
|
||||
return
|
||||
@ -462,7 +600,168 @@ class PhotomagneticGUI:
|
||||
self.on_log(f"→ Raw: CMD=0x{cmd:02X} data={data.hex()}")
|
||||
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 ──────────────────────────────────────────
|
||||
|
||||
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):
|
||||
if self.worker.monitoring:
|
||||
self._stop_monitor()
|
||||
@ -524,6 +823,7 @@ class PhotomagneticGUI:
|
||||
# ── Frame handling (runs in main thread via after) ───
|
||||
def _handle_frame(self, cmd: int, data: bytes):
|
||||
raw_hex = data.hex(" ")
|
||||
print(f"[DEBUG] Frame received: cmd=0x{cmd:02X}, data={raw_hex}", flush=True)
|
||||
|
||||
# ── 0x00 TEMP: 设备主动推送, 只更新 live value, 不输出 log ──
|
||||
if cmd == Cmd.TEMP:
|
||||
@ -541,7 +841,25 @@ class PhotomagneticGUI:
|
||||
id_str = data.hex(" ")
|
||||
id_show = id_str[:47] + "..." if len(id_str) > 50 else id_str
|
||||
self._disp_labels["dev_id"].configure(text=id_show)
|
||||
self._append_log(f"🆔 Device ID ({len(data)}B): {raw_hex}")
|
||||
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}")
|
||||
|
||||
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:
|
||||
state = data[0] if data else -1
|
||||
@ -577,6 +895,19 @@ class PhotomagneticGUI:
|
||||
t = temp_from_data(data)
|
||||
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:
|
||||
self._append_log(f"📦 CMD=0x{cmd:02X} data={raw_hex}")
|
||||
|
||||
|
||||
16
src/led.cpp
16
src/led.cpp
@ -3,6 +3,9 @@
|
||||
#include <led.hpp>
|
||||
#include <led_strip_indicator/led_strip_indicator.hpp>
|
||||
#include <zephyr/init.h>
|
||||
#ifdef CONFIG_APP_DEBUG_LED_STRIP
|
||||
#include <zephyr/drivers/led_strip.h> // struct led_rgb
|
||||
#endif
|
||||
using namespace ther;
|
||||
namespace {
|
||||
using McuState = Led<LED_DT_SPEC_GET(DT_NODELABEL(led_mcu_state))>;
|
||||
@ -20,6 +23,19 @@ static auto Init() -> int {
|
||||
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;
|
||||
McuState::Flash(1s);
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
|
||||
#include <zephyr/app_version.h>
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/kernel.h>
|
||||
|
||||
auto main(void) -> int {
|
||||
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) {
|
||||
k_sleep(K_SECONDS(1));
|
||||
}
|
||||
|
||||
165
src/uid.cpp
Normal file
165
src/uid.cpp
Normal file
@ -0,0 +1,165 @@
|
||||
#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
|
||||
@ -1,5 +1,14 @@
|
||||
# 启用 MCUboot bootloader
|
||||
SB_CONFIG_BOOTLOADER_MCUBOOT=y
|
||||
|
||||
# 固件签名密钥(开发阶段用 MCUboot 自带密钥,量产替换为自生成密钥)
|
||||
SB_CONFIG_BOOT_SIGNATURE_KEY_FILE="${ZEPHYR_BASE}/../bootloader/mcuboot/root-rsa-2048.pem"
|
||||
# 把 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"
|
||||
|
||||
@ -1,8 +1,19 @@
|
||||
# ── Serial Recovery 配置 ──
|
||||
# ── 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
|
||||
|
||||
# 禁用 GPIO 按钮检测
|
||||
# 禁用 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
|
||||
@ -13,10 +24,27 @@ CONFIG_BOOT_SERIAL_WAIT_FOR_DFU_TIMEOUT=3000
|
||||
CONFIG_BOOT_WATCHDOG_FEED=y
|
||||
|
||||
# ── 签名验证 ──
|
||||
CONFIG_BOOT_SIGNATURE_TYPE_RSA=y
|
||||
# 签名类型由 sysbuild.conf 的 SB_CONFIG_BOOT_SIGNATURE_TYPE_ECDSA_P256 决定
|
||||
# (写在这里的 CONFIG_BOOT_SIGNATURE_TYPE_* 会被 sysbuild 强制覆盖,无效)
|
||||
|
||||
# ── 优化 ──
|
||||
CONFIG_BOOT_ERASE_PROGRESSIVELY=y
|
||||
|
||||
# ── 日志 ──
|
||||
CONFIG_MCUBOOT_LOG_LEVEL_WRN=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
|
||||
|
||||
@ -1,2 +1,9 @@
|
||||
/* MCUboot serial recovery 使用 zephyr,uart-mcumgr (已在板级 dts 指定为 &usart1)
|
||||
* zephyr,console 保持 &usart3 (调试口),两者不能指向同一设备 */
|
||||
/ {
|
||||
chosen {
|
||||
/* MCUboot 从 boot_partition 启动(Nordic 教程 Step 2.5)
|
||||
* Serial Recovery 使用板级 dts 中的 zephyr,uart-mcumgr = &usart1
|
||||
* console 使用 zephyr,console = &usart3(调试口)
|
||||
*/
|
||||
zephyr,code-partition = <&boot_partition>;
|
||||
};
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user