Administrator
发布于 2026-09-25 / 0 阅读
0
0

裸机也该有“操作系统”:80 行写一个软定时器任务调度器

一句话结论:把 HAL_Delay() 从主循环里彻底删掉,改成「1ms 中断打点 + 主循环轮询到期任务」,你的裸机程序立刻变得跟 RTOS 一样有条理。

一、完整工程下载

压缩包内含全部源码、platformio.ini、Makefile、README.md,解压即用,不需要额外配置。

下载 soft-timer.zip (7.7 KB,共 7 个文件)

.gitignore
Makefile
README.md
include/
  soft_timer.h
platformio.ini
src/
  soft_timer.c
test/
  test_soft_timer.c

裸机程序的三种写法,你在第几种

/* 写法一:Delay 大法(新手) */
while (1) {
    HAL_Delay(1);      /* 这 1ms 什么都没干 */
    read_sensor();
    HAL_Delay(9);      /* 又是 9ms 白白浪费 */
    control();         /* 一旦 control() 变慢,read_sensor 的周期就不准了 */
}

/* 写法二:标志位大法(进阶) */
while (1) {
    if (flag_1ms)  { flag_1ms = 0;  read_sensor(); }
    if (flag_10ms) { flag_10ms = 0; control(); }
    if (flag_100ms){ flag_100ms = 0; send_report(); }
}
/* 问题:加第 8 个任务就要加第 8 个标志位,中断里还要一个个置位 */

/* 写法三:软定时器(本文) */
while (1) {
    soft_timer_poll();   /* 一行搞定,任务表自己管 */
}

设计要点

1. 时间基准只来自一个地方

系统里只有 SysTick(或一个基本定时器)中断负责累加一个 64 位毫秒计数器:

void SysTick_Handler(void) {
    g_tick_ms++;      /* 唯一职责 */
}

所有任务的周期判断都基于这个计数器,没有任何其它 delay。

2. 到期时间用「比较」而不是「减法计数」

每个定时器记录 expire_at(绝对到期时刻),判断条件是:

if ((int32_t)(now - t->expire_at) >= 0) { /* 到期 */ }

必须用有符号差值比较,这样即使 now 溢出回绕也是正确的 (这是嵌入式里判断时间的标准姿势,别写 now > expire_at)。

3. 回调而不是标志位

每个定时器绑定一个函数指针,到点了直接调用。 表驱动的好处是:新增任务只要在表里加一行。

4. 回调要短

主循环是串行执行的,某个回调跑了 50ms,其它任务就被推迟 50ms。 所以回调里只做状态更新和标志置位,耗时活儿(打印、刷屏、写 Flash) 放到主循环的任务队列里。

工程内容

  • soft_timer.c/h:静态分配的定时器池 + 上电自动重载 + 周期修改 + 单次/周期模式
  • test/test_soft_timer.c:主机端测试,用虚拟时间模拟 10 秒,验证每个任务的触发次数完全正确

怎么接到 STM32 上

/* 1. 定时器中断里打点 */
void TIM2_IRQHandler(void) {
    if (TIM2->SR & TIM_SR_UIF) {
        TIM2->SR = ~TIM_SR_UIF;
        soft_timer_tick();          /* 内部把 ms 计数 +1 */
    }
}

/* 2. 注册任务 */
soft_timer_create("sample",  1,   0, on_sample);   /* 每 1ms */
soft_timer_create("control", 10,  0, on_control);  /* 每 10ms */
soft_timer_create("report",  100, 0, on_report);   /* 每 100ms */

/* 3. 主循环 */
while (1) {
    soft_timer_poll();
}

调试要点

  • 任务跑飞了怎么看? 在 soft_timer_poll 里统计每个回调的执行耗时,

超时(比如 > 周期的一半)就记录一条告警

  • 抖动大查两件事:是不是有回调太长;是不是主循环里还有别的 HAL_Delay
  • 优先级:软定时器不解决优先级问题。真正的硬实时(比如电流环)

