欢迎光临
我们一直在努力

移植numworks图形计算器:8.按键扫描——用74HC595和74HC165扩展GPIO实现矩阵键盘

按键扫描——用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的键盘是一个典型的矩阵结构,例如:

Col0Col1Col2…
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

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

    #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

    #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的内部闪存存储,以便保存用户数据和应用。敬请期待!

    赞(0)
    未经允许不得转载:171主机测评 » 移植numworks图形计算器:8.按键扫描——用74HC595和74HC165扩展GPIO实现矩阵键盘
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址