188数码管从入门到精通
188数码管从入门到精通
适用对象: 嵌入式开发初学者、电子爱好者、Arduino/STM32开发者
涵盖平台: Arduino · STM32 · ESP32 · 51单片机
驱动方案: TM1637 · 74HC595 · MAX7219 · 三极管动态扫描
目录
1. 什么是188数码管
1.1 基本概念
188数码管是电子市场上最常见的一类 4 位 7 段数码管模组的俗称。"188"这个名称源于早期某款经典 4 位数码管模块的产品编号,后来被广泛用来指代0.56 英寸、4 位、共阳极/共阴极的红色(或其它颜色)7 段 LED 数码管。
它通常以两种形态出现:
| 形态 | 说明 | 常见驱动器 |
|---|---|---|
| 裸数码管 | 仅 12 个引脚(4 位选 + 8 段选),需自行搭建驱动电路 | 三极管 + MCU IO |
| 带驱动模块 | PCB 上已集成驱动芯片,仅需 2~4 根线即可控制 | TM1637 / TM1650 / 74HC595 |
1.2 为什么叫"7段数码管"



每一个数字位由 7 个 LED 段 加 1 个小数点 组成,分别命名为:
a
---
f | g | b
---
e | | c
---
d · dp

通过点亮不同的段组合,可以显示数字 0~9、部分字母(A、b、C、d、E、F)以及自定义图案。
1.3 188数码管的应用场景
- 🔢 数字时钟 — 显示 HH:MM 时间
- 🌡️ 温湿度计 — 与 DHT11/DHT22 配合显示温湿度
- ⏱️ 倒计时器 — 秒表、厨房计时器
- 📊 计数器 — 生产线计数、人数统计
- 🔬 仪器仪表 — 电压表、电流表、频率计
2. 工作原理
2.1 单个7段数码管的驱动
一个单独的 7 段数码管(含小数点共 8 段),需要 8 个 IO 口 来控制每一段的亮灭。对共阴极数码管,COM 端接 GND,各段接高电平时点亮;对共阳极数码管,COM 端接 VCC,各段接低电平时点亮。
共阴极:COM → GND,段引脚 → 高电平 = 亮
共阳极:COM → VCC,段引脚 → 低电平 = 亮

2.2 多位数码管与动态扫描
如果 4 位数码管各段都独立连接到 MCU,需要 4 × 8 = 32 个 IO 口——这在实际应用中是不可接受的。
解决方法:段选线复用 + 动态扫描
段选线 (a b c d e f g dp)
│ │ │ │ │ │ │ │
┌────────┼──┼──┼──┼──┼──┼──┼──┼────────┐
│ │ │ │ │ │ │ │ │ │
┌──┴──┐ ┌──┴──┐ ┌──┴──┐ ┌──┴──┐ 段选线
│ 位1 │ │ 位2 │ │ 位3 │ │ 位4 │ 并联
└──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘
│ │ │ │
D1 D2 D3 D4 ← 位选线(独立)
- 段选线(8根):所有位共用,决定显示什么图案
- 位选线(4根):每位独立,决定哪个位被点亮
动态扫描过程:
时刻 t1:位1 使能 → 段选线输出"数字1"的段码 → 位1 亮,其余灭
时刻 t2:位2 使能 → 段选线输出"数字2"的段码 → 位2 亮,其余灭
时刻 t3:位3 使能 → 段选线输出"数字3"的段码 → 位3 亮,其余灭
时刻 t4:位4 使能 → 段选线输出"数字4"的段码 → 位4 亮,其余灭
→ 回到 t1,循环往复
关键参数:
- 扫描频率 ≥ 50Hz(每位刷新率 ≥ 50Hz,4 位则扫描频率 ≥ 200Hz)
- 利用人眼的视觉暂留效应,看起来就像 4 位同时点亮
- 每位导通时间 = 扫描周期 / 位数(4 位时各占 1/4 时间)
- 代价:亮度降低 → 通过增大峰值电流补偿(通常 5~10 倍静态电流)
2.3 段码表
以共阴极数码管为例(1 = 亮,0 = 灭,段序:dp g f e d c b a):
| 字符 | dp | g | f | e | d | c | b | a | 十六进制 |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 0x3F |
| 1 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0x06 |
| 2 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 0x5B |
| 3 | 0 | 1 | 0 | 0 | 1 | 1 | 1 | 1 | 0x4F |
| 4 | 0 | 1 | 1 | 0 | 0 | 1 | 1 | 0 | 0x66 |
| 5 | 0 | 1 | 1 | 0 | 1 | 1 | 0 | 1 | 0x6D |
| 6 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 1 | 0x7D |
| 7 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0x07 |
| 8 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0x7F |
| 9 | 0 | 1 | 1 | 0 | 1 | 1 | 1 | 1 | 0x6F |
| A | 0 | 1 | 1 | 1 | 0 | 1 | 1 | 1 | 0x77 |
| b | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0x7C |
| C | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 1 | 0x39 |
| d | 0 | 1 | 0 | 1 | 1 | 1 | 1 | 0 | 0x5E |
| E | 0 | 1 | 1 | 1 | 1 | 0 | 0 | 1 | 0x79 |
| F | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 1 | 0x71 |
| - | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0x40 |
| 灭 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0x00 |
注意: 共阳极数码管的段码是上述值的按位取反(
~segCode & 0xFF)。
3. 常见型号与脚位定义
3.1 经典 4 位 12Pin 裸数码管(0.56 英寸)
市面上最常见的 4 位 0.56 英寸数码管为 12 引脚直插封装,引脚排列如下(底视图,左下角为 Pin1):
┌─────────────────────────┐
Pin1 │○ ○│ Pin12
│ │
│ □ □ □ □ │
│ │
│ │
└─────────────────────────┘
典型脚位定义(共阳极 — 5641AS 为例):
| 引脚 | 功能 | 引脚 | 功能 |
|---|---|---|---|
| 1 | E 段 | 7 | A 段 |
| 2 | D 段 | 8 | F 段 |
| 3 | C 段 | 9 | G 段 |
| 4 | B 段 | 10 | DP(小数点) |
| 5 | 位4(千位) | 11 | 位3(百位) |
| 6 | 位2(十位) | 12 | 位1(个位) |
⚠️ 重要: 不同厂家、不同型号的脚位可能不同,务必查阅对应的数据手册或用万用表二极管档实测确认。
3.2 TM1637 驱动模块
这是目前最流行的带驱动成品模块,淘宝几块钱一个:
TM1637 Module
┌─────────────────┐
│ 8 8 : 8 8 │
│ │
│ GND VCC DIO CLK │ ← 仅 4 个引脚!
└─────────────────┘
| 引脚 | 说明 |
|---|---|
| VCC | 电源正(3.3V ~ 5V) |
| GND | 电源地 |
| DIO | 数据线(双向) |
| CLK | 时钟线 |
优点: 仅需 2 个 MCU IO 口,内置动态扫描,8 级亮度可调,极低成本。
3.3 74HC595 驱动模块
另一常见方案,模块上已集成 74HC595 移位寄存器:
| 引脚 | 说明 |
|---|---|
| VCC | 电源(3.3V ~ 5V) |
| GND | 电源地 |
| SDI (DS) | 串行数据输入 |
| SCK (SH_CP) | 移位时钟 |
| RCK (ST_CP) | 锁存时钟 |
3.4 MAX7219 方案
MAX7219 是一款专业的 LED 显示驱动芯片,可驱动多达 8 位 7 段数码管或 8×8 LED 点阵:
| 引脚 | 说明 |
|---|---|
| DIN | 串行数据输入 |
| CS (LOAD) | 片选 / 数据锁存 |
| CLK | 时钟 |
| VCC / GND | 电源 |
优点: 无需动态扫描(芯片自动处理),16 级亮度调节,支持级联。





