飞不高 发表于 2025-4-15 23:23:39

基于FreeRTOS和LVGL的多功能低功耗智能手表(APP篇)

目次

一、简介
二、软件框架
2.1 MDK工程架构
2.2 CubeMX框架
2.3 板载驱动BSP 
1、LCD驱动 
2、各个I2C传感器驱动
3、硬件看门狗驱动
4、按键驱动
5、KT6328蓝牙驱动
2.4 管理函数
2.4.1 StrCalculate.c 计算器管理函数
2.4.2 硬件访问机制-HWDataAccess
2.4.3 LVGL页面管理-PageManager
2.5 FreeRTOS多线程任务
2.5.1 任务初始化 (TaskInit.c)
2.5.2 HardwareInitTask 硬件初始化任务 
1、usart start 串口收发
2、sys delay 体系延时设置 
3、LVGL初始化
4、任务删除
2.5.3 ChargPageEnterTask 充电界面任务
2.5.4 SensorDataUpdateTask 传感器数值更新任务
2.5.5 ScrRenewTask界面革新任务 以及 KeyTask按键任务 
2.5.6 DataSaveTask 数据生存任务
2.5.7 MessageSendTask 串口数据收发任务
2.5.8 IdleEnterTask 空闲任务 以及 StopEnterTask 停止模式任务
2.5.9 WDOGFeedTask 看门狗任务
2.5.10 LvHandlerTask任务
三、总结

一、简介

        本篇开始介绍这个项目标软件部门,我们这里先去介绍APP部门,APP和Bootloader是独立的,如果各人不需要相识Bootloader的话,直接看这篇即可,同样可以实现我们整个手表的功能、、
二、软件框架

2.1 MDK工程架构

 https://i-blog.csdnimg.cn/direct/7c10660c435b429eb9e619ead62b407a.png
├─Application/MDK-ARM               # 用于存放.s文件
├─Application/User/Core             # 用于存放CubeMX生成的初始化文件
││main.c
││gpio.c
││...

├─Application/User/System         # 用于存放自定义的delay.c sys.h等
││delay.c
││...

├─Application/User/Tasks            # 用于存放任务线程的函数
││user_TaskInit.c
││user_HardwareInitTask.c
││user_RunModeTasks.c
││...

├─Application/User/MidFunc          # 用于存放管理函数
││StrCalculate.c
││HWDataAccess.c
││PageManager.c

├─Application/User/GUI_APP          # 用于存放用户的ui app
││ui.c
││...

├─Application/User/GUI_FONT_IMG   # 用于存放字体和图片
││...

├─Drivers/CMSIS                     #内核文件
││...

├─Drivers/User/BSP                  # 用于存放板载设备驱动
││...

├─Middleware/FreeRTOS               # FreeRTOS的底层
││...

├─Middleware/LVGL/GUI               # LVGL的底层
││...

└─Middleware/LVGL/GUI_Port          # 用于存放LVGL驱动
    ├─lv_port_disp.c
    ├─lv_port_indev.c
 2.2 CubeMX框架

        我们的工程是使用CubeMX生成的MDK工程,信任各人都可以纯熟的使用CubeMX和HAL库了,HAL库淡化了硬件层,非常得当我们软件开发。
        本次手表项目使用到的片上外设包括GPIO, IIC, SPI, USART, TIM, ADC, DMA, 具体的对PCB板上器件的驱动,比方LCD, EEPROM等,详见BSP(板载设备驱动层) 
简述一下各个片上外设的用途: 
1、DMA这里重要是配合SPI,SPI通讯不通过CPU而是通过DMA直接发送,视觉上来讲,刷屏应该就会快一些,由于CPU可以去实行别的任务;

https://i-blog.csdnimg.cn/direct/9f0035f37d184cde9d616db3046c4a84.png
https://i-blog.csdnimg.cn/direct/b1123d0bb69948699a69c5ca8b1b15f0.png

2、IIC重要用来跟Back板各个传感器进行通讯,传感器都挂在一个总线上的,这里我们不需要去初始化CubeMX,由于我们这里接纳的是软件I2C。 
3、TIM重要是提供时基,另外一个就是给LCD调节背光;
https://i-blog.csdnimg.cn/direct/220868b89fc54c1599452b0057f9baf0.png
https://i-blog.csdnimg.cn/direct/9b623e6155fa4787be0802ea38414095.png
4、ADC只接了一个电池的分压,进行电池电压采样,预估剩余电量;
https://i-blog.csdnimg.cn/direct/045a9363e1284b01b62dfd8e4ddf8796.png
5、USART接了蓝牙,方便进行IAP和与手机和电脑的助手通讯。
https://i-blog.csdnimg.cn/direct/951c5bd6ac58435ba1f859d42eed6ac3.png
6、RTC实时时钟,提供秒、分、时、日期(日/月/年)和星期的计时。
https://i-blog.csdnimg.cn/direct/321fbbda96b44ea4893b55a8d967deeb.png
同时,我们FreeRTOS的移植也是直接使用我们的CubeMX的,我信任各人都是很熟悉FreeRTOS的,这里我们只需要使能FREERTOS,interface选择CMSIS_V2,其他默认即可。
https://i-blog.csdnimg.cn/direct/4df80a3936d3461999dce5fe5f2cc14d.png

2.3 板载驱动BSP 

这里我们先简朴的去介绍一下BSP,具体的话,各人可以细致的去看看我们的源码,各人只需要知道,BSP帮我封装好了板载的驱动,我们之后需要去和板载各个驱动通讯的时间,只需要直接调用BSP即可。
https://i-blog.csdnimg.cn/direct/3e91edf2710942b4af8064dc9514f8a5.png
这里我们简述一下:
1、LCD驱动 

这里有个地方非常的精妙,就是利用了DMA配合SPI发送,可以大大提高我们CPU的利用率。
https://i-blog.csdnimg.cn/direct/d52ff7fbe7964c1ab1e2a5985edbcc78.png
单字节的发送,我们直接接纳SPI直接发送,由于这个速率非常的快,我们配置DMA去发送的话,反而还浪费时间去配置DMA,得不偿失。
https://i-blog.csdnimg.cn/direct/116aebd06df041c4b75a3b30fa62f9ca.png
        这里我们一次发送多个字节(固定死在对应数值),我们接纳SPI+DMA的形式去发送,但是我们最后一个while(__HAL_DMA_GET_COUNTER(&hdma_spi1_tx)!=0); 最后照旧去等待DMA传输完毕我们在进行下一步操作,这里不免有一个疑问?我们使用DMA传输的话,就是为了去解放我们的CPU,这里死等的话意义在哪里呢?
        要先明白这个道理的话,我们得去看看LVGL任务的优先级,如图所示:
https://i-blog.csdnimg.cn/direct/4636ad8cbe604558bd82c52a81f476b8.png
这里可以看到,LVGL的任务优先级照旧很低的,意味着,我们革新屏幕的优先级也是最低的,会被经常打断,所以说,我们很大大概在死等的过程当中,被其他任务打断,这个时间,我们切换到其他的任务当中去,此时DMA依旧还在传输,这样子,也算是解放了我们的CPU去干其他的事情。 
2、各个I2C传感器驱动

        我这里只介绍一下I2C的流程,具体各个I2C传感器我这里不去细讲,由于这不是我们的重点,由于BSP这些底层的硬件驱动很多都是厂家给我们提供好的,我们只需要知道就行,无需去具体的相识。 
https://i-blog.csdnimg.cn/direct/50b5e1b4a3ea4605a8481486ac1a34cd.png
我们看看I2C驱动有啥函数:
https://i-blog.csdnimg.cn/direct/b4d58de8be9442209b3b9277f9ab0667.png  
首先有一个结构体,我们每个传感器设备都需要创建一个结构体,这样子我们之后进行调用发送起始信号、停止信号、数据发送都可以通过这个结构体的GPIO口进行软件发送。 
可以看到,这些传感器都是通过我们I2C一起和我们MCU通讯的。我们看一下根本流程是如何的,这里拿AHT21温湿度模块举例。
iic_bus_t AHT_bus =
{
        .IIC_SDA_PORT = GPIOB,
        .IIC_SCL_PORT = GPIOB,
        .IIC_SDA_PIN= GPIO_PIN_13,
        .IIC_SCL_PIN= GPIO_PIN_14,
};
首先先声明这个模块连接到的I2C总线,留意,由于我们各个传感器都是挂在在同一个总线上面,所以每一个模块的这个结构体的内容都是一样的。
然后我们就可以根据我们的I2C驱动,去封装我们各个I2C模块,AHT21例子如下:
https://i-blog.csdnimg.cn/direct/bc11dc9dda1c4f418e89184633d0c54a.png
https://i-blog.csdnimg.cn/direct/debaded9074c417b8b96f94482218697.png

