摘要
一、队列结构体数组
typedef struct
{
u8 type;
u8 para;
}SCENE_EVENT;
typedef struct
{
u8 curScene; //当前状态
SCENE_EVENT queue[SCENE_QUEUE_LENGTH];//头文件中定义"api_scene.h"
u8 read;
u8 write;
}SCENE_CB;
SCENE_EVENT queue[SCENE_QUEUE_LENGTH];
表示的是每一个数组中的元素都是一个SCENE_EVENT
结构体。
typedef struct
{
u8 type;
u8 para;
}SCENE_EVENT;
event->type
:是事件的“标签”,决定了 如何响应。事件类型标识
作用:
type
字段用于区分不同种类的事件(例如键盘输入、鼠标点击、定时器触发等)。它是一个整型或枚举值,每个值对应一种特定的事件类型。示例:
若
type = 1
表示键盘事件;type = 2
表示鼠标事件;type = 3
表示自定义逻辑事件(如场景切换)。
通过类型标识,事件处理函数无需遍历所有可能的参数,可直接根据
type
快速分派到对应的处理逻辑。
event->para
:是事件的“内容”,提供了 响应的具体依据。
作用:
para
字段存储事件的具体参数,通常是一个通用指针(void*
) 或联合体(union),以便灵活承载不同类型的数据。参数内容:
键盘事件:存储按下的键值(如
SDLK_LEFT
表示左方向键);鼠标事件:保存坐标
(x, y)
或按键状态;自定义事件:传递结构体指针(如场景ID、对象句柄等)。
设计目的:解耦事件触发与处理逻辑,使事件队列能统一存储各类事件,处理时再按需解析参数。
统一事件队列:通过 type
+ para
的设计,不同来源的事件(I/O、定时器、信号)可被统一管理。
异步处理:事件生产者(如硬件中断)将事件推入队列,消费者(如主循环)通过 SCENE_PullEvent
拉取事件并解析,实现异步解耦。
扩展性:新增事件类型只需扩展 type
枚举和 para
的解析逻辑,无需修改队列机制。
二、函数指针、结构体
typedef struct
{
u8 sceneId;
void (*action)(u8,u8);
}SCENE_ACTION;
_const SCENE_ACTION sceneAction[SCENE_ID_MAX]=
{
{SCENE_ID_POWER_ON, BOARD_PowerOn},
{SCENE_ID_TURN_OFF, BOARD_TurnOff},
{SCENE_ID_TURN_ON, BOARD_TurnOn},
{SCENE_ID_ALARM, BOARD_Alarm},
{SCENE_ID_TEST , BOARD_Test},
{SCENE_ID_SHOW_MODE, BOARD_ShowMode},
};
(*sceneAction[sceneCB.curScene].action)(ev.type, ev.para);
这个里面涉及到一个问题就是结构体中的元素和我涉及的数组两者元素的对其关系。
u8 sceneId;
对应的是 SCENE_ID_POWER_ON
void (*action)(u8,u8);
对应的是 BOARD_PowerOn
结构体定义 (SCENE_ACTION
)
这个结构体有两个成员:
u8 sceneId
: 这是一个标识符(例如场景ID、事件类型、命令码)。它用来唯一标识应该执行哪个操作。void (*action)(u8,u8)
: 这是一个函数指针,指向一个接受两个u8
类型参数并返回void
的函数。它定义了如何处理这个标识符对应的任务。
结构体数组初始化 (sceneAction
)
这里使用的是 C99 的 指定初始化器 。每个花括号
{}
都对SCENE_ACTION
结构体的一个元素进行初始化。{SCENE_ID_POWER_ON, BOARD_PowerOn}
: 这行代码的意思是:SCENE_ID_POWER_ON
这个场景 ID,对应的是BOARD_PowerOn
这个函数。这建立了一个清晰的 映射关系。
函数指针赋值
BOARD_PowerOn
等函数名,本身就是函数地址(指针)。在初始化数组中,直接将函数名(地址)赋值给结构体的action
成员,不需要括号()
。加上括号就变成了函数调用。这是一种约定俗成的语法,是C语言标准所允许的,并不是什么特殊的“默认”规定。编译器看到这种语法就知道你要把函数的地址赋给指针。
在C语言中,结构体初始化列表的赋值顺序严格遵循成员的定义顺序。
这个一定要注意的。并且sceneId
和 action
就相当于是两个成员,如果需要使用,就需要引用,至于action
它就是一个函数指针,用来定义的。所以说,只学习理论还是不够,需要结合实际项目进行考虑。
但是使用C99的指定初始化器(Designated Initializer),显式关联成员与值,彻底消除顺序依赖:
_const SCENE_ACTION sceneAction[] = {
[SCENE_ID_POWER_ON] = { .sceneId = SCENE_ID_POWER_ON, .action = BOARD_PowerOn },
[SCENE_ID_TURN_ON] = { .sceneId = SCENE_ID_TURN_ON, .action = BOARD_TurnOn },
};
不依赖成员声明顺序。
可跳过部分成员初始化。
代码意图清晰直观。
const 关键字:你代码中的 _const
(可能定义为 const
)表明这个数组是只读的,通常会被存储在程序的只读数据段(如Flash),这在嵌入式系统中很常见,可以节省RAM并防止意外修改。
三、左移、后移记忆技巧
#define MASK(i) (1UL<<(i))
当 i=5
时,MASK(5)
表示将无符号长整型(unsigned long
)数值 1
向左移动5位。
1UL
:一个无符号长整型(Unsigned Long)的数值1,确保了操作在无符号环境下进行,避免符号位带来的意外问题。
<<
:位左移运算符。x << y
表示将 x
的二进制位向左移动 y
位,右侧空位补0。
把 1UL << i
想象成 “生成一个只有第 i
位是1,其他位都是0的二进制数”。这个生成的二进制数就是一个“掩码”(Mask),就像一个小印章,可以精准地盖在特定的位置上。
#include <stdio.h>
#define MASK(i) (1UL << (i))
int main() {
int i = 5;
unsigned long mask_value = MASK(i);
printf("MASK(%d) = 0x%lx\n", i, mask_value); // 输出: MASK(5) = 0x20
printf("Binary (simplified): 100000 (base2)\n");
return 0;
}
原本是 0000 0001 那么对于这个数左移五位,就相当于是将1从右向左开始查五位,
这样就是0010 0000 直接在前位置进行向左移动对应的位数就可以了。
当然对于这种还可以使用其他的场景:
设置特定位:REG |= MASK(5);
// 将某个寄存器(REG)的第5位置1。 (本文使用的)
清除特定位:REG &= ~MASK(5);
// 将第5位清零。
检查特定位:if (REG & MASK(5))
// 检查第5位是否为1。
宏定义中参数 i
两侧的括号是为了防止如果 i
是一个表达式时,因运算符优先级问题而导致的意外错误。
使用 UL
(无符号长整型)有助于避免在移位操作时可能发生的溢出或未定义行为,尤其是在对 int
类型进行高位移位时。如果你在处理64位数据,有时可能会看到使用 ULL
(Unsigned Long Long)。
四、任务标识符注意
TIMER_CreateTask(TIMER_ID_DELAY, 5000, Down_BoxFunc_Dist, 0, false, false);
TIMER_CreateTask(TIMER_ID_DELAY, 5000, Down_BoxFunc_Bake, 0, false, false);
关键点:任务ID (TIMER_ID_DELAY
) 应该是每个定时任务的唯一标识符。这就像每个人的身份证号码,应该是独一无二的。系统依靠这个ID来区分、查找和操作不同的任务。
当前问题:当你为两个不同的任务使用了相同的ID (TIMER_ID_DELAY
),系统就无法通过这个ID唯一确定你究竟想操作哪个任务。
删除操作:当你请求删除 TIMER_ID_DELAY
时,定时器模块的内部逻辑可能会:
删除所有关联此ID的任务(因为这被认为是同一个任务的多个实例或一次清理所有相同ID的任务)。
仅删除它找到的第一个关联此ID的任务,这会导致行为不确定和难以调试的问题。
由于内部数据结构(如最小堆)可能不支持高效的直接任意删除,它可能采用“惰性删除”策略(即在任务触发时再检查标志位并跳过执行,而非立即从堆中移除),但共享ID会使这种策略也难以正确工作。
特性 | 可能导致的问题 (使用相同ID) | 推荐做法 (使用唯一ID) |
---|---|---|
任务标识 | 多个任务共享同一个 TIMER_ID_DELAY |
每个任务拥有自己唯一的标识符 |
删除操作 | 删除 TIMER_ID_DELAY 时,系统无法区分要删除哪一个任务,通常会导致所有关联此ID的任务被移除,或者操作失败。 |
通过唯一ID可以精确定位并删除特定任务,不会影响其他任务。 |
任务独立性 | 任务之间缺乏独立性,一个任务的管理操作(如删除)会干扰其他任务。 | 每个任务都是独立的,管理操作互不影响。 |
代码可维护性 | 较差,难以调试和追踪具体是哪个任务出了问题或需要管理。 | 较好,通过唯一ID可以清晰管理每个任务的生命周期。 |
底层机制参考 | 类似Java Timer 的 cancel() 方法若不加区分地操作,会影响任务。 |
类似最小堆定时器管理中的惰性删除策略,通过唯一标识和字典映射来精确控制 |
所以这种操作是错误的,一定要避免这种多个任务使用一个标识符。
如果觉得我的内容对您有帮助,希望不要吝啬您的赞和关注,您的赞和关注是我更新优质内容的最大动力。
专栏介绍
《嵌入式通信协议解析专栏》
《PID算法专栏》
《C语言指针专栏》
《单片机嵌入式软件相关知识》
《FreeRTOS源码理解专栏》
《嵌入式软件分层架构的设计原理与实践验证》
文章源码获取方式:
如果您对本文的源码感兴趣,欢迎在评论区留下您的邮箱地址。我会在空闲时间整理相关代码,并通过邮件发送给您。由于个人时间有限,发送可能会有一定延迟,请您耐心等待。同时,建议您在评论时注明具体的需求或问题,以便我更好地为您提供针对性的帮助。
【版权声明】
本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议。这意味着您可以自由地共享(复制、分发)和改编(修改、转换)本文内容,但必须遵守以下条件:
署名:您必须注明原作者(即本文博主)的姓名,并提供指向原文的链接。
相同方式共享:如果您基于本文创作了新的内容,必须使用相同的 CC 4.0 BY-SA 协议进行发布。
感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言,笔者一定知无不言,言无不尽。