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,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);
}
}
}