单片机教程9.步进电机和蜂鸣器的学习
首页 > 硬件电路 > 单片机教程9.步进电机和蜂鸣器的学习     2018-11-14

9.1 单片机的IO口结构

上节课咱们提到了单片机的IO口的其中一种“准双向IO”的内部结构,实际上咱们的单片机IO口还有此外三种状况,分别是开漏、推挽、高阻态,咱们通过图9-1来看下三种状况。


单片机IO口状况示用意

图9-1 单片机IO口状况示用意    

前边咱们简单介绍“准双向IO”的时候,咱们是用三极管来讲明的,出于严谨的态度,咱们这里依照实际情况用MOS管画图示意。实际上三极管是靠电流导通的,而MOS管是靠电压导通的,具体原因以及他们的内部构造有关系,在这里咱们暂且没必要关心,如果今后有必要了解可以直接查找摹拟电子书或者者百度相关资料进行细致学习。在单片机IO口状况这一块内容上,咱们可以把MOS管当三极管来理解。在咱们的图9-1中,T1至关于一个PNP三极管,T2至关于一个NPN三极管。

其中准双向IO口原理已经经讲过了,开漏输出以及准双向IO的仅有区分,就是开漏输出把内部的上拉电阻去掉了。开漏输出如果要输出高电平时,T2关断,IO电平要靠外部的上拉电阻才能拉成高电平,如果没有外部上拉电阻IO电平就是一个不确定态。标准51单片机的P0口默认就是开漏输出,如果要用的时候外部需要加之拉电阻。而强推挽输出就有对比强的驱动能力,如图9-1中第三张小图,当内部输出一个高电平时,通过MOS管直接输出电流,没有电阻的限流,电流输出能力也对比大;如果内部输出一个低电平,那反向电流也能够很大,强推挽的一个特色就是驱动能力强。

单片机IO还有一种状况叫高阻态。通常咱们用来做输入引脚的时候,可以将IO口设置成高阻态,高阻态引脚本身如果悬空,用万用表测量的时候多是高多是低,他的状况完整取决于外部输入引脚的电平,高阻态引脚对GND的电阻很大至关于一个无限大,所以称之为高阻。

这就是单片机的IO口的四种状况,在咱们51单片机学习进程中,咱们的主要利用是准双向IO口,随着咱们学习的深入,其他状况也会有接触,在这里介绍给大家学习一下。

9.2 上下拉电阻

前边似乎咱们多次提到了上拉电阻,下拉电阻,具体到底甚么样的电阻算是上下拉电阻,上下拉电阻都有何作用呢?

上拉电阻就是将不确定的信号通过一个电阻拉到高电平,同时此电阻也起到一个限流作用,下拉就是下拉到低电平。

譬如咱们的IO设置为开漏输出高电平或者者是高阻态时,默认的电位是不确定的,外部经一个电阻接到VCC,也就是上拉电阻,那末相应的引脚就是高电平;经一个电阻到GND,也就是下拉电阻,那末相应的引脚就是一个低电平。

上拉电阻利用得多,均可以起到甚么作用呢?咱们现在主要先了解最经常使用的以下4点。

第1章 OC门要输出高电平,必须外部加之拉电阻才能正常使用,其实OC门就至关于单片机IO的开漏输出,其原理可参照图9-1中的开漏电路。

第2章 加大普通IO口的驱动能力。标准51单片机的内部IO口的上拉电阻,一般都是在几十K欧,譬如STC89C52RC内部是20K的上拉电阻,所以最大输出电流是250uA,因此外部加个上拉电阻,可以形成以及内部上拉电阻的并联结构,增大高电平时电流的输出能力。

第3章 在电平转换电路中,譬如咱们前边讲的5V转12V的电路中,上拉电阻其实起到的是限流电阻的作用,如图9-2所示。

上拉电阻R2

图9-2 上拉电阻R2

 

第4章 譬如单片机总线引脚,不使用的引脚悬空的时候,容易遭到电磁干扰而处于一个紊乱状况,加之一个对VCC的上拉电阻或者者一个对GND的下拉电阻后,可以有效的抵御电磁干扰。

咱们在进行电路设计的时候,如何正确选择适合的上下拉电阻的阻值呢?

1、从勤俭功耗的方面斟酌理当足够大,因为电阻越大,电流越小。

2、从确保足够的引脚驱动电流斟酌理当足够小,电阻小了,电流才能大。

3、在开漏输出时,过大的上拉电阻会导致信号上升沿变缓。咱们来解释一下:实际电平的变化都是需要时间的,虽然很小,但永远都达不到零,而开漏输出时上拉电阻的大小就直接影响了这个上升进程所需要的时间,如图9-3所示。想一下,如果电阻很大,而信号频率又很快的话,终究将导致信号还没等上升到高电平就又变为低了,于是信号就没法正确传送了。

上拉电阻对波形的影响 

图9-3 上拉电阻对波形的影响

综合斟酌,咱们经常使用的上下拉电阻值大多选取在1k到10k之间,具体到底多大通常要依据实际需求来选,通常情况下在标准范围内就能够了,不必定是一个固定的值。

9.3 28BYJ-48型步进机电详解与实例 9.3.1 机电的分类

机电的分类方式有得多,从用处的角度可划分机电分为驱动类机电以及节制类机电。直流机电属于驱动类机电,这类机电是将电能转换成机械能,主要利用在电钻、小车轮子、电风扇、洗衣机等等设备上。步进机电属于节制类机电,它是将脉冲信号转换成一个滚动角度的机电,在非超载的情况下,机电的转速、住手的位置只取决于脉冲信号的频率以及脉冲数,主要利用在自动化仪表、机器人、自动出产流水线、空调扇叶滚动等设备。