3、硬件看门狗驱动

WDOG接纳外置的原因是,想要做睡眠低功耗,那么使用MCU内部的看门狗关闭不了,只能不停叫醒喂狗,否则就要重启,那么这样就失去了睡眠的意义了;
//WDOG_EN
#define WDOG_EN_PORT        GPIOB
#define WDOG_EN_PIN                GPIO_PIN_1
//WDI
#define WDI_PORT        GPIOB
#define WDI_PIN                GPIO_PIN_2

void WDOG_Port_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure = {0};

/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOB_CLK_ENABLE();

GPIO_InitStructure.Pin = WDOG_EN_PIN;
GPIO_InitStructure.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStructure.Pull = GPIO_PULLUP;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(WDOG_EN_PORT, &GPIO_InitStructure);
GPIO_InitStructure.Pin = WDI_PIN;
GPIO_InitStructure.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(WDI_PORT, &GPIO_InitStructure);
}

void WDOG_Enable(void)
{
HAL_GPIO_WritePin(WDOG_EN_PORT,WDOG_EN_PIN,GPIO_PIN_RESET);
}

void WDOG_Disnable(void)
{
HAL_GPIO_WritePin(WDOG_EN_PORT,WDOG_EN_PIN,GPIO_PIN_SET);
}

void WDOG_Feed(void)
{
HAL_GPIO_TogglePin(WDI_PORT,WDI_PIN);
} 我们通过翻转GPIO电平的方式,手动喂狗。 
 4、power(电源)驱动
#include "power.h"
#include "adc.h"
#include "delay.h"

#define INTERNAL_RES 0.128
#define CHARGING_CUR 1

void Power_Pins_Init()
{
        GPIO_InitTypeDef GPIO_InitStruct = {0};

/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOA_CLK_ENABLE();

/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(POWER_PORT, POWER_PIN, GPIO_PIN_RESET);

/*Configure GPIO pin : PA3 */
GPIO_InitStruct.Pin = POWER_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(POWER_PORT, &GPIO_InitStruct);

/*Configure GPIO pin : PA2 */
GPIO_InitStruct.Pin = CHARGE_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING_FALLING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(CHARGE_PORT, &GPIO_InitStruct);

HAL_NVIC_SetPriority(EXTI2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI2_IRQn);
       
}

void Power_Enable()
{
        HAL_GPIO_WritePin(POWER_PORT,POWER_PIN,GPIO_PIN_SET);
}

void Power_DisEnable()
{
        HAL_GPIO_WritePin(POWER_PORT,POWER_PIN,GPIO_PIN_RESET);
}

uint8_t ChargeCheck()//1:charging
{
        return HAL_GPIO_ReadPin(CHARGE_PORT,CHARGE_PIN);
}

float BatCheck()
{
        uint16_t dat;
        float BatVoltage;
        HAL_ADC_Start(&hadc1);
        HAL_ADC_PollForConversion(&hadc1,5);
        dat = HAL_ADC_GetValue(&hadc1);
        HAL_ADC_Stop(&hadc1);
        BatVoltage = dat *2 *3.3 /4096;
        return BatVoltage;
}

float BatCheck_8times()
{
        uint32_t dat=0;
        uint8_t i;
        float BatVoltage;
        for(i=0;i<8;i++)
        {
                HAL_ADC_Start(&hadc1);
                HAL_ADC_PollForConversion(&hadc1,5);
                dat += HAL_ADC_GetValue(&hadc1);
                HAL_ADC_Stop(&hadc1);
                delay_ms(1);
        }
        dat = dat>>3;
        BatVoltage = dat *2 *3.3 /4096;
        return BatVoltage;
}

uint8_t PowerCalculate()
{
        uint8_t power;
        float voltage;
        voltage = BatCheck_8times();
       
        if(ChargeCheck())
        {voltage -= INTERNAL_RES * CHARGING_CUR;}
       
        if((voltage >= 4.2))
        {power = 100;}
        else if(voltage >= 4.06 && voltage <4.2)
        {power = 90;}
        else if(voltage >= 3.98 && voltage <4.06)
        {power = 80;}
        else if(voltage >= 3.92 && voltage <3.98)
        {power = 70;}
        else if(voltage >= 3.87 && voltage <3.92)
        {power = 60;}
        else if(voltage >= 3.82 && voltage <3.87)
        {power = 50;}
        else if(voltage >= 3.79 && voltage <3.82)
        {power = 40;}
        else if(voltage >= 3.77 && voltage <3.79)
        {power = 30;}
        else if(voltage >= 3.74 && voltage <3.77)
        {power = 20;}
        else if(voltage >= 3.68 && voltage <3.74)
        {power = 10;}
        else if(voltage >= 3.45 && voltage <3.68)
        {power = 5;}
        return power;
}

void Power_Init(void)
{
        Power_Pins_Init();
        Power_Enable();
}



        电源部门的话,我们首先要通过使能POWER_EN来包管TPS63020DSJR模块给我们提高电源,以及设置一个电源按键的停止,进行停止叫醒我们的低功耗模式。 
        电源电量检测,我们使用ADC来进行检测,通过检测电池的电压(两个电阻分压后的电压值),来确定当前电池的电量,当读取到TP4056M(充电芯片)的CHARG的引脚为高电平的时间,说明此时正在进行充电,那么屏幕就会革新出我们的充电界面。
https://i-blog.csdnimg.cn/direct/e2d5362e7e5b4ba8b4a148269181eca9.png
4、按键驱动

void Key_Port_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};

/* GPIO Ports Clock Enable */
__HAL_RCC_GPIOA_CLK_ENABLE();

/*Configure GPIO pin : PA5 */
GPIO_InitStruct.Pin = KEY1_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(KEY1_PORT, &GPIO_InitStruct);

        /*Configure GPIO pin : PA4 */
GPIO_InitStruct.Pin = GPIO_PIN_4;
GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING;
GPIO_InitStruct.Pull = GPIO_NOPULL;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
       
/* EXTI interrupt init*/
HAL_NVIC_SetPriority(EXTI9_5_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI9_5_IRQn);
       
        HAL_NVIC_SetPriority(EXTI4_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI4_IRQn);
}

uint8_t KeyScan(uint8_t mode)
{
        static uint8_t key_up = 1;
        static uint8_t key_down = 0;
        uint8_t keyvalue = 0;

        if(mode)
        {
                key_up = 1;
                key_down = 0;
        }

        if( key_up && ((!KEY1) || KEY2))
        {
                osDelay(3);//ensure the key
                if(!KEY1)
                        key_down = 1;
                if(KEY2)
                        key_down = 2;
                if(key_down)
                        key_up = 0;
        }

        if ( key_down && (KEY1 && (!KEY2)) )
        {
                osDelay(3);//ensure the key
                if(KEY1 && (!KEY2))
                {
                        key_up = 1;
                        keyvalue = key_down;
                        key_down = 0;
                }
        }

        return keyvalue;
}

key按键的驱动,通过不断扫描GPIO的电平,判断哪个按键按下,而且GPIO设置有添加停止,这个是为了按键叫醒进入STOP模式的MCU。
5、KT6328蓝牙驱动

#include "KT6328.h"

