为什么在嵌入式系统固件中可以使用结构体来表示 MMIO 寄存器,而不是位域?

问题描述 投票:0回答:1

在嵌入式系统固件中,通常使用结构体来表示外设的多个内存映射寄存器,假设编译器的代码生成将遵循给定平台上的特定内存布局。例如,以下代码是标准的:

typedef struct
{
    volatile uint32_t CRL;  /* bytes  0 to  4 */
    volatile uint32_t CRH;  /* bytes  4 to  8 */
    volatile uint32_t IDR;  /* bytes  8 to 12 */
    volatile uint32_t ODR;  /* bytes 12 to 16 */
} GPIO_t;

#define GPIOA         ((GPIO_t *) 0xDEADBEEF)
#define GPIOB         ((GPIO_t *) 0xDEADCODE)

void loop(void)
{
    GPIOB->CRL = 0x01;
    GPIOA->CRH = 0x01;
}

此方法的理由是:

  1. 它用纯 C 代码创建一个模拟命名空间。不同的GPIO端口可以有相同的寄存器,但它们之间的区别很明显,不需要使用丑陋的前缀,并且外设结构体可以直接作为一个整体传递。

  2. 重复的寄存器布局只需定义一次。

  3. 尽管 C 标准对结构的确切内存布局几乎没有要求(如果有的话),但它们通常在给定平台上是可预测的。由于所有寄存器都是 MCU 特定的,因此可移植性并不重要。

因此,我的印象是这种技术很常见。

为了表示物理 MMIO 寄存器中的字段,在结构中使用位字段是一种密切相关的技术,可以实现类似的结果。例如,

CRH
寄存器中的位字段可以使用以下结构表示:

typedef struct
{
    /* 
     * 00: Analog Input, 01: Floating Input
     * 10: Pull-up/down, 11: Reserved
     */
    volatile unsigned int CTL7  : 2; 

    /* 00: Pull Pull,    01: Open Drain */
    volatile unsigned int MODE7 : 2; 

    volatile unsigned int CTL6  : 2;
    volatile unsigned int MODE6 : 2;
    volatile unsigned int CTL5  : 2;
    volatile unsigned int MODE5 : 2;
    volatile unsigned int CTL4  : 2;
    volatile unsigned int MODE4 : 2;
    volatile unsigned int CTL3  : 2;
    volatile unsigned int MODE3 : 2;
    volatile unsigned int CTL2  : 2;
    volatile unsigned int MODE2 : 2;
    volatile unsigned int CTL1  : 2;
    volatile unsigned int MODE1 : 2;
    volatile unsigned int CTL0  : 2;
    volatile unsigned int MODE0 : 2;
} CRH_t;

问题:

  1. 在低级嵌入式固件中,使用结构体表示多个 MMIO 寄存器是否可以接受?

  2. 在低级嵌入式固件中,使用结构中的位字段表示同一寄存器内的多个选项是否可以接受?

  3. 这两种技术都是相关的,并且都依赖于潜在的不可移植的假设。但在我看来,第一个问题的答案往往是“是”,但第二个问题的答案往往是“否”——或者至少存在相当大的分歧。例如,在问题在嵌入式系统固件中使用位域可以吗?,可以找到很多反对意见:

    Lundin:认为永远不需要移植该程序的想法是天真的。即使硬件保持不变,相当多的程序在某些时候也必须移植到不同的编译器。对于嵌入式系统尤其如此。此外,能够在新项目中重用已经编写的代码也是非常好的。我还可以提到至少 10 个以上的[编译器]。然后是所有这些编译器的不同硬件端口。它们不实现相同的填充、字节顺序甚至位顺序。当您在同一结构中混合不同类型时,它们处理位填充的方式完全不同。

    另一方面:我在 64 位 PC 上为我的 MCU 程序编写了大量模拟器和测试用例。例如,有一次我设计了一种无线电频谱分配算法,需要一个图形模拟器来可视化和调试该算法。目标平台是 8 位 MCU,但我在 64 位 x86 上运行了所有测试。通过拼凑一些 LCD 图形库 + 修复硬件来完成同样的任务,比编写可移植代码然后在 RAD 工具中拼凑一个简单的 Windows 应用程序要耗时得多。

    为什么在嵌入式系统固件中可以使用结构体来表示 MMIO 寄存器,而不是位域?

c struct embedded bit-fields firmware
1个回答
0
投票

位字段的表示方式因机器而异,特别是在字节序(字节存储顺序)方面,有些机器在写入数字时最高有效位在左侧,因此我们可能会认为第 31 位是最左边的,而第 31 位是最左边的。 0 为最右边, 但是当您在嵌入式系统中使用结构体来表示 MMIO 寄存器时,编译器会确保结构体的字段根据目标机器的体系结构正确排列。

例如:

假设我们有这个结构:

struct Data {
    int number1; //(4 bytes)
    int number2; //(4 bytes)
};

我们有两台机器,第一台是 Big Endian 第二台是 Little Endian

发送结构:-

1- 发送机(大端):

Data dataToSend = {123, 456};

2- 接收机(小端):

Data dataReceived = {123, 456};

发送一些位:-

1- 发送机(大端):

sends (00 00 00 7B 00 00 01 C8)

number1 (4 bytes): 00 00 00 7B (hexadecimal for 123)

number2 (4 bytes): 00 00 01 C8 (hexadecimal for 456)

1- 接收机(小端):

received (00 00 00 7B 42 35 76 66)

number1 (4 bytes): 00 00 00 7B (hexadecimal for 2063597568)

number2 (4 bytes): 00 00 01 C8 (hexadecimal for 3355508736)

注:

如果您希望接收器正确获取位,则应像这样发送:

7B 00 00 00 C8 01 00 00

另外,请注意,如果两台机器都是大端或都是小端,那么位发送是有效的。或者,如果您在接收数据时执行从大端到小端的转换,并添加指示机器类型的额外参数,它也可以工作。或者发送一个结构体并让编译器完成工作。

© www.soinside.com 2019 - 2024. All rights reserved.