大模板V4T和调度器指导笔记
本笔记以 STC15F2K60S2(蓝桥杯竞赛板)为例,整理一套可复用的「大模板」工程:
分层清晰、全外设覆盖、非阻塞设计,方便比赛时快速套用。
文末附有「新旧模板对比」章节。
一、工程文件夹的建立
建议采用「底层 + 应用」的分层结构,而不是把所有代码堆在一个 main.c 里:
我的考号/
├─ prj/ # Keil 工程文件(.uvproj 等)
└─ src/
├─ driver/ # 底层驱动(Led、Key、Seg、Init、ds1302、iic、onewire、ultrasound、uart)
└─ user/ # 应用层(main.c)
建立步骤:
-
新建文件夹(例如
我的考号)。 -
在 Keil 51 中新建工程(
Project → New μVision Project)。
- 选择芯片:
STC15F2K60S2。
- 打开「魔法棒」配置(
Options for Target,工具栏的魔术棒图标):
-
在 C51 界面,把
src/driver加入 Include Paths(头文件搜索路径); -
在 Output 界面,勾选 Create HEX File(生成 Hex 文件)。
- 在
src/user下创建main.c并加入工程;在src/driver下创建各外设的底层文件(.c/.h)并加入工程。
为什么要分层:底层驱动只负责「怎么操作硬件」,
main.c只负责「业务逻辑」。比赛时换题目,只需改main.c,底层文件基本不动。
二、基本外设
1. LED(Led.c)
核心思想:
-
把 8 个 LED 的状态(
ucLed[0..7])用位运算合并成一个字节,一次写入P0; -
用
temp_1_old记录上一次的值,只有状态真正变化时才刷新端口,避免重复操作; -
蓝桥杯板上 LED、数码管等外设共用
P0端口,靠P2高三位(P2.5~P2.7)选通不同的 74HC573 锁存器,写完数据后要把P2高三位关闭,防止误写其他外设。
#include "Led.h"
idata unsigned char temp_1 = 0x00;
idata unsigned char temp_1_old = 0xff;
void Led_Disp(unsigned char *ucLed)
{
unsigned char temp;
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; // LED 低电平点亮,取反输出
temp = P2 & 0x1f; // 清空高三位
temp = P2 | 0x80; // 选通 Y4C(LED)
P2 = temp;
temp = P2 & 0x1f; // 关闭锁存
P2 = temp;
temp_1_old = temp_1;
}
}
void Led_off()
{
unsigned char temp;
P0 = 0xff; // 全灭
temp = P2 & 0x1f;
temp = P2 | 0x80; // Y4C(LED)
P2 = temp;
temp = P2 & 0x1f;
P2 = temp;
temp_1_old = 0x00; // 置为关闭,下次变化时可重新刷新
}
idata unsigned char temp_2 = 0x00;
idata unsigned char temp_2_old = 0xff;
void Beep(bit enable)
{
unsigned char temp;
if(enable)
temp_2 |= 0x40; // D6 给高电平
else
temp_2 &= (~0x40); // 清空 D6
if(temp_2 != temp_2_old)
{
P0 = temp_2;
temp = P2 & 0x1f;
temp = P2 | 0xa0; // Y5C(蜂鸣器/继电器/电机)
P2 = temp;
temp = P2 & 0x1f;
P2 = temp;
temp_2_old = temp_2;
}
}
void Motor(bit enable)
{
unsigned char temp;
if(enable)
temp_2 |= 0x20; // D5 给高电平
else
temp_2 &= (~0x20); // 清空 D5
if(temp_2 != temp_2_old)
{
P0 = temp_2;
temp = P2 & 0x1f;
temp = P2 | 0xa0; // Y5C
P2 = temp;
temp = P2 & 0x1f;
P2 = temp;
temp_2_old = temp_2;
}
}
void Relay(bit enable)
{
unsigned char temp;
if(enable)
temp_2 |= 0x10; // D4 给高电平
else
temp_2 &= (~0x10); // 清空 D4
if(temp_2 != temp_2_old)
{
P0 = temp_2;
temp = P2 & 0x1f;
temp = P2 | 0xa0; // Y5C
P2 = temp;
temp = P2 & 0x1f;
P2 = temp;
temp_2_old = temp_2;
}
}
锁存器地址速记:
P2高三位P2.7 P2.6 P2.5对应Y4=100、Y5=101、Y6=110、Y7=111,
即0x80(Y4C/LED)、0xa0(Y5C/蜂鸣器继电器电机)、0xc0(Y6C/数码管位选)、0xe0(Y7C/数码管段选)。
2. 初始化(Init.c)
上电后先关闭所有可控负载,避免出现上电瞬间的误动作。
#include "Init.h"
void System_Init()
{
unsigned char temp;
// 关闭 LED
P0 = 0xff; // 全灭
temp = P2 & 0x1f; // 清空高三位
temp = P2 | 0x80; // Y4C(LED)
P2 = temp;
temp = P2 & 0x1f;
P2 = temp;
// 关闭继电器、蜂鸣器、电机
P0 = 0x00;
temp = P2 & 0x1f;
temp = P2 | 0xa0; // Y5C
P2 = temp;
temp = P2 & 0x1f;
P2 = temp;
}
3. 数码管(Seg.c)
底层只负责「点亮某一位」,真正的动态扫描放在中断里做。
#include "Seg.h"
pdata unsigned char seg_dula[] = {0xC0,0xF9,0xA4,0xB0,0x99,0x92,0x82,0xF8,0x80,0x90,0xFF};
void Seg_Disp(unsigned char wela, unsigned char dula, bit point)
{
unsigned char temp;
// 消隐(先全灭,避免拖影)
P0 = 0xff;
temp = P2 & 0x1f;
temp = P2 | 0xe0; // Y7C(段选)
P2 = temp;
temp = P2 & 0x1f;
P2 = temp;
// 位选
P0 = 0x01 << wela;
temp = P2 & 0x1f;
temp = P2 | 0xc0; // Y6C(位选)
P2 = temp;
temp = P2 & 0x1f;
P2 = temp;
// 段选
P0 = seg_dula[dula];
if(point) P0 &= 0x7f; // 点亮小数点(段码最高位清 0)
temp = P2 & 0x1f;
temp = P2 | 0xe0; // Y7C
P2 = temp;
temp = P2 & 0x1f;
P2 = temp;
}
新的中断写法(数码管刷新 + 小数点标记)
seg_pos = (++seg_pos) % 8;
if(seg_buf[seg_pos] >= ',')
seg_disp(seg_pos, seg_buf[seg_pos] - ',' , 1);
else
seg_disp(seg_pos, seg_buf[seg_pos], 0);
说明:
-
seg_pos = (++seg_pos) % 8;让seg_pos在 0~7 之间循环,每次中断点亮下一位,实现动态扫描。 -
小数点标记技巧:把「带小数点」的数字本身加上字符
','的 ASCII 码 44。
例如要显示带小数点的5,就存为5 + 44 = 49。只要seg_buf[seg_pos]的值 >= 44(即','),
就说明这一位需要点亮小数点。 -
如果带小数点(
>= ','):
-
seg_buf[seg_pos] - ',':减去 44,还原出原始数字(0~9); -
第三个参数传
1:点亮该位的小数点。
这样只用一个字节就同时表达了「数字 + 是否带小数点」,比额外维护一个点标志位更简洁。
4. 定时器中断
选择定时器 1、12T,1ms 中断,对应中断号 3。
void Timer1_Init(void) //1毫秒@12.000MHz
{
AUXR &= 0xBF; // 定时器时钟 12T 模式
TMOD &= 0x0F; // 设置定时器模式
TL1 = 0x18; // 设置定时初值
TH1 = 0xFC; // 设置定时初值
TF1 = 0; // 清除 TF1 标志
TR1 = 1; // 定时器 1 开始计时
ET1 = 1; // 使能定时器 1 中断
EA = 1; // 开总中断
}
5. 按键(Key.c)
采用「矩阵按键 + 串口冲突检测」:扫描前先检测 P30 是否被拉低(串口忙),若是则直接退出,避免矩阵扫描与串口互相干扰。
#include "Key.h"
unsigned char Key_Read()
{
unsigned char temp = 0;
/*独立按键(备用写法)*/
//if(P30 == 0) temp = 7;
//if(P31 == 0) temp = 6;
//if(P32 == 0) temp = 5;
//if(P33 == 0) temp = 4;
/*矩阵按键*/
// 若要测量频率,需注释掉和 P34 相关的代码
// 串口冲突检测
P44=1;P42=1;P35=1;//P34=1;
if(P30 == 0) return 0; // P3.0 被拉低?说明串口在忙,直接退出
P44=0;P42=1;P35=1;//P34=1;
if(P30 == 0) temp = 7;
if(P31 == 0) temp = 6;
if(P32 == 0) temp = 5;
if(P33 == 0) temp = 4;
// 串口冲突检测
P44=1;P42=1;P35=1;//P34=1;
if(P30 == 0) return 0;
P44=1;P42=0;P35=1;//P34=1;
if(P30 == 0) temp = 11;
if(P31 == 0) temp = 10;
if(P32 == 0) temp = 9;
if(P33 == 0) temp = 8;
// 串口冲突检测
P44=1;P42=1;P35=1;//P34=1;
if(P30 == 0) return 0;
P44=1;P42=1;P35=0;//P34=1;
if(P30 == 0) temp = 15;
if(P31 == 0) temp = 14;
if(P32 == 0) temp = 13;
if(P33 == 0) temp = 12;
// 第四列(用到 P34,测频率时屏蔽)
//P44=1;P42=1;P35=1;//P34=1;
//if(P30 == 0) return 0;
//P44=1;P42=1;P35=0;P34=1;
//if(P30 == 0) temp = 19;
//if(P31 == 0) temp = 18;
//if(P32 == 0) temp = 17;
//if(P33 == 0) temp = 16;
P44=1;P42=1;P35=1;//P34=1;
P3 |= 0xef;
}
P3.4 是 T0 外部计数引脚,外部待测频率信号从这里输入。测频率时,不把 P34 当按键线用,
因此要注释掉所有对 P34 的输出操作(//P34=1;),让它只作为频率输入引脚。
三、其他外设
以下外设普遍采用「节流读取」模式:用一个
xxx_Slow_Down计数变量,在 1ms 定时器中断里自增,
达到阈值才真正读取一次外设,然后清零。这样既避免了在主循环里用阻塞delay()卡住整个系统,
又控制了慢速器件(DS1302、DS18B20、IIC 等)的访问频率。
1. DS1302 时钟
底层(读写字节)
#include "ds1302.h"
#include "intrins.h"
sbit SCK = P1 ^ 7;
sbit SDA = P2 ^ 3;
sbit RST = P1 ^ 3;
void Set_Rtc(unsigned char *ucRtc)
{
unsigned char i;
Write_Ds1302_Byte(0x8e,0x00); // 关闭写保护
Write_Ds1302_Byte(0x80,0x80); // 关闭 CH,停止计时
for(i=0;i<3;i++)
Write_Ds1302_Byte(0x84-2*i, ucRtc[i]/10*16 + ucRtc[i]%10); // 十进制转 BCD 写入
Write_Ds1302_Byte(0x8e,0x00); // 重新开启写保护
}
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;
}
应用层调用
pdata unsigned char ucRtc[3] = {11,12,13};
idata unsigned char Time_Slow_Down;
void Get_Time()
{
if(Time_Slow_Down < 100) return; // 每 100ms 读一次
Time_Slow_Down = 0;
Read_Rtc(ucRtc);
}
void main()
{
System_Init();
Set_Rtc(ucRtc); // 初始化时钟
......
}
2. DS18B20 温度
底层
#include "onewire.h"
sbit DQ = P1 ^ 4;
float Read_Temperature()
{
unsigned char high, low;
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 ((high << 8) | low) * 0.0625; // 分辨率 0.0625℃
}
应用层调用
idata unsigned int Temperature_10x;
idata unsigned int Temperature_Slow_Down;
void Get_Temperature()
{
if(Temperature_Slow_Down < 300) return; // 每 300ms 读一次
Temperature_Slow_Down = 0;
Temperature_10x = Read_Temperature() * 10; // 放大 10 倍,方便整数显示
}
3. AD/DA 与 EEPROM(IIC)
底层
#include "iic.h"
#include "intrins.h"
#define DELAY_TIME 5 //10
sbit scl = P2^0;
sbit sda = P2^1;
/*
* AD 输入通道(PCF8591):
* 0x40/0x00 外部输入
* 0x41/0x01 光敏电阻
* 0x42/0x02 差分输入
* 0x43/0x03 滑动变阻器输入
*/
unsigned char AD_Read(unsigned char addr)
{
unsigned char temp;
I2CStart();
I2CSendByte(0x90); // 选中 PCF8591,写操作
I2CWaitAck();
I2CSendByte(addr);
I2CWaitAck();
I2CStart();
I2CSendByte(0x91); // 选中 PCF8591,读操作
I2CWaitAck();
temp = I2CReceiveByte();
I2CSendAck(1);
I2CStop();
return temp;
}
// 0-255 -> 0-5V
void DA_Write(unsigned char dat)
{
I2CStart();
I2CSendByte(0x90); // 选中 PCF8591,写操作
I2CWaitAck();
I2CSendByte(0x41); // 写入 DA
I2CWaitAck();
I2CSendByte(dat); // 数字电压 -> 模拟电压
I2CWaitAck();
}
void EEPROM_Write(unsigned char *str, unsigned char addr, unsigned num)
{
I2CStart();
I2CSendByte(0xa0); // 选中 AT24C02,写操作
I2CWaitAck();
I2CSendByte(addr); // 写入起始地址
I2CWaitAck();
while(num--)
{
I2CSendByte(*str++); // 写入一个字节,指针后移
I2CWaitAck();
I2C_Delay(200); // 确保完全写入
}
I2CStop();
// 连续延时,确保 EEPROM 写入完成
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);
}
void EEPROM_Read(unsigned char *str, unsigned char addr, unsigned num)
{
I2CStart();
I2CSendByte(0xa0); // 选中 AT24C02,写操作(先送地址)
I2CWaitAck();
I2CSendByte(addr); // 读取起始地址
I2CWaitAck();
I2CStart();
I2CSendByte(0xa1); // 选中 AT24C02,读操作
I2CWaitAck();
/* 如果在按键里处理,可关中断:EA = 0; */
while(num--)
{
*str++ = I2CReceiveByte(); // 读一个字节,指针后移
if(num)
I2CSendAck(0); // 还有数据,应答
else
I2CSendAck(1); // 最后一个,不应答
}
I2CStop();
//EA = 1;
}
应用层调用
idata unsigned char AD_1_Data_10x, AD_3_Data_10x;
idata unsigned char AD_Slow_Down;
void AD_DA()
{
if(AD_Slow_Down < 120) return; // 每 120ms 读一次
AD_Slow_Down = 0;
AD_1_Data_10x = AD_Read(0x41)*10/51; // 光敏电阻,放大 10 倍
AD_3_Data_10x = AD_Read(0x43)*10/51; // 滑动变阻器,放大 10 倍
DA_Write(3*51); // 输出约 3V
}
pdata unsigned char EEPROM_Data_R[8] = {1,2,3,4,5,6,7,8};
pdata unsigned char EEPROM_Data_W[8] = {1,2,3,4,5,6,7,8};
void main()
{
......
EEPROM_Read(EEPROM_Data_R, 0, 8); // 上电读回上次保存的值
EEPROM_Write(EEPROM_Data_W, 0, 8); // 写入
EEPROM_Read(EEPROM_Data_R, 0, 8); // 回读校验
......
}
4. 超声波测距
底层
#include "ultrasound.h"
#include "intrins.h"
sbit Tx = P1 ^ 0; // 发送
sbit Rx = P1 ^ 1; // 接收
/*12us 延时函数*/
void Delay12us(void) //@12.000MHz
{
unsigned char data i;
_nop_();
i = 3;
while (--i);
}
/*超声波初始化(发送 8 个脉冲)*/
void Ut_Wave_Init()
{
unsigned char i;
for(i=0;i<8;i++)
{
Tx = 1;
Delay12us();
Tx = 0;
Delay12us();
}
}
unsigned char Ut_Wave_Data()
{
unsigned char time;
CH = CL = 0; // 计数清零
CMOD = 0x00; // 配置 PCA 定时器
EA = 0; // 关闭中断,保证时序
Ut_Wave_Init();
EA = 1;
CR = 1; // 开始计时
while(Rx && !CF); // 等待回波或计数溢出
CR = 0; // 停止计时
if(!CF) // 计数没溢出
{
time = (CH << 8) | CL;
return (0.017 * time); // 单位:cm
}
else // 计数溢出
{
CF = 0; // 清除溢出位
return 0;
}
}
应用层调用
idata unsigned char Distance;
idata unsigned char Distance_Slow_Down;
void Get_Distance()
{
if(Distance_Slow_Down < 150) return; // 每 150ms 测一次
Distance_Slow_Down = 0;
Distance = Ut_Wave_Data();
}
5. NE555 频率测量
-
定时器 0:12MHz、16 位(不自动重载)、12T 计数模式,千万千万别开中断(它只负责计数);
-
定时器 1:1ms 中断,累计到 1s 时读一次计数值即频率(单位 Hz)。
void Timer0_Init(void) //1毫秒@12.000MHz(此处为计数模式)
{
AUXR &= 0x7F; // 定时器时钟 12T 模式
TMOD &= 0xF0; // 设置定时器模式
TMOD |= 0x05; // 定时器 0 计数模式,16 位不自动重载
TL0 = 0x00; // 设置定时初值
TH0 = 0x00; // 设置定时初值
TF0 = 0; // 清除 TF0 标志
TR0 = 1; // 定时器 0 开始计数
}
与普通定时的区别:
TMOD |= 0x05启用计数模式,并把初值都设为 0(清空计数)。
定时器 1 中断服务函数(1s 读一次频率)
unsigned int time_all_1s;
void Timer1_Init(void) //1毫秒@12.000MHz
{
AUXR &= 0xBF; // 定时器时钟 12T 模式
TMOD &= 0x0F; // 设置定时器模式
TL1 = 0x18; // 设置定时初值
TH1 = 0xFC; // 设置定时初值
TF1 = 0; // 清除 TF1 标志
TR1 = 1; // 定时器 1 开始计时
ET1 = 1; // 使能定时器 1 中断
EA = 1;
}
void Timer1_Isr(void) interrupt 3
{
if(++time_all_1s == 1000)
{
time_all_1s = 0;
Freq = TH0 << 8 | TL0; // 1s 内 T0 计数到的脉冲数 = 频率(Hz)
TH0 = TL0 = 0; // 清空计数,开始下一轮
}
}
注意事项
-
必须在
main里遵循「先初始化定时器 0,再初始化定时器 1」的顺序,否则第一次测量的频率会不对。 -
一定要用跳线帽把 P34 和 SIGNAL 连起来,否则测不到信号。
-
按键的最后一列用到 P34,测频率时要屏蔽按键底层里与 P34 相关的代码,否则 P34 的电平翻转会引入测量误差,影响评分。
-
第一次测量数据若在 4T 上测量可能不准。解决办法:让数码管 1s 后才显示——定义一个
bit标志,1s 获取到完整值后把标志置 1,标志为 1 时才刷新数码管。
bit show_flag_first = 0;
void Seg_Proc()
{
if(show_flag_first)
{
// 进行频率显示
}
}
void Timer1_Isr(void) interrupt 3
{
if(++time_all_1s == 1000)
{
show_flag_first = 1; // 拿到首个完整测量值后,允许显示
time_all_1s = 0;
Freq = TH0 << 8 | TL0;
TH0 = TL0 = 0;
}
}
6. PWM 调光
用「计数累加 + 比较」的方式在 1ms 中断里产生 PWM(10 级占空比)。
idata unsigned char Pwm_Period; // 累加对比
idata unsigned char Pwm_Compare; // 比较值(0~9)
Pwm_Period = (++Pwm_Period) % 10; // 0~9 之间循环
if(Pwm_Period < Pwm_Compare)
Led_Disp(ucLed); // 点亮
else
Led_off(); // 熄灭
原理:
Pwm_Period每 1ms 加 1、在 0~9 循环;Pwm_Compare决定「点亮比例」。
例如Pwm_Compare = 5,则 10 个周期里有 5 次点亮,占空比 50%。
7. 串口(UART)

