电机的分类方式有很多,从用途角度可划分为驱动类电机和控制类电机。直流电机属于驱动类电机,这种电机是将电能转换成机械能,主要应用在电钻、小车轮子、电风扇、洗衣机等设备上。步进电机属于控制类电机,它是将脉冲信号转换成一个转动角度的电机,在非超载的情况下,电机的转速、停止的位置只取决于脉冲信号的频率和脉冲数,主要应用在自动化仪表、机器人、自动生产流水线、空调扇叶转动等设备。
步进电机又分为反应式、永磁式和混合式三种:
- 反应式步进电机:结构简单成本低,但是动态性能差、效率低、发热大、可靠性难以保证,所以现在基本已经被淘汰了。
- 永磁式步进电机:动态性能好、输出力矩较大,但误差相对来说大一些,因其价格低而广泛应用于消费性产品。
- 混合式步进电机:综合了反应式和永磁式的优点,力矩大、动态性能好、步距角小,精度高,但是结构相对来说复杂,价格也相对高,主要应用于工业。
我们本章内容主要讲解 28BYJ-48 这款步进电机,先介绍型号中包含的具体含义:
- 28:步进电机的有效最大外径是 28 毫米
- B:表示是步进电机
- Y:表示是永磁式
- J:表示是减速型
- 48:表示四相八拍
28BYJ-48步进电机原理
28BYJ-48 是 4 相永磁式减速步进电机,其外观如图 9-3 所示:
图 9-3 步进电机外观
我们先来解释“4 相永磁式”的概念,28BYJ-48 的内部结构示意图 9-4 所示。先看里圈,它上面有 6 个齿,分别标注为 0~5,这个叫做转子,顾名思义,它是要转动的,转子的每个齿上都带有永久的磁性,是一块永磁体,这就是“永磁式”的概念。再看外圈,这个就是定子,它是保持不动的,实际上它是跟电机的外壳固定在一起的,它上面有 8 个齿,而每个齿上都缠上了一个线圈绕组,正对着的 2 个齿上的绕组又是串联在一起的,也就是说正对着的2 个绕组总是会同时导通或关断的,如此就形成了 4 相,在图中分别标注为 A-B-C-D,这就是“4 相”的概念。
图 9-4 步进电机内部结构示意图
现在我们分析一下它的工作原理:
假定电机的起始状态就如图 9-4 所示,逆时针方向转动,起始时是 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 相步进电机的最佳工作模式,能最大限度的发挥电机的各项性能,也是绝大多数实际工程中所选择的模式,因此我们就重点来讲解如何用单片机程序来控制电机按八拍模式工作。
让28BYJ-48步进电机转起来
再重新看一下上面的步进电机外观图和内部结构图:步进电机一共有 5 根引线,其中红色的是公共端,连接到 5V 电源,接下来的橙、黄、粉、蓝就对应了 A、B、C、D 相;那么如果要导通 A 相绕组,就只需将橙色线接地即可,B 相则黄色接地,依此类推;再根据上述单四拍和八拍工作过程的讲解,可以得出下面的绕组控制顺序表,如表 9-1 所示:
表 9-1 八拍模式绕组控制顺序表 12345678
P1-红VCCVCCVCCVCCVCCVCCVCCVCC
P2-橙GNDGND GND
P3-黄 GNDGNDGND
P4-粉 GNDGNDGND
P5-蓝 GNDGNDGND
我们板子上控制步进电机部分是和板子上的显示控制的 74HC138 译码器部分复用的P1.0~P1.3,关于跳线我们在第 3 章已经讲过了,通过调整跳线帽的位置可以让 P1.0~P1.3控制步进电机的四个绕组,如图 9-5 所示。
图 9-5 显示译码与步进电机的选择跳线
如果要使用电机的话,需要把 4 个跳线帽都调到跳线组的左侧(开发板上的实际位置),即左侧针和中间针连通(对应原理图中的中间和下边的针),就可以使用 P1.0 到 P1.3 控制步进电机了,如要再使用显示部分的话,就要再换回到右侧了。那如果大家既想让显示部分正常工作,又想让电机工作该怎么办呢?跳线帽保持在右侧,用杜邦线把步进电机的控制引脚(即左侧的排针)连接到其它的暂不使用的单片机 IO 上即可。
再来看一下我们步进电机的原理图,步进电机的控制电路如图 9-6 所示。
图 9-6 步进电机控制电路
诚然,单片机的 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 就是由厂家提供的步进电机参数表,我们来看一下。
表 9-2 28BYJ-48 步进电机参数表供电
电压相数相电阻
Ω步进角度减速比启动频率
P.P.S转矩
g.cm噪声
dB绝缘介
电强度
5V450±10%5.625/641:64≥550≥300≤35600VAC
表中给出的参数是≥550,单位是 P.P.S,即每秒脉冲数,这里的意思就是说:电机保证在你每秒给出 550 个步进脉冲的情况下,可以正常启动。那么换算成单节拍持续时间就是1s/550=1.8ms,为了让电机能够启动,我们控制节拍刷新时间大于 1.8ms 就可以了。有了这个参数,我们就可以动手写出最简单的电机转动程序了,如下:纯文本复制
- #include <reg52.h>
- unsigned char code BeatCode[8] = { //步进电机节拍对应的 IO 控制代码
- 0xE, 0xC, 0xD, 0x9, 0xB, 0x3, 0x7, 0x6
- };
- void delay();
- void main(){
- unsigned char tmp; //定义一个临时变量
- unsigned char index = 0; //定义节拍输出索引
- while (1){
- tmp = P1; //用 tmp 把 P1 口当前值暂存
- tmp = tmp & 0xF0; //用&操作清零低 4 位
- //用|操作把节拍代码写到低 4 位
- tmp = tmp | BeatCode[index];
- //把低 4 位的节拍代码和高 4 位的原值送回 P1
- P1 = tmp;
- index++; //节拍输出索引递增
- index = index & 0x07; //用&操作实现到 8 归零
- delay(); //延时 2ms,即 2ms 执行一拍
- }
- }
- /* 软件延时函数,延时约 2ms */
- void delay(){
- unsigned int i = 200;
- while (i--);
- }
把程序编译下载到板子上试试吧!看看电机转了没有?要记得换跳线哦!
28BYJ-48步进电机转动精度与深入分析
转是转了,但是不是感觉有点不太对劲呢?太慢了?别急,咱们继续。根据本章开头讲解的原理,八拍模式时,步进电机转过一圈是需要 64 个节拍,而我们程序中是每个节拍持续 2ms,那么转一圈就应该是 128ms,即 1 秒钟转 7 圈多,可怎么看上去它好像是 7 秒多才转了一圈呢?
那么,是时候来了解“永磁式减速步进电机”中这个“减速”的概念了。图 9-7 是这个28BYJ-48 步进电机的拆解图,从图中可以看到,位于最中心的那个白色小齿轮才是步进电机的转子输出,64 个节拍只是让这个小齿轮转了一圈,然后它带动那个浅蓝色的大齿轮,这就是一级减速。大家看一下右上方的白色齿轮的结构,除电机转子和最终输出轴外的 3 个传动齿轮都是这样的结构,由一层多齿和一层少齿构成,而每一个齿轮都用自己的少齿层去驱动下一个齿轮的多齿层,这样每 2 个齿轮都构成一级减速,一共就有了 4 级减速,那么总的减速比是多少呢?即转子要转多少圈最终输出轴才转一圈呢?
图 9-7 步进电机内部齿轮示意图
回头看一下电机参数表中的减速比这个参数吧——1:64,转子转 64 圈,最终输出轴才会转一圈,也就是需要 64*64=4096 个节拍输出轴才转过一圈,2ms*4096=8192ms,8 秒多才转一圈呢,是不是跟刚才的实验结果正好吻合了?4096 个节拍转动一圈,那么一个节拍转动的角度——步进角度就是 360/4096,看一下表中的步进角度参数 5.625/64,算一下就知道这两个值是相等的,一切都已吻合了。
关于基本的控制原理本该到这里就全部结束了,但是,我们希望大家都能培养一种“实践是检验真理的唯一标准”的思维方式!回想一下,步进电机最大的特点是什么?精确控制转动量!那么我们是不是应该检验一下它到底是不是能精确呢?精确到什么程度呢?怎么来检验呢?让它转过 90 度,然后量一下准不准?也行,但是如果它只差了 1 度甚至不到 1 度,你能准确测量出来吗?在没有精密仪器的情况很难。我们还是让它多转几个整圈,看看它最后停下的位置还是不是原来的位置。对应的,我们把程序修改一下,以方便控制电机转过任意的圈数。
- #include <reg52.h>
- void TurnMotor(unsigned long angle);
- void main(){
- TurnMotor(360*25); //360 度*25,即 25 圈
- while (1);
- }
- /* 软件延时函数,延时约 2ms */
- void delay(){
- unsigned int i = 200;
- while (i--);
- }
- /* 步进电机转动函数,angle-需转过的角度 */
- void TurnMotor(unsigned long angle){
- unsigned char tmp; //临时变量
- unsigned char index = 0; //节拍输出索引
- unsigned long beats = 0; //所需节拍总数
- //步进电机节拍对应的 IO 控制代码
- unsigned char code BeatCode[8] = {
- 0xE, 0xC, 0xD, 0x9, 0xB, 0x3, 0x7, 0x6
- };
- //计算需要的节拍总数,4096 拍对应一圈
- beats = (angle*4096) / 360;
- //判断 beats 不为 0 时执行循环,然后自减 1
- while (beats--){
- tmp = P1; //用 tmp 把 P1 口当前值暂存
- tmp = tmp & 0xF0; //用&操作清零低 4 位
- tmp = tmp | BeatCode[index]; //用|操作把节拍代码写到低 4 位
- P1 = tmp; //把低 4 位的节拍代码和高 4 位的原值送回 P1
- index++; //节拍输出索引递增
- index = index & 0x07; //用&操作实现到 8 归零
- delay(); //延时 2ms,即 2ms 执行一拍
- }
- P1 = P1 | 0x0F; //关闭电机所有的相
- }
上述程序中,我们先编写了一个控制电机转过指定角度的函数,这个角度值由函数的形式参数给出,然后在主函数中就可以方便的通过更改调用时的实际参数来控制电机转过任意的角度了。我们用了 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 已经足够精确了,这也是合情合理的。然而,正如我们的程序那样,我们不一定是要用它来驱动空调扇叶,我们可以让它转动很多圈来干别的,这个时候就需要更为精确的数据了,这也是我们希望同学们都能了解并掌握的,就是说我们要能自己“设计”系统并解决其中发现的问题,而不要被所谓的“现成的方案”限制住思路。
28BYJ-48步进电机控制程序基础
解决了精度问题,让我们再次回到我们的电机控制程序上吧。上面给出的两个例程都不是实用的程序,为什么?因为程序中存在大段的延时,而在延时的时候是什么其它的事都干不了的,想想第二个程序,整整 200 秒什么别的事都干不了,这在实际的控制系统中是绝对不允许的。那么怎么改造一下呢?当然还是用定时中断来完成了,既然每个节拍持续时间是2ms,那我们直接用定时器定时 2ms 来刷新节拍就行了。改造后的程序如下:
- #include <reg52.h>
- unsigned long beats = 0; //电机转动节拍总数
- void StartMotor(unsigned long angle);
- void main(){
- EA = 1; //使能总中断
- TMOD = 0x01; //设置 T0 为模式 1
- TH0 = 0xF8; //为 T0 赋初值 0xF8CD,定时 2ms
- TL0 = 0xCD;
- ET0 = 1; //使能 T0 中断
- TR0 = 1; //启动 T0
- StartMotor(360*2+180); //控制电机转动 2 圈半
- while (1);
- }
- /* 步进电机启动函数,angle-需转过的角度 */
- void StartMotor(unsigned long angle){
- //在计算前关闭中断,完成后再打开,以避免中断打断计算过程而造成错误
- EA = 0;
- beats = (angle * 4076) / 360; //实测为 4076 拍转动一圈
- EA = 1;
- }
- /* T0 中断服务函数,用于驱动步进电机旋转 */
- void InterruptTimer0() interrupt 1{
- unsigned char tmp; //临时变量
- static unsigned char index = 0; //节拍输出索引
- unsigned char code BeatCode[8] = { //步进电机节拍对应的 IO 控制代码
- 0xE, 0xC, 0xD, 0x9, 0xB, 0x3, 0x7, 0x6
- };
- TH0 = 0xF8; //重新加载初值
- TL0 = 0xCD;
- //节拍数不为 0 则产生一个驱动节拍
- if (beats != 0){
- tmp = P1; //用 tmp 把 P1 口当前值暂存
- tmp = tmp & 0xF0; //用&操作清零低 4 位
- //用|操作把节拍代码写到低 4 位
- tmp = tmp | BeatCode[index];
- //把低 4 位的节拍代码和高 4 位的原值送回 P1
- P1 = tmp;
- index++; //节拍输出索引递增
- index = index & 0x07; //用&操作实现到 8 归零
- beats--; //总节拍数-1
- }else{ //节拍数为 0 则关闭电机所有的相
- P1 = P1 | 0x0F;
- }
- }
程序还是比较简单的,电机转动的启动函数 StartMotor 只负责计算一个需要的总节拍数beats,然后在中断函数内检测这个变量,不为 0 时就执行节拍操作,同时将其减 1,直到减到 0 为止。
这里,我们要特别说明一下的是 StartMotor 函数中对 EA 的两次操作。我们可以看到对beats 的赋值计算语句是夹在 EA=0;EA=1;这两行语句中间的,也就是说这行赋值计算语句在执行前先关闭了中断,而等它执行完后,才又重新打开了中断。在它执行过程中单片机是不会响应中断的,即中断函数 InterruptTimer0 不会被执行,即使这时候定时器溢出了,中断发生了,也只能等待 EA 重新置 1 后,才能得到响应,中断函数 InterruptTimer0 才会被执行。
那么为什么要这么做呢?我们来想一下:在本书开始我们就曾提到,我们所使用的STC89C52 单片机是 8 位单片机,这个 8 位的概念就是说单片机操作数据时都是按 8 位即按1 个字节进行的,那么要操作多个字节(不论是读还是写)就必须分多次进行了。而我们程序中定义的 beats 这个变量是 unsigned long 型,它要占用 4 个字节,那么对它的赋值最少也要分 4 次才能完成了。我们想象一下,假如在完成了其中第一个字节的赋值后,恰好中断发生了,InterruptTimer0 函数得到执行,而这个函数内可能会对 beats 进行减 1 的操作,减法就有可能发生借位,借位就会改变其它的字节,但因为此时其它的字节还没有被赋入新值,于是错误就会发生了,减 1 所得到的结果就不是预期的值了!所以要避免这种错误的发生就得先暂时关闭中断,等赋值完成后再打开中断。而如果我们使用的是 char 或 bit 型变量的话,因为它们都是在 CPU 的一次操作中就完成的,所以即使不关中断,也不会发生错误。问题分析清楚了,如何取舍还得根据实际情况来,遇上这类问题的时候多多考虑考虑吧。
实用的28BYJ-48步进电机控制程序
上面我们虽然完成了用中断控制电机转动的程序,但实际上这个程序还是没多少实用价值的,我们不能每次想让它转动的时候都上下电啊,是吧。还有就是它不但能正转还得能反转啊,也就是说不但能转过去,还得能转回来呀。好吧,我们就来做一个实例程序吧,结合第 8 章的按键程序,我们设计这样一个功能程序:按数字键 1~9,控制电机转过 1~9 圈;配合上下键改变转动方向,按向上键后正向转 1~9 圈,向下键则反向转 1~9 圈;左键固定正转 90 度,右键固定反转 90;Esc 键终止转动。通过这个程序,我们也可以进一步体会到如何用按键来控制程序完成复杂的功能,以及控制和执行模块之间如何协调工作,而你的编程水平也可以在这样的实践练习中得到锻炼和提升。纯文本复制
- #include <reg52.h>
- sbit KEY_IN_1 = P2^4;
- sbit KEY_IN_2 = P2^5;
- sbit KEY_IN_3 = P2^6;
- sbit KEY_IN_4 = P2^7;
- sbit KEY_OUT_1 = P2^3;
- sbit KEY_OUT_2 = P2^2;
- sbit KEY_OUT_3 = P2^1;
- sbit KEY_OUT_4 = P2^0;
- unsigned char code KeyCodeMap[4][4] = { //矩阵按键编号到标准键盘键码的映射表
- { 0x31, 0x32, 0x33, 0x26 }, //数字键 1、数字键 2、数字键 3、向上键
- { 0x34, 0x35, 0x36, 0x25 }, //数字键 4、数字键 5、数字键 6、向左键
- { 0x37, 0x38, 0x39, 0x28 }, //数字键 7、数字键 8、数字键 9、向下键
- { 0x30, 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 KeyDriver();
- void main(){
- EA = 1; //使能总中断
- TMOD = 0x01; //设置 T0 为模式 1
- TH0 = 0xFC; //为 T0 赋初值 0xFC67,定时 1ms
- TL0 = 0x67;
- ET0 = 1; //使能 T0 中断
- TR0 = 1; //启动 T0
- while (1){
- KeyDriver(); //调用按键驱动函数
- }
- }
- /* 步进电机启动函数,angle-需转过的角度 */
- void StartMotor(signed long angle){
- //在计算前关闭中断,完成后再打开,以避免中断打断计算过程而造成错误
- EA = 0;
- beats = (angle * 4076) / 360; //实测为 4076 拍转动一圈
- EA = 1;
- }
- /* 步进电机停止函数 */
- void StopMotor(){
- EA = 0;
- beats = 0;
- EA = 1;
- }
- /* 按键动作函数,根据键码执行相应的操作,keycode-按键键码 */
- void KeyAction(unsigned char keycode){
- static bit dirMotor = 0; //电机转动方向
- //控制电机转动 1-9 圈
- if ((keycode>=0x30) && (keycode<=0x39)){
- if (dirMotor == 0){
- StartMotor(360*(keycode-0x30));
- }else{
- StartMotor(-360*(keycode-0x30));
- }
- }else if (keycode == 0x26){ //向上键,控制转动方向为正转
- dirMotor = 0;
- }else if (keycode == 0x28){ //向下键,控制转动方向为反转
- dirMotor = 1;
- }else if (keycode == 0x25){ //向左键,固定正转 90 度
- StartMotor(90);
- }else if (keycode == 0x27){ //向右键,固定反转 90 度
- StartMotor(-90);
- }else if (keycode == 0x1B){ //Esc 键,停止转动
- StopMotor();
- }
- }
- /* 按键驱动函数,检测按键动作,调度相应动作函数,需在主循环中调用 */
- void KeyDriver(){
- unsigned char i, j;
- static unsigned char backup[4][4] = { //按键值备份,保存前一次的值
- {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}
- };
- for (i=0; i<4; i++){ //循环检测 4*4 的矩阵按键
- for (j=0; j<4; j++){
- if (backup[j] != KeySta[j]){ //检测按键动作
- if (backup[j] != 0){ //按键按下时执行动作
- KeyAction(KeyCodeMap[j]); //调用按键动作函数
- }
- backup[j] = KeySta[j]; //刷新前一次的备份值
- }
- }
- }
- }
- /* 按键扫描函数,需在定时中断中调用,推荐调用间隔 1ms */
- 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] & 0x0F) == 0x00){
- //连续 4 次扫描值为 0,即 4*4ms 内都是按下状态时,可认为按键已稳定的按下
- KeySta[keyout] = 0;
- }else if ((keybuf[keyout] & 0x0F) == 0x0F){
- //连续 4 次扫描值为 1,即 4*4ms 内都是弹起状态时,可认为按键已稳定的弹起
- KeySta[keyout] = 1;
- }
- }
- //执行下一次的扫描输出
- keyout++; //输出索引递增
- keyout = keyout & 0x03; //索引值加到 4 即归零
- //根据索引,释放当前输出引脚,拉低下次的输出引脚
- 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 TurnMotor(){
- unsigned char tmp; //临时变量
- static unsigned char index = 0; //节拍输出索引
- unsigned char code BeatCode[8] = { //步进电机节拍对应的 IO 控制代码
- 0xE, 0xC, 0xD, 0x9, 0xB, 0x3, 0x7, 0x6
- };
- if (beats != 0){ //节拍数不为 0 则产生一个驱动节拍
- if (beats > 0){ //节拍数大于 0 时正转
- index++; //正转时节拍输出索引递增
- index = index & 0x07; //用&操作实现到 8 归零
- beats--; //正转时节拍计数递减
- }else{ //节拍数小于 0 时反转
- index--; //反转时节拍输出索引递减
- index = index & 0x07; //用&操作同样可以实现到-1 时归 7
- beats++; //反转时节拍计数递增
- }
- tmp = P1; //用 tmp 把 P1 口当前值暂存
- tmp = tmp & 0xF0; //用&操作清零低 4 位
- tmp = tmp | BeatCode[index]; //用|操作把节拍代码写到低 4 位
- P1 = tmp; //把低 4 位的节拍代码和高 4 位的原值送回 P1
- }else{ //节拍数为 0 则关闭电机所有的相
- P1 = P1 | 0x0F;
- }
- }
- /* T0 中断服务函数,用于按键扫描与电机转动控制 */
- void InterruptTimer0() interrupt 1{
- static bit div = 0;
- TH0 = 0xFC; //重新加载初值
- TL0 = 0x67;
- KeyScan(); //执行按键扫描
- //用一个静态 bit 变量实现二分频,即 2ms 定时,用于控制电机
- div = ~div;
- if (div == 1){
- TurnMotor();
- }
- }
- if (backup[j] != 0){ //按键按下时执行动作
这个程序是第 8 章和本章知识的一个综合——用按键控制步进电机转动。程序中有这么几点值得注意,我们分述如下:
- 针对电机要完成正转和反转两个不同的操作,我们并没有使用正转启动函数和反转启动函数这么两个函数来完成,也没有在启动函数定义的时候增加一个形式参数来指明其方向。我们这里的启动函数 void StartMotor(signed long angle)与单向正转时的启动函数唯一的区别就是把形式参数 angle 的类型从 unsigned long 改为了 signed long,我们用有符号数固有的正负特性来区分正转与反转,正数表示正转 angle 度,负数就表示反转 angle 度,这样处理是不是很简洁又很明了呢?而你对有符号数和无符号数的区别用法是不是也更有体会了?
- 针对终止电机转动的操作,我们定义了一个单独的 StopMotor 函数来完成,尽管这个函数非常简单,尽管它也只在 Esc 按键分支内被调用了,但我们仍然把它单独提出来作为了一个函数。而这种做法就是基于这样一条编程原则:尽可能用单独的函数来完成硬件的某种操作,当一个硬件包含多个操作时,把这些操作函数组织在一起,形成一个对上层的统一接口。这样的层次化处理,会使得整个程序条理清晰,既有利于程序的调试维护,又有利于功能的扩充。
- 中断函数中要处理按键扫描和电机驱动两件事情,而为了避免中断函数过于复杂,我们就又分出了按键扫描和电机驱动两个函数(这也同样符合上述 2 的编程原则),而中断函数的逻辑就变得简洁而清晰了。这里还有个矛盾,就是按键扫描我们选择的定时时间是 1ms,而本章之前的实例中电机节拍持续时间都是 2ms;很显然,用 1ms 的定时可以定出 2ms 的间隔,而用 2ms 的定时却得不到准确的 1ms 间隔;所以我们的做法就是,定时器依然定时 1ms,然后用一个 bit 变量做标志,每 1ms 改变一次它的值,而我们只选择值为 1 的时候执行一次动作,这样就是 2ms 的间隔了;如果我要 3ms、4ms„„呢,把 bit 改为 char 或 int 型,然后对它们递增,判断到哪个值该归零,就可以了。这就是在硬件定时器的基础上实现准确的软件定时,其实类似的操作我们在讲数码管的时候也用过了,回想一下吧。