使用MCU分层操作数码管和LED灯,利用Union联合体实现便捷修改

基本思路

定义同类型的ledbuffer与ledbufferdisplay,一个用于逻辑上应用层的控制,一个用于底层io口的控制,用于底层的在中断中调用

用于逻辑控制的在主循环,这样应用层与底层分离,在修改主循环逻辑时我们只需关注主循环内的ledbuffer就可以

数据类型如何定义

我们的数码管规定了abcdegf这几个段,如下图

在c语言中有union(联合体)这个概念,

关于union类型的知识,这里不做重点,不懂的可以看下面这篇文章

C语言-联合体union_c语言union-CSDN博客

我们可以这样定义数据结构

这里以两个数码管为例,一个com对应一个数码管七个段,

我们拿出com1来看

u8 DispNUM1Place_A					:1;//
u8 DispNUM1Place_B					:1;//
u8 DispNUM1Place_C					:1;//
u8 DispNUM1Place_D					:1;//
u8 DispNUM1Place_E					:1;//
u8 DispNUM1Place_F					:1;//
u8 DispNUM1Place_G					:1;//
u8 DispRemain_0						:1;//

这八个变量每个都占1位(注意是位不是字节),这个操作叫位域

C 位域 | 菜鸟教程

前七个变量是分别代表数码管abcdefg七个段

最后一个   u8 DispRemain_0                        :1;//

这个是占位的,目的是凑齐8位,为什么这样呢?因为union所占字节数是其内部最大的来算

例如:我既可以通过DispBuffer.DispNUM1Place_A去操作数码管的a段,

        也可以通过DispArray[0]=0x01;去操作数码管的a段

如果不凑这一位,DispBuffer这个结构体里面就是14位,那在用DispArray去操作就会出问题

最开头也说过,要定义一个ledbuffer用于主循环操作,定义一个ledbufferdisplay用于1ms中断内io口操作

这里我改了一下变量名,gDispContentBufferbuf用于主循环,gDispContentBuffer用于1ms中断

1ms中断中操作io口部分(显示底层)

在实际操作io口的时候要遵循一个原则,

1.关所有com

2.开对应seg

3.再开对应com

不遵循这个原则会出什么问题呢?如果一开始com还开着,同时上一次这个com上有seg打开了,而这一次是没有先关com,会导致上一次这个com上开着的seg刷新到这一次,体现出来的效果就是那个灯微微亮

void DispScan(void)
{
	u8 i=10;
	static unsigned char	S_U8_Shift=0;
    COM1_OFF;
	COM2_OFF;
	COM3_OFF;
	COM4_OFF;
	COM5_OFF;

	while(i)
	{
		i--;
	}
	if (S_U8_Shift <= 2)
	{
		
		if ((gDispContentBuffer.DispArray[S_U8_Shift] & 0x01) !=0)
		{
			SEG1_ON;
		}
		else
		{
			SEG1_OFF;
		}
		if ((gDispContentBuffer.DispArray[S_U8_Shift] & 0x02) !=0)
		{
			SEG2_ON;
		}
		else
		{
			SEG2_OFF;
		}
		if ((gDispContentBuffer.DispArray[S_U8_Shift] & 0x04) !=0)
		{
			SEG3_ON;
		}
		else
		{
			SEG3_OFF;
		}
		if ((gDispContentBuffer.DispArray[S_U8_Shift] & 0x08) !=0)
		{
			SEG4_ON;
		}
		else
		{
			SEG4_OFF;
		}
		if ((gDispContentBuffer.DispArray[S_U8_Shift] & 0x10) !=0)
		{
			SEG5_ON;
		}
		else
		{
			SEG5_OFF;
		}
		if ((gDispContentBuffer.DispArray[S_U8_Shift] & 0x20) !=0)
		{
			SEG6_ON;
		}
		else
		{
			SEG6_OFF;
		}
		if ((gDispContentBuffer.DispArray[S_U8_Shift] & 0x40) !=0)
		{
			SEG7_ON;
		}
		else
		{
			SEG7_OFF;
		}
	}
	else
	{
	}
	switch(S_U8_Shift)
	{
		case 0:
			  COM1_ON;
			  break;

		case 1:
			  COM2_ON;
			  break;

			  
		default:
			  break;
				
	}
	if (++S_U8_Shift >= 2)
	{
		S_U8_Shift = 0;
	}
}

