【C语言学习笔记】精讲篇2 - 位操作符在嵌入式中的应用

在STM32中操作寄存器,一般都是向这些内部外设的寄存器写入一些特定的值来操控这个内部外设,进而操控硬件动作。
即:读写寄存器就是操控硬件

寄存器的特点是按位进行规划和使用。但是寄存器的读写却是整体32位一起进行的。也就是说你只想修改bit5~bit7是不行的,必须整体32bit全部写入。

因此,寄存器操作要求就是:在设定特定位时不能影响其他位。关于位操作的基本概念可以参考我之前的一篇文章:

【C语言学习笔记】精讲篇1 - 位操作符的基本概念

1、位与、位或、位异或在操作寄存器时的特殊作用

如何做到在设定特定位时不能影响其他位呢?
答:读——改——写三部曲

操作理念就是:
当我想改变一个寄存器中某些特定位时,我不会直接去给他写,我会先读出寄存器整体原来的值,然后在这个基础上修改我想要修改的特定位,再将修改后的值整体写入寄存器。这样达到的效果是:在不影响其他位原来值的情况下,我关心的位的值已经被修改了。

1.1、特定位清零用位与 &

回顾【C语言学习笔记】精讲篇1 - 位操作符的基本概念

  1. 位与操作的特点:
    (任何数,其实就是1或者0)与1位与无变化,与0位与变成0
  2. 如果希望将一个寄存器的某些特定位变成0而不影响其他位,可以构造一个合适的1和0组成的数和这个寄存器原来的值进行位与操作,就可以将特定位清零。
  3. 举例:
    假设原来32位寄存器中的值为:0xAAAAAAAA,我们希望将bit8~bit15清零而其他位不变,可以将这个数与0xFFFF00FF进行位与即可。
#include 

int main(void)
{
	unsigned int a = 0x12aaaaa7;
	unsigned int b = 0xFFFF00FF;
	unsigned int c;
	
	c = a & b;
	printf("a & b = 0x%x.\n", c);		//a & b = 0x12aa00a7.
	
	return 0;
}

1.2、特定位置1用位或 |

  1. 位或操作的特点:
    任何数,其实就是1或者0)与1位或变成1,与0位或无变化
  2. 操作手法和刚才讲的位与是类似的。我们要构造这样一个数:要置1的特定位为1,其他位为0,然后将这个数与原来的数进行位或即可。
#include 

int main(void)
{
//把一个寄存器值的bit4~bit7置1,其他位不变
	unsigned int a = 0x123d0cd7;
	unsigned int b = 0xf0;
	unsigned int c;
	
	c = a | b;
	printf("a & b = 0x%x.\n", c);

	return 0;
}

1.3、特定位取反用位异或 ^

  1. 位异或操作的特点:
    (任何数,其实就是1或者0)与1位异或会取反,与0位异或无变化
  2. 操作手法和刚才讲的位与是类似的。我们要构造这样一个数:要取反的特定位为1,其他位为0,然后将这个数与原来的数进行位异或即可。
#include 

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

	return 0;
}

2、用位运算构建特定二进制数

从上节可知,对寄存器特定位进行置1或者清0或者取反,关键性的难点在于要事先构建一个特别的数,这个数和原来的值进行位与、位或、位异或等操作,即可达到我们对寄存器操作的要求。

2.1、使用移位获取特定位为1的二进制数

  • 最简单的就是用移位来获取一个特定位为1的二进制数。
    譬如:我们需要一个bit3~bit7为1(隐含意思就是其他位全部为0)的二进制数,可以这样:(0x1f<<3)
  • 更难一点的要求:获取bit3~bit7为1,同时bit23~bit25为1,其余位为0的数:
    ((0x1f<<3) |(7<<23))
#include 

int main(void)
{
	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;
}

2.2、位取反获取特定位为0的二进制数

  • 这次我们要获取bit4~bit10为0,其余位全部为1的数。怎么做?
  • 利用上面讲的方法就可以:(0xf<<0)|(0x1fffff<<11)
  • 但是问题是:连续为1的位数太多了,这个数字本身就很难构造,所以这种方法的优势损失掉了。
  • 这种特定位(比较少)为0而其余位(大部分)为1的数,不适合用很多个连续1左移的方式来构造,适合左移加位取反的方式来构造。
  • 思路是:先试图构造出这个数的位相反数,再取反得到这个数。(譬如本例中要构造的数bit4~bit10为0其余位为1,那我们就先构造一个bit4~bit10为1,其余位为0的数,然后对这个数按位取反即可)
