按鍵和液晶,可以組成我們最簡易的計(jì)算器。下面我們來寫一個(gè)簡易整數(shù)計(jì)算器提供給大家學(xué)習(xí)。為了讓程序不過于復(fù)雜,我們這個(gè)計(jì)算器不考慮連加,連減等連續(xù)計(jì)算,不考慮小數(shù)情況。加減乘除分別用上下左右來替代,回車表示等于,ESC 表示歸0。程序共分為三部分,一部分是 1602 液晶顯示,一部分是按鍵動(dòng)作和掃描,一部分是主函數(shù)功能。
/***************************Lcd1602.c 文件程序源代碼*****************************/
#include <reg52.h>
#define LCD1602_DB P0
sbit LCD1602_RS = P1^0;
sbit LCD1602_RW = P1^1;
sbit LCD1602_E = P1^5;
/* 等待液晶準(zhǔn)備好 */
void LcdWaitReady(){
unsigned char sta;
LCD1602_DB = 0xFF;
LCD1602_RS = 0;
LCD1602_RW = 1;
do {
LCD1602_E = 1;
sta = LCD1602_DB; //讀取狀態(tài)字
LCD1602_E = 0;
//bit7 等于 1 表示液晶正忙,重復(fù)檢測直到其等于 0 為止
}while (sta & 0x80);
}
/* 向 LCD1602 液晶寫入一字節(jié)命令,cmd-待寫入命令值 */
void LcdWriteCmd(unsigned char cmd){
LcdWaitReady();
LCD1602_RS = 0;
LCD1602_RW = 0;
LCD1602_DB = cmd;
LCD1602_E = 1;
LCD1602_E = 0;
}
/* 向 LCD1602 液晶寫入一字節(jié)數(shù)據(jù),dat-待寫入數(shù)據(jù)值 */
void LcdWriteDat(unsigned char dat){
LcdWaitReady();
LCD1602_RS = 1;
LCD1602_RW = 0;
LCD1602_DB = dat;
LCD1602_E = 1;
LCD1602_E = 0;
}
/* 設(shè)置顯示 RAM 起始地址,亦即光標(biāo)位置,(x,y)-對應(yīng)屏幕上的字符坐標(biāo) */
void LcdSetCursor(unsigned char x, unsigned char y){
unsigned char addr;
if (y == 0){ //由輸入的屏幕坐標(biāo)計(jì)算顯示 RAM 的地址
addr = 0x00 + x; //第一行字符地址從 0x00 起始
}else{
addr = 0x40 + x; //第二行字符地址從 0x40 起始
}
LcdWriteCmd(addr | 0x80); //設(shè)置 RAM 地址
}
/* 在液晶上顯示字符串,(x,y)-對應(yīng)屏幕上的起始坐標(biāo),str-字符串指針 */
void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str){
LcdSetCursor(x, y); //設(shè)置起始地址
while (*str != '\0'){ //連續(xù)寫入字符串?dāng)?shù)據(jù),直到檢測到結(jié)束符
LcdWriteDat(*str++);
}
}
/* 區(qū)域清除,清除從(x,y)坐標(biāo)起始的 len 個(gè)字符位 */
void LcdAreaClear(unsigned char x, unsigned char y, unsigned char len){
LcdSetCursor(x, y); //設(shè)置起始地址
while (len--){ //連續(xù)寫入空格
LcdWriteDat(' ');
}
}
/* 整屏清除 */
void LcdFullClear(){
LcdWriteCmd(0x01);
}
/* 初始化 1602 液晶 */
void InitLcd1602(){
LcdWriteCmd(0x38); //16*2 顯示,5*7 點(diǎn)陣,8 位數(shù)據(jù)接口
LcdWriteCmd(0x0C); //顯示器開,光標(biāo)關(guān)閉
LcdWriteCmd(0x06); //文字不動(dòng),地址自動(dòng)+1
LcdWriteCmd(0x01); //清屏
}
Lcd1602.c 文件中根據(jù)上層應(yīng)用的需要增加了2個(gè)清屏函數(shù):區(qū)域清屏——LcdAreaClear,整屏清屏——LcdFullClear。
/**************************keyboard.c 文件程序源代碼*****************************/
#include <reg52.h>
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;
unsigned char code KeyCodeMap[4][4] = { //矩陣按鍵編號(hào)到標(biāo)準(zhǔn)鍵盤鍵碼的映射表
{ '1', '2', '3', 0x26 }, //數(shù)字鍵 1、數(shù)字鍵 2、數(shù)字鍵 3、向上鍵
{ '4', '5', '6', 0x25 }, //數(shù)字鍵 4、數(shù)字鍵 5、數(shù)字鍵 6、向左鍵
{ '7', '8', '9', 0x28 }, //數(shù)字鍵 7、數(shù)字鍵 8、數(shù)字鍵 9、向下鍵
{ '0', 0x1B, 0x0D, 0x27 } //數(shù)字鍵 0、ESC 鍵、 回車鍵、 向右鍵
};
unsigned char pdata KeySta[4][4] = { //全部矩陣按鍵的當(dāng)前狀態(tài)
{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}
};
extern void KeyAction(unsigned char keycode);
/* 按鍵驅(qū)動(dòng)函數(shù),檢測按鍵動(dòng)作,調(diào)度相應(yīng)動(dòng)作函數(shù),需在主循環(huán)中調(diào)用 */
void KeyDriver(){
unsigned char i, j;
static unsigned char 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++){ //循環(huán)檢測 4*4 的矩陣按鍵
for (j=0; j<4; j++){
if (backup[i][j] != KeySta[i][j]){ //檢測按鍵動(dòng)作
if (backup[i][j] != 0){ //按鍵按下時(shí)執(zhí)行動(dòng)作
KeyAction(KeyCodeMap[i][j]); //調(diào)用按鍵動(dòng)作函數(shù)
}
backup[i][j] = KeySta[i][j]; //刷新前一次的備份值
}
}
}
}
/* 按鍵掃描函數(shù),需在定時(shí)中斷中調(diào)用,推薦調(diào)用間隔 1ms */
void KeyScan(){
unsigned char i;
static unsigned char keyout = 0; //矩陣按鍵掃描輸出索引
static unsigned char keybuf[4][4] = { //矩陣按鍵掃描緩沖區(qū)
{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF},
{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}
};
//將一行的 4 個(gè)按鍵值移入緩沖區(qū)
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;
//消抖后更新按鍵狀態(tài)
for (i=0; i<4; i++){ //每行 4 個(gè)按鍵,所以循環(huán) 4 次
if ((keybuf[keyout][i] & 0x0F) == 0x00){
//連續(xù) 4 次掃描值為 0,即 4*4ms 內(nèi)都是按下狀態(tài)時(shí),可認(rèn)為按鍵已穩(wěn)定的按下
KeySta[keyout][i] = 0;
}else if ((keybuf[keyout][i] & 0x0F) == 0x0F){
//連續(xù) 4 次掃描值為 1,即 4*4ms 內(nèi)都是彈起狀態(tài)時(shí),可認(rèn)為按鍵已穩(wěn)定的彈起
KeySta[keyout][i] = 1;
}
}
//執(zhí)行下一次的掃描輸出
keyout++; //輸出索引遞增
keyout &= 0x03; //索引值加到 4 即歸零
switch (keyout){ //根據(jù)索引,釋放當(dāng)前輸出引腳,拉低下次的輸出引腳
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;
}
}
keyboard.c 是對之前已經(jīng)用過多次的矩陣按鍵驅(qū)動(dòng)的封裝,具體到某個(gè)按鍵要執(zhí)行的動(dòng)作函數(shù)都放到上層的 main.c 中實(shí)現(xiàn),在這個(gè)按鍵驅(qū)動(dòng)文件中只負(fù)責(zé)調(diào)用上層實(shí)現(xiàn)的按鍵動(dòng)作函數(shù)即可。
/*****************************main.c 文件程序源代碼******************************/
#include <reg52.h>
unsigned char step = 0; //操作步驟
unsigned char oprt = 0; //運(yùn)算類型
signed long num1 = 0; //操作數(shù) 1
signed long num2 = 0; //操作數(shù) 2
signed long result = 0; //運(yùn)算結(jié)果
unsigned char T0RH = 0; //T0 重載值的高字節(jié)
unsigned char T0RL = 0; //T0 重載值的低字節(jié)
void ConfigTimer0(unsigned int ms);
extern void KeyScan();
extern void KeyDriver();
extern void InitLcd1602();
extern void LcdShowStr(unsigned char x, unsigned char y, unsigned char *str);
extern void LcdAreaClear(unsigned char x, unsigned char y, unsigned char len);
extern void LcdFullClear();
void main(){
EA = 1; //開總中斷
ConfigTimer0(1); //配置 T0 定時(shí) 1ms
InitLcd1602(); //初始化液晶
LcdShowStr(15, 1, "0"); //初始顯示一個(gè)數(shù)字 0
while (1){
KeyDriver(); //調(diào)用按鍵驅(qū)動(dòng)
}
}
/* 長整型數(shù)轉(zhuǎn)換為字符串,str-字符串指針,dat-待轉(zhuǎn)換數(shù),返回值-字符串長度 */
unsigned char LongToString(unsigned char *str, signed long dat){
signed char i = 0;
unsigned char len = 0;
unsigned char buf[12];
if (dat < 0){ //如果為負(fù)數(shù),首先取絕對值,并在指針上添加負(fù)號(hào)
dat = -dat;
*str++ = '-';
len++;
}
do { //先轉(zhuǎn)換為低位在前的十進(jìn)制數(shù)組
buf[i++] = dat % 10;
dat /= 10;
} while (dat > 0);
len += i; //i 最后的值就是有效字符的個(gè)數(shù)
while (i-- > 0){ //將數(shù)組值轉(zhuǎn)換為 ASCII 碼反向拷貝到接收指針上
*str++ = buf[i] + '0';
}
*str = '\0'; //添加字符串結(jié)束符
return len; //返回字符串長度
}
/* 顯示運(yùn)算符,顯示位置 y,運(yùn)算符類型 type */
void ShowOprt(unsigned char y, unsigned char type){
switch (type){
case 0: LcdShowStr(0, y, "+"); break; //0 代表+
case 1: LcdShowStr(0, y, "-"); break; //1 代表-
case 2: LcdShowStr(0, y, "*"); break; //2 代表*
case 3: LcdShowStr(0, y, "/"); break; //3 代表/
default: break;
}
}
/* 計(jì)算器復(fù)位,清零變量值,清除屏幕顯示 */
void Reset(){
num1 = 0;
num2 = 0;
step = 0;
LcdFullClear();
}
/* 數(shù)字鍵動(dòng)作函數(shù),n-按鍵輸入的數(shù)值 */
void NumKeyAction(unsigned char n){
unsigned char len;
unsigned char str[12];
if (step > 1){ //如計(jì)算已完成,則重新開始新的計(jì)算
Reset();
}
if (step == 0){ //輸入第一操作數(shù)
num1 = num1*10 + n; //輸入數(shù)值累加到原操作數(shù)上
len = LongToString(str, num1); //新數(shù)值轉(zhuǎn)換為字符串
LcdShowStr(16-len, 1, str); //顯示到液晶第二行上
}else{ //輸入第二操作數(shù)
num2 = num2*10 + n; //輸入數(shù)值累加到原操作數(shù)上
len = LongToString(str, num2); //新數(shù)值轉(zhuǎn)換為字符串
LcdShowStr(16-len, 1, str); //顯示到液晶第二行上
}
}
/* 運(yùn)算符按鍵動(dòng)作函數(shù),運(yùn)算符類型 type */
void OprtKeyAction(unsigned char type){
unsigned char len;
unsigned char str[12];
if (step == 0){ //第二操作數(shù)尚未輸入時(shí)響應(yīng),即不支持連續(xù)操作
len = LongToString(str, num1); //第一操作數(shù)轉(zhuǎn)換為字符串
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;
}
}
/* 計(jì)算結(jié)果函數(shù) */
void GetResult(){
unsigned char len;
unsigned char str[12];
if (step == 1){ //第二操作數(shù)已輸入時(shí)才執(zhí)行計(jì)算
step = 2;
switch (oprt){ //根據(jù)運(yùn)算符類型計(jì)算結(jié)果,未考慮溢出問題
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 = LongToString(str, num2); //原第二操作數(shù)和運(yùn)算符顯示到第一行
ShowOprt(0, oprt);
LcdAreaClear(1, 0, 16-1-len);
LcdShowStr(16-len, 0, str);
len = LongToString(str, result); //計(jì)算結(jié)果和等號(hào)顯示在第二行
LcdShowStr(0, 1, "=");
LcdAreaClear(1, 1, 16-1-len);
LcdShowStr(16-len, 1, str);
}
}
/* 按鍵動(dòng)作函數(shù),根據(jù)鍵碼執(zhí)行相應(yīng)的操作,keycode-按鍵鍵碼 */
void KeyAction(unsigned char 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){ //回車鍵,計(jì)算結(jié)果
GetResult();
}else if (keycode == 0x1B){ //Esc 鍵,清除
Reset();
LcdShowStr(15, 1, "0");
}
}
/* 配置并啟動(dòng) T0,ms-T0 定時(shí)時(shí)間 */
void ConfigTimer0(unsigned int ms){
unsigned long tmp; //臨時(shí)變量
tmp = 11059200 / 12; //定時(shí)器計(jì)數(shù)頻率
tmp = (tmp * ms) / 1000; //計(jì)算所需的計(jì)數(shù)值
tmp = 65536 - tmp; //計(jì)算定時(shí)器重載值
tmp = tmp + 28; //補(bǔ)償中斷響應(yīng)延時(shí)造成的誤差
T0RH = (unsigned char)(tmp>>8); //定時(shí)器重載值拆分為高低字節(jié)
T0RL = (unsigned char)tmp;
TMOD &= 0xF0; //清零 T0 的控制位
TMOD |= 0x01; //配置 T0 為模式 1
TH0 = T0RH; //加載 T0 重載值
TL0 = T0RL;
ET0 = 1; //使能 T0 中斷
TR0 = 1; //啟動(dòng) T0
}
/* T0 中斷服務(wù)函數(shù),執(zhí)行按鍵掃描 */
void InterruptTimer0() interrupt 1{
TH0 = T0RH; //重新加載重載值
TL0 = T0RL;
KeyScan(); //按鍵掃描
}
main.c 文件實(shí)現(xiàn)所有應(yīng)用層的操作函數(shù),即計(jì)算器功能所需要信息顯示、按鍵動(dòng)作響應(yīng)等,另外還包括主循環(huán)和定時(shí)中斷的調(diào)度。
通過這樣一個(gè)程序,大家一方面學(xué)習(xí)如何進(jìn)行多個(gè) .c 文件的編程,另外一個(gè)方面學(xué)會(huì)多個(gè)函數(shù)之間的靈活調(diào)用。可以把這個(gè)程序看成是一個(gè)簡單的小項(xiàng)目,學(xué)習(xí)一下項(xiàng)目編程都是如何進(jìn)行和布局的。不要把項(xiàng)目想象的太難,再復(fù)雜的項(xiàng)目也是這種簡單程序的組合和擴(kuò)展而已。