当前位置: 首页 > 科技观察

从零开始构建嵌入式实时操作系统——重构

时间:2023-03-12 17:02:24 科技观察

1.前言我是一个普通的中年程序员,不是圈内的大牛。这个嵌入式操作系统系列文章并不是为了展示自己的技术,而是出于对嵌入式的热爱。我好幸运。毕业后从事嵌入式行业十余年。遇到过各种各样的陷阱,也收获过各种各样的快乐。希望嵌入式操作系统系列文章能够对其他嵌入式爱好者有所帮助,帮助热爱嵌入式行业的朋友快速了解嵌入式操作系统的运行原理。我会一步步完善我们的嵌入式实时操作系统enuo。每一步软件搭建完成后,我都会输出一份总结文档,分享软件搭建过程和开源软件工程及源码。操作系统enuo的名字来源于我5岁儿子的Enuo。希望在我的保护下,恩诺和恩诺都能健康、快乐、茁壮成长!2.设计背景书接上文,我们完成了一个任务切换软件工程V0.01版。V0.01版本的软件工程包含三个文件:main.c、startup_stm32f401xc.s和readme。startup_stm32f401xc.s文件是STM32F401的启动文件,main.c文件实现了任务初始化、任务切换和任务轮询调度等功能,readme文件用于记录版本修改日志。V0.01版本只能算是一个功能验证软件。其次,需要采用形式化的软件设计方法对整个项目进行改造重构,使软件系统具有较高的可扩展性、可移植性、可重用性和可读性。.3、设计目标首先,利用软件设计五原则中的单一性原则,建立一个独立的文件夹enuo,用于存放与操作系统相关的源文件,每个源文件完成单一的功能。采用“分而治之”的设计思想,将操作系统划分为多个功能单一的模块,每个模块以C文件的形式承载,提高了软件的可读性和可移植性。其次,同时采用面向对象的设计思想,将任务设计为一个抽象对象,任务对象封装了任务的信息,提高了软件的可扩展性和复用性。C语言虽然不是面向对象的程序设计语言,但是可以通过一些设计技巧来实现面向对象的设计。最后,构建任务列表,用于添加和删除任务。链表数据结构可以在保持原有物理顺序的情况下高效地插入和删除。4.设计环境硬件环境是以STM32F401RE为核心的自制开发板。软件环境使用的是KEILV5.2开发工具。5.设计过程(1)构造任务对象任务对象包含一个栈指针和一个任务链表,定义如下:任务栈指针是第一个元素,所以栈指针和任务对象具有相同的address(结构体的第一个元素是结构体的首地址),可以大大简化任务切换过程中对栈指针的操作。任务堆栈指针指向用户为任务定义的静态数据块。通常,任务栈是一个全局静态数组,数组的大小就是任务栈的大小。任务链表的作用是将多个任务串联起来,方便顺序检索和操作。(2)构建链表结构我们使用链表结构来存储任务对象。使用链表结构有以下优点:1、在保留原有物理顺序的情况下,插入和删除速度快,效率高。插入和删除只需要改变几个指针变量。2.链表的条目数没有上限。存储条目的上限只与内存空间大小有关。理论上,如果内存是无限的,链表中的条目可以动态增加到无限个。3、动态分配内存,根据需要分配多少表项,无需预先分配内存,不存在内存浪费。链表结构定义如下:链表采用数据中包含链表的数据结构。使用这种方式的好处是,当用户数据结构发生变化时,整个链表结构可以保持不变,不同的用户数据都可以使用这个链表。结构。示意图如下:数据包含链表的数据结构,操作链表后无法获取到整个数据对象的地址。从下图可以看出,我们可以通过next指针获取到下一个列表元素的地址,但是无法获取到整个数据对象的地址。在链表结构中添加一个void*owner指针。void*类型指针可以指向任何类型的对象。用这个指针指向整个数据对象的地址,这样就可以定位到整个数据对象的地址。structlist_node_def{structlist_node_def*next;/*指向下一个列表节点*/structlist_node_def*previous;/*指向上一个列表节点*/void*owner;/*指向链表节点数据结构*/};哨兵机制(tableHeadMechanism)哨兵是一个虚拟对象,可以简化边界条件的处理。使用sentinel机制(表头机制)后,链表在空状态和非空状态下插入和删除对象的操作是一样的,可以使代码简洁明了。typedefstructlist_def{list_node_t*index;/*索引指针*/list_node_thead;/*列表头*/}list_t;(3)任务初始化任务初始化代码如下:在创建任务之前,需要定义任务栈空间和用户任务函数:/************************************************************************************************************************@姓名:恩诺*@作者:李魏*****************************************************************************************************************/#defineSTACK_NUM(64)/*任务0-任务2堆栈空间*/uint32_ttask0_stack[STACK_NUM];uint32_ttask1_stack[STACK_NUM];uint32_ttask2_stack[STACK_NUM];/*任务0-任务2对象*/task_tcb_tmy_task0;task_tcb_tmy_task1;task_tcb_tmy_task2;voidtask0(void){staticuint16_tclk=0;while(1){if((clk++)%9999)==0){task_debug_num0++;/*测试轨迹*/test_function();}}}task_create函数代码如下:/*查找列表尾端*/while(node_tail->next!=NULL)node_tail=node_tail->next;/*添加下一个任务到任务列表*/node_tail->next=&task->task_list;/*列表所有者指针指向任务*/task->task_list.owner=task;/*当前任务的下一个列表指针设置为空*/task->task_list.next=NULL;/*初始化任务栈*/task_stack_init((uint32_t*)task,function,stack_space,stack_number);}task_create函数完成任务链表操作,初始化任务栈指针和任务栈空间task_stack_init代码如下:voidtask_stack_init(uint32_t*stack_pointer,task_function_ttask,uint32_t*stack_space,uint32_tstack_number){/*将任务psp栈指针指向任务栈底*/*stack_pointer=((uint32_t)stack_space+((stack_number)<<2));/*初始化任务堆栈中的程序寄存器*/*((uint32_t*)((uint32_t)(*stack_pointer)+(14<<2)))=(uint32_t)task;/*初始化任务XPSR*/*((uint32_t*)((uint32_t)(*stack_pointer)+(15<<2)))=0x01000000;}在栈中(4)开始调度任务完成任务创建后,你可以启动调度任务,启动调度任务的代码如下:enuo_schedule函数代码如下:/**********************************************************************************************************************@姓名:enuo*@作者:李薇****************************************************************************************************************/__asmvoidstart_schedule(void){/*设置CONTROL寄存器配置PSP栈指针模式*/MOVR0,#0X02MSRCONTROL,R0/*读取current_task地址*/LDRR3,=__cpp(¤t_task)/*读取curr_task中的PSP指针值*/LDRR1,[R3]LDRR0,[R1]/*弹出R4-R11八个寄存器*/LDMIAR0!,{R4-R11}/*设置PSP指针*/MSRPSP,R0/*POPregisterPOPPC实现跳转*/POP{R0-R3,R12,R11,PC}/*alignment*/ALIGN4}enuo_schedule函数主要完成3个功能:设置处理器栈指针模式读取current_task地址。恢复任务寄存器,POPPC实现跳转到用户任务代码。(5)调度任务方法任务调度采用时间片循环调度,使用SysTick定时器产生定时中断,在定时器中断函数中依次从task_list任务列表中读取任务对象,并使用next_task指向新读取的任务对象,最后挂起PendSV系统中断标志,在SysTick定时器中断函数退出时执行PendSV_Handler切换任务。SysTick定时器中断函数代码如下:/*********************************************************************************************************************@姓名:enuo*@作者:李薇**********************************************************************************************************************/voidSysTick_Handler(void){staticlist_node_t*node_tail=&task_list.head;/*轮流切换任务*/if(node_tail->next!=NULL){/*当下一个列表项不为NULL时,next_task为下一个列表项指向的任务*/next_task=node_tail->next->所有者;/*更新任务指针*/node_tail=node_tail->next;}else{/*当下一个列表项为NULL时,node_tail指向任务列表的头部*/node_tail=&task_list.head;/*next_task是head指向的下一个任务*/next_task=node_tail->next->owner;node_tail=node_tail->下一个;}/*PendSV系统中断设置*/SCB->ICSR|=SCB_ICSR_PENDSVSET_Msk;return;}(6)Taskswitching任务切换异常在PendSV_Handler(中断)任务切换。PendSV_Handler函数代码如下:/********************************************************************************************************************@姓名:enuo*@作者:李薇********************************************************************************************************************/__asmvoidPendSV_Handler(void){/*读取当前进程栈指针值*/MRSR0,PSP//isb/*将8个寄存器R4-R11的值保存到当前任务栈中,并将回写地址写入R0*/STMDBR0!,{R4-R11}/*读取current_task栈指针地址*/LDRR3,=__cpp(¤t_task)LDRR3,[R3]/*将当前进程PSP指针值写入对应的current_task*/STRR0,[R3]/*获取next_task栈指针地址*/LDRR4,=__cpp(&next_task)LDRR4,[R4]/*读取next_task中的stack_point指针*/LDRR0,[R4]/*更新current_task*/LDRR3,=__cpp(¤t_task)STRR4,[R3]/*OutStackR4-R11八个寄存器*/LDMIAR0!,{R4-R11}/*设置PSPpointer*/MSRPSP,R0/*中断返回*/BXLR/*对齐*/ALIGN4}PendSV_Handler函数完成以下3个功能:处理器自动保存R0,R1,R2,R3,R12,LR,PC,和XPSR在进入PendSV_Handler中断前完成,进入中断后完成R4~R11入栈保存,实现任务保存。storedwork读取current_task地址,将当前进程的PSP指针值保存到current_task指向的task对象中。读取next_task指向的task对象,加载task对象的PSP指针值。弹出8个寄存器R4-R11,设置PSP指针。当中断返回时,处理器自动保存R0、R1、R2、R3、R12、LR、PC、XPSR,实现任务恢复。六、运行结果代码模拟运行后的结果如下:运行结果反映创建的三个任务已经被调度,表明enuo系统运行正常。enuo系统目前包括3个文件:(1)task.h文件该文件包含任务对象的定义、链表数据结构的定义和任务列表数据结构的定义。(2)task.c文件这个文件包含两个函数task_create和enuo_schedule。task.c文件只是用来存放与任务操作相关的函数(符合单一原则)。(3)interface.c文件该文件包含三个函数SysTick_Handler、PendSV_Handler、start_schedule和task_stack_init。interface.c文件仅用于存储与处理器相关的操作。以后更换处理器时,只需要修改这个文件,增强了系统的可移植性。