我正在开发一个涉及编程 32位 ARM微控制器的项目。与许多嵌入式软件编码工作一样,设置和清除位是必不可少且非常重复的任务。当使用微型而不是32位来设置和清除位时,屏蔽策略很有用。但是当使用32位微控制器时,每次我们需要设置/清除单个位时写入掩码并不实际。
编写函数来处理这个问题可能是一个解决方案;但是有一个函数占用内存,这在我的情况下并不理想。
使用32位微处理器时,是否有更好的替代来处理位设置/清除?
答案 0 :(得分:11)
在C或C ++中,您通常会为位掩码定义宏,并根据需要将它们组合在一起。
/* widget.h */
#define WIDGET_FOO 0x00000001u
#define WIDGET_BAR 0x00000002u
/* widget_driver.c */
static uint32_t *widget_control_register = (uint32_t*)0x12346578;
int widget_init (void) {
*widget_control_register |= WIDGET_FOO;
if (*widget_control_register & WIDGET_BAR) log(LOG_DEBUG, "widget: bar is set");
}
如果要从位位置而不是绝对值定义位掩码,请根据移位操作定义常量(如果编译器没有优化这些常量,则无望)。 / p>
#define WIDGET_FOO (1u << 0)
#define WIDGET_BAR (1u << 1)
您可以定义宏来设置位:
/* widget.h */
#define WIDGET_CONTROL_REGISTER_ADDRESS ((uint32_t*)0x12346578)
#define SET_WIDGET_BITS(m) (*WIDGET_CONTROL_REGISTER_ADDRESS |= (m))
#define CLEAR_WIDGET_BITS(m) (*WIDGET_CONTROL_REGISTER_ADDRESS &= ~(uint32_t)(m))
您可以定义函数而不是宏。这具有在编译期间添加类型验证的优点。如果在标题中将函数声明为static inline
(或者甚至只是static
),那么一个好的编译器会在任何地方内联函数,因此在源代码中使用函数不会花费任何代码内存(假设函数体的生成代码小于函数调用,对于只设置寄存器中某些位的函数应该是这种情况。)
/* widget.h */
#define WIDGET_CONTROL_REGISTER_ADDRESS ((uint32_t*)0x12346578)
static inline void set_widget_bits(uint32_t m) {
*WIDGET_CONTROL_REGISTER_ADDRESS |= m;
}
static inline void set_widget_bits(uint32_t m) {
*WIDGET_CONTROL_REGISTER_ADDRESS &= ~m;
}
答案 1 :(得分:6)
提供对各个位或位组访问的寄存器的另一个常用习惯是为器件的每个寄存器定义包含struct
的位域。这可能会变得棘手,它依赖于C编译器实现。但它也可以比宏更清晰。
具有单字节数据寄存器,控制寄存器和状态寄存器的简单器件可能如下所示:
typedef struct {
unsigned char data;
unsigned char txrdy:1;
unsigned char rxrdy:1;
unsigned char reserved:2;
unsigned char mode:4;
} COMCHANNEL;
#define CHANNEL_A (*(COMCHANNEL *)0x10000100)
// ...
void sendbyte(unsigned char b) {
while (!CHANNEL_A.txrdy) /*spin*/;
CHANNEL_A.data = b;
}
unsigned char readbyte(void) {
while (!CHANNEL_A.rxrdy) /*spin*/;
return CHANNEL_A.data;
}
访问mode
字段只是CHANNEL_A.mode = 3;
,这比写*CHANNEL_A_MODE = (*CHANNEL_A_MODE &~ CHANNEL_A_MODE_MASK) | (3 << CHANNEL_A_MODE_SHIFT);
之类的内容要清晰得多。当然,后者丑陋的表达通常会(大部分)由宏覆盖。
根据我的经验,一旦你建立了描述外围寄存器的风格,你就可以在整个项目中遵循这种风格。一致性将为将来的代码维护带来巨大的好处,并且在项目的整个生命周期中,无论您采用struct
和位域还是宏样式,相对较小的细节都可能更为重要。
如果您正在为已经在其制造商提供的头文件和惯用编译器工具链中设置样式的目标编码,那么为您自己的自定义硬件和低级代码采用该样式可能是最好的,因为它将提供最佳匹配制造商文档和您的编码风格。
但是如果你有一开始就为开发建立风格,那么你的编译器平台就足够了,可以让你用位域可靠地描述设备寄存器,并且你希望在生命周期内使用相同的编译器。产品,那通常是一个很好的方式。
你实际上也可以两种方式。将位域声明包装在描述物理寄存器的union
中并不是很少见,允许它们的值一次容易地操作所有位。 (我知道我已经看到了这种变化,其中条件编译用于提供两个版本的位域,每个位顺序一个,公共头文件使用工具链特定的定义来决定选择哪个。)
typedef struct {
unsigned char data;
union {
struct {
unsigned char txrdy:1;
unsigned char rxrdy:1;
unsigned char reserved:2;
unsigned char mode:4;
} bits;
unsigned char status;
};
} COMCHANNEL;
// ...
#define CHANNEL_A_MODE_TXRDY 0x01
#define CHANNEL_A_MODE_TXRDY 0x02
#define CHANNEL_A_MODE_MASK 0xf0
#define CHANNEL_A_MODE_SHIFT 4
// ...
#define CHANNEL_A (*(COMCHANNEL *)0x10000100)
// ...
void sendbyte(unsigned char b) {
while (!CHANNEL_A.bits.txrdy) /*spin*/;
CHANNEL_A.data = b;
}
unsigned char readbyte(void) {
while (!CHANNEL_A.bits.rxrdy) /*spin*/;
return CHANNEL_A.data;
}
假设您的编译器理解匿名联合,那么您可以简单地引用CHANNEL_A.status
来获取整个字节,或CHANNEL_A.mode
仅引用模式字段。
如果你走这条路,有一些事情要注意。首先,您必须对平台中定义的结构打包有一个很好的理解。相关问题是在其存储中分配位字段的顺序,这可能会有所不同。我假设在我的示例中首先分配了低位。
可能还需要担心硬件实施问题。如果必须一次只读取和写入32位特定寄存器,但是您将其描述为一堆小位字段,则编译器可能会生成违反该规则的代码,并且只访问寄存器的单个字节。通常有一个技巧可以防止这种情况,但它将高度依赖于平台。在这种情况下,使用具有固定大小寄存器的宏将不太可能导致与您的硬件设备的奇怪交互。
这些问题依赖于非常编译器供应商。即使不更改编译器供应商,#pragma
设置,命令行选项或更可能的优化级别选择都会影响内存布局,填充和内存访问模式。作为副作用,他们可能会将您的项目锁定到单个特定的编译器工具链,除非使用英雄努力来创建使用条件编译来为不同编译器不同地描述寄存器的寄存器定义头文件。即便如此,您可能还应该至少包含一个验证您的假设的回归测试,以便对工具链的任何升级(或对优化级别的良好调整)将导致任何问题在它们成为神秘错误之前被捕获代码“已经工作了多年”。
好消息是,这种技术有意义的深层嵌入式项目已经受到许多工具链锁定的影响,这种负担可能根本不是负担。即使您的产品开发团队转向使用下一个产品的新编译器,对于特定产品的固件通常在其生命周期内使用相同的工具链进行维护也是至关重要的。
答案 2 :(得分:5)
如果您使用Cortex M3,则可以使用bit-banding
比特带将完整的存储器字映射到位带区域中的单个位。例如,写入其中一个别名字将设置或清除位带区域中的相应位。
这允许使用单个LDR指令从字对齐的地址直接访问位带区域中的每个单独位。它还允许从C切换单个位,而不执行读 - 修改 - 写指令序列。
答案 3 :(得分:2)
如果你有可用的C ++,并且有一个不错的编译器,那么像QFlags
这样的东西是个好主意。它为您提供了位标志的类型安全接口。
与使用结构中的位域相比,它可能产生更好的代码,因为位域只能一次更改一个,并且可能会转换为每个更改的位域至少一个加载/修改/存储。使用类似QFlags
的方法,您可以为每个或分配或分配语句获得一个加载/修改/存储。请注意,使用QFlags
不需要包含整个Qt框架。这是一个独立的头文件(经过小调整后)。
答案 4 :(得分:1)
在驱动程序级别设置和清除带掩码的位是非常常见的,有时是唯一的方法。此外,它是一个非常快速的操作;只有几条指示。设置一个可以清除或设置某些位的功能以获得可读性和可重用性可能是值得的。
目前尚不清楚您正在设置什么类型的寄存器并清除位,但一般情况下,您需要在嵌入式系统中担心两种情况:
设置和清除读/写寄存器中的位 如果要更改读写寄存器中的单个位(或少数位),首先必须读取寄存器,使用掩码设置或清除相应的位以及其他任何可以获得正确行为的位,然后写入回到同一个寄存器。这样你就不会改变其他位。
写入单独的Set和Clear寄存器(在ARM微处理器中常见)有时会有单独的Set和Clear寄存器。您只需将一个位写入清零寄存器即可清除该位。例如,如果有一个寄存器要清除第9位,只需将(1 <&lt; 9)写入清除寄存器即可。您不必担心修改其他位。类似于设置寄存器。
答案 5 :(得分:1)
您可以使用一个功能设置和清除位,该功能占用的内存与使用掩码一样多:
#define SET_BIT(variableName, bitNumber) variableName |= (0x00000001<<(bitNumber));
#define CLR_BIT(variableName, bitNumber) variableName &= ~(0x00000001<<(bitNumber));
int myVariable = 12;
SET_BIT(myVariable, 0); // myVariable now equals 13
CLR_BIT(myVariable, 1); // myVariable now equals 11
这些宏将生成与掩码完全相同的汇编指令。
或者,你可以这样做:
#define BIT(n) (0x00000001<<n)
#define NOT_BIT(n) ~(0x00000001<<n)
int myVariable = 12;
myVariable |= BIT(4); //myVariable now equals 28
myVariable &= NOT_BIT(3); //myVariable now equals 20
myVariable |= BIT(5) |
BIT(6) |
BIT(7) |
BIT(8); //myVariable now equals 500