阶段3
前言
- 系统性自律的培养(学习如何自律)
- 注意向上社交的重要性,注意保留平时向上社交的机会!
一,2024零基础入门(四)
1.1国赛模板v1.0
详情参考视频2024零基础入门(四)、协会第四次培训.pdf
多尝试,多敲
1.2补充代码
图 1 新增代码1 图 2 新增代码21.3补充代码说明
核心思想:把CPU时间切成片,像餐厅服务员一样工作
想象一下,CPU就像一家餐厅里只有一个服务员。这个服务员需要同时照顾好几桌客人(不同的任务,比如按键检测、数码管显示、计算等)。如果他一直待在某一桌客人那里直到点完所有菜,其他客人就会等得不耐烦(程序“卡住”)。
一个好的服务员会这样做:他为每一桌服务一小会儿,然后迅速切换到下一桌,循环往复。这样,所有客人都感觉被照顾到了。这在程序里,就叫 “时间片轮询”。
“减速”和定时器,就是实现这个服务员工作模式的工具。
1. “减速”到底是什么?—— 防止“霸占”CPU
你的代码里 Key_Slow_Down和 Seg_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_Down和Seg_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程序模板思路
- 文件说明
- .c文件 编写底层函数
- .h文件 声明底层函数
- 整体流程
- 先写
Key.c,Key.h,Seg.c,Seg.h。 - 再写
main.c
main.c整体流程
- 头文件
- 变量声明
- 按键处理函数
Key_Proc() - 信息处理函数
Seg_Proc() - 其他显示函数
- 定时器0初始化
- 定时器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;
}
具体步骤:
- 使用STC-ISP生成函数
- 额外添加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做题思路
- 先略读一遍题目要求
详细文件见第二届 创意智造·奠基未来 单片机设计与开发大赛 模拟题(2).pdf
- 建立模板
- 首先写数码管
- 按键功能由易到难依次写功能
- 写完一个模块或者一个功能就可以编译仿真看看有没有问题
- 数字思维:
- 蓝桥杯中,定义数组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完整代码
==重要说明==:
- 多动手敲代码,按照做题思路来,不要死背代码。
- 培养自己检查错误的能力。
- 写一部分就可以编译,仿真看看效果是否合理。
- 注重模板的作用,熟练使用模板。
- 完整代码如下:
/*头文件声明*/
#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
- 整体看看题目
- 看到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 由数码管入手
- 所有程序都从数码管开始写,把需要显示的数据写上去
/*变量声明区*/
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次) 才触发条件。这引入了一次中断周期的延迟,通常不是设计定时周期的本意。
两种代码区别总结
简单来说,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();
}
}





