嵌入式系统分层开发:架构模式与工程实践(二)(创建任务篇(二))
文章目录
- 摘要
- 二、HDL层(Hardware Driver Layer,硬件驱动层)---操作设备的技工(读写寄存器)
- 实例三、任务创建蕴含的思想
- 创建任务框架搭建
- 任务状态枚举
- 初始化任务结构体
- 核心函数
- 实例四、任务创建蕴含的思想
摘要
继续分析
二、HDL层(Hardware Driver Layer,硬件驱动层)—操作设备的技工(读写寄存器)
实例三、任务创建蕴含的思想
创建任务框架搭建
任务状态枚举
enum
{TIMER_STOP = 0, /* 表示任务暂停 */TIMER_RUN, /* 表示任务运行 */TIMER_SLEEP, /* 表示任务休眠 */
};
一般任务状态分为三种:暂停、运行、休眠。
暂停和运行很容易可以理解,但是休眠应该如何理解?
休眠就是针对的低功耗状态:将任务置入低功耗状态,同时释放硬件资源(如关闭时钟、停用外设)。唤醒后需恢复上下文。
如:
1、等待外部事件唤醒(如中断触发)
2、电池供电设备(如传感器周期性休眠)
通过区分暂停(流程控制)和休眠(功耗优化),即满足实时性需求,又能实现高效能耗管理。
由于笔者还没有接触过低功耗项目实战,因此这个地方留下一坑,后续会增加文章分析低功耗架构。
初始化任务结构体
TaskCB.task[i].Func = Timer_DoNothing;
的设计意图是为未启用的定时器任务提供安全的默认回调函数,避免空指针风险,同时确保系统初始状态的稳定性和可预测性。
static void Timer_DoNothing(void *para) { para = para; }
是一个无实际操作的占位函数。其核心目的:
-
消除编译器警告:
para = para;
语句避免编译器因“未使用参数”产生警告(如GCC的-Wunused-parameter
)。 -
安全吞没参数:无论传入何种参数(包括
NULL
),均不执行操作,避免空指针解引用导致的崩溃。
在 Timer_Init()
中,通过循环将每个任务的回调函数设为 Timer_DoNothing
,主要解决以下问题:
-
避免野指针风险:
-
若未初始化
Func
字段,其值可能为随机地址(野指针)。若定时器意外启动并调用Func
,将导致硬件异常或系统崩溃。 -
赋值为
Timer_DoNothing
确保所有未启用的任务均有安全的默认回调,即使被误调用也不会引发致命错误。
-
-
统一初始状态管理:
-
所有任务初始状态为
TIMER_STOP
(停止),此时逻辑上无需执行操作。Timer_DoNothing
作为状态一致的体现,明确表达“当前无任务需执行”。 -
后续启用任务时(如调用
Timer_Start()
),只需覆盖Func
为目标函数,无需额外检查指针是否为空。
-
-
简化错误处理逻辑:
- 系统可能在未显式配置的任务槽中意外触发回调(如中断误触发)。
Timer_DoNothing
作为哨兵函数(Sentinel Function),直接吞没此类异常调用,无需在中断服务程序中添加判空保护。
- 系统可能在未显式配置的任务槽中意外触发回调(如中断误触发)。
-
防御性编程实践:
嵌入式系统对稳定性要求极高,尤其在中断上下文中。未初始化的函数指针是常见隐患,通过默认赋值空操作函数,显著提升容错能力。
-
资源受限场景优化:
-
相比动态分配函数指针或频繁判空,静态绑定
Timer_DoNothing
节省ROM/RAM开销(无需存储多个判断分支代码)。 -
例如,在低功耗模式下,暂停的任务仍可能被调度器扫描,但通过空函数避免实际操作,减少无谓功耗。
-
任务状态 | 回调函数行为 | 目的 |
---|---|---|
TIMER_STOP | Timer_DoNothing (空操作) | 初始状态/主动停止 |
TIMER_RUN | 用户自定义函数(如数据采集) | 执行实际逻辑 |
TIMER_SLEEP | 空操作或低功耗钩子函数 | 释放资源,进入休眠模式 |
- 当任务从
TIMER_STOP
切换到TIMER_RUN
时,只需更新Func
并重置状态,无需关心之前的函数指针内容。
在删除或暂停定时器任务时,也应将 Func
重置为 Timer_DoNothing
,形成闭环防护。接下来会有代码体现这一点。
核心函数
void TaskRun(void){static uint8_t id;static uint8_t interval;static uint8_t curTick;static uint8_t lastTick=0;curTick = TaskCB.timerTick;if(curTick == lastTick){return;}if(curTick > lastTick){interval = curTick - lastTick; }else{interval = (0xFF - lastTick) + curTick + 1; // 此处运算顺序如果没处理好会导致溢出 }lastTick = curTick;for(id=0;id<TIMER_ID_MAX;id++){if(TIMER_RUN != TaskCB.task[id].State) // 休眠和停止的任务都必须停止运行continue;TaskCB.task[id].count += interval;if(TaskCB.task[id].count >= TaskCB.task[id].TaskCycle){if(TaskCB.task[id].accurate)// 精准计时不能丢掉时间脉冲TaskCB.task[id].count -= TaskCB.task[id].TaskCycle;elseTaskCB.task[id].count = 0;if(TaskCB.task[id].loop > 1)TaskCB.task[id].loop--;else if(1 == TaskCB.task[id].loop)TaskCB.task[id].State = TIMER_STOP;if(NULL != TaskCB.task[id].Func)// 虽然不加指针也可以执行action,但是标准的语法是需要加指针的(*TaskCB.task[id].Func)(TaskCB.task[id].para);}}}
首先要看计数器是否增加,如果不增加说明出问题了,直接退出。
因为curTick = TaskCB.timerTick;
是从0~255周期性重复的,因此需要单独做处理。
两种情况,一种是还没有达到最大值,那就正常相减。
另外一种情况就是小于,这就说明是第一次溢出了,并且不存在多个循环以后的溢出,因为对于上一个状态,要么是大,要么是小,如果是多次循环就需要重复出现,而我们使用的是if语句,因此在第一次循环的时候已经处理了。
curTick = TaskCB.timerTick; // 当前滴答值
if(curTick == lastTick) return; // 无新滴答,直接返回// 处理计数器溢出(8位计数器)
if(curTick > lastTick) interval = curTick - lastTick;
else interval = (0xFF - lastTick) + curTick + 1;
lastTick = curTick; // 更新上次滴答
-
溢出计算需确保正确性(如
0xFF → 0
时,interval = (0xFF - lastTick) + curTick + 1
)。 -
若未正确处理溢出,会导致时间间隔错误(例如从255到0时误算为1ms而非实际2ms)。
从255递增到0需经历 256ms(255→256溢出→0,共2ms:255到256为1ms,256溢出后0为第2ms)。
从255到0的完整过程是 2个计数周期(255→256→0),但计数器值仅记录0~255,因此需通过外部记录溢出次数或扩大计数器位数解决。
状态 | 行为 | 应用场景 |
---|---|---|
TIMER_RUN | 累计时间,触发回调 | 周期性任务(如传感器采集) |
TIMER_STOP | 跳过执行,等待外部启动 | 手动暂停任务 |
TIMER_SLEEP | 未处理(需扩展) | 低功耗休眠 |
精准计时模式:
-
accurate=true
时保留多余时间(count -= TaskCycle
),避免长期累积误差,适用于对时间敏感的任务(如通信协议时序控制)。 -
非精准模式(
accurate=false
)直接清零,适用于容忍误差的场景(如LED闪烁)。
任务生命周期管理
-
有限次执行:通过
loop
参数控制任务执行次数,归零后自动进入TIMER_STOP
状态,避免资源泄露。 -
回调安全调用:检查
Func
非空后再执行回调,防止空指针崩溃。
阻塞风险:回调函数若执行时间过长,会阻塞其他任务调度(例:Func()
中执行耗时算法)。
针对阻塞的一些解决办法:
双缓冲技术(Double Buffering):
- 为高频数据采集设计两个缓冲区:回调填充前台缓冲区,任务处理后台缓冲区,通过指针交换避免拷贝开销。
任务队列(生产者-消费者模型):
- 回调函数仅将数据推入队列,由独立任务异步处理。
内存预分配:
- 在初始化阶段预分配任务所需内存(如队列、缓冲区),避免动态分配碎片化或延迟。
零拷贝传输:
- 通过传递数据指针而非拷贝数据,减少回调执行时间(需确保指针生命周期安全)。
策略 | 适用场景 | 优势 | 风险控制 |
---|---|---|---|
异步任务队列 | 数据流处理、高频事件 | 解耦回调与处理逻辑,避免阻塞 | 队列深度需合理设置,防溢出 |
双缓冲技术 | 高速数据采集(如ADC/DMA) | 零延迟切换,无拷贝开销 | 缓冲区大小需匹配数据速率 |
优先级分级 | 多任务混合场景 | 确保实时任务响应及时 | 需合理划分优先级层次 |
优先级继承互斥锁 | 共享资源访问 | 解决优先级反转问题 | 锁粒度需最小化 |
实例四、任务创建蕴含的思想
具体的实现细节已经在之前文章有过分析,
ARM单片机滴答定时器理解与应用(一)(详细解析)-CSDN博客
ARM单片机滴答定时器理解与应用(二)(详细解析)(完)-CSDN博客
需要注意的是任务创建里面的通用基准时间和产生时间(10ms、100ms)这两个函数都在FML层被调用,这个后续会详细解释,什么是FML层。
通过FML层得到的都是最基本的时间,也就是通过系统时钟或者是定时器可以获取到最小单元,拿到这些最小单元后,在其他地方进行相关处理,也就是说只需要通过这个接口函数记录已经过去的最小时基从而确定我们想要的时间。
如代码中,函数void Timer_InterruptRun(void)
被中断调用,而timerTick
一直会递增,而另外一个接口函数是Timer_TaskRun
被其他地方调用,在调用的时候会记录到最新的值,从而互不影响。
这里有一个问题,中断会增加timerTick
,而Timer_TaskRun
怎么实现实时读取timerTick
这是因为:
-
TaskCB.timerTick
是 全局变量(定义在结构体TaskCB
中)。 -
全局变量存储在 静态内存区(非栈或堆),生命周期与程序运行周期完全相同。
-
无论中断是否触发,该变量占用的内存永不释放,始终可读写。
无中断时:读取的是当前最新值(上次中断更新后的值)。
中断发生时:若读取与中断冲突可能引发数据不一致(需原子访问保护)。
个人理解中断发生的时候,这个对目前我开发的时间影响不是太大,因为时间是us级别,太短了。
至此实现了任务创建和时间的获取。
这种思想还是需要慢慢体会。
如果觉得我的内容对您有帮助,希望不要吝啬您的赞和关注,您的赞和关注是我更新优质内容的最大动力。
专栏介绍
《嵌入式通信协议解析专栏》
《PID算法专栏》
《C语言指针专栏》
《单片机嵌入式软件相关知识》
《FreeRTOS源码理解专栏》
《嵌入式软件分层架构的设计原理与实践验证》
文章源码获取方式:
如果您对本文的源码感兴趣,欢迎在评论区留下您的邮箱地址。我会在空闲时间整理相关代码,并通过邮件发送给您。由于个人时间有限,发送可能会有一定延迟,请您耐心等待。同时,建议您在评论时注明具体的需求或问题,以便我更好地为您提供针对性的帮助。
【版权声明】
本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议。这意味着您可以自由地共享(复制、分发)和改编(修改、转换)本文内容,但必须遵守以下条件:
署名:您必须注明原作者(即本文博主)的姓名,并提供指向原文的链接。
相同方式共享:如果您基于本文创作了新的内容,必须使用相同的 CC 4.0 BY-SA 协议进行发布。
感谢您的理解与支持!如果您有任何疑问或需要进一步协助,请随时在评论区留言,笔者一定知无不言,言无不尽。