FB驱动框架
fb驱动框架构建步骤
fb驱动框架采用总线设备驱动模型,然后用设备树去匹配。框架步骤如下:
编写设备树文件,注意里面的
.compatible属性;构建自己的
platform_driver结构体;注册平台驱动。
当和总线匹配成功时候,会调用平台驱动下的probe函数,在probe含中获取匹配的硬件资源,然后初始创建字符设备,创建class,在class下创建字符设备。
imx6ull fb驱动框架分析
编写设备树文件,注意里面的
.compatible属性;arch\arm\boot\dts\imx6ull.dtsi文件,定义了compatible属性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"; };
arch\arm\boot\dts\imx6ull-14x14-evk.dts文件包含imx6ull.dtsi文件,修改lcdif节点如下&lcdif { pinctrl-names = "default"; pinctrl-0 = <&pinctrl_lcdif_dat &pinctrl_lcdif_ctrl>; display = <&display0>; status = "okay"; display0: display@0 { bits-per-pixel = <16>; bus-width = <24>; display-timings { native-mode = <&timing0>; timing0: timing0 { mode_name = "TFT50AB"; clock-frequency = <27000000>; hactive = <800>; vactive = <480>; hfront-porch = <23>; hback-porch = <46>; hsync-len = <1>; vback-porch = <22>; vfront-porch = <22>; vsync-len = <1>; hsync-active = <0>; vsync-active = <0>; de-active = <1>; pixelclk-active = <0>; }; }; }; };
构建自己的
platform_driver结构体一般fb驱动位于drivers/video/fbdev目录,查看drivers/video/fbdev下的makefile,可知关于fsl芯片加入编译的文件mx3fb.c或者mxsfb.c,我们通过查看mx3fb.c或者mxsfb.c文件发现,和设备树
compatible属性匹配只有mxsfb.c,故可以确定,mxsfb.c正是imx6ull的fb驱动文件。# Platform or fallback drivers go here ……………… obj-$(CONFIG_FB_MX3) += mx3fb.o ……………… obj-$(CONFIG_FB_MXS) += mxsfb.o ………………
分析mxsfb.c文件如下:
构建自己的platform_driver结构体,名为 mxsfb_driver,里面有设备树匹配结构体mxsfb_dt_ids和匹配成功的时调用的mxsfb_probe函数
platform_driver结构体定义如下:
of_device_id结构体定义如下:
从上面的代码可以看出,设备树compatible 最总是
fsl,imx28-lcdif名字和mxsfb_dt_ids[1]匹配成功的。注册平台驱动
在第二步的时候使用宏
module_platform_driver完成平台驱动的注册module_platform_driver(mxsfb_driver); //宏定义展开后如下: static int __init mxsfb_driver_init(void) { return platform_driver_register(&mxsfb_driver); } module _init(mxsfb_driver_init) static int __exit mxsfb_driver_exit(void) { return platform_driver_unregister(&mxsfb_driver); } module _exit(mxsfb_driver_exit)
当和总线匹配成功时候,会调用平台驱动下的probe函数
分析mxsfb.c文件的probe函数
mxsfb_probe分配
fb_info
设置
fb_info:该结构体里面保存了大量的lcd相关的参数和操作函数指针,并且初始化硬件,需要用户自己去设置
注册
fb_info:实际在register_framebuffer里面完成在类fb_class下创建设备节点的功能
register_framebuffer函数功能如下:
并且在函数中还完成了一个重要的操作把
fb_info更加次设备号索引i放到数组registered_fb里面,后面会用。
疑问:我们不应该在probe函数下完成字符设备创建那一套:register_chrdev->cdev_init->class_create这3个步骤吗,为什么这里只完成了在类fb_class下创建设备节点第4个步骤,并且fb_class来自哪里?
解答:实际上系统已经为我们做了register_chrdev->cdev_init->class_create这三步骤,在fbmem.c里面,找到入口函数如下:

所以当系统加载fb驱动的时候,步骤如下:
会先调用fbmem.c里面的fbmem_init函数完成字符设备驱动的前三步骤
当具体的单板的设备树和分析mxsfb.c里面的匹配时候,调用文件mxsfb.c里面函数
mxsfb_probe来完成字符设备驱动的最后一步,在类下创建设备节点。
总结
分为上下两层:
fbmem.c:承上启下
实现、注册file_operations结构体
把APP的调用向下转发到具体的硬件驱动程序
xxxfb.c:硬件相关的驱动程序,比如mxsfb.c文件
实现、注册fb_info结构体
实现硬件操作
调用关系:
app: open("/dev/fb0", ...) 主设备号: 29, 次设备号: 0
--------------------------------------------------------------
kernel:
fb_open
int fbidx = iminor(inode);
struct fb_info *info = = registered_fb[0];
例子2:
app: read()
---------------------------------------------------------------
kernel:
fb_read
int fbidx = iminor(inode);
struct fb_info *info = registered_fb[fbidx];
if (info->fbops->fb_read)
return info->fbops->fb_read(info, buf, count, ppos); //一般不提供该函数
src = (u32 __iomem *) (info->screen_base + p);
dst = buffer;
*dst++ = fb_readl(src++);
copy_to_user(buf, buffer, c)
具体框架如下图所示:


在mxsfb_probe函数中还初始化了mxsfb_ops结构体,这里面我们并没有提供open、read等函数,如下:

