蓝桥杯单片机第3阶段,第零讲视频

阶段3

前言

  1. 系统性自律的培养(学习如何自律)
  2. 注意向上社交的重要性,注意保留平时向上社交的机会!

一,2024零基础入门(四)

1.1国赛模板v1.0

详情参考视频2024零基础入门(四)协会第四次培训.pdf

多尝试,多敲

1.2补充代码

图 1 新增代码1

图 2 新增代码2

1.3补充代码说明

核心思想:把CPU时间切成片,像餐厅服务员一样工作

想象一下,CPU就像一家餐厅里只有一个服务员。这个服务员需要同时照顾好几桌客人(不同的任务,比如按键检测、数码管显示、计算等)。如果他一直待在某一桌客人那里直到点完所有菜,其他客人就会等得不耐烦(程序“卡住”)。

一个好的服务员会这样做:他为每一桌服务一小会儿,然后迅速切换到下一桌,循环往复。这样,所有客人都感觉被照顾到了。这在程序里,就叫 “时间片轮询”

“减速”和定时器,就是实现这个服务员工作模式的工具。

1. “减速”到底是什么?—— 防止“霸占”CPU

你的代码里 Key_Slow_DownSeg_Slow_Down的作用,不是让程序变慢,而是给这些函数设置一个“工作节奏”,防止它们执行得太频繁,从而把CPU时间让给其他任务。

  • 为什么要减速? 芯片速度太快了:单片机执行一条指令只需要微秒或纳秒级时间。如果不加控制,Key_Proc()函数一秒钟会执行几十万次,但按键状态根本没那么快变化,这纯属浪费CPU资源。 防止按键抖动误判:机械按键按下时会产生物理抖动,会导致电平快速变化。如果每次检测到变化都认为是一次按键,就会误判多次。减速(比如10ms检测一次)可以避开抖动期,提高稳定性。
  • “减速变量”是怎么工作的? 它的工作原理就像一个“工作日历”,结合定时器中断来使用:
// 在定时器中断服务函数中,每1ms进来一次
void Timer0Server() interrupt 1 {
  // ... 重装定时初值 ...
  if(++Key_Slow_Down == 10) Key_Slow_Down = 0; // 每10ms,日历翻一页
  if(++Seg_Slow_Down == 500) Seg_Slow_Down = 0;// 每500ms,日历翻一页
}

// 在主要的处理函数中
void Key_Proc() {
  if(Key_Slow_Down) return; // 看看日历:这10ms的工作日还没过完吗?没过完就回去休息。
  Key_Slow_Down = 1;        // 工作日刚开始,先把“今日已打卡”标志立起来。

  // ... 真正的按键处理逻辑 ... // 开始干活(这个活儿很短,可能几微秒就干完了)
}
  • 简单来说if(Key_Slow_Down) return;这行代码的意思是:“如果不是我的工作时间,我立刻退出,不占用CPU。

2. 定时器——精准的“秒表”/“心跳”

那么,是谁来负责精确地计算10ms、500ms这些时间呢?就是定时器

  • 你可以把定时器配置成一个精准的秒表,每隔一个固定时间(比如1ms)就“滴答”一次。
  • 每次“滴答”,它都会中断主程序的执行,跳转到一个特定的函数(中断服务函数)里去执行一些紧急的、有定时要求的事情,比如: 给那些“减速变量”加1(翻日历)。 直接执行一些实时性要求很高的任务,比如数码管扫描(否则会闪烁)。

3. “前后端分离”的操作系统思路

