1. 裸机单片机任务调度器设计背景
在嵌入式开发领域,裸机系统(Bare-metal)依然占据着重要地位,特别是在资源受限的单片机应用中。与RTOS相比,裸机编程对硬件资源的利用率更高,但缺乏任务调度机制往往会导致代码结构混乱。这正是我们需要实现一个高效任务调度器的核心原因。
我曾在GD32F103C8T6这类Cortex-M3内核单片机上,遇到过因缺乏任务调度而导致的状态机嵌套问题。一个典型的案例是:当串口接收、ADC采样和按键检测三个功能需要并行处理时,传统的超级循环(Super Loop)架构会让代码迅速变得难以维护。通过引入分层架构的任务调度器,我们可以在不增加RTOS开销的情况下,获得类似多任务的处理能力。
2. COM/APP/SRV/DRV分层架构解析
2.1 四层架构设计理念
我们的架构分为四个层级,自底向上分别是:
- DRV(Driver Layer):直接操作寄存器的硬件驱动层
- SRV(Service Layer):提供硬件抽象服务的中间层
- APP(Application Layer):业务逻辑实现层
- COM(Communication Layer):对外交互接口层
这种分层方式借鉴了汽车电子领域的AUTOSAR架构思想,但在资源受限的单片机上做了大量精简。以串口通信为例:
- DRV层只关心USART寄存器的配置
- SRV层实现环形缓冲区和基本收发函数
- APP层处理协议解析
- COM层提供统一的API给外部调用
2.2 层级间通信机制
各层之间通过严格定义的接口进行通信:
- 上层调用下层时,必须通过函数指针接口
- 下层通知上层时,使用事件回调机制
- 禁止跨层直接访问硬件资源
这种设计带来两个显著优势:
- 硬件更换时(如从STM32切换到GD32),只需修改DRV层
- 单元测试时可以方便地Mock底层函数
3. 任务调度器核心实现
3.1 时间片轮询调度算法
我们采用改进的时间片轮询算法,其核心数据结构如下:
c复制typedef struct {
void (*task_func)(void);
uint32_t interval;
uint32_t last_run;
} task_t;
task_t task_list[] = {
{task_led, 100, 0},
{task_uart, 10, 0},
{task_adc, 50, 0}
};
调度器的主循环通过SysTick中断维护一个微秒级的时间基准:
c复制void SysTick_Handler(void) {
timebase_us++;
}
void scheduler_run(void) {
uint32_t now = timebase_us;
for(int i=0; i<TASK_COUNT; i++) {
if(now - task_list[i].last_run >= task_list[i].interval) {
task_list[i].task_func();
task_list[i].last_run = now;
}
}
}
3.2 关键性能优化技巧
- 避免浮点运算:所有时间计算使用无符号整型
- 任务执行时间监控:在任务入口和出口记录时间戳
- 优先级处理:通过调整轮询顺序实现软优先级
实测在72MHz的Cortex-M3内核上,调度器本身的开销小于1us,任务切换延迟在3us以内。
4. 微秒延时函数实现
4.1 精准延时的实现原理
传统的毫秒级延时依赖于SysTick,而微秒级延时需要更高精度的计时源。我们利用CPU内核的DWT(Data Watchpoint and Trace)单元:
c复制#define DEMCR_TRCENA (1 << 24)
#define DWT_CTRL_CYCCNTENA (1 << 0)
void dwt_init(void) {
CoreDebug->DEMCR |= DEMCR_TRCENA;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA;
}
void delay_us(uint32_t us) {
uint32_t start = DWT->CYCCNT;
uint32_t cycles = us * (SystemCoreClock / 1000000);
while((DWT->CYCCNT - start) < cycles);
}
注意:使用前必须调用dwt_init()初始化DWT单元。在GD32F103上,SystemCoreClock需要根据实际时钟配置调整。
4.2 延时精度实测数据
| 目标延时(us) | 实测平均值(us) | 最大偏差(us) |
|---|---|---|
| 1 | 1.02 | 0.15 |
| 10 | 10.05 | 0.3 |
| 100 | 100.1 | 0.8 |
5. 串口重定向实现与问题排查
5.1 标准库重定向方法
通过重写_write和_read函数实现printf到串口:
c复制int _write(int fd, char *ptr, int len) {
if(fd == STDOUT_FILENO || fd == STDERR_FILENO) {
for(int i=0; i<len; i++) {
while(!(USART1->SR & USART_SR_TXE));
USART1->DR = ptr[i];
}
}
return len;
}
5.2 GD32F103C8T6常见问题
问题现象:printf无输出,但直接操作DR寄存器可以发送数据。
排查步骤:
- 检查USART时钟使能
- 确认复用功能配置正确(AFIO->PCF1)
- 验证GPIO模式设置为复用推挽输出
- 检查重定向函数是否被正确链接
特别注意:GD32的USART寄存器布局与STM32略有不同,TXE标志位在STAT寄存器而非SR寄存器。
6. 完整项目集成示例
6.1 项目目录结构
code复制project/
├── drv/
│ ├── usart.c
│ └── gpio.c
├── srv/
│ ├── printf.c
│ └── delay.c
├── app/
│ ├── task_led.c
│ └── task_uart.c
└── com/
├── cli.c
└── api.c
6.2 典型任务实现示例
以LED闪烁任务为例:
c复制// app/task_led.c
void task_led(void) {
static uint32_t counter = 0;
if(++counter >= 500) {
gpio_toggle(LED_PIN);
counter = 0;
}
}
// drv/gpio.c
void gpio_toggle(uint16_t pin) {
GPIOA->ODR ^= pin;
}
7. 实际应用中的经验技巧
-
任务划分原则:
- 耗时超过1ms的任务需要拆分为多个状态
- 关键任务(如通信)应放在调度列表前端
- 周期任务间隔建议取2^n毫秒(便于计算)
-
调试技巧:
- 使用空闲GPIO引脚输出脉冲信号,用示波器测量任务执行时间
- 在调度器中添加任务执行时间统计功能
- 通过串口重定向输出调度器状态信息
-
内存优化:
- 将const数据放在FLASH区
- 使用位域结构体节省RAM
- 关键变量使用__attribute__((aligned(4)))
在GD32F103C8T6上实测,完整调度器系统占用资源如下:
- FLASH: 4.2KB (包含printf浮点支持)
- RAM: 256字节(不含应用任务)
- CPU负载:<1%(无任务运行时)