void KT6328_GPIO_Init(void)
{

GPIO_InitTypeDef GPIO_InitStruct = {0};

__HAL_RCC_GPIOA_CLK_ENABLE();

/*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);

GPIO_InitStruct.Pin = GPIO_PIN_8;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

}


void KT6328_Enable(void)
{
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_SET);
}


void KT6328_Disable(void)
{
        HAL_GPIO_WritePin(GPIOA, GPIO_PIN_8, GPIO_PIN_RESET);
}

这里通过使能BLE_EN这个引脚,来开启和关闭蓝牙,我们串口的配置以及在我们CubeMX中进行配置了。
https://i-blog.csdnimg.cn/direct/8ea1043b72424e6f95c0c737309399c3.png

2.4 管理函数

https://i-blog.csdnimg.cn/direct/8224e8653a544eac9ebdea31d5fb9940.png
这里存放了三个管理文件,我们这里去一个个给各人进行介绍。
2.4.1 StrCalculate.c 计算器管理函数

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../Inc/StrCalculate.h"

uint8_t strput(StrStack_t * st,char strin)
{
    if(st->Top_Point == 15 - 1)
    {return -1;}

    st->strque = strin;
    return 0;
}

uint8_t strdel(StrStack_t * st)
{
    if(st->Top_Point == 0)
    {return -1;}

    st->strque[--st->Top_Point] = NULL;
    return 0;
}

uint8_t strstack_isEmpty(StrStack_t* st)
{
    if(st->Top_Point == 0)
    {return 1;}

    return 0;
}

void strclear(StrStack_t* sq)
{
    while(!strstack_isEmpty(sq))
    {
      strdel(sq);
    }
}

uint8_t NumStackPut(NumStack_t * st, float in)
{
   if(st->Top_Point == CAL_DEPTH - 1)
        {return -1;}

        st->data = in;
        return 0;
}

uint8_t NumStackDel(NumStack_t * st)
{
    if(st->Top_Point == 0)
        {return -1;}
    st->data = 0;
        return 0;
}

uint8_t NumStack_isEmpty(NumStack_t* st)
{
        if(st->Top_Point == 0)
        {return 1;}

        return 0;
}

void NumStackClear(NumStack_t* st)
{
        while(!NumStack_isEmpty(st))
        {
                NumStackDel(st);
        }
}

uint8_t SymStackPut(SymStack_t * st, char in)
{
        if(st->Top_Point == CAL_DEPTH - 1)
        {return -1;}

        st->data = in;
        return 0;
}

uint8_t SymStackDel(SymStack_t * st)
{
if(st->Top_Point == 0)
        {return -1;}
    st->data = 0;
        return 0;
}

uint8_t SymStack_isEmpty(SymStack_t* st)
{
        if(st->Top_Point == 0)
        {return 1;}

        return 0;
}

void SymStackClear(SymStack_t* st)
{
        while(!SymStack_isEmpty(st))
        {
                SymStackDel(st);
        }
}

uint8_t SymisHighPriority(char top, char present)
{
    //乘除的优先级最大
    if(top == '*' || top == '/')
    {
      return 1;
    }
    else if(top == '+')
    {
      if(present == '-')
      {return 1;}
      else
      {return 0;}
    }
    else if(top == '-')
    {
      if(present == '+')
      {return 1;}
      else
      {return 0;}
    }
}

void CalculateOne(NumStack_t * numstack, SymStack_t * symstack)
{
    caldata_t temp;
    temp.datatype = NUMBER_TYPE;
    temp.symbol = NULL;
    //计算数字栈中的顶部两数,结果存到temp中
    if(symstack->data == '+')
      temp.number = (numstack->data) + (numstack->data);

    else if(symstack->data == '-')
      temp.number = (numstack->data) - (numstack->data);

    else if(symstack->data == '*')
      temp.number = (numstack->data) * (numstack->data);

    else if(symstack->data == '/')
      temp.number = (numstack->data) / (numstack->data);

    //运算前两数出栈,运算结果数入栈
    NumStackDel(numstack);
    NumStackDel(numstack);
    NumStackPut(numstack,temp.number);
    SymStackDel(symstack);

}

uint8_t NumSymSeparate(char * str, uint8_t strlen, NumStack_t * NumStack, SymStack_t * SymStack)
{
                NumStackClear(NumStack);
                SymStackClear(SymStack);
    caldata_t temp,temp_pre;
    char NumBehindPoint_Flag = 0;//数字是否在小数点后,后多少位
    temp.datatype = NUMBER_TYPE;
    temp.number = 0;
    temp.symbol = NULL;
    temp_pre = temp;
    temp_pre.datatype = SYMBOL_TYPE;
    if(str>'9' || str<'0')
      return 1;//erro
                int i;
    for(i=0;i<strlen;i++)
    {
      if(str=='.')
      {
            temp.datatype = POINT_TYPE;
            if(temp_pre.datatype == NUMBER_TYPE)
            {}
            else
            {return 2;}
            temp_pre = temp;
      }
      if(str<='9' && str>='0')
      {
            //溢出报错
            if(NumStack->Top_Point>CAL_DEPTH || SymStack->Top_Point>CAL_DEPTH)
            {return 3;}
            //读取当前的字符到temp中
            temp.datatype = NUMBER_TYPE;
            temp.number = (str - '0');
            temp.symbol = NULL;
            //如果为连续数字,需要进行进位,将数字栈顶读出进位,再加上现在位,再入栈
            if(temp_pre.datatype == NUMBER_TYPE)
            {
                if(!NumBehindPoint_Flag)
                {temp.number += NumStack->data * 10;}
                else
                {
                  NumBehindPoint_Flag += 1;
                  char i = NumBehindPoint_Flag;
                  while(i--)
                  {temp.number /= 10;}
                  temp.number += NumStack->data;
                }
                NumStackDel(NumStack);
                NumStackPut(NumStack,temp.number);
            }
            //当前数字刚好是小数点后一位
            else if(temp_pre.datatype == POINT_TYPE)
            {
                NumBehindPoint_Flag = 1;
                temp.number /= 10;
                temp.number += NumStack->data;
                NumStackDel(NumStack);
                NumStackPut(NumStack,temp.number);
            }
            //前一位不是数字或小数点,现在读取的这一位是数字,直接入栈
            else
            {
                NumStackPut(NumStack,temp.number);
            }
            temp_pre = temp;
      }
      else if(str == '+' || str == '-' || str == '*' || str == '/')
      {
            //溢出报错
            if(NumStack->Top_Point>CAL_DEPTH || SymStack->Top_Point>CAL_DEPTH)
            {return 4;}
            //读取当前的字符到temp中
            temp.datatype = SYMBOL_TYPE;
            temp.symbol = str;
            temp.number = 0;
            NumBehindPoint_Flag = 0;//小数点计算已经结束
            //重复输入了运算符号
            if(temp_pre.datatype == SYMBOL_TYPE)
            {
                return 5 ;//erro
            }
            else
            {
                if((!SymStack_isEmpty(SymStack)) && SymisHighPriority(SymStack->data,temp.symbol))
                {
                  CalculateOne(NumStack, SymStack);
                  SymStackPut(SymStack,temp.symbol);
                }
                else
                {
                  //符号压入符号栈
                  SymStackPut(SymStack,temp.symbol);
                }
                temp_pre = temp;
            }
      }
    }
    return 0;
}

uint8_t StrCalculate(char * str,NumStack_t * NumStack, SymStack_t * SymStack)

{
    if(NumSymSeparate(str,strlen(str),NumStack,SymStack))
    {
      //erro, clear all
      NumStackClear(NumStack);
      SymStackClear(SymStack);
      return -1;
    }
    else
    {
      while(!SymStack_isEmpty(SymStack))
      {
            CalculateOne(NumStack,SymStack);
      }
    }
    return 0;
}

uint8_t isIntNumber(float number)
{
        if(number == (int)number)
        {return 1;}
        return 0;
}


计算器的逻辑就是很经典的计算器问题,经典的就是开两个栈,一个存放符号,一个存数字,然后进行出栈计算等等操作。
具体过程是:
1、遍历表达式,当碰到操作数,将其压入操作数栈。
2、碰到运算符时,如果运算符栈为空,则直接将其压入运算符栈。
3、如果运算符栈不为空,那就与运算符栈顶元素进行比力:如果当前运算符优先级比栈顶运算符高,则继续将其压入运算符栈,如果当前运算符优先级比栈顶运算符低大概相当,则从操作数符栈顶取两个元素,从栈顶取出运算符进行运算,并将运算结果压入操作数栈。
4、继续将当前运算符与运算符栈顶元素比力。
5、继续按照以上步骤进行遍历,当遍历结束之后,则将当前两个栈内元素取出来进行运算即可得到最终结果。
这里我简朴的介绍一下这个算法:
2.4.1.1 数据结构 
   StrStack_t:字符栈
---用于临时存储输入字符
---供strput(入栈)、strdel(出栈)等操作
NumStack_t:数字栈(浮点数)
---存储运算中的数字
---深度为CAL_DEPTH(15)
SymStack_t:符号栈
---存储运算符(+-*/)
---同样具有栈操作函数
2.4.1.2 焦点算法流程
uint8_t NumSymSeparate(...) 这个可以说是整个算法焦点部门了,NumSymSeparate函数,它负责将输入的字符串分解为数字和运算符,并处置惩罚运算顺序的问题。这里需要特别留意数字的小数点处置惩罚和运算符优先级的判断。比如,当碰到小数点时,标记NumBehindPoint_Flag,并调整数字的位数。运算符处置惩罚时,通过SymisHighPriority函数比力栈顶运算符和当前运算符的优先级,决定是否立即进行计算,从而保持正确的运算顺序。另外,在NumSymSeparate函数中,当处置惩罚到运算符时,会检查前一个元素是否是符号范例,如果是则报错,这样处置惩罚连续的运算符(如"5++3")会被视为错误,这是正确的。但如果是负数的情况,这里就会导致错误,所以代码不支持负数的运算。前面做的全部都是为了这个函数进行铺垫,我们可以在函数调用关系看到:
https://i-blog.csdnimg.cn/direct/ea5a3bd63eac4cfcbe14a58dcd8f89a8.png
优先级判断(SymisHighPriority函数)
uint8_t SymisHighPriority(...)   优先级规则:* / > + > -
栈顶运算符优先级 >= 当前运算符时返回1
比方:
栈顶+ vs 当前- → 同优先级,返回1
栈顶+ vs 当前* → 当前优先级高,返回0
void CalculateOne(NumStack_t * numstack, SymStack_t * symstack) CalculateOne函数用于实行实际的运算操作,取出数字栈顶的两个数字和符号栈顶的运算符,计算结果后再将结果压回数字栈。这一步是实际计算的焦点。
uint8_t StrCalculate(char * str,NumStack_t * NumStack, SymStack_t * SymStack)
1、调用NumSymSeparate进行表达式分解
2、循环实行CalculateOne直到符号栈为空
3、最终结果存储在数字栈顶
2.4.2 硬件访问机制-HWDataAccess

