简介: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个:SER(14脚)是串行数据输入,SRCLK(11脚)是移位寄存器时钟,RCLK(12脚)是存储寄存器时钟(也叫锁存时钟),Q0~Q7是并行输出,QH'(9脚)是串行数据输出,还有OE(13脚)和SRCLR(10脚)用来控制输出使能和清零。
数据走的路径是这样的: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片为1,2片为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对应第一个进入的bit,Q0对应最后进入的bit,所以MSB先行时dat的bit7会出现在Q7上,bit0出现在Q0上。如果你希望bit0对应Q0,那就改成从dat & 0x01开始判断,并右移。这一点在写显示驱动时要特别留意,否则数字显示出来会是镜像的。
/** * @brief 级联写入多字节数据 * @param buf 数据缓冲区,buf[0]对应第1片(靠近MCU),buf[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 |
|---|---|---|---|
| 数据 | PA4 | SER(14) | 第1片QH'(9)→ SER |
| 时钟 | PA5 | SRCLK(11) | SRCLK(11)并联 |
| 锁存 | PA6 | RCLK(12) | RCLK(12)并联 |
| 输出使能 | VCC | OE(13) | OE(13) |
| 清零 | VCC | SRCLR(10) | SRCLR(10) |
| 电源 | 3.3V/5V | VCC(16) | VCC(16) |
| 地 | GND | GND(8) | GND(8) |
4.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而不是直接接地。如果OE(13脚)由STM32的一个GPIO控制,你可以在主循环的任意位置把OE拉高,瞬间熄灭所有输出,实现"一键黑屏"效果。在动态扫描显示中做一个淡入淡出效果,也只需要在OE引脚上输出PWM信号。不要小看这一个引脚,它能让你免去很多额外硬件设计。
心得三:级联超过4片后,建议在QH'与下一片SER之间串联一个100Ω电阻。高速移位时,长走线会产生振铃和过冲,串电阻能吸收一部分反射。我的板子级联到6片时遇到第5片偶尔数据错一位的问题,加了电阻之后彻底解决。
5.3 硬件SPI版本的快速改造思路
如果你已经跑通软件模拟版本,想换成硬件SPI,只需保留HC595_WriteBuffer的倒序发送逻辑,把HC595_ShiftByte内部替换成SPI发送,锁存仍然用GPIO控制。SPI参数这样配置:模式0(CPOL=0,CPHA=0),MSB先行,时钟分频到1~4MHz起步,确认正常后再提速。
void HC595_ShiftByte(uint8_t dat) { /* SPI发送一个字节,SCK上升沿自动移出数据 */ HAL_SPI_Transmit(&hspi1, &dat, 1, 10); }有一点需要提醒:SPI的SCK引脚空闲电平在模式0下是低电平,如果SPI配置成模式3(CPOL=1),SCK空闲是高电平,与74HC595要求的时序不匹配,数据会全面错乱。所以首选模式0,确认无误后再去优化速度。
6. 扩展应用:从LED控制到更多场景
74HC595的应用不止是点灯。这个方案在我后续的项目中用过好几次:驱动4位共阴数码管时,把段码通过级联芯片输出,位选用三极管扩流,一颗STM32同时驱动两块4位数码管还绰绰有余;做简易LED点阵屏时,用两片74HC595分别控制行和列,16x16的点阵也就是4片芯片的事;甚至有一个小项目里用它输出并行数据给老式的LCD1602并口接口,硬是把八根数据线压缩成了三根线。
如果输出端要驱动继电器或电磁阀,注意74HC595的驱动能力有限,不要在Q端直接接感性负载,加一颗ULN2003或者用三极管搭达林顿结构即可。LED串接限流电阻时,使用灌电流方式(LED阳极接VCC,阴极接Q端)可以让亮灭控制更直观,因为74HC595灌电流能力通常比拉电流强一些。
这个驱动的核心思路——用有限资源换无限扩展——在嵌入式开发中是一个很通用的方法论。理解了74HC595的移位寄存器和锁存机制之后,你会发现它和74HC165(串行输入并行输出)、74HC164(8位移位寄存器)等芯片都是同一个套路,花一天时间吃透一颗芯片,换来的是一整类芯片的触类旁通。
本文还有配套的精品资源,点击获取