为什么要使用位移运算符?

5
我不理解在微控制器编程中何时以及为什么要使用位移运算符,例如...
SWITCH_DDR &= ~SWITCH_BIT;   
SWITCH_PORT |= SWITCH_BIT;

为什么要使用这些运算符?

或者...

void SerialInit(void)
{
   UBRRH = ((XTAL / (8 * 250000)) - 1)>>8;   // 250kbps at 16Mhz
   UBRRL = (XTAL / (8 * 250000)) - 1;
   UCSRA = (1<<U2X);
   UCSRB = (1<<TXEN);
   UCSRC = (1<<URSEL) + (1<<UCSZ1) + (1<<UCSZ0);
}

这里到底是怎么回事?有人可以用0和1的图形来解释一下吗?或者这里有另一个例子:

ulong MesureLC(void)
{
 int i;

 TCCR1B = 0;
 CountHigh = 0;
 TCNT1 = 0;

 for (i=0;i<25000;i++)
 {
  TCCR1B = (1<<CS12) + (1<<CS11) + (1<<CS10);   // WTF ???
  UDR = 0x55;
  while(!(UCSRA & (1<<UDRE)));
 }
 while(!(UCSRA & (1<<TXC)));
 TCCR1B = 0;

 CountLow = TCNT1;
 Count = CountLow + (CountHigh << 16);

 return Count;
}

我需要理解这个东西,任何帮助都会感激。


9
为什么不自己在纸上勾画一下呢?这并不难 - | 代表按位或,& 代表按位与,<<>> 是位移运算符。只要你理解二进制数字,这就是你需要知道的全部。 - tdammers
1
我不知道什么是“二进制数”。也许你的意思是“二进制记数法中的数字”吗? ;) - MvanGeest
2
请注意,位移运算符实际上是将位移。& 和 |(C 语法)只是按位运算符。 - Lasse V. Karlsen
2
这很可能不是作业。听起来更像是一个桌面开发人员被扔到了没有准备或指导的固件(或驱动程序)任务中。这种情况经常发生。如果这是作业,课程材料应该清楚地解释所有这些。如果这是为了工作,而你被扔到了一个超出你能力范围的任务中...你需要想办法获得所需的培训或回到桌面工作。找一些嵌入式编程书籍,买一个Arduino之类的东西。如果你在位操作方面遇到麻烦,等到你遇到中断和分页内存再说。 - darron
1
讽刺话直接飞过了你的脑袋... - R.. GitHub STOP HELPING ICE
显示剩余6条评论
8个回答

17

有许多原因可以解释为什么你要使用它们。

首先,有时候微控制器类型设备的内存很紧张,您可能希望尽可能地在较小的空间中存储尽可能多的数据。

这种需求可能还源于您要与其他设备或软件进行接口,这些设备或软件也具有相同的最小空间要求。它们可能使用子字节数据类型来执行工作,其中一个例子是TCP/IP头,其中字段范围从一个位(例如DF字段)到更多(例如IP地址)。

事实上,您提供的第二个代码片段SerialInit正在设置串行通信芯片的属性(UART =通用异步收发器)。UCSRx通常代表UART控制/状态寄存器#x,因此您正在向一个相当低级的设备写入信息以控制其行为。

另一个原因是您可能具有映射I/O内存。这意味着虽然您认为正在写入内存,但您很可能直接将这些位发送到某种I/O设备。

经典例子是使用内存映射字节控制七段LED。

LED可能结构如下:

+---+---+---+---+---+---+---+---+
| d | e | g | f | a | b | c | p |
+---+---+---+---+---+---+---+---+
  #   #   #   #   #   #   #   #
  #   #   #   #   #   #   #   #===========#
  #   #   #   #   #   #   #               #
  #   #   #   #   #   #   #===========#   #
  #   #   #   #   #   #               #   #
  #   #   #   #   #   #===========#   #   #
  #   #   #   #   #               #   #   #
  #   #   #   #   #=====#         #   #   #
  #   #   #   #         #         #   #   #
  #   #   #   #     +-------+     #   #   #
  #   #   #   #     |aaaaaaa|     #   #   #
  #   #   #   #   +-+-------+-+   #   #   #
  #   #   #   #   |f|       |b|   #   #   #
  #   #   #   #===|f|       |b|===#   #   #
  #   #   #       |f|       |b|       #   #
  #   #   #       +-+-------+-+       #   #
  #   #   #=========|ggggggg|         #   #
  #   #           +-+-------+-+       #   #
  #   #           |e|       |c|       #   #
  #   #===========|e|       |c|=======#   #
  #               |e|       |c|           #
  #               +-+-------+-+ +---+     #
  #=================|ddddddd|   |ppp|=====#
                    +-------+   +---+
