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

时间:2019-02-02 06:59:08

标签: c pointers struct embedded unions

我试图对在嵌入式应用程序中为外围设备编写驱动程序的方式更感兴趣。

自然地,对预定义的内存映射区域进行读写是一项常见的任务,因此我尝试将尽可能多的内容包装在结构中。

有时,我想写整个寄存器,有时我想操纵该寄存器中的一部分位。最近,我读了一些东西,建议建立一个包含单个uintX类型的联合,该联合足以容纳整个寄存器(通常为8或16位),以及一个具有位域集合的结构代表该寄存器的特定位。

在阅读了其中一些概述了用于管理外设的多个控制/状态寄存器的策略的文章后,我得出结论,大多数具有这种嵌入式开发经验的人不喜欢位域,主要是因为缺乏可移植性和不同编译器之间的安全性问题……更不用说调试也可能被位域混淆。

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

我的问题是:

  1. 我该如何采取这样的措施:

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

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

  2. 这些人的文章here的第3部分描述了我通常所说的...最后,请注意,他将所有寄存器(包装成并集)放入一个结构中,然后建议执行以下操作:

      

    定义一个指针以引用can基地址,并将其强制转换为指向(CAN)寄存器文件的指针,如下所示。

    #define CAN0 (*(CAN_REG_FILE *)CAN_BASE_ADDRESS)
    

    这个可爱的小动作到底是怎么回事? CAN0是指向#数字定义为CAN_BASE_ADDRESS的函数的指针?我不知道...他在那一个上让我迷失了。

2 个答案:

答案 0 :(得分:2)

1。 摆脱位域时的问题是,您不能再使用简单的赋值语句,而必须移位该值以进行写入,创建掩码,进行“与”操作以擦除先前的位,并使用“或”操作来写入新的位。阅读是相反的。例如,让我们来定义一个如下所示的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,并且所有位以及可能的所有字节或字都以正确的语义神奇地到达其正确位置。当然使用好的编译器-但是谁不想部署好的编译器?

某些编译器具有扩展功能,可以将给定地址分配给变量;例如,旧的turbo pascal具有ABSOLUTE关键字:

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

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

答案 1 :(得分:2)

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

使用大于unsigned char的类型也具有实现定义的效果,但实际上它具有更大的可移植性。在现实世界中,uintNN_t只有两种选择:大端或小端,通常对于给定的CPU,每个人都使用相同的顺序,因为这是CPU本地使用的顺序。 (一些架构,例如mips和arm都支持这两种字节序,但是通常人们会在多种CPU模型中坚持一种字节序。)如果要访问CPU自己的寄存器,则字节序仍然可能是CPU的一部分。另一方面,如果要访问外围设备,则需要保重。

您要访问的设备的文档将告诉您一次要寻址的存储单元有多大(在您的示例中显然为2个字节)以及位的排列方式。例如,它可能声明该寄存器是一个16位寄存器,无论CPU的字节顺序如何,都可以通过16位加载/存储指令访问该寄存器,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)

这里没有功能。函数声明将具有返回类型,后跟括号中的参数列表。它采用值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);