这其实就是一种简单的程序设计架构(或称为调度器),体现了操作系统的核心思想。

  • 后端(定时器中断):像后台管理员,不问原因,只按固定节奏做事任务1:每1ms给 Key_Slow_DownSeg_Slow_Down加1。 任务2:每1ms刷新一次数码管显示。 它的特点是准时、高效、代码短
  • 前端(主循环main函数中的 Key_Proc(), Seg_Proc():像前台业务员,只关心逻辑,不关心时间Key_Proc()只负责:如果轮到我了,我就读取按键,处理按键逻辑。 Seg_Proc()只负责:如果轮到我了,我就根据最新数据,更新一下数码管要显示的内容。 它们不知道时间是怎么过去的,只会在“日历”翻到新的一页时被调用。

这种“前后端分离”的好处是结构清晰。你修改按键逻辑时,完全不用操心显示问题;想改变显示速度(比如从500ms改成800ms),也只需要修改定时器里的一行代码。

4. 阻塞 vs. 非阻塞(时间片轮询的优势)

  • 阻塞性调度(坏例子):就像服务员在A桌等客人慢慢看菜单,期间B桌、C桌怎么喊他都不理。在程序里,就是用 delay(500)这种函数,让CPU空等500ms,期间什么也做不了。
  • 非阻塞性调度(你的代码采用的好方法):服务员利用每一桌客人思考的碎片时间,去服务其他客人。CPU永远不会空等,总是在高效地执行某个任务,从而实现“同时”处理多件事的效果。

下面的流程图清晰地展示了这种非阻塞调度的工作模式,以及“前端”和“后端”是如何协同工作的:

flowchart TD
    A[主程序循环 Main Loop] --> B[执行按键处理 Key_Proc]
    B --> C[“执行显示处理 Seg_Proc<br>(其他任务...)”]
    C --> A

    D[定时器中断<br>(每1ms触发)] --> E[更新系统计时<br>(Key_Slow_Down等变量加1)]
    E --> F[执行实时性最高任务<br>(如数码管扫描)]
    F --> G[中断返回]
    
    H[“ ”] --> I{“Key_Slow_Down<br>等变量到期了吗?”}
    I -- 是 --> J[执行对应任务]
    J --> K[重置“减速变量”]
    I -- 否/无任务 --> H
    
    subgraph Frontend [前端:主循环(业务逻辑层)]
        A
        B
        C
        I
        J
        K
    end
    
    subgraph Backend [后端:定时器中断(时间调度层)]
        D
        E
        F
        G
    end

1.4代码示例(整合理解)

// 后端:定时器中断,系统的心跳和调度器
void Timer0Server() interrupt 1 {
    // 1. 重装初值,保证下次中断还是1ms后
    TL0 = 0x18;
    TH0 = 0xFC;

    // 2. 【调度核心】为所有任务更新“工作日历”
    if(++Key_Slow_Down  >= 10) Key_Slow_Down  = 0; // 10ms到
    if(++Seg_Slow_Down >= 500) Seg_Slow_Down = 0; // 500ms到

    // 3. 执行必须非常及时的任务,如数码管扫描,防止闪烁
    if(++Seg_Pos == 6) Seg_Pos = 0;
    Seg_Disp(Seg_Pos, Seg_Buf[Seg_Pos]); // 这个函数执行很快
}

// 前端:按键处理任务
void Key_Proc() {
    // 看日历:10ms到了吗?没到就直接回家(return)
    if(Key_Slow_Down) return;
    // 到了?先标记“我已开始工作”,然后干活
    Key_Slow_Down = 1;

    // ... 这里是你的按键读取和边缘检测逻辑 ...
    Key_Val = Key_Read();
    Key_Down = Key_Val & (Key_Val ^ Key_Old);
    Key_Old = Key_Val;
    // 处理逻辑很快,几微秒就做完,不做任何延时!
}

// 前端:显示数据处理任务
void Seg_Proc() {
    // 看日历:500ms到了吗?没到就直接回家
    if(Seg_Slow_Down) return;
    // 到了,标记后开始工作
    Seg_Slow_Down = 1;

    // ... 这里更新Seg_Buf数组里的显示数据,不涉及实际点亮数码管 ...
    // 例如:Seg_Buf[4] = Time_Count / 10 % 10;
}

// 主函数:前台经理,负责在各个任务之间轮询
void main() {
    Timer0_Init(); // 启动定时器,让“后台调度员”开始工作
    while(1) {
        Key_Proc();  // 问按键任务:到你工作时间了吗?
        Seg_Proc();  // 问显示任务:到你工作时间了吗?
        // Led_Proc(); // 还可以有其他的任务...
    }
}

二,基于定时器的倒计时程序设计1

2.1程序模板思路

  • 文件说明
  1. .c文件 编写底层函数
  2. .h文件 声明底层函数
  • 整体流程
  1. 先写Key.cKey.hSeg.cSeg.h
  2. 再写main.c
  • main.c整体流程
  1. 头文件
  2. 变量声明
  3. 按键处理函数Key_Proc()
  4. 信息处理函数Seg_Proc()
  5. 其他显示函数
  6. 定时器0初始化
  7. 定时器0中断服务函数

2.2具体程序模板

2.2.1Key.c,Key.h,Seg.c,Seg.h。

Key.c
#include "Key.h"

unsigned char Key_Read()
{
	unsigned char temp = 0;
	if(P3_4 == 0) temp = 1;
	if(P3_5 == 0) temp = 2;
	if(P3_6 == 0) temp = 3;
	if(P3_7 == 0) temp = 4;
	return temp;
}
Key.h
#include <REGX52.H>

unsigned char Key_Read();
Seg.c
#include "Seg.h"

unsigned char Seg_Wela[] = {0xfe,0xfd,0xfb,0xf7,0xef,0xdf};//位码
unsigned char Seg_Dula[] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x00};//段码

void Seg_Disp(unsigned char wela,dula)
{
	//消影(擦除当前段选),位选,段选
	P0=0x00;
	P2_6=1;
	P2_6=0;
	
	//
	P0=Seg_Wela[wela];
	P2_7=1;
	P2_7=0;
	
	//
	P0=Seg_Dula[dula];
	P2_6=1;
	P2_6=0;
}
Seg.h
#include <REGX52.H>

void Seg_Disp(unsigned char wela,dula);

2.2.2main.c

按键处理函数
/*按键处理函数*/
void Key_Proc()
{
	if(Key_Slow_Down) return;
	Key_Slow_Down = 1;//按键减速
	
	//中断里0-9循环,减速成10ms执行一次
    //
	Key_Val = Key_Read();
	Key_Down = Key_Val & (Key_Val ^ Key_Old);
	Key_Up = ~Key_Val & (Key_Val ^ Key_Old);
	Key_Old = Key_Val;
	
	switch(Key_Down)
	{
		
	}
}
  • 四行代码,需记忆
//写在按键处理函数中
    Key_Val = Key_Read();//读取键码值
    Key_Down = Key_Val & (Key_Val ^ Key_Old);//检测下降沿
    Key_Up =  ~Key_Val & (Key_Val ^ Key_Old);//检测上升沿
    Key_Old = Key_Val;//扫描辅助变量
信息处理函数
/*信息处理函数*/
void Seg_Proc()
{
	if(Seg_Slow_Down) return;
	Seg_Slow_Down=1;//数码管减速
}
其他显示函数
/*其他显示函数*/
void Led_Proc()
{
	
}
定时器0初始化函数
/*定时器0初始化函数*/
void Timer0_Init(void)		//1毫秒@12.000MHz
{
	AUXR &= 0x7F;			//定时器时钟12T模式
	TMOD &= 0xF0;			//设置定时器模式
	TMOD |= 0x01;			//设置定时器模式
	TL0 = 0x18;				//设置定时初始值
	TH0 = 0xFC;				//设置定时初始值
	TF0 = 0;				//清除TF0标志
	TR0 = 1;				//定时器0开始计时
	
	ET0=1;	//额外添加2行代码
	EA=1;
	
}

具体步骤:

  1. 使用STC-ISP生成函数

图 3 定时器0初始化函数生成步骤截图
  1. 额外添加2行代码:
	ET0=1;
	EA=1;
