LCD 是很常用的一个外设,在搭配 QT 这样的 GUI 库下可以制作出非常精美的 UI 界面。
1.Linux下LCD驱动简析
1.1Framebuffer 设备
裸机 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。
file_operations 操作集 :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 static const struct file_operations fb_fops = {
.owner = THIS_MODULE,
.read = fb_read,
.write = fb_write,
.unlocked_ioctl = fb_ioctl,
.compat_ioctl = fb_compat_ioctl,
.mmap = fb_mmap,
.open = fb_open,
.release = fb_release,
.get_unmapped_area = get_fb_unmapped_area,
.fsync = fb_deferred_io_fsync,
.llseek = default_llseek,
};通过freanmbuffer机制将底层的LCD抽象为/dev/fbx,x=0,1,应用程序可以通过操作/dev/fbx来操作屏幕。
Linux 下Framebuffer 驱动的编写流程:
Linux 内核将所有的 Framebuffer 抽象为一个叫做 fb_info 的结构体, fb_info 结构体包含了 Framebuffer 设备的完整属性和操作集合,因此每一个 Framebuffer 设备都必须有一个 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 struct fb_info {
atomic_t count;
int node;
int flags;
struct mutex lock; /* 互斥锁 */
struct mutex mm_lock; /* 互斥锁,用于 fb_mmap 和 smem_*域*/
struct fb_var_screeninfo var; /* 当前可变参数 */
struct fb_fix_screeninfo fix; /* 当前固定参数 */
struct fb_monspecs monspecs; /* 当前显示器特性 */
struct work_struct queue; /* 帧缓冲事件队列 */
struct fb_pixmap pixmap; /* 图像硬件映射 */
struct fb_pixmap sprite; /* 光标硬件映射 */
struct fb_cmap cmap; /* 当前调色板 */
struct list_head modelist; /* 当前模式列表 */
struct fb_videomode *mode; /* 当前视频模式 */
/* assigned backlight device */
/* set before framebuffer registration,
remove after unregister */
struct backlight_device *bl_dev; /* 背光设备 */
/* Backlight level curve */
struct mutex bl_curve_mutex;
u8 bl_curve[FB_BACKLIGHT_LEVELS];
.........
struct fb_ops *fbops;/* 帧缓冲操作函数集 */
struct device *device; /* 父设备 */
struct device *dev; /* 当前 fb 设备 */
int class_flag; /* 私有 sysfs 标志 */
.........
char __iomem *screen_base; /* 虚拟内存基地址(屏幕显存) */
unsigned long screen_size; /* 虚拟内存大小(屏幕显存大小) */
void *pseudo_palette; /* 伪 16 位调色板 */
..........
};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
函数原型如下:
1
2
3
4
5
6
7
8
9
10 int register_framebuffer(struct fb_info *fb_info)
{
int ret;
mutex_lock(®istration_lock);
ret = do_register_framebuffer(fb_info);
mutex_unlock(®istration_lock);
return ret;
}
- fb_info:需要上报的 fb_info。
- 返回值: 0,成功;负值,失败。
卸载用
unregister_framebuffer
1.2LCD 驱动简析
LCD 裸机例程主要分两部分:
- 获取 LCD 的屏幕参数。
- 根据屏幕参数信息来初始化 eLCDIF 接口控制器。
- 不同分辨率的 LCD 屏幕其 eLCDIF 控制器驱动代码都是一样的,只需要修改好对应的屏幕参数即可。
- 屏幕参数信息属于屏幕设备信息内容,这些肯定是要放到设备树中的,因此我们本章实验的主要工作就是修改设备树。
打开 imx6ull.dtsi,然后找到 lcdif
1
2
3
4
5
6
7
8
9
10 lcdif: lcdif@021c8000 {
compatible = "fsl,imx6ul-lcdif", "fsl,imx28-lcdif";
reg = <0x021c8000 0x4000>;
interrupts = <GIC_SPI 5 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_LCDIF_PIX>,
<&clks IMX6UL_CLK_LCDIF_APB>,
<&clks IMX6UL_CLK_DUMMY>;
clock-names = "pix", "axi", "disp_axi";
status = "disabled";
};搜索compatible属性,即可找到对应的驱动函数:drivers/video/fbdev/mxsfb.c
1
2
3
4
5 static const struct of_device_id mxsfb_dt_ids[] = {
{ .compatible = "fsl,imx23-lcdif", .data = &mxsfb_devtype[0], },
{ .compatible = "fsl,imx28-lcdif", .data = &mxsfb_devtype[1], },
{ /* sentinel */ }
};
驱动文件为mxsfb.c为platform驱动框架,驱动和设备匹配以后,mxsfb_probe函数执行:
struct mxsfb_info,给mxsfb_info申请内存,申请fb_info,然后将两个联系起来。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 host = devm_kzalloc(&pdev->dev, sizeof(struct mxsfb_info), GFP_KERNEL);
if (!host) {
dev_err(&pdev->dev, "Failed to allocate IO resource\n");
return -ENOMEM;
}
fb_info = framebuffer_alloc(sizeof(struct fb_info), &pdev->dev);
if (!fb_info) {
dev_err(&pdev->dev, "Failed to allocate fbdev\n");
devm_kfree(&pdev->dev, host);
return -ENOMEM;
}
host->fb_info = fb_info;
fb_info->par = host;host->base就是内存映射以后的LCDIF外设的基地址。
1
2
3
4
5
6 host->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(host->base)) {
dev_err(&pdev->dev, "ioremap failed\n");
ret = PTR_ERR(host->base);
goto fb_release;
}mxsfb_init_fbinfo 初始化fb_info
1 ret = mxsfb_init_fbinfo(host);register_framebuffer注册
1
2
3
4
5 ret = register_framebuffer(fb_info);
if (ret != 0) {
dev_err(&pdev->dev, "Failed to register framebuffer\n");
goto fb_destroy;
}mxsfb_probe函数重点工作:
1.初始化fb_info并且向内核注册
2.初始化LCDIF控制器
mxsfb_init_fbinfo_dt
函数会从设备树中读取相关属性信息。mxsfb_init_fbinfo_dt:
1 host->id = of_alias_get_id(np, "lcdif"); //寻找节点
2.修改设备树参数
2.1屏幕引脚设置
屏幕引脚设置:
- 将屏幕引脚电器属性0X49,就是修改LCD引脚驱动能力。
bus-width = <24>;打开绑定文档:
1
2
3 - bits-per-pixel : <16> for RGB565, <32> for RGB888/666.
bits-per-pixel = <32>;修改过后 //我用的是480*800的屏幕。具体参数得看自己的屏幕尺寸
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>;
display = <&display0>;
status = "okay";
display0: display {
bits-per-pixel = <32>;
bus-width = <24>;
display-timings {
native-mode = <&timing0>;
timing0: timing0 {
clock-frequency = <31500000>;
hactive = <800>;
vactive = <480>;
hfront-porch = <40>;
hback-porch = <88>;
hsync-len = <48>;
vback-porch = <32>;
vfront-porch = <13>;
vsync-len = <3>;
hsync-active = <0>;
vsync-active = <0>;
de-active = <1>;
pixelclk-active = <0>;
};
};
};
};
2.2背光
一般屏幕背光用PWM控制亮度,一般测试屏幕的时候直接将背光引脚拉高或拉低。
背光设备树节点设置了 8 个等级的背光调节,可以设置为 0~7,我们可以通过设置背光等级来实现 LCD 背光亮度的调节,进入如下目录:
1 /sys/devices/platform/backlight/backlight/backlightbrightness 表示当前亮度等级, max_bgigntness 表示最大亮度等级。
当前屏幕亮度等级为 6,根据前面的分析可以,这个是 50%亮度。屏幕最大亮度等级为 7。如果我们要修改屏幕亮度,只需要向 brightness 写入需要设置的屏幕亮度等级即可。比如设置屏幕亮度等级为 7,那么可以使用如下命令:
1 echo 7 > brightness
3.实验
修改好设备树之后,输入命令,即可在屏幕上显示字符。
4.设置LCD作为终端控制台
4.1设置uboot中的bootargs
1 setenv bootargs 'console=tty1 console=ttymxc0,115200 root=/dev/nfs rw nfsroot=192.168.10.100:/home/zwl/linux/nfs/my-rootfs ip=192.168.10.50:192.168.10.100:192.168.10.1:255.255.255.0::eth0:off'
4.2修改/etc/inittab文件
打开开发板根文件系统中的/etc/inittab 文件,在里面加入下面这一行:
1 tty1::askfirst:-/bin/sh
插上键盘,就可输入了。
5.LCD 自动关闭解决方法
默认情况下 10 分钟以后 LCD 就会熄屏,这个并不是代码有问题,而是 Linux 内核设置的,就和我们用手机或者电脑一样,一段时间不操作的话屏幕就会熄灭,以节省电能。
1、按键盘唤醒
最简单的就是按下回车键唤醒屏幕,我们在前面将KEY按键注册为了回车键,因此按下开发板上的 KEY 按键即可唤醒屏幕。如果开发板上没有按键的话可以外接 USB 键盘,然后按下 USB 键盘上的回车键唤醒屏幕。
2、关闭 10 分钟熄屏功能
在 Linux 源码中找到 drivers/tty/vt/vt.c 这个文件,在此文件中找到 blankinterval 变量,如下所示:
1
2
3 static int vesa_blank_mode;
static int vesa_off_interval;
static int blankinterval = 10*60;blankinterval 变量控制着 LCD 关闭时间,默认是 10*60,也就是 10 分钟。将 blankinterval的值改为 0 即可关闭 10 分钟熄屏的功能,修改完成以后需要重新编译 Linux 内核,得到新的zImage,然后用新的 zImage 启动开发板。
3、编写一个 APP 来关闭熄屏功能
在 ubuntu 中新建一个名为 lcd_always_on.c 的文件,然后在里面输入如下所示内容:
1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, char *argv[])
{
int fd;
fd = open("/dev/tty1", O_RDWR);
write(fd, "\033[9;0]", 8);
close(fd);
return 0;
}使用如下命令编译 lcd_always_on.c 这个文件:
1 ueabihf-gcc lcd_always_on.c -o lcd_always_on编译生成 lcd_always_on 以后将此可执行文件拷贝到开发板根文件系统的/usr/bin 目录中,然后给予可执行权限。设置 lcd_always_on 这个软件为开机自启动,打开/etc/init.d/rcS,在此文件最后面加入如下内容:
1
2
3 cd /usr/bin
./lcd_always_on
cd ..修改完成以后保存/etc/init.d/rcS 文件,然后重启开发板即可。