STM32驱动74HC165:三根GPIO实现8路输入扩展与级联方案

发布时间:2026/10/21 22:21:30

STM32驱动74HC165:三根GPIO实现8路输入扩展与级联方案 简介面向STM32开发者的74HC165驱动代码包定位为并行转串行的IO扩展方案。74HC165是常用的8位并行输入移位寄存器仅需少量GPIO即可扩展多个输入端口解决MCU引脚不足的问题特别适用于矩阵键盘、多位数码管、多通道数据采集等需要同时读取多路信号的场景。资源包体积小巧仅含2个文件1个.h头文件与1个.c源文件压缩后约1KB头文件清晰声明了寄存器配置、引脚映射与数据读取接口源文件则实现了完整的GPIO初始化、SH_CP串行时钟输出、LE移位锁存控制以及并行数据到串行数据的移位读取逻辑可直接加入STM32工程调用。已有734人学习/下载这份驱动让开发者无需再对照数据手册逐条编写时序拿来即用同时代码分层清晰引脚宏定义与延时参数集中管理便于根据实际电路灵活调整。非常适合正在调试74HC165外设、希望在STM32项目中快速完成IO扩展的嵌入式开发者。1. 74HC165驱动把GPIO省到极致一个芯片换来8个输入做单片机项目最烦的一件事就是按键和开关太多尤其是做8路拨码开关、矩阵键盘、控制面板这类需求时IO口永远不够用。74HC165这个芯片很多人听过但真正动手接过的并不多因为网上能找到的驱动代码要么是Arduino的要么是51的拿到STM32上要么时序不对要么代码风格用着别扭。这套驱动资源的核心思路很简单用3个IO口读回8个甚至更多输入信号通过级联还能线性扩展一片芯片管8路两片管16路。项目上用过一次就离不开了特别是引脚紧张的板子这个方案能直接救急。这篇笔记把从硬件连接到STM32驱动代码的完整路径拆开讲碰到的问题和踩过的坑也一并列出来。2. 74HC165到底怎么工作并行输入转串行输出的核心时序2.1 芯片内部结构和引脚功能别被8个输入端口吓住74HC165是一个8位并行输入、串行输出的移位寄存器。所谓并行输入就是8个输入引脚D0到D7可以直接被“锁存”到芯片内部的寄存器里然后在一个时钟信号的控制下这8位数据会按顺序从串行输出引脚Q7一位一位地送出去。对单片机来说这就是把8个IO的读取任务变成了3根线的通信任务一根锁存信号SH/LD、一根时钟CLK、一根数据Q7或SO。先看引脚。D0到D7是并行数据输入接你要检测的按键、开关、传感器数字输出都可以输入高低电平就行。Q7是串行数据输出在时钟上升沿到来时寄存器里的数据会一位一位从这里出来。CLK是时钟输入每一个上升沿移出一位数据。SH/LD是移位/加载控制引脚这个引脚拉低时D0-D7的数据被锁存进内部寄存器拉高时芯片进入移位模式数据开始从Q7串行移出。还有一个Q7非输出是Q7的反相输出用不上的话可以直接不接。CE是时钟使能接低电平表示时钟总有效简化控制。这一堆引脚听起来复杂但实际连接非常简单。锁存信号和时钟各占一个GPIO数据读取占一个GPIO三个引脚搞定。如果你用的是STM32的SPI外设理论上也可以复用硬件时钟但大部分场景GPIO模拟时序更灵活不易受引脚复用冲突影响。2.2 时序图拆解锁存、移位、读取三步走74HC165的核心操作只有两个加载Load和移位Shift。加载阶段把SH/LD引脚拉低这时候D0到D7的当前电平状态会全部被快照进内部寄存器。注意这里是“快照”也就是说之后你不断改变D0-D7的电平寄存器里的数据不会跟着变直到下一次SH/LD拉低。这个特性非常重要它意味着在读取期间输入信号的变化不会导致数据错位。移位阶段把SH/LD拉高芯片回到移位模式然后在CLK引脚上给脉冲。每来一个时钟上升沿内部寄存器里的数据就向右移动一位Q7引脚上输出的是当前最高位的数据。8个时钟脉冲之后一个8位字节的数据就全部从Q7引脚上按顺序移出来了。第一个时钟沿读出的是Q7对应的那一位也就是内部寄存器的最高位这个位序关系后面写代码时要特别留意。如果有多片芯片级联Q7引脚接到下一片74HC165的串行输入DS引脚所有芯片的CLK和SH/LD并联这样一片一片串下去读回来的数据就是按级联顺序拼接的字节数组。级联不会增加控制引脚的数量只需要占用一个额外的数据输入引脚。2.3 为什么用74HC165而不是直接扩IO或上I2C常见的方案还有I2C的PCF8574、SPI的MCP23S17以及直接换更大封装的MCU。各有优点但74HC165在特定场景下更合适。首选场景是输入信号本身是开关量的、不需要双向通信的地方。按键、拨码开关、限位开关、拨轮编码器这些信号方向是固定的要么输入要么输出不需要配置方向。74HC165不需要地址配置不用考虑总线地址冲突接线少了地址引脚和方向引脚简单直接。与I2C和SPI方案比74HC165的实时性更好。I2C有地址帧、数据帧、应答位一个字节读回来要十几条指令74HC165直接拉GPIO时序完全是程序控制的想快想慢都行。而且GPIO模拟时序不受I2C总线速率限制系统时钟高的时候能轻松跑到几十万次每秒的读取频率对于扫描按键这种需求绰绰有余。与直接换MCU比74HC165的成本优势就很明显了。一颗芯片几毛钱16个输入也才两颗比换更高引脚的MCU省得多而且改板工作量小老设计里加一个芯片比换主控现实得多。3. STM32接入与驱动代码三根线扫八路输入从初始化到按键状态解析3.1 硬件接线最小系统之外只需三根线STM32与74HC165的接线是这套方案里最简单的部分。以STM32F103系列为例用GPIOA的PIN0、PIN1、PIN2分别接SH/LD、CLK、Q7电源和地接好就行。| 74HC165引脚 | STM32 GPIO | 说明 | ||---|---|---| | SH/LD (第1脚) | PA0 | 锁存控制拉低锁存输入 | | CLK (第2脚) | PA1 | 移位时钟上升沿移出数据 | | Q7 (第9脚) | PA2 | 串行数据输入到STM32 | | VCC (第16脚) | 3.3V | 与STM32同电源域 | | GND (第8脚) | GND | 共地 | | CE (第15脚) | GND | 直接拉低使能时钟 | | D0-D7 | 按键/开关 | 每个引脚接一个输入信号 |提醒一点74HC165的供电电压范围是2V到6V3.3V供电完全没有问题直接和STM32共用电源即可不需要电平转换。CE引脚一定要接低电平否则时钟信号内部被屏蔽移出来的数据全是乱的。有些人第一次用会忽略这个引脚结果读回来的数据完全不对头排查半天发现CE悬空了。3.2 驱动代码底层读取函数和按键状态解析先把驱动框架搭起来。这里采用STM32标准外设库风格写GPIO初始化新的HAL库逻辑也一样只是函数名不同。// 74HC165引脚定义移植时只需要改这三个宏 #define HC165_LD_PIN GPIO_Pin_0 #define HC165_LD_PORT GPIOA #define HC165_CLK_PIN GPIO_Pin_1 #define HC165_CLK_PORT GPIOA #define HC165_DATA_PIN GPIO_Pin_2 #define HC165_DATA_PORT GPIOA // GPIO初始化三个引脚都配置为推挽输出数据引脚配置为浮空输入 void HC165_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; // 打开GPIOA时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // SH/LD引脚推挽输出频率50MHz GPIO_InitStructure.GPIO_Pin HC165_LD_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(HC165_LD_PORT, GPIO_InitStructure); // CLK引脚推挽输出默认低电平 GPIO_InitStructure.GPIO_Pin HC165_CLK_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(HC165_CLK_PORT, GPIO_InitStructure); // 数据引脚浮空输入 GPIO_InitStructure.GPIO_Pin HC165_DATA_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(HC165_DATA_PORT, GPIO_InitStructure); // 初始状态锁存引脚拉高时钟引脚拉低 GPIO_SetBits(HC165_LD_PORT, HC165_LD_PIN); GPIO_ResetBits(HC165_CLK_PORT, HC165_CLK_PIN); }GPIO初始化里最需要注意的是数据引脚的模式必须是输入不能复用为推挽输出否则会把Q7引脚的电平强制拉高读回来的永远是一串高电平。SH/LD初始为高电平让芯片处于移位模式但这个时候还没开始移位时钟保持低不让数据提前输出。然后是读取一个字节的底层函数。74HC165有8位数据需要8个时钟脉冲才能全部移出。每个脉冲的上升沿Q7引脚上会依次出现D7到D0的数据位。这个顺序必须清楚D7对应第一个时钟沿的输出D0对应最后一个。// 读取一整字节的数据返回值的高位在前bit7是D7 uint8_t HC165_ReadByte(void) { uint8_t value 0; uint8_t i; // 第一步拉低锁存信号锁存D0-D7的当前电平 GPIO_ResetBits(HC165_LD_PORT, HC165_LD_PIN); // 保持一段短延时确保锁存完成 delay_us(5); // 拉高回到移位模式 GPIO_SetBits(HC165_LD_PORT, HC165_LD_PIN); // 第二步循环8次读取每个时钟上升沿的输出数据 for (i 0; i 8; i) { // 先将数据位左移腾出最低位空间 value 1; // 读Q7引脚的电平置入最低位 if (GPIO_ReadInputDataBit(HC165_DATA_PORT, HC165_DATA_PIN)) { value | 0x01; } // 产生时钟上升沿触发芯片内部移位 GPIO_SetBits(HC165_CLK_PORT, HC165_CLK_PIN); // 时钟高电平保持 delay_us(2); // 时钟拉低准备下一个上升沿 GPIO_ResetBits(HC165_CLK_PORT, HC165_CLK_PIN); // 时钟低电平保持 delay_us(2); } return value; }这段代码的逻辑很清楚先锁存输入再移出数据。每一步都有一个GPIO操作顺序不能乱。延时参数在没有特殊要求的情况下5微秒和2微秒都已经很富余了74HC165的时钟频率上限是几十兆赫兹GPIO模拟的速度远远达不到芯片极限这些延时主要是为了确保电平稳定和引脚状态完整变化不是硬性需求。这里有个细节值得注意初始化时时钟引脚已经是低电平进入循环后先读数据再拉高时钟这样读到的就是当前输出引脚的稳定电平。如果先拉时钟再读数据第一个位的读取时序就会错位读出来的字节整体移了一位。3.3 按键去抖和状态解析裸数据不能直接用直接读回来的字节每一位对应一个输入信号。但机械按键有个绕不开的问题抖动。按键按下和松开的瞬间电平会有几十毫秒的不稳定期如果不处理程序会误判成多次触发。常见的做法是延时消抖也就是检测到变化后等20毫秒再读一次确认。// 用延时的方式做按键消抖is_pressed是读取到的原始状态 uint8_t DebounceRead(uint8_t is_pressed) { // 第一次检测到低电平按下延时20ms再读一次 if (is_pressed 0) { delay_ms(20); // 再次读取74HC165同一通道的状态 return HC165_ReadByte()? (uint8_t)((HC165_ReadByte() (1 channel)) 0) : 0; } return 0; }这个去抖函数的思路是第一次读到0按下不立即确认等20毫秒再读一次如果还是0才是真按下。因为抖动造成的毛刺通常只有几毫秒20毫秒的延时足以把它过滤掉。要注意的是第二次读取需要重新调用HC165_ReadByte()因为锁存信号每次读取时都会重新锁存当前输入状态。如果只做按键扫描不用去抖直接把读回来的字节和上次读的字节做异或就能知道哪一位发生了变化但这样得到的上升沿和下降沿都算变化实际应用时还得再判断电平方向。3.4 把多片级联的读法扩展成数组一片74HC165只有8位实际项目里8个输入根本不够用级联是常态。级联的方法是把第一片Q7接到第二片的DS引脚两片的CLK和SH/LD并联数据线一根就够。读回来的时候第一片的数据先出来第二片的数据后出来代码上只需要把循环次数从8改成16、24或更多。// 通用级联读取函数buffer接收数据chip_count是芯片数量 void HC165_ReadMulti(uint8_t *buffer, uint8_t chip_count) { uint8_t i, j; uint16_t total_bits chip_count * 8; // 锁存所有芯片的输入 GPIO_ResetBits(HC165_LD_PORT, HC165_LD_PIN); delay_us(5); GPIO_SetBits(HC165_LD_PORT, HC165_LD_PIN); // 清零缓冲区 for (i 0; i chip_count; i) { buffer[i] 0; } // 移位读取所有数据位 for (j 0; j total_bits; j) { // 缓冲区按字节索引从最后一个字节开始移位因为第一片数据在最前面 uint8_t byte_index j / 8; uint8_t bit_index 7 - (j % 8); if (GPIO_ReadInputDataBit(HC165_DATA_PORT, HC165_DATA_PIN)) { buffer[chip_count - 1 - byte_index] | (1 bit_index); } // 产生时钟脉冲 GPIO_SetBits(HC165_CLK_PORT, HC165_CLK_PIN); delay_us(2); GPIO_ResetBits(HC165_CLK_PORT, HC165_CLK_PIN); delay_us(2); } }这个函数里稍微有点绕的是位序处理。级联时第一片的数据最先从Q7出来对应的是buffer的最高字节还是最低字节取决于你如何理解“最先出来”的含义。以两片级联为例第一个时钟沿输出的位是第一片Q7的D7第二个是D6依此类推。如果把两片看成整体第一片是高位字节。这里把第一片的数据放到了buffer[1]第二片的放到buffer[0]扫描按键时按通道1-8对应第一片、通道9-16对应第二片来用。4. 常见问题排查锁存失效、数据错位、时钟毛刺项目里真实遇到的四个坑4.1 数据全为0xFF按下按键后状态不变现象读取出来的数据一直是0xFF按键按下后没有反应拨码开关怎么拨都不变。原因排查最常见的是数据引脚配置错误。GPIO初始化时如果数据引脚配置成了推挽输出而不是浮空输入引脚电平被强制拉高Q7读回来永远是高电平。其次就是芯片供电出了问题74HC165没有上电数据引脚悬空读出来也是高。还有一种情况是CE引脚没接低电平芯片一直处于时钟屏蔽状态数据移不出来。解决先检查STM32的GPIO配置确认数据引脚是GPIO_Mode_IN_FLOATING。然后用万用表量芯片VCC和GND之间的电压确认供电正常。再确认CE引脚已经接GND。基本这三步能覆盖90%以上读不出数据的问题。4.2 数据错位按下第3个按键却变成第5位变化现象按键明明按在D2上读回来的字节却显示D4变了整个位序乱了。原因排查锁存时序不对。SH/LD拉低的时间太短输入信号还没稳定就被锁存了导致读回来的是之前的状态。另一个常见原因是时钟脉冲多给了或者少给了。比如GPIO模拟时钟的时候初始化状态下时钟本来就是高电平但代码里忘记在读取前把时钟拉低第一个时钟脉冲的前一个下降沿就可能造成数据提前移位。解决把SH/LD拉低后的延时拉长到10微秒以上给芯片足够的锁存时间。锁存完成拉高后再延时个1-2微秒让内部寄存器数据稳定再开始时钟。时钟引脚要确保从低电平开始每产生一个脉冲后务必拉低不能在循环结束时停留在高电平。4.3 读取结果偶尔出现一位跳变用示波器看波形正常现象连续读10次有1-2次数据里出现一个错误的位但示波器看CLK和Q7的波形没什么异常。原因排查这是典型的GPIO读取时序竞争问题。STM32的GPIO读操作在时钟上升沿附近采样时如果Q7的电平恰好发生跳变采样点落在电平变化的窗口期读到的值就不稳定。说白了就是读取时刻和芯片输出时刻贴太近存在竞争风险。解决把读取数据放在时钟上升沿之前完成。也就是先读Q7引脚电平再拉高时钟这样每次读到的都是上一个时钟沿之后已经稳定的电平。这个顺序问题在代码里很隐蔽因为看起来只是两条语句换一下位置但时序上差别很大。如果还是偶发错误把时钟高电平延时从2微秒加到5微秒给数据引脚更多的稳定时间。4.4 级联读回来的数据第二位芯片的数据总是少一位现象两片级联第二片的数据读回来整体左移或右移了一位第一位和第三位的电平串到了第二位上。原因排查级联接法错了。第二片的数据输入应该接上一片的Q7不是接上一片的Q7非。Q7非是反相输出接错的话数据和位序全反了。另外级联时第二片的CE也必须拉低如果悬空第二片不会参与移位。解决检查第二片的DS引脚接的是第一片的Q7第9脚还是Q7非第10脚。把万用表打到通断档顺着线排查一遍。确认两片的CE都接GND。级联时CLK和SH/LD都是并联的不能串接。5. 从驱动到应用把74HC165封装成可复用的按键模块附调试技巧驱动代码写完只是第一步真正好用还要封装成模块。这里说一个我自己一直在用的封装思路把74HC165的按键通道抽象成事件而不是暴露原始寄存器值。typedef struct { uint8_t current_state; // 当前扫描到的状态 uint8_t prev_state; // 上次扫描的状态 uint8_t changed; // 状态变化标志 } KeyChannel_t; // 每次调用更新所有通道返回变化的事件数 uint8_t KeyScan_Update(KeyChannel_t *channels, uint8_t channel_count) { uint8_t event_count 0; uint8_t i; uint8_t new_state[4] {0}; // 读取级联的74HC165数据 HC165_ReadMulti(new_state, (channel_count 7) / 8); // 逐通道比较状态 for (i 0; i channel_count; i) { uint8_t new_bit (new_state[i / 8] (i % 8)) 0x01; uint8_t old_bit (channels[i].prev_state (i % 8)) 0x01; channels[i].current_state new_bit; channels[i].changed (new_bit ! old_bit); if (channels[i].changed) { event_count; } } // 更新prev_state供下次比较 for (i 0; i channel_count; i) { channels[i].prev_state channels[i].current_state; } return event_count; }这个结构的价值在哪做过多路扫描的人一眼就能看出来它把“读数据”和“用数据”彻底分开。上层逻辑只关心某个通道变了没有不用管底层是74HC165还是别的芯片。以后要换成PCF8574或者直接读MCU的GPIO只需要改HC165_ReadMulti这一层就行上层代码一行不用动。调试技巧方面最实用的是用GPIO翻转法看时序。在锁存操作前把一个空闲GPIO拉高读取结束后拉低用示波器或者逻辑分析仪看这个GPIO的波形宽度就能精确知道一次完整读取耗时多少。如果扫描周期太慢导致按键响应迟钝优化延时参数时就有据可依。还有一个常见的高级需求长按和短按的区分。这在74HC165场景下也很容易实现只需要在通道结构体里加一个计数器字段每次扫描时如果通道状态为按下就累加计数超过阈值判定长按。因为扫描周期固定计数器乘扫描周期就是按住的时间。这套资源里附带的代码还有一点做得好的地方数据位和物理通道的映射关系在头文件里用宏定义统一管理。实际项目里PCB布线不可能完全按照D0到D7的顺序走线定义一套宏把“逻辑通道0”映射到“物理D3引脚”可以避免在业务代码里到处写魔数。从那次之后我每次拿到新的输入扩展芯片都会先花十分钟用GPIO翻转法和示波器确认时序确认无误再写业务逻辑。这个习惯帮我省下了很多排查时间。希望这份74HC165的驱动经验能帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/21 22:16:29

