位运算

负数

        左移:低位补 0

        右移:高位补 1

        左移数大于变量位数,都为 0

        右移数大于变量位数,都为 1

正数

        左移:低位补 0

        右移:高位补 0

        左移数大于变量位数,都为 0

        右移数大于变量位数,都为 0

测试平台:STM32F104RGT6

l  按位与运算符:两位同时为1,结果才为1,否则为0

l  按位或运算符:两位中有一个为1,结果就为1

l  异或运算符:两位值不同,结果为1,否则为0

l  取反运算符:将0110,就是反着来

l  << 左移运算符:各二进制位全部左移若干位,左边丢弃,右边补0

l  >> 右移运算符:各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃

两个不同长度的数据进行位运算时,系统会将二者按右端对齐,然后进行位运算。短的那个数据如果是负数,左边补1,否则补0

现在看看使用位运算的一些使用技巧。

对于刚学习开发的人来说,最常使用的位运算应该是 &  | ,对于经常和 I/O 打交道的嵌入式开发人员来说,它们确实给我们的操作带极大的方便,那么它们有什么实际应用呢?

用的最多的就是I/O操作,以它为例进行说明。

对于单片机来说,有很多端口,如ABCD……每个端口中又分为很多位,对于八位单片机来说,每个端口是8 bit,即有8个引脚,对于stm32来说,每个端口是16 bit,即有16个引脚。每个引脚有两种电平状态:高电平或低电平(所谓高低电平就如名字一样,电的水平,超过了一个水平线就是高电平,低于这个水平线就是低电平,所以说数字电路比模拟电路简单,并且容错性更好,只要在一个电压范围内都可以认为是一个电平状态,不容易因为多一点电压少一点电压得到的结果就不一样)。一般来说,认为高电平寄存器的状态位为1,低电平寄存器的状态位为0,其实从单片机开发的角度来看就是寄存器的值。比如端口A,它对应的寄存器的地址是0x20000004(随便写的一个地址,实际地址需要看单片机对应的手册查找),假设这个端口A16个引脚,也就是说需要一个16 bit空间进行表示。如下

现在可以看到每一个引脚的电平状态,bit0高电平,bit1低电平,bit2高电平……如果我们需要改变一个bit或多个bit,应该如何改变呢?对于51单片机来说,它可以直接对某一个bit操作,那么只需要找到某一个引脚地址并赋值即可,而stm32单片机有一种称之为位带操作的方法也可以对其中的某一个位进行操作。但是对于那些不能使用这些方法的单片机来说又当如何呢?我们不可能因为这个单片机没有位操作而换一种单片机开发。这个时候就可以使用 &  | 了。

bit00

*(short unsignedint*)(0x20000004) &= (~(short unsigned int)0x01);

bit11

*(short unsignedint*)(0x20000004) |= (short unsigned int)(0x02);

看到这里希望各位同学别一脸懵逼啊(虽然当时我也是如此,囧)。其实只要理解了其中的含义,就很容易写出这样的表达式了,也很容易看懂。借此稍微讲讲指针的知识,该部分会在指针小节中进行比较详细的说明。

首先将0x20000004转化为指针,因为编译器可不知道0x20000004它是一个地址,而可能会认为这是个值,所以我们要告诉编译器,别犯浑,这是一个地址,是一个指针,别弄错了。现在已经确定了这是一个地址,现在往这个地址赋值,指针的赋值就用*,这样编译器就知道应该讲C语言该如何编译成汇编语言了。现在看右边部分,这里使用了~,就是将0x01每一位取反。那为什么要使用0x01,而不直接使用0xfffe呢,那你说对于开发人员来说0x01更直观还是0x0xff fe更直观呢?另外再说一说为什么要使用强制转化。因为0x01在编译器看来,可以是0x01,可以是0x0001,也可以是0x0000 0001,所以一旦取反,编译器就不知道该取反成0xfe,还是0xfffe,还是0xffff fffe,这个时候编译器就会按照默认的情况进行取反了,所以这个值有可能不正确,所以必须强制转化,让编译器知道到底0x01是一个几位的值。其实所谓的强制转化在汇编语言里面是没有这个功能的,这是C语言用来告诉编译器该如何用汇编语言去实现这条C语言语句,这样汇编器就会按照你的C语言要求用对应的汇编指令去实现。