定时器0中断服务函数
/*定时器0中断服务函数*/
void Time0Sever() interrupt 1
{
	//复制定时初始值
	TL0 = 0x18;				//设置定时初始值
	TH0 = 0xFC;				//设置定时初始值
	
	if(++Key_Slow_Down == 10) Key_Slow_Down = 0;
	if(++Seg_Slow_Down == 500) Seg_Slow_Down = 0;
	
	if(++Seg_Pos == 6) Seg_Pos = 0;
	Seg_Disp(Seg_Pos,Seg_Buf[Seg_Pos]);
}
main.c
/*Main.c*/
void main()
{
	Timer0_Init();
	while(1)
	{
		Key_Proc();
		Seg_Proc();
		Led_Proc();
	}
}

2.3蜂鸣器

仿真软件中,蓝色表示响了,红色表示没有响。

P2_3输出低电平时,蜂鸣器使能。

图 4 蜂鸣器原理图
单片机引脚状态 PNP晶体管状态 蜂鸣器状态 原理说明
输出低电平 (0V) 饱和导通 工作 单片机引脚低电平使得基极(B)电位远低于发射极(E)(V_B ≈ 0V, V_E ≈ VCC),发射结正偏,产生足够的 I_B,晶体管进入饱和区,蜂鸣器两端获得接近VCC的电压,电流通路形成:VCC → 蜂鸣器 → 晶体管(E→C) → GND。
输出高电平 (如3.3V/5V) 截止 不工作 单片机引脚高电平使得基极(B)电位与发射极(E)电位接近或相等(V_B ≈ V_E ≈ VCC),发射结零偏或反偏,I_B ≈ 0,晶体管截止,集电极电流 I_C几乎为零,蜂鸣器无电流通过。

2.4做题思路

  1. 先略读一遍题目要求

详细文件见第二届 创意智造·奠基未来 单片机设计与开发大赛 模拟题(2).pdf

  1. 建立模板
  2. 首先写数码管
  3. 按键功能由易到难依次写功能
  4. 写完一个模块或者一个功能就可以编译仿真看看有没有问题
  • 数字思维:
    • 蓝桥杯中,定义数组a和变量b,a[b]来获取不同的参数(15,30,60)

2.4.1 1000ms倒计时

/*变量声明区*/
unsigned int Timer_1000ms; //1000ms标志位
unsigned int Time_Count = 30;//系统计时变量

/*定时器0中断服务函数*/
		if(++Timer_1000ms == 1000)
		{
			Timer_1000ms = 0;
			Time_Count--;
			            
			if(Time_Count==255) Time_Count=0;
		}

2.4.2 4个按键按下的功能

说明:可以一个一个的写,由易到难来写,按照逻辑先后顺序写,不用强行按照S1、S2、S3、S4的顺序写。

/*变量声明区*/
bit System_Flag;//系统标志位 0停止 1运行
unsigned char Set_Dat[3] = {15,30,60};//设置界面设置的倒计时参数数组
unsigned char Set_Dat_Index = 1; //倒计时参数数组索引

unsigned int Timer_500ms;//500ms倒计时标志位
bit Seg_Flag;//数码管闪烁功能

/*按键处理函数*/
switch(Key_Down)
	{
		case 1://启动
			if(Seg_Mode == 0) System_Flag=1;
		break;
		case 3://切换
			if(Seg_Mode == 1) Time_Count=Set_Dat[Set_Dat_Index];
			Seg_Mode ^= 1;
		break;
		case 4://设置
			if(Seg_Mode == 1)
			{
				if(++Set_Dat_Index == 3) Set_Dat_Index=0;
				
			}		
		break;
		case 2:
			if(Seg_Mode == 0) Time_Count=Set_Dat[Set_Dat_Index];
			System_Flag = 0;
		break;
	}

2.4.3 数码管显示

/*信息处理函数*/
void Seg_Proc()
{
	if(Seg_Slow_Down) return;
	Seg_Slow_Down=1;//数码管减速
	
	Seg_Buf[0]=Seg_Mode+1;
	if(Seg_Mode == 0)
	{
		Seg_Buf[4] = Time_Count / 10 % 10;
		Seg_Buf[5] = Time_Count % 10; 
	}
	else//系统处于设置界面
	{
		if(Seg_Flag==1)//显示
		{
			Seg_Buf[4] = Set_Dat[Set_Dat_Index] / 10 % 10;
			Seg_Buf[5] = Set_Dat[Set_Dat_Index] % 10;
		}
		else
		{
			Seg_Buf[4] = 10;
			Seg_Buf[5] = 10;
		}
		
	}
}

2.5.3其他显示函数

/*其他显示函数*/
void Led_Proc()
{
	if(Time_Count == 0)
	{
		P1=0X00;
		P2_3 = 0;
	}
	else
	{
		P1=0XFF;
		P2_3 = 1;
	}
}

这部分用于倒计时结束后所有LED亮起,蜂鸣器响起。

2.5.4完整代码

==重要说明==:

  1. 多动手敲代码,按照做题思路来,不要死背代码。
  2. 培养自己检查错误的能力。
  3. 写一部分就可以编译,仿真看看效果是否合理。
  4. 注重模板的作用,熟练使用模板。
  • 完整代码如下:
/*头文件声明*/
#include <REGX52.H>
#include "Key.h"
#include "Seg.h"

/*变量声明区*/
unsigned char Key_Slow_Down;	//按键减速专用变量	10ms
unsigned int Seg_Slow_Down;	//数码管减速专用变量	500ms
unsigned char Key_Val,Key_Down,Key_Up,Key_Old;//按键扫描专用变量
unsigned char Seg_Pos;	//数码管扫描变量
unsigned char Seg_Buf[6] = {10,10,10,10,10,10};	//数码管默认显示数据,数码管显示数字存放数组
unsigned char Seg_Mode;//数码管显示界面 0-显示 1-设置
unsigned int Timer_1000ms; //1000ms标志位
unsigned int Time_Count = 30;//系统计时变量

bit System_Flag;//系统标志位 0停止 1运行
unsigned char Set_Dat[3] = {15,30,60};
unsigned char Set_Dat_Index = 1; 

unsigned int Timer_500ms;
bit Seg_Flag;


