理论部分的要点:

数码管的内部结构原理:

数码管有8个独立的发光的二极管。如下图

动态扫描的原理:

既然A-DP的8个端口都是相同的,那我们好像没有办法让8个数码管输出不一样的数字啊。

这里就需要一个:动态内存扫描

假设我们想要让前4个数码管显示1234(如图)

基于我们刚刚的,虽然A-DP是公用的,但是每一位数码管我们都有独立的com引脚来控制哪一个数码管亮,例如我们让4个端口都是显示1

然后熄灭后三位(com2-4给低电平)

这个是可以完全实现的

类似的让他全部显示为2

然后熄灭1,3,4(com1,com3,com4低电平),com2高电平

以此类推,我们让最后只显示4

我们就让他对应的位置显示相应的数字。

那么我们就可以让它更快切换1234,让他循环的速度足够快,这样我们人眼就看出来他是连贯的,那么这样以此无论是8个数码管也是一样的原理。这就是动态扫描的原理。

那么他的a1-dp1又是连接到哪里的呢?看下图

这里就是一个74HC573锁存器的原理了,再之前的LED点亮的博客里面有写:

单片机的LED原理和模块分析(STC的IAP15F2K61S2芯片为例)-CSDN博客

共阳数码管段码表:

解释:

举第一个例子:0xC0

0xc0换成2进制就是1100 0000,分别对应着8个端口dp g f e d c b a(从高到低的),根据上面分析的,数码管的LED的正极已经是高电平了,我们直接给负极(8个端口的那一段)输入低电平就行,就是现在的dp g这两个给高电平(不会亮),其他低电平(点亮),那么这样就可以知道,他在数码管上显示的就是0了。

其他的显示数字原理一样不在重复赘述。

关于上面段码的注意事项

为什么字母A-F有些显示的是大写,有些是小写呢,b显示大写就跟8一样了,d写成大写就是0,他们就很数字共用了一个段码,就没有必要了。

实践部分和代码原理分析:——主要看两个函数

Seg_Tran函数——显示的数字转换成数码管能识别的段码

代码:

void Seg_Tran(unsigned char* pucseg_buf,unsigned char* pucseg_code)
{
	//这里pucseg_buf是我们想要让数码管显示的数字,pucseg_code是段码,char*是要传入数组,要用指针变量的形式接收
	unsigned char i,j;
	for(i=0,j=0;i<=7;i++,j++)
	{
		switch(pucSeg_Buf[j])
		{
			case '0':
				pucSeg_Code[i] = 0xC0;
				break;
			case '1':
				pucSeg_Code[i] = 0xF9;
				break;
			case '2':
				pucSeg_Code[i] = 0xA4;
				break;
			case '3':
				pucSeg_Code[i] = 0xB0;
				break;
			case '4':
				pucSeg_Code[i] = 0x99;
				break;
			case '5':
				pucSeg_Code[i] = 0x92;
				break;
			case '6':
				pucSeg_Code[i] = 0x82;
				break;
			case '7':
				pucSeg_Code[i] = 0xF8;
				break;
			case '8':
				pucSeg_Code[i] = 0x80;
				break;
			case '9':
				pucSeg_Code[i] = 0x90;
				break;
			case 'A':
				pucSeg_Code[i] = 0x88;
				break;
			case 'b':
				pucSeg_Code[i] = 0x83;
				break;
			case 'C':
				pucSeg_Code[i] = 0xC6;
				break;
			case 'd':
				pucSeg_Code[i] = 0xA1;
				break;
			case 'E':
				pucSeg_Code[i] = 0x86;
				break;
			case 'F':
				pucSeg_Code[i] = 0x8E;
				break;
			case ' ':
				pucSeg_Code[i] = 0xFF;
				break;
			
		}
		if(pucSeg_Buf[j+1] == '.')//1234.5678
		{
			pucSeg_Code[i] &= 0x7F;//123(4.)5678
			j++;
		}
	}
}