为什么加入HWDataAccess.c,而不直接调用BSP的API呢,重要是为了方便移植和管理。 
上面图片所示这个../User文件夹中的Func文件夹和GUI_APP文件夹,全部复制到LVGL仿真文件夹中,如下所示,即完成了仿真的移植。 
https://i-blog.csdnimg.cn/direct/2b9c975683ec4842b76a17bc8d6c84be.png
https://i-blog.csdnimg.cn/direct/c6683d67c37d4cb5aa80313daa8a6761.png
固然,MDK工程和LVGL仿真工程的移植过程需要改一个东西,就是HWDataAccess.h中的使能: 
https://i-blog.csdnimg.cn/direct/37290f531c324e7f9cd1e99d2c9c785f.png
如果是在仿真中,就把HW_USE_HARDWARE定义为0即可,MDK中自然就是定义为1。使用这个HWDataAccess就方便把硬件抽象出来了,具体的代码详见代码。 
HWDataAccess具体使用方式: 
/***************************
*External Variables
***************************/
HW_InterfaceTypeDef HWInterface = {
    .RealTimeClock = {
      .GetTimeDate = HW_RTC_Get_TimeDate,
      .SetDate = HW_RTC_Set_Date,
      .SetTime = HW_RTC_Set_Time,
      .CalculateWeekday = HW_weekday_calculate
    },
    .BLE = {
      .Enable = HW_BLE_Enable,
      .Disable = HW_BLE_Disable
    },
    .Power = {
      .power_remain = 0,
      .Init = HW_Power_Init,
      .Shutdown = HW_Power_Shutdown,
      .BatCalculate = HW_Power_BatCalculate
    },
    .LCD = {
      .SetLight = HW_LCD_Set_Light
    },

    .IMU = {
      .ConnectionError = 1,
      .Steps = 0,
      .wrist_is_enabled = 0,
      .wrist_state = WRIST_UP,
      .Init = HW_MPU_Init,
      .WristEnable = HW_MPU_Wrist_Enable,
      .WristDisable = HW_MPU_Wrist_Disable,
      .GetSteps = HW_MPU_Get_Steps,
      .SetSteps = HW_MPU_Set_Steps
    },
    .AHT21 = {
      .ConnectionError = 1,
      .humidity = 67,
      .temperature = 26,
      .Init = HW_AHT21_Init,
      .GetHumiTemp = HW_AHT21_Get_Humi_Temp
    },
    .Barometer = {
      .ConnectionError = 1,
      .altitude = 19,
      .Init = HW_Barometer_Init,
    },
    .Ecompass = {
      .ConnectionError = 1,
      .direction = 45,
      .Init = HW_Ecompass_Init,
      .Sleep = HW_Ecompass_Sleep
    },
    .HR_meter = {
      .ConnectionError = 1,
      .HrRate = 0,
      .SPO2 = 99,
      .Init = HW_HRmeter_Init,
      .Sleep = HW_HRmeter_Sleep
    }
};  如安在UI层使用HWDataAccess呢,比方在HomePage中的调节LCD亮度的回调函数中,这么使用,可以看到直接调用HWInterface.LCD.SetLight(ui_LightSliderValue);即可。
void ui_event_LightSlider(lv_event_t * e)
{
    lv_event_code_t event_code = lv_event_get_code(e);
    lv_obj_t * target = lv_event_get_target(e);
    if(event_code == LV_EVENT_VALUE_CHANGED)
    {
      ui_LightSliderValue = lv_slider_get_value(ui_LightSlider);
      HWInterface.LCD.SetLight(ui_LightSliderValue);
    }
} 那么他是如安在有硬件的MDK工程中也能用,LVGL无硬件的仿真也能用,我们看到HWInterface.LCD.SetLight对应的函数是什么:
HW_InterfaceTypeDef HWInterface = {
    // 省略前面
    .LCD = {
            .SetLight = HW_LCD_Set_Light
      },
    // 省略后面
} 首先看到HWInterface.LCD.SetLight定义的是函数HW_LCD_Set_Light,而这个函数的内容如下,即当HW_USE_LCD使能时,运行这个函数,能够正常调光,当LVGL仿真中不使能硬件HW_USE_HARDWARE时, HW_USE_LCD也不使能,则此函数实行空,工程也不会报错。 
void HW_LCD_Set_Light(uint8_t dc)
{
    #if HW_USE_LCD
      LCD_Set_Light(dc);
    #endif
}  2.4.3 LVGL页面管理-PageManager

这个可以说是一个万用模板了,LVGL中的项目中,都几乎离不开这个管理模式。手表项目标LVGL页面有很多,在GUI_App文件夹中,Screen文件夹中存放着全部的page。由于screen很多,所以有必要进行页面管理。这里开一个栈进行页面管理。首先看到PageManager.h, Page_t结构体是用于描述一个LVGL页面的,里面的对象有初始化函数init,反初始化函数deinit以及一个用于存放lvgl对象的地点的lv_obj_t **page_obj。PageStack_t结构体描述一个界面栈,用于存放Page_t页面结构体,top体现栈顶。
// 页面栈深度
#define MAX_DEPTH 6

// 页面结构体
typedef struct {
    void (*init)(void);
    void (*deinit)(void);
    lv_obj_t **page_obj;
} Page_t;


// 页面堆栈结构体
typedef struct {
    Page_t* pages;
    uint8_t top;
} PageStack_t;

extern PageStack_t PageStack; 再看到PageManager.c,栈的初始化尚有push和pop操作就不再赘述了,在pop函数中,除了将top减1,还调用了页面deinit函数,负责反初始化当前页面,这里我们不是直接删除当前页面,是将当前界面临应的LVGL软件定时器关闭掉。https://i-blog.csdnimg.cn/direct/b249ec84c95e4ae6bc6c2ad2b8f2b95c.png
Page_Back(), Page_Back_Bottom(), Page_Load()就是重要在代码中调用的函数了,分别的作用是Back到上一个界面,Back到最底部的Home界面,以及load新的界面。 
我们这里给各人演示一个页面的流程,选择一个对象较少的充电界面,首先我们需要注册一个Page结构体存储当前的页面,添补好初始化init,反初始化函数deinit以及LVGL页面临象&ui_ChargPage,然后我的deinit是用于删除定时器timer的,这里的timer重要用于刷当前页面的数据,所以不在当前页面时需要删除掉。 
// 省略前面...

/ Page Manager //
Page_t Page_Charg = {ui_ChargPage_screen_init, ui_ChargPage_screen_deinit, &ui_ChargPage};

/// Timer //
// need to be destroyed when the page is destroyed
static void ChargPage_timer_cb(lv_timer_t * timer)
{
    if(Page_Get_NowPage()->page_obj == &ui_ChargPage)
    {
      // 刷新数据等操作
    }
}

/ SCREEN init
void ui_ChargPage_screen_init(void)
{
    ui_ChargPage = lv_obj_create(NULL);//创建界面对象
    // 省略中间...
    // private timer
    ui_ChargPageTimer = lv_timer_create(ChargPage_timer_cb, 2000,NULL);
}

/// SCREEN deinit
void ui_ChargPage_screen_deinit(void)
{
lv_timer_del(ui_ChargPageTimer);
}

// 省略后面... 2.5 FreeRTOS多线程任务

这里默认各人已经会用FreeRTOS了,此项目都用的CMSIS_OS_V2的API。Tasks文件以及其作用如下所示,我们这里一个个的去解说任务。
├─Application/User/Tasks            # 用于存放任务线程的函数
│├─user_TaskInit.c                # 初始化任务
│├─user_HardwareInitTask.c      # 硬件初始化任务
│├─user_RunModeTasks.c            # 运行模式任务
│├─user_KeyTask.c               # 按键任务
│├─user_DataSaveTask.c            # 数据保存任务
│├─user_MessageSendTask.c         # 消息发送任务
│├─user_ChargeCheckTask.c         # 充电检查任务
│├─user_SensUpdateTask.c          # 传感器更新任务
│├─user_ScrRenewTask.c            # 屏幕刷新任务 2.5.1 任务初始化 (TaskInit.c)