LangGraph实战:构建多Agent分析报告平台

去年年中,我接到一个让我头疼了大半个月的需求:团队攒了一大批零散的调研素材,希望系统能在十分钟内把它变成一份多维度的分析报告——要包含市场判断、技术路线评估和潜在风险提示。一开始,我用了最直觉的方案:把全部…

2026/10/21 22:16:29

从零构建可复用的Skill体系:设计、编排与避坑指南

1. 从“skills”这个标题说起:一个被严重低估的硬核话题“skills”这个词,乍一看平平无奇,甚至有点太泛了。但如果你在技术社区、职场论坛或者个人成长圈子里泡过一段时间,就会发现这个词最近被反复提起的频率高得离谱。有人把它翻…

2026/10/22 0:01:49

rea项目极简架构实战:响应式与实时系统的设计、实现与避坑指南

1. 从“rea”这个标题说起:一个被低估的通用缩写第一次看到“rea”这个标题的时候,我脑子里蹦出来的第一反应是——这大概率又是一个被缩写玩坏的项目名。在技术圈混久了你会发现,越是短到只有三个字母的标题,背后藏的东西往往越不…

2026/10/22 0:01:49

大数据人才需求分析毕设实践:从招聘数据到可视化系统

拿到《基于大数据专业岗位招聘信息的人才需求特征分析》这个毕设题目的时候,我第一反应是:终于有个既贴热点、又好落地的方向了。但等我认真帮人搭完一遍完整流程后,发现这个题目远没有看上去那么简单——很多人把它做成了“爬了几千条招聘数…

