屏幕是一个嵌入式设备中相当重要的外设了。在做裸机驱动开发的时候,闹疫情隔离在家,手里没有触摸屏,所以就没看驱动开发。好在内核已经为我们提供了现成的驱动,我们只需要在设备树里定义好LCD的相关信息,就可以点亮屏幕。至于实际底层是驱动的,我们这里就先不搞了,以后如果有机会再回头看看裸机驱动里的LCD篇。有一点要注意的是:这一章我们只考虑LCD的正常点亮,暂时不考虑屏幕的触摸驱动。

FrameBuffer设备

可以先参考一下教程里是如何实现LCD的裸机驱动的:

  1. 初始化I.MX的eLCDif控制器,重点是LCD的信息
  2. 初始化LCD像素时钟
  3. 设置RGBLCD显存
  4. 直接通过操作显存来实现在LCD上显示字符、图片等信息

我们在Linux中最终也是通过RGBLCD的显存来实现LCD的显示操作都,但是我们在一开始写Linux驱动就讲过,内存都是要申请才能使用的,并且由于MMU的介入我们并不能直接对内存进行操作。所以就有了Framebuffer这个概念。在Linux中,“Framebuffer”或者“fb”都指的是framebuffer设备(与其说是设备,更不如说是一种机制),Linux内核将所有和显示有关的软件和硬件集成到一起,虚拟出来了一个fb设备,在LCD能够正常驱动以后就会有一个/dev/fbn(n从0开始的任意数)设备文件,这个fb有些时候还被称作“帧缓存”。

Linux驱动VFD屏幕 linux 显示屏驱动_设备树

 用户应用程序直接访问这个设备就可以实现LCD的操作了。具体的使用方法我们这里暂时也用不到,也不再展开讲了,如果以后有兴趣的可以百度一下framebuffer编程,大把的资源可以参考。

framebuffer驱动流程

framebuffer在Linux内核中时通过结构特fb_info来进行描述的(include/linux/fb.h)

1 struct fb_info {
 2     atomic_t count;
 3     int node;
 4     int flags;
 5     struct mutex lock;        /* Lock for open/release/ioctl funcs */
 6     struct mutex mm_lock;        /* Lock for fb_mmap and smem_* fields */
 7     struct fb_var_screeninfo var;    /* Current var */
 8     struct fb_fix_screeninfo fix;    /* Current fix */
 9     struct fb_monspecs monspecs;    /* Current Monitor specs */
10     struct work_struct queue;    /* Framebuffer event queue */
11     struct fb_pixmap pixmap;    /* Image hardware mapper */
12     struct fb_pixmap sprite;    /* Cursor hardware mapper */
13     struct fb_cmap cmap;        /* Current cmap */
14     struct list_head modelist;      /* mode list */
15     struct fb_videomode *mode;    /* current mode */
16 
17 #ifdef CONFIG_FB_BACKLIGHT
18     /* assigned backlight device */
19     /* set before framebuffer registration, 
20        remove after unregister */
21     struct backlight_device *bl_dev;
22 
23     /* Backlight level curve */
24     struct mutex bl_curve_mutex;    
25     u8 bl_curve[FB_BACKLIGHT_LEVELS];
26 #endif
27 #ifdef CONFIG_FB_DEFERRED_IO
28     struct delayed_work deferred_work;
29     struct fb_deferred_io *fbdefio;
30 #endif
31 
32     struct fb_ops *fbops;
33     struct device *device;        /* This is the parent */
34     struct device *dev;        /* This is this fb device */
35     int class_flag;                    /* private sysfs flags */
36 #ifdef CONFIG_FB_TILEBLITTING
37     struct fb_tile_ops *tileops;    /* Tile Blitting */
38 #endif
39     char __iomem *screen_base;    /* Virtual address */
40     unsigned long screen_size;    /* Amount of ioremapped VRAM or 0 */ 
41     void *pseudo_palette;        /* Fake palette of 16 colors */ 
42 #define FBINFO_STATE_RUNNING    0
43 #define FBINFO_STATE_SUSPENDED    1
44     u32 state;            /* Hardware state i.e suspend */
45     void *fbcon_par;                /* fbcon use-only private area */
46     /* From here on everything is device dependent */
47     void *par;
48     /* we need the PCI or similar aperture base/size not
49        smem_start/size as smem_start may just be an object
50        allocated inside the aperture so may not actually overlap */
51     struct apertures_struct {
52         unsigned int count;
53         struct aperture {
54             resource_size_t base;
55             resource_size_t size;
56         } ranges[0];
57     } *apertures;
58 
59     bool skip_vt_switch; /* no VT switch on suspend/resume required */
60 };

