看到吴老师的技术连载,里面大量实例和注释。很实在很有用!PDF版如下,90讲,含源程序。
  手把手教你单片机程序框架(90讲)含源程序.pdf (4.0 MB)
  
第一节:吴坚鸿谈初学单片机的误区。
第二节:delay()延时实现LED灯的闪烁。
第三节:累计主循环次数使LED灯闪烁。
第四节:累计定时中断次数使LED灯闪烁。
第五节:蜂鸣器的驱动程序。
第六节:在主函数中利用累计主循环次数来实现独立按键的检测。
第七节:在主函数中利用累计定时中断的次数来实现独立按键的检测。
第八节:在定时中断函数里执行独立按键的扫描程序。
第九节:独立按键的双击按键触发。
第十节:两个独立按键的组合按键触发。
第十一节:同一个按键短按与长按的区别触发。
第十二节:按住一个独立按键不松手的连续步进触发。
第十三节:按住一个独立按键不松手的加速匀速触发。
第十四节:矩阵键盘的单个触发。
第十五节:矩阵键盘单个触发的压缩代码编程。
第十六节:矩阵键盘的组合按键触发。
第十七节:两片联级74HC595驱动16个LED灯的基本驱动程序。
第十八节:把74HC595驱动程序翻译成类似单片机IO口直接驱动的方式。
第十九节:依次逐个点亮LED之后,再依次逐个熄灭LED的跑马灯程序。
第二十节:依次逐个亮灯并且每次只能亮一个灯的跑马灯程序。
第二十一节:多任务并行处理两路跑马灯。
第二十二节:独立按键控制跑马灯的方向。
第二十三节:独立按键控制跑马灯的速度。
第二十四节:独立按键控制跑马灯的启动和暂停。
第二十五节:用LED灯和按键来模拟工业自动化设备的运动控制。
第二十六节:在主函数while循环中驱动数码管的动态扫描程序。
第二十七节:在定时中断里动态扫描数码管的程序。
第二十八节:数码管通过切换窗口来设置参数。
第二十九节:数码管通过切换窗口来设置参数,并且不显示为0的高位。
第三十节:数码管通过闪烁来设置数据。
第三十一节:数码管通过一二级菜单来设置数据的综合程序。
第三十二节:数码管中的倒计时程序。
第三十三节:能设置速度档位的数码管倒计时程序。
第三十四节:在数码管中实现iphone4S开机密码锁的程序。
第三十五节:带数码管显示的象棋比赛专用计时器。
第三十六节:带数码管显示的加法简易计算器。
第三十七节:数码管作为仪表盘显示跑马灯的方向,速度和运行状态。
第三十八节:判断数据尾来接收一串数据的串口通用程序框架。
第三十九节:判断数据头来接收一串数据的串口通用程序框架。
第四十节:常用的自定义串口通讯协议。
第四十一节:在串口接收中断里即时解析数据头的特殊程序框架。
第四十二节:通过串口用delay延时方式发送一串数据。
第四十三节:通过串口用计数延时方式发送一串数据。
第四十四节:从机的串口收发综合程序框架
第四十五节:主机的串口收发综合程序框架
第四十六节:利用AT24C02进行掉电后的数据保存。
第四十七节:操作AT24C02时,利用“一气呵成的定时器延时”改善数码管的闪烁现象。
第四十八节:利用DS1302做一个实时时钟  。
第四十九节:利用DS18B20做一个温控器  。
第五十节:利用ADC0832采集电压信号,用平均法和区间法进行软件滤波处理。
第五十一节:利用ADC0832采集电压信号,用连续N次一致性的方法进行滤波处理。
第五十二节:程序后续升级修改的利器,return语句鲜为人知的用法。
第五十三节:指针的第一大好处,让一个函数可以封装多个相当于return语句返回的参数。
第五十四节:指针的第二大好处,指针作为数组在函数中的输入接口。
第五十五节:指针的第三大好处,指针作为数组在函数中的输出接口。
第五十六节:指针的第四大好处,指针作为数组在函数中的输入输出接口。
第五十七节:为指针加上紧箍咒const,避免意外修改了只做输入接口的数据。
第五十八节:指针的第五大好处,指针在众多数组中的中转站作用。
第五十九节:串口程序第40,44,45节中存在一个bug,特此紧急公告。
第六十节:用关中断和互斥量来保护多线程共享的全局变量。
第六十一节:组合BCD码,非组合BCD码,以及数值三者之间的相互转换和关系。
第六十二节:大数据的加法运算。
第六十三节:大数据的减法运算。
第六十四节:大数据的乘法运算。
第六十五节:大数据的除法运算。
第六十六节:单片机外部中断的基础。
第六十七节:利用外部中断实现模拟串口数据的收发。
第六十八节:单片机C语言的多文件编程技巧。
第六十九节:使用static关键字可以减少全局变量的使用。
第七十节:深入讲解液晶屏的构字过程。
第七十一节:液晶屏的字符,16点阵,24点阵和32点阵的显示程序。
第七十二节:在液晶屏中把字体顺时针旋转90度显示的算法程序。
第七十三节:在液晶屏中把字体镜像显示的算法程序。
第七十四节:在液晶屏中让字体可以跨区域无缝对接显示的算法程序。
第七十五节:在12864液晶屏中让字体以1个点阵为单位进行移动显示的算法程序。
第七十六节:如何把一个任意数值的变量显示在液晶屏上。
第七十七节:在1个窗口里通过移动光标来设置不同参数的液晶屏菜单程序。
第七十八节:在多个窗口里通过移动光标来设置不同参数的液晶屏菜单程序。
第七十九节:通过主菜单移动光标来进入子菜单窗口的液晶屏程序。
第八十节:调用液晶屏内部字库来显示汉字或字符的坐标体系和本质。
第八十一节:液晶屏显示串口发送过来的任意汉字和字符。
第八十二节:如何通过调用液晶屏内部字库把一个任意数值的变量显示出来。
第八十三节:矩阵键盘输入任意数字或小数点的液晶屏显示程序。
第八十四节:实时同步把键盘输入的BCD码数组转换成数值的液晶屏显示程序。
第八十五节:实时同步把加减按键输入的数值转换成BCD码数组的液晶屏显示程序。
第八十六节:数字键盘与液晶菜单的综合程序。
第八十七节:郑文显捐赠的工控项目源代码。
第八十八节:电子称连续不断从串口对外发送数据,单片机靠关键字快速截取有效数据串。
第八十九节:用单片机内部定时器做一个时钟。
第九十节:针对行程开关感应器,分享一种既能及时响应,又能抗干扰处理的识别思路。
    第一节:吴坚鸿谈初学单片机的误区。
  (1)很难记住繁杂的寄存器?寄存器不用死记硬背,我做了那么久单片机项目的开发,连一个寄存器都记不住。需要配置寄存器的时候,直接在网上或者书本上参考别人现成的配置程序是上策,查找芯片数据手册是中策,死记硬背寄存器是最最下策。
  (2)很难记住繁杂的汇编语言指令?除非是在校学生要应付考试或者少数工作中绕不开汇编,否则学汇编就是浪费时间。我从来就没有用汇编帮客户做过一个项目。
  (3)C语言很难学?你不用学指针,你不用学带形参的函数,你不用学结构体,你不用学宏定义,你不用学文件操作,你也不用死记繁琐的数据类型。你只要会:
      5条指令语句switch语句,if else语句,while语句,for语句,=赋值语句。
      7个运算符+,-,*,/,|,&,!。
      4个逻辑关系符||,&&,!=,==.
      3个数据类型unsigned char, unsigned int, unsigned long。
      3个进制相互转化,二进制,十六进制,十进制。
      1个void函数。         
      1个一维数组code(或const) unsigned char array[]。
      那么世界上任何一种逻辑功能的单片机软件你都能做出来。
      我当年刚毕业出来工作的时候才知道可以用C语言开发单片机,一开始只用if语句就把项目做出来了,没有用指针,没有用带形参的函数等复杂的功能。再到后来才慢慢开始用C语言其他的高级功能,但是我发现C语言其他的高级功能,本质上都是用我前面列举出来的最基本功能集合而成,只是书写更加简单方便了一点,编译后的机器码都大同小异。所以不会指针等高级功能你不用自卑,恰恰相反,当你会最简单的几个语句,就把这些高级功能的程序都做出来了,你才发现你对底层了解得更加透切,再学那些高级功能轻而易举。当你裸机跑的程序都能够协调得很好的时候,你才发现所谓高深的操作系统也不过如此,只要给你时间和金钱你也可以写个操作系统来玩玩。
  (4)很难记住精确时间的计算公式?经常看到时间公式等于晶振,时钟周期,执行指令次数他们之间的乘除关系式。我认为这些都是浮云,不用纠结也不用去记,大概了解一下就可以了。不管你对公式掌握得有多精确,你都不可能做出非常精确的时间。想用单片机做一个非常精确的时间这种想法一开始就是错的,不可能的。真想做一个比较精确的时间,应该用外围时钟芯片或者FPGA和CPLD,而不是单片机。
  (5)很难记住繁杂的各种通信协议?什么IIC,SPI,232串口通讯,CAN,USB等等。这些都是浮云,你不用记那么多,你只要理解两种通讯方式就够了,那就是串行通讯方式和并行通讯方式。不管世界上有多少种通讯协议,物理世界上只有这两种通讯方式,其他各种名称的通讯协议都基于此两种方式演变而来。
  (6)很难写短小精悍的程序?初学者不要纠结于此。做项目开发,程序容量不是刻意追求的目标,程序多一点少一点没关系,现在大容量的单片机品种非常多,容量不会是寸土寸金的事情,我们更加要关注程序的运行效率,可读性和可修改性。
  既然我列出了那么多误区,那么什么才是初学者关注的核心?预知详情,请听下回分解----delay()延时实现LED灯的闪烁。
  第二节:delay()延时实现LED灯的闪烁。
  开场白:
    上一节鸿哥列出了初学者七大误区,到底什么才是初学者关注的核心?那就是裸机奔跑的程序结构。一个好的程序结构,本身就是一个微型的多任务操作系统。鸿哥教给大家的就是如何编写这个简单的操作系统。在main函数循环中用switch语句实现多任务并行处理的任务切换,再外加一个定时器中断,这两者的结合就是鸿哥多年来所有实战项目的核心。鸿哥的程序结构看似简单,实际上就是那么简单。大家不用着急,本篇连载文章现在才正式开始,这一节我要教会大家两个知识点:
第一点:鸿哥首次提出的“三区一线”理论。此理论把程序代码分成三个区,一个延时分割线。
第二点:delay()延时的用途。
  (1)硬件平台:基于朱兆祺51单片机学习板。
  (2)实现功能:让一个LED闪烁。
  (3)源代码讲解如下:
  
#include "REG52.H"  void initial_myself();    void initial_peripheral();  void delay_short(unsigned int uiDelayshort); void delay_long(unsigned int uiDelaylong); void led_flicker();  /* 注释一: * 吴坚鸿个人的命名风格:凡是输出后缀都是_dr,凡是输入后缀都是_sr。 * dr代表drive驱动,sr代表sensor感应器 */ sbit led_dr=P3^5;    void main()  //学习要点:深刻理解鸿哥首次提出的三区一线理论   { /* 注释二: * initial_myself()函数属于鸿哥三区一线理论的第一区, * 专门用来初始化单片机自己的寄存器以及个别外围要求响应速度快的输出设备, * 防止刚上电之后,由于输出IO口电平状态不确定而导致外围设备误动作, * 比如继电器的误动作等等。 */    initial_myself();  /* 注释三: * 此处的delay_long()延时函数属于第一区与第二区的分割线, * 延时时间一般是0.3秒到2秒之间,等待外围芯片和模块上电稳定。 * 比如液晶模块,AT24C02存储芯片,DS1302时钟芯片, * 这类芯片有个特点,一般都是跟单片机进行串口或并口通讯的, * 并且不要求上电立即处理的。 */    delay_long(100);  /* 注释四: * initial_peripheral()函数属于鸿哥三区一线理论的第二区, * 专门用来初始化不要求上电立即处理的外围芯片和模块. * 比如液晶模块,AT24C02存储芯片,DS1302时钟芯片。 * 本程序基于朱兆祺51单片机学习板。 */    initial_peripheral();  /* 注释五: * while(1){}主函数循环区属于鸿哥三区一线理论的第三区, * 专门用来编写被循环扫描到的非中断应用程序 */    while(1)    {       led_flicker();   //LED闪烁应用程序    }  }  void led_flicker() //LED闪烁应用程序 {   led_dr=1;  //LED亮   delay_short(50000);  //延时50000个空指令的时间  /* 注释六: * delay_long(100)延时50000个空指令的时间,因为内嵌了一个500次的for循环 */   led_dr=0;  //LED灭   delay_long(100);    //延时50000个空指令的时间   }   /* 注释七: * delay_short(unsigned int uiDelayShort)是小延时函数, * 专门用在时序驱动的小延时,一般uiDelayShort的数值取10左右, * 最大一般也不超过100.本例为了解释此函数的特点,取值范围超过100。 * 此函数的特点是时间的细分度高,延时时间不宜过长。uiDelayShort数值 * 的大小就代表里面执行了多少条空指令的时间。数值越大,延时越长。 * 时间精度不要刻意去计算,感觉差不多就行。 */ void delay_short(unsigned int uiDelayShort) {    unsigned int i;      for(i=0;i<uiDelayShort;i++)    {      ;   //一个分号相当于执行一条空语句    } }   /* 注释八: * delay_long(unsigned int uiDelayLong)是大延时函数, * 专门用在上电初始化的大延时, * 此函数的特点是能实现比较长时间的延时,细分度取决于内嵌for循环的次数, * uiDelayLong的数值的大小就代表里面执行了多少次500条空指令的时间。 * 数值越大,延时越长。时间精度不要刻意去计算,感觉差不多就行。 */ void delay_long(unsigned int uiDelayLong) {    unsigned int i;    unsigned int j;    for(i=0;i<uiDelayLong;i++)    {       for(j=0;j<500;j++)  //内嵌循环的空指令数量           {              ; //一个分号相当于执行一条空语句           }    } }    void initial_myself()  //初始化单片机 {   led_dr=0;  //LED灭 } void initial_peripheral() //初始化外围 {   ;   //本例为空 }  总结陈词:
鸿哥首次提出的“三区一线”理论概况了各种项目程序的基本分区。我后续的程序就按此分区编写。
Delay()函数的长延时适用在上电初始化。
Delay()函数的短延时适用在驱动时序的脉冲延时,此时的时间不能太长,本例中暂时没有列出这方面的例子,在后面的章节中会提到。
在本例源代码中,在led_flicker()闪烁应用程序里用到的两个延时delay,它们的延时时间都太长了,在实战项目中肯定不能用这种延时,因为消耗的时间太长了,其它任务根本没有机会执行。那怎么办呢?我们应该如何改善?欲知详情,请听下回分解-----累计主循环次数使LED灯闪烁。
  第三节:累计主循环次数使LED灯闪烁。
  开场白:
上一节鸿哥提到delay()延时函数消耗的时间太长了,其它任务根本没有机会执行,我们该怎么改善?本节教大家利用累计主循环次数的方法来解决这个问题。这一节要教会大家两个知识点:
第一点:利用累计主循环次数的方法实现时间延时
第二点:switch核心语句之初体验。 鸿哥所有的实战项目都是基于switch语句实现多任务并行处理。
(1)硬件平台:基于朱兆祺51单片机学习板。
(2)实现功能:让一个LED闪烁。
(3)源代码讲解如下:
  
