Linux驱动开发(15):Framebuffer子系统–LCD驱动实验

Framebuffer是用一个视频输出设备从包含完整的帧数据的一个内存缓冲区中来驱动一个视频显示设备。 也就是说Framebuffer是一块内存保存着一帧的图像,向这块内存写入数据就相当于向屏幕中写入数据, 如果使用32位的数据来表示一个像素点(使用BBP表示),假设屏幕的显示频分辨率为1920x1080, 那么 Framebuffer所需要的内存为1920x1080x32/8=8,294,400字节约等于7.9M。

简单来说Framebuffer把屏幕上的每个点映射成一段线性内存空间, 程序可以简单的改变这段内存的值来改变屏幕上某一点的颜色。

1. Framebuffer子系统简介

Framebuffer子系统为用户空间操作显示设备提供了统一的接口,屏蔽了底层硬件之间的差异,用户只需要操作一块内存缓冲区即可把需要的图像显示到LCD设备上。Framebuffer子系统主要分为两个部分,如下图所示:

  • 核心层: 主要实现字符设备的创建,为不同的显示设备提供文件通用处理接口;同时创建graphics设备类,占据主设备号29。

  • 硬件设备层: 主要提供显示设备的时序、显存、像素格式等硬件信息,实现显示设备的私有文件接口,并创建显示设备文件/dev/fbx(x=0~n)暴露给用户空间。 硬件设备层的代码需要驱动开发人员根据具体的显示设备提供给内核。

1.1. 核心层分析

1.1.1. 注册字符设备、创建设备类

在内核启动时,Framebuffer子系统的 fbmem_init() 函数会被系统自动调用,该函数的具体实现如下:

fbmem_init (位于 内核源码/drivers/video/fbdev/core/fbmem.c):

cpp 复制代码
static int __init
fbmem_init(void)
{
  int ret;

  if (!proc_create_seq("fb", 0, NULL, &proc_fb_seq_ops))
    return -ENOMEM;

  ret = register_chrdev(FB_MAJOR, "fb", &fb_fops);
  if (ret) {
    printk("unable to get major %d for fb devs\n", FB_MAJOR);
    goto err_chrdev;
  }

  fb_class = class_create(THIS_MODULE, "graphics");
  if (IS_ERR(fb_class)) {
    ret = PTR_ERR(fb_class);
    pr_warn("Unable to create fb class; errno = %d\n", ret);
    fb_class = NULL;
    goto err_class;
  }

  fb_console_init();

  return 0;

err_class:
  unregister_chrdev(FB_MAJOR, "fb");
err_chrdev:
  remove_proc_entry("fb", NULL);
  return ret;
}

第9行:register_chrdev注册一个设备号为FB_MAJOR 的字符设备,与该设备号绑定的file_operations结构体为fb_fops, fb_fops为显示设备提供通用的文件操作接口。

  • FB_MAJOR 宏定义

FB_MAJOR 宏定义 (位于 内核源码/include/uapi/linux/major.h):

cpp 复制代码
#define FB_MAJOR 29
  • fb_fops文件操作接口

fb_fops 文件接口 (位于 内核源码/drivers/video/fbdev/core/fbmem.c):

cpp 复制代码
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,
#if defined(HAVE_ARCH_FB_UNMAPPED_AREA) || \
  (defined(CONFIG_FB_PROVIDE_GET_FB_UNMAPPED_AREA) && \
   !defined(CONFIG_MMU))
  .get_unmapped_area = get_fb_unmapped_area,
#endif
#ifdef CONFIG_FB_DEFERRED_IO
  .fsync =  fb_deferred_io_fsync,
