一、项目演示目标

本工程基于 STM32F103C8T6 单片机,搭配 4×4 矩阵键盘、I2C OLED 屏幕实现按键识别显示功能。整体工作流程: 按下矩阵键盘任意按键 → 对应行列触点导通 → 单片机逐列拉低电平、读取行电平状态 → 判定按键行列位置 → 20ms 软件消抖过滤机械抖动 → 换算 1~16 按键编号 → OLED 实时打印按键编号、行号、列号。

简单概括:4×4 键盘将 16 个按键排布为 4 行 4 列,单片机逐列输出检测电平,通过行引脚电平判断按下的交叉按键。

视频讲解:STM32F103C8T6驱动4×4矩阵键盘+OLED完整原理讲解(附故障排查)https://www.bilibili.com/video/BV1eLg56REWY/?share_source=copy_web&vd_source=91b967b41a17a5686913521545a8c3fb

二、硬件接线说明

2.1 4×4 矩阵键盘 GPIO 接线

键盘共 8 根引脚,分为 4 列输出线、4 行检测输入线,硬件分配如下:

矩阵键盘引脚STM32 引脚GPIO 模式功能说明
COL1PC13开漏输出1 号列扫描线
COL2PC14开漏输出2 号列扫描线
COL3PC15开漏输出3 号列扫描线
COL4PA0开漏输出4 号列扫描线
ROW1PA1内部上拉输入第一行电平检测
ROW2PA2内部上拉输入第二行电平检测
ROW3PA3内部上拉输入第三行电平检测
ROW4PA4内部上拉输入第四行电平检测

补充说明:矩阵键盘仅机械触点,无需单独接 3.3V、GND,仅连接 8 根行列引脚即可。

2.2 I2C OLED 屏幕接线

采用软件模拟 I2C 驱动 OLED,引脚对应关系:

OLED 模块引脚STM32 引脚功能
SCLPB8I2C 时钟信号线
SDAPB9I2C 数据信号线
VCC3.3V屏幕供电(以模块丝印为准)
GNDGND单片机与屏幕共地

2.3 PC14/PC15 引脚使用重要注意点

PC14、PC15 是 STM32 内置低速外部晶振 LSE 专用引脚,本项目将其复用为普通 GPIO。 使用限制:工程内必须关闭 32.768KHz 外部低速晶振,二者功能不能同时启用。

多数 F103C8T6 开发板默认焊接了 32.768K 晶振与匹配电容,会干扰 PC14、PC15 的按键扫描。若对应两列按键失灵、识别错乱,优先排查板载 LSE 晶振电路。

三、4×4 矩阵键盘底层工作原理

键盘内部由 4 根行线、4 根列线交叉组成,每一个按键都位于行列交叉点,按下按键即可连通对应行、列导线。

3.1 无按键按下状态

行引脚配置内部上拉输入,无按键按压时,ROW1~ROW4 全部稳定输出高电平。 原理:行引脚通过单片机内部上拉电阻接 3.3V,按键触点断开,行列无导通,引脚不会悬空乱跳。

3.2 按键按下状态

程序单次只拉低 1 路列线,当该列存在按键按下时,低电平会通过机械触点传导至对应行引脚,单片机读取到低电平,即可锁定按键所在行列。

3.3 矩阵键盘优势

16 个独立按键若分开接线,需要占用 16 个单片机输入引脚;4 行 4 列矩阵结构仅占用 4 输出 + 4 输入共 8 个 GPIO,大幅节省引脚资源。

四、GPIO 配置逻辑与逐列扫描流程

4.1 列线配置开漏输出原因

所有 COL 列引脚设置为开漏输出模式

  • 输出高电平:引脚高阻释放,无驱动电平;
  • 输出低电平:引脚主动拉低,输出检测信号。

扫描逻辑:同一时刻仅拉低 1 列,剩余全部释放,两大优势:

  1. 多键同时按下时,避免高低电平直接对冲短路;
  2. PC13~PC15 引脚驱动能力较弱,2MHz 低速开漏模式完全满足按键扫描速率需求。

