资讯动态

74HC165驱动详解:3根IO扩展16路按键与STM32实现

发布时间:2026/10/29 14:28:58 来源:尧图企业网站定制
简介这份资源面向使用STM32等微控制器进行I/O口扩展的嵌入式开发者聚焦74HC165并行转串行移位寄存器的驱动实现帮助解决GPIO资源不足时连接更多外部设备的问题。压缩包共2个文件包含1个C源文件与1个头文件分别承载驱动逻辑与接口声明整体约1KB体量轻便便于直接加入现有工程。已有738人学习下载具备一定的参考基础。内容围绕GPIO初始化、串行时钟与移位使能时序控制、数据串行输入、清零与保持操作以及并行数据读取等环节展开可应用于矩阵键盘、多位数码管显示、多通道数据采集等场景。读者可据此快速理解74HC165与STM32的接口设计思路获得可复用的驱动代码框架并在此基础上按实际时序需求修改优化提升系统扩展能力。1. 74HC165驱动一块并转串芯片怎么把16路按键塞进3根IO手上有个板子要读16个拨码开关MCU的IO已经排得满满当当再占16个脚基本没戏。这种场景下74HC165就是那个“后悔药”——8位并行输入转串行输出两片级联就能把16路状态压到3根线上时钟、锁存、数据。这份名为“74HC165驱动.rar”的资源核心就是围绕STM32把这块芯片的时序跑通包含驱动代码和配套的读写逻辑。它适合正在做按键矩阵、拨码开关采集、IO扩展的嵌入式从业者尤其是被IO数量卡住、又不想上I2C扩展芯片的人。下面按“芯片怎么工作→代码怎么落地→坑在哪”的顺序拆开讲参数和时序都能直接抄。2. 74HC165的时序骨架先锁存再移位别把顺序搞反74HC165本质是一个8位并行输入的移位寄存器工作分两步锁存Load和移位Clock。理解这两步的先后关系是驱动能不能跑通的分水岭。很多人第一次写驱动翻车不是代码语法错而是把“先给时钟再锁存”当成了默认动作结果读回来的永远是上一次的旧数据。2.1 三个控制脚各自管什么芯片上跟MCU打交道的信号线主要有四根驱动里必须一一对应引脚方向作用常见接法PLSH/LDMCU→芯片低电平锁存并行数据高电平进入移位GPIO推挽输出默认拉高CPCLKMCU→芯片移位时钟上升沿移出一位GPIO推挽输出默认低Q7QH芯片→MCU串行数据输出GPIO输入上拉或浮空Q7QH芯片→下级级联输出接下一片Q7仅级联时用PL是低有效这一点必须记牢拉低的那一瞬间8个并行输入被“拍照”进寄存器拉高之后每来一个时钟上升沿数据就从Q7往外吐一位。所以正确的动作序列是“PL拉低→PL拉高→再打时钟”顺序反了读到的就是移位寄存器里的残留值。我一般会在初始化时把PL置高、CP置低让芯片处于待命状态避免上电瞬间误锁存。2.2 级联时数据是怎么排队的两片74HC165级联第一片的Q7接第二片的Q7输入注意是串行输入脚不是并行脚MCU只读第一片的Q7。移位时第一片的8位先出去紧接着第二片的8位被“推”进第一片继续往外吐所以读16位要打16个时钟。这里有个容易忽略的点级联后第一个读出的位是最后一片的最高位还是最低位取决于你接线时Q7接的是哪一端。常见做法是把靠近MCU的那片当“高位片”读回来的16位里高8位来自它低8位来自远端片拼装时按这个约定移位就行。/* 读取两片级联的16位并行输入返回拼装后的值 */ uint16_t hc165_read16(void) { uint16_t data 0; uint8_t i; /* 1. 锁存PL拉低再拉高把16路并行状态拍进寄存器 */ HAL_GPIO_WritePin(HC165_PL_PORT, HC165_PL_PIN, GPIO_PIN_RESET); __NOP(); /* 给芯片一点建立时间几纳秒即可NOP足够 */ HAL_GPIO_WritePin(HC165_PL_PORT, HC165_PL_PIN, GPIO_PIN_SET); /* 2. 移位打16个时钟每个上升沿读一位 */ for (i 0; i 16; i) { data 1; /* 先左移给新来的位腾位置 */ HAL_GPIO_WritePin(HC165_CP_PORT, HC165_CP_PIN, GPIO_PIN_SET); __NOP(); if (HAL_GPIO_ReadPin(HC165_Q7_PORT, HC165_Q7_PIN) GPIO_PIN_SET) { data | 0x01; } HAL_GPIO_WritePin(HC165_CP_PORT, HC165_CP_PIN, GPIO_PIN_RESET); __NOP(); } return data; }这段代码的逻辑说明先执行锁存动作PL低脉冲把并行口状态装进移位寄存器然后循环16次每次先把CP拉高产生上升沿芯片在上升沿移出下一位紧接着读Q7再把CP拉低准备下一次。data 1放在读之前保证第一次读到的位最终落在最高位这样拼出来的16位和物理接线顺序一致。参数上__NOP()是给GPIO和芯片留的建立/保持余量STM32跑几十兆主频时一个NOP约几十纳秒对74HC165这种百纳秒级器件够用如果主频很高或走线很长可以换成几个NOP或短暂延时。注意读Q7要在CP拉高之后、拉低之前完成这个窗口是数据有效的区间。3. STM32上的驱动落地GPIO配置、读函数与消抖时序清楚了落到STM32上就是GPIO初始化和读函数两件事。这份驱动资源的价值在于把“能跑”的细节固定下来而不是让你每次重新推一遍时序。下面按初始化、读取、消抖三个层次展开参数都能直接改。3.1 GPIO初始化输出推挽、输入上拉的取舍PL和CP是MCU输出配推挽、低速即可74HC165对边沿速度没要求低速反而减少振铃。Q7是输入这里有个选择配上拉还是浮空。如果板上Q7外部已经有上拉电阻MCU内部上拉可以关掉如果没有建议开内部上拉防止芯片未上电或断线时读到随机电平。我一般默认开上拉多花的那点功耗在按键采集场景里可以忽略。/* 74HC165 GPIO初始化STM32 HAL库写法 */ void hc165_init(void) { GPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); /* 按实际端口改时钟 */ /* PL锁存控制推挽输出默认高电平不锁存 */ gpio.Pin HC165_PL_PIN; gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Pull GPIO_NOPULL; gpio.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(HC165_PL_PORT, gpio); HAL_GPIO_WritePin(HC165_PL_PORT, HC165_PL_PIN, GPIO_PIN_SET); /* CP移位时钟推挽输出默认低电平 */ gpio.Pin HC165_CP_PIN; HAL_GPIO_Init(HC165_CP_PORT, gpio); HAL_GPIO_WritePin(HC165_CP_PORT, HC165_CP_PIN, GPIO_PIN_RESET); /* Q7串行数据输入上拉防止悬空误读 */ gpio.Pin HC165_Q7_PIN; gpio.Mode GPIO_MODE_INPUT; gpio.Pull GPIO_PULLUP; HAL_GPIO_Init(HC165_Q7_PORT, gpio); }逻辑说明三个脚分三组配置PL和CP同为输出但默认电平相反——PL默认高不锁存CP默认低无时钟沿这个初始状态让芯片上电后处于“等锁存”的稳定态。Q7开内部上拉是关键细节很多板子为了省成本不焊外部上拉靠MCU内部上拉兜底读到的空闲电平才是确定的高。参数上GPIO_SPEED_FREQ_LOW对74HC165完全够别设成HIGH高速边沿在长排线上容易引入串扰。3.2 读函数与位映射物理开关怎么对上逻辑位读回来的16位是“移位顺序”不等于“物理引脚顺序”。假设第一片接开关1~8、第二片接9~16且第一片Q7接第二片输入那么读出的bit15对应开关1bit0对应开关16具体取决于接线端。这一步必须在代码里用宏或映射表固定下来否则后面判断按键时全是错位。常见做法是定义一个映射数组把逻辑位号映射到物理开关编号读完后统一转换。/* 把16位原始数据映射成开关状态bit1表示开关闭合按接线极性调整 */ #define SW_COUNT 16 static const uint8_t bit_map[SW_COUNT] { /* 逻辑开关0~15 对应的原始数据位号按实际接线填 */ 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 }; void hc165_get_switches(uint8_t *sw) { uint16_t raw hc165_read16(); uint8_t i; for (i 0; i SW_COUNT; i) { sw[i] (raw bit_map[i]) 0x01; } }逻辑说明bit_map把“第i个开关”对应到原始数据的哪一位接线一变只改这张表读函数不用动。(raw bit_map[i]) 0x01取出单个位赋给开关状态数组。参数上如果开关是低有效按下接地映射后取反即可或者在读函数里统一异或0xFFFF。这一步看着简单但实际项目里接线顺序和代码假设不一致是最高频的翻车点建议第一次调试时逐个短接开关、打印raw值把映射表核对一遍再往下走。3.3 软件消抖机械开关的抖动怎么滤74HC165读的是电平机械拨码开关或按键在动作瞬间会有几毫秒抖动直接读会得到一串跳变。常见做法是“定时采样连续一致才认”不需要复杂滤波。比如10ms读一次连续3次相同才更新状态对拨码开关足够按键则可以用20ms间隔、连续2次一致。这里给一个轻量的状态机写法不占太多RAM。/* 简易消抖每10ms调用一次连续3次一致才更新 */ typedef struct { uint8_t stable; /* 消抖后的稳定状态 */ uint8_t count; /* 连续一致计数 */ uint8_t last; /* 上次采样值 */ } debounce_t; void debounce_update(debounce_t *d, uint8_t sample) { if (sample d-last) { if (d-count 3) d-count; if (d-count 3) d-stable sample; } else { d-count 0; d-last sample; } }逻辑说明每次采样和上次比较相同则计数累加累计到3就把稳定状态更新为当前值不同则计数清零、记录新值重新开始。参数上3次×10ms30ms的确认窗口对拨码开关偏保守但安全按键可以降到2次。注意这个函数要放在定时器中断或主循环固定节拍里调用采样间隔不稳会让消抖窗口失真。如果开关数量多给每个开关配一个debounce_t实例即可16个也就几十字节。4. 避坑与排查读回全1、数据错位、级联丢位怎么查驱动写完后最常见的三类故障基本都能从“锁存时序、接线映射、时钟数量”三个方向定位。下面按现象→原因→解决列出来都是实际调试里反复遇到的。4.1 读回全是0xFF或全0现象不管开关怎么拨hc165_read16()返回恒定值。原因通常是Q7输入脚配置错比如误配成输出、PL没有产生低脉冲一直高或一直低、或者芯片供电/接地没接好。解决先用万用表量Q7在打时钟时是否有高低变化没有就查PL是否真的拉低过——用示波器看PL引脚确认有一个低电平窄脉冲再确认Q7的GPIO模式是输入且上拉使能。如果Q7恒定高多半是芯片没锁存成功PL时序是首要嫌疑。4.2 数据整体错一位或高低8位颠倒现象读到的值和预期差一个移位或者开关1~8和9~16对调。原因一是读循环里data 1和读Q7的先后顺序写反导致首位丢失二是级联时两片的Q7接线端搞反高低字节顺序颠倒。解决先单片刻试只接8个开关确认单片的位序正确再上两片用已知状态比如全拨到ON读raw值看是0xFFFF还是0x00FF据此判断是移位顺序还是字节顺序问题。位序问题改循环顺序字节顺序问题改拼装时的移位方向或映射表。4.3 级联后高8位正常、低8位全错现象第一片的8位读得对第二片的数据全是乱值。原因通常是级联线接错——第二片的Q7应该接第一片的串行输入脚不是并行脚或者时钟只打到了第一片、第二片CP没连上。解决确认两片CP并联、PL并联只有数据线是串的用示波器看第二片CP是否有16个脉冲。另一个隐蔽原因是第二片Q7输出没加上拉长线导致电平塌陷补一个4.7k上拉到VCC即可。4.4 按键偶发误触发现象开关没动状态偶尔跳变一次。原因是采样节拍不稳或消抖窗口太短也可能是Q7走线和时钟线并行太长引入耦合。解决把消抖确认次数从2提到3采样间隔固定用定时器硬件上让CP和Q7走线尽量分开必要时Q7串一个100Ω电阻。软件层面还可以在读函数里连续读两次、两次一致才采用代价是多花十几个时钟周期对16路采集完全可接受。提示调试74HC165时先把“锁存脉冲有没有、时钟有没有、数据有没有动”这三件事用示波器确认再回头看代码能省掉大半瞎猜的时间。5. 进阶用法用DMA定时器把采集彻底从主循环里摘出去前面几章的读函数是阻塞式的16个时钟打下来虽然只有微秒级但如果系统里有更紧的任务或者要高频轮询几十路扩展主循环里反复调用还是占CPU。更彻底的做法是把CP时钟交给定时器PWM或输出比较Q7接定时器输入捕获或SPI的MISO用DMA把移位数据搬进缓冲区主循环只读结果。74HC165的时序本质就是一个“时钟数据”的串行流和SPI的Mode 0在电气上能对上CP空闲低、上升沿采样所以常见做法是把Q7接到SPI的MISO、CP接SCKPL用一个GPIO在每次传输前打低脉冲然后启动一次16位的SPI接收。/* 用SPI读取两片级联74HC165SPI Mode 016位帧 */ uint16_t hc165_read_spi(void) { uint16_t rx 0; /* 锁存PL低脉冲把并行状态装进移位寄存器 */ HAL_GPIO_WritePin(HC165_PL_PORT, HC165_PL_PIN, GPIO_PIN_RESET); __NOP(); HAL_GPIO_WritePin(HC165_PL_PORT, HC165_PL_PIN, GPIO_PIN_SET); /* 一次16位SPI传输SCK自动产生16个时钟MISO收回数据 */ HAL_SPI_Receive(hspi1, (uint8_t *)rx, 1, 10); /* 1个16位帧 */ return rx; }逻辑说明PL低脉冲完成锁存后SPI主机模式发一次16位接收硬件自动在SCK上产生16个时钟74HC165在每个上升沿把数据推到MISOSPI外设按位收进rx。参数上SPI要配成16位数据帧、Mode 0CPOL0、CPHA1边沿采样需按芯片手册核对74HC165是上升沿移出对应SPI在第一个边沿采样、波特率别超过芯片上限手册一般标到几十兆实际按走线长度降到1~4MHz更稳。注意SPI的位序要设成MSB first和74HC165先出高位一致否则读回来是反的。这套方案的好处是把16个时钟的时序完全交给硬件CPU只在传输前后各动一次GPIO配合DMA还能做到零占用。验证方法很简单先用阻塞版读函数和SPI版对同一组开关各读一次两个值相等就说明时序对齐了不等就查SPI的CPOL/CPHA和位序。我一般会在初始化后跑一次自检把两版结果打印出来比对确认一致再切到SPI路径。从那以后我每次给新板子写74HC165驱动都强制先用阻塞版跑通、再用SPI版替换两步都留日志省得后面分不清是硬件还是时序的锅。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