3.3 FreeRTOS移植(标准库)

FreeRTOS源码

进入FreeRTOS官网下载文件:FreeRTOS™ – FreeRTOS™

下载 -> FreeRTOS文件夹 -> Source文件夹 :

我们需要include文件夹,里面包含了FreeRTOS的头文件。

我们需要portable文件夹,里面包含了FreeRTOS的移植文件。

我们需要list.c、queue.c、tasks.c这三个C文件,分别是列表、队列和任务相关的文件。

portable文件夹

FreeRTOS操作系统归根到底是一个软件层面的东西,那FreeRTOS是如何跟硬件联系在一起的呢?

和硬件联系在一起,说白了就是软件可以操作寄存器,portable文件夹里面的东西就是连接桥梁 。

我们使用Keil开发,因此这里只需要选用Keil的移植文件,我们只需要portable文件夹中的三个:

FreeRTOS移植

工程添加源码

我们将FreeRTOS中的上述源码,先添加至我们的Keil5的工程中(别忘了头文件路径):

增加配置文件

我们之前说了,FreeRTOS是可以被裁剪的,有些用不到的功能,我们可以通过一些宏定义,给他去掉。

在这一步,我们可以直接用官方Demo文件中的FreeRTOSConfig.h文件,你是什么型号的芯片,就在Demo里找对应的文件夹,然后找到这个C文件,直接复制粘贴,直接拿来用。

因为是初学者,我们现在肯定看不懂这个C文件里是什么,而且这个文件是我们自己拿Demo里面的文件,报错了也不知道会怎么改,所以当你不知道怎么办的时候,就要编译你的程序,看你的报错内容,然后针对性的修改配置文件。

什么是钩子函数?

钩子函数是FreeRTOS提供的用户自定义函数,用于在特定事件发生时执行特定操作。它们允许用户在不修改内核代码的情况下扩展FreeRTOS的功能。以下是常用的钩子函数:

1. **vApplicationTickHook**:
– 每个系统节拍(tick)发生时调用。
– 用于执行周期性任务。

2. **vApplicationIdleHook**:
– 系统空闲时调用。
– 常用于进入低功耗模式。

3. **vApplicationStackOverflowHook**:
– 当检测到任务堆栈溢出时调用。
– 用于错误处理和调试。

4. **vApplicationMallocFailedHook**:
– 当内存分配失败时调用。
– 用于处理内存不足的情况。

这些钩子函数帮助开发者在关键事件上插入自定义逻辑,提高系统的可调试性和稳定性。

 

下面是最常见的错误案例:

Undefined symbol vApplicationTickHook (referred from tasks.o)

Undefined symbol vApplicationStackOverflowHook (referred from tasks.o)

Undefined symbol vApplicationIdleHook (referred from tasks.o)

Undefined symbol vApplicationMallocFailedHook (referred from heap_4.o)

这是因为缺少钩子函数的定义,解决方法很简单,只需要在FreeRTOSConfig.h中找到对应的宏定义,然后将其置0。

修改SYSTEM文件

这里所谓修改system文件(sys.c、usart.c、delay.c),无非就是在自己写的文件里进行增删查改,如果你的系统之中没有这些文件,就不必更改。如果你的代码是拷贝别人的代码,我推荐你删除,现在也用不到,等用到了自己再写,这样更能锻炼自身的技术。

同时,在我后续的文章中,会逐一讲解要怎么写这些文件,现在这里我们用不到,我们只是移植一个最基础的工程。

修改中断相关文件

在stm324xx_it.c(你是什么芯片,就找自己对应的芯片的中断文件),注释掉下面这三个函数,为什么要注释掉这三个函数?

因为在我们刚才移植的FreeRTOS中,已经定义了这三个中断函数,老的就不再用了,不删除会导致重定义。

添加应用程序

上述步骤我们移植完成之后,就需要运行一下面的Demo,来看一下是否正常。

#include "FreeRTOS.h"
#include "task.h"
#include "stm32f4xx.h"                  // Device header

//任务优先级
#define START_TASK_PRIO		1
//任务堆栈大小	
#define START_STK_SIZE 		128  
//任务句柄
TaskHandle_t StartTask_Handler;
//任务函数
void start_task(void *pvParameters);

//任务优先级
#define LED0_TASK_PRIO		2
//任务堆栈大小	
#define LED0_STK_SIZE 		50  
//任务句柄
TaskHandle_t LED0Task_Handler;
//任务函数
void led0_task(void *pvParameters);