两个变量

  • j:用来遍历 pucseg_buf,取要显示的字符

  • i:用来给 pucseg_code 赋值

注意:这里 i 和 j 是同步增加的,所以效果上 pucseg_code[i] 和 pucseg_buf[j] 是对应位置的。

switch(pucseg_buf[j]):

代码作用
根据 pucseg_buf[j] 的值(一个字符),决定执行哪个 case。

举例
当 j=0pucseg_buf[0] 是 '1',就会匹配 case '1'

case '1': pucseg_code[i] = 0xf9;:

代码作用
将段码 0xF9 存入 pucseg_code 的第 i 个位置。

段码 0xF9 的含义

  • 0xF9 = 1111 1001(二进制)

  • 对应数码管的段:dp g f e d c b a

  • 1 表示灭,0 表示亮

  • 结果是:b、c 段亮,其他灭 → 显示数字 1

if(pucSeg_Buf[j+1] == '.')

{
            pucSeg_Code[i] &= 0x7F;//123(4.)5678
            j++;

 }

如果要显示的数字的下一位是有.的话,那就再他的前一位加上0(点亮要取反),把4.看成是一个整体

Seg_Disp函数——将指定的数码管位置上显示对应的段码。

void Seg_Disp(unsigned char *pucSeg_Code, unsigned char ucSeg_Pos)
{
	P0 = pucSeg_Code[ucSeg_Pos];
	P2 = P2 & 0x1F | 0xE0;
	P2 &= 0x1F;

	P0 = 1<<ucSeg_Pos;
	P2 = P2 & 0x1F | 0xC0;
	P2 &= 0x1F;
}

1、送段码

1、P0 = pucseg_code[ucSeg_Pos];//送段码

把当前要显示的段码送到 P0 口。

举例
如果 ucSeg_Pos = 2,就取 pucseg_code[2] 的值(比如 0xB0,显示 3)送到 P0。

2、P2 = P2 & 0x1F | 0xE0;

拆解

  • 0xE0 = 1110 0000

  • P2 & 0x1F:保留低 5 位

  • | 0xE0:强制 P27=1, P26=1, P25=1

结果
P27=1, P26=1, P25=1 → 二进制 111 → 选中 Y7

硬件意义
选中 Y7 对应的锁存器(段码锁存器),LE 端为高电平,数据进入锁存器

3、    P2 &= 0x1F;

清零高 3 位,P27~P25 全变 0,选中 Y0。

硬件意义
LE 端变低,数据被锁存。段码被固定住。

 2、送位码

1、P0 = 1<<ucSeg_Pos;

将 1 左移 ucSeg_Pos 位,结果送到 P0。

移位效果

  • ucSeg_Pos = 0 → 1 << 0 = 0000 0001

  • ucSeg_Pos = 1 → 1 << 1 = 0000 0010

  • ucSeg_Pos = 2 → 1 << 2 = 0000 0100

  • ...

  • ucSeg_Pos = 7 → 1 << 7 = 1000 0000

硬件意义
每个位对应一个数码管的公共端(COM)。哪个位是 1,哪个数码管就被选中点亮。

    2、P2 = P2 & 0x1F | 0xC0;

拆解

  • 0xC0 = 1100 0000

  • 强制 P27=1, P26=1, P25=0 → 二进制 110 → 选中 Y6

硬件意义
选中 Y6 对应的锁存器(位码锁存器),LE 端为高电平,位码数据进入。

  3、  P2 &= 0x1F;

代码作用
再次锁存,位码被固定。此时指定位置的数码管亮起,显示刚才送进去的段码。

主文件的代码:

#include "init.h"
#include "led.h"
#include "seg.h"
#include "time.h"

//Seg
unsigned char pucSeg_Buf[12], pucSeg_Code[8], pucSeg_Pos = 0;

//Timer
unsigned long ulms = 0;
unsigned int uiSeg_Dly = 0;

void Seg_Proc(void);

