查看: 1566|回复: 0

【保姆级教程】不到一元32位芯片 DIY迷你时钟

[复制链接]

21

主题

227

帖子

1621

积分

版主

Rank: 7Rank: 7Rank: 7

积分
1621
发表于 2023-5-12 18:02:25 | 显示全部楼层 |阅读模式
本帖最后由 hcm0915 于 2023-5-15 08:35 编辑

前言
       对于大多数大学生和初学开发者来说,单片机的学习是晦涩难懂的,但是运用制作项目的方式来学习,就会显得更加趣味易学。现基于CH32V003并以项目流程制作方式讲解如何制作一个迷你的多功能时钟。

一、 项目介绍
1.1 设计初衷
      时间的流易和不可逆性是一个古今中外一再提到的内容,中国古代的计时仪器有太阳钟和机械钟两类。太阳钟是以太阳的投影和方位来计时,分别以土圭、圭表、日晷为代表。由于地球轨道偏心率以及地球倾角的影响,真太阳时和平太阳时是不一致的,机械钟应运而生,代表有水钟、香篆钟、太阳仪。
      电子爱好者喜欢DIY各种创意时钟,主要有辉光管时钟、点阵屏时钟、液晶屏时钟这几种形态,基本功能包括计时、闹钟,还有些可以通过网络自动校时,天气预报等。
image.png image.png image.png image.png
       我们这次准备设计一个超迷你的时钟,在一个0.56英寸的数码管的基础上实现基本计时、闹钟功能,整体硬件成本控制在5元以内。
image.png
       时钟项目很适合嵌入式初学者的第一个综合作品,综合运用了数码管驱动显示技术,RTC时钟模块的I2C驱动,蜂鸣器的PWM驱动,按键的长短按处理,定时器的多任务时序控制。

1.2 演示视频
项目全部源文件开源在Gitee
https://gitee.com/haohaodada-official/mini_clock
二、 功能设计
1.1 功能需求
  • 计时功能:能显示年、月、日、时、分、秒。
  • 闹钟功能:设定好几点几分后,到了设定时间会播放闹铃。
  • 供电要求:内置锂电池,可独立工作3天以上,也可以通过USB线外接供电和充电。
  • 成本要求:BOM价格控制在5元以内。
  • 尺寸要求:长宽和0.56英寸的数码管基本相同,厚度在满足锂电池容量要求下。

1.2 功能分析
      我们选用的数码管只有4位,要显示这么多内容,就需要轮流来显示,我们定义第一页显示年,效果如下图所示:
image.png
      第二页显示月和日,效果如下图所示:
image.png
      第三页显示时和分,同时用冒号每隔1秒闪烁来表示秒,效果如下图所示:
image.png
image.png
      现在我们可以让数码管轮流显示上述几个页面,就可以实现计时功能。
接下来,我们要能校准时间,需要通过按键来实现,这里我们用一个按键来实现,通过按键的短按和长按来实现。
我们定义短按来修改具体的数值大小,每短按一次,数值加1,超过范围后,返回到最小值。比如设置小时,超过23后要回到0开始,设置分钟,超过59后要回到0开始,其它类似处理。
      我们定义长按来切换不同的页面,同时也是一个确认操作。另外我们还有一个闹钟功能,闹钟也需要设置,闹钟只是设置小时和分,设置的界面和小时分钟界面一样,这样容易让用户不容易察觉,所以我们可以在切换到闹钟界面时,用蜂鸣器发出声音来提示下,表示设置闹钟成功。
      当每天时间到了设定的闹钟时间时,蜂鸣器发出闹铃声,我们可以通过短按按键来关闭闹铃。
       电源部分需要增加锂电池电源管理来解决,同时要考虑整个系统的功耗。
三、 硬件设计
1.1 电源管理模块
      这里选用TP4056作为锂电池充放电管理芯片,该芯片支持单节锂离子电池恒流/恒压线性充电,充电电流可以设置,最大支持1000mA,带有电池温度监测,欠压锁定,自动再充电和两个状态引脚以显示充电和充电终止功能。
