1. FreeRTOS简介1.1. 定义FreeRTOS是RTOS的一种其中RTOS是-Real Time Operating System-实时操作系统是一种可以运行在单片机上的操作系统实现多任务并行1.2. 举例可以为不同的任务分别编写函数这样跟写main()函数的方法无区别了。将不同的任务代码交给FreeRTOS的调度器此时调度器会合理安排每个任务的执行时间。完成任务。此时只需要专心于编写业务代码。FreeRTOS会自动将任务在单片机中运行。1.3. FreeRTOS的历史2. 初次使用FreeRTOS2.1. 举例说明例1相同周期闪灯编写代码驱动开发板上的LED1和LED3按照如下方式闪烁通过状态机区分好每个状态。intmain(void){while(1){HAL_GPIO_WritePin(LED1,GPIO_PIN_SET);// 1. 点亮LED1和LED3HAL_GPIO_WritePin(LED3,GPIO_PIN_SET);HAL_Delay(500);// 2. 延迟500msHAL_GPIO_WritePin(LED3,GPIO_PIN_RESET);// 3. 熄灭LED3HAL_Delay(500);// 4. 延迟500msHAL_GPIO_WritePin(LED1,GPIO_PIN_RESET);// 5. 熄灭LED1HAL_Delay(1000);// 6. 延迟1000ms}}例2不同周期闪灯闪灯周期不一致情况此时周期不同不方便写在一个while循环中。此时使用状态机处理uint8_tled1State0;// 状态0-熄灭1-点亮uint32_tled1ChangeTime0;// 用来记录上次状态转换的时间voidLED1_StateMachine(void){uint32_tnowHAL_GetTick();// 获取当前时间// 状态转换状态0 - 状态1if((led1State0)(now-led1ChangeTime1000)){HAL_GPIO_WritePin(LED1,GPIO_PIN_SET);led1State1;led1ChangeTimenow;}// 状态转换状态1 - 状态0elseif((led1State1)(now-led1ChangeTime1000)){HAL_GPIO_WritePin(LED1,GPIO_PIN_RESET);led1State0;led1ChangeTimenow;}}uint8_tled1State0;// 状态0-熄灭1-点亮uint32_tled1ChangeTime0;// 用来记录上次状态转换的时间voidLED1_StateMachine(void){uint32_tnowHAL_GetTick();// 获取当前时间// 状态转换状态0 - 状态1if((led1State0)(now-led1ChangeTime1000)){HAL_GPIO_WritePin(LED1,GPIO_PIN_SET);led1State1;led1ChangeTimenow;}// 状态转换状态1 - 状态0elseif((led1State1)(now-led1ChangeTime1000)){HAL_GPIO_WritePin(LED1,GPIO_PIN_RESET);led1State0;led1ChangeTimenow;}}2.2. FreeRTOS登场1. FreeRTOS的初识使用FreeRTOS解决上述问题只需要按照main函数的方法写上述代码即可。给任务代码的函数起一个名字固定写法有个形参使用FreeRTOS专用的延迟函数voidvLED3Task(void*pvParameters){for(;;){HAL_GPIO_WritePin(LED3,GPIO_PIN_SET);//点亮vTaskDelay(pdMS_TO_TICKS(300));HAL_GPIO_WritePin(LED3,GPIO_PIN_RESET);//熄灭vTaskDelay(pdMS_TO_TICKS(1500));}}2. 函数前缀的含义前缀含义例子v函数返回 voidvTaskDelay()x函数返回 BaseType_t成功 / 失败xTaskCreate()pv变量void* 空指针pvParameterspcchar* 字符串指针pcTaskNameuxunsigned BaseType_tuxPriorityuluint32_tulTicksprvstatic 私有函数只本文件可见prvInitHardware()3. 调用函数的实现函数xTaskCreate()FreeRTOS 用来创建一个任务的函数。xTaskCreate(vLED1Task,LED1,128,NULL,1,pxCreatedTask);具体参数含义BaseType_txTaskCreate(TaskFunction_t pxTaskCode,// 参数1constchar*constpcName,// 参数2constconfigSTACK_DEPTH_TYPE usStackDepth,// 参数3void*constpvParameters,// 参数4UBaseType_t uxPriority,// 参数5TaskHandle_t*constppxCreatedTask// 参数6);序号参数本例值含义1pxTaskCodevLED1Task任务函数入口地址就是你写的任务函数名任务跑起来就执行这个函数。2pcName“LED1”任务名字字符串仅用于调试内核不使用这个名字。前缀pc → char*3usStackDepth128任务栈深度单位是 word字不是字节STM32 32 位1word4 字节128512 字节栈空间。4pvParametersNULL传给任务函数voidpvParameters的参数这里不传入参数填 NULL。前缀pv → void5uxPriority1任务优先级数字越大优先级越高。0 是最低优先级。前缀ux → unsigned BaseType_t6ppxCreatedTaskpxCreatedTask任务句柄指针用来保存新建任务的句柄后续可以用这个句柄操作任务删除、挂起等不需要句柄可以填 NULL。4. FreeRTOS程序的一般写法为每个子任务创建一个任务代码注意事项给函数起一个单独的名字任务函数有一个void *类型的参数使用FreeRTOS专用的延迟函数vTaskDelay()在main()函数中创建任务并启动调度器// 分别为任务编写任务代码// 任务1的代码voidvTask1(void*pvParameters){// 跟main()方法的写法类似// 给任务函数起一个单独的名字// 任务函数有一个void *类型的参数// 使用FreeRTOS专用的延迟函数vTaskDelay()}// 任务2的代码voidvTask2(void*pvParameters){...}// 在main()方法里创建任务、并启动调度器intmain(void){...// 使用xTaskCreate()创建任务xTaskCreate(vTask1,Task1,128,NULL,1,NULL);xTaskCreate(vTask2,Task2,128,NULL,1,NULL);// 启动调度器vTaskStartScheduler();// 写一个while循环防止程序跑飞while(1){}}3. FreeRTOS总图调度器在运行时总要找到代码执行此时创建空闲任务是的单片机有代码运行。3.1 调度器代码实例闪灯程序当LED熄灭延迟的时候需要调用空函数执行。注意这里的while函数并不是空闲函数。只有调度器启动失败内存不足等才会跑到这个 while 死循环卡住。// 作用LED1的任务函数voidvLED1Task(void*pvParameters){while(1){HAL_GPIO_WritePin(LED1_GPIO_Port,LED1_Pin,GPIO_PIN_SET);// 点亮vTaskDelay(pdMS_TO_TICKS(100));// 延迟100msHAL_GPIO_WritePin(LED1_GPIO_Port,LED1_Pin,GPIO_PIN_RESET);// 熄灭vTaskDelay(pdMS_TO_TICKS(100));// 延迟100ms}}intmain(void){xTaskCreate(vLED1Task,LED1,128,NULL,1,NULL);// 创建LED1任务vTaskStartScheduler();while(1){}}时序图这里亮灯和灭灯是需要执行时间的通常时间很短。3.2 三种中断SVC, pendSV和Systick类比单片机中断不同中断的作用SVC服务调用中断优先级最高作用用来启动第一个任务PendSV可挂起系统调用优先级最低作用后续所有任务之间的切换主要任务切换中断SysTick系统滴答定时器中断优先级最低作用产生系统节拍实现时间片调度、vTaskDelay 延时3.3 内核对象定义由 FreeRTOS 内核管理、在内核中创建用来实现任务通信、同步、资源管理的一类数据结构统称为内核对象任务本身也是内核对象。3.4 堆内存管理方案FreeRTOS 内核对象任务、队列、信号量等动态创建时内存都从FreeRTOS 堆分配4. FreeRTOS内核文件结构FreeRTOS是开源项目下载文件得到一系列的.c .h文件。需要进一步进行移植。此部分是针对无法使用CubeMX进行一键生成的芯片。命名介绍文件夹内容4.1 FreeRTOS-Kernel下的众多.c文件其中task.c与list.c文件必须添加至自己项目中文件名必选作用关联的内核对象或功能task.c✅与任务调度相关的内容存储在此文件中该文件必须添加到工程中任务、任务通知、调度器list.c✅用于实现“列表”数据类型仅FreeRTOS内部使用但它是支撑FreeRTOS运行的关键。该文件必须添加到工程中queue.c与队列、各种信号量相关的源文件。如果项目中用到队列或信号量则需要添加此文件队列、各种信号量二进制信号量、计数信号量、互斥锁、递归互斥锁timer.c与软件定时器相关的源文件。如果项目中用到了定时器则需要添加此文件软件定时器event_group.c与事件组相关的源文件。如果项目中使用到了事件组则需添加此文件事件组croutine.c与协程相关的源文件。协程是一种比较早期的技术用于资源极度匮乏的嵌入式硬件系统现在已经废弃。协程stream_buffer.c与流缓冲区和消息缓冲区有关的源文件。如果项目中使用到了流缓冲区或者消息缓冲区则需要添加此文件。注意本课程暂不介绍与流缓冲区和消息缓冲区相关的内容。流缓冲区、消息缓冲区4.2 FreeRTOS-Kernel下的众多.h文件其中FreeRTOS.h 与task.h文件必须添加至自己项目中文件名必选作用关联的内核对象或功能FreeRTOS.h✅FreeRTOS内核的头文件该头文件必须被引用。task.h✅如果使用到与任务、任务通知相关的功能则该头文件要被引用任务、任务通知、调度器queue.h队列相关的头文件如果使用到与队列相关的功能则该头文件要被引用队列semphr.h信号量相关的头文件如果使用到与信号量相关的功能则该头文件要被引用二进制信号量、计数信号量、互斥锁、递归互斥锁timers.h软件定时器相关的头文件如果使用到与软件定时器相关的功能则该头文件要被引用软件定时器event_group.h事件组相关的头文件如果使用到与事件组相关的功能则该头文件要被引用事件组message_buffer.h消息缓冲区相关的头文件如果使用到消息缓冲区相关的功能则该头文件要被引用。注意本课程暂不介绍与流缓冲区和消息缓冲区相关的内容。消息缓冲区stream_buffer.h流缓冲区相关的头文件如果使用到与流缓冲区相关的功能则该头文件要被引用。注意本课程暂不介绍与流缓冲区和消息缓冲区相关的内容。流缓冲区4.3 FreeRTOS-Kernel下的portable文件移植层是FreeRTOS为了适应不同工具链类型和单片机内核种类而提供的一组代码。通过此文件中不同工具链实现不同的工具链开发。如果开发工具是CubeIDESTM32F1使用工具链为GCCARM-CM3那么选择portable层下此部分进行移植4.4 MemMang文件夹内存管理文件夹对应heap1-5 五种不同的内存管理方案。一般使用heap4方案。注意 这个方案一般只能选择一个将heap4文件粘贴至Src文件夹。4.5 config.h文件将配置文件添加至工程中4.6 补充头文件引用使用 scanf ()、printf ()、putchar ()、getchar () 等函数的时候需要引用 “标准输入输出头文件” - stdio.h#includestdio.hintmain(void){printf(Hello world);// 使用printf()打印你好世界return0;}使用 malloc () 和 free () 管理堆内存的时候需要引用 “标准库头文件” - stdlib.h#includestdlib.hintmain(void){void*ptrmalloc(100);// 使用malloc()分配100个字节的堆内存free(ptr);// 使用free释放掉它return0;}使用数学计算相关的函数的时候需要引用 “数学” 头文件 - math.h#includemath.hintmain(void){doublex,y;x1.0;ysin(x);// 计算x的正弦值return0;}使用字符串处理相关的函数的时候需要引用 “字符串” 头文件 - string.h#includestring.hintmain(void){intlenstrlen(TTSY FreeRTOS);// 测量字符的长度return0;}同样的对于FreeRTOS头文件使用到对应的内容时也需要引用对应的头文件。必须引用信号量队列定时器事件组流缓冲区消息缓冲区FreeRTOS.htask.hsemphr.hqueue.htimers.hevent_groups.hstream_buffer.hmessage_buffer.h5. 配置文件FreeRTOS的配置文件通过宏定义的形式体现。5.1 配置项1configCPU-CLOCK-HZ系统内核的时钟运行频率。对应代码中//This is very often, but not always, equal to the main system clock frequency.#defineconfigCPU_CLOCK_HZ((unsignedlong)720000000)5.2 配置项2configTICK-TYPE-WIDTH-IN-BITS设置TickType-t类型的位宽TickType_t是 FreeRTOS 用来存放系统节拍Tick计数值的数据类型// 三选一#defineconfigTICK_TYPE_WIDTH_IN_BITSTICK_TYPE_WIDTH_16_BITS#defineconfigTICK_TYPE_WIDTH_IN_BITSTICK_TYPE_WIDTH_32_BITS#defineconfigTICK_TYPE_WIDTH_IN_BITSTICK_TYPE_WIDTH_64_BITS搭配configTICK_RATE_HZ5.3 配置项3configKERNEL_INTERRUPT_PRIORITY设置 PendSV 和 SysTick 这两个内核专用中断的优先级其中SysTick系统节拍定时器周期性触发产生 tick用来实现任务延时、超时判断PendSV可挂起服务调用专门做任务上下文切换保存 / 恢复任务栈5.4 configMAX_SYSCALL_INTERRUPT_PRIORITY能够调用 FreeRTOS FromISR 系列 API 的中断的最高抢占优先级有一些单片机程序比如电机控制。要求实时性高不想要FreeRTOS接入。此设置可以确定FreeRTOS无法接入的最高优先级。5.5 configUSE_TIME_SLICING开启 / 关闭同优先级任务的时间片轮转调度开启1时同优先级任务公平分配 CPU关闭2时同优先级里长运行任务会 “霸占 CPU”同优先级其他任务得不到及时执行。6. 安装中断服务程序对应的中断服务函数SVC,SystickPendSV函数异常核心功能什么时候执行xPortPendSVHandlerPendSV任务上下文切换需要任务切换时等其他中断跑完才执行xPortSysTickHandlerSysTick系统节拍、时间片周期性定时器中断vPortSVCHandlerSVC启动第一个任务vTaskStartScheduler 里仅执行一次6.1. 替换HAL库的中断向量表对应的函数6.2. 替换HAL库的Systick时间基准因为上述的SystickHandler被替换所以需要更改时间基准函数。此时HAL库中含有时间的函数均不可使用了。使用图形化配置直接切换Timebase Source即可7. 移植后代码测试7.1 引用头文件#includeFreeRTOS.h// FreeRTOS的核心头文件#includetask.h// 与任务、调度器相关的头文件7.2 参照main函数写任务代码voidvLED1Task(void*pvParameters){for(;;){HAL_GPIO_WritePin(LED1_GPIO_Port,LED1_Pin,GPIO_PIN_SET);vTaskDelay(pdMS_TO_TICKS(500));HAL_GPIO_WritePin(LED1_GPIO_Port,LED1_Pin,GPIO_PIN_RESET);vTaskDelay(pdMS_TO_TICKS(500));}}voidvLED3Task(void*pvParameters){for(;;){HAL_GPIO_WritePin(LED3_GPIO_Port,LED1_Pin,GPIO_PIN_SET);vTaskDelay(pdMS_TO_TICKS(500));HAL_GPIO_WritePin(LED3_GPIO_Port,LED1_Pin,GPIO_PIN_RESET);vTaskDelay(pdMS_TO_TICKS(500));}}7.3 在main函数创建任务并启动调度器intmain(void){......xTaskCreate(vLED1Task,LED1,128,NULL,1,NULL);// 创建LED1任务xTaskCreate(vLED3Task,LED3,128,NULL,1,NULL);// 创建LED3任务vTaskStartScheduler();// 启动调度器while(1){}}