imx6ull fb驱动关于硬件的操作
上面介绍了imx6关于FB的驱动框架的流程,可以看到关于硬件的操作主要在mxsfb.c文件的probe函数里面。
硬件的操作主要分为下面几步:
GPIO设置
LCD引脚
背光引脚
时钟设置
确定LCD控制器的时钟
根据LCD的DCLK计算相关时钟
LCD控制器本身的设置
比如设置Framebuffer的地址
设置Framebuffer中数据格式、LCD数据格式
设置时序
GPIO设置
有两种方法:
直接读写相关寄存器
使用设备树,在设备树中设置pinctrl
本课程专注于LCD,所以使用pinctrl简化程序
设备树arch\arm\boot\dts\imx6ull-14x14-evk.dts中:

pinctrl_lcdif_dat:数据引脚
pinctrl_lcdif_ctrl:控制引脚
pinctrl_pwm1:背光引脚,这里使用pwm控制背光
时钟设置
IMX6ULL的LCD控制器涉及2个时钟:

LCD控制器时钟设备树参考:
arch/arm/boot/dts/imx6ull.dtsilcdif: 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"; };
定义了3个时钟:
pix:Pixel clock,用于LCD接口,设置为LCD手册上的参数
axi:AXI clock,用于传输数据、读写寄存器,使能即可
disp_axi:一个虚拟的时钟,可以不用设置
该时钟在代码中初始化到寄存器的过程如下:
获得时钟
host->clk_pix = devm_clk_get(&host->pdev->dev, "pix"); if (IS_ERR(host->clk_pix)) { host->clk_pix = NULL; ret = PTR_ERR(host->clk_pix); goto fb_release; } host->clk_axi = devm_clk_get(&host->pdev->dev, "axi"); if (IS_ERR(host->clk_axi)) { host->clk_axi = NULL; ret = PTR_ERR(host->clk_axi); dev_err(&pdev->dev, "Failed to get axi clock: %d\n", ret); goto fb_release; } host->clk_disp_axi = devm_clk_get(&host->pdev->dev, "disp_axi"); if (IS_ERR(host->clk_disp_axi)) { host->clk_disp_axi = NULL; ret = PTR_ERR(host->clk_disp_axi); dev_err(&pdev->dev, "Failed to get disp_axi clock: %d\n", ret); goto fb_release; }
设置频率:只需要设置pixel clock的频率
ret = clk_set_rate(host->clk_pix, PICOS2KHZ(fb_info->var.pixclock) * 1000U);
使能时钟
clk_enable_pix(host); clk_prepare_enable(host->clk_pix); clk_enable_axi(host); clk_prepare_enable(host->clk_axi); clk_enable_disp_axi(host); clk_prepare_enable(host->clk_disp_axi);
clock-frequency时钟参考设备树`arch/arm/boot/dts/imx6ull-14x14-evk.dts
`
display-timings { native-mode = <&timing0>; timing0: timing0 { clock-frequency = <27000000>;
该时钟在代码中初始化到寄存器的过程如下
从设备树获得dot clock,存入display_timing,文件:drivers\video\of_display_timing.c
ret |= parse_timing_property(np, "clock-frequency", &dt->pixelclock);
使用display_timing来设置videomode,文件:drivers\video\videomode.c,代码:
void videomode_from_timing(const struct display_timing *dt, struct videomode *vm) { vm->pixelclock = dt->pixelclock.typ; vm->hactive = dt->hactive.typ; vm->hfront_porch = dt->hfront_porch.typ; vm->hback_porch = dt->hback_porch.typ; vm->hsync_len = dt->hsync_len.typ; vm->vactive = dt->vactive.typ; vm->vfront_porch = dt->vfront_porch.typ; vm->vback_porch = dt->vback_porch.typ; vm->vsync_len = dt->vsync_len.typ; vm->flags = dt->flags; }
根据videomode的值,使用时钟子系统的函数设置时钟:,文件:drivers\video\fbdev\mxc\ldb.c,代码:
image-20210115201815912
LCD控制器的配置
以设置分辨率为例。
在设备树里指定频率:
文件:arch/arm/boot/dts/imx6ull-14x14-evk.dts,代码:clock-frequency
timing0: timing0 { mode_name = "TFT50AB"; clock-frequency = <27000000>; hactive = <800>; vactive = <480>;
从设备树获得分辨率,存入display_timing
文件:drivers\video\of_display_timing.c,代码:
ret |= parse_timing_property(np, "hactive", &dt->hactive); ret |= parse_timing_property(np, "vactive", &dt->vactive);
使用display_timing来设置videomode
文件:drivers\video\videomode.c,代码:
void videomode_from_timing(const struct display_timing *dt, struct videomode *vm) { vm->hactive = dt->hactive.typ;
vm->vactive = dt->vactive.typ;
* 根据videomode的值,设置fb_videomode
文件:drivers\video\fbdev\core\fbmon.c,代码:
```c
int fb_videomode_from_videomode(const struct videomode *vm,
struct fb_videomode *fbmode)
{
unsigned int htotal, vtotal;
fbmode->xres = vm->hactive;
fbmode->yres = vm->vactive;
根据fb_videomode的值,设置fb_info中的var:
文件:drivers\video\fbdev\core\modedb.c,代码:
void fb_videomode_to_var(struct fb_var_screeninfo *var, const struct fb_videomode *mode) { var->xres = mode->xres; var->yres = mode->yres;
* 根据var的分辨率,设置寄存器
文件:drivers\video\fbdev\mxsfb.c,代码:
```c
writel(TRANSFER_COUNT_SET_VCOUNT(fb_info->var.yres) |
TRANSFER_COUNT_SET_HCOUNT(fb_info->var.xres),
host->base + host->devdata->transfer_count);