找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 3|回复: 0
打印 上一主题 下一主题
收起左侧

4个74HC595控制的点阵时钟-时间由51单片机芯片中断产生

[复制链接]
跳转到指定楼层
楼主
研究完数码管时钟,我就准备做一个自己设计的点阵时钟。搜索过淘宝,自己现阶段能模仿的只有点阵时钟,而且是51自己中断计时,不加时钟芯片的那种。见下图。淘宝售卖的散件才30多元钱(可能还ESP芯片控制的,更高级),我自己做,从做PCB设计,到打板,采购零件,焊接,还要做外壳,不是3D打印,就是激光切割亚克力板,估计还得要自己做外壳的机械设计,想想都头痛。钱不会少花,估计得百元,但这是自己成长的必经之路,不走一遍还不行。这一遍走通了,我才敢进行更复杂的设计,并实践出来。

下面是点阵时钟的显示部分程序,按键修改时间部分,可以参考文章:解决了初学者常遇到的糟心问题,51单片机proteus仿真数码管点阵的动态显示乱码故障 - 单片机学习教程(初学者入门)

为了便于阅读,我让AI尽量帮我把每一句都写上了注释。关于AI编程,我的体会是可以让它给你思路,暂时别指望它能编出完美运行的程序,人脑和电脑目前还不能完美统一。反正AI给我出的程序是漏洞百出,冗余颇多,越改越烂,而且它还一本正经给你瞎解释。最终还是按我自己的思路与构思框架,将程序修改完善。像取个字模啥的烂事交给AI干还是挺让人舒心的。

/*
* 24小时点阵时钟程序
* 使用三个8*8点阵(24列)作为一个整体显示时间:HH:MM
* 布局:[间隔1列][小时十位4列][间隔1列][小时个位4列][间隔1列][冒号2列][间隔1列][分钟十位4列][间隔1列][分钟个位4列][间隔1列]
* 冒号每秒闪烁一次(亮1秒、灭1秒),指示秒的走动
*
* 硬件配置:
* - 行驱动:1个74HC595芯片,输出8位行选通信号
* - 列驱动:3个74HC595芯片,分别控制三个点阵的8位列数据
* - 级联方式:4片595串行级联,共32位数据一次移入后由RCLK同时锁存输出
*
* 列数据位序(按实际硬件接线):
* 每个595的最高位输出Q7(bit7)接该组第1列(最左),最低位Q0(bit0)接第8列(最右):
* - 小时595:bit7->第1列  ... bit0->第8列
* - 中间595:bit7->第9列  ... bit0->第16列
* - 分钟595:bit7->第17列 ... bit0->第24列
* 因此列字节内高位在左、低位在右(正序),字模也按"高位在左"定义
*
* 引脚定义:
* P3.0 -> SER   (串行数据输入)
* P3.1 -> SRCLK (移位寄存器时钟)
* P3.2 -> RCLK  (存储寄存器锁存时钟)
*
* 硬件极性:
* - 行控制:低电平有效(0=点亮)
* - 列控制:高电平有效(1=点亮)
*
* 显示布局(第1列为最左列,按每组字节内的位号标注):
* col0(小时595):bit7=第1列(间隔) bit6~3=第2~5列(小时十位) bit2=第6列(间隔) bit1~0=第7~8列(小时个位左2列)
* col1(中间595):bit7~6=第9~10列(小时个位右2列) bit5=第11列(间隔) bit4~3=第12~13列(冒号) bit2=第14列(间隔) bit1~0=第15~16列(分钟十位左2列)
* col2(分钟595):bit7~6=第17~18列(分钟十位右2列) bit5=第19列(间隔) bit4~1=第20~23列(分钟个位) bit0=第24列(间隔)
*
* 时序设计(11.0592MHz晶振,12分频后1个机器周期约1.085us):
* - 定时器0:约5ms中断一次,逐行刷新点阵;
*  - 中断内只做一次32位移位(约0.5ms内完成)
*   保证主循环和秒计时有CPU时间可用(若中断耗时超过周期,主循环将被
*   完全饿死,表现为时间不走、冒号不闪)
* -
*/