image.png
      这里,我们把TEMP引脚接地,不使用温度监测功能。我们把R13电阻设置为1.2K,这样就设置充电电流大小为1000mA,内部计算公式如下:
image.png
      设置红色LED为充电状态指示灯,蓝色LED为充满状态指示灯,分别连接到CHRGSTDBY引脚,充电状态表如下图所示:
image.png
      整个系统,在锂电池供电时,系统电压就是锂电池的电压(3~4.2V),在USB插入充电时,系统电压稳定在4.2V。

1.2 单片机最小系统
      CH32V003系统工作电压范围为2.7~5.5V,通过数据手册可以看到CH32V003复位引脚有别于常见的单片机,需要外接上拉电阻,芯片已经内置上拉电阻,我们只需要外接一个0.1uF电容到地就可以,这里我们也不需要按键来复位。
image.png
      所以CH32V003只需要给VSSVDD引脚供电就可以工作了,当然为了方便程序下载调试,我们还需要引出SWIO口,芯片内核自带一个串行单线调试的接口,SWIO 引脚(Single Wire Input Output)。系统上电或复位后默认调试接口引脚功能开启。SWIO口也可以通过程序配置设置通用IO口。
image.png
image.png image.png
      程序出了可以用SWIO口连接专用下载器来下载以外,我们也可以通串口的ISP功能来更新程序,天问五幺出厂的CH32V003已经内置了ISP Bootloader在里面,可以用通用的USB转串口模块来下载程序,这里我们选用Type-C接口来作为充电和程序下载接口。
1.3 显示模块
      数码管的每个8字的一小段,内部实际就是一个LED,用A/B/C/D/E/F/G 7来标示对应的段,外加用DP来标示一个小数点或者冒号,设置对应的段亮,就可以组成0到F 十六个字符,根据公共端是共阴还是共阳,分为共阴数码管和共阳数码管两种。
image.png image.png
image.png
      四位数码管就有四个公共端,我们一般称作位码,当我设置好需要显示的段码后,选通对应的位码,就可以在对应的位上显示相应的字符。
image.png
      但是如果我们要显示上图所示的123,要怎么操作呢?我们可以先设置段码显示字符3,同时选通第4位,然后设置段码显示字符2,同时选通第2位,最后设置段码显示字符1,同时选通第2位。这样轮流显示,只要速度够快,人的眼睛还没反应过来,就已经显示下一位了,这是利用了人眼的视觉暂留现象,这种显示技术,我们叫做动态扫描。
显示部分的整体电路如下图所示:
image.png
      我们选用的数码管是12脚的0.56英寸的共阴数码管,为了降低成本和做到最小体积,不用TM1650这类的数码管专用显示芯片,采用单片机引脚直驱模式,用PC端口连接段码,方便后续程序处理。因为我们选用的是带中间冒号的类型,所以DP脚控制的是中间冒号的亮灭,数码管的亮度可以通过串联的电阻来控制,我们这里综合考虑亮度和功耗后选用1K。因为IO口驱动电流比较小,四个公共端通过NPN三极管来控制通断。

1.4 时钟模块
     CH32V003内部没有RTC模块,我们需要外接一个时钟模块,这里我们选用PCF8563时钟芯片,基于32.768kHz计时,可以提供年、月、日、时、分、秒、星期数据,通过I2C总线通讯,支持报警时间设置,工作电压范围为1.0~5.5V。
image.png
      两个I2C总线引脚需要上拉电阻到电源,掉电备用电池选用CR1220纽扣电池,D3二极管为防止VCC对电池充电,CR1220为非可充电电池;D4二极管为防止电池对VCC电路放电,快速消耗电池电量。备用电池可以不用,即把锂电池当作备用电池来使用。
1.5 按键模块
      按键部分这里因为电路板空间问题,没加上拉电阻,到时通过设置IO口上拉输入,采用芯片内部上拉,按键按下时电平为低,松开时电平为高。