/* Private includes -----------------------------------------------------------*/
//includes
#include "user_TasksInit.h"
//sys
#include "sys.h"
#include "stdio.h"
#include "lcd.h"
#include "WDOG.h"
//gui
#include "lvgl.h"
#include "ui_TimerPage.h"
//tasks
#include "user_HardwareInitTask.h"
#include "user_RunModeTasks.h"
#include "user_KeyTask.h"
#include "user_ScrRenewTask.h"
#include "user_SensUpdateTask.h"
#include "user_ChargCheckTask.h"
#include "user_MessageSendTask.h"
#include "user_DataSaveTask.h"

/* Private typedef -----------------------------------------------------------*/


/* Private define ------------------------------------------------------------*/


/* Private variables ---------------------------------------------------------*/


/* Timers --------------------------------------------------------------------*/
osTimerId_t IdleTimerHandle;


/* Tasks ---------------------------------------------------------------------*/
// Hardwares initialization
osThreadId_t HardwareInitTaskHandle;
const osThreadAttr_t HardwareInitTask_attributes = {
.name = "HardwareInitTask",
.stack_size = 128 * 10,
.priority = (osPriority_t) osPriorityHigh3,
};

//LVGL Handler task
osThreadId_t LvHandlerTaskHandle;
const osThreadAttr_t LvHandlerTask_attributes = {
.name = "LvHandlerTask",
.stack_size = 128 * 24,
.priority = (osPriority_t) osPriorityLow,
};

//WDOG Feed task
osThreadId_t WDOGFeedTaskHandle;
const osThreadAttr_t WDOGFeedTask_attributes = {
.name = "WDOGFeedTask",
.stack_size = 128 * 1,
.priority = (osPriority_t) osPriorityHigh2,
};

//Idle Enter Task
osThreadId_t IdleEnterTaskHandle;
const osThreadAttr_t IdleEnterTask_attributes = {
.name = "IdleEnterTask",
.stack_size = 128 * 1,
.priority = (osPriority_t) osPriorityHigh,
};

//Stop Enter Task
osThreadId_t StopEnterTaskHandle;
const osThreadAttr_t StopEnterTask_attributes = {
.name = "StopEnterTask",
.stack_size = 128 * 16,
.priority = (osPriority_t) osPriorityHigh1,
};

//Key task
osThreadId_t KeyTaskHandle;
const osThreadAttr_t KeyTask_attributes = {
.name = "KeyTask",
.stack_size = 128 * 1,
.priority = (osPriority_t) osPriorityNormal,
};

//ScrRenew task
osThreadId_t ScrRenewTaskHandle;
const osThreadAttr_t ScrRenewTask_attributes = {
.name = "ScrRenewTask",
.stack_size = 128 * 10,
.priority = (osPriority_t) osPriorityLow1,
};

//SensorDataRenew task
osThreadId_t SensorDataTaskHandle;
const osThreadAttr_t SensorDataTask_attributes = {
.name = "SensorDataTask",
.stack_size = 128 * 5,
.priority = (osPriority_t) osPriorityLow1,
};

//HRDataRenew task
osThreadId_t HRDataTaskHandle;
const osThreadAttr_t HRDataTask_attributes = {
.name = "HRDataTask",
.stack_size = 128 * 5,
.priority = (osPriority_t) osPriorityLow1,
};

//ChargPageEnterTask
osThreadId_t ChargPageEnterTaskHandle;
const osThreadAttr_t ChargPageEnterTask_attributes = {
.name = "ChargPageEnterTask",
.stack_size = 128 * 10,
.priority = (osPriority_t) osPriorityLow1,
};

//messagesendtask
osThreadId_t MessageSendTaskHandle;
const osThreadAttr_t MessageSendTask_attributes = {
.name = "MessageSendTask",
.stack_size = 128 * 5,
.priority = (osPriority_t) osPriorityLow1,
};

//MPUCheckTask
osThreadId_t MPUCheckTaskHandle;
const osThreadAttr_t MPUCheckTask_attributes = {
.name = "MPUCheckTask",
.stack_size = 128 * 3,
.priority = (osPriority_t) osPriorityLow2,
};

//DataSaveTask
osThreadId_t DataSaveTaskHandle;
const osThreadAttr_t DataSaveTask_attributes = {
.name = "DataSaveTask",
.stack_size = 128 * 5,
.priority = (osPriority_t) osPriorityLow2,
};


/* Message queues ------------------------------------------------------------*/
//Key message
osMessageQueueId_t Key_MessageQueue;
osMessageQueueId_t Idle_MessageQueue;
osMessageQueueId_t Stop_MessageQueue;
osMessageQueueId_t IdleBreak_MessageQueue;
osMessageQueueId_t HomeUpdata_MessageQueue;
osMessageQueueId_t DataSave_MessageQueue;

/* Private function prototypes -----------------------------------------------*/
void LvHandlerTask(void *argument);
void WDOGFeedTask(void *argument);

/**
* @briefFreeRTOS initialization
* @paramNone
* @retval None
*/
void User_Tasks_Init(void)
{
/* add mutexes, ... */

/* add semaphores, ... */

/* start timers, add new ones, ... */

        IdleTimerHandle = osTimerNew(IdleTimerCallback, osTimerPeriodic, NULL, NULL);
        osTimerStart(IdleTimerHandle,100);//100ms

/* add queues, ... */
        Key_MessageQueue= osMessageQueueNew(1, 1, NULL);
        Idle_MessageQueue = osMessageQueueNew(1, 1, NULL);
        Stop_MessageQueue = osMessageQueueNew(1, 1, NULL);
        IdleBreak_MessageQueue = osMessageQueueNew(1, 1, NULL);
        HomeUpdata_MessageQueue = osMessageQueueNew(1, 1, NULL);
        DataSave_MessageQueue = osMessageQueueNew(2, 1, NULL);

        /* add threads, ... */
HardwareInitTaskHandle= osThreadNew(HardwareInitTask, NULL, &HardwareInitTask_attributes);
LvHandlerTaskHandle= osThreadNew(LvHandlerTask, NULL, &LvHandlerTask_attributes);
WDOGFeedTaskHandle   = osThreadNew(WDOGFeedTask, NULL, &WDOGFeedTask_attributes);
        IdleEnterTaskHandle= osThreadNew(IdleEnterTask, NULL, &IdleEnterTask_attributes);
        StopEnterTaskHandle= osThreadNew(StopEnterTask, NULL, &StopEnterTask_attributes);
        KeyTaskHandle                        = osThreadNew(KeyTask, NULL, &KeyTask_attributes);
        ScrRenewTaskHandle   = osThreadNew(ScrRenewTask, NULL, &ScrRenewTask_attributes);
        SensorDataTaskHandle = osThreadNew(SensorDataUpdateTask, NULL, &SensorDataTask_attributes);
        HRDataTaskHandle               = osThreadNew(HRDataUpdateTask, NULL, &HRDataTask_attributes);
        ChargPageEnterTaskHandle = osThreadNew(ChargPageEnterTask, NULL, &ChargPageEnterTask_attributes);
MessageSendTaskHandle = osThreadNew(MessageSendTask, NULL, &MessageSendTask_attributes);
        MPUCheckTaskHandle                = osThreadNew(MPUCheckTask, NULL, &MPUCheckTask_attributes);
        DataSaveTaskHandle                = osThreadNew(DataSaveTask, NULL, &DataSaveTask_attributes);

/* add events, ... */


        /* addothers ... */
        uint8_t HomeUpdataStr;
        osMessageQueuePut(HomeUpdata_MessageQueue, &HomeUpdataStr, 0, 1);

}


/**
* @briefFreeRTOS Tick Hook, to increase the LVGL tick
* @paramNone
* @retval None
*/
void TaskTickHook(void)
{
        //to increase the LVGL tick
        lv_tick_inc(1);
        //to increase the timerpage's timer(put in here is to ensure the Real Time)
        if(ui_TimerPageFlag)
        {
                        ui_TimerPage_ms+=1;
                        if(ui_TimerPage_ms>=10)
                        {
                                ui_TimerPage_ms=0;
                                ui_TimerPage_10ms+=1;
                        }
                        if(ui_TimerPage_10ms>=100)
                        {
                                        ui_TimerPage_10ms=0;
                                        ui_TimerPage_sec+=1;
                                        uint8_t IdleBreakstr = 0;
                                        osMessageQueuePut(IdleBreak_MessageQueue, &IdleBreakstr, 0, 0);
                        }
                        if(ui_TimerPage_sec>=60)
                        {
                                        ui_TimerPage_sec=0;
                                        ui_TimerPage_min+=1;
                        }
                        if(ui_TimerPage_min>=60)
                        {
                                        ui_TimerPage_min=0;
                        }
        }
        user_HR_timecount+=1;
}


