STM32F030软件SPI驱动74HC165实现多路按键扫描
1. 硬件连接与原理分析
74HC165是一款经典的8位并行输入/串行输出移位寄存器,特别适合用来扩展GPIO资源紧张的微控制器。我最近在一个智能家居控制面板项目中使用STM32F030驱动这款芯片,实测下来稳定性相当不错。先说说硬件连接要点:
74HC165的引脚功能需要特别注意:
- PL(并行加载)引脚低电平时会将8个并行输入口的状态锁存到内部寄存器
- CP(时钟)引脚每个上升沿会将数据从DS引脚移入,同时Q7引脚移出数据
- QH(串行输出)引脚连接MCU的输入GPIO
实际接线时,我用杜邦线连接了STM32F030的以下引脚:
- PA4连接PL(用作片选信号)
- PB3连接CP(时钟信号)
- PA6连接QH(数据输入)
这里有个小技巧:如果按键数量超过8个,可以通过级联多个74HC165来实现扩展。只需要将第一个芯片的QH输出接到第二个芯片的DS输入,共用PL和CP信号即可。我在测试时级联了3个芯片,成功实现了24路按键扫描。
2. 软件SPI时序实现
STM32F030虽然有硬件SPI外设,但在某些场景下使用软件模拟SPI反而更灵活。下面是我调试通过的驱动代码关键部分:
#define HC165_PL_PIN GPIO_PIN_4 #define HC165_PL_PORT GPIOA #define HC165_CP_PIN GPIO_PIN_3 #define HC165_CP_PORT GPIOB #define HC165_DS_PIN GPIO_PIN_6 #define HC165_DS_PORT GPIOA uint8_t HC165_ReadByte(void) { uint8_t value = 0; // 拉低PL引脚加载并行数据 HAL_GPIO_WritePin(HC165_PL_PORT, HC165_PL_PIN, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(HC165_PL_PORT, HC165_PL_PIN, GPIO_PIN_SET); // 逐位读取串行数据 for(uint8_t i=0; i<8; i++) { value <<= 1; if(HAL_GPIO_ReadPin(HC165_DS_PORT, HC165_DS_PIN)) { value |= 0x01; } // 产生时钟上升沿 HAL_GPIO_WritePin(HC165_CP_PORT, HC165_CP_PIN, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(HC165_CP_PORT, HC165_CP_PIN, GPIO_PIN_SET); } return ~value; // 按键按下时为低电平,所以取反 }这段代码有几个关键点需要注意:
- PL信号需要保持至少25ns的低电平(实测1ms更可靠)
- 时钟信号要在读取数据位之后翻转
- 按键按下时输入为低电平,所以最后对读取值取反
3. 实际应用中的优化技巧
在真实项目中直接使用上面的基础代码可能会遇到一些问题。我分享几个踩坑后总结的经验:
3.1 消抖处理
机械按键通常需要10-20ms的消抖时间。我的做法是在主循环中这样处理:
#define DEBOUNCE_TIME 20 // 消抖时间20ms uint8_t currentKey, lastKey; uint32_t lastKeyTime = 0; while(1) { currentKey = HC165_ReadByte(); if(currentKey != lastKey) { lastKeyTime = HAL_GetTick(); } else if((HAL_GetTick() - lastKeyTime) > DEBOUNCE_TIME) { if(currentKey != 0) { // 处理有效按键 printf("Key pressed: 0x%02X\n", currentKey); } } lastKey = currentKey; HAL_Delay(5); // 适当延时减少CPU占用 }3.2 多芯片级联处理
当级联多个74HC165时,读取顺序是从最后一个芯片开始。比如级联3个芯片时:
uint8_t HC165_ReadMultiple(uint8_t chipCount) { uint8_t value = 0; // 加载所有芯片的并行数据 HAL_GPIO_WritePin(HC165_PL_PORT, HC165_PL_PIN, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(HC165_PL_PORT, HC165_PL_PIN, GPIO_PIN_SET); // 读取所有芯片的数据 for(uint8_t i=0; i<chipCount*8; i++) { value <<= 1; if(HAL_GPIO_ReadPin(HC165_DS_PORT, HC165_DS_PIN)) { value |= 0x01; } // 产生时钟上升沿 HAL_GPIO_WritePin(HC165_CP_PORT, HC165_CP_PIN, GPIO_PIN_RESET); HAL_Delay(1); HAL_GPIO_WritePin(HC165_CP_PORT, HC165_CP_PIN, GPIO_PIN_SET); } return ~value; }4. 性能测试与优化
在实际测试中,我发现软件SPI的读取速度完全能满足按键扫描的需求。使用72MHz主频的STM32F030,读取一个8位74HC165大约需要50μs,即使级联3个芯片也只需要150μs左右。
如果需要进一步提高速度,可以考虑以下优化:
- 使用寄存器直接操作替代HAL库函数
- 减少时钟延时间隔
- 使用中断方式代替轮询
这里给出一个优化后的快速读取实现:
#define HC165_PL_BSRR (GPIOA->BSRR = GPIO_BSRR_BR_4) #define HC165_PL_SET (GPIOA->BSRR = GPIO_BSRR_BS_4) #define HC165_CP_CLR (GPIOB->BSRR = GPIO_BSRR_BR_3) #define HC165_CP_SET (GPIOB->BSRR = GPIO_BSRR_BS_3) #define HC165_DS_READ (GPIOA->IDR & GPIO_IDR_6) uint8_t HC165_FastRead(void) { uint8_t value = 0; HC165_PL_BSRR; __NOP(); __NOP(); // 约25ns延时 HC165_PL_SET; for(uint8_t i=0; i<8; i++) { value <<= 1; if(HC165_DS_READ) value |= 0x01; HC165_CP_CLR; __NOP(); __NOP(); HC165_CP_SET; } return ~value; }这种实现方式将读取时间缩短到了约5μs,适合对实时性要求更高的应用场景。不过要注意,直接操作寄存器会降低代码的可移植性,建议在关键路径上使用。