归根到底LCD的驱动就是对这个fb_info里面的成员进行初始化,然后通过register_framebuffer函数进行注册就行了。恩智浦还对I.MX系列的控制器还定义了一个专用的结构体(drivers/video/fbdev/mxsfb.c)

struct mxsfb_info {
    struct fb_info *fb_info;
    struct platform_device *pdev;
    struct clk *clk_pix;
    struct clk *clk_axi;
    struct clk *clk_disp_axi;
    bool clk_pix_enabled;
    bool clk_axi_enabled;
    bool clk_disp_axi_enabled;
    void __iomem *base;    /* registers */
    u32 sync;        /* record display timing polarities */
    unsigned allocated_size;
    int enabled;
    unsigned ld_intf_width;
    unsigned dotclk_delay;
    const struct mxsfb_devdata *devdata;
    struct regulator *reg_lcd;
    bool wait4vsync;
    struct completion vsync_complete;
    struct completion flip_complete;
    int cur_blank;
    int restore_blank;
    char disp_dev[32];
    struct mxc_dispdrv_handle *dispdrv;
    int id;
    struct fb_var_screeninfo var;
};

这个mxsfb_info里除了封装了一个fb_info结构体,还有一些常用的LCD相关属性。如果我们想要分析恩智浦是怎么写的这个LCD屏幕的驱动,就可以分析这个mxsfb.c文件。下面粗略的捋一下流程:

mxsfb.c实质上也是一个platform框架下的驱动,所以也就可以倒着看下(为了能看到行号,这里用截图,不再单独粘贴代码)

最后是注册模块

Linux驱动VFD屏幕 linux 显示屏驱动_Linux驱动VFD屏幕_02

所以我们要关注的就是mxsfb_driver结构体

Linux驱动VFD屏幕 linux 显示屏驱动_设备树_03

这个是不是很眼熟, 就是用来做platform驱动设备直接匹配到结构体。看看那个mxsfb_dt_ids

Linux驱动VFD屏幕 linux 显示屏驱动_设备树_04

 

Linux驱动VFD屏幕 linux 显示屏驱动_设备树_05

在无设备树的情况下要将LCD设备命名为mxsfb,但是我们是使用设备树的,就要将匹配的名称定义成上面那个列表里的fsl,imx23(8)-lcdif。 

在设备和驱动匹配程勇以后要执行probe对应的函数,这个函数比较大,就一步步大概的功能说一下吧

1.gpio相关初始化

Linux驱动VFD屏幕 linux 显示屏驱动_寄存器_06

这里第1388行的gpio设置是lcd的电源设置(供电输出),我们使用的LCD电源是直接拉在板子电源上的,这行实际没有实际意义。

  2.资源获取

Linux驱动VFD屏幕 linux 显示屏驱动_初始化_07

获取到资源是IORSOURCE_MEM类型,也就是寄存器类型的数据,我们可以搜索一下设备树信息,可以在imx6ull.dtsi里找到和驱动name匹配的参数

Linux驱动VFD屏幕 linux 显示屏驱动_寄存器_08

也就是收上面资源获取到的就是0x021c8000。在开发手册里能找到这个地址对应的寄存器

