数码之家

 找回密码
 立即注册
搜索
查看: 1057|回复: 1

[C51] 单片机入门杂谈(四):单片机普通I/O直接驱动LED数码管

[复制链接]
发表于 2024-3-1 23:01:54 | 显示全部楼层 |阅读模式

爱科技、爱创意、爱折腾、爱极致,我们都是技术控

您需要 登录 才可以下载或查看,没有账号?立即注册

x
大部分人学习单片机都从点亮LED开始,尤其是从MCS-51单片机开始学习单片机的。

学会点亮LED之后,如果需要显示复杂一点的内容,如数字、特定字符,可以选择使用7段LED数码管作为显示设备,为额外显示“.”、“:”等符号,LED数码管一般设计成8段。LED数码管价格便宜、驱动方法简单,多数单片机都能直接驱动,即使单片机自身驱动能力不足,也可以使用三极管、缓冲器增强驱动能力来驱动数码管。

数码管按公共端分为共阴极数码管和共阳数码管两种,按显示方式分为静态显示和动态显示两种,静态显示每个字符位使用独立的I/O驱动,动态显示若干个字符位有连在一起的驱动端和用于分时控制的独立驱动端,利用人眼视觉暂留分时显示,只有刷新率够高,若干个字符位看起来是同时显示一样。静态显示占用较多I/O,而且设计PCB时走线麻烦很多,所以,多字符位数码管常用动态显示。将若干个数码管封在一起做成一个多位数码管,段驱动极连在一起,引出驱动极(SEG)和公共端(COM),使用时PCB走线简单很多。

常用的4位数码管外形如下图:
4位数码管_1.jpg

不同数码管4个8的逻辑图一般是一样的,主要差别是“.”和“:”,数码管表面上有的“.”和“:”,可能里面并没有LED(无法点亮),以4位共阴数码管为例,实际逻辑图可能是下图中的一种。
4位共阴数码管_1.png

下面以4位共阳数码管为例简述一下单片机直接驱动数码管的方法(之所以用共阳数码管,是因为部分内容用Proteus仿真演示,Proteus库里只有共阳的的4位时钟点数码管)。

一、字符码表

说到数码管显示,一般人会告诉你共阴数码管用这个码表、共阳数码管用那个码表,这些码表大致是这样:

字符--共阴码--共阳码
0-----0x3F----0xC0
1-----0x06----0xF9
2-----0x5B----0xA4
3-----0x4F----0xB0
4-----0x66----0x99
5-----0x6D----0x92
6-----0x7D----0x82
7-----0x07----0xF8
8-----0x7F----0x80
9-----0x6F----0x90
A-----0x77----0x88
B-----0x7C----0x83
C-----0x39----0xC6
D-----0x5E----0xA1
E-----0x79----0x86
F-----0x71----0x8E


这些码表一般假设同一个8位端口控制数码管8个SEG且顺序固定,如:
SEG1----P1.0
SEG2----P1.1
SEG3----P1.2
SEG4----P1.3
SEG5----P1.4
SEG6----P1.5
SEG7----P1.6
SEG8----P1.7

这样的对应关系,显示程序比较简单,只需写“P1=**”即可输出SEG信号。

实际应用时,为了走线方便,可能需要调整SEG与I/O对应关系,如:
SEG1----P1.5
SEG2----P1.7
SEG3----P1.6
SEG4----P1.3
SEG5----P1.4
SEG6----P1.0
SEG7----P1.2
SEG8----P1.1