image.png
1.6 蜂鸣器模块
      采用无源蜂鸣器,后续程序里可以通过PWM驱动,发出特定声音,蜂鸣器驱动电流比较大,我们通过三极管来驱动,电阻R14为限流电阻,防止流过基极电流过大损坏三极管。电阻R15有着重要的作用,第一个作用:R15 相当于基极的下拉电阻。如果A端被悬空则由于R15的存在能够使三极管保持在可靠的关断状态,如果删除R15则当BEEP输入端悬空时则易受到干扰而可能导致三极管状态发生意外翻转或进入不期望的放大状态,造成蜂鸣器意外发声。第二个作用:R15可提升高电平的门槛电压。如果删除R15,则三极管的高电平门槛电压就只有0.7V,即A端输入电压只要超过0.7V 就有可能导通,添加R15的情况就不同了,当从A端输入电压达到约2.2V 时三极管才会饱和导通。
image.png
         有源蜂鸣器和无源蜂鸣器的驱动电路区别主要在于无源蜂鸣器本质上是一个感性元件, 其电流不能瞬变,因此必须有一个续流二极管提供续流。否则,在蜂鸣器两端会有反向感应电动势,产生几十伏的尖峰电压,可能损坏驱动三极管,并干扰整个电路系统的其它部分。而如果电路中工作电压较大,要使用耐压值较大的二极管,而如果电路工作频率高,则要选用高速的二极管。这里选择的是 IN4148 的开关二极管。
四、 软件设计
      整体程序框架如下图的程序流程图所示,主要包含按键处理模块,设置显示模式处理模块、日常显示模式处理模块、闹钟处理模块。
image.png
1.1 数码管的动态扫描显示
      数码管动态扫描原理,我们在硬件设计章节里已经分析过,我们程序上先设置好需要显示数字的段码,这里还加了一个NONE类型,就是什么都不显示的黑屏状态,这个后续会用到。
image.png
  1. 0    1    2    3    4    5    6    7    8    9    A    B    C    D    E    F    NONE
  2. const uint16_t seg_code[17]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,0x7d,0x07,0x7f,0x6f,0x77,0x7c,0x39,0x5e,0x79,0x71,0x00};
复制代码

      同时设一个四字节的显示缓存,分别对应数码管的四个8字,后续我们只需要往显示缓存里送数据,数码管就能显示对应的数字,这里我们初始化时,让他默认显示四个8字作为开机动画,可以作为检测数码管或者电路是否有问题的一个自检程序。
image.png
  1. uint8_t disp_buff[4]={seg_code[8],seg_code[8],seg_code[8],seg_code[8]};
复制代码

        接下来,我们让数码管的4个位选轮流导通,每位导通时,把显示缓存对应位的段码点亮,对应的操作为直接给PC端口的输出寄存器赋值就可以了,这里我们在每次操作之前先清零,这样能消除残影,程序结构上采用了switch case结构,用curindex变量来切换状态。
image.png
  1. void Seg_Disp(){
  2.   switch (curindex) {
  3.    case 0:
  4.     GPIOC->OUTDR=0;
  5.     digitalWrite(PD0, 1);
  6.     digitalWrite(PD1, 0);
  7.     digitalWrite(PD2, 0);
  8.     digitalWrite(PD3, 0);
  9.     GPIOC->OUTDR=disp_buff[0];
  10.     curindex = 1;
  11.     break;
  12.    case 1:
  13.     GPIOC->OUTDR=0;
  14.     digitalWrite(PD0, 0);
  15.     digitalWrite(PD1, 1);
  16.     digitalWrite(PD2, 0);
  17.     digitalWrite(PD3, 0);
  18.     GPIOC->OUTDR=disp_buff[1];
  19.     curindex = 2;
  20.     break;
  21.    case 2:
  22.     GPIOC->OUTDR=0;
  23.     digitalWrite(PD0, 0);
  24.     digitalWrite(PD1, 0);
  25.     digitalWrite(PD2, 1);
  26.     digitalWrite(PD3, 0);   
  27.     GPIOC->OUTDR=disp_buff[2];
  28.     curindex = 3;
  29.     break;
  30.    case 3:
  31.     GPIOC->OUTDR=0;
  32.     digitalWrite(PD0, 0);
  33.     digitalWrite(PD1, 0);
  34.     digitalWrite(PD2, 0);
  35.     digitalWrite(PD3, 1);
  36.     GPIOC->OUTDR=disp_buff[3];
  37.     curindex = 0;
  38.     break;
  39.    default:
  40.     break;
  41.   }
  42. }
