基于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工程架构

 

├─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可以去执行其它任务;

2、IIC主要用来跟Back板各个传感器进行通信,传感器都挂在一个总线上的,这里我们不需要去初始化CubeMX,因为我们这里采用的是软件I2C。 

3、TIM主要是提供时基,另外一个就是给LCD调节背光;

4、ADC只接了一个电池的分压,进行电池电压采样,预估剩余电量;

5、USART接了蓝牙,方便进行IAP和与手机和电脑的助手通信。

6、RTC实时时钟,提供秒、分、时、日期(日/月/年)和星期的计时。

同时,我们FreeRTOS的移植也是直接使用我们的CubeMX的,我相信大家都是很熟悉FreeRTOS的,这里我们只需要使能FREERTOS,interface选择CMSIS_V2,其他默认即可。

2.3 板载驱动BSP 

这里我们先简单的去介绍一下BSP,具体的话,大家可以仔细的去看看我们的源码,大家只需要知道,BSP帮我封装好了板载的驱动,我们之后需要去和板载各个驱动通讯的时候,只需要直接调用BSP即可。

这里我们简述一下:

1、LCD驱动 

这里有个地方非常的精妙,就是利用了DMA配合SPI发送,可以大大提高我们CPU的利用率。

单字节的发送,我们直接采用SPI直接发送,因为这个速度非常的快,我们配置DMA去发送的话,反而还浪费时间去配置DMA,得不偿失。

        这里我们一次发送多个字节(固定死在对应数值),我们采用SPI+DMA的形式去发送,但是我们最后一个while(__HAL_DMA_GET_COUNTER(&hdma_spi1_tx)!=0); 最后还是去等待DMA传输完毕我们在进行下一步操作,这里不免有一个疑问?我们使用DMA传输的话,就是为了去解放我们的CPU,这里死等的话意义在哪里呢?

        要先明白这个道理的话,我们得去看看LVGL任务的优先级,如图所示:

这里可以看到,LVGL的任务优先级还是很低的,意味着,我们刷新屏幕的优先级也是最低的,会被经常打断,所以说,我们很大可能在死等的过程当中,被其他任务打断,这个时候,我们切换到其他的任务当中去,此时DMA依旧还在传输,这样子,也算是解放了我们的CPU去干其他的事情。 

2、各个I2C传感器驱动

        我这里只介绍一下I2C的流程,具体各个I2C传感器我这里不去细讲,因为这不是我们的重点,因为BSP这些底层的硬件驱动很多都是厂家给我们提供好的,我们只需要知道就行,无需去具体的了解。 

我们看看I2C驱动有啥函数:

  