步进机电分为反应式、永磁式以及混合式三种。

反应式步进机电:结构简单本钱低,但是动态性状差、效力低、发热大、可靠性难以保证,所以现在基本已经经被淘汰。

永磁式步进机电:动态性状好、输出力矩较大,但误差相对来讲大一些,因其价格低广泛利用于消费性产品。

混合式步进机电:综合了反应式以及永磁式的优点,力矩大、动态性状好、步距角小,精度高,但是结构相对来讲复杂,价格也相对高,主要利用于工业。

咱们本章内容主要讲解28BYJ-48这款步进机电,其中

28——步进机电的有效最大外径是28毫米

B ——表示是步进机电

Y ——表示是永磁式

J ——表示是减速型

48——表示四相八拍

9.3.2 28BYJ-48型步进机电原理详解

28BYJ-48是4相永磁式减速步进机电,其外观以下图所示:

步进机电外观    
步进机电外观

图9-4 步进机电外观

咱们先来解释甚么是“4相永磁式”的概念,28BYJ-48的内部结构示用意9-5所示。先看里圈,它上面有6个齿,分别标注为0-5,这个叫做转子,顾名思义,它是要滚动的,转子的每一个齿上都带有永远的磁性,是一块永磁体,这就是“永磁式”的概念;再看外圈,这个就是定子,它是保持不动的,实际上它是跟机电的外壳固定在一块儿的,它上面有8个齿,而每一个齿上都缠上了一个线圈绕组,正对着的2个齿上的绕组又是串联在一块儿的,也就是说正对着的2个绕组总是会同时导通或者关断的,如此就形成了4相,在图中分别标注为A-B-C-D,这就是“4相”的概念。

步进机电内部结构示用意 

图9-5 步进机电内部结构示用意

现在咱们分析一下它的工作原理:

假定机电的起始状况就如上图所示,起始时是B相绕组的开关闭合,B相绕组导通,那末导通电流就会在正上以及正下两个定子齿上产生磁性,这两个定子齿上的磁性就会对转子上的0以及3号齿产生最强的吸引力,就会如图所示的那样,转子的的0号齿在正上、3号齿在正下而处于平衡状况;此时咱们会发现,转子的1号齿与右上的定子齿也就是C相的一个绕组显现一个很小的夹角,2号齿与右侧的定子齿也就是D相绕组显现一个略微大一点的夹角,很显明这个夹角是1号齿以及C绕组夹角的2倍,同理,左侧的情况也是一样的。

接下来,咱们把B相绕组断开,而使C相绕组导通,那末很显明,右上的定子齿将对转子1号齿产生最大的吸引力,而左下的定子齿将对转子4号齿,产生最大的吸引力,在这个吸引力的作用下,转子1、4号齿将对齐到右上以及左下的定子齿上而保持平衡,如此,转子就转过了起始状况时1号齿以及C相绕组那个夹角的角度。

再接下来,断开C相绕组,导通D相绕组,进程与上述的情况完整相同,终究将使转子2、5号齿与定子D相绕组对齐,转子又转过了上述一样的角度。

那末很显明,当A相绕组导通,即完成一个B-C-D-A的四节拍操作后,转子的0、3号齿将由原来的对齐到上下2个定子齿,而变为了对齐到左上以及右下的两个定子齿上,即转子转过了一个定子齿的角度。依此类推,再来一个四节拍,转子就将再转过一个齿的角度,8个四节拍之后转子将转过完整的一圈,而其中单个节拍使转子转过的角度就很容计算出来了,即360度÷(8×4)=11.25度,这个值就叫做步进角度。而上述这类工作模式就是步进机电的单四拍模式——单相绕组通电四节拍。

咱们再来讲解一种具备更优性状的工作模式,那就是在单四拍的每一两个节拍之间再插入一个双绕组导通的中间节拍,组成八拍模式。譬如,在从B相导通到C项导通的进程中,假设一个B相以及C相同时导通的节拍,这个时候,因为B、C两个绕组的定子齿对它们附近的转子齿同时产生相同的吸引力,这将导致这两个转子齿的中心线对比到B、C两个绕组的中心线上,也就是新插入的这个节拍使转子转过了上述单四拍模式中步进角度的一半,即5.625度。这样一来,就使滚动精度增加了一倍,而转子滚动一圈则需要8×8=64拍了。此外,新增加的这个中间节拍,还会在原来单四拍的两个节拍引力之间又加了一把引力,从而可以大大增加机电的整体扭力输出,使机电更“有劲”了。

除上述的单四拍以及八拍的工作模式外,还有一个双四拍的工作模式——双绕组通电四节拍。其实就是把八拍模式中的两个绕组同时通电的那四拍单独拿出来,而舍弃掉单绕组通电的那四拍而已经。其步进角度同单四拍是一样的,但因为它是两个绕组同时导通,所以扭矩会比单四拍模式大,在此就不做过多解释了。

八拍模式是这类4相步进机电的最佳工作模式,能最大限度的施展机电的各项性状,也是绝大多数实际工程中所选择的模式,因此咱们就重点来讲解如何用单片机程序来节制机电按八拍模式工作。

9.3.3 如何让机电滚动

再重新看一下上面的步进机电外观图以及内部结构图:步进机电一共有5根引线,其中红色的是公共端,链接到5V电源,接下来的橙、黄、粉、蓝就对应了A、B、C、D相;那末如果要导通A相绕组,就只需将橙色线接地便可,B相则黄色接地,依此类推;再依据上述单四拍以及八拍工作进程的讲解,可以得出下面的绕组节制顺序表:

表9-1  八拍模式绕组节制顺序表

 

1

2

3

4

5

6

7

