Files
2026-03-22 15:24:40 +08:00

330 lines
10 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 自然写智能点阵笔嵌入式固件软件 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();
}
}