还是要放在中断里或上 RTOS

进阶方向

  • 加上任务最坏执行时间统计(WCET),做成一个轻量级性能剖析器
  • 换成抢占式调度器(时间片 + 优先级),就是一个极简 RTOS
  • 和事件队列结合,做成「生产者中断 → 事件队列 → 消费者任务」的完整模型

完整代码

Makefile

CC      ?= gcc
CFLAGS  ?= -std=c99 -Wall -Wextra -O2 -Iinclude
LDLIBS  ?= 
SRC      = src/soft_timer.c
TEST     = test/test_soft_timer.c

ifeq ($(OS),Windows_NT)
EXT = .exe
endif
BIN = build/test$(EXT)

all: run

$(BIN): $(SRC) $(TEST)
	@mkdir -p build
	$(CC) $(CFLAGS) $(SRC) $(TEST) -o $(BIN) $(LDLIBS)

run: $(BIN)
	@$(BIN)

clean:
	rm -rf build

.PHONY: all run clean

include/soft_timer.h

/**
 * soft_timer.h - 裸机软定时器调度器
 *
 * 时间基准由外部中断提供(例如 1ms 的 SysTick),主循环调用 soft_timer_poll()。
 * 全部静态分配,无 malloc。
 */
#ifndef SOFT_TIMER_H
#define SOFT_TIMER_H

#include <stdbool.h>
#include <stdint.h>

#ifdef __cplusplus
extern "C" {
#endif

#define SOFT_TIMER_MAX 12
#define SOFT_TIMER_NAME_LEN 12

typedef void (*soft_timer_cb_t)(void *arg);

typedef struct {
    char name[SOFT_TIMER_NAME_LEN];
    uint32_t period_ms;
    uint32_t expire_at;   /* 绝对到期时刻 */
    soft_timer_cb_t cb;
    void *arg;
    bool repeat;          /* true 周期任务,false 单次任务 */
    bool active;
    uint32_t run_cnt;     /* 累计执行次数(调试用) */
    uint32_t max_cost_us; /* 该回调历史最大耗时(调试用) */
} soft_timer_t;

/** 由 1ms 定时器中断调用 */
void soft_timer_tick(void);
/** 由主循环调用(也可由 1ms 中断调用,视实时性要求而定) */
void soft_timer_poll(void);

/**
 * 创建一个定时器
 * @param name      名字(最长 11 字符,仅调试用)
 * @param period_ms 周期(毫秒),至少 1
 * @param repeat    true 周期触发,false 只触发一次
 * @param cb        回调,禁止为空
 * @param arg       传给回调的参数
 * @return 定时器句柄;NULL 表示池已满
 */
soft_timer_t *soft_timer_create(const char *name, uint32_t period_ms, bool repeat,
                                soft_timer_cb_t cb, void *arg);

/** 启动 / 停止 / 删除 */
void soft_timer_start(soft_timer_t *t);
void soft_timer_stop(soft_timer_t *t);
void soft_timer_release(soft_timer_t *t);
/** 运行中修改周期,下一次到期按新周期计算 */
void soft_timer_set_period(soft_timer_t *t, uint32_t period_ms);
/** 由回调返回 true 表示「本任务还没做完,先别触发我」,用于背压 */
void soft_timer_delay(soft_timer_t *t, uint32_t add_ms);

/** 提供时间基准(由调用方在 1ms 中断里累加或直接调用 soft_timer_tick) */
uint32_t soft_timer_now(void);
/** 直接设置时间基准。用于与 RTC / 上位机对时,或主机端测试时间回绕 */
void soft_timer_set_now(uint32_t ms);
/** 调试:打印所有定时器的触发次数 */
void soft_timer_dump(void);

#ifdef __cplusplus
}
#endif

#endif /* SOFT_TIMER_H */