4. 驱动方式详解
4.1 方式一:MCU 直驱 + 动态扫描
原理: 8 个段选 IO → 经限流电阻 → 段选线;4 个位选 IO → 经三极管 → 位选线。
MCU (Arduino / STM32)
┌────────────────────────────────┐
│ │
│ PA0─┬─[220Ω]── a ────┐ │
│ PA1─┬─[220Ω]── b ────┤ │
│ PA2─┬─[220Ω]── c ────┤ │
│ PA3─┬─[220Ω]── d ────┤─── 数码管段选
│ PA4─┬─[220Ω]── e ────┤ │
│ PA5─┬─[220Ω]── f ────┤ │
│ PA6─┬─[220Ω]── g ────┤ │
│ PA7─┬─[220Ω]── dp ───┘ │
│ │
│ PB0 ────────[1K]───┬─ S8050 ── 位1 (共阴则 PNP) │
│ PB1 ────────[1K]───┬─ S8050 ── 位2
│ PB2 ────────[1K]───┬─ S8050 ── 位3
│ PB3 ────────[1K]───┬─ S8050 ── 位4
│ │
└────────────────────────────────┘
三极管选型:
- 共阴极数码管: 位选用 NPN(如 S8050、2N3904),集电极接数码管 COM,发射极接 GND
- 共阳极数码管: 位选用 PNP(如 S8550、2N3906),发射极接 VCC,集电极接数码管 COM
限流电阻计算:
R = (VCC - Vf_LED - Vce_sat) / I_segment
例:5V 供电,红色 LED Vf ≈ 2V,三极管饱和压降 ≈ 0.2V,目标段电流 10mA
R = (5V - 2V - 0.2V) / 0.01A = 280Ω → 取标准值 270Ω 或 330Ω
动态扫描时,每位只导通 1/4 时间,等效平均电流只有静态的 1/4。可以适当增大峰值电流(减小限流电阻至 100Ω~150Ω)来维持亮度,但不要超过 LED 最大脉冲电流(通常 50~100mA)。
4.2 方式二:TM1637(推荐入门)
TM1637 是专门为数码管驱动设计的芯片,内部集成了:
- 按键扫描接口(可接最多 16 个按键)
- 8 级亮度 PWM 调节
- 内部 RC 振荡器(无需外部时钟)
- I2C-like 两线通信协议
通信协议(非标准 I2C,需软件模拟):
起始条件: CLK=H, DIO H→L
停止条件: CLK=H, DIO L→H
数据传输: CLK=L → 设 DIO → CLK=H(上升沿采样)
应答位: 第 8 个 CLK 后,从机拉低 DIO 一个 CLK 周期
数据命令:
| 命令 | 值 | 说明 |
|---|---|---|
| 数据命令 | 0x40 | 地址自动加 1 模式 |
| 地址命令 | 0xC0 | 起始地址 (C0 ~ C5) |
| 显示控制 | 0x80 ~ 0x8F | bit0~2: 亮度 (0~7),bit3: 开/关显示 |
发送流程:
1. 发起始条件
2. 写 0x40(数据命令:自动地址增加)
3. 发停止条件
4. 发起始条件
5. 写 0xC0(地址命令:从 0x00 开始)
6. 依次写 N 个数据字节(段码)
7. 发停止条件
8. 发起始条件
9. 写 0x8F(显示开,最大亮度)
10. 发停止条件
4.3 方式三:74HC595 移位寄存器
74HC595 是 8 位串入并出移位寄存器,每片可控制 8 个段选。4 位共阳极数码管通常需要 1 片 74HC595 控制段选 + 4 个 IO 控制位选,或者 2 片级联 分别控制段选和位选。
发送字节流程:
for (bit = 7; bit >= 0; bit--) {
SDI = (data >> bit) & 0x01; // 先发高位
SCK = 1; // 上升沿移位
SCK = 0;
}
RCK = 1; // 上升沿锁存输出
RCK = 0;
4.4 四种方案对比
| 特性 | MCU 直驱 | TM1637 | 74HC595 | MAX7219 |
|---|---|---|---|---|
| IO 占用 | 12 个 | 2 个 | 3 个 | 3 个 |
| 硬件复杂度 | 高 | 极低 | 中 | 中 |
| 亮度控制 | 软件 PWM | 8 级内置 | 手动 | 16 级内置 |
| 扫描负担 | MCU 承担 | 芯片自动 | MCU 承担 | 芯片自动 |
| 最大位数 | 受 IO 限制 | 6 位 | 8 位×N | 8 位×N |
| 成本 | 低 | 极低 | 低 | 较高 |
| 上手难度 | ⭐⭐⭐⭐ | ⭐ | ⭐⭐ | ⭐⭐ |
| 推荐场景 | 学习原理 | 入门首选 | 灵活扩展 | 高端应用 |
5. 硬件接线指南
5.1 TM1637 模块 → Arduino UNO
TM1637 Arduino
VCC → 5V
GND → GND
DIO → D2
CLK → D3
5.2 TM1637 模块 → STM32 (STM32F103C8T6)
TM1637 STM32
VCC → 3.3V (或 5V)
GND → GND
DIO → PB6 (开漏输出 + 上拉)
CLK → PB7 (开漏输出 + 上拉)
5.3 4位共阳极数码管 + 三极管 → Arduino
段选 (通过 220Ω 电阻):
D2 → a, D3 → b, D4 → c, D5 → d
D6 → e, D7 → f, D8 → g, D9 → dp
位选 (通过 1KΩ 电阻 → PNP 三极管基极 S8550):
D10 → 位1(个位), D11 → 位2(十位)
D12 → 位3(百位), D13 → 位4(千位)
5.4 接线注意事项
- 限流电阻不可省! 否则烧 LED 或 MCU IO 口
- TM1637 的 DIO 和 CLK 线上拉 4.7KΩ 到 VCC(部分模块已内置上拉)
- 3.3V 系统驱动 5V 模块时:确认模块是否兼容 3.3V 逻辑电平;否则需加电平转换
- 电源去耦:VCC 与 GND 间加 100nF + 10μF 电容
- 共阴/共阳确认:用万用表二极管档(红笔接 COM,黑笔接段 → 亮 = 共阴;反之 = 共阳)
6. 编程实战
6.1 Arduino + TM1637(使用 TM1637Display 库)
安装库: Arduino IDE → 项目 → 加载库 → 管理库 → 搜索 “TM1637” → 安装 TM1637Display by Avishay Orpaz
/**
* TM1637 四位数码管显示示例
* 功能:交替显示数字和温度值
* 接线:CLK → D3, DIO → D2
*/
#include <TM1637Display.h>
#define CLK 3
#define DIO 2
TM1637Display display(CLK, DIO);
// 共阳极段码表(该库默认使用共阳极段码)
const uint8_t SEG_DONE[] = {
SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F, // 0
SEG_B | SEG_C, // 1
SEG_A | SEG_B | SEG_D | SEG_E | SEG_G, // 2
SEG_A | SEG_B | SEG_C | SEG_D | SEG_G, // 3
SEG_B | SEG_C | SEG_F | SEG_G, // 4
SEG_A | SEG_C | SEG_D | SEG_F | SEG_G, // 5
SEG_A | SEG_C | SEG_D | SEG_E | SEG_F | SEG_G, // 6
SEG_A | SEG_B | SEG_C, // 7
SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F | SEG_G, // 8
SEG_A | SEG_B | SEG_C | SEG_D | SEG_F | SEG_G, // 9
};
void setup() {
display.setBrightness(7); // 亮度 0~7,7 最亮
display.clear();
}
void loop() {
// 显示数字 1234
display.showNumberDec(1234, false); // false = 不显示前导零
delay(2000);
// 显示温度 25°C(带 °C 后缀)
int temp = 25;
display.showNumberDec(temp, false, 2, 0); // 2位数字 + °C
delay(2000);
// 显示带冒号的时间 12:59
uint8_t data[] = { 0xFF, 0xFF, 0xFF, 0xFF };
data[0] = display.encodeDigit(1);
data[1] = display.encodeDigit(2) | 0x80; // bit7 = 冒号亮
data[2] = display.encodeDigit(5);
data[3] = display.encodeDigit(9);
display.setSegments(data);
delay(2000);
// 显示自定义段
uint8_t custom[] = {
SEG_A | SEG_B | SEG_C | SEG_D | SEG_E | SEG_F, // 0
SEG_B | SEG_C, // 1
SEG_A | SEG_B | SEG_G | SEG_E | SEG_D, // 2
SEG_A | SEG_B | SEG_C | SEG_D | SEG_G // 3
};
display.setSegments(custom);
delay(2000);
}
6.2 STM32 HAL 库 + TM1637(寄存器级驱动)
/**
* tm1637.h
* STM32 HAL 库 TM1637 驱动
*/
#ifndef __TM1637_H__
#define __TM1637_H__
#include "main.h"
#define TM1637_CLK_PORT GPIOB
#define TM1637_CLK_PIN GPIO_PIN_6
#define TM1637_DIO_PORT GPIOB
#define TM1637_DIO_PIN GPIO_PIN_7
/* 显示控制命令 */
#define TM1637_CMD_DATA 0x40 // 数据命令:地址自动加1
#define TM1637_CMD_ADDR 0xC0 // 地址命令起始
#define TM1637_CMD_DISP_ON 0x88 // 显示开 + 亮度 0
#define TM1637_CMD_DISP_OFF 0x80 // 显示关
void TM1637_Init(void);
void TM1637_DisplayNum(int16_t num);
void TM1637_DisplayTime(uint8_t hour, uint8_t minute, uint8_t colon);
void TM1637_SetBrightness(uint8_t brightness);
void TM1637_Clear(void);
#endif /* __TM1637_H__ */
/**
* tm1637.c
* STM32 HAL 库 TM1637 驱动实现
*/
#include "tm1637.h"
/* 共阴极段码表 (0~9, A~F, -, 灭) */
static const uint8_t segCode[] = {
0x3F, // 0
0x06, // 1
0x5B, // 2
0x4F, // 3
0x66, // 4
0x6D, // 5
0x7D, // 6
0x07, // 7
0x7F, // 8
0x6F, // 9
0x77, // A
0x7C, // b
0x39, // C
0x5E, // d
0x79, // E
0x71, // F
0x40, // -
0x00 // 灭
};
static uint8_t _brightness = 7;
/* ---- 底层 IO 操作 ---- */
/* 注意:TM1637 要求开漏输出 + 外部上拉;或推挽输出 + 方向切换 */
static void DIO_Out(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = TM1637_DIO_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD; // 开漏输出
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(TM1637_DIO_PORT, &GPIO_InitStruct);
}
static void DIO_In(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = TM1637_DIO_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(TM1637_DIO_PORT, &GPIO_InitStruct);
}
#define CLK_H() HAL_GPIO_WritePin(TM1637_CLK_PORT, TM1637_CLK_PIN, GPIO_PIN_SET)
#define CLK_L() HAL_GPIO_WritePin(TM1637_CLK_PORT, TM1637_CLK_PIN, GPIO_PIN_RESET)
#define DIO_H() HAL_GPIO_WritePin(TM1637_DIO_PORT, TM1637_DIO_PIN, GPIO_PIN_SET)
#define DIO_L() HAL_GPIO_WritePin(TM1637_DIO_PORT, TM1637_DIO_PIN, GPIO_PIN_RESET)
#define DIO_RD() HAL_GPIO_ReadPin(TM1637_DIO_PORT, TM1637_DIO_PIN)
static void delay_us(uint32_t us) {
// 简单延时(或用定时器微秒延时)
for (volatile uint32_t i = 0; i < us * 8; i++) { __NOP(); }
}
/* ---- TM1637 通信协议 ---- */
static void TM1637_Start(void) {
CLK_H();
DIO_H();
delay_us(2);
DIO_L(); // CLK 高时 DIO 下降沿
delay_us(2);
CLK_L();
}
static void TM1637_Stop(void) {
CLK_L();
DIO_L();
delay_us(2);
CLK_H();
delay_us(2);
DIO_H(); // CLK 高时 DIO 上升沿
delay_us(2);
}
static uint8_t TM1637_WriteByte(uint8_t data) {
for (uint8_t i = 0; i < 8; i++) {
CLK_L();
if (data & 0x01) {
DIO_H();
} else {
DIO_L();
}
delay_us(1);
CLK_H(); // 上升沿写入数据
delay_us(1);
data >>= 1;
}
/* 读应答 (第9个CLK) */
CLK_L();
DIO_H();
DIO_In(); // 切换为输入
delay_us(1);
CLK_H();
delay_us(1);
uint8_t ack = DIO_RD();
CLK_L();
DIO_Out(); // 切回输出
delay_us(1);
return ack; // 0 = ACK, 1 = NACK
}
/* ---- 公共接口 ---- */
void TM1637_Init(void) {
/* 初始化 GPIO */
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = TM1637_CLK_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(TM1637_CLK_PORT, &GPIO_InitStruct);
DIO_Out();
TM1637_Clear();
TM1637_SetBrightness(7);
}
void TM1637_DisplayNum(int16_t num) {
uint8_t disp[4] = {0};
uint8_t negative = 0;
if (num < 0) {
negative = 1;
num = -num;
}
if (num > 9999) num = 9999;
for (int8_t i = 3; i >= 0; i--) {
if (num > 0 || i == 3) {
disp[i] = segCode[num % 10];
num /= 10;
} else if (negative && i == 0) {
disp[i] = segCode[16]; // 显示 '-'
negative = 0;
} else {
disp[i] = segCode[17]; // 灭(不显示前导零)
}
}
/* 发送数据 */
TM1637_Start();
TM1637_WriteByte(TM1637_CMD_DATA);
TM1637_Stop();
TM1637_Start();
TM1637_WriteByte(TM1637_CMD_ADDR | 0x00); // 起始地址
for (uint8_t i = 0; i < 4; i++) {
TM1637_WriteByte(disp[i]);
}
TM1637_Stop();
/* 显示控制 */
TM1637_Start();
TM1637_WriteByte(TM1637_CMD_DISP_ON | (_brightness & 0x07));
TM1637_Stop();
}
void TM1637_DisplayTime(uint8_t hour, uint8_t minute, uint8_t colon) {
uint8_t disp[4];
disp[0] = segCode[hour / 10];
disp[1] = segCode[hour % 10];
if (colon) disp[1] |= 0x80; // 点亮中间冒号
disp[2] = segCode[minute / 10];
disp[3] = segCode[minute % 10];
TM1637_Start();
TM1637_WriteByte(TM1637_CMD_DATA);
TM1637_Stop();
TM1637_Start();
TM1637_WriteByte(TM1637_CMD_ADDR | 0x00);
for (uint8_t i = 0; i < 4; i++) {
TM1637_WriteByte(disp[i]);
}
TM1637_Stop();
TM1637_Start();
TM1637_WriteByte(TM1637_CMD_DISP_ON | (_brightness & 0x07));
TM1637_Stop();
}
void TM1637_SetBrightness(uint8_t brightness) {
_brightness = (brightness > 7) ? 7 : brightness;
}
void TM1637_Clear(void) {
TM1637_Start();
TM1637_WriteByte(TM1637_CMD_DATA);
TM1637_Stop();
TM1637_Start();
TM1637_WriteByte(TM1637_CMD_ADDR | 0x00);
for (uint8_t i = 0; i < 4; i++) {
TM1637_WriteByte(0x00);
}
TM1637_Stop();
}
6.3 Arduino 裸驱动(定时器动态扫描)
当不使用专用驱动芯片,直接用 MCU IO 口驱动 4 位共阳极数码管时,核心是利用定时器中断进行动态扫描:
/**
* Arduino 定时器动态扫描 4 位共阳极数码管
* 接线:
* 段选 D2~D9 → a~dp(经 220Ω 限流)
* 位选 D10~D13 → 位1~位4(经 1K + PNP 三极管 S8550)
*/
#include <TimerOne.h> // 需安装 TimerOne 库
// 段选引脚
const uint8_t segPins[8] = {2, 3, 4, 5, 6, 7, 8, 9};
// 位选引脚
const uint8_t digPins[4] = {10, 11, 12, 13};
// 共阳极段码表(0 = 亮,1 = 灭)
const uint8_t segCode[18] = {
0xC0, // 0
0xF9, // 1
0xA4, // 2
0xB0, // 3
0x99, // 4
0x92, // 5
0x82, // 6
0xF8, // 7
0x80, // 8
0x90, // 9
0x88, // A
0x83, // b
0xC6, // C
0xA1, // d
0x86, // E
0x8E, // F
0xBF, // -
0xFF // 灭
};
// 显示缓冲区:每位要显示的字符索引
volatile uint8_t displayBuffer[4] = {0, 1, 2, 3};
volatile uint8_t currentDigit = 0; // 当前扫描到哪一位
// 定时器中断服务函数 — 每次点亮一位
void scanDisplay() {
// 关掉所有位选(共阳极:位选高电平时灭)
for (uint8_t i = 0; i < 4; i++) {
digitalWrite(digPins[i], HIGH); // PNP → 高电平关
}
// 输出当前位的段码
uint8_t code = segCode[displayBuffer[currentDigit]];
for (uint8_t i = 0; i < 8; i++) {
digitalWrite(segPins[i], (code >> i) & 0x01);
}
// 打开当前位选
digitalWrite(digPins[currentDigit], LOW); // PNP → 低电平开
// 移到下一位
currentDigit++;
if (currentDigit >= 4) currentDigit = 0;
}
void setup() {
// 初始化段选引脚
for (uint8_t i = 0; i < 8; i++) {
pinMode(segPins[i], OUTPUT);
digitalWrite(segPins[i], HIGH); // 共阳极:初始高电平 = 灭
}
// 初始化位选引脚
for (uint8_t i = 0; i < 4; i++) {
pinMode(digPins[i], OUTPUT);
digitalWrite(digPins[i], HIGH); // PNP 截止 = 灭
}
// 启动定时器中断,扫描频率 250Hz(周期 4ms)
// 每位刷新率 = 250Hz / 4 = 62.5Hz > 50Hz,无闪烁
Timer1.initialize(4000); // 4ms = 250Hz
Timer1.attachInterrupt(scanDisplay);
}
void loop() {
// 主循环只需更新显示缓冲区
static unsigned long lastUpdate = 0;
static uint16_t count = 0;
if (millis() - lastUpdate >= 1000) {
lastUpdate = millis();
count++;
// 将计数值分解到 4 位
displayBuffer[0] = (count / 1000) % 10;
displayBuffer[1] = (count / 100) % 10;
displayBuffer[2] = (count / 10) % 10;
displayBuffer[3] = count % 10;
}
}
6.4 ESP32 + TM1637(MicroPython)
"""
ESP32 MicroPython TM1637 驱动
接线:CLK → GPIO4, DIO → GPIO5
"""
from machine import Pin
from time import sleep_us, sleep_ms
class TM1637:
_SEG_CODE = [
0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D,
0x07, 0x7F, 0x6F, 0x77, 0x7C, 0x39, 0x5E,
0x79, 0x71, 0x40, 0x00
]
def __init__(self, clk_pin, dio_pin, brightness=7):
self.clk = Pin(clk_pin, Pin.OUT)
self.dio = Pin(dio_pin, Pin.OUT)
self.brightness = min(brightness, 7)
self.clk.value(0)
self.dio.value(1)
def _start(self):
self.dio.value(1)
self.clk.value(1)
sleep_us(2)
self.dio.value(0)
sleep_us(2)
self.clk.value(0)
def _stop(self):
self.clk.value(0)
self.dio.value(0)
sleep_us(2)
self.clk.value(1)
sleep_us(2)
self.dio.value(1)
sleep_us(2)
def _write_byte(self, data):
for i in range(8):
self.clk.value(0)
self.dio.value(1 if (data & 0x01) else 0)
sleep_us(1)
self.clk.value(1)
sleep_us(1)
data >>= 1
# ACK
self.clk.value(0)
self.dio.value(1)
self.dio = Pin(self.dio, Pin.IN) # 切换为输入
sleep_us(1)
self.clk.value(1)
ack = self.dio.value()
sleep_us(1)
self.clk.value(0)
self.dio = Pin(self.dio, Pin.OUT) # 切换回输出
return ack
def number(self, num):
"""显示数字(0~9999),不显示前导零"""
num = max(0, min(9999, num))
digits = []
for i in range(3, -1, -1):
if num > 0 or i == 3:
digits.append(self._SEG_CODE[num % 10])
num //= 10
else:
digits.append(0x00) # 灭
digits.reverse()
self._show(digits)
def time(self, hour, minute, colon=True):
"""显示时间 HH:MM"""
digits = [
self._SEG_CODE[hour // 10],
self._SEG_CODE[hour % 10],
self._SEG_CODE[minute // 10],
self._SEG_CODE[minute % 10],
]
if colon:
digits[1] |= 0x80 # 冒号
self._show(digits)
def _show(self, data):
self._start()
self._write_byte(0x40) # 数据命令
self._stop()
self._start()
self._write_byte(0xC0) # 地址 0
for d in data:
self._write_byte(d)
self._stop()
self._start()
self._write_byte(0x80 | 0x08 | self.brightness) # 显示开
self._stop()
def clear(self):
self._show([0x00, 0x00, 0x00, 0x00])
# ===== 使用示例 =====
if __name__ == '__main__':
tm = TM1637(clk_pin=4, dio_pin=5, brightness=5)
# 显示数字
tm.number(188)
sleep_ms(2000)
# 显示时间
tm.time(12, 59)
sleep_ms(2000)
tm.clear()
7. 进阶技巧
7.1 亮度调节
TM1637 方案: 发送 0x88 | (brightness & 0x07) 即可,brightness 范围 0~7。
动态扫描方案: 通过调整每位导通时间的占空比来实现 PWM 调光:
// 在定时器中断中
const uint8_t brightness = 128; // 0~255 亮度值
static uint8_t pwmCounter = 0;
void scanDisplay_PWM() {
pwmCounter++;
for (uint8_t i = 0; i < 4; i++) {
digitalWrite(digPins[i], HIGH); // 关所有位
}
// 输出段码
outputSegCode(displayBuffer[currentDigit]);
// PWM 判断:亮度值决定导通时间
if (pwmCounter < brightness) {
digitalWrite(digPins[currentDigit], LOW); // 当前位置亮
}
// 否则该位跳过(更暗)
currentDigit = (currentDigit + 1) % 4;
if (currentDigit == 0) pwmCounter = 0; // 一轮扫描完成后重置
}
7.2 消影处理(Ghost Elimination)
在动态扫描中,若在切换位选之前没有先关闭全部位选,会出现"上一数字残影闪现在下一位置"的现象(俗称"鬼影")。
解决: 每次切换位选时,先执行以下顺序:
1. 关闭所有位选 ← 关键!先关再改数据
2. 更新段选数据
3. 打开目标位选
// 正确做法
ALL_DIGITS_OFF(); // 先全关
SET_SEGMENTS(nextCode); // 再改段码
DIGIT_ON(nextDigit); // 最后开目标位
7.3 消除闪烁
扫描频率不足会导致可见的闪烁。保证:
- 每位刷新率 ≥ 50Hz,最好 ≥ 100Hz
- 4 位时,扫描中断频率 =
50Hz × 4 = 200Hz,推荐 250Hz ~ 500Hz - 使用硬件定时器驱动扫描,不要在
loop()中用delay()控制时序
7.4 自定义字符与动画
// 在 TM1637 上定义自定义段码
// 例如:画一个" °C "符号
const uint8_t DEGREE_C = SEG_A | SEG_F | SEG_E | SEG_D | SEG_G;
// 跑马灯效果(字符从右向左滚动)
void scrollText(const char* text, uint8_t len, uint16_t speed_ms) {
uint8_t buf[4];
for (int16_t pos = len + 4; pos >= -4; pos--) {
for (uint8_t i = 0; i < 4; i++) {
int16_t idx = pos - i;
if (idx >= 0 && idx < len) {
buf[i] = charToSeg(text[idx]);
} else {
buf[i] = 0x00;
}
}
display.setSegments(buf);
delay(speed_ms);
}
}
7.5 降低功耗
- 不显示时调用
TM1637_CMD_DISP_OFF(0x80)关断显示,可节省 90% 功耗 - 需要时调低亮度等级
- 睡眠期间将 TM1637 的 CLK 和 DIO 设为高阻输入态(减少漏电流)
7.6 多模块级联
MAX7219 级联: 将第一个模块的 DOUT 接到第二个模块的 DIN:
MCU → DIN₁ → MAX7219₁ → DOUT₁ → DIN₂ → MAX7219₂ → ...
CS 和 CLK 并联
TM1637 方案: TM1637 不支持级联。需要多个模块时,每个模块占用独立的 DIO + CLK 引脚对。
8. 常见问题与排错
Q1:显示全灭,什么都不亮
| 排查步骤 | 说明 |
|---|---|
| 检查供电 | VCC 和 GND 是否正确连接?电压是否达标? |
| 检查接线 | DIO 和 CLK 是否接反? |
| 检查上拉 | TM1637 的 DIO/CLK 是否需要外接上拉电阻? |
| 检查代码 | 是否调用了 setBrightness() 和显示函数? |
| 检查显示开关 | TM1637 发送了 0x8F(显示开)命令? |
| 万用表检测 | 直接给段引脚供电(串联 220Ω),看是否亮 → 不亮则数码管损坏 |
Q2:显示乱码或数字不对
- 段码表错误: 你用的共阴段码表还是共阳段码表?共阳极需要取反
- 段选与位选接反: 将段选线接到了位选线
- 数据位序错误: TM1637 先发高位还是低位?不同库可能不同
- 引脚映射错误: 检查代码中引脚定义是否与硬件一致
Q3:显示闪烁
- 动态扫描频率太低(< 50Hz 每位)
- 主循环中有阻塞延时(如
delay(1000)),影响了定时器中断 - 中断优先级被更高优先级中断抢占
Q4:亮度不均匀
- 不同位的限流电阻阻值不一致
- 位选三极管驱动能力不足(基极电阻太大 → 三极管未饱和)
- 个别 LED 段老化
Q5:个别段不亮
- 对应段引脚虚焊
- 对应限流电阻损坏或漏焊
- 对应 MCU IO 口损坏
- 数码管内部该段 LED 损坏(万用表二极管档验证)
Q6:TM1637 通信失败(ACK 不返回 0)
- DIO 引脚未配置为开漏模式(STM32 特别注意!)
- 上拉电阻缺失(尝试外接 4.7KΩ 到 VCC)
- 时序太快(尝试增加
delay_us) - 模块或芯片损坏
9. 项目实战:简易数字时钟
9.1 项目概述
基于 Arduino + TM1637 + DS3231 实时时钟模块,制作一个带闹钟功能的数字时钟。
9.2 所需材料
| 材料 | 数量 | 说明 |
|---|---|---|
| Arduino Nano / UNO | 1 | 主控 |
| TM1637 4位数码管模块 | 1 | 显示 |
| DS3231 RTC 模块 | 1 | 高精度实时时钟 |
| 按键(微动开关) | 3 | 模式 / 加 / 减 |
| 有源蜂鸣器模块 | 1 | 闹钟提醒 |
| 面包板 + 杜邦线 | 若干 |
9.3 电路接线
DS3231 (I2C) Arduino
VCC → 5V
GND → GND
SDA → A4 (UNO) / A4 (Nano)
SCL → A5 (UNO) / A5 (Nano)
TM1637 Arduino
VCC → 5V
GND → GND
DIO → D2
CLK → D3
按键1(模式) Arduino
一端 → D4
一端 → GND(启用内部上拉)
按键2(加) D5 → GND
按键3(减) D6 → GND
蜂鸣器 Arduino
VCC → 5V
I/O → D7
GND → GND
9.4 完整代码
/**
* 基于 TM1637 + DS3231 的数字时钟
* 功能:时间显示、闹钟设置、蜂鸣提醒
*/
#include <TM1637Display.h>
#include <Wire.h>
#include <RTClib.h>
// ---- TM1637 ----
#define CLK 3
#define DIO 2
TM1637Display display(CLK, DIO);
// ---- DS3231 ----
RTC_DS3231 rtc;
// ---- 按键 ----
#define BTN_MODE 4
#define BTN_PLUS 5
#define BTN_MINUS 6
#define BUZZER 7
// ---- 状态 ----
enum Mode {
MODE_CLOCK, // 正常时钟显示
MODE_ALARM_HOUR, // 设置闹钟小时
MODE_ALARM_MIN // 设置闹钟分钟
};
Mode currentMode = MODE_CLOCK;
uint8_t alarmHour = 7;
uint8_t alarmMin = 30;
bool alarmEnabled = true;
bool alarmTriggered = false;
unsigned long lastBtnCheck = 0;
unsigned long lastColonToggle = 0;
unsigned long alarmStartTime = 0;
bool colonState = true;
void setup() {
Serial.begin(9600);
// TM1637 初始化
display.setBrightness(5);
display.clear();
// DS3231 初始化
if (!rtc.begin()) {
Serial.println("DS3231 未找到!");
display.showNumberDec(0); // 显示 0000 表示错误
while (1);
}
// rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); // 首次设置时间
// 按键(内部上拉)
pinMode(BTN_MODE, INPUT_PULLUP);
pinMode(BTN_PLUS, INPUT_PULLUP);
pinMode(BTN_MINUS, INPUT_PULLUP);
// 蜂鸣器
pinMode(BUZZER, OUTPUT);
digitalWrite(BUZZER, LOW);
Serial.println("时钟初始化完成");
}
void loop() {
DateTime now = rtc.now();
// ---- 按键处理 ----
if (millis() - lastBtnCheck > 50) { // 50ms 去抖
lastBtnCheck = millis();
handleButtons();
}
// ---- 显示处理 ----
if (millis() - lastColonToggle > 500) {
lastColonToggle = millis();
colonState = !colonState;
}
switch (currentMode) {
case MODE_CLOCK:
displayTime(now.hour(), now.minute(), colonState);
checkAlarm(now);
break;
case MODE_ALARM_HOUR:
display.showNumberDec(alarmHour * 100 + alarmMin, true);
delay(200);
display.clear();
delay(100);
break;
case MODE_ALARM_MIN:
display.showNumberDec(alarmHour * 100 + alarmMin, true);
break;
}
// ---- 闹钟触发 ----
if (alarmTriggered) {
digitalWrite(BUZZER, (millis() / 200) % 2); // 蜂鸣器间歇响
if (millis() - alarmStartTime > 30000) { // 30 秒后自动停
alarmTriggered = false;
digitalWrite(BUZZER, LOW);
}
}
}
void displayTime(uint8_t hour, uint8_t min, bool colon) {
uint8_t data[4];
data[0] = display.encodeDigit(hour / 10);
data[1] = display.encodeDigit(hour % 10);
data[2] = display.encodeDigit(min / 10);
data[3] = display.encodeDigit(min % 10);
if (colon) {
data[1] |= 0x80; // 点亮冒号
}
display.setSegments(data);
}
void handleButtons() {
static bool lastMode = HIGH;
static bool lastPlus = HIGH;
static bool lastMinus = HIGH;
bool btnMode = digitalRead(BTN_MODE);
bool btnPlus = digitalRead(BTN_PLUS);
bool btnMinus = digitalRead(BTN_MINUS);
// 模式键:切换模式
if (lastMode == HIGH && btnMode == LOW) { // 按下
if (alarmTriggered) {
// 闹钟响时按模式键 = 关闭闹钟
alarmTriggered = false;
digitalWrite(BUZZER, LOW);
} else {
currentMode = (Mode)((currentMode + 1) % 3);
}
}
// 加键
if (lastPlus == HIGH && btnPlus == LOW) {
switch (currentMode) {
case MODE_ALARM_HOUR:
alarmHour = (alarmHour + 1) % 24;
break;
case MODE_ALARM_MIN:
alarmMin = (alarmMin + 1) % 60;
break;
default: break;
}
}
// 减键
if (lastMinus == HIGH && btnMinus == LOW) {
switch (currentMode) {
case MODE_ALARM_HOUR:
alarmHour = (alarmHour == 0) ? 23 : alarmHour - 1;
break;
case MODE_ALARM_MIN:
alarmMin = (alarmMin == 0) ? 59 : alarmMin - 1;
break;
default: break;
}
}
lastMode = btnMode;
lastPlus = btnPlus;
lastMinus = btnMinus;
}
void checkAlarm(const DateTime& now) {
if (!alarmEnabled || alarmTriggered) return;
if (now.hour() == alarmHour && now.minute() == alarmMin && now.second() == 0) {
alarmTriggered = true;
alarmStartTime = millis();
}
}
9.5 运行效果
- 正常模式: 交替闪烁的冒号显示当前时间(如
12:59) - 按模式键 1 次: 进入闹钟小时设置(数字闪烁)
- 按模式键 2 次: 进入闹钟分钟设置
- 按模式键 3 次: 回到正常时钟
- 闹钟触发时: 蜂鸣器间歇响声,按任意键关闭
10. 总结与资源推荐
📝 核心要点回顾
- 188数码管 = 4 位 0.56 英寸 7 段 LED 数码管的通俗叫法
- 动态扫描 = 利用人眼视觉暂留,依次快速点亮每位,节省 IO 口
- 共阴 vs 共阳:段码表互为取反,三极管极性相反
- TM1637 是入门最佳选择:2 根线、库丰富、淘宝几元包邮
- 消影和防闪烁是关键——先关位选,再改段码,最后开目标位
- 限流电阻不能省,动态扫描时可适当减小阻值补偿亮度
📚 推荐资源
| 资源 | 链接/搜索关键词 |
|---|---|
| TM1637 数据手册 | TM1637 datasheet PDF |
| Arduino TM1637Display 库 | GitHub: avishorp/TM1637 |
| DS3231 数据手册 | DS3231 datasheet |
| 74HC595 教程 | Arduino 官方 ShiftOut 教程 |
| 段码字模工具 | 搜索引擎: “7 segment font generator” |
🛒 硬件采购建议
- 新手套装: Arduino Nano + TM1637 数码管模块 + DS3231 RTC + 面包板套件(约 30~50 元)
- 进阶升级: 加入 DHT22 温湿度传感器、光敏电阻(自动调光)、ESP8266(WiFi 校时)
写在最后: 188 数码管虽然是一个"古老"的显示器件,但它价格低廉、驱动简单、显示清晰,至今仍在大量电子产品中广泛应用。掌握了它,你不仅学会了一种显示方案,更深刻理解了动态扫描、段码编码、定时中断等嵌入式开发的核心概念。从点亮第一个数字开始,享受硬件编程的乐趣吧!🚀
本文由作者原创编写,发布于 CSDN。转载请注明出处。如有疑问欢迎留言交流。
更多推荐


所有评论(0)