复制代码

        显示的扫描程序,我们要让它定时执行,就能完成数码管的正常显示,这里我们配置定时器2为1毫秒的定时周期。
image.png
  1. TIM_attachInterrupt(TIM2, 1000, TIM_attachInterrupt_2);
复制代码

image.png
  1. void TIM_attachInterrupt_2() {
  2.     Seg_Disp();
  3. }
复制代码

        这样我们完成了显示部分底层驱动,后续我们只需要往显示缓存送数据,数码管就显示对应的数据。
1.2 RTC时钟模块的设置和读取
        PCF8563时钟芯片采用I2C通讯,I2C地址读地址为0xA3,写地址为0xA2。通过芯片手册,我们可以看到如下寄存器说明:
image.png
        其中表4为控制相关设置寄存器,表5为时间数据寄存器,其中存储格式为BCD码,所以我们在操作时需要做相应的格式转换处理。
        这里我们把RTC部分的驱动代码做成了一个专门的类库,里面具体实现可以在源代码里查看,现在我们只需要关心如下几个函数:
        我们通过PCF8563类申明一个pcf8563对象,设置PD7SDA引脚,PD4SCL引脚。
image.png
  1. PCF8563 pcf8563(PD7,PD4);
复制代码

        通过setDateTime()函数来设置年、月、日、星期、时、分、秒。
image.png
  1. void setDateTime(uint16_t year, uint8_t month, uint8_t day, uint8_t weekday,
  2.             uint8_t hour,uint8_t minute, uint8_t sec);
复制代码

        通过getDateTime()函数一次性获取年、月、日、星期、时、分、秒数据到内部缓存里,
image.png
  1. void getDateTime();
复制代码

        再通过对应的函数来获取对应时间数据。
image.png
  1. uint8_t getMinute(); //获取分钟
  2. uint8_t getHour(); //获取小时
  3. uint8_t getDay();  //获取日期
  4. uint8_t getMonth(); //获取月
  5. uint16_t getYear(); //获取年
复制代码

        有关闹钟的函数,通过setAlarm()来设置时间,如果输入参数为99,则无效。
image.png
  1. void setAlarm(uint8_t min, uint8_t hour, uint8_t day, uint8_t weekday);
复制代码

        通过alarmActive()来查询闹钟时间是否到达。
image.png
  1. bool alarmActive();   // true if alarm is active (going off)
复制代码

         判断到闹钟时间到了后,通过clearAlarm()来清除报警状态。
image.png
  1. void clearAlarm(); /* clear alarm flag and interrupt */
复制代码


1.3 按键的长短按扫描程序
        我们前面学过用延迟的办法来做按键消抖处理,但是在复杂的系统里,程序里的延迟函数会阻塞程序运行,如果在这个过程中有其他需要及时处理的事件,实时性上就有问题。所以我们在复杂系统里,按键消抖常用定时器来处理,同时还可以做到长按、短按等功能。整个按键扫描程序流程图如下:
image.png
        前面我们的Seg_Disp()数码管扫描函数放在了周期为1ms的定时器2的回调函数里,而我们的按键扫描函数需要10ms周期,这种情况下,我们一般不会再去单独设置一个10ms的定时器来处理,因为单片机中的定时器资源十分有限且宝贵。我们可以通过程序来让一个定时器产生一个时间基准,这里我们用一个my_1ms变量来作为时间计数器,然后通过取余运算就可以实现自定义周期执行指定任务。
