屏幕是一个嵌入式设备中相当重要的外设了。在做裸机驱动开发的时候,闹疫情隔离在家,手里没有触摸屏,所以就没看驱动开发。好在内核已经为我们提供了现成的驱动,我们只需要在设备树里定义好LCD的相关信息,就可以点亮屏幕。至于实际底层是驱动的,我们这里就先不搞了,以后如果有机会再回头看看裸机驱动里的LCD篇。有一点要注意的是:这一章我们只考虑LCD的正常点亮,暂时不考虑屏幕的触摸驱动。
FrameBuffer设备
可以先参考一下教程里是如何实现LCD的裸机驱动的:
- 初始化I.MX的eLCDif控制器,重点是LCD的信息
- 初始化LCD像素时钟
- 设置RGBLCD显存
- 直接通过操作显存来实现在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文件最后追加这个文件的运行指令
保存以后重启开发板,屏就不会熄灭了。