其中每个七段数码管和小数点都由不同的位控制。如果您想要打开一个单独的段而保留其他段关闭,则可以使用位运算符,就像您的问题所包含的那样。
例如,打开g段涉及类似以下操作:
mmap_byte |= 0x20; // binary 00100000

关闭它需要:

mmap_byte &= 0xdf; // binary 11011111

有时候,您可能会发现一个字节中的单个位或一组位完全控制不同的设备,并且您不希望一个设备的操作影响另一个设备。


至于位运算符的作用是什么,它们是对多位值进行操作的运算符,但概念上是逐位进行的:

  • AND仅在其输入都为1时为1。
  • OR只要其输入中有一个或多个输入为1时为1。
  • XOR仅当其输入恰好有一个1时才为1。
  • NOT仅在其输入为0时为1。
  • 左移将位左移一定量。
  • 右移将位右移一定量。

暂时不考虑移位,这些可以最好地描述为真值表。输入可能性位于顶部和左侧,结果位是两个(在NOT的情况下只有一个)输入交汇处显示的四个值之一。

AND | 0 1     OR | 0 1     XOR | 0 1    NOT | 0 1
----+-----    ---+----     ----+----    ----+----
 0  | 0 0      0 | 0 1       0 | 0 1        | 1 0
 1  | 0 1      1 | 1 1       1 | 1 0

举一个例子,如果你只需要整数的低4位,你可以使用按位与运算符(&)和15(二进制1111)进行运算:

    201: 1100 1001
AND  15: 0000 1111
------------------
 IS   9  0000 1001

另一个例子是如果您有两个4位值,想将它们打包成一个8位值,您可以使用这三种运算符(左移、按位与和按位或)中的所有三种:
packed_val = ((val1 & 15) << 4) | (val2 & 15)
  • & 15 操作会确保这两个值只有低4位。
  • << 4 是一个4位左移操作,将 val1 移动到8位值的高4位。
  • | 简单地将这两个值组合在一起。

如果 val1 是7,val2 是4:

                val1            val2
                ====            ====
 & 15 (and)   xxxx-0111       xxxx-0100  & 15
 << 4 (left)  0111-0000           |
                  |               |
                  +-------+-------+
                          |
| (or)                0111-0100

5
  1. 如果您有可以启用或禁用的功能,则可以使用单个位来设置状态:1或0。这样,在1字节中,您可以设置8个功能。

  2. 如果您需要存储值(比方说从0到7),则只需要3位(二进制中的000 = 0,001 = 1,010 = 2,...,111 = 7)。而且你还有5个8位中的空余位。

这里发生了什么???请有人用0和1的图形方式解释一下

1 << 2 = 00000001 << 2 = 00000100 = 4

4 >> 2 = 00000100 >> 2 = 00000001 = 1


4
我认为真正的问题不是“这到底是什么意思”,而是“为什么写微控制器程序的开发人员似乎喜欢这种方式呢?”
我不知道他们是否喜欢这种方式。我根本不是开发者,只对微控制器有点兴趣,但以下是我的看法。
你见过芯片文档吗?这是来自Atmel pdf的一个例子。
USART控制和状态寄存器-UCSRnB •第7位-RXCIEn:RX完成中断使能n将此位写为一,可以在RXCn标志上启用中断。 只有在将RXCIEn位写为1、SREG中的全局中断标志被写为1并且UCSRnA中的RXCn位设置时, 才会生成USART接收完成中断。 •第6位-TXCIEn:TX完成中断使能n将此位写为一,可以在TXCn标志上启用中断。 只有在将TXCIEn位写为1、SREG中的全局中断标志被写为1并且UCSRnA中的TXCn位设置时, 才会生成USART传输完成中断。 ... •第5位-UDRIEn:... •第4位-RXENn:... •第3位-TXENn:... •第2位-UCSZn2:... •第1位-RXB8n:... •第0位-TXB8n:...
问题是,芯片是通过在某些控制寄存器中设置或清除单个位来控制的。 文档中这些寄存器具有丑陋的名称,位也有丑陋的名称。开发环境已经定义了与文档类似的宏名称。
现在假设一个人想要在RXCn标志上启用中断,并保留所有其他设置不变。这需要在特定寄存器中设置一个位,同时保持其他位不变。操作符| = 是最简单的方法
假设寄存器的地址为0x3F(我编的号码,无关紧要),可以写入以下内容:
*((unsigned char*)0x3F) |= 0x80;// I want to set bit number 7 (RXCIEn)