/*按键处理函数*/
void Key_Proc()
{
	if(Key_Slow_Down) return;
	Key_Slow_Down = 1;//按键减速
	
	//中断里0-9循环,减速成10ms执行一次
	Key_Val = Key_Read();
	Key_Down = Key_Val & (Key_Val ^ Key_Old);
	Key_Up = ~Key_Val & (Key_Val ^ Key_Old);
	Key_Old = Key_Val;
	
	switch(Key_Down)
	{
		case 1://启动
			if(Seg_Mode == 0) System_Flag=1;
		break;
		case 3://切换
			if(Seg_Mode == 1) Time_Count=Set_Dat[Set_Dat_Index];
			Seg_Mode ^= 1;
		break;
		case 4://设置
			if(Seg_Mode == 1)
			{
				if(++Set_Dat_Index == 3) Set_Dat_Index=0;
				
			}		
		break;
		case 2:
			if(Seg_Mode == 0) Time_Count=Set_Dat[Set_Dat_Index];
			System_Flag = 0;
		break;
	}
}

/*信息处理函数*/
void Seg_Proc()
{
	if(Seg_Slow_Down) return;
	Seg_Slow_Down=1;//数码管减速
	
	Seg_Buf[0]=Seg_Mode+1;
	if(Seg_Mode == 0)
	{
		Seg_Buf[4] = Time_Count / 10 % 10;
		Seg_Buf[5] = Time_Count % 10; 
	}
	else//系统处于设置界面
	{
		if(Seg_Flag==1)//显示
		{
			Seg_Buf[4] = Set_Dat[Set_Dat_Index] / 10 % 10;
			Seg_Buf[5] = Set_Dat[Set_Dat_Index] % 10;
		}
		else
		{
			Seg_Buf[4] = 10;
			Seg_Buf[5] = 10;
		}
		
	}
}

/*其他显示函数*/
void Led_Proc()
{
	if(Time_Count == 0)
	{
		P1=0X00;
		P2_3 = 0;
	}
	else
	{
		P1=0XFF;
		P2_3 = 1;
	}
}

/*定时器0初始化函数*/
void Timer0_Init(void)		//1毫秒@12.000MHz
{
	TMOD &= 0xF0;			//设置定时器模式
	TMOD |= 0x01;			//设置定时器模式
	TL0 = 0x18;				//设置定时初始值
	TH0 = 0xFC;				//设置定时初始值
	TF0 = 0;				//清除TF0标志
	TR0 = 1;				//定时器0开始计时
	
	ET0=1;	//额外添加2行代码
	EA=1;
	
}

/*定时器0中断服务函数*/
void Time0Sever() interrupt 1	//一定要有 interrupt 1
{
	//复制定时初始值
	TL0 = 0x18;				//设置定时初始值
	TH0 = 0xFC;				//设置定时初始值
	
	if(++Key_Slow_Down == 10) Key_Slow_Down = 0;	//按键消抖,确保稳定检测
	if(++Seg_Slow_Down == 500) Seg_Slow_Down = 0;	//控制数码管显示数据的更新频率
	
	if(++Seg_Pos == 6) Seg_Pos = 0;
	Seg_Disp(Seg_Pos,Seg_Buf[Seg_Pos]);	
	
	if(System_Flag==1)
	{
		if(++Timer_1000ms == 1000)
		{
			Timer_1000ms = 0;
			Time_Count--;
			
			if(Time_Count==255) Time_Count=0;
		}
	}
	if(++Timer_500ms == 500)
	{
		Timer_500ms=0;
		Seg_Flag ^= 1;
		
	}
		
}


/*Main.c*/
void main()
{
	Timer0_Init();
	while(1)
	{
		Key_Proc();
		Seg_Proc();
		Led_Proc();
	}
}

三,基于定时器的倒计时程序设计1

3.1 四行代码复习

	Key_Val = Key_Read();
	Key_Down = Key_Val & (Key_Val ^ Key_Old);
	Key_Up = ~Key_Val & (Key_Val ^ Key_Old);
	Key_Old = Key_Val;
	/*
	按键1按下->Key_Down=1
	按键2松起->Key_Up=2
	按键3长按->Key_Old=3
	*/

3.2 比赛说明

  • 国赛靠背程序,背模板没有分。
  • 决赛不给模拟题
  • 文件(单片机-模板)

3.3 做题思路

题目:电子钟.pdf

  1. 整体看看题目
  2. 看到S7,S8 → 先把矩阵按键写上

3.3.1 矩阵按键

Key.c

#include "Key.h"

unsigned char Key_Read()
{
	unsigned char temp = 0;
	P3_0=0;P3_1=1;P3_3=1;P3_4=1;
	if(P3_4 == 0) temp = 1;
	if(P3_5 == 0) temp = 2;
	if(P3_6 == 0) temp = 3;
	if(P3_7 == 0) temp = 4;
	P3_0=1;P3_1=0;P3_3=1;P3_4=1;
	if(P3_4 == 0) temp = 5;
	if(P3_5 == 0) temp = 6;
	if(P3_6 == 0) temp = 7;
	if(P3_7 == 0) temp = 8;
	P3_0=1;P3_1=1;P3_3=0;P3_4=1;
	if(P3_4 == 0) temp = 9;
	if(P3_5 == 0) temp = 10;
	if(P3_6 == 0) temp = 11;
	if(P3_7 == 0) temp = 12;
	P3_0=1;P3_1=1;P3_3=0;P3_4=1;
	if(P3_4 == 0) temp = 13;
	if(P3_5 == 0) temp = 14;
	if(P3_6 == 0) temp = 15;
	if(P3_7 == 0) temp = 16;
	
	return temp;
}

3.3.2 由数码管入手

  1. 所有程序都从数码管开始写,把需要显示的数据写上去