Linux驱动VFD屏幕 linux 显示屏驱动_Linux驱动VFD屏幕_09

 这个地址就是eLCDIF通用寄存器的首地址。然后通过devm_kzalloc申请了一段内存给参数host,内存大小跟mxsfb_info一致,host就是一个前面说的mxsfb_info对象。

Linux驱动VFD屏幕 linux 显示屏驱动_初始化_10

接下来通过framebuffer_alloc申请了一个fb_info,然后把这个fb_info指向了mxsfb_info(1413和1414行)

Linux驱动VFD屏幕 linux 显示屏驱动_Linux驱动VFD屏幕_11

下面是申请中断

Linux驱动VFD屏幕 linux 显示屏驱动_寄存器_12

 中断处理函数是mxsfb_irq_handler,在中断处理函数中主要是一对寄存器的处理,这里不再细说

从1425行开始就是进行内存映射了,res是获取到的LCDIF寄存器的首地址,host->base就是LCDIF寄存器组首地址的映射地址。后面一对代码就是获取时钟什么的映射地址

Linux驱动VFD屏幕 linux 显示屏驱动_初始化_13

 到1462行,就开始对fb_info进行初始化了。

Linux驱动VFD屏幕 linux 显示屏驱动_寄存器_14

 到1473行,调用了mxsfb_init_fb_info函数对host,也就是mxsfb_inof进行了初始化

Linux驱动VFD屏幕 linux 显示屏驱动_初始化_15

到1494行,对初始化后的fb_info对象进行注册

 

Linux驱动VFD屏幕 linux 显示屏驱动_设备树_16

这样就完成了驱动的加载。有些函数可以再展开看下,比如哪个mxsfb_init_fbinfo ,里面就有根据分辨率计算各种数据,然后根据计算结果通过mxsfb_map_videomem去申请显存(放给成员变量screen_base)。还有通过of函数从设备树文件里获取到一系列信息。

在mxsfb_init_fbinfo里还定义了一个mxsfb_ops,相当于我们前面写驱动时候那个文件操作集合。

Linux驱动VFD屏幕 linux 显示屏驱动_初始化_17

这些函数NXP也已经为我们写好了。这里也不分析。

总之,mxsfb_probe的主要作用就是用来初始化fb_info然后向内核注册,还有就是初始化LCDIF控制器。

点亮屏幕

上面说过了,LCD的驱动也属于platform架构驱动,并且NXP已经为我们完成了驱动的编写,我们要做的就是在设备树里完善我们显示屏的相关信息就可以了。在上面我们截取了imx6ull.dtsi里的lcdif节点的信息,可以里面内容比较少,哪个imx6ull.dtsi是基于imx6ull架构的通用设备信息,我们可以在板级的设备树里找一下有没有相关的节点

&lcdif {
    pinctrl-names = "default";
    pinctrl-0 = <&pinctrl_lcdif_dat
             &pinctrl_lcdif_ctrl
             &pinctrl_lcdif_reset>;
    display = <&display0>;
    status = "okay";

    display0: display {
        bits-per-pixel = <16>;
        bus-width = <24>;

        display-timings {
            native-mode = <&timing0>;
            timing0: timing0 {
            clock-frequency = <9200000>;
            hactive = <480>;
            vactive = <272>;
            hfront-porch = <8>;
            hback-porch = <4>;
            hsync-len = <41>;
            vback-porch = <2>;
            vfront-porch = <4>;
            vsync-len = <10>;

            hsync-active = <0>;
            vsync-active = <0>;
            de-active = <1>;
            pixelclk-active = <0>;
            };
        };
    };

};

注意一下他这个节点信息,是一个480X272分辨率的屏幕,并且和函数xsfb_init_fbinfo_dt要通过of函数获取的东西一样,我们要修改的内容就是这个设备树里的信息

 

Linux驱动VFD屏幕 linux 显示屏驱动_初始化_18

 

 上面的表格就是我们使用的7寸的LCD对应的时钟参数,可以参考Documentation/devicetree/bindings/fb/mxsfb.txt里的参考文档

