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 = <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>;
    			};
    		};
    	};
    };
    
  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结构体定义如下:

    ../../_images/image-20210906161125676.png

    of_device_id结构体定义如下:

    ../../_images/image-20210906161241463.png

    从上面的代码可以看出,设备树compatible 最总是fsl,imx28-lcdif名字和mxsfb_dt_ids[1]匹配成功的。

  3. 注册平台驱动

    在第二步的时候使用宏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)
    
  4. 当和总线匹配成功时候,会调用平台驱动下的probe函数

    分析mxsfb.c文件的probe函数mxsfb_probe

    1. 分配fb_info

      ../../_images/image-20210906163241505.png

    2. 设置fb_info:该结构体里面保存了大量的lcd相关的参数和操作函数指针,并且初始化硬件,需要用户自己去设置

      ../../_images/image-20210906163437772.png

    3. 注册fb_info:实际在register_framebuffer里面完成在类fb_class下创建设备节点的功能

      ../../_images/image-20210906163511756.png

      register_framebuffer函数功能如下:

      ../../_images/image-20210906164052093.png

      并且在函数中还完成了一个重要的操作把fb_info更加次设备号索引i放到数组registered_fb里面,后面会用。

      ../../_images/image-20210906170609379.png

      疑问:我们不应该在probe函数下完成字符设备创建那一套:register_chrdev->cdev_init->class_create这3个步骤吗,为什么这里只完成了在类fb_class下创建设备节点第4个步骤,并且fb_class来自哪里?

      解答:实际上系统已经为我们做了register_chrdev->cdev_init->class_create这三步骤,在fbmem.c里面,找到入口函数如下:

      ../../_images/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)   

具体框架如下图所示:

../../_images/image-20210906200959639.png

../../_images/image-20210906194122255.png

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

../../_images/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中: ../../_images/image-20210907111318478.png

  1. pinctrl_lcdif_dat:数据引脚

    ../../_images/image-20210907111618718.png

  2. pinctrl_lcdif_ctrl:控制引脚

    ../../_images/image-20210907111543342.png

  3. pinctrl_pwm1:背光引脚,这里使用pwm控制背光

    ../../_images/image-20210907111709997.png

时钟设置

IMX6ULL的LCD控制器涉及2个时钟:

../../_images/032_lcd_controller_clk.png

  1. LCD控制器时钟设备树参考:arch/arm/boot/dts/imx6ull.dtsi

    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";
    };
    

    定义了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);
      
  2. clock-frequency时钟参考设备树`arch/arm/boot/dts/imx6ull-14x14-evk.dts

    1. `

    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,代码: ../../_images/030_set_clk.pngimage-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);