#endif
  .llseek = default_llseek,
};
  • 第15行:调用class_create()函数在**/sys/class/**目录下创建graphics设备类。
1.1.2. fb_fops 文件通用处理接口
1.1.2.1. fb_open() 函数

fb_open() 函数 (位于 内核源码/drivers/video/fbdev/core/fbmem.c):

cpp 复制代码
static int fb_open(struct inode *inode, struct file *file)
{
  int fbidx = iminor(inode);
  struct fb_info *info;
  int res = 0;

  info = get_fb_info(fbidx);
  ...
  file->private_data = info;
  if (info->fbops->fb_open) {
    res = info->fbops->fb_open(info,1);
    if (res)
      module_put(info->fbops->owner);
  }
#ifdef CONFIG_FB_DEFERRED_IO
  if (info->fbdefio)
    fb_deferred_io_open(info, inode, file);
#endif
out:
  mutex_unlock(&info->lock);
  if (res)
    put_fb_info(info);
  return res;
}
  • 第3行:调用iminor函数获取inode设备文件对应的次设备号;

  • 第7行:调用get_fb_info函数根据设备文件的次设备号,从registered_fb全局数组找到对应的fb_info结构体;

  • 第10-11行:判断LCD设备的fb_info结构体的fbops成员是否有提供fb_open函数,如果有提供就执行该函数;

1.1.2.2. fb_read() 函数

fb_read() 函数 (位于 内核源码/drivers/video/fbdev/core/fbmem.c):

cpp 复制代码
static ssize_t
fb_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
      unsigned long p = *ppos;
      struct fb_info *info = file_fb_info(file);
      u8 *buffer, *dst;
      u8 __iomem *src;
      int c, cnt = 0, err = 0;
      unsigned long total_size;

      if (!info || ! info->screen_base)
              return -ENODEV;

      if (info->state != FBINFO_STATE_RUNNING)
              return -EPERM;

      if (info->fbops->fb_read)
              return info->fbops->fb_read(info, buf, count, ppos);

      total_size = info->screen_size;

      if (total_size == 0)
              total_size = info->fix.smem_len;

      if (p >= total_size)
              return 0;

      if (count >= total_size)
              count = total_size;

      if (count + p > total_size)
              count = total_size - p;

      buffer = kmalloc((count > PAGE_SIZE) ? PAGE_SIZE : count,
                       GFP_KERNEL);
      if (!buffer)
              return -ENOMEM;

      src = (u8 __iomem *) (info->screen_base + p);

      if (info->fbops->fb_sync)
              info->fbops->fb_sync(info);

      while (count) {
              c  = (count > PAGE_SIZE) ? PAGE_SIZE : count;
              dst = buffer;
              fb_memcpy_fromfb(dst, src, c);
              dst += c;
              src += c;

              if (copy_to_user(buf, buffer, c)) {
                      err = -EFAULT;
                      break;
              }
              *ppos += c;
              buf += c;
              cnt += c;
              count -= c;
      }

      kfree(buffer);

      return (err) ? err : cnt;
}
  • 第5行:file_fb_info()函数的作用也是根据文件的次设备号,从registered_fb全局数组找到对应的fb_info结构体;

  • 第17-18行:判断LCD设备的fb_info结构体是否有提供私有的fb_read文件操作接口,如果有提供直接调用LCD的私有fb_read函数并返回; 若没有提供,则继续往下执行通用的fb_read函数;

  • 第34~35行:分配一块最大为PAGE_SIZE(值为4096)的buffer;

  • 第39行:把源地址指向需要从framebuffer读取数据的起始位置;

  • 第44~59行:使用copy_to_user()函数把从framebuffer读取到的数据拷贝到用户空间。

1.1.2.3. fb_write() 函数

fb_write()函数和fb_read()函数实现的几乎是相同的,不相同的地方是fb_read()函数使用copy_to_user()把内核空间的数据拷贝到用户空间, 而fb_write()函数是使用copy_from_user()把用户空间的数据拷贝到内核空间。大家可自行阅读fbmem.c的代码对比这两个函数。

1.1.2.4. fb_ioctl() 函数

fb_ioctl() 函数 (位于 内核源码/drivers/video/fbdev/core/fbmem.c):

cpp 复制代码
static long fb_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
      struct fb_info *info = file_fb_info(file);

      if (!info)
              return -ENODEV;
      return do_fb_ioctl(info, cmd, arg);
}

从上面的代码可以看出,fb_ioctl()函数从全局数组registered_fb获取到fb_info结构体后,调用的是do_fb_ioctl()函数, 通过传入的cmd命令参数设置或者获取fb_info结构体的相关信息。do_fb_ioctl()函数的部分代码如下:

do_fb_ioctl() 函数 (位于 内核源码/drivers/video/fbdev/core/fbmem.c):

cpp 复制代码
static long do_fb_ioctl(struct fb_info *info, unsigned int cmd,
                      unsigned long arg)
{
      ...

      switch (cmd) {
      case FBIOGET_VSCREENINFO:
              if (!lock_fb_info(info))
                      return -ENODEV;
              var = info->var;
              unlock_fb_info(info);

              ret = copy_to_user(argp, &var, sizeof(var)) ? -EFAULT : 0;
              break;
      case FBIOPUT_VSCREENINFO:
              if (copy_from_user(&var, argp, sizeof(var)))
                      return -EFAULT;
              console_lock();
              if (!lock_fb_info(info)) {
                      console_unlock();
                      return -ENODEV;
              }
              info->flags |= FBINFO_MISC_USEREVENT;
              ret = fb_set_var(info, &var);
              info->flags &= ~FBINFO_MISC_USEREVENT;
              unlock_fb_info(info);
              console_unlock();
              if (!ret && copy_to_user(argp, &var, sizeof(var)))
                      ret = -EFAULT;
              break;
      case FBIOGET_FSCREENINFO:
              if (!lock_fb_info(info))
                      return -ENODEV;
              fix = info->fix;
              unlock_fb_info(info);

              ret = copy_to_user(argp, &fix, sizeof(fix)) ? -EFAULT : 0;
              break;

      ...
}

以上是Framebuffer应用编程时用到的几个常用的命令:

  • FBIOGET_VSCREENINFO:获取可变参数fb_var_screeninfo结构体;

  • FBIOPUT_VSCREENINFO:设置可变参数fb_var_screeninfo结构体;

  • FBIOGET_FSCREENINFO:获取固定参数fb_fix_screeninfo结构体。

1.2. 硬件设备层分析

在我们编写具体的LCD驱动程序时,我们只需要关心硬件设备层,在硬件设备层根据具体的单板和LCD屏幕 参数编写对应驱动程序。每一个LCD驱动 对应一个fb_info结构体 ,编写驱动程序时需要把它注册到核心层 的registered_fb数组,以便内核管理。

1.2.1. fb_info 结构体

fb_info 结构体 (位于 内核源码/include/linux/fb.h):

cpp 复制代码
struct fb_info {
      atomic_t count;
      int node;
      int flags;
      /*
       * -1 by default, set to a FB_ROTATE_* value by the driver, if it knows
       * a lcd is not mounted upright and fbcon should rotate to compensate.
       */
      int fbcon_rotate_hint;
      struct mutex lock;              /* Lock for open/release/ioctl funcs */
      struct mutex mm_lock;           /* Lock for fb_mmap and smem_* fields */
      struct fb_var_screeninfo var;   /* Current var */
      struct fb_fix_screeninfo fix;   /* Current fix */
      struct fb_monspecs monspecs;    /* Current Monitor specs */
      struct work_struct queue;       /* Framebuffer event queue */
      struct fb_pixmap pixmap;        /* Image hardware mapper */
      struct fb_pixmap sprite;        /* Cursor hardware mapper */
      struct fb_cmap cmap;            /* Current cmap */
      struct list_head modelist;      /* mode list */
      struct fb_videomode *mode;      /* current mode */