4.2 行线配置上拉输入原因

ROW 行引脚统一开启内部上拉输入:

  1. 未按键时,上拉电阻固定拉高电平,杜绝引脚悬空带来的电平随机跳变、误触发;
  2. 按键按下后,低电平列线可直接拉低对应行引脚,电平变化清晰易识别。

4.3 完整逐列扫描流程

  1. 先释放全部列线,所有列置为高阻态;
  2. 依次拉低第 1~4 列,每拉低一列后延时 5μs 等待电平稳定;
  3. 读取 4 路行引脚电平,若检测到低电平,记录当前行号、列号;
  4. 未检测到低电平则切换下一列重复检测;
  5. 遍历全部列后释放所有列线,返回原始扫描行列数据。

4.4 工程配套驱动函数说明

驱动代码分层封装,各文件核心函数功能:

文件路径函数名功能作用
KEY4_4.cKEY4_4_Init()初始化行列 GPIO、设置引脚初始电平
同上KEY4_4_ReleaseAllColumns()释放全部列引脚,全部置高阻
同上KEY4_4_DriveColumnLow()指定某一列引脚拉低输出
同上KEY4_4_ReadRaw()单次原始键盘扫描,无消抖
同上KEY4_4_Scan()带软件消抖的稳定按键扫描
同上KEY4_4_GetRow()根据按键编号换算对应行号
同上KEY4_4_GetColumn()根据按键编号换算对应列号
main.cKEY4_4_ShowState()将按键行列、编号数据刷新至 OLED

main.c代码

/* 4 x 4 matrix keyboard teaching demonstration for STM32F103C8T6. */
#include "delay.h"
#include "sys.h"
#include "oled.h"
#include "KEY4_4.h"

#define KEY_POLL_INTERVAL_MS  10U

static void KEY4_4_ShowState(u8 key_code);

int main(void)
{
	u8 key_code;
	u8 last_key_code = 0xFFU;

	delay_init();
	NVIC_Configuration();
	OLED_Init();
	KEY4_4_Init();
	OLED_ColorTurn(0);
	OLED_DisplayTurn(0);

	while(1)
	{
		key_code = KEY4_4_Scan();

		/* Refresh only on a state change to avoid visible OLED flicker. */
		if(key_code != last_key_code)
		{
			KEY4_4_ShowState(key_code);
			last_key_code = key_code;
		}

		delay_ms(KEY_POLL_INTERVAL_MS);
	}
}

static void KEY4_4_ShowState(u8 key_code)
{
	u8 key_text[] = "Key No.: --";
	u8 row_text[] = "Row    : -";
	u8 column_text[] = "Column : -";

	OLED_Clear();
	OLED_ShowString(0, 0, (u8 *)"4x4 KEY 1-16", 16);

	if(key_code == KEY4_4_KEY_NONE)
	{
		OLED_ShowString(0, 16, (u8 *)"State: READY", 12);
	}
	else
	{
		if(key_code < 10U)
		{
			key_text[9] = (u8)('0' + key_code);
			key_text[10] = '\0';
		}
		else
		{
			key_text[9] = (u8)('0' + key_code / 10U);
			key_text[10] = (u8)('0' + key_code % 10U);
		}
		row_text[9] = (u8)('0' + KEY4_4_GetRow(key_code));
		column_text[9] = (u8)('0' + KEY4_4_GetColumn(key_code));
		OLED_ShowString(0, 16, (u8 *)"State: PRESSED", 12);
	}

	OLED_ShowString(0, 28, key_text, 12);
	OLED_ShowString(0, 40, row_text, 12);
	OLED_ShowString(0, 52, column_text, 12);
	OLED_Refresh();
}

KEY4_4.c代码

#include "KEY4_4.h"
#include "delay.h"

