STM32裸机时间片轮转调度——用SysTick实现多任务与低功耗,综合应用总结(一)

前言

在裸机单片机开发中,我们经常需要同时处理多个任务:按键扫描、LED闪烁、传感器采集、通信协议解析……如果所有逻辑都塞在 while(1) 里顺序执行,一旦某个任务耗时过长,其他任务就会被延误,系统实时性荡然无存。引入RTOS固然可以优雅地解决并发问题,但它会额外消耗Flash和RAM,在资源极度受限的MCU上并不划算。

有没有一种方法,能在不跑操作系统的前提下实现“伪多任务”呢?时间片轮转调度(Round-Robin Scheduling) 正是为此而生。它利用定时器将CPU时间划分成固定长度的时间片,轮流分配给各个任务,从而在宏观上实现多任务并发。

本文将以 STM32F103C8T6标准库函数 为基础,利用 SysTick 定时器 实现一个轻量级的协作式时间片轮转调度器,并演示如何与低功耗模式配合。所有代码均可直接复制到你的工程中运行。
在这里插入图片描述

一、时间片轮转调度原理

1.1 基本思想

时间片轮转是多任务操作系统中最简单的调度算法之一:

  • 将CPU时间划分成连续的、固定长度的时间片(如10ms);
  • 维护一个任务就绪队列;
  • 每个任务按顺序获得一个时间片的CPU使用权;
  • 时间片用完后,无论任务是否执行完毕,都必须让出CPU,调度器将切换到下一个就绪任务。

1.2 裸机下的协作式实现

真正的操作系统可以使用硬件上下文切换实现抢占,但裸机环境不具备这个条件。因此我们采用 协作式时间片轮转

  • 每个任务函数被设计成非阻塞的状态机,每次被调度时只执行一小段逻辑,然后立即返回;
  • 调度器在主循环中运行,当 SysTick 中断标记“时间片耗尽”后,主循环负责把执行权交给下一个任务;
  • SysTick 中断仅负责计时和置标志,不进行实际的上下文切换。

这样既避免了复杂的堆栈操作,又保证了 CPU 时间被公平地分配,非常适合资源受限的单片机项目。


二、时基源:SysTick 定时器

STM32 的 Cortex-M3 内核内置了一个 24 位向下计数的 SysTick 定时器,专用于产生系统节拍。用它做调度器时钟源具有先天优势:

  • 配置简单,调用 SysTick_Config() 即可;
  • 中断优先级可调整;
  • 与系统时钟同步,精确稳定。

我们将 SysTick 配置为每 1ms 中断一次,作为调度器的时基。


三、调度器设计

3.1 任务控制块(TCB)

#define MAX_TASKS       5          /* 最大任务数 */
#define TASK_NAME_LEN   8          /* 任务名长度 */

/* 任务控制块 */
typedef struct {
    char     name[TASK_NAME_LEN];  /* 任务名称(调试用) */
    void   (*func)(void);          /* 任务函数指针 */
    uint32_t timeSlice;            /* 分配的时间片(ms) */
    int32_t  remainTicks;          /* 当前时间片剩余 ticks */
    uint8_t  state;                /* 0=挂起, 1=就绪 */
} TaskTCB;

3.2 调度器全局变量

static TaskTCB  taskList[MAX_TASKS];
static uint8_t  taskCount      = 0;      /* 已注册任务数 */
static uint8_t  currentTask    = 0;      /* 当前执行的任务索引 */
static uint8_t  taskSwitchFlag = 0;      /* 任务切换请求标志 */

3.3 任务注册函数

/**
 * @brief  注册一个任务
 * @param  name      任务名(调试用)
 * @param  func      任务函数(不能阻塞,必须快速返回)
 * @param  timeSlice 分配的时间片(单位ms)
 */
void Task_Register(const char *name, void (*func)(void), uint32_t timeSlice) {
    if (taskCount >= MAX_TASKS) return;

    TaskTCB *t = &taskList[taskCount];
    strncpy(t->name, name, TASK_NAME_LEN - 1);
    t->func        = func;
    t->timeSlice   = timeSlice;
    t->remainTicks = timeSlice;
    t->state       = 1;               /* 就绪 */
    taskCount++;
}

3.4 SysTick 初始化与中断服务

/* 系统运行毫秒计数(供任务使用) */
volatile uint32_t sysTickUptime = 0;

/**
 * @brief  初始化 SysTick,产生 1ms 节拍
 */
