在标准C语言的的教材中,对于位运算的操作是基本不涉及的,但是在单片机系统的程序中,需要经常操作各类以字节为单位的寄存器,而这些寄存器通常都是以二进制中的位为控制单位的数据组合。往往一个8位寄存器中的每一位都有各自的控制对象,例如端口B的方向寄存器DDRB,如下图所示
它实际上控制着PB口的8个端口PB0-PB7的方向,也就是说它的每一位都控制一个端口的方向,如果我们要把端口PB0-PB3设置为输出口,而把PB4-PB7设置为输入口,在不用位运算符的情况下,我们可以直接使用赋值语句DDRB=0x0f来实现,这样是完全可以实现的。
但是如果出现下面的情况:在程序中PB口的8位端口的状态本来是1、3、5、7为输入。0、2、4、6为输出(即DDRB=0x55),接下来要将PB口的第1位设置为输出,其它端口的状态不变,然后又要将第2位设置为输入,其它端口的状态不变。该怎么实现?也许我们仍然可以使用赋值语句来实现,比如DDRB=0x55;接下来设置DDRB=0x57;然后再设置DDRB=0x53;首先要肯定的是,这种做法是绝对正确的。但是我们可能有没有注意到,在改变其中一位的值的时候,我们同时还要考虑其它7位的状态,并且要小心翼翼的避免不小心改变了其它位的值。
那么有没有一种方法,可以简单的实现修改某一位的状态,同时不会改变其它位的状态呢?
这就牵出了单片机C语言程序设计中的位运算的概念。
我们来看这个语句:DDRE |= (1 << PE5); 这个语句实现的功能是将PE口的第5位设置为输出口,其余口的状态不变。它是怎么实现的?首先我们来看1 << PE5这个表达式,我们前面已经介绍了,AVR各寄存器的宏定义是在头文件io.h中定义好的,我们可以直接调用,现在我们就来看看在io.h中PE5是如何定义的(在WINAVR的安装目录下查找iom64.h),我们可以看到PE口的8位分别定义如下:
/* Port E Data Register - PORTE */
#define PE7 7
#define PE6 6
#define PE5 5
#define PE4 4
#define PE3 3
#define PE2 2
#define PE1 1
#define PE0 0
可以看出,实际上PE5=5;那么1 << PE5,就很容易理解了,它的作用是把1左移5位,最后的结果按照二进制表示就是0b00100000,而DDRE |= (1 << PE5);实际上是DDRE= DDRE | (1 << PE5);首先“|”表示的是“或”操作DDRE是端口E的方向寄存器,在iom64.h中定义为:#define DDRE _SFR_IO8(0x02);它实际上是定义了一个标识符,这个标识符对应数据存储区RAM中的某个地址,这个我们暂且不去深究。我们还是回过头来看DDRE= DDRE | (1 << PE5);这句话实现的功能,这句话实际上是将寄存器DDRE中的内容(数据)跟二进制数0b00100000进行或操作,我们知道两个数的或操作的结果是:只要有一个是1,结果就是1.那么假如DDRE中本来的值是0b10001010(0x8a),它和0b00100000进行“或”操作以后的结果变成了0b10101010(0xaa)。我们可以看出,相或以后DDRE中的第5位以外的各位的值都没有改变,而第5位变成了1,我们的目的就是要将第5位设为输出口(即将第5位设置为1)。
现在我们来看一下从语言中有几种位运算符:
移位运算符:左移<<,右移>>
与运算符:&
或运算符:|
取反运算符:~
异或运算符:^
就这些了,总共只有6中位运算符。现在我们来看一下这些运算符都起什么作用;
左移运算符:表达形式为x<<n,意思是将数据x向左移动n位,在这里x和n都必须是无符号整形数据(所有的位运算符的操作对象都是无符号整形);
例如,x是一个unsigned char类型的数据(即x是一个单字节数据,共有8位),设x的初值为0x00000001,执行x<<n的操作:
n=0时,x<<n表示x左移0位,实际就是不移动,x的值不改变
n=1时,x<<n表示x左移1位,运算结果为0b00000010
n=2时,x<<n表示x左移2位,运算结果为0b00000100
...
n=7时,x<<n表示x左移7位,运算结果为0b10000000
n=8时,x<<n表示x左移8位,运算结果为0b00000000
从结果来看,当n在1-7之间取值时,运算的结果总是1依次向左移动一位,但是当n>=8以后,x的值就一直是0了,这是因为x是一个只有8位的整形变量,它的最大二进制长度是8位,当n超过8以后,移位操作的结果已经超出8位的范围了,产生了溢出现象。
右移的原理跟左移相似,只是它的运算结果跟左移刚好相反。
在单片机C语言编程中,经常使用移位操作来实现将数据乘以(左移)或除以(右移)2的n次方的乘除运算,利用移位操作实现乘除运算可以显著提高单片机的运算速度和效率。其详细原理我们可以翻阅相关的C语言书籍来进行更深了解。
“取反”、“与”、“或”、“非”运算经常用于对寄存器的某一位进行操作,
例如,使端口B的第二位输出高电平,同时不改变其余端口的状态,我们可以采用如下方法:
PORTB |= (1<<PB2),
实际上,想将一个8位寄存器的某一位设置为1,可以采用这样的语句:寄存器名(如PORTB) |= (1 << X),式中X表示第X位。
相反的,如果想将一个8位寄存器的某一位设置为0,可以采用这样的语句:寄存器名(如PORTB) &= ~(1 << X),式中X表示第X位。
写着的时候,总感觉心里清楚,但是表达不出来,本来是相结合单片机来讲解如何用C语言来开发单片机程序的,但总是没有办法将两者的结合很直观的表达出来。有些困惑!
这或许就是现在市面上相当多的讲解单片机C语言开发的书为什么总是将C语言的讲解和具体的程序设计分开来讲的原因吧。大家都没有更好的办法在讲解具体的单片机开发的同时把C语言的知识逐步融合到实例中! |