1.实验目的要求
- 掌握行列式键盘、LED、数码管、蜂鸣器、继电器等人机接口和机电设备的工作原理,以及使用单片机C语言对其进行控制的方法;
- 掌握基于状态转移及定时调度的系统分析方法,并使用此方法对系统软件结构进行分析和设计,实现所要求的功能;
- 掌握使用集成开发环境Keil进行单片机程序的设计、开发及调试的方法和过程。
2.实验要求
- 通过单片机的IO端口控制人机接口及机电设备,完成一个定时开关的设计;
- 定时开关的工作方式可设置为定时开或定时关;
- 系统通过行列式键盘接受用户的按键输入,设置工作方式和定时时长;
- 系统通过控制LED、数码管及蜂鸣器对用户的操作提供反馈和提示;
- 当用户控制计时启动时,系统对用户设定的时长进行倒计时;
- 如用户设置系统工作在定时开方式,则倒计时结束(计数到0)时控制继电器吸合;
- 如用户设置系统工作在定时关方式,则倒计时开始时继电器吸合,倒计时结束(计数到0)时继电器断开。
3.实验仪器
4.系统的分析和设计:
此系统为小型定时开关控制系统,实现两个功能,定时开和定时关功能,通过4个LED灯的亮灭了实现开关。
主函数设计,进行51单片机的初始化,等待键盘事件发生,利用solveKey函数处理键盘事件。
图1 主函数逻辑
设置了4个状态state。利用4个LED灯来提示处于哪个状态,由上到下依次为空闲状态,设置状态,定时开状态,定时关状态。
图2 各个状态说明
利用4x4的键盘进行功能实现,具体按钮功能如下:
图3 键盘功能设置说明
定时中断实现数码管显示时间,扫描键盘。
图4 定时器实现功能
具体实验操作:
导入代码后,
- 按下0行3列按钮将空闲状态切换为设置状态。(LED灯由第一个跳至第二个)
- 通过键盘功能说明设置倒计时时间。
- 按下1行2列(或1行3列)选择定时开状态(定时关状态)
- 按下0行3列,“确认”开始进行倒计时。
- 按照设置的功能状态LED在倒计时结束时点亮或熄灭。
5.关键代码的说明:
(1)主函数代码:
void main(void)
{
int i;
// 初始化串行口(T1)和T0
TMOD = 0x21; // T1,工作方式2,8位自动重装计数器;T0,工作方式1,16位计数器,软件重装
PCON |= 0x80; // SMOD = 1
SCON = 0x50; // 串口模式1,允许接收
TH1 = 256 - (OSC / 12 / 16 / BAUD); // 9600bps,N81 数据格式为N81(无校验位,8位数据位,1位停止位)。
TL1 = 256 - (OSC / 12 / 16 / BAUD);
TR1 = 1; // 启动串口(T1)
TI = 1; // 使用printf()的需要
Ticks = 0; //中断定时 次数判断
TH0 = (65536 - (OSC / 12 / TPS)) / 256; // T0 5ms中断定时常数高8位
TL0 = (65536 - (OSC / 12 / TPS)) % 256; // T0 5ms中断定时常数低8位
ET0 = 1; // 允许T0中断
EA = 1; // 打开总中断允许
TR0 = 1; // 启动T0
printf("\r\nStart StopWatch...");
POSPORT = 0xff; // 关闭所有位置驱动
CHARPORT = 0xff; // 关闭所有的段驱动
for (i = 0; i < 8; i++) DispBuf[i] = 0; // 初始化显示缓冲区为全0
DispBuf[2] = 16;
DispBuf[5] = 16; // 初始化时间串中间两个"-"的字形码位置 xx-xx-xx
LEDPos = 0; // 准备从最左边刷新显示
state = 0; //初始空闲状态
endflag = 0; //结束标志为0
startflag = 0; //开始标志为0
hour = 0; //倒计时时间为0
minute = 0;
second = 0;
viewstate(); //显示空闲状态LED
P41 = !P41; //关闭设置状态的LED
while (1)
{
if (KeyPressed)
{
printf("\r\nKey Line%bd colum%bd pressed!", LineNum, ColNum);
solveKey();
printf("\r\n%bd %bd %bd", hour,minute,second);
printf("\r\n%bd", state);
KeyPressed = 0;
}
}
}
(2)处理键盘事件函数
//响应4x4键盘的事件
//00 加一秒 01 减一秒 02 切换空闲和设置状态 03 确认开始
//10 加一分钟 11 减一分钟 12 选择定时开 13 选择定时关
//21 加一小时 22 减一小时
void solveKey(void)
{
if (LineNum == 0 && ColNum ==0) //加1s
{
if (state == 1) //只有设置状态才可以+1s
{
second++;
if (second >= 60)
{
second -= 60;
minute++;
if (minute >= 60)
{
minute -= 60;
hour++;
}
}
}
}
else if (LineNum == 0 && ColNum == 1) //减1s
{
if (state == 1) //只有设置状态才可以-1s
{
second--;
if (second < 0)
{
second += 60;
minute--;
if (minute < 0)
{
minute += 60;
hour--;
}
}
}
}
else if (LineNum == 1 && ColNum == 0) //+1分钟
{
if (state == 1) //只有设置状态才可以+1分钟
{
minute++;
if (minute >= 60)
{
minute -= 60;
hour++;
}
}
}
else if (LineNum == 1 && ColNum == 1) //-1分钟
{
if (state == 1) //只有设置状态才可以-1分钟
{
minute--;
if (minute < 0)
{
minute += 60;
hour--;
}
}
}
else if (LineNum == 2 && ColNum == 0) //+1小时
{
if (state == 1)
hour++;
}
else if (LineNum == 2 && ColNum == 1) //-1小时
{
if (state == 1)
hour--;
}
else if (LineNum == 0 && ColNum == 2) //切换系统状态,只有空闲状态和设置状态可以互相切换,定时状态要在设置状态下按下确认键进入
{
if (state == 0)
{
state = 1; viewstate();
}
else if (state == 1)
{
state = 0; viewstate();
}
}
else if (LineNum == 0 && ColNum == 3) //确认
{
startdo();
}
else if (LineNum == 1 && ColNum == 2) //切换定时开
{
state = 2; viewstate();
}
else if (LineNum == 1&& ColNum == 3) //切换定时关
{
state = 3; viewstate();
}
else //按键按错了,无操作
{
}
}
(3) 定时中断函数
void T0ISR(void) interrupt 1 // T0定时中断服务程序
{
TR0 = 0; // 暂停T0计时
TH0 = (65536 - (OSC / 12 / TPS)) / 256; // 重装T0中断定时常数高8位
TL0 = (65536 - (OSC / 12 / TPS)) % 256; // 重装T0中断定时常数低8位
TR0 = 1; // 重启T0计时
Ticks++;//中断计数 TPS每秒240次中断
/* if ((Ticks == (TPS / 2)) || (Ticks == TPS))//Ticks 的值等于半秒(TPS / 2)或者1秒(TPS),则对 P40、P41、P42 和 P43 进行取反操作。
{
P40 = !P40;
P41 = !P41;
P42 = !P42;
P43 = !P43;
}
*/
if (Ticks >= TPS) // 如果计时满1秒
{
Ticks = 0;
if (startflag == 1)
{
Secs--; // 更新计时变量
if (Secs == 0)
{
endflag = 1;
enddo();
hour = 0;
minute = 0;
second = 0;
startflag = 0;
}
}
UpdateDispBuf(); // 更新显示缓冲区
getSecs();
}
RefreshLED(); // 执行每5ms刷新显示任务
KeyScan();
}
(4)扫描键盘函数
void KeyScan(void)
{
unsigned char tmp, cnt;
LastScan = CurrScan; //保存上一次的扫描结果。
if (KeyDown) // 如果上次按下的键没有释放
{
CurrScan = P1; // 继续扫描该键位
CurrScan &= 0xf0; // 高四位为有效扫描返回值 11110000按位与操作会将 CurrScan 的低四位全部置为0,保留高四位不变。
if ((CurrScan ^ LastScan) != 0) // 表示按键释放了 在进行按位异或操作后,检查其结果是否不等于0 【当前扫描结果和上一次扫描结果是否有不同】
{//不同
KeyDown = 0;//无键按下
KeyPressed = 1;//一次按键完成
tmp = (~LastScan) & 0xf0; //保留高四位不变。
for (cnt = 0; cnt < 4; cnt++)//确定按键对应的列号,并设置相应的按键状态
{
if (tmp == ColMask[cnt])
{
ColNum = cnt;//确定列号
break;
}
}
}
}
else if(KeyDown ==0&& KeyPressed == 0) // 如果没有按键按下
{
LineNum = Ticks % 4; // 每个5ms扫描行号加1,0~3循环
P1 = ~LineMask[LineNum]; // P1低4位输出行扫描码,高4位输出1,准备输入
_nop_(); _nop_(); _nop_(); _nop_();
CurrScan = P1;
CurrScan &= 0xf0;
if (CurrScan != 0xf0) KeyDown = 1;
}
}
(5)更新显示缓冲区函数
void UpdateDispBuf(void)
{
if (startflag == 1)
{
hour = Secs / 3600;
minute = (Secs % 3600) / 60;
second = (Secs % 3600) % 60;
DispBuf[0] = hour / 10;
DispBuf[1] = hour % 10;
DispBuf[3] = minute / 10;
DispBuf[4] = minute % 10;
DispBuf[6] = second / 10;
DispBuf[7] = second % 10;
}
else
{
DispBuf[0] = hour / 10;
DispBuf[1] = hour % 10;
DispBuf[3] = minute / 10;
DispBuf[4] = minute % 10;
DispBuf[6] = second / 10;
DispBuf[7] = second % 10;
}
(6)刷新数码管显示
void RefreshLED(void)
{
POSPORT = 0xff; //关闭所有LED
CHARPORT = CharCode[DispBuf[LEDPos]];//根据 DispBuf 中的数据确定要显示的LED段码,并将其存储在 CHARPORT 中。
POSPORT = ~PosMask[LEDPos]; //即打开对应LED的位置。 ~为按位取反
LEDPos++; //检查是否超过8,如果超过8则将 LEDPos 重置为0,以实现LED位置的循环显示。
if (LEDPos >= 8) LEDPos = 0;
}
6.实验结果
(1)初始化状态(空闲状态)LED0亮
(2)切换设置状态,可随意设置时间LED1亮
(3)选择定时开功能LED2亮。我们为了方便,设置7秒。
(4)定时开功能,过程中,LED0-3全灭
(5)倒计时结束结果LED0-3全亮
(6)同理,选择定时关功能,定时7秒。LED3亮
(7)定时关过程中,LED0-3全亮
(8) 定时关结果 LED0-3全灭
7.源代码
// 电子秒表
#include <stdio.h>
#include <intrins.h>
#include <stc/stc89c5xrc.h>
//******************************************************************************
// 宏定义晶振频率、串口波特率和基本时间片时长
// 关于基本时间片时长,原来的定义是200,即每秒200次定时中断,因此定时间隔Ticks为5ms
// 实际测试中发现5ms间隔下数码管显示有些闪烁,于是将TPS改为240,即每秒240次中断,
// 此时定时间隔Ticks为4.17ms,即显示刷新的速率从200次/秒提升为240次/秒,显著改善显示效果
//******************************************************************************
#define OSC 22118400
#define BAUD 9600
#define TPS 240
//******************************************************************************
// LED位置控制(P2口):
// P20 P21 P22 P23 P24 P25 P26 P27
// (左) 1 2 3 4 5 6 7 8 (右)
// LED段码控制(P0口):
// P07 P06 P05 P04 P03 P02 P01 P00
// h g f e d c b a
//******************************************************************************
#define POSPORT P2
#define CHARPORT P0
//表示状态 0 空闲状态
// 1 设置状态
// 2 定时开状态 P4倒计时结束亮
// 3 定时关状态 P4亮,倒计时结束灭
// 4 P4全灭
// 5 P4全亮
unsigned char state;
//倒计时结束标志
//0 为未结束
//1 为结束
unsigned char endflag;
//计时开始标志
//0 为未开始
//1 为开始
unsigned char startflag;
//蜂鸣器标志
unsigned char BZIflag;
// 定义字形码,从0~F,以及"-"
unsigned char code CharCode[] =
{ 0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0x88,0x83,0xa7,0xa1,0x86,0x8e,0xbf };
// 定义显示位置掩码
unsigned char code PosMask[] = { 0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80 };
// 定义行扫描位置掩码,行线依次输出低电平
unsigned char code LineMask[] = { 0x01,0x02,0x04,0x08 };
// 定义列输入位置掩码
unsigned char code ColMask[] = { 0x10,0x20,0x40,0x80 };
unsigned char LEDPos; // LED刷新显示位置,0~7,对应左~右
unsigned char Ticks; // 中断计数,满TPS次为1秒
unsigned char hour, minute, second; // 定义时分秒变量
unsigned char DispBuf[8]; // 定义显示缓冲区,内容为时间串,格式为00-00-00
unsigned long Secs; // 总计秒数,累加
//******************************************************************************
// 行列式键盘控制(P1口):
// P10 P11 P12 P13 P14 P15 P16 P17
// 行0 行1 行2 行3 列0 列1 列2 列3
// 行线依次输出0,列线输入,P1读入的高4位不为全1表示有键按下
//******************************************************************************
bit KeyDown; // 扫描判断有按键按下=1,否则=0
bit KeyPressed; // 扫描判断有按键按下后再释放,表示一次按键动作完成
// 按下的按键编号(高4位表示读入的按键列、低4位表示输出的按键行)
unsigned char KeyCode[4][4] =
{ 0x00,0x01,0x02,0x03,
0x10,0x11,0x12,0x13,
0x20,0x21,0x22,0x23,
0x30,0x31,0x32,0x33,
};
unsigned char LineNum; // 存放扫描行计数,取值范围0~3
unsigned char ColNum; // 存放按键列编号,取值范围0~3
unsigned char CurrScan, LastScan; // 存放本次扫描和上次扫描结果
/*******************************************************************************
函数名:UpdateDispBuf
功 能:每一秒调用一次,更新时间显示缓冲区
参 数:无,操作全局变量
返 回:无
*******************************************************************************/
void UpdateDispBuf(void)
{
if (startflag == 1)
{
hour = Secs / 3600;
minute = (Secs % 3600) / 60;
second = (Secs % 3600) % 60;
DispBuf[0] = hour / 10;
DispBuf[1] = hour % 10;
DispBuf[3] = minute / 10;
DispBuf[4] = minute % 10;
DispBuf[6] = second / 10;
DispBuf[7] = second % 10;
}
else
{
DispBuf[0] = hour / 10;
DispBuf[1] = hour % 10;
DispBuf[3] = minute / 10;
DispBuf[4] = minute % 10;
DispBuf[6] = second / 10;
DispBuf[7] = second % 10;
}
}
/*******************************************************************************
函数名:RefreshLED
功 能:每5ms调用一次,更新1位LED显示内容
参 数:无,操作全局变量
返 回:无
*******************************************************************************/
void RefreshLED(void)
{
POSPORT = 0xff; //关闭所有LED
CHARPORT = CharCode[DispBuf[LEDPos]];//根据 DispBuf 中的数据确定要显示的LED段码,并将其存储在 CHARPORT 中。
POSPORT = ~PosMask[LEDPos]; //即打开对应LED的位置。 ~为按位取反
LEDPos++; //检查是否超过8,如果超过8则将 LEDPos 重置为0,以实现LED位置的循环显示。
if (LEDPos >= 8) LEDPos = 0;
}
/*******************************************************************************
函数名:KeyScan
功 能:每5ms调用一次,处理按键扫描
参 数:无,操作全局变量KeyPressed、KeyNum
返 回:无
*******************************************************************************/
void KeyScan(void)
{
unsigned char tmp, cnt;
LastScan = CurrScan; //保存上一次的扫描结果。
if (KeyDown) // 如果上次按下的键没有释放
{
CurrScan = P1; // 继续扫描该键位
CurrScan &= 0xf0; // 高四位为有效扫描返回值 11110000按位与操作会将 CurrScan 的低四位全部置为0,保留高四位不变。
if ((CurrScan ^ LastScan) != 0) // 表示按键释放了 在进行按位异或操作后,检查其结果是否不等于0 【当前扫描结果和上一次扫描结果是否有不同】
{//不同
KeyDown = 0;//无键按下
KeyPressed = 1;//一次按键完成
tmp = (~LastScan) & 0xf0; //保留高四位不变。
for (cnt = 0; cnt < 4; cnt++)//确定按键对应的列号,并设置相应的按键状态
{
if (tmp == ColMask[cnt])
{
ColNum = cnt;//确定列号
break;
}
}
}
}
else if(KeyDown ==0&& KeyPressed == 0) // 如果没有按键按下
{
LineNum = Ticks % 4; // 每个5ms扫描行号加1,0~3循环
P1 = ~LineMask[LineNum]; // P1低4位输出行扫描码,高4位输出1,准备输入
_nop_(); _nop_(); _nop_(); _nop_();
CurrScan = P1;
CurrScan &= 0xf0;
if (CurrScan != 0xf0) KeyDown = 1;
}
}
//显示状态LED P4
void viewstate(void)
{
switch (state)
{
case 0://0号灯亮
P40 = !P40;
P41 = !P41;
break;
case 1: //1号灯亮
P40 = !P40;//把第一个led灭了
P41 = !P41;
break;
case 2: //2号灯亮
P41 = !P41;
P42 = !P42;
break;
case 3: //3号灯亮
P41 = !P41;
P43 = !P43;
break;
case 4: //保持不变
P40 = P40;
P41 = P41;
P42 = P42;
P43 = P43;
break;
}
}
//时分秒转化总数
void getSecs()
{
Secs = hour * 3600 + minute * 60 + second;
}
//定时关和开 开始
void startdo()
{
if (state == 2)//定时开功能
{
P42 = !P42;
}
else if (state == 3)//定时关功能
{
P40 = !P40;
P41 = !P41;
P42 = !P42;
}
startflag = 1;
}
//定时关和开 结束
void enddo()
{
if (endflag == 1 && state == 2)
{
P40 = !P40;
P41 = !P41;
P42 = !P42;
P43 = !P43;
}
else if (endflag == 1 && state == 3)
{
P40 = !P40;
P41 = !P41;
P42 = !P42;
P43 = !P43;
}
}
/*******************************************************************************
函数名:T0ISR
功 能:通过T0的中断方式计时
参 数:
返 回:
*******************************************************************************/
void T0ISR(void) interrupt 1 // T0定时中断服务程序
{
TR0 = 0; // 暂停T0计时
TH0 = (65536 - (OSC / 12 / TPS)) / 256; // 重装T0中断定时常数高8位
TL0 = (65536 - (OSC / 12 / TPS)) % 256; // 重装T0中断定时常数低8位
TR0 = 1; // 重启T0计时
Ticks++;//中断计数 TPS每秒240次中断
/* if ((Ticks == (TPS / 2)) || (Ticks == TPS))//Ticks 的值等于半秒(TPS / 2)或者1秒(TPS),则对 P40、P41、P42 和 P43 进行取反操作。
{
P40 = !P40;
P41 = !P41;
P42 = !P42;
P43 = !P43;
}
*/
if (Ticks >= TPS) // 如果计时满1秒
{
Ticks = 0;
if (startflag == 1)
{
Secs--; // 更新计时变量
if (Secs == 0)
{
endflag = 1;
enddo();
hour = 0;
minute = 0;
second = 0;
startflag = 0;
BZIflag = 1;
}
}
if (BZIflag == 1)
{
P32 = !P32;
}
UpdateDispBuf(); // 更新显示缓冲区
getSecs();
}
RefreshLED(); // 执行每5ms刷新显示任务
KeyScan();
}
//响应4x4键盘的事件
//00 加一秒 01 减一秒 02 切换空闲和设置状态 03 确认开始
//10 加一分钟 11 减一分钟 12 选择定时开 13 选择定时关
//21 加一小时 22 减一小时
void solveKey(void)
{
if (LineNum == 0 && ColNum ==0) //加1s
{
if (state == 1) //只有设置状态才可以+1s
{
second++;
if (second >= 60)
{
second -= 60;
minute++;
if (minute >= 60)
{
minute -= 60;
hour++;
}
}
}
}
else if (LineNum == 0 && ColNum == 1) //减1s
{
if (state == 1) //只有设置状态才可以-1s
{
second--;
if (second < 0)
{
second += 60;
minute--;
if (minute < 0)
{
minute += 60;
hour--;
}
}
}
}
else if (LineNum == 1 && ColNum == 0) //+1分钟
{
if (state == 1) //只有设置状态才可以+1分钟
{
minute++;
if (minute >= 60)
{
minute -= 60;
hour++;
}
}
}
else if (LineNum == 1 && ColNum == 1) //-1分钟
{
if (state == 1) //只有设置状态才可以-1分钟
{
minute--;
if (minute < 0)
{
minute += 60;
hour--;
}
}
}
else if (LineNum == 2 && ColNum == 0) //+1小时
{
if (state == 1)
hour++;
}
else if (LineNum == 2 && ColNum == 1) //-1小时
{
if (state == 1)
hour--;
}
else if (LineNum == 0 && ColNum == 2) //切换系统状态,只有空闲状态和设置状态可以互相切换,定时状态要在设置状态下按下确认键进入
{
if (state == 0)
{
state = 1; viewstate();
}
else if (state == 1)
{
state = 0; viewstate();
}
}
else if (LineNum == 0 && ColNum == 3) //确认
{
startdo();
}
else if (LineNum == 1 && ColNum == 2) //切换定时开
{
state = 2; viewstate();
}
else if (LineNum == 1&& ColNum == 3) //切换定时关
{
state = 3; viewstate();
}
else //按键按错了,无操作
{
}
}
void main(void)
{
int i;
// 初始化串行口(T1)和T0
TMOD = 0x21; // T1,工作方式2,8位自动重装计数器;T0,工作方式1,16位计数器,软件重装
PCON |= 0x80; // SMOD = 1
SCON = 0x50; // 串口模式1,允许接收
TH1 = 256 - (OSC / 12 / 16 / BAUD); // 9600bps,N81 数据格式为N81(无校验位,8位数据位,1位停止位)。
TL1 = 256 - (OSC / 12 / 16 / BAUD);
TR1 = 1; // 启动串口(T1)
TI = 1; // 使用printf()的需要
Ticks = 0; //中断定时 次数判断
TH0 = (65536 - (OSC / 12 / TPS)) / 256; // T0 5ms中断定时常数高8位
TL0 = (65536 - (OSC / 12 / TPS)) % 256; // T0 5ms中断定时常数低8位
ET0 = 1; // 允许T0中断
EA = 1; // 打开总中断允许
TR0 = 1; // 启动T0
printf("\r\nStart StopWatch...");
POSPORT = 0xff; // 关闭所有位置驱动
CHARPORT = 0xff; // 关闭所有的段驱动
for (i = 0; i < 8; i++) DispBuf[i] = 0; // 初始化显示缓冲区为全0
DispBuf[2] = 16;
DispBuf[5] = 16; // 初始化时间串中间两个"-"的字形码位置 xx-xx-xx
LEDPos = 0; // 准备从最左边刷新显示
state = 0; //初始空闲状态
endflag = 0; //结束标志为0
startflag = 0; //开始标志为0
BZIflag = 0; //蜂鸣器标志为0
hour = 0; //倒计时时间为0
minute = 0;
second = 0;
viewstate(); //显示空闲状态LED
P41 = !P41; //关闭设置状态的LED
while (1)
{
if (KeyPressed)
{
printf("\r\nKey Line%bd colum%bd pressed!", LineNum, ColNum);
solveKey();
printf("\r\n%bd %bd %bd", hour,minute,second);
printf("\r\n%bd", state);
KeyPressed = 0;
}
}
}