platformio.ini

[platformio]
default_envs = bluepill

[env:bluepill]
platform = ststm32
board = bluepill_f103c8
framework = arduino
upload_protocol = stlink
monitor_speed = 115200
build_flags =
    -Wall
    -Wextra
    -Isrc
lib_ldf_mode = deep+

src/soft_timer.c

#include "soft_timer.h"

#include <stdio.h>
#include <string.h>

/* 时间基准:由 soft_timer_tick() 累加。用 int32 差值比较,回绕也安全 */
static volatile uint32_t s_now_ms = 0;
static soft_timer_t s_pool[SOFT_TIMER_MAX];
static uint8_t s_used = 0;

static uint32_t cost_now_us(void);
static void cost_start(void);

/* 简单的耗时统计钩子:主机上用 clock(),嵌入式上换成 DWT->CYCCNT */
#ifdef SOFT_TIMER_HOST
#include <time.h>
static clock_t s_t0;
static uint32_t cost_now_us(void)
{
    return (uint32_t)((clock() - s_t0) * 1000000 / CLOCKS_PER_SEC);
}
static void cost_start(void)
{
    s_t0 = clock();
}
#else
static uint32_t s_dwt_start;
static uint32_t cost_now_us(void)
{
    return 0; /* 嵌入式默认不统计,需要时替换为 DWT 计数器 */
}
static void cost_start(void)
{
    (void)s_dwt_start;
}
#endif

void soft_timer_tick(void)
{
    s_now_ms++;
}

uint32_t soft_timer_now(void)
{
    return s_now_ms;
}

void soft_timer_set_now(uint32_t ms)
{
    s_now_ms = ms;
}

/* 有符号差值比较:now 溢出回绕时依然正确 */
static bool is_expired(uint32_t now, uint32_t expire_at)
{
    return (int32_t)(now - expire_at) >= 0;
}

soft_timer_t *soft_timer_create(const char *name, uint32_t period_ms, bool repeat,
                                soft_timer_cb_t cb, void *arg)
{
    soft_timer_t *t;
    uint8_t i;

    if (cb == NULL || period_ms == 0 || name == NULL) {
        return NULL;
    }
    for (i = 0; i < SOFT_TIMER_MAX; i++) {
        if (!s_pool[i].active && s_pool[i].cb == NULL) {
            break;
        }
    }
    if (i == SOFT_TIMER_MAX) {
        return NULL;
    }
    t = &s_pool[i];
    memset(t, 0, sizeof(*t));
    strncpy(t->name, name, SOFT_TIMER_NAME_LEN - 1);
    t->period_ms = period_ms;
    t->repeat = repeat;
    t->cb = cb;
    t->arg = arg;
    t->expire_at = s_now_ms + period_ms;
    t->active = true;
    s_used++;
    return t;
}

void soft_timer_start(soft_timer_t *t)
{
    if (t == NULL || t->cb == NULL) {
        return;
    }
    t->expire_at = s_now_ms + t->period_ms;
    t->active = true;
}

void soft_timer_stop(soft_timer_t *t)
{
    if (t != NULL) {
        t->active = false;
    }
}

void soft_timer_release(soft_timer_t *t)
{
    if (t != NULL && t->cb != NULL) {
        t->active = false;
        t->cb = NULL;
        if (s_used > 0) {
            s_used--;
        }
    }
}

void soft_timer_set_period(soft_timer_t *t, uint32_t period_ms)
{
    if (t != NULL && period_ms > 0) {
        t->period_ms = period_ms;
    }
}

void soft_timer_delay(soft_timer_t *t, uint32_t add_ms)
{
    if (t != NULL) {
        t->expire_at = s_now_ms + add_ms;
    }
}

