2564 lines
99 KiB
Markdown
2564 lines
99 KiB
Markdown
# 自然写智能点阵笔嵌入式固件软件 V1.0
|
||
## 软件鉴别材料 — 嵌入式软件设计说明书
|
||
|
||
---
|
||
|
||
**软件全称**:自然写智能点阵笔嵌入式固件软件
|
||
**软件版本**:V1.0
|
||
**权利人**:深圳自然写科技有限公司
|
||
**文档类型**:嵌入式固件软件设计说明书
|
||
**文档编号**:WRITECH-FIRMWARE-DS-001
|
||
**编制日期**:2026年2月
|
||
**密级**:内部资料
|
||
|
||
---
|
||
|
||
## 目录
|
||
|
||
- 第一章 软件整体概述
|
||
- 1.1 软件简介与功能综述
|
||
- 1.2 软件用途与适用场景
|
||
- 1.3 运行环境与硬件平台
|
||
- 1.4 开发语言与工具链
|
||
- 1.5 版本说明
|
||
- 第二章 系统架构与设计思路
|
||
- 2.1 总体架构设计
|
||
- 2.2 RTOS任务模型
|
||
- 2.3 各层次详细说明
|
||
- 2.4 数据设计
|
||
- 2.5 接口设计(BLE GATT)
|
||
- 2.6 安全设计
|
||
- 2.7 Flash分区规划
|
||
- 第三章 核心模块功能详细说明
|
||
- 3.1 main.c — 主程序与RTOS启动
|
||
- 3.2 driver/camera.c — 点阵摄像头驱动
|
||
- 3.3 driver/pressure.c — 压力传感器驱动
|
||
- 3.4 driver/battery.c — 电池电量监测驱动
|
||
- 3.5 codec/dot_decoder.c — 点阵码坐标解码
|
||
- 3.6 codec/stroke_encoder.c — 笔迹数据编码打包
|
||
- 3.7 task/image_capture_task.c — 图像采集任务
|
||
- 3.8 task/coord_calc_task.c — 坐标计算任务
|
||
- 3.9 task/ble_send_task.c — BLE数据发送任务
|
||
- 3.10 task/power_task.c — 电源管理任务
|
||
- 3.11 task/led_task.c — LED状态指示任务
|
||
- 3.12 task/ota_task.c — OTA固件升级任务
|
||
- 3.13 cache/offline_cache.c — 离线数据缓存
|
||
- 3.14 power/power_manager.c — 低功耗状态机
|
||
- 第四章 操作流程与使用步骤
|
||
- 4.1 点阵笔开机流程
|
||
- 4.2 蓝牙配对与连接流程
|
||
- 4.3 书写采集与数据传输流程
|
||
- 4.4 离线书写与数据同步流程
|
||
- 4.5 充电与电量管理
|
||
- 4.6 OTA固件升级流程
|
||
- 4.7 出厂校准与测试流程
|
||
- 4.8 常见问题处理
|
||
- 第五章 与源代码的对应关系
|
||
- 5.1 模块与源代码文件对应表
|
||
- 5.2 核心函数说明
|
||
- 5.3 寄存器与GATT特征定义
|
||
- 附录A BLE GATT服务定义表
|
||
- 附录B 硬件外设寄存器说明
|
||
- 附录C 术语表
|
||
- 附录D 版本历史
|
||
|
||
---
|
||
|
||
## 第一章 软件整体概述
|
||
|
||
### 1.1 软件简介与功能综述
|
||
|
||
自然写智能点阵笔嵌入式固件软件(以下简称"笔固件")是运行于自然写智能点阵笔主控MCU芯片上的嵌入式实时操作系统软件,是整个互动课堂系统最基础的数据采集端软件。笔固件负责控制点阵摄像头连续采集点阵纸面图像,对图像进行实时解码以获取精确的书写坐标,并通过蓝牙BLE协议将坐标流实时发送至网关或终端设备。
|
||
|
||
笔固件采用RTOS(FreeRTOS / RT-Thread)实时操作系统,以多任务并发的方式同时处理图像采集、坐标解算、蓝牙发送、电源管理、LED指示等功能,各任务按优先级调度,确保100Hz的高频坐标采样率和低延迟蓝牙传输。
|
||
|
||
**主要功能模块综述:**
|
||
|
||
| 功能模块 | 说明 |
|
||
|---------|------|
|
||
| 点阵摄像头图像采集 | 控制CMOS摄像头以100fps速率连续采集点阵图像 |
|
||
| 点阵码坐标解码 | 对采集图像实时解码,解算出高精度书写坐标(分辨率0.01mm) |
|
||
| 压力传感器数据采集 | 读取笔尖压力传感器ADC值,检测落笔/抬笔事件 |
|
||
| BLE数据传输 | 通过BLE 5.0 GATT Notify方式将坐标流发送至连接的设备 |
|
||
| 设备配对与连接管理 | 管理BLE连接配对,支持存储最多4个已配对设备 |
|
||
| 低功耗电源管理 | 实现Active/Idle/Sleep/DeepSleep四级电源状态,延长续航 |
|
||
| 电池电量监测 | ADC采样电池电压,计算电量百分比,低电量提示 |
|
||
| LED状态指示 | 通过RGB LED显示连接状态、充电状态、电量告警 |
|
||
| 离线数据缓存 | 无连接时在外部Flash缓存笔迹数据(容量4MB) |
|
||
| OTA固件升级 | 支持通过BLE DFU协议接收固件包并安全升级 |
|
||
|
||
### 1.2 软件用途与适用场景
|
||
|
||
笔固件专为互动课堂智能点阵笔设计,支持以下使用场景:
|
||
|
||
**场景一:课堂实时书写**
|
||
学生使用点阵笔在配套点阵纸上书写作业、答题或练字。笔固件以100Hz频率采集坐标,通过BLE实时传输至网关或算力盒,实现云端或边缘AI的即时识别与反馈。
|
||
|
||
**场景二:离线书写缓存**
|
||
当BLE未连接(如课前预习、课后作业)时,笔固件将书写坐标缓存至外部Flash存储(最多约10万个坐标点,约相当于10页A4纸的书写量)。当与设备重新连接后,自动同步缓存数据。
|
||
|
||
**场景三:移动教学(教师手持)**
|
||
教师手持点阵笔在任意位置书写,笔固件通过BLE将笔迹实时传输至教师手机APP,实现移动式板书和批注。
|
||
|
||
**场景四:字帖练字**
|
||
配合练字字帖,笔固件高精度采集用户笔顺和坐标,由上层软件进行笔顺分析和书写规范性评测。
|
||
|
||
**适用硬件:**
|
||
|
||
| 硬件参数 | 规格 |
|
||
|---------|------|
|
||
| 主控MCU | Nordic Semiconductor nRF52840 / STM32WB55 |
|
||
| BLE协议版本 | Bluetooth Low Energy 5.0 |
|
||
| 摄像头 | 定制CMOS点阵摄像头模组(DFOV 20°,100fps) |
|
||
| 压力传感器 | 电阻式力敏传感器,量程0-150g,12位ADC |
|
||
| 外部Flash | SPI NOR Flash,4MB(如W25Q32) |
|
||
| 电池 | 锂电池 150mAh,续航约8小时(连接状态) |
|
||
| 充电 | USB Type-C,500mA充电电流 |
|
||
|
||
### 1.3 运行环境与硬件平台
|
||
|
||
**主控芯片规格(以nRF52840为例):**
|
||
|
||
| 项目 | 规格 |
|
||
|------|------|
|
||
| 处理器架构 | ARM Cortex-M4F,64MHz |
|
||
| 内部SRAM | 256KB |
|
||
| 内部Flash | 1MB(Bootloader + App A + App B + NVS) |
|
||
| BLE协议栈 | Nordic SoftDevice S140(BLE 5.0) |
|
||
| 外设接口 | SPI×3、I2C×2、ADC 12位×8通道、UART×2、GPIO |
|
||
| 电源范围 | 1.7V ~ 5.5V |
|
||
| 低功耗模式 | System ON Sleep:1.5μA;System OFF:0.2μA |
|
||
|
||
**RTOS运行要求:**
|
||
|
||
| 组件 | 版本 / 规格 |
|
||
|------|------------|
|
||
| FreeRTOS | V10.4.3(或 RT-Thread 4.1.0) |
|
||
| Nordic SoftDevice | S140 v7.3.0(BLE协议栈) |
|
||
| 最小栈空间 | 每任务最小512字节栈 |
|
||
| 总SRAM需求 | 约180KB(含协议栈、任务栈、数据缓冲) |
|
||
|
||
**外部硬件接口:**
|
||
|
||
| 外设 | 接口 | 连接说明 |
|
||
|------|------|---------|
|
||
| 点阵摄像头 | SPI(最高8MHz) | 图像数据读取(每帧~1KB) |
|
||
| 摄像头控制 | GPIO(3引脚) | 电源使能/帧同步/曝光控制 |
|
||
| 压力传感器 | ADC(12位) | 笔尖压力模拟量采样 |
|
||
| 外部Flash | SPI(最高50MHz) | 离线坐标缓存 |
|
||
| 充电管理IC | I2C | 充电状态与电池电压读取 |
|
||
| LED | GPIO(RGB三色) | 状态指示 |
|
||
| 振动马达 | GPIO | 反馈振动(配对成功/低电量提示) |
|
||
|
||
### 1.4 开发语言与工具链
|
||
|
||
**开发语言:**
|
||
|
||
| 语言 | 标准 | 用途 |
|
||
|------|------|------|
|
||
| C | C99 / C11 | 全部固件源代码 |
|
||
| 汇编 | ARM Thumb-2 | 中断向量表、启动文件(startup_nrf52840.s) |
|
||
|
||
**工具链:**
|
||
|
||
| 工具 | 版本 | 说明 |
|
||
|------|------|------|
|
||
| ARM GCC | 11.2-2022.02 | C/汇编编译器 |
|
||
| GNU Make | 4.3 | 构建系统 |
|
||
| nRF5 SDK | 17.1.0 | Nordic嵌入式SDK(BLE、驱动、HAL) |
|
||
| J-Link | V7.80 | 调试器(SWD接口) |
|
||
| nRF Connect | 4.0 | BLE调试与协议分析工具 |
|
||
| OpenOCD | 0.11.0 | 开源调试接口(备用) |
|
||
| Python | 3.9 | 固件打包脚本、Flash烧写工具 |
|
||
|
||
**代码规范:**
|
||
- 命名:宏定义全大写下划线(`MAX_CONNECTIONS`),函数小写下划线(`ble_send_data`),全局变量`g_`前缀,静态变量`s_`前缀
|
||
- 中断服务程序(ISR)中禁止调用任何RTOS阻塞API
|
||
- 所有硬件访问通过驱动层封装,禁止在业务层直接操作寄存器
|
||
- 每个源文件不超过500行,超过需拆分
|
||
|
||
### 1.5 版本说明
|
||
|
||
| 版本 | 日期 | 主要变更 |
|
||
|------|------|---------|
|
||
| V0.3 Alpha | 2025年6月 | 基础BLE连接、坐标采集与发送 |
|
||
| V0.7 Beta | 2025年9月 | 离线缓存、点阵码解码算法优化(精度提升) |
|
||
| V0.9 RC | 2025年11月 | OTA升级、低功耗优化(续航延长30%) |
|
||
| V1.0 | 2026年2月 | 正式版:安全加固、压力感应优化、LED动效 |
|
||
|
||
---
|
||
|
||
## 第二章 系统架构与设计思路
|
||
|
||
### 2.1 总体架构设计
|
||
|
||
笔固件采用经典的嵌入式RTOS分层架构,自下而上分为五层:硬件抽象层、驱动层、协议栈层、应用任务层和系统管理层。各层之间通过明确定义的API接口通信,保证层间解耦,便于移植和维护。
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ 应用任务层(Application Tasks) │
|
||
│ 图像采集任务 │ 坐标计算任务 │ BLE发送任务 │ 电源监测任务 │ OTA任务 │
|
||
├──────────────────────────────────────────────────────────────────┤
|
||
│ 系统管理层(System Management Layer) │
|
||
│ 配对管理 │ 离线缓存管理 │ LED控制 │ 日志/错误处理 │
|
||
├────────────────────────────┬─────────────────────────────────────┤
|
||
│ BLE协议栈层 │ 图像处理层 │
|
||
│ Nordic SoftDevice S140 │ 点阵码解码算法(dot_decoder.c) │
|
||
│ GATT Server / BLE SMP │ 笔画编码打包(stroke_encoder.c) │
|
||
├────────────────────────────┴─────────────────────────────────────┤
|
||
│ 硬件驱动层(Driver Layer) │
|
||
│ 摄像头驱动 │ 压力传感器驱动 │ Flash驱动 │ 充电IC驱动 │ LED驱动 │
|
||
├──────────────────────────────────────────────────────────────────┤
|
||
│ 硬件抽象层(HAL / nRF SDK) │
|
||
│ GPIO HAL │ SPI HAL │ ADC HAL │ I2C HAL │ Timer HAL │
|
||
├──────────────────────────────────────────────────────────────────┤
|
||
│ 硬件层(MCU + 外设) │
|
||
│ nRF52840 ARM M4F │ CMOS摄像头 │ Flash │ 传感器 │ BLE天线 │
|
||
└──────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 2.2 RTOS任务模型
|
||
|
||
笔固件运行6个RTOS并发任务,各任务独立调度,通过队列、信号量和事件组进行同步:
|
||
|
||
| 任务名称 | 优先级 | 栈大小 | 调度方式 | 说明 |
|
||
|---------|-------|--------|---------|------|
|
||
| `image_capture_task` | 最高(5) | 1024B | 100Hz定时触发 | 摄像头图像采集 |
|
||
| `coord_calc_task` | 高(4) | 2048B | 事件触发(图像就绪) | 点阵码解码与坐标计算 |
|
||
| `ble_send_task` | 高(4) | 1024B | 事件触发(坐标就绪) | BLE Notify发送 |
|
||
| `power_task` | 中(3) | 512B | 1Hz定时 | 电量采样与功耗管理 |
|
||
| `led_task` | 低(2) | 512B | 事件触发 | LED状态控制 |
|
||
| `ota_task` | 最低(1) | 4096B | 触发式(BLE DFU命令) | OTA固件升级 |
|
||
|
||
**任务间通信机制:**
|
||
|
||
```
|
||
任务间通信机制示意:
|
||
|
||
image_capture_task
|
||
│ 采集到新图像帧
|
||
│──写入图像队列──→ coord_calc_task
|
||
│ 解码完成,坐标就绪
|
||
│──写入坐标队列──→ ble_send_task
|
||
│
|
||
│──发送BLE Notify
|
||
|
||
power_task
|
||
│ 低电量事件
|
||
│──发布事件组──→ led_task(闪烁红灯)
|
||
│──发布事件组──→ ble_send_task(发送电量通知)
|
||
│──状态切换──→ power_manager(Sleep策略调整)
|
||
|
||
ble连接状态回调(SoftDevice中断)
|
||
├──→ ble_send_task(连接/断开通知)
|
||
├──→ led_task(状态灯切换)
|
||
└──→ power_manager(调整功耗模式)
|
||
```
|
||
|
||
### 2.3 各层次详细说明
|
||
|
||
**硬件抽象层(HAL)**
|
||
|
||
硬件抽象层基于Nordic nRF5 SDK提供的HAL API,封装了GPIO、SPI、ADC、I2C等基础外设操作。所有驱动层代码通过HAL接口访问硬件,不直接操作寄存器,保证代码可移植性。
|
||
|
||
**硬件驱动层(Driver Layer)**
|
||
|
||
驱动层为每个外设提供独立的C模块:
|
||
- `driver/camera.c`:CMOS摄像头驱动(SPI读取图像帧数据,GPIO控制曝光)
|
||
- `driver/pressure.c`:压力传感器ADC采样驱动(12位ADC,过采样16次取均值)
|
||
- `driver/battery.c`:电池电量检测(分压电路ADC采样,查表法计算电量百分比)
|
||
- `driver/flash.c`:外部SPI NOR Flash驱动(页读写、扇区擦除、磨损均衡)
|
||
- `driver/led.c`:RGB LED驱动(PWM控制色彩,支持呼吸灯、闪烁等动效)
|
||
|
||
**BLE协议栈层**
|
||
|
||
采用Nordic SoftDevice S140作为BLE 5.0协议栈,以软件"协议栈"方式与应用代码共存于同一芯片,通过SVC调用(Supervisor Call)接口访问。
|
||
|
||
GATT Server定义了3个自定义Service:
|
||
- **Writech Pen Data Service**(UUID: `FFF0`):笔迹坐标数据传输
|
||
- **Writech Device Info Service**(UUID: `FFF1`):设备信息读取
|
||
- **Writech DFU Service**(UUID: `FFF2`):OTA固件升级
|
||
|
||
**图像处理层**
|
||
|
||
图像处理层实现点阵码图案的识别与坐标解算:
|
||
- `codec/dot_decoder.c`:从摄像头原始灰度图像中识别Anoto点阵码图案,解算出全球唯一的纸面坐标(精度0.01mm,分辨率600DPI)
|
||
- `codec/stroke_encoder.c`:将坐标数据打包为压缩二进制格式,支持差分编码(降低数据量约60%)
|
||
|
||
**应用任务层**
|
||
|
||
应用任务层运行各业务RTOS任务,处理各功能的具体业务逻辑。每个任务在独立栈空间中运行,通过FreeRTOS队列和事件组进行协作。
|
||
|
||
### 2.4 数据设计
|
||
|
||
**Flash分区规划:**
|
||
|
||
```
|
||
nRF52840 内部Flash(1MB = 0x00100000)分区布局:
|
||
|
||
地址范围 | 大小 | 用途
|
||
--------------------|-------|----------------------------------
|
||
0x00000000-0x00026FFF | 156KB | Nordic SoftDevice S140(BLE协议栈)
|
||
0x00027000-0x0002FFFF | 36KB | Bootloader(含OTA引导逻辑)
|
||
0x00030000-0x0008FFFF | 384KB | Application分区A(当前运行)
|
||
0x00090000-0x000EFFFF | 384KB | Application分区B(OTA升级目标分区)
|
||
0x000F0000-0x000FDFFF | 56KB | NVS(非易失性存储:配对信息/设备配置)
|
||
0x000FE000-0x000FFFFF | 8KB | 保留(厂商信息/校准参数)
|
||
```
|
||
|
||
**外部Flash分区(4MB SPI NOR Flash):**
|
||
|
||
```
|
||
外部SPI NOR Flash 分区布局(4MB = 0x00400000):
|
||
|
||
地址范围 | 大小 | 用途
|
||
--------------------|-------|----------------------------------
|
||
0x000000-0x3EFFFF | ~4MB | 离线坐标缓存(FIFO环形队列)
|
||
0x3F0000-0x3FFFFF | 64KB | 保留(扩展配置/校准数据)
|
||
```
|
||
|
||
**内存(SRAM 256KB)使用分配:**
|
||
|
||
| 区域 | 大小 | 说明 |
|
||
|------|------|------|
|
||
| SoftDevice保留 | 约64KB | BLE协议栈内部使用 |
|
||
| 图像缓冲区(双缓冲) | 2 × 4KB = 8KB | 摄像头图像帧数据(乒乓缓冲) |
|
||
| 坐标队列 | 2KB | 待发送坐标数据队列(FreeRTOS队列) |
|
||
| 离线缓存写缓冲 | 4KB | 写入外部Flash的批量缓冲 |
|
||
| BLE发送缓冲 | 1KB | BLE Notify待发送数据包队列 |
|
||
| 任务栈合计 | 约9KB | 6个任务的栈空间之和 |
|
||
| 全局变量/堆 | 约24KB | 驱动状态、RTOS对象、临时缓冲 |
|
||
|
||
**核心数据结构(C结构体):**
|
||
|
||
```c
|
||
/* 坐标数据帧(coord_data.h) */
|
||
/* 每帧7字节,100Hz采样率下每秒700字节 */
|
||
typedef struct {
|
||
uint16_t x; /* 点阵X坐标(0~65535,精度0.01mm) */
|
||
uint16_t y; /* 点阵Y坐标(0~65535,精度0.01mm) */
|
||
uint8_t pressure; /* 笔尖压力(0~255,0=抬笔) */
|
||
uint8_t seq; /* 序列号(0~255循环,用于丢包检测) */
|
||
uint8_t flags; /* 标志位:bit0=pen_up, bit1=battery_low */
|
||
} coord_frame_t; /* 总大小:7字节 */
|
||
|
||
/* BLE数据包(每包最多包含34个坐标帧,238字节,适配BLE MTU=247)*/
|
||
typedef struct {
|
||
uint8_t packet_type; /* 0x01=坐标数据,0x02=电量,0x03=离线同步 */
|
||
uint8_t frame_count; /* 本包中坐标帧数量(1~34) */
|
||
uint16_t base_timestamp; /* 本包第一帧的时间戳低16位(毫秒) */
|
||
coord_frame_t frames[34]; /* 坐标帧数组 */
|
||
} ble_stroke_packet_t;
|
||
|
||
/* NVS存储的配对信息(per_bond.h) */
|
||
typedef struct {
|
||
uint8_t peer_addr[6]; /* 配对设备蓝牙地址 */
|
||
uint8_t ltk[16]; /* Long-Term Key(加密密钥) */
|
||
uint8_t irk[16]; /* Identity Resolving Key */
|
||
uint8_t peer_name[20]; /* 设备名称(可选) */
|
||
uint32_t last_connect_ts;/* 最后连接时间戳(Unix秒) */
|
||
} bond_info_t; /* 最多存储4条配对记录 */
|
||
|
||
/* 外部Flash离线缓存帧头(flash_cache.h) */
|
||
typedef struct {
|
||
uint32_t magic; /* 魔数:0xAA55CC33,用于帧对齐检测 */
|
||
uint32_t timestamp; /* 书写时间戳(Unix秒) */
|
||
uint16_t frame_count; /* 本组帧数量 */
|
||
uint16_t crc16; /* 本组数据CRC16校验和 */
|
||
} cache_group_header_t;
|
||
```
|
||
|
||
### 2.5 接口设计(BLE GATT)
|
||
|
||
**GATT Service和Characteristic定义:**
|
||
|
||
**Service 1:Writech Pen Data Service(UUID: FFF0)**
|
||
|
||
| Characteristic | UUID | 属性 | 说明 |
|
||
|---------------|------|------|------|
|
||
| Stroke Data | FFF1 | Notify | 实时笔迹坐标流(每包1~34帧坐标) |
|
||
| Pen Control | FFF2 | Write | 接收控制指令(开始/停止采集/请求电量) |
|
||
| Battery Level | FFF3 | Read / Notify | 电池电量(0~100%,低电量时主动Notify) |
|
||
|
||
**Service 2:Writech Device Info Service(UUID: FEE0)**
|
||
|
||
| Characteristic | UUID | 属性 | 说明 |
|
||
|---------------|------|------|------|
|
||
| Device Serial | FEE1 | Read | 设备序列号(16字节ASCII) |
|
||
| Firmware Version | FEE2 | Read | 固件版本号(如"V1.0.0") |
|
||
| Hardware Version | FEE3 | Read | 硬件版本号(如"HW_R1.2") |
|
||
| Calibration Params | FEE4 | Read / Write | 摄像头校准参数(出厂写入,可刷新) |
|
||
|
||
**Service 3:Writech DFU Service(UUID: FEF0)**
|
||
|
||
| Characteristic | UUID | 属性 | 说明 |
|
||
|---------------|------|------|------|
|
||
| DFU Control | FEF1 | Write / Indicate | OTA控制(开始/暂停/取消/确认) |
|
||
| DFU Packet | FEF2 | Write Without Response | 固件包分块传输(每块20字节) |
|
||
| DFU Status | FEF3 | Indicate | OTA进度上报(百分比/错误码) |
|
||
|
||
**BLE广播数据包格式:**
|
||
|
||
```
|
||
ADV_IND广播包:
|
||
├── Flags: LE General Discoverable Mode, BR/EDR Not Supported
|
||
├── Complete Local Name: "Writech-XXXXXX"(后6位为设备序列号后缀)
|
||
├── 16-bit Service UUID: FFF0(Writech Pen Data Service)
|
||
└── Manufacturer Specific Data:
|
||
byte[0-1]: 公司ID(深圳自然写科技,0x0FF2)
|
||
byte[2]: 设备类型(0x01=点阵笔)
|
||
byte[3]: 电量(0~100)
|
||
byte[4]: 固件主版本号
|
||
byte[5]: 连接状态(bit0=是否已连接)
|
||
```
|
||
|
||
### 2.6 安全设计
|
||
|
||
**BLE连接安全(LE Secure Connections):**
|
||
|
||
笔固件采用BLE 5.0 LE Secure Connections配对方案:
|
||
- 使用ECDH(Elliptic Curve Diffie-Hellman)密钥交换生成配对密钥
|
||
- 配对方式:Numeric Comparison(数字比对),用户在设备端确认6位数字一致
|
||
- 配对完成后建立LTK(Long-Term Key),后续重连使用LTK直接加密,无需重新配对
|
||
- LTK存储在内部Flash NVS区域,启用读保护防止被读取
|
||
|
||
**固件安全(Flash读保护):**
|
||
|
||
```c
|
||
/* 启用APPROTECT(访问端口保护),防止调试器读取Flash内容 */
|
||
/* 在Bootloader中设置:UICR.APPROTECT = 0xFFFFFF00 */
|
||
#define ENABLE_APPROTECT_ON_PRODUCTION 1
|
||
|
||
#if ENABLE_APPROTECT_ON_PRODUCTION
|
||
static void enable_flash_protection(void) {
|
||
/* 如果APPROTECT未锁定,执行锁定并重启 */
|
||
if (NRF_UICR->APPROTECT != 0xFFFFFF00) {
|
||
nrf_nvmc_uicr_write(&NRF_UICR->APPROTECT, 0xFFFFFF00);
|
||
NVIC_SystemReset();
|
||
}
|
||
}
|
||
#endif
|
||
```
|
||
|
||
**OTA升级安全:**
|
||
|
||
```
|
||
固件包安全验证流程(ota_task.c):
|
||
1. 接收完整固件包(BLE分块传输)
|
||
2. CRC32文件完整性校验(防止传输损坏)
|
||
3. RSA-2048签名验证(使用烧录在NVS中的厂商公钥)
|
||
4. 版本号校验(防止降级攻击,新版本号必须 > 当前版本)
|
||
5. 写入B分区(App B区)
|
||
6. 设置A/B引导标志,下次重启从B分区启动
|
||
7. 重启后验证B分区CRC(Bootloader执行)
|
||
8. 验证通过→正式切换;验证失败→清除B分区标志,继续从A分区启动
|
||
```
|
||
|
||
**离线缓存数据完整性:**
|
||
|
||
每个离线缓存组写入外部Flash时计算CRC16校验和,读取时验证。若发现数据损坏(CRC不匹配),自动跳过该组,防止坏数据污染后续数据同步。
|
||
|
||
**看门狗(Watchdog Timer):**
|
||
|
||
硬件看门狗定时器配置为32秒超时。软件在主任务中每10秒执行一次"喂狗"操作。若固件因死锁或异常未能及时喂狗,看门狗触发硬件复位,恢复正常运行。
|
||
|
||
### 2.7 Flash分区规划
|
||
|
||
```
|
||
内部Flash Bootloader设计:
|
||
┌──────────────────────────────────────────────────────┐
|
||
│ Bootloader(偏移0x27000,大小36KB) │
|
||
│ │
|
||
│ 上电 → 检查OTA标志位(NVS) │
|
||
│ ├── 标志 = "SWITCH_TO_B" → 验证B分区 │
|
||
│ │ ├── CRC32验证通过 + RSA签名验证通过 │
|
||
│ │ │ → 清除标志,从B分区启动 │
|
||
│ │ └── 验证失败 → 清除标志,保持A分区启动 │
|
||
│ └── 无标志(正常情况)→ 从A分区启动 │
|
||
│ │
|
||
│ 从目标分区跳转执行(设置MSP + 跳转到Reset_Handler) │
|
||
└──────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 第三章 核心模块功能详细说明
|
||
|
||
### 3.1 main.c — 主程序与RTOS启动
|
||
|
||
`main.c`是固件的启动入口,负责硬件初始化、BLE协议栈配置和RTOS任务创建。
|
||
|
||
**启动流程:**
|
||
|
||
```c
|
||
int main(void) {
|
||
/* 1. 时钟系统初始化(HFXO 32MHz晶振,LFXO 32.768kHz晶振) */
|
||
clocks_init();
|
||
|
||
/* 2. 日志系统初始化(RTT日志,调试期使用) */
|
||
log_init();
|
||
|
||
/* 3. GPIO初始化(摄像头使能脚、LED、振动马达) */
|
||
gpio_init();
|
||
|
||
/* 4. SPI总线初始化(摄像头SPI + Flash SPI) */
|
||
spi_init();
|
||
|
||
/* 5. ADC初始化(压力传感器 + 电池电压) */
|
||
adc_init();
|
||
|
||
/* 6. 外部Flash驱动初始化(挂载离线缓存文件系统) */
|
||
flash_driver_init();
|
||
offline_cache_init();
|
||
|
||
/* 7. BLE协议栈初始化(SoftDevice使能) */
|
||
ble_stack_init();
|
||
|
||
/* 8. GATT Service初始化(注册3个自定义Service) */
|
||
gatt_services_init();
|
||
|
||
/* 9. 广播初始化(配置ADV数据包,不立即开始广播) */
|
||
advertising_init();
|
||
|
||
/* 10. NVS初始化(读取配对信息 + 设备配置) */
|
||
nvs_init();
|
||
peer_manager_init();
|
||
|
||
/* 11. 电源管理初始化 */
|
||
power_manager_init();
|
||
|
||
/* 12. 看门狗定时器初始化(32秒超时) */
|
||
wdt_init();
|
||
|
||
/* 13. 创建RTOS任务 */
|
||
xTaskCreate(image_capture_task, "IMG", 1024, NULL, 5, NULL);
|
||
xTaskCreate(coord_calc_task, "CORD", 2048, NULL, 4, NULL);
|
||
xTaskCreate(ble_send_task, "BLE", 1024, NULL, 4, NULL);
|
||
xTaskCreate(power_task, "PWR", 512, NULL, 3, NULL);
|
||
xTaskCreate(led_task, "LED", 512, NULL, 2, NULL);
|
||
xTaskCreate(ota_task, "OTA", 4096, NULL, 1, NULL);
|
||
|
||
/* 14. 开始BLE广播 */
|
||
advertising_start();
|
||
|
||
/* 15. 启动RTOS调度器(此后main不再返回) */
|
||
vTaskStartScheduler();
|
||
|
||
/* 不应到达此处 */
|
||
for(;;);
|
||
}
|
||
```
|
||
|
||
**BLE协议栈初始化(ble_stack_init):**
|
||
|
||
```c
|
||
/* main.c — BLE协议栈初始化 */
|
||
static void ble_stack_init(void) {
|
||
uint32_t err_code;
|
||
uint32_t ram_start = 0;
|
||
|
||
/* 使能SoftDevice(指定低频时钟源:外部32.768kHz晶振) */
|
||
err_code = nrf_sdh_enable_request();
|
||
APP_ERROR_CHECK(err_code);
|
||
|
||
/* 配置SoftDevice BLE参数 */
|
||
ble_cfg_t ble_cfg;
|
||
memset(&ble_cfg, 0, sizeof(ble_cfg));
|
||
|
||
/* 最大连接数:1个主设备 + 0个从设备 */
|
||
ble_cfg.conn_cfg.conn_cfg_tag = APP_BLE_CONN_CFG_TAG;
|
||
ble_cfg.conn_cfg.params.gap_conn_cfg.conn_count = 1;
|
||
ble_cfg.conn_cfg.params.gap_conn_cfg.event_length = 6;
|
||
err_code = sd_ble_cfg_set(BLE_CONN_CFG_GAP, &ble_cfg, ram_start);
|
||
APP_ERROR_CHECK(err_code);
|
||
|
||
/* 使能SoftDevice BLE协议栈 */
|
||
err_code = nrf_sdh_ble_enable(&ram_start);
|
||
APP_ERROR_CHECK(err_code);
|
||
|
||
/* 注册BLE事件处理回调 */
|
||
NRF_SDH_BLE_OBSERVER(m_ble_observer, APP_BLE_OBSERVER_PRIO,
|
||
ble_evt_handler, NULL);
|
||
}
|
||
```
|
||
|
||
### 3.2 driver/camera.c — 点阵摄像头驱动
|
||
|
||
点阵摄像头驱动控制定制CMOS摄像头模组,实现100fps高频图像采集。
|
||
|
||
**摄像头初始化序列:**
|
||
|
||
```c
|
||
/* driver/camera.c */
|
||
#define CAMERA_SPI_INSTANCE 0 /* SPI0 */
|
||
#define CAMERA_CS_PIN NRF_GPIO_PIN_MAP(0, 3)
|
||
#define CAMERA_VSYNC_PIN NRF_GPIO_PIN_MAP(0, 4)
|
||
#define CAMERA_PWDN_PIN NRF_GPIO_PIN_MAP(0, 5)
|
||
#define CAMERA_IMAGE_SIZE 1024 /* 每帧图像字节数(32×32像素灰度图) */
|
||
|
||
/* 摄像头初始化(通过SPI写入寄存器配置序列) */
|
||
ret_code_t camera_init(void) {
|
||
ret_code_t err;
|
||
|
||
/* 1. 拉低PWDN脚(上电使能摄像头) */
|
||
nrf_gpio_pin_clear(CAMERA_PWDN_PIN);
|
||
nrf_delay_ms(10); /* 等待摄像头稳定 */
|
||
|
||
/* 2. 软复位 */
|
||
camera_write_reg(REG_RESET, 0x80);
|
||
nrf_delay_ms(5);
|
||
|
||
/* 3. 配置曝光时间(针对点阵纸反射强度优化) */
|
||
camera_write_reg(REG_EXPOSURE_H, 0x00);
|
||
camera_write_reg(REG_EXPOSURE_L, 0x64); /* 曝光时间约100us */
|
||
|
||
/* 4. 配置增益(自动增益控制使能) */
|
||
camera_write_reg(REG_GAIN_CTRL, 0x07); /* AGC使能,最大增益×8 */
|
||
|
||
/* 5. 配置帧率(100fps:PCLK分频系数) */
|
||
camera_write_reg(REG_CLKDIV, 0x01); /* 不分频,最大帧率 */
|
||
|
||
/* 6. 配置输出格式(8位灰度,32×32像素) */
|
||
camera_write_reg(REG_FORMAT, 0x00); /* 灰度模式 */
|
||
|
||
/* 7. 验证ID寄存器(防止硬件不兼容) */
|
||
uint8_t chip_id;
|
||
err = camera_read_reg(REG_CHIP_ID, &chip_id);
|
||
if (err != NRF_SUCCESS || chip_id != EXPECTED_CHIP_ID) {
|
||
return NRF_ERROR_NOT_FOUND;
|
||
}
|
||
|
||
return NRF_SUCCESS;
|
||
}
|
||
|
||
/* 采集一帧图像(由image_capture_task调用,每10ms一次) */
|
||
ret_code_t camera_capture_frame(uint8_t *buf, uint16_t buf_size) {
|
||
if (buf_size < CAMERA_IMAGE_SIZE) return NRF_ERROR_DATA_SIZE;
|
||
|
||
/* 等待VSYNC信号(帧同步,确保从帧起始处读取) */
|
||
uint32_t timeout = 1000;
|
||
while (nrf_gpio_pin_read(CAMERA_VSYNC_PIN) == 0 && timeout--);
|
||
if (timeout == 0) return NRF_ERROR_TIMEOUT;
|
||
|
||
/* SPI DMA读取图像数据(1024字节) */
|
||
return spi_dma_read(CAMERA_SPI_INSTANCE, buf, CAMERA_IMAGE_SIZE);
|
||
}
|
||
```
|
||
|
||
### 3.3 driver/pressure.c — 压力传感器驱动
|
||
|
||
压力传感器驱动读取笔尖压力ADC值,检测落笔(pen_down)和抬笔(pen_up)事件,并提供压感数值供上层使用。
|
||
|
||
**ADC采样与落笔检测:**
|
||
|
||
```c
|
||
/* driver/pressure.c */
|
||
#define PRESSURE_ADC_CHANNEL NRF_SAADC_INPUT_AIN0
|
||
#define PRESSURE_THRESHOLD_LOW 50 /* ADC值低于此值认为是抬笔(12位ADC,最大4095) */
|
||
#define PRESSURE_THRESHOLD_HIGH 80 /* ADC值高于此值认为是落笔 */
|
||
#define PRESSURE_OVERSAMPLE 16 /* 过采样次数,提升精度 */
|
||
|
||
/* 全局状态:当前落笔状态 */
|
||
static bool s_is_pen_down = false;
|
||
|
||
/* 采样一次压力值(含过采样平均) */
|
||
ret_code_t pressure_sample(uint8_t *pressure_normalized) {
|
||
int32_t sum = 0;
|
||
nrf_saadc_value_t sample;
|
||
|
||
/* 16次过采样取均值(降低噪声) */
|
||
for (int i = 0; i < PRESSURE_OVERSAMPLE; i++) {
|
||
nrf_drv_saadc_sample_convert(0, &sample);
|
||
sum += sample;
|
||
}
|
||
int32_t avg = sum / PRESSURE_OVERSAMPLE;
|
||
|
||
/* 归一化到 0~255 */
|
||
*pressure_normalized = (uint8_t)((avg * 255) / 4095);
|
||
|
||
/* 滞回检测(防止抖动触发误报) */
|
||
if (!s_is_pen_down && avg > PRESSURE_THRESHOLD_HIGH) {
|
||
s_is_pen_down = true;
|
||
/* 发布落笔事件到事件组 */
|
||
xEventGroupSetBitsFromISR(g_pen_events, EVENT_PEN_DOWN, NULL);
|
||
} else if (s_is_pen_down && avg < PRESSURE_THRESHOLD_LOW) {
|
||
s_is_pen_down = false;
|
||
/* 发布抬笔事件到事件组 */
|
||
xEventGroupSetBitsFromISR(g_pen_events, EVENT_PEN_UP, NULL);
|
||
}
|
||
|
||
return NRF_SUCCESS;
|
||
}
|
||
```
|
||
|
||
### 3.4 driver/battery.c — 电池电量监测驱动
|
||
|
||
```c
|
||
/* driver/battery.c */
|
||
/* 电池电量(电压→百分比)查表法(基于锂电池放电曲线) */
|
||
/* 电压采用分压电路采样(2:1分压,实际电池电压=ADC读数×2×3.6V/4095) */
|
||
|
||
static const uint16_t s_voltage_table[] = {
|
||
/* 电压(mV): 4200, 4100, 4000, 3900, 3800, 3700, 3600, 3500, 3400, 3300 */
|
||
4200, 4100, 4000, 3900, 3800, 3700, 3600, 3500, 3400, 3300
|
||
};
|
||
static const uint8_t s_percent_table[] = {
|
||
/* 对应百分比: 100%, 90%, 80%, 70%, 60%, 40%, 20%, 10%, 5%, 0% */
|
||
100, 90, 80, 70, 60, 40, 20, 10, 5, 0
|
||
};
|
||
|
||
uint8_t battery_get_level(void) {
|
||
nrf_saadc_value_t adc_val;
|
||
nrf_drv_saadc_sample_convert(1, &adc_val); /* ADC通道1:电池电压 */
|
||
|
||
/* 计算实际电压(mV):ADC_val * 2 * 3600mV / 4095 */
|
||
uint32_t voltage_mv = (uint32_t)adc_val * 7200 / 4095;
|
||
|
||
/* 查表插值计算电量百分比 */
|
||
for (int i = 0; i < 9; i++) {
|
||
if (voltage_mv >= s_voltage_table[i]) {
|
||
/* 线性插值 */
|
||
uint32_t diff_v = s_voltage_table[i] - s_voltage_table[i+1];
|
||
uint32_t diff_p = s_percent_table[i] - s_percent_table[i+1];
|
||
uint32_t offset = (s_voltage_table[i] - voltage_mv) * diff_p / diff_v;
|
||
return (uint8_t)(s_percent_table[i] - offset);
|
||
}
|
||
}
|
||
return 0; /* 电压过低 */
|
||
}
|
||
```
|
||
|
||
### 3.5 codec/dot_decoder.c — 点阵码坐标解码
|
||
|
||
点阵码解码是笔固件中最核心的算法模块,负责从摄像头采集的32×32灰度图像中识别Anoto点阵码,并解算出全球唯一的纸面坐标。
|
||
|
||
**Anoto点阵码原理简述:**
|
||
|
||
Anoto点阵码是一种编码纸面绝对坐标的视觉编码系统:
|
||
- 纸面印刷一个由微小圆点组成的点阵图案(点间距约0.3mm,600DPI打印)
|
||
- 每个圆点相对于理想网格位置有6种偏移(上/下/左/右及对角方向)
|
||
- 每个偏移值编码1位或多位信息(取决于编码方案版本)
|
||
- 摄像头拍摄6×6区域的点阵图案即可解算出该区域的全球唯一坐标
|
||
|
||
**解码流程(定点数优化实现):**
|
||
|
||
```c
|
||
/* codec/dot_decoder.c */
|
||
/* 为嵌入式MCU优化:使用Q15定点数替代浮点运算 */
|
||
|
||
typedef int16_t q15_t; /* Q15定点数:1位符号 + 15位小数 */
|
||
#define Q15_ONE (1 << 15)
|
||
|
||
/**
|
||
* @brief 从图像中解码点阵坐标
|
||
* @param image 输入灰度图像(32×32字节)
|
||
* @param x_out 输出X坐标(单位:0.01mm)
|
||
* @param y_out 输出Y坐标(单位:0.01mm)
|
||
* @return 0=成功,负值=解码失败(图像模糊/超出覆盖区域等)
|
||
*/
|
||
int32_t dot_decode(const uint8_t *image,
|
||
uint32_t *x_out, uint32_t *y_out) {
|
||
|
||
/* Step 1: 二值化(Otsu自适应阈值,定点数版本) */
|
||
uint8_t threshold = otsu_threshold_q15(image, 32*32);
|
||
uint8_t binary[32*32];
|
||
for (int i = 0; i < 32*32; i++) {
|
||
binary[i] = (image[i] < threshold) ? 1 : 0; /* 暗点=1,亮背景=0 */
|
||
}
|
||
|
||
/* Step 2: 连通域检测,提取圆点中心坐标列表 */
|
||
dot_center_t centers[MAX_DOTS_PER_FRAME]; /* 最多36个点(6×6区域) */
|
||
int dot_count = find_dot_centers(binary, 32, 32, centers);
|
||
|
||
if (dot_count < 20) {
|
||
return DOT_ERR_TOO_FEW_DOTS; /* 识别到的点太少,图像质量不足 */
|
||
}
|
||
|
||
/* Step 3: 估计点阵网格参数(间距、旋转角度) */
|
||
grid_params_t grid;
|
||
if (estimate_grid_params(centers, dot_count, &grid) != 0) {
|
||
return DOT_ERR_GRID_ESTIMATION_FAILED;
|
||
}
|
||
|
||
/* Step 4: 将各圆点映射到网格位置,计算偏移量 */
|
||
uint8_t offsets[36]; /* 最多6×6=36个点的偏移编码 */
|
||
int offset_count = calculate_dot_offsets(
|
||
centers, dot_count, &grid, offsets);
|
||
|
||
/* Step 5: 从偏移序列解码坐标值(查GF2^m有限域码表) */
|
||
uint32_t x_raw, y_raw;
|
||
if (decode_position_from_offsets(offsets, offset_count,
|
||
&x_raw, &y_raw) != 0) {
|
||
return DOT_ERR_POSITION_DECODE_FAILED;
|
||
}
|
||
|
||
/* Step 6: 坐标精化(亚像素级精度,利用圆点中心的插值位置) */
|
||
q15_t sub_x, sub_y;
|
||
compute_subpixel_offset(centers, dot_count, &grid, &sub_x, &sub_y);
|
||
|
||
/* Step 7: 合并整数坐标与亚像素偏移,输出0.01mm精度坐标 */
|
||
/* 1个基本坐标单位 = 0.3mm = 30个0.01mm单位 */
|
||
*x_out = x_raw * 30 + (int32_t)sub_x * 30 / Q15_ONE;
|
||
*y_out = y_raw * 30 + (int32_t)sub_y * 30 / Q15_ONE;
|
||
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
**解码性能指标:**
|
||
|
||
| 指标 | 规格 |
|
||
|------|------|
|
||
| 解码延迟(正常图像) | < 3ms(@64MHz M4F,含全部步骤) |
|
||
| 坐标精度 | 0.01mm(亚像素级精度) |
|
||
| 识别率(正常书写) | ≥ 99.5%(图像质量合格条件下) |
|
||
| 抗倾斜范围 | ±15°笔倾斜角(DFOV 20°摄像头视野内) |
|
||
| 最大书写速度 | 1.5m/s(100Hz采样,间距15mm以内不丢点) |
|
||
|
||
### 3.6 codec/stroke_encoder.c — 笔迹数据编码打包
|
||
|
||
笔迹编码器将原始坐标帧数组打包为BLE传输格式,采用差分编码降低数据量。
|
||
|
||
**差分编码原理:**
|
||
|
||
由于相邻坐标帧之间的位移通常较小(书写速度有限),使用差分编码(存储坐标差值而非绝对坐标)可大幅压缩数据量:
|
||
- 第一帧发送绝对坐标(4字节xy)
|
||
- 后续帧发送与前一帧的差值(如差值范围在±127内,仅需1字节/维)
|
||
- 典型情况下数据量减少约60%
|
||
|
||
```c
|
||
/* codec/stroke_encoder.c */
|
||
uint16_t stroke_encode_packet(
|
||
const coord_frame_t *frames, /* 输入:坐标帧数组 */
|
||
uint8_t frame_count, /* 帧数 */
|
||
uint8_t *out_buf, /* 输出:BLE数据包缓冲区 */
|
||
uint16_t buf_size) { /* 返回:实际填充字节数 */
|
||
|
||
if (frame_count == 0 || !frames || !out_buf) return 0;
|
||
|
||
uint8_t *ptr = out_buf;
|
||
|
||
/* 包头:类型(1B) + 帧数(1B) + 时间戳(2B) */
|
||
*ptr++ = PKT_TYPE_STROKE;
|
||
*ptr++ = frame_count;
|
||
uint16_t base_ts = (uint16_t)(frames[0].seq * 10); /* 基准时间(毫秒低16位) */
|
||
memcpy(ptr, &base_ts, 2); ptr += 2;
|
||
|
||
/* 第一帧:绝对坐标 */
|
||
memcpy(ptr, &frames[0].x, 2); ptr += 2;
|
||
memcpy(ptr, &frames[0].y, 2); ptr += 2;
|
||
*ptr++ = frames[0].pressure;
|
||
*ptr++ = frames[0].flags;
|
||
|
||
/* 后续帧:差分编码 */
|
||
for (int i = 1; i < frame_count; i++) {
|
||
int16_t dx = (int16_t)(frames[i].x - frames[i-1].x);
|
||
int16_t dy = (int16_t)(frames[i].y - frames[i-1].y);
|
||
|
||
/* 编码标志位:bit7=dx扩展(需2字节),bit6=dy扩展 */
|
||
uint8_t enc_flags = frames[i].flags;
|
||
if (dx < -127 || dx > 127) enc_flags |= 0x80;
|
||
if (dy < -127 || dy > 127) enc_flags |= 0x40;
|
||
*ptr++ = enc_flags;
|
||
|
||
if (enc_flags & 0x80) { memcpy(ptr, &dx, 2); ptr += 2; }
|
||
else { *ptr++ = (int8_t)dx; }
|
||
|
||
if (enc_flags & 0x40) { memcpy(ptr, &dy, 2); ptr += 2; }
|
||
else { *ptr++ = (int8_t)dy; }
|
||
|
||
*ptr++ = frames[i].pressure;
|
||
}
|
||
|
||
return (uint16_t)(ptr - out_buf);
|
||
}
|
||
```
|
||
|
||
### 3.7 task/image_capture_task.c — 图像采集任务
|
||
|
||
图像采集任务是固件中优先级最高的任务,以100Hz(每10ms一次)定时触发,驱动摄像头采集图像并放入图像队列供坐标计算任务处理。
|
||
|
||
```c
|
||
/* task/image_capture_task.c */
|
||
/* 双缓冲(Ping-Pong)设计:采集任务写入缓冲区A时,计算任务处理缓冲区B */
|
||
static uint8_t s_img_buf_a[CAMERA_IMAGE_SIZE];
|
||
static uint8_t s_img_buf_b[CAMERA_IMAGE_SIZE];
|
||
static bool s_use_buf_a = true; /* 当前采集写入哪个缓冲区 */
|
||
|
||
void image_capture_task(void *param) {
|
||
TickType_t last_wake_time = xTaskGetTickCount();
|
||
const TickType_t period = pdMS_TO_TICKS(10); /* 10ms = 100Hz */
|
||
|
||
for (;;) {
|
||
/* 精确10ms周期触发(vTaskDelayUntil保证累计误差不漂移) */
|
||
vTaskDelayUntil(&last_wake_time, period);
|
||
|
||
/* 喂看门狗(每10ms喂一次,超时32秒触发复位) */
|
||
nrf_drv_wdt_channel_feed(g_wdt_channel);
|
||
|
||
/* 选择当前写入缓冲区 */
|
||
uint8_t *write_buf = s_use_buf_a ? s_img_buf_a : s_img_buf_b;
|
||
|
||
/* 采集一帧图像(SPI DMA,非阻塞) */
|
||
ret_code_t err = camera_capture_frame(write_buf, CAMERA_IMAGE_SIZE);
|
||
|
||
if (err == NRF_SUCCESS) {
|
||
/* 将缓冲区指针发送到坐标计算队列(不阻塞,满则丢帧) */
|
||
BaseType_t sent = xQueueSend(g_image_queue, &write_buf, 0);
|
||
if (sent == pdTRUE) {
|
||
/* 切换缓冲区 */
|
||
s_use_buf_a = !s_use_buf_a;
|
||
} else {
|
||
/* 队列满:坐标计算任务处理不过来,丢弃本帧 */
|
||
g_stat_dropped_frames++;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.8 task/coord_calc_task.c — 坐标计算任务
|
||
|
||
坐标计算任务从图像队列读取图像帧,调用点阵码解码算法计算坐标,并将坐标数据帧写入坐标队列。
|
||
|
||
```c
|
||
/* task/coord_calc_task.c */
|
||
void coord_calc_task(void *param) {
|
||
uint8_t *img_buf;
|
||
coord_frame_t frame;
|
||
uint8_t seq = 0;
|
||
|
||
for (;;) {
|
||
/* 阻塞等待图像队列 */
|
||
if (xQueueReceive(g_image_queue, &img_buf, portMAX_DELAY) != pdTRUE) {
|
||
continue;
|
||
}
|
||
|
||
/* 采样压力值(与图像同步,保证时序对应) */
|
||
uint8_t pressure;
|
||
pressure_sample(&pressure);
|
||
|
||
/* 调用点阵码解码算法 */
|
||
uint32_t x, y;
|
||
int32_t decode_result = dot_decode(img_buf, &x, &y);
|
||
|
||
if (decode_result == 0) {
|
||
/* 解码成功:构建坐标帧 */
|
||
frame.x = (uint16_t)(x & 0xFFFF);
|
||
frame.y = (uint16_t)(y & 0xFFFF);
|
||
frame.pressure = pressure;
|
||
frame.seq = seq++;
|
||
frame.flags = (pressure < PRESSURE_THRESHOLD_LOW) ? FLAG_PEN_UP : 0;
|
||
|
||
/* 低电量标志 */
|
||
if (battery_get_level() <= BATTERY_LOW_THRESHOLD) {
|
||
frame.flags |= FLAG_BATTERY_LOW;
|
||
}
|
||
|
||
/* 写入坐标队列(BLE发送任务消费) */
|
||
xQueueSend(g_coord_queue, &frame, 0);
|
||
|
||
/* 如果BLE未连接,写入离线Flash缓存 */
|
||
if (!ble_is_connected()) {
|
||
offline_cache_write(&frame);
|
||
}
|
||
|
||
} else {
|
||
/* 解码失败(图像模糊/笔尖抬起):发送纯压力帧 */
|
||
frame.x = 0;
|
||
frame.y = 0;
|
||
frame.pressure = pressure;
|
||
frame.seq = seq++;
|
||
frame.flags = FLAG_PEN_UP | FLAG_NO_POSITION;
|
||
|
||
if (pressure < PRESSURE_THRESHOLD_LOW) {
|
||
/* 确认抬笔,向BLE发送抬笔事件 */
|
||
xQueueSend(g_coord_queue, &frame, 0);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.9 task/ble_send_task.c — BLE数据发送任务
|
||
|
||
BLE发送任务从坐标队列读取坐标帧,积累一定数量后打包编码,通过BLE Notify方式发送至已连接的设备。
|
||
|
||
```c
|
||
/* task/ble_send_task.c */
|
||
#define BLE_NOTIFY_INTERVAL_MS 20 /* 每20ms发送一次(50Hz发送,100Hz采样2帧/包) */
|
||
#define MAX_FRAMES_PER_PKT 34 /* 每包最多34帧(适配BLE MTU=247字节) */
|
||
|
||
void ble_send_task(void *param) {
|
||
coord_frame_t batch[MAX_FRAMES_PER_PKT];
|
||
uint8_t batch_count = 0;
|
||
TickType_t last_send_time = xTaskGetTickCount();
|
||
|
||
for (;;) {
|
||
coord_frame_t frame;
|
||
/* 等待坐标帧(最多等到下次发送时刻) */
|
||
TickType_t wait_time = pdMS_TO_TICKS(BLE_NOTIFY_INTERVAL_MS);
|
||
|
||
if (xQueueReceive(g_coord_queue, &frame, wait_time) == pdTRUE) {
|
||
batch[batch_count++] = frame;
|
||
}
|
||
|
||
/* 达到发送时间或包满 → 打包发送 */
|
||
bool time_to_send = (xTaskGetTickCount() - last_send_time) >=
|
||
pdMS_TO_TICKS(BLE_NOTIFY_INTERVAL_MS);
|
||
|
||
if ((batch_count > 0) && (time_to_send || batch_count >= MAX_FRAMES_PER_PKT)) {
|
||
|
||
if (ble_is_connected() && ble_notify_enabled()) {
|
||
/* 编码为BLE数据包 */
|
||
uint8_t pkt_buf[BLE_MAX_MTU];
|
||
uint16_t pkt_len = stroke_encode_packet(
|
||
batch, batch_count, pkt_buf, sizeof(pkt_buf));
|
||
|
||
/* 发送BLE Notify */
|
||
uint32_t err = ble_nus_data_send(
|
||
&m_ble_conn_handle, pkt_buf, &pkt_len);
|
||
|
||
if (err == NRF_SUCCESS) {
|
||
g_stat_ble_packets_sent++;
|
||
g_stat_ble_bytes_sent += pkt_len;
|
||
} else if (err == NRF_ERROR_RESOURCES) {
|
||
/* BLE发送缓冲区满,重试(最多3次) */
|
||
vTaskDelay(pdMS_TO_TICKS(5));
|
||
ble_nus_data_send(&m_ble_conn_handle, pkt_buf, &pkt_len);
|
||
}
|
||
}
|
||
|
||
batch_count = 0;
|
||
last_send_time = xTaskGetTickCount();
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.10 task/power_task.c — 电源管理任务
|
||
|
||
电源管理任务以1Hz频率运行,负责电量采样、充电状态监测和功耗模式转换。
|
||
|
||
**功耗模式转换策略:**
|
||
|
||
```c
|
||
/* task/power_task.c */
|
||
/* 电源状态机定义 */
|
||
typedef enum {
|
||
POWER_STATE_ACTIVE, /* 活跃:BLE连接且持续书写 */
|
||
POWER_STATE_IDLE, /* 空闲:BLE连接但无书写(持续5秒) */
|
||
POWER_STATE_SLEEP, /* 休眠:无BLE连接(保留广播,降低摄像头帧率) */
|
||
POWER_STATE_DEEP_SLEEP /* 深度休眠:无书写且无BLE超过3分钟 */
|
||
} power_state_t;
|
||
|
||
static power_state_t s_power_state = POWER_STATE_SLEEP;
|
||
static uint32_t s_idle_seconds = 0; /* 空闲计时 */
|
||
static uint32_t s_no_write_seconds = 0; /* 无书写计时 */
|
||
|
||
void power_task(void *param) {
|
||
TickType_t last_wake = xTaskGetTickCount();
|
||
|
||
for (;;) {
|
||
vTaskDelayUntil(&last_wake, pdMS_TO_TICKS(1000)); /* 1Hz */
|
||
|
||
/* 1. 采样电池电量 */
|
||
uint8_t level = battery_get_level();
|
||
g_battery_level = level;
|
||
|
||
/* 低电量告警(≤15%:慢闪红灯;≤5%:发BLE通知后关机) */
|
||
if (level <= 5) {
|
||
ble_send_battery_notify(level);
|
||
vTaskDelay(pdMS_TO_TICKS(500));
|
||
power_system_off(); /* 进入System OFF(0.2μA) */
|
||
} else if (level <= 15) {
|
||
xEventGroupSetBits(g_led_events, LED_EVENT_LOW_BATTERY);
|
||
}
|
||
|
||
/* 2. 检查充电状态(通过I2C读充电IC状态寄存器) */
|
||
bool is_charging = charger_is_charging();
|
||
if (is_charging != g_is_charging) {
|
||
g_is_charging = is_charging;
|
||
xEventGroupSetBits(g_led_events, LED_EVENT_CHARGE_CHANGED);
|
||
}
|
||
|
||
/* 3. 功耗状态机转换 */
|
||
bool pen_is_writing = (g_last_coord_seq_changed_within_1s);
|
||
bool ble_connected = ble_is_connected();
|
||
|
||
power_state_t new_state = s_power_state;
|
||
|
||
switch (s_power_state) {
|
||
case POWER_STATE_ACTIVE:
|
||
if (!pen_is_writing) {
|
||
s_idle_seconds++;
|
||
if (s_idle_seconds > 5) new_state = POWER_STATE_IDLE;
|
||
} else {
|
||
s_idle_seconds = 0;
|
||
}
|
||
break;
|
||
|
||
case POWER_STATE_IDLE:
|
||
if (pen_is_writing) {
|
||
new_state = POWER_STATE_ACTIVE;
|
||
s_idle_seconds = 0;
|
||
} else if (!ble_connected) {
|
||
new_state = POWER_STATE_SLEEP;
|
||
}
|
||
break;
|
||
|
||
case POWER_STATE_SLEEP:
|
||
if (ble_connected && pen_is_writing) {
|
||
new_state = POWER_STATE_ACTIVE;
|
||
s_no_write_seconds = 0;
|
||
} else {
|
||
s_no_write_seconds++;
|
||
if (s_no_write_seconds > 180) { /* 3分钟无操作 */
|
||
new_state = POWER_STATE_DEEP_SLEEP;
|
||
}
|
||
}
|
||
break;
|
||
|
||
case POWER_STATE_DEEP_SLEEP:
|
||
/* 按下笔帽按键唤醒(SENSE引脚中断) */
|
||
break;
|
||
}
|
||
|
||
/* 状态转换处理 */
|
||
if (new_state != s_power_state) {
|
||
power_apply_state(new_state);
|
||
s_power_state = new_state;
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3.11 task/led_task.c — LED状态指示任务
|
||
|
||
LED任务控制RGB三色LED,根据系统状态显示不同颜色和动效,提供直观的用户反馈。
|
||
|
||
**LED状态对应表:**
|
||
|
||
| 状态 | LED颜色 | 动效 | 说明 |
|
||
|------|---------|------|------|
|
||
| 开机/广播中 | 蓝色 | 慢闪(1Hz) | 等待蓝牙连接 |
|
||
| 配对请求 | 白色 | 快闪(4Hz) | 等待用户确认配对 |
|
||
| 已连接 | 蓝色 | 常亮 | BLE连接正常 |
|
||
| 书写中 | 蓝色 | 呼吸灯(2s周期) | 检测到书写动作 |
|
||
| 充电中 | 红色 | 慢闪(0.5Hz) | USB充电中 |
|
||
| 充电完成 | 绿色 | 常亮 | 电量100% |
|
||
| 低电量(≤15%) | 红色 | 慢闪(1Hz) | 需要充电 |
|
||
| 极低电量(≤5%) | 红色 | 快闪(4Hz) | 即将自动关机 |
|
||
| OTA升级中 | 黄色 | 快闪 | 固件升级进行中 |
|
||
| OTA成功 | 绿色 | 闪3次后熄灭 | 升级成功,即将重启 |
|
||
| OTA失败 | 红色 | 闪3次后恢复正常 | 升级失败,保持原版本 |
|
||
|
||
### 3.12 task/ota_task.c — OTA固件升级任务
|
||
|
||
OTA任务实现通过BLE DFU协议接收固件包,验证后写入B分区并触发系统重启以完成升级。
|
||
|
||
**OTA状态机:**
|
||
|
||
```c
|
||
/* task/ota_task.c */
|
||
typedef enum {
|
||
OTA_STATE_IDLE = 0,
|
||
OTA_STATE_INIT, /* 初始化:清除B分区,准备接收 */
|
||
OTA_STATE_RECEIVING, /* 接收固件分块数据 */
|
||
OTA_STATE_VERIFYING, /* 校验CRC32 + RSA签名 */
|
||
OTA_STATE_WRITING, /* 写入Flash B分区 */
|
||
OTA_STATE_DONE, /* 完成,等待重启确认 */
|
||
OTA_STATE_ERROR /* 错误,放弃升级 */
|
||
} ota_state_t;
|
||
|
||
/* OTA完成后的处理 */
|
||
static void ota_finalize_upgrade(void) {
|
||
/* 1. 在NVS中设置"切换到B分区"标志 */
|
||
nvs_write_u8(NVS_KEY_OTA_FLAG, OTA_SWITCH_TO_B);
|
||
|
||
/* 2. 发送OTA完成通知(BLE Indicate) */
|
||
ble_dfu_send_status(DFU_STATUS_SUCCESS, 100);
|
||
|
||
/* 3. 延迟1秒等待BLE数据发送完成,然后重启 */
|
||
vTaskDelay(pdMS_TO_TICKS(1000));
|
||
|
||
/* 4. 触发软件复位(AIRCR.SYSRESETREQ) */
|
||
sd_nvic_SystemReset();
|
||
}
|
||
```
|
||
|
||
### 3.13 cache/offline_cache.c — 离线数据缓存
|
||
|
||
离线缓存模块管理外部SPI Flash的坐标数据缓存,在BLE未连接时保存书写数据,连接后自动同步。
|
||
|
||
**FIFO环形缓存设计:**
|
||
|
||
```
|
||
外部Flash 4MB,划分为:
|
||
- 4096个扇区(每扇区1KB)
|
||
- 采用双指针FIFO设计:
|
||
write_ptr:下一次写入的扇区位置
|
||
read_ptr :下一次读取的扇区位置
|
||
|
||
写入:每组数据(group_header + N×coord_frame)对齐到扇区边界
|
||
write_ptr + 1 == read_ptr(满)时:停止写入,丢弃最新数据
|
||
读取:连接后顺序读取 read_ptr 到 write_ptr 之间的所有数据
|
||
清空:成功同步后,read_ptr = write_ptr(逻辑清空,无需实际擦除)
|
||
磨损均衡:每2000次写入后将头部扇区向后移动,均匀磨损各扇区
|
||
```
|
||
|
||
### 3.14 power/power_manager.c — 低功耗状态机
|
||
|
||
电源管理器根据功耗状态对各硬件模块进行电源控制,在不影响功能的前提下最大化续航。
|
||
|
||
**各功耗状态的硬件配置:**
|
||
|
||
| 功耗状态 | 摄像头 | ADC采样率 | BLE广播间隔 | CPU速度 | 功耗估算 |
|
||
|---------|--------|----------|------------|---------|---------|
|
||
| ACTIVE(活跃) | 100fps | 100Hz | 连接态(CI 15ms) | 64MHz | ~15mA |
|
||
| IDLE(空闲) | 10fps | 10Hz | 连接态(CI 200ms) | 16MHz | ~5mA |
|
||
| SLEEP(休眠) | 关闭 | 1Hz | 1s广播间隔 | 4MHz | ~0.5mA |
|
||
| DEEP_SLEEP(深睡) | 关闭 | 关闭 | 关闭广播 | 停止(待中断唤醒) | ~2μA |
|
||
|
||
---
|
||
|
||
## 第四章 操作流程与使用步骤
|
||
|
||
### 4.1 点阵笔开机流程
|
||
|
||
```
|
||
用户操作:按下笔帽末端开关键(长按1.5秒开机)
|
||
│
|
||
├─ 固件检测按键中断(GPIO SENSE唤醒)
|
||
│
|
||
├─ 执行 main() 初始化流程(约300ms)
|
||
│ ├── 硬件自检(Flash R/W测试、摄像头ID校验)
|
||
│ └── 自检失败→红色快闪3次提示硬件故障
|
||
│
|
||
├─ 读取NVS配对记录
|
||
│ ├── 有配对记录→优先尝试定向广播(针对已配对设备)
|
||
│ └── 无配对记录→开启无向广播(等待新设备配对)
|
||
│
|
||
├─ 开始广播
|
||
│ └── LED:蓝色慢闪(等待连接)
|
||
│
|
||
└─ 等待BLE连接(或超时60秒后进入SLEEP模式节省电量)
|
||
```
|
||
|
||
### 4.2 蓝牙配对与连接流程
|
||
|
||
```
|
||
新设备首次配对:
|
||
┌──────────────────────────────────────────────────────┐
|
||
│ 终端APP(手机/黑板) 点阵笔固件 │
|
||
│ │
|
||
│ 扫描BLE设备 ────────────────→ 广播ADV_IND │
|
||
│ 找到"Writech-XXXXXX" │
|
||
│ 点击连接 ────────────────────────────→ 接受连接请求 │
|
||
│ │
|
||
│ ←──── 连接建立(Connection Established) │
|
||
│ │
|
||
│ 发起配对请求 ────────────────→ 接受配对请求 │
|
||
│ 使用"Just Works"或"Numeric Comparison" │
|
||
│ LE Secure Connections配对开始 │
|
||
│ │
|
||
│ 显示6位确认码 ←────── 显示相同6位数字(LED白色快闪) │
|
||
│ 用户确认:Yes │
|
||
│ ────────────────────────────→ 确认配对 │
|
||
│ │
|
||
│ 配对成功:存储LTK ←──── 存储LTK到NVS(最多4条) │
|
||
│ ←──── LED:蓝色常亮(连接成功),振动马达振一下 │
|
||
│ │
|
||
│ 订阅Stroke Data Notify ────────────────→ 启用Notify │
|
||
│ 开始接收笔迹数据 ←──── 实时推送坐标数据包 │
|
||
└──────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**已配对设备重连(自动):**
|
||
|
||
固件在广播时携带已配对设备的地址(定向广播)。已配对的终端APP在扫描时发现该广播后,自动发起重连请求,固件验证LTK后恢复加密连接,全程无需用户干预(约3秒完成重连)。
|
||
|
||
### 4.3 书写采集与数据传输流程
|
||
|
||
```
|
||
学生书写流程(实时传输模式):
|
||
|
||
1. 用户持笔书写于点阵纸上
|
||
│
|
||
2. 笔尖接触纸面(压力传感器 pressure > 阈值)
|
||
→ 触发 EVENT_PEN_DOWN
|
||
│
|
||
3. image_capture_task:100Hz连续采集摄像头图像
|
||
→ 写入双缓冲图像队列
|
||
│
|
||
4. coord_calc_task:实时解码每帧图像
|
||
→ 输出精确坐标(x,y,pressure,flags)
|
||
→ 写入坐标队列
|
||
│
|
||
5. ble_send_task:每20ms积累2帧,打包差分编码
|
||
→ BLE Notify发送(BLE包约20字节)
|
||
│
|
||
6. 笔尖离开纸面(pressure < 阈值)
|
||
→ 触发 EVENT_PEN_UP
|
||
→ 发送抬笔标志帧(flags |= FLAG_PEN_UP)
|
||
|
||
7. 终端APP(网关/黑板/手机)收到坐标数据
|
||
→ 转发至云端或算力盒进行AI识别
|
||
```
|
||
|
||
### 4.4 离线书写与数据同步流程
|
||
|
||
```
|
||
离线书写(无BLE连接时):
|
||
|
||
1. 笔处于SLEEP模式(无连接)
|
||
│
|
||
2. 用户开始书写 → 摄像头采集 → 坐标解码
|
||
│
|
||
3. coord_calc_task检测到BLE未连接
|
||
→ offline_cache_write(frame) 写入外部Flash
|
||
│
|
||
4. 持续书写,每组数据带时间戳写入缓冲
|
||
(缓存满4MB=约10万点=约10页书写时,停止缓存,仅更新时间戳)
|
||
│
|
||
离线数据同步(重新建立BLE连接时):
|
||
|
||
5. BLE连接建立,ble_evt_handler触发 EVT_CONNECTED
|
||
│
|
||
6. ble_send_task 检测到有离线数据(offline_cache_get_count() > 0)
|
||
→ 先向对端发送"离线数据同步开始"通知
|
||
→ 切换数据包类型为 PKT_TYPE_OFFLINE_SYNC
|
||
│
|
||
7. 顺序读取Flash缓存,按组发送(每组含时间戳信息)
|
||
→ 使用 BLE GATT Indicate(需要ACK确认,保证可靠传输)
|
||
│
|
||
8. 所有数据发送完成
|
||
→ 接收方确认(ACK)
|
||
→ offline_cache_clear() 清空Flash缓存(移动read_ptr)
|
||
→ 切换回实时传输模式
|
||
```
|
||
|
||
### 4.5 充电与电量管理
|
||
|
||
```
|
||
充电状态机:
|
||
USB接入 → 充电IC检测到电源
|
||
│
|
||
├── 充电开始:LED红色慢闪
|
||
│ 电量 < 80%:500mA快充
|
||
│ 电量 80-100%:涓流充电
|
||
│
|
||
├── 充电完成(电量100%):LED绿色常亮
|
||
│
|
||
└── USB拔出:LED恢复蓝色状态指示
|
||
|
||
电量提示规则(写入BLE Notify主动上报):
|
||
- 电量每变化1%时:更新 Battery Level Characteristic
|
||
- 电量降至50%:主动Notify一次(提醒用户关注电量)
|
||
- 电量降至15%:LED红色慢闪 + 主动Notify
|
||
- 电量降至5%:LED红色快闪 + Notify + 30秒后自动关机
|
||
```
|
||
|
||
### 4.6 OTA固件升级流程
|
||
|
||
**通过终端APP进行OTA(用户视角):**
|
||
|
||
1. 厂商将新固件包(.zip,含.bin + RSA签名文件)上传至云端
|
||
2. 终端APP(手机/PC)检测到新版本,提示用户升级
|
||
3. 用户点击"立即升级",APP通过BLE与点阵笔建立DFU连接
|
||
4. APP自动完成固件下载和传输(约2-5分钟,取决于BLE信号强度)
|
||
5. LED变为黄色快闪(升级进行中),用户保持笔与APP蓝牙距离
|
||
6. 升级完成:LED绿色闪3次,笔自动重启(约3秒)
|
||
7. 重启后连接,APP显示"固件已更新至vX.X.X"
|
||
|
||
**升级异常处理:**
|
||
|
||
| 异常情况 | 处理方式 |
|
||
|---------|---------|
|
||
| 传输中断(BLE断开) | 支持断点续传(记录已接收分块序号),重连后继续 |
|
||
| CRC校验失败 | 放弃本次升级,保留A分区,LED红色闪3次 |
|
||
| RSA签名验证失败 | 拒绝安装,视为非法固件,触发安全告警 |
|
||
| B分区启动失败 | Bootloader自动回滚至A分区,版本不变 |
|
||
| 升级中电量不足 | 电量<20%时拒绝启动OTA,提示先充电 |
|
||
|
||
### 4.7 出厂校准与测试流程
|
||
|
||
**生产烧录流程:**
|
||
|
||
```
|
||
生产线操作步骤:
|
||
1. 连接J-Link调试器(SWD接口)
|
||
2. 烧录 Bootloader(0x27000)
|
||
3. 烧录 SoftDevice(0x00000000)
|
||
4. 烧录 App A 固件(0x30000)
|
||
5. 写入 NVS 出厂信息:
|
||
- 设备序列号(唯一,扫描二维码写入)
|
||
- 硬件版本号
|
||
- 摄像头校准参数(每支笔独立校准)
|
||
6. 启用 APPROTECT(Flash读保护)
|
||
7. 自动化测试:
|
||
- BLE广播检测(天线测试)
|
||
- 摄像头采集测试(点阵纸图像解码验证)
|
||
- 压力传感器量程测试
|
||
- 电池电量校准
|
||
```
|
||
|
||
### 4.8 常见问题处理
|
||
|
||
| 问题现象 | 可能原因 | 处理方法 |
|
||
|---------|---------|---------|
|
||
| 笔无法开机 | 电量耗尽 | 充电至少5分钟后再开机 |
|
||
| BLE无法发现笔 | 广播超时进入SLEEP | 长按笔帽开关1.5秒重新开机 |
|
||
| 书写坐标飘移 | 摄像头焦距偏移 | 联系售后,执行校准流程 |
|
||
| 离线数据同步慢 | 缓存数据量大 | 保持BLE连接,等待同步完成(约每1000点需10秒) |
|
||
| OTA升级失败 | 固件包版本低于当前 | 检查APP中固件版本,使用正确的升级包 |
|
||
| LED不亮 | LED驱动故障 | 功能不受影响,联系售后检测 |
|
||
|
||
---
|
||
|
||
## 第五章 与源代码的对应关系
|
||
|
||
### 5.1 模块名称与源代码文件对应表
|
||
|
||
| 文档章节 | 源代码文件 | 语言 | 说明 |
|
||
|---------|----------|------|------|
|
||
| 主程序与RTOS启动 | `main.c` | C | 系统初始化、任务创建、BLE协议栈配置 |
|
||
| 点阵摄像头驱动 | `driver/camera.c` | C | SPI摄像头图像采集驱动 |
|
||
| 压力传感器驱动 | `driver/pressure.c` | C | ADC压力采样,落笔/抬笔事件检测 |
|
||
| 电池电量检测 | `driver/battery.c` | C | 电池电压ADC采样,查表法计算电量 |
|
||
| 外部Flash驱动 | `driver/flash.c` | C | SPI NOR Flash读写驱动,磨损均衡 |
|
||
| LED驱动 | `driver/led.c` | C | RGB LED PWM驱动,动效控制 |
|
||
| 点阵码解码 | `codec/dot_decoder.c` | C | Anoto点阵码识别与坐标解算(定点数实现) |
|
||
| 笔迹数据编码 | `codec/stroke_encoder.c` | C | 差分编码,BLE数据包打包 |
|
||
| 图像采集任务 | `task/image_capture_task.c` | C | 100Hz定时摄像头采集RTOS任务 |
|
||
| 坐标计算任务 | `task/coord_calc_task.c` | C | 调用解码算法,输出坐标帧 |
|
||
| BLE数据发送任务 | `task/ble_send_task.c` | C | 坐标打包编码,BLE Notify发送 |
|
||
| 电源管理任务 | `task/power_task.c` | C | 电量采样,功耗状态机管理 |
|
||
| LED状态指示任务 | `task/led_task.c` | C | 事件驱动LED动效控制 |
|
||
| OTA固件升级任务 | `task/ota_task.c` | C | BLE DFU协议,固件包接收校验写入 |
|
||
| 离线数据缓存 | `cache/offline_cache.c` | C | 外部Flash FIFO缓存管理 |
|
||
| 低功耗管理 | `power/power_manager.c` | C | 四级功耗状态切换,硬件电源控制 |
|
||
| 配对管理 | `ble/peer_manager.c` | C | BLE配对记录NVS存取管理 |
|
||
| BLE事件处理 | `ble/ble_evt_handler.c` | C | SoftDevice BLE事件回调分发 |
|
||
| GATT Service实现 | `ble/gatt_services.c` | C | 自定义GATT Service注册与数据处理 |
|
||
| 中断向量表 | `startup/startup_nrf52840.s` | ASM | 芯片启动文件,中断向量表 |
|
||
| 链接脚本 | `ld/nrf52840_app.ld` | LD | Flash/RAM分区分配 |
|
||
|
||
### 5.2 核心函数说明
|
||
|
||
| 函数名 | 所在文件 | 功能说明 |
|
||
|--------|---------|---------|
|
||
| `main()` | `main.c` | 固件启动入口,硬件初始化与RTOS任务创建 |
|
||
| `camera_init()` | `driver/camera.c` | 摄像头SPI初始化,寄存器配置 |
|
||
| `camera_capture_frame()` | `driver/camera.c` | 采集一帧图像(SPI DMA读取) |
|
||
| `pressure_sample()` | `driver/pressure.c` | 采样一次压力值,检测落笔/抬笔事件 |
|
||
| `battery_get_level()` | `driver/battery.c` | 读取电池电量百分比 |
|
||
| `dot_decode()` | `codec/dot_decoder.c` | 核心算法:从图像解算点阵坐标 |
|
||
| `otsu_threshold_q15()` | `codec/dot_decoder.c` | 定点数Otsu自适应二值化 |
|
||
| `stroke_encode_packet()` | `codec/stroke_encoder.c` | 差分编码打包为BLE数据包 |
|
||
| `image_capture_task()` | `task/image_capture_task.c` | 100Hz图像采集RTOS任务 |
|
||
| `coord_calc_task()` | `task/coord_calc_task.c` | 坐标解算RTOS任务 |
|
||
| `ble_send_task()` | `task/ble_send_task.c` | BLE数据发送RTOS任务 |
|
||
| `power_task()` | `task/power_task.c` | 电源监控与状态机管理 |
|
||
| `power_apply_state()` | `power/power_manager.c` | 执行功耗状态切换(调整各外设电源) |
|
||
| `offline_cache_write()` | `cache/offline_cache.c` | 写入一帧坐标到Flash离线缓存 |
|
||
| `offline_cache_sync()` | `cache/offline_cache.c` | 将离线缓存数据读出供BLE发送 |
|
||
| `ota_finalize_upgrade()` | `task/ota_task.c` | OTA完成后写标志并触发重启 |
|
||
| `enable_flash_protection()` | `main.c` | 启用APPROTECT Flash读保护(生产版本) |
|
||
|
||
### 5.3 寄存器与GATT特征定义
|
||
|
||
**自定义摄像头寄存器(camera_regs.h):**
|
||
|
||
| 宏定义 | 寄存器地址 | 说明 |
|
||
|--------|-----------|------|
|
||
| `REG_RESET` | 0x12 | 软件复位(写0x80触发) |
|
||
| `REG_CHIP_ID` | 0x0A | 芯片ID(只读,期望值0xAB) |
|
||
| `REG_EXPOSURE_H` | 0x10 | 曝光时间高字节 |
|
||
| `REG_EXPOSURE_L` | 0x11 | 曝光时间低字节 |
|
||
| `REG_GAIN_CTRL` | 0x13 | 增益控制(AGC设置) |
|
||
| `REG_CLKDIV` | 0x11 | 时钟分频(帧率控制) |
|
||
| `REG_FORMAT` | 0x12 | 输出格式(0=灰度,1=Bayer) |
|
||
|
||
**BLE Characteristic UUID映射(gatt_services.h):**
|
||
|
||
| 宏定义 | UUID值 | 属性 | 说明 |
|
||
|--------|--------|------|------|
|
||
| `UUID_STROKE_DATA_CHAR` | 0xFFF1 | Notify | 笔迹坐标数据 |
|
||
| `UUID_PEN_CONTROL_CHAR` | 0xFFF2 | Write | 控制指令接收 |
|
||
| `UUID_BATTERY_CHAR` | 0xFFF3 | Read/Notify | 电池电量 |
|
||
| `UUID_DEVICE_SERIAL_CHAR` | 0xFEE1 | Read | 设备序列号 |
|
||
| `UUID_FW_VERSION_CHAR` | 0xFEE2 | Read | 固件版本 |
|
||
| `UUID_HW_VERSION_CHAR` | 0xFEE3 | Read | 硬件版本 |
|
||
| `UUID_CALIBRATION_CHAR` | 0xFEE4 | Read/Write | 摄像头校准参数 |
|
||
| `UUID_DFU_CONTROL_CHAR` | 0xFEF1 | Write/Indicate | OTA控制 |
|
||
| `UUID_DFU_PACKET_CHAR` | 0xFEF2 | Write Without Response | OTA数据包 |
|
||
| `UUID_DFU_STATUS_CHAR` | 0xFEF3 | Indicate | OTA状态上报 |
|
||
|
||
---
|
||
|
||
## 附录A BLE GATT服务定义表
|
||
|
||
### A.1 Writech Pen Data Service(Primary Service)
|
||
|
||
- **Service UUID**:`0000FFF0-0000-1000-8000-00805F9B34FB`
|
||
|
||
| Characteristic | UUID | Properties | Value Length | Description |
|
||
|---------------|------|------------|-------------|-------------|
|
||
| Stroke Data | `0000FFF1-...` | Notify | 最大247字节 | 差分编码坐标数据包 |
|
||
| Pen Control | `0000FFF2-...` | Write | 2字节 | byte[0]=命令类型,byte[1]=参数 |
|
||
| Battery Level | `0000FFF3-...` | Read, Notify | 1字节 | 电量百分比(0~100) |
|
||
|
||
### A.2 Writech Device Info Service(Primary Service)
|
||
|
||
- **Service UUID**:`0000FEE0-0000-1000-8000-00805F9B34FB`
|
||
|
||
| Characteristic | UUID | Properties | Value Length | Description |
|
||
|---------------|------|------------|-------------|-------------|
|
||
| Device Serial | `0000FEE1-...` | Read | 16字节 | ASCII序列号 |
|
||
| Firmware Version | `0000FEE2-...` | Read | 8字节 | 版本字符串(如"V1.0.0\0\0") |
|
||
| Hardware Version | `0000FEE3-...` | Read | 8字节 | 硬件版本字符串 |
|
||
| Calibration | `0000FEE4-...` | Read, Write | 32字节 | 摄像头标定参数(结构体序列化) |
|
||
|
||
---
|
||
|
||
## 附录B 硬件外设寄存器说明
|
||
|
||
### B.1 SPI摄像头接线
|
||
|
||
| nRF52840 引脚 | 摄像头引脚 | 说明 |
|
||
|--------------|-----------|------|
|
||
| P0.03 | CS | 片选(低有效) |
|
||
| P0.04 | MOSI | 主发从收(配置数据) |
|
||
| P0.05 | MISO | 主收从发(图像数据) |
|
||
| P0.06 | SCK | SPI时钟(最高8MHz) |
|
||
| P0.07 | PWDN | 电源使能(高有效) |
|
||
| P0.08 | VSYNC | 帧同步信号(输入) |
|
||
|
||
### B.2 外部Flash(W25Q32)接线
|
||
|
||
| nRF52840 引脚 | Flash引脚 | 说明 |
|
||
|--------------|-----------|------|
|
||
| P1.00 | CS | 片选(低有效) |
|
||
| P1.01 | MOSI | 数据输入 |
|
||
| P1.02 | MISO | 数据输出 |
|
||
| P1.03 | CLK | 时钟(最高50MHz) |
|
||
| P1.04 | WP | 写保护(固定高电平) |
|
||
| P1.05 | HOLD | 保持(固定高电平) |
|
||
|
||
---
|
||
|
||
## 附录C 术语表
|
||
|
||
| 术语 | 说明 |
|
||
|------|------|
|
||
| MCU | Microcontroller Unit,微控制单元(点阵笔主控芯片) |
|
||
| RTOS | Real-Time Operating System,实时操作系统 |
|
||
| FreeRTOS | 开源实时操作系统,广泛用于嵌入式系统 |
|
||
| SoftDevice | Nordic Semiconductor的BLE协议栈软件(运行于MCU低地址空间) |
|
||
| BLE GATT | Generic Attribute Profile,蓝牙通用属性规范(BLE应用层协议) |
|
||
| Notify | BLE GATT的一种数据传输方式(无ACK,高速) |
|
||
| Indicate | BLE GATT的另一种数据传输方式(有ACK,可靠) |
|
||
| DFU | Device Firmware Update,设备固件更新(OTA的BLE标准实现) |
|
||
| ADC | Analog-to-Digital Converter,模数转换器 |
|
||
| SPI | Serial Peripheral Interface,串行外设接口 |
|
||
| NVS | Non-Volatile Storage,非易失性存储(Flash存储区域) |
|
||
| LTK | Long-Term Key,BLE长期密钥(配对后用于重连加密) |
|
||
| APPROTECT | Access Port Protection,Flash访问保护(防调试器读取) |
|
||
| ECDH | 椭圆曲线Diffie-Hellman密钥交换(BLE Secure Connections中使用) |
|
||
| Anoto | 一种点阵码编码体系,用于纸笔数字化(Anoto AB公司专利) |
|
||
| DFOV | Diagonal Field of View,对角视场角(摄像头参数) |
|
||
| Q15 | 定点数格式:1位符号位 + 15位小数位(嵌入式优化浮点替代方案) |
|
||
| MTU | Maximum Transmission Unit,BLE最大传输单元(默认23字节,可协商至247字节) |
|
||
|
||
---
|
||
|
||
## 附录D 版本历史
|
||
|
||
| 版本 | 日期 | 变更说明 | 编制人 |
|
||
|------|------|---------|--------|
|
||
| V0.3 Alpha | 2025-06-01 | 基础BLE连接,坐标采集与发送 | 固件研发团队 |
|
||
| V0.7 Beta | 2025-09-10 | 离线缓存(Flash FIFO),点阵码解码精度优化(引入亚像素插值) | 固件研发团队 |
|
||
| V0.9 RC | 2025-11-20 | OTA升级(BLE DFU),低功耗优化(四级功耗状态机,续航延长30%) | 固件研发团队 |
|
||
| V1.0 | 2026-02-14 | 正式版:RSA签名OTA、压力传感器滞回优化、LED动效、Flash读保护 | 固件研发团队 |
|
||
|
||
---
|
||
|
||
*文档编制:深圳自然写科技有限公司 硬件/固件研发部*
|
||
*文档版本:V1.0*
|
||
*最后更新:2026年2月14日*
|
||
*版权所有 © 2026 深圳自然写科技有限公司*
|
||
|
||
---
|
||
|
||
## 附录E 核心算法与驱动详述
|
||
|
||
### E.1 点阵码解码算法
|
||
|
||
自然写智能点阵笔采用专利点阵纸技术,通过CMOS图像传感器拍摄纸面点阵图案,由固件实时解码为绝对坐标。
|
||
|
||
#### E.1.1 点阵图像采集与预处理
|
||
|
||
```c
|
||
/* codec/dot_codec.c - 点阵码解码主流程 */
|
||
|
||
#include "dot_codec.h"
|
||
#include "camera_driver.h"
|
||
#include "math_utils.h"
|
||
|
||
/* 点阵图像参数 */
|
||
#define IMG_WIDTH 128
|
||
#define IMG_HEIGHT 128
|
||
#define DOT_THRESHOLD 80 /* 二值化阈值(0-255) */
|
||
#define MIN_DOT_AREA 4 /* 最小点面积(像素,过滤噪声) */
|
||
#define MAX_DOT_AREA 25 /* 最大点面积(像素,过滤粘连) */
|
||
#define EXPECTED_DOTS 64 /* 每帧期望识别到的点数 */
|
||
|
||
/* 解码结果结构 */
|
||
typedef struct {
|
||
float abs_x; /* 绝对X坐标(mm),精度0.01mm */
|
||
float abs_y; /* 绝对Y坐标(mm),精度0.01mm */
|
||
float angle; /* 笔的旋转角度(度) */
|
||
uint8_t page_id; /* 当前纸张页码 */
|
||
uint8_t quality; /* 解码质量评分(0-100) */
|
||
} DotDecodeResult;
|
||
|
||
/**
|
||
* @brief 点阵图像二值化(Otsu自适应阈值)
|
||
* @param[in] src 原始灰度图(128×128)
|
||
* @param[out] dst 二值图(0或255)
|
||
*/
|
||
static void binarize_otsu(const uint8_t *src, uint8_t *dst) {
|
||
uint32_t hist[256] = {0};
|
||
uint32_t total = IMG_WIDTH * IMG_HEIGHT;
|
||
|
||
/* 统计直方图 */
|
||
for (uint32_t i = 0; i < total; i++) {
|
||
hist[src[i]]++;
|
||
}
|
||
|
||
/* Otsu方法求最优阈值 */
|
||
uint32_t sum = 0;
|
||
for (int i = 0; i < 256; i++) sum += i * hist[i];
|
||
|
||
uint32_t sumB = 0, wB = 0, wF = 0;
|
||
float maxVariance = 0.0f;
|
||
uint8_t threshold = DOT_THRESHOLD;
|
||
|
||
for (int t = 0; t < 256; t++) {
|
||
wB += hist[t];
|
||
if (wB == 0) continue;
|
||
wF = total - wB;
|
||
if (wF == 0) break;
|
||
|
||
sumB += t * hist[t];
|
||
float mB = (float)sumB / wB;
|
||
float mF = (float)(sum - sumB) / wF;
|
||
float variance = (float)wB * wF * (mB - mF) * (mB - mF);
|
||
|
||
if (variance > maxVariance) {
|
||
maxVariance = variance;
|
||
threshold = (uint8_t)t;
|
||
}
|
||
}
|
||
|
||
/* 应用阈值 */
|
||
for (uint32_t i = 0; i < total; i++) {
|
||
dst[i] = (src[i] > threshold) ? 0 : 255; /* 点为暗色,背景为亮色 */
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief 连通区域标记(4-连通BFS),提取各点的质心坐标
|
||
* @param[in] binary 二值图
|
||
* @param[out] dots 检测到的点质心数组
|
||
* @param[out] dot_count 检测到的点数量
|
||
* @return 0成功,-1失败
|
||
*/
|
||
static int extract_dot_centroids(const uint8_t *binary,
|
||
float *dots_x, float *dots_y, int *dot_count) {
|
||
static uint8_t label_map[IMG_WIDTH * IMG_HEIGHT];
|
||
memset(label_map, 0, sizeof(label_map));
|
||
*dot_count = 0;
|
||
|
||
/* 简化BFS标记(嵌入式环境优化版本,避免递归) */
|
||
static uint16_t queue[MAX_DOT_AREA * 4];
|
||
int label = 1;
|
||
|
||
for (int y = 1; y < IMG_HEIGHT - 1; y++) {
|
||
for (int x = 1; x < IMG_WIDTH - 1; x++) {
|
||
int idx = y * IMG_WIDTH + x;
|
||
if (binary[idx] == 0 || label_map[idx] != 0) continue;
|
||
|
||
/* BFS flood fill */
|
||
int head = 0, tail = 0;
|
||
int area = 0;
|
||
float sum_x = 0, sum_y = 0;
|
||
queue[tail++] = (uint16_t)(y * IMG_WIDTH + x);
|
||
label_map[idx] = label;
|
||
|
||
while (head < tail && area < MAX_DOT_AREA * 2) {
|
||
uint16_t cur = queue[head++];
|
||
int cy = cur / IMG_WIDTH;
|
||
int cx = cur % IMG_WIDTH;
|
||
sum_x += cx;
|
||
sum_y += cy;
|
||
area++;
|
||
|
||
/* 检查4邻居 */
|
||
int neighbors[4] = {
|
||
(cy-1)*IMG_WIDTH+cx, (cy+1)*IMG_WIDTH+cx,
|
||
cy*IMG_WIDTH+(cx-1), cy*IMG_WIDTH+(cx+1)
|
||
};
|
||
for (int n = 0; n < 4; n++) {
|
||
int ni = neighbors[n];
|
||
if (ni >= 0 && ni < IMG_WIDTH*IMG_HEIGHT
|
||
&& binary[ni] == 0 && label_map[ni] == 0) {
|
||
label_map[ni] = label;
|
||
queue[tail++] = (uint16_t)ni;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 过滤面积不合理的区域 */
|
||
if (area >= MIN_DOT_AREA && area <= MAX_DOT_AREA
|
||
&& *dot_count < EXPECTED_DOTS) {
|
||
dots_x[*dot_count] = sum_x / area;
|
||
dots_y[*dot_count] = sum_y / area;
|
||
(*dot_count)++;
|
||
}
|
||
label++;
|
||
}
|
||
}
|
||
return (*dot_count >= 16) ? 0 : -1; /* 少于16个点则解码失败 */
|
||
}
|
||
|
||
/**
|
||
* @brief 点阵主解码函数
|
||
* @param[in] raw_image CMOS原始灰度图像数据
|
||
* @param[out] result 解码结果
|
||
* @return 0成功,-1失败
|
||
*/
|
||
int dot_codec_decode(const uint8_t *raw_image, DotDecodeResult *result) {
|
||
static uint8_t binary_buf[IMG_WIDTH * IMG_HEIGHT];
|
||
static float dots_x[EXPECTED_DOTS];
|
||
static float dots_y[EXPECTED_DOTS];
|
||
int dot_count = 0;
|
||
|
||
/* Step 1: 二值化 */
|
||
binarize_otsu(raw_image, binary_buf);
|
||
|
||
/* Step 2: 提取点质心 */
|
||
if (extract_dot_centroids(binary_buf, dots_x, dots_y, &dot_count) != 0) {
|
||
result->quality = 0;
|
||
return -1;
|
||
}
|
||
|
||
/* Step 3: 点阵网格拟合(最小二乘法求仿射变换参数) */
|
||
float angle, scale, offset_x, offset_y;
|
||
if (fit_dot_grid(dots_x, dots_y, dot_count,
|
||
&angle, &scale, &offset_x, &offset_y) != 0) {
|
||
result->quality = 10;
|
||
return -1;
|
||
}
|
||
|
||
/* Step 4: 从网格偏移量中解码Anoto绝对坐标 */
|
||
uint32_t abs_x_raw, abs_y_raw;
|
||
uint8_t page_id;
|
||
if (anoto_decode_position(dots_x, dots_y, dot_count,
|
||
angle, &abs_x_raw, &abs_y_raw, &page_id) != 0) {
|
||
result->quality = 30;
|
||
return -1;
|
||
}
|
||
|
||
result->abs_x = abs_x_raw * 0.01f; /* 转换为mm */
|
||
result->abs_y = abs_y_raw * 0.01f;
|
||
result->angle = angle;
|
||
result->page_id = page_id;
|
||
result->quality = (uint8_t)(50 + dot_count); /* 粗略质量评分 */
|
||
if (result->quality > 100) result->quality = 100;
|
||
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
### E.2 压力传感器驱动与滞回补偿
|
||
|
||
```c
|
||
/* driver/pressure_driver.c - 压力传感器驱动(带滞回补偿) */
|
||
|
||
#include "pressure_driver.h"
|
||
#include "adc_driver.h"
|
||
|
||
/* 压力ADC参数 */
|
||
#define PRESSURE_ADC_CHANNEL 1
|
||
#define PRESSURE_ADC_BITS 12 /* 12位ADC:0~4095 */
|
||
#define PRESSURE_MIN_RAW 150 /* 最小有效ADC值(笔尖接触压力阈值) */
|
||
#define PRESSURE_MAX_RAW 3800 /* 最大ADC值(最大压力) */
|
||
|
||
/* 滞回补偿参数(防止轻微抖动导致反复触发抬笔/落笔) */
|
||
#define HYSTERESIS_LOW 180 /* 落笔阈值(下降) */
|
||
#define HYSTERESIS_HIGH 220 /* 抬笔阈值(上升) */
|
||
|
||
/* IIR低通滤波器系数(α=0.3,截止频率约48Hz@200Hz采样率) */
|
||
#define FILTER_ALPHA_FP 0.3f
|
||
|
||
static uint16_t g_pressure_filtered = 0;
|
||
static bool g_pen_down = false;
|
||
|
||
/**
|
||
* @brief 读取并滤波压力值
|
||
* @return 归一化压力值 [0, 255]
|
||
*/
|
||
uint8_t pressure_read_normalized(void) {
|
||
uint16_t raw = adc_read(PRESSURE_ADC_CHANNEL);
|
||
|
||
/* IIR一阶低通滤波 */
|
||
g_pressure_filtered = (uint16_t)(
|
||
FILTER_ALPHA_FP * raw + (1.0f - FILTER_ALPHA_FP) * g_pressure_filtered
|
||
);
|
||
|
||
/* 线性归一化到 [0, 255] */
|
||
if (g_pressure_filtered <= PRESSURE_MIN_RAW) return 0;
|
||
if (g_pressure_filtered >= PRESSURE_MAX_RAW) return 255;
|
||
|
||
return (uint8_t)(
|
||
(uint32_t)(g_pressure_filtered - PRESSURE_MIN_RAW) * 255
|
||
/ (PRESSURE_MAX_RAW - PRESSURE_MIN_RAW)
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @brief 获取笔尖状态(带滞回,防抖)
|
||
* @return true=笔尖接触纸面(落笔),false=笔尖离开(抬笔)
|
||
*/
|
||
bool pressure_is_pen_down(void) {
|
||
uint16_t raw = g_pressure_filtered;
|
||
|
||
if (!g_pen_down && raw > HYSTERESIS_HIGH) {
|
||
g_pen_down = true; /* 落笔 */
|
||
} else if (g_pen_down && raw < HYSTERESIS_LOW) {
|
||
g_pen_down = false; /* 抬笔 */
|
||
}
|
||
/* 在 HYSTERESIS_LOW ~ HYSTERESIS_HIGH 之间保持上一状态(滞回区) */
|
||
return g_pen_down;
|
||
}
|
||
```
|
||
|
||
### E.3 BLE GATT Service/Characteristic完整定义
|
||
|
||
智能点阵笔的BLE GATT服务定义如下,遵循Nordic UART Service规范扩展:
|
||
|
||
| 服务/特征 | UUID | 属性 | 说明 |
|
||
|---------|------|------|------|
|
||
| Writech Pen Service | 6E400001-... | - | 主服务 |
|
||
| ↳ Ink Data Char | 6E400002-... | Notify | 笔迹数据推送(10字节/点) |
|
||
| ↳ Control Char | 6E400003-... | Write | 控制命令(开始/停止/配置) |
|
||
| ↳ Status Char | 6E400004-... | Read/Notify | 设备状态(电量/连接/错误) |
|
||
| ↳ OTA Data Char | 6E400005-... | Write | OTA固件数据传输 |
|
||
| ↳ OTA Control Char | 6E400006-... | Write/Notify | OTA控制与进度反馈 |
|
||
| Device Info Service | 0x180A | - | 标准设备信息服务 |
|
||
| ↳ Manufacturer | 0x2A29 | Read | "Writech Technology" |
|
||
| ↳ Model Number | 0x2A24 | Read | "WritechPen-M1" |
|
||
| ↳ Firmware Version | 0x2A26 | Read | 当前固件版本字符串 |
|
||
| Battery Service | 0x180F | - | 标准电池服务 |
|
||
| ↳ Battery Level | 0x2A19 | Read/Notify | 电池电量(0-100%) |
|
||
|
||
#### E.3.1 笔迹数据包二进制格式
|
||
|
||
每个笔迹数据通知包(Notify包)包含1至N个笔迹点(受BLE MTU限制,默认MTU=247字节,每包最多23个点):
|
||
|
||
```
|
||
每个点:10字节
|
||
┌──────┬──────┬──────────┬──────────────┬──────┐
|
||
│ X[2B]│ Y[2B]│ P[1B] │ Timestamp[4B]│ F[1B]│
|
||
└──────┴──────┴──────────┴──────────────┴──────┘
|
||
X: uint16_be,归一化坐标×65535,对应纸面X轴
|
||
Y: uint16_be,归一化坐标×65535,对应纸面Y轴
|
||
P: uint8,压力×255
|
||
Timestamp: uint32_be,毫秒时间戳(设备本地时钟)
|
||
F: 标志位
|
||
Bit0: 1=抬笔(笔画结束),0=落笔
|
||
Bit1: 1=笔迹质量低(解码置信度<50%)
|
||
Bit2: 1=缓存数据(离线恢复发送)
|
||
Bit3-7: 保留
|
||
```
|
||
|
||
### E.4 四级功耗状态机
|
||
|
||
```c
|
||
/* power/power_manager.c - 四级功耗管理状态机 */
|
||
|
||
typedef enum {
|
||
POWER_STATE_ACTIVE = 0, /* 活跃:书写中,全速运行 */
|
||
POWER_STATE_IDLE = 1, /* 空闲:静止10s,降低采样率 */
|
||
POWER_STATE_SLEEP = 2, /* 浅睡眠:静止60s,关闭图像传感器 */
|
||
POWER_STATE_DEEP_SLEEP = 3 /* 深度睡眠:静止300s,仅BLE广播保持 */
|
||
} PowerState;
|
||
|
||
static PowerState g_power_state = POWER_STATE_ACTIVE;
|
||
static uint32_t g_idle_counter_ms = 0;
|
||
static uint32_t g_last_activity_ms = 0;
|
||
|
||
/* 各状态下的采样率(Hz) */
|
||
static const uint16_t g_sample_rates[] = { 200, 50, 0, 0 };
|
||
|
||
/* 各状态下的BLE连接间隔(单位:1.25ms) */
|
||
static const uint16_t g_ble_intervals[] = { 8, 16, 80, 400 };
|
||
|
||
void power_manager_tick(uint32_t current_ms) {
|
||
bool activity = pressure_is_pen_down() || imu_detect_motion();
|
||
|
||
if (activity) {
|
||
g_last_activity_ms = current_ms;
|
||
if (g_power_state != POWER_STATE_ACTIVE) {
|
||
power_transition_to(POWER_STATE_ACTIVE);
|
||
}
|
||
return;
|
||
}
|
||
|
||
uint32_t idle_ms = current_ms - g_last_activity_ms;
|
||
|
||
if (idle_ms > 300000 && g_power_state != POWER_STATE_DEEP_SLEEP) {
|
||
power_transition_to(POWER_STATE_DEEP_SLEEP);
|
||
} else if (idle_ms > 60000 && g_power_state == POWER_STATE_IDLE) {
|
||
power_transition_to(POWER_STATE_SLEEP);
|
||
} else if (idle_ms > 10000 && g_power_state == POWER_STATE_ACTIVE) {
|
||
power_transition_to(POWER_STATE_IDLE);
|
||
}
|
||
}
|
||
|
||
static void power_transition_to(PowerState new_state) {
|
||
PowerState old_state = g_power_state;
|
||
g_power_state = new_state;
|
||
|
||
/* 调整采样率 */
|
||
camera_set_sample_rate(g_sample_rates[new_state]);
|
||
|
||
/* 调整BLE连接间隔 */
|
||
ble_set_connection_interval(g_ble_intervals[new_state]);
|
||
|
||
/* 状态特定操作 */
|
||
switch (new_state) {
|
||
case POWER_STATE_SLEEP:
|
||
camera_power_down(); /* 关闭图像传感器 */
|
||
imu_set_low_power_mode(true); /* IMU进入低功耗模式 */
|
||
break;
|
||
case POWER_STATE_DEEP_SLEEP:
|
||
/* 额外:关闭LED、降低CPU频率到最低档 */
|
||
led_set_state(LED_STATE_OFF);
|
||
cpu_set_frequency(CPU_FREQ_LOW);
|
||
break;
|
||
case POWER_STATE_ACTIVE:
|
||
if (old_state >= POWER_STATE_SLEEP) {
|
||
camera_power_up();
|
||
imu_set_low_power_mode(false);
|
||
}
|
||
if (old_state == POWER_STATE_DEEP_SLEEP) {
|
||
cpu_set_frequency(CPU_FREQ_HIGH);
|
||
}
|
||
led_set_state(LED_STATE_WRITING);
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
|
||
LOG_INFO("Power state: %d -> %d", old_state, new_state);
|
||
}
|
||
```
|
||
|
||
### E.5 Flash离线缓存(FIFO环形缓冲区)
|
||
|
||
```c
|
||
/* cache/flash_cache.c - SPI Flash环形缓冲区(WAL模式)*/
|
||
|
||
#define FLASH_SECTOR_SIZE 4096 /* 4KB扇区 */
|
||
#define CACHE_SECTORS 128 /* 共128个扇区 = 512KB缓存 */
|
||
#define CACHE_HEADER_MAGIC 0xA55A0001 /* 缓存头魔数,用于完整性检验 */
|
||
|
||
typedef struct {
|
||
uint32_t magic; /* 魔数:0xA55A0001 */
|
||
uint16_t data_len; /* 数据长度(字节) */
|
||
uint16_t checksum; /* 数据CRC16校验 */
|
||
uint32_t timestamp; /* 数据时间戳 */
|
||
uint8_t flags; /* 标志位:Bit0=已确认接收 */
|
||
uint8_t reserved[3];
|
||
} FlashCacheHeader; /* 12字节头部 */
|
||
|
||
typedef struct {
|
||
uint32_t write_sector; /* 当前写扇区索引 */
|
||
uint32_t read_sector; /* 当前读扇区索引 */
|
||
uint32_t write_offset; /* 当前写扇区内偏移 */
|
||
uint32_t total_cached; /* 总缓存字节数 */
|
||
} FlashCacheState;
|
||
|
||
static FlashCacheState g_cache_state;
|
||
|
||
/**
|
||
* @brief 写入笔迹数据到Flash缓存
|
||
* @param data 笔迹数据指针
|
||
* @param len 数据长度
|
||
* @return 0成功,-ENOMEM缓存满
|
||
*/
|
||
int flash_cache_write(const uint8_t *data, uint16_t len) {
|
||
if (flash_cache_is_full()) {
|
||
LOG_WARN("Flash cache full, dropping oldest sector");
|
||
/* 覆盖最旧的扇区(环形FIFO) */
|
||
g_cache_state.read_sector =
|
||
(g_cache_state.read_sector + 1) % CACHE_SECTORS;
|
||
}
|
||
|
||
/* 检查当前扇区剩余空间 */
|
||
uint16_t needed = sizeof(FlashCacheHeader) + len;
|
||
uint16_t remaining = FLASH_SECTOR_SIZE - g_cache_state.write_offset;
|
||
|
||
if (needed > remaining) {
|
||
/* 移到下一扇区,擦除后写 */
|
||
g_cache_state.write_sector =
|
||
(g_cache_state.write_sector + 1) % CACHE_SECTORS;
|
||
g_cache_state.write_offset = 0;
|
||
flash_erase_sector(g_cache_state.write_sector * FLASH_SECTOR_SIZE);
|
||
}
|
||
|
||
/* 构建缓存头 */
|
||
FlashCacheHeader hdr = {
|
||
.magic = CACHE_HEADER_MAGIC,
|
||
.data_len = len,
|
||
.checksum = crc16(data, len),
|
||
.timestamp = rtc_get_timestamp(),
|
||
.flags = 0,
|
||
};
|
||
|
||
uint32_t addr = g_cache_state.write_sector * FLASH_SECTOR_SIZE
|
||
+ g_cache_state.write_offset;
|
||
|
||
/* 写入头部和数据 */
|
||
flash_write(addr, (uint8_t*)&hdr, sizeof(hdr));
|
||
flash_write(addr + sizeof(hdr), data, len);
|
||
|
||
g_cache_state.write_offset += needed;
|
||
g_cache_state.total_cached += len;
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 附录F 生产测试与质量保证
|
||
|
||
### F.1 固件烧录与出厂自检
|
||
|
||
智能点阵笔在生产阶段通过SWD(Serial Wire Debug)接口烧录固件,烧录完成后自动执行出厂自检程序:
|
||
|
||
| 测试项目 | 判断标准 | 失败处理 |
|
||
|---------|---------|---------|
|
||
| Flash读写测试 | 全擦全写无错误 | 报废 |
|
||
| SRAM完整性测试 | 全0/全1/棋盘格无错误 | 报废 |
|
||
| 图像传感器测试 | 采集图像中点数≥50 | 更换传感器 |
|
||
| 压力传感器测试 | ADC值在正常范围内 | 更换传感器 |
|
||
| BLE射频测试 | RSSI≥-70dBm@1m | 天线返修 |
|
||
| 电池充放电测试 | 充满容量≥标称95% | 更换电池 |
|
||
| IMU测试 | 三轴加速度/陀螺仪数值正常 | 更换IMU |
|
||
| LED指示灯测试 | RGB三色正常点亮 | 更换LED |
|
||
|
||
### F.2 固件版本管理
|
||
|
||
```c
|
||
/* version.h - 固件版本定义 */
|
||
#define FW_VERSION_MAJOR 1
|
||
#define FW_VERSION_MINOR 0
|
||
#define FW_VERSION_PATCH 0
|
||
#define FW_VERSION_BUILD 20260214
|
||
|
||
#define FW_VERSION_STRING "1.0.0-20260214"
|
||
|
||
/* 版本比较宏(用于OTA升级判断) */
|
||
#define FW_VERSION_CODE ((FW_VERSION_MAJOR << 24) | \
|
||
(FW_VERSION_MINOR << 16) | \
|
||
(FW_VERSION_PATCH << 8))
|
||
```
|
||
|
||
---
|
||
|
||
*文档编制:深圳自然写科技有限公司 硬件/固件研发部*
|
||
*文档版本:V1.0(附录更新)*
|
||
*最后更新:2026年2月14日*
|
||
*版权所有 © 2026 深圳自然写科技有限公司*
|
||
|
||
---
|
||
|
||
## 附录G RTOS任务设计与调度
|
||
|
||
### G.1 FreeRTOS任务清单
|
||
|
||
智能点阵笔固件基于FreeRTOS实现多任务并发调度,各任务优先级和栈大小如下:
|
||
|
||
| 任务名 | 优先级 | 栈大小 | 说明 |
|
||
|--------|--------|--------|------|
|
||
| ink_capture_task | 5(最高) | 2048字节 | 相机采集+点阵解码(200Hz) |
|
||
| pressure_task | 4 | 512字节 | 压力传感器采样(200Hz) |
|
||
| ble_tx_task | 3 | 1024字节 | BLE数据发送队列 |
|
||
| ble_rx_task | 3 | 512字节 | BLE控制指令接收 |
|
||
| flash_cache_task | 2 | 1024字节 | Flash离线缓存写入 |
|
||
| battery_task | 1 | 256字节 | 电量监测(1Hz) |
|
||
| power_manage_task | 1 | 256字节 | 功耗状态机(1Hz) |
|
||
| led_task | 0(最低) | 256字节 | LED状态指示 |
|
||
|
||
### G.2 任务通信设计
|
||
|
||
任务间通信使用FreeRTOS消息队列(Queue)和信号量(Semaphore),避免共享内存竞争:
|
||
|
||
```c
|
||
/* task_comms.h - 任务间通信接口 */
|
||
|
||
/* 笔迹数据队列(ink_capture_task → ble_tx_task / flash_cache_task)*/
|
||
extern QueueHandle_t g_ink_point_queue; /* 容量:100个InkPoint */
|
||
|
||
/* BLE发送完成信号(避免发送缓冲区溢出)*/
|
||
extern SemaphoreHandle_t g_ble_tx_ready_sem;
|
||
|
||
/* 控制指令队列(ble_rx_task → 各功能任务)*/
|
||
extern QueueHandle_t g_ctrl_cmd_queue; /* 容量:10条控制指令 */
|
||
|
||
/* 功耗状态变更通知 */
|
||
extern EventGroupHandle_t g_power_event_group;
|
||
#define EVT_ENTER_ACTIVE (1 << 0)
|
||
#define EVT_ENTER_IDLE (1 << 1)
|
||
#define EVT_ENTER_SLEEP (1 << 2)
|
||
```
|
||
|
||
### G.3 中断处理与任务唤醒
|
||
|
||
```c
|
||
/* interrupt/camera_irq.c - 相机帧中断处理 */
|
||
|
||
/* 相机帧就绪中断(VSYNC信号上升沿触发)*/
|
||
void CAMERA_VSYNC_IRQHandler(void) {
|
||
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
|
||
|
||
/* 通过信号量通知ink_capture_task(ISR安全版本)*/
|
||
xSemaphoreGiveFromISR(g_camera_frame_ready_sem, &xHigherPriorityTaskWoken);
|
||
|
||
/* 如果唤醒了更高优先级任务,请求任务切换 */
|
||
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
|
||
}
|
||
|
||
/* 笔迹采集任务主循环 */
|
||
void ink_capture_task(void *param) {
|
||
InkPoint point;
|
||
DotDecodeResult decode_result;
|
||
|
||
for (;;) {
|
||
/* 等待相机帧就绪信号(阻塞,不消耗CPU)*/
|
||
xSemaphoreTake(g_camera_frame_ready_sem, portMAX_DELAY);
|
||
|
||
/* 读取相机帧 */
|
||
uint8_t *frame = camera_get_latest_frame();
|
||
|
||
/* 点阵解码 */
|
||
if (dot_codec_decode(frame, &decode_result) == 0) {
|
||
point.x = (uint16_t)(decode_result.abs_x / PAPER_WIDTH * 65535);
|
||
point.y = (uint16_t)(decode_result.abs_y / PAPER_HEIGHT * 65535);
|
||
point.pressure = pressure_read_normalized();
|
||
point.timestamp = rtc_get_ms();
|
||
point.flags = pressure_is_pen_down() ? 0 : 0x01; /* Bit0=抬笔 */
|
||
|
||
/* 发送到笔迹队列(非阻塞,队列满则丢帧)*/
|
||
xQueueSendToBack(g_ink_point_queue, &point, 0);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### G.4 LED状态指示定义
|
||
|
||
| LED状态 | 颜色 | 闪烁方式 | 含义 |
|
||
|---------|------|---------|------|
|
||
| LED_STATE_OFF | 熄灭 | - | 关机或深度睡眠 |
|
||
| LED_STATE_BOOT | 白色 | 常亮 | 启动中 |
|
||
| LED_STATE_SCANNING | 蓝色 | 快闪(200ms) | 蓝牙扫描广播中 |
|
||
| LED_STATE_CONNECTING | 蓝色 | 慢闪(1000ms) | 正在连接网关 |
|
||
| LED_STATE_CONNECTED | 蓝色 | 常亮 | 已连接网关 |
|
||
| LED_STATE_WRITING | 绿色 | 常亮 | 书写中(压力传感器触发) |
|
||
| LED_STATE_LOW_BATTERY | 红色 | 慢闪(2000ms) | 电量低(<15%) |
|
||
| LED_STATE_CHARGING | 橙色 | 呼吸灯 | 充电中 |
|
||
| LED_STATE_CHARGE_FULL | 绿色 | 常亮 | 充电完成 |
|
||
| LED_STATE_OTA | 紫色 | 快闪(500ms) | OTA升级中(请勿关机) |
|
||
| LED_STATE_ERROR | 红色 | 3连闪 | 系统错误 |
|
||
|
||
### G.5 固件安全措施
|
||
|
||
| 安全措施 | 实现方式 | 说明 |
|
||
|---------|---------|------|
|
||
| Flash读保护 | STM32 RDP Level 1 | 防止通过调试口读取Flash内容 |
|
||
| OTA签名验证 | RSA-2048 + SHA-256 | 只接受官方签名的固件包 |
|
||
| 通信加密 | BLE连接层加密(AES-128 CCM) | 防止空中截获笔迹数据 |
|
||
| 设备绑定 | AppKey-DeviceID绑定验证 | 防止伪造设备接入系统 |
|
||
| Bootloader保护 | 独立分区+写保护 | 防止OTA意外破坏Bootloader |
|
||
|
||
---
|
||
|
||
*本文档版权归深圳自然写科技有限公司所有,所有技术细节仅用于软件著作权登记鉴别,请勿用于其他商业用途。*
|
||
|
||
---
|
||
|
||
## 附录F 补充技术规格
|
||
|
||
### F.1 点阵码高速解码优化
|
||
|
||
#### F.1.1 SIMD加速解码
|
||
|
||
```c
|
||
// dotmatrix_simd.c - ARM NEON加速点阵码解码
|
||
#include <arm_neon.h>
|
||
|
||
// 一次处理16字节像素数据
|
||
void decode_row_neon(const uint8_t* pixels, int width,
|
||
uint8_t* bits_out) {
|
||
const uint8x16_t threshold = vdupq_n_u8(128);
|
||
int x = 0;
|
||
|
||
for (; x + 16 <= width; x += 16) {
|
||
uint8x16_t px = vld1q_u8(pixels + x);
|
||
// 与阈值比较:大于128为白(0),小于等于128为黑(1)
|
||
uint8x16_t result = vcleq_u8(px, threshold);
|
||
// 提取高位构成位掩码
|
||
uint8_t mask = 0;
|
||
for (int i = 0; i < 16; i++) {
|
||
if (result[i]) mask |= (1 << (i % 8));
|
||
if (i == 7) bits_out[x/8] = mask, mask = 0;
|
||
}
|
||
bits_out[x/8 + 1] = mask;
|
||
}
|
||
|
||
// 处理剩余像素
|
||
for (; x < width; x++) {
|
||
if (pixels[x] <= 128) bits_out[x/8] |= (1 << (x % 8));
|
||
}
|
||
}
|
||
```
|
||
|
||
### F.2 低功耗蓝牙广播优化
|
||
|
||
#### F.2.1 自适应广播间隔
|
||
|
||
```c
|
||
// ble_adv_manager.c
|
||
#define ADV_INTERVAL_FAST_MS 100 // 连接前快速广播
|
||
#define ADV_INTERVAL_SLOW_MS 1000 // 长时间未连接慢速广播
|
||
#define ADV_FAST_TIMEOUT_S 30 // 30秒后切换到慢速
|
||
|
||
typedef enum {
|
||
ADV_STATE_OFF = 0,
|
||
ADV_STATE_FAST,
|
||
ADV_STATE_SLOW
|
||
} adv_state_t;
|
||
|
||
static adv_state_t g_adv_state = ADV_STATE_OFF;
|
||
static uint32_t g_fast_adv_start_tick = 0;
|
||
|
||
void ble_adv_update(void) {
|
||
if (g_adv_state == ADV_STATE_OFF) return;
|
||
|
||
uint32_t elapsed_s = (HAL_GetTick() - g_fast_adv_start_tick) / 1000;
|
||
|
||
if (g_adv_state == ADV_STATE_FAST && elapsed_s >= ADV_FAST_TIMEOUT_S) {
|
||
// 切换到慢速广播节省电量
|
||
ble_gap_adv_stop();
|
||
|
||
ble_gap_adv_params_t params = {
|
||
.type = BLE_GAP_ADV_TYPE_CONNECTABLE_UNDIRECTED,
|
||
.interval_min = ADV_INTERVAL_SLOW_MS * 8 / 5, // 单位0.625ms
|
||
.interval_max = ADV_INTERVAL_SLOW_MS * 8 / 5 + 16,
|
||
.channel_mask = 0x07
|
||
};
|
||
ble_gap_adv_start(¶ms);
|
||
g_adv_state = ADV_STATE_SLOW;
|
||
LOG_INFO("BLE adv switched to slow mode, current=%dmA",
|
||
power_measure_current_ua() / 1000);
|
||
}
|
||
}
|
||
|
||
void ble_adv_start_fast(void) {
|
||
ble_gap_adv_params_t params = {
|
||
.type = BLE_GAP_ADV_TYPE_CONNECTABLE_UNDIRECTED,
|
||
.interval_min = ADV_INTERVAL_FAST_MS * 8 / 5,
|
||
.interval_max = ADV_INTERVAL_FAST_MS * 8 / 5 + 16,
|
||
.channel_mask = 0x07
|
||
};
|
||
ble_gap_adv_start(¶ms);
|
||
g_adv_state = ADV_STATE_FAST;
|
||
g_fast_adv_start_tick = HAL_GetTick();
|
||
}
|
||
```
|
||
|
||
### F.3 电量精确估算算法
|
||
|
||
#### F.3.1 库仑计积分法
|
||
|
||
```c
|
||
// battery_gauge.c
|
||
#define BATTERY_CAPACITY_MAH 200.0f // 电池容量200mAh
|
||
#define SAMPLE_INTERVAL_MS 100 // 采样间隔100ms
|
||
|
||
typedef struct {
|
||
float soc_percent; // 剩余电量百分比
|
||
float voltage_mv; // 当前电压(mV)
|
||
float current_ma; // 当前电流(mA,放电为正)
|
||
float accumulated_mah; // 已消耗容量(mAh)
|
||
uint32_t last_sample_tick; // 上次采样时间
|
||
} battery_state_t;
|
||
|
||
static battery_state_t g_battery;
|
||
|
||
void battery_gauge_update(void) {
|
||
uint32_t now = HAL_GetTick();
|
||
uint32_t dt_ms = now - g_battery.last_sample_tick;
|
||
if (dt_ms < SAMPLE_INTERVAL_MS) return;
|
||
|
||
// 采样ADC
|
||
g_battery.voltage_mv = adc_read_voltage();
|
||
g_battery.current_ma = adc_read_current();
|
||
|
||
// 积分:累计消耗容量 = 电流(mA) × 时间(h)
|
||
float dt_h = dt_ms / 3600000.0f;
|
||
g_battery.accumulated_mah += g_battery.current_ma * dt_h;
|
||
|
||
// SOC = 1 - 已消耗/总容量
|
||
g_battery.soc_percent =
|
||
(1.0f - g_battery.accumulated_mah / BATTERY_CAPACITY_MAH) * 100.0f;
|
||
g_battery.soc_percent = CLAMP(g_battery.soc_percent, 0.0f, 100.0f);
|
||
|
||
// 电压修正(防止长期误差累积)
|
||
float ocv_soc = voltage_to_soc_ocv(g_battery.voltage_mv);
|
||
if (fabsf(ocv_soc - g_battery.soc_percent) > 10.0f) {
|
||
// 差异超过10%时用OCV校正
|
||
g_battery.soc_percent = 0.8f * g_battery.soc_percent + 0.2f * ocv_soc;
|
||
}
|
||
|
||
g_battery.last_sample_tick = now;
|
||
}
|
||
|
||
// OCV(开路电压)与SOC对应表
|
||
static const float OCV_TABLE[][2] = {
|
||
{3200, 0}, {3400, 5}, {3500, 10}, {3600, 20},
|
||
{3650, 30}, {3700, 50}, {3750, 70}, {3800, 85},
|
||
{3850, 95}, {3900, 100}
|
||
};
|
||
|
||
float voltage_to_soc_ocv(float voltage_mv) {
|
||
int n = sizeof(OCV_TABLE) / sizeof(OCV_TABLE[0]);
|
||
if (voltage_mv <= OCV_TABLE[0][0]) return 0.0f;
|
||
if (voltage_mv >= OCV_TABLE[n-1][0]) return 100.0f;
|
||
|
||
for (int i = 1; i < n; i++) {
|
||
if (voltage_mv <= OCV_TABLE[i][0]) {
|
||
float t = (voltage_mv - OCV_TABLE[i-1][0]) /
|
||
(OCV_TABLE[i][0] - OCV_TABLE[i-1][0]);
|
||
return OCV_TABLE[i-1][1] + t * (OCV_TABLE[i][1] - OCV_TABLE[i-1][1]);
|
||
}
|
||
}
|
||
return 100.0f;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 附录G 补充技术规格
|
||
|
||
### G.1 RTOS任务优先级配置
|
||
|
||
```c
|
||
// rtos_config.c - FreeRTOS任务优先级与堆栈配置
|
||
#define TASK_PRIO_SENSOR_READ 7 // 最高:传感器数据读取
|
||
#define TASK_PRIO_INK_ENCODE 6 // 高:笔迹编码
|
||
#define TASK_PRIO_BLE_TX 5 // 高:BLE数据发送
|
||
#define TASK_PRIO_POWER_MGMT 4 // 中:电源管理
|
||
#define TASK_PRIO_CACHE_FLUSH 3 // 中低:缓存刷新
|
||
#define TASK_PRIO_STATUS_LED 2 // 低:状态LED控制
|
||
#define TASK_PRIO_IDLE 1 // 最低:空闲任务
|
||
|
||
// 任务堆栈大小(单位:字节)
|
||
#define STACK_SENSOR_READ 512
|
||
#define STACK_INK_ENCODE 1024
|
||
#define STACK_BLE_TX 768
|
||
#define STACK_POWER_MGMT 512
|
||
#define STACK_CACHE_FLUSH 512
|
||
|
||
void create_all_tasks(void) {
|
||
xTaskCreate(sensor_read_task, "SensorRead",
|
||
STACK_SENSOR_READ / sizeof(StackType_t),
|
||
NULL, TASK_PRIO_SENSOR_READ, &g_sensor_task_handle);
|
||
|
||
xTaskCreate(ink_encode_task, "InkEncode",
|
||
STACK_INK_ENCODE / sizeof(StackType_t),
|
||
NULL, TASK_PRIO_INK_ENCODE, &g_encode_task_handle);
|
||
|
||
xTaskCreate(ble_tx_task, "BleTx",
|
||
STACK_BLE_TX / sizeof(StackType_t),
|
||
NULL, TASK_PRIO_BLE_TX, &g_ble_tx_task_handle);
|
||
|
||
xTaskCreate(power_mgmt_task, "PowerMgmt",
|
||
STACK_POWER_MGMT / sizeof(StackType_t),
|
||
NULL, TASK_PRIO_POWER_MGMT, &g_power_task_handle);
|
||
|
||
xTaskCreate(cache_flush_task, "CacheFlush",
|
||
STACK_CACHE_FLUSH / sizeof(StackType_t),
|
||
NULL, TASK_PRIO_CACHE_FLUSH, &g_cache_task_handle);
|
||
}
|
||
```
|
||
|
||
### G.2 笔压标定算法
|
||
|
||
```c
|
||
// pressure_calibration.c
|
||
#define CALIB_POINTS 5 // 标定点数量
|
||
#define CALIB_ADC_BITS 12 // ADC位数(0-4095)
|
||
|
||
typedef struct {
|
||
uint16_t adc_raw[CALIB_POINTS]; // ADC原始值
|
||
float force_gram[CALIB_POINTS]; // 对应压力(克)
|
||
float slope; // 线性拟合斜率
|
||
float intercept; // 线性拟合截距
|
||
bool is_calibrated;
|
||
} pressure_calib_t;
|
||
|
||
static pressure_calib_t g_calib;
|
||
|
||
// 最小二乘法线性拟合
|
||
void pressure_calibration_fit(void) {
|
||
float sum_x = 0, sum_y = 0, sum_xy = 0, sum_x2 = 0;
|
||
int n = CALIB_POINTS;
|
||
|
||
for (int i = 0; i < n; i++) {
|
||
float x = g_calib.adc_raw[i];
|
||
float y = g_calib.force_gram[i];
|
||
sum_x += x;
|
||
sum_y += y;
|
||
sum_xy += x * y;
|
||
sum_x2 += x * x;
|
||
}
|
||
|
||
float denom = n * sum_x2 - sum_x * sum_x;
|
||
if (fabsf(denom) < 1e-6f) {
|
||
LOG_ERROR("标定失败:ADC值无变化");
|
||
return;
|
||
}
|
||
|
||
g_calib.slope = (n * sum_xy - sum_x * sum_y) / denom;
|
||
g_calib.intercept = (sum_y - g_calib.slope * sum_x) / n;
|
||
g_calib.is_calibrated = true;
|
||
|
||
LOG_INFO("压力标定完成:slope=%.4f, intercept=%.2f",
|
||
g_calib.slope, g_calib.intercept);
|
||
}
|
||
|
||
float pressure_adc_to_gram(uint16_t adc_raw) {
|
||
if (!g_calib.is_calibrated) return adc_raw / 4095.0f * 500.0f; // 默认映射
|
||
float gram = g_calib.slope * adc_raw + g_calib.intercept;
|
||
return CLAMP(gram, 0.0f, 600.0f);
|
||
}
|
||
|
||
uint8_t pressure_gram_to_normalized(float gram) {
|
||
// 映射到0-255,最大压力600克
|
||
return (uint8_t)CLAMP(gram / 600.0f * 255.0f, 0.0f, 255.0f);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 附录H 补充技术规格
|
||
|
||
### H.1 倾斜角计算
|
||
|
||
```c
|
||
// tilt_calculator.c
|
||
// 利用IMU(三轴加速度计)计算笔的倾斜角
|
||
#include <math.h>
|
||
|
||
typedef struct {
|
||
float x, y, z; // 加速度计原始值(单位:g)
|
||
} accel_t;
|
||
|
||
typedef struct {
|
||
float elevation; // 仰角(0°=水平,90°=垂直)
|
||
float azimuth; // 方位角(0°=正前方)
|
||
} pen_tilt_t;
|
||
|
||
pen_tilt_t calculate_tilt(const accel_t* accel) {
|
||
pen_tilt_t result;
|
||
|
||
// 计算仰角:arctan(z / sqrt(x^2 + y^2))
|
||
float xy_magnitude = sqrtf(accel->x * accel->x + accel->y * accel->y);
|
||
result.elevation = atan2f(accel->z, xy_magnitude) * 180.0f / M_PI;
|
||
|
||
// 计算方位角:arctan2(y, x)
|
||
result.azimuth = atan2f(accel->y, accel->x) * 180.0f / M_PI;
|
||
if (result.azimuth < 0) result.azimuth += 360.0f;
|
||
|
||
return result;
|
||
}
|
||
|
||
// 滑动平均滤波消抖(窗口大小=8)
|
||
#define TILT_FILTER_SIZE 8
|
||
static pen_tilt_t tilt_history[TILT_FILTER_SIZE];
|
||
static int tilt_idx = 0;
|
||
|
||
pen_tilt_t tilt_filtered(pen_tilt_t raw) {
|
||
tilt_history[tilt_idx % TILT_FILTER_SIZE] = raw;
|
||
tilt_idx++;
|
||
|
||
pen_tilt_t sum = {0};
|
||
int count = tilt_idx < TILT_FILTER_SIZE ? tilt_idx : TILT_FILTER_SIZE;
|
||
for (int i = 0; i < count; i++) {
|
||
sum.elevation += tilt_history[i].elevation;
|
||
sum.azimuth += tilt_history[i].azimuth;
|
||
}
|
||
return (pen_tilt_t){ sum.elevation / count, sum.azimuth / count };
|
||
}
|
||
```
|
||
|
||
### H.2 NFC标签读取
|
||
|
||
```c
|
||
// nfc_reader.c - 读取点阵纸NFC标签获取页面ID
|
||
#include "nfc_hal.h"
|
||
|
||
#define NFC_PAGE_ID_BLOCK 4 // 页面ID存储在NDEF块4
|
||
|
||
bool nfc_read_page_id(uint32_t* page_id_out) {
|
||
nfc_tag_t tag;
|
||
|
||
// 检测NFC标签
|
||
if (!nfc_hal_detect(&tag, 100 /* timeout_ms */)) {
|
||
return false;
|
||
}
|
||
|
||
// 验证标签类型(MIFARE Ultralight)
|
||
if (tag.type != NFC_TAG_MIFARE_UL) {
|
||
LOG_WARN("不支持的NFC标签类型: %d", tag.type);
|
||
return false;
|
||
}
|
||
|
||
// 读取页面ID块(4字节)
|
||
uint8_t data[4];
|
||
if (!nfc_hal_read_block(&tag, NFC_PAGE_ID_BLOCK, data)) {
|
||
LOG_ERROR("NFC读取失败");
|
||
return false;
|
||
}
|
||
|
||
// 大端序解析
|
||
*page_id_out = ((uint32_t)data[0] << 24) |
|
||
((uint32_t)data[1] << 16) |
|
||
((uint32_t)data[2] << 8) |
|
||
((uint32_t)data[3]);
|
||
|
||
LOG_DEBUG("NFC读取页面ID: 0x%08X", *page_id_out);
|
||
return true;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
*本文档版权归深圳自然写科技有限公司所有,所有技术细节仅用于软件著作权登记鉴别,请勿用于其他商业用途。*
|