# 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` 添加到工程中 ![添加文件到工程](img/img-add-source.png "将 dled_ctl.c 添加到工程中") 然后将库所在目录,添加到工程的包含目录列表 (`INCLUDE DIR`) ![添加包含目录](img/img-add-include-path.png "将 dled_ctl 库文件所在路径添加到包含目录列表中") 然后在要调用库函数、或是使用本功能的源文件中引用头文件 `dled_ctl.h` ![包含头文件](img/img-include-header.png "在源文件中包含头文件 dled_ctl.h") ### 减小空间占用 本库提供了多个函数,但实际用户可能就用到其中的一个或两个函数。可以**通过删除未使用的函数减小空间占用**。 可以在 `Target Options` 中用 `LX51` 代替 `BL51`,然后在 `LX51 Misc` 标签页中的 `Misc Control` 中加入 `REMOVEUNUSED`。 使用 `LX51` 代替 `BL51` ![配置连接器](img/img-use-lx51.png "在 Device 选项卡中设置使用 LX51 连接器") 添加 `REMOVEUNUSED` 连接器参数 ![配置连接参数](img/img-lx51-controls.png "在 LX51 Misc 选项卡中添加 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 的数码管,即右数第三组。 ![选择数码管](img/img-74hc138.png "74HC138 译码器负责根据位选线选择数码管") **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); } } 其实验现象如下图
![print123 运行结果](img/sample-print-123.jpg "print123.c 运行结果") // 文件 sample/print_neg.c #include "dled_ctl.h" void main() { // 同时点亮多组数码管,需要用到动态扫描的方式 // 需要不断调用函数点亮数码管 while(1) { // 在数码管中点亮一个 -123456,向右对齐,前补 0,负号将显示在最左边 dledShowNumber(-123456, 1); } } 其实验现象如下图
![print_neg 运行结果](img/sample-print-neg.jpg "print_neg.c 运行结果")
从图中可以看到,位数较多时,数码管的显示效果不是很好,部分数码管的亮度较低。而在实际应用中,单片机要同时处理多个任务,可能导致显示效果更差。这种现象是 `dledShowNumber` 的工作机制引起的。 `dledShowNumber` 函数会调用 `dledMakeBuffer`,根据传过去的数值,填充对应的缓冲区,再调用 `dledShowBuffer` 进行显示。而 `dledMakeBuffer` 函数用到了很吃单片机性能的除法运算。在之前的循环中,我们反复调用 `dledShowNumber` 函数,期间大部分时间消耗于整数除法运算上,数码管点亮时间相对较短,故而看起来是较暗的。 合理优化顺序,在主要的循环中剥离缓冲区的构造过程,循环中仅负责根据缓冲区内容点亮数码管,则效率会显著提升。那么应在什么时候构造缓冲区呢?这个应视具体情况而定。 对于固定的内容,我们可以将其置于循环体外,提前构造,如下面的代码,我们固定点亮一排数字 12345678 // 文件 sample/buffered.c #include "dled_ctl.h" void main() { // 提前构造好缓冲区 dledMakeBuffer(12345678, 0); while(1) { // 反复调用函数输出缓冲区的内容 dledShowBuffer(); } } 其实验现象如下图
![buffered 运行结果](img/sample-buffered.jpg "buffered.c 运行结果") 对于动态变化的内容,我们可以在其变化时临时构造一次,来更新内容,然后在其未变时,则直接根据之前的缓冲区内容点亮数码管。 #### 任意类型数据 可借助 `DLED_ABC` 宏函数实现在指定位上进行点亮,从而实现少数字母的显示,或者其他自定义图案。 为了简便,我们以字符 7 为例,我们手动绘制一个 7,要点亮的位示意如下 ![字符 7](img/img-single-led-7.png "字符 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); } 数码管点亮效果示意如下图 ![点亮多组数码管](img/img-led-303.png "点亮多组数码管") 由于模块暴露了 `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(); } ![点亮字符 CEO](img/img-led-ceo.png "修改缓冲区点亮字符 CEO") ### 类型定义 库的代码中定义了一个 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)*** 按照要点亮的位置,直接构建并返回一个字节码。
![位置对应关系](img/img-led-pos.png "位置对应关系") ***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` 实现自定义的内容绘制。