如果仍想简单地写“P1=**”输出SEG信号,码表需要修改,按直接立即数方式修改比较麻烦,可以用笔画码组合方式生成码表,只需要修改SEG1~SEG8的码表即可。

  1. #define SEG1            (0x01<<0)    //SEG1~SEG8显示驱动值
  2. #define SEG2            (0x01<<1)
  3. #define SEG3            (0x01<<2)
  4. #define SEG4            (0x01<<3)
  5. #define SEG5            (0x01<<4)
  6. #define SEG6            (0x01<<5)
  7. #define SEG7            (0x01<<6)
  8. #define SEG8            (0x01<<7)

  9. //无论共阴还是共阳,均按共阴显示生成码表
  10. #define CHAR_0          (SEG1|SEG2|SEG3|SEG4|SEG5|SEG6)
  11. #define CHAR_1          (SEG2|SEG3)
  12. #define CHAR_2          (SEG1|SEG2|SEG4|SEG5|SEG7)
  13. #define CHAR_3          (SEG1|SEG2|SEG3|SEG4|SEG7)
  14. #define CHAR_4          (SEG2|SEG3|SEG6|SEG7)
  15. #define CHAR_5          (SEG1|SEG3|SEG4|SEG6|SEG7)
  16. #define CHAR_6          (SEG1|SEG3|SEG4|SEG5|SEG6|SEG7)
  17. #define CHAR_7          (SEG1|SEG2|SEG3)
  18. #define CHAR_8          (SEG1|SEG2|SEG3|SEG4|SEG5|SEG6|SEG7)
  19. #define CHAR_9          (SEG1|SEG2|SEG3|SEG4|SEG6|SEG7)
  20. #define CHAR_A          (SEG1|SEG2|SEG3|SEG5|SEG6|SEG7)
  21. #define CHAR_B          (SEG3|SEG4|SEG5|SEG6|SEG7)
  22. #define CHAR_C          (SEG1|SEG4|SEG5|SEG6)
  23. #define CHAR_D          (SEG2|SEG3|SEG4|SEG5|SEG7)
  24. #define CHAR_E          (SEG1|SEG4|SEG5|SEG6|SEG7)
  25. #define CHAR_F          (SEG1|SEG5|SEG6|SEG7)
  26. #define CHAR_NULL       0x00
复制代码


二、显示程序

4位共阳数码管显示时序大致如下图,COM输出高电平时显示对应COM的字符,帧周期一般不超过(1/25Hz)即可,但为了减弱闪烁感,应尽量减短帧周期,比如减短到(1/100Hz),如果没有调节显示亮度需求,每个COM显示时间按帧周期均分即可。
共阳数码管显示时序.png

显示程序建议按“接口层+应用层”方式编写,接口层输出实际SEG、COM信号,应用层装载显示内容,两层程序各分配一个独立的显示缓冲区。接口层在一帧开始时查询应用层显示缓冲区数据是否有已更新,如果已更新,应用层缓冲区数据复制到接口层缓冲区,然后清除应用层数据已更新标志。应用层如果查询到应用层缓冲区未更新则更新应用层缓冲区,然后置应用层数据已更新标志。


显示流程大致如下图:

数码管显示流程图.png

显示接口层:
  1. void LED_Display_Output(void)
  2. {
  3.         INT8U   idata   *idata_byte_p;


  4.         COM_ALL_OFF();          //关显示
  5.         SEG_ALL_OFF();

  6.         if(LED_Display_COM_Count==0)    //COM计数
  7.         {
  8.                 if(Return_LED_Display_Data_Updated_Flag())       //查询显示数据已更新标志
  9.                 {
  10.                         idata_byte_p=Return_LED_Display_Data();  //更新显示

  11.                         LED_Display_Buffer[0]=*idata_byte_p;        //应用层缓冲区数据复制到接口层缓冲区

  12.                         idata_byte_p++;
  13.                         LED_Display_Buffer[1]=*idata_byte_p;

  14.                         idata_byte_p++;
  15.                         LED_Display_Buffer[2]=*idata_byte_p;

  16.                         idata_byte_p++;
  17.                         LED_Display_Buffer[3]=*idata_byte_p;

  18.                         Clear_LED_Display_Data_Updated_Flag();   //清除显示数据已更新标志
  19.                 }
  20.         }

  21.         ACC=LED_Display_Buffer[LED_Display_COM_Count];  //输出SEG信号

  22.         SEG_A=ACC&BIT0;
  23.         SEG_B=ACC&BIT0;
  24.         SEG_C=ACC&BIT0;
  25.         SEG_D=ACC&BIT0;
  26.         SEG_E=ACC&BIT0;
  27.         SEG_F=ACC&BIT0;
  28.         SEG_G=ACC&BIT0;
  29.         SEG_DP=ACC&BIT0;

  30.         switch(LED_Display_COM_Count)   //COM计数
  31.         {
  32.                 case 0: //COM1
  33.                 {
  34.                         COM_1_ON();     //输出COM信号

  35.                         LED_Display_COM_Count++;

  36.                         break;
  37.                 }
  38.                 case 1: //COM2
  39.                 {
  40.                         COM_2_ON();     //输出COM信号

  41.                         LED_Display_COM_Count++;

  42.                         break;
  43.                 }
  44.                 case 2: //COM3
  45.                 {
  46.                         COM_3_ON();     //输出COM信号

  47.                         LED_Display_COM_Count++;

  48.                         break;
  49.                 }
  50.                 case 3: //COM4
  51.                 {
  52.                         COM_4_ON();     //输出COM信号

  53.                         LED_Display_COM_Count=0;

  54.                         break;
  55.                 }
  56.         }
  57. }
