蓝桥杯单片机学习笔记(十一)—— 调度器大模板构建

温馨提示:内容源自米醋电子工作室培训课,由本人总结

写在前面

本期我们来做一个大整合。前面几期我们分别学习了 LED、数码管、按键、DS1302、DS18B20、PCF8591、AT24C02、超声波、NE555、PWM 调光和串口通信。现在需要把它们全部装进一个工程里,形成一套 “比赛专用大模板”。这套模板最大的亮点是引入了一个调度器,让多个任务可以“和平共处”,互不干扰,再也不用写一大堆减速变量了。

为了让小白也能看懂,我会多用比喻,对代码进行详细的注释(注意:代码本身一个字都不改,注释都写在代码块外面)。最后还会附上一个附录,把各个函数的使用间隔做成表格,方便你查阅。


一、初始化底层

注意事项

在蓝桥杯中,我们需要一个初始化底层,在上电的时候调用初始化函数,来关闭所有的外设。

具体代码

Init.c文件
#include "Init.h"

void System_Init()
{
	//关闭LED
	P0 = 0xff;
  P2 = P2 & 0x1f | 0x80;
  P2 &= 0x1f;

	//关闭继电器,蜂鸣器,电机
  P0 = 0x00;
  P2 = P2 & 0x1f | 0xa0;
  P2 &= 0x1f;
}
Init.h文件
#include <STC15F2K60S2.H>

void System_Init();

二、LED 底层

注意事项

  1. Led_Disp() 函数:新版本中传入的参数只能是 0 或 1(数组元素必须为 0 或 1),用来控制 8 个 LED 的亮灭。
  2. 功能位配置
    • 蜂鸣器:0x40
    • 继电器:0x10
    • 电机:0x20
  3. 新增 Led_Off() 函数:一键关闭所有 LED。以前要用 ucLed 数组一个个清零,现在直接调用它即可,特别适合 PWM 调光时快速熄灭 LED。

底层代码

Led.c
#include "Led.h"

idata unsigned char temp_1 = 0x00;
idata unsigned char temp_1_old = 0xff;

/**
 * @brief 刷新 LED 状态
 * @param ucLed 长度为 8 的数组,元素只能为 0 或 1
 */
void Led_Disp(unsigned char *ucLed)
{
    temp_1 = 0x00;
    temp_1 = (ucLed[0] << 0) | (ucLed[1] << 1) | (ucLed[2] << 2) | (ucLed[3] << 3) |
             (ucLed[4] << 4) | (ucLed[5] << 5) | (ucLed[6] << 6) | (ucLed[7] << 7);
    if(temp_1 != temp_1_old)
    {
        P0 = ~temp_1;               // 低电平点亮
        P2 = (P2 & 0x1f) | 0x80;    // 打开 Y4C(LED 锁存器)
        P2 &= 0x1f;                  // 关闭所有锁存器
        temp_1_old = temp_1;
    }
}

/**
 * @brief 关闭所有 LED
 */
void Led_Off()
{
    P0 = 0xff;                       // 所有 LED 灭
    P2 = (P2 & 0x1f) | 0x80;
    P2 &= 0x1f;
    temp_1_old = 0x00;               // 同步 old 值
}

idata unsigned char temp_2 = 0x00;
idata unsigned char temp_2_old = 0xff;

/**
 * @brief 蜂鸣器控制
 * @param enable 1-响,0-不响
 */
void Beep(bit enable)
{
    if(enable) temp_2 |= 0x40;
    else temp_2 &= ~0x40;
    if(temp_2 != temp_2_old)
    {
        P0 = temp_2;
        P2 = (P2 & 0x1f) | 0xa0;     // 打开 Y5C(蜂鸣器/继电器/电机锁存器)
        P2 &= 0x1f;
        temp_2_old = temp_2;
    }
}

/**
 * @brief 继电器控制
 * @param enable 1-吸合,0-断开
 */
void Relay(bit enable)
{
    if(enable) temp_2 |= 0x10;
    else temp_2 &= ~0x10;
    if(temp_2 != temp_2_old)
    {
        P0 = temp_2;
        P2 = (P2 & 0x1f) | 0xa0;
        P2 &= 0x1f;
        temp_2_old = temp_2;
    }
}

/**
 * @brief 电机控制
 * @param enable 1-启动,0-停止
 */
void MOTOR(bit enable)
{
    if(enable) temp_2 |= 0x20;
    else temp_2 &= ~0x20;
    if(temp_2 != temp_2_old)
    {
        P0 = temp_2;
        P2 = (P2 & 0x1f) | 0xa0;
        P2 &= 0x1f;
        temp_2_old = temp_2;
    }
}
Led.h
#include <STC15F2K60S2.H>

void Led_Disp(unsigned char *ucLed);
void Led_Off();
void Beep(bit enable);
void Relay(bit enable);
void MOTOR(bit enable);

三、数码管底层

注意事项

  1. 小数点显示新方法:不再使用 Seg_Point[] 数组,而是用逗号标记。
    例如:Seg_Buf[1] = 5 + ',',表示第 1 位显示数字 5 并点亮小数点。
    在定时器中断中,判断如果某位数值 > 20,就减去逗号的 ASCII 码(44),然后显示小数点。
  2. 刷新间隔建议20ms

底层代码

Seg.c
#include "Seg.h"

// 0-9 以及熄灭(下标 10 对应 0xff)
pdata unsigned char Seg_Dula[] = {0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0xff};

/**
 * @brief 数码管单次显示(一位)
 * @param wela 位选(0~7)
 * @param dula 段选(0~9 或 10 表示熄灭)
 * @param point 小数点:1-点亮,0-不点亮
 */
