EDN首页   博客首页 用户登陆  |  注册
aaa
发表于 2009/5/20 17:06:17

0

关于投票

单片机控制32路舵机

利用STC12C5406+4个74164  单片机控制32路舵机

上图:点击看大图

点击看大图

 

点击看大图

有兴趣的朋友欢迎交流:QQ:437796990

系统分类: 单片机  |  用户分类: 单片机  |  标签: 无标签  |  来源: 无分类  | 

点击查看原文

发表评论 阅读全文(468) | 回复(0)

发表于 2008/6/24 10:28:19

3

关于投票

关于51单片机的上拉问题

关于51单片机P0口的结构及上拉问题
 

1.P0作为地址数据总线时,T1和T2是一起工作的,构成推挽结构。高电平时,T1打开,T2截止;低电平时,T1截止,T2打开。这种情况下不用外接上拉电阻.而且,当T1打开,T2截止,输出高电平的时候,因为内部电源直接通过T1输出到P0口线上,因此驱动能力(电流)可以很大,这就是为什么教科书上说可以"驱动8个TTL负载"的原因.

2.P0作为一般端口时,T1就永远的截止,T2根据输出数据0导通和1截止,导通时拉地,当然是输出低电平;截止时,PO口就没有输出了,(注意,这种情况就是所谓的高阻浮空状态),如果加上外部上拉电阻,输出就变成了高电平1.

3.其他端口P1、P2、P3,在内部直接将P1口中的T1换成了上拉电阻,所以不用外接,但内部上拉电阻太大,电流太小,有时因为电流不够,也会再并一个上拉电阻。

4.在某个时刻,P0口上输出的是作为总线的地址数据信号还是作为普通I/O口的电平信号,是依靠多路开关MUX来切换的.而MUX的切换,又是根据单片机指令来区分的.当指令为外部存储器/IO口读/写时,比如 MOVX A,@DPTR ,MUX是切换到地址/数据总线上;而当普通MOV传送指令操作P0口时,MUX是切换到内部总线上的.

PS:

Because Ports 1, 2, and 3 have fixed internal pullups, they are sometimes called “quasi- bidirectional” ports.
因为端口1、2、3有固定的内部上拉,所以有时候他们被称为"准双向"口.

Port 0, on the other hand, is considered “true” bidirectional, because when configured as an input it floats. 
端口0,从另外一方面来说,就被认为是"真正的"双向,因为当它被设置为输入的时候是浮空(高阻态)的.

       51单片机P0口的作为I/O的问题,其实看了51的P1口的电路就很容易理解了,主要是一个锁存器和推拉结构,在此作些说明。

       当用作输出,所有口线的状态都与SFR锁存位的设置有密切的联系。
       P0口为低除外。当P0口的一个位写入0时,这个位被拉低。但是对P0口的其中一个位写入1时,这个位呈现高阻,也就是未能连机,不能使用。要想获得1输出,你必须在P0口外加上拉电阻。一般驱动LED的上拉电阻为470Ω,外接逻辑电路的上拉电阻为4.7K。       补充:一些口线被作为简单的高电平输入也与SFR锁存位有关。因为P1、P2、P3有内部上拉电阻,可以随意被拉高,拉低。而P0口作为高电平输入时,也会呈现高阻态。
       P0口和P2口的输入缓冲被用来作存取外部存贮用,P0口用作外部存贮器的低位字节的位址,并与数据读写多工。输出第一位元址,当位置线是16位时,P2口用作高8位的位址线,因此当对外面存贮时,P0口、P2口没法当作I/O口线。
       P1口具有内部上拉电阻,当端口用作输入时,必须通过指令将端口的位锁存器置1,以关闭输出驱动场效应管,这时P1口的引脚由内部上拉电阻拉为高电平,所以向P1写入1,工作正常。
       P0则不同,它没有内部上拉电阻,在驱动场效应管的上方有一个提升场效应管,它只是在对外存储器进行读写操作,用作地址/数据时才起作用,当向位锁存器写入1,使驱动场效应管截止,则引脚浮空,所以写入1而未获得。

P0口上拉电阻的阻值:

1、如果是驱动led,那么用1K左右的就行了。如果希望亮度大一些,电阻可减小,最小不要小于200欧姆,否则电流太大;如果希望亮度小一些,电阻可增大,增加到多少呢,主要看亮度情况,以亮度合适为准,一般来说超过3K以上时,亮度就很弱了,但是对于超高亮度的LED,有时候电阻为10K时觉得亮度还能够用。我通常就用1k的。

2、对于驱动光耦合器,如果是高电位有效,即耦合器输入端接端口和地之间,那么和LED的情况是一样的;如果是低电位有效,即耦合器输入端接端口和VCC之间,那么除了要串接一个1——4.7k之间的电阻以外,同时上拉电阻的阻值就可以用的特别大,用100k——500K之间的都行,当然用10K的也可以,但是考虑到省电问题,没有必要用那么小的。

3、对于驱动晶体管,又分为PNP和NPN管两种情况:对于NPN,毫无疑问NPN管是高电平有效的,因此上拉电阻的阻值用2K——20K之间的,具体的大小还要看晶体管的集电极接的是什么负载,对于LED类负载,由于发管电流很小,因此上拉电阻的阻值可以用20k的,但是对于管子的集电极为继电器负载时,由于集电极电流大,因此上拉电阻的阻值最好不要大于4.7K,有时候甚至用2K的。对于PNP管,毫无疑问PNP管是低电平有效的,因此上拉电阻的阻值用100K以上的就行了,且管子的基极必须串接一个1——10K的电阻,阻值的大小要看管子集电极的负载是什么,对于LED类负载,由于发光电流很小,因此基极串接的电阻的阻值可以用20k的,但是对于管子的集电极为继电器负载时,由于集电极电流大,因此基极电阻的阻值最好不要大于4.7K。

3、对于驱动TTL集成电路,上拉电阻的阻值要用1——10K之间的,有时候电阻太大的话是拉不起来的,因此用的阻值较小。但是对于CMOS集成电路,上拉电阻的阻值就可以用的很大,一般不小于20K,我通常用100K的,实际上对于CMOS电路,上拉电阻的阻值用1M的也是可以的,但是要注意上拉电阻的阻值太大的时候,容易产生干扰,尤其是线路板的线条很长的时候,这种干扰更严重,这种情况下上拉电阻不宜过大,一般要小于100K,有时候甚至小于10K。

根据以上分析,上拉电阻的阻值的选取是有很多讲究的,不能乱用,具体情况比较复杂,如果你是个莱鸟,那么你尽量用小一些的,这样牺牲一些电源功耗。不过最好请教一下有关人员。

系统分类: 单片机  |  用户分类: 单片机  |  标签: 上拉电阻 单片机  |  来源: 无分类  | 

点击查看原文

发表评论 阅读全文(888) | 回复(0)

发表于 2008/3/11 12:55:13

0

关于投票

基于nRF401和AT89C2051的无线数字通信系统

 

基于nRF401和AT89C2051的无线数字通信系统
湖南工业大学 欧伟明
引言

目前,机器人足球比赛系统主要有3种控制方式:第1种称为远程遥控无智能机器人足球比赛系统,主机通过无线方式遥控机器人;第2种称为基于视觉的机器人足球比赛系统,主机通过处理由摄像机获取的信息来控制机器人;第3种称为基于机器人的机器人足球比赛系统,此系统无需主机的控制,每个机器人通过自身的传感器获取信息并作出判断。

基于视觉的足球机器人系统作为实验平台,其实现难度适中,而且利于模糊推理、神经网络、机器学习等人工智能领域的研究。从设备上看,基于视觉的机器人足球比赛系统包括小车、摄像装置、计算机和无线收发装置;从功能上看,基于视觉的足球机器人比赛系统由视觉子系统、决策子系统、通信子系统和机器人子系统等四个部分以闭环系统形式构成。基于视觉的机器人足球比赛系统结构框图如图1所示。

系统硬件设计

通信子系统是机器人足球比赛系统中一个重要组成部分,其通信性能好坏,将严重影响机器人的运动和比赛的顺利进行。如果在通信过程中有误差,它将导致机器人小车错误的动作,失去运动目标。一般来说,对通信子系统的要求是:通信频率可选,硬件电路结构紧凑,通信速率高和性能稳定可靠。为了便于机器人小车的活动,通信子系统一般采用无线通信方式。

根据对通信子系统的设计要求,我们采用微控制器(俗称单片机)AT89C2051作为无线通信子系统的控制核心,并选用基于蓝牙核心技术的无线通信芯片nRF401,通信子系统结构框图如图2所示。机器人小车的控制命令由PC机发出,PC机的RS232串口连接到图2所示的无线通信子系统的输入端,控制命令经AT89C2051处理后,通过芯片nRF401以无线的方式发送给机器人小车。



AT89C2051芯片简介

微控制器采用美国Atmel公司的AT89C2051芯片。它的指令系统与MCS-51产品兼容,具有2K字节可重编程闪速存储器,128 8位内部RAM,两个16位定时器/计数器,六个中断源,编程串行UART通道,15根可 编程I/O引线。在无线通信子系统中,AT89C2051完成接收PC机从RS232串口送来的机器人小车控制指令,并将控制指令经无线通信芯片nRF401送给机器人小车子系统。

nRF401芯片介绍

nRF401[3]是由挪威Nordic VLSI ASA公司推出的集收、发于一体的无线通信芯片,在一个20管脚芯片内集成了高频发射、高频接收、FSK调制与解调、PLL锁相环、放大器等单元电路。工作于433MHz ISM频段,采用FSK调制与解调技术,数据通信速率高达20kb/s,最大传输功率为+10dBM,并可以调整传输功率,差分式天线接口,非常适合做成PCB天线,以节约成本。

n RF401的内部结构如图3所示。nRF401的配置电路图如图4所示。


无线通信子系统电路原理图

无线通信子系统电路原理图如图5所示。CPU选用AT89C2051,它既接收来自上位机(PC机)的数据,同时又将从PC机接收的数据通过nRF401以广播形式发送给每个机器人小车子系统,每个机器人小车子系统根据设定的识别码,从接收缓冲区取出左右轮速度值,从而控制机器人小车的运动。

AT89C2051微控制器的RXD/p3.0口与电平转换芯片MAX232相连,MAX232通过DB_9/male插头与PC机的RS232串口相接,用以接收PC机发送过来的命令控制字。AT89C2051微控制器的TXD/p3.1口与nRF401的DOUT/pin9脚相接,结合其他的控制引脚,AT89C2051控制nRF401的无线发射过程,用来完成将命令控制字经nRF401实现无线传输给机器人小车。AT89C2051微控制器的P1.0、P1.1、P1.2分别与nRF401的CS、PWR_UP、TXEN相连接。


基于ERTOS的系统软件设计

通信格式

nRF401可以使用全双工模式,因此,机器人小车子系统不但可以接收主机发出的命令,而且可以向主机发出信息,甚至可以实现机器人小车子系统之间的通信。但是当信息量过大时,有可能发生通信死锁,所以应考虑通信协议的设计。为确保通信的顺畅,我们 只允许主机向机器人发送命令,而禁止其他形式的通信。命令控制字的具体格式如下:

其中Si(i=1,2,3,...),为i号机器人小车标识;

Li(i=1,2,3,...),为i号机器人小车左轮速度;

Ri(i=1,2,3,...),为i号机器人小车右轮速度。

PC机发给每个机器人小车的控制命令字包括3个字节,第1字节是小车标号,第2字节是该小车左轮速度,第3字节是该小车右轮速度。一次性将所有机器人小车的控制命令打包发送。每个小车都能接收到PC机发送的每1条指令,机器人小车子系统上的通信专用MCU对标识信号进行比较,相符则随后数据有效,否则不予接收。

软件设计

为 了能够实时地完成无线通信的目的,系统的软件我们用C51语言编写,并采用 Keil Vision2 6.20集成开发环境中的RTX51 Tiny实时操作系统来完成无通信子系统中微控制器AT89C2051的软件设计。RTX51 Tiny实时操作系统是德国Keil公司开发的一种应用于MCS-51系列单片机功能强大的、可用于目前世界上由Intel 8051标准内核派生出的很多种增强型微控制器的实时操作 系统。RTX51 Tiny短小精悍,只占用900字节ROM、7字节DATA型及3倍于任务数量的IDATA型RAM空间,可以很容易地运行在没有扩展外部存储器的单片机系统上。使用RTX51 Tiny的用户程序可以访问外部存储器,允许循环任务切换,并且支持信号传递和事件驱动,还能并行地利用中断功能。RTX51 Tiny允许“准并行”地同时执行16个任务。

根 据对无通信子系统功能的分析,我们把软件分解为三个任务,各任务之间的运行关系如图6所示。这三个任务的具体情况如下。

任务0:系统初始化,如设置MCU的串行口工作在方式1,并设置波特率为9600bps。在启动任务1和任务2后自动删除任务0,使得任务0只在系统复位时执行一次。

任务1:接收PC机的命令控制字。若接收到了命令控制字则向任务2发送触发信号。
任 务2:等待触发信号,若SIGNAL被置 位,则控制nRF401并将命令控制字传送给nRF401,完成无线发射。

结束语

人 类对机器人的研究已走过了漫长的历程。随着科学技术的不断发展,人们对机器人的要求也随之越来越高而现实。在机器人足球比赛系统的开发过程中,不仅需要机器人学、通信与计算机技术等,而且还需要图像处理、智能控制等学科内容。 本 文主要叙述了机器人足球比赛通信子系统的硬件、软件设计。采用AT89C2051和nRF401所设计的通信子系统,电路核心芯片少,外围电路简单,体积小巧,成本低,其无线数字通信距离及其通信的可靠性均能满足机器人足球比赛系统的要求,从实际使用情况来看,使用效果 相当好。

系统分类: 单片机  |  用户分类: 单片机  |  标签: 无标签  |  来源: 转贴  | 

点击查看原文

发表评论 阅读全文(1107) | 回复(1)

发表于 2008/2/20 13:59:26

4

关于投票

原创:51单片机控制多个舵机源程序

原创:51单片机控制多个舵机源程序,刚刚调试成功!用一个定时器控制8个舵机。

keil 文件rar

欢迎讨论:QQ:437796990

系统分类: 单片机  |  用户分类: 单片机  |  标签: 无标签  |  来源: 原创  | 

点击查看原文

发表评论 阅读全文(3926) | 回复(2)

发表于 2008/2/2 16:04:22

