简介:这是一份面向STM32初学者的74HC595驱动数码管源码示例,核心解决单片机IO资源不足时如何通过串转并方式扩展LED显示端口的问题。工程仅两个文件(一个头文件与一个C源文件),代码量精简,适合直接阅读和移植;源码中演示了74HC595移位寄存器工作原理,包括串行数据写入、时钟触发、锁存输出等关键时序,并提供了常用段码表与多位数码管动态扫描方法。文件包仅1KB,无多余工程配置,可快速集成到Keil或IAR环境;已有526人学习/下载,适用于课程设计、电子竞赛或个人DIY。通过这份代码,可以直观理解级联能力,用少量IO控制多组数码管,并为后续扩展矩阵按键或点阵屏打下基础,适合配合原理图快速验证。
1. 三根线驱动八位数码管:74HC595把STM32从IO困局里捞出来
做多位数码管显示最头疼的不是显示逻辑,而是IO分配。4位7段数码管带小数点要32个段引脚,哪怕只用共阳/共阴加位选扫描,也得吃掉8个段选+4个位选共12个IO;一旦板上还要挂按键、传感器和串口,STM32的GPIO立刻见底。这个LEDS_595方案的思路是把段选全部交给74HC595做串转并,MCU只留出数据、时钟、锁存三根线,剩下的引脚全部还给业务逻辑。595级联之后8位甚至16位数码管依然只有3个IO,显示刷新由软件定时器轮询。这篇文章按实际拆解顺序走:先把595的移位锁存时序说清楚,再贴出可用的驱动写法,最后给动态扫描和级联的完整参数。
2. SPI时序与74HC595移位锁存对齐
2.1 先从引脚关系理解74HC595的移位锁存
74HC595内部是两级寄存器:移位寄存器和输出锁存器。DS引脚的数据在SH_CP上升沿被移入移位寄存器,连续移满8位后,ST_CP产生一个上升沿,把移位寄存器的内容一次性推到Q0~Q7并行输出。还有两个控制脚:OE是输出使能,低电平有效;MR是复位,低电平会把移位寄存器清零。实际画板时OE直接接地、MR接VCC,否则浮空会导致偶发乱码。
| 引脚 | 功能 | 接法建议 |
|---|---|---|
| DS | 串行数据输入 | 接STM32的MOSI或任意GPIO |
| SH_CP | 移位时钟,上升沿采样 | 接SCLK或GPIO,空闲拉低 |
| ST_CP | 锁存时钟,上升沿输出 | 必须由MCU单独控制 |
| OE | 输出使能,低有效 | 直接接GND |
| MR | 复位,低有效 | 接VCC,不可悬空 |
| Q0~Q7 | 并行输出 | 经限流电阻接数码管段引脚 |
时序上最容易被忽略的是ST_CP的控制位置。有人图省事,每发送一位就把ST_CP拉高一次,结果是数码管全程乱闪。正确做法是先让8个数据位全部移入,最后一刻再触发ST_CP,锁存只发生一次。
2.2 硬件SPI与GPIO模拟:两种方式都要会
STM32的硬件SPI可以直接对接595,但有一个细节要先确认:资源配套的leds_595.c是GPIO模拟版本,不依赖SPI外设,目的是让驱动能平移到任何一款STM32上。我一般先在CubeMX里验证时序,再把关键参数搬到寄存器版本里。
CubeMX里打开SPI1,设置为Master,参数如下:
- CPOL = Low,即空闲时SCLK保持低电平
- CPHA = 1 Edge,数据在第一个沿被采样
- 数据宽度 8 Bits
- 波特率预分频 /32,72MHz主频下得到2.25MHz的SCLK
- MSS = Software,NSS引脚不用
这个组合等价于SPI Mode 0,刚好匹配74HC595的时序要求:SH_CP上升沿采样DS。发数据用HAL_SPI_Transmit,锁存引脚单独操作:
void HC595_SendViaSPI(uint8_t dat) { HAL_SPI_Transmit(&hspi1, &dat, 1, 10); // 发送1字节,超时10ms HC595_ST_CP_LOW(); // 锁存引脚拉低 delay_us(1); HC595_ST_CP_HIGH(); // 上升沿,数据到Q0~Q7 }注意HAL_SPI_Transmit只负责移出8个bit,真正让数码管亮起来的是后面那次ST_CP上升沿。如果把锁存放进中断回调里反复触发,显示会闪烁,所以锁存必须放在主流程或定时器中断的末尾。
GPIO模拟版本盯住三个动作:拉低SH_CP、给DS赋值、拉高SH_CP。每一位一个循环:
#define HC595_DS_GPIO GPIOA #define HC595_DS_PIN GPIO_PIN_7 #define HC595_SH_GPIO GPIOA #define HC595_SH_PIN GPIO_PIN_5 #define HC595_ST_GPIO GPIOA #define HC595_ST_PIN GPIO_PIN_6 void HC595_ShiftByte(uint8_t dat) { for (uint8_t i = 0; i < 8; i++) { HC595_SH_CP_LOW(); // 时钟拉低 if (dat & 0x80) // 高位在前 HC595_DS_HIGH(); else HC595_DS_LOW(); HC595_SH_CP_HIGH(); // 上升沿移入1位 dat <<= 1; } HC595_ST_CP_LOW(); // 8位移完再锁存 HC595_ST_CP_HIGH(); }参数说明:这里按高位先发,因为数码管段码的高位对应a段还是dp段取决于硬件接线,但多数段码表按位7=dp、位6=g设计。移位循环里dat <<= 1是逐位把最高位送到DS上。SH_CP拉高的动作要大于几十纳秒,GPIO翻转速度完全够用。如果改成低位先发,段码表要整体反向排列,两个方案选一个并保持一致。
3. 段码表推算与leds_595.c驱动拆解
3.1 段码表不是背出来的,是算出来的
数码管段码表最容易出错。拿共阴数码管举例:a段接595的Q0,b段接Q1,依此类推到g接Q6、dp接Q7,那么数字0点亮a、b、c、d、e、f,二进制就是0b0111111,也就是0x3F。数字1只点亮b、c,对应0x06。每次换单片机或者换板子,段位顺序一变,表就得重算。
共用一张表,遇到共阳取反即可:
| 字符 | 共阴段码 | 共阳段码 |
|---|---|---|
| 0 | 0x3F | 0xC0 |
| 1 | 0x06 | 0xF9 |
| 2 | 0x5B | 0xA4 |
| 3 | 0x4F | 0xB0 |
| 4 | 0x66 | 0x99 |
| 5 | 0x6D | 0x92 |
| 6 | 0x7D | 0x82 |
| 7 | 0x07 | 0xF8 |
| 8 | 0x7F | 0x80 |
| 9 | 0x6F | 0x90 |
注意8的共阳段码是0x80,很多新手会写成0x00,结果数字8变成全灭。共阳表为什么是反的:共阳数码管公共端接VCC,某段要亮必须给低电平,所以共阴表的每一位取反。
3.2 leds_595.c核心发送函数
资源包里的leds_595.h暴露的函数不多,核心就是发送、锁存、清屏。典型实现:
void LEDS_595_WriteSegment(uint8_t seg_data) { HC595_ShiftByte(seg_data); // 移位寄存器接收段码 HC595_Latch(); // ST_CP上升沿输出 } void LEDS_595_Clear(void) { HC595_ShiftByte(0x00); // 共阴全灭 HC595_Latch(); }这里发送和锁存分成两个函数是刻意的。多位数码管场景下,数据更新的顺序一般是:先关闭所有位选,再逐片发送新段码,最后统一锁存,这样能避免残影。如果每片595都单独锁存一次,位选还亮着,中间态就会被肉眼捕捉到。
3.3 静态显示与位选的关系
单个数码管只靠一片595就能亮,但四位以上必须处理位选。位选信号控制当前哪一位数码管被通电,位选可以挂在595上,也可以直接由STM32的GPIO输出。
挂在GPIO上时逻辑最简单:
void LEDS_595_DisplayDigit(uint8_t pos, uint8_t num) { uint8_t code = segment_table[num]; LEDS_595_Clear(); // 关位选,防止残影 LEDS_595_WriteSegment(code); // 段码进入595 GPIO_SetBits(SEG_GPIO, 1 << pos); // 打开第pos位 }注意Clear和WriteSegment之间不要插入延时。位选打开之前必须先让段码在595输出端稳定,否则数码管会闪出上一个字符的残像。实际工程中很多人把595挂在GPIO位选后面,段码晚到半个时钟周期,肉眼看到的就是重影。把Clear放在最前面,利用595锁存的瞬间同时切断所有位,再更新数据,问题就没了。
4. 动态扫描刷新率与74HC595级联扩展
4.1 为什么多位数码管必须动态扫描
每位数码管独立静态显示需要8个段引脚加1个公共端,4位就是36个引脚,这在任何单片机上都不可接受。动态扫描的核心是视觉暂留:同一时刻只点亮一位,轮流点亮所有位,刷新率足够高时人眼看到的是完整数字。
扫描频率至少要50Hz才不会闪,工程上取100Hz比较稳,也就是一轮完整的4位扫描耗时10ms,每位点亮2.5ms。这个参数在STM32定时器中断里实现:中断周期 = 10ms / 位数。4位取2.5ms,8位取1.25ms。周期太短,单段点亮时间不足会显得暗;太长则闪烁。用定时器做时基比在主循环里delay可靠得多,主循环被串口或按键阻塞时显示不会顿挫。
4.2 定时器中断里的扫描循环
volatile uint8_t scan_pos = 0; uint8_t display_buf[4] = {1, 2, 3, 4}; // 要显示的数字 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); LEDS_595_Clear(); // 关闭所有位选 LEDS_595_WriteSegment(segment_table[display_buf[scan_pos]]); SEG_SelectPin(scan_pos); // 打开当前位 scan_pos++; if (scan_pos >= 4) scan_pos = 0; } }逻辑说明:每进一次中断,先把上一轮的位选全部关掉,再送当前位段码,最后选通当前位。Clear和WriteSegment中间不能有其它IO操作,否则595输出端的毛刺会被位选放大。
参数说明:TIM2预分频和重载值的计算方法是 中断频率 = 72MHz / (PSC + 1) / (ARR + 1)。要得到2.5ms中断周期,也就是400Hz频率,PSC取71、ARR取2499,误差为零。如果用的是8位数码管,ARR改成1249即可。中断里不要做耗时操作,段码查表是常量数组访问,微秒级完成。
4.3 级联时先发最后一级
74HC595级联后,DS接前一级的Q7S。数据流从最后一片往前传:先发送最后一片要显示的数据,再发送第一片的数据,这样最先到达的数据最后被推到末端芯片。两片级联驱动8位数码管时:
void LEDS_595_Display8(uint8_t hi_code, uint8_t lo_code) { HC595_ShiftByte(hi_code); // 先送高位片的段码 HC595_ShiftByte(lo_code); // 再送低位片的段码 HC595_Latch(); // 所有数据就位,统一锁存 }级联还有一层好处:段选和位选分开各用一片595。第一片输出段码,第二片输出位选,MCU仍只占用DS、SH_CP、ST_CP三根线。此时位选输出接三极管基极,由三极管驱动数码管的公共端,而不是直接把595输出接数码管。595每个脚在3.3V供电时拉电流只有4mA左右,直接点亮8段数码管会明显变暗,加ULN2003或者S8550扩流是常见做法。
5. 逻辑分析仪验证与常见时序坑
5.1 上板前先用逻辑分析仪抓时序
把DS、SH_CP、ST_CP三根线接到逻辑分析仪上,发送一个0x3F。正确波形应该是:SH_CP产生8个上升沿,每个上升沿前DS的电平已经稳定,第8个上升沿之后ST_CP才出现一个上升沿。如果ST_CP和SH_CP同时跳变,锁存时最后一bit还没移完,显示的就是错误段码。
没有逻辑分析仪时,可以用万用表验证静态输出。把数码管焊下来,给595发送0x01,只有Q0输出高电平。依次发送0x02、0x04、0x08、0x10、0x20、0x40、0x80,每发一次测一次Q0~Q7的电压,电平应该逐位变化。这一步能同时排除虚焊和引脚顺序错误。
5.2 五个高频故障点
| 现象 | 原因 | 排查方向 |
|---|---|---|
| 全部数码管不亮 | OE悬空或MR被拉低 | OE接地、MR接VCC |
| 数字乱跳 | ST_CP在每个bit都触发 | 只在8位移完后锁存 |
| 有重影 | 位选关闭和段码更新顺序反了 | 先Clear再送段码最后选通 |
| 亮度不均 | 扫描周期和位数不匹配 | 总周期10ms除以位数 |
| 级联后高低位数字互换 | 发送顺序反了 | 先送显示在最右端的字节 |
最后一类坑是模板工程常见的:MR引脚没接上拉,上电瞬间595处于复位状态,偶发一次全灭后恢复正常。这属于硬件复位毛刺,软件上可以在初始化时对HC595_ShiftByte(0x00)再锁存一次,把输出寄存器刷成已知状态。GPIO模拟版本移植到新板子时,先确认DS、SH_CP、ST_CP三个引脚都配成推挽输出,不要漏掉GPIO时钟使能。74HC595的VCC建议就近放一个0.1uF去耦电容,级联线超过10cm时在末端并一个100uF电解电容,不然动态扫描电流突变会把MCU电压拉低,严重的会触发看门狗复位。
本文还有配套的精品资源,点击获取