[RISC-V MCU 应用开发] 基于CH32V003F4P9芯片制作的迷你多功能时钟(二)

[复制链接]
 楼主| 天问51单片机 发表于 2023-5-15 09:12 | 显示全部楼层 |阅读模式
本帖最后由 天问51单片机 于 2023-6-25 17:28 编辑

四、 软件设计
整体程序框架如下图的程序流程图所示,主要包含按键处理模块,设置显示模式处理模块、日常显示模式处理模块、闹钟处理模块。

39206645df0eb99767.png
4.1 数码管的动态扫描显示
数码管动态扫描原理,我们在硬件设计章节里已经分析过,我们程序上先设置好需要显示数字的段码,这里还加了一个NONE类型,就是什么都不显示的黑屏状态,这个后续会用到。 31332645df1336cd3d.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字作为开机动画,可以作为检测数码管或者电路是否有问题的一个自检程序。 48064645df14360b5b.png
  1. uint8_t disp_buff[4]={seg_code[8],seg_code[8],seg_code[8],seg_code[8]};

        接下来,我们让数码管的4个位选轮流导通,每位导通时,把显示缓存对应位的段码点亮,对应的操作为直接给PC端口的输出寄存器赋值就可以了,这里我们在每次操作之前先清零,这样能消除残影,程序结构上采用了switch case结构,用curindex变量来切换状态。
95328645df180751d0.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毫秒的定时周期。
88435645df1bbd987a.png
  1. TIM_attachInterrupt(TIM2, 1000, TIM_attachInterrupt_2);

66728645df1d19c8aa.png
  1. void TIM_attachInterrupt_2() {

  2.   Seg_Disp();

  3. }

        这样我们完成了显示部分底层驱动,后续我们只需要往显示缓存送数据,数码管就显示对应的数据。

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

通过setDateTime()函数来设置年、月、日、星期、时、分、秒。
68703645df21659183.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()函数一次性获取年、月、日、星期、时、分、秒数据到内部缓存里,
90257645df22f733e0.png
  1. void getDateTime();

再通过对应的函数来获取对应时间数据。
6646645df242b7068.png
  1. uint8_t getMinute(); //获取分钟

  2. uint8_t getHour(); //获取小时

  3. uint8_t getDay();  //获取日期

  4. uint8_t getMonth(); //获取月

  5. uint16_t getYear(); //获取年

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

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

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


4.3 按键的长短按扫描程序
我们前面学过用延迟的办法来做按键消抖处理,但是在复杂的系统里,程序里的延迟函数会阻塞程序运行,如果在这个过程中有其他需要及时处理的事件,实时性上就有问题。所以我们在复杂系统里,按键消抖常用定时器来处理,同时还可以做到长按、短按等功能。整个按键扫描程序流程图如下:

3368964657ed2701fc.png


前面我们的Seg_Disp()数码管扫描函数放在了周期为1ms的定时器2的回调函数里,而我们的按键扫描函数需要10ms周期,这种情况下,我们一般不会再去单独设置一个10ms的定时器来处理,因为单片机中的定时器资源十分有限且宝贵。我们可以通过程序来让一个定时器产生一个时间基准,这里我们用一个my_1ms变量来作为时间计数器,然后通过取余运算就可以实现自定义周期执行指定任务。

8597664657ee743320.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.             

  21.               TimeCnt++;  

  22.             

  23.               /*按键时长判断*/

  24.               if(TimeCnt > 100)            // 长按 1 s

  25.               {

  26.                   g_KeyActionFlag = LONG_KEY;

  27.                   TimeCnt = 0;  

  28.                   lock = 0;               //重新检查

  29.                   KeyState =  KEY_RELEASE;    // 需要进入按键释放状态

  30.               }                                 

  31.           }  

  32.           else                     

  33.           {

  34.               if(1==lock)                // 不是第一次进入,  释放按键才执行

  35.               {



  36.                   g_KeyActionFlag = SHORT_KEY;          // 短按

  37.                   KeyState =  KEY_RELEASE;    // 需要进入按键释放状态

  38.               }

  39.               else                          // 当前Key值为1,确认为抖动,则返回上一个状态

  40.               {

  41.                   KeyState =  KEY_CHECK;    // 返回上一个状态

  42.               }

  43.      

  44.           }

  45.           break;  

  46.         case  KEY_RELEASE:

  47.             if(Key)                     //当前Key值为1,说明按键已经释放,返回开始状态

  48.             {

  49.                 KeyState =  KEY_CHECK;   

  50.             }

  51.             break;   

  52.         default: break;

  53.     }   

  54. }


