基于 SysTick 定时器实现任务轮询调度器
文章目录
- 前言
- 一、SysTick 定时器介绍
- 二、SysTick 驱动设计
- 1. 初始化方法
- 2. SysTick 中断函数
- 3. 时间类 API
- 三、任务调度器设计
- 1. 任务结构体
- 2. 任务初始化
- 3. 主调度器
- 4. 调度器更新
- 四、任务函数实现
- 五、总结
- 1. 优缺点分析
- 2. 扩展建议
前言
在嵌入式系统中,对于资源受限、实时性要求较强的小型项目,使用一个轻量级的 轮询调度器(Polling Scheduler) 往往是比使用完整 RTOS 更合适的选择。本文将介绍如何基于 SysTick 定时器,构建一个简单、可配置、易于维护的任务轮询调度框架。
轮询调度器是一种按照固定频率循环调用多个任务的调度方式。与传统的 RTOS 使用任务优先级和时间片管理不同,轮询调度器结构简单,没有上下文切换开销,适用于嵌入式裸机系统。
SysTick 是 Cortex-M 内核自带的一个 24-bit 的定时器,非常适合实现毫秒级的系统节拍(系统心跳),可以定期触发中断来增加系统“时钟节拍数”。
一、SysTick 定时器介绍
SysTick 是 Cortex-M3/M4/M7 系列核内部自带的一个定时器,有如下特点:
-
可以设置一个重装值,到值时触发一次中断
-
可以选择不同的时钟源(如 HCLK/无分频或 HCLK/8)
-
通常用于 RTOS 的时钟引擎,也可用于实时延时
在本组件中,我们把 SysTick 设置为 10kHz 高精度定时器,即 0.1ms 一次中断
二、SysTick 驱动设计
1. 初始化方法
void drv_systick_init(void)
{
crm_clocks_freq_type clocks_struct;
systick_clock_source_config(SYSTICK_CLOCK_SOURCE_AHBCLK_NODIV); // 使用 AHB 时钟
crm_clocks_freq_get(&clocks_struct);
uint32_t tick_cnt = clocks_struct.ahb_freq / SYSTICK_FREQUENCE;
systick_interrupt_config(tick_cnt);
}
根据当前 AHB 主时钟,计算定时轮被动进制值,定时频率为 SYSTICK_FREQUENCE = 10000 即 10kHz。
2. SysTick 中断函数
void SysTick_Handler(void)
{
++systick_count;
}
每 0.1ms 一次,代表一个精度较高的 “系统时钟基准” 增加。
3. 时间类 API
uint32_t get_systick_count(void)
{
return systick_count;
}
uint32_t get_systick_ms(void)
{
return (uint32_t)((float)systick_count / ((float)SYSTICK_FREQUENCE * 0.001f));
}
uint32_t get_systick_s(void)
{
return (uint32_t)((float)systick_count / (float)SYSTICK_FREQUENCE);
}
void delay_ms(uint32_t ms)
{
uint32_t start_cnt = get_systick_count();
while ( (get_systick_count() - start_cnt) < ms * SYSTICK_FREQUENCE * 0.001f) {
}
}
void delay_s(uint32_t s)
{
uint32_t start_cnt = get_systick_count();
while ( (get_systick_count() - start_cnt) < s * SYSTICK_FREQUENCE) {
}
}
-
get_systick_count()
:返回自己滴答数 -
get_systick_ms()
:系统运行时间,单位ms -
delay_ms()
:延时操作,单位ms
三、任务调度器设计
核心思想: 每个任务记录上次执行时间 last_tick,按照预设频率 frequence 判断是否超时,如果是,就执行任务函数。
1. 任务结构体
struct loop_task_info {
char *name;
void (*function)(void); // 任务函数指针
uint32_t frequence; // 任务执行频率(Hz)
uint32_t tick; // 已执行次数
uint32_t last_tick; // 上次执行时的系统节拍数
};
所有任务被组织在一个静态数组 task_info[] 中。
2. 任务初始化
INIT_LOOP_TAST(task_info[LOOP_TASK_A], "Task A", task_a, 10000, 0, 0); // 10000Hz
INIT_LOOP_TAST(task_info[LOOP_TASK_B], "Task B", task_b, 500, 0, 0); // 500Hz
3. 主调度器
主循环不断轮询所有任务:
void loop_task_ontick(void)
{
while (1) {
for (i = 0; i < LOOP_TASK_MAX; ++i)
loop_task_updata(&task_info[i]);
}
}
4. 调度器更新
static void loop_task_updata(struct loop_task_info *loop_task)
{
uint32_t tick_interval = get_systick_count() - loop_task->last_tick;
if (tick_interval >= (get_systick_frequence() / loop_task->frequence)) {
loop_task->last_tick = get_systick_count();
++loop_task->tick;
if(loop_task->function != NULL)
loop_task->function();
}
}
四、任务函数实现
任务函数内部可以再细分子任务,如在 Task A 中实现 10KHz、1KHz、1Hz 的子任务:
void task_a(void)
{
if (get_loop_task_tick(LOOP_TASK_A) % (get_loop_task_frequence(LOOP_TASK_A) / 10000) == 0) {
// 10000Hz 执行
}
if (get_loop_task_tick(LOOP_TASK_A) % (get_loop_task_frequence(LOOP_TASK_A) / 1000) == 0) {
// 1000Hz 执行
}
if (get_loop_task_tick(LOOP_TASK_A) % (get_loop_task_frequence(LOOP_TASK_A) / 1) == 0) {
// 1Hz 执行
}
}
通过tick计数除以频率的方式,在一个任务函数中实现多频率分支处理,非常适合多粒度控制。
五、总结
1. 优缺点分析
优点:
-
结构简单:无需操作系统,适合裸机项目。
-
执行频率可控:每个任务可单独配置运行频率。
-
任务切换无上下文开销:极大节省系统资源。
缺点:
-
不适合实时性要求极高的任务:任务间是串行执行,容易被阻塞。
-
不支持任务优先级:所有任务平等轮询。
-
没有“抢占机制”:任务函数内部不能阻塞太久,否则影响整体调度精度。
2. 扩展建议
-
支持任务优先级(按优先级或 deadline 排序执行)
-
增加任务 Watchdog 功能,防止任务异常长时间卡死
-
动态添加、删除任务支持
-
基于时间轮的调度器优化版