#ifdef CONFIG_FB_BACKLIGHT
      /* 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
#ifdef CONFIG_FB_DEFERRED_IO
      struct delayed_work deferred_work;
      struct fb_deferred_io *fbdefio;
#endif

      struct fb_ops *fbops;
      struct device *device;          /* This is the parent */
      struct device *dev;             /* This is this fb device */
      int class_flag;                    /* private sysfs flags */
#ifdef CONFIG_FB_TILEBLITTING
      struct fb_tile_ops *tileops;    /* Tile Blitting */
#endif
      union {
              char __iomem *screen_base;      /* Virtual address */
              char *screen_buffer;
      };
      unsigned long screen_size;      /* Amount of ioremapped VRAM or 0 */
      void *pseudo_palette;           /* Fake palette of 16 colors */
#define FBINFO_STATE_RUNNING  0
#define FBINFO_STATE_SUSPENDED        1
      u32 state;                      /* Hardware state i.e suspend */
      void *fbcon_par;                /* fbcon use-only private area */
      /* From here on everything is device dependent */
      void *par;
      /* we need the PCI or similar aperture base/size not
         smem_start/size as smem_start may just be an object
         allocated inside the aperture so may not actually overlap */
      struct apertures_struct {
              unsigned int count;
              struct aperture {
                      resource_size_t base;
                      resource_size_t size;
              } ranges[0];
      } *apertures;

      bool skip_vt_switch; /* no VT switch on suspend/resume required */
};
  • var: 用于提供显示设备的可变参数,包括显示设备的分辨率、显示时序和像素格式等硬件信息;

  • fix: 用于提供显示设备的固定参数,包括显示设备的行长度、显存大小、显存物理基地址等信息;

  • screen_base:用于提供显示设备的显存虚拟的基地址;

  • screen_szie:保存LCD显存的大小;

  • fbops:显示设备的私有文件操作接口。

1.2.2. register_framebuffer() 函数

register_framebuffer() 函数 (位于 内核源码/drivers/video/fbdev/core/fbmem.c):

cpp 复制代码
int register_framebuffer(struct fb_info *fb_info)
{
  int ret;

  mutex_lock(&registration_lock);
  ret = do_register_framebuffer(fb_info);
  mutex_unlock(&registration_lock);

  return ret;
}

从以上代码可知,真正注册fb_info结构体的函数是do_register_framebuffer(),该函数代码如下:

do_register_framebuffer() 函数 (位于 内核源码/drivers/video/fbdev/core/fbmem.c):

cpp 复制代码
static int do_register_framebuffer(struct fb_info *fb_info)
{
  ...
  for (i = 0 ; i < FB_MAX; i++)
    if (!registered_fb[i])
      break;
  fb_info->node = i;

  ...

  fb_info->dev = device_create(fb_class, fb_info->device,
             MKDEV(FB_MAJOR, i), NULL, "fb%d", i);
  ...
  registered_fb[i] = fb_info;
  ...
}
  • 第4-6行:找出registered_fb数组中空闲的元素的下标;

  • 第11-12行:以registered_fb数组空闲元素的下标作为LCD设备的次设备号,在/dev/目录下创建fbi(i为registered_fb数组空闲元素的下标)设备文件;

  • 第14行:把LCD硬件设备对应的fb_info结构体存入registered_fb数组。registered_fb数组如下:

registered_fb 数组 (位于 内核源码/drivers/video/fbdev/core/fbmem.c):

cpp 复制代码
struct fb_info *registered_fb[FB_MAX]

FB_MAX 宏 (位于 内核源码/include/uapi/linux/fb.h):

cpp 复制代码
#define FB_MAX 32      /* sufficient for now */

2. Framebuffer子系统实验

本章配套源码和设备树插件位于 ~/linux_driver/fb_sub_system 目录下。

LCD的硬件原理可参考 eLCDIF---液晶显示 章节。

2.1. 设备树插件实现

LCD配置参数主要包括LCD引脚相关配置和LCD显示参数配置。 其中背光控制引脚是独立出来的,修改背光引脚的同时也要修改对应的PWM设备信息。

2.1.1. LCD引脚配置

这里我们使用pinctrl子系统配置LCD接口和背光控制引脚,对应的设备树引脚配置如下:

lcd引脚配置 (位于 linux_driver/fb_sub_system/imx-fire-lcd5-no-ts-overlay.dts):