首先有一个结构体,我们每个传感器设备都需要创建一个结构体,这样子我们之后进行调用发送起始信号、停止信号、数据发送都可以通过这个结构体的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例子如下:

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_2void 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 1void 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的引脚为高电平的时候,说明此时正在进行充电,那么屏幕就会刷新出我们的充电界面。

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 keyif(!KEY1)key_down = 1;if(KEY2)key_down = 2;if(key_down) key_up = 0;}if ( key_down && (KEY1 && (!KEY2)) ){osDelay(3);//ensure the keyif(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中进行配置了。

2.4 管理函数

这里存放了三个管理文件,我们这里去一个个给大家进行介绍。

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[st->Top_Point++] = 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[st->Top_Point++] = in;return 0;
}uint8_t NumStackDel(NumStack_t * st)
{if(st->Top_Point == 0){return -1;}st->data[st->Top_Point--] = 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[st->Top_Point++] = in;return 0;
}uint8_t SymStackDel(SymStack_t * st)
{if(st->Top_Point == 0){return -1;}st->data[st->Top_Point--] = 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[symstack->Top_Point-1] == '+')temp.number = (numstack->data[numstack->Top_Point-2]) + (numstack->data[numstack->Top_Point-1]);else if(symstack->data[symstack->Top_Point-1] == '-')temp.number = (numstack->data[numstack->Top_Point-2]) - (numstack->data[numstack->Top_Point-1]);else if(symstack->data[symstack->Top_Point-1] == '*')temp.number = (numstack->data[numstack->Top_Point-2]) * (numstack->data[numstack->Top_Point-1]);else if(symstack->data[symstack->Top_Point-1] == '/')temp.number = (numstack->data[numstack->Top_Point-2]) / (numstack->data[numstack->Top_Point-1]);//运算前两数出栈,运算结果数入栈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[0]>'9' || str[0]<'0')return 1;//erroint i;for(i=0;i<strlen;i++){if(str[i]=='.'){temp.datatype = POINT_TYPE;if(temp_pre.datatype == NUMBER_TYPE){}else{return 2;}temp_pre = temp;}if(str[i]<='9' && str[i]>='0'){//溢出报错if(NumStack->Top_Point>CAL_DEPTH || SymStack->Top_Point>CAL_DEPTH){return 3;}//读取当前的字符到temp中temp.datatype = NUMBER_TYPE;temp.number = (str[i] - '0');temp.symbol = NULL;//如果为连续数字,需要进行进位,将数字栈顶读出进位,再加上现在位,再入栈if(temp_pre.datatype == NUMBER_TYPE){if(!NumBehindPoint_Flag){temp.number += NumStack->data[NumStack->Top_Point-1] * 10;}else{NumBehindPoint_Flag += 1;char i = NumBehindPoint_Flag;while(i--){temp.number /= 10;}temp.number += NumStack->data[NumStack->Top_Point-1];}NumStackDel(NumStack);NumStackPut(NumStack,temp.number);}//当前数字刚好是小数点后一位else if(temp_pre.datatype == POINT_TYPE){NumBehindPoint_Flag = 1;temp.number /= 10;temp.number += NumStack->data[NumStack->Top_Point-1];NumStackDel(NumStack);NumStackPut(NumStack,temp.number);}//前一位不是数字或小数点,现在读取的这一位是数字,直接入栈else{NumStackPut(NumStack,temp.number);}temp_pre = temp;}else if(str[i] == '+' || str[i] == '-' || str[i] == '*' || str[i] == '/'){//溢出报错if(NumStack->Top_Point>CAL_DEPTH || SymStack->Top_Point>CAL_DEPTH){return 4;}//读取当前的字符到temp中temp.datatype = SYMBOL_TYPE;temp.symbol = str[i];temp.number = 0;NumBehindPoint_Flag = 0;//小数点计算已经结束//重复输入了运算符号if(temp_pre.datatype == SYMBOL_TYPE){return 5 ;//erro}else{if((!SymStack_isEmpty(SymStack)) && SymisHighPriority(SymStack->data[SymStack->Top_Point-1],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 allNumStackClear(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")会被视为错误,这是正确的。但如果是负数的情况,这里就会导致错误,所以代码不支持负数的运算。前面做的所有都是为了这个函数进行铺垫,我们可以在函数调用关系看到:

优先级判断(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仿真文件夹中,如下所示,即完成了仿真的移植。 

当然,MDK工程和LVGL仿真工程的移植过程需要改一个东西,就是HWDataAccess.h中的使能: 

如果是在仿真中,就把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_LCDLCD_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[MAX_DEPTH];uint8_t top;
} PageStack_t;extern PageStack_t PageStack;

再看到PageManager.c,栈的初始化还有push和pop操作就不再赘述了,在pop函数中,除了将top减1,还调用了页面deinit函数,负责反初始化当前页面,这里我们不是直接删除当前页面,是将当前界面对应的LVGL软件定时器关闭掉。

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 timerui_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);/*** @brief  FreeRTOS initialization* @param  None* @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, ... *//* add  others ... */uint8_t HomeUpdataStr;osMessageQueuePut(HomeUpdata_MessageQueue, &HomeUpdataStr, 0, 1);}/*** @brief  FreeRTOS Tick Hook, to increase the LVGL tick* @param  None* @retval None*/
void TaskTickHook(void)
{//to increase the LVGL ticklv_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;
}/*** @brief  LVGL Handler task, to run the lvgl* @param  argument: 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 0osMessageQueuePut(IdleBreak_MessageQueue, &IdleBreakstr, 0, 0);}lv_task_handler();osDelay(1);}
}/*** @brief  Watch Dog Feed task* @param  argument: Not used* @retval None*/
void WDOGFeedTask(void *argument)
{//owdgWDOG_Port_Init();while(1){WDOG_Feed();WDOG_Enable();osDelay(100);}
}

        注册各个任务,分配空间,注册一些信号量,任务的汇总可以看上面的源码,我之后将一个个任务的去进行讲解。

        同时也创建了一个软件定时器,用于记录空闲时间,即用户没有操作过长就会发出idle信号,idle任务读取到这个队列之后,就会进行一些处理,如果idle过长,就会发出STOP信号,STOP任务读取到这个队列之后,进入睡眠。

        并且LVGL的时基提供也在这个文件夹,以及我们计时功能的时间也在这里进行提供,我们这里去看一下。