#include "REG52.H"   /* 注释一: * const_time_level是统计循环次数的设定上限,数值越大,LED延时的时间越久 */ #define const_time_level 10000    void initial_myself();    void initial_peripheral(); void delay_long(unsigned int uiDelaylong); void led_flicker();  sbit led_dr=P3^5;    /* 注释二: * 吴坚鸿个人的命名风格:凡是switch语句里面的步骤变量后缀都是Step. * 前缀带uc,ui,ul分别表示此变量是unsigned char,unsigned int,unsigned long. */ unsigned char ucLedStep=0; //步骤变量 unsigned int  uiTimeCnt=0; //统计循环次数的延时计数器 void main()   {    initial_myself();      delay_long(100);       initial_peripheral();    while(1)       {       led_flicker();       }  }  void led_flicker() //第三区 LED闪烁应用程序 {      switch(ucLedStep)   {      case 0: /* 注释三: * uiTimeCnt累加循环次数,只有当它的次数大于或等于设定上限const_time_level时, * 才会去改变LED灯的状态,否则CPU退出led_flicker()任务,继续快速扫描其他的任务, * 这样的程序结构就可以达到多任务并行处理的目的。 * 本程序基于朱兆祺51单片机学习板 */           uiTimeCnt++;  //累加循环次数,                   if(uiTimeCnt>=const_time_level) //时间到                   {                      uiTimeCnt=0; //时间计数器清零              led_dr=1;    //让LED亮                          ucLedStep=1; //切换到下一个步骤                   }               break;      case 1:           uiTimeCnt++;  //累加循环次数,                   if(uiTimeCnt>=const_time_level) //时间到                   {                      uiTimeCnt=0; //时间计数器清零              led_dr=0;    //让LED灭                          ucLedStep=0; //返回到上一个步骤                   }               break;      }  }   void delay_long(unsigned int uiDelayLong) {    unsigned int i;    unsigned int j;    for(i=0;i<uiDelayLong;i++)    {       for(j=0;j<500;j++)  //内嵌循环的空指令数量           {              ; //一个分号相当于执行一条空语句           }    } }   void initial_myself()  //第一区 初始化单片机 {   led_dr=0;  //LED灭 } void initial_peripheral() //第二区 初始化外围 {   ;   //本例为空 }  总结陈词:
    在实际项目中,用累计主循环次数实现时间延时是一个不错的选择。这种方法能胜任多任务处理的程序框架,但是它本身也有一个小小的不足。随着主函数里任务量的增加,我们为了保证延时时间的准确性,要不断修正设定上限const_time_level 。我们该怎么解决这个问题呢?欲知详情,请听下回分解-----累计定时中断次数使LED灯闪烁。
  第四节:累计定时中断次数使LED灯闪烁。
  开场白:
上一节提到在累计主循环次数来实现计时,随着主函数里任务量的增加,为了保证延时时间的准确性,要不断修正设定上限阀值const_time_level 。我们该怎么解决这个问题呢?本节教大家利用累计定时中断次数的方法来解决这个问题。这一节要教会大家四个知识点:
第一点:利用累计定时中断次数的方法实现时间延时
第二点:展现鸿哥最完整的实战程序框架。在主函数循环里用switch语句实现状态机的切换,在定时中断里累计中断次数,这两个的结合就是我写代码最本质的框架思想。
第三点:提醒大家C语言中的int ,long变量是由几个字节构成的数据,凡是在main函数和中断函数里有可能同时改变的变量,这个变量应该在主函数中被更改之前,先关闭相应的中断,更改完了此变量,再打开中断,否则会留下不宜察觉的漏洞。当然在大部分的项目中可以不用这么操作,但是在一些要求非常高的项目中,有一些核心变量必须这么做。
第四点:定时中断的初始值该怎么设置。不用严格按公式来计算时间,一般取个经验值是最大初始值减去1000就可以了。
具体内容,请看源代码讲解。
  (1)硬件平台:基于朱兆祺51单片机学习板。
  (2)实现功能:让一个LED闪烁。
  (3)源代码讲解如下:
  
#include "REG52.H"  #define const_time_level 200    void initial_myself();    void initial_peripheral(); void delay_long(unsigned int uiDelaylong); void led_flicker(); void T0_time();  //定时中断函数  sbit led_dr=P3^5;    unsigned char ucLedStep=0; //步骤变量 unsigned int  uiTimeCnt=0; //统计定时中断次数的延时计数器   void main()   {    initial_myself();      delay_long(100);       initial_peripheral();    while(1)       {       led_flicker();       }  }  void led_flicker() //第三区 LED闪烁应用程序 {      switch(ucLedStep)   {      case 0: /* 注释一: * uiTimeCnt累加定时中断的次数,每一次定时中断它都会在中断函数里自加一。 * 只有当它的次数大于或等于设定上限const_time_level时, * 才会去改变LED灯的状态,否则CPU退出led_flicker()任务,继续快速扫描其他的任务, * 这样的程序结构就可以达到多任务并行处理的目的。这就是鸿哥在所有开发项目中的核心框架。 */                   if(uiTimeCnt>=const_time_level) //时间到                   {  /* 注释二: * ET0=0;uiTimeCnt=0;ET0=1;----在清零uiTimeCnt之前,为什么要先禁止定时中断? * 因为uiTimeCnt是unsigned int类型,本质上是由两个字节组成。 * 在C语言中uiTimeCnt=0看似一条指令,实际上经过编译之后它不只一条汇编指令。 * 由于定时中断函数里也对这个变量进行累加操作,如果不禁止定时中断, * 那么uiTimeCnt这个变量在main()函数中还没被完全清零的时候,如果这个时候 * 突然来一个定时中断,并且在中断里又更改了此变量,这种情况在某些要求高的 * 项目上会是一个不容易察觉的漏洞,为项目带来隐患。当然,大部分的普通项目, * 都可以不用那么严格,可以不用禁止定时中断。在这里只是提醒各位初学者有这种情况。 */              ET0=0;  //禁止定时中断                      uiTimeCnt=0; //时间计数器清零              ET0=1; //开启定时中断              led_dr=1;    //让LED亮                          ucLedStep=1; //切换到下一个步骤                   }               break;      case 1:                   if(uiTimeCnt>=const_time_level) //时间到                   {              ET0=0;  //禁止定时中断                      uiTimeCnt=0; //时间计数器清零              ET0=1;   //开启定时中断              led_dr=0;    //让LED灭                          ucLedStep=0; //返回到上一个步骤                   }               break;      }  }   /* 注释三: * C51的中断函数格式如下: * void 函数名() interrupt 中断号 * { *    中断程序内容 * } * 函数名可以随便取,只要不是编译器已经征用的关键字。 * 这里最关键的是中断号,不同的中断号代表不同类型的中断。 * 定时中断的中断号是 1.至于其它中断的中断号,大家可以查找 * 相关书籍和资料。大家进入中断时,必须先清除中断标志,并且 * 关闭中断,然后再写代码,最后出来时,记得重装初始值,并且 * 打开中断。 */ void T0_time() interrupt 1 {   TF0=0;  //清除中断标志   TR0=0; //关中断    if(uiTimeCnt<0xffff)  //设定这个条件,防止uiTimeCnt超范围。   {       uiTimeCnt++;  //累加定时中断的次数,   }  TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f TL0=0x2f; TR0=1;  //开中断 }   void delay_long(unsigned int uiDelayLong) {    unsigned int i;    unsigned int j;    for(i=0;i<uiDelayLong;i++)    {       for(j=0;j<500;j++)  //内嵌循环的空指令数量           {              ; //一个分号相当于执行一条空语句           }    } }   void initial_myself()  //第一区 初始化单片机 {  /* 注释四: * 单片机有几个定时器,每个定时器又有几种工作方式, * 那么多种变化,我们记不了那么多,怎么办? * 大家记住鸿哥的话,无论一个单片机有多少内置资源, * 我们做系统框架的,只需要一个定时器,一种工作方式。 * 开定时器越多这个系统越不好。需要哪种定时工作方式呢? * 就需要响应定时中断后重装一下初始值继续跑那种。 * 在51单片机中就是工作方式1。其它的工作方式很少项目能用到。 */   TMOD=0x01;  //设置定时器0为工作方式1     /* 注释五: * 装定时器的初始值,就像一个水桶里装的水。如果这个桶是空桶,那么想 * 把这个桶灌满水的时间就很长,如果是里面已经装了大半的水,那么想 * 把这个桶灌满水的时间就相对比较短。也就是定时器初始值越小,产生一次 * 定时中断的时间就越长。如果初始值太小了,每次产生定时中断 * 的时间分辨率太粗,如果初始值太大了,虽然每次产生定时中断的时间分辨率很细, * 但是太频繁的产生中断,不但会影响主函数main()的执行效率,而且累记中断次数 * 的时间误差也会很大。凭鸿哥多年的江湖经验, * 我觉得最大初始值减去2000是比较好的经验值。当然,大一点小一点没关系。不要走 * 两个极端就行。 */ TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f TL0=0x2f;    led_dr=0;  //LED灭 } void initial_peripheral() //第二区 初始化外围 {   EA=1;     //开总中断   ET0=1;    //允许定时中断   TR0=1;    //启动定时中断  }  总结陈词:
本节程序麻雀虽小五脏俱全。在本节中已经展示了我最完整的实战程序框架。
本节程序只有一个LED灯闪烁的单任务,如果要多增加一个任务来并行处理,该怎么办?
欲知详情,请听下回分解-----蜂鸣器的驱动程序。
  第五节:蜂鸣器的驱动程序。
  开场白:
