第八周 学习笔记

大模板V4T和调度器指导笔记

本笔记以 STC15F2K60S2(蓝桥杯竞赛板)为例,整理一套可复用的「大模板」工程:
分层清晰、全外设覆盖、非阻塞设计,方便比赛时快速套用。
文末附有「新旧模板对比」章节。


一、工程文件夹的建立

建议采用「底层 + 应用」的分层结构,而不是把所有代码堆在一个 main.c 里:

我的考号/
├─ prj/                 # Keil 工程文件(.uvproj 等)
└─ src/
   ├─ driver/           # 底层驱动(Led、Key、Seg、Init、ds1302、iic、onewire、ultrasound、uart)
   └─ user/             # 应用层(main.c)

建立步骤:

  1. 新建文件夹(例如 我的考号)。

  2. 在 Keil 51 中新建工程(Project → New μVision Project)。

  1. 选择芯片:STC15F2K60S2。

  1. 打开「魔法棒」配置(Options for Target,工具栏的魔术棒图标):
  • 在 C51 界面,把 src/driver 加入 Include Paths(头文件搜索路径);

  • 在 Output 界面,勾选 Create HEX File(生成 Hex 文件)。

  1. 在 src/user 下创建 main.c 并加入工程;在 src/driver 下创建各外设的底层文件(.c / .h)并加入工程。

:light_bulb: 为什么要分层:底层驱动只负责「怎么操作硬件」,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;
    }
}

:light_bulb: 锁存器地址速记: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);

说明:

  1. seg_pos = (++seg_pos) % 8; 让 seg_pos 在 0~7 之间循环,每次中断点亮下一位,实现动态扫描。

  2. 小数点标记技巧:把「带小数点」的数字本身加上字符 ',' 的 ASCII 码 44。
    例如要显示带小数点的 5,就存为 5 + 44 = 49。只要 seg_buf[seg_pos] 的值 >= 44(即 ','),
    就说明这一位需要点亮小数点。

  3. 如果带小数点(>= ','):

  • 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;
}

:warning: 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;          // 清空计数,开始下一轮
	}
}

注意事项

  1. 必须在 main 里遵循「先初始化定时器 0,再初始化定时器 1」的顺序,否则第一次测量的频率会不对。

  2. 一定要用跳线帽把 P34 和 SIGNAL 连起来,否则测不到信号。

  3. 按键的最后一列用到 P34,测频率时要屏蔽按键底层里与 P34 相关的代码,否则 P34 的电平翻转会引入测量误差,影响评分。

  4. 第一次测量数据若在 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)

![image-20261015081143209](file:///C:/Users/LKC4144/AppData/Roaming/Typora/typora-user-images/image-20261015081143209.png?lastModify=1792313558)

  • 用 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;
	}
}

:light_bulb: 为确保 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();
	}
}

五、新旧模板对比

相同点(底层硬件约定一致)

  1. 芯片与端口机制:都是 STC15F2K60S2,外设共用 P0,靠 P2 高三位选通 74HC573 锁存器。

  2. 锁存器地址:完全一致——0x80(Y4C/LED)、0xa0(Y5C/蜂鸣器/继电器)、0xc0(Y6C/位选)、0xe0(Y7C/段选)。

  3. 数码管:共阳极、低电平点亮,段码表相同(0xc0…0xff),显示流程都是「消隐 → 位选 → 段选」,小数点都是段码最高位 &= 0x7f。

  4. 系统初始化:都是上电先关 LED、再关蜂鸣器/继电器,代码逻辑几乎逐行一致。

  5. 节流思想:都引入了 Slow_Down 计数器(旧模板按键 10ms、显示 500ms;新模板全面推广到所有外设)。

  6. 按键沿检测:都用 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 是否被拉低,串口忙则直接退出,避免矩阵扫描与串口互相干扰。

:warning: 注意两处列引脚标注不同(旧 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.主程序调用

程序流程:

  1. 先初始化各种外设。

  2. Scheduler_Init() 计算出任务数量。

  3. Timer1_Init() 启动定时器中断,uwTick 开始每 1ms 递增。

  4. 主循环中不断调用 Scheduler_Run()。

Scheduler_Run() 本身不会死等延时,它只检查任务是否到期。如果没有任务到期,它很快就会返回,主循环继续下一轮,这样 CPU 可以反复查询。