//任务优先级
#define LED1_TASK_PRIO		3
//任务堆栈大小	
#define LED1_STK_SIZE 		50  
//任务句柄
TaskHandle_t LED1Task_Handler;
//任务函数
void led1_task(void *pvParameters);


 
 void GPIO_INIT()
 {
    // 使能GPIOF时钟
    RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOF, ENABLE);

    // 初始化结构体
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10; // 替换为实际引脚
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; // 输出模式
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 速度
    GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; // 推挽输出
    GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; // 无上拉下拉
    GPIO_Init(GPIOF, &GPIO_InitStructure);	 
 }
 
 void start_task(void *pvParameters)
{
	 taskENTER_CRITICAL();           //进入临界区
    //创建LED0任务
    xTaskCreate((TaskFunction_t )led0_task,     	
                (const char*    )"led0_task",   	
                (uint16_t       )LED0_STK_SIZE, 
                (void*          )NULL,				
                (UBaseType_t    )LED0_TASK_PRIO,	
                (TaskHandle_t*  )&LED0Task_Handler); 
    //创建LED1任务
    xTaskCreate((TaskFunction_t )led1_task,     
                (const char*    )"led1_task",   
                (uint16_t       )LED1_STK_SIZE, 
                (void*          )NULL,
                (UBaseType_t    )LED1_TASK_PRIO,
                (TaskHandle_t*  )&LED1Task_Handler);         
    vTaskDelete(StartTask_Handler); //删除开始任务
    taskEXIT_CRITICAL();            //退出临界区
}
//LED0 任务函数
void led0_task(void *pvParameters)
{
 while(1)
 {
	  GPIO_SetBits(GPIOF,GPIO_Pin_9);
	  vTaskDelay(500);
	  GPIO_ResetBits(GPIOF,GPIO_Pin_9);
	  vTaskDelay(500);
 }
}
//LED1 任务函数
void led1_task(void *pvParameters)
{
	while(1)
	{
 		GPIO_ResetBits(GPIOF,GPIO_Pin_10);
		vTaskDelay(300);
		GPIO_SetBits(GPIOF,GPIO_Pin_10);	
		vTaskDelay(300);
	}
}
 int main(void)
 {	
	 
	 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);//设置系统中断优先级分组 4 
	 GPIO_INIT();
	 
	 //创建开始任务
    xTaskCreate((TaskFunction_t )start_task,            //任务函数
                (const char*    )"start_task",          //任务名称
                (uint16_t       )START_STK_SIZE,        //任务堆栈大小
                (void*          )NULL,                  //传递给任务函数的参数
                (UBaseType_t    )START_TASK_PRIO,       //任务优先级
                (TaskHandle_t*  )&StartTask_Handler);   //任务句柄              
    vTaskStartScheduler();          //开启任务调度
		 	  
}

问题与解决

在我移植FreeRTOS成功后,运行了Demo程序,LED闪烁,但是出现了一个问题,那就是闪烁频率过慢,不是FreeRTOS设置的1s,下面我给出两个解决方法:

闪烁问题,就直接定位到了是时钟的问题,当确保在FreeRTOSConfig.h文件中:

#define configTICK_RATE_HZ ( ( TickType_t ) 1000 )

确保这个宏定义是1000,不建议通过修改 configTICK_RATE_HZ 来调整任务的延时,这是极其不规范的做法!

configTICK_RATE_HZ 是 FreeRTOS 中的一个配置项,表示系统的时钟节拍频率,即每秒产生的时钟节拍(Tick)的次数。 单位是Hz(赫兹),即每秒的次数。

如果这里我们确实是1000,那么可以肯定,是系统时钟的问题了,查看系统时钟的代码:

查看SystemInit()函数,找到SetSysClock(),对SetSysClock()函数进行替换或者修改,下面是两种解决方法:

方法1:对SetSysClock()函数进行更改:

将宏定义更改为下方的参数,即可修改正确的系统时钟。

#define PLL_M      8
#define PLL_N      336
#define PLL_P      2
#define PLL_Q      7

 

方法2:对SetSysClock()函数进行替换:

直接清空SetSysClock()函数中的内容,置换成以下函数(我的是STM32F40X系列,不是同一系列的,注意修改参数):

void SetSysClock(void)
{
    /* 1. 启用 HSE */
    RCC_HSEConfig(RCC_HSE_ON);
    if (RCC_WaitForHSEStartUp() == SUCCESS)
    {
        /* 2. 设置电源调节器电压输出级别,以适应更高的系统频率 */
        RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
        PWR_MainRegulatorModeConfig(PWR_Regulator_Voltage_Scale1);

        /* 3. 配置 Flash 所需的等待状态 */
        FLASH_SetLatency(FLASH_Latency_5);
        FLASH_PrefetchBufferCmd(ENABLE);
        FLASH_InstructionCacheCmd(ENABLE);
        FLASH_DataCacheCmd(ENABLE);

        /* 4. 配置 PLL */
        RCC_PLLConfig(RCC_PLLSource_HSE, 8, 336, 2, 7); // 8MHz 外部晶振

        /* 5. 启用 PLL */
        RCC_PLLCmd(ENABLE);
        while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET);

        /* 6. 选择 PLL 作为系统时钟源 */
        RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
        while (RCC_GetSYSCLKSource() != 0x08);

        /* 7. 配置 AHB、APB 时钟分频器 */
        RCC_HCLKConfig(RCC_SYSCLK_Div1);    // AHB 时钟 = 系统时钟 / 1 = 168MHz
        RCC_PCLK1Config(RCC_HCLK_Div4);     // APB1 时钟 = AHB 时钟 / 4 = 42MHz
        RCC_PCLK2Config(RCC_HCLK_Div2);     // APB2 时钟 = AHB 时钟 / 2 = 84MHz

        /* 8. 更新 SystemCoreClock 变量 */
        SystemCoreClockUpdate();
    }
    else
    {
        /* HSE 启动失败,处理错误 */
        while (1);
    }
}