2026/10/22 0:01:49

若依框架AI代码生成不稳定?用Agent Skill三层结构+五步法搞定

最近一段时间,我一直想把后台管理系统里那些机械重复的增删改查代码交给AI去写。框架用的是若依,按理说这类业务代码模式非常固定,应该是AI最擅长的事情。可真试下来却不是那么回事——你让AI生成一个用户管理模块,它给出的Contro…

2026/10/22 0:01:49

电商智能运营体系从0到1:架构设计、落地步骤与避坑指南

前两年我们团队接了一个电商平台的运营中台项目,目标很直白:把过去靠运营拍脑袋做的选品、定价、发券、推活动这些事,逐步用数据算法和自动化流程代替。项目名字内部就叫“电商智能运营体系”。最近有不少朋友问我当时是怎么设计的、踩过哪些…

2026/10/21 23:56:48

从零搭建可复用能力体系:skills设计、编排与避坑指南

1. 从“skills”这个词说起:它为什么值得单独拎出来聊“skills”这个词,放在任何一个行业里都不算新鲜。但如果你最近在技术社区、产品圈或者职场分享里频繁看到它被单独拿出来讨论,那大概率不是指泛泛的“技能”概念,而是指向一个…

2026/10/21 4:53:08

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/22 0:01:49

电商智能运营体系从0到1:架构设计、落地步骤与避坑指南

前两年我们团队接了一个电商平台的运营中台项目,目标很直白:把过去靠运营拍脑袋做的选品、定价、发券、推活动这些事,逐步用数据算法和自动化流程代替。项目名字内部就叫“电商智能运营体系”。最近有不少朋友问我当时是怎么设计的、踩过哪些…

2026/10/22 0:01:49

若依框架AI代码生成不稳定?用Agent Skill三层结构+五步法搞定

最近一段时间,我一直想把后台管理系统里那些机械重复的增删改查代码交给AI去写。框架用的是若依,按理说这类业务代码模式非常固定,应该是AI最擅长的事情。可真试下来却不是那么回事——你让AI生成一个用户管理模块,它给出的Contro…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