jianhong_wu 发表于 2017-7-9 09:15:34

本帖最后由 jianhong_wu 于 2017-7-9 09:28 编辑

第七十七节: 指针唯一的“单向”输出通道return。

【77.1   指针的“单向”输出通道。】

      函数的接口有两个地方,一个是函数名“后面”的小括号所包含的接口参数,另一个是函数名“前面”通过函数内部return返回出来的“return返回类型”。比如:


return返回类型   函数名(接口参数,接口参数...)

unsigned char HanShu(unsigned char a,unsigned char b) //a和b是函数名“后面”的接口参数
{
   unsigned char c;
   c=a+b;
   return c;   //函数内部返回出来的“return返回类型”
}

       指针在“函数名后面小括号所包含的接口参数”的地方时,可以是一个“双向”口(输入和输出),如果在指针前面加上const关键字修饰,可以把“双向”改为只能输入的“单向”口,注意,这里所说的“单向”是指“输入的单向”,但是做不到“输出的单向”,指针如果想做到“输出的单向”,就必须通过return这个通道。return返回指针这个功能很常用,比如用32位单片机想做比较漂亮的显示界面时,大家往往喜欢用到emWIN这个界面显示系统,而emWIN提供了很多库函数,这些库函数用了很多return返回的“句柄”,“句柄”其实就是指针,比如类似以下行代码:

hItem = WM_GetDialogItem(hWin_FrameWin_GetClientWindow, ID_LISTVIEW_0); //获取某个控件的句柄

       其中hItem就是“句柄”,本质就是函数内部return返回出来的指针。

       所以本节内容主要是想告诉大家,return不仅可以返回普通的变量,也是可以返回指针的,而且还很常用。具体内容请看下面77.2例子中的讲解。

【77.2   例程练习和分析。】

       编写一个函数,要从一个二维表格的数组中提取其中某一行的数据,用return这个返回输出的通道来接收该行数据的地址(指针),然后再通过这个指针的间接调用,把该行数据全部显示出来。


/*---C语言学习区域的开始。-----------------------------------------------*/

unsigned char *GetRowData(unsigned char (*pu8Table),unsigned char u8RowSec);//函数声明

unsigned char table[]=//二维数组
{
{0x00,0x01,0x02},//二维数组的第0行数据
{0x10,0x11,0x12},//二维数组的第1行数据
{0x20,0x21,0x22},//二维数组的第2行数据
};

//函数名前面是unsigned char *,代表内部return返回的是unsigned char *的指针。
unsigned char *GetRowData(unsigned char (*pu8Table),unsigned char u8RowSec)
{
   unsigned char *pu8Row;
   pu8Row=(unsigned char *)&pu8Table;//提取某一行开始的地址(指针)
   return pu8Row;   //经过return通道对外输出指针,pu8Row是一个指针类型的变量。
}

unsigned char *pGu8Row; //接收return输出的指针
unsigned charGu8Buffer;    //一维数组,存放从二维数组里提取出来的某一行数据
unsigned chari; // for循环的变量

void main() //主函数
{
    pGu8Row=GetRowData(table,0);//这里的0是表示选择二维表格的第0行数据
for(i=0;i<3;i++)
{
    Gu8Buffer=pGu8Row;//通过指针pGu8Row来搬运数据到一维数组Gu8Buffer
}
    View(Gu8Buffer);//在电脑端观察存放二维数组某行数据的一维数组的内容
    View(Gu8Buffer);//在电脑端观察存放二维数组某行数据的一维数组的内容
    View(Gu8Buffer);//在电脑端观察存放二维数组某行数据的一维数组的内容
    while(1)
    {
    }
}
/*---C语言学习区域的结束。-----------------------------------------------*/


       在电脑串口助手软件上观察到的程序执行现象如下:

开始...

第1个数
十进制:0
十六进制:0
二进制:0

第2个数
十进制:1
十六进制:1
二进制:1

第3个数
十进制:2
十六进制:2
二进制:10

分析:
       Gu8Buffer是0,提取了二维数组的第0行第0个数据。
       Gu8Buffer是1,提取了二维数组的第0行第1个数据。
       Gu8Buffer是2,提取了二维数组的第0行第2个数据。

【77.3   如何在单片机上练习本章节C语言程序?】

       直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。


jianhong_wu 发表于 2017-7-16 12:34:52

本帖最后由 jianhong_wu 于 2017-7-16 13:09 编辑

第七十八节: typedef和#define和enum。

【78.1   typedef和#define和enum。】

       typedef称为“类型定义”,#define称为“宏定义”,enum称为“枚举”。三者都有“一键替换”的能力,但是应用的侧重点各有不同。请看下面的例子,要写一个函数,把学生的分数分为3个等级,第1等级是“优”(范围:“优”>=90分),第2等级是“中”(范围:70分<=“中”<90分),第3等级是“差”(范围:“差”<70分),实现此算法的函数需要一个输入口和一个输出口,用来输入分数和输出判断结果,判断的结果用三个数字常量0,1,2来表示,0代表“优”,1代表“中”,2代表“差”。代码如下:

unsigned char GetGrade(unsigned char u8Score)
{
   if(u8Score<70)
{
         return 2;//2代表“差”
}
else if(u8Score>=70&&u8Score<90)
{
         return 1;//1代表“中”
}
else
{
         return 0;//0代表“优”
}
}

       上述代码没有添加任何“typedef,#define,enum”,是“素颜照”级别的原始代码。现在对上述代码做一些美容,加入“typedef,#define,enum”的元素,代码如下:

#define BAD_MEDIUM   70//宏定义。用BAD_MEDIUM来表示“差”和“中”分数的分界线
#define MEDIUM_GOOD90//宏定义。用MEDIUM_GOOD来表示“中”和“优”分数的分界线

typedef unsignedchar u8;//用typedef为类型“unsigned char”增加一个名为“u8”的代言人

enum {GOOD = 0,MEDIUM,BAD}; //用enum把“0,1,2”三个常量转换为“GOOD,MEDIUM,BAD”

u8 GetGrade(u8 u8Score)
{
      if(u8Score<BAD_MEDIUM) //等级分数分界线的判断
{
         return BAD;    //BAD就是常量2,代表“差”。
}
else if(u8Score>=BAD_MEDIUM&&u8Score<MEDIUM_GOOD)//等级分数分界线的判断
{
         return MEDIUM;//MEDIUM就是常量1,代表“中”
}
else
{
         return GOOD;    //GOOD就是常量0,代表“优”
}
}

      代码赏析:

      赏析片段一:
#define BAD_MEDIUM   70//宏定义。用BAD_MEDIUM来表示“差”和“中”分数的分界线
#define MEDIUM_GOOD90//宏定义。用MEDIUM_GOOD来表示“良”和“优”分数的分界线
      这里,用宏定义#define来关联分界线判断的分数,给后续代码的升级维护带来了便捷,因为用户有可能会要求把“差”“中”“优”三者的分数线进行调整,这时直接更改70和90这个数值就可以实现分数线的调整。可见,宏定义#define经常用在涉及“分界线”判断的场合。

       赏析片段二:
typedef unsignedchar u8;//用typedef为类型“unsigned char”增加一个名为“u8”的代言人
       用类型定义typedef为类型“unsigned char”增加一个名为“u8”的代言人,u代表unsigned的u,8代表此类型占用8位,比如unsignedchar就是占用8位的unsigned类型,所以用u8。如果是16位的unsigned类型就用u16,32位则用u32,这都是单片机界的常用命名习惯。上述代码用了类型定义,今后代码中凡是想定义一个unsigned char变量,都可以直接用u8来替代。这样有两个好处:第一个好处,u8的字符个数明显比unsigned char少,省了敲代码的力气。第二个好处,方便代码在各种不同硬件平台上的移植,因为不同的单片机不同的编译器对unsigned char,unsigned int,unsigned long翻译所得的结果是不一样的,比如,51单片机的unsigned int是占用16位的,而很多32位单片机的unsigned int是占用32位的,它们的16位则用unsigned short int类型,而不是unsigned int。

       当我们用51单片机写代码的时候,可以如下类型定义:
typedef unsignedchar   u8;
typedef unsignedint      u16;
typedef unsignedlong   u32;

       当我们用32位的单片机写代码的时候,可以如下类型定义:
typedef unsigned char          u8;
typedef unsigned short int   u16;
typedef unsigned int         u32;

      这样,当我们想把51单片机的代码移到32位的单片机上时,只需要修改类型定义typedef这部分的代码,就可以快速做到代码在不同编译器平台上的类型兼容。

      赏析片段三:
enum {GOOD = 0,MEDIUM,BAD}; //用enum把“0,1,2”三个常量转换为“GOOD,MEDIUM,BAD”
      用枚举enum把“0,1,2”三个常量转换为“GOOD,MEDIUM,BAD”英文单词,最大的好处就是方便代码的阅读和修改。再多补充一点枚举的基础知识,上述代码中,第一个英文单词GOOD,经过“GOOD = 0”这条初始化的语句后,等效于常量0,后面的MEDIUM和BAD则C编译器自动对它们进行“累加1”排序,所以MEDIUM和BAD分别为常量1,2,这是C语言的语法规则。枚举enum的应用侧重在某些涉及到“状态”的数据类型,但是也不绝对。

【78.2   enum和typedef的相结合。】

       enum一旦搭载上typedef后,可以把各自的特性发挥得淋漓尽致,产生另外一种常见的用途,那就是“人造”数据类型的用途,这里的“人造”解读为“人为制造”之意。比如上述78.1的函数u8 GetGrade(u8 u8Score),输出接口接收的是u8类型,但是内部return返回的是枚举类型的“GOOD,MEDIUM,BAD”其中之一,而u8虽然也能接收和兼容常量“GOOD,MEDIUM,BAD”,但是总是感觉有点“类型不匹配”的“不适感”,如果想消除这点“不适感”,可以用enum和typedef相结合的办法,修改后代码如下:

#define BAD_MEDIUM   70//宏定义。用BAD_MEDIUM来表示“差”和“中”分数的分界线
#define MEDIUM_GOOD90//宏定义。用MEDIUM_GOOD来表示“良”和“优”分数的分界线

typedef unsignedchar u8;//用typedef为类型“unsigned char”增加一个名为“u8”的代言人

typedef enum {   
GOOD = 0,
MEDIUM,
BAD
} Grade;//通过typedef 和enum的相结合,“人造”出一个新的数据类型 Grade。

Grade GetGrade(u8 u8Score)//这里返回的类型是Grade,而“GOOD,MEDIUM,BAD”就是属于Grade
{
      if(u8Score<BAD_MEDIUM) //等级分数分界线的判断
{
         return BAD;    //BAD就是常量2,代表“差”。
}
else if(u8Score>=BAD_MEDIUM&&u8Score<MEDIUM_GOOD)//等级分数分界线的判断
{
         return MEDIUM;//MEDIUM就是常量1,代表“中”
}
else
{
         return GOOD;    //GOOD就是常量0,代表“优”
}
}

【78.3   例程练习和分析。】

       为了熟悉typedef,#define,enum的用法,现在要写一个函数,把学生的分数分为3个等级,第1等级是“优”(范围:“优”>=90分),第2等级是“中”(范围:70分<=“中”<90分),第3等级是“差”(范围:“差”<70分),实现此算法的函数需要一个输入口和一个输出口,用来输入分数和输出判断结果,判断的结果用三个数字常量0,1,2来表示,0代表“优”,1代表“中”,2代表“差”。

/*---C语言学习区域的开始。-----------------------------------------------*/


#define BAD_MEDIUM   70//宏定义。用BAD_MEDIUM来表示“差”和“中”分数的分界线
#define MEDIUM_GOOD90//宏定义。用MEDIUM_GOOD来表示“良”和“优”分数的分界线

typedef unsignedchar u8;//用typedef为类型“unsigned char”增加一个名为“u8”的代言人

typedef enum {   
GOOD = 0,
MEDIUM,
BAD
} Grade;//通过typedef 和enum的相结合,“人造”出一个新的数据类型 Grade。

Grade GetGrade(u8 u8Score);//函数声明

Grade a;//“人造”出Grade类型的变量a,用来接收函数的判断结果。
    Grade b;//“人造”出Grade类型的变量b,用来接收函数的判断结果。
    Grade c;//“人造”出Grade类型的变量c,用来接收函数的判断结果。

Grade GetGrade(u8 u8Score)//这里返回的类型是Grade,而“GOOD,MEDIUM,BAD”就是属于Grade
{
      if(u8Score<BAD_MEDIUM) //等级分数分界线的判断
{
         return BAD;    //BAD就是常量2,代表“差”。
}
else if(u8Score>=BAD_MEDIUM&&u8Score<MEDIUM_GOOD)//等级分数分界线的判断
{
         return MEDIUM;//MEDIUM就是常量1,代表“中”
}
else
{
         return GOOD;    //GOOD就是常量0,代表“优”
}
}

void main() //主函数
{
a=GetGrade(98);//输入98分,a来接收判断的结果
b=GetGrade(88);//输入88分,b来接收判断的结果
c=GetGrade(68);//输入68分,c来接收判断的结果

View(a);//在电脑端观察98分的判断结果a
View(b);//在电脑端观察88分的判断结果b
View(c);//在电脑端观察68分的判断结果c
while(1)
    {
    }
}
/*---C语言学习区域的结束。-----------------------------------------------*/


      在电脑串口助手软件上观察到的程序执行现象如下:

开始...

第1个数
十进制:0
十六进制:0
二进制:0

第2个数
十进制:1
十六进制:1
二进制:1

第3个数
十进制:2
十六进制:2
二进制:10

分析:
      98分的判断结果a为0,0代表“优”。
      88分的判断结果b为1,1代表“中”。
      68分的判断结果c为2,2代表“差”。

【78.4   如何在单片机上练习本章节C语言程序?】

       直接复制前面章节中第十一节的模板程序,练习代码时只需要更改“C语言学习区域”的代码就可以了,其它部分的代码不要动。编译后,把程序下载进带串口的51学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。


jianhong_wu 发表于 2017-7-23 10:23:46

本帖最后由 jianhong_wu 于 2017-7-23 10:44 编辑

第七十九节: 各种变量常量的命名规范。

【79.1   命名规范的必要。】

       一个大型的项目程序,涉及到的变量常量非常多,各种变量常量眼花缭乱,名字不规范就无法轻松掌控全局。若能一开始就遵守特定的命名规范,则普天之下,率土之滨,都被你牢牢地掌控在手里,天下再也没有难维护的代码。本节教给大家的是我多年实践所沿用的命名规范和习惯,它不是唯一绝对的,只是给大家参考,大家今后也可以在自己的实践中慢慢总结出一套适合自己的命名规范和习惯。

【79.2   普通变量常量的命名规范和习惯。】

       在C51编译器的平台下,unsigned char ,unsigned int ,unsigned long三类常用的变量代表了“无符号的8位,16位,32位”,这类型的变量前缀分别加“u8,u16,u32”来表示。但是这种类型的变量还分全局变量和局部变量,为了有所区分,就在全局变量前加“G”来表示,不带“G”的就默认是局部变量。比如:

unsigned char Gu8Number;    //Gu8就代表全局的8位变量
unsigned int Gu16Number;    //Gu16就代表全局的16位变量
unsigned long Gu32Number;   //Gu32就代表全局的32位变量

void HanShu(unsigned char u8Data) //u8就代表局部的8位变量
{
unsigned char u8Number;    //u8就代表局部的8位变量
unsigned int u16Number;    //u16就代表局部的16位变量
unsigned long u32Number;   //u32就代表局部的32位变量
}

      全局变量和局部变量继续往下细分,还分“静态”和“非静态”,为了有所区分,就在前面增加“ES”或“S”来表示,“ES”代表全局的静态变量,“S”代表局部的静态变量。比如:

static unsigned char ESu8Number;    //ESu8就代表全局的8位静态变量
static unsigned intESu16Number;    //ESu16就代表全局的16位静态变量
static unsigned long ESu32Number;   //ESu32就代表全局的32位静态变量

void HanShu(unsigned char u8Data) //u8就代表局部的8位变量
{
static unsigned char Su8Number;    //Su8就代表局部的8位静态变量
static unsigned int Su16Number;    //Su16就代表局部的16位静态变量
static unsigned long Su32Number;   //Su32就代表局部的32位静态变量
}

      刚才讲的只是针对“变量”,如果是“常量”,则前缀加“C”来表示,不管是全局的常量还是局部的常量,都统一用“C”来表示,不再刻意区分“全局常量”和“静态常量”,比如:

const unsigned char Cu8Number=1;    //Cu8就代表8位常量,不刻意区分“全局”和“局部”
const unsigned int Cu16Number=1;    //Cu16就代表16位常量,不刻意区分“全局”和“局部”
const unsigned long Cu32Number=1;   //Cu32就代表32位常量,不刻意区分“全局”和“局部”

void HanShu(unsigned char u8Data) //u8就代表局部的8位变量
{
const unsigned char Cu8Number=1;//Cu8就代表8位常量,不刻意区分“全局”和“局部”
const unsigned int Cu16Number=1;//Cu16就代表16位常量,不刻意区分“全局”和“局部”
const unsigned long Cu32Number=1; //Cu32就代表32位常量,不刻意区分“全局”和“局部”
}


