基于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.pswp.cn/bicheng/76985.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/76985.shtml
英文地址,請注明出處:http://en.pswp.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…