正点原子imx6ull-mini-Linux驱动之Linux LCD 驱动实验(19)

LCD 是很常用的一个外设,在裸机篇中我们讲解了如何编写 LCD 裸机驱动,在 Linux 下 LCD 的使用更加广泛,在搭配 QT 这样的 GUI 库下可以制作出非常精美的 UI 界面。本章我们 就来学习一下如何在 Linux 下驱动 LCD 屏幕。

1:Linux 下 LCD 驱动简析

1.1:Framebuffer 设备

先来回顾一下裸机的时候 LCD 驱动是怎么编写的,裸机 LCD 驱动编写流程如下:

①、初始化 I.MX6U 的 eLCDIF 控制器,重点是 LCD 屏幕宽(width)、高(height)、hspw、 hbp、hfp、vspw、vbp 和 vfp 等信息。

②、初始化 LCD 像素时钟。

③、设置 RGBLCD 显存。

④、应用程序直接通过操作显存来操作 LCD,实现在 LCD 上显示字符、图片等信息。

在 Linux 中应用程序最终也是通过操作 RGB LCD 的显存来实现在 LCD 上显示字符、图片 等信息 。在裸机中我们可以随意的分配显存,但是在Linux 系统中内存的管理很严格,显存是 需要申请的,不是你想用就能用的 。而且因为虚拟内存的存在,驱动程序设置的显存和应用程 序访问的显存要是同一片物理内存。 为了解决上述问题,Framebuffer 诞生了, Framebuffer 翻译过来就是帧缓冲,简称 fb,因 此大家在以后的 Linux 学习中见到"Framebuffer"或者"fb"的话第一反应应该想到 RGBLCD 或者显示设备。fb 是一种机制,将系统中所有跟显示有关的硬件以及软件集合起来 ,虚拟出一 个 fb 设备,当我们编写好 LCD 驱动以后会生成一个名为/dev/fbX(X=0~n)的设备,应用程序通 过访问/dev/fbX 这个设备就可以访问 LCD。NXP 官方的 Linux 内核默认已经开启了 LCD 驱动, 因此我们是可以看到/dev/fb0 这样一个设备,如图 59.1.1.1 所示:

图 59.1.1.1 中的/dev/fb0 就是 LCD 对应的设备文件,/dev/fb0 是个字符设备,因此肯定有 file_operations 操作集,fb 的 file_operations 操作集定义在 drivers/video/fbdev/core/fbmem.c 文件 中,如下所示:

cpp 复制代码
1495 static const struct file_operations fb_fops = {
1496 .owner = THIS_MODULE,
1497 .read = fb_read,
1498 .write = fb_write,
1499 .unlocked_ioctl = fb_ioctl,
1500 #ifdef CONFIG_COMPAT
1501 .compat_ioctl = fb_compat_ioctl,
1502 #endif
1503 .mmap = fb_mmap,
1504 .open = fb_open,
1505 .release = fb_release,
1506 #ifdef HAVE_ARCH_FB_UNMAPPED_AREA
1507 .get_unmapped_area = get_fb_unmapped_area,
1508 #endif
1509 #ifdef CONFIG_FB_DEFERRED_IO
1510 .fsync = fb_deferred_io_fsync,
1511 #endif
1512 .llseek = default_llseek,
1513 };

关于 fb 的详细处理过程就不去深究了,本章我们的重点是驱动起来 ALPHA 开发板上的 LCD。

1.2:LCD 驱动简析

LCD 裸机例程主要分两部分:

①、获取 LCD 的屏幕参数。

②、根据屏幕参数信息来初始化 eLCDIF 接口控制器。

不同分辨率的 LCD 屏幕其 eLCDIF 控制器驱动代码都是一样的,只需要修改好对应的屏 幕参数即可。屏幕参数信息属于屏幕设备信息内容,这些肯定是要放到设备树中的,因此我们 本章实验的主要工作就是修改设备树,NXP 官方的设备树已经添加了 LCD 设备节点,只是此 节点的 LCD 屏幕信息是针对 NXP 官方 EVK 开发板所使用的 4.3 寸 480*272 编写的,我们需 要将其改为我们所使用的屏幕参数。 我们简单看一下 NXP 官方编写的 Linux 下的 LCD 驱动,打开 imx6ull.dtsi,然后找到 lcdif 节点内容,如下所示:

cpp 复制代码
1 lcdif: lcdif@021c8000 {
2 compatible = "fsl,imx6ul-lcdif", "fsl,imx28-lcdif";
3 reg = <0x021c8000 0x4000>;
4 interrupts = <GIC_SPI 5 IRQ_TYPE_LEVEL_HIGH>;
5 clocks = <&clks IMX6UL_CLK_LCDIF_PIX>,
6 <&clks IMX6UL_CLK_LCDIF_APB>,
7 <&clks IMX6UL_CLK_DUMMY>;
8 clock-names = "pix", "axi", "disp_axi";
9 status = "disabled";
10 };

示例代码 59.1.2.1 中的 lcdif 节点信息是所有使用 I.MX6ULL 芯片的板子所共有的,并不是 完整的 lcdif 节点信息。像屏幕参数这些需要根据不同的硬件平台去添加 ,比如向 imx6ull-alientek-emmc.dts 中的 lcdif 节点添加其他的属性信息。从示例代码 59.1.2.1 可以看出 lcdif 节点 的 compatible 属性值为"fsl,imx6ul-lcdif"和"fsl,imx28-lcdif",因此在 Linux 源码中搜索这两个 字符串即可找到 I.MX6ULL 的 LCD 驱动文件,这个文件为 drivers/video/fbdev/mxsfb.c,mxsfb.c 就是 I.MX6ULL 的 LCD 驱动文件,在此文件中找到如下内容:

cpp 复制代码
1362 static const struct of_device_id mxsfb_dt_ids[] = {
1363 { .compatible = "fsl,imx23-lcdif", .data = &mxsfb_devtype[0], },
1364 { .compatible = "fsl,imx28-lcdif", .data = &mxsfb_devtype[1], },
1365 { /* sentinel */ }
1366 };
......
1625 static struct platform_driver mxsfb_driver = {
1626 .probe = mxsfb_probe,
1627 .remove = mxsfb_remove,
1628 .shutdown = mxsfb_shutdown,
1629 .id_table = mxsfb_devtype,
1630 .driver = {
1631 .name = DRIVER_NAME,
1632 .of_match_table = mxsfb_dt_ids,
1633 .pm = &mxsfb_pm_ops,
1634 },
1635 };
1636
1637 module_platform_driver(mxsfb_driver);

从示例代码 59.1.2.2 可以看出,这是一个标准的 platform 驱动,当驱动和设备匹配以后 mxsfb_probe 函数就会执行。在看 mxsfb_probe 函数之前我们先简单了解一下 Linux 下 Framebuffer 驱动的编写流程,Linux 内核将所有的 Framebuffer 抽象为一个叫做 fb_info 的结构 体 ,fb_info 结构体包含了 Framebuffer 设备的完整属性和操作集合,因此每一个 Framebuffer 设 备都必须有一个 fb_info。换言之就是,LCD 的驱动就是构建 fb_info,并且向系统注册 fb_info 的过程。fb_info 结构体定义在 include/linux/fb.h 文件里面,内容如下(省略掉条件编译):

cpp 复制代码
448 struct fb_info {
449 atomic_t count;
450 int node;
451 int flags;
452 struct mutex lock; /* 互斥锁 */
453 struct mutex mm_lock; /* 互斥锁,用于 fb_mmap 和 smem_*域*/
454 struct fb_var_screeninfo var; /* 当前可变参数 */
455 struct fb_fix_screeninfo fix; /* 当前固定参数 */
456 struct fb_monspecs monspecs; /* 当前显示器特性 */
457 struct work_struct queue; /* 帧缓冲事件队列 */
458 struct fb_pixmap pixmap; /* 图像硬件映射 */
459 struct fb_pixmap sprite; /* 光标硬件映射 */
460 struct fb_cmap cmap; /* 当前调色板 */
461 struct list_head modelist; /* 当前模式列表 */
462 struct fb_videomode *mode; /* 当前视频模式 */
463
464 #ifdef CONFIG_FB_BACKLIGHT /* 如果 LCD 支持背光的话 */
465 /* assigned backlight device */
466 /* set before framebuffer registration, 
467 remove after unregister */
468 struct backlight_device *bl_dev; /* 背光设备 */
469
470 /* Backlight level curve */
471 struct mutex bl_curve_mutex; 
472 u8 bl_curve[FB_BACKLIGHT_LEVELS];
473 #endif
......
479 struct fb_ops *fbops; /* 帧缓冲操作函数集 */ 
480 struct device *device; /* 父设备 */
481 struct device *dev; /* 当前 fb 设备 */
482 int class_flag; /* 私有 sysfs 标志 */
......
486 char __iomem *screen_base; /* 虚拟内存基地址(屏幕显存) */
487 unsigned long screen_size; /* 虚拟内存大小(屏幕显存大小) */
488 void *pseudo_palette; /* 伪 16 位调色板 */
......
507 };

fb_info 结构体的成员变量很多,我们重点关注 var、fix、fbops、screen_base、screen_size 和 pseudo_palette。mxsfb_probe 函数的主要工作内容为:

①、申请 fb_info。

②、初始化 fb_info 结构体中的各个成员变量。

③、初始化 eLCDIF 控制器。

④、使用 register_framebuffer 函数向 Linux 内核注册初始化好的 fb_info。register_framebuffer 函数原型如下:

int register_framebuffer(struct fb_info *fb_info)

函数参数和返回值含义如下:

fb_info:需要上报的 fb_info。

返回值:0,成功;负值,失败。

感觉跟输入设备差不多啊,把注册的设备先申请一个input,再初始化,再注册把设备注册为input

接下来我们简单看一下 mxsfb_probe 函数,函数内容如下(有缩减):

