Framebuffer

在 Linux 中应用程序最终也是通过操作 RGB LCD 的显存来实现在 LCD 上显示字符、图片等信息。 因为虚拟内存的存在,驱动程序设置的显存和应用程序访问的显存要是同一片物理内存。

Framebuffer 诞生了, Framebuffer 翻译过来就是帧缓冲,简称 fb 。fb 是一种机制,将系统中所有跟显示有关的硬件以及软件集合起来,虚拟出一个 fb 设备。帧缓冲是linux系统中的一种显示驱动接口,它将显示设备(比如LCD)进行抽象、屏蔽了不同显示设备硬件的实现,对应用层抽象为一块显示内存(显存),它允许上层应用程序直接对显示缓冲区进行读写操作,而用户不关心物理显存的位置等具体细节,这些都由Framebuffer设备驱动来完成。

显示设备被称为 FrameBuffer 设备(帧缓冲设备),所以 LCD 显示屏自然而言就是 FrameBuffer 设备。 FrameBuffer 设备对应的设备文件为/dev/fbX(X 为数字, 0、 1、 2、 3 等) , Linux下可支持多个 FrameBuffer 设备,最多可达 32 个,分别为/dev/fb0 到/dev/fb31, 开发板出厂系统中, /dev/fb0设备节点便是 LCD 屏 。

fb 的 file_operations 操作集定义在drivers/video/fbdev/core/fbmem.c 文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 };

LCD设备节点

1
2
3
4
5
6
7
8
9
10
11
						/*imx6ull.dtsi 文件中 lcdif 节点内容*/
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 };

之后还要向 imx6ullalientek-emmc.dts 中的 lcdif 节点添加其他的属性信息。

属性值为“fsl,imx6ul-lcdif”和“fsl,imx28-lcdif”,因此在 Linux 源码中搜索这两个字符串即可找到 I.MX6ULL 的 LCD 驱动文件,这个文件为 drivers/video/fbdev/mxsfb.c,mxsfb.c就是 I.MX6ULL 的 LCD 驱动文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);

这是一个标准的 platform 驱动,当驱动和设备匹配以后mxsfb_probe 函数就会执行。 LCD 的驱动就是构建 fb_info,并且向系统注册 fb_info的过程。

fb_info 结构体定义在 include/linux/fb.h 文件里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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];原子哥在线教学:www.yuanzige.com 论坛:www.openedv.com
1416
I.MX6U 嵌入式 Linux 驱动开发指南
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 };

mxsfb_probe 函数的主要工作内容为 :

①、申请 fb_info。
②、初始化 fb_info 结构体中的各个成员变量。
③、初始化 eLCDIF 控制器。
④、使用 register_framebuffer 函数向 Linux 内核注册初始化好的 fb_info。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
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; //IMX6uLL主控接口,mxsfb_info为NXP定义的fb设备结构体
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); //获取eLCDIF接口控制器的寄存器首地址
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); //给host申请内存
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); //给fb申请内存
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; //将申请的host和fb_info联系到一起
1415
1416 ret = devm_request_irq(&pdev->dev, irq, mxsfb_irq_handler, 0,dev_name(&pdev->dev), host);
1417 //申请中断
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); //对从设备树中获取寄存器首地址进行内存映射
// 得到虚拟地址保存到host变量中
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) * 16, GFP_KERNEL);//申请内存
1463
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); //初始化fb_info
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); //设置eLCDIF控制器相应的寄存器
1490 mxsfb_enable_controller(fb_info);
1491 pm_runtime_get_sync(&host->pdev->dev);
1492 }
1493
1494 ret = register_framebuffer(fb_info); //内核注册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 }

NXP 提供的 fbops 为mxsfb_ops

1
2
3
4
5
6
7
8
9
10
11
12
13
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_init_fbinfo 函数通过调用 mxsfb_init_fbinfo_dt 函数从设备树中获取到 LCD 的各个参数信息。最后, mxsfb_init_fbinfo
函数会调用 mxsfb_map_videomem 函数申请 LCD 的帧缓冲内存(也就是显存)。

驱动编写

重点要注意三个地方:
①、 LCD 所使用的 IO 配置。
②、 LCD 屏幕节点修改,修改相应的属性值,所使用的 LCD 屏幕参数。
③、 LCD 背光节点信息修改,要根据实际所使用的背光 IO 来修改相应的设备节点信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
				//设备树 LCD IO 配置
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 >; //为RGBLCD的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 >; //RGB LCD 的 4 根控制线配置项,包括 CLK、ENABLE、 VSYNC(场同步信号)和 HSYNC(行同步信号)。
37 pinctrl_pwm1: pwm1grp {
38 fsl,pins = <
39 MX6UL_PAD_GPIO1_IO08__PWM1_OUT 0x110b0
40 >; //LCD 背光 PWM 引脚配置项。这个引脚要根据实际情况设置,
41 };

