目錄
一、簡介
二、軟件框架
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部分。