STM32多路开关采集:74HC165驱动包设计与级联实现

发布时间:2026/10/29 14:28:22
STM32多路开关采集:74HC165驱动包设计与级联实现 简介这份资源面向使用STM32等微控制器进行I/O扩展的嵌入式开发者聚焦74HC165并行转串行移位寄存器的驱动实现帮助解决GPIO口线不足时如何稳定读取多路外部输入的问题。压缩包共2个文件以1个.c源文件与1个.h头文件组成包体约1KB源文件承载时序控制与数据读取逻辑头文件提供引脚定义与接口声明便于直接加入工程调用。目前已有738人学习下载适合需要快速验证硬件接口方案的中初级开发者参考。内容围绕SH_CP、LE、SRCLR等引脚时序展开涵盖GPIO初始化、串行时钟控制、数据移入与并行读取等关键环节可应用于矩阵键盘、多位数码管显示及多通道数据采集等场景。读者可据此理解移位寄存器与STM32的接口设计思路并在此基础上按实际引脚与时序要求做适配修改减少从零编写驱动时的调试成本。1. 一块并转串芯片为什么值得单独建一个驱动包做按键矩阵或者拨码开关采集的时候最省事的做法是把每个开关都接到一个 GPIO 上。八个开关占八个脚十六个开关占十六个脚板子画到后面引脚不够用只能换封装更大的主控成本一下就上去了。74HC165 这类 8 位并行输入、串行输出的移位寄存器就是专门解决这个问题的八路开关只占主控三个脚时钟、锁存、数据多片还能级联十六路、三十二路照样三个脚搞定。这份 74HC165 驱动包面向的就是 STM32 平台上做多路开关采集的场景把时序、级联、消抖这些容易翻车的地方封装成可复用的驱动层适合正在画板子、写按键扫描、或者被引脚数量卡住的嵌入式开发者直接拿去改。2. 驱动架构拆解三层结构让芯片时序和业务逻辑解耦2.1 为什么不能把时序代码直接塞进按键扫描函数很多人在第一次用 74HC165 的时候习惯在按键扫描函数里直接拉时钟、读数据几行代码就完事。单路、单芯片、不消抖的情况下确实能跑但只要出现下面任意一种变化代码就得推倒重写芯片从一片变成两片级联、采集从主循环挪进定时器中断、开关从拨码变成机械按键需要消抖、主控从一款 STM32 换到另一款。时序代码和业务代码搅在一起改一处牵动全身这是血泪经验。这份驱动的思路是把职责切成三层层与层之间只通过明确的接口通信层级职责典型文件依赖硬件抽象层GPIO 读写、微秒级延时hc165_port.c / hc165_port.hHAL 库或寄存器操作芯片驱动层锁存、移位、级联读取时序hc165.c / hc165.h仅依赖硬件抽象层应用层按键消抖、状态映射、业务回调main.c 或独立业务模块依赖芯片驱动层这样分层之后换主控只需要重写 hc165_port.c 里的几个函数芯片驱动层一行不动换芯片数量只需要改一个宏定义业务层想怎么处理数据都行驱动层不关心。常见做法是在 hc165.h 里用一个宏HC165_CHAIN_NUM控制级联片数驱动内部按片数循环读取应用层拿到的永远是一个完整的位图。2.2 目录结构与关键文件说明拿到压缩包解压后目录大致是这样组织的不同版本可能略有出入以实际文件为准74HC165_Driver/ ├── Inc/ │ ├── hc165.h # 驱动对外接口、级联片数宏、数据结构定义 │ └── hc165_port.h # 硬件抽象层接口声明 ├── Src/ │ ├── hc165.c # 核心时序实现锁存、移位、级联读取 │ └── hc165_port.c # GPIO 初始化、电平读写、延时实现 ├── Example/ │ └── main.c # 采集示例初始化 周期读取 串口打印 └── README.md # 接线说明与移植步骤hc165.h是整个驱动的门面对外只暴露初始化、读取、片选控制这几类接口内部时序细节全部藏在hc165.c里。hc165_port.c是移植时唯一需要动的地方里面通常是四个函数三个引脚初始化、锁存脚控制、时钟脚控制、数据脚读取外加一个微秒延时。把这几块拆开移植工作量就从「读懂整个驱动」降到「填四个空」。2.3 一次完整读取的时序链路74HC165 的读取分两步先拉低锁存脚PL / SH/LD把八路并行输入的状态锁进芯片内部然后给时钟脚CP打脉冲数据脚Q7在每个时钟上升沿输出一位从最高位开始往外吐。级联的时候前一级的串行输出接到后一级的串行输入时钟和锁存共用读 N 片就是连续打 8×N 个时钟脉冲。这里有个容易忽略的细节74HC165 的时钟有个时钟禁止脚CE低电平有效。如果硬件上直接接地那没问题如果接在 GPIO 上初始化时必须拉低否则时钟打不进去读出来全是固定值。驱动里一般会在初始化函数里统一处理但移植时如果发现读出来永远是 0xFF 或 0x00先查这个脚。3. 核心代码逐段讲锁存、移位、级联读取怎么写3.1 硬件抽象层四个函数撑起移植工作先看hc165_port.c里最关键的几个实现。以 STM32 HAL 库为例引脚定义放在头文件里方便统一改/* hc165_port.h —— 移植时只需要改这里的引脚宏 */ #define HC165_PL_GPIO_PORT GPIOA #define HC165_PL_PIN GPIO_PIN_0 /* 锁存脚 SH/LD */ #define HC165_CP_GPIO_PORT GPIOA #define HC165_CP_PIN GPIO_PIN_1 /* 时钟脚 CP */ #define HC165_Q7_GPIO_PORT GPIOA #define HC165_Q7_PIN GPIO_PIN_2 /* 数据脚 Q7 */ #define HC165_CE_GPIO_PORT GPIOA #define HC165_CE_PIN GPIO_PIN_3 /* 时钟禁止脚低有效 *//* hc165_port.c —— 引脚操作与延时 */ void hc165_port_init(void) { GPIO_InitTypeDef gpio {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); /* PL、CP、CE 为输出Q7 为输入 */ gpio.Pin HC165_PL_PIN | HC165_CP_PIN | HC165_CE_PIN; gpio.Mode GPIO_MODE_OUTPUT_PP; gpio.Pull GPIO_NOPULL; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(HC165_PL_GPIO_PORT, gpio); gpio.Pin HC165_Q7_PIN; gpio.Mode GPIO_MODE_INPUT; gpio.Pull GPIO_PULLUP; /* 开关另一端接地时用上拉 */ HAL_GPIO_Init(HC165_Q7_GPIO_PORT, gpio); /* 初始状态锁存高、时钟低、时钟禁止拉低使能 */ HAL_GPIO_WritePin(HC165_PL_GPIO_PORT, HC165_PL_PIN, GPIO_PIN_SET); HAL_GPIO_WritePin(HC165_CP_GPIO_PORT, HC165_CP_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(HC165_CE_GPIO_PORT, HC165_CE_PIN, GPIO_PIN_RESET); } void hc165_pl_write(uint8_t level) { HAL_GPIO_WritePin(HC165_PL_GPIO_PORT, HC165_PL_PIN, level ? GPIO_PIN_SET : GPIO_PIN_RESET); } void hc165_cp_write(uint8_t level) { HAL_GPIO_WritePin(HC165_CP_GPIO_PORT, HC165_CP_PIN, level ? GPIO_PIN_SET : GPIO_PIN_RESET); } uint8_t hc165_q7_read(void) { return HAL_GPIO_ReadPin(HC165_Q7_GPIO_PORT, HC165_Q7_PIN) GPIO_PIN_SET; } void hc165_delay_us(uint32_t us) { /* 简单循环延时主频 72MHz 时约 1us 一次按实际主频校准 */ volatile uint32_t n us * 12; while (n--) { __NOP(); } }这段代码的逻辑很直白初始化把三个输出脚配好、数据脚配上拉输入然后给一个确定的初始电平。hc165_delay_us用循环延时精度不高但够用因为 74HC165 在 5V 下时钟频率可以到几十 MHzSTM32 的 GPIO 翻转速度远低于芯片上限真正需要延时的地方是锁存脉冲之后的建立时间通常几百纳秒就够循环延时留足余量即可。参数上唯一要校准的是us * 12这个系数主频不同要按实际调整调大了读取变慢调小了可能时序不够。3.2 芯片驱动层级联读取的核心循环hc165.c里最核心的是读取函数。级联片数用宏控制读取时先锁存再按位循环/* hc165.h —— 对外接口与级联配置 */ #define HC165_CHAIN_NUM 2 /* 级联片数2 片 16 路输入 */ void hc165_init(void); void hc165_read_all(uint8_t *buf, uint8_t chain_num);/* hc165.c —— 锁存 移位读取 */ void hc165_init(void) { hc165_port_init(); } void hc165_read_all(uint8_t *buf, uint8_t chain_num) { uint8_t i, bit; /* 1. 锁存PL 拉低再拉高并行数据进入移位寄存器 */ hc165_pl_write(0); hc165_delay_us(1); hc165_pl_write(1); hc165_delay_us(1); /* 2. 逐位读取每片 8 位共 chain_num * 8 位 */ for (i 0; i chain_num; i) { buf[i] 0; for (bit 0; bit 8; bit) { buf[i] 1; /* 左移先读到的是高位 */ buf[i] | hc165_q7_read(); /* 读当前数据脚 */ hc165_cp_write(1); /* 时钟上升沿芯片移出一位 */ hc165_delay_us(1); hc165_cp_write(0); hc165_delay_us(1); } } }逐段说明。第一步锁存PL 低电平期间并行输入被采样拉高后数据冻结在移位寄存器里这个脉冲宽度 datasheet 要求最小几十纳秒代码里给 1 微秒是保守做法。第二步循环外层按片数走内层按位走每读一位之前先左移腾出最低位再把数据脚电平或进去然后打一个时钟脉冲让芯片移出下一位。注意顺序是先读后打时钟因为第一个数据位在锁存后就已经出现在 Q7 上了如果先打时钟再读会丢掉最高位、多读一位无效数据这是新手最常翻车的地方。参数上buf是调用者提供的缓冲区长度至少chain_num字节chain_num传级联片数和宏HC165_CHAIN_NUM保持一致。级联时数据顺序是第一片的 8 位先出来然后是第二片依此类推所以buf[0]对应第一片接线时把离主控最近的那片当第一片映射关系最直观。3.3 应用层把位图变成按键状态驱动层吐出来的是原始位图应用层要做的第一件事是消抖。机械按键抖动通常在 5 到 20 毫秒常见做法是定时器每 10 毫秒采一次连续两次相同才认/* main.c —— 周期采集 简单消抖 */ static uint8_t last_raw[HC165_CHAIN_NUM]; static uint8_t stable[HC165_CHAIN_NUM]; void key_scan_task(void) /* 每 10ms 调用一次 */ { uint8_t raw[HC165_CHAIN_NUM]; uint8_t i; hc165_read_all(raw, HC165_CHAIN_NUM); for (i 0; i HC165_CHAIN_NUM; i) { if (raw[i] last_raw[i]) { stable[i] raw[i]; /* 连续两次一致认为稳定 */ } last_raw[i] raw[i]; } /* stable[] 里就是消抖后的状态可映射成按键事件 */ }这段逻辑用「连续两次采样一致」代替复杂的计数器消抖代码短、内存占用小对拨码开关和响应要求不高的按键足够用。如果按键要求快速响应可以把采样周期缩到 5 毫秒或者改成计数式消抖。stable数组的每一位对应一路开关取反还是取正取决于硬件接法开关一端接地、芯片输入上拉时按下读到 0业务层按需翻转。4. 跑起来从接线到串口看到数据4.1 硬件接线与上电检查接线是这类驱动最容易出问题的地方按下面的对应关系接接完先别急着烧程序用万用表量一遍74HC165 引脚接到哪里说明VCC / GND3.3V / GND和主控共地别用 5V 直连 3.3V 主控SH/LDPLPA0锁存驱动输出CPPA1时钟驱动输出CEPA3 或 GND低有效接 GPIO 时初始化拉低Q7PA2串行数据驱动输入D0~D7开关 / 拨码每路一端接输入、一端接地输入上拉Q7级联下一片 DS级联时前片 Q7 接后片串行输入上电后先量三件事VCC 是不是 3.3V、CE 是不是低电平、PL 初始是不是高电平。这三项任何一项不对读出来都是固定值后面调代码全是白费功夫。4.2 移植到自己的工程把Inc和Src下的文件加进工程然后按三步走。第一步改hc165_port.h里的引脚宏换成自己板子实际用的引脚第二步确认hc165_port.c里的 GPIO 时钟使能对应的是哪个端口比如用 GPIOB 就要改成__HAL_RCC_GPIOB_CLK_ENABLE()第三步在main.c里初始化并周期调用/* main.c —— 初始化与主循环 */ int main(void) { HAL_Init(); SystemClock_Config(); MX_USART1_UART_Init(); /* 串口用于打印调试 */ hc165_init(); while (1) { uint8_t raw[HC165_CHAIN_NUM]; hc165_read_all(raw, HC165_CHAIN_NUM); printf(raw: %02X %02X\r\n, raw[0], raw[1]); HAL_Delay(100); } }烧进去之后打开串口拨动开关看打印的十六进制值有没有对应位翻转。如果两片级联raw[0]对应第一片、raw[1]对应第二片逐位对照接线表验证。这一步跑通说明时序和接线都没问题再往上叠消抖和业务逻辑。4.3 用示波器确认时序串口数据不对的时候别急着改代码先看波形。把探头挂在 CP 脚上触发方式设成下降沿应该能看到一串等间隔的脉冲数量等于 8×级联片数再把探头挂 PL 上每次读取前应该有一个低脉冲。如果 CP 上脉冲数量不对查循环里的片数参数如果 PL 没有脉冲查锁存函数有没有被调用如果脉冲都有但数据不变查 Q7 接线和 CE 电平。这套排查顺序比盲改代码快得多。5. 避坑与排查五个真实踩过的坑5.1 读出来永远是 0xFF 或 0x00现象串口打印固定值拨开关毫无反应。原因通常是三类CE 脚没拉低时钟被禁止Q7 数据脚配置成了输出或者没配上拉锁存脉冲根本没发出来。解决先量 CE 电平再确认 Q7 是输入模式最后用示波器看 PL 有没有脉冲。三个都排除基本就是接线虚焊。5.2 级联后数据错位一位现象单片正常两片级联后第二片的数据整体偏移。原因多半是读取循环里先打时钟后读数据或者级联线接反了——把第一片的 Q7 接到了主控、第二片的数据脚悬空。解决确认循环内是先读后打时钟再按「离主控最近的是第一片」重新核对级联线。5.3 按键偶尔误触发现象没碰开关状态偶尔跳变。原因是采样周期和开关抖动撞上了或者数据线太长引入干扰。解决把采样周期固定到 10 毫秒并加连续两次一致的消抖数据线超过十厘米时在 Q7 上加 100nF 到地或者降低时钟频率、增加位间延时。5.4 换主控后编译不过现象换了一款 STM32hc165_port.c里一堆未定义。原因是 HAL 库版本或者寄存器操作方式不同。解决只改hc165_port.c把 HAL 调用换成新平台的等价写法驱动层和头文件不动。这也是分层设计的意义所在移植成本被限制在一个文件里。5.5 中断里读取导致数据错乱现象把读取放进定时器中断后偶尔读到半截数据。原因是中断里调用了带延时的函数或者和主循环里的读取发生重入。解决读取函数本身不加锁但调用方要保证同一时刻只有一处调用中断里读取时把延时函数换成更短的忙等或者干脆在主循环读取、中断只置标志。6. 二次开发把驱动改成适合自己项目的形状6.1 三个最值得改的方向拿到这份驱动直接能用但真正落地时通常要按项目改三处。第一处是级联片数HC165_CHAIN_NUM改成实际片数缓冲区大小跟着变如果片数超过 8 片uint8_t数组要换成uint16_t或者按字节数组处理读取循环里的位操作也要相应调整。第二处是消抖策略拨码开关不需要消抖机械按键需要可以给驱动加一个编译开关拨码模式下跳过消抖直接输出。第三处是数据映射把原始位图映射成业务语义常见做法是加一张映射表/* 位图到业务 ID 的映射按实际接线填 */ static const uint8_t bit_to_key[HC165_CHAIN_NUM][8] { { 0, 1, 2, 3, 4, 5, 6, 7 }, /* 第一片 D0~D7 */ { 8, 9, 10, 11, 12, 13, 14, 15 } /* 第二片 D0~D7 */ };映射表的好处是接线变了只改表业务代码不动。如果项目里开关数量固定、语义明确这张表比在代码里到处写if (raw 0x01)好维护得多。6.2 验证改造是否正确的两个方法改完之后怎么确认没改坏第一个方法是回环验证把某一路输入固定接地或接高读出来对应位应该稳定为 0 或 1逐位过一遍确认映射表每一位都对得上。第二个方法是边界验证级联片数改大之后把最后一片的输入全部拉高、前面全部拉低读出来应该只有最后一片的字节是 0xFF其余是 0x00这个测试能一次性抓出循环边界和缓冲区越界的问题。6.3 一个容易被忽略的优化点如果采集频率要求高比如每毫秒读一次循环延时就成了瓶颈。可以把hc165_delay_us里的循环延时换成基于 DWT 周期计数器的精确延时或者干脆去掉位间延时——74HC165 的建立保持时间在几十纳秒量级STM32 的 GPIO 翻转本身就够慢多数情况下不加延时也能稳定工作。但去掉延时之前一定用示波器确认波形别凭感觉删。从那以后我每次移植这类移位寄存器驱动都强制先量 CE、PL、CP 三个脚的电平再动代码因为十次读不出数据里有八次是硬件状态不对代码本身反而是最不容易错的部分。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询