void soft_timer_poll(void)
{
    uint8_t i;
    uint32_t now = s_now_ms;

    for (i = 0; i < SOFT_TIMER_MAX; i++) {
        soft_timer_t *t = &s_pool[i];
        uint32_t start;

        if (!t->active || t->cb == NULL) {
            continue;
        }
        if (!is_expired(now, t->expire_at)) {
            continue;
        }

        /* 先排下一次,保证回调耗时不影响周期精度 */
        if (t->repeat) {
            t->expire_at = now + t->period_ms;
        } else {
            t->active = false;
        }

        cost_start();
        t->cb(t->arg);
        start = cost_now_us();
        if (start > t->max_cost_us) {
            t->max_cost_us = start;
        }
        t->run_cnt++;
    }
}

void soft_timer_dump(void)
{
    uint8_t i;
    printf("name          period   runs   max_cost_us\n");
    for (i = 0; i < SOFT_TIMER_MAX; i++) {
        if (s_pool[i].cb == NULL) {
            continue;
        }
        printf("%-12s  %5u  %5u   %8u\n", s_pool[i].name, s_pool[i].period_ms,
               s_pool[i].run_cnt, s_pool[i].max_cost_us);
    }
}

test/test_soft_timer.c

/**
 * 主机端测试:用「虚拟时间」跑 10 秒,检查每个任务的触发次数
 * gcc -std=c99 -Wall -Wextra -DSOFT_TIMER_HOST -Iinclude src/soft_timer.c test/test_soft_timer.c -o build/test
 */
#include <stdio.h>
#include <string.h>

#include "soft_timer.h"

static int g_pass = 0;
static int g_fail = 0;

#define CHECK(cond, msg)                                       \
    do {                                                       \
        if (cond) { g_pass++; }                                \
        else { g_fail++; printf("  [FAIL] %s (line %d)\n", msg, __LINE__); } \
    } while (0)

static uint32_t c_1ms, c_10ms, c_100ms, c_oneshot;
static uint32_t order[16];
static int order_n = 0;
static int slow_flag = 0;

static void on_1ms(void *a)   { (void)a; c_1ms++; }
static void on_10ms(void *a)  { (void)a; c_10ms++; if (order_n < 16) order[order_n++] = 10; }
static void on_100ms(void *a) { (void)a; c_100ms++; if (order_n < 16) order[order_n++] = 100; }
static void on_once(void *a)  { (void)a; c_oneshot++; }
static void on_slow(void *a)  { (void)a; slow_flag++; }

/* 模拟主循环以 1ms 为步长推进虚拟时钟 */
static void run_ms(uint32_t ms)
{
    for (uint32_t i = 0; i < ms; i++) {
        soft_timer_tick();
        soft_timer_poll();
    }
}

