【51单片机练习3——智能电梯控制系统2】

书接上回,我们完成了步进电机和按键扫描的组合,接下来就是要实现智能电梯控制系统的各相任务需求了。
为了方便阅读,硬件介绍和软硬件原理图我再Ctrl C V一下(并没有水字数)。

任务需求

2019年安徽省机器人大赛单片机与嵌入式系统应用技能竞赛试题

  1. 设计并制作智能电梯控制系统,开机后屏幕第一行显示"ZNDTKZQ",第二行显示四位数字,并自下而上滚动,3秒后停止滚动。
  2. 使用4×4矩阵键盘模拟电梯轿厢内的楼层选择按钮。当按键按下时,电梯控制系统记录对应楼层(建筑共9层楼高)。
  3. 使用步进电机驱动模块控制步进电机的转动,顺时针转动表示电梯上升,逆时针表示电梯下降。电机每转一圈表示电梯升降一个楼层。
  4. 使用LCD12864显示电梯所在的楼层信息。
  5. 当电梯空闲时(3秒内键盘未有按键按下),电梯停留到5楼。
  6. 当电梯启动前和电梯停止后,使用LED灯和蜂鸣器实现1S声光提示。
  7. 设置电梯具有互锁功能(运行时,门开不了;门开状态,不能运行)。使用继电器模块模拟电梯门状态互锁。门开时,LED灯亮电机停止;电梯门关闭,LED灯灭,电机运行。
  8. 设置电梯按键具有记忆功能。电梯在运行时可以及时响应各楼层按键的呼叫信号,以先方向后距离的优先原则(即当电梯在5楼到6楼的过程中同时按下3楼和9楼的按键,电梯到达6楼后运行方向不变,会继续上升到9楼后再下降到达4楼)进行判断,自动优化运行路径,运行过程具备不可逆响应功能,任何反方向的呼叫均无效。应符合实际电梯的运行模式。

硬件环境介绍

1. 开发平台套件介绍

本练习所采用的开发套件为单片机与嵌入式系统竞赛实训平台。
单片机嵌入式系统竞赛实训平台
单片机与嵌入式竞赛实训平台拥有丰富的板载资源,一共分为公共资源去和四个实训场景,实训场景分别为智慧农业、智能音箱、智能小车以及工业互联网。在每个场景中都有丰富的传感器以及执行器,例如温湿度传感器、超声波测距传感器、步进电机以及直流电机等。
同时,该竞赛实训平台提供了三种不同的核心板——STC51核心板、STM21核心板以及FPGA核心板,可以通过核心板的插拔实现不同单片机核心的切换,大大提高利用率。
该实训平台的场景切换只需要拨动核心板上的拨码开关到对应的编码即可实现元器件的链接和切换,不需要再使用杜邦线进行连接,更加美观简洁。
本次项目练习为51单片机为核心的联系项目,因此所采用的是STC51核心板作为实训平台的核心控制模块。

2. 主控制单片机介绍

STC51核心板的核心芯片采用的是宏晶科技的STC15W4K56S4,板载256Kb外置SRAM存储器以及2个100P BTB高速连接器用于和底板连接。
在本实训平台中所使用的晶振值为24MHz。
在本次练习实验中,串口测试波特率为9600。
STC51核心板

3. 相关元器件介绍

3.1 LCD12864液晶显示屏

LCD12864液晶显示屏是一种具有4位/8位并行、2线或3线串行多种接口方式的点阵图形液晶
显示模块,这里使用的是8位并行总线驱动。
引脚连接:
数据端(D0~7) ——————–> P0
RS ————————————–> P20
RW ————————————-> P21
EN ————————————–> P22
电路原理图如下:
12864电路原理图
对于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矩阵键盘

4×4矩阵键盘电路原理图:
4x4矩阵键盘原理图
引脚分配图:
引脚分配图

3.5 LED灯&蜂鸣器

LED的电路原理图如下所示,LED与单片机P53引脚连接。
LED电路原理图
蜂鸣器的电路原理图如下说是,蜂鸣器与单片机的P55引脚连接。
蜂鸣器电路原理图

4.硬件连接示意图