#define KEY4_4_COL_PC_PINS  (GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15)
#define KEY4_4_ROW_PA_PINS  (GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4)
#define KEY4_4_SETTLE_US     5U

static const u8 KEY4_4_KeyMap[4][4] =
{
	{'1', '2', '3', 'A'},
	{'4', '5', '6', 'B'},
	{'7', '8', '9', 'C'},
	{'*', '0', '#', 'D'}
};

static u8 KEY4_4_LastSample = KEY4_4_KEY_NONE;
static u8 KEY4_4_StableKey = KEY4_4_KEY_NONE;
static u8 KEY4_4_SameSampleCount = 0U;

static void KEY4_4_ReleaseAllColumns(void);
static void KEY4_4_DriveColumnLow(u8 column);
static u8 KEY4_4_ReadRaw(void);

void KEY4_4_Init(void)
{
	GPIO_InitTypeDef GPIO_InitStructure;

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |
						   RCC_APB2Periph_GPIOC, ENABLE);

	/*
	 * 在切换 GPIO 模式前先释放所有开漏列线,防止产生瞬间低电平。
	 * PC13~PC15 的驱动能力有限,矩阵键盘扫描使用 2 MHz 已足够。
	 */
	GPIO_SetBits(GPIOC, KEY4_4_COL_PC_PINS);
	GPIO_SetBits(GPIOA, GPIO_Pin_0);

	GPIO_InitStructure.GPIO_Pin = KEY4_4_COL_PC_PINS;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_2MHz;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
	GPIO_Init(GPIOC, &GPIO_InitStructure);

	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

	/* 行线使用内部上拉;按键按下后,对应行线会连接到当前拉低的列线。 */
	GPIO_InitStructure.GPIO_Pin = KEY4_4_ROW_PA_PINS;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_Init(GPIOA, &GPIO_InitStructure);

	KEY4_4_ReleaseAllColumns();
	KEY4_4_LastSample = KEY4_4_KEY_NONE;
	KEY4_4_StableKey = KEY4_4_KEY_NONE;
	KEY4_4_SameSampleCount = 0U;
}

u8 KEY4_4_Scan(void)
{
	u8 sample;

	sample = KEY4_4_ReadRaw();

	/* 以 10 ms 为周期连续获得两次相同结果,实现约 20 ms 的按键消抖。 */
	if(sample == KEY4_4_LastSample)
	{
		if(KEY4_4_SameSampleCount < KEY4_4_DEBOUNCE_SAMPLES)
		{
			KEY4_4_SameSampleCount++;
		}
	}
	else
	{
		KEY4_4_LastSample = sample;
		KEY4_4_SameSampleCount = 1U;
	}

	if(KEY4_4_SameSampleCount >= KEY4_4_DEBOUNCE_SAMPLES)
	{
		KEY4_4_StableKey = KEY4_4_LastSample;
	}

	return KEY4_4_StableKey;
}

u8 KEY4_4_GetKeyChar(u8 key_code)
{
	if((key_code < 1U) || (key_code > 16U))
	{
		return 0U;
	}

	return KEY4_4_KeyMap[(key_code - 1U) / 4U][(key_code - 1U) % 4U];
}

u8 KEY4_4_GetRow(u8 key_code)
{
	if((key_code < 1U) || (key_code > 16U))
	{
		return 0U;
	}

	return (u8)(((key_code - 1U) / 4U) + 1U);
}

u8 KEY4_4_GetColumn(u8 key_code)
{
	if((key_code < 1U) || (key_code > 16U))
	{
		return 0U;
	}

	return (u8)(((key_code - 1U) % 4U) + 1U);
}

static void KEY4_4_ReleaseAllColumns(void)
{
	GPIO_SetBits(GPIOC, KEY4_4_COL_PC_PINS);
	GPIO_SetBits(GPIOA, GPIO_Pin_0);
}