8

P1-红

VCC

VCC

VCC

VCC

VCC

VCC

VCC

VCC

P2-橙

GND

GND

 

 

 

 

 

GND

P3-黄

 

GND

GND

GND

 

 

 

 

P4-粉

 

 

 

GND

GND

GND

 

 

P5-蓝

 

 

 

 

 

GND

GND

GND

咱们板子上节制步进机电部分是以及板子上的显示节制的74HC138译码器部分复用的P1.0到P1.3,这个部分咱们在“全板子测试视频”里边已经经讲过了,可以通过调整跳线帽实现步进机电的节制,如图9-6所示。

 

显示译码器以及步进机电接口跳线帽 

图9-6 显示译码器以及步进机电接口跳线帽

    如果大家使用机电的话,需要把4个跳线帽都调到跳线组的左侧,即左侧针以及中间针连通,就能够使用P1.0到P1.3节制步进机电了,要再使用显示部分的话,就要再换回到右边了。那如果大家既想让显示部分正常工作,又想让机电工作该怎么办呢?跳线帽保持在右边,用杜邦线把步进机电的节制引脚(即左侧的排针)链接到其它的暂不使用的单片机IO上便可。

再来看一下咱们步进机电的原理图,步进机电的节制电路以下:

步进机电节制电路 

图9-7  步进机电节制电路

诚然,单片机的IO口可以直接输出0V以及5V的电压,但是电流驱动能力,也就是带载能力无比有限,所以咱们在每一相的节制线上都增加一个三极管来提高驱动能力。由图中可以看出,若要使A相导通,则必须是Q2导通,此时A相也就是橙色线就至关于接地了,于是A相绕组导通,此时单片机P1口低4位应输出0b1110,即0xE;如要A、B相同时导通,那末就是Q2、Q3导通,P1口低4位应输出0b1100,即0xC,依此类推,咱们可以得到下面的八拍节拍的IO节制代码数组:

unsigned char code BeatCode[8] = { 0xE, 0xC, 0xD, 0x9, 0xB, 0x3, 0x7, 0x6 };

到这里,似乎所有的逻辑问题都解决了,重复将这个数组内的值送到P1口就行了。但是,只要再深入想一下就会发现还有个问题:多长时间送一次数据,也就是说一个节拍要持续多长时间适合呢?是随意的吗?固然不是了,这个时间是由步进机电的启动频率抉择的。启动频率,就是步进机电在空载情况下能够正常启动的最高脉冲频率,如果脉冲频率高于该值,机电就不能正常启动。下表是由厂家提供的步进机电参数表:

表9-2  28BYJ-48步进机电参数表

供电电压

相数

相电阻

Ω

步进角度

减速比

启动频率

P.P.S

转矩

g.cm

噪声

dB

绝缘介

电强度

5V

4

50±10%

5.625/64

1:64

≥550

≥300

≤35

600VAC

表中给出的参数是≥550,单位是P.P.S,即每一秒脉冲数,这里的意思就是说:每一个机电保证在你每一秒给出550个步进脉冲的情况下,机电可以启动。换算成单节拍持续时间就是1s÷550=1.8ms,那为了让机电能够启动,咱们节制节拍刷新时间大于1.8ms就能够了。有了这个参数,咱们就能够动手写出最简单的机电滚动程序了,以下:

#include 

 

unsigned char code BeatCode[8] = {  //步进机电节拍对应到IO节制电平的代码

    0xE, 0xC, 0xD, 0x9, 0xB, 0x3, 0x7, 0x6

};

 

void delay(unsigned int cnt);

 

void main()

{

    unsigned char buf;

    unsigned char step = 0;

    

     while(1)

    {

        buf  = P1 & 0xF0;       //用buf暂存P1口的高4位,而低4位清零

        buf |= BeatCode[step];  //buf低4位改为相应的节拍代码值

        P1   = buf;             //修改后终了后的值送回到P1口

        step++;        //步进节拍递增

        step &= 0x07;  //用“与”方式实现到8归零

        delay(200);    //延时2ms,即2ms执行一拍

    }

}

void delay(unsigned int cnt)

{

    while (cnt--);

}

赶紧编译下载到板子上试试吧!看看机电转了没有?记得换跳线哦!

9.3.4 滚动精度与深入分析

转是转了,但是否是感觉有点不太对劲呢?太慢了?别急,咱们继续。依据本章开头讲解的原理,八拍模式时,步进机电转过一圈是需要64个节拍,而咱们程序中是每一个节拍持续2ms,那末转一圈就应当是128ms,即1秒钟转7圈多,可怎么看上去它好像是7秒多才转了一圈呢?

那末,是时候来了解“永磁式减速步进机电”中这个“减速的概念了”。下图是这个28BYJ-48步进机电的拆除图,从图中可以看到,位于最中心的那个白色小齿轮才是步进机电的转子输出,64个节拍只是让这个小齿轮转了一圈,然后它带动那个浅蓝色的大齿轮,这就是一级减速。大家看一下右上方的白色齿轮的结构,除机电转子以及终究输出轴外的3个传动齿轮都是这样的结构,一层多齿以及一层少齿形成,而每个齿轮都用自己的少齿层去驱动下一个齿轮的多齿层,这样每一2个齿轮都形成一级减速,一共就有了4级减速,那末总的减速比是多少呢?即转子要转多少圈终究输出轴才转一圈呢?

步进机电内部齿轮示用意 

图9-8 步进机电内部齿轮示用意

