发帖
6 1 1

裸机时代的多任务幻觉:从定时器中断到简易调度框架

ddyty
金牌会员

8

主题

65

回帖

2592

积分

金牌会员

积分
2592
技术杂谈 709 6 2025-12-9 16:31:13
[i=s] 本帖最后由 ddyty 于 2025-12-9 16:47 编辑 [/i]

1 从裸机开发到操作系统

在学校里刚学习嵌入式软件开发的时候,是怎么写程序的,一个SystemInit(),里面塞满外设的初始化,然后配置中断,定时器等参数。在main函数中,写一个while(1),然后开干。

以大多数人都做过的智能小车举例,初始化串口(蓝牙或者wifi),初始化Oled,初始化按键,初始化红外传感器、光电传感器(巡黑线),初始化编码器。再另外新开一个5ms或者10ms的定时器中断,主函数中负责进行传感器的读取和业务交互,定时器中断中进行pid的计算。大多数都是这样设计的。

然后毕业以后,开始学习操作系统,例如经典的freeRtos,了解时间片调度,钩子函数,信号量,消息队列等等等等。

当然freertos等嵌入式实时操作系统的诞生是历史的必然,代表着工程师对实时调度的深切渴望,但是我希望能为大家简单介绍下,没有操作系统的年代,工程师们是怎么去实现一个简易调度框架的。

2 回到那个“没有 RTOS 可选”的年代

回到那个“没有 RTOS 可选”的年代,“老板要求同时做:每 10ms 采样 ADC、每 100ms 发串口、按键实时响应、LCD 动画流畅……但我的 main() 里全是 delay(),改一处全崩。”

那么如何去优化呢,工程师们开始实现简易的调度器,如何设计呢?在揭谜底之前,我们来讨论下大学毕业面试时几乎百分百会问到的问题,什么是回调函数(函数指针、指针函数有什么区别)

先来回答这个问题,什么是回调函数(函数指针与指针函数)