void Seg_Disp(unsigned char wela,unsigned char dula,bit point)
{
    // 消隐
    P0 = 0xff;
    P2 = P2 & 0x1f | 0xe0;    // 打开 Y7C(段选锁存器)
    P2 &= 0x1f;

    // 位选
    P0 = 0x01 << wela;
    P2 = P2 & 0x1f | 0xc0;    // 打开 Y6C(位选锁存器)
    P2 &= 0x1f;

    // 段选
    P0 = Seg_Dula[dula];
    if(point) P0 &= 0x7f;      // 点亮小数点(DP = 0)
    P2 = P2 & 0x1f | 0xe0;    // 打开 Y7C
    P2 &= 0x1f;
}
Seg.h
#include <STC15F2K60S2.H>

void Seg_Disp(unsigned char wela,unsigned char dula,bit point);

四、按键底层

注意事项

  1. NE555 频率测量:如果题目要求测频,必须注释掉所有 P34 相关的代码,因为 P34 被频率计占用。
  2. 双按键识别:例如同时按下按键 6 和 7,可以返回一个特殊值(如 67),在主函数中判断。
  3. 按键扫描间隔建议10ms

底层代码

Key.c
#include "Key.h"

/**
 * @brief 读取按键值(支持矩阵按键)
 * @return 按键编号(0~19),0 表示无按键
 */
unsigned char Key_Read()
{
    unsigned char temp = 0;

    // 独立按键(可选)
    // if(P33 == 0) temp = 4;
    // if(P32 == 0) temp = 5;
    // if(P31 == 0) temp = 6;
    // if(P30 == 0) temp = 7;

    // 矩阵按键扫描(逐行拉低)
    P44 = 1; P42 = 1; P35 = 1; // P34 = 1;   // 全部拉高(测频时去掉 P34)
    if(P30 == 0) return 0;

    P44 = 0; P42 = 1; P35 = 1; // P34 = 1;   // 第 1 行拉低
    if(P33 == 0) temp = 4;
    if(P32 == 0) temp = 5;
    if(P31 == 0) temp = 6;
    if(P30 == 0) temp = 7;
    /* 双按键示例:同时按 6 和 7
    if(P31 == 0 && P30 == 0) return 67; */

    P44 = 1; P42 = 1; P35 = 1; // P34 = 1;
    if(P30 == 0) return 0;

    P44 = 1; P42 = 0; P35 = 1; // P34 = 1;   // 第 2 行拉低
    if(P33 == 0) temp = 8;
    if(P32 == 0) temp = 9;
    if(P31 == 0) temp = 10;
    if(P30 == 0) temp = 11;

    P44 = 1; P42 = 1; P35 = 1; // P34 = 1;
    if(P30 == 0) return 0;

    P44 = 1; P42 = 1; P35 = 0; // P34 = 1;   // 第 3 行拉低
    if(P33 == 0) temp = 12;
    if(P32 == 0) temp = 13;
    if(P31 == 0) temp = 14;
    if(P30 == 0) temp = 15;

    // 第 4 行(测频时需注释掉)
    // P44 = 1; P42 = 1; P35 = 1; P34 = 0;   // 第 4 行拉低
    // if(P33 == 0) temp = 16;
    // if(P32 == 0) temp = 17;
    // if(P31 == 0) temp = 18;
    // if(P30 == 0) temp = 19;

    P44 = 1; P42 = 1; P35 = 1; // P34 = 1;
    P3 = P3 | 0xef;             // 恢复 P34 为双向口(11101111)
    return temp;
}
Key.h
#include <STC15F2K60S2.H>

unsigned char Key_Read();

五、DS1302 时钟芯片底层

注意事项

  1. 官方提供的 ds1302.c 需要补充引脚定义和读写函数。
  2. 需要包含 <intrins.h> 头文件,用于 _nop_()
  3. 防重复包含技巧:.h 文件中只声明我们自己写的函数(Set_RtcRead_Rtc),官方函数不用全声明。
  4. 使用间隔建议100ms
  5. 上电时先用 Set_Rtc 写入初始时间,然后在主循环中每隔 100ms 调用 Read_Rtc 读取。

底层代码

ds1302.c
#include "ds1302.h"
#include <intrins.h>

// 引脚定义
sbit SCK = P1 ^ 7;
sbit SDA = P2 ^ 3;
sbit RST = P1 ^ 3;

// 官方底层函数(保持原样)
void Write_Ds1302(unsigned char temp)
{
    unsigned char i;
    for (i=0;i<8;i++)
    {
        SCK = 0;
        SDA = temp&0x01;
        temp>>=1;
        SCK=1;
    }
}

void Write_Ds1302_Byte( unsigned char address,unsigned char dat )
{
    RST=0; _nop_();
    SCK=0; _nop_();
    RST=1; _nop_();
    Write_Ds1302(address);
    Write_Ds1302(dat);
    RST=0;
}

unsigned char Read_Ds1302_Byte ( unsigned char address )
{
    unsigned char i,temp=0x00;
    RST=0; _nop_();
    SCK=0; _nop_();
    RST=1; _nop_();
    Write_Ds1302(address);
    for (i=0;i<8;i++)
    {
        SCK=0;
        temp>>=1;
        if(SDA) temp|=0x80;
        SCK=1;
    }
    RST=0; _nop_();
    SCK=0; _nop_();
    SCK=1; _nop_();
    SDA=0; _nop_();
    SDA=1; _nop_();
    return (temp);
}

/**
 * @brief 写入初始时间(时、分、秒)
 * @param ucRtc 长度为3的数组,[0]=时,[1]=分,[2]=秒(十进制)
 */
void Set_Rtc(unsigned char* ucRtc)
{
    unsigned char i;
    Write_Ds1302_Byte(0x8e,0x00);          // 关闭写保护
    Write_Ds1302_Byte(0x80,0x80);          // 停止振荡
    for(i=0;i<3;i++)
        Write_Ds1302_Byte(0x84 - 2*i, ucRtc[i]/10%10*16 + ucRtc[i]%10); // 十进制转BCD
    Write_Ds1302_Byte(0x8e,0x80);          // 打开写保护
}