回头看一下机电参数表中的减速比这个参数吧——1:64,转子转64圈,终究输出轴才会转一圈,也就是需要64×64=4096个节拍输出轴才转过一圈,2ms×4096=8192ms,8秒多才转一圈呢,是否是跟刚才的试验结果正好吻合了?4096个节拍滚动一圈,那末一个节拍滚动的角度——步进角度就是360/4096,看一下表中的步进角度参数5.625/64,算一下就晓得这两个值是相等的,一切都已经吻合了。

OK,关于基本的节制原理本该到这里就全体结束了,但是,咱们希望大家都能培育一种“实践是检验真谛的仅有标准”的思惟方式!回想一下,步进机电最大的特色是甚么?精确节制滚动量!那末咱们是否是应当检验一下它究竟是不是能精确呢?精确到甚么程度呢?怎么来检验呢?让它转过90度,然后量一下准不许?也行,但是如果它只差了1度乃至不到1度,你能准确测量出来吗?在没有精密仪器的情况很难。咱们还是让它多转几个整圈,看看它最后停下的位置还是否是原来的位置。对应的,咱们把程序修改一下,以方便节制机电转过任意的圈数。

#include 

 

void delay(unsigned int cnt);

void TrunMotor(unsigned long angle);

 

void main()

{

    TrunMotor(360*25); //360度*25,即25圈

    while(1);

}

 

void TrunMotor(unsigned long angle)

{

    unsigned char buf;

    unsigned char step = 0;

    unsigned long beats = 0;

    unsigned char code BeatCode[8] = {  //步进机电节拍对应到IO节制电平的代码

        0xE, 0xC, 0xD, 0x9, 0xB, 0x3, 0x7, 0x6

    };

    

    beats = (angle * 4096) / 360; //计算需要的节拍数,4096拍对应一圈

    while (beats--)

    {

        buf  = P1 & 0xF0;       //用buf暂存P1口的高4位,而低4位清零

        buf |= BeatCode[step];  //buf低4位改为相应的节拍代码值

        P1   = buf;             //修改后终了后的值送回到P1口

        step++;        //步进节拍递增

        step &= 0x07;  //用“与”方式实现到8归零

        delay(200);    //延时2ms,即2ms执行一拍

    }

    P1 |= 0x0F; //关闭机电所有的相

}

void delay(unsigned int cnt)

{

    while (cnt--);

}

上述程序中,咱们先编写了一个节制机电转过指定角度的函数,这个角度值由函数的形式参数给出,然后在主函数中就能够方便的通过更改调用时的实际参数来节制机电转过任意的角度了。咱们用了360*25,也就是25圈,固然你也能够随意改为其它的值,看看是甚么结果。咱们的程序会执行25*8=200秒的时间,先记下输出轴的初始位置,然后上电并耐心等它执行终了,看一下,是否是……有误差?怎么回事,哪儿出问题了,不是说能精确节制滚动量吗?

这个问题其实是出在了减速比上,再来看一下,厂家给出的减速比是1:64,无论是哪一个厂家出产的机电,只要型号是28BYJ-48,其标称的减速比就都是1:64。但实际上呢?经过咱们的拆除计算发现:真实准确的减速比并非这个值1:64,而是1:63.684!得出这个数据的法子也很简单,实际数一下每一个齿轮的齿数,然后将各级减速比相乘,就能够得出结果了,实测的减速比为(32/9)*(22/11)*(26/9)*(31/10)≈63.684,从而得出实际误差为0.0049,即约为百分之0.5,转100圈就会差出半圈,那末咱们刚才转了25圈,是否是就差了八分之一圈了,也就是45度,看一下刚才的误差是45度吧。那末依照1:63.684的实际减速比,可以得出转过一圈所需要节拍数是64*63.684≈4076。那末就把上面程序中机电驱动函数里的4096改为4076再试一下吧。是否是看不出丝毫的误差了?但实际上误差还是存在的,因为上面的计算结果都是约等得出的,实际误差大约是0.000056,即万分之0.56,转一万圈才会差出半圈,已经经可以忽略不计了。

那末厂家的参数为甚么会有误差呢?莫非厂家不晓得吗?要解释这个问题,咱们得回到实际利用中,步进机电最通常的目的是节制目标转过必定的角度,通常都是在360度之内的,而这个28BYJ-48最初的设计目的是用来节制空调的扇叶的,扇叶的流动范围是不会超过180度的,所以在这类利用场合下,厂商给出一个近似的整数减速比1:64已经经足够精确了,这也是通情达理的。但是,正如咱们的程序那样,咱们不必定是要用它来驱动空调扇叶,咱们可让它滚动得多圈来干别的,这个时候就需要更为精确的数据了,这也是咱们希望读者都能了解并掌握的,就是说咱们要能自己“设计”系统并解决其中发现的问题,而不要被所谓的“现成的方案”限制住思路。

9.3.5 编写实用程序的基础

解决了精度问题,让咱们再次回到咱们的机电节制程序上吧。上面给出的两个例程都不是实用的程序,为甚么?因为程序中存在大段的延时,而在延时的时候是甚么其它的事都干不了的,想一想第二个程序,整整200秒甚么别的事都干不了,这在实际的节制系统中是绝对不允许的。那末怎么改造一下呢?固然还是用定时中断来完成了,既然每一个节拍持续时间是2ms,那咱们直接用定时器定时2ms来刷新节拍就行了。改造后的程序以下:

#include 

 

unsigned long beats = 0;

 

void TrunMotor(unsigned long angle);

 

void main()

{

    //配置T0工作在模式1,定时2ms

    TMOD = 0x01;

    TH0 = 0xF8;

     TL0 = 0xCD;

    TR0 = 1;

    ET0 = 1;

     EA = 1;

    

    TrunMotor(360*2+180); //节制机电滚动2圈半

    while(1);

}

 

void TrunMotor(unsigned long angle)

