|
AVR上的汇编圈圈操作系统
写前随想
在一般的单片机系统中,是以前后台的方式(大循环+中断)来处理数据和作出反应的。这应该是一般用C语言写程序的结构。我觉得这样写的程序是不可能灵活多变的,维护很困难。
早就想写关于AVR汇编圈圈操作系统的说明与分析描述,当我第一次编译通过它并仿真正确的时候,我太激动了,我对自己说:“我对UCOSⅡ源代码的研究不是在浪费时间”。
我在大学的时候学的是MCS—51型的微处理器,由于对单片机的热爱,我在网上找了一些单片机、模拟电路、数字电路、电路理论、C语言的视频教程,当我的同学在玩魔兽的时候,我在学,我感觉单片机很有趣,现在我对它的理解就是:“不断取指令与执行指令的过程”它就是我的家,这个家的资源完全由我来分配,感觉比玩魔兽有味多了。我对操作系统的理解就是“不断保存与恢复堆栈指针与寄存器”,后来参加了学校举办的电子电路大赛,由于没有指导老师和有关条件。我买了所有相关元器件和通用板以及电烙铁,在寝室整整焊了二天才完成,由于自己的表达能力太差,没有进入前三名。但我一直很怀念那段时光。
后来老师推荐我去学ARM和LINUX,面对找工作的压力和自己对嵌入式的热爱,大三下学期经老师的介绍我开始正规的学习ARM、LINUX技术,并且是免费的,郑老师也很热情,我很感谢我的单片机实验老师,没有他们的帮助,我不可能在这里乱写东西。经过5个月的学习,我对ARM、LINUX有了比较深刻的了解,一直盼望能有机会继续学习。后来就找了一份工作,老板给了我宝贵的机会接触AVR单片机,在同事的帮助下,又因为控制都是相通的,在一个半星期的努力下,自己已经就有了自己的模块库,我一直认为AVR比51强很多,无论从速度、指令集、片上资源、性价比等我都觉得AVR好很多,参考书籍也不比51少。在公司近一年里,我每天都在努力学,不紧学AVR、还有Pretel99se以及USB和电容触摸芯片CP2508等,由于我的公司离广州购书中心很近,我几乎每天晚上都呆在里面看电子与工作相关的书籍。
后来买了一本UCOSⅡ方面的书,每天晚上看上几十面,自己能勉强看的懂。看的时间多了,对自己的编程思想也有了很大的影响,看完一偏后看第二偏,第二偏的感觉就完全不一样了,感觉自己能够理解那些不是很容易理解的东西。
后来干脆自己用汇编结合UCOSⅡ思想来写一个关于自己的操作系统。刚开始,我只是抱着试试的态度来写,后来越写越感觉我一定会写成功的,因为AVR汇编指令功能太强了。不到二天,一个三任务并行运行程序写成功了,当时真的很激动!程序不到500字节。现在经过自己程序优化和试验,感觉驾驶自己写的操作系统开发速度更快,程序可读性明显增强、复杂度明显下降、任务与任务之间的接口问题消失了,根本不用担心寄存器与资源的冲突,只需要定义任务与任务通信的单元地址以及规划好任务与任务之间的同步等等。
由于在公司使用的是Atmega48V的片子,RAM只有512字节,前100字节被用做特殊功能寄存器使用,剩下412字节作为内存来使用。假如每个任务使用50个字节作为自己的私有堆栈,其中寄存器与堆栈指针占用33个单元,还剩下17个单元供程序嵌套与临时中断使用。这样我们可以定义8个任务。每个任务都相当于一个While(1);也可以在任务的结尾加入单位时间的结束语句,以进入任务调度中。
包括我在内的很多人都对51使用操作系统呈悲观态度,因为51的片上资源太少。但对于很多要求不高的系统来说,使用操作系统可以使代码变得更直观,易于维护,所以在51上仍有操作系统的生存机会。 流行的uCos,Tiny51等,其实都不适合在2051这样的片子上用,占资源较多,唯有自已动手,以不变应万变,才能让51也有操作系统可用.
51上使用操作系统不适合,AVR上使用操作系统还行,ARM上使用操作系统是必须的。当然这是我目前的理解,实际的应用我并没有验证。只是我根据芯片的硬件结构与指令得出的。操作系统是硬件与应用之间的一层软件,负责管理整个系统,同时将硬件细节与应用隔离开来,为应用提供一个更容易理解和进行程序设计的接口。一般操作系统都具有代码量大的特点,而该汇编圈圈操作系统可以并行运行和调度7个任务而代码只有1KB。
每个操作系统都是针对某一类型的微处理器而设计的。比如51、ARM、AVR等等。我写的这个汇编圈圈操作系统是专门针对AVR微处理器的。因为我觉得51的片上资源太少,速度太慢,内存太小。而AVR的片上资源丰富,速度在某种程度上是51的12倍,且性价比高等特点。ARM是专门针对高端系统的设计,关于它的操作系统很多,最常见的有UCOSⅡ、LINUX等。而关于AVR的操作系统很少,AVRX是我目前了解的源码公开的针对AVR操作系统。
我用一个在www.ouravr.com上对嵌入式操作系统的形象描述来解释一下操作系统吧,希望能更加直观的了解操作系统。
人脑比较容易接受"类比"这种表达方式,我就用"公交系统"来类比"操作系统"吧。 当我们要解决一个问题的时候,是用某种处理手段去完成它,这就是我们常说的"方法",计算机里叫"程序"(有时候也可以叫它"算法")。 以出行为例,当我们要从A地走到B地的时候,可以走着去,也可以飞着去,可以走直线,也可以绕弯路,只要能从A地到B地,都叫作方法。这种从A地到B的需求,相当于计算机里的"任务",而实现从A地到B地的方法,叫作"任务处理流程" 。 很显然,这些走法中,并不是每种都合理,有些傻子都会采用的,有些是傻子都不采会用的。用计算机的话来说就是,有的任务处理流程好,有的处理流程差。 可以归纳出这么几种真正算得上方法的方法: 有些走法比较快速,适合于赶时间的人;有些走法比较省事,适合于懒人;有些走法比较便宜,适合于穷人。 用计算机的话说就是,有些省CPU,有些流程简单,有些对系统资源要求低。
现在我们可以看到一个问题: 如果全世界所有的资源给你一个人用(单任务独占全部资源),那最适合你需求的方法就是好方法。但事实上要外出的人很多,例如10个人(10个任务),却只有1辆车(1套资源),这叫作"资源争用"。 如果每个人都要使用最适合他需求的方法,那司机就只好给他们一人跑一趟了,而在任一时刻里,车上只有一个乘客。这叫作"顺序执行",我们可以看到这种方法对系统资源的浪费是严重的。 如果我们没有法力将1台车变成10台车来送这10个人,就只好制定一些机制和约定,让1台车看起来像10台车,来解决这个问题的办法想必大家都知道,那就是制定公交线路。 最简单的办法是将所有旅客需要走的起点与终点串成一条线,车在这条线上开,乘客则自已决定上下车。这就是最简单的公交线路。它很差劲,但起码解决客人们对车争用。对应到计算机里,就是把所有任务的代码混在一起执行。 这样做既不优异雅,也没效率,于是司机想了个办法,把这些客户叫到一起商量,将所有客人出行的起点与终点罗列出来,统计这些线路的使用频度,然后制定出公交线路:有些路线可以合并起来成为一条线路,而那些不能合并的路线,则另行开辟行车车次,这叫作"任务定义"。另外,对于人多路线,车次排多点,时间上也优先安排,这叫作"任务优先级"。 经过这样的安排后,虽然仍只有一辆车,但运载能力却大多了。这套车次/路线的安排,就是一套"公交系统"。哈,知道什么叫操作系统了吧?它也就是这么样的一种约定。
公交系统 操作系统
汽车 系统资源
客户出行 任务
正在走的路线 进程
一个一个的运送旅客 顺序执行
同时运送所有旅客 多任务并行
按不同的使用频度制定路线并优先跑较繁忙的路线 任务优先级
计算机内有各种资源,单从硬件上说,就有CPU,内存,定时器,中断源,I/O端口等。而且还会派生出来很多软件资源,例如消息池。 操作系统的存在,就是为了让这些资源能被合理地分配。 最后我们来总结一下,所谓操作系统,以我们目前权宜的理解就是:为"解决计算机资源争用而制定出的一种约定"。
我解释一下我为什么用汇编写而没有用C语言?因为AVR汇编指令有131条以及X/Y/Z三个指针,131条指令足已证明汇编的灵活性,也是目前我见过汇编指令最多的一种微处理器,X/Y/Z三个指针使操作地址更加简单。AVRstudio是集编译、调试、仿真于一体的汇编开发环境软件。C语言不能操作PC、没有汇编操作硬件资源灵活以及实时性可靠性不强等特点。但C语言的可读性、移植性、通用性比汇编强。但我们用汇编写的任务调度系统可以克服汇编可读性差的特点,我们完全可以在团队开发中选择汇编作为开发语言,不用担心资源分配的问题,每一个人都可以独自享用所有资源而不用担心接口的问题。
为什么叫圈圈操作系统呢?我用一个图来模拟一下:
如上图所示:有7个任务,每个任务都可以独自享有所有资源以及任意的运行时间,它们分别都被分配到了3个圈圈里面,每个圈在计算机中被当作多任务并行运行系统。我们把圈比喻成家庭,PC比喻成访客。这里有三个家庭,每个家庭有多个成员在“同时”交流。访客刚开始访问的时候是进入了第一个家庭,当有家庭邀请的时候,她才与第一个家庭告别并进入相应的家庭做客,如果没有家庭邀请,她将一直做客某个家庭。
该圈圈操作系统也可以叫做多任务并行运行系统,在多任务应用中,每个任务都是独立运行的,内核给每个任务提供了单独的堆栈空间。在多任务应用中要创建任务、消息队列、事件、堆、分区、信号量、软件定时器和用户扩展等各种内核对象,其数目根据应用的需要是可变的。
应用开发者决定分配给每个任务的堆栈空间时,应尽可能使之接近实际需求量(这一点汇编很容易做到,但C语言就很困难)。我们可以把实时性很强的任务安排在中断服务程序中,51通常有6个中断源,AVR通常有26个左右。比如键盘程序模块、通信模块等实时性强的任务安排在中断程序服务中。对于实时系统来说,中断通常都是必不可少的机制,以确保具有时间关键性的功能部分能够得到及时运行。该汇编圈圈操作系统提供了管理中断的机制,该机制方便了中断处理程序的开发,提高了中断处理的可靠性,并使中断处理程序与任务有机地结合起来。
该内核的时间管理以系统时钟为基础,提供给应用程序所有与时间有关的服务。系统时钟是由定时器/计数器产生的中断来实现的。相邻中断时间间隔为系统的时间单位,可以随时和任意修改。对维持相对时间和日历时间、任务有限等待的计时、定时功能、时间片轮转调度提供时间参考。
1、内核的关键设计问题
1、1实时性
实时性是实时内核最重要的特性之一,所谓实时性是指实时内核应该保证系统尽可能快地对外部事件产生响应。实时性有三个特点:1、确定性。2、响应性。3、响应时间。确定性是指系统对外部事件响应的最坏时间是可预知的。一个系统是确定的,就意味着它在固定的、预先确定的时间间隔内操作。我们可以通过仿真精确的计算出每个任务运行的时间,其仿真的运行时间与实际运行时间差在1US左右。确定性和响应性结合在一起就构成了系统对外部事件的响应时间。
该系统实际上是一个多任务并发运行系统,其单位时间可以任意修改,我们可以根据每个任务运行的准确时间计算出每个圈运行一次所需要的时间,然后计算出准确的单位时间。可以将一个任务分配给多个圈,以提高该任务的实时性。
1、2可靠性
可靠性对于实时系统来说通常比非实时系统更为重要。在一个非实时系统中的一个瞬间错误,可以通过简单地重新启动系统来解决;但是一个实时系统是以实时的方式响应和控制事件的,性能的丢失或降级可能会带来灾难性的后果。为保证应用系统运行的可靠性,实时内核提供诸多机制,最常用的是异常处理、内存保护等。异常处理为用户提供了处理应用程序造成的非正常情况的机制;而对于内核运行时出现的错误,异常处理可以记录错误的来源或终止程序的运行。该系统在异常的时候会重启、停止运行或报警
1、3应用编程接口
每一个操作系统提供的系统调用的功能和种类都不同。当然,对于一个操作系统来说,它提供的系统调用越多,功能越强,也就越能对应用程序的开发提高高效的支持,同时也会减少应用程序的维护工作量。该系统可以只保留应用程序调用的系统调用程序,以减少代码量。可以将该系统裁减到只具有实时调度和信号量操作的几百字节的代码。这是目前各种操作系统不能做到的。
1、4系统的启动
在启动的时候,我们要对系统时钟的初始化、为各个任务分配私有堆栈和初始化、各种外部器件的初始化和看门狗的设置等!上电初始化完成后让最后一个任务运行以启动系统。详细程序见程序清单
1、5中断管理
中断本身是一种机制,中断服务程序不要内核的调度就可以运行。但是有时要求ISR和其他应用任务之间协同工作,以快速、合理地响应外部事件。在AVR微处理器中大约有26个外部中断,在相应的中断向量入口处加一条无条件转移指令,转到中断服务例程中。ISR运行时可以使用当前被中断任务的堆栈,也可以使用专门的中断堆栈。该系统使用当前被中断任务的堆栈,以减少其复杂度。
1、6时间管理
内核的时间管理功能为应用系统的实时响应提供支持,保证其实时性、正确性,以提高整个系统的实时工作能力,维护系统时基,执行与系统时基相关的处理或操作,比如任务延时、资源限时等待等,设置和取得时间信息、创建与删除定时器、设置定时器的触发时间、终止定时器计时。利用定时器功能可以为系统提供多个“软定时闹钟”,以满足对多种不同的定时事件进行处理的需求。定时的事件可以是单次触发,也可以是周期性触发的。
1、7对共享资源的互斥管理
与共享资源打招呼时,实现资源互斥访问的方法很多,不同之处在于互斥的范围和程度。这些方法包括关中断、使用测试并置位指令、禁止任务切换和使用信号量。
1.7.1关中断
处理共享数据时保证互斥,最简单快捷的办法是关中断。内核在处理内部变量和数据结构时就是使用的这种手段,即使不是全部,也是大部分。这也是在中断服务子程序中处理共享变量或共享数据结构的唯一方法。内核可以提供两个调用(宏),允许用户在应用程序中关中断和开中断。
从互斥的粒度来说,禁止中断是最强有力的互斥机制,这种上锁保证了对CPU的独占访问。这种方法涉及到中断级的互斥,也就是说,在互斥期间,即使外部事件引发相应的中断,系统也不会切换到相应的中断服务程序。所以在上锁期间,它可能会造成系统对外部事件反应迟钝。对于多数实时系统而言,这使得系统的实时性得不的到保证,影响着中断相应时间,因而不适合作为一种通用的互斥机制 。在任何情况下,关中断都要尽量短。如果使用不恰当,将会明显增加中断延迟。
1.7.2使用测试并置位指令
如果不使用实时内核提供的机制,当两个任务共享一个资源时,可以采用如下方法:先测试某一全局变量,如果该变量为0,这允许该任务访问共享资源,为防止另一个任务也要使用该资源,只要简单地将全局变量设置为1即可,这通常称为测试并置位。但这有个前提:测试和置位是微处理器的一条不会被中断的指令,或者在做该操作时关中断,以后再开中断。有的微处理器有硬件和置位指令。
1.7.3禁止任务切换
另一种比关中断稍弱的互斥机制是禁止任务抢占,即不允许其他任务强占当前任务的执行。禁止强占提供了一种比较小限制性的互斥,在这种情况下,ISR仍然能够执行。因为此时虽然任务切换被禁止了,但中断还可以是开着的。如果这时中断来了,中断服务程序还是会在这一临界区内执行。中断服务程序结束时,即使有优先级高的任务进入就绪态,内核还是返回到原来被中断了的任务。临界区执行完后开调度时,内核才看是否有优先级更高的任务被中断服务程序激活而进入就绪态;如果有,则作任务切换。虽然禁止任务切换也是一种有效的互斥方法,但也应该尽量避免这一类的操作,因为内核最主要的功能就是作任务调度与协调。
另外,这种方法仍然可能造成系统的实时性得不到保证。这是因为在上锁的任务离开临界区解锁之前,处于就绪态的更高优先级的任务也不能够执行,尽管这个高优先级的任务可能根本没有涉及到临界区操作。当然,这种互斥方法非常简单。使用这种方法时,同样需要控制禁止抢占的时间尽可能短。一种更好的机制是信号量。
1.7.4使用信号量
信号量是提供任务间通信、同步和互斥的最优选择,也是同步和互斥的主要手段。内核可以提高专门优化了的互斥信号量,以解决信号量机制内在的互斥、优先级反转、删除安全和递归等情况。
信号量提供比禁止中断或禁止抢占更为精确的互斥粒度,与这种方法相比,信号量将互斥仅仅限于与之联系的资源的访问。通过对共享资源上锁,实现高效的互斥访问。
信号量常被用过了头,处理简单的共享变量也使用信号量是多余的。请求和释放信号量的过程是要花一定的时间的,有时这种额外的负荷是不必要的。用户可能只需要通过关中断、开中断来处理简单的共享变量,以提高效率。
上面的4种方法,我经常使用的是信号量的互斥机制。比如我会定义一个单元(地址0X0122),该单元的内容作为任务切换的依据或者作为圈与圈跳转的同步。只需要查找该单元的内容,然后根据内容选择进程。比用C语言更简单、直接。
写UCOS的Jean J.Labrosse在他的书上有这样一句话,“渐渐地,我自然会想到,写个实时内核真有那么难吗?不就是不断地保存,恢复CPU的那些寄存器嘛。” 好了,当这一切准备好后,我们就可以开始我们的Rtos for mega48的实验之旅了。 下面用到的汇编代码并不是完整可用,完整的代码可以在我的QQ空间(343863774)去下载。只需要将所有的代码连在一起。就可以通过编译并应用。我相信,只要适当可用,最简单的就是最好的,这样可以排除一些不必要的干扰,让大家专注到每一个过程的学习。
第一步:系统初始化与启动
初始化的模块有时钟效准、看门狗、定时器T0、堆栈指针、内存的分区、任务就绪。现在我们来看一下具体的代码!
看门狗代码如下:ldi r16,0x18 //修改定时器溢出时间
sts WDTCSR,r16 //及禁止看门狗定时器时间序列程序
LDI R16,0X00
STS WDTCSR,R16
当WDTON未编程时,利用时间序列可以禁止看门狗功能。当WDTON编程时,不能禁止看门狗功能。
当WDTON未编程时,该安全等级为1,在此模式下看门狗定时器的初始化状态是禁止的,可以通过置位WDE来使能它。改变定时器溢出时间及禁止看门狗定时器需要执行一个特定的时间序列:1、 在同一个指令内对WDCE和WDE写1,即使WDE已经为12、 在紧接的4个时钟周期内将WDE和EDP设置为合适的值,用来改变定时器溢出时间或禁止看门狗。而WDCE写0。
时钟效准时序代码:在ATmega48中的OSCCAL寄存器中数值的改变的确会改变整个系统的时钟频率。读取校准字地址为:FFFH。Atmega48单片机除FALSH、E2PROM、和SRAM存储器以外,还有一个4*16的只读存储器。内部校准RC振荡器的校准参数位于4*16只读存储器高字节,低字节为芯片标志位。(1)用户可以先使用编程器或仿真器读取该只读存储器003H单元中的8MHZ校准参数,并将它写入到FLASH或E2PROM中某个地址单元。(2)用户在应用程序的系统初始化程序段中应安排从某个地址单元读取8MHZ校准参数,并将之存入校准寄存器OSCCAL。误差不会操过1%改变系统时钟频率如下: ldi zl,low(0x7ff*2) ldi zh,high(0x7ff*2) lpm sts OSCCAL,r0
用定时器T0定时产生的中断来作为系统的时钟,该系统时钟可以任意时间任意合适的位置进行修改。T0/T1引脚上计数脉冲的最高频率不能高于Fclki/o÷2.5,脉冲宽度也要大于1个CLKi/o,否则T0/T1引脚上的外部脉冲就不能被响应同步检测器所采样。T/C1既可以用作16位定时器,也可以在OC1A和OCIB引脚上输出两路PWM波形,并可在ICP引脚上输入脉冲作用下捕捉一次TCNT1中瞬时记数值。因此,T/C1是一个真正意义上的16位定时器/计数器。
我们来看一下具体的代码:
LDI R16,(1<<TOIE0)
sts TIMSK0,R16 //允许T/C0溢出中断
LDI R16,1
OUT TCNT0,R16 //定时时间为2.048MS
LDI R16,(1<<CS02)//改频率在这里改:(1<<CS00),没有分频,时间间隔32US
//(1<<CS01),8分频,时间间隔256US
//(1<<CS00|1<<CS01) 64分频, 时间间隔2.048MS
//(1<<CS02),256分频,时间间隔8.192MS
//(1<<CS02|1<<CS00),1024分频,时间间隔32.768
OUT TCCR0B,R16
系统启动的时候是使用最后一个任务的私有堆栈,因为系统在初始化后,启动的第一个任务是最后一个任务即任务7(也叫做空操作任务)。
具体代码为:
ldi r16,$FE
OUT SPL,R16
LDI R16,$02
OUT SPH,R16 //栈底地址为任务6的堆栈地址02FEH送SP
下面我们看一下内存的分区,为简单直观,我们为每个任务都分配60个字节!在这里只给出七个任务的代码!任务的私有堆栈听上去你可能会觉得不是那么容易写代码,其实我们只需要解决每个任务的堆栈首地址与堆栈指针代码就可以啦,下面的内存定义代码我解释一下:因为AVR的堆栈是向下增长的,我们最重要的是保存SP的值,只要将SP的值保存在对应任务的堆栈指针中,就可以进行简单的任务调度。
//频率为8MHZ
//CLKt0为CLKi/o的64分频
//定时时间为2.048MS
//我们规定各个任务的堆栈使用情况如下:
//保存从R0--R27、R30、R31一共30个
//利用Y指针作为堆栈操作指针。
//任务0的堆栈首地址为0X0100
//堆栈空间大小为60个字节。
//任务0的堆栈末地址为0X013B
//任务0的堆栈指针存放地址为0X013C
//任务0的堆栈指针存放地址为0X013D
//任务1的堆栈首地址为0X013E
//堆栈空间大小为60个字节。
//任务1的堆栈末地址为0X0179
//任务1的堆栈指针存放地址为0X017A
//任务1的堆栈指针存放地址为0X017B
//任务2的堆栈首地址为0X017C
//堆栈空间大小为60个字节。
//任务2的堆栈末地址为0X01B7
//任务2的堆栈指针存放地址为0X01B8
//任务2的堆栈指针存放地址为0X01B9
//任务3的堆栈首地址为0X01BA
//堆栈空间大小为60个字节。
//任务3的堆栈末地址为0X01F5
//任务3的堆栈指针存放地址为0X01F6
//任务3的堆栈指针存放地址为0X01F7
//任务4的堆栈首地址为0X01f8
//堆栈空间大小为60个字节。
//任务4的堆栈末地址为0X0233
//任务4的堆栈指针存放地址为0X0234
//任务4的堆栈指针存放地址为0X0235
//任务5的堆栈首地址为0X0236
//堆栈空间大小为60个字节。
//任务5的堆栈末地址为0X0271
//任务5的堆栈指针存放地址为0X0272
//任务5的堆栈指针存放地址为0X0273
//任务6的堆栈首地址为0X0274
//堆栈空间大小为60个字节。
//任务6的堆栈末地址为0X02af
//任务6的堆栈指针存放地址为0X02b0
//任务6的堆栈指针存放地址为0X02b1
.include"m48def.inc" //包含的头文件与字符定义
//堆栈指针存放地址定义如下
.EQU task0_stk_ptr=0x013C
.EQU task1_stk_ptr=0x017A
.EQU task2_stk_ptr=0x01B8
.EQU task3_stk_ptr=0x01F6
.EQU task4_stk_ptr=0x0234
.EQU task5_stk_ptr=0x0272
.EQU task6_stk_ptr=0x02b0
//END
//各个任务堆栈空间首地址定义如下:
.EQU task0_stk=0x0100
.EQU task1_stk=0x013E
.EQU task2_stk=0x017C
.EQU task3_stk=0x01BA
.EQU task4_stk=0x01f8
.EQU task5_stk=0x0236
.EQU task6_stk=0x0274
//END
//各个任务的SREG保存地址如下:
.equ task0_sreg_stk=0x02b2
.equ task1_sreg_stk=0x02b3
.equ task2_sreg_stk=0x02b4
.equ task3_sreg_stk=0x02b5
.equ task4_sreg_stk=0x02b6
.equ task5_sreg_stk=0x02b7
.equ task6_sreg_stk=0x02b8
.equ task_all_sreg_stk=0x02b9 //一旦进入任务调度,就把SREG保存在此
//上一个任务是哪个任务在运行不知道,所以我们把上个任务号存在此。
.equ task_shang_stk=0x02ba
从上面的地址分配我们可以知道:我们定义了每个任务的堆栈空间首地址,是为了好保存寄存器使指针从下往上增长,看起来可读性强一点;定义了每个任务的堆栈指针存放地址,是为了方便直观存放堆栈指针SP。在下面的任务切换代码中你就会知道。
任务就绪:在初始化的时候必须运行的步骤,是为了在任务调度的时候有所有任务的记录或存在。
//////////////////////////////////////////////////////////////
//保存任务5在FLASH中的起始地址在task4堆栈空间中
ldi Yl,low(task5_stk_ptr-2)
ldi yh,high(task5_stk_ptr-2)
ldi r16,high(task5)
st y+,r16
ldi r16,low(task5)
st y,r16
//保存任务5堆栈指针的保存
ldi Yl,low(task5_stk_ptr)
ldi yh,high(task5_stk_ptr)
ldi r16,low(task5_stk_ptr-3) //这里特别需要注意是减3
st y+,r16
ldi r16,high(task5_stk_ptr-3)
st y,r16
///////////////////////////////////////////////////////////////////
大家在这里要注意到:最后的一个任务我没有写任务就绪程序!这是因为系统初始化完后运行的就是最后一个任务。所以就不用写啦!
我解释其中一个任务的任务就绪代码:
ldi Yl,low(task5_stk_ptr-2) //将任务5的堆栈末地址传送给Y指针。
ldi yh,high(task5_stk_ptr-2) //减2是因为AVR的FLASH和RAM的地址都是16位的。需要2个地址单元进行存放一个地址。
ldi r16,high(task5) //将任务5的高地址字节传送给R16
st y+,r16 //将R16的内容传送给Y指针指向的地址中同时Y指针指向的地址加1。
ldi r16,low(task5) //将任务5的低地址字节传送给R16
st y,r16 //将R16的内容传送给Y指针指向的地址中
这6句是把任务5的标号地址写入任务堆栈末地址中,以记录该任务的存在。
第二步:任务的调度
任务调度目的是为了解决PC与SP的交换和保存。当T0定时器定时的时间到了的时候,会产生定时器T0中断,在进入中断的时候,PC的值会自动保存在对应任务的堆栈中。只要不使用象RCALL或中断等保存PC指针的指令,首先弹出的内容就是当前任务的PC值。前面我们不是定义了堆栈指针保存的单元地址吗!只要我们在没有返回的时候,偷换堆栈指针保存的单元地址,就可以偷换PC与SP。从而实现我们的目的。
下面我详细解释每条语句的目的:R25的内容作为任务切换的依据,比如R25等于0,则在下一个时间片到来的时候,会让任务0运行。Y指针作为任务切换时的专用指针,是保存或恢复寄存器指令中必须用到的。因此在所有任务的代码中不能使用Y指针和R25。
.org $010
rjmp TIM0_OVF //定时器T0的中断向量入口为0X0010
…………
TIM0_OVF:
in r28,sreg //在任务调度中首先要保存的是SREG,这里的R28是YL
STS task_all_sreg_stk,r28
LDI R27,1 //修正时间常数初值TC
OUT TCNT0,R28 // 重装时间常数初值TC
LDI R28,$00
STS (TCCR0B+0X20),R28 //T0不工作
//这里存放任务切换代码
Clz //清除0标志位
cpi r25,0 //比较R25的内容是否等于0
breq task0_chang //相等,则切换到任务0
cpi r25,1 //比较R25的内容是否等于1
breq task1_chang //相等,则切换到任务1
cpi r25,2 //比较R25的内容是否等于2
breq task2_chang //相等,则切换到任务2
cpi r25,3 //比较R25的内容是否等于3
breq task3_chang //相等,则切换到任务3
cpi r25,4 //比较R25的内容是否等于4
breq task4_chang //相等,则切换到任务4
cpi r25,5 //比较R25的内容是否等于5
breq task5_chang //相等,则切换到任务5
cpi r25,6 //比较R25的内容是否等于6
breq task6_chang //相等,则切换到任务6
rjmp task6_chang //如果任务调度错误,则让任务6运行
task1_chang:rjmp task1__chang
task2_chang:rjmp task2__chang
task3_chang:rjmp task3__chang
task4_chang:rjmp task4__chang
task5_chang:rjmp task5__chang
task6_chang:rjmp task6__chang
//////////////////////////////////////////////////////////////////
task0_chang:
ldi r25,1 //指向下一个任务的参数,R25的内容决定下一个时//间片CPU执行哪一个任务,在这里也是应用程序开发者灵活编程以及构件自己的圈圈系统//的关键。是编写圈与圈之间跳转代码的条件判断。也是存储上一个任务的参数判断。这里//相当于十字路口,往哪里行走,任务与任务之间、圈与圈之间可以协商,制定出最合适的//路线。所以这里是应用程序开发者最值得花时间、思考的地方。
clz
lds r28,task_shang_stk //将上一个任务的标号送给R28,并判断上一个
//时间片的任务号。
cpi R28,0 //判断R28的内容是否等于任务号0
BREQ task00_shang //相等,则保存任务号0的SP,不等,则继续判断!
cpi r28,1 //判断R28的内容是否等于任务号1
breq task10_shang //相等,则保存任务号1的SP,不等,则继续判断!
cpi r28,2 //判断R28的内容是否等于任务号2
breq task20_shang //相等,则保存任务号2的SP,不等,则继续判断!
cpi r28,3 //判断R28的内容是否等于任务号3
breq task30_shang //相等,则保存任务号3的SP,不等,则继续判断!
cpi r28,4 //判断R28的内容是否等于任务号4
breq task40_shang //相等,则保存任务号4的SP,不等,则继续判断!
cpi r28,5 //判断R28的内容是否等于任务号5
breq task50_shang //相等,则保存任务号5的SP,不等,则继续判断!
cpi r28,6 //判断R28的内容是否等于任务号6
breq task60_shang //相等,则保存任务号6的SP,不等,则继续判断!
task00_shang:
rcall task0_cun_chu //调用任务号0的保存寄存器和SP的子程序
ldi r16,0 //将当前任务号0保存在task_shang_stk中
sts task_shang_stk,r16 //记录该任务为上一次任务
rjmp task0_huifu //跳转到任务号0恢复寄存器和SP的子程序
task10_shang:
rcall task1_cun_chu //调用任务号1的保存寄存器和SP的子程序
ldi r16,0 //将当前任务号0保存在task_shang_stk中
sts task_shang_stk,r16 //记录该任务为上一次任务
rjmp task0_huifu //跳转到任务号0恢复寄存器和SP的子程序
task20_shang:
rcall task2_cun_chu //调用任务号2的保存寄存器和SP的子程序
ldi r16,0 //将当前任务号0保存在task_shang_stk中
sts task_shang_stk,r16 //记录该任务为上一次任务
rjmp task0_huifu //跳转到任务号0恢复寄存器和SP的子程序
task30_shang:
rcall task3_cun_chu //调用任务号3的保存寄存器和SP的子程序
ldi r16,0 //将当前任务号0保存在task_shang_stk中
sts task_shang_stk,r16 //记录该任务为上一次任务
rjmp task0_huifu //跳转到任务号0恢复寄存器和SP的子程序
task40_shang:
rcall task4_cun_chu //调用任务号4的保存寄存器和SP的子程序
ldi r16,0 //将当前任务号0保存在task_shang_stk中
sts task_shang_stk,r16 //记录该任务为上一次任务
rjmp task0_huifu //跳转到任务号0恢复寄存器和SP的子程序
task50_shang:
rcall task5_cun_chu //调用任务号5的保存寄存器和SP的子程序
ldi r16,0 //将当前任务号0保存在task_shang_stk中
sts task_shang_stk,r16 //记录该任务为上一次任务
rjmp task0_huifu //跳转到任务号0恢复寄存器和SP的子程序
task60_shang:
rcall task6_cun_chu //调用任务号6的保存寄存器和SP的子程序
ldi r16,0 //将当前任务号0保存在task_shang_stk中
sts task_shang_stk,r16 //记录该任务为上一次任务
rjmp task0_huifu //跳转到任务号0恢复寄存器和SP的子程序
还有其他的任务号X的保存与恢复寄存器的代码和上面任务号0的代码结构是相同的。没有什么区别,在这里就不在重复了。其实这样重复的结构代码可以进行程序优化,但需要更多的切换时间资源。
现在让我们看一看是如何保存寄存器和SP的。rcall task0_cun_chu 调用任务号0的保存寄存器和SP的子程序。Task0_cun_chu子程序详细解释代码如下:
task0_cun_chu:
//任务号0的寄存器保存
ldi yl,low(task0_stk) //将任务号0的堆栈首地址传送给YL
ldi yh,high(task0_stk) //将任务号0的堆栈首地址传送给YH
rcall stst //将R0—R24、R26、R27、R30、R31寄存器保存在任务//号0的堆栈空间中。
lds r27,task_all_sreg_stk //将SREG内容保存到任务号0的私有堆栈SREG单元地址中
STS task0_sreg_stk,R27
//任务堆栈指针SP的保存
ldi Yl,low(task0_stk_ptr) //保存堆栈指针SP在任务号0的私有堆栈的堆栈指针存放地址单元中
ldi yh,high(task0_stk_ptr)
in r27,spl //当前任务的SP低字节保存在R27中,这里大家也许会说:“R27也不能在应用程序当中出现”。如果你这样问,我觉得你看的懂汇编。我解释一下:R27能在应用程序当中出现,并不会引起数据的丢失。因为我们已经保存了除R25、R28、R29之外的所有寄存器,也没有激活下一个时间片的任务的堆栈。
inc r27
inc r27 //这里将R27的内容加2是因为从当前的SP弹出的内容不是当前任务被中断的入口地址,因为我们使用了rcall task0_cun_chu修改堆栈内容的指令,又因为PC是以16位格式保存在堆栈中,而单元内容是8位的。
st y+,r27 //
in r27,sph
st y,r27
ret
这样就完成了SP保存到私有堆栈的堆栈指针存放地址单元中。说到这里大家会说:“那怎么恢复寄存器与对应的任务堆栈呢?不用急,慢慢来,急是做不好的。我们在任务切换的代码中不是有rjmp task0_huifu 指令吗!这条指令是将PC的内容修改为标号task0_huifu在FLASH中所对应的地址值,CPU跳到标号task0_huifu处执行。下面我们来看一下代码:
task0_huifu:
//堆栈指针的恢复
ldi Yl,low(task0_stk_ptr) //将任务号0的存放堆栈指针的地址传送给Y指针,目的是为了将该任务号的堆栈指针传送给SP。
ldi yh,high(task0_stk_ptr)
ld r27,Y+ //将任务0的栈底地址送SP
out spl, r27 //R27的内容为SP的低字节并送给SPL,这里大家也许有疑问:“为什么R27的内容就是下一个时间片要执行的任务的堆栈指针的低地址并且堆栈指针弹出的内容正好是下一个要执行任务的断点入口地址呢?因为我们在保存SP的时候,只有仅有保存与对应任务的断点入口地址相对应的SP。否则在执行RETI的时候,就不能返回到下一个将要执行的任务。这也是为什么我们使用RJMP指令而不能用RCALL指令的原因。
ld r27,y
out sph,r27
//寄存器的恢复
LDS R27,task0_sreg_stk
OUT SREG,R27 //这两句是将下一个任务的SREG恢复,大家可能又会问:“现在恢复SREG会不会太早啦?”我的理解是不早也不迟,下面的指令都不会引起状态寄存器的内容发生变化,并且任务切换马上就会结束。
ldi yl,low(task0_stk) //恢复下一个任务的寄存器。
ldi yh,high(task0_stk)
RCALL ldld //调用恢复寄存器的子程序
LDI R28,(1<<CS00|1<<CS01) //这里的指令是激活系统时钟,修改任务时间片长短。如果该任务的实时性很强或者使用频度很高,可以将该任务运行的时间改长一点。
//(1<<CS01),8分频,时间间隔256US
//(1<<CS00|1<<CS01) 64分频, 时间间隔2.048MS
//(1<<CS02),256分频,时间间隔8.192MS
//(1<<CS02|1<<CS00)//1024分频,时间间隔32.768
OUT TCCR0B,R28
RETI //将PC指针指向任务0 ,即任务调度成功。
总结:写一个适合自己的操作系统是十分有必要的,但肯定有人说:“你以为我们都是神啦,说写就写”!其实我在写的过程中也是遇到了很多问题,但我觉得只要你掌握了AVR的硬件结构与体系以及知道CPU是如何运行与处理资源的。就一定可以写。学汇编也就是学硬件。熟练掌握汇编指令,并不要求你背与记,最重要的是理解执行过程。因为单片机就是取指令与执行指令的过程。
写程序是一步一步的,特别是汇编,一步就是一步,不能少也没有必要多。
要理解单片机并不是一天二天就能会的。还是要有一定的基础与坚持,最基础的模拟电路与数字电路以及C语言,还是要学的。特别是作为一名嵌入式工程师,学的东西更多,要掌握好它必须不断的积累与坚持。敢于尝试,敢于创新,敢于挑战。有自己的实验室,不断实验!心静心静!
邓贻军制作
2009.02.05
E_mail:dengyijun103@163.com |
阿莫论坛20周年了!感谢大家的支持与爱护!!
你熬了10碗粥,别人一桶水倒进去,淘走90碗,剩下10碗给你,你看似没亏,其实你那10碗已经没有之前的裹腹了,人家的一桶水换90碗,继续卖。说白了,通货膨胀就是,你的钱是挣来的,他的钱是印来的,掺和在一起,你的钱就贬值了。
|