一、输入子系统结构体设计
在这个项目中,有使用一个按键,仿照高手代码进行编程,抽象对应的结构体如下
input_system.h
#ifndef __INPUT_SYSTEM_H #define __INPUT_SYSTEM_H #ifndef NULL #define NULL (void *)0 #endif #define TIME_T int #define INPUT_BUF_LEN 20 /* 事件类型 */ typedef enum { INPUT_EVENT_TYPE_KEY, INPUT_EVENT_TYPE_TOUCH, INPUT_EVENT_TYPE_NET, INPUT_EVENT_TYPE_STDIO } INPUT_EVENT_TYPE; /* 按键状态*/ typedef enum { KEY_STATE_PRESSED, /* 按下 */ KEY_STATE_RELEASED, /* 弹起 */ KEY_STATE_LONG_PRESS, /* 长按 */ KEY_STATE_LONG_RELEASED, /* 长按弹起 */ KEY_STATE_REPEAT, /* 长按连发 */ KEY_STATE_DOUBLE_CLICK, /* 双击 */ KEY_STATE_MULTI_CLICK /* 多击 */ } KEY_STATE; /* 输入事件结构体扩展 */ typedef struct InputEvent { TIME_T time; /* 事件时间戳 */ INPUT_EVENT_TYPE eType; /* 事件类型 */ /* 通用事件数据 */ union { /* 按键事件数据 */ struct { int iKey; /* 按键代码 */ KEY_STATE eState; /* 按键状态 */ int iDuration; /* 持续时间(ms),用于长按判断 */ int iClickCount; /* 点击次数,用于多击判断 */ } key; /* 触摸事件数据 */ struct { int iX; int iY; int iPressure; } touch; /* 网络事件数据 */ struct { int iEventCode; char strData[INPUT_BUF_LEN]; } net; /* 标准输入事件数据 */ struct { char strInput[INPUT_BUF_LEN]; } stdio; } data; } InputEvent, *PInputEvent; typedef struct InputDevice { char *name; int (*GetInputEvent)(PInputEvent ptInputEvent); int (*DeviceInit)(void); int (*DeviceExit)(void); struct InputDevice *pNext; } InputDevice, *PInputDevice; /********************************************************************** * 函数名称: AddInputDevices * 功能描述: 注册多个输入设备 * 输入参数: 无 * 输出参数: 无 * 返 回 值: 无 ***********************************************************************/ void AddInputDevices(void); /********************************************************************** * 函数名称: InitInputDevices * 功能描述: 初始化所有的输入设备 * 输入参数: 无 * 输出参数: 无 * 返 回 值: 无 ***********************************************************************/ void InitInputDevices(void); /********************************************************************** * 函数名称: InputDeviceRegister * 功能描述: 注册一个输入设备 * 输入参数: ptInputDevice-输入设备 * 输出参数: 无 * 返 回 值: 无 ***********************************************************************/ void InputDeviceRegister(PInputDevice ptInputDevice); #endif /* __INPUT_SYSTEM_H */使用共用体设计
在上述的代码片段中,使用了union共用体进行设计,主要原因是节省内存空间,
输入事件在某个时刻只能是一种类型: 不可能同时是按键事件和触摸事件
不可能同时是网络事件和标准输入事件 这种互斥性非常适合使用共用体,因为同一时间只需要存储一种事件的数据。
二、输入子系统按键驱动测试
1、硬件原理图
设计思路:找到自己积累的按键驱动代码,进行移植即可,移植过程略。设计一些c函数,代码片段如下:
2、扫描任务
/* ********************************************************************************************************* * 函 数 名: key_scan_task * 功能说明: 按键扫描任务,每10ms调用一次bsp_KeyScan10ms * 形 参: pvParameters - 任务参数 * 返 回 值: 无 ********************************************************************************************************* */ static void key_scan_task(void *pvParameters) { while (1) { /* 每10ms调用一次按键扫描函数 */ bsp_KeyScan10ms(); /* 处理按键事件 */ key_event_process(); /* 延时10ms */ vTaskDelay(10); } }3、按键处理部分
/* ********************************************************************************************************* * 函 数 名: key_event_process * 功能说明: 处理按键事件,转换为InputEvent并发送到队列 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ static void key_event_process(void) { uint8_t key_code; InputEvent event; /* 获取按键事件 */ key_code = bsp_GetKey(); if (key_code != KEY_NONE) { /* 填充InputEvent结构 */ event.time = xTaskGetTickCount(); event.eType = INPUT_EVENT_TYPE_KEY; /* 根据按键代码解析按键信息 */ switch (key_code) { case KEY_1_DOWN: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_PRESSED; event.data.key.iTime = xTaskGetTickCount(); event.data.key.iClickCount = 0; break; case KEY_1_UP: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_RELEASED; event.data.key.iTime = xTaskGetTickCount(); event.data.key.iClickCount = 1; break; case KEY_1_LONG_DOWN: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_LONG_PRESS; event.data.key.iTime = xTaskGetTickCount(); event.data.key.iClickCount = 0; break; case KEY_1_LONG_UP: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_LONG_RELEASED; event.data.key.iTime = xTaskGetTickCount(); event.data.key.iClickCount = 0; break; case KEY_1_AUTO_UP: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_REPEAT; event.data.key.iTime = xTaskGetTickCount(); event.data.key.iClickCount = 0; break; case KEY_1_DB_UP: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_DOUBLE_CLICK; event.data.key.iTime = xTaskGetTickCount(); event.data.key.iClickCount = 2; break; default: return; } /* 发送事件到队列 */ xQueueSend(xKeyQueue, &event, 0); } }4、接收队列
/* ********************************************************************************************************* * 函 数 名: input_test_task * 功能说明: 按键测试任务 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ static void input_test_task(void *pvParameters) { InputEvent event; while (1) { /* 从队列中获取按键事件 */ if (xQueueReceive(xKeyQueue, &event, portMAX_DELAY) == pdTRUE) { /* 打印按键事件信息 */ DBG_log("[INFO] Key event: type=%d, key=%d, state=%d, iTime=%d, click_count=%d\n", event.eType, event.data.key.iKey, event.data.key.eState, event.data.key.iTime, event.data.key.iClickCount); } } }5、函数入口
/* ********************************************************************************************************* * 函 数 名: input_test_start * 功能说明: 启动按键扫描任务 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void input_test_start(void) { BaseType_t xReturn = pdPASS; /* 初始化按键测试 */ input_test_init(); /* 创建按键扫描任务 */ xReturn = xTaskCreate( (TaskFunction_t)key_scan_task, (const char *)"key_scan", (uint16_t)128, (void *)NULL, (UBaseType_t)1, (TaskHandle_t *)&xKeyScanTask ); if (xReturn != pdPASS) { DBG_log("[ERROR] Create key scan task failed\n"); return; } /* 创建按键测试任务 */ xReturn = xTaskCreate( (TaskFunction_t)input_test_task, (const char *)"input_test", (uint16_t)128, (void *)NULL, (UBaseType_t)2, (TaskHandle_t *)&xInputTestTask ); if (xReturn != pdPASS) { DBG_log("[ERROR] Create input test task failed\n"); return; } DBG_log("[INFO] Input test started\n"); }实验结果
可以看到,按键驱动可以正常驱动起来,日志比较正常。
总结
本文设计了一个按键扫描任务,将按键的键值放进队列中,在接收队列中拿出来,进行处理,算是一个freertos的队列实战。
三、按键输入事件管理系统设计与实现
设计思路如下,从输入事件得到数据,将数据放入对应的buffer进行管理,方便供上层代码进行调用管理。
1、系统架构
按键输入事件管理系统采用分层设计,主要包括以下几个层次:
底层驱动层:负责硬件按键的扫描和状态检测
设备抽象层:将硬件按键抽象为输入设备
输入系统层:管理所有输入设备,提供统一的事件获取接口
应用层:从输入系统获取事件并进行处理
2、核心组件
2.1 输入事件结构体
在input_system.h中定义了InputEvent结构体,用于表示各种输入事件:
typedef struct InputEvent { TIME_T time; /* 事件时间戳 */ INPUT_EVENT_TYPE eType; /* 事件类型 */ /* 通用事件数据 */ union { /* 按键事件数据 */ struct { int iKey; /* 按键代码 */ KEY_STATE eState; /* 按键状态 */ int iTime; /* 时间ms */ int iClickCount; /* 点击次数,用于多击判断 */ } key; /* 触摸事件数据 */ struct { int iX; int iY; int iPressure; } touch; /* 网络事件数据 */ struct { int iEventCode; char strData[INPUT_BUF_LEN]; } net; /* 标准输入事件数据 */ struct { char strInput[INPUT_BUF_LEN]; } stdio; } data; } InputEvent, *PInputEvent;2.2 输入设备结构体
在input_system.h中定义了InputDevice结构体,用于表示输入设备:
typedef struct InputDevice { char *name; /* 设备名称 */ int (*GetInputEvent)(PInputEvent ptInputEvent); /* 获取输入事件的函数 */ int (*DeviceInit)(void); /* 设备初始化函数 */ int (*DeviceExit)(void); /* 设备退出函数 */ struct InputDevice *pNext; /* 指向下一个设备的指针 */ } InputDevice, *PInputDevice;2.3 输入缓冲区
在input_system.c中实现了一个环形缓冲区,用于存储输入事件:
#define INPUT_BUFFER_SIZE 10 static InputEvent g_tInputBuffer[INPUT_BUFFER_SIZE]; static volatile int g_iInputBufferRead = 0; static volatile int g_iInputBufferWrite = 0;3. 调用流程
3.1 系统初始化
调用
AddInputDevices()注册所有输入设备调用
InitInputDevices()初始化所有输入设备创建
key_scan_task任务,每10ms进行一次按键扫描
3.2 按键扫描与事件处理
key_scan_task每10ms调用一次bsp_KeyScan10ms()进行按键扫描bsp_KeyScan10ms()检测按键状态并将按键事件放入按键FIFO调用
GetInputEvent()从所有输入设备中获取输入事件调用
InputBufferPut()将输入事件放入输入缓冲区
3.3 事件获取与处理
应用层调用
InputBufferGet()从输入缓冲区中获取输入事件根据事件类型和事件数据进行相应的处理
4. 代码实现
4.1 输入系统层实现
4.1.1 设备注册
/********************************************************************** * 函数名称: InputDeviceRegister * 功能描述: 注册一个输入设备(头插法) ***********************************************************************/ void InputDeviceRegister(PInputDevice ptInputDevice) { if (ptInputDevice == NULL) return; ptInputDevice->pNext = g_ptInputDevices; g_ptInputDevices = ptInputDevice; } /********************************************************************** * 函数名称: AddInputDevices * 功能描述: 注册所有需要的输入设备 ***********************************************************************/ void AddInputDevices(void) { /* 注册按键设备 */ extern void AddInputDeviceKey(void); AddInputDeviceKey(); /* 未来扩展:触摸、网络等设备 */ // extern void AddInputDeviceTouch(void); // AddInputDeviceTouch(); }4.1.2 设备初始化
/********************************************************************** * 函数名称: InitInputDevices * 功能描述: 初始化所有已注册的输入设备 ***********************************************************************/ void InitInputDevices(void) { PInputDevice pDev = g_ptInputDevices; while (pDev) { if (pDev->DeviceInit != NULL) { pDev->DeviceInit(); } pDev = pDev->pNext; } }4.1.3 事件获取
/********************************************************************** * 函数名称: GetInputEvent * 功能描述: 从所有输入设备中获取输入事件 * 输入参数: ptInputEvent - 输入事件指针 * 输出参数: 无 * 返 回 值: 0 - 成功,非0 - 失败 ***********************************************************************/ int GetInputEvent(PInputEvent ptInputEvent) { PInputDevice pDev = g_ptInputDevices; while (pDev) { if (pDev->GetInputEvent != NULL) { if (pDev->GetInputEvent(ptInputEvent) == 0) { return 0; } } pDev = pDev->pNext; } return -1; }4.1.4 输入缓冲区管理
/********************************************************************** * 函数名称: InputBufferPut * 功能描述: 向输入缓冲区中放入一个输入事件 * 输入参数: ptInputEvent - 输入事件指针 * 输出参数: 无 * 返 回 值: 0 - 成功,非0 - 失败 ***********************************************************************/ int InputBufferPut(PInputEvent ptInputEvent) { int iNextWrite = (g_iInputBufferWrite + 1) % INPUT_BUFFER_SIZE; if (iNextWrite == g_iInputBufferRead) { /* 缓冲区已满 */ return -1; } g_tInputBuffer[g_iInputBufferWrite] = *ptInputEvent; g_iInputBufferWrite = iNextWrite; return 0; } /********************************************************************** * 函数名称: InputBufferGet * 功能描述: 从输入缓冲区中获取一个输入事件 * 输入参数: ptInputEvent - 输入事件指针 * 输出参数: 无 * 返 回 值: 0 - 成功,非0 - 失败 ***********************************************************************/ int InputBufferGet(PInputEvent ptInputEvent) { if (g_iInputBufferRead == g_iInputBufferWrite) { /* 缓冲区为空 */ return -1; } *ptInputEvent = g_tInputBuffer[g_iInputBufferRead]; g_iInputBufferRead = (g_iInputBufferRead + 1) % INPUT_BUFFER_SIZE; return 0; } /********************************************************************** * 函数名称: InputBufferClear * 功能描述: 清空输入缓冲区 * 输入参数: 无 * 输出参数: 无 * 返 回 值: 无 ***********************************************************************/ void InputBufferClear(void) { g_iInputBufferRead = 0; g_iInputBufferWrite = 0; }4.2 设备抽象层实现
4.2.1 按键设备初始化
static int GPIOKeyInit(void) { KAL_GPIOKkeyInit(); /* 创建按键事件队列 */ xKeyQueue = xQueueCreate(10, sizeof(InputEvent)); if (xKeyQueue == NULL) { return -1; } /* 初始化按键驱动 */ bsp_InitKey(); return 0; }4.2.2 按键事件获取
static int GPIOKeyGetInputEvent(PInputEvent ptInputEvent) { uint8_t key_code; InputEvent event; /* 获取按键事件 */ key_code = bsp_GetKey(); if (key_code != KEY_NONE) { /* 填充InputEvent结构 */ event.time = xTaskGetTickCount(); event.eType = INPUT_EVENT_TYPE_KEY; /* 根据按键代码解析按键信息 */ switch (key_code) { case KEY_1_DOWN: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_PRESSED; event.data.key.iTime = event.time; event.data.key.iClickCount = 0; break; case KEY_1_UP: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_RELEASED; event.data.key.iTime = event.time; event.data.key.iClickCount = 1; break; case KEY_1_LONG_DOWN: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_LONG_PRESS; event.data.key.iTime = event.time; event.data.key.iClickCount = 0; break; case KEY_1_LONG_UP: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_LONG_RELEASED; event.data.key.iTime = event.time; event.data.key.iClickCount = 0; break; case KEY_1_AUTO_UP: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_REPEAT; event.data.key.iTime = event.time; event.data.key.iClickCount = 0; break; case KEY_1_DB_UP: event.data.key.iKey = 1; event.data.key.eState = KEY_STATE_DOUBLE_CLICK; event.data.key.iTime = event.time; event.data.key.iClickCount = 2; break; default: return -1; } /* 复制事件到输出参数 */ *ptInputEvent = event; return 0; } return -1; }4.2.3 按键设备注册
static InputDevice g_tKeyDevice = { "gpio_key", GPIOKeyGetInputEvent, GPIOKeyInit, NULL, }; void AddInputDeviceKey(void) { InputDeviceRegister(&g_tKeyDevice); }4.3 应用层实现
4.3.1 按键扫描任务
/********************************************************************** * 函数名称: key_scan_task * 功能描述: 按键扫描任务,每10ms调用一次bsp_KeyScan10ms * 输入参数: pvParameters - 任务参数 * 输出参数: 无 * 返 回 值: 无 ***********************************************************************/ void key_scan_task(void *pvParameters) { InputEvent event; while (1) { /* 每10ms调用一次按键扫描函数 */ bsp_KeyScan10ms(); /* 获取按键事件并放入输入缓冲区 */ if (GetInputEvent(&event) == 0) { InputBufferPut(&event); } /* 延时10ms */ vTaskDelay(10); } }4.3.2 输入测试任务
static void input_test_task(void *pvParameters) { InputEvent event; while (1) { /* 从队列中获取按键事件 */ if (xQueueReceive(xKeyQueue, &event, portMAX_DELAY) == pdTRUE) { /* 打印按键事件信息 */ DBG_log("[INFO] Key event: type=%d, key=%d, state=%d, iTime=%d, click_count=%d\n", event.eType, event.data.key.iKey, event.data.key.eState, event.data.key.iTime, event.data.key.iClickCount); } } }4.3.3 输入系统单元测试
/********************************************************************** * 函数名称: input_test * 功能描述: 输入系统单元测试函数 * 输入参数: 无 * 输出参数: 无 * 返 回 值: 无 ***********************************************************************/ void input_test(void) { InputEvent event; AddInputDevices(); InitInputDevices(); DBG_log("Input system unit test start...\n"); while (1) { if (GetInputEvent(&event) == 0) { /* 打印按键事件信息 */ DBG_log("[INFO] Key event: type=%d, key=%d, state=%d, iTime=%d, click_count=%d\n", event.eType, event.data.key.iKey, event.data.key.eState, event.data.key.iTime, event.data.key.iClickCount); /* 将事件放入输入缓冲区 */ if (InputBufferPut(&event) == 0) { DBG_log("[INFO] Event put into buffer success\n"); } else { DBG_log("[ERROR] Event put into buffer failed\n"); } } /* 尝试从输入缓冲区获取事件 */ if (InputBufferGet(&event) == 0) { DBG_log("[INFO] Event get from buffer: type=%d, key=%d, state=%d\n", event.eType, event.data.key.iKey, event.data.key.eState); } /* 延时10ms */ vTaskDelay(10); } }5. 使用方法
5.1 初始化输入系统
/* 注册并初始化输入设备 */ AddInputDevices(); InitInputDevices(); /* 创建按键扫描任务 */ xTaskCreate( key_scan_task, "key_scan", 128, NULL, 1, &xKeyScanTask );5.2 获取并处理输入事件
/* 从输入缓冲区中获取输入事件 */ InputEvent event; if (InputBufferGet(&event) == 0) { /* 根据事件类型进行处理 */ switch (event.eType) { case INPUT_EVENT_TYPE_KEY: /* 处理按键事件 */ printf("Key event: key=%d, state=%d\n", event.data.key.iKey, event.data.key.eState); break; /* 处理其他类型的事件... */ } }6. 扩展建议
添加更多输入设备:可以添加触摸屏幕、网络输入等其他类型的输入设备
优化输入缓冲区:可以根据实际需求调整输入缓冲区的大小
添加事件过滤:可以添加事件过滤机制,过滤掉不需要的事件
添加事件回调:可以添加事件回调机制,当有输入事件时自动调用回调函数
添加事件优先级:可以为不同类型的事件添加优先级,优先处理重要的事件
7. 总结
按键输入事件管理系统采用分层设计,通过输入设备抽象和输入缓冲区管理,提供了一个统一、高效的输入事件处理接口。系统支持多种按键状态的检测,包括按下、弹起、长按、双击等,可以满足各种应用场景的需求。
四、任务优先级优化划分
前言:在本次开发空气检测仪项目中,划分了一些任务,有些中任务优先级没处理好,故本篇文章进行记录与修复下。
1. 现象分析
uart_test任务未能运行,怀疑是优先级设置不当导致。 原系统所有任务(KeyScan, InputTest, UartTest, LogPrint)优先级均为2,且LedTest为1。 虽然理论上开启时间片轮转(Time Slicing)后同优先级任务应轮流执行,但若配置缺失或某些任务占用过多(尽管检查发现都有 Delay),可能导致响应迟缓或饿死风险。
2. 优先级重新规划
为了保证系统响应性和实时性,采用Rate Monotonic Scheduling (RMS)的思想,按照任务的重要性和实时性要求重新分配优先级。
FreeRTOS 优先级范围: 0 (Low) - 4 (High) [configMAX_PRIORITIES = 5]
任务名称 | 原优先级 | 新优先级 | 说明 |
|---|---|---|---|
| KeyScan | 2 | 4 (最高) | 硬件扫描任务,需要严格的 10ms 周期,抖动会影响按键体验。 |
| uart_test | 2 | 3 (高) | 通信任务,需要及时响应接收中断和处理数据,避免缓冲区溢出。 |
| input_test | 2 | 2 (中) | 业务逻辑/事件处理,对实时性要求稍低,只要能处理完 buffer 即可。 |
| LogPrint | 2 | 1 (低) | 后台日志打印,不应抢占业务资源。 |
| LedTest | 1 | 1 (低) | 后台状态指示,低优先级。 |
3. 修改记录
3.1 FreeRTOSConfig.h
显式定义
#define configUSE_TIME_SLICING 1,确保同优先级任务(如 LogPrint 和 LedTest)能公平共享 CPU。
3.2 User/main.c
将
vTaskLogPrint优先级从 2 降为1。
3.3 uart_test.c
将
uart_test_task优先级从 2 升为3。
3.4 gpio_key.c
将
vTaskKeyScan优先级从 2 升为4。
3.5 input_test.c
保持
input_test_task优先级为2(或明确注释为 Medium)。
4. 预期效果
KeyScan将获得最高优先权,确保按键扫描不丢失。
uart_test其次,保证串口通信流畅。
input_test在空闲时处理事件。
LogPrint和LedTest在系统空闲时运行,不会影响关键业务。
这种梯队设计能有效避免关键任务被饿死,同时保证系统的实时响应能力。