int main(void)
{
    soft_timer_t *t1;
    soft_timer_t *t10;
    soft_timer_t *t100;
    soft_timer_t *t_once;

    printf("===== soft_timer 主机端测试 =====\n");

    printf("[1] 创建任务并跑 1000ms\n");
    t1 = soft_timer_create("sample", 1, true, on_1ms, NULL);
    t10 = soft_timer_create("control", 10, true, on_10ms, NULL);
    t100 = soft_timer_create("report", 100, true, on_100ms, NULL);
    CHECK(t1 && t10 && t100, "创建三个周期任务");
    run_ms(1000);
    printf("      1ms 任务 %u 次,10ms 任务 %u 次,100ms 任务 %u 次\n",
           c_1ms, c_10ms, c_100ms);
    CHECK(c_1ms >= 999 && c_1ms <= 1001, "1ms 任务约 1000 次");
    CHECK(c_10ms == 100, "10ms 任务恰好 100 次");
    CHECK(c_100ms == 10, "100ms 任务恰好 10 次");

    printf("[2] 单次任务只触发一次\n");
    t_once = soft_timer_create("oneshot", 50, false, on_once, NULL);
    run_ms(500);
    CHECK(c_oneshot == 1, "单次任务只跑 1 次");
    CHECK(!t_once->active, "单次任务执行后自动停止");

    printf("[3] 停止任务后不再触发\n");
    soft_timer_stop(t10);
    {
        uint32_t before = c_10ms;
        run_ms(200);
        CHECK(c_10ms == before, "停止后不再触发");
    }
    soft_timer_start(t10);
    run_ms(100);
    CHECK(c_10ms > 100, "重新启动后恢复触发");

    printf("[4] 运行中改周期\n");
    soft_timer_set_period(t100, 50);
    {
        uint32_t before = c_100ms;
        run_ms(500);
        uint32_t got = c_100ms - before;
        printf("      改成 50ms 后 500ms 内触发 %u 次(期望 10)\n", got);
        CHECK(got >= 9 && got <= 10, "改周期生效");
    }

    printf("[5] 时间回绕:把时钟直接推到 0xFFFFF000,跨过溢出点再跑 8000ms\n");
    {
        uint32_t before;
        soft_timer_set_now(0xFFFFF000u); /* 距 uint32 溢出只剩 4096ms */
        soft_timer_start(t10);           /* 按新时基重新对齐 */
        before = c_10ms;
        run_ms(8000);                    /* 中途 now 会回绕到 0 附近 */
        printf("      回绕期间 10ms 任务触发 %u 次(期望 800)\n", c_10ms - before);
        CHECK(c_10ms - before >= 799 && c_10ms - before <= 800, "回绕后仍然正常触发");
    }

    printf("[6] 池满保护\n");
    {
        int created = 0;
        soft_timer_t *tmp[16];
        for (int i = 0; i < 16; i++) {
            tmp[i] = soft_timer_create("extra", 1000, true, on_slow, NULL);
            if (tmp[i]) { created++; }
        }
        printf("      SOFT_TIMER_MAX=%d,再创建最多成功 %d 个\n", SOFT_TIMER_MAX, created);
        CHECK(created <= SOFT_TIMER_MAX, "不超过池容量");
        for (int i = 0; i < 16; i++) {
            if (tmp[i]) { soft_timer_release(tmp[i]); }
        }
    }

    printf("[7] 任务执行顺序(同一时刻到期的按注册顺序)\n");
    {
        order_n = 0;
        soft_timer_t *a = soft_timer_create("oa", 1, true, on_10ms, NULL);
        soft_timer_t *b = soft_timer_create("ob", 1, true, on_100ms, NULL);
        run_ms(1);
        printf("      顺序: ");
        for (int i = 0; i < order_n; i++) { printf("%u ", order[i]); }
        printf("\n");
        CHECK(order_n >= 2, "两个任务都被调用");
        soft_timer_release(a);
        soft_timer_release(b);
    }

    printf("\n");
    soft_timer_dump();
    printf("----- 通过 %d 项,失败 %d 项 -----\n", g_pass, g_fail);
    return g_fail == 0 ? 0 : 1;
}

实测输出

下面这段输出是把上面的核心算法用 本机 gcc 真编译、真运行得到的(不含任何硬件依赖):

===== soft_timer 主机端测试 =====
[1] 创建任务并跑 1000ms
      1ms 任务 1000 次,10ms 任务 100 次,100ms 任务 10 次
[2] 单次任务只触发一次
[3] 停止任务后不再触发
[4] 运行中改周期
      改成 50ms 后 500ms 内触发 9 次(期望 10)
[5] 时间回绕:把时钟直接推到 0xFFFFF000,跨过溢出点再跑 8000ms
      回绕期间 10ms 任务触发 800 次(期望 800)
[6] 池满保护
      SOFT_TIMER_MAX=12,再创建最多成功 8 个
[7] 任务执行顺序(同一时刻到期的按注册顺序)
      顺序: 10 100 

name          period   runs   max_cost_us
sample            1   3905          0
control          10   1010          0
report           50     59          0
oneshot          50      1          0
----- 通过 12 项,失败 0 项 -----

评论