LCD是一个常用外设,一般情况下半导体厂商会为自家的芯片编写好相应的LCD接口驱动程序。开发者无需修改LCD驱动部分,只要按照所使用的LCD设备来修改设备树即可。虽然不需要修改驱动,但是我们还是有必要了解一下LCD驱动的流程
1. Framebuffer设备
在Linux中应用程序最终是通过操作RGB LCD的显存来实现 LCD上显示字符、图片等信息。Linux系统中内存的管理很严格,显存是需要申请的,而且因为虚拟内存的存在,驱动程序设置的显存和应用程序访问的显存必须要是同一片物理内存。
为了解决该问题,Framebuffer诞生了,Framebuffer就是帧缓冲(简称 fb)。Framebuffer是保存着一帧图像的一块内存,向这块内存写入数据就相当于向屏幕中写入数据。简单来说Framebuffe 把屏幕上的每个点映射成一段线性内存空间,程序可以简单的改变这段内存的值来改变屏幕上某一点的颜色。
Framebuffer机制为用户空间操作显示设备提供了统一的接口,屏蔽了底层硬件之间的差异,虚拟出一个fb设备,当LCD驱动加载成功后,会生成名为 /dev/fbX的设备,应用程序通过访问 /dev/fbX这个设备就可以访问 LCD
/dev/fbX是 LCD对应的设备文件, 是个字符设备,因此有其对应的file_operations操作集, 定义在 drivers/video/fbdev/core/fbmem.c文件中,如下所示:
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,
};
Linux内核将所有的Framebuffer抽象为fb_info结构体, fb_info结构体包含了Framebuffer设备的完整属性和操作集合,因此每一个Framebuffer设备都必须有一 个fb_info。该结构体定义在include/linux/fb.h文件里面,内容如下
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, 467 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
2. LCD驱动程序分析
同一个芯片下,不同分辨率LCD屏幕的eLCDIF控制器驱动代码都是一样的,下面以NXP官方编写的IMX6ULL芯片在Linux下的LCD驱动为例,来简单梳理一下LCD驱动的流程
- 打开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属性值,在Linux源码中搜索
"fsl,imx6ul-lcdif", "fsl,imx28-lcdif"
符串,即可找到IMX6ULL的LCD驱动文件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 */ }
};
......
......
static struct platform_driver mxsfb_driver = {
.probe = mxsfb_probe,
.remove = mxsfb_remove,
.shutdown = mxsfb_shutdown,
.id_table = mxsfb_devtype,
.driver = {
.name = DRIVER_NAME,
.of_match_table = mxsfb_dt_ids,
.pm = &mxsfb_pm_ops,
},
};
module_platform_driver(mxsfb_driver);
- 可见这是一个标准的platform驱动,当驱动和设备匹配以后mxsfb_probe函数就会执行
static int mxsfb_probe(struct platform_device *pdev) {
const struct of_device_id *of_id = of_match_device(mxsfb_dt_ids, &pdev->dev);
struct resource *res;
struct mxsfb_info *host; //LCD主控接口
struct fb_info *fb_info; //
struct pinctrl *pinctrl;
int irq = platform_get_irq(pdev, 0);
int gpio, ret;
......
//从设备树中获取eLCDIF接口控制器的寄存器首地址
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!res) {
dev_err(&pdev->dev, "Cannot get memory IO resource\n");
return -ENODEV;
}
//给host申请内存,
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申请内存,也就是申请fb_info
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; //设置host的fb_info成员变量为fb_info,
fb_info->par = host; //设置fb_info的par成员变量为host
//申请中断,中断服务函数为mxsfb_irq_handler
ret = devm_request_irq(&pdev->dev, irq, mxsfb_irq_handler, 0, dev_name(&pdev->dev), host);
if (ret) {
dev_err(&pdev->dev, "request_irq (%d) failed with
error %d\n", irq, ret);
ret = -ENODEV;
goto fb_release;
}
//对从设备树中获取到的寄存器首地址进行内存映射得到虚拟地址,并保存到host的base成员变量
//因此通过访问host的base成员即可访问IMX6ULL的整个eLCDIF寄存器
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;
}
......
//给fb_info中的pseudo_palette申请内存
fb_info->pseudo_palette = devm_kzalloc(&pdev->dev, sizeof(u32) * 16, GFP_KERNEL);
if (!fb_info->pseudo_palette) {
ret = -ENOMEM;
goto fb_release;
}
INIT_LIST_HEAD(&fb_info->modelist);
pm_runtime_enable(&host->pdev->dev);
//初始化fb_info
ret = mxsfb_init_fbinfo(host);
if (ret != 0)
goto fb_pm_runtime_disable;
mxsfb_dispdrv_init(pdev, fb_info);
if (!host->dispdrv) {
pinctrl = devm_pinctrl_get_select_default(&pdev->dev);
if (IS_ERR(pinctrl)) {
ret = PTR_ERR(pinctrl);
goto fb_pm_runtime_disable;
}
}
if (!host->enabled) {
writel(0, host->base + LCDC_CTRL);
mxsfb_set_par(fb_info); //设置eLCDIF控制器的相应寄存器
mxsfb_enable_controller(fb_info); //设置eLCDIF控制器的相应寄存器
pm_runtime_get_sync(&host->pdev->dev);
}
//向Linux内核注册fb_info
ret = register_framebuffer(fb_info);
if (ret != 0) {
dev_err(&pdev->dev, "Failed to register framebuffer\n");
goto fb_destroy;
}
......
return ret;
}
mxsfb_probe 函数的主要工作内容为:
– 申请 fb_info
– 初始化 fb_info 结构体中的各个成员变量
– 初始化 eLCDIF 控制器
– 使用 register_framebuffer 函数向 Linux 内核注册初始化好的 fb_info
至此,LCD驱动的基本流程就结束了,后续文章会继续介绍如何通过调整设备树参数,来点亮LCD屏幕。