复制代码
显示应用层:
  1. void User_Display_Function(void)
  2. {
  3.         if(System_500mS_Flag)        //500mS计时标志
  4.         {
  5.                 Colon_Display_Timer++;                //“:”闪烁显示计时
  6.         }

  7.         if(Display_Data_Updated_Flag==0)        //显示缓冲区数据未更新
  8.         {
  9.                 User_Display_Clear_All();       //清空显示
  10.                 User_Display_Digit();           //数码管显示
  11.                 User_Display_Char_Code();       //数码管显示字符转驱动码

  12.         #if(C_LED_DISPLAY_TYPE!=C_LED_DISPLAY_CATHODE)  //共阳数码管

  13.                 User_Display_Common_Anode();    //显示数据取反

  14.         #endif

  15.                 Display_Data_Updated_Flag=1;    //显示缓冲区数据已更新
  16.         }
  17. }
复制代码
接口层程序可以在主循环定时调用,也可以在定时器中断中调用,如果主循环有长时间占用CPU时间的程序要执行,需使用后一种方式以保证每个COM的显示时间差不多。

在主循环定时调用:

  1. void main(void)
  2. {
  3.         ……
  4.         while(1)
  5.         {
  6.                 if(System_1mS_Flag)     //1mS定时标志
  7.                 {
  8.                         System_1mS_Flag=0;

  9.                         LED_Display_Output();   //LED显示驱动
  10.                         ……
  11.                 }
  12.         }
  13. }
复制代码
在定时器中断中调用:
  1. void Timer2_ISR(void) interrupt VECTOR_Timer2
  2. {
  3.         LED_Display_Output();   //LED显示驱动

  4.         System_1mS_Flag=1;      //1mS标志

  5.         TF2=0;
  6. }
复制代码


三、显示内容装载实例


下图是一款电子手表显示时间的方法:显示“小时+分钟”5秒,“:”1Hz闪烁显示 --> “秒”显示5秒,“:”常亮显示 --> 显示“小时+分钟”5秒,“:”1Hz闪烁显示 --> ……
显示时间-2.gif

