蓝桥杯单片机省赛“最难”14届:定时器中断与状态机的实战精要

第一次拿到14届蓝桥杯单片机省赛题目时,我盯着开发板上密密麻麻的外设接口倒吸一口凉气——DS1302时钟芯片、DS18B20温度传感器、光敏电阻、555定时器、矩阵键盘、8位数码管、LED阵列,这些都需要在有限的内存和时钟周期内协同工作。更棘手的是,题目要求所有功能模块必须实现实时响应无阻塞刷新,这对传统的前后台编程模式提出了严峻挑战。经过72小时的反复调试,最终让我突破瓶颈的,是定时器中断调度状态机架构的深度结合。本文将分享如何用这两种核心技术,在51单片机的苛刻资源限制下构建出稳定可靠的工业级控制系统。

1. 系统架构设计的核心矛盾

面对多外设协同工作的复杂系统,开发者首先需要解决三个基本矛盾:

  1. 实时性要求与CPU单线程特性:当数码管需要每5ms刷新一次、温度传感器每800ms采集一次、键盘需要每50ms扫描一次时,传统的while(1)轮询架构必然导致某些任务错过最佳执行时机
  2. 外设响应延迟与系统吞吐量:DS18B20温度转换需要750ms,期间若CPU被阻塞,其他外设将失去响应能力
  3. 代码可维护性与功能扩展需求:直接面向过程编程会导致各功能模块深度耦合,后期添加新功能时牵一发而动全身

在14届省赛的实战中,我采用时间片轮转的思想重构了整个系统框架。通过配置定时器0和定时器1的工作模式,建立了三个关键时间基准:

void init_timer01() {
    AUXR &= 0xBF;  // 定时器时钟12T模式
    TMOD = 0x06;   // 设置定时器模式
    TL1 = 0xCE;    // 50μs定时初值
    TH1 = 0xFF;    
    TR1 = 1;       // 启动定时器1
    EA = 1;        // 全局中断使能
    ET1 = 1;       // 定时器1中断使能
}

这个配置产生了每50μs触发一次的硬件中断,成为整个系统的"心跳"。基于此心跳,我们可以派生出不同粒度的时间片:

时间片类型 产生方式 应用场景
5ms 100次50μs累积 数码管动态扫描
100ms 2000次50μs累积 LED状态检测
1s 10次100ms累积 环境参数记录

2. 状态机实现多任务调度

定时器中断解决了时间基准问题,但如何管理十余个外设的协同工作?状态机(State Machine)架构给出了优雅的解决方案。在14届赛题中,我设计了六种主要工作状态:

enum {
    STATE_CLOCK = 1,    // 时钟显示
    STATE_TEMP_STAT,    // 温度统计
    STATE_HUMI_STAT,    // 湿度统计
    STATE_TRIG_RECORD,  // 触发记录
    STATE_TEMP_SET,     // 温度设置
    STATE_ALERT         // 异常警报
};

unsigned char SMG_flag = STATE_CLOCK;  // 初始状态

每个状态对应独立的显示逻辑和处理规则。以温度统计状态为例,其显示逻辑通过状态迁移表实现:

void flash_SMG() {
    switch(SMG_flag) {
        case STATE_TEMP_STAT:
            if(trg_count == 0) {
                display_char(0, 12);  // 显示'T'
            } else {
                display_number(2, max_temperature/100);
                display_number(3, (max_temperature/10)%10);
                display_symbol(4, '-');
                display_float(5, average_temperature);
            }
            break;
        // 其他状态处理...
    }
}

状态之间的转换由按键事件触发,通过有限状态机(FSM)确保逻辑清晰:

[时钟显示] --S4--> [温度统计] --S4--> [温度设置] --S4--> [时钟显示]
  ↑                   |                   |
  |__S5_______________|___________________|

3. 标志位法的精妙运用

