基于FreeRTOS和LVGL的多功能低功耗智能手表(APP篇)
本篇开始介绍这个项目的软件部分,我们这里先去介绍APP部分,APP和Bootloader是独立的,如果大家不需要了解Bootloader的话,直接看这篇即可,同样可以实现我们整个手表的功能、、这里我们把该项目的软件部分和逻辑都讲解了一遍,这里还没有去细讲LVGL部分,因为LVGL部分相对较为独立,可以之后单独开一篇来讲,下一篇我将会去讲解这个项目的LVGL部分。
目录
2.5.2 HardwareInitTask 硬件初始化任务
2.5.3 ChargPageEnterTask 充电界面任务
2.5.4 SensorDataUpdateTask 传感器数值更新任务
2.5.5 ScrRenewTask界面刷新任务 以及 KeyTask按键任务
2.5.7 MessageSendTask 串口数据收发任务
2.5.8 IdleEnterTask 空闲任务 以及 StopEnterTask 停止模式任务
一、简介
本篇开始介绍这个项目的软件部分,我们这里先去介绍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_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的引脚为高电平的时候,说明此时正在进行充电,那么屏幕就会刷新出我们的充电界面。

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中进行配置了。

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;//erro
int 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 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")会被视为错误,这是正确的。但如果是负数的情况,这里就会导致错误,所以代码不支持负数的运算。前面做的所有都是为了这个函数进行铺垫,我们可以在函数调用关系看到:

优先级判断(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_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[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 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);
/**
* @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 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;
}
/**
* @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 0
osMessageQueuePut(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)
{
//owdg
WDOG_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 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[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);
}
}
// 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[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 init
lv_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 Home
uint8_t HomeUpdataStr;
if(osMessageQueueGet(HomeUpdata_MessageQueue, &HomeUpdataStr, NULL, 0)==osOK)
{
//bat
uint8_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;}
//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按键任务


按键任务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=20230629125555
else 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 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);
}
}
/**
* @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
//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进入一次:

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

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

尽管这个任务的源码非常的少,但是这个任务其实是最最复杂的,这里设计了LVGL的基本知识。之后会单独拿一篇来进行讲解。
三、总结
这里我们把该项目的软件部分和逻辑都讲解了一遍,这里还没有去细讲LVGL部分,因为LVGL部分相对较为独立,可以之后单独开一篇来讲,下一篇我将会去讲解这个项目的LVGL部分。
更多推荐



所有评论(0)