{

    //在计算前关闭中断,完成后再打开,以避免中断打断计算进程而造成错误

    EA = 0;

    beats = (angle * 4076) / 360; //实测为4076拍滚动一圈

    EA = 1;

}

 

void InterruptTimer0() interrupt 1

{

    unsigned char buf;

    static unsigned char step = 0;      //使用静态变量以留存住前一次的值

    unsigned char code BeatCode[8] = {  //步进机电节拍对应到IO节制电平的代码

        0xE, 0xC, 0xD, 0x9, 0xB, 0x3, 0x7, 0x6

    };

    

    TH0 = 0xF8;  //溢出后进入中断重新赋值

    TL0 = 0xCD;

    

    if (beats != 0)

    {

        buf  = P1 & 0xF0;       //用buf暂存P1口的高4位,而低4位清零

        buf |= BeatCode[step];  //buf低4位改为相应的节拍代码值

        P1   = buf;             //修改后终了后的值送回到P1口

        step++;        //步进节拍递增

        step &= 0x07;  //用“与”方式实现到8归零

        beats--;

    }

    else

    {

        P1 |= 0x0F; //关闭机电所有的相

    }

}

程序还是对比简单的,机电滚动的启动函数TrunMotor只负责计算一个需要的总节拍数beats,然后在中断函数内检测这个变量,不为0时就执行节拍刷新操作,同时将其减1,直到减到0位置。

这里,咱们要特别说明一下的是TrunMotor函数中对EA的两次操作。咱们可以看到对beats的赋值计算语句是夹在EA=0;EA=1;这两行语句中间的,也就是说这行赋值计算语句在执行前先关闭了中断;而等它执行完后,才又重新打开了中断;在它执行进程中CPU是不会响应中断的,即中断函数InterruptTimer0不会被执行;即便这时候候定时器溢出了,中断产生了,也只能等待在EA重新置1后,才能得到响应,中断函数InterruptTimer0才会被执行。

那末为甚么要这么做呢?咱们来想一下:一开始就提到了,咱们所使用的STC89C52单片机是8位单片机,这个8位的概念就是说单片机操作数据时都是按8位即1个字节进行的,那末要操作多个字节(不管是读还是写)就必须分多次进行了;而咱们程序中定义的beats这个变量是unsigned long型,它要占用4个字节,那末对它的赋值起码也要分4次才能完成了;咱们想象一下,假设在完成了其中第一个字节的赋值后,刚好中断产生了,InterruptTimer0函数得到执行,而这个函数内可能会对beats进行减1的操作,减法就有可能产生借位,借位就会改变其它的字节,但因为此时其它的字节尚未被赋入新值,于是错误就会产生了,减1所得到的结果就不是预期的值了!所以要避免这类错误的产生就得先暂时关闭中断,等赋值完成后再打开中断;而如果咱们使用的是char或者bit型变量的话,因为它们都是在CPU的一次操作中就完成的,所以即便不关中断,也不会产生错误。问题分析清楚了,如何取舍还得依据实际情况来,遇上这类问题的时候多多斟酌斟酌吧。

9.3.6 蕴含综合利用的实用程序

上面咱们虽然完成了用中断节制滚动的程序,但实际上这个程序还是没多少实用价值的,咱们不能每一次想让它滚动的时候都上下电啊,是吧。还有它非但得能正转还得能反转啊,也就是说非但能转过去,还得能转回来呀。好吧,咱们就来做一个实例程序吧,结合第八章的按键程序,咱们设计这样一个程序:按数字键1-9,节制机电转过1-9圈;配合上下键改变滚动方向,按向上键后正向转1-9圈,向下键则反向转1-9圈;左键固定正转90度,右键固定反转90;Esc键终止滚动。程序以下:

#include 

 

sbit KEY_IN_1  = P2^4;  //矩阵按键的扫描输入引脚1

sbit KEY_IN_2  = P2^5;  //矩阵按键的扫描输入引脚2

sbit KEY_IN_3  = P2^6;  //矩阵按键的扫描输入引脚3

sbit KEY_IN_4  = P2^7;  //矩阵按键的扫描输入引脚4

sbit KEY_OUT_1 = P2^3;  //矩阵按键的扫描输出引脚1

sbit KEY_OUT_2 = P2^2;  //矩阵按键的扫描输出引脚2

sbit KEY_OUT_3 = P2^1;  //矩阵按键的扫描输出引脚3

sbit KEY_OUT_4 = P2^0;  //矩阵按键的扫描输出引脚4

 

const unsigned char code KeyCodeMap[4][4] = { //矩阵按键编号到PC标准键盘键码的映照表

    { '1',  '2',  '3', 0x26 }, //数字键1、数字键2、数字键3、向上键

    { '4',  '5',  '6', 0x25 }, //数字键4、数字键5、数字键6、向左键

    { '7',  '8',  '9', 0x28 }, //数字键7、数字键8、数字键9、向下键

    { '0', 0x1B, 0x0D, 0x27 }  //数字键0、ESC键、  回车键、 向右键

};

unsigned char KeySta[4][4] = {  //全体矩阵按键确当前状况

    {1, 1, 1, 1},

    {1, 1, 1, 1},

    {1, 1, 1, 1},

    {1, 1, 1, 1}

};

 

signed long beats = 0;   //步进机电滚动的总节拍数

 

void KeyAction(unsigned char keycode);

 

void main(void)