/**
 * @brief 读取当前时间
 * @param ucRtc 长度为3的数组,用于存储读取的时、分、秒(十进制)
 */
void Read_Rtc(unsigned char* ucRtc)
{
    unsigned char i;
    unsigned char temp;
    EA = 0;                                 // 关闭总中断(保证时序)
    for(i=0;i<3;i++)
    {
        temp = Read_Ds1302_Byte(0x85 - 2*i);
        ucRtc[i] = temp/16*10 + temp%16;    // BCD转十进制
    }
    EA = 1;                                 // 恢复中断
}
ds1302.h
#include <STC15F2K60S2.H>

void Set_Rtc(unsigned char* ucRtc);
void Read_Rtc(unsigned char* ucRtc);

六、DS18B20 温度芯片底层

注意事项

  1. 官方提供的 onewire.c 需要补充引脚定义和温度读取函数。
  2. 上电后会先显示 85.0℃(芯片复位后的默认值),解决方法是上电后先读取一次,然后延时 750ms 再读取第二次。
  3. 使用间隔建议300ms

底层代码

onewire.c
#include "onewire.h"

sbit DQ = P1 ^ 4;   // 数据线引脚

// 官方延时函数
void Delay_OneWire(unsigned int t)
{
    unsigned char i;
    while(t--){
        for(i=0;i<12;i++);
    }
}

// 官方写函数
void Write_DS18B20(unsigned char dat)
{
    unsigned char i;
    for(i=0;i<8;i++)
    {
        DQ = 0;
        DQ = dat&0x01;
        Delay_OneWire(5);
        DQ = 1;
        dat >>= 1;
    }
    Delay_OneWire(5);
}

// 官方读函数
unsigned char Read_DS18B20(void)
{
    unsigned char i;
    unsigned char dat;
    for(i=0;i<8;i++)
    {
        DQ = 0;
        dat >>= 1;
        DQ = 1;
        if(DQ)
        {
            dat |= 0x80;
        }
        Delay_OneWire(5);
    }
    return dat;
}

// 官方初始化函数
bit init_ds18b20(void)
{
    bit initflag = 0;
    DQ = 1;
    Delay_OneWire(12);
    DQ = 0;
    Delay_OneWire(80);
    DQ = 1;
    Delay_OneWire(10);
    initflag = DQ;
    Delay_OneWire(5);
    return initflag;
}

/**
 * @brief 读取温度值
 * @return 温度值(浮点数,单位℃)
 */
float Read_Temperature()
{
    unsigned char low, high;
    init_ds18b20();
    Write_DS18B20(0xcc);        // 跳过 ROM
    Write_DS18B20(0x44);        // 启动温度转换
    Delay_OneWire(200);         // 等待转换完成
    init_ds18b20();
    Write_DS18B20(0xcc);        // 跳过 ROM
    Write_DS18B20(0xbe);        // 读取暂存器
    low = Read_DS18B20();
    high = Read_DS18B20();
    return (float)(high << 8 | low) / 16.0;
}
onewire.h
#include <STC15F2K60S2.H>

float Read_Temperature();

七、IIC 协议底层(PCF8591 与 AT24C02)

注意事项

  1. 官方提供的 iic.c 需要补充引脚定义、包含 <intrins.h>,并且建议将延时宏 DELAY_TIME 改为 5
  2. PCF8591
    • AD 读取函数:0x41 对应光敏电阻,0x43 对应滑动变阻器。
    • 连续读取时会通道交换,例如先读光敏再读滑动变阻器,结果会互换。解决办法是交换读取顺序或在应用层处理。
    • DA 写入:参数 0~255 对应 0~5V 输出。
    • 使用间隔建议150ms
  3. AT24C02
    • 写入后需加适当延时(例如 10 个 255 循环)保证时序。
    • 读取时建议关闭总中断(EA = 0)防止干扰。
    • 使用间隔无特殊要求,但通常几十毫秒即可。

底层代码

iic.c
#include "iic.h"
#include "intrins.h"

#define DELAY_TIME 5   // 建议改为 5,提高通信稳定性

sbit scl = P2 ^ 0;
sbit sda = P2 ^ 1;

// 官方延时函数
static void I2C_Delay(unsigned char n)
{
    do
    {
        _nop_();_nop_();_nop_();_nop_();_nop_();
        _nop_();_nop_();_nop_();_nop_();_nop_();
        _nop_();_nop_();_nop_();_nop_();_nop_();
    } while(n--);
}

// IIC 起始信号
void I2CStart(void)
{
    sda = 1;
    scl = 1;
    I2C_Delay(DELAY_TIME);
    sda = 0;
    I2C_Delay(DELAY_TIME);
    scl = 0;
}

// IIC 停止信号
void I2CStop(void)
{
    sda = 0;
    scl = 1;
    I2C_Delay(DELAY_TIME);
    sda = 1;
    I2C_Delay(DELAY_TIME);
}

// IIC 发送一个字节
void I2CSendByte(unsigned char byt)
{
    unsigned char i;
    for(i=0; i<8; i++){
        scl = 0;
        I2C_Delay(DELAY_TIME);
        if(byt & 0x80) sda = 1;
        else sda = 0;
        I2C_Delay(DELAY_TIME);
        scl = 1;
        byt <<= 1;
        I2C_Delay(DELAY_TIME);
    }
    scl = 0;
}

// IIC 接收一个字节
unsigned char I2CReceiveByte(void)
{
    unsigned char da;
    unsigned char i;
    for(i=0;i<8;i++){
        scl = 1;
        I2C_Delay(DELAY_TIME);
        da <<= 1;
        if(sda) da |= 0x01;
        scl = 0;
        I2C_Delay(DELAY_TIME);
    }
    return da;
}

