从单片机基础到程序框架(全集 2019pdf版) - 第359页
void main( ) { while(1) { switch(G u8CycleStep ) { case 0: Gu32CycleC nt++; //这里就是累 计 main 函数内部的 主循环 w hile(1)的次 数 if(Gu32Cyc leCnt>=CY CLE_SUM) //累计 的次数 达到设定值 CY CLE_SUM 就跳 到下一步骤 { Gu32Cycl eCnt=0; //及时清零计 数器,为 下一步骤的…

第八十三节: 累计主循环的“非阻塞”延时控制 LED 闪烁。
【83.1 累计主循环的“非阻塞”。】
上一节提到,当 Delay 的“阻塞”时间超过 1ms 并且被频繁调用的时候,由于 Delay 做“独占式无用功”
而消耗的延时太长,会影响其它任务的并行处理,整个系统给人的感觉非常卡顿不流畅。为了解决此问题,
本节引入累计主循环的“非阻塞”,同时,希望通过此例子,让大家第一次感受到 switch 语句在多任务并行
处理时候的优越性。switch 的精髓在于“根据某个判断条件实现步骤之间的灵活跳转”,这个思路是以后做
所有大项目的框架性思路。
为什么“累计主循环”可以兼顾到其它任务的并行处理?因为单片机进入 main 函数以后,在一个主循
环里要扫描 N 个任务,从头到尾,把 N 个任务扫描一遍,每扫描一遍算“一次主循环”,每一次“主循环”
都是要消耗一点时间,累计的“主循环”次数越多,所要消耗的时间就越长,但是跟 Delay 唯一的差别是,
Delay 做延时的时候没有办法扫描其它任务,而“累计主循环”内部本身就是在不断扫描其它任务,产生时
间越长扫描其它任务的次数就越多,两者是完全相互促进而没有矛盾的。具体内容,请看下面的例子。
【83.2 累计主循环“非阻塞”的一个例子。】
现在利用“累计主循环非阻塞”编写一个练习程序,让一个 LED 灯闪烁。例子如下:
图 83.2.1 灌入式驱动 8 个 LED
#include "REG52.H"
#define CYCLE_SUM 5000 //累计主循环次数的设定阀值,该值决定了 LED 闪烁频率
sbit P0_0=P0^0; //利用 sbit 和符号“^”的组合,把变量名字 P0_0 与 P0.0 引脚关联起来
unsigned char Gu8CycleStep=0; //switch 的跳转步骤
unsigned long Gu32CycleCnt=0; //累计主循环的计数器

void main()
{
while(1)
{
switch(Gu8CycleStep)
{
case 0:
Gu32CycleCnt++; //这里就是累计 main 函数内部的主循环 while(1)的次数
if(Gu32CycleCnt>=CYCLE_SUM) //累计的次数达到设定值 CYCLE_SUM 就跳到下一步骤
{
Gu32CycleCnt=0; //及时清零计数器,为下一步骤的新一轮计数准备
P0_0=0; //LED 灯亮。
Gu8CycleStep=1; //跳到下一步骤
}
break;
case 1:
Gu32CycleCnt++; //这里就是累计 main 函数内部的主循环 while(1)的次数
if(Gu32CycleCnt>=CYCLE_SUM) //累计的次数达到设定值 CYCLE_SUM 就返回上一步骤
{
Gu32CycleCnt=0; //及时清零计数器,为返回上一步骤的新一轮计数准备
P0_0=1; //LED 灯灭。
Gu8CycleStep=0; //返回到上一个步骤
}
break;
}
}
}
【83.3 累计主循环的不足。】
上述 83.2 例子中,“累计主循环次数”实现时间延时是一个不错的选择。这种方法能胜任多任务处理的
程序框架,但是本身也有一个小小的不足,比如“阀值 CYCLE_SUM 到底应该取多少才能产生多长的时间”是
没有标准的,只能依靠不断上机实验来拿到一个你所需要的数值,这种“不规范”,当程序要移植到其它单
片机平台上的时候就特别麻烦,需要重新修正阀值 CYCLE_SUM。除此之外,哪怕在同样的一个单片机里,随
着主函数里任务量的增加,累计一次主循环所消耗的时间长度也会发生变化,意味着靠“累计主循环次数”
所获得的时间也会发生变化而导致不准确,此时,为了保证延时时间的准确性,必须要做的就是再一次修正
“设定累计主循环次数”的阀值 CYCLE_SUM,这样显然给我们带来了一丝不便,怎么办?假设单片机没有“定
时中断”这个资源,那么这种“累计主循环次数”在多任务处理中确实是不二之选,但是,因为现在几乎所
有的单片机内部都有“定时中断”这个资源,所以,大家不用为这个“不足”而烦恼,我们只要用上本节的
switch 思路,再外加一个“定时中断”,就可以轻松解决此问题,下一节就跟大家讲“定时中断”的内容。

第八十四节: 中断与中断函数。
【84.1 中断。】
单片机的“中断”跟日常生活的“中断”差不多,我正在做“常事”的时候,突然遇到优先级更高的“急
事”,这时我必须先暂停手上的“常事”,马上去处理突如其来的“急事”,处理完“急事”再返回来继续做
“常事”。要理解单片机的“中断”,有六个关键点,第一点是“配置中断”,第二点是“做常事”,第三点是
“中断请求”,第四点是“保护中断现场”,第五点是“处理急事”,第六点是“返回中断现场”。举一个我生
活的例子如下:
第一点:我老婆随时都会打电话给我,所以我的手机 24 小时都打开处于待机的状态。(配置中断)
第二点:我正在读一本书《道德经》(做常事)。
第三点:当我读到第 18 页的时候,老婆突然给我打电话,让我去幼儿园帮接一下小孩(中断请求)。
第四点:我在第 18 页里夹了一张书签做标记(保护中断现场)。
第五点:我放下手上的书去幼儿园接小孩(处理急事)。
第六点:接了小孩,我回来继续打开《道德经》,找到书签标记的第 18 页(返回中断现场),继续阅读。
上述六点,在单片机的 C 语言里,“配置中断”放在主函数的初始化那里,“做常事”放在主函数的主循
环里(main 函数内部的 while(1)循环),“中断请求”单片机内部硬件检测到符合发生中断的条件,“保护中
断现场”是单片机内部硬件电路自动处理的(不需要我们软件干涉),“处理急事”是单片机自动跳转到另外
开辟的一个特殊中断函数处理(自动跳转是单片机的硬件自动完成不需要我们软件干涉),执行完一次中断
函数后单片机再自动跳转到主函数的主循环的现场点继续从现场点开始继续做常事(返回中断现场)。在这
六点中,其中第四点的“保护中断现场”与第六点的“返回中断现场”是要特别强调的,单片机从 main 函
数的主循环 while(1)准备跳转到中断函数之前,它会自动记录当前的位置(做好路标),以便处理完中断函
数后再返回 main 函数的主循环 while(1)时,能找到之前的被中断跳转前的位置,这样就可以接上原来的步
骤去处理原来的“常事”,在步骤上既不提前也不滞后恰到好处,中断就不会影响到常事的完整性。代码分
布图的模板描述如下:
void main()
{
配置中断;
while(1)
{
处理常事;
}
}
void 中断函数() interrupt 序号 //中断函数后缀带“interrupt 序号”特别修饰
{
急事;
}
奇怪!上述代码,为什么“main 函数”与“中断函数”在软件上看不到任何关联,既不存在“main 函
数”调用“中断函数”,也不存在“中断函数”调用“main 函数”的情况,在观感上,“main 函数”与“中