按键扫描------用74HC595和74HC165扩展GPIO实现矩阵键盘
在上一篇文章中,我们成功地将LCD驱动集成到了NumWorks的Ion层,实现了显示输出。接下来,我们需要解决输入问题------如何让ESP32-S3读取NumWorks的按键。
NumWorks原版采用按键矩阵 设计,直接使用MCU的GPIO连接行线和列线。对于一个典型的8行×8列的矩阵,需要16个GPIO引脚。ESP32-S3虽然GPIO数量不少,但考虑到其他外设(如PSRAM、LCD、USB、SD卡等)的占用,直接使用16个引脚会显得捉襟见肘。为了节省宝贵的GPIO,我们引入串行扩展芯片 :用74HC595 作为列驱动(输出),用74HC165 作为行读取(输入),并且让它们共用时钟引脚,只需少量GPIO即可驱动整个矩阵。
本文将详细介绍这种硬件设计方案、电路连接、以及如何在ESP32-S3上实现按键扫描,并最终将其适配到NumWorks的Ion::Keyboard接口中。
1. 为什么需要串行扩展?
1.1 原版NumWorks的按键矩阵
NumWorks的键盘是一个典型的矩阵结构,例如:
| Col0 | Col1 | Col2 | ... | |
|---|---|---|---|---|
| Row0 | A | B | C | ... |
| Row1 | D | E | F | ... |
| ... | ... | ... | ... | ... |
扫描原理:MCU将某一列拉低(输出低电平),然后读取所有行的电平。如果某行被拉低,说明该行与当前列的交点处有按键按下。如此循环扫描所有列,即可检测所有按键。
原版设计中,列线和行线都直接连接到MCU的GPIO。对于8×8的矩阵,需要16个GPIO。
1.2 ESP32-S3的引脚压力
ESP32-S3虽然有40多个GPIO,但许多引脚有特殊功能或已被占用:
- LCD I8080并口:至少占用8条数据线 + WR、DC、CS、RST等,约12~15个引脚。
- PSRAM:通常占用专用引脚(如GPIO 26-32)。
- USB、SD卡、调试串口等也会占用一些引脚。
如果直接使用16个GPIO做键盘,几乎不可能。因此,我们需要使用串行扩展芯片来大幅减少引脚占用。
2. 硬件设计方案:74HC595 + 74HC165
2.1 芯片简介
| 芯片 | 功能 | 特点 |
|---|---|---|
| 74HC595 | 8位串行输入/并行输出移位寄存器 | 用于输出,可级联,带有输出锁存器 |
| 74HC165 | 8位并行输入/串行输出移位寄存器 | 用于输入,可级联 |
2.2 工作原理
我们设计一个共用时钟、独立数据线的方案:
- 时钟线(SCK):74HC595和74HC165共用同一个时钟引脚(例如GPIO 4)。
- 数据线 :
- 74HC595的数据输入(DS):用于向595写入列扫描数据(哪个列被拉低)。
- 74HC165的串行输出(Q7):用于从165读取当前行的状态。
- 锁存/加载引脚 :
- 74HC595的锁存(RCLK):当一帧数据移位完成后,拉高锁存引脚,将数据并行输出到列引脚。
- 74HC165的并行加载(PL):低电平时将当前行电平锁存到内部寄存器,然后可以通过时钟移位读出。
这样,总共只需要4个GPIO(SCK、595_DS、165_Q7、595_RCLK/165_PL,若共用则5个,但通常将RCLK和PL分开,以便独立控制)。
2.3 电路连接示意图
假设我们使用4个74HC595级联(用于16列)和4个74HC165级联(用于16行),但NumWorks通常是8列×8行,所以我们各用1片即可(可扩展性依然保留)。下面以8×8矩阵为例:
text
scss
ESP32-S3 74HC595 (列驱动) 74HC165 (行读取)
GPIO4 (SCK) ---> SCK (pin 11) ---> SCK (pin 2) [共用时钟]
GPIO5 (DS) ---> DS (pin 14) - [不连接]
GPIO6 (Q7) --- [不连接] <--- Q7 (pin 9) (串行输出)
GPIO7 (RCLK) ---> RCLK (pin 12) - [不连接]
GPIO8 (PL) --- [不连接] ---> PL (pin 1) (并行加载)
矩阵连接:
595并行输出 QA~QH (pin15,1-7) 接矩阵的列线0~7
165并行输入 A~H (pin11-4,3,10) 接矩阵的行线0~7
注:如果595和165的时钟极性要求相同(通常都是上升沿移位),共用时钟没有问题。但需注意595的锁存(RCLK)和165的加载(PL)是独立控制的,因此需要两个额外的GPIO,总共4+2=6个。如果你希望进一步节省引脚,可以将RCLK和PL也共用一个引脚,但时序上要小心处理(先完成移位,然后同时锁存和加载,可能会互相干扰)。建议保留独立控制以确保稳定性。
2.4 节省引脚的效果
使用上述方案,驱动8×8键盘仅需:
- SCK : 1
- 595_DS : 1
- 165_Q7 : 1
- 595_RCLK: 1
- 165_PL : 1 总共5个GPIO(如果RCLK和PL可合并则只需4个),相比16个GPIO节省了11个!
3. 按键扫描软件实现
3.1 扫描原理
扫描过程分为两步:
- 输出列扫描数据:通过74HC595将某一列拉低(其余列高电平)。由于595是并行输出,我们只需在软件中生成一个8位数据,其中只有一位为0(表示拉低),其余为1。
- 读取行状态:通过74HC165读取8行的电平。如果某行被拉低,说明该行对应的按键被按下。
为了提高抗干扰能力,通常会在每个扫描周期内读取多次并去抖动。
3.2 ESP-IDF中的GPIO控制
我们使用ESP-IDF的GPIO驱动来控制上述引脚。需要将SCK、DS、RCLK、PL配置为推挽输出,Q7配置为输入(可能带上拉)。
3.3 关键代码片段
以下是一个简化的按键扫描函数示例(假设有5个控制引脚):
c
ini
#include "driver/gpio.h"
// 引脚定义(需与硬件一致)
#define PIN_SCK 4
#define PIN_595_DS 5
#define PIN_165_Q7 6
#define PIN_595_RCLK 7
#define PIN_165_PL 8
// 初始化GPIO
void keypad_init(void) {
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL<<PIN_SCK) | (1ULL<<PIN_595_DS) |
(1ULL<<PIN_595_RCLK) | (1ULL<<PIN_165_PL),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
gpio_config(&io_conf);
io_conf.pin_bit_mask = (1ULL<<PIN_165_Q7);
io_conf.mode = GPIO_MODE_INPUT;
io_conf.pull_up_en = GPIO_PULLUP_ENABLE; // 内部上拉,确保未按下时高电平
gpio_config(&io_conf);
}
// 向595写入一个字节(列数据)
void shift_out_595(uint8_t col_data) {
for (int i = 0; i < 8; i++) {
gpio_set_level(PIN_SCK, 0);
gpio_set_level(PIN_595_DS, (col_data >> (7-i)) & 1); // 高位先出
gpio_set_level(PIN_SCK, 1); // 上升沿移位
}
}
// 从165读取一个字节(行状态)
uint8_t shift_in_165(void) {
uint8_t row_data = 0;
for (int i = 0; i < 8; i++) {
gpio_set_level(PIN_SCK, 0);
row_data = (row_data << 1) | gpio_get_level(PIN_165_Q7); // 读取Q7(高位先出)
gpio_set_level(PIN_SCK, 1); // 上升沿移位
}
return row_data;
}
// 按键扫描:获取当前所有按键状态(矩阵形式)
void keypad_scan(uint8_t key_state[8][8]) {
// 先拉低PL,锁存当前行电平到165内部
gpio_set_level(PIN_165_PL, 0);
gpio_set_level(PIN_165_PL, 1); // 上升沿锁存(实际是低有效,拉高后保持)
for (int col = 0; col < 8; col++) {
// 准备列数据:只有当前列为0,其余为1
uint8_t col_data = ~(1 << col); // 注意:595输出高电平有效?需要看实际电路。通常595输出高电平驱动列线,所以拉低一列应输出0,其余输出1。
shift_out_595(col_data);
// 锁存到595输出(RCLK上升沿)
gpio_set_level(PIN_595_RCLK, 0);
gpio_set_level(PIN_595_RCLK, 1);
// 延时一小段时间,等待信号稳定(可选)
esp_rom_delay_us(5);
// 读取当前行的状态
uint8_t row_data = shift_in_165();
// 将行数据存入状态矩阵(row_data的每一位对应一行)
for (int row = 0; row < 8; row++) {
key_state[row][col] = (row_data >> row) & 1 ? 0 : 1; // 假设低电平表示按下
}
}
}
注意:实际电平极性取决于硬件连接。如果595输出高电平导通列线,那么拉低一列应输出0;如果行线上拉了电阻,按下时行线被拉低,那么读取到的电平0表示按下。你需要根据实际电路调整极性。
3.4 去抖动处理
机械按键存在抖动,通常需要在扫描中加入去抖动逻辑。简单的方法是:每隔10~20ms扫描一次,并记录前后两次扫描结果,只有当某按键连续两次检测到相同状态时才认为状态变化。NumWorks本身也有一套去抖机制,我们只需要提供原始的按键状态矩阵即可。
3.5 集成到Ion::Keyboard
NumWorks的键盘抽象层位于ion/include/ion/keyboard.h和ion/src/device/shared/drivers/keyboard.h。我们需要为ESP32-S3实现以下函数:
Ion::Keyboard::State:获取当前按键状态。通常返回一个Keyboard::State对象,其中包含所有按键的按下状态。Ion::Keyboard::scan:执行一次扫描,更新内部状态。
在ESP32-S3的实现中,我们将上面的扫描函数封装起来,并按照NumWorks的按键映射(每个按键对应一个矩阵坐标)将原始矩阵转换为Keyboard::State。
例如,在ion/src/esp32s3/keyboard.cpp中:
cpp
ini
#include <ion/keyboard.h>
#include "keypad_hardware.h" // 上面的硬件驱动函数
namespace Ion {
namespace Keyboard {
State scan() {
uint8_t matrix[8][8];
keypad_scan(matrix); // 从硬件读取原始矩阵
State state;
// 清空state
for (int i = 0; i < sizeof(state)/sizeof(state.uint8); i++) {
((uint8_t*)&state)[i] = 0;
}
// 将matrix映射到state的各个位
// 假设按键映射表 keyMap[row][col] 对应 Keyboard::Key 枚举
for (int row = 0; row < 8; row++) {
for (int col = 0; col < 8; col++) {
if (matrix[row][col]) {
Key k = keyMap[row][col]; // 你需要定义一个映射表
if (k != Key::None) {
state.setKey(k, true);
}
}
}
}
return state;
}
State* state() {
static State state;
return &state;
}
}
}
其中keyMap需要根据NumWorks的键盘布局定义,可以在原版keyboard.h中找到。
4. 电路设计注意事项
- 电源去耦:每个74HC595和74HC165的VCC和GND之间应放置0.1μF陶瓷电容,靠近芯片引脚。
- 上拉电阻 :行线一般需接上拉电阻(10kΩ左右)至VCC,确保未按下时输入为高电平。如果使用了165的内部上拉(如通过
gpio_pullup_en),也可以不接外部电阻,但165本身没有内部上拉,需要在ESP32的输入端启用上拉,或者外部上拉。 - 限流电阻:如果列线直接驱动LED(一般键盘没有),不需要;但为了防止短路,可在595输出和列线之间串联100Ω电阻。
- 级联扩展:如果将来需要更多按键(如16×8),可以将多个595和165级联,只需将前一级的Q7'(595的串行输出)连接到后一级的DS,以及将前一级的Q7(165的串行输出)连接到后一级的SER(串行输入),时钟和锁存/加载共用。软件上相应地增加移位字节数即可。
5. 总结
通过使用74HC595和74HC165串行扩展芯片,我们成功地将ESP32-S3的键盘接口从16个GPIO减少到5个GPIO,为其他外设留出了充足的空间。硬件设计简单可靠,软件扫描算法清晰,易于集成到NumWorks的Ion层中。
接下来,我们将进入更复杂的模块------存储模拟(Ion::Storage),即如何用ESP32-S3的NVS分区或SPIFFS来模拟NumWorks的内部闪存存储,以便保存用户数据和应用。敬请期待!