现在,这段文字的可读性如何?

在开发环境中,已经定义了像 UCSRnBRXCIEn 这样的宏变量,并且使用它们是一件显而易见的事情。恰好 RXCIEn 是一个位数,而不是该位的值,因此要编写与上面相同的代码,必须编写:

UCSRnB |= (1 << RXCIEn);

感谢文档,能让我们做到像这样:
UCSRnB = (1<<RXENn)|(1<<TXENn);

被认为比较易读

*((unsigned char*)0x3F) = 0x18; // I want to set bits number 4 and 3 (RXENn and TXENn)

文档本身充满了使用这些定义的宏的代码示例,我猜开发人员很快就习惯了它,试图寻找更好的方法。


3
在您提供的第一个示例中:
SWITCH_DDR &= ~SWITCH_BIT;   
SWITCH_PORT |= SWITCH_BIT;

这不是位移操作,而是位掩码。

具体来说,第一个语句关闭给定值中的某个位,第二个语句打开同一位,同时保留所有其他位。

假设SWITCH_DDR和SWITCH_PORT是控制某些设备行为的特殊内存值。它们的每个位都可以打开/关闭某个功能。如果您想单独控制某个功能,则必须能够更改位而不干扰其他位。 假设由SWITCH_BIT控制的功能是字节中最左边的位。因此,SWITCH_BIT将具有值0x80(二进制中的10000000)。 当您执行第一个语句时,您使用~运算符反转SWITCH_BIT(得到二进制的01111111),并对SWITCH_DDR应用二进制AND。这有效地清除了最左边的位并保留了其他位。 第二个语句执行二进制OR,因此结果相反。

现在,关于位移操作,它们有许多应用程序(在另一个答案中已经提到了很多),我将简单解释一下您发布的代码中的特定用途。所以你有:

void SerialInit(void)
{
   UBRRH = ((XTAL / (8 * 250000)) - 1)>>8;   // 250kbps at 16Mhz
   UBRRL = (XTAL / (8 * 250000)) - 1;
   UCSRA = (1<<U2X);
   UCSRB = (1<<TXEN);
   UCSRC = (1<<URSEL) + (1<<UCSZ1) + (1<<UCSZ0);
}

这里的情况与前一个例子类似(设置值中的特定位),但由于情况不同,存在一些差异:
a) 在第一个例子中,你已经制作了位掩码(SWITCH_BIT),而在这里,常量仅是位的位置。例如,U2X包含需要打开/关闭的位的位置(从右到左),而不是其位掩码。执行(1<<U2X)实际上会产生相应的位掩码。如果需要打开的位是最左边的位(如我在SWITCH_BIT示例中所做的那样),则U2X将为7,移位的结果将是相同的二进制10000000。
b) 在最后一行:
UCSRC = (1<<URSEL) + (1<<UCSZ1) + (1<<UCSZ0);

这是将每个常量产生的位掩码组合在一起(其值再次为从右侧开始的位位置)。问题在于程序员决定使用+运算符而不是二进制OR,因为他/她知道当没有“碰撞”位时,它们的结果是相同的。个人而言,我总是使用二进制OR,以明确我所做的不是算术加法。
c)最后,关于第一行
UBRRH = ((XTAL / (8 * 250000)) - 1)>>8;

看起来更加复杂,显然是一个时钟分频系数,我需要查阅文档才能更好地理解。现在不用担心它,它可能会随着时间的推移变得更加清晰。


