野火电子论坛

 找回密码
 注册

QQ登录

只需一步,快速开始

查看: 1022|回复: 3

【瑞萨RA MCU创意氛围赛】基于FreeRTOS+LVGL V8智能家居仪表盘

[复制链接]
发表于 2023-8-19 01:57:37 | 显示全部楼层 |阅读模式
本帖最后由 POMIN 于 2023-8-21 11:17 编辑

用野火启明6M5开发板制作了一个基于 FreeRTOS 和 LVGL V8 的智能家居仪表盘,颜值较高,也可以作为桌面摆件使用,具体特点如下:
  • 采用 SPI + DTC 驱动 1.8寸 SPI 屏幕,超高帧率刷屏。
  • 采用 LVGL V8 界面库绘制界面,有丰富控件、动画(FPS稳定50以上!)。
  • 采用 ESP8266 联网,使用心知天气 API 获取当前天气并显示到屏幕。
  • 采用 ESP8266 联网,通过 MQTT 协议连接到云服务器,上传状态数据。
  • 采用鲁班猫2安装 EMQ 作为 MQTT 服务器,接收启明 6M5 上传数据。
  • 采用 Node-RED + Homeassistant 接入家庭自动化,与智能家居设备完美联动。

硬件平台介绍野火启明 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 温湿度传感器
软件设计方案
  • 采用 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 的全屏缓冲区,需要更新屏幕上的数据时只需要搬运数据即可。

  1. #if 1

  2. /*********************
  3. *      INCLUDES
  4. *********************/
  5. #include "lv_port_disp.h"
  6. #include <stdbool.h>

  7. /*********************
  8. *      DEFINES
  9. *********************/
  10. #ifndef MY_DISP_HOR_RES
  11.     #warning Please define or replace the macro MY_DISP_HOR_RES with the actual screen width, default value 320 is used for now.
  12.     #define MY_DISP_HOR_RES    128
  13. #endif

  14. #ifndef MY_DISP_VER_RES
  15.     #warning Please define or replace the macro MY_DISP_HOR_RES with the actual screen height, default value 240 is used for now.
  16.     #define MY_DISP_VER_RES    160
  17. #endif

  18. /**********************
  19. *      TYPEDEFS
  20. **********************/

  21. /**********************
  22. *  STATIC PROTOTYPES
  23. **********************/
  24. static void disp_init(void);

  25. static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p);

  26. /**********************
  27. *  STATIC VARIABLES
  28. **********************/

  29. /**********************
  30. *      MACROS
  31. **********************/

  32. /**********************
  33. *   GLOBAL FUNCTIONS
  34. **********************/

  35. void lv_port_disp_init(void)
  36. {
  37.     /*-------------------------
  38.      * Initialize your display
  39.      * -----------------------*/
  40.     disp_init();

  41.     /*-----------------------------
  42.      * Create a buffer for drawing
  43.      *----------------------------*/

  44.     /* Example for 2) */
  45.     static lv_disp_draw_buf_t draw_buf_dsc_2;
  46.     static lv_color_t buf_2_1[MY_DISP_HOR_RES * MY_DISP_VER_RES];   
  47.     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*/

  48.     /*-----------------------------------
  49.      * Register the display in LVGL
  50.      *----------------------------------*/

  51.     static lv_disp_drv_t disp_drv;                         /*Descriptor of a display driver*/
  52.     lv_disp_drv_init(&disp_drv);                    /*Basic initialization*/

  53.     /*Set up the functions to access to your display*/

  54.     /*Set the resolution of the display*/
  55.     disp_drv.hor_res = MY_DISP_HOR_RES;
  56.     disp_drv.ver_res = MY_DISP_VER_RES;

  57.     /*Used to copy the buffer's content to the display*/
  58.     disp_drv.flush_cb = disp_flush;

  59.     /*Set a display buffer*/
  60.     disp_drv.draw_buf = &draw_buf_dsc_2;

  61.     /*Required for Example 3)*/
  62.     //disp_drv.full_refresh = 1;

  63.     /* Fill a memory array with a color if you have GPU.
  64.      * Note that, in lv_conf.h you can enable GPUs that has built-in support in LVGL.
  65.      * But if you have a different GPU you can use with this callback.*/
  66.     //disp_drv.gpu_fill_cb = gpu_fill;

  67.     /*Finally register the driver*/
  68.     lv_disp_drv_register(&disp_drv);
  69. }

  70. /**********************
  71. *   STATIC FUNCTIONS
  72. **********************/

  73. /*Initialize your display and the required peripherals.*/
  74. static void disp_init(void)
  75. {
  76.     /*You code here*/
  77. }

  78. volatile bool disp_flush_enabled = true;

  79. /* Enable updating the screen (the flushing process) when disp_flush() is called by LVGL
  80. */
  81. void disp_enable_update(void)
  82. {
  83.     disp_flush_enabled = true;
  84. }

  85. /* Disable updating the screen (the flushing process) when disp_flush() is called by LVGL
  86. */
  87. void disp_disable_update(void)
  88. {
  89.     disp_flush_enabled = false;
  90. }

  91. /*Flush the content of the internal buffer the specific area on the display
  92. *You can use DMA or any hardware acceleration to do this operation in the background but
  93. *'lv_disp_flush_ready()' has to be called when finished.*/
  94. extern uint8_t lcd_buff[160][128][2];
  95. static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
  96. {
  97.     if(disp_flush_enabled) {
  98.         /*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/

  99.         int32_t x;
  100.         int32_t y;
  101.         for(y = area->y1; y <= area->y2; y++) {
  102.             for(x = area->x1; x <= area->x2; x++) {
  103.                 /*Put a pixel to the display. For example:*/
  104.                 /*put_px(x, y, *color_p)*/
  105.                 lcd_buff[y][x][0] = color_p->full >> 8;
  106.                 lcd_buff[y][x][1] = color_p->full;
  107.                 color_p++;
  108.             }
  109.         }
  110.     }

  111.     /*IMPORTANT!!!
  112.      *Inform the graphics library that you are ready with the flushing*/
  113.     lv_disp_flush_ready(disp_drv);
  114. }

  115. #else /*Enable this file at the top*/

  116. /*This dummy typedef exists purely to silence -Wpedantic.*/
  117. typedef int keep_pedantic_happy;
  118. #endif
复制代码


对于刷屏函数的移植只需实现数据的搬运,代码如下。

  1. extern uint8_t lcd_buff[160][128][2];
  2. static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
  3. {
  4.     if(disp_flush_enabled) {
  5.         /*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/

  6.         int32_t x;
  7.         int32_t y;
  8.         for(y = area->y1; y <= area->y2; y++) {
  9.             for(x = area->x1; x <= area->x2; x++) {
  10.                 /*Put a pixel to the display. For example:*/
  11.                 /*put_px(x, y, *color_p)*/
  12.                 lcd_buff[y][x][0] = color_p->full >> 8;
  13.                 lcd_buff[y][x][1] = color_p->full;
  14.                 color_p++;
  15.             }
  16.         }
  17.     }

  18.     /*IMPORTANT!!!
  19.      *Inform the graphics library that you are ready with the flushing*/
  20.     lv_disp_flush_ready(disp_drv);
  21. }
复制代码


在 lcd_thread 线程的 while 循环中只需使用 SPI 发送全屏缓冲到屏幕,代码如下


  1. void lcd_push_buff(void) {
  2.     R_SPI_Write(spilcd_spi0.p_ctrl, lcd_buff, LCD_W * LCD_H * 2, SPI_BIT_WIDTH_8_BITS);
  3. }
  4. /* 下面是主函数调用 */
  5. void lcd_thread_entry(void* pvParameters) {
  6.     FSP_PARAMETER_NOT_USED(pvParameters);
  7.     lcd_setup();

  8.     while (1) {
  9.         lcd_push_buff();
  10.         lv_task_handler();
  11.     }
  12. }
复制代码



界面设计与仿真
采用 NXP 的 GUI Guider 作为 PC 端的设计器和仿真器,GUI Guider 可以在 PC 端完成一站式的 LVGL 界面设计与仿真,例如下图所示。
在 GUI Guider 中对两个页面分别创建了一个定时器,并且实现了两个回调函数,代码如下,通过这个定时器回调函数来实现周期性的刷新屏幕显示的内容,更新网络连接状态、当前温湿度、当前时间、当前天气等数据。


  1. void timer_main_reflash_cb(lv_timer_t *t)
  2. {
  3.     static uint32_t tick;
  4.     lv_ui * gui = t->user_data;
  5. #ifdef __ARMCC_VERSION
  6.     float sensor_info[2];
  7.     if (pdTRUE == xQueueReceive(g_sensor2lcd_queue, sensor_info, pdMS_TO_TICKS(0))) {
  8.         lv_bar_set_value(gui->main_bar_humi, (uint32_t) sensor_info[0], LV_ANIM_ON);
  9.         lv_bar_set_value(gui->main_bar_temp, (uint32_t) sensor_info[1], LV_ANIM_ON);
  10.         lv_label_set_text_fmt(gui->main_label_humi, "%2d%%", (uint32_t) sensor_info[0]);
  11.         lv_label_set_text_fmt(gui->main_label_temp, "%2d'C", (uint32_t) sensor_info[1]);
  12.     }
  13.     rtc_time_t get_time;
  14.     if (pdTRUE == xQueueReceive(g_clock2lcd_queue, &get_time, pdMS_TO_TICKS(0))) {
  15.         lv_label_set_text_fmt(gui->main_label_hour, "%02d", get_time.tm_hour);
  16.         lv_label_set_text_fmt(gui->main_label_min, "%02d", get_time.tm_min);
  17.         lv_label_set_text_fmt(gui->main_label_sec, "%02d", get_time.tm_sec);
  18.     }
  19.     uint32_t num = 0;
  20.     if (pdTRUE == xQueueReceive(g_esp2lcd_queue, &num, pdMS_TO_TICKS(0))) {
  21.         if (num > 38) {
  22.             num = 99;
  23.         }
  24.         char path [30];
  25.         sprintf(path, "1:1:lvgl/weather/%d.jpg", num);
  26.         lv_img_set_src(gui->main_img_weather, path);
  27.     }
  28. #endif
  29. }

  30. const char str_ch[][40] = {
  31.     "连接WI-Fi...",
  32.     "连接WI-Fi失败!",
  33.     "连接WI-Fi成功!",
  34.     "连接MQTT服务器...",
  35.     "连接MQTT服务器失败",
  36.     "订阅MQTT主题...",
  37. };

  38. void timer_loading_reflash_cb(lv_timer_t *t)
  39. {
  40.     static uint32_t num = 0;
  41.     lv_ui * gui = t->user_data;
  42. #ifdef __ARMCC_VERSION
  43.     if (pdTRUE == xQueueReceive(g_esp2lcd_queue, &num, pdMS_TO_TICKS(0))) {
  44.         lv_label_set_text(gui->loading_tip, str_ch[num]);
  45.         lv_bar_set_value(gui->loading_process, num * 20, LV_ANIM_ON);
  46.         if (num >= 5) {
  47.             setup_scr_main(gui);
  48.             lv_scr_load(gui->main);
  49.         }
  50.     }
  51. #else
  52.     num += 3;
  53.     lv_label_set_text(gui->loading_tip, str_ch[num / 20]);
  54.     lv_bar_set_value(gui->loading_process, num, LV_ANIM_ON);
  55.     if (num >= 100) {
  56.         setup_scr_main(gui);
  57.         lv_scr_load(gui->main);
  58.     }
  59. #endif
  60. }
复制代码



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 的设备。

  1. var field = msg.payload.hum;
  2. var out;

  3. if (field == null) {
  4.     out = { payload: null };
  5. } else {
  6.     if (typeof field === 'number') {
  7.         if (Number(field) === Math.round(field)) {
  8.             /* 整数 */
  9.             out = { payload: field };
  10.         } else {
  11.             /* 小数 */
  12.             out = { payload: field.toFixed(2) };
  13.         }
  14.     } else if (typeof field === 'boolean') {
  15.         /* 布尔 */
  16.         out = { payload: field };
  17.     } else if (typeof field === 'string') {
  18.         /* 字符串 */
  19.         out = { payload: field };
  20.     }
  21. }
  22. return out;
复制代码

经过 HTTP 访问知心天气的 API 后,耶对得到的 JSON 结果进行解析,消息形如
  1. {
  2.     "results": [
  3.         {
  4.             "location": {
  5.                 "id": "WTW3SJ5ZBJUY",
  6.                 "name": "Shanghai",
  7.                 "country": "CN",
  8.                 "path": "Shanghai,Shanghai,China",
  9.                 "timezone": "Asia/Shanghai",
  10.                 "timezone_offset": "+08:00"
  11.             },
  12.             "now": {
  13.                 "text": "Cloudy",
  14.                 "code": "4",
  15.                 "temperature": "35"
  16.             },
  17.             "last_update": "2023-08-13T12:10:14+08:00"
  18.         }
  19.     ]
  20. }
复制代码


解析代码也非常简单,text 为当前的天气文本,code 为当前的天气代码
  1. var text = msg.payload.results[0].now.text;
  2. var code = msg.payload.results[0].now.code;

  3. return { payload: code };
复制代码


然后发送最终的天气码到主题 /test/esp8266/sub,这个主题是 ESP8266 已经订阅的, ESP8266 线程完成数据的获取,然后发送天气码到消息队列,LCD 读取消息队列,得到天气码,然后读取 SD 卡中的天气图标,显示到屏幕上,完成天气图标的更新。
最终效果联网进度显示界面
开机自动联网、进度条提示,FPS最低 50!这个瑞萨的 MCU 跑 LVGL 完全无压力
实时温湿度、时间数据显示
接入Homeassistant 记录温湿度数据
通过 node-red 接入到 HA 作为一个设备显示当前的温湿度数据和板载 LED 的状态
温度数据的历史曲线(开了空调温度是直线下降啊)
湿度数据的历史曲线
天猫精灵获取板载 LED 状态
设置了单击触摸按键开关 LED2 亮灭的逻辑操作,然后会自动上传这个 LED2 的开关状态到 MQTT 服务器上,通过 node-red 来上传到 Homeassistent,搭配巴法云平台接入到语音助手,我用的是天猫精灵,可以通过语音助手获取到当前 LED2 的状态,当然只是做一个演示,可以实现的自动化智能家居当然还有很多的玩法。


总结
  • 本作品开发过程中体会到了瑞萨的开发软件十分的易用,方便,也学习到了 LVGL V8、MQTT 服务器数据包的收发,node-red 桥接 MQTT 消息包到 HA 的知识
  • 完成以上所有的功能后 Flash 使用了 1 MB 出头(主要是 GUI 的资源文件),这个单片机是有 2MB 的Flash,界面开发还有很大的发挥空间。
  • 1.8 寸的小屏比较小,可以换成更大的屏和增加触摸,但是 RA6M5 没有专门的屏幕驱动外设,如果要拓展成并口 MCU 屏或者 RGB屏还是有点受限的。
开源链接第三方软件包开源协议
使用到了如下第三方软件包,除 FatFs 使用 BSD 外别的均为 MIT 开源协议
  • CJSON
  • EasyLogger
  • FatFs
  • letter-shell
  • MultiButton
  • LVGL V8
  • FreeRTOS
开源代码链接
代码开源在 gitee: https://gitee.com/pomin-163/ebf_6m5_lvgl_env_panel
这个小作品的所有代码采用 BSD 开源协议,随意使用



回复

使用道具 举报

发表于 2023-8-20 08:55:47 | 显示全部楼层
大佬牛逼,建议出个教程;
回复 支持 反对

使用道具 举报

发表于 2023-8-22 10:45:47 | 显示全部楼层
大佬太牛了,建议出教程,我也在玩Home Assistant
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

联系站长|手机版|野火电子官网|野火淘宝店铺|野火电子论坛 ( 粤ICP备14069197号 ) 大学生ARM嵌入式2群

GMT+8, 2024-5-2 12:18 , Processed in 0.047905 second(s), 25 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表