#include 

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);
	*/
	return 0;
}

2.2、位运算构建特定二进制数总结

  1. 如果你要的这个数比较少位为1,大部分位为0,则可以通过连续很多个1左移n位得到。
  2. 如果你想要的数是比较少位为0,大部分位为1,则可以通过先构建其位反数,然后再位取反来得到。
  3. 如果你想要的数中连续1(连续0)的部分不止1个,那么可以通过多段分别构造,然后再彼此位与即可。这时候因为参与位或运算的各个数为1的位是不重复的,所以这时候的位或其实相当于几个数的叠加。

3、位运算实战演练 - 基础巩固

  1. 给定一个整型数a,设置a的bit3,保证其他位不变。
		a = a | (1<<3)	//或者 a |= (1<<3)
  1. 给定一个整形数a,设置a的bit3~bit7,保持其他位不变。
		a = a | (0b11111<<3)	或者 a |= (0x1f<<3);
  1. 给定一个整型数a,清除a的bit15,保证其他位不变。
		a = a & (~(1<<15));		或者 a &= (~(1<<15));
  1. 给定一个整形数a,清除a的bit15~bit23,保持其他位不变。
		a = a & (~(0x1ff<<15));		或者 a &= (~(0x1ff<<15));
  1. 给定一个整形数a,取出a的bit3~bit8。
    思路:
    第一步:先将这个数bit3~bit8不变,其余位全部清零。
    第二步,再将其右移3位得到结果。
    第三步,想明白了上面的2步算法,再将其转为C语言实现即可。
	a &= (0x3f<<3);
	a >>= 3;
  1. 用C语言给一个寄存器的bit7~bit17赋值937(其余位不受影响)。
    关键点:
    第一,不能影响其他位;
    第二,你并不知道原来bit7~bit17中装的值。
    思路:
    第一步,先将bit7~bit17全部清零,当然不能影响其他位。
    第二步,再将937写入bit7~bit17即可,当然不能影响其他位。
	  a &= ~(0x7ff<<7);
	  a |= (937<<7);

4、位运算实战演练 - 提高

  1. 用C语言将一个寄存器的bit7~bit17中的值加17(其余位不受影响)。
    关键点:不知道原来的值是多少
    思路:第一步,先读出原来bit7~bit17的值
    第二步,给这个值加17
    第三步,将bit7~bit17清零
    第四步,将第二步算出来的值写入bit7~bit17
#include 

int main(void)
{
	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);
	
	return 0;
}
  1. 用C语言给一个寄存器的bit7~bit17赋值937,同时给bit21~bit25赋值17.
    思路:1的升级版,两倍的1中的代码即可解决。
    分析:这样做也可以,但是效果不够高,我们有更优的解法就是合两步为一步。
    第一种解法:直接两倍的1中的代码
#include 

int main(void)
{
	// 第一种解法:直接double第1题。
	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
	
	return 0;
}

第二种解法:更优的解法

#include 

int main(void)
{
	// 第二种解法
	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
	
	return 0;
}

5、用宏定义来完成位运算

5.1、直接用宏来置位、复位(最右边为第1位)。

//用宏定义将32位数x的第n位(右边起算,也就是bit0算第1位)置位
#define     SET_NTH_BIT(x, n)       (x | ((1U)<<(n-1)))
// 用宏定义将32位数x的第n位(右边起算,也就是bit0算第1位)清零
#define     CLEAR_NTH_BIT(x, n)     (x & ~((1U)<<(n-1)))

5.2、截取变量的部分连续位。

例如:变量0x88, 也就是10001000b,若截取第2~4位,则值为:100b = 4

#define     GETBITS(x, n, m)     ((x & ~(~(0U)<<(m-n+1))<<(n-1)) >> (n-1)) 

分析:这个题目相当于是要把x的bit(n-1)到bit(m-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坨到底应该先左边取反再右边<<,还是先右边<<再左边取反。~的优先级比<<要高,因此

~(~(0U)<<(m-n+1))		<<		(n-1)

再以此类推分析剩余的部分。

位操作符在嵌入式中的应用至此完结!5.2部分可能理解起来有些困难,可以多写写位操作相关的代码,再分析这个复杂的宏定义。

结束

欢迎大家关注我的微信公众号:redeemer奇
一起交流!一起努力!

软件下载 | 学习视频 | 嵌入式书籍 | 项目资料 |公众号中持续更新…

redeemer

你可能感兴趣的:(#,C语言,嵌入式,编程语言)