【79.3   循环体变量的命名规范和习惯。】

      循环体变量是一个很特定场合用的变量,为了突出它的特殊,这类变量在命名上用单个字母,可以不遵守命名规范,这里的“不遵守命名规范”就是它的“命名规范”,颇有道家“无为就是有为”的韵味,它是命名界的另类。比如:

unsigned char i; //超越了规则约束的循环体变量,用单个字母来表示。
unsigned long k; //超越了规则约束的循环体变量,用单个字母来表示。
void HanShu(unsigned char u8Data) //u8就代表局部的8位变量
{
    unsigned int c; //超越了规则约束的循环体变量,用单个字母来表示。
    for(c=0;c<5;c++)//用在循环体的变量
    {
u8Data=u8Data+1;//u8就代表局部的8位变量
}

    for(i=0;i<5;i++)//用在循环体的变量
    {
u8Data=u8Data+1;//u8就代表局部的8位变量
}

    for(k=0;k<5;k++)//用在循环体的变量
    {
u8Data=u8Data+1;//u8就代表局部的8位变量
}
}


【79.4   数组的命名规范和习惯。】

       数组有四种应用场合,一种是普通数组,一种是字符串,一种是表格,一种是信息。在命名上分别加入后缀“Buffer,String,Table,Message”来区分,但是它们都是数组。比如:

unsigned intGu16NumberBuffer;//后缀是Buffer。16位的全局变量数组。用在普通数组。
unsigned char Gu8NumberString;   //后缀是String。8位的全局变量数组。用在字符串。

//根据原理图得出的共阴数码管字模表
code unsigned char Cu8DigTable[]=//后缀是Table。这里的code是代表C51的常量(类似const)。
{
0x3f,//0       序号0
0x06,//1       序号1
0x5b,//2       序号2
0x4f,//3       序号3
0x66,//4       序号4
0x6d,//5       序号5
0x7d,//6       序号6
0x07,//7       序号7
0x7f,//8       序号8
0x6f,//9       序号9
0x00,//不显示序号10
};

void HanShu(unsigned char u8Data) //u8就代表局部的8位变量
{
    unsigned char u8NumberMessage;//后缀是Message。8位的局部变量数组。用在信息。
}

【79.5   指针的命名规范和习惯。】

      指针的前缀加“p”来区分。再往下细分,指针有全局和局部,有“静态”和“非静态”,有“8位宽度”和“16位宽度”和“32位宽度”,有变量指针和常量指针。比如:

unsigned char *pGu8NumberString;//pGu8代表全局的8位变量指针
void HanShu(const unsigned char *pCu8Data) //pCu8代表局部的8位常量指针
{
unsigned char *pu8NumberBuffer;            //pu8代表局部的8位变量指针
static unsigned int *pSu16NumberBuffer;    //pSu16代表局部的16位静态变量指针
static unsigned long *pSu32NumberBuffer;   //pSu32代表局部的32位静态变量指针
}

【79.6   结构体的命名规范和习惯。】

      结构体的前缀加“t”来区分。再往下细分,指针有全局和局部,有“静态”和“非静态”,有结构体变量和结构体指针。比如:

struct StructSignData//带符号的数
{
   unsigned charu8Sign;//符号0为正数 1为负数
   unsigned long u32Data;//数值
};

struct StructSignData GtNumber; //Gt代表全局的结构体变量。
void HanShu(struct StructSignData *ptData) //pt代表局部的结构体指针
{
struct StructSignData tNumber; //t代表局部的结构体变量。
static struct StructSignData StNumber; //St代表局部的静态结构体变量。
}

【79.7   宏常量的命名规范和习惯。】

       所谓“宏常量”往往是指用#define语句定义的常量。宏常量的所有字符都用大写字母。比如:

#define DELAY_TIME   30//宏常量所有字符都用大写字母。DELAY_TIME代表延时的时间。
void HanShu(void)
{
    delay(DELAY_TIME); //相当于delay(30),这里的delay代表某个延时函数(这里没有具体写出来)
}

【79.8   首字符用大写字母以及下划线“_”的灵活运用。】

       两个以上的英文单词连在一起命名时,每个单词的首字符用大写,其余用小写,这样可以把每个单词“断句”开来,方便阅读。如果遇到两个英文单词连在一起不好“断句”的情况(比如某个英文单词全部是大写字母的专用名词),只要在两个英文单词之间插入下划线“_”就可以清晰的“断句”了。比如:

unsigned long Gu32GetFileLength; //GetFileLength寓意“获取某个文件的长度”。
unsigned char Gu8ESD_Flag; //ESD是专业用名词,代表“静电释放”的意思。用下划线“_”断句。

jianhong_wu 发表于 2017-7-30 11:27:34

本帖最后由 jianhong_wu 于 2017-7-30 11:44 编辑

第八十节: 单片机IO口驱动LED。

【80.1   不再依赖第11节模板程序。】

      前面大量的章节主要是讲C语言本身的基础知识,因此每次的练习例程都要依赖第11节的模板程序。从本节开始,正式进入到单片机主题,如果没有特殊说明,以后的练习程序就不再需要依赖第11节模板程序,可以脱离模板单飞了。

【80.2   寄存器。】

       寄存器是跨越在软件与硬件之间的桥梁,单片机的C语言想控制单片机引脚输出0V或者5V的物理电压,本质就是通过往寄存器里填数字,往哪个寄存器填数字,填什么样的数字,对应的引脚就输出什么样的电压。至于“为什么往寄存器填数字就会在引脚上输出对应的电压”这个问题,对于我们“应用级”工程师来说是一个黑匣子。我们写软件的最底层就是操作到“寄存器”这个层面,至于“寄存器与物理电压之间是如何关联如何实现”的这个问题,其实是“芯片级”半导体工程师所研究的事,因为单片机本身其实就是一个成品,我们从“芯片级”半导体工程师那里拿到这个成品,这个成品的说明书告诉了我们该成品的每个寄存器的作用,我们只能在这个基础上去做更上层的应用。该说明书其实就是大家通常所说的芯片的datasheet。
寄存器在单片机C语言层面,是一个全局变量,是一个具备特定名字的全局变量,是一个被系统征用的全局变量。寄存器的名字就像古代皇帝的名字,所有普通老百姓的变量名字都要“避尊者讳”,不能跟寄存器的名字重名,否则C编译器就编译不通过。


         
               图80.2.1单片机的32个IO口引脚

       本教程用的STC89C52单片机IO口寄存器有4个,分别是P0,P1,P2,P3这4个寄存器,每个寄存器都是一个8位的全局变量,每一位代表控制一个单片机的IO口引脚,因此,该单片机一共有32个(4乘以8)IO口引脚,每个引脚都是可以单独控制的(俗称位操作)。往该位填入0,对应的引脚就输出0V的物理电压。往该位填入1,对应的引脚就输出5V的物理电压。

【80.3   C语言操作IO口寄存器。】

       C语言操作单片机IO口寄存器,以便在对应的引脚上输出对应的物理电压,有两种方式。一种是并口的方式,另外一种是位操作的方式。并口方式,一次操作8个位(8个引脚),往往用在并口数据总线上。位操作方式,一次操作1个位(1个引脚),该方式因为单独控制到某个引脚,所以应用更加灵活广泛。
       并口方式。并口方式的时候,可以直接对P0,P1,P2,P3这4个寄存器赋值,就像对一个unsigned char的全局变量赋值一样。比如:

#include "REG52.H"
void main()
{
   P0=0xF0; //直接对P0赋值0xF0,意味着P0口的8个引脚,高4位全部输出5V,低4位全部输出0V。
   while(1)
   {
   }

}

       “P0=0xF0”这行代码,把十六进制的0xF0分解成二进制11110000来理解,P0.7,P0.6,P0.5,P0.4这4个引脚分别输出5V物理电压,而P0.3,P0.2,P0.1,P0.0这4个引脚分别输出0V物理电压。

       位操作方式。并口方式因为一次操作就绑定了8个引脚,是非常不方便的,因此,位操作就显得特别灵活实用,你可以直接操作P0,P1,P2,P3这4组引脚中(共32个)的某1个引脚,而不影响其它引脚的状态。比如,P1.4引脚是属于P1组的8个引脚中的某1个引脚,如果想直接位操作P1.4引脚,要用到特定的关键词sbit和符号“^”这个组合,sbit和符号“^”的组合类似宏定义,使用方式如下。

#include "REG52.H"
sbit P1_4=P1^4;//利用sbit和符号“^”的组合,把变量名字P1_4与P1.4引脚关联起来
void main()
{
   P1_4=0;//P1.4引脚输出0V物理电压,而不影响其它P1口引脚的状态。
   while(1)
   {
   }
}

【80.4   点亮LED。】

       LED灯要有电流通过,才会发光。要有电流通过,必须要有电压的“正压差”,“压差”可以用水压来比喻。
       比如在2楼的水,对于1楼来说,它就有“正压差”(2减去1等于“正1”),因此只要构成回路(有水管),2楼的水是可以往1楼流动的。
       比如在2楼的水,对于3楼来说,它虽然有压差,但是有的只是“负压差”(2减去3等于“负1”),因此哪怕构成回路(有水管),2楼的水也是不可以往3楼流动的。
       比如在2楼的水,对于同楼层的2楼来说,它的压差是0压差(2减去2等于“0压差”),因此哪怕构成回路(有水管),2楼的水也是不可以在2楼之间流动的。
      上面三个比喻很关键,精髓在于是否有“正压差”。要点亮一个LED灯,并不是说你单片机引脚直接输出一个5V的物理电压就能点亮的,还要看它构成的整个LED灯回路,也就是实际的电路图是什么样的。在本教程的原理图中,我们点亮LED灯是采样“灌入式”的电路,也就是单片机输出5V电压的时候LED灯是熄灭的,而输出0V物理电压时LED灯反而是被点亮的。如下两个图:

               
               图80.4.1灌入式驱动8个LED


               
               图80.4.2灌入式驱动4个LED

       现在根据这原理图,编写一个并口和位操作的练习例子,直接把程序烧录进开发板,就可以看到对应的LED灯的状态。

#include "REG52.H"
sbit P1_4=P1^4;//利用sbit和符号“^”的组合,把变量名字P1_4与P1.4引脚关联起来
void main()
{
   P0=0xF0; //直接对P0赋值0xF0,意味着P0口的8个引脚,高4位全部输出5V,低4位全部输出0V。
   P1_4=0;//P1.4引脚输出0V物理电压,而不影响其它P1口引脚的状态。
   while(1)
   {
   }
}

现象分析:   
       “P0=0xF0”直接对P0赋值0xF0,意味着P0口的8个引脚,高4位全部输出5V(LED灯反而灭),低4位全部输出0V(LED灯反而被点亮)。
       “P1_4=0”P1.4引脚输出0V物理电压(LED灯反而被点亮)。

jianhong_wu 发表于 2017-8-6 11:33:15

本帖最后由 jianhong_wu 于 2017-8-6 11:44 编辑

第八十一节: 时间和速度的起源(指令周期和晶振频率)。

【81.1   节拍。】

       单片机的C语言经过C编译器后,翻译成很多条机器指令,单片机逐条执行这些指令,每执行一条指令都是按照固定的节奏进行的,两条指令之间是存在几乎固定的时间间隔(实际上不是所有指令的间隔时间都绝对一致,这里方便理解暂时看作是一致),这就是节拍,每个节拍之间的时间间隔其实就是指令周期,因此,指令周期越短,节拍就越短,单片机的运算速度就越快。指令周期是由什么决定的呢?指令周期是由“心跳速度”和“心跳个数”决定的。指令周期都是由固定的N个“心跳个数”组成的,指令周期到底由多少个“心跳个数”组成?每种单片机每类指令各不一样。我们用的51系列单片机,最短的单周期指令是由12个“心跳个数”组成,依次类推,双周期指令由24个“心跳个数”组成,4周期指令由48个“心跳个数”组成。但是光有“心跳个数”还不够,还必须搭配知道“心跳速度”才能最终计算出指令周期。这里的“心跳速度”就是晶振的频率,“心跳个数”就是累计晶振的起振次数。比如,假设我们用的51单片机是12MHz(本教程实际用的是11.0592MHz),那么每个单周期的指令执行的时间是:12x(1/12000000)秒=1微秒。这个公式左边的“12”代表“12个晶振起振的次数”,这个公式右边的“(1/12000000)”代表晶振每起振1次所需要的单位时间。二者结合,刚好就是“心跳个数”乘以“单个心跳周期”等于指令周期,而指令周期就是节拍的时间。

      
                图81.1.1单片机的晶振

【81.2   累计节拍次数产生延时时间。】

       有了这个最原始的“节拍”概念,现在开始编写一个练习程序,让一个LED灯闪烁,闪烁的本质,就是让一个LED灯先亮一会(“一会”就是延时),然后紧接着让LED灯熄灭一会(“一会”就是延时),依次循环,在视觉上看到的连贯动作就是LED闪烁。这里的关键是如何产生这个“一会”的延时,本节教程所用的就是一个for循环来执行N条空指令,每执行一条空指令就需要消耗掉1个左右的指令周期的时间(大概1微秒左右),空指令执行的循环次数越多,产生的延时时间就越长。例子如下:

               
               图81.2.1灌入式驱动8个LED

#include "REG52.H"
sbit P0_0=P0^0;//利用sbit和符号“^”的组合,把变量名字P0_0与P0.0引脚关联起来
unsigned long i;//for循环用的累计变量
//unsigned int i; //如果把for循环的变量i改成unsigned int类型,闪烁的频率会加快。
void main()
{
   while(1)
   {
       //第(1)步
       P0_0=0;//LED灯亮。

       //第(2)步
       for(i=0;i<5000;i++) //累计的循环次数越大,这里的延时就越长,“亮”持续的时间就越长。
{   
            ;//分号代表一条空指令
}

       //第(3)步
       P0_0=1;//LED灯灭。

       //第(4)步
       for(i=0;i<5000;i++) //累计的循环次数越大,这里的延时就越长,“灭”持续的时间就越长。
{   
            ;//分号代表一条空指令
}

       //第(5)步:这里已经触碰到主循环while(1)的“底线”,所以接着跳转到第(1)步继续循环
   }
}

现象分析:
      理论上,每执行1条指令大概1微秒左右,但是实际上,我们看到的实验现象,发现累计循环才5000次,按理论计算,应该产生0.005秒左右的延时才合理,但是实际上居然能产生类似0.5秒的闪烁效果,中间相差100倍!为什么?C语言跟机器指令之间是存在翻译的“中间商”环节,一条C指令并不代表一条机器指令,往往一条C指令翻译后产生N条机器指令,比如上面的代码,用到for循环变量i,用的是unsigned long变量,意味4个字节,即使一条C语言赋值指令估计可能也要消耗4条单周期指令,在加上for循环的判断指令,和累加指令,以及跳转指令,所以我们看到的for(i=0;i<5000;i++)并不代表是真正仅仅执行了5000个指令周期,而是有可能执行了500000条指令周期!假如我们把上述代码中的i改成unsigned int变量(2字节),是会看到闪烁的速度明显加快的,其中原因就是C编译器与机器指令之间存在翻译后的“1对N”的关系。

jianhong_wu 发表于 2017-8-13 12:12:35

本帖最后由 jianhong_wu 于 2017-8-13 12:33 编辑

第八十二节: Delay阻塞延时控制LED闪烁。

【82.1   “阻塞”与“非阻塞”。】

       做项目写程序,大框架大思路上就是在“阻塞”与“非阻塞”这两种模式下不断切换。“阻塞”可以理解成“单任务处理”模式,“非阻塞”可以理解成“多任务并行处理”模式。“阻塞”的优点是它全神贯注不分心地专注于当下这一件事,它等待某个事件的响应速度是最快的,同时省去了“来回切换、反复扫描”的额外开销,而且在编程思路上不用太费脑力只需“记流水账式”的编程即可,但是它的缺点是当下只能干一件事,其它事情无法兼顾,做不到多任务并行处理。而“非阻塞”恰恰相反,它的有优点就是“阻塞”的缺点,它的缺点就是“阻塞”的优点,对于“非阻塞”本节暂时不多讲。在实际项目中,有时候“大 阻塞”中分支了N个“小 非阻塞”,也有时候“大 非阻塞”中分支了N个“小 阻塞”。能在“阻塞”与“非阻塞”之间运用自如者,谓之神。
       “阻塞等待”是指单片机在某个死循环里(比如“while(1)”这类)一直不断循环地在等待某个标志变量的状态,如果这个标志变量满足条件才会跳出这个死循环然后才能干其它的事情,否则一直在死循环里死等,给人一种全神贯注心无旁骛的感觉,
       “阻塞延时”是指单片机在产生“延时时间”的时候做不了别的事,延时多久它就要被“阻塞”多久,只有延时过后它才能解脱去干别的事。比如,在编程上,常用for循环产生N个空指令来达到产生“延时时间”的目的,这种编程方式就是最常见的“阻塞延时”。

【82.2   Delay阻塞延时的一个例子。】

      现在利用“Delay阻塞延时”编写一个练习程序,让一个LED灯闪烁。例子如下:

               
               图82.2.1灌入式驱动8个LED