cpp 复制代码
pinctrl_lcdif_ctrl: lcdifctrlgrp {
    fsl,pins = <
        MX6UL_PAD_LCD_CLK__LCDIF_CLK        0x79
        MX6UL_PAD_LCD_ENABLE__LCDIF_ENABLE  0x79
        MX6UL_PAD_LCD_HSYNC__LCDIF_HSYNC    0x79
        MX6UL_PAD_LCD_VSYNC__LCDIF_VSYNC    0x79
    >;
};
pinctrl_lcdif_dat: lcdifdatgrp {
    fsl,pins = <
        MX6UL_PAD_LCD_DATA00__LCDIF_DATA00  0x79
        MX6UL_PAD_LCD_DATA01__LCDIF_DATA01  0x79
        MX6UL_PAD_LCD_DATA02__LCDIF_DATA02  0x79
        MX6UL_PAD_LCD_DATA03__LCDIF_DATA03  0x79
        MX6UL_PAD_LCD_DATA04__LCDIF_DATA04  0x79
        MX6UL_PAD_LCD_DATA05__LCDIF_DATA05  0x79
        MX6UL_PAD_LCD_DATA06__LCDIF_DATA06  0x79
        MX6UL_PAD_LCD_DATA07__LCDIF_DATA07  0x79
        MX6UL_PAD_LCD_DATA08__LCDIF_DATA08  0x79
        MX6UL_PAD_LCD_DATA09__LCDIF_DATA09  0x79
        MX6UL_PAD_LCD_DATA10__LCDIF_DATA10  0x79
        MX6UL_PAD_LCD_DATA11__LCDIF_DATA11  0x79
        MX6UL_PAD_LCD_DATA12__LCDIF_DATA12  0x79
        MX6UL_PAD_LCD_DATA13__LCDIF_DATA13  0x79
        MX6UL_PAD_LCD_DATA14__LCDIF_DATA14  0x79
        MX6UL_PAD_LCD_DATA15__LCDIF_DATA15  0x79
        MX6UL_PAD_LCD_DATA16__LCDIF_DATA16  0x79
        MX6UL_PAD_LCD_DATA17__LCDIF_DATA17  0x79
        MX6UL_PAD_LCD_DATA18__LCDIF_DATA18  0x79
        MX6UL_PAD_LCD_DATA19__LCDIF_DATA19  0x79
        MX6UL_PAD_LCD_DATA20__LCDIF_DATA20  0x79
        MX6UL_PAD_LCD_DATA21__LCDIF_DATA21  0x79
        MX6UL_PAD_LCD_DATA22__LCDIF_DATA22  0x79
        MX6UL_PAD_LCD_DATA23__LCDIF_DATA23  0x79
    >;
};


pinctrl_pwm1: pwm1grp {
    fsl,pins = <
            MX6UL_PAD_GPIO1_IO08__PWM1_OUT 0x110b0
    >;
};

如果修改了LCD显示和背光的引脚,需要修改以上代码的引脚。

2.1.2. LCD背光设置

lcd背光pwm设置 (位于 linux_driver/fb_sub_system/imx-fire-lcd5-no-ts-overlay.dts):

cpp 复制代码
backlight {
    compatible = "pwm-backlight";
    pwms = <&pwm1 0 5000000>;
    brightness-levels = <0 4 8 16 32 64 128 255>;
    default-brightness-level = <6>;
    status = "okay";
};

背光引脚被复用为PWM1的输出,如果修改了背光引脚也要在这里修改使用的pwm。

2.1.3. LCD属性设置

当我们需用到不同的LCD时,由于不同的LCD的配置参数不同,例如分辨率、时钟、无效行数等。以下是本实验LCD的配置参数:

lcd配置参数 (位于 linux_driver/fb_sub_system/imx-fire-lcd5-no-ts-overlay.dts):

cpp 复制代码
/*-------第一组---------*/
clock-frequency = <27000000>;
hactive = <800>;
vactive = <480>;

/*-------第二组---------*/
hfront-porch = <23>;
hback-porch = <46>;
vback-porch = <22>;
vfront-porch = <22>;

/*-------第三组---------*/
hsync-len = <1>;
vsync-len = <1>;

/*-------第四组---------*/
hsync-active = <0>;
vsync-active = <0>;
de-active = <1>;
pixelclk-active = <0>;

配置参数可分为四组:

  • 第一组是设置分辨率和时钟。

  • 第二组设置"可视区域",它们的缩写就是我们常说的HFP、hbp、vbp、vfp、行同步信号到第一个像素点的延时时间,单位(像素), 一行的最后一个像素点到下一个行同步信号的延时时间(单位像素),帧同步信号到第一个有效行之间的时间,最后一行到下一个 帧同步信号 之间的时间。

  • 第三组,设置行同步信号和帧同步信号的脉宽。

  • 第四组,设置行同步信号、帧同步信号、数据信号、像素时钟信号的极性。

以上内容要根据自己使用的显示屏说明文档配置。

本实验的使用野火的5.0寸800x480RGB电容触摸屏, 设备树插件完整代码位于~/linux_driver/fb_sub_system/imx-fire-lcd5-no-ts-overlay.dts。 若想使用内核自带的LCD驱动(位于 内核源码/drivers/video/fbdev/mxsfb.c),请自行编译 ~/linux_driver/fb_sub_system/imx-fire-lcd5-no-touchscreen-overlay.dts设备树插件并拷贝到开发板测试。

2.2. 驱动程序实现

编程思路:

  1. 分配fb_info结构体;

  2. 设置fb_info结构体;

  3. 向核心层注册fb_info结构体;

  4. 根据LCD的相关参数设置LCD控制器。

注: LCD接口相关的引脚配置,在驱动加载时pinctrl子系统会根据设备树自动把它配置为LCD相关的引脚功能, 在我们的驱动程序里无需配置。其次,LCD背光部分这里我们使用内核自带的背光驱动程序(位于 内核源码/drivers/video/backlight/pwm_bl.c)。 LCD驱动和背光驱动是两个独立的驱动模块,关于背光模块是如何在LCD模块显示图像时及时打开背光的问题,感兴趣的可以查阅"Linux内核事件通知链"相关的资料。

2.2.1. 驱动入口和出口函数实现

本实验的驱动程序代码是基于平台设备驱动编写的,驱动入口和出口函数仅用于平台驱动的注册和注销,代码如下:

LCD 驱动入口和出口函数 (位于 linux_driver/fb_sub_system/fb_sub_system.c):

cpp 复制代码
 static struct of_device_id   lcd_of_match[] = {
      {.compatible = "fire,lcd_drv",},
      {},
 };

 static struct platform_driver lcd_driver = {
      .probe  = lcd_driver_probe,
      .remove = lcd_driver_remove,
      .driver = {
              .name = "lcd_drv",
              .of_match_table = lcd_of_match,
      },
 };

 static int __init lcd_driver_init(void)
 {
      return platform_driver_register(&lcd_driver);
 }

 static void __exit lcd_driver_exit(void)
 {
      platform_driver_unregister(&lcd_driver);
 }

 module_init(lcd_driver_init);
 module_exit(lcd_driver_exit);
 MODULE_LICENSE("GPL");
  • 第1~4行:定义LCD设备树匹配表。

  • 第6~13行:定义LCD平台驱动结构体。

  • 第7~8行:在驱动加载注册平台驱动时会与设备树进行匹配,若匹配成功则会执行.probe函数;在驱动卸载注销平台驱动时.remove函数会被执行; 我们可以在.probe函数实现一些初始化的工作,在.remove函数实现一些清理工作。

  • 第11行:.of_match_table 用于和设备树节点匹配。

  • 第15~18行:在驱动程序的入口函数注册平台驱动。

  • 第20~23行:在驱动程序的出口函数注销平台驱动。

  • 第25行:把lcd_driver_init修饰为驱动的入口函数。

  • 第26行:把lcd_driver_exit修饰为驱动的出口函数。

2.2.2. .prob函数实现

lcd_driver_probe() 函数 (位于 linux_driver/fb_sub_system/fb_sub_system.c):

cpp 复制代码
static int lcd_driver_probe(struct platform_device *pdev)
{
  struct device_node *display_np;
  struct display_timings *timings = NULL;
  struct display_timing  *dt = NULL;
  struct resource *res = NULL;
  unsigned int bits_per_pixel;
  unsigned int bus_width;

  res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
  elcdif = devm_ioremap_resource(&pdev->dev, res);

  display_np = of_parse_phandle(pdev->dev.of_node, "display", 0);
  timings = of_get_display_timings(display_np);
  dt = timings->timings[timings->native_mode];

  of_property_read_u32(display_np, "bits-per-pixel", &bits_per_pixel);
  if (bits_per_pixel != 16)
  {
    printk(KERN_EMERG"not support %d bpp!\n", bits_per_pixel);
    return -1;
  }

  of_property_read_u32(display_np, "bus-width", &bus_width);

  clk_pix = devm_clk_get(&pdev->dev, "pix");
  clk_axi = devm_clk_get(&pdev->dev, "axi");

  clk_set_rate(clk_pix, dt->pixelclock.typ);

  clk_prepare_enable(clk_pix);
  clk_prepare_enable(clk_axi);

  /* 分配一个fb_info结构体 */
  lcdfb_info = framebuffer_alloc(0, &pdev->dev);

  /* LCD屏幕参数设置 */
  lcdfb_info->var.xres   = dt->hactive.typ;
  lcdfb_info->var.yres   = dt->vactive.typ;
  lcdfb_info->var.width  = dt->hactive.typ;
  lcdfb_info->var.height = dt->vactive.typ;
  lcdfb_info->var.xres_virtual = dt->hactive.typ;
  lcdfb_info->var.yres_virtual = dt->vactive.typ;
  lcdfb_info->var.bits_per_pixel = bits_per_pixel;

    /* LCD信号时序设置 */
  lcdfb_info->var.pixclock  = dt->pixelclock.typ;
  lcdfb_info->var.left_margin  = dt->hback_porch.typ;
  lcdfb_info->var.right_margin = dt->hfront_porch.typ;
  lcdfb_info->var.upper_margin = dt->vback_porch.typ;
  lcdfb_info->var.lower_margin = dt->vfront_porch.typ;
  lcdfb_info->var.vsync_len = dt->vsync_len.typ;
  lcdfb_info->var.hsync_len = dt->hsync_len.typ;

  /* LCD RGB格式设置, 这里使用的是RGB565 */
  lcdfb_info->var.red.offset   = 11;
  lcdfb_info->var.red.length   = 5;
  lcdfb_info->var.green.offset = 5;
  lcdfb_info->var.green.length = 6;
  lcdfb_info->var.blue.offset  = 0;
  lcdfb_info->var.blue.length  = 5;

  /* 设置固定参数 */
  strcpy(lcdfb_info->fix.id, "fire,lcd");
  lcdfb_info->fix.type   = FB_TYPE_PACKED_PIXELS;
  lcdfb_info->fix.visual = FB_VISUAL_TRUECOLOR;
  lcdfb_info->fix.line_length = dt->hactive.typ * bits_per_pixel / 8;
  lcdfb_info->fix.smem_len    = dt->hactive.typ * dt->vactive.typ * bits_per_pixel / 8;

  /* 其他参数设置 */
  lcdfb_info->screen_size = dt->hactive.typ * dt->vactive.typ * bits_per_pixel / 8;

  /* dma_alloc_writecombine:分配smem_len大小的内存,返回screen_base虚拟地址,对应的物理地址保存在smem_start */
  lcdfb_info->screen_base = dma_alloc_writecombine(&pdev->dev, lcdfb_info->fix.smem_len, (dma_addr_t*)&lcdfb_info->fix.smem_start, GFP_KERNEL);
  lcdfb_info->pseudo_palette = pseudo_palette;
  lcdfb_info->fbops = &lcdfb_ops;

    /* elcdif控制器硬件初始化 */
  imx6ull_elcdif_init(elcdif, lcdfb_info, dt, bus_width);
  imx6ull_elcdif_enable(elcdif);

  /* 注册fb_info结构体 */
  register_framebuffer(lcdfb_info);

  printk(KERN_EMERG"match success!\n");
  return 0;
}
  • 第10~11行:获取节点的内存资源,即获取设备树节点的reg属性的信息,该属性包含了elcdif控制器的寄存器的基地址和寄存器空间的大小, 并进行物理地址的映射,返回供我们我们操作寄存器的虚拟地址。

  • 第13~15行:根据句柄的名称display获取对应的display0设备节点,然后进一步解析display0的子节点display-timings得到LCD屏的时序参数信息并保存到display_timing结构体变量dt里。

  • 第17~24行:从设备树中获取lcd的bits-per-pixel参数和总线宽度bus-width,需要注意的是本驱动程序只支持16bpp像素格式。

  • 第26~32行:从设备树获"pix"和"axi"时钟,并使能它。"pix"是像素时钟,需要根据具体的LCD屏参数设置。

  • 第35行:分配一个fb_info结构体。

  • 第38~76行:填充fb_info结构体的var、fix、screen_base、screen_size等硬件信息。

  • 第79~80行:初始化elcdif控制器,并使能它。

  • 第83行:向核心层注册fb_info结构体。