/**
* @briefLVGL Handler task, to run the lvgl
* @paramargument: Not used
* @retval None
*/
void LvHandlerTask(void *argument)
{
        uint8_t IdleBreakstr=0;
while(1)
{
                if(lv_disp_get_inactive_time(NULL)<1000)
                {
                        //Idle time break, set to 0
                        osMessageQueuePut(IdleBreak_MessageQueue, &IdleBreakstr, 0, 0);
                }
                lv_task_handler();
    osDelay(1);
        }
}


/**
* @briefWatch Dog Feed task
* @paramargument: Not used
* @retval None
*/
void WDOGFeedTask(void *argument)
{
        //owdg
        WDOG_Port_Init();
while(1)
{
                WDOG_Feed();
                WDOG_Enable();
    osDelay(100);
}
}


        注册各个任务,分配空间,注册一些信号量,任务的汇总可以看上面的源码,我之后将一个个任务的去进行解说。
        同时也创建了一个软件定时器,用于纪录空闲时间,即用户没有操作过长就会发出idle信号,idle任务读取到这个队列之后,就会进行一些处置惩罚,如果idle过长,就会发出STOP信号,STOP任务读取到这个队列之后,进入睡眠。
https://i-blog.csdnimg.cn/direct/5def9bef46c241c1ab049306bed0f21b.png
https://i-blog.csdnimg.cn/direct/8a26fa42156d4ae48c90fc60ad8d4185.png
        而且LVGL的时基提供也在这个文件夹,以及我们计时功能的时间也在这里进行提供,我们这里去看一下。
https://i-blog.csdnimg.cn/direct/0129483d10f145bc8123d6d00d747895.png
本质上是利用我们FreeRTOS的钩子函数,void vApplicationTickHook( void ); 
https://i-blog.csdnimg.cn/direct/56694f59781c498b8915e18caa6919cd.png
vApplicationTickHook()函数的运行周期由configTICK_RATE_HZ决定,一般都设置为1ms。
 https://i-blog.csdnimg.cn/direct/5460e627ff874e73b0c70c8af7e433f2.png
2.5.2 HardwareInitTask 硬件初始化任务 

void HardwareInitTask(void *argument)
{
        while(1)
        {
    vTaskSuspendAll();

    // RTC Wake
    if(HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, 2000, RTC_WAKEUPCLOCK_RTCCLK_DIV16) != HAL_OK)
    {
      Error_Handler();
    }
    // usart start
    HAL_UART_Receive_DMA(&huart1,(uint8_t*)HardInt_receive_str,25);
    __HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);

    // PWM Start
    HAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_3);

    // sys delay
    delay_init();
    // wait
    // delay_ms(1000);

    // power
    HWInterface.Power.Init();

    // key
    Key_Port_Init();

    // sensors
    uint8_t num = 3;
    while(num && HWInterface.AHT21.ConnectionError)
    {
      num--;
      HWInterface.AHT21.ConnectionError = HWInterface.AHT21.Init();
    }

    num = 3;
    while(num && HWInterface.Ecompass.ConnectionError)
    {
      num--;
      HWInterface.Ecompass.ConnectionError = HWInterface.Ecompass.Init();
    }
    if(!HWInterface.Ecompass.ConnectionError)
      HWInterface.Ecompass.Sleep();

    num = 3;
    while(num && HWInterface.Barometer.ConnectionError)
    {
      num--;
      HWInterface.Barometer.ConnectionError = HWInterface.Barometer.Init();
    }

    num = 3;
    while(num && HWInterface.IMU.ConnectionError)
    {
      num--;
      HWInterface.IMU.ConnectionError = HWInterface.IMU.Init();
      // Sensor_MPU_Erro = MPU_Init();
    }

    num = 3;
    while(num && HWInterface.HR_meter.ConnectionError)
    {
      num--;
      HWInterface.HR_meter.ConnectionError = HWInterface.HR_meter.Init();
    }
    if(!HWInterface.HR_meter.ConnectionError)
      HWInterface.HR_meter.Sleep();


    // EEPROM
    EEPROM_Init();
    if(!EEPROM_Check())
    {
      uint8_t recbuf;
      SettingGet(recbuf,0x10,2);
      if((recbuf!=0 && recbuf!=1) || (recbuf!=0 && recbuf!=1))
      {
      HWInterface.IMU.wrist_is_enabled = 0;
      ui_APPSy_EN = 0;
      }
      else
      {
      HWInterface.IMU.wrist_is_enabled = recbuf;
      ui_APPSy_EN = recbuf;
      }

      RTC_DateTypeDef nowdate;
      HAL_RTC_GetDate(&hrtc,&nowdate,RTC_FORMAT_BIN);

      SettingGet(recbuf,0x20,3);
      if(recbuf == nowdate.Date)
      {
      uint16_t steps=0;
      steps = recbuf&0x00ff;
      steps = steps<<8 | recbuf;
      if(!HWInterface.IMU.ConnectionError)
          dmp_set_pedometer_step_count((unsigned long)steps);
      }
    }


    // BLE
    KT6328_GPIO_Init();
    KT6328_Disable();

    //set the KT6328 BautRate 9600
    //default is 115200
    //printf("AT+CT01\r\n");

    // touch
    CST816_GPIO_Init();
    CST816_RESET();

    // lcd
    LCD_Init();
    LCD_Fill(0,0, LCD_W, LCD_H, BLACK);
    delay_ms(10);
    LCD_Set_Light(50);
    LCD_ShowString(72,LCD_H/2,(uint8_t*)"Welcome!", WHITE, BLACK, 24, 0);//12*6,16*8,24*12,32*16
    uint8_t lcd_buf_str;
    sprintf(lcd_buf_str, "OV-Watch V%d.%d.%d", watch_version_major(), watch_version_minor(), watch_version_patch());
    LCD_ShowString(34, LCD_H/2+48, (uint8_t*)lcd_buf_str, WHITE, BLACK, 24, 0);
    delay_ms(1000);
    LCD_Fill(0, LCD_H/2-24, LCD_W, LCD_H/2+49, BLACK);


    // ui
    // LVGL init
    lv_init();
    lv_port_disp_init();
    lv_port_indev_init();
    ui_init();

    xTaskResumeAll();
                vTaskDelete(NULL);
                osDelay(500);
        }
} 这里有根本外设的初始化、硬件驱动的初始化、LVGL的初始化。最后运行完之后,还会把自己删除,即调用 vTaskDelete(NULL); 可谓是居功至为。 
这里我讲几个我认为比力值得学习的地方。
1、usart start 串口收发

https://i-blog.csdnimg.cn/direct/1f6cdf71ffed496e90265b5c14cef186.png
我们这里使用USART 配合 DMA 来进行数据的收发。这里我们首先需要补充一下知识点,IDLE 停止以及DMA 发送/DMA+IDLE 接收。
IDLE 停止
IDLE,空闲的定义是:总线上在一个字节的时间内没有再接收到数据。
UART 的 IDLE 停止何时发生?RxD 引脚一开始就是空闲的啊,难道 IDLE 停止不停产生?
不是的。当我们使能 IDLE 停止后,它并不会立即产生,而是:至少收到 1 个数据后,发现在一个字节的时间里,都没有接收到新数据,才会产生 IDLE 停止。 
DMA传输 
我们使用 DMA 接收数据时,确实可以提高 CPU 的服从,但是“无法预知要接收多少数据”,而我们想尽快处置惩罚接收到的数据。怎么办?比如我想读取 100 字节的数据,但是接收到 60 字节后对方就不再发送数据了,怎么办?我们怎么判断数据传输中止了?可以使用IDLE 停止。
我们首先使用DMA进行接收,即去调用:
HAL_UART_Receive_DMA(&huart1,(uint8_t*)HardInt_receive_str,25); 
当我们串口收到数据之后,就会通过DMA去把数据传输到我们指定的内存数组,不需要我们没接收一个字节就进去一次停止,来对数据进行处置惩罚。
然后我们再使能IDLE空闲停止,即去调用:
 __HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);
