在嵌入式系统固件中,通常使用结构体来表示外设的多个内存映射寄存器,假设编译器的代码生成将遵循给定平台上的特定内存布局。例如,以下代码是标准的:
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;
}
此方法的理由是:
它用纯 C 代码创建一个模拟命名空间。不同的GPIO端口可以有相同的寄存器,但它们之间的区别很明显,不需要使用丑陋的前缀,并且外设结构体可以直接作为一个整体传递。
重复的寄存器布局只需定义一次。
尽管 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;
在低级嵌入式固件中,使用结构体表示多个 MMIO 寄存器是否可以接受?
在低级嵌入式固件中,使用结构中的位字段表示同一寄存器内的多个选项是否可以接受?
这两种技术都是相关的,并且都依赖于潜在的不可移植的假设。但在我看来,第一个问题的答案往往是“是”,但第二个问题的答案往往是“否”——或者至少存在相当大的分歧。例如,在问题在嵌入式系统固件中使用位域可以吗?,可以找到很多反对意见:
Lundin:认为永远不需要移植该程序的想法是天真的。即使硬件保持不变,相当多的程序在某些时候也必须移植到不同的编译器。对于嵌入式系统尤其如此。此外,能够在新项目中重用已经编写的代码也是非常好的。我还可以提到至少 10 个以上的[编译器]。然后是所有这些编译器的不同硬件端口。它们不实现相同的填充、字节顺序甚至位顺序。当您在同一结构中混合不同类型时,它们处理位填充的方式完全不同。
另一方面:我在 64 位 PC 上为我的 MCU 程序编写了大量模拟器和测试用例。例如,有一次我设计了一种无线电频谱分配算法,需要一个图形模拟器来可视化和调试该算法。目标平台是 8 位 MCU,但我在 64 位 x86 上运行了所有测试。通过拼凑一些 LCD 图形库 + 修复硬件来完成同样的任务,比编写可移植代码然后在 RAD 工具中拼凑一个简单的 Windows 应用程序要耗时得多。
为什么在嵌入式系统固件中可以使用结构体来表示 MMIO 寄存器,而不是位域?
位字段的表示方式因机器而异,特别是在字节序(字节存储顺序)方面,有些机器在写入数字时最高有效位在左侧,因此我们可能会认为第 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
另外,请注意,如果两台机器都是大端或都是小端,那么位发送是有效的。或者,如果您在接收数据时执行从大端到小端的转换,并添加指示机器类型的额外参数,它也可以工作。或者发送一个结构体并让编译器完成工作。