新闻中心

EEPW首页>嵌入式系统>设计应用> DIY:给单片机写个实时操作系统内核!

DIY:给单片机写个实时操作系统内核!

作者: 时间:2016-11-29 来源:网络 收藏

4. 汇编语言。为什么要学汇编?可能有些人会学得汇编难理解,而且现在C语言已经可以很方便地编程了,所以不想学汇编,其实C语言再怎么方便强大,最后还是要通过编译器转换为汇编语言再由汇编转换为机器码,才能在机器中执行。可以说,掌握了汇编之后你一定会对”代码是怎么在CPU里面执行的“这个哲学命题有进一步的了解。另外,不学汇编你还真写不出一个操作系统内核来,因为操作系统的最低层是要直接操作CPU寄存器的,C语言无法实现,只能用汇编写出来,再由上层的C语言调用。汇编的书很多,这里就不介绍了,找一本去狂敲上个把星期就大概掌握了。


5. 另外你还要懂得计算机原理以及单片机,其实单片机就是一台阉割版的计算机,你得对CPU寄存器,数据总线,地址总线,以及执行方式这些有一定的了解才行,这方面的书也挺多的,不过介绍两本个人觉得写得挺好的书供课外闲读,《编程卓越之道》1、2卷,这本书大体上介绍了高级语言是怎么样在CPU里面执行的,另外也对CPU内部结构做了一些介绍,比那些课内教材写得好,有空可以去看一下。


最后介绍一本《嵌入式实时操作系统UCOS II》,这本书介绍了UCOS II这个操作系统的内部源代码以及实现原理,我就是从这本书中学到了怎样写一个可以用的操作系统内核。

书单推荐完毕,下面进入重点~~~~~~~~~~~~~~~~~


/**************************************************************************************/
什么是操作系统?其实就是一个程序, 这个程序可以控制计算机的所有资源,对资源进行分配,包括CPU时间,内存,IO端口等,按一定规则分配给所需要的进程(进程?也就是一个程序,可以单独执行),并且自动控制让CPU可以执行多个互不相关的任务,按照书中的介绍,一个操作系统需要具备四个要素:进程调度、内存管理、IO管理、文件管理。

那怎么样可以让CPU同时执行多个任务呢?首先想象一下如果让CPU执行单道程序,它会从MAIN函数开始一直顺序地执行下去,CPU里面有一个叫PC的寄存器,也就是程序计数器,它永远指向下一条要执行的指令的存放地址,因为大多数情况下指令都是逐条执行的,所以PC寄存器也只是简单地加一,所以大家都叫它”程序计数器“,从PC寄存器的特点也许我们可以做点文章?比如人为地让PC寄存器指到另外一段程序的入口地址,那CPU不就自动地跑到另一段程序了么?哈哈。假如我们可以这样做,那没错,CPU确定是跑到别人的领地去执行代码了,问题是:怎么样让它回来继续执行?换句话说,PC寄存器改变之后CPU 已经不知道刚刚这段程序执行到哪里了,亦即跑不回来了,就像断了线的风筝。呃。。这问题麻烦。。解决了这个问题就似乎有点苗头了。。

好吧,我们来看看有一个很相似的问题,就是单片机在执行代码的时候,突然有一个中断信号过来了,单片机马上就屁颠屁颠地跑到中断服务程序里面去执行了,执行完毕之后,奇怪!!它怎么还记得跑回来原来的地方!!??OH NO .它是怎么办到的。其实这里还要介绍另外一个寄存器叫SP的,即:STACK POINTER堆栈指针,这个指针指向一个内存的地址,里面存放了一些数据。首先,单片机遇到中断信号的时候,它就把当前的PC寄存器的值保存到SP所指的地址,这就相当于它记住了当前执行的地方,叫做断点保护,然后PC寄存器就指向中断服务程序的地址,下一个时刻CPU就自动执行中断服务程序里面的代码了,执行完毕之后中断服务程序调用了一个指令:RETI,这条指令叫返回指令,在函数结束之后调用,它会自动从SP指针指向的地址把值取出来放到PC寄存器里面,然后CPU就会自动回到之前断掉的地方继续执行了!基于这个原理,我们可以回到上面的问题:首先,让CPU把当前的PC保存起来,然后把PC指向别段程序地址,CPU就跑到别人的领地去执行了,执行完了之后我们可以把SP指向的内容放回PC,这样调用RET指令之后,CPU就会回到原来的地方继续执行了!!貌似这个问题完美地解决了!!

可是还有一个关键的问题:CPU在执行当前代码的时候 CPU里面所有的寄存器都保存的当前这个程序所用到的值,比如做加法的时候用到PSW寄存器的进位标志位,如果此时切换到别的任务,那再回到当前程序的时候,这些值都会被改变,CPU会陷入混乱然后直接跑飞!!解决这问题同样要靠SP同学,在切换任务的时候我们把所有寄存器依次入到SP指向的地址,称为入栈操作,每次入栈SP指针的值都会加一或者减一,视不同CPU而定。而要恢复的时候,就从SP指向的地址依次把值取出来放回原来的地方,称为弹栈操作。最后才弹出地址到PC寄存器,下一时刻,CPU自动跑到原来的地址继续执行,从CPU的角度看就像没有发生任务切换一样,一切依旧,继续工作。如果CPU的执行速度够快,切换速度也够快,这样就可以给人感觉CPU同时在执行很多任务,这就是操作系统里面最基本的原理。

