单片机裸机框架:任务调度器

120 阅读8分钟

制作一个可周期性调用任务的调度器,适用于单片机裸机系统,易于扩展,且调用简单


/*
 * @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 执行一次;

系统设计思路

  1. 采用滴答定时器每毫秒实现一次中断,作为系统时基,实现间隔时间的计数;
  2. 在一片内存中顺序放置初始化函数,启动时顺序调用执行函数初始化;
  3. 在一片内存中顺序放置三个任务,主函数轮流检测三个函数是否延时到期,延时到则调用相应处理函数,否则不执行;

实现原理:

在一片内存中顺序存放三个任,在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
  • 功能:将 varline 拼接,生成唯一的变量名。
  • 示例
    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 = .;
}

这样做就确保:

  1. 所有初始化项是连续排列的。
  2. .init.item.1.init.item.3 的内容就在 start 和 end 之间
  3. 如果链接脚本中没有明确指定,链接器默认按照 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

img

需要该工程源码的,关注 + 留言“ 裸机框架 ”

申明:原作者源码地址:github.com/morro-tech/…; 修改后源码主要是重新做了目录划分。