在多中断环境下,直接操作外设可能导致资源冲突。为此,我采用了标志位中转的策略,所有外设操作都遵循"中断置位-主循环执行"的原则:

  1. 数据采集标志:定时器中断设置采集需求

    bit flash_ds18b20 = 0;
    void timer1_service() interrupt 3 {
        if(count_50us % 50 == 0) {
            flash_ds18b20 = 1;  // 触发温度采集
        }
    }
    
  2. 主循环响应标志:避免在中断中执行耗时操作

    void main() {
        while(1) {
            if(flash_ds18b20) {
                read_temperature();  // 实际采集函数
                flash_ds18b20 = 0;
            }
        }
    }
    
  3. 显示刷新标志:确保数码管稳定显示

    void timer1_service() interrupt 3 {
        if(++flash_count > 7) flash_count = 0;
        need_refresh = 1;
    }
    

这种架构带来了三个显著优势:

  • 中断服务程序保持极短执行时间(通常<50μs)
  • 外设操作原子性得到保证
  • 各功能模块实现松耦合

4. 关键外设的实战技巧

4.1 DS18B20温度传感器的稳定读取

单总线器件对时序要求极为严格,在中断环境中需要特殊处理:

void flash_temperature() {
    if(flash_ds18b20) {
        unsigned char LSB, MSB;
        old_temperature = temperature;
        
        init_ds18b20();
        write_byte(0xCC);  // 跳过ROM
        write_byte(0x44);  // 启动转换
        
        Delay700ms();       // 保证转换完成
        
        init_ds18b20();
        write_byte(0xCC);
        write_byte(0xBE);   // 读取暂存器
        LSB = read_byte();
        MSB = read_byte();
        
        temperature = ((MSB << 8) | LSB) >> 4;
        temperature = temperature * 10 + (LSB & 0x0F) * 0.625;
        
        flash_ds18b20 = 0;
    }
}

避坑指南

  • 转换完成后必须重新初始化总线
  • 小数部分处理建议使用整数运算(避免浮点开销)
  • 每次读取前校验数据有效性(如85℃为默认值)

4.2 矩阵键盘的防抖处理

在中断驱动架构下,键盘扫描需要特殊设计:

void keyrunning() {
    static unsigned char last_key = 0;
    static unsigned char debounce_cnt = 0;
    
    scan_keyboard();  // 扫描当前按键
    
    if(current_key != last_key) {
        debounce_cnt++;
        if(debounce_cnt >= 3) {  // 连续3次检测一致
            process_key(current_key);
            last_key = current_key;
        }
    } else {
        debounce_cnt = 0;
    }
}

4.3 数码管的多模式显示

不同状态下的数码管需要差异化刷新策略:

状态 刷新频率 显示内容 特殊处理
时钟显示 5ms 时:分:秒 秒闪烁效果
温度统计 10ms 最大温度-平均温度 无触发时显示'T'
异常警报 20ms 当前温湿度 低于阈值显示'--'
void flash_SMG() {
    static unsigned char pos = 0;
    
    switch(SMG_flag) {
        case STATE_CLOCK:
            display_colon(flag_100ms);  // 秒闪烁效果
            if(++pos >= 8) pos = 0;
            display_digit(pos, time_data[pos]);
            break;
        // 其他状态处理...
    }
}

5. 性能优化与调试心得

在资源受限的51单片机上实现复杂功能,每个字节和时钟周期都弥足珍贵。以下是几个关键优化点:

  1. 变量类型选择

    • 温度值使用unsigned int存储(范围0-999)
    • 标志位使用bit类型(1字节可存8个标志)
    • 频繁访问的全局变量加xdata关键字
  2. 中断服务程序优化

    • 禁用不必要的中断嵌套
    • 关键代码用#pragma disable保护
    • 使用_nop_()精确控制时序
  3. 内存管理技巧

    unsigned char xdata display_buf[8];  // 数码管显示缓存
    unsigned char idata key_state[4];    // 键盘状态缓存
    
  4. 调试辅助手段

    • 利用未使用的IO口输出调试信号
    • 编写内存检查函数防止溢出
    • 建立看门狗复位计数器

在最终实现的系统中,所有外设的响应延迟都控制在设计要求的10%以内,CPU利用率保持在70%左右,为后续功能扩展预留了充足资源。这个案例证明,即使在8位单片机上,通过合理的架构设计也能实现接近现代操作系统的多任务管理能力。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