* Freescale MXS LCD Interface (LCDIF)

Required properties:
- compatible: Should be "fsl,<chip>-lcdif".  Supported chips include
  imx23 and imx28.
- reg: Address and length of the register set for lcdif
- interrupts: Should contain lcdif interrupts
- display : phandle to display node (see below for details)

* display node

Required properties:
- bits-per-pixel : <16> for RGB565, <32> for RGB888/666.
- bus-width : number of data lines.  Could be <8>, <16>, <18> or <24>.

Required sub-node:
- display-timings : Refer to binding doc display-timing.txt for details.

Examples:

lcdif@80030000 {
    compatible = "fsl,imx28-lcdif";
    reg = <0x80030000 2000>;
    interrupts = <38 86>;

    display: display {
        bits-per-pixel = <32>;
        bus-width = <24>;

        display-timings {
            native-mode = <&timing0>;
            timing0: timing0 {
                clock-frequency = <33500000>;
                hactive = <800>;
                vactive = <480>;
                hfront-porch = <164>;
                hback-porch = <89>;
                hsync-len = <10>;
                vback-porch = <23>;
                vfront-porch = <10>;
                vsync-len = <10>;
                hsync-active = <0>;
                vsync-active = <0>;
                de-active = <1>;
                pixelclk-active = <0>;
            };
        };
    };
};

我们的屏幕使用的像素为RGB888(RGB各占8位,还有一个Alpha透明度也占8位)所以bits-per-pixel就是32,bus-width跟我们硬件电路有关系,可以看下图

Linux驱动VFD屏幕 linux 显示屏驱动_Linux驱动VFD屏幕_19

 

 LCD_DATA,也就是数据线一共是24根,所以带宽就是24。后面display-timings里的属性就按LCD供货商给我们提供的参数就行了。

display1: display {
    bits-per-pixel = <32>;
    bus-width = <24>;

    display-timings {
        native-mode = <&timing0>;
        timing0: timing0 {
        clock-frequency = <51200000>;
        hactive = <1024>;
        vactive = <600>;
        hfront-porch = <160>;
        hback-porch = <140>;
        hsync-len = <20>;
        vback-porch = <20>;
        vfront-porch = <12>;
        vsync-len = <3>;

        hsync-active = <0>;
        vsync-active = <0>;
        de-active = <1>;
        pixelclk-active = <0>;
        };
    };
};

时间的说明可以按照文档里给出的提示文档

Required sub-node:
- display-timings : Refer to binding doc display-timing.txt for details.

可以查到Documentation/devicetree/bindings/video/display-timing.txt里面对这个有描述

1 timing subnode
 2 --------------
 3 
 4 required properties:
 5  - hactive, vactive: display resolution
 6  - hfront-porch, hback-porch, hsync-len: horizontal display timing parameters
 7    in pixels
 8    vfront-porch, vback-porch, vsync-len: vertical display timing parameters in
 9    lines
10  - clock-frequency: display clock in Hz
11 
12 optional properties:
13  - hsync-active: hsync pulse is active low/high/ignored
14  - vsync-active: vsync pulse is active low/high/ignored
15  - de-active: data-enable pulse is active low/high/ignored
16  - pixelclk-active: with
17             - active high = drive pixel data on rising edge/
18                     sample data on falling edge
19             - active low  = drive pixel data on falling edge/
20                     sample data on rising edge
21             - ignored     = ignored
22  - interlaced (bool): boolean to enable interlaced mode
23  - doublescan (bool): boolean to enable doublescan mode
24  - doubleclk (bool): boolean to enable doubleclock mode
25 
26 All the optional properties that are not bool follow the following logic:
27     <1>: high active
28     <0>: low active
29     omitted: not used on hardware

文档里说了,属性为1的时候是高电平有效,0的时候为低电平有效。这个就是最后4行同步信号的属性,这个同步信号可以从屏幕的时序图里找到。如果使用哪家的屏幕一定要来一份详细的资料。

