单片机超轻量级多任务操作系统实战指南

一、 引言:为什么需要多任务?

在8位单片机(如ATmega328P、STC89C52等)上,资源极其有限:

  • RAM:通常只有几十到几百字节

  • Flash:几KB到几十KB

  • 无MMU(内存管理单元)

  • 单核CPU

在这样的环境下,运行像Linux或FreeRTOS(虽然FreeRTOS有裁剪版)这样的完整OS是不现实的。但我们仍然希望实现:

  • 伪并行处理:同时处理多个任务(如按键扫描、显示刷新、数据采集)

  • 更好的代码结构:将复杂功能分解为独立任务

  • 响应性:确保关键任务能及时得到执行

这就是我们要实现的超轻量级多任务系统 ,通常称为协作式调度器超级循环任务调度器

二、 核心原理:协作式多任务

与复杂的抢占式系统不同,我们的系统基于协作式多任务

  • 协作式:每个任务必须主动"让出"CPU,其他任务才能运行

  • 无优先级:所有任务平等轮流执行

  • 无任务栈:所有任务共享同一个栈,节省内存

  • 无上下文切换:不需要保存/恢复任务状态

工作流程

text

复制代码
初始化系统
↓
无限循环 {
    执行任务1(如果到了执行时间)
    执行任务2(如果到了执行时间)
    ...
    执行任务N(如果到了执行时间)
    系统空闲处理(可选)
}
三、 核心实现:时间片轮转调度
1. 任务控制块(Task Control Block, TCB)

这是系统的核心数据结构,记录每个任务的信息:

c

复制代码
// 任务函数指针类型
typedef void (*TaskFunction)(void);

// 任务控制块结构
typedef struct {
    TaskFunction task;      // 任务函数指针
    uint16_t interval;      // 执行间隔(ms)
    uint16_t last_run;      // 上次执行时间
    uint8_t enabled;        // 任务使能标志
} TCB;

// 任务列表
TCB task_list[MAX_TASKS];
uint8_t task_count = 0;
2. 系统时钟源

我们需要一个精确的时间基准,通常使用定时器中断:

c

复制代码
// 全局系统时钟(每1ms递增)
volatile uint32_t system_tick = 0;

// 定时器中断服务函数(1ms中断)
ISR(TIMER1_COMPA_vect) {
    system_tick++;
}
3. 任务调度器核心

c

复制代码
// 初始化调度器
void scheduler_init(void) {
    // 配置定时器产生1ms中断
    // 这里以AVR为例
    TCCR1A = 0;
    TCCR1B = (1 << WGM12) | (1 << CS10); // CTC模式,无分频
    OCR1A = 15999; // 16MHz / 1 = 16000000Hz,16000000/1000 = 16000-1
    TIMSK1 = (1 << OCIE1A);
    
    sei(); // 开启全局中断
    task_count = 0;
}

// 添加任务到调度器
uint8_t scheduler_add_task(TaskFunction func, uint16_t interval) {
    if (task_count >= MAX_TASKS) {
        return 0; // 失败
    }
    
    task_list[task_count].task = func;
    task_list[task_count].interval = interval;
    task_list[task_count].last_run = system_tick;
    task_list[task_count].enabled = 1;
    task_count++;
    
    return 1; // 成功
}

// 调度器主循环
void scheduler_run(void) {
    uint8_t i;
    uint32_t current_tick;
    
    while(1) {
        current_tick = system_tick;
        
        // 遍历所有任务
        for (i = 0; i < task_count; i++) {
            // 检查任务是否使能且到了执行时间
            if (task_list[i].enabled && 
                (current_tick - task_list[i].last_run >= task_list[i].interval)) {
                
                // 更新上次执行时间
                task_list[i].last_run = current_tick;
                
                // 执行任务
                task_list[i].task();
            }
        }
        
        // 空闲任务(可选)
        idle_task();
    }
}
四、 实战示例:三任务系统

让我们实现一个具体的例子:LED闪烁、按键扫描、串口输出。

c

复制代码
#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

#define MAX_TASKS 8

// 系统变量
volatile uint32_t system_tick = 0;
TCB task_list[MAX_TASKS];
uint8_t task_count = 0;

// 任务1:LED闪烁(500ms间隔)
void task_led_blink(void) {
    static uint8_t led_state = 0;
    led_state = !led_state;
    
    if (led_state) {
        PORTB |= (1 << PB5);  // LED亮
    } else {
        PORTB &= ~(1 << PB5); // LED灭
    }
}

// 任务2:按键扫描(50ms间隔)
void task_key_scan(void) {
    static uint8_t last_key_state = 0;
    uint8_t current_key_state = PINB & (1 << PB0);
    
    // 检测下降沿(按键按下)
    if (last_key_state && !current_key_state) {
        // 按键处理
        handle_key_press();
    }
    
    last_key_state = current_key_state;
}

// 任务3:串口数据发送(1000ms间隔)
void task_uart_send(void) {
    static uint8_t counter = 0;
    uart_send_string("Counter: ");
    uart_send_number(counter++);
    uart_send_string("\r\n");
}