上面这个函数DispScan()就放到1ms中断里面,这就是我前面说的用于控制底层的部分

SEG_ON,SEG_OFF,COM_ON,COM_OFF做了宏定义,指代对应的io口开关,这个要根据你自己的电路如何接的,高低电平这个逻辑要看你自己的电路

#define COM1_ON     ald_gpio_write_pin(GPIOC, GPIO_PIN_8, 0);
#define COM1_OFF     ald_gpio_write_pin(GPIOC, GPIO_PIN_8, 1);

解释一下这个函数,其实就做了那么几个操作

1.每次进1ms中断,先关所有com

2.while(i)延时个几us,等待com全关闭

3.扫前面定义的每个com,也就是这个数据结构里定义的两个com的每个段

举个例子

if ((gDispContentBuffer.DispArray[S_U8_Shift] & 0x01) !=0)
{
	SEG1_ON;
}
else
{
	SEG1_OFF;
}

这一段,当S_U8_Shift是0的时候,也就是gDispContentBuffer.DispArray[0]对第一位做与运算,

从位域上看就是DispNUM1Place_A:1这一位,

我这里默认电路上seg1-seg7,是按顺序接的数码管段的a-g

那么com1的seg1对应的led也就是第一个数码管的a段,

按以上依此判断我所有用到的位,对应io口输出对应的电平

4.seg开完了,那就开第一个com

5.这样第一次1ms中断,就把com1上的所有seg刷新了一遍,++S_U8_Shift,下一个1ms中断刷com2,

直到S_U8_Shift >= 2时(com2刷完),S_U8_Shift 清零,下一个1ms中断就再从com1开始刷

这样有什么优势?

1.先从这个数据结构看

每一位对应了一个seg,这样如果电路板io口有更改,只需要改这个数据结构就好不需要再去算具体的值,比如数码管1的a段在com2与数码管2的a段在com1,只需要这样改

其余就不需要管了,为什么呢?因为DispNUM2Place_A这一位的赋值在主循环做,而显示是在1ms内的,1ms内调用的DispScan();是把com1和com2全部扫描了一遍,只是说,

数码管1的a段在com2时是扫com2时刷新的

数码管2的a段在com1时是扫com1时刷新的

电路甚至可以随意接,改代码也不麻烦,甚至这个com接不全一个数码管又接了其他灯也好修改,甚至可以com1接9个seg,com2接2个seg,只是要更改

DispArray[]和所有位域改为16位的,com1用9个位com2用2个位剩下的补起来就可以,补起来的那些主循环不操作全部赋值为0就好了,同时DispScan();也要改要让一个com能扫9位

主循环

主循环只需要操作DispArray或者DispNUM2Place_A这样的某一位就可以

这里我们还是设seg1-seg7与abcdefg段从左到右一一对应,而且这是com1的七个seg

如果要显示数字2,那么就要让abged这几个段亮,

从代码上在主循环中,也就是

DispNUM1Place_A = 1;
DispNUM1Place_B = 1;
DispNUM1Place_C = 0;
DispNUM1Place_D = 1;
DispNUM1Place_E = 1;
DispNUM1Place_F = 0;
DispNUM1Place_G = 1;

这样可以,但是对于电路上用标准接法的数码管(就是我上面假设的这个一一对应的接法)来讲有些麻烦,

我们可以这样规整,把a-g段用1位表示出来

#define   LED_SEG_A 	0X01	//0000 0001
#define   LED_SEG_B 	0X02	//0000 0010
#define   LED_SEG_C 	0X04	//0000 0100
#define   LED_SEG_D 	0X80	//0000 1000
#define   LED_SEG_E 	0X10	//0001 0000 
#define   LED_SEG_F 	0X20	//0010 0000 
#define   LED_SEG_G 	0X40	//0100 0000 

定义一个标签数组

