说明
FreeRTOS学习笔记
搭建STM32F4HAL库开发环境
- GitHub上ST官方库:https://github.com/STMicroelectronics/STM32CubeF4
由于github
仓库使用git submodule
命令创建的,不能直接下载zip
压缩包,需要使用git
指令下载:
git clone --recursive https://github.com/STMicroelectronics/STM32CubeF4.git
git pull
git submodule update --init --recursive
硬件开发环境:
- 正点原子
STM32F429IGT6
开发板 keil5
STM32F429IGT6
系列MCU
:
- 内核:ARM Cortex-M4
- 架构:ARMv7-M
- 支持:Thumb-2指令集、单精度FPU,DSP指令
- Flash:1M
- RAM:256K(192K+64K CCM)
创建文件夹
-
cminc:存放
STM32CubeF4\Drivers\CMSIS\Include
路劲下的头文件,只需要4个头文件:cmsis_armclang.h
,cmsis_compiler.h
,cmsis_version.h
,core_cm4.h
,mpu_armv7.h
,cmsis_armcc.h
注意keil5
中使用版本5编译器会调用cmsis_armcc.h
头文件,使用版本6编译器会调用cmsis_armclang.h
头文件,所以最好两个文件都包含
-
halsrc:存放
STM32CubeF4\Drivers\STM32F4xx_HAL_Driver\Src
下面的库文件 -
halinc:存放
STM32CubeF4\Drivers\STM32F4xx_HAL_Driver\Inc
下面的头文件 -
cmsrc:存放
STM32CubeF4\Drivers\CMSIS\Device\ST\STM32F4xx\Source\Templates\
路径下的system_stm32f4xx.c
文件 -
startup:存放
STM32CubeF4\Drivers\CMSIS\Device\ST\STM32F4xx\Source\Templates\arm
下面的启动文件,选择startup_stm32f429xx.s
启动文件 -
config:
- 存放
STM32CubeF4\Drivers\CMSIS\Device\ST\STM32F4xx\Include\
路径下的system_stm32f4xx.h
,stm32f4xx.h
,stm32f429xx.h
文件。 - 存放
STM32CubeF4\Projects\STM32F429I-Discovery\Templates\Inc\
路径下的stm32f4xx_hal_conf.h
,stm32f4xx_it.h
,main.h
文件
- 存放
-
user:存放
STM32CubeF4\Projects\STM32F429I-Discovery\Templates\Src\
路径下的main.c
,stm32f4xx_it.c
,stm32f4xx_hal_msp.c
文件
至此,所需文件全部找到,使用keil5开始添加文件
新建一个keil5工程
创建完成之后的空项目如下图:
添加相应文件:
添加相应头文件:
添加宏定义:
USE_HAL_DRIVER,STM32F429xx
修改晶振频率,开发板使用的25M晶振,默认是8M,需要修改,否则串口输出会乱码。找到 stm32f4xx_hal_conf.h
文件,修改#define HSE_VALUE (25000000U)
编译程序
测试程序
修改时钟频率
最大为180M,主要修改时钟分频系数
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Configure the main internal regulator output voltage
*/
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 15;
RCC_OscInitStruct.PLL.PLLN = 216;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 4;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Activate the Over-Drive mode
*/
if (HAL_PWREx_EnableOverDrive() != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
{
Error_Handler();
}
}
添加LED
led.c
文件程序
#include "led.h"
void LED_Init(void)
{
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_Initure.Pin=GPIO_PIN_0|GPIO_PIN_1;
GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP;
GPIO_Initure.Pull=GPIO_PULLUP;
GPIO_Initure.Speed=GPIO_SPEED_HIGH;
HAL_GPIO_Init(GPIOB,&GPIO_Initure);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_1,GPIO_PIN_SET);
}
led.h
文件程序:
#ifndef __LED_H__
#define __LED_H__
#include "main.h"
void LED_Init(void);
#endif
添加串口
uart.c文件程序:
#include "uart.h"
#include <stdio.h>
UART_HandleTypeDef UART1_Handler;
void uart_init(uint32_t bound)
{
UART1_Handler.Instance=USART1;
UART1_Handler.Init.BaudRate=bound;
UART1_Handler.Init.WordLength=UART_WORDLENGTH_8B;
UART1_Handler.Init.StopBits=UART_STOPBITS_1;
UART1_Handler.Init.Parity=UART_PARITY_NONE;
UART1_Handler.Init.HwFlowCtl=UART_HWCONTROL_NONE;
UART1_Handler.Init.Mode=UART_MODE_TX_RX;
HAL_UART_Init(&UART1_Handler);
}
void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
GPIO_InitTypeDef GPIO_Initure;
if(huart->Instance==USART1)
{
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_USART1_CLK_ENABLE();
GPIO_Initure.Pin=GPIO_PIN_9;
GPIO_Initure.Mode=GPIO_MODE_AF_PP;
GPIO_Initure.Pull=GPIO_PULLUP;
GPIO_Initure.Speed=GPIO_SPEED_FAST;
GPIO_Initure.Alternate=GPIO_AF7_USART1;
HAL_GPIO_Init(GPIOA,&GPIO_Initure);
GPIO_Initure.Pin=GPIO_PIN_10;
HAL_GPIO_Init(GPIOA,&GPIO_Initure);
__HAL_UART_DISABLE_IT(huart,UART_IT_TC);
#if EN_USART1_RX
__HAL_UART_ENABLE_IT(huart,UART_IT_RXNE);
HAL_NVIC_EnableIRQ(USART1_IRQn);
HAL_NVIC_SetPriority(USART1_IRQn,3,3);
#endif
}
}
//重定义fputc函数
int fputc(int ch, FILE *f)
{
while((USART1->SR&0X40)==0);//循环发送,直到发送完毕
USART1->DR = (uint8_t) ch;
return ch;
}
uart.h文件程序:
#ifndef __UART_H__
#define __UART_H__
#include "main.h"
void uart_init(uint32_t bound);
#endif
添加文件之后完整工程
main函数测试
/**
******************************************************************************
* @file Templates/Src/main.c
* @author MCD Application Team
* @brief Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2017 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "led.h"
#include "uart.h"
#include <stdio.h>
/** @addtogroup STM32F4xx_HAL_Examples
* @{
*/
/** @addtogroup Templates
* @{
*/
/* Private typedef -----------------------------------------------------------*/
/* Private define ------------------------------------------------------------*/
/* Private macro -------------------------------------------------------------*/
/* Private variables ---------------------------------------------------------*/
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
void Error_Handler(void);
/* Private functions ---------------------------------------------------------*/
/**
* @brief Main program
* @param None
* @retval None
*/
int main(void)
{
/* STM32F4xx HAL library initialization:
- Configure the Flash prefetch, Flash preread and Buffer caches
- Systick timer is configured by default as source of time base, but user
can eventually implement his proper time base source (a general purpose
timer for example or other time source), keeping in mind that Time base
duration should be kept 1ms since PPP_TIMEOUT_VALUEs are defined and
handled in milliseconds basis.
- Low Level Initialization
*/
HAL_Init();
/* Configure the System clock to 180 MHz */
SystemClock_Config();
/* Add your application code here
*/
LED_Init();
uart_init(115200);
printf("Hello World!\r\n");
/* Infinite loop */
while (1)
{
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_1,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_SET);
HAL_Delay(500);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_1,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_RESET);
HAL_Delay(500);
}
}
/**
* @brief System Clock Configuration
* The system Clock is configured as follow :
* System Clock source = PLL (HSE)
* SYSCLK(Hz) = 180000000
* HCLK(Hz) = 180000000
* AHB Prescaler = 1
* APB1 Prescaler = 4
* APB2 Prescaler = 2
* HSE Frequency(Hz) = 8000000
* PLL_M = 8
* PLL_N = 360
* PLL_P = 2
* PLL_Q = 7
* VDD(V) = 3.3
* Main regulator output voltage = Scale1 mode
* Flash Latency(WS) = 5
* @param None
* @retval None
*/
static void SystemClock_Config(void)
{
RCC_ClkInitTypeDef RCC_ClkInitStruct;
RCC_OscInitTypeDef RCC_OscInitStruct;
/* Enable Power Control clock */
__HAL_RCC_PWR_CLK_ENABLE();
/* The voltage scaling allows optimizing the power consumption when the device is
clocked below the maximum system frequency, to update the voltage scaling value
regarding system frequency refer to product datasheet. */
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
/* Enable HSE Oscillator and activate PLL with HSE as source */
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 25;
RCC_OscInitStruct.PLL.PLLN = 360;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 8;
if(HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
/* Initialization Error */
Error_Handler();
}
if(HAL_PWREx_EnableOverDrive() != HAL_OK)
{
/* Initialization Error */
Error_Handler();
}
/* Select PLL as system clock source and configure the HCLK, PCLK1 and PCLK2
clocks dividers */
RCC_ClkInitStruct.ClockType = (RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2);
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
if(HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
{
/* Initialization Error */
Error_Handler();
}
}
/**
* @brief This function is executed in case of error occurrence.
* @param None
* @retval None
*/
static void Error_Handler(void)
{
/* User may add here some code to deal with this error */
while(1)
{
}
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t* file, uint32_t line)
{
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* Infinite loop */
while (1)
{
}
}
#endif
/**
* @}
*/
/**
* @}
*/
可以看见LED闪烁,串口打印输出。
移植FreeRTOS
TIM6替代滴答时钟
在上一个HAL库基础上添加FreeRTOS操作系统,由于FreeRTOS使用了滴答时钟,默认HAL库也使用了滴答时钟,需要将原来HAL库换成定时器方式。
第一步:实现TIM6定时1ms
tim.c
文件
#include "tim.h"
#include "stm32f4xx_hal.h"
#include "stm32f4xx_hal_tim.h"
//
TIM_HandleTypeDef htim6;
void tim6_init(void)
{
TIM_MasterConfigTypeDef sMasterConfig = {0};
htim6.Instance = TIM6;
htim6.Init.Prescaler = 1000-1;
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
htim6.Init.Period = 90-1;
htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_Base_Init(&htim6) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
}
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle)
{
if(tim_baseHandle->Instance==TIM6)
{
/* USER CODE BEGIN TIM6_MspInit 0 */
/* USER CODE END TIM6_MspInit 0 */
/* TIM6 clock enable */
__HAL_RCC_TIM6_CLK_ENABLE();
/* TIM6 interrupt Init */
HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn);
/* USER CODE BEGIN TIM6_MspInit 1 */
/* USER CODE END TIM6_MspInit 1 */
}
}
void HAL_TIM_Base_MspDeInit(TIM_HandleTypeDef* tim_baseHandle)
{
if(tim_baseHandle->Instance==TIM6)
{
/* USER CODE BEGIN TIM6_MspDeInit 0 */
/* USER CODE END TIM6_MspDeInit 0 */
/* Peripheral clock disable */
__HAL_RCC_TIM6_CLK_DISABLE();
/* TIM6 interrupt Deinit */
HAL_NVIC_DisableIRQ(TIM6_DAC_IRQn);
/* USER CODE BEGIN TIM6_MspDeInit 1 */
/* USER CODE END TIM6_MspDeInit 1 */
}
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM6)
{
// 每 1 ms 触发一次
HAL_IncTick(); // 可以用来替代 SysTick
}
}
tim.h
文件:
#ifndef __TIM_H__
#define __TIM_H__
#include "main.h"
extern TIM_HandleTypeDef htim6;
void tim6_init(void);
#endif
同时需要注释掉 stm32f4xx_it.c 文件中
void SysTick_Handler(void)
{
// HAL_IncTick();
}
第二步:在main.c中开启定时器TIM6
HAL_TIM_Base_Start_IT(&htim6);
HAL_Delay
函数中,延迟多了1ms,需要修改,创建一个delay.c
和delay.h
文件
delay.c
文件内容:
#include "delay.h"
void HAL_Delay(uint32_t Delay)
{
uint32_t tickstart = HAL_GetTick();
uint32_t wait = Delay - 1;
/* Add a freq to guarantee minimum wait */
if (wait < HAL_MAX_DELAY)
{
wait += (uint32_t)(uwTickFreq);
}
while((HAL_GetTick() - tickstart) < wait)
{
}
}
delay.h
文件内容:
#ifndef __DELAY_H__
#define __DELAY_H__
#include "main.h"
#endif
在中断文件 stm32f4xx_it.c
文件中,添加tim6
中断处理函数:
void TIM6_DAC_IRQHandler(void)
{
/* USER CODE BEGIN TIM6_DAC_IRQn 0 */
/* USER CODE END TIM6_DAC_IRQn 0 */
HAL_TIM_IRQHandler(&htim6);
/* USER CODE BEGIN TIM6_DAC_IRQn 1 */
/* USER CODE END TIM6_DAC_IRQn 1 */
}
main.c
函数测试:
/**
******************************************************************************
* @file Templates/Src/main.c
* @author MCD Application Team
* @brief Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2017 STMicroelectronics.
* All rights reserved.
*
* This software is licensed under terms that can be found in the LICENSE file
* in the root directory of this software component.
* If no LICENSE file comes with this software, it is provided AS-IS.
*
******************************************************************************
*/
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "led.h"
#include "uart.h"
#include <stdio.h>
#include "tim.h"
#include "delay.h"
/** @addtogroup STM32F4xx_HAL_Examples
* @{
*/
/** @addtogroup Templates
* @{
*/
/* Private typedef -----------------------------------------------------------*/
/* Private define ------------------------------------------------------------*/
/* Private macro -------------------------------------------------------------*/
/* Private variables ---------------------------------------------------------*/
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* Private functions ---------------------------------------------------------*/
/**
* @brief Main program
* @param None
* @retval None
*/
int main(void)
{
/* STM32F4xx HAL library initialization:
- Configure the Flash prefetch, Flash preread and Buffer caches
- Systick timer is configured by default as source of time base, but user
can eventually implement his proper time base source (a general purpose
timer for example or other time source), keeping in mind that Time base
duration should be kept 1ms since PPP_TIMEOUT_VALUEs are defined and
handled in milliseconds basis.
- Low Level Initialization
*/
HAL_Init();
/* Configure the System clock to 180 MHz */
SystemClock_Config();
/* Add your application code here
*/
LED_Init();
uart_init(115200);
tim6_init();
HAL_TIM_Base_Start_IT(&htim6);
printf("Hello World!\r\n");
/* Infinite loop */
while (1)
{
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_1,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_SET);
HAL_Delay(500);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_1,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_RESET);
HAL_Delay(500);
}
}
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/** Configure the main internal regulator output voltage
*/
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
/** Initializes the RCC Oscillators according to the specified parameters
* in the RCC_OscInitTypeDef structure.
*/
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 15;
RCC_OscInitStruct.PLL.PLLN = 216;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 4;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/** Activate the Over-Drive mode
*/
if (HAL_PWREx_EnableOverDrive() != HAL_OK)
{
Error_Handler();
}
/** Initializes the CPU, AHB and APB buses clocks
*/
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
{
Error_Handler();
}
}
/**
* @brief This function is executed in case of error occurrence.
* @param None
* @retval None
*/
void Error_Handler(void)
{
/* User may add here some code to deal with this error */
__disable_irq();
while(1)
{
}
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t* file, uint32_t line)
{
/* User can add his own implementation to report the file name and line number,
ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* Infinite loop */
while (1)
{
}
}
#endif
/**
* @}
*/
/**
* @}
*/
/* USER CODE BEGIN 4 */
测试LED闪烁,串口输出正常。
下载FreeRTOS
官方下载地址:https://www.freertos.org/
目前只有两个版本,下载最新版本 FreeRTOS 202406.01 LTS
版本
添加FreeRTOS文件
找到FreeRTOSv202406.01-LTS\FreeRTOS-LTS\FreeRTOS\FreeRTOS-Kernel\
目录
文件名 | 描述 |
---|---|
include目录 | 公共头文件目录 |
portable目录 | 移植层代码,针对不同的CPU架构和编译器,包含上下文切换,启动代码,临界区管理,tick配置 |
croutine.c | 协程支持(co-routines),轻量级的任务机制,不推荐在项目中使用 |
event_groups.c | 事件标志组,允许任务等待多个事件的组合(逻辑与/或) |
list.c | 内核内部使用的双向链表实现,用于任务就绪链表、延时链表、事件链表等,不直接给应用层用 |
queue.c | 实现队列、信号量(包括二值信号量、计数信号量)、互斥锁、消息队列,是任务之间通信和同步的核心 |
stream_buffer.c | 流缓冲区,用于任务或者中断与任务之间传递字节流数据,典型应用场景:UART接收缓冲 |
tasks,c | 核心任务管理模块,包含任务的创建、删除、调度、阻塞、挂起等逻辑,调度器(scheduler)的主要实现就在tasks.c里 |
timers.c | 软件定时器服务,提供独立于硬件定时器的,基于RTOS tick的定时回调机制 |
- include目录所有头文件都需要
- portable目录下,针对STM32F429IGT6需要MemMang目录和RVDS中的ARM_CM4F目录
- croutine.c、event_groups.c、list.c、queue.c、stream_buffer.c、tasks,c、timers.c都添加进来
- 还需要STM32CubeF4库文件中的CMSIS_RTOS_V2目录下的文件,路径
STM32CubeF4\Middlewares\Third_Party\FreeRTOS\Source\CMSIS_RTOS_V2
- 还需要一个配置文件
FreeRTOSConfig.h
,路径FreeRTOSv202406.01-LTS\FreeRTOS-LTS\FreeRTOS\FreeRTOS-Kernel\examples\template_configuration\FreeRTOSConfig.h
CMSIS-RTOS_V2 是API的适配层,有ARM CMSIS定义,ST在Cube库里面提供,作用是把FreeRTOS的API映射到CMSIS-RTOS v2的接口,比如 osThreadNew()–>xTaskCreate(),osSemaphoreAcquire()–>xSemaphoreTake() 等
添加之后的文件目录:
开始添加文件
新版编译错误
需要使用ARM Compiler 5
版本,不然编译会报大量不兼容的错误,目前FreeRTOS
不支持ARM Compiler 6
。
修改FreeRTOSConfig.h
//一般这类32位MCU都用32bit,不要用16bit,容易溢出
#define configTICK_TYPE_WIDTH_IN_BITS TICK_TYPE_WIDTH_32_BITS
/*-----------------------------------------------------------
* CPU & Tick settings
*----------------------------------------------------------*/
#define configCPU_CLOCK_HZ (180000000)
#define configTICK_RATE_HZ ((TickType_t)1000)
#define configUSE_16_BIT_TICKS 0
#define configTICK_TYPE_WIDTH_IN_BITS TICK_TYPE_WIDTH_32_BITS
/* Task priorities */
#define configMAX_PRIORITIES 56 /* CMSIS-RTOS2 Thread API 必须 = 56 */
#define configMINIMAL_STACK_SIZE ((uint16_t)128)
#define configTOTAL_HEAP_SIZE ((size_t)(20 * 1024))
/* Cortex-M specific */
#define configPRIO_BITS __NVIC_PRIO_BITS /* STM32F429 默认 4 位 */
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15 //中断最低优先级
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 //系统可管理的最高中断优先级
#define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configUSE_PREEMPTION 1
#define configUSE_TIME_SLICING 1
#define configIDLE_SHOULD_YIELD 1
/*-----------------------------------------------------------
* Optional FreeRTOS API
*----------------------------------------------------------*/
/* Semaphore / Mutex */
#define INCLUDE_vSemaphoreDelete 1
#define INCLUDE_vSemaphoreCreateBinary 1
#define INCLUDE_xSemaphoreGetMutexHolder 1
/* Thread / Task */
#define INCLUDE_vTaskPrioritySet 1
#define INCLUDE_vTaskDelay 1
#define INCLUDE_vTaskDelete 1
#define INCLUDE_vTaskSuspend 1
#define INCLUDE_uxTaskGetStackHighWaterMark 1
#define INCLUDE_eTaskGetState 1
/* Timer / Event API */
#define INCLUDE_vTaskDelayUntil 1
#define INCLUDE_xTimerPendFunctionCall 1
/* Trace / Debug */
#define configUSE_TRACE_FACILITY 1
port.c(核心文件包含)
#include <stdint.h>
#include "stm32f4xx.h"
CMSIS-RTOS2 SysTick 处理
将cmsis_os2.c
文件中SysTick_Handler
保留,FreeRTOS
会用,删除 stm32f4xx_it.c
文件中的SysTick_Handler
FreeRTOSConfig.h 中的宏重定向
将 stm32f4xx_it.c
文件中的SVC_Handler
和 PendSV_Handler
函数注释掉,在FreeRTOSConfig.h
重新定向:
//FreeRTOSConfig.h
#define vPortSVCHandler SVC_Handler
#define xPortPendSVHandler PendSV_Handler
CMSIS-RTOS2 钩子函数签名
将 cmsis_os2.c
文件中的 vApplicationStackOverflowHook
函数参数由原来的 void vApplicationStackOverflowHook(TaskHandle_t xTask, signed char *pcTaskName)
改为 void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
,即signed char
改为 char
,参数类型必须与 task.h
完全一致
void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName)
{
(void)xTask;
(void)pcTaskName;
while(1);
}
void vApplicationMallocFailedHook(void)
{
while(1);
}
freertos_os2.h 宏定义
添加CMSIS_device_header
宏定义
#ifndef CMSIS_device_header
#define CMSIS_device_header "stm32f4xx.h" // STM32F429 对应头文件
#endif
#include CMSIS_device_header
error: unknown type name ‘__forceinline’
🔎 背景
- __forceinline 不是 C 标准关键字,而是 某些编译器特定的内联修饰符。
- 在 FreeRTOS 新版(尤其是 LTS 内核)里,引入了一个宏 FORCE_INLINE,内部可能定义成了 __forceinline。
- 但是 Keil ARMCC/ARMCLANG 编译器 不支持 __forceinline。
✅ 解决办法
使用 ARM Compiler 5版本编译,否则会报错
增加freertos.c文件
在 main.c
中添加FreeRTOS 代码:
int main(void)
{
/* STM32F4xx HAL library initialization:
- Configure the Flash prefetch, Flash preread and Buffer caches
- Systick timer is configured by default as source of time base, but user
can eventually implement his proper time base source (a general purpose
timer for example or other time source), keeping in mind that Time base
duration should be kept 1ms since PPP_TIMEOUT_VALUEs are defined and
handled in milliseconds basis.
- Low Level Initialization
*/
HAL_Init();
/* Configure the System clock to 180 MHz */
SystemClock_Config();
/* Add your application code here
*/
LED_Init();
uart_init(115200);
tim6_init();
HAL_TIM_Base_Start_IT(&htim6);
printf("Hello World!\r\n");
/* Init scheduler */
osKernelInitialize();
/* Call init function for freertos objects (in cmsis_os2.c) */
MX_FREERTOS_Init();
/* Start scheduler */
osKernelStart();
/* Infinite loop */
while (1)
{
//不会执行到此处
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_1,GPIO_PIN_RESET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_SET);
HAL_Delay(500);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_1,GPIO_PIN_SET);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_RESET);
HAL_Delay(500);
}
}
freertos.c文件内容:
/* Includes ------------------------------------------------------------------*/
#include "FreeRTOS.h"
#include "task.h"
#include "main.h"
#include "cmsis_os.h"
#include <stdio.h>
void MX_FREERTOS_Init(void);
void StartDefaultTask(void *argument);
osThreadId_t defaultTaskHandle;
const osThreadAttr_t defaultTask_attributes = {
.name = "defaultTask",
.stack_size = 128,
.priority = (osPriority_t) osPriorityNormal,
};
void MX_FREERTOS_Init(void) {
defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);
if (defaultTaskHandle == NULL) {
printf("osThreadNew error!\r\n");
}
}
/* USER CODE BEGIN Header_StartDefaultTask */
/**
* @brief Function implementing the defaultTask thread.
* @param argument: Not used
* @retval None
*/
void StartDefaultTask(void *argument)
{
/* USER CODE BEGIN StartDefaultTask */
/* Infinite loop */
for(;;)
{
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_RESET);
HAL_Delay(500);
HAL_GPIO_WritePin(GPIOB,GPIO_PIN_0,GPIO_PIN_SET);
HAL_Delay(500);
}
/* USER CODE END StartDefaultTask */
}
程序正常运行,LED闪烁,至此,手动添加HAL库文件,手动添加FreeRTOS文件已经全部完成!
搭建开发环境总结
-
SVC_Handler
Cortex-M系列有一个指令SVC,用户请求特权模式,FreeRTOS使用SVC来启动调度器
SVC #0
vPortSVCHandler 会被调用,功能:
1.保存当前上下文(寄存器)
2.切换到第一个任务的栈
3.恢复第一个任务的上下文
-
PendSV_Handler
PendSV是延迟可挂起的系统调用中断,FreeRTOS使用它来执行上下文切换,当任务满足切换条件时:
1.设置PendSV pending位
2.CPU异步进入PendSV_Handler
3.保存当前任务上下文
4.切换到下一个任务的上下文
-
SysTick_Handler(系统滴答)
Cortex-M4的
sysTick
是一个周期性定时器中断,FreeRTOS用它来触发tick:1.增加系统tick计数
2.检查是否需要切换任务–>设置PendSV
SVC_Handler只会执行一次,PendSV和SysTick才是调度器运行的核心循环。
FreeRTOS数据类型和编程规范
数据类型
在 portmacro.h
头文件中,定义了两个数据类型:
-
TickType_t
可以是16位的,也可以是32位的。
FreeRTOSConfig.h
中定义configUSE_16_BIT_TICKS
为uint32_t
,则TickType_t
就是uint32_t
。 -
BaseType_t
这是该架构最高效的数据类型,32位架构中,它就是
uint32_t
;16位架构中,它就是uint16_t
;8位架构中,它就是uint8_t
,通常用作简单的返回值的类型
变量名
变量名有前缀
变量名前缀 | 含义 |
---|---|
c | char |
s | int16,short |
l | int32_t,long |
x | BaseType_t |
u | unsigned |
p | 指针 |
uc | uint8_t,unsigned char |
pc | char指针 |
函数名
函数名的前缀有2部分:返回值类型、在那个文件定义
函数名前缀 | 含义 |
---|---|
vTaskPrioritySet | 返回值类型:void,在task.c中定义 |
xQueueReceive | 返回值类型:BaseType_t,在queue.c中定义 |
pvTimerGetTimerID | 返回值类型:pointer to void,在timer.c中定义 |
宏的名
宏的名字是大小,可以添加小写的前缀,前缀是用来表示宏在那个文件中定义
宏的前缀 | 含义 |
---|---|
port(如portMAX_DELAY) | portable.h或portmacro.h |
task(如taskENTER_CRITICAL()) | task.h |
pd(如pdTRUE) | projdefs.h |
config(比如configUSE_PREEMPTION) | FreeRTOSConfig.h |
err(比如errQUEUE_FULL) | projdefs.h |
通用的宏定义如下:
宏 | 值 |
---|---|
pdTRUE | 1 |
pdFALSE | 0 |
pdPASS | 1 |
pdFAIL | 0 |
FreeRTOS配置文件
在FreeRTOSConfig.h
文件中,关注几个关键配置值
configCPU_CLOCK_HZ
设置系统运行频率,STM32F429IGT6
最大是180MHZ
#define configCPU_CLOCK_HZ ( 180000000 )
configTICK_RATE_HZ
#define configTICK_RATE_HZ ((TickType_t)1000)
FreeRTOS调度时间设置的1000HZ,也就是1ms调度一次
configUSE_PREEMPTION
#define configUSE_PREEMPTION 1
- 是否可被抢占,高优先级可以抢占低优先级的任务,如果关闭,则高优先级一直运行
- 新版FreeRTOS不
configUSE_TIME_SLICING
#define configUSE_TIME_SLICING 1
时间片轮转,使能它,相同优先级可以共享CPU的使用;如果关闭,则相同优先级某个单一任务会一直运行。
configIDLE_SHOULD_YIELD
#define configIDLE_SHOULD_YIELD 1
主要影响空闲中断,使能表示空闲中断只执行很短的时间就会让出CPU,如果关闭表示空闲中断和其它同优先级任务共享CPU
configKERNEL_INTERRUPT_PRIORITY and configMAX_SYSCALL_INTERRUPT_PRIORITY
#define configPRIO_BITS __NVIC_PRIO_BITS
#define configLIBRARY_LOWEST_INTERRUPT_PRIORITY 15 //中断最低优先级
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5 //系统可管理的最高中断优先级
#define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
FreeRTOS的中断管理机制
FreeRTOS内核跑在ARM Cortex-M内核(NVIC)上,和中断的关系主要有三点:
-
任务调度靠中断触发
SysTick/PendSV/SVC
是FreeRTOS的核心中断- 它们的优先级必须正确配置,否则调度会异常,比如停在
SVC_Handler
-
中断里能不能调用FreeRTOS API?
- 低优先级(数值大)中断:可以安全调用
xQueueDendFromISR()
、xSwmaphoreGiveFromISR()
等 “FromISR” API - 高优先级(数值小)中断:禁止调用FreeRTOS API,否则可能导致内核崩溃。
- 低优先级(数值大)中断:可以安全调用
-
通过优先级掩码区分
FreeRTOS用
configKERNEL_INTERRUPT_PRIORITY
和configMAX_SYSCALL_INTERRUPT_PRIORITY
来划分:- 内核中断优先级(调度器用的)
- 应用中断可用的最大优先级
两个关键配置
-
configKERNEL_INTERRUPT_PRIORITY
- 定义FreeRTOS内核中断的优先级(PendSV、SysTick、SVC)
- 必须设置为最低优先级,米面打断你的应用中断
#define configKERNEL_INTERRUPT_PRIORITY ( 15 << (8 - __NVIC_PRIO_BITS) )
STM32F4
__NVIC_PRIO_BITS=4
,所以有0~15个优先级,15表示最低优先级(数值大=优先级低)。 -
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
(老版本叫configMAX_SYSCALL_INTERRUPT_PRIORITY
)
能够安全调用FreeRTOS API的最高中断优先级,所有优先级数值>=这个值的中断,都可以用 FromISR()
这样的函数,所有优先级<这个值 的中断,只能跑裸函数,不能调用RTOS
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( 5 << (8 - __NVIC_PRIO_BITS) )
表示NVIC优先级5及以下的中断,可以安全调用FreeRTOS API,优先级0~4的中断(数值小,优先级高)不能调用FreeRTOS API
配置总结
假设 __NVIC_PRIO_BITS = 4
(16 级中断优先级,0 最高,15 最低):
参数 | 作用 | 推荐值 | 说明 |
---|---|---|---|
configKERNEL_INTERRUPT_PRIORITY | 内核中断优先级 (PendSV, SysTick, SVC) | 15<<8-4 | 必须最低优先级,不能被打断 |
configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY | 应用中断可调用 FreeRTOS API 的最高优先级 | 5 | 推荐设 5~7 |
configMAX_SYSCALL_INTERRUPT_PRIORITY | 和上面对应的移位版本 | 5 << (8-4) | FreeRTOS 内部使用 |
configUSE_PORT_OPTIMISED_TASK_SELECTION
#define configUSE_PORT_OPTIMISED_TASK_SELECTION 1
任务调度器性能优化配置,FreeRTOS调度器在切换任务时,需要快速找到最高优先级的就绪任务,这个查找过程可以有两种方式:
- 通用C实现:遍历任务就绪图(一个
uxTopReadyPriority
位掩码),效率相对低 - 处理器相关优化实现(汇编或内建指令):利用CPU的CLZ(Conunt Leading Zeros)或类似指令,一条指令就能找到最高优先级任务
限制条件: configMAX_PRIORITIES
不超过32,一般5~7就够了
在Cortex-M3/M4/M7 上都有CLZ指令,可以直接用,推荐开启设置;对于Cortex-M0等低端CPU无法使用
configUSE_TICKLESS_IDLE
#define configUSE_TICKLESS_IDLE 0
- 0:禁用tickless模式(默认)
- 1:启用tickless模式
FreeRTOS内核正常工作时会周期性产生SysTick中断(比如1ms一次),用于任务调度和时间管理。这种模式下,即使系统空闲,也会不停产生SYsTick中断,造成CPU无谓的唤醒,增加功耗。适用于低功耗应用(如电池供电设备、物联网节点、传感器)。
configMAX_PRIORITIES
#define configMAX_PRIORITIES 5
- 系统可用的任务优先级数量
- 优先级编号从0到(configMAX_PRIORITIES – 1)
- 0等于最低优先级(通常用于idle任务)
- configMAX_PRIORITES-1 = 最高优先级
注意,Cortex-M中断优先级通常是0~15,FreeRTOS任务优先级由 configMAX_PRIORITIES决定
举例:如果我有三个任务:
- LED任务,任务优先级15
- 通信任务,任务优先级14
- 传感器任务,任务优先级13
如果定义configMAX_PRIORITIES=3
,加上空闲任务中断,总共就有4种任务优先级,超过了可用优先级总数,FreeRTOS会自动截断超出范围的优先级,所有用户任务最终都会在最高优先级configMAX_PRIORITIES-1
上运行,也就是所有用户任务都会在优先级为2的任务下运行,并且会平分CPU时间,空闲任务依然优先级是0。
关键点总结:
1.FreeRTOS
不会报错,只是会把超出范围的优先级截断
2.configMAX_PRIORITIES = 可用优先级总数,包含空闲任务的 0 优先级。
3.超出范围的任务都会被压倒configMAX_PRIORITIES-1
的优先级上
如果上述例子需要正常运行,设置
#define configMAX_PRIORITIES 4 // 0~3
任务 | 优先级 |
---|---|
idle | 0 |
传感器 | 1 |
通信 | 2 |
LED | 3 |
这样就能实现想要的优先级策略。
CubeMX 生成的 FreeRTOS 配置里,configMAX_PRIORITIES
默认是 56(比较大),这样基本不会不够用,但占点 RAM。实际开发经验:
- 小系统(STM32F1、F0等):一般用5或者7
- 复杂应用(STM32F4/F7/H7、ESP32等):可用15~32
- CubeMX默认56:有点浪费,但也避免了优先级不够用的问题
configMINIMAL_STACK_SIZE
#define configMINIMAL_STACK_SIZE 128
最小任务栈大小(单位通常是StackType_t
的数量,而不是字节)
- 在Cortex-M 平台,
StackType_t
通常是uint32_t
(4字节) - 所有
128
实际上是128x4=512
字节
Cortex-M实际经验:
MCU类型 | 推荐最小栈大小 |
---|---|
STM32F1/F0 | 128~256 |
STM32F4/F7/H7 | 128~512 |
高级任务(使用浮点) | 256~1024 |
configMAX_TASK_NAME_LEN
#define configMAX_TASK_NAME_LEN 16
表示任务名最大只能有15个有效字符+1个\0
,超过15个字符,FreeRTOS会自动截断,常见取值8、16、32
configTICK_TYPE_WIDTH_IN_BITS
#define configTICK_TYPE_WIDTH_IN_BITS TICK_TYPE_WIDTH_32_BITS
FreeRTOS新版,LTS20240601
里面引入的配置选项,用来控制RTOS系统节拍计数的位宽。
不同位宽的影响
- 16 位 Tick
TickType_t = uint16_t
最大 tick 计数 = 65535
如果 tick 周期 = 1 ms,则溢出周期 = 65.535 秒
适合 小内存 MCU(节省 RAM & ROM)
- 32 位 Tick(最常用)
TickType_t = uint32_t
最大 tick 计数 = 4,294,967,295
如果 tick 周期 = 1 ms,则溢出周期 ≈ 49.7 天
适合大部分 Cortex-M MCU(如 STM32F1/F4/F7)
- 64 位 Tick
溢出时间几乎可以忽略(数百万年)
但会占用更多 RAM & ROM,不常用
使用经验:
-
STM32F103 / STM32F429 这种 Cortex-M MCU:推荐用 32 位。
-
资源极少的 8 位 MCU:可以用 16 位,节省内存。
同步和互斥
FreeRTOS中不能使用全局变量实现同步和互斥,会出现一系列隐患,需要用官方提供的信号量、互斥锁、队列、事件组、任务通知等机制,主要问题如下:
竞态条件(Race Condition)
多个任务/中断可能同时读写全局变量,因为Cortex-M等CPU的读/改/写操作不是原子的,可能出现:
// 任务A
if (flag == 0) { flag = 1; /* 临界区操作 */ }
// 任务B
if (flag == 0) { flag = 1; /* 临界区操作 */ }
两个任务几乎同时看到 flag==0
,都进入临界区,从而破环互斥。
优先级反转(Priority Inversion)
- 假设低优先级任务设置了一个全局变量锁住资源,但在释放前被高优先级任务抢占
- 高优先级任务需要该资源,于是只能忙等,调度停滞
- FreeRTOS的互斥量内置了优先级继承机制,能解决这个问题
缺乏任务阻塞机制
用全局变量同步时,常见的做法是忙等(busy-wait),这会导致CPU空转浪费资源,正确做法是使用队列/信号量/通知,等待时任务会挂起阻塞,让出CPU。
中断与任务的竞争
中断可能在任务访问全局变量时修改它,如果没有合适的关中断/屏蔽临界区保护,容易出现数据不一致,FreeRTOS提供了taskENTER_CRITICAL/taskEXIT_CRITICAL或者FromISR API来保证安全。
代码可读性和可维护性差
全局变量隐藏了同步的语义,维护者很难看出哪些任务依赖这个变量,逻辑复杂是容易出错
推荐做法:
- 互斥量(Mutex):保护共享资源,支持优先级继承
- 二值/计数信号量(Semaphore):事件同步,任务间通信
- 队列(Queue):数据传递和同步
- 任务通知(Task Notification):更轻量的事件/数据同步
队列
使用队列实现数据传递的功能,也顺带实现了同步和互斥的功能。
队列实现同步(任务之间的事件协调)
- 如果一个任务调用 xQueueReceive() ,但队列里面没有数据,它会进入阻塞态,直到有任务或ISR向队列里发送数据。
- 这就实现了任务间的同步(生产者任务发送数据的动作,正好唤醒消费者任务)
// Task A 生产者
int value = 123;
xQueueSend(xQueue, &value, portMAX_DELAY); // 唤醒 Task B
// Task B 消费者
int recvValue;
xQueueReceive(xQueue, &recvValue, portMAX_DELAY); // 等待 Task A 发送数据
这里即使你不关心传递的 value
,仅仅用队列满/空的特性,也能做到 事件同步,类似 二值信号量。
队列实现互斥(资源访问控制)
-
当一个队列长度为1(最多只存一个元素)时,它的用法就接近二值信号量
-
例如,先往队列里放入一个“令牌”(token),表示资源可用;
- 一个任务取走这个令牌 → 进入临界区;
- 用完后再把令牌放回队列 → 释放资源。
// 创建一个长度为 1 的队列(相当于二值信号量) xQueue = xQueueCreate(1, sizeof(uint8_t)); uint8_t token = 1; xQueueSend(xQueue, &token, 0); // 初始资源可用 // Task A 获取资源 xQueueReceive(xQueue, &token, portMAX_DELAY); // >>> 临界区代码 xQueueSend(xQueue, &token, 0); // 释放资源 // Task B 获取资源 xQueueReceive(xQueue, &token, portMAX_DELAY); // >>> 临界区代码 xQueueSend(xQueue, &token, 0);
这样两个任务就不会同时进入临界区了。
为什么 FreeRTOS 还提供 信号量/互斥量?
虽然队列能做到同步/互斥,但它不是专门设计的,缺点有:
- 队列维护了缓冲区,管理开销比信号量/互斥量大
- 队列互斥没有优先级继承(Priority Inheritance),高优先级任务可能被低优先级任务阻塞(优先级反转问题)
- 队列需要复制数据(尤其消息较大时效率低)
所以:
- 同步事件:推荐使用二值信号量
- 资源互斥:推荐使用互斥量(Mutex)(支持优先级继承)
- 数据传递:推荐用队列
- 高性能事件通知:任务通知(比信号量/队列更加轻量,效率最高)
举个例子:
- 串口接收数据(中断):用队列把字节流传递给解析任务
- ADC转换完成中断:用二值信号量唤醒处理任务
- 共享SPI Flash:用互斥量避免多任务同时访问
- 任务之间的简短信号(比如启动/停止):用任务通知
队列集
✅ 优点:
- 一个任务同时等待多个事件(队列/信号量),效率高
- 避免轮询和复杂的if-else
- 逻辑清晰:任务只需要阻塞在一个点上
⚠️ 限制:
- 队列集不能包含互斥量(Mutex) ,因为互斥量有优先级继承机制,不适合放进集合
- 队列集大小哟啊能容纳所有子对象的容量之和
- 只能等待“集合中的对象有数据/信号”,不能直接等待事件标志组
应用场景:
- 一个任务处理多种输入源(UART、CAN、按键、中断信号)
- 统一事件分发,简化任务间通信
- 替代复杂的事件轮询逻辑
✅ 总结:
队列集就是FreeRTOS提供的多路复用机制,类似于Linux里的 select()/poll()
,让任务能高效等待多个队列/信号量中的任意一个。
信号量
分为二进制信号量(Binary Semaphore)和计数型信号量(Counting Semaphore)
1️⃣ 二进制信号量(Binary Semaphore)
特点:
- 只能是0或者1
- 常用于任务与中断、任务与任务之间的事件同步
- 拿到信号量之后就会变成0,直到其它地方释放
2️⃣ 计数型信号量(Counting Semaphore)
特点:
- 值的范围是0~最大值
- 用来统计事件发生的次数,或者表示资源池的剩余数量
- 每次
xSemaphoreGive()
数值+1
,每次xSemaphoreTake()
数值-1
互斥量
互斥量用于任务间的互斥访问,它和二进制信号量类似,但有几个重要的区别和特征,主要解决了:
- 优先级继承问题
- 递归上锁的问题
递归所有权
- 当一个任务获得互斥量后,这个互斥量的“所有权”属于该任务
- 这有这个任务才能释放它
- 如果另外一个任务尝试释放互斥量,系统会报错
优先级继承机制(Priority Inheritance)
- 防止优先级反转
- 例如:
- 高优先级任务A想要访问资源,但是资源被低优先级任务B占用。
- 如果中优先级任务C一直运行,任务B永远得不到CPU去释放互斥量
- 互斥量支持优先级继承机制:B会“临时提升”到A的优先级,直到释放互斥量
- 这样A能尽快获取资源,避免优先级反转问题
限制
和二进制信号量不同,互斥量不能用于中断服务程序(ISR),因为涉及任务优先级继承
使用场景
- 保护共享资源:
- UART、SPI、I2C外设访问
- LCD屏幕刷新
- 全局变量(非原子操作)
- 任务之间的互斥
保证同一时间只有一个任务访问某个临界区。
普通互斥量 vs 递归互斥量
-
普通互斥量:适合不同任务之间的资源互斥,普通
xSemaphoreTake / xSemaphoreGive
不能和递归互斥量混用。 -
递归互斥量:适合同一个任务内部的函数嵌套调用,避免死锁。必须用
xSemaphoreTakeRecursive
和xSemaphoreGiveRecursive
配对使用。
事件组
事件组是FreeRTOS提供的一种任务间通信与同步机制,本质上就是一个32位的二进制标志集合,每一位代表一个独立的事件,事件组可以用来:
- 通知一个任务多个事件的发生
- 等待一个或多个事件满足条件后任务才继续运行
- 实现多任务之间的同步
使用场景:
-
多任务同步
多个任务都等待某些bit,等到所有条件满足在一起运行,类似栅栏
-
时间通知
一个任务通知另外一个任务多个不同的事件(用不同bit表示)
-
替代二值信号量
单bit的事件组就像一个二值信号量
注意事项:
- 事件组不适合中断直接操作,如果要在中断里用,需要用
xEventGroupSetBitsFromISR()
- 一个事件组的32位有限,最多可以表示32个独立事件
- 事件组适合广播式通知(一个事件可以唤醒多个等待任务)
- 如果任务只等待一个条件,推荐用信号量或通知机制;如果等待多个条件,用事件组更合适
任务通知
FreeRTOS作者Richard Barry强烈推荐使用任务通知替代信号量、事件组甚至队列,因为更轻量、更快。
1️⃣ 任务通知是什么
- 每个任务都有一个内置的“通知槽”,可以理解为一个轻量级的32位消息邮箱
- 任务通知是任务与任务或ISR与任务之间的通信方式
- 速度优势:比信号量/队列/事件组更快、内存占用更小(因为不需要创建额外的内核对象)
2️⃣ 常用 API
发送通知
BaseType_t xTaskNotify(TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction);
BaseType_t xTaskNotifyGive(TaskHandle_t xTaskToNotify);
BaseType_t xTaskNotifyFromISR(TaskHandle_t xTaskToNotify,
uint32_t ulValue,
eNotifyAction eAction,
BaseType_t *pxHigherPriorityTaskWoken);
BaseType_t xTaskNotifyGiveFromISR(TaskHandle_t xTaskToNotify,
BaseType_t *pxHigherPriorityTaskWoken);
接收通知
BaseType_t xTaskNotifyWait(uint32_t ulBitsToClearOnEntry,
uint32_t ulBitsToClearOnExit,
uint32_t *pulNotificationValue,
TickType_t xTicksToWait);
uint32_t ulTaskNotifyTake(BaseType_t xClearCountOnExit,
TickType_t xTicksToWait);
3️⃣ eNotifyAction 的几种模式
任务通知的关键就是 eAction
,它决定了通知的“处理方式”:
-
eNoAction:仅仅是更新通知状态为“pending”,未使用ulValue。这个选项相当于轻量级的、更高效的二进制信号量
-
eSetBits:把任务通知值看多事件位掩码,设置对应bit(类似事件组,但是每次通知都会唤醒接收者)
-
eIncrement:对通知值做自增,未使用ulValue。相当于轻量级的、更高效的二进制信号量,计数型信号量,相当于
xTaskNotifyGive()
函数。 -
eSetValueWithOverwrite:直接写入一个值,覆盖原来的
-
eSetValueWithoutOverwrite:只在空闲时写入(不覆盖),类似队列长度为1
4️⃣ 特点总结
✅ 优点:
- 内存零开销(不需要单独创建信号量/队列对象)
- 执行速度更快(API是内联的)
- 灵活,可模拟信号量、事件组、计数器。
⚠️ 限制:
- 每一任务只有一个通知槽,不能像队列那样存多个消息,只能多对一
📌 一句话总结
任务通知是FreeRTOS中最轻量、最快的任务间同步与事件传递机制,能替代大部分信号量/事件组应用场景。
定时器
FreeRTOS定时器是软件定时器,是由RTOS内核管理的定时服务,它依赖系统tick中断作为时间基准。优点是不占用硬件资源,数量几乎无限,缺点是精度差且依赖调度。
应用场景:
- LED心跳闪烁
- 周期性上传
- 超时监测
- 延迟初始化任务
调试
FreeRTOS提供了多种调试手段:
- 打印
- 断言:
configASSERT
- Trace
- Hook函数(回调函数)
打印
printf:FreeRTOS工程使用了microlib
,里面实现了printf函数
需要实现以下fputc
函数
int fputc( int ch, FILE *f );
断言
一般的C库里面,断言就是一个函数:
void assert(scalar expression);
它的作用是确认expression必须为真,如果expression为假就会中止程序
在FreeRTOS
里面,使用configASSERT()
,比如:
#define configASSERT(x) if (!x) while(1);
我们可以让它提供更多信息,比如:
#define configASSERT(x) \
if (!x) \
{
printf("%s %s %d\r\n", __FILE__, __FUNCTION__, __LINE__); \
while(1); \
}
configASSERT(x)中,如果x为假,表示发生很严重的错误,必须停止系统的运行。
Trace
FreeRTOS中定义了很多trace开头的宏,这些宏被放在系统关键位置。它们一般都是空的宏,这不会影响代码;我们要调试某些功能时,可以修改宏:修改某些标记变量、打印信息等待
trace宏 | 描述 |
---|---|
traceTASK_INCREMENT_TICK(xTickCount) | 当tick计数自增之前此宏函数被调用。参数xTickCount当前的Tick值,它还没有增加。 |
traceTASK_SWITCHED_OUT() | vTaskSwitchContext中,把当前任务切换出去之前调用此宏函数。 |
traceTASK_SWITCHED_IN() | vTaskSwitchContext中,新的任务已经被切换进来了,就调用此函数。 |
traceBLOCKING_ON_QUEUE_RECEIVE(pxQueue) | 当正在执行的当前任务因为试图去读取一个空的队列、信号或者互斥量而进入阻塞状态时,此函数会被立即调用。参数pxQueue保存的是试图读取的目标队列、信号或者互斥量的句柄,传递给此宏函数。 |
traceBLOCKING_ON_QUEUE_SEND(pxQueue) | 当正在执行的当前任务因为试图往一个已经写满的队列或者信号或者互斥量而进入了阻塞状态时,此函数会被立即调用。参数pxQueue保存的是试图写入的目标队列、信号或者互斥量的句柄,传递给此宏函数。 |
traceQUEUE_SEND(pxQueue) | 当一个队列或者信号发送成功时,此宏函数会在内核函数xQueueSend(),xQueueSendToFront(),xQueueSendToBack(),以及所有的信号give函数中被调用,参数pxQueue是要发送的目标队列或信号的句柄,传递给此宏函数。 |
traceQUEUE_SEND_FAILED(pxQueue) | 当一个队列或者信号发送失败时,此宏函数会在内核函数xQueueSend(),xQueueSendToFront(),xQueueSendToBack(),以及所有的信号give函数中被调用,参数pxQueue是要发送的目标队列或信号的句柄,传递给此宏函数。 |
traceQUEUE_RECEIVE(pxQueue) | 当读取一个队列或者接收信号成功时,此宏函数会在内核函数xQueueReceive()以及所有的信号take函数中被调用,参数pxQueue是要接收的目标队列或信号的句柄,传递给此宏函数。 |
traceQUEUE_RECEIVE_FAILED(pxQueue) | 当读取一个队列或者接收信号失败时,此宏函数会在内核函数xQueueReceive()以及所有的信号take函数中被调用,参数pxQueue是要接收的目标队列或信号的句柄,传递给此宏函数。 |
traceQUEUE_SEND_FROM_ISR(pxQueue) | 当在中断中发送一个队列成功时,此函数会在xQueueSendFromISR()中被调用。参数pxQueue是要发送的目标队列的句柄。 |
traceQUEUE_SEND_FROM_ISR_FAILED(pxQueue) | 当在中断中发送一个队列失败时,此函数会在xQueueSendFromISR()中被调用。参数pxQueue是要发送的目标队列的句柄。 |
traceQUEUE_RECEIVE_FROM_ISR(pxQueue) | 当在中断中读取一个队列成功时,此函数会在xQueueReceiveFromISR()中被调用。参数pxQueue是要发送的目标队列的句柄。 |
traceQUEUE_RECEIVE_FROM_ISR_FAILED(pxQueue) | 当在中断中读取一个队列失败时,此函数会在xQueueReceiveFromISR()中被调用。参数pxQueue是要发送的目标队列的句柄。 |
traceTASK_DELAY_UNTIL() | 当一个任务因为调用了vTaskDelayUntil()进入了阻塞状态的前一刻此宏函数会在vTaskDelayUntil()中被立即调用。 |
traceTASK_DELAY() | 当一个任务因为调用了vTaskDelay()进入了阻塞状态的前一刻此宏函数会在vTaskDelay中被立即调用。 |
Malloc Hook函数
编程时,一般的逻辑错误都容易解决。难以处理的是内存越界、栈溢出等。内存越界经常发生在堆的使用过程中:堆,就是使用malloc
得到的内存。并没有很好的方法检测内存越界,但是可以提供一些回调函数:
-
使用
pvPortMalloc
失败时,如果在FreeRTOSCOnfig.h
配置了configUSE_MALLOC_FAILED_HOOK
为1,会调用:void vApplicationMallocFailedHook( void );
栈溢出Hook函数
在切换任务(vTaskSwitchContext
)时调用taskCHECK_FOR_STACK_OVERFLOW
来检测栈是否溢出,如果溢出会调用
void vApplicationStackOverflowHook( TaskHandle_t xTask, char * pcTaskName );
怎么判断栈溢出?有两种方法:
-
方法一:
- 当前任务被切换出去之前,它的整个运行现场都被保存在栈里,这时很可能就是它对栈的使用到达了峰值。
- 这方法很高效,但是并不准确
-
方法二:
- 创建任务时,它的栈被填入固定的值,比如:0xa5
- 监测栈里最后16字节的数据,如果不是0xa5表示栈即将或者已经用完了
推荐方法二。