proteus仿真51单片机通过矩阵按键和数码管制作简单计算器

基于 51 单片机、4×4 矩阵键盘和两位共阴极数码管的简易整数 / 小数计算器,核心功能覆盖 "输入 - 运算 - 显示 - 错误处理 - 重置"

本篇分为三个模块

模块一 矩阵按键

模块二 数码管

模块三 计算器

首先是电路图如上

模块一 :矩阵按键

如果想看视频的话

[6-1] 矩阵键盘_哔哩哔哩_bilibili这个

我在这里就简单把原理讲一下。

在单片机项目里,按键是最常用的输入设备。如果我们用最基础的独立按键,1 个按键就要占用 1 个单片机 IO 口;如果项目需要 16 个按键,就要占用 16 个 IO 口 ------ 这对 IO 资源本就紧张的单片机来说,是极大的浪费。

有没有办法用更少的 IO 口,实现更多按键的检测?这就是我们今天要讲的矩阵键盘,它的核心价值,就是用极少的 IO 口,实现大量按键的检测。


一、矩阵键盘的硬件结构

矩阵键盘的核心设计,是把按键排成N 行 M 列的矩阵结构,我们以最常用的 4×4 矩阵键盘(16 个按键)为例,结合原理图给大家讲清楚:

  1. 我们把横向的 4 根线叫做行线 (图中 P17、P16、P15、P14),纵向的 4 根线叫做列线(图中 P13、P12、P11、P10)。
  2. 每一个按键,都接在一根行线和一根列线的交叉点上:按键未按下时,行线和列线完全断开;按键按下时,对应的行线和列线就会直接导通。

这样设计的好处一目了然:4 行 4 列共 16 个按键,我们只需要4+4=8 个 IO 口就能完成全部检测,比独立按键节省了一半的 IO 口;如果是 8×8 的 64 键矩阵,仅需 16 个 IO 口,按键数量越多,IO 口的节省效果越夸张。


二、矩阵键盘核心工作原理:逐行 / 逐列扫描法

矩阵键盘的核心,就是逐行(逐列)扫描 ,我们先讲最通用、最好理解的逐行扫描法

前置准备:IO 口模式配置

要实现扫描,我们先要给单片机 IO 口做固定配置,这是新手最容易忽略的关键点:

  • 行线:配置为推挽输出模式,可以主动输出高 / 低电平;

  • 列线:配置为上拉输入模式,默认状态下是高电平,只有被外部拉低时,才会变成低电平。

扫描的完整步骤(结合实例讲解)

我们以原理图中按键 5(第 2 行 P16、第 1 列 P13)按下为例,一步步拆解扫描的全过程,大家可以对着原理图同步理解。

第一步:预检测,快速判断有没有按键按下

先给所有行线,全部输出低电平,然后读取所有列线的电平:

  • 如果没有任何按键按下:所有列线都是上拉的高电平,我们读到的列值全是 1,直接结束本轮扫描,不用做后续操作,提高程序效率;

  • 如果有按键按下:按下的按键会把对应的行线(低电平)和列线导通,这根列线就会被拉成低电平,我们读到的列值就会出现 0,确认有按键按下,进入下一步定位。

第二步:逐行扫描,定位按键的精确位置

逐行扫描的核心逻辑:一次只拉低一根行线,其余所有行线全部输出高电平,然后依次读取每一列的电平。我们继续用按键 5 按下的场景演示:

  1. 先给第 1 行(P17)输出低电平,剩下的 P16、P15、P14 全部输出高电平,读取 4 根列线的电平:按键 5 在 P16 行,此时 P16 是高电平,就算按键按下,列线也不会被拉低,所以 4 根列线全是高电平,判定这一行没有按键按下。

  2. 接着给第 2 行(P16)输出低电平,剩下的 P17、P15、P14 全部输出高电平,再次读取列线电平:此时我们发现,第 1 列 P13 的电平变成了低电平,其他列还是高电平。这里我们就拿到了两个关键信息:输出低电平的行是 P16(第 2 行),读取到低电平的列是 P13(第 1 列)

  3. 有了唯一的行号和列号,我们就能 100% 确定:按下的按键,就是第 2 行第 1 列的按键 5!

  4. 后续我们继续给第 3 行(P15)、第 4 行(P14)依次输出低电平,读取列线,完成一轮完整的扫描。

