在C语言中应用可移植的位掩码和标志

5

我使用各种微控制器体系结构/字长来开发嵌入式系统的固件,并尽可能强调可移植性。我的问题是如何生成/应用比特掩码和标志,以确保安全和可移植性,并对于任何非通用代码,角落案例是什么。

比特掩码

#define MASK_8_V1(x) ((x) & 0xFF)
#define MASK_8_V2(x) ((x) & 0x00FF)

a = MASK_8_V1(b)
a = MASK_8_V2(b)

这两个版本是否总是保证获取a的宽度值,其中除了b的低8位之外,所有位都被清零?如果需要,它们应该进行符号扩展,这两个版本是否有任何区别?

标志

#define GEN_FLAG_16(x) ((0xFFFF) & ((unsigned) 1 << (x)))
#define GEN_FLAG_32(x) ((0xFFFFFFFF) & ((unsigned long) 1 << (x)))

如果我需要一个生成标志常量的通用宏,这是否总是会生成列出宽度的标志常量? 两者都可以。
#define CHECK_FLAG_16(x, y) ((x) & GEN_FLAG_16(y))
#define CHECK_FLAG_32(x, y) ((x) & GEN_FLAG_32(y))

if(CHECK_FLAG_16(a, b))
{
    // Do something.
}

结合前面的场景,如果b的原始值中所需的位被设置,这段代码是否总是会执行内部代码?
对于所有情况,请假设:
- C90或C99兼容编译器 - a和b可以是任意组合的本机C类型,带符号或无符号 - x始终评估为正整数类型 - 任意本机字大小
请注意提到使用stdint.h的内容:我最近遇到一个问题,需要将我撰写的串行协议处理程序移植到另一个微控制器系列上,但发现RAM不可按字节寻址。我们最终删除了所有uint8_t的使用,并进行了修改以适应16位可寻址内存。这让我想知道是否可以使用本机C类型以不需要以后进行修改的方式实现它。我的问题间接来自那个问题。

1
你尝试过哪些编译器?当你尝试它们时发生了什么?你阅读了哪些与此相关的规格说明?它们说了什么? - old_timer
2
你认为 0xff0x00000000ff 有什么不同?为什么不使用专门定义用于固定宽度数据的固定宽度类型?请参阅 stdint.h!另外,不要使用带符号整数。 - too honest for this site
你有两个语句有点相互矛盾:1. 我尽可能强调可移植性。2. 我只对值的低8位感兴趣。 - barak manos
@Olaf 在我的工作场所,我们实际上强制使用 stdint.h,并且近年来开发的所有代码都遵循这个规则。最近我遇到了一个问题,需要将我编写的串行协议处理程序移植到另一种微控制器系列,结果发现 RAM 不是按字节寻址的。我们最终删除了所有 uint8_t 的使用,并进行了修改以适应 16 位可寻址内存。这让我想知道是否可以使用本机 C 类型以不同的方式实现它,以避免后期修改。我的问题间接地来源于那个问题。 - derrick
@Olaf 很好的发现,我忘记加上 unsigned 了。 - derrick
显示剩余2条评论
2个回答

3

这些宏是否总是保证获取a的宽度值,其中除了b的低8位之外,所有位都为零?

是的。

但是,由于b的类型不同,宏的结果类型可能也会有所不同。这可能会导致可移植性问题。因此最好将结果转换为预期的类型,例如uint32_t

这两个版本有区别吗?

没有,它们是等价的。

如果必须要进行符号扩展,是否存在任何差异?

在有符号类型上使用位运算符通常没有意义。

如果我需要一个用于生成标志常量的通用宏,这是否总会导致列出的宽度的标志常量?

是的,但结果类型将取决于int或long的大小。

最近我遇到一个问题,我们需要将我编写的串行协议处理程序移植到另一种微控制器系列中,但发现RAM不是字节寻址的。

那主要是编译器的问题。此外,在这些系统上使用uint8_t并不清楚会有什么问题,因为总是会有隐式整数提升。听起来更像是您遇到了一些算法问题,可能是使用了uint8_t*指针或类似的代码问题。

严谨地说,完全可移植的代码应该是这样的:

#define MASK8_32(x) ((uint32_t)(x) & 0xFFul)

#define GEN_FLAG_16(x) (uint16_t)( 0xFFFFu & (1u << (x)) )
#define GEN_FLAG_32(x) (uint32_t)( 0xFFFFFFFFu & (1ul << (x)) )

现在大部分关于int大小和隐式类型提升的依赖已经被消除。

这里的主要可移植性问题是:

  • 您的代码依赖于intlong的大小。这种代码是不可移植的,因为这些类型可以有任何大小。使用stdint.h中的固定宽度整数类型将解决许多这些问题。
  • 您的代码不知道默认类型和整数字面值的符号。在许多情况下,您在有符号操作数上使用位运算符,这总是一个坏主意。
  • 您似乎并不了解C语言中的隐式类型提升是如何工作的。

事实证明,所有这些问题都可以通过MISRA-C来解决。我建议购买MISRA-C:2012并阅读它以进行教育目的。


作为一个旁注,像 a = OBSUCRE_MACRO(b); 这样的代码比像 a = b & 0xFF; 这样的代码 难以阅读。因为你可以总是假设读者是 C 程序员,并且知道 C 语言,但不知道你的私有、秘密宏语言。
此外,类似函数的宏在可能的情况下应该避免使用,因为不安全。
所以我质疑这些宏一开始就有何用处。

谢谢,这正是我在寻找的。关于非字节可寻址问题,你是正确的,它来自使用 uint8_t * 解析串行通信的字节流。你如何消除这种依赖性?至于晦涩的宏,我同意它可能是不必要的。有时我使用这些来强制一致性,但也许这个案例不太好。一个更合理的情况是像 #define CHECK_INCLUSIVE(val, min, max) (((val) >= (min)) && ((val) <= (max))) 这样的东西。它消除了使用比较运算符时出现难以发现的拼写错误的可能性。 - derrick
@ohitsderrick,除非编译器实现了一些巧妙的解决方法,否则你对齐访问无能为力。也就是说,当你编写C代码并说“在地址0x1001处读取1个字节”时,编译器需要生成代码“在地址0x1000处读取1个字,并屏蔽掉一个字节”。 - Lundin
@ohitsderrick 关于那个宏,似乎在编写该宏时出现拼写错误或错误比编写简单的行if(val>=min && val<=max)更容易发生... - Lundin

-1
你真正应该做的是使用在<stdint.h>中定义的类型。你知道你需要多少位来表示你的标志,因此选择适当的数据类型。例如:
#define GEN_FLAG_8(x) ((uint8_t)(1 << (x)))
#define GEN_FLAG_16(x) ((uint16_t)(1 << (x)))

你也可以选择使用 uint_fast16_t 等具有潜在更大数据类型的相同保证。

值得一提的是,你应该编写宏或函数来执行掩码操作,以避免在代码中使用整数字面量。


1
是的,掩码宏的观点很好。我修改了我的原始问题以保持一致性。顺便说一下,在你列出的顺序中进行转换可能会在某些情况下产生不正确的结果。例如,当目标平台上的int为16位时,#define GEN_FLAG_32(x) ((uint32_t) (1 << (x)))。这最近发生在我身上。 - derrick
@ohitsderrick:此外,如果“int”为32位且“x == 31”,则行为未定义。 - Nisse Engström

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