本次练习实验所需要的元器件与单片机的引脚连接示意图如下图所示,以此可以参考元器件的引脚连接。
硬件连接示意图

5.软件流程图

对于完成任务需求,软件的大致流程如下图所示:
软件流程图

6.任务需求实现

6.1 开机后屏幕第一行显示"ZNDTKZQ",第二行显示四位数字,并自下而上滚动,3秒后停止滚动。

这个要求和在之前的练习1中是相同的,就是初始化LCD12864屏幕后使用for循环完成三次位置变换,每次延迟1S,结束for循环后再次显示。
代码如下:

for(i; i>0; i--)
{
	LCD12864_ShowStr(i - 1, 0, "DCFZBJQ");
	LCD12864_ShowStr(i, 0, "8957");
	//i--;
	Delay_ms(1000);
	LCD12864_WriteCmd(0x01);
}
LCD12864_ShowStr(2, 0, "DCFZBJQ");
LCD12864_ShowStr(3, 0, "8957");

6.2 使用4×4矩阵键盘模拟电梯轿厢内的楼层选择按钮

这个需求在上一篇的模块测试中就已经完成,通过中断函数每毫秒扫描一次矩阵键盘,将键盘的状态位映射到功能表上,在对各按键功能进行switch挨个确定,就能实现按下几,就去几,同时在LCD12864上显示目前所在楼层和目标楼层。
部分功能函数代码如下:

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}
};

//按键动作函数,设置按键对应功能
void KeyAction(unsigned char keycode)
{
	static bit dirMotor = 0;	//电机转动方向 0:正转 1:反转
	if((keycode >= 0x30) && (keycode <= 0x39))	//控制电机转动1-9圈
	{
		num = keycode - loucen;
		UartSendStr("num:");
		UartSendInt(num);
		UartSendStr("\r\n");
		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)			//按键按下时执行的操作
				{
					if(IfRun)
					{
						if(jiyiindex < 3)
						{
							jiyi[jiyiindex] = KeyCodeMap[i][j];
							jiyiindex ++;
						}
						else
						{
							LCD12864_ShowStr(2, 0, "记忆已满");
						}
					}
					else
					{
						//if(IfNull)
						{
							KeyAction(KeyCodeMap[i][j]); //调用按键动作函数
							UartSendByte(KeyCodeMap[i][j]);
							UartSendStr("\r\n");
							LCD12864_SetWindow(1, 5);
							LCD12864_WriteData(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;
	}
}

6.3 使用步进电机驱动模块控制步进电机的转动,顺时针转动表示电梯上升,逆时针表示电梯下降。

是不是发现在进行单独模块测试后,写需求的速度和方便性越来越高,这就是万事俱备,现在再次融合一下就是“东风”。那就不再多说,直接上电机控制相关的代码。

//启动电机
void StartMotor(signed long angle)
{
	EA = 0;							//在计算前先把中断关闭
	beats = (angle * 4080) / 360;	//实测4080拍转动一圈
	EA = 1;
}

//电机转动控制函数
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,就产生一个驱动节拍
	{
		IfRun = 1;					//运行状态设置为1 表示电机进入工作状态
		time = 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,关闭所有电机的相位
		IfRun = 0;
		if(IfNull(jiyi, 3))
		{
			time ++;					//电机不转 默认按键没有按下 开始计时等待
			if(time == 1500)
			{
				KeyAction(KeyCodeMap[1][1]);
				LCD12864_SetWindow(1, 5);
				LCD12864_WriteData(KeyCodeMap[1][1]);
			}
		}
		else
		{
			for(index_jiyi = 0; index_jiyi < 3; index_jiyi++)
			{
				if(jiyi[index_jiyi] != 0x00)
				{
					KeyAction(jiyi[index_jiyi]);
					LCD12864_SetWindow(1, 5);
					LCD12864_WriteData(jiyi[index_jiyi]);
					jiyi[index_jiyi] = 0x00;
				}
			}
		}
	}	
}

6.4使用LCD12864显示电梯所在的楼层信息

实现方式:
使用LCD12864显示楼层信息这个功能,我认为的难点在于如何实现步进电机旋转一圈后楼层上下刚好改变一层。我实现的方式是将总节拍数和旋转一周所要的节拍数4080进行取余判断,如果取余结果为0,那就说明已经刚好转过一周,那就控制楼层数加一或是减一操作。
重点展示在电机运行过程中如何对LCD显示的操作代码:

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)	//防止过9,实际与总节拍数相关,不会超过
					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
		}