// 按键处理函数
void handle_key_press(void) {
    uart_send_string("Key Pressed!\r\n");
}

// 空闲任务
void idle_task(void) {
    // 可以在这里进入低功耗模式
    // _delay_ms(1);
}

int main(void) {
    // 硬件初始化
    DDRB = (1 << PB5);    // PB5设为输出(LED)
    PORTB = (1 << PB0);   // PB0上拉电阻(按键)
    uart_init();          // 初始化串口
    
    // 调度器初始化
    scheduler_init();
    
    // 添加任务
    scheduler_add_task(task_led_blink, 500);    // 500ms间隔
    scheduler_add_task(task_key_scan, 50);      // 50ms间隔  
    scheduler_add_task(task_uart_send, 1000);   // 1000ms间隔
    
    // 启动调度器(永不返回)
    scheduler_run();
    
    return 0;
}
五、 高级特性与优化
1. 任务优先级模拟

虽然我们是协作式调度,但可以模拟优先级:

c

复制代码
void scheduler_run_priority(void) {
    uint8_t i;
    uint32_t current_tick = system_tick;
    
    // 高优先级任务放在前面检查
    for (i = 0; i < task_count; i++) {
        if (task_list[i].enabled && 
            (current_tick - task_list[i].last_run >= task_list[i].interval)) {
            
            task_list[i].last_run = current_tick;
            task_list[i].task();
            
            // 重要:高优先级任务执行后立即返回,确保及时性
            return;
        }
    }
    
    // 中优先级任务...
    // 低优先级任务...
}
2. 动态任务管理

c

复制代码
// 启用/禁用任务
void scheduler_set_task_enable(uint8_t task_id, uint8_t enable) {
    if (task_id < task_count) {
        task_list[task_id].enabled = enable;
    }
}

// 修改任务间隔
void scheduler_set_task_interval(uint8_t task_id, uint16_t interval) {
    if (task_id < task_count) {
        task_list[task_id].interval = interval;
    }
}
3. 系统状态监控

c

复制代码
// 获取CPU使用率
uint8_t get_cpu_usage(void) {
    static uint32_t last_idle_time = 0;
    static uint32_t last_total_time = 0;
    
    uint32_t current_idle_time = idle_counter;
    uint32_t current_total_time = system_tick;
    
    uint32_t idle_delta = current_idle_time - last_idle_time;
    uint32_t total_delta = current_total_time - last_total_time;
    
    last_idle_time = current_idle_time;
    last_total_time = current_total_time;
    
    if (total_delta == 0) return 100;
    
    return 100 - (idle_delta * 100 / total_delta);
}
六、 最佳实践与注意事项
  1. 任务设计原则

    • 每个任务必须短小精悍,执行时间要远小于其间隔时间

    • 避免在任务中使用长延时(如_delay_ms(100)

    • 任务函数应为无阻塞设计

  2. 中断使用

    • 保持中断服务函数尽可能短

    • 在ISR中只做标记,在主循环中处理

  3. 资源共享

    • 由于没有真正的任务保护,要注意共享数据(如全局变量)的访问

    • 可以使用"原子操作"或简单的开关中断来保护关键代码段

  4. 内存优化

    • 根据实际任务数量设置MAX_TASKS

    • 使用最小的合适数据类型(uint8_t vs int

七、 资源开销分析

以ATmega328P为例:

  • ROM占用:约 500-800 字节(核心调度器)

  • RAM占用:约 20-50 字节(任务表+变量)

  • CPU开销:< 1%(在任务设计合理的情况下)

八、 总结

这种超轻量级多任务系统的优势:

  • 极低资源消耗:适合最基础的8位单片机

  • 简单可靠:没有复杂的上下文切换,调试容易

  • 可预测性:任务执行时间可预测

  • 易于扩展:可以方便地添加新任务

虽然功能简单,但这种设计思想是许多复杂RTOS的基础。掌握了它,您就理解了嵌入式多任务的核心原理。

相关推荐
黑符石8 天前
【产品调研】MATB-II 软件用户指南总结
人机交互·资源管理·多任务·nasa·matb·认知负荷·飞行
木鬼与槐1 年前
数据处理方式,线程与进程,多任务,Spark与MR的区别
spark·多任务
思考实践1 年前
深度学习多任务学习笔记
笔记·学习·多任务·loss
林鸿群1 年前
golang协程工作池处理多任务示例
开发语言·golang·多任务·协程池
liubinkaixin2 年前
03-单片机商业项目编程,从零搭建低功耗系统设计
c语言·单片机·内核·低功耗·多任务·时间片·低功耗传感器
图灵追慕者2 年前
Python多任务处理---多线程
多线程·多任务·多任务处理
糖果罐子♡2 年前
鸿蒙小车之多任务调度实验
服务器·华为·嵌入式·harmonyos·多任务
墩墩分墩2 年前
【Python】从入门到上头— 多进程与分布式进程(10)
开发语言·分布式·python·分布式进程·多任务