转自:http://www.51hei.com/bbs/dpj-59966-1.html
作者: 朱有田
本文仅讨论笔者在实践分时系统过程中的方法和思想,不拘泥于编程语言工具及其技巧,不用程序语言描述,全文采用叙事的方法,引出工程实践过程中解决一些问题的过程和思路。
第一部分: 单CPU计算机运行的模型
计算机最基本模型是图灵模型(相关知识自行脑补),计算机将所有要处理的复杂任务分解到有限的基本的操作(指令),这个操作的集合就是指令集,指令集被设计固化到硬件(CPU或处理器)中。程序是为了解决特定问题而编制的一个指令序列,计算机的运行就是一个在时间上串行的一个指令流。
如果有2个程序需要在一台计算机上运行,常见的场景是先运行其中一个程序,运行结束后,再运行第二个程序。
第二部分: 单CPU计算机中多个程序并发执行
多个程序并行的概念出现的很早,早期为了共享昂贵的计算机资源,人们试图使一个计算机为多个用户同时提供服务。多个程序并发执行,采取分时的方法来实现,称分时系统。
分时即将时间视为资源进行分配,将时间切分为人们感知上较小的一个单位,比如20毫秒(0.02秒),称为一个时间片。若程序1和程序2都要被执行,每个程序轮流被执行一个时间片。从宏观(感官)上来看,程序1和程序2是同时一起执行的,就像两台计算机在同步工作一样。
若程序1和程序2同步运行,计算机是这样进行的:先运行程序1,20毫秒后,切换到程序2,20毫秒后又切换到程序1,20毫秒后再次切换到程序2,如此反复… 一秒钟切换了50次,程序1和程序2都在运行、暂停、运行、暂停这样的状态中进行,由于切换时间够快,人的感官认为程序1和程序2是同步运行的。
第三部分: 中断和定时器的作用
要实现分时的机制,离不开定时器和中断机制。定时器就是定时发出中断信号让计算机能够进入切换程序;中断机制是指计算机的硬件要能够支持在执行过程中被中断,跳转到指定的中断程序中去运行,并在运行结束后能够返回到被中断的点继续运行原来的程序。如果计算机硬件没有中断机制,则无法实现分时。
对于信号的输入处理,程序总是以顺序的方式进行的。比如按下一个按键,程序并不是立即就知道有按键被按下,而是要等到程序指针运行到按键处理程序时才会被发现。按键响应的速度取决于程序指针本身的运行速度和按键处理程序的长短及间隔距离,前者依赖于硬件主频,后者依赖于程序设计的水平。这种依赖程序主动去发现的信号输入方式一般称查询方式,与查询方式相对应的是中断方式。
中断方式是设计在硬件级别的,假如按下一个按键采用中断的方式处理,那么当按键按下时触发中断,系统执行完当前指令后保存当前的程序指针位置(返回时候用),然后将程序指针跳转到指定的点,程序设计时在这个点编写好按键处理程序,程序结束后返回到被中断的点继续运行。很显然,中断方式的响应时间很小且容易估算,程序也不需要“定时的去查询”。中断机制依赖于硬件提供的中断资源(可接受的中断源数量),往往比较有限;其次当多个中断信号发生时,要考虑优先次序和中断嵌套(中断过程中再响应中断)的问题。
事实上,中断的实现也是一种查询方式,当中断信号发生时,计算机并不是真正的立即响应,而是要等待当前的指令被执行完,硬件才会去查询是否有中断信号存在。中断也可以理解为在微指令级别(一个指令由一段微指令组成)下进行查询处理的,其颗粒度是一条指令。
定时器是一种二进制计数器,其硬件上原理非常简单:用一些边沿触发器串接起来就能构成一个二进制计数器,只要输入一个方波,就能使计数器的数值加一。当计数器加到溢出时,将溢出信号引出到中断信号,就能起到定时的作用。我们通过给定时器设置一个初始值来设置定时器时间的长短。
比如在程序中要等待一秒钟的延时,如不用定时器,则必须让程序指针在一个循环体中空转N次,达到延时的作用。这种方式有两个弊端:一是程序空转浪费计算机资源,在这个延时过程中,程序啥也干不了,无法响应按键,无法刷新显示,如同死机一样;二是延时精度难控制,由于程序指令本身执行时间有长短之分,要达到精确的延时,程序设计要反复调教循环体空转的次数。
如采用定时器进行一秒延时,只要在定时器内设置好初始值(使初始值到溢出的时间刚好为1秒),开启定时器后,程序可以继续执行其他工作,如按键扫描等。当1秒时间到达,定时器溢出发出中断信号,使程序指针跳转到中断响应程序,程序在此进行延时后的处理。由此可以发现,定时器相当于硬件级别并行的设备,程序指针的跑动和定时器内计数值的增加是并行的。
言归正传,定时器和中断是实现分时系统的基础,其中中断机制尤为重要,70年代流行的PDP系列小型机上,分时系统的定时中断是由一个从工频电源上获取的50-60Hz的定时中断外设产生,称之为电源时钟,令人脑洞大开。现代操作系统的祖宗UNIX就是在这种机型上产生的一种分时操作系统,UNIX最初的用户是贝尔实验室的文员们,他们用它来处理专利文书,由于多个终端上可以同步进行操作,又能方便进行共享和调用,得到当时女职员们的好评。
摘要:UNIX是用于DEC PDP-11及Interdata 8/32计算机的一个通用的交互作用的分时操作系统。从1971年开始以来,使用日趋广泛。UNIX所论及的内容有以下领域: 1.文件结构:一个统一的、可随机寻址的字节序列。取消“记录”的概念。文件寻址的效率。2.文件系统设备的结构:目录与文件。3.I/O设备与文件系统的一体化。4.用户接口:外壳程序原理,I/O重新定向以及管道。5.进程环境:系统调用、信号、以及地址空间。6.可靠性:瘫痪,文件的丢失。7.安全性:损坏与检查时数据保护;阻塞情况下的系统保护。8.一个高级语言的使用—收益与代价。9.UNIX系统没有一般意义上的“实时”、进程间通讯、异步的I/O等功能。
UNIX内核由10,000行左右的C语言代码和1,000行左右的汇编语言代码所组成。汇编语言代码又可进一步分成两部份:一部份为200行,包括为提高系统效率而设计的那些代码(可以用C语言来写);另一部份为800行,包括不能用C语言写的、执行硬件功能的那些代码。
第四部分: 分时的意义
从程序设计角度,程序是一个单线顺序的指令流(语句流),在程序里,指令(语句)一个接一个被执行,从开始到结束,或条件转移,或反复循环。程序员安排好这一切前后处理的次序。在应用角度,用户打开程序,运行程序,直到结束退出,再打开另一个程序。在DOS时代,人们就是这么做的,那真是一个单纯的时代。
在分时系统下,多个程序并行,程序员设计程序时可以设计多个线程,比如一个线程负责显示,一个线程负责键盘输入,一个线程负责数据处理。每一个线程都是单独的程序,最终他们将同步运行,程序员除了要考虑单个线程的运行,还要考虑线程之间的同步、通信和互斥。这改变了传统的程序设计思维,使软件的表现和可能性更加丰富多彩,程序设计手段也更加多样。
在应用角度,当你在编辑文档时,同时又在下载电影,耳机里又能播放着音乐。这些当下看起来稀松平常的事情,在计算机还是单任务时代真是难以想象的。
第五部分: 在80c52单片机上实践分时系统
分时系统并不高不可攀,了解以上的概念后,任何有经验的程序员都能对它进行实践,笔者就是在最便宜和古老的51单片机上实践了分时系统。
80c52是intel的一款增强型mcs-51单片机,51系列8位单片机自80年代开始至今仍被广泛使用在各种场景,这种芯片很便宜,也很容易就能搞到。后来Intel将mcs-51授权给了多个芯片制造商。以ATMEL生产的AT89S52为例,它支持总计111条的51指令集,256字节RAM,8k字节程序闪存(可反复刷写),3个16位计时器,5个中断源,1个串口,4个8位IO口。要使她运行起来非常的方便:只需在XTAL管脚之间加一个晶振和2个27pF电容(晶振的频率决定运行速度),在VCC脚加5V电源,然后把RESET管脚对地短路一下(复位),计算机便开始从程序闪存的0地址开始取指令执行..
没玩过51单片机的读者一定好奇程序是怎么编制到芯片的闪存里面去的。单片机程序的开发主要依赖个人电脑,首先通过文本编辑器编写源程序,然后通过51的编译器程序编译成目标文件,最后通过芯片烧录器将目标文件复制(烧入)到51芯片内。然后你的程序就可以在单片机跑动了。
事实上,无论是伟福,还是uvsion,都提供了包括编辑器、编译器、仿真和调试为一体的开发环境,程序在编制完成后,直接在开发环境里面仿真调试,经过排错后,将问题降到最低再烧入到单片机运行,这比反复烧写芯片验证程序效率要高的多。
最要紧的事情是:AT89S52具备实现分时系统的必要条件:1支持中断,2具备计时器,3勉强够用的RAM空间。
第六部分: 时间片的分配
实现分时系统的第一要考虑怎么进行时间片的分配,也就是程序之间的切换问题。这个很简单,可利用一个定时器产生一个固定的延时,当延时到达,进入定时器中断,将这个中断的入口跳转到切换程序。注意:程序的切换不是简单的跳转,而是要先保存当前程序的执行状态、被中断的地址,不至于下次回来继续当前程序时变量、状态都被改变,那就会乱套了。然后再选择下一个要运行的程序,将被选中程序的状态恢复到当前的各状态寄存器当中,恢复到这个程序上次被打断的点继续运行,等待下一个时间片用完,进中断,重复以上过程。
既然每个程序都要保存和恢复它的运行状态,那么问题来了。一是要保存哪些信息,称之为保护的范围;二是需要多少内存,能够支撑多少个程序进行分时。
第一个问题,保护范围取决于用户程序的断点地址、状态字、寄存器组,以及堆栈空间。第二个问题,支撑多少个程序进行分时取决于内存的大小,89C52的256字节支撑8个线程后,留给应用程序的内存只有36个字节,因此80c51的128个字节RAM是不够的。
在此,为了区别于系统程序,改称以上所谓被分时的程序为任务或线程。
第七部分: 内存组织的方法
我们能设想到最原始的方法是通过数组(数据表)的方式,将每一个线程的保护内容固定的保存起来。这种方法的缺点是不灵活,要增补被保护的内容就要重新定义数组,改动操作数据的代码。当然还有更值得推荐的方法:用堆栈来组织内存。
堆栈是一种数据结构,数据如被压入冲锋步枪AK47弹匣的子弹,最先被压进去的子弹,最后才被弹出到枪膛。在计算机内一般设有一个SP寄存器,存放堆栈指针,如果执行PUSH指令,SP先向前推一格,数据被写入SP所指向的内存;如果执行POP指令,数据从SP所指向的内存读出,SP再向后退一格。这种先入后出的结构能够很好的支撑子程序嵌套调用时的数据存储。举一个栗子:
主程序运行时调用子程序A,当前的程序指针PC被压入堆栈(必须留下跳转点的脚印,否则子程序完成后返回不到原来被跳过去的点),接着PC跳转到子程序A中开始运行,在子程序A里面又有调用子程序B,此刻的程序指针PC也被压入堆栈,接着跳到子程序B中,子程序B完成后返回,将堆栈中最近保存的PC弹出到PC寄存器,程序返回到子程序A,子程序A完成后返回,从堆栈弹出最早压入的PC,程序返回到主程序。
系统在线程之间切换时,将当前线程的所有需要保护的内容全部压入堆栈,最后仅需要保存好它的堆栈指针(就像把东西全扔箱子里,只需保管好钥匙)。将下一个线程的堆栈指针取出来,从堆栈弹出所有要恢复的数据,然后返回到新线程运行下一个时间片。
第八部分: 关于线程调度
当一个时间片用完,定时器发生中断后,程序指针跳转到线程切换程序,首先是保护现场。然后是选择下一个线程。那么问题又来了,我们打算怎么来选择下一个线程?这就是所谓的线程调度,操作系统的教课书里叫进程调度。
最原始粗暴的方法就是顺序循环调度,所有线程先来后到排个队,挨个轮着,实行平均主义的制度。在一些实时系统里面,比如μCOS,是按优先级来确定调度权的:永远只选择优先级最高的线程,也就是说,如果线程一直处于最高优先级,那么它就一直占着CPU不放,就不会被抢占。如果要让线程立即得到执行,就必须设置最高优先级。这种方法能够提供手段让用户设计的任务满足实时的要求(所谓实时,就是对响应时间可评估,可控制,不是字面理解上的实时。)
除了优先级方式,还可以设定一种抢占的机制,比如在顺序调度的过程中来一个半道插队方式的抢占,也是一种可以满足实时要求的方法。半路杀出个陈咬金,需要提前设置一个信号通知给调度程序,表示当前有某个线程需要插队抢占。这里要考虑到原有的顺序调度不能被打乱,或者被抢占的线程正好又是被轮到的线程,那么下次调度应继续往后调度。
调度算法有很多,应按工程实际需求来设计,读者不妨也可以设想更有创新意义的方法。
第九部分: 系统效率和延迟
分时总是要付出一些代价的,假如一颗CPU全力运行一个单程序,那么CPU利用率是100%。如果这颗CPU要分时运行两个程序,那么每个程序对CPU的利用率都不足50%,因为切换要耗费CPU时间,切换次数越多越耗费,“切换可是要上税的”。切换次数和时间片的设置相关,一般来说时间片设置范围在1ms到20ms之间,时间片太小,浪费了大量CPU时间在上下文切换上,时间片太大,线程被轮到的时间就长,响应变差。一个线程的运行延迟在分时系统里很难被准确的估算,它与已就绪的线程数量、CPU主频、时间片长短、中断响应等各种因素相关,这些因素动态影响着单个线程的运行延迟。
分时系统中单个线程性能的一种简单的估算方法:假如一个CPU工作在30MHz的主频下,有3个已就绪的线程被平均调度,时间片相等,那么单个线程的运行性能相当于它独占了一个10MHz不到的CPU的效果,相当于这个CPU被拆分成了3个10MHz不到的CPU在同步运行。由于现场就绪与否是动态变化的,因此很难完全准确估算。
总而言之,分时尽管耗费了一些CPU时间,但相对它所带来惊奇效果和并行思维给程序设计带来更多的可能性而言,微不足道。分时就像变魔术一样,能把一颗CPU拆分成N个弱小的CPU同步运行不同的程序。从宏观上,分时能够充分挖掘CPU资源,当一个线程需要等待或有目的的延时,完全可以把CPU时间让给其他线程,而不是白白的空等。
第十部分: 线程的休眠和就绪
当一个线程需要等待或延时,或者索性暂停掉,可以通过状态标记,使调度程序跳过对它的调度,那么这个线程就是被休眠了,或称该线程处于休眠态。反过来,如果线程等着被调度,称该线程处于就绪态。这个功能在调度程序内做一个判断不难实现。
如果要使一个线程休眠,系统可以提供一个函数(子程序)来修改线程的休眠标志(变量)。任何线程,包括线程自己都可以进行休眠操作。所谓想睡就睡,但不是想醒就醒,唤醒一定是其他线程或时钟服务来帮助唤醒你。
除了休眠,还可以设置一个杀死的操作,所谓杀死线程,就是让线程的状态信息恢复初始化,下次再唤醒该线程,相当于要从头开始执行。杀死线程,或线程自杀,可以理解为线程的复位并休眠。
可以设想一种简单的设计场景:用线程1来处理用户输入,线程2、3、4为不同功能的独立任务,线程1可通过用户输入选择唤醒或休眠线程2、3、4来调取不同的功能。对于任务的开发来说,确实提供了与往常不同的手段,用的好,绝对顺手。
第十一部分: 线程之间通信和互斥
感官上,线程是独立运行的,微观上,线程跑着跑着不知道什么时候就咔的被喊停,CPU被强行切换到别的线程去运行。也就是说,线程的运行是随时随刻随地都可能被中断的。
线程和线程之间要通信,一般通过全局变量、全局的数据结构(内存的一块空间)来共享信息。比如线程A往全局变量内写信息,线程B从该变量读信息。以达到让线程A的信息传达给线程B的目的。那么问题又来了,如果线程A对全局变量写了一半,咔,时间片到了,轮到线程B运行,这时候线程B从该变量读到的信息就是一个错误的信息。
先来分析这个问题:
中断是在指令和指令之间响应的,也就是其最小颗粒度是单条指令,如果写变量是一条指令搞定的,由于指令在执行过程中不会被中断,所以不会发生以上情况。单条指令的操作也称为原子操作。
可惜的是,一条指令至多可写一个字。对于没有接触过汇编的程序员来说,一条高级语言的语句很容易被认为是一个原子操作,其实不然,经过编译后,一条高级语言的语句往往都由若干条指令组成。
思考解决问题:
要解决写变量时候不被破坏的问题,就要排斥其他线程对这块变量的操作,或防止在写的过程中被中断。方法有几种:
最简单粗暴的方法是开始写共享变量之前直接把中断给关了,写完以后再开。还有更流氓的做法,就是在写共享变量之前直接让计时器停摆,定住,结束后再继续往前计数。方法虽然简单,但是这么做会让整个系统短暂停滞,明显有副作用。
推荐的方法是让调度程序带上锁功能,如果处于上锁状态,调度程序不执行切换。线程在写共享变量之前先加锁,结束后再解锁。在加锁期间,虽然中断和计时都不受影响,但是其他所有线程都被禁止了,还是有些缺憾。
更好的办法是采用信号量,相当于给共享变量加一个红绿灯。设置红绿灯的操作必须是原子操作,线程在操作共享变量时先查一下是不是绿灯,如果是绿灯就把它变为红灯,然后操作变量,结束后再设为绿灯;如果是红灯就排队等待,直到绿灯来了再进行操作。
名词摘要:在教材中,把对需要互斥的共享变量和设备进行独占操作,叫临界区操作,结束后要退出临界区。
第十二部分: 挂钟和闹铃叫醒服务
线程在运行过程中经常会用到等待相对精确的延时、以及超时判断等和时间相关的操作。传统的单线程程序,经常用最粗暴的空操作指令循环来获得一定的延时,这无可厚非,单线程中一段程序的运行时间能够被准确的估算出来。多线程环境就不同了,一段程序的运行时间长短是不可准确估算的,取决于就绪线程的数量和时间片的长短,这些都是变化的。
需求场景1:线程需要就地等待1分钟。
需求场景2:线程进入一个循环等待某个信号,当时间超过5秒钟判断为超时,退出循环。
以下是一种设计方案:
设计一个系统挂钟,让他一直以单位时间往前走。好比在墙上挂一个钟头,让所有线程都能看时间。为每个线程提供一个闹铃指针和唤醒服务标志,如果某个线程需要唤醒服务,就把闹铃指针拨到目标时间,然后开启唤醒服务。系统挂钟每走一个刻度都要判断当前时间和需要唤醒服务线程的闹铃时间是否一致,一致的话就唤醒这个线程。
当某个线程需要就地等待1分钟就可以这样做:线程先看一下当前的时间,然后把时间往前推1分钟设置一个闹铃,然后告诉系统时钟:闹铃到了记得叫醒我!接着线程就自我休眠了,等着1分钟后系统时钟的唤醒并继续运行。
实践的栗子:启用硬件另一个计时器,每10ms发生一次中断。系统中设置一个16位的字,每过10ms将这个字加一,这个16位字相当于一个量程为65536的挂钟。设置一个数组用于存储每个线程的闹铃时间,设置一个状态字,用于标志线程的唤醒服务。当时钟往前走一个刻度时,同时进行一个唤醒的判断,将当前时间和闹铃时间一致的线程唤醒。
时钟的最小刻度设定为10ms,也就意味着延时设定的最小单位是10ms,那么问题又来了,若需要延时1ms或更小的时间刻度该怎么解决?
如果把时钟刻度直接调至1ms,会导致时钟中断次数增加了10倍,时钟和闹铃的程序段的执行次数也就增加了10倍,这严重消耗了CPU时间,降低了整个系统的性能。如何解决这个问题留给读者去思考。
第十三部分: 公用的函数(或子程序)
如果一个函数仅仅使用堆栈和局部变量,那么这个函数就能天然的被多个线程同时调用。因为堆栈和局部变量(或CPU寄存器)都是受保护的,假使2个线程同时调用一个函数,在线程切换的情况下,虽然都是运行在相同的函数代码段,但各自线程里的局部变量和堆栈内容不同,所以运行结果也是各自分开的。这个函数就像使了分身之术一样,在不同的线程里同时运行着。这种函数也叫作可重入函数。
如果一个函数使用了全局变量,那么假使2个线程同时调用了它,那么在线程A里面所定义的全局变量和线程B里面所定义的全局变量指向同一个位置,就会发生问题。这种不能被多个线程同时调用的函数,叫不可重入函数。
第十四部分: 后记
事实上,分时系统是现代操作系统的核心技术内容,它并不过时。本文所描述的内容涉及到操作系统的部分原理知识,当然描述不够专业,没有准确引用教材中的各种名词和术语,不能完整的讨论操作系统。但笔者希望本文能够成为读者对操作系统知识的一种指引,如果觉得意犹未尽,可以去翻一翻《操作系统》相关教材,看看专业的角度这些概念是怎么被描述的。很多程序员总是以为软件能够解决一切问题,其实不然,有些想法或算法不得不依赖硬件的支持。事实上,操作系统的各种思想也深刻影响了硬件的设计,CPU设计者在设计新的CPU时,充分考虑并纳入了好多为操作系统考虑的事情。
现代操作系统往往鉴于UNIX这样的通用操作系统来描述的,其内容除了讲分时调度以外,还包括内存管理,外存管理(也就是文件系统),系统调用,设备驱动等内容。硬件往往基于个人计算机(intel的架构)来描述,头绪非常多,让学习者觉得课程就像是空中阁楼,可远观而不可触碰。
用单片机做实践是最能摸到计算机本质的路径,试想,在一台裸机上让你的程序最接近硬件的跑动起来,是多么踏实的赶脚(好吧,牵强了)。通用计算机和单片机原理是一样的,只不过通用计算机规模更大,主频更高,为了达到更大的规模,架构起来就要用更多的手段和技巧。
比如在通用计算机内,程序指令并不是固化在ROM或闪存内的,而是存放在低速容量更大的外存(硬盘、软盘、光驱、U盘)中,需要执行程序时,把程序文件成块的复制到内存中,CPU从内存中取指令执行。那么问题来了,程序文件必须被装载在指定内存地址段,(跳转指令中包含了具体的地址),如果地址和指令错位,那么程序跳转将找不到北。要解决这个问题,CPU的设计者从早期的内存分段管理,发展到现在的虚拟地址空间转换,可谓费尽心思。随着程序规模越做越大,当单个程序的长度超出内存的总容量时,问题又来了。为了解决这个问题,又引入了分页和虚拟内存的方法,分页允许程序不用一次性全部调入内存,而是将要被执行部分的页面调入内存,剩余的还是存在硬盘里,当程序执行的指令已经不在内存时,CPU将会发生一个缺页中断,将硬盘中的目标页调入内存,将不用的页调回到硬盘。这些都是内存管理单元MMU做的事情,而MMU基本是硬件实现(做在CPU内部),但它需要软件配合才能用起来。所有的这些机制都是为了解决过程中碰到的问题而提出的解决方案,要搞清楚源头。
讨论操作系统离不开信息安全的话题,黑客和病毒总是存在。理论上,用户程序如果要想搞破坏是非常容易做到的,单靠软件防止黑客或病毒的攻击理论上是做不到的,因为理想状态下所有指令都是公平对等的,你不能用程序指令去阻止用于恶意破坏的程序指令,比如故意改写数据(破坏正确的数据)。所以,计算机安全必须依靠硬件提供的机制,例如CPU设计者将处理器的运行分成多个状态,低权级状态下只能执行部分安全的指令,高权级状态下才可以执行全部指令,用户程序只能运行在低权级,需要进行读写操作时须通过调用系统提供的功能调用来实现,系统是工作在特权级下的,这样就为计算机安全提供了可控手段。
性能是计算机永恒的话题。分时系统上就绪的线程数量越多,单个线程的性能就越低(可视为CPU的性能被线程瓜分了,线程越多,单个线程瓜分到的性能就少)。为了提升计算机性能,除了设置流水线、多级缓存等方式外,单个CPU主要靠提升主频来实现性能的数量级提升。除此之外,引入多个CPU并行工作,通过增加系统的并行度也是提高性能的主要手段。多个CPU使得线程从物理上真正的并行了,一个多核的CPU在同一时刻可以同时运行多个线程,多CPU在硬件组织上又是一件费尽心机的活(不要简单的理解成多插几个CPU就完事那么简单)。超级计算机(用于气象、原子能、航天等领域的测算)往往以集群方式组织起来的高度并行的结构,这也要求软件设计必须要被分割为高度的并行化才能发挥超级计算机的性能。如果一个问题的算法必须至始至终前后串行,无法被分割成多个同步运行的部分,那么这种程序放在超级计算机上运行,纵使这台超算有千万个CPU,程序也只能在其中一个CPU上跑完,剩下的CPU等于闲着没事干。同样的道理,多核CPU体现的也是并行性能,只有在线程数量多的分时系统内才能体现其性能。如果用一个8核的CPU仅仅只运行一个线程,那么剩下7个核心就闲着没事做。
备注:本文一直采用线程来描述并发执行的程序,其实线程是专有名词,有更清晰的概念。操作系统中还有进程的概念,进程也是并发的程序,一个进程还能再分多个线程。在实时操作系统中,线程又被称为任务。
写在最后
通过以上叙述,我想读者一定对分时、多线程、并发执行有了概念,同时也一定留下了一堆的疑问和不解,这可是好现象,进步都是在疑问和不解中开始的。
还有2个应用例程可以从这里下载:http://www.51hei.com/bbs/dpj-59966-1.html
豆豆的打开关上
需求:假设有一个指针,可以作圆周运动,通过在键盘上输入具体的角度,使指针指向该角度。
输入:数字键盘,按3位数字后回车生效。范围0-999,表示圆周角度,对360度取模。
显示:3位段式液晶数码显示。
执行:减速步进电机,按输入的角度作圆周运动。
结构:木结构为主,在电机上安装臂杆,做成类停车杆的结构。可360度自由转动。
技术上要考虑的问题:
1、液晶驱动的问题,拟采用HT1621芯片驱动;
2、归零问题,拟采用12V电池+5V模块的供电方式,设置断电继电器和断电归零程序;
3、转动角度问题,单步判断逼近方法,or距离计算一次到位方法;拟采用前者;
操作模型:
1、打开开关,液晶显示---;臂杆角度为0;(开关处并联的继电器吸合,开关的触点留一组去cpu)
2、输入0-360之间的一个整数表示圆周的绝对角度,每输入一个数字,显示到液晶屏;其他输入作无效处理;
3、回车后,液晶屏内的数字被作为目标值,执行电机转到该角度;
4、转动过程中,可以按暂停键,暂停或继续;
5、转动过程中,可以继续输入新的角度值,按回车后变更新的目标值执行。
6、执行完成后,电机去电待命。
7、开关关闭,cpu检测到关机指令,执行归零程序,完成后,继电器断开。(归零程序不可被中断,归零后须再确认关机指令,若还是关机,则继电器断开。)
实施步骤:
1、整体方案构思;资料消化;
2、硬件草图;
3、电机驱动试验,正反向,速度和角度;
4、液晶显示试验;
5、按键扫描模拟和试验;
6、软件构思;
7、编码和模块测试;
8、最终调试。
全局变量:当前值,目标值,暂停标志,键值,显示数字,ram区,8拍数组;
按键扫描获取键值(任务2)
键值改变数字or启动执行(转换为目标值)or暂停标志变更,无效值;新的数字来了,要标志,用于判断回车键的作用;数字 转换 成ram区;ram区写入HT1621的ram;(任务3)
暂停标志没有时and当前值!=目标值时执行:目标值在左侧还是在右侧:前进一步或后退一步,延时,返回;(任务4)
[一个8拍数组;前进一步,当前值(0-4096变更);后退一步,当前值(0-4096变更);]
不停的查开关标志,若关机,改变目标为0,执行,等待执行完成,再判断是否关机,关断电源继电器。否则返回。(任务0)
- ;kernel: sys51 r0.99
- ;project name: doudou's up & down
- ;designer: ut
- ;version: 1.0
- ;date: 2016/11/1
- ;=============================================================================================
- ; TIME-SHARING SYSTEM FOR MCS51 RELEASE 0.99
- ; UT.ZUZU
- ; COPYRIGHT(2012/5/10-2016/11/--)
- ;=============================================================================================
- ;详细请查看手册
- ;硬件要求
- ;1、52系列兼容的51单片机,内存256字节或以上。本程序在AT89S52运行,24.576MHZ晶振,改变晶振需调整计数器值。晶振频率越高,控制器性能越好。
- ;2、256字节内存中,系统使用了大部分高地址部分,0-47由用户支配,具体请看内存分配说明。
- ;3、一共8个线程:TASK0到TASK2为3个主线程,其余为次线程;主线程对9个寄存器和PSW、AB、DPTR进行保护,并预留堆栈最大嵌套调用为7级;次线程仅保护PSW,AB,R0-R3,最大嵌套调用为2级。
- ;4、系统可以在调度程序中喂看门狗,时间片不可过大,超过4MS不喂狗看门狗发出系统复位信号。看门狗功能可以在配置定义中取消。
- ;5、分时过程通过定时器0进行,其初值定义在T0_VALUE中,目前设置的是5MS时间片。
- ;6、系统时间记录在SYS_TIME变量中,通过定时器2进行,目前设置是10MS加1.
- ;任务操作说明
- ;0、任务的边界应该是循环。不建议跳出边界。尽可能的使用系统提供的调用。
- ;1、不同任务可以调用同一个子程序,注意子程序内受保护的范围。
- ;2、主任务拥有独立的R0-R7、ACC、B、PSW寄存器、DPTR指针。次任务仅保护7个寄存器。
- ;3、任务之间可以通过内存变量来传递信息,注意在写内存时必须占用系统,写完后再释放系统,建议使用加锁和解锁调用。
- ;4、系统初始化后所有任务都是睡眠的,系统会唤醒任务0和任务7,其他任务的唤醒由用户操作。任务7为伺服任务,不建议休眠它或在该任务中使用系统延时调用。(有一种风险:所有任务处于休眠态,会进入待机)
- ;5、系统的性能与晶振频率、唤醒的任务数量、任务占用的时间片有关系。
- ;6、任务有权杀死或休眠任何任务,如果系统所有任务都被杀死或休眠,系统会进入节电POWER_DOWN模式,等待复位激活。
- ;7、系统提供10MS刻度的16位系统时间,由TIMER2来完成。任务可以根据自己需要来完成延时功能,其性能优于普通的空等待DELAY子程序。
- ;8、任务不可以操作TIMER0和TIMER2这两个定时器,需要时,可以使用TIMER1. 建议不要设置为高优先级,可能导致系统时间停走。
- ;注:杀死和休眠的区别:任务被杀死后再次唤醒从头开始运行,任务被休眠后再次唤醒是从原来休眠的地方继续运行(就像暂停)。
- ;用户使用注意:
- ;1.总计8个任务,单个线程是死循环,所有线程并发执行,可以有限调整每个线程的时间片,默认5MS时间片,合理使用可以满足实时要求。
- ;2.任务0到2是主线程,线程内寄存器A和B,R0-R7,DPTR都受保护,子程序嵌套调用最大达8级。
- ;3.任务3到7是次线程,线程内寄存器A和B,R0-R3受保护,子程序嵌套调用最大2级。注意这个限制条件。嵌套调用超限将导致堆栈过界破坏,使系统崩溃。
- ;4.用户只能使用0-59之间的内存空间。
- ;5.用户无需考虑堆栈的分配,禁止任务程序修改堆栈指针SP。
- ;6.中断响应程序中要注意保护现场和恢复现场。
- ;2012-5-22 R0.91 占用系统和释放系统改用停止和开启计时器的方式实现。UNDEBUG
- ;2012-5-23 R0.92 喂狗简化到CPL指令 UNDEBUG
- ;使用时注意:所有中断程序内要用到PSW,A,B,R0~R7,DPTR,必须事先暂存,返回前恢复,注意它们不受保护
- ;2016-11-08 确认BUG和注释。可以作为稳定版。
- ;2016-11-08 R0.99
- ;为了增强实用性,拟重新布局内存,改用堆栈方式保护现场,保证3个主线程,每个线程分配29字节,增加对DPTR的保护,;增加对PSW的保护
- ;可以最大嵌套29-2(PC)-2(DPTR)-10(AB,RG)-1(PSW)=7个CALL;阉割剩余5个次线程,每个线程分配13字节:AB,R0-R3,PC,PSW,最大嵌套2个CALL。
- ;总体256字节的内存:3个主线程:29*3=87;5个次线程:13*5=65;8个线程状态(优先级、SP、闹铃H、闹铃L)=32;
- ;系统变量:10;系统堆栈:14;R0-R7:8个除去。 剩余用户可用的内存区:40字节
- ;真可谓:螺蛳壳里做道场。
- ;20161110
- ;备忘:调度程序要增加加锁功能(不切换,好处是总是能进系统区做一些系统要做的事,比如喂狗)
- ;延时误差太大,最大误差是一个单位(不累计),考虑系统(放在时钟程序内)来负责高精度大跨度的计时,和唤醒服务,需要额外16字节用于8个线程的闹钟记录。
- ;看门狗使用指南:时间片调节的太大就会触发看门狗,应能根据需要关闭看门狗。13位,每一个机器周期+1
- ;时钟要方便配置
- ;注意中断嵌套的影响 ;注意测量 系统服务的时间,及其与中断时间的比重,比重和效率成正比 ;中断响应前后次序的关系分析,用户怎么用中断
- ;唤醒服务要注意的是:必须留一个伺服线程,该线程始终保持就绪(不能使用系统延时)。否则有一种风险:所有任务同时调用系统延时而休眠,调度程序将转入节电模式。要复位或外部中断才能恢复。
- ;20161111 r0.99基本调试完成
- ;0.99版本比较0.92版本特点如下:
- ;1、充分利用堆栈的特点布局内存,使得保护内容的调整变的灵活。
- ;2、不改变8个任务的总数,但集中资源到3个主任务上,增加对psw、dptr寄存器的保护(原来没考虑周全,如psw是必须保护的)。使主任务不再有束缚。
- ;3、取消原有的延时服务,增加系统时钟的定时唤醒服务功能,每个任务可以设置自己的延时时间,然后进入休眠态等待,时间到了系统时钟会唤醒你。
- ;4、改变了杀、休眠、唤醒的方式,采用位表示杀死信号、就绪态、唤醒服务,可以用逻辑的方法快速操作。
- ;5、增加了调度程序的加锁功能,加锁状态下,调度程序不进行任务切换,但继续执行其他系统功能。
- ;6、看门狗、初始时间片可配置。
- ;7、任务7作为伺服线程,可以做一些简单的脉搏动作。伺服线程必须始终就绪,否则有任务全部休眠的风险。
- ;0.99的篇幅反而比0.92下降了7%,除了更加实用以外,显得更加优美。
- ;实际应用达到3个以上时,修复一些潜在的bug之后,可以升为r1.0版本,并出一份《51多任务内核的应用手册》
- ;内存地图规划
- ;0-47 用户
- ;48-63 闹钟数组-每个任务2个字节,用于指示闹钟时间 /30H
- ;64-73 系统变量 /40H
- ;74-87 系统堆栈 7个CALL 包括中断 SP_SYS:73 /49H 再压缩至6个call
- ;88-95 任务优先状态字节 /58H
- ;96-103 任务SP指针 /60H
- ;104-132 任务0堆栈 SP0:103 /67H
- ;133-161 任务1堆栈 SP1:132 /84H
- ;162-190 任务2堆栈 SP2:161 /A1H
- ;191-203 任务3堆栈 SP3:190 /BEH
- ;204-216 任务4堆栈 SP4:203 /CBH
- ;217-229 任务5堆栈 SP5:216 /D8H
- ;230-242 任务6堆栈 SP6:229 /E5H
- ;243-255 任务7堆栈 SP7:242 /F2H
- ;NOTE:OPRATING SFR OR RAM WHERE HAVE THE SAME ADDRESS WITH EACH OTHER WILL BE ATTENTED! CARE <DATASHEET OF AT89S52>
- ;-------------------------------------------------------------------------------
- ;标号定义
- PRI_BYTE EQU 0D8H ;INIT PRIORITY OF EVERY TASK ;时间片;0.5MS:0FCH,1MS:0F8H,2MS:0F0H,5MS:D8H,10MS:B0H,20MS:60H 这里是参考值,初始化时间片请定义在PRI_BYTE
- SYS_SP EQU 4bH ;SYSTEM STACK HEAD
- START_TASK_SP EQU 67H
- TAB_PRI EQU 58H ;基址 见内存分配规划
- TAB_SP EQU 60H ;基址
- TAB_CLK EQU 30H ;BASE
- WDT_PIE EQU 00H ;设置为1E,看门狗开启,其他值则关闭看门狗 NO TEST 13位计时器1FFF复位,合计4MS :意味着启用看门狗时,时间片必须小于4MS,占用系统时也要注意这个问题,建议用加锁功能代替占用系统
- ;系统全局变量定义
- sys_bit_byte equ 2fh ;留给系统的8个标志位 位地址78-7fh
- TMP_A EQU 40H
- TASK_CURT_P EQU 41H ;当前的任务指针
- task_sch_p equ 4ah ;调度任务指针
- CLK_ALARM EQU 42H ;闹钟字节 从左到右每一位依次标志任务0到7的闹铃请求,1为有闹铃请求
- DEAD_SIG EQU 43H ;从左到右每一位依次标志任务0到7的杀死请求,1为有杀死请求
- READY_BYTE EQU 44H ;从左到右每一位依次标志任务0到7的就绪状态,1为就绪
- LOCK_BYTE EQU 45H ;5A表示加锁,其他值表示解锁
- TMP_SP EQU 46H
- WDT_BYTE EQU 47H ;狗盆子
- SYS_TIME_H EQU 48H ;系统时钟高8位
- SYS_TIME_L EQU 49H ;系统时钟低8位
- nouse equ 4bh ;预留
- preempt_bit bit 78h ;是否抢占
- delay_sv_bit bit 79h ;定时器1中断服务 标志 用于小刻度的延时需求
- preempt_task EQU 2eh ;抢占任务号 仅0-7有效,抢占后作废,用于调度程序切换到指定的任务去。
- delay_times equ 2dh ;用于timer1计时刻度的次数
- ;系统晶振:24.576MHZ
- T0_VALUE_H EQU 0D8H ;时间片;0.5MS:0FCH,1MS:0F8H,2MS:0F0H,5MS:D8H,10MS:B0H,20MS:60H 这里是初始赋值,初始化时间片请定义在PRI_BYTE
- T0_VALUE_L EQU 00H
- T2_VALUE_H EQU 0B0H ;时钟刻度 参考上面
- T2_VALUE_L EQU 00H
- T1_VALUE_H EQU 0fcH ;时钟刻度 参考上面 500us
- T1_VALUE_L EQU 00h ;
- ;------------------------------规划程序入口
- ORG 00H
- JMP SYS_START
- ORG 03H
- ;LJMP INT_INT0 ;(INT0)
- RETI
- ORG 0BH
- ;LJMP INT_T0 ;(IF0)
- LJMP SHARE_SYS
- RETI
- ORG 13H
- ;LJMP INT_INT1 ;(INT1)
- RETI
- ORG 1BH
- ;LJMP INT_T1 ;(IF1)
- JMP sys_ms_svrs
- RETI
- ORG 23H
- ;LJMP INT_RTX ;(RI,TI)
- RETI
- ORG 2BH
- ;LJMP INT_T2 ;(IF2)
- JMP SYS_TIME_RUN
- RETI
- ;标记中断返回:如果意外中断,直接返回,不至于跳飞;-)
- ;以下是任务的入口,应和表格中定义一致
- ORG 30H
- LJMP TASK_0
- ORG 38H
- LJMP TASK_1
- ORG 40H
- LJMP TASK_2
- ORG 48H
- LJMP TASK_3
- ORG 50H
- LJMP TASK_4
- ORG 58H
- LJMP TASK_5
- ORG 60H
- LJMP TASK_6
- ORG 68H
- LJMP TASK_7
- ;;开机,从00H跳过来*******************************************
- SYS_START:
- MOV SP,#SYS_SP ;SYSTEM STACK
- MOV WDT_BYTE,#WDT_PIE ;准备好狗粮
- clr preempt_bit
- clr delay_sv_bit
- mov preempt_task,#0
- CALL INIT_RAM ;初始化系统内存
- CALL INIT_TIMER ;初始化定时器
- CALL USER_INIT ;用户初始化程序
- CALL SYS_TIMER_START ;启动系统定时器
-
- MOV DEAD_SIG,#0 ;清空杀手信号
- MOV R1,#0F8H;
- MOV R0,#7;
- CALL SET_PRIBYTE ;任务7时间片设置为1MS
- MOV READY_BYTE,#10000001B ;任务0就绪,任务7当作伺服线程,如果没有一个线程就绪,会进待机
- MOV TASK_CURT_P,#0
- MOV TASK_sch_P,#0
- MOV TAB_PRI,#PRI_BYTE ;任务正常运行的要素:不被杀,就绪,优先级(时间片)不要太长(看门狗会叫),SP状态
- MOV SP,#START_TASK_SP
- LJMP TASK_0 ;进入任务0,启动分时,START SHARE
- ;;上面用到的子程序:任务1到7依次初始化各自内存空间-----------------------------
- INIT_RAM:
- MOV R0,#7 ;以此对各任务进行内存初始化赋值
- ITR0:
- CALL TASKRAM_INIT
- DJNZ R0,ITR0 ;TASK0任务作为系统启动的入口,可以不用初始,其内容会在第一个时间片中断后调度程序会给予。
- ;CALL TASKRAM_INIT ;JUST FOR TEST TASK0 RAM INIT
- RET
- ;以下表格用于初始化内存用
- TAB_1:
- DB 067H,084H,0A1H,0BEH,0CBH,0D8H,0E5H,0F2H,00H ;任务栈顶地址
- TAB_2:
- DB 030H,038H,040H,048H,050H,058H,060H,068H,00H ;任务入口地址 和ORG 30H.. 对应
- ;上面用到的子程序:开机初始化任务内存操作:1、根据任务号查表得栈顶位置、入口位置;2、在栈顶压入:入口、现场;3、将SP存到SP_I; 4、清就绪态
- ;初始化任务内存分主次 ;任务号先存R0
- TASKRAM_INIT:
- MOV A,R0
- MOV DPTR,#TAB_1
- MOVC A,@A+DPTR ;查表得初始SP
- MOV TMP_SP,SP
- MOV SP,A ;开始压栈
- MOV A,R0
- MOV DPTR,#TAB_2
- MOVC A,@A+DPTR ;查表得初始PC
-
- MOV 02H,A
- PUSH 02H ;PUSH PC_L
- MOV 02H,#0
- PUSH 02H ;PUSH PC_H PC是16位的
- PUSH 02H ;PSW,AB,R0-R7,DPTR
- PUSH 02H
- PUSH 02H
- PUSH 02H
- PUSH 02H
- PUSH 02H
- PUSH 02H
- ;区分主次任务
- ;任务号大于2则跳过以下步骤
- CLR C
- MOV A,#2
- SUBB A,R0
- JC TKI00
- PUSH 02H ;R4 R5 R6 R7 DPL DPH
- PUSH 02H
- PUSH 02H
- PUSH 02H
- PUSH 02H
- PUSH 02H
- TKI00:
- ;保存SP到数组,SP--> SP_I
- MOV A,#TAB_SP
- ADD A,R0
- MOV R1,A ;这个是指针变量,指向当前SP的存放地址
- MOV @R1,SP ;记录SP
- MOV SP,TMP_SP ;压栈完成,恢复SP
- ;优先级字节赋值初始值
- MOV A,#TAB_PRI
- ADD A,R0
- MOV R1,A
- MOV @R1,#PRI_BYTE
- ;清就绪态
- CALL CLR_READY_BIT
- RET
- ;子程序:以下初始化系统定时器 TIMER2 DEBUGED 120516 --------------------------
- INIT_TIMER:
- ;TIMER2 SETUP
- MOV 0C8H,#00H ;MOV T2CON,#00H
- MOV 0C9H,#00H ;MOV T2MOD,#00H
- MOV 0CCH,#T2_VALUE_L ;MOV TL2,#T2_VALUE_L
- MOV 0CDH,#T2_VALUE_H ;MOV TH2,#T2_VALUE_H
- MOV 0CAH,0CCH ;MOV RCAP2L,TL2
- MOV 0CBH,0CDH ;MOV RCAP2H,TH2
- ;TIMER0 SETUP
- ANL 88H,#11101111B;TCON CLR TR0 : STOP TIMER0
- ANL 89H,#11110000B ;TMOD(SET TIMER0)
- ORL 89H,#00000001B ;TMOD(SET TIMER0) MODE:01 16BIT COUNT UP
- MOV 8AH,#T0_VALUE_L ;TL0
- MOV 8CH,#T0_VALUE_H ;TH0
- ;TIMER1 SETUP
- ANL 88H,#10111111B;TCON CLR TR0 : STOP TIMER1
- ANL 89H,#00001111B ;TMOD(SET TIMER0)
- ORL 89H,#00010000B ;TMOD(SET TIMER0)MODE:01 16BIT COUNT UP MODE:02 8BIT autoCOUNT UP
- MOV 8bH,#T1_VALUE_L ;TL0
- MOV 8dH,#T1_VALUE_H ;TH0
- RET
- ;子程序:启动TIMER0和TIMER2 ;DEBUGED 120516---------------------------------
- SYS_TIMER_START:
- MOV SYS_TIME_H,#00
- MOV SYS_TIME_L,#00
- MOV IP,#00000000B ;SET PRIORITY
- MOV IE,#10101010B ;SETB EA ;SETB ET2 ;SETB ET0 ET1 TO ENABLE INTERUPT OF TIMER2 AND TIMER0 AND TIMER1
- ORL 88H,#01010000B ;TCON SETB TR0,TR1 START TIMER0 TIMER1
- ORL 0C8H,#00000100B ;ORL T2CON,#00000100B ;SETB TR2 TO START TIMER2
- RET
- ;;系统时间处理,在TIMER2中断后跳进来
- ;系统时间处理有2大内容:1、比较各闹钟的目标时间是否到达,到达并且该任务有唤醒服务,就执行唤醒;2、时钟刻度加一。
- SYS_TIME_RUN:
- CLR EA
- MOV TMP_SP,SP ;保存A 保护现场
- MOV SP,#SYS_SP ;--------------------------------界面,以下系统区
- MOV TMP_A,PSW
- PUSH TMP_A
- MOV TMP_A,A ;PUSH A
- PUSH TMP_A
- MOV TMP_A,B ;PUSH B
- PUSH TMP_A
- PUSH 00H ;PUSH R0
- PUSH 01H ;PUSH R1
- PUSH 02H ;PUSH R2
- PUSH 03H ;PUSH R3
- ;处理CLK_ALARM字节、TAB_CLK数组
- MOV A,CLK_ALARM
- JZ STR00 ;没有服务时跳过
- MOV R0,#TAB_CLK
- MOV R3,SYS_TIME_L
- CALL PROC_CMP_BYTE ;低8位比较
- MOV R1,A
- MOV R0,#TAB_CLK
- DEC R0
- MOV R3,SYS_TIME_H
- CALL PROC_CMP_BYTE ;高8位比较,对比结果保存到A 1表示相等 0表示不等
- ANL A,R1 ;H和L的比较结果合并
- MOV R1,A
- MOV A,CLK_ALARM
- ANL A,R1 ;与唤醒服务合并
- ORL READY_BYTE,A ;执行唤醒
- MOV A,R1
- CPL A
- ANL CLK_ALARM,A ;清唤醒标志,表示完成唤醒
- STR00:
- ;16位系统时钟+1 放在后面处理,延时00时可立即生效
- MOV A,SYS_TIME_L
- INC SYS_TIME_L
- INC A
- JNZ $+4
- INC SYS_TIME_H
- ANL 0C8H,#01111111B ;ANL T2CON,#01111111B ;CLEAR TF2 清TIMER2中断标志
- POP 03H ;POP R3
- POP 02H ;POP R2
- POP 01H ;POP R1
- POP 00H ;POP R0
- POP TMP_A
- MOV B,TMP_A ;POP B
- POP TMP_A
- MOV A,TMP_A ;POP A ;恢复现场
- POP TMP_A
- MOV PSW,TMP_A
- MOV SP,TMP_SP ;---------------------------------------------界面,以上系统区
- SETB EA
- RETI
- ;;;;;;;中断返回
- ;用于刻度为500us,次数255的等待服务。只提供一个线程使用,出于系统消耗的考虑,500us中断必须篇幅足够小。
- ;定时器1中断服务:500us中断一次,无服务直接返回。有服务:次数(time_us字节)为0则让waiting_task_p任务抢占(标志完成)。不为0时,减一。
- ;系统需要用一个字节的标志位2fh,用户要避开。
- ;preempt_bit bit 78h ;是否抢占
- ;delay_sv_bit bit 79h ;定时器1中断服务 标志 用于小刻度的延时需求
- ;preempt_task EQU 3fh ;抢占任务号 仅0-7有效,抢占后作废,用于调度程序切换到指定的任务去。
- ;delay_times equ 3eh ;用于timer1计时刻度的次数
- sys_ms_svrs:
- jb delay_sv_bit,smsv0 ;无服务直接返回
- MOV 8bH,#T1_VALUE_L ;TL1
- MOV 8dH,#T1_VALUE_H ;TH1
- reti
- smsv0:
- ;保护现场
- mov tmp_a,a
-
- ;查delay_times次数:等于0时,置抢占任务preempt_bit
- mov a,delay_times
- jnz smsv1
- setb preempt_bit ;置抢占位
- clr delay_sv_bit ;清服务位
- ORL 88H,#00100000B ;SETB TF0 ;SOFT INTERUPT TIMER0 TCON-->:TF1:TR1:TF0:TR0:IE1:IT1:IE0:IT0:
- ;中断不嵌套,本次中断返回后进入系统中断
- MOV 8bH,#T1_VALUE_L ;TL1
- MOV 8dH,#T1_VALUE_H ;TH1
- mov tmp_a,a
- reti
- ;次数减一
- smsv1: dec delay_times
- ;恢复现场
- mov a,tmp_a
- MOV 8bH,#T1_VALUE_L ;TL1
- MOV 8dH,#T1_VALUE_H ;TH1
- reti
- ;;上面要用到
- ;;子程序:基地址存R0(间隔1个字节的8个数组),与系统时钟(H或L字节)R3进行比较,8次,结果存放在ACC对应的位里面,1表示相等
- PROC_CMP_BYTE:
- MOV B,#0
- MOV R2,#8
- MOV A,#15
- ADD A,R0
- MOV R0,A
- PCB00:
- MOV A,@R0
- CJNE A,03H,PCB01
- MOV A,B
- SETB C
- RRC A
- JMP PCB02
- PCB01:
- MOV A,B
- CLR C
- RRC A
- PCB02: MOV B,A
- DEC R0
- DEC R0 ;间隔1字节的指针,从右到左
-
- DJNZ R2,PCB00
- MOV A,B
- RET
- ;;调度程序,在timer0中断后跳过来。
- ;;调度程序的内容:1、保护现场;2、存sp;3、喂狗、执行任务死刑、判断加锁;4、切换下一个就绪的任务指针;5、调取新任务的时间片设置到定时器;6、调取新sp;7、恢复现场;8、返回到新任务。
- SHARE_SYS: ;保护现场先
- MOV TMP_A,PSW ;PUSH PSW
- PUSH TMP_A
- MOV TMP_A,A ;PUSH A
- PUSH TMP_A
- MOV TMP_A,B ;PUSH B
- PUSH TMP_A
- PUSH 00H ;PUSH R0
- PUSH 01H ;PUSH R1
- PUSH 02H ;PUSH R2
- PUSH 03H ;PUSH R3
- ;区分主次任务
- ;TASK_CURT_P 大于2则跳过以下步骤
- CLR C
- MOV A,#2
- SUBB A,TASK_CURT_P
- JC SS00
- PUSH 04H ;PUSH R4
- PUSH 05H ;PUSH R5
- PUSH 06H ;PUSH R6
- PUSH 07H ;PUSH R7
- PUSH DPL
- PUSH DPH
- SS00: ;存SP到数组SP
- MOV A,#TAB_SP
- ADD A,TASK_CURT_P
- MOV R0,A ;这个是指针变量,指向当前SP的存放地址
- MOV @R0,SP ;记录SP
-
- ;切换SP,以下进入系统区----------------------------------------------------------------INTERFACE
- MOV SP,#SYS_SP ;SP指向系统SP
- CALL WDT ;喂狗
- CALL KILL_TASK ;根据DEAD_SIG字节,执行任务的死刑 ;-*
- ;是否上锁,如果上锁 LOCK_BYTE= 5AH 则不执行任务切换
- MOV A,LOCK_BYTE
- CJNE A,#5AH,SS04
- JMP SS05
- SS04:
- mov r1,task_sch_p ;暂存
- MOV R6,#10
- SELECT_P: ;选择下一个任务
- DJNZ R6,SS01 ;选择次数计时,如果连续选择超10次就得进节电模式了
- MOV P1,#0FFH
- ORL 87H,#02H ;INTO POWER-DOWN MODE
- LJMP SYS_START ;醒来的话就重新开机咯
- ;切换任务指针(0-7) 全局变量TASK_sch_P 任务指针,仅此进行写操作
- SS01:
- INC TASK_sch_P
- MOV R0,TASK_sch_P
- CJNE R0,#8,SS02 ;超限
- MOV TASK_sch_P,#0
- ;判就绪位,不在就绪态就跳回 SELECT_P,重复以上步骤
- SS02:
- MOV R0,TASK_sch_P
- CALL GET_READY_BIT
- JNC SELECT_P
- ;调度结束,新的指针在task_sch_p
- ;是否有抢占信号
- jnb preempt_bit,ss06
- mov a,preempt_task
- clr c
- subb a,#8
- jnc ss06 ;抢占任务号无效(大于7)
- mov a,preempt_task
- cjne a,task_sch_p,ss07 ;如果抢占任务和本次应该调度的任务相同,则下一次不要再调这个任务了。(本次调度生效,否则退回上一次调度指针)。
- jmp ss08
- ss07:
- mov task_sch_p,r1 ;恢复调度指针
- ss08:
- mov r0,preempt_task
- call set_ready_bit ;抢占任务就绪位
- mov task_curt_p,preempt_task ;直接指定任务号,切换
- clr preempt_bit
- jmp ss05
- ss06: mov task_curt_p,task_sch_p ;调度盘指针 确定调度指针和实际任务指针分离,解决抢占后调度不公平问题
- SS05:
- ;取优先字节地址
- MOV A,#TAB_PRI
- ADD A,TASK_CURT_P
- MOV R0,A
- ;时间片赋值 ;RESET THE TIMER0
- MOV 8AH,#T0_VALUE_L ;TL0
- MOV 8CH,@R0 ;TH0 ;MOV TH0,@R0;选中后,优先级设置到时间片
- ;取SP_I --> SP
- MOV A,#TAB_SP
- ADD A,TASK_CURT_P
- MOV R0,A
- MOV SP,@R0
- ;以下退出系统态,回到新的任务态,恢复现场-------------------------------------------INTERFACE
- ;区分主次任务
- ;TASK_CURT_P 大于2则跳过以下步骤
- CLR C
- MOV A,#2
- SUBB A,TASK_CURT_P
- JC SS03
- POP DPH
- POP DPL
- POP 07H ;POP R7
- POP 06H ;POP R6
- POP 05H ;POP R5
- POP 04H ;POP R4
- SS03:
- POP 03H ;POP R3
- POP 02H ;POP R2
- POP 01H ;POP R1
- POP 00H ;POP R0
- POP TMP_A
- MOV B,TMP_A ;POP B
- POP TMP_A
- MOV A,TMP_A ;POP A
- POP TMP_A
- MOV PSW,TMP_A
- ;此时堆栈内当前应是中断返回时的PC值,RETI可以返回。
- ;ANL 88H,#11011111B ;CLR TF0 ;SOFT INTERUPT TIMER0 TCON-->:TF1:TR1:TF0:TR0:IE1:IT1:IE0:IT0:
- RETI
- ;;子程序:根据被杀任务信号字节8位,从左到右每一位代表任务0-7是否要杀掉,1为杀死,0为不杀 来执行死刑
- ;执行内容:将该任务的内存区重新初始化(初始化后为休眠态),下次再轮到时,从头开始。
- KILL_TASK:
- MOV R3,#8
- KTA00:
- MOV A,DEAD_SIG
- RRC A
- MOV DEAD_SIG,A
- JNC KTA01
- MOV a,R3
- DEC a
- mov r0,a
- CALL TASKRAM_INIT
- KTA01:
- DJNZ R3,KTA00
- MOV DEAD_SIG,#0 ;清掉所有DEAD信息
- RET
- ;;喂狗子程序
- WDT:
- MOV 0A6H,WDT_BYTE ;MOV WDTRST,WDT_BYTE WDT_BYTE= 1EH OR E1H
- MOV A,WDT_BYTE
- CPL A ;取反
- MOV WDT_BYTE,A
- RET
- ;;获取就绪位:在调度程序中用到
- GET_READY_BIT: ;任务号R0, 执行结束后,结果的位在C
- MOV B,R0
- INC B
- MOV A,READY_BYTE
- GRB00: RLC A
- DJNZ B,GRB00
- RET
- ;提供的系统调用
- ;-----------------------------------------------------------------------------------------------
- ;子程序:修改任务的时间片,任务号在R0,优先字节(时间片)在R1,将优先字节写入到数组
- SET_PRIBYTE:
- MOV A,#TAB_PRI
- ADD A,R0
- MOV R0,A
- MOV A,R1
- MOV @R0,A
- RET
- ;子程序:回到调度程序 DEBUGED 120516
- WAITING:
- NOP ;留给中断响应的间隙
- ORL 88H,#00100000B ;SETB TF0 ;SOFT INTERUPT TIMER0 TCON-->:TF1:TR1:TF0:TR0:IE1:IT1:IE0:IT0:
- RET
- ;子程序:占用系统,任务在读写的时候不允许系统中断,和frees配套使用
- OCCUPY:
- ;ORL IE,#00000010B ;ENABLE INTERRUPT OF TIMER0 方法1:关闭timer0的中断
- ANL 88H,#11101111B ;TCON CLR TR0, STOP TIMER0 方法2:关闭timer0的计时
- RET
- ;子程序:释放系统 和occupy配套使用,任务占用系统后应及时释放
- FREES:
- ;ANL IE,#11111101B ;DISABLE INTERUPT OF TIMER0
- ORL 88H,#00010000B ;TCON SETB TR0, START TIMER0
- RET
- ;注意:occupy和free要配套用,他们之间就是临界区,然而occupy会导致不进调度程序,不建议使用。建议用加锁和解锁来实现临界区的操作。
- LOCK_SYS:
- MOV LOCK_BYTE,#5AH
- RET
- UNLOCK_SYS:
- MOV LOCK_BYTE,#0EEH
- RET
- ;精确的系统延时-将16位的延时数,每一位为一个时刻,存放在DPTR,计算目标时间,设置唤醒任务,休眠自己。等待系统时钟在时间到了再唤醒你,误差为一个调度周期。
- ;任务号为全局变量指针TASK_CURT_P
- ;延时步骤:1、将dptr个刻度和当前时间相加得到目标时间,存入到闹铃数组当前任务位置;2、设置本任务的唤醒服务位,当目标时间到达,系统时钟会唤醒你;3、进入休眠态;
- DELAY_SYS:
- ;计算目标16位目标值,存放在TAB_CLK对应的位置
- MOV A,SYS_TIME_L
- ADD A,DPL
- MOV DPL,A
- MOV A,SYS_TIME_H
- ADDC A,DPH ;带进位
- MOV DPH,A
- MOV A,#TAB_CLK
- MOV R0,TASK_CURT_P
-
- ADD A,R0
- ADD A,R0
- ;双字节指针
- MOV R0,A
- MOV @R0,DPH
- INC R0
- MOV @R0,DPL
- ;设置唤醒位,在CLK_ALARM字节,8个位标志8个任务的唤醒服务,1为有服务。
- MOV R0,TASK_CURT_P
- CALL SET_ALARM_BIT
- ;清就绪位,在READY_BYTE
- MOV R0,TASK_CURT_P
- CALL CLR_READY_BIT
- ;回调度
- ORL 88H,#00100000B
- RET
- ;上面用到的子程序:设置唤醒服务的位,任务号预先放在R0
- SET_ALARM_BIT:
- MOV B,R0
- MOV A,#10000000B
- INC B ;最小任务号为1
- SAB00: DJNZ B,SAB01 ;循环左移
- ORL CLK_ALARM,A
- JMP SAB02
- SAB01: RR A
- JMP SAB00
- SAB02:
- RET
- ;子程序:任务自杀
- KILL_SELF:
- MOV B,TASK_CURT_P
- MOV A,#10000000B
- INC B ;最小任务号为1
- KSF00: DJNZ B,KSF01 ;循环左移
- ORL DEAD_SIG,A
- JMP KSF02
- KSF01: RR A
- JMP KSF00
- KSF02:
- RET
- ;子程序:杀死,任务号存R0
- KILL_TASK_CALL:
- MOV B,R0
- MOV A,#10000000B
- INC B ;最小任务号为1
- KTSK00: DJNZ B,KTSK01 ;循环左移
- ORL DEAD_SIG,A
- JMP KTSK02
- KTSK01: RR A
- JMP KTSK00
- KTSK02:
- RET
- ;子程序:清就绪位,就绪态字节 8位 从左到右每一位分别代表任务0-7是否就绪,1为就绪,0为休眠
- ;任务号存在R0
- CLR_READY_BIT:
- MOV B,R0
- MOV A,#01111111B
- INC B ;最小任务号为1
- CRB00: DJNZ B,CRB01 ;循环左移
- ANL READY_BYTE,A
- JMP CRB02
- CRB01: RR A
- JMP CRB00
- CRB02:
- RET
- ;子程序:置就绪位,上面的相反操作 ;任务号存在R0
- SET_READY_BIT:
- MOV B,R0
- MOV A,#10000000B
- INC B ;最小任务号为1
- SRB00: DJNZ B,SRB01 ;循环左移
- ORL READY_BYTE,A
- JMP SRB02
- SRB01: RR A
- JMP SRB00
- SRB02:
- RET
- ;子程序:小刻度的延时功能(通过定时器1和抢占机制完成),次数放在r0
- delay_sys_us:
- mov delay_times,r0
- mov preempt_task,task_curt_p ;占用的任务号预存
- mov r0,task_curt_p
- call clr_ready_bit ;延时期间要休眠
- setb delay_sv_bit ;开启延时服务
- ORL 88H,#00100000B ;SETB TF0 ;SOFT INTERUPT TIMER0 TCON-->:TF1:TR1:TF0:TR0:IE1:IT1:IE0:IT0:
- nop
- nop ;中断响应
- ret
- ;;;;;;;;;;;;;;;;;;伺服线程:任务7
- task_7:
- mov r2,#77h
- mov r3,#77h
- tk700:
- mov r0,#01h
- mov r1,#0eh
- call delay16b
- setb p1.7 ;led 熄灭100ms
- mov r0,#0dh
- mov r1,#0bdh
- call delay16b
- clr p1.7 ;led 点亮900ms
- jmp tk700
- jmp task_7
- ;r0:h r1:l 16位数的nop延时 一个周期为10.25us(全速) ,高8位放在r0,低8位放在r1
- delay16b:
- dll00:
- mov a,r1
- clr c
- subb a,#1
- mov r1,a
- mov a,r0
- subb a,#0 ;进位 16位数减一
- mov r0,a
- div ab ;纯粹为了延时
- div ab
- nop
- nop
- mov a,r0
- orl a,r1
- jnz dll00
- ret
- ;注意:以上仅做了关于杀死、休眠、唤醒任务的调用,仅为了使用方便,实际使用时推荐使用更高效的逻辑方法:
- ;比如:要杀任务3和6,可以将dead_sig ORL 00010010 即可
- ;要休眠任务2和4,可以将ready_byte ANL 11010111 即可
- ;要唤醒任务1和7,可以将ready_byte ORL 10000001 即可
- ;SYSTEM END==============================================================line number of r0.92 is 750
- ;User's code
- ;*********************************************************************
- ;project name: 360度指示器 豆豆的打开关上
- ;designer: ut
- ;version: 1.0
- ;date: 16-11-16
- ;*********************************************************************
- ;用户在此定义自己的变量地址 及 标号 0-46d 0-2cH :一共45个字节,除去0-7,可以用8-2cH这37个字节
- ;需求:
- ;1\ 16位数字键,0-9 abcd * #
- ; 2\ 3位段码LCD显示
- ; 3\ 按下数字,插入到LCD的左侧。
- ; 4\ 按下D(回车),LCD的数字作为角度值,电机转动到指定角度,三位数字范围0-999,对360取模,执行完毕后再输入数字时LCD清零再插入。
- ; 5\ 按下A电机向上微调,按下B电机向下微调
- ; 6\ 按下C,LCD清零。
- ; 7\ 关机时,壁板归位至270度位置,再切断电源
- ; 8\ 开机时,壁板开启到0度位置。
- ;task0: 主线程,开机初始化,启动其他任务,主循环是不停的取键、取键值成功后处理键值。
- ;task1: 电机移动,始终试图将当前位置靠近目标位置,直到达到为止。
- ;task2: 按键扫描,转换为键值存入到缓冲区。
- ;处理键值:0-9,执行循环插入 BCD 数组,如果有清屏标志,则先清屏再插入。
- ;处理键值:A-B, 执行电机走12拍,约1度,A为正方向,B为反方向。
- ;处理键值:C, 将BCD全部设置为0
- ;处理键值:D,将BCD转成一个16位数,再mod360运算,将结果写到电机目标值。设置清屏标志。
- ;关于显示:在主线程的循环中,涉及到BCD变化时,才会触发显示,显示过程:将BCD码转换成段码,将段码输出到HT1621驱动器。
- WR_1621 BIT P3.6
- ;RD_1621 BIT P3.7
- DATA_1621 BIT P3.5
- CS_1621 BIT P3.7
- BIAS EQU 52H; //0B1000 0101 0010 1/3DUTY 4COM
- SYSDIS EQU 0; //0B1000 0000 0000 关振系统荡器和LCD偏压发生器
- SYSEN EQU 02H; //0B1000 0000 0010 打开系统振荡器
- LCDOFF EQU 04H; //0B1000 0000 0100 关LCD偏压
- LCDON EQU 06H; //0B1000 0000 0110 打开LCD偏压
- XTAL EQU 28H; //0B1000 0010 1000 外部接时钟
- RC256 EQU 30H; //0B1000 0011 0000 内部时钟
- TONEON EQU 12H; //0B1000 0001 0010 打开声音输出
- TONEOFF EQU 10H; //0B1000 0001 0000 关闭声音输出
- WDTDIS EQU 0AH; //0B1000 0000 1010 禁止看门狗
- ;;;内存变量,范围(8-2cH)
- nouse1 equ 20h ;预留给可寻址的位
- nouse2 equ 21h
- pool_key equ 21h ;22,23,24;类似堆栈指针,前推一位
- p_key equ 25h
- pool_bcd equ 26h ;26,27,28;存放bcd码 循环覆盖
- p_bcd equ 29h ;存放bcd指针,0-2 循环
- pool_print equ 8h ;8 9 10 存放3位数码管的段码
- key_value equ 0bh
- p_moto_H equ 0ch
- p_moto_L equ 0dh
- targ_moto_H equ 0eh
- targ_moto_L equ 0fh
- p_step equ 10h ;表的指针
- p_deg equ 11h
- ;定义位
- key_catched bit 00h ;获取到一个按键后置位
- bcd_ready bit 01h ;bcd插入新值时置位
- moto_dir bit 02h ;电机方向
- tmp_dir bit 03h
- reset_bcd bit 04h ;重置bcd
- ;;用户在这里写初始化程序,在系统开机初始化时,被调用,注意:此处不可进行系统功能的调用
- user_init:
- mov p1,#0f0h ;关电机
- mov 08h,#0 ;段码区
- mov 09h,#0
- mov 0ah,#0
- mov 26h,#0 ;bcd区
- mov 27h,#0
- mov 28h,#0
-
- mov p_key,#0 ;表示无按键
- mov p_bcd,#0
- mov p_step,#0
- mov p_deg,#0
- mov p_moto_h,#0
- mov p_moto_l,#0
- mov targ_moto_h,#0
- mov targ_moto_l,#0
- clr reset_BCD
- clr p2.2 ;开启关机继电器
-
- ret
-
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;;;;;;;;任务0 主线程 ;
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;主线程,1、从键盘缓冲池取一个按键; 2、处理该按键(数字键插入bcd区)其他键(步数增减、清零、回车)3、bcd转换为段码,隐去尾部0
- ;4、段码输出
- ;;;其他按键处理:步数增加一个幅度,不改变参数,步数减少一个幅度,不改变参数,bcd归零,bcd转换为目标值;;;;;;;;;;
- ;;-----------------------------------任务0
- task_0:
- CALL Ht1621_Init;() ; 上电初始化LCD驱动芯片
- mov dptr,#tab_ht1621
- mov r3,#0
- mov r5,#16
- call Ht1621WrAllData;(0,Ht1621Tab,16) ;清除1621寄存器数据,清屏
- mov dptr,#tab_ht1621_dou
- mov r3,#0
- mov r5,#3
- call Ht1621WrAllData;(0,Ht1621Tab,3);显示 ;logo
- ;LCD的扫描是不需要延时的
- orl ready_byte,#01100000b ;开启任务2:键盘扫描程序 ;开启任务1:电机驱动:实际值逼近目标值
-
- ;臂板垂直向下270度为初始态(压缩状态,方便包装和移动)在电气驱动里面初始化。
- tsk0tv0:
- jb p2.1,tsk04 ;关机信号判断
- clr key_catched
- call catch_a_key
- jnb key_catched,tsk0tv0 ;没有获取到键值
- clr bcd_ready
- call key_proc
- jnb bcd_ready,tsk0tv0 ;不涉及到bcd变化
-
- call bcd2print ;bcd 转换为段码
- call hide_zero ;消隐尾部的0
- mov r3,#0
- mov r4,#pool_print
- mov r5,#3
- ; mov lock_byte,#5ah ;加锁
- call print ;输出到LCD
- ; mov lock_byte,#11h
-
- jmp tsk0tv0
- ;;关机流程,回到270度位置
- tsk04:
- mov dptr,#tab_ht1621_off
- mov r3,#0
- mov r5,#3
- call Ht1621WrAllData;(0,Ht1621Tab,3);显示off
- mov targ_moto_l,#0eh
- mov targ_moto_h,#01h ;电机目标为270
- ;等待电机到点
- tsk040: mov a,P_moto_l
- cjne a,targ_moto_l,tsk040
- mov a,p_moto_h
- cjne a,targ_moto_h,tsk040
- jnb p2.1,tsk0tv0 ;最后确认是否关机
- setb p2.2 ;关机
- ORL 87H,#02H ;INTO POWER-DOWN MODE
- LJMP SYS_START ;醒来的话就重新开机咯
- ;步骤:1、指针为0则无按键值,2、取键值,指针-1,(临界区);3处理键值;
- ;print_LCD: 函数 将3个字节的内容显示到LCD 0-f 都能显示 步奏:1、字节数转换到 段码字节 2发送给ht1621
- ;如何循环显示,三个字节要构成单向环,abcabcabc,始终显示指针后3位数字,加入新的字节时指针往前推。
- ;将上面的三个字节,转为一个整数<=999,占2个字节。
- ;设为目标值
- ;当前值与目标值比较,不等于则靠近,等于则关闭。
- ;关机线程
- ;正或反,步数n个。函数
- jmp task_0
- ;--------task0 end----------
- ;任务0子程序:从pool_key,p_key取一个键值,存放在key_value,并置位key_catched
- catch_a_key:
- ;clr key_catched
- mov a,p_key
- jnz cak00
- ret ;p_key 为0表示键值池空
- cak00:
- ;临界区:取一个键值
- mov lock_byte,#5ah
- mov a,#pool_key
- add a,p_key
- mov r0,a
- mov a,@r0
- dec p_key
- mov lock_byte,#11;临界区
- mov key_value,a ;键值
- setb key_catched
- ret
- ;任务0子程序:处理当前键值,小于10放到循环的bcd池里(pool_bcd,p_bcd),大于10则调用相关功能
- key_proc:
- mov a,key_value
- clr c
- subb a,#10
- jc kpc00
- mov a,key_value
- cjne a,#0ah,kpc01
- ;0a键功能
- call up_a_bit
- kpc01: cjne a,#0bh,kpc02
- ;0b键功能
- call down_a_bit
- kpc02: cjne a,#0ch,kpc03
- ;0c键功能
- call clr_bcd
- kpc03: cjne a,#0dh,kpc04
- ;0d键功能
- setb reset_BCD ;回车后前面的数据在下次按键输入后,清掉
- call set_target
- ret
- kpc00: call keyv2bcd
- kpc04:
- ret
- ;;;;;;第二层子程序
- clr_bcd:
- mov r0,#pool_bcd
- mov @r0,#0
- inc r0
- mov @r0,#0
- inc r0
- mov @r0,#0
- setb bcd_ready
- ret
- set_target: ;将bcd里的3位数字转换16位数字,并存入到targ_moto_L, targ_moto_H 中
-
- mov r1,p_bcd ;0-2范围
- mov r3,#0
- mov r4,#0
- ;;个位数
- mov a,#pool_bcd
- add a,r1
-
- mov r0,a
- mov a,@r0 ; 取到bcd值 从个位数查起
- mov r4,a ;r3存H,r4存L 100* + 10* + L
- ;;;重复
- dec r1
- mov a,r1
- cjne a,#0ffh,ste00;
- mov r1,#2 ;过界处理
- ste00:
- mov a,#pool_bcd
- add a,r1
- mov r0,a
- mov a,@r0
- mov b,#10
- mul ab ;十位数
- clr c ;16位加法
- addc a,r4
- mov r4,a
- mov a,b
- addc a,r3
- mov r3,a
- ;;;重复以上
- dec r1
- mov a,r1
- cjne a,#0ffh,ste01;
- mov r1,#2 ;过界处理
- ste01:
- mov a,#pool_bcd
- add a,r1
- mov r0,a
- mov a,@r0
- mov b,#100
- mul ab ;百位数
- clr c ;16位加法
- addc a,r4
- mov r4,a
- mov a,b
- addc a,r3
- mov r3,a
- call targ_mod360
- mov lock_byte,#5ah
- mov targ_moto_L,r4
- mov targ_moto_H,r3
- mov lock_byte,#11h
- ret
- ;目标值在r3,r4(HL),结果调整后还是在R3 R4
- targ_mod360: ;输入的bcd值(0-999)转换为0-360度范围,与360取模:求余
- mov dph,r3 ;暂存
- mov dpl,r4
- mov a,r4 ;360d=0168h
- clr c
- subb a,#68h
- mov r4,a
- mov a,r3
- subb a,#01h
- mov r3,a
- jc tmd00;表示过头了
- jmp targ_mod360
- tmd00:
- mov r3,dph
- mov r4,dpl
- ret
- up_a_bit: ;电机向上微调一个距离 ;临界区处理,禁止其他控制电机的操作
- anl ready_byte,#10111111b ;休眠电机任务(task1)
- mov r3,#12 ;走12拍
-
- uab00:
- dec p_step
- mov a,p_step
- cpl a
- jnz uab03 ;过0则回到7
- mov p_step,#7
- uab03:
- mov a,p_step
- mov dptr,#tab_step
- MOVC A,@a+dptr
- anl P1,#11110000b ;驱动电机
- orl P1,a
- call waiting
-
- djnz r3,uab00
- anl p1,#11110000b ;关电机
- orl ready_byte,#01000000b ;唤醒电机任务
- ret
- down_a_bit: ;电机向下微调一个距离 ;临界区处理,禁止其他控制电机的操作
- anl ready_byte,#10111111b ;休眠电机任务(task1)
- mov r3,#12 ;走12拍
- dab00:
- inc p_step
- mov a,p_step
- cjne a,#8,dab03 ;过7则回到0
- mov p_step,#0
- dab03:
- mov a,p_step
- mov dptr,#tab_step
- MOVC A,@a+dptr
- anl P1,#11110000b ;驱动电机
- orl P1,a
-
- call waiting
- djnz r3,dab00
- anl p1,#11110000b ;关电机
- orl ready_byte,#01000000b ;唤醒电机任务
- ret
- ;任务0子程序:键值key_value放到循环的bcd池里(pool_bcd,p_bcd),置位bcd_ready *****orig
- keyv2bcd:
- jnb reset_bcd,k2b10
- call clr_bcd
- clr reset_bcd
- k2b10: mov b,key_value ;键值
- ;存放在pool_bcd
- mov a,p_bcd ;容错处理:p_bcd只能0-2,超范围就置0
- clr c
- subb a,#3
- jnc k2b01
- ;先推指针
- inc p_bcd
- mov a,p_bcd
- cjne a,#3,k2b02 ;0-2循环处理
- k2b01: mov p_bcd,#0
- k2b02:
- mov a,#pool_bcd
- add a,p_bcd
- mov r0,a
- mov @r0,b ;再存键值
- setb bcd_ready
- ret
- ;任务0子程序:从循环的bcd池里取最近3个值(pool_bcd,p_bcd),查表转换成段码,存放到段码数组(pool_print);
- bcd2print:
- mov r1,p_bcd ;0-2范围
- mov r2,#3 ;依次取3个数
- b2p00: mov a,#pool_bcd
- add a,r1
- mov r0,a
- mov a,@r0 ; 取到bcd值然后 查表获取段码
- mov dptr,#tab_ht1621_seg
- movc a,@a+dptr
- mov r3,a
- mov a,r2
- dec a
- add a,#pool_print
- mov r0,a
- mov a,r3
- mov @r0,a
- dec r1
- mov a,r1
- cpl a
- jnz b2p01
- mov r1,#2
- b2p01:
- djnz r2,b2p00
- ret
- ;任务0子程序:将段码数组pool_print的3个字节前面的0隐去
- hide_zero:
- mov r3,#2 ;2次,个位数不管
- mov r0,#pool_print
- hzo00:
- mov a,@r0
- cjne a,#5fh,hzo01 ; 5f 为0的段码
- mov @r0,#0
- inc r0
- djnz r3,hzo00
- hzo01:
- ret
- ;任务0子程序:将段码数组pool_print的3个字节输出到ht1621
- print: ;(uchar Addr,uchar *p,uchar cnt) R3:ADDR r4:P R5:CNT
- CLR CS_1621;
- MOV A,#0A0H
- MOV R0,#3
- CALL Ht1621Wr_Data ;(0xa0,3); // - - 写入数据标志101
- MOV A,R3
- RLC A
- RLC A ;Ht1621Wr_Data(Addr<<2,6); // - - 写入地址数据
- MOV R0,#6
- CALL Ht1621Wr_Data
-
- prt00:
- mov a,r4
- mov r0,a
- MOV A,@r0 ;取段码
- MOV R0,#8
- CALL Ht1621Wr_Data ;Ht1621Wr_Data(*p,8); // - - 写入数据
- INC r4
- DJNZ R5,prt00
- SETB CS_1621
- CALL delay_a_while
- RET
- ;任务0清零表
- TAB_HT1621:
- DB 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0;
- TAB_HT1621_off:
- DB 033h,078h,078h,055h,055h,055h,000h,05h,05h,05h,0,0,0,0,0,0;
- TAB_HT1621_dou:
- DB 0b7h,0b3h,093h,055h,055h,055h,000h,05h,05h,05h,0,0,0,0,0,0;
- ;任务0段码表,依据硬件线序确定
- tab_ht1621_seg:
- db 5fh; 0
- db 06h; 1
- db 3dh; 2
- db 2fh; 3
- db 66h; 4
- db 6bh; 5
- db 7bh; 6
- db 0eh; 7
- db 7fh; 8
- db 6fh; 9
- db 7eh; A
- db 73h; b
- db 31h; c
- db 37h; d
- db 79h; E
- db 78h; F
- db 33h; o
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;;;;;任务1:电机驱动,实际值逼近目标值 ;
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;实际值:p_moto_L、p_moto_H、目标值targ_moto_L、targ_moto_H 最大360度
- ;步骤:目标值-实际值,记录符号(正负)到moto_dir, 结果等于0时,关闭电机,返回;结果小于180时,moto_dir反置(寻找最短路径)
- ;让电机逼近一步,实际值+1或-1。返回。
- ;;------------------------------------------任务1
- task_1:
- mov p_moto_L,#0eh
- mov p_moto_H,#01h ; 0-359范围
- mov targ_moto_L,#0
- mov targ_moto_H,#0 ; 0-999范围 ;要mod360处理,变为0-359范围
-
- ;臂板垂直向下270度为初始态(压缩状态,方便包装和移动)在电气驱动里面初始化。
- tsk100:
- ;16位减法 目标值-当前值,默认为+
- clr moto_dir ;默认电机方向
- clr c
- mov a,targ_moto_L
- subb a,P_moto_L
- mov b,a
- mov a,targ_moto_H
- subb a,P_moto_H ;结果高位在a,低位在b
- jnc tsk105 ;结果为负数的话 被减数+360,再减
- clr c
- mov a,targ_moto_L
- addc a,#68h
- mov r0,a
- mov a,#01h
- addc a,targ_moto_h
- mov r1,a
- clr c ;重新算一次
- mov a,r0
- subb a,P_moto_L
- mov b,a
- mov a,r1
- subb a,P_moto_H ;结果高位在a,低位在b
- tsk105:
- mov r1,a ;H
- mov r0,b ;L 暂存结果(正偏差:0-359)
- jnz tsk104
- mov a,b
- jnz tsk104
- ;结果为0 关闭电机 并返回
- anl p1,#11110000b ;驱动电机
- orl ready_byte,#11100001b ;开启其他任务
- jmp tsk100
- tsk104: ;偏差如果大于180,电机方向反向
- anl ready_byte,#11011111b ;暂停键盘线程
- clr c
- mov a,r0
- subb a,#180
- mov b,a
- mov a,r1
- subb a,#0 ;16位减去180
- jc tsk101 ;
- cpl moto_dir
- tsk101:
- call moto_move
- ;实际位置指针调整一位
- jb moto_dir,tsk102
- clr c
- mov a,p_moto_L
- addc a,#1
- mov p_moto_L,a
- clr a
- addc a,p_moto_H
- mov p_moto_H,a
- cjne a,#01h,tsk100 ;如果等于360则归零
- mov a,p_moto_L
- cjne a,#68h,tsk100
- mov p_moto_L,#0
- mov p_moto_H,#0
- jmp tsk100
- tsk102:
- clr c
- mov a,p_moto_L
- subb a,#1
- mov p_moto_L,a
- mov a,p_moto_H
- subb a,#0
- mov p_moto_H,a
- cpl a ;如果等于ffff,则改为359
- jnz tsk100
- mov a,p_moto_L
- cpl a
- jnz tsk100
- mov p_moto_L,#67h
- mov p_moto_H,#01h
- jmp tsk100
- jmp task_1
- ;----------task1 end-------}}}}--
- ;任务1子程序:电机走一度,方向在moto_dir,
- ;涉及2张表:表1,45度折合512拍表,tab_deg,p_deg(0-44), 表2,8拍表tab_step,p_step(0-7)
- ;步骤:1根据方向调整度数指针,取一个度数拍数 2根据方向走N拍并更新拍数指针;
- moto_move:
- mov c,moto_dir ;防止过程中改变方向
- mov tmp_dir,c
- mmv00:
- jb tmp_dir,mmv01 ;正方向
- dec p_deg
- mov a,p_deg
- cpl a
- jnz mmv03 ;过0则回到44
- mov p_deg,#44
- mmv03:
- mov a,p_deg
- mov dptr,#tab_deg
- movc a,@a+dptr
- jmp mmv02
- mmv01:
- inc p_deg ;反方向
- mov a,p_deg
- cjne a,#45,mmv04 ;过44则回到0
- mov p_deg,#0
- mmv04:
- mov a,p_deg
- mov dptr,#tab_deg
- movc a,@a+dptr
- mmv02:
- mov r3,a
- call move_n_step
- ret
- move_n_step:;方向在tmp_dir,步数在r3, 表2,8拍表tab_step,p_step(0-7)
- mns00:
- jb tmp_dir,msn01 ;正方向
- dec p_step
- mov a,p_step
- cpl a
- jnz msn03 ;过0则回到7
- mov p_step,#7
- msn03:
- mov a,p_step
- mov dptr,#tab_step
- MOVC A,@a+dptr
- jmp msn02
- msn01:
- inc p_step ;反方向
- mov a,p_step
- cjne a,#8,msn04 ;过7则回到0
- mov p_step,#0
- msn04:
- mov a,p_step
- mov dptr,#tab_step
- MOVC A,@a+dptr
-
- msn02:
- anl p1,#11110000b ;驱动电机
- orl p1,a
- ;至此,电机走动了一拍,下面是延时:需要2ms,采用不可重入的delay_sys_us完成
- ;CALL delay_a_step
- ;call waiting
- ;mov dptr,#10
- ;call delay_sys
-
- mov r0,#3
- call delay_sys_us
-
- djnz r3,mns00 ;走r3步数
- ret
- tab_step: ;步进电机8拍表,循环使用
- DB 1001B,0001B,0011B,0010B,0110B,0100B,1100B,1000B
- tab_deg: ;45度折合512拍表,循环使用
- db 11,11,12,11,11,12,11,11,12,11,12,12,11,11,12
- db 11,11,12,11,11,12,11,11,12,11,11,12,11,11,12
- db 11,11,12,12,11,12,11,11,12,11,11,12,11,11,12
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ; ;
- ;;;;;任务2:按键扫描 ;
- ;;标准的4*4按键扫描程序,键值为0-fh,设置3个字节的缓冲pool_key,设置一个缓冲指针p_key(0-3),当缓冲区满,丢弃新的按键
- task_2:
- ;初始化
- mov p_key,#0 ;0表示缓冲区空,3表示满了,类似堆栈指针,注意定义时往前推一格
- scan_key:
- mov a,p_key
- cjne a,#3,tk301 ;缓冲区满了
- jmp scan_key
- tk301:
- anl p0,#00001111b ;p0.7 p0.6 p0.5 p0.4 为竖线 从左到右
- mov a,p2 ;p2.4 p2.5 p2.6 p2.7 为横线 从上到下
- orl a,#00001111b
- cpl a
- jz scan_key ;快速判断,无任何按键时不要去挨个扫了,这样响应更快
- mov r3,#4 ;竖线循环4次
- mov a,#01111111b
- tk300:
- orl p0,#11110000b
- anl p0,a
- mov r2,a ;暂存
- jb p2.4,tk303
- mov r0,#0
- call take_keyv
- tk303:
- jb p2.5,tk304
- mov r0,#1
- call take_keyv
- tk304:
- jb p2.6,tk305
- mov r0,#2
- call take_keyv
- tk305:
- jb p2.7,tk302
- mov r0,#3
- call take_keyv
- tk302:
- mov a,r2
- rr a ;下一个竖线
- djnz r3,tk300
- jmp scan_key
-
- jmp task_2
- ;----------------------------task2 end---}}}}}-----
- ;
- tab_key16: ;二位数组的4*4键值表
- db 0ah,0bh,0ch,0dh
- db 03,06,09,0fh
- db 02,05,08,0
- db 01,04,07,0eh
- ;子程序:查表取键值,r3:行号,r0:列号
- take_keyv:
- mov a,r3 ;1-4 转为 0-3
- dec a
-
- mov dptr,#tab_key16
- mov b,#4
- mul ab ;调整基地址
- add a,dpl
- mov dpl,a
- clr a
- addc a,dph ;进位考虑
- mov dph,a
- mov a,r0
- movc a,@a+dptr ;查表取到对应的键值 在b
- mov b,a
- ;存到缓冲池
- mov a,p_key
- cjne a,#3,tkv00 ;缓冲区满了
- ret
- tkv00:
-
- mov lock_byte,#5ah ;;临界区,加锁
- inc p_key
- mov a,p_key
- add a,#pool_key
- mov r1,a
- mov @r1,b ;存缓冲
- mov lock_byte,#11h ;;;;退临界区,解锁
- mov a,#30
- add a,sys_time_l ;设置300ms时限
- mov r1,a
- tkv01: ;按键释放时立即返回,连续按住时要间隔延时
- ;超时退出
- mov a,sys_time_l
- clr c
- subb a,r1
- clr c
- subb a,#5 ;时间模糊处理,只要接近目标时间50ms以内,就算超时,担心有错过时钟刻度的考虑
- jc tkv02
- ;anl p0,#00001111b ;p0.7 p0.6 p0.5 p0.4 为竖线 改变p0可能会扰乱竖线扫描
- mov a,p2 ;p2.4 p2.5 p2.6 p2.7 为横线 从上到下
- orl a,#00001111b
- cpl a
- jnz tkv01 ;判断是否释放(范围为本行)
- tkv02:
- ret
- ;;--------------任务3-----次任务,注意保护范围:psw\a\b\r0-r3 以及最大嵌套2个call
- ;;-----------------------------------任务3
- ;;步进电机每走一步的延时唤醒线程
- task_3:
- jmp task_3
- ;-----------------------------------------------task3 end--------------
- ;
- ;;-----------------------------------任务4
- task_4:
- jmp task_4
- ;-----------------------------------------------task5 end--------------
- ;
- ;;-----------------------------------任务5
- task_5:
- jmp task_5
- ;-----------------------------------------------task5 end--------------
- ;
- ;;-----------------------------------任务6
- task_6:
- jmp task_6
- ;-----------------------------------------------task6 end--------------
- ;
- ;
- ;--------------------------------------------------------------------------------------
- ;以下用户子程序区
- ;;ht1621b driver RD WR DATA CS
- ;/********************************************************
- ;函数名称:void Ht1621_Init(void)
- ;功能描述: HT1621初始化
- Ht1621_Init:
- SETB CS_1621;
- SETB WR_1621;
- SETB DATA_1621;
- MOV dptr,#5;
- CALL delay_sys; // - - 延时使LCD工作电压稳定
- MOV R1,#BIAS
- CALL Ht1621WrCmd;
- MOV R1,#RC256
- CALL Ht1621WrCmd; // - - 使用内部振荡器
- MOV R1,#SYSDIS
- CALL Ht1621WrCmd; // - - 关振系统荡器和LCD偏压发生器
- MOV R1,#WDTDIS
- CALL Ht1621WrCmd;; // - - 禁止看门狗
- MOV R1,#SYSEN; // - - 打开系统振荡器
- CALL Ht1621WrCmd;
- MOV R1,#LCDON; // - - 打开声音输出
- CALL Ht1621WrCmd;
- ret
- ;**写数据到ht1621,数据存A,发送位数存R0*****************************************************/
- Ht1621Wr_Data:;(uchar Data,uchar cnt) A:DATA R0:number of send-bit
- CLR WR_1621;
- CALL delay_a_while
- RLC A;
- MOV DATA_1621,C;
- CALL delay_a_while
- SETB WR_1621;
- CALL delay_a_while
- DJNZ R0,Ht1621Wr_Data
- ret
- ;****写命令给HT1621****************************************************
- Ht1621WrCmd: ;(uchar Cmd) cmd byte store in R1
- CLR CS_1621
- CALL delay_a_while
- MOV A,#80H
- MOV R0,#4
- CALL Ht1621Wr_Data; // - - 写入命令标志1000
- MOV A,R1
- MOV R0,#8
- CALL Ht1621Wr_Data; // - - 写入命令数据
- SETB CS_1621
- CALL delay_a_while
- RET
- ;*******************************************************
- ;函数名称:void Ht1621WrOneData(uchar Addr,uchar Data)
- ;功能描述: HT1621在指定地址写入数据函数
- ;全局变量:无
- ;参数说明:Addr为写入初始地址,Data为写入数据
- ;返回说明:无
- ;说 明:因为HT1621的数据位4位,所以实际写入数据为参数的后4位
- ;********************************************************/
- Ht1621WrOneData:;(uchar Addr,uchar Data) R2,R3
- CLR CS_1621;
- MOV A,#0A0H
- MOV R0,#3
- CALL Ht1621Wr_Data;(0xa0,3); // - - 写入数据标志101
- MOV A,R2
- RLC A
- RLC A
- MOV R0,#6
- CALL Ht1621Wr_Data;(Addr<<2,6); // - - 写入地址数据
- MOV A,R3
- RLC A
- RLC A
- RLC A
- RLC A
- MOV R0,#4
- CALL Ht1621Wr_Data;(Data<<4,4); // - - 写入数据
- SETB CS_1621
- CALL delay_a_while
- RET
- ;*********函数名称:void Ht1621WrAllData(uchar Addr,uchar *p,uchar cnt)
- ;功能描述: HT1621连续写入方式函数
- ;参数说明:Addr为写入初始地址,*p为连续写入数据指针,
- ;cnt为写入数据总数
- ;返回说明:无
- ;说 明:HT1621的数据位4位,此处每次数据为8位,写入数据
- ;总数按8位计算
- ;********************************************************/
- Ht1621WrAllData:;(uchar Addr,uchar *p,uchar cnt) R3:ADDR DPTR:P R5:CNT
- CLR CS_1621;
- MOV A,#0A0H
- MOV R0,#3
- CALL Ht1621Wr_Data ;(0xa0,3); // - - 写入数据标志101
- MOV A,R3
- RLC A
- RLC A ;Ht1621Wr_Data(Addr<<2,6); // - - 写入地址数据
- MOV R0,#6
- CALL Ht1621Wr_Data
-
- hwd00:
- CLR A
- MOVC A,@A+DPTR
- MOV R0,#8
- CALL Ht1621Wr_Data ;Ht1621Wr_Data(*p,8); // - - 写入数据
- INC DPTR
- DJNZ R5,hwd00
- SETB CS_1621
- CALL delay_a_while
- RET
- ;;;----------------
- delay_a_while:
- mov r3,#50
- daw00:
- nop
- djnz r3,daw00
- ret
- delay_a_step:
- mov r6,#0ffh
- dast00:
- nop
- nop
- nop
- djnz r6,dast00
- ret
- ;---------------------------------------以下用户数据表区------------------
- ;用户可以在此定义所需要的数据表
- end
- ;*******************************************the end**********************
复制代码
|