// 引入51单片机寄存器定义头文件

  1. #include <reg51.h>


  2. // 定义74HC595芯片的串行数据引脚,接P3.0
  3. sbit SER = P3^0;
  4. // 定义74HC595芯片的移位寄存器时钟引脚,接P3.1
  5. sbit SRCLK = P3^1;
  6. // 定义74HC595芯片的存储寄存器锁存时钟引脚,接P3.2
  7. sbit RCLK = P3^2;

  8. // 显示缓冲区,8行×3字节(24列),每行对应24个LED列数据
  9. // 每个字节内高位在左(bit7=该组第1列),与硬件接线一致
  10. // display_buf[i][0] = 小时595:间隔 + h1(4列) + 间隔 + h2左2列
  11. // display_buf[i][1] = 中间595:h2右2列 + 间隔 + 冒号 + 间隔 + m1左2列
  12. // display_buf[i][2] = 分钟595:m1右2列 + 间隔 + m2(4列) + 间隔
  13. // 缓冲区内1=点亮,与列硬件极性(高电平点亮)一致,输出时无需取反
  14. unsigned char display_buf[8][3] = {0};
  15. // 当前扫描的行号,0-7循环
  16. unsigned char current_row = 0;

  17. // 当前小时数,范围0-23
  18. unsigned char hours = 12;
  19. // 当前分钟数,范围0-59
  20. unsigned char minutes = 0;
  21. // 当前秒数,范围0-59
  22. unsigned char seconds = 0;
  23. // 毫秒计数器,由定时器1每约1ms加1,累计到1000时秒数加1
  24. unsigned int ms_count = 0;
  25. // 定义秒标志位
  26. unsigned char secflag=0;
  27. // 冒号闪烁标志:秒数为偶数时置1(冒号亮),奇数时置0(冒号灭)
  28. // 由定时器0中断在每次秒变化时更新;主循环检测到秒变化后重绘显示,实现每秒闪烁
  29. bit colon_blink = 1;

  30. // 定义T0定时器高8位,低8位重载数值变量
  31. unsigned char T0RH,T0RL;

  32. // 4列宽数字字模表,digits_4col[数字][行号]
  33. // 字模为正序(高位在左):bit3=数字最左列(第1列),bit0=数字最右列(第4列),这样设置硬件及取出的字模让人感觉清晰,后面的逻辑转换也更容易懂
  34. // 与硬件"bit7=最左列"的位序一致,1=点亮,0=熄灭
  35. // 每个数字占满8行(统一高度)
  36. // 加code关键字:字模表存放在程序存储器ROM中,不占用宝贵的内部RAM
  37. unsigned char code digits_4col[10][8] = {
  38.     {0x06, 0x09, 0x09, 0x09, 0x09, 0x09, 0x09, 0x06},  // 0
  39.     {0x04, 0x0C, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x04},  // 1
  40.     {0x06, 0x09, 0x01, 0x02, 0x04, 0x08, 0x09, 0x06},  // 2
  41.     {0x06, 0x01, 0x01, 0x07, 0x01, 0x01, 0x01, 0x06},  // 3
  42.     {0x09, 0x09, 0x09, 0x0F, 0x01, 0x01, 0x01, 0x01},  // 4
  43.     {0x0F, 0x08, 0x08, 0x0F, 0x01, 0x01, 0x01, 0x0F},  // 5
  44.     {0x06, 0x08, 0x08, 0x07, 0x09, 0x09, 0x09, 0x06},  // 6
  45.     {0x0F, 0x01, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08},  // 7
  46.     {0x06, 0x09, 0x09, 0x06, 0x09, 0x09, 0x09, 0x06},  // 8
  47.     {0x06, 0x09, 0x09, 0x0F, 0x01, 0x01, 0x01, 0x06}   // 9
  48. };

  49. // 向74HC595芯片发送一个字节的数据
  50. // 参数dat:要发送的8位数据(高位在前)
  51. // 说明:使用固定掩码循环代替"变量位数移位",每位移位开销小、耗时确定,
  52. //       保证显示扫描中断能在一个扫描周期内完成,不阻塞秒计时和主循环
  53. void shift_out_byte(unsigned char dat)
  54. {
  55.     unsigned char mask;
  56.     // 从最高位(bit7)到最低位(bit0)依次发送
  57.     for(mask = 0x80; mask != 0; mask >>= 1)
  58.     {
  59.         // 将当前位写入SER引脚(掩码对应位为1则SER=1,否则SER=0)
  60.         SER = (dat & mask) ? 1 : 0;
  61.         // SRCLK产生上升沿,将SER上的数据移入移位寄存器
  62.         SRCLK = 0;      // 拉低时钟,建立数据
  63.         SRCLK = 1;      // 拉高时钟,上升沿移入数据
  64.          }
  65. }

  66. // 向4个级联的74HC595芯片发送32位数据(1字节行+3字节列)
  67. // 参数row_data:行选通数据(8位)
  68. // 参数col0:小时595列数据(第1~8列),间隔+h1+间隔+h2左2列
  69. // 参数col1:中间595列数据(第9~16列),h2右2列+间隔+冒号+间隔+m1左2列
  70. // 参数col2:分钟595列数据(第17~24列),m1右2列+间隔+m2+间隔
  71. // 发送顺序:row_data -> col2 -> col1 -> col0
  72. // 先发送的数据经过整条级联链,最终row_data到达最远端的行595,
  73. // col2/col1/col0分别落在分钟/中间/小时点阵对应的列595
  74. // 全部32位移完后RCLK统一锁存,行与列同时更新输出,所以就不需要消隐了。
  75. void shift_out_32bit(unsigned char row_data, unsigned char col0, unsigned char col1, unsigned char col2)
  76. {
  77.     // 1. 发送行数据(移到级联链最远端的行驱动595)
  78.     shift_out_byte(row_data);
  79.     // 2. 发送分钟点阵列数据(col2)
  80.     shift_out_byte(col2);
  81.     // 3. 发送中间点阵列数据(col1,含冒号和小时个位高2位)
  82.     shift_out_byte(col1);
  83.     // 4. 发送小时点阵列数据(col0,最后发送,落在最靠近MCU的列595)
  84.     shift_out_byte(col0);
  85.     // RCLK产生上升沿,将移位寄存器中的32位数据同时锁存到输出端
  86.     RCLK = 0;       // 拉低锁存时钟
  87.     RCLK = 1;       // 拉高时钟,数据输出到引脚
  88. }

  89. // 初始化函数:配置定时器和中断
  90. void configtime(unsigned char ms)
  91. {
  92.         unsigned long tmp;
  93.         tmp=ms*11059200/12/1000;
  94.     //延时补偿,我根据经验加了15,当然也不一定是15,根据你的检测平均而来。
  95.         tmp=65536-tmp+15;
  96.         T0RH=(unsigned char)(tmp>>8);
  97.         T0RL=(unsigned char)tmp;
  98.         EA=1;
  99.         TMOD=0X01;
  100.         TH0=T0RH;
  101.         TL0=T0RL;
  102.         ET0=1;
  103.         TR0=1;
  104. }



  105. // 定时器0中断服务函数,用于点阵动态扫描刷新
  106. // 每约5ms触发一次,依次点亮每行LED
  107. // 说明:4片595的输出由RCLK同时锁存,行与列在同一锁存边沿一起切换,
  108. // 不存在"新行数据遇上旧列数据"的中间状态,因此不需要消隐步骤,
  109. // 每次中断只移位一次32位,中断耗时约0.5ms,远小于扫描周期,
  110. // 主循环和秒计时都能分到CPU时间
  111. void display_refresh() interrupt 1
  112. {
  113.     // 重置定时器0初值,下次中断间隔约5ms
  114.     TH0 =T0RH;  // 高8位
  115.     TL0 = T0RL;  // 低8位

  116.     // ========== 显示当前行 ==========
  117.     // ~(0x01 << current_row):当前行位为0(行低有效,0=点亮该行)
  118.     // display_buf[current_row][x]:列数据直接输出(列高有效,1=点亮)
  119.     shift_out_32bit(~(0x01 << current_row),
  120.                     display_buf[current_row][0],
  121.                     display_buf[current_row][1],
  122.                     display_buf[current_row][2]);

  123.     // 行号递增,循环扫描0-7行
  124.     current_row++;
  125.     if(current_row >= 8)
  126.         current_row = 0;

  127. // 毫秒计数器递增
  128.     ms_count++;
  129.     // 累计1000ms(1秒)
  130.     if(ms_count >= 200)
  131.     {
  132.         // 重置毫秒计数器
  133.         ms_count = 0;
  134.         secflag=1;
  135.    }

  136. }


  137. // 更新显示缓冲区函数:将当前时间转换为24列LED点阵数据
  138. // 列字节内高位在左(bit7=该组第1列),字模正序(bit3=数字最左列)
  139. void update_display()
  140. {
  141.     unsigned char i;                    // 行索引
  142.     unsigned char h1 = hours / 10;      // 小时十位(0-2)
  143.     unsigned char h2 = hours % 10;      // 小时个位(0-9)
  144.     unsigned char m1 = minutes / 10;    // 分钟十位(0-5)
  145.     unsigned char m2 = minutes % 10;    // 分钟个位(0-9)
  146.     unsigned char temp[3];              // 临时缓冲区,构建当前行的24列数据
  147.     unsigned char d;                    // 当前数字的行数据


  148.     // 逐行构建显示数据(共8行)
  149.     for(i = 0; i < 8; i++)
  150.     {
  151.         // 初始化临时缓冲区为全0
  152.         temp[0] = 0x00;
  153.         temp[1] = 0x00;
  154.         temp[2] = 0x00;

  155.         // ========== 小时十位(h1):第2~5列 -> col0的bit6~3 ==========
  156.         // 字模bit3(最左列)对应col0的bit6,即左移3位:
  157.         // bit3->bit6(第2列), bit2->bit5(第3列), bit1->bit4(第4列), bit0->bit3(第5列)
  158.         d = digits_4col[h1][i] & 0x0F;
  159.         temp[0] |= (d << 3);

  160.         // ========== 小时个位(h2):第7~10列,跨col0和col1两个字节 ==========
  161.         // 左2列(第7~8列):字模bit3->col0的bit1(第7列),bit2->bit0(第8列),即右移2位
  162.         // 右2列(第9~10列):字模bit1->col1的bit7(第9列),bit0->bit6(第10列),即左移6位
  163.         d = digits_4col[h2][i] & 0x0F;
  164.         temp[0] |= (d >> 2);
  165.         temp[1] |= (d << 6);

  166.         // ========== 冒号:第12~13列 -> col1的bit4~3 ==========
  167.         // 只在第2、3、5、6行点亮(形成上下两个圆点)
  168.         // colon_blink为1时显示,实现每秒闪烁
  169.         // 0x18 = 0b00011000,对应col1的bit4(第12列)和bit3(第13列)
  170.         if(colon_blink && (i == 2 || i == 3 || i == 5 || i == 6))
  171.         {
  172.             temp[1] |= 0x18;
  173.         }

  174.         // ========== 分钟十位(m1):第15~18列,跨col1和col2两个字节 ==========
  175.         // 左2列(第15~16列):字模bit3->col1的bit1(第15列),bit2->bit0(第16列),即右移2位
  176.         // 右2列(第17~18列):字模bit1->col2的bit7(第17列),bit0->bit6(第18列),即左移6位
  177.         d = digits_4col[m1][i] & 0x0F;
  178.         temp[1] |= (d >> 2);
  179.         temp[2] |= (d << 6);

  180.         // ========== 分钟个位(m2):第20~23列 -> col2的bit4~1 ==========
  181.         // 字模bit3(最左列)对应col2的bit4,即左移1位:
  182.         // bit3->bit4(第20列), bit2->bit3(第21列), bit1->bit2(第22列), bit0->bit1(第23列)
  183.         d = digits_4col[m2][i] & 0x0F;
  184.         temp[2] |= (d << 1);

  185.         // 将构建好的当前行数据写入显示缓冲区
  186.         display_buf[i][0] = temp[0];
  187.         display_buf[i][1] = temp[1];
  188.         display_buf[i][2] = temp[2];
  189.     }

  190. }


  191. void time()
  192. {
  193.   if (secflag==1)
  194.    {
  195.        secflag=0;
  196.         // 冒号闪烁控制:秒数为偶数时亮,奇数时灭(每秒切换一次)
  197.         colon_blink = (seconds % 2 == 0);
  198.         // 秒数加1
  199.       update_display();
  200.         seconds++;
  201.         // 秒数达到60,进位到分钟
  202.         if(seconds >= 60)
  203.         {
  204.             seconds = 0;
  205.                  update_display();  // 可能不需要,第一个update_display()就把时分秒显示全干了。
  206.             minutes++;
  207.             // 分钟达到60,进位到小时
  208.             if(minutes >= 60)
  209.             {
  210.                 minutes = 0;
  211.                 update_display(); //  也许用不到,第一个update_display()就把时分秒的事办了。
  212.                 hours++;
  213.                 // 小时达到24,归零(24小时制)
  214.                 if(hours >= 24)
  215.                     hours = 0;
  216.                 }

  217.         }
  218.         }
  219. }



  220. // 主函数
  221. void main()
  222. {

  223.     // 初始化定时器和中断
  224.        configtime(5);

  225.     // 首次更新显示缓冲区(初始时间12:00,秒数为偶数,冒号点亮)
  226.     // 主循环:持续检测时间变化并更新显示
  227.     while(1)
  228.     {

  229.          time();
  230.     }
  231. }
复制代码

sav1.jpg (1.08 MB, 下载次数: 0)

sav1.jpg

matrixclock.jpg (520.08 KB, 下载次数: 0)

proteus9原理图

proteus9原理图

sav2.jpg (717.97 KB, 下载次数: 0)

sav2.jpg

matrixclock3.zip

33.43 KB, 下载次数: 0, 下载积分: 黑币 -5

原理图与程序

分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 分享淘帖 顶 踩
回复

使用道具 举报

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

本版积分规则

小黑屋|51黑电子论坛 |51黑电子论坛6群 QQ 管理员QQ:125739409;技术交流QQ群281945664

Powered by 单片机教程网

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