2
看一下你的SerialInit函数。
虽然你没有提供很多细节,但仍然足够理解。显然你正在初始化串口,前几行是时钟分频器。由于芯片可以以不同的时钟运行,而且芯片通常不会猜测您想要的时钟,比如9600波特率,因此您必须进行数学计算,将12mhz参考时钟降低到250000,采用8倍过采样。我猜那个数字是0x200,但为了举例,假设该数字是0x234。 UBBRH寄存器似乎是高半部分寄存器,因此需要上位字节,显然是上8位(或更少),UBBRL是该除数的较低半部分,因此需要较低的8位。
0x0234的上8位是0x02,要从0x0234得到0x02,您需要进行移位。
0x0234 = 0b0000001000110100(其中0b表示二进制,而0x表示十六进制)
这不是旋转,而是逻辑移位,C语言中没有旋转(也没有算术移位)。这意味着我们向右移动的位数最终会落在一个位桶中。
因此,0b0000001000110100向右移动8次变为0bxxxxxxxx00000010。XX是新移入的位,在C中实际上为零。但这里并不重要,因为要写入的寄存器是8位的,因此只有较低的8位有效。
因此,在这两行代码之间,我们计算了串行时钟的除数0x234,并将0x02写入上除数寄存器,将0x34写入下除数寄存器。
接下来的一行UCSRA = (1 << U2X);
显然有一个寄存器需要设置单个位。我不知道U2X是什么,但假设它是5。1 << 5表示取0x01或0b00000001。向左移动五次意味着将存在的位向左移动,在C中在右侧带入5个零。变量顶部的位、最左侧的5位掉落到位桶中。
所以 0b00000001 添加五个零以可视化 0b0000000100000 然后从左边削掉五个 0b00100000 我们最终得到0x20。
UCSRB行的工作方式相同。
UCSRC行也是相同的,但是设置了三个位。

UCSRC = (1 << URSEL) | (1 << UCSZ1) | (1 << UCSZ0);

这里是一个关于IT技术的例子。在你提供的代码中,URSEL、UCSZ1和UCSZ0都没有被定义,所以我将使用一些数字来填充这些未定义的部分。

UCSRC = (1 << 5) | (1 << 2) | (1 << 4);

我们像处理UCSRA和UCSRB一样,对这些数字进行可视化处理。

将五个零左移
0b100000
在左侧添加零,使其成为完整的8位数
0b00100000
一个有两个零,一个有四个零
0b100加上填充后是0b00000100
0b10000加上填充后是0b00010000

到目前为止,三个组件分别是:

0b00100000
0b00000100
0b00010000

当它们相加时,结果为

0b00110100 = 0x34

这个值就是写入寄存器的值。

现在你必须小心使用加法而不是或运算。如果你没有注意到你的定义或者没有仔细查看,你可能会发现同一个位被两个名称定义,而你可能不知道它是相同的位,加法会搞砸它,或运算则不会。例如,如果URSEL和UCSZ1恰好是相同的位

UCSRC = (1 << URSEL) + (1 << UCSZ1) + (1 << UCSZ0);

UCSRC = (1 << 5) + (1 << 5) + (1 << 4);

你会得到
0b00100000
0b00100000
0b00010000
这些数字相加得到
0b01010000
而你可能希望将它们进行或运算并得到
0b00110000

还有其他情况下,或运算是不好的,你需要使用加法,所以在进行这种计算时,你必须了解你的数字而不仅仅是定义的名称。

通常为什么要进行这种位移的形式,特别是在微控制器和驱动程序中,是因为设备中的寄存器可能定义了多个东西。串口就是一个完美的例子,假设你有一个控制寄存器,它看起来像这样:

0b0SPPLLLL

其中SS是停止位,1=2个停止位,0=1个停止位 PP是奇偶校验,0b00 = 无奇偶校验,0b01 = 偶校验,0b10 = 奇校验 LLLL是长度,0b1000 = 8,0b0111 = 7位等等

你经常会发现这样的寄存器代码:

SCONTROL = (0 << 7)|(2 << 4)|(8 << 0);

除了硬编码数字被替换为定义外:

SCONTROL = (ONESTOPBIT << STOPBITS)|(NOPARITY << PARITYBITS)|(DATABITS8 << DATABITS);

移位运算允许程序员独立考虑每个字段,而不使用混乱、错误和非常糟糕(永远不要使用)的位字段。

按位与非是一种不必处理变量长度的简单方法