// 等待应答信号
unsigned char I2CWaitAck(void)
{
    unsigned char ackbit;
    scl = 1;
    I2C_Delay(DELAY_TIME);
    ackbit = sda;
    scl = 0;
    I2C_Delay(DELAY_TIME);
    return ackbit;
}

// 发送应答信号
void I2CSendAck(unsigned char ackbit)
{
    scl = 0;
    sda = ackbit;
    I2C_Delay(DELAY_TIME);
    scl = 1;
    I2C_Delay(DELAY_TIME);
    scl = 0;
    sda = 1;
    I2C_Delay(DELAY_TIME);
}

/**
 * @brief PCF8591 AD 读取
 * @param addr 通道地址:0x41(光敏),0x43(滑动变阻器)
 * @return 8 位数字量(0~255)
 */
unsigned char Ad_Read(unsigned char addr)
{
    unsigned char temp;
    I2CStart();
    I2CSendByte(0x90);          // 写地址
    I2CWaitAck();
    I2CSendByte(addr);           // 发送控制字(选择通道)
    I2CWaitAck();
    I2CStart();
    I2CSendByte(0x91);           // 读地址
    I2CWaitAck();
    temp = I2CReceiveByte();
    I2CSendAck(1);               // 非应答
    I2CStop();
    return temp;
}

/**
 * @brief PCF8591 DA 输出
 * @param dat 0~255 对应 0~5V
 */
void Da_Write(unsigned char dat)
{
    I2CStart();
    I2CSendByte(0x90);
    I2CWaitAck();
    I2CSendByte(0x40);           // 使能 DA 输出
    I2CWaitAck();
    I2CSendByte(dat);             // 写入数字值
    I2CWaitAck();
}

/**
 * @brief AT24C02 写入多个字节
 * @param str 数据源数组
 * @param addr 起始地址(0~255)
 * @param num 写入字节数
 */
void EEPROM_Write(unsigned char* str,unsigned char addr,unsigned char num)
{
    I2CStart();
    I2CSendByte(0xa0);           // 器件地址 + 写
    I2CWaitAck();
    I2CSendByte(addr);            // 写入起始地址
    I2CWaitAck();
    while(num--)
    {
        I2CSendByte(*str++);
        I2CWaitAck();
        I2C_Delay(200);           // 保证内部写周期完成
    }
    I2CStop();
    // 额外延时,确保时序稳定
    I2C_Delay(255); I2C_Delay(255); I2C_Delay(255); I2C_Delay(255); I2C_Delay(255);
    I2C_Delay(255); I2C_Delay(255); I2C_Delay(255); I2C_Delay(255); I2C_Delay(255);
}

/**
 * @brief AT24C02 读取多个字节
 * @param str 存放读取数据的数组
 * @param addr 起始地址
 * @param num 读取字节数
 */
void EEPROM_Read(unsigned char* str,unsigned char addr,unsigned char num)
{
    I2CStart();
    I2CSendByte(0xa0);           // 写地址
    I2CWaitAck();
    I2CSendByte(addr);            // 起始地址
    I2CWaitAck();
    I2CStart();
    I2CSendByte(0xa1);           // 读地址
    I2CWaitAck();
    EA = 0;                       // 关总中断,保证连续读取
    while(num--)
    {
        *str++ = I2CReceiveByte();
        if(num) I2CSendAck(0);    // 发送应答(继续读)
        else I2CSendAck(1);       // 最后字节发送非应答
    }
    I2CStop();
    EA = 1;
}
iic.h
#include <STC15F2K60S2.H>

unsigned char Ad_Read(unsigned char addr);
void Da_Write(unsigned char dat);
void EEPROM_Write(unsigned char* str,unsigned char addr,unsigned char num);
void EEPROM_Read(unsigned char* str,unsigned char addr,unsigned char num);

八、超声波测距底层

注意事项

  1. 官方没有提供超声波底层,需要自己编写。根据原理图定义引脚:P1^0 为发送(TX),P1^1 为接收(RX)。
  2. 用 STC-ISP 生成 12MHz 下的 12μs 延时函数(指令集 Y1)。
  3. 发射 8 个 40kHz 脉冲,然后开启定时器 0(计数模式)测量回波时间。
  4. 使用间隔建议120ms

底层代码

ultrasound.c
#include "ultrasound.h"
#include "intrins.h"

sbit US_TX = P1 ^ 0;   // 发送引脚
sbit US_RX = P1 ^ 1;   // 接收引脚

/**
 * @brief 12μs 延时(用于产生 40kHz 脉冲)
 */
void Delay12us(void)    //@12.000MHz
{
    unsigned char data i;
    _nop_();
    i = 3;
    while (--i);
}

/**
 * @brief 发送 8 个 40kHz 脉冲
 */
void Ut_Wave_Init()
{
    unsigned char i;
    EA = 0;                      // 关中断,保证时序
    for(i=0;i<8;i++)
    {
        US_TX = 1;
        Delay12us();
        US_TX = 0;
        Delay12us();
    }
    EA = 1;
}

/**
 * @brief 测量距离
 * @return 距离(单位 cm),0 表示超出量程或无回波
 */
unsigned char Ut_Wave_Data()
{
    unsigned int time;
    CMOD = 0x00;                 // 定时器 0 工作在 12T 模式
    CH = CL = 0;                 // 计数器清零
    Ut_Wave_Init();              // 发射超声波
    CR = 1;                      // 启动计数器(定时器 0 开始计数)
    while(US_RX == 1 && CF == 0); // 等待回波或溢出
    CR = 0;                      // 停止计数
    if(CF == 0)                  // 正常接收到回波
    {
        time = CH << 8 | CL;
        return (time * 0.017);    // 距离 = 时间 * 声速 / 2 (单位 cm)
    }
    else                         // 溢出(超时)
    {
        CF = 0;
        return 0;
    }
}
ultrasound.h
#include <STC15F2K60S2.H>

