想出一个关于Protues仿真的系列,回想起在大学时做课程设计,大多数是用这个来做,这个系列就围绕一些常用的外设或者说课程设计题目来进行对以前知识的回顾,温故而知新,同时也希望能对新手读者的学习有所帮助,我一般在文章最后都会给出所有的源代码,如果懒得复制的话,想要所有的工程文件,仿真文件,那就给我点赞赏吧
使用单片机来制作一个简易的计算器,实现两个整数的加减乘除等运算功能。首先,需要LCD1602来显示计算的公式,以及计算的结果。其次需要一个矩阵键盘来输入数据。
这个电路也不是很难画,我们先把仿真电路画好,尽量画漂亮一些:
下面来先把LCD1602给点亮。
1602是字符型的液晶屏,字符型LCD的引脚定义都是差不多的,在网上也能轻易找到,我在网上找了一个比较清晰的表格,1602的引脚定义如图:
然后我们就是要知道如何去控制这个LCD,对于LCD1602的驱动程序,最重要的就是两个函数了,一个是发数据函数,一个是发命令函数,然后再寻找相关手册,查看如何写命令,写数据。
查看LCD1602的手册可知:RS是命令/数据选择引脚,接单片机的一个IO扣,若RS为低电平,则为命令模式,反之则为数据模式;RW是读/写选择引脚,若RW为低电平时,则为写入,若RW为高电平时,则为读取;E脚为执行写入命令/数据的使能引脚,也就是说,每写入一次命令或数据,需要使能此引脚才会执行。
那么写入命令/数据的程序就很容易写出来了:
//--------------------------------------------
//--------------------------------------------
// 写数据函数
//--------------------------------------------
void write_data(char dat)
{
RS=1; //数据
DATA_PORT=dat; //把数据总到P口
delay_ms(5); //延时
EN=1;
delay_ms(5); //延时
EN=0; //关使能
}
然后就要了解显示字符的位置怎么判断,LCD1602第一行的首地址对应0c80,,第二行的首地址对应0xc0。
在LCD1602的初始化程序中需要配置LCD1602的一些配置指令,就是那些显示屏的配置指令,是否显示光标,地址是否自加等等配置,按照自己的需求进行配置即可,之后将光标移到最开始的位置,方便之后的操作。我的如下:
//--------------------------------------------
// 1602初始化
//--------------------------------------------
void lcd_init()
{
EN=0;
LCDRW=0;
RS=0;
write_com(0x36); //set text home address
write_com(0x38);
write_com(0x0c); //开显示,不显示光标 0x0f为开显示,开光标
write_com(0x06); //写一个字符后地址加一
write_com(0x01); //显示清零,数据指针清零
write_com(0x80); //第一行
}
这些写还是不太方便的,因为驱动1602我还是需要写很多,先要写入显示地址,再要一个字符一个字符显示,这样操作起来不是很方便,因此,我又编写了一个函数,也就是直接显示字符串,然后如果在第一行显示,就写入1,在第二行显示就写入2,程序如下:
//--------------------------------------------
// 打印函数函数
// 第一个参数为要打印的字符串 第二个参数为要显示的行
//--------------------------------------------
void print_string(char *str,uchar line){
int i;
/@@*如果line参数为1则光标移到第一行 如果为2则移到第二行*/
if(line == 1)
write_com(0x80); //第一行
else if(line == 2)
write_com(0xc0); //第二行
for(i=0;str[i]!=0;i++)
{
write_data(str[i]);
}
}
这样用这个函数操作起来,就简单多了,先声明一个数字,然后借助C语言的sprintf函数,将字符串与要显示的数字一起转化到数字里。具体sprintf的用法请自行百度,我这里举几个例子:
void main()
{
char LCD_STR_LINE[10];
sprintf((char*)LCD_STR_LINE,"Hello");
lcd_init(); //lcd初始化
print_string(LCD_STR_LINE,1); //在第一行显示字符串
while(1)
{
}
}
效果如图:
void main()
{
char LCD_STR_LINE[10];
float a = 0;
a = 1.1 + 2.2 + 3.3;
sprintf((char*)LCD_STR_LINE,"a = %f",a);
lcd_init(); //lcd初始化
print_string(LCD_STR_LINE,1); //在第一行显示字符串
while(1)
{
}
}
注意我的写法:
效果如图:
不知道你们感觉如何,我是觉得这样的函数封装比较方便。
然后我们把LCD的接口进行封装,然后写成.c与.h文件的方式,这样避免在主文件那里程序繁多,而且可以进行模块化封装,方便以后直接移植驱动。写好之后将文件添加到工程即可。
在1602做好之后,便是编写矩阵键盘的驱动。其实矩阵键盘无非也就是循环扫描,然后记住按键消抖即可。这部分的资料很好找,又比较简单,我就不再赘述。
同样的,对于按键检测,也进行文件的封装,方便以后移植,循环使用:
这里我编写了一个按键扫描函数,函数的功能就是,矩阵键盘,左上角第一个按键被按下时,返回0,第一行第二个按键被按下时返回1,依次类推,没有按键按下时,返回16。
然后我们试一下把按键检测与LCD1602结合起来,按下哪个,在LCD上就显示它的编号,代码如下:
int key_flag; //按键标志
void main()
{
char LCD_STR_LINE[10];
lcd_init(); //lcd初始化
print_string(LCD_STR_LINE,1);
while(1)
{
key_flag = key_scan(); //按键检测 没有按键被按下时 返回值为16
if(key_flag!=16) //有按键被按下
{
sprintf((char*)LCD_STR_LINE,"Key:%d ",key_flag);
print_string(LCD_STR_LINE,1);
}
}
}
效果如图:
这里我是按下了那个9对应的按键,返回值是10,是正确的。
之后要做的便是计算器的功能了,首先我们要实现的是,使用第一行作为公式的输入行,也就是说,我按下按键,在第一行要能显示我依次按下的东西,这个功能也比较好实现,也就是将按键检测结果进行字符的转换,然后将字符依次写入一个数组,然后将数组打印出来即可,初步程序如下:
uchar Line1[20]; //第一行显示数组
uchar Line2[20]; //第二行显示数组
uint key_flag; //按键标志
/@@*第一行显示函数 将按下的按键依次显示在第一行*/
void display_line1(uint key)
{
static uint num = 0;
uchar str = 0;
switch(key)
{
case 0 : str = '1'; break;
case 1 : str = '2'; break;
case 2 : str = '3'; break;
case 3 : str = '+'; break;
case 4 : str = '4'; break;
case 5 : str = '5'; break;
case 6 : str = '6'; break;
case 7 : str = '-'; break;
case 8 : str = '7'; break;
case 9 : str = '8'; break;
case 10 : str = '9'; break;
case 11 : str = '*'; break;
case 12 : break;
case 13 : str = '0'; break;
case 14 : str = '='; break;
case 15 : str = '/'; break;
default:break;
}
Line1[num] = str;
print_string(Line1,1);
num ++;
if(num >= 20)
num = 0;
}
void main()
{
lcd_init(); //lcd初始化
while(1)
{
key_flag = key_scan(); //按键检测 没有按键被按下时 返回值为16
if(key_flag!=16) //有按键被按下
{
display_line1(key_flag);
}
}
}
思路就是先声明两个数组Line1[20]与Line2[20],这两个数组就是用来存储第一行与第二行要显示的东西。如果有按键按下,就将按键结果传入第一行的显示函数,然后将按键结果进行识别,注意,识别结果直接使用字符型,这样就可以直接写入数组,而且也不用使用sprintf函数了,省了一个步骤。我随便按了几个,结果如图:
程序没什么大问题,然后进行下一步。
然后我们要做的就是要识别出来按键按下输入的数是什么,也就是说,加入我按下了一个1,又按下了一个2,又按下了一个3,能否把这一堆操作按下的数字合成一个数字,也就是一百二十三(123)。我们先实现这个功能:
这个功能就是要把数字与运算符区别开来,因此我们先声明两个变量,一个来记录第一个数字,另一个来记录运算符,我们先实现按下1,2,9,+,当我按下‘+’的时候,就把前面的1,2,9合成一个129,程序如下:
int result1 = 0; //存储第一个整数
int result2 = 0;
int Oper = 0; //存储运算符
/@@*识别第一个数字*/
void distinguish_num1(uint key)
{
static int num = 0;
/@@*如果被按下的是数字*/
switch(key)
{
case 0 : num = num * 10 + 1; break;
case 1 : num = num * 10 + 2; break;
case 2 : num = num * 10 + 3; break;
case 4 : num = num * 10 + 4; break;
case 5 : num = num * 10 + 5; break;
case 6 : num = num * 10 + 6; break;
case 8 : num = num * 10 + 7; break;
case 9 : num = num * 10 + 8; break;
case 10 : num = num * 10 + 9; break;
case 13 : num = num * 10 + 0; break;
default:break;
}
/@@*如果被按下的是运算符,则将之前的数字进行合成,然后保存运算符*/
if(key == 3)
{
/@@*加法*/
Oper = 1;
result1 = num;
sprintf((char *)Line2,"%d",result1);
print_string(Line2,2);
num = 0; //将num清零,避免影响之后的计算
}
else if(key == 7)
{
/@@*减法*/
Oper = 2;
result1 = num;
num = 0; //将num清零,避免影响之后的计算
}
else if(key == 11)
{
/@@*乘法*/
Oper = 3;
result1 = num;
num = 0; //将num清零,避免影响之后的计算
}
else if(key == 15)
{
/@@*除法*/
Oper = 4;
result1 = num;
num = 0; //将num清零,避免影响之后的计算
}
}
效果如图:
现在我们已经可以识别第一个数字以及运算符了,接下来就是识别第二个数字,然后计算,当然,我们首先需要确定怎么样才算用户或者说操作者结束输入,按照正常计算器的功能,也就是当操作者按下‘=’的时候,结束输入,然后进行计算,如此,我们接下来就是识别第二个数字,然后按下‘=’就结束运算,输出计算结果。这部分程序完全可以仿照上一个函数来写:
/@@*识别第二个数字 Oper变量用来保存运算符*/
void distinguish_num2(uint key)
{
static int num = 0;
/@@*如果被按下的是数字*/
switch(key)
{
case 0 : num = num * 10 + 1; break;
case 1 : num = num * 10 + 2; break;
case 2 : num = num * 10 + 3; break;
case 4 : num = num * 10 + 4; break;
case 5 : num = num * 10 + 5; break;
case 6 : num = num * 10 + 6; break;
case 8 : num = num * 10 + 7; break;
case 9 : num = num * 10 + 8; break;
case 10 : num = num * 10 + 9; break;
case 13 : num = num * 10 + 0; break;
default:break;
}
/@@*当等于号被按下时,识别第二个数字,并将结果计算,显示*/
if(key == 14)
{
result2 = num;
if(Oper == 1) /@@*加*/
{
result2 = result1 + result2;
}
else if(Oper == 2) /@@*减*/
{
result2 = result1 - result2;
}
else if(Oper == 3) /@@*乘*/
{
result2 = result1 * result2;
}
else if(Oper == 4) /@@*除*/
{
result2 = result1 / result2;
}
sprintf((char *)Line2,"%d",result2);
print_string(Line2,2);
/@@*将第一,二个数字,运算符以及保存的num清零*/
result1 = 0;
result2 = 0;
Oper = 0;
num = 0;
}
}
上面程序就是实现识别第二个整数,识别之后,如果按下的是等于号,则计算结果,并且显示第二行的数据。但是程序到这里,是并不能实现功能的,我们需要确定什么时候执行识别第一个数字函数,什么时候执行识别第二个数字函数。因为我们是已是否识别到第一个运算符来确定的,因此,就根据运算符的结果来确定此时应该执行哪一个函数。运算符变量最开始是0,确定之后就不是0了,因此主程序就可以这样写:
void main()
{
lcd_init(); //lcd初始化
while(1)
{
key_flag = key_scan(); //按键检测 没有按键被按下时 返回值为16
if(key_flag!=16) //有按键被按下
{
display_line1(key_flag);
/@@*当第一个数字没有识别时,执行此函数,识别第一个数字以及运算符*/
if(Oper == 0)
{
distinguish_num1(key_flag);
}
/@@*当第一个数字以及运算符已经确定的时候,执行识别第二个数字函数*/
else
{
distinguish_num2(key_flag);
}
}
}
}
到这里,可以实现基本功能了,效果如图:
但就只能计算一次,相当于是一次性计算器,额,这计算器体验极差,功能已经比较low了,不能再设计成一次性的了,至少得可以自动多次使用。
按照平常的计算器,在按了等于之后,出计算结果,然后再按下一个按键的时候,把之前的计算公式清除。按照这个思路,就是在显示第一行的公式的那个函数那里下手,在按下等于之后的下一个按键时,把显示的内容清楚掉,要实现这个,我们只需要在申请个标志位,当按下等于之后,这个标志位置1,然后在显示函数这里,判断是否有这个标志,如果有,那就把之前的东西清除掉,然后再重新显示,然后把这个标志位清零即可。在此之前,在处理等于号那段程序里,加入计算完成标志位置1:
在显示函数中加入如下处理:
再放个操作的动态图(转成GIF的清晰度....emmmm......体谅一下):
主程序 main.c:
#include
#include
#include "key.h"
#include "1602.h"
#define uchar unsigned char
#define uint unsigned int
uchar Line1[20]; //第一行显示数组
uchar Line2[20]; //第二行显示数组
uint key_flag; //按键标志
int result1 = 0; //存储第一个整数
int result2 = 0;
int Oper = 0; //存储运算符
uint succeed_flag = 0; //计算完成标志
/@@*第一行显示函数 将按下的按键依次显示在第一行*/
void display_line1(uint key)
{
static uint num = 0;
uchar str = 0;
/@@*如果有计算完成标志,将显示内容清除,并将此标志位清零*/
if(succeed_flag == 1)
{
sprintf((char *)Line1," "); //直接输入空格便会清除
print_string(Line1,1);
sprintf((char *)Line2," ");
print_string(Line2,2);
succeed_flag = 0;
num = 0;
}
switch(key)
{
case 0 : str = '1'; break;
case 1 : str = '2'; break;
case 2 : str = '3'; break;
case 3 : str = '+'; break;
case 4 : str = '4'; break;
case 5 : str = '5'; break;
case 6 : str = '6'; break;
case 7 : str = '-'; break;
case 8 : str = '7'; break;
case 9 : str = '8'; break;
case 10 : str = '9'; break;
case 11 : str = '*'; break;
case 12 : break;
case 13 : str = '0'; break;
case 14 : str = '='; break;
case 15 : str = '/'; break;
default:break;
}
Line1[num] = str;
print_string(Line1,1);
num ++;
if(num >= 20)
num = 0;
}
/@@*识别第一个数字与运算符*/
void distinguish_num1(uint key)
{
static int num = 0;
/@@*如果被按下的是数字*/
switch(key)
{
case 0 : num = num * 10 + 1; break;
case 1 : num = num * 10 + 2; break;
case 2 : num = num * 10 + 3; break;
case 4 : num = num * 10 + 4; break;
case 5 : num = num * 10 + 5; break;
case 6 : num = num * 10 + 6; break;
case 8 : num = num * 10 + 7; break;
case 9 : num = num * 10 + 8; break;
case 10 : num = num * 10 + 9; break;
case 13 : num = num * 10 + 0; break;
default:break;
}
/@@*如果被按下的是运算符,则将之前的数字进行合成,然后保存运算符*/
if(key == 3)
{
/@@*加法*/
Oper = 1;
result1 = num;
num = 0; //将num清零,避免影响之后的计算
}
else if(key == 7)
{
/@@*减法*/
Oper = 2;
result1 = num;
num = 0; //将num清零,避免影响之后的计算
}
else if(key == 11)
{
/@@*乘法*/
Oper = 3;
result1 = num;
num = 0; //将num清零,避免影响之后的计算
}
else if(key == 15)
{
/@@*除法*/
Oper = 4;
result1 = num;
num = 0; //将num清零,避免影响之后的计算
}
}
/@@*识别第二个数字*/
void distinguish_num2(uint key)
{
static int num = 0;
/@@*如果被按下的是数字*/
switch(key)
{
case 0 : num = num * 10 + 1; break;
case 1 : num = num * 10 + 2; break;
case 2 : num = num * 10 + 3; break;
case 4 : num = num * 10 + 4; break;
case 5 : num = num * 10 + 5; break;
case 6 : num = num * 10 + 6; break;
case 8 : num = num * 10 + 7; break;
case 9 : num = num * 10 + 8; break;
case 10 : num = num * 10 + 9; break;
case 13 : num = num * 10 + 0; break;
default:break;
}
/@@*当等于号被按下时,识别第二个数字,并将结果计算,显示*/
if(key == 14)
{
result2 = num;
if(Oper == 1) /@@*加*/
{
result2 = result1 + result2;
}
else if(Oper == 2) /@@*减*/
{
result2 = result1 - result2;
}
else if(Oper == 3) /@@*乘*/
{
result2 = result1 * result2;
}
else if(Oper == 4) /@@*除*/
{
result2 = result1 / result2;
}
sprintf((char *)Line2,"%d ",result2);
print_string(Line2,2);
/@@*将第一,二个数字,运算符以及保存的num清零*/
result1 = 0;
result2 = 0;
Oper = 0;
num = 0;
/@@*计算完成标志置1*/
succeed_flag = 1;
}
}
void main()
{
lcd_init(); //lcd初始化
while(1)
{
key_flag = key_scan(); //按键检测 没有按键被按下时 返回值为16
if(key_flag != 16) //有按键被按下
{
display_line1(key_flag);
/@@*当第一个数字没有识别时,执行此函数,识别第一个数字以及运算符*/
if(Oper == 0)
{
distinguish_num1(key_flag);
}
/@@*当第一个数字以及运算符已经确定的时候,执行识别第二个数字函数*/
else
{
distinguish_num2(key_flag);
}
}
}
}
矩阵按键检测头文件 key.h:
#ifndef _key_H__
#define _key_H__
//头文件: 函数声明、变量声明、宏定义、数据类型定义。
#include
#define uchar unsigned char
#define uint unsigned int
//引脚定义
#define KEY_PORT P3 //数据口
void key_delay_ms(uchar t); //延时
uint key_scan(void); //按键检测 如果无按键按下,则返回16
#endif
矩阵按键检测源文件 key.c:
#include "key.h"
//--------------------------------------------
// 延时函数
//--------------------------------------------
void key_delay_ms(uchar t)
{
int j;
for(;t!=0; t--)
for (j=0;j<255;j++);
}
//--------------------------------------------
// 按键检测 如果无按键按下,则返回16
//--------------------------------------------
uint key_scan(void)
{
uchar temp;
uint key = 16;
/@@*********************第一行*************************/
KEY_PORT = 0xfe;
temp = KEY_PORT;
temp = temp&0xf0;
if(temp!=0xf0)
{
key_delay_ms(10); //消抖
temp = KEY_PORT;
temp = temp&0xf0;
if(temp!=0xf0)
{
temp = KEY_PORT;
switch(temp)
{
case 0xee:key = 0;break;
case 0xde:key = 1;break;
case 0xbe:key = 2;break;
case 0x7e:key = 3;break;
}
while(temp!=0xf0)
{
temp = KEY_PORT;
temp = temp&0xf0;
} //等待按键释放
}
}
/@@*********************第二行*************************/
KEY_PORT = 0xfd;
temp = KEY_PORT;
temp = temp&0xf0;
if(temp!=0xf0)
{
key_delay_ms(10); //消抖
temp = KEY_PORT;
temp = temp&0xf0;
if(temp!=0xf0)
{
temp = KEY_PORT;
switch(temp)
{
case 0xed:key = 4;break;
case 0xdd:key = 5;break;
case 0xbd:key = 6;break;
case 0x7d:key = 7;break;
}
while(temp!=0xf0)
{
temp = KEY_PORT;
temp = temp&0xf0;
} //等待按键释放
}
}
/@@*********************第三行*************************/
KEY_PORT = 0xfb;
temp = KEY_PORT;
temp = temp&0xf0;
if(temp!=0xf0)
{
key_delay_ms(10); //消抖
temp = KEY_PORT;
temp = temp&0xf0;
if(temp!=0xf0)
{
temp = KEY_PORT;
switch(temp)
{
case 0xeb:key = 8;break;
case 0xdb:key = 9;break;
case 0xbb:key = 10;break;
case 0x7b:key = 11;break;
}
while(temp!=0xf0)
{
temp = KEY_PORT;
temp = temp&0xf0;
} //等待按键释放
}
}
/@@*********************第四行*************************/
KEY_PORT = 0xf7;
temp = KEY_PORT;
temp = temp&0xf0;
if(temp!=0xf0)
{
key_delay_ms(10); //消抖
temp = KEY_PORT;
temp = temp&0xf0;
if(temp!=0xf0)
{
temp = KEY_PORT;
switch(temp)
{
case 0xe7:key = 12;break;
case 0xd7:key = 13;break;
case 0xb7:key = 14;break;
case 0x77:key = 15;break;
}
while(temp!=0xf0)
{
temp = KEY_PORT;
temp = temp&0xf0;
} //等待按键释放
}
}
return key; //返回检测结果
}
LCD1602头文件 1602.h:
#ifndef _1602_H__
#define _1602_H__
//头文件: 函数声明、变量声明、宏定义、数据类型定义。
#include
#define uchar unsigned char
#define uint unsigned int
#define DATA_PORT P2 //数据口
//===========================================
// 1602函数声明
//-------------------------------------------
void delay_ms(uchar t); //延时函数
void delay_lcd(char);
void write_data(char); //写数据
void write_com(char); //写命令
void lcd_init(); //LCD初始化
void print_string(char *str,uchar line); //打印函数
#endif
LCD1602源文件 1602.c:
#include "1602.h"
#define uchar unsigned char
#define uint unsigned int
//引脚定义
sbit RS=P1^0; //数据(低)/命令(高)选择引脚
sbit LCDRW=P1^1; //写,低电平有效 3.6
sbit EN=P1^2; //使能,低电平有效 3.5
//--------------------------------------------
// 延时函数
//--------------------------------------------
void delay_ms(uchar t)
{
int j;
for(;t!=0; t--)
for (j=0;j<255;j++);
}
//--------------------------------------------
// 写数据函数
//--------------------------------------------
void write_data(char dat)
{
RS=1; //数据
DATA_PORT=dat; //把数据总到P口
delay_ms(5); //延时
EN=1;
delay_ms(5); //延时
EN=0; //关使能
}
//--------------------------------------------
// 写命令函数
//--------------------------------------------
void write_com(char com)
{
RS=0; //命令
DATA_PORT=com;
delay_ms(5); //当晶振较高时加延时
EN=1;
delay_ms(5); //当晶振较高时加延时
EN=0;
}
//--------------------------------------------
// 打印函数函数
// 第一个参数为要打印的字符串 第二个参数为要显示的行
//--------------------------------------------
void print_string(char *str,uchar line){
int i;
/@@*如果line参数为1则光标移到第一行 如果为2则移到第二行*/
if(line == 1)
write_com(0x80); //第一行
else if(line == 2)
write_com(0xc0); //第二行
for(i=0;str[i]!=0;i++)
{
write_data(str[i]);
}
}
//--------------------------------------------
// 1602初始化
//--------------------------------------------
void lcd_init()
{
EN=0;
LCDRW=0;
RS=0;
write_com(0x36); //set text home address
write_com(0x38);
write_com(0x0c); //开显示,不显示光标 0x0f为开显示,开光标
write_com(0x06); //写一个字符后地址加一
write_com(0x01); //显示清零,数据指针清零
write_com(0x80); //第一行
}