这种显示效果需要一个0.5秒定时器,这个时间长一点、短一点人眼无法区分,可以通过累积一个合适的时间段得到,如累积50段10mS得到,每0.5秒对一个计数器加1然后判断第0位是0还是1或取反一个标志位判断标志位是0还是1来控制“:”的亮和灭。显示内容切换可以直接根据时间秒控制,(秒%10)<5是显示“小时+分钟”,5≤(秒%10)≤9时显示 “秒”。

  1. //10mS调用一次,累积10mS得到0.5S
  2. void System_Timer_Function(void)
  3. {
  4.         System_50mS_Flag=0;     //清除时间标志
  5.         System_100mS_Flag=0;
  6.         System_500mS_Flag=0;
  7.         System_1S_Flag=0;

  8.         System_10mS_Count++;    //累积10mS * N
  9.         
  10.         if((System_10mS_Count%(50/10))==0)
  11.         {
  12.                 System_50mS_Flag=1;
  13.         }
  14.         
  15.         if((System_10mS_Count%(100/10))==0)
  16.         {
  17.                 System_100mS_Flag=1;
  18.         }
  19.         
  20.         if(System_10mS_Count%(500/10)==0)
  21.         {
  22.                 System_500mS_Flag=1;
  23.         }
  24.         
  25.         if(System_10mS_Count>=(1000/10))
  26.         {
  27.                 System_10mS_Count=0;
  28.                
  29.                 System_1S_Flag=1;
  30.         }
  31. }


  32. //显示时间
  33. void User_Display_Time(void)
  34. {
  35.         if((Calendar.Second%10)<5)      //10秒切换一次显示
  36.         {
  37.                 User_Display_Buffer.COM1=Calendar.Hour/10;
  38.                 User_Display_Buffer.COM2=Calendar.Hour%10;
  39.                 User_Display_Buffer.COM3=Calendar.Minute/10;
  40.                 User_Display_Buffer.COM4=Calendar.Minute%10;

  41.                 if((Colon_Display_Timer&BIT0)==0)       //:闪亮
  42.                 {
  43.                         Dot3_Flag=1;
  44.                         Dot4_Flag=1;
  45.                 }
  46.         }
  47.         else
  48.         {
  49.                 User_Display_Buffer.COM3=Calendar.Second/10;
  50.                 User_Display_Buffer.COM4=Calendar.Second%10;

  51.                 Dot3_Flag=1;    //:常亮
  52.                 Dot4_Flag=1;
  53.         }
  54. }


  55. //填充数码管显示字符
  56. void User_Display_Digit(void)
  57. {
  58.         User_Display_Time();            //显示时间
  59. }
复制代码
填充数码管要显示的字符后,如COM1=1、COM2=2、COM3=4、COM4=4,查表把字符码填充到显示缓冲区。

  1. void User_Display_Char_Code(void)
  2. {
  3.         User_Display_Buffer.COM1=Char_7_Segment_Table[User_Display_Buffer.COM1];        //数码管显示字符码查表
  4.         User_Display_Buffer.COM2=Char_7_Segment_Table[User_Display_Buffer.COM2];
  5.         User_Display_Buffer.COM3=Char_7_Segment_Table[User_Display_Buffer.COM3];
  6.         User_Display_Buffer.COM4=Char_7_Segment_Table[User_Display_Buffer.COM4];
  7.         
  8.         if(Dot1_Flag)   //COM1 .
  9.         {
  10.                 User_Display_Buffer.COM1|=SEG8;
  11.         }
  12.         if(Dot2_Flag)   //COM2 .
  13.         {
  14.                 User_Display_Buffer.COM2|=SEG8;
  15.         }
  16.         if(Dot3_Flag)   //COM3 .
  17.         {
  18.                 User_Display_Buffer.COM3|=SEG8;
  19.         }
  20.         if(Dot4_Flag)   //COM4 .
  21.         {
  22.                 User_Display_Buffer.COM4|=SEG8;
  23.         }
  24. }
复制代码
共阳显示时,为简化接口层程序,将显示缓冲区内的数据取反。
  1. void User_Display_Common_Anode(void)
  2. {
  3.         INT8U   i;
  4.         INT8U   idata   *idata_byte_p;
  5.         
  6.         idata_byte_p=(INT8U idata *)&User_Display_Buffer;
  7.         i=sizeof(User_Display_Buffer);
  8.         
  9.         do
  10.         {
  11.                 *idata_byte_p=(~(*idata_byte_p));
  12.                 idata_byte_p++;
  13.         }while(--i);
  14. }
复制代码
至此,数码管显示内容对应的I/O输出值填充完成。


Keil C51和Proteus仿真演示工程:
LED_Display_Demo.zip (42.93 KB, 下载次数: 0)

打赏

参与人数 1家元 +30 收起 理由
不长叶子的树 + 30

查看全部打赏

发表于 2024-3-2 11:03:59 | 显示全部楼层
谢谢楼主的详细教程,学习了!
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

APP|手机版|小黑屋|关于我们|联系我们|法律条款|技术知识分享平台

闽公网安备35020502000485号

闽ICP备2021002735号-2

GMT+8, 2024-4-27 15:52 , Processed in 0.187200 second(s), 14 queries , Redis On.

Powered by Discuz!

© 2006-2023 smzj.net

快速回复 返回顶部 返回列表