然后再看看这次的主角 & ,这里面是 &=,这里就涉及到C语言的简写了。实际上分开写应该是这样的:

*(short unsignedint*)(0x20000004) =*(short unsigned int*)(0x20000004) & (~(short unsignedint)0x01);

但是明显这样写的很长,不是很方便。(其实对于初学者来说,直接使用指针操作端口的方法也很少见,更多的是使用写好的头文件,利用头文件中定义去写代码,比如#define PORTA  *(shortunsigned int*) 类似的,然后 PORTA &= (~(shortunsigned int)0x01),这样又更简单明了了,复杂的活交给编译器就行了,并且这种写法也不会降低操作效率,直观又不降低效率,何乐而不为呢,所以你看到很多大神写的代码,移植性很强,并且修改的时候会非常方便,但是对于初学者就苦逼了,明明一个数值直接写在语句里就行了,非要跳来跳去,跳了很久才找到这个值是哪个,但是等你真正理解了这种写法的好处时,你就不会抱怨了,而是自然而然成为其中的一员,扯远了,扯回来),但是这条语句其实可以进一步拆分,可能更好理解一些:

temp =*(shortunsigned int*)(0x20000004);

*(short unsignedint*)(0x20000004) = temp & (~(short unsigned int)0x01);

这里的temp是一个short unsigned int中间变量,首先将地址0x20000004处的值存储到temp 中,再和(~(short unsigned int)0x01)位与(还有一个叫逻辑与,即 &&),之后再存储到地址0x20000004处。这样根据位与的特点,就可以将bit0清零,其它bit原来是什么就是什么。还有在说一点,为什么可以使用*(short unsigned int*)(0x20000004) =*(short unsigned int*)(0x20000004)& (~(short unsigned int)0x01);来操作,而不需要中间变量temp,其实不是说没有中间变量,而是这个中间变量对于我们来说不可见,是透明的,在相应的汇编语句里面,你可以看到,其实它使用了一个寄存器作为中间变量的。

同样的道理,通过 | 就可以对其中某一位置1,而不用担心其它位的变化。并且这个操作也可以同时对多个bit进行操作。比如对bit0bit1清零:

*(short unsignedint*)(0x20000004) &= (~(short unsigned int)0x03);

这里只是将某些位置1,但是如果要想同时让bit0 = 0bit1=1bit2=0呢?因为不知道原来的值是多少,不能根据原来的状态进行来对需要改变的位进行操作,所以就需要对bit0bit1bit3全部清零,再按照要求置位。

*(short unsignedint*)(0x20000004) &= (~(short unsigned int)0x07);

*(short unsignedint*)(0x20000004) |= ((short unsigned int)0x02);

这样就实现了要求。

说了优点,是时候说一说使用这种方法的潜在的风险了。从C语言的角度来看,这里只是一条语句,但是实际的对应汇编指令却由很多条指令组成,并且存在读取-改写-存储三个步骤,这样一旦在读取完数据后,如果某一个引脚状态发生改变,那么势必引起错误操作。比如:

读取数据之前端口引脚是0x0011,读取后保存到中间变量的值是0x0011,之后引脚状态改变,变成0x0001,那么因为中间变量存储的之前的是0x0011,通过和0x0002位或,变成了0x0013,那么通过赋值语句,端口状态就变成了0x0013,但是实际上它应该是0x0003,因为此时bit4已经变成了0,而不是之前的1。

这样就违背了通过操作不改变其它位的初衷了。所以为了防止打断这个读取-改写-存储的连续操作,最简单方法就是禁止中断。在使用这个方法时一定要慎重,普通的变量进行 & | 操作也是如此,要考虑这个变量是否除了在此处改变,还有没有可能在其他地方改变。不过还好,很多单片机都提供了类似位带的操作。

现在再看 ^ 异或操作,这个操作有什么好处呢?翻转。比如说要一个LED灯进行闪烁,此时就可以使用这个 ^ 来实现。让一个数在0101之间变化,就可以使用这个异或了。当然也可直接自加,然后通过 &0x01提取最低位即可。

声明:本内容为作者独立观点,不代表电子星球立场。未经允许不得转载。授权事宜与稿件投诉,请联系:editor@netbroad.com
觉得内容不错的朋友,别忘了一键三连哦!
赞 1
收藏 2
关注 151
成为作者 赚取收益
全部留言
0/200
成为第一个和作者交流的人吧