51单片机多任务的原理和实现
51单片机原理
51单片机原理
51单片机原理介绍
51单片机是一种常用的微控制器,它采用哈佛结构体系,通
过处理和控制数据和信号来完成各种任务。
下面将介绍51单
片机的基本原理。
1. 架构
51单片机采用8位的数据总线,具有以8051内核为基础的架构。
它包括存储器、中央处理单元(CPU)、输入/输出端口、定时器/计数器和串行通信接口等组件。
2. 存储器
51单片机内部包括片内ROM和RAM。
ROM用于存储程序代码,RAM用于存储数据。
51单片机可以通过访问存储器来读
取和写入数据。
3. 中央处理单元(CPU)
51单片机的CPU是它的核心部件,负责执行指令和控制各个
组件的操作。
它包括累加寄存器、程序计数器、指令寄存器、标志寄存器等。
4. 输入/输出端口
51单片机具有多个输入/输出端口,用于与外部设备进行通信。
它可以接收来自外部设备的输入信号,并且可以输出信号给外部设备。
5. 定时器/计数器
51单片机内部包含多个定时器/计数器,它们可以用来产生和测量时间间隔。
通过配置这些定时器/计数器,可以实现连接传感器、驱动电机等功能。
6. 串行通信接口
51单片机具备串行通信接口,可以通过串口与外部设备进行通信。
这使得它可以实现与计算机之间的数据传输、与其他单片机之间的通信等功能。
总结:
51单片机是一种高度集成的微控制器,具有强大的处理和控制能力。
它的架构包括存储器、CPU、输入/输出端口、定时器/计数器和串行通信接口等组件。
通过合理配置和编程,可以实现各种功能和应用。
51单片机原理及应用
51单片机原理及应用51单片机是一种常用的微控制器,广泛应用于各种电子设备中。
它具有体积小、功耗低、性能稳定等特点,因此在电子产品设计领域有着广泛的应用。
本文将介绍51单片机的基本原理及其在各个领域的应用。
首先,我们来了解一下51单片机的基本原理。
51单片机是一种8位的单片机,它采用哈佛结构,具有较强的数据处理能力。
它的核心是由CPU、存储器、I/O口、定时器/计数器等部分组成。
其中,CPU是单片机的核心部分,它负责控制整个系统的运行。
存储器用于存储程序和数据,而I/O口则用于与外部设备进行通信。
定时器/计数器则可以用于生成精确的时钟信号,实现定时、计数等功能。
在实际应用中,51单片机有着广泛的用途。
首先是在家电领域,比如空调、洗衣机、微波炉等电器中常常会采用51单片机来控制整个系统的运行。
其次是在工业控制领域,比如自动化生产线、机械设备等也会采用51单片机来实现控制和监测。
另外,在通信领域,比如手机、路由器等设备中也会采用51单片机来实现各种功能。
此外,51单片机还广泛应用于汽车电子、医疗设备、安防监控等领域。
除了以上领域外,51单片机还有着其他的应用。
比如在智能家居领域,通过51单片机可以实现对家居设备的智能控制,实现远程遥控、定时开关等功能。
在物联网领域,通过51单片机可以实现各种传感器数据的采集和传输,实现对环境的监测和控制。
在教育领域,通过51单片机可以实现对学生的电子设计能力的培养,让学生学会如何使用单片机来实现各种功能。
综上所述,51单片机作为一种常用的微控制器,在各个领域都有着广泛的应用。
它的基本原理简单易懂,同时具有较强的数据处理能力,因此在电子产品设计领域有着重要的地位。
希望通过本文的介绍,读者能对51单片机有更深入的了解,并在实际应用中发挥其作用。
试论51单片机多任务编程设计及应用
试论51单片机多任务编程设计及应用51单片机是一种基于哈弗曼编码技术的单片机,广泛应用于嵌入式系统中。
随着嵌入式系统复杂度的不断提高,多任务编程设计已经成为了一种必要的需求。
本文将试论51单片机多任务编程设计及应用。
多任务设计可以使嵌入式系统在同时运行多个任务时,具有更高的系统效率和响应速度。
在多任务环境中,各任务可同时执行,每个任务都具有自己的优先级和独立的数据空间。
对于51单片机而言,要实现多任务编程设计,需要考虑以下几个方面:1.任务划分首先需要将任务划分为多个单独的任务。
根据任务的优先级和时间限制,进行合理的划分和安排。
2.任务调度在多任务环境中,需要进行任务调度,确定任务执行的优先级和时间片。
一般采用时间片轮转方式,即每个任务分配一个时间片,时间到了就切换到下一个任务。
3.任务间通信不同任务之间需要共享数据和通信,可采用共享变量或消息机制等方式进行任务间通信。
在实际应用中,多任务编程设计可应用于多种场景,例如自动控制、机器人控制、传感器采集等。
下面以自动控制为例,介绍51单片机多任务编程设计及应用。
自动控制系统通常包括数据采集、数据处理和执行控制三个任务。
数据采集任务负责采集传感器数据,数据处理任务负责对采集数据进行处理和分析,执行控制任务负责根据处理结果执行相应的控制操作。
在这个模块中,执行控制任务的优先级最高,数据采集任务的优先级最低。
首先应进行任务划分,将数据采集、数据处理和执行控制分别划分为独立的任务。
接着需要确定任务调度的方式和时间限制,可采用时间片轮转方式,每个任务分配一个时间片,执行控制任务的时间片最短,数据采集任务的时间片最长。
最后需要进行任务间通信,数据采集任务将采集到的数据存入共享变量中,数据处理任务通过共享变量获取数据进行处理,执行控制任务通过共享变量获取处理结果进行操作。
综上所述,对于51单片机的多任务编程设计及应用,需要进行任务划分、任务调度和任务间通信。
在实际应用中,多任务设计可提高嵌入式系统的效率和响应速度,降低系统复杂度。
单片机中的多任务处理技术
单片机中的多任务处理技术多任务处理技术是指在单片机系统中同时执行多个任务的能力。
随着单片机技术的快速发展,越来越多的应用需要同时处理多个任务,因此掌握多任务处理技术成为了单片机编程的重要一环。
一、多任务处理技术的原理及实现方法多任务处理技术的实现原理主要有两种:基于时间片轮转和基于优先级调度。
下面将分别对这两种方法进行介绍。
1. 基于时间片轮转的多任务处理技术基于时间片轮转的多任务处理技术是将系统时间划分为若干个时间片,每个任务被分配一个时间片进行执行。
当任务的时间片用完时,系统会切换到下一个任务,并将当前任务的状态保存,以便后续恢复执行。
这种方法的优点是能够保证每个任务都有机会被执行,避免任一任务长时间独占处理器资源。
同时,由于任务之间的切换速度很快,能够达到近乎同时执行的效果。
2. 基于优先级调度的多任务处理技术基于优先级调度的多任务处理技术是根据每个任务的优先级确定执行顺序。
具有较高优先级的任务将先于优先级较低的任务执行。
这种方法的优点是可以按照任务的紧急程度进行排序,确保紧急任务能够及时得到处理。
对于时间敏感的应用来说,这种方法更加适合。
二、多任务处理技术的应用场景多任务处理技术在单片机系统中有着广泛的应用场景,下面将介绍几个常见的应用。
1. 实时控制系统实时控制系统通常需要同时处理多个任务,例如数据采集、参数处理和控制输出等。
通过多任务处理技术,可以确保各个任务按时完成,提高系统的响应速度和稳定性。
2. 通信系统通信系统中常常需要同时进行数据的接收、处理和发送等任务。
多任务处理技术能够有效地分配处理器资源,确保数据能够实时处理和传输。
3. 智能家居系统智能家居系统需要同时处理多个设备的控制和数据处理任务,例如照明系统、安防系统和环境监测系统等。
多任务处理技术能够快速响应用户的操作,并确保各个子系统之间的平稳协调。
三、多任务处理技术的注意事项在使用多任务处理技术时,需要注意以下几点。
1. 任务的划分合理的任务划分是多任务处理技术的关键。
51单片机多任务机制的实现策略研究
51单片机多任务机制的实现策略研究本文针对51单片机实现多任务处理这一问题,首先对该系列单片机多任务机制实现的原理和存在的主要问题进行了研究,然后提出了两种实现多任务机制的策略,最后对两种策略的优缺点进行了对比,对该型单片机的多任务机制研究有一定的借鉴意义。
标签:51单片机;多任务机制;实现策略1 多任务机制的实现原理和存在问题多任务机制就是在同一时间内能够执行多个任务的机制,在多个处理器的系统内,这样的机制不难实现,但在只有一个处理器的控制系统内,就势必要求多个任务在时间域内进行快速的切换,所以这种情况下的多任务并不是真正意义上的同一时间内处理多个任务,而是通过快速切换任务造成的错觉。
多任务系统任务的切换策略可分为协同式和抢占式。
协同式多任务系统,是指每个任务程序都可以释放对CPU的控制权,也即可将对CPU的使用权切换给其它程序,通常分为显式和隐式。
在抢占式多任务系统是指各程序不能自主的释放CPU的控制权,而是由操作系统统一行使CPU的控制权,操作系统能够从任何正在运行的程序上取走控制权,并将控制权赋予另一个程序[1]。
51单片机只有一个执行单元,这就意味着所有的程序只能按照执行单元的流程顺序执行,除中断外,其它的程序必须逐一完成,所以中断是进行多任务机制设计的关键切入点,只有通过中断的方式才能实现多任务的切换,应该重点考虑解决以下几个问题:一是并行任务的数量问题。
51单片机的8个工作寄存器的地址可映射到0-3区的工作区内,单片机可设置状态寄存器的数值进行工作区域的切换,所以多任务处理时的并行任务数量最大值不能超过4,这样可以保证任务切换的时效性。
二是任务切换的时间片分配方式。
在多任务处理时对时间的分配方式是需要重点考虑的问题。
可将时间的长度进行固定和量化,需要运行的各个任务含有不同时间长度的时间片段,每运行一个任务的时间片段,其状态寄存器中的时间片数相应的减少1,直至所有的时间片数减少为0,在时间片内任务不切换,在时间片段之间按照任务的优先级进行排序完成,全部任务完成后重新赋值。
51单片机多线程实现机制
51单片机多线程实现机制1.引言1.1 概述概述:随着科技的不断发展,现代社会对于嵌入式系统的要求也越来越高。
而在嵌入式系统中,如何实现多线程机制成为一个重要的研究课题。
多线程技术可以使得单片机在处理多个任务时更加高效和灵活,并能够提高系统的响应速度和吞吐量。
本文将主要介绍51单片机多线程实现的机制,旨在通过深入的研究和分析,探讨其原理、优势以及应用场景。
同时,本文将讨论多线程概念,并详细介绍在51单片机中如何实现多线程,包括线程的创建、调度以及资源共享等关键技术。
在本章中,我们将首先简要介绍多线程概念,阐述其对于嵌入式系统的意义和作用。
其次,我们将重点探讨51单片机多线程实现的机制,包括线程的创建与管理、线程的调度算法以及线程间的通信与同步等内容。
最后,我们将总结本章的内容,并对未来的研究方向进行展望。
通过本文的学习,读者将能够深入了解51单片机多线程实现的原理和技术,并能够在实际的嵌入式系统开发中灵活运用多线程技术,提升系统的性能和可靠性。
同时,本文也为进一步研究和探索多线程在嵌入式系统中的应用奠定了基础。
1.2 文章结构文章结构部分的内容可以包括以下内容:文章结构是指整篇文章的组织和安排方式,它对于读者能否清晰地理解文章的内容起着至关重要的作用。
在本文中,我将按照以下结构来组织我的文章:1. 引言:在引言部分,我将对本文的主题进行概述,并说明本文的目的和意义。
我会简要介绍51单片机和多线程的概念,并指出本文的重点是探讨如何在51单片机上实现多线程。
2. 正文:在正文部分,我将详细介绍多线程的概念,包括多线程的定义、特点、优点和应用领域。
我还会解释多线程在嵌入式系统中的重要性,并介绍一些常用的多线程实现机制。
在本文的重点部分,我将详细介绍如何在51单片机上实现多线程。
我会解释单片机的特点和限制,以及为什么需要在单片机上使用多线程。
我还会介绍一些常用的51单片机多线程实现方法,比如时间片轮转调度算法和互斥锁机制等。
单片机多任务的时间片方式实现
单片机多任务的时间片方式实现由于单片机具有价格低、运行要求低、易于开发、稳定可靠等优点,广泛应用于仪器仪表、家用电器、医用设备、航空航天、专用设备的智能化管理及过程控制等领域。
但是,单片机的位数少、频率低、内存小、I/O口少等缺点限制了其加载操作系统的可能。
因此,单片机不能像ARM等较高性能的处理器一样,利用加载的操作系统实现管理与配置内存、决定系统资源供需的优先次序、控制输入与输出设备、操作网络与管理文件系统等功能。
但是,我们可以根据单片机所拥有的内存大小、CPU频率等因素,来为单片机量身定做一个小型的操作系统,以实现单片机的多任务运行。
1 微机实现多任务的方式微机实现多任务的方式一般是由加载的操作系统来实现的。
通过操作系统提供的函数来创建多进程或者多线程来实现多任务方式。
由于多进程耗费的资源多,而多线程的开销相对小的多,因此我们采用单片机模仿多线程的方式来实现。
操作系统创建多个线程后,将管理各个线程占用CPU的时间。
操作系统以轮换方式向线程提供CPU时间片,从而使多个线程看起来是同时运行的,而不是等待一个线程执行结束后再去执行下一个线程。
PC(Program Counter,程序计数器)是用于存放下一条指令地址的地方。
某个线程正在占用CPU时间,其实是PC值指向该线程所占的内存,并正在逐条取到CPU寄存器中进行运算。
该时间片结束后,PC值要指向下一个线程所占用的内存中,进行类似的运算。
其他线程都轮流一遍后,将又回到原来那个线程暂停的位置继续运算。
所以,从一个线程转换到另外一个线程去执行时,要保存此线程的现场,包括此线程下一条指令的位置(PC值)、此线程所使用的各个寄存器值等。
当此线程又拥有CPU时间时,将保存的PC值赋给PC寄存器,保存的各个寄存器值再赋给各个寄存器。
除了保存现场与恢复现场外,另外关键的一点是,操作系统能够改变PC 值;--;强制把使用CPU的权限从一个任务切换到另一个任务,这就用到了中断。
51单片机原理及应用
51单片机原理及应用51单片机是一种常见的微控制器,以其高性能和广泛应用而受到广大工程师的青睐。
本文将介绍51单片机的原理和应用。
51单片机的原理可以从其硬件结构和工作流程两方面来讲解。
首先是硬件结构。
51单片机包括中央处理器(CPU),存储器(包括存储器管理单元、内部RAM和ROM),输入/输出端口(I/O口),定时器/计数器,串行通信接口等。
CPU是整个系统的核心,负责指令的执行和数据的处理。
存储器用于存储程序和数据,其中ROM存储程序代码,RAM用于暂存数据。
I/O口用于与外部设备进行信息交互。
定时器/计数器用于产生精确的时间延迟和计数操作。
串行通信接口用于与其他设备进行数据传输。
其次是工作流程。
51单片机的工作流程一般包括初始化、输入/输出控制和运算处理三个阶段。
初始化阶段主要是对各个模块的配置和初始化,例如设置时钟频率、串口波特率等。
输入/输出控制阶段通过读取输入设备(如按键、传感器等)的状态,控制外部设备(如LED灯、马达等)的状态。
运算处理阶段通过执行指令,对数据进行处理和计算。
至于应用方面,51单片机具有广泛的应用领域。
主要应用包括控制系统、嵌入式系统、通信系统、工业自动化等。
在控制系统中,51单片机可以用于控制家电、机器人、机械设备等。
在嵌入式系统中,51单片机可以应用于智能家居、智能交通、智能仪表等。
在通信系统中,51单片机可以用于电话、网络和无线通信设备等。
在工业自动化中,51单片机可以用于工厂生产线控制、仪器仪表控制等。
总结起来,51单片机的原理和应用都是非常重要的。
通过了解其硬件结构和工作流程,可以更好地理解其工作原理。
而了解其应用领域,则可以为工程师在实际项目中的选择和设计提供参考。
51单片机多任务编程设计及应用
51单片机多任务编程设计及应用作者:李鹏来源:《科技风》2016年第16期摘要:在51单片机上实现多任务处理主要借鉴现代操作系统的分时处理方法,有几种不同的实现策略。
本文将基于此背景,讨论51单片机多任务编程的设计和应用,详细阐述其实现策略。
关键词:51单片机;多任务编程;操作系统单片机技术经历几十年的发展逐步走向成熟,被广泛应用于各行业。
51单片机指兼容Intel8031系统的单片机系统,在智能控制领域有广泛的引用[ 1 ]。
一、多任务执行原理51单片机的多任务执行机制主要借鉴现代操作系统的分时处理方法。
事实上,多任务执行并非多个任务同时运行,而是CPU在不同的任务之间不停的切换,每次执行一个任务的一小部分,之后迅速切换至下一个任务,并执行这个任务的一小部分,然后在切换至另一个任务,以此循环往复。
从宏观上来看,就好像多个任务在同时执行。
系统通过合理的调度,将CPU的运行时间合理分配给各个任务,每个任务轮流占用一小部分时间。
这就是现代操作系统分时机制的原理[ 3 ],也是51单片机多任务执行的基本方法。
二、实现策略51单片机多任务执行机制针对不同的应用场景和不同的单片机型号,在具体实现上有所区别,但从根本上来说都是以现代操作系统分时理论为基础来实现的。
下面将详细讲解具体如何使用时间片分配机制来实现51单片机的多任务执行。
基于时间片分配机制来实现51单片机多任务执行主要涉及三项内容。
一是待执行程序,以列表形式组织,二是运算资源,也就是CPU,三是调度器,用于统筹安排待执行程序的执行顺序,合理给各待执行程序分配运算资源。
具体的运行机制如下。
首先,在初始化阶段,待执行任务被组织为列表,然后调度器根据具体情况为各个任务分配不同数量的时间片。
然后在调度器的组织下,各个任务依次占用CPU,占用时间为各自对应数量的时间片。
通常来说,根据具体情况不同,各任务占用的时间片数目有所区别,但总数量都不会很多,CPU只执行任务的一小部分,然后迅速切换至下一个任务。
基于51单片机多任务嵌入式操作系统模型
这个操作系统很给力一种裸奔多任务模型一个网友的总结:stateMachine + timerTick + queue。
在RTOS环境下的多任务模型:任务通常阻塞在一个OS调用上(比如从消息队列取数据)。
外部如果想让该任务运转,就要向消息队列发送消息。
任务收到消息时,根据当前状态,决定如何处理消息。
这就是状态机。
任务将消息队列中的消息处理完毕后,重新进入阻塞状态。
任务在处理中,有时要延时一段时间,然后才继续工作:为了充分使用CPU,可以通过OS调用让其它任务去工作。
OS通常会提供一个taskDelay调用。
当任务调用taskDelay时,即进入阻塞状态,直到超时,才重新进入可工作状态(就绪状态)。
下面说说裸奔环境下的多任务模型:裸奔也可以多任务,但调度是由用户自主控制。
在RTOS环境下,一般提供抢占式调度。
在裸奔时,一般是任务在处理告一段落后,主动结束处理。
RTOS环境下的任务,一般处于一个while(1)循环中。
while(1){从消息队列接收消息。
如果没有,将阻塞。
处理消息。
}裸奔下的任务,一般采用查询方式:{查询是否有待处理的事件。
如果没有,返回。
如果有,根据任务的当前状态,进行处理。
处理完毕后,可能返回,也可能将待处理事件全部处理完毕后再返回。
}裸奔任务其实也处于一个while(1)循环中,只不过这个循环在任务外部。
main(){A_taskInit(); //任务的初始化B_taskInit();...while(1){A_taskProc(); //任务的处理B_taskProc();}}状态机既适用于OS环境,也适用于裸奔环境。
但在裸奔环境下,状态可能被切分得更细。
例如后面讲的如何在裸奔环境实现taskDelay()。
消息队列既适用于OS环境,也适用于裸奔环境。
在OS环境下,消息队列机制由OS提供。
在裸奔环境下,消息队列要自己来实现。
如果对队列的概念不清楚,可参考《数据结构》教材。
这个队列机制,可做成通用模块,在不同的程序中复用。
51单片机的工作原理
51单片机的工作原理首先,我们需要了解51单片机的基本结构。
51单片机是一种集成了CPU、RAM、ROM、I/O端口和定时/计数器等功能模块的芯片。
它的CPU部分包括指令执行单元、寄存器组和时钟电路,可以实现各种指令的执行和数据的处理。
RAM用来存储临时数据,而ROM则用来存储程序代码和常量数据。
I/O端口用于与外部设备进行数据交换,而定时/计数器则用于产生精确的定时信号和计数功能。
其次,我们来看一下51单片机的工作原理。
当51单片机上电后,时钟电路开始工作,CPU开始按照程序存储区中的指令序列执行程序。
首先,CPU从ROM中读取程序的第一条指令,然后根据指令的操作码和地址码执行相应的操作。
在执行指令的过程中,CPU可能需要从RAM中读取数据,对数据进行运算,然后将结果存储回RAM或者输出到外部设备。
此外,51单片机的I/O端口可以与外部设备进行数据交换。
当需要与外部设备进行通信时,CPU通过读写I/O端口的方式来实现数据的输入和输出。
通过编程控制I/O端口的状态,可以实现与外部设备的各种交互操作,比如控制LED的亮灭、读取传感器的数据等。
最后,定时/计数器模块可以产生精确的定时信号和实现计数功能。
通过编程设置定时/计数器的工作模式和计数值,可以实现定时触发某些操作或者实现精确的计数功能,比如测量时间间隔、生成脉冲信号等。
总的来说,51单片机的工作原理是通过CPU执行程序指令,与RAM、ROM、I/O端口和定时/计数器等功能模块进行数据交换和控制操作,从而实现各种复杂的功能。
它的工作原理涉及到计算机体系结构、数字电路、嵌入式系统等多个领域的知识,是一种功能强大的微控制器。
希望通过本文的介绍,读者对51单片机的工作原理有了更深入的了解,这将有助于他们在实际应用中更好地理解和使用51单片机。
同时,也希望本文能够激发读者对微控制器和嵌入式系统的兴趣,促进相关领域的学习和研究。
51单片机多任务的原理及其实现
51单片机多任务操作系统的原理与实现51单片机多任务操作系统的原理与实现-- 一个超轻量级的操作系统前言想了很久,要不要写这篇文章最后觉得对操作系统感兴趣的人还是很多,写吧.我不一定能造出玉,但我可以抛出砖.包括我在内的很多人都对51使用操作系统呈悲观态度,因为51的片上资源太少.但对于很多要求不高的系统来说,使用操作系统可以使代码变得更直观,易于维护,所以在51上仍有操作系统的生存机会.流行的uCos,Tiny51等,其实都不适合在2051这样的片子上用,占资源较多,唯有自已动手,以不变应万变,才能让51也有操作系统可用.这篇贴子的目的,是教会大家如何现场写一个OS,而不是给大家提供一个OS版本.提供的所有代码,也都是示例代码,所以不要因为它没什么功能就说LAJI之类的话.如果把功能写全了,一来估计你也不想看了,二来也失去灵活性没有价值了.下面的贴一个示例出来,可以清楚的看到,OS本身只有不到10行源代码,编译后的目标代码60字节,任务切换消耗为20个机器周期.相比之下,KEIL内嵌的TINY51目标代码为800字节,切换消耗100~700周期.唯一不足之处是,每个任务要占用掉十几字节的堆栈,所以任务数不能太多,用在128B内存的51里有点难度,但对于52来说问题不大.这套代码在36M主频的STC12C4052上实测,切换任务仅需2uS.#include <>#define MAX_TASKS 2 须和实际任务数一至#define MAX_TASK_DEP 12 低不得少于2个,保守值为12.unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP];unsigned char task_id; 指定的函数(参数1)装入指定(参数2)的任务槽中.如果该槽中原来就有任务,则原任务丢失,但系统本身不会发生错误.void task_load(unsigned int fn, unsigned char tid){task_sp[tid] = task_stack[tid] + 1;task_stack[tid][0] = (unsigned int)fn & 0xff;task_stack[tid][1] = (unsigned int)fn >> 8;}用该宏后,将永不返回.#define os_start(tid) {task_id = tid,SP = task_sp[tid];return;}/*======================以下为测试代码======================*/void task1(){static unsigned char i;while(1){i++;task_switch(); 么是操作系统?人脑比较容易接受"类比"这种表达方式,我就用"公交系统"来类比"操作系统"吧.当我们要解决一个问题的时候,是用某种处理手段去完成它,这就是我们常说的"方法",计算机里叫"程序"(有时候也可以叫它"算法").以出行为例,当我们要从A地走到B地的时候,可以走着去,也可以飞着去,可以走直线,也可以绕弯路,只要能从A地到B地,都叫作方法.这种从A地到B的需求,相当于计算机里的"任务",而实现从A地到B地的方法,叫作"任务处理流程"很显然,这些走法中,并不是每种都合理,有些傻子都会采用的,有些是傻子都不采会用的.用计算机的话来说就是,有的任务处理流程好,有的任务处理流程好,有的处理流程差.可以归纳出这么几种真正算得上方法的方法:有些走法比较快速,适合于赶时间的人;有些走法比较省事,适合于懒人;有些走法比较便宜,适合于穷人.用计算机的话说就是,有些省CPU,有些流程简单,有些对系统资源要求低.现在我们可以看到一个问题:如果全世界所有的资源给你一个人用(单任务独占全部资源),那最适合你需求的方法就是好方法.但事实上要外出的人很多,例如10个人(10个任务),却只有1辆车(1套资源),这叫作"资源争用".如果每个人都要使用最适合他需求的方法,那司机就只好给他们一人跑一趟了,而在任一时刻里,车上只有一个乘客.这叫作"顺序执行",我们可以看到这种方法对系统资源的浪费是严重的.如果我们没有法力将1台车变成10台车来送这10个人,就只好制定一些机制和约定,让1台车看起来像10台车,来解决这个问题的办法想必大家都知道,那就是制定公交线路.最简单的办法是将所有旅客需要走的起点与终点串成一条线,车在这条线上开,乘客则自已决定上下车.这就是最简单的公交线路.它很差劲,但起码解决客人们对车争用.对应到计算机里,就是把所有任务的代码混在一起执行.这样做既不优异雅,也没效率,于是司机想了个办法,把这些客户叫到一起商量,将所有客人出行的起点与终点罗列出来,统计这些线路的使用频度,然后制定出公交线路:有些路线可以合并起来成为一条线路,而那些不能合并的路线,则另行开辟行车车次,这叫作"任务定义".另外,对于人多路线,车次排多点,时间上也优先安排,这叫作"任务优先级".经过这样的安排后,虽然仍只有一辆车,但运载能力却大多了.这套车次/路线的按排,就是一套"公交系统".哈,知道什么叫操作系统了吧它也就是这么样的一种约定.操作系统:我们先回过头归纳一下:汽车系统资源.主要指的是CPU,当然还有其它,比如内存,定时器,中断源等.客户出行任务正在走的路线进程一个一个的运送旅客顺序执行同时运送所有旅客多任务并行按不同的使用频度制定路线并优先跑较繁忙的路线任务优先级计算机内有各种资源,单从硬件上说,就有CPU,内存,定时器,中断源,I/O端口等.而且还会派生出来很多软件资源,例如消息池.操作系统的存在,就是为了让这些资源能被合理地分配.最后我们来总结一下,所谓操作系统,以我们目前权宜的理解就是:为"解决计算机资源争用而制定出的一种约定".二.51上的操作系统对于一个操作系统来说,最重要的莫过于并行多任务.在这里要澄清一下,不要拿当年的DOS来说事,时代不同了.况且当年IBM和小比尔着急将PC搬上市,所以才抄袭PLM(好象是叫这个名吧记不太清)搞了个今天看来很"粗制滥造"的DOS出来.看看当时真正的操作系统---UNIX,它还在纸上时就已经是多任务的了.对于我们PC来说,要实现多任务并不是什么问题,但换到MCU却很头痛:1.系统资源少在PC上,CPU主频以G为单位,内存以GB为单位,而MCU的主频通常只有十几M,内存则是Byts.在这么少的资源上同时运行多个任务,就意味着操作系统必须尽可能的少占用硬件资源.2.任务实时性要求高PC并不需要太关心实时性,因为PC上几乎所有的实时任务都被专门的硬件所接管,例如所有的声卡网卡显示上都内置有DSP以及大量的缓存.CPU只需坐在那里指手划脚告诉这些板卡如何应付实时信息就行了.而MCU不同,实时信息是靠CPU来处理的,缓存也非常有限,甚至没有缓存.一旦信息到达,CPU必须在极短的时间内响应,否则信息就会丢失.就拿串口通信来举例,在标准的PC架构里,巨大的内存允许将信息保存足够长的时间.而对于MCU来说内存有限,例如51仅有128字节内存,还要扣除掉寄存器组占用掉的8~32个字节,所以通常都仅用几个字节来缓冲.当然,你可以将数据的接收与处理的过程合并,但对于一个操作系统来说,不推荐这么做.假定以115200bps通信速率向MCU传数据,则每个字节的传送时间约为9uS,假定缓存为8字节,则串口处理任务必须在70uS内响应.这两个问题都指向了同一种解决思路:操作系统必须轻量轻量再轻量,最好是不占资源(那当然是做梦啦).可用于MCU的操作系统很多,但适合51(这里的51专指无扩展内存的51)几乎没有.前阵子见过一个"圈圈操作系统",那是我所见过的操作系统里最轻量的,但仍有改进的余地.很多人认为,51根本不适合使用操作系统.其实我对这种说法并不完全接受,否则也没有这篇文章了.我的看法是,51不适合采用"通用操作系统".所谓通用操作系统就是,不论你是什么样的应用需求,也不管你用什么芯片,只要你是51,通通用同一个操作系统.这种想法对于PC来说没问题,对于嵌入式来说也不错,对AVR来说还凑合,而对于51这种"贫穷型"的MCU来说,不行.怎样行量体裁衣,现场根据需求构建一个操作系统出来!看到这里,估计很多人要翻白眼了,大体上两种:1.操作系统那么复杂,说造就造,当自已是神了2.操作系统那么复杂,现场造一个会不会出BUG哈哈,看清楚了问题出在"复杂"上面,如果操作系统不复杂,问题不就解决了事实上,很多人对操作系统的理解是片面的,操作系统不一定要做得很复杂很全面,就算仅个多任务并行管理能力,你也可以称它操作系统.只要你对多任务并行的原理有所了解,就不难现场写一个出来,而一旦你做到了这一点,为各任务间安排通信约定,使之发展成一个为你的应用系统量身定做的操作系统也就不难了.为了加深对操作系统的理解,可以看一看<<演变>>这份PPT,让你充分了解一个并行多任务是如何一步步从顺序流程演变过来的.里面还提到了很多人都在用的"状态机",你会发现操作系统跟状态机从原理上其实是多么相似.会用状态机写程序,都能写出操作系统.三.我的第一个操作系统直接进入主题,先贴一个操作系统的示范出来.大家可以看到,原来操作系统可以做得么简单.当然,这里要申明一下,这玩意儿其实算不上真正的操作系统,它除了并行多任务并行外根本没有别的功能.但凡事都从简单开始,搞懂了它,就能根据应用需求,将它扩展成一个真正的操作系统.好了,代码来了.将下面的代码直接放到KEIL里编译,在每个task()函数的"task_switch();"那里打上断点,就可以看到它们的确是"同时"在执行的.#include <>#define MAX_TASKS 2 须和实际任务数一至#define MAX_TASK_DEP 12 低不得少于2个,保守值为12.unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP];unsigned char task_id; 指定的函数(参数1)装入指定(参数2)的任务槽中.如果该槽中原来就有任务,则原任务丢失,但系统本身不会发生错误.void task_load(unsigned int fn, unsigned char tid){task_sp[tid] = task_stack[tid] + 1;task_stack[tid][0] = (unsigned int)fn & 0xff;task_stack[tid][1] = (unsigned int)fn >> 8;}用该宏后,将永不返回.#define os_start(tid) {task_id = tid,SP = task_sp[tid];return;}/*==================以下为测试代码=====================*/void task1(){static unsigned char i;while(1){i++;task_switch();现在来看看这个多任务系统的原理:这个多任务系统准确来说,叫作"协同式多任务".所谓"协同式",指的是当一个任务持续运行而不释放资源时,其它任务是没有任何机会和方式获得运行机会,除非该任务主动释放CPU.在本例里,释放CPU是靠task_switch()来完成的.task_switch()函数是一个很特殊的函数,我们可以称它为"任务切换器".要清楚任务是如何切换的,首先要回顾一下堆栈的相关知识.有个很简单的问题,因为它太简单了,所以相信大家都没留意过:我们知道,不论是CALL还是JMP,都是将当前的程序流打断,请问CALL和JMP的区别是什么你会说:CALL可以RET,JMP不行.没错,但原因是啥呢为啥CALL过去的就可以用RET跳回来,JMP过去的就不能用RET来跳回呢很显然,CALL通过某种方法保存了打断前的某些信息,而在返回断点前执行的RET指令,就是用于取回这些信息.不用多说,大家都知道,"某些信息"就是PC指针,而"某种方法"就是压栈.很幸运,在51里,堆栈及堆栈指针都是可被任意修改的,只要你不怕死.那么假如在执行RET前将堆栈修改一下会如何往下看:当程序执行CALL后,在子程序里将堆栈刚才压入的断点地址清除掉,并将一个函数的地址压入,那么执行完RET后,程序就跳到这个函数去了.事实上,只要我们在RET前将堆栈改掉,就能将程序跳到任务地方去,而不限于CALL里压入的地址.重点来了......首先我们得为每个任务单独开一块内存,这块内存专用于作为对应的任务的堆栈,想将CPU交给哪个任务,只需将栈指针指向谁内存块就行了.接下来我们构造一个这样的函数:当任务调用该函数时,将当前的堆栈指针保存一个变量里,并换上另一个任务的堆栈指针.这就是任务调度器了.OK了,现在我们只要正确的填充好这几个堆栈的原始内容,再调用这个函数,这个任务调度就能运行起来了.那么这几个堆栈里的原始内容是哪里来的呢这就是"任务装载"函数要干的事了.在启动任务调度前将各个任务函数的入口地址放在上面所说的"任务专用的内存块"里就行了!对了,顺便说一下,这个"任务专用的内存块"叫作"私栈",私栈的意思就是说,每个任务的堆栈都是私有的,每个任务都有一个自已的堆栈.话都说到这份上了,相信大家也明白要怎么做了:1.分配若干个内存块,每个内存块为若干字节:这里所说的"若干个内存块"就是私栈,要想同时运行几少个任务就得分配多少块.而"每个子内存块若干字节"就是栈深.记住,每调一层子程序需要2字节.如果不考虑中断,4层调用深度,也就是8字节栈深应该差不多了.unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP]当然,还有件事不能忘,就是堆指针的保存处.不然光有堆栈怎么知道应该从哪个地址取数据啊unsigned char idata task_sp[MAX_TASKS]上面两项用于装任务信息的区域,我们给它个概念叫"任务槽".有些人叫它"任务堆",我觉得还是"槽"比较直观对了,还有任务号.不然怎么知道当前运行的是哪个任务呢unsigned char task_id当前运行存放在1号槽的任务时,这个值就是1,运行2号槽的任务时,这个值就是2....2.构造任务调度函函数:void task_switch(){task_sp[task_id] = SP;}3.装载任务:将各任务的函数地址的低字节和高字节分别入在task_stack[任务号][0]和task_stack[任务号][1]中:为了便于使用,写一个函数: task_load(函数名, 任务号)void task_load(unsigned int fn, unsigned char tid){task_sp[tid] = task_stack[tid] + 1;task_stack[tid][0] = (unsigned int)fn & 0xff;task_stack[tid][1] = (unsigned int)fn >> 8;}4.启动任务调度器:将栈指针指向任意一个任务的私栈,执行RET指令.注意,这可很有学问的哦,没玩过堆栈的人脑子有点转不弯:这一RET,RET到哪去了嘿嘿,别忘了在RET前已经将堆栈指针指向一个函数的入口了.你别把RET看成RET,你把它看成是另一种类型的JMP就好理解了.SP = task_sp[任务号];return;做完这4件事后,任务"并行"执行就开始了.你可以象写普通函数一个写任务函数,只需(目前可以这么说)注意在适当的时候(例如以前调延时的地方)调用一下task_switch(),以让出CPU控制权给别的任务就行了.最后说下效率问题.这个多任务系统的开销是每次切换消耗20个机器周期(CALL和RET都算在内了),贵吗不算贵,对于很多用状态机方式实现的多任务系统来说,其实效率还没这么高--- case switch和if()可不像你想像中那么便宜.关于内存的消耗我要说的是,当然不能否认这种多任务机制的确很占内存.但建议大家不要老盯着编译器下面的那行字"DATA = XXXbyte".那个值没意义,堆栈没算进去.关于比较省内存多任务机制,我将来会说到.概括来说,这个多任务系统适用于实时性要求较高而内存需求不大的应用场合,我在运行于36M主频的STC12C4052上实测了一把,切换一个任务不到3微秒.下回我们讲讲用KEIL写多任务函数时要注意的事项.下下回我们讲讲如何增强这个多任务系统,跑步进入操作系统时代.四.用KEIL写多任务系统的技巧与注意事项C51编译器很多,KEIL是其中比较流行的一种.我列出的所有例子都必须在KEIL 中使用.为何不是因为KEIL好所以用它(当然它的确很棒),而是因为这里面用到了KEIL的一些特性,如果换到其它编译器下,通过编译的倒不是问题,但运行起来可能是堆栈错位,上下文丢失等各种要命的错误,因为每种编译器的特性并不相同.所以在这里先说清楚这一点.但是,我开头已经说了,这套帖子的主要目的是阐述原理,只要你能把这几个例子消化掉,那么也能够自已动手写出适合其它编译器的OS.好了,说说KEIL的特性吧,先看下面的函数:sbit sigl = P1^7;void func1(){register char data i;i = 5;do{sigl = !sigl;}while(--i);}你会说,这个函数没什么特别的嘛!呵呵,别着急,你将它编译了,然后展开汇编代码再看看:193: void func1(){194: register char data i;195: i = 5;C:0x00C3 7F05 MOV R7,#0x05196: do{197: sigl = !sigl;C:0x00C5 B297 CPL sigl198: }while(--i);C:0x00C7 DFFC DJNZ R7,C:00C5199: }C:0x00C9 22 RET看清楚了没这个函数里用到了R7,却没有对R7进行保护!有人会跳起来了:这有什么值得奇怪的,因为上层函数里没用到R7啊.呵呵,你说的没错,但只说对了一半:事实上,KEIL编译器里作了约定,在调子函数前会尽可能释放掉所有寄存器.通常性况下,除了中断函数外,其它函数里都可以任意修改所有寄存器而无需先压栈保护(其实并不是这样,但现在暂时这样认为,饭要一口一口吃嘛,我很快会说到的).这个特性有什么用呢有!当我们调用任务切换函数时,要保护的对象里可以把所有的寄存器排除掉了,就是说,只需要保护堆栈即可!现在我们回过头来看看之前例子里的任务切换函数:void task_switch(){task_sp[task_id] = SP;}看到没,一个寄存器也没保护,展开汇编看看,的确没保护寄存器.好了,现在要给大家泼冷水了,看下面两个函数:void func1(){register char data i;i = 5;do{sigl = !sigl;}while(--i);}void func2(){register char data i;i = 5;do{func1();}while(--i);}父函数fun2()里调用func1(),展开汇编代码看看: 193: void func1(){194: register char data i;195: i = 5;C:0x00C3 7F05 MOV R7,#0x05 196: do{197: sigl = !sigl; C:0x00C5 B297 CPL sigl 198: }while(--i);C:0x00C7 DFFC DJNZ R7,C:00C5 199: }C:0x00C9 22 RET200: void func2(){201: register char data i;202: i = 5;C:0x00CA 7E05 MOV R6,#0x05 203: do{204: func1();C:0x00CC 11C3 ACALL func1(C:00C3) 205: }while(--i);C:0x00CE DEFC DJNZ R6,C:00CC206: }C:0x00D0 22 RET看清楚没函数func2()里的变量使用了寄存器R6,而在func1和func2里都没保护.听到这里,你可能又要跳一跳了:func1()里并没有用到R6,干嘛要保护没错,但编译器是怎么知道func1()没用到R6的呢是从调用关系里推测出来的.一点都没错,KEIL会根据函数间的直接调用关系为各函数分配寄存器,既不用保护,又不会冲突,KEIL好棒哦!!等一下,先别高兴,换到多任务的环境里再试试:void func1(){register char data i;i = 5;do{sigl = !sigl;}while(--i);}void func2(){register char data i;i = 5;do{sigl = !sigl;}while(--i);}展开汇编代码看看:193: void func1(){194: register char data i;195: i = 5;C:0x00C3 7F05 MOV R7,#0x05 196: do{197: sigl = !sigl; C:0x00C5 B297 CPL sigl 198: }while(--i);C:0x00C7 DFFC DJNZ R7,C:00C5 199: }C:0x00C9 22 RET200: void func2(){201: register char data i;202: i = 5;C:0x00CA 7F05 MOV R7,#0x05203: do{204: sigl = !sigl;C:0x00CC B297 CPL sigl205: }while(--i);C:0x00CE DFFC DJNZ R7,C:00CC206: }C:0x00D0 22 RET看到了吧哈哈,这回神仙也算不出来了.因为两个函数没有了直接调用的关系,所以编译器认为它们之间不会产生冲突,结果分配了一对互相冲突的寄存器,当任务从func1()切换到func2()时,func1()中的寄存器内容就给破坏掉了.大家可以试着去编译一下下面的程序:sbit sigl = P1^7;void func1(){register char data i;i = 5;do{sigl = !sigl;task_switch();} while (--i);}void func2(){register char data i;i = 5;do{sigl = !sigl;task_switch();}while(--i);}我们这里只是示例,所以仍可以通过手工分配不同的寄存器避免寄存器冲突,但在真实的应用中,由于任务间的切换是非常随机的,我们无法预知某个时刻哪个寄存器不会冲突,所以分配不同寄存器的方法不可取.那么,要怎么办呢这样就行了:sbit sigl = P1^7;void func1(){static char data i;while(1){i = 5;do{sigl = !sigl;task_switch();}while(--i);}}void func2(){static char data i;while(1){i = 5;do{sigl = !sigl;task_switch();}while(--i);}}将两个函数中的变量通通改成静态就行了.还可以这么做:sbit sigl = P1^7;void func1(){register char data i;while(1){i = 5;do{sigl = !sigl;}while(--i);task_switch();}}void func2(){register char data i;while(1){i = 5;do{sigl = !sigl;}while(--i);task_switch();}}即,在变量的作用域内不切换任务,等变量用完了,再切换任务.此时虽然两个任务仍然会互相破坏对方的寄存器内容,但对方已经不关心寄存器里的内容了.以上所说的,就是"变量覆盖"的问题.现在我们系统地说说关于"变量覆盖".变量分两种,一种是全局变量,一种是局部变量(在这里,寄存器变量算到局部变量里).对于全局变量,每个变量都会分配到单独的地址.而对于局部变量,KEIL会做一个"覆盖优化",即没有直接调用关系的函数的变量共用空间.由于不是同时使用,所以不会冲突,这对内存小的51来说,是好事.但现在我们进入多任务的世界了,这就意味着两个没有直接调用关系的函数其实是并列执行的,空间不能共用了.怎么办呢一种笨办法是关掉覆盖优化功能.呵呵,的确很笨.比较简单易行一个解决办法是,不关闭覆盖优化,但将那些在作用域内需要跨越任务(换句话说就是在变量用完前会调用task_switch()函数的)变量通通改成静态(static)即可.这里要对初学者提一下,"静态"你可以理解为"全局",因为它的地址空间一直保留,但它又不是全局,它只能在定义它的那个花括号对{}里访问.静态变量有个副作用,就是即使函数退出了,仍会占着内存.所以写任务函数的时候,尽量在变量作用域结束后才切换任务,除非这个变量的作用域很长(时间上长),会影响到其它任务的实时性.只有在这种情况下才考虑在变量作用域内跨越任务,并将变量申明为静态.事实上,只要编程思路比较清析,很少有变量需要跨越任务的.就是说,静态变量并不多.说完了"覆盖"我们再说说"重入".所谓重入,就是一个函数在同一时刻有两个不同的进程复本.对初学者来说可能不好理解,我举个例子吧:有一个函数在主程序会被调用,在中断里也会被调用,假如正当在主程序里调用时,中断发生了,会发生什么情况void func1(){static char data i;i = 5;do{sigl = !sigl;}while(--i);}假定func1()正执行到i=3时,中断发生,一旦中断调用到func1()时,i的值就被破坏了,当中断结束后,i == 0.以上说的是在传统的单任务系统中,所以重入的机率不是很大.但在多任务系统中,很容易发生重入,看下面的例子:void func1()....delay();....}void func2(){....delay();....}void delay(){static unsigned char i;申明为static会发生重入问题.麻烦啊for(i=0;i<10;i++)task_switch();两个并行执行的任务都调用了delay(),这就叫重入.问题在于重入后的两个复本都依赖变量i来控制循环,而该变量跨越了任务,这样,两个任务都会修改i值了.重入只能以防为主,就是说尽量不要让重入发生,比如将代码改成下面的样子:#define delay() {static unsigned char i; for(i=0;i<10;i++)task_switch();}void func1(){....delay();....}void func2(){....delay();....用宏来代替函数,就意味着每个调用处都是一个独立的代码复本,那么两个delay实际使用的内存地址也就不同了,重入问题消失.但这种方法带来的问题是,每调用一次delay(),都会产生一个delay的目标代码,如果delay的代码很多,那就会造成大量的rom空间占用.有其它办法没本人所知有限,只有最后一招了:void delay() reentrant{unsigned char i;for(i=0;i<10;i++)task_switch();}加入reentrant申明后,该函数就可以支持重入.但小心使用,申明为重入后,函数效率极低!最后附带说下中断.因为没太多可说的,就不单独开章了.中断跟普通的写法没什么区别,只不过在目前所示例的多任务系统里因为有堆栈的压力,所以要使用using来减少对堆栈的使用(顺便提下,也不要调用子函数,同样是为了减轻堆栈压力)用using,必须用#pragma NOAREGS关闭掉绝对寄存器访问,如果中断里非要调用函数,连同函数也要放在#pragma NOAREGS的作用域内.如例所示:#pragma SAVE#pragma NOAREGS 是说,如果你在不用中断时任务栈深定为8的话,现在就要定为8+4 = 12了.另外说句废话,中断里处理的事一定要少,做个标记就行了,剩下的事交给对应的任务去处理.现在小结一下:切换任务时要保证没有寄存器跨越任务,否则产生任务间寄存器覆盖. 使用静态变量解决切换任务时要保证没有变量跨越任务,否则产生任务间地址空间(变量)覆盖. 使用静态变量解决两个不同的任务不要调用同时调用同一个函数,否则产生重入覆盖. 使用重入申明解决。
51单片机原理与应用
51单片机原理与应用51单片机是一种常用的单片机,其原理和应用十分广泛。
本文将从原理、结构、工作原理、应用领域等方面进行介绍。
一、原理和结构51单片机是指Intel公司推出的一种8位单片机,其核心是8051系列的芯片。
它具有高度集成、低功耗、易于编程等特点。
51单片机的结构包括中央处理器、存储器、输入输出端口、定时器计数器、串行通信接口等部分。
其中,中央处理器是51单片机的核心,负责执行各种指令和控制整个系统的运行。
二、工作原理51单片机的工作原理是通过执行存储在存储器中的指令来完成各种功能。
它通过中央处理器获取指令,然后根据指令的要求进行相应的操作。
51单片机的指令由操作码和操作数组成,操作码表示要执行的操作,操作数表示操作的对象。
通过不同的指令和操作数的组合,可以实现各种功能,如输入输出控制、定时器计数、串行通信等。
三、应用领域由于51单片机具有体积小、功耗低、成本低等优势,因此在各个领域都有广泛的应用。
以下是几个常见的应用领域:1. 嵌入式系统:51单片机可以用于控制各种嵌入式系统,如家电、智能家居、机器人等。
通过编程控制,可以实现各种功能,如温度控制、灯光控制、运动控制等。
2. 工业自动化:51单片机可以用于工业控制系统,如自动化生产线、仪器仪表等。
通过与传感器、执行器等设备的连接,可以实现对生产过程的监控与控制。
3. 通信设备:51单片机可以用于各种通信设备,如无线模块、蓝牙模块等。
通过与通信模块的配合,可以实现无线通信、数据传输等功能。
4. 汽车电子:51单片机可以用于汽车电子控制系统,如发动机控制单元、车身电子控制单元等。
通过编程控制,可以实现对汽车各个系统的监控与控制。
5. 教育领域:由于51单片机易于学习和应用,因此在教育领域也有广泛的应用。
学生可以通过实践操作,了解单片机的工作原理和应用,提高动手能力和创新思维。
51单片机是一种应用广泛的单片机,它具有高度集成、低功耗、易于编程等特点。
51单片机的工作原理
51单片机的工作原理
51单片机是一种高性能、低功耗的微控制器。
它采用先进的CMOS工艺制造,内部集成了中央处理器(CPU)、存储器、输入输出(I/O)端口以及定时器等功能模块。
在工作时,51单片机首先通过外部晶体振荡器提供时钟信号,驱动CPU执行指令。
CPU根据程序计数器(PC)中的地址,
从存储器中读取指令,然后逐条执行。
指令可以包括数据处理、控制流程、IO操作等多种功能。
存储器分为程序存储器(ROM)和数据存储器(RAM)。
ROM存储了程序的指令和常量数据,而RAM用于存储程序
执行过程中产生的临时数据。
CPU可以通过地址总线将指令
的地址发送到ROM或RAM中,获取相应的数据。
输入输出端口用于与外部设备进行通信。
它们可以作为输入口接收外部信号,或者作为输出口发送信号给外部设备。
单片机通过向I/O端口写入或读取数据来实现与外设的交互。
定时器是单片机的另一个重要模块。
它可以生成精确的时间延迟,或者通过计数脉冲得到一段时间的长度。
定时器常用于时间测量、定时中断等应用。
在工作过程中,51单片机还会通过中断机制实现多任务处理。
当发生某种特定的事件,如外部中断、定时器中断等,单片机会暂时中断正在执行的指令,转而执行相应的中断服务程序。
中断是提高系统响应速度和处理效率的重要手段。
总之,51单片机通过CPU、存储器、输入输出端口和定时器等模块的协同工作,实现了程序的运行和与外部设备的交互。
它具有较高的性能和可编程性,广泛应用于嵌入式系统、自动控制等领域。
51单片机原理
51单片机原理51单片机是一种广泛应用于嵌入式系统中的微控制器,它具有成本低、性能稳定、易于编程等特点,因此在电子产品中得到了广泛的应用。
本文将介绍51单片机的原理及其工作方式,希望能够帮助读者更好地理解和应用这一技术。
首先,我们来了解一下51单片机的基本原理。
51单片机是由哈佛结构的存储器组成的,包括程序存储器和数据存储器。
程序存储器用于存储程序代码,而数据存储器用于存储数据。
在51单片机中,程序存储器和数据存储器是分开的,这样可以在程序执行的同时进行数据的读写操作,提高了系统的效率。
其次,51单片机的工作方式是怎样的呢?在51单片机中,有一个时钟电路用于控制系统的时序,以确保各个部件按照正确的顺序进行工作。
当系统上电后,时钟电路开始工作,51单片机开始执行程序。
程序执行过程中,单片机会根据程序存储器中的指令逐条执行,同时进行数据的读写操作,最终完成所需的功能。
除此之外,51单片机还具有丰富的外设接口,可以连接各种传感器、执行器等外部设备,实现更加复杂的功能。
通过外部设备的输入和输出,51单片机可以实现与外部环境的交互,从而应用到各种嵌入式系统中,如智能家居、工业控制、汽车电子等领域。
在实际应用中,程序的编写是至关重要的。
51单片机的程序通常使用汇编语言或C语言进行编写,开发工具包括Keil、IAR等集成开发环境。
程序的编写需要充分考虑51单片机的特性和外部设备的接口,合理设计程序结构,确保程序的稳定性和可靠性。
总的来说,51单片机是一种功能强大、应用广泛的微控制器,它的原理和工作方式对于理解和应用嵌入式系统技术具有重要意义。
希望通过本文的介绍,读者能够对51单片机有更深入的了解,为实际应用提供帮助。
以上就是关于51单片机原理的介绍,希望能够对大家有所帮助。
如果你对51单片机还有其他问题,欢迎继续探讨和交流。
51单片机原理
51单片机原理51单片机是一种常见的微型计算机,广泛应用于各种电子设备中。
它具有体积小、功耗低、性能稳定等特点,因此备受工程师和电子爱好者的青睐。
本文将介绍51单片机的原理及其应用。
首先,我们来了解一下51单片机的基本原理。
51单片机是一种基于哈佛结构的微型计算机,它包含CPU、存储器、输入输出接口等基本部件。
其中,CPU是单片机的核心部件,它负责执行各种指令,控制整个系统的运行。
存储器用于存储程序和数据,输入输出接口则负责与外部设备进行通信。
通过这些基本部件的协作,51单片机能够完成各种复杂的任务,如控制电机、采集传感器数据等。
其次,我们来看一下51单片机的工作原理。
在实际应用中,我们通常需要编写程序并将其下载到单片机中,以实现特定的功能。
编写程序需要使用特定的集成开发环境(IDE),如Keil、IAR等。
通过IDE,我们可以编写程序、进行调试,并将程序下载到单片机中。
一旦程序下载完成,单片机就会按照程序中的指令执行相应的操作,从而实现我们预期的功能。
除此之外,51单片机还具有丰富的外设接口,如串口、定时器、中断等。
这些外设接口可以帮助我们实现更加复杂的功能,如与PC进行通信、定时采集数据、实现实时控制等。
通过合理地配置这些外设接口,我们可以充分发挥51单片机的性能,实现各种应用需求。
最后,我们来谈一下51单片机的应用。
由于其体积小、功耗低、性能稳定等特点,51单片机在各种电子设备中都有广泛的应用。
比如,智能家居领域中的智能插座、智能灯具等产品,工业控制领域中的自动化设备、传感器采集系统等产品,都离不开51单片机的支持。
另外,51单片机还被广泛应用于教育领域,用于教学和科研。
综上所述,51单片机是一种功能强大、应用广泛的微型计算机,它的原理和应用涉及到计算机、电子、自动控制等多个领域。
通过学习和掌握51单片机的原理,我们可以更好地应用它于实际项目中,实现各种有趣的功能。
希望本文能够帮助读者更好地理解和应用51单片机,同时也欢迎大家在实践中不断探索和创新。
单片机多任务处理技术与实现方法介绍
单片机多任务处理技术与实现方法介绍随着科技的发展,单片机在嵌入式系统中的应用越来越广泛。
在许多嵌入式系统中,同时需要处理多个任务,如控制系统、通信系统、传感器数据采集等。
为了有效地处理这些任务,单片机多任务处理技术应运而生。
本文将介绍单片机多任务处理技术的原理和实现方法。
一、单片机多任务处理技术的原理多任务处理是指单片机系统同时处理(或者说切换)多个任务的能力。
多任务处理分为并行处理和时间分片处理两种方式。
1.1 并行处理并行处理是指多个任务在同一时刻同时执行,每个任务分配到独立的硬件资源,如独立的处理器核心或者独立的处理周期。
这种方式适用于对响应时间要求非常高的系统,如航空航天控制系统和军事装备等。
然而,由于单片机资源和性能的限制,常见的单片机很难实现真正的并行处理。
因此,在单片机多任务处理中,更常用的是时间分片处理方式。
1.2 时间分片处理时间分片处理是指多个任务按照一定的时间片轮流执行,每个任务在一个时间片内执行一段时间,然后切换到下一个任务。
这种方式适用于对响应时间要求较为适中的系统。
单片机通过在不同的时间片中不断切换任务,实现了多个任务同时进行。
这种方式可以使单片机处理多个任务的效率更高,提高系统的响应速度和效率。
二、单片机多任务处理的实现方法单片机多任务处理技术的实现方法主要有协作式调度和抢占式调度两种方式。
2.1 协作式调度协作式调度是指任务的切换由任务自身控制,任务在执行期间必须主动释放控制权,让其他任务执行。
这种方式要求任务具有良好的协作精神,能够自觉地释放控制权。
实现协作式调度的方法有两种:由任务主动调用切换函数和由中断触发切换。
在任务主动调用切换函数的方法中,每个任务执行一段时间后,主动调用切换函数将控制权交给下一个任务。
这种方法简单易用,但存在一个任务运行时间过长可能导致其他任务无法得到执行的问题。
另一种方法是由中断触发切换。
每个任务执行期间注册一个中断服务函数,当中断发生时,切换到下一个任务执行。
单片机系统中的多任务多线程机制的实现
单片机系统中的多任务多线程机制的实现
随着单片机应用场景的日益广泛,单片机系统的需求也越来越高,越来越复杂。
在这个背景下,单片机系统的多任务多线程机制成了越
来越必要的一项功能。
在单片机系统中,多任务多线程机制能够使得系统能够同时处理
多个任务,从而提高系统的效率和性能。
它可以将系统的各种功能模
块分别处理,使得各个功能模块相互独立,互不干扰。
这样,当每个
模块负责完自己的任务后,就可以将结果传递给下一个模块进行处理。
这样的方式能够更快地完成各个任务,从而提高系统的响应速度和效率。
在单片机系统中,多任务多线程机制的实现需要使用一些相关的
技术。
首先,我们需要使用中断来实现多任务机制。
当单片机在处理
某个任务时,如果需要处理其他任务,可以通过中断来实现切换任务,以便更快地完成各个任务。
其次,我们需要使用任务队列来实现多线
程机制。
任务队列可以将各个任务按照优先级进行排列,从而完成一
个快速的任务切换过程。
最后,我们也需要考虑系统的实时性,以保
证每个任务能够在我们需要的时间内完成处理。
总体来看,单片机系统中的多任务多线程机制可以充分提高系统
的效率和性能,保证各个任务的正常进行,为用户提供更加稳定、快速、高效的服务。
在实际使用中,我们需要根据系统的具体需求,综
合考虑各种因素,选择适合的多任务多线程机制方案,以保证系统的稳定性和可靠性。
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
51单片机多任务操作系统的原理与实现51单片机多任务操作系统的原理与实现-- 一个超轻量级的操作系统前言想了很久,要不要写这篇文章?最后觉得对操作系统感兴趣的人还是很多,写吧.我不一定能造出玉,但我可以抛出砖.包括我在的很多人都对51使用操作系统呈悲观态度,因为51的片上资源太少.但对于很多要求不高的系统来说,使用操作系统可以使代码变得更直观,易于维护,所以在51上仍有操作系统的生存机会.流行的uCos,Tiny51等,其实都不适合在2051这样的片子上用,占资源较多,唯有自已动手,以不变应万变,才能让51也有操作系统可用.这篇贴子的目的,是教会大家如何现场写一个OS,而不是给大家提供一个OS版本.提供的所有代码,也都是示例代码,所以不要因为它没什么功能就说LAJI之类的话.如果把功能写全了,一来估计你也不想看了,二来也失去灵活性没有价值了.下面的贴一个示例出来,可以清楚的看到,OS本身只有不到10行源代码,编译后的目标代码60字节,任务切换消耗为20个机器周期.相比之下,KEIL嵌的TINY51目标代码为800字节,切换消耗100~700周期.唯一不足之处是,每个任务要占用掉十几字节的堆栈,所以任务数不能太多,用在128B存的51里有点难度,但对于52来说问题不大.这套代码在36M主频的STC12C4052上实测,切换任务仅需2uS. #include <reg51.h>#define MAX_TASKS 2 //任务槽个数.必须和实际任务数一至#define MAX_TASK_DEP 12 //最大栈深.最低不得少于2个,保守值为12.unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP]; //任务堆栈.unsigned char task_id; //当前活动任务号//任务切换函数(任务调度器)void task_switch(){task_sp[task_id] = SP;if(++task_id == MAX_TASKS)task_id = 0;SP = task_sp[task_id];}//任务装入函数.将指定的函数(参数1)装入指定(参数2)的任务槽中.如果该槽中原来就有任务,则原任务丢失,但系统本身不会发生错误.void task_load(unsigned int fn, unsigned char tid){task_sp[tid] = task_stack[tid] + 1;task_stack[tid][0] = (unsigned int)fn & 0xff;task_stack[tid][1] = (unsigned int)fn >> 8;}//从指定的任务开始运行任务调度.调用该宏后,将永不返回.#define os_start(tid) {task_id = tid,SP = task_sp[tid];return;}/*======================以下为测试代码======================*/void task1(){static unsigned char i;while(1){i++;task_switch(); //编译后在这里打上断点}}void task2(){static unsigned char j;while(1){j+=2;task_switch(); //编译后在这里打上断点}}void main(){//这里装载了两个任务,因此在定义MAX_TASKS时也必须定义为2task_load(task1, 0); //将task1函数装入0号槽task_load(task2, 1); //将task2函数装入1号槽os_start(0);}这样一个简单的多任务系统虽然不能称得上真正的操作系统,但只要你了解了它的原理,就能轻易地将它扩展得非常强大,想知道要如何做吗?一.什么是操作系统?人脑比较容易接受"类比"这种表达方式,我就用"公交系统"来类比"操作系统"吧.当我们要解决一个问题的时候,是用某种处理手段去完成它,这就是我们常说的"方法",计算机里叫"程序"(有时候也可以叫它"算法").以出行为例,当我们要从A地走到B地的时候,可以走着去,也可以飞着去,可以走直线,也可以绕弯路,只要能从A地到B地,都叫作方法.这种从A地到B的需求,相当于计算机里的"任务",而实现从A地到B地的方法,叫作"任务处理流程" 很显然,这些走法中,并不是每种都合理,有些傻子都会采用的,有些是傻子都不采会用的.用计算机的话来说就是,有的任务处理流程好,有的任务处理流程好,有的处理流程差.可以归纳出这么几种真正算得上方法的方法:有些走法比较快速,适合于赶时间的人;有些走法比较省事,适合于懒人;有些走法比较便宜,适合于穷人.用计算机的话说就是,有些省CPU,有些流程简单,有些对系统资源要求低.现在我们可以看到一个问题:如果全世界所有的资源给你一个人用(单任务独占全部资源),那最适合你需求的方法就是好方法.但事实上要外出的人很多,例如10个人(10个任务),却只有1辆车(1套资源),这叫作"资源争用".如果每个人都要使用最适合他需求的方法,那司机就只好给他们一人跑一趟了,而在任一时刻里,车上只有一个乘客.这叫作"顺序执行",我们可以看到这种方法对系统资源的浪费是严重的.如果我们没有法力将1台车变成10台车来送这10个人,就只好制定一些机制和约定,让1台车看起来像10台车,来解决这个问题的办法想必大家都知道,那就是制定公交线路.最简单的办法是将所有旅客需要走的起点与终点串成一条线,车在这条线上开,乘客则自已决定上下车.这就是最简单的公交线路.它很差劲,但起码解决客人们对车争用.对应到计算机里,就是把所有任务的代码混在一起执行.这样做既不优异雅,也没效率,于是司机想了个办法,把这些客户叫到一起商量,将所有客人出行的起点与终点罗列出来,统计这些线路的使用频度,然后制定出公交线路:有些路线可以合并起来成为一条线路,而那些不能合并的路线,则另行开辟行车车次,这叫作"任务定义".另外,对于人多路线,车次排多点,时间上也优先安排,这叫作"任务优先级".经过这样的安排后,虽然仍只有一辆车,但运载能力却大多了.这套车次/路线的按排,就是一套"公交系统".哈,知道什么叫操作系统了吧?它也就是这么样的一种约定.操作系统:我们先回过头归纳一下:汽车系统资源.主要指的是CPU,当然还有其它,比如存,定时器,中断源等.客户出行任务正在走的路线进程一个一个的运送旅客顺序执行同时运送所有旅客多任务并行按不同的使用频度制定路线并优先跑较繁忙的路线任务优先级计算机有各种资源,单从硬件上说,就有CPU,存,定时器,中断源,I/O端口等.而且还会派生出来很多软件资源,例如消息池.操作系统的存在,就是为了让这些资源能被合理地分配.最后我们来总结一下,所谓操作系统,以我们目前权宜的理解就是:为"解决计算机资源争用而制定出的一种约定".二.51上的操作系统对于一个操作系统来说,最重要的莫过于并行多任务.在这里要澄清一下,不要拿当年的DOS来说事,时代不同了.况且当年IBM和小比尔着急将PC搬上市,所以才抄袭PLM(好象是叫这个名吧?记不太清)搞了个今天看来很"粗制滥造"的DOS出来.看看当时真正的操作系统---UNIX,它还在纸上时就已经是多任务的了.对于我们PC来说,要实现多任务并不是什么问题,但换到MCU却很头痛:1.系统资源少在PC上,CPU主频以G为单位,存以GB为单位,而MCU的主频通常只有十几M,存则是Byts.在这么少的资源上同时运行多个任务,就意味着操作系统必须尽可能的少占用硬件资源.2.任务实时性要求高PC并不需要太关心实时性,因为PC上几乎所有的实时任务都被专门的硬件所接管,例如所有的声卡网卡显示上都置有DSP以及大量的缓存.CPU只需坐在那里指手划脚告诉这些板卡如何应付实时信息就行了.而MCU不同,实时信息是靠CPU来处理的,缓存也非常有限,甚至没有缓存.一旦信息到达,CPU必须在极短的时间响应,否则信息就会丢失.就拿串口通信来举例,在标准的PC架构里,巨大的存允许将信息保存足够长的时间.而对于MCU来说存有限,例如51仅有128字节存,还要扣除掉寄存器组占用掉的8~32个字节,所以通常都仅用几个字节来缓冲.当然,你可以将数据的接收与处理的过程合并,但对于一个操作系统来说,不推荐这么做.假定以115200bps通信速率向MCU传数据,则每个字节的传送时间约为9uS,假定缓存为8字节,则串口处理任务必须在70uS响应.这两个问题都指向了同一种解决思路:操作系统必须轻量轻量再轻量,最好是不占资源(那当然是做梦啦).可用于MCU的操作系统很多,但适合51(这里的51专指无扩展存的51)几乎没有.前阵子见过一个"圈圈操作系统",那是我所见过的操作系统里最轻量的,但仍有改进的余地.很多人认为,51根本不适合使用操作系统.其实我对这种说法并不完全接受,否则也没有这篇文章了.我的看法是,51不适合采用"通用操作系统".所谓通用操作系统就是,不论你是什么样的应用需求,也不管你用什么芯片,只要你是51,通通用同一个操作系统. 这种想法对于PC来说没问题,对于嵌入式来说也不错,对AVR来说还凑合,而对于51这种"贫穷型"的MCU来说,不行.怎样行?量体裁衣,现场根据需求构建一个操作系统出来!看到这里,估计很多人要翻白眼了,大体上两种:1.操作系统那么复杂,说造就造,当自已是神了?2.操作系统那么复杂,现场造一个会不会出BUG?哈哈,看清楚了?问题出在"复杂"上面,如果操作系统不复杂,问题不就解决了? 事实上,很多人对操作系统的理解是片面的,操作系统不一定要做得很复杂很全面,就算仅个多任务并行管理能力,你也可以称它操作系统.只要你对多任务并行的原理有所了解,就不难现场写一个出来,而一旦你做到了这一点,为各任务间安排通信约定,使之发展成一个为你的应用系统量身定做的操作系统也就不难了.为了加深对操作系统的理解,可以看一看<<演变>>这份PPT,让你充分了解一个并行多任务是如何一步步从顺序流程演变过来的.里面还提到了很多人都在用的"状态机",你会发现操作系统跟状态机从原理上其实是多么相似.会用状态机写程序,都能写出操作系统.三.我的第一个操作系统直接进入主题,先贴一个操作系统的示出来.大家可以看到,原来操作系统可以做得么简单.当然,这里要申明一下,这玩意儿其实算不上真正的操作系统,它除了并行多任务并行外根本没有别的功能.但凡事都从简单开始,搞懂了它,就能根据应用需求,将它扩展成一个真正的操作系统.好了,代码来了.将下面的代码直接放到KEIL里编译,在每个task?()函数的"task_switch();"那里打上断点,就可以看到它们的确是"同时"在执行的.#include <reg51.h>#define MAX_TASKS 2 //任务槽个数.必须和实际任务数一至#define MAX_TASK_DEP 12 //最大栈深.最低不得少于2个,保守值为12.unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP];//任务堆栈. unsigned char task_id; //当前活动任务号//任务切换函数(任务调度器)void task_switch(){task_sp[task_id] = SP;if(++task_id == MAX_TASKS)task_id = 0;SP = task_sp[task_id];}//任务装入函数.将指定的函数(参数1)装入指定(参数2)的任务槽中.如果该槽中原来就有任务,则原任务丢失,但系统本身不会发生错误.void task_load(unsigned int fn, unsigned char tid){task_sp[tid] = task_stack[tid] + 1;task_stack[tid][0] = (unsigned int)fn & 0xff;task_stack[tid][1] = (unsigned int)fn >> 8;}//从指定的任务开始运行任务调度.调用该宏后,将永不返回.#define os_start(tid) {task_id = tid,SP = task_sp[tid];return;}/*==================以下为测试代码=====================*/void task1(){static unsigned char i;while(1){i++;task_switch();//编译后在这里打上断点}}void task2(){static unsigned char j;while(1){j+=2;task_switch();//编译后在这里打上断点}}void main(){//这里装载了两个任务,因此在定义MAX_TASKS时也必须定义为2task_load(task1, 0);//将task1函数装入0号槽task_load(task2, 1);//将task2函数装入1号槽os_start(0);}限于篇幅我已经将代码作了简化,并删掉了大部分注释,大家可以直接下载源码包,里面完整的注解,并带KEIL工程文件,断点也打好了,直接按ctrl+f5就行了. 现在来看看这个多任务系统的原理:这个多任务系统准确来说,叫作"协同式多任务".所谓"协同式",指的是当一个任务持续运行而不释放资源时,其它任务是没有任何机会和方式获得运行机会,除非该任务主动释放CPU.在本例里,释放CPU是靠task_switch()来完成的.task_switch()函数是一个很特殊的函数,我们可以称它为"任务切换器".要清楚任务是如何切换的,首先要回顾一下堆栈的相关知识.有个很简单的问题,因为它太简单了,所以相信大家都没留意过:我们知道,不论是CALL还是JMP,都是将当前的程序流打断,请问CALL和JMP的区别是什么?你会说:CALL可以RET,JMP不行.没错,但原因是啥呢?为啥CALL过去的就可以用RET跳回来,JMP过去的就不能用RET来跳回呢?很显然,CALL通过某种方法保存了打断前的某些信息,而在返回断点前执行的RET指令,就是用于取回这些信息.不用多说,大家都知道,"某些信息"就是PC指针,而"某种方法"就是压栈.很幸运,在51里,堆栈及堆栈指针都是可被任意修改的,只要你不怕死.那么假如在执行RET前将堆栈修改一下会如何?往下看:当程序执行CALL后,在子程序里将堆栈刚才压入的断点地址清除掉,并将一个函数的地址压入,那么执行完RET后,程序就跳到这个函数去了.事实上,只要我们在RET前将堆栈改掉,就能将程序跳到任务地方去,而不限于CALL里压入的地址.重点来了......首先我们得为每个任务单独开一块存,这块存专用于作为对应的任务的堆栈,想将CPU交给哪个任务,只需将栈指针指向谁存块就行了.接下来我们构造一个这样的函数:当任务调用该函数时,将当前的堆栈指针保存一个变量里,并换上另一个任务的堆栈指针.这就是任务调度器了.OK了,现在我们只要正确的填充好这几个堆栈的原始容,再调用这个函数,这个任务调度就能运行起来了.那么这几个堆栈里的原始容是哪里来的呢?这就是"任务装载"函数要干的事了. 在启动任务调度前将各个任务函数的入口地址放在上面所说的"任务专用的存块"里就行了!对了,顺便说一下,这个"任务专用的存块"叫作"私栈",私栈的意思就是说,每个任务的堆栈都是私有的,每个任务都有一个自已的堆栈.话都说到这份上了,相信大家也明白要怎么做了:1.分配若干个存块,每个存块为若干字节:这里所说的"若干个存块"就是私栈,要想同时运行几少个任务就得分配多少块.而"每个子存块若干字节"就是栈深.记住,每调一层子程序需要2字节.如果不考虑中断,4层调用深度,也就是8字节栈深应该差不多了.unsigned char idata task_stack[MAX_TASKS][MAX_TASK_DEP]当然,还有件事不能忘,就是堆指针的保存处.不然光有堆栈怎么知道应该从哪个地址取数据啊unsigned char idata task_sp[MAX_TASKS]上面两项用于装任务信息的区域,我们给它个概念叫"任务槽".有些人叫它"任务堆",我觉得还是"槽"比较直观对了,还有任务号.不然怎么知道当前运行的是哪个任务呢?unsigned char task_id当前运行存放在1号槽的任务时,这个值就是1,运行2号槽的任务时,这个值就是2....2.构造任务调度函函数:void task_switch(){task_sp[task_id] = SP; //保存当前任务的栈指针if(++task_id == MAX_TASKS) //任务号切换到下一个任务task_id = 0;SP = task_sp[task_id]; //将系统的栈指针指向下个任务的私栈.}3.装载任务:将各任务的函数地址的低字节和高字节分别入在task_stack[任务号][0]和task_stack[任务号][1]中:为了便于使用,写一个函数: task_load(函数名, 任务号)void task_load(unsigned int fn, unsigned char tid){task_sp[tid] = task_stack[tid] + 1;task_stack[tid][0] = (unsigned int)fn & 0xff;task_stack[tid][1] = (unsigned int)fn >> 8;}4.启动任务调度器:将栈指针指向任意一个任务的私栈,执行RET指令.注意,这可很有学问的哦,没玩过堆栈的人脑子有点转不弯:这一RET,RET到哪去了?嘿嘿,别忘了在RET前已经将堆栈指针指向一个函数的入口了.你别把RET看成RET,你把它看成是另一种类型的JMP就好理解了.SP = task_sp[任务号];return;做完这4件事后,任务"并行"执行就开始了.你可以象写普通函数一个写任务函数,只需(目前可以这么说)注意在适当的时候(例如以前调延时的地方)调用一下task_switch(),以让出CPU控制权给别的任务就行了.最后说下效率问题.这个多任务系统的开销是每次切换消耗20个机器周期(CALL和RET都算在了),贵吗?不算贵,对于很多用状态机方式实现的多任务系统来说,其实效率还没这么高--- case switch和if()可不像你想像中那么便宜.关于存的消耗我要说的是,当然不能否认这种多任务机制的确很占存.但建议大家不要老盯着编译器下面的那行字"DATA = XXXbyte".那个值没意义,堆栈没算进去.关于比较省存多任务机制,我将来会说到.概括来说,这个多任务系统适用于实时性要求较高而存需求不大的应用场合,我在运行于36M主频的STC12C4052上实测了一把,切换一个任务不到3微秒. 下回我们讲讲用KEIL写多任务函数时要注意的事项.下下回我们讲讲如何增强这个多任务系统,跑步进入操作系统时代.四.用KEIL写多任务系统的技巧与注意事项C51编译器很多,KEIL是其中比较流行的一种.我列出的所有例子都必须在KEIL 中使用.为何?不是因为KEIL好所以用它(当然它的确很棒),而是因为这里面用到了KEIL的一些特性,如果换到其它编译器下,通过编译的倒不是问题,但运行起来可能是堆栈错位,上下文丢失等各种要命的错误,因为每种编译器的特性并不相同.所以在这里先说清楚这一点.但是,我开头已经说了,这套帖子的主要目的是阐述原理,只要你能把这几个例子消化掉,那么也能够自已动手写出适合其它编译器的OS.好了,说说KEIL的特性吧,先看下面的函数:sbit sigl = P1^7;void func1(){register char data i;i = 5;do{sigl = !sigl;}while(--i);}你会说,这个函数没什么特别的嘛!呵呵,别着急,你将它编译了,然后展开汇编代码再看看:193: void func1(){194: register char data i;195: i = 5;C:0x00C3 7F05 MOV R7,#0x05196: do{197: sigl = !sigl;C:0x00C5 B297 CPL sigl(0x90.7) 198: }while(--i);C:0x00C7 DFFC DJNZ R7,C:00C5199: }C:0x00C9 22 RET看清楚了没?这个函数里用到了R7,却没有对R7进行保护!有人会跳起来了:这有什么值得奇怪的,因为上层函数里没用到R7啊.呵呵,你说的没错,但只说对了一半:事实上,KEIL编译器里作了约定,在调子函数前会尽可能释放掉所有寄存器.通常性况下,除了中断函数外,其它函数里都可以任意修改所有寄存器而无需先压栈保护(其实并不是这样,但现在暂时这样认为,饭要一口一口吃嘛,我很快会说到的).这个特性有什么用呢?有!当我们调用任务切换函数时,要保护的对象里可以把所有的寄存器排除掉了,就是说,只需要保护堆栈即可!现在我们回过头来看看之前例子里的任务切换函数:void task_switch(){task_sp[task_id] = SP; //保存当前任务的栈指针if(++task_id == MAX_TASKS) //任务号切换到下一个任务task_id = 0;SP = task_sp[task_id]; //将系统的栈指针指向下个任务的私栈.}看到没,一个寄存器也没保护,展开汇编看看,的确没保护寄存器.好了,现在要给大家泼冷水了,看下面两个函数:void func1(){register char data i;i = 5;do{sigl = !sigl;}while(--i);}void func2(){register char data i;i = 5;do{func1();}while(--i);}父函数fun2()里调用func1(),展开汇编代码看看:193: void func1(){194: register char data i;195: i = 5;C:0x00C3 7F05 MOV R7,#0x05196: do{197: sigl = !sigl;C:0x00C5 B297 CPL sigl(0x90.7) 198: }while(--i);C:0x00C7 DFFC DJNZ R7,C:00C5199: }C:0x00C9 22 RET200: void func2(){201: register char data i;202: i = 5;C:0x00CA 7E05 MOV R6,#0x05203: do{204: func1();C:0x00CC 11C3 ACALL func1(C:00C3)205: }while(--i);C:0x00CE DEFC DJNZ R6,C:00CC206: }C:0x00D0 22 RET看清楚没?函数func2()里的变量使用了寄存器R6,而在func1和func2里都没保护.听到这里,你可能又要跳一跳了:func1()里并没有用到R6,干嘛要保护?没错,但编译器是怎么知道func1()没用到R6的呢?是从调用关系里推测出来的.一点都没错,KEIL会根据函数间的直接调用关系为各函数分配寄存器,既不用保护,又不会冲突,KEIL好棒哦!!等一下,先别高兴,换到多任务的环境里再试试: void func1(){register char data i;i = 5;do{sigl = !sigl;}while(--i);}void func2(){register char data i;i = 5;do{sigl = !sigl;}while(--i);}展开汇编代码看看:193: void func1(){194: register char data i;195: i = 5;C:0x00C3 7F05 MOV R7,#0x05196: do{197: sigl = !sigl;C:0x00C5 B297 CPL sigl(0x90.7)198: }while(--i);C:0x00C7 DFFC DJNZ R7,C:00C5199: }C:0x00C9 22 RET200: void func2(){201: register char data i;202: i = 5;C:0x00CA 7F05 MOV R7,#0x05203: do{204: sigl = !sigl;C:0x00CC B297 CPL sigl(0x90.7)205: }while(--i);C:0x00CE DFFC DJNZ R7,C:00CC206: }C:0x00D0 22 RET看到了吧?哈哈,这回神仙也算不出来了.因为两个函数没有了直接调用的关系,所以编译器认为它们之间不会产生冲突,结果分配了一对互相冲突的寄存器,当任务从func1()切换到func2()时,func1()中的寄存器容就给破坏掉了.大家可以试着去编译一下下面的程序:sbit sigl = P1^7;void func1(){register char data i;i = 5;do{sigl = !sigl;task_switch();} while (--i);}void func2(){register char data i;i = 5;do{sigl = !sigl;task_switch();}while(--i);}我们这里只是示例,所以仍可以通过手工分配不同的寄存器避免寄存器冲突,但在真实的应用中,由于任务间的切换是非常随机的,我们无法预知某个时刻哪个寄存器不会冲突,所以分配不同寄存器的方法不可取.那么,要怎么办呢?这样就行了:sbit sigl = P1^7;void func1(){static char data i;while(1){i = 5;do{sigl = !sigl;task_switch(); }while(--i);}}void func2(){static char data i;while(1){i = 5;do{sigl = !sigl;task_switch();}while(--i);}}将两个函数中的变量通通改成静态就行了.还可以这么做:sbit sigl = P1^7;void func1(){register char data i;while(1){i = 5;do{sigl = !sigl;}while(--i);task_switch();}}void func2(){register char data i;while(1){i = 5;do{sigl = !sigl;}while(--i);task_switch();}}即,在变量的作用域不切换任务,等变量用完了,再切换任务.此时虽然两个任务仍然会互相破坏对方的寄存器容,但对方已经不关心寄存器里的容了.以上所说的,就是"变量覆盖"的问题.现在我们系统地说说关于"变量覆盖".变量分两种,一种是全局变量,一种是局部变量(在这里,寄存器变量算到局部变量里).对于全局变量,每个变量都会分配到单独的地址.而对于局部变量,KEIL会做一个"覆盖优化",即没有直接调用关系的函数的变量共用空间.由于不是同时使用,所以不会冲突,这对存小的51来说,是好事.但现在我们进入多任务的世界了,这就意味着两个没有直接调用关系的函数其实是并列执行的,空间不能共用了.怎么办呢?一种笨办法是关掉覆盖优化功能.呵呵,的确很笨.比较简单易行一个解决办法是,不关闭覆盖优化,但将那些在作用域需要跨越任务(换句话说就是在变量用完前会调用task_switch()函数的)变量通通改成静态(static)即可.这里要对初学者提一下,"静态"你可以理解为"全局",因为它的地址空间一直保留,但它又不是全局,它只能在定义它的那个花括号对{}里访问. 静态变量有个副作用,就是即使函数退出了,仍会占着存.所以写任务函数的时候,尽量在变量作用域结束后才切换任务,除非这个变量的作用域很长(时间上长),会影响到其它任务的实时性.只有在这种情况下才考虑在变量作用域跨越任务,并将变量申明为静态.事实上,只要编程思路比较清析,很少有变量需要跨越任务的.就是说,静态变量并不多.说完了"覆盖"我们再说说"重入".所谓重入,就是一个函数在同一时刻有两个不同的进程复本.对初学者来说可能不好理解,我举个例子吧:有一个函数在主程序会被调用,在中断里也会被调用,假如正当在主程序里调用时,中断发生了,会发生什么情况?void func1(){static char data i;i = 5;do{sigl = !sigl;}while(--i);}假定func1()正执行到i=3时,中断发生,一旦中断调用到func1()时,i的值就被破坏了,当中断结束后,i == 0.以上说的是在传统的单任务系统中,所以重入的机率不是很大.但在多任务系统中,很容易发生重入,看下面的例子:void func1(){....delay();....}void func2(){....delay();....}void delay(){static unsigned char i;//注意这里是申明为static,不申明static的话会发生覆盖问题.而申明为static会发生重入问题.麻烦啊for(i=0;i<10;i++)task_switch();}两个并行执行的任务都调用了delay(),这就叫重入.问题在于重入后的两个复本都依赖变量i来控制循环,而该变量跨越了任务,这样,两个任务都会修改i值了.重入只能以防为主,就是说尽量不要让重入发生,比如将代码改成下面的样子:。