蓝桥杯单片机学习笔记(十一)—— 调度器大模板构建
本文介绍了一个用于蓝桥杯单片机比赛的集成化调度器模板,整合了LED、数码管、按键、传感器等外设驱动。模板采用调度器架构实现多任务协同运行,避免了传统延时变量方法的问题。详细讲解了各模块的初始化、LED控制(含蜂鸣器/继电器/电机)、数码管显示(含小数点处理)、按键扫描(支持矩阵按键和双按键识别)等核心功能的实现方法,并提供了优化建议如20ms刷新间隔、NE555测频注意事项等。所有代码均配有详细注
蓝桥杯单片机学习笔记(十一)—— 调度器大模板构建
温馨提示:内容源自米醋电子工作室培训课,由本人总结
写在前面
本期我们来做一个大整合。前面几期我们分别学习了 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 底层
注意事项
Led_Disp()函数:新版本中传入的参数只能是 0 或 1(数组元素必须为 0 或 1),用来控制 8 个 LED 的亮灭。- 功能位配置:
- 蜂鸣器:
0x40 - 继电器:
0x10 - 电机:
0x20
- 蜂鸣器:
- 新增
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);
三、数码管底层
注意事项
- 小数点显示新方法:不再使用
Seg_Point[]数组,而是用逗号标记。
例如:Seg_Buf[1] = 5 + ',',表示第 1 位显示数字 5 并点亮小数点。
在定时器中断中,判断如果某位数值 > 20,就减去逗号的 ASCII 码(44),然后显示小数点。 - 刷新间隔建议: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);
四、按键底层
注意事项
- NE555 频率测量:如果题目要求测频,必须注释掉所有 P34 相关的代码,因为 P34 被频率计占用。
- 双按键识别:例如同时按下按键 6 和 7,可以返回一个特殊值(如 67),在主函数中判断。
- 按键扫描间隔建议: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 时钟芯片底层
注意事项
- 官方提供的
ds1302.c需要补充引脚定义和读写函数。 - 需要包含
<intrins.h>头文件,用于_nop_()。 - 防重复包含技巧:
.h文件中只声明我们自己写的函数(Set_Rtc和Read_Rtc),官方函数不用全声明。 - 使用间隔建议:100ms。
- 上电时先用
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 温度芯片底层
注意事项
- 官方提供的
onewire.c需要补充引脚定义和温度读取函数。 - 上电后会先显示 85.0℃(芯片复位后的默认值),解决方法是上电后先读取一次,然后延时 750ms 再读取第二次。
- 使用间隔建议: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)
注意事项
- 官方提供的
iic.c需要补充引脚定义、包含<intrins.h>,并且建议将延时宏DELAY_TIME改为 5。 - PCF8591:
- AD 读取函数:
0x41对应光敏电阻,0x43对应滑动变阻器。 - 连续读取时会通道交换,例如先读光敏再读滑动变阻器,结果会互换。解决办法是交换读取顺序或在应用层处理。
- DA 写入:参数 0~255 对应 0~5V 输出。
- 使用间隔建议:150ms。
- AD 读取函数:
- 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);
八、超声波测距底层
注意事项
- 官方没有提供超声波底层,需要自己编写。根据原理图定义引脚:
P1^0为发送(TX),P1^1为接收(RX)。 - 用 STC-ISP 生成 12MHz 下的 12μs 延时函数(指令集 Y1)。
- 发射 8 个 40kHz 脉冲,然后开启定时器 0(计数模式)测量回波时间。
- 使用间隔建议: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 频率测量
注意事项
- 将定时器 0 配置为计数模式(
TMOD |= 0x05),用于统计外部脉冲。 - 定时器 1 用作 1ms 时基,累计 1000 次(即 1 秒)后读取定时器 0 的计数值,即为频率。
- 初始化顺序:先初始化定时器 0,再初始化定时器 1。
- 频率值存放在全局变量
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 调光
注意事项
- 通过定时器中断实现软件 PWM,控制 LED 亮度。
- 定义两个变量:
pwm_period:周期计数器(0~9),每个中断自动递增并取模 10。pwm_compare:比较值(0~9),决定占空比。
- 在定时器中断中判断:
pwm_period < pwm_compare时点亮 LED,否则熄灭。 - 点亮 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(); // 熄灭
}
}
十一、串口通信底层
注意事项
- 使用 STC-ISP 的“波特率计算器”生成初始化代码,选择定时器 2 作为波特率发生器,波特率 9600,12MHz。
- 重定义
putchar函数,实现printf输出。 - 定义接收缓冲区、索引和超时计数器,实现非阻塞接收。
- 使用间隔建议: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_Temperature的rate_ms改为 1000。
更多推荐



所有评论(0)