static void KEY4_4_DriveColumnLow(u8 column)
{
	KEY4_4_ReleaseAllColumns();

	/*
	 * 本键盘正面从左到右对应 COL4、COL3、COL2、COL1。
	 * 因此按照 PA0、PC15、PC14、PC13 的顺序扫描,使 OLED 显示的
	 * 位置编号保持为第一排 1~4、第二排 5~8、第三排 9~12、
	 * 第四排 13~16。
	 */
	switch(column)
	{
		case 0U:
			GPIO_ResetBits(GPIOA, GPIO_Pin_0);
			break;

		case 1U:
			GPIO_ResetBits(GPIOC, GPIO_Pin_15);
			break;

		case 2U:
			GPIO_ResetBits(GPIOC, GPIO_Pin_14);
			break;

		default:
			GPIO_ResetBits(GPIOC, GPIO_Pin_13);
			break;
	}

	delay_us(KEY4_4_SETTLE_US);
}

static u8 KEY4_4_ReadRaw(void)
{
	u8 column;
	u8 row;
	u8 key_code = KEY4_4_KEY_NONE;
	static const u16 RowPins[4] =
	{
		GPIO_Pin_1, GPIO_Pin_2, GPIO_Pin_3, GPIO_Pin_4
	};

	for(column = 0U; column < 4U; column++)
	{
		KEY4_4_DriveColumnLow(column);

		for(row = 0U; row < 4U; row++)
		{
			if(GPIO_ReadInputDataBit(GPIOA, RowPins[row]) == Bit_RESET)
			{
				key_code = (u8)(row * 4U + column + 1U);
				break;
			}
		}

		if(key_code != KEY4_4_KEY_NONE)
		{
			break;
		}
	}

	KEY4_4_ReleaseAllColumns();
	return key_code;
}

KEY4_4.h代码

#ifndef __KEY4_4_H
#define __KEY4_4_H

#include "sys.h"

/*
 * 4×4 矩阵键盘与 STM32F103C8T6 的接线:
 *
 *   COL1 -> PC13       ROW1 -> PA1
 *   COL2 -> PC14       ROW2 -> PA2
 *   COL3 -> PC15       ROW3 -> PA3
 *   COL4 -> PA0        ROW4 -> PA4
 *
 * 本模块正面从左到右的实际列顺序为 COL4、COL3、COL2、COL1,
 * 软件已按照正面按键位置重新排序,并使用 1~16 作为位置编号:
 *
 *          第1列  第2列  第3列  第4列
 *   第1行     1      2      3      4
 *   第2行     5      6      7      8
 *   第3行     9     10     11     12
 *   第4行    13     14     15     16
 *
 * 对应键盘面板字符为:
 *   1  2  3  A
 *   4  5  6  B
 *   7  8  9  C
 *   *  0  #  D
 */

#define KEY4_4_KEY_NONE          0U
#define KEY4_4_DEBOUNCE_SAMPLES  2U

void KEY4_4_Init(void);

/*
 * 每隔 10 ms 调用一次,返回经过消抖处理的按键位置编号。
 * 返回 0 表示没有按键,返回 1~16 表示从左到右、从上到下的位置。
 */
u8 KEY4_4_Scan(void);

/* 将位置编号 1~16 转换为面板字符:0~9、A~D、* 或 #。 */
u8 KEY4_4_GetKeyChar(u8 key_code);

/* 获取按键所在的行号或列号(1~4);编号无效时返回 0。 */
u8 KEY4_4_GetRow(u8 key_code);
u8 KEY4_4_GetColumn(u8 key_code);

#endif

五、列序调整逻辑与 1~16 按键编号规则

5.1 为什么需要反转扫描列序

市面上通用 4×4 键盘模块引脚和面板显示顺序相反: 键盘面板从左至右物理列:COL4、COL3、COL2、COL1,对应引脚 PA0、PC15、PC14、PC13。

如果直接按照 COL1→COL4 顺序扫描,第一行按键识别顺序错乱,显示为 A、3、2、1。 本程序修正扫描顺序为:PA0 → PC15 → PC14 → PC13,保证面板从左到右、从上到下编号连续有序。