上一节讲了利用累计定时中断次数实现LED灯闪烁,这个例子同时也第一次展示了我最完整的实战程序框架:用switch语句实现状态机,外加定时中断。这个框架看似简单,实际上就是那么简单。我做的所有开发项目都是基于这个简单框架,但是非常好用。上一节只有一个单任务的LED灯在闪烁,这节开始,我们多增加一个蜂鸣器报警的任务,要教会大家四个知识点:
第一点:蜂鸣器的驱动程序框架编写。
第二点:多任务处理的程序框架。
第三点:如何控制蜂鸣器声音的长叫和短叫。
第四点:如何知道1秒钟需要多少个定时中断,也就是如何按比例修正时间精度。
  具体内容,请看源代码讲解。
  (1)硬件平台:基于朱兆祺51单片机学习板。
  (2)实现功能:同时跑两个任务,第一个任务让一个LED灯1秒钟闪烁一次。第二个任务让蜂鸣器在前面3秒发生一次短叫报警,在后面6秒发生一次长叫报警,反复循环。
  (3)源代码讲解如下:
  
#include "REG52.H"  /* 注释一: * 如何知道1秒钟需要多少个定时中断? * 这个需要编写一段小程序测试,得到测试的结果后再按比例修正。 * 步骤: * 第一步:在程序代码上先写入1秒钟大概需要200个定时中断。 * 第二步:基于以上1秒钟的基准,编写一个60秒的简单测试程序(如果编写超过 * 60秒的时间,这个精度还会更高)。比如,编写一个用蜂鸣器的声音来识别计时的 * 起始和终止的测试程序。 * 第三步:把程序烧录进单片机后,上电开始测试,手上同步打开手机里的秒表。 *         如果单片机仅仅跑了27秒。 * 第四步:那么最终得出1秒钟需要的定时中断次数是:const_time_1s=(200*60)/27=444 */ #define const_time_05s 222   //0.5秒钟的时间需要的定时中断次数 #define const_time_1s 444   //1秒钟的时间需要的定时中断次数 #define const_time_3s 1332   //3秒钟的时间需要的定时中断次数 #define const_time_6s 2664   //6秒钟的时间需要的定时中断次数  #define const_voice_short  40   //蜂鸣器短叫的持续时间 #define const_voice_long   200  //蜂鸣器长叫的持续时间  void initial_myself();    void initial_peripheral(); void delay_long(unsigned int uiDelaylong); void led_flicker(); void alarm_run();    void T0_time();  //定时中断函数  sbit beep_dr=P2^7; //蜂鸣器的驱动IO口 sbit led_dr=P3^5;  //LED灯的驱动IO口  unsigned char ucLedStep=0; //LED灯的步骤变量 unsigned int  uiTimeLedCnt=0; //LED灯统计定时中断次数的延时计数器  unsigned char ucAlarmStep=0; //报警的步骤变量 unsigned int  uiTimeAlarmCnt=0; //报警统计定时中断次数的延时计数器  unsigned int  uiVoiceCnt=0;  //蜂鸣器鸣叫的持续时间计数器  void main()   {    initial_myself();      delay_long(100);       initial_peripheral();    while(1)      {       led_flicker();  //第一个任务LED灯闪烁           alarm_run();    //第二个任务报警器定时报警    }  }  void led_flicker() //第三区 LED闪烁应用程序 {      switch(ucLedStep)   {      case 0:             if(uiTimeLedCnt>=const_time_05s) //时间到            {              uiTimeLedCnt=0; //时间计数器清零              led_dr=1;    //让LED亮              ucLedStep=1; //切换到下一个步骤            }            break;      case 1:            if(uiTimeLedCnt>=const_time_05s) //时间到            {               uiTimeLedCnt=0; //时间计数器清零               led_dr=0;    //让LED灭               ucLedStep=0; //返回到上一个步骤            }            break;   }  }  void alarm_run() //第三区 报警器的应用程序 {      switch(ucAlarmStep)   {      case 0:             if(uiTimeAlarmCnt>=const_time_3s) //时间到            {              uiTimeAlarmCnt=0; //时间计数器清零 /* 注释二: * 只要变量uiVoiceCnt不为0,蜂鸣器就会在定时中断函数里启动鸣叫,并且自减uiVoiceCnt * 直到uiVoiceCnt为0时才停止鸣叫。因此控制uiVoiceCnt变量的大小就是控制声音的长短。 */              uiVoiceCnt=const_voice_short;  //蜂鸣器短叫              ucAlarmStep=1; //切换到下一个步骤            }            break;      case 1:            if(uiTimeAlarmCnt>=const_time_6s) //时间到            {               uiTimeAlarmCnt=0; //时间计数器清零               uiVoiceCnt=const_voice_long;  //蜂鸣器长叫               ucAlarmStep=0; //返回到上一个步骤            }            break;   }  }  void T0_time() interrupt 1 {   TF0=0;  //清除中断标志   TR0=0; //关中断    if(uiTimeLedCnt<0xffff)  //设定这个条件,防止uiTimeLedCnt超范围。   {       uiTimeLedCnt++;  //LED灯的时间计数器,累加定时中断的次数,   }    if(uiTimeAlarmCnt<0xffff)  //设定这个条件,防止uiTimeAlarmCnt超范围。   {       uiTimeAlarmCnt++;  //报警的时间计数器,累加定时中断的次数,   }   /* 注释三: * 为什么不把驱动蜂鸣器这段代码放到main函数的循环里去? * 因为放在定时中断里,能保证蜂鸣器的声音长度是一致的, * 如果放在main循环里,声音的长度就有可能受到某些必须 * 一气呵成的任务干扰,得不到及时响应,影响声音长度的一致性。 */     if(uiVoiceCnt!=0)   {      uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫          beep_dr=0;  //蜂鸣器是PNP三极管控制,低电平就开始鸣叫。   }   else   {      ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。            beep_dr=1;  //蜂鸣器是PNP三极管控制,高电平就停止鸣叫。   }     TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f   TL0=0x2f;   TR0=1;  //开中断 }   void delay_long(unsigned int uiDelayLong) {    unsigned int i;    unsigned int j;    for(i=0;i<uiDelayLong;i++)    {       for(j=0;j<500;j++)  //内嵌循环的空指令数量           {              ; //一个分号相当于执行一条空语句           }    } }   void initial_myself()  //第一区 初始化单片机 {   beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。   led_dr=0;  //LED灭    TMOD=0x01;  //设置定时器0为工作方式1     TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f   TL0=0x2f;  } void initial_peripheral() //第二区 初始化外围 {   EA=1;     //开总中断   ET0=1;    //允许定时中断   TR0=1;    //启动定时中断  }  总结陈词:
本节程序已经展示了一个多任务处理的基本思路,假如要实现一个独立按键检测,能不能也按照这种思路来处理呢?欲知详情,请听下回分解-----在主函数中利用累计主循环次数来实现独立按键的检测。
  第六节:在主函数中利用累计主循环次数来实现独立按键的检测。
  开场白:
上一节讲了多任务中蜂鸣器驱动程序的框架,这节继续利用多任务处理的方式,在主函数中利用累计主循环次数来实现独立按键的检测。要教会大家四个知识点:
第一点:独立按键的驱动程序框架。
第二点:用累计主循环次数来实现去抖动的延时。
第三点:灵活运用防止按键不松手后一直触发的按键自锁标志。
第四点:在按键去抖动延时计时中,添加一个抗干扰的软件监控判断。一旦发现瞬间杂波干扰,马上把延时计数器清零。这种方法是我在复杂的工控项目中总结出来的。以后凡是用到开关感应器的地方,都可以用类似的方法实现软件上的抗干扰处理。
  具体内容,请看源代码讲解。
  (1)硬件平台:基于朱兆祺51单片机学习板。用矩阵键盘中的S1和S5号键作为独立按键,记得把输出线P0.4一直输出低电平,模拟独立按键的触发地GND。
  (2)实现功能:有两个独立按键,每按一个独立按键,蜂鸣器发出“滴”的一声后就停。
  (3)源代码讲解如下:
  