系统配置文件

FreeRTOS的系统配置文件是FreeRTOSConfig.h,我们来详细介绍一下这个文件。

这个文件是对FreeRTOS的功能进行配置和裁剪,以及API函数的使能等。

我们在这个文件里,可以通过宏定义,来裁剪掉我们不使用的功能,这就是裁剪。

之前也说过,我们的调度器算法有三种:抢占式、协作式、时间片这三种,我们可以通过该文件来配置,看用哪种算法,这就是配置。

API函数的使能和失能,有些函数用得到,就使能,用不到,就失能;这样可以大大节省我们的空间。

 

这些宏定义可以分为三大类:

“INCLUDE”开头的 : 配置FreeRTOS中可选的API函数。

“config”开头的: 完成FreeRTOS的功能配置和裁剪。

其他配置项 : PendSV宏定义、SVC宏定义。

部分宏定义解释

configUSE_PREEMPTION :1是抢占式调度,0是协程式调度。

configUSE_PORT_OPTIMISED_TASK_SELECTION:计算下一个任务是哪个,一种是硬件计算方式(特殊方式),另一种是软件计算方式(通用方式)。但是软件方式效率比较低,硬件方式效率高。(STM32支持硬件计算方式)

configUSE_TICKLESS_IDLE :  1就是使能tickless低功耗模式, 默认是0,不使能。

configCPU_CLOCK_HZ   : SystemCoreClock 是系统时钟频率,我的是168MHz。

configSYSTICK_CLOCK_HZ  : (configCPU_CLOCK_HZ / 8) ,这个是只有F1才会使用这个宏,F4、F7、H7这个是不用的,这是不同系列的唯一区别。SysTick时钟频率(时钟源),当SysTick时钟频率与内核时钟频率不同时才可以定义。

configTICK_RATE_HZ :定义系统时钟节拍频率, 单位: Hz, 无默认需定义 ,我们这里设置1000,就是1000Hz,计数一次就是1ms。

configMAX_PRIORITIES :最大优先级数量,硬件的话就是32。

configMINIMAL_STACK_SIZE : 空闲任务的堆栈大小,单位是字(Word)。我们CPU是不能闲起来的,所以会提前创建一个空闲任务。

configMAX_TASK_NAME_LEN :任务名字的最大字符。一般我们选择16。

configUSE_16_BIT_TICKS:用于定义系统节拍计数器的数据类型,为1的时候就是16位无符号整型;为0的时候,就是32位无符号整型。STM32是32位的,所以我们定义为0,32位无符号整型。

configIDLE_SHOULD_YIELD: 当为1时,使能在抢占式调度下,同优先级的任务能抢占空闲任务。比如我们空闲任务时最低优先级的,也就是0,那么如果我们也创建了一个优先级为0的任务,我们可以抢占空闲任务。

configUSE_TASK_NOTIFICATIONS:使能任务间直接的消息传递,包括信号量、事件标志组和消息邮箱, 默认: 1。

configTASK_NOTIFICATION_ARRAY_ENTRIES:使能任务间直接的消息传递,包括信号量、事件标志组和消息邮箱, 默认: 1。

configUSE_MUTEXES:使能互斥信号量, 默认: 0 。

configUSE_RECURSIVE_MUTEXES:使能递归互斥信号量, 默认: 0 。

configUSE_COUNTING_SEMAPHORES :1: 使能计数信号量, 默认: 0 。

configQUEUE_REGISTRY_SIZE :定义可以注册的信号量和消息队列的个数, 默认: 0 。

configUSE_QUEUE_SETS  :1: 使能队列集, 默认: 0 。

configUSE_TIME_SLICING : 1: 使能时间片调度, 默认: 1 。

#define configUSE_NEWLIB_REENTRANT 0 /* 1: 任务创建时分配Newlib的重入结构体, 默认: 0 */

#define configENABLE_BACKWARD_COMPATIBILITY 0 /* 1: 使能兼容老版本, 默认: 1 ,如果设置1,就说明你兼容一些老版本的API函数*/

钩子函数,所谓钩子函数其实就是回调函数,钩子函数里面的内容,就是用户自己实现了。