/
...
/
/
FreeRTOS Structure
Search
Try Notion
FreeRTOS Structure
一.数据类型
数据类型C重定义
port前缀: port是接口,需要我们配置
portCHAR→char
注意: FreeRTOS的char需要为无符号
keil 默认为无符号,但也可以手动修改
🖼️修改方法
portLONG→long
portTickType→unsigned short/unsigned int
根据FreeRTOSConfig.h的宏定义configUSE_16_BIT_TICKS确定
configUSE_16_BIT_TICKS: 1→16位(unsigned short); 0→32位(unsigned int)
portBASE_TYPE
由处理器的架构来决定是多少位的,32/16/8bit 的处理器则是 32/16/8bit 的 数据类型
变量名规则
char→c前缀
short-s前缀
long-l前缀
portBASE_TYPE→x前缀
数据结构,任务句柄,队列句柄等定义的变量名的前缀也是 x
函数名规则
私有函数→prv(private)
返回类型为void(vTaskPrioritySet)→v前缀
返回为portBASE_TYPE(xQueueReceive)→x前缀
宏规则
大写字母表示,并有小写字母的前缀,前缀由所在的宏定义文件或作用决定
port(portMAX_DELAY)→portable.h
task(taskENTER_CRITICAL)→task.h
pd (pdTRUE)→projdefs.h
config(configUSE_PREEMPTION)→FreeRTOSConfig.h
err(errQUEUE_FULL)→projdefs.h
二.工程搭建
🖼️目录结构
New Project
Select Device For Target(选择ARM CM3/4/7)
Manage Run-Time Environment
🖼️CMSIS选择CORE与Stratup
工程目录组织
🖼️添加User Port Source三类
📑添加对应的文件
main.c→User
调试设定
🖼️Debug→软件仿真
🖼️Target→修改时钟大小(根据system_ARMCM3.c宏定义)
💻相应的代码
//system_ARMCM3.c 1 #define __HSI ( 8000000UL) 2 #define __XTAL ( 5000000UL) 3 4 #define __SYSTEM_CLOCK (5*__XTAL)
Copy
C
🖼️添加头文件
三.多任务系统
裸机系统
轮询系统
//轮询系统伪代码 int main(void) { /* 硬件相关初始化 */ HardWareInit(); /* 无限循环 */ for (;;) { /* 处理事情 1 */ DoSomething1(); /* 处理事情 2 */ DoSomething2(); /* 处理事情 3 */ DoSomething3(); } }
Copy
C
前后台系统
区别:
在轮询系统的基础上加入了中断
外部事件的响应在中断(前台)里面完成
事件的处理还是回到轮询系统(后台)中完成.
优点
确保了事件不会丢失
再加上中断具有可嵌套的功能
提高程序的实时响应能力
多任务系统
新概念-任务:
任务跟中断一样,也具有优先级
任务之间独立
由调度器控制
事件的响应也是在中断总进行, 但事件处理则是在任务中进行
🖼️简单比较三种系统
四.数据结构
链表的C实现
🤔其他语言中都是通过泛型来实现,C要如何实现
has-A: 用户数据类型包含一个Node,用作表头,来指明地址(采用这个)
is-A: Node中包含用户数据类似
FreeRTOS中链表的实现
采用has-A方式,将Node且为双向链表,位于list.h
List-Node组成
💻结构定义
//节点定义 struct xLIST_ITEM { TickType_t xItemValue; /* 辅助值,用于帮助节点做顺序排列 */ (1) struct xLIST_ITEM * pxNext; /* 指向链表下一个节点 */ (2) struct xLIST_ITEM * pxPrevious; /* 指向链表前一个节点 */ (3) void * pvOwner; /* 指向拥有该节点的内核对象,通常是 TCB */(4) void * pvContainer; /* 指向该节点所在的链表 */ (5) }; typedef struct xLIST_ITEM ListItem_t; /* 节点数据类型重定义 */ (6) //精简节点定义 struct xMINI_LIST_ITEM { TickType_t xItemValue; /* 辅助值,用于帮助节点做升序排列 */ struct xLIST_ITEM * pxNext; /* 指向链表下一个节点 */ struct xLIST_ITEM * pxPrevious; /* 指向链表前一个节点 */ }; typedef struct xMINI_LIST_ITEM MiniListItem_t; /* 精简节点数据类型重定义 */ //根节点定义 typedef struct xLIST { UBaseType_t uxNumberOfItems; /* 链表节点计数器 */ (1) ListItem_t * pxIndex; /* 链表节点索引指针 */ (2) MiniListItem_t xListEnd; /* 链表最后一个节点 */ (3) }
Copy
C
FreeRTOS都会将标准的据类型用 typedef\重定义的数据类型,放在portmacro.h
Node节点
前后连接节点
List所有者 内核所有者
排序辅助值
List根
总节点数目
节点指针(pxIndex)
精简节点
精简节点: 去掉了内核对象,和链表(List 所有者)对象
🤔为何根节点没有指向第一个节点
链表是首尾相连的,是一个圈,首尾一致,这里从字面上理解就是链表的最后一个节点,我们称之为生产者.该生产者的数据类型是一个精简的节点
初始化
💻List-精简节点-节点初始化
void vListInitialiseItem( ListItem_t * const pxItem ) { /* 初始化该节点所在的链表为空,表示节点还没有插入任何链表 */ pxItem->pvContainer = NULL; } void vListInitialise( List_t * const pxList ) { /* 将链表索引指针指向最后一个节点 */(1) pxList->pxIndex = ( ListItem_t * ) &( pxList->xListEnd ); /* 将链表最后一个节点的辅助排序的值设置为最大,确保该节点就是链表的最后节点 */(2) pxList->xListEnd.xItemValue = portMAX_DELAY; /* 将最后一个节点的 pxNext 和 pxPrevious 指针均指向节点自身,表示链表为空 */(3) pxList->xListEnd.pxNext = ( ListItem_t * ) &( pxList->xListEnd ); pxList->xListEnd.pxPrevious = ( ListItem_t * ) &( pxList->xListEnd ); /* 初始化链表节点计数器的值为 0,表示链表为空 */(4) pxList->uxNumberOfItems = ( UBaseType_t ) 0U; }
Copy
C
初始化节点的时候只需将pvContaine初始化为空
初始化根节点(List)
本身值: 计数器清零,将指针指向精简节点
精简节点参数: 最后一个节点辅助值设为最大,指针指向自己
精简节点: 不存在所有者和内核所有者(一定和List绑定)
插入操作
原型 vListInsert( List_t * const pxList, ListItem_t * const pxNewListItem )
代码逻辑
💻代码定义
/* 寻找节点要插入的位置 */ (2) if ( xValueOfInsertion == portMAX_DELAY ) { pxIterator = pxList->xListEnd.pxPrevious; } else { for ( pxIterator = ( ListItem_t * ) &( pxList->xListEnd ); pxIterator->pxNext->xItemValue <= xValueOfInsertion; pxIterator = pxIterator->pxNext ) { /* 没有事情可做,不断迭代只为了找到节点要插入的位置 */ } }
Copy
C
创建检索迭代器(检索插入位置)
检索
若 pxNewListItem→辅助值为最大portMAX_DELAY则 迭代器指向精简Node的previous
若 pxNewListItem→辅助值≥当前Node→辅助值,则迭代下一个
最后在迭代器Node 和 迭代器Node.Next中插入新Node
22 /* 根据升序排列,将节点插入 */ (3) 23 pxNewListItem->pxNext = pxIterator->pxNext; 24 pxNewListItem->pxNext->pxPrevious = pxNewListItem; 25 pxNewListItem->pxPrevious = pxIterator; 26 pxIterator->pxNext = pxNewListItem;
Copy
C
设置新Node的链表根地址
链表计数+1
删除操作
原型: UBaseType_t uxListRemove( ListItem_t * const pxItemToRemove )
代码逻辑
调整前后节点指针
pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious; pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext;
Copy
C
防止根节点指针指向被删除的节点
清空被删除节点的归属链表
pxItemToRemove->pvContainer = NULL;
Copy
C
根节点计数器-1
返回链表节点计数器(当前节点数)
五.任务定义
任务定义: 根据功能的不同,把整个系统分割成独立的且无法返回的函数,此函数称为任务
任务栈
💻任务Stack定义代码
#define TASK1_STACK_SIZE 128 StackType_t Task1Stack[TASK1_STACK_SIZE]; #define TASK2_STACK_SIZE 128 StackType_t Task2Stack[TASK2_STACK_SIZE];
Copy
C
存放全局变量,函数调用的局部变量,函数返回地址
Stack的大小在启动文件,链接脚本指定,最后在_main中初始化
一般在RTOS在预先定义一个大数组.或者动态分配
多少个任务,就需要多少个Stack
StackType_t 定义于portmacro.h 为处理器原生字长
任务控制块tskTCB(Tash Control Block-线程控制模块)定义
💻任务控制块tskTCB
typedef struct tskTaskControlBlock { volatile StackType_t *pxTopOfStack; /* 栈顶 */ ListItem_t xStateListItem; /* 任务节点 */ StackType_t *pxStack; /* 任务栈起始地址 */ /* 任务名称,字符串形式 */(4) char pcTaskName[ configMAX_TASK_NAME_LEN ]; } tskTCB; typedef tskTCB TCB_t;
Copy
C
任务函数地址
任务节点(非指针)
栈顶 栈起始地址
任务名称
任务创建 TaskHandle_t xTaskCreateStatic
💻任务创建函数定义
#if( configSUPPORT_STATIC_ALLOCATION == 1 ) TaskHandle_t xTaskCreateStatic( TaskFunction_t pxTaskCode, const char * const pcName, const uint32_t ulStackDepth, void * const pvParameters, StackType_t * const puxStackBuffer, TCB_t * const pxTaskBuffer ) { TCB_t *pxNewTCB; TaskHandle_t xReturn; if ( ( pxTaskBuffer != NULL ) && ( puxStackBuffer != NULL ) ) { pxNewTCB = ( TCB_t * ) pxTaskBuffer; pxNewTCB->pxStack = ( StackType_t * ) puxStackBuffer; /* 创建新的任务 */ prvInitialiseNewTask( pxTaskCode, /* 任务入口 */ pcName, /* 任务名称,字符串形式 */ ulStackDepth, /* 任务栈大小,单位为字 */ pvParameters, /* 任务形参 */ &xReturn, /* 任务句柄 */ pxNewTCB ); /* 任务栈起始地址 */ } else { xReturn = NULL; } /* 返回任务句柄,如果任务创建成功,此时 xReturn 应该指向任务控制块 */ return xReturn; (10) } #endif /* configSUPPORT_STATIC_ALLOCATION */
Copy
C
宏定义意义:
FreeRTOS中表头有两种方法,一种是动态,一种是静态,
动态创建: 任务控制块TCB和Stack 动态分配,删除任务时可以释放
静态创建: 与上面相反 ...
configSUPPORT_STATIC_ALLOCATION 定义于FreeRTOSConfig.h
入口参数
任务函数位置(TaskFunction_t pxTaskCode) : ,而TaskFunction_t实际为一个空指针,重定义于projdefs.h
任务名称: const char * const pcName
Stack大小 const uint32_t ulStackDepth: 单位为字
任务起始地址: StackType_t * const puxStackBuffer
任务TCB指针: TCB_t * const pxTaskBuffer
处理逻辑:
如果TCB和任务起始地址均不为空,则将输入pxTaskBuffer.pxStack指向输入的puxStackBuffer
然后将已经处理完的TCB送入初始化函数prvInitialiseNewTask,同时送入一个&xReturn,供子函数修改为任务TCB地址
💡
显然则是一个中间简单函数,仅作简单处理(TCB指向StackBuffer),真的关键为任务初始化
若整个流程成功,则返回xReturn即新任务TCB,否则返回xReturn = NULL
任务初始化 static void prvInitialiseNewTask
💻任务初始化函数
设置TCB一些选项剩下的交给Stack初始化
计算StackTop: 为Stack起始地址+Stack大小-1
Stack做8字节对齐 Stack顶部= Stack顶部 & ~0x0007(8进制下,去除最后一位数,保证可被8整除)
任务节点初始化
将任务名存入TCB,并检测名字长度
初始化TCB中根List(初始化精简节点: 辅助值 本身: 计数器清零→指针指向自己)
初始化任务栈
让任务句柄指向任务控制块
任务栈初始化 pxPortInitialiseStack()
💻栈初始化代码
#define portINITIAL_XPSR ( 0x01000000 ) #define portSTART_ADDRESS_MASK ( ( StackType_t ) 0xfffffffeUL ) static void prvTaskExitError( void ) { /* 函数停止在这里 */ for (;;); } StackType_t *pxPortInitialiseStack( StackType_t *pxTopOfStack, TaskFunction_t pxCode, void *pvParameters ) { /* 异常发生时,自动加载到 CPU 寄存器的内容 */ pxTopOfStack--; *pxTopOfStack = portINITIAL_XPSR; pxTopOfStack--; *pxTopOfStack = ( ( StackType_t ) pxCode ) & portSTART_ADDRESS_MASK; (3) pxTopOfStack--; *pxTopOfStack = ( StackType_t ) prvTaskExitError; pxTopOfStack -= 5; /* R12, R3, R2 and R1 默认初始化为 0 */ *pxTopOfStack = ( StackType_t ) pvParameters; /* 异常发生时,手动加载到 CPU 寄存器的内容 */ pxTopOfStack -= 8; /* 返回栈顶指针,此时 pxTopOfStack 指向空闲栈 */ return pxTopOfStack; }
Copy
C
🖼️Stack结构示意图
初始化目的
当异常发生时,CPU 自动从栈一块区域中加载到CPU寄存器的内容,分别为 R0,R1,R2,R3,R12,R14,R15和xPSR的位24,且顺序不能变。
Stack初始化结构
—-最高地址区—
xPSR第24位 必须为1
R15(PC: 任务入口地址)(自动加载)
R14(LR寄存器): 任务返回地址,由于任务要求不能返回,设置为0(prvTaskExitError)使得返回报错
R12 R3 R2 R1初始化0(可被异常自动加载)
[R11→R4]
空闲区
—-最低地址区—
就绪列表
💻就序列表实际为List_t数组
1 /* 任务就绪列表 */ 2 List_t pxReadyTasksLists[ configMAX_PRIORITIES ];
Copy
C
定义于task.c
就绪列表初始化void prvInitialiseTaskLists()
💻就绪列表初始化图解
对每个List_t数组的每个成员调用链表初始化 vListInitialise
任务插入就绪列表
任务控制块里面有一个 xStateListItem 成员 类型为Node(ListItem_t) 将之插入即可
任务列表示例
💻创建任务相关示例
/* 初始化与任务相关的列表,如就绪列表 */ prvInitialiseTaskLists(); Task1_Handle = /* 任务句柄 */ xTaskCreateStatic( (TaskFunction_t)Task1_Entry, /* 任务入口 */ (char *)"Task1", /* 任务名称,字符串形式 */ (uint32_t)TASK1_STACK_SIZE , /* 任务栈大小,单位为字 */ (void *) NULL, /* 任务形参 */ (StackType_t *)Task1Stack, /* 任务栈起始地址 */ (TCB_t *)&Task1TCB ); /* 任务控制块 */ /* 将任务添加到就绪列表 */ vListInsertEnd( &( pxReadyTasksLists[1] ), &( ((TCB_t *)(&Task1TCB))->xStateListItem ) ); Task2_Handle = /* 任务句柄 */ xTaskCreateStatic( (TaskFunction_t)Task2_Entry, /* 任务入口 */ (char *)"Task2", /* 任务名称,字符串形式 */ (uint32_t)TASK2_STACK_SIZE , /* 任务栈大小,单位为字 */ (void *) NULL, /* 任务形参 */ (StackType_t *)Task2Stack, /* 任务栈起始地址 */ (TCB_t *)&Task2TCB ); /* 任务控制块 */ /* 将任务添加到就绪列表 */ vListInsertEnd( &( pxReadyTasksLists[2] ), &( ((TCB_t *)(&Task2TCB))->xStateListItem ) );
Copy
C
🖼️插入就绪列表示意图
初始化就绪列表和相关表格→创建任务→加入就绪列表
六,调度器实现
调度器启动vTaskStartScheduler()
💻调度器定义
void vTaskStartScheduler( void ) { /* 手动指定第一个运行的任务 */ pxCurrentTCB = &Task1TCB; (1) /* 启动调度器 */ if ( xPortStartScheduler() != pdFALSE ) { /* 调度器启动成功,则不会返回,即不会来到这里 */ (2) } }
Copy
C
同样定义域task.c
将当前TCB赋给全局指针pxCurrentTCB
启动调度器中断配置xPortStartScheduler
调度器中断配置 BaseType_t xPortStartScheduler
💻中断配置定义
/* * 参考资料《STM32F10xxx Cortex-M3 programming manual》4.4.7,百度搜索“PM0056”即可找到这个文档 * 在 Cortex-M 中,内核外设 SCB 中 SHPR3 寄存器用于设置 SysTick 和 PendSV 的异常优先级 * System handler priority register 3 (SCB_SHPR3) SCB_SHPR3:0xE000 ED20 * Bits 31:24 PRI_15[7:0]: Priority of system handler 15, SysTick exception * Bits 23:16 PRI_14[7:0]: Priority of system handler 14, PendSV */ #define portNVIC_SYSPRI2_REG (*(( volatile uint32_t *) 0xe000ed20)) #define portNVIC_PENDSV_PRI (((uint32_t) configKERNEL_INTERRUPT_PRIORITY ) << 16UL) #define portNVIC_SYSTICK_PRI (((uint32_t) configKERNEL_INTERRUPT_PRIORITY ) << 24UL ) BaseType_t xPortStartScheduler( void ) { /* 配置 PendSV 和 SysTick 的中断优先级为最低 */ (1) portNVIC_SYSPRI2_REG |= portNVIC_PENDSV_PRI; portNVIC_SYSPRI2_REG |= portNVIC_SYSTICK_PRI; /* 启动第一个任务,不再返回 */ prvStartFirstTask(); (2) /* 不应该运行到这里 */ return 0; }
Copy
C
置SysTick PendSV中断优先级最低: 通过修改寄存器思想
启动第一个任务不返回 prvStartFirstTask()
第一个任务启动(汇编编写) void prvStartFirstTask
💻第一个任务启动汇编定义
/* * 参考资料《STM32F10xxx Cortex-M3 programming manual》4.4.3,百度搜索“PM0056”即可找到这个文档 * 在 Cortex-M 中,内核外设SCB 的地址范围为:0xE000ED00-0xE000ED3F * 0xE000ED008 为 SCB 外设中 SCB_VTOR 这个寄存器的地址,里面存放的是向量表的起始地址,即 MSP 的地址 */ __asm void prvStartFirstTask( void ) { PRESERVE8 /*8位地址对齐*/ /* 在 Cortex-M 中,0xE000ED08 是 SCB_VTOR 这个寄存器的地址, (3) 里面存放的是向量表的起始地址,即 MSP 的地址 那么可知 memory:0x00000000 处存放的就是 MSP 的值。*/ ldr r0, =0xE000ED08 /*r0指向立即数 即SVC_VTOR地址*/ ldr r0, [r0] /*取地址内容即为0x00000000 即MSP地址*/ ldr r0, [r0] /*取地址内容 即 取0000内容 为0x200008DB*/ /* 设置主堆栈指针 msp 的值 */ msr msp, r0 /*有点多余,因为当系统启动的时候,执行完 Reset_Handler 的时候,向量表已经初始化完毕,MSP 的值就已经更新为向量表的起始值,即指向主堆栈 的栈顶指针。*/ /* 使能全局中断 */ cpsie i cpsie f dsb isb /* 调用 SVC 去启动第一个任务 */ svc 0 nop nop } /*中断指令简介*/ 1 CPSID I ;PRIMASK=1 ;关中断 2 CPSIE I ;PRIMASK=0 ;开中断 3 CPSID F ;FAULTMASK=1 ;关异常 4 CPSIE F ;FAULTMASK=0 ;开异常
Copy
C
任务操作
更新 MSP (主堆栈指针)
SVC_VTOR寄存器→MSP表(向量表)地址→向量表第一个内容→MSP寄存器
中断使能
📑🖼️Cortex-M中断寄存器解释
是产生SVC中断, 服务号为0代表SVC中断服务函数 (系统调用 Supervisor Call)
在 SVC 的中断服务函数里面真正切换到第一个任务
vPortSVCHandler() 中断服务函数函数
函数名称一致性问题
SVC 中断要想被成功响应,其函数名必须与向量表注册的名称一致
在启动文件的向量表中,SVC 的中断服务函数注册的名称是 SVC_Handler
但是在 FreeRTOS 中,官方版本写的是 vPortSVCHandler(),
解决方案
修改启动文件向量表的定义
FreeRTOS宏修改(官方支持的)
1 #define xPortPendSVHandler PendSV_Handler 2 #define xPortSysTickHandler SysTick_Handler 3 #define vPortSVCHandler SVC_Handler
Copy
MATLAB
函数内容
r3加载pxCurrentTCB的地址→r1(pxCurretTCB)→r0(新任务栈顶)
根据r0,加载手动区寄存器,r0自增
将新r0加载到psp,同时加载自动区,任务仅使用psp而不用msp
设置bascpro为0,
basepri 是一个中断屏蔽寄存器,大于等于此寄存器值的中断都将被屏蔽
此处即屏蔽所有的中断
打开中断优先级屏蔽(basepri)
设置r14(即返回的函数地址-返回函数指令)
当 r14 为 0xFFFFFFFX,执行是中断返回指令,而非返回地址
cortext-m3 的做法,
X 的 bit0 为 1 表示返回 thumb 状态
bit1 和 bit2 分别表回后 sp 用 msp 还是 psp以及返回到特权模式还是用户模式
任务切换的中断配置 portYIELD()
💻portYIELD定义
/* 在 task.h 中定义 */ #define taskYIELD() portYIELD() /* 在 portmacro.h 中定义 */ /* 中断控制状态5 /* 在 portmacro.h 中定义 */ /* 中断控制状态寄存器:0xe000ed04 * Bit 28 PENDSVSET: PendSV 悬起位寄存器:0xe000ed04 */ #define portNVIC_INT_CTRL_REG (*(( volatile uint32_t *) 0xe000ed04)) #define portNVIC_PENDSVSET_BIT ( 1UL << 28UL ) #define portSY_FULL_READ_WRITE ( 15 ) #define portYIELD() \ { \ /* 触发 PendSV,产生上下文切换 */ \ portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; (1) \ __dsb( portSY_FULL_READ_WRITE ); \ __isb( portSY_FULL_READ_WRITE ); \ }
Copy
C
任务切换就是在就绪列表中寻找优先级最高的就绪任务,然后去执行该任务,但是我们还不支持优先级,仅实现两个任务轮流切换
portYIELD 很简单,实际就是将 PendSV 的悬起位置1,没有其它中断运行的时候响应PendSV中断,执行我们写好的 PendSV中断服务函数
xPortPendSVHandler
函数内容
第一部分,由于被调用,原任务自动区进入相应任务Stack,腾出寄存器给中断函数用,同时切换到MSP,这是PSP指向了(Stack-r0位置)
🤔对于从bx切换而言,新任务的自动区不会自动加载,需要手动加载
r0=psp 入栈手动区寄存器 之后r0指向TopStack
r3加载pxCurretTCB的地址 r2加载对应的pxCurretTCB的值,为当前PCB的指针
R3 R14临时入栈(主MSP的堆栈 R3指向pxCurretTCB的地址 R14为中断结束后所需的状态)
🤔: 对于bl切换子任务,R3 R14虽然在自动区也不会自动入栈?
配置中断屏蔽BASEPRI ,准备进入临界区,关中断
vTaskSwitchContext 切换pxCurretTCB
开中断,BASEPRI清0
恢复r3 r14
加载r3→r1(pxCurretTCB)→r0(新任务栈顶)
加载手动区 加载psp(同时加载自动区)
通过R14 切换状态
七.临界段实现和中断开关
八.空闲任务实现
创建最低优先级