-
用 STC-ISP 的波特率计算器生成初始化代码,并在末尾加上
EA = 1;开启总中断(uart.c)。 -
串口重定向:引入
stdio.h,重写putchar,之后就能像在电脑上一样用printf打印(uart.c)。
#include "stdio.h"
extern char putchar(char ch)
{
SBUF = ch; // 把字符丢进发送寄存器
while(TI == 0); // 等它发完
TI = 0; // 清标志
return ch;
}
printf("%bu", my_data); // 直接打印,就像在电脑上一样
- 超时解析(
main.c):接收数据后靠超时判断「一帧结束」,再统一解析,避免在中断里做复杂处理。
void Uart_Proc()
{
if(Uart_Rx_Index == 0) // 没有数据
return; // 直接返回
/*超时解析*/
if(Uart_Recv_Tick >= 10) // 接收超时,说明一帧结束
{
Uart_Recv_Tick = 0;
Uart_Rx_Flag = 0;
/*处理函数*/
memset(Uart_Buf, 0, Uart_Rx_Index); // 清空缓冲区
Uart_Rx_Index = 0;
}
}
// 定时器中断里:接收计时自增
void Timer1_Isr(void) interrupt 3
{
if(Uart_Rx_Flag)
Uart_Recv_Tick++;
}
- 串口中断服务函数
void Uart1_Isr() interrupt 4
{
if(RI) // 接收到数据
{
Uart_Rx_Flag = 1; // 开始计时
Uart_Recv_Tick = 0;
Uart_Buf[Uart_Rx_Index++] = SBUF; // 数据送入缓冲区
RI = 0; // 清接收中断标志
/*防止溢出,大小按需调整*/
if(Uart_Rx_Index > 10)
{
Uart_Rx_Index = 0;
memset(Uart_Buf, 0, 10); // 清空缓冲区
}
}
}
- 串口初始化放在定时器 0 和定时器 1 之间
Timer0_Init();
Uart1_Init();
Timer1_Init();
- 基本使用与调试方法
按键调试:按下按键时打印键值。
/*按键调试*/
if(Key_Down != 0)
printf("Key_Down:%bu", Key_Down);
回显调试:接收后用 sscanf 解析,用 %n 做长度验证,回显确认收发一致。
void Uart_Proc()
{
unsigned char x, y;
unsigned int chars_read;
unsigned int result = sscanf(Uart_Buf, "(%u,%u)%n", &x, &y, &chars_read);
if(Uart_Rx_Index == 0) // 没有数据
return;
/*超时解析*/
if(Uart_Recv_Tick >= 10)
{
Uart_Recv_Tick = 0;
Uart_Rx_Flag = 0;
printf("%s", Uart_Buf); // 回显,确认收发一致
if(result == 2 && chars_read == strlen(Uart_Buf))
{
printf("\r\nI Get x=%bu,y=%bu\r\n", x, y);
}
else
{
printf("ERROR");
}
memset(Uart_Buf, 0, Uart_Rx_Index); // 清空缓冲区
Uart_Rx_Index = 0;
}
}
为确保
sscanf()不会错误读取数据,通常加一个%n做「长度验证」:
%n会记录已读入的字符数,用它和缓冲区实际长度比较,就能判断是否完整解析成功。
四、主程序架构总览(main.c)
这是整套模板的「骨架」,记住这个固定结构即可:
void Timer1_Isr() interrupt 3
{
// 1. 各外设的节流计数(Slow_Down 自增)
Seg_Slow_Down++; Key_Slow_Down++; Time_Slow_Down++;
Temperature_Slow_Down++; AD_Slow_Down++; Distance_Slow_Down++;
// 2. 数码管动态扫描
Seg_Pos = (++Seg_Pos) % 8;
if(Seg_Buf[Seg_Pos] >= ',')
Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos] - ',', 1);
else
Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos], 0);
// 3. 频率测量(1s 读一次)
if(time_all_1s == 1000) { time_all_1s = 0; Freq = (TH0<<8)|TL0; TH0 = TL0 = 0; }
// 4. PWM 调光
Pwm_Period = (++Pwm_Period) % 10;
if(Pwm_Period < Pwm_Compare) Led_Disp(ucLed); else Led_off();
// 5. 串口接收计时
if(Uart_Rx_Flag) Uart_Recv_Tick++;
}
void main()
{
System_Init();
Set_Rtc(ucRtc);
EEPROM_Read(EEPROM_Data_R, 0, 8);
EEPROM_Write(EEPROM_Data_W, 0, 8);
EEPROM_Read(EEPROM_Data_R, 0, 8);
Timer0_Init(); // 先 T0
Uart1_Init();
Timer1_Init(); // 再 T1
while(1)
{
Key_Proc();
Seg_Proc();
Led_Proc();
Get_Time();
Get_Temperature();
AD_DA();
Get_Distance();
Uart_Proc();
}
}
五、新旧模板对比
相同点(底层硬件约定一致)
-
芯片与端口机制:都是 STC15F2K60S2,外设共用
P0,靠P2高三位选通 74HC573 锁存器。 -
锁存器地址:完全一致——
0x80(Y4C/LED)、0xa0(Y5C/蜂鸣器/继电器)、0xc0(Y6C/位选)、0xe0(Y7C/段选)。 -
数码管:共阳极、低电平点亮,段码表相同(
0xc0…0xff),显示流程都是「消隐 → 位选 → 段选」,小数点都是段码最高位&= 0x7f。 -
系统初始化:都是上电先关 LED、再关蜂鸣器/继电器,代码逻辑几乎逐行一致。
-
节流思想:都引入了
Slow_Down计数器(旧模板按键 10ms、显示 500ms;新模板全面推广到所有外设)。 -
按键沿检测:都用
Key_Down = Key_Val & (Key_Old ^ Key_Val)这类表达式。
不同点(核心差异)
1. 定时器分工(最关键的架构变化)
-
旧模板:只用定时器 0(16 位自动重装载,
interrupt 1),1ms 节拍里同时干「按键/显示节流 + 数码管扫描 + LED 刷新」。 -
新模板:定时器 1(12T,1ms,
interrupt 3)负责节拍和扫描;定时器 0 被专门腾出来做 NE555 频率计数(16 位不自动重载、计数模式、不开中断)。 -
新模板还强制
main里的初始化顺序:Timer0_Init() → Uart1_Init() → Timer1_Init()(先 T0 后 T1,否则首测频率不准)。
2. 数码管小数点
-
旧模板:单独维护一个
Seg_Point[8]小数点缓冲数组,和段码数组一一对应。 -
新模板:用「数字 + ASCII
','(44)」合并进一个字节——seg_buf[x] >= ','判断带点,- ','还原数字。省掉一个数组。
3. LED 接口
-
旧模板:
Led_Disp(addr, enable),一次控制单个 LED,用static temp/temp_old逐位|= / &=。 -
新模板:
Led_Disp(ucLed[]),一次传入 8 位数组,位运算合并成一个字节后整体写P0,变化才刷新;并额外扩展了Beep()/Motor()/Relay()(都走 Y5C)。
4. 矩阵按键引脚与键值范围
-
旧模板:列引脚用
P37~P34,4 列全用,键值4~19,无串口冲突检测。 -
新模板:列引脚写成
P44/P42/P35(P34被注释掉留给频率输入),只扫 3 列,键值4~15;扫描前先检测P30是否被拉低,串口忙则直接退出,避免矩阵扫描与串口互相干扰。
注意两处列引脚标注不同(旧
P37~P34,新P44/P42/P35)。旧笔记里也提到「部分原理图标注为 P44、P42、P35、P34」,两者可能是指向同一物理按键线,但代码写法不一致,套用时需按实际板子确认。
5. 外设覆盖
-
旧模板:无 DS1302、DS18B20、IIC、超声波、频率测量、PWM、串口。
-
新模板:全部补齐,并且统一用「
xxx_Slow_Down节流读取」替代阻塞delay(),控制慢速器件访问频率。
6. 串口调试
-
旧模板:完全没有串口内容。
-
新模板:
putchar重定向printf+ 超时解析 +sscanf("%n")长度校验 + 按键/回显调试方法。
7. 主循环组织
-
旧模板:只有
Key_Proc / Seg_Proc / Led_Proc三个 +Timer0_Server。 -
新模板:
while(1)固定调度Key/Seg/Led/Get_Time/Get_Temperature/AD_DA/Get_Distance/Uart_Proc,结构更完整统一。
六、调度器
idata unsigned long int uwTick;
typedef struct
{
void (*task_func)(void);//任务函数
unsigned long int rate_ms;//任务执行周期
unsigned long int last_ms;//上次执行的时间
}task_t;
idata task_t Scheduler_Task[] = {
{Led_Proc, 1, 0},
{Key_Proc, 10, 0},
{Seg_Proc, 20, 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();
}
}
}
void main()
{
......
Timer0_Init();
Scheduler_Init();//放在中断之前
Uart1_Init();
Timer1_Init();
while(1)
{
Scheduler_Run();
}
}
1.系统时间基准:uwTick
-
uwTick是一个全局软件计数器。 -
从
Timer1_Init()的配置可以看出,定时器 1 的目标是每 1ms 中断一次:
2. 任务结构体:task_t
每个任务包含三部分:
| 成员 | 含义 |
|---|---|
task_func |
指向要执行的任务函数,函数类型必须是 void func(void) |
rate_ms |
任务的执行周期,例如 10ms 执行一次 |
last_ms |
记录这个任务上次在哪一个 uwTick 时刻被执行 |
3. 任务表:Scheduler_Task[]
这里注册了 8 个任务:
| 任务函数 | 周期 | 说明 |
|---|---|---|
Led_Proc |
1ms | LED 处理 |
Key_Proc |
10ms | 按键扫描处理 |
Seg_Proc |
20ms | 数码管处理 |
Get_Time |
100ms | 读取 RTC 时间 |
Get_Temperature |
300ms | 读取温度传感器 |
AD_DA |
150ms | ADC 采集/DA 输出 |
Get_Distance |
120ms | 超声波测距 |
Uart_Proc |
10ms | 串口接收处理 |
表中每个任务的 last_ms 初值都是 0,表示系统启动后还没有执行过。
5. 调度器初始化:Scheduler_Init()
-
sizeof(Scheduler_Task)是整个任务数组占用的字节数。 -
sizeof(task_t)是单个任务结构体占用的字节数。 -
两者相除即可得到任务数量。
例如如果有 8 个任务,那么 task_num 就等于 8。
Scheduler_Run() 会使用这个值作为循环上限。
6. 调度器核心:Scheduler_Run()
7.工作流程
1.每进入一次 Scheduler_Run(),先读取当前系统时间:
unsigned long int now_time = uwTick;
2.遍历所有任务:
for (i = 0; i < task_num; i++)
3.判断当前任务是否应该执行:
if (now_time >= (Scheduler_Task[i].rate_ms + Scheduler_Task[i].last_ms))
这行代码等价于:
if (当前时间 >= 任务周期 + 上次执行时间)
也就是说:
-
Scheduler_Task[i].rate_ms是任务周期。 -
Scheduler_Task[i].last_ms是上次执行时间。 -
rate_ms + last_ms就是下一次应该执行的时间点。
如果当前时间已经达到了这个时间点,就进入 if。
4.更新上次执行时间:
cScheduler_Task[i].last_ms = now_time;
这里使用的是 last_ms = now_time,而不是 last_ms += rate_ms。
意思是:
-
如果任务刚好在预定时间执行,那么下一次执行时间会正常延后一个周期。
-
如果任务因为某些原因晚执行了,下一次执行时间会从“实际执行时刻”开始顺延一个周期,而不是补执行之前错过的次数。
这种方式更适合嵌入式任务,可以避免任务堆积。
5.执行任务函数:
Scheduler_Task[i].task_func();
8.主程序调用
程序流程:
-
先初始化各种外设。
-
Scheduler_Init()计算出任务数量。 -
Timer1_Init()启动定时器中断,uwTick开始每 1ms 递增。 -
主循环中不断调用
Scheduler_Run()。
Scheduler_Run() 本身不会死等延时,它只检查任务是否到期。如果没有任务到期,它很快就会返回,主循环继续下一轮,这样 CPU 可以反复查询。