#include "REG52.H"  #define const_voice_short  40   //蜂鸣器短叫的持续时间   /* 注释一: * 调整抖动时间阀值的大小,可以更改按键的触发灵敏度。 * 去抖动的时间本质上等于累计主循环次数的时间。 */ #define const_key_time1  500    //按键去抖动延时的时间 #define const_key_time2  500    //按键去抖动延时的时间  void initial_myself();    void initial_peripheral(); void delay_long(unsigned int uiDelaylong); void T0_time();  //定时中断函数 void key_service(); //按键服务的应用程序 void key_scan(); //按键扫描函数  sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键 sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键 sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平  sbit beep_dr=P2^7; //蜂鸣器的驱动IO口  unsigned char ucKeySec=0;   //被触发的按键编号  unsigned int  uiKeyTimeCnt1=0; //按键去抖动延时计数器 unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志  unsigned int  uiKeyTimeCnt2=0; //按键去抖动延时计数器 unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志  unsigned int  uiVoiceCnt=0;  //蜂鸣器鸣叫的持续时间计数器  void main()   {    initial_myself();      delay_long(100);       initial_peripheral();    while(1)      {        key_scan(); //按键扫描函数        key_service(); //按键服务的应用程序    }  }  void key_scan()//按键扫描函数 {   /* 注释二: * 独立按键扫描的详细过程: * 第一步:平时没有按键被触发时,按键的自锁标志和去抖动延时计数器一直被清零。 * 第二步:一旦有按键被按下,去抖动延时计数器开始累加,在还没累加到 *         阀值const_key_time1时,如果在这期间由于受外界干扰或者按键抖动,而使 *         IO口突然瞬间触发成高电平,这个时候马上又把延时计数器uiKeyTimeCnt1 *         清零了,这个过程非常巧妙,非常有效地去除瞬间的杂波干扰。这是我实战中摸索出来的。 *         以后凡是用到开关感应器的时候,都可以用类似这样的方法去干扰。 * 第三步:如果按键按下的时间超过了阀值const_key_time1,则触发按键,把编号ucKeySec赋值。 *         同时,马上把自锁标志ucKeyLock1置位,防止按住按键不松手后一直触发。 * 第四步:等按键松开后,自锁标志ucKeyLock1及时清零,为下一次自锁做准备。 * 第五步:以上整个过程,就是识别按键IO口下降沿触发的过程。 */   if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位   {      ucKeyLock1=0; //按键自锁标志清零          uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。         }   else if(ucKeyLock1==0)//有按键按下,且是第一次被按下   {      ++uiKeyTimeCnt1;  //延时计数器      if(uiKeyTimeCnt1>const_key_time1)      {         uiKeyTimeCnt1=0;         ucKeyLock1=1;  //自锁按键置位,避免一直触发         ucKeySec=1;    //触发1号键      }   }    if(key_sr2==1)   {      ucKeyLock2=0;          uiKeyTimeCnt2=0;   }   else if(ucKeyLock2==0)   {      ++uiKeyTimeCnt2;      if(uiKeyTimeCnt2>const_key_time2)      {         uiKeyTimeCnt2=0;         ucKeyLock2=1;         ucKeySec=2;     //触发2号键      }   }  }   void key_service() //第三区 按键服务的应用程序 {   switch(ucKeySec) //按键服务状态切换   {     case 1:// 1号键 对应朱兆祺学习板的S1键                uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。               ucKeySec=0;  //响应按键服务处理程序后,按键编号清零,避免一致触发           break;             case 2:// 2号键 对应朱兆祺学习板的S5键                uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。               ucKeySec=0;  //响应按键服务处理程序后,按键编号清零,避免一致触发           break;                       }                }    void T0_time() interrupt 1 {   TF0=0;  //清除中断标志   TR0=0; //关中断    if(uiVoiceCnt!=0)   {      uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫          beep_dr=0;  //蜂鸣器是PNP三极管控制,低电平就开始鸣叫。   }   else   {      ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。            beep_dr=1;  //蜂鸣器是PNP三极管控制,高电平就停止鸣叫。   }     TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f   TL0=0x2f;   TR0=1;  //开中断 }   void delay_long(unsigned int uiDelayLong) {    unsigned int i;    unsigned int j;    for(i=0;i<uiDelayLong;i++)    {       for(j=0;j<500;j++)  //内嵌循环的空指令数量           {              ; //一个分号相当于执行一条空语句           }    } }   void initial_myself()  //第一区 初始化单片机 { /* 注释三: * 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平, * 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。 * 朱兆祺51学习板的S1和S5两个按键就是本程序中用到的两个独立按键。 */   key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平     beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。     TMOD=0x01;  //设置定时器0为工作方式1     TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f   TL0=0x2f;  } void initial_peripheral() //第二区 初始化外围 {   EA=1;     //开总中断   ET0=1;    //允许定时中断   TR0=1;    //启动定时中断  }  总结陈词:
    本节程序已经展示了在主函数中,利用累计主循环次数来实现独立按键的检测。这种方法我经常在实战用应用,但是它也有一个小小的不足,随着在主函数循环中任务量的增加,为了保证去抖动延时的时间一致性,要适当调整一下去抖动的阀值const_key_time1。如何解决这个问题呢?欲知详情,请听下回分解-----在主函数中利用累计定时中断的次数来实现独立按键的检测。
  第七节:在主函数中利用累计定时中断的次数来实现独立按键的检测。
  开场白:
上一节讲了在主函数中利用累计主循环次数来实现独立按键的检测,但是它也有一个小小的不足,随着在主函数中任务量的增加,为了保证去抖动延时的时间一致性,要适当调整一下去抖动的时间阀值const_key_time1。如何解决这个问题呢?这一节教大家在主函数中利用累计定时中断的次数来实现独立按键的检测,可以有效地避免这个问题。要教会大家一个知识点:如何在上一节的基础上,略作修改,就可以在主函数中,利用累计定时中断的次数来实现去抖动的延时。
  具体内容,请看源代码讲解。
  (1)硬件平台:基于朱兆祺51单片机学习板。用矩阵键盘中的S1和S5号键作为独立按键,记得把输出线P0.4一直输出低电平,模拟独立按键的触发地GND。
  (2)实现功能:有两个独立按键,每按一个独立按键,蜂鸣器发出“滴”的一声后就停。
  (3)源代码讲解如下:
  
