在嵌入式寄存器结构中用位移位替换位域

Replacing Bitfields with Bitshifting in an Embedded Register Struct

我正在努力了解如何为嵌入式应用程序中的外围设备编写驱动程序。

当然,读取和写入预定义的内存映射区域是一项常见的任务,所以我尽量将尽可能多的东西包装在一个结构中。

有时,我想写入整个寄存器,有时我想操作这个寄存器中的一个位子集。最近,我读到一些建议创建一个包含单个 uintX 类型的联合,该类型足以容纳整个寄存器(通常为 8 或 16 位),以及一个具有位域集合的结构在其中表示该寄存器的特定位。

在阅读了关于其中一些 post 的一些评论后,这些评论概述了为外围设备管理多个 control/status 寄存器的策略,我得出结论,大多数在这种级别的嵌入式方面有经验的人开发不喜欢位域主要是因为缺乏可移植性和不同编译器之间的一致性问题......更不用说调试也会被位域混淆。

大多数人似乎推荐的替代方法是使用位移位来确保驱动程序可以在平台、编译器和环境之间移植,但我很难看到它的实际应用。

我的问题是:

  1. 我怎么拍这样的东西:

    typedef union data_port
    {
        uint16_t CCR1;
        struct
            {
                data1 : 5;
                data2 : 3;
                data3 : 4;
                data4 : 4;
            }
    }
    

    并摆脱位域并以理智的方式转换为位移位方案?

  2. 这家伙的第 3 部分 post here 大致描述了我在说什么...注意最后,他把所有的寄存器(包裹起来作为联合)在结构中,然后建议执行以下操作:

    define a pointer to refer to the can base address and cast it as a pointer to the (CAN) register file like the following.

    #define CAN0 (*(CAN_REG_FILE *)CAN_BASE_ADDRESS)
    

    这个可爱的小动作到底是怎么回事? CAN0 是一个指针,指向一个函数的指针,该函数的函数是#defined as CAN_BASE_ADDRESS?我不知道...他在那件事上让我迷路了。

1。 摆脱位域的问题是你不能再使用简单的赋值语句,但你必须移动值来写入,创建一个掩码,做一个 AND 来擦除以前的位,并使用 OR 写入新的位.阅读也是类似的颠倒。例如,让我们以这样定义的 8 位寄存器为例:

val2.val1
0000.0000

val1为低4位,val2为高4位,整个寄存器命名为REG.
要将 val1 读入 tmp,应该发出:

tmp = REG & 0x0F;

并读取 val2:

tmp = (REG >> 4) & 0xF;   // AND redundant in this particular case

tmp = (REG & 0xF0) >> 4;

但是要将tmp写入val2,例如,你需要做:

REG = (REG & 0x0F) | (tmp << 4);

当然可以使用一些宏来实现这一点,但对我来说,问题是读取和写入需要两个不同的宏。

我认为位域是最好的方式,认真的编译器应该有选项来定义此类位域的字节顺序和位顺序。无论如何,这就是未来,即使现在可能不是每个编译器都提供完全支持。

2.

#define CAN0 (*(CAN_REG_FILE *)CAN_BASE_ADDRESS)

此宏将 CAN0 定义为指向 CAN 寄存器基地址的解引用指针,不涉及函数声明。假设您在地址 0x800 处有一个 8 位寄存器。你可以这样做:

#define REG_BASE 0x800     // address of the register
#define REG (*(uint8_t *) REG_BASE)

REG = 0;    // becomes *REG_BASE = 0
tmp = REG;  // tmp=*REG_BASE

您可以使用结构类型来代替 uint_t,所有的位,可能还有所有的字节或单词,都以正确的语义神奇地到达它们正确的位置。当然要使用一个好的编译器——但是谁不想部署一个好的编译器呢?

一些编译器 have/had 将给定地址分配给变量的扩展;例如旧的 turbo pascal 有 ABSOLUTE 关键字:

var CAN: byte absolute 0x800:0000;  // seg:ofs...!

语义与以前相同,只是更直接,因为不涉及指针,但这由宏和编译器自动管理。

