天空星GD32F407开发板驱动4x4矩阵键盘实战引脚配置与扫描算法详解最近在用天空星的GD32F407开发板做个小项目需要接一个4x4的矩阵键盘。很多刚开始玩嵌入式的小伙伴一看到矩阵键盘就有点发怵感觉又是扫描又是算法的挺复杂。其实啊只要把原理搞清楚了代码写起来也就那么回事。今天我就手把手带你走一遍从硬件连接到代码编写再到实际调试保证你看完就能在自己的项目里用起来。咱们这篇教程的目标很明确让你学会如何在天空星GD32F407开发板上用最经典的“行列扫描”法来读取4x4矩阵键盘的按键值。我会先讲讲矩阵键盘到底是个啥为啥要用它然后重点放在GD32的GPIO配置和扫描算法的代码实现上最后再教你怎么把代码整合到自己的工程里验证效果。1. 矩阵键盘是啥为啥要用它咱们先解决第一个问题放着好好的独立按键不用为啥要用矩阵键盘想象一下如果你要做个计算器或者小键盘上面有16个按键。如果用独立按键每个按键都需要占用单片机的一个GPIO引脚那16个按键就得用掉16个引脚太浪费了GD32F407引脚虽然多但也不能这么挥霍啊。矩阵键盘的聪明之处就在于它用“行列交叉”的方式把16个按键组织成了4行4列。你只需要用4个引脚做“行线”4个引脚做“列线”总共8个引脚就能管理16个按键。引脚利用率直接翻了一倍。它的工作原理是这样的你把4根行线配置成输出4根列线配置成输入并且内部上拉。平时所有行线都输出高电平。当没有按键按下时由于列线被上拉到高电平你读到的列线状态全是“1”。一旦某个按键被按下比如第2行第3列的按键那么第2行和第3列就被这个按键“接通”了。这时候如果你让第2行输出低电平其他行保持高电平那么这条低电平就会通过被按下的按键传到第3列上导致你读取第3列的状态变成了“0”。单片机通过检测是哪一列变成了低电平再结合当前输出低电平的是哪一行就能唯一确定是哪个按键被按下了。这个过程就像在坐标纸上找点你先确定行X坐标再确定列Y坐标交点就是你要找的按键。2. 硬件连接与引脚规划工欲善其事必先利其器。在写代码之前咱们得先把硬件连好并确定用哪几个GPIO口。我手头这块4x4矩阵键盘模块有8个引脚分别标着C1, C2, C3, C4, R1, R2, R3, R4。这里的C就是Column列R就是Row行。按照前面讲的原理我们需要把R1-R4这4个行引脚接到单片机的4个GPIO上并配置为输出把C1-C4这4个列引脚接到另外4个GPIO上配置为输入。在天空星GD32F407开发板上GPIO资源很丰富我们可以自由选择。为了演示方便也为了和原始参考代码保持一致我这里直接使用代码里定义好的引脚。咱们先通过一个表格看清楚连接关系键盘引脚对应功能GD32F407引脚宏定义名称R1行1 (输出)PA2GPIO_IN1R2行2 (输出)PA0GPIO_IN2R3行3 (输出)PA4GPIO_IN3R4行4 (输出)PA6GPIO_IN4C1列1 (输入)PC4GPIO_IN5C2列2 (输入)PB0GPIO_IN6C3列3 (输入)PE7GPIO_IN7C4列4 (输入)PE9GPIO_IN8注意这个引脚分配不是唯一的你可以根据自己板子的布局和走线方便程度换成其他任意GPIO引脚只要在代码里相应修改宏定义即可。按照上表用杜邦线把键盘模块的8个引脚和开发板对应的8个GPIO口连接起来。连接时务必断电操作避免短路。3. 工程搭建与GPIO驱动代码编写硬件连好了接下来就是写代码了。咱们的代码主要分两部分一部分是硬件初始化配置GPIO的工作模式另一部分是核心的扫描算法。3.1 创建驱动文件与宏定义首先在你的工程里新建两个文件bsp_matrixkey.c和bsp_matrixkey.h。.h文件用来放引脚宏定义和函数声明.c文件用来实现具体函数。咱们先看头文件bsp_matrixkey.h怎么写。这里最关键的就是把上面表格里的引脚映射用宏定义的方式写清楚。#ifndef _BSP_MATRIXKEY_H_ #define _BSP_MATRIXKEY_H_ #include gd32f4xx.h // 行引脚的定义 (R1-R4, 配置为输出) #define RCU_IN1 RCU_GPIOA // 行1的时钟 #define PORT_IN1 GPIOA // 行1的端口 #define GPIO_IN1 GPIO_PIN_2 // 行1的引脚 #define RCU_IN2 RCU_GPIOA #define PORT_IN2 GPIOA #define GPIO_IN2 GPIO_PIN_0 #define RCU_IN3 RCU_GPIOA #define PORT_IN3 GPIOA #define GPIO_IN3 GPIO_PIN_4 #define RCU_IN4 RCU_GPIOA #define PORT_IN4 GPIOA #define GPIO_IN4 GPIO_PIN_6 // 列引脚的定义 (C1-C4, 配置为输入) #define RCU_IN5 RCU_GPIOC #define PORT_IN5 GPIOC #define GPIO_IN5 GPIO_PIN_4 #define RCU_IN6 RCU_GPIOB #define PORT_IN6 GPIOB #define GPIO_IN6 GPIO_PIN_0 #define RCU_IN7 RCU_GPIOE #define PORT_IN7 GPIOE #define GPIO_IN7 GPIO_PIN_7 #define RCU_IN8 RCU_GPIOE #define PORT_IN8 GPIOE #define GPIO_IN8 GPIO_PIN_9 // 函数声明 void MatrixKey_GPIO_Init(void); uint8_t key_scan(void); #endif代码解释RCU_INx宏对应GPIO端口的时钟使能宏比如RCU_GPIOA就是打开GPIOA的时钟。在GD32里用任何外设之前都必须先打开它的时钟。PORT_INx宏指定具体的GPIO端口如GPIOA。GPIO_INx宏指定端口下的具体引脚如GPIO_PIN_2。这样定义的好处是如果你想换引脚只需要修改这个头文件里的宏下面的.c文件完全不用动代码可移植性非常好。3.2 GPIO初始化函数详解头文件定义好了接下来在bsp_matrixkey.c里实现第一个关键函数MatrixKey_GPIO_Init。这个函数负责把8个GPIO引脚配置成正确的工作模式。#include bsp_matrixkey.h #include board.h // 假设你的工程里有这个头文件包含了系统初始化等 #include stdio.h // 为了方便扫描我们把行和列的端口、引脚信息分别放到数组里 // 行数组 uint32_t port_row[] {PORT_IN1, PORT_IN2, PORT_IN3, PORT_IN4}; uint32_t gpio_row[] {GPIO_IN1, GPIO_IN2, GPIO_IN3, GPIO_IN4}; // 列数组 uint32_t port_col[] {PORT_IN5, PORT_IN6, PORT_IN7, PORT_IN8}; uint32_t gpio_col[] {GPIO_IN5, GPIO_IN6, GPIO_IN7, GPIO_IN8}; void MatrixKey_GPIO_Init(void) { /* 第一步开启所有用到的GPIO端口的时钟 */ rcu_periph_clock_enable(RCU_IN1); // GPIOA rcu_periph_clock_enable(RCU_IN2); // GPIOA (已经开过再开一次也无妨) rcu_periph_clock_enable(RCU_IN3); // GPIOA rcu_periph_clock_enable(RCU_IN4); // GPIOA rcu_periph_clock_enable(RCU_IN5); // GPIOC rcu_periph_clock_enable(RCU_IN6); // GPIOB rcu_periph_clock_enable(RCU_IN7); // GPIOE rcu_periph_clock_enable(RCU_IN8); // GPIOE /* 第二步配置4个行引脚为输出模式 */ // 行引脚需要主动输出高低电平去扫描所以模式是 GPIO_MODE_OUTPUT // 上拉模式 GPIO_PUPD_PULLUP 可以让引脚默认输出高电平 // 输出类型 GPIO_OTYPE_PP 是推挽输出驱动能力强 // 输出速度 GPIO_OSPEED_50MHZ 设为高速其实控制键盘用2MHz也够这里按例程来 gpio_mode_set(PORT_IN1, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_IN1); gpio_output_options_set(PORT_IN1, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_IN1); gpio_mode_set(PORT_IN2, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_IN2); gpio_output_options_set(PORT_IN2, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_IN2); gpio_mode_set(PORT_IN3, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_IN3); gpio_output_options_set(PORT_IN3, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_IN3); gpio_mode_set(PORT_IN4, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_IN4); gpio_output_options_set(PORT_IN4, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, GPIO_IN4); /* 第三步配置4个列引脚为输入模式 */ // 列引脚用来读取状态所以模式是 GPIO_MODE_INPUT // 必须配置为上拉模式 GPIO_PUPD_PULLUP这是关键。 // 因为当没有按键按下时列线是悬空的上拉可以保证读到稳定的高电平。 // 当某一行输出低电平且该行某列的按键被按下时对应的列线就会被拉低我们就能读到0。 gpio_mode_set(PORT_IN5, GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, GPIO_IN5); gpio_mode_set(PORT_IN6, GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, GPIO_IN6); gpio_mode_set(PORT_IN7, GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, GPIO_IN7); gpio_mode_set(PORT_IN8, GPIO_MODE_INPUT, GPIO_PUPD_PULLUP, GPIO_IN8); }这个初始化函数我拆解成了三步每一步的作用都写在注释里了。这里有个坑我踩过就是列引脚的上拉电阻必须使能。如果不使能引脚悬空时电平不确定会读到乱跳的值导致误判按键按下。GD32的GPIO内部上拉电阻强度足够直接使能就行不需要外接电阻。3.3 核心扫描算法实现硬件初始化好了重头戏来了——扫描函数key_scan。这个函数会按照“逐行扫描”的方法检查16个按键中是否有被按下的并返回按键编号1-16。uint8_t key_scan(void) { uint8_t i, j, key_val 0; // 外层循环依次扫描4行 for (i 0; i 4; i) { // 1. 选中第i行将其设置为低电平有效电平 // 其他行保持高电平 gpio_bit_reset(port_row[i], gpio_row[i]); // 2. 短暂延时等待电平稳定非常重要 // 这里原文代码没有但实际必须加后面会讲为什么 delay_us(10); // 增加一个微秒级延时 // 3. 内层循环检查4列的状态 for (j 0; j 4; j) { // 读取第j列的电平如果为低电平0 if (!gpio_input_bit_get(port_col[j], gpio_col[j])) { // 计算按键编号行号*4 列号 1 // 因为数组从0开始按键编号我们希望从1开始 key_val i * 4 j 1; break; // 找到按键跳出列循环 } } // 4. 扫描完第i行后将该行恢复为高电平 gpio_bit_set(port_row[i], gpio_row[i]); // 5. 如果本轮扫描已经发现按键就跳出整个行循环不再扫描剩余行 if (key_val) break; } // 返回按键编号如果key_val为0表示没有按键按下 return key_val; }这个算法是矩阵键盘的经典实现我加了详细的注释。它的逻辑很清晰选中一行让当前扫描的行输出低电平其他行输出高电平。读取所有列检查每一列的电平。如果某一列是低电平说明当前低电平的行和这一列交叉点的按键被按下了。计算键值用公式行号 * 4 列号 1算出按键编号。比如第0行第0列i0 j0就是1号键第2行第3列i2 j3就是2*43112号键。恢复并退出把当前行恢复高电平如果找到按键就提前结束扫描。提示我在代码里加了一句delay_us(10)这是原文没有但实际非常重要的。因为GPIO电平从改变到稳定需要一点时间如果不加延时可能在电平还没稳定时就读取导致误判或读取不到。这个延时时间不用很长几个微秒到几十微秒都可以根据你的主频调整。4. 在主程序中测试与验证驱动代码写好了最后一步就是在主函数里调用它看看效果。#include board.h #include bsp_matrixkey.h #include stdio.h // 为了使用printf int main(void) { uint8_t key_value 0; // 开发板基础初始化系统时钟、延时函数等 board_init(); // 串口初始化用于打印信息需要你事先实现bsp_uart_init bsp_uart_init(); printf(Matrix Keyboard Test Start!\r\n); // 初始化矩阵键盘的GPIO MatrixKey_GPIO_Init(); while(1) { // 循环扫描按键 key_value key_scan(); // 如果扫描结果不是0说明有按键按下 if( key_value ! 0 ) { printf(Key Pressed: %d\r\n, key_value); } // 延时一段时间再扫描防止过于频繁 delay_ms(100); // 延时100ms可以根据需要调整 } }把代码编译下载到天空星开发板打开串口助手。当你依次按下键盘上的16个按键时应该能看到串口依次打印出1到16的数字。调试中可能遇到的问题按下没反应首先检查杜邦线是否接牢引脚定义是否和代码一致。然后用万用表蜂鸣档测一下按键按下时对应的行和列是否真的导通了。同时打印多个键值或键值错乱很可能是扫描函数里缺少我上面提到的delay_us(10)稳定延时。加上它再试试。某个键一直显示被按下检查对应的列引脚的上拉是否使能。或者硬件上该列引脚是否对地短路了。到这里整个4x4矩阵键盘在GD32F407上的驱动就完成了。其实核心思想就是“行列扫描”理解了这一点不管是用在GD32、STM32还是其他任何单片机上思路都是一样的只是GPIO操作的库函数不同而已。希望这篇教程能帮你把这个知识点彻底搞明白下次用到时就能信手拈来了。