资讯动态

74HC165驱动详解:STM32 GPIO模拟时序实现输入扩展与级联

发布时间:2026/10/21 22:21:06 来源:尧图企业网站定制
简介移位寄存器是嵌入式系统中扩展数字输入输出最常用的基础器件。74HC165作为典型的并行输入串行输出移位寄存器通过SH/LD锁存与CLK时钟移位可将8路并行输入转为串行数据流仅占用MCU的3个GPIO即可完成采集。相比74HC595实现输出扩展74HC165专门解决输入资源不足问题尤其适合按键阵列、拨码开关与传感器状态采集等场景。本文基于STM32 HAL库完整拆解GPIO模拟时序的驱动实现包括单字节读取、多片级联、按键扫描实例并针对CLK空闲电平、SH/LD释放时序、硬件SPI位序等常见坑给出工程验证方法是一份可直接落地的74HC165驱动参考。1. 队站在 IO 不够用的板子上74HC165 驱动到底在解决什么问题做嵌入式控制面板时最烦的就是 IO 不够用。几年前我调一块 8 路拨码开关的板子MCU 引脚已经所剩无几硬生生挤 8 个 GPIO 读开关状态布线绕得跟蜘蛛网一样。后来换了思路用一片 74HC165 把 8 路并行输入转成串行输出3 根线就搞定了还顺带支持多片级联20 路、40 路的输入阵列也能从容应对。这个 74HC165 驱动工程核心就是给你一套能直接落地的 STM32 驱动代码GPIO 模拟时序、单字节/多字节读取、按键扫描实例都有拿过来改改引脚就能跑。尤其适合做按键阵列、拨码开关、传感器状态采集这类输入扩展场景手里正好缺一份可靠驱动参考的开发者这份资源能省不少翻数据手册的时间。2. 硬件与时序SH/LD、CLK、Q7 这三根线是怎么配合工作的2.1 74HC165 与 74HC595一对互补的移位寄存器很多开发者在接触 74HC165 之前先玩过 74HC595。74HC595 是串行输入、并行输出一片可以扩展 8 个输出口常用于控制 LED 或数码管段选。而 74HC165 正好反过来并行输入、串行输出一片可以扩展 8 个输入口适合批量采集按键、开关和传感器状态。两者放在一起理解非常直观输出不够用找 595输入不够用找 165。从电路设计角度看74HC165 把 8 路并行的电平信号“折叠”成一根线上的串行数据流MCU 只需要根数据线和一根时钟线就能展开读取。这里的成本是时间——一次采样要打 8 个时钟脉冲才能拿到一个完整字节但换来的是宝贵的 GPIO 资源。在按键扫描这类低速场景里这个时间开销几乎可以忽略不计。2.2 锁存与移位一次完整读操作的时序拆解74HC165 的引脚里SH/LDShift/Load有的手册写作 SHIFT/LOAD 或 PL是锁存控制端CLK 是移位时钟Q7 是串行数据输出。读取一个字节的标准流程分两步先锁存再移位。第一步把 SH/LD 拉低。此时芯片会把 8 个并行输入引脚D7~D0上的电平“拍照”存进内部寄存器同时 Q7 会立刻输出 D7 的电平不需要等任何时钟。这是很多人容易忽略的点第一个数据位在锁存完成后就已经在 Q7 上了和第一个时钟边沿没有关系。第二步把 SH/LD 拉高然后连续给出 8 个时钟脉冲。在每个时钟上升沿内部数据右移一位Q7 依次输出 D6、D5……直到 D0。从时序图的角度看SH/LD 拉低后需要保持一小段建立时间手册上通常是 ns 级确保并行数据被稳定锁存拉高后同样需要一点保持时间才能开始送时钟。用 GPIO 模拟这种时序非常简单因为主频几十 MHz 的 MCU 本身就比 ns 的时间尺度慢得多只要保证 SH/LD 低电平持续时间不低于一个 GPIO 翻转周期就绝对满足手册要求。真正容易翻车的反而是后面要说的时钟顺序问题。2.3 多片级联的接线与数据顺序一片 74HC165 只有 8 路输入接 16 路按键就需要两片级联。级联的物理连接方式很规律后一片的 Q7 接到前一片的 DS 引脚串行数据输入74HC165 专门为级联留的引脚前一片的 Q7 接 MCU。时钟和锁存信号并联到所有芯片。读取两片的数据时MCU 先锁存所有芯片同时锁存各自的并行输入然后连续打 16 个时钟脉冲。前 8 个脉冲从第一片离 MCU 最近的那片的 Q7 移出数据后 8 个脉冲移出的是第二片的数据——第二片的数据会通过 DS 引脚“流过”第一片最终出现在 Q7 上。所以先读到的字节是第一片的后读到的字节是第二片的。这个顺序在写驱动时把它固化成数组下标就行物理上哪片靠近 MCU哪片就排在数组前面不用额外倒序。3. 驱动实现拆解从 GPIO 电平到一整个字节3.1 工程里驱动文件怎么摆这个驱动工程的结构不复杂我把代码分成了驱动层和应用层两层。驱动层是hc165.c和hc165.h只干一件事把 GPIO 电平转换成字节流不关心业务逻辑。应用层由开发者自己写比如按键扫描、编码器读取之类的模块。接口就两个一个初始化函数一个读字节或读多字节函数。这种分层的做法有利于二次开发。比如某个项目里读取的芯片从 1 片变成 3 片应用层代码不需要改动驱动层把多字节读取接口做好就行。文件结构大致如下project/ ├── Core/ │ ├── Inc/hc165.h │ └── Src/hc165.c │ └── Src/main.c └── ...hc165.h推荐把引脚定义全放在里面用宏或者枚举管理换板子时只改头文件不碰实现代码。3.2 GPIO 初始化的具体写法我一般用 HAL 库写这组驱动标准外设库的写法大同小异。需要注意3 个引脚里SH/LD 和 CLK 是输出Q7 是输入。Q7 的输入模式建议根据硬件电路选择上拉或下拉如果并行输入端接了按键到 GND就选上拉如果悬空读到的值会乱跳后面避坑章节会展开讲。void HC165_Init(void) { GPIO_InitTypeDef gpio_init {0}; // 使能 GPIOA 和 GPIOB 时钟这里按实际接线修改 __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); // CLK 和 SH/LD 配置为推挽输出速度选 LOW 就够 gpio_init.Pin HC165_CLK_PIN | HC165_SH_LD_PIN; gpio_init.Mode GPIO_MODE_OUTPUT_PP; gpio_init.Pull GPIO_NOPULL; gpio_init.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(HC165_CLK_PORT, gpio_init); // Q7 配置为输入没有外部上拉时开内部上拉 gpio_init.Pin HC165_Q7_PIN; gpio_init.Mode GPIO_MODE_INPUT; gpio_init.Pull GPIO_PULLUP; HAL_GPIO_Init(HC165_Q7_PORT, gpio_init); // 初始状态SH/LD 保持高电平CLK 保持低电平 HC165_SH_LD_HIGH(); HC165_CLK_LOW(); }参数说明Speed 选GPIO_SPEED_FREQ_LOW是因为 74HC165 的时钟频率在几百 kHz 就足够了太高的翻转速度反而可能引入信号完整性问题。Pull 的选择取决于外部电路——如果并行输入默认高电平按键接地就选GPIO_PULLUP默认低电平则选GPIO_PULLDOWN。初始状态把 SH/LD 置高、CLK 置低是为了让芯片在非读取时段处于“移位就绪但不动作”的状态避免误触发。3.3 读单字节核心函数逐行拆读取单字节是整个驱动的心脏。函数不长但时序顺序必须理清楚。uint8_t HC165_Read_Byte(void) { uint8_t data 0; // 1. 锁存并行输入拉低 SH/LD保持一小段时间 HC165_SH_LD_LOW(); delay_us(1); HC165_SH_LD_HIGH(); // 2. 连续打 8 个时钟逐位移出数据 for (uint8_t i 0; i 8; i) { HC165_CLK_LOW(); // 时钟拉低准备采样 data (data 1) | HAL_GPIO_ReadPin(HC165_Q7_PORT, HC165_Q7_PIN); // 读 Q7 HC165_CLK_HIGH(); // 时钟拉高上升沿触发移位 } return data; }逻辑说明锁存完成后Q7 上已经是最低位还是最高位74HC165 的移位方向是并行输入的最高位 D7 最先出现在 Q7 上所以循环里第一次读到的 bit 应放在字节的最高位于是每读一位就先左移再或上当前值正好和 D7→D0 的输出顺序对齐。比较关键的一点是我在循环里有意让CLK_LOW()出现在读数之前。这是因为锁存刚结束时 Q7 输出的第一个位是有效的不需要时钟就能采而后面的 7 个位都是在上升沿之后才更新的。用“先拉低、再读、再拉高”的顺序能保证每次采样都发生在时钟上升沿之前的稳定窗口里规避了边沿竞争这在 GPIO 模拟时序里是常见的血泪教训。参数说明delay_us(1)不是必须的但建议保留。SH/LD 低电平宽度手册要求只有几十 nsGPIO 翻转本身就满足但加这一微秒能应对不同厂家芯片的差异也方便调试时用示波器观察锁存窗口。如果嫌这个延时影响扫描频率可以删掉实际工程里我删过也没有出过问题但初次调通不建议省。3.4 多片读取和按键扫描实例多片读取不复杂把上面的单字节逻辑套一层循环就行。这里用数组指针对齐物理级联顺序。void HC165_Read_Multi(uint8_t *buf, uint8_t chip_count) { // 所有芯片共用 SH/LD一起锁存 HC165_SH_LD_LOW(); delay_us(1); HC165_SH_LD_HIGH(); for (uint8_t chip 0; chip chip_count; chip) { uint8_t data 0; for (uint8_t i 0; i 8; i) { HC165_CLK_LOW(); data (data 1) | HAL_GPIO_ReadPin(HC165_Q7_PORT, HC165_Q7_PIN); HC165_CLK_HIGH(); } buf[chip] data; // 先读到的属于靠近 MCU 的那片 } }参数说明chip_count是级联的芯片数量buf是输出缓冲区长度必须不小于chip_count。这个函数内部只打一次锁存脉冲因为所有芯片的 SH/LD 并联同时锁存各自并行输入后续移位是连续完成的不能每片单独锁存一次否则后一片的数据会在等待期间丢失。配套的按键扫描应用层代码也顺手贴出来假设按键低有效void Key_Scan(uint8_t *key_state) { uint8_t buf[1]; HC165_Read_Multi(buf, 1); for (uint8_t i 0; i 8; i) { // 每一位对应一个按键0 表示按下 if ((buf[0] (0x01 i)) 0) { key_state[i] 1; } else { key_state[i] 0; } } }这个扫描函数放在定时器中断里每 10ms 调用一次就行比在主循环里忙等要干净。注意位序对应关系buf[0]的 bit0 对应 74HC165 的 D0 引脚bit7 对应 D7。硬件接线时理清这个映射软件里就不用做额外翻转。4. 移植与接入把这套驱动装进自己的工程4.1 用 CubeMX 分配引脚时要注意的选项如果你的工程是用 CubeMX 生成的引脚分配有几个细节值得花半分钟确认。SH/LD 和 CLK 选 GPIO_OutputQ7 选 GPIO_Input。输出引脚的初始电平在 CubeMX 的 GPIO 设置页里可以直接配建议把 SH/LD 的初始电平设为 HighCLK 设为 Low这样生成代码后驱动初始化之前芯片不会乱移位。输入引脚 Q7 的上拉/下拉设置要和硬件设计保持一致。很多开发板上的 74HC165 并行输入直接接按键按键另一端接地这种电路常态是高电平Q7 引脚在 CubeMX 里选 Pull-up。如果并行输入接的是传感器开漏输出可能需要外部上拉内部上拉不一定扛得住注意看数据手册的电流参数。还有一点容易踩CubeMX 的 GPIO 速度选项。同一个引脚如果被复用成其他外设比如 Q7 恰好和某个串口的 RX 冲突CubeMX 会在生成代码时报错。设计电路时尽量给这 3 个引脚选在普通的 GPIO 上避免和调试口冲突。4.2 把驱动文件接进已有代码的步骤把这个驱动接进旧工程我一般按三步走。第一步把hc165.c复制进工程的Src目录hc165.h放进Inc目录然后在 CubeMX 或 IDE 里把源文件加进编译列表。如果是 Keil直接在 Project 窗口右键 Add Existing Files如果是 CMake 工程在 CMakeLists.txt 里补一行源文件路径。第二步改头文件里的引脚宏。hc165.h里会有类似下面的定义#define HC165_CLK_PORT GPIOA #define HC165_CLK_PIN GPIO_PIN_0 #define HC165_SH_LD_PORT GPIOA #define HC165_SH_LD_PIN GPIO_PIN_1 #define HC165_Q7_PORT GPIOA #define HC165_Q7_PIN GPIO_PIN_2把端口和引脚号改成你自己工程里的实际接线。如果用了多个 74HC165Q7 仍然是接到 MCU 的那一根线从第二片开始的 Q7 都接到前一片的 DS不占 MCU 引脚。第三步在main()初始化阶段调用HC165_Init()然后就可以在业务代码里调用读取接口了。注意HC165_Init()必须在HAL_Init()和时钟配置之后执行因为它依赖 GPIO 时钟已经使能。4.3 一个可跑的扫键换灯示例为了验证驱动是否正常工作我习惯做一个最简单的“扫键换灯”实验8 个按键接 74HC165 并行输入8 个 LED 直接接 MCU 的另外 8 个 GPIO按下哪个键就点亮对应的 LED。int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); // 初始化 LED 引脚 HC165_Init(); // 初始化 74HC165 驱动 uint8_t key_buf[1]; while (1) { HC165_Read_Multi(key_buf, 1); // 直接把字节写给 LED 的 GPIO 寄存器 // 这里假设 LED 低电平点亮按键按下为低电平 uint8_t led_value ~key_buf[0]; HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin_All, led_value); HAL_Delay(10); } }这个实验能一次性验证三件事硬件接线是否正确、位序是否对应、锁存时序是否稳定。如果按下第 3 个键点亮的却是第 5 个 LED多半是位序理解反了把~key_buf[0]换成字节反转函数即可。如果按键按下时 LED 闪烁不定说明读到了抖动需要在应用层加消抖后面会提到。5. 避坑手记这几个 74HC165 时序细节最容易翻车5.1 现象CLK 空闲电平没拉对数据提前移了一位有次我把 CLK 初始状态设成了高电平读出来的字节整体像是“错位”了——按下一个键读到的是相邻位的状态。原因在于 74HC165 的移位发生在时钟上升沿如果 CLK 空闲时保持高电平锁存完成后第一个下降沿也会触发移位动作等于数据在读取循环开始前就偷偷移了一位。解决方法是把 CLK 的初始状态强制设为低电平并且保证整个读取循环期间每个时钟脉冲都从低到高完整翻转一次。检查代码里有没有在循环外多打了半个脉冲用示波器看 CLK 波形正常应该是一串干净的方波起始和结束都在低电平。5.2 现象SH/LD 释放太晚第一个位读到的是瞬态SH/LD 拉低完成锁存后如果我紧接着不延时就直接拉高并开始读 Q7有时会读到第一个位的不稳定状态。原因是芯片内部在锁存后需要极短的建立时间让 Q7 输出稳定手册上这个时间是几十 ns但工程上留点余量总没错。解决方式就是驱动里那 1 微秒的延时。如果觉得微秒级延时太粗暴可以用几个 NOP 指令替代效果一样。这里不要试图省这几条指令省出来的时间微乎其微但省掉的可靠性可能让你排查半天。5.3 现象输入脚悬空读到的值像在随机跳74HC165 的并行输入引脚如果悬空读到的值会随环境噪声变化。我第一次搭实验板时没接上拉电阻结果发现即使没有任何按键按下扫描出来的字节也时不时冒出一个 1像在黑匣子里乱跳。解决方法是区分场景按键一端接 GND、一端接 74HC165 输入引脚的必须在输入引脚和 VCC 之间接上拉电阻或者开启 MCU 内部上拉反过来按键接 VCC 的就接下拉电阻。另外也可以在软件里做一次“连续两次读取一致才采信”的确认逻辑两层保险同时上。5.4 现象换成硬件 SPI 后读出来的字节位序全反了有人图省事想把 GPIO 模拟改成 SPI 硬件读取把 CLK 接 SCK、Q7 接 MISO、SH/LD 用普通 GPIO 控制。配置好 SPI 主模式后读回来的字节经常发现位序和 GPIO 模拟时是相反的。原因在于 SPI 的 CPOL/CPHA 配置如果是默认的 Mode 0时钟空闲低、第一个边沿采样采样时刻落在第一个边沿而 74HC165 的数据在时钟上升沿之后才更新时序不匹配。需要把 SPI 配成 Mode 1时钟空闲低、第二个边沿采样同时注意 MSB 优先还是 LSB 优先的设置。建议第一次用硬件 SPI 读取时先读一个已知值比如 0xAA看输出对不对再对接业务逻辑。6. 二次开发思路从 8 路输入到上百路输入的改造路径6.1 级联扩展用数组接口替代单字节接口驱动里已经提供了HC165_Read_Multi(uint8_t *buf, uint8_t chip_count)二次开发时把按键扫描函数从单字节改成多字节即可。比如用两片 74HC165 接 16 路按键只需把缓冲区从 1 字节扩到 2 字节然后逐位判断。级联数量上去之后建议把按键状态做成位图数组一个 bit 对应一个按键既省 RAM 又方便批量处理。6.2 把 GPIO 模拟替换成硬件 SPI 的接线与配置如果项目里的主控本身有 SPI 外设而且你不想让 GPIO 模拟占用 CPU 翻转指令可以改成硬件 SPI 方式读取。接线如上一条避坑所说SCK 接 CLKMISO 接 Q7SH/LD 用任意一个 GPIO 控制。SPI 配置成主模式、Mode 1CPOL0, CPHA1、8 位帧、MSB 优先。每次读取先拉低 SH/LD 再拉高完成锁存然后调用 HAL 的 SPI 接收函数读一个字节读完后立刻拉高 SH/LD 复位。需要注意的是跳线距离稍长时 SPI 速度不要拉到最高几 MHz 足够信号质量优先。6.3 按键状态机与消抖让扫描结果更可靠裸读 74HC165 的字节直接当按键状态用机械按键的抖动会带来误触发。我一般把消抖做成一个简单的状态机连续两次扫描间隔 10ms读到的字节一致才认为按键状态稳定。对每个按键位维护“上次状态”和“当前状态”只有从松开变为按下时才触发一次按压事件避免长按被反复触发。做完这套状态机后74HC165 的驱动就可以稳定服务于各种输入面板了。从最开始为了省 IO 临时抱佛脚到后来把这份驱动封装成标准化模块我每次画板子做输入采集都会复用这一套唯一要改的就是头文件里的引脚宏。从那以后我每次焊接完输入面板都会先跑一遍扫键换灯实验确认位序和锁存时序再往上叠业务逻辑。希望这篇拆解能帮你少踩几个时序坑顺利把 74HC165 用起来。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