第三步:循环扫描,实现 "全按键实时检测"

我们只要让单片机快速循环执行上面的逐行扫描过程,就能实现所有按键的实时检测。这里有个很关键的点:人的按键动作是很慢的(按下到松开至少几十毫秒),只要单片机的扫描频率足够快,就能在你按下按键的瞬间,完成扫描定位,给人的体感就是 "所有按键都能同时被检测"。

补充:逐列扫描法

逐列扫描和逐行扫描是完全对称的逻辑:我们把列线配置为输出模式,行线配置为上拉输入模式,一次只拉低一根列线,其余列线输出高电平,然后读取每一行的电平,通过 "低电平的列号 + 低电平的行号",定位按下的按键,原理和逐行扫描完全一致。

我们讲代码写一下

三.矩阵按键代码

cpp 复制代码
// ====================== 4x4矩阵键盘扫描函数 ======================
// 功能:扫描4x4矩阵键盘,返回按键值
// 返回值:按键编号(1-16),0表示无按键按下
// 键盘布局:
//   P1_7-P1_4为行线(输出),P1_3-P1_0为列线(输入)
//   按键映射:1-9为数字,10=+, 11=-, 12==, 13=撤回, 14=×, 15=÷, 16=C(清除)
unsigned char Key()
{
	unsigned char Number=0;  // 按键编号,初始化为0(无按键)
	
	// ====================== 扫描第一行(P1_7=0) ======================
	P1=0XFF;      // P1端口全部输出高电平
	P1_7=0;       // 第一行输出低电平
	// 检测列线状态,判断是否有按键按下
	if(P1_3==0){Delayms(20);if(P1_3==1){Delayms(20);Number=1;}}  // 按键1:第一行第四列
	if(P1_2==0){Delayms(20);if(P1_2==1){Delayms(20);Number=2;}}  // 按键2:第一行第三列
	if(P1_1==0){Delayms(20);if(P1_1==1){Delayms(20);Number=3;}}  // 按键3:第一行第二列
	if(P1_0==0){Delayms(20);if(P1_0==1){Delayms(20);Number=4;}}  // 按键4:第一行第一列
	
	// ====================== 扫描第二行(P1_6=0) ======================
	P1=0XFF;      // P1端口全部输出高电平
	P1_6=0;       // 第二行输出低电平
	if(P1_3==0){Delayms(20);if(P1_3==1){Delayms(20);Number=5;}}  // 按键5:第二行第四列
	if(P1_2==0){Delayms(20);if(P1_2==1){Delayms(20);Number=6;}}  // 按键6:第二行第三列
	if(P1_1==0){Delayms(20);if(P1_1==1){Delayms(20);Number=7;}}  // 按键7:第二行第二列
	if(P1_0==0){Delayms(20);if(P1_0==1){Delayms(20);Number=8;}}  // 按键8:第二行第一列
	
	// ====================== 扫描第三行(P1_5=0) ======================
	P1=0XFF;      // P1端口全部输出高电平
	P1_5=0;       // 第三行输出低电平
	if(P1_3==0){Delayms(20);if(P1_3==1){Delayms(20);Number=9;}}   // 按键9:第三行第四列
	if(P1_2==0){Delayms(20);if(P1_2==1){Delayms(20);Number=10;}}  // 按键10(+):第三行第三列
	if(P1_1==0){Delayms(20);if(P1_1==1){Delayms(20);Number=11;}}  // 按键11(-):第三行第二列
	if(P1_0==0){Delayms(20);if(P1_0==1){Delayms(20);Number=12;}}  // 按键12(=):第三行第一列
	
	// ====================== 扫描第四行(P1_4=0) ======================
	P1=0XFF;      // P1端口全部输出高电平
	P1_4=0;       // 第四行输出低电平
	if(P1_3==0){Delayms(20);if(P1_3==1){Delayms(20);Number=13;}}  // 按键13(撤回):第四行第四列
	if(P1_2==0){Delayms(20);if(P1_2==1){Delayms(20);Number=14;}}  // 按键14(×):第四行第三列
	if(P1_1==0){Delayms(20);if(P1_1==1){Delayms(20);Number=15;}}  // 按键15(÷):第四行第二列
	if(P1_0==0){Delayms(20);if(P1_0==1){Delayms(20);Number=16;}}  // 按键16(C):第四行第一列
	
	return Number;  // 返回按键编号
}