/*变量声明区*/
unsigned char Seg_Disp_Mode;//0-时钟显示界面 ,1-时钟设置界面 ,2闹钟设置界面 
unsigned char Clock_Disp[3] = {23,59,55};//上电后显示的时间,时分秒
/*信息处理函数*/
switch(Seg_Disp_Mode)
	{
		case 0:
			Seg_Buf[0] = Clock_Disp[0] / 10 % 10;
			Seg_Buf[1] = Clock_Disp[0] / 1 % 10;
			Seg_Buf[2] = Clock_Disp[1] / 10 % 10;
			Seg_Buf[3] = Clock_Disp[1] / 1 % 10;
			Seg_Buf[4] = Clock_Disp[2] / 10 % 10;
			Seg_Buf[5] = Clock_Disp[2] / 1 % 10;
		break;
	}

也可用for循环写这6行代码。

			for(i=0;i<3;i++)
			{
				Seg_Buf[2*i] = Clock_Disp[i] / 10 % 10;
				Seg_Buf[2*i+1] = Clock_Disp[i] / 1 % 10;
			}

3.3.3 定时器0中断服务函数

if(++Timer_1000ms == 1000)
{
    Timer_1000ms = 0;
    Clock_Disp[2]++;
    if(Clock_Disp[2] == 60)
    {
        Clock_Disp[2] = 0;
        Clock_Disp[1]++;
        if(Clock_Disp[1] == 60)
        {
            Clock_Disp[1] = 0;
            Clock_Disp[0]++;
            if(Clock_Disp[0] == 24)
            {
                Clock_Disp[0] = 0;
            }
        }
    }
}
补充:两种代码的区别

代码一:

if(++Timer_1000ms == 1000)
{
    
}

代码二:

if(Timer_1000ms++ == 1000)
{
    
}

代码一:if(++Timer_1000ms == 1000)(前缀递增)

这种写法是立即更新,立即判断

  • 第1次中断Timer_1000ms从0变为1,判断 1 == 1000
  • 第999次中断Timer_1000ms从998变为999,判断 999 == 1000
  • 第1000次中断Timer_1000ms从999变为1000,判断 1000 == 1000!执行 Timer_1000ms = 0
  • 第1001次中断Timer_1000ms从0变为1,判断 1 == 1000

结论:此写法严格地在第1000次中断时触发条件并清零,实现精准的1000ms周期(假设中断间隔为1ms)。

代码二:if(Timer_1000ms++ == 1000)(后缀递增)

这种写法是先按旧值判断,再自我更新

  • 第1次中断:判断 0 == 1000,然后 Timer_1000ms变为1。
  • 第1000次中断:判断 999 == 1000,然后 Timer_1000ms变为1000。
  • 第1001次中断:判断 1000 == 1000!执行 Timer_1000ms = 0(或其他操作),然后 Timer_1000ms从0变为1。
  • 第1002次中断:判断 1 == 1000

结论:此写法在 Timer_1000ms达到1000后的下一次中断(第1001次) 才触发条件。这引入了一次中断周期的延迟,通常不是设计定时周期的本意。

:gem_stone: 两种代码区别总结

简单来说,if(++Timer_1000ms == 1000)意味着:“立刻把计数加1,如果新计数正好是1000,就执行操作”。这是实现精确定时的正确方式。而如果使用后缀递增,则意味着:“如果当前计数是1000就执行操作,但不管怎样,计数之后都会加1”,这会导致操作延迟发生。

3.3.4 数码管显示小数点

核心思想:或运算 |

图 5 数码管原理图

图 6 小数点2进制转16进制

Seg.c

#include "Seg.h"

unsigned char Seg_Wela[6] = {0xfe,0xfd,0xfb,0xf7,0xef,0xdf};//位码
unsigned char Seg_Dula[11] = {0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x00};//段码

void Seg_Disp(unsigned wela,dula,point)
{
	//消影段码,位码,段码
	P0=0X00;
	P2_6=1;
	P2_6=0;
	
	P0=Seg_Wela[wela];
	P2_7=1;
	P2_7=0;
	 
	if(point == 1) 
	{
		P0 = Seg_Dula[dula] | 0X80;	//如果需要显示小数点,point赋值为1,显示小数点
	}
	else
	{
		P0=Seg_Dula[dula];
	}
	P2_6=1;
	P2_6=0;
}

==修改完Seg.c后,也需要再修改Seg.h和main.c中引用函数的代码==

Seg.h

#include <REGX52.H>

void Seg_Disp(unsigned wela,dula,point);

main.c

/*变量声明区*/
unsigned Seg_Buf[] = {1,2,3,4,5,6};
unsigned Seg_Pos;

unsigned char Seg_Point[6] = {0,1,0,1,0,1};	//新增数组

/*定时器0中断服务函数*/
	if(++Seg_Pos == 6) Seg_Pos=0;
	Seg_Disp(Seg_Pos,Seg_Buf[Seg_Pos],Seg_Point[Seg_Pos]);//修改后代码

3.3.5 时钟设置界面

再定义一个时钟设置的数组,每个显示界面的数据互不干扰!

按下按键时,把显示界面的数据传入设置界面。

	/*按键处理函数*/
	switch(Key_Down)
	{
		case 1:
			Clock_Set[0] =  Clock_Disp[0];
			Clock_Set[1] =  Clock_Disp[1];
			Clock_Set[2] =  Clock_Disp[2];
			Seg_Disp_Mode = 1;
		break;
	}
	/*信息处理函数*/
		case 1:
			Seg_Buf[0] = Clock_Set[0] / 10 % 10;
			Seg_Buf[1] = Clock_Set[0] / 1 % 10;
			Seg_Buf[2] = Clock_Set[1] / 10 % 10;
			Seg_Buf[3] = Clock_Set[1] / 1 % 10;
			Seg_Buf[4] = Clock_Set[2] / 10 % 10;
			Seg_Buf[5] = Clock_Set[2] / 1 % 10;
		break;

闪烁设置:

/*变量声明区*/
bit Seg_Flag;//0显示 1消失
unsigned int Timer_500ms;

/*按键处理函数*/
		case 3:
			if(++Clock_Set_Index == 3) Clock_Set_Index = 0;
		break;

