打印
[51单片机]

从单片机基础到程序框架(连载)

[复制链接]
楼主: jianhong_wu
手机看帖
扫描二维码
随时随地手机跟帖
281
in的市场

使用特权

评论回复
282
YLSJWGX| | 2017-6-25 11:16 | 只看该作者
感谢您的回复,市场上有兼容你写的技术帖子51单片机开发板吗?再次感谢!吴国新

使用特权

评论回复
283
YLSJWGX| | 2017-6-25 11:17 | 只看该作者
感谢您的回复,市场上有兼容你写的技术帖子51单片机开发板吗?再次感谢!

使用特权

评论回复
284
nth2do| | 2017-7-1 15:47 | 只看该作者
感谢无私分享

使用特权

评论回复
285
jianhong_wu|  楼主 | 2017-7-2 11:16 | 只看该作者
本帖最后由 jianhong_wu 于 2017-7-2 11:30 编辑

第七十六节: 二维数组的指针。

【76.1   二维数组指针的用途。】

       前面章节讲了一维指针操作二维数组,本质是通过“类型强制转换”实现的,这种应用局限于某些特定的场合,毕竟一维有1个下标,二维有2个下标,一维和二维在队形感上是有明显差别的,强行用一维指针操作二维数组会破坏了代码原有的队形感,大多数的情况,还是用二维指针操作二维数组。
       二维指针主要应用在两个方面,一方面是N个二维数组的“中转站”应用,另一方面是函数接口的应用。比如,当某项目有N个二维数组表格时,要通过某个变量来切换处理某个特定的表格,以便实现“N选一”的功能,此时,二维指针在这N个二维数组之间就起到中转站的作用。又,当某个函数接口想输入或者输出一个二维数组时,就必然要用到二维指针作为函数的接口参数。

       其余段落的内容请下载附件直接看完整的pdf或者wps版本。
第七十六节.zip (103.51 KB)

使用特权

评论回复
评论
a15851404012 2018-3-12 17:55 回复TA
for(L=0;L<3;L++) //列循环 { SaveBuffer[R][L]=pTable[R][L]; //这里能看到,二维指针维护了二维数组的队形感 } 这里是否可以直接写成SaveBuffer=pTable; 
286
jianhong_wu|  楼主 | 2017-7-9 09:22 | 只看该作者
第七十七节: 指针唯一的“单向”输出通道return。
第七十七节_pdf文件.pdf (70.74 KB)
【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)[3],unsigned char u8RowSec);  //函数声明

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

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

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

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



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

开始...

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

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

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


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

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

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

使用特权

评论回复
287
jianhong_wu|  楼主 | 2017-7-16 12:41 | 只看该作者
第七十八节: typedef和#define和enum。
第七十八节_pdf文件.pdf (92.03 KB)
【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_GOOD  90  //宏定义。用MEDIUM_GOOD来表示“中”和“优”分数的分界线

typedef unsigned  char 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_GOOD  90  //宏定义。用MEDIUM_GOOD来表示“良”和“优”分数的分界线

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

       赏析片段二:
typedef unsigned  char u8;  //用typedef为类型“unsigned char”增加一个名为“u8”的代言人

       用类型定义typedef为类型“unsigned char”增加一个名为“u8”的代言人,u代表unsigned的u,8代表此类型占用8位,比如unsigned  char就是占用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 unsigned  char     u8; 
typedef unsigned  int      u16;
typedef unsigned  long     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_GOOD  90  //宏定义。用MEDIUM_GOOD来表示“良”和“优”分数的分界线

typedef unsigned  char 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_GOOD  90  //宏定义。用MEDIUM_GOOD来表示“良”和“优”分数的分界线

typedef unsigned  char 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学习板,通过电脑端的串口助手软件就可以观察到不同的变量数值,详细方法请看第十一节内容。

使用特权

评论回复
288
jianhong_wu|  楼主 | 2017-7-23 10:29 | 只看该作者
第七十九节: 各种变量常量的命名规范。
第七十九节_pdf文件.pdf (81.78 KB)
【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 int  ESu16Number;    //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 int  Gu16NumberBuffer[5];  //后缀是Buffer。16位的全局变量数组。用在普通数组。
unsigned char Gu8NumberString[5];   //后缀是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[5];  //后缀是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 char  u8Sign;  //符号  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是专业用名词,代表“静电释放”的意思。用下划线“_”断句。