{

    unsigned char i, j;

    unsigned char backup[4][4] = {  //按键值备份,留存前一次的值

        {1, 1, 1, 1},

        {1, 1, 1, 1},

        {1, 1, 1, 1},

        {1, 1, 1, 1}

    };

    

    //配置T0工作在模式1,定时1ms

    TMOD = 0x01;

    TH0 = 0xFC;

    TL0 = 0x67;

    TR0 = 1;

    ET0 = 1;

     EA = 1;

 

    while(1)

    {

        //检索按键状况的变化

        for (i=0; i<4; i++)

        {

            for (j=0; j<4; j++)

            {

                if (backup[i][j] != KeySta[i][j])

                {

                    if (backup[i][j] == 0)  //按键弹起时执行为作

                    {

                        KeyAction(KeyCodeMap[i][j]);

                    }

                    backup[i][j] = KeySta[i][j];

                }

            }

        }

    }

}

 

void TrunMotor(signed long angle)

{

    //在计算前关闭中断,完成后再打开,以避免中断打断计算进程而造成错误

    EA = 0;

    beats = (angle * 4076) / 360; //实测为4076拍滚动一圈

    EA = 1;

}

void StopMotor()

{

    EA = 0;

    beats = 0;

    EA = 1;

}

 

void KeyAction(unsigned char keycode)

{

    static bit dirMotor = 0;  //机电滚动方向

    

    if ((keycode>='1') && (keycode<='9'))  //节制机电滚动1-9圈

    {

        if (dirMotor == 0)

        {

            TrunMotor(360*(keycode-'0'));

        }

        else

        {

            TrunMotor(-360*(keycode-'0'));

        }

    }

    else if (keycode == 0x26)  //向上键,节制滚动方向为正转

    {

        dirMotor = 0;

    }

    else if (keycode == 0x28)  //向下键,节制滚动方向为反转

    {

        dirMotor = 1;

    }

    else if (keycode == 0x25)  //向左键,固定正转90度

    {

        TrunMotor(90);

    }

    else if (keycode == 0x27)  //向右键,固定反转90度

    {

        TrunMotor(-90);

    }

    else if (keycode == 0x1B)  //Esc键,住手滚动

    {

        StopMotor();

    }

}

 

void MotorDrive()

{

    unsigned char buf;

    static unsigned char step = 0;      //使用静态变量以留存住前一次的值

    unsigned char code BeatCode[8] = {  //步进机电节拍对应到IO节制电平的代码

        0xE, 0xC, 0xD, 0x9, 0xB, 0x3, 0x7, 0x6

    };

    

    if (beats != 0)

    {

        if (beats > 0)

        {

            step++;        //正转时步进节拍递增

            step &= 0x07;  //用“与”方式实现到8归零

            beats--;       //正转时节拍计数递减

        }

        else

        {

            step--;        //反转时步进节拍递增

            step &= 0x07;  //用“与”方式一样可以实现到-1时归7

            beats++;       //反转时节拍计数递增

        }

        buf  = P1 & 0xF0;       //用buf暂存P1口的高4位,而低4位清零

        buf |= BeatCode[step];  //buf低4位改为相应的节拍代码值

        P1   = buf;             //修改后终了后的值送回到P1口

    }

    else

    {

        P1 |= 0x0F;  //节拍计数到0时关闭机电所有的相

    }

}

 

void KeyScan()

{

    unsigned char i;

    static unsigned char keyout = 0;  //矩阵按键扫描输出计数器

    static unsigned char keybuf[4][4] = {  //按键扫描缓冲区,留存一段时间内的扫描值

        {0xFF, 0xFF, 0xFF, 0xFF},

        {0xFF, 0xFF, 0xFF, 0xFF},

        {0xFF, 0xFF, 0xFF, 0xFF},

        {0xFF, 0xFF, 0xFF, 0xFF}

    };

 

    //将一行的4个按键值移入缓冲区

    keybuf[keyout][0] = (keybuf[keyout][0] << 1) | KEY_IN_1;

    keybuf[keyout][1] = (keybuf[keyout][1] << 1) | KEY_IN_2;

    keybuf[keyout][2] = (keybuf[keyout][2] << 1) | KEY_IN_3;

    keybuf[keyout][3] = (keybuf[keyout][3] << 1) | KEY_IN_4;

 

    //消抖后更新按键状况

    for (i=0; i<4; i++)  //每一行4个按键,所以重复4次

    {

        if ((keybuf[keyout][i] & 0x0F) == 0x00)

        {   //连续4次扫描值为0,即16ms(4*4ms)内都只检测到按下状况时,可认为按键已经按下

            KeySta[keyout][i] = 0;

        }

        else if ((keybuf[keyout][i] & 0x0F) == 0x0F)

        {   //连续4次扫描值为1,即16ms(4*4ms)内都只检测到弹起状况时,可认为按键已经弹起

            KeySta[keyout][i] = 1;

        }

    }

    

    //执行下一次的扫描输出

    keyout++;

    keyout &= 0x03;

    switch (keyout)

    {

        case 0:

            KEY_OUT_4 = 1;

            KEY_OUT_1 = 0;

            break;

        case 1:

            KEY_OUT_1 = 1;

            KEY_OUT_2 = 0;

            break;

        case 2:

            KEY_OUT_2 = 1;

            KEY_OUT_3 = 0;

            break;

        case 3:

            KEY_OUT_3 = 1;

            KEY_OUT_4 = 0;

            break;

        default:

            break;

    }

}

 

void InterruptTimer0() interrupt 1

{

    static bit div = 0;

    

     TH0 = 0xFC;  //溢出后进入中断重新赋值,定时1ms

    TL0 = 0x67;

    KeyScan();   //执行按键扫描

    

    //用一个静态bit变量实现二分频,即2ms定时,用于驱动机电

    div = ~div;

    if (div == 1)

    {

        MotorDrive();

    }

}

这个程序是第八章以及本章知识的一个综合——用按键节制步进机电滚动。程序中有这么几点值得注意,咱们分述以下:

1、针对机电要完成正转以及反转两个不同的操作,咱们并无使用正转启动函数以及反转启动函数这么两个函数来完成,也没有在启动函数定义的时候增加一个形式参数来指明其方向。咱们这里的启动函数void TrunMotor(signed long angle)与单向正转时的启动函数仅有的区分就是把形式参数angle的类型从unsigned long改为了signed long,咱们用有符号数固有的正负特性来辨别正转与反转,正数表示正转angle度,负数就表示反转angle度,这样处理是否是很简洁又很明了呢?而你对有符号数以及无符号数的区分用法是否是也更有体会了?

2、针对终止机电滚动的操作,咱们定义了一个单独的StopMotor函数来完成,虽然这个函数无比简单,虽然它也只在Esc按键分支内被调用了,但咱们依然把它单独提出来作为了一个函数。而这类做法就是基于这样一条编程原则:尽量用单独的函数来完成硬件的某种操作,当一个硬件蕴含多个的操作时,把这些操作函数组织在一块儿,形成一个对上层的统一接口。这样的层次化处理,会使得整个程序条理清晰,即有利于程序的调试维护,又有利于功能的扩充。

3、中断函数中要处理按键扫描以及机电驱动两件事情,而为了避免中断函数过于复杂,咱们就又分出了按键扫描以及机电驱动两个函数(这也一样符合上述2的编程原则),而中断函数的逻辑就变得简洁而清晰了。这里还有个矛盾,就是按键扫描咱们选择的定时时间是1ms,而本章之前的实例中机电节拍持续时间都是2ms;很显然,用1ms的定时可以定出2ms的间隔,而用2ms的定时却得不到准确的1ms间隔;所以咱们的做法就是,定时器依然定时1ms,然后用一个bit变量做标志,每一1ms改变一次它的值,而咱们只选择值为1的时候执行一次动作,这样就是2ms的间隔了;如果我要3ms、4ms……呢,把bit改为char或者int型,然后对它们递增,判断到哪一个值该归零,就能够了;这就是在硬件定时器的基础上实现准确的软件定时,其实类似的操作咱们在讲数码管的时候也用过了,回想一下吧。

9.4 蜂鸣器的学习

蜂鸣器从结构辨别分为压电式蜂鸣器以及电磁式蜂鸣器。压电式为压电陶瓷片发音,电流对比小一些,电磁式蜂鸣器为线圈通电震撼发音,体积对比小。

依照驱动方式分为有源蜂鸣器以及无源蜂鸣器。这里的有源以及无源不是指电源,而是振荡源。有源蜂鸣器内部带了振荡源,如图9-9所示中,给了BUZZ引脚一个低电平,蜂鸣器就会直接响。而无源蜂鸣器内部是不带振荡源的,要让他响必须给500Hz到4.5kHz之间的脉冲频率信号来驱动它才会响。有源蜂鸣器往往比无源蜂鸣器贵一些,因为里边多了振荡电路,驱动发音也简单,靠电平就能够驱动,而无源蜂鸣器价格对比便宜,此外无源蜂鸣器声音频率可以节制,而音阶与频率又有确定的对应关系,因此就能够做出来“do re mi fa sol la si”的效果,可以用它制作出简单的音乐曲目,譬如生日歌、两只老虎等等。

蜂鸣器工作原理图 

图9-9 蜂鸣器工作原理图

咱们来看一下图9-9,蜂鸣器电流依然相对较大,因此需要用三极管驱动,并且加了一个100欧的电阻作为限流电阻。此外还加了一个D4二极管,这个二极管叫做续流二极管。咱们的蜂鸣器是感性器件,当三极管导通给蜂鸣器供电时,就会有导通电流流过蜂鸣器。而咱们晓得,电感的一个特色就是电流不能突变,导通时电流是逐步加大的,这点没有问题,但当关断时,经“电源-三极管-蜂鸣器-地”这条回路就截断了,过不了任何电流了,那末贮存的电流往哪儿去呢,就是经过这个D4以及蜂鸣器本身的环路来损耗掉了,从而就避免了关断时因为电感电流酿成的反向冲击。接续关断时的电流,这就是续流二极管名称的由来。

蜂鸣器经经常使用于电脑、打印机、万用表这些设备上做提醒音,提醒音一般也很简单,就是简单发出个声音就行,咱们程序简单做了个4kHZ频率下的发声以及1kHZ频率下的发声程序,同窗们自己对比一下便可。

#include 

 

sbit BUZZ = P1^6;  //蜂鸣器节制引脚

unsigned char T0LoadH = 0;  //T0重载值的高字节

unsigned char T0LoadL = 0;  //T0重载值的低字节

 

void OpenBuzz(unsigned int frequ);

void StopBuzz();

 

void main()

{

    unsigned int i=0;

    

    TMOD = 0x01; //配置T0工作在模式1,暂不启动

    EA = 1;       //使能全局中断

    

    while (1)

    {

        OpenBuzz(4000); //以4KHz的频率启动蜂鸣器

        for (i=0; i<40000; i++);

        StopBuzz();

        for (i=0; i<40000; i++);

        OpenBuzz(1000); //以1KHz的频率启动蜂鸣器

        for (i=0; i<40000; i++);

        StopBuzz();

        for (i=0; i<40000; i++);

    }

}

 

void OpenBuzz(unsigned int frequ)

{

    unsigned int reload;

    

    reload = 65536 - (11059200/12) / (frequ*2);  //由给定频率值计算定时器重载值

    T0LoadH = reload >> 8;  //16位重载值分解为高低两个字节

    T0LoadL = reload;

    TH0 = 0xFF;   //设定一个接近溢出的初值,以使定时器马上投入工作

    TL0 = 0xFE;

    ET0 = 1;   //使能T0中断

    TR0 = 1;   //启动T0

}