4.4 日常显示模式的设计
日常显示模式下,我们需要的效果是年、月日、时分三个界面轮流显示,我们通过宏定义定义三个界面的显示时长,这里我们让时分界面显示的稍微长一点,因为时间是一直在变的,用户关注的多。

5431664657effa7034.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结构实现三个界面的轮流跳转。完整代码如下:

6515364657f18ad954.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. }


4.5 设置显示模式的设计
设置时间时,我们需要让数码管闪烁显示,和正常显示区别开来,要实现闪烁,也就是先正常显示相应的数据,等待一段时间后熄灭,再等待一段时间后再显示。
数码管熄灭,我们可以让显示缓存disp_buff[4]的对应数据位seg_code[16]就可以。要实现闪烁,这里我们也不能用延迟,我们可以用一个闪烁标志位blink_flag来实现。在定时器2的中断回调函数里,每隔500ms,blink_flag会切换状态,实现0,1来回切换。代码如下:

2030164657f3381914.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表示为日常显示模式。
主要程序如下所示,完整程序可以查看源代码。

458664657f4d9888e.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;


4.6 按键应用层功能的设计
按键部分,我们定义长按为进入时间设置界面,每次长按,切换到下一个设置显示模式下,短按为调整时间,每次短按,数值递增,到达最大值后,返回到最小值继续开始递增。
所以如果检测到长按,就是切换mode模式状态变量,退出设置模式时,保存相关的时间到RTC里,代码如下:

7459364657f604b966.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;

短按就是要先判断当前在哪个模式下,然后设置对应的设置时间变量数值,设置的时候要注意时间的范围限定,尤其是月份和日期的范围。如下是部分代码,完整代码请查看源代码。

4173864657f7619c6b.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;


4.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来实现蜂鸣器播放歌曲。

6889364657f9197baf.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};

另外,我们还要实现闹铃响了以后,需要用户按下按钮,才能停止播放,不然闹铃一直在播放,所以在整个播放闹铃歌曲时,要一直判断按键是否被按下。代码如下:

8811064657fa18fb66.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. }

五、 结构设计


5.1 设计思路
整体采用最简约的设计风格,背后只留出按键孔和声音孔,侧面留出USB接口,把电池和模块整个包进去,这里只有按键柄长度和电池厚度需要权衡选择。

4068164657fbe6e917.png 4968364657fc3a51db.png

5.2 三维建模
采用Solidworks根据尺寸要求设计整个外壳,有能力还可以同时建整个电路模块和电池的三维模型,可以做整体的装配体来确认各个部件尺寸上是否有干涉。

9619264657fcca4c3a.png 51636498085e4b232.png

5.3 打印测试
三维模型设计完后,导出STL格式的3D打印文件,用3D打印机配套的切片软件加载模型,调整好打印角度,设置相关打印参数后,可以在软件中模拟打印,确认打印设置有没有问题,没问题后实际打印测试。
305806498086981bdc.png 4338264980879b7b3c.png


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


gangong 发表于 2024-10-26 20:33 | 显示全部楼层
好长细了
您需要登录后才可以回帖 登录 | 注册

本版积分规则

2

主题

3

帖子

0

粉丝
快速回复 在线客服 返回列表 返回顶部

2

主题

3

帖子

0

粉丝
快速回复 在线客服 返回列表 返回顶部