任务需求
2019年安徽省机器人大赛单片机与嵌入式系统应用技能竞赛试题
- 设计并制作智能电梯控制系统,开机后屏幕第一行显示"ZNDTKZQ",第二行显示四位数字,并自下而上滚动,3秒后停止滚动。
- 使用4x4矩阵键盘模拟电梯轿厢内的楼层选择按钮。当按键按下时,电梯控制系统记录对应楼层(建筑共9层楼高)。
- 使用步进电机驱动模块控制步进电机的转动,顺时针转动表示电梯上升,逆时针表示电梯下降。电机每转一圈表示电梯升降一个楼层。
- 使用LCD12864显示电梯所在的楼层信息。
- 当电梯空闲时(3秒内键盘未有按键按下),电梯停留到5楼。
- 当电梯启动前和电梯停止后,使用LED灯和蜂鸣器实现1S声光提示。
- 设置电梯具有互锁功能(运行时,门开不了;门开状态,不能运行)。使用继电器模块模拟电梯门状态互锁。门开时,LED灯亮电机停止;电梯门关闭,LED灯灭,电机运行。
- 设置电梯按键具有记忆功能。电梯在运行时可以及时响应各楼层按键的呼叫信号,以先方向后距离的优先原则(即当电梯在5楼到6楼的过程中同时按下3楼和9楼的按键,电梯到达6楼后运行方向不变,会继续上升到9楼后再下降到达4楼)进行判断,自动优化运行路径,运行过程具备不可逆响应功能,任何反方向的呼叫均无效。应符合实际电梯的运行模式。
硬件环境介绍
1. 开发平台套件介绍
本练习所采用的开发套件为单片机与嵌入式系统竞赛实训平台。
单片机与嵌入式竞赛实训平台拥有丰富的板载资源,一共分为公共资源去和四个实训场景,实训场景分别为智慧农业、智能音箱、智能小车以及工业互联网。在每个场景中都有丰富的传感器以及执行器,例如温湿度传感器、超声波测距传感器、步进电机以及直流电机等。
同时,该竞赛实训平台提供了三种不同的核心板——STC51核心板、STM21核心板以及FPGA核心板,可以通过核心板的插拔实现不同单片机核心的切换,大大提高利用率。
该实训平台的场景切换只需要拨动核心板上的拨码开关到对应的编码即可实现元器件的链接和切换,不需要再使用杜邦线进行连接,更加美观简洁。
本次项目练习为51单片机为核心的联系项目,因此所采用的是STC51核心板作为实训平台的核心控制模块。
2. 主控制单片机介绍
STC51核心板的核心芯片采用的是宏晶科技的STC15W4K56S4,板载256Kb外置SRAM存储器以及2个100P BTB高速连接器用于和底板连接。
在本实训平台中所使用的晶振值为24MHz。
在本次练习实验中,串口测试波特率为9600。
3. 相关元器件介绍
3.1 LCD12864液晶显示屏
LCD12864液晶显示屏是一种具有4位/8位并行、2线或3线串行多种接口方式的点阵图形液晶
显示模块,这里使用的是8位并行总线驱动。
引脚连接:
数据端(D0~7) --------------------> P0
RS --------------------------------------> P20
RW -------------------------------------> P21
EN --------------------------------------> P22
电路原理图如下:
对于LCD的操作指令以及时序图,可以看一下51单片机练习1中的介绍,更为详细,在此就不再赘述。
3.2 28BYJ-48 型步进电机
28BYJ-48型步进电机是一款4相永磁减速步进电机,其内部结构示意图如下图所示。其中中间带有0~5数字标号的为“转子”,外侧连接导线标有ABCD的为“定子”,其中“定子”一般与外壳固定,对定子上的线圈通电和断电即可通过产生的磁场吸引转子转动。
3.2.1 步进电机转动原理
28BYJ-48 型步进电机的接线一共有红、橙、黄、粉、蓝五根,其中红色线为公共端用于外接5V电源,橙、黄、粉、蓝则是对应A、B、C、D四相,如果需要其中一相导通只需要将其对应的那根线接地即可。通过各相的不断导通、关闭从而产生对应的磁场用来“吸”和“推”动转子转动,为了使步进电机处于最佳工作模式发挥其最大工作性能,一般使用八拍模式来驱动步进电机工作。
八拍模式的绕组控制顺序表如下:
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 |
因此只需要按照上表中的八拍模式,不断的将对应的相位控制线进行置低操作,即可保证步进电机的旋转。由于单片机的IO引脚输出的电流较小,一般仅为几mA,而步进电机的驱动电流会高达几百mA。为了保证步进电机的稳定工作,在本实训平台中加入了ULN2003驱动芯片用来放大驱动电流,通过ULN2003与单片机的IO口相连,引脚连接如下表所示:
ULN2003 | 单片机IO口 |
---|---|
ULN-O1 | P11 |
ULN-O2 | P12 |
ULN-O3 | P13 |
ULN-O4 | P14 |
由八拍绕组控制顺序表可知,想要使步进电机转动起来就需要按照顺序对所对应相位的单片机IO口进置低操作,由于在本实训平台中,各相的控制端口为P11~14,那个口输出低电平则导通对应的相位,所以对步进电机的八拍节拍的IO控制代码数组如下:
unsigned char code BeatCode1[8] = { //步进电机反转节拍对应的 IO 控制代码
0x1C, 0x18, 0x1A, 0x12, 0x16, 0x6, 0xE, 0xC
};
只要按照上述数组按照一定的频率进行输出,即可驱动步进电机旋转。各位可以针对自己的步进电机控制引脚修改。
3.2.2 28BYJ-48 型步进电机相关参数解读
在3.2.1中说到如果想驱动步进电机旋转,需要按一定频率对IO口进行控制代码数组的输出,那这个“一定频率”是多少呢?此时要借助一位“古人”所言,“有疑问?那就读手册!”,是的,你想知道的一切都在厂商所给的手册当中,作为一个嵌入式工程师,手册就是你的“新华字典”。
28BYJ-48 型步进电机的参数如下图所示:
在上图中我们可以看到启动频率所给的参数为 ≥550P.P.S,P.P.S即每秒脉冲数。按照厂商所给参数,那只要保证每秒至少给出550个步进脉冲就可以启动步进电机,那通过一些列初中除法运算就换算得出单节拍持续时间大于1.8毫秒的情况下就能保证有550P.P.S以上的输出,那我们只要保证输出间隔为2ms,那步进电机就一定可以转起来。
在解释了启动频率之后,接下来最重要的一个参数就是减速比。各位如果真的参考我下面所写的各模块测试中的步进电机旋转一周测试,并且真的编码测试后你会发现步进电机转一圈的时间比较长,大概在7-8秒左右,但按照我们八拍的工作模式来看,转子转动一圈所需要的节拍数只有8*8 = 64拍,而刚刚我们算了一个节拍只要2ms,那就是说其实转一圈只要64 * 2 = 128ms就够了,但实际上我们肉眼看到的是7-8秒才转了一圈。此时就是“减速比”干的好事了。
我们先看一下25BYJ-48步进电机的内部拆解图:
如图所示,我们看到中间红色方框框起来的才是真的转子,而我们在外面肉眼所看到的是经过多级齿轮传动后所连接的一个传动轴,因此实际上我们所看到的步进电机转一圈是在经过多级齿轮进行降速后的一圈。那按照厂商所给参数来看,减速比为1:64,即红色框中的转子转动64圈后外面的大传动轴才转动一圈,即需要64 *64 = 4096个节拍才行,那所要的时间就是转子转动一圈的时间就是128ms * 63 = 8192ms,差不多就是8秒左右。另外在减速比参数旁还有一个参数叫步进角度的参数,其值为5.625/64,将这个值分子分母各乘64就会发现是360/4096,刚好是一个节拍转动的角度,这个角度就叫步进角度。
这样与我们本次练习实验相关的步进电机参数也就介绍完毕。
在本次实验中,步进电机驱动原理图如下图所示:
3.3 继电器模块
在本实训平台中,继电器模块电路由三极管控制并驱动继电器,继电器模块控制原理图如下图所示。其中三极管的基极与单片机的IO口相连接。
继电器的控制引脚为P12。
3.4 4X4矩阵键盘
4x4矩阵键盘电路原理图:
引脚分配图:
3.5 LED灯&蜂鸣器
LED的电路原理图如下所示,LED与单片机P53引脚连接。
蜂鸣器的电路原理图如下说是,蜂鸣器与单片机的P55引脚连接。
4.硬件连接示意图
本次练习实验所需要的元器件与单片机的引脚连接示意图如下图所示,以此可以参考元器件的引脚连接。
5.软件流程图
对于完成任务需求,软件的大致流程如下图所示:
6.各模块测试
在完成一个完整的练习项目前,需要对构成整个项目的各部分元器件模块进行单独测试,在此基础上进行融合以及综合调试,就能诞生一个完整的单片机项目。因此进行各模块测试是完成一个项目开发的基础,也是重中之重。
6.1 步进电机旋转一周测试
在3.2章节中,我们对步进电机的启动和旋转一周进行了理论分析。通过厂家所给出的参数来看,我们要让步进电机旋转我们所要的一圈需要4096个节拍,那这次的任务需求也对我们旋转一周有要求,那刚好可以进行测试。
测试代码如下,测试效果是让步进电机转10圈(因为任务要求最多转9圈即可,那10圈不出问题,9圈也就不会有什么问题),看停止位置与开始位置是否重合:
#include "stc15f2k60s2.h"
/****************************
*****电机控制引脚为P11-14*****
*****************************/
unsigned long beats = 0; //电机转动节拍总数
void InitTimer0();
void StartMotor(unsigned long angle);
void main()
{
InitTimer0();
StartMotor(360 * 10);
while(1);
}
//初始化T0
void InitTimer0()
{
TMOD = 0x01; //设置T0为模式1
TH0 = 0xF0; //赋初值 定时2ms
TL0 = 0x60;
EA = 1; //开启总中断
ET0 = 1; //开启T0中断
TR0 = 1; //启动T0
}
//启动电机
void StartMotor(unsigned long angle)
{
EA = 0; //在计算前先把中断关闭
beats = (angle * 4096) / 360; //按照厂商所给参数为4096拍转动一圈
EA = 1;
}
//电机转动控制函数
void TurnMotor()
{
unsigned char tmp;
static unsigned char index = 0; //节拍输出索引
unsigned char code BeatCode[8] = { //步进电机节拍对应的 IO 控制代码
0x1C, 0x18, 0x1A, 0x12, 0x16, 0x6, 0xE, 0xC
};
if(beats != 0) //如果节拍数不为0,就产生一个驱动节拍
{
index ++;
index = index & 0x07; //到最后一位自动归0 即逢8归0 自己体会
beats --;
tmp = P1; //将P1口的状态暂存
tmp &= 0xE1; //将P11-14置0
tmp |= BeatCode[index]; //按位将IO控制码设置到P11-14
P1 = tmp; //将设置好的P1口参数写出
}
else
{
P1 |= 0x1E; //若节拍数为0,关闭所有电机的相位
}
}
//T0中断服务函数,驱动步进电机旋转
void InterruptTimer0() interrupt 1
{
TH0 = 0xF0; //赋初值 定时2ms
TL0 = 0x60;
TurnMotor();
}
在执行完代码后,我所使用的步进电机转完10圈后停止位置与起始位置并不重合,偏差还甚至有点大。在一阵苦思冥想后通过万能的百度和CSDN才知道了厂家所给参数是存在误差的,齿轮的减速比公式为:
减速比=从动齿轮齿数÷主动齿轮齿数
那真实的减速比应该是按照各齿轮的齿轮数相除得到减速比,再将各级减速比相乘计算得出真正的减速比,即:
(32/9)(22/11)(26/9)*(31/10)≈63.684
那以这个减速比可以算出旋转一周的节拍数大概为4076,以4076为参数输入后,电机旋转10圈仍有偏差,这次偏差为还未到起始位置就已经停下,因此我将节拍数定为4080,更好在起始位置停下。
由此可见,伟人教导我们的“实践是检验真理的唯一标准”是有道理的,凡是不能只看理论,还得是实践出真知。
6.2 控制步进电机正转反转
在确定好步进电机旋转一周所要的节拍数具体为多少后,就要实现任务要求的另外一个小点:不仅要让电机正转,还要让电机反转。正转就是按照八拍控制数组按2ms间隔输出,那反转就很简单了,将正序输出的数组反过来执行,那不就是让电机反转嘛。那如何告诉电机我要反转呢,我是通过一个有符号的长整数来定义节拍数,当节拍数输入负值的时候就开始反转。如何实现的,各位看代码即可体会。(有一说一,参考《手把手教你学51单片机》这本书对写代码真的有技巧上的帮助!!这段代码就是参考这本书中,只用一个数组就能实现正转和反转两个现象)。为了不占用太大的篇幅,只讲有改动的TurnMotor()函数贴了出来,其他照抄不误!当然!记得把beats的定义从unsigned 该为signed!!!
代码:
//电机转动控制函数
void TurnMotor()
{
unsigned char tmp;
static unsigned char index = 0; //节拍输出索引
unsigned char code BeatCode[8] = { //步进电机节拍对应的 IO 控制代码
0x1C, 0x18, 0x1A, 0x12, 0x16, 0x6, 0xE, 0xC
};
if(beats != 0) //如果节拍数不为0,就产生一个驱动节拍
{
if(beats > 0) //节拍数大于0 实现正转
{
index ++;
index = index & 0x07;
beats --;
}
else //节拍数小于0,实现反转
{
index--; //反转时节拍输出索引递减
index = index & 0x07; //用&操作同样可以实现到-1 时归 7
beats++; //反转时节拍计数递增
}
tmp = P1; //将P1口的状态暂存
tmp &= 0xE1; //将P11-14置0
tmp |= BeatCode[index]; //按位将IO控制码设置到P11-14
P1 = tmp; //将设置好的P1口参数写出
}
else
{
P1 |= 0x1E; //若节拍数为0,关闭所有电机的相位
}
}
6.3 按键扫描确定楼层并控制电机旋转
在完成电机的正转和反转自如切换后,就要开始慢慢符合任务需求了。任务需求通过步进电机旋转模拟电梯上下行,电机旋转一周即表示楼层上下一层,那电梯旋转需要几圈就需要键盘输入了,因为任务需求只说了存在9层楼,因此使用4x4矩阵键盘来实现绰绰有余。
那进行测试的思路就是先对矩阵键盘的按键进行定义,由于是测试矩阵键盘的每个按键我都设置了对应的功能,各位可以在代码注释中详细查看。使用T0中断,实现每毫秒对矩阵键盘扫描一次,通过矩阵键盘按键按下的位置定位功能表,如果是数字键,则控制电机转动对应圈数。
部分测试代码如下:
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 IO_init();
void InitTimer0();
void KeyDriver();
void main()
{
IO_init();
LCD12864_Init();
while(1)
{
KeyDriver();
}
}
void IO_init()
{
P0M0 = 0X00; P0M1 = 0X00;
P1M0 = 0X00; P1M1 = 0X00;
P2M0 = 0X00; P2M1 = 0X00;
P3M0 = 0X00; P3M1 = 0X00;
P4M0 = 0X00; P4M1 = 0X00;
P5M0 = 0X00; P5M1 = 0X00;
P6M0 = 0X00; P6M1 = 0X00;
P7M0 = 0X00; P7M1 = 0X00;
//设置P20 P21 P22为推挽输出
P2M1 &= ~(1 << 0), P2M0 |= (1 << 0);
P2M1 &= ~(1 << 1), P2M0 |= (1 << 1);
P2M1 &= ~(1 << 2), P2M0 |= (1 << 2);
//设置P11 P12 P13 P14为推挽输出
P1M1 &= ~(1 << 1), P1M0 |= (1 << 1);
P1M1 &= ~(1 << 2), P1M0 |= (1 << 2);
P1M1 &= ~(1 << 3), P1M0 |= (1 << 3);
P1M1 &= ~(1 << 4), P1M0 |= (1 << 4);
}
//初始化T0
void InitTimer0()
{
TMOD = 0x01; //设置T0为模式1
TH0 = 0xF8; //赋初值 定时2ms
TL0 = 0x30;
EA = 1; //开启总中断
ET0 = 1; //开启T0中断
TR0 = 1; //启动T0
}
//启动电机
void StartMotor(signed long angle)
{
EA = 0; //在计算前先把中断关闭
beats = (angle * 4080) / 360; //实测4080拍转动一圈
EA = 1;
}
//停止电机
void StopMotor()
{
EA = 0;
beats = 0;
EA = 1;
}
//按键动作函数,设置按键对应功能
void KeyAction(unsigned char keycode)
{
static bit dirMotor = 0; //电机转动方向 0:正转 1:反转
if((keycode >= 0x30) && (keycode <= 0x39)) //控制电机转动1-9圈
{
num = keycode - loucen;
StartMotor(360 * num);
}
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) //停止键
{
StopMotor();
}
}
//按键驱动函数,检测按键动作
void KeyDriver()
{
unsigned char i, j, index;
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++)
{
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 KeyScan()
{
unsigned char i, j = 0;
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}
};
//将一列的四个按键移入缓冲区
keybuf[0][keyout] = (keybuf[0][keyout] << 1) | KEY_IN_4;
keybuf[1][keyout] = (keybuf[1][keyout] << 1) | KEY_IN_3;
keybuf[2][keyout] = (keybuf[2][keyout] << 1) | KEY_IN_2;
keybuf[3][keyout] = (keybuf[3][keyout] << 1) | KEY_IN_1;
//消除抖动后更新按键状态
for (i=0; i<4; i++) //每行 4 个按键,所以循环 4 次
{
if ((keybuf[i][keyout] & 0x0F) == 0x00)
{ //连续 4 次扫描值为 0,即 4*4ms 内都是按下状态时,可认为按键已稳定的按下
KeySta[i][keyout] = 0;
}
else if ((keybuf[i][keyout] & 0x0F) == 0x0F)
{ //连续 4 次扫描值为 1,即 4*4ms 内都是弹起状态时,可认为按键已稳定的弹起
KeySta[i][keyout] = 1;
}
}
//执行下一次的扫描输出
keyout ++;
keyout &= 0x03; //索引值到4后自动归0
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, index_jiyi; //节拍输出索引
unsigned char code BeatCode1[8] = { //步进电机反转节拍对应的 IO 控制代码
0x1C, 0x18, 0x1A, 0x12, 0x16, 0x6, 0xE, 0xC
};
unsigned char code BeatCode[8] = { //步进电机正转节拍对应的 IO 控制代码
0xC, 0xE, 0x6, 0x16, 0x12, 0x1A, 0x18, 0x1C
};
//只要电机开始运转,按键数据都扔到数组里 数组为空再开启正常启动
if(beats != 0) //如果节拍数不为0,就产生一个驱动节拍
{
if(beats > 0) //节拍数大于0 实现正转
{
index ++;
index = index & 0x07;
beats --;
if((beats % 4080) == 0)
{
loucen += 1;
LCD12864_SetWindow(0, 5);
LCD12864_WriteData(loucen);
if(loucen >= 0x39)
loucen = 0x39;
}
tmp = P1; //将P1口的状态暂存
tmp &= 0xE1; //将P11-14置0
tmp |= BeatCode[index]; //按位将IO控制码设置到P11-14
}
else //节拍数小于0,实现反转
{
index ++;
index = index & 0x07;
beats ++;
if((beats % 4080) == 0)
{
loucen -= 1;
LCD12864_SetWindow(0, 5);
LCD12864_WriteData(loucen);
if(loucen <= 0x31)
loucen = 0x31;
}
tmp = P1; //将P1口的状态暂存
tmp &= 0xE1; //将P11-14置0
tmp |= BeatCode1[index]; //按位将IO控制码设置到P11-14
}
P1 = tmp; //将设置好的P1口参数写出
}
else
{
P1 |= 0x1E; //若节拍数为0,关闭所有电机的相位
}
}
void InterruptTimer0() interrupt 1
{
static bit div = 0; //实现二分频
TH0 = 0xF8; //赋初值 定时2ms
TL0 = 0x30;
KeyScan();
div = ~div;
if(div == 1)
{
TurnMotor();
}
}
7. 总结
“实践是检验真理的唯一标准!!!”
以上为本次实验练习的各单元模块测试,其实在进行到6.3的测试之后,本次实验练习的功能就已经实现了大半了,经过这些练习和编码之后,对于51单片机的理解和操作已经达到了一个新的高度(原来在学习过程中的我就是井底之蛙,虽然51单片机只是各入门单片机,但是吊锤我丝毫没压力)。各种片载资源以及定时器和中断的使用也越发熟练,但时钟没有达到随心所欲的境界。
接下来就是对于逻辑方面的练习了,需要选择合适的算法完成要求的最后一条,并且还要保证整个系统的稳定运行,各模块之间不会相互干扰,各位,且听下回慢慢因式分解。