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



我试图在嵌入式应用程序中为外围设备编写驱动程序的方式更漂亮一点。

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

有时,我想写入整个寄存器,有时我想操作此寄存器中的位子集。 最近,我读到一些东西,建议做一个联合,其中包含一个足够大的uintX类型来容纳整个寄存器(通常为 8 或 16 位),以及一个结构,其中包含一组位字段来表示该寄存器的特定位。

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

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

我的问题是:

  1. 我该如何采取这样的东西:

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

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

  2. 这家伙帖子的第 3 部分在这里描述了我一般在说什么......请注意,最后,他将所有寄存器(包装为联合)放在一个结构中,然后建议执行以下操作:

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

    #define CAN0 (*(CAN_REG_FILE *)CAN_BASE_ADDRESS)
    

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

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

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

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

这里没有功能。函数声明将具有返回类型,后跟括号中的参数列表。这取值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: %dn", (int)CAN0.status);

1. 删除位字段时的问题在于,您不能再使用简单的赋值语句,但您必须移动值进行写入,创建掩码,创建 AND 以清除以前的位,并使用 OR 写入新位。阅读是类似的颠倒。例如,让我们采用一个定义如下的 8 位寄存器:

val2.val1
0000.0000

val1 是较低的 4 位,Val2 是较高的 4 位。要将
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);

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

我认为位域是最好的方法,一个认真的编译器应该有选项来定义这些位字段的尾部和位顺序。无论如何,这是未来,即使目前可能不是每个编译器都有完全支持。

阿拉伯数字。

#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...!

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

最新更新