资讯动态

STM32驱动74HC165:多路输入扩展与按键采集实战

发布时间:2026/10/29 14:28:37 来源:尧图企业网站定制
简介这份资源面向使用STM32等微控制器进行I/O口扩展的嵌入式开发者聚焦74HC165并行转串行移位寄存器的驱动实现帮助解决GPIO资源不足时连接更多外部设备的问题。压缩包共2个文件以C语言源文件与头文件为主分别承载驱动逻辑与接口声明整体约1KB体量轻便便于直接加入现有工程。已有738人学习下载具备一定的参考基础。内容围绕74HC165的引脚功能与工作时序展开涵盖GPIO初始化、串行时钟与移位使能信号控制、数据串行输入、清零与保持操作以及并行数据读取等关键环节并说明在矩阵键盘、多位数码管显示、多通道数据采集等场景中的典型用法。读者可据此理解芯片与STM32的接口设计思路将驱动代码嵌入项目后按实际时序需求调整优化减少从零编写底层驱动的调试成本。1. 从一颗并转串芯片说起74HC165 驱动在 STM32 上到底解决什么问题做按键矩阵或者拨码开关采集的时候最直接的做法是把每个开关都接到一个 GPIO 上代码里轮询或者上中断。可一旦输入点超过十几个引脚就不够用了PCB 走线也开始变得难看。这时候 74HC165 这类 8 位并行输入、串行输出的移位寄存器就派上用场了8 个开关只占 MCU 的 3 根线时钟、装载、数据多片还能级联16 路、24 路输入照样只吃 3 个引脚。这份 74HC165 驱动资源就是围绕 STM32 平台把这套时序封装成可复用代码适合正在做多路按键采集、拨码开关读取、面板输入扩展的嵌入式开发者。它不复杂但时序细节和边沿处理很容易翻车下面按“能干什么、怎么跑、怎么改”拆开讲。2. 驱动架构与硬件连接三根线背后的时序设计2.1 74HC165 的工作机制与引脚角色74HC165 内部是 8 个 D 触发器加一个移位寄存器。并行输入端 D0~D7 在装载信号PL也叫 SH/LD拉低时被一次性锁存进移位寄存器随后 PL 拉高每个时钟上升沿把数据从 Q7 往 QH 串行推出一位。所以整个读取过程分两步先给一个装载脉冲把当前 8 路电平“拍照”再打 8 个时钟把这张照片逐位移出来。对 STM32 来说需要三根线信号74HC165 引脚STM32 侧方向说明装载PL (SH/LD)任意 GPIO 输出MCU→165低电平锁存并行输入时钟CP (CLK)任意 GPIO 输出MCU→165上升沿移位数据QH (Q7)任意 GPIO 输入165→MCU串行数据输出时钟使能CE (CLK INH)接地—低电平有效常接地串行输入DS接地或前级 QH—级联时接前一片 QH这里有个容易被忽略的点CE 必须拉低芯片才响应时钟很多人焊完发现读出来全是 0 或全 1最后查出来是 CE 悬空。级联时把第一片的 QH 接到第二片的 DS时钟和装载信号并联读的时候连续打 8×N 个时钟先出来的是最后一片的数据。2.2 驱动分层GPIO 抽象与读取接口驱动代码一般分两层底层是三个 GPIO 的读写宏上层是“装载 移位 拼字节”的读取函数。这样换引脚只改宏换平台只改底层。下面是一个典型的头文件定义用宏把引脚操作抽出来方便移植/* hc165.h —— 74HC165 驱动接口 */ #ifndef __HC165_H #define __HC165_H #include stm32f10x.h /* 按实际芯片系列替换 */ /* 引脚定义按实际接线修改这三组宏 */ #define HC165_PL_PORT GPIOA #define HC165_PL_PIN GPIO_Pin_4 #define HC165_CLK_PORT GPIOA #define HC165_CLK_PIN GPIO_Pin_5 #define HC165_DAT_PORT GPIOA #define HC165_DAT_PIN GPIO_Pin_6 /* 片数级联时改这里读取长度自动跟着变 */ #define HC165_CHAIN_NUM 1 void HC165_Init(void); void HC165_Read(uint8_t *buf, uint8_t chip_num); #endif宏定义把端口和引脚集中在一处HC165_CHAIN_NUM控制级联片数。参数说明buf是输出缓冲区长度至少chip_num字节chip_num是实际级联片数允许运行时传入方便同一套代码适配不同板子。把片数做成参数而不是写死是我在多个项目间复用时最省事的一个决定。2.3 初始化与读取的完整实现初始化就是把 PL 和 CLK 配成推挽输出、DAT 配成上拉或浮空输入初始电平 PL 拉高、CLK 拉低。读取函数的核心是“先锁存、再移位”注意时钟空闲电平必须是低因为 165 在上升沿移位。/* hc165.c —— 初始化与读取实现 */ #include hc165.h /* 简单延时保证 165 在低速下也能跟上主频高时可缩短 */ static void hc165_delay(void) { __NOP(); __NOP(); __NOP(); __NOP(); } void HC165_Init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); /* PL 与 CLK推挽输出 */ gpio.GPIO_Mode GPIO_Mode_Out_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; gpio.GPIO_Pin HC165_PL_PIN; GPIO_Init(HC165_PL_PORT, gpio); gpio.GPIO_Pin HC165_CLK_PIN; GPIO_Init(HC165_CLK_PORT, gpio); /* DAT输入上拉避免悬空误读 */ gpio.GPIO_Mode GPIO_Mode_IPU; gpio.GPIO_Pin HC165_DAT_PIN; GPIO_Init(HC165_DAT_PORT, gpio); /* 空闲态PL 高、CLK 低 */ GPIO_SetBits(HC165_PL_PORT, HC165_PL_PIN); GPIO_ResetBits(HC165_CLK_PORT, HC165_CLK_PIN); } void HC165_Read(uint8_t *buf, uint8_t chip_num) { uint8_t i, b; /* 1. 装载PL 拉低锁存并行输入再拉高 */ GPIO_ResetBits(HC165_PL_PORT, HC165_PL_PIN); hc165_delay(); GPIO_SetBits(HC165_PL_PORT, HC165_PL_PIN); hc165_delay(); /* 2. 逐片逐位读出先出的是高位 Q7 */ for (b 0; b chip_num; b) { uint8_t val 0; for (i 0; i 8; i) { val 1; if (GPIO_ReadInputDataBit(HC165_DAT_PORT, HC165_DAT_PIN)) val | 0x01; /* 上升沿移位先拉高再拉低 */ GPIO_SetBits(HC165_CLK_PORT, HC165_CLK_PIN); hc165_delay(); GPIO_ResetBits(HC165_CLK_PORT, HC165_CLK_PIN); hc165_delay(); } buf[b] val; } }逻辑说明装载脉冲宽度只要满足芯片手册的最小值纳秒级hc165_delay里的几个 NOP 在几十 MHz 主频下已经足够但如果主频拉到上百 MHz 且开了指令流水建议用示波器确认一下 PL 低电平宽度。移位循环里先读数据再打时钟是因为第一位数据在装载后就已经出现在 QH 上第一个上升沿会把它推走所以必须在第一个时钟之前采样。val 1配合先读后移拼出来的字节正好是 D7 在最高位、D0 在最低位和并行端口的位序一致。级联时外层循环按片读先读出的字节对应离 MCU 最远的那片如果发现顺序反了把buf的写入下标倒过来即可。3. 跑通验证从裸机工程到示波器抓波形3.1 最小验证工程搭建拿到这份驱动后最快的验证方式不是直接塞进大工程而是新建一个最小工程一个 main 循环里反复调用HC165_Read把结果通过串口打印出来同时手动把某一路输入接地或接 VCC看对应位是否翻转。工程配置上STM32 标准外设库或 HAL 都能用上面代码用的是标准库风格HAL 的话把 GPIO 操作换成HAL_GPIO_WritePin/HAL_GPIO_ReadPin即可逻辑不变。/* main.c —— 最小验证读 8 路并打印 */ #include stm32f10x.h #include hc165.h #include usart.h /* 假定已有串口打印封装 */ int main(void) { uint8_t data[HC165_CHAIN_NUM]; USART1_Init(115200); HC165_Init(); while (1) { HC165_Read(data, HC165_CHAIN_NUM); printf(HC165: 0x%02X\r\n, data[0]); for (volatile int i 0; i 500000; i); /* 粗延时避免刷屏 */ } }这段代码的作用是把读取结果可视化。上电后串口应该稳定输出某个值用手把 D0 那一路拉到 GND打印值的最低位应该从 1 变 0假设该路平时被上拉为高。如果八路全不变先查 CE 是否接地、PL 和 CLK 有没有接反如果值在跳但不对应多半是位序问题把并行端 D0~D7 的接线顺序和代码里的移位方向对一遍。3.2 用示波器确认时序而不是靠猜嵌入式里最省时间的调试手段就是看波形。把探头挂在 CLK 和 DAT 上触发设在 PL 的下降沿应该能看到PL 一个窄低脉冲随后 CLK 出现 8 个或 8×N 个等间距脉冲DAT 在每个 CLK 上升沿之前稳定。常见异常是 CLK 空闲电平为高导致第一个上升沿丢失读出来整体错一位——这种“玄学错位”靠打印很难定位波形一眼就看出来。另一个检查点是 PL 脉冲和第一个 CLK 之间的间隔太短的话 165 还没完成锁存第一位就是旧数据。提示验证阶段把主频降下来、延时加长先保证功能正确再逐步提速。上来就满速跑时序余量不够时问题会间歇出现排查成本翻倍。3.3 中断与轮询的取舍按键采集用轮询还是外部中断取决于响应要求。74HC165 本身没有中断输出只能靠 MCU 定时轮询。常见做法是开一个 5~10ms 的定时器中断在中断里调用HC165_Read再做软件消抖。注意读取函数里有阻塞延时放在中断里要保证总耗时远小于中断周期8 位读取在 72MHz 下也就几微秒问题不大但如果级联到 8 片、延时又给得保守就要算一下时间账必要时把延时改成基于 DWT 的精确短延时或直接去掉。4. 避坑与排查那些让读数全 0 或错位的细节4.1 读数恒为 0x00 或 0xFF现象串口打印永远是 0x00 或 0xFF手动拨动输入毫无反应。原因通常是三类CE时钟禁止脚悬空或接高芯片根本不响应时钟DAT 线接错到了 QH 以外的引脚或者 PL 一直为低芯片停在装载态不进入移位。解决先用万用表确认 CE 对地导通再确认 DAT 接的是 QH第 9 脚不同封装编号略有差异以手册为准最后用示波器看 PL 是否有高电平空闲态。这三步走完九成的“全 0 全 1”都能定位。4.2 数据整体错一位现象读出来的值和实际输入对不上但存在明显的移位关系比如实际是 0b00000001读出来是 0b00000010 或 0b10000000。原因CLK 空闲电平配错或者采样和移位的先后顺序写反了。165 是上升沿移位如果代码里先打时钟再读数据第一位就被推走了。解决确认 CLK 初始为低循环里严格“先读 DAT、再拉高 CLK、再拉低”位序方向用val 1还是val 1要和硬件接线核对。4.3 级联时字节顺序颠倒现象两片级联第一片的输入却出现在第二个字节里。原因级联时数据是从最后一片开始逐位移出的先读出的字节对应物理上离 MCU 最远的那片。解决要么在硬件上把靠近 MCU 的那片作为“最后一片”接线要么在软件里把buf的写入顺序反过来。我一般倾向软件处理改一行下标比改板子现实。4.4 高速下偶发误读现象低速测试一切正常主频提上去或者延时去掉后偶尔读到错误值。原因PL 脉冲宽度或 CLK 高低电平保持时间逼近芯片极限加上 GPIO 翻转速度受负载电容影响。解决保留最小延时别为了“优化”把hc165_delay整个删掉必要时把 GPIO 速度档位调低反而更稳因为过快的边沿会带来振铃。用示波器看边沿是否干净有振铃就在数据线上加小电容或串阻。4.5 输入悬空导致读数漂移现象没接开关的那几路读数随机跳变。原因74HC165 的并行输入是高阻 CMOS 输入悬空时电平不确定。解决每路输入加上拉或下拉电阻10kΩ 量级拨码开关一端接 VCC、另一端接输入并配下拉或者反过来。这一点在原理图阶段就要定事后飞线很痛苦。5. 二次开发把这套驱动改成你自己的输入采集层5.1 从“读字节”到“读按键事件”原始驱动只负责把 8 路电平读成一个字节真正做产品还得往上包一层消抖、边沿检测、映射成按键事件。我的习惯是在驱动之上加一个采集任务维护last和curr两个字节用异或找出变化的位再配合计数器做消抖。这样上层业务只关心“哪个键按下/松开”不碰时序。/* key_scan.c —— 基于 HC165 的按键消抖与边沿检测 */ #include hc165.h #define KEY_NUM 8 static uint8_t key_last 0; static uint8_t key_cnt[KEY_NUM] {0}; /* 返回本次新按下的键位图bit 置 1 表示该键刚按下 */ uint8_t Key_Scan(void) { uint8_t curr, changed, pressed 0, i; HC165_Read(curr, 1); changed curr ^ key_last; /* 找出电平变化的位 */ for (i 0; i KEY_NUM; i) { if (changed (1 i)) { if (key_cnt[i] 3) { /* 连续 3 次一致才认账 */ key_cnt[i] 0; if (curr (1 i)) /* 假设高电平为按下 */ pressed | (1 i); } } else { key_cnt[i] 0; } } key_last curr; return pressed; }参数说明key_cnt是每个键的稳定计数阈值 3 对应 3 次扫描周期若扫描周期 5ms 则消抖时间约 15ms可按手感调整。pressed只在新按下时置位适合触发一次性动作如果要检测长按再维护一个按住计数即可。这套结构的好处是驱动层和业务层彻底解耦换芯片比如换成 74HC595 做输出扩展时只改HC165_Read这一处。5.2 移植到 HAL 与不同 STM32 系列标准库转 HAL 的改动集中在 GPIO 操作GPIO_SetBits换成HAL_GPIO_WritePin(port, pin, GPIO_PIN_SET)GPIO_ReadInputDataBit换成HAL_GPIO_ReadPin。初始化部分用HAL_GPIO_Init配GPIO_InitTypeDef字段名从GPIO_Mode_Out_PP变成GPIO_MODE_OUTPUT_PP。跨系列F1 到 F4、G0时注意时钟使能宏不同F4 用__HAL_RCC_GPIOA_CLK_ENABLE()。移植完先跑 3.1 的最小验证别直接上大工程否则出问题分不清是驱动还是业务。5.3 提速与稳定性验证方法想提高读取频率先量化再优化用定时器测HC165_Read单次耗时再逐步缩短hc165_delay每改一次跑一轮长时间循环读固定输入统计错误次数。我的血泪经验是延时删到零往往还能跑但换一批芯片或换温度环境就翻车所以最终版本一定保留一个保守的最小延时并在注释里写清楚“此处不可删”的原因。验证时把输入固定成 0x55 和 0xAA 交替跑几万次比对比手动拨开关靠谱得多。从那以后我每次做这类移位寄存器驱动都强制先搭最小验证工程、先用示波器确认时序、再谈集成绝不把“能打印出数”当成“驱动没问题”。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