裸机系统架构详解:前后台系统
说到裸机系统,我第一个想到的就是「前后台系统」。这名字听着挺玄乎,其实说白了就是:一个死循环当后台,中断服务程序当前台。
我刚开始做嵌入式那会儿,用的就是这种架构。那时候单片机资源紧张,RTOS?想都不敢想。一个8位MCU,2KB的RAM,跑个循环加中断,刚刚好。
超级循环——系统的「心脏」
超级循环,也叫主循环。它的工作方式特别简单:
void main(void)
{
// 初始化
System_Init();
Periph_Init();
// 超级循环
while(1)
{
Task_ScanKey(); // 扫描按键
Task_UpdateDisplay(); // 更新显示
Task_ProcessData(); // 处理数据
Task_CommPoll(); // 通信轮询
}
}
你看,就是这么个死循环。每个任务按顺序执行,跑完一遍再从头来。我管这叫「轮询式多任务」——虽然它压根儿不算多任务。
核心特点:
- 任务顺序执行,没有优先级
- 一个任务卡住,后面的全完蛋
- 响应时间取决于循环长度
- 代码简单,容易理解
我在项目中遇到过一个问题:一个LED闪烁任务里加了延时,结果整个系统都跟着「眨眼」了。按键扫描、通信处理,全被拖慢。嗯,这就是超级循环的典型坑。
中断服务程序——系统的「救火队」
超级循环有个致命弱点:它不知道什么时候该处理紧急事件。这时候,中断就派上用场了。
中断服务程序(ISR)就像个「救火队」。平时没事儿,一旦有紧急情况(比如按键按下、数据到达),它立刻冲进去处理。
// 定时器中断 - 每1ms触发一次
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
// 处理定时任务
g_uiTick++;
if(g_uiTick % 10 == 0)
{
g_bScanKey = 1; // 设置按键扫描标志
}
}
}
// 串口接收中断
void USART1_IRQHandler(void)
{
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)
{
uint8_t data = USART_ReceiveData(USART1);
// 将数据存入环形缓冲区
RingBuf_Write(&g_rxBuf, data);
}
}
你想想看,中断和主循环配合起来,是不是有点像「前台接待」和「后台办公」?前台(中断)处理紧急事务,后台(主循环)处理常规工作。这就是「前后台系统」名字的由来。
我的经验:中断里千万别做耗时操作。我曾经在中断里调了个printf,结果系统直接崩了。中断服务程序的原则是:快进快出,只做最必要的事。
任务调度机制——没有调度就是最好的调度
裸机系统有任务调度吗?说实话,没有。或者说,它的调度机制就是「顺序执行」。
但这不代表我们没办法实现「伪并行」。常用的技巧有:
- 状态机调度:每个任务拆成多个状态,每次循环只执行一个状态
- 标志位调度:中断设置标志,主循环检查标志再执行
- 时间片轮转:用定时器分配时间片,每个任务跑固定时间
// 状态机调度示例
typedef enum {
STATE_IDLE,
STATE_READ_SENSOR,
STATE_PROCESS_DATA,
STATE_SEND_RESULT
} TaskState_t;
void Task_Sensor(void)
{
static TaskState_t state = STATE_IDLE;
switch(state)
{
case STATE_IDLE:
if(g_bStartMeasure)
{
state = STATE_READ_SENSOR;
}
break;
case STATE_READ_SENSOR:
ADC_StartConversion();
state = STATE_PROCESS_DATA;
break;
case STATE_PROCESS_DATA:
if(ADC_GetFlagStatus(ADC_FLAG_EOC))
{
g_u16Result = ADC_GetConversionValue();
state = STATE_SEND_RESULT;
}
break;
case STATE_SEND_RESULT:
UART_SendData(g_u16Result);
state = STATE_IDLE;
break;
}
}
说白了,裸机系统的调度就是「程序员手动调度」。你写代码的时候,就得想好每个任务什么时候跑、跑多久。这活儿挺累的,但好处是——你完全掌控了系统的每一纳秒。
资源管理方式——共享资源的「交通规则」
裸机系统里,资源管理是个大问题。主循环和中断都在访问同一个变量,搞不好就出乱子。
我常用的资源管理方式有:
| 资源类型 | 管理方式 | 适用场景 |
|---|---|---|
| 全局变量 | volatile + 原子访问 | 中断与主循环共享 |
| 环形缓冲区 | 读写指针分离 | 数据流处理 |
| 临界区 | 关中断保护 | 多中断共享资源 |
| 标志位 | 位操作 + 原子指令 | 事件通知 |
注意:关中断的时间一定要短。我见过有人关中断做复杂计算,结果系统响应延迟从微秒级变成了毫秒级。嗯,这锅得程序员背。
举个实际例子:
// 中断和主循环共享的变量
volatile uint32_t g_uiSystemTick;
// 中断中更新
void SysTick_Handler(void)
{
g_uiSystemTick++;
}
// 主循环中读取
uint32_t GetSystemTick(void)
{
uint32_t tick;
// 原子读取,防止读取过程中被中断修改
__disable_irq();
tick = g_uiSystemTick;
__enable_irq();
return tick;
}
这里用了volatile关键字,告诉编译器这个变量可能被意外修改。我刚开始做嵌入式时,就因为忘了加volatile,debug了一整天。从那以后,凡是中断和主循环共享的变量,我第一件事就是加volatile。
裸机系统的优缺点总结
做了这么多年嵌入式,我对裸机系统的感受是:
优点:
- 代码简单,容易调试
- 资源占用极低(RAM、ROM都省)
- 实时性可预测(没有调度开销)
- 适合简单、固定的应用场景
缺点:
- 任务耦合度高,一个任务出问题影响全局
- 扩展性差,加个功能可能得重构
- 紧急任务响应依赖循环周期
- 复杂逻辑用状态机实现,代码量爆炸
我个人习惯是:如果项目功能少于10个,且实时性要求不高,裸机系统完全够用。但要是功能多了,或者有严格的响应时间要求,那就得考虑RTOS了。
下一章,我会详细讲讲RTOS的架构和调度机制。到时候你就能对比出来,什么时候该用裸机,什么时候该上RTOS。