# FB驱动框架 ## fb驱动框架构建步骤 fb驱动框架采用总线设备驱动模型,然后用设备树去匹配。框架步骤如下: 1. 编写设备树文件,注意里面的`.compatible`属性; 2. 构建自己的`platform_driver`结构体; 3. 注册平台驱动。 4. 当和总线匹配成功时候,会调用平台驱动下的probe函数,在probe含中获取匹配的硬件资源,然后初始创建字符设备,创建class,在class下创建字符设备。 ## imx6ull fb驱动框架分析 1. 编写设备树文件,注意里面的`.compatible`属性; `arch\arm\boot\dts\imx6ull.dtsi`文件,定义了`compatible`属性 ``` lcdif: lcdif@021c8000 { compatible = "fsl,imx6ul-lcdif", "fsl,imx28-lcdif"; reg = <0x021c8000 0x4000>; interrupts = ; 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>; }; }; }; }; ``` 2. 构建自己的`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`结构体定义如下: ![](media/image-20210906161125676.png) `of_device_id`结构体定义如下: ![](media/image-20210906161241463.png) 从上面的代码可以看出,设备树compatible 最总是`fsl,imx28-lcdif`名字和mxsfb_dt_ids[1]匹配成功的。 3. 注册平台驱动 在第二步的时候使用宏`module_platform_driver`完成平台驱动的注册 ```c 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) ``` 4. 当和总线匹配成功时候,会调用平台驱动下的probe函数 分析mxsfb.c文件的probe函数`mxsfb_probe` 1. 分配`fb_info` ![](media/image-20210906163241505.png) 2. 设置`fb_info`:该结构体里面保存了大量的lcd相关的参数和操作函数指针,并且初始化硬件,需要用户自己去设置 ![](media/image-20210906163437772.png) 3. 注册`fb_info`:实际在`register_framebuffer`里面完成在类fb_class下创建设备节点的功能 ![](media/image-20210906163511756.png) `register_framebuffer`函数功能如下: ![](media/image-20210906164052093.png) 并且在函数中还完成了一个重要的操作把`fb_info`更加次设备号索引`i`放到数组`registered_fb`里面,后面会用。 ![](media/image-20210906170609379.png) 疑问:我们不应该在probe函数下完成字符设备创建那一套:register_chrdev->cdev_init->class_create这3个步骤吗,为什么这里只完成了在类fb_class下创建设备节点第4个步骤,并且fb_class来自哪里? 解答:实际上系统已经为我们做了register_chrdev->cdev_init->class_create这三步骤,在fbmem.c里面,找到入口函数如下: ![](media/image-20210906164936156.png) 所以当系统加载fb驱动的时候,步骤如下: 1. 会先调用fbmem.c里面的fbmem_init函数完成字符设备驱动的前三步骤 2. 当具体的单板的设备树和分析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) ``` 具体框架如下图所示: ![](media/image-20210906200959639.png) ![](media/image-20210906194122255.png) 在`mxsfb_probe`函数中还初始化了`mxsfb_ops`结构体,这里面我们并没有提供open、read等函数,如下: ![](media/image-20210906200556081.png) ## 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`中: ![](media/image-20210907111318478.png) 1. `pinctrl_lcdif_dat`:数据引脚 ![](media/image-20210907111618718.png) 2. `pinctrl_lcdif_ctrl`:控制引脚 ![](media/image-20210907111543342.png) 3. `pinctrl_pwm1`:背光引脚,这里使用pwm控制背光 ![](media/image-20210907111709997.png) ### 时钟设置 IMX6ULL的LCD控制器涉及2个时钟: ![](media/032_lcd_controller_clk.png) 1. LCD控制器时钟设备树参考:`arch/arm/boot/dts/imx6ull.dtsi` ```shell lcdif: lcdif@021c8000 { compatible = "fsl,imx6ul-lcdif", "fsl,imx28-lcdif"; reg = <0x021c8000 0x4000>; interrupts = ; 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:一个虚拟的时钟,可以不用设置 该时钟在代码中初始化到寄存器的过程如下: * 获得时钟 ```c 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的频率 ```c ret = clk_set_rate(host->clk_pix, PICOS2KHZ(fb_info->var.pixclock) * 1000U); ``` * 使能时钟 ```c 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); ``` 2. clock-frequency时钟参考设备树`arch/arm/boot/dts/imx6ull-14x14-evk.dts 1. ` ```shell display-timings { native-mode = <&timing0>; timing0: timing0 { clock-frequency = <27000000>; ``` 该时钟在代码中初始化到寄存器的过程如下 * 从设备树获得dot clock,存入display_timing,文件:drivers\video\of_display_timing.c ```c ret |= parse_timing_property(np, "clock-frequency", &dt->pixelclock); ``` * 使用display_timing来设置videomode,文件:drivers\video\videomode.c,代码: ```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](media/030_set_clk.png) ### LCD控制器的配置 以设置分辨率为例。 * 在设备树里指定频率: 文件:arch/arm/boot/dts/imx6ull-14x14-evk.dts,代码:clock-frequency ```shell timing0: timing0 { mode_name = "TFT50AB"; clock-frequency = <27000000>; hactive = <800>; vactive = <480>; ``` * 从设备树获得分辨率,存入display_timing 文件:drivers\video\of_display_timing.c,代码: ```c ret |= parse_timing_property(np, "hactive", &dt->hactive); ret |= parse_timing_property(np, "vactive", &dt->vactive); ``` * 使用display_timing来设置videomode 文件:drivers\video\videomode.c,代码: ```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,代码: ```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); ```