项目简介
预计设计出一款家庭使用计算器同时具有测量电压功能,考虑到家庭使用对精度要求并不需要使用到高精度。所以我们预计使用两块AT89C51完成该项目。达到计算设计出精度为10位char类型的计算器,小数精度可达到为6位,并且具有测量电压的功能,并采用较为简单的ADC0809对电池电压进行采样,精度可达到为0.0192V。并使用LCD1602对结果进行显示。使用了所有基本内容知识。
总体设计(要包括项目的总体功能描述,包括总体设计框图)
此项目主要分为两个设计功能,分别是基于51单片机的简易计算器设计以及采集模拟电压值并转化为数字电压值,最后都能在液晶显示屏上显示输出相关数据。计算器键盘模块采用 4*4 矩阵式键盘,实现一个能够进行基本四则运算(加、减、乘、除)的计算器功能。电压检测采用AD0809进行采样,AT89C51单片机组及显示模块完成0~5V的直流电压的检测,并通过LCD1602实时显示。项目总体设计框图如图1:
图1:项目总体设计框图
硬件设计(包括原理图以及各功能模块说明)
该项目的原理图如图二:
图2:原理图
模拟电压输入:使用ADC0809对电压源进行采集模拟电压信号。
模数转换器(ADC):通过ADC0809将采集到的模拟电压信号转换为数字信号并传入单片机中。
核心控制器:选用AT89C51系列单片机作为主控芯片。第一块单片机(Assister)接收ADC传来的数字信号,通过串口通信传输给另一块单片机(Leader)。
键盘输入模块:通过4*4矩阵键盘输入数字和运算符。通过独立按键进行定时保证单片机的平稳运行。
显示模块:使用LCD1602显示采集到的数字电压值以及计算过程和结果。
各功能模块图如图3:
图3:各功能模块图
软件设计(包括流程图、算法说明)
计算器模块
键盘扫描算法
目的:检测键盘状态,识别用户按下的键。
算法:
i.循环检测:使用定时器中断键盘矩阵。
ii.行列扫描:对于矩阵键盘,先置低一行,读取列;再依次置低其他行,读取列,直到所有行扫描完成。记录下有效按键的行列坐标。
iii.去抖动:为防止按键按下瞬间产生的机械抖动导致误判,对检测到的按键状态进行一段时间的延时后再确认是否仍被按下。
运算处理
目的:根据用户输入的运算符执行相应的数学运算,并显示计算结果。
算法:
i.数组操作:使用数组对储操作数与存储运算符进行储存。
ii.计算:当遇到等号时,对储存的数据进行运算,当检测到运算符时,会将前面的数值存入另外一个数组中,在最后进行运算。
iii.类型转换:确保在进行除法时,结果精确可以达到小数点6为后。
结果输出显示
目的:将计算结果显示在LCD屏上。
算法:
i.格式化输出:将计算结果转换为字符串格式,根据LCD屏的特性进行适当的格式化(如限制小数点后位数)。
ii.屏幕更新:使用LCD控制函数,将结果显示在屏幕的指定位置,可能需要清除之前的显示内容。
电压检测器
ADC读取与转换
目的:读取ADC转换结果,并将模拟电压值其转换为数字电压值。
算法:
i.等待转换完成:通过中断方式,等待ADC完成转换并准备好数据。
ii.读取数据:从ADC的数据寄存器中读取转换后的数字值。
iii.转换计算:根据ADC的分辨率,将数字值转换为实际电压值。公式为 电压值 = (数字值 * Vref) / (2^n - 1),其中 Vref 是ADC的参考电压,n 是ADC的位数。
显示处理
目的:将转换后的数字电压值显示在LCD屏或其他显示设备上。
算法:
i.数据格式化:将数字电压值转换为适合显示的字符串格式。
ii.屏幕初始化:初始化LCD屏幕,设置其工作模式、显示方向等。
iii.数据显示:将格式化后的电压值字符串发送到LCD,显示在指定位置。
iiii.屏幕刷新:根据需要定期更新显示,以反映最新的电压值。
流程图如图4:
图4:流程图
调试及仿真结果(包括设计成果展示以及仿真设计结果等)
使用protues进行仿真
功能一简易计算器仿真结果如图5:
图5:功能一简易计算器仿真结果
在Proteus仿真中,可以看到模拟的按键操作。用户通过点击虚拟按键,如数字键(0-9)、运算符键(+、-、*、/)以及等于号(=)、清除键(C)等,这些操作会被单片机捕获并处理。在仿真中,输入的数字和计算结果会正确地显示在LCD屏幕上,随着用户的操作动态更新。输入一系列数字和运算符后,按下等于号,屏幕应显示出正确的计算结果,并通过LCD(1602液晶显示器)来展示。设计中还考虑了运算优先级,仿真能正确处理括号内的运算先于其他运算,以及乘除优于加减的原则。
功能二电压检测器仿真结果如图6:
图6:功能二电压检测器仿真结果
在Proteus仿真中,可以看到ADC0809芯片正确连接到单片机的相应I/O端口,并且输入的模拟电压值能够被ADC芯片识别。设置一个可调的电压源(通过滑动变阻器)来模拟不同幅度的模拟电压输入,分压电路用于将电压调整至ADC可以处理的范围内(如0-5V)
转换后的数字电压值可以通过连接的显示设备(LCD1602液晶屏)显示出来。仿真中,能看到数值随模拟输入电压的变化而实时更新。进行多次仿真,改变模拟输入电压,观察系统能够稳定、准确地完成转换和显示任务。
代码
//************************************//
// author:BoronLi //
// data:2024.5.10 //
// brief: 计算器模块 //
//************************************//
#include "reg51.h"
#include <stdio.h>
#include "intrins.h"
#define u8 unsigned char
#define u16 unsigned char
sbit LCDEN=P3^4;
sbit RS=P3^5;
sbit RW=P3^6;
sbit BF=P0^7;
sbit mode_sel=P3^7;
bit flag = 0;
unsigned int volts;
unsigned char code table[]="0123456789";
u8 key;
u8 y = 16;
u8 code keyval[]="789/456*123-c0=+"; //按键对应的符号
u8 data1[10];
u8 k=0;
char m[10]={0};
double sum=0;
void delay(u16 x){ //1ms
u16 i,j;
for(i=0;i<x;i++)
for(j=0;j<115;j++);
}
void Uart_init(){
TMOD &= 0x0F;
TMOD |= 0x20; //定时器0方式2
TL1=0xf3;
TH1=0xf3;
TR1=1;
SM0=0;
SM1=1;
REN=1;
ES=1;
EA=1;
}
void Timer0Init(){
TMOD &= 0xF0;
TMOD |= 0x01;
TL0=(65535 - 5)%256+1;
TH0=(65535 - 5)/256;
//TCON配置
TF0 = 0; //清除TF0标志
TR0 = 1; //定时器0开始计时
//中断配置
ET0=1; //enable time1 interrupt
EA=1; //enable global interrupt switch
PT0=0;//低优先级
}
void Int0Init(){
IT0=1;
EX0=1;
EA=1;
}
u8 keypad4_4(){
u8 i,row,temp;
u8 key=16;
row=0x10;
for(i=0;i<4;i++)
{
P1=0x00;
P1=row;
row=_crol_(row,1);
delay(1);
temp=P1;
P2=temp;
temp=temp&0x0f;
if(temp!=0x00)
{
delay(10);//消抖
temp=P1;
temp=temp&0x0f;
if(temp!=0x00)
{
switch(temp)
{
case 0x01:key=0+i;break;
case 0x02:key=4+i;break;
case 0x04:key=8+i;break;
case 0x08:key=12+i;break;
}
do
{
temp=P1;
temp=temp&0x0f;
}while(temp!=0x00);
}
}
}
return(key);
}
unsigned char DectectBusyBit(void){
bit result;
P0 = 0xff;
RS = 0;
delay(5);
RW = 1;
LCDEN = 1;
delay(5);
result=BF;
LCDEN = 0;
return result;
}
void WrComLCD(unsigned char ComVal){//写命令函数
while(DectectBusyBit()==1); //先检测LCM是否空闲
RS = 0;
delay(1);
RW = 0;
LCDEN = 1;
P0 = ComVal;
delay(1);
LCDEN = 0;
}
void WrDatLCD(unsigned char DatVal){//写数据函数
while(DectectBusyBit()==1);
RS = 1;
delay(1);
RW = 0;
LCDEN = 1;
P0 = DatVal;
delay(1);
LCDEN = 0;
}
void LCD_Init(void){//1602初始化函数
WrComLCD(0x38); // 功能设定:16*2行、5*7点阵、8位数据接口
WrComLCD(0x38);
WrComLCD(0x38);
//多次重复设定功能指令,因为LCD启动后并不知道使用的是4位数据接口还是8位的,所以开始时总是默认为4位
WrComLCD(0x01); // 清屏
WrComLCD(0x06); // 光标自增、屏幕不动
delay(1); // 延时,等待上面的指令生效,下面再显示,防止出现乱码
WrComLCD(0x0c); // 开显示
}
void compute(){
u8 i,j=0,k,n=0;
char data3[10]={0};
int sum1,data2[10]={0};
sum=0;
for(i=0;data1[i]!='\0';i++){
if(data1[i]!='+' && data1[i]!='-' && data1[i]!='*' && data1[i]!='/'){
data2[j] =data2[j]*10+(data1[i]-'0');
}
else{
data3[n++] = data1[i];
j++;
}
}
for(i=0;i<n;i++){
if(i==0){
if(data3[0]=='+') sum = data2[0] + data2[1];
if(data3[0]=='-') sum = data2[0] - data2[1];
if(data3[0]=='*') sum = data2[0] * data2[1];
if(data3[0]=='/') sum = data2[0] / (double)data2[1];
}
if(i==1){
if(data3[1]=='+') sum = sum+data2[2];
if(data3[1]=='-') sum = sum-data2[2];
if(data3[1]=='*') sum = sum*data2[2];
if(data3[1]=='/') sum = sum/((float)data2[2]);
}
if(i==2){
if(data3[2]=='+') sum = sum+data2[3];
if(data3[2]=='-') sum = sum-data2[3];
if(data3[2]=='*') sum = sum*data2[3];
if(data3[2]=='/') sum = sum/((float)data2[3]);
}
}
//判断是小数输出还是整数输出
sum1 = sum;
if(sum1 == sum){
sprintf(m,"%d",sum1);
}
else{
sprintf(m,"%f",sum);
}
//把结果输出出来
for(k=0;m[k]!='\0';k++){
WrDatLCD(m[k]);
}
}
void main(){
LCD_Init();
Int0Init();
Uart_init();
Timer0Init();
delay(5); //延时,等待初始化完成
WrDatLCD('0');
WrComLCD(0x80); //设置显示地址第一行第一位:0X00(0x80+0x00)
while(1){
P1 = 0xF0;
if(flag == 1){
y = keypad4_4();
flag = 0;
}
}
}
void Timer0() interrupt 3{
TL0=(65535 - 5000)%256+1;
TH0=(65535 - 5000)/256;
if(mode_sel == 1){
if(y==12){
k=0;
WrComLCD(0x01);
WrDatLCD('0');
WrComLCD(0x80);
y=16;
} //清屏
if(y==14){
WrComLCD(0xC0);
WrDatLCD(keyval[y]);
WrDatLCD(' ');
data1[k]='\0';
compute();//调用出结果函数
y=16;
}
if(y<16 && y!=12 && y!=14){
WrDatLCD(keyval[y]);
data1[k++]= keyval[y];
y=16;
}
}
else{
WrComLCD(0x01);
WrComLCD(0x80);
WrDatLCD('V');
WrDatLCD('o');
WrDatLCD('l');
WrDatLCD('t');
WrDatLCD(':');
WrDatLCD(table[volts/1000]);
WrDatLCD(table[volts/100%10]);
WrDatLCD('.');
WrDatLCD(table[volts/10%10]);
WrDatLCD(table[volts%10]);
WrDatLCD('V');
delay(20);
y = 12;
}
}
void KeypadInt0() interrupt 0{
flag = 1;
}
void uart() interrupt 4{
u8 receiveData;
receiveData=SBUF; //出去接收到的数据
volts = receiveData*1.96; //转化为十进制
// P2=receiveData;
RI=0; //清除接收中断标志位
}
//************************************//
// author:BoronLi //
// data:2024.5.10 //
// brief: 电压采集模块 //
//************************************//
#include<reg51.h>
#define uchar unsigned char
#define uint unsigned int
sbit OE=P2^0; //AD转换结果输出允许端
sbit START=P2^1;//AD启动信号输入端与接受C、B、A编码时的锁存控制信号
sbit EOC=P3^2;//转换结束输出信号,AD转换开始时为低电平,转换结束时为高电平
sbit A_data=P2^3;
sbit B_data=P2^4;
sbit C_data=P2^5;
uchar i;
uint information=0; //转换出的数据
void Int0Init(){
EA=1; //开总中断
IT0=1; //跳变沿方式感知外部中断(触发方式)
EX0=1; //开外部中断0
IE0=1;
}
//串口中断初始化
void Usart_Init(void)
{
TMOD=0X20;//定时器1方式2
TH1=0xF3; //计数器初始值设置,注意波特率是4800
TL1=0xF3;
ET1=0;
TR1=1;//打开定时器
SM0=0;//设置串口工作方式
SM1=1;
EA=1;
ES=1;
}
//发送数据函数
void Send_Data(uchar Key_val)
{
SBUF=Key_val; //将要发送的数据存入发送缓冲器中
while(!TI); //若发送中断标志位没有置1(正在发送数据),就等待
TI=0; //若发送完成,TI自动置1,这里把它清零
}
void delay1ms(uint digit){
uint i,j;
for(i=0;i<digit;i++)
for(j=0;j<120;j++);
}
void main(){
Int0Init();
Usart_Init();
while(1){
START=0;
A_data=0;
B_data=0;
C_data=0;
START=1;
START=0;
Send_Data(0x88);
delay1ms(5);
}
}
void InT0(void) interrupt 0{
if(OE==1){
Send_Data(0x88);
OE=0;
}
//information=information*1.96; //将数据转换为十进制(5V/256约等于1.96)
}