屏幕是一个嵌入式设备中相当重要的外设了。在做裸机驱动开发的时候,闹疫情隔离在家,手里没有触摸屏,所以就没看驱动开发。好在内核已经为我们提供了现成的驱动,我们只需要在设备树里定义好LCD的相关信息,就可以点亮屏幕。至于实际底层是驱动的,我们这里就先不搞了,以后如果有机会再回头看看裸机驱动里的LCD篇。有一点要注意的是:这一章我们只考虑LCD的正常点亮,暂时不考虑屏幕的触摸驱动。
FrameBuffer设备
可以先参考一下教程里是如何实现LCD的裸机驱动的:
我们在Linux中最终也是通过RGBLCD的显存来实现LCD的显示操作都,但是我们在一开始写Linux驱动就讲过,内存都是要申请才能使用的,并且由于MMU的介入我们并不能直接对内存进行操作。所以就有了Framebuffer这个概念。在Linux中,“Framebuffer”或者“fb”都指的是framebuffer设备(与其说是设备,更不如说是一种机制),Linux内核将所有和显示有关的软件和硬件集成到一起,虚拟出来了一个fb设备,在LCD能够正常驱动以后就会有一个/dev/fbn(n从0开始的任意数)设备文件,这个fb有些时候还被称作“帧缓存”。
用户应用程序直接访问这个设备就可以实现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框架下的驱动,所以也就可以倒着看下(为了能看到行号,这里用截图,不再单独粘贴代码)
最后是注册模块
所以我们要关注的就是mxsfb_driver结构体
这个是不是很眼熟, 就是用来做platform驱动设备直接匹配到结构体。看看那个mxsfb_dt_ids
在无设备树的情况下要将LCD设备命名为mxsfb,但是我们是使用设备树的,就要将匹配的名称定义成上面那个列表里的fsl,imx23(8)-lcdif。
在设备和驱动匹配程勇以后要执行probe对应的函数,这个函数比较大,就一步步大概的功能说一下吧
1.gpio相关初始化
这里第1388行的gpio设置是lcd的电源设置(供电输出),我们使用的LCD电源是直接拉在板子电源上的,这行实际没有实际意义。
2.资源获取
获取到资源是IORSOURCE_MEM类型,也就是寄存器类型的数据,我们可以搜索一下设备树信息,可以在imx6ull.dtsi里找到和驱动name匹配的参数
也就是收上面资源获取到的就是0x021c8000。在开发手册里能找到这个地址对应的寄存器
这个地址就是eLCDIF通用寄存器的首地址。然后通过devm_kzalloc申请了一段内存给参数host,内存大小跟mxsfb_info一致,host就是一个前面说的mxsfb_info对象。
接下来通过framebuffer_alloc申请了一个fb_info,然后把这个fb_info指向了mxsfb_info(1413和1414行)
下面是申请中断
中断处理函数是mxsfb_irq_handler,在中断处理函数中主要是一对寄存器的处理,这里不再细说
从1425行开始就是进行内存映射了,res是获取到的LCDIF寄存器的首地址,host->base就是LCDIF寄存器组首地址的映射地址。后面一对代码就是获取时钟什么的映射地址
到1462行,就开始对fb_info进行初始化了。
到1473行,调用了mxsfb_init_fb_info函数对host,也就是mxsfb_inof进行了初始化
到1494行,对初始化后的fb_info对象进行注册
这样就完成了驱动的加载。有些函数可以再展开看下,比如哪个mxsfb_init_fbinfo ,里面就有根据分辨率计算各种数据,然后根据计算结果通过mxsfb_map_videomem去申请显存(放给成员变量screen_base)。还有通过of函数从设备树文件里获取到一系列信息。
在mxsfb_init_fbinfo里还定义了一个mxsfb_ops,相当于我们前面写驱动时候那个文件操作集合。
这些函数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函数获取的东西一样,我们要修改的内容就是这个设备树里的信息
上面的表格就是我们使用的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跟我们硬件电路有关系,可以看下图
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需要在编译内核的时候配置出来
按照上面的路径进去以后会看到有三个选项,我们都选中
保存退出。启动开发板,如果屏幕连接正常,就可以在进入内核后显示一个小企鹅的logo在左上角
拍照的时候把自己映进去了,打个码。这样就说明屏幕驱动没问题。要注意这个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这个文件
添加第4行那条命令(注意tty1后面是两个冒号!)重启开发板以后就会在各个终端(LCD或串口输出)显示一句话:
就是上面图中最后一行提示,按下回车键就可以使用终端。我们可以在开发板上插个键盘,就可以直接当个简单的电脑使用了。
背光调节
我们的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"; };
也可以看一下设备文件
展开以后找到这个文件
从设备树信息里可以看到,背光的默认值是6,可以向这个文件里写入0-7就会改变背光的亮度。
/sys/devices/platform/backlight/backlight/backlight # echo 7 > brightness
如果写入值为0,屏幕亮度最低,基本上看不到内容,写入7就是最亮了。
屏幕休眠
屏幕在没有操作的时候大概10分钟就熄屏了,我们在讲input子系统的时候把那个按键定义成了回车键,在熄屏以后可以点击按键重新点亮屏幕,如果不想要熄屏可以用下面两种方法搞定!
修改内核
如果我们不想要屏幕熄灭可以修改内核文件(drivers/tty/vt/vt.c)
那个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文件最后追加这个文件的运行指令
保存以后重启开发板,屏就不会熄灭了。