unsigned char Ut_Wave_Data();

九、NE555 频率测量

注意事项

  1. 将定时器 0 配置为计数模式TMOD |= 0x05),用于统计外部脉冲。
  2. 定时器 1 用作 1ms 时基,累计 1000 次(即 1 秒)后读取定时器 0 的计数值,即为频率。
  3. 初始化顺序:先初始化定时器 0,再初始化定时器 1
  4. 频率值存放在全局变量 Freq 中,标志位 Freq_Flag 为 1 时表示有新的频率值。

相关代码(main.c 片段)

// 全局变量
idata unsigned int Freq;       // 频率值(单位 Hz)
idata unsigned int Time_1s;    // 1 秒计时器
idata bit Freq_Flag;           // 频率有效标志

// 定时器 0 初始化(计数模式)
void Timer0_Init(void)
{
    AUXR &= 0x7F;
    TMOD &= 0xF0;
    TMOD |= 0x05;    // 计数模式,16 位不自动重装
    TL0 = 0x00;
    TH0 = 0x00;
    TF0 = 0;
    TR0 = 1;         // 启动计数器
}

// 定时器 1 初始化(1ms 时基)
void Timer1_Init(void)
{
    AUXR &= 0xBF;
    TMOD &= 0x0F;
    TMOD |= 0x10;    // 定时模式,16 位不自动重装
    TL1 = 0x18;      // 12MHz 下 1ms 初始值
    TH1 = 0xFC;
    TF1 = 0;
    TR1 = 1;
    ET1 = 1;
    EA = 1;
}

// 定时器 1 中断服务函数
void Timer1_Isr(void) interrupt 3
{
    if(++Time_1s == 1000)   // 1 秒到
    {
        Time_1s = 0;
        Freq = (TH0 << 8) | TL0;   // 读取计数值
        TH0 = TL0 = 0;              // 清零计数器
        Freq_Flag = 1;              // 置标志
    }
    // ... 其他中断处理(数码管扫描、PWM 等)
}

十、PWM 调光

注意事项

  1. 通过定时器中断实现软件 PWM,控制 LED 亮度。
  2. 定义两个变量:
    • pwm_period:周期计数器(0~9),每个中断自动递增并取模 10。
    • pwm_compare:比较值(0~9),决定占空比。
  3. 在定时器中断中判断:pwm_period < pwm_compare 时点亮 LED,否则熄灭。
  4. 点亮 LED 时需调用 Led_Disp(ucLed)(但仅当显示模式为 4 时才允许 PWM 调光)。

相关代码(main.c 片段)

idata unsigned char pwm_period;
idata unsigned char pwm_compare = 6;   // 默认亮度 60%

// 定时器 1 中断服务函数(片段)
void Timer1_Isr(void) interrupt 3
{
    // ... 其他中断处理

    // PWM 调光逻辑
    pwm_period = (++pwm_period) % 10;
    if(pwm_period < pwm_compare)
    {
        if(Seg_Disp_Mode == 4)          // 只在模式 4 下进行 PWM 调光
            Led_Disp(ucLed);             // 点亮
    }
    else
    {
        Led_Off();                       // 熄灭
    }
}

十一、串口通信底层

注意事项

  1. 使用 STC-ISP 的“波特率计算器”生成初始化代码,选择定时器 2 作为波特率发生器,波特率 9600,12MHz。
  2. 重定义 putchar 函数,实现 printf 输出。
  3. 定义接收缓冲区、索引和超时计数器,实现非阻塞接收
  4. 使用间隔建议10ms 处理一次接收到的数据。

底层代码

Uart.c
#include <stdio.h>     // 需要使用 printf

/**
 * @brief 串口初始化(9600bps, 定时器2)
 */
void Uart1_Init(void)
{
    SCON = 0x50;        // 8 位数据,可变波特率
    AUXR |= 0x01;       // 选择定时器 2 作为波特率发生器
    AUXR &= 0xFB;       // 定时器 2 时钟 12T 模式
    T2L = 0xE6;         // 初始值(9600bps @12MHz)
    T2H = 0xFF;
    AUXR |= 0x10;       // 启动定时器 2
    ES = 1;             // 使能串口中断
    EA = 1;
}

/**
 * @brief 重定向 putchar,支持 printf
 */
char putchar(char ch)
{
    SBUF = ch;
    while(TI == 0);
    TI = 0;
    return ch;
}

// 串口接收相关变量
idata unsigned char Uart_Rx_Index;          // 接收缓冲区索引
pdata unsigned char Uart_Rx_Buf[10];        // 接收缓冲区
idata unsigned char Uart_Rx_Flag;           // 接收标志(有数据)
idata unsigned char Uart_Rx_Tick;            // 超时计时

/**
 * @brief 串口中断服务函数
 */
void Uart1_Isr(void) interrupt 4
{
    if (RI)
    {
        Uart_Rx_Flag = 1;
        Uart_Rx_Tick = 0;                     // 重置超时计数器
        Uart_Rx_Buf[Uart_Rx_Index++] = SBUF;
        RI = 0;
        if(Uart_Rx_Index > 10)                // 缓冲区溢出保护
        {
            Uart_Rx_Index = 0;
            memset(Uart_Rx_Buf, 0, 10);
        }
    }
}

十二、调度器——任务的“智能排班系统”

为什么要用调度器?

以前我们写单片机程序,常常在 while(1) 里放一堆函数,再用各种减速变量控制执行频率。但这种方法有个隐患:如果某个函数执行时间稍长,就可能错过某些任务的执行时机。

现在引入一个调度器,它就像一个工厂里的工头,手里拿着一份“任务排班表”,定期检查每个任务“到点了没”,到了就叫它干活,没到就继续等。这样每个任务都能按照自己设定的周期执行,互不干扰。

调度器的数据结构

首先,我们定义一张“工牌模板”——task_t 结构体:

typedef struct
{
    void(*task_func)(void);      // 任务函数指针(干活的工人)
    unsigned long int rate_ms;    // 执行周期(单位:ms)(多久干一次活)
    unsigned long int last_ms;    // 上次执行的时间(上次干活的时间戳)
} task_t;

这个结构体把三个信息打包在一起:谁干活干活的间隔上次啥时候干的

填写任务清单

接下来,我们创建一个数组 Scheduler_Task[],把所有的任务填进去,就像工头手里的“花名册”:

idata task_t Scheduler_Task[] = 
{
    {Seg_Proc,         20, 0},   // 数码管刷新,每 20ms 一次
    {Key_Proc,         10, 0},   // 按键扫描,每 10ms 一次
    {Led_Proc,          1, 0},   // LED 状态更新,每 1ms 一次
    {Get_Time,        100, 0},   // 读取时钟,每 100ms 一次
    {Get_Temperature, 300, 0},   // 读取温度,每 300ms 一次
    {AD_DA,           150, 0},   // AD/DA 转换,每 150ms 一次
    {Get_Distance,    120, 0},   // 超声波测距,每 120ms 一次
    {Uart_Proc,        10, 0},   // 串口数据处理,每 10ms 一次
};

每一行就是一张填好的“工牌”,第三列 last_ms 初始为 0,表示从未执行过。

时间基准——uwTick

调度器需要一个“全局时钟”,我们用一个变量 uwTick 来记录从开机到现在经过的毫秒数。这个变量在定时器 1 的中断里每 1ms 加 1。

idata unsigned long int uwTick;   // 毫秒级时间戳,最大可计 49 天

void Timer1_Isr(void) interrupt 3
{
    uwTick++;   // 每 1ms 加 1
    // ... 其他中断处理
}

初始化调度器

我们写一个初始化函数,自动计算任务的数量,这样以后增减任务时不用手动改数字。

idata unsigned char task_num;

void Scheduler_Init()
{
    task_num = sizeof(Scheduler_Task) / sizeof(task_t);
}

调度器运行核心

while(1) 主循环里,工头不停地检查每一个任务:

void Scheduler_Run()
{
    unsigned char i;
    for(i=0; i<task_num; i++)
    {
        unsigned long int now_time = uwTick;   // 获取当前时间
        if(now_time >= (Scheduler_Task[i].rate_ms + Scheduler_Task[i].last_ms))
        {
            // 到点了!干活!
            Scheduler_Task[i].last_ms = now_time;      // 记录本次干活时间
            Scheduler_Task[i].task_func();             // 调用任务函数
        }
    }
}

判断逻辑很简单:当前时间 ≥ 上次执行时间 + 执行周期,就执行。

为什么这种调度器更好?

  • 每个任务独立计时,互不干扰。
  • 周期精准,不会因为某个任务卡顿而错过执行。
  • 易于扩展,新增任务只需在数组中加一行,初始化函数自动计算数量。
  • 主循环里没有 delay,CPU 利用率高。

附录

附录 A:各函数使用间隔建议表

下表汇总了各个模块推荐的调用间隔,你可以根据实际题目要求适当调整。

函数名 功能描述 建议调用间隔 (ms) 备注
Seg_Proc 数码管数据更新 20 人眼视觉暂留,20ms 足够
Key_Proc 按键扫描 10 防抖需要,太短容易误判
Led_Proc LED 状态更新 1 快速响应按键/蜂鸣器/继电器
Get_Time 读取 DS1302 时间 100 时间变化慢,没必要太频繁
Get_Temperature 读取 DS18B20 温度 300 温度变化慢,且转换需要时间
AD_DA AD 采集与 DA 输出 150 兼顾实时性与 CPU 负载
Get_Distance 超声波测距 120 测一个周期约几十 ms
Uart_Proc 串口数据处理 10 及时响应上位机指令
定时器 1 中断 数码管扫描、PWM 1 必须 1ms,作为系统时基
定时器 0(计数模式) 频率测量 连续计数 配合定时器 1 每秒读取一次

附录 B:main.c 完整代码

/* 头文件声明区域 */
#include <STC15F2K60S2.H>
#include <string.h>
#include <stdio.h>
#include "Led.h"
#include "Init.h"
#include "Seg.h"
#include "Key.h"
#include "ds1302.h"
#include "onewire.h"
#include "iic.h"
#include "ultrasound.h"
#include "Uart.h"

/* 变量声明区域 */
idata unsigned long int uwTick;//调度器专用变量,上限49天

pdata unsigned char ucLed[8] = {1,1,1,1,0,0,0,0};//LED显示数据存放数组

idata bit Relay_Flag = 0;//继电器使能标志位
idata bit Beep_Flag = 0;//蜂鸣器使能标志位

pdata unsigned char Seg_Buf[8] = {10,10,10,10,10,10,10,10};//数码管显示数据存放数组
idata unsigned char Seg_Pos = 0;//数码管扫描数据存放变量

idata unsigned char Key_Val,Key_Old,Key_Down,Key_Up;//按键扫描专用变量

pdata unsigned char ucRtc[3] = {11,12,13};//时钟芯片时分秒数据储存数组

idata unsigned int Temperature_10X;//温度实时变量的10倍

idata unsigned char Ad_1_Data_10x,Ad_3_Data_10x;//ADDA数据存储变量
idata unsigned char DA_Data;//DA电压输出值数据存储变量

pdata unsigned char EEPROM_Data_W[8] = {1,2,3,4,5,6,7,8};//存储芯片写入数组
pdata unsigned char EEPROM_Data_R[8] = {0,0,0,0,0,0,0,0};//存储芯片读取数组

idata unsigned char Distance;//超声波测量距离数据存储变量

idata unsigned int Freq;//频率数据存储变量
idata unsigned int Time_1s;//1s计时变量