本质上是利用我们FreeRTOS的钩子函数,void vApplicationTickHook( void ); 

vApplicationTickHook()函数的运行周期由configTICK_RATE_HZ决定,一般都设置为1ms。

 

2.5.2 HardwareInitTask 硬件初始化任务 

void HardwareInitTask(void *argument)
{while(1){vTaskSuspendAll();// RTC Wakeif(HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, 2000, RTC_WAKEUPCLOCK_RTCCLK_DIV16) != HAL_OK){Error_Handler();}// usart startHAL_UART_Receive_DMA(&huart1,(uint8_t*)HardInt_receive_str,25);__HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);// PWM StartHAL_TIM_PWM_Start(&htim3,TIM_CHANNEL_3);// sys delaydelay_init();// wait// delay_ms(1000);// powerHWInterface.Power.Init();// keyKey_Port_Init();// sensorsuint8_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();// EEPROMEEPROM_Init();if(!EEPROM_Check()){uint8_t recbuf[3];SettingGet(recbuf,0x10,2);if((recbuf[0]!=0 && recbuf[0]!=1) || (recbuf[1]!=0 && recbuf[1]!=1)){HWInterface.IMU.wrist_is_enabled = 0;ui_APPSy_EN = 0;}else{HWInterface.IMU.wrist_is_enabled = recbuf[0];ui_APPSy_EN = recbuf[1];}RTC_DateTypeDef nowdate;HAL_RTC_GetDate(&hrtc,&nowdate,RTC_FORMAT_BIN);SettingGet(recbuf,0x20,3);if(recbuf[0] == nowdate.Date){uint16_t steps=0;steps = recbuf[1]&0x00ff;steps = steps<<8 | recbuf[2];if(!HWInterface.IMU.ConnectionError)dmp_set_pedometer_step_count((unsigned long)steps);}}// BLEKT6328_GPIO_Init();KT6328_Disable();//set the KT6328 BautRate 9600//default is 115200//printf("AT+CT01\r\n");// touchCST816_GPIO_Init();CST816_RESET();// lcdLCD_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*16uint8_t lcd_buf_str[17];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 initlv_init();lv_port_disp_init();lv_port_indev_init();ui_init();xTaskResumeAll();vTaskDelete(NULL);osDelay(500);}
}

这里有基本外设的初始化、硬件驱动的初始化、LVGL的初始化。最后运行完之后,还会把自己删除,即调用 vTaskDelete(NULL); 可谓是居功至为。 

这里我讲几个我认为比较值得学习的地方。

1、usart start 串口收发

我们这里使用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 :

  

之后我们就可以根据 HardInt_uart_flag  这个标志位来判断是否接收到数据,然后去对应的任务机进行处理,调用关系如下图所示:

2、sys delay 系统延时设置 

我们时钟树上规定了HCLK的时钟为100MHZ,如图所示:

        HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq));计算并设置 SysTick 的重装载值(Reload Value),使其按指定时间间隔触发中断。我们这里的SysTick 1ms进入一次中断。

        uwTickFreq 的值在 HAL_Init() 函数中通过 HAL_InitTick() 初始化。具体流程如下: 

1、HAL_Init() 调用 HAL_InitTick():

2、HAL_InitTick() 设置 uwTickFreq,并配置时钟

注意了,我们这里的时基是TIME1,并不是滴答定时器,这是因为,我们的FreeRTOS中,需要使用到Systick滴答定时器来提供任务的基本时基,如果hal库的延时才采用Systick滴答定时器的话,会导致运行出现非常大的问题,所以我们再 HAL_InitTick() 配置的是TIME1。

