【Funpack2-3】基于FireBeetle 2 ESP32-E开发板的LVGL移植及传感器显示
Github-KafCoppelia/FireBeetle2_lvgl_sensors
本项目基于FireBeetle2 ESP32-E开发板,使用VSCode+Arduino插件开发,通过TFT_eSPI库使用硬件SPI驱动LCD屏幕,并移植LVGL。搭配SHT40温湿度传感器与VL53L0激光测距传感器,将检测的数据较为美观地显示在屏幕上。
Github-lvgl/lvgl
FireBeetle ESP32-E是一款基于ESP-WROOM-32E双核芯片的主控板,它专为IoT设计。
它支持WIFI和蓝牙双模通信并具有体积小巧、超低功耗、板载充电电路、接口易用等特性。可灵活的用于家庭物联网改装、工业物联网改装、可穿戴设备等等。
硬件资源:
此外,介绍本工程内的两款传感器。MIKROE TEMP&HUM 15 CLICK 为一款SHT40温湿度传感器,I2C模式通信,可以获取温度、湿度信息。本工程通过Adafruit SHT4x库实现数据的读取,可直接在Arduino搜索下载该库。
MIKROE-TEMP&HUM 15 CLICK产品介绍
Github-adafruit/Adafruit_SHT4X
DFRobot的VL53L0激光测距传感器是一款基于意法半导体(STMicroelectronics)新出的基于飞行时间测距 (ToF) 原理设计的高精度测距传感器,通过I2C通信,其Arduino库文件通过官网帮助页面可直接获得,也可直接在Arduino内搜索下载该库。
DFRobot-VL53L0
SHT40及VL53L0直接连在FireBeetle2的两路硬件I2C上,如此可直接调用库函数实现对传感器数据的读取,十分方便。两个传感器通过I2C地址查询,所以无所谓I2C1、I2C2的区分。
此外,TFT LCD 240x320通过硬件SPI驱动,具体连接关系如下(除电源与地外):
引脚 | 含义 | 引脚编号 |
---|---|---|
I2C SCL | SHT40的数据信号 | 开发板左侧22 |
I2C SDA | SHT40的时钟信号 | 开发板左侧21 |
I2C SCL | VL53L0的数据信号 | 开发板右侧22 |
I2C SDA | VL53L0的时钟信号 | 开发板右侧21 |
SCL | LCD的时钟信号 | 18,SPI SCK |
MOSI | LCD的数据信号 | 23,SPI MOSI |
MISO | 不用 | 19 |
CS | LCD的片选信号 | 16,普通IO |
DC | LCD的D/C信号 | 17,普通IO |
RST | LCD的复位信号 | 连接至开发板RESET |
BL | LCD的背光信号 | 4,普通IO |
上述LCD与开发板的连接关系需要在配置TFT_eSPI驱动时再次用到。
首先在Arduino内,安装TFT_eSPI库,再在TFT_eSPI库内找到User_Setup.h
,并根据屏幕实际配置做修改,注意看注释的说明。主要需要修改的地方有:
Github-Bodmer/TFT_eSPI
例如,本工程的LCD屏幕为ST7789,对应保留的宏定义有:
#define ST7789_DRIVER
#define TFT_RGB_ORDER TFT_BGR
#define TFT_WIDTH 240
#define TFT_HEIGHT 320
#define TFT_INVERSION_OFF
#define TFT_BL 4 // LED back-light control pin
#define TFT_BACKLIGHT_ON HIGH // Level to turn ON back-light (HIGH or LOW)
#define TFT_MISO 19
#define TFT_MOSI 23
#define TFT_SCLK 18
#define TFT_CS 16 // Chip select control pin
#define TFT_DC 17 // Data Command control pin
#define TFT_RST -1 // Set TFT_RST to -1 if display RESET is connected to ESP32 board RST
#define SPI_FREQUENCY 27000000
之后,下载LVGL release 源码,并通过zip包安装至Arduino库内。主要步骤可参考LVGL on Arduino帮助页面,主要进行:
lv_conf_template.h
到与Arduino 库目录中lvgl同级的地方,改名为lv_conf.h
。lv_conf.h
并将第一个更改为启用文件的内容#if 1
LV_COLOR_DEPTH 16
(根据屏幕实际,本工程为16)LV_TICK_CUSTOM 1
LV_USE_LOG 1
LV_BUILD_EXAMPLES 1
外,参见LVGL on Arduino进行一些额外的步骤在代码上,需要为LVGL实现一个my_disp_flush()
函数,使用TFT_eSPI库的函数进行包装,即可将绘图功能打包给LVGL实现。Arduino实现的最简单的例子(LVGL库内可找到该例):
#include
#include
#include
/*Change to your screen resolution*/
static const uint16_t screenWidth = 480;
static const uint16_t screenHeight = 320;
static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf[ screenWidth * 10 ];
TFT_eSPI tft = TFT_eSPI(screenWidth, screenHeight); /* TFT instance */
/* Display flushing */
void my_disp_flush( lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p )
{
uint32_t w = ( area->x2 - area->x1 + 1 );
uint32_t h = ( area->y2 - area->y1 + 1 );
tft.startWrite();
tft.setAddrWindow( area->x1, area->y1, w, h );
tft.pushColors( ( uint16_t * )&color_p->full, w * h, true );
tft.endWrite();
lv_disp_flush_ready( disp );
}
void setup()
{
Serial.begin( 115200 ); /* prepare for possible serial debug */
String LVGL_Arduino = "Hello Arduino! ";
LVGL_Arduino += String('V') + lv_version_major() + "." + lv_version_minor() + "." + lv_version_patch();
Serial.println( LVGL_Arduino );
Serial.println( "I am LVGL_Arduino" );
lv_init();
#if LV_USE_LOG != 0
lv_log_register_print_cb( my_print ); /* register print function for debugging */
#endif
tft.begin(); /* TFT init */
tft.setRotation( 3 ); /* Landscape orientation, flipped */
lv_disp_draw_buf_init( &draw_buf, buf, NULL, screenWidth * 10 );
/*Initialize the display*/
static lv_disp_drv_t disp_drv;
lv_disp_drv_init( &disp_drv );
/*Change the following line to your display resolution*/
disp_drv.hor_res = screenWidth;
disp_drv.ver_res = screenHeight;
disp_drv.flush_cb = my_disp_flush;
disp_drv.draw_buf = &draw_buf;
lv_disp_drv_register( &disp_drv );
lv_demo_widgets();
Serial.println( "Setup done" );
}
void loop()
{
lv_timer_handler(); /* let the GUI do its work */
delay( 5 );
}
首先,实现两款传感器数据的读取函数,便于之后在lvgl事务内直接获取数据。对于SHT40,在初始化后,读取温度及湿度信息
Adafruit_SHT4x sht4 = Adafruit_SHT4x(); /* Global instance */
void sht40_get_measured(sensors_event_t *humidity, sensors_event_t *temp) {
sht4.getEvent(humidity, temp);// populate temp and humidity objects with fresh data
Serial.print("Temperature: "); Serial.print(temp->temperature); Serial.println(" degrees C");
Serial.print("Humidity: "); Serial.print(humidity->relative_humidity); Serial.println("% rH");
}
对于VL53L0,TOF测距,在初始化后,读取测量的距离信息:
DFRobot_VL53L0X sensor; /* Global instance */
float vl53l0x_get_measured() {
float distance = sensor.getDistance();
Serial.print("Distance: ");Serial.println(distance);
return distance;
}
屏幕主要分为三块地方:
主要地,通过lvgl实现三个定时器事务,温湿度定时器,每秒获取并刷新显示文字:
lv_timer_t *sht4x_update_timer;
sht4x_update_timer = lv_timer_create(temperature_update, 1000, NULL); /* Always working */
static void temperature_update(lv_timer_t *timer) {
sht40_get_measured(&sht4x_humidity, &sht4x_temp);
lv_label_set_text_fmt(temp_label, "Temp: %.1f °C", sht4x_temp.temperature);
lv_label_set_text_fmt(humd_label, "Humidity: %.1f% rH", sht4x_humidity.relative_humidity);
}
对应的两个标签对象:
lv_obj_t *temp_label = lv_label_create(lv_scr_act());
lv_label_set_text(temp_label, "Temp: 0 °C");
lv_obj_set_style_text_color(lv_scr_act(), lv_color_black(), LV_PART_MAIN);
lv_obj_align(temp_label, LV_ALIGN_TOP_LEFT, 10, 10);
lv_obj_t *humd_label = lv_label_create(lv_scr_act());
lv_label_set_text(humd_label, "Humidity: 0% rH");
lv_obj_set_style_text_color(lv_scr_act(), lv_color_black(), LV_PART_MAIN);
lv_obj_align(humd_label, LV_ALIGN_TOP_LEFT, 10, 30);
按键检测定时器,每30毫秒检测按键是否按下,其中还需要有改变开关控件的交互,当按键按下,开关切换状态,若为打开,则旁边标签显示“TOF measuring”,且TOF开始测距,vl53l0x_update_timer
定时器继续工作;若为关闭,则边标签显示“TOF idle”,且TOF停止工作,vl53l0x_update_timer
定时器暂停:
int last_btn_state = HIGH;
lv_timer_t *btn_state_timer;
btn_state_timer = lv_timer_create(btn_pressed_check, 30, NULL);
static void btn_pressed_check(lv_timer_t *timer) {
int reading = digitalRead(BTN_PIN);
if (reading != last_btn_state) {
if (reading == LOW) {
if (lv_obj_has_state(tof_measuring_sw, LV_STATE_CHECKED)) {
lv_obj_clear_state(tof_measuring_sw, LV_STATE_CHECKED);
vl53l0x_stop();
lv_timer_pause(vl53l0x_update_timer);
lv_label_set_text(btn_label, "TOF idle");
}
else {
lv_obj_add_state(tof_measuring_sw, LV_STATE_CHECKED);
vl53l0x_start();
lv_timer_resume(vl53l0x_update_timer);
lv_label_set_text(btn_label, "TOF measuring");
}
}
}
last_btn_state = reading;
}
对应的开关控件及标签对象:
lv_obj_t *btn_label = lv_label_create(lv_scr_act());
lv_label_set_text(btn_label, "TOF measuring");
lv_obj_set_style_text_color(lv_scr_act(), lv_color_black(), LV_PART_MAIN);
lv_obj_align(btn_label, LV_ALIGN_CENTER, 50, -100);
lv_obj_t *tof_measuring_sw = lv_switch_create(lv_scr_act());
lv_obj_set_size(tof_measuring_sw, 40, 20);
lv_obj_align(tof_measuring_sw, LV_ALIGN_TOP_RIGHT, -10, 10);
lv_obj_add_state(tof_measuring_sw, LV_STATE_CHECKED); /* switch on in default */
VL530L0定时任务,每250毫秒更新测量,将下一个数据显示在折线图右侧:
lv_timer_t *vl53l0x_update_timer;
vl53l0x_update_timer = lv_timer_create(tof_update, 250, NULL);
static void tof_update(lv_timer_t *timer) {
float tof_new_data = vl53l0x_get_measured();
lv_chart_set_next_value(tof_data_chart, tof_data_series, (int)tof_new_data);
lv_chart_refresh(tof_data_chart);
lv_label_set_text_fmt(y_next_value_label, "%.1f", tof_new_data);
if (tof_new_data > 1000) {
lv_obj_align_to(y_next_value_label, tof_data_chart, LV_ALIGN_OUT_RIGHT_MID, -40, 10);
}
else {
lv_obj_align_to(y_next_value_label, tof_data_chart, LV_ALIGN_OUT_RIGHT_MID, -40, -10);
}
}
折线图的更新显示,通过LVGL可以很简单的实现,一些标签对象的位置摆放,请参考LVGL帮助文档,查看具体函数的用法即可。
上电后,两个传感器即刻开始工作。
当按下按键,关闭TOF测量开关时,折线图停止更新,开关控件关闭且显示“TOF idle”:
当按下按键,开启TOF测量开关时,折线图继续动态更新,开关控件开启且显示“TOF measuring”:
此次成功移植了LVGL至LCD上,实现了对SHT40、VL53L0温湿度信息、ToF测距信息的简单显示,使用LVGL显示各种效果都很方便、很好看,代码上也很有条理,通过事件、定时器等功能可以实现多种控件的交互。
美中不足的是,限于时间,原本想实现动态显示ToF测距折线图的纵坐标数据,但是实现起来确实不是想的那么容易,因此目前无法实现折线图纵坐标的灵活切换。当测量数据较大时,数据会绘制在折线图范围之外。