1

关于投票

mc9s08gb中文数据手册

MC9S08GB60的特点
①60K带块保护和安全机制的可片上在线编程的 FLASH存储器
②4K片上随机存储器(RAM)
③8 通道,10 位模数转换器(ATD)
④两个串行通信接口模块(SCI)
⑤串行外设接口模块(SPI)
⑥时钟源可选晶体振荡器、陶瓷谐振器、外部时钟或经精确 NVM校准的内部时钟
⑦高达 100 kbps的 IIC总线(IIC)
⑧一个 3 通道和一个 5 通道的 16 位定时器/脉宽调制器(TPM)模块,每个通道可以
PWM等功能。每个定时模块的每个通道可配置为
选择输入捕捉、输出比较和边缘对齐
(CPWM)模式
带缓冲的中心对齐的脉宽调制
⑨8 引脚键盘中断模块(KBI)
⑩16 个大电流引脚(受封装形式限制)
11 端口引脚作为输入时,可以通过软件设置是否有内部上拉,端口的每一位都可
以单独设置,输出时上拉禁止
12 RESET和 IRQ引脚内部上拉可减少用户系统开销
13 56 个通用输入输出引脚(I/O),和封装形式有关
14 64 引脚薄型方型扁平式封装(LQFP)

 

PDF文件太大了  加QQ:437796990,验证信息:gb60;

系统分类: 单片机  |  用户分类: 单片机  |  标签: HCS08 mc9S08GB60  |  来源: 整理  | 

点击查看原文

发表评论 阅读全文(916) | 回复(0)

发表于 2007/7/23 18:07:44

1

关于投票

NXT与PCF8574的通讯

I2C Interfacing Part 1: Adding Digital I/O Ports

By Sivan Toledo
October 2006

The sensor ports on the NXT support a serial digital protocol called I2C, which was developed by Philips in the 1980's for use in consumer electronics (in television sets, for example). This article describes the use of a simple I2C chip that provides 8 digital I/O ports with the NXT.

Each one of these eight ports can be used as either input or output (and in some restricted ways also as both). These inputs and outputs are binary: they are either on or off. Input ports can be used for touch sensor (switches), for example. Output ports can drive LEDs, and through relays or other devices they can turn motors on or off. In the setup that this article describes, two ports are used to drive LEDs, two other ports are used as inputs, connected to push switches, and four ports remain unused.

The most important thing to realize about this project is that it is VERY SIMPLE. Connecting your NXT to this the interface chip is as easy as electronics experimentation goes, and programming your NXT to communicate with this chip is also easy. 

Warning: connecting your NXT to any home-made gizmo (like the one described here) can damage it. Beware.

I2C

I2C is a two-wire serial bus. One wire carries a clock signal and one carries the data signals. In simple configurations the bus has one master that can communicate with as many as 128 slave devices connected to the same two wires. Only the master initiates communication. It can send commands to the slaves, and it can query them for data. The slaves only respond to the master; they cannot initiate communication on the I2C bus. (Many I2C chips have an interrupt output that can be used to notify the master that the chip needs some attention, but this is not part of the I2C protocol, and there is no way to use interrupts in the NXT sensor ports.)

Two of the six wires in the NXT sensor ports carry the I2C clock and data signals. The NXT always serves as a master. Another wire carries a 4.3v supply line, two more are grounded (one of them needs to be used if you use the NXT's power). The sixth line is an RCX-type 9v analog sensor line.

I2C chips usually come with a partially-configured bus address. I2C slaves have a 7-bit address (that is why there can be 128 of them on a bus). In many cases some of these bits are set when the chip is designed, and the rest can be configured by connecting specific address pins to either ground or to the supply line.

Communication on the I2C bus follows a simple protocol. The master signals that it wants to send information (a start signal). Then it sends the first byte of the command, which consists of the 7-bit address of the slave the master wants to communicate with, and a read/write bit. If this bit is 0, the master is not expecting a response; this is a write command. If this bit is 1, the master is expecting a response. When the master receives the response it expected, it signals that the communication is terminated (a stop signal). Apart from the first byte of the command, which always consists of the slave's address, the rest of the command structure is specific to each slave chip.

The PCF8574 Chip

The chip that I used is a basic and low-cost I2C chip from Philips called PCF8574. It provides 8 bi-directional binary ports. The 4 most-significant bits of its address are 0100, and the lower 3 are configurable through pins. It requires a supply voltage of 2.5-6v, so it can be directly connected to the sensor port's 4.3v supply. It can supply 25mA to each port, which means that it can drive LEDs directly. Pairs of outputs can be connected in parallel to drive higher loads.

There are many similar chips from other both Philips and from other manufacturers. Some of them provide 16 rather than 8 ports and some also includes some memory.

Communicating with the PCF8574 is very simply. To set the ports, you send one byte with the chip's address and a 0 read/write bit, followed by a byte that specifies the value of each port. To read the state of the ports, you send one byte with the chip's address and a 1 read/write bit, and the chip responds with a byte in which each bit describes the state of one port. There is just one more detail: to be able to read the value of a specific port, you must first write a 1 into it (set it to high, so that it can sense being pulled to zero).

Cables

To connect something to the NXT, you need NXT cables. I converted the plugs on two halves of a a 6-wire telephone line with standard RJ-12 plugs to NXT plugs, according to the instructions published by Philippe (Philo) Hurbain. I deviated a little from his instructions: I used a fine modeling hand saw rather than a power tool to cut the latch, and I used a 10-minute epoxy rather than a slow-cure epoxy to glue the latch back in its new place. The latch of one of my two plugs later came off, so I my technique is not completely reliable. Perhaps it came off because of the weak glue, and perhaps the less accurate cut of the hand saw.

Since LEGO sells original NXT cables at a reasonable cost, using spare original cables is probably a better idea than creating your own cables. If NXT-style sockets became available, they would be ideal for interfacing projects.

The Circuit

As you can see from the schematic diagram, the circuit is really mostly the PCF8574 chip. The only components on the input side are the two 82k pull-up resistors, which are required according to the NXT Hardware Developer Kit (a PDF document), and the bypass capacitor on the right side of the drawing. Apart from them, there are only two LEDs, each in series with a resistor, and two push switches, each with a pull-up resistor. I wanted to use smaller resistors in both cases, but didn't have the values I wanted; but the resistors I used worked as well. The unused pins on the pins, which include 4 ports pins and the interrupt line, are not connected to anything.

Prototyping

I built the circuit on a simple breadboard. The only difficult part was to connecting the NXT wire. I was not able to reliably connect the 4 wires I needed to the breadboad because there were not stiff enough. So I threaded them twice through a prototyping PCB (to hold them in place), soldered them to the prototyping PCB, and soldered stiffer wires to them. These stiffer wires worked just fine in the breadboard. The PCB is the little yellow board you see in the pictures (most of it is not used).

Programming

You can't access the I2C functionality of the NXT from the NXT-G environment. The ultrasound-sensor block in NXT-G does use the I2C functionality, but it is specific to the ultrasound sensor and cannot be used with other I2C chips. Therefore, I used NBC, which does provide access to the I2C system calls.

The firmware provides three system calls to communicate with I2C chips. Before you can use them, you must configure the sensor port to I2C  mode using setin commands and wait for the configuration to take effect. One system call, CommLSCheckStatus, simply checks whether the previous attempt to communicate with an I2C device was successful. If it returns error 32, then the previous communication attempt is still pending and you must wait. If it returns a negative number, then there is some error. Some errors are fatal, but sometimes you can simply try again. My program does not handle all the error cases properly. In particular, it just ignores negative errors (not a good idea). The second system call, CommLSWrite, is the one that actually initiates communication. It is used to send commands to an I2C slave and to query it for data. When you call it, you specify how many bytes should be in the slave's response. If you specify 0, this means that you are simply sending a command to the slave and expect no reply (but the slave always acknowledges communication as part of the protocol; this helps the NXT know whether the slave is working properly or not). If you request a positive number of bytes back, the NXT sends the command and waits for the slave to respond. It keeps the slave's response in an internal buffer, from which you can retrieve the data using the third system call, CommLSRead. This system call does not query the slave; it just retrieves previously sent information from the firmware's internal buffer.

The only surprise I encountered was the way the firmware handles the read/write bit. I was able to set the output ports (to toggle the LEDs) without a problem by writing a message containing 0x40 (the address of the 8574 slave in my circuit with a 0 read/write bit) and a second byte specifying the desired state of each port. I assumed that to read the state of the ports, I need to send a one-byte message containing 0x41 and expecting a one-byte reply. The value 0x41 consists of the 7-bit address of the slave and of a 1 in the read/write bit position. This didn't work. Eventually I discovered that I need to send 0x40. Apparently, the firmware adds the read/write bit. (I assume that it it adding it arithmetically rather than by or'ing it, otherwise 0x41 would have worked.) Once I figured that out, everything worked fine.