我们拿第一段进行讲解(注意 按键检测有更好的写法 这里是与江协保持一致)

  1. 准备工作:初始化端口状态
cpp 复制代码
P1=0XFF;      // P1端口全部输出高电平
  • 作用:这是扫描前的 "清零 / 复位" 操作。

    • 给 P1 口的所有 8 个引脚(P1.0 到 P1.7)全部写入高电平(1)

    • 目的是确保之前的扫描结果或残留电平不会干扰本次判断,让所有行线都回到 "未选中" 状态。

  1. 选中第一行:拉低行线
cpp 复制代码
P1_7=0;       // 第一行输出低电平
  • 作用 :拉低第一行的行线(假设你的硬件连接是:行线接 P1.7-P1.4,列线接 P1.3-P1.0)。

    • 这是 "逐行扫描" 的核心:一次只让一根行线为低电平(0),其他行线全为高电平(1)

    • 此时,如果第一行上有按键按下,那个按键就会把对应的列线 "短路" 到这个低电平上。


  1. 检测第一行的按键(以第一个 if 为例,其余同理)

我们以 "按键 1(第一行第四列)" 的检测代码为例进行拆解:

cpp 复制代码
// 检测列线 P1.3(第四列)
if(P1_3==0) {
    Delayms(20);          // 第1次延时
    if(P1_3==1) {         // 内部判断
        Delayms(20);      // 第2次延时
        Number=1;         // 赋值按键值
    }
}

第一层:初步触发电平检测

cpp 复制代码
if(P1_3==0)  // 判断列线 P1.3 是否为低电平
  • 作用:检查有没有按键把这根列线拉低。

    • 因为我们刚才把第一行(P1.7)设为 0 了,如果 "第一行第四列" 的按键被按下,P1.3(列线)就会通过按键被强行拉到 GND,变成 0

    • 如果读到 P1_3 == 0,说明可能有按键按下了(但也可能是干扰或抖动)。

第二层:软件消抖(关键步骤)

cpp 复制代码
Delayms(20);  // 延时 20 毫秒
  • 作用机械按键消抖

    • 机械按键按下时,金属片会发生物理弹跳,导致电平在 0 和 1 之间快速跳变几次(约 5-20ms)。如果不延时直接读,单片机会误以为你按了很多次。

    • 延时 20ms,就是为了等电平稳定下来。

第三层:逻辑陷阱(注意!这里有问题)

cpp 复制代码
if(P1_3==1)  // 再次判断列线 P1.3 是否为高电平
  • 代码字面意思 :延时结束后,检查 P1.3 是否变回了 1
cpp 复制代码
Delayms(20);  // 再延时 20 毫秒
Number=1;     // 将变量 Number 赋值为 1
  • 作用 :如果通过了上面的判断,就把变量 Number 设为 1,代表 "按键 1 被按下了"。

我们将其封装成函数 按键读取的值返回。

模块二 : 数码管