#include "REG52.H"
void Delay(unsigned long u32DelayTime); //函数的声明
sbit P0_0=P0^0;//利用sbit和符号“^”的组合,把变量名字P0_0与P0.0引脚关联起来


void Delay(unsigned long u32DelayTime) //产生“阻塞延时”的延时函数
{
   static unsigned long i; //函数在频繁调用时,加static可以省去一条额外的初始化语句的开销。
   for(i=0;i<u32DelayTime;i++);
}
void main()
{
   while(1)
   {
       //第(1)步
       P0_0=0;//LED灯亮。

       //第(2)步
Delay(5000);//这里就是阻塞延时,时间就越长,“亮”持续的时间就越长。

       //第(3)步
       P0_0=1;//LED灯灭。

       //第(4)步
Delay(5000);//这里就是阻塞延时,时间就越长,“灭”持续的时间就越长。

       //第(5)步:这里已经触碰到主循环while(1)的“底线”,所以接着跳转到第(1)步继续循环
   }
}

【82.3   累加型和累减型的两种Delay函数,哪家强?】

       上述82.2例子中,用到一个Delay函数,该函数内部的for循环用的是“累加型”的,比如:

void Delay(unsigned long u32DelayTime)
{
         static unsigned long i;       //“累加型”函数内部多开销了一个变量i。
         for(i=0;i<u32DelayTime;i++);//因为这里的“i++”是加法运算,所以称为“累加型”。
}

       现在在跟大家分享一种“累减型”的Delay函数,例子如下:

void Delay(unsigned long u32DelayTime)
{
                                             //“累减型”函数内部节省了一个变量i。
         for(;u32DelayTime>0;u32DelayTime--);//“u32DelayTime--”意味着“累减型”。
}

      仔细对比“累加型”和“累减型”,会发现在实现同样“阻塞延时”的功能下,因为“累减型”巧妙的借用了函数入口的局部变量u32DelayTime来充当for循环的变量,而省去了一个i变量。因此,“累减型”比“累加型”强一点。

【82.4   Delay函数让初学者容易犯的错误。】

      初学者刚接触Delay函数,常常容易犯的错误就是忽略了for循环变量的类型,for循环变量的类型决定了你能输入的数值范围,比如上面例子中用到的是unsigned long变量,因此可以最大输入Delay(4294967295)。如果是unsigned int变量,最大可以输入Delay(65535)。如果是unsignedchar变量,最大可以输入Delay(255)。

【82.5   Delay内部的for循环嵌套可产生无穷长的时间。】

      刚才讲到,如果用最大的变量类型unsigned long ,最大的输入是Delay(4294967295),那么问题来,难道Delay函数的阻塞延时的时间有最大极限?其实不存在最大极限,理论上,你要多大的延时都可以,只需要在Delay函数内部用上for循环的嵌套,就可以产生“乘法级”的无穷长的时间,例子如下:
void Delay(unsigned long u32DelayTime)
{
      static unsigned long i;   
      static unsigned long k;   
      for(i=0;i<u32DelayTime;i++)
      {
         for(k=0;k<5000;k++); //内部嵌套的for循环,意味着乘法的关系u32DelayTime的5000倍!
}
}

【82.6   “阻塞延时”与“非阻塞延时”的各自应用范围。】

       “阻塞延时”一般应用在两个地方,一个是上电初始化进入主循环之前的延时,另一个是进入主循环之后,跟外部驱动芯片通信时候产生的时钟节拍小延时,而这个类延时一般是低于1ms的小延时。
       “非阻塞延时”在项目中是被大量应用的,进入主循环之后,只要大于或等于1ms的延时,大多数都采样“非阻塞延时”,因为进入“任务框架级”的层面,只有“非阻塞延时”才能保证项目可以继续“多任务并行处理”。“非阻塞延时”的方式后续章节会讲到。
       综上所述,1ms是“阻塞延时”与“非阻塞延时”的一个分解线,1ms这个时间不是绝对的,只是一个经验值。

jianhong_wu 发表于 2017-8-20 09:35:08

本帖最后由 jianhong_wu 于 2017-8-20 09:55 编辑

第八十三节: 累计主循环的“非阻塞”延时控制LED闪烁。

【83.1   累计主循环的“非阻塞”。】

      上一节提到,当Delay的“阻塞”时间超过1ms并且被频繁调用的时候,由于Delay做“独占式无用功”而消耗的延时太长,会影响其它任务的并行处理,整个系统给人的感觉非常卡顿不流畅。为了解决此问题,本节引入累计主循环的“非阻塞”,同时,希望通过此例子,让大家第一次感受到switch语句在多任务并行处理时候的优越性。switch的精髓在于“根据某个判断条件实现步骤之间的灵活跳转”,这个思路是以后做所有大项目的框架性思路。
      为什么“累计主循环”可以兼顾到其它任务的并行处理?因为单片机进入main函数以后,在一个主循环里要扫描N个任务,从头到尾,把N个任务扫描一遍,每扫描一遍算“一次主循环”,每一次“主循环”都是要消耗一点时间,累计的“主循环”次数越多,所要消耗的时间就越长,但是跟Delay唯一的差别是,Delay做延时的时候没有办法扫描其它任务,而“累计主循环”内部本身就是在不断扫描其它任务,产生时间越长扫描其它任务的次数就越多,两者是完全相互促进而没有矛盾的。具体内容,请看下面的例子。

【83.2    累计主循环“非阻塞”的一个例子。】

      现在利用“累计主循环非阻塞”编写一个练习程序,让一个LED灯闪烁。例子如下:

               
               图83.2.1灌入式驱动8个LED
#include "REG52.H"

#defineCYCLE_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思路,再外加一个“定时中断”,就可以轻松解决此问题,下一节就跟大家讲“定时中断”的内容。

jianhong_wu 发表于 2017-8-27 21:46:59

本帖最后由 jianhong_wu 于 2017-8-27 21:58 编辑

第八十四节: 中断与中断函数。

【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函数”与“中断函数”仿佛是隔离的毫无“物理连接”的,为什么单片机还能在“main函数”与“中断函数”两者中切换自如?没错,确实,“main函数”与“中断函数”在书写上是隔离的毫无关联的,但是它们之间之所以能相互切换,是因为背后有一只无形的手在自动操控这一切,这只手就是单片机硬件自身,这是一种特殊机制,也可以理解成一种特殊的游戏规则,我们只要遵守就好了,除了普通函数,其它凡是中断函数的,都不用跟main函数发生软件上的关联调用,它们之间的切换都是硬件自动完成的,这就是main函数与中断函数的特殊跳转机制(或者称为游戏规则也可以)。


【84.2   常用的中断函数有哪三类?】

      单片机的中断有很多,但常用在项目上的有三类:
      第一类是定时中断。配置中断后,使其每隔一段时间就产生一次中断,比如“1ms一次的定时中断”几乎是所有的系统里的标配,因为它对程序框架起到一个时间节拍的作用。
      第二类是通讯中断。比如串口每接收完一个字节就会产生一个中断通知我们去处理。
      第三类是电平变化的中断。下降沿或者上升沿的中断,常常用在采集高速的脉冲信号。

【84.3   我们如何操控中断?】

      刚才84.1提到“单片机硬件自动”这个概念,但是说它“硬件自动”并不意味着它不可控。单片机本身出厂的时候内部就携带了很多种类的中断,这些中断是否开启取决于你的“配置中断”代码,你要开启或者关闭某类中断,只需编写对应的“配置中断”代码就可以,而“配置中断”的代码本质就是填写某些寄存器数值。


jianhong_wu 发表于 2017-9-4 19:26:04

本帖最后由 jianhong_wu 于 2017-9-4 19:42 编辑

第八十五节: 定时中断的寄存器配置。

【85.1   寄存器配置的本质。】

      单片机内部可供我们选择的资源非常丰富,有定时器,有串口,有外部中断,等等。这些丰富的资源,就像你进入一家超市,你只需选择你所需要的东西就可以了,所以配置寄存器的关键在于选择,所谓选择就是往寄存器里面做填空题,单片机系统内部再根据你的“选择清单”,去启动对应的资源。那么我们怎么知道某个型号的单片机内部有哪些资源呢?看该型号“单片机的说明书”呀,“单片机的说明书”就是我们通常所说的“芯片的datasheet”,或者说是“芯片的数据手册”,这些资料单片机厂家会提供的。
      跟单片机打交道,其实跟人打交道没什么区别,你要让单片机按照你的“意愿”走,你首先要把你的“意愿”表达清楚,这个“意愿”就是信息,信息要具备准确性和唯一性,不能模凌两可。比如,现在要让单片机“每1ms产生一次中断”,你想想你可能需要给单片机提供哪些信息?
      (1)51单片机有2个定时器,一个是0号定时器,一个是1号定时器,我们要面临“二选一”的选择,本例子中用的是“0号定时器”。
      (2)0号定时器内部又有4种工作方式:方式0,方式1,方式2,方式3,本例子中用的是“方式1”。
      (3)定时器到底多长时间中断一次,这就涉及到填充与中断时间有关的寄存器的数值,该数值是跟时间成比例关系,本例子中配置的是1ms中断,就要填充对应的数值。
      (4)默认状态下,定时器是不会被开启的,如果要开启,这里就是涉及到定时器的“开关”,本例子要开启此开关。
      (5)定时器时间到了就要产生中断,中断也有“总开关”和“定时器的局部开关”,这两个开关都必须同时打开,中断才会有效。
      要配置定时器“每1ms产生一次中断”,大概就上述这些信息,根据这些信息提示,下面开始讲解一下寄存器的具体内容。

【85.2定时器/计数器的模式控制寄存器TMOD。】

      寄存器TMOD是一个8位的特殊变量,里面每一位都代表了不同的功能选择。根据芯片的说明书,TMOD的8位从左到右依次对应从D7到D0(左高位,右低位),定义如下:

      GATE    C/T    M1   M0    GATE    C/T    M1    M0

      仔细观察,发现左4位与右4位是对称的,分别都是“GATE,C/T , M1 , M0”,左4位控制的是“定时器1”,右4位控制的是“定时器0”,因为本例子用的是“定时器0”,因此“定时器1”的左4位都设置为0的默认数值,我们只需重点关注右4位的“定时器0”即可。
      GATE:定时器是否受“其它外部开关”的影响的标志位。定时器的开启或者停止,受到两个开关的影响,第一个开关是“自身原配开关”,第二个开关是“其它外部开关”。GATE取1代表定时器受“其它外部开关”的影响,取0代表定时器不受“其它外部开关”的影响。本例子中,定时器只受到“自身原配开关”的影响,而不受到“其它外部开关”的影响,因此,GATE取0。
      C/T:定时器有两种模式,该位取1代表“计数器模式”,取0代表“定时器模式”。本例子是“定时器模式”,因此,C/T取0。
      M1与M0:工作方式的选择。M1与M0这两位的01搭配,可以有4种组合(00,01,10,11),每一种组合就代表一种工作方式。本例子选用“方式1”,因此M1与M0取“01”的组合。
      综上所述,TMOD的配置代码是:TMOD=0x01;

【85.3决定时间长度的寄存器TH0与TL0。】

      TH0与TL0,T代表定时器英文单词TIME的T,H代表高位,L代表低位,0代表定时器0。
      TH0是一个8位宽度的寄存器,TL0也是一个8位宽度的寄存器,两者合并起来成为一个整体,实际上就是一个16位宽度的寄存器,TH0是高8位,TL0是低8位,它们合并后的数值范围是:0到65535。该16位寄存器取值越大,定时中断一次的时间反倒越小,为什么?TH0与TL0的初始值,就像一个水桶里装的水。如果这个桶是空桶(取值为0),“雨水”想把这个桶“滴满溢出”所需要的时间就很大。如果里面已经装了大半的水(取值为大于32767),“雨水”想把这个桶“滴满溢出”所需要的时间就很小。这里的关键词“滴满溢出”的“滴”与“满溢出”,“滴”的速度是由单片机晶振决定的,而“满溢出”一次就代表产生一次中断,执行完中断函数在即将返回主函数之前,我们重新装入特定容量的水(重装初值),为下一次的“滴满溢出”做准备,依次循环,从而连续不断地产生间歇的定时中断。
         配置中断时间的大小是需要经验的,因为,每次定时中断的时间太长,就意味着时间的可分度太粗,而如果每次定时中断的时间太短,则会产生很频繁的中断,势必会影响主函数main()的执行效率,而且累记中断次数的时间误差也会增大。因此,配置中断时间是需要经验的,根据经验,定时中断取1ms一次,是几乎所有单片机项目的最佳选择,按我的理解,“1ms定时中断一次”已经是单片机界公认的一种“标配”。
         要配置1ms定时中断,TH0与TL0如何取值?刚才提到一个形象的例子“桶,滴,满溢出”。TH0与TL0的最大取值范围是65535,可以理解成为最大65535“滴”,如果超过65535“滴”(比如加1“滴”后变成65536“滴”)就会“满溢出”,从而产生一次中断(65536是中断发生的临界值)。而“滴一次的时间”就刚好是单片机执行“一次单指令的时间”,“一次单指令的时间”等于12个晶振周期,比如12MHz的晶振,晶振周期是(1/12000000)秒,而“一次单指令的时间”就等于12乘以(1/12000000)秒,等于0.000001秒,也就是1us。1us“滴”一次,要产生1ms的时间就需要“滴”1000次。“满溢出”的前提条件是“桶里”一共需要装入65536滴才溢出,因此,在12MHz的晶振下要产生1ms的定时中断,TH0与TL0的初值应该是64536(65536减去1000等于64536),而64536变成十六进制0xfc17,再分解到高8位TH0为0xfc,低8位TL0为0x17。
         刚才的例子是假如晶振在12MHz的情况下所计算出来的结果,而本教程所用的晶振是11.0592MHz,根据11.0592MHz产生1ms的定时中断,TH0与TL0应该取值多少?根据刚才的计算方式:   

初值=[溢出值]-(/([晶振周期的12个]*(/[晶振频率])))
初值=65536-(0.001/(12*(1/11059200)))
初值=65536-922      (注:922是921.6的四舍五入)
初值=64614
初值=64614
初值=0xfc66
初值TH0=0xfc
初值TL0=0x66


【85.4中断的总开关EA与局部开关ET0。】

         EA:中断的总开关。宽度是1位的位变量。此开关如果取0,就会强行屏蔽所有的中断,因此,只要用到中断,此开关必须取1。
         ET0:专门针对定时器0中断的局部开关。宽度是1位的位变量。此开关如果取0,则会屏蔽定时器0的中断,如果取1则允许定时器0中断。如果要定时器0能产生中断,那么总开关EA与ET0必须同时都打开(都取1),两者缺一不可。

【85.5定时器0的“自身原配开关”TR0。】

         TR0:定时器的“自身原配开关”。宽度是1位的位变量。很多初学者会把EA,ET0,TR0三者搞不清。定时器可以工作在“查询标志位”和“中断”这两种状态,也就是说在没有中断的情况下定时器也可以单独使用的。TR0是定时器0自身的发动引擎,要不要把这个发动引擎所产生的能量传输到中断的渠道,则取决于中断开关EA和ET0。TR0是源头开关,EA是中断总渠道开关,ET0是中断分支渠道的定时器0开关。TR0取1表示启动定时器0,取0表示关闭定时器0。

【85.6定时器0的中断函数的书写格式。】

void 函数名() interrupt 1
{
       ...中断程序内容;
       ...此处省去若干代码
       ...中断程序内容;
       ...最后面的代码,要记得重装TH0与TL0的初值;
}
         函数名可以随便取,只要不是编译器已经征用的关键字。这里的1是定时器0的中断号。不同的中断号代表不同类型的中断,至于哪类中断对应哪个中断号,大家可以查找相关书籍和资料。本节用的定时器0处于工作方式1的情况下,在即将退出中断之前,需要重装TH0与TL0的初始值。

【85.7寄存器的名字来源。】

         前面讲的寄存器都有固定的名字,而且这些名字都是唯一的,拼写的时候少一个字母或者多一个字母,C编译器都会报错不让你通过,因此问题来了,初学者刚接触一款单片机的时候,如何知道某个寄存器它特定的唯一的名字?有两个来源。
         第一个来源,可以打开C编译器的某个头文件(.h格式)查看这些寄存器的名字。比如51单片机可以查看REG52.H这个头文件。如何打开REG52.H这个文件?在keil源代码编辑器界面下,选中上面REG52.H这几个字符,在右键弹出的菜单下点击Open ducument“REG52.H”即可。
         第二个来源是直接参考一些现成的范例程序,这些范例程序网上很多,有的是原厂提供的,有的是热心网友的分享,有的是技术书籍或者学习板开发板厂家提供的。

【85.8如何快速配置寄存器。】

         建议一边阅读芯片的数据手册,一边参考一些现成的范例程序,这些范例程序网上很多,有的是原厂提供的,有的是热心网友的分享,有的是技术书籍或者学习板开发板厂家提供的。

【85.9练习例程。】

         现在编写一个定时中断程序,让两个LED灯闪烁,一个是在主函数里用累计主循环次数的方式实现(P0.0控制),另一个是在定时中断函数里用累计定时中断次数的方式实现(P0.1控制)。这两个闪烁的LED灯,一个在main函数,一个是在中断函数,两路任务互不干涉独立运行,并行处理的“雏形”略显出来。

         
         图85.9.1灌入式驱动8个LED