GPIO修改

从设备树里可以看到Pinctrl的配置

pinctrl-0 = <&pinctrl_lcdif_dat
            &pinctrl_lcdif_ctrl
            &pinctrl_lcdif_reset>;

 主要分了数据组、控制组、复位组。可以看下对应的参数设置

pinctrl_lcdif_dat: lcdifdatgrp {
            fsl,pins = <
                MX6UL_PAD_LCD_DATA00__LCDIF_DATA00  0x79
                MX6UL_PAD_LCD_DATA01__LCDIF_DATA01  0x79
                MX6UL_PAD_LCD_DATA02__LCDIF_DATA02  0x79
                MX6UL_PAD_LCD_DATA03__LCDIF_DATA03  0x79
                MX6UL_PAD_LCD_DATA04__LCDIF_DATA04  0x79
                MX6UL_PAD_LCD_DATA05__LCDIF_DATA05  0x79
                MX6UL_PAD_LCD_DATA06__LCDIF_DATA06  0x79
                MX6UL_PAD_LCD_DATA07__LCDIF_DATA07  0x79
                MX6UL_PAD_LCD_DATA08__LCDIF_DATA08  0x79
                MX6UL_PAD_LCD_DATA09__LCDIF_DATA09  0x79
                MX6UL_PAD_LCD_DATA10__LCDIF_DATA10  0x79
                MX6UL_PAD_LCD_DATA11__LCDIF_DATA11  0x79
                MX6UL_PAD_LCD_DATA12__LCDIF_DATA12  0x79
                MX6UL_PAD_LCD_DATA13__LCDIF_DATA13  0x79
                MX6UL_PAD_LCD_DATA14__LCDIF_DATA14  0x79
                MX6UL_PAD_LCD_DATA15__LCDIF_DATA15  0x79
                MX6UL_PAD_LCD_DATA16__LCDIF_DATA16  0x79
                MX6UL_PAD_LCD_DATA17__LCDIF_DATA17  0x79
                MX6UL_PAD_LCD_DATA18__LCDIF_DATA18  0x79
                MX6UL_PAD_LCD_DATA19__LCDIF_DATA19  0x79
                MX6UL_PAD_LCD_DATA20__LCDIF_DATA20  0x79
                MX6UL_PAD_LCD_DATA21__LCDIF_DATA21  0x79
                MX6UL_PAD_LCD_DATA22__LCDIF_DATA22  0x79
                MX6UL_PAD_LCD_DATA23__LCDIF_DATA23  0x79
            >;
        };

pinctrl_lcdif_ctrl: lcdifctrlgrp {
    fsl,pins = <
        MX6UL_PAD_LCD_CLK__LCDIF_CLK        0x79
        MX6UL_PAD_LCD_ENABLE__LCDIF_ENABLE  0x79
        MX6UL_PAD_LCD_HSYNC__LCDIF_HSYNC    0x79
        MX6UL_PAD_LCD_VSYNC__LCDIF_VSYNC    0x79
    >;
};

正点原子这个阿尔法开发板的电路设计会导致BUG,需要将GPIO的电气属性修改一下,将原来的0x79修改成0x49,主要就是修改了对应GPIO输出引脚的输出能力(原来79的时候输出能力强,有可能会影响到网络,这里就把GPIO的驱动能力降下来)。reset我们没有使用,是直接跟电源走的,一上电屏幕点亮,掉电关闭。不用reset功能。

显示测试

现在已经完成了platform设备的定义,把设备树make一下启动系统,下面我们需要用一个能显示出来的东西测试一下驱动。最直接的方法就是显示出来Linux的小企鹅logo。这个logo需要在编译内核的时候配置出来

Linux驱动VFD屏幕 linux 显示屏驱动_初始化_20

 按照上面的路径进去以后会看到有三个选项,我们都选中

Linux驱动VFD屏幕 linux 显示屏驱动_寄存器_21

