用野火啟明6M5開發板制作了一個基于FreeRTOS和LVGL V8的智能家居儀表盤,顏值較高,也可以作為桌面擺件使用,具體特點如下:
采用SPI+DTC驅動1.8寸SPI屏幕,超高幀率刷屏
采用LVGL V8界面庫繪制界面,有豐富控件、動畫(FPS穩定50以上!)
采用ESP8266聯網,使用心知天氣API獲取當前天氣并顯示到屏幕
采用ESP8266聯網,通過MQTT協議連接到云服務器,上傳狀態數據
采用魯班貓2安裝EMQ作為MQTT服務器,接收啟明6M5上傳數據
采用Node-RED + Homeassistant接入家庭自動化,與智能家居設備完美聯動
01
硬件平臺介紹
野火啟明6M5開發板
使用野火啟明6M5開發板來進行開發,開發板采用R7FA6M5BH3CFC作為主控芯片,有2MB Flash,2MB!!拿來開發GUI時的可發揮空間很大,接口有SD卡、以太網、PMOD、USB等等,接口很豐富,功能模塊有ESP8266、電容按鍵和實體按鍵等,功能十分的豐富。
外接模塊
由于開發板板載的模塊已經十分豐富,這里只外接了一個SPI屏幕和溫濕度傳感器模塊
采用1.8寸的液晶顯示屏,驅動芯片為ST7735S,SPI接口。
溫濕度傳感器采用瑞薩的HS3003溫濕度傳感器,I2C接口。
外設使用情況
本次使用到了許多的外設,其中有如下外設
串口4 (SCI_UART4) 作為調試串口使用
串口9 (SCI_UART9) 連接到ESP8266-AT模塊
SDHI連接到SD卡,提供文件系統的支持
AGT定時器為LVGL提供計時器
RTC提供實時的時間 (需要安裝CR1220電池)
SPI+DTC來實現屏幕的驅動,SPI以最大速度50MHz運行
TOUCH提供電容按鍵
I2C (SCI_I2C6) 連接到HS3003溫濕度傳感器
02
軟件設計方案
① 采用FreeRTOS作為本作品使用的RTOS
② 采用LVGL V8界面庫來進行界面開發
③ 采用letter-shell終端組件方便開發調試
④ 采用easylogger日志組件方便調試
⑤ 采用cJSON組件配合來完成網絡數據包打包與解包
多線程
由于代碼較多,所以不作全面的介紹,只介紹幾個線程的任務內容和軟件包的使用,文末有開源鏈接,作品的代碼全部開源,線程列表如下圖,下面依次介紹。
調試線程(debug_thread)
該線程使用了letter-shell和easylogger軟件包,提供完整的終端操作支持,同時支持日志打印,例如打印esp8266線程的調試日志。
使用自定義的命令來打印當前運行的任務列表
ESP8266線程(esp8266_thread)
該線程使用AT指令,實現開機自動連接Wi-Fi、自動連接MQTT服務器、訂閱主題。當收到消息隊列的數據后,更新溫濕度數據、LED狀態,然后使用cJSON來打包為JSON數據包,發布到MQTT服務器的指定主題。當收到MQTT發來的數據后,使用cJSON來解析JSON數據包,更新當前天氣等。
(觸摸)按鍵、LED、RTC線程(misc_thread)
該線程使用了MultiButton軟件包,可以實現一個按鍵的單擊、雙擊、連擊、長按等事件的處理,這里使用觸摸按鍵來搭配這個軟件包實現觸摸按鍵控制板載的LED亮滅,并且發送狀態信息到消息隊列中,交由ESP8266線程上傳到服務器端。
該線程同時也使用了RTC時鐘,每秒觸發一次中斷,發送當前時間到消息隊列中,交由LCD線程來顯示當前時間。
SD卡線程
該線程使用了Fatfs來掛載文件系統,自動將SD卡掛載到1: 分區下,提供給LVGL FS接口,實現LVGL加載SD卡中的文本、圖片等文件。
屏幕驅動線程(lcd_thread)
屏幕驅動使用硬件SPI+DTC的方案,這里沒有使用SCI上的SPI接口,因為根據瑞薩6M5的文檔得知掛在SCI上的SPI最大時鐘頻率為25Mhz,而直接連接的SPI最大時鐘頻率為50Mhz,顯然使用直連SPI接口可以獲得更快的刷屏速度。
該線程會接收多個線程傳入的消息隊列:接收RTC時鐘中斷發來的消息隊列,在LVGL中注冊的timer callback函數中讀取后顯示到屏幕上,每秒刷新一次時間數據;接收溫濕度線程發來的消息隊列,讀取后更新當前屏幕上的溫濕度數值和進度條控件。
溫濕度傳感器線程(sensor_thread)
該線程每隔十秒使用硬件I2C來讀取HS3003的數據并解算出溫濕度數據,發送溫濕度數據到消息隊列中,交由ESP8266線程來上傳到服務器和LCD線程來顯示到屏幕。
LVGL移植、界面設計LVGL移植
在本作品中對LVGL的顯示接口和文件系統接口做了移植,下面對LVGL的顯示接口移植做介紹,LVGL的顯示接口只有三個函數需要修改,分別是緩沖區的初始化、屏幕的初始化和刷屏函數的接口,對于屏幕的初始化在lcd_thread中已經完成過,所以只需完成緩沖區的初始化和刷屏函數接口的適配。
為了實現更快的刷屏速度,使用官方提供的example2程序,并且給LVGL申請一個全屏緩沖區,搭配SPI+DTC的全屏緩沖區,需要更新屏幕上的數據時只需要搬運數據即可。
上下滑動查看完整內容
左右滑動即可查看完整代碼
#if 1 /********************* * INCLUDES *********************/ #include "lv_port_disp.h" #include/********************* * DEFINES *********************/ #ifndef MY_DISP_HOR_RES #warning Please define or replace the macro MY_DISP_HOR_RES with the actual screen width, default value 320 is used for now. #define MY_DISP_HOR_RES 128 #endif #ifndef MY_DISP_VER_RES #warning Please define or replace the macro MY_DISP_HOR_RES with the actual screen height, default value 240 is used for now. #define MY_DISP_VER_RES 160 #endif /********************** * TYPEDEFS **********************/ /********************** * STATIC PROTOTYPES **********************/ static void disp_init(void); static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p); /********************** * STATIC VARIABLES **********************/ /********************** * MACROS **********************/ /********************** * GLOBAL FUNCTIONS **********************/ void lv_port_disp_init(void) { /*------------------------- * Initialize your display * -----------------------*/ disp_init(); /*----------------------------- * Create a buffer for drawing *----------------------------*/ /* Example for 2) */ static lv_disp_draw_buf_t draw_buf_dsc_2; static lv_color_t buf_2_1[MY_DISP_HOR_RES * MY_DISP_VER_RES]; lv_disp_draw_buf_init(&draw_buf_dsc_2, buf_2_1, NULL, MY_DISP_HOR_RES * MY_DISP_VER_RES); /*Initialize the display buffer*/ /*----------------------------------- * Register the display in LVGL *----------------------------------*/ static lv_disp_drv_t disp_drv; /*Descriptor of a display driver*/ lv_disp_drv_init(&disp_drv); /*Basic initialization*/ /*Set up the functions to access to your display*/ /*Set the resolution of the display*/ disp_drv.hor_res = MY_DISP_HOR_RES; disp_drv.ver_res = MY_DISP_VER_RES; /*Used to copy the buffer's content to the display*/ disp_drv.flush_cb = disp_flush; /*Set a display buffer*/ disp_drv.draw_buf = &draw_buf_dsc_2; /*Required for Example 3)*/ //disp_drv.full_refresh = 1; /* Fill a memory array with a color if you have GPU. * Note that, in lv_conf.h you can enable GPUs that has built-in support in LVGL. * But if you have a different GPU you can use with this callback.*/ //disp_drv.gpu_fill_cb = gpu_fill; /*Finally register the driver*/ lv_disp_drv_register(&disp_drv); } /********************** * STATIC FUNCTIONS **********************/ /*Initialize your display and the required peripherals.*/ static void disp_init(void) { /*You code here*/ } volatile bool disp_flush_enabled = true; /* Enable updating the screen (the flushing process) when disp_flush() is called by LVGL */ void disp_enable_update(void) { disp_flush_enabled = true; } /* Disable updating the screen (the flushing process) when disp_flush() is called by LVGL */ void disp_disable_update(void) { disp_flush_enabled = false; } /*Flush the content of the internal buffer the specific area on the display *You can use DMA or any hardware acceleration to do this operation in the background but *'lv_disp_flush_ready()' has to be called when finished.*/ extern uint8_t lcd_buff[160][128][2]; static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { if(disp_flush_enabled) { /*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/ int32_t x; int32_t y; for(y = area->y1; y <= area->y2; y++) { for(x = area->x1; x <= area->x2; x++) { /*Put a pixel to the display. For example:*/ /*put_px(x, y, *color_p)*/ lcd_buff[y][x][0] = color_p->full >> 8; lcd_buff[y][x][1] = color_p->full; color_p++; } } } /*IMPORTANT!!! *Inform the graphics library that you are ready with the flushing*/ lv_disp_flush_ready(disp_drv); } #else /*Enable this file at the top*/ /*This dummy typedef exists purely to silence -Wpedantic.*/ typedef int keep_pedantic_happy; #endif
對于刷屏函數的移植只需實現數據的搬運,代碼如下:
extern uint8_t lcd_buff[160][128][2]; static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { if(disp_flush_enabled) { /*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/ int32_t x; int32_t y; for(y = area->y1; y <= area->y2; y++) { for(x = area->x1; x <= area->x2; x++) { /*Put a pixel to the display. For example:*/ /*put_px(x, y, *color_p)*/ lcd_buff[y][x][0] = color_p->full >> 8; lcd_buff[y][x][1] = color_p->full; color_p++; } } } /*IMPORTANT!!! *Inform the graphics library that you are ready with the flushing*/ lv_disp_flush_ready(disp_drv); }
在lcd_thread線程的while循環中只需使用SPI發送全屏緩沖到屏幕,代碼如下:
void lcd_push_buff(void) { R_SPI_Write(spilcd_spi0.p_ctrl, lcd_buff, LCD_W * LCD_H * 2, SPI_BIT_WIDTH_8_BITS); } /* 下面是主函數調用 */ void lcd_thread_entry(void* pvParameters) { FSP_PARAMETER_NOT_USED(pvParameters); lcd_setup(); while (1) { lcd_push_buff(); lv_task_handler(); } }
界面設計與仿真
采用NXP的GUI Guider作為PC端的設計器和仿真器,GUI Guider可以在PC端完成一站式的LVGL界面設計與仿真,例如下圖所示:
在GUI Guider中對兩個頁面分別創建了一個定時器,并且實現了兩個回調函數,代碼如下,通過這個定時器回調函數來實現周期性的刷新屏幕顯示的內容,更新網絡連接狀態、當前溫濕度、當前時間、當前天氣等數據。
左右滑動即可查看完整代碼
void timer_main_reflash_cb(lv_timer_t *t) { static uint32_t tick; lv_ui * gui = t->user_data; #ifdef __ARMCC_VERSION float sensor_info[2]; if (pdTRUE == xQueueReceive(g_sensor2lcd_queue, sensor_info, pdMS_TO_TICKS(0))) { lv_bar_set_value(gui->main_bar_humi, (uint32_t) sensor_info[0], LV_ANIM_ON); lv_bar_set_value(gui->main_bar_temp, (uint32_t) sensor_info[1], LV_ANIM_ON); lv_label_set_text_fmt(gui->main_label_humi, "%2d%%", (uint32_t) sensor_info[0]); lv_label_set_text_fmt(gui->main_label_temp, "%2d'C", (uint32_t) sensor_info[1]); } rtc_time_t get_time; if (pdTRUE == xQueueReceive(g_clock2lcd_queue, &get_time, pdMS_TO_TICKS(0))) { lv_label_set_text_fmt(gui->main_label_hour, "%02d", get_time.tm_hour); lv_label_set_text_fmt(gui->main_label_min, "%02d", get_time.tm_min); lv_label_set_text_fmt(gui->main_label_sec, "%02d", get_time.tm_sec); } uint32_t num = 0; if (pdTRUE == xQueueReceive(g_esp2lcd_queue, &num, pdMS_TO_TICKS(0))) { if (num > 38) { num = 99; } char path [30]; sprintf(path, "1lvgl/weather/%d.jpg", num); lv_img_set_src(gui->main_img_weather, path); } #endif } const char str_ch[][40] = { "連接WI-Fi...", "連接WI-Fi失敗!", "連接WI-Fi成功!", "連接MQTT服務器...", "連接MQTT服務器失敗", "訂閱MQTT主題...", }; void timer_loading_reflash_cb(lv_timer_t *t) { static uint32_t num = 0; lv_ui * gui = t->user_data; #ifdef __ARMCC_VERSION if (pdTRUE == xQueueReceive(g_esp2lcd_queue, &num, pdMS_TO_TICKS(0))) { lv_label_set_text(gui->loading_tip, str_ch[num]); lv_bar_set_value(gui->loading_process, num * 20, LV_ANIM_ON); if (num >= 5) { setup_scr_main(gui); lv_scr_load(gui->main); } } #else num += 3; lv_label_set_text(gui->loading_tip, str_ch[num / 20]); lv_bar_set_value(gui->loading_process, num, LV_ANIM_ON); if (num >= 100) { setup_scr_main(gui); lv_scr_load(gui->main); } #endif }
MQTT與服務器解析
使用ESP8266模塊連接到MQTT服務器,因為MQTT也是自建的EMQX服務器,自由度相對onenet平臺要大很多,這里的上傳數據、下載數據都是統一由MQTT服務器搭配node-red來完成,避免來回地將ESP8266切換為透傳模式來實現HTTP訪問,全由服務器來進行數據的處理與打包,拖拽化開發自定義的MQTT消息處理流程不香嗎?
例如上傳當前溫濕度、LED狀態、知心天氣API獲得當前的天氣數據的流程設置如下:
服務器端解析溫濕度數據時,上傳的數據包格式為 JSON 數據,形如
{“hum”:51.498504638671872,”tem”:30.258193969726564}
為了解析MQTT的數據包,需要編寫一段代碼來實現數據類型的限定,這里還加了保留到兩位小數,其中的 “get humidity” 等函數只需編寫如下一段JavaScript代碼,經過解析后得到濕度數據,傳入后面的 “is null ?” 節點后若不為空就更新數據給Homeassistant的設備。
var field = msg.payload.hum; var out; if (field == null) { out = { payload: null }; } else { if (typeof field === 'number') { if (Number(field) === Math.round(field)) { /* 整數 */ out = { payload: field }; } else { /* 小數 */ out = { payload: field.toFixed(2) }; } } else if (typeof field === 'boolean') { /* 布爾 */ out = { payload: field }; } else if (typeof field === 'string') { /* 字符串 */ out = { payload: field }; } } return out;
經過HTTP訪問知心天氣的API后,耶對得到的JSON結果進行解析,消息形如:
{ "results": [ { "location": { "id": "WTW3SJ5ZBJUY", "name": "Shanghai", "country": "CN", "path": "Shanghai,Shanghai,China", "timezone": "Asia/Shanghai", "timezone_offset": "+08:00" }, "now": { "text": "Cloudy", "code": "4", "temperature": "35" }, "last_update": "2023-08-13T1214+08:00" } ] }
解析代碼也非常簡單,text為當前的天氣文本,code為當前的天氣代碼:
var text = msg.payload.results[0].now.text; var code = msg.payload.results[0].now.code; return { payload: code };
然后發送最終的天氣碼到主題 /test/esp8266/sub,這個主題是ESP8266已經訂閱的,ESP8266線程完成數據的獲取,然后發送天氣碼到消息隊列,LCD讀取消息隊列,得到天氣碼,然后讀取SD卡中的天氣圖標,顯示到屏幕上,完成天氣圖標的更新。
03
最終效果
聯網進度顯示界面
開機自動聯網、進度條提示,FPS最低50!這個瑞薩的MCU跑LVGL完全無壓力
實時溫濕度、時間數據顯示?
接入Homeassistant記錄溫濕度數據
通過node-red接入到HA作為一個設備顯示當前的溫濕度數據和板載LED的狀態
溫度數據的歷史曲線(開了空調溫度是直線下降啊)
濕度數據的歷史曲線
天貓精靈獲取板載LED狀態
設置了單擊觸摸按鍵開關LED2亮滅的邏輯操作,然后會自動上傳這個LED2的開關狀態到MQTT服務器上,通過node-red來上傳到Homeassistent,搭配巴法云平臺接入到語音助手,我用的是天貓精靈,可以通過語音助手獲取到當前LED2的狀態,當然只是做一個演示,可以實現的自動化智能家居當然還有很多的玩法。
04
視頻展示
05
總結
本作品開發過程中體會到了瑞薩的開發軟件十分的易用,方便,也學習到了LVGL V8、MQTT服務器數據包的收發,node-red橋接MQTT消息包到HA的知識。
完成以上所有的功能后Flash使用了1MB出頭(主要是GUI的資源文件),這個單片機是有2MB的Flash,界面開發還有很大的發揮空間。
1.8寸的小屏比較小,可以換成更大的屏和增加觸摸,但是RA6M5沒有專門的屏幕驅動外設,如果要拓展成并口MCU屏或者RGB屏還是有點受限的。
使用到了如下第三方軟件包,除FatFs使用BSD外別的均為MIT開源協議
CJSON
EasyLogger
FatFs
letter-shell
MultiButton
LVGL V8
FreeRTOS
審核編輯:劉清
-
SPI
+關注
關注
17文章
1731瀏覽量
92836 -
智能家居
+關注
關注
1932文章
9646瀏覽量
187519 -
FreeRTOS
+關注
關注
12文章
484瀏覽量
62772 -
ESP8266
+關注
關注
50文章
963瀏覽量
45847 -
LVGL
+關注
關注
1文章
94瀏覽量
3268
原文標題:【瑞薩RA MCU創意氛圍賽】項目23——基于FreeRTOS+LVGL V8智能家居儀表盤
文章出處:【微信號:瑞薩MCU小百科,微信公眾號:瑞薩MCU小百科】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
原裝IC網基于瑞薩單片機的儀表盤(總線型)解決方案
汽車儀表盤解決方案
虛擬儀表盤是未來的趨勢嗎?
汽車儀表盤MCU背后的故事
汽車儀表盤MCU背后的故事
一文淺析汽車儀表盤
開發汽車obd數字儀表盤的過程記錄
集成TPMS功能的儀表盤設計方案解析

集成TPMS功能的電動汽車儀表盤設計解析

評論