本人【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的编码规则很简单,只有两条:
- 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的Unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
- 对于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案例中关于温度计宏定义值的由来。