
简介STM32驱动74HC595的可级联函数包面向嵌入式系统开发人员特别适合数码管动态扫描、LED点阵显示和多通道并行输出等应用能够有效节省微控制器引脚资源。代码基于Keil MDK工程编写使用普通GPIO模拟SPI时序在SHCP上升沿将DS引脚的数据逐位移入内部寄存器再由STCP上升沿锁存至并行输出端完整实现了74HC595的串入并出工作方式配合多片级联可灵活扩充更多输出口。压缩包共2个文件包含1个头文件和1个C源文件体积仅1KB结构紧凑加入工程即可使用。源码中定义了引脚结构体并提供了初始化函数和写数据函数用户只需按实际接线修改引脚定义就能驱动数码管或LED矩阵开发效率较高。目前已有3436人学习下载适合初步接触74HC595的开发者参考也适合需要快速扩展多路并行输出的项目选用。1. 方案选型为什么用74HC595做GPIO扩展STM32开发中IO口不够用几乎是一个必然遇到的坎。一块常见的STM32F103C8T6满打满算37个IO接了屏幕、传感器、按键、串口、I2C、SPI之后真正能自由支配的引脚往往只剩个位数。这时候与其换更大封装的芯片不如用74HC595做串行转并行的IO扩展用3个IO换8路输出级联后3个IO可以换16路、24路甚至更多输出性价比极高。可能有人会问I2C接口的PCF8574也能做扩展为什么选74HC595核心原因有三点第一74HC595是纯硬件移位寄存器没有地址配置问题级联时直接QH接下一片SER就能无限串联而PCF8574最多只能挂8片还需要设置A0/A1/A2地址第二74HC595的移位时钟可以跑到几十MHz在动态扫描、PWM调光这类需要快速刷新输出的场景下优势明显第三它是TTL电平兼容的5V供电时可以直接驱动LED、数码管、继电器模块不需要额外的电平转换。在项目里我用74HC595做了一块8路LED指示灯板和一块4位数码管显示板整机只占了STM32的PA4数据、PA5时钟、PA6锁存三个引脚。这个方案在成本和PCB面积上都比直接上STM32F103ZET6大芯片划算得多尤其适合毕业设计、竞赛作品和产品原型阶段快速验证。2. 74HC595核心原理与级联机制2.1 芯片引脚与内部结构一次看懂开始写驱动之前必须把74HC595的引脚和内部数据流彻底搞明白。芯片一共有16个引脚核心的就这6个SER14脚是串行数据输入SRCLK11脚是移位寄存器时钟RCLK12脚是存储寄存器时钟也叫锁存时钟Q0~Q7是并行输出QH9脚是串行数据输出还有OE13脚和SRCLR10脚用来控制输出使能和清零。数据走的路径是这样的SER引脚上的数据在SRCLK每个上升沿被移入移位寄存器8个时钟后填满一个字节此时并行输出端Q0~Q7还保持原来的状态不变直到RCLK来一个上升沿移位寄存器里的数据才会一次性锁存到存储寄存器进而出现在Q0~Q7上。这个设计非常巧妙它保证了在数据逐位移入的过程中输出端不会产生毛刺和抖动所有输出状态在同一时刻统一更新。2.2 级联时数据顺序的坑级联的原理很简单把前一片的QH接到后一片的SER两片共享同一个SRCLK和RCLK。数据从第一片的SER进入每经过8个时钟最早进入的8位数据就会被挤到第二片的移位寄存器里。如果两片级联需要16个时钟完成数据移位三片就24个以此类推。这里有一个几乎所有新手都会踩的坑发送数据的顺序与芯片排列顺序是反的。假设MCU先发送一个字节Byte0再发送Byte1那么移位结束后Byte0会在远离MCU的那一片里Byte1才在靠近MCU的这一片里。换句话说如果你想让第1片输出某个值、第2片输出另一个值必须先发送第2片的数据再发送第1片的数据也就是数组倒序发送。这个顺序问题在上述代码里通过for(i len - 1; i 0; i--)解决了照着写就行但理解背后的机制能帮你避免后面碰到三片、四片级联时搞乱数据映射关系。3. 驱动函数设计思路与代码实现3.1 软件模拟时序 vs 硬件SPI驱动74HC595有两条路线一是用GPIO软件模拟时序二是用STM32的硬件SPI外设。软件模拟的优点是引脚任意、代码直观、便于理解和移植缺点是会占用CPU且速度受GPIO翻转速率限制。硬件SPI则可以把数据移位交给外设自动完成主程序只需要往SPI-DR寄存器写数据效率高很多。我的建议是项目里先写好软件模拟版本把逻辑调通之后再决定要不要切换成硬件SPI。原因很简单软件模拟版本调试方便每个位都可以停在任意时刻观察波形而硬件SPI一旦配错模式CPOL/CPHA很难直观定位问题。下面给出的代码以软件模拟为主同时我也会讲解如何改造成SPI版本。3.2 函数封装把引脚和级联数抽象出来很多网上流传的代码都是针对单一芯片、写死引脚的定义换成另一块板子就得改好几处。实际项目中我习惯用一个结构体把引脚封装起来配合宏定义操作位这样移植到不同型号的STM32时只需要改宏驱动逻辑一行都不用动。/* hc595.h */ #ifndef __HC595_H #define __HC595_H #include stm32f1xx_hal.h /* 引脚映射——根据实际接线修改 */ #define HC595_SER_GPIO GPIOA #define HC595_SER_PIN GPIO_PIN_4 #define HC595_SRCLK_GPIO GPIOA #define HC595_SRCLK_PIN GPIO_PIN_5 #define HC595_RCLK_GPIO GPIOA #define HC595_RCLK_PIN GPIO_PIN_6 /* 级联数量1片为12片为2最多不建议超过8片 */ #define HC595_CASCADE_NUM 2 #define HC595_SER_HIGH() HAL_GPIO_WritePin(HC595_SER_GPIO, HC595_SER_PIN, GPIO_PIN_SET) #define HC595_SER_LOW() HAL_GPIO_WritePin(HC595_SER_GPIO, HC595_SER_PIN, GPIO_PIN_RESET) #define HC595_SRCLK_HIGH() HAL_GPIO_WritePin(HC595_SRCLK_GPIO, HC595_SRCLK_PIN, GPIO_PIN_SET) #define HC595_SRCLK_LOW() HAL_GPIO_WritePin(HC595_SRCLK_GPIO, HC595_SRCLK_PIN, GPIO_PIN_RESET) #define HC595_RCLK_HIGH() HAL_GPIO_WritePin(HC595_RCLK_GPIO, HC595_RCLK_PIN, GPIO_PIN_SET) #define HC595_RCLK_LOW() HAL_GPIO_WritePin(HC595_RCLK_GPIO, HC595_RCLK_PIN, GPIO_PIN_RESET) void HC595_Init(void); void HC595_ShiftByte(uint8_t dat); void HC595_WriteBuffer(uint8_t *buf, uint8_t len); #endif3.3 核心函数实现与逐行讲解/* hc595.c */ #include hc595.h /** * brief 初始化74HC595控制引脚 * note 三个引脚均配置为推挽输出速度拉满 */ void HC595_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitStruct.Pin HC595_SER_PIN | HC595_SRCLK_PIN | HC595_RCLK_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); /* 初始状态时钟和数据线拉低锁存线拉高 */ HC595_SER_LOW(); HC595_SRCLK_LOW(); HC595_RCLK_HIGH(); }初始化这里有个细节RCLK初始状态我习惯设为高电平因为锁存操作实际上是上升沿生效初始化时让RCLK处于高电平后续写入数据时只需要将它拉低再拉高就能产生一个干净的上升沿。如果初始是低电平那么第一次锁存前的第一次拉高就是上升沿容易在意外状态下触发一次锁存。/** * brief 向74HC595移位寄存器发送一个字节 * param dat 要发送的8位数据 * note MSB先行即数据的bit7最先进入芯片 */ void HC595_ShiftByte(uint8_t dat) { for (uint8_t i 0; i 8; i) { /* 先设置数据线 */ if (dat 0x80) { HC595_SER_HIGH(); } else { HC595_SER_LOW(); } /* 时钟上升沿数据被移入移位寄存器 */ HC595_SRCLK_HIGH(); HC595_SRCLK_LOW(); /* 左移一位准备发送下一位 */ dat 1; } }关于MSB先行还是LSB先行取决于你希望的输出映射关系。74HC595的Q7对应第一个进入的bitQ0对应最后进入的bit所以MSB先行时dat的bit7会出现在Q7上bit0出现在Q0上。如果你希望bit0对应Q0那就改成从dat 0x01开始判断并右移。这一点在写显示驱动时要特别留意否则数字显示出来会是镜像的。/** * brief 级联写入多字节数据 * param buf 数据缓冲区buf[0]对应第1片靠近MCUbuf[n-1]对应最后一片 * param len 数据长度必须等于HC595_CASCADE_NUM * note **关键**必须先发送最后一片的数据再发送第1片的数据 */ void HC595_WriteBuffer(uint8_t *buf, uint8_t len) { /* 倒序发送从最后一片开始向前发送 */ for (int8_t i len - 1; i 0; i--) { HC595_ShiftByte(buf[i]); } /* 所有字节移位完成后再统一锁存 */ HC595_RCLK_LOW(); HC595_RCLK_HIGH(); }3.4 级联顺序与锁存时机两个最核心的注意点HC595_WriteBuffer里有两个细节决定级联能否正常工作。第一个是倒序发送。前文说过数据逐位移入时先发送的字节会一路向后穿越前面所有芯片最终停留在最远端。倒序发送保证了buf[0]对应第1片、buf[1]对应第2片这种直观的映射关系。如果你发现级联后第1片和第2片的数据互相换了位置第一反应就应该是检查这里的循环是不是写成了正序。第二个是锁存时机。ShiftByte函数只负责把数据送进移位寄存器完成全部16位两片或24位三片的移位之后RCLK才允许产生上升沿。绝不能每发一个字节就锁存一次。如果每发一个字节都锁存那么前8位数据到达第1片时就会被提前输出出来第2片的数据还没到达结果就是输出闪烁或数据错乱。在动态扫描场景下尤其明显会出现肉眼可见的拖影和残影。4. 硬件连接与实测记录4.1 两片级联接线表以STM32F103C8T6最小系统板为例PA4接SER数据PA5接SRCLK时钟PA6接RCLK锁存两片74HC595的具体接线如下信号STM32引脚第1片74HC595第2片74HC595数据PA4SER14第1片QH9→ SER时钟PA5SRCLK11SRCLK11并联锁存PA6RCLK12RCLK12并联输出使能VCCOE13OE13清零VCCSRCLR10SRCLR10电源3.3V/5VVCC16VCC16地GNDGND8GND84.2 电源与去耦的实践建议74HC595的供电范围是2V~6V可以直接用STM32的3.3V供电但要注意如果输出端带了LED或继电器电流会从VCC引脚流入芯片必须核算总电流是否在芯片允许范围内。74HC595每个输出的灌电流/拉电流典型值是6mA两片共16路满载时理论最大电流约100mA虽然3.3V稳压器一般能扛住但建议在每片芯片的VCC和GND之间就近加一个0.1uF去耦电容避免输出跳变时电压跌落导致芯片逻辑混乱。我之前踩过一个大坑板子上没有加去耦电容一开启LED扫描功能数码管就出现无规律的乱码用示波器一查VCC上有几百mV的毛刺加装电容后问题立刻消失。所以去耦电容不是可选项而是必选项哪怕用洞洞板飞线也应该在芯片引脚附近焊一个瓷片电容。4.3 实测波形与性能数据在72MHz主频的STM32F103上GPIO翻转速率设置为50MHz实测软件模拟时序发送两片16位数据加一次锁存总共耗时约6us等效速度为2.7Mbps左右。这个速度驱动LED流水灯、数码管动态扫描完全够用动态扫描4位数码管时刷新率可以达到200Hz以上肉眼完全看不到闪烁。如果对速度有更高要求把发送循环展开成固定8次判断并用dat (0x80 i)的位掩码方式操作可以进一步缩短到4us以内。再往上就得用硬件SPI了SPI时钟配置为18MHz时16位数据加锁存操作总计不到2us。5. 常见问题排查与避坑指南5.1 实战中遇到的5个经典问题现象可能原因快速定位方法解决方案所有输出都是高电平芯片未工作或OE悬空万用表测OE引脚电压OE接GND低电平使能输出有数据输出但完全不变化RCLK没有锁存脉冲示波器看RCLK引脚检查锁存代码是否在移位完成后执行级联后两块芯片数据互换发送顺序写成了正序对比buf[0]实际输出的芯片编号改为倒序发送数码管/灯带有重影未锁存或每字节都锁存了一次用逻辑分析仪看RCLK上升沿次数确保只有一次锁存输出出现随机乱码电源毛刺或数据线干扰示波器抓VCC和SER波形加去耦电容缩短飞线时钟线远离电源线5.2 几个调试中的独家心得心得一善用发光二极管观察数据流。在SER、SRCLK、RCLK三个引脚上各接一个LED引脚拉高时LED亮、拉低时灭。这样软件跑起来后你能肉眼看到数据线在跳变、时钟线在闪烁锁存线偶尔闪一下。如果数据线完全不动说明程序没进发送循环如果时钟线没闪说明初始化或时钟配置有问题。这个土办法在没有示波器的时候极其管用。心得二把OE接GPIO而不是直接接地。如果OE13脚由STM32的一个GPIO控制你可以在主循环的任意位置把OE拉高瞬间熄灭所有输出实现一键黑屏效果。在动态扫描显示中做一个淡入淡出效果也只需要在OE引脚上输出PWM信号。不要小看这一个引脚它能让你免去很多额外硬件设计。心得三级联超过4片后建议在QH与下一片SER之间串联一个100Ω电阻。高速移位时长走线会产生振铃和过冲串电阻能吸收一部分反射。我的板子级联到6片时遇到第5片偶尔数据错一位的问题加了电阻之后彻底解决。5.3 硬件SPI版本的快速改造思路如果你已经跑通软件模拟版本想换成硬件SPI只需保留HC595_WriteBuffer的倒序发送逻辑把HC595_ShiftByte内部替换成SPI发送锁存仍然用GPIO控制。SPI参数这样配置模式0CPOL0CPHA0MSB先行时钟分频到1~4MHz起步确认正常后再提速。void HC595_ShiftByte(uint8_t dat) { /* SPI发送一个字节SCK上升沿自动移出数据 */ HAL_SPI_Transmit(hspi1, dat, 1, 10); }有一点需要提醒SPI的SCK引脚空闲电平在模式0下是低电平如果SPI配置成模式3CPOL1SCK空闲是高电平与74HC595要求的时序不匹配数据会全面错乱。所以首选模式0确认无误后再去优化速度。6. 扩展应用从LED控制到更多场景74HC595的应用不止是点灯。这个方案在我后续的项目中用过好几次驱动4位共阴数码管时把段码通过级联芯片输出位选用三极管扩流一颗STM32同时驱动两块4位数码管还绰绰有余做简易LED点阵屏时用两片74HC595分别控制行和列16x16的点阵也就是4片芯片的事甚至有一个小项目里用它输出并行数据给老式的LCD1602并口接口硬是把八根数据线压缩成了三根线。如果输出端要驱动继电器或电磁阀注意74HC595的驱动能力有限不要在Q端直接接感性负载加一颗ULN2003或者用三极管搭达林顿结构即可。LED串接限流电阻时使用灌电流方式LED阳极接VCC阴极接Q端可以让亮灭控制更直观因为74HC595灌电流能力通常比拉电流强一些。这个驱动的核心思路——用有限资源换无限扩展——在嵌入式开发中是一个很通用的方法论。理解了74HC595的移位寄存器和锁存机制之后你会发现它和74HC165串行输入并行输出、74HC1648位移位寄存器等芯片都是同一个套路花一天时间吃透一颗芯片换来的是一整类芯片的触类旁通。本文还有配套的精品资源点击获取