#include "REG52.H"

#defineCYCLE_SUM   5000   //主循环的次数

#defineINTERRUPT_SUM   500   //中断的次数

sbit P0_0=P0^0;//在主循环里的LED灯
sbit P0_1=P0^1;//在定时中断里的LED灯

unsigned char Gu8CycleStep=0;
unsigned long Gu32CycleCnt=0;      //累计主循环的计数器

unsigned char Gu8InterruptStep=0;
unsigned long Gu32InterruptCnt=0;//累计定时中断次数的计数器

void main()
{
TMOD=0x01;//设置定时器0为工作方式1
TH0=0xfc;   //产生1ms中断的TH0初始值
TL0=0x66;   //产生1ms中断的TL0初始值
EA=1;       //开总中断
ET0=1;      //允许定时0的中断
TR0=1;      //启动定时0的中断

    while(1)//主循环
    {
       switch(Gu8CycleStep)
       {
         case 0:
               Gu32CycleCnt++;   
               if(Gu32CycleCnt>=CYCLE_SUM)
{
Gu32CycleCnt=0;   
                  P0_0=0;//主循环的LED灯亮。
Gu8CycleStep=1;
}
               break;

         case 1:
               Gu32CycleCnt++;   
               if(Gu32CycleCnt>=CYCLE_SUM)
{
Gu32CycleCnt=0;   
                  P0_0=1;   //主循环的LED灯灭。
Gu8CycleStep=0;
}
               break;
}
   }
}


void T0_time() interrupt 1    //定时器0的中断函数,每1ms单片机自动执行一次此函数
{
       switch(Gu8InterruptStep)
       {
         case 0:
               Gu32InterruptCnt++;   //累计中断次数的次数
               if(Gu32InterruptCnt>=INTERRUPT_SUM) //次数达到设定值就跳到下一步骤
{
Gu32InterruptCnt=0;   //及时清零计数器,为下一步骤的新一轮计数准备
                  P0_1=0;//定时中断的LED灯亮。
Gu8InterruptStep=1;//跳到下一步骤
}
               break;

         case 1:
               Gu32InterruptCnt++;   //累计中断次数的次数
               if(Gu32InterruptCnt>=INTERRUPT_SUM)//次数达到设定值就返回上一步骤
{
Gu32InterruptCnt=0;   //及时清零计数器,为返回上一步骤的新一轮计数准备
                  P0_1=1;//定时中断的LED灯灭。
Gu8InterruptStep=0;//返回到上一个步骤
}
               break;
}

TH0=0xfc;   //重装初值,不能忘。
TL0=0x66;   //重装初值,不能忘。
}



jianhong_wu 发表于 2017-9-10 10:48:08

本帖最后由 jianhong_wu 于 2017-9-10 11:08 编辑

第八十六节: 定时中断的“非阻塞”延时控制LED闪烁。

【86.1   定时中断应用的四大关键词。】

       本节主要内容有四大个关键词:1ms,互斥量,volatile,switch。
      (1)1ms。把定时中断设置为1ms中断一次,几乎是单片机界公认的“标配”。这个1 ms是系统时间的节拍来源,有了1ms“标配”意识,你的程序在不同单片机平台上移植的时候会得心应手运用自如。
      (2)互斥量。“主函数”与“定时中断函数”,本质上是两个独立进程在不断切换并行运行,两个进程之间不断切换,就会涉及到数据的安全保护,数据的安全保护主要是针对多字节的变量,比如int类型(2个字节),long类型(4个字节)。但是单字节的char变量不用额外保护,因为“字节”是变量中的最小单位(在不考虑“位”的情况下),这里的“最小单位不可分”就像“原子是最小单位不可分”一样,因此也有很多前辈把“互斥量”称为“原子锁”。为什么要用互斥量?因为,在多个线程同时访问同一个全局变量的时候,如果双方都是“读操作”,则不会出现问题,但是,如果双方都是“既有写操作也有读操作”的情况下,比如,我在主函数里正在修改(写操作)一个unsigned int类型的变量,unsigned int类型的变量占用2个字节,在更改数据的时候至少需要2条指令,当我刚执行完第1条指令还没来得及执行第2指令的时候,突然来了一个定时中断,并且在定时中断函数里也对这个变量进行了修改(写操作)并且还进行了读取判断操作,这个瞬间就可能给程序带来了隐患。话说回来,互斥量到底有没有必要,其实还是有点争议的,我曾经为这个问题纠结过很久,毕竟,如果不用互斥量,这么微观的隐患到底存不存在,目前很难做一个“让故障重现”的实验去证明,最后,我是本着“宁可信其有不可信其无”的态度,把互斥量应用在了我的工作中。
      (3)volatile。volatile是一个前缀的修饰关键词,也是用来保护主函数与中断函数共用的全局变量的,只不过,volatile是针对C编译器的,预防“C编译器在优化代码的时候误伤一些重要的共享数据”,就像预防杀毒软件用力过猛把一些合法软件当作病毒而误杀。加了volatile修饰的全局变量,就能提醒C编译器不要对这类特殊变量擅作主张去优化。
      (4)switch。switch是“非阻塞程序框架”的核心语句,在以switch为核心的框架下,进行不同步骤之间的程序跳转,是做大型裸机程序的常态。

【86.2主函数与定时中断函数的程序框架。】

      主函数与定时中断函数之间相互配合,主函数负责做什么,中断函数负责做什么,对于初学者来说可能是一头雾水,但是对于像我这种在单片机界深耕多年即将修炼成精的工程师来说,我心中是有很清晰的模板和套路的,这种模板和套路是经过多年沉淀下来的经验。比如,定时中断函数尽量放一些精简的计时器代码,一般不调用函数,但是“输入IO口的消抖动”(按键扫描)以及“蜂鸣器鸣叫”这两类特殊函数我是喜欢破例放在定时中断函数里调用的。定时中断如何产生时间,这个时间如何跟主函数关联起来,请看下面的框架代码:


volatile unsigned char vGu8TimeFlag=0;//互斥量变量标志
volatile unsigned int vGu16TimeCnt=0;//计时器变量

void main()
{
vGu8TimeFlag=0;//在“写操作”vGu16TimeCnt全局变量之前,互斥量vGu8TimeFlag的“加锁”
vGu16TimeCnt=1000;//全局变量的赋值,就是“写操作”
vGu8TimeFlag=1; //互斥量vGu8TimeFlag的“解锁”。同时也起到“启动计时器”的开关作用

    while(1)//主循环
{
   if(0==vGu16TimeCnt)//时间变量为0则表示时间到了
   {
          ...在这里执行具体的功能代码
}
   }
}


void T0_time() interrupt 1    //每1ms中断一次的定时中断函数
{
if(1==vGu8TimeFlag&&vGu16TimeCnt>0) //判断vGu8TimeFlag是否等于1,就是互斥量的判断。
{
vGu16TimeCnt--;//“自减一”的操作
}

}

      分析:上述代码中,vGu8TimeFlag是一箭双雕,既起到互斥量的作用,也起到了计数器vGu16TimeCnt开始计时的启动开关作用。

【86.3练习例程。】

      现在根据上述程序框架,编写一个LED灯闪烁的程序。

   
   图86.3.1灌入式驱动8个LED

#include "REG52.H"

#defineBLINK_TIME   500   //时间是500ms

sbit P0_0=P0^0;

volatile unsigned char vGu8TimeFlag=0;//互斥量变量标志
volatile unsigned int vGu16TimeCnt=0;//计时器变量

unsigned char Gu8Step=0;//switch的切换步骤
void main()
{
TMOD=0x01;//设置定时器0为工作方式1
TH0=0xfc;   //产生1ms中断的TH0初始值
TL0=0x66;   //产生1ms中断的TL0初始值
EA=1;       //开总中断
ET0=1;      //允许定时0的中断
TR0=1;      //启动定时0的中断


    while(1)//主循环
    {
       switch(Gu8Step)
       {
         case 0:
               if(0==vGu16TimeCnt)//时间到
               {
                  P0_0=0;   //LED灯亮
vGu8TimeFlag=0;//互斥量“加锁”
vGu16TimeCnt=BLINK_TIME;//计时器的写操作。设定计时的长度
vGu8TimeFlag=1;//互斥量“解锁”,同时蕴含了计时器“启动”的动作

Gu8Step=1;//切换到case 1这个步骤
}
               break;

         case 1:

            if(0==vGu16TimeCnt)//时间到
               {
                  P0_0=1;   //LED灯灭。
vGu8TimeFlag=0;//互斥量“加锁”
vGu16TimeCnt=BLINK_TIME;//计时器的写操作。设定计时的长度
vGu8TimeFlag=1;//互斥量“解锁”,同时蕴含了计时器“启动”的动作

Gu8Step=0;//切换到case 0这个步骤,依次循环
}
               break;
}
   }
}


void T0_time() interrupt 1    //定时器0的中断函数,每1ms单片机自动执行一次此函数
{
if(1==vGu8TimeFlag&&vGu16TimeCnt>0) //判断vGu8TimeFlag是否等于1,就是互斥量的判断
{
vGu16TimeCnt--;//“自减一”的操作
}

TH0=0xfc;   //重装初值,不能忘
TL0=0x66;   //重装初值,不能忘
}


【86.4解决闪烁出现不规则“非对称感”现象的方法。】

       上述例子,实验现象应该是LED闪烁很有规则的每1s闪烁一次,但是也有一部分初学者可能会遇到闪烁出现不规则“非对称感”的现象,这个问题的解决办法如下:在keil2的project下拉菜单下,选择Options for Target选项,弹出的窗口中,切换到Target选项,在Memory Model选项中选择small:variables in Data。




                   图86.4.1设置窗口


jianhong_wu 发表于 2017-9-19 09:54:31

本帖最后由 jianhong_wu 于 2017-9-19 10:18 编辑

第八十七节: 一个定时中断产生N个软件定时器。

【87.1   信手拈来的软件定时器。】

      初学者会疑惑,51单片机只有2个定时器T0和T1,是不是太少了一点?2个定时器怎能满足实际项目的需要,很多项目涉及到的定时器往往十几个,怎么办?这个问题的奥秘就在本节的内容。
      51单片机内置的2个定时器T0和T1,是属于硬件定时器,硬件定时器是一个母体,它可以孕育出N个软件定时器,实际项目中,我们需要多少个定时器只需要从同一个硬件定时器中断里构造出对应数量的软件定时器即可。构造N个软件定时器的框架如下:

//“软件定时器1”的相关变量
volatile unsigned char vGu8TimeFlag_1=0;
volatile unsigned int vGu16TimeCnt_1=0;   

//“软件定时器2”的相关变量
volatile unsigned char vGu8TimeFlag_2=0;
volatile unsigned int vGu16TimeCnt_2=0;

//“软件定时器3”的相关变量
volatile unsigned char vGu8TimeFlag_3=0;
volatile unsigned int vGu16TimeCnt_3=0;

void main()
{
vGu8TimeFlag_1=0;
vGu16TimeCnt_1=1000;//“软件定时器1”的定时时间是1000ms
vGu8TimeFlag_1=1;

vGu8TimeFlag_2=0;
vGu16TimeCnt_2=500;//“软件定时器2”的定时时间是500ms
vGu8TimeFlag_2=1;

vGu8TimeFlag_3=0;
vGu16TimeCnt_3=250;//“软件定时器3”的定时时间是250ms
vGu8TimeFlag_3=1;

    while(1)//主循环
{
   if(0==vGu16TimeCnt_1)//“软件定时器1”的时间到了
   {
          ...在这里执行具体的功能代码
}

   if(0==vGu16TimeCnt_2)//“软件定时器2”的时间到了
   {
          ...在这里执行具体的功能代码
}

   if(0==vGu16TimeCnt_3//“软件定时器3”的时间到了
   {
          ...在这里执行具体的功能代码
}

   }
}

void T0_time() interrupt 1    //每1ms中断一次的定时中断函数
{
if(1==vGu8TimeFlag_1&&vGu16TimeCnt_1>0) //在定时中断里衍生出“软件定时器1”
{
vGu16TimeCnt_1--;
}

if(1==vGu8TimeFlag_2&&vGu16TimeCnt_2>0) //在定时中断里衍生出“软件定时器2”
{
vGu16TimeCnt_2--;
}

if(1==vGu8TimeFlag_3&&vGu16TimeCnt_3>0) //在定时中断里衍生出“软件定时器3”
{
vGu16TimeCnt_3--;
}

//按上面的套路继续写,可以衍生出N个“软件定时器”,只要不超过单片机的RAM和ROM。
}


【87.2练习例程。】

       现在根据上述程序框架,编写3个LED灯闪烁的程序。第1个LED灯的一闪一灭的周期是2秒,第2个LED灯的一闪一灭的周期是1秒,第3个LED灯一闪一灭的周期是0.5秒。这3个灯的闪烁频率是不一样的,因此需要3个软件定时器。该例子其实也是一个多任务并行处理的典型例子,这3个LED灯就代表3个不同的任务,它们之间是通过switch这个关键语句进行多任务并行处理的。switch的精髓在于根据某个特定条件切换到对应的步骤(或称“跳转到对应的步骤”)。


       图87.2.1灌入式驱动8个LED

#include "REG52.H"

#defineBLINK_TIME_1   1000   //时间是1000ms
#defineBLINK_TIME_2   500    //时间是500ms
#defineBLINK_TIME_3   250    //时间是250ms

sbit P0_0=P0^0;
sbit P0_1=P0^1;
sbit P0_2=P0^2;

//“软件定时器1”的相关变量
volatile unsigned char vGu8TimeFlag_1=0;
volatile unsigned int vGu16TimeCnt_1=0;   

//“软件定时器2”的相关变量
volatile unsigned char vGu8TimeFlag_2=0;
volatile unsigned int vGu16TimeCnt_2=0;

//“软件定时器3”的相关变量
volatile unsigned char vGu8TimeFlag_3=0;
volatile unsigned int vGu16TimeCnt_3=0;

unsigned char Gu8Step_1=0;//软件定时器1的switch切换步骤
unsigned char Gu8Step_2=0;//软件定时器2的switch切换步骤
unsigned char Gu8Step_3=0;//软件定时器3的switch切换步骤

void main()
{
TMOD=0x01;//设置定时器0为工作方式1
TH0=0xfc;   //产生1ms中断的TH0初始值
TL0=0x66;   //产生1ms中断的TL0初始值
EA=1;       //开总中断
ET0=1;      //允许定时0的中断
TR0=1;      //启动定时0的中断


    while(1)//主循环
{

//软件定时器1控制的LED灯闪烁
       switch(Gu8Step_1)
       {
         case 0:
               if(0==vGu16TimeCnt_1)
               {
                  P0_0=0;   
vGu8TimeFlag_1=0;
vGu16TimeCnt_1=BLINK_TIME_1;
vGu8TimeFlag_1=1;

Gu8Step_1=1;
}
               break;

         case 1:

            if(0==vGu16TimeCnt_1)
               {
                  P0_0=1;   
vGu8TimeFlag_1=0;
vGu16TimeCnt_1=BLINK_TIME_1;
vGu8TimeFlag_1=1;

Gu8Step_1=0;
}
               break;
}

//软件定时器2控制的LED灯闪烁
       switch(Gu8Step_2)
       {
         case 0:
               if(0==vGu16TimeCnt_2)
               {
                  P0_1=0;   
vGu8TimeFlag_2=0;
vGu16TimeCnt_2=BLINK_TIME_2;
vGu8TimeFlag_2=1;

Gu8Step_2=1;
}
               break;

         case 1:

            if(0==vGu16TimeCnt_2)
               {
                  P0_1=1;   
vGu8TimeFlag_2=0;
vGu16TimeCnt_2=BLINK_TIME_2;
vGu8TimeFlag_2=1;

Gu8Step_2=0;
}
               break;
}

//软件定时器3控制的LED灯闪烁
       switch(Gu8Step_3)
       {
         case 0:
               if(0==vGu16TimeCnt_3)
               {
                  P0_2=0;   
vGu8TimeFlag_3=0;
vGu16TimeCnt_3=BLINK_TIME_3;
vGu8TimeFlag_3=1;

Gu8Step_3=1;
}
               break;

         case 1:

            if(0==vGu16TimeCnt_3)
               {
                  P0_2=1;   
vGu8TimeFlag_3=0;
vGu16TimeCnt_3=BLINK_TIME_3;
vGu8TimeFlag_3=1;

Gu8Step_3=0;
}
               break;
}

   }
}


void T0_time() interrupt 1    //定时器0的中断函数,每1ms单片机自动执行一次此函数
{
if(1==vGu8TimeFlag_1&&vGu16TimeCnt_1>0) //在定时中断里衍生出“软件定时器1”
{
vGu16TimeCnt_1--;
}

if(1==vGu8TimeFlag_2&&vGu16TimeCnt_2>0) //在定时中断里衍生出“软件定时器2”
{
vGu16TimeCnt_2--;
}

if(1==vGu8TimeFlag_3&&vGu16TimeCnt_3>0) //在定时中断里衍生出“软件定时器3”
{
vGu16TimeCnt_3--;
}


TH0=0xfc;   //重装初值,不能忘
TL0=0x66;   //重装初值,不能忘
}


jianhong_wu 发表于 2017-9-24 10:18:17

本帖最后由 jianhong_wu 于 2017-9-24 10:32 编辑

第八十八节: 两大核心框架理论(四区一线,switch外加定时中断)。

【88.1   四区一线。】

       提出“四区一线”理论,主要方便初学者理解单片机程序大概的“空间分区”。
       “四区”代表四大主流函数,分别是:系统初始化函数,外设初始化函数,主程序的任务函数,定时中断函数。
       “一线”是指“系统初始化函数”与“外设初始化函数”的“分割线”,这个“分割线”是一个delay的延时函数。
       “四区一线”的布局如下:

void main()
{
SystemInitial();            //“四区一线”的“第一区”
Delay(10000);               //“四区一线”的“一线”
PeripheralInitial();      //“四区一线”的“第二区”
    while(1)//主循环
{
LedService();         //“四区一线”的“第三区”
KeyService();         //“四区一线”的“第三区”
UsartService();         //“四区一线”的“第三区”
...                     //凡是在主循环里的函数都是属于“第三区”
   }
}

void T0_time() interrupt 1      //“四区一线”的“第四区”
{

}

       “第一区”的函数SystemInitial(),是一个系统的初始化函数,专门用来初始化单片机自己的寄存器以及个别外围要求响应速度快的输出设备,防止刚上电之后,由于输出IO口电平状态不确定而导致外围设备误动作,比如驱动继电器的误动作等等。
       “一线”的函数Delay(10000),是一个延时函数,为什么这里要插入一个延时函数?主要目的是为接下来的PeripheralInitial()做准备的。上电后先延时一段时间,再执行PeripheralInitial()函数,因为PeripheralInitial()函数专门用来初始化不要求上电立即处理的外设芯片和模块。比如液晶模块,AT24C02存储芯片,DS1302时钟芯片,等等。这些芯片在上电的瞬间,内部自身的复位需要一点时间,以及外部电压稳定也需要一点时间,只有过了这一点时间,这些芯片才处于正常的工作状态,这个时候单片机才能跟它正常通信,所以“一线”函数Delay(10000)的意义就在这里。
       “第二区”的函数PeripheralInitial(),是一个外设的初始化函数。专门用来初始化不要求上电立即处理的外设芯片和模块。
       “第三区”的函数LedService(),KeyService(),UsartService(),等等,是一些在主循环里不断扫描的任务函数。
       “第四区”的函数void T0_time() interrupt 1,是一个定时中断函数,一个系统必须标配一个定时中断函数才算完美齐全,这个中断函数提供系统的节拍时间,以及处理扫描一些跟IO口消抖动相关的函数,以及跟蜂鸣器驱动相关的函数。

【88.2   switch外加定时中断。】

      提出“switch外加定时中断”理论,主要方便初学者理解单片机程序大概的“逻辑框架”。
      switch是一个万能语句,它外加while与for循环就可以做任何复杂的算法,比如,搜索算法,运动算法,提取关键词算法,等等。它外加定时中断,就可以搭建一个系统的基本框架。比如,做通信的程序框架,人机界面的程序框架,按键服务的程序框架,等等。switch的精髓在于“根据条件进行步骤的灵活切换”。具体内容请看本节的练习程序。

【88.3练习例程。】

      根据上述的两大核心框架理论,编写1个LED灯闪烁的程序。


   图88.3.1灌入式驱动8个LED

#include "REG52.H"

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;
void LedService(void);

#defineBLINK_TIME_1   1000

sbit P0_0=P0^0;

volatile unsigned char vGu8TimeFlag_1=0;
volatile unsigned int vGu16TimeCnt_1=0;   

void main()
{
SystemInitial();            //“四区一线”的“第一区”
Delay(10000);               //“四区一线”的“一线”
PeripheralInitial();      //“四区一线”的“第二区”
    while(1)//主循环
{
LedService();         //“四区一线”的“第三区”
   }
}

void T0_time() interrupt 1   //“四区一线”的“第四区”
{
if(1==vGu8TimeFlag_1&&vGu16TimeCnt_1>0)
{
vGu16TimeCnt_1--;
}

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

void LedService(void)
{
static unsigned char Su8Step=0; //加static修饰的局部变量,每次进来都会保留上一次值。

       switch(Su8Step)
       {
         case 0:
               if(0==vGu16TimeCnt_1)//时间到
               {
                  P0_0=0;   
vGu8TimeFlag_1=0;
vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
vGu8TimeFlag_1=1;

Su8Step=1;//切换到下一个步骤,精髓语句!
}
               break;

         case 1:

            if(0==vGu16TimeCnt_1)   //时间到
               {
                  P0_0=1;   
vGu8TimeFlag_1=0;
vGu16TimeCnt_1=BLINK_TIME_1;//重装定时的时间
vGu8TimeFlag_1=1;

Su8Step=0;   //返回到上一个步骤,精髓语句!
}
               break;
}

}



jianhong_wu 发表于 2017-10-2 11:45:57

本帖最后由 jianhong_wu 于 2017-10-2 12:01 编辑

第八十九节: 跑马灯的三种境界。

【89.1   跑马灯的三种境界。】

      跑马灯也称为流水灯,排列的几个LED依次循环的点亮和熄灭,给人“跑动起来”的感觉,故称为“跑马灯”。实现跑马灯的效果,编程上有三种思路,分别代表了跑马灯的三种境界,分别是:移位阻塞,移位非阻塞,状态切换非阻塞。


       图89.1.1灌入式驱动8个LED

       本节用的是8个LED灯依次挨个熄灭点亮,如上图所示。

【89.2   移位阻塞。】

       移位阻塞,“移位”用的是C语言的左移或者右移语句,“阻塞”用的是delay延时。代码如下:

#include "REG52.H"

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;
void LedTask(void);

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
LedTask();   
    }
}

