# dled_ctl
**Repository Path**: li0702/dled_ctl
## Basic Information
- **Project Name**: dled_ctl
- **Description**: 一个易用且高效的单片机动态数码管控制库,用于驱动 3461AS-1 数码管。
依赖 74HC138 和 74HC245 两个译码器。(51单片机)
- **Primary Language**: C
- **License**: ISC
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 1
- **Created**: 2022-05-13
- **Last Updated**: 2022-05-13
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# dled_ctl
----------
### 介绍
dled_ctl 是一个易用且高效的动态数码管控制库,用于驱动 3461AS-1 数码管。
依赖 **74HC138** 和 **74HC245** 两个译码器。
目前,本库在 51 单片机(STC89C52)上测试通过,3461AS-1 是一个共阴极的数码管。
### 关于此库
*dlec_ctl* - 动态数码管控制库 (3461AS-1) - Version 1.3.2
*by Nixsawe *
由于作者目前仍在学习单片机,因此代码或文档叙述中可能包含错误或者有语言不当的地方,请大家抱着质疑的态度来阅读文档及代码。
欢迎报告 BUG!欢迎交流!
### 为什么编写这个库
抽象与封装,是现代计算机能够高效设计、开发、运行的基石。
在每次使用时现写代码,现场设计点亮数码管的逻辑并不现实。
为了方便开发,更方便地使用 3461AS-1 数码管,特封装此库。
### 使用方法
将 `dled_ctl.c` 添加到工程中

然后将库所在目录,添加到工程的包含目录列表 (`INCLUDE DIR`)

然后在要调用库函数、或是使用本功能的源文件中引用头文件 `dled_ctl.h`

### 减小空间占用
本库提供了多个函数,但实际用户可能就用到其中的一个或两个函数。可以**通过删除未使用的函数减小空间占用**。
可以在 `Target Options` 中用 `LX51` 代替 `BL51`,然后在 `LX51 Misc` 标签页中的 `Misc Control` 中加入 `REMOVEUNUSED`。
使用 `LX51` 代替 `BL51`

添加 `REMOVEUNUSED` 连接器参数

### 功能原理
3461AS-1 数码管属于**动态数码管**,无法同时点亮多组数码管 *(在本库中,一个数码的位置认为是一组)*。
若需要同时显示多个数字,则需要利用发光管的余晖和人眼的视觉残留效果实现,即,**以较高的频率不断扫描点亮所有数码管**。
在普中 A2 开发板中,单片机要完整实现点亮数码管的功能,需要用到两个译码器,**74HC138** 和 **74HC245**。
**74HC138 译码器负责选择特定的一组数码管**。这个功能是通过**位选线**实现的。74HC138 的 A、B、C 分别与单片机 P2 口的 P2.2、P2.3、P2.4 脚连接。
位选的逻辑是这样的:定义低电平为 0,高电平为 1,则 **(A + B\*2 + C\*2\*2)** 为实际选中的数码管编号减 1。
例如,当 74HC138 的 A、B、C 口输入均为低电平,则将选中编号为 1 的数码管,即右数第一组。此时表达式 (A + B\*2 + C\*2\*2) 的值为 0,**为了便于描述,我们就说该组数码管的地址(或称索引)是 0**。
而当 A、C 输入低电平,B 输入高电平时,将选中编号为 3 的数码管,即右数第三组。