保存退出。启动开发板,如果屏幕连接正常,就可以在进入内核后显示一个小企鹅的logo在左上角

Linux驱动VFD屏幕 linux 显示屏驱动_初始化_22

拍照的时候把自己映进去了,打个码。这样就说明屏幕驱动没问题。要注意这个LOGO不是开机时候显示的NXP那个LOGO,NXP是通过Uboot里的屏幕显示出来的,即便我们不做那个设备树都有可能显示出来。

此外我们还可以在屏幕上通过命令直接打印一个信息出来

/ # echo hello world>/dev/tty1

 tty1就是LCD上的终端。执行完上面的指令就会在屏幕上打印出hello world。

LCD终端

在完成了LCD驱动以后,可以让LCD作为终端来工作,

修改uboot

首先是修改uboot里的环境变量

=> setenv bootargs 'console=tty1 console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.0.100:/home/qi/IMX6UL/rootfs ip=192.168.0.200:192.168.0.100:192.168.0.1:255.255.255.0::eth0:off'
=> saveenv

开机以后进入Uboot,用上面的命令修改bootargs。后面的根文件系统路径以及IP根据实际情况修改。主要就是添加红色加粗的部分。开机以后就可以看到LCD上打印出来跟串口一样的信息。但是这个时候还没法使用,我们还要修改一个文件

运行级别修改

我们要修改/etc/inittab这个文件

Linux驱动VFD屏幕 linux 显示屏驱动_Linux驱动VFD屏幕_23

添加第4行那条命令(注意tty1后面是两个冒号!)重启开发板以后就会在各个终端(LCD或串口输出)显示一句话:

Linux驱动VFD屏幕 linux 显示屏驱动_寄存器_24

就是上面图中最后一行提示,按下回车键就可以使用终端。我们可以在开发板上插个键盘,就可以直接当个简单的电脑使用了。

背光调节

我们的LCD是有背光的,背光亮度在设备树中给了8个级别可以调节(如果我没记错是基于PWM来实现的)。背光是一个单独的设备呈现在内核中,也是一个platform框架下的驱动设备,可以看一下设备树中定义的节点信息

backlight {
    compatible = "pwm-backlight";
    pwms = <&pwm1 0 5000000>;
    brightness-levels = <0 4 8 16 32 64 128 255>;
    default-brightness-level = <6>;
    status = "okay";
};

也可以看一下设备文件

Linux驱动VFD屏幕 linux 显示屏驱动_寄存器_25

 

 

展开以后找到这个文件

Linux驱动VFD屏幕 linux 显示屏驱动_Linux驱动VFD屏幕_26

 

 

从设备树信息里可以看到,背光的默认值是6,可以向这个文件里写入0-7就会改变背光的亮度。

/sys/devices/platform/backlight/backlight/backlight # echo 7 > brightness

如果写入值为0,屏幕亮度最低,基本上看不到内容,写入7就是最亮了。

屏幕休眠

屏幕在没有操作的时候大概10分钟就熄屏了,我们在讲input子系统的时候把那个按键定义成了回车键,在熄屏以后可以点击按键重新点亮屏幕,如果不想要熄屏可以用下面两种方法搞定!

修改内核

如果我们不想要屏幕熄灭可以修改内核文件(drivers/tty/vt/vt.c)

Linux驱动VFD屏幕 linux 显示屏驱动_寄存器_27

那个blankinterval就是熄屏的时间,10*60就是10分钟,把它设置为0就关闭了熄屏功能

脚本

如果不想重新编译内核,可以直接写个脚本控制

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

用arm-linux-gnueabihf-gcc(一定要用交叉编译器编译啊!)编译完生成的执行文件放在rootfs下,在熄屏状态下直接运行一下看看屏有没有恢复点亮,如果没问题可以在/etc/init.d/rcS文件最后追加这个文件的运行指令

Linux驱动VFD屏幕 linux 显示屏驱动_初始化_28

 

保存以后重启开发板,屏就不会熄灭了。