6.5 当电梯空闲时(3秒内键盘未有按键按下),电梯停留到5楼

实现方式:
因为按键按下就会自动扫描开始驱动电机进行转动,那如果电机没转也就意味着并没有按键按下。由于电机转动的函数是在T0中断中执行,本身就有计时的效果在,因此就不需要再写一个计时或者是延时触发函数,所以我就可以在T0中断函数中执行电机转动TurnMotor()函数中加入一个计时变量time用来判断是否达到3秒。因为中断是每1毫秒触发一次,而电机的启动间隔需要1.8毫秒,因此我在中断中加入了二分法,通过一个div变量只有它为1时才执行电机转动,这样刚好就是2ms间隔。
如何实现计时3秒呢,那就是对那个计时变量time进行自增操作,不管电机是否要转动TurnMotor()函数固定每2ms要执行一次,那就是在没有节拍数传入的时候每2ms对time进行自增操作,只要自增到1500,更好就是1500 * 2 = 3000 ms也就是3s。如果在等待过程中有按键按下,那在执行电机转动前,直接将time清零直到再次没有按下开始重新自增计时。
对应代码:

time ++;					//电机不转 默认按键没有按下 开始计时等待
if(time == 1500)			
{
	KeyAction(KeyCodeMap[1][1]);//到3秒自动传入到5楼的按键功能码
	LCD12864_SetWindow(1, 5);
	LCD12864_WriteData(KeyCodeMap[1][1]);
}

6.6 当电梯启动前和电梯停止后,使用LED灯和蜂鸣器实现1S声光提示

这个只需要在启动电机前和电机旋转完成后进行一次LED灯以及蜂鸣器的操作即可。
代码:

void Led_Bee_Relay()
{
	LED = 0;
	BEEP = ~BEEP;
	delay(500);
	LED = 1;
	
	if(IfRun = 1)	//运行过程中 继电器关
	{
		relay = 0;
	}
	else
	{
		relay = 1;
	}
}

6.7 置电梯按键具有记忆功能,电梯在运行时可以及时响应各楼层按键的呼叫信号,以先方向后距离的优先原则进行判断,自动优化运行路径,运行过程具备不可逆响应功能

该部分的功能,我只实现了记忆功能,通过一个数组,在电机运行过程中将所有的按键扫描所得到的按键保留在数组中,当电机停下时对数组进行一次非零判断,只要数组中有楼层数据,那就继续执行对应的楼层,执行完毕后将对应的数组元素清零,最后再次判断。如果数组已经为零,那就开始执行3S按键等待,到3秒后自动停靠5楼。
关于以先方向后距离的判断原则还在调试中,后续会保持更新,如果各位有更好的idea还请不吝赐教。
实现代码:

unsigned char jiyi[3] = {0x00, 0x00, 0x00};	//实现电梯按键记忆

//判断数组是否为空
unsigned char IfNull(unsigned char *jiyi, unsigned char len)
{
	unsigned char i;
	unsigned char num;
	for(i = 0; i < len; i++)
	{
		num += jiyi[i];
	}
	if(num != 0x00)
	{
		return 1;
	}
	else
		return 0;
}

if(IfRun)
{
	if(jiyiindex < 3)
	{
		jiyi[jiyiindex] = KeyCodeMap[i][j];
		jiyiindex ++;
	}
	else
	{
		LCD12864_ShowStr(2, 0, "记忆已满");
	}
}
else
{
	KeyAction(KeyCodeMap[i][j]); //调用按键动作函数
	UartSendByte(KeyCodeMap[i][j]);
	UartSendStr("\r\n");
	LCD12864_SetWindow(1, 5);
	LCD12864_WriteData(KeyCodeMap[i][j]);
}

效果演示:

按键演示

物联沃分享整理
物联沃-IOTWORD物联网 » 【51单片机练习3——智能电梯控制系统2】

发表评论