image.png
  1. void TIM_attachInterrupt_2()
  2. {
  3.   my_1ms++;
  4.   if(my_1ms % 10 == 0) //每10ms运行一次
  5. {
  6.     Key_Scan();
  7.   }
  8. Seg_Disp();
  9. }
复制代码

        根据需求定义短按时间范围为10ms到1000ms之间, 长按时间为大于1000ms。按键扫描周期10ms,刚好跳过抖动;使用状态机方式,扫描单个按键,状态机使用switch case语句实现状态之间的跳转,lock变量用于判断是否是第一次进行按键确认状态,长按键事件到时执行,短按键事件释放后才执行。主要代码如下:
  1. void Key_Scan(void)
  2. {
  3.     static uint8_t TimeCnt = 0;
  4.     static uint8_t lock = 0;
  5.     switch (KeyState)
  6.     {
  7.       //按键未按下状态,此时判断Key的值
  8.       case   KEY_CHECK:   
  9.           if(!Key)   
  10.           {
  11.               KeyState =  KEY_COMFIRM;  //如果按键Key值为0,说明按键开始按下,进入下一个状态
  12.           }
  13.           TimeCnt = 0;                  //计数复位
  14.           lock = 0;
  15.           break;   
  16.       case   KEY_COMFIRM:
  17.           if(!Key)                     //查看当前Key是否还是0,再次确认是否按下
  18.           {
  19.               if(!lock)   lock = 1;

  20.               TimeCnt++;  

  21.               /*按键时长判断*/
  22.               if(TimeCnt > 100)            // 长按 1 s
  23.               {
  24.                   g_KeyActionFlag = LONG_KEY;
  25.                   TimeCnt = 0;  
  26.                   lock = 0;               //重新检查
  27.                   KeyState =  KEY_RELEASE;    // 需要进入按键释放状态
  28.               }                                   
  29.           }   
  30.           else                       
  31.           {
  32.               if(1==lock)                // 不是第一次进入, 释放按键才执行
  33.               {

  34.                   g_KeyActionFlag = SHORT_KEY;          // 短按
  35.                   KeyState =  KEY_RELEASE;    // 需要进入按键释放状态
  36.               }
  37.               else                          // 当前Key值为1,确认为抖动,则返回上一个状态
  38.               {
  39.                   KeyState =  KEY_CHECK;    // 返回上一个状态
  40.               }

  41.           }
  42.           break;  
  43.         case  KEY_RELEASE:
  44.             if(Key)                     //当前Key值为1,说明按键已经释放,返回开始状态
  45.             {
  46.                 KeyState =  KEY_CHECK;   
  47.             }
  48.             break;   
  49.         default: break;
  50.     }   
  51. }
复制代码


1.4 日常显示模式的设计
        日常显示模式下,我们需要的效果是年、月日、时分三个界面轮流显示,我们通过宏定义定义三个界面的显示时长,这里我们让时分界面显示的稍微长一点,因为时间是一直在变的,用户关注的多。
image.png
  1. #define DISP_YEAR_TIME_OUT 2000  //年显示2秒
  2. #define DISP_DATE_TIME_OUT 2000  //日期显示2秒
  3. #define DISP_TIME_TIME_OUT 5000  //时间显示5秒
复制代码

        让数码管显示对应的数据,我们前面已经说过,只需要往显示缓存disp_buff[4]里送对应的数据就可以了,这里时间数据通过getDateTime()函数来读取,在通过取余和除法运算获取对应位上的数字就可以。
        接下来要实现轮流显示,最简单的就是延迟,但前面我们说过,延迟要阻塞任务处理,按键扫描我们用定时器来处理,这里我们用millis()函数来实现,millis()会返回系统上电以来的运行时间,单位为毫秒,这个其实和我们的my_1ms变量是同样的原理。相应还有一个micros()函数,也是返回运行时间,不过单位为微秒。
       我们可以通过如下这种程序结构来实现,当延时时间到达后处理对应的任务,而不阻塞系统。
  1. disp_time_cnt = millis();
  2. if((millis() - disp_time_cnt)>DISP_TIME_TIME_OUT)
  3. {
  4.   //fun()
  5. }
