当前位置:首页 > 嵌入式 > 嵌入式分享
[导读]在MCU底层开发里,改一个寄存器位最常见也最危险。写REG |= (1<


在MCU底层开发里,改一个寄存器位最常见也最危险。写REG |= (1<<n)看起来一行搞定,但编译后通常是“读—改—写”三条指令;若主循环和中断同时改同一寄存器不同位,中断里的修改可能被回写覆盖,出现LED状态随机错乱、通信标志丢失等问题。真正安全的做法不是“多写几行”,而是把读改写压缩成单次硬件写。下面给三套可直接用3行实现的方案。

一、GPIO场景:用厂家原子置/清寄存器

以STM32的BSRR为例,低16位置位、高16位复位,写0不影响其它位,硬件单写完成:

// 原子置位PA5、原子清PA6

#define SET_BIT_BSRR(reg, n)   ((reg)->BSRR = (1U << (n)))

#define CLR_BIT_BSRR(reg, n)   ((reg)->BSRR = (1U << ((n)+16)))

SET_BIT_BSRR(GPIOA, 5);   // 只动bit5

CLR_BIT_BSRR(GPIOA, 6);   // 只动bit6

这三行(宏定义算封装、调用两行)不需要关中断,也不依赖内核。其它厂家若有类似“置位/复位/中断标志清除”专用寄存器,优先用它,比位带更省事、移植性更好。

二、Cortex-M3/M4通用外设:位带别名单写

外设区0x40000000起1MB可位带,别名基址0x42000000,公式:别名=0x42000000+(原地址-0x40000000)32+bit4。宏封装后改单bit就是一次32位写,总线层原子:

#define BB_PERI(reg, bit) \

   (*(volatile uint32_t *)(0x42000000UL + (((uint32_t)(reg)-0x40000000UL)<<5) + ((bit)<<2)))

BB_PERI(&GPIOA->ODR, 5) = 1;   // 原子置PA5输出高

BB_PERI(&USART1->CR1, 3) = 1;  // 原子使能TXE中断

注意:位带只覆盖前1MB外设/SDRAM区,且必须用32位访问;Cortex-M0/M0+无位带,M7因Cache一致性问题通常不建议用。若目标不在位带区,回到方案一无专用原子寄存器再考虑关中断。

三、无原子寄存器/裸机中断竞争:临界区3行

改状态寄存器、时钟控制位等没有BSRR/位带时,用保存PRIMASK+关中断+写+恢复,保证读改写不被中断拆开:

uint32_t atomic_set_bit(volatile uint32_t *reg, uint32_t bit){

   uint32_t pm = __get_PRIMASK(); __disable_irq();

   *reg |= (1U<<bit); __set_PRIMASK(pm); return 0;

}

atomic_set_bit(&RCC->CR, 0);   // 例:原子置HSION

多任务RTOS中别长期关中断,改用调度锁或信号量;M3/M4还可用LDREX/STREX做无锁原子或读改写。

四、状态寄存器W1C别用“&=~”

很多中断状态位是write-1-to-clear:写1清、写0无效。用STATUS &= ~ERR_FLAG可能按读改写执行,且不符合W1C语义;正确直接写掩码:

#define CLR_W1C(reg, mask)  (*(volatile uint32_t *)(reg) = (mask))

CLR_W1C(&EXTI->PR, (1U<<3));   // 只清line3挂起,不影响其它位

这行是单次写,硬件按位清,避免读回旧值再回写把其它pending位误清或漏清。

五、选型小结

GPIO置/清:BSRR类专用寄存器,首选,3行宏调用最稳;

通用外设单bit、M3/M4且在位带区:位带别名单写,免关中断;

无专用寄存器、裸机并发:临界区或LDREX/STREX;

W1C标志:直接写1掩码,别用读改写清。

把“只改目标位、不碰其它位、单次写完成”作为寄存器位操作铁律,中断竞争、标志丢失、多任务互覆盖类Bug会少一大半。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除( 邮箱:macysun@21ic.com )。
关闭