3、LVGL初始化

这个不在细说,相信学过LVGL的,一眼就可以知道。

4、任务删除

这个任务运行完后,会把本任务删除,即调用 vTaskDelete(NULL);

2.5.3 ChargPageEnterTask 充电界面任务

当TP4056M芯片的CHARG的电平发生跳变,意味着开始充电或者结束充电,这时候中断就会挂起HardInt_Charg_flag这个标志位,任务检测到之后,就会进行充电界面的切换。

2.5.4 SensorDataUpdateTask 传感器数值更新任务

void SensorDataUpdateTask(void *argument)
{uint8_t value_strbuf[6];uint8_t IdleBreakstr=0;while(1){// Update the sens data showed in Homeuint8_t HomeUpdataStr;if(osMessageQueueGet(HomeUpdata_MessageQueue, &HomeUpdataStr, NULL, 0)==osOK){//batuint8_t value_strbuf[5];HWInterface.Power.power_remain = HWInterface.Power.BatCalculate();if(HWInterface.Power.power_remain>0 && HWInterface.Power.power_remain<=100){}else{HWInterface.Power.power_remain = 0;}//stepsif(!(HWInterface.IMU.ConnectionError)){HWInterface.IMU.Steps = HWInterface.IMU.GetSteps();}//temp and humiif(!(HWInterface.AHT21.ConnectionError)){//temp and humi messurefloat humi,temp;HWInterface.AHT21.GetHumiTemp(&humi,&temp);//checkif(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 queueuint8_t Datastr = 3;osMessageQueuePut(DataSave_MessageQueue, &Datastr, 0, 1);}// SPO2 Pageif(Page_Get_NowPage()->page_obj == &ui_SPO2Page){osMessageQueuePut(IdleBreak_MessageQueue, &IdleBreakstr, 0, 1);//sensor wake up//receive the sensor wakeup message, sensor wakeupif(0){//SPO2 messure}}// Env Pageelse if(Page_Get_NowPage()->page_obj == &ui_EnvPage){osMessageQueuePut(IdleBreak_MessageQueue, &IdleBreakstr, 0, 1);//receive the sensor wakeup message, sensor wakeupif(!HWInterface.AHT21.ConnectionError){//temp and humi messurefloat humi,temp;HWInterface.AHT21.GetHumiTemp(&humi,&temp);//checkif(temp>-10 && temp<50 && humi>0 && humi<100){HWInterface.AHT21.temperature = (int8_t)temp;HWInterface.AHT21.humidity = (int8_t)humi;}}}// Compass pageelse if(Page_Get_NowPage()->page_obj == &ui_CompassPage){osMessageQueuePut(IdleBreak_MessageQueue, &IdleBreakstr, 0, 1);//receive the sensor wakeup message, sensor wakeupLSM303DLH_Wakeup();//SPL_Wakeup();//if the sensor is no problemif(!HWInterface.Ecompass.ConnectionError){//messureint16_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 offsetif(temp<0){temp+=360;}//checkif(temp>=0 && temp<=360){HWInterface.Ecompass.direction = (uint16_t)temp;}}//if the sensor is no problemif(!HWInterface.Barometer.ConnectionError){//messurefloat alti = Altitude_Calculate();//checkif(1){HWInterface.Barometer.altitude = (int16_t)alti;}}}osDelay(500);}
}

这里就是判断当前再哪个界面,然后去获取对应界面需要的传感器数值,然后更新在 HWInterface

这个结构体 ,然后LVGL显示数值的时候,可以根据 HWInterface 来显示我们需要的数据在屏幕上。

2.5.5 ScrRenewTask界面刷新任务 以及 KeyTask按键任务 

        按键任务keytask,按键发生即发出信号量,调osMessageQueuePut(Key_MessageQueue, &keystr, 0, 1);和osMessageQueuePut(IdleBreak_MessageQueue, &IdleBreakstr, 0, 1);,一个是按键信号量,一个是空闲打断信号量; 这里需要补充,我们这里按键初始化的时候,使能了中断,这样子可以确保我们按下任意一个按键的时候,都可以退出我们的低功耗模式。

        屏幕切换任务user_ScrRenewTask.c,接受按键信号量,然后调用PageManager中的函数;如果是在传感器界面的时候,还会把对应传感器失能,来降低我们功耗。

2.5.6 DataSaveTask 数据保存任务

        将我们的部分数据保存到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=20230629125555else if(strlen(HardInt_receive_str)==20){uint8_t cmd[10];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 darkif(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 touchingif(osMessageQueueGet(IdleBreak_MessageQueue,&IdleBreakstr,NULL,1)==osOK){IdleTimerCount = 0;LCD_Set_Light(ui_LightSliderValue);}osDelay(10);}
}/*** @brief  enter the stop mode and resume* @param  argument: 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//usartHAL_UART_MspDeInit(&huart1);//lcdLCD_RES_Clr();LCD_Close_Light();//touchCST816_Sleep();/***********************************************************************************/vTaskSuspendAll();//Disnable Watch DogWDOG_Disnable();//systick intCLEAR_BIT(SysTick->CTRL, SysTick_CTRL_TICKINT_Msk);//enter stop modeHAL_PWR_EnterSTOPMode(PWR_MAINREGULATOR_ON,PWR_STOPENTRY_WFI);//here is the sleep period//resume run mode and reset the sysclkSET_BIT(SysTick->CTRL, SysTick_CTRL_TICKINT_Msk);HAL_SYSTICK_Config(SystemCoreClock / (1000U / uwTickFreq));SystemClock_Config();WDOG_Feed();xTaskResumeAll();/****************************** your wakeup operations ******************************///MPU Checkif(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;}//usartHAL_UART_MspInit(&huart1);//lcdLCD_Init();LCD_Set_Light(ui_LightSliderValue);//touchCST816_Wakeup();//check if is Chargingif(ChargeCheck()){HardInt_Charg_flag = 1;}//send the Home Updata messageosMessageQueuePut(HomeUpdata_MessageQueue, &HomeUpdataStr, 0, 1);/**************************************************************************************/}osDelay(100);}
}void IdleTimerCallback(void *argument)
{IdleTimerCount+=1;//make sure the LightOffTime<TurnOffTimeif(IdleTimerCount == (ui_LTimeValue*10)){uint8_t Idlestr=0;//send the Light off messageosMessageQueuePut(Idle_MessageQueue, &Idlestr, 0, 1);}if(IdleTimerCount == (ui_TTimeValue*10)){uint8_t Stopstr = 1;IdleTimerCount  = 0;//send the Stop messageosMessageQueuePut(Stop_MessageQueue, &Stopstr, 0, 1);}
}

 我们一开始任务初始化的时候,创建了一个软件定时器,100ms进入一次:

        当我们检测到IdleTimerCount超过一定时间,就会给空闲任务和停止任务发送信息量,来执行对应的任务。

        当空闲任务发现时间到了之后,就会把我们的屏幕亮度降低。

        停止模式发现时间到了之后,就会进入停止模式,但是这里注意的是,我们这里有两种方法唤醒停止模式,第一种就是按键触发的方式,我们通过按键按下,触发中断来唤醒,第二种是RTC中断唤醒的方式,我们之前RTC中断设置的是200ms进入一次中断,所以这里,即使你什么都不做,也不会不断被唤醒,然后继续进入停止模式,但是这里RTC唤醒到我们睡眠的这段期间内,我们会通过MPU6050来检测上一次的姿态和这一次的姿态,来判断是否抬腕,如果发现抬腕的话,直接退出停止任务。并且进去停止模式的时候,我们会失能外部看门狗,来防止没有喂狗而被一直复位。