这样子的话,我们就只有再接收到完整的数据之后,才会进入一次停止,来对数据一起进行处置惩罚,我们这里看一下 USART1_IRQHandler :
https://i-blog.csdnimg.cn/direct/b121042c79f24031ada49682fabc3392.png  
之后我们就可以根据 HardInt_uart_flag  这个标记位来判断是否接收到数据,然后去对应的任务机进行处置惩罚,调用关系如下图所示:
https://i-blog.csdnimg.cn/direct/09cc2effae7e4c48b75f141bf8d2bde2.png
2、sys delay 体系延时设置 

我们时钟树上规定了HCLK的时钟为100MHZ,如图所示:
https://i-blog.csdnimg.cn/direct/046a7cff8b9c4cf9a72287a6ccec5e42.png
        HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq));计算并设置 SysTick 的重装载值(Reload Value),使其按指定时间隔断触发停止。我们这里的SysTick 1ms进入一次停止。
        uwTickFreq 的值在 HAL_Init() 函数中通过 HAL_InitTick() 初始化。具体流程如下: 
1、HAL_Init() 调用 HAL_InitTick():
https://i-blog.csdnimg.cn/direct/e15defd06d87443c83acc2174a43abdc.png
2、HAL_InitTick() 设置 uwTickFreq,并配置时钟
https://i-blog.csdnimg.cn/direct/5a611c9eb1534f3989d2c619050f87f3.png
留意了,我们这里的时基是TIME1,并不是滴答定时器,这是由于,我们的FreeRTOS中,需要使用到Systick滴答定时器来提供任务的根本时基,如果hal库的延时才接纳Systick滴答定时器的话,会导致运行出现非常大的问题,所以我们再 HAL_InitTick() 配置的是TIME1。
3、LVGL初始化

这个不在细说,信任学过LVGL的,一眼就可以知道。
https://i-blog.csdnimg.cn/direct/b7aa6abc4db74d32aa164553fdce00bd.png
4、任务删除

这个任务运行完后,会把本任务删除,即调用 vTaskDelete(NULL);
2.5.3 ChargPageEnterTask 充电界面任务

https://i-blog.csdnimg.cn/direct/0dcd1450c1244d2d8cd95ee94777dc00.png
当TP4056M芯片的CHARG的电平发生跳变,意味着开始充电大概结束充电,这时间停止就会挂起HardInt_Charg_flag这个标记位,任务检测到之后,就会进行充电界面的切换。
2.5.4 SensorDataUpdateTask 传感器数值更新任务

void SensorDataUpdateTask(void *argument)
{
        uint8_t value_strbuf;
        uint8_t IdleBreakstr=0;
        while(1)
        {
                // Update the sens data showed in Home
                uint8_t HomeUpdataStr;
                if(osMessageQueueGet(HomeUpdata_MessageQueue, &HomeUpdataStr, NULL, 0)==osOK)
                {
                        //bat
                        uint8_t value_strbuf;

                        HWInterface.Power.power_remain = HWInterface.Power.BatCalculate();
                        if(HWInterface.Power.power_remain>0 && HWInterface.Power.power_remain<=100)
                        {}
                        else
                        {HWInterface.Power.power_remain = 0;}

                        //steps
                        if(!(HWInterface.IMU.ConnectionError))
                        {
                                HWInterface.IMU.Steps = HWInterface.IMU.GetSteps();
                        }

                        //temp and humi
                        if(!(HWInterface.AHT21.ConnectionError))
                        {
                                //temp and humi messure
                                float humi,temp;
                                HWInterface.AHT21.GetHumiTemp(&humi,&temp);
                                //check
                                if(temp>-10 && temp<50 && humi>0 && humi<100)
                                {
                                        // ui_EnvTempValue = (int8_t)temp;
                                        // ui_EnvHumiValue = (int8_t)humi;
                                        HWInterface.AHT21.humidity = humi;
                                        HWInterface.AHT21.temperature = temp;
                                }
                        }

                        //send data save message queue
                        uint8_t Datastr = 3;
                        osMessageQueuePut(DataSave_MessageQueue, &Datastr, 0, 1);

                }


                // SPO2 Page
                if(Page_Get_NowPage()->page_obj == &ui_SPO2Page)
                {
                        osMessageQueuePut(IdleBreak_MessageQueue, &IdleBreakstr, 0, 1);
                        //sensor wake up

                        //receive the sensor wakeup message, sensor wakeup
                        if(0)
                        {
                                //SPO2 messure
                        }
                }
                // Env Page
                else if(Page_Get_NowPage()->page_obj == &ui_EnvPage)
                {
                        osMessageQueuePut(IdleBreak_MessageQueue, &IdleBreakstr, 0, 1);
                        //receive the sensor wakeup message, sensor wakeup
                        if(!HWInterface.AHT21.ConnectionError)
                        {
                                //temp and humi messure
                                float humi,temp;
                                HWInterface.AHT21.GetHumiTemp(&humi,&temp);
                                //check
                                if(temp>-10 && temp<50 && humi>0 && humi<100)
                                {
                                        HWInterface.AHT21.temperature = (int8_t)temp;
                                        HWInterface.AHT21.humidity = (int8_t)humi;
                                }
                        }

                }
                // Compass page
                else if(Page_Get_NowPage()->page_obj == &ui_CompassPage)
                {
                        osMessageQueuePut(IdleBreak_MessageQueue, &IdleBreakstr, 0, 1);
                        //receive the sensor wakeup message, sensor wakeup
                        LSM303DLH_Wakeup();
                        //SPL_Wakeup();
                        //if the sensor is no problem
                        if(!HWInterface.Ecompass.ConnectionError)
                        {
                                //messure
                                int16_t Xa,Ya,Za,Xm,Ym,Zm;
                                LSM303_ReadAcceleration(&Xa,&Ya,&Za);
                                LSM303_ReadMagnetic(&Xm,&Ym,&Zm);
                                float temp = Azimuth_Calculate(Xa,Ya,Za,Xm,Ym,Zm)+0;//0 offset
                                if(temp<0)
                                {temp+=360;}
                                //check
                                if(temp>=0 && temp<=360)
                                {
                                        HWInterface.Ecompass.direction = (uint16_t)temp;
                                }
                        }
                        //if the sensor is no problem
                        if(!HWInterface.Barometer.ConnectionError)
                        {
                                //messure
                                float alti = Altitude_Calculate();
                                //check
                                if(1)
                                {
                                        HWInterface.Barometer.altitude = (int16_t)alti;
                                }
                        }
                }

                osDelay(500);
        }
}
这里就是判断当前再哪个界面,然后去获取对应界面需要的传感器数值,然后更新在 HWInterface
这个结构体 ,然后LVGL显示数值的时间,可以根据 HWInterface 来显示我们需要的数据在屏幕上。
2.5.5 ScrRenewTask界面革新任务 以及 KeyTask按键任务 

https://i-blog.csdnimg.cn/direct/0ef9524e08cb4c9cb572c24b2b2a91e2.png
https://i-blog.csdnimg.cn/direct/ce1996c88fbc47e69223af010c8b1844.png
        按键任务keytask,按键发生即发出信号量,调osMessageQueuePut(Key_MessageQueue, &keystr, 0, 1);和osMessageQueuePut(IdleBreak_MessageQueue, &IdleBreakstr, 0, 1);,一个是按键信号量,一个是空闲打断信号量; 这里需要补充,我们这里按键初始化的时间,使能了停止,这样子可以确保我们按下任意一个按键的时间,都可以退出我们的低功耗模式。
        屏幕切换任务user_ScrRenewTask.c,担当按键信号量,然后调用PageManager中的函数;如果是在传感器界面的时间,还会把对应传感器失能,来低沉我们功耗。
2.5.6 DataSaveTask 数据生存任务

https://i-blog.csdnimg.cn/direct/99a464f412e84c9091823ba8da65323f.png
        将我们的部门数据生存到EEPROM,如果就算失去电源,也能之后重新获取我们部门数据,包括抬腕叫醒、APP同步提醒(蓝牙去修改我们的时间)。
        然后去对比我们上一次存储的日期,如果日期不一样的话,说明新的一天来了,我们就用DMP库把我们的步数清0,如果日期是同一天的话,会继续把我们当他的日期和步数存到我们的EEPROM里面。
        这里我们只有在上电和从停止模式被叫醒之后,才会去实行一次这个任务,具体为什么,可以去通过队列PUT和GET的关系来知道。
2.5.7 MessageSendTask 串口数据收发任务

