为什么AVR代码使用位移位[关闭]


7

在AVR编程中,通过将a左1移到合适的位位置来始终设置寄存器位-并用相同的补码清除它们。

例如:对于一个ATtiny85,我可以这样设置PORTB,b 4

PORTB |= (1<<PB4);

或像这样清除它:

PORTB &= ~(1<<PB4);

我的问题是:为什么要这样做?最简单的代码最终是一团乱码。为什么将位定义为位位置而不是掩码。

例如,ATtiny85的IO标头包括以下内容:

#define PORTB   _SFR_IO8(0x18)
#define PB5     5
#define PB4     4
#define PB3     3
#define PB2     2
#define PB1     1
#define PB0     0

对我来说,将这些位定义为掩码(这样)会更加合乎逻辑:

#define PORTB   _SFR_IO8(0x18)
#define PB5     0x20
#define PB4     0x10
#define PB3     0x08
#define PB2     0x04
#define PB1     0x02
#define PB0     0x01

所以我们可以做这样的事情:

// as bitmasks
PORTB |=  PB5 |  PB3 |  PB0;
PORTB &= ~PB5 & ~PB3 & ~PB0;

分别打开和关闭位b 5,b 3和b 0。相对于:

// as bit-fields
PORTB |=  (1<<PB5) |  (1<<PB3) |  (1<<PB0);
PORTB &= ~(1<<PB5) & ~(1<<PB3) & ~(1<<PB0);

该位掩码代码读取更清晰:设置位PB5PB3PB0。此外,由于不再需要移位位,因此似乎可以节省操作。

我认为可能是为了保留通用性而这样做的,以便允许将代码从n位AVR 移植到m位(例如8位到32位)。但这不是事实,因为#include <avr/io.h>解析为特定于目标微控制器的定义文件。即使改变从8位ATtiny目标,以一个8位的Atmega(其中位定义语法从改变PBxPORTBx,例如),需要更改代码。


3
我第二。即使利用无处不在_BV(b)而不是(1<<b)使事情变得混乱。我通常定义与位助记符_BV(),如#define ACK _BV(1)
钙3000年

2
一旦意识到编译器会将它们解释为相同的常量,那么在源代码中使用它实际上就是一个优先事项。用您自己的代码执行您认为最明智的操作;在修改现有项目时,请遵循其传统。
克里斯·斯特拉顿

3
“因为采用位掩码方法显然可以产生更具可读性的最终用户代码”-您的个人看法。我发现将1和0移到正确的位置要比猜测必须将几个数字加起来是否是位掩码要清楚得多。
汤姆·卡彭特

3
@TomCarpenter有趣。好吧,也许我无意中问了一个基于观点的问题。无论哪种方式,都有一些不错的反馈。来自更多(TI)DSP背景(位掩码为标准),它看起来像是一种奇怪的语法,我认为这是有一些具体原因的。
布莱尔·冯维尔

1
@BlairFonville也许您已经知道这一点,但是ARM的工作方式与您描述的完全一样(使用位掩码)。

Answers:


7

最简单的代码最终是一团乱码。为什么将位定义为位位置而不是掩码。

一点都不。移位仅在C源代码中,而不在编译的机器代码中。您显示的所有示例都可以并且将在编译时由编译器解析,因为它们是简单的常量表达式。

(1<<PB4) 只是说“ PB4位”的一种方式。

  • 因此,它不仅有效,而且不会创建更多的代码大小。
  • 对于人类程序员来说,也应该通过其索引(例如5)而不是其位掩码(例如32)来命名这些位,因为这样可以使用连续的数字0..7来标识这些位,而不是用笨拙的方式标识这些位。两个(1、2、4、8,.. 128)。

  • 还有另一个原因(可能是主要原因):
    C-header文件不仅可以用于C代码,而且可以用于汇编程序源代码(或嵌入在C源代码中的汇编程序代码)。在AVR汇编程序代码中,您绝对不仅要使用位掩码(可以通过移位从索引中创建位掩码)。对于某些AVR位操作汇编程序指令(例如SBI,CBI,BST,BLD),您必须在其指令操作码中使用位索引作为立即运算符
    仅当您通过索引识别SFR的位时(不是按位掩码),您可以将此类标识符直接用作汇编程序指令的立即操作数。否则,您必须为每个SFR位有两个定义:一个定义其位索引(例如可以用作上述位操纵汇编程序指令中的操作数),另一个定义其位掩码(仅用于整个字节的指令)被操纵)。


1
我明白那个。我不是在问它是否有效。我知道 我在问为什么要按原样写这些定义。对我来说,如果将它们定义为掩码而不是位位置,它将大大提高代码的可读性。
布莱尔·冯维尔

5
我认为这个答案没有重点。他从不谈论代码效率或编译器。这都是关于源代码的混乱。
管道

4
@Blair Fonville:没有简单的方法来定义这样的宏。它需要计算以2为底的对数。没有预处理器功能可以计算对数。即,只能使用表来完成此操作,我认为这将是一个非常糟糕的主意。
凝乳

2
@pipe:我不谈论它,因为我只是不认为它是“代码污染”或“源代码混乱”(或任何您想称呼它的东西)。相反,我认为提醒程序员/阅读者他使用的常数是2的幂(甚至可以通过使用shift表达式完成)很有用。
凝乳

1
@RJR,布莱尔·丰维尔(Blair Fonville):当然,在使用简单的预处理程序定义的同时可以轻松定义此类宏,但是我会尽可能避免使用预处理程序宏(又名preprosessor functios),因为它们可以进行调试(使用C逐步遍历C源代码)调试器)非常不透明。
凝乳

4

也许位移并不是PB*定义的唯一用例。也许还有另外一些用例,其中PB*直接使用定义而不是将其用作移动量。如果是这样的话,那么我相信DRY原理将使您实现可用于两个用例的一组定义(如这些PB*定义),而不是实现具有重复信息的两组不同的定义。

例如,我编写了一个应用程序,可以从多达8个ADC通道进行测量。它具有一个界面来启动新的测量,在该界面中,您可以通过8位字段指定多个通道,每个通道一位。(当指定了多个通道时,将并行执行测量。)然后它具有另一个接口,可返回单个通道的测量结果。因此,一个接口使用通道号作为移入位字段的方式,而另一个接口则直接使用通道号。我定义了一个枚举来涵盖两个用例。

typedef enum
{
    CHANNEL_XL_X = 0,
    CHANNEL_XL_Y = 1,
    CHANNEL_XL_Z = 2,
    CHANNEL_G_X = 3,
    CHANNEL_G_Y = 4,
    CHANNEL_G_Z = 5,
    CHANNEL_AUX1 = 6,
    CHANNEL_AUX2 = 7
} ChannelNum;

struct MeasurementResult;

void StartMeasurement(uint8_t channel_mask);
MeasurementResult ReadMeasurementResult(ChannelNum channel_num);

main
{
    ...

    StartMeasurement( (1 << CHANNEL_XL_X) | (1 << CHANNEL_XL_Y) | (1 << CHANNEL_XL_Z) );

    meas_result_x = ReadMeasurementResult(CHANNEL_XL_X);
    meas_result_y = ReadMeasurementResult(CHANNEL_XL_Y);
    meas_result_z = ReadMeasurementResult(CHANNEL_XL_Z);
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.