使用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