Here is the final NBC program (it doesn't do much, just tests the circuit).

It's Really Easy

To give you a sense of how easy or difficult it is to interface I2C chips to the NXT, I'll tell you what was my biggest problem (apart from the read/write bit confusion and the difficulty of connecting the NXT wires to the breadboard). After I wired the circuit and wrote the program to toggle the LEDs, I ran it and nothing happened. So I added code to display the error codes from the system calls, and discovered that the firmware was reporting error -35, a bus error, which can indicate a device failure. I started to check voltages in the circuit with a voltmeter, and eventually discovered that the chip was not getting the full supply voltage. I looked at it carefully and discovered that I didn't push it all the way into the breadboard. This shows you that

  • I never used these breadboards before, but in spite of this I was still able to build the circuit,
  • I did build electronic circuits before, and this experience probably helped me find the problem.

So my own conclusion is that interfacing I2C chips to the NXT is pretty easy even if you have very little background in electronics. But since things rarely work the first time around when you prototype, you do need to be able to do basic troubleshooting on a circuit (mainly to test connections).

 

系统分类: 单片机  |  用户分类: 单片机  |  标签: 无标签  |  来源: 转贴  | 

点击查看原文

发表评论 阅读全文(931) | 回复(0)

发表于 2007/7/19 17:55:17

1

关于投票

基于AT89C2051的多路舵机控制器设计

                                                   基于AT89C2051的多路舵机控制器设计     

   舵机是机器人、机电系统和航模的重要执行机构。舵机控制器为舵机提供必要的能源和控制信号。本文提出一种以外部中断计数为基础的PWM波形实现方法。该方法具有简单方便,成本低,可实现多路独立PWM输出的优点。

  •      舵机是一种位置伺服的驱动器。它接收一定的控制信号,输出一定的角度,适用于那些需要角度不断变化并可以保持的控制系统。在微机电系统和航模中,它是一个基本的输出执行机构。
     
      1 舵机的工作原理
       
      以日本FUTABA-S3003型舵机为例,图1是FUFABA-S3003型舵机的内部电路。

    点击看大图


         舵机的工作原理是:PWM信号由接收通道进入信号解调电路BA66881。的12脚进行解调,获得一个直流偏置电压。该直流偏置电压与电位器的电压比较,获得电压差由BA6688的3脚输出。该输出送人电机驱动集成电路BA6686,以驱动电机正反转。当电机转速一定时,通过级联减速齿轮带动电位器R。,旋转,直到电压差为O,电机停止转动。舵机的控制信号是PWM信号,利用占空比的变化改变舵机的位置。
     
      2 舵机的控制方法
       
      标准的舵机有3条导线,分别是:电源线、地线、控制线,如图2所示。

         电源线和地线用于提供舵机内部的直流电机和控制线路所需的能源.电压通常介于4~6V,一般取5V。注意,给舵机供电电源应能提供足够的功率。控制线的输入是一个宽度可调的周期性方波脉冲信号,方波脉冲信号的周期为20 ms(即频率为50 Hz)。当方波的脉冲宽度改变时,舵机转轴的角度发生改变,角度变化与脉冲宽度的变化成正比。某型舵机的输出轴转角与输入信号的脉冲宽度之间的关系可用围3来表示。

      3 舵机控制器的设计
      (1)舵机控制器硬件电路设计
       
      从上述舵机转角的控制方法可看出,舵机的控制信号实质是一个可嗣宽度的方波信号(PWM)。该方波信号可由FPGA、模拟电路或单片机来产生。采用FPGA成本较高,用模拟电路来实现则电路较复杂,不适合作多路输出。一般采用单片机作舵机的控制器。
     
      目前采用单片机做舵机控制器的方案比较多,可以利用单片机的定时器中断实现PWM。该方案将20ms的周期信号分为两次定时中断来完成:一次定时实现高电平定时Th;一次定时实现低电平定时T1。Th、T1的时间值随脉冲宽度的变换而变化,但,Th+T1=20ms。该方法的优点是,PWM信号完全由单片机内部定时器的中断来实现,不需要添加外围硬件。缺点是一个周期中的PWM信号要分两次中断来完成,两次中断的定时值计算较麻烦;为了满足20ms的周期,单片机晶振的频率要降低;不能实现多路输出。也可以采用单片机+8253计数器的实现方案。
     
      该方案由单片机产生计数脉冲(或外部电路产生计数脉冲)提供给8253进行计数,由单片机给出8253的计数比较值来改变输出脉宽。该方案的优点是可以实现多路输出,软件设计较简单;缺点是要添加l片8253计数器,增加了硬件成本。本文在综合上述两个单片机舵机控制方案基础上,提出了一个新的设计方案,如图4所示。

    点击看大图

         该方案的舵机控制器以AT89C2051单片机为核心,555构成的振荡器作为定时基准,单片机通过对555振荡器产生的脉冲信号进行计数来产生PWM信号。该控制器中单片机可以产生8个通道的PWM信号,分别由AT89C2051的P1.0~Pl.7(12~19引脚)端口输出。输出的8路PWM信号通过光耦隔离传送到下一级电路中。因为信号通过光耦传送过程中进行了反相,因此从光耦出来的信号必须再经过反相器进行反相。
     
      方波信号经过光耦传输后,前沿和后沿会发生畸变,因此反相器采用CD40106施密特反相器对光耦传输过来的信号进行整形,产生标准的PWM方波信号。笔者在实验过程中发现,舵机在运行过程中要从电源吸纳较大的电流,若舵机与单片机控制器共用一个电源,则舵机会对单片机产生较大的干扰。因此,舵机与单片机控制器采用两个电源供电,两者不共地,通过光耦来隔离,并且给舵机供电的电源最好采用输出功率较大的开关电源。该舵机控制器占用单片机的个SCI串口。串口用于接收上位机传送过来的控制命令,以调节每一个通道输出信号的脉冲宽度。MAX232为电平转换器,将上位机的RS232电平转换成TTL电平。
     
      (2)实现多路PWM信号的原理
       
      在模拟电路中,PWM脉冲信号可以通过直流电平与锯齿波信号比较来得到。在单片机中,锯齿波可以通过对整型变量加1操作来实现,如图5所示。假定单片机程序中设置一整型变量SawVal,其值变化范围为O~N。555振荡电路产生的外部计数时钟信号输入到AT89C2051的INTO脚。每当在外部计数时钟脉冲的下降沿,单片机产生外部中断,执行外部中断INT0的中断服务程序。每产生一次外部中断,对SawVal执行一次加1操作,若SawVal已达到最大值N,则对SawVal清O。SawVal值的变化规律相当于锯齿波,如图5所示。若在单片机程序中设置另一整型变量DutyVal,其值的变化范围为O~N。每当在SawVal清0时,DulyVal从上位机发送的控制命令中读入脉冲宽度系数值,例如为H(0≤H≤N)。若DutyVal≥SawVal,则对应端口输出高电平;若DutyVal<Sawval,则对应端口输出低电平。从图5中可看出,若改变DutyVal的值,则对应端口输出脉冲的宽度发生变化,但输出脉冲的频率不变,此即为PWM波形。

    点击看大图

         设外部计数时钟周期为TINT0,锯齿波周期(PWM脉冲周期)为TPWM,PWM脉冲宽度占空比为D,由图5可得出如下关系:
       

    点击看大图


         由式(3)可知,PWM波形的周期TPWM一旦确定下来,只须选定计数最大值N,就可以确定外部时钟脉冲所需周期(频率)。外部时钟脉冲周期TINT0显然是PWM脉冲宽度变换的最小步距,即调节精度。由式(4)可知,N越大,步距所占PWM周期的百分比越小,精度越高。例如,若采用8位整型变量,最大值N=28-1=255,则精度为1/(255+1)=1/255;若采用16位整型变量,最大值N=216-1=65535,则精度为1/65536。文中计数变量SawVal采用8位整型变量,因此N=255。对于一般应用,其精度已足够。就舵机而言,要求TPWM=20ms,则可算得外部时钟周期为:
        点击看大图
        因此,设计555振荡电路时,其输出脉冲的频率应为:
       
         当有多个变量与SawVal比较,将比较结果输出到多个端口时。就形成了多路PWM波形。各个变量的值可以独立变化,因此各路PWM波形的占空比也可以独立调节,互不相干。多路PWM波形的产生如图6所示。图中以3路PWM波形为例。

    点击看大图

      4 舵机控制器软件的设计
       
      舵机控制器的控制核心为单片机AT89C2051。文中,程序用C5l编写,工作方式为前后台工作方式。单片机程序包括系统初始化程序、串口通信程序、上位机命令解释与PWM脉宽生成程序和多路PWM波形输出程序。串行通信程序和多路PWM波形输出程序采用中断方式。串口通信格式为渡特率9600bps、8位数据位、1位停止位、无校验、ASCII码字符通信。串口通信程序用于接收上位机发送过来的控制命令。控制命令采用自定义文本协议,即协议内容全部为ASCII码字符。通信协议格式如图7所示。

         例如,要控制通道1的PWM脉宽,脉宽系数为25,则通信协议内容为“#”“1”“0”“2”“5”“!”这6个字符。这时通道l的PWM占空比为25/256=O.098。一个通道号对应一个PWM脉冲输出端口。本设计为8个通道,号码为l~8,对应单片机的P1.o~P1.7。起始符和终止符起到帧同步的作用。串口通信程序流程如图8所示。

    点击看大图

         图8中,CHNo存放的是PWM通道号ASCII码,Dutyl00、DutylO、Duoyl分别存放的是脉宽系数的百位数、十位数和个位数的ASCII码(注意,若高位数为O,则该位的字符应为“0”,不能省略。如25,完整字符应为“O”“2”“5”。CharNo为信号量,用于对串口接收的字符顺序以及串口中断与上位机命令解释程序之间进行同步。

      5 舵机控制器实验
       
      图9为舵机控制板输出的其中一路PWM波形(带舵机负载)。
        从图9中可看出,舵机控制器输出的PWM波形稳定、干净,符合设计要求。

      6 结论
       
      本文提出的多路舵机控制器设计方法,以单片机AT89C2051为核心,由外部振荡电路提供PWM脉冲的定时基准,控制部分与舵机驱动部分由两个电源供电,两者电气隔离。这种设计方案的优点是:
       
       ①PWM波形由外部振荡电路提供定时基准,与单片机内部振荡器的频率无关,不影响串口通信、定时器等参数的配置。
         ②PWM波形的调整精度可任意确定。
         ③本没计思路可应用于任意多路的PWM输出,只要单片机能提供足够多的输出端口,例如将AT89C2051换成AT89S5l,就可以提供至少24路的PWM输出(P0、Pl、P2)。
         ④控制参数由SCI串口输入,适应面广,上位机可以是PC机、单片机或是PLC。
         ⑤本方法具有一般性,任何单片机只要能提供SCI中断、外部中断就可以应用本方法。
        编者注:本文为期刊缩略版,全文见本刊网站www.mesnet.com.cn。

系统分类: 单片机  |  用户分类: 单片机  |  标签: 无标签  |  来源: 转贴  | 

点击查看原文

发表评论 阅读全文(1897) | 回复(0)

发表于 2007/7/19 17:52:28

2

关于投票

利用单片机PWM信号进行舵机控制

利用单片机PWM信号进行舵机控制(图)
基于单片机的舵机控制方法具有简单、精度高、成本低、体积小的特点,并可根据不同的舵机数量加以灵活应用。

   在机器人机电控制系统中,舵机控制效果是性能的重要影响因素。舵机可以在微机电系统和航模中作为基本的输出执行机构,其简单的控制和输出使得单片机系统非常容易与之接口。

   舵机是一种位置伺服的驱动器,适用于那些需要角度不断变化并可以保持的控制系统。其工作原理是:控制信号由接收机的通道进入信号调制芯片,获得直流偏置电压。它内部有一个基准电路,产生周期为20ms,宽度为1.5ms的基准信号,将获得的直流偏置电压与电位器的电压比较,获得电压差输出。最后,电压差的正负输出到电机驱动芯片决定电机的正反转。当电机转速一定时,通过级联减速齿轮带动电位器旋转,使得电压差为0,电机停止转动。
 

图1  舵机的控制要求


  舵机的控制信号是PWM信号,利用占空比的变化改变舵机的位置。一般舵机的控制要求如图1所示。

单片机实现舵机转角控制
   可以使用FPGA、模拟电路、单片机来产生舵机的控制信号,但FPGA成本高且电路复杂。对于脉宽调制信号的脉宽变换,常用的一种方法是采用调制信号获取有源滤波后的直流电压,但是需要50Hz(周期是20ms)的信号,这对运放器件的选择有较高要求,从电路体积和功耗考虑也不易采用。5mV以上的控制电压的变化就会引起舵机的抖动,对于机载的测控系统而言,电源和其他器件的信号噪声都远大于5mV,所以滤波电路的精度难以达到舵机的控制精度要求。

   也可以用单片机作为舵机的控制单元,使PWM信号的脉冲宽度实现微秒级的变化,从而提高舵机的转角精度。单片机完成控制算法,再将计算结果转化为PWM信号输出到舵机,由于单片机系统是一个数字系统,其控制信号的变化完全依靠硬件计数,所以受外界干扰较小,整个系统工作可靠。

   单片机系统实现对舵机输出转角的控制,必须首先完成两个任务:首先是产生基本的PWM周期信号,本设计是产生20ms的周期信号;其次是脉宽的调整,即单片机模拟PWM信号的输出,并且调整占空比。

   当系统中只需要实现一个舵机的控制,采用的控制方式是改变单片机的一个定时器中断的初值,将20ms分为两次中断执行,一次短定时中断和一次长定时中断。这样既节省了硬件电路,也减少了软件开销,控制系统工作效率和控制精度都很高。

   具体的设计过程:例如想让舵机转向左极限的角度,它的正脉冲为2ms,则负脉冲为20ms-2ms=18ms,所以开始时在控制口发送高电平,然后设置定时器在2ms后发生中断,中断发生后,在中断程序里将控制口改为低电平,并将中断时间改为18ms,再过18ms进入下一次定时中断,再将控制口改为高电平,并将定时器初值改为2ms,等待下次中断到来,如此往复实现PWM信号输出到舵机。用修改定时器中断初值的方法巧妙形成了脉冲信号,调整时间段的宽度便可使伺服机灵活运动。

   为保证软件在定时中断里采集其他信号,并且使发生PWM信号的程序不影响中断程序的运行(如果这些程序所占用时间过长,有可能会发生中断程序还未结束,下次中断又到来的后果),所以需要将采集信号的函数放在长定时中断过程中执行,也就是说每经过两次中断执行一次这些程序,执行的周期还是20ms。软件流程如图2所示。

如图2 产生PWM信号的软件流程

   如果系统中需要控制几个舵机的准确转动,可以用单片机和计数器进行脉冲计数产生PWM信号。

   脉冲计数可以利用51单片机的内部计数器来实现,但是从软件系统的稳定性和程序结构的合理性看,宜使用外部的计数器,还可以提高CPU的工作效率。实验后从精度上考虑,对于FUTABA系列的接收机,当采用1MHz的外部晶振时,其控制电压幅值的变化为0.6mV,而且不会出现误差积累,可以满足控制舵机的要求。最后考虑数字系统的离散误差,经估算误差的范围在±0.3%内,所以采用单片机和8253、8254这样的计数器芯片的PWM信号产生电路是可靠的。图3是硬件连接图。点击看大图

图3 PWA信号的计数和输出电路

   基于8253产生PWM信号的程序主要包括三方面内容:一是定义8253寄存器的地址,二是控制字的写入,三是数据的写入。软件流程如图4所示,具体代码如下。
//关键程序及注释:
//定时器T0中断,向8253发送控制字和数据
void T0Int() interrupt 1
{
TH0 = 0xB1;
TL0 = 0xE0;    //20ms的时钟基准
//先写入控制字,再写入计数值
SERVO0 = 0x30; //选择计数器0,写入控制字
PWM0 = BUF0L;  //先写低,后写高
PWM0 = BUF0H;
SERVO1 = 0x70;  //选择计数器1,写入控制字
PWM1 = BUF1L;
PWM1 = BUF1H;
SERVO2 = 0xB0;  //选择计数器2,写入控制字
PWM2 = BUF2L;
PWM2 = BUF2H;
}

图4 基于8253产生PWA信号的软件流程

   当系统的主要工作任务就是控制多舵机的工作,并且使用的舵机工作周期均为20ms时,要求硬件产生的多路PWM波的周期也相同。使用51单片机的内部定时器产生脉冲计数,一般工作正脉冲宽度小于周期的1/8,这样可以在1个周期内分时启动各路PWM波的上升沿,再利用定时器中断T0确定各路PWM波的输出宽度,定时器中断T1控制20ms的基准时间。

   第1次定时器中断T0按20ms的  1/8设置初值,并设置输出I/O口,第1次T0定时中断响应后,将当前输出I/O口对应的引脚输出置高电平,设置该路输出正脉冲宽度,并启动第2次定时器中断,输出I/O口指向下一个输出口。第2次定时器定时时间结束后,将当前输出引脚置低电平,设置此中断周期为20ms的1/8减去正脉冲的时间,此路PWM信号在该周期中输出完毕,往复输出。在每次循环的第16次(2×8=16)中断实行关定时中断T0的操作,最后就可以实现8路舵机控制信号的输出。

   也可以采用外部计数器进行多路舵机的控制,但是因为常见的8253、8254芯片都只有3个计数器,所以当系统需要产生多路PWM信号时,使用上述方法可以减少电路,降低成本,也可以达到较高的精度。调试时注意到由于程序中脉冲宽度的调整是靠调整定时器的初值,中断程序也被分成了8个状态周期,并且需要严格的周期循环,而且运行其他中断程序代码的时间需要严格把握。

   在实际应用中,采用51单片机简单方便地实现了舵机控制需要的PWM信号。对机器人舵机控制的测试表明,舵机控制系统工作稳定,PWM占空比 (0.5~2.5ms 的正脉冲宽度)和舵机的转角(-90°~90°)线性度较好。

参考文献
1 胡汉才.单片机原理及接口技术.清华大学出版社.1996
2 王时胜,姜建平.采用单片机实现PWM式D/A转换技术.电子质量.2004
3 刘歌群.卢京潮.闫建国.薛尧舜.用单片机产生7路舵机控制PWM波的方法.机械与电子.2004

系统分类: 工业控制  |  用户分类: 单片机  |  标签: 无标签  |  来源: 转贴  | 

点击查看原文

发表评论 阅读全文(1191) | 回复(0)

发表于 2007/7/16 18:10:40

1

关于投票

刚刚调好的18B20(图)

想想已经两年的没有写过单片机程序,这几天用C51调了几个程序,居然发现还没有忘光!哈哈…………

刚刚弄好的18B20温度采集,送数码管显示,通过串口送PC………………

点击看大图

 

用的是求新工作室的开发板(http://www.mcuqx.com/) 

接下来几天陆续要做几个实验:1.1602显示

              2.直流电机

              3.伺服电机(舵机)

              4.I2C 等

                                                      …………………………期待!

有需要源码的 可以跟我联系。

系统分类: 单片机  |  用户分类: 单片机  |  标签: 无标签  |  来源: 原创  | 

点击查看原文

发表评论 阅读全文(5212) | 回复(4)

发表于 2007/7/15 22:17:27

3

关于投票

Keil使用中的若干问题

一、混合编程
 1、模块内接口:
使用如下标志符:
#pragma asm
  汇编语句
#pragma endasm
注意:如果在c51程序中使用了汇编语言,注意在keil编译器中需要激活Properties中的“Generate Assembler SRC File” 和“Assembler SRC File ”两个选项
来个实例吧:
#i nclude
void main(void)
{
P2=1;
#pragma asm
    MOV R7,#10
DEL:MOV R6,#20
    DJNZ R6,$
    DJNZ R7,DEL
#pragma endasm
P2=0;
}
另:
1、把"xx.c"加入工程中,右击"xx.c"选择“options for file"xx.c" 选择“Generate Assembler SRC File”和“Assemble SRC File”打上黑勾有效;
2、根据选择的编译模式,把相应的库文件象加"xx.c"一样加入工程中并放在"xx.c"下面,如smail模式下选"keil\c51\lib\c51s.lib"加入工程中,如果要进行浮点运算把"keil\c51\lib\c51fpl.lib"也加入工程中。
在 Keil 安装目录下的 \C51\LIB\ 目录的LIB 文件如下:
C51S.LIB - 没有浮点运算的 Small model
C51C.LIB - 没有浮点运算的 Compact model
C51L.LIB - 没有浮点运算的 Large model
C51FPS.LIB - 带浮点运算的 Small model
C51FPC.LIB - 带浮点运算的 Compact model
C51FPL.LIB - 带浮点运算的 Large model
3、在"xx.c"头文件中加入优化:比如#pragma OT(4,speed)
4、在"xx.c"中加入汇编代码 
                             #pragma ASM
                             ;Assembler Code Here
                             #pragma ENDASM
5、编译生成xx.hex
注意:
      没有做第一步会有如下警告:'asm/endasm' requires src-control to be active
      没有做第二步会有如下警告:UNRESOLVED EXTERNAL SYMBOL
                                REFERENCE MADE TO UNRESOLVED EXTERNAL
      没有做第三步会有如下警告:UNDEFINED SYMBOL (PASS-2)

二、中断使用
interrupt   xx   using   y  
  跟在interrupt   后面的xx   值得是中断号,就是说这个函数对应第几个中断端口,一般在51中  
  0   外部中断0    
  1   定时器0  
  2   外部中断1  
  3   定时器1  
  4   串行中断  
  其它的根据相应的单片机有自己的含义,实际上c在编译的时候就是把你这个函数的入口地址放到这个对应中断的跳转地址  。  using   y   这个y时说这个中断函数使用的那个寄存器组就是51里面一般有4个   r0   --   r7寄存器,如果你的终端函数和别的程序用的不是同一个寄存器组则进入中断的时候就不会将寄存器组压入堆栈返回时也不会弹出来节省代码和时间。

三、关于reentrant的使用方法
云清燕 发表于 2006-11-15 21:27:00 
我在程序中出现了如下警告:
*** WARNING L15: MULTIPLE CALL TO SEGMENT
    SEGMENT: ?PR?_CRCDATA?PANEL_DISP
    CALLER1: ?C_C51STARTUP
    CALLER2: ?PR?UART_RECV?PANEL_DISP
*** WARNING L15: MULTIPLE CALL TO SEGMENT
    SEGMENT: ?PR?ANALOGALLBECKON?PANEL_DISP
    CALLER1: ?C_C51STARTUP
    CALLER2: ?PR?UART_RECV?PANEL_DISP
*** WARNING L15: MULTIPLE CALL TO SEGMENT
    SEGMENT: ?PR?SWITCHALLBECKON?PANEL_DISP
    CALLER1: ?C_C51STARTUP
    CALLER2: ?PR?UART_RECV?PANEL_DISP
我的程序编译出来就这3个警告,但是程序可以正常下载运行。但是我觉得有这些警告会使程序存在bug。从字面上看是它的意思是我程序中接受函数UART_RECV()多调用了analogAllBeckon()、switchAllBeckon()。
因为51的普通函数是不可重入的,变量放在固定的地址,两个函数同时运行时,就会修改同一个变量,从而导致结果错误。于是我在analogAllBeckon()、switchAllBeckon()函数后面加了void analogAllBeckon()reentrant{//All Analog data beckon使程序消除了警告。这种方法是表明函数是可被多哥任务调用而不修改函数里边的变量值,以此来实现函数的重入性。
关于reentrant的使用keil的官方论坛上有详细的讨论.
Andy Neil(官方工程师)建议
"Are you sure that you really need to make everything reentrant?...A reading of the Keil app notes & knowledgebase articles on this subject showed that it was not necessary. "
由于每一次调用被reentrant声明的函数都要把函数的参数和内部变量压栈,所以很容易使堆栈区溢出,S52只有256Bytes的data段,一个简单的函数如果有一个参数三个内部变量,则需要压栈4字节以上,这还不包括函数调用堆栈.reentrant其实并不是适合低端的单片机,keil论坛上有人说对于那些有KB以上RAM的单片机reentrant才适合.

四、变量声明有关
在51系列中data,idata,xdata,pdata的区别 data:固定指前面0x00-0x7f的128个RAM,可以用acc直接读写的,速度最快,生成的代码也最小。 idata:固定指前面0x00-0xff的256个RAM,其中前128和data的128完全相同,只是因为访问的方式不同。idata是用类似C中的指针方式访问的。汇编中的语句为:mox ACC,@Rx.(不重要的补充:c中idata做指针式的访问效果很好) xdata:外部扩展RAM,一般指外部0x0000-0xffff空间,用DPTR访问。 pdata:外部扩展RAM的低256个字节,地址出现在A0-A7的上时读写,用movx ACC,@Rx读写。这个比较特殊,而且C51好象有对此BUG, 建议少用。但也有他的优点,具体用法属于中级问题,这里不提。
startup.a51的作用和汇编一样,在C中定义的那些变量和数组的初始化就在startup.a51中进行,如果你在定义全局变量时带有数值,如unsigned char data xxx="100";,那startup.a51中就会有相关的赋值。如果没有=100,startup.a51就会把他清0。(startup.a51==变量的初始化)。这些初始化完毕后,还会设置SP指针。对非变量区域,如堆栈区,将不会有赋值或清零动作。有人喜欢改startup.a51,为了满足自己一些想当然的爱好,这是不必要的,有可能错误的。比如掉电保护的时候想保存一些变量, 但改startup.a51来实现是很笨的方法,实际只要利用非变量区域的特性,定义一个指针变量指向堆栈低部:0xff处就可实现。, 为什么还要去改? 可以这么说:任何时候都可以不需要改startup.a51,如果你明白它的特性。

五、类型有关
用bit能够定义一个变量,用sbit却不行,sbit能够定义端口。

系统分类: 单片机  |  用户分类: 单片机  |  标签: 无标签  |  来源: 整理  | 

点击查看原文

发表评论 阅读全文(1333) | 回复(2)

发表于 2007/7/14 23:06:27

1

关于投票

基于AT89C51单片机设计的简易智能机器人(转)

 
基于AT89C51单片机设计的简易智能机器人
张宏,王德合
(空军第一航空学院,河南省信阳市 464000)


引言

随着微电子技术的不断发展,微处理器芯片的集成程度越来越高,单片机已可以在一块芯片上同时集成CPU、存储器、定时器/计数器、并行和串行接口、看门狗、前置放大器、A/D转换器、D/A转换器等多种电路,这就很容易将计算机技术与测量控制技术结合,组成智能化测量控制系统。这种技术促使机器人技术也有了突飞猛进的发展,目前人们已经完全可以设计并制造出具有某些特殊功能的简易智能机器人。

1 设计思想与总体方案

1.1 简易智能机器人的设计思想

本机器人能在任意区域内沿引导线行走,自动绕障,在有光源引导的条件下能沿光源行走。同时,能检测埋在地下的金属片,发出声光指示信息,并能实时存储、显示检测到的断点数目以及各断点至起跑线间的距离,最后能停在指定地点,显示出整个运行过程的时间。

1.2 总体设计方案和框图

本设计以AT89C5l单片机作为检测和控制核心。采用红外光电传感器检测路面黑线及障碍物,使用金属传感器检测路面下金属铁片,应用光电码盘测距,用光敏电阻检测、判断车库位置,利用PWM(脉宽调制)技术动态控制电动机的转动方向和转速。通过软件编程实现机器人行进、绕障、停止的精确控制以及检测数据的存储、显示。通过对电路的优化组合,可以最大限度地利用51单片机的全部资源。

P0口用于数码管显示,P1口用于电动机的PWM驱动控制,P2,P3口用于传感器的数据采集与中断控制。这样做的优点是:充分利用了单片机的内部资源,降低了总体设计的成本。该方案总体方案见图1。

2 系统的硬件组成及设计原理

此系统的硬件部分由单片机单元、传感器单元、电源单元、声光报警单元、键盘输入单元、电机控制单元和显示单元组成,如图2所示。

2.1 单片机单元

本系统采用AT89C51单片机作为中央处理器。其主要任务是扫描键盘输入的信号启动机器人,在机器人行走过程中不断读取传感器采集到的数据,将得到的数据进行处理后,根据不同的情况产生占空比不同的PWM脉冲来控制电机,同时将相关数据送显示单元动态显示,产生声光报警信号。其中,P0用于数码管动态显示,P1.0一P1.5控制2个电机,P1.6、P1.7为独立式键盘接口,P2接传感器,P3.2接计里程的光电码盘,P3.7接声光报警单元,P3.4、P3.5、P3.6接用于显示断点数目的发光二极管。

2.2 电机控制单元

本机器人采用了双电机双轮驱动的小车作为其底座。2个电机分别独立控制其左右两边的车轮,靠两边电机的转速的不同来实现转弯功能,还可让其原地转弯,便于控制。而传统的小车是靠动力电机和转向电机驱动,转弯角度难以控制,不便于使用。

电机控制电路采用大功率对管BDl39、BDl40组成的H型驱动电路,通过单片机产生占空比不同的PWM脉冲,精确调整电机的转速。这种电路由于工作在晶体管饱和或截止状态,避免了在线性放大区工作时晶体管的管耗,可以最大限度地提高效率;H型电路保证了可以简单地实现电机转速和方向的控制;电子开关的速度和稳定性也完全可满足需要,整套驱动电路是一种被广泛采用的电机驱动技术。电路见图3。

点击看大图

2.3 传感器单元

整个机器人共采用了9个传感器,分布在整个机器人的不同部位,相互配合起不同的作用,见图4。

图4中各传感器说明如下:

传感器1置于机器人正前方朝下的金属探测传感器,用于探测金属。

传感器2置于机器人正前方朝前的超声波传感器,用于检测障碍物。超声波来源于555产生40 kHz的方波信号,经超声波发射头发出。发射头不断发出信号,当遇到障碍物时,信号会被反射回来,从而接收头会接受到信号,将信号送入单片机进行相应的判断和处理。

传感器3置于机器人正前方朝下的红外光电传感器,用于检测停止线。红外发射管发出信号,经不同的反射介质反射,根据红外接收管是否接收到信号做出相应的判断。

传感器4、5置于机器人底座下方朝下的红外光电传感器,用于检测地面的引导线,原理同传感器3。

传感器6、7置于机器人正前方朝前的光敏电阻传感器,用于寻找光源。当机器人前方有光源照射时,光敏电阻的大小将会改变,将2个传感器的改变量进行比较处理后送入单片机,单片机将会产生相应的调整信号,使机器人朝光强的方向行走。

传感器8置于机器人后方两侧朝外的超声波传感器,用于在机器人遇到障碍物时的转弯处理,判断机器人是否完全绕开障碍物,原理同传感器2。

传感器9置于机器人正后方的光电码盘,用于计里程,借助于鼠标原理,选用直径为2.6 cm的塑料小轮自制光电码盘,经过打磨使其周长为8 cm,再在该小轮上打等距离的8个孔,如图5所示。最小测距精度可达到1 cm,足以满足要求,两侧装上光电传感器,将其安装在车尾,使之与车的行驶同步。就实际情况自制出来的各个孔之间的距离无法精确相等,但经过具体测量该光电码盘,能保证行驶50 cm产生50个脉冲,于是采用其作为计算距离的基准单位。在直道区,可由该电路产生的脉冲数,计算出铁片中心线至起跑线间的距离。

此外,为了清楚直观地观察到各传感器的工作状态,电路中还专门为每个传感器设计了工作指示灯,实时显示每个传感器的工作状态。

2.4 键盘输入单元

键盘输入单元采用独立式键盘,由2个按键组成,其中一个为启动键,另一个为显示切换键,当机器人行走完全程后,按下该键,将显示整个行走过程的时间。

2.5 显示单元

显示单元由2个7段数码管组成,为了减少整个系统的功耗,采用了由单片机软件译码,动态显示,实时显示每个断点到起点的距离以及整个运行过程的时间。

2.6 声光报警单元

用555作为振荡源,用单片机触发振荡源驱动电磁讯响器作为声音指示器和1只发光二极管作为光指示装置,从而组成声光报警单元。

2.7 电源单元

本系统采用2套电源分别对电机和控制电路进行单独供电。系统控制电路采用经7805稳压后的输出供电(5V),电机则采用4节AA电池来供电。

3 系统的软件设计

该系统配套的软件程序采用模块结构,由C语言编写完成。主要由初始化程序、偏道调整程序、偏离光源调整程序、声光指示子程序、读传感器状态、显示程序、定时器0的中断服务程序、定时器1的中断服务程序、外部中断0的服务程序、停车处理等模块组成。系统的主体流程如图6所示。

4 结束语

该机器人在认为设定的跑道上经过多次实验,达到了预期的效果,但是其智能化程度还远远不够。随着人工智能和神经网络技术的不断研究和深入,智能机器人的发展前景将会越来越广阔。

系统分类: 单片机  |  用户分类: 单片机  |  标签: 无标签  |  来源: 转贴  | 

点击查看原文

发表评论 阅读全文(799) | 回复(1)

发表于 2007/6/29 22:26:16

2

关于投票

单片机加密方法


单片机加密方法 (1)

    科研成果保护是每一个科研人员最关心的事情 , 目的不使自己的辛苦劳动付注东流。加密方法有软件加密 , 硬件加密 , 软硬件综合加密 ,  时间加密 , 错误引导加密 , 专利保护等措施。 有矛就有盾 , 有盾就有矛 , 有矛、有盾 , 才促进矛、盾质量水平的提高。加密只讲盾 , 也希望网友提供更新的加密思路。
    现先讲一个软件加密 : 利用 MCS-51 中 A5 指令加密 ,( 本人 85 年发现的 , 名软件陷阱 ), 其实世界上所有资料 , 包括英文资料都没有讲这条指令 , 其实这是很好的加密指令。 A5 功能是二字节空操作指令。加密方法在 A5 后加一个二字节或三字节操作码 , 因为所有反汇编软件都不会反汇编 A5 指令 , 造成正常程序反汇编乱套 , 执行程序无问题。仿制者就不能改变你的源程序 , 你应在程序区写上你的大名、单位、开发时间及仿制必究的说法 , 以备获得法律保护。我曾抓到过一位“获省优产品”仿制者 , 我说你们为什么把我的名字也写到你的产品中 ? 根据本人2002.1.24实验发现,用A5指令加密完全无用!
    硬件加密 :8031/8052 单片机就是 8031/8052 掩模产品中的不合格产品 , 内部有 ROM( 本人 85 年发现的 ), 可以把 8031/8052 当 8751/8752 来用 , 再扩展外部程序器 , 然后调用 8031 内部子程序。当然你所选的同批 8031 芯片的首地址及所需用的中断入口均应转到外部程序区。

单片机加密方法 (2)

    各位 , 我在这里公开场合讲加密 , 有的只能讲思路 , 有的要去实验 , 要联想 , 要综合应用各种方法 , 甚至有的不能言传 , 只能意会。因为这里有的造矛者也在看我们如何造盾 , 当然 , 我们也要去看人家怎样造矛 , 目前国内、外最高造矛的水平怎样。“知已知彼 , 才能百战百胜”。
    硬件加密 :  使他人不能读你的程序
    ①   用高电压或激光烧断某条引脚 , 使其读不到内部程序 , 用高电压会造成一些器件损坏。
    ②   重要 RAM 数据采用电池 ( 大电容 , 街机采用的办法 ) 保护 , 拔出芯片数据失去。机器不能起动 , 或能初始化 , 但不能运行。
    用真真假假方法加密 :
    擦除芯片标识。
    把 8X52 单片机 , 标成 8X51 单片机 , 并用到后 128B 的 RAM 等方法 , 把 AT90S8252 当 AT89C52, 初始化后程序段中并用到 EEPROM 内容 , 你再去联想吧 !
    用激光 ( 或丝印 ) 打上其它标识。如有的单片机引脚兼容 , 有的又不是同一种单片机 , 可张冠李戴 , 只能意会了 , 这要求你知识面广一点。
    用最新出厂编号的单片机 , 如 2000 年后的 AT89C 就难解密 , 或新的单片机品种 , 如 AVR 单片机。
DIP 封装改成 PLCC,TQFP,SOIC,BGA 等封装。
    如果量大可以做定制 ASIC, 或软封装。
    用不需外晶振的单片机工作 ( 如 AVR 单片机中的 AT90S1200) 。
    使用更复杂的单片机 FPGA+AVR+SRAM=AT40K 系列。

单片机加密方法 (3)

    硬件加密与软件加密只是为叙说方便而分开来讲 ,  其实它们是分不开的 , 互相支撑 , 互相依存的。   
    软件加密 : 其目的是不让人读懂你的程序 , 不能修改程序 , 你可以 .......
    利用单片机未公开 , 未被利用的标志位或单元 , 作为软件标志位 , 如 8031/8051 有一个用户标志位 ,PSW.1 位 , 是可以利用的。
    程序入口地址不要用整地址 , 如 :XX00H,XXX0H, 可用整地址 -1, 或 -2, 而在整地址处加二字节或三字节操作码。
在无程序的空单元也加上程序机器码 , 最好要加巧妙一点。
    用大容量芯片 , 用市场上仿真器不能仿真的芯片 , 如内部程序为 64KB 或大于 64KB 的器件 , 如 :AVR 单片机中 ATmega103 的 Flash 程序存储器为 128KB 。
    AT89S8252/AT89S53 中有 EEPROM, 关键数据存放在 EEPROM 中 , 或程序初始化时把密码写到 EEPROM 中 , 程序执行时再查密码正确与否 , 然后 ....... 。当然不能告说人家这是什么器件 , 尽量不让人家读懂程序 , 在这里说谎 , 骗人是正当防卫。
    用“真真假假 ,  假假真真” , 把几种不同品种的单片机放在同一设备中 , 如主芯片用 AVR( 说是 MCS51), 键盘显示用 AT89C2051( 说是 GAL),I/O 口扩展驱动用 PIC( 说是 AT90S1200) 等 , 当然要求你知识面广一点。如果你用高级语言 C 编写程序就简单了 , 因为 C 语言程序移植方便。
    有些国家的产品能做到三年保修 , 三年保不坏 , 三年后保坏 , 或三年后保有故障 , 可能用什么技术 ? 你去想吧。例 : 每次开机或关机 ,EEPROM 某单元加 1, 也可二个、三个单元连接起来计数 , 达到某值停止工作。
    硬件用软件代替 , 软件用硬件代替。用大规模 CPLD 可编程器件。
    关于单片机加密 , 讲到这里 , 就算抛砖引玉 , 下面请各位高手把玉亮出来吧。
    对付购买你设备 , 想不付钱或想少付钱的人 , 你可采用先供限时 ( 次 ) 使用版软件 , 钱付清下载正式版软件 ( 监控 )!

系统分类: 单片机  |  用户分类: 单片机  |  标签: 无标签  |  来源: 转贴  | 

点击查看原文

发表评论 阅读全文(603) | 回复(0)

发表于 2007/6/27 23:13:39

0

关于投票

从单片机指针说到黑客程序


2004年7月的一天,在电子BBS讨论区上溜达,看到一个有趣的帖子,整个帖子内容如下:

纯C51复位功能函数:一个大三学生,让人又爱又怕

现单列复位部分如下:

main()

{

unsigned char code rst[]={0xe4,0xc0,0xe0,0xc0,0xe0,0x32}; // 复位代码

(*((void (*)())(rst)))(); // 执行上一行代码,将rst数组当函数调用

}

本来我告诉他嵌入如下代码:

clr a

push acc

push acc

reti

结果他却玩了前面哪一段,而数组rst[]中的内容恰恰是上面的汇编机器码,他的做法是将
rst数组的数据当作代码保存,然后采用绝对地址方式指向该数组,将该数组中的代码当作
函数来运行。居然通过了!

我觉得有问题,我说即使如此,那绝对地址调用也应该写成(*((void (*)())(&rst)))()
才对呀,结果他反驳说,那样的话,rst的地址就会当成参数传递给这个绝对地址函数,而
实际LJMP调用的地址并非rst的地址,而是一个不确定的地址。于是我按照自己的说法尝试
了一下,看看汇编结果,还真的是将rst的地址传递给了R1 R2,而绝对函数最终LJMP到了
一个莫名其妙的地址上去了,死翘!

看来C真是一匹不容易驾驭的野马,这个大三学生理解力在我之上,我30多岁的人了,干了
这么多年还没他的境界呢,唉,人家才学了几天啊,翻了几天书就这么厉害了,服了!

l 首先分析帖子的C语言代码

第一句定义一个数组rst[],数组内数据就是完成复位功能的汇编机器码,具体对应关系
为:clr a == 0xe4、push acc == 0xc0,0xe0、reti ==0x32

第二句是一个函数指针的用法,函数指针用法稍微有点复杂,可参看本人著的书,:),以
下为快速入门讲解。

定义一个返回值是空函数指针的定义形式如下:

void (*p) ( )

当把函数指针赋值后,就能通过函数指针调用函数,调用形式如下,

(*p) ( );

或等价的简化形式:

p ( );

假设rst就是函数指针,则如下调用形式就可以令单片机复位再起。

(*rst ) ( );

但可惜,rst不是函数指针,而是数组名,虽然两者都是地址,但不可直接调用数组名。

如同把char型变量a赋值给int型变量b,(int) 表示强制类型转换:

b = (int) a

函数指针的强制类型转换公式如下(C语言的哲学是定义形式和使用一致):

( (void (*)() ) rst

这样经过转换后的rst就可以当作函数指针使用了,简单的调用形式如下:

#define K ( (void (*)( ) ) rst

(*K) ( )

或:

( * ( void (*)( ) )rst ) ( );

这样的语句就完成复位再启功能了。类型转换符()的优先级跟指针运算符*的优先级相同,
二者的结合方向是自右至左,所以上述语句就能完成复位功能了。保险起见有些程序员常
常喜欢再加个括号:

#define K ( ( (void (*)( ) ) rst )

(*K) ( )



( *( ( void (*)( ) )rst ) ) ( );

由于没有输入参数,上述复位代码更严谨的写法是:

#define K ( ( (void (*)(void ) ) rst )

(*K) ( )



( *( ( void (*)(void ) )rst ) ) ( );

l 关于帖子作者的解释

千万不要犯“&rst”形式的错误,对于一维数组而言,数组名rst就代表地址。以下二者等
价,更常用的是等式左边的形式:

rst == &rst[0]

整个函数指针无所谓参数传递,只是把rst当作程序执行地址调用而已,那个学生的解释也
有问题。

还有一点必须提及,不是说能通过编译,甚至生成正确代码,就表示某语句一定是对的。
对很复杂的语句,要考虑到编译器不严格甚至出错的可能性。

l 哈佛结构和一个蠕虫病毒

请注意,定义数组rst[]时用了关键字code,这是C51特有的关键字,意味着把数组定义到
程序空间。标准C是没有关键字code的。

哈佛结构和普林斯顿结构:

哈佛结构——程序空间和存储空间分开的。C51算是不太严格的哈佛结构——虽地址线分
开,但数据线没有分开。DSP是增强的哈佛结构。

PC电脑上奔腾CPU是普林斯顿结构——数据空间和程序空间统一编址。

如果数组rst[]数据的汇编机器码是删除文件的机器码,这算不算是病毒?

曾经流行过一种蠕虫病毒,其发作机理采取的就是将恶意代码保存成文本文件,然后通过
指针调用执行这个文本,很多杀毒程序也不会查询文本文件。

程序也罢,数据也罢都是二进制形式,如果数据空间和程序空间是统一编码的, 数据当然
可以当作程序运行。

在这一点上,相对而言,哈佛结构的CPU安全性会好一点点。但嵌入式应用少有病毒,一般
不用关心。

l 单片机复位的更好方法

帖子中汇编语言解释如下:

clr a //清除ACC=0

push acc //压0到堆栈——8位

push acc //再压0到堆栈——再8位

reti //返回到0地址,从而执行。

帖子作者的这种复位方法比较麻烦,更加简单的复位写法是(摘自《C缺陷与陷阱》):

( * ( void (*)( ) )0 ) ( );

本句的分析方法同上,但更加精炼,没有多余的汇编语句。

上述复位的方法可称为软件复位。

软件复位跟真正上电复位有很大差别:上电复位时大部分寄存器都有确定的复位值;软件复位则只相当于从0地址开始执行而已,寄存器不会变为确定的复位值。

如果用户要编程实现上电复位这种情况,在程序中不要踢看门狗即可。大部分单片机都有看门狗吧。

系统分类: 单片机  |  用户分类: 单片机  |  标签: 无标签  |  来源: 转贴  | 

点击查看原文

发表评论 阅读全文(590) | 回复(0)

发表于 2007/6/15 16:17:55

0

关于投票

基于LabVIEW的智能车仿真平台(转载)

基于LabVIEW的智能车仿真平台(转载)
本仿真平台是在LabVIEW图形化编程环境下开发完成的,并将做不断的优化和改进,为广大参赛队伍(飞思卡尔杯全国大学生智能车邀请赛)更好地完成开发任务而服务。

基于LabVIEW的智能车仿真平台(转载)


提供:中国测控网

引 言

2006年8月,清华大学将举办第一届&quot;飞思卡尔&quot;杯全国大学生智能车邀请赛。全国50多所著名高校将参加此项赛事,目前正在积极准备之中。

我们知道,按照传统的开发思想,主要分为制造赛道,硬件布置、控制算法,通过实车的调试再进行相应的修改,如此循环,如图1所示。这种模式具有成本高,开发周期长以及试验无法重现和归档等缺陷。

图1 智能车开发传统模式

如果换一种思路,采用虚拟开发模式,先进行虚拟仿真,得到优化的硬件布置和控制算法后,再进行实车开发,这样将大大提高效率和降低成本,见图2。正是在这样的构想下,我们开发了Plastid智能仿真软件。值得一提的是,在韩国4届大赛中,还没有类似软件出现,因此它具有一定的独创性。


图2 智能车开发虚拟模式

V型开发模式

软件的开发流程,分为开发平台、仿真内核、操作界面以及匹配标定,由于有&quot;匹配标定&quot;这一个环节,因此我们采取软硬件同步开发的V型开发模式,如图3所示。

图3 V型开发模式

我们采用LabVIEW作为软件的开发平台,是因为图形化编程环境LabVIEW,满足建立灵活的可扩展式测试测量和控制应用系统的要求,同时满足以最小成本最快速地开发系统的需求。LabVIEW支持多任务,同时对外设有C语言接口。选用LabVIEW开发还可以提高程序的可靠性。

内核算法涉及汽车专业知识,如图4所示,在每个计算周期中,系统首先计算出传感器输出和赛车车速,输入智能车控制算法中,通过匹配标定单元可得出加速度和前轮转角,在刚体的运动模型算法中得出下一计算周期的车速和赛车坐标。

利用LabVIEW简单易用的GUI控件,可以完全按使用者的需求进行界面的设计。Plastid系统的使用界面做到了美观、大方,简明且操作方便,符合人机工程学。

如图3所示,软件开发的最后环节为匹配标定过程。通过大量的实车试验,我们得到驱动电机Map图,转向性能,加减速性能以及舵机转向性能等实车参数,并将其补充进内核算法中的匹配标定单元(图4)中,从而完善了Plastid。


图4 内核算法图

系统构架

图5是整个仿真系统的构架图,主要分为基本模型层、控制算法层、通讯层以及仿真环境层。


图5 Plastid平台构架图

基本模型层包括赛车模型与赛道模型,使用者可根据实际情况设定模型参数,它为整个系统提供了底层的驱动,仿真结果都是在这两个模型的基础上计算得来的。

一个具有高级控制策略的智能车应该在不同的赛道上都具有稳定的发挥。在Plastid中很容易解决避免制作很多实际赛道困难,如图6所示,我们可以用点、弧、手绘等方式方便地设计出各种赛道进行仿真使用设计出不同的赛道,并将其保存成文件,在仿真时将其调用即可。

点击看大图
图6 赛道设计界面

赛车也是整个仿真的基础模型之一。如图7所示,在赛车参数面板中,左边可以任意设置赛车的几何参数,右边传感器布置区支持各种传感器布置方式的设定。试驾功能可以在设计阶段对赛车的运动和传感器性能进行测试,提高了开发效率。

点击看大图
图7 赛车参数界面

控制算法层为使用者提供了3种不同的仿真方案:SubVI、C结点以及单片机在线仿真。使用者可选择其中一个方案输入或移植自己的控制算法。

通讯层只用于单片机的在线仿真,使用CAN模块,可以使单片机与仿真系统进行即时的数据交流,从而实现动态仿真。

首先,最接近于LabVIEW编程环境的即为SubVI方案。此方案对于熟悉LabVIEW G语言编程方法的使用者来说非常简单,但将单片机的控制算法转换为子VI的程序需要一个过程。

其次,C结点方案则更适合于采用C语言编程的使用者们,其程序直接用C语言编写,用Visual Studio IDE将其编译为dll文件,系统在仿真时会自动调用该dll,从而实现与SubVI一样的控制和反馈。

最后,利用CAN或串口模块,系统可以直接与单片机进行直接通讯,并实现在线仿真。单片机方面只需要在其CAN接口或串口即时地传送其控制量,而Plastid则通过模块得到这些量,并传送反馈量给单片机。

动态仿真环境层基于赛车、赛道模型以及控制算法所输出的控制信号(电机控制、转向控制及车速信号等),计算出车的行走路线,并即时地将数据传回控制算法层(其计算周期可调)。

在比赛仿真界面中,通过调用控制算法、赛道、赛车,可以在仿真内核上进行仿真运算(对应调用各个文件)。如图8,界面上实时显示车速、前轮转角等参量的变化。仿真后,可以将仿真过程保存,以供回放时调用。

点击看大图
图8 比赛仿真界面


凭借LabVIEW软件的优势,系统可以方便地将仿真过程中的各种变量记录下来,特别是一些实际试验时无法测量的量,保存于文件中。在回放模式中,用户可以调用这些文件,对其仿真结果进行后期分析和处理,从而可以更准确地发现问题,指导赛车设置和控制算法的优化。

成果

通过测试和对比,Plastid智能车仿真平台可以有效地仿真出赛车的实际行驶路线以及直道波动、弯道超调、弯道回转以及交替弯道等现象,具有相当高的仿真精度。

此外,基于LabVIEW的本仿真平台还作为本次智能车大赛的官方软件,与赛车一起配套发布,给其他参赛者们提供了帮助和服务。我们给参赛队提供多次现场培训以及网上在线答疑,使他们能够迅速掌握软件的使用。凭借出色的创意和较高的实用性,Plastid仿真平台在今年清华大学第二届NI杯虚拟仪器设计大赛中获得第一名和最佳创新奖。在清华大学第二十三届&quot;挑战杯&quot;学生课外学术科技作品竞赛中获得了一等奖的殊荣。

综上所述,本仿真平台是在LabVIEW图形化编程环境下开发完成的,并将做不断的优化和改进,为广大参赛队伍更好地完成开发任务而服务。

系统分类: 单片机  |  用户分类: 单片机  |  标签: 无标签  |  来源: 整理  | 

点击查看原文

发表评论 阅读全文(1120) | 回复(0)

发表于 2007/6/13 21:11:58

2

关于投票

在51系列单片机上移植uCOS-II

内容摘要:本文详细系统地介绍了uC/OS-II在51单片机上的移植、重入实现方法、硬件仿真、固化、人机界面等关键内容。
关键词:嵌入式实时多任务操作系统、uC/OS-II、C51 

 
引言:随着各种应用电子系统的复杂化和系统实时性需求的提高,并伴随应用软件朝着系统化方向发展的加速,在16位/32位单片机中广泛使用了嵌入式实时操作系统。然而实际使用中却存在着大量8位单片机,从经济性考虑,对某些应用场合,在8位MCU上使用操作系统是可行的。从学习操作系统角度,uC/OS-II for 51即简单又全面,学习成本低廉,值得推广。
结语:μC/OS-II具有免费、简单、可靠性高、实时性好等优点,但也有缺乏便利开发环境等缺点,尤其不像商用嵌入式系统那样得到广泛使用和持续的研究更新。但开放性又使得开发人员可以自行裁减和添加所需的功能,在许多应用领域发挥着独特的作用。当然,是否在单片机系统中嵌入μC/OS-II应视所开发的项目而定,对于一些简单的、低成本的项目来说,就没必要使用嵌入式操作系统了。

uC/OS-II原理:
uCOSII包括任务调度、时间管理、内存管理、资源管理(信号量、邮箱、消息队列)四大部分,没有文件系统、网络接口、输入输出界面。它的移植只与4个文件相关:汇编文件(OS_CPU_A.ASM)、处理器相关C文件(OS_CPU.H、OS_CPU_C.C)和配置文件(OS_CFG.H)。有64个优先级,系统占用8个,用户可创建56个任务,不支持时间片轮转。它的基本思路就是 “近似地每时每刻总是让优先级最高的就绪任务处于运行状态” 。为了保证这一点,它在调用系统API函数、中断结束、定时中断结束时总是执行调度算法。原作者通过事先计算好数据,简化了运算量,通过精心设计就绪表结构,使得延时可预知。任务的切换是通过模拟一次中断实现的。
uCOSII工作核心原理是:近似地让最高优先级的就绪任务处于运行状态。
操作系统将在下面情况中进行任务调度:调用API函数(用户主动调用),中断(系统占用的时间片中断OsTimeTick(),用户使用的中断)。
调度算法书上讲得很清楚,我主要讲一下整体思路。
(1)在调用API函数时,有可能引起阻塞,如果系统API函数察觉到运行条件不满足,需要切换就调用OSSched()调度函数,这个过程是系统自动完成的,用户没有参与。OSSched()判断是否切换,如果需要切换,则此函数调用OS_TASK_SW()。这个函数模拟一次中断(在51里没有软中断,我用子程序调用模拟,效果相同),好象程序被中断打断了,其实是OS故意制造的假象,目的是为了任务切换。既然是中断,那么返回地址(即紧邻OS_TASK_SW()的下一条汇编指令的PC地址)就被自动压入堆栈,接着在中断程序里保存CPU寄存器(PUSHALL)……。堆栈结构不是任意的,而是严格按照uCOSII规范处理。OS每次切换都会保存和恢复全部现场信息(POPALL),然后用RETI回到任务断点继续执行。这个断点就是OSSched()函数里的紧邻OS_TASK_SW()的下一条汇编指令的PC地址。切换的整个过程就是,用户任务程序调用系统API函数,API调用OSSched(),OSSched()调用软中断OS_TASK_SW()即OSCtxSw,返回地址(PC值)压栈,进入OSCtxSw中断处理子程序内部。反之,切换程序调用RETI返回紧邻OS_TASK_SW()的下一条汇编指令的PC地址,进而返回OSSched()下一句,再返回API下一句,即用户程序断点。因此,如果任务从运行到就绪再到运行,它是从调度前的断点处运行。
(2)中断会引发条件变化,在退出前必须进行任务调度。uCOSII要求中断的堆栈结构符合规范,以便正确协调中断退出和任务切换。前面已经说到任务切换实际是模拟一次中断事件,而在真正的中断里省去了模拟(本身就是中断嘛)。只要规定中断堆栈结构和uCOSII模拟的堆栈结构一样,就能保证在中断里进行正确的切换。任务切换发生在中断退出前,此时还没有返回中断断点。仔细观察中断程序和切换程序最后两句,它们是一模一样的,POPALL+RETI。即要么直接从中断程序退出,返回断点;要么先保存现场到TCB,等到恢复现场时再从切换函数返回原来的中断断点(由于中断和切换函数遵循共同的堆栈结构,所以退出操作相同,效果也相同)。用户编写的中断子程序必须按照uCOSII规范书写。任务调度发生在中断退出前,是非常及时的,不会等到下一时间片才处理。OSIntCtxSw()函数对堆栈指针做了简单调整,以保证所有挂起任务的栈结构看起来是一样的。
(3)在uCOSII里,任务必须写成两种形式之一(《uCOSII中文版》p99页)。在有些RTOS开发环境里没有要求显式调用OSTaskDel(),这是因为开发环境自动做了处理,实际原理都是一样的。uCOSII的开发依赖于编译器,目前没有专用开发环境,所以出现这些不便之处是可以理解的。
移植过程:
(1)拷贝书后附赠光盘sourcecode目录下的内容到C:\YY下,删除不必要的文件和EX1L.C,只剩下p187(《uCOSII》)上列出的文件。
(2)改写最简单的OS_CPU.H
数据类型的设定见C51.PDF第176页。注意BOOLEAN要定义成unsigned char 类型,因为bit类型为C51特有,不能用在结构体里。
EA=0关中断;EA=1开中断。这样定义即减少了程序行数,又避免了退出临界区后关中断造成的死机。
MCS-51堆栈从下往上增长(1=向下,0=向上),OS_STK_GROWTH定义为0
#define OS_TASK_SW() OSCtxSw() 因为MCS-51没有软中断指令,所以用程序调用代替。两者的堆栈格式相同,RETI指令复位中断系统,RET则没有。实践表明,对于MCS-51,用子程序调用入栈,用中断返回指令RETI出栈是没有问题的,反之中断入栈RET出栈则不行。总之,对于入栈,子程序调用与中断调用效果是一样的,可以混用。在没有中断发生的情况下复位中断系统也不会影响系统正常运行。详见《uC/OS-II》第八章193页第12行
(3)改写OS_CPU_C.C
我设计的堆栈结构如下图所示:

 

TCB结构体中OSTCBStkPtr总是指向用户堆栈最低地址,该地址空间内存放用户堆栈长度,其上空间存放系统堆栈映像,即:用户堆栈空间大小=系统堆栈空间大小+1。
SP总是先加1再存数据,因此,SP初始时指向系统堆栈起始地址(OSStack)减1处(OSStkStart)。很明显系统堆栈存储空间大小=SP-OSStkStart。
任务切换时,先保存当前任务堆栈内容。方法是:用SP-OSStkStart得出保存字节数,将其写入用户堆栈最低地址内,以用户堆栈最低地址为起址,以OSStkStart为系统堆栈起址,由系统栈向用户栈拷贝数据,循环SP-OSStkStart次,每次拷贝前先将各自栈指针增1。
其次,恢复最高优先级任务系统堆栈。方法是:获得最高优先级任务用户堆栈最低地址,从中取出“长度”,以最高优先级任务用户堆栈最低地址为起址,以OSStkStart为系统堆栈起址,由用户栈向系统栈拷贝数据,循环“长度”数值指示的次数,每次拷贝前先将各自栈指针增1。
用户堆栈初始化时从下向上依次保存:用户堆栈长度(15),PCL,PCH,PSW,ACC,B,DPL,DPH,R0,R1,R2,R3,R4,R5,R6,R7。不保存SP,任务切换时根据用户堆栈长度计算得出。
OSTaskStkInit函数总是返回用户栈最低地址。
操作系统tick时钟我使用了51单片机的T0定时器,它的初始化代码用C写在了本文件中。
最后还有几点必须注意的事项。本来原则上我们不用修改与处理器无关的代码,但是由于KEIL编译器的特殊性,这些代码仍要多处改动。因为KEIL缺省情况下编译的代码不可重入,而多任务系统要求并发操作导致重入,所以要在每个C函数及其声明后标注reentrant关键字。另外,“pdata”、“data”在uCOS中用做一些函数的形参,但它同时又是KEIL的关键字,会导致编译错误,我通过把“pdata”改成“ppdata”,“data”改成“ddata”解决了此问题。OSTCBCur、OSTCBHighRdy、OSRunning、OSPrioCur、OSPrioHighRdy这几个变量在汇编程序中用到了,为了使用Ri访问而不用DPTR,应该用KEIL扩展关键字IDATA将它们定义在内部RAM中。
(4)重写OS_CPU_A.ASM
A51宏汇编的大致结构如下:
>定义重定位段 必须按照C51格式定义,汇编遵守C51规范。段名格式为:?PR?函数名?模块名
声明引用全局变量和外部子程序 注意关键字为“EXTRN”没有‘E’
全局变量名直接引用
无参数/无寄存器参数函数 FUNC
带寄存器参数函数 _FUNC
重入函数 _?FUNC
分配堆栈空间
只关心大小,堆栈起点由keil决定,通过标号可以获得keil分配的SP起点。切莫自己分配堆栈起点,只要用DS通知KEIL预留堆栈空间即可。
?STACK段名与STARTUP.A51中的段名相同,这意味着KEIL在LINK时将把两个同名段拼在一起,我预留了40H个字节,STARTUP.A51预留了1个字节,LINK完成后堆栈段总长为41H。查看yy.m51知KEIL将堆栈起点定在21H,长度41H,处于内部RAM中。
定义宏
宏名 MACRO 实体 ENDM
子程序
OSStartHighRdy
OSCtxSw
OSIntCtxSw
OSTickISR
SerialISR
END ;声明汇编源文件结束

一般指针占3字节。+0类型+1高8位数据+2低8位数据 详见C51.PDF第178页
低位地址存高8位值,高位地址存低8位值。例如0x1234,基址+0:0x12 基址+1:0x34

(5)移植串口驱动程序
在此之前我写过基于中断的串口驱动程序,包括打印字节/字/长字/字符串,读串口,初始化串口/缓冲区。把它改成重入函数即可直接使用。
系统提供的显示函数是并发的,它不是直接显示到串口,而是先输出到显存,用户不必担心IO慢速操作影响程序运行。串口输入也采用了同样的技术,他使得用户在CPU忙于处理其他任务时照样可以盲打输入命令。
(6)编写测试程序Demo(YY.C)
Demo程序创建了3个任务A、B、C优先级分别为2、3、4,A每秒显示一次,B每3秒显示一次,C每6秒显示一次。从显示结果看,显示3个A后显示1个B,显示6个A和2个B后显示1个C,结果显然正确。
显示结果如下:
AAAAAA111111 is active
AAAAAA111111 is active
AAAAAA111111 is active
BBBBBB333333 is active
AAAAAA111111 is active
AAAAAA111111 is active
AAAAAA111111 is active
BBBBBB333333 is active
CCCCCC666666 is active
AAAAAA111111 is active
AAAAAA111111 is active
AAAAAA111111 is active
BBBBBB333333 is active
AAAAAA111111 is active
AAAAAA111111 is active
AAAAAA111111 is active
BBBBBB333333 is active
CCCCCC666666 is active
Demo程序经Keil701编译后,代码量为7-8K,可直接在KeilC51上仿真运行。
编译时要将OS_CPU_C.C、UCOS_II.C、OS_CPU_A.ASM、YY.C加入项目

文件名 : OS_CPU_A.ASM

$NOMOD51
EA BIT 0A8H.7
SP DATA 081H
B DATA 0F0H
ACC DATA 0E0H
DPH DATA 083H
DPL DATA 082H
PSW DATA 0D0H
TR0 BIT 088H.4
TH0 DATA 08CH
TL0 DATA 08AH

>
定义重定位段
?PR?OSStartHighRdy?OS_CPU_A SEGMENT CODE
?PR?OSCtxSw?OS_CPU_A SEGMENT CODE
?PR?OSIntCtxSw?OS_CPU_A SEGMENT CODE
?PR?OSTickISR?OS_CPU_A SEGMENT CODE

?PR?_?serial?OS_CPU_A SEGMENT CODE

声明引用全局变量和外部子程序
EXTRN IDATA (OSTCBCur)
EXTRN IDATA (OSTCBHighRdy)
EXTRN IDATA (OSRunning)
EXTRN IDATA (OSPrioCur)
EXTRN IDATA (OSPrioHighRdy)

EXTRN CODE (_?OSTaskSwHook)
EXTRN CODE (_?serial)
EXTRN CODE (_?OSIntEnter)
EXTRN CODE (_?OSIntExit)
EXTRN CODE (_?OSTimeTick)

对外声明4个不可重入函数
PUBLIC OSStartHighRdy
PUBLIC OSCtxSw
PUBLIC OSIntCtxSw
PUBLIC OSTickISR

PUBLIC SerialISR

分配堆栈空间。只关心大小,堆栈起点由keil决定,通过标号可以获得keil分配的SP起点。
?STACK SEGMENT IDATA
RSEG ?STACK
OSStack:
DS 40H
OSStkStart IDATA OSStack-1

定义压栈出栈宏
PUSHALL MACRO
PUSH PSW
PUSH ACC
PUSH B
PUSH DPL
PUSH DPH
MOV A,R0 ;R0-R7入栈
PUSH ACC
MOV A,R1
PUSH ACC
MOV A,R2
PUSH ACC
MOV A,R3
PUSH ACC
MOV A,R4
PUSH ACC
MOV A,R5
PUSH ACC
MOV A,R6
PUSH ACC
MOV A,R7
PUSH ACC
PUSH SP ;不必保存SP,任务切换时由相应程序调整
ENDM

POPALL MACRO
POP ACC ;不必保存SP,任务切换时由相应程序调整
POP ACC ;R0-R7出栈
MOV R7,A
POP ACC
MOV R6,A
POP ACC
MOV R5,A
POP ACC
MOV R4,A
POP ACC
MOV R3,A
POP ACC
MOV R2,A
POP ACC
MOV R1,A
POP ACC
MOV R0,A
POP DPH
POP DPL
POP B
POP ACC
POP PSW
ENDM

子程序
-------------------------------------------------------------------------
RSEG ?PR?OSStartHighRdy?OS_CPU_A
OSStartHighRdy:
USING 0 ;上电后51自动关中断,此处不必用CLR EA指令,因为到此处还未开中断,本程序退出后,开中断。
LCALL _?OSTaskSwHook

OSCtxSw_in:

OSTCBCur ===> DPTR 获得当前TCB指针,详见C51.PDF第178页
MOV R0,#LOW (OSTCBCur) ;获得OSTCBCur指针低地址,指针占3字节。+0类型+1高8位数据+2低8位数据
INC R0
MOV DPH,@R0 ;全局变量OSTCBCur在IDATA中
INC R0
MOV DPL,@R0

OSTCBCur->OSTCBStkPtr ===> DPTR 获得用户堆栈指针
INC DPTR ;指针占3字节。+0类型+1高8位数据+2低8位数据
MOVX A,@DPTR ;.OSTCBStkPtr是void指针
MOV R0,A
INC DPTR
MOVX A,@DPTR
MOV R1,A
MOV DPH,R0
MOV DPL,R1

*UserStkPtr ===> R5 用户堆栈起始地址内容(即用户堆栈长度放在此处) 详见文档说明 指针用法详见C51.PDF第178页
MOVX A,@DPTR ;用户堆栈中是unsigned char类型数据
MOV R5,A ;R5=用户堆栈长度

恢复现场堆栈内容
MOV R0,#OSStkStart

restore_stack:

INC DPTR
INC R0
MOVX A,@DPTR
MOV @R0,A
DJNZ R5,restore_stack

恢复堆栈指针SP
MOV SP,R0

OSRunning=TRUE
MOV R0,#LOW (OSRunning)
MOV @R0,#01

POPALL
SETB EA ;开中断
RETI
-------------------------------------------------------------------------
RSEG ?PR?OSCtxSw?OS_CPU_A
OSCtxSw:
PUSHALL

OSIntCtxSw_in:

获得堆栈长度和起址
MOV A,SP
CLR C
SUBB A,#OSStkStart
MOV R5,A ;获得堆栈长度

OSTCBCur ===> DPTR 获得当前TCB指针,详见C51.PDF第178页
MOV R0,#LOW (OSTCBCur) ;获得OSTCBCur指针低地址,指针占3字节。+0类型+1高8位数据+2低8位数据
INC R0
MOV DPH,@R0 ;全局变量OSTCBCur在IDATA中
INC R0
MOV DPL,@R0

OSTCBCur->OSTCBStkPtr ===> DPTR 获得用户堆栈指针
INC DPTR ;指针占3字节。+0类型+1高8位数据+2低8位数据
MOVX A,@DPTR ;.OSTCBStkPtr是void指针
MOV R0,A
INC DPTR
MOVX A,@DPTR
MOV R1,A
MOV DPH,R0
MOV DPL,R1

保存堆栈长度
MOV A,R5
MOVX @DPTR,A

MOV R0,#OSStkStart ;获得堆栈起址
save_stack:

INC DPTR
INC R0
MOV A,@R0
MOVX @DPTR,A
DJNZ R5,save_stack

调用用户程序
LCALL _?OSTaskSwHook

OSTCBCur = OSTCBHighRdy
MOV R0,#OSTCBCur
MOV R1,#OSTCBHighRdy
MOV A,@R1
MOV @R0,A
INC R0
INC R1
MOV A,@R1
MOV @R0,A
INC R0
INC R1
MOV A,@R1
MOV @R0,A

OSPrioCur = OSPrioHighRdy 使用这两个变量主要目的是为了使指针比较变为字节比较,以便节省时间。
MOV R0,#OSPrioCur
MOV R1,#OSPrioHighRdy
MOV A,@R1
MOV @R0,A

LJMP OSCtxSw_in
-------------------------------------------------------------------------
RSEG ?PR?OSIntCtxSw?OS_CPU_A

OSIntCtxSw:

调整SP指针去掉在调用OSIntExit(),OSIntCtxSw()过程中压入堆栈的多余内容
SP=SP-4

MOV A,SP
CLR C
SUBB A,#4
MOV SP,A

LJMP OSIntCtxSw_in
-------------------------------------------------------------------------
CSEG AT 000BH ;OSTickISR
LJMP OSTickISR ;使用定时器0
RSEG ?PR?OSTickISR?OS_CPU_A

OSTickISR:

USING 0
PUSHALL

CLR TR0
MOV TH0,#70H ;定义Tick=50次/秒(即0.02秒/次)
MOV TL0,#00H ;OS_CPU_C.C 和 OS_TICKS_PER_SEC
SETB TR0

LCALL _?OSIntEnter
LCALL _?OSTimeTick
LCALL _?OSIntExit
POPALL
RETI
-------------------------------------------------------------------------
CSEG AT 0023H ;串口中断
LJMP SerialISR ;工作于系统态,无任务切换。
RSEG ?PR?_?serial?OS_CPU_A

SerialISR:

USING 0
PUSHALL
CLR EA
LCALL _?serial
SETB EA
POPALL
RETI
-------------------------------------------------------------------------
END
-------------------------------------------------------------------------

文件名 : OS_CPU_C.C

void *OSTaskStkInit (void (*task)(void *pd), void *ppdata, void *ptos, INT16U opt) reentrant
{
OS_STK *stk;

ppdata = ppdata;
opt = opt; //opt没被用到,保留此语句防止告警产生
stk = (OS_STK *)ptos; //用户堆栈最低有效地址
*stk++ = 15; //用户堆栈长度
*stk++ = (INT16U)task & 0xFF; //任务地址低8位
*stk++ = (INT16U)task >> 8; //任务地址高8位
*stk++ = 0x00; //PSW
*stk++ = 0x0A; //ACC
*stk++ = 0x0B; //B
*stk++ = 0x00; //DPL
*stk++ = 0x00; //DPH
*stk++ = 0x00; //R0
*stk++ = 0x01; //R1
*stk++ = 0x02; //R2
*stk++ = 0x03; //R3
*stk++ = 0x04; //R4
*stk++ = 0x05; //R5
*stk++ = 0x06; //R6
*stk++ = 0x07; //R7
//不用保存SP,任务切换时根据用户堆栈长度计算得出。
return ((void *)ptos);
}

#if OS_CPU_HOOKS_EN
void OSTaskCreateHook (OS_TCB *ptcb) reentrant
{
ptcb = ptcb; /* Prevent compiler warning */
}

void OSTaskDelHook (OS_TCB *ptcb) reentrant
{
ptcb = ptcb; /* Prevent compiler warning */
}

void OSTimeTickHook (void) reentrant
{
}
#endif

//初始化定时器0
void InitTimer0(void) reentrant
{
TMOD=TMOD&0xF0;
TMOD=TMOD|0x01; //模式1(16位定时器),仅受TR0控制
TH0=0x70; //定义Tick=50次/秒(即0.02秒/次)
TL0=0x00; //OS_CPU_A.ASM 和 OS_TICKS_PER_SEC
ET0=1; //允许T0中断
TR0=1;
}

文件名 : YY.C

#i nclude

#define MAX_STK_SIZE 64

void TaskStartyya(void *yydata) reentrant;
void TaskStartyyb(void *yydata) reentrant;
void TaskStartyyc(void *yydata) reentrant;

OS_STK TaskStartStkyya[MAX_STK_SIZE+1];//注意:我在ASM文件中设置?STACK空间为40H即64,不要超出范围。
OS_STK TaskStartStkyyb[MAX_STK_SIZE+1];//用户栈多一个字节存长度
OS_STK TaskStartStkyyc[MAX_STK_SIZE+1];

void main(void)
{
OSInit();

InitTimer0();
InitSerial();
InitSerialBuffer();

OSTaskCreate(TaskStartyya, (void *)0, &TaskStartStkyya[0],2);
OSTaskCreate(TaskStartyyb, (void *)0, &TaskStartStkyyb[0],3);
OSTaskCreate(TaskStartyyc, (void *)0, &TaskStartStkyyc[0],4);

OSStart();
}


void TaskStartyya(void *yydata) reentrant
{
yydata=yydata;
clrscr();
PrintStr("\n\t\t*******************************\n");
PrintStr("\t\t* Hello! The world. *\n");
PrintStr("\t\t*******************************\n\n\n");

for(;;){
PrintStr("\tAAAAAA111111 is active.\n");
OSTimeDly(OS_TICKS_PER_SEC);
}
}

void TaskStartyyb(void *yydata) reentrant
{
yydata=yydata;

for(;;){
PrintStr("\tBBBBBB333333 is active.\n");
OSTimeDly(3*OS_TICKS_PER_SEC);
}
}

void TaskStartyyc(void *yydata) reentrant
{
yydata=yydata;

for(;;){
PrintStr("\tCCCCCC666666 is active.\n");
OSTimeDly(6*OS_TICKS_PER_SEC);
}
}

重入问题的解决:
任务函数中带有形参和局部变量时若使用reentrant关键字会引起重入,从C51.PDF 129-131页的内容知:为了函数重入,形参和局部变量必须保存在堆栈里,由于51硬件堆栈太小,KEIL将根据内存模式在相应内存空间仿真堆栈(生长方向由上向下,与硬件栈相反)。对于大模式编译,函数返回地址保存在硬件堆栈里,形参和局部变量放在仿真堆栈中,栈指针为?C_XBP,XBPSTACK=1时,起始值在startup.a51中初始化为FFFFH+1。仿真堆栈效率低下,KEIL建议尽量不用,但为了重入操作必须使用。KEIL可以混合使用3种仿真堆栈(大、中、小模式),为了提高效率,针对51推荐统一使用大模式编译。
为了支持重入,重新设计了堆栈结构(如下图)。增加了保存仿真堆栈指针?C_XBP和堆栈内容的数据结构。相应改变的文件有:OS_CPU_A.ASM、OS_CPU_C.C、OS_CPU.H、YY.C。由图可知,用户栈中保存的仿真栈与硬件栈相向生长,中间为空闲间隔,显然uCOSII的堆栈检测函数失效。硬件栈的保存恢复详见上节,仿真堆栈的保存与8086移植中的一样,OS只提供堆栈空间和只操作堆栈指针,不进行内存拷贝,效率相对很高。
建议使用统一的固定大小的堆栈空间,尽管uCOSII原作者把不同任务使用不同空间看成是优点,但为了在51上有效实现任务重入,针对51笔者还是坚持不使用这个优点。
用户堆栈空间的大小是可以精确计算出来的。用户堆栈空间=硬件堆栈空间+仿真堆栈空间。硬件栈占用内部RAM,内部RAM执行效率高,如果堆栈空间过大,会影响KEIL编译的程序性能。如果堆栈空间小,在中断嵌套和程序调用时会造成系统崩溃。综合考虑,我把硬件堆栈空间大小定成了64字节,用户根据实际情况可以自行设定。仿真堆栈大小取决于形参和局部变量的类型及数量,可以精确算出。因为所有用户栈使用相同空间大小,所以取占用空间最大的任务函数的空间大小为仿真堆栈空间大小。这样用户堆栈空间大小就唯一确定了。我将用户堆栈空间大小用宏定义在OS_CFG.H文件中,宏名为MaxStkSize。
51的SP只有8位,无法在64K空间中自由移动,只好采用拷贝全部硬件堆栈内容的笨办法。51 本来就弱,这么一来缺点更明显了。其实,引入OS必然要付出代价,一般OS要占用CPU10%-20%的负荷能力,请权衡利弊决定。切换频率决定了CPU的耗费,频率越高耗费越大,大到一定程度就该换更强的CPU了。我选了50Hz的切换频率,不高也不低,用户可以根据需要自行定夺。在耗费无法避免的情况下,我采取了几个措施来提高效率:1。ret和reti混用减少代码;2。IE、SP不入出栈,通过另外方式解决;3。用IDATA关键字声明在汇编中用到的全局变量,变DPTR操作为Ri操作;4。设计堆栈结构,简化算法;5。让串口输入输出工作在系统态,不占用任务TCB和优先级,增加弹性缓冲区,减少等待。

在51单片机上硬件仿真uCOS51的说明:
zyware网友2002/11/22来信询问uCOS51在单片机上的硬件仿真问题,具体情况是“在51上用uCOS51核,以及一些构件,keilc上仿真通过,用wave接硬件仿真程序乱飞,wave仿真以前的程序没有问题,不知是何缘故”。
由于我的OS程序已经在KEIL软件仿真和硬件上实际测试过,所以不可能是程序错。可能的原因只能是硬件仿真软件设置问题。本人用的是Medwin软件,在Insight上调试,使用uCOS51编译测试程序一样跑飞。即使添加修改后的startup.a51(详见《在51单片机上固化uCOS51的说明》)也不正常。我发现Medwin似乎没有编译startup.a51,因为它把该文件加在了other Files目录下而不是source Files目录,于是我猜测只有放在source Files目录下的文件才被编译。由观察知,以.c和.asm做后缀的文件均被放在此目录下且被编译。于是我立即将startup.a51改成startup.asm并加入项目编译,结果测试正常。不必担心startup改名造成冲突,KEIL在链接目标文件时会自动处理重名段,本目录的文件优先级高(我是这么理解的,具体原理不清楚,这只是根据实践得到的结论,希望了解此处理过程的朋友能告之,不胜感激。)。

具体做法如下:
1。按《在51单片机上固化uCOS51的说明》一文修改startup.a51,并将其更名为startup.asm。
2。将startup.asm、yy1.c、os_cpu_c.c、ucos_ii.c、os_cpu_a.asm五个文件加入项目编译。
3。运行

在51单片机上固化uCOS51的说明:
近来,收到多位网友来信询问uCOS51在51单片机上的固化问题,归纳其焦点就是:为什么OS在KeilC51上模拟可以正常运行,但把它烧录在CPU上却不能工作?理论上,程序在软件仿真通过测试后,将其烧录在硬件上,硬件调试应该一次成功。许多网友也有这个经验,可为什么在调试uCOS51时失效了呢?难道操作系统调试很特殊吗?
其实问题出在重入函数的引入。原来KEILC51软件仿真在不修改startup.a51文件的情况下,缺剩使用64K外部RAM,它把0000H-FFFFH全部仿真为可读写的RAM,而用户的硬件系统可能没有用到那么大的RAM空间,比如只用了8K/16K/32K等,或者用户把一些地址空间映射给了别的设备,比如8019AS等。在没有调用OSTaskCreate前,定义为reentrant的函数将用FFE0H做仿真堆栈栈顶指针,而此处在用户的系统里不是RAM,造成程序跑飞。比如在我的用户板上,将FE00H-FFFFH空间的一部分分配给8019AS使用,如果把demo程序编译后直接烧到51上,将不能运行。解决办法是根据系统RAM配置,修改startup.a51文件,并将其加入项目编译,如下所示:

XBPSTACK EQU 1 ; set to 1 if large reentrant is used.
XBPSTACKTOP EQU 07FFFH+1; set top of stack to highest location+1.

按此修改后,在有32K外部RAM的系统上可以正常运行。用户可根据自己XRAM的实际配置情况修改startup.a51相关参数,并将其添加到项目里编译。不必理会KEIL/C51/LIB目录下的同名文件,此处的startup.a51优先级高,KEIL将按此处该文件的配置编译项目。
这也解释了有些网友问到的,“为什么加入reentrant关键字,在软件仿真时正确,烧在芯片上就死机,去掉reentrant后两者都正常”的问题。由于大多数人很少使用重入函数,往往不了解这个细节,特此提请大家注意。

关于uCOS51不能正常工作的原因还可能是因为串口波特率和OS_TICKS_PER_SEC及TH0、TL0设置不正确引起的。demo程序默认使用22.1184MHz晶体,19200波特率,切换频率为50Hz。为此,1。在SERIAL.C中设置“TL1=0xFD;TH1=0xFD;”使波特率为19200;2。在OS_CPU_C.C和OS_CPU_A.ASM中设置“TH0=0x70;TL0=0x00;”使时钟节拍tick=50次/秒;3。在OS_CFG.H中设置OS_TICKS_PER_SEC为50Hz。用户应根据实际情况,相应地修改这些参数,否则运行不正确。

定时器初值设置:

定时器0用于时钟节拍发生器
//*****************************************************************************
//初值计算公式:
// (2^16-x)*F=Fosc/12
// 其中:F=时钟节拍频率tick;Fosc=晶体或晶振频率;x=初值;
// 本例中,F=50;Fosc=21.1184MHz;所以x=0x7000。
//*****************************************************************************

定时器1用于波特率发生器
//*****************************************************************************
//初值计算公式:
// TH1=256-(2^SMOD/32*Fosc/12*1/Bound)
// 其中:SMOD=0,1;Fosc=晶体或晶振频率;Bound=波特率
// 本例中,SMOD=0;Fosc=21.1184MHz;Bound=19200,所以TH1=0xFD。
//*****************************************************************************

demo程序项目中增加按如上方法改写的startup.a51后,在我的用户板硬件上运行正确。

为uCOS51增加Shell界面:
uCOSII只提供了操作系统内核,用户要自己添加文件处理、人机界面、网络接口等重要部分。其中Shell(人机界面)提供了人与机器交互的界面,是机器服务于人的体现,是系统必不可少的重要组成部分。现代的很多OS如UNIX、DOS、VxWorks都提供了友好的命令行界面。Windows更是提供了GUI。大部分人认识OS都是从这里开始的。uCOS51同样拥有Shell,它是我从以前写的前后台程序中移植过来的。

命令行Shell的工作原理比较简单,主要思路就是单片机接收用户键盘输入的字符存入命令缓冲区,并回显到屏幕,当用户按下回车键,触发软件状态机状态变迁,从输入态转移到命令解释态,然后根据用户命令调用相关子程序执行相应操作,执行完毕后重新回到输入态。
我感觉原理很好掌握,程序也不长,但是细节部分要反复调试多次才能稳定工作。比如:命令行左右边界的保护、退格键的处理、词表的设计等等。
Shell程序由词表、取词子程序、状态机框架程序(输入回显和命令解释执行)、命令相关子程序组成(详见源程序清单)。
词表结构如程序清单所示,由词数目,左括号数,右括号数,每个词的具体信息(长度,字符串)构成。左右括号数用于括号匹配检查;词数目用于程序循环;词的具体信息作为解释/执行程序的输入参数。
取词子程序从命令行语句中提取单词并存入词表同时进行匹配检查和词法分析。默认字符为:0-9、a-z、A-Z、'.';定界符为:空格、逗号,左/右括号。建议用户补充默认字符集(? / \ -)以便实现更灵活的语法。注意:现在版本的Shell只检查左右括号数量的匹配,无优先级和语法含义。
输入回显程序循环检查用户键盘输入。如果输入回车,程序状态转入解释执行态;如果输入退格(8)则回显退格、空格、退格,模拟删除字符,同时输入缓冲区清除相应字节,清除前先检查左边界是否越界。如越界则鸣响报警且不执行清除操作;其他字符输入直接存入输入缓冲区并回显,此前检查右边界是否溢出,如果溢出则鸣响报警且抛弃刚输入的字符。
命令解释程序调用取词子程序分析用户命令行输入,根据词表第一个单词在散转表中的位置调用相应执行子程序处理命令,如果散转表中无此单词,则打印“Bad command!”。取词子程序返回错误指示时也打印此句。
命令解释程序向相应的命令相关子程序传入词表指针,具体执行由用户自行决定。由命令相关子程序返回后重新回到命令输入态,完成一次输入执行全过程。此过程周而复始地循环执行。

Shell界面的命令按功能分为以下几组:
1。操作系统相关命令:
查看就绪任务lt / 中止任务kill / 恢复任务执行call / CPU利用率usage / 版本查询ver / 查某个任务信息(TCB、堆栈内容)lt
查看切换次数和时间lts

2。网络相关命令:
显示配置MAC地址macadr / 显示配置主机IP地址host / 显示配置子网掩码mask / 显示配置缺省网关gateway
显示网络配置总情况lc / 连通测试命令ping / 用户数据报发送命令udp / telnet命令tel / 相关应用命令**
显示ARP高速缓冲区地址对ls / 显示发送缓冲区信息lti

3。屏幕显示相关命令:
清屏clr / 帮助help / 功能键F3、F7处理 / 组合键Ctrl+C、Ctrl+B处理

4。外设(闪盘X5045和I/O口)相关命令:
读闪盘rdx / 读I/O口rdp / 写闪盘wdx

5。安全相关命令:
身份认证密码权限usr、pass

6。应用相关命令:
用户自行定义

用户命令大小写不敏感,程序将命令字符串统一成小写形式。程序中各种参数(如:最大词长度、词数量……)定义成宏放在一个头文件中,随时可修改配置,很方便。Shell作为一个任务工作于内核之外,占用一个任务号。

源程序:
词表
typedef struct{
int Num;
int LeftCurveNum,RightCurveNum;
struct{
int Length;
unsigned char Str[MaxLenWord+1]; /*for '\0'*/
} wt[MaxLenWordTable];
} WORDTABLE;

取词
bit GetWord(unsigned char *ComBuf,WORDTABLE *WordTable)
{
int i="0"; /*ComBuf String pointer*/
int j="0"; /*Length of Word */
int k="-1"; /*The number of WordTable*/
int StrFlag="0"; /*There is "0-9/a-z/A-Z" before " ,()"*/
int SentenceEndFlag="0"; /*Sentence end*/
char ch;

WordTable->Num=0;
WordTable->LeftCurveNum=0;
WordTable->RightCurveNum=0;

ch=ComBuf[0];
while(!SentenceEndFlag&&i if((ch>='0'&&ch<='9')||(ch>='a'&&ch<='z')||(ch>='A'&&ch<='Z')||(ch=='.')){
if(StrFlag==0){
StrFlag=1;k=k+1;j=0;
if(k>=MaxLenWordTable) return 0;
WordTable->wt[k].Str[j]=ch;
WordTable->Num=k+1;
}
else{
j=j+1;
if(j>=MaxLenWord) return 0;
WordTable->wt[k].Str[j]=ch;
}
}
else if(ch==' '||ch==','||ch=='('||ch==')'||ch=='\0'){
if(ch=='(') WordTable->LeftCurveNum++;
if(ch==')') WordTable->RightCurveNum++;
if(StrFlag==1){
StrFlag=0;j=j+1;
WordTable->wt[k].Str[j]='\0';
WordTable->wt[k].Length=j;
}
if(ch=='\0') SentenceEndFlag="1";
}
else{
return 0;
}
i=i+1;
ch=ComBuf[i];
}
if(i if(WordTable->LeftCurveNum==WordTable->RightCurveNum) return 1;
else return 0;
}
else{
return 0;
}
}

输入回显和命令解释执行
void yyshell(void *yydata) reentrant
{
yydata=yydata;
clrscr();
PrintStr("\t\t***********************************************\n");
PrintStr("\t\t* Welcom to use this program *\n");
PrintStr("\t\t* Author:YangYi 20020715 *\n");
PrintStr("\t\t***********************************************\n\n\n");

/*Login & Password*/

PrintStr("% ");
while(!ShellEnd){

switch(State){
case StatInputCom:{
if(yygetch(&ch)){
if(ch==13) /*Enter return key*/
{
PrintStr("\n");
ComBuf[i+1]='\0';
if(i+1==0) PrintStr("% ");
else
State=StatExeCom;
}
else{
i=i+1;
if((i>=MaxLenComBuf)&&(ch!=8)){
PrintChar(7);
i=MaxLenComBuf-1;
}
else{
if(ch==8){
i=i-2;
if(i<-1) {i=-1;PrintChar(7);}
else{
PrintChar(8);
PrintChar(' ');
PrintChar(8);
}
}
else{
PrintChar(ch);
ComBuf[i]=ch;
}
}
}
break;
}
else{
//OSTimeDly(10);
break;
}
}
case StatExeCom:{
if(GetWord(ComBuf,&WordTable)==1&&WordTable.Num!=0){
yystrlwr(WordTable.wt[0].Str);
for(tem=0;tem if(yystrcmp(WordTable.wt[0].Str,ComTable[tem])==0) ComMatchFlag="1";
if(ComMatchFlag){
tem--;
switch(tem){
case 0:{DisplayTask(&WordTable);break;}
case 1:{Kill(&WordTable);break;}
case 2:{PingCommand(&WordTable);break;}
case 3:{UDPCommand(&WordTable);break;}
case 4:{CfgHost(&WordTable);break;}
case 5:{CfgMask(&WordTable);break;}
case 6:{CfgGateway(&WordTable);break;}
case 7:{
//ShellEnd=1;
PrintStr("\n\tThis Command is limited!\n\n");
break;
}
case 8:{PrintConfig(&WordTable);break;}
case 9:{clrscr();break;}
case 10:{DisplayHelpMenu(&WordTable);break;}
}
}
else
PrintStr(" Bad command!\n\n");
}
else{
if(WordTable.Num) PrintStr(" Bad command!\n\n");
}

ComMatchFlag=0;
State=StatInputCom;
if(ShellEnd) {PrintStr("\n\n");}
else PrintStr("% ");
i=-1;
break;
}
default:{
//ShellEnd=1;
PrintStr("System fatal error!\n");
PrintChar(7);PrintChar(7);PrintChar(7);
}
}
}
}

以上是我这次移植uCOS51的一些心得,写出来只是让准备在51上运行操作系统的同行们少走弯路并增强使用信心。我强烈推荐大家在自己的51系统中使用uCOS这个简单实用的自己的操作系统。它的大小应该不是问题,性能上的提高却是显著的。但愿此文能对朋友们有所帮助,错误在所难免,希望读者指正。

参考文献:
1.邵贝贝译. μC/OS-II 源码公开的实时嵌入式操作系统 中国电力出版社

系统分类: 嵌入式  |  用户分类: 单片机  |  标签: 无标签  |  来源: 转贴  | 

点击查看原文

发表评论 阅读全文(911) | 回复(0)

2Next >Total , Page /