关注公众号,回复“入门资料”获取单片机入门到高级开挂教程
开发板带你入门,我们带你飞
文 | 无际(微信:2777492857)
全文约2903字,阅读大约需要 10 分钟
作为一名老单片机工程师,我承认,当年刚入行的时候,最怕的就是看那些密密麻麻的寄存器定义,以及那些让人眼花缭乱的位操作。
尤其是遇到那种“明明改了寄存器,硬件就是不听话”的情况,简直想把示波器砸了!那时心里默默吐槽:这谁设计的寄存器,就不能给个明确的开关按钮吗,非要让我扭来扭去?
其实,每个单片机工程师都经历过这段“痛苦”的旅程。在第一家公司,我特别佩服那个把NXP单片机寄存器玩得溜溜转的大佬,同时又对那些藏在代码深处的位操作充满恐惧。毕竟,一个不小心,就可能让你的程序跑飞,硬件罢工。
如果你也正为位操作而苦恼,那么恭喜你,找到了组织!这篇文章不会教你背诵晦涩的位操作定义,而是会用最通俗易懂的语言,带你掌握嵌入式C语言中位操作的几种常见用法。
学完之后,你不仅能轻松应对各种硬件控制任务,还能在代码优化方面更上一层楼。告别抓耳挠腮,让你也能在位操作的世界里“横着走”!
记得有一次做消费类产品,我负责一个资源非常紧张的51单片机项目,Flash和RAM都快要爆炸了。当时,我想尽各种办法优化代码,最后发现,使用位操作可以极大地压缩数据存储空间,提高程序的运行效率。
通过巧妙地运用位操作,我成功盘下了这个项目,节省了硬件成本,还赢得了老板欢心,后面我离职了几年,又以技术入股的方式把我请回去,从此踏入更大的坑,算了,血泪史,不说也罢。。。从那以后,我就也深刻体会到,掌握位操作,真的是单片机工程师的必备技能。
1. 位操作,其实没那么可怕!
1.1 位操作的基石:二进制世界
在深入位操作之前,我们需要先回到二进制的世界。
单片机本质上就是处理二进制数据的机器,一切指令、数据,最终都会转化为0和1。所以,理解二进制是掌握位操作的基础。
举个例子,我们常说的“8位单片机”,指的是它的数据总线宽度是8位,也就是一次可以处理8个二进制位的数据。比如,0xAA(十六进制)在二进制中表示为10101010。而位操作,就是对这些二进制位进行各种各样的操作。
1.2 位与(&):提取信息的过滤器
位与操作符(&)的作用是,将两个操作数的对应位进行“与”运算。只有当两个位都为1时,结果才为1,否则为0。
uint8_t a = 0b10101010;
uint8_t b = 0b00001111;
uint8_t result = a & b; // result 的值为 0b00001010
位与操作最常见的应用场景是清除特定位和提取特定位。
•清除特定位: 假设我们需要清除一个寄存器reg的第3位(从0开始计数),我们可以使用以下代码:
reg = reg & (~(1 << 3)); // 将第3位清零
这里,(1 << 3) 会生成一个掩码0b00001000,然后取反得到0b11110111。再与reg进行位与操作,就可以将第3位清零,而其他位保持不变。
•提取特定位: 假设我们需要提取reg的第4位到第7位,可以使用以下代码:
uint8_t extracted = (reg >> 4) & 0x0F; // 提取第4位到第7位
这里,(reg >> 4) 会将reg右移4位,使得第4位到第7位移动到最低位。然后与0x0F(0b00001111)进行位与操作,就可以提取出第4位到第7位的值。
1.3 位或(|):设置信息的开关
位或操作符(|)的作用是,将两个操作数的对应位进行“或”运算。只要两个位中有一个为1,结果就为1,否则为0。
uint8_t a = 0b10101010;
uint8_t b = 0b00001111;
uint8_t result = a | b; // result 的值为 0b10101111
位或操作最常见的应用场景是设置特定位。
•设置特定位: 假设我们需要设置reg的第2位为1,可以使用以下代码:
reg = reg | (1 << 2); // 将第2位设置为1
这里,(1 << 2) 会生成一个掩码0b00000100。然后与reg进行位或操作,就可以将第2位设置为1,而其他位保持不变。
1.4 位异或(^):翻转信息的魔法棒
位异或操作符(^)的作用是,将两个操作数的对应位进行“异或”运算。当两个位不同时,结果为1,相同时为0。
uint8_t a = 0b10101010;
uint8_t b = 0b00001111;
uint8_t result = a ^ b; // result 的值为 0b10100101
位异或操作最常见的应用场景是翻转特定位和简单加密。
•翻转特定位: 假设我们需要翻转reg的第5位,可以使用以下代码:
reg = reg ^ (1 << 5); // 翻转第5位
这里,(1 << 5) 会生成一个掩码0b00100000。然后与reg进行位异或操作,就可以将第5位翻转(0变1,1变0)。
•简单加密: 位异或操作可以用于简单的加密和解密。同一个数据与同一个密钥进行两次位异或操作,就可以恢复原始数据。
uint8_t data = 0x5A;
uint8_t key = 0x3C;
uint8_t encrypted = data ^ key; // 加密
uint8_t decrypted = encrypted ^ key; // 解密,恢复为 data
1.5 位取反(~):反转世界的钥匙
位取反操作符(~)的作用是,将操作数的每一位取反。
uint8_t a = 0b10101010;
uint8_t result = ~a; // result 的值为 0b01010101
位取反操作通常用于生成掩码,配合其他位操作实现更复杂的功能。比如,前面清除特定位的例子中,我们就用到了位取反。
1.6 左移(<<)和右移(>>):移形换影的魔术
左移操作符(<<)的作用是,将操作数的每一位向左移动指定的位数,右边补0。
右移操作符(>>)的作用是,将操作数的每一位向右移动指定的位数,左边补0(无符号数)或补符号位(有符号数)。
uint8_t a = 0b00000011;
uint8_t result_left = a << 2; // result_left 的值为 0b00001100
uint8_t result_right = a >> 1; // result_right 的值为 0b00000001
左移和右移操作最常见的应用场景是**乘以或除以2的幂**、**提取特定位**和**组合数据**。
•乘以或除以2的幂: 左移n位相当于乘以2的n次方,右移n位相当于除以2的n次方。这比直接使用乘除法运算更快。
•提取特定位: 就像前面提取reg的第4位到第7位的例子。
•组合数据: 假设我们有两个8位数据,需要将它们组合成一个16位数据:
uint8_t high = 0x12;
uint8_t low = 0x34;
uint16_t combined = (high << 8) | low; // combined 的值为 0x1234
2. 实战演练:GPIO控制
说了这么多,我们来一个实战演练:使用位操作控制GPIO。
假设我们需要控制一个LED的亮灭,LED连接到GPIO的第5个引脚。
// 点亮LED
void led_on() {
GPIO_PORT |= LED_PIN; // 设置GPIO引脚为高电平
}
// 熄灭LED
void led_off() {
GPIO_PORT &= ~LED_PIN; // 设置GPIO引脚为低电平
}
// 翻转LED状态
void led_toggle() {
GPIO_PORT ^= LED_PIN; // 翻转GPIO引脚状态
}
这个例子清晰地展示了位操作在控制硬件方面的简洁和高效。
3. 注意事项:别踩这些坑!
•位宽问题: 确保操作的变量类型足够容纳所需的位数,避免数据溢出。
•符号扩展: 在对有符号数进行右移操作时,注意符号位的扩展。
•移位溢出: 移位位数不应超过变量的位宽,否则行为未定义。
•优先级: 位操作符的优先级比较低,需要注意加括号,避免运算顺序错误。
位操作是嵌入式C语言的精髓,也是单片机工程师的必备技能。掌握位操作,你就能更高效地控制硬件,更巧妙地优化代码,在单片机世界里施展你的魔法。
希望这篇文章能帮助你打开位操作的大门,让你在嵌入式开发的道路上越走越远!记住,位操作不仅是技术,更是一种思考方式,它能让你以更精巧、更高效的方式解决问题。干吧,骚年。
end
下面是更多无际原创的个人成长经历、行业经验、技术干货。
1.电子工程师是怎样的成长之路?10年5000字总结
2.如何快速看懂别人的代码和思维
3.单片机开发项目全局变量太多怎么管理?
4.C语言开发单片机为什么大多数都采用全局变量的形式?
5.单片机怎么实现模块化编程?实用程度让人发指!
6.c语言回调函数的使用及实际作用详解
7.手把手教你c语言队列实现代码,通俗易懂超详细!
8.c语言指针用法详解,通俗易懂超详细!