使用C语言开发8051单片机程序时,除了要控制好各I/O端口外,还要掌握8051单片机的外部中断、定时/计数器及串口通信等程序设计。定时/计数器及串口功能模块既可以工作于中断方式,也可以工作于非中断方式。图1-13给出了8051单片机中断系统结构。
图1-13 8051单片机中断系统结构
8051单片机有以下5个中断源。
· 外部中断请求0:从
(P3.2)引脚输入,由TCON寄存器IT0位设置其触发方式。
· 外部中断请求1:从
(P3.3)引脚输入,由TCON寄存器IT1位设置其触发方式。
· 片内定时/计数器0(TIMER0,简记为T0或T/C0)溢出中断请求。
· 片内定时/计数器1(TIMER1,简记为T1或T/C1)溢出中断请求。
· 片内串口收/发中断请求。
这5个中断源的中断号分别是0、2、1、3、4,用C语言编写中断程序时,中断函数名后要添加“interrupt n”,其中n为中断号。
8051单片机仅支持两级中断优先级,允许高优先级中断屏蔽低优先级中断,不允许新到达的中断屏蔽同级或低级中断。中断优先级由中断优先级寄存器(Interrupt Priority,IP)管理。由图1-13可知,将IP寄存器中的PS、PT1、PX1、PT0、PX0位设置为“1”或“0”,就可将每个中断源设置为高优先级或低优先级。如果通过IP寄存器同时配置了几个高优先级和几个低优先级中断,那么同属于高优先级的若干中断与同属于低优先级的若干中断将分别按照内部“自然优先级顺序”查询逻辑确定执行顺序。8051单片机的自然优先级顺序为
有关外部中断、定时/计数器及串口应用的多个特殊功能寄存器支持按位寻址。在程序设计中,既可以给这些特殊功能寄存器直接赋值,也可以对其中的相应位赋值。例如,某程序同时允许INT0和T0中断,可有如下代码:
或者写成
标准8051单片机寄存器头文件reg51.h已提供了它们的位定义,例如:
这些位定义将在C语言程序设计中被大量使用,对其含义及用途要熟练掌握。
下面再来看一下STC15中断请求源类型。STC15中断源汇总如表1-5所示,中断系统结构如图1-14所示。本书重点应用的STC15W4K32S4单片机支持所有这21个中断源,除INT2、INT3、T2、T3、T4、串口3、串口4中断及比较器中断固定为最低优先级中断外,其他中断均具有两个中断优先级。
表1-5 STC15中断源汇总
注:√表示对应的系列有相应的中断源。
图1-14 STC15中断系统结构
STC15中断触发条件如表1-6所示。STC15中断设置信息如表1-7所示。
表1-6 STC15中断触发条件
表1-7 STC15中断设置信息
Keil C51的数据类型如表1-8所示。大量案例中会使用到无符号数,对于255以内的整数,可定义为u8类型(相当于字节类型BYTE);对于0~65 535范围内的整数,可定义为u16类型(相当于字类型WORD)。涉及正负数(有符号数)处理的案例,例如,温度控制程序中有零上温度与零下温度,由于其温度传感器实际上可处理范围为温55~125℃,为使程序对温度值进行正确比较,程序中将温度数据类型定义为char类型(Keil C默认char为signed char类型,即有符号字符型,其取值范围为其128~127)。
表1-8 Keil C51的数据类型
另外,大量案例用到的延时程序中可能有u8类型,也可能有u16类型。编写C程序时,例如,给某延时参数x赋值2000(0x07D0),如果参数x定义为u8类型而不是u16类型,编译时并不会报错,但x实际所获得的值将为0x07D0的低字节0xD0(208),编写的语句为x=2000,而实际上x=208。这一点在单片机C语言程序设计过程中要特别注意。
大量设计涉及用数码管显示整数或浮点数,这就要对显示数据进行数位分解,例如:
又如:
如果要得到x的各个数位,可以先将x乘以100,然后再分解各数位:
上面for循环中的循环条件本来要写成i>=0,但当i=0时,如果将i再减1,i变为0xFF,这个无符号数仍被认为大于或等于0,这样就不能保证5次循环了,因此要改写成i !=0xFF。如果将i定义成char类型而不是u8类型,使用i>=0时才能得到正确结果,这是因为前面已提到Keil C默认char为signed char类型。
上述数位分解常用于数码管数字显示,这是因为数码管显示时要根据各数位提取数码管段码。
在设计一般的C语言程序时,位操作较少被使用,但在单片机应用系统设计过程中,位操作将被大量使用。在有关发光二极管(LED)流水灯、数码管位扫描控制、串行收/发信息、键盘扫描等大量案例中,位的各相关操作符及相关函数均会频繁出现。因此,要熟练掌握字节位循环左移函数(_crol_)、字节位循环右移函数(_cror_)、位左移(<<)、位右移(>>)、与(&)、或(|)、取反(~)、异或(^)、非(!)等。要注意,对于单个位,非(!)操作与取反(~)操作是等价的;但对于单个位以外的其他类型或定义,其操作则不等价。
下面是有关位操作的几个简单应用。
例如,将P1的P1.7~P1.0引脚逐个循环轮流置1,可先设字节变量c=0x01,然后在循环语句中执行c=_cror_(c,1),并使P1=c,这样即可使P1依次为10000000,01000000,00100000,…, 00000001,如此重复。如果要将P1.7~P1.0引脚逐个循环轮流置0,可先设c=0xFE,然后使用同样的循环移位操作即可。对于这两项操作,如果已经有循环语句及控制变量i(取值为0~7),还可以有P1=0x80>>i及P1=~(0x80>>i)语句。这类位操作在LED流水灯或集成式数码管位扫描中时常会被用到。
又如,已知P2.3连接外部LED或蜂鸣器,如果要使LED闪烁或蜂鸣器发声,可先定义sbit LED=P2^3或sbit BEEP=P2^3,然后在循环语句中执行语句LED=~LED或BEEP=~BEEP即可。对于独立的位定义,执行取反(~)与非(!)操作效果是一样的,但对于字节变量则是不同的。对于这里的LED闪烁或蜂鸣器发声操作,可以使用等效语句LED=!LED及BEEP=!BEEP。
如果熟悉“异或”操作符,还可以用P2 ^=(1<<3)这样的写法。这个写法可以省略位定义。
另外,在本书4×4键盘矩阵扫描程序中,假设P1端口高4位引脚连接矩阵行,低4位引脚连接矩阵列。为判断16个按键中是否有键被按下,通常会先在矩阵行上发送4位扫描码0000,即P1=0x0F,然后检查矩阵列上是否出现0。这时,使用位操作语句:
由于“!=”的优先级高于“&”,因此要给该语句中的“与操作”表达式加括号。
在本书涉及的多个字符液晶显示器案例中,当向连接在P0端口的液晶屏发送显示数据时,要先判断液晶屏是否忙。因此,又有类似语句: