基于FreeRTOS+LVGL V8智能家居仪表盘设计

用野火启明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 y2; y++) {            for(x = area->x1; x 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 y2; y++) {            for(x = area->x1; x 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


医疗界又添新设备 电子皮肤可100%自我修复
石墨烯研究频现重大突破,新一轮‘投资热潮’欲涌动
Atmel扩展ARM926-based微控制器系列
2022年浙江省职业院校技能大赛高职组集成电路开发及应用赛项圆满举办
忆芯科技:将全面打入国产替代存储市场
基于FreeRTOS+LVGL V8智能家居仪表盘设计
苹果新iPhone 11性能、价格和详参来了!
港大研发出“被动式LED电源”技术将用于路灯
产业互联网和消费互联网融合创新对经济社会有何影响
有什么原因能够影响网络中设备的通信状况呢?
60GHz工业毫米波可覆盖24GHz无法满足的应用
具有ETHERCAT从站功能的通讯网关产品介绍
Google Play如何为数十亿用户提供安全的体验
自举电路的自举电容在布线时,为什么电容的低电压脚要采用蛇形布线的方式?
128内核、512GB内存的电脑,是什么样的,有多无敌
什么是电池记忆效应?怎样消除?
如何实现基于SSD神经网络构建实时手部检测
苹果11怎么投屏到电视?
PC GPU价格还在稳步下跌,甚至跌破建议零售价
抓住5G时代三大机遇,打造电信企业破竹之势