#include "REG52.H"  #define const_voice_short  40   //蜂鸣器短叫的持续时间   /* 注释一: * 调整抖动时间阀值的大小,可以更改按键的触发灵敏度。 * 去抖动的时间本质上等于累计定时中断次数的时间。 */ #define const_key_time1  30    //按键去抖动延时的时间 #define const_key_time2  30    //按键去抖动延时的时间  void initial_myself();    void initial_peripheral(); void delay_long(unsigned int uiDelaylong); void T0_time();  //定时中断函数 void key_service(); //按键服务的应用程序 void key_scan(); //按键扫描函数  sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键 sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键 sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平  sbit beep_dr=P2^7; //蜂鸣器的驱动IO口  unsigned char ucKeySec=0;   //被触发的按键编号  unsigned char ucKeyStartFlag1=0; //启动定时中断计数的开关 unsigned int  uiKeyTimeCnt1=0; //按键去抖动延时计数器 unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志  unsigned char ucKeyStartFlag2=0; //启动定时中断计数的开关 unsigned int  uiKeyTimeCnt2=0; //按键去抖动延时计数器 unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志  unsigned int  uiVoiceCnt=0;  //蜂鸣器鸣叫的持续时间计数器  void main()   {    initial_myself();      delay_long(100);       initial_peripheral();    while(1)      {        key_scan(); //按键扫描函数        key_service(); //按键服务的应用程序    }  }  void key_scan()//按键扫描函数 {   /* 注释二: * 独立按键扫描的详细过程: * 第一步:平时没有按键被触发时,按键的自锁标志,计时器开关和去抖动延时计数器一直被清零。 * 第二步:一旦有按键被按下,启动计时器,去抖动延时计数器开始在定时中断函数里累加,在还没累加到 *         阀值const_key_time1时,如果在这期间由于受外界干扰或者按键抖动,而使 *         IO口突然瞬间触发成高电平,这个时候马上停止计时,并且把延时计数器uiKeyTimeCnt1 *         清零了,这个过程非常巧妙,非常有效地去除瞬间的杂波干扰。这是我实战中摸索出来的。 *         以后凡是用到开关感应器的时候,都可以用类似这样的方法去干扰。 * 第三步:如果按键按下的时间超过了阀值const_key_time1,则触发按键,把编号ucKeySec赋值。 *         同时,马上把自锁标志ucKeyLock1置位,防止按住按键不松手后一直触发。 * 第四步:等按键松开后,自锁标志ucKeyLock1及时清零,为下一次自锁做准备。 * 第五步:以上整个过程,就是识别按键IO口下降沿触发的过程。 */   if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位   {      ucKeyLock1=0; //按键自锁标志清零          ucKeyStartFlag1=0; //停止计数器          uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。         }   else if(ucKeyLock1==0)//有按键按下,且是第一次被按下   {          ucKeyStartFlag1=1; //启动计数器      if(uiKeyTimeCnt1>const_key_time1)      {                  ucKeyStartFlag1=0; //停止计数器         uiKeyTimeCnt1=0;         ucKeyLock1=1;  //自锁按键置位,避免一直触发         ucKeySec=1;    //触发1号键      }   }    if(key_sr2==1)   {      ucKeyLock2=0;          ucKeyStartFlag2=0; //停止计数器          uiKeyTimeCnt2=0;   }   else if(ucKeyLock2==0)   {          ucKeyStartFlag2=1; //启动计数器      if(uiKeyTimeCnt2>const_key_time2)      {             ucKeyStartFlag2=0; //停止计数器         uiKeyTimeCnt2=0;         ucKeyLock2=1;         ucKeySec=2;     //触发2号键      }   }  }   void key_service() //第三区 按键服务的应用程序 {   switch(ucKeySec) //按键服务状态切换   {     case 1:// 1号键 对应朱兆祺学习板的S1键                uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。               ucKeySec=0;  //响应按键服务处理程序后,按键编号清零,避免一致触发           break;             case 2:// 2号键 对应朱兆祺学习板的S5键                uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。               ucKeySec=0;  //响应按键服务处理程序后,按键编号清零,避免一致触发           break;                       }                }    void T0_time() interrupt 1 {   TF0=0;  //清除中断标志   TR0=0; //关中断     if(ucKeyStartFlag1==1)//启动计数器   {      if(uiKeyTimeCnt1<0xffff)  //防止计数器超范围          {             uiKeyTimeCnt1++;          }   }     if(ucKeyStartFlag2==1)//启动计数器   {      if(uiKeyTimeCnt2<0xffff) //防止计数器超范围          {             uiKeyTimeCnt2++;          }   }    if(uiVoiceCnt!=0)   {      uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫          beep_dr=0;  //蜂鸣器是PNP三极管控制,低电平就开始鸣叫。   }   else   {      ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。            beep_dr=1;  //蜂鸣器是PNP三极管控制,高电平就停止鸣叫。   }     TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f   TL0=0x2f;   TR0=1;  //开中断 }   void delay_long(unsigned int uiDelayLong) {    unsigned int i;    unsigned int j;    for(i=0;i<uiDelayLong;i++)    {       for(j=0;j<500;j++)  //内嵌循环的空指令数量           {              ; //一个分号相当于执行一条空语句           }    } }   void initial_myself()  //第一区 初始化单片机 { /* 注释三: * 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平, * 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。 * 朱兆祺51学习板的S1和S5两个按键就是本程序中用到的两个独立按键。 */   key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平     beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。     TMOD=0x01;  //设置定时器0为工作方式1     TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f   TL0=0x2f;  } void initial_peripheral() //第二区 初始化外围 {   EA=1;     //开总中断   ET0=1;    //允许定时中断   TR0=1;    //启动定时中断  }  总结陈词:
本节程序已经展示了在主函数中,利用累计定时中断次数来实现独立按键的检测。这种方法我也经常在实战用应用,但是如果在某些项目中,需要在主函数里间歇性地执行一些一气呵成的耗时任务,这种方法就不是很实用,因为当主函数正在处理一气呵成的耗时任务时,这个时候如果有按键按下来,就有可能没有及时被响应到而遗漏了。那有什么方法可以解决这类项目中遇到的问题?欲知详情,请听下回分解-----在定时中断函数里执行独立按键的扫描程序。
  第八节:在定时中断函数里执行独立按键的扫描程序。
  开场白:
上一节讲了在主函数中利用累计定时中断的次数来实现独立按键的检测,但是如果在某些项目中,需要在主函数里间歇性地执行一些一气呵成的耗时任务,当主函数正在处理一气呵成的耗时任务时(前提是没有关闭定时器中断),这个时候如果有按键按下来,就有可能没有及时被响应到而遗漏了。在定时中断函数里处理独立按键的扫描程序,可以避免这个问题。要教会大家一个知识点:如何在上一节的基础上,略作修改,就可以在定时中断函数里处理独立按键的扫描程序。
  具体内容,请看源代码讲解。
  (1)硬件平台:基于朱兆祺51单片机学习板。用矩阵键盘中的S1和S5号键作为独立按键,记得把输出线P0.4一直输出低电平,模拟独立按键的触发地GND。
  (2)实现功能:有两个独立按键,每按一个独立按键,蜂鸣器发出“滴”的一声后就停。
  (3)源代码讲解如下:
  
#include "REG52.H"  #define const_voice_short  40   //蜂鸣器短叫的持续时间   /* 注释一: * 调整抖动时间阀值的大小,可以更改按键的触发灵敏度。 * 去抖动的时间本质上等于累计定时中断次数的时间。 */ #define const_key_time1  20    //按键去抖动延时的时间 #define const_key_time2  20    //按键去抖动延时的时间  void initial_myself();    void initial_peripheral(); void delay_long(unsigned int uiDelaylong); void T0_time();  //定时中断函数 void key_service(); //按键服务的应用程序 void key_scan(); //按键扫描函数 放在定时中断里  sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键 sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键 sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平  sbit beep_dr=P2^7; //蜂鸣器的驱动IO口  unsigned char ucKeySec=0;   //被触发的按键编号  unsigned int  uiKeyTimeCnt1=0; //按键去抖动延时计数器 unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志   unsigned int  uiKeyTimeCnt2=0; //按键去抖动延时计数器 unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志  unsigned int  uiVoiceCnt=0;  //蜂鸣器鸣叫的持续时间计数器  void main()   {    initial_myself();      delay_long(100);       initial_peripheral();    while(1)      {        key_service(); //按键服务的应用程序    }  }  void key_scan()//按键扫描函数 放在定时中断里 {   /* 注释二: * 独立按键扫描的详细过程: * 第一步:平时没有按键被触发时,按键的自锁标志,去抖动延时计数器一直被清零。 * 第二步:一旦有按键被按下,去抖动延时计数器开始在定时中断函数里累加,在还没累加到 *         阀值const_key_time1时,如果在这期间由于受外界干扰或者按键抖动,而使 *         IO口突然瞬间触发成高电平,这个时候马上把延时计数器uiKeyTimeCnt1 *         清零了,这个过程非常巧妙,非常有效地去除瞬间的杂波干扰。这是我实战中摸索出来的。 *         以后凡是用到开关感应器的时候,都可以用类似这样的方法去干扰。 * 第三步:如果按键按下的时间超过了阀值const_key_time1,则触发按键,把编号ucKeySec赋值。 *         同时,马上把自锁标志ucKeyLock1置位,防止按住按键不松手后一直触发。 * 第四步:等按键松开后,自锁标志ucKeyLock1及时清零,为下一次自锁做准备。 * 第五步:以上整个过程,就是识别按键IO口下降沿触发的过程。 */   if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位   {      ucKeyLock1=0; //按键自锁标志清零          uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。         }   else if(ucKeyLock1==0)//有按键按下,且是第一次被按下   {      uiKeyTimeCnt1++; //累加定时中断次数      if(uiKeyTimeCnt1>const_key_time1)      {         uiKeyTimeCnt1=0;         ucKeyLock1=1;  //自锁按键置位,避免一直触发         ucKeySec=1;    //触发1号键      }   }    if(key_sr2==1)   {      ucKeyLock2=0;          uiKeyTimeCnt2=0;   }   else if(ucKeyLock2==0)   {      uiKeyTimeCnt2++; //累加定时中断次数      if(uiKeyTimeCnt2>const_key_time2)      {         uiKeyTimeCnt2=0;         ucKeyLock2=1;         ucKeySec=2;     //触发2号键      }   }  }   void key_service() //第三区 按键服务的应用程序 {   switch(ucKeySec) //按键服务状态切换   {     case 1:// 1号键 对应朱兆祺学习板的S1键                uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。               ucKeySec=0;  //响应按键服务处理程序后,按键编号清零,避免一致触发           break;             case 2:// 2号键 对应朱兆祺学习板的S5键                uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。               ucKeySec=0;  //响应按键服务处理程序后,按键编号清零,避免一致触发           break;                       }                }    void T0_time() interrupt 1 {   TF0=0;  //清除中断标志   TR0=0; //关中断    key_scan(); //按键扫描函数    if(uiVoiceCnt!=0)   {      uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫          beep_dr=0;  //蜂鸣器是PNP三极管控制,低电平就开始鸣叫。   }   else   {      ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。            beep_dr=1;  //蜂鸣器是PNP三极管控制,高电平就停止鸣叫。   }     TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f   TL0=0x2f;   TR0=1;  //开中断 }   void delay_long(unsigned int uiDelayLong) {    unsigned int i;    unsigned int j;    for(i=0;i<uiDelayLong;i++)    {       for(j=0;j<500;j++)  //内嵌循环的空指令数量           {              ; //一个分号相当于执行一条空语句           }    } }   void initial_myself()  //第一区 初始化单片机 { /* 注释三: * 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平, * 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。 * 朱兆祺51学习板的S1和S5两个按键就是本程序中用到的两个独立按键。 */   key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平     beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。     TMOD=0x01;  //设置定时器0为工作方式1     TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f   TL0=0x2f;  } void initial_peripheral() //第二区 初始化外围 {   EA=1;     //开总中断   ET0=1;    //允许定时中断   TR0=1;    //启动定时中断  }  总结陈词:
本节程序已经展示了在定时中断函数里执行独立按键的扫描程序。这节和前面两节所讲的扫描方式,我都在项目上用过,具体跟项目的侧重点不同来选择不同的方式,我本人用得最多的就是当前这种方式。假如要独立按键实现类似鼠标的双击功能,我们改怎么写程序?欲知详情,请听下回分解-----独立按键的双击按键触发。
  第九节:独立按键的双击按键触发。
  开场白:
