赞
踩
最近在研究实时操作系统FreeRTOS。FreeRTOS作为开源的RTOS,源码规模不大,可以供操作系统学习,加上我的STM32 Nano开发板正好可以学习OS。借着五一放假宅家里学习。
FreeRTOS有很强的可配置性和硬件适配性。代码里面有大量的宏定义和宏代码分支,看着比较费解,还有一些汇编和关键操作比较费解。然后融合了其他功能增加初学难度。读源码很久才搞通,为了验证是否理解到位,阉割出一个Mini版的系统,只包含基本的任务调度,最小内核。
本次开发基于FreeRTOS代码阉割。去掉了很多可配置性,实现任务创建和任务调度。理解本次代码的前提是先理前两篇xList数据结构和Heap4内存管理。
使用内核调度任务需要三步:
1.引入InitOS.h头文件后调用InitHardware()初始化硬件环境设置时钟和NVIC等。
2.调用xTaskCreate创建任务,创建任务的内部涉及到tcb内存申请、栈内存申请、初始化任务列表。把创建的任务加入就绪状态列表。
3.调用vTaskStartScheduler启动任务调度。内部先创建空闲任务,然后设置PendSV和SYSTick中断优先级为最低和设置SYSTick的定时频率。然后触发SVC中断启动第一个任务。由PendSVC中断读取pxCurrentTCB的栈信息引导执行进入第一个任务方法。后续就在Systick中断下判断是否需要切换上下文,如果需要切换上下文就在Systick中断里写PendSV中断触发器。PendSV中断处理函数完成OS任务切换。
实现分下面步骤:
1.定义基础类型FreeRTOSBase.h
基础的类型别名和定义都放在这个头文件里面,所有内核文件都直接或者间接引用了该文件。
2.定义FreeRTOSMini.h
定性内核基础数据结构TCB(任务控制块)和基础操作,主要的内核定义在该文件了,重点数据结构就是TCB定义,和基于各种状态列表的调度。
3.定义list.h
定义内核调度依赖list的数据结构和列表基本操作,内核的就绪列表,延迟列表等就是依赖该双向列表实现。
4.实现list.c
实现内核调度的列表,和列表基础操作。
5.定义heap_4.h
定义内存管理的链表结构,堆和基本操作。基于自由块链表管理内存控制。
6.实现heap_4.c
实现内存申请和释放,为内核提供内存申请基础。内核通过pvPortMalloc方法申请内存,通过vPortFree方法释放内存。
7.定义port.h
移植层头文件定性,和硬件移植相关放这里面。中断函数、屏蔽中断、初始化Systick时钟、触发PendSV等。
8.实现port.c
实现OS移植层。内核调度重要的Systick中断、PendSV中断、SVC中断、屏蔽中断、放开中断屏蔽等都在这里面实现。
9.实现FreeRTOSMini.c
基于前面的基础融合内核任务调度,任务调度由此实现。通过创建任务的方法,初始化内存分配、初始化调度列表、构造任务TCB、加入TCB到就绪列表等。通过开启调度方法初始化Systick时钟。触发SVC服务,通过SCV中断引导调度第一个任务,至此OS通过Systick在多个死循环的任务逻辑进行上下文切换来让不同任务调度使用CPU,中断实验MSP栈指针,任务用PSP栈指针,切换上下文时候PendSV逻辑通过MSP在主栈执行,在PendSV里面压栈当前任务的寄存器值到PSP指向的当前任务栈,然后切换当前任务指针,再出栈切换后任务的压栈寄存器值,在退出PendSV时候PSP和寄存器值已经换成切换的新任务的值了。就这样一直在Systick时钟节拍下判断是否需要切换上下文,需要切换上下文就触发PendSV异常进行上下文切换。这个代码和以上共同构造Mini内核。后面部分就是使用和测试内核的部分了。
10.定义InitOS.h
定义初始化OS硬件的操作,方便使用。
11.实现InitOS.c
实现操作系统的硬件初始化工作。初始化时钟等。
12.定义led.h
定义led的操作和初始化。
13.定义led.c
实现led操作逻辑。
14.定义beep.h
定义蜂鸣器操作。
15.实现beep.c
实现蜂鸣器操作。
16.实现main.c
测试OS的任务调度
具体的就只能看代码理解了。每个概念都够一个讲解了。TC、Systick、Nvic、PendSV、SVC、屏蔽中断、切换上下文和保存现场等。
实现代码如下:
1.定义基础类型FreeRTOSBase.h
//定义基础类型的别名 //定义宏,防止头文件重复引用 #ifndef __FreeRTOSBase #define __FreeRTOSBase #include <stdlib.h> //定义BaseType_t typedef long BaseType_t; //定义UBaseType_t typedef unsigned long UBaseType_t; //定义任务函数必须符合的原型 typedef void (*TaskFunction_t)(void*); //定义uint16_t typedef unsigned short int uint16_t; //定义configSTACK_DEPTH_TYPE #define configSTACK_DEPTH_TYPE uint16_t //定义uint32_t typedef unsigned int uint32_t; //定义portSTACK_TYPE #define portSTACK_TYPE uint32_t //定义portSTACK_TYPE typedef portSTACK_TYPE StackType_t; //定义TickType_t typedef uint32_t TickType_t; //定义uint8_t typedef unsigned char uint8_t; //定义pdFALSE #define pdFALSE ( ( BaseType_t ) 0 ) //定义pdTRUE #define pdTRUE ( ( BaseType_t ) 1 ) //定义pdPASS #define pdPASS ( pdTRUE ) //定义pdFAIL #define pdFAIL ( pdFALSE ) //私有方法 #define PRIVILEGED_FUNCTION //私有数据 #define PRIVILEGED_DATA //定义8位对齐 #define portBYTE_ALIGNMENT 8 //定义对齐标识 #define portBYTE_ALIGNMENT_MASK ( 0x0007 ) //定义错误分支 #define mtCOVERAGE_TEST_MARKER() //定义指针类型 #define portPOINTER_SIZE_TYPE uint32_t //设置初始堆栈所需的常量。 #define portINITIAL_XPSR ( 0x01000000 ) //为了严格遵守 Cortex-M 规范,任务起始地址应 //清除第0位,因为它是在退出ISR时加载到PC中的。 #define portSTART_ADDRESS_MASK ( ( StackType_t ) 0xfffffffeUL ) //NVIC寄存器相关定义 #define portNVIC_SHPR3_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 ) #define portNVIC_SYSTICK_CTRL_REG ( *( ( volatile uint32_t * ) 0xe000e010 ) ) #define portNVIC_SYSTICK_LOAD_REG ( *( ( volatile uint32_t * ) 0xe000e014 ) ) #define portNVIC_SYSTICK_CURRENT_VALUE_REG ( *( ( volatile uint32_t * ) 0xe000e018 ) ) #define portNVIC_SYSTICK_CLK_BIT ( 1UL << 2UL ) #define portNVIC_SYSTICK_INT_BIT ( 1UL << 1UL ) #define portNVIC_SYSTICK_ENABLE_BIT ( 1UL << 0UL ) #define portNVIC_INT_CTRL_REG ( *( ( volatile uint32_t * ) 0xe000ed04 ) ) #define portNVIC_PENDSVSET_BIT ( 1UL << 28UL ) #define portSY_FULL_READ_WRITE ( 15 ) //空闲任务优先级定义最低 #define tskIDLE_PRIORITY ((UBaseType_t ) 0U) //每个任务在关键嵌套中维护自己的中断状态 static UBaseType_t uxCriticalNesting = 0xaaaaaaaa; //最大延迟值 #define portMAX_DELAY ( TickType_t ) 0xffffffffUL //重置就绪优先级 #define portRESET_READY_PRIORITY( uxPriority, uxReadyPriorities ) ( uxReadyPriorities ) &= ~( 1UL << ( uxPriority ) ) #endif
2.定义FreeRTOSMini.h
//实现一个迷你版的FreeRTOS作为FreeRTOS的学习。功能只包含任务调度部分。且不让挂起和删除任务。 //并且去掉了很多宏配置性来减少代码宏的复杂度 //提供xTaskCreate方法创建任务 //提供vTaskStartScheduler方法开启调度器 //提供vTaskDelay进行任务延时 //定义宏,防止头文件重复引用 #ifndef __FreeRTOSMini #define __FreeRTOSMini #include <stdlib.h> //引入基础类型定义的头文件 #include "FreeRTOSBase.h" //芯片头文件引入 #include "stm32f10x_it.h" //芯片中断nvic引入 #include "stm32f10x_nvic.h" //引入列表 #include "list.h" //写入实际的CPU内核时钟频率,也就是CPU指令执行频率,一般称为Fcclk。配置此值是为了正确的配置系统节拍中断周期。 #define configCPU_CLOCK_HZ ( ( unsigned long ) 72000000 ) //RTOS 系统节拍中断的频率。即一秒中断的次数,每次中断RTOS都会进行任务调度。 //系统节拍中断用来测量时间,所以,越高的测量频率意味着可测到越高的分辨率时间。 //可是,高的系统节拍中断频率也意味着RTOS内核占用更多的CPU时间,所以会下降效率。 //RTOS演示例程都是使用系统节拍中断频率为1000HZ,这是为了测试RTOS内核,比实际使用的要高。(实际使用时不用这么高的系统节拍中断频率) #define configTICK_RATE_HZ ( ( TickType_t ) 100 ) // RTOS内核总计可用的有效的RAM大小。仅在你使用官方下载包中附带的内存分配策略时,才有可能用到此值。 //每当建立任务、队列、互斥量、软件定时器或信号量时,RTOS内核会为此分配RAM,这里的RAM都属于configTOTAL_HEAP_SIZE指定的内存区。 //后续的内存配置会详细讲到官方给出的内存分配策略。 //到了FreeRTOS下,这个系统栈主要是手动入栈的寄存器,中断服务程序里面的局部变量,中断嵌入要用到 #define configTOTAL_HEAP_SIZE (( size_t) (17*1024)) //配置系统内核优先级,这里使用的是4bit优先级,所以最低为15,跟1中的配置相同 #define configLIBRARY_KERNEL_INTERRUPT_PRIORITY 15 //配置应用程序有效的优先级数目。任何数量的任务均可以共享一个优先级,使用协程能够单独的给与它们优先权。 //在RTOS内核中,每一个有效优先级都会消耗必定量的RAM,所以这个值不要超过你的应用实际须要的优先级数目。 #define configMAX_PRIORITIES ( 32 ) //配置内核使用的中断优先级,这里是8bit,默认最低优先级FF #define configKERNEL_INTERRUPT_PRIORITY 255 //配置系统所能调用管理的最高优先级191为BF,最高优先级是11,优先级0-10不归系统管理 #define configMAX_SYSCALL_INTERRUPT_PRIORITY 191 /* equivalent to 0xb0, or priority 11. */ //任务控制块结构体TCB,创建任务成功后返回该结构体 typedef struct tskTaskControlBlock { //告诉编译器该变量值可能随时发生变化,且这种变化并不是代码引起的。 //指向放置在任务堆栈上的最后一项的位置。 这必须是 TCB 结构的第一个成员 volatile StackType_t* pxTopOfStack; //表示任务状态,不同的状态会挂接在不同的状态链表下 //一共有四种状态分别是运行状态(Running State),就绪状态(Ready State), //阻塞状态(Blocked State),挂起状态(Suspended State) //运行状态:正在执行的任务。 //就绪状态:等待获得执行权的任务。 //阻塞状态:直到某些条件达成才会重新进入就绪态等待获得执行权,否则不会执行的任务。 //挂起状态:除非被主动恢复,否则永远不会执行。 //这四种链表分别对应着pxCurrentTCB,pxReadyTasksLists,pxDelayedTaskList,xSuspendedTaskList这四个变量。 //除运行状态外,任务处于其它状态时,都是通过将任务TCB中的xStateListItem挂到相应的链表下来表示的 ListItem_t xStateListItem; /*< The list that the state list item of a task is reference from denotes the state of that task (Ready, Blocked, Suspended ). */ //事件链表项,会挂接到不同事件链表下 ListItem_t xEventListItem; /*< Used to reference a task from an event list. */ //任务优先级,0是最低的,越大优先级越高 UBaseType_t uxPriority; /*< The priority of the task. 0 is the lowest priority. */ //指向堆栈起始位置(低地址),这只是单纯的一个分配空间的地址,可以用来检测堆栈是否溢出 StackType_t* pxStack; /*< Points to the start of the stack. */ //任务名称 char pcTaskName[16]; //记录临界段的嵌套层数 UBaseType_t uxCriticalNesting; //跟踪调试用的变量 UBaseType_t uxTCBNumber; /*< Stores a number that increments each time a TCB is created. It allows debuggers to determine when a task has been deleted and then recreated. */ UBaseType_t uxTaskNumber; /*< Stores a number specifically for use by third party trace code. */ //任务优先级被临时提高时,保存任务原本的优先级 UBaseType_t uxBasePriority; /*< The priority last assigned to the task - used by the priority inheritance mechanism. */ UBaseType_t uxMutexesHeld; //用来标记这个任务的栈是不是静态分配的 uint8_t ucStaticallyAllocated; //延时是否被打断 uint8_t ucDelayAborted; //错误标识 int iTaskErrno; } tskTCB; //老别名 typedef tskTCB TCB_t; //定义tcb别名 typedef struct tskTaskControlBlock* TaskHandle_t; //将 pxTCB 表示的任务放入相应的就绪列表中任务。 它被插入到列表的末尾。 //把PCB的状态列表项加入对应优先级的状态列表后面 #define prvAddTaskToReadyList(pxTCB) \ taskRECORD_READY_PRIORITY((pxTCB)->uxPriority); \ /*把TCB的状态列表项插入对应优先级的结尾*/ \ listINSERT_END(&(pxReadyTasksLists[(pxTCB)->uxPriority]), &((pxTCB)->xStateListItem)); //pxTaskCode:任务函数 void 函数名(void *pvParameters) //pcName:任务名称,小于configMAX_TASK_NAME_LEN,建议小于16 //usStackDepth:堆栈大小(整数) //pvParameters:给任务传递的参数 //uxPriority:任务优先级(整数) //pxCreatedTask:任务的句柄,后期用他操作任务 BaseType_t xTaskCreate(TaskFunction_t pxTaskCode, const char* const pcName, const configSTACK_DEPTH_TYPE usStackDepth, void* const pvParameters, UBaseType_t uxPriority, TaskHandle_t* const pxCreatedTask); //执行新任务的初始化 static void prvInitialiseNewTask(TaskFunction_t pxTaskCode, const char* const pcName, const uint32_t ulStackDepth, void* const pvParameters, UBaseType_t uxPriority, TaskHandle_t* const pxCreatedTask, TCB_t* pxNewTCB); //任务退出错误,任务不是死循环会走到这里 void prvTaskExitError(void); //把任务加入就绪列表 static void prvAddNewTaskToReadyList(TCB_t* pxNewTCB); //初始化任务列表。在第一个任务被创建时候调用 static void prvInitialiseTaskLists(void) PRIVILEGED_FUNCTION; //重置下一个出块时间 static void prvResetNextTaskUnblockTime(void); //挂起所有 void vTaskSuspendAll(void); //恢复所有 BaseType_t xTaskResumeAll(void); //开启调度器 void vTaskStartScheduler(void); //任务增加节拍数 //如果调度器没被挂起 //xConstTickCount如果为0那么tick溢出了,切换延迟列表和溢出延迟列表 //如果系统节拍数大于等于下一个出块时间,遍历整个延迟列表把延迟超期的任务都推入就绪列表,同时把下一个出块时间设置为下一个延迟超时时间,如果没有就设置为最大值 //以下三种情况进行上下文切换 //移入就绪队列的任务优先级大于当前任务就标记需要进行切换 //如果是抢占模式且按时间切片,那么和当前运行任务相同优先级列表数量大于1也要切换,同级别任务共享时间片 //xYieldPending为TRUE时候也要切换 BaseType_t xTaskIncrementTick(void); //退出临界区 void vPortExitCritical(void); //设置屏蔽优先级 static void vPortSetBASEPRI(uint32_t ulBASEPRI); //切换任务上下文 void vTaskSwitchContext(void); //空闲任务 static void prvIdleTask(void* pvParameters); //启动调度器 BaseType_t xPortStartScheduler(void); //开启第一个任务 /* 取MSP的初始值的思路是先根据向量表的位置寄存器VTOR(0xE000ED08)来获取向量表存储的地址; 再根据向量表存储的地址,取出第一个元素__initial_sp,写入 MSP Cortex-M3 处理器,上电默认进入线程的特权模式,使用 MSP 作为堆栈指针; 从上电跑到这里,经过一系列的函数调用,出栈,入栈,MSP已经不是最开始的初始化的位置; 这里通过 MSR 重新初始化 MSP,丢弃主堆栈中的数据;这是一条不归路,代码跑到这里,不会再返回之前的调用路径。 */ void prvStartFirstTask(void); //任务延时 //xTicksToDelay:要延迟的节拍数 void vTaskDelay(const TickType_t xTicksToDelay); //把当前任务加入延迟列表 //xTicksToWait:延迟的节拍数 static void prvAddCurrentTaskToDelayedList(TickType_t xTicksToWait, const BaseType_t xCanBlockIndefinitely); #endif
3.定义list.h
//定义宏,防止头文件重复引用 #ifndef __list #define __list #include <stdlib.h> #include "FreeRTOSBase.h" //申明列表结构 struct xLIST; //定义列表项结构 struct xLIST_ITEM { //列表项的值,告诉编译器不要优化,随时可能变化 volatile TickType_t xItemValue; //指向下一个列表项 struct xLIST_ITEM* volatile pxNext; //指向前一个列表项 struct xLIST_ITEM* volatile pxPrevious; //指向所属对象,主要指列表项所在的tcb void* pvOwner; //指向所属列表 struct xLIST* volatile pxContainer; }; typedef struct xLIST_ITEM ListItem_t; //定义列表项结构迷你版。End节点不需要所属对象指针和所属列表。为了少8个字节设计的 struct xMINI_LIST_ITEM { //列表项的值,默认设置类型最大值 volatile TickType_t xItemValue; //指向下一个列表项 struct xLIST_ITEM* volatile pxNext; //指向前一个列表项 struct xLIST_ITEM* volatile pxPrevious; }; typedef struct xMINI_LIST_ITEM MiniListItem_t; //定义列表结构体,调度器基于该结构进行调度实现 typedef struct xLIST { //该列表的元素个数 volatile UBaseType_t uxNumberOfItems; //通过他遍历列表。指向一个列表节点或终结点 ListItem_t* volatile pxIndex; //终结点 MiniListItem_t xListEnd; } List_t; //得到列表第一项 #define listGET_OWNER_OF_HEAD_ENTRY( pxList ) ( ( &( ( pxList )->xListEnd ) )->pxNext->pvOwner ) //得到列表是否为空 #define listLIST_IS_EMPTY(pxList) (((pxList)->uxNumberOfItems == (UBaseType_t )0) ? pdTRUE : pdFALSE) //设置列表项所属对象。通常指向tcb #define listSET_LIST_ITEM_OWNER(pxListItem, pxOwner) (( pxListItem )->pvOwner = (void *)(pxOwner)) //设置列表项的值 #define listSET_LIST_ITEM_VALUE(pxListItem, xValue) ((pxListItem)->xItemValue = (xValue)) //得到列表头部第一项的值 #define listGET_ITEM_VALUE_OF_HEAD_ENTRY(pxList) (((pxList)->xListEnd).pxNext->xItemValue ) //得到列表项的值 #define listGET_LIST_ITEM_VALUE(pxListItem) ((pxListItem)->xItemValue) //得到列表项的容器对象 #define listLIST_ITEM_CONTAINER( pxListItem ) ( ( pxListItem )->pxContainer ) //得到列表项目个数 #define listCURRENT_LIST_LENGTH(pxList) ((pxList)->uxNumberOfItems) //得到下一个tcb #define listGET_OWNER_OF_NEXT_ENTRY(pxTCB,pxList) \ { \ List_t * const pxConstList = (pxList); \ /*移动指针到下一个元素*/ \ (pxConstList)->pxIndex = (pxConstList)->pxIndex->pxNext; \ /*移动到最后了*/ \ if((void *) (pxConstList)->pxIndex ==(void *) &((pxConstList)->xListEnd)) \ { \ (pxConstList)->pxIndex=(pxConstList)->pxIndex->pxNext; \ } \ /*得到TCB*/ \ (pxTCB)=(pxConstList)->pxIndex->pvOwner; \ } //用宏定义的把列表项插入列表结尾 //pxList:列表常量指针 //pxNewListItem:要插入的新列表项指针 #define listINSERT_END( pxList, pxNewListItem ) \ { \ ListItem_t * const pxIndex = (pxList)->pxIndex; \ (pxNewListItem)->pxNext = pxIndex; \ (pxNewListItem)->pxPrevious = pxIndex->pxPrevious; \ \ pxIndex->pxPrevious->pxNext = (pxNewListItem); \ pxIndex->pxPrevious = (pxNewListItem); \ \ (pxNewListItem)->pxContainer = (pxList); \ \ ((pxList)->uxNumberOfItems)++; \ } //把列表项从列表里面移除,返回列表数量 //pxItemToRemove:要异常的列表项 #define listREMOVE_ITEM(pxItemToRemove) \ { \ /*得到要移除列表项指向的列表指针*/ \ List_t * const pxList = (pxItemToRemove)->pxContainer; \ \ /*要移除列表项的后面节点的前一项指针指向要移除节点的前一个节点*/ \ ( pxItemToRemove)->pxNext->pxPrevious = (pxItemToRemove)->pxPrevious; \ /*要移除列表项前一个节点的后一项指针指向要移除节点后一项*/ \ ( pxItemToRemove)->pxPrevious->pxNext = (pxItemToRemove)->pxNext; \ /*确保索引指向一个有效的项目*/ \ if(pxList->pxIndex == (pxItemToRemove)) \ { \ pxList->pxIndex = (pxItemToRemove)->pxPrevious; \ } \ /*把要移除项的所属设置空指针,不属于任何列表*/ \ (pxItemToRemove)->pxContainer = NULL; \ /*列表数量减1*/ \ (pxList->uxNumberOfItems)--; \ } //初始化列表像 //pxItem:列表项常量指针 void vListInitialiseItem(ListItem_t* const pxItem); //初始化列表方法 //pxList:列表指针 void vListInitialise(List_t* const pxList); //往列表里插入列表项,按值排序 //pxList:列表常量指针 //pxNewListItem:要插入的新列表项指针 void vListInsert(List_t* const pxList, ListItem_t* const pxNewListItem); //把列表项从列表里面移除,返回列表数量 //pxItemToRemove:要异常的列表项 UBaseType_t uxListRemove(ListItem_t* const pxItemToRemove); #endif
4.实现list.c
#include "list.h" //初始化列表像 //pxItem:列表项常量指针 void vListInitialiseItem(ListItem_t* const pxItem) { //设置列表项所属为空指针,不属于任何列表 pxItem->pxContainer = NULL; } //初始化列表方法 //pxList:列表指针 void vListInitialise(List_t* const pxList) { //pxIndex指向xListEnd pxList->pxIndex = (ListItem_t*)&(pxList->xListEnd); //设置结束元素的值为最大值 pxList->xListEnd.xItemValue = portMAX_DELAY; //结束元素的下一个元素指向自己 pxList->xListEnd.pxNext = (ListItem_t*)&(pxList->xListEnd); //结束元素的前一个元素也指向自己 pxList->xListEnd.pxPrevious = (ListItem_t*)&(pxList->xListEnd); //设置列表元素数量为0 pxList->uxNumberOfItems = (UBaseType_t)0U; } //往列表里插入列表项,按值排序 //pxList:列表常量指针 //pxNewListItem:要插入的新列表项指针 void vListInsert(List_t* const pxList, ListItem_t* const pxNewListItem) { ListItem_t* pxIterator; //要插入的列表项的值 const TickType_t xValueOfInsertion = pxNewListItem->xItemValue; //按值大小排序插入新项目到列表里。如果有相同值的项目,那么放在相同值项目后面。确保TCB相同值共享CPU if (xValueOfInsertion == portMAX_DELAY) { pxIterator = pxList->xListEnd.pxPrevious; } else { //从xListEnd节点开始。一直找到值大于新插入项值的节点 for (pxIterator = (ListItem_t*)&(pxList->xListEnd); pxIterator->pxNext->xItemValue <= xValueOfInsertion; pxIterator = pxIterator->pxNext) { } } //新节点执行目标节点下一个节点 pxNewListItem->pxNext = pxIterator->pxNext; //目标节点下一个节点的前一个节点指向新节点。这两步把新节点和目标节点下一个节点双向连好了 pxNewListItem->pxNext->pxPrevious = pxNewListItem; //新节点前一个节点指向目标节点。 pxNewListItem->pxPrevious = pxIterator; //前一个节点的下一个节点指向新节点。这两步把新节点和目标节点双向连好了 pxIterator->pxNext = pxNewListItem; //新节点所属指向列表 pxNewListItem->pxContainer = pxList; //列表数量加1 (pxList->uxNumberOfItems)++; } //把列表项从列表里面移除,返回列表数量 //pxItemToRemove:要异常的列表项 UBaseType_t uxListRemove(ListItem_t* const pxItemToRemove) { //得到要移除列表项指向的列表指针 List_t* const pxList = pxItemToRemove->pxContainer; //要移除列表项的后面节点的前一项指针指向要移除节点的前一个节点 pxItemToRemove->pxNext->pxPrevious = pxItemToRemove->pxPrevious; //要移除列表项前一个节点的后一项指针指向要移除节点后一项 pxItemToRemove->pxPrevious->pxNext = pxItemToRemove->pxNext; //确保索引指向一个有效的项目 if (pxList->pxIndex == pxItemToRemove) { pxList->pxIndex = pxItemToRemove->pxPrevious; } else { mtCOVERAGE_TEST_MARKER(); } //把要移除项的所属设置空指针,不属于任何列表 pxItemToRemove->pxContainer = NULL; //列表数量减1 (pxList->uxNumberOfItems)--; //返回列表数量 return pxList->uxNumberOfItems; }
5.定义heap_4.h
//定义宏,防止头文件重复引用
#ifndef __heap4
#define __heap4
//申请内存
//xWantedSize:想要申请的大小
void* pvPortMalloc(size_t xWantedSize);
//释放分配的内存
//要释放的内存首地址
void vPortFree(void* pv);
#endif
6.实现heap_4.c
//堆内存管理的实现,该模式可以申请和释放内存。定义了ucHeap全局数组充当堆内存池。 //实际分配的该数组的空间。采用链表管理分配,将多个自由内存块链接起来。 //定义了#define pvPortMallocStack pvPortMalloc,所有内核栈也是从这里申请内存 /*动态内存分配是C语言编程中的一个概念,而不是FreeRTOS或者多任务里面的特有的概念。 FreeRTOS需要涉及到动态内存分配是因为内核对象是动态构造的。 在通用的C语言编程环境里,我们可以使用标准库中的malloc()和free()来进行动态内存分配的操作, 但这些函数对于实时应用是不合适的,原因有如下几点: 1.在嵌入式系统中他们不总是可用的,有的环境不支持或者没有实现 2.他们的底层实现可能需要较多的资源,调用时开销大 3.他们很少情况下是线程安全的 4.他们的调用耗时是不确定的(not deterministic),每次调用耗时都可能不一样(标准只要求他们的执行尽可能的快速完成并返回) 5.可能会造成内存碎片问题 6.他们会使连接器的配置变的复杂 7.会使得代码很难调试 提供下面方法给内核申请和释放内存 void *pvPortMalloc( size_t xWantedSize ) void vPortFree( void *pv ) */ #include "FreeRTOSMini.h" #include "heap_4.h" //最小可用做内存分配的堆大小 //一个内存块分配给需求之后剩余大小如果小于对结构体大小就不能当做空闲块。因为分配内存要用堆结构体记录分配信息 //剩余空间都小于结构体大小了就不能当新的空闲块 //所以最小堆大小就是堆结构体打印左一1位,即堆结构体大小加八个比特 #define heapMINIMUM_BLOCK_SIZE ((size_t) ( xHeapStructSize << 1)) /*假设8bit 字节!*/ #define heapBITS_PER_BYTE ((size_t) 8) /*如果配置了运用程序自己定义堆数组就使用外部的堆。 ucHeap的存放位置是在编译期间,由链接器(linker)决定的, 如果ucHeap被放到了访问速度较慢的外部SRAM,则任务的执行速度将受到不利影响。 这个时候可以由开发者自己定义ucHeap内存池数组*/ #if (configAPPLICATION_ALLOCATED_HEAP == 1) //引入自定义堆 extern uint8_t ucHeap[configTOTAL_HEAP_SIZE]; #else //堆就是一个u8类型大数组 PRIVILEGED_DATA static uint8_t ucHeap[configTOTAL_HEAP_SIZE]; #endif //用于内存管理的链表结构,通过该链表把空闲内块链接起来管理 typedef struct A_BLOCK_LINK { //下一个空闲块 struct A_BLOCK_LINK* pxNextFreeBlock; //空闲块的大小 size_t xBlockSize; } BlockLink_t; /*-----------------------------------------------------------*/ //把要释放的内存块放入自由内存块链表。按地址找到插入位置。如果能和前后正好接上就做块合并处理 static void prvInsertBlockIntoFreeList(BlockLink_t* pxBlockToInsert) PRIVILEGED_FUNCTION; //初始化堆,第一个内存分配时候自动调用 static void prvHeapInit(void) PRIVILEGED_FUNCTION; /*-----------------------------------------------------------*/ //堆结构体的大小。每次分配要用堆结构体记录分配的一块信息。记录的结构体本身要占空间 static const size_t xHeapStructSize = (sizeof(BlockLink_t) + ((size_t)(portBYTE_ALIGNMENT - 1))) & ~((size_t)portBYTE_ALIGNMENT_MASK); /*创建内存块开始和结束节点,通过xStart和pxEnd遍历找到合适内存分配*/ PRIVILEGED_DATA static BlockLink_t xStart, * pxEnd = NULL; /* 跟踪分配和释放内存的调用次数以及 * 剩余的空闲字节数,但没有说明碎片。 */ PRIVILEGED_DATA static size_t xFreeBytesRemaining = 0U; PRIVILEGED_DATA static size_t xMinimumEverFreeBytesRemaining = 0U; PRIVILEGED_DATA static size_t xNumberOfSuccessfulAllocations = 0; PRIVILEGED_DATA static size_t xNumberOfSuccessfulFrees = 0; //已经分配的比特数 PRIVILEGED_DATA static size_t xBlockAllocatedBit = 0; /*-----------------------------------------------------------*/ //申请内存 //xWantedSize:想要申请的大小 void* pvPortMalloc(size_t xWantedSize) { BlockLink_t* pxBlock, * pxPreviousBlock, * pxNewBlockLink; void* pvReturn = NULL; //挂起所有任务 vTaskSuspendAll(); { /* 如果这是对 malloc 的第一次调用,那么堆将需要 * 初始化设置空闲块列表。 */ if (pxEnd == NULL) { //第一次申请内存就初始化堆 prvHeapInit(); } else { mtCOVERAGE_TEST_MARKER(); } //检查申请分配的内存是否具有合法性和检查申请的内存是否过大 if ((xWantedSize & xBlockAllocatedBit) == 0) { //分配的数必须大于0 if ((xWantedSize > 0) && ((xWantedSize + xHeapStructSize) > xWantedSize)) { //申请的大小要加上堆记录结构体大小 xWantedSize += xHeapStructSize; /*确保块对齐*/ if ((xWantedSize & portBYTE_ALIGNMENT_MASK) != 0x00) { /*需要字节对齐。检查溢出。*/ if ((xWantedSize + (portBYTE_ALIGNMENT - (xWantedSize & portBYTE_ALIGNMENT_MASK))) > xWantedSize) { xWantedSize += (portBYTE_ALIGNMENT - (xWantedSize & portBYTE_ALIGNMENT_MASK)); } else { xWantedSize = 0; } } else { mtCOVERAGE_TEST_MARKER(); } } else { xWantedSize = 0; } //申请大小小于剩余的内存大小 if ((xWantedSize > 0) && (xWantedSize <= xFreeBytesRemaining)) { /*从开始(最低地址)块遍历列表,直到 * 找到一个足够大小的。 */ //前一个块,从开始块找 pxPreviousBlock = &xStart; //当前块,从开始取下一个空闲块 pxBlock = xStart.pxNextFreeBlock; //一直找到大小大于申请大小的块或者找到最后块 while ((pxBlock->xBlockSize < xWantedSize) && (pxBlock->pxNextFreeBlock != NULL)) { //前一个块等于当前块 pxPreviousBlock = pxBlock; //当前块往下移一个 pxBlock = pxBlock->pxNextFreeBlock; } //如果当前块不等于结束就代表找到了符合的内存块了 if (pxBlock != pxEnd) { //返回当前块地址加上一个堆结构大小的地址 pvReturn = (void*)(((uint8_t*)pxPreviousBlock->pxNextFreeBlock) + xHeapStructSize); //前一个块执行当前块的下一个块 pxPreviousBlock->pxNextFreeBlock = pxBlock->pxNextFreeBlock; //当前块分配给要的空间后如果剩余大小大于堆最小大小。剩下的空间就是一个可用的块 //构造一个新的空闲块 if ((pxBlock->xBlockSize - xWantedSize) > heapMINIMUM_BLOCK_SIZE) { //新空闲块执行当前块分配给想要空间大小后的地址 pxNewBlockLink = (void*)(((uint8_t*)pxBlock) + xWantedSize); //新空闲块大小为当前块大小减去要申请的大小 pxNewBlockLink->xBlockSize = pxBlock->xBlockSize - xWantedSize; //当前块已经把剩下空间构成新块了,他本身大小变为申请的大小了 pxBlock->xBlockSize = xWantedSize; //把新的空闲块插入空闲块列表 prvInsertBlockIntoFreeList(pxNewBlockLink); } else { mtCOVERAGE_TEST_MARKER(); } //空闲内存大小减去申请的大小 xFreeBytesRemaining -= pxBlock->xBlockSize; if (xFreeBytesRemaining < xMinimumEverFreeBytesRemaining) { xMinimumEverFreeBytesRemaining = xFreeBytesRemaining; } else { mtCOVERAGE_TEST_MARKER(); } /* The block is being returned - it is allocated and owned * by the application and has no "next" block. */ pxBlock->xBlockSize |= xBlockAllocatedBit; //当前块已经分配出去了,所以下一个自由块执行空指针 pxBlock->pxNextFreeBlock = NULL; //成功分配内存的数量加1 xNumberOfSuccessfulAllocations++; } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } //恢复挂起的任务 (void)xTaskResumeAll(); //返回内存地址 return pvReturn; } /*-----------------------------------------------------------*/ //释放分配的内存 //要释放的内存首地址 void vPortFree(void* pv) { uint8_t* puc = (uint8_t*)pv; //堆分配块指针 BlockLink_t* pxLink; if (pv != NULL) { //由于申请内存返回地址加了堆结构体的大小。所以这里减去堆结构体大小就得到要释放内存的结构体 puc -= xHeapStructSize; //转换为内存结构体指针 pxLink = (void*)puc; if ((pxLink->xBlockSize & xBlockAllocatedBit) != 0) { //是使用的内存块的下一个自由块为空指针 //防止是用户程序传错了要释放的地址 if (pxLink->pxNextFreeBlock == NULL) { /* 该块正在返回到堆中 - 它不再是分配 */ pxLink->xBlockSize &= ~xBlockAllocatedBit; //挂起所有任务 vTaskSuspendAll(); { //空闲内存大小加上释放块的大小 xFreeBytesRemaining += pxLink->xBlockSize; //把要释放的内存块加入空闲内存列表 prvInsertBlockIntoFreeList(((BlockLink_t*)pxLink)); //成功释放内存次数加1 xNumberOfSuccessfulFrees++; } //恢复所有挂起的任务 (void)xTaskResumeAll(); } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } } /*-----------------------------------------------------------*/ //得到自由堆大小 size_t xPortGetFreeHeapSize(void) { return xFreeBytesRemaining; } /*-----------------------------------------------------------*/ size_t xPortGetMinimumEverFreeHeapSize(void) { return xMinimumEverFreeBytesRemaining; } /*-----------------------------------------------------------*/ void vPortInitialiseBlocks(void) { /*这只是为了保持链接器安静。*/ } /*-----------------------------------------------------------*/ //用来第一次申请内存时候初始化堆 //第一步:起始地址做字节对齐,保存pucAlignedHeap 可用空间大小为xTotalHeapSize //第二步:计算首尾 ,这里需要注意的是链表的尾部指针是保存到该地址尾部的 //第三部:完成链表的初始化,记录内存块信息 static void prvHeapInit(void) { BlockLink_t* pxFirstFreeBlock; uint8_t* pucAlignedHeap; size_t uxAddress; size_t xTotalHeapSize = configTOTAL_HEAP_SIZE; //起始地址做字节对齐处理 //执行堆数组开始位置 uxAddress = (size_t)ucHeap; //开始地址做8位对齐 if ((uxAddress & portBYTE_ALIGNMENT_MASK) != 0) { uxAddress += (portBYTE_ALIGNMENT - 1); uxAddress &= ~((size_t)portBYTE_ALIGNMENT_MASK); //减去对齐舍弃的字节 xTotalHeapSize -= uxAddress - (size_t)ucHeap; } //对齐后可以用的起始地址 pucAlignedHeap = (uint8_t*)uxAddress; //开始的下一个自由块执行对齐的堆首地址 xStart.pxNextFreeBlock = (void*)pucAlignedHeap; //块大小设置0 xStart.xBlockSize = (size_t)0; //按对齐的堆的首地址和堆的总大小计算堆的结束位置 uxAddress = ((size_t)pucAlignedHeap) + xTotalHeapSize; uxAddress -= xHeapStructSize; uxAddress &= ~((size_t)portBYTE_ALIGNMENT_MASK); //设置计算地址到结束地址,结束块大小为0,结束块的下一个自由块执行空指针 pxEnd = (void*)uxAddress; pxEnd->xBlockSize = 0; pxEnd->pxNextFreeBlock = NULL; //得到第一个自由块。开始地址是对齐堆的首地址 pxFirstFreeBlock = (void*)pucAlignedHeap; //第一个自由块大小就是堆结束地址减堆对齐的首地址 pxFirstFreeBlock->xBlockSize = uxAddress - (size_t)pxFirstFreeBlock; //第一个自由块执行结束块 pxFirstFreeBlock->pxNextFreeBlock = pxEnd; //记录最小的空闲内存块大小,即开始堆的总大小 xMinimumEverFreeBytesRemaining = pxFirstFreeBlock->xBlockSize; //剩余内存堆大小 xFreeBytesRemaining = pxFirstFreeBlock->xBlockSize; //已经申请的大小 xBlockAllocatedBit = ((size_t)1) << ((sizeof(size_t) * heapBITS_PER_BYTE) - 1); } /*-----------------------------------------------------------*/ //插入块到空闲列表 static void prvInsertBlockIntoFreeList(BlockLink_t* pxBlockToInsert) { //遍历用的游标 BlockLink_t* pxIterator; uint8_t* puc; //从开始块一直找到第一个地址大于要插入块地址的内存块 for (pxIterator = &xStart; pxIterator->pxNextFreeBlock < pxBlockToInsert; pxIterator = pxIterator->pxNextFreeBlock) { /* 这里没什么可做的,只是迭代到正确的位置。 */ } //找到的块地址转换为u8 puc = (uint8_t*)pxIterator; //如果找到块的首地址加上块大小等于要插入块的首地址 if ((puc + pxIterator->xBlockSize) == (uint8_t*)pxBlockToInsert) { //就把要插入的块合并到找到的块 pxIterator->xBlockSize += pxBlockToInsert->xBlockSize; //要插入的块指向找到的块。即两个块合并了 pxBlockToInsert = pxIterator; } else { mtCOVERAGE_TEST_MARKER(); } //要插入块的首地址 puc = (uint8_t*)pxBlockToInsert; //如果要插入的块能和后面合并就再合并 if ((puc + pxBlockToInsert->xBlockSize) == (uint8_t*)pxIterator->pxNextFreeBlock) { //没到最后 if (pxIterator->pxNextFreeBlock != pxEnd) { //记录合并后的大小 pxBlockToInsert->xBlockSize += pxIterator->pxNextFreeBlock->xBlockSize; //指向更后一个自由块 pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock->pxNextFreeBlock; } //已经是最后了,插入块就指向End else { pxBlockToInsert->pxNextFreeBlock = pxEnd; } } //如果不能合并就让插入块执行当前块后一个块 else { pxBlockToInsert->pxNextFreeBlock = pxIterator->pxNextFreeBlock; } //如果当前块和要插入块指向不同,当前块的下一块指向要插入的块 if (pxIterator != pxBlockToInsert) { pxIterator->pxNextFreeBlock = pxBlockToInsert; } else { mtCOVERAGE_TEST_MARKER(); } } /*-----------------------------------------------------------*/
7.定义port.h
//定义基础类型的别名 //定义宏,防止头文件重复引用 #ifndef __port #define __port #include "FreeRTOSMini.h" //滴答时钟中断。内核通过滴答中断判断是否需要切换上下文 //如果需要切换上下文则设置PendSV寄存器触发PendSV中断进行上下文切换 void xPortSysTickHandler(void); /*PendSV是可悬起异常,如果我们把它配置最低优先级,那么如果同时有多个异常被触发,它会在其他异常执行完毕后再执行, 而且任何异常都可以中断它。更详细的内容在《Cortex-M3 权威指南》里有介绍,下面我摘抄了一段。 OS 可以利用它“缓期执行”一个异常——直到其它重要的任务完成后才执行动 作。悬起 PendSV 的方法是: 手工往NVIC的PendSV悬起寄存器中写 1。悬起后,如果优先级不够 高,则将缓期等待执行。 PendSV的典型使用场合是在上下文切换时(在不同任务之间切换)。例如,一个系统中有两个就绪的任务,上下文切换被触发的场合可以是: 1、执行一个系统调用 2、系统滴答定时器(SYSTICK)中断,(轮转调度中需要) 让我们举个简单的例子来辅助理解。假设有这么一个系统,里面有两个就绪的任务,并且通过SysTick异常启动上下文切换。 但若在产生 SysTick 异常时正在响应一个中断,则 SysTick异常会抢占其 ISR。在这种情况下,OS是不能执行上下文切换的, 否则将使中断请求被延迟,而且在真实系统中延迟时间还往往不可预知——任何有一丁点实时要求的系统都决不能容忍这 种事。 因此,在 CM3 中也是严禁没商量——如果 OS 在某中断活跃时尝试切入线程模式,将触犯用法fault异常。 为解决此问题,早期的 OS 大多会检测当前是否有中断在活跃中,只有在无任何中断需要响应 时,才执行上下文切换(切换期间无法响应中断)。 然而,这种方法的弊端在于,它可以把任务切 换动作拖延很久(因为如果抢占了 IRQ,则本次 SysTick在执行后不得作上下文切换, 只能等待下 一次SysTick异常),尤其是当某中断源的频率和SysTick异常的频率比较接近时,会发生“共振”, 使上下文切换迟迟不能进行。 现在好了,PendSV来完美解决这个问题了。PendSV异常会自动延迟上下文切换的请求,直到 其它的 ISR都完成了处理后才放行。 为实现这个机制,需要把 PendSV编程为最低优先级的异常。如果 OS检测到某 IRQ正在活动并且被 SysTick抢占, 它将悬起一个 PendSV异常,以便缓期执行 上下文切换。 1.关闭中断 2.保存上文 3.加载下文 4.打开中断 */ //进入该中断后讲使用MSP栈指针,PSP不会被盖。这时候Context内核已经自动压栈了xPSR,R15(PC),R14(LR),R12(SP),R3,R2,R1,R0到用户栈空间 __asm void xPortPendSVHandler(void); //SVC中断处理函数 //启动调度器后触发SVC异常调用第一个任务 __asm void vPortSVCHandler(void); // 屏蔽比OS优先级低的中断,并且刷新指令和数据流水线 // 向basepri中写入configMAX_SYSCALL_INTERRUPT_PRIORITY, // 表明优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断都会被屏蔽 void vPortRaiseBASEPRI(void); //清除屏蔽优先级 void vPortClearBASEPRIFromISR(void); //设置定时器中断 void vPortSetupTimerInterrupt(void); //模拟硬件初始化栈 //pxTopOfStack: //pxCode: //pvParameters: StackType_t* pxPortInitialiseStack(StackType_t* pxTopOfStack, TaskFunction_t pxCode, void* pvParameters); //触发PendSV #define portYIELD() \ { \ /* 写PendSV触发寄存器,触发PendSV调度 */ \ portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; \ /*清除数据流水线和指令流水线*/ \ __dsb( portSY_FULL_READ_WRITE ); \ __isb( portSY_FULL_READ_WRITE ); \ } #endif
8.实现port.c
#include "port.h" //滴答时钟中断。内核通过滴答中断判断是否需要切换上下文 //如果需要切换上下文则设置PendSV寄存器触发PendSV中断进行上下文切换 void xPortSysTickHandler(void) { /*SysTick 以最低的中断优先级运行,所以当这个中断 * 执行所有中断必须被取消屏蔽。 因此没有必要 * 保存然后恢复中断屏蔽值,因为它的值已经是 * 已知 - 因此使用稍快的 vPortRaiseBASEPRI() 函数 * 代替 portSET_INTERRUPT_MASK_FROM_ISR()。*/ //屏蔽比OS级别低的中断和刷新指令和数据流水线 vPortRaiseBASEPRI(); { //增加OS的节拍数。返回是否需要切换上下文 if (xTaskIncrementTick() != pdFALSE) { //需要上下文切换。产生PendSV中断 。 portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; } } //取消屏蔽中断 vPortClearBASEPRIFromISR(); } /*PendSV是可悬起异常,如果我们把它配置最低优先级,那么如果同时有多个异常被触发,它会在其他异常执行完毕后再执行, 而且任何异常都可以中断它。更详细的内容在《Cortex-M3 权威指南》里有介绍,下面我摘抄了一段。 OS 可以利用它“缓期执行”一个异常——直到其它重要的任务完成后才执行动 作。悬起 PendSV 的方法是: 手工往NVIC的PendSV悬起寄存器中写 1。悬起后,如果优先级不够 高,则将缓期等待执行。 PendSV的典型使用场合是在上下文切换时(在不同任务之间切换)。例如,一个系统中有两个就绪的任务,上下文切换被触发的场合可以是: 1、执行一个系统调用 2、系统滴答定时器(SYSTICK)中断,(轮转调度中需要) 让我们举个简单的例子来辅助理解。假设有这么一个系统,里面有两个就绪的任务,并且通过SysTick异常启动上下文切换。 但若在产生 SysTick 异常时正在响应一个中断,则 SysTick异常会抢占其 ISR。在这种情况下,OS是不能执行上下文切换的, 否则将使中断请求被延迟,而且在真实系统中延迟时间还往往不可预知——任何有一丁点实时要求的系统都决不能容忍这 种事。 因此,在 CM3 中也是严禁没商量——如果 OS 在某中断活跃时尝试切入线程模式,将触犯用法fault异常。 为解决此问题,早期的 OS 大多会检测当前是否有中断在活跃中,只有在无任何中断需要响应 时,才执行上下文切换(切换期间无法响应中断)。 然而,这种方法的弊端在于,它可以把任务切 换动作拖延很久(因为如果抢占了 IRQ,则本次 SysTick在执行后不得作上下文切换, 只能等待下 一次SysTick异常),尤其是当某中断源的频率和SysTick异常的频率比较接近时,会发生“共振”, 使上下文切换迟迟不能进行。 现在好了,PendSV来完美解决这个问题了。PendSV异常会自动延迟上下文切换的请求,直到 其它的 ISR都完成了处理后才放行。 为实现这个机制,需要把 PendSV编程为最低优先级的异常。如果 OS检测到某 IRQ正在活动并且被 SysTick抢占, 它将悬起一个 PendSV异常,以便缓期执行 上下文切换。 1.关闭中断 2.保存上文 3.加载下文 4.打开中断 */ //进入该中断后讲使用MSP栈指针,PSP不会被盖。这时候Context内核已经自动压栈了xPSR,R15(PC),R14(LR),R12(SP),R3,R2,R1,R0到用户栈空间 __asm void xPortPendSVHandler(void) { extern uxCriticalNesting; //当前运行的任务 extern pxCurrentTCB; //任务切换上下文 extern vTaskSwitchContext; //指定当前文件的堆栈按照 8 字节对齐 PRESERVE8 //R0=PSP Process Stack Pointer (PSP) //取出用户栈指针到r0.r0存当前用户栈地址 mrs r0, psp //指令同步屏障,清除指令流水线 isb //LDR指令用于从存储器中将一个32位的字数据传送到目的寄存器中。该指令通常用亍从存储器中读取32位的字数据到通用寄存器,然后对数据迕行处理 //获取pxCurrentTCB变量的地址到r3(这时候pxCurrentTCB变量存当前运行任务的tcb首地址) ldr r3, =pxCurrentTCB /* Get the location of the current TCB. */ //把pxCurrentTCB变量指向的tcb地址读入r2 ldr r2, [r3] //stmdb将寄存器压栈,保存剩余的寄存器。叹号标识自动调整地址 //r0里面地址从高到低自动调节,依次把r4到r11寄存器的内容储存r0的任务栈里面 //把r4到r11寄存器值压入当前用户的栈(PSP指向的栈) stmdb r0 !, { r4 - r11 } /* Save the remaining registers. */ //STR{条件} 源寄存器,<存储器地址> //STR指令用亍从源寄存器中将一个32位的字数据传送到存储器中。该指令在程序设计中比较常用 //将新的栈顶保存到TCB的第一个成员中。 //把r0存的用户栈地址存入r2指向的地址里,即pxCurrentTCB的开始位置,也就是第一个成员里 str r0, [r2] /* Save the new top of stack into the first member of the TCB. */ //stmdb将寄存器压栈,入栈保存R3(即&pxCurrentTCB)和 R14(链接寄存器)。叹号标识自动调整地址 //sp里面地址从高到低自动调节,依次把r3和r14寄存器的内容储存sp的任务栈里面 //把r3(现在存的pxCurrentTCB变量地址)和r14(链接寄存器)值压入主栈 stmdb sp !, { r3, r14 } //读入最大优先级进入r0 mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY //设置屏蔽中断寄存器basepri为OS所能调用管理的最高优先级 //这里中断优先级0-255,越大优先级越低。即屏蔽所有比设置的OS所能管理的最大优先级低的中断 msr basepri, r0 //数据同步令牌,清除数据流水线 dsb //指令同步屏障,清除指令流水线 isb //进行任务切换,在临界段切换就绪队列中优先级最高的任务,更新pxCurrentTCB //跳转到vTaskSwitchContext执行需要返回 bl vTaskSwitchContext //把0读入r0 mov r0, #0 //设置屏蔽中断寄存器basepri为0,开启中断 msr basepri, r0 //从主堆栈中恢复寄存器R3和R14的值,此时SP使用的是MSP。叹号标识自动调整地址 //sp里面地址由低到高自动调节,依次把sp的任务栈弹出到r3到r14寄存器 //从主栈里恢复r3(r3存的是pxCurrentTCB变量的地址)和r14(链接寄存器,也就是做切换逻辑前的链接) //和stmdb sp !, { r3, r14 }是对应的,切换任务前压入主栈了,切换完成后从主栈弹出 ldmia sp !, { r3, r14 } //r3里面是pxCurrentTCB变量指向的地址。把该地址存的当前tcb地址读入r1(由于切换了任务pxCurrentTCB变量存了新任务的tcb首地址) ldr r1, [r3] //pxCurrentTCB中的第一项是栈顶任务,读出PCB存的栈顶位置到r0(pxCurrentTCB这时候已经存的新任务的tcb,这样就得到新任务的栈顶) //和str r0, [ r2 ]是对应的,str r0, [ r2 ]存PSP到tcb第一个元素的栈顶指针变量里 ldr r0, [r1] /* The first item in pxCurrentTCB is the task top of stack. */ //弹出寄存器和关键嵌套计数 //r0里面地址由低到高自动调节,依次把r0的任务栈弹出到r4到r11寄存器。叹号标识自动调整地址 //从r0指向的栈依次弹出r4-r11(这时候r0存的新任务的栈顶,因此是弹出新要执行的任务的寄存器值) ldmia r0 !, { r4 - r11 } /* Pop the registers and the critical nesting count. */ //PSP=R0,更新PSP使异常退出时PSP为基地址进行其他寄存器的自动出栈 //把新要执行任务栈顶地址写入用户栈顶指针寄存器 msr psp, r0 //指令同步屏障,清除指令流水线 isb //系统以PSP作为SP指针出栈,把新任务的任务堆栈中剩下的内容加载到CPU寄存器: //R0(任务形参)、R1、R2、R3、R12、R14(LR)、R15(PC)和xPSR,切换到新任务 //调转到r14的指令不用返回 bx r14 //空指令,占用一个时钟周期 nop } //SVC中断处理函数 //启动调度器后触发SVC异常调用第一个任务 __asm void vPortSVCHandler(void) { //当前运行的任务 extern pxCurrentTCB; PRESERVE8 //把pxCurrentTCB地址读入r3 ldr r3, = pxCurrentTCB //从pxCurrentTCB读取值到r1 ldr r1, [r3] //读出当前tcb第一个元素,即栈顶位置 ldr r0, [r1] //从当前任务的栈里面弹出r4到r11寄存器的值 ldmia r0 !, { r4 - r11 } //设置当前栈顶位置到psp msr psp, r0 //指令同步令牌,清除指令流水线 isb //读入0到r0 mov r0, # 0 //放开中断屏蔽 msr basepri, r0 /*在进入异常服务程序后,将自动更新LR的值为特殊的EXC_RETURN。 这是一个高28位全为1的值,只有[3:0]的值有特殊含义。 当异常服务例程把这个值送往PC时,就会启动处理器的中断返回序列。 因为LR的值是由CM3自动设置的,所以只要没有特殊需求,就不要改动它。 合法的EXC_RETURN值及其功能 0xFFFF_FFF1 返回handler模式 0xFFFF_FFF9 返回线程模式,并使用主堆栈(SP=MSP) 0xFFFF_FFFD 返回线程模式,并使用线程堆栈(SP=PSP) */ //这个是第一次任务是MSP进来的所以进入异常服务程序后,LR的为 0xFFFF_FFF9 /*当从SVC中断服务退出前,通过向r14寄存器最后4位按位或上0x0D, 使得硬件在退出时使用进程堆栈指针PSP完成出栈操作并返回后进入线程模式、返回Thumb状态 */ orr r14, # 0xd //执行bx R14,告诉处理器ISR完成,需要返回,此刻处理器便会进行出栈操作,PC被我们赋值成为了执行任务的函数的入口,也即正式跑起来 bx r14 } // 屏蔽比OS优先级低的中断,并且刷新指令和数据流水线 // 向basepri中写入configMAX_SYSCALL_INTERRUPT_PRIORITY, // 表明优先级低于configMAX_SYSCALL_INTERRUPT_PRIORITY的中断都会被屏蔽 void vPortRaiseBASEPRI(void) { //系统所能调用的最高优先级 uint32_t ulNewBASEPRI = configMAX_SYSCALL_INTERRUPT_PRIORITY; //汇编代码 __asm { //MRS指令用于将程序状态寄存器的内容传送到通用寄存器中。该指令一般在以下两种情况使用 //1.当需要改变程序状态寄存器的内容时,可用MRS将程序状态寄存器的内容读入通用寄存器修改后再写回程序状态寄存器 //2.当在异常处理戒迕程切换时,需要保存程序状态寄存器的值,可先?该指令读出程序状态寄存器的值,然后保存 //把OS管理最高优先级写入屏蔽中断寄存器basepri,把优先级低于OS的中断屏蔽 msr basepri, ulNewBASEPRI /*DSB 和 ISB 指令对于自修改代码很重要。 例如,如果一个 程序改变了自己的程序代码,下一条执行的指令应该基于 更新程序。 然而,由于处理器是流水线的,修改后的指令位置 可能已经被提取了。 使用 DSB 再使用 ISB 可以确保修改后的 再次获取程序代码。*/ //数据同步屏障 dsb //指令同步屏障 isb } } //取消中断屏蔽 void vPortClearBASEPRIFromISR(void) { //汇编代码 __asm { //MRS指令用于将程序状态寄存器的内容传送到通用寄存器中。该指令一般在以下两种情况使用 //1.当需要改变程序状态寄存器的内容时,可用MRS将程序状态寄存器的内容读入通用寄存器修改后再写回程序状态寄存器 //2.当在异常处理戒迕程切换时,需要保存程序状态寄存器的值,可先?该指令读出程序状态寄存器的值,然后保存 //把OS管理最高优先级写入屏蔽中断寄存器basepri,把优先级低于OS的中断屏蔽。0就是最低优先级,即不屏蔽中断 msr basepri, # 0 } } //设置systick中断 void vPortSetupTimerInterrupt(void) { //停止并清空SysTick portNVIC_SYSTICK_CTRL_REG = 0UL; portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL; //设置SysTick中断到需要取频率 portNVIC_SYSTICK_LOAD_REG = (configCPU_CLOCK_HZ / configTICK_RATE_HZ) - 1UL; portNVIC_SYSTICK_CTRL_REG = (portNVIC_SYSTICK_CLK_BIT | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT); } //模拟硬件初始化栈 //pxTopOfStack: //pxCode: //pvParameters: StackType_t* pxPortInitialiseStack(StackType_t* pxTopOfStack, TaskFunction_t pxCode, void* pvParameters) { //模拟堆栈帧,因为它将由上下文切换创建中断。 //PSR:状态寄存器 //PC:程序计数器 //LR:链接寄存器 //依次压入PSR,PC,LR,R12,R3,R2,R1,R0 pxTopOfStack--; //入栈初始状态 *pxTopOfStack = portINITIAL_XPSR; pxTopOfStack--; //PC赋值成为了执行任务的函数的入口 //为了严格遵守 Cortex-M 规范,任务起始地址应清除第0位,因为它是在退出ISR时加载到PC中的。 *pxTopOfStack = ((StackType_t)pxCode) & portSTART_ADDRESS_MASK; pxTopOfStack--; //入栈链接到错误跳转 *pxTopOfStack = (StackType_t)prvTaskExitError; //空出位置 pxTopOfStack -= 5; //任务参数放入R0对应位置 *pxTopOfStack = (StackType_t)pvParameters; //空出位置 pxTopOfStack -= 8; return pxTopOfStack; }
9.实现FreeRTOSMini.c
#include "FreeRTOSMini.h" #include "heap_4.h" #include "port.h" //当前的任务序号,生成任务唯一号 PRIVILEGED_DATA static volatile UBaseType_t uxCurrentNumberOfTasks = (UBaseType_t)0U; //当前执行任务的tcb PRIVILEGED_DATA TCB_t* volatile pxCurrentTCB = NULL; //就绪任务列表 //定义优先级个数的就绪任务列表数组。每个优先级可以维持一个任务List PRIVILEGED_DATA static List_t pxReadyTasksLists[configMAX_PRIORITIES]; //两个延迟列表轮流着用,系统tick溢出了就调换延迟列表 //用两个延迟列表来解决延迟定时器溢出问题。如果延迟计算后的tick小于当前系统tick,那么就延时到溢出了,这时候延时任务就加入延迟溢出列表 //延迟任务列表1 PRIVILEGED_DATA static List_t xDelayedTaskList1; //延迟任务列表2 PRIVILEGED_DATA static List_t xDelayedTaskList2; //延迟任务列表的指针 PRIVILEGED_DATA static List_t* volatile pxDelayedTaskList; //延迟超时任务列表的指针 PRIVILEGED_DATA static List_t* volatile pxOverflowDelayedTaskList; //调度程序挂起时已准备好的任务,当调度器恢复时,它们将被移动到就绪列表中。 PRIVILEGED_DATA static List_t xPendingReadyList; //系统节拍计数 PRIVILEGED_DATA static volatile TickType_t xTickCount = (TickType_t)0; //下一个出块时间 //在调度程序启动之前初始化为 portMAX_DELAY。 PRIVILEGED_DATA static volatile TickType_t xNextTaskUnblockTime = (TickType_t)0U; //任务是否被挂起 PRIVILEGED_DATA static volatile UBaseType_t uxSchedulerSuspended = (UBaseType_t)pdFALSE; //调度器是否挂起 PRIVILEGED_DATA static volatile BaseType_t xSchedulerRunning = pdFALSE; //是否要执行上下文切换 PRIVILEGED_DATA static volatile BaseType_t xYieldPending = pdFALSE; //延迟的节拍数 PRIVILEGED_DATA static volatile TickType_t xPendedTicks = (TickType_t)0U; //系统任务数 PRIVILEGED_DATA static UBaseType_t uxTaskNumber = (UBaseType_t)0U; //溢出的节拍次数 PRIVILEGED_DATA static volatile BaseType_t xNumOfOverflows = (BaseType_t)0; //空闲任务句柄 PRIVILEGED_DATA static TaskHandle_t xIdleTaskHandle = NULL; //最高的就行任务的优先级 PRIVILEGED_DATA static volatile UBaseType_t uxTopReadyPriority = tskIDLE_PRIORITY; //最高的任务优先级 const volatile UBaseType_t uxTopUsedPriority = configMAX_PRIORITIES - 1U; //设置最高的就绪优先级 #define taskRECORD_READY_PRIORITY(uxPriority) \ { \ if((uxPriority)>uxTopReadyPriority) \ { \ uxTopReadyPriority = (uxPriority); \ } \ } //pxTaskCode:任务函数 void 函数名(void *pvParameters) //pcName:任务名称,小于configMAX_TASK_NAME_LEN,建议小于16 //usStackDepth:堆栈大小(整数) //pvParameters:给任务传递的参数 //uxPriority:任务优先级(整数) //pxCreatedTask:任务的句柄,后期用他操作任务 BaseType_t xTaskCreate(TaskFunction_t pxTaskCode, const char* const pcName, const configSTACK_DEPTH_TYPE usStackDepth, void* const pvParameters, UBaseType_t uxPriority, TaskHandle_t* const pxCreatedTask) { //TCB对象 TCB_t* pxNewTCB; //返回值 BaseType_t xReturn; StackType_t* pxStack; //调用heap申请内存,申请栈的空间 pxStack = pvPortMalloc((((size_t)usStackDepth) * sizeof(StackType_t))); //栈申请成功就申请TCB if (pxStack != NULL) { //调用heap申请内存,申请TCB空间 pxNewTCB = (TCB_t*)pvPortMalloc(sizeof(TCB_t)); if (pxNewTCB != NULL) { //TCB指向栈底 pxNewTCB->pxStack = pxStack; } else { //TCB申请失败就是否栈空间 vPortFree(pxStack); } } else { pxNewTCB = NULL; } //申请TCB和栈成功后的操作 if (pxNewTCB != NULL) { //初始化新创建的任务 prvInitialiseNewTask(pxTaskCode, pcName, (uint32_t)usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB); //把任务加入就绪列表 prvAddNewTaskToReadyList(pxNewTCB); //返回创建成功1 xReturn = pdPASS; } else { //返回申请空间失败-1 xReturn = -1; } return xReturn; } //执行新任务的初始化 static void prvInitialiseNewTask(TaskFunction_t pxTaskCode, const char* const pcName, const uint32_t ulStackDepth, void* const pvParameters, UBaseType_t uxPriority, TaskHandle_t* const pxCreatedTask, TCB_t* pxNewTCB) { //栈顶 StackType_t* pxTopOfStack; UBaseType_t x; //栈向下生长,算的栈结束地址 //得到栈顶地址。空栈时候指向栈的开始位置。ARM栈向下生长,所以栈开始时候栈顶是高内存位置 pxTopOfStack = &(pxNewTCB->pxStack[ulStackDepth - (uint32_t)1]); pxTopOfStack = (StackType_t*)(((portPOINTER_SIZE_TYPE)pxTopOfStack) & (~((portPOINTER_SIZE_TYPE)portBYTE_ALIGNMENT_MASK))); //保存任务名称 if (pcName != NULL) { for (x = (UBaseType_t)0; x < (UBaseType_t)16; x++) { pxNewTCB->pcTaskName[x] = pcName[x]; if (pcName[x] == (char)0x00) { break; } else { mtCOVERAGE_TEST_MARKER(); } } //设置结束位 pxNewTCB->pcTaskName[16 - 1] = '\0'; } else { //没名字直接设置结束 pxNewTCB->pcTaskName[0] = 0x00; } //如果给的优先级大于最大优先级,就给最大的优先级 if (uxPriority >= (UBaseType_t)configMAX_PRIORITIES) { uxPriority = (UBaseType_t)configMAX_PRIORITIES - (UBaseType_t)1U; } else { mtCOVERAGE_TEST_MARKER(); } //保存优先级 pxNewTCB->uxPriority = uxPriority; //设置基础优先级 pxNewTCB->uxBasePriority = uxPriority; pxNewTCB->uxMutexesHeld = 0; //初始化状态列表像 vListInitialiseItem(&(pxNewTCB->xStateListItem)); //初始化事件列表项 vListInitialiseItem(&(pxNewTCB->xEventListItem)); //将pxNewTCB设置为从ListItem_t返回的链接。这样我们就可以得到 //从列表中的通用项返回包含 TCB listSET_LIST_ITEM_OWNER(&(pxNewTCB->xStateListItem), pxNewTCB); //事件列表始终按优先顺序排列。按最大优先级减去优先级保存 listSET_LIST_ITEM_VALUE(&(pxNewTCB->xEventListItem), (TickType_t)configMAX_PRIORITIES - (TickType_t)uxPriority); /*lint !e961 MISRA exception as the casts are only redundant for some ports. */ listSET_LIST_ITEM_OWNER(&(pxNewTCB->xEventListItem), pxNewTCB); //delay终止标志默认为FALSE pxNewTCB->ucDelayAborted = pdFALSE; //初始化 TCB 堆栈,使其看起来好像任务已经在运行, //但已被调度程序中断。 返回地址已设置 //到任务功能的开始。 堆栈初始化后 //栈顶变量被更新。 pxNewTCB->pxTopOfStack = pxPortInitialiseStack(pxTopOfStack, pxTaskCode, pvParameters); //传入了任务句柄,就把TCB设置到任务句柄 if (pxCreatedTask != NULL) { //设置tcb到句柄 *pxCreatedTask = (TaskHandle_t)pxNewTCB; } else { mtCOVERAGE_TEST_MARKER(); } } //挂起所有 void vTaskSuspendAll(void) { /* A critical section is not required as the variable is of type * BaseType_t. Please read Richard Barry's reply in the following link to a * post in the FreeRTOS support forum before reporting this as a bug! - * https://goo.gl/wu4acr */ /* portSOFTWARE_BARRIER() is only implemented for emulated/simulated ports that * do not otherwise exhibit real time behaviour. */ //portSOFTWARE_BARRIER(); /* The scheduler is suspended if uxSchedulerSuspended is non-zero. An increment * is used to allow calls to vTaskSuspendAll() to nest. */ ++uxSchedulerSuspended; /* Enforces ordering for ports and optimised compilers that may otherwise place * the above increment elsewhere. */ //portMEMORY_BARRIER(); } //任务错误会调用到这个方法 //任务退出错误,任务不是死循环会走到这里 void prvTaskExitError(void) { //屏蔽比OS管理优先级低的中断 vPortRaiseBASEPRI(); //运行死循环,否则调度器就运行结束了 for (; ; ) { } } //进入临界区 void vPortEnterCritical(void) { //屏蔽比os设置管理优先级低的中断 vPortRaiseBASEPRI(); //屏蔽数量增加 uxCriticalNesting++; } //把任务加入就绪列表 static void prvAddNewTaskToReadyList(TCB_t* pxNewTCB) { //进入临界区,确保当任务列表在更新的时候中断不会访问任务列表 vPortEnterCritical(); { //当前任务序号加一 uxCurrentNumberOfTasks++; //没有当前任务就设置到当前任务 if (pxCurrentTCB == NULL) { //当前任务没有肯定就直接设置到运行任务 pxCurrentTCB = pxNewTCB; //第一个任务时候初始化任务列表 if (uxCurrentNumberOfTasks == (UBaseType_t)1) { //第一个任务时候初始化调度列表 prvInitialiseTaskLists(); } else { mtCOVERAGE_TEST_MARKER(); } } else { //如果调度程序尚未运行,请将此任务设置为当前任务,如果它是目前创建的最高优先级任务 if (xSchedulerRunning == pdFALSE) { //当前任务优先级小于要加入任务的优先级就把加入的任务设置为当前优先级 if (pxCurrentTCB->uxPriority <= pxNewTCB->uxPriority) { pxCurrentTCB = pxNewTCB; } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } //任务序号加1 uxTaskNumber++; //配置允许了跟踪调试 pxNewTCB->uxTCBNumber = uxTaskNumber; //把任务加到就绪列表 prvAddTaskToReadyList(pxNewTCB); } //退出临界保护 vPortExitCritical(); //如果任务调度在运行 if (xSchedulerRunning != pdFALSE) { //如果创建的任务的优先级高于当前任务,那么它现在应该运行。 if (pxCurrentTCB->uxPriority < pxNewTCB->uxPriority) { //调用移植层设置PendSVN的触发寄存器逻辑,触发上下文切换 portYIELD(); } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } //初始化任务列表 static void prvInitialiseTaskLists(void) { //优先级 UBaseType_t uxPriority; //从优先级0到最高优先级初始化每个优先级的就绪列表 for (uxPriority = (UBaseType_t)0U; uxPriority < (UBaseType_t)configMAX_PRIORITIES; uxPriority++) { vListInitialise(&(pxReadyTasksLists[uxPriority])); } //初始化延迟列表1 vListInitialise(&xDelayedTaskList1); //初始化延迟列表2 vListInitialise(&xDelayedTaskList2); //初始化延迟就绪列表 vListInitialise(&xPendingReadyList); //初始化延迟列表指针 pxDelayedTaskList = &xDelayedTaskList1; pxOverflowDelayedTaskList = &xDelayedTaskList2; } //恢复所有 BaseType_t xTaskResumeAll(void) { TCB_t* pxTCB = NULL; //是否已经让出cpu BaseType_t xAlreadyYielded = pdFALSE; //进入临界区 vPortEnterCritical(); { --uxSchedulerSuspended; if (uxSchedulerSuspended == (UBaseType_t)pdFALSE) { if (uxCurrentNumberOfTasks > (UBaseType_t)0U) { /* Move any readied tasks from the pending list into the * appropriate ready list. */ //把所有挂起队列任务移到就绪队列 while (listLIST_IS_EMPTY(&xPendingReadyList) == pdFALSE) { pxTCB = listGET_OWNER_OF_HEAD_ENTRY((&xPendingReadyList)); //移除事件 listREMOVE_ITEM(&(pxTCB->xEventListItem)); //移除状态 listREMOVE_ITEM(&(pxTCB->xStateListItem)); //加入就绪队列 prvAddTaskToReadyList(pxTCB); //大于当前运行任务优先级标识需要调度 if (pxTCB->uxPriority >= pxCurrentTCB->uxPriority) { xYieldPending = pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } } //计算下一个出块时间 if (pxTCB != NULL) { prvResetNextTaskUnblockTime(); } /* If any ticks occurred while the scheduler was suspended then * they should be processed now. This ensures the tick count does * not slip, and that any delayed tasks are resumed at the correct * time. */ { TickType_t xPendedCounts = xPendedTicks; /* Non-volatile copy. */ //追加系统节拍数 if (xPendedCounts > (TickType_t)0U) { do { if (xTaskIncrementTick() != pdFALSE) { xYieldPending = pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } --xPendedCounts; } while (xPendedCounts > (TickType_t)0U); xPendedTicks = 0; } else { mtCOVERAGE_TEST_MARKER(); } } if (xYieldPending != pdFALSE) { //触发PendSV portYIELD(); } else { mtCOVERAGE_TEST_MARKER(); } } } else { mtCOVERAGE_TEST_MARKER(); } } //退出临界区 vPortExitCritical(); return xAlreadyYielded; } //重置下一个出块时间 static void prvResetNextTaskUnblockTime(void) { //延迟列表为空,设置下一个出块时间为最大 if (listLIST_IS_EMPTY(pxDelayedTaskList) != pdFALSE) { xNextTaskUnblockTime = portMAX_DELAY; } else { //得到延迟列表的头部出块时间 xNextTaskUnblockTime = listGET_ITEM_VALUE_OF_HEAD_ENTRY(pxDelayedTaskList); } } //退出临界区 void vPortExitCritical(void) { //屏蔽数量减少 uxCriticalNesting--; //减少到0了就放开中断 if (uxCriticalNesting == 0) { //放开中断屏蔽 vPortSetBASEPRI(0); } } //设置屏蔽中断 static void vPortSetBASEPRI(uint32_t ulBASEPRI) { //汇编代码 __asm { //设置basepri寄存器值屏蔽中断。0不屏蔽 msr basepri, ulBASEPRI /* *INDENT-ON* */ } } //滴答计数器溢出后切换延迟列表和溢出列表 #define taskSWITCH_DELAYED_LISTS() \ { \ List_t * pxTemp; \ \ pxTemp = pxDelayedTaskList; \ pxDelayedTaskList = pxOverflowDelayedTaskList; \ pxOverflowDelayedTaskList = pxTemp; \ xNumOfOverflows++; \ prvResetNextTaskUnblockTime(); \ } //任务增加节拍数。返回是否需要切换上下文 //如果调度器没被挂起 //xConstTickCount如果为0那么tick溢出了,切换延迟列表和溢出延迟列表 //如果系统节拍数大于等于下一个出块时间,遍历整个延迟列表把延迟超期的任务都推入就绪列表,同时把下一个出块时间设置为下一个延迟超时时间,如果没有就设置为最大值 //以下三种情况进行上下文切换 //移入就绪队列的任务优先级大于当前任务就标记需要进行切换 //如果是抢占模式且按时间切片,那么和当前运行任务相同优先级列表数量大于1也要切换,同级别任务共享时间片 //xYieldPending为TRUE时候也要切换 BaseType_t xTaskIncrementTick(void) { //tcb对象 TCB_t* pxTCB; //项目值 TickType_t xItemValue; //是否需要切换上下文 BaseType_t xSwitchRequired = pdFALSE; /* 每次发生滴答中断时由可移植层调用。 * 增加Tick然后检查新Tick值是否会导致任何要解锁的任务。 */ //如果调度器没有被挂起,判断调度 if (uxSchedulerSuspended == (UBaseType_t)pdFALSE) { //小优化,滴答计数不能在此更改 const TickType_t xConstTickCount = xTickCount + (TickType_t)1; //Tick数加1 xTickCount = xConstTickCount; //整数溢出后为0,切换延迟链表指针 if (xConstTickCount == (TickType_t)0U) { //任务切换到延迟列表 taskSWITCH_DELAYED_LISTS(); } else { mtCOVERAGE_TEST_MARKER(); } //滴答节拍大于下一个任务出块时间就切换 if (xConstTickCount >= xNextTaskUnblockTime) { //循环处理每个延迟列表的块,直到为空或者xConstTickCount<xItemValue //把所有的延迟到期的tcb全部移入就绪队列 for (; ; ) { //延迟列表里没有东西设置下一个出块时间为最大值 if (listLIST_IS_EMPTY(pxDelayedTaskList) != pdFALSE) { xNextTaskUnblockTime = portMAX_DELAY; break; } else { /*延迟列表不为空,获取 * 延迟列表头部的项目。 这是时候 * 延迟列表头部的任务必须 * 从阻塞状态中移除。*/ pxTCB = listGET_OWNER_OF_HEAD_ENTRY(pxDelayedTaskList); //取出延迟列表头部TCB的值 xItemValue = listGET_LIST_ITEM_VALUE(&(pxTCB->xStateListItem)); //滴答小于头部块值就把下一次出块时间设置为头部块值 if (xConstTickCount < xItemValue) { /*现在还不是解锁该项目的时候,但是 * item value 是任务在头部的时间 * 必须从阻止列表中删除 * state - 所以将项目值记录在 * xNextTaskUnblockTime。*/ xNextTaskUnblockTime = xItemValue; break; } else { mtCOVERAGE_TEST_MARKER(); } //从状态列表移除 listREMOVE_ITEM(&(pxTCB->xStateListItem)); /*任务是否也在等待事件? 如果是这样删除 * 它来自事件列表。*/ if (listLIST_ITEM_CONTAINER(&(pxTCB->xEventListItem)) != NULL) { listREMOVE_ITEM(&(pxTCB->xEventListItem)); } else { mtCOVERAGE_TEST_MARKER(); } //添加到就绪队列 prvAddTaskToReadyList(pxTCB); //如果解除状态的任务优先级大于当前运行的任务优先级就标记要切换任务 if (pxTCB->uxPriority >= pxCurrentTCB->uxPriority) { xSwitchRequired = pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } } } } //当前运行任务优先级的就绪列表任务数量大于1,也要切换。大家共享时间片 if (listCURRENT_LIST_LENGTH(&(pxReadyTasksLists[pxCurrentTCB->uxPriority])) > (UBaseType_t)1) { xSwitchRequired = pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } //xYieldPending变量为true也要切换上下文 if (xYieldPending != pdFALSE) { xSwitchRequired = pdTRUE; } else { mtCOVERAGE_TEST_MARKER(); } } //调度器被挂起了 else { //挂起的系统节拍数 ++xPendedTicks; } //返回是否需要调度 return xSwitchRequired; } //找到最高的优先级任务 #define taskSELECT_HIGHEST_PRIORITY_TASK() \ { \ UBaseType_t uxTopPriority = uxTopReadyPriority; \ \ /* Find the highest priority queue that contains ready tasks. */ \ while(listLIST_IS_EMPTY( &( pxReadyTasksLists[ uxTopPriority ]))) \ { \ --uxTopPriority; \ } \ \ /* listGET_OWNER_OF_NEXT_ENTRY indexes through the list, so the tasks of \ * the same priority get an equal share of the processor time. */ \ listGET_OWNER_OF_NEXT_ENTRY(pxCurrentTCB, &(pxReadyTasksLists[uxTopPriority])); \ uxTopReadyPriority = uxTopPriority; \ } //切换任务上下文 void vTaskSwitchContext(void) { //调度器挂起了,不允许切换上下文 if (uxSchedulerSuspended != (UBaseType_t)pdFALSE) { //挂起调度器,不允许上下文切换 xYieldPending = pdTRUE; } else { //不挂起调度器 xYieldPending = pdFALSE; //选择新的最高优先级的任务运行 taskSELECT_HIGHEST_PRIORITY_TASK(); } } //开启调度器 void vTaskStartScheduler(void) { //返回值 BaseType_t xReturn; //把空闲任务加入最低优先级 //动态创建空闲任务 xReturn = xTaskCreate(prvIdleTask,"IDLE",120,(void*)NULL,0,&xIdleTaskHandle); //如果创建空闲任务成功了就启动调度 if (xReturn == pdPASS) { //此处关闭比设置的系统所能管理最高优先级低的中断,以确保在调用xPortStartScheduler()之前或期间不会发生滴答中断 //创建的任务包含一个打开中断的状态字 //所以中断将在第一个任务时自动重新启用运行 vPortRaiseBASEPRI(); //下一个未阻塞的任务时间为0xFFFF_FFFF xNextTaskUnblockTime = portMAX_DELAY; //标识调度在运行 xSchedulerRunning = pdTRUE; //Tick的计数器为 0 xTickCount = (TickType_t)0; //启动调度 if (xPortStartScheduler() != pdFALSE) { //代码逻辑不会走到这里,因为所有任务都是死循环,就算失败也会统一进入失败方法的死循环 } else { //如果有任务调xTaskEndScheduler会走到这里 } } else { } } //空闲任务 static void prvIdleTask(void* pvParameters) { /* Stop warnings. */ (void)pvParameters; //死循环 for (; ; ) { //就绪列表大于1就标识要切换上下文 if (listCURRENT_LIST_LENGTH(&(pxReadyTasksLists[tskIDLE_PRIORITY])) > (UBaseType_t)1) { portYIELD(); } else { mtCOVERAGE_TEST_MARKER(); } } } //启动调度器 BaseType_t xPortStartScheduler(void) { //使PendSV和SysTick成为最低优先级的中断。 portNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI; portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI; //启动滴答计时器,设置频率。中断在这里被禁用 vPortSetupTimerInterrupt(); //初始化为第一个任务准备的关键嵌套计数。 uxCriticalNesting = 0; //开启第一个任务 prvStartFirstTask(); //正常执行不到这里 return 0; } //开启第一个任务 /* 取MSP的初始值的思路是先根据向量表的位置寄存器VTOR(0xE000ED08)来获取向量表存储的地址; 再根据向量表存储的地址,取出第一个元素__initial_sp,写入 MSP Cortex-M3 处理器,上电默认进入线程的特权模式,使用 MSP 作为堆栈指针; 从上电跑到这里,经过一系列的函数调用,出栈,入栈,MSP已经不是最开始的初始化的位置; 这里通过 MSR 重新初始化 MSP,丢弃主堆栈中的数据;这是一条不归路,代码跑到这里,不会再返回之前的调用路径。 */ __asm void prvStartFirstTask(void) { PRESERVE8 /*使用NVIC偏移寄存器来定位堆栈。*/ //地址处为VTOR(向量表偏移量)寄存器,存储向量表起始地址 //向量表寄存器地址加载到r0 ldr r0, =0xE000ED08 //启动文件中, 最初地址放置的__initial_sp //从向量表寄存器地址把向量表地址读入r0 ldr r0, [r0] //根据向量表实际存储地址,取出向量表中的第一项,向量表第一项存储主堆栈指针MSP的初始值 ldr r0, [r0] /*将__initial_sp的初始值写入MSP中*/ msr msp, r0 //cpsie i=开启中断cpsi enable interrupt //cpsid i关闭中断cpsi disable interrupt cpsie i //cpsie f=开启异常cpsi enable faultmask //cpsid f关闭异常cpsi disable faultmask cpsie f //数据同步令牌,清除数据流水线 dsb //指令同步令牌,清除指令流水线 isb /*调用SVC中断启动第一个任务。*/ svc 0 //等待一个时钟 nop nop } //任务延时 //xTicksToDelay:要延迟的节拍数 void vTaskDelay(const TickType_t xTicksToDelay) { BaseType_t xAlreadyYielded = pdFALSE; //延迟大于0才执行 if (xTicksToDelay > (TickType_t)0U) { //挂起所有 vTaskSuspendAll(); { //把当前任务加入延迟列表 prvAddCurrentTaskToDelayedList(xTicksToDelay, pdFALSE); } //恢复所有 xAlreadyYielded = xTaskResumeAll(); } else { mtCOVERAGE_TEST_MARKER(); } /* Force a reschedule if xTaskResumeAll has not already done so, we may * have put ourselves to sleep. */ if (xAlreadyYielded == pdFALSE) { portYIELD(); } else { mtCOVERAGE_TEST_MARKER(); } } //把当前任务加入延迟列表 //xTicksToWait:延迟的节拍数 static void prvAddCurrentTaskToDelayedList(TickType_t xTicksToWait, const BaseType_t xCanBlockIndefinitely) { //计算唤醒的系统节拍 TickType_t xTimeToWake; //得到当前系统节拍数 const TickType_t xConstTickCount = xTickCount; //延迟状态记为没中止 pxCurrentTCB->ucDelayAborted = pdFALSE; //从就绪列表移除 if (uxListRemove(&(pxCurrentTCB->xStateListItem)) == (UBaseType_t)0) { //重新设置就绪状态的最高优先级 portRESET_READY_PRIORITY(pxCurrentTCB->uxPriority, uxTopReadyPriority); /*lint !e931 pxCurrentTCB cannot change as it is the calling task. pxCurrentTCB->uxPriority and uxTopReadyPriority cannot change as called with scheduler suspended or in a critical section. */ } else { mtCOVERAGE_TEST_MARKER(); } //醒来的系统节拍 xTimeToWake = xConstTickCount + xTicksToWait; //设置醒来的系统节拍到状态值 listSET_LIST_ITEM_VALUE(&(pxCurrentTCB->xStateListItem), xTimeToWake); //如果计算的醒来值小于当前系统节拍就说明节拍溢出从0开始了,就把任务插入延迟列表 if (xTimeToWake < xConstTickCount) { vListInsert(pxOverflowDelayedTaskList, &(pxCurrentTCB->xStateListItem)); } //否则加入普通列表 else { vListInsert(pxDelayedTaskList, &(pxCurrentTCB->xStateListItem)); //如果解除延迟时间小于系统下一个解块时间就是更新系统下一个解块时间 if (xTimeToWake < xNextTaskUnblockTime) { xNextTaskUnblockTime = xTimeToWake; } else { mtCOVERAGE_TEST_MARKER(); } } }
10.定义InitOS.h
//定义宏,防止头文件重复引用
#ifndef __INITOS
#define __INITOS
//OS调度引入
#include "FreeRTOSMini.h"
//初始化硬件
void InitHardware(void);
#endif
11.实现InitOS.c
#include "InitOS.h" //初始化硬件 void InitHardware(void) { //从处于预期状态的时钟开始 RCC_DeInit(); //启动HSE高速时钟 RCC_HSEConfig(RCC_HSE_ON); //等待时钟就绪 while (RCC_GetFlagStatus(RCC_FLAG_HSERDY) == RESET) { } //闪存需要 2 个等待状态 *((unsigned long*)0x40022000) = 0x02; //HCLK=SYSCLK RCC_HCLKConfig(RCC_SYSCLK_Div1); //PCLK2=HCLK RCC_PCLK2Config(RCC_HCLK_Div1); //PCLK1=HCLK/2 RCC_PCLK1Config(RCC_HCLK_Div2); //PLLCLK=8MHz*9=72MHz RCC_PLLConfig(RCC_PLLSource_HSE_Div1, RCC_PLLMul_9); //使能PLL RCC_PLLCmd(ENABLE); //等待PLL时钟准备就绪 while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) == RESET) { } //设置PLL时钟为系统时钟源 RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); //等待时钟使用系统时钟源成功 while (RCC_GetSYSCLKSource() != 0x08) { } //使能GPIOA,GPIOB,GPIOC,GPIOD,GPIOE,AFIO时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD | RCC_APB2Periph_GPIOE | RCC_APB2Periph_AFIO, ENABLE); //使能SPI2外设时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); //设置向量表的基础地址 NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x0); //设置权限组 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); //设置SysTick的时钟源 SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK); //配置串口中断优先级 NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQChannel; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = configLIBRARY_KERNEL_INTERRUPT_PRIORITY; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); }
12.定义led.h
//定义宏,防止头文件重复引用 #ifndef __LED #define __LED //初始化LED的环境 void InitLedEnv(void); //测试LED void TestLed(void); //LED0任务 void LED0Task(void *pvParameters); //LED1任务 void LED1Task(void *pvParameters); //LED2任务 void LED2Task(void *pvParameters); //LED3任务 void LED3Task(void *pvParameters); #endif
13.定义led.c
#include "InitOS.h" #include "led.h" //初始化LED环境 void InitLedEnv(void) { //设置ABP2外设时钟第4位为1,即可用,对应GPIOC时钟 //1<<4:1左移4位位1000,与APB2ENR寄存器值或运算就是第4位设置1,其他不变的意思 RCC->APB2ENR|=1<<4; //设置LED0为推挽输出模式 //CRL寄存器和十六进制的值且运算,设置指定位置的电平为指定4位位0000 GPIOC->CRL&=0XFFFFFFF0; //CRL寄存器和十六进制的值或运算,设置指定位置的电平为指定4位位0011,四位里面高两位00表示通用推挽输出。低两位11表示输出模式,最大速度50MHz GPIOC->CRL|=0X00000003; //依次设置其他7个LED输出空为推挽输出模式 //LED1 GPIOC->CRL&=0XFFFFFF0F; GPIOC->CRL|=0X00000030; //LED2 GPIOC->CRL&=0XFFFFF0FF; GPIOC->CRL|=0X00000300; //LED3 GPIOC->CRL&=0XFFFF0FFF; GPIOC->CRL|=0X00003000; //LED4 GPIOC->CRL&=0XFFF0FFFF; GPIOC->CRL|=0X00030000; //LED5 GPIOC->CRL&=0XFF0FFFFF; GPIOC->CRL|=0X00300000; //LED6 GPIOC->CRL&=0XF0FFFFFF; GPIOC->CRL|=0X03000000; //LED7 GPIOC->CRL&=0X0FFFFFFF; GPIOC->CRL|=0X30000000; //通过操作ODR寄存器设置每个LED位置为1即高电平,不亮 GPIOC->ODR|=1<<0; GPIOC->ODR|=1<<1; GPIOC->ODR|=1<<2; GPIOC->ODR|=1<<3; GPIOC->ODR|=1<<4; GPIOC->ODR|=1<<5; GPIOC->ODR|=1<<6; GPIOC->ODR|=1<<7; } //测试LED void TestLed(void) { //初始化LED灯 InitLedEnv(); //当前该点亮的LED索引 int curIndex=0; //死循环 while(1) { //改版为点亮所以led做跑马灯 for(int i=0;i<8;i++) { //当前该亮的灯点亮 if(i==curIndex) { //1左移当前索引位数后取非再做且运算就是把当前位数的电平设置低位。比如i=2即LED2,00000100取非之后为11111011再和ODR的值与运算 //第3位电平设置低,其他不变 GPIOC->ODR&=~(1<<i); } //其他熄灭 else { //1左移当前索引位数再做或运算就是把当前位数的电平设置高位。比如i=2即LED2,ODR寄存器值和00000100或运算 //第3位电平设置高,其他不变 GPIOC->ODR|=1<<i; } } //下次点亮下一个灯 curIndex++; //8个灯取余数 curIndex=curIndex%8; //延迟半秒 vTaskDelay(50); } } //LED0任务 //系统节拍中断的频率设置了100,一个节拍10毫秒 //100毫秒一闪 void LED0Task( void *pvParameters ) { //当前该点亮的LED索引 //这里不用静态变量不好使 int statLED0=0; //死循环 while(1) { //当前该亮的灯点亮 if(statLED0==0) { statLED0=1; //1左移当前索引位数后取非再做且运算就是把当前位数的电平设置低位。比如i=2即LED2,00000100取非之后为11111011再和ODR的值与运算 //第0位电平设置低,其他不变 GPIOC->ODR&=~(1<<0); } //其他熄灭 else { statLED0=0; //1左移当前索引位数再做或运算就是把当前位数的电平设置高位。比如i=2即LED2,ODR寄存器值和00000100或运算 //第0位电平设置高,其他不变 GPIOC->ODR|=1<<0; } //延迟 vTaskDelay(10); } } //LED1任务 //系统节拍中断的频率设置了100,一个节拍10毫秒 //200毫秒一闪 void LED1Task( void *pvParameters ) { //当前该点亮的LED索引 //这里不用静态变量不好使 int statLED1=0; //死循环 while(1) { //当前该亮的灯点亮 if(statLED1==0) { statLED1=1; //1左移当前索引位数后取非再做且运算就是把当前位数的电平设置低位。比如i=2即LED2,00000100取非之后为11111011再和ODR的值与运算 //第1位电平设置低,其他不变 GPIOC->ODR&=~(1<<1); } //其他熄灭 else { statLED1=0; //1左移当前索引位数再做或运算就是把当前位数的电平设置高位。比如i=2即LED2,ODR寄存器值和00000100或运算 //第1位电平设置高,其他不变 GPIOC->ODR|=1<<1; } //延迟 vTaskDelay(20); } } //LED2任务 //系统节拍中断的频率设置了100,一个节拍10毫秒 //400毫秒一闪 void LED2Task( void *pvParameters ) { //当前该点亮的LED索引 //这里不用静态变量不好使 int statLED2=0; //死循环 while(1) { //当前该亮的灯点亮 if(statLED2==0) { statLED2=1; //1左移当前索引位数后取非再做且运算就是把当前位数的电平设置低位。比如i=2即LED2,00000100取非之后为11111011再和ODR的值与运算 //第2位电平设置低,其他不变 GPIOC->ODR&=~(1<<2); } //其他熄灭 else { statLED2=0; //1左移当前索引位数再做或运算就是把当前位数的电平设置高位。比如i=2即LED2,ODR寄存器值和00000100或运算 //第2位电平设置高,其他不变 GPIOC->ODR|=1<<2; } //延迟半秒 vTaskDelay(40); } } //LED3任务 //系统节拍中断的频率设置了100,一个节拍10毫秒 //1秒一闪 void LED3Task( void *pvParameters ) { //当前该点亮的LED索引 //这里不用静态变量不好使 int statLED3=0; //死循环 while(1) { //当前该亮的灯点亮 if(statLED3==0) { statLED3=1; //1左移当前索引位数后取非再做且运算就是把当前位数的电平设置低位。比如i=2即LED2,00000100取非之后为11111011再和ODR的值与运算 //第3位电平设置低,其他不变 GPIOC->ODR&=~(1<<3); } //其他熄灭 else { statLED3=0; //1左移当前索引位数再做或运算就是把当前位数的电平设置高位。比如i=2即LED2,ODR寄存器值和00000100或运算 //第3位电平设置高,其他不变 GPIOC->ODR|=1<<3; } //延迟半秒 vTaskDelay(100); } }
14.定义beep.h
//定义宏,防止头文件重复引用
#ifndef __BEEP
#define __BEEP
//初始化蜂鸣器的环境
void InitBeepEnv(void);
//测试蜂鸣器
void TestBeep(void);
//蜂鸣器任务
void BeepTask(void *pvParameters);
#endif
15.实现beep.c
#include "InitOS.h" #include "beep.h" #include "led.h" //初始化蜂鸣器的环境 void InitBeepEnv(void) { //设置ABP2外设时钟第2位为1,即可用,对应GPIOB时钟 //1<<3:1左移3位位0100,与APB2ENR寄存器值或运算就是第3位设置1,其他不变的意思 RCC->APB2ENR|=1<<3; //设置PB8为推挽输出模式 //CRH寄存器和十六进制的值且运算,设置指定位置的电平为指定4位位0000 GPIOB->CRH&=0XFFFFFFF0; //CRH寄存器和十六进制的值或运算,设置指定位置的电平为指定4位位0011,四位里面高两位00表示通用推挽输出。低两位11表示输出模式,最大速度50MHz GPIOB->CRH|=0X00000003; //通过操作ODR寄存器设置9位置为1即高电平,不响 GPIOB->ODR|=1<<8; } //测试蜂鸣器 void TestBeep(void) { //初始化LED灯 InitLedEnv(); //初始化蜂鸣器 InitBeepEnv(); //死循环 while(1) { //通过操作ODR寄存器设置9位置为0即低电平,响 GPIOB->ODR&=~(1<<8); //点亮LED0 GPIOC->ODR&=~(1<<0); //延迟半秒 vTaskDelay(50); //通过操作ODR寄存器设置9位置为1即高电平,不响 GPIOB->ODR|=1<<8; //关闭LED0 GPIOC->ODR|=1<<0; //延迟半秒 vTaskDelay(50); } } //蜂鸣器任务 void BeepTask(void *pvParameters) { //死循环 while(1) { //通过操作ODR寄存器设置9位置为0即低电平,响 GPIOB->ODR&=~(1<<8); //延迟 vTaskDelay(50); //通过操作ODR寄存器设置9位置为1即高电平,不响 GPIOB->ODR|=1<<8; //延迟 vTaskDelay(500); } }
16.实现main.c
#include "InitOS.h" #include "led.h" #include "beep.h" //定时器2中断 void vTimer2IntHandler( void ) { TIM2->SR &= (u16)~((u16)0x0001); } //任务优先级定义和栈大小定义 //主检查任务优先级为空闲任务优先级加3 //LED任务优先级和栈大小 #define LedTaskPriority (tskIDLE_PRIORITY+3) //任务的栈大小 #define LedTaskStakSize (170) //蜂鸣器任务优先级和栈大小 #define BeepTaskPriority (tskIDLE_PRIORITY+4) //任务的栈大小 #define BeepTaskStakSize (170) //主函数 int main( void ) { //初始化硬件信息 InitHardware(); //初始化LED灯硬件环境 InitLedEnv(); //初始化蜂鸣器环境 InitBeepEnv(); //要注意堆栈大小 //LED0任务 xTaskCreate(LED0Task,"LED0",LedTaskStakSize,NULL,LedTaskPriority,NULL); //LED1任务 xTaskCreate(LED1Task,"LED1",LedTaskStakSize,NULL,LedTaskPriority,NULL); //蜂鸣器任务 xTaskCreate(BeepTask,"BEEP",BeepTaskStakSize,NULL,BeepTaskPriority,NULL); //开始任务调度 vTaskStartScheduler(); return 0; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。