2.5.9 WDOGFeedTask 看门狗任务

 通过手动翻转GPIO电平,进行外部看门狗,如果没有定时喂狗,则会复位。

 2.5.10 LvHandlerTask任务

尽管这个任务的源码非常的少,但是这个任务其实是最最复杂的,这里设计了LVGL的基本知识。之后会单独拿一篇来进行讲解。 

三、总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/76985.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【初阶数据结构】——算法复杂度

一、前言 1、数据结构是什么&#xff1f; 数据结构(Data Structure)是计算机存储、组织数据的⽅式&#xff0c;指相互之间存在⼀种或多种特定关系的数 据元素的集合。没有⼀种单⼀的数据结构对所有⽤途都有⽤&#xff0c;所以我们要学各式各样的数据结构&#xff0c; 如&…

记录 | Pycharm中如何调用Anaconda的虚拟环境

目录 前言一、步骤Step1 查看anaconda 环境名Step2 Python项目编译器更改 更新时间 前言 参考文章&#xff1a; 参考视频&#xff1a;如何在pycharm中使用Anaconda创建的python环境 自己的感想 这里使用的Pycharm 2024专业版的。我所使用的Pycharm专业版位置&#xff1a;【仅用…

linux如何用关键字搜索日志

在 Linux 系统中搜索日志是日常运维的重要工作&#xff0c;以下是几种常用的关键字搜索日志方法&#xff1a; 1. 基础 grep 搜索 bash 复制 # 基本搜索&#xff08;区分大小写&#xff09; grep "keyword" /var/log/syslog# 忽略大小写搜索 grep -i "error&…

