1838|2

155

帖子

1

TA的资源

一粒金砂(高级)

楼主
 

【Luckfox幸狐 RV1106 Linux 开发板测评】十四、LVGL显示DHT11的温湿度 [复制链接]

 

本人【Luckfox幸狐 RV1106 Linux 开发板测评】帖子链接:

一、开箱及测试

二、SDK获取与编译镜像

三、GPIO点灯

四、通过PC机共享网络接入Internet和Ubuntu下Python点灯

五、编译Buildroot系统并测试摄像头

六、PWM控制——命令和C方式

七、PWM控制——Python和设备树方式

八、ADC和UART测试

九、Python控制I2C驱动OLED

十、C程序控制I2C驱动OLED

十一、SPI驱动LCD

十二、实现FrameBuffer设备及LVGL应用

十三、五向开关作为LVGL输入设备

 

本篇记录Luckfox Pro Max开发板编写DHT11内核驱动,并将测出的温湿度通过LVGL框架显示到0.96寸LCD上的过程,并利用lv_font_conv工具自建字体文件实现图标显示。

1、DHT11内核驱动

DHT11的驱动主要参考Luckfox Wiki的“温湿度传感器DHT11模块使用”篇(https://wiki.luckfox.com/zh/Luckfox-Pico/Luckfox-Pico-dht11)。文档上提供了DHT11.zip(百度网盘),其中包含内核驱动源文件dht11_drv.c、测试应用源文件dht11_test.c和Makefile,以及设备树文件。

不过官方提供的设备树文件使用GPIO1_C7,而这个管脚已经被用作LCD模块控制信号了。所以本人这里改用管脚GPIO2_PA1即板上25号脚,相应的设备树文件源码如下:

/ {
	model = "Luckfox Pico Max";
	compatible = "rockchip,rv1103g-38x38-ipc-v10", "rockchip,rv1106";

	/*DHT11*/
	dht11_sensor {
		compatible = "dht11";
		pinctrl-names = "default";
		status = "okay";
		pinctrl-0 = <&gpio2_pa1>;

		dht11[url=home.php?mod=space&uid=490]@1[/url] {
			gpios = <&gpio2 RK_PA1 GPIO_ACTIVE_HIGH>;
			label = "dht11";
			linux,default-trigger = "humidity";
		};
	};
// 省略其它部分
}

&pinctrl {
	/*DHT11*/
	gpio2-pa1 {
		gpio2_pa1:gpio2-pa1 {
			rockchip,pins = <2 RK_PA1 RK_FUNC_GPIO &pcfg_pull_none>;
		};
	};
// 省略其它部分
}

 

修改设备树文件rv1106g-luckfox-pico-pro-max.dts后,依然是编译内核并重新烧录系统。新系统启动后,开发板中会增加设备文件“/dev/dht11”,而案例提供的内核驱动和测试应用都是基于“/dev/dht11”文件的读写操作,所以不用修改代码,直接make即可生成测试程序。

 

 

图14-1 DHT11项目make后的生成文件

 

项目make后生成内核驱动模块文件dht11_drv.ko和应用程序文件dht11。这两个文件都要推送到开发板,然后利用命令“insmod dht11_drv.ko”安装内核模块,再赋予dht11执行权限,并连接传感器执行程序进行测试,开发板可以正确读取温湿度。

 

 

图14-2 dht11测试效果——每秒输出温湿度值

 

 

图14-3 LVGL显示温湿度并带温度计、湿度计图标

 

上图是将DHT11读取代码整合到lvgl_demo项目中的效果,这里显示了两个图标,另外温湿度值也是自建字体库实现的,main.c具体代码如下:

#include "lvgl/lvgl.h"
#include "DEV_Config.h"
#include "lv_drivers/display/fbdev.h"
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>

typedef unsigned char u8;
typedef unsigned short u16;
// 存放DHT11读数的结构体类型
typedef struct DHT11_SENSOR_DATA
{
  u16 temp; // Temperature
  u16 hum;  // Humidity
} dht11_data;
// “Ctrl+C”触发sigint_handler()并退出程序
volatile sig_atomic_t exit_flag = 0;
void sigint_handler(int signo)
{
    if (signo == SIGINT)
    {
        exit_flag = 1;
    }
}

/**
  申明字体:myfont.c自建方正姚体、iconfont30.c是两个图标
*/
LV_FONT_DECLARE(myfont);
LV_FONT_DECLARE(iconfont30);
// 两个图标字串(图标的utf-8编码)采用宏定义形式
#define ICON_TEMP "\xEE\x9A\x92"
#define ICON_HUMI "\xEE\x99\xA9"

/**
  by Firr. 五向按键做LVGL输入设备——本例输入功能未使用
*/
// 按键读取函数——返回按键对应IO编号以区分不同的按键
uint8_t getKeyValue(void) {
    if(GET_KEY_RIGHT == 0)     return KEY_RIGHT_PIN;
    else if(GET_KEY_LEFT == 0) return KEY_LEFT_PIN;
    else if(GET_KEY_UP == 0)   return KEY_UP_PIN;
    else if(GET_KEY_DOWN == 0) return KEY_DOWN_PIN;
    else if(GET_KEY_PRESS == 0)return KEY_PRESS_PIN;
    else                       return 0;
}
// 自定义输入事件接口——输入读取回调
void keypadRead(lv_indev_drv_t *indev_drv, lv_indev_data_t *data) {
  static uint32_t last_key = 0;
  // 读取按键值——即按键对应IO编号,作为“输入事件的数值”
  uint32_t act_key = getKeyValue();
  if(act_key != 0) {
    // 有按键按下,则修改“输入事件的状态”
    data->state = LV_INDEV_STATE_PR;

    /* 转换按键值为“LVGL控件字符(LVGL control characters)” */
    switch(act_key) {
      case KEY_LEFT_PIN:
          act_key = LV_KEY_LEFT;  // 减少值或向左移动
          break;
      case KEY_RIGHT_PIN:
          act_key = LV_KEY_RIGHT; // 减少值或向右移动
          break;
      case KEY_UP_PIN:
          act_key = LV_KEY_PREV;  // 聚焦到上一对象
          break;
      case KEY_DOWN_PIN:
          act_key = LV_KEY_NEXT;  // 聚焦到下一对象
          break;
      case KEY_PRESS_PIN:
          act_key = LV_KEY_ENTER; // 触发LV_EVENT_PRESSED/CLICKED/LONG_PRESSED
          break;
    }
    last_key = act_key;
  } else {
      data->state = LV_INDEV_STATE_REL;
  }
  // 将不同按键转化为LVGL定义的“输入事件key”
  data->key = last_key;
}

// LVGL显示缓存区及显示回调声明
#define DISP_BUF_SIZE (160 * 128)
void fbdev_flush(lv_disp_drv_t * drv, const lv_area_t * area, lv_color_t * color_p);

// 按键设备指针——用于设置给“按键设备”设置“(焦点)组”
lv_indev_t *indev_keypad;

// 温湿度界面显示函数
void fontExample(dht11_data* data) {
  char result[100] = {0};
  lv_obj_clean(lv_scr_act());     // 清屏

  // 创建风格,用以设置字体
  static lv_style_t icon_style; // 图标字体
  lv_style_init(&icon_style);
  lv_style_set_text_font(&icon_style, &iconfont30);

  static lv_style_t text_style; // 文字字体
  lv_style_init(&text_style);
  lv_style_set_text_font(&text_style, &myfont);

  // 显示温度计图标
  lv_obj_t *temp_icon = lv_label_create( lv_scr_act() );
  lv_label_set_text(temp_icon, ICON_TEMP);
  lv_obj_align(temp_icon, LV_ALIGN_LEFT_MID, 25, -10);
  lv_obj_add_style(temp_icon, &icon_style, LV_PART_MAIN);
  // 显示温度值
  lv_obj_t *temp_text = lv_label_create( lv_scr_act() );
  sprintf(result, "%02d C", data->temp >> 8);
  lv_label_set_text(temp_text, result);
  lv_obj_align(temp_text, LV_ALIGN_LEFT_MID, 10, 20);
  lv_obj_add_style(temp_text, &text_style, LV_PART_MAIN);
  // 显示湿度计图标
  lv_obj_t *humi_icon = lv_label_create( lv_scr_act() );
  lv_label_set_text(humi_icon, ICON_HUMI);
  lv_obj_align(humi_icon, LV_ALIGN_RIGHT_MID, -25, -10);
  lv_obj_add_style(humi_icon, &icon_style, LV_PART_MAIN);
  // 显示湿度值
  lv_obj_t *humi_text = lv_label_create( lv_scr_act() );
  sprintf(result, "%02d %%", data->hum >> 8);
  lv_label_set_text(humi_text, result);
  lv_obj_align(humi_text, LV_ALIGN_RIGHT_MID, -10, 20);
  lv_obj_add_style(humi_text, &text_style, LV_PART_MAIN);
}

int main(void)
{
    int fd;                   // DHT11文件描述符
    int retval;
    int counter = 0;          // 计数变量
    dht11_data Curdht11_data; // DHT11读数

    signal(SIGINT, sigint_handler);
    printf("Press CTRL+C to exit.\n");

    /*打开DHT11设备文件*/
    fd = open("/dev/dht11", O_RDONLY);
    if (fd == -1)
    {
        perror("open dht11 error\n");
        exit(-1);
    }

    sleep(1);
    printf("open /dev/dht11 successfully\n");

    /*LittlevGL init*/
    lv_init();

    /*Linux frame buffer device init*/
    fbdev_init();

    /*A small buffer for LittlevGL to draw the screen's content*/
    static lv_color_t buf[DISP_BUF_SIZE];

    /*Initialize a descriptor for the buffer*/
    static lv_disp_draw_buf_t disp_buf;
    lv_disp_draw_buf_init(&disp_buf, buf, NULL, DISP_BUF_SIZE);

    /*Initialize and register a display driver*/
    static lv_disp_drv_t disp_drv;
    lv_disp_drv_init(&disp_drv);
    disp_drv.draw_buf   = &disp_buf;
    disp_drv.flush_cb   = fbdev_flush;
    disp_drv.hor_res    = 160;
    disp_drv.ver_res    = 128;
    lv_disp_drv_register(&disp_drv);

    /*Initialize pin*/
    DEV_ModuleInit();

    /* lvgl输入初始化 */
    static lv_indev_drv_t indev_drv;     // 输入设备实例初始化
    lv_indev_drv_init(&indev_drv);
    indev_drv.type = LV_INDEV_TYPE_KEYPAD;
    indev_drv.read_cb = keypadRead;      // 输入事件回调
    indev_keypad = lv_indev_drv_register(&indev_drv); // 注册输入设备获取返回指针

    // 显示应用UI——先做一次显示因为DHT11未检测,所以初运行时显示0值
    fontExample(&Curdht11_data);

    /*Handle LitlevGL tasks (tickless mode)*/
    while(!exit_flag) {
        lv_timer_handler();
        usleep(5000);
        if(counter++ >= 1000) {
          counter = 0;
          retval = read(fd, &Curdht11_data, sizeof(Curdht11_data));
          if (retval != -1)
          {
            fontExample(&Curdht11_data);
          }
          if (Curdht11_data.temp != 0xffff)
            printf("Temperature:%d.%d C, Humidity:%d.%d %%RH\n", Curdht11_data.temp >> 8, Curdht11_data.temp & 0xff, \
                                                                    Curdht11_data.hum >> 8, Curdht11_data.hum & 0xff);
        }
    }

    close(fd);
    sleep(1);
    return 0;
}

/*Set in lv_conf.h as `LV_TICK_CUSTOM_SYS_TIME_EXPR`*/
uint32_t custom_tick_get(void)
{
    static uint64_t start_ms = 0;
    if(start_ms == 0) {
        struct timeval tv_start;
        gettimeofday(&tv_start, NULL);
        start_ms = (tv_start.tv_sec * 1000000 + tv_start.tv_usec) / 1000;
    }

    struct timeval tv_now;
    gettimeofday(&tv_now, NULL);
    uint64_t now_ms;
    now_ms = (tv_now.tv_sec * 1000000 + tv_now.tv_usec) / 1000;

    uint32_t time_ms = now_ms - start_ms;
    return time_ms;
}

2、LVGL字体自建

上述案例使用的字体myfont和iconfont30,实际是myfont.c和iconfont30.c两个源文件,与main.c同在项目根目录,由LVGL提供的字体工具lv_font_conv生成。

LVGL提供在线方式和离线方式两种自定义字体的方法,这里使用离线方式(需要Node.js)。

 

 

图14-4 LVGL自定义字体文档截图

 

lv_font_conv是LVGL官方编写的一套离线字体转换工具,由于此工具是由node.js编写的,所以要在第一步安装node运行环境。

离线工具没有图形化界面,需要使用命令行输入命令来转换,要比在线复杂点,优点就是无需网络,而且速度非常快。下述是其项目链接及使用流程图,gitee上有一些个人用户fork的版本,访问速度更快些。

lv_font_conv GitHub链接:https://github.com/lvgl/lv_font_conv 。

lv_font_conv gitee个人fork版:https://gitee.com/dmcus/lv_font_conv 

 

 

图14-5 lv_font_conv使用流程图

 

 

图14-6 lv_font_conv全局安装(npm i lv_font_conv -g)

 

lv_font_conv建议通过npm进行全局安装(命令如上图所示)。然后,就是选取字体文件(*.ttf格式),这里选取系统自带字体“方正姚体”作为示例——当然也可以网上下载其它有特色的字体文件。

系统字体位于“..\Windows\Fonts”目录下,在其中找到“方正姚体 常规(实际文件名FZYTK.TTF)”,然后拷贝到自建文件夹下以方便后续操作。

 

 

图14-7 拷贝字体文件

 

接着,命令行进入到自建文件夹目录,通过lv_font_conv进行字体转换,比如:

lv_font_conv --no-compress --format lvgl --font ./FZYTK.TTF -o ./myfont.c --bpp 4 --size 25 --symbols 天津城建大学 -r 0x20-0x7F

 

 

图14-8 命令行转换字体

 

上述命令生成“方正姚体 25号 英文数字+天津城建大学六个汉字”的字体库源文件“myfont.c”。命令相关参数项解释如下:

--no-compress: 不压缩

--format lvgl: 输出格式LVGL(还支持bin格式,不过需要引入文件系统)

--font ./FZYTK.TTF 要转换字体文件(当前目录下的FZYTK.TTF)

-o ./myfont.c 输出文件的路径及文件名

--bpp 4 抗锯齿大小设置为4

--size 25 输出字体为25像素高

--symbols 天津城建大学 要转换的字符“天津城建大学”

-r 0x20-0x7F ASCII编码范围(即全部可见ASCII字符)

 

另外,还要注意一点,lv_font_conv默认创建的是UTF-8编码字符,针对使用的IDE,需要注意设置编码为“UTF-8”,否则字体加载会失效。

3、图标以字体方式导入

其实LVGL中已经集成了很多图标字体,如:WIFI,蓝牙,保存,复制,等图标。那如果项目中需要用到其它没有的图标,就要用到阿里的一个免费的图标字体平台“iconfont”。iconfont平台允许用户自定义下载多种格式的icon,平台也可将图标转换为字体文件,用户可以免费注册使用。

使用图标字体的关键点有两个:一是得到图标字体的ttf文件;二是图标字体编码转UTF-8——iconfont平台提供的是Unicode统一码。

我们首先来获取图标字体的ttf文件,这需要申请iconfont账户并登录,然后建立“项目”,并将选取的图标加入购物车再导入项目,最后下载项目。项目以压缩包形式下载,其中就包含ttf文件。

 

 

 

         

     

图14-9 iconfont得到图标字体文件

 

上图可以看到,我们下载的图标文件包含两个图标,其Unicode编码对应16进制分别为:湿度计图标--0xe669,温度计图标--0xe692(图中“&#x<16进制数>;”是HTML实体编码形式,其16进制数就是对应Unicode码)。

导出的“iconfont.ttf”文件用同样的方法转换为lvgl字体库,命令示例:

lv_font_conv --no-compress --format lvgl --font ./iconfont.ttf -o ./iconfont30.c --bpp 4 --size 30 -r 0xe669,0xe692

上述命令生成字码库源文件“iconfont30.c”,图标大小30像素,其中重点关注输入参数“-r”,用于设置所需字符的unicode编码范围,这里表示设置“0xe669和0xe692”两个字符。

 

 

图14-10 lv_font_conv命令的-r参数传值示例

 

可见lv_font_conv在转码时需要用到Unicode编码,但是源程序中则需要使用UTF-8编码(实际UTF-8是Unicode的一种存储方式上的编码)。

UTF-8的编码规则很简单,只有两条:

  1. 对于单字节的符号字节的第一位设为0后面7位为这个符号的Unicode因此对于英语字母UTF-8编码和ASCII码是相同的
  2. 对于n字节的符号n>1),第一个字节的前n位都设为1第n+1位设为0后面字节的前两位一律设为10剩下的没有提及的二进制位全部为这个符号的Unicode

 

 

图14-11 UTF-8编码规则示意图

 

依照上述规则,本人编写了一段JS脚本用以进行编码转换,利用Node执行脚本,直接在命令行后将两个字符的Unicode码作为输入参数,执行后控制台输出转换的UTF-8编码——且以C语言字符串形式给出。

/**
 * Unicode --> UTF-8,传参和返回都是Number型
 * @param {Unicode编码数值} unicode 
 * @returns UTF-8编码数组
 */
const unicodeToUTF8 = (unicode) => {
  if(unicode>=0x00000000 && unicode<=0x0000007F) {
    return unicode;
  } else if(unicode>=0x00000080 && unicode<=0x000007FF) {
    var r1 = (((unicode & 0x7C0) >> 6) | 0xC0) << 8;
    var r2 = (unicode & 0x03F) | 0x80;
    return r1 | r2;
  } else if(unicode>=0x00000800 && unicode<=0x0000FFFF) {
    var r1 = (((unicode & 0xF000) >> 12) | 0xE0) << 16;
    var r2 = (((unicode & 0x0FC0) >> 6) | 0x80) << 8;
    var r3 = ((unicode & 0x003F) | 0x80);
    return r1 | r2 | r3;
  } else if(unicode>=0x00010000 && unicode<=0x0010FFFF) {
    var r1 = (((unicode & 0x1C0000) >> 18) | 0xE0) << 24;
    var r2 = (((unicode & 0x03F000) >> 12) | 0x80) << 16;
    var r3 = (((unicode & 0x000FC0) >> 6) | 0x80) << 8;
    var r4 = ((unicode & 0x00003F) | 0x80);
    return r1 | r2 | r3 | r4;
  } else {
    return false;
  }
}

/**
 * 将Number型传参转为字节数组
 * @param {Number型数值} num 
 * @returns 对应字节数组
 */
const numberToBytes = (num) => {  
  let bytes = [];  
  while(num > 0) {  
    let byte = num & 0xFF; // 取得最低8位  
    bytes.unshift(byte);   // 将这个字节放到数组的开始  
    num >>= 8;             // 右移8位,相当于除以256  
  }  
  return bytes;  
}

/**
 * process.argv是Node执行JS脚本时的输入命令行构成的数组
 * 索引0为Node命令,索引1为js文件名
 * 从索引2开始是命令行输入参数
 * 比如:当前脚本执行命令“node .\utf.js 0xe669 0xe692”
 * 控制台输出0xe669 0xe692的对应转换结果:"\xEE\x99\xA9" "\xEE\x9A\x92"
 */
let result = '';
// console.log(process.argv);
for(let i=2; i<process.argv.length; i++) {
  let num = parseInt(process.argv[i], 16); // 输入参数即16进制字串转整数
  let resNum = unicodeToUTF8(num);
  let resBytes = numberToBytes(resNum);
  let resStr = resBytes.map(b => `\\x${b.toString(16).toUpperCase()}`).join('');
  result += `"${resStr}" `;
}
// 输出最终转换结果
console.log(result);

 

 

图14-12 UTF-8转换脚本执行展示

 

上图可以看到两个图标字体最终的UTF-8编码为:湿度计图标--"\xEE\x99\xA9" 温度计图标--"\xEE\x9A\x92"。这两个输出就是lvgl_demo案例中关于温度计宏定义值的由来。

最新回复

看了一下LVGL与stm32的TouchGFX来比,就显示差了很多了。  详情 回复 发表于 2024-3-14 10:14
点赞 关注
 
 

回复
举报

7048

帖子

11

TA的资源

版主

沙发
 
看了一下LVGL与stm32的TouchGFX来比,就显示差了很多了。

点评

嗯,LVGL在Linux环境用也是有点勉强,不如QT。在MCU端应该是资源要求上要比TouchGFX低,适合中低端MCU吧。  详情 回复 发表于 2024-3-16 12:16
 
 
 

回复

155

帖子

1

TA的资源

一粒金砂(高级)

板凳
 
lugl4313820 发表于 2024-3-14 10:14 看了一下LVGL与stm32的TouchGFX来比,就显示差了很多了。

嗯,LVGL在Linux环境用也是有点勉强,不如QT。在MCU端应该是资源要求上要比TouchGFX低,适合中低端MCU吧。

 
 
 

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

查找数据手册?

EEWorld Datasheet 技术支持

相关文章 更多>>
关闭
站长推荐上一条 1/7 下一条

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

About Us 关于我们 客户服务 联系方式 器件索引 网站地图 最新更新 手机版

站点相关: 国产芯 安防电子 汽车电子 手机便携 工业控制 家用电子 医疗电子 测试测量 网络通信 物联网

北京市海淀区中关村大街18号B座15层1530室 电话:(010)82350740 邮编:100190

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2025 EEWORLD.com.cn, Inc. All rights reserved
快速回复 返回顶部 返回列表