裸机编程的困境:前后台系统的局限性

很多刚入行的朋友,甚至一些工作两三年的工程师,写单片机程序时最习惯的方式就是——一个 while(1) 超级循环,加上几个中断服务函数。这其实就是典型的前后台系统。前台是中断,后台是主循环。

我刚开始做项目时也是这么干的。简单、直接,看起来没什么问题。但做着做着,问题就来了。

超级循环的“假象”

先看一段典型的代码:

void main(void)
{
    // 初始化
    System_Init();
    
    while(1)
    {
        Task_KeyScan();      // 按键扫描
        Task_LedDisplay();   // LED显示
        Task_DataProcess();  // 数据处理
        Task_CommSend();     // 通信发送
        // ... 更多任务
    }
}

看起来挺整齐,对吧?每个任务轮流执行。但你想想看,如果 Task_DataProcess() 执行时间很长,比如处理一个复杂的算法,那后面的 Task_CommSend() 就得等着。这时候如果按键来了,也得等。

这就是超级循环的第一个问题:任务之间互相阻塞。

核心矛盾:超级循环是顺序执行的,但嵌入式系统需要的是“并发”处理。一个任务卡住,所有任务都卡住。

中断与主循环的“战争”

有人会说:“那我用中断啊!按键检测放中断里,通信也放中断里。” 嗯,这确实能解决一部分问题。但中断不是万能的。

我在项目中遇到过这样一个案例:一个温控器产品,用定时器中断每1ms采集一次温度,然后在中断里做PID计算。看起来没问题,但产品批量生产后,偶尔会出现温度跳变。查了很久才发现——PID计算耗时太长,导致下一次中断触发时,前一次还没处理完。中断嵌套一多,栈溢出,数据就乱了。

中断的局限性,我总结了几点:

问题 说明 后果
中断响应延迟 高优先级中断会阻塞低优先级中断 实时性要求高的任务可能错过时机
中断服务函数过长 ISR里做太多事,影响系统响应 主循环被“饿死”,系统卡顿
共享资源冲突 中断和主循环访问同一变量 数据不一致,逻辑错误
可预测性差 中断触发时机不确定 系统行为难以调试和验证

我曾经踩过的坑:在一个电机控制项目中,我在中断里修改了一个全局变量,主循环也在读这个变量。结果电机偶尔会“抽搐”一下。后来加了 volatile 关键字,又加了临界区保护,才解决。但代码已经变得很难看了。

前后台系统的“死穴”

说白了,前后台系统最大的问题就是:没有统一的调度机制。

中断来了就执行,执行完就回去。主循环里任务顺序写死了,谁先谁后全靠程序员手动安排。一旦系统复杂起来,比如同时要处理按键、显示、通信、数据采集、报警输出……你就会发现:

  • 任务优先级无法动态调整
  • 任务执行时间不可控
  • 代码耦合严重,改一个地方可能影响一片
  • 调试困难,尤其是时序相关的问题

我见过一个同事写的项目,主循环里塞了30多个函数,每个函数里又有各种延时等待。整个循环跑一圈要200多毫秒。按键按下去,要等200ms才有反应。用户体验极差。

一个典型的“崩溃”场景

咱们来模拟一个场景:

  1. 主循环正在执行 Task_DataProcess(),这个函数需要50ms
  2. 第10ms时,串口中断来了,要接收一帧数据
  3. 中断服务函数开始处理,但处理到一半,定时器中断又来了
  4. 定时器中断优先级更高,抢占了串口中断
  5. 定时器中断里要更新PWM占空比,但用到了和主循环共享的变量
  6. 主循环此时正在修改这个变量……

结果是什么?数据错乱、PWM输出异常、系统行为不可预测。这就是前后台系统在多任务并发时的典型问题。

我的建议:如果你现在的项目只有2-3个简单任务,前后台系统还能应付。一旦超过5个任务,或者有严格的时间要求,就该考虑引入分层调度了。

前后台系统 vs 分层调度:一张图看懂

下面这张图展示了前后台系统和分层调度系统的核心区别:

前后台系统 vs 分层调度系统 前后台系统(超级循环) 中断层(前台) ISR1 | ISR2 | ISR3 ... 主循环层(后台) Task_KeyScan Task_LedDisplay Task_DataProcess Task_CommSend ← 顺序执行,互相阻塞 ← 无优先级,无调度 分层调度系统 硬件中断层 中断服务 → 发送事件/信号 任务调度器 优先级管理 | 时间片分配 | 状态切换 任务层 按键任务 显示任务 通信任务 数据处理 报警输出 ... 任务独立运行,调度器统一管理,互不阻塞

什么时候该放弃前后台系统?

我个人的经验是,当你的项目出现以下任何一个信号时,就该考虑换方案了:

  • 主循环执行时间超过10ms —— 这意味着按键响应至少延迟10ms
  • 中断服务函数里出现循环或延时 —— 这是大忌,ISR应该尽快退出
  • 需要多个定时器来触发不同任务 —— 说明任务的时间要求已经复杂了
  • 代码里到处是全局变量和标志位 —— 这是“面条代码”的前兆
  • 调试时经常出现“偶发”问题 —— 大概率是时序冲突

一句话总结:前后台系统不是不能用,但它只适合“玩具级”的项目。真正要做出稳定、可靠、可维护的产品,必须引入分层调度和状态机设计。这也是这门课接下来要讲的核心内容。