今年大四,在准备自己的毕业设计。因为毕设题目是一个比较复杂的多传感器监控的嵌入式系统,然后最近自己有使用一些rtos,比方说freertos和ucos,感觉比起单纯对单片机的裸机开发还是有很多好玩的地方。特别喜欢这种抢占式和时间片轮询这两种内核调度模式,所以最近在开始想自己尝试去写一个实时的操作系统的内核调度,看看用自己浅薄的技术,自己去实现会怎么弄,纯粹为了好玩哈哈哈。花了大概几天左右的时间,现在已完成了一个时间片轮询和优先级抢占的实时任务调度内核了,可能有些地方还有些bug,后面有空再慢慢修改,希望通过这个博客记录一下,为以后的开发养成记录和保存的习惯,后面有时间慢慢添加内容。 先说一下硬件平台,我使用的STMF1系列的单片机,F1系列采用的内核是ARM的Crotex M3内核,最高主频 72MHz。使用的开发软件是MDK4.0,参考的操作系统是freertos和ucos,重要参考书籍:《嵌入式操作系统内核调度:底层开发者手册》,《CM3权威指南CnR2》。 一、关于Crotex M3内核的一些小知识 1.ARM的Crotex M3内核使用的事Thumb-2指令集。Thumb-2是16位Thumb 指令集的一个超集,在Thumb-2中,16位指令首次与32位指令并存,无需烦心地把处理器状态在Thumb和ARM之间来回的切换。 2.Crotex-M3 处理器拥有 R0-R15 的寄存器组。其中 R13 作为堆栈指针 SP。SP 有两个,但在同 一时刻只能有一个可以看到,这也就是所谓的“banked”寄存器。R0-R12是通用寄存器。R0-R12 都是 32 位通用寄存器,用于数据操作(注意:绝大多数 16 位 Thumb 指令只能访 问 R0-R7,而 32 Thumb-2 指令可以访问所有寄存器)。 3.R13寄存器(SP):Cortex-M3 拥有两个堆栈指针,然而它们是 banked,因此任一时刻只能使用其中的一个。主堆栈指针(MSP):复位后缺省使用的堆栈指针,用于操作系统内核以及异常处理例程(包 括中断服务例程);进程堆栈指针(PSP):由用户的应用程序代码使用。堆栈指针的最低两位永远是 0,这意味着堆栈总是 4 字节对齐的。 4.R14寄存器(LR):当呼叫一个子程序时,由 R14 存储返回地址。 5.R15寄存器(PC):指向当前的程序地址。如果修改它的值,就能改变程序的执行流。 6.Cortex-M3 还在内核水平上搭载了若干特殊功能寄存器,包括:程序状态字寄存器组(PSRs)、中断屏蔽寄存器组(PRIMASK, FAULTMASK, BASEPRI)、控制寄存器(CONTROL),具体功能请翻阅《CM3权威指南CnR2》第二章。 7.Cortex-M3 处理器支持两种处理器的操作模式,还支持两级特权操作。两种操作模式分别为:处理者模式和线程模式。引入两个模式的本意,是用于区别普通应用程序的代码和异常服务例程的代码——包括中断服务例程的代码。Cortex-M3
的另一个侧面则是特权的分级——特权级和用户级。这可以提供一种存储器访问的保护机制,使得普通的用户程序代码不能意外地,甚至是恶意地执行涉及到要害的操作。处理器
支持两种特权级,这也是一个基本的安全模型。(引自《CM3权威指南CnR2》) 操作系统的内核通常都在特权级下执行,所有没有被MPU禁掉的存储器都可以访问。在操作系统开启了一个用户程序后,通常都会让它在用户级下执行,从而使系统不会因某个程序的崩溃或恶意破坏而受损。这个是很多rtos需要用到SVC这个汇编指令触发SVC软中断的原因,因为程序在用户级的时候如果产生PendSV中断会引发硬件异常,导致程序奔溃;但是程序进入中断回拥有特权及权限,所以可以通过触发软中断,在软中断里面出大PendSV中断进行任务调度,保证实时任务的上下文切换。 8.Cortex-M3
在内核水平上搭载了一个异常响应系统, 支持为数众多的系统异常和外部中断。其中,编号为 1-15 的对应系统异常,大于等于 16
的则全是外部中断。除了个别异常的优先级被定死外,其它异常的优先级都是可编程的。优先级的数值越小,则优先级越高。 CM3
支持中断嵌套,使得高优先级异常会抢占(preempt)低优先级异常。有 3 个系统异常:复位, NMI 以及硬
fault,它们有固定的优先级,并且它们的优先级号是负数,从而高于所有其它异常。所有其它异常的优先级则都是可编程的。 STM32
的中断向量具有两个属性,一个为抢占属性,另一个为响应属性,抢占,是指打断其他中断的属性,即因为具有这个属性会出现嵌套中断(在执行中断服务函数 A
的过程中被中断 B 打断,执行完中断服务函数 B 再继续执行中断服务函数A),抢占属性由
NVIC_IRQChannelPreemptionPriority 的参数配置。而响应属性则应用在抢占属性相同的情况下,当
两个中断向量的抢占优先级相同时,如果两个中断同时到达,则相应优先级更高的中断,由NVIC_IRQChannelSubPriority
参数配置。NVIC 只可配置 16 种中断向量的优先级,也就是说,抢占优先级和响应优先级的数量由一个 4
位的数字来决定,把这个4位数字的位数分配成抢占优先级部分和响应优先级部分。有 5 组分配方式,其中第 4 组:所有 4 位用来配置抢占优先级,即
NVIC 配置的 24 =16 种中断向量都是只有抢占属性,没有响应属性。 所以,一个抢占式的实时操作系统,中断优先级分组应该配置位第4组。 10.SVC(系统服务调用,亦简称系统调用)和 PendSV(可悬起系统调用),它们多用在上了操作系统的软件开发中。 SVC 用于产生系统函数的调用请求。例如,操作系统通常不让用户程序直接访问硬件,而是通过提供一些系统服务函数,让用户程序使用 SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件。因此,当用户程序想要控制特定的硬件时,它就要产生一个SVC 异常,然后操作系统提供的 SVC 异常服务例程得到执行,它再调用相关的操作系统函数,后者完成用户程序请求的服务。 这种“提出要求——得到满足”的方式,很好、很强大、很方便、很灵活、很能可持续发展。首先,它使用户程序从控制硬件的繁文缛节中解脱出来,而是由 OS 负责控制具体的硬件。第二,OS 的代码可以经过充分的测试,从而能使系统更加健壮和可靠。第三,它使用户程序无需在特权级序变得与硬件无关,因此在开发应用程序时无需了解硬件的操作细节,从而简化了开发的难度和繁琐度,并且使应用程序跨硬件平台移植成为可能。开发应用程序唯一需要知道的就是操作系统提供的应用编程接口( API),并且在了解了各个请求代号和参数表后,就可以使用 SVC 来提出要求了。SVC 异常通过执行”SVC”指令来产生。该指令需要一个立即数,充当系统调用代号。 SVC 异常服务例程稍后会提取出此代号,从而获知本次调用的具体要求,再调用相应的服务函数。例如, SVC 0x3 ; 调用 3 号系统服务 11.另一个相关的异常是 PendSV(可悬起的系统调用),它和 SVC 协同使用。一方面, SVC 异常是必须在执行 SVC 指令后立即得到响应的(对于 SVC 异常来说,若因优先级不比当前正处理的高,或是其它原因使之无法立即响应,将上访成硬 fault),应用程序执行 SVC 时都是希望所需的请求立即得到响应。另一方面, PendSV 则不同,它是可以像普通的中断一样被悬起的(不像SVC 那样会上访)。 OS 可以利用它“缓期执行”一个异常——直到其它重要的任务完成后才执行动作。悬起 PendSV 的方法是:手工往 NVIC 的 PendSV 悬起寄存器中写 1。悬起后,如果优先级不够高,则将缓期等待执行。PendSV 异常会自动延迟上下文切换的请求,直到其它的 ISR 都完成了处理后才放行。为实现这个机制,需要把 PendSV 编程为最低优先级的异常。 以上内容为基于CM3内核开发一个实时操作系统我们需要知道的一些关于CM3的知识,建议去看《CM3权威指南CnR2》。 二、开始一个最简单的任务调度 (一)、任务最开始的地方 1 NVIC_INT_CTRL EQU 0xE000ED04 ; Interrupt control state register.2 NVIC_SYSPRI14 EQU 0xE000ED22 ; System priority register (priority 14).3 NVIC_PENDSV_PRI EQU 0xFF ; PendSV priority value (lowest).4 NVIC_PENDSVSET EQU 0x10000000 ; Value to trigger PendSV exception.
这是一段从ucos截取出来的代码,这段汇编程序其实特别简单,做了以下几个事情: 1.将pendSV中断设置为最低优先级 1 LDR R0, =NVIC_SYSPRI14 ; Set the PendSV exception priority2 LDR R1, =NVIC_PENDSV_PRI3 STRB R1, [R0] 2.将PSP置0 1 MOVS R0, #0 ; Set the PSP to 0 for initial context switch call2 MSR PSP, R0 3.分配堆栈给MSR,这个堆栈的作用其实是在中断嵌套的时候可以将寄存器和局部变量等进行入栈。如果中断程序较大的话或者中断嵌套较多的话,建议将这个堆栈空间设置得更大一些,我们不能只是关心任务堆栈。PS.取最后一个数组元素地址的原因是因为我们CM3的堆栈方向是从高到低的。 简单普及一下:MSR的意思是move to special register from register的缩写,可以将普通寄存器的数值保存到xpsr寄存器中。 1 ;/*在前面的头文件里定义的,这里这是写出来容易看*/2 ;unsigned int* OS_CPU_ExceptStackBase = &CPU_ExceptStack[1023];3
4
5 LDR R0, = OS_CPU_ExceptStackBase ; Initialize the MSP to the OS_CPU_ExceptStkBase6 LDR R1, [R0]7 MSR MSP, R1 4.触发pendSV异常,实现任务切换,顺便enable interrupts. LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
CPSIE I ; Enable interrupts at processor level (二)、pendSV异常服务实现任务切换 简单普及一下:MRS的意思是move to register from special register的缩写,可以将xpsr寄存器的数值保存到普通寄存器中。 1 OS_CPU_PendSVHandler 2 CPSID I ; Prevent interruption during context switch 3 MRS R0, PSP ; PSP is process stack pointer 4 CBZ R0, OS_CPU_PendSVHandler_nosave ; Skip register save the first time 5
6 SUBS R0, R0, #0x20 ; Save remaining regs r4-11 on process stack 7 STM R0, {R4-R11} 8
9 LDR R1, =OSTCBCurPtr ; OSTCBCurPtr->OSTCBStkPtr = SP;10 LDR R1, [R1]11 STR R0, [R1] ; R0 is SP of process being switched out12
13 ; At this point, entire context of process has been saved14 OS_CPU_PendSVHandler_nosave15 LDR R0, =OSTCBCurPtr ; OSTCBCurPtr = OSTCBHighRdyPtr;16 LDR R1, =OSTCBHighRdyPtr17 LDR R2, [R1]18 STR R2, [R0]19
20 LDR R0, [R2] ; R0 is new process SP; SP = OSTCBHighRdyPtr->StkPtr;21 LDM R0, {R4-R11} ; Restore r4-11 from new process stack22 ADDS R0, R0, #0x2023 MSR PSP, R0 ; Load PSP with new process SP24 ORR LR, LR, #0x04 ; Ensure exception return uses process stack25 CPSIE I26 BX LR ; Exception return will restore remaining context27 NOP28
29 END 1.这是一段从ucos截取出来的然后我修改了一下的代码,这段汇编程序也特别简单,做了以下几个事情,注释如下: 1 OS_CPU_PendSVHandler 2.几个关键的点 (1)进入pendSV异常服务程序,因为我们的任务在运行的时候使用的是进程堆栈psp,进入异常服务后使用的堆栈会自动切换称msr,同时还会修改我们CONTROL寄存器的1位为0和LR寄存器的数值为EXC_RETURN(0xFFFFFFFD),并更新PC、xPSR等关键寄存器。 (2)除第一次任务切换时可以不用对r4~r11进行入栈,其他时候我们都要对这几个寄存器进行入栈,防止被修改。 (3)15行到18行很重要,r0保存的是最新任务的堆栈指针PSP的地址,r2保存的是最高优先级就绪任务的堆栈指针,这个操作实现了将最高优先级就绪任务的堆栈指针放到PSP直接中去。通过MSR PSP, R0 将新的堆栈指针的地址赋给PSP。 (4)我们任务运行使用的是PSP,异常服务使用的是MSR,所以在退出异常的时候要使用PSP指针,所以通过修改EXC_RETURN的2位为1。 (5) 根据《CM3权威指南CnR2》,当LR寄存器的数值为EXC_RETURN时,BX LR即可实现异常返回。异常返回的时候,CPU会自动弹栈 ,按顺序将xPSP、PC、LR、R12、以及R3-R0从新的任务中弹出,保存到这些寄存器中去。此时,完成了原任务的寄存器的保存和新任务的寄存器的弹栈,其中使得PC寄存器保存了下一条指令的地址,也就是我们新的任务的执行的开始地址。完成了任务的切换。 ///////////////////// 我是最新更新的分界线 //////////////////////// (三)、初始化任务的堆栈 完成任务最开始的调度和pendSV异常服务,其实我们已将可以开始任务调度了。要实现任务调度,其实我们只要将我们的任务堆栈赋值给OSTCBHighRdyPtr,然后在通过触发pendSV中断,即可实现任务的调用,这时候就涉及到一个任务堆栈初始化的事情了。 在这里要先说一个初始化任务堆栈的一个很重要的原因,我们每次切换到新的任务时,都要从新任务的堆栈中弹出寄存器的值,而新任务的堆栈都是从上一次任务切换的时候将寄存器入栈后保存下来的结果。但是在我们第一次运行一个任务的时候,堆栈中的数值是从哪里来的呢?所以,需要我们在创建任务的时候,对任务堆栈先进行初始化,我们可以通过模拟CPU的入栈顺序,将我们的“内容”按cpu的入栈顺序填进去我们一开始i自己分配好的任务堆栈中去。我们CM3内核的CPU入栈顺序是xPSP、PC、LR、R12、R3、R2、R1、R0。接下来的内容特别重要,程序如下: 1 typedef struct os_tcb /* 任务tcb声明 */2 {3 unsigned int *pstrStack; /* 任务堆栈指针 */4 }TCB;5
6 extern unsigned int CPU_ExceptStack[1024]; /* 给MSR分配的主堆栈,不是任务堆栈 */7 extern unsigned int* OS_CPU_ExceptStackBase ; /* 主堆栈的指针 */8 extern TCB* OSTCBCurPtr; /* 当前任务控制块指针 */9 extern TCB* OSTCBHighRdyPtr; /* 就绪表的最高优先级任务 */ 1 void Task_End(void) 2 { 3 while(1) 4 { 5 ; 6 } 7 } 8
9
10 /*
11 参数1:任务TCB指针12 参数2:任务函数指针13 参数3:堆栈栈顶14 */15 void vTaskCreate(TCB* tcb,void (*task)(void),unsigned int* stack)16 {17 unsigned int *pstrStack;18 pstrStack = stack;19 pstrStack = (unsigned int*) ((unsigned int)(pstrStack)&0xfffffff8u);/* 8字节对齐 */20 *(--pstrStack) = (unsigned int)0x01000000ul; /* XPSR*/21 *(--pstrStack) = (unsigned int)task; /* r15 */22 *(--pstrStack) = (unsigned int) Task_End; /* r14 */23 *(--pstrStack) = (unsigned int)0x12121212ul; /*r12*/24 *(--pstrStack) = (unsigned int)0x03030303ul; /*r3*/25 *(--pstrStack) = (unsigned int)0x02020202ul; /*r2*/26 *(--pstrStack) = (unsigned int)0x01010101ul; /*r1*/27 *(--pstrStack) = (unsigned int)0x00000000ul; /*r0*/28
29 *(--pstrStack) = (unsigned int)0x11111111ul; /*r11*/30 *(--pstrStack) = (unsigned int)0x10101010ul; /*r10*/31 *(--pstrStack) = (unsigned int)0x09090909ul; /*r9*/32 *(--pstrStack) = (unsigned int)0x08080808ul; /*r8*/33 *(--pstrStack) = (unsigned int)0x07070707ul; /*r7*/34 *(--pstrStack) = (unsigned int)0x06060606ul; /*r6*/35 *(--pstrStack) = (unsigned int)0x05050505ul; /*r5*/36 *(--pstrStack) = (unsigned int)0x04040404ul; /*r4*/37
38 tcb->pstrStack = pstrStack;39 } 我们程序做得工作主要如下: (2)定义一个变量pstrStack指针指向栈顶,接下来程序里做的事情是初始化中断返回后从栈中恢复的8个寄存器。首先初始化的是xPSP寄存器,将它的第24位置1,表示处于Thumb状态;在c语言中,我们的函数名就是函数的首地址,从这个地址开始存放着函数的指令,我们只需跳转到这个地址就可以执行函数,所以我们开始运行一个任务需要做的事情就是跳转到这个任务的函数名,所以我们接下来做的事就是让PC寄存器指向该函数的首地址;接下来我们初始化的是LR寄存器,用来保存函数的返回地址, 我们任务执行到最后会跳转到LR寄存器指向的地址,所以如果我们的任务没有写成无限循环的形式的话,最后就会跳转到LR指向的地址。为了防止因为我们忘记将任务写成无限循环而出现系统奔溃情况,我们将LR寄存器指向了一个无限循环的函数Task_End()的地址,这增加了我们代码的健壮性。在ucos中,系统在这个函数里面可以将任务删除掉。 (3)后面的寄存器我们都是简单地随便赋值,其实是为了debug可以方便点。但是其实我们还是要关注R0~R3这四个寄存器的。在ARM中(如果我没记错的话),函数传参的时候,前四个形参都是直接都过R0~R3这四个寄存器实现参数传递的,当形参个数大于4个的话,其余的入口参数则依次压入当前栈,通过栈传参。还有一个比较重要的,我们子函数通过R0寄存器将函数返回值传递给父函数。所以,我们如果要给我们的任务函数传参,我们需要把传进来的形参存放到R0~R3寄存器中。比如uCOS和freeRTOS就都用R0寄存器传参给任务函数,uCOS还通过R1存放堆栈限制增长到的内存地址。 (4)最后,我们将我们初始化好的任务堆栈地址赋值给我们任务TCB的pstrStack指针。我们只要将这个指针指向的地址赋值给我们的OSTCBHighRdyPtr就可以任务的切换了。 (四)、任务切换演示 1.main.c的代码 1 #include "stm32f10x.h" 2 #include "bsp_usart1.h" 3 #include "OS.h" 4 #include "os_task.h" 5
6 /* 7 初始化变量和堆栈 8 */ 9 TCB tcb_task1,tcb_task2;10 unsigned int TASK_STK1[128],TASK_STK2[128];11
12 /*13 任务切换14 */15 void taskSwitch(void)16 {17 if(OSTCBCurPtr==&tcb_task1)18 OSTCBHighRdyPtr = &tcb_task2;19 else20 OSTCBHighRdyPtr = &tcb_task1;21
22 OS_CtxSw();23 }24
25 /*26 任务127 */28 void task1(void)29 {30 while(1)31 {32 printf("task1\n");33 taskSwitch();34 }35 }36
37 /*38 任务239 */40 void task2(void)41 {42 while(1)43 {44 printf("task2\n");45 taskSwitch();46 }47 }48
49 /*50 * 主函数51 */52 int main(void)53 {54 /* USART1 config 115200 8-N-1 */55 USART1_Config();56
57 vTaskCreate(&tcb_task1,task1,&TASK_STK1[128]);58 vTaskCreate(&tcb_task2,task2,&TASK_STK2[128]);59
60 OSTCBHighRdyPtr = &tcb_task1;61 OSStartHighRdy();62 }63 /*********************************************END OF FILE**********************/ 2.OS_CtxSw函数,触发pendSV异常服务
3.程序分析 (1)通过vTaskCreate函数位task1和task2初始化任务堆栈,然后将tcb_task1的地址赋值给我们的OSTCBHighRdyPtr,调用OSStartHighRdy(void)函数初始化系统,并触发pendSV中断,实现任务切换。 (2)通过在taskSwitch(void)函数,获取OSTCBCurPtr指针,然后用一个简单的if判断,对OSTCBHighRdyPtr赋值,然后触发pendSV中断,实现任务的切换。 (3)实验结果如下:可以看到task1和task2轮流被调度了。 /***************************************************************************** 第一部分终于更新完了,后面可以不要在扯CM3的那些东西了,可以愉快地讲数据结构的事了~~ |
|