在申请的小华TL-HC32L021,由于其主打的是低功耗功能,所以这里我们可以尝试使用真个芯片制作一款可穿戴时钟,正好用于低功耗可穿戴设备,所以在此我们制作一款低功耗的时钟,使用GPS来进行时间的更新,方便我们实时获取最新的数据。
首先我们需要知道GPS数据的格式,在此我们使用到的GPS数据帧如下:
$GNGGA,081505.000,,,,,0,00,25.5,,,,,,*70
$GNGLL,,,,,112923.000,V,M*6D
$GPGSA,A,1,,,,,,,,,,,,,25.5,25.5,25.5*02
$BDGSA,A,1,,,,,,,,,,,,,25.5,25.5,25.5*13
$GPGSV,1,1,03,01,,,30,07,,,19,195,,,26*4E
$BDGSV,1,1,02,27,,,40,59,,,31*65
$GNRMC,112923.000,V,,,,,,,051125,,,M*58
$GNVTG,,,,,,,,,M*2D
$GNZDA,112923.000,05,11,2025,00,00*42
$GPTXT,01,01,01,ANTENNA OK*35
这里没有任何的地理位置数据,但是存在时间信息,如果会获取时间,那么获取经纬度和方向角,速度也是很容易的。
时间数据在这里很常见,我们看倒数第二行:
$GNZDA,112923.000,05,11,2025,00,00*42
在$GNZDA,之后存在着081505.000,这就是UTC时间,是英国本初子午线的时间,和北京时间相差8小时,所以需要进行+8操作就是我们的时间,在这里就是16点15分05秒,2025年11月七号。
接下来我们需要对这个数据进行解析,来实现获取时间的操作,我们在这里需要使用到串口和RTC实时时钟来实现这个操作。
首先我们进行串口的配置,需要开启对应的时钟和对于的GPIO功能的AFIO功能:
void Uart_Config(void)
{
stc_gpio_init_t GPIOA_Init = {0};
SYSCTRL_PeriphClockEnable(PeriphClockLpuart1); /*开启GPIO时钟*/
stc_lpuart_init_t LPUART_Config = {0};
GPIO_PA01_AF_LPUART1_TXD();
GPIO_PA02_AF_LPUART1_RXD();
GPIOA_Init.bOutputValue = TRUE;
GPIOA_Init.u32Mode = GPIO_MD_OUTPUT_PP;
GPIOA_Init.u32Pin = GPIO_PIN_01;
GPIOA_Init.u32PullUp = GPIO_PULL_NONE;
GPIOA_Init(&GPIOA_Init);
GPIOA_Init.bOutputValue = TRUE;
GPIOA_Init.u32Mode = GPIO_MD_INPUT;
GPIOA_Init.u32Pin = GPIO_PIN_02;
GPIOA_Init.u32PullUp = GPIO_PULL_UP;
GPIOA_Init(&GPIOA_Init);
/* 串口配置*/
LPUART_Config.stcBaudRate.u32Baud = 9600;
LPUART_Config.stcBaudRate.u32Sclk = SYSCTRL_HclkFreqGet();
LPUART_Config.stcBaudRate.u32SclkSelect = LPUART_SCLK_SEL_PCLK;
LPUART_Config.u32BaudRateGenSelect = LPUART_BAUD_NORMAL;
LPUART_Config.u32FrameLength = LPUART_FRAME_LEN_8B_NOPAR;
LPUART_Config.u32HwControl = LPUART_HWCONTROL_NONE;
LPUART_Config.u32Parity = LPUART_B8_MULTI_DATA_OR_ADDR;
LPUART_Config.u32StopBits = LPUART_STOPBITS_1;
LPUART_Config.u32TransMode = LPUART_MODE_TX_RX;
LPUART_Init(LPUART1,&LPUART_Config);
LPUART_IntFlagClearAll(LPUART1); /* 清除所有状态标志 */
/*串口接受部分配置*/
EnableNvic(LPUART1_IRQn,IrqPriorityLevel0,TRUE);
LPUART_IntEnable(LPUART1,LPUART_INT_RC);
}开启了GPIO和UART功能以后,接下来就是对串口重定向进行配置:
int fputc(int ch, FILE *f)
{
LPUART_TransmitPoll(LPUART1, (uint8_t *)(&ch), 1);
return ch;
}
配置好串口重定向可以帮助我们进行Log的打印功能。
接下来配置一下RTC的相关功能,在配置RTC之前,我需要提及一下,RTC在这个板子上没有任何的外部晶振,连对MCU提供主时钟的晶振都没加,所以都需要使用内部的48Mhz时钟源和32768Hz的内部时钟源进行配置,所以需要在使用之前开启一下这个时钟源从而方便的使用这个时钟进行RTC的操作:
这个对RTC的细节进行设置,开启以后就按照内部的时钟进行运行。
void RTC_Config(void)
{
SYSCTRL_PeriphClockEnable(PeriphClockRtc);
SYSCTRL_ClockSrcEnable(SYSCTRL_CLK_SRC_RCL_32768);
stc_rtc_init_t RTC_Config = {0};
//RTC_RtcStcInit(&RTC_Config);
RTC_Config.stcRtcTime.u8Month = 0x01;
RTC_Config.stcRtcTime.u8Day = 0x01;
RTC_Config.stcRtcTime.u8Hour = 0x00;
RTC_Config.stcRtcTime.u8Minute = 0x01;
RTC_Config.stcRtcTime.u8Second = 0x02;
RTC_Config.u32ClockSelect = RTC_CLK_RCL;
RTC_Config.u8Format = RTC_FMT_24H;
RTC_Unlock();
RTC_Init(&RTC_Config);
RTC_Enable();
//RTC_Lock();
}这个函数获取RTC时钟的数据,赋值给一个全局的结构体,然后通过这个结构体传递时间数据,这样就可以获取到年月日时分秒的数据,从而实现了数据的传递功能。
GPS数据解析的代码如下:
uint8_t GPS_Time_Get(void)
{
char * p =strstr((char *)Buffer,"$GNZDA,");
uint16_t Year;
uint8_t Day;
if(p != NULL)
{
p += strlen("$GNZDA,");
*(p + 6) = '\0';
uint32_t Data = atoi(p);
if(Data!= 0)
{
Data += 80000;
Time_Struct.Hour = ((Data / 100000 *16) + ((Data / 10000) %10)) ;
Time_Struct.Minute = ((((Data / 100) %100) /10) *16) + ((Data / 100) %10) ;
Time_Struct.Second = ((Data %100) / 10 *16) + (Data %100) %10;
RTC_Data_Set();
printf("%x:%x:%x\r\n",Time_Struct.Hour,Time_Struct.Minute,Time_Struct.Second);
*(p + 6) = ' ';
}
else
{
return 0;
}
char *Get_Date = strchr(p,',');
if(Get_Date != NULL)
{
Get_Date += 1;
uint8_t Month = atoi(Get_Date);
Month= ((Month %100) / 10 *16) + (Month %100) %10;
Get_Date = strchr(Get_Date,',');
if(Get_Date != NULL)
{
Get_Date += 1;
Day = atoi(Get_Date);
Day= ((Day %100) / 10 *16) + (Day %100) %10;
Get_Date = strchr(Get_Date,',');
}
if(Get_Date != NULL)
{
Get_Date += 1;
Year = atoi(Get_Date);
Year= ((Year %100) / 10 *16) + (Year %100) %10;
}
Time_Struct.Year = Year;
Time_Struct.Month =Month;
Time_Struct.Day = Day;
}
}
return 1;
}获取RTC数据的代码:
void RTC_Data_Get(void)
{
stc_rtc_time_t RTC_Time = {0};
RTC_DateTimeRead(&RTC_Time);
Time_Struct.Hour = RTC_Time.u8Hour;
Time_Struct.Minute = RTC_Time.u8Minute;
Time_Struct.Second = RTC_Time.u8Second;
}设置时间的代码如下:
void RTC_Data_Set(void)
{
stc_rtc_time_t RTC_Time = {0};
RTC_Time.u8Hour = Time_Struct.Hour ;
RTC_Time.u8Minute = Time_Struct.Minute;
RTC_Time.u8Second = Time_Struct.Second;
RTC_Time.u8Month = 0x01;
RTC_Time.u8Day = 0x01;
// RTC_Time.u8Hour = 0x22;
// RTC_Time.u8Minute = 0x55;
// RTC_Time.u8Second = 0x55;
RTC_Unlock();
SET_REG32_BIT(RTC->CR1, RTC_CR1_WAIT_Msk);
while (RTC_CR1_WAITF_Msk != READ_REG32_BIT(RTC->CR1, RTC_CR1_WAITF_Msk)) /* 等待直到WAITF=1 */
{
}
if(RTC_DateTimeSet(&RTC_Time) == ErrorInvalidParameter)
{
printf("Time_Set_Error\r\n");
}
CLR_REG32_BIT(RTC->CR1, RTC_CR1_WAIT_Msk);
while (RTC_CR1_WAITF_Msk == READ_REG32_BIT(RTC->CR1, RTC_CR1_WAITF_Msk)) /* 等待直到WAITF=0 */
{
}
//RTC_Lock();
}串口中断函数如下:
void LpUart1_IRQHandler(void)
{
if(LPUART_IntFlagGet(LPUART1,LPUART_INT_RC) == TRUE)
{
LPUART_IntFlagClear(LPUART1,LPUART_INT_RC);
*P = READ_REG8(LPUART1->SBUF);
P++;
}
}
由于是使用非阻塞方式获取时间,所以使用一个定时器进行数据的更新操作,通过定时器中断进行数据的解析操作,这样就可以间隔固定的时间来进行时间数据的校准。
定时器GTIM0的配置:
void Timer_Config(void)
{
stc_gtim_init_t stcGtimInit = {0};
SYSCTRL_FuncDisable(SYSCTRL_FUNC_CTIMER0_USE_BTIM); /* 配置GTIM0有效,BTIMER0/1/2无效 */
SYSCTRL_PeriphClockEnable(PeriphClockCtim0); /* 使能CTIM0外设时钟 */
GTIM_StcInit(&stcGtimInit); /* 结构体变量初始值初始化 */
stcGtimInit.u32Mode = GTIM_MD_PCLK; /* 工作模式: 定时器模式,计数时钟源来自PCLK */
stcGtimInit.u32OneShotEn = GTIM_CONTINUOUS_COUNTER; /* 连续计数模式 */
stcGtimInit.u32Prescaler = GTIM_COUNTER_CLK_DIV256; /* 对计数时钟进行预除频 */
stcGtimInit.u32ToggleEn = GTIM_TOGGLE_DISABLE; /* TOG输出禁止 */
stcGtimInit.u32AutoReloadValue = 15625 - 1; /* 自动重载寄存ARR赋值,计数周期为PRS*(ARR+1)*TPCLK */
GTIM_Init(GTIM0, &stcGtimInit); /* GTIM0初始化 */
GTIM_CompareCaptureAllDisable(GTIM0); /* 禁止所有通道比较捕获功能 */
GTIM_IntFlagClear(GTIM0, GTIM_FLAG_UI); /* 清除溢出中断标志位 */
GTIM_IntEnable(GTIM0, GTIM_INT_UI); /* 允许GTIM0溢出中断 */
EnableNvic(CTIM0_IRQn, IrqPriorityLevel0, TRUE); /* NVIC 中断使能 */
}定时器中断函数:
void Ctim0_IRQHandler(void)
{
volatile static uint8_t Temp_Value = 0;
if (TRUE == GTIM_IntFlagGet(GTIM0, GTIM_FLAG_UI))
{
if(Temp_Value == 5)
{
Gloable_Status = true;
Temp_Value = 0;
}
Temp_Value++;
GPIO_PA06_RESET();
//GPIO_PA06_SET();
GTIM_IntFlagClear(GTIM0, GTIM_FLAG_UI); /* 清除GTIM0的溢出中断标志位 */
}
}
这样就可以实现固定间隔时间进行时间的更新校准。
显示机制采用彩屏进行更新,彩屏代码如下:
#include "lcd.h"
#include "lcd_init.h"
/******************************************************************************
函数说明:在指定区域填充颜色
入口数据:xsta,ysta 起始坐标
xend,yend 终止坐标
color 要填充的颜色
返回值: 无
******************************************************************************/
void LCD_Fill(u16 xsta,u16 ysta,u16 xend,u16 yend,u16 color)
{
u16 i,j;
LCD_Address_Set(xsta,ysta,xend-1,yend-1);//设置显示范围
for(i=ysta;i<yend;i++)
{
for(j=xsta;j<xend;j++)
{
LCD_WR_DATA(color);
}
}
}
/******************************************************************************
函数说明:在指定位置画点
入口数据:x,y 画点坐标
color 点的颜色
返回值: 无
******************************************************************************/
void LCD_DrawPoint(u16 x,u16 y,u16 color)
{
LCD_Address_Set(x,y,x,y);//设置光标位置
LCD_WR_DATA(color);
}
/******************************************************************************
函数说明:画线
入口数据:x1,y1 起始坐标
x2,y2 终止坐标
color 线的颜色
返回值: 无
******************************************************************************/
void LCD_DrawLine(u16 x1,u16 y1,u16 x2,u16 y2,u16 color)
{
u16 t;
int xerr=0,yerr=0,delta_x,delta_y,distance;
int incx,incy,uRow,uCol;
delta_x=x2-x1; //计算坐标增量
delta_y=y2-y1;
uRow=x1;//画线起点坐标
uCol=y1;
if(delta_x>0)incx=1; //设置单步方向
else if (delta_x==0)incx=0;//垂直线
else {incx=-1;delta_x=-delta_x;}
if(delta_y>0)incy=1;
else if (delta_y==0)incy=0;//水平线
else {incy=-1;delta_y=-delta_y;}
if(delta_x>delta_y)distance=delta_x; //选取基本增量坐标轴
else distance=delta_y;
for(t=0;t<distance+1;t++)
{
LCD_DrawPoint(uRow,uCol,color);//画点
xerr+=delta_x;
yerr+=delta_y;
if(xerr>distance)
{
xerr-=distance;
uRow+=incx;
}
if(yerr>distance)
{
yerr-=distance;
uCol+=incy;
}
}
}
/******************************************************************************
函数说明:画矩形
入口数据:x1,y1 起始坐标
x2,y2 终止坐标
color 矩形的颜色
返回值: 无
******************************************************************************/
void LCD_DrawRectangle(u16 x1, u16 y1, u16 x2, u16 y2,u16 color)
{
LCD_DrawLine(x1,y1,x2,y1,color);
LCD_DrawLine(x1,y1,x1,y2,color);
LCD_DrawLine(x1,y2,x2,y2,color);
LCD_DrawLine(x2,y1,x2,y2,color);
}
/******************************************************************************
函数说明:画圆
入口数据:x0,y0 圆心坐标
r 半径
color 圆的颜色
返回值: 无
******************************************************************************/
void Draw_Circle(u16 x0,u16 y0,u8 r,u16 color)
{
int a,b;
a=0;b=r;
while(a<=b)
{
LCD_DrawPoint(x0-b,y0-a,color); //3
LCD_DrawPoint(x0+b,y0-a,color); //0
LCD_DrawPoint(x0-a,y0+b,color); //1
LCD_DrawPoint(x0-a,y0-b,color); //2
LCD_DrawPoint(x0+b,y0+a,color); //4
LCD_DrawPoint(x0+a,y0-b,color); //5
LCD_DrawPoint(x0+a,y0+b,color); //6
LCD_DrawPoint(x0-b,y0+a,color); //7
a++;
if((a*a+b*b)>(r*r))//判断要画的点是否过远
{
b--;
}
}
}
void LCD_Show_Chinese(uint16_t X0,uint16_t Y0,uint8_t Index,uint8_t Size ,uint8_t *Buffer,uint16_t Back_Color , uint16_t Front_Color)
{
uint16_t i=0;
LCD_Address_Set(X0,Y0,X0+Size -1,Y0+Size -1);//设置显示范围
for(i=0;i<Size *(Size / 8);i++)
{
for(int j=0;j<8;j++)
{
if((Buffer[i + Index * Size *(Size / 8)] & (1 << j)) == 0)
{
LCD_WR_DATA(Back_Color);
}
else
{
LCD_WR_DATA(Front_Color);
}
}
}
}
void LCD_Show_Char(uint16_t X0,uint16_t Y0,uint8_t Index,uint8_t Size ,uint8_t *Buffer,uint16_t Back_Color , uint16_t Front_Color)
{
uint16_t i=0;
LCD_Address_Set(X0,Y0,X0+Size / 2 -1,Y0+Size -1);//设置显示范围
for(i=0;i<Size *(Size / 8) / 2 ;i++)
{
for(int j=0;j<8;j++)
{
if((Buffer[i + Index * Size *(Size / 8) / 2 ] & (1 << j)) == 0)
{
LCD_WR_DATA(Back_Color);
}
else
{
LCD_WR_DATA(Front_Color);
}
}
}
}
void LCD_Show_Num(uint16_t X0,uint16_t Y0,uint8_t Size ,uint8_t *Buffer,uint16_t Back_Color , uint16_t Front_Color,uint32_t Num)
{
while(Num != 0)
{
LCD_Show_Char(X0,Y0,Num % 10,Size,Buffer,Back_Color,Front_Color);
Num /= 10;
}
}
void LCD_Show_String(uint16_t X0,uint16_t Y0,uint8_t Size ,uint8_t *Buffer,uint16_t Back_Color , uint16_t Front_Color,uint8_t* Str)
{
uint8_t *P = Str;
uint8_t i =0;
while(*P != '\0')
{
LCD_Show_Char(X0 + i*16,Y0,*P - 32,Size,Buffer,Back_Color,Front_Color);
i++;
P++;
}
}
如此即可实现彩屏的显示,也就为这个“时钟”提供了显示部件。
main函数如下,使用__WFI()函数进入低功耗睡眠模式,由于这个板子上没有晶振,所以使用的是PCLK,但是手册上说,不支持在DeepSleep模式在使用PCLK唤醒,所以无法使用DeepSleep模式,这里就使用休眠模式(Sleep Mode)进行低功耗演示。
int32_t main(void)
{
GpioConfig(); /* LED端口初始化 */
LCD_Init();
LCD_Fill(0,0,LCD_W,LCD_H,BLACK);
RTC_Config();
LCD_Show_String(65,0,32,(uint8_t *)tfont16_c,BLACK,WHITE,(uint8_t *)"GPS");
LCD_Show_Chinese(65+16*3,0,0,32,(uint8_t *)tfront64,BLACK,WHITE);
LCD_Show_Chinese(65+16*3 +32,0,1,32,(uint8_t *)tfront64,BLACK,WHITE);
//LCD_Show_Char(64,10,3,32,(uint8_t *)tfont16_c,BLACK,WHITE);
LCD_Show_String(55,70,32,(uint8_t *)tfont16_c,BLACK,WHITE,(uint8_t *)"00:00:00");
Uart_Config();
LPUART_TransmitPoll(LPUART1, (uint8_t *)"Usart_Polling\r\n", strlen("Usart_Polling\r\n"));
Timer_Config();
GTIM_Enable(GTIM0);
while (1)
{
RTC_Data_Get();
sprintf((char *)P_Buffer,"20%02x:%02x:%02x",Time_Struct.Year,Time_Struct.Month,Time_Struct.Day);
LCD_Show_String(40,35,32,(uint8_t *)tfont16_c,BLACK,GBLUE,(uint8_t *)P_Buffer);
sprintf((char *)P_Buffer,"%02x:%02x:%02x",Time_Struct.Hour,Time_Struct.Minute,Time_Struct.Second);
LCD_Show_String(55,70,32,(uint8_t *)tfont16_c,BLACK,YELLOW,(uint8_t *)P_Buffer);
if(Gloable_Status)
{
GPS_Time_Get();
printf("20%02x:%02x:%02x %x:%x:%x\r\n",Time_Struct.Year,Time_Struct.Month,Time_Struct.Day,Time_Struct.Hour,Time_Struct.Minute,Time_Struct.Second);
Gloable_Status = false;
}
P = Buffer;
memset(Buffer,0,360);
//*(volatile uint32_t*)0xE000ED10 |= 0x04;
GTIM_IntDisable(GTIM0, GTIM_INT_UI);
__WFI();
GTIM_IntEnable(GTIM0, GTIM_INT_UI);
}
}这就是我的基于小华TL-HC32L021的低功耗GPS时钟演示,在显示完数据以后就立马进入Sleep Mode,这样就可以更进一步节约功耗,实现低功耗,唤醒源这里选用串口中断进行唤醒(关闭定时器中断防止干扰),这样实现一秒唤醒一次(因为串口数据一秒来一次),唤醒以后从程序睡眠的地方继续运行,不会从头开始。
稳定运行后的功耗如下(差不多0.5w),除去屏幕这个大头剩下的芯片功耗还是蛮低的。