void T0_time() interrupt 1   
{

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

//跑马灯的任务程序   
void LedTask(void)
{
static unsigned char Su8Data=0x01; //加static修饰的局部变量,每次进来都会保留上一次值。
static unsigned char Su8Cnt=0;   //加static修饰的局部变量,每次进来都会保留上一次值。

P0=Su8Data;   //Su8Data的8个位代表8个LED的状态,0为点亮,1为熄灭。
Delay(10000) ; //阻塞延时
Su8Data=Su8Data<<1;//左移一位
Su8Cnt++; //计数器累加1
if(Su8Cnt>=8) //移位大于等于8次后,重新赋初值
{
Su8Cnt=0;
Su8Data=0x01;//重新赋初值,继续下一次循环移动
}   
}


      分析总结:这是第1种境界的跑马灯,这种思路虽然实现了跑马灯的效果,但是因为“阻塞延时”,整个程序显得僵硬机械,缺乏多任务并行的框架。

【89.3   移位非阻塞。】

      移位非阻塞,“移位”用的是C语言的左移或者右移语句,“非阻塞”用的是定时中断衍生出来的软件定时器。代码如下:

#include "REG52.H"

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;
void LedTask(void);

#defineBLINK_TIME_1   1000

volatile unsigned char vGu8TimeFlag_1=0;
volatile unsigned int vGu16TimeCnt_1=0;   

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
LedTask();   
    }
}

void T0_time() interrupt 1   
{
if(1==vGu8TimeFlag_1&&vGu16TimeCnt_1>0) //软件定时器
{
vGu16TimeCnt_1--;
}

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

//跑马灯的任务程序   
void LedTask(void)
{
static unsigned char Su8Data=0x01; //加static修饰的局部变量,每次进来都会保留上一次值。
static unsigned char Su8Cnt=0;   //加static修饰的局部变量,每次进来都会保留上一次值。

      if(0==vGu16TimeCnt_1)   //时间到
      {
vGu8TimeFlag_1=0;
vGu16TimeCnt_1=BLINK_TIME_1;//重装定时的时间
vGu8TimeFlag_1=1;

P0=Su8Data;   //Su8Data的8个位代表8个LED的状态,0为点亮,1为熄灭。
Su8Data=Su8Data<<1;//左移一位
Su8Cnt++; //计数器累加1
if(Su8Cnt>=8) //移位大于等于8次后,重新赋初值
{
Su8Cnt=0;
Su8Data=0x01;//重新赋初值,继续下一次循环移动
}   
}
}


       分析总结:这是第2种境界的跑马灯,这种思路虽然实现了跑马灯的效果,也用到了多任务并行处理的基本元素“软件定时器”,但是因为还停留在“移位”语句的阶段,此时的程序并没有超越跑马灯本身,跑马灯还是跑马灯,处于“看山还是山”的境界。

【89.4   状态切换非阻塞。】

       状态切换非阻塞,“状态切换”用的是switch语句中根据特定条件进行步骤切换,“非阻塞”用的是定时中断衍生出来的软件定时器。代码如下:

#include "REG52.H"

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;
void LedTask(void);

#defineBLINK_TIME_1   1000

sbit P0_0=P0^0;
sbit P0_1=P0^1;
sbit P0_2=P0^2;
sbit P0_3=P0^3;
sbit P0_4=P0^4;
sbit P0_5=P0^5;
sbit P0_6=P0^6;
sbit P0_7=P0^7;

volatile unsigned char vGu8TimeFlag_1=0;
volatile unsigned int vGu16TimeCnt_1=0;   

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
LedTask();   
    }
}

void T0_time() interrupt 1   
{
if(1==vGu8TimeFlag_1&&vGu16TimeCnt_1>0) //软件定时器
{
vGu16TimeCnt_1--;
}

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

//跑马灯的任务程序   
void LedTask(void)
{
static unsigned char Su8Step=0;    //加static修饰的局部变量,每次进来都会保留上一次值。

       switch(Su8Step)
       {
         case 0:
               if(0==vGu16TimeCnt_1)//时间到
               {

vGu8TimeFlag_1=0;
vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
vGu8TimeFlag_1=1;

                  P0_0=1;   //第0个灯熄灭
                  P0_1=0;   
                  P0_2=0;   
                  P0_3=0;   
                  P0_4=0;   
                  P0_5=0;   
                  P0_6=0;   
                  P0_7=0;   

Su8Step=1;//切换到下一个步骤,精髓语句!
}
               break;

         case 1:
               if(0==vGu16TimeCnt_1)//时间到
               {

vGu8TimeFlag_1=0;
vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
vGu8TimeFlag_1=1;

                  P0_0=0;   
                  P0_1=1;   //第1个灯熄灭
                  P0_2=0;   
                  P0_3=0;   
                  P0_4=0;   
                  P0_5=0;   
                  P0_6=0;   
                  P0_7=0;   

Su8Step=2;//切换到下一个步骤,精髓语句!
}
               break;

         case 2:
               if(0==vGu16TimeCnt_1)//时间到
               {

vGu8TimeFlag_1=0;
vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
vGu8TimeFlag_1=1;

                  P0_0=0;   
                  P0_1=0;   
                  P0_2=1;   //第2个灯熄灭
                  P0_3=0;   
                  P0_4=0;   
                  P0_5=0;   
                  P0_6=0;   
                  P0_7=0;   

Su8Step=3;//切换到下一个步骤,精髓语句!
}
               break;

         case 3:
               if(0==vGu16TimeCnt_1)//时间到
               {

vGu8TimeFlag_1=0;
vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
vGu8TimeFlag_1=1;

                  P0_0=0;   
                  P0_1=0;   
                  P0_2=0;   
                  P0_3=1;   //第3个灯熄灭
                  P0_4=0;   
                  P0_5=0;   
                  P0_6=0;   
                  P0_7=0;   

Su8Step=4;//切换到下一个步骤,精髓语句!
}
               break;

         case 4:
               if(0==vGu16TimeCnt_1)//时间到
               {

vGu8TimeFlag_1=0;
vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
vGu8TimeFlag_1=1;

                  P0_0=0;   
                  P0_1=0;   
                  P0_2=0;   
                  P0_3=0;   
                  P0_4=1;   //第4个灯熄灭
                  P0_5=0;   
                  P0_6=0;   
                  P0_7=0;   

Su8Step=5;//切换到下一个步骤,精髓语句!
}
               break;

         case 5:
               if(0==vGu16TimeCnt_1)//时间到
               {

vGu8TimeFlag_1=0;
vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
vGu8TimeFlag_1=1;

                  P0_0=0;   
                  P0_1=0;   
                  P0_2=0;   
                  P0_3=0;   
                  P0_4=0;   
                  P0_5=1;   //第5个灯熄灭
                  P0_6=0;   
                  P0_7=0;   

Su8Step=6;//切换到下一个步骤,精髓语句!
}
               break;

         case 6:
               if(0==vGu16TimeCnt_1)//时间到
               {

vGu8TimeFlag_1=0;
vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
vGu8TimeFlag_1=1;

                  P0_0=0;   
                  P0_1=0;   
                  P0_2=0;   
                  P0_3=0;   
                  P0_4=0;   
                  P0_5=0;   
                  P0_6=1;   //第6个灯熄灭
                  P0_7=0;   

Su8Step=7;//切换到下一个步骤,精髓语句!
}
               break;

         case 7:

            if(0==vGu16TimeCnt_1)   //时间到
               {
vGu8TimeFlag_1=0;
vGu16TimeCnt_1=BLINK_TIME_1;//重装定时的时间
vGu8TimeFlag_1=1;

                  P0_0=0;   
                  P0_1=0;   
                  P0_2=0;   
                  P0_3=0;   
                  P0_4=0;   
                  P0_5=0;   
                  P0_6=0;   
                  P0_7=1;   //第7个灯熄灭

Su8Step=0;   //返回到第0个步骤重新开始往下走,精髓语句!
}
               break;
}
}

       分析总结:这是第3种境界的跑马灯,很多初学者咋看此程序,表示不理解,人家一条赋值语句就解决8个LED一次性显示的问题,你非要拆分成8条按位赋值的语句,人家只用一个判断就实现了LED灯移动显示的功能,你非要整出8个步骤的切换,况且,整个程序的代码量明显增加了很多,这个程序好在哪?其实,我这么做是用心良苦呀。这个程序的代码量虽然增多了,但是仔细一看,并没有影响运行的效率。之所以把8个LED灯拆分成一个一个的LED灯单独赋值显示,是因为,在我眼里,这个8个LED灯代表的不仅仅是LED灯,而是8个输出信号!这8个输出信号未来驱动的可能是不同的继电器,气缸,电机,大炮,导弹,以及它们的各种千变万化的组合逻辑,拆分之后程序框架就有了无限可能的扩展性。之所以整出8个步骤的切换,也是同样的道理,为了增加程序框架无限可能的扩展性。这个程序虽然表面看起来繁琐,但是仔细一看它是“多而不乱”,非常富有“队形感”。因此可以这么说,这个看似繁琐的跑马灯程序,其实背后蕴藏了编程界的大智慧,它已经突破了“看山还是山”的境界。


jianhong_wu 发表于 2017-10-9 09:51:27

本帖最后由 jianhong_wu 于 2017-10-9 10:10 编辑

第九十节: 多任务并行处理两路跑马灯。

【90.1   多任务并行处理。】

      两路速度不同的跑马灯,代表了两路独立运行的任务,单片机如何“并行”处理这两路任务,就涉及到“多任务并行处理的编程思路”。

                  
                  上图90.1.1灌入式驱动8个LED   第1路跑马灯


               
               上图90.1.2灌入式驱动4个LED新增加的第2路跑马灯

       如上图,本节特别值得一提的是,新增加的第2路跑马灯用的是4个LED,这4个LED的驱动IO口是“散装的”,因为,前面3个是P1口的(P1.4,P1.5,P1.6),最后1个是P3口的(P3.3),这种情况下,肯定用不了“移位”的处理思路,只能用跑马灯第3种境界里所介绍的“状态切换非阻塞”思路,可见,“IO口拆分”和“switch状态切换”又一次充分体现了它们“程序框架万能扩展”的优越性。代码如下:#include "REG52.H"

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;
void Led_1_Task(void);
void Led_2_Task(void);

#defineBLINK_TIME_1   1000//控制第1路跑马灯的速度,数值越大“跑动”越慢。
#defineBLINK_TIME_2   200   //控制第2路跑马灯的速度,数值越大“跑动”越慢。

sbit P0_0=P0^0;
sbit P0_1=P0^1;
sbit P0_2=P0^2;
sbit P0_3=P0^3;
sbit P0_4=P0^4;
sbit P0_5=P0^5;
sbit P0_6=P0^6;
sbit P0_7=P0^7;

sbit P1_4=P1^4;
sbit P1_5=P1^5;
sbit P1_6=P1^6;
sbit P3_3=P3^3;

volatile unsigned char vGu8TimeFlag_1=0;
volatile unsigned int vGu16TimeCnt_1=0;   

volatile unsigned char vGu8TimeFlag_2=0;
volatile unsigned int vGu16TimeCnt_2=0;

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
Led_1_Task();   //第1路跑马灯
Led_2_Task();   //第2路跑马灯
    }
}

