替代为32位微控制器编写掩码

gbu*_*dan 12 c embedded microcontroller bit-manipulation

我正在开发一个涉及编程32位 ARM微控制器的项目.与许多嵌入式软件编码工作一样,设置和清除位是必不可少且非常重复的任务.当使用微型而不是32位来设置和清除位时,屏蔽策略很有用.但是当使用32位微控制器时,每次我们需要设置/清除单个位时写入掩码并不实际.

编写函数来处理这个可能是一个解决方案; 但是有一个功能占用的内存在我的情况下是不理想的.

使用32位微处理器时,有没有更好的替代方法来处理位设置/清除?

Gil*_*il' 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");
}
Run Code Online (Sandbox Code Playgroud)

如果要定义从位的位置,而不是绝对值位掩码,定义基于换档动作(如果你的编译器不优化这些常量,它是无望)常数.

#define WIDGET_FOO (1u << 0)
#define WIDGET_BAR (1u << 1)
Run Code Online (Sandbox Code Playgroud)

您可以定义宏来设置位:

/* 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))
Run Code Online (Sandbox Code Playgroud)

您可以定义函数而不是宏.这具有在编译期间添加类型验证的优点.如果在头文件中声明函数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;
}
Run Code Online (Sandbox Code Playgroud)


RBe*_*eig 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;
}
Run Code Online (Sandbox Code Playgroud)

访问该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;
}
Run Code Online (Sandbox Code Playgroud)

假设您的编译器理解匿名联合,那么您可以简单地引用CHANNEL_A.status获取整个字节,或CHANNEL_A.mode仅引用模式字段.

如果你走这条路,有一些事情要注意.首先,您必须对平台中定义的结构打包有一个很好的理解.相关问题是在其存储中分配位字段的顺序,这可能会有所不同.我假设在我的示例中首先分配了低位.

可能还需要担心硬件实现问题.如果必须一次只读取和写入32位特定寄存器,但是您将其描述为一堆小位字段,则编译器可能会生成违反该规则的代码,并且只访问寄存器的单个字节.通常有一个技巧可以防止这种情况,但它将高度依赖于平台.在这种情况下,使用具有固定大小寄存器的宏将不太可能导致与您的硬件设备的奇怪交互.

这些问题非常依赖于编译器供应商.即使不更改编译器供应商,#pragma设置,命令行选项或更可能的优化级别选择都会影响内存布局,填充和内存访问模式.作为副作用,他们可能会将您的项目锁定到单个特定的编译器工具链,除非使用英雄努力来创建使用条件编译来为不同编译器不同地描述寄存器的寄存器定义头文件.即便如此,您可能还应该至少包含一个验证您的假设的回归测试,以便对工具链的任何升级(或对优化级别的良好调整)都会导致任何问题在它们成为神秘错误之前被捕获代码"已经工作多年".

好消息是,深嵌入式项目的地方这种技术是有道理的种类已受到势力中的一些工具链锁,并且这种负担可能不是一个负担都没有.即使你的产品开发团队转移到下一个产品一个新的编译器,它往往是关键的固件为特定的产品,在其生命周期非常相同的工具链来维持.

  • 我会强调编译器依赖性.如果你总是在同一个硬件上使用相同的编译器,那么位域方法就可以了.但是如果使用不同的编译器,它们可能会将位置于不同的位置(从MSB或LSB开始,或填充到不同的宽度). (3认同)

小智 5

如果使用Cortex M3,则可以使用位带

位带将完整的存储器字映射到位带区域中的单个位.例如,写入别名字之一将设置或清除位带区域中的相应位.

这允许使用单个LDR指令从字对齐的地址直接访问位带区域中的每个单独位.它还允许从C切换各个位,而不执行读 - 修改 - 写指令序列.