K-均值聚类机器学习算法的优缺点

K-均值聚类是一种常用的无监督学习算法&#xff0c;用于将具有相似特征的数据点聚集到一起。以下是K-均值聚类算法的步骤及其优缺点&#xff1a; K-均值聚类算法步骤&#xff1a; 初始化&#xff1a;随机选择K个点作为初始的聚类中心。分配数据点&#xff1a;将每个数据点分配…

AI驱动SEO关键词实战策略

内容概要 AI驱动的SEO关键词优化体系通过技术融合实现了策略升级。该框架以语义理解模型为基础&#xff0c;结合实时流量监测与行业数据库&#xff0c;构建了包含关键词挖掘、竞争评估、内容适配三大核心模块的闭环系统。通过自然语言处理&#xff08;NLP&#xff09;技术解析…

Golang|在线排查协程泄漏

根据我们的代码&#xff0c;前5毫秒内&#xff0c;每隔1毫秒就会来一个请求&#xff0c;5毫秒之后由于前面的协程执行完&#xff0c;后面又会来新的协程&#xff0c;所以协程数目会保持稳定但是代码一运行&#xff0c;协程数量一直增长&#xff0c;发生了协程泄漏 我们可以list…

Java项目之基于ssm的QQ村旅游网站的设计(源码+文档)

项目简介 QQ村旅游网站实现了以下功能&#xff1a; 管理员权限操作的功能包括管理景点路线&#xff0c;板块信息&#xff0c;留言板信息&#xff0c;旅游景点信息&#xff0c;酒店信息&#xff0c;对景点留言&#xff0c;景点路线留言以及酒店留言信息等进行回复&#xff0c;…

高级语言调用C接口(四)结构体(2)-Python

这个专栏好久没有更新了&#xff0c;主要是坑开的有点大&#xff0c;也不知道怎么填&#xff0c;涉及到的开发语言比较多&#xff0c;写起来比较累&#xff0c;需要看的人其实并不多&#xff0c;只能说&#xff0c;慢慢填吧&#xff0c;中间肯定还会插很多别的东西&#xff0c;…

JAVA 主流微服务常用框架及简介

Java微服务架构的优势在于其轻量级、高效资源利用&#xff0c;支持快速开发与灵活部署&#xff0c;拥有强大的生态系统与跨平台兼容性&#xff0c;能够实现高性能与稳定性&#xff0c;并允许独立扩展与技术栈多样性。然而&#xff0c;其劣势也不容忽视&#xff0c;包括架构复杂…

儿童后期至青少年早期脑网络隔离增强的发育机制研究

目录 1 研究背景 2 研究方法 2.1 纵向数据集 2.2 图像预处理 2.3 个体化区域放射组学相似网络构建 2.4 分离度&#xff08;模块化&#xff09;度量 2.5 分离度指数发育变化的建模 2.6 分离指数与认知表现的相关性分析 2.7 成像转录组分析 3 研究结果 3.1 三个尺度上…

redis 内存中放哪些数据?