SO,解释完原理,我们首先来就来实现简单的任务切换,这里的难点就在于:执行这一动作必须要操作CPU的寄存器,而C语言是无法实现的,这就是为什么要用到汇编的原因了,所有操作系统的最底层代码都是用汇编语言实现的,否则根本无法实现任务切换。下面要介绍汇编里面的几条相关指令。PS:虽然每种CPU的汇编都不同,但是基本原理还是相通的。
第一条:CALL。函数调用指令,当我们要调用一个函数的时候,就会用到CALL这条指令,它执行再从个动作,第一,先把当前的PC值保存起来,即现场保护,第二,把要调用的函数的入口地址送到PC,这样,在下一时刻到来的时候,CPU就自动跳转到特定的函数入口地址开始执行了。
第二条:RET/RETI。当一个函数执行完毕的时候,需要返回到原来执行的地方,这时候就要调用 RET指令(在中断函数中返回的时候调用RETI指令)。它把SP指向的数据,即上一次调用CALL时保存的那个地址原来到PC,这样,当下一时刻到来的时候,CPU就会跳回到原来的地方了。实际上函数调用过程就是这样的,所以有时候一些简单简短的函数宁愿用#define宏定义写出来,因为这样写出来就不用使用调用/返回过程,节省了时间。
第三/四条:PUSH/POP。这两个指令是两兄弟,即入栈及出栈。关于堆栈的特性说明一下:堆栈这种结构的特性就是后进先出,就像叠盘子一样,最后叠上去的盘子会被最先取出,这种原理非常好用,想象一下函数嵌套的时候发生的一切,就是利用到这种思路。PUSH指令用到把寄存器的值保存起来,它会把值到保存到SP指针所指的地方。POP指令则把数据从SP所指的地址恢复到原来的寄存器中。

用这几条指令,我们就可以写出一个任务切换函数了,不过写之前还要说明一下什么叫人工堆栈。其实上,一个程序在执行的时候,它会用到一块内存空间用于保存各种变量,比如调用函数的时候这块地方会用于保存地址以及寄存器,而在执行一些复杂算法的时候,如果CPU的寄存器已经用完了,这块地方也会作为临时中间变量的存放区,另外,在向一个函数传递参数的时候,比如:printf(a,b,c,d,e....),如果参数过多,多余的参数也会先存放到这块地方。所以说,这块地方就像是这个程序的仓库一样,存放着要用的东西。如果是在单道程序中,显然这样用没问题,但是如果是多道程序的话,问题就来了,因为如果所有任务共用那块区域,那旧任务保存的东西就会被新任务所冲掉,CPU一下子就疯掉了。。解决的办法就是:每个任务都给它提供一块专用的区域,这块专用区域就叫人工堆栈,每个任务都不一样,保证了不会相互冲突。

PS:因为51单片机的内存太小,基本无法实现多任务,实现了也不实用,所以硬件平台我选用了AVR单片机ATMEGA16,有1KB内存,应该够用了,花了两天时间把AVR的汇编指令看了一遍

首先,当需要切换任务的时候,要先把当前的所有寄存器全部入栈,在AVR单片机中有32个通用寄存器R0-R31,还有PC指针,PSW程序状态寄存器,这些都要入栈,所以需要的内存挺多的。现在的编译器都支持在线汇编,就是在C语言里面嵌入汇编语言,方便得多,下面我宏定义了一组入栈操作:PUSH_REG(),里面是用PUSH指令把32个寄存器全部入栈

#define PUSH_REG()
{_asm("PUSH R0" "PUSH R1" "PUSH R2" "PUSH R3"
"PUSH R4" "PUSH R5" "PUSH R6" "PUSH R7"
"PUSH R8" "PUSH R9" "PUSH R10" "PUSH R11"
"PUSH R12" "PUSH R13" "PUSH R14" "PUSH R15"
"PUSH R16" "PUSH R17" "PUSH R18" "PUSH R19"
"PUSH R20" "PUSH R21" "PUSH R22" "PUSH R23"
"PUSH R24" "PUSH R25" "PUSH R26" "PUSH R27"
"PUSH R28" "PUSH R29" "PUSH R30" "PUSH R31" ); }
入完栈完接下来要保护当前程序的SP指针,以便下次它要返回的时候能找到该人工堆栈的地址:
OS_LastThread->ThreadStackTop=(OS_DataType_ThreadStack *)SP;
这一句用C语言就可以实现了。
接下来关于当前这段程序的现场算是保护好了,然后找到要切换到的任务的人工堆栈地址,把它赋给SP指针,如下:
SP=(uint16_t)OS_CurrentThread->ThreadStackTop;
出栈跟入栈的语法差不多,只是出栈顺序要相反:
POP_REG();
接下来,要调用一条很重要的指令了!!!此令一出,CPU就乖乖地切换任务了!
_asm("RET");
调用返回指令,它就从SP里面取出函数地址放到PC,注意他取出的是刚刚放入SP指向地址的函数入口,所以它会返回到新任务执行。
就这样,一个操作系统里面最核心的”任务调度器“的模型就这样简单地实现了,操作系统里面所作的跟任务切换有关的事情到最后都要调用到这个任务调度器,现在我们实现调度器了,相当于成功了1/3,接下来的事情就是考虑在什么情况下调用这个调度器。

评论


技术专区

关闭