目錄
- AIR32F103(一) 合宙AIR32F103CBT6開發板上手報告
- AIR32F103(二) Linux環境和LibOpenCM3專案模板
- AIR32F103(三) Linux環境基于標準外設庫的專案模板
- AIR32F103(四) 27倍頻216MHz,CoreMark跑分測驗
- AIR32F103(五) FreeRTOSv202112核心庫的集成和示例代碼
- AIR32F103(六) ADC,I2S,DMA和ADPCM實作的錄音播放功能
- AIR32F103(七) AIR32F103CBT6/CCT6啟用96K記憶體
- AIR32F103(八) 集成Helix MP3解碼庫播放MP3
- AIR32F103(九) CAN總線的通信和ID過濾機制及實體
- AIR32F103(十) 在無系統環境和FreeRTOS環境集成LVGL
LVGL簡介
嵌入式常用的圖形顯示庫
- 官網: https://lvgl.io/
- GitHub倉庫: https://github.com/lvgl/lvgl
對設備的要求是 "all you need is at least 32kB RAM and 128 kB Flash, a C compiler, a frame buffer, and at least an 1/10 screen sized buffer for rendering". 最低要求是128KB Flash, 但實際上這個大小基本上什么也做不了, 所以直接用256K Flash 的 AIR32F103CCT6 和 AIR32F103RPT6.
集成LVGL到AIR32F103
Principle 大佬寫過一篇 Air32F103試玩-移植LVGL+FreeRTOS Keil5 用戶可以參考. 基于STM32標準庫, 用的螢屏是 GC9306X 320x240LCD.
我沒有這個型號的螢屏, 手里能找到現成的串口屏只有一個128x160的ST7735, 就用這個做測驗吧.
基本步驟
參考LVGL的檔案, 這兩片內容差不多的, 第二篇會更細節一點
- https://docs.lvgl.io/master/get-started/quick-overview.html
- https://docs.lvgl.io/master/porting/project.html
需要做的步驟為
- 將 lvgl 庫目錄放到專案里
- 復制一份 lvgl/lv_conf_template.h , 改名為 lv_conf.h 并修改定制
- 在專案中需要使用lvgl的地方, 包含 lvgl/lvgl.h 頭檔案
- 建一個定時器, 每隔1到10毫秒呼叫一次 lv_tick_inc(x) 用于lvgl內部定時. 如果不用這個方法, 就要定義 LV_TICK_CUSTOM 讓 LVGL 可以直接讀取時間.
- 呼叫 lv_init() 執行初始化
- 創建一個影像緩沖, 最小為1/10個螢屏尺寸所需要的資料大小.
- 實作一個繪圖函式, 用于LVGL呼叫后往設備的指定區域寫入顯示內容.
- 如果有輸入設備, 還可以再實作一個輸入讀取函式
- 在主回圈 main while(1) 中, 如果是RTOS環境則在一個回圈任務中, 每隔幾個毫秒呼叫一次 lv_timer_handler(), 用于LVGL繪制更新影像顯示.
最小化實作
1.將LVGL添加到專案中
從 https://github.com/lvgl/lvgl/releases 下載LVGL, 當前版本是v8.3.5, 解壓.
在專案 Libraries 下創建lvgl目錄, 復制必須的檔案到這個目錄下
demos/
examples/
src/
LICENCE.txt
lv_conf_template.h
lvgl.h
lvgl.mk
復制后的 Libraries 目錄結構為
Libraries
├───AIR32F10xLib
├───CMSIS
├───Debug
├───DeviceSupport
├───EPaper
├───FreeRTOS
├───Helix
├───LDScripts
├───lvgl
│ ├───demos
│ ├───examples
│ └───src
│ ├───core
│ ├───draw
│ ├───extra
│ ├───font
│ ├───hal
│ ├───misc
│ └───widgets
在 Makefile 中添加 LVGL 選項
# Build with lvgl, y:yes, n:no
USE_LVGL ?= n
LVGL的編譯串列和頭檔案路徑都已經在 lvgl.mk 里定義好了, 這里只需要把它 include 進來, 再合并到專案的串列中.
ifeq ($(USE_LVGL),y)
LVGL_DIR ?= Libraries
LVGL_DIR_NAME ?= lvgl
include Libraries/lvgl/lvgl.mk
CFILES += $(CSRCS)
INCLUDES += Libraries/lvgl
else
CFLAGS ?=
endif
將 USE_LVGL 設為 y 之后, make 就會帶上 LVGL 一起編譯. 因為 LVGL 檔案很多, 編譯時間較長, 可以根據自己電腦的CPU個數設定并發編譯, 例如對于8個邏輯核的L480, 可以執行
make -j8
因為編譯結果有200多KByte, 寫入的速度也很慢, 暫時沒有什么好辦法.
2. 定制 lv_conf.h
將 lvgl/lv_conf_template.h, 復制到 user 目錄下, 改名為 lv_conf.h, 編輯
將#if 0
改為#if 1
/* clang-format off */
#if 1 /*Set it to "1" to enable content*/
因為ST7735支持的是2byte的像素, 色深設為 16
/*Color depth: 1 (1 byte per pixel), 8 (RGB332), 16 (RGB565), 32 (ARGB8888)*/
#define LV_COLOR_DEPTH 16
再往下, 都是用0和1代表對應功能項的關和開, 可以保持默認. 因為ST7735螢屏解析度較小, 所以再修改一下字體, 將 LV_FONT_MONTSERRAT_10
改為1, 將LV_FONT_MONTSERRAT_14
改為0 啟用10像素字體
#define LV_FONT_MONTSERRAT_10 0
#define LV_FONT_MONTSERRAT_12 0
#define LV_FONT_MONTSERRAT_14 1
再設定一下LV_FONT_DEFAULT
, 改為&lv_font_montserrat_10
, 替換為剛才啟用的 10像素字體
/*Always set a default font*/
#define LV_FONT_DEFAULT &lv_font_montserrat_14
3. 創建 lv_tick_inc(x) 定時器
這里使用TIM3, 將定時間隔設為1毫秒, 開啟 TIM_IT_Update 中斷
void TIM3_Configuration(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
// Set counter limit to 100 -- interval will be 1ms
TIM_TimeBaseStructure.TIM_Period = 100 - 1;
/**
* Clock source of TIM2,3,4,5,6,7: if(APB1 prescaler =1) then PCLK1 x1, else PCLK1 x2
* */
if (clocks.HCLK_Frequency == clocks.PCLK1_Frequency)
{
// clock source is PCLK1 x1.
// Note: TIM_Prescaler is 16bit, [0, 65535], given PCLK1 is 36MHz, divider should > 550
TIM_TimeBaseStructure.TIM_Prescaler = clocks.PCLK1_Frequency / 100000 - 1;
}
else
{
// clock source is PCLK1 x2
TIM_TimeBaseStructure.TIM_Prescaler = clocks.PCLK1_Frequency * 2 / 100000 - 1;
}
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseStructure);
// Enable interrupt from 'TIM update'
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);
// NVIC config
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM3, ENABLE);
}
創建TIM3的中斷回呼函式, 因為定時器間隔為1毫秒, 因此使用lv_tick_inc(1)
void TIM3_IRQHandler(void)
{
if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)
{
// Clear INT flag
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
// Required for the internal timing of LVGL
lv_tick_inc(1);
}
}
如果提示找不到 lv_tick_inc(), 需要加上對頭檔案 lvgl/lvgl.h 的 include
4. 創建繪圖函式
這里涉及到三部分: 初始化 SPI 和對應的 GPIO, 初始化 ST7735, 最后才是 ST7735 的繪圖函式.
初始化 GPIO, 這4個pin是需要宣告為推挽輸出的 PA2:BL, PA3:CS, PA4:DC(Data/Command), PA6:RESET
void APP_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA, GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_6);
}
初始化 SPI, 這里還需要設定 PA5:SCK/SCL 和 PA7:SI/SDA
void APP_SPI_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA,GPIO_Pin_5 | GPIO_Pin_7);
SPI_InitStructure.SPI_Direction = SPI_Direction_1Line_Tx;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_16;
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 0;
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
}
初始化 ST7735, 這部分已經在 st7735.c 中封裝, 直接在main()中呼叫即可
ST7735_Init();
創建 ST7735 的區域繪圖函式
void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p)
{
ST7735_WriteAddrWindow(area->x1, area->y1, area->x2, area->y2, (uint16_t *)color_p);
// Indicate you are ready with the flushing
lv_disp_flush_ready(disp);
}
對應的 ST7735_WriteAddrWindow() 函式實作, 因為來源是16bit, SPI介面是8bit, 每一次呼叫分別寫入兩次
void ST7735_WriteAddrWindow(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2, uint16_t *data)
{
uint32_t tmp, i;
if (x1 > x2)
{
tmp = x1; x1 = x2; x2 = tmp;
}
if (y1 > y2)
{
tmp = y1; y1 = y2; y2 = tmp;
}
tmp = (x2 - x1 + 1) * (y2 - y1 + 1);
ST7735_CS_LOW;
ST7735_SetAddrWindow(x1, y1, x2, y2);
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
for(i = 0; i < tmp; i ++)
{
SPI_I2S_SendData(SPI1, (uint8_t)(*data >> 8));
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET);
SPI_I2S_SendData(SPI1, (uint8_t)(*data & 0xFF));
while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET);
data++;
}
ST7735_CS_HIGH;
}
5. 創建影像緩沖, 初始化LVGL
最小為1/10個螢屏尺寸所需資料大小
static lv_disp_draw_buf_t draw_buf;
// Declare a buffer for 1/10 screen size
static lv_color_t buf1[ST7735_WIDTH * ST7735_HEIGHT / 10];
// Descriptor of a display driver
static lv_disp_drv_t disp_drv;
在 main() 中進行初始化, 注意這部分官網給代碼里的型別不太對, 這部分的代碼已經修改
lv_init();
// Initialize the display buffer.
lv_disp_draw_buf_init(&draw_buf, buf1, NULL, ST7735_WIDTH * ST7735_HEIGHT / 10);
lv_disp_drv_init(&disp_drv); /*Basic initialization*/
disp_drv.flush_cb = my_disp_flush; /*Set your driver function*/
disp_drv.draw_buf = &draw_buf; /*Assign the buffer to the display*/
disp_drv.hor_res = ST7735_WIDTH; /*Set the horizontal resolution of the display*/
disp_drv.ver_res = ST7735_HEIGHT; /*Set the vertical resolution of the display*/
lv_disp_drv_register(&disp_drv); /*Finally register the driver*/
6. 主回圈添加 lv_timer_handler()
因為這個 ST7735 沒有觸屏功能, 所以輸入讀取函式就省了. 在 main() while(1)主回圈中加上 lv_timer_handler()
while (1)
{
lv_timer_handler();
Delay_Ms(10);
}
6. 執行示例
經過以上的設定, LVGL就已經集成到專案中了, 可以運行LVGL自帶的一些例子查看控制元件的顯示效果
文字標簽, 居中和滾動的效果
lv_example_label_1();
按鈕效果
lv_example_btn_1();
源代碼
以上LVGL整合示例的完整源代碼已經提交到 GitHub: https://github.com/IOsetting/air32f103-template/tree/master/Examples/NonFreeRTOS/SPI/ST7735_LVGL
修改為 DMA 輸出
上面的例子是使用 SPI_I2S_SendData()
函式傳輸影像資料的, 可以修改為 DMA 傳輸, 因為傳輸方式的變化, 外設初始化和影像更新要做對應的調整, 這里沒有使用中斷.
1. 外設調整
GPIO 不變, 啟用 SPI 的 DMA
在 APP_SPI_Config()
中啟用SPI1 DMA
/* Enable SPI1 DMA TX request */
SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE);
開啟 DMA 時鐘
void APP_DMA_Configuration(void)
{
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
}
因為DMA每次傳輸的數量在變化, 所以DMA的初始化在影像輸出的方法里
2. 修改影像更新方法
影像更新方法的變化比較大, 這里需要根據輸入的坐標, 計算實際的資料長度, 并對DMA進行初始化, 然后啟動傳輸, 等待完成后關閉DMA
void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p)
{
uint16_t len;
DMA_InitTypeDef initStructure;
len = (area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1) * 2;
ST7735_CS_LOW;
ST7735_SetAddrWindow(area->x1, area->y1, area->x2, area->y2);
/* DMA1 Channel3 (triggered by SPI1 Tx event) Config */
DMA_DeInit(DMA1_Channel3);
initStructure.DMA_BufferSize = len;
initStructure.DMA_M2M = DMA_M2M_Disable;
initStructure.DMA_DIR = DMA_DIR_PeripheralDST;
initStructure.DMA_MemoryBaseAddr = (uint32_t)color_p;
initStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
initStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
initStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR;
initStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
initStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
initStructure.DMA_Priority = DMA_Priority_High;
initStructure.DMA_Mode = DMA_Mode_Normal;
DMA_Init(DMA1_Channel3, &initStructure);
// Start transfer
DMA_Cmd(DMA1_Channel3, ENABLE);
while (!DMA_GetFlagStatus(DMA1_FLAG_TC3));
DMA_ClearFlag(DMA1_FLAG_TC3);
DMA_Cmd(DMA1_Channel3, DISABLE);
ST7735_CS_HIGH;
// Indicate you are ready with the flushing
lv_disp_flush_ready(disp);
}
3. 修改色彩位元組順序
上面修改完成后, 再次運行LVGL示例, 會發現顏色不正確, 這是因為在DMA傳輸中, 將一個 16bit 強轉為兩個 8bit 了, ST7735收到的兩個位元組的順序有變化, 需要編輯 lv_conf.h, 將LV_COLOR_16_SWAP
設為1
/*Swap the 2 bytes of RGB565 color. Useful if the display has an 8-bit interface (e.g. SPI)*/
#define LV_COLOR_16_SWAP 1
源代碼
以上LVGL DMA 示例的完整源代碼已經提交到 GitHub: https://github.com/IOsetting/air32f103-template/tree/master/Examples/NonFreeRTOS/SPI/ST7735_LVGL_DMA
集成到 FreeRTOS
進一步, 在 FreeRTOS 中運行 DMA 傳輸的 LVGL. 在 FreeRTOS 中 LVGL 的初始化是一樣的, 有變化的是初始化的時間點, 還有延時函式的修改
1. 將初始化從 main() 移入任務handler
新建lvglTaskHandler()
用于處理LVGL初始化, 快取初始化和執行benchmark, 并用固定間隔呼叫lv_timer_handler()
static void lvglTaskHandler(void *pvParameters)
{
TickType_t xLastWakeTime = xTaskGetTickCount();
const TickType_t xPeriod = pdMS_TO_TICKS(10);
(void)(pvParameters); // Suppress "unused parameter" warning
ST7735_Init();
lv_init();
// Initialize the display buffer.
lv_disp_draw_buf_init(&draw_buf, buf1, NULL, ST7735_WIDTH * ST7735_HEIGHT / 10);
lv_disp_drv_init(&disp_drv); /*Basic initialization*/
disp_drv.flush_cb = my_disp_flush; /*Set your driver function*/
disp_drv.draw_buf = &draw_buf; /*Assign the buffer to the display*/
disp_drv.hor_res = ST7735_WIDTH; /*Set the horizontal resolution of the display*/
disp_drv.ver_res = ST7735_HEIGHT; /*Set the vertical resolution of the display*/
lv_disp_drv_register(&disp_drv); /*Finally register the driver*/
lv_demo_benchmark();
while (1)
{
lv_timer_handler();
vTaskDelayUntil(&xLastWakeTime, xPeriod);
}
}
在 main() 中創建任務, 堆疊深度1024, 需要 4KByte 記憶體
xTaskCreate(
lvglTaskHandler, // Task function point
"LVGL Task", // Task name
1024, // Stack size, each take 4 bytes(32bit)
NULL, // Parameters
LVGL_TASK_PRORITY, // Priority
NULL); // Task handler
2. 修改延時函式
更新影像的方法不變, 但是需要修改 ST7735 的延時函式, 修改 st7735.c, 引入FreeRTOS.h
, 將Delay_Ms(ms);
替換為 vTaskDelay(ms);
3. 中斷設定
在 FreeRTOSConfig.h 中, 設定系統最高的, 可以安全使用FreeRTOS方法的中斷優先級為1
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 0x01
因為 AIR32F103 的中斷為 3 bit, 可用的優先級為 0 到 7, 這樣對于優先級為 0 的中斷是不受FreeRTOS控制的, 小于等于 1 的是受FreeRTOS控制的, 可以在中斷處理中呼叫 FreeRTOS 的方法.
將TIM3的中斷優先級設定為2
NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
4. 裁剪 LVGL
因為集成FreeRTOS, 加上運行 Benchmark, 256KB 的 Flash 容量就捉襟見肘了, 默認配置下編譯出來會有300多KB, 需要進行壓縮
首先確認編譯引數已經優化, rules.mk 中, 優化項改為-O3
或-Os
# c flags
OPT ?= -O3
編輯 lv_conf.h 關閉一切不必要的組件, 使用盡可能小的字體(可以用10像素字體), 具體的改動可以參考示例代碼.
源代碼
以上LVGL+FreeRTO 示例的源代碼已經提交到 GitHub: https://github.com/IOsetting/air32f103-template/tree/master/Examples/FreeRTOS/LVGL/ST7735_128x160
最后
以上就是在 AIR32F103 上集成 LVGL 的步驟和說明, ST7735 也是常見模塊. 對于 DMA 的例子, 可以進一步修改為使用中斷判斷 DMA 傳輸結束. 留有空再改了.
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/546391.html
標籤:嵌入式