上一节讲了在定时中断函数里处理独立按键的扫描程序,这种结构的程序我用在了很多项目上。这一节教大家如何实现按键双击触发的功能,这种功能类似鼠标的双击。要教会大家一个知识点:如何在上一节的基础上,略作修改,就可以实现按键的双击功能。
  具体内容,请看源代码讲解。
  (1)硬件平台:基于朱兆祺51单片机学习板。用矩阵键盘中的S1和S5号键作为独立按键,记得把输出线P0.4一直输出低电平,模拟独立按键的触发地GND。
  (2)实现功能:有两个独立按键,每双击一个独立按键,蜂鸣器发出“滴”的一声后就停。
  (3)源代码讲解如下:
  
#include "REG52.H"  #define const_voice_short  40   //蜂鸣器短叫的持续时间   /* 注释一: * 调整抖动时间阀值的大小,可以更改按键的触发灵敏度。 * 去抖动的时间本质上等于累计定时中断次数的时间。 */ #define const_key_time1  20    //按键去抖动延时的时间 #define const_key_time2  20    //按键去抖动延时的时间  /* 注释二: * 有效时间差,是指连续两次按键触发的最大有效间隔时间。 * 如果双击的两个按键按下的时间间隔太长,则视为无效双击。 */ #define const_interval_time1  200     //连续两次按键之间的有效时间差 #define const_interval_time2  200     //连续两次按键之间的有效时间差  void initial_myself();    void initial_peripheral(); void delay_long(unsigned int uiDelaylong); void T0_time();  //定时中断函数 void key_service(); //按键服务的应用程序 void key_scan(); //按键扫描函数 放在定时中断里  sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键 sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键 sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平  sbit beep_dr=P2^7; //蜂鸣器的驱动IO口  unsigned char ucKeySec=0;   //被触发的按键编号  unsigned int  uiKeyTimeCnt1=0; //按键去抖动延时计数器 unsigned char ucKeyLock1=0; //按键触发后自锁的变量标志 unsigned char ucKeyTouchCnt1=0; //按键按下的次数记录 unsigned int  uiKeyIntervalCnt1=0; //按键间隔的时间计数器  unsigned int  uiKeyTimeCnt2=0; //按键去抖动延时计数器 unsigned char ucKeyLock2=0; //按键触发后自锁的变量标志 unsigned char ucKeyTouchCnt2=0; //按键按下的次数记录 unsigned int  uiKeyIntervalCnt2=0; //按键间隔的时间计数器  unsigned int  uiVoiceCnt=0;  //蜂鸣器鸣叫的持续时间计数器  void main()   {    initial_myself();      delay_long(100);       initial_peripheral();    while(1)      {        key_service(); //按键服务的应用程序    }  }  void key_scan()//按键扫描函数 放在定时中断里 {   /* 注释三: * 独立双击按键扫描的详细过程: * 第一步:平时没有按键被触发时,按键的自锁标志,去抖动延时计数器一直被清零。 *         如果之前已经有按键触发过一次,那么启动时间间隔计数器uiKeyIntervalCnt1, *         在这个允许的时间差范围内,如果一直没有第二次按键触发,则把累加按键触发的 *         次数ucKeyTouchCnt1也清零。 * 第二步:一旦有按键被按下,去抖动延时计数器开始在定时中断函数里累加,在还没累加到 *         阀值const_key_time1时,如果在这期间由于受外界干扰或者按键抖动,而使 *         IO口突然瞬间触发成高电平,这个时候马上把延时计数器uiKeyTimeCnt1 *         清零了,这个过程非常巧妙,非常有效地去除瞬间的杂波干扰。这是我实战中摸索出来的。 *         以后凡是用到开关感应器的时候,都可以用类似这样的方法去干扰。 * 第三步:如果按键按下的时间超过了阀值const_key_time1,马上把自锁标志ucKeyLock1置位, *         防止按住按键不松手后一直触发。与此同时,累加一次按键次数,如果按键次数累加有两次以上, *         则认为触发双击按键,并把编号ucKeySec赋值。 * 第四步:等按键松开后,自锁标志ucKeyLock1及时清零,为下一次自锁做准备。并且累加间隔时间, *         防止两次按键的间隔时间太长。 * 第五步:以上整个过程,就是识别按键IO口下降沿触发的过程。 */   if(key_sr1==1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位   {          ucKeyLock1=0; //按键自锁标志清零          uiKeyTimeCnt1=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。                        if(ucKeyTouchCnt1>0) //之前已经有按键触发过一次,再来一次就构成双击                  {                      uiKeyIntervalCnt1++; //按键间隔的时间计数器累加                          if(uiKeyIntervalCnt1>const_interval_time1) //超过最大允许的间隔时间                          {                             uiKeyIntervalCnt1=0; //时间计数器清零                             ucKeyTouchCnt1=0; //清零按键的按下的次数                          }                  }   }   else if(ucKeyLock1==0)//有按键按下,且是第一次被按下   {      uiKeyTimeCnt1++; //累加定时中断次数      if(uiKeyTimeCnt1>const_key_time1)      {         uiKeyTimeCnt1=0;         ucKeyLock1=1;  //自锁按键置位,避免一直触发                 uiKeyIntervalCnt1=0; //按键有效间隔的时间计数器清零                  ucKeyTouchCnt1++;                 if(ucKeyTouchCnt1>1)  //连续被按了两次以上                 {                     ucKeyTouchCnt1=0;  //统计按键次数清零                     ucKeySec=1;    //触发1号键                 }       }   }       if(key_sr2==1)   {          ucKeyLock2=0;          uiKeyTimeCnt2=0;                   if(ucKeyTouchCnt2>0)                  {                      uiKeyIntervalCnt2++; //按键间隔的时间计数器累加                          if(uiKeyIntervalCnt2>const_interval_time2) //超过最大允许的间隔时间                          {                             uiKeyIntervalCnt2=0; //时间计数器清零                             ucKeyTouchCnt2=0; //清零按键的按下的次数                          }                  }   }   else if(ucKeyLock2==0)   {      uiKeyTimeCnt2++; //累加定时中断次数      if(uiKeyTimeCnt2>const_key_time2)      {         uiKeyTimeCnt2=0;         ucKeyLock2=1;                 uiKeyIntervalCnt2=0; //按键有效间隔的时间计数器清零                  ucKeyTouchCnt2++;                 if(ucKeyTouchCnt2>1)  //连续被按了两次以上                 {                     ucKeyTouchCnt2=0;  //统计按键次数清零                     ucKeySec=2;    //触发2号键                 }      }   }  }   void key_service() //第三区 按键服务的应用程序 {   switch(ucKeySec) //按键服务状态切换   {     case 1:// 1号键 双击  对应朱兆祺学习板的S1键                uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。               ucKeySec=0;  //响应按键服务处理程序后,按键编号清零,避免一致触发           break;             case 2:// 2号键 双击  对应朱兆祺学习板的S5键                uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。               ucKeySec=0;  //响应按键服务处理程序后,按键编号清零,避免一致触发           break;                       }                }    void T0_time() interrupt 1 {   TF0=0;  //清除中断标志   TR0=0; //关中断    key_scan(); //按键扫描函数    if(uiVoiceCnt!=0)   {      uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫          beep_dr=0;  //蜂鸣器是PNP三极管控制,低电平就开始鸣叫。   }   else   {      ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。            beep_dr=1;  //蜂鸣器是PNP三极管控制,高电平就停止鸣叫。   }     TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f   TL0=0x2f;   TR0=1;  //开中断 }   void delay_long(unsigned int uiDelayLong) {    unsigned int i;    unsigned int j;    for(i=0;i<uiDelayLong;i++)    {       for(j=0;j<500;j++)  //内嵌循环的空指令数量           {              ; //一个分号相当于执行一条空语句           }    } }   void initial_myself()  //第一区 初始化单片机 { /* 注释四: * 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平, * 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。 * 朱兆祺51学习板的S1和S5两个按键就是本程序中用到的两个独立按键。 */   key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平     beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。     TMOD=0x01;  //设置定时器0为工作方式1     TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f   TL0=0x2f;  } void initial_peripheral() //第二区 初始化外围 {   EA=1;     //开总中断   ET0=1;    //允许定时中断   TR0=1;    //启动定时中断  }  总结陈词:
假如要两个独立按键实现组合按键的功能,我们该怎么写程序?欲知详情,请听下回分解-----独立按键的组合按键触发。
  第十节:两个独立按键的组合按键触发。
  开场白:
上一节讲了按键双击触发功能的程序,这一节讲类似电脑键盘组合按键触发的功能,要教会大家一个知识点:如何在上一节的基础上,略作修改,就可以实现两个独立按键的组合按键触发功能。
  具体内容,请看源代码讲解。
  (1)硬件平台:基于朱兆祺51单片机学习板。用矩阵键盘中的S1和S5号键作为独立按键,记得把输出线P0.4一直输出低电平,模拟独立按键的触发地GND。
  (2)实现功能:有两个独立按键,当把两个独立按键都按下后,蜂鸣器发出“滴”的一声后就停。直到松开任一个按键后,才能重新进行下一次的组合按键触发。
  (3)源代码讲解如下:
  
#include "REG52.H"  #define const_voice_short  40   //蜂鸣器短叫的持续时间   /* 注释一: * 调整抖动时间阀值的大小,可以更改按键的触发灵敏度。 * 去抖动的时间本质上等于累计定时中断次数的时间。 */ #define const_key_time12  20    //按键去抖动延时的时间   void initial_myself();    void initial_peripheral(); void delay_long(unsigned int uiDelaylong); void T0_time();  //定时中断函数 void key_service(); //按键服务的应用程序 void key_scan(); //按键扫描函数 放在定时中断里  sbit key_sr1=P0^0; //对应朱兆祺学习板的S1键 sbit key_sr2=P0^1; //对应朱兆祺学习板的S5键 sbit key_gnd_dr=P0^4; //模拟独立按键的地GND,因此必须一直输出低电平  sbit beep_dr=P2^7; //蜂鸣器的驱动IO口  unsigned char ucKeySec=0;   //被触发的按键编号  unsigned int  uiKeyTimeCnt12=0; //按键去抖动延时计数器 unsigned char ucKeyLock12=0; //按键触发后自锁的变量标志   unsigned int  uiVoiceCnt=0;  //蜂鸣器鸣叫的持续时间计数器  void main()   {    initial_myself();      delay_long(100);       initial_peripheral();    while(1)      {        key_service(); //按键服务的应用程序    }  }  void key_scan()//按键扫描函数 放在定时中断里 {   /* 注释二: * 独立组合按键扫描的详细过程: * 第一步:平时只要两个按键中有一个没有被按下时,按键的自锁标志,去抖动延时计数器一直被清零。 * 第二步:一旦两个按键都被按下,去抖动延时计数器开始在定时中断函数里累加,在还没累加到 *         阀值const_key_time12时,如果在这期间由于受外界干扰或者按键抖动,而使 *         IO口突然瞬间触发成高电平,这个时候马上把延时计数器uiKeyTimeCnt12 *         清零了,这个过程非常巧妙,非常有效地去除瞬间的杂波干扰。这是我实战中摸索出来的。 *         以后凡是用到开关感应器的时候,都可以用类似这样的方法去干扰。 * 第三步:如果按键按下的时间超过了阀值const_key_time12,马上把自锁标志ucKeyLock12置位, *         防止按住按键不松手后一直触发。并把编号ucKeySec赋值。 组合按键触发 * 第四步:等按键松开后,自锁标志ucKeyLock12及时清零,为下一次自锁做准备。 * 第五步:以上整个过程,就是识别按键IO口下降沿触发的过程。 */   if(key_sr1==1||key_sr2==1)//IO是高电平,说明两个按键没有全部被按下,这时要及时清零一些标志位   {          ucKeyLock12=0; //按键自锁标志清零          uiKeyTimeCnt12=0;//按键去抖动延时计数器清零,此行非常巧妙,是我实战中摸索出来的。         }   else if(ucKeyLock12==0)//有按键按下,且是第一次被按下   {      uiKeyTimeCnt12++; //累加定时中断次数      if(uiKeyTimeCnt12>const_key_time12)      {         uiKeyTimeCnt12=0;         ucKeyLock12=1;  //自锁按键置位,避免一直触发         ucKeySec=1;    //触发1号键                     }   }     }   void key_service() //第三区 按键服务的应用程序 {   switch(ucKeySec) //按键服务状态切换   {     case 1:// 1号键 组合按键  对应朱兆祺学习板的S1键和S5键                uiVoiceCnt=const_voice_short; //按键声音触发,滴一声就停。               ucKeySec=0;  //响应按键服务处理程序后,按键编号清零,避免一致触发           break;                          }                }    void T0_time() interrupt 1 {   TF0=0;  //清除中断标志   TR0=0; //关中断    key_scan(); //按键扫描函数    if(uiVoiceCnt!=0)   {      uiVoiceCnt--; //每次进入定时中断都自减1,直到等于零为止。才停止鸣叫          beep_dr=0;  //蜂鸣器是PNP三极管控制,低电平就开始鸣叫。   }   else   {      ; //此处多加一个空指令,想维持跟if括号语句的数量对称,都是两条指令。不加也可以。            beep_dr=1;  //蜂鸣器是PNP三极管控制,高电平就停止鸣叫。   }     TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f   TL0=0x2f;   TR0=1;  //开中断 }   void delay_long(unsigned int uiDelayLong) {    unsigned int i;    unsigned int j;    for(i=0;i<uiDelayLong;i++)    {       for(j=0;j<500;j++)  //内嵌循环的空指令数量           {              ; //一个分号相当于执行一条空语句           }    } }   void initial_myself()  //第一区 初始化单片机 { /* 注释三: * 矩阵键盘也可以做独立按键,前提是把某一根公共输出线输出低电平, * 模拟独立按键的触发地,本程序中,把key_gnd_dr输出低电平。 * 朱兆祺51学习板的S1和S5两个按键就是本程序中用到的两个独立按键。 */   key_gnd_dr=0; //模拟独立按键的地GND,因此必须一直输出低电平     beep_dr=1; //用PNP三极管控制蜂鸣器,输出高电平时不叫。     TMOD=0x01;  //设置定时器0为工作方式1     TH0=0xf8;   //重装初始值(65535-2000)=63535=0xf82f   TL0=0x2f;  } void initial_peripheral() //第二区 初始化外围 {   EA=1;     //开总中断   ET0=1;    //允许定时中断   TR0=1;    //启动定时中断  }  总结陈词:
以前寻呼机流行的时候,寻呼机往往只有一个设置按键,它要求用一个按键来设置不同的参数,这个时候就要用到同一个按键来实现短按和长按的区别触发功能。要现实这种功能,我们该怎么写程序?欲知详情,请听下回分解-----同一个按键短按与长按的区别触发。
  十节后内容见附件。
  手把手教你单片机程序框架(90讲)含源程序.pdf (4.0 MB)