一、引言

   相信新手在接触到188型数码管的时候都是一脸懵。笔者今天也是在实际工程中遇见了这个型号的数码管。这次是扫描型数码管。6个引脚的
在这里插入图片描述
   在开始之前要搞明白他的显示原理

二、数码管显示原理

   首先由电路图可知当上面这一行为 高电平(也就是对应端口写“1”) 时数码管导通,下面这一行为低电平是(也就是对应端口写“0”时则熄灭)
   反之,高电平(也就是对应端口写“0”)时数码管截至,下面这一行为低电平是(也就是对应端口写“0”时则熄灭)
   也就是表格里写的这样
在这里插入图片描述
在这里插入图片描述

0、段码的原理

   数码管的每个数字由7个LED段组成,排列如下:
在这里插入图片描述

   每个段对应一个二进制位,约定好顺序后,用1表示点亮、0表示熄灭,就得到了一个8位的二进制数。

1、段与位的含意

(1) 位(Digit)-- (个、十、百、千、万… …)

  • 就是整个数字的位置
  • 您的 188 数码管有 3 个"位":百位、十位、个位。
  • 就像电梯面板上有多个数字显示器,每个显示器叫一个"位"。

(2) 段(Segment)-- (小灯)

  • 就是组成一个数字的每一根笔画
  • 7 段数码管的每个数字由 7 根 LED 灯条组成:A、B、C、D、E、F、G
  • 就像写数字 “8” 时用的每一笔。
    (下图为16段 数码管)
    在这里插入图片描述

2、矩阵扫描的关系

概念 英文 硬件对应 代码中的变量
Digit 高电平引脚(PIN1~PIN5) ScanDigit (0~2)
Segment 低电平引脚(PIN1~PIN6) ScanSeg (0~6)