视频([4-1] 静态数码管显示_哔哩哔哩_bilibili


一、数码管的核心结构:8 个 LED 的组合

8 段数码管的本质,是8 个独立的发光二极管(LED)封装在一个器件里,通过控制不同 LED 的亮灭,组合出 0-9 的数字、小数点和少量简单字符。结合你图中的标注,8 个 LED 的定义如下:

  • 7 个长条形 LED 组成数字 "8" 的笔画,分别命名为A、B、C、D、E、F、G 段,对应数字的 7 个笔画;
  • 1 个圆形 LED 是DP 段,就是右下角的小数点;
  • 加起来正好 8 个段,因此叫 "8 段数码管"。
类型 公共端定义 点亮逻辑 原理图对应
共阳极数码管 所有 LED 的正极(阳极)全部连在一起,作为公共端(COM 端) 公共端接高电平(VCC/5V),对应段的引脚给低电平(0),LED 点亮;给高电平(1),LED 熄灭 完全匹配,图中 3、8 脚是连在一起的公共阳极,A~DP 是各 LED 的阴极
共阴极数码管 所有 LED 的负极(阴极)全部连在一起,作为公共端(COM 端) 公共端接低电平(GND),对应段的引脚给高电平(1),LED 点亮;给低电平(0),LED 熄灭 与原理图逻辑完全相反

共阳极:低电平点亮,高电平熄灭;共阴极:高电平点亮,低电平熄灭,两者的显示段码完全相反。

二 显示数字

要显示一个数字,本质就是让对应笔画的 LED 点亮,不需要的笔画熄灭 。举个例子:要显示数字0,需要 A、B、C、D、E、F 段点亮,G 段和 DP 小数点熄灭。

显示数字 亮灯段 8 位二进制(DP G F E D C B A) 16 进制段码
0 A、B、C、D、E、F 1100 0000 0xC0
1 B、C 1111 1001 0xF9
2 A、B、G、E、D 1010 0100 0xA4
3 A、B、G、C、D 1011 0000 0xB0
4 F、G、B、C 1001 1001 0x99
5 A、F、G、C、D 1001 0010 0x92
6 A、F、G、C、D、E 1000 0010 0x82
7 A、B、C 1111 1000 0xF8
8 A、B、C、D、E、F、G 1000 0000 0x80
9 A、B、C、D、F、G 1001 0000 0x90
全灭 1111 1111 0xFF
仅小数点 DP 0111 1111 0x7F

补充:共阴极数码管的段码,就是上表的二进制数按位取反,比如数字 0 的段码是 0x3F,数字 1 是 0x06。

显示数字 亮灯段 8 位二进制(DP G F E D C B A) 16 进制段码
0 A、B、C、D、E、F 0011 1111 0x3F
1 B、C 0000 0110 0x06
2 A、B、G、E、D 0101 1011 0x5B
3 A、B、G、C、D 0100 1111 0x4F
4 F、G、B、C 0110 0110 0x66
5 A、F、G、C、D 0110 1101 0x6D
6 A、F、G、C、D、E 0111 1101 0x7D
7 A、B、C 0000 0111 0x07
8 A、B、C、D、E、F、G 0111 1111 0x7F
9 A、B、C、D、F、G 0110 1111 0x6F
全灭 0000 0000 0x00
仅小数点 DP 1000 0000 0x80

我们仿真里面用的数码管

7SEG-MPX2-CC-BLUE 器件

型号字段 含义说明
7SEG 7 段数码管(实际带小数点 DP,共 8 个发光段,行业通用简称 7 段)
MPX2 Multiplex 2,即 **2 位一体、支持多路复用(动态扫描)** 的数码管
CC Common Cathode,共阴极类型
BLUE 数码管的发光颜色为蓝色
引脚分类 引脚名称 功能说明
位选脚(2 个) COM1(1 脚) 第 1 位(左位)数码管的公共阴极,拉低电平(0)时,该位被选通,允许点亮
COM2(2 脚) 第 2 位(右位)数码管的公共阴极,拉低电平(0)时,该位被选通,允许点亮
段选脚(8 个) a、b、c、d、e、f、g、dp 两个数码管共用的段选脚,高电平(1)时,对应段点亮(共阴极逻辑)

好了,那我们开始数码管的代码

三 代码片段

cpp 复制代码
// ====================== 两位数码管显示函数 ======================
// 功能:在指定位置显示数字,支持小数点
// 参数:
//   Location - 显示位置(1=十位,2=个位)
//   Digital  - 显示数字(0-9,10=E错误,11=熄灭)
//   Dot      - 小数点标志(1=点亮小数点,0=熄灭)
// 说明:使用共阴数码管,P0输出段码,P2控制位选
void Display(int Location, unsigned char Digital, bit Dot)
{
	// 共阴数码管段码表:0-9、E(0x79)、熄灭(0x00)
	// 每个段码对应数码管的a-g和dp段
	unsigned char Num[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x79,0x00};
	unsigned char seg_code = Num[Digital];  // 获取对应数字的段码
	
	// 点亮小数点(任意位有效,共阴:高电平点亮)
	if(Dot)
	{
		seg_code |= 0x80;  // 将最高位(小数点位)置1
	}
	
	// 位选控制(共阴:低电平点亮对应位)
	switch(Location)
	{
		case 1: P2_0=0; P2_1=1; break;  // 十位:P2_0=0选中
		case 2: P2_0=1; P2_1=0; break;  // 个位:P2_1=0选中
	}
	
	P0 = seg_code;  // 输出段码到数码管
	Delayms(1);     // 动态扫描延时,保持显示
	P0 = 0x00;      // 消隐,防止鬼影
}

模块三 :计算器(10以内的计算)

1.输入第一位数值,在第一位上面显示,然后输入运算符号,数码管无变化,再输入第二位数字

第一位数值,移到第二位,第一位显示第二个数值

2.按下等号 保留一位小数。

3有撤销功能,比如 第一个输入3 再输入+号,此时按撤销键后,可以再输入符号如 --- 号。

代码如下

cpp 复制代码
// ====================== 主函数:计算器逻辑 ======================
// 功能:实现两位数码管计算器,支持加减乘除运算
// 状态机设计:
//   state=0:输入第一个操作数num1
//   state=1:输入第二个操作数num2
//   state=2:显示运算结果
void main()
{
	unsigned char KeyNum;       // 当前按键值
	unsigned char num1 = 0;     // 第一个操作数(0-9)
	unsigned char num2 = 0;     // 第二个操作数(0-9)
	unsigned char op = 0;       // 运算符:0=无,1=+,2=-,3=×,4=÷
	float result = 0.0;         // 运算结果(浮点数,支持小数)
	unsigned char int_part = 0; // 结果的整数部分
	unsigned char dec_part = 0; // 结果的小数部分(保留1位)
	unsigned char state = 0;    // 状态机:0=输入num1,1=输入num2,2=显示结果
	unsigned char error_flag = 0;// 错误标志:1=负数/除零错误
	
	while(1)  // 主循环:持续扫描按键和更新显示
	{
		KeyNum = Key();  // 读取键盘按键值
		
		// ====================== 按键处理逻辑 ======================
		if(KeyNum != 0)  // 有按键按下
		{
			// 清除键(C):重置所有状态
			if(KeyNum == 16)
			{
				num1 = 0;        // 清除第一个操作数
				num2 = 0;        // 清除第二个操作数
				op = 0;          // 清除运算符
				result = 0.0;    // 清除结果
				int_part = 0;    // 清除整数部分
				dec_part = 0;    // 清除小数部分
				state = 0;       // 返回初始状态
				error_flag = 0;   // 清除错误标志
			}
			
			// ====================== 状态0:输入第一个操作数 num1 ======================
			else if(state == 0)
			{
				if(KeyNum >= 1 && KeyNum <= 9) num1 = KeyNum;  // 输入数字1-9
				else if(KeyNum == 13) num1 = 0;                // 输入数字0
				else if(KeyNum == 10) { op = 1; state = 1; }   // 输入+号,进入状态1
				else if(KeyNum == 11) { op = 2; state = 1; }   // 输入-号,进入状态1
				else if(KeyNum == 14) { op = 3; state = 1; }   // 输入×号,进入状态1
				else if(KeyNum == 15) { op = 4; state = 1; }   // 输入÷号,进入状态1
			}
			
			// ====================== 状态1:输入第二个操作数 num2 ======================
			else if(state == 1)
			{
				if(KeyNum==13)
				{
                   state=0;//返回上一个状态 (输入运算符号)
				}
				else if(KeyNum >= 1 && KeyNum <= 9) num2 = KeyNum;  // 输入数字1-9
				              
				else if(KeyNum == 12)  // 等号=:执行运算
				{
					error_flag = 0;  // 清除错误标志
					// 根据运算符执行相应运算
					switch(op)
					{
						case 1: result = num1 + num2; break;  // 加法
						case 2: if(num1 >= num2) result = num1 - num2; else error_flag = 1; break;  // 减法(不允许负数)
						case 3: result = num1 * num2; break;  // 乘法
						case 4: if(num2 == 0) error_flag = 1; else result = (float)num1 / num2; break;  // 除法(不允许除零)
					}
					
					// 拆分整数和小数部分(四舍五入保留1位小数)
					if(!error_flag)
					{
						int_part = (unsigned char)result;  // 提取整数部分
						dec_part = (unsigned char)((result - int_part) * 10 + 0.5);  // 提取小数部分并四舍五入
						if(dec_part >= 10) { dec_part = 0; int_part++; }  // 处理进位(如9.95→10.0)
					}
					state = 2;  // 进入结果显示状态
				}
			}
		}
		
		// ====================== 两位数码管显示逻辑 ======================
		if(state == 0)  // 状态0:显示第一个操作数
		{
			Display(1, 0, 0);      // 十位:显示0
			Display(2, num1, 0);   // 个位:显示num1(无小数点)
		}
		else if(state == 1)  // 状态1:显示第二个操作数
		{
			Display(1, num1, 0);      // 十位:显示0
			Display(2, num2, 0);   // 个位:显示num2(无小数点)
		}
		else if(state == 2)  // 状态2:显示运算结果
		{
			if(error_flag)  // 有错误:显示错误提示
			{
				Display(1, 10, 0);  // 十位:显示E(Error)
				Display(2, 0, 0);    // 个位:显示0
			}
			else  // 无错误:显示运算结果
			{
				if(dec_part == 0)  // 结果为整数
				{
					if(int_part < 10)  // 整数部分小于10:只显示个位
					{
						Display(1, 11, 0);      // 十位:熄灭
						Display(2, int_part, 0); // 个位:显示整数部分(无小数点)
					}
					else  // 整数部分大于等于10:显示两位
					{
						Display(1, int_part / 10, 0);  // 十位:显示十位数
						Display(2, int_part % 10, 0);  // 个位:显示个位数
					}
				}
				else  // 结果有小数
				{
					if(int_part < 10)  // 整数部分小于10:显示格式"X.X"
					{
						Display(1, int_part, 1);  // 十位:显示整数部分(带小数点)
						Display(2, dec_part, 0);  // 个位:显示小数部分(不带小数点)
					}
					else  // 整数部分大于等于10:显示格式"XX."(小数部分被截断)
					{
						Display(1, int_part / 10, 0);  // 十位:显示十位数
						Display(2, int_part % 10, 1);  // 个位:显示个位数(带小数点)
					}
				}
			}
		}
	}
}

好了

这就本次的内容

如果你喜欢,可以给我一个免费的赞和收藏,非常感谢。

相关推荐
17(无规则自律)4 小时前
【Linux驱动实战】:字符设备之ioctl与mutex全解析
linux·c语言·驱动开发·嵌入式硬件
电子工程师成长日记-C514 小时前
51单片机4乘4计算器
单片机·嵌入式硬件·51单片机
没有医保李先生5 小时前
esp32和stm32的工程宏定义
stm32·单片机·嵌入式硬件
szxinmai主板定制专家5 小时前
基于ZYNQ MPSOC船舶数据采集仪器设计(一)总体设计方案,包括振动、压力、温度、流量等参数
arm开发·人工智能·嵌入式硬件·fpga开发
2501_918126916 小时前
学习所有6502写游戏存档的语句
汇编·嵌入式硬件·学习·游戏·个人开发
普中科技6 小时前
【普中STM32F1xx开发攻略--标准库版】-- 第 38 章 RS485 通信实验
stm32·单片机·嵌入式硬件·开发板·通信·rs485·普中科技
DLGXY6 小时前
STM32(二十七)——独立看门狗&窗口看门狗
stm32·嵌入式硬件·算法
weixin_462901976 小时前
方案 3:手机控制 ESP32
单片机·嵌入式硬件
風清掦7 小时前
【江科大STM32学习笔记-09】USART串口协议 - 9.1 STM32 USART串口外设
笔记·stm32·单片机·嵌入式硬件·学习