/*信息处理函数*/
	switch(Seg_Disp_Mode)
	{
		case 1:
			Seg_Buf[0] = Clock_Set[0] / 10 % 10;
			Seg_Buf[1] = Clock_Set[0] / 1 % 10;
			Seg_Buf[2] = Clock_Set[1] / 10 % 10;
			Seg_Buf[3] = Clock_Set[1] / 1 % 10;
			Seg_Buf[4] = Clock_Set[2] / 10 % 10;
			Seg_Buf[5] = Clock_Set[2] / 1 % 10;
			
			switch(Clock_Set_Index)
			{
				case 0:	//小时闪烁
					Seg_Buf[0] = Seg_Flag ? (Clock_Set[0] / 10 % 10) : 10;
					Seg_Buf[1] = Seg_Flag ? (Clock_Set[0] / 1 % 10) : 10;
				break;
				case 1:	//分钟闪烁
					Seg_Buf[2] = Seg_Flag ? (Clock_Set[1] / 10 % 10) : 10;
					Seg_Buf[3] = Seg_Flag ? (Clock_Set[1] / 1 % 10) : 10;
				break;
				case 2:	//秒钟闪烁
					Seg_Buf[4] = Seg_Flag ? (Clock_Set[2] / 10 % 10) : 10;
					Seg_Buf[5] = Seg_Flag ? (Clock_Set[2] / 1 % 10) : 10;
				break;
			}
		break;

补充:条件运算符

对于代码:Seg_Buf[0] = Seg_Flag ? (Clock_Set[0] / 10 % 10) : 10;

  • Seg_Flag=1,显示
  • Seg_Flag=2,消失

语法:变量 = 条件表达式 ? 表达式1 : 表达式2;

其执行逻辑是:如果条件表达式为真(非0),则整个表达式取 表达式1的值;如果为假(0),则取 表达式2的值,最后将这个值赋给等号左边的变量

int num = 5;
printf("%s", (num >= 0) ? "正数或零" : "负数");
  • 输出

正数或零

    int a;
    int b = 1; // 条件为真
    int c = 10;
    int d = 20;

    a = b ? c : d; // 因为 b 为真 (1),所以 a 被赋值为 c (10)
    printf("a = %d\n", a); // 输出:a = 10

    // 现在,将条件 b 改为假 (0)
    b = 0;
    a = b ? c : d; // 因为 b 为假 (0),所以 a 被赋值为 d (20)
    printf("a = %d\n", a); // 输出:a = 20
  • 输出

a=10

a=20

补充:简化为for循环
		Seg_Buf[2*Clock_Set_Index] = Seg_Flag ? (Clock_Set[Clock_Set_Index] / 10 % 10) : 10;
		Seg_Buf[2*Clock_Set_Index+1] = Seg_Flag ? (Clock_Set[Clock_Set_Index] / 1 % 10) : 10;

3.3.6 按键按下增加时间

		/*按键处理函数*/
		case 5:
			if(Seg_Disp_Mode == 1)
			{
				Clock_Set[Clock_Set_Index]++;
				if(Clock_Set[Clock_Set_Index] == ((Clock_Set_Index==0) ? 24 : 60))
					Clock_Set[Clock_Set_Index] = 0;
			}	
		break;

3.3.7 按键按下减少时间

类比3.3.6可得按键按下减少时间的代码:

		/*按键处理函数*/
		case 6:
			if(Seg_Disp_Mode == 1)
			{
				Clock_Set[Clock_Set_Index]--;
				if(Clock_Set[Clock_Set_Index] == 255)
					Clock_Set[Clock_Set_Index] = (Clock_Set_Index==0) ? 23: 59;
			}
		break;

3.3.8 保存设置数据退出

类比3.3.5,把设置界面的数字复制给显示界面的数字

		case 7:
			Clock_Disp[0] =  Clock_Set[0];
			Clock_Disp[1] =  Clock_Set[1];
			Clock_Disp[2] =  Clock_Set[2];
			Seg_Disp_Mode = 0;
		break;

3.3.9 不保存数字,直接退出

		case 8:
			if(Seg_Disp_Mode == 1)
			{
				Seg_Disp_Mode = 0;
			}
		break;

3.3.10 闹钟切换界面与闹钟设置

unsigned char Alarm[3] = {0,0,0};
unsigned char Alarm_Set[3];
	switch(Key_Down)
	{
		case 1:
			Clock_Set_Index = 0;
			Clock_Set[0] =  Clock_Disp[0];
			Clock_Set[1] =  Clock_Disp[1];
			Clock_Set[2] =  Clock_Disp[2];
			Seg_Disp_Mode = 1;
		break;
		
		case 3:
			if(Seg_Disp_Mode != 0)
			{
				if(++Clock_Set_Index == 3) Clock_Set_Index = 0;
			}	
		break;
		case 5:
			if(Seg_Disp_Mode == 1)
			{
				Clock_Set[Clock_Set_Index]++;
				if(Clock_Set[Clock_Set_Index] == ((Clock_Set_Index==0) ? 24 : 60))
					Clock_Set[Clock_Set_Index] = 0;
			}
			if(Seg_Disp_Mode == 2)
			{
				Alarm_Set[Clock_Set_Index]++;
				if(Alarm_Set[Clock_Set_Index] == ((Clock_Set_Index==0) ? 24 : 60))
					Alarm_Set[Clock_Set_Index] = 0;
			}
		break;
		case 6:
			if(Seg_Disp_Mode == 1)
			{
				Clock_Set[Clock_Set_Index]--;
				if(Clock_Set[Clock_Set_Index] == 255)
					Clock_Set[Clock_Set_Index] = (Clock_Set_Index==0) ? 23: 59;
			}
			if(Seg_Disp_Mode == 2)
			{
				Alarm_Set[Clock_Set_Index]--;
				if(Alarm_Set[Clock_Set_Index] == 255)
					Alarm_Set[Clock_Set_Index] = (Clock_Set_Index==0) ? 23: 59;
			}
		break;
		case 7:
			if(Seg_Disp_Mode == 1)
			{
				Clock_Disp[0] =  Clock_Set[0];
				Clock_Disp[1] =  Clock_Set[1];
				Clock_Disp[2] =  Clock_Set[2];
				Seg_Disp_Mode = 0;
			}
			if(Seg_Disp_Mode == 2)
			{
				Alarm[0] = Alarm_Set[0];
				Alarm[1] = Alarm_Set[1];
				Alarm[2] = Alarm_Set[2];
				Seg_Disp_Mode = 0;
			}
		break;
		case 8:
			Seg_Disp_Mode = 0;
		break;
		case 2:
			Clock_Set_Index = 0;
			Alarm_Set[0] = Alarm[0];
			Alarm_Set[1] = Alarm[1];
			Alarm_Set[2] = Alarm[2];
			Seg_Disp_Mode = 2;
		break;
		case 4:
			Alarm_Flag ^= 1;
		break;
	}		

3.3.11 显示函数

/*变量声明区*/
unsigned char Led;
bit Alarm_Enable_Flag;

/*其他显示函数*/
void Led_Proc()
{
	if(Alarm_Flag == 1)
	{
		if(Clock_Disp[0]==Alarm[0] && Clock_Disp[1]==Alarm[1] && Clock_Disp[2]==Alarm[2])
		{
			Alarm_Enable_Flag = 1;
		}
		if(Alarm_Enable_Flag == 1)
		{
			P2_3 = 0;
			P1 = Led;
		}	
		else
		{
			P2_3 = 1;
			P1 = 0XFF;
		}
	}
	else
	{
		P2_3 = 1;
		P1 = 0XFF;
	}
}

/*定时器0中断服务函数*/
if(++Timer_500ms == 500)
	{
		Timer_500ms = 0;
		Seg_Flag ^= 1;
		
		if(Clock_Disp[0] >= 12)
			Led ^= 0XF0;
		else
			Led ^= 0X0F;
	}

3.3.12 完整代码

/*头文件声明区*/
#include <REGX52.H>
#include "Key.h"
#include "Seg.h"

/*变量声明区*/
unsigned char Key_Slow_Down;//按键减速变量 0-9
unsigned char Key_Val,Key_Down,Key_Up,Key_Old;//按键状态变量
unsigned int Seg_Slow_Down;//数码管减速变量 0-499

unsigned Seg_Buf[] = {1,2,3,4,5,6};
unsigned Seg_Pos;

unsigned char Seg_Disp_Mode;//0-时钟显示界面 ,1-时钟设置界面 ,2闹钟设置界面 
unsigned char Clock_Disp[3] = {23,59,55};//界面A,上电后显示的时间,时分秒

unsigned int Timer_1000ms; //1000ms变量

unsigned char Seg_Point[6] = {0,1,0,1,0,1};

unsigned char Clock_Set[3];//界面B,时钟设置,闪烁
unsigned char Clock_Set_Index;//0-小时 1-分钟 2-秒钟

bit Seg_Flag;//0显示 1消失
unsigned int Timer_500ms;


unsigned char Alarm[3] = {0,0,0};
unsigned char Alarm_Set[3];
//unsigned char Alarm_Set_Index;

bit Alarm_Flag;

unsigned char Led;
bit Alarm_Enable_Flag;

/*按键处理函数*/
void Key_Proc()
{
	if(Key_Slow_Down) return;
	Key_Slow_Down = 1;
	
	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 != 0) Alarm_Enable_Flag = 0;
	
	switch(Key_Down)
	{
		case 1:
			Clock_Set_Index = 0;
			Clock_Set[0] =  Clock_Disp[0];
			Clock_Set[1] =  Clock_Disp[1];
			Clock_Set[2] =  Clock_Disp[2];
			Seg_Disp_Mode = 1;
		break;
		
		case 3:
			if(Seg_Disp_Mode != 0)
			{
				if(++Clock_Set_Index == 3) Clock_Set_Index = 0;
			}	
		break;
		case 5:
			if(Seg_Disp_Mode == 1)
			{
				Clock_Set[Clock_Set_Index]++;
				if(Clock_Set[Clock_Set_Index] == ((Clock_Set_Index==0) ? 24 : 60))
					Clock_Set[Clock_Set_Index] = 0;
			}
			if(Seg_Disp_Mode == 2)
			{
				Alarm_Set[Clock_Set_Index]++;
				if(Alarm_Set[Clock_Set_Index] == ((Clock_Set_Index==0) ? 24 : 60))
					Alarm_Set[Clock_Set_Index] = 0;
			}
		break;
		case 6:
			if(Seg_Disp_Mode == 1)
			{
				Clock_Set[Clock_Set_Index]--;
				if(Clock_Set[Clock_Set_Index] == 255)
					Clock_Set[Clock_Set_Index] = (Clock_Set_Index==0) ? 23: 59;
			}
			if(Seg_Disp_Mode == 2)
			{
				Alarm_Set[Clock_Set_Index]--;
				if(Alarm_Set[Clock_Set_Index] == 255)
					Alarm_Set[Clock_Set_Index] = (Clock_Set_Index==0) ? 23: 59;
			}
		break;
		case 7:
			if(Seg_Disp_Mode == 1)
			{
				Clock_Disp[0] =  Clock_Set[0];
				Clock_Disp[1] =  Clock_Set[1];
				Clock_Disp[2] =  Clock_Set[2];
				Seg_Disp_Mode = 0;
			}
			if(Seg_Disp_Mode == 2)
			{
				Alarm[0] = Alarm_Set[0];
				Alarm[1] = Alarm_Set[1];
				Alarm[2] = Alarm_Set[2];
				Seg_Disp_Mode = 0;
			}
		break;
		case 8:
			Seg_Disp_Mode = 0;
		break;
		case 2:
			Clock_Set_Index = 0;
			Alarm_Set[0] = Alarm[0];
			Alarm_Set[1] = Alarm[1];
			Alarm_Set[2] = Alarm[2];
			Seg_Disp_Mode = 2;
		break;
		case 4:
			Alarm_Flag ^= 1;
		break;
	}		
}