SWITCH_DDR &= ~SWITCH_BIT;

SWITCH_PORT |= SWITCH_BIT;

因此,如果您想读取-修改-写入某些内容,并且说低3位是要修改的内容,而不想弄乱寄存器中的其他位,您可能会做以下操作:

ra = SOMEREGISTER;
ra&=~7;
ra|=newnumber&7
SOMEREGISTER = ra;

ra&=~7 的意思是

从 
0x0000....0000111 开始
取补码,即将零变成一,将一变成零
0x1111....1111000

与 ra 中原有的值进行按位与运算,与 1 进行按位与运算表示保留原有的值,与 0 进行按位与运算则表示清零,因此,低三位被强制为 0,其他无关紧要的位不变。然后,或入新数字以将低三位设置为您想要更改的内容,然后写回到寄存器中已更改的值。

然后,使用反转掩码的 and 操作可能比自己进行数学计算更容易, ~7 而不是 0xFFFF...FF8。它的帮助在于您可能只需要一个定义:

#define SOMEMASK 0x7
然后使用 ra&=~SOMEMASK; ra|=newvalue&SOMEMASK.

您甚至可以更聪明地说:

您甚至不必考虑 0x7 是三个 1。您只需在其中放置一个 3 即可。


1

位移运算符有两种主要的变体(我不是在谈论方向):移位和旋转。

此外,它们都有两个方向,因此通常有四个:

  • 左移
  • 右移
  • 左旋转
  • 右旋转

前两个将位移一定数量的位到一个方向。任何“掉落”末尾的位都会消失。出现在另一端的任何位都为零。

通常还需要指定要移动值的位数。

所以:

1000 shl 1 = 0000 (the 1 fell off the end, and a 0 appeared on the other end)
1000 shr 1 = 0100 (a zero fell off the right end)

旋转不会丢失掉落的位,而是将它们从另一侧旋转回来。
1000 rol 1 = 0001 (the 1 was rotated back in on the other side)

你可以将这两个操作看作:

  • 对于移位,数字在两端都包含无限数量的零,随着移位而跟随值
  • 对于旋转,数字在两个方向上无限重复,随着移位而跟随值

还有一种旋转的变体,通过进位标志在过程中使用额外的位。

如果进位标志开始为0,则会发生以下情况:

1000 rcl 1 = 0000 (rcl = rotate through carry to left)
0000 rcl 1 = 0001 (now the 1 came back, it was temporarily stored in carry flag)

在机器代码中,最后一种可以将单个位从一个寄存器移动到另一个寄存器:

rcl ax, 1    ; rotate AX-register, 16-bit, left 1 bit, through carry
rcr bx, 1    ; rotate BX-register, 16-bit, right 1 bit, through carry

在这里,我们从AX中取出最左边的位,将其暂时旋转到进位标志中,然后再将其旋转回BX的最左边位。

现在,您通常可以将移位与其他按位运算符结合使用。例如,要设置值的第N位(其中N是基于0的,而第0位是最右边的一位),可以执行以下操作:

value = value OR (1 shl N)

在这里,我们首先将值1向左移动N次。 如果N为0,则根本不会转移位。

然后我们将该移位的结果与现有值进行OR运算,并存储它。 OR的效果是将1合并在一起,因此如果任一值在特定位置具有1位,则结果在该位置也是1位。

因此,对于移位:

1 shl 0 = 00000001 shl 0 = 00000001
1 shl 1 = 00000001 shl 0 = 00000010
1 shl 2 = 00000001 shl 0 = 00000100
1 shl 3 = 00000001 shl 0 = 00001000
1 shl 4 = 00000001 shl 0 = 00010000
1 shl 5 = 00000001 shl 0 = 00100000
1 shl 6 = 00000001 shl 0 = 01000000
1 shl 7 = 00000001 shl 0 = 10000000

然后是OR:

???????? OR 00100000 = ??1?????, where ? means whatever it was before

让我来看看你发布的几行代码:

UBRRH = ((XTAL / (8 * 250000)) - 1)>>8;   // 250kbps at 16Mhz
UBRRL = (XTAL / (8 * 250000)) - 1;

首先进行了一个计算,(XTAL / (8 * 250000)) - 1,我不知道它的目的是什么。但这只是普通的数学运算,因此它会计算出某些东西。根据注释,我们可以称之为频率。