void main(void)
{
	Cls_Peripheral();//清除外设函数(led 蜂鸣器)
	Timer0Init();//定时器的声明
	EA = 1;
	
	while(1)
	{
		Seg_Proc();//单片机工作停不下来
	}
	
}

void Seg_Proc(void)
{
	if(uiSeg_Dly < 200)
		return;
	
	uiSeg_Dly = 0;
	
	sprintf(pucSeg_Buf, "12.3 124 ");
	Seg_Tran(pucSeg_Buf, pucSeg_Code);
	
}

void Time_0(void) interrupt 1
{
	ulms++;
	uiSeg_Dly++;
	
	if(ulms % 2 == 0)
	{
		pucSeg_Pos = (pucSeg_Pos + 1)%8;
		Seg_Disp(pucSeg_Code, pucSeg_Pos);
	}
}

pucSeg_Buf,长度为 12确保足够的容量

1、Cls_Peripheral();

代码作用
调用外设清除函数,让所有外设处于初始安全状态。

注释解读
函数会关掉LED和蜂鸣器。

2、Timer0Init();

代码作用
调用定时器 0 初始化函数,设置每 1ms 中断一次。

注释解读

调用

EA=1;

代码作用
打开总中断允许位。

硬件意义
CPU 开始响应所有已打开的中断源。此时定时器 0 每 1ms 就会触发一次中断。

Seg_Proc() 函数分析

if(uiSeg_Dly < 200) return;

代码作用
判断 uiSeg_Dly 是否小于 200,如果是,立即退出函数。

变量追踪

  • uiSeg_Dly 在定时器中断里每 1ms 加 1

  • 初始为 0

执行流程

  • 第 1~199 次进入:uiSeg_Dly 从 0 到 199,每次都 <200 → 成立 → 返回

  • 第 200 次进入:uiSeg_Dly = 200 → <200 不成立 → 继续往下执行

作用
实现每 200ms 才执行一次 sprintf 和 Seg_Tran,即每 200ms 更新一次要显示的内容


uiSeg_Dly = 0

代码作用
将计数器清零,让下一次计时重新开始。


sprintf(pucSeg_Buf, "12.3 124 ");

代码作用
将字符串 "12.3 124 " 格式化后存入 pucSeg_Buf 数组。

字符串分析

位置: 0    1    2    3    4    5    6    7    8    9
字符: '1'  '2'  '.'  '3'  ' '  '1'  '2'  '4'  ' '  '\0'

显示效果
如果连接到 8 位数码管,显示效果是:

  • 第1位:1

  • 第2位:2

  • 第3位:.(小数点)

  • 第4位:3

  • 第5位:空格(灭)

  • 第6位:1

  • 第7位:2

  • 第8位:4

实际显示12.3 124(中间有个空格)


Seg_Tran(pucSeg_Buf, pucSeg_Code);

代码作用
调用段码转换函数,将 pucSeg_Buf 里的字符转换成段码,存入 pucSeg_Code

执行频率
每 200ms 执行一次。

定时器中断函数

1、ulms++

代码作用
毫秒计数器加 1,每 1ms 一次。

2、 uiSeg_Dly++

代码作用
软件定时器计数器加 1,供主循环的 Seg_Proc() 判断使用。

3 、if(ulms % 2 == 0)

代码作用
判断 ulms 是否是偶数。

执行频率

  • ulms 每 1ms 加 1

  • 奇数和偶数交替出现

  • 所以这个条件每 2ms 成立一次

硬件意义
实现每 2ms 切换一次数码管显示位置

4 、pucSeg_Pos = (pucSeg_Pos + 1) % 8

代码作用
将 pucSeg_Pos 加 1,然后对 8 取余,保证在 0~7 循环。

执行频率
每 2ms 执行一次。

作用
实现数码管的动态扫描,每次切换一个数码管。

5、 Seg_Disp(pucSeg_Code, pucSeg_Pos)

代码作用
调用显示函数,把 pucSeg_Code 里的段码显示在 pucSeg_Pos 指定的数码管上。

执行频率
每 2ms 执行一次。

Logo

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

更多推荐