用野火啟明6M5開(kāi)發(fā)板制作了一個(gè)基于FreeRTOS和LVGL V8的智能家居儀表盤(pán),顏值較高,也可以作為桌面擺件使用,具體特點(diǎn)如下:
采用SPI+DTC驅(qū)動(dòng)1.8寸SPI屏幕,超高幀率刷屏
采用LVGL V8界面庫(kù)繪制界面,有豐富控件、動(dòng)畫(huà)(FPS穩(wěn)定50以上?。?/p>
采用ESP8266聯(lián)網(wǎng),使用心知天氣API獲取當(dāng)前天氣并顯示到屏幕
采用ESP8266聯(lián)網(wǎng),通過(guò)MQTT協(xié)議連接到云服務(wù)器,上傳狀態(tài)數(shù)據(jù)
采用魯班貓2安裝EMQ作為MQTT服務(wù)器,接收啟明6M5上傳數(shù)據(jù)
采用Node-RED + Homeassistant接入家庭自動(dòng)化,與智能家居設(shè)備完美聯(lián)動(dòng)
01
硬件平臺(tái)介紹
野火啟明6M5開(kāi)發(fā)板
使用野火啟明6M5開(kāi)發(fā)板來(lái)進(jìn)行開(kāi)發(fā),開(kāi)發(fā)板采用R7FA6M5BH3CFC作為主控芯片,有2MB Flash,2MB!!拿來(lái)開(kāi)發(fā)GUI時(shí)的可發(fā)揮空間很大,接口有SD卡、以太網(wǎng)、PMOD、USB等等,接口很豐富,功能模塊有ESP8266、電容按鍵和實(shí)體按鍵等,功能十分的豐富。
外接模塊
由于開(kāi)發(fā)板板載的模塊已經(jīng)十分豐富,這里只外接了一個(gè)SPI屏幕和溫濕度傳感器模塊
采用1.8寸的液晶顯示屏,驅(qū)動(dòng)芯片為ST7735S,SPI接口。
溫濕度傳感器采用瑞薩的HS3003溫濕度傳感器,I2C接口。
外設(shè)使用情況
本次使用到了許多的外設(shè),其中有如下外設(shè)
串口4 (SCI_UART4) 作為調(diào)試串口使用
串口9 (SCI_UART9) 連接到ESP8266-AT模塊
SDHI連接到SD卡,提供文件系統(tǒng)的支持
AGT定時(shí)器為L(zhǎng)VGL提供計(jì)時(shí)器
RTC提供實(shí)時(shí)的時(shí)間 (需要安裝CR1220電池)
SPI+DTC來(lái)實(shí)現(xiàn)屏幕的驅(qū)動(dòng),SPI以最大速度50MHz運(yùn)行
TOUCH提供電容按鍵
I2C (SCI_I2C6) 連接到HS3003溫濕度傳感器
02
軟件設(shè)計(jì)方案
① 采用FreeRTOS作為本作品使用的RTOS
② 采用LVGL V8界面庫(kù)來(lái)進(jìn)行界面開(kāi)發(fā)
③ 采用letter-shell終端組件方便開(kāi)發(fā)調(diào)試
④ 采用easylogger日志組件方便調(diào)試
⑤ 采用cJSON組件配合來(lái)完成網(wǎng)絡(luò)數(shù)據(jù)包打包與解包
多線程
由于代碼較多,所以不作全面的介紹,只介紹幾個(gè)線程的任務(wù)內(nèi)容和軟件包的使用,文末有開(kāi)源鏈接,作品的代碼全部開(kāi)源,線程列表如下圖,下面依次介紹。
調(diào)試線程(debug_thread)
該線程使用了letter-shell和easylogger軟件包,提供完整的終端操作支持,同時(shí)支持日志打印,例如打印esp8266線程的調(diào)試日志。
使用自定義的命令來(lái)打印當(dāng)前運(yùn)行的任務(wù)列表
ESP8266線程(esp8266_thread)
該線程使用AT指令,實(shí)現(xiàn)開(kāi)機(jī)自動(dòng)連接Wi-Fi、自動(dòng)連接MQTT服務(wù)器、訂閱主題。當(dāng)收到消息隊(duì)列的數(shù)據(jù)后,更新溫濕度數(shù)據(jù)、LED狀態(tài),然后使用cJSON來(lái)打包為JSON數(shù)據(jù)包,發(fā)布到MQTT服務(wù)器的指定主題。當(dāng)收到MQTT發(fā)來(lái)的數(shù)據(jù)后,使用cJSON來(lái)解析JSON數(shù)據(jù)包,更新當(dāng)前天氣等。
(觸摸)按鍵、LED、RTC線程(misc_thread)
該線程使用了MultiButton軟件包,可以實(shí)現(xiàn)一個(gè)按鍵的單擊、雙擊、連擊、長(zhǎng)按等事件的處理,這里使用觸摸按鍵來(lái)搭配這個(gè)軟件包實(shí)現(xiàn)觸摸按鍵控制板載的LED亮滅,并且發(fā)送狀態(tài)信息到消息隊(duì)列中,交由ESP8266線程上傳到服務(wù)器端。
該線程同時(shí)也使用了RTC時(shí)鐘,每秒觸發(fā)一次中斷,發(fā)送當(dāng)前時(shí)間到消息隊(duì)列中,交由LCD線程來(lái)顯示當(dāng)前時(shí)間。
SD卡線程
該線程使用了Fatfs來(lái)掛載文件系統(tǒng),自動(dòng)將SD卡掛載到1: 分區(qū)下,提供給LVGL FS接口,實(shí)現(xiàn)LVGL加載SD卡中的文本、圖片等文件。
屏幕驅(qū)動(dòng)線程(lcd_thread)
屏幕驅(qū)動(dòng)使用硬件SPI+DTC的方案,這里沒(méi)有使用SCI上的SPI接口,因?yàn)楦鶕?jù)瑞薩6M5的文檔得知掛在SCI上的SPI最大時(shí)鐘頻率為25Mhz,而直接連接的SPI最大時(shí)鐘頻率為50Mhz,顯然使用直連SPI接口可以獲得更快的刷屏速度。
該線程會(huì)接收多個(gè)線程傳入的消息隊(duì)列:接收RTC時(shí)鐘中斷發(fā)來(lái)的消息隊(duì)列,在LVGL中注冊(cè)的timer callback函數(shù)中讀取后顯示到屏幕上,每秒刷新一次時(shí)間數(shù)據(jù);接收溫濕度線程發(fā)來(lái)的消息隊(duì)列,讀取后更新當(dāng)前屏幕上的溫濕度數(shù)值和進(jìn)度條控件。
溫濕度傳感器線程(sensor_thread)
該線程每隔十秒使用硬件I2C來(lái)讀取HS3003的數(shù)據(jù)并解算出溫濕度數(shù)據(jù),發(fā)送溫濕度數(shù)據(jù)到消息隊(duì)列中,交由ESP8266線程來(lái)上傳到服務(wù)器和LCD線程來(lái)顯示到屏幕。
LVGL移植、界面設(shè)計(jì)LVGL移植
在本作品中對(duì)LVGL的顯示接口和文件系統(tǒng)接口做了移植,下面對(duì)LVGL的顯示接口移植做介紹,LVGL的顯示接口只有三個(gè)函數(shù)需要修改,分別是緩沖區(qū)的初始化、屏幕的初始化和刷屏函數(shù)的接口,對(duì)于屏幕的初始化在lcd_thread中已經(jīng)完成過(guò),所以只需完成緩沖區(qū)的初始化和刷屏函數(shù)接口的適配。
為了實(shí)現(xiàn)更快的刷屏速度,使用官方提供的example2程序,并且給LVGL申請(qǐng)一個(gè)全屏緩沖區(qū),搭配SPI+DTC的全屏緩沖區(qū),需要更新屏幕上的數(shù)據(jù)時(shí)只需要搬運(yùn)數(shù)據(jù)即可。
上下滑動(dòng)查看完整內(nèi)容
左右滑動(dòng)即可查看完整代碼
#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
對(duì)于刷屏函數(shù)的移植只需實(shí)現(xiàn)數(shù)據(jù)的搬運(yùn),代碼如下:
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循環(huán)中只需使用SPI發(fā)送全屏緩沖到屏幕,代碼如下:
void lcd_push_buff(void) { R_SPI_Write(spilcd_spi0.p_ctrl, lcd_buff, LCD_W * LCD_H * 2, SPI_BIT_WIDTH_8_BITS); } /* 下面是主函數(shù)調(diào)用 */ void lcd_thread_entry(void* pvParameters) { FSP_PARAMETER_NOT_USED(pvParameters); lcd_setup(); while (1) { lcd_push_buff(); lv_task_handler(); } }
界面設(shè)計(jì)與仿真
采用NXP的GUI Guider作為PC端的設(shè)計(jì)器和仿真器,GUI Guider可以在PC端完成一站式的LVGL界面設(shè)計(jì)與仿真,例如下圖所示:
在GUI Guider中對(duì)兩個(gè)頁(yè)面分別創(chuàng)建了一個(gè)定時(shí)器,并且實(shí)現(xiàn)了兩個(gè)回調(diào)函數(shù),代碼如下,通過(guò)這個(gè)定時(shí)器回調(diào)函數(shù)來(lái)實(shí)現(xiàn)周期性的刷新屏幕顯示的內(nèi)容,更新網(wǎng)絡(luò)連接狀態(tài)、當(dāng)前溫濕度、當(dāng)前時(shí)間、當(dāng)前天氣等數(shù)據(jù)。
左右滑動(dòng)即可查看完整代碼
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服務(wù)器...", "連接MQTT服務(wù)器失敗", "訂閱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與服務(wù)器解析
使用ESP8266模塊連接到MQTT服務(wù)器,因?yàn)镸QTT也是自建的EMQX服務(wù)器,自由度相對(duì)onenet平臺(tái)要大很多,這里的上傳數(shù)據(jù)、下載數(shù)據(jù)都是統(tǒng)一由MQTT服務(wù)器搭配node-red來(lái)完成,避免來(lái)回地將ESP8266切換為透?jìng)髂J絹?lái)實(shí)現(xiàn)HTTP訪問(wèn),全由服務(wù)器來(lái)進(jìn)行數(shù)據(jù)的處理與打包,拖拽化開(kāi)發(fā)自定義的MQTT消息處理流程不香嗎?
例如上傳當(dāng)前溫濕度、LED狀態(tài)、知心天氣API獲得當(dāng)前的天氣數(shù)據(jù)的流程設(shè)置如下:
服務(wù)器端解析溫濕度數(shù)據(jù)時(shí),上傳的數(shù)據(jù)包格式為 JSON 數(shù)據(jù),形如
{“hum”:51.498504638671872,”tem”:30.258193969726564}
為了解析MQTT的數(shù)據(jù)包,需要編寫(xiě)一段代碼來(lái)實(shí)現(xiàn)數(shù)據(jù)類型的限定,這里還加了保留到兩位小數(shù),其中的 “get humidity” 等函數(shù)只需編寫(xiě)如下一段JavaScript代碼,經(jīng)過(guò)解析后得到濕度數(shù)據(jù),傳入后面的 “is null ?” 節(jié)點(diǎn)后若不為空就更新數(shù)據(jù)給Homeassistant的設(shè)備。
var field = msg.payload.hum; var out; if (field == null) { out = { payload: null }; } else { if (typeof field === 'number') { if (Number(field) === Math.round(field)) { /* 整數(shù) */ out = { payload: field }; } else { /* 小數(shù) */ out = { payload: field.toFixed(2) }; } } else if (typeof field === 'boolean') { /* 布爾 */ out = { payload: field }; } else if (typeof field === 'string') { /* 字符串 */ out = { payload: field }; } } return out;
經(jīng)過(guò)HTTP訪問(wèn)知心天氣的API后,耶對(duì)得到的JSON結(jié)果進(jìn)行解析,消息形如:
{ "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" } ] }
解析代碼也非常簡(jiǎn)單,text為當(dāng)前的天氣文本,code為當(dāng)前的天氣代碼:
var text = msg.payload.results[0].now.text; var code = msg.payload.results[0].now.code; return { payload: code };
然后發(fā)送最終的天氣碼到主題 /test/esp8266/sub,這個(gè)主題是ESP8266已經(jīng)訂閱的,ESP8266線程完成數(shù)據(jù)的獲取,然后發(fā)送天氣碼到消息隊(duì)列,LCD讀取消息隊(duì)列,得到天氣碼,然后讀取SD卡中的天氣圖標(biāo),顯示到屏幕上,完成天氣圖標(biāo)的更新。
03
最終效果
聯(lián)網(wǎng)進(jìn)度顯示界面
開(kāi)機(jī)自動(dòng)聯(lián)網(wǎng)、進(jìn)度條提示,F(xiàn)PS最低50!這個(gè)瑞薩的MCU跑LVGL完全無(wú)壓力
實(shí)時(shí)溫濕度、時(shí)間數(shù)據(jù)顯示?
接入Homeassistant記錄溫濕度數(shù)據(jù)
通過(guò)node-red接入到HA作為一個(gè)設(shè)備顯示當(dāng)前的溫濕度數(shù)據(jù)和板載LED的狀態(tài)
溫度數(shù)據(jù)的歷史曲線(開(kāi)了空調(diào)溫度是直線下降?。?/p>
濕度數(shù)據(jù)的歷史曲線
天貓精靈獲取板載LED狀態(tài)
設(shè)置了單擊觸摸按鍵開(kāi)關(guān)LED2亮滅的邏輯操作,然后會(huì)自動(dòng)上傳這個(gè)LED2的開(kāi)關(guān)狀態(tài)到MQTT服務(wù)器上,通過(guò)node-red來(lái)上傳到Homeassistent,搭配巴法云平臺(tái)接入到語(yǔ)音助手,我用的是天貓精靈,可以通過(guò)語(yǔ)音助手獲取到當(dāng)前LED2的狀態(tài),當(dāng)然只是做一個(gè)演示,可以實(shí)現(xiàn)的自動(dòng)化智能家居當(dāng)然還有很多的玩法。
04
視頻展示
05
總結(jié)
本作品開(kāi)發(fā)過(guò)程中體會(huì)到了瑞薩的開(kāi)發(fā)軟件十分的易用,方便,也學(xué)習(xí)到了LVGL V8、MQTT服務(wù)器數(shù)據(jù)包的收發(fā),node-red橋接MQTT消息包到HA的知識(shí)。
完成以上所有的功能后Flash使用了1MB出頭(主要是GUI的資源文件),這個(gè)單片機(jī)是有2MB的Flash,界面開(kāi)發(fā)還有很大的發(fā)揮空間。
1.8寸的小屏比較小,可以換成更大的屏和增加觸摸,但是RA6M5沒(méi)有專門(mén)的屏幕驅(qū)動(dòng)外設(shè),如果要拓展成并口MCU屏或者RGB屏還是有點(diǎn)受限的。
使用到了如下第三方軟件包,除FatFs使用BSD外別的均為MIT開(kāi)源協(xié)議
CJSON
EasyLogger
FatFs
letter-shell
MultiButton
LVGL V8
FreeRTOS
審核編輯:劉清
-
SPI
+關(guān)注
關(guān)注
17文章
1706瀏覽量
91581 -
智能家居
+關(guān)注
關(guān)注
1928文章
9562瀏覽量
185114 -
FreeRTOS
+關(guān)注
關(guān)注
12文章
484瀏覽量
62178 -
ESP8266
+關(guān)注
關(guān)注
50文章
962瀏覽量
45008 -
LVGL
+關(guān)注
關(guān)注
1文章
83瀏覽量
2969
原文標(biāo)題:【瑞薩RA MCU創(chuàng)意氛圍賽】項(xiàng)目23——基于FreeRTOS+LVGL V8智能家居儀表盤(pán)
文章出處:【微信號(hào):瑞薩MCU小百科,微信公眾號(hào):瑞薩MCU小百科】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論