2.2.3. .remove函数实现

lcd_driver_remove() 函数 (位于 linux_driver/fb_sub_system/fb_sub_system.c):

cpp 复制代码
static int lcd_driver_remove(struct platform_device *pdev)
{
  unregister_framebuffer(lcdfb_info);
  imx6ull_elcdif_disable(elcdif);
  framebuffer_release(lcdfb_info);

  printk(KERN_EMERG"module exit!\n");
  return 0;
}

.remove 函数工作是注销、释放fb_info结构体,并且失能LCD控制器。

2.2.4. LCD控制器硬件操作

LCD控制器的寄存器操作与裸机是一样的,详细的内容可以阅读裸机 eLCDIF---液晶显示 章节。 代码如下:

LCD控制器硬件操作 (位于 linux_driver/fb_sub_system/fb_sub_system.c):

cpp 复制代码
static void imx6ull_elcdif_enable(struct imx6ull_elcdif *elcdif)
{
  elcdif->CTRL |= (1<<0);
}

static void imx6ull_elcdif_disable(struct imx6ull_elcdif *elcdif)
{
  elcdif->CTRL &= ~(1<<0);
}

static int imx6ull_elcdif_init(struct imx6ull_elcdif *elcdif, struct fb_info *info, struct display_timing  *dt, unsigned int bus_width)
{
  unsigned int input_data_format;
  unsigned int data_bus_width;
  unsigned int hsync_active = 0;
  unsigned int vsync_active = 0;
  unsigned int de_active = 0;
  unsigned int pixelclk_active = 0;

  /* elcdif正常运行bit31、bit30  必须设置为0         */
  elcdif->CTRL &= ~((1 << 31) | (1 << 30));

  if(info->var.bits_per_pixel == 16)
    input_data_format = 0x0;
  else if (info->var.bits_per_pixel == 8)
    input_data_format = 0x1;
  else if (info->var.bits_per_pixel == 18)
    input_data_format = 0x2;
  else if (info->var.bits_per_pixel == 24)
    input_data_format = 0x3;
  else
  {
    printk(KERN_EMERG"Don't support %d bpp\n", info->var.bits_per_pixel);
    return -1;
  }

  if (bus_width == 16)
    data_bus_width = 0x0;
  else if (bus_width == 8)
    data_bus_width = 0x01;
  else if (bus_width == 18)
    data_bus_width = 0x02;
  else if (bus_width == 24)
    data_bus_width = 0x03;
  else
  {
    printk(KERN_EMERG"Don't support %d bit data bus mode\n", info->var.bits_per_pixel);
    return -1;
  }

  /* 设置RGB格式和数据宽度 */
  elcdif->CTRL &= ~((0x03 << 8) | (0x03 << 10));
  elcdif->CTRL |= ((input_data_format << 8) | (data_bus_width << 10));

  elcdif->CTRL |= (1 << 17); /* 选择 RGB 模式 */
  elcdif->CTRL |= (1 << 19); /* 选择 RGB 模式 开启显示 */
  elcdif->CTRL |= (1 << 5);  /* 设置elcdf接口为主模式 */

  if (info->var.bits_per_pixel == 24 || info->var.bits_per_pixel == 32)
  {
    /* 设置32位有效位的低24位有效 */
    elcdif->CTRL1 &= ~(0xf << 16);
    elcdif->CTRL1 |= (0x7 << 16);
  }
  else
  {
    elcdif->CTRL1 |= (0xf << 16);
  }

  /* 设置LCD分辨率 */
  elcdif->TRANSFER_COUNT &= ~0xffffffff;
  elcdif->TRANSFER_COUNT |= (dt->vactive.typ << 16); /* 设置LCD垂直分辨率 */
  elcdif->TRANSFER_COUNT |= (dt->hactive.typ << 0); /* 设置LCD垂直分辨率 */

  elcdif->VDCTRL0 |= (1 << 28);  /* 生成使能信号 */
  elcdif->VDCTRL0 |= (1 << 21);  /* 设置VSYNC周期 的单位为显示时钟的时钟周期 */
  elcdif->VDCTRL0 |= (1 << 20);  /* 设置VSYNC 脉冲宽度的单位为显示时钟的时钟周期 */

  if (dt->flags & DISPLAY_FLAGS_DE_HIGH)
    de_active = 1;
  if (dt->flags & DISPLAY_FLAGS_PIXDATA_POSEDGE)
    pixelclk_active = 1;
  if (dt->flags & DISPLAY_FLAGS_HSYNC_HIGH)
    hsync_active = 1;
  if (dt->flags & DISPLAY_FLAGS_VSYNC_HIGH)
    vsync_active = 1;

  /* 设置信号极性 */
  elcdif->VDCTRL0 &= ~(0xf << 24);            /* bit24~bit27清零 */
  elcdif->VDCTRL0 |= (de_active << 24);       /* 设置数据使能信号的有效电平 */
  elcdif->VDCTRL0 |= (pixelclk_active << 25); /* 设置时钟信号极性 */
  elcdif->VDCTRL0 |= (hsync_active << 26);    /* 设置HSYNC有效电平 */
  elcdif->VDCTRL0 |= (vsync_active << 27);    /* 设置VSYNC有效电平 */

  elcdif->VDCTRL0 |= (dt->vsync_len.typ << 0);/* 设置vysnc脉冲宽度 */

  /* 设置VSYNC信号周期 */
  elcdif->VDCTRL1 = dt->vsync_len.typ + dt->vactive.typ + dt->vfront_porch.typ + dt->vback_porch.typ;

  elcdif->VDCTRL2 |= (dt->hsync_len.typ << 18); /* 设置hysnc脉冲宽度 */
  /*设置HSYNC信号周期 */
  elcdif->VDCTRL2 |= (dt->hfront_porch.typ + dt->hback_porch.typ + dt->hactive.typ + dt->hsync_len.typ);

  elcdif->VDCTRL3 |= ((dt->hback_porch.typ + dt->hsync_len.typ) << 16);
  elcdif->VDCTRL3 |= (dt->vback_porch.typ + dt->vsync_len.typ);

  elcdif->VDCTRL4 |= (1 << 18);
  elcdif->VDCTRL4 |= (dt->hactive.typ << 0);

  /* 设置显存基地址 */
  elcdif->CUR_BUF  = info->fix.smem_start;
  elcdif->NEXT_BUF = info->fix.smem_start;

  return 0;
}

