任务和要求:
1、用按键和液晶实现最简易计算器,可以完成整数的加减乘除运算;
2、加减乘除分别用上下左右键来代替,回车表示等于,ESC 表示归 0(也可以只用回车键,
第 1 次表示等于,第 2 次表示归 0,循环);
3、在 LCD1602 上显示操作数 1、操作数 2、操作符(+ -/)和运算结果。
扩展部分
1、考虑小数;
2、将操作数 1、操作数 2、操作符(±/)和运算结果发给串口调试助手。
我的工程下有4个文件:
1.主函数.c
#include <reg52.h>
#define uchar unsigned char
#define uint unsigned int
#define ulong unsigned long
#define slong signed long
#define schar signed char
uchar step = 0;//操作步骤
uchar oprt = 0;//运算类型
double num1 = 0;//操作数1
double num2 = 0;//操作数2
float result = 0;//运算结果
uchar T0RH = 0;//T0重载的高字节
uchar T0RL = 0;//T0重载的低字节
bit exchange = 0;//改变回车和Esc功能
bit flagdot = 0;//小数点记录
uchar div10 = 0;
uchar str2[12];
uchar count_str2=0;
void ConfigTimer0(uint ms);
extern void KeyScan();
extern void KeyDriver();
extern void InitLcd1602();
extern void LcdShowStr(uchar x, uchar y, uchar *str);
extern void LcdAreaClear(uchar x, uchar y, uchar len);
extern void LcdFullClear();
extern void UartRxMonitor(uchar ms);
extern void UartWrite(uchar *buf);
extern void UartDriver();
extern void ConfigUART(uint baud);
void process(uchar *str);
uchar LongToString(uchar *str, slong dat);
uchar FloatToString(uchar *str, double dat);
void main()
{
EA = 1;//开关总中断
ConfigTimer0(1);//配置T0定时1ms
InitLcd1602();//初始化液晶
LcdShowStr(15, 1, "0");//初始时显示一个数字0
ConfigUART(9600);
while(1)
{
KeyDriver();//按键驱动
UartDriver();
}
}
void copy(uchar *str,uchar length)
{
uchar i,j,str4[3];
for(i=count_str2;i<count_str2+length;i++)
{
j = i - count_str2;
str2[i] = *str++;
//str2[i] = '1';
}
count_str2 += length;
//UartWrite(str2);
//str4[0]='\r';
//str4[1]='\n';
//str4[2]='\0';
//UartWrite(str4);
}
//长整形转换为字符串,str-字符串指针,dat-待转换数
uchar LongToString(uchar *str, slong dat)
{
schar i = 0;
uchar len = 0;
uchar buf[12];
if(dat < 0) //如果时负数,首先取绝对值,并在指针上加上负号
{
dat = -dat;
*str++ = '-';
len++;
}
do { //先转换为低位在前的十进制数组
buf[i++] = dat%10;
dat /= 10;
} while (dat > 0);
len += i; //i最后的值就是有效字符的个数
while (i-- > 0)//将数组的ASCII值反向拷贝到接收指针上
{
*str++ = buf[i] + '0';
}
* str = '\0';//加上字符串结束符
return len;//返回字符串长度
}
//长整形转换为字符串,str-字符串指针,dat-待转换数
uchar FloatToString(uchar *str, double dat)//*str是最终显示的结果
{
schar i = 0;//总的部分
schar i1 = 0;//整数部分
uchar i2 = 0;//小数部分
uchar len = 0;
uchar buf[12];
bit flag = 0;
double tmp=0;
unsigned long tmp2;
unsigned long tmp3;
if(dat < 0) //如果时负数,首先取绝对值,并在指针上加上负号
{
dat = -dat;
*str++ = '-';
len++;
}
tmp3 = dat;//tmp3取dat的正的部分
do { //先转换为低位在前的十进制数组,因为是倒着显示的
buf[i++] = tmp3%10;
tmp3 /= 10;
} while (tmp3 > 0);
i1 = i;
if(dat - (unsigned long)dat > 0.0001)//判断是否存在小数部分
{
flag = 1;
tmp = dat;
tmp = tmp;
do {
tmp = 10 * tmp;
tmp2 = tmp;
buf[i++] = tmp2%10;
} while(tmp - tmp2 > 0.0001 & i<4);//不能达到很精确
}
i2=i1;
len += i; //i最后的值就是有效字符的个数
while (i1-- > 0)//将数组的ASCII值反向拷贝到接收指针上
{
*str++ = buf[i1] + '0';
}
if (flag != 0)
{
* str++ = '.';
while (i2<i)
{
*str++ = buf[i2] + '0';
//*str++ = (uchar)tmp2%10 + '0';
i2++;
}
len++;
}
* str = '\0';//加上字符串结束符
return len;//返回字符串长度,加上小数点部分
}
//显示运算符,显示位置y,运算符类型type
void ShowOprt(uchar y, uchar type)
{
uchar str3;
switch(type)
{
case 0:str3 = '+';break;//0代表+
case 1:str3 = '-'; break;
case 2:str3 = '*'; break;
case 3:str3 = '/'; break;
default:break;
}
LcdShowStr(0, y, &str3);
if(y==1)
copy(&str3,1);//复制操作符
}
//计算器复位,清零变量值,清除屏幕显示
void Reset()
{
num1 = 0;
num2 = 0;
step = 0;
count_str2 = 0;
LcdFullClear();
}
//数字键动作函数,n-按键输入的数值
void NumKeyAction(float n)
{
uchar len;
uchar str[12];
uchar i;
if (step > 1)//如果已经计算完成,则重新开始新的运算
{
Reset();
flagdot=0;
div10=0;
exchange = 0;
}
if (step == 0)//输入第一操作数
{
if(flagdot==0)
{
num1 = num1*10 + n;//输入数值累加到原操作数上
len = LongToString(str, (slong)num1);//新数值转换为字符串
LcdShowStr(16-len, 1, str);//显示到液晶第二行上
}
else
{
div10++;
for(i=0;i<div10;i++)
{
n=n/10;//每次除以10的次数不一样
}
num1 = num1 + n;
len = FloatToString(str, num1);//新数值转换为字符串
LcdShowStr(16-len, 1, str);//显示到液晶第二行上
}
}
else//输入第二操作数
{
if(flagdot==0)
{
num2 = num2*10 + n;//输入数值累加到原操作数上
len = LongToString(str,(slong)num2);//新数值转换为字符串
LcdShowStr(16-len, 1, str);//显示到液晶第二行上
}
else
{
div10++;
for(i=0;i<div10;i++)
n=n/10;//每次除以10的次数不一样
num2 = num2 + n;
len = FloatToString(str, num2);//新数值转换为字符串
LcdShowStr(16-len, 1, str);//显示到液晶第二行上
}
}
}
//运算符按键动作函数,运算符类型为type
void OprtKeyAction(uchar type)
{
uchar len;
uchar str[12];
if (step == 0)//第二操作数尚未输入时响应,即不支持连续操作
{
len = FloatToString(str, num1);//第一操作数转换为字符串
copy(str,len);//复制第一操作数
LcdAreaClear(0, 0, 16-len);//清除第一行左边的字符位
LcdShowStr(16-len, 0, str);//字符串靠右显示在第一行
ShowOprt(1, type);//第二行显示操作符
LcdAreaClear(1, 1, 14);//清除第二行中间
LcdShowStr(15, 1, "0");//在第二行最右端显示0
oprt = type;//记录操作类型
step = 1;
flagdot=0;
div10=0;
}
}
//计算结果函数
void GetResult()
{
uchar len;
uchar str[12];
if (step == 1)//第二操作数已经输入时才执行计算
{
step = 2;
switch(oprt)//根据运算符类型计算结果,未考虑溢出问题
{
case 0:result =num1 + num2; break;
case 1:result =num1 - num2; break;
case 2:result =num1 * num2; break;
case 3:result =num1 / num2; break;
default:break;
}
len = FloatToString(str, num2);//原第二操作数和运算符显示到第一行
copy(str,len); //复制第二操作符
copy("=",1);
ShowOprt(0, oprt);
LcdAreaClear(1, 0, 16-1-len);
LcdShowStr(16-len,0,str);
len = FloatToString(str, result);
copy(str,len);
LcdShowStr(0, 1, "=");
LcdAreaClear(1, 1, 16-1-len);
LcdShowStr(16-len, 1, str);
str2[count_str2+1]='\r';
str2[count_str2+2]='\n';
//FloatToString(str, count_str2);
str2[count_str2]='\0';
UartWrite(str2);
UartWrite("\r\n\0");
}
}
//按键动作函数,根据按键码执行相应的操作,keycode-按键码
void KeyAction(uchar keycode)
{
if ((keycode>='0') && (keycode<='9'))
{
NumKeyAction(keycode - '0');
}
else if(keycode == 0x26)//向上键表示+
{
OprtKeyAction(0);
}
else if(keycode == 0x28)//向下键表示-
{
OprtKeyAction(1);
}
else if(keycode == 0x25)//向左键 *
{
OprtKeyAction(2);
}
else if(keycode == 0x27)//向右键 /
{
OprtKeyAction(3);
}
else if(keycode == 0x0D && exchange == 0 )//回车键,计算结果
{
GetResult();
exchange = ~exchange;
}
else if(keycode == 0x0D && exchange == 1 )//Esc键,清除
{
Reset();
LcdShowStr(15, 1, "0");
exchange = ~exchange;
flagdot=0;
div10=0;
}
else if(keycode == 0x1B)
{
flagdot=1;
}
}
//配置并启动T0,ms-T0定时时间
void ConfigTimer0(uint ms)
{
ulong tmp;
tmp = 11059200 / 12;
tmp = (tmp * ms) / 1000;
tmp = 65536 - tmp;
tmp = tmp + 28;
T0RH = (uchar)(tmp >> 8);
T0RL = (uchar)tmp;
TMOD &= 0xf0;
TMOD |= 0x01;
TH0 = T0RH;
TL0 = T0RL;
ET0 = 1;//使能T0中断
TR0 = 1;//启动T0
}
//T0中断服务函数,执行按键扫描
void InterruptTimer0() interrupt 1
{
TH0 = T0RH;
TL0 = T0RL;
KeyScan();//按键扫描
UartRxMonitor(1);
}
2.keyboard.c键盘驱动程序
#include <reg52.h>
#define uchar unsigned char
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;
uchar code KeyCodeMap[4][4] = {
{'1', '2', '3', 0x26},//向上键
{'4', '5', '6', 0x25},//向左键
{'7', '8', '9', 0x28},//向下键
{'0',0x1B, 0x0D, 0x27}//ESC键,回车键,向右键
};
uchar pdata KeySta[4][4] = {
{1,1,1,1}, {1,1,1,1}, {1,1,1,1}, {1,1,1,1}
};
extern void KeyAction(uchar keycode);
void KeyDriver()
{
uchar i, j;
static uchar pdata 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()
{
uchar i;
static uchar keyout = 0;//矩阵按键扫描输出索引
static uchar keybuf[4][4] = {
{0xff,0xff,0xff,0xff}, {0xff,0xff,0xff,0xff},
{0xff,0xff,0xff,0xff}, {0xff,0xff,0xff,0xff}
};
//将一行的四个键值移入缓冲区
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++)
{
if((keybuf[keyout][i] & 0x0f) == 0x00)
{
KeySta[keyout][i] = 0;
}
else if((keybuf[keyout][i] & 0x0f) == 0x0f)
{
KeySta[keyout][i] = 1;
}
}
//执行下一次的扫描输出
keyout++;
keyout &= 0x03;
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;
}
}
3.LCD1602.c屏幕显示程序:
#include <reg52.h>
#define uchar unsigned char
#define lcd1602_db P0
sbit lcd1602_rs = P1^0;
sbit lcd1602_rw = P1^1;
sbit lcd1602_e = P1^5;
void LcdWaitReady()
{
uchar sta;
lcd1602_db = 0xff;
lcd1602_rs = 0;
lcd1602_rw = 1;
do {
lcd1602_e = 1;
sta = lcd1602_db;
lcd1602_e = 0;
} while(sta & 0x80);//只是检测是否忙没有执行任何操作,1表示在忙,0表示空闲
}
//向液晶写入一行命令,cmd-待写入命令值
void LcdWriteCmd(uchar cmd)
{
LcdWaitReady();
lcd1602_rs = 0;//0为命令
lcd1602_rw = 0;//0表示写入
lcd1602_db = cmd;
lcd1602_e = 1;//产生高脉冲
lcd1602_e = 0;
}
//向液晶写入一字节数据,dat-待写入数据值
void LcdWriteDat(uchar dat)
{
LcdWaitReady();
lcd1602_rs = 1;//1为数据
lcd1602_rw = 0;//0表示写入
lcd1602_db = dat;
lcd1602_e = 1;//产生高脉冲
lcd1602_e = 0;
}
//设置显示RAM起始地址,亦即光标位置,(x,y)-对应屏幕上的字符坐标
void LcdSetCursor(uchar x, uchar y)
{
uchar addr;
if(y==0)
addr = 0x00 + x;//这意味着第一个显示字符x=0
else
addr = 0x40 + x;
LcdWriteCmd(addr | 0x80);//设置RAM地址
}
/*
//在液晶上显示字符串,和设置函数的功能部分重复
void LcdShowStr(uchar x,uchar y,uchar *str,uchar len)
{
LcdSetCursor(x, y);
while(len--)//通过长度来改变,而静态显示时是通过判断最后一个字符是否为/0,while(*str!='\0')
{
LcdWriteDat(*str++);
}
}
*/
void LcdShowStr(uchar x,uchar y,uchar *str)
{
LcdSetCursor(x, y);
while(*str != '\0')//通过长度来改变,而静态显示时是通过判断最后一个字符是否为/0,while(*str!='\0')
{
LcdWriteDat(*str++);
}
}
//打开光标闪烁效果
void LcdOpenCursor()
{
LcdWriteCmd(0x0f);
}
//关闭光标显示
void LcdCloseCursor()
{
LcdWriteCmd(0x0c);
}
//区域清除,清除从(x,y)坐标起始的len个字符位
void LcdAreaClear(uchar x,uchar y,uchar len)
{
LcdSetCursor(x, y);
while(len--)
{
LcdWriteDat(' ');
}
}
//整屏清除
void LcdFullClear()
{
LcdWriteCmd(0x01);
}
//初始化1602液晶
void InitLcd1602()
{
LcdWriteCmd(0x38);//写入
LcdWriteCmd(0x0c);//显示器开,光标关闭
LcdWriteCmd(0x06);//文字不动,地址自动+1
LcdWriteCmd(0x01);//清屏操作
}
4.Uart串口通信部分:
#include <reg52.h>
#define uchar unsigned char
#define uint unsigned int
bit flagFrame = 0;//帧接收完成标志
bit flagTxd = 0;//单字节发送完成标志,用来代替TXD中断标志位
uchar cntRxd = 0;//接收字节计数器
uchar pdata bufRxd[64];//接收字节缓冲区
//extern void UartAction(uchar *buf, uchar len);
//串口配置函数,baud-通信波特率
void ConfigUART(uint baud)
{
SCON = 0x50;//配置串口为模式1
TMOD &= 0x0F;//清零T1的控制位
TMOD |= 0x20;//配置T1为模式2
TH1 = 256 - (11059200/12/32)/baud;//计算T1重载值
TL1 = TH1;//初值等于重载值
ET1 = 0;//禁止T1中断
ES = 1;//使能串口中断
TR1 = 1;//启动T1
}
//串口数据写入,即串口发送函数,buf-待发送数据的指针,len-指定发送的长度
void UartWrite(uchar *buf)
{
while(*buf != '\0')
{
flagTxd = 0;//清零发送标志
SBUF = *buf++;//发送一个字节数据
while(!flagTxd);//等待该字节发送完成
}
flagTxd = 0;//清零发送标志
SBUF = '\n';//发送一个字节数据
while(!flagTxd);//等待该字节发送完成
}
/*
void UartWrite(uchar *buf, uchar len)
{
while(len--)
{
flagTxd = 0;//清零发送标志
SBUF = *buf++;//发送一个字节数据
while(!flagTxd);//等待该字节发送完成
}
}
*/
//串口数据读取函数,buf-接收指针,len-指定读取长度,返回值实际读到的长度
uchar UartRead(uchar *buf, uchar len)
{
uchar i;
if(len > cntRxd)//指定读取长度实际接收到的数据长度
{
len = cntRxd;//读取长度设置为实际接收到的数据长度
}
for(i=0;i<len;i++)
{
*buf++=bufRxd[i];
}
cntRxd = 0;//接收计数器清零
return len;//返回实际读取长度
}
//串口接收监控,由空闲时间判定帧结束,需在定时中断中调用,ms-定时间隔
void UartRxMonitor(uchar ms)
{
static uchar cntbkp = 0;
static uchar idletmr = 0;
if (cntRxd > 0)//接收计数器大于零时,监控总线空闲时间
{
if (cntbkp != cntRxd)//接收计数器发生改变,即刚接收到数据时,清零空闲计时
{
cntbkp = cntRxd;
idletmr = 0;
}
else//空闲计时小于30ms时,持续累积
{
if (idletmr < 30)//空闲计时小于30ms时,持续累加
{
idletmr += ms;
if (idletmr >= 30)//空闲时间达到30ms时,即判断一帧接收完毕
{
flagFrame = 1;//设置帧接收完成标志
}
}
}
}
else
{
cntbkp = 0;
}
}
//串口驱动函数,检测数据帧的接收,调度功能
void UartDriver()
{
uchar len;
uchar pdata buf[40];
if(flagFrame)//有命令到达时,读取处理该命令
{
flagFrame = 0;
len = UartRead(buf, sizeof(buf));
//UartAction(buf, len);
}
}
//串口中断服务函数
void InterruptUART() interrupt 4
{
if (RI)//接收到新字节
{
RI = 0;//清零接收中断标志位
if(cntRxd < sizeof(bufRxd))
{
bufRxd[cntRxd++] = SBUF;
}
}
if(TI)
{
TI = 0;//清零发送中断标志位
flagTxd = 1;//设置字节发送完成标志
}
}
结果分析:
①可以实现基础计算功能,以及后面的扩展功能。
②在小数部分还有待改进,因为最多只能输出3位小数牺牲了准确性,应该有能够计算更加准确的算法。