1. 单片机多任务调度的核心挑战
参加蓝桥杯单片机竞赛的同学经常会遇到一个经典难题:如何在资源有限的单片机上同时处理多个实时任务。以STC15F系列单片机为例,当我们需要同时实现超声波测距、NE555频率测量、PWM输出和数码管显示等功能时,定时器资源分配就会变得捉襟见肘。
我去年指导的一个参赛队伍就遇到了这种情况。他们的系统需要同时处理超声波测距(需要定时器计时)、NE555频率测量(需要定时器计数)、数码管动态扫描(需要定时器中断)以及PWM输出。最初他们尝试让定时器0同时负责超声波和数码管,结果发现超声波测量误差高达30%,数码管还出现了明显的闪烁。
资源冲突的本质在于单片机硬件资源的排他性使用。比如:
- 定时器在同一时刻只能工作在一种模式(计时或计数)
- 同一个I2C总线不能同时与多个设备通信
- CPU在某个时刻只能执行一个中断服务程序
2. 定时器的精打细算
2.1 定时器资源分配策略
STC15F2系列单片机通常有三个定时器(T0/T1/T2),但每个都有特定限制。经过多次实测,我发现这样的分配方案最稳定:
// 定时器0:超声波测距计时(16位自动重装载模式) void Timer0_Init(void) { AUXR = 0x80; // 1T模式 TMOD = 0x04; // 外部计数模式 TH0 = TL0 = 0x00; TR0 = 1; // 启动定时器 } // 定时器1:NE555频率测量(16位计数模式) void Timer1_Init(void) { AUXR |= 0x40; // 1T模式 TMOD &= 0x0F; TL1 = 0x20; TH1 = 0xD1; TR1 = 1; } // 定时器2:数码管扫描(1ms中断) void Timer2_Init(void) { AUXR |= 0x04; // 1T模式 T2L = 0x20; T2H = 0xD1; AUXR |= 0x10; // 启动定时器 IE2 |= 0x04; // 使能中断 }2.2 中断优先级的艺术
当多个中断可能同时发生时,优先级设置就至关重要。在STC15中,中断优先级寄存器IP的设置需要特别注意:
// 设置定时器2中断优先级最高 IP2 |= 0x04; PT0 = 0; // 定时器0低优先级 PT1 = 0; // 定时器1低优先级实测发现,数码管扫描中断如果被长时间阻塞,人眼会立即察觉到闪烁。因此需要保证定时器2中断能够及时响应,即使正在处理超声波或NE555的中断服务程序。
3. 任务调度框架设计
3.1 状态机驱动架构
对于需要并发处理的任务,我推荐采用状态机架构。下面是一个典型的任务调度框架:
enum TaskState { TASK_READY, TASK_RUNNING, TASK_WAITING }; struct Task { enum TaskState state; unsigned int interval; unsigned int timer; void (*handler)(void); }; struct Task tasks[] = { {TASK_READY, 500, 0, read_ultrasonic}, // 每500ms测距 {TASK_READY, 1000, 0, read_ne555}, // 每1s测频 {TASK_READY, 1, 0, scan_display} // 每1ms刷新显示 }; void scheduler(void) { for(int i=0; i<sizeof(tasks)/sizeof(tasks[0]); i++) { if(tasks[i].state == TASK_READY) { tasks[i].handler(); tasks[i].state = TASK_WAITING; tasks[i].timer = tasks[i].interval; } } }3.2 时间片轮转技巧
在main函数的循环中,可以采用这样的时间片管理方式:
void main() { // 初始化代码... while(1) { static unsigned long tick = 0; tick++; // 每1ms更新任务计时器 if(tick % 1 == 0) { for(int i=0; i<TASK_COUNT; i++) { if(tasks[i].timer > 0) tasks[i].timer--; if(tasks[i].timer == 0) tasks[i].state = TASK_READY; } } scheduler(); handle_buttons(); // 按键处理 } }4. 典型外设的冲突解决
4.1 I2C总线共享方案
当多个设备(如PCF8591、AT24C02)共用I2C总线时,必须解决总线冲突问题。我的经验是:
- 为每个设备设计独立的读写函数
- 在操作前后增加适当延时
- 关键操作重试机制
unsigned char read_pcf8591(unsigned char ch) { unsigned char val = 0; int retry = 3; while(retry--) { I2CStart(); if(I2CSendByte(0x90) && I2CSendByte(0x40|ch)) { I2CStop(); I2CStart(); if(I2CSendByte(0x91)) { val = I2CReceiveByte(); break; } } I2CStop(); Delay5ms(); // 关键重试延时 } return val; }4.2 PWM输出的轻量级实现
当定时器资源紧张时,可以采用延时法生成PWM。虽然会占用CPU资源,但在简单场景下很实用:
void pwm_out(unsigned char duty, unsigned int freq) { unsigned int period = 1000000/freq; // 周期(us) unsigned int high_time = period*duty/100; MOTOR_ON(); DelayUS(high_time); // 自定义微秒延时 MOTOR_OFF(); DelayUS(period-high_time); }在实际项目中,我发现当PWM频率低于2kHz时,这种方法产生的波形相当稳定。但要特别注意:
- 避免在中断服务程序中使用延时
- main循环中不能有其他阻塞操作
- 需要精确校准延时函数
5. 调试与优化经验
5.1 系统监控设计
为了实时掌握系统状态,我通常会预留一个调试接口:
void debug_monitor(void) { static unsigned char cnt = 0; if(++cnt >= 50) { // 每50ms输出一次 cnt = 0; printf("US:%ucm NE555:%uHz PWM:%u%%\r\n", ultrasonic_dist, ne555_freq, pwm_duty); } }通过串口输出关键参数,可以快速定位:
- 任务执行是否按时触发
- 传感器数据是否正常
- CPU负载情况
5.2 内存优化技巧
在资源受限的单片机中,内存使用需要精打细算:
使用
code关键字将常量存入Flashcode unsigned char Seg_Table[] = {0xc0,0xf9...};共用缓冲区减少RAM占用
union { unsigned char display_buf[8]; unsigned long sensor_data; } memory_pool;使用位域节约标志位存储
struct { unsigned task_ready:1; unsigned sensor_updated:1; unsigned pwm_active:1; } flags;
6. 稳定性保障方案
6.1 看门狗的应用
在复杂的多任务环境中,看门狗是最后的保障:
void enable_watchdog(void) { WDT_CONTR = 0x34; // 2.3s超时 } void feed_dog(void) { WDT_CONTR |= 0x10; // 喂狗 }建议在任务调度器的顶层循环中定期喂狗,但要注意:
- 关键硬件操作期间不要喂狗
- 喂狗间隔要小于看门狗超时时间
- 调试时可以临时禁用看门狗
6.2 异常恢复机制
对于关键功能,建议实现软件冗余:
unsigned int read_ultrasonic_safe(void) { unsigned int dist = 0; for(int i=0; i<3; i++) { dist = read_ultrasonic(); if(dist > 0 && dist < 500) break; // 有效范围检查 Delay100ms(); } return dist; }这种"尝试多次+有效性校验"的模式,在我的项目中成功解决了很多偶发性的传感器读数异常问题。
7. 工程实践建议
经过多个项目的验证,我总结出几个关键经验点:
定时器分配黄金法则:
- 高频任务用高优先级定时器(如显示刷新)
- 耗时任务用低优先级定时器(如传感器读取)
- 硬件相关功能优先使用专用定时器
中断服务程序设计原则:
- 执行时间尽可能短
- 避免调用其他中断可能使用的函数
- 关键操作前后关闭中断
资源冲突排查步骤:
- 先单独测试每个功能
- 逐步增加并发任务
- 用示波器观察关键信号时序
- 在中断入口/出口设置调试引脚信号
代码组织技巧:
- 为每个硬件功能建立独立模块
- 使用条件编译控制调试输出
- 保持全局变量最少化
在实际开发中,我习惯先用示波器确认各个定时器的中断间隔是否准确,再用逻辑分析仪检查I2C等总线的通信波形。这种"先硬件后软件"的调试顺序,往往能快速定位问题根源。