51单片机计算器课程设计
项目概述
本项目旨在设计并制作一个基于51系列单片机的简易计算器,该计算器能够实现基本的四则运算(加、减、乘、除),并通过4x4矩阵键盘输入数字和运算符,同时使用LCD1602液晶显示屏显示输入的数字、运算符和最终的计算结果。
设计目标:

- 实现数字键
0-9的输入与显示。 - 实现运算符键 , , , 的输入与显示。
- 实现等号键 ,用于触发计算并显示结果。
- 实现清除键
C,用于清除当前输入。 - 支持连续运算(如
3+5*2)。 - 在LCD1602上实时显示输入过程和计算结果。
核心技术:
- 主控芯片: STC89C52RC (或任何兼容的51单片机,如AT89S52)
- 显示模块: LCD1602 字符液晶屏
- 输入模块: 4x4 矩阵键盘
- 开发环境: Keil C51 + Proteus 仿真 / 硬件焊接调试
硬件设计
硬件清单: | 器件名称 | 型号/规格 | 数量 | 备注 | | :--- | :--- | :--- | :--- | | 单片机最小系统板 | STC89C52RC | 1 | 包含晶振、复位电路 | | LCD1602液晶屏 | 1602A | 1 | 带背光 | | 矩阵键盘 | 4x4 | 1 | 或8个独立按键+4个功能键 | | 排阻 | 10KΩ | 1 | 用于LCD1602数据线D0-D7上拉 | | 杜邦线 | 若干 | | 用于连接 |
硬件连接原理图 (关键引脚):
-
LCD1602 与单片机连接:

VSS-> GNDVDD-> +5VV0-> 中间接一个10KΩ的电位器,用于调节对比度RS(寄存器选择) -> P2.5RW(读写选择) -> GND (只写模式)EN(使能) -> P2.7D0-D7(数据线) -> P0.0 - P0.7 (P0口需外接上拉排阻)A(背阳极) -> +5V (通过一个限流电阻,如220Ω)K(背阴极) -> GND
-
4x4矩阵键盘 与单片机连接:
- 将4行连接到单片机的一个I/O口组,
P1.0, P1.1, P1.2, P1.3。 - 将4列连接到另一个I/O口组,
P1.4, P1.5, P1.6, P1.7。 - 这样总共使用P1口的8个引脚,实现16个按键。
- 将4行连接到单片机的一个I/O口组,
Proteus 仿真原理图示例: 你可以在Proteus中绘制如下连接,或者在网上搜索 "51单片机计算器 Proteus 原理图" 找到现成的参考图。
软件设计
软件设计是项目的核心,我们将采用模块化的思想,将代码分为多个功能模块,便于编写和调试。
程序流程图:

graph TD
A[开始] --> B{系统初始化};
B --> C{LCD1602初始化};
B --> D{矩阵键盘初始化};
B --> E{清空显示缓冲区};
E --> F[主循环];
F --> G{扫描键盘};
G --> H{是否有按键按下?};
H -- 否 --> F;
H -- 是 --> I{消抖};
I --> J{确认按键};
J --> K{获取按键值};
K --> L{按键处理};
L --> M{是数字键?};
M -- 是 --> N{追加到显示缓冲区};
M -- 否 --> O{是运算符键?};
O -- 是 --> P{处理运算符};
O -- 否 --> Q{是等号键?};
Q -- 是 --> R{计算结果};
Q -- 否 --> S{是清除键?};
S -- 是 --> T{清空所有};
N --> U{更新LCD显示};
P --> U;
R --> U;
T --> U;
U --> F;
模块化代码结构:
main.c: 主函数,负责调用各个模块函数,实现主循环逻辑。lcd1602.c/lcd1602.h: LCD1602驱动模块,封装所有LCD操作函数(如Lcd_Init(),Lcd_ShowChar(),Lcd_ShowString()等)。matrix_key.c/matrix_key.h: 矩阵键盘扫描模块,封装按键检测和获取函数(如Key_Scan())。calculator.c/calculator.h: 计算逻辑核心模块,处理输入、运算和显示更新。
核心算法与代码实现:
a. 矩阵键盘扫描 (Key_Scan)
采用行列扫描法,通过逐行置低电平,然后读取列状态来判断是哪个按键被按下。
// matrix_key.h
#ifndef __MATRIX_KEY_H__
#define __MATRIX_KEY_H__
unsigned char Key_Scan(); // 返回按键值,0表示无按键
#endif
// matrix_key.c
#include "reg52.h"
#include "matrix_key.h"
// 定义连接矩阵键盘的端口
sbit KEY_P1 = P1^0; // 行0
sbit KEY_P2 = P1^1; // 行1
sbit KEY_P3 = P1^2; // 行2
sbit KEY_P4 = P1^3; // 行3
sbit KEY_C1 = P1^4; // 列0
sbit KEY_C2 = P1^5; // 列1
sbit KEY_C3 = P1^6; // 列2
sbit KEY_C4 = P1^7; // 列3
unsigned char Key_Scan() {
unsigned char key_value = 0;
unsigned char i;
// 扫描行
for (i = 0; i < 4; i++) {
// 置当前行为低电平,其他行为高电平
KEY_P1 = 1; KEY_P2 = 1; KEY_P3 = 1; KEY_P4 = 1;
switch (i) {
case 0: KEY_P1 = 0; break;
case 1: KEY_P2 = 0; break;
case 2: KEY_P3 = 0; break;
case 3: KEY_P4 = 0; break;
}
// 检测列
if (KEY_C1 == 0) { key_value = i * 4 + 1; while(!KEY_C1); }
else if (KEY_C2 == 0) { key_value = i * 4 + 2; while(!KEY_C2); }
else if (KEY_C3 == 0) { key_value = i * 4 + 3; while(!KEY_C3); }
else if (KEY_C4 == 0) { key_value = i * 4 + 4; while(!KEY_C4); }
}
// 按键值映射
switch(key_value) {
case 1: return '7'; break;
case 2: return '8'; break;
case 3: return '9'; break;
case 4: return '/'; break;
case 5: return '4'; break;
case 6: return '5'; break;
case 7: return '6'; break;
case 8: return '*'; break;
case 9: return '1'; break;
case 10: return '2'; break;
case 11: return '3'; break;
case 12: return '-'; break;
case 13: return 'C'; break; // 清除
case 14: return '0'; break;
case 15: return '='; break; // 等号
case 16: return '+'; break;
default: return 0; // 无按键
}
}
b. 计算逻辑核心
这是最复杂的部分,为了处理连续运算(如 3+5*2),我们需要一个表达式求值算法,对于51单片机,一个简单有效的方法是使用逆波兰表达式(后缀表达式),或者更简单的双栈法。
这里我们采用一个更直观的两步法(适用于简单表达式):
- 将输入的字符串(如 "3+5*2")分解成数字和运算符的列表。
- 根据运算符优先级进行计算。
为了简化,我们先实现一个不支持优先级的版本(从左到右计算),然后再升级为支持优先级的版本。
简化版 (不支持优先级):
// calculator.c
#include "calculator.h"
#include "stdio.h" // 用于 sprintf
// 假设我们有两个操作数和一个运算符
float num1 = 0, num2 = 0;
float result = 0;
char op = 0;
unsigned char input_buffer[16]; // 存储输入的字符串
unsigned char buffer_index = 0;
void Process_Key(unsigned char key) {
if (key >= '0' && key <= '9') {
// 数字输入
if (buffer_index < 15) {
input_buffer[buffer_index++] = key;
input_buffer[buffer_index] = '\0'; // 字符串结束符
}
} else if (key == '+' || key == '-' || key == '*' || key == '/') {
// 运算符输入
if (buffer_index > 0) { // 确保前面有数字
num1 = atof(input_buffer); // 将字符串转为浮点数
op = key;
input_buffer[buffer_index++] = op;
input_buffer[buffer_index] = '\0';
}
} else if (key == '=') {
// 等号,进行计算
if (buffer_index > 0 && op != 0) {
num2 = atof(input_buffer); // 获取第二个操作数
switch (op) {
case '+': result = num1 + num2; break;
case '-': result = num1 - num2; break;
case '*': result = num1 * num2; break;
case '/':
if (num2 != 0) result = num1 / num2;
else result = 0; // 防止除零
break;
}
// 显示结果
sprintf(input_buffer, "%.2f", result); // 格式化结果
buffer_index = strlen(input_buffer);
}
} else if (key == 'C') {
// 清除
num1 = 0; num2 = 0; result = 0; op = 0;
buffer_index = 0;
input_buffer[0] = '\0';
}
}
// 在main.c中调用
// char key = Key_Scan();
// if (key != 0) {
// Process_Key(key);
// Lcd_ShowString(0, 0, input_buffer); // 显示当前输入
// }
升级版 (支持优先级 - 使用双栈法):
这个版本更复杂,但功能更强大,它需要两个栈:一个存数字,一个存运算符。
// calculator.h
#ifndef __CALCULATOR_H__
#define __CALCULATOR_H__
void Calculate_Expression(char* expr);
#endif
// calculator.c
#include "calculator.h"
#include <string.h>
#include <stdlib.h>
#define MAX_STACK_SIZE 16
float num_stack[MAX_STACK_SIZE];
char op_stack[MAX_STACK_SIZE];
int num_top = -1;
int op_top = -1;
// 运算符优先级
char Precedence(char op) {
if (op == '+' || op == '-') return 1;
if (op == '*' || op == '/') return 2;
return 0;
}
// 执行一次计算
void Calculate() {
float b = num_stack[num_top--];
float a = num_stack[num_top--];
char op = op_stack[op_top--];
switch (op) {
case '+': num_stack[++num_top] = a + b; break;
case '-': num_stack[++num_top] = a - b; break;
case '*': num_stack[++num_top] = a * b; break;
case '/': num_stack[++num_top] = (b != 0) ? a / b : 0; break;
}
}
void Calculate_Expression(char* expr) {
num_top = -1;
op_top = -1;
for (int i = 0; expr[i] != '\0'; i++) {
if (expr[i] >= '0' && expr[i] <= '9') {
// 解析数字 (这里简化处理,只处理整数)
float num = 0;
while (expr[i] >= '0' && expr[i] <= '9') {
num = num * 10 + (expr[i] - '0');
i++;
}
i--; // 回退一位,因为for循环会i++
num_stack[++num_top] = num;
} else if (expr[i] == '+' || expr[i] == '-' || expr[i] == '*' || expr[i] == '/') {
// 处理运算符
while (op_top != -1 && Precedence(op_stack[op_top]) >= Precedence(expr[i])) {
Calculate();
}
op_stack[++op_top] = expr[i];
}
}
// 处理栈中剩余的运算符
while (op_top != -1) {
Calculate();
}
}
注意: 双栈法版本需要更完善的输入解析逻辑来处理多位数和小数点,这里为了简化只展示了核心框架。
调试与优化
-
Proteus 仿真调试:
- 在Keil中编写好代码,生成
.hex文件。 - 在Proteus中绘制好原理图,将生成的
.hex文件加载到单片机中。 - 点击运行,观察LCD1602的显示和按键响应。
- 常见问题:
- LCD不显示或乱码: 检查接线(RS, RW, EN, D0-D7)、对比度电位器调节、LCD初始化函数是否正确。
- 按键无响应: 检查键盘扫描逻辑、消抖延时是否足够、引脚定义是否正确。
- 计算结果错误: 检查
atof函数是否可用(某些51标准库不支持,需自行实现字符串转浮点数),或检查双栈法逻辑。
- 在Keil中编写好代码,生成
-
硬件焊接与调试:
- 将所有元件焊接洞洞板或PCB上。
- 焊接完成后,先不要上电,仔细检查有无短路、虚焊。
- 上电,先测试LCD是否能正常显示(例如显示 "Hello World")。
- 再测试键盘,每按一个键,串口打印出对应的值(如果使用串口调试)或通过LED灯指示。
- 最后整合测试计算功能。
-
功能扩展 (可选):
- 增加小数点支持: 在输入处理和计算逻辑中加入对 '.' 的判断。
- 增加正负号支持: 增加 键。
- 增加开方、百分比等高级功能。
- 使用串口进行调试: 将按键值和计算结果通过串口打印到电脑上,方便调试。
总结与报告撰写
课程设计完成后,需要撰写一份课程设计报告,内容应包括:
- 设计目的: 简述项目内容和目标。
- 方案论证与选择: 比较不同方案的优缺点,并说明最终选择的原因。
- 硬件设计: 详细说明各模块的选型、引脚连接图和原理图。
- 软件设计: 详细阐述程序流程、模块划分和核心算法(特别是计算逻辑部分)。
- 系统调试: 描述仿真和硬件调试过程中遇到的问题及解决方法。
- 结论与心得: 总结项目完成情况、个人收获和体会。
- 附录: 完整的C51源代码、Proteus仿真工程文件。
这个设计方案为你提供了一个清晰的路线图,从简单的开始,先实现能加减的版本,再逐步添加乘除和优先级,这样会更容易上手和调试,祝你项目顺利!