要点亮个位的 A 段(对照下图 – A3),需要同时:
在这里插入图片描述
在这里插入图片描述

  • 选中的 :PIN3 输出高电平(DIG3_ON()
  • 选中的 :PIN6 输出低电平(DIG6_OFF()

3、约定顺序

   通常的顺序是:DP G F E D C B A (DP是小数点,我们不用,固定为0)
   也就是:

7 6 5 4 3 2 1 0
DP G F E D C B A

4、人眼的视觉暂留

   视觉成像依赖眼睛晶状体将光线聚焦于视网膜,感光细胞接收光信号并转化为神经冲动,经视神经传入大脑产生视觉。感光色素的再生需一定时间,导致光刺激消失后视觉印象仍持续0.1至0.4秒,形成视觉暂留。此过程体现人眼的视觉惰性,属于正常的生理特性。
   实验验证,使用Flash CS6软件进行视觉暂留测试:将帧频设为10帧/秒(每帧0.1秒)。第一帧插入图像,第二帧设为空白关键帧。第六帧插入另一图像,第七帧为空白关键帧并延续至第十帧,循环播放。初始状态下两张图像无法同时显现。逐步删除每图后一帧后,可观察到两图像部分重叠现象,表明视觉印象持续存在。当两图间隔为4帧(0.4秒)时仍可重叠感知,验证视觉暂留时间约为0.4秒。

三、手动推导每个数字与二进制的编码关系

注:DP为小数点

0、数字 0

需要点亮的段:A、B、C、D、E、F(不亮G)

DP G F E D C B A
0 0 1 1 1 1 1 1

二进制:0011 1111 = 十六进制 0x3F

1、数字 1

需要点亮的段:B、C

DP G F E D C B A
0 0 0 0 0 1 1 0

二进制:0000 0110 = 十六进制 0x06

2、数字 2

需要点亮的段:A、B、G、E、D

DP G F E D C B A
0 1 0 1 1 0 1 1

二进制:0101 1011 = 十六进制 0x5B

3、数字 3

需要点亮的段:A、B、G、C、D

DP G F E D C B A
0 1 0 0 1 1 1 1

二进制:0100 1111 = 十六进制 0x4F

4、数字 4

需要点亮的段:F、G、B、C

DP G F E D C B A
0 1 1 0 0 1 1 0

二进制:0110 0110 = 十六进制 0x66

5、数字 5

需要点亮的段:A、F、G、C、D

DP G F E D C B A
0 1 1 0 1 1 0 1

二进制:0110 1101 = 十六进制 0x6D

6、数字 6

需要点亮的段:A、F、G、E、C、D

DP G F E D C B A
0 1 1 1 1 1 0 1

二进制:0111 1101 = 十六进制 0x7D

7、数字 7

需要点亮的段:A、B、C

DP G F E D C B A
0 0 0 0 0 1 1 1

二进制:0000 0111 = 十六进制 0x07

8、数字 8

需要点亮的段:A、B、C、D、E、F、G(全部)

DP G F E D C B A
0 1 1 1 1 1 1 1

二进制:0111 1111 = 十六进制 0x7F

9 、数字 9

需要点亮的段:A、B、C、D、F、G(不亮E)

DP G F E D C B A
0 1 1 0 1 1 1 1

二进制:0110 1111 = 十六进制 0x6F

10、完整对照表

数字 点亮段 二进制(DP-G-F-E-D-C-B-A) 十六进制
0 ABCDEF 0011 1111 0x3F
1 BC 0000 0110 0x06
2 ABDEG 0101 1011 0x5B
3 ABCDG 0100 1111 0x4F
4 BCFG 0110 0110 0x66
5 ACDFG 0110 1101 0x6D
6 ACDEFG 0111 1101 0x7D
7 ABC 0000 0111 0x07
8 ABCDEFG 0111 1111 0x7F
9 ABCDFG 0110 1111 0x6F

四、数码管C语言驱动代码

   根据您提供的 Tube188.h 头文件,我编写了与之匹配的 Tube188.c 源文件。利用 GPIO 宏操作引脚,实现了 188 数码管的动态扫描驱动,支持在指定位显示数字,并包含内部扫描函数供定时器中断调用。

1、 Tube188.h(精简完整版)

/**
 * @file Tube188.h
 * @author Mwang (1324xxx@qq.com)
 * @brief 188扫描型数码管驱动头文件(完整版)
 * @version 1.0
 * @date 2026-04-22
 *
 * @copyright Copyright (c) 2026
 *
 * @par 修改日志:
 * Date           Version    Author      Description
 * 2026-04-20        v1.0       Mwang       初次创建
 * 2026-04-22        v1.1       Mwang       精简结构,采用直接引脚操作
 */
#ifndef __TUBE188_H__
#define __TUBE188_H__

/*==============================================================
 * 头文件包含
 *==============================================================*/
#include "jsc8p012.h"   // MCU寄存器定义
#include "gpio.h"       // GPIO操作宏
#include <stdint.h>     // uint8_t 类型定义

/*==============================================================
 * 引脚映射:数码管引脚 → MCU引脚
 * 
 * 数码管有6个引脚(PIN1~PIN6),分别连接到MCU的6个GPIO
 * 高电平扫描:PIN1~PIN5 输出高电平,用于选择"位"(百位/十位/个位)
 * 低电平扫描:PIN1~PIN6 输出低电平,用于选择"段"(A~G)
 *==============================================================*/
/* P50 → 数码管 PIN1 */ 
#define DIG1_PORT  GPIO5
#define DIG1_PIN   PIN0

/* P51 → 数码管 PIN2 */ 
#define DIG2_PORT  GPIO5
#define DIG2_PIN   PIN1

/* P52 → 数码管 PIN3 */ 
#define DIG3_PORT  GPIO5
#define DIG3_PIN   PIN2

/* P65 → 数码管 PIN4 */ 
#define DIG4_PORT  GPIO6
#define DIG4_PIN   PIN5

/* P64 → 数码管 PIN5 */ 
#define DIG5_PORT  GPIO6
#define DIG5_PIN   PIN4

/* P61 → 数码管 PIN6 */ 
#define DIG6_PORT  GPIO6
#define DIG6_PIN   PIN1

/*==============================================================
 * 引脚操作宏
 *==============================================================*/

// ---------- 初始化引脚为输出模式 ----------
// 调用 gpio.h 中的 PIN_MODE_OUT 宏,将引脚配置为输出
#define DIG1_INIT()  PIN_MODE_OUT(DIG1_PORT, DIG1_PIN)   // P50 → 输出
#define DIG2_INIT()  PIN_MODE_OUT(DIG2_PORT, DIG2_PIN)   // P51 → 输出
#define DIG3_INIT()  PIN_MODE_OUT(DIG3_PORT, DIG3_PIN)   // P52 → 输出
#define DIG4_INIT()  PIN_MODE_OUT(DIG4_PORT, DIG4_PIN)   // P65 → 输出
#define DIG5_INIT()  PIN_MODE_OUT(DIG5_PORT, DIG5_PIN)   // P64 → 输出
#define DIG6_INIT()  PIN_MODE_OUT(DIG6_PORT, DIG6_PIN)   // P61 → 输出

// ---------- 输出高电平(用于选中"位") ----------
// PIN(port, pin) 是 gpio.h 中定义的宏,返回对应引脚的值
#define DIG1_ON()    (PIN(DIG1_PORT, DIG1_PIN) = 1)      // P50 = 1
#define DIG2_ON()    (PIN(DIG2_PORT, DIG2_PIN) = 1)      // P51 = 1
#define DIG3_ON()    (PIN(DIG3_PORT, DIG3_PIN) = 1)      // P52 = 1
#define DIG4_ON()    (PIN(DIG4_PORT, DIG4_PIN) = 1)      // P65 = 1
#define DIG5_ON()    (PIN(DIG5_PORT, DIG5_PIN) = 1)      // P64 = 1
#define DIG6_ON()    (PIN(DIG6_PORT, DIG6_PIN) = 1)      // P61 = 1

// ---------- 输出低电平(用于选中"段"或关闭引脚) ----------
#define DIG1_OFF()   (PIN(DIG1_PORT, DIG1_PIN) = 0)      // P50 = 0
#define DIG2_OFF()   (PIN(DIG2_PORT, DIG2_PIN) = 0)      // P51 = 0
#define DIG3_OFF()   (PIN(DIG3_PORT, DIG3_PIN) = 0)      // P52 = 0
#define DIG4_OFF()   (PIN(DIG4_PORT, DIG4_PIN) = 0)      // P65 = 0
#define DIG5_OFF()   (PIN(DIG5_PORT, DIG5_PIN) = 0)      // P64 = 0
#define DIG6_OFF()   (PIN(DIG6_PORT, DIG6_PIN) = 0)      // P61 = 0

/*==============================================================
 * 段码表:每个数字对应的7段码
 * 
 * 位顺序约定:DP G F E D C B A
 *             7  6 5 4 3 2 1 0
 * 
 * DP(小数点)固定为0,1表示点亮,0表示熄灭
 * 例如数字0:A,B,C,D,E,F亮 → 0011 1111 → 0x3F
 *==============================================================*/
static const uint8_t SegCode[10] = 
{
    0x3F, // 0: 二进制 0011 1111 → A,B,C,D,E,F 亮,G 灭
    0x06, // 1: 二进制 0000 0110 → B,C 亮,其余灭
    0x5B, // 2: 二进制 0101 1011 → A,B,D,E,G 亮,C,F 灭
    0x4F, // 3: 二进制 0100 1111 → A,B,C,D,G 亮,E,F 灭
    0x66, // 4: 二进制 0110 0110 → B,C,F,G 亮,A,D,E 灭
    0x6D, // 5: 二进制 0110 1101 → A,C,D,F,G 亮,B,E 灭
    0x7D, // 6: 二进制 0111 1101 → A,C,D,E,F,G 亮,B 灭
    0x07, // 7: 二进制 0000 0111 → A,B,C 亮,其余灭
    0x7F, // 8: 二进制 0111 1111 → A,B,C,D,E,F,G 全亮
    0x6F  // 9: 二进制 0110 1111 → A,B,C,D,F,G 亮,E 灭
};

/*==============================================================
 * 显示缓冲区
 * 
 * 存储三个位(百位、十位、个位)各自要显示的数字
 * 取值范围:0~9 显示对应数字,0xFF 表示该位熄灭
 * volatile 关键字:变量可能被中断修改,每次读取必须从内存加载
 *==============================================================*/
static volatile uint8_t Disp_H = 0xFF;  // 百位要显示的数字(初始熄灭)
static volatile uint8_t Disp_T = 0xFF;  // 十位要显示的数字(初始熄灭)
static volatile uint8_t Disp_U = 0xFF;  // 个位要显示的数字(初始熄灭)

/*==============================================================
 * 扫描状态
 * 
 * 记录当前扫描进行到哪一位、哪一段
 * ScanDigit:0=百位, 1=十位, 2=个位
 * ScanSeg:  0=A段, 1=B段, 2=C段, 3=D段, 4=E段, 5=F段, 6=G段
 *==============================================================*/
static volatile uint8_t ScanDigit = 0;   // 当前扫描位:0=百位, 1=十位, 2=个位
static volatile uint8_t ScanSeg   = 0;   // 当前扫描段:0=A,1=B,2=C,3=D,4=E,5=F,6=G

/*==============================================================
 * 公共函数声明
 *==============================================================*/

/**
 * @brief 初始化188数码管GPIO
 * 
 * 将6个数码管控制引脚配置为输出模式,并初始化为熄灭状态
 * 系统上电时调用一次
 */
void Tube188_Init(void);

/**
 * @brief 显示电量百分比 (0~100)
 * @param percent 电量值(0~100),超过100自动截断为100
 * 
 * 将百分数拆分后存入显示缓冲区(Disp_H, Disp_T, Disp_U)
 * 实际的硬件点亮由 Tube188_Scan() 完成
 */
void Tube188_DisplayPercent(uint8_t percent);

/**
 * @brief 动态扫描函数
 * 
 * 每次调用点亮当前 ScanDigit 位、ScanSeg 段对应的那一个 LED
 * 必须在定时器中断中周期性调用(建议每1~2ms调用一次)
 * 
 * 状态机会自动推进:每次调用后 ScanSeg++,扫完7段后 ScanDigit++
 * 完成 3位×7段 = 21 次调用为一个完整刷新周期
 */
void Tube188_Scan(void);

#endif /* __TUBE188_H__ */

2、 Tube188.c(极简实现)

/**
 * @file Tube188.c
 * @brief 188数码管驱动实现
 */
#include "Tube188.h"

/**
 * @brief 初始化GPIO
 * @note  系统上电时调用一次,配置所有数码管相关引脚为输出模式
 */
void Tube188_Init(void) 
{
    // 调用头文件中定义的引脚初始化宏,将6个MCU引脚配置为输出模式
    // 这些宏展开后类似:P5CR &= ~BIT0 (将P50设为输出)
    DIG1_INIT();    // P50 → 输出模式,对应数码管 PIN1(高电平扫描脚)
    DIG2_INIT();    // P51 → 输出模式,对应数码管 PIN2(高电平扫描脚)
    DIG3_INIT();    // P52 → 输出模式,对应数码管 PIN3(高电平扫描脚)
    DIG4_INIT();    // P65 → 输出模式,对应数码管 PIN4(高电平扫描脚)
    DIG5_INIT();    // P64 → 输出模式,对应数码管 PIN5(高电平扫描脚)
    DIG6_INIT();    // P61 → 输出模式,对应数码管 PIN6(低电平扫描脚)
    
    // 关闭所有引脚输出,防止初始化过程中产生鬼影
    // 内部执行:DIG1_OFF(); DIG2_OFF(); ... DIG6_OFF();
    ClearAllPins();
    
    // 将显示缓冲区全部设置为"熄灭"状态(0xFF = 255,不是0~9的合法数字)
    // 这样数码管初始状态是全部熄灭的,不会乱显示
    Disp_H = Disp_T = Disp_U = 0xFF;  // 初始熄灭
}
/**
 * @brief 关闭GPIO
 * @note  系统上电时调用一次,关闭引脚
 */
void ClearAllPins(void) 
{
    DIG1_OFF(); DIG2_OFF(); DIG3_OFF();
    DIG4_OFF(); DIG5_OFF(); DIG6_OFF();
}
/**
 * @brief 显示电量百分比 (0~100)
 * @param percent 电量值(0~100),超过100自动截断为100
 * @note  此函数只更新软件缓冲区,不直接操作硬件
 */
void Tube188_DisplayPercent(uint8_t percent) 
{
    // 参数合法性检查:如果传入的值大于100,强制设为100
    // 因为数码管只有3位,最大只能显示100
    if (percent > 100) percent = 100;

    // 特殊情况:电量正好是100%
    if (percent == 100) 
    {
        // 显示 "100" 三个数字
        Disp_H = 1;     // 百位显示数字 1
        Disp_T = 0;     // 十位显示数字 0
        Disp_U = 0;     // 个位显示数字 0
    } 
    else 
    {
        // 通用情况:0~99 的电量值
        // 通过整数除法和取模运算,将百分数拆分为百位、十位、个位
        uint8_t h = percent / 100;          // 百位数字(0~9),例如 85 → h=0
        uint8_t t = (percent % 100) / 10;   // 十位数字(0~9),例如 85 → t=8
        uint8_t u = percent % 10;           // 个位数字(0~9),例如 85 → u=5

        // 百位处理:如果百位是0,则熄灭该位(高位零不显示)
        // 例如 85 的百位是0,我们不想显示 "085",所以给 0xFF(熄灭)
        Disp_H = (h == 0) ? 0xFF : h;
        
        // 十位处理:当数值是0~9的个位数时,十位也熄灭
        // 条件:percent < 10(个位数)且百位为0(本来就是两位数以下)
        // 例如 5 的十位是0,我们不想显示 "05",所以十位熄灭
        // 但 15 的十位是1,正常显示为 "15"
        Disp_T = (percent < 10 && h == 0) ? 0xFF : t;
        
        // 个位始终显示,不需要特殊处理
        Disp_U = u;
    }
}

/**
 * @brief 动态扫描函数(需在定时中断中周期性调用,建议每1~2ms调用一次)
 * @note  此函数每次只点亮一个段,利用人眼视觉暂留实现稳定显示
 */
void Tube188_Scan(void) 
{
    // ========== 第1步:读取当前扫描位置 ==========
    // 从静态变量中取出当前要扫描的"位"和"段"
    // ScanDigit: 0=百位, 1=十位, 2=个位
    // ScanSeg:   0=A段, 1=B段, 2=C段, 3=D段, 4=E段, 5=F段, 6=G段
    uint8_t digit = ScanDigit;   // 当前处理第几个数码管(0/1/2)
    uint8_t seg   = ScanSeg;     // 当前处理哪一个段(0~6)
    uint8_t num;                 // 将要存储当前位应该显示的数字(0~9或0xFF)

    // ========== 第2步:从缓冲区获取当前位要显示的数字 ==========
    // 根据 digit 的值,从对应的缓冲区变量中取出数字
    if (digit == 0)           // 当前扫描的是百位
        num = Disp_H;         // 从百位缓冲区取数字
    else if (digit == 1)      // 当前扫描的是十位
        num = Disp_T;         // 从十位缓冲区取数字
    else                      // 当前扫描的是个位(digit == 2)
        num = Disp_U;         // 从个位缓冲区取数字

    // ========== 第3步:关闭所有引脚(消除残影) ==========
    // 在点亮新段之前,必须先关闭上一段的所有引脚
    // 否则两个段会同时亮,产生"鬼影"
    ClearAllPins();  // 内部执行 DIG1_OFF() ~ DIG6_OFF()

    // ========== 第4步:判断当前段是否需要点亮 ==========
    // 如果该位不是熄灭状态(num != 0xFF),说明有数字需要显示
    if (num != 0xFF) 
    {
        // 从段码表中取出数字 num 对应的7段码
        // 例如 num=8 时,SegCode[8] = 0x7F(二进制 0111 1111,A~G全亮)
        uint8_t mask = SegCode[num];
        
        // 检查第 seg 位是否需要点亮
        // mask & (1 << seg) 是按位与运算,用于测试 mask 的第 seg 位是0还是1
        // 例如:mask=0x5B, seg=0 → (0x5B & 0x01) = 1,A段需要点亮
        if (mask & (1 << seg)) 
        {
            // ========== 第5步:根据位和段,驱动对应的硬件引脚 ==========
            // 进入 switch-case 结构,根据当前是百位、十位还是个位来选择不同的引脚组合
            switch (digit) 
            {
                // -------------------- 百位 (digit = 0) --------------------
                case 0:
                    // 百位只有 B1、C1 有明确的硬件连接,A1 是推测的,D1~G1 未连接
                    if (seg == 0) 
                    { 
                        // A1段:PIN1输出高电平,PIN6输出低电平
                        DIG1_ON();   // PIN1 (P50) 输出高电平 → 选中百位
                        DIG6_OFF();  // PIN6 (P61) 输出低电平 → 选中A段
                    }      
                    else if (seg == 1) 
                    { 
                        // B1段:PIN2输出高电平,PIN1输出低电平
                        DIG2_ON();   // PIN2 (P51) 输出高电平 → 选中百位(通过B1连接)
                        DIG1_OFF();  // PIN1 (P50) 输出低电平 → 选中B段
                    } 
                    else if (seg == 2) 
                    { 
                        // C1段:PIN2输出高电平,PIN3输出低电平
                        DIG2_ON();   // PIN2 (P51) 输出高电平
                        DIG3_OFF();  // PIN3 (P52) 输出低电平 → 选中C段
                    }
                    // seg == 3~6 (D1~G1) 在硬件上未连接,所以不做任何操作
                    break;

                // -------------------- 十位 (digit = 1) --------------------
                case 1:
                    // 十位有完整的7段连接(A2~G2),全部都有定义
                    if (seg == 0)      
                    { 
                        // A2段:PIN2输出高电平,PIN4输出低电平
                        DIG2_ON(); 
                        DIG4_OFF(); 
                    }
                    else if (seg == 1) 
                    { 
                        // B2段:PIN2输出高电平,PIN5输出低电平
                        DIG2_ON(); 
                        DIG5_OFF(); 
                    }
                    else if (seg == 2) 
                    { 
                        // C2段:PIN2输出高电平,PIN6输出低电平
                        DIG2_ON(); 
                        DIG6_OFF(); 
                    }
                    else if (seg == 3) 
                    { 
                        // D2段:PIN3输出高电平,PIN1输出低电平
                        DIG3_ON(); 
                        DIG1_OFF(); 
                    }
                    else if (seg == 4) 
                    { 
                        // E2段:PIN3输出高电平,PIN2输出低电平
                        DIG3_ON(); 
                        DIG2_OFF(); 
                    }
                    else if (seg == 5) 
                    { 
                        // F2段:PIN3输出高电平,PIN4输出低电平
                        DIG3_ON(); 
                        DIG4_OFF(); 
                    }
                    else if (seg == 6) 
                    { 
                        // G2段:PIN3输出高电平,PIN5输出低电平
                        DIG3_ON(); 
                        DIG5_OFF(); 
                    }
                    break;

                // -------------------- 个位 (digit = 2) --------------------
                case 2:
                    // 个位也有完整的7段连接(A3~G3)
                    if (seg == 0)      
                    { 
                        // A3段:PIN3输出高电平,PIN6输出低电平
                        DIG3_ON(); 
                        DIG6_OFF(); 
                    }
                    else if (seg == 1) 
                    { 
                        // B3段:PIN4输出高电平,PIN1输出低电平
                        DIG4_ON(); 
                        DIG1_OFF(); 
                    }
                    else if (seg == 2) 
                    { 
                        // C3段:PIN4输出高电平,PIN2输出低电平
                        DIG4_ON(); 
                        DIG2_OFF(); 
                    }
                    else if (seg == 3) 
                    { 
                        // D3段:PIN4输出高电平,PIN3输出低电平
                        DIG4_ON(); 
                        DIG3_OFF(); 
                    }
                    else if (seg == 4) 
                    { 
                        // E3段:PIN4输出高电平,PIN5输出低电平
                        DIG4_ON(); 
                        DIG5_OFF(); 
                    }
                    else if (seg == 5) 
                    { 
                        // F3段:PIN4输出高电平,PIN6输出低电平
                        DIG4_ON(); 
                        DIG6_OFF(); 
                    }
                    else if (seg == 6) 
                    { 
                        // G3段:PIN5输出高电平,PIN1输出低电平
                        DIG5_ON(); 
                        DIG1_OFF(); 
                    }
                    break;
            }
        }
    }

    // ========== 第6步:状态机推进,准备下一次扫描 ==========
    // 当前段扫描完毕,移动到下一个段
    ScanSeg++;
    
    // 如果一个位的7个段都扫描完了(seg 从 0 到 6,然后变成7)
    if (ScanSeg >= 7) 
    {
        ScanSeg = 0;      // 段索引归零,从 A 段重新开始
        ScanDigit++;      // 切换到下一个位
        
        // 如果3个位都扫描完了(digit 从 0 到 2,然后变成3)
        if (ScanDigit >= 3) 
        {
            ScanDigit = 0;   // 位索引归零,从百位重新开始新一轮扫描
        }
    }
    
    // 函数返回后,刚才点亮的段会一直保持,直到下一次中断调用此函数
    // 下一次调用时,ClearAllPins() 会关闭这个段,然后点亮下一个段
}

3、作用原理解析

(1) Tube188_Scan() 逐行解析

a、读取当前扫描位置
 uint8_t digit = ScanDigit;  // 当前轮到哪一位(0:百, 1:十, 2:个)
 uint8_t seg   = ScanSeg;    // 当前轮到哪一个段(0:A, 1:B ... 6:G)
 uint8_t num;
b、从缓冲区取出这一位要显示的数字
// 步骤1:从缓冲区取出这一位要显示的数字
    if (digit == 0)      num = Disp_H;
    else if (digit == 1) num = Disp_T;
    else      

注一: 写入缓冲区Tube188_DisplayPercent是写入缓冲区 的函数

void Tube188_DisplayPercent(uint8_t percent) 
{
    // ... 计算 h, t, u ...
    
    Disp_H = (h == 0) ? 0xFF : h;   // ⬅ 写入缓冲区
    Disp_T = (percent < 10 && h == 0) ? 0xFF : t;  // ⬅ 写入缓冲区
    Disp_U = u;                      // ⬅ 写入缓冲区
}

注二: 这里是用void Tube188_Scan()取出缓冲区数据的函数

void Tube188_Scan(void) 
{

    // ⬅ 从缓冲区读取数字
    if (digit == 0)      
        num = Disp_H;    // 读取百位缓冲区
    else if (digit == 1) 
        num = Disp_T;    // 读取十位缓冲区
    else                 
        num = Disp_U;    // 读取个位缓冲区
    
    // 然后根据 num 的值去驱动硬件点亮对应的段
}
  • 一句话总结
  • Tube188_DisplayPercent:把数字写入缓冲区 (生产者)
  • Tube188_Scan:从缓冲区读取数字并驱动硬件 (消费者)
c、 关闭所有引脚,消除上一个段的残影
// 步骤2:关闭所有引脚,消除上一个段的残影
    ClearAllPins();
d、 判断当前段是否需要点亮
// ========== 第4步:判断当前段是否需要点亮 ==========
    // 如果该位不是熄灭状态(num != 0xFF),说明有数字需要显示
    if (num != 0xFF) 
    {
        // 从段码表中取出数字 num 对应的7段码
        // 例如 num=8 时,SegCode[8] = 0x7F(二进制 0111 1111,A~G全亮)
        uint8_t mask = SegCode[num];
        
        // 检查第 seg 位是否需要点亮
        // mask & (1 << seg) 是按位与运算,用于测试 mask 的第 seg 位是0还是1
        // 例如:mask=0x5B, seg=0 → (0x5B & 0x01) = 1,A段需要点亮
        if (mask & (1 << seg)) 
        {
            // ========== 第5步:根据位和段,驱动对应的硬件引脚 ==========
            // 进入 switch-case 结构,根据当前是百位、十位还是个位来选择不同的引脚组合
            switch (digit) 
            {
                // -------------------- 百位 (digit = 0) --------------------
                case 0:
                    // 百位只有 B1、C1 有明确的硬件连接,A1 是推测的,D1~G1 未连接
                    if (seg == 0) 
                    { 
                        // A1段:PIN1输出高电平,PIN6输出低电平
                        DIG1_ON();   // PIN1 (P50) 输出高电平 → 选中百位
                        DIG6_OFF();  // PIN6 (P61) 输出低电平 → 选中A段
                    }      
                    else if (seg == 1) 
                    { 
                        // B1段:PIN2输出高电平,PIN1输出低电平
                        DIG2_ON();   // PIN2 (P51) 输出高电平 → 选中百位(通过B1连接)
                        DIG1_OFF();  // PIN1 (P50) 输出低电平 → 选中B段
                    } 
                    else if (seg == 2) 
                    { 
                        // C1段:PIN2输出高电平,PIN3输出低电平
                        DIG2_ON();   // PIN2 (P51) 输出高电平
                        DIG3_OFF();  // PIN3 (P52) 输出低电平 → 选中C段
                    }
                    // seg == 3~6 (D1~G1) 在硬件上未连接,所以不做任何操作
                    break;

                // -------------------- 十位 (digit = 1) --------------------
                case 1:
                    // 十位有完整的7段连接(A2~G2),全部都有定义
                    if (seg == 0)      
                    { 
                        // A2段:PIN2输出高电平,PIN4输出低电平
                        DIG2_ON(); 
                        DIG4_OFF(); 
                    }
                    else if (seg == 1) 
                    { 
                        // B2段:PIN2输出高电平,PIN5输出低电平
                        DIG2_ON(); 
                        DIG5_OFF(); 
                    }
                    else if (seg == 2) 
                    { 
                        // C2段:PIN2输出高电平,PIN6输出低电平
                        DIG2_ON(); 
                        DIG6_OFF(); 
                    }
                    else if (seg == 3) 
                    { 
                        // D2段:PIN3输出高电平,PIN1输出低电平
                        DIG3_ON(); 
                        DIG1_OFF(); 
                    }
                    else if (seg == 4) 
                    { 
                        // E2段:PIN3输出高电平,PIN2输出低电平
                        DIG3_ON(); 
                        DIG2_OFF(); 
                    }
                    else if (seg == 5) 
                    { 
                        // F2段:PIN3输出高电平,PIN4输出低电平
                        DIG3_ON(); 
                        DIG4_OFF(); 
                    }
                    else if (seg == 6) 
                    { 
                        // G2段:PIN3输出高电平,PIN5输出低电平
                        DIG3_ON(); 
                        DIG5_OFF(); 
                    }
                    break;

                // -------------------- 个位 (digit = 2) --------------------
                case 2:
                    // 个位也有完整的7段连接(A3~G3)
                    if (seg == 0)      
                    { 
                        // A3段:PIN3输出高电平,PIN6输出低电平
                        DIG3_ON(); 
                        DIG6_OFF(); 
                    }
                    else if (seg == 1) 
                    { 
                        // B3段:PIN4输出高电平,PIN1输出低电平
                        DIG4_ON(); 
                        DIG1_OFF(); 
                    }
                    else if (seg == 2) 
                    { 
                        // C3段:PIN4输出高电平,PIN2输出低电平
                        DIG4_ON(); 
                        DIG2_OFF(); 
                    }
                    else if (seg == 3) 
                    { 
                        // D3段:PIN4输出高电平,PIN3输出低电平
                        DIG4_ON(); 
                        DIG3_OFF(); 
                    }
                    else if (seg == 4) 
                    { 
                        // E3段:PIN4输出高电平,PIN5输出低电平
                        DIG4_ON(); 
                        DIG5_OFF(); 
                    }
                    else if (seg == 5) 
                    { 
                        // F3段:PIN4输出高电平,PIN6输出低电平
                        DIG4_ON(); 
                        DIG6_OFF(); 
                    }
                    else if (seg == 6) 
                    { 
                        // G3段:PIN5输出高电平,PIN1输出低电平
                        DIG5_ON(); 
                        DIG1_OFF(); 
                    }
                    break;
            }`在这里插入代码片`
        }
      } 
e、状态机推进,准备下一次扫描
// ========== 第6步:状态机推进,准备下一次扫描 ==========
    // 当前段扫描完毕,移动到下一个段
    ScanSeg++;
f、段扫描完毕,切换到下一位
// 如果一个位的7个段都扫描完了(seg 从 0 到 6,然后变成7)
    if (ScanSeg >= 7) 
    {
        ScanSeg = 0;      // 段索引归零,从 A 段重新开始
        ScanDigit++;      // 切换到下一个位
        
        // 如果3个位都扫描完了(digit 从 0 到 2,然后变成3)
        if (ScanDigit >= 3) 
        {
            ScanDigit = 0;   // 位索引归零,从百位重新开始新一轮扫描
        }
    }

(2) 扫描起始位的规律

数码管位数 起始扫描位 对应的 digit
3位(百十个) 百位 0
4位(千百十个) 千位 0
5位(万千百十个) 万位 0

规律:永远从最高位开始扫描,digit = 0 始终对应最高位。

a、代码体现

扫描起始值由 ScanDigit 的初始值决定:

static volatile uint8_t ScanDigit = 0;   // 初始为 0,即最高位

如果要改成 4 位数码管,只需修改两处:

// 1. 缓冲区增加一位
static volatile uint8_t Disp_TH = 0xFF;  // 千位
static volatile uint8_t Disp_H  = 0xFF;  // 百位
static volatile uint8_t Disp_T  = 0xFF;  // 十位
static volatile uint8_t Disp_U  = 0xFF;  // 个位

// 2. 归零条件从 3 改为 4
if (ScanDigit >= 4)     // 原来是 >= 3
{
    ScanDigit = 0;
}

b、为什么从最高位开始?
原因 说明
习惯 日常阅读数字从左到右,最高位在最左边
代码简单 digit = 0 对应最高位,digit++ 依次向右扫描
扩展方便 无论几位,digit = 0 永远是最高位
c、 各位数码管映射表
位数 digit=0 digit=1 digit=2 digit=3 digit=4
3位 百位 十位 个位 - -
4位 千位 百位 十位 个位 -
5位 万位 千位 百位 十位 个位

核心原则:digit = 0 永远是最高位,digit 递增,位权递减。

(3)时序示例:显示 85

假设当前定时中断频率为 2ms 一次,扫描过程如下:

中断次数 ScanDigit ScanSeg num 动作
1 0(百) 0(A) 0xFF(灭) 无操作
2 0 1(B) 0xFF 无操作
百位7段全跳过
8 1(十) 0(A) 8 点亮 A2:DIG2_ON, DIG4_OFF
9 1 1(B) 8 点亮 B2:DIG2_ON, DIG5_OFF
十位7段依次点亮
15 2(个) 0(A) 5 点亮 A3:DIG3_ON, DIG6_OFF
个位7段依次点亮
22 0 0 新一轮开始

   每个段点亮后,只持续约 2ms,然后熄灭。但由于循环很快 (一圈约 21×2ms = 42ms),人眼看不出闪烁。

五、使用说明

1、 初始化

  在主程序初始化阶段调用 Tube188_Init()

2、设置显示内容

   使用 Tube188_DisplayPercent(uint8_t percent);
   例如:

Tube188_DisplayPercent(uint8_t percent); // 百位显示1
Tube188_DisplayPercent(uint8_t percent);  // 十位显示2
Tube188_DisplayPercent(uint8_t percent); // 个位显示3

3、不直接操作硬件?要申请一个Tube188_Scan()扫描驱动函数

(1)核心概念:显示缓冲区(写)Tube188_DisplayPercent扫描驱动(读) Tube188_Scan() 分离

   这是一个经典的单片机编程模式,叫 “双缓冲”“模型-视图分离”

部分 作用 类比
Disp_H/T/U 缓冲区,存储"想要显示什么" 剧本
Tube188_Scan() 扫描驱动,负责"如何在硬件上呈现" 演员表演

(2)为什么要Tube188_Scan()进行扫描直接操作不行吗?

   因为数码管是动态扫描显示的,同一时刻只能点亮一个段,需要靠人眼视觉暂留形成完整画面。

  • 如果 DisplayPercent 直接操作,它必须瞬间把21个段(3位×7段)全点亮,这在矩阵驱动中物理上不可能(引脚冲突)。
  • 所以 DisplayPercent 只负责更新"意图"(存到缓冲区),然后一个独立的定时器中断不断调用 Scan() 去逐个段地点亮硬件。

   Tube188_DisplayPercent() 就是在告诉扫描函数:“下次轮到该位时,请显示数字几”,至于怎么让数码管显示数字即(即:在硬件层就是哪个引脚高、哪个引脚低),那是 Scan() 的事。
在这里插入图片描述

(3) 动态扫描
  在定时器中断服务函数中(如已有的 100us 中断)调用 Tube188_Scan()。 建议每次中断调用一次,扫描频率需保证完整刷新周期小于 20ms

  • 例如:每 2ms 扫描一个段,则 7段×3位=21段,约 42ms 刷新一次,可适当提高中断频率或调整延时)。
  1. 熄灭所有显示
      调用 Tube188_DigOff() 可立即关闭所有段并清空缓冲区。

六、答疑与原理详解

1、关于十位显示逻辑 Disp_T = (percent < 10 && h == 0) ? 0xFF : t;

(1) 变量含义

  • h:百位数字(hundreds)
  • t:十位数字(tens)
  • u:个位数字(units)

示例:对于电量值 85

  • h = 85 / 100 = 0
  • t = (85 % 100) / 10 = 8
  • u = 85 % 10 = 5

(2)为什么要特殊处理十位?

  这是一个显示美观的需求。我们希望电量显示符合日常阅读习惯,高位零不显示

电量值 期望显示 百位状态 十位状态 个位状态
100 100 显示 1 显示 0 显示 0
85 85 熄灭 显示 8 显示 5
9 9 9 熄灭 熄灭 显示 9

  对于 9,若不特殊处理,硬件会显示 09(十位显示0),这不符合阅读习惯,因此需要将十位熄灭。

(3)条件表达式拆解

Disp_T = (percent < 10 && h == 0) ? 0xFF : t;

逻辑翻译

如果(电量小于10 并且 百位是0),那么十位熄灭(赋值 0xFF);否则,十位正常显示数字 t

验证

  • 85percent < 10 为假 → Disp_T = 8
  • 9percent < 10 为真,且 h == 0 为真 → Disp_T = 0xFF(熄灭) ✅

2、digit == 1 的含义辨析

(1)源代码

void Tube188_Scan(void) 
{
////////////
	if (digit == 0)           // 当前扫描的是百位
	        num = Disp_H;         // 从百位缓冲区取数字
	    else if (digit == 1)      // 当前扫描的是十位
	        num = Disp_T;         // 从十位缓冲区取数字
	    else                      // 当前扫描的是个位(digit == 2)
	        num = Disp_U;         // 从个位缓冲区取数字
///////////////////
	}

(2) 核心误解澄清

  代码 else if (digit == 1) num = Disp_T; 中的 digit 不是数字的值,而是硬件位选索引

(3) digit 与显示缓冲区的映射关系

digit 硬件含义 对应的缓冲区变量 示例(显示85)
0 百位 Disp_H 0xFF(熄灭)
1 十位 Disp_T 8
2 个位 Disp_U 5

(4) 代码执行流程

if (digit == 0)      num = Disp_H;   // 扫描第0位,取百位数字
else if (digit == 1) num = Disp_T;   // 扫描第1位,取十位数字
else                 num = Disp_U;   // 扫描第2位,取个位数字

3、为什么要使用显示缓冲区而不是直接操作硬件?

(1) 核心设计模式:模型与视图分离

  单片机编程中常用的 “双缓冲” 机制:

组成部分 职责 类比
Disp_H/T/U 显示缓冲区:存储“想显示什么” 剧本
Tube188_Scan() 扫描驱动:负责“如何在硬件上演出来” 演员

(2)不能直接操作硬件的原因

  188数码管是矩阵式连接,同一时刻物理上只能点亮一个LED段。

  • 如果直接在 DisplayPercent (写缓冲区)中操作硬件,无法瞬间同时点亮16个段(2位+2位×7段)。
  • 必须依靠动态扫描:由定时器中断高频调用 Scan() (读缓冲区) 轮流点亮每个段,利用人眼视觉暂留形成完整画面。

结论Disp_H = 1 只是下达指令:“下次轮到百位时,显示数字1”。具体怎么点亮是 Scan() 的事。

4、动态扫描原理 (Tube188_Scan)

(1) 数码管物理结构

  • 高电平引脚(阳极):PIN1~PIN5,负责选择(百位/十位/个位)。
  • 低电平引脚(阴极):PIN1~PIN6,负责选择(A/B/C/D/E/F/G)。

  点亮条件:对应位输出高电平,对应段输出低电平
  例如:点亮个位A段:DIG3_ON(); DIG6_OFF();
在这里插入图片描述

(2) 扫描必要性

  由于引脚复用,同一时刻只能点亮一个段。必须通过高速轮询,让每个段轮流亮起。

  扫描周期示意

[个位A段亮 → 灭] → [个位B段亮 → 灭] → … → [十位A段亮 → 灭] → … → [百位G段亮 → 灭] → 循环

(3)实例推演:显示数字 2 (mask = 0x5B)

  a、 段码二进制解析

  0x5B 二进制为 0101 1011
  位顺序约定:DP G F E D C B A(DP恒为0)。

位7 (DP) 位6 (G) 位5 (F) 位4 (E) 位3 (D) 位2 ( C ) 位1 (B) 位0 (A)
0 1 0 1 1 0 1 1

  结论:数字2需点亮 A、B、D、E、G 段。

   b、位测试运算

  代码 if (mask & (1 << seg)) 用于判断当前段是否需要点亮。其中seg = 0表示要处理哪段
用全局静态变量 ScanSeg赋值。为什么 ScanSeg可以存储当前段码值。因为他在.h文件中用了static修饰.存储在静态区不随机器动态改变

  • 假设 seg = 0 (A段):(1 << 0) = 0000 00010x5B & 0x01 = 1,点亮。
  • 假设 seg = 5 (F段):(1 << 5) = 0010 00000x5B & 0x20 = 0,熄灭。
   c、 硬件驱动

  假设当前扫描状态为:个位(digit = 2)的 A 段(seg = 0以A段为例

    i 、进入对应的 switch-case 分支
switch (digit) 
{
    case 0: // 百位
        // ...
        break;
        
    case 1: // 十位
        // ...
        break;
        
    case 2: // 个位
        // 当前 digit = 2,进入这里
        if (seg == 0)      { DIG3_ON(); DIG6_OFF(); }  // A3
        else if (seg == 1) { DIG4_ON(); DIG1_OFF(); }  // B3
        else if (seg == 2) { DIG4_ON(); DIG2_OFF(); }  // C3
        else if (seg == 3) { DIG4_ON(); DIG3_OFF(); }  // D3
        else if (seg == 4) { DIG4_ON(); DIG5_OFF(); }  // E3
        else if (seg == 5) { DIG4_ON(); DIG6_OFF(); }  // F3
        else if (seg == 6) { DIG5_ON(); DIG1_OFF(); }  // G3
        break;
}
    ii:匹配段条件

  当前 seg = 0,匹配第一个条件:

if (seg == 0) { DIG3_ON(); DIG6_OFF(); }  // A3: PIN3高 - PIN6低
    iii :执行引脚操作宏
// 宏定义
#define DIG3_ON()   (PIN(DIG3_PORT, DIG3_PIN) = 1)  // P52 = 1
#define DIG6_OFF()  (PIN(DIG6_PORT, DIG6_PIN) = 0)  // P61 = 0
    v :引脚电平变化
MCU 引脚 数码管引脚 电平变化 作用
DIG3_ON() P52 PIN3 0→10 \to 101(高电平) 选中个位(阳极)
DIG6_OFF() P61 PIN6 1→01 \to 010(低电平) 选中 A 段(阴极)
    iv :电流回路形成

  根据 188 数码管的矩阵连接关系:

  • PIN3(高电平) → 个位的 A 段 LED 阳极
  • PIN6(低电平) → 个位的 A 段 LED 阴极

  电流从 P52 流出,经过个位 A 段 LED,流入 P61,LED 点亮。

在这里插入图片描述

    vi.总结:位与数字的对应关系
digit 对应位 显示的数字来自 示例(显示数字 2)
000 百位 Disp_H 0xFF(熄灭)
111 十位 Disp_T 0xFF(熄灭)
222 个位 Disp_U 2(点亮)

  数字 222 是个位数,只有个位需要点亮,百位和十位都熄灭。所以扫描数字 222 的段时,digit 应该是 222(个位),而不是 111(十位)。

  d、状态机轮询机制
ScanSeg++;
if (ScanSeg >= 7) 
{    // 扫完7段
    ScanSeg = 0;
    ScanDigit++;       // 换下一位
    if (ScanDigit >= 3) 
    { // 扫完3位
        ScanDigit = 0;
    }
}
  e、时序模拟(假设每2ms触发一次中断)
中断次数 ScanDigit(位) ScanSeg(段) 动作
1 0(百) 0(A) 扫描百位A段
7 0(百) 6(G) 百位结束 → 进位
8 1(十) 0(A) 扫描十位A段
21 2(个) 6(G) 个位结束 → 归零
22 0(百) 0(A) 重新开始新一轮
  • 周期计算:3位 × 7段 = 21次中断。
  • 视觉效果:若每2ms中断一次,周期约42ms < 人眼视觉暂留时间(50ms),因此看起来是稳定显示

5、移位原理———即如何从个位数码管移动到十位数码管的

  现在详细解释 Tube188_Scan() 是如何实现从个位移动到十位的。

(1)核心:通过 ScanDigit(位) 变量的自增来实现位切换

  a、关键变量回顾
static volatile uint8_t ScanDigit = 0;   // 当前扫描位:0=百位, 1=十位, 2=个位
static volatile uint8_t ScanSeg   = 0;   // 当前扫描段:0=A, 1=B ... 6=G

  这两个变量是 静态局部变量static volatile),它们的值在函数调用结束后不会丢失,下次调用时保留上一次的值。

(2)位切换的完整流程

   a、每次只扫描一个段

  定时器每 2ms 调用一次 Tube188_Scan(),每次调用只执行一遍函数体,然后退出。

void Tube188_Scan(void) 
{
    uint8_t digit = ScanDigit;   // 读取当前是第几位
    uint8_t seg   = ScanSeg;     // 读取当前是第几段
    
    // ... 点亮当前这一段 ...
    
    // ========== 关键:状态推进 ==========
    ScanSeg++;                   // 段索引 +1
    
    if (ScanSeg >= 7)            // 如果当前位的7段都扫完了
    {
        ScanSeg = 0;             // 段索引归零
        ScanDigit++;             // ⭐ 位索引 +1,切换到下一位
    }
    
    if (ScanDigit >= 3)          // 如果3个位都扫完了
    {
        ScanDigit = 0;           // 位索引归零,重新从百位开始
    }
}
   b、状态机推进时序表

  假设我们从 个位 开始看起(实际上是从百位开始,但原理相同):

中断次数 调用前 ScanDigit(位) 调用前 ScanSeg(段) 扫描内容 调用后 ScanDigit(位) 调用后 ScanSeg(段) 说明
15 2(个位) 0 个位A段 2 1
16 2(个位) 1 个位B段 2 2
17 2(个位) 2 个位C段 2 3
18 2(个位) 3 个位D段 2 4
19 2(个位) 4 个位E段 2 5
20 2(个位) 5 个位F段 2 6
21 2(个位) 6 个位G段 0 0 ⭐ 个位扫完,切回百位
22 0(百位) 0 百位A段 0 1 新一轮开始
    i、移位核心代码
if (ScanSeg >= 7) 
    {
        ScanSeg = 0;      // 段索引归零,从 A 段重新开始
        ScanDigit++;      // 切换到下一个位
        
        // 如果3个位都扫描完了(digit 从 0 到 2,然后变成3)
        if (ScanDigit >= 3) 
        {
            ScanDigit = 0;   // 位索引归零,从百位重新开始新一轮扫描
        }
    }

(3)从十位切换到个位的具体时刻

  让我用十位切换到个位作为例子:

中断次数 调用前状态 执行动作 调用后状态 说明
8 digit=1, seg=0 扫十位A段 digit=1, seg=1
9 digit=1, seg=1 扫十位B段 digit=1, seg=2
10 digit=1, seg=2 扫十位C段 digit=1, seg=3
11 digit=1, seg=3 扫十位D段 digit=1, seg=4
12 digit=1, seg=4 扫十位E段 digit=1, seg=5
13 digit=1, seg=5 扫十位F段 digit=1, seg=6
14 digit=1, seg=6 扫十位G段 digit=2, seg=0 ⭐ 这里切换到个位!
15 digit=2, seg=0 扫个位A段 digit=2, seg=1 开始扫描个位
    i、移位核心代码
if (ScanSeg >= 7) 
    {
        ScanSeg = 0;      // 段索引归零,从 A 段重新开始
        ScanDigit++;      // 切换到下一个位
        
        // 如果3个位都扫描完了(digit 从 0 到 2,然后变成3)
        if (ScanDigit >= 3) 
        {
            ScanDigit = 0;   // 位索引归零,从百位重新开始新一轮扫描
        }
    }
  a、第14次调用时的详细执行过程:
void Tube188_Scan(void) 
{
    // 此时 ScanDigit = 1, ScanSeg = 6
    uint8_t digit = ScanDigit;   // digit = 1(十位)
    uint8_t seg   = ScanSeg;     // seg = 6(G段)
    
    // ... 点亮十位的G段 ...
    
    // ========== 状态推进 ==========
    ScanSeg++;                   // ScanSeg 从 6 变成 7
    
    if (ScanSeg >= 7)            // 7 >= 7,条件成立!
    {
        ScanSeg = 0;             // ScanSeg(段) 归零
        ScanDigit++;             // ScanDigit(位) 从 1 变成 2 ⭐ 切换到个位!
    }
    
    if (ScanDigit >= 3)          // 2 >= 3,条件不成立
    {
        // 不执行
    }
    
    // 函数结束,ScanDigit=2, ScanSeg=0
}

第15次调用时

void Tube188_Scan(void) 
{
    // 此时 ScanDigit = 2, ScanSeg = 0
    uint8_t digit = ScanDigit;   // Scandigit(位) = 2(个位)⭐ 已经切换到个位了
    uint8_t seg   = ScanSeg;     // Scanseg (段) = 0(A段)
    
    // 开始扫描个位的A段...
}

(4)图示:188数码管动态扫描状态机流程图

  a、完整扫描流程图

定时器中断入口

百位 A段

百位 B段

百位 C段

百位 D段

百位 E段

百位 F段

百位 G段

ScanSeg(段) >= 7 ?

ScanSeg(段) = 0
ScanDigit(位)++

继续本位的下一段

十位 A段

十位 B段

十位 C段

十位 D段

十位 E段

十位 F段

十位 G段

ScanSeg(段) >= 7 ?

ScanSeg(段) = 0
ScanDigit(位)++

继续本位的下一段

个位 A段

个位 B段

个位 C段

个位 D段

个位 E段

个位 F段

个位 G段

ScanSeg(段) >= 7 ?

ScanSeg(段) = 0
ScanDigit(位)++

继续本位的下一段

ScanDigit(位) >= 3 ?

ScanDigit(位) = 0

继续扫描

(5)小结

  从个位移到十位的核心机制

  1. 静态变量保持状态ScanDigit(位)ScanSeg(段) 在函数调用之间保持值不变。

/*==============================================================
 * 扫描状态
 * 
 * 记录当前扫描进行到哪一位、哪一段
 * ScanDigit:0=百位, 1=十位, 2=个位
 * ScanSeg:  0=A段, 1=B段, 2=C段, 3=D段, 4=E段, 5=F段, 6=G段
 *==============================================================*/
static volatile uint8_t ScanDigit = 0;   // 当前扫描位:0=百位, 1=十位, 2=个位
static volatile uint8_t ScanSeg   = 0;   // 当前扫描段:0=A,1=B,2=C,3=D,4=E,5=F,6=G

  2. 每次调用推进一格ScanSeg(段)++ 每调用一次函数,段索引加1。

// ========== 第6步:状态机推进,准备下一次扫描 ==========
    // 当前段扫描完毕,移动到下一个段
    ScanSeg++; //段加一

  3. 扫完7段进位:当 ScanSeg(段) >= 7 时,说明一个位的所有段都扫过了,此时 ScanSeg(段) 归零,ScanDigit(位)++,切换到下一位。

 if (ScanSeg >= 7)            // 7 >= 7,条件成立!
    {
        ScanSeg = 0;             // ScanSeg(段) 归零
        ScanDigit++;             // ScanDigit(位) 从 1 变成 2 ⭐ 切换到个位!
    }

  4. 定时器高频调用:每 1~2ms 调用一次,整个状态机自动运转,无需外部干预。

  这就是为什么你只需要在定时器中断里不断调用 Tube188_Scan(),它就能自动完成百位→十位→个位→百位的循环扫描!

5、完整数据流图

定时器中断 每2ms触发

主循环

等待定时器触发

下一中断

Tube188_DisplayPercent 参数: 85

计算位值: h=0, t=8, u=5

更新缓冲区: Disp_H=0xFF, Disp_T=8, Disp_U=5

硬件无动作,仅更新内存

Tube188_Scan 扫描函数

读取状态: digit=1十位, seg=0 A段

读取缓冲区: num = Disp_T = 8

查段码表: SegCode8 = 0x7F

位测试: A段需要点亮

驱动引脚: DIG2_ON, DIG4_OFF

状态推进: ScanSeg++

硬件层: 十位A段LED亮起约2ms

视觉效果: 人眼看到稳定的85

6、注意事项

  1. 硬件映射:代码中的引脚连接(如 DIG2_ON(); DIG4_OFF();)需与实际硬件原理图严格对应,若有差异请调整 case 中的引脚操作。
  2. 共阴/共阳:本驱动基于共阴极数码管编写(1点亮)。若为共阳极,需将段码取反(mask = ~SegCode[num])。
  3. 扫描频率Tube188_Scan() 内部无软件延时,依赖调用频率控制亮度。建议在定时器中每 1~2ms 调用一次。
  4. 扩展功能:电池图标(K1~K8)及跑马灯逻辑未包含在当前驱动中,可在现有框架上参照段驱动方式自行扩展。
Logo

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

更多推荐