以上代码是参照裸机章节编写的,详细的介绍请阅读 eLCDIF---液晶显示 章节。

2.3. 测试应用程序实现

这里编写一个简单的应用程序用于测试驱动是否正常,主要实现的功能是分别向显示器刷红、绿、蓝三种颜色,最后在显示屏显示 一副图片。

测试应用程序 (位于 linux_driver/fb_sub_system/test_app/test_app.c):

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include "test_app.h"
#include <sys/ioctl.h>

/*显示屏相关头文件*/
#include <linux/fb.h>
#include <sys/mman.h>

extern unsigned int test_picture[];

void lcd_show_pixel(unsigned char *fbmem, int line_length, int bpp,
            int x, int y, unsigned int color)
{
  unsigned char  *p8  = (unsigned char *)(fbmem + y * line_length + x * bpp / 8);
  unsigned short *p16 = (unsigned short *)p8;
  unsigned int   *p32 = (unsigned int *)p8;

  unsigned int red, green, blue;

  switch(bpp)
  {
    case 8:
    {
      *p8 = (unsigned char)(color & 0xf);
      break;
    }

    case 16:
    {
      red   = (color >> 16) & 0xff;
      green = (color >> 8) & 0xff;
      blue  = (color >> 0) & 0xff;
      *p16  = (unsigned short)(((red >> 3) << 11) | ((green >> 2) <<5) | ((blue >> 3) << 0));
      break;
    }
    case 32:
    {
      *p32 = color;
      break;
    }
    default:
    {
      printf("can not support %d bpp", bpp);
      break;
    }
  }
}


