1.简介
大家都知道,计算机只认识0和1,所以任何数据在计算机中都是以0和1这种二进制的方式进行存储。位运算可以直接对二进制数进行直接操作,执行效率非常高。
在嵌入式软件开发中会经常对一些模块的寄存器进行配置,使用位运算操作可以让配置过程变得快捷方便。
1.1 常用位运算符
位操作符 | 名称 | 简介 |
& | 按位与 | 将两个数的每一位进行与运算,只有当两个位都为1时,结果才为1,否则为0。 |
| | 按位或 | 将两个数的每一位进行或运算,当两个位有一个是1,结果就是1,只有两个都是0结果才是0。 |
^ | 按位异或 | 将两个数的每一位进行异或运算,当两个位相同(都为0或都为1)时,结果为0,否则为1。 |
<< | 按位左移 | 将一个数的所有位向左移动指定的位数,右侧补0。 |
>> | 按位右移 | 将一个数的所有位向右移动指定的位数,左侧补0或补符号位(取决于所需的移位操作)。 |
~ | 按位取反 | 对一个数的每一位进行取反操作,将0变为1,将1变为0。 |
注:由于在嵌入式开发过程中较少对负数、小数进行位运算,故本文示例操作均不考虑小数,负数,一般情况下也不建议对小数,负数进行位运算。
1.2 基础知识
C语言常用整数数据类型
类型 | 占用空间 |
char | 1 字节 |
short | 2 字节 |
int | 4字节 |
long | 4或8字节 |
由于运行平台不一样,不同数据类型占用字节数也不尽相同。
一般来说在电脑端32位系统下的long为4字节,64位系统下为8字节,变量地址占用空间也不一样也是4字节或者8字节,可以利用这一点来判断电脑系统位数。(右键我的电脑-属性来查询系统位数好像更快,更方便)
具体情况可以使用下列代码测试一下。
int main()
{
long long int size1 = 0;
long long size2 = 0;
long size3 = 0;
int size4 = 0;
short size5 = 0;
char size6 = 0;
printf("long long int 占用字节数:%d\r\n",sizeof(size1));
printf("long long 占用字节数:%d\r\n",sizeof(size2));
printf("long 占用字节数:%d\r\n",sizeof(size3));//在我电脑端测试,前三个变量都占用8字节,也说明long 和 long long int 没区别
printf("int 占用字节数:%d\r\n",sizeof(size4));
printf("short 占用字节数:%d\r\n",sizeof(size5));
printf("char 占用字节数:%d\r\n",sizeof(size6));
//查看一下地址占用字节数
printf("地址占用字节数:%d\r\n",sizeof(&size6));//一般64位系统下地址为8字节,32位系统下地址4字节
return 0;
}
当然,这也不绝对。我曾经就使用过一款DSP,他的原生int就不是4字节,实际数据范围与预期不符。导致我在开发过程中形成了bug,我查了好久最后发现竟然是这个问题,也是很无奈。
在开发中建议不要使用c语言原生变量,比如int之类,很容易因为嵌入式平台不同产生问题。建议使用typedef ,根据平台不同,定义不同,便于使用,也便于程序移植。
typedef unsigned char uint8; // 无符号 8 bits
typedef unsigned short int uint16; // 无符号 16 bits
typedef unsigned int uint32; // 无符号 32 bits
typedef unsigned long long uint64; // 无符号 64 bits
typedef signed char int8; // 有符号 8 bits
typedef signed short int int16; // 有符号 16 bits
typedef signed int int32; // 有符号 32 bits
typedef signed long long int64; // 有符号 64 bits
8个bit(位)为一个字节(Byte),1Byte=8bit。
数据有无符号决定了是否去掉负号来扩大数据表示范围。
uint8范围0~[0-255],int8范围-~[-128~127]。//闭区间
在使用自定数据类型如uint8时,可以更好的掌握数据范围,防止数据溢出。也防止数据类型选的太大,造成空间浪费。
在使用uint8这种8位变量时,分为低4位,高4位。(同理,u16就分为高8和低8)
比如:
uint8 reg = 0x34;//0011 0100
^
|这是最低位,也叫第0位
这8位数据中,低4位为“右面”的一部分,也就是4,转为二进制就是0100;高4位就是3,转为二进制就是0011。最低位(第0位)是最“右边”的那个0。(寄存器一般都是从第0位开始定义,一个32位的寄存器,那么他的位号从0~31,和数组一样)
2.位运算常用操作
以下操作均对一个32位寄存器进行操作,我们将该寄存器相关定义如下:
(寄存器一般都是从第0位开始,一个32位的寄存器,那么他的位号从0~31,和数组一样)
//以下这种写法更加稳妥一点,使用了对寄存器位宽求余,防止输入溢出。
#define REG_WIDE 32//寄存器位宽
#define BIT(n) (1UL<<((n)%REG_WIDE))
//寄存器默认值如下
uint32 REG = 0x0000AAAA;//1010 1010 1010 1010
定义成这种方式也可以,没有问题。
#define BIT(n) (1UL<<(n))
uint32 REG = 0x0000AAAA;//1010 1010 1010 1010
2.1 将寄存器某一位置1
#define BIT(n) (1UL<<(n))
REG|= BIT(n);
测试代码如下:
typedef unsigned int uint32; // 无符号 32 bits
#define BIT(n) (1UL<<(n))
//寄存器默认值
uint32 REG = 0x0000AAAA; //1010 1010 1010 1010
// |
// 第2位原本是0
int main()
{
REG|= BIT(2);
printf("%X\r\n",REG);//AAAE
//1010 1010 1010 1110
// |
// 第2位原本是0,现在是1
return 0;
}
2.2 将寄存器某一位置0
#define BIT(n) (1UL<<(n))
REG&=~BIT(n);
测试代码如下:
typedef unsigned int uint32; // 无符号 32 bits
#define BIT(n) (1UL<<(n))
//寄存器默认值
uint32 REG = 0x0000AAAA; //1010 1010 1010 1010
// |
// 第5位原本是1
int main()
{
REG&=~BIT(5);
printf("%X\r\n",REG);//AA8A
//1010 1010 1000 1010
// |
//第5位原本是1,现在是0
return 0;
}
2.3 将寄存器某一位取反
#define BIT(n) (1UL<<(n))
REG^=BIT(n);
测试代码如下:
typedef unsigned int uint32; // 无符号 32 bits
#define BIT(n) (1UL<<(n))
//寄存器默认值
uint32 REG = 0x0000AAAA; //1010 1010 1010 1010
// |
// 第11位原本是1
int main()
{
REG^=BIT(11);
printf("%X\r\n",REG);//A2AA
//1010 0010 1010 1110
// |
// 第11位原本是1,现在是0
REG^=BIT(11);
printf("%X\r\n",REG);//AAAA
//1010 1010 1010 1110
// |
// 第11位原本是0,现在是1
return 0;
}
2.4 查询寄存器某一位值
#define BIT(n) (1UL<<(n))
Val = REG&BIT(n);//注意,Val == 0,表明这一位是0,Val 值为其他数表明这一位是1,Val位宽应与寄存器位宽一致
测试代码如下:
typedef unsigned char uint8; // 无符号 8 bits
typedef unsigned int uint32; // 无符号 32 bits
#define BIT(n) (1UL<<(n))
//寄存器默认值
uint32 REG = 0x0000AAAA; //1010 1010 1010 1010
// |
// 第11位是1
int main()
{
uint32 Val = 0;//这里记得开u32,因为他会将查询的那个位的值放到自己的对应的位上,如果开小了会直接变成0
Val = REG & BIT(11);
if(0 == Val)//注意,Val == 0,表明这一位是0,输出其他的数表明这一位是1
{
printf("此位是0\r\n");
}
else
{
printf("此位是1\r\n");
}
return 0;
}
还有一种写法,针对上面val的u32改进
typedef unsigned char uint8; // 无符号 8 bits
typedef unsigned int uint32; // 无符号 32 bits
#define BIT(n) (1UL<<(n))
//寄存器默认值
uint32 REG = 0x0000AAAA; //1010 1010 1010 1010
// |
// 第11位是1
int main()
{
uint8 Val = 0;
Val = (REG & BIT(11))>>11;//将11改为需要查的那个bit位即可
printf("此位是%d\r\n",Val);
return 0;
}
这是略加改进版,这样val的值只有0和1,但是代码看起来不够优雅,有更好想法的可以私聊我。
2.5 改变寄存器连续某几位值
uint8 VAL=0;
REG = (REG & 0xFFFFFF00)|(VAL<<4*0);
REG = (REG & 0xFFFFF00F)|(VAL<<4*1);
REG = (REG & 0xFFFF00FF)|(VAL<<4*2);
REG = (REG & 0xFFF00FFF)|(VAL<<4*3);
REG = (REG & 0xFF00FFFF)|(VAL<<4*4);
REG = (REG & 0xF00FFFFF)|(VAL<<4*5);
REG = (REG & 0x00FFFFFF)|(VAL<<4*6);
uint16 VAL=0;
REG = (REG & 0xFFFF0000)|(VAL<<4*0);
REG = (REG & 0xFFF0000F)|(VAL<<4*1);
REG = (REG & 0xFF0000FF)|(VAL<<4*2);
REG = (REG & 0xFF000FFF)|(VAL<<4*3);
REG = (REG & 0x0000FFFF)|(VAL<<4*4);
//这里每次偏移4位,当然可以偏移的更小,如有需要请自行修改
测试代码如下:
typedef unsigned char uint8; // 无符号 8 bits
typedef unsigned int uint32; // 无符号 32 bits
uint32 REG = 0xAAAAAAAA;
int main()
{
uint8 VAL = 0xBB;
REG = (REG & 0xFFFFFF00)|(VAL<<4*0);
printf("%X\r\n",REG);//AAAAAABB
REG = 0xAAAAAAAA;
REG = (REG & 0xFFFF00FF)|(VAL<<4*2);
printf("%X\r\n",REG);//AAAABBAA
REG = 0xAAAAAAAA;
REG = (REG & 0xFF00FFFF)|(VAL<<4*4);
printf("%X\r\n",REG);//AABBAAAA
REG = 0xAAAAAAAA;
REG = (REG & 0x00FFFFFF)|(VAL<<4*6);
printf("%X\r\n",REG);//BBAAAAAA
return 0;
}
2.6 读取寄存器连续某几位值
uint8 VAL=0;
VAL = (REG & 0x000000FF)>>4*0
VAL = (REG & 0x00000FF0)>>4*1
VAL = (REG & 0x0000FF00)>>4*2
VAL = (REG & 0x000FF000)>>4*3
VAL = (REG & 0x00FF0000)>>4*4
VAL = (REG & 0x0FF00000)>>4*5
VAL = (REG & 0xFF000000)>>4*6
uint16 VAL=0;
VAL = (REG & 0x0000FFFF)>>4*0
VAL = (REG & 0x000FFFF0)>>4*1
VAL = (REG & 0x00FFFF00)>>4*2
VAL = (REG & 0x0FFFF000)>>4*3
VAL = (REG & 0xFFFF0000)>>4*4
//这里每次偏移4位,当然可以偏移的更小,如有需要请自行修改
测试代码如下:
typedef unsigned char uint8; // 无符号 8 bits
typedef unsigned short uint16; // 无符号 16 bits
typedef unsigned int uint32; // 无符号 32 bits
uint32 REG = 0xABCDEF98;
int main()
{
uint8 VAL_8 = 0;
uint16 VAL_16 = 0;
VAL_8 = (REG & 0x000000FF)>>4*0;
printf("%x\r\n",VAL_8 );//98
VAL_8 = (REG & 0x00000FF0)>>4*1;
printf("%x\r\n",VAL_8 );//f9
VAL_16= (REG & 0x0000FFFF)>>4*0;
printf("%x\r\n",VAL_16);//ef98
VAL_16= (REG & 0x000FFFF0)>>4*1;
printf("%x\r\n",VAL_16);//def9
return 0;
}
2.7 拼接
如果A=0x12,B=0x04,我想让C=0x1204
测试代码如下:
typedef unsigned char uint8; // 无符号 8 bits
typedef unsigned short int uint16; // 无符号 16 bits
int main()
{
uint8 A = 0x12;
uint8 B = 0x04;
uint16 C = 0;
C = A<<8 | B;
printf("%x\r\n",C);//0x1204
return 0;
}
注意,这里要使用左移和或运算,不可使用+运算。
我曾在公司代码中见到左移配合+运算的写法,但是这段代码被注释掉了。代码长这样:
C = A<<8 + B;
我想了一下,貌似没什么问题。A左移8位后低8位是0,0或上任何数都是任何数,我也仿照他的样子写了我的代码,然后我写的代码死活跑不动。现象如下:
typedef unsigned char uint8; // 无符号 8 bits
typedef unsigned short int uint16; // 无符号 16 bits
int main()
{
uint8 A = 0x12;
uint8 B = 0x04;
uint16 C =0;
C = A<<8 + B;
printf("%x\r\n",C);//0x2000
return 0;
}
输出个什么玩意??
后面我查了半天才知道,+的运算优先级要高于<<左移,所以上面的代码其实是这样运行的。
C = A << (8 + B);
此处B我们设置的为0x04,那么这段代码就变成这样:
C = A << (8 + 0x04);
C = A << 12;
A有8位,还向左位移了12位,那么起码需要12+8=20位才能装得下这个数。
然而这里设置的C只有16位,那么只能舍弃最高的4位。于是A自己的0x12的高4位的1被舍弃,只剩下2,低位经过位移后补0,导致输出0x2000。
其实我们将C设的大一点,就可以看到完整的数字。
typedef unsigned char uint8; // 无符号 8 bits
typedef unsigned short int uint16; // 无符号 16 bits
typedef unsigned int uint32; // 无符号 32 bits
int main()
{
uint8 A = 0x12;
uint8 B = 0x04;
uint32 C =0;
C = A<<8 + B; //等效这样C = A << (8 + B);
printf("%x\r\n",C);//0x12000,就是A被左移了12位
return 0;
}
所以我知道那段代码为什么被注释掉了,因为他根本无法正常工作!不过既然无法正常工作,为啥不删掉?只注释掉?反正我接手后,把这段害人的注释狠狠的删掉了。
当然想让他正常工作也可以,加个括号就行。
typedef unsigned char uint8; // 无符号 8 bits
typedef unsigned short int uint16; // 无符号 16 bits
int main()
{
uint8 A = 0X12;
uint8 B = 0X04;
uint16 C =0;
C = (A<<8) + B;
printf("%x\r\n",C);//0x1204
return 0;
}
但是不建议这么干,因为不优雅,而且很容易忘掉括号,导致排查问题都不好弄。建议直接使用按位或即可。
2.8 判断奇偶
这里也比较简单,偶数的二进制末尾肯定是0(因为肯定是2的倍数),奇数末尾肯定是1。利用这判断就好。
if((num & 1) == 1)
{
//K为奇数
}
else
{
//K为偶数
}
2.9 位操作快速求余
由于二进制数的性质,有一些数的求余可以快速运算。
被除数 | 除数 | 余数 |
K | 2 | K & 1 |
K | 4 | K & 3 |
K | 8 | K & 7 |
K | 16 | K & 15 |
K | … | … |
K | K & ( ) |
测试如下:
这种算法会比普通求余快很多,有需求可以使用。
int main()
{
printf("%d\r\n",122%8); //2
printf("%d\r\n",122&7); //2
printf("%d\r\n",212%16); //4
printf("%d\r\n",212&15); //4
printf("%d\r\n",1204%32); //20
printf("%d\r\n",1204&31); //20
printf("%d\r\n",1212%64); //60
printf("%d\r\n",1212&63); //60
return 0;
}
2.10 快速乘除法
由于二进制数的性质,对于一些乘法和除法运算,我们可以使用左移或者右移进行快速计算。
A >> 1;//等效A/2;结果取整
A >> 2;//等效A/4;结果取整
A >> 3;//等效A/8;结果取整
...
A >> N; //等效A=A/(2^N),结果取整
A << 1;//等效A*2;
A << 2;//等效A*4;
A << 3;//等效A*8;
...
A << N;//等效A*2^N;
测试如下:
typedef unsigned char uint8; // 无符号 8 bits
typedef unsigned short int uint16; // 无符号 16 bits
typedef unsigned int uint32; // 无符号 32 bits
typedef unsigned long long uint64; // 无符号 64 bits
int main()
{
uint8 A = 0;
A = 255;
printf("%d\r\n",A>>1); //127
printf("%d\r\n",A/2); //127
printf("%d\r\n",A>>2); //63
printf("%d\r\n",A/4); //63
printf("%d\r\n",A>>3); //31
printf("%d\r\n",A/8); //31
printf("%d\r\n",A>>4); //15
printf("%d\r\n",A/16); //15
A = 150;
printf("%d\r\n",A<<1); //300
printf("%d\r\n",A*2); //300
printf("%d\r\n",A<<2); //600
printf("%d\r\n",A*4); //600
printf("%d\r\n",A<<3); //1200
printf("%d\r\n",A*8); //1200
printf("%d\r\n",A<<4); //2400
printf("%d\r\n",A*16); //2400
return 0;
}