嵌入式开发常用位运算操作

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

48字节

由于运行平台不一样,不同数据类型占用字节数也不尽相同。

一般来说在电脑端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~(2^{8}-1)[0-255],int8范围-2^{7}~(2^{7}-1)[-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,和数组一样)

STM32的一个寄存器

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);
C语言运算符优先级表

此处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

2^{i}

K & ( 2^{i}-1)

测试如下:

这种算法会比普通求余快很多,有需求可以使用。

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;
}

  • 15
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值