简介:这份资源面向正在学习STM32单片机与数码管驱动开发的嵌入式初学者及进阶者,核心解决如何用SN74HC595串行移位寄存器扩展IO、驱动四位数码管动态显示的问题。压缩包共117个文件,约1.63MB,以C语言源文件与头文件为主,包含stm32f10x系列外设驱动、工程配置与编译产物,如uvprojx工程文件、hex烧录文件、map映射文件及sct分散加载文件,结构完整,可直接在Keil环境下打开编译。资源标签涉及STM32与SN74HC595,适合作为GPIO模拟时序、移位寄存器级联与数码管段码扫描的实践参考。目前已有132人学习下载。读者可从中获得一套可运行的完整工程模板,理解595芯片的数据锁存与时钟控制逻辑,并借助配套博文完成移植与调试,快速掌握四位数码管的显示驱动方法。
1. 基于STM32的SN74HC595控制四位数码管:从引脚到显示,一条能跑通的路
手头有块 STM32 最小系统板,几颗 SN74HC595 和一只四位数码管,想把它们连起来显示点东西,结果发现数码管要么全亮、要么乱码、要么亮度暗得看不清——这是很多人做 STM32 数码管驱动时的真实开局。这个标题讲的就是用 STM32 的普通 GPIO,通过 SN74HC595 串行移位寄存器,去驱动一个四位数码管,把动态扫描这件事从“玄学”变成可复现的工程。它解决的核心问题是:STM32 的 IO 不够用、或者不想让数码管动态扫描占用 CPU 太多时间,于是用 595 做串并转换,把段码和位选都交给它。适合谁?适合刚学完 GPIO 和定时器、想找一个完整外设练手的嵌入式新手,也适合做仪表、工控面板、小型计数器的从业者,需要一套稳定、便宜、可量产的显示方案。
2. 先搞清硬件链路:STM32、SN74HC595 和四位数码管怎么接
2.1 为什么选 595 而不是直接 IO 扫描
四位数码管如果直接由 STM32 驱动,段码需要 8 根线,位选需要 4 根线,一共 12 根 IO。对于引脚紧张的 F103C8T6 来说,这已经占掉一大半。更麻烦的是动态扫描需要定时器中断不断刷新,一旦中断被高优先级任务打断,显示就会闪烁。SN74HC595 把“串行输入、并行输出”这件事做成了硬件:只需要 3 根 STM32 引脚(数据、移位时钟、锁存时钟),就能扩展出 8 位输出。两片级联就是 16 位,刚好覆盖 8 段段码加 4 位位选。常见做法是:第一片 595 输出段码(a~g、dp),第二片 595 输出位选(D1~D4),级联方式是把第一片的 Q7' 接到第二片的 DS。
这里有一个容易翻车的点:四位数码管分共阴和共阳。共阴数码管的段码高电平点亮,位选低电平选中;共阳则相反。595 的输出电流有限,单引脚通常 35mA 左右,但整个芯片有总电流限制。如果数码管每段需要 8mA,8 段全亮就是 64mA,已经超过单芯片安全范围。所以实际电路中,段码输出一般会加限流电阻(220Ω~1kΩ),位选则可能用三极管扩流。我一般会在段码线上串 330Ω,位选直接用 595 灌电流,因为动态扫描时每位数码管只亮 1/4 时间,平均电流不大。
2.2 级联接线与引脚定义
先明确 STM32 侧要用的三个信号:
- STCP(锁存时钟):上升沿把移位寄存器的数据锁到输出
- SHCP(移位时钟):上升沿移入一位
- DS(串行数据):要移入的位
两片 595 级联时,STM32 的 DS 接第一片 DS,第一片 Q7' 接第二片 DS,两片的 SHCP 并在一起接 STM32 同一个引脚,STCP 也并在一起。这样一次移入 16 位,前 8 位会进入第二片,后 8 位留在第一片。具体哪 8 位对应段码、哪 8 位对应位选,取决于你把哪片当第一片。我习惯让靠近 STM32 的那片输出位选,这样代码里先发位选再发段码,锁存后位选和段码同时生效。
下面是一个典型的引脚分配表,基于 STM32F103C8T6:
| STM32 引脚 | 连接目标 | 说明 |
|---|---|---|
| PA5 | 595 第一片 SHCP | 移位时钟,推挽输出 |
| PA6 | 595 第一片 STCP | 锁存时钟,推挽输出 |
| PA7 | 595 第一片 DS | 串行数据,推挽输出 |
| 3.3V | 595 VCC、MR | 电源和主复位 |
| GND | 595 GND、OE | 地,输出使能接低 |
注意 OE 是输出使能,低电平有效,直接接地即可。MR 是主复位,高电平正常工作,接 VCC。如果手头没有 595,也可以用 74HC164 之类的移位寄存器,但 595 多了锁存,动态扫描时不会看到移位过程中的乱码,这是它比 164 更适合数码管的原因。
2.3 四位数码管的内部结构确认
拿到四位数码管先别急着写代码,用万用表二极管档测一下。共阴数码管的公共端是阴极,找到四个公共引脚,分别对应四位。段引脚是 a~g、dp,一般分布在两侧。常见型号 3641AS 是共阴,3641BS 是共阳。如果测出来公共端到段引脚正向压降约 1.8V,红表笔接公共端时亮,那就是共阴。这一步省不得,接反了轻则不亮,重则烧段。
3. 用 STM32 标准库写 595 驱动:移位、锁存和动态扫描
3.1 移位和锁存的时序代码
595 的时序不复杂,但顺序错了就会显示错位。核心是:先把 16 位数据按位从高到低或从低到高移入,然后给 STCP 一个上升沿锁存。下面是一段基于 STM32 标准库的驱动代码,假设 SHCP 在 PA5,STCP 在 PA6,DS 在 PA7。
#include "stm32f10x.h" #define SHCP_PIN GPIO_Pin_5 #define STCP_PIN GPIO_Pin_6 #define DS_PIN GPIO_Pin_7 #define PORT GPIOA void HC595_Init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin = SHCP_PIN | STCP_PIN | DS_PIN; gpio.GPIO_Mode = GPIO_Mode_Out_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(PORT, &gpio); GPIO_ResetBits(PORT, SHCP_PIN | STCP_PIN | DS_PIN); } // 移入一个字节,MSB 先出 void HC595_ShiftByte(uint8_t data) { for (uint8_t i = 0; i < 8; i++) { if (data & 0x80) GPIO_SetBits(PORT, DS_PIN); else GPIO_ResetBits(PORT, DS_PIN); // 上升沿移位 GPIO_ResetBits(PORT, SHCP_PIN); GPIO_SetBits(PORT, SHCP_PIN); data <<= 1; } } // 锁存,上升沿把移位数据送到输出 void HC595_Latch(void) { GPIO_ResetBits(PORT, STCP_PIN); GPIO_SetBits(PORT, STCP_PIN); } // 发送 16 位:先发位选,再发段码 void HC595_Send(uint8_t seg, uint8_t pos) { HC595_ShiftByte(pos); // 位选字节 HC595_ShiftByte(seg); // 段码字节 HC595_Latch(); }逻辑说明:HC595_ShiftByte每次处理最高位,先设置 DS,然后给 SHCP 一个上升沿,数据进入移位寄存器。循环 8 次后一个字节移完。HC595_Send先移位选再移段码,因为级联时先移入的会跑到第二片,后移入的留在第一片。如果你的接线是反的,把这两个参数调换即可。参数说明:seg是段码字节,pos是位选字节,位选通常只有低 4 位有效,高 4 位补 0。
3.2 共阴段码表和位选编码
段码表取决于你怎么接 a~g 和 dp 到 595 的输出。假设 595 的 Q0 接 a,Q1 接 b,…… Q7 接 dp,共阴数码管高电平点亮,那么数字 0 的段码是 a、b、c、d、e、f 亮,g、dp 灭,二进制00111111,即 0x3F。下面是一张常用段码表:
| 数字 | 段码(Q7~Q0) | 十六进制 |
|---|---|---|
| 0 | 0011 1111 | 0x3F |
| 1 | 0000 0110 | 0x06 |
| 2 | 0101 1011 | 0x5B |
| 3 | 0100 1111 | 0x4F |
| 4 | 0110 0110 | 0x66 |
| 5 | 0110 1101 | 0x6D |
| 6 | 0111 1101 | 0x7D |
| 7 | 0000 0111 | 0x07 |
| 8 | 0111 1111 | 0x7F |
| 9 | 0110 1111 | 0x6F |
位选编码:假设第二片 595 的 Q0~Q3 分别接四位公共端,共阴数码管位选低电平有效,那么选中第 1 位是1110,第 2 位1101,第 3 位1011,第 4 位0111。如果位选高电平有效,取反即可。注意位选字节只有低 4 位有意义,高 4 位写 0。
3.3 定时器中断做动态扫描
动态扫描的本质是:每一时刻只点亮一位,四位轮流,利用人眼视觉暂留。扫描频率太低会闪,太高会暗。一般每位点亮 1~2ms,四位一轮 4~8ms,刷新率 125~250Hz。用 SysTick 或者 TIM2 定时中断都可以。下面用 TIM2 每 1ms 中断一次,在中断里切换位。
volatile uint8_t disp_buf[4] = {0, 0, 0, 0}; // 四位要显示的数字 volatile uint8_t cur_pos = 0; const uint8_t seg_table[10] = {0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F}; const uint8_t pos_table[4] = {0x0E, 0x0D, 0x0B, 0x07}; // 低电平有效 void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); // 先消隐,避免鬼影 HC595_Send(0x00, 0x00); // 发送当前位的段码和位选 HC595_Send(seg_table[disp_buf[cur_pos]], pos_table[cur_pos]); cur_pos = (cur_pos + 1) & 0x03; } }逻辑说明:中断里先发一次全灭,再发当前位的数据,这是消除鬼影的关键。如果不消隐,上一位的段码会短暂停留在下一位上,看起来就是重影。参数说明:disp_buf存放四位要显示的数字,范围 0~9;pos_table是位选编码,低电平有效;cur_pos循环 0~3。TIM2 初始化时设预分频和重装载值,让中断频率为 1kHz。
4. 调亮度、消鬼影、省 IO:595 驱动数码管的避坑清单
4.1 显示重影:现象、原因和解决
现象:数码管每一位上都能看到相邻位的数字残影,尤其是数字变化时更明显。原因:动态扫描切换位选时,段码还没有完全关断,或者锁存时序不对,导致上一位的段码串到下一位。解决:在每次发送新位数据之前,先发送一次段码全灭(0x00),再发送新段码和位选。如果还不行,检查 STCP 锁存是否在数据移位完成后才给上升沿,有些代码把锁存放在移位中间,就会出错。
4.2 亮度不均:现象、原因和解决
现象:四位数字亮度不一样,或者整体偏暗。原因:限流电阻太大,或者位选驱动能力不足。共阴数码管的位选是灌电流,595 单引脚灌电流能力有限,四位轮流时平均电流小,但瞬时电流可能超过 595 的总限制。解决:段码限流电阻降到 220Ω 左右,位选如果电流大,加 S8050 三极管扩流。另外检查扫描频率,如果每位点亮时间太短,亮度也会下降,可以适当加大占空比,但不要超过 2ms,否则会闪。
4.3 上电乱码:现象、原因和解决
现象:STM32 刚上电时,数码管显示乱码或者全亮,过一会儿才正常。原因:595 上电时移位寄存器和输出锁存器状态不确定,STM32 初始化前引脚浮空,595 可能输出随机值。解决:在 595 的 OE 引脚上加一个上拉电阻到 VCC,或者用 STM32 的一个 IO 控制 OE,初始化时先拉高禁用输出,等 595 初始化完成后再拉低。另外 MR 引脚不要悬空,接 VCC。
4.4 级联顺序错:现象、原因和解决
现象:段码和位选反了,比如该亮第一位时第二位亮,或者数字显示成乱码。原因:两片 595 的级联顺序和代码里发送顺序不匹配。解决:确认第一片 Q7' 接第二片 DS,代码里先发的字节会进入第二片。如果发现反了,把HC595_Send里的两个参数调换,或者把两片的接线对调。用万用表测一下每片 595 的输出,就能定位。
4.5 中断冲突:现象、原因和解决
现象:数码管闪烁,或者某些位不亮,尤其是在有串口中断、定时器中断同时运行时。原因:动态扫描中断被高优先级中断打断,导致某一位点亮时间过长或过短。解决:把扫描中断优先级设高一点,或者用 DMA 加定时器自动发送,减少中断里执行的时间。如果用了 FreeRTOS,扫描任务优先级要高于其他非实时任务,但不要放在中断里做太多事。
5. 把显示封装成可复用模块:从计数到滚动显示的进阶技巧
5.1 用结构体管理显示缓冲区
前面disp_buf是全局数组,实际项目里我会把它封装成一个结构体,带上小数点、闪烁标志和更新标志。这样上层应用只需要调用Display_SetNumber(1234),底层自动拆位。下面是一个简化版:
typedef struct { uint8_t digit[4]; uint8_t dot_mask; // 哪一位亮小数点 uint8_t blink_mask; // 哪一位闪烁 uint8_t dirty; } Display_t; Display_t disp; void Display_SetNumber(uint16_t num) { disp.digit[0] = num / 1000; disp.digit[1] = (num / 100) % 10; disp.digit[2] = (num / 10) % 10; disp.digit[3] = num % 10; disp.dirty = 1; }在定时器中断里根据dot_mask和blink_mask决定是否给段码加上 0x80(小数点)或者输出全灭。这样显示逻辑和业务逻辑分开,改起来不容易翻车。
5.2 用移位代替除法做拆位
如果嫌除法慢,可以用查表或者移位。但 4 位数除法在 72MHz 的 STM32 上也就几个微秒,没必要过度优化。真正需要注意的是disp_buf在中断里被读取,在主循环里被写入,要加临界区保护。我一般会在Display_SetNumber里关中断,写完再开,避免拆位过程中被扫描中断打断导致某一位显示旧值。
5.3 验证显示是否稳定的三个方法
第一,用手机慢动作拍摄数码管,如果每一帧都是完整的数字,没有半亮或重影,说明扫描和消隐没问题。第二,把扫描频率从 1kHz 降到 100Hz,如果开始闪,说明频率余量不够,正常应该 200Hz 以上。第三,长时间运行 24 小时,观察有没有某一位变暗或熄灭,如果有,检查 595 的温升和限流电阻功率。我自己的习惯是,每次做完数码管驱动,都会先让它在实验室跑一晚上,第二天再看一眼,很多接触不良和时序边界问题都是这样暴露出来的。希望帮到你。
本文还有配套的精品资源,点击获取