器件一览
实现目标:完成模拟时钟/数字时钟显示,时间设置,整点报时,姿态感应旋转屏幕等功能。
平台选择:STM32CubeMX,Keil5 MDK
原理简介及实现过程:
-
借助STM32CubeMX配置各IO引脚,外设驱动,LCD驱动引脚,按键驱动,IIC驱动,RTC驱动,以及单片机内部时钟配置等,通过可视化配置外设,大大提高开发效率,Keil5 MDK用于CubeMX生成的工程进行代码编写,编译,通过板载仿真器Dap-link,进行仿真,烧录。
-
显示功能需要由该开发板的LCD实现,故首先需要完成LCD驱动,该LCD采用的是驱动IC的SPI接口,由于相连的引脚没有可使用的硬件SPI可供使用,所以进行软件SPI的编写(正点、野火资源很多,成功移植即可)。驱动成功后能够调用一些GUI画图函数初步实现简单的构图或打印数字字母等;
-
既然是时钟,就需要一个稳定可靠的计时功能,有两个选择,首先想到的就是用定时器控制,定时1s产生中断来达到计时功能;另一个是RTC(Real Time Clock)实时时钟来计时,开始我也没想到这个,后来看芯片用户手册看到了,百度了解才知它是单片机内专门用来提供日历时钟功能,还具有闹钟中断和阶段性中断功能。得益于STM32CubeMX,RTC寄存器配置也十分简单,相比之下,用RTC它不香嘛,还能省下一个定时器;
HAL_RTC_GetTime(&hrtc, &RTCTime, RTC_FORMAT_BIN);//获取时间函数 HAL_RTC_SetTime(&hrtc, &RTCTime, RTC_FORMAT_BIN);//设置时间函数 //俩函数搞定,直呼太香了
-
下一步是绘制表盘、刻度、指针,此部分也是调用LCD中各种函数完成,画圆画线画点。绘制指针原理如下,已知中心原点坐标,圆的半径r,指针线长,这里假设也为r,则走过时间t后该点坐标即可得出(如下图),进而用直线绘制函数可获得每秒的指针了,注意:不同象限的计算公式不同,做出一点修改就可。
void draw_sec_needle(u8 position,u8 len,u16 colour,u8 width)//绘制秒针 { u16 xt,yt,xr,yr,x0,y0; xr=(u16)(sin(position*6*PI/180)*7);//中心圆点的尺寸 yr=(u16)(cos(position*6*PI/180)*7); if(position<15) { xt=(u16)(sin(position*6*PI/180)*len); yt=(u16)(cos(position*6*PI/180)*len); x0=119+xr; y0=119-yr; GUI_LineWith(119+xt,119-yt,x0,y0,width,colour); } ...... }
-
时间设置可以通过中断来设置,这时候需要按键来触发中断,对按键引脚配置寄存器即可,在中断函数对RTC时间设置即可实现更改时间的功能,由于板子上按键有限,所以我为按键增加定义有单击,双击,长按,以此来实现更多功能,当然,此功能需要定时器,所以仅定义了一个键有这三种功能,相当于有六个按键可以使用;
//按键状态检测函数 void key1_read(void) { if(KEY1==0) //当按键按下 { if(short_key1_flag==0)//如果短按标志值为0 { short_key1_flag=1;//开始第一次按键键值扫描 key1_time=0;//按键按下计时变量清0,开始计时,1ms加1 HAL_TIM_Base_Start_IT(&htim2); } else if(short_key1_flag==1) { if(key1_time>=1000) { long_k1_press=1; short_key1_flag=0; } } } if(KEY1==1)//当按键抬起 { if(short_key1_flag==1)//当按键抬起后,此标志如果为1,说明是短按不是长按 { short_key1_flag=0; if(dou_key1_flag==0) { dou_key1_flag=1;//按键双击标志置1,等待确认按键是否为双击 key1_double_time=0;//开始计时,同样1ms加1 } else { if(key1_double_time<500)//第一次短按发生后,在500ms内,发生第二次短按,则完成一次双击,刷新键值 { dou_key1_flag=0; double_k1_press=1; } } } else if(dou_key1_flag==1) { if(key1_double_time>500) { dou_key1_flag=0; short_k1_press=1; } } } }
-
整点报时需要使用蜂鸣器,开发板上的蜂鸣器是无源的,需要2k-4kHz的PWM波驱动它,很遗憾蜂鸣器的引脚PC10没有定时器复用功能,不能由该引脚直接产生PWM,需要初始化其他定时器定时中断反转PC10电平达到PWM驱动效果,至于整点报时只需判断时间是否到整点,再执行蜂鸣器即可;
* TIM4 init function */ void MX_TIM4_Init(void) { TIM_ClockConfigTypeDef sClockSourceConfig = {0}; TIM_MasterConfigTypeDef sMasterConfig = {0}; htim4.Instance = TIM4; htim4.Init.Prescaler = 71; htim4.Init.CounterMode = TIM_COUNTERMODE_UP; htim4.Init.Period = 249; //4kHz htim4.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; htim4.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; if (HAL_TIM_Base_Init(&htim4) != HAL_OK) { Error_Handler(); } sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; if (HAL_TIM_ConfigClockSource(&htim4, &sClockSourceConfig) != HAL_OK) { Error_Handler(); } sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET; sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE; if (HAL_TIMEx_MasterConfigSynchronization(&htim4, &sMasterConfig) != HAL_OK) { Error_Handler(); } } //定时器中断处理函数 void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if(htim == (&htim4)) { HAL_GPIO_TogglePin(BEEP_GPIO_Port, BEEP_Pin);//蜂鸣器驱动 } }
-
姿态感应是通过板子上的mpu6050来获取板子实时的三轴加速度值以及重力加速度值,mpu6050通过I2C总线驱动,该mpu6050恰好接到板子硬件IIC外设上,这里移植的正点原子的mpu6050驱动程序,并用HAL库模拟IIC实现对mpu6050的通信。初始化好mpu6050就可以直接读取三轴加速度值以及重力加速度值了。
MPU_Get_Accelerometer(&ax,&ay,&az);//读取三轴加速度值 MPU_Get_Gyroscope(&gx,&gy,&gz);//读取重力加速度值
虽说对获得的值还需要软件滤波以获取更精确的角度值来反映姿态,但本功能中姿态感应只用来翻转屏幕,故对这部分简单化处理了。通过串口实时向上位机发送x,y轴加速度值,同时翻转板子即可大概确定翻转导致各轴加速度变化的阈值。再将预估阈值设为条件判断屏幕翻转即可。
-
最后的最后,实现屏幕的翻转,这部分我有两个想法,一种是通过实时更改LCD驱动中相应寄存器(控制色块刷新以及数据传输方向的寄存器,驱动代码中有),另一种则是更改画图函数,根据翻转角度实时更新,即做出每种翻转情况下的作图(想想就麻烦)。也因为当时我做的时候是把屏幕翻转放在最后一步,故选择了第一种方法。由于最开始用的LCD驱动不能改方向,这部分研究甚久,功夫不负有心人,更换移植了多次LCD驱动终于成功。
/***************************************************************************** * @name :void LCD_direction(uint8_t direction) * @date :2018-08-09 * @function :Setting the display direction of LCD screen * @parameters :direction:0-0 degree 1-90 degree 2-180 degree 3-270 degree * @retvalue :None ******************************************************************************/ void LCD_direction(uint8_t direction) { lcddev.setxcmd=0x2A; lcddev.setycmd=0x2B; lcddev.wramcmd=0x2C; switch(direction){ case 0: lcddev.width=LCD_W; lcddev.height=LCD_H; lcddev.xoffset=0; lcddev.yoffset=0; LCD_WriteReg(0x36,0);//BGR==1,MY==0,MX==0,MV==0 break; case 1: lcddev.width=LCD_H; lcddev.height=LCD_W; lcddev.xoffset=0; lcddev.yoffset=0; LCD_WriteReg(0x36,(1<<6)|(1<<5));//BGR==1,MY==1,MX==0,MV==1 break; case 2: lcddev.width=LCD_W; lcddev.height=LCD_H; lcddev.xoffset=0; lcddev.yoffset=80; LCD_WriteReg(0x36,(1<<6)|(1<<7));//BGR==1,MY==0,MX==0,MV==0 break; case 3: lcddev.width=LCD_H; lcddev.height=LCD_W; lcddev.xoffset=80; lcddev.yoffset=0; LCD_WriteReg(0x36,(1<<7)|(1<<5));//BGR==1,MY==1,MX==0,MV==1 break; default:break; } }
总结:这次项目让我更加地了解了单片机的工作模式以及STM32的开发应用,为以后的学习更是打下了基础,感谢硬禾平台给予我们这次学习的机会!!
工程文件:https://scuer.lanzous.com/b0166upmb 密码:8vts