# 自然写智能点阵笔嵌入式固件软件 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 // 一次处理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 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; } ``` --- *本文档版权归深圳自然写科技有限公司所有,所有技术细节仅用于软件著作权登记鉴别,请勿用于其他商业用途。*