之后修改lcdif节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
&lcdif{
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_lcdif_dat
&pinctrl_lcdif_ctrl
/*&pinctrl_lcdif_reset*/>;//本显示lcd没有复位引脚所以删掉
display = <&display0>;
status = "okay";
display0: display { /* LCD 属性信息 */
bits-per-pixel = <24>; /* 一个像素占用几个 bit ,RGB888 8+8+8 =24bit*/
bus-width = <24>; /* 总线宽度 */

display-timings {
native-mode = <&timing0>; /* 时序信息 */
timing0: timing0 {
clock-frequency = <33300000>; /* LCD 像素时钟,单位 Hz */
hactive = <800>; /* LCD X 轴像素个数 */
vactive = <480>; /* LCD Y 轴像素个数 */
hfront-porch = <210>; /* LCD hfp 参数 */
hback-porch = <46>; /* LCD hbp 参数 */
hsync-len = <1>; /* LCD hspw 参数 */
vback-porch = <23>; /* LCD vbp 参数 */
vfront-porch = <22>; /* LCD vfp 参数 */
vsync-len = <1>; /* LCD vspw 参数 */

hsync-active = <0>; /* hsync 数据线极性 */
vsync-active = <0>; /* vsync 数据线极性 */
de-active = <1>; /* de 数据线极性 */
pixelclk-active = <0>; /* clk 数据线先极性 */
};
};
};
};

背光节点信息

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

LCD 背光要用到 PWM1,因此也要设置 PWM1 节点,在 imx6ull.dtsi 文件中找到如下内容:

1
2
3
4
5
6
7
8
9
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 };

在整个 Linux 源码文件中搜索 compatible 属性的这两个值即可找到 imx6ull 的 pwm 驱动文件。

继续在 imx6ull-alientek-emmc.dts 文件中找到向 pwm1追加的内容,如下所示:

1
2
3
4
5
1 &pwm1 {
2 pinctrl-names = "default";
3 pinctrl-0 = <&pinctrl_pwm1>;//之前定义的GPIO1_08
4 status = "okay"; //设置okay
5 };

要让Linux知道PWM1_OUT是控制背光的,需要一个节点来将 LCD 背光和 PWM1_OUT连 接 起 来 。 这 个 节 点 就 是 backlight , backlight 节 点 描 述 可 以 参 考Documentation/devicetree/indings/video/backlight/pwm-backlight.txt 这个文档,此文档详细讲解了
backlight 节点该如何去创建,这里大概总结一下:

  1. 节点名称要为“backlight”。
  2. 节点的 compatible 属性值要为“pwm-backlight”,因此可以通过在 Linux 内核中搜索“ pwm-backlight ” 来 查 找 PWM 背 光 控 制 驱 动 程 序 , 这 个 驱 动 程 序 文 件 为drivers/video/backlight/pwm_bl.c,感兴趣的可以去看一下这个驱动程序。
  3. pwms属性用于描述背光所使用的PWM以及PWM频率,比如本章我们要使用的pwm1,pwm 频率设置为 200Hz(NXP 官方推荐设置)。
  4. brightness-levels 属性描述亮度级别,范围为 0~255, 0 表示 PWM 占空比为 0%,也就是亮度最低, 255 表示 100%占空比,也就是亮度最高
  5. default-brightness-level 属性为默认亮度级别。
1
2
3
4
5
6
7
backlight {
compatible = "pwm-backlight";
pwms = <&pwm1 0 5000000>; //设置背光使用 pwm1, PWM 频率为 200Hz。
brightness-levels = <0 4 8 16 32 64 128 255>; /*设置背 8 级背光(0~7),分别为 0、 4、 8、 16、 32、 64、 128、 255,对应占空比为0%、 1.57%、 3.13%、 6.27%、 12.55%、 25.1%、 50.19%、 100%*/
default-brightness-level = <6>; //设置默认背光等级为 6,也就是 50.19%的亮度
status = "okay";
};

运行测试

设置

1
make dtbs	//编译设备树

使能 Linux logo 显示 ,make menuconfig打开 Linux内核图形化配置界面,按下路径找到对应的配置项:

重新编译 Linux 内核,编译完成以后使用新编译出来的 imx6ull-alientek-emmc.dtb 和 zImage 镜像启动系统 ,如果显示正常的话,LCD左上角会出现一个彩色的小企鹅logo

设置 LCD 作为终端控制台

  1. 设置uboot中的bootargs

    1
    2
    setenv bootargs 'console=tty1 console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.1.250:
    /home/moss/linux/nfs/rootfs ip=192.168.0.128:192.168.1.250:192.168.0.1:255.255.255.0::eth0:off'

    设置了两遍 console,第一次设置 console=tty1,也就是设置 LCD 屏幕为控制台,第二遍又设置 console=ttymxc0,115200,也就是设置串口也作为控制台。相当于我们打开了两个 console,一个是 LCD,一个是串口,大家重启开发板就会发现 LCD 和串口都会显示 Linux 启动 log 信息。

  2. 修改/etc/inittab 文件

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

1
tty1::askfirst:-/bin/sh

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

·····

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

1
Please press Enter to activate this console.

至此,我们就拥有了两套终端,一个是基于串口的 SecureCRT,一个就是我们开发板的 LCD屏幕,但是为了方便调试,我们以后还是以 SecureCRT 为主。我们可以通过下面这一行命令向LCD 屏幕输出“hello linux!”

1
echo hello linux > /dev/tty1 

通过设置背光等级来实现 LCD 背光亮度的调节,进入如下目录:/sys/devices/platform/backlight/backlight/backlight

1
echo 7 > brightness	//设置背光等级为7