C 标准没有指定位域序列占用多少内存或位域的顺序。在您的示例中,一些编译器可能决定为位域使用 32 位,即使尽管您显然希望它涵盖 16 位。因此,使用位字段会将您锁定到特定的编译器和特定的编译标志。

使用大于 unsigned char 的类型也有实现定义的效果,但实际上它更便携。在现实世界中,uintNN_t 只有两种选择:big-endian 或 little-endian,通常对于给定的 CPU 每个人都使用相同的顺序,因为这是 CPU 本机使用。 (一些体系结构,如 mips 和 arm 支持两种字节序,但通常人们在大量 CPU 模型中坚持一种字节序。)如果你正在访问 CPU 自己的寄存器,它的字节序无论如何都可能是 CPU 的一部分。另一方面,如果您要访问外围设备,则需要小心。

您正在访问的设备的文档将告诉您一次要寻址的内存单元有多大(在您的示例中显然是 2 个字节)以及这些位是如何排列的。例如,它可能声明该寄存器是一个 16 位寄存器,可通过 16 位 load/store 指令访问,而不管 CPU 的字节序是什么,即 data1 包含 5 个低位 -顺序位,data2 包含下一个 3,data3 包含下一个 4,data4 包含下一个 4。在这种情况下,您可以将寄存器声明为 uint16_t.

typedef volatile uint16_t data_port_t;
data_port_t *port = GET_DATA_PORT_ADDRESS();

设备中的内存地址几乎总是需要声明 volatile,因为编译器在正确的时间读取和写入它们很重要。

要访问寄存器的各个部分,请使用位移位和位掩码运算符。例如:

#define DATA2_WIDTH 3
#define DATA2_OFFSET 5
#define DATA2_MAX (((uint16_t)1 << DATA2_WIDTH) - 1) // in binary: 0000000000000111
#define DATA2_MASK (DATA2_MAX << DATA2_OFFSET) // in binary: 0000000011100000
void set_data2(data_port_t *port, unsigned new_field_value)
{
    assert(new_field_value <= DATA2_MAX);
    uint16_t old_register_value = *port;
    // First, mask out the data2 bits from the current register value.
    uint16_t new_register_value = (old_register_value & ~DATA2_MASK);
    // Then mask in the new value for data2.
    new_register_value |= (new_field_value << DATA2_OFFSET);
    *port = new_register_value; 
}

显然,您可以使代码更短。我把它分成单独的小步骤,这样逻辑应该很容易理解。我在下面包含了一个较短的版本。除了在非优化模式下,任何称职的编译器都应该编译成相同的代码。请注意,在上面,我使用了一个中间变量而不是对 *port 进行两次赋值,因为对 *port 进行两次赋值会改变行为:它会导致设备看到中间值(和另一个读取,因为 |= 既是读又是写)。这是较短的版本和一个读取功能:

void set_data2(data_port_t *port, unsigned new_field_value)
{
    assert(new_field_value <= DATA2_MAX);
    *port = (*port & ~(((uint16_t)1 << DATA2_WIDTH) - 1) << DATA2_OFFSET))
                   | (new_field_value << DATA2_OFFSET);
}
unsigned get_data2(data_port *port)
{
     return (*port >> DATA2_OFFSET) & DATA2_MASK;
}

#define CAN0 (*(CAN_REG_FILE *)CAN_BASE_ADDRESS)

这里没有功能。函数声明将具有 return 类型,后跟括号中的参数列表。这取值 CAN_BASE_ADDRESS,大概是某种类型的指针,然后将指针转换为指向 CAN_REG_FILE 的指针,最后取消引用该指针。换句话说,它访问 CAN_BASE_ADDRESS 给定地址处的 CAN 寄存器文件。例如,可能有像

这样的声明
void *CAN_BASE_ADDRESS = (void*)0x12345678;
typedef struct {
    const volatile uint32_t status;
    volatile uint16_t foo;
    volatile uint16_t bar;
} CAN_REG_FILE;
#define CAN0 (*(CAN_REG_FILE *)CAN_BASE_ADDRESS)

然后你可以做

CAN0.foo = 42;
printf("CAN0 status: %d\n", (int)CAN0.status);