STM32裸机与RTOS环境下的线程安全问题解析及STM32cubeMX中的线程安全策略探讨
STM32线程安全问题
术语“线程” 和“多线程” 适用于裸机和基于RTOS的应用程序,线程安全问题并不只存在于基于RTOS的应用程序中;裸机应用程序中也存在这个问题,在裸机应用程序中,中断服务程序允许调用C库函数。线程安全问题可能出现在多线程应用程序中, 如其中两个线程试图操作共享内存的一个实例, 如malloc()或free()。当然一般也不会在中断中进行malloc(动态内存分配)。但是在开发阶段可能会存在有使用C库函数中的printf函数,那么就会有线程安全问题,C库函数可以进行不那么明显的调用(隐式调用)导致类似的问题。例如,printf()可以调用malloc()。
RTOS应用程序:多个任务或ISR。
在RTOS应用中,并发调用C库函数的情况可能有三个来源:
- 低优先级中断:
①用于对时间不敏感的操作
②用于RTOS的时基
③用于RTOS的任务切换 - 高优先级中断:可能在应用程序中有对执行时间敏感的操作
- 任务切换
裸机应用程序:主循环被ISR中断, 那么中断服务程序也被视为第二个执行线程。
裸机编程的时候通常会勾选Use MicroLIB,通过把printf函数重定向到串口输出的方式打印一些log,当主循环中使用printf时发生中断,在中断中也使用printf可能导致异常。这种异常在RTOS工程中更容易复现。比如使用STM32CubeMX生成FreeRTOS工程,同时创建两个优先级相同的任务,任务每隔1s使用printf函数打印log,使能抢占式调度(configUSE_PREEMPTION)和时间片轮转(configUSE_TIME_SLICING)。
/* definition and creation of led_task */
osThreadDef(led_task, led_func, osPriorityNormal, 0, 256);
led_taskHandle = osThreadCreate(osThread(led_task), NULL);
/* definition and creation of lcd_task */
osThreadDef(lcd_task, lcd_func, osPriorityNormal, 0, 256);
lcd_taskHandle = osThreadCreate(osThread(lcd_task), NULL);
void led_func(void const * argument)
{
const TickType_t xDelay = 1000 / portTICK_PERIOD_MS;
for(;;)
{
LED_R_TOGGLE();
printf("led_func running\r\n");
vTaskDelay(xDelay);
}
}
void lcd_func(void const * argument)
{
const TickType_t xDelay = 1000 / portTICK_PERIOD_MS;
for(;;)
{
LED_R_TOGGLE();
printf("lcd_func running\r\n");
vTaskDelay(xDelay);
}
}
理想情况下的输出应该是两个灯每隔1s翻转状态,两个任务每隔一秒输出一次,实际情况是两个灯每隔1s翻转状态,但是串口输出异常,串口输出如下:
可见,printf不是线程安全函数,在printf前后使用taskENTER_CRITICAL()和taskEXIT_CRITICAL()函数进行临界段代码保护,输出结果就正常。
FreeRTOS任务级临界段代码保护
#define taskENTER_CRITICAL() portENTER_CRITICAL()
#define portENTER_CRITICAL() vPortEnterCritical()
#define taskEXIT_CRITICAL() portEXIT_CRITICAL()
#define portEXIT_CRITICAL() vPortExitCritical()
taskENTER_CRITICAL()和taskEXIT_CRITICAL()函数为任务级进入临界段代码,在进入函数 vPortEnterCritical()以后会首先关闭中断,然后给变量 uxCriticalNesting加一, uxCriticalNesting 是个全局变量,用来记录临界段嵌套次数的。函数 vPortExitCritical()是退出临界段调用的,函数每次将 uxCriticalNesting 减一,只有当 uxCriticalNesting 为 0 的时候才会调用函数 portENABLE_INTERRUPTS()使能中断。这样保证了在有多个临界段代码的时候不会因为某一个临界段代码的退出而打乱其他临界段的保护,只有所有的临界段代码都退出以后才会使能中断。最终调用的函数如下:
void vPortEnterCritical( void )
{
portDISABLE_INTERRUPTS();
uxCriticalNesting++;
if( uxCriticalNesting == 1 )
{
configASSERT( ( portNVIC_INT_CTRL_REG & portVECTACTIVE_MASK ) == 0 );
}
}
void vPortExitCritical( void )
{
configASSERT( uxCriticalNesting );
uxCriticalNesting--;
if( uxCriticalNesting == 0 )
{
portENABLE_INTERRUPTS();
}
}
其中,portDISABLE_INTERRUPTS和portENABLE_INTERRUPTS定义如下:
#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()
#define portENABLE_INTERRUPTS() vPortSetBASEPRI( 0 )
static portFORCE_INLINE uint32_t ulPortRaiseBASEPRI( void )
{
uint32_t ulReturn, ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY;
__asm
{
/* Set BASEPRI to the max syscall priority to effect a critical
section. */
mrs ulReturn, basepri
msr basepri, ulNewBASEPRI
dsb
isb
}
return ulReturn;
}
static portFORCE_INLINE void vPortSetBASEPRI( uint32_t ulBASEPRI )
{
__asm
{
/* Barrier instructions are not used as this function is only used to
lower the BASEPRI value. */
msr basepri, ulBASEPRI
}
}
假设stm32中断优先级分组设置为4,那就是4位抢占优先级,没有子优先级,即0-15,因此宏configLIBRARY_LOWEST_INTERRUPT_PRIORITY定义了最低优先级为15,configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY定义为5,也就是优先级高于5(数值小于5)的中断不归FreeRTOS管理。
vPortRaiseBASEPRI函数的作用是屏蔽所有低于configMAX_SYSCALL_INTERRUPT_PRIORITY(数值大于configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY)宏的中断。
#define configPRIO_BITS 4
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
FreeRTOS中断级临界段代码保护
#define taskENTER_CRITICAL_FROM_ISR() portSET_INTERRUPT_MASK_FROM_ISR()
#define portSET_INTERRUPT_MASK_FROM_ISR() ulPortRaiseBASEPRI()
#define taskEXIT_CRITICAL_FROM_ISR( x ) portCLEAR_INTERRUPT_MASK_FROM_ISR( x )
#define portCLEAR_INTERRUPT_MASK_FROM_ISR(x) vPortSetBASEPRI(x)
taskENTER_CRITICAL_FROM_ISR()和taskEXIT_CRITICAL_FROM_ISR( x )函数为中断级进入临界段代码,可以看到是没有嵌套处理,直接操作BASEPRI寄存器实现。
STM32cubeMX中的线程安全策略
使用STM32cubeMX生成工程时,可选的线程安全策略有五种:
如果选择Default,裸机应用会自动选择策略2,RTOS应用会自动选择策略4。
对于单核项目,策略1会额外生成stm32_lock.h、 armlib_lock_glue.c和stm32 _lock_user.h三个文件;策略2/3/4/5会额外生成stm32_lock.h、 armlib_lock_glue.c两个文件;对于多核项目,每个核引用相同的文件(stm32_lock.h和armlib_lock_glue.c), 每个核使用一个单独的文件(stm32_lock_user.h)。


typedef struct
{
uint32_t basepri[STM32_LOCK_MAX_NESTED_LEVELS];
uint8_t nesting_level;
} LockingData_t;
然而, 策略4高优先级中断也是不安全的(数值小于configMAX_SYSCALL_INTERRUPT_PRIORITY宏的中断)。高优先级中断仍然可能发生, 代价是不安全的并发C库函数调用。

FreeRTOS中动态内存分配
FreeRTOS中动态内存分配使用pvPortMalloc()和vPortFree()函数,这两个函数在操作内存前后分别使用vTaskSuspendAll()和xTaskResumeAll()函数来暂停和恢复所有任务,和上述策略5相同。
作者:~狂想家~