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 操作集 :
static const struct file_operations fb_fops = { .owner = THIS_MODULE, .read = fb_read, .write = fb_write, .unlocked_ioctl = fb_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = fb_compat_ioctl, #endif .mmap = fb_mmap, .open = fb_open, .release = fb_release, #ifdef HAVE_ARCH_FB_UNMAPPED_AREA .get_unmapped_area = get_fb_unmapped_area, #endif #ifdef CONFIG_FB_DEFERRED_IO .fsync = fb_deferred_io_fsync, #endif .llseek = default_llseek, };
通过freanmbuffer机制将底层的LCD抽象为/dev/fbx,x=0,1,应用程序可以通过操作/dev/fbx来操作屏幕。
Linux 下Framebuffer 驱动的编写流程:
Linux 内核将所有的 Framebuffer 抽象为一个叫做 fb_info 的结构体, fb_info 结构体包含了 Framebuffer 设备的完整属性和操作集合,因此每一个 Framebuffer 设备都必须有一个 fb_info。
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; /* 当前视频模式 */ #ifdef CONFIG_FB_BACKLIGHT /* 如果 LCD 支持背光的话 */ /* 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]; #endif ......... 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
函数原型如下: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
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
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,然后将两个联系起来。
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外设的基地址。
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
ret = mxsfb_init_fbinfo(host);
register_framebuffer注册
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:
host->id = of_alias_get_id(np, "lcdif"); //寻找节点
2.修改设备树参数
2.1屏幕引脚设置
屏幕引脚设置:
- 将屏幕引脚电器属性0X49,就是修改LCD引脚驱动能力。
bus-width = <24>;打开绑定文档:
- bits-per-pixel : <16> for RGB565, <32> for RGB888/666. bits-per-pixel = <32>;
修改过后 //我用的是480*800的屏幕。具体参数得看自己的屏幕尺寸
&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 背光亮度的调节,进入如下目录:
/sys/devices/platform/backlight/backlight/backlight
brightness 表示当前亮度等级, max_bgigntness 表示最大亮度等级。
当前屏幕亮度等级为 6,根据前面的分析可以,这个是 50%亮度。屏幕最大亮度等级为 7。如果我们要修改屏幕亮度,只需要向 brightness 写入需要设置的屏幕亮度等级即可。比如设置屏幕亮度等级为 7,那么可以使用如下命令:
echo 7 > brightness
3.实验
修改好设备树之后,输入命令,即可在屏幕上显示字符。
4.设置LCD作为终端控制台
4.1设置uboot中的bootargs
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 文件,在里面加入下面这一行:
tty1::askfirst:-/bin/sh
插上键盘,就可输入了。
5.LCD 自动关闭解决方法
默认情况下 10 分钟以后 LCD 就会熄屏,这个并不是代码有问题,而是 Linux 内核设置的,就和我们用手机或者电脑一样,一段时间不操作的话屏幕就会熄灭,以节省电能。
1、按键盘唤醒
最简单的就是按下回车键唤醒屏幕,我们在前面将KEY按键注册为了回车键,因此按下开发板上的 KEY 按键即可唤醒屏幕。如果开发板上没有按键的话可以外接 USB 键盘,然后按下 USB 键盘上的回车键唤醒屏幕。
2、关闭 10 分钟熄屏功能
在 Linux 源码中找到 drivers/tty/vt/vt.c 这个文件,在此文件中找到 blankinterval 变量,如下所示:
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 的文件,然后在里面输入如下所示内容:
#include <fcntl.h> #include <stdio.h> #include <sys/ioctl.h> 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 这个文件:
ueabihf-gcc lcd_always_on.c -o lcd_always_on
编译生成 lcd_always_on 以后将此可执行文件拷贝到开发板根文件系统的/usr/bin 目录中,然后给予可执行权限。设置 lcd_always_on 这个软件为开机自启动,打开/etc/init.d/rcS,在此文件最后面加入如下内容:
cd /usr/bin ./lcd_always_on cd ..
修改完成以后保存/etc/init.d/rcS 文件,然后重启开发板即可。