这个值被计算了两次,所以让我们重新编写上述语句:

UBRRH = value >>8;   // 250kbps at 16Mhz
UBRRL = value;

在这里我必须猜测,但我猜测UBRRH和UBRRL都被声明为“BYTE”类型,这意味着它们最多可以存储8位的值。这意味着代码实际上是这样的:

  1. UBRRH获取“value”的高8位,将其移入低8位并存储。由于它只存储一个字节,因此它截断了剩余部分,这意味着它获取了8-15位
  2. UBRRL获取低8位并截断剩余部分,这意味着它获取了0-7位

由于这两个名称以LH结尾,它们符合这个假设。


XTAL很可能是一个#define,因此重复的计算将在编译时评估。您已经在其中放置了250kHz数字和8的因子...这可能意味着UART需要以比特率的8倍频率运行。这对于UART来说是非常标准的事情。 - darron

1

位运算符(按照它们的名称操作位)在位操作中使用。在您提供的第一段代码中,这些运算符用于设置和重置特定位。为简单起见,假设SWITCH_DDR是一个8位整数,并且SWITCH_BIT也是一个8位整数,其常量值为2:

SWITCH_DDR = 00000000;  // initial value of SWITCH_DDR is 0
SWITCH_BIT = 00000010;

然后,您可以使用按位或运算符将SWITCH_DDR的特定位设置为1:

SWITCH_DDR |= SWITCH_BIT; // SWITCH_DDR is 00000010 now

要验证SWITCH_BIT位是否设置,您可以使用AND运算符:

TEMP = 10101010 & SWITCH_BIT; // TEMP is 00000010 now (1 in TEMP is set only if there's 1 in both operands)
if (TEMP == SWITCH_BIT) // The condition is true
{ /* Do something */ }
TEMP = SWITCH_DDR & SWITCH_BIT;  // TEMP is again 00000010 because we set it to 00000010 before and the AND operator doesn't therefore change anything
if (TEMP == SWITCH_BIT)  // The condition is also true
{ /* Do something */ }

要取消设置特定位,可以使用以下代码:

TEMP = ~SWITCH_BIT;  // TEMP is now 11111101
SWITCH_DDR &= TEMP;  // This preserves everything (because 1 & 1 = 1 and 1 & 0 = 0) but the SWITCH_BIT bit which will be always set to 0 (anything & 0 = 0)

移位运算符只是将位向左或向右移动:

RESULT = 10010010 << 1;  // RESULT is 00100100
RESULT <<= 1;  // RESULT is 01001000
RESULT <<= 2;  // RESULT is 00100000
RESULT >>= 1;  // RESULT is 00010000

关于移位运算符有一个特别的事情 - 你可以使用它们来快速进行2的幂次方的除法/乘法:

RESULT = 3 << 1;  // Result is 6 (3 * 2)
RESULT = 5 << 2;  // Result is 20 (5 * 4)
RESULT = 1 << 7;  // Result is 128 (1 * 128)
RESULT = 36 >> 1; // Result is 18 (36 / 2)
RESULT = 35 >> 1; // Result is 17 (35 / 2)

0
值得注意的是,在许多小处理器上,有专门的指令用于设置和清除I/O端口的各个位。建议您了解将要使用的任何处理器的信息,因为最优编码样式因处理器而异。例如,请考虑以下语句组:
  some_port |= 8;  /* 语句#1a--请注意,值是2的幂 */
  some_port |= 2;  /* 语句#1b--请注意,值是2的幂 */
some_port |= 9; /* 语句#2--请注意,值不是2的幂 */
在某些处理器(例如ARM或Z80)上,如果已声明some_port为volatile(它应该是),则每个语句将是三个指令,因此第一个序列将需要两倍时间和空间。 在一些其他处理器(例如PIC)上,前两个语句将是一个指令,而第二个语句将是两个指令,因此两个序列将花费相同的时间和空间。还有其他处理器将为其中一个提供空间优势,为另一个提供时间优势。
第一种和第二种设置位的时间/空间(以周期为单位,除非另有说明) 1st 2nd 2/4 2/3 8051(标准) 4/4 3/3 8051(加速克隆版) 2/2 2/2 PIC 10/4 8/6 6805

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接