在 Java 开发中,Redis 作为高性能内存数据库,通常用于存储高频访问、低延迟要求、短期有效或需要原子操作的数据。以下是 Redis 内存中常见的数据类型及对应的使用场景,适合面试回答: 1. 缓存数据(高频访问,降低数据库压力) 用户会话(Session):存储用户登录状态、临时…

Spring AOP 学习笔记 之 Advice详解

学习材料&#xff1a;https://docs.spring.io/spring-framework/reference/core/aop/ataspectj/advice.html 1. 什么是 Advice&#xff08;通知&#xff09; 定义&#xff1a;Advice 是 AOP 的核心概念之一&#xff0c;表示在特定的连接点&#xff08;Join Point&#xff09;上…

数智读书笔记系列029 《代数大脑:揭秘智能背后的逻辑》

《代数大脑:揭秘智能背后的逻辑》书籍简介 作者简介 加里F. 马库斯(Gary F. Marcus)是纽约大学心理学荣休教授、人工智能企业家,曾创立Geometric Intelligence(后被Uber收购)和Robust.AI公司。他在神经科学、语言学和人工智能领域发表了大量论文,并著有《重启AI》等多部…

如何看电脑的具体配置?

李升伟 整理 要查看电脑的具体配置&#xff0c;可以通过系统工具、命令行工具或第三方软件实现&#xff0c;以下是具体方法&#xff1a; 一、系统自带工具查看&#xff08;无需安装软件&#xff09; Windows系统&#xff1a; 系统设置&#xff1a; 右键点击桌面“此电脑”…

开源TTS项目GPT-SoVITS,支持跨语言合成、支持多语言~

简介 GPT-SoVITS 是一个开源的文本转语音&#xff08;TTS&#xff09;项目&#xff0c;旨在通过少量语音数据实现高质量的语音合成。其核心理念是将基于变换器的模型&#xff08;如 GPT&#xff09;与语音合成技术&#xff08;如 SoVITS&#xff0c;可能指“唱歌语音合成”&am…

D1084低功耗LDO稳压器:技术解析与应用设计

引言 在现代电子设计中&#xff0c;低功耗和高效率是至关重要的。D1084是一款5A低功耗低压差线性稳压器&#xff08;LDO&#xff09;&#xff0c;以其出色的负载调节能力和快速瞬态响应&#xff0c;成为低电压微处理器应用的理想选择。本文将深入解析D1084的技术特性和应用设计…

Log4j详解:Java日志系统全指南

文章目录 1. 日志系统简介1.1 什么是日志1.2 为什么使用日志框架1.3 Java中的常见日志框架 2. Log4j概述2.1 Log4j简介2.2 Log4j的版本历史2.3 Log4j与Log4j 2的主要区别 3. Log4j架构与核心组件3.1 Logger&#xff08;日志记录器&#xff09;3.2 日志级别&#xff08;Level&am…

【信息系统项目管理师】高分论文:论信息系统项目的整合管理(银行数据仓库项目)

更多内容请见: 备考信息系统项目管理师-专栏介绍和目录 文章目录 正文一、制定项目章程二、制定项目管理计划三、指导和管理项目的实施四、管理项目知识五、监控项目工作六、实施整体变更控制七、结束项目或阶段正文 2023年6月,我以项目经理的身份,参加了 xx银行xx省分行数…

sql server 预估索引大小

使用deepseek工具预估如下&#xff1a; 问题&#xff1a; 如果建立一个数据类型是datetime的索引&#xff0c;需要多大的空间&#xff1f; 回答&#xff1a; 如果建立一个数据类型是 datetime 的索引&#xff0c;索引的大小取决于以下因素&#xff1a; 索引键的大小&#…

干货 | 高性能 Nginx 优化配置总结

文章目录 一、前言二、配置优化2.1 并发处理架构优化2.1.1 工作进程配置2.1.2 事件驱动模型 2.2 传输效率优化2.2.1 零拷贝技术2.2.2 长连接复用 2.3 缓存体系构建2.3.1 文件描述符缓存2.3.2 代理缓存2.3.3 静态资源缓存 2.4 协议层深度优化2.4.1 HTTP/2 支持2.4.2 TLS优化 2.5…