制作一个可周期性调用任务的调度器,适用于单片机裸机系统,易于扩展,且调用简单
/*
* @brief 主程序入口
* @return none
*/
int main(void)
{
module_task_init(); /*模块初始化*/
while (1)
{
module_task_process(); /*任务轮询*/
}
}
主函数就是如此简单明了
任务要求:
创建三个任务:
- Task A 间隔 100 ms 执行一次;
- Task B 间隔 10 ms 执行一次;
- Task C 间隔 20 ms 执行一次;
系统设计思路
- 采用滴答定时器每毫秒实现一次中断,作为系统时基,实现间隔时间的计数;
- 在一片内存中顺序放置初始化函数,启动时顺序调用执行函数初始化;
- 在一片内存中顺序放置三个任务,主函数轮流检测三个函数是否延时到期,延时到则调用相应处理函数,否则不执行;
实现原理:
在一片内存中顺序存放三个任,在main的主函数中,用指针t轮询这三个任务,判断任务是否延时达到,如下图所示。
时基配置
将滴答定时器配置为每毫秒中断一次,中断函数每次系统时基自增1
static volatile unsigned int tick; // 系统滴答计时
/*
* @brief 增加系统节拍数(定时器中断中调用,1ms 1次)
*/
void systick_increase(unsigned int ms)
{
tick += ms;
}
void SysTick_Handler(void)
{
systick_increase(SYS_TICK_INTERVAL);
}
定义任务控制块
/*任务处理项*/
typedef struct
{
const char *name; // 模块名称
void (*handle)(void); // 初始化接口
unsigned int interval; // 轮询间隔
unsigned int *timer; // 指向定时器指针
} task_item_t;
核心代码详解
采用__attribute__((section(x)))属性将task_item_t结构体指定在内存段
核心代码
/*匿名类型定义 -----------------------------------------------------------*/
#define ANONY_CONN(type, var, line) type var##line
#define ANONY_DEF(type,prefix,line) ANONY_CONN(type, prefix, line)
#define ANONY_TYPE(type,prefix) ANONY_DEF(type, prefix, __LINE__)
#define SECTION(x) __attribute__((section(x)))
#define USED __attribute__((used))
/*
* @brief 任务注册
* @param[in] name - 任务名称
* @param[in] handle - 初始化处理(void func(void){...})
* @param[in] interval- 轮询间隔(ms)
*/
#define task_register(name, handle, interval) \
static unsigned int __task_timer_##handle; \
USED ANONY_TYPE(const task_item_t, task_item_##handle) \
SECTION("task.item.1") = \
{name, handle, interval, &__task_timer_##handle}
/*
* @brief 空处理,用于定位段入口
*/
static void nop_process(void) {}
// 第一个任务项
const task_item_t task_tbl_start SECTION("task.item.0") = {
"", nop_process};
// 最后个任务项
const task_item_t task_tbl_end SECTION("task.item.2") = {
"", nop_process};
第一部分:匿名类型构造辅助宏
#define ANONY_CONN(type, var, line) type var##line
-
功能:将
var与line拼接,生成唯一的变量名。 -
示例:
ANONY_CONN(int, myvar, 12) → int myvar12; -
使用
##是 C 语言的预处理符号拼接操作符。
#define ANONY_DEF(type,prefix,line) ANONY_CONN(type, prefix, line)
-
功能:再次封装
ANONY_CONN,以便中间多一步控制。 -
示例:
ANONY_DEF(int, myvar, 12) → int myvar12;
#define ANONY_TYPE(type,prefix) ANONY_DEF(type, prefix, __LINE__)
- 功能:生成一个以
prefix开头,带当前代码行号的变量名。
- LINE:是 C 语言预定义宏,表示当前代码的行号,用于生成唯一变量。
-
示例(假设在第 88 行):
ANONY_TYPE(const init_item_t, init_tbl_) → const init_item_t init_tbl_88;
第二部分: section 属性宏封装
#define SECTION(x) __attribute__((section(x)))
-
功能:将变量/函数/结构体分配到用户自定义的段中。
-
作用:可实现按功能分类放入不同的内存区域,例如
.init.item.level0。 -
示例:
int x SECTION(".mydata") = 5; // 表示变量 x 放到段 .mydata 中
第三部分:任务函数注册核心宏
#define task_register(name, handle, interval) \
static unsigned int __task_timer_##handle; \
USED ANONY_TYPE(const task_item_t, task_item_##handle) \
SECTION("task.item.1") = \
{name, handle, interval, &__task_timer_##handle}
这一行是整个系统的重点:
第 2 行:
static unsigned int __task_timer_##handle;
功能说明:
定义一个静态的局部任务句柄变量,用于记录该任务的基本信息。
## 是什么?
这是 Token-Pasting Operator(拼接符),在预处理时把 __task_timer_ 和传入的 handle 拼接成一个变量名,比如:
task_register("LED", led_task, 100)
会展开为:
static unsigned int __task_timer_led_task;
第 3 行:
USED ANONY_TYPE(const task_item_t, task_item_##handle)
功能说明:
这是 组合宏,等价于声明一个常量结构体(任务项):
__attribute__((used)) static const task_item_t task_item_led_task
USED是宏,等价于__attribute__((used)),防止链接器优化掉未显式引用的变量。
第 4 行:
SECTION("task.item.1") =
功能说明:
这个结构体变量将被放入名为 .task.item.1 的段中,用于 链接期间的段内排序与查找。这对任务调度器的启动初始化和遍历是必要的。
SECTION(x)是宏,等价于__attribute__((section(x)))。
示例:
#define SECTION(x) __attribute__((section(x)))
第 5 行:
{name, handle, interval, &__task_timer_##handle}
功能说明:
初始化结构体 task_item_t,如下:
{
.name = "LED", // 任务名
.handle = led_task, // 执行函数
.interval = 100, // 每 100ms 调用一次
.timer = &__task_timer_led_task // 指向对应的定时器变量
}
关键技术点总结:
| 特性 | 功能说明 |
|---|---|
__attribute__((section)) | 将变量放入特定段,在链接时可排序、查找 |
__attribute__((used)) | 防止链接器优化掉变量 |
## 拼接符 | 宏拼接符号生成变量名 |
| 静态变量 | 每个任务一个独立任务句柄,不污染全局命名空间 |
| 指针传递 timer | 支持 module_task_process() 时动态更新时间戳 |
| 自定义段 | 构造任务表,在启动时遍历 task_tbl_start+1 ~ task_tbl_end 实现注册任务 |
示例展开代码为:
static unsigned int __task_timer_led_task;
__attribute__((used))
static const task_item_t task_item_led_task
__attribute__((section("task.item.1"))) =
{
"LED", led_task, 100, &__task_timer_led_task
};
定义任务的起始位置和结尾的位置
// 第一个任务项
const task_item_t task_tbl_start SECTION("task.item.0") = {
"", nop_process};
// 最后个任务项
const task_item_t task_tbl_end SECTION("task.item.2") = {
"", nop_process};
/*
* @brief 任务轮询处理
* @param[in] none
* @return none
*/
void module_task_process(void)
{
const task_item_t *t;
for (t = &task_tbl_start + 1; t < &task_tbl_end; t++)
{
if ((uint32_t)(get_tick() - *t->timer) >= t->interval)
{
*t->timer = get_tick();
t->handle();
}
}
}
这一句代码:
if ((uint32_t)(get_tick() - *t->timer) >= t->interval)
是判断定时器是否超时的经典写法
判断逻辑
逻辑过程是判断:从上次调用 t->handle() 到现在,是否已经过了 interval 时间。
get_tick() 是什么?
它是系统毫秒定时器,比如从开机起经过的毫秒数:
/*
* @brief 获取系统滴答时钟值(通常单位是1ms)
*/
unsigned int get_tick(void)
{
return tick; // 当前毫秒数
}
tick 可能会溢出怎么办?
get_tick() 返回的是 uint32_t,最大值 0xFFFFFFFF,约等于49.7天(如果以1ms为单位)。超过这个值后就会从0重新开始计数,例如:
*t->timer = 0xFFFFFFF0;
get_tick() = 0x00000010;
直接减法:
get_tick() - *t->timer = 0x00000010 - 0xFFFFFFF0 = 0x00000020
这是因为在无符号整型下,C标准规定:
当两个
unsigned相减时,结果是自动模2^32的(对于uint32_t)。
所以,即使溢出也能得到正确的差值。
四、为何是安全的?
这句表达式:
(uint32_t)(get_tick() - *t->timer) >= t->interval
在溢出后,依然可以得到从 *t->timer 到现在经过的真实时间差(mod 2^32) 。
前提条件: interval 不超过 2^31 - 1(即最多支持 24.8 天时间间隔),否则会引起误判。
示意图
时间轴: ----------->
tick: 0xFFFFFFF0 0x00000000 0x00000010
↑ ↑
*t->timer get_tick()
Diff = 0x00000010 - 0xFFFFFFF0 = 0x20
自动初始化部分代码
核心机制总览
#define system_init(name, func) __module_initialize(name, func, "1")
#define driver_init(name, func) __module_initialize(name, func, "2")
#define module_init(name, func) __module_initialize(name, func, "3")
这些宏本质上都是通过:
#define __module_initialize(name, func, level) \
USED ANONY_TYPE(const init_item_t, init_tbl_##func) \
SECTION("init.item." level) = {name, func}
最终定义成一个 const init_item_t 结构体,存入 init.item.level 段中。
SECTION("init.item." level) 是如何工作的?
"init.item." level 是 C 语言中字符串字面量的拼接:
SECTION("init.item." level)
例如 level = "1",拼接后变成:
__attribute__((section("init.item.1")))
编译器把这个变量放进 .init.item.1 段中。
全流程结构理解
结构体定义:
typedef struct {
const char *name;
void (*func)(void);
} init_item_t;
示例展开后的样子:
const init_item_t init_tbl_start SECTION("init.item.0") = {"", nop_process};
const init_item_t init_1 SECTION("init.item.1") = {"sys", sys_init};
const init_item_t init_2 SECTION("init.item.2") = {"drv", drv_init};
const init_item_t init_3 SECTION("init.item.3") = {"mod", mod_init};
const init_item_t init_tbl_end SECTION("init.item.4") = {"", nop_process};
为何能确保这些段位于中间?
这是 链接器(Linker)保证的。
在 Scatter 文件或 LD 链接脚本中,段的顺序通常是这样写的:
SECTIONS {
. = ALIGN(4);
__init_start = .;
KEEP(*(.init.item.0)) // 起始点
KEEP(*(.init.item.1)) // system_init
KEEP(*(.init.item.2)) // driver_init
KEEP(*(.init.item.3)) // module_init
KEEP(*(.init.item.4)) // 终止点
__init_end = .;
}
这样做就确保:
- 所有初始化项是连续排列的。
.init.item.1到.init.item.3的内容就在 start 和 end 之间。- 如果链接脚本中没有明确指定,链接器默认按照 section 段名的 ASCII 顺序对这些变量排序,比如:
.init.item.0
.init.item.1
.init.item.2
.init.item.3
.init.item.4
自然成为一个范围:中间的所有初始化项都在这个范围内。
运行时处理逻辑(遍历调用)
在系统启动阶段,自动初始化模块按顺序 system_init > driver_init > module_init逐个初始化
/*
* @brief 模块初始处理
* 初始化模块优化级 system_init > driver_init > module_init
* @param[in] none
* @return none
*/
void module_task_init(void)
{
const init_item_t *it = &init_tbl_start;
while (it < &init_tbl_end)
{
it++->init();
}
}
本文示例工程目录说明
.vscode // Visual Studio Code 工具目录
P1_PRJ // Keil ARM 工程目录
P2_APP // 应用层目录
P3_BSP // 驱动层目录,主要与硬件相关
P4_CPN // 第三方组件
P5_Libraries // F103标准库文件
|-- framework // 裸机框架文件
P6_DOC // 说明文件,主要是规格书
pics // ReadMe.md 的图片
keilkill.bat // 清空编译过程产生的文件的windows脚本
ReadMe.md // 关于工程的说明文件
STM32F1标准库下载地址 https://www.st.com.cn/zh/embedded-software/stm32-standard-peripheral-libraries.html
img
需要该工程源码的,关注 + 留言“ 裸机框架 ”
申明:原作者源码地址:github.com/morro-tech/…; 修改后源码主要是重新做了目录划分。