void SysTick_Init(void) {
    if (SysTick_Config(SystemCoreClock / 1000)) {
        /* 重装载值超出范围时会返回 1,此处正常情况下不会发生 */
        while (1);
    }
    /* 设置 SysTick 中断优先级为最低 */
    NVIC_SetPriority(SysTick_IRQn, 0x0F);
}

/**
 * @brief  SysTick 中断服务函数
 */
void SysTick_Handler(void) {
    sysTickUptime++;                     /* 递增系统时间 */

    if (taskCount > 0) {
        TaskTCB *t = &taskList[currentTask];
        if (t->remainTicks > 0) {
            t->remainTicks--;
            if (t->remainTicks == 0) {
                taskSwitchFlag = 1;      /* 时间片耗尽,请求切换 */
            }
        }
    }
}

3.5 调度器主循环

/**
 * @brief  启动调度器(应在 main 中调用,永不返回)
 */
void Scheduler_Run(void) {
    while (1) {
        /* 如果有任务切换请求,且存在任务 */
        if (taskSwitchFlag && taskCount > 0) {
            taskSwitchFlag = 0;

            /* 寻找下一个就绪任务 */
            uint8_t next = currentTask;
            do {
                next = (next + 1) % taskCount;
            } while (taskList[next].state == 0 && next != currentTask);

            /* 切换到该任务并重置时间片 */
            currentTask = next;
            taskList[currentTask].remainTicks = taskList[currentTask].timeSlice;
        }

        /* 执行当前任务(如果就绪) */
        if (taskCount > 0 && taskList[currentTask].state == 1) {
            taskList[currentTask].func();
        }

        /* 低功耗:若无紧急事件可进入休眠 */
        if (taskSwitchFlag == 0) {
            __WFI();   /* 等待中断,SysTick 会自动唤醒 */
        }
    }
}

四、完整多任务示例代码

本示例实现 4 个任务:LED1 500ms 闪烁、LED2 200ms 闪烁、按键扫描(去抖)、按键动作处理。使用 STM32F103C8T6,LED 接 PB0 和 PB1,按键接 PA0(上拉,按下低电平)。

4.1 头文件与宏定义

#include "stm32f10x.h"
#include <string.h>

#define MAX_TASKS       5
#define TASK_NAME_LEN   8

#define LED1_GPIO       GPIOB
#define LED1_PIN        GPIO_Pin_0
#define LED2_GPIO       GPIOB
#define LED2_PIN        GPIO_Pin_1

#define KEY_GPIO        GPIOA
#define KEY_PIN         GPIO_Pin_0

4.2 全局变量与 TCB

volatile uint32_t sysTickUptime = 0;

typedef struct {
    char     name[TASK_NAME_LEN];
    void   (*func)(void);
    uint32_t timeSlice;
    int32_t  remainTicks;
    uint8_t  state;
} TaskTCB;

static TaskTCB  taskList[MAX_TASKS];
static uint8_t  taskCount      = 0;
static uint8_t  currentTask    = 0;
static uint8_t  taskSwitchFlag = 0;

4.3 硬件初始化

void GPIO_Init_All(void) {
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE);

    /* LED1(PB0), LED2(PB1) 推挽输出 */
    GPIO_InitStructure.GPIO_Pin   = LED1_PIN | LED2_PIN;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(LED1_GPIO, &GPIO_InitStructure);

    /* 按键 PA0 上拉输入 */
    GPIO_InitStructure.GPIO_Pin   = KEY_PIN;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IPU;
    GPIO_Init(KEY_GPIO, &GPIO_InitStructure);
}

void SysTick_Init(void) {
    if (SysTick_Config(SystemCoreClock / 1000)) {
        while (1);
    }
    NVIC_SetPriority(SysTick_IRQn, 0x0F);
}

4.4 任务注册与调度器

(此处直接使用第 3 节中的 Task_RegisterSysTick_HandlerScheduler_Run,不再重复。复制时请将三个函数完整放在工程中。)

4.5 任务函数实现

/* 全局按键状态,任务间共享 */
volatile uint8_t keyPressed = 0;

/**
 * @brief  任务1:LED1 500ms 翻转(状态机)
 */
void Task_LED1_Blink(void) {
    static uint32_t prevTime = 0;
    if (sysTickUptime - prevTime >= 500) {
        prevTime = sysTickUptime;
        /* 翻转 LED1 */
        GPIO_WriteBit(LED1_GPIO, LED1_PIN,
            (BitAction)(1 - GPIO_ReadOutputDataBit(LED1_GPIO, LED1_PIN)));
    }
}

/**
 * @brief  任务2:LED2 200ms 翻转
 */