**74HC245 译码器负责点亮所选数码管的特定位置。** 该译码器的 A 口与单片机的 P0 口是一一连接的,B 口则按 B0-B7 的顺序依次与译码管的 abcdefg 脚及 dp 脚连接。**A 口输入的针如果是高电平,则将点亮 B 口对应位置的发光管。**
若将 74HC245 的 A0-A7 整体视为一个字节,则可认为**字节中二进制 1 代表对应位置被点亮,二进制 0 代表对应位置熄灭**。这个字节值,在本库中称为**字节码**,用于代表特定的发光组合。易知每一组数码管共有 256 种发光组合。
(字节码每一位与发光位置对应关系见[宏定义](#宏定义))
本库已将所需功能封装成多个方法,可直接调用函数完成点亮或清除数码管显示的功能。不过有一点要特殊强调,***为了更方便理解,本库对数码管的索引顺序与 74HC138 译码器的索引顺序是相反的***。
译码器中使用地址 0-7 依次索引**从右至左**的各组数码管,而本库中使用地址 0-7 依次索引**从左至右**的数码管,顺序恰好相反。
例如,当在本库中访问索引为 5 的数码管时,代表访问左数第 6 组数码管,将置 74HC138 译码器的 A 为低、B 为高、C 为低,此时的真实索引地址为 2,选中右数第 3 组数码管(和左数第 6 组是同一回事)。
| A | B | C | pos索引 | 真实索引 | 含义 |
|:-:|:-:|:-:|:-------:|:--------:|:---------------:|
| 0 | 0 | 0 | 7 | 0 | 从左至右第 8 组 |
| 0 | 0 | 1 | 6 | 1 | 从左至右第 7 组 |
| 0 | 1 | 0 | 5 | 2 | 从左至右第 6 组 |
| 0 | 1 | 1 | 4 | 3 | 从左至右第 5 组 |
| 1 | 0 | 0 | 3 | 4 | 从左至右第 4 组 |
| 1 | 0 | 1 | 2 | 5 | 从左至右第 3 组 |
| 1 | 1 | 0 | 1 | 6 | 从左至右第 2 组 |
| 1 | 1 | 1 | 0 | 7 | 从左至右第 1 组 |
### 调用逻辑
相较于手动方式,本库提供了一套易于使用的函数接口。
输出内容分两种,一是纯数字内容 (可能包含负号),二是任意类型数据 (可通过数码管的位置组合点亮成自己想要的图形)。
#### 对于纯数字内容
如果仅想测试数码管是否能点亮,不需要较好的显示效果,亦不对性能做要求,则可以直接调用库函数 `dledShowNumber`,关于函数的说明内容可见[后文](#方法调用 "目标章节包含了所有方法的原型及其使用说明")。
下面两个例子展示了 `dledShowNumber` 函数的使用方式:
// 文件 sample/print123.c
#include "dled_ctl.h"
void main()
{
// 同时点亮多组数码管,需要用到动态扫描的方式
// 需要不断调用函数点亮数码管
while(1)
{
// 在数码管中点亮一个 123,向右对齐
dledShowNumber(123, 0);
}
}
其实验现象如下图

// 文件 sample/print_neg.c
#include "dled_ctl.h"
void main()
{
// 同时点亮多组数码管,需要用到动态扫描的方式
// 需要不断调用函数点亮数码管
while(1)
{
// 在数码管中点亮一个 -123456,向右对齐,前补 0,负号将显示在最左边
dledShowNumber(-123456, 1);
}
}
其实验现象如下图

从图中可以看到,位数较多时,数码管的显示效果不是很好,部分数码管的亮度较低。而在实际应用中,单片机要同时处理多个任务,可能导致显示效果更差。这种现象是 `dledShowNumber` 的工作机制引起的。
`dledShowNumber` 函数会调用 `dledMakeBuffer`,根据传过去的数值,填充对应的缓冲区,再调用 `dledShowBuffer` 进行显示。而 `dledMakeBuffer` 函数用到了很吃单片机性能的除法运算。在之前的循环中,我们反复调用 `dledShowNumber` 函数,期间大部分时间消耗于整数除法运算上,数码管点亮时间相对较短,故而看起来是较暗的。
合理优化顺序,在主要的循环中剥离缓冲区的构造过程,循环中仅负责根据缓冲区内容点亮数码管,则效率会显著提升。那么应在什么时候构造缓冲区呢?这个应视具体情况而定。
对于固定的内容,我们可以将其置于循环体外,提前构造,如下面的代码,我们固定点亮一排数字 12345678
// 文件 sample/buffered.c
#include "dled_ctl.h"
void main()
{
// 提前构造好缓冲区
dledMakeBuffer(12345678, 0);
while(1)
{
// 反复调用函数输出缓冲区的内容
dledShowBuffer();
}
}
其实验现象如下图

对于动态变化的内容,我们可以在其变化时临时构造一次,来更新内容,然后在其未变时,则直接根据之前的缓冲区内容点亮数码管。
#### 任意类型数据
可借助 `DLED_ABC` 宏函数实现在指定位上进行点亮,从而实现少数字母的显示,或者其他自定义图案。
为了简便,我们以字符 7 为例,我们手动绘制一个 7,要点亮的位示意如下

则对应的字节码应构造为 `DLED_ABC(1, 1, 1, 0, 0, 0, 0, 0)`。
想要在左数第二组数码管的位置处点亮这个字符 7,我们可以用如下的代码
dledShow(DLED_ABC(1, 1, 1, 0, 0, 0, 0, 0), 1);
我们可以使用 `dledShowBytes` 同时点亮多组数码管,当然,是动态显示。
// 定义一个数组
DLED_T buf[3] = {
DLED_ABC(1, 1, 1, 1, 0, 0, 1, 0), // 字符 3
DLED_ABC(1, 1, 1, 1, 1, 1, 0, 0), // 字符 0
DLED_ABC(1, 1, 1, 1, 0, 0, 1, 0) // 字符 3
};
while(1)
{
// 从第 6 个位置起点亮连续 3 组数码管
dledShowBytes(buf, 5, 3);
}
可以使用宏定义来简化代码,如上述代码可以写成
// 定义字符 3 和字符 0 的字节码
#define DLED_DIGIT_0 DLED_ABC(1, 1, 1, 1, 1, 1, 0, 0)
#define DLED_DIGIT_3 DLED_ABC(1, 1, 1, 1, 0, 0, 1, 0)
// 定义一个数组
DLED_T buf[3] = {
DLED_DIGIT_3, // 字符 3
DLED_DIGIT_0, // 字符 0
DLED_DIGIT_3 // 字符 3
};
while(1)
{
// 从第 6 个位置起点亮连续 3 组数码管
dledShowBytes(buf, 5, 3);
}
在实际使用中,`dled_ctl.h` 中已封装好数字字符的定义,用户可直接使用该宏而无需自行定义。详见后文[宏定义](#宏定义)。
也可以使用 `dledShowDigits` 达到同样的效果,效率几乎不受影响。不过 `dledShowDigits` 只能显示数字。
// 定义一个数组
DLED_T buf[3] = { 3, 0, 3 };
while(1)
{
// 从第 6 个位置起点亮连续 3 组数码管
dledShowDigits(buf, 5, 3);
}
数码管点亮效果示意如下图

由于模块暴露了 `dled_buffer` 缓冲区数组,用户代码中,可以直接对这个数组进行修改,然后再用 `dledShowBuffer` 将缓冲区内容显示出来。例如,下面的代码将在第 4、5、6 组数码管上点亮字符 CEO。
// 将缓冲区清零(仅在缓冲区之前有内容时才需要)
dledClear();
// 修改缓冲区数据
dled_buffer[3] = DLED_ABC(1, 0, 0, 1, 1, 1, 0, 0);
dled_buffer[4] = DLED_ABC(1, 0, 0, 1, 1, 1, 1, 0);
dled_buffer[5] = DLED_ABC(1, 1, 1, 1, 1, 1, 0, 0);
while(1)
{
// 点亮数码管
dledShowBuffer();
}

### 类型定义
库的代码中定义了一个 8 位无符号整数类型 ***DLED_T***。
typedef unsigned char DLED_T;
当为函数传递一个数组或一个字节码时,需要使用该类型进行定义,比如
DLED_T buffer[8]; // 定义一个数组
dledFormat(-1023, buffer, 8); // 调用函数 dledFormat,将 -1023 数值转换成可显示的字节码,右对齐置于 buffer 中
### 方法调用
本库提供了多个已封装好的方法,可以按如下分类:
1. 点亮或清除单独一组数码管
* ***void*** dledShowDP(***DLED_T*** byte, ***DLED_T*** dp, ***DLED_T*** pos);
* ***void*** dledShow(***DLED_T*** byte, ***DLED_T*** pos);
上面这两个函数都可以用于点亮或清除特定的一组数码管。
其中 `byte` 为表示发光组合的**字节码**,`dp` 用于单独指定小数点的发光状态 (`dledShow` 中使用 `byte` 的最高位指定它)。
`pos` 则为所选的数码管地址 (索引)。
当 `byte` 的数值非 0 时,代表点亮对应的数码管,当 `byte` 的数值为
0,代表该组数码管的所有位置都熄灭,即清除数码管显示。
2. 点亮或清除所有数码管
* ***void*** dledShowBytes(***DLED_T \**** a, ***int*** start_pos, ***int*** length);
* ***void*** dledShowDigits(***DLED_T \**** a, ***int*** start_pos, ***int*** length);
这两个函数通过 `start_pos` (起始索引) 和 `length` (显示长度) 来指定欲点亮数码管的范围。
对于 `dledShowBytes`:
`a` 应指向一个数组,这个数组内**应至少包括 `length` 个元素**,每个元素均为一个字节码。
对于 `dledShowDigits`:
类似上者,不过 `a` 的每个元素为一个 10 以内的自然数 (不包括 10),**函数会将其转换成对应的字节码再进行使用** (转换过程不影响原数组内容)。
3. 将整型数值格式化成字节码数组
* ***void*** dledFormat(***long*** number, ***DLED_T \**** a, ***int*** length);
本函数将数值 `number` 按正序转换成多个字节码,置于数组 `a` 中。数值的十进制位数应不超过 `length`。
转换得到的字节码会**右对齐**置于数组 `a` 的**尾部**,前面的部分 (如果有) 被清空 (不点亮)。
**数值可以为负**,这样第一个字节码为一个负号的组合。
如果数值的十进制位数 (包括负号) 比 `length` 长,则整个数组被清空,数组的最后一个字节被设为负号的字节码。
4. 按数值内容点亮一组或多组数码管
* ***void*** dledShowNumber(***long*** number, ***int*** show_prefix);
本函数将数值 `number` 逐 10 进制位转换成多个字节码,并按这些字节码点亮一组或多组数码管。
若 `show_prefix` 不为零,则当 `number` 数值不足 8 个 10 进制位 (对于负数,也包括负号) 时,前面空余位置将显示 0。
本函数会将转换好的字节码存于库内的一个缓冲区内 (`dled_buffer`,见后文“变量使用”),所以后续维持点亮可以直接调用 `dledShowBuffer` 函数 **(也会大大提高效率)**。
*这个函数会修改缓冲区的内容。*
5. 点亮/清空所有数码管显示
* ***void*** dledSetAll();
* ***void*** dledClear();
其中,`dledSetAll` 函数将点亮所有数码管的所有发光管;`dledClear` 则用于清除所有数码管显示内容。
一般用于进行提示,或是测试数码管好坏。
*这两个函数均会修改缓冲区的内容。*
6. 直接缓冲区操作
* ***void*** dledMakeBuffer(***long*** number, ***int*** show_prefix);
* ***void*** dledShowBuffer();
这两个函数将直接访问/修改库内的缓冲区。
其中 `dledMakeBuffer` 类似 `dledShowNumber`,不同的是 `dledMakeBuffer` 仅进行数值到字节码的转换,并写到缓冲区内,并不实际点亮数码管。
`dledShowBuffer` 则是根据缓冲区内容,直接点亮对应的数码管。
将数值转换成字节码需要进行整数除法,**而单片机处理该运算较慢,动态扫描点亮会导致严峻的性能损耗**。如果有大量显示需求,或者对性能要求较高,则一般需要**先进行转换,预先生成目标字节码,然后动态扫描点亮。**
*`dledMakeBuffer` 会修改缓冲区的内容。*
注:点亮一组数码管后,除非再次点亮别组的数码管,否则**该组数码管将维持点亮状态**,因此可能需要清除。而同时点亮多组数码管,是通过**动态扫描的方式,不断依次点亮各组数码管**,当动态扫描停止,将只有最后一次访问的数码管保留点亮状态。
### 宏定义
用户可以使用库里定义的宏:
***DLED_T_(x)*** 将整数 x 强制转换成 ***`DLED_T`*** 类型,返回转换后的值。
***DLED_ABC(a,b,c,d,e,f,g,dp)*** 按照要点亮的位置,直接构建并返回一个字节码。

***DLED_BYTE_DP(byte,dp)*** 构建并返回字节码,用指定的 `dp` 单独覆盖小数点位置。
***DLED_DIGIT_0*** 到 ***DLED_DIGIT_9*** 分别表示数字 0 到 9 的字节码。
***DLED_DIGIT_MINUS*** 负号的字节码。
***DLED_ADDR*** 动态数码管对应的特殊寄存器,可用 `DLED_ADDR = byte;` 这样的写法手动调整当前数码管的点亮状态。
***DLED_PCTL_A***、***DLED_PCTL_B***、***DLED_PCTL_C*** 控制位选线的三个特殊寄存器,分别为一个单独引脚。
用于可以在工程中设置定义以下的宏来配置所使用的引脚等:
***DLED_ADDR*** 配置 74HC245 译码器的 A 口所连的口,如 P0、P1
***DLED_PCTL_A*** 配置 74HC138 译码器的 A 所连线
***DLED_PCTL_B*** 配置 B 所连线
***DLED_PCTL_C*** 配置 C 所连线
### 变量使用
*DLED_T* dled_digit\[10\];
包含 0-9 对应的字节码,用户可以自己配置映射关系。
*DLED_T* dled_buffer\[8\];
内部的缓冲区,可手动修改缓冲区内容,然后调用 `dledShowBuffer` 实现自定义的内容绘制。