int main()
{
  struct fb_var_screeninfo vinfo;
  struct fb_fix_screeninfo finfo;
  unsigned int *temp;
  unsigned int color;
  unsigned char *fbp = 0;
  int fp = 0;
  int x, y;

  fp = open("/dev/fb0", O_RDWR);
  if (fp < 0)
  {
      printf("Error : Can not open framebuffer device/n");
      exit(1);
  }

  if (ioctl(fp, FBIOGET_FSCREENINFO, &finfo))
  {
      printf("Error reading fixed information/n");
      exit(2);
  }

  if (ioctl(fp, FBIOGET_VSCREENINFO, &vinfo))
  {
      printf("Error reading variable information/n");
      exit(3);
  }
  printf("The mem is :%d\n", finfo.smem_len);
  printf("The line_length is :%d\n", finfo.line_length);
  printf("The xres is :%d\n", vinfo.xres);
  printf("The yres is :%d\n", vinfo.yres);
  printf("bits_per_pixel is :%d\n", vinfo.bits_per_pixel);

   /*这就是把fp所指的文件中从开始到screensize大小的内容给映射出来,得到一个指向这块空间的指针*/
  fbp =(unsigned char *) mmap (0, finfo.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fp,0);
  if ((int) fbp == -1)
  {
     printf ("Error: failed to map framebuffer device to memory./n");
     exit (4);
  }

  /*刷红色*/
  color = 0xff << 16;
  for (y = 0; y < vinfo.yres; y++)
  {
    for(x = 0; x < vinfo.xres; x++)
      lcd_show_pixel(fbp, finfo.line_length, vinfo.bits_per_pixel, x, y, color);
  }
    usleep(1000*2000);

  /*刷绿色*/
  color = 0xff << 8;
  for (y = 0; y < vinfo.yres; y++)
  {
    for(x = 0; x < vinfo.xres; x++)
      lcd_show_pixel(fbp, finfo.line_length, vinfo.bits_per_pixel, x, y, color);
  }
    usleep(1000*2000);

  /*刷蓝色*/
  color = 0xff;
  for (y = 0; y < vinfo.yres; y++)
  {
    for(x = 0; x < vinfo.xres; x++)
      lcd_show_pixel(fbp, finfo.line_length, vinfo.bits_per_pixel, x, y, color);
  }
  usleep(1000*2000);

  /*显示图片*/
  temp = (unsigned int *)test_picture;
  for (y = 0; y < vinfo.yres; y++)
  {
    for(x = 0; x < vinfo.xres; x++)
    {
      lcd_show_pixel(fbp, finfo.line_length, vinfo.bits_per_pixel, x, y, *temp);
      temp++;
    }
  }

  munmap (fbp, finfo.smem_len); /*解除映射*/
  close(fp);
  return 0;
}
  • 第15~51行:根据不同的像素格式在lcd的x、y坐标显示一个像素。

  • 第71行:通过ioctl函数传入FBIOGET_FSCREENINFO命令从内核的fb_info结构体获取LCD的固定参数fix;

  • 第77行:通过ioctl函数传入FBIOGET_VSCREENINFO命令从内核的fb_info结构体获取LCD的可变参数var;

  • 第88行:利用mmap映射一块finfo.smem_len大小的内存空间,并返回指向这块内存空间的基地址(虚拟地址)。

  • 第97~121行:分别向LCD刷屏红、绿、蓝三种颜色。

  • 第124~132行:在LCD显示一副图片。

  • 第134行:解除framebuffer的内存映射。

2.4. 实验准备

下面我们就以野火的5.0寸RGB屏幕为例,为大家测试在framebuffer驱动框架下的驱动效果,我们进行屏幕的测试。

2.4.1. 添加设备树插件

方法参考如下:

本实验中,在鲁班猫系统中默认使能了 LCD 的设备功能,取消 LCD 设备树插件,以释放系统对应LCD资源,操作如下:

bash 复制代码
dtoverlay=/usr/lib/linux-image-4.19.35-imx6/overlays/imx-fire-lcd5-no-ts.dtbo

如若运行代码时出现"Device or resource busy"或者运行代码卡死等等现象, 请按上述情况检查并按上述步骤操作。

如出现 Permission denied 或类似字样,请注意用户权限,大部分操作硬件外设的功能,几乎都需要root用户权限,简单的解决方案是在执行语句前加入sudo或以root用户运行程序。

2.4.2. 编译设备树插件

linux_driver/fb_sub_system/imx-fire-lcd5-no-ts-overlay.dts 拷贝到 内核源码/arch/arm/boot/dts/overlays 目录下, 并修改同级目录下的Makefile,追加 imx-fire-lcd5-no-ts.dtbo 编译选项。然后执行如下命令编译设备树插件:

bash 复制代码
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- npi_v7_defconfig

make ARCH=arm -j4 CROSS_COMPILE=arm-linux-gnueabihf- dtbs

编译成功后生成同名的设备树插件文件(imx-fire-lcd5-no-ts.dtbo)位于 内核源码/arch/arm/boot/dts/overlays 目录下。

2.4.3. 编译驱动程序

将 linux_driver/fb_sub_system 拷贝到内核源码同级目录,执行里面的MakeFile,生成fb_sub_system.ko。

2.4.4. 编译应用程序

在 linux_driver/fb_sub_system/test_app 目录中执行里面的MakeFile,生成test_app。

2.5. 下载验证

将前面生成的设备树插件、驱动程序、应用程序通过scp等方式拷贝到开发板。

2.5.1. 加载设备树插件和驱动文件

将设备树插件拷贝到开发板 /usr/lib/linux-image-4.19.35-imx6/overlays/ 目录下,并且在/boot/uEnv.txt中添加 dtoverlay=/usr/lib/linux-image-4.19.35-imx6/overlays/imx-fire-lcd5-no-ts.dtbo ,然后 sudo reboot 重启开发板。

加载驱动程序 insmod fb_sub_system.ko ,驱动程序加载成功打印match successed。

2.5.2. 测试效果

驱动加载成功后直接运行测试应用程序 ./test_app ,正常情况下LCD会先后分别刷屏红色、绿色、蓝色,最后显示一幅图片。

相关推荐
嵌入(师)2 小时前
嵌入式驱动开发详解6(RTC)
驱动开发·实时音视频
嵌入(师)8 小时前
嵌入式驱动开发详解15(电容触摸屏gt9147)
驱动开发
嵌入式大圣8 小时前
嵌入式电机驱动开发
驱动开发·单片机·嵌入式硬件
@嵌入式Linux小白8 小时前
Linux 支持多个spi-nor flash
linux·驱动开发
lishing68 小时前
Linux驱动开发(13):输入子系统–按键输入实验
linux·运维·驱动开发
嵌入(师)20 小时前
嵌入式驱动开发详解16(音频驱动开发)
驱动开发·音视频
三菱-Liu2 天前
三菱Q系列PLC系统配置
运维·驱动开发·嵌入式硬件·制造
少年、潜行2 天前
树莓派3B+驱动开发(2)- LED驱动(传统模式)
驱动开发·树莓派·3b+
lishing62 天前
Linux驱动开发(12):中断子系统–按键中断实验
驱动开发