void T0_time() interrupt 1   
{
if(1==vGu8TimeFlag_1&&vGu16TimeCnt_1>0) //软件定时器1
{
vGu16TimeCnt_1--;
}

if(1==vGu8TimeFlag_2&&vGu16TimeCnt_2>0) //软件定时器2
{
vGu16TimeCnt_2--;
}


TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{

}

//第1路跑马灯
void Led_1_Task(void)
{
static unsigned char Su8Step=0;    //加static修饰的局部变量,每次进来都会保留上一次值。

       switch(Su8Step)
       {
         case 0:
               if(0==vGu16TimeCnt_1)//时间到
               {

vGu8TimeFlag_1=0;
vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
vGu8TimeFlag_1=1;

                  P0_0=1;   //第0个灯熄灭
                  P0_1=0;   
                  P0_2=0;   
                  P0_3=0;   
                  P0_4=0;   
                  P0_5=0;   
                  P0_6=0;   
                  P0_7=0;   

Su8Step=1;//切换到下一个步骤,精髓语句!
}
               break;

         case 1:
               if(0==vGu16TimeCnt_1)//时间到
               {

vGu8TimeFlag_1=0;
vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
vGu8TimeFlag_1=1;

                  P0_0=0;   
                  P0_1=1;   //第1个灯熄灭
                  P0_2=0;   
                  P0_3=0;   
                  P0_4=0;   
                  P0_5=0;   
                  P0_6=0;   
                  P0_7=0;   

Su8Step=2;//切换到下一个步骤,精髓语句!
}
               break;

         case 2:
               if(0==vGu16TimeCnt_1)//时间到
               {

vGu8TimeFlag_1=0;
vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
vGu8TimeFlag_1=1;

                  P0_0=0;   
                  P0_1=0;   
                  P0_2=1;   //第2个灯熄灭
                  P0_3=0;   
                  P0_4=0;   
                  P0_5=0;   
                  P0_6=0;   
                  P0_7=0;   

Su8Step=3;//切换到下一个步骤,精髓语句!
}
               break;

         case 3:
               if(0==vGu16TimeCnt_1)//时间到
               {

vGu8TimeFlag_1=0;
vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
vGu8TimeFlag_1=1;

                  P0_0=0;   
                  P0_1=0;   
                  P0_2=0;   
                  P0_3=1;   //第3个灯熄灭
                  P0_4=0;   
                  P0_5=0;   
                  P0_6=0;   
                  P0_7=0;   

Su8Step=4;//切换到下一个步骤,精髓语句!
}
               break;

         case 4:
               if(0==vGu16TimeCnt_1)//时间到
               {

vGu8TimeFlag_1=0;
vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
vGu8TimeFlag_1=1;

                  P0_0=0;   
                  P0_1=0;   
                  P0_2=0;   
                  P0_3=0;   
                  P0_4=1;   //第4个灯熄灭
                  P0_5=0;   
                  P0_6=0;   
                  P0_7=0;   

Su8Step=5;//切换到下一个步骤,精髓语句!
}
               break;

         case 5:
               if(0==vGu16TimeCnt_1)//时间到
               {

vGu8TimeFlag_1=0;
vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
vGu8TimeFlag_1=1;

                  P0_0=0;   
                  P0_1=0;   
                  P0_2=0;   
                  P0_3=0;   
                  P0_4=0;   
                  P0_5=1;   //第5个灯熄灭
                  P0_6=0;   
                  P0_7=0;   

Su8Step=6;//切换到下一个步骤,精髓语句!
}
               break;

         case 6:
               if(0==vGu16TimeCnt_1)//时间到
               {

vGu8TimeFlag_1=0;
vGu16TimeCnt_1=BLINK_TIME_1;   //重装定时的时间
vGu8TimeFlag_1=1;

                  P0_0=0;   
                  P0_1=0;   
                  P0_2=0;   
                  P0_3=0;   
                  P0_4=0;   
                  P0_5=0;   
                  P0_6=1;   //第6个灯熄灭
                  P0_7=0;   

Su8Step=7;//切换到下一个步骤,精髓语句!
}
               break;

         case 7:

            if(0==vGu16TimeCnt_1)   //时间到
               {
vGu8TimeFlag_1=0;
vGu16TimeCnt_1=BLINK_TIME_1;//重装定时的时间
vGu8TimeFlag_1=1;

                  P0_0=0;   
                  P0_1=0;   
                  P0_2=0;   
                  P0_3=0;   
                  P0_4=0;   
                  P0_5=0;   
                  P0_6=0;   
                  P0_7=1;   //第7个灯熄灭

Su8Step=0;   //返回到第0个步骤重新开始往下走,精髓语句!
}
               break;
}
}

//第2路跑马灯
void Led_2_Task(void)
{
/*
疑点讲解(1):
    这里第2路跑马灯的“Su8Step”与第1路跑马灯的“Su8Step”虽然同名,但是,因为它们是静态的局部变量,在两个不同的函数内部,是两个不同的变量,这两个变量所分配的RAM内存地址是不一样的,因此,它们虽然同名,但是不矛盾不冲突。
*/
static unsigned char Su8Step=0;    //加static修饰的局部变量,每次进来都会保留上一次值。

       switch(Su8Step)
       {
         case 0:
               if(0==vGu16TimeCnt_2)//时间到
               {

vGu8TimeFlag_2=0;
vGu16TimeCnt_2=BLINK_TIME_2;   //重装定时的时间
vGu8TimeFlag_2=1;

                  P1_4=1; //第0个灯熄灭
                  P1_5=0;   
                  P1_6=0;   
                  P3_3=0;   

Su8Step=1;//切换到下一个步骤,精髓语句!
}
               break;

         case 1:
               if(0==vGu16TimeCnt_2)//时间到
               {

vGu8TimeFlag_2=0;
vGu16TimeCnt_2=BLINK_TIME_2;   //重装定时的时间
vGu8TimeFlag_2=1;

                  P1_4=0;   
                  P1_5=1;   //第1个灯熄灭
                  P1_6=0;   
                  P3_3=0;   

Su8Step=2;//切换到下一个步骤,精髓语句!
}
               break;

         case 2:
               if(0==vGu16TimeCnt_2)//时间到
               {

vGu8TimeFlag_2=0;
vGu16TimeCnt_2=BLINK_TIME_2;   //重装定时的时间
vGu8TimeFlag_2=1;

                  P1_4=0;
                  P1_5=0;   
                  P1_6=1;   //第2个灯熄灭
                  P3_3=0;   

Su8Step=3;//切换到下一个步骤,精髓语句!
}
               break;

         case 3:
               if(0==vGu16TimeCnt_2)//时间到
               {

vGu8TimeFlag_2=0;
vGu16TimeCnt_2=BLINK_TIME_2;   //重装定时的时间
vGu8TimeFlag_2=1;

                  P1_4=0;
                  P1_5=0;   
                  P1_6=0;   
                  P3_3=1;   //第3个灯熄灭

Su8Step=0;   //返回到第0个步骤重新开始往下走,精髓语句!
}
               break;

}
}





jianhong_wu 发表于 2017-10-17 17:51:27

本帖最后由 jianhong_wu 于 2017-10-17 18:07 编辑

第九十一节: 蜂鸣器的“非阻塞”驱动。

【91.1   蜂鸣器的硬件电路简介。】

      
       上图91.1.1PNP三极管驱动有源蜂鸣器

       蜂鸣器有两种,一种是有源蜂鸣器,一种是无源蜂鸣器。有源蜂鸣器的驱动最简单,只要通电就一直响,断电就停,跟驱动LED灯一样。无源蜂鸣器则不一样,无源蜂鸣器一直断电不响,奇怪的是一直通电也不响,只有“通,关,通,关...”反复通电关电的状态,才会持续发生稳定的声音,此方式称为脉冲驱动方式,或者PWM驱动方式。本教程用的是有源蜂鸣器。
       蜂鸣器的驱动电路也有两种常用的方式,一种是NPN三极管驱动,一种是PNP三极管驱动。NPN三极管驱动电路,单片机输出“1”(高电平)蜂鸣器导通,输出“0”(低电平)蜂鸣器关闭。而PNP三极管驱动电路恰恰相反,单片机输出“0”(低电平)蜂鸣器导通,输出“1”(高电平)蜂鸣器关闭。本教程所用的是PNP三极管驱动电路,如上图。

【91.2   “非阻塞”驱动程序。】

       “驱动层”是相对“应用层”而言。“应用层”发号施令,“驱动层”负责执行。一个好的“驱动层”必须给“应用层”提供快捷便利的调用接口,此接口可以是函数或者全局变量。本节驱动蜂鸣器所用的是全局变量vGu16BeepTimerCnt。“应用层”只需给vGu16BeepTimerCnt赋值,就可以控制蜂鸣器发声,赋值越大,发声越长,500代表发声500ms,1000代表发声1000ms,具体细节实现,则由“驱动层”的驱动函数负责执行,驱动函数放在定时中断函数里定时扫描。为什么不把驱动函数放到main函数的循环里去?因为放在定时中断里,能保证蜂鸣器的声音长度是一致的,如果放在main循环里,声音的长度有可能在某些项目中受到某些必须一气呵成的任务干扰,得不到及时响应,影响声音长度的一致性。下面代码实现的功能是,单片机只要一上电,蜂鸣器就发出一次1000ms长度的“嘀”声音。#include "REG52.H"

#define BEEP_TIME1000   //控制蜂鸣器发声的长度,此处是1000ms

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   //蜂鸣器发声
void BeepClose(void);//蜂鸣器关闭
void VoiceScan(void);//蜂鸣器的驱动函数,放在定时中断里

sbit P3_4=P3^4;//控制蜂鸣器的IO口。0代表发声,1代表关闭。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;//控制蜂鸣器发声长度的计时器

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();   //此函数内部有“应用层”的赋值操作,控制上电的声音长度。
    while(1)
{
   ;
    }
}

void T0_time() interrupt 1   
{
VoiceScan();//蜂鸣器的驱动函数

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{
    vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=BEEP_TIME;//“应用层”只需赋值,一上电,蜂鸣器发出1000ms长度的声音。
    vGu8BeepTimerFlag=1;

}


//蜂鸣器发声
void BeepOpen(void)
{
P3_4=0;//0代表发声
}

//蜂鸣器关闭
void BeepClose(void)
{
P3_4=1;//1代表关闭
}

//蜂鸣器的驱动函数,放在定时中断函数里每定时1ms扫描一次。
void VoiceScan(void)
{
//Su8Lock的作用是避免BeepOpen()被重复扫描影响效率,发声时只执行一次此函数即可。
//同时,也巧妙借用else结构,实现逻辑顺序分解成“先发声,下一次再开始定时”的两个步骤。

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;//进入触发声音后就自锁起来
BeepOpen(); //发声,此处封装成函数,为了今后代码的移植性。
   }
    else//巧妙借用else结构,实现先发声,下一次中断再开始计时的逻辑顺序。比如,
{   //如果赋值1,就能确保有1ms的计时发声。

                     vGu16BeepTimerCnt--;          //定时器自减,控制蜂鸣器发声的时间长度

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   //关闭声音后,及时解锁,为下一次触发做准备
BeepClose();//关闭声音,此处封装成函数,为了今后代码的移植性。
                   }

}
          }         
}

jianhong_wu 发表于 2017-10-25 11:11:37

本帖最后由 jianhong_wu 于 2017-10-28 09:07 编辑

第九十二节: 独立按键的四大要素(自锁,消抖,非阻塞,清零式滤波)。

【92.1   独立按键的硬件电路简介。】



   
                上图92.1.1独立按键电路

       按键有两种驱动方式,一种是独立按键,一种是矩阵按键。1个独立按键要占用1个IO口,IO口不能共用。而矩阵按键的IO口是分时片选复用的,用少量的IO口就可以驱动翻倍级别的按键数量。比如,用8个IO口只能驱动8个独立按键,但是却可以驱动16个矩阵按键(4x4)。因此,按键少的时候就用独立按键,按键多的时候就用矩阵按键。这两种按键的驱动本质是一样的,都是靠识别输入信号的下降沿(或上升沿)来识别按键的触发。
       独立按键的硬件原理基础,如上图,P2.2这个IO口,在按键K1没有被按下的时候,P2.2口因为单片机内部自带上拉电阻把电平拉高,此时P2.2口是高电平的输入状态。当按键K1被按下的时候,按键K1左右像一根导线连接到电源的负极(GND),直接把原来P2.2口的电平拉低,此时P2.2口变成了低电平的输入状态。编写按键驱动程序,就是要识别这个电平从高到低的过程,这个过程也叫下降沿。多说一句,51单片机的P1,P2,P3口是内部自带上拉电阻的,而P0口是内部没有上拉电阻的,需要外接上拉电阻。除此之外,很多单片机内部其实都没有上拉电阻的,因此,建议大家在做独立按键电路的时候,养成一个习惯,凡是按键输入状态都外接上拉电阻。
       识别按键的下降沿触发有四大要素:自锁,消抖,非阻塞,清零式滤波。
       “自锁”,按键一旦进入到低电平,就要“自锁”起来,避免不断触发按键,只有当按键被松开变成高电平的时候,才及时“解锁”为下一次触发做准备。
       “消抖”,按键是一个机械触点器件,在接触的瞬间必然存在微观上的机械抖动,反馈到电平的瞬间就是“高,低,高,低...”这种不稳定的电平状态是一种干扰,但是,按键一旦按下去稳定了之后,这种状态就消失,电平就一直保持稳定的低电平。消抖的本质就是滤波,要把这种接触的瞬间抖动过滤掉,避免按键的“一按多触发”。
       “非阻塞”,在处理消抖的时候,必须用到延时,如果此时用阻塞的delay延时就会影响其它任务的运行效率,因此,用非阻塞的定时延时更加有优越性。
       “清零式滤波”,在消抖的时候,有两种境界,第一种境界是判断两次电平的状态,中间插入“固定的时间”延时,这种方法前后一共判断了两次,第一次是识别到低电平就进入延时的状态,第二次是延时后再确认一次是否继续是低电平的状态,这种方法的不足是,“固定的时间”全凭经验值,但是不同的按键它们的抖动时间长度是不同的,除此之外,前后才判断了两次,在软件的抗干扰能力上也弱了很多,“密码等级”不够高。第二种境界就是“清零式滤波”,“清零式滤波”非常巧妙,抗扰能力超强,它能自动过滤不同按键的“抖动时间”,然后再进入一个“稳定时间”的“N次识别判断”,更加巧妙的是,在“抖动时间”和“稳定时间”两者时间内,只要发现一次是高电平的干扰,就马上自动清零计时器,重新开始计时。“稳定时间”一般取20ms到30ms之间,而“抖动时间”是隐藏的,在代码上并没有直接描写出来,但是却无形地融入了代码之中,只有慢慢体会才能发现它的存在。
      具体的代码如下,实现的功能是按一次K1或者K2按键,就触发一次蜂鸣器鸣叫。
#include "REG52.H"

#define KEY_VOICE_TIME   50 //按键触发后发出的声音长度
#define KEY_FILTER_TIME25//按键滤波的“稳定时间”25ms

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);
void VoiceScan(void);
void KeyScan(void);    //按键识别的驱动函数,放在定时中断里
void KeyTask(void);    //按键任务函数,放在主函数内

sbit P3_4=P3^4;
sbit KEY_INPUT1=P2^2;//K1按键识别的输入口。
sbit KEY_INPUT2=P2^1;//K2按键识别的输入口。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

volatile unsigned char vGu8KeySec=0;//按键的触发序号,全局变量意味着是其它函数的接口。

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
   KeyTask();    //按键任务函数
    }
}