复制代码

        这里我们使用switch case结构实现三个界面的轮流跳转。完整代码如下:
image.png
  1. void Normal_Disp_Proc()
  2. {
  3.   switch (disp_state)
  4.   {
  5.   case TIME_TO_DISP_YEAR:
  6.     if((millis() - disp_time_cnt)>DISP_TIME_TIME_OUT)
  7.     {
  8.       pcf8563.getDateTime();
  9.       disp_buff[0] = seg_code[((pcf8563.getYear()) / 1000)];
  10.       disp_buff[1] = seg_code[(((pcf8563.getYear()) % 1000) / 100)];
  11.       disp_buff[2] = seg_code[(((pcf8563.getYear()) % 100) / 10)];
  12.       disp_buff[3] = seg_code[((pcf8563.getYear()) % 10)];
  13.       disp_year_cnt = millis();
  14.       disp_state = TIME_TO_DISP_DATE;
  15.     }
  16.     break;
  17.   case TIME_TO_DISP_DATE:
  18.     if((millis() - disp_year_cnt)>DISP_YEAR_TIME_OUT)
  19.     {
  20.       pcf8563.getDateTime();
  21.       disp_buff[0] = seg_code[((pcf8563.getMonth()) / 10)];
  22.       disp_buff[1] = seg_code[((pcf8563.getMonth()) % 10)];
  23.       disp_buff[2] = seg_code[((pcf8563.getDay()) / 10)];
  24.       disp_buff[3] = seg_code[((pcf8563.getDay()) % 10)];
  25.       disp_date_cnt = millis();
  26.       disp_state = TIME_TO_DISP_TIME;
  27.     }
  28.     break;
  29.   case TIME_TO_DISP_TIME:
  30.     if((millis() - disp_date_cnt)>DISP_DATE_TIME_OUT)
  31.     {
  32.       pcf8563.getDateTime();
  33.       disp_buff[0] = seg_code[((pcf8563.getHour()) / 10)];
  34.       disp_buff[1] = seg_code[((pcf8563.getHour()) % 10)];
  35.       disp_buff[2] = seg_code[((pcf8563.getMinute()) / 10)];
  36.       disp_buff[3] = seg_code[((pcf8563.getMinute()) % 10)];
  37.       disp_time_cnt = millis();
  38.       disp_state = TIME_TO_DISP_YEAR;
  39.     }
  40.     break;

  41.   default:
  42.     break;
  43.   }
  44. }
复制代码


1.5 设置显示模式的设计
        设置时间时,我们需要让数码管闪烁显示,和正常显示区别开来,要实现闪烁,也就是先正常显示相应的数据,等待一段时间后熄灭,再等待一段时间后再显示。
         数码管熄灭,我们可以让显示缓存disp_buff[4]的对应数据位seg_code[16]就可以。要实现闪烁,这里我们也不能用延迟,我们可以用一个闪烁标志位blink_flag来实现。在定时器2的中断回调函数里,每隔500ms,blink_flag会切换状态,实现0,1来回切换。代码如下:
image.png
  1. void TIM_attachInterrupt_2()
  2. {
  3.   my_1ms = my_1ms + 1;
  4.   if(my_1ms % 10 == 0)//每10ms运行一次
  5.   {
  6.     Key_Scan();
  7.   }
  8.   if(my_1ms % 500 == 0){
  9.     blink_flag = (blink_flag^1);
  10.   }

  11.   Seg_Disp();
  12. }
复制代码

        设置界面里有设置年、月、日、时、分、闹钟时、闹钟分,7种模式,所以我们这里用一个mode变量来表示,其中mode=0表示为日常显示模式。
       主要程序如下所示,完整程序可以查看源代码。