const unsigned char Digit_Led_Display_Table[] = 
{
	LED_SEG_A+LED_SEG_B+LED_SEG_C+LED_SEG_D+LED_SEG_E+LED_SEG_F, 			// 0
	LED_SEG_B+LED_SEG_C,													// 1
	LED_SEG_A+LED_SEG_B+LED_SEG_D+LED_SEG_E+LED_SEG_G,						// 2
	LED_SEG_A+LED_SEG_B+LED_SEG_C+LED_SEG_D+LED_SEG_G,						// 3
	LED_SEG_B+LED_SEG_C+LED_SEG_F+LED_SEG_G,								// 4
	LED_SEG_A+LED_SEG_C+LED_SEG_D+LED_SEG_F+LED_SEG_G,						// 5
	LED_SEG_A+LED_SEG_C+LED_SEG_D+LED_SEG_E+LED_SEG_F+LED_SEG_G,			// 6
	LED_SEG_A+LED_SEG_B+LED_SEG_C,											// 7
	LED_SEG_A+LED_SEG_B+LED_SEG_C+LED_SEG_D+LED_SEG_E+LED_SEG_F+LED_SEG_G,	// 8
	LED_SEG_A+LED_SEG_B+LED_SEG_C+LED_SEG_D+LED_SEG_F+LED_SEG_G,			// 9
	
	LED_SEG_A+LED_SEG_B+LED_SEG_C+LED_SEG_E+LED_SEG_F+LED_SEG_G,			//A
	LED_SEG_C+LED_SEG_D+LED_SEG_E+LED_SEG_F+LED_SEG_G,						//b
	LED_SEG_A+LED_SEG_D+LED_SEG_E+LED_SEG_F,								//c 
	LED_SEG_B+LED_SEG_C+LED_SEG_D+LED_SEG_E+LED_SEG_G,						//d
	LED_SEG_A+LED_SEG_D+LED_SEG_E+LED_SEG_F+LED_SEG_G,						//E
	LED_SEG_A+LED_SEG_E+LED_SEG_F+LED_SEG_G,								//F

	LED_SEG_B+LED_SEG_C+LED_SEG_E+LED_SEG_F+LED_SEG_G,						//H
	LED_SEG_A+LED_SEG_B+LED_SEG_C+LED_SEG_D+LED_SEG_E+LED_SEG_F,			//O
	LED_SEG_A+LED_SEG_B+LED_SEG_E+LED_SEG_F+LED_SEG_G,						//P
	LED_SEG_E+LED_SEG_G,													//r 
	LED_SEG_A+LED_SEG_C+LED_SEG_D+LED_SEG_F+LED_SEG_G,						//S
	LED_SEG_A+LED_SEG_E+LED_SEG_F,											//T

	LED_SEG_G,																//-		
	0,																		//off 0X1e
	LED_SEG_B+LED_SEG_C+LED_SEG_D+LED_SEG_E+LED_SEG_F,						//U												
	LED_SEG_D+LED_SEG_E+LED_SEG_F,											//L
};

这样Digit_Led_Display_Table[0]恰好就是显示0,同时也是数组的第0个字节

我们再定义一个函数,

void ScreemNumCompile(u8 NUM1PlaceIndex, u8 NUM2PlaceIndex)
{
	gDispContentBufferbuf.DispBuffer.DispNUM1Place_A = ((Digit_Led_Display_Table[NUM1PlaceIndex] & LED_SEG_A) != 0);
	gDispContentBufferbuf.DispBuffer.DispNUM1Place_B = ((Digit_Led_Display_Table[NUM1PlaceIndex] & LED_SEG_B) != 0);
	gDispContentBufferbuf.DispBuffer.DispNUM1Place_C = ((Digit_Led_Display_Table[NUM1PlaceIndex] & LED_SEG_C) != 0);
	gDispContentBufferbuf.DispBuffer.DispNUM1Place_D = ((Digit_Led_Display_Table[NUM1PlaceIndex] & LED_SEG_D) != 0);
	gDispContentBufferbuf.DispBuffer.DispNUM1Place_E  = ((Digit_Led_Display_Table[NUM1PlaceIndex] & LED_SEG_E) != 0);
	gDispContentBufferbuf.DispBuffer.DispNUM1Place_F  = ((Digit_Led_Display_Table[NUM1PlaceIndex] & LED_SEG_F) != 0);
	gDispContentBufferbuf.DispBuffer.DispNUM1Place_G = ((Digit_Led_Display_Table[NUM1PlaceIndex] & LED_SEG_G) != 0);
							
	gDispContentBufferbuf.DispBuffer.DispNUM2Place_A = ((Digit_Led_Display_Table[NUM2PlaceIndex] & LED_SEG_A) != 0);
	gDispContentBufferbuf.DispBuffer.DispNUM2Place_B = ((Digit_Led_Display_Table[NUM2PlaceIndex] & LED_SEG_B) != 0);
	gDispContentBufferbuf.DispBuffer.DispNUM2Place_C = ((Digit_Led_Display_Table[NUM2PlaceIndex] & LED_SEG_C) != 0);
	gDispContentBufferbuf.DispBuffer.DispNUM2Place_D = ((Digit_Led_Display_Table[NUM2PlaceIndex] & LED_SEG_D) != 0);
	gDispContentBufferbuf.DispBuffer.DispNUM2Place_E = ((Digit_Led_Display_Table[NUM2PlaceIndex] & LED_SEG_E) != 0);
	gDispContentBufferbuf.DispBuffer.DispNUM2Place_F = ((Digit_Led_Display_Table[NUM2PlaceIndex] & LED_SEG_F) != 0);
	gDispContentBufferbuf.DispBuffer.DispNUM2Place_G = ((Digit_Led_Display_Table[NUM2PlaceIndex] & LED_SEG_G) != 0);
    return;
}

