# xiaocheng **Repository Path**: genvex/xiaocheng ## Basic Information - **Project Name**: xiaocheng - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-23 - **Last Updated**: 2026-08-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # xiaocheng — ESP32-S3 矩阵按键驱动 基于 **2×3 矩阵扫描** 的按键驱动,配合 **XL9535 I²C I/O 扩展器** 省引脚, 实现 **2 根 GPIO + 3 个 XL9535 列引脚 = 6 个独立按键** 的复用方案。 硬件目标:**ESP32-S3(rev v0.2)**,ESP-IDF v6.0.2。 --- ## 一、为什么用矩阵按键 6 个实体按键,如果用独立按键接法,需要占用 **6 个 GPIO**。 对于 ESP32-S3 这种主控引脚本就紧张的设备,6 个引脚划给按键代价太高。 矩阵按键把按键排成 **行 × 列**,行用 GPIO 输出,列用输入检测, **轮流拉高每一行,看电流能不能从行流到列**,从而区分是哪个键按下了。 2×3 排列,**2 根行线 + 3 根列线 = 5 根线管 6 个键**。 --- ## 二、硬件连接 ``` XL9535 列(I²C 读) P0.0 P0.4 P0.6 ┌────────┬──────┬──────┐ GPIO0 ──┤ A │RIGHT │ UP │ ├────────┼──────┼──────┤ GPIO46──┤ B │DOWN │ LEFT │ └────────┴──────┴──────┘ ``` | 硬件 | 说明 | |---|---| | 行 A | ESP32-S3 **GPIO0**,推挽输出 | | 行 B | ESP32-S3 **GPIO46**,推挽输出 | | 列 | **XL9535**(I²C 地址 0x20)的 P0.0 / P0.4 / P0.6 引脚 | | I²C 总线 | ESP32-S3 I²C0(SCL=GPIO7, SDA=GPIO12) | --- ## 三、扫描原理 扫描分两轮,每轮扫 3 个键。 ### 第 1 轮:拉高 GPIO0,读列 1. GPIO0 = **HIGH**,GPIO46 = **LOW** 2. 把 XL9535 的 P0.0 / P0.4 / P0.6 设为**输入** 3. 读这三个引脚的电平 物理过程:如果按下了 A 键,电流从 GPIO0 → A 键 → P0.0,读到 P0.0 = HIGH。 同理 RIGHT → P0.4 = HIGH,UP → P0.6 = HIGH。 > 这一轮就能确定 **A / RIGHT / UP** 三个键的状态。 ### 第 2 轮:拉高 GPIO46,读列 GPIO46 = HIGH,GPIO0 = LOW,读列。 B → P0.0 = HIGH,DOWN → P0.4 = HIGH,LEFT → P0.6 = HIGH。 两轮加起来,6 个键全部判完。 --- ## 四、两个必须绕开的坑 ### 坑 1:先放电 10ms XL9535 是 I²C 芯片,靠 I²C 读写列引脚。芯片内部引脚有 **RC 充放电延迟**, 上一次扫描残留的电荷如果不放掉,读到的就是脏值——**按键没按也读出 HIGH,产生误触发**。 做法:扫描之前,先把所有列引脚设为**输出低电平**,强制放电 **10ms**,再切回输入读值。 ``` 列引脚电压 HIGH ───────────┐ │ ← 残留电荷 │ LOW ───────────┴────────────────────────────── ↑ ↑ 强制输出低 切输入后读值,干净了 (放电 10ms) ``` ### 坑 2:未扫描的行必须拉低 第一轮扫描行 A(GPIO0=HIGH)时,GPIO46 必须明确拉低。 如果不拉低而是悬空,按下 B 键时电流会从 **GPIO0 → A 键 → P0.0 → B 键 → GPIO46 反向灌入**, 导致 P0.0 被拉高,误判 A 键也被按下了。 这个坑很隐蔽,单独测每个键没事,多键组合按一下全乱。 --- ## 五、完整扫描时序 ``` 第 1 轮 第 2 轮 ┌────────────┬──────────┬────┐ ┌────────────┬──────────┬────┐ │放电 10ms │扫描读取 │收尾│ │放电 10ms │扫描读取 │收尾│ ├────────────┼──────────┼────┤ ├────────────┼──────────┼────┤ GPIO0 = LOW │ HIGH │ LOW│ │ LOW │ LOW │ LOW│ GPIO46= LOW │ LOW │ LOW│ │ LOW │ HIGH │ LOW│ 列 = 输出低 │ 切输入 │输出低│ │ 输出低 │ 切输入 │输出低│ │读列 │ │ │ │读列 │ │ ``` --- ## 六、驱动架构 ``` ┌─────────────────────────────────────────┐ │ app_main() │ │ │ │ 注册按键回调 │ │ matrix_keys_set_key_callback(KEY_A, ...)│ │ │ ├────────────────┬────────────────────────┤ │ matrix_keys │ ws2812_effects │ │ (Timer ISR) │ (独立 task) │ │ │ │ │ FreeRTOS Timer │ 每 5ms tick 推进一帧 │ │ 1ms 扫描周期 │ RMT 驱动 WS2812 │ │ I2C 合并传输 │ 6 种灯效 │ └────────────────┴────────────────────────┘ ``` ### 核心设计 | 模块 | 文件 | 说明 | |---|---|---| | `matrix_keys.c` | 按键驱动 | FreeRTOS Timer 1ms 后台扫描,**I²C 合并传输** 优化 | | `ws2812_effects.c` | 灯效 | RMT TX 通道 + simple encoder 驱动 WS2812 | | `main.c` | 应用 | I²C 总线初始化 → 装驱动 → 起灯效任务 | ### I²C 合并传输 XL9535 的 I²C 寄存器地址**自动递增**。利用这个特性,把多次 I²C 交易合并为一次: ```c /* 放电:1 次 I²C 写 3 字节(CFG=0x00, OUT=0x00)*/ uint8_t buf[3] = { XL9535_P1_CFG, 0x00, 0x00 }; i2c_master_transmit(s_i2c_dev, buf, 3, 100); /* 切输入:1 次 I²C 写 2 字节(CFG=0xFB, OUT=0x00)*/ uint8_t buf[2] = { 0xFB, 0x00 }; i2c_master_transmit(s_i2c_dev, buf, 2, 100); /* 读列:1 次 transmit_receive */ i2c_master_transmit_receive(s_i2c_dev, ®, 1, &col, 1, 100); ``` 原来一轮扫描需要 4 次 I²C 交易(写 CFG → 写 OUT → 写 CFG → 读 IN), 合并后只需要 **3 次**。两轮总共 **5 次 I²C 交易**。 ### 消抖 + 长按 - 消抖:连续 **15 个扫描周期(15ms)** 稳定才判定状态变化 - 长按:按住超过 **200ms** 后,每 **50ms** 触发一次重复事件 --- ## 七、使用方式 ### 初始化 ```c // 1. 初始化 I²C 总线 i2c_master_bus_config_t bus_cfg = { .i2c_port = I2C_NUM_0, .sda_io_num = GPIO_NUM_12, .scl_io_num = GPIO_NUM_7, }; i2c_master_bus_handle_t bus; i2c_new_master_bus(&bus_cfg, &bus); // 2. 装按键驱动 matrix_keys_install(bus); // 3. 注册按键回调(每个键独立) matrix_keys_set_key_callback(KEY_A, on_a_press, NULL, NULL); matrix_keys_set_key_callback(KEY_B, on_b_press, on_b_release, NULL); matrix_keys_set_key_callback(KEY_UP, on_up_press, NULL, on_up_repeat); ``` ### 回调签名 ```c typedef void (*key_action_t)(key_event_t event); ``` `event` 是 `KEY_EVENT_PRESS` / `KEY_EVENT_RELEASE` / `KEY_EVENT_REPEAT` 三者之一。 不需要的回调传 `NULL`。 ### 查询状态 ```c bool pressed = matrix_keys_is_pressed(KEY_A); ``` --- ## 八、资源占用 | 项目 | 数值 | |---|---| | 扫描频率 | 1ms/次(1000Hz)| | 单次 I²C 交易 | 5 次/轮,~0.5ms | | 占 I²C 总线 | ~5% | | 占 RAM | ~300 字节(按键状态 + Timer + task stack)| | 占 CPU | 极低(大部分时间 `vTaskDelay` 挂起)| --- ## 九、按键 → 灯效映射 | 按键 | 灯效 | |---|---| | A | rainbow 彩虹 | | B | breathe 呼吸 | | UP | chase 追逐 | | DOWN | sparkle 闪烁 | | LEFT | theater 剧场 | | RIGHT | wipe 擦除 | --- ## 十、编译 ```bash # 使用 ESP-IDF v6.0.2 export IDF_PATH=~/esp/esp-idf idf.py -DIDF_TARGET=esp32s3 build idf.py -p /dev/ttyUSB0 flash monitor ``` 固件大小:约 241 KB。 --- ## 十一、参考 - 微信公众号文章:[搞定小程ESP32-S3掌机按键](https://mp.weixin.qq.com/s/ccymjcUkwtx_DC5uo3Ft1g) - ESP-IDF v6.0.2 RMT TX 示例:`led_strip_simple_encoder`