idata unsigned char pwm_period;//pwm周期数据存储变量,累加对比
idata unsigned char pwm_compare = 6;//pwm比较值数据存储变量,也就是亮度
  
idata unsigned char Uart_Rx_Index;//串口索引值
pdata unsigned char Uart_Rx_Buf[10] = {10,10,10,10,10,10,10,10,10,10};//串口接收缓冲区
idata unsigned char Uart_Rx_Flag;//串口接收标志位
idata unsigned char Uart_Rx_Tick;//超时解析计时器

idata unsigned char Seg_Disp_Mode = 0;//0-时间,1-温度,2-AD,3-超声波,5-频率,4-pwm参数

/* 信息处理函数 */
void Seg_Proc()
{
	unsigned char i;//循环控制变量
	
	//DS1302
//	Seg_Buf[0] = ucRtc[0] / 10 % 10;
//	Seg_Buf[1] = ucRtc[0] % 10;
//	Seg_Buf[3] = ucRtc[1] / 10 % 10;
//	Seg_Buf[4] = ucRtc[1] % 10;
//	Seg_Buf[6] = ucRtc[2] / 10 % 10;
//	Seg_Buf[7] = ucRtc[2] % 10;
	
	//DS18B20
//	Seg_Buf[0] = Temperature_10X / 100 % 10;
//	Seg_Buf[1] = (Temperature_10X / 10 % 10) + ',';
//	Seg_Buf[2] = Temperature_10X % 10;
	
	//PCF8591
//	Seg_Buf[0] = Ad_1_Data_10x / 10 % 10 + ',';
//	Seg_Buf[1] = Ad_1_Data_10x % 10;
//	Seg_Buf[6] = Ad_3_Data_10x / 10 % 10 + ',';
//	Seg_Buf[7] = Ad_3_Data_10x % 10;

	//Ultrasound
//	Seg_Buf[0] = Distance / 100 % 10;
//	Seg_Buf[1] = Distance / 10 % 10;
//	Seg_Buf[2] = Distance % 10;
	
	switch(Seg_Disp_Mode)
	{
		case 0://时间界面
			Seg_Buf[0] = ucRtc[0] / 10 % 10;
			Seg_Buf[1] = ucRtc[0] % 10;
			Seg_Buf[2] = 10 + ',';
			Seg_Buf[3] = ucRtc[1] / 10 % 10;
			Seg_Buf[4] = ucRtc[1] % 10;
			Seg_Buf[5] = 10 + ',';
			Seg_Buf[6] = ucRtc[2] / 10 % 10;
			Seg_Buf[7] = ucRtc[2] % 10;
		break;
		case 1://温度界面
			Seg_Buf[0] = Temperature_10X / 100 % 10;
			Seg_Buf[1] = (Temperature_10X / 10 % 10) + ',';
			Seg_Buf[2] = Temperature_10X % 10;
			for(i=3;i<8;i++)
				Seg_Buf[i] = 10;
		break;
		case 2://ADDA界面
			Seg_Buf[0] = Ad_1_Data_10x / 10 % 10 + ',';
			Seg_Buf[1] = Ad_1_Data_10x % 10;
			Seg_Buf[6] = Ad_3_Data_10x / 10 % 10 + ',';
			Seg_Buf[7] = Ad_3_Data_10x % 10;
			Seg_Buf[2] = 10;
		break;
		case 3://超声波界面
			Seg_Buf[0] = (Distance > 100)?Distance / 100 % 10:10;
			Seg_Buf[1] = (Distance > 10)?Distance / 10 % 10:10;
			Seg_Buf[2] = Distance % 10;
			Seg_Buf[6] = Seg_Buf[7] = 10;
		break;
		case 4://pwm调光
			if(pwm_compare == 9)
			{
				Seg_Buf[0] = 1;
				Seg_Buf[1] = 0;
				Seg_Buf[2] = Seg_Buf[6] = 10;
			}
			else
			{
				Seg_Buf[0] = pwm_compare + 1;
				Seg_Buf[1] = Seg_Buf[2] = Seg_Buf[6] = 10;
			}
		break;
		case 5://频率
			Seg_Buf[0] = (Freq > 1000000)?Freq  / 1000000 % 10:10;
			Seg_Buf[1] = (Freq > 100000)?Freq  / 100000 % 10:10;
			Seg_Buf[2] = (Freq > 10000)?Freq  / 10000 % 10:10;
			Seg_Buf[3] = (Freq > 1000)?Freq  / 1000 % 10:10;
			Seg_Buf[4] = (Freq > 100)?Freq  / 100 % 10:10;
			Seg_Buf[5] = Freq  / 10 % 10;
			Seg_Buf[6] = Freq % 10;
		break;
	}
	
}

/* 按键处理函数 */
void Key_Proc()
{
	Key_Val = Key_Read();//读取键码值
	Key_Down = Key_Val & (Key_Val ^ Key_Old);//扫描下降沿
	Key_Up = ~Key_Val & (Key_Val ^ Key_Old);//扫描上升沿
	Key_Old = Key_Val;//辅助扫描
	
//	if(Key_Down == 4)
//		pwm_compare = (++pwm_compare) % 10;
	
	if(Key_Down != 0)
		printf("Key_Down=%bu",Key_Down);
	
//	if(Key_Down == 5)
//		Seg_Disp_Mode = (++Seg_Disp_Mode) % 5;
	
	switch(Key_Down)
	{
		case 4:
			if(Seg_Disp_Mode == 4)
				pwm_compare = (++pwm_compare) % 10;
		break;
		case 5:
			Seg_Disp_Mode = (++Seg_Disp_Mode) % 6;
		break;
		case 6:
			Beep_Flag ^= 1;
		break;
		case 7:
			Relay_Flag ^= 1;
		break;
	}
	
	
}

