1. 为什么需要并行输入扩展
在现代嵌入式系统设计中,我们经常会遇到一个尴尬的局面:主控芯片的GPIO引脚数量有限,但外设需求却在不断增加。以STM32F103RC为例,这款性价比极高的Cortex-M3芯片虽然功能强大,但其GPIO数量在面对复杂控制系统时仍显捉襟见肘。
我在去年设计一个工业控制面板时就深有体会:需要同时监测32个机械开关状态,而STM32F103RC的可用GPIO只有48个,除去通信接口、显示驱动等必要功能后,留给开关检测的引脚所剩无几。这时候,并行输入扩展芯片就成了救命稻草。
MC74HC165A作为经典的8位并行输入转串行输出移位寄存器,能以极低成本实现GPIO扩展。通过级联多个74HC165,理论上可以无限扩展输入通道(当然实际受限于时序和系统响应要求)。这种方案相比直接使用GPIO扩展器IC(如PCA9555)有以下优势:
- 成本低廉:74HC165单价通常不到1元人民币
- 电路简单:仅需3个控制信号(CLK, SH/LD, DATA)
- 兼容性强:标准SPI接口,几乎所有MCU都能驱动
- 响应快速:在10MHz时钟下,读取8个开关状态仅需0.8μs
2. MC74HC165A核心特性解析
2.1 引脚功能与电气特性
拆开一颗MC74HC165A,你会发现这个16脚DIP封装的芯片内部结构相当精妙。其关键引脚包括:
- SH/LD(第1脚):移位/装载控制,低电平时并行装载输入数据,高电平时允许移位
- CLK(第2脚):时钟输入,上升沿触发数据移位
- QH(第9脚):串行数据输出(MSB优先)
- /QH(第10脚):反相串行输出(较少使用)
- SER(第15脚):串行数据输入,用于级联扩展
- A-H(第3-6,11-14脚):8位并行输入
电气参数方面特别要注意:
- VCC范围:2V至6V(与3.3V的STM32完美兼容)
- 输入高电平最小值:3.15V @ VCC=5V(与STM32对接时建议加电平转换)
- 最大时钟频率:36MHz @ VCC=4.5V
- 典型功耗:80μA静态电流
2.2 工作时序深度剖析
理解74HC165的关键在于掌握其工作时序。我绘制了以下典型操作序列:
装载阶段(采样输入):
- 拉低SH/LD引脚至少35ns(典型值)
- 此时并行输入A-H的状态被锁存到内部寄存器
移位阶段(读取数据):
- 拉高SH/LD引脚
- 在CLK上升沿,数据从QH依次移出(MSB优先)
- 每个时钟周期移出1位,共需8个时钟读取完整字节
关键提示:在STM32的3.3V系统下,建议将CLK频率控制在5MHz以内以确保可靠通信。我曾因使用10MHz时钟导致数据错位,最终通过示波器捕获发现是上升时间不足所致。
3. STM32硬件设计要点
3.1 典型应用电路设计
下图展示了STM32F103RC与74HC165的标准连接方式:
+-----+ PA4 | | 1 SH/LD PA5 | | 2 CLK PA6 | | 9 QH +3.3V| | 15 SER GND | | +-----+实际设计中必须注意:
- 上拉电阻:所有未使用的并行输入引脚应通过10kΩ电阻上拉
- 去耦电容:每个74HC165的VCC引脚需要100nF陶瓷电容
- 级联连接:前一级的QH接下一级的SER,共用CLK和SH/LD
- 线长控制:时钟信号走线应尽量短,超过5cm需考虑终端匹配
3.2 电平转换方案
当系统中有5V器件时,必须进行电平转换。我的项目中使用的是BSS138 MOSFET方案:
- 74HC165侧:上拉电阻接5V
- STM32侧:通过BSS138转换
- 成本约0.3元/通道,比专用电平转换芯片便宜80%
4. 软件驱动实现
4.1 基于HAL库的驱动代码
以下是经过生产验证的驱动程序核心片段:
#define HC165_SH_LD_PIN GPIO_PIN_4 #define HC165_CLK_PIN GPIO_PIN_5 #define HC165_DATA_PIN GPIO_PIN_6 uint8_t HC165_ReadByte(void) { uint8_t value = 0; // 装载并行数据 HAL_GPIO_WritePin(GPIOA, HC165_SH_LD_PIN, GPIO_PIN_RESET); delay_us(1); HAL_GPIO_WritePin(GPIOA, HC165_SH_LD_PIN, GPIO_PIN_SET); // 移位读取 for(uint8_t i=0; i<8; i++) { value <<= 1; if(HAL_GPIO_ReadPin(GPIOA, HC165_DATA_PIN)) value |= 0x01; HAL_GPIO_WritePin(GPIOA, HC165_CLK_PIN, GPIO_PIN_SET); delay_us(0.1); HAL_GPIO_WritePin(GPIOA, HC165_CLK_PIN, GPIO_PIN_RESET); } return value; }4.2 性能优化技巧
通过实测发现几个关键优化点:
使用GPIO端口寄存器直接操作比HAL库快20倍:
#define HC165_PORT GPIOA #define HC165_SH_LD (1<<4) #define HC165_CLK (1<<5) #define HC165_DATA (1<<6) void HC165_FastRead(uint8_t *buf, uint8_t count) { HC165_PORT->BRR = HC165_SH_LD; // 拉低SH/LD __NOP(); __NOP(); // 约35ns延时 HC165_PORT->BSRR = HC165_SH_LD; // 拉高SH/LD for(uint8_t j=0; j<count; j++) { uint8_t val = 0; for(uint8_t i=0; i<8; i++) { val <<= 1; if(HC165_PORT->IDR & HC165_DATA) val |= 1; HC165_PORT->BSRR = HC165_CLK; // 上升沿 HC165_PORT->BRR = HC165_CLK; // 下降沿 } buf[j] = val; } }对于级联多芯片的情况,采用DMA+SPI硬件方案可进一步提升效率:
- 配置SPI为主机模式,MSB优先
- 将SH/LD信号连接到SPI的NSS引脚
- 使用DMA自动接收数据
5. 典型问题排查指南
5.1 数据错位问题
症状:读取的数据位与物理开关状态不对应 排查步骤:
- 确认SH/LD信号有足够低电平时间(示波器测量>50ns)
- 检查时钟信号质量(上升时间<20ns)
- 验证级联顺序是否正确(第一级的QH接第二级的SER)
- 检查PCB布局是否有信号串扰
5.2 信号抖动处理
在工业环境中,我遇到开关抖动导致误触发的问题。解决方案:
- 硬件方案:在并行输入引脚加RC滤波(典型值:R=1kΩ, C=100nF)
- 软件方案:实现去抖动算法
#define DEBOUNCE_TIME 20 // ms uint8_t HC165_ReadDebounced(void) { static uint32_t last_time = 0; static uint8_t last_state = 0; static uint8_t stable_state = 0; uint32_t now = HAL_GetTick(); uint8_t current = HC165_ReadByte(); if(current != last_state) { last_time = now; last_state = current; } else if((now - last_time) > DEBOUNCE_TIME) { stable_state = current; } return stable_state; }
6. 进阶应用实例
6.1 64通道输入扩展系统
在我的一个自动化测试设备项目中,需要监测64个光电传感器状态。设计方案如下:
- 级联8个74HC165(成本总计约8元)
- 使用STM32的SPI1接口驱动
- 硬件连接:
- SPI1_SCK 接所有165的CLK
- SPI1_MISO接第一级的QH
- PA4作为NSS信号控制SH/LD
- 读取流程:
- 拉低PA4装载数据
- 启动SPI接收8字节(64位)
- 数据自动存储在接收缓冲区
6.2 与矩阵键盘的结合应用
将74HC165用于4x4矩阵键盘扫描可节省7个GPIO:
- 连接4列线到165的并行输入
- 使用4个GPIO控制行线
- 扫描时依次激活每行,通过165读取列状态
- 相比传统矩阵扫描方案,IO占用从8个降至5个
通过实际项目验证,这套方案在降低硬件复杂度的同时,键盘响应速度仍能保持在10ms以内,完全满足人机交互需求。