C宏:将由位掩码定义的寄存器字段设置为给定值

3

我有32位寄存器,其中包含用作位掩码的字段,例如:

#define BM_TEST_FIELD 0x000F0000

我需要一个宏,允许我将寄存器(由其地址定义)的字段(由其位掩码定义)设置为给定值。以下是我想到的代码:
#include <stdio.h>
#include <assert.h>

typedef unsigned int u32;

/* 
 * Set a given field defined by a bit-mask MASK of a 32-bit register at address
 * ADDR to a value VALUE.
 */
#define SET_REGISTER_FIELD(ADDR, MASK, VALUE)                                      \
{                                                                                  \
  u32 mask=(MASK); u32 value=(VALUE);                                              \
  u32 mem_reg = *(volatile u32*)(ADDR); /* Get current register value           */ \
  assert((MASK) != 0);                  /* Null masks are not supported         */ \
  while(0 == (mask & 0x01))             /* Shift the value to the left until    */ \
  {                                     /* it aligns with the bit field         */ \
    mask = mask >> 1; value = value << 1;                                          \
  }                                                                                \
  mem_reg &= ~(MASK);                   /* Clear previous register field value  */ \
  mem_reg |= value;                     /* Update register field with new value */ \
  *(volatile u32*)(ADDR) = mem_reg;     /* Update actual register               */ \
}

/* Test case */
#define BM_TEST_FIELD 0x000F0000
int main()
{
  u32 reg = 0x12345678;
  printf("Register before: 0x%.8X\n", reg);/* should be 0x12345678 */
  SET_REGISTER_FIELD(&reg, BM_TEST_FIELD, 0xA);
  printf("Register after: 0x%.8X\n", reg); /* should be 0x123A5678 */
  return 0;
}

有没有更简单的方法来完成这件事?
编辑:特别是,我正在寻找一种减少运行时计算要求的方法。有没有一种方式让预处理器计算所需左移的数量?
4个回答

5

编辑:特别是,我正在寻找一种减少运行时计算要求的方法。有没有一种方法可以让预处理器计算值所需的左移位数?

是的:

value *= ((MASK) & ~((MASK) << 1))

这将会把 value 乘以 MASK 中最低位的值。编译时,乘数已知为2的常量次幂,因此任何一个合理的编译器都会将其编译为简单的左移操作。


太棒了!不知怎么的,我陷入了这样一种假设中,即在某个时刻我们需要用确切的位数来表示偏移值。但实际上我们并不需要!你也可以使用 MASK & -MASK 来仅保留最低位的 1。 - AnT stands with Russia

3
为什么不把掩码和值都放在正确的位置上呢?
#define BM_TEST_FIELD (0xfUL << 16)
#define BM_TEST_VALUE (0xaUL << 16)
#define mmioMaskInsert(reg, mask, value) \
   (*(volatile u32 *)(reg) = (*(volatile u32 *)(reg) & ~(mask)) | value)

然后您可以像这样使用它:
mmioMaskInsert(reg, BM_TEST_FIELD, BM_TEST_VALUE);

毫无疑问,你所拥有的东西非常危险。寄存器写入通常会产生副作用,而这些操作:

mem_reg &= ~(MASK);
mem_reg |= value;

您实际上是在寄存器中写入两次,而不是一次,这可能不是您想要的。此外,为什么不支持0的掩码?如果我想写入整个寄存器(定时器计数匹配或其他),该怎么办?您是否有不同的宏来执行该操作?如果有,为什么不将其作为此系统的一部分使用?
另一个注意点-在将值放入寄存器之前,最好对该值应用掩码,以防传递了比掩码更多位的值。类似于:
#define maskInsert(r, m, v) \
  (*(volatile u32 *)(r) = (*(volatile u32 *)r & ~(m)) | ((v) & ~(m)))

Carl,我需要写入的寄存器位于连接到处理器总线的外设模块中。mem_reg只是一个通常会分配在堆栈上的变量,访问它不应该产生任何副作用。 - geschema
不支持零掩码,因为按定义位域必须至少包含一位。将整个寄存器写入是通过将位掩码设置为全1来完成的。 - geschema

2

我会考虑使用位域(bitfields)来“格式化”硬件中的位,例如:

#include <stdio.h>
#include <inttypes.h>

struct myregister {
    unsigned upper_bits:12;
    unsigned myfield:4;
    unsigned lower_bits:16;
};

typedef union {
    struct myregister fields;
    uint32_t value;
} myregister_t;

int main (void) {
    myregister_t r;
    r.value = 0x12345678;
    (void) printf("Register before: 0x%.8" PRIX32 "\n", r.value);
    r.fields.myfield = 0xA;
    (void) printf("Register after: 0x%.8" PRIX32 "\n", r.value);
    return 0;
}

编辑:请注意评论中的后续讨论。使用位域存在合理的反对意见,但是我认为也有好处(特别是在语法方面,我非常重视)。应该根据代码将要使用的情况来做出决定。


5
请永远不要使用位域(bitfields)进行这种操作。在许多编译器中,它们得到的支持很差,标准也没有保证它们的布局,并且它们会导致巨大的可移植性问题。如果你遵循这个建议,你将会遭受严重的打击。访问给定位域的读/修改/写规则尤其模糊,你最终会遇到难以调试或无法调试的副作用和使你哭泣的代码生成。 - Carl Norum
@Carl Norum: 嗯,修改硬件寄存器是最不可移植的事情之一;如果位域在这种本质上不可移植的情况下工作,为什么不使用它们呢?当然,应该检查生成的代码是否满足任何性能要求,并运行上述示例以验证正确的结果。 - Arkku
C代码在相同硬件上不同编译器之间可移植。仅仅因为它们能在该硬件上的一个编译器上工作并不意味着它们会在相同硬件上的另一个编译器上工作。我可以向您保证的是,总有一天您需要使用不同的编译器。 - Carl Norum
@Carl Norum:如果编译器不能正确地布局硬件寄存器大小的位域,我会感到非常惊讶,但确实没有严格的保证。但正如我在之前的评论中所说,应该检查生成的代码性能和正确性。如果当前编译器既没有问题,我会使用位域,并在必要时更改为手动移位。但这可能是因为我在使用它们来探测硬件寄存器的任何编译器(用于微控制器)中都没有遇到过问题。 - Arkku
1
使用“正确地”这个词,我认为表明你有很多潜在的危险假设。只要存储的数据检索出来没有被改变,那么就是正确的。没有任何保证(甚至是隐含的),数据会以任何特定的方式打包到内存中。它可能根本没有被打包!只是问一下,在你的“正确”的版本中,第一个项目是在字的最高位还是最低位声明的?无论哪种方式都可以很好地打包,那么为什么一种方式比另一种方式更正确呢? - Southern Hospitality
显示剩余4条评论

1

如果您坚持使用特定的接口(字段的位置由掩码定义),那么您实现中可能唯一可以改变/改进的是将值移动到正确位置(与掩码对齐)的循环。基本上,您需要做的就是找到以位为单位表示的偏移量,然后将该值左移该位数。您使用了一个简单的循环来执行该操作,而且没有明确计算出位偏移量,而是在每次迭代时将该值向左移动1位。这样做是可行的。但是,对于驻留在寄存器上部的字段,它们将需要更多的移位循环迭代,因此可能被视为低效。

为了提高效率,您还可以使用任何已知的、潜在更有效的方法来计算偏移值,例如this page上描述的方法。不过我不知道在您的情况下是否值得这样做。这可能会使您的代码更加高效,但也可能会使其更难阅读。请自行决定。


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