ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

74HC165驱动详解:3个GPIO扩展8路输入,级联与SPI读取实战

2026/9/9 1:49:05 拓冰建站 浏览量
74HC165驱动详解:3个GPIO扩展8路输入,级联与SPI读取实战 简介面向STM32开发者的74HC165并行转串行移位寄存器驱动模块适用于GPIO口不足、需要扩展多路输入的场合典型应用包括矩阵键盘、多位数码管显示以及多通道数据采集。压缩包体积仅为1KB包含74HC165.c和74HC165.h两个文件不夹带多余工程文件代码简洁可直接加入现有STM32工程。驱动通过GPIO模拟时序完成并行输入捕获和串行移位输出已封装初始化、数据读取等核心函数同时将关键引脚以宏定义方式开放便于用户根据开发板调整接线。目前已有722人学习下载。这套代码将74HC165的繁琐时序处理集中处理使用者只需调用接口即可读取8路并行输入如果采用多片级联也能沿用相同逻辑进一步扩展为16路、32路并行采样大幅节省底层调试时间适合需要快速验证硬件或构建外设轮询逻辑的STM32项目。 如果你是在一个嵌入式项目里搜到“74HC165 驱动”这几个字的八成是遇到了同一个问题开关、按键、限位开关、编码器加起来一大堆而主控芯片的输入口不够用了。你从某个网盘里下载了一份74HC165驱动.rar解压之后发现里面有一个 .c、一个 .h、一个不知道哪个版本的工程文件还有一个写得像没写一样的 README。我见过不少人卡在这一步——文件就在手边却不知道从哪里改起。这篇文章不评价某个具体驱动包写得好不好而是把这一类驱动背后的东西讲透74HC165 到底怎么工作、驱动代码每行在干嘛、接线上有哪些隐藏的雷、级联多片时字节顺序怎么处理。如果你在玩 Arduino、STM32 或 ESP32想用 3 根 GPIO 读回 8 路甚至几十路开关量这篇可以直接照着操作。1. 解压这份“74HC165驱动.rar”之前先搞清楚它在解决什么问题1.1 输入口不够用一次让工程师反复纠结的 IO 算账我遇到过很典型的一回一个控制盒里有 8 个功能按键、2 个旋转编码器、3 个限位开关加起来 17 个输入点而手头主控板空闲 GPIO 只剩 6 个。这种尴尬在低成本板子上太常见了尤其是用 STM32F103C8T6、ESP32、Arduino Nano 这类引脚并不富余的板子时。遇到这种情况普通人第一反应是换大芯片。但换主控意味着重新设计 PCB、重铺引脚映射、改底层初始化工作量一下大好几倍。第二个常见思路是矩阵键盘但矩阵只适合规则排列的按键限位开关、编码器、拨码开关这些信号根本不可能按矩阵方式扫描。最合适的手段其实是外扩输入用 74HC165 这种“并行输入、串行输出”的移位寄存器一次扩展 8 路输入而且只需要 3 根 MCU 引脚。多片级联后3 根线能带回来的输入数量几乎是无限的成本也就几毛钱一片。所以你在网上下载“74HC165驱动.rar”本质不是为了那几行代码而是为了省下换主控的麻烦。明白这一点就知道后面的重点该放在哪里。1.2 rar 里真正值钱的不是文件而是时序思路这类压缩包我下载过不少文件名可能叫 “74HC165库”、“shiftIn测试工程”、“并转串读取Demo”解压后的内容大同小异。通常结构是一个74HC165.h/74HC165.c封装了初始化函数和读取函数一个主程序 demo通常是读 8 个按键然后通过串口打印可能有几份不同平台的移植版本比如 Arduino 的和 STM32 的。但说实话这份压缩包里真正核心的逻辑只有十几行剩下的都是工程外壳。而且很多网上流传的驱动包质量参差不齐有的不支持级联有的读回来位序是反的有的甚至用阻塞式 delay 做时序搞到主程序卡顿。我不建议你拿到手就直接塞进工程。先花十分钟把时序搞清楚再决定是改它的参数、还是干脆自己写一个。等下第 5 节我会给一个完整的极简版本看完你就知道为什么我说“最靠谱的驱动是自己写的”。1.3 此“驱动”非彼“驱动”别和串口驱动软件混在一起这里必须澄清一个容易踩的坑74HC165 的“驱动”指的是单片机固件层的驱动代码也就是让你 MCU 能读取这颗芯片数据的库文件不是电脑上那种 CH340 串口驱动、CP2102 USB 转串口驱动或者显卡驱动软件。在论坛和搜索引擎里“驱动”这两个字很容易把各种东西混到一块。还有像 TB6612、L293D、H 桥电机驱动模块这类完全不同的“驱动芯片”也会在搜索结果里一起冒出来。搜资料的时候先看清是不是同一类东西不然很容易下载一堆没用的文件浪费时间。2. 看懂74HC165的时序代码才有地方落脚2.1 拍完合影再报数并行装载和串行移位74HC165 的全称叫“8 位并行输入/串行输出移位寄存器”理解它有个很好的生活类比一群人站成一排先拍一张照片然后摄影师挨个问“你叫什么”每问一次队伍里最右边那个人回答其他人依次往右挪一位。这个流程就是 74HC165 的全部工作方式。“拍照片”这个动作对应的是并行装载。74HC165 有 8 个并行输入脚 D0~D7每个脚可以接一个按键或开关信号。当 PL并行装载控制脚被拉低时这 8 个引脚上的电平会瞬间被打进芯片内部的 8 个寄存器里相当于把当时所有人的状态冻结下来。为什么要先冻结因为你准备用时钟一位一位把数据移出来整个过程需要一定时间。如果读取过程中某个按键刚好按下或松开寄存器里的数据就会“串味”读出来的结果既不是之前的状态也不是之后的状态。先锁存再慢慢移出就能保证读到的是某一瞬间的完整快照。2.2 PL、CP、CE、Q7四个引脚把时序演一遍74HC165 常用引脚里真正需要 MCU 控制的只有 4 个引脚名称作用PLSH/LD并行装载控制低电平有效。拉低时锁存输入CPCLK移位时钟上升沿/下降沿触发数据移动CECLK INH时钟使能低电平有效。不用时直接接地Q7QH串行输出数据从这里一位一位送出去完整读一次的时序是先把 PL 拉低并保持一小段时间让 8 个并行输入可靠锁存然后把 PL 拉高芯片进入移位模式接着把 CE 固定接 GND在 CP 引脚上连续送出 8 个时钟脉冲。每来一个脉冲Q7 引脚上就出现一个数据位MCU 在合适的时刻把 Q7 的电平读进来。有一点需要特别注意第一次读 Q7 的时机。典型实现是 PL 拉高之后、送出时钟脉冲之前先把 Q7 读一次这一位对应的是 D7 的状态然后每送一个脉冲读一次依次读到 D6、D5……直到 D0。如果你用 SPI 硬件读采样点由 SPI 模式决定这个坑我放到第 3 节细说。2.3 位序和时钟极性两个常见的小陷阱74HC165 的 Q7 最先输出的其实是 D7 这一位不是 D0。这意味着如果你在代码里用value | digitalRead(Q7) i循环i 必须从 7 递减到 0这样读出来的 bit7 才对应 D7、bit0 对应 D0。如果循环方向反了读出来就是一个左右镜像的数据按键 1 按下去代码里却看到 bit7 变化。时钟极性是另一个经典问题。74HC165 手册上通常写的是在 CP 上升沿移位但实际项目中用 GPIO 模拟和用硬件 SPI 是两套逻辑。GPIO 模拟时由于你拉高拉低电平之间有足够的时间让输出稳定只要保证“读”和“给脉冲”的顺序固定一般都能工作。但用 SPI 读取时如果 SPI 的采样点落在芯片数据刚好跳变的瞬间就可能读到旧值或者不确定值。我实测下来74HC165 和 STM32 硬件 SPI 搭配时SPI_MODE1下降沿采样比 SPI_MODE0 更稳具体原因后面展开。3. 最小接线和read8函数用三根线换回8个输入3.1 Arduino 与 STM32 最小系统接线对照先给一张可以直接照抄的接线表。这里以读取 8 个独立按键为例按键一端接 D0~D7另一端接 GND启用内部上拉或者外部接上拉电阻。这样按键按下时读到 0松开时读到 1。74HC165 引脚Arduino 引脚STM32 引脚示例说明VCC5V/3.3V3.3V电源注意电平匹配GNDGNDGND共地PLD8PA4并行装载控制低有效CPD9PA5移位时钟CEGNDGND时钟使能低有效不用时直接接地Q7D10PA6串行输出到 MCUD0~D7按键/开关按键/开关并行输入信号DSGNDGND串行输入单芯片不级联时接地防悬空这里有两个细节容易忽略。第一74HC 系列芯片在 3.3V 供电下完全能工作高电平阈值大约是 2.1VSTM32 的 3.3V 输出足够驱动它。第二PL 引脚千万不能随手接地或者悬空。如果 PL 一直为低芯片会一直处于并行装载状态时钟来了也不移位读出来的数据永远是同一份快照你会以为芯片坏了。3.2 read8 函数的每一行到底在干什么这是我自己最常用的单芯片读取代码基于 Arduino 的 GPIO 模拟但逻辑同样适用于 STM32#define PL_PIN 8 #define CLK_PIN 9 #define Q7_PIN 10 uint8_t read165_chip() { uint8_t value 0; // 1. 并行装载拉低 PL锁存 D0~D7 当前电平 digitalWrite(PL_PIN, LOW); digitalWrite(PL_PIN, HIGH); // 2. 先读一次 Q7此时对应 D7 // 3. 循环读出剩余 7 位每读完一位就送一个时钟脉冲 for (int i 7; i 0; i--) { value | (digitalRead(Q7_PIN) i); digitalWrite(CLK_PIN, HIGH); digitalWrite(CLK_PIN, LOW); } return value; }这段代码理解起来并不难。第一步的 PL 拉低再拉高就是前面说的“拍照片”锁存当前输入。第二步进入循环i 从 7 开始意味着第一个读到的位被放到最高位。每次循环先读 Q7再给一个时钟脉冲让下一位数据移动到 Q7。循环 8 次之后value 里就是 8 个按键的完整状态。如果你觉得手动移位移容易错Arduino 也内置了shiftIn函数uint8_t read165_shiftin() { digitalWrite(PL_PIN, LOW); digitalWrite(PL_PIN, HIGH); return shiftIn(Q7_PIN, CLK_PIN, MSBFIRST); }用 shiftIn 时要特别注意 MSBFIRST 这个参数它表示先读高位。因为 74HC165 输出顺序是高位在前所以必须用 MSBFIRST。你要是用了 LSBFIRST按 1 号键读到 bit0按键 8 读到 bit7功能没错但后面数据处理很容易搞混。3.3 从 GPIO 模拟到硬件 SPI提速与资源权衡GPIO 模拟的好处是引脚随意、代码直观缺点是每个脉冲都要用几条语句去翻转电平速度和 CPU 占用都不理想。如果你的系统里还有大量其他任务建议改用硬件 SPI 来读取。接线方式变化不大Q7 接 MCU 的 MISOCP 接 MCU 的 SCKPL 仍然用普通 GPIO 控制相当于片选信号。代码长这样#include SPI.h void read165_spi(uint8_t *buf, uint8_t chipCount) { SPI.beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE1)); digitalWrite(PL_PIN, LOW); digitalWrite(PL_PIN, HIGH); for (uint8_t i 0; i chipCount; i) { buf[i] SPI.transfer(0x00); // 每 transfer 一次就是 8 个时钟 } SPI.endTransaction(); }这里我特意用了 SPI_MODE1 而不是默认的 SPI_MODE0是因为 74HC165 在时钟上升沿移位而 SPI_MODE0 的采样点也在上升沿MCU 采样时芯片输出可能刚好在跳变会读到不稳定数据。改成 SPI_MODE1 后采样点移到下降沿这时芯片输出已经完全稳定。实测下来这个组合从来没问题。如果你改成 SPI_MODE0 也能读对说明你的 MCU 的采样时序恰好错开了但保险起见还是推荐 MODE1。4. 实测排错记录全0xFF、镜像数据、级联乱序的根因4.1 一张现象对照表全 FF、全 00、镜像分别是谁的锅我踩过的坑比你想象的多。以下这张表是从实际项目中总结出来的可以直接用来排查问题现象常见原因处理办法读到的字节全是 0xFF输入脚悬空读到高电平按键按下没有把端口拉低PL 没有产生有效低电平检查 D0~D7 电平变化用万用表量 PL 在读取瞬间是否出现低电平读到的字节全是 0x00数据引脚短接到 GNDQ7 引脚接错MCU 数据脚被配置成输出模式先松开所有按键再读检查 Q7 连接和引脚模式数据左右镜像位序处理反了确认循环方向是 7→0 而不是 0→7或者 MSBFIRST 改成 LSBFIRST某一位随机跳变按键抖动或输入悬空加外部上拉/下拉软件读多次取中间值消抖时钟不起作用CE 悬空被噪声拉高时钟被禁止CE 必须可靠接地不要悬空其中有一个隐藏原因值得单独点名PL 引脚如果只接了普通 IO代码里却忘了在读取前操作它或者初始化时把它设成了高电平会导致 74HC165 永远处于移位模式读回来的数据永远是上电瞬间锁存的那一组旧数据。这种问题特别难发现因为代码看着没问题数据也稳定但就是不变。解决方法是初始化时先把 PL 拉高然后每次读取前拉低再拉高顺序别反。4.2 级联两片以上时字节顺序和脉冲数量要算清楚一个 74HC165 只有 8 路输入如果不够用最简单的方法是级联。级联的接线方式第一片离 MCU 远的那片的 Q7 接到第二片离 MCU 近的那片的 DS 串行输入脚所有芯片的 PL 和 CP 并联在一起MCU 只从靠近自己的那一片 Q7 读取数据。级联后第一个要算清的是脉冲数量。两片级联就需要读 16 位单片驱动里那种只给 8 个脉冲的代码必须改否则数据根本传不过来。正确写法是循环读 2 次每次内部再循环 8 次总共 16 个时钟脉冲。第二个要算清的是字节顺序。我在第 3 节的 GPIO 模拟代码里采用的是“先读 Q7 再送时钟”的顺序这种顺序下 buf[0] 对应的是离 MCU 最近的那一片buf[1] 才是更远的那一片。如果用硬件 SPI 并采用的采样模式不同顺序可能反过来。所以遇到级联时别猜顺序直接串口打印出来对照按键位置确认一次就记住了。我习惯在代码里留个注释标明 buf[0] 是第几片避免过几天自己都忘了。级联还有一个容易忽略的点DS 引脚不能悬空。如果你只用了两片中的一片或者级联线上有接触不良DS 悬空会导致串进去的全是噪声读出来高四位全乱。电路板打样回来如果发现某一片数据全乱先拿万用表量 DS 到前级 Q7 的通断。4.3 和74HC595输出扩展混用时的顺序陷阱很多项目做输入扩展的同时也会做输出扩展于是 74HC165 和 74HC595 同时出现在一块板上。74HC595 是“串行输入、并行输出”和 165 是一对互补品经常一起用但它们的锁存控制逻辑正好相反74HC165先锁存输入再移位输出。74HC595先移位输入再锁存输出。如果两个芯片共用同一条 SCK 时钟线很容易出问题。595 的 RCLK输出锁存必须在所有移位结束后再拉高让输出一次性更新而 165 的 PL输入锁存必须在移位开始前拉低把输入冻结。顺序写反了你 595 控制的 LED 会无规律闪烁165 读到的按键状态则永远是旧值。这两个锁存信号最好分成两个独立 GPIO 控制名字也别混代码里一个是latch_out一个是load_in一眼能分清。5. 与其到处找通用驱动不如自己写一个15行的版本5.1 通用库的抽象往往没有自己 10 行代码顺手网上的通用驱动库为了适配不同平台和引脚通常会有大量条件编译、回调函数、动态分配内存的代码。看头文件就头大。而且你真正需要的功能可能只占库的 10%剩下 90% 都是你永远用不到的特性。更关键的是通用库为了兼容性往往会牺牲性能和可读性。有些库甚至有一个隐藏 bug被设计成只适配单芯片级联时根本不工作。所以我的建议是把时序原理搞懂之后直接自己封装一个按自己项目的引脚、位序、级联方式定制比找任何现成的“驱动.rar”都靠谱。5.2 支持多片级联的极简驱动长什么样下面这个版本是我每次新项目都会用到的模板代码只有十几行支持 1 到 N 片级联void read165(uint8_t *buf, uint8_t chipCount) { // 并行装载所有芯片 digitalWrite(PL_PIN, LOW); digitalWrite(PL_PIN, HIGH); for (uint8_t c 0; c chipCount; c) { uint8_t v 0; for (int8_t i 7; i 0; i--) { v | (digitalRead(Q7_PIN) i); digitalWrite(CLK_PIN, HIGH); digitalWrite(CLK_PIN, LOW); } buf[c] v; } }移植到 STM32 时只需要把 GPIO 操作替换成 HAL 函数或者寄存器操作v | (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_6) i); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET);这个版本最大的好处是逻辑一目了然外层循环切芯片内层循环读 8 位位数和时钟脉冲数量严格对应不会出现级联时少一个脉冲的隐性 bug。你还可以把读到的字节映射成 16 位、32 位变量直接丢给上层逻辑用。5.3 别只用来读按键编码器、矩阵开关、工业输入其实都能读74HC165 的价值远远不止读几个按键。我见过有人用它批量读旋转编码器每个编码器的 A、B 相占两个输入脚一片 165 可以接 4 个编码器3 根线全部搞定比单独给每个编码器分配中断引脚效率高多了。矩阵键盘也能和 165 结合。常规矩阵扫描需要不断翻转行列引脚但如果你把每一列的输入直接接到 165 的并行输入脚扫描时只需要控制行线和读取 165轮询速度和稳定性都很好代码也更干净。在工业设备上它更适合作为“输入端扩展器”行程开关、光电开关、接近开关、急停按钮这些信号基本只有高低电平两种状态完全不需要高速采样。用 74HC165 把几十个开关量汇集成 3 根线传到主控板上布线工作量能少一大截。需要注意的是如果现场有强电干扰输入端要加光耦隔离74HC165 本身不提供任何隔离保护。我自己现在的习惯是一旦发现 IO 紧张第一反应不是换主控而是先算一笔账——加一片 165 能不能解决如果只是多几个按键、几个传感器输入那答案通常是肯定的。这种方案成本低、逻辑简单还能少改一次板子比折腾主控替换划算太多。本文还有配套的精品资源点击获取