188数码管从入门到精通

适用对象: 嵌入式开发初学者、电子爱好者、Arduino/STM32开发者
涵盖平台: Arduino · STM32 · ESP32 · 51单片机
驱动方案: TM1637 · 74HC595 · MAX7219 · 三极管动态扫描


目录

  1. 什么是188数码管
  2. 工作原理
  3. 常见型号与脚位定义
  4. 驱动方式详解
  5. 硬件接线指南
  6. 编程实战
  7. 进阶技巧
  8. 常见问题与排错
  9. 项目实战:简易数字时钟
  10. 总结与资源推荐

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 接线注意事项

  1. 限流电阻不可省! 否则烧 LED 或 MCU IO 口
  2. TM1637 的 DIO 和 CLK 线上拉 4.7KΩ 到 VCC(部分模块已内置上拉)
  3. 3.3V 系统驱动 5V 模块时:确认模块是否兼容 3.3V 逻辑电平;否则需加电平转换
  4. 电源去耦:VCC 与 GND 间加 100nF + 10μF 电容
  5. 共阴/共阳确认:用万用表二极管档(红笔接 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. 总结与资源推荐

📝 核心要点回顾

  1. 188数码管 = 4 位 0.56 英寸 7 段 LED 数码管的通俗叫法
  2. 动态扫描 = 利用人眼视觉暂留,依次快速点亮每位,节省 IO 口
  3. 共阴 vs 共阳:段码表互为取反,三极管极性相反
  4. TM1637 是入门最佳选择:2 根线、库丰富、淘宝几元包邮
  5. 消影和防闪烁是关键——先关位选,再改段码,最后开目标位
  6. 限流电阻不能省,动态扫描时可适当减小阻值补偿亮度

📚 推荐资源

资源 链接/搜索关键词
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。转载请注明出处。如有疑问欢迎留言交流。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