software copyright

This commit is contained in:
jiahong
2026-03-22 15:24:40 +08:00
parent e303bb868a
commit 60f336e345
155 changed files with 127262 additions and 0 deletions
@@ -0,0 +1,311 @@
/*
* 自然写智能点阵笔嵌入式固件软件 V1.0
* ble_send_task.c - BLE数据发送任务
*
* 功能说明:
* 1. 从坐标队列获取数据并打包为BLE通知帧
* 2. 7字节紧凑坐标编码格式
* 3. 发送速率控制(适配BLE连接间隔)
* 4. 笔落下/抬起事件通知
* 5. 设备信息特征值更新(电量/固件版本)
*/
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "event_groups.h"
#include "ble_gatt_server.h"
/* ========== BLE帧格式定义 ========== */
/* 帧头标识 */
#define BLE_FRAME_HEADER 0xAA55
/* 帧类型 */
#define FRAME_TYPE_COORDINATE 0x00 /* 坐标数据帧 */
#define FRAME_TYPE_PEN_DOWN 0x01 /* 笔落下事件 */
#define FRAME_TYPE_PEN_UP 0x02 /* 笔抬起事件 */
#define FRAME_TYPE_DEVICE_INFO 0x03 /* 设备信息帧 */
#define FRAME_TYPE_PAGE_CHANGE 0x04 /* 翻页事件 */
/* 最大BLE MTU通知载荷 */
#define BLE_MAX_NOTIFY_SIZE 20
/* 批量发送缓冲区大小(可打包多个坐标点) */
#define BATCH_BUFFER_SIZE 3
/* ========== 外部引用 ========== */
extern QueueHandle_t g_coordinate_queue;
extern EventGroupHandle_t g_ble_event_group;
extern SemaphoreHandle_t g_system_mutex;
/* 坐标数据包结构 */
typedef struct {
uint32_t raw_x;
uint32_t raw_y;
uint16_t pressure;
uint32_t timestamp_ms;
uint32_t page_id;
uint8_t pen_state;
} CoordinatePacket;
/* ========== 静态变量 ========== */
/* 发送缓冲区 */
static uint8_t s_send_buffer[BLE_MAX_NOTIFY_SIZE];
/* BLE连接状态 */
static volatile bool s_ble_connected = false;
/* 当前页面ID(检测翻页) */
static uint32_t s_current_page_id = 0;
/* 发送统计 */
static uint32_t s_total_sent = 0;
static uint32_t s_send_failures = 0;
/* ========== CRC-16 CCITT计算 ========== */
/**
* CRC-16 CCITT校验计算
* 用于BLE传输数据帧的完整性校验
*
* @param data 数据缓冲区
* @param length 数据长度
* @return CRC-16校验值
*/
static uint16_t crc16_ccitt(const uint8_t *data, uint16_t length) {
uint16_t crc = 0xFFFF;
uint16_t i;
for (i = 0; i < length; i++) {
crc ^= (uint16_t)data[i] << 8;
uint8_t j;
for (j = 0; j < 8; j++) {
if (crc & 0x8000) {
crc = (crc << 1) ^ 0x1021;
} else {
crc <<= 1;
}
}
}
return crc;
}
/* ========== 坐标编码 ========== */
/**
* 将坐标数据编码为7字节紧凑格式
*
* 编码格式:
* 字节0-1: X坐标高16位(大端序)
* 字节2-3: Y坐标高16位
* 字节4: X低4位(高半字节) | Y低4位(低半字节)
* 字节5: 压力值高8位
* 字节6: 压力值低4位(高半字节) | 标志位(低半字节)
*
* @param packet 坐标数据包
* @param output 输出缓冲区(至少7字节)
* @param flags 标志位(低2位:00=数据, 01=笔落下, 02=笔抬起)
*/
static void encode_coordinate(const CoordinatePacket *packet, uint8_t *output,
uint8_t flags) {
/* X坐标(20位精度) */
uint32_t x = packet->raw_x & 0xFFFFF;
output[0] = (uint8_t)((x >> 12) & 0xFF); /* X高8位 */
output[1] = (uint8_t)((x >> 4) & 0xFF); /* X次高8位 */
/* Y坐标(20位精度) */
uint32_t y = packet->raw_y & 0xFFFFF;
output[2] = (uint8_t)((y >> 12) & 0xFF); /* Y高8位 */
output[3] = (uint8_t)((y >> 4) & 0xFF); /* Y次高8位 */
/* X低4位和Y低4位合并到一个字节 */
output[4] = (uint8_t)(((x & 0x0F) << 4) | (y & 0x0F));
/* 压力值(12位精度) */
uint16_t p = packet->pressure & 0x0FFF;
output[5] = (uint8_t)((p >> 4) & 0xFF); /* 压力高8位 */
/* 压力低4位 | 标志位 */
output[6] = (uint8_t)(((p & 0x0F) << 4) | (flags & 0x0F));
}
/* ========== BLE通知发送 ========== */
/**
* 通过BLE GATT通知发送数据帧
*
* @param data 帧数据
* @param length 帧长度
* @return 0成功, -1未连接, -2发送失败
*/
static int ble_send_notification(const uint8_t *data, uint16_t length) {
if (!s_ble_connected) {
return -1;
}
/* 调用BLE GATT服务器发送通知 */
int ret = ble_gatt_notify(BLE_CHAR_STROKE_DATA, data, length);
if (ret == 0) {
s_total_sent++;
} else {
s_send_failures++;
}
return ret;
}
/**
* 发送笔状态事件(落下/抬起)
*/
static void send_pen_event(uint8_t event_type) {
uint8_t frame[7];
memset(frame, 0, sizeof(frame));
/* 事件帧只需要标志位,坐标和压力都为0 */
frame[6] = event_type & 0x0F;
ble_send_notification(frame, 7);
}
/**
* 发送翻页事件
* 当检测到坐标所在页面发生变化时通知上位机
*/
static void send_page_change_event(uint32_t new_page_id) {
uint8_t frame[8];
frame[0] = FRAME_TYPE_PAGE_CHANGE;
frame[1] = (uint8_t)((new_page_id >> 24) & 0xFF);
frame[2] = (uint8_t)((new_page_id >> 16) & 0xFF);
frame[3] = (uint8_t)((new_page_id >> 8) & 0xFF);
frame[4] = (uint8_t)(new_page_id & 0xFF);
/* CRC校验 */
uint16_t crc = crc16_ccitt(frame, 5);
frame[5] = (uint8_t)((crc >> 8) & 0xFF);
frame[6] = (uint8_t)(crc & 0xFF);
frame[7] = 0;
ble_send_notification(frame, 8);
}
/**
* 更新设备信息特征值(电量、固件版本等)
* 上位机可以随时读取此特征值获取笔的状态
*/
static void update_device_info(uint8_t battery_percent) {
uint8_t info[4];
info[0] = battery_percent; /* 电量百分比 */
info[1] = 2; /* 固件主版本号 */
info[2] = 1; /* 固件次版本号 */
info[3] = 5; /* 固件补丁版本号 → V2.1.5 */
ble_gatt_update_characteristic(BLE_CHAR_DEVICE_INFO, info, sizeof(info));
}
/* ========== BLE连接事件回调 ========== */
/**
* BLE连接建立回调(由BLE协议栈调用)
*/
void on_ble_connected(void) {
s_ble_connected = true;
BaseType_t higher_priority_woken = pdFALSE;
xEventGroupSetBitsFromISR(g_ble_event_group, EVT_BLE_CONNECTED,
&higher_priority_woken);
portYIELD_FROM_ISR(higher_priority_woken);
}
/**
* BLE连接断开回调
*/
void on_ble_disconnected(void) {
s_ble_connected = false;
BaseType_t higher_priority_woken = pdFALSE;
xEventGroupSetBitsFromISR(g_ble_event_group, EVT_BLE_DISCONNECTED,
&higher_priority_woken);
portYIELD_FROM_ISR(higher_priority_woken);
}
/* ========== BLE发送主任务 ========== */
/**
* BLE发送任务(FreeRTOS任务函数)
*
* 运行流程:
* 1. 等待BLE连接建立
* 2. 监听笔状态事件(落下/抬起)并发送事件通知
* 3. 从坐标队列读取数据,编码为7字节格式发送
* 4. 翻页检测与通知
* 5. 定期更新设备信息特征值
*/
void ble_send_task(void *pvParameters) {
(void)pvParameters;
CoordinatePacket packet;
uint32_t info_update_counter = 0;
/* 注册BLE连接回调 */
ble_gatt_register_connect_callback(on_ble_connected);
ble_gatt_register_disconnect_callback(on_ble_disconnected);
/* 启动BLE广播 */
ble_gatt_start_advertising();
while (1) {
/* 等待BLE连接 */
if (!s_ble_connected) {
xEventGroupWaitBits(g_ble_event_group, EVT_BLE_CONNECTED,
pdTRUE, pdFALSE, portMAX_DELAY);
}
/* 检查笔状态事件 */
EventBits_t events = xEventGroupGetBits(g_ble_event_group);
if (events & EVT_PEN_DOWN) {
xEventGroupClearBits(g_ble_event_group, EVT_PEN_DOWN);
send_pen_event(FRAME_TYPE_PEN_DOWN);
}
if (events & EVT_PEN_UP) {
xEventGroupClearBits(g_ble_event_group, EVT_PEN_UP);
send_pen_event(FRAME_TYPE_PEN_UP);
}
/* 从坐标队列读取数据(超时10ms,避免永久阻塞) */
if (xQueueReceive(g_coordinate_queue, &packet, pdMS_TO_TICKS(10)) == pdTRUE) {
/* 翻页检测 */
if (packet.page_id != s_current_page_id && s_current_page_id != 0) {
send_page_change_event(packet.page_id);
}
s_current_page_id = packet.page_id;
/* 编码并发送坐标 */
uint8_t encoded[7];
encode_coordinate(&packet, encoded, FRAME_TYPE_COORDINATE);
ble_send_notification(encoded, 7);
}
/* 每500次循环更新一次设备信息(约每5秒) */
info_update_counter++;
if (info_update_counter >= 500) {
info_update_counter = 0;
/* 读取当前电量 */
extern uint8_t power_get_battery_percent(void);
uint8_t battery = power_get_battery_percent();
update_device_info(battery);
}
}
}
@@ -0,0 +1,373 @@
/*
* 自然写智能点阵笔嵌入式固件软件 V1.0
* coordinate_task.c - 坐标计算任务
*
* 功能说明:
* 1. 从图像帧中解码Anoto点阵图案
* 2. 计算笔尖在纸面的物理坐标
* 3. 坐标滤波与去抖(卡尔曼滤波)
* 4. 坐标打包为BLE传输格式
*/
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <math.h>
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "dot_decoder.h"
/* ========== 坐标数据包结构 ========== */
typedef struct {
uint32_t raw_x; /* 原始X坐标(20位精度) */
uint32_t raw_y; /* 原始Y坐标 */
uint16_t pressure; /* 压力值(12位) */
uint32_t timestamp_ms; /* 时间戳 */
uint32_t page_id; /* 页面ID */
uint8_t pen_state; /* 笔状态:0=书写中, 1=笔落下, 2=笔抬起 */
} CoordinatePacket;
/* ========== 卡尔曼滤波器 ========== */
typedef struct {
float x_estimate; /* X状态估计值 */
float y_estimate; /* Y状态估计值 */
float x_error; /* X估计误差协方差 */
float y_error; /* Y估计误差协方差 */
float process_noise; /* 过程噪声 Q */
float measurement_noise; /* 测量噪声 R */
bool initialized; /* 是否已初始化 */
} KalmanFilter2D;
/* ========== 外部引用 ========== */
extern QueueHandle_t g_image_data_queue;
extern QueueHandle_t g_coordinate_queue;
/* ========== 图像帧元数据结构(与image_capture_task一致) ========== */
typedef struct {
uint8_t *pixel_buffer;
uint32_t frame_id;
uint32_t timestamp_ms;
uint8_t quality_score;
uint8_t exposure_value;
uint16_t pressure_raw;
} ImageFrameMetadata;
/* ========== 静态变量 ========== */
/* 卡尔曼滤波器实例 */
static KalmanFilter2D s_kalman;
/* 上一次有效坐标(用于抖动检测) */
static float s_last_valid_x = 0;
static float s_last_valid_y = 0;
/* 点阵码解码工作缓冲区 */
static uint8_t s_decode_buffer[128];
/* 统计信息 */
static uint32_t s_total_decoded = 0;
static uint32_t s_decode_failures = 0;
/* ========== 卡尔曼滤波实现 ========== */
/**
* 初始化卡尔曼滤波器
* @param kf 滤波器实例
* @param q 过程噪声(越大跟踪越快,噪声越多)
* @param r 测量噪声(越大滤波越强,延迟越大)
*/
static void kalman_init(KalmanFilter2D *kf, float q, float r) {
kf->x_estimate = 0;
kf->y_estimate = 0;
kf->x_error = 1.0f;
kf->y_error = 1.0f;
kf->process_noise = q;
kf->measurement_noise = r;
kf->initialized = false;
}
/**
* 卡尔曼滤波更新
* @param kf 滤波器实例
* @param measured_x 测量X值
* @param measured_y 测量Y值
* @param out_x 滤波后X输出
* @param out_y 滤波后Y输出
*/
static void kalman_update(KalmanFilter2D *kf, float measured_x, float measured_y,
float *out_x, float *out_y) {
if (!kf->initialized) {
/* 第一次测量,直接使用测量值 */
kf->x_estimate = measured_x;
kf->y_estimate = measured_y;
kf->initialized = true;
*out_x = measured_x;
*out_y = measured_y;
return;
}
/* 预测步骤:状态不变模型(笔的位置预测 = 上一次估计) */
float x_pred = kf->x_estimate;
float y_pred = kf->y_estimate;
float x_err_pred = kf->x_error + kf->process_noise;
float y_err_pred = kf->y_error + kf->process_noise;
/* 更新步骤:计算卡尔曼增益 */
float kx = x_err_pred / (x_err_pred + kf->measurement_noise);
float ky = y_err_pred / (y_err_pred + kf->measurement_noise);
/* 融合预测与测量 */
kf->x_estimate = x_pred + kx * (measured_x - x_pred);
kf->y_estimate = y_pred + ky * (measured_y - y_pred);
/* 更新误差协方差 */
kf->x_error = (1.0f - kx) * x_err_pred;
kf->y_error = (1.0f - ky) * y_err_pred;
*out_x = kf->x_estimate;
*out_y = kf->y_estimate;
}
/**
* 重置卡尔曼滤波器(新笔画开始时调用)
*/
static void kalman_reset(KalmanFilter2D *kf) {
kf->initialized = false;
kf->x_error = 1.0f;
kf->y_error = 1.0f;
}
/* ========== 抖动检测 ========== */
/**
* 检测坐标是否为抖动(笔静止时传感器的微小抖动)
* 如果两次坐标之间的距离小于阈值,视为抖动并丢弃
*
* @param x 当前X坐标
* @param y 当前Y坐标
* @param threshold 抖动阈值(坐标单位)
* @return true表示是抖动,应丢弃
*/
static bool is_jitter(float x, float y, float threshold) {
float dx = x - s_last_valid_x;
float dy = y - s_last_valid_y;
float distance_sq = dx * dx + dy * dy;
return (distance_sq < threshold * threshold);
}
/* ========== 点阵码图像解码 ========== */
/**
* 从32x32灰度图像中解码Anoto点阵图案
*
* 解码步骤:
* 1. 二值化:将灰度图转为黑白图
* 2. 点检测:定位图案中的各个墨点位置
* 3. 网格对齐:将检测到的点对齐到规则网格
* 4. 编码读取:根据点相对于网格中心的偏移方向读取编码值
* 5. 坐标计算:将编码序列映射为全局坐标
*
* @param pixels 32x32灰度图像数据
* @param quality 图像质量评分
* @param out_x 解码输出X坐标
* @param out_y 解码输出Y坐标
* @param out_page_id 解码输出页面ID
* @return 0成功, -1解码失败
*/
static int decode_dot_pattern(const uint8_t *pixels, uint8_t quality,
uint32_t *out_x, uint32_t *out_y,
uint32_t *out_page_id) {
/* 步骤1:自适应二值化 */
uint8_t threshold = 128;
/* 根据图像亮度动态调整阈值(Otsu方法简化版) */
uint32_t histogram[256] = {0};
uint16_t i;
for (i = 0; i < SENSOR_PIXELS; i++) {
histogram[pixels[i]]++;
}
/* 寻找双峰之间的谷值作为阈值 */
uint32_t total = SENSOR_PIXELS;
uint32_t sum = 0;
for (i = 0; i < 256; i++) {
sum += i * histogram[i];
}
uint32_t sum_bg = 0;
uint32_t weight_bg = 0;
float max_variance = 0;
for (i = 0; i < 256; i++) {
weight_bg += histogram[i];
if (weight_bg == 0) continue;
uint32_t weight_fg = total - weight_bg;
if (weight_fg == 0) break;
sum_bg += i * histogram[i];
float mean_bg = (float)sum_bg / weight_bg;
float mean_fg = (float)(sum - sum_bg) / weight_fg;
float diff = mean_bg - mean_fg;
float variance = (float)weight_bg * weight_fg * diff * diff;
if (variance > max_variance) {
max_variance = variance;
threshold = (uint8_t)i;
}
}
/* 步骤2:二值化并检测墨点中心 */
uint8_t dot_count = 0;
int16_t dot_x[64], dot_y[64]; /* 最多检测64个点 */
for (int row = 1; row < SENSOR_HEIGHT - 1; row++) {
for (int col = 1; col < SENSOR_WIDTH - 1; col++) {
uint8_t center = pixels[row * SENSOR_WIDTH + col];
if (center < threshold) {
/* 暗像素,检查是否为局部极小值(简单的点中心检测) */
uint8_t up = pixels[(row - 1) * SENSOR_WIDTH + col];
uint8_t down = pixels[(row + 1) * SENSOR_WIDTH + col];
uint8_t left = pixels[row * SENSOR_WIDTH + (col - 1)];
uint8_t right = pixels[row * SENSOR_WIDTH + (col + 1)];
if (center <= up && center <= down &&
center <= left && center <= right) {
if (dot_count < 64) {
dot_x[dot_count] = col;
dot_y[dot_count] = row;
dot_count++;
}
}
}
}
}
/* 至少需要检测到4个点才能解码 */
if (dot_count < 4) {
return -1;
}
/* 步骤3-5:调用点阵码解码器(核心算法在dot_decoder模块中) */
DotDecodeResult result;
int ret = dot_decoder_process(dot_x, dot_y, dot_count, &result);
if (ret == 0) {
*out_x = result.coordinate_x;
*out_y = result.coordinate_y;
*out_page_id = result.page_id;
return 0;
}
return -1;
}
/* ========== 压力值处理 ========== */
/**
* 处理原始ADC压力值
* 12位ADC → 归一化并应用非线性映射
*
* @param raw_adc 原始ADC值(0-4095
* @return 处理后的压力值(0-4095,非线性映射后)
*/
static uint16_t process_pressure(uint16_t raw_adc) {
/* 去除静态偏移(笔尖自重产生的基础压力) */
const uint16_t offset = 200;
if (raw_adc < offset) {
return 0;
}
uint16_t adjusted = raw_adc - offset;
/* 非线性映射(平方根曲线,使轻触更灵敏) */
float normalized = (float)adjusted / (4095.0f - offset);
float mapped = sqrtf(normalized);
return (uint16_t)(mapped * 4095);
}
/* ========== 坐标计算主任务 ========== */
/**
* 坐标计算任务(FreeRTOS任务函数)
*
* 运行流程:
* 1. 从图像数据队列接收帧元数据
* 2. 解码点阵图案获得原始坐标
* 3. 卡尔曼滤波去噪
* 4. 抖动检测
* 5. 坐标打包并放入BLE发送队列
*/
void coordinate_task(void *pvParameters) {
(void)pvParameters;
ImageFrameMetadata frame;
CoordinatePacket packet;
/* 初始化卡尔曼滤波器 */
/* Q=0.1 跟踪速度适中, R=0.5 中等滤波强度 */
kalman_init(&s_kalman, 0.1f, 0.5f);
/* 抖动检测阈值(坐标单位,约0.1mm) */
const float jitter_threshold = 3.0f;
while (1) {
/* 阻塞等待图像帧数据 */
if (xQueueReceive(g_image_data_queue, &frame, portMAX_DELAY) != pdTRUE) {
continue;
}
/* 解码点阵图案 */
uint32_t raw_x, raw_y, page_id;
int decode_ret = decode_dot_pattern(frame.pixel_buffer, frame.quality_score,
&raw_x, &raw_y, &page_id);
if (decode_ret != 0) {
s_decode_failures++;
continue;
}
s_total_decoded++;
/* 卡尔曼滤波 */
float filtered_x, filtered_y;
kalman_update(&s_kalman, (float)raw_x, (float)raw_y,
&filtered_x, &filtered_y);
/* 抖动检测 */
if (is_jitter(filtered_x, filtered_y, jitter_threshold)) {
continue; /* 丢弃抖动数据 */
}
/* 更新最后有效坐标 */
s_last_valid_x = filtered_x;
s_last_valid_y = filtered_y;
/* 处理压力值 */
uint16_t pressure = process_pressure(frame.pressure_raw);
/* 构建坐标数据包 */
packet.raw_x = (uint32_t)filtered_x;
packet.raw_y = (uint32_t)filtered_y;
packet.pressure = pressure;
packet.timestamp_ms = frame.timestamp_ms;
packet.page_id = page_id;
packet.pen_state = 0; /* 书写中 */
/* 放入BLE发送队列(非阻塞,满则丢弃最老的) */
if (xQueueSend(g_coordinate_queue, &packet, 0) != pdTRUE) {
/* 队列满,读出一个旧数据再写入 */
CoordinatePacket dummy;
xQueueReceive(g_coordinate_queue, &dummy, 0);
xQueueSend(g_coordinate_queue, &packet, 0);
}
}
}
@@ -0,0 +1,329 @@
/*
* 自然写智能点阵笔嵌入式固件软件 V1.0
* image_capture_task.c - 图像采集任务
*
* 功能说明:
* 1. 以100Hz频率驱动CMOS图像传感器采集点阵图案
* 2. DMA方式高速传输图像数据
* 3. 笔尖接触检测(上升沿/下降沿中断)
* 4. 图像帧质量快速评估(丢弃模糊帧)
* 5. 采集参数自适应调节(曝光、增益)
*/
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "event_groups.h"
#include "camera_driver.h"
#include "hal_spi.h"
#include "hal_dma.h"
#include "hal_gpio.h"
/* ========== 常量定义 ========== */
/* 采集频率(Hz */
#define CAPTURE_FREQUENCY_HZ 100
/* 采集周期(毫秒) */
#define CAPTURE_PERIOD_MS (1000 / CAPTURE_FREQUENCY_HZ)
/* 图像传感器分辨率 */
#define SENSOR_WIDTH 32
#define SENSOR_HEIGHT 32
#define SENSOR_PIXELS (SENSOR_WIDTH * SENSOR_HEIGHT)
/* 单帧图像字节数(8位灰度) */
#define FRAME_SIZE_BYTES SENSOR_PIXELS
/* DMA传输缓冲区(双缓冲) */
#define DMA_BUFFER_COUNT 2
/* 图像质量阈值(低于此值判定为模糊/无效帧) */
#define QUALITY_THRESHOLD 30
/* 最大连续无效帧数(超过则认为笔离开纸面) */
#define MAX_INVALID_FRAMES 5
/* 自动曝光调节步长 */
#define AUTO_EXPOSURE_STEP 4
/* ========== 数据结构 ========== */
/* 图像帧元数据(放入队列传递给坐标计算任务) */
typedef struct {
uint8_t *pixel_buffer; /* 指向DMA缓冲区的像素数据 */
uint32_t frame_id; /* 帧序号 */
uint32_t timestamp_ms; /* 采集时间戳 */
uint8_t quality_score; /* 图像质量评分(0-255 */
uint8_t exposure_value; /* 当前曝光值 */
uint16_t pressure_raw; /* 同步采集的压力原始ADC值 */
} ImageFrameMetadata;
/* 传感器配置参数 */
typedef struct {
uint8_t exposure; /* 曝光时间(寄存器值) */
uint8_t gain; /* 模拟增益 */
uint8_t threshold; /* 二值化阈值 */
bool auto_exposure_enabled; /* 是否启用自动曝光 */
} SensorConfig;
/* ========== 外部引用 ========== */
extern QueueHandle_t g_image_data_queue;
extern EventGroupHandle_t g_ble_event_group;
/* ========== 静态变量 ========== */
/* DMA双缓冲区 */
static uint8_t s_dma_buffer[DMA_BUFFER_COUNT][FRAME_SIZE_BYTES]
__attribute__((aligned(4)));
/* 当前活跃的DMA缓冲区索引 */
static volatile uint8_t s_active_buffer = 0;
/* 帧计数器 */
static uint32_t s_frame_counter = 0;
/* 连续无效帧计数 */
static uint8_t s_invalid_frame_count = 0;
/* 传感器配置 */
static SensorConfig s_sensor_config;
/* 笔尖状态 */
static volatile bool s_pen_touching = false;
/* ========== 笔尖接触检测 ========== */
/**
* 笔尖接触检测GPIO中断回调
* 通过检测微动开关或红外反射判断笔尖是否接触纸面
*/
void pen_tip_irq_handler(void) {
bool state = hal_gpio_read(GPIO_PEN_TIP_PIN);
BaseType_t higher_priority_woken = pdFALSE;
if (state && !s_pen_touching) {
/* 笔落下(接触纸面) */
s_pen_touching = true;
xEventGroupSetBitsFromISR(g_ble_event_group, EVT_PEN_DOWN,
&higher_priority_woken);
} else if (!state && s_pen_touching) {
/* 笔抬起(离开纸面) */
s_pen_touching = false;
xEventGroupSetBitsFromISR(g_ble_event_group, EVT_PEN_UP,
&higher_priority_woken);
}
portYIELD_FROM_ISR(higher_priority_woken);
}
/* ========== DMA传输完成回调 ========== */
/**
* SPI DMA传输完成中断回调
* 图像数据已从传感器通过DMA传输到内存缓冲区
*/
void spi_dma_complete_handler(void) {
/* 切换到另一个DMA缓冲区(乒乓缓冲) */
s_active_buffer = (s_active_buffer + 1) % DMA_BUFFER_COUNT;
}
/* ========== 图像质量评估 ========== */
/**
* 快速评估图像帧质量
* 通过计算图像的对比度(标准差近似)来判断是否有效
* 有效的点阵图案应该有清晰的明暗对比
*
* @param pixels 像素数据
* @param length 数据长度
* @return 质量评分(0-255,越高越好)
*/
static uint8_t evaluate_frame_quality(const uint8_t *pixels, uint16_t length) {
uint32_t sum = 0;
uint32_t sum_sq = 0;
uint16_t i;
/* 采样统计(每4个像素取1个,减少计算量) */
uint16_t sample_count = 0;
for (i = 0; i < length; i += 4) {
uint32_t val = pixels[i];
sum += val;
sum_sq += val * val;
sample_count++;
}
if (sample_count == 0) return 0;
/* 计算方差的近似值(反映对比度) */
uint32_t mean = sum / sample_count;
uint32_t variance = (sum_sq / sample_count) - (mean * mean);
/* 方差映射到0-255评分 */
if (variance > 2000) return 255;
return (uint8_t)(variance * 255 / 2000);
}
/* ========== 自动曝光调节 ========== */
/**
* 根据图像亮度自动调节传感器曝光参数
* 目标:使图像平均亮度保持在中间范围(100-150)
*/
static void auto_exposure_adjust(const uint8_t *pixels, uint16_t length) {
if (!s_sensor_config.auto_exposure_enabled) {
return;
}
/* 计算平均亮度 */
uint32_t sum = 0;
uint16_t i;
for (i = 0; i < length; i += 8) {
sum += pixels[i];
}
uint8_t avg_brightness = (uint8_t)(sum / (length / 8));
/* 根据亮度偏差调节曝光值 */
if (avg_brightness < 80 && s_sensor_config.exposure < 250) {
/* 过暗,增加曝光 */
s_sensor_config.exposure += AUTO_EXPOSURE_STEP;
camera_set_exposure(s_sensor_config.exposure);
} else if (avg_brightness > 170 && s_sensor_config.exposure > AUTO_EXPOSURE_STEP) {
/* 过亮,减少曝光 */
s_sensor_config.exposure -= AUTO_EXPOSURE_STEP;
camera_set_exposure(s_sensor_config.exposure);
}
}
/* ========== 传感器初始化 ========== */
/**
* 配置CMOS图像传感器初始参数
*/
static void sensor_setup(void) {
/* 设置默认曝光和增益 */
s_sensor_config.exposure = 128;
s_sensor_config.gain = 64;
s_sensor_config.threshold = 128;
s_sensor_config.auto_exposure_enabled = true;
/* 写入传感器寄存器 */
camera_set_exposure(s_sensor_config.exposure);
camera_set_gain(s_sensor_config.gain);
/* 配置为连续帧模式 */
camera_set_mode(CAMERA_MODE_CONTINUOUS);
/* 配置SPI DMA接收 */
hal_dma_config(DMA_CHANNEL_SPI_RX,
(uint32_t)camera_get_data_register(),
(uint32_t)s_dma_buffer[0],
FRAME_SIZE_BYTES);
}
/* ========== 图像采集主任务 ========== */
/**
* 图像采集任务(FreeRTOS任务函数)
*
* 运行流程:
* 1. 等待笔尖接触纸面
* 2. 以100Hz频率触发CMOS传感器拍摄
* 3. DMA传输图像数据到双缓冲区
* 4. 评估图像质量,丢弃无效帧
* 5. 将有效帧元数据放入队列供坐标计算任务处理
* 6. 笔抬起后暂停采集,进入低功耗等待
*/
void image_capture_task(void *pvParameters) {
(void)pvParameters;
TickType_t last_wake_time;
/* 初始化传感器参数 */
sensor_setup();
/* 注册笔尖GPIO中断 */
hal_gpio_set_irq(GPIO_PEN_TIP_PIN, GPIO_IRQ_BOTH_EDGE, pen_tip_irq_handler);
while (1) {
/* 等待笔落下事件(低功耗阻塞) */
xEventGroupWaitBits(g_ble_event_group, EVT_PEN_DOWN,
pdTRUE, pdFALSE, portMAX_DELAY);
/* 笔已接触纸面,启动高速采集 */
camera_power_on();
/* 重置帧计数和无效帧计数 */
s_frame_counter = 0;
s_invalid_frame_count = 0;
/* 记录采集起始时间 */
last_wake_time = xTaskGetTickCount();
/* 采集循环:持续采集直到笔抬起 */
while (s_pen_touching) {
/* 触发传感器拍摄 */
camera_trigger_capture();
/* 启动DMA传输(异步,CPU可做其他事) */
uint8_t current_buffer = s_active_buffer;
hal_dma_start(DMA_CHANNEL_SPI_RX,
(uint32_t)s_dma_buffer[current_buffer],
FRAME_SIZE_BYTES);
/* 等待DMA完成(通常< 1ms */
hal_dma_wait_complete(DMA_CHANNEL_SPI_RX, 5);
/* 同步读取压力传感器ADC值 */
uint16_t pressure_raw = pressure_sensor_read_raw();
/* 评估图像质量 */
uint8_t quality = evaluate_frame_quality(
s_dma_buffer[current_buffer], FRAME_SIZE_BYTES);
if (quality >= QUALITY_THRESHOLD) {
/* 有效帧,放入队列 */
ImageFrameMetadata metadata;
metadata.pixel_buffer = s_dma_buffer[current_buffer];
metadata.frame_id = s_frame_counter;
metadata.timestamp_ms = xTaskGetTickCount() * portTICK_PERIOD_MS;
metadata.quality_score = quality;
metadata.exposure_value = s_sensor_config.exposure;
metadata.pressure_raw = pressure_raw;
/* 非阻塞方式入队(如果队列满则丢弃) */
xQueueSend(g_image_data_queue, &metadata, 0);
s_invalid_frame_count = 0;
} else {
s_invalid_frame_count++;
/* 连续多个无效帧,可能笔已离开但中断未触发 */
if (s_invalid_frame_count >= MAX_INVALID_FRAMES) {
s_pen_touching = false;
break;
}
}
/* 每16帧调整一次曝光(避免频繁调节) */
if ((s_frame_counter & 0x0F) == 0) {
auto_exposure_adjust(s_dma_buffer[current_buffer], FRAME_SIZE_BYTES);
}
s_frame_counter++;
/* 精确定时:等待到下一个采集时间点 */
vTaskDelayUntil(&last_wake_time, pdMS_TO_TICKS(CAPTURE_PERIOD_MS));
}
/* 笔抬起,关闭传感器降低功耗 */
camera_power_off();
}
}
@@ -0,0 +1,428 @@
/*
* 自然写智能点阵笔嵌入式固件软件 V1.0
* power_monitor_task.c - 电源监测与低功耗管理任务
*
* 功能说明:
* 1. 电池电压ADC采样与电量百分比估算
* 2. 充电检测与充电状态管理
* 3. 低电量告警与自动关机保护
* 4. 低功耗状态机(Active → Light Sleep → Deep Sleep
* 5. USB充电IC状态监测
*/
#include <stdint.h>
#include <stdbool.h>
#include "FreeRTOS.h"
#include "task.h"
#include "event_groups.h"
#include "semphr.h"
#include "hal_adc.h"
#include "hal_gpio.h"
#include "power_manager.h"
#include "led_driver.h"
/* ========== 电池参数定义 ========== */
/* 电池满充电压(mV):锂聚合物3.7V标称 */
#define BATTERY_FULL_MV 4200
/* 电池截止电压(mV):低于此值必须关机保护 */
#define BATTERY_CUTOFF_MV 3300
/* 低电量告警阈值(百分比) */
#define LOW_BATTERY_THRESHOLD 10
/* 极低电量关机阈值 */
#define CRITICAL_BATTERY_THRESHOLD 3
/* ADC参考电压(mV */
#define ADC_VREF_MV 3300
/* ADC分辨率(12位) */
#define ADC_MAX_VALUE 4095
/* 电池电压分压比(电阻分压器:R1=100K, R2=100K → 2:1 */
#define BATTERY_DIVIDER_RATIO 2
/* 电压采样滤波窗口大小 */
#define VOLTAGE_FILTER_WINDOW 8
/* 电源监测周期(毫秒) */
#define POWER_MONITOR_PERIOD_MS 5000
/* 自动休眠超时(毫秒):笔静止超过此时间自动进入深度睡眠 */
#define AUTO_SLEEP_TIMEOUT_MS 300000 /* 5分钟 */
/* ========== 电源状态枚举 ========== */
typedef enum {
POWER_STATE_ACTIVE, /* 活跃状态(正常工作) */
POWER_STATE_LIGHT_SLEEP, /* 轻度睡眠(BLE保持连接) */
POWER_STATE_DEEP_SLEEP, /* 深度睡眠(仅保留RTC唤醒) */
POWER_STATE_CHARGING, /* 充电中 */
POWER_STATE_SHUTDOWN /* 关机保护 */
} PowerState;
/* ========== 外部引用 ========== */
extern EventGroupHandle_t g_ble_event_group;
extern SemaphoreHandle_t g_system_mutex;
/* 系统状态结构体(在main.c中定义) */
typedef struct {
bool pen_is_down;
bool ble_connected;
bool is_charging;
uint8_t battery_percent;
uint32_t total_strokes;
uint32_t uptime_seconds;
uint8_t error_flags;
} SystemState;
extern SystemState g_system_state;
/* ========== 静态变量 ========== */
/* 当前电源状态 */
static PowerState s_power_state = POWER_STATE_ACTIVE;
/* 电压采样滤波缓冲区 */
static uint16_t s_voltage_buffer[VOLTAGE_FILTER_WINDOW];
static uint8_t s_voltage_buffer_index = 0;
static bool s_voltage_buffer_full = false;
/* 最后一次活动时间(用于自动休眠判断) */
static uint32_t s_last_activity_time = 0;
/* ========== 电压采样与滤波 ========== */
/**
* 读取电池原始ADC值并转换为电压(mV)
*/
static uint16_t read_battery_voltage_mv(void) {
/* 读取ADC原始值 */
uint16_t adc_raw = hal_adc_read(ADC_CHANNEL_BATTERY);
/* ADC值 → 分压后电压 → 实际电池电压 */
uint32_t voltage_mv = (uint32_t)adc_raw * ADC_VREF_MV / ADC_MAX_VALUE;
voltage_mv *= BATTERY_DIVIDER_RATIO;
return (uint16_t)voltage_mv;
}
/**
* 移动平均滤波
* 对连续采样的电压值取平均,消除ADC噪声
*
* @param new_sample 新采样的电压值(mV
* @return 滤波后的电压值
*/
static uint16_t voltage_filter(uint16_t new_sample) {
s_voltage_buffer[s_voltage_buffer_index] = new_sample;
s_voltage_buffer_index = (s_voltage_buffer_index + 1) % VOLTAGE_FILTER_WINDOW;
if (s_voltage_buffer_index == 0) {
s_voltage_buffer_full = true;
}
uint8_t count = s_voltage_buffer_full ? VOLTAGE_FILTER_WINDOW : s_voltage_buffer_index;
uint32_t sum = 0;
uint8_t i;
for (i = 0; i < count; i++) {
sum += s_voltage_buffer[i];
}
return (uint16_t)(sum / count);
}
/* ========== 电量百分比估算 ========== */
/**
* 根据电池电压估算电量百分比
* 使用分段线性插值模拟锂电池放电曲线
*
* 锂聚合物电池典型放电曲线(近似分段线性):
* 4200mV → 100%
* 4060mV → 90%
* 3920mV → 80%
* 3830mV → 70%
* 3750mV → 60%
* 3680mV → 50%
* 3620mV → 40%
* 3570mV → 30%
* 3500mV → 20%
* 3400mV → 10%
* 3300mV → 0%
*/
static uint8_t estimate_battery_percent(uint16_t voltage_mv) {
/* 放电曲线查找表(电压mV → 百分比) */
static const struct {
uint16_t voltage;
uint8_t percent;
} discharge_curve[] = {
{4200, 100},
{4060, 90},
{3920, 80},
{3830, 70},
{3750, 60},
{3680, 50},
{3620, 40},
{3570, 30},
{3500, 20},
{3400, 10},
{3300, 0}
};
const uint8_t table_size = sizeof(discharge_curve) / sizeof(discharge_curve[0]);
/* 边界检查 */
if (voltage_mv >= discharge_curve[0].voltage) {
return 100;
}
if (voltage_mv <= discharge_curve[table_size - 1].voltage) {
return 0;
}
/* 分段线性插值 */
uint8_t i;
for (i = 0; i < table_size - 1; i++) {
if (voltage_mv >= discharge_curve[i + 1].voltage) {
uint16_t v_high = discharge_curve[i].voltage;
uint16_t v_low = discharge_curve[i + 1].voltage;
uint8_t p_high = discharge_curve[i].percent;
uint8_t p_low = discharge_curve[i + 1].percent;
/* 线性插值 */
uint16_t v_range = v_high - v_low;
uint16_t v_offset = voltage_mv - v_low;
return p_low + (uint8_t)((uint32_t)v_offset * (p_high - p_low) / v_range);
}
}
return 0;
}
/* ========== 充电检测 ========== */
/**
* 检测USB充电状态
* 通过GPIO读取充电IC的状态引脚
*
* @return 0=未充电, 1=充电中, 2=充满
*/
static uint8_t detect_charging_state(void) {
/* STAT1引脚:低电平=充电中,高电平=充满或未充电 */
bool stat1 = hal_gpio_read(GPIO_CHARGE_STAT1);
/* STAT2引脚:低电平=充满 */
bool stat2 = hal_gpio_read(GPIO_CHARGE_STAT2);
/* USB电源检测引脚 */
bool usb_power = hal_gpio_read(GPIO_USB_DETECT);
if (!usb_power) {
return 0; /* USB未连接,未充电 */
}
if (!stat1) {
return 1; /* 充电中 */
}
if (!stat2) {
return 2; /* 充满 */
}
return 0;
}
/* ========== LED状态指示 ========== */
/**
* 根据电源状态和电量更新LED指示
*/
static void update_led_indication(uint8_t battery_percent, uint8_t charge_state) {
if (charge_state == 1) {
/* 充电中:绿色呼吸灯 */
led_set_mode(LED_MODE_BREATH_GREEN);
} else if (charge_state == 2) {
/* 充满:绿色常亮 */
led_set_mode(LED_MODE_SOLID_GREEN);
} else if (battery_percent <= LOW_BATTERY_THRESHOLD) {
/* 低电量:红色慢闪 */
led_set_mode(LED_MODE_BLINK_RED);
} else if (battery_percent <= CRITICAL_BATTERY_THRESHOLD) {
/* 极低电量:红色快闪 */
led_set_mode(LED_MODE_FAST_BLINK_RED);
} else if (g_system_state.ble_connected) {
/* 已连接:蓝色常亮 */
led_set_mode(LED_MODE_SOLID_BLUE);
} else {
/* 未连接:蓝色慢闪 */
led_set_mode(LED_MODE_BLINK_BLUE);
}
}
/* ========== 低功耗管理 ========== */
/**
* 进入轻度睡眠模式
* 关闭不必要的外设,降低CPU频率
* BLE连接保持,可被笔尖触摸或BLE命令唤醒
*/
static void enter_light_sleep(void) {
if (s_power_state == POWER_STATE_LIGHT_SLEEP) {
return;
}
/* 关闭摄像头 */
camera_power_off();
/* 关闭SPI(传感器通信) */
hal_spi_disable(SPI_PORT_1);
/* 降低CPU频率到16MHz */
SystemClock_SetLow();
/* LED关闭 */
led_set_mode(LED_MODE_OFF);
s_power_state = POWER_STATE_LIGHT_SLEEP;
}
/**
* 进入深度睡眠模式
* 关闭所有外设和BLE,仅保留RTC和GPIO唤醒
* 适用于长时间不使用的场景
*/
static void enter_deep_sleep(void) {
/* 断开BLE连接 */
ble_gatt_disconnect();
ble_gatt_stop_advertising();
/* 关闭所有外设 */
camera_power_off();
hal_spi_disable(SPI_PORT_1);
hal_i2c_disable(I2C_PORT_1);
hal_adc_disable(ADC_CHANNEL_BATTERY);
/* 保存系统状态到Flash */
offline_storage_flush();
/* 配置唤醒源(笔尖GPIO中断唤醒) */
hal_gpio_set_wakeup(GPIO_PEN_TIP_PIN, GPIO_WAKEUP_RISING);
/* 进入MCU深度睡眠模式(不应返回) */
hal_enter_deep_sleep();
}
/**
* 从轻度睡眠唤醒,恢复正常工作状态
*/
static void wake_from_light_sleep(void) {
/* 恢复CPU频率 */
SystemClock_Config();
/* 重新使能SPI */
hal_spi_enable(SPI_PORT_1);
s_power_state = POWER_STATE_ACTIVE;
s_last_activity_time = xTaskGetTickCount();
}
/* ========== 电源监测主任务 ========== */
/**
* 电源监测任务(FreeRTOS任务函数)
*
* 运行流程:
* 1. 定期读取电池电压并估算电量
* 2. 检测充电状态
* 3. 低电量告警和自动关机保护
* 4. 更新LED状态指示
* 5. 自动休眠判断
*/
void power_monitor_task(void *pvParameters) {
(void)pvParameters;
TickType_t last_wake_time = xTaskGetTickCount();
s_last_activity_time = last_wake_time;
while (1) {
/* 读取并滤波电池电压 */
uint16_t raw_mv = read_battery_voltage_mv();
uint16_t filtered_mv = voltage_filter(raw_mv);
/* 估算电量百分比 */
uint8_t battery_percent = estimate_battery_percent(filtered_mv);
/* 检测充电状态 */
uint8_t charge_state = detect_charging_state();
/* 更新全局系统状态 */
if (xSemaphoreTake(g_system_mutex, pdMS_TO_TICKS(100)) == pdTRUE) {
g_system_state.battery_percent = battery_percent;
g_system_state.is_charging = (charge_state == 1);
xSemaphoreGive(g_system_mutex);
}
/* 更新LED指示 */
update_led_indication(battery_percent, charge_state);
/* 低电量告警处理 */
if (battery_percent <= LOW_BATTERY_THRESHOLD && charge_state == 0) {
/* 通知上位机低电量 */
xEventGroupSetBits(g_ble_event_group, EVT_LOW_BATTERY);
}
/* 极低电量自动关机保护 */
if (battery_percent <= CRITICAL_BATTERY_THRESHOLD && charge_state == 0) {
enter_deep_sleep();
}
/* 充电状态变化通知 */
if (charge_state > 0) {
xEventGroupSetBits(g_ble_event_group, EVT_CHARGING);
s_power_state = POWER_STATE_CHARGING;
s_last_activity_time = xTaskGetTickCount();
}
/* 自动休眠检查:笔没有书写且BLE空闲超时 */
if (!g_system_state.pen_is_down && charge_state == 0) {
uint32_t idle_time = (xTaskGetTickCount() - s_last_activity_time)
* portTICK_PERIOD_MS;
if (idle_time > AUTO_SLEEP_TIMEOUT_MS) {
if (s_power_state == POWER_STATE_ACTIVE) {
enter_light_sleep();
} else if (idle_time > AUTO_SLEEP_TIMEOUT_MS * 2) {
/* 静止超过10分钟,进入深度睡眠 */
enter_deep_sleep();
}
}
} else {
/* 有活动,重置计时器 */
s_last_activity_time = xTaskGetTickCount();
if (s_power_state == POWER_STATE_LIGHT_SLEEP) {
wake_from_light_sleep();
}
}
/* 休眠到下一个监测周期 */
vTaskDelayUntil(&last_wake_time, pdMS_TO_TICKS(POWER_MONITOR_PERIOD_MS));
}
}
/* ========== 外部查询接口 ========== */
/** 获取当前电量百分比(供其他模块调用) */
uint8_t power_get_battery_percent(void) {
return g_system_state.battery_percent;
}
/** 获取当前电源状态 */
uint8_t power_get_state(void) {
return (uint8_t)s_power_state;
}