这样例如Digit_Led_Display_Table[0]中的每一位都可以精准的赋值给我们一开始定义的变量

这还没完我们前面讲了,用这个联合体定义了两个同类型的变量

gDispContentBufferbuf用于主循环赋值

gDispContentBuffer用于1ms中断读取,不改写值

所以在主循环中我们要在给gDispContentBufferbuf赋值后把gDispContentBufferbuf拷贝到gDispContentBuffer中去,例如我让两个数码管亮48

void Display_Run(void)
{
	
	ScreemNumCompile(4, 8);
    DISABLE_INT;
	(void)memcpy(&gDispContentBuffer, &gDispContentBufferbuf, sizeof(gDispContentBufferbuf));
	ENABLE_INT;
	return ;
}

要注意的是在复制时要把中断禁掉,因为gDispContentBuffer是在1ms内用的,如果不禁中断会出现还没复制完就显示了

你可能有一个问题,为什么要定义两个变量,一个不行吗,

一个原因跟关中断一个道理,是我们在对这个联合体的变量赋值时,中断可能来了,如果定义一个势必要在主逻辑里面开关中断,而你的主逻辑可能很复杂,这样可能时间长不太好,

另一个原因是我们操作的是位,位域不是原子操作,原子操作简单理解就是比如我定义个变量

u8 a = 0;哪怕赋值这个时候来了一个中断,a也会先赋完值再去跳转,但位域不是,他不是一条指令,具体可以看汇编,(具体原因记不太清了)我印象中是,如果这个位域一位在主循环赋值他会先给一个寄存器赋值,这个时候中断来了就会跳转,而中断同一个位域下的另一位如果也在赋不同值会用同样的寄存器,那就会改变这个寄存器值,退出中断的时候,主循环的这个位域的那一位由于这个寄存器的值改变了,导致原本理想中的值发生了变化(好像是这样),感兴趣可以测一测,这个现象是程序一开始正常执行一段时间,然后主循环的位域那一位赋值就出问题,

所以位域尽量不要在主循环跟中断都有赋值操作,都有赋值操作还是用c的基本数据类型

当然了在这个程序里面中断只是去读,而且不断刷新,即便影响,也很小,不过还是建议资源够用就分层去处理

另一个要注意的就是在中断里面扫io口的函数DispScan();没必要1ms执行一次,比如两个com,5ms执行一次一个周期10ms,频率很高了

1ms中断调用

void Time1msISR(void)
{
	static u8 pre_20ms = 0;
	static u8 pre_50ms = 0;
	static u8 pre_100ms = 0;
	static u8 pre_500ms = 0;

	DispScan();

	++t_sys_1ms;

	++pre_20ms;
	if (pre_20ms >= 20)
	{
		pre_20ms = 0;
		++t_sys_20ms;
		++pre_100ms;
		if (pre_100ms >= 5)
		{
			pre_100ms = 0;
			++t_sys_100ms;
			
		}
	}

	++pre_50ms;
	if (pre_50ms >= 50)
	{
		pre_50ms = 0;
		++t_sys_50ms;
		++pre_500ms;
		if (pre_500ms >= 10)
		{
			pre_500ms = 0;
			++t_sys_500ms;
			
		}
	}


	return ;
}

作者:Lain_laserbird

物联沃分享整理
物联沃-IOTWORD物联网 » 使用MCU分层操作数码管和LED灯,利用Union联合体实现便捷修改

发表回复