一句话结论:把 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 cleaninclude/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 项 -----