void T0_time() interrupt 1   
{
VoiceScan();
KeyScan();    //按键识别的驱动函数

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{
   
}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

/* 注释一:
* 独立按键扫描的详细过程,以按键K1为例,如下:
* 第一步:平时没有按键被触发时,按键的自锁标志,去抖动延时计数器一直被清零。
* 第二步:一旦有按键被按下,去抖动延时计数器开始在定时中断函数里累加,在还没累加到
*         阀值KEY_FILTER_TIME时,如果在这期间由于受外界干扰或者按键抖动,而使
*         IO口突然瞬间触发成高电平,这个时候马上把延时计数器Su16KeyCnt1清零了,这个过程
*         非常巧妙,非常有效地去除瞬间的杂波干扰。以后凡是用到开关感应器的时候,
*         都可以用类似这样的方法去干扰。
* 第三步:如果按键按下的时间达到阀值KEY_FILTER_TIME时,则触发按键,把编号vGu8KeySec赋值。
*         同时,马上把自锁标志Su8KeyLock1置1,防止按住按键不松手后一直触发。
* 第四步:等按键松开后,自锁标志Su8KeyLock1及时清零(解锁),为下一次自锁做准备。
* 第五步:以上整个过程,就是识别按键IO口下降沿触发的过程。
*/
void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock1; //1号按键的自锁
   static unsigned intSu16KeyCnt1; //1号按键的计时器
   static unsigned char Su8KeyLock2; //2号按键的自锁
   static unsigned intSu16KeyCnt2; //2号按键的计时器

   //1号按键
   if(0!=KEY_INPUT1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
   {
      Su8KeyLock1=0; //按键解锁
      Su16KeyCnt1=0; //按键去抖动延时计数器清零,此行非常巧妙,是全场的亮点。      
   }
   else if(0==Su8KeyLock1)//有按键按下,且是第一次被按下。这行很多初学者有疑问,请看专题分析。
   {
      Su16KeyCnt1++; //累加定时中断次数
      if(Su16KeyCnt1>=KEY_FILTER_TIME) //滤波的“稳定时间”KEY_FILTER_TIME,长度是25ms。
      {
         Su8KeyLock1=1;//按键的自锁,避免一直触发
         vGu8KeySec=1;    //触发1号键
      }
   }

   //2号按键
   if(0!=KEY_INPUT2)
   {
      Su8KeyLock2=0;
      Su16KeyCnt2=0;      
   }
   else if(0==Su8KeyLock2)
   {
      Su16KeyCnt2++;
      if(Su16KeyCnt2>=KEY_FILTER_TIME)
      {
         Su8KeyLock2=1;
         vGu8KeySec=2;    //触发2号键
      }
   }


}

void KeyTask(void)    //按键任务函数,放在主函数内
{
if(0==vGu8KeySec)
{
return; //按键的触发序号是0意味着无按键触发,直接退出当前函数,不执行此函数下面的代码
}

switch(vGu8KeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //1号按键

      vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//触发按键后,发出固定长度的声音
      vGu8BeepTimerFlag=1;
vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;

   case 2:   //2号按键

      vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//触发按键后,发出固定长度的声音
      vGu8BeepTimerFlag=1;
vGu8KeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;

}
}


【92.2   专题分析:else if(0==Su8KeyLock1)。】

       疑问:
   if(0!=KEY_INPUT1)
   {
      Su8KeyLock1=0;
      Su16KeyCnt1=0;      
   }
   else if(0==Su8KeyLock1)//有按键按下,且是第一次被按下。为什么?为什么?为什么?
   {
      Su16KeyCnt1++;
      if(Su16KeyCnt1>KEY_FILTER_TIME)
      {
         Su8KeyLock1=1;
         vGu8KeySec=1;   
      }
   }

       解答:
       首先,我们要明白C语言的语法中,
if(条件1)
{

}
else if(条件2)
{

}
       以上语句是一对组合语句,不能分开来看。当(条件1)成立的时候,它是绝对不会判断(条件2)的。当(条件1)不成立的时候,才会判断(条件2)。
       回到刚才的问题,当程序执行到(条件2) else if(0==Su8KeyLock1)的时候,就已经默认了(条件1) if(0!=KEY_INPUT1)不成立,这个条件不成立,就意味着0==KEY_INPUT1,也就是有按键被按下,因此,这里的else if(0==Su8KeyLock1)等效于else if(0==Su8KeyLock1&&0==KEY_INPUT1),而Su8KeyLock1是一个自锁标志位,一旦按键被触发后,这个标志位会变1,防止按键按住不松手的时候不断触发按键。这样,按键只能按一次触发一次,松开手后再按一次,又触发一次。

【92.3   专题分析:if(0!=KEY_INPUT1)。】

       疑问:为什么不用if(1==KEY_INPUT1)而用if(0!=KEY_INPUT1)?
       解答:其实两者在功能上是完全等效的,在这里都可以用。之所以本教程优先选用后者if(0!=KEY_INPUT1),是因为考虑到了代码在不同单片机平台上的可移植性和兼容性。很多32位的单片机提供的是库函数,库函数返回的按键状态是一个字节变量来表示,当被按下的时候是0,但是,当没有按下的时候并不一定等于1,而是一个“非0”的数值。

【92.4   专题分析:把KeyScan函数放在定时器中断里。】

       疑问:为什么把KeyScan函数放在定时器中断里?
       解答:中断函数里放的函数或者代码越少越好,但是KeyScan函数是特殊的函数,是涉及到IO口输入信号的滤波,滤波就涉及到时间的及时性与均匀性,放在定时中断函数里更加能保证时间的一致性。比如,蜂鸣器驱动,动态数码管驱动,按键扫描驱动,我个人都习惯放在定时中断函数里。

【92.5   专题分析:if(0==vGu8KeySec)return。】

       疑问:if(0==vGu8KeySec)return是不是多此一举?
       解答:在KeyTask函数这里,if(0==vGu8KeySec)return这行代码删掉,对程序功能是没有影响的,这里之所以多插入这行判断语句,是因为,当按键多达几十个的时候,避免主函数每次进入KeyTask函数,都挨个扫描判断switch的状态进行多次判断,如果增加了这行if(0==vGu8KeySec)return代码,就可以直接退出省事,在理论上感觉更加运行高效。其实,不同单片机不同的C编译器可能对switch语句的翻译不一样,因此,这里的是不是更加高效我不敢保证。但是可以保证的是,加了这行代码也没有其它副作用。

jianhong_wu 发表于 2017-10-29 10:46:59

本帖最后由 jianhong_wu 于 2017-10-29 11:45 编辑

第九十三节: 独立按键鼠标式的单击与双击。

【93.1   鼠标式的单击与双击。】




                上图93.1.1独立按键电路


      
                上图93.1.2LED电路

            
                上图93.1.3有源蜂鸣器电路

      鼠标的左键,可以触发单击,也可以触发双击。双击的规则是这样的,两次单击,如果第1次单击与第2次单击的时间比较“短”的时候,则这两次单击就构成双击。编写这个程序的最大亮点是如何控制好第1次单击与第2次单击的时间间隔。程序例程要实现的功能是:(1)单击改变LED灯的显示状态,单击一次LED从原来“灭”的状态变成“亮”的状态,或者从原来“亮”的状态变成“灭”的状态,依次循环切换。(2)双击则蜂鸣器发出“嘀”的一声。代码如下:
#include "REG52.H"

#define KEY_VOICE_TIME   50   //按键触发后发出的声音长度
#define KEY_FILTER_TIME25   //按键滤波的“稳定时间”25ms
#define KEY_INTERVAL_TIME250//连续两次单击之间的最大有效时间250ms

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);
void LedOpen(void);   
void LedClose(void);

void VoiceScan(void);
void KeyScan(void);    //按键识别的驱动函数,放在定时中断里
void SingleKeyTask(void);   //单击按键任务函数,放在主函数内
void DoubleKeyTask(void);   //双击按键任务函数,放在主函数内

sbit P3_4=P3^4;       //蜂鸣器
sbit P1_4=P1^4;       //LED

sbit KEY_INPUT1=P2^2;//K1按键识别的输入口。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

unsigned char Gu8LedStatus=0; //记录LED灯的状态,0代表灭,1代表亮
volatile unsigned char vGu8SingleKeySec=0;//单击按键的触发序号
volatile unsigned char vGu8DoubleKeySec=0;//双击按键的触发序号

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
   SingleKeyTask();    //单击按键任务函数
   DoubleKeyTask();    //双击按键任务函数
    }
}

void T0_time() interrupt 1   
{
VoiceScan();
KeyScan();    //按键识别的驱动函数

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{
/* 注释一:
* 把LED的初始化放在PeripheralInitial而不是放在SystemInitial,是因为LED显示内容对上电
* 瞬间的要求不高。但是,如果是控制继电器,则应该把继电器的输出初始化放在SystemInitial。
*/

    //根据Gu8LedStatus的值来初始化LED当前的显示状态,0代表灭,1代表亮
if(0==Gu8LedStatus)
{
    LedClose();//关闭LED
}
else
{
    LedOpen();   //点亮LED
}
}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}
void LedOpen(void)
{
P1_4=0;
}

void LedClose(void)
{
P1_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;       

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }       
}

/* 注释二:
* 双击按键扫描的详细过程:
* 第一步:平时没有按键被触发时,按键的自锁标志,去抖动延时计数器一直被清零。
*         如果之前已经有按键触发过1次单击,那么启动时间间隔计数器Su16KeyIntervalCnt1,
*         在KEY_INTERVAL_TIME这个允许的时间差范围内,如果一直没有第2次单击触发,
*         则把累加按键触发的次数Su8KeyTouchCnt1也清零,上一次累计的单击数被清零,
*         就意味着下一次新的双击必须重新开始累加两次单击数。
* 第二步:一旦有按键被按下,去抖动延时计数器开始在定时中断函数里累加,在还没累加到
*         阀值KEY_FILTER_TIME时,如果在这期间由于受外界干扰或者按键抖动,而使
*         IO口突然瞬间触发成高电平,这个时候马上把延时计数器Su16KeyTimeCnt1
*         清零了,这个过程非常巧妙,非常有效地去除瞬间的杂波干扰,以后凡是用到开关感应器的时候,
*         都可以用类似这样的方法去干扰。
* 第三步:如果按键按下的时间超过了阀值KEY_FILTER_TIME,马上把自锁标志Su8KeyLock1置1,
*         防止按住按键不松手后一直触发。与此同时,累加1次按键次数,如果按键次数累加有2次,
*         则认为触发双击按键,并把编号vGu8DoubleKeySec赋值。
* 第四步:等按键松开后,自锁标志Su8KeyLock1及时清零解锁,为下一次自锁做准备。并且累加间隔时间,
*         防止两次按键的间隔时间太长。如果连续2次单击的间隔时间太长达到了KEY_INTERVAL_TIME
*         的长度,立即清零当前按键次数的计数器,这样意味着上一次的累加单击数无效,下一次双击
*         必须重新累加新的单击数。
*/

void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock1;         //1号按键的自锁
   static unsigned intSu16KeyCnt1;         //1号按键的计时器
   static unsigned char Su8KeyTouchCnt1;       //1号按键的次数记录
   static unsigned intSu16KeyIntervalCnt1;   //1号按键的间隔时间计数器

   //1号按键
   if(0!=KEY_INPUT1)//IO是高电平,说明按键没有被按下,这时要及时清零一些标志位
   {
       Su8KeyLock1=0; //按键解锁
       Su16KeyCnt1=0; //按键去抖动延时计数器清零,此行非常巧妙。      
       if(Su8KeyTouchCnt1>=1) //之前已经有按键触发过一次,再来一次就构成双击
       {
         Su16KeyIntervalCnt1++; //按键间隔的时间计数器累加
         if(Su16KeyIntervalCnt1>=KEY_INTERVAL_TIME) //达到最大允许的间隔时间
         {
               Su16KeyIntervalCnt1=0; //时间计数器清零
               Su8KeyTouchCnt1=0;   //清零按键的按下的次数
         }
       }
   }
   else if(0==Su8KeyLock1)//有按键按下,且是第一次被按下。此行如有疑问,请看第92节的讲解。
   {
      Su16KeyCnt1++; //累加定时中断次数
      if(Su16KeyCnt1>=KEY_FILTER_TIME) //滤波的“稳定时间”KEY_FILTER_TIME,长度是25ms。
      {
         Su8KeyLock1=1;//按键的自锁,避免一直触发
         Su16KeyIntervalCnt1=0;   //按键有效间隔的时间计数器清零
         Su8KeyTouchCnt1++;       //记录当前单击的次数
         if(1==Su8KeyTouchCnt1)   //只按了1次
         {
            vGu8SingleKeySec=1;   //单击任务
         }
         else if(Su8KeyTouchCnt1>=2)//连续按了两次以上
         {
            Su8KeyTouchCnt1=0;    //统计按键次数清零
            vGu8SingleKeySec=1;   //单击任务
vGu8DoubleKeySec=1;   //双击任务
         }
      }
   }

}

void SingleKeyTask(void)    //单击按键任务函数,放在主函数内
{
if(0==vGu8SingleKeySec)
{
return; //按键的触发序号是0意味着无按键触发,直接退出当前函数,不执行此函数下面的代码
}

switch(vGu8SingleKeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //单击任务
      //通过Gu8LedStatus的状态切换,来反复切换LED的“灭”与“亮”的状态
      if(0==Gu8LedStatus)
      {
            Gu8LedStatus=1; //标识并且更改当前LED灯的状态。0就变成1。
            LedOpen();   //点亮LED
}
      else
      {
            Gu8LedStatus=0; //标识并且更改当前LED灯的状态。1就变成0。
            LedClose();//关闭LED
}

vGu8SingleKeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;
}

}

void DoubleKeyTask(void)    //双击按键任务函数,放在主函数内
{
if(0==vGu8DoubleKeySec)
{
return; //按键的触发序号是0意味着无按键触发,直接退出当前函数,不执行此函数下面的代码
}

switch(vGu8DoubleKeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //双击任务

      vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//触发双击后,发出“嘀”一声
      vGu8BeepTimerFlag=1;
vGu8DoubleKeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;
}

}




jianhong_wu 发表于 2017-11-5 11:14:57

本帖最后由 jianhong_wu 于 2017-11-5 11:30 编辑

第九十四节: 两个独立按键构成的组合按键。

【94.1   组合按键。】




                上图94.1.1独立按键电路



                上图94.1.2LED电路

               
                上图94.1.3有源蜂鸣器电路

      组合按键的触发,是指两个按键同时按下时的“非单击”触发。一次组合按键的产生,必然包含了三类按键的触发。比如,K1与K2两个独立按键,当它们产生一次组合按键的操作时,就包含了三类触发:K1单击触发,K2单击触发,K1与K2的组合触发。这三类触发可以看作是底层的按键驱动程序,在按键应用层的任务函数SingleKeyTask和CombinationKeyTask中,可以根据项目的实际需要进行响应。本节程序例程要实现的功能是:(1)K1单击让LED变成“亮”的状态。(2)K2单击让LED变成“灭”的状态。(3)K1与K2的组合按键触发让蜂鸣器发出“嘀”的一声。代码如下:

#include "REG52.H"

#define KEY_VOICE_TIME   50   //组合按键触发后发出的声音长度
#define KEY_FILTER_TIME25   //按键滤波的“稳定时间”25ms

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);
void LedOpen(void);   
void LedClose(void);

void VoiceScan(void);
void KeyScan(void);    //按键识别的驱动函数,放在定时中断里
void SingleKeyTask(void);   //单击按键任务函数,放在主函数内
void CombinationKeyTask(void);   //组合按键任务函数,放在主函数内

sbit P3_4=P3^4;       //蜂鸣器
sbit P1_4=P1^4;       //LED

sbit KEY_INPUT1=P2^2;//K1按键识别的输入口。
sbit KEY_INPUT2=P2^1;//K2按键识别的输入口。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

volatile unsigned char vGu8SingleKeySec=0;//单击按键的触发序号
volatile unsigned char vGu8CombinationKeySec=0;//组合按键的触发序号

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
   CombinationKeyTask();//组合按键任务函数
   SingleKeyTask();    //单击按键任务函数
    }
}

void T0_time() interrupt 1   
{
VoiceScan();
KeyScan();    //按键识别的驱动函数

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{
LedClose();//初始化关闭LED
}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}
void LedOpen(void)
{
P1_4=0;
}

void LedClose(void)
{
P1_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}


/* 注释一:
* 组合按键扫描的详细过程:
* 第一步:平时只要K1与K2两个按键中有一个没有被按下时,按键的自锁标志,去抖动延时计数器
* 一直被清零。
* 第二步:一旦两个按键都处于被按下的状态,去抖动延时计数器开始在定时中断函数里累加,在还没
*         累加到阀值KEY_FILTER_TIME时,如果在这期间由于受外界干扰或者按键抖动,而使其中一个
*         IO口突然瞬间触发成高电平,这个时候马上把延时计数器Su16CombinationKeyTimeCnt
*         清零了,这个过程非常巧妙,非常有效地去除瞬间的杂波干扰。
* 第三步:如果两个按键按下的时间超过了阀值KEY_FILTER_TIME,马上把自锁标志Su8CombinationKeyLock
*         置1,防止按住两个按键不松手后一直触发。并把按键编号vGu8CombinationKeySec赋值,
*         触发一次组合按键。
* 第四步:等其中一个按键松开后,自锁标志Su8CombinationKeyLock及时清零,为下一次自锁做准备。
*/

void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock1;      
   static unsigned intSu16KeyCnt1;         
   static unsigned char Su8KeyLock2;         
   static unsigned intSu16KeyCnt2;         

   static unsigned char Su8CombinationKeyLock;//组合按键的自锁
   static unsigned intSu16CombinationKeyCnt;//组合按键的计时器


   //K1按键与K2按键的组合触发
   if(0!=KEY_INPUT1||0!=KEY_INPUT2)//两个按键只要有一个按键没有按下,处于“非组合按键”的状态。
   {
      Su8CombinationKeyLock=0; //组合按键解锁
      Su16CombinationKeyCnt=0;//组合按键去抖动延时计数器清零,此行非常巧妙,是全场的亮点。      
   }
   else if(0==Su8CombinationKeyLock)//两个按键被同时按下,且是第一次被按下。此行请看专题分析。
   {
      Su16CombinationKeyCnt++; //累加定时中断次数
      if(Su16CombinationKeyCnt>=KEY_FILTER_TIME) //滤波的“稳定时间”KEY_FILTER_TIME。
      {
         Su8CombinationKeyLock=1;//组合按键的自锁,避免一直触发
         vGu8CombinationKeySec=1;   //触发K1与K2的组合键操作
      }
   }

   //K1按键的单击
   if(0!=KEY_INPUT1)
   {
      Su8KeyLock1=0;
      Su16KeyCnt1=0;      
   }
   else if(0==Su8KeyLock1)
   {
      Su16KeyCnt1++;
      if(Su16KeyCnt1>=KEY_FILTER_TIME)
      {
         Su8KeyLock1=1;
         vGu8SingleKeySec=1;    //触发K1的单击键
      }
   }

   //K2按键的单击
   if(0!=KEY_INPUT2)
   {
      Su8KeyLock2=0;
      Su16KeyCnt2=0;      
   }
   else if(0==Su8KeyLock2)
   {
      Su16KeyCnt2++;
      if(Su16KeyCnt2>=KEY_FILTER_TIME)
      {
         Su8KeyLock2=1;
         vGu8SingleKeySec=2;    //触发K2的单击键
      }
   }

}

