64_188数码管驱动技术(扫描型)详细技术解析
该代码实现了188数码管的动态扫描驱动,基于JSC8P012平台,通过GPIO宏操作引脚。主要功能包括:定义数码管段码表、引脚映射关系、显示缓冲区管理,以及内部扫描函数。采用分时扫描方式,支持在指定位置显示数字,包含清除所有引脚和驱动特定段的功能。代码结构清晰,包含详细的注释和修改日志,适用于三位数码管的显示控制。
一、引言
相信新手在接触到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 刷新一次,可适当提高中断频率或调整延时)。
- 熄灭所有显示
调用Tube188_DigOff()可立即关闭所有段并清空缓冲区。
六、答疑与原理详解
1、关于十位显示逻辑 Disp_T = (percent < 10 && h == 0) ? 0xFF : t;
(1) 变量含义
h:百位数字(hundreds)t:十位数字(tens)u:个位数字(units)
示例:对于电量值 85
h = 85 / 100 = 0t = (85 % 100) / 10 = 8u = 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。
验证:
85:percent < 10为假 →Disp_T = 8✅9:percent < 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 0001,0x5B & 0x01 = 1→ 真,点亮。 - 假设
seg = 5(F段):(1 << 5) = 0010 0000,0x5B & 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 10→1(高电平) | 选中个位(阳极) |
DIG6_OFF() |
P61 | PIN6 | 1→01 \to 01→0(低电平) | 选中 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、完整扫描流程图
(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、完整数据流图
6、注意事项
- 硬件映射:代码中的引脚连接(如
DIG2_ON(); DIG4_OFF();)需与实际硬件原理图严格对应,若有差异请调整case中的引脚操作。 - 共阴/共阳:本驱动基于共阴极数码管编写(1点亮)。若为共阳极,需将段码取反(
mask = ~SegCode[num])。 - 扫描频率:
Tube188_Scan()内部无软件延时,依赖调用频率控制亮度。建议在定时器中每 1~2ms 调用一次。 - 扩展功能:电池图标(K1~K8)及跑马灯逻辑未包含在当前驱动中,可在现有框架上参照段驱动方式自行扩展。
更多推荐



所有评论(0)