一、临界区
临界区是指一段代码,在执行过程中不允许被中断(例如:任务切换或硬件中断)。 临界区的作用是确保对共享资源的操作是原子性的,避免多个任务或中断同时操作共享资源导致数据错误。 对于实时系统来说,进入临界区时需要快速完成操作,以免影响系统的实时性。 在 FreeRTOS 中,可以通过以下方式进入和退出临界区: - 进入临界区:使用 taskENTER_CRITICAL()
函数,FreeRTOS 会关闭所有中断,暂停任务调度。这确保在临界区内的代码执行过程中,不会有其他任务或中断打断当前的任务。 - 退出临界区:使用 taskEXIT_CRITICAL()
函数恢复中断和任务调度。 注意: 在临界区内应避免进行长时间的操作,尤其是像延时、等待等操作,因为这可能会影响系统的实时性能。理想情况下,进入临界区后,应尽快完成任务并退出临界区。 中断中进入临界区
UBaseType_t uxSavedInterruptStatus;
uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR ( ) ;
portCLEAR_INTERRUPT_MASK_FROM_ISR ( uxSavedInterruptStatus) ;
- 举例说明
int __io_putchar ( int ch)
{
UBaseType_t uxSavedInterruptStatus;
uxSavedInterruptStatus = portSET_INTERRUPT_MASK_FROM_ISR ( ) ;
HAL_UART_Transmit ( & huart1, ( uint8_t * ) & ch, 1 , 1 ) ;
portCLEAR_INTERRUPT_MASK_FROM_ISR ( uxSavedInterruptStatus) ;
return ch;
}
二、时间管理
FreeRTOS 的时间管理基于硬件定时器 SysTick,这是一个硬件定时器,它每隔 1 毫秒产生一次中断,用来增加系统的时间计数 ticks。 获取当前时间
xTaskGetTickCount():用于普通任务获取当前系统的 tick 计数,即系统启动后经过的时间(以 tick 为单位)。1 个 tick 通常等于 1 毫秒,具体取决于配置。 xTaskGetTickCountFromISR():在中断服务程序(ISR)中使用的版本,用来获取当前 tick 值。 任务睡眠
FreeRTOS 提供了 osDelay() 函数,可以让任务暂停执行一段时间 但是,osDelay() 可能不够精确,因为它会让任务在执行完工作后再睡眠指定时间,导致延迟时间包括了任务执行时间。 更精确的延时
FreeRTOS 提供了 vTaskDelayUntil() 函数,它可以保证任务每隔固定时间间隔执行
TickType_t lastWakeTime = xTaskGetTickCount ( ) ;
for ( ; ; )
{
do_something ( ) ;
vTaskDelayUntil ( & lastWakeTime, 500 ) ;
}
vTaskDelayUntil ( ) 基于上一次唤醒时间来计算下次睡眠的时长,因此能保证任务周期精确,而不会受任务处理时间的影响。
三、堆栈管理
在嵌入式系统中,内存分为多个区域,包括代码段、全局变量区、堆区和栈区。
栈:用于存储局部变量、函数参数、返回地址等。栈的空间是有限的。 堆:用于动态分配的内存,例如通过 malloc() 分配的空间。
在裸机开发中(没有操作系统的情况下),堆和栈是共享的。在 FreeRTOS 中,每个任务都有自己独立的堆栈空间,而这些堆栈空间是从系统的堆中分配的。 freeRTOS的栈空间 从堆区分配 栈: 自动从heap分配,自动释放不需要我们关心 在 STM32 开发中,使用 CubeIDE 配置 FreeRTOS 堆大小。可以通过以下路径设置堆大小:
CubeIDE ( 硬件配置界面) -> System View -> Middleware -> FreeRTOS -> Config Parameters -> TOTAL_HEAP_SIZE
要填入一个完整的数, 不要 + - * / , 而且要随机一点, 不要 是512 的整数倍
动态内存分配策略
FreeRTOS 提供了四种内存分配策略,常用的是带有碎片整理功能的动态分配策略。即 FreeRTOS 提供了 pvPortMalloc() 和 vPortFree() 这两个函数,类似于标准 C 的 malloc() 和 free(),用来动态分配和释放内存。
如何检查剩余堆内存
可以使用 xPortGetMinimumEverFreeHeapSize() 函数来获取最小时刻的堆剩余空间。如果堆内存过小,说明系统内存分配有风险,可能需要调整配置。
四. 调度策略
FreeRTOS 采用的是 抢占式调度 和 时间片轮转 的结合:
抢占式:只要有更高优先级的任务,系统会立即中断当前任务,执行高优先级任务。 时间片轮转:当多个任务的优先级相同时,系统会给每个任务分配一个固定的时间片,轮流执行。
任务的切换时机: 通常在中断发生时,系统会检查是否有更高优先级的任务需要运行,如果有则进行任务切换。
五、FreeRTOS调试
volatile 修饰共享变量:如果多个任务共享某个变量,为了确保每次都从内存中读取最新的值,而不是使用缓存,应该使用 volatile 关键字修饰变量。 中断优先级的设置:使用 FreeRTOS 时,中断的优先级需要降低到一定范围内,否则可能与 FreeRTOS 的调度机制产生冲突。可以在 CubeIDE 中配置中断优先级。 初始化顺序:在 main() 函数中,初始化各个模块时要注意顺序。例如,FreeRTOS 的启动必须在所有硬件初始化完成后进行。 中断中的 API 调用:在中断服务程序中调用 FreeRTOS 的 API 时,要使用以 FromISR 结尾的函数,例如 xQueueSendFromISR() 等。 断言与错误处理:FreeRTOS 提供了一个断言机制,可以在出现错误时触发断言。例如,在 FreeRTOSConfig.h 中定义:
# define configASSERT ( x) if ( ( x) == 0 ) { taskDISABLE_INTERRUPTS ( ) ; printf ( "%s-%d\r\n" , __func__ , __LINE__ ) ; for ( ; ; ) ; }
当系统检测到错误时,会进入断言并输出错误信息,帮助定位问题
栈和堆溢出检测
栈溢出检测: FreeRTOS 提供了栈溢出检测功能,当某个任务的栈溢出时,会调用栈溢出钩子函数:
void vApplicationStackOverflowHook ( TaskHandle_t xTask, char * pcTaskName)
{
printf ( "Stack overflow in task: %s\r\n" , pcTaskName) ;
}
要启用栈溢出检测功能,需要设置 configCHECK_FOR_STACK_OVERFLOW = 2 。
cubide-> middleware-- > config parameter-- > configCHECK_FOR_STACK_OVERFLOW -- > option 2
FreeRTOS 也支持堆溢出检测。当内存分配失败时,系统会调用堆溢出钩子函数
void vApplicationMallocFailedHook ( void )
{
printf ( "Heap memory allocation failed!\r\n" ) ;
}
启用堆溢出检测
configUSE_MALLOC_FAILED_HOOK= 1
方法: cubide-> middleware-- > config parameter-- > configUSE_MALLOC_FAILED_HOOK= 1
六、异常处理
在 FreeRTOS 和嵌入式系统中,异常(如中断错误、数据异常、硬件异常等)是不可避免的。为了方便调试和快速定位问题,我们可以在常见的异常处理函数中加上打印日志。 STM32 的异常处理函数通常实现在 core/stm32f1xx_it.c 文件中,包括以下几个关键函数:
HardFault_Handler:处理硬件错误(例如非法内存访问)。 MemManage_Handler:内存管理错误(例如堆栈溢出)。 UsageFault_Handler:处理非法指令、未对齐的内存访问等错误。 DebugMon_Handler:用于监控调试事件。 为了调试这些异常,可以在每个处理函数中添加调试打印,以便在异常发生时输出详细的错误信息,例如当前的函数名和出错的位置。
void HardFault_Handler ( void )
{
printf ( "HardFault occurred!\n" ) ;
while ( 1 ) ;
}
void MemManage_Handler ( void )
{
printf ( "Memory management fault!\n" ) ;
while ( 1 ) ;
}
void UsageFault_Handler ( void )
{
printf ( "Usage fault!\n" ) ;
while ( 1 ) ;
}
七、堆栈信息打印
在 FreeRTOS 中,每个任务都有其独立的栈空间。如果某个任务的栈空间不足,会引发栈溢出,导致系统崩溃。因此,监控任务的栈使用情况非常重要。 步骤1:获取任务栈的剩余大小
可以使用 uxTaskGetStackHighWaterMark() 函数获取某个任务的栈剩余量,单位是 4 字节。这个函数会返回任务曾经使用过的最小栈空间,即可以用来评估是否需要调整任务的栈大小。
UBaseType_t freestacksz;
freestacksz = uxTaskGetStackHighWaterMark ( task1Handle) ;
printf ( "Task1 remaining stack: %lu bytes\n" , freestacksz * 4 ) ;
默认情况下,这个功能是关闭的。可以通过 CubeIDE 打开:
CubeIDE -> Middleware -> FreeRTOS -> Config Parameters -> Include uxTaskGetStackHighWaterMark
步骤2:任务栈分配调整
创建任务时,建议给任务分配稍大的栈空间,初始值可以是 128 * 3(表示 128 字×3),并通过 uxTaskGetStackHighWaterMark() 动态调整,确保每个任务有足够的栈空间。
八、系统任务状态打印
可以打印系统中所有任务的运行状态,方便整体调试。 获取所有任务的状态:使用 vTaskList() 函数可以获取当前系统中所有任务的运行信息,包括任务名、任务状态、任务优先级、剩余栈大小等。
char * pbuffer = ( char * ) calloc ( 1 , 512 ) ;
printf ( "\r\n-name---------stat---Priority--lastword---ID---\r\n" ) ;
vTaskList ( pbuffer) ;
printf ( "%s" , pbuffer) ;
printf ( "-X:run-B:block-R:ready-D:del-S:suspend-Heap mini %d-\r\n" , xPortGetMinimumEverFreeHeapSize ( ) ) ;
free ( pbuffer) ;
configUSE_TRACE_FACILITY 和 configUSE_STATS_FORMATTING_FUNCTIONS 使能
示例输出
- name-- -- -- -- - stat-- - Priority-- lastword-- - ID-- -
taskPrint X 0 129 5
IDLE R 0 106 6
taskStackDetect B 0 83 2
taskStackDetect B 0 143 3
taskStackDetect B 0 143 4
defaultTask B 3 97 1
- X: run- B: block- R: ready- D: del- S: suspend- Heap mini 3592 -
X 代表正在运行的任务
B 代表阻塞状态
R 代表就绪状态
S 代表挂起状态
D 代表删除的任务
输出的堆内存剩余量可以通过 xPortGetMinimumEverFreeHeapSize ( ) 获取,帮助我们监控系统堆的使用情况。