void CombinationKeyTask(void)    //组合按键任务函数,放在主函数内
{
if(0==vGu8CombinationKeySec)
{
return; //按键的触发序号是0意味着无按键触发,直接退出当前函数,不执行此函数下面的代码
}

switch(vGu8CombinationKeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //K1与K2的组合按键任务

      vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//触发一次组合按键后,发出“嘀”一声
      vGu8BeepTimerFlag=1;
vGu8CombinationKeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;
}

}

void SingleKeyTask(void)    //单击按键任务函数,放在主函数内
{
if(0==vGu8SingleKeySec)
{
return; //按键的触发序号是0意味着无按键触发,直接退出当前函数,不执行此函数下面的代码
}

switch(vGu8SingleKeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //K1单击任务
      LedOpen();    //LED亮

vGu8SingleKeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;

   case 2:   //K2单击任务
      LedClose();   //LED灭

vGu8SingleKeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;

}

}


【94.2   专题分析:else if(0==Su8CombinationKeyLock)。】

      疑问:
   if(0!=KEY_INPUT1||0!=KEY_INPUT2)
   {
      Su8CombinationKeyLock=0;
      Su16CombinationKeyCnt=0;      
   }
   else if(0==Su8CombinationKeyLock)//两个按键被同时按下,且是第一次被按下。为什么?
   {
      Su16CombinationKeyCnt++;
      if(Su16CombinationKeyCnt>=KEY_FILTER_TIME)
      {
         Su8CombinationKeyLock=1;
         vGu8CombinationKeySec=1;   
      }
   }

       解答:
       首先,我们要明白C语言的语法中,
if(条件1)
{

}
else if(条件2)
{

}
       以上语句是一对组合语句,不能分开来看。当(条件1)成立的时候,它是绝对不会判断(条件2)的。当(条件1)不成立的时候,才会判断(条件2)。
       回到刚才的问题,当程序执行到(条件2) else if(0==Su8CombinationKeyLock)的时候,就已经默认了(条件1) if(0!=KEY_INPUT1||0!=KEY_INPUT2)不成立,这个条件不成立,就意味着0==KEY_INPUT1和0==KEY_INPUT2,也就是有两个按键被同时按下,因此,这里的else if(0==Su8CombinationKeyLock)等效于else if(0==Su8CombinationKeyLock&&0==KEY_INPUT1&&0==KEY_INPUT2),而Su8CombinationKeyLock是一个自锁标志位,一旦组合按键被触发后,这个标志位会变1,防止两个按键按住不松手的时候不断触发组合按键。这样,组合按键只能同时按下一次触发一次,任意松开其中一个按键后再同时按下一次两个按键,又触发一次新的组合按键。


jianhong_wu 发表于 2017-11-12 15:22:13

本帖最后由 jianhong_wu 于 2017-11-12 15:37 编辑

第九十五节: 两个独立按键的“电脑键盘式”组合按键。
【95.1   “电脑键盘式”组合按键。】



                上图95.1.1独立按键电路

            
                上图95.1.2LED电路
               
                上图95.1.3有源蜂鸣器电路
      上一节也讲了由K1和K2构成的组合按键,但是这种组合按键是普通的组合按键,因为它们的K1和K2是不分先后顺序的,你先按住K1然后再按K2,或者你先按住K2然后再按K1,效果都一样。本节讲的组合按键是分先后顺序的,比如,像电脑的复制快捷键(Ctrl+C),你必须先按住Ctrl再按住C此时“复制快捷键”才有效,如果你先按住C再按住Ctrl此时“复制快捷键”无效。本节讲的例程就是要实现这个功能,用K1代表C这类“字符数字键”,用K2代表Ctrl这类“辅助按键”,因此,要触发组合键(K2+K1),必须先按住K2再按K1才有效。本节讲的例程功能如下:(1)K1每单击一次,LED要么从“灭”变成“亮”,要么从“亮”变成“灭”,在两种状态之间切换。(2)如果先按住K2再按K1,就认为构造了“电脑键盘式”组合键,蜂鸣器发出“嘀”的一声。代码如下:#include "REG52.H"

#define KEY_VOICE_TIME   50   //组合按键触发后发出的声音长度 50ms
#define KEY_FILTER_TIME25   //按键滤波的“稳定时间”25ms

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);
void LedOpen(void);   
void LedClose(void);

void VoiceScan(void);
void KeyScan(void);    //按键识别的驱动函数,放在定时中断里
void SingleKeyTask(void);   //单击按键任务函数,放在主函数内
void CombinationKeyTask(void);   //组合按键任务函数,放在主函数内

sbit P3_4=P3^4;       //蜂鸣器
sbit P1_4=P1^4;       //LED

sbit KEY_INPUT1=P2^2;//K1按键识别的输入口。
sbit KEY_INPUT2=P2^1;//K2按键识别的输入口。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

unsigned char Gu8LedStatus=0; //记录LED灯的状态,0代表灭,1代表亮

volatile unsigned char vGu8SingleKeySec=0;//单击按键的触发序号
volatile unsigned char vGu8CombinationKeySec=0;//组合按键的触发序号

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
   CombinationKeyTask();//组合按键任务函数
   SingleKeyTask();       //单击按键任务函数
    }
}

void T0_time() interrupt 1   
{
VoiceScan();
KeyScan();    //按键识别的驱动函数

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{
if(0==Gu8LedStatus)
{
LedClose();
}
else
{
LedOpen();
}
}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}
void LedOpen(void)
{
P1_4=0;
}

void LedClose(void)
{
P1_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}


/* 注释一:
* “电脑键盘式”组合按键扫描的详细过程:
* 第一步:K2与K1构成的组合按键触发是融合在K1单击按键程序里的,只需稍微更改一下K1单击的程序
*         ,就可以兼容到K2与K1构成的“电脑键盘式”组合按键。平时只要K1没有被按下时,按
*         键的自锁标志Su8KeyLock1和去抖动延时计数器Su16KeyCnt1一直被清零。
* 第二步:一旦K1按键被按下,去抖动延时计数器Su16KeyCnt1开始在定时中断函数里累加,在还没
*         累加到阀值KEY_FILTER_TIME时,如果在这期间由于受外界干扰或者按键抖动,而使
*         IO口突然瞬间触发成高电平,这个时候马上把延时计数器Su16KeyCnt1清零了,
*         这个过程非常巧妙,非常有效地去除瞬间的杂波干扰。
* 第三步:如果K1按键按下的时间超过了阀值KEY_FILTER_TIME,马上把自锁标志Su8KeyLock1置1,
*         防止按住按键不松手后一直触发,此时才开始判断一次K2按键的电平状态,如果K2为低电
*         平就认为是组合按键,并给按键编号vGu8CombinationKeySec赋值,否则,就认为是K1的单击
*         按键,并给按键编号vGu8SingleKeySec赋值。
* 第四步:等K1按键松开后,自锁标志Su8KeyLock1及时清零,为下一次自锁做准备。
*/

void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock1;      
   static unsigned intSu16KeyCnt1;         


   //K1的单击,或者K2与K1构成的“电脑键盘式组合按键”。
   if(0!=KEY_INPUT1)//单个K1按键没有按下,及时清零一些标志。
   {
      Su8KeyLock1=0; //按键解锁
      Su16KeyCnt1=0;//去抖动延时计数器清零,此行非常巧妙,是全场的亮点。      
   }
   else if(0==Su8KeyLock1)//单个按键K1被按下
   {
      Su16KeyCnt1++; //累加定时中断次数
      if(Su16KeyCnt1>=KEY_FILTER_TIME) //滤波的“稳定时间”KEY_FILTER_TIME。
      {
         if(0==KEY_INPUT2)//此时才开始判断一次K2的电平状态,为低电平则是组合按键。
         {
            Su8KeyLock1=1;
            vGu8CombinationKeySec=1;//组合按键的触发
}
else   
         {
            Su8KeyLock1=1;
            vGu8SingleKeySec=1;    //K1单击按键的触发
}
      }
   }
}

void CombinationKeyTask(void)    //组合按键任务函数,放在主函数内
{
if(0==vGu8CombinationKeySec)
{
return; //按键的触发序号是0意味着无按键触发,直接退出当前函数,不执行此函数下面的代码
}

switch(vGu8CombinationKeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //K1与K2的组合按键任务

      vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//触发一次组合按键后,发出“嘀”一声
      vGu8BeepTimerFlag=1;
vGu8CombinationKeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;
}

}

void SingleKeyTask(void)    //单击按键任务函数,放在主函数内
{
if(0==vGu8SingleKeySec)
{
return; //按键的触发序号是0意味着无按键触发,直接退出当前函数,不执行此函数下面的代码
}

switch(vGu8SingleKeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //K1单击任务
if(0==Gu8LedStatus)
{
Gu8LedStatus=1;
LedOpen();    //LED亮   
}
else
{
Gu8LedStatus=0;
LedClose();    //LED灭   
}

vGu8SingleKeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;
}

}


jianhong_wu 发表于 2017-11-19 10:57:14

本帖最后由 jianhong_wu 于 2017-11-19 11:30 编辑

第九十六节: 独立按键“一键两用”的短按与长按。

【96.1   “一键两用”的短按与长按。】


                上图96.1.1独立按键电路


                上图96.1.2LED电路

               
                上图96.1.3有源蜂鸣器电路

      某些项目,当外部按键的资源比较少的时候,一个按键也可以“一键多用”。“一键多用”有很多种玩法,比如,谍战片的无线电通信,依赖一个按键的“不同敲击频率”就可以发送内容丰富的情报。本节“一键两用”也是属于“一键多用”的众多玩法之一。“短按与长按”的原理是依赖“按键按下的时间长度”来区分识别。“短按”是指从按下的“下降沿”到松手的“上升沿”时间,“长按”是指从按下的“下降沿”到一直按住不松手的“低电平持续时间”。本节的例程功能如下:(1)K1每“短按”一次(25ms),LED要么从“灭”变成“亮”,要么从“亮”变成“灭”,在两种状态之间切换。(2)K1每“长按”一次(500ms),蜂鸣器发出“嘀”的一声。代码如下:#include "REG52.H"

#define KEY_VOICE_TIME   50   //按键“长按”触发后发出的声音长度 50ms

#define KEY_SHORT_TIME25      //按键的“短按”兼“滤波”的“稳定时间”25ms
#define KEY_LONG_TIME500      //按键的“长按”兼“滤波”的“稳定时间”500ms

void T0_time();
void SystemInitial(void) ;
void Delay(unsigned long u32DelayTime) ;
void PeripheralInitial(void) ;

void BeepOpen(void);   
void BeepClose(void);
void LedOpen(void);   
void LedClose(void);

void VoiceScan(void);
void KeyScan(void);    //按键识别的驱动函数,放在定时中断里
void SingleKeyTask(void);   //单击按键任务函数,放在主函数内

sbit P3_4=P3^4;       //蜂鸣器
sbit P1_4=P1^4;       //LED

sbit KEY_INPUT1=P2^2;//K1按键识别的输入口。

volatile unsigned char vGu8BeepTimerFlag=0;
volatile unsigned int vGu16BeepTimerCnt=0;

unsigned char Gu8LedStatus=0; //记录LED灯的状态,0代表灭,1代表亮

volatile unsigned char vGu8SingleKeySec=0;//单击按键的触发序号

void main()
{
SystemInitial();            
Delay(10000);               
PeripheralInitial();      
    while(1)
{
   SingleKeyTask();       //单击按键任务函数
    }
}

void T0_time() interrupt 1   
{
VoiceScan();
KeyScan();    //按键识别的驱动函数

TH0=0xfc;   
TL0=0x66;   
}


void SystemInitial(void)
{
TMOD=0x01;
TH0=0xfc;   
TL0=0x66;   
EA=1;      
ET0=1;      
TR0=1;      
}

void Delay(unsigned long u32DelayTime)
{
    for(;u32DelayTime>0;u32DelayTime--);
}

void PeripheralInitial(void)
{
if(0==Gu8LedStatus)
{
LedClose();
}
else
{
LedOpen();
}
}

void BeepOpen(void)
{
P3_4=0;
}

void BeepClose(void)
{
P3_4=1;
}
void LedOpen(void)
{
P1_4=0;
}

void LedClose(void)
{
P1_4=1;
}

void VoiceScan(void)
{

          static unsigned char Su8Lock=0;

if(1==vGu8BeepTimerFlag&&vGu16BeepTimerCnt>0)
          {
                  if(0==Su8Lock)
                  {
                   Su8Lock=1;
BeepOpen();
   }
    else
{   

                     vGu16BeepTimerCnt--;         

                   if(0==vGu16BeepTimerCnt)
                   {
                           Su8Lock=0;   
BeepClose();
                   }

}
          }         
}

/* 注释一:
* “长按”与“短按”的识别过程:
* 第一步:平时只要K1没有被按下,按键的自锁标志Su8KeyLock1和去抖动延时计数器Su16KeyCnt1
*         一直被清零。此时属于按键“松手时间”,因此同时检测“短按”标志Su8KeyShortFlag
*         是否有效,如果有效就触发一次“短按”。
* 第二步:一旦K1按键被按下,去抖动延时计数器Su16KeyCnt1开始在定时中断函数里累加,在还没
*         累加到阀值KEY_SHORT_TIME和KEY_LONG_TIME时,如果在这期间由于受外界干扰或者
*         按键抖动,而使IO口突然瞬间触发成高电平,这个时候马上把延时计数器Su16KeyCnt1清零,
*         这个过程非常巧妙,非常有效地去除瞬间的杂波干扰。
* 第三步:如果K1按键按下的时间超过了“短按”阀值KEY_SHORT_TIME,马上把“短按”标志
*         Su8KeyShortFlag置1,如果此时还没有松手,直到发现按下的时间超过“长按”阀值
*         KEY_LONG_TIME时,先把“短按”标志ucShortTouchFlag1清零,然后触发“长按”,同时,为
*         了防止按住按键不松手后一直触发,要及时把Su8KeyLock1置1“自锁”。
* 第四步:等K1按键松手后,自锁标志Su8KeyLock1及时清零,为下一次自锁做准备,同时,也检测
*         “短按”标志Su8KeyShortFlag是否有效,如果有效就触发一次“短按”。
*/

void KeyScan(void)//此函数放在定时中断里每1ms扫描一次
{
   static unsigned char Su8KeyLock1;      
   static unsigned intSu16KeyCnt1;
   static unsigned char Su8KeyShortFlag=0;//按键“短按”触发的标志      

    if(0!=KEY_INPUT1)//单个K1按键没有按下,及时清零一些标志。
   {
      Su8KeyLock1=0; //按键解锁
      Su16KeyCnt1=0;//去抖动延时计数器清零,此行非常巧妙,是全场的亮点。
      if(1==Su8KeyShortFlag)//松手的时候,如果“短按”标志有效就触发一次“短按”
      {
Su8KeyShortFlag=0;   //先清零“短按”标志避免一直触发。
vGu8SingleKeySec=1;    //触发K1的“短按”
}
   }
   else if(0==Su8KeyLock1)//单个按键K1被按下
   {
      Su16KeyCnt1++; //累加定时中断次数

      if(Su16KeyCnt1>=KEY_SHORT_TIME) //“短按”兼“滤波”的“稳定时间”KEY_SHORT_TIME
      {
            //注意,这里不能“自锁”。后面“长按”触发的时候才“自锁”。
            Su8KeyShortFlag=1;    //K1的“短按”标志有效,待松手时触发。
      }


      if(Su16KeyCnt1>=KEY_LONG_TIME) //“长按”兼“滤波”的“稳定时间”KEY_LONG_TIME
      {
            Su8KeyLock1=1;      //此时“长按”触发才“自锁”
Su8KeyShortFlag=0;//既然此时“长按”有效,那么就要废除潜在的“短按”。
            vGu8SingleKeySec=2; //触发K1的“长按”
      }
   }
}

void SingleKeyTask(void)    //单击按键任务函数,放在主函数内
{
if(0==vGu8SingleKeySec)
{
return; //按键的触发序号是0意味着无按键触发,直接退出当前函数,不执行此函数下面的代码
}

switch(vGu8SingleKeySec) //根据不同的按键触发序号执行对应的代码
{
   case 1:   //K1“短按”触发的任务
if(0==Gu8LedStatus)
{
Gu8LedStatus=1;
LedOpen();    //LED亮   
}
else
{
Gu8LedStatus=0;
LedClose();    //LED灭   
}
vGu8SingleKeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;

   case 2:   //K1“长按”触发的任务
      vGu8BeepTimerFlag=0;
vGu16BeepTimerCnt=KEY_VOICE_TIME;//触发一次“长按”后,发出“嘀”一声
      vGu8BeepTimerFlag=1;
vGu8SingleKeySec=0;//响应按键服务处理程序后,按键编号必须清零,避免一致触发
break;

}
}





页: 1 2 3 4 [5] 6 7
查看完整版本: 从单片机基础到程序框架(连载)