先聊聊分享这篇文章的原因,在使用STM32时,我发现对于GPIO输出操作,可以使用GPIOx_ODR寄存器,也可以使用GPIOx_BSRR寄存器。
对应的标准外设库API接口有
void GPIO_ToggleBits(GPIO_TypeDef* GPIOx, uint16_t PortVal) void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
对于我来说,我一直在用GPIO_SetBits和GPIO_ResetBits接口,一直对GPIO_ToggleBits无感。最近注意的这个问题,经过查资料和FAE确认,这样做的,目的是防止同一个port的其他GPIO被篡改。
看下GPIO_ToggleBits的具体实现
void GPIO_ToggleBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin) { /* Check the parameters */ assert_param(IS_GPIO_ALL_PERIPH(GPIOx)); GPIOx->ODR ^= GPIO_Pin; }
GPIOx->ODR ^= GPIO_Pin;等于先读取GPIOx->ODR,再修改对应的GPIO的值,最后写入GPIOx->ODR。这就是一个读-写-改的常规操作。这个操作是存在风险的。在我们读取时是存在被其他代码中断的情况的。
举个栗子,假设我们想要修改GPIO0,且bit1是一个重要的GPIO,比如电源的使能引脚。我们读出的GPIOx->ODR是0x0001,也就是bit0为1,bit1为0。这个时候触发了某个中断,在中断里我们需要给某个系统上电,我们将GPIO1拉高了。退出中断继续执行刚才的代码,读出的GPIOx->ODR是0x0001,将bit0清零,也就是将0写入GPIOx->ODR。
那么这个时候问题就大了啊,GPIO1被拉低了,等于没给另外的系统上电。而且这种bug不易察觉,且一般情况下不必现,在客户现场偶现,这就很抓狂了。
所以看到这里大家也就明白了芯片厂家为什么设计GPIOx_BSRR寄存器操作GPIO原因了。GPIOx_BSRR寄存器可以直接操作对应的GPIO,不需要读写改操作,就避免了上面的bug。
当然,你也可以在使用GPIOx->ODR ^= GPIO_Pin;先屏蔽所有中断,操作后再打开所有中断,这是共享资源保护的一种常规操作,但GPIO作为一个使用频率很高的外设,频繁关闭中断是不好的,所以还是使用GPIO_SetBits和GPIO_ResetBits接口为好。
那么GPIOx->ODR 存在即合理,它对应的是GPIO_Write接口,可以一次写入一个port的所有GPIO数据,这对于一些特殊场景是非常有用的,有些场景需要一次性写入同一个port的所有GPIO,类似并口操作,这里效率很高。
2、共享资源的保护
上文我们提到了共享资源保护,linux中采用原子操作,FreeRTOS中一般采用互斥信号量,也称互斥锁。希望大家都要有一种意识,像ODR这样的寄存器也是一种共享资源。
对于共享资源的操作都是需要保护的,如果使用RTOS,对于串口,SPI等这样外设一定要注意共享资源的保护。
像是ODR寄存器,一些在RTOS多个任务都要读写的全局变量都需要进行保护的。在一些读写操作,并不是我们刚看到的GPIOx->ODR ^= GPIO_Pin;操作这么明显。
大家要明确,判断语句也是读操作。
假设在RTOS中有个全局变量event_flg,如果它为1时,在两个任务中都要进行一段操作,比如向语音芯片发送一段语音。发送完毕将event_flg清零,并且这两个任务中的语音不能都播放。伪代码如下
void low_task_entry(void *pvParameters) { while(1) { if(event_flg) { /*发送语音1*/ event_flg =0; } vTaskDelay(500); } } void high_Task_entry(void *pvParameters) { while(1) { if(event_flg) { /*发送语音2*/ event_flg =0; } vTaskDelay(100); } }
那么就存在low_task_entry执行完第5句代码,判断event_flg为1,即执行下一段代码时,被high_Task_entry打断,并且在high_Task_entry中成功播放了语音,且将event_flg清零。
当回到任务low_task_entry时,虽然event_flg已经是0了,但是不好意思,退出low_task_entry已经判断过了,现在回到函数会直接往下执行第6行代码,播放语音。这样神奇的bug就出来了。
那么有同学说,在high_Task_entry播放语音前,将某个全局变量置为1,在low_task_entry播放语音前,再判断这个全局变量。是的,可以的,这是软件层的解决办法,能解决问题就行。
本例的是希望大家体会到判断语句也是读操作,注意共享资源的保护。
大家留意看一些RTOS源码时,某个简单的if判断语句也要进行保护,就是这个原因
3、后记
今天没有特殊的后记内容,之前我看到一个RTOS源码经常对简单的if语句进行保护不懂其中奥秘,今天也算是明白了。
点击查看本文所在的专辑:日常杂谈