概念 本质 声明示例 典型用途
函数指针 指向函数的指针变量 int (*p)(int, int); 实现回调、构建任务调度表、状态机优化
指针函数 返回值为指针的普通函数 int* create_array(void); 返回动态数组、缓冲区地址、字符串等
回调函数 被其他代码“回头调用”的函数 void my_handler(void); 中断处理、事件通知、库的扩展接口(如 qsort

也就是说 函数指针 是指针变量,和int 指针 char指针一样是个指针,而指针函数就只是返回指针的普通函数而已。

事实上在知道这个函数指针之后,你的思维就会豁然开朗,简易时间片调度器的实现方式就是回调。

没搞明白的同学需要复习下C语言的精髓,指针。

3 如何设计简易时间片调度器

1、将原本你的业务逻辑,也就是任务,比如任务1 每100ms发送串口数据进行设备间保活,任务2 每1000ms闪烁一次流水灯,任务3 每5ms进行一次pid的计算,将这些业务逻辑都写成一个一个的函数,然后,赋值给函数指针数组。如callback = LedOn;

2、注册一个1ms一次的定时器中断,在这个定时器中断当中,1、自增一个时间之值,2、轮询函数指针数组,轮询这个函数上一次的执行时间与当前时间的差值,如果差值大于所设置的值,那么将函数指针调用,callback(); 如果差值不大于所设置的值,判断下一个。

方向有了,接下来就是编写了,如何去写一个函数指针?

 // 函数指针的原始语法:
// 返回类型 (*指针变量名)(参数列表);

// 示例 1:指向一个无参数、无返回值的函数
void simple_func(void) {
    // do nothing
}

void (*func_ptr_1)(void) = simple_func;  // 声明并初始化
// └─┬─┘ └──────┘ └─────┘
//   │     │        └─── 函数名(自动转换为地址,&simple_func 等价)
//   │     └──────────── 参数列表必须与目标函数一致
//   └────────────────── 指针变量名,* 必须与变量名括在一起!
//第一个void 说明函数指针指向的函数是void返回值的,第二个void是函数的入参列表 

// 示例 2:指向一个带参数、有返回值的函数
int add(int a, int b) {
    return a + b;
}

int (*func_ptr_2)(int, int) = add;  // 正确:* 与变量名绑定  *func_ptr_2是变量名  int说明指向的函数的返回值是int (int , int)说明指向的函数的入参是两个int
//注意:不能写成 int *func_ptr_2(int, int); ← 这是“指针函数”声明!

// 调用方式(两种等价):
int result1 = func_ptr_2(3, 4);        // 推荐:像普通函数一样调用
int result2 = (*func_ptr_2)(3, 4);     // 显式解引用(C 语言允许省略 *) &取地址 *解引用

// 示例 3:作为函数参数(常用于回调)//这也是精髓
void execute_operation(int (*op)(int, int), int x, int y) {
    int res = op(x, y);  // 通过函数指针调用
    printf("Result: %d\n", res);
}

// 调用
execute_operation(add, 5, 6);  // 传入函数名(即地址)

// 示例 4:数组形式 —— 构建函数跳转表(常用于状态机或命令解析)
int cmd_ping(void) { return 0; }
int cmd_reboot(void) { return 1; }

// 声明一个包含 2 个函数指针的数组
int (*cmd_table[])(void) = { cmd_ping, cmd_reboot };
// 等价于:
// typedef int (*cmd_handler_t)(void);
// cmd_handler_t cmd_table[] = { cmd_ping, cmd_reboot };

// 调用
int status = cmd_table[1]();  // 执行 cmd_reboot()

这个是函数指针原始语法,execute_operation(int (*op)(int, int), int x, int y)是不是很绕,所以我们可以通过typedef来给这个函数指针类型取一个好认识的别名。那问题可能又来了,typedef是什么,在学习结构体时,我们就理解了typedef stcuct1 stc;结构体1 的另外一个名字是 stc 我们就可以使用stc 来定义新的变量,比如 stc stc1 stc格式的结构体变量。或者typedef uint32_t u32。

typedef void (*func_ptr_1)(void) 如何解释呢,即 我定义了一个func_ptr_1 类型 这个类型的原型是一个函数指针,一个什么样子的函数指针,一个返回值为void,入参为void的函数指针,至于这个函数叫什么名字都可以,只要符合返回值是void 入参也是void 就可以被赋值给func_ptr_1类型的指针。

// 1. 先定义类型别名
typedef void (*event_handler_t)(int, void*);

// 2. 声明变量
event_handler_t my_handler = NULL;

// 3. 作为函数参数(可读性大幅提升)
void register_handler(event_handler_t handler, void *ctx) {
    my_handler = handler;
}

4 代码实现

main.c

#include "my_task_manager.h"
LogLevel global_log_level = LOG_LEVEL_DEBUG;

static volatile uint32_t g_sys_tick_ms = 0;

__INTERRUPT
__HIGH_CODE
void TMR2_IRQHandler(void)
{
    if(TMR2_GetITFlag(TMR0_3_IT_CYC_END))
    {
        g_sys_tick_ms++;                    // 系统时钟递增
        task_manager_tick();                // 更新任务管理器时钟
        TMR2_ClearITFlag(TMR0_3_IT_CYC_END); // 清除中断标志
    }
}


uint32_t get_sys_tick(void)
{
    return g_sys_tick_ms;
}

static void task_uart_protocol(void) 
{
    DEBUG_PRINT("UART protocol processing @ tick=%u\n", get_sys_tick());
    // 调用你原来的 protrcol() 函数
}

static void task_nfc_polling(void)
{
    DEBUG_PRINT("NFC polling @ tick=%u\n", get_sys_tick());
    // NFC处理逻辑
}

static void task_ui_update(void) 
{
    DEBUG_PRINT("UI update @ tick=%u\n", get_sys_tick());
    // UI显示更新
}

static void task_flash_maintenance(void)
{
    DEBUG_PRINT("Flash maintenance @ tick=%u\n", get_sys_tick());
    // Flash数据维护
}

int main(void) {
    // 硬件初始化
    RTC_InitTime(2025, 1, 1, 0, 0, 0);
    uart1_init(115200);
    TMR2_TimerInit(FREQ_SYS / 100);//1秒计数 /10 则是100ms计数
    TMR2_ITCfg(ENABLE, TMR0_3_IT_CYC_END);
    PFIC_EnableIRQ(TMR2_IRQn);
    DEBUG_PRINT("System starting...\n");
  
    // 初始化任务管理器
    task_manager_init();
  
    // 注册所有任务
    task_register("UART_Protocol", task_uart_protocol, 1000);    // 每10ms
    task_register("NFC_Polling", task_nfc_polling, 5000);        // 每50ms
    task_register("UI_Update", task_ui_update, 2000);           // 每200ms
    task_register("Flash_Maint", task_flash_maintenance, 10000); // 每1000ms
  
    // 如果需要,可以禁用某个任务
    // task_set_enable("Flash_Maint", false);
  
    DEBUG_PRINT("System started. Entering main loop.\n");
  
    while (1)
    {
  
        // 执行任务调度
        task_manager_run();
        // 短延时,让出CPU(如果没其他事做)
        DelayMs(1);
    }
}

头文件

// my_task_manager.h
#ifndef __MY_TASK_MANAGER_H
#define __MY_TASK_MANAGER_H

#include <sys.h>

// 任务结构体
typedef struct {
    const char *name;           // 任务名称(调试用)
    void (*handler)(void);      // 任务处理函数
    uint32_t interval_ms;       // 执行间隔(毫秒)
    uint32_t last_run_time;     // 上次执行时间
    bool enabled;               // 任务是否启用
    uint32_t run_count;         // 运行次数统计(调试)
} task_t;

// 最大任务数(根据需求调整)
#define MAX_TASKS 20

// 任务管理器状态
typedef struct {
    task_t tasks[MAX_TASKS];
    uint8_t task_count;
    uint32_t sys_tick;          // 系统时钟(毫秒)
} task_manager_t;

// 函数声明
void task_manager_init(void);
bool task_register(const char *name, void (*handler)(void), uint32_t interval_ms);
void task_set_enable(const char *name, bool enable);
void task_manager_tick(void);      // 在1ms中断或主循环中调用
void task_manager_run(void);       // 在主循环中调用
void task_manager_dump(void);      // 打印所有任务状态(调试用)

#endif

c文件

// my_task_manager.c
#include "my_task_manager.h"

static task_manager_t g_task_mgr = {0};

// 初始化任务管理器
void task_manager_init(void) {
    g_task_mgr.task_count = 0;
    g_task_mgr.sys_tick = 0;
  
    for (int i = 0; i < MAX_TASKS; i++) {
        g_task_mgr.tasks[i].handler = NULL;
        g_task_mgr.tasks[i].enabled = false;
        g_task_mgr.tasks[i].run_count = 0;
    }
  
    //DEBUG_PRINT("Task manager initialized\n");
}

// 注册任务
bool task_register(const char *name, void (*handler)(void), uint32_t interval_ms) {
    if (g_task_mgr.task_count >= MAX_TASKS || !handler) {
        //DEBUG_PRINT("Failed to register task: %s\n", name);
        return false;
    }
  
    task_t *task = &g_task_mgr.tasks[g_task_mgr.task_count];
    task->name = name;
    task->handler = handler;
    task->interval_ms = interval_ms;
    task->last_run_time = g_task_mgr.sys_tick;  // 从当前时间开始
    task->enabled = true;
    task->run_count = 0;
  
    g_task_mgr.task_count++;
  
    //DEBUG_PRINT("Task registered: %s (interval=%ums)\n", name, interval_ms);
    return true;
}

// 设置任务启用状态
void task_set_enable(const char *name, bool enable) {
    for (int i = 0; i < g_task_mgr.task_count; i++) {
        if (g_task_mgr.tasks[i].name && strcmp(g_task_mgr.tasks[i].name, name) == 0) {
            g_task_mgr.tasks[i].enabled = enable;
            //DEBUG_PRINT("Task %s %s\n", name, enable ? "enabled" : "disabled");
            return;
        }
    }
    //DEBUG_PRINT("Task not found: %s\n", name);
}

// 系统时钟更新(必须在1ms中断或主循环中频繁调用)
void task_manager_tick(void) {
    g_task_mgr.sys_tick++;
}

// 运行任务调度(在主循环中调用)
void task_manager_run(void)
{
    static uint32_t last_dump_time = 0;
    uint32_t now = g_task_mgr.sys_tick;
  
    for (int i = 0; i < g_task_mgr.task_count; i++) {
        task_t *task = &g_task_mgr.tasks[i];
    
        if (!task->enabled || !task->handler) {
            continue;
        }
    
        // 检查是否到达执行时间(处理32位回绕)
        uint32_t elapsed = (now >= task->last_run_time) ? 
                          (now - task->last_run_time) : 
                          (0xFFFFFFFF - task->last_run_time + now);
    
        if (elapsed >= task->interval_ms) {
            task->last_run_time = now;
            task->run_count++;
        
            // 执行任务
            task->handler();
        }
    }
  
    // 每5秒打印一次任务状态(调试用)
    if (now - last_dump_time >= 5000) {
        task_manager_dump();
        last_dump_time = now;
    }
}

// 打印任务状态(调试)
void task_manager_dump(void) {
   // DEBUG_PRINT("\n=== Task Manager Status (tick=%u) ===\n", g_task_mgr.sys_tick);
    for (int i = 0; i < g_task_mgr.task_count; i++) {
        task_t *task = &g_task_mgr.tasks[i];
       // DEBUG_PRINT("[%2d] %-16s: interval=%4ums, enabled=%d, runs=%u\n",
        //           i, task->name, task->interval_ms, task->enabled, task->run_count);
    }
   // DEBUG_PRINT("==================================\n");
}

5 总结

这个简易的任务调度器,自然是有很多局限的,比如不能自由的创建任务,删除任务(需要实现数组的删除与复制),没有内存管理,没有优先级的规划,每个任务都是一个优先级必须等待上一个函数执行完成才能执行下一个函数,不能精确保证任务执行时间,但是仍然不失为一个裸机开发的学习方向。在没有富裕falsh空间的情况下下,也能相对高效率的进行开发。

──── 1人觉得很赞 ────

使用道具 举报

2025-12-9 20:24:52
typedef void (*func_type)(int, int);
过来学习知识😁
2025-12-12 08:39:05
看不懂,好高级😍
2025-12-16 09:49:57
一直在跑裸机
2025-12-16 10:57:29
认真看完了,有收获
2026-1-6 14:35:09
好高级的样子
您需要登录后才可以回帖 立即登录
高级模式
返回
统计信息
  • 会员数: 30957 个
  • 话题数: 44900 篇