void StopBuzz()

{

    ET0 = 0;   //禁用T0中断

    TR0 = 0;   //住手T0

}

 

void InterruptTimer0() interrupt 1

{

    TH0 = T0LoadH;  //溢出后进入中断重新赋值

    TL0 = T0LoadL;

    BUZZ = ~BUZZ; //反转蜂鸣器节制电平

}

此外用蜂鸣器来输出音乐,仅仅是好玩而已经,利用很少,里边蕴含了音阶、乐谱的相关内容,程序也有一点复杂,所以就不详细给大家去讲解了。我仅仅写了个《两只老虎》的程序,大家下载到板子上玩玩,知足一下好奇心。

 

#include 

 

//中音1-7以及高音1-7对应频率列表

unsigned int code NoteFrequ[] = {

    523,  587,  659,  698,  784,  880,  988,  //中音1-7

    1047, 1175, 1319, 1397, 1568, 1760, 1976  //高音1-7

};

 

//中音1-7以及高音1-7对应的定时器重载值

unsigned int code NoteReload[] = {

    65536 - (11059200/12) / (523*2),

    65536 - (11059200/12) / (587*2),

    65536 - (11059200/12) / (659*2),

    65536 - (11059200/12) / (698*2),

    65536 - (11059200/12) / (784*2),

    65536 - (11059200/12) / (880*2),

    65536 - (11059200/12) / (988*2),

    

    65536 - (11059200/12) / (1047*2),

    65536 - (11059200/12) / (1175*2),

    65536 - (11059200/12) / (1319*2),

    65536 - (11059200/12) / (1397*2),

    65536 - (11059200/12) / (1568*2),

    65536 - (11059200/12) / (1760*2),

    65536 - (11059200/12) / (1976*2),

};

 

sbit BUZZ = P1^6;  //蜂鸣器节制引脚

 

bit enable = 1;    //蜂鸣器发声使能标志

bit tmrflag = 0;    //定时器中断完成标志

unsigned char T0LoadH = 0xFF;  //T0重载值的高字节

unsigned char T0LoadL = 0x00;  //T0重载值的低字节

 

void PlayTwoTiger();

 

void main()

{

    unsigned int i=0;

    

     TMOD = 0x01; //配置T0工作在模式1

    TH0 = T0LoadH;

    TL0 = T0LoadL;

    ET0 = 1;   //使能T0中断

    TR0 = 1;   //启动T0

    EA = 1;    //使能全局中断

    

    while (1)

    {

        PlayTwoTiger();

        for (i=0; i<40000; i++);

    }

}

 

void PlayTwoTiger()

{

    unsigned char beat;   //记录当前节拍索引

    unsigned char note;   //当前节拍对应的音符

    unsigned int time = 0;      //当前节拍计时

    unsigned int beatTime = 0;  //当前节拍总时间

    unsigned int soundTime = 0; //当前节拍需发声时间

    unsigned char code TwoTigerNote[] = {  //两只老虎音符表

        1,   2,   3, 1,    1,   2,   3, 1,   3, 4, 5,   3, 4, 5,

        5,6, 5,4, 3, 1,    5,6, 5,4, 3, 1,   1, 5, 1,   1, 5, 1,

    };

    unsigned char code TwoTigerBeat[] = {  //两只老虎节拍表,4表示一拍,1就是1/4拍,8就是2拍

        4,   4,   4, 4,    4,   4,   4, 4,   4, 4, 8,   4, 4, 8,

        3,1, 3,1, 4, 4,    3,1, 3,1, 4, 4,   4, 4, 8,   4, 4, 8,

    };

    

    for (beat=0; beat用节拍索引作为重复变量

    {

        while (!tmrflag);  //每一次定时器中断完成后,检测并处理节拍

        tmrflag = 0;

        if (time == 0)

        {

            //启动一个新的节拍

            note = TwoTigerNote[beat] - 1;

            T0LoadH = NoteReload[note] >> 8;

            T0LoadL = NoteReload[note];

            beatTime = (TwoTigerBeat[beat] * NoteFrequ[note]) >> 2; //计算节拍总时间,右移2位至关于除4,移位代替除法可以加快执行速度

            soundTime = beatTime - (beatTime >> 2); //计算发声时间,为总时间的0.75,移位原理同上

            enable = 1;  //指导蜂鸣器开始发声

            time++;

        }

        else

        {

            if (time >= beatTime)

            {

                time = 0;  //当前持续时间达到节拍总时间时归零,并递增节拍索引,以筹备启动新节拍

                beat++;

            }

            else

            {

                time++;

                if (time == soundTime)

                {

                    enable = 0;  //当前持续时间达到发声时间后,指导关闭蜂鸣器,以插入0.25*总时间的静音间隔,以辨别连续的节拍

                }

            }

        }

    }

}

 

void InterruptTimer0() interrupt 1

{

    TH0 = T0LoadH;  //溢出后进入中断重新赋值

    TL0 = T0LoadL;

    tmrflag = 1;

    if (enable == 1)

    {

        BUZZ = ~BUZZ;  //反转蜂鸣器节制电平

    }

    else

    {

        BUZZ = 1;   //关闭蜂鸣器

    }

}

9.5 功课

1、能够理解清楚单片机IO口的结构。

2、能够看懂上下拉电阻的电路利用并且熟练使用上下拉电阻。

3、理解28BYJ-48减速步进机电的工作原理。

4、能够熟练编写步进机电正反转任意角度的程序。

5、学会蜂鸣器发声的法子。

 

© Copyright 吾爱微电子 | 琥珀川