/* Led处理函数 */
void Led_Proc()
{
	//Led_Disp(ucLed);
	//若无PWM调光,则直接将Led显示函数写在LED处理函数里
	Beep(Beep_Flag);
	Relay(Relay_Flag);
	
}

/* 时钟芯片函数 */
void Get_Time()
{
	Read_Rtc(ucRtc);
}

/* 温度芯片函数 */
void Get_Temperature()
{
	Temperature_10X = Read_Temperature() * 10;
}

/* ADDA函数 */
void AD_DA()
{
	Ad_1_Data_10x = Ad_Read(0x41) * 10 / 51;
	Ad_3_Data_10x = Ad_Read(0x43) * 10 / 51;
	
	Da_Write(DA_Data);
}

/* 超声波测距函数 */
void Get_Distance()
{
	Distance = Ut_Wave_Data();
}

/* 串口处理函数 */
void Uart_Proc()
{
	unsigned char x,y;
	if(Uart_Rx_Index == 0) return;
	if(Uart_Rx_Tick >= 10)//超过了10ms
	{
		Uart_Rx_Flag = 0;
		Uart_Rx_Tick = 0;//复位
		
		printf("%s",Uart_Rx_Buf);//回显,确保预期接收和真实接收一致
		if(sscanf(Uart_Rx_Buf,"(%bu,%bu)",&x,&y) == 2)
			printf("I Get x=%bu,y=%bu\r\n",x,y);
		else
			printf("ERROR\r\n");
		
		memset(Uart_Rx_Buf,0,Uart_Rx_Index);
		Uart_Rx_Index = 0;
			
	}
}


/* 定时器1中断服务函数 */ 
void Timer1_Isr(void) interrupt 3
{
	uwTick++;
	
	Seg_Pos = (++Seg_Pos) % 8;
	if(Seg_Buf[Seg_Pos] > 20)//我们的底层没有20的下标(超出上限),也就代表我们加了1个逗号,就显示小数点
		Seg_Disp(Seg_Pos,Seg_Buf[Seg_Pos] - ',',1);
	else//正常显示
		Seg_Disp(Seg_Pos,Seg_Buf[Seg_Pos],0);  
		
	if(++Time_1s == 1000)
	{
		Time_1s = 0;
		Freq = TH0 << 8 | TL0;
		TH0 = TL0 = 0;
	}
	
	//调光
	pwm_period = (++pwm_period) % 10;
	if(pwm_period < pwm_compare)
	{
		if(Seg_Disp_Mode == 4)
			Led_Disp(ucLed);//如果不调光,这个函数要放在Led_Proc()里
	}
	else
		Led_Off();
	
	if(Uart_Rx_Flag) Uart_Rx_Tick++;
		
}

/* 定时器初始化函数 */
void Timer1_Init(void)		//1毫秒@11.0592MHz
{
	AUXR &= 0xBF;			//定时器时钟12T模式
	TMOD &= 0x0F;			//设置定时器模式
	TL1 = 0x66;				//设置定时初始值
	TH1 = 0xFC;				//设置定时初始值
	TF1 = 0;				//清除TF1标志
	TR1 = 1;				//定时器1开始计时
	ET1 = 1;				//使能定时器1中断
	EA = 1;
}

/* 定时器0初始化函数 */
void Timer0_Init(void)		//1毫秒@12.000MHz
{
	AUXR &= 0x7F;			//定时器时钟12T模式
	TMOD &= 0xF0;			//设置定时器模式
	TMOD |= 0x05;			//开启计数器模式,且不自动重装
	TL0 = 0x00;				//设置定时初始值
	TH0 = 0x00;				//设置定时初始值
	TF0 = 0;				//清除TF0标志
	TR0 = 1;				//定时器0开始计时
}

//串口中断服务函数
void Uart1_Isr(void) interrupt 4
{
	if (RI)				//检测串口1接收中断
	{
		Uart_Rx_Flag = 1;
		Uart_Rx_Tick = 0;//重置计数器
		Uart_Rx_Buf[Uart_Rx_Index++] = SBUF;
		RI = 0;			//清除串口1接收中断请求位
		
		if(Uart_Rx_Index > 10)//溢出
		{
			Uart_Rx_Index = 0;
			memset(Uart_Rx_Buf,0,10);//批量清空数组,这个函数是<string.h>函数里的
		}
	}
}

/* 调度器 */
typedef struct
{
	void(*task_func)(void);//任务函数
	unsigned long int rate_ms;//任务执行的周期
	unsigned long int last_ms;//上一次执行任务的时间
}	task_t;

idata task_t Scheduler_Task[] = {
	{Seg_Proc,20,0},
	{Key_Proc,10,0},
	{Led_Proc,1,0},
	{Get_Time,100,0},
	{Get_Temperature,300,0},
	{AD_DA,150,0},
	{Get_Distance,120,0},
	{Uart_Proc,10,0},
};

idata unsigned char task_num;//调度器任务数量
void Scheduler_Init()
{
	task_num = sizeof(Scheduler_Task) / sizeof(task_t);
}
void Scheduler_Run()
{
	unsigned char i;//循环控制变量
	for(i=0;i<task_num;i++)
	{
		unsigned long int now_time = uwTick;//获取当前时间
		if(now_time >= (Scheduler_Task[i].rate_ms + Scheduler_Task[i].last_ms))
		{
			Scheduler_Task[i].last_ms = now_time;
			Scheduler_Task[i].task_func();
		}
	}
}


/* Main */
void main()
{
	System_Init();
	Set_Rtc(ucRtc);
	
	Timer0_Init();
	Scheduler_Init();
	Uart1_Init();
	Timer1_Init();
	while(1)
	{
		Scheduler_Run();
	}
}

注:附录 A 的表格仅供参考,实际比赛时请根据题目具体要求调整周期。例如,如果题目要求温度每 1 秒刷新一次,可将 Get_Temperaturerate_ms 改为 1000。

Logo

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

更多推荐