使用特权

评论回复
289
liux1600111| | 2017-7-23 20:05 | 只看该作者
好,学习了!

使用特权

评论回复
290
liux1600111| | 2017-7-23 20:05 | 只看该作者
持续关注中

使用特权

评论回复
291
jianhong_wu|  楼主 | 2017-7-30 11:37 | 只看该作者
第八十节: 单片机IO口驱动LED。
第八十节_pdf文件.pdf (135.81 KB)
【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灯反而被点亮)。

使用特权

评论回复
292
dingbo95| | 2017-7-30 20:27 | 只看该作者
厉害了,这么多字很辛苦吧。

使用特权

评论回复
293
jianhong_wu|  楼主 | 2017-8-6 11:39 | 只看该作者
第八十一节: 时间和速度的起源(指令周期和晶振频率)。
第八十一节_pdf文件.pdf (120.48 KB)
【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”的关系。

使用特权

评论回复
294
执柯伐柯| | 2017-8-10 14:49 | 只看该作者
谢谢楼主的分享,很受用,谢谢

使用特权

评论回复
295
海布里| | 2017-8-11 11:57 | 只看该作者
对初学者是一个很好的帮助

使用特权

评论回复
296
jianhong_wu|  楼主 | 2017-8-13 12:27 | 只看该作者
第八十二节: Delay阻塞延时控制LED闪烁。
第八十二节_pdf文件.pdf (98.45 KB)
【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)。如果是unsigned  char变量,最大可以输入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这个时间不是绝对的,只是一个经验值。

使用特权

评论回复
297
左手笔右手刀| | 2017-8-14 13:41 | 只看该作者
666,楼主加油

使用特权

评论回复
298
jianhong_wu|  楼主 | 2017-8-20 09:47 | 只看该作者
第八十三节: 累计主循环的“非阻塞”延时控制LED闪烁。
第八十三节_pdf文件.pdf (93.17 KB)
【83.1   累计主循环的“非阻塞”。】

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

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

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

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

#define  CYCLE_SUM   5000   //累计主循环次数的设定阀值,该值决定了LED闪烁频率

sbit P0_0=P0^0;  //利用sbit和符号“^”的组合,把变量名字P0_0与P0.0引脚关联起来

unsigned char Gu8CycleStep=0;  //switch的跳转步骤
unsigned long Gu32CycleCnt=0;  //累计主循环的计数器

void main()
{
   while(1)
   {  
       switch(Gu8CycleStep)
       {
           case 0:
               Gu32CycleCnt++;   //这里就是累计main函数内部的主循环while(1)的次数
               if(Gu32CycleCnt>=CYCLE_SUM) //累计的次数达到设定值CYCLE_SUM就跳到下一步骤
{
Gu32CycleCnt=0;     //及时清零计数器,为下一步骤的新一轮计数准备
                    P0_0=0;  //LED灯亮。
Gu8CycleStep=1;  //跳到下一步骤
}
               break;

           case 1:
               Gu32CycleCnt++;   //这里就是累计main函数内部的主循环while(1)的次数
               if(Gu32CycleCnt>=CYCLE_SUM) //累计的次数达到设定值CYCLE_SUM就返回上一步骤
{
Gu32CycleCnt=0;     //及时清零计数器,为返回上一步骤的新一轮计数准备
                    P0_0=1;  //LED灯灭。
Gu8CycleStep=0;  //返回到上一个步骤
}
               break;
}
   }
}

【83.3   累计主循环的不足。】

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

使用特权

评论回复
299
arima| | 2017-8-20 23:28 | 只看该作者
楼主加油!!!**就是胜利....

使用特权

评论回复
300
jianhong_wu|  楼主 | 2017-8-27 21:53 | 只看该作者
第八十四节: 中断与中断函数。
第八十四节_pdf文件.pdf (80.44 KB)
【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提到“单片机硬件自动”这个概念,但是说它“硬件自动”并不意味着它不可控。单片机本身出厂的时候内部就携带了很多种类的中断,这些中断是否开启取决于你的“配置中断”代码,你要开启或者关闭某类中断,只需编写对应的“配置中断”代码就可以,而“配置中断”的代码本质就是填写某些寄存器数值。

使用特权

评论回复
发新帖 本帖赏金 72.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则