/*信息处理函数*/
void Seg_Proc()
{
	//unsigned char i;
	//unsigned char j;
	
	if(Seg_Slow_Down) return;
	Seg_Slow_Down = 1;
	
	switch(Seg_Disp_Mode)
	{
		case 0:
			Seg_Buf[0] = Clock_Disp[0] / 10 % 10;
			Seg_Buf[1] = Clock_Disp[0] / 1 % 10;
			Seg_Buf[2] = Clock_Disp[1] / 10 % 10;
			Seg_Buf[3] = Clock_Disp[1] / 1 % 10;
			Seg_Buf[4] = Clock_Disp[2] / 10 % 10;
			Seg_Buf[5] = Clock_Disp[2] / 1 % 10;
//			for(i=0;i<3;i++)
//			{
//				Seg_Buf[2*i] = Clock_Disp[i] / 10 % 10;
//				Seg_Buf[2*i+1] = Clock_Disp[i] / 1 % 10;
//			}
		break;
		case 1:
			Seg_Buf[0] = Clock_Set[0] / 10 % 10;
			Seg_Buf[1] = Clock_Set[0] / 1 % 10;
			Seg_Buf[2] = Clock_Set[1] / 10 % 10;
			Seg_Buf[3] = Clock_Set[1] / 1 % 10;
			Seg_Buf[4] = Clock_Set[2] / 10 % 10;
			Seg_Buf[5] = Clock_Set[2] / 1 % 10;	
		/*
			switch(Clock_Set_Index)
			{
				case 0:	//小时闪烁
					Seg_Buf[0] = Seg_Flag ? (Clock_Set[0] / 10 % 10) : 10;
					Seg_Buf[1] = Seg_Flag ? (Clock_Set[0] / 1 % 10) : 10;
				break;
				case 1:	//分钟闪烁
					Seg_Buf[2] = Seg_Flag ? (Clock_Set[1] / 10 % 10) : 10;
					Seg_Buf[3] = Seg_Flag ? (Clock_Set[1] / 1 % 10) : 10;
				break;
				case 2:	//秒钟闪烁
					Seg_Buf[4] = Seg_Flag ? (Clock_Set[2] / 10 % 10) : 10;
					Seg_Buf[5] = Seg_Flag ? (Clock_Set[2] / 1 % 10) : 10;
				break;
			}
		*/	
			Seg_Buf[2*Clock_Set_Index] = Seg_Flag ? (Clock_Set[Clock_Set_Index] / 10 % 10) : 10;
			Seg_Buf[2*Clock_Set_Index+1] = Seg_Flag ? (Clock_Set[Clock_Set_Index] / 1 % 10) : 10;
		break;
		case 2:
			Seg_Buf[0] = Alarm_Set[0] / 10 % 10;
			Seg_Buf[1] = Alarm_Set[0] / 1 % 10;
			Seg_Buf[2] = Alarm_Set[1] / 10 % 10;
			Seg_Buf[3] = Alarm_Set[1] / 1 % 10;
			Seg_Buf[4] = Alarm_Set[2] / 10 % 10;
			Seg_Buf[5] = Alarm_Set[2] / 1 % 10;
		
			Seg_Buf[2*Clock_Set_Index] = Seg_Flag ? (Alarm_Set[Clock_Set_Index] / 10 % 10) : 10;
			Seg_Buf[2*Clock_Set_Index+1] = Seg_Flag ? (Alarm_Set[Clock_Set_Index] / 1 % 10) : 10;
			//指针可共用
		break;
	}
}