void MessageSendTask(void *argument)
{
        while(1)
        {
                if(HardInt_uart_flag)
                {
                        HardInt_uart_flag = 0;
                        uint8_t IdleBreakstr = 0;
                        osMessageQueuePut(IdleBreak_MessageQueue,&IdleBreakstr,NULL,1);
                        printf("RecStr:%s\r\n",HardInt_receive_str);
                        if(!strcmp(HardInt_receive_str,"OV"))
                        {
                                printf("OK\r\n");
                        }
                        else if(!strcmp(HardInt_receive_str,"OV+VERSION"))
                        {
                                printf("VERSION=V%d.%d.%d\r\n", VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH);
                        }
                        else if(!strcmp(HardInt_receive_str,"OV+SEND"))
                        {
                                HAL_RTC_GetTime(&hrtc,&(BLEMessage.nowtime),RTC_FORMAT_BIN);
                                HAL_RTC_GetDate(&hrtc,&BLEMessage.nowdate,RTC_FORMAT_BIN);
                                BLEMessage.humi = HWInterface.AHT21.humidity;
                                BLEMessage.temp = HWInterface.AHT21.temperature;
                                BLEMessage.HR = HWInterface.HR_meter.HrRate;
                                BLEMessage.SPO2 = HWInterface.HR_meter.SPO2;
                                BLEMessage.stepNum = HWInterface.IMU.Steps;

                                printf("data:%2d-%02d\r\n",BLEMessage.nowdate.Month,BLEMessage.nowdate.Date);
                                printf("time:%02d:%02d:%02d\r\n",BLEMessage.nowtime.Hours,BLEMessage.nowtime.Minutes,BLEMessage.nowtime.Seconds);
                                printf("humidity:%d%%\r\n",BLEMessage.humi);
                                printf("temperature:%d\r\n",BLEMessage.temp);
                                printf("Heart Rate:%d%%\r\n",BLEMessage.HR);
                                printf("SPO2:%d%%\r\n",BLEMessage.SPO2);
                                printf("Step today:%d\r\n",BLEMessage.stepNum);
                        }
                        //set time//OV+ST=20230629125555
                        else if(strlen(HardInt_receive_str)==20)
                        {
                                uint8_t cmd;
                                memset(cmd,0,sizeof(cmd));
                                StrCMD_Get(HardInt_receive_str,cmd);
                                if(ui_APPSy_EN && !strcmp(cmd,"OV+ST"))
                                {
                                        TimeFormat_Get(HardInt_receive_str);
                                }
                        }
                        memset(HardInt_receive_str,0,sizeof(HardInt_receive_str));
                }
                osDelay(1000);
        }
} 当HardInt_uart_flag 这个标记位被置1的时间,说明我们接收到了蓝牙数据,然后我们对他进行一系列处置惩罚,而且回复它。


2.5.8 IdleEnterTask 空闲任务 以及 StopEnterTask 停止模式任务

void IdleEnterTask(void *argument)
{
        uint8_t Idlestr=0;
        uint8_t IdleBreakstr=0;
        while(1)
        {
                //light get dark
                if(osMessageQueueGet(Idle_MessageQueue,&Idlestr,NULL,1)==osOK)
                {
                        LCD_Set_Light(5);
                }
                //resume light if light got dark and idle state breaked by key pressing or screen touching
                if(osMessageQueueGet(IdleBreak_MessageQueue,&IdleBreakstr,NULL,1)==osOK)
                {
                        IdleTimerCount = 0;
                        LCD_Set_Light(ui_LightSliderValue);
                }
                osDelay(10);
        }
}

/**
* @briefenter the stop mode and resume
* @paramargument: Not used
* @retval None
*/
void StopEnterTask(void *argument)
{
        uint8_t Stopstr;
        uint8_t HomeUpdataStr;
        uint8_t Wrist_Flag=0;
        while(1)
        {
                if(osMessageQueueGet(Stop_MessageQueue,&Stopstr,NULL,0)==osOK)
                {

                        /****************************** your sleep operations *****************************/
                        sleep:
                        IdleTimerCount = 0;

                        //sensors

                        //usart
                        HAL_UART_MspDeInit(&huart1);

                        //lcd
                        LCD_RES_Clr();
                        LCD_Close_Light();
                        //touch
                        CST816_Sleep();

                        /***********************************************************************************/

                        vTaskSuspendAll();
                        //Disnable Watch Dog
                        WDOG_Disnable();
                        //systick int
                        CLEAR_BIT(SysTick->CTRL, SysTick_CTRL_TICKINT_Msk);
                        //enter stop mode
                        HAL_PWR_EnterSTOPMode(PWR_MAINREGULATOR_ON,PWR_STOPENTRY_WFI);

                        //here is the sleep period

                        //resume run mode and reset the sysclk
                        SET_BIT(SysTick->CTRL, SysTick_CTRL_TICKINT_Msk);
                        HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq));
                        SystemClock_Config();
                        WDOG_Feed();
                        xTaskResumeAll();

                        /****************************** your wakeup operations ******************************/

                        //MPU Check
                        if(HWInterface.IMU.wrist_is_enabled)
                        {
                                uint8_t hor;
                                hor = MPU_isHorizontal();
                                if(hor && HWInterface.IMU.wrist_state == WRIST_DOWN)
                                {
                                        HWInterface.IMU.wrist_state = WRIST_UP;
                                        Wrist_Flag = 1;
                                        //resume, go on
                                }
                                else if(!hor && HWInterface.IMU.wrist_state == WRIST_UP)
                                {
                                        HWInterface.IMU.wrist_state = WRIST_DOWN;
                                        IdleTimerCount= 0;
                                        goto sleep;
                                }
                        }

                        //
                        if(!KEY1 || KEY2 || HardInt_Charg_flag || Wrist_Flag)
                        {
                                Wrist_Flag = 0;
                                //resume, go on
                        }
                        else
                        {
                                IdleTimerCount= 0;
                                goto sleep;
                        }

                        //usart
                        HAL_UART_MspInit(&huart1);
                        //lcd
                        LCD_Init();
                        LCD_Set_Light(ui_LightSliderValue);
                        //touch
                        CST816_Wakeup();
                        //check if is Charging
                        if(ChargeCheck())
                        {HardInt_Charg_flag = 1;}
                        //send the Home Updata message
                        osMessageQueuePut(HomeUpdata_MessageQueue, &HomeUpdataStr, 0, 1);

                        /**************************************************************************************/

                }
                osDelay(100);
        }
}

void IdleTimerCallback(void *argument)
{
        IdleTimerCount+=1;
        //make sure the LightOffTime<TurnOffTime
        if(IdleTimerCount == (ui_LTimeValue*10))
        {
                uint8_t Idlestr=0;
                //send the Light off message
                osMessageQueuePut(Idle_MessageQueue, &Idlestr, 0, 1);

        }
        if(IdleTimerCount == (ui_TTimeValue*10))
        {
                uint8_t Stopstr = 1;
                IdleTimerCount= 0;
                //send the Stop message
                osMessageQueuePut(Stop_MessageQueue, &Stopstr, 0, 1);
        }
}

 我们一开始任务初始化的时间,创建了一个软件定时器,100ms进入一次:
https://i-blog.csdnimg.cn/direct/8c20dc7db0f741a9aed6db529f01a9d7.png
        当我们检测到IdleTimerCount超过肯定时间,就会给空闲任务和停止任务发送信息量,来实行对应的任务。
        当空闲任务发现时间到了之后,就会把我们的屏幕亮度低沉。
        停止模式发现时间到了之后,就会进入停止模式,但是这里留意的是,我们这里有两种方法叫醒停止模式,第一种就是按键触发的方式,我们通过按键按下,触发停止来叫醒,第二种是RTC停止叫醒的方式,我们之前RTC停止设置的是200ms进入一次停止,所以这里,即使你什么都不做,也不会不断被叫醒,然后继续进入停止模式,但是这里RTC叫醒到我们睡眠的这段期间内,我们会通过MPU6050来检测上一次的姿态和这一次的姿态,来判断是否抬腕,如果发现抬腕的话,直接退出停止任务。而且进去停止模式的时间,我们会失能外部看门狗,来防止没有喂狗而被不停复位。
2.5.9 WDOGFeedTask 看门狗任务

https://i-blog.csdnimg.cn/direct/0bc1b5dfdf09404680b01934bf17e308.png
 通过手动翻转GPIO电平,进行外部看门狗,如果没有定时喂狗,则会复位。
 2.5.10 LvHandlerTask任务

https://i-blog.csdnimg.cn/direct/58d4aae35bdd47ff83e35ca9618f15ca.png
尽管这个任务的源码非常的少,但是这个任务实在是最最复杂的,这里设计了LVGL的根本知识。之后会单独拿一篇来进行解说。 
三、总结

这里我们把该项目标软件部门和逻辑都解说了一遍,这里还没有去细讲LVGL部门,由于LVGL部门相对较为独立,可以之后单独开一篇来讲,下一篇我将会去解说这个项目标LVGL部门。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 基于FreeRTOS和LVGL的多功能低功耗智能手表(APP篇)