1

关于共享资源保护的思考 - Fireflycjd

 1 year ago
source link: https://www.cnblogs.com/Fireflycjd/p/16988663.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

1、引言

先聊聊分享这篇文章的原因,在使用STM32时,我发现对于GPIO输出操作,可以使用GPIOx_ODR寄存器,也可以使用GPIOx_BSRR寄存器。

2123514-20221217101708566-1506248732.png

 对应的标准外设库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判断语句也要进行保护,就是这个原因

今天没有特殊的后记内容,之前我看到一个RTOS源码经常对简单的if语句进行保护不懂其中奥秘,今天也算是明白了。

点击查看本文所在的专辑:日常杂谈


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK