位操作符
-
位与&
-
注意:位与符号是一个&,两个&&是逻辑与。
-
真值表:1&0=0 1&1=1 0&0=0 0&1=0
-
从真值表可以看出:位与操作的特点是,只有1和1位于结果为1,其余全是0.
-
位与和逻辑与的区别:
位与时两个操作数是按照二进制位彼次对应位相与的,逻辑与是两个操作数作为整体来相与的。(举例:0xAA&0xF0=0xA0, 0xAA && 0xF0=1)
-
-
位或|
-
注意:位或符号是一个|,两个||是逻辑或。
-
真值表:1|0=1 1|1=1 0|0=0 0|1=1
-
从真值表可以看出:位或操作的特点是:只有2个0相位或才能得到0,只要有1个1结果就一定是1.
-
位或和逻辑或的区别:
位或时两个操作数是按照二进制位彼次对应位相与的,逻辑或是两个操作数作为整体来相或的。
-
-
位取反~
- 注意:C语言中位取反是~,C语言中的逻辑取反是!
- 按位取反是将操作数的二进制位逐个按位取反(1变成0,0变成1);而逻辑取反是真变成假、假变成真。
在C语言中只要不是0的任何数都是真
在C语言中只有0表示假
任何非0的数被按逻辑取反再取反就会得到1;
任何非0的数倍按位取反再取反就会得到他自己;
-
位异或^
-
位异或真值表:1^1=0 0^0=0 1^0=1 0^1=1
-
位异或的特点:2个数如果相等结果为0,不等结果为1。记忆方法:异或就是相异就或操作起来。
先同为0,不同为1
-
-
位与、位或、位异或的特点总结:
-
位与:与1位与无变化,与0位与变成0
-
位或:与1位或变成1,与0位或无变化
-
位异或:与1位异或会取反,与0位异或无变化
-
-
左移位<< 与 右移位>>
C语言的移位要取决于数据类型。
对于无符号数,左移时右侧补0(相当于逻辑移位)
对于无符号数,右移时左侧补0(相当于逻辑移位)
对于有符号数,左移时右侧补0(叫算术移位,相当于逻辑移位)
对于有符号数,右移时左侧补符号位(如果正数就补0,负数就补1,叫算术移位)嵌入式中研究的移位,以及使用的移位都是无符号数
// 一些演示程序
#include <stdio.h>
int main(void)
{
/*
* 45:101101
* 23:010111
* 58:111010 ^操作
*/
// 位异或
unsigned int a = 45, b = 23;
unsigned int c;
c = a ^ b; // 58
printf("c = %d.\n", c);
/*
// 按位和按逻辑取反再取反
unsigned int a = 45;
unsigned int b, c;
b = ~~a; // 按位取反,逐个位操作,1变0,0变1
c = !!a; // 按逻辑取反,真变假,假变真
printf("b = %u.\n", b);
printf("c = %d.\n", c);
*/
/*
// 按位和按逻辑取反
unsigned int a = 45;
unsigned int b, c;
b = ~a; // 按位取反,逐个位操作,1变0,0变1
c = !a; // 按逻辑取反,真变假,假变真
printf("b = %u.\n", b);
printf("c = %d.\n", c);
*/
return 0;
}
位 与 位或 位异或 在操作寄存器时的特殊作用
-
寄存器操作的要求(特定位改变而不影响其他位)
-
ARM是内存与IO统一编址的,ARM中有很多内部外设,SoC中CPU通过向这些内部外设的寄存器写入一些特定的值来操控这个内部外设,进而操控硬件动作。
所以可以说:读写寄存器就是操控硬件。
-
寄存器的特点是按位进行规划和使用。
但是寄存器的读写却是整体32位一起进行的
就是说你只想修改bit5~bit7是不行的,必须整体32bit全部写入
-
寄存器操作要求就是:
在设定特定位时不能影响其他位。
-
如何做到?
答案是:读-改-写三部曲。
读改写的操作理念,就是:
当我想改变一个寄存器中某些特定位时,我不会直接去给他写,
-
我会先读出寄存器整体原来的值,
-
然后在这个基础上修改我想要修改的特定位,
-
再将修改后的值整体写入寄存器。
这样达到的效果是:在不影响其他位原来值的情况下,我关心的位的值已经被修改了。
-
-
-
特定位清零用&(和 0 位与)
回顾上节讲的位与操作的特点:(任何数,其实就是1或者0)与1位与无变化,与0位与变成0
如果希望将一个寄存器的某些特定位变成0而不影响其他位,可以构造一个合适的1和0组成的数和这个寄存器原来的值进行位与操作,就可以将特定位清零。
举例:假设原来32位寄存器中的值为:0xAAAAAAAA,我们希望将bit8~bit15清零而其他位不变,可以将这个数与0xFFFF00FF进行位与即可。
-
特定位置1用|(和 1 位或)
回顾上节讲的位或操作的特点:任何数,其实就是1或者0)与1位或变成1,与0位或无变化
操作手法和刚才讲的位与是类似的。我们要构造这样一个数:要置1的特定位为1,其他位为0,然后将这个数与原来的数进行位或即可。
-
特定位取反用^
回顾上节讲的位异或操作的特点:(任何数,其实就是1或者0)与1位异或会取反,与0位异或无变化
操作手法和刚才讲的位与是类似的。我们要构造这样一个数:要取反的特定位为1,其他位为0,然后将这个数与原来的数进行位异或即可。
// 一些演示程序
#include <stdio.h>
int main(void)
{
/*
// 把一个寄存器值的bit4~bit7取反,其他位不变
unsigned int a = 0x123d0c37;
unsigned int b = 0xf0;
unsigned int c;
c = a ^ b;
printf("a & b = 0x%x.\n", c);
*/
/*
// 把一个寄存器值的bit4~bit7置1,其他位不变
unsigned int a = 0x123d0cd7;
unsigned int b = 0xf0;
unsigned int c;
c = a | b;
printf("a & b = 0x%x.\n", c);
*/
/*
// 把一个寄存器值的bit13~21清0,其他位不变
unsigned int a = 0x123d0c57;
unsigned int b = 0xffc01fff;
unsigned int c;
c = a & b;
printf("a & b = 0x%x.\n", c); // 0xaaaa00aa
*/
/*
unsigned int a = 0x12aaaaa7;
unsigned int b = 0xFFFF00FF;
unsigned int c;
c = a & b;
printf("a & b = 0x%x.\n", c); // 0xaaaa00aa
*/
}
如何用位运算构建特定二进制数
-
寄存器位操作经常需要特定位给特定值
从上节可知,对寄存器特定位进行置1或者清0或者取反,关键性的难点在于要事先构建一个特别的数,这个数和原来的值进行位与、位或、位异或等操作,即可达到我们对寄存器操作的要求。
-
解法1:
用工具软件或者计算器或者自己大脑计算,直接给出完整的32位特定数。
优势:可以完成工作,难度也不大,操作起来也不是太麻烦。
劣势:依赖工具,而且不直观,读程序的人不容易理解。
评价:凑活能用,但是不好用,应该被更好用的方法替代。 -
解法2:
自己写代码用位操作符号(主要是移位和位取反)来构建这个特定的二进制数
-
-
使用移位获取特定位为1的二进制数
最简单的就是用移位来获取一个特定位为1的二进制数。譬如我们需要一个bit3~bit7为1(隐含意思就是其他位全部为0)的二进制数,可以这样:(0x1f<<3)
更难一点的要求:获取bit3~bit7为1,同时bit23~bit25为1,其余位为0的数:((0x1f<<3) | (7<<23))
-
再结合位取反获取特定位为0的二进制数
这次我们要获取bit4~bit10为0,其余位全部为1的数。怎么做?
利用上面讲的方法就可以:(0xf<<0)|(0x1fffff<<11)
但是问题是:连续为1的位数太多了,这个数字本身就很难构造,所以这种方法的优势损失掉了。这种特定位(比较少)为0而其余位(大部分)为1的数,不适合用很多个连续1左移的方式来构造,适合左移加位取反的方式来构造。
思路是:先试图构造出这个数的位相反数,再取反得到这个数。(譬如本例中要构造的数bit4~bit10为0其余位为1,那我们就先构造一个bit4~bit10为1,其余位为0的数,然后对这个数按位取反即可)
-
总结:位与、位或结合特定二进制数即可完成寄存器位操作需求
如果你要的这个数比较少位为1,大部分位为0,则可以通过连续很多个1左移n位得到。
如果你想要的数是比较少位为0,大部分位为1,则可以通过先构建其位反数,然后再位取反来得到。
如果你想要的数中连续1(连续0)的部分不止1个,那么可以通过多段分别构造,然后再彼此位与即可。这时候因为参与位或运算的各个数为1的位是不重复的,所以这时候的位或其实相当于几个数的叠加。
// 一些演示程序
#include <stdio.h>
int main(void)
{
// 聪明的方法
unsigned int a;
a = ~(0x7f<<4); // 0xfffff80f
printf("a = 0x%x.\n", a);
/*
// 笨方法
unsigned int a;
a = (0xf<<0)|(0x1fffff<<11); // 0xfffff80f
printf("a = 0x%x.\n", a);
*/
/*
unsigned int a;
// 下面表达式含义:位或说明这个数字由2部分组成,第一部分中左移3位说明第一部分从bit3开始,
// 第一部分数字为0x1f说明这部分有5位,所以第一部分其实就是bit3到bit7;
// 第二部分的解读方法同样的,可知第二部分其实就是bit23到bit25;
// 所以两部分结合起来,这个数的特点就是:bit3~bit7和bit23~bit25为1,其余位全部为0.
a = ((0x1f<<3) | (0x7<<23));
printf("a = 0x%x.\n", a); // 工具算出来:0x0380_00f8 程序运行:0x38000f8
*/
return 0;
}
位运算实战演练
-
给定一个整型数a,设置a的bit3,保证其他位不变。
a = a | (1<<3) 或者 a |= (1<<3)
-
给定一个整形数a,设置a的bit3~bit7,保持其他位不变。
a = a | (0b11111<<3) 或者 a |= (0x1f<<3);
-
给定一个整型数a,清除a的bit15,保证其他位不变。
a = a & (~(1<<15)); 或者 a &= (~(1<<15));
-
给定一个整形数a,清除a的bit15~bit23,保持其他位不变。
a = a & (~(0x1ff<<15)); 或者 a &= (~(0x1ff<<15));
-
给定一个整形数a,取出a的bit3~bit8。
思路:
第一步:先将这个数bit3~bit8不变,其余位全部清零。
第二步,再将其右移3位得到结果。
第三步,想明白了上面的2步算法,再将其转为C语言实现即可。
a &= (0x3f<<3);
a >>= 3; -
用C语言给一个寄存器的bit7~bit17赋值937(其余位不受影响)。
关键点:第一,不能影响其他位;第二,你并不知道原来bit7~bit17中装的值。
思路:-
第一步,先将bit7~bit17全部清零,当然不能影响其他位。
-
第二步,再将937写入bit7~bit17即可,当然不能影响其他位。
a &= ~(0x7ff<<7);
a |= (937<<7);
写入一个数据,先 清零 待被写入数据的相应位,再将要写入数据与待被写入 位或 运算
-
-
用C语言将一个寄存器的bit7~bit17中的值加17(其余位不受影响)。
关键点:不知道原来的值是多少
思路:- 第一步,先读出原来bit7~bit17的值
- 第二步,给这个值加17
- 第三步,将bit7~bit17清零
- 第四步,将第二步算出来的值写入bit7~bit17
unsigned int a = 0xc30288f8; // 0xc34648f8 //第一步,先读出原来bit7~bit17的值 unsigned int tmp = 0; tmp = a & (0x3ff<<7); //printf("befor shift, tmp = 0x%x.\n", tmp); tmp >>= 7; //printf("after shift, tmp = 0x%x.\n", tmp); //第二步,给这个值加17 tmp += 0; // 怎么加??? //第三步,将a的bit7~bit17清零 a &= ~(0x3ff<<7); //第四步,将第二步算出来的值写入bit7~bit17 a |= tmp<<7; printf("a = 0x%x.\n", a);
-
用C语言给一个寄存器的bit7~bit17赋值937,同时给bit21~bit25赋值17.
思路:4.2.4.6的升级版,两倍的4.2.4.6中的代码即可解决。
分析:这样做也可以,但是效果不够高,我们有更优的解法就是合两步为一步。// 第一种解法:直接double第6题。 unsigned int a = 0xc30288f8; // bit7~bit17赋值937 a &= ~(0x3ff<<7); // bit7~ bit17清零 a |= 937<<7; // bit21~bit25赋值17 a &= ~(0x1f<<21); // bit21~bit25清零 a |= 17<<21; printf("a = 0x%x.\n", a); // 0xc223d4f8 // 第二种解法 unsigned int a = 0xc30288f8; a &= ~((0x3ff<<7) | (0x1f<<21)); // bit7~bit17和bit21~bit25全清零 a |= ((937<<7) | (17<<21)); // 937和17全部赋值 printf("a = 0x%x.\n", a); // 0xc223d4f8
技术升级:用宏定义来完成位运算
-
直接用宏来置位、复位(最右边为第1位)。
#define SET_NTH_BIT(x, n) (x | ((1U)<<(n-1))) #define CLEAR_NTH_BIT(x, n) (x & ~((1U)<<(n-1)))
加 U 的意思是无符号数
-
截取变量的部分连续位。
例如:变量0x88, 也就是10001000b,若截取第2~4位,则值为:100b = 4
#define GETBITS(x, n, m) ((x & ~(~(0U)<<(m-n+1)) << (n-1)) >> (n-1))
分析:这个题目相当于我们前面实战演练第5题中做的事情,只不过要用宏来实现。
这个题目相当于是要把x的bit(n-1)到bit(m-1)取出来
复杂宏怎么分析:((x & ~(~(0U)<<(m-n+1))<<(n-1)) >> (n-1))
第一步,先分清楚这个复杂宏分为几部分:2部分
(x & ~(~(0U)<<(m-n+1))<<(n-1)) >> (n-1)
分析为什么要>>(n-1),将特定位右移取出想要的位
第二步,继续解析剩下的:又分为2部分
x & ~(~(0U)<<(m-n+1))<<(n-1)
分析为什么要&,除特定位外全清零
第三步,继续分析剩下的:
~ (~(0U)<<(m-n+1)) << (n-1)
这个分析时要搞清楚第2坨到底应该先左边取反 再 右边<< 还是 先右边<<再左边取反。
方法一,查C语言优先级表;方法二,自己实际写个代码测试。// 写代码测试 ~和 << 谁的优先级高 unsigned int a = 0xf; unsigned int b = 0; b = ~a<<4; // 如果先~结果是:0xFFFFFF00 如果先<<结果是:0xffffff0f printf("b = 0x%x.\n", b); // 结果是0xffffff00,说明~优先级高
说明这个式子应该是
~(~(0U)<<(m-n+1)) << (n-1)
,这就又分为2部分了
#include <stdio.h>
// 第一题:用宏定义将32位数x的第n位(右边起算,也就是bit0算第1位)置位
#define SET_BIT_N(x, n) (x | (1U<<(n-1)))
// 第二题:用宏定义将32位数x的第n位(右边起算,也就是bit0算第1位)清零
#define CLEAR_BIT_N(x, n) (x & ~(1U<<(n-1)))
// 第三题:用宏定义将32位数x的第n位到第m位(右边起算,也就是bit0算第1位,m是高位)置位
// 分析:加入n=3,m=6,题目就是要把bit2到bit5置位
// 我们需要一个算式来得到(m-n+1)个1
// 算法:第1步:先得到32位1: ~0U
// 第2步:将第1步得到的数右移x位即可得到(m-n+1)个1 (~0U)>>(32-(m-n+1))
#define SET_BIT_N_M(x, n, m) (x | (((~0U)>>(32-(m-n+1)))<<(n-1)))
// 第四题:截取变量x的第n到第m位
int main(void)
{
// 写代码测试 ~和 << 谁的优先级高
unsigned int a = 0xf;
unsigned int b = 0;
b = ~a<<4; // 如果先~结果是:0xFFFFFF00 如果先<<结果是:0xffffff0f
printf("b = 0x%x.\n", b); // 结果是0xffffff00,说明~优先级高
/*
unsigned int a = 0x0;
unsigned int b = 0;
b = SET_BIT_N_M(a, 5, 8);
printf("b = 0x%x.\n", b);
*/
/*
// test for CLEAR_BIT_N
unsigned int a = 0xFFFFFFFF;
unsigned int b = 0;
b = CLEAR_BIT_N(a, 4);
printf("b = 0x%x.\n", b);
*/
/*
// test for SET_BIT_N
unsigned int a = 0;
unsigned int b = 0;
b = SET_BIT_N(a, 4);
printf("b = 0x%x.\n", b);
*/
return 0;
}