image.png
  1. switch (mode)
  2.   {
  3.   case 1:
  4.       if(blink_flag)
  5.       {
  6.         disp_buff[0] = seg_code[((setyear) / 1000)];
  7.         disp_buff[1] = seg_code[((setyear % 1000) / 100)];
  8.         disp_buff[2] = seg_code[((setyear % 100) / 10)];
  9.         disp_buff[3] = seg_code[((setyear) % 10)];
  10.       }
  11.       else
  12.       {
  13.         disp_buff[0] = seg_code[16];
  14.         disp_buff[1] = seg_code[16];
  15.         disp_buff[2] = seg_code[16];
  16.         disp_buff[3] = seg_code[16];
  17.       }
  18.   break;
复制代码

1.6 按键应用层功能的设计
       按键部分,我们定义长按为进入时间设置界面,每次长按,切换到下一个设置显示模式下,短按为调整时间,每次短按,数值递增,到达最大值后,返回到最小值继续开始递增。
        所以如果检测到长按,就是切换mode模式状态变量,退出设置模式时,保存相关的时间到RTC里,代码如下:
image.png
  1. case LONG_KEY:
  2.     mode++;
  3.     if(mode>7)
  4.     {
  5.       mode = 0;
  6.       //保存设置时间,设置RTC
  7.       pcf8563.setDateTime(setyear, setmonth, setday, 0,sethour, setminute, 0); //设置日期和时间

  8.       //24:60为闹钟不启用
  9.       pcf8563.setAlarm(setalarmminute,setalarmhour,99,99);
  10.     }
  11.     g_KeyActionFlag = NULL_KEY;
  12. break;
复制代码

        短按就是要先判断当前在哪个模式下,然后设置对应的设置时间变量数值,设置的时候要注意时间的范围限定,尤其是月份和日期的范围。如下是部分代码,完整代码请查看源代码。
image.png
  1. case SHORT_KEY:
  2.     switch (mode) {
  3.       case 1:
  4.         setyear++;
  5.         if(setyear > 2033)
  6.         {
  7.           setyear = 2023;
  8.         }
  9.       break;
  10.      case 2:
  11.         setmonth++;
  12.         if(setmonth > 12)
  13.         {
  14.           setmonth = 1;
  15.         }
  16.       break;
  17.     case 3:
  18.         setday++;
  19.         if((setmonth == 1)||(setmonth == 3)||(setmonth == 5)||(setmonth == 7)||(setmonth == 8)||(setmonth == 10)||(setmonth == 12))
  20.         {
  21.           if(setday > 31)
  22.           {
  23.             setday = 1;
  24.           }
  25.         }
  26.         else if(setmonth == 2)
  27.         {
  28.           if(setyear % 4)
  29.           {
  30.             if(setday > 28)
  31.             {
  32.               setday = 1;
  33.             }
  34.           }
  35.           else //闰年
  36.           {
  37.             if(setday > 29)
  38.             {
  39.               setday = 1;
  40.             }
  41.           }
  42.         }
  43.         else
  44.         {
  45.           if(setday > 30)
  46.           {
  47.             setday = 1;
  48.           }
  49.         }
  50.       break;
复制代码


1.7 按键提示音和闹铃声的设计
        按键提示音,可以用PWM控制无源蜂鸣器发出短暂的声音,自己可以通过修改PWM频率实现不同的音调。我们把这部分函数放到按键事件里,就可以实现按键提示音,代码如下:

  1. void Beep()
  2. {
  3.   PWM_Init(TIM1_CH2, PA1, 5000, 20);
  4.   delay(50);
  5.   PWM_Duty_Updata(TIM1_CH2, 0);
  6. }
复制代码

        闹铃声音,可以自己根据歌曲的乐谱生成对应的音调和节拍数据,然后通过PWM来实现蜂鸣器播放歌曲。
image.png
  1. const uint16_t song[]={330,294,330,441,330,294,330,495,330,294,330,525,495,393,330,294,330,441,330,294,330,495,393,294,248,330,294,330,441,330,294,330,495,330,294,330,525,495,393,294,330,221,294,330,221,196,221,262,248};
  2. const uint16_t durt[]={250,250,250,250,250,250,250,250,250,250,250,250,500,500, 250,250,250,250,250,250,250,250, 500,500,1000, 250,250,250,250,250,250,250,250, 250,250,250,250,500,500, 250,250,500,250,250,250,250,500,500,1000,   250,250,250,250,250,250,250,125,125, 750,250,1000, 250,250,250,250,250,250,500,500,1500, 250,250,250,250,250,250,250,125,125, 750,250,1000, 250,250,500,250,250,250,250,1500,  250,250,750,250,500,250,250, 750,250,500,250,250,500,250,250,250,250,500,500,1000, 250,250,875,125,500,250,250,500,500,1000, 250,250,500,250,250,250,250,1500,  250,250,750,250,500,250,250, 500,250,250,500,250,250,500,250,250,250,250,250,250,1500, 250,250,750,250,500,250,250, 500,250,250,1000,250,250,500,250,250,250,250,2000};
复制代码

        另外,我们还要实现闹铃响了以后,需要用户按下按钮,才能停止播放,不然闹铃一直在播放,所以在整个播放闹铃歌曲时,要一直判断按键是否被按下。代码如下:
image.png
  1. void Alarm_Beep(){
  2.   if(pcf8563.alarmActive()){
  3.     pcf8563.clearAlarm();
  4.     while (digitalRead(PA2)) {
  5.       pinMode(PA1, GPIO_Mode_AF_PP);
  6.       for (int i = (0); i < (sizeof(song)/sizeof(song[0])); i = i + 1) {
  7.         PWM_Frequency_Updata(TIM1_CH2, song[(i)], 20);
  8.         delay(durt[(i)]);
  9.         if(digitalRead(PA2) == 0){
  10.           break;
  11.         }
  12.       }
  13.     }
  14.   }
  15. }
复制代码

五、 结构设计
1.1 设计思路
      整体采用最简约的设计风格,背后只留出按键孔和声音孔,侧面留出USB接口,把电池和模块整个包进去,这里只有按键柄长度和电池厚度需要权衡选择。
image.png image.png
1.2 三维建模
        采用Solidworks根据尺寸要求设计整个外壳,有能力还可以同时建整个电路模块和电池的三维模型,可以做整体的装配体来确认各个部件尺寸上是否有干涉。
image.png image.png
1.3 打印测试
        三维模型设计完后,导出STL格式的3D打印文件,用3D打印机配套的切片软件加载模型,调整好打印角度,设置相关打印参数后,可以在软件中模拟打印,确认打印设置有没有问题,没问题后实际打印测试。
image.png image.png

六、 项目优化
        在功能上,还可以加入倒计时功能,闹铃类型选择,如果有多余引脚还可以加入电池电量监测和提醒功能。另外,数码管目前显示时整机工作电流为16mA,熄灭的时候整机工作电流为7mA,我们的锂电池容量为200mAh,也就是迷你时钟一直显示的工作时长差不多为12小时,这样续航时间上还是不满意,要延长续航时间可以从两方面来入手。第一种,加大电池容量,但整体的体积也变大。第二种,降低整机系统功耗,目前主要一直显示时间导致系统耗电大,后期可以通过软件加入息屏功能,然后进入低功耗模式,唤醒可以通过按键来唤醒。如果有多余引脚还可以通过震动或者声音来唤醒。


回复

使用道具 举报

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

本版积分规则

QQ|Archiver|手机版|小黑屋|好好搭搭在线 ( © 好好搭搭在线 浙ICP备19030393号-1 )

GMT+8, 2024-4-23 22:53 , Processed in 0.344172 second(s), 26 queries .

Powered by Discuz!

© 2001-2024 Comsenz Inc.

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