void Task_LED2_Blink(void) {
    static uint32_t prevTime = 0;
    if (sysTickUptime - prevTime >= 200) {
        prevTime = sysTickUptime;
        GPIO_WriteBit(LED2_GPIO, LED2_PIN,
            (BitAction)(1 - GPIO_ReadOutputDataBit(LED2_GPIO, LED2_PIN)));
    }
}

/**
 * @brief  任务3:按键扫描(每20ms去抖一次)
 */
void Task_KeyScan(void) {
    static uint32_t prevTime = 0;
    static uint8_t  lastState = 1;
    if (sysTickUptime - prevTime < 20) return;
    prevTime = sysTickUptime;

    uint8_t curState = GPIO_ReadInputDataBit(KEY_GPIO, KEY_PIN);
    if (lastState == 1 && curState == 0) {   /* 下降沿 */
        keyPressed = 1;
    }
    lastState = curState;
}

/**
 * @brief  任务4:按键动作处理
 */
void Task_KeyAction(void) {
    if (keyPressed) {
        keyPressed = 0;
        /* 这里可插入自定义动作,如点亮LED1 */
        GPIO_SetBits(LED1_GPIO, LED1_PIN);
    }
}

4.6 主函数

int main(void) {
    GPIO_Init_All();
    SysTick_Init();

    /* 注册 4 个任务并分配时间片 */
    Task_Register("LED1",  Task_LED1_Blink,  10);   /* 10ms 时间片 */
    Task_Register("LED2",  Task_LED2_Blink,  10);
    Task_Register("Key",   Task_KeyScan,     5);
    Task_Register("Act",   Task_KeyAction,   5);

    Scheduler_Run();    /* 启动调度器,永不返回 */
}

4.7 代码运行说明

  • 时间片分配:LED任务各分配10ms,按键任务各分配5ms。由于SysTick节拍为1ms,时间片数字即代表该任务最多连续执行多少个节拍。
  • 非阻塞设计:所有任务都使用 static 变量保存历史状态,通过比较 sysTickUptime 差值决定是否动作,单次调用后立即返回,绝不死等。
  • 任务切换时机:SysTick中断中递减当前任务的时间片,减至0后置 taskSwitchFlag;主循环检测到标志后立即切换到下一个就绪任务,并重置其时间片。

五、低功耗集成思路

Scheduler_Run 的循环末尾已加入 __WFI() 指令。当 taskSwitchFlag == 0 且所有任务都暂时无需动作时,MCU会进入休眠模式,SysTick 定时中断将自动唤醒CPU继续运行,从而显著节省功耗。

如果某些任务需要长时间挂起,可将对应 TCB 的 state 字段置为 0,调度器会跳过它们,直至外部事件(如按键中断)将其重新激活。


六、调试与常见问题

6.1 验证时间片效果

  • 使用示波器或逻辑分析仪同时监测 PB0 和 PB1,若两个LED分别以500ms和200ms周期稳定闪烁,说明两个任务在并发执行。
  • 可通过串口打印当前任务名和 sysTickUptime,观察调度时序。

6.2 常见故障排查

现象可能原因解决方法
只有第一个任务在运行SysTick中断未使能;taskSwitchFlag 未被置位检查 SysTick_Config 调用;确认中断优先级未被屏蔽
某个任务“饿死”其他任务函数内有死循环或长时间阻塞将长流程拆分成状态机,保证每次调用在1ms内返回
低功耗唤醒失败未开启SysTick中断或其他唤醒源确保 SysTick 中断正常,或增加外部中断作为辅助唤醒源
sysTickUptime 溢出导致逻辑错误32位无符号数约49天归零使用差值比较(now - last >= period)可安全处理溢出
按键响应迟钝扫描任务时间片太小或去抖周期过长适当增加按键任务的时间片或减小去抖间隔

6.3 扩展建议

  • 动态时间片:可根据任务负载动态调整时间片长度。
  • 优先级支持:可加入优先级字段,在切换时优先选择高优先级任务。
  • 任务挂起/恢复:通过修改 state 字段可灵活启停任务。

七、总结

本文基于 STM32 标准库和 SysTick 定时器,从零搭建了一个极简的协作式时间片轮转调度器。通过将顺序执行的逻辑拆分成非阻塞的状态机,再配合时间片轮转机制,成功在裸机上实现了多个任务的“伪并发”。整套代码仅百余行,资源占用极小,非常适合成本敏感的嵌入式项目。

配合之前讲解的 I2C、SPI、PWM 等外设驱动,你可以轻松构建功能丰富的裸机多任务系统。如果你对抢占式调度或更复杂的调度算法感兴趣,欢迎在评论区继续交流!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值