/*其他显示函数*/
void Led_Proc()
{
	if(Alarm_Flag == 1)
	{
		if(Clock_Disp[0]==Alarm[0] && Clock_Disp[1]==Alarm[1] && Clock_Disp[2]==Alarm[2])
		{
			Alarm_Enable_Flag = 1;
		}
		if(Alarm_Enable_Flag == 1)
		{
			P2_3 = 0;
			P1 = Led;
		}	
		else
		{
			P2_3 = 1;
			P1 = 0XFF;
		}
	}
	else
	{
		P2_3 = 1;
		P1 = 0XFF;
	}
}

/*定时器0初始化函数*/
void Timer0_Init(void)		//1毫秒@12.000MHz
{
	//删除12T行
	//添加2行
	TMOD &= 0xF0;			//设置定时器模式
	TMOD |= 0x01;			//设置定时器模式
	TL0 = 0x18;				//设置定时初始值
	TH0 = 0xFC;				//设置定时初始值
	TF0 = 0;				//清除TF0标志
	TR0 = 1;				//定时器0开始计时
	
	ET0 = 1;	//先写ET0,再写EA
	EA = 1;
}

/*定时器0中断服务函数*/
void Timer0_Server() interrupt 1
{
	TL0 = 0x18;				//设置定时初始值
	TH0 = 0xFC;				//设置定时初始值
	
	if(++Key_Slow_Down == 10) Key_Slow_Down=0;
	if(++Seg_Slow_Down == 500) Seg_Slow_Down=0;
	
	if(++Seg_Pos == 6) Seg_Pos=0;
	Seg_Disp(Seg_Pos,Seg_Buf[Seg_Pos],Seg_Point[Seg_Pos]);
	
	if(++Timer_1000ms == 1000)
	{
		Timer_1000ms = 0;
		Clock_Disp[2]++;
		if(Clock_Disp[2] == 60)
		{
			Clock_Disp[2] = 0;
			Clock_Disp[1]++;
			if(Clock_Disp[1] == 60)
			{
				Clock_Disp[1] = 0;
				Clock_Disp[0]++;
				if(Clock_Disp[0] == 24)
				{
					Clock_Disp[0] = 0;
				}
			}
		}
	}
	
	if(++Timer_500ms == 500)
	{
		Timer_500ms = 0;
		Seg_Flag ^= 1;
		
		if(Clock_Disp[0] >= 12)
			Led ^= 0XF0;
		else
			Led ^= 0X0F;
	}
}

/*main*/
void main()
{
	Timer0_Init();
	while(1)
	{
		Key_Proc();
		Seg_Proc();
		Led_Proc();
	}
}