5.2 按键编号换算公式

按键编号范围 1~16,行、列变量从 0 开始计数,计算公式: key_code = row * 4 + column + 1

按键排布对照表:

面板第 1 列面板第 2 列面板第 3 列面板第 4 列
第 1 行 PA11234
第 2 行 PA25678
第 3 行 PA39101112
第 4 行 PA413141516

计算示例:

  1. 第 1 行第 1 列:0×4+0+1=1
  2. 第 2 行第 3 列:1×4+2+1=7
  3. 第 4 行第 4 列:3×4+3+1=16

六、软件消抖实现原理

6.1 按键抖动现象

机械按键金属触点按下 / 松开瞬间,金属片会短时间反复通断,产生几毫秒的电平抖动。若不做消抖处理,单次按键会被单片机识别成多次连续触发。

6.2 本项目非阻塞 20ms 消抖方案

主循环每 10ms 执行一次键盘扫描,消抖逻辑:

  1. 读取本次原始键值,和上一次稳定采样值对比;
  2. 数值不一致:重置计数,保存新采样值;
  3. 数值一致:连续采样计数 + 1;
  4. 连续 2 次采样数值完全相同时,判定为稳定按键,更新有效键值。

总消抖时长:10ms × 2 次采样 = 20ms。 优势:全程无阻塞延时,单片机可以同步运行 OLED 刷新、其他外设逻辑,属于非阻塞消抖,不占用系统运行时间。

七、OLED 屏幕显示内容说明

7.1 无按键按下待机界面

4x4 KEY 1-16
State: READY
Key No.: --
Row    : -
Column : -

7.2 检测到有效按键界面(示例:4 行 1 列,编号 13)

4x4 KEY 1-16
State: PRESSED
Key No.: 13
Row    : 4
Column : 1

界面参数释义:

  1. State: READY:当前无稳定按键按下;
  2. State: PRESSED:检测到经过消抖确认的有效按键;
  3. Key No.:按键 1~16 位置编号;
  4. Row:按键所在物理行(1~4);
  5. Column:按键面板列号(1~4)。

优化设计:仅在按键状态发生变化时刷新屏幕,避免循环内频繁清屏刷屏,消除画面闪烁问题。

八、项目使用限制 + 全套故障排查表

8.1 功能使用限制

本程序面向入门教学,仅支持单键识别:扫描过程检测到第一个按键后直接返回,多键同时按压仅识别扫描顺序靠前的按键。

矩阵键盘多键按下会产生 “鬼键” 误识别现象,若需要组合按键功能,硬件需在每个按键串联二极管,同时修改多键扫描算法,本教学工程不支持组合键。

8.2 常见故障现象、原因、排查方案

故障现象故障原因检查处理方法
全部按键无任何响应行列接线错误、驱动未初始化核对 8 根 GPIO 接线,确认 main 函数中调用 KEY4_4_Init () 初始化
第一行按键识别顺序错乱(A、3、2、1)键盘面板列序与引脚标号反向确认程序扫描顺序为 PA0、PC15、PC14、PC13
完整一整行按键全部无响应对应行 ROW 引脚接线断路检查 PA1~PA4 对应行线接线是否松动
完整一整列按键全部无响应对应列 COL 引脚接线断路检查 PA0、PC13/14/15 列线接线
PC14、PC15 对应列按键识别异常开发板板载 32.768K LSE 晶振干扰工程关闭 LSE 低速晶振,检查板载晶振电容电路
未按按键屏幕随机弹出按键值行引脚悬空、接触不良确认行引脚配置为内部上拉输入模式 IPU
某一个按键持续显示按下状态按键卡住、行列引脚短路断电后万用表测量按键触点是否短路粘连
OLED 屏幕完全无显示屏幕供电异常、I2C 接线错误核对 SCL 接 PB8、SDA 接 PB9,单片机与屏幕共地,确认 3.3V 供电正常
Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