cpp 复制代码
1369 static int mxsfb_probe(struct platform_device *pdev)
1370 {
1371 const struct of_device_id *of_id =
1372 of_match_device(mxsfb_dt_ids, &pdev->dev);
1373 struct resource *res;
1374 struct mxsfb_info *host;
1375 struct fb_info *fb_info;
1376 struct pinctrl *pinctrl;
1377 int irq = platform_get_irq(pdev, 0);
1378 int gpio, ret;
1379
......
1394
1395 res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
1396 if (!res) {
1397 dev_err(&pdev->dev, "Cannot get memory IO resource\n");
1398 return -ENODEV;
1399 }
1400
1401 host = devm_kzalloc(&pdev->dev, sizeof(struct mxsfb_info),
GFP_KERNEL);
1402 if (!host) {
1403 dev_err(&pdev->dev, "Failed to allocate IO resource\n");
1404 return -ENOMEM;
1405 }
1406
1407 fb_info = framebuffer_alloc(sizeof(struct fb_info), &pdev->dev);
1408 if (!fb_info) {
1409 dev_err(&pdev->dev, "Failed to allocate fbdev\n");
1410 devm_kfree(&pdev->dev, host);
1411 return -ENOMEM;
1412 }
1413 host->fb_info = fb_info;
1414 fb_info->par = host;
1415
1416 ret = devm_request_irq(&pdev->dev, irq, mxsfb_irq_handler, 0,
1417 dev_name(&pdev->dev), host);
1418 if (ret) {
1419 dev_err(&pdev->dev, "request_irq (%d) failed with 
1420 error %d\n", irq, ret);
1421 ret = -ENODEV;
1422 goto fb_release;
1423 }
1424
1425 host->base = devm_ioremap_resource(&pdev->dev, res);
1426 if (IS_ERR(host->base)) {
1427 dev_err(&pdev->dev, "ioremap failed\n");
1428 ret = PTR_ERR(host->base);
1429 goto fb_release;
1430 }
......
1461
1462 fb_info->pseudo_palette = devm_kzalloc(&pdev->dev, sizeof(u32) *
1463 16, GFP_KERNEL);
1464 if (!fb_info->pseudo_palette) {
1465 ret = -ENOMEM;
1466 goto fb_release;
1467 }
1468
1469 INIT_LIST_HEAD(&fb_info->modelist);
1470
1471 pm_runtime_enable(&host->pdev->dev);
1472
1473 ret = mxsfb_init_fbinfo(host);
1474 if (ret != 0)
1475 goto fb_pm_runtime_disable;
1476
1477 mxsfb_dispdrv_init(pdev, fb_info);
1478
1479 if (!host->dispdrv) {
1480 pinctrl = devm_pinctrl_get_select_default(&pdev->dev);
1481 if (IS_ERR(pinctrl)) {
1482 ret = PTR_ERR(pinctrl);
1483 goto fb_pm_runtime_disable;
1484 }
1485 }
1486
1487 if (!host->enabled) {
1488 writel(0, host->base + LCDC_CTRL);
1489 mxsfb_set_par(fb_info);
1490 mxsfb_enable_controller(fb_info);
1491 pm_runtime_get_sync(&host->pdev->dev);
1492 }
1493
1494 ret = register_framebuffer(fb_info);
1495 if (ret != 0) {
1496 dev_err(&pdev->dev, "Failed to register framebuffer\n");
1497 goto fb_destroy;
1498 }
......
1525 return ret;
1526 }

第 1374 行,host 结构体指针变量,表示 I.MX6ULL 的 LCD 的主控接口,mxsfb_info 结构 体是 NXP 定义的针对 I.MX 系列 SOC 的 Framebuffer 设备结构体。也就是我们前面一直说的设 备结构体,此结构体包含了 I.MX 系列 SOC 的 Framebuffer 设备详细信息,比如时钟、eLCDIF 控制器寄存器基地址、fb_info 等。

第 1395 行,从设备树中获取 eLCDIF 接口控制器的寄存器首地址,设备树中 lcdif 节点已 经设置了 eLCDIF 寄存器首地址为 0X021C8000,因此 res=0X021C8000。 第 1401 行,给 host 申请内存,host 为 mxsfb_info 类型结构体指针。

第 1407 行,给 fb_info 申请内存,也就是申请 fb_info。

第 1413~1414 行,设置 host 的 fb_info 成员变量为 fb_info,设置 fb_info 的 par 成员变量为 host。通过这一步就将前面申请的 host 和 fb_info 联系在了一起。

第 1416 行,申请中断,中断服务函数为 mxsfb_irq_handler。

第 1425 行,对从设备树中获取到的寄存器首地址(res)进行内存映射,得到虚拟地址,并保 存到 host 的 base 成员变量。因此通过访问 host 的 base 成员即可访问 I.MX6ULL 的整个 eLCDIF 寄存器。其实在 mxsfb.c 中已经定义了 eLCDIF 各个寄存器相比于基地址的偏移值,如下所示:

cpp 复制代码
67 #define LCDC_CTRL 0x00
68 #define LCDC_CTRL1 0x10
69 #define LCDC_V4_CTRL2 0x20
70 #define LCDC_V3_TRANSFER_COUNT 0x20
71 #define LCDC_V4_TRANSFER_COUNT 0x30
......
89 #define LCDC_V4_DEBUG0 0x1d0
90 #define LCDC_V3_DEBUG0 0x1f0

大家可以对比着《I.MX6ULL 参考手册》中的 eLCDIF 章节检查一下示例代码 59.1.2.4 中 的这些寄存器有没有错误。 继续回到示例代码 59.1.2.5中的 mxsfb_probe 函数,

第1462 行,给 fb_info 中的 pseudo_palette 申请内存。

第 1473 行,调用 mxsfb_init_fbinfo 函数初始化 fb_info,重点是 fb_info 的 var、fix、fbops, screen_base 和 screen_size。其中 fbops 是 Framebuffer 设备的操作集,NXP 提供的 fbops 为 mxsfb_ops,内容如下:

cpp 复制代码
987 static struct fb_ops mxsfb_ops = {
988 .owner = THIS_MODULE,
989 .fb_check_var = mxsfb_check_var,
990 .fb_set_par = mxsfb_set_par,
991 .fb_setcolreg = mxsfb_setcolreg,
992 .fb_ioctl = mxsfb_ioctl,
993 .fb_blank = mxsfb_blank,
994 .fb_pan_display = mxsfb_pan_display,
995 .fb_mmap = mxsfb_mmap,
996 .fb_fillrect = cfb_fillrect,
997 .fb_copyarea = cfb_copyarea,
998 .fb_imageblit = cfb_imageblit,
999 };

关于 mxsfb_ops 里面的各个操作函数这里就不去详解的介绍了。mxsfb_init_fbinfo 函数通过 调用 mxsfb_init_fbinfo_dt 函数从设备树中获取到 LCD 的各个参数信息。最后,mxsfb_init_fbinfo 函数会调用 mxsfb_map_videomem 函数申请 LCD 的帧缓冲内存(也就是显存)。

第 1489~1490 行,设置 eLCDIF 控制器的相应寄存器。

第 1494 行,最后调用 register_framebuffer 函数向 Linux 内核注册 fb_info。 mxsfb.c 文件很大,还有一些其他的重要函数,比如 mxsfb_remove、mxsfb_shutdown 等, 这里我们就简单的介绍了一下 mxsfb_probe 函数,至于其他的函数大家自行查阅。

用SOC厂商的linux内核还怪好的嘞,人家给写好了,我们改参数就好了,不然自己要去写这么多东西吗,难度太大了吧

2:LCD 驱动程序编写

前面已经说了,6ULL 的 eLCDIF 接口驱动程序 NXP 已经编写好了,因此 LCD 驱动部分 我们不需要去修改。我们需要做的就是按照所使用的 LCD 来修改设备树。重点要注意三个地 方:

①、LCD 所使用的 IO 配置。

②、LCD 屏幕节点修改,修改相应的属性值,换成我们所使用的 LCD 屏幕参数。

③、LCD 背光节点信息修改,要根据实际所使用的背光 IO 来修改相应的设备节点信息。 接下来我们依次来看一下上面这两个节点改如何去修改:

2.1:LCD 屏幕 IO 配置

首先要检查一下设备树中 LCD 所使用的 IO 配置,这个其实 NXP 都已经给我们写好了, 不需要修改,不过我们还是要看一下。打开 imx6ull-alientek-emmc.dts 文件,在 iomuxc 节点中 找到如下内容:

cpp 复制代码
1 pinctrl_lcdif_dat: lcdifdatgrp {
2 fsl,pins = <
3 MX6UL_PAD_LCD_DATA00__LCDIF_DATA00 0x79
4 MX6UL_PAD_LCD_DATA01__LCDIF_DATA01 0x79
5 MX6UL_PAD_LCD_DATA02__LCDIF_DATA02 0x79
6 MX6UL_PAD_LCD_DATA03__LCDIF_DATA03 0x79
7 MX6UL_PAD_LCD_DATA04__LCDIF_DATA04 0x79
8 MX6UL_PAD_LCD_DATA05__LCDIF_DATA05 0x79
9 MX6UL_PAD_LCD_DATA06__LCDIF_DATA06 0x79
10 MX6UL_PAD_LCD_DATA07__LCDIF_DATA07 0x79
11 MX6UL_PAD_LCD_DATA08__LCDIF_DATA08 0x79
12 MX6UL_PAD_LCD_DATA09__LCDIF_DATA09 0x79
13 MX6UL_PAD_LCD_DATA10__LCDIF_DATA10 0x79
14 MX6UL_PAD_LCD_DATA11__LCDIF_DATA11 0x79
15 MX6UL_PAD_LCD_DATA12__LCDIF_DATA12 0x79
16 MX6UL_PAD_LCD_DATA13__LCDIF_DATA13 0x79
17 MX6UL_PAD_LCD_DATA14__LCDIF_DATA14 0x79
18 MX6UL_PAD_LCD_DATA15__LCDIF_DATA15 0x79
19 MX6UL_PAD_LCD_DATA16__LCDIF_DATA16 0x79
20 MX6UL_PAD_LCD_DATA17__LCDIF_DATA17 0x79
21 MX6UL_PAD_LCD_DATA18__LCDIF_DATA18 0x79
22 MX6UL_PAD_LCD_DATA19__LCDIF_DATA19 0x79
23 MX6UL_PAD_LCD_DATA20__LCDIF_DATA20 0x79
24 MX6UL_PAD_LCD_DATA21__LCDIF_DATA21 0x79
25 MX6UL_PAD_LCD_DATA22__LCDIF_DATA22 0x79
26 MX6UL_PAD_LCD_DATA23__LCDIF_DATA23 0x79
27 >;
28 };
29
30 pinctrl_lcdif_ctrl: lcdifctrlgrp {
31 fsl,pins = <
32 MX6UL_PAD_LCD_CLK__LCDIF_CLK 0x79
33 MX6UL_PAD_LCD_ENABLE__LCDIF_ENABLE 0x79
34 MX6UL_PAD_LCD_HSYNC__LCDIF_HSYNC 0x79
35 MX6UL_PAD_LCD_VSYNC__LCDIF_VSYNC 0x79
36 >;
37 pinctrl_pwm1: pwm1grp {
38 fsl,pins = <
39 MX6UL_PAD_GPIO1_IO08__PWM1_OUT 0x110b0
40 >;
41 };

第 2~27 行,子节点 pinctrl_lcdif_dat,为 RGB LCD 的 24 根数据线配置项。

第 30~36 行,子节点 pinctrl_lcdif_ctrl,RGB LCD 的 4 根控制线配置项,包括 CLK、 ENABLE、VSYNC 和 HSYNC。

第 37~40 行,子节点 pinctrl_pwm1,LCD 背光 PWM 引脚配置项。这个引脚要根据实际 情况设置,这里我们建议大家在以后的学习或工作中,LCD 的背光 IO 尽量和半导体厂商的官 方开发板一致。

注意示例代码 59.3.1 中默认将 LCD 的电气属性都设置为 0X79,这里将其都改为 0X49, 也就是将 LCD 相关 IO 的驱动能力改为 R0/1,也就是降低 LCD 相关 IO 的驱动能力。因为前 面已经说了,正点原子的 ALPHA 开发板上的 LCD 接口用了三个 SGM3157 模拟开关,为了防 止模拟开关影响到网络,因此这里需要降低 LCD 数据线的驱动能力,如果你所使用的板子没 有用到模拟开关那么就不需要将 0X79 改为 0X49。

2.2:LCD 屏幕参数节点信息修改

继续在 imx6ull-alientek-emmc.dts 文件中找到 lcdif 节点,节点内容如下所示:

cpp 复制代码
1 &lcdif {
2 pinctrl-names = "default";
3 pinctrl-0 = <&pinctrl_lcdif_dat /* 使用到的 IO */
4 &pinctrl_lcdif_ctrl
5 &pinctrl_lcdif_reset>;
6 display = <&display0>;
7 status = "okay";
8 
9 display0: display { /* LCD 属性信息 */
10 bits-per-pixel = <16>; /* 一个像素占用几个 bit */
11 bus-width = <24>; /* 总线宽度 */
12
13 display-timings {
14 native-mode = <&timing0>; /* 时序信息 */
15 timing0: timing0 { 
16 clock-frequency = <9200000>; /* LCD 像素时钟,单位 Hz */
17 hactive = <480>; /* LCD X 轴像素个数 */
18 vactive = <272>; /* LCD Y 轴像素个数 */
19 hfront-porch = <8>; /* LCD hfp 参数 */
20 hback-porch = <4>; /* LCD hbp 参数 */
21 hsync-len = <41>; /* LCD hspw 参数 */
22 vback-porch = <2>; /* LCD vbp 参数 */
23 vfront-porch = <4>; /* LCD vfp 参数 */
24 vsync-len = <10>; /* LCD vspw 参数 */
25
26 hsync-active = <0>; /* hsync 数据线极性 */
27 vsync-active = <0>; /* vsync 数据线极性 */
28 de-active = <1>; /* de 数据线极性 */
29 pixelclk-active = <0>; /* clk 数据线先极性 */
30 };
31 };
32 };
33 };

示例代码 59.3.2 就是向 imx6ull.dtsi 文件中的 lcdif 节点追加的内容,我们依次来看一下示 例代码 59.3.2 中的这些属性都是写什么含义。

第 3 行,pinctrl-0 属性,LCD 所使用的 IO 信息,这里用到了 pinctrl_lcdif_dat、pinctrl_lcdif_ctrl 和 pinctrl_lcdif_reset 这三个 IO 相关的节点,前两个在示例代码 59.3.1 中已经讲解了。 pinctrl_lcdif_reset 是 LCD 复位 IO 信息节点,正点原子的 I.MX6U-ALPHA 开发板的 LCD 没有 用到复位 IO,因此 pinctrl_lcdif_reset 可以删除掉

第 6 行,display 属性,指定 LCD 属性信息所在的子节点,这里为 display0,下面就是 display0 子节点内容。

第 9~32 行,display0 子节点,描述 LCD 的参数信息,

第 10 行的 bits-per-pixel 属性用于指 明一个像素占用的 bit 数,默认为 16bit。本教程我们将 LCD 配置为 RGB888 模式,因此一个像 素点占用 24bit,bits-per-pixel 属性要改为 24。

第 11 行的 bus-width 属性用于设置数据线宽度, 因为要配置为 RGB888 模式,因此 bus-width 也要设置为 24。

第 13~30 行,这几行非常重要!因为这几行设置了 LCD 的时序参数信息,NXP 官方的 EVK 开发板使用了一个 4.3 寸的 480*272 屏幕,因此这里默认是按照 NXP 官方的那个屏幕参数设置 的。每一个属性的含义后面的注释已经写的很详细了,大家自己去看就行了,这些时序参数就 是我们重点要修改的,需要根据自己所使用的屏幕去修改。 这里以正点原子的 ATK7016(7 寸 1024*600)屏幕为例,将 imx6ull-alientek-emmc.dts 文件中 的 lcdif 节点改为如下内容:

cpp 复制代码
1 &lcdif {
2 pinctrl-names = "default";
3 pinctrl-0 = <&pinctrl_lcdif_dat /* 使用到的 IO */
4 &pinctrl_lcdif_ctrl>;
5 display = <&display0>;
6 status = "okay";
7 
8 display0: display { /* LCD 属性信息 */
9 bits-per-pixel = <24>; /* 一个像素占用 24bit */
10 bus-width = <24>; /* 总线宽度 */
11
12 display-timings {
13 native-mode = <&timing0>; /* 时序信息 */
14 timing0: timing0 { 
15 clock-frequency = <51200000>;/* LCD 像素时钟,单位 Hz */
16 hactive = <1024>; /* LCD X 轴像素个数 */
17 vactive = <600>; /* LCD Y 轴像素个数 */
18 hfront-porch = <160>; /* LCD hfp 参数 */
19 hback-porch = <140>; /* LCD hbp 参数 */
20 hsync-len = <20>; /* LCD hspw 参数 */
21 vback-porch = <20>; /* LCD vbp 参数 */
22 vfront-porch = <12>; /* LCD vfp 参数 */
23 vsync-len = <3>; /* LCD vspw 参数 */
24
25 hsync-active = <0>; /* hsync 数据线极性 */
26 vsync-active = <0>; /* vsync 数据线极性 */
27 de-active = <1>; /* de 数据线极性 */
28 pixelclk-active = <0>; /* clk 数据线先极性 */
29 };
30 };
31 };
32 };

由于我的屏幕是4.3寸 800*480的,所以我们要根据裸机实验的内容去设置

cpp 复制代码
if(lcdid == ATK7084) {
		tftlcd_dev.height = 480;	
		tftlcd_dev.width = 800;
		tftlcd_dev.vspw = 1;
		tftlcd_dev.vbpd = 23;
		tftlcd_dev.vfpd = 22;
		tftlcd_dev.hspw = 1;
		tftlcd_dev.hbpd = 46;
		tftlcd_dev.hfpd = 210;	
		lcdclk_init(30, 3, 7);	/* 初始化LCD时钟 34.2MHz */
}

转换成设备树的形式就是

cpp 复制代码
1 &lcdif {
2 pinctrl-names = "default";
3 pinctrl-0 = <&pinctrl_lcdif_dat /* 使用到的 IO */
4 &pinctrl_lcdif_ctrl>;
5 display = <&display0>;
6 status = "okay";
7 
8 display0: display { /* LCD 属性信息 */
9 bits-per-pixel = <24>; /* 一个像素占用 24bit */
10 bus-width = <24>; /* 总线宽度 */
11
12 display-timings {
13 native-mode = <&timing0>; /* 时序信息 */
14 timing0: timing0 { 
15 clock-frequency = <34200000>;/* LCD 像素时钟,单位 Hz */
16 hactive = <800>; /* LCD X 轴像素个数 */
17 vactive = <480>; /* LCD Y 轴像素个数 */
18 hfront-porch = <210>; /* LCD hfp 参数 */
19 hback-porch = <46>; /* LCD hbp 参数 */
20 hsync-len = <1>; /* LCD hspw 参数 */
21 vback-porch = <23>; /* LCD vbp 参数 */
22 vfront-porch = <22>; /* LCD vfp 参数 */
23 vsync-len = <1>; /* LCD vspw 参数 */
24
25 hsync-active = <0>; /* hsync 数据线极性 */
26 vsync-active = <0>; /* vsync 数据线极性 */
27 de-active = <1>; /* de 数据线极性 */
28 pixelclk-active = <0>; /* clk 数据线先极性 */
29 };
30 };
31 };
32 };

第 3 行,设置 LCD 屏幕所使用的 IO,删除掉原来的 pinctrl_lcdif_reset,因为没有用到屏 幕复位 IO,其他的 IO 不变。

第 9 行,使用 RGB888 模式,所以一个像素点是 24bit。

第 15~23 行,ATK4384 屏幕时序参数,根据自己所使用的屏幕修改即可。

2.3:LCD 屏幕背光节点信息

正点原子的 LCD 接口背光控制 IO 连接到了 I.MX6U 的 GPIO1_IO08 引脚上,GPIO1_IO08 复用为 PWM1_OUT,通过 PWM 信号来控制 LCD 屏幕背光的亮度,这个我们已经在第二十九 章详细的讲解过了。正点原子 I.MX6U-ALPHA 开发板的 LCD 背光引脚和 NXP 官方 EVK 开发 板的背光引脚一样,因此背光的设备树节点是不需要修改的,但是考虑到其他同学可能使用别 的开发板或者屏幕,LCD 背光引脚和 NXP 官方 EVK 开发板可能不同,因此我们还是来看一下 如何在设备树中添加背光节点信息。 首先是 GPIO1_IO08 这个 IO 的配置,在 imx6ull-alientek-emmc.dts 中找到如下内容:

cpp 复制代码
1 pinctrl_pwm1: pwm1grp {
2 fsl,pins = <
3 MX6UL_PAD_GPIO1_IO08__PWM1_OUT 0x110b0
4 >;
5 };

pinctrl_pwm1 节点就是 GPIO1_IO08 的配置节点,从第 3 行可以看出,设置 GPIO1_IO08 这个 IO 复用为 PWM1_OUT,并且设置电气属性值为 0x110b0。 LCD 背光要用到 PWM1,因此也要设置 PWM1 节点,在 imx6ull.dtsi 文件中找到如下内 容:

cpp 复制代码
1 pwm1: pwm@02080000 {
2 compatible = "fsl,imx6ul-pwm", "fsl,imx27-pwm";
3 reg = <0x02080000 0x4000>;
4 interrupts = <GIC_SPI 83 IRQ_TYPE_LEVEL_HIGH>;
5 clocks = <&clks IMX6UL_CLK_PWM1>,
6 <&clks IMX6UL_CLK_PWM1>;
7 clock-names = "ipg", "per";
8 #pwm-cells = <2>;
9 };

imx6ull.dtsi 文件中的 pwm1 节点信息大家不要修改,如果要修改 pwm1 节点内容的话请在 imx6ull-alientek-emmc.dts 文件中修改。在整个 Linux 源码文件中搜索 compatible 属性的这两个 值即可找到 imx6ull 的 pwm 驱动文件,imx6ull 的 PWM 驱动文件为 drivers/pwm/pwm-imx.c, 这里我们就不详细的去分析这个文件了。继续在 imx6ull-alientek-emmc.dts 文件中找到向 pwm1 追加的内容,如下所示:

cpp 复制代码
1 &pwm1 {
2 pinctrl-names = "default";
3 pinctrl-0 = <&pinctrl_pwm1>;
4 status = "okay";
5 };

第 3 行,设置 pwm1 所使用的 IO 为 pinctrl_pwm1,也就是示例代码 59.3.4 所定义的 GPIO1_IO08 这个 IO。

第 4 行,将 status 设置为 okay。 如果背光用的其他 pwm 通道,比如 pwm2,那么就需要仿照示例代码 59.3.6 的内容,向 pwm2 节点追加相应的内容。pwm 和相关的 IO 已经准备好了,但是 Linux 系统怎么知道 PWM1_OUT 就是控制 LCD 背光的呢?因此我们还需要一个节点来将 LCD 背光和 PWM1_OUT 连 接 起 来 。这 个 节 点 就 是 backlight, backlight 节 点 描 述 可 以 参 考 Documentation/devicetree/indings/video/backlight/pwm-backlight.txt 这个文档,此文档详细讲解了 backlight 节点该如何去创建,这里大概总结一下:

①、节点名称要为"backlight"。

②、节点的 compatible 属性值要为"pwm-backlight",因此可以通过在 Linux 内核中搜索 " pwm-backlight "来查找 PWM 背 光 控 制 驱 动 程 序 , 这 个 驱 动 程 序 文 件 为 drivers/video/backlight/pwm_bl.c,感兴趣的可以去看一下这个驱动程序。

③、pwms属性用于描述背光所使用的PWM以及PWM频率,比如本章我们要使用的pwm1, pwm 频率设置为 5KHz(NXP 官方推荐设置)。

④、brightness-levels 属性描述亮度级别,范围为 0~255,0 表示 PWM 占空比为 0%,也就 是亮度最低,255 表示 100%占空比,也就是亮度最高。至于设置几级亮度,大家可以自行填写 此属性。

3:运行测试

3.1:LCD 屏幕基本测试

3.1.1:编译新的设备树

上一小节我们已经配置好了设备树,所以需要输入如下命令重新编译一下设备树:

cpp 复制代码
make dtbs

等待编译生成新的 imx6ull-alientek-emmc.dtb 设备树文件,一会要使用新的设备树启动 Linux 内核。

Linux 内核启动的时候可以选择显示小企鹅 logo,只要这个小企鹅 logo 显示没问题那么我 们的 LCD 驱动基本就工作正常了。这个 logo 显示是要配置的,不过 Linux 内核一般都会默认 开启 logo 显示,但是奔着学习的目的,我们还是来看一下如何使能 Linux logo 显示。打开 Linux 内核图形化配置界面,按下路径找到对应的配置项:

cpp 复制代码
-> Device Drivers
     -> Graphics support 
         -> Bootup logo (LOGO [=y]) 
            -> Standard black and white Linux logo
            -> Standard 16-color Linux logo 
            -> Standard 224-color Linux logo

图 59.4.1.1 中这三个选项分别对应黑白、16 位、24 位色彩格式的 logo,我们把这三个都选 中,都编译进 Linux 内核里面。设置好以后保存退出,重新编译 Linux 内核,编译完成以后使 用新编译出来的 imx6ull-alientek-emmc.dtb 和 zImage 镜像启动系统,如果 LCD 驱动工作正常的 话就会在 LCD 屏幕左上角出现一个彩色的小企鹅 logo,屏幕背景色为黑色,如图 59.4.1.2 所 示:

3.2:设置 LCD 作为终端控制台

我们一直使用SecureCRT作为Linux开发板终端,开发板通过串口和SecureCRT进行通信。 现在我们已经驱动起来 LCD 了,所以可以设置 LCD 作为终端,也就是开发板使用自己的显示 设备作为自己的终端,然后接上键盘就可以直接在开发板上敲命令了,将 LCD 设置为终端控制 台的方法如下:

3.2.1:设置 uboot 中的 bootargs

重启开发板,进入 Linux 命令行,重新设置 bootargs 参数的 console 内容,命令如下所示:

注意红色字体部分设置 console,这里我们设置了两遍 console,第一次设置 console=tty1, 也就是设置 LCD 屏幕为控制台,第二遍又设置 console=ttymxc0,115200,也就是设置串口也作 为控制台。相当于我们打开了两个 console,一个是 LCD,一个是串口,大家重启开发板就会发 现 LCD 和串口都会显示 Linux 启动 log 信息。但是此时我们还不能使用 LCD 作为终端进行交 互,因为我们的设置还未完成。

3.2.2:修改/etc/inittab 文件

打开开发板根文件系统中的/etc/inittab 文件,在里面加入下面这一行:

bash 复制代码
tty1::askfirst:-/bin/sh

添加完成以后的/etc/inittab 文件内容如图 59.4.2.1 所示:

修改完成以后保存/etc/inittab 并退出,然后重启开发板,重启以后开发板 LCD 屏幕最后一 行会显示下面一行语句:

bash 复制代码
Please press Enter to activate this console.

上述提示语句说的是:按下回车键使能当前终端,我们在第五十八章已经将 I.MX6UALPHA 开发板上的 KEY 按键注册为了回车键,因此按下开发板上的 KEY 按键即可使能 LCD 这个终端。

当然了,大家也可以接上一个 USB 键盘,Linux 内核默认已经使能了 USB 键盘驱动 了,因此可以直接使用 USB 键盘。 至此,我们就拥有了两套终端,一个是基于串口的 SecureCRT,一个就是我们开发板的 LCD 屏幕,但是为了方便调试,我们以后还是以 SecureCRT 为主。我们可以通过下面这一行命令向 LCD 屏幕输出"hello linux!"

bash 复制代码
echo hello linux > /dev/tty1

3.3:LCD 背光调节

59.3 小节已经讲过了,背光设备树节点设置了 8 个等级的背光调节,可以设置为 0~7,我 们可以通过设置背光等级来实现 LCD 背光亮度的调节,进入如下目录:

cpp 复制代码
/sys/devices/platform/backlight/backlight/backlight

此目录下的文件如图 59.4.3.1 所示:

图 59.4.3.1 中的 brightness 表示当前亮度等级,max_bgigntness 表示最大亮度等级。当前这 两个文件内容如图 59.4.3.2 所示:

从图 59.4.3.2 可以看出,当前屏幕亮度等级为 6,根据前面的分析可以,这个是 50%亮度。 屏幕最大亮度等级为 7。如果我们要修改屏幕亮度,只需要向 brightness 写入需要设置的屏幕亮 度等级即可。比如设置屏幕亮度等级为 7,那么可以使用如下命令:

bash 复制代码
echo 7 > brightness

输入上述命令以后就会发现屏幕亮度增大了,如果设置 brightness 为 0 的话就会关闭 LCD 背光,屏幕就会熄灭。

3.4:LCD 自动关闭解决方法

默认情况下 10 分钟以后 LCD 就会熄屏,这个并不是代码有问题,而是 Linux 内核设置的, 就和我们用手机或者电脑一样,一段时间不操作的话屏幕就会熄灭,以节省电能。解决这个问 题有多种方法,我们依次来看一下:

3.4.1:按键盘唤醒

最简单的就是按下回车键唤醒屏幕,我们在第 58 章将 I.MX6U-ALPHA 开发板上的 KEY 按键注册为了回车键,因此按下开发板上的 KEY 按键即可唤醒屏幕。如果开发板上没有按键的 话可以外接 USB 键盘,然后按下 USB 键盘上的回车键唤醒屏幕。

3.4.2:关闭 10 分钟熄屏功能

在 Linux 源码中找到 drivers/tty/vt/vt.c 这个文件,在此文件中找到 blankinterval 变量,如下 所示:

cpp 复制代码
179 static int vesa_blank_mode;
180 static int vesa_off_interval;
181 static int blankinterval = 10*60;

blankinterval 变量控制着 LCD 关闭时间,默认是 10*60,也就是 10 分钟。将 blankinterval 的值改为 0 即可关闭 10 分钟熄屏的功能,修改完成以后需要重新编译 Linux 内核,得到新的 zImage,然后用新的 zImage 启动开发板。

3.4.3:编写一个 APP 来关闭熄屏功能

在 ubuntu 中新建一个名为 lcd_always_on.c 的文件,然后在里面输入如下所示内容:

cpp 复制代码
1 #include <fcntl.h>
2 #include <stdio.h>
3 #include <sys/ioctl.h>
4 
5 
6 int main(int argc, char *argv[])
7 {
8 int fd;
9 fd = open("/dev/tty1", O_RDWR);
10 write(fd, "\033[9;0]", 8);
11 close(fd);
12 return 0;
13 }

使用如下命令编译 lcd_always_on.c 这个文件:

bash 复制代码
arm-linux-gnueabihf-gcc lcd_always_on.c -o lcd_always_on

编译生成 lcd_always_on 以后将此可执行文件拷贝到开发板根文件系统的/usr/bin 目录中, 然后给予可执行权限。设置 lcd_always_on 这个软件为开机自启动,打开/etc/init.d/rcS,在此文 件最后面加入如下内容:

bash 复制代码
1 cd /usr/bin
2 ./lcd_always_on
3 cd ..

修改完成以后保存/etc/init.d/rcS 文件,然后重启开发板即可。关于 Linux 下的 LCD 驱动 我们就讲到这里。

相关推荐
IOT.FIVE.NO.14 分钟前
Linux实操笔记2 Ubuntu安装Nginx的不同方法
linux·笔记·ubuntu
geekrabbit39 分钟前
Ubuntu 22.04上安装Python 3.10.x
linux·python·ubuntu
ID_14 HuFei2 小时前
sed编辑器与awk的用法
linux·编辑器
驯龙高手_追风2 小时前
Ubuntu下安装最新版本Apache2文件服务器
linux·服务器·ubuntu
炸裂狸花猫2 小时前
Linux技术04-IPVS
linux·运维·服务器
J老熊3 小时前
Linux下抓包分析Java应用程序HTTP接口调用:基于tcpdump与Wireshark的综合示例
java·linux·运维·web安全·http·面试
HoweWWW3 小时前
k8s中的存储
linux·容器·kubernetes
我叫啥都行4 小时前
计算机基础知识复习9.13
linux·笔记·后端·系统架构
躺下睡觉~4 小时前
Unity-Transform-坐标转换
linux·unity·游戏引擎
库库的里昂4 小时前
【Linux修行路】网络套接字编程——UDP
linux·运维·服务器·网络·udp