同步与互斥

  • atomic,适用于单个变量的操作,不可分割,无锁操作。
  • spinlock, 忙等,一直占用 cpu, 只能由于非常短的临界区,比较适合多核系统。
  • semaphore, 用于多个线程访问临界资源,阻塞睡眠。
  • mutex, 只有一个线程访问临界资源,阻塞睡眠,比 spinlock 更加适合单核系统。

LCD驱动

硬件原理

  • 像素表示: 24位的 RGB888, 16位的 RGB565 和 RGB555。
  • MIPI-DBI, 8080接口:GRAM一般用 SRAM 和 lcd 控制器 与屏幕,封装在一起,是 LCM.
  • MIPI-DPI, TFT RGB 接口: GRAM 一般用可能是外接内存芯片比如 DDR 和 SDRAM,LCD 控制器在 ARM 芯片内部,屏幕就是单纯的屏幕。

Framebuffer驱动程序框架

module_init(fbmem_init) -> fbmem_init -> ret = register_chrdev(FB_MAJOR, "fb", &fb_fops); -> static const struct file_operations fb_fops

fb_open

static int fb_open(struct inode *inode, struct file *file) __acquires(&info->lock) __releases(&info->lock)

  • __acquires(&info->lock) __releases(&info->lock) 用于注解本函数在使用中会获取和释放锁,同样的还有 __must_hold(&lock) 这样的表示调用函数时必须已经持有锁。 这种注释可以用静态代码分析工具(如 Sparse)来检查。
  • 获取 info,获取时,使用 atomic_inc(&fb_info->count) 增加资源使用量,释放时,使用 atomic_dec_and_test(&fb_info->count) 减少资源使用量,当资源使用量为 0 的时候,返回 true,就可以 destory 资源。
    int fbidx = iminor(inode);
    struct fb_info *info;
    info = get_fb_info(fbidx);
    // static struct fb_info *get_fb_info(unsigned int idx)
    mutex_lock(&registration_lock);
    fb_info = registered_fb[idx];
    if (fb_info)
        atomic_inc(&fb_info->count);
    mutex_unlock(&registration_lock);
    //
    static void put_fb_info(struct fb_info *fb_info)
    {
    if (!atomic_dec_and_test(&fb_info->count))
        return;
    if (fb_info->fbops->fb_destroy)
        fb_info->fbops->fb_destroy(fb_info);
    }
  • open
    if (info->fbops->fb_open) {
        res = info->fbops->fb_open(info,1);
        if (res)
            module_put(info->fbops->owner);
    }

fb_write

static ssize_t fb_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)

  • 获取 info struct fb_info *info = file_fb_info(file);
  • write, 先调用 fb_write,然后使用 copy_from_user 从用户空间的 buf 中复制内容到 buffer 中,再使用 fb_memcpy_tofb 把 buffer 中的数据复制到 info 指向的显存地址中去。
    if (info->fbops->fb_write)
        return info->fbops->fb_write(info, buf, count, ppos);
    // ...
    buffer = kmalloc((count > PAGE_SIZE) ? PAGE_SIZE : count,
             GFP_KERNEL);
    // ...
    dst = (u8 __iomem *) (info->screen_base + p);
    // ...
    while (count) {
        c = (count > PAGE_SIZE) ? PAGE_SIZE : count;
        src = buffer;
        if (copy_from_user(src, buf, c)) {
            err = -EFAULT;
            break;
        }
        fb_memcpy_tofb(dst, src, c);
    // ...
    }

fb_compat_ioctl

static long fb_compat_ioctl(struct file *file, unsigned int cmd, unsigned long arg)

fb_compat_ioctl -> ret = do_fb_ioctl(info, cmd, arg); -> var = info->var; ret = copy_to_user(argp, &var, sizeof(var)) ? -EFAULT : 0;

总结

  • 可以看出来,open, ioctl 之类的,都需要用到 info, 而 info->varstruct fb_var_screeninfo 其中包含了分辨率,rgb 等多种信息。
  • write 中,用到 info->fbops->fb_write,对应的是 struct fb_info -> struct fb_ops *fbops -> fb_write. 其他 open 之类的也类似。

驱动框架

前面 open, write 等当中,使用的都是 fb_info,主要调用的就是 struct fb_var_screeninfo var;struct fb_ops *fbops;, 一个是屏幕信息,一个是操作函数。

  • 查找 fb_info 的来源,fb_write -> file_fb_info -> struct fb_info *info = registered_fb[fbidx]; 说明来源于 registered_fb 这个数组。
  • 查找写入 registered_fb 的地方
    • 找到 struct fb_info *registered_fb[FB_MAX] __read_mostly; __read_mostly; 用于经常读取很少写入,会被加载到 cpu 缓存中;
    • registered_fbstatic int do_register_framebuffer(struct fb_info *fb_info) 中被写入,但是这个是 static 函数,还要继续找调用这个函数的地方。
    • 找到 do_register_framebufferint register_framebuffer(struct fb_info *fb_info) 调用,说明其他文件中,需要调用的是 register_framebuffer 这个函数,来存入 fb_info 。
  • 搜索其他使用 register_framebuffer 的地方,发现确实很多驱动都是调用这个函数的。

s3c2410fb.c

  • s3c2410fb_init -> int ret = platform_driver_register(&s3c2410fb_driver);
  • struct platform_driver s3c2410fb_driver -> .probe = s3c2410fb_probe, -> return s3c24xxfb_probe(pdev, DRV_S3C2410);
  • static int s3c24xxfb_probe(struct platform_device *pdev, enum s3c_drv_type drv_type)
    // alloc 一个 fb_info 和私有结构体 s3c2410fb_info 的空间,s3c2410fb_info 在 fb_info 后面,fb_info->par = s3c2410fb_info 
    fbinfo = framebuffer_alloc(sizeof(struct s3c2410fb_info), &pdev->dev);
    // ...
    // 指针指向 s3c2410fb_info,然后配置 s3c2410fb_info
    info = fbinfo->par;
    info->dev = &pdev->dev;
    info->drv_type = drv_type;
    // ...
    // 配置 fbinfo 的 fix, var 和 fbops
    fbinfo->fix.type        = FB_TYPE_PACKED_PIXELS;
    fbinfo->fix.type_aux        = 0;
    fbinfo->fix.xpanstep        = 0;
    fbinfo->fix.ypanstep        = 0;
    fbinfo->fix.ywrapstep       = 0;
    fbinfo->fix.accel       = FB_ACCEL_NONE;
    fbinfo->var.nonstd      = 0;
    fbinfo->var.activate        = FB_ACTIVATE_NOW;
    fbinfo->var.accel_flags     = 0;
    fbinfo->var.vmode       = FB_VMODE_NONINTERLACED;
    fbinfo->fbops           = &s3c2410fb_ops;
    // ...
    // 部分硬件配置
    s3c2410fb_init_registers(fbinfo);
    // ...
    // 注册 fbinfo
    ret = register_framebuffer(fbinfo);

编写框架

  • init, exit
  • init 中,alloc fb_info, 设置 fb_info, 注册 fb_info.
  • alloc fb_info 可以根据自己的需要,增加需要的私有结构体 size
  • 设置 fb_info,也就是 fix, var, ops
    • var: 分辨率相关的 xres, yres, 像素大小的 bits_per_pixel, 像素在字节中占哪些位的 red green blue.
    • fix: 和分辨率相关的显存大小 smem_len, 显存开始位置 smem_start, FBTYPE type, FBVISUAL visual . 在配置 smem_start 的时候,需要使用 dma_alloc_wc,这个函数返回的是虚拟地址需要放到 fbinfo->screen_base, 通过参数返回的是物理地址,配置给 smem_start .
    • ops: 基本的 read, write 不需要自己实现,需要实现的是: 矩形 fb_fillrect, 区域复制 fb_copyarea, 图片显示 fb_imageblit
  • 注册 fb_info, 使用 register_framebuffer.

基于 QEMU 的 lcd 驱动

按照 qemu 中预设的寄存器,用 ioremap 配置一下即可。

QEMU 上机 lcd

  • 运行 ~/ubuntu-18.04_imx6ul_qemu_system/qemu-imx6ull-gui.sh
  • qemu 的账号是 root
  • 运行 fb-test 启动 lcd 显示测试程序。

配置 qemu 内核编译环境

可以参考官方页面 https://ldd.100ask.net/zh/03_LCD/05.html

  • 下载源码
    book@100ask:~$ mkdir -p 100ask_imx6ull-qemu && cd 100ask_imx6ull-qemu
    book@100ask:~/100ask_imx6ull-qemu$ ../repo/repo init -u https://e.coding.net/weidongshan/manifests.git -b linux-sdk -m  imx6ull/100ask-imx6ull_qemu_release_v1.0.xml --no-repo-verify
    book@100ask:~/100ask_imx6ull-qemu$ ../repo/repo sync -j4
  • 设置工具链
    export ARCH=arm
    export CROSS_COMPILE=arm-linux-gnueabihf-
    export PATH=$PATH:/home/book/100ask_imx6ull-qemu/ToolChain/gcc-linaro-6.2.1-2016.11-x86_64_arm-linux-gnueabihf/bin
  • 配置、编译内核
    book@100ask:~/100ask_imx6ull-qemu$ cd linux-4.9.88
    book@100ask:~/100ask_imx6ull-qemu/linux-4.9.88$ make mrproper
    book@100ask:~/100ask_imx6ull-qemu/linux-4.9.88$ make 100ask_imx6ull_qemu_defconfig
    book@100ask:~/100ask_imx6ull-qemu/linux-4.9.88$ make zImage 
  • 在QEMU中使用新的zImage
    book@100ask:~$ cd ~/ubuntu-18.04_imx6ul_qemu_system/
    book@100ask:~/ubuntu-18.04_imx6ul_qemu_system$ cp ~/100ask_imx6ull-qemu/linux-4.9.88/arch/arm/boot/zImage  imx6ull-system-image/ 
  • 替换LCD驱动程序
    • 只需要把新的驱动源码放到 linux-4.9.88/drivers/video/fbdev 里面,
    • 然后修改 linux-4.9.88/drivers/video/fbdev/Makefile ,注释掉原有的 #obj-y += 100ask_qemu_fb.o, 再增加新的驱动即可。
    • 最后使用 make zImage 编译内核。
  • 启动 qemu 之后,可以通过 uname -a 来查看内核编译的时间,确认是新的内核。

调试

  • 解决基本编译错误
  • 根据调试情况,添加必须的,ops->fb_setcolreg,
    • 并且对应的 s3c2410fb_setcolreg 中需要用到 FB_VISUAL_TRUECOLORu32 *pal = info->pseudo_palette;
    • 搜索 pseudo_palette 发现在 s3c24xxfb_probe 中,fbinfo->pseudo_palette = &info->pseudo_pal;, 这个 pseudo_pal 是私有结构体中的一个成员变量数组
    • 所以,需要新增一个数组变量,并配置给 fbinfo.
  • 根据调试情况,配置 fbinfo 中 var 的,虚拟分辨率xres_virtual, yres_virtual, xoffset, yoffset
  • 根据调试情况,配置 fbinfo 中 fix 的,id, line_length.

结合 app 分析 lcd

  • app 中,open 调用,fbmem 中的 open file->private_data = info;, 把 fbinfo 放入 file 中,但是因为 底层并没有配置 info->fbops->fb_open, 所以不会调用底层 open.
  • app 中,mmap 调用, fbmem 中的 fb_mmap , 会先检查底层有没有配置 info->fbops->fb_mmap, 因为没有配置,所以使用 vm_iomap_memoryinfo->fix.smem_start info->fix.smem_len 映射到用户控制中。

TFT RGB 接口

IMX6ULL 的 dotclk 接口,使用 hsync 从上一行的末尾到下一行的开头,使用 vsync 从最后一行的末尾到第一行的开头。

eLCDIF 寄存器

主要用到的寄存器:

  • LCDIF_CTRL:
    • SFTRST: 复位
    • BYPASS_COUNT, DOTCLK_MODE: dotclk 模式
    • LCD_DATABUS_WIDTH: 总线宽度
    • WORD_LENGTH: 显存中每个像素位数; DATA_FORMAT_16_BIT: 555 还是 565
    • MASTER:bus master
  • LCDIF_CTRL1: BYTE_PACKING_FORMAT 32位中哪些字节有效
  • LCDIF_TRANSFER_COUNT: V_COUNT 行数, H_COUNT 每行像素
  • LCDIF_VDCTRL0:
    • VSYNC_OEB: 不同模式 VSYNC信号设置
    • ENABLE_PRESENT: 硬件产生使能信号
    • VSYNC_POL, HSYNC_POL, DOTCLK_POL, ENABLE_POL: VSYNC, HSYNC, DOTCLK, ENABLE 脉冲信号极性
    • VSYNC_PERIOD_UNIT, VSYNC_PULSE_WIDTH_UNIT:
    • VSYNC_PULSE_WIDTH: VSYNC 脉冲的宽度
  • LCDIF_VDCTRL1, LCDIF_VDCTRL2 : VSYNC, HSYNC 信号宽度和间隔
  • LCDIF_VDCTRL3: HORIZONTAL_WAIT_CNT, VERTICAL_WAIT_CNT 水平和垂直上的等待个数
  • LCDIF_VDCTRL4: SYNC_SIGNALS_ON dotclk 设置位1;DOTCLK_H_VALID_DATA_CNT 水平方向上的像素个数
  • LCDIF_CUR_BUF, LCDIF_NEXT_BUF: 当前帧和下一帧在显存中的地址

分析内核自带驱动

官方放出的源码,编译之后,ls drivers/video/fbdev/*.o 查看编译后的文件,也可以使用 make menuconfig 查看 fb 开启的条目,可以查看对应源码文件中的内容,imx6ull 对应的是 mxsfb.c

相关宏

module_platform_driver

module_platform_driver(mxsfb_driver); module_platform_driver 是 Linux 内核中用于简化平台设备驱动(Platform Driver)注册和注销的一个宏,自动生成入口和出口函数,自动注册和卸载。

MODULE_DEVICE_TABLE

MODULE_DEVICE_TABLE 是 Linux 内核中用于 导出设备表 的宏,它使得内核模块能够与设备匹配,并在加载时自动绑定到相应的设备。根据设备和驱动的匹配方式,MODULE_DEVICE_TABLE 可以用于不同的设备表类型,例如 平台设备 和 设备树。

  • MODULE_DEVICE_TABLE(platform, mxsfb_devtype); static const struct platform_device_id mxsfb_devtype[] 内核能够在加载模块时将其与平台设备匹配, 用于传统的平台设备匹配机制(非设备树)。
  • MODULE_DEVICE_TABLE(of, mxsfb_dt_ids); static const struct of_device_id mxsfb_dt_ids[] 内核能够在加载模块时将其与设备树节点匹配, 用于设备树(Device Tree)匹配机制。 mxsfb_dt_ids->compatible 和 设备树中的 compatible 进行匹配

设备树

  • struct platform_driver mxsfb_drivermxsfb_driver->driver->of_match_table 对应 mxsfb_dt_ids, 查看 const struct of_device_id mxsfb_dt_ids[] 中的 .compatible 这个是和设备树匹配的。
  • 在内核中 arch/arm/boot/dts 可以 grep -nr --include="*.dts" --include="*.dtsi" "fsl,imx28-lcdif" 搜索设备树匹配到的内容。
  • dtsi 中只指定了最基本的一些寄存器信息,具体的单板相关的还要找 dts 文件。 可以通过 grep -nr --include="*.dts" --include="*.dtsi" "include \"imx6ull.dtsi\"" 来查找有哪些文件引用了 dtsi
  • 打开单板 dts 文件后,搜索 &lcdif 来查看具体的 lcd 相关的单板配置。 这个 lcdif 来源于 dtsi 中的 tag.

驱动代码

  • struct platform_driver mxsfb_drivermxsfb_driver->.probe 对应 mxsfb_probe
  • res = platform_get_resource(pdev, IORESOURCE_MEM, 0); 从设备树中,获取资源。IORESOURCE_MEM 指定内存资源。
  • host = devm_kzalloc(&pdev->dev, sizeof(struct mxsfb_info), GFP_KERNEL); 申请 mxsfb 自用的结构体 包含 fbinfo 和 硬件等相关信息。 devm_kzalloc 用于设备资源管理的内存分配函数。是 kzalloc 的增强版,专门为设备驱动设计,能够自动管理分配的内存,初始化为 0,当设备卸载或驱动退出时,内核会自动释放这块内存,避免内存泄漏问题。
  • fb_info = framebuffer_alloc(0, &pdev->dev); 申请 fbinfo 结构体。
  • ret = devm_request_irq(&pdev->dev, irq, mxsfb_irq_handler, 0, dev_name(&pdev->dev), host);, 参数 host 是 mxsfb 自用结构体,devm_request_irq 可以让后续的中断处理函数,使用 host 这个结构体。
  • host->base = devm_ioremap_resource(&pdev->dev, res); devm_ioremap_resource 用于设备资源管理的内存映射函数。作用是将设备的 I/O 内存资源映射到内核的虚拟地址空间,并自动管理映射的生命周期,避免资源泄漏。
  • host->devdata = &mxsfb_devdata[pdev->id_entry->driver_data];, mxsfb_devdata 中的 MXSFB_V3 用了 enum 的默认转为 0 之类的数值。
    enum mxsfb_devtype {
    MXSFB_V3,
    MXSFB_V4,
    MXSFB_V5,
    };
    static const struct mxsfb_devdata mxsfb_devdata[] = {
    [MXSFB_V3] = {
        .transfer_count = LCDC_V3_TRANSFER_COUNT,
        .cur_buf = LCDC_V3_CUR_BUF,
        .next_buf = LCDC_V3_NEXT_BUF,
        .debug0 = LCDC_V3_DEBUG0,
        .hs_wdth_mask = 0xff,
        .hs_wdth_shift = 24,
        .ipversion = 3,
        .flags = MXSFB_FLAG_NULL,
    },
    [MXSFB_V4] = {
        .transfer_count = LCDC_V4_TRANSFER_COUNT,
        .cur_buf = LCDC_V4_CUR_BUF,
        .next_buf = LCDC_V4_NEXT_BUF,
        .debug0 = LCDC_V4_DEBUG0,
        .hs_wdth_mask = 0x3fff,
        .hs_wdth_shift = 18,
        .ipversion = 4,
        .flags = MXSFB_FLAG_BUSFREQ,
    },
    [MXSFB_V5] = {
        .transfer_count = LCDC_V4_TRANSFER_COUNT,
        .cur_buf = LCDC_V4_CUR_BUF,
        .next_buf = LCDC_V4_NEXT_BUF,
        .debug0 = LCDC_V4_DEBUG0,
        .hs_wdth_mask = 0x3fff,
        .hs_wdth_shift = 18,
        .ipversion = 4,
        .flags = MXSFB_FLAG_PMQOS,
    },
    };
    static const struct platform_device_id mxsfb_devtype[] = {
    {
        .name = "imx23-fb",
        .driver_data = MXSFB_V3,
    }, {
        .name = "imx28-fb",
        .driver_data = MXSFB_V4,
    }, {
        .name = "imx7ulp-fb",
        .driver_data = MXSFB_V5,
    }, {
        /* sentinel */
    }
    };
  • host->clk_pix = devm_clk_get(&host->pdev->dev, "pix"); devm_clk_get 用于 设备资源管理 的时钟获取函数。它的作用是从设备中获取时钟信号(clock),并自动管理时钟资源的生命周期,避免资源泄漏。 可以从设备树中,获取对应 "pix" 的数值。
  • host->reg_lcd = devm_regulator_get(&pdev->dev, “lcd”); 用于 设备资源管理 的电压调节器(Regulator)获取函数。它的作用是从设备中获取电压调节器,并自动管理调节器资源的生命周期,避免资源泄漏。 可以从设备树中,获取对应 "lcd" 的数值。
  • pm_runtime_enable(&host->pdev->dev); 用于 运行时电源管理 的函数。它的作用是启用设备的运行时电源管理功能,使得设备可以根据需要自动进入低功耗状态(如挂起)或恢复全功率状态(如唤醒)。
    • probe 函数中启用运行时电源管理。pm_runtime_enable(&pdev->dev);
    • 设置设备为活动状态(唤醒设备)pm_runtime_get_sync(&pdev->dev);
    • 设置设备为空闲状态(允许挂起) pm_runtime_put_sync(&pdev->dev);
    • remove 函数中禁用运行时电源管理。 pm_runtime_disable(&pdev->dev);
    • 挂起设备(进入低功耗状态)pm_runtime_suspend, 恢复设备(退出低功耗状态) pm_runtime_resume
    • pm_runtime_set_active 设置设备为活动状态(忽略引用计数), pm_runtime_set_suspended 设置设备为挂起状态(忽略引用计数)。
  • ret = mxsfb_init_fbinfo(host); 配置 fbinfo
  • ret = mxsfb_init_fbinfo_dt(host); 配置 fbinfo 的数据来自于设备树
  • host->id = of_alias_get_id(np, "lcdif"); of_alias_get_id 用于 从设备树别名(alias)中获取设备 ID 的函数。它的作用是根据设备树中的别名(aliases 节点)获取设备的唯一 ID,通常用于区分多个相同类型的设备。
  • display_np = of_parse_phandle(np, "display", 0); of_parse_phandle 用于 从设备树中解析句柄(phandle) 的函数。它的作用是从设备树中获取一个指向另一个设备节点的引用(即句柄),并返回该节点的 device_node 结构体指针。
  • timings = of_get_display_timings(display_np); of_get_display_timings 用于 从设备树中获取显示时序(display timings) 的函数。它的作用是从设备树中解析显示设备的时序信息 "display-timings",并返回一个 struct display_timings 结构体指针,用于配置显示设备。
  • fb_videomode_to_var(var, &modelist->mode); 用 fbinfo 中的 video 数据来配置 varscreen 数据
  • if (mxsfb_map_videomem(fb_info) < 0) 分配显存。
  • ret = register_framebuffer(fb_info); 注册 fbinfo

硬件相关

  • lcdif 下面有 pinctrl-0 = <&pinctrl_lcdif_dat 这样的,可以搜索 pinctrl_lcdif_dat 就可以获得更加详细的 gpio 配置。 这些配置一般使用厂商的工具生成即可。
  • &lcdif 下面有 display-timings, 其中的 clock-frequency 这个需要和 lcd 规格书里面的 dotclock 匹配。具体解析内核代码在 drivers/video/of_display_timing.c 中。 static int of_parse_display_timing(const struct device_node *np, struct display_timing *dt) 会匹配设备树中的字符串,然后把相关数值给到 结构体成员中。
  • drivers/video/videomode.c 中,void videomode_from_timing(const struct display_timing *dt, struct videomode *vm) 会把 display_timing 中的 dt->pixelclock.typ 转化为 videomode 中的 vm->pixelclock
  • drivers/video/fbdev/mxc/ldb.c 中, static int ldb_setup(struct mxc_dispdrv_handle *mddh, struct fb_info *fbi)serial_clk = ldb->spl_mode ? chan.vm.pixelclock * 7 / 2 : chan.vm.pixelclock * 7; 先计算,然后再 clk_set_rate(ldb_di_sel_parent, serial_clk); 写入。
  • 还可以从寄存器反着推算。
    • probe 函数中,host->base = devm_ioremap_resource(&pdev->dev, res);, 说明寄存器基地址是 host->base;
    • 搜索 host->base 可以看到 ctrl1 = readl(host->base + LCDC_CTRL1); 再搜索 LCDC_CTRL 就可以看到寄存器宏定义的地方,然后再根据寄存器偏移地址,获取寄存器宏名称 #define LCDC_V4_TRANSFER_COUNT 0x30
    • 搜索 LCDC_V4_TRANSFER_COUNT 得到 .transfer_count = LCDC_V4_TRANSFER_COUNT,,
    • 搜索 transfer_count 得到 writel(TRANSFER_COUNT_SET_VCOUNT(fb_info->var.yres) |TRANSFER_COUNT_SET_HCOUNT(fb_info->var.xres), host->base + host->devdata->transfer_count);static int mxsfb_set_par(struct fb_info *fb_info) 中。 mxsfb_set_parstatic struct fb_ops mxsfb_ops中,由系统调用.

整体来看,probe -> mxsfb_init_fbinfo -> mxsfb_init_fbinfo_dt 这个调用用于获取设备树数据并把 display_timing 先转为 videomode, 然后再转为 fb_videomode, 再加到 modelist 中;probe -> mxsfb_init_fbinfo -> fb_videomode_to_var 把 modelist中第一个转化为 fbinfo 的 varscreeninfo; 后续等系统调用 mxsfb_ops 中的 mxsfb_set_par 就可以写入寄存器了。

lcd 驱动框架

可以参考 drivers/video/fbdev/simplefb.c 按照这个框架就可以了。

引脚配置

可以使用 nxp 官方的 pin tools 工具,先加载预设配置,然后根据自己需要,选择引脚状态,然后就可以生成对应的设备树内容。 根据自己的需要,把里面的内容合并到源码设备树中。 具体写法,可以参考设备树已有的内容。

// root
    framebuffer-mylcd {
            compatible = "100ask,lcd_drv";
            pinctrl-names = "default";
            pinctrl-0 = <&mylcd_pinctrl>;
            backlight-gpios = <&gpio1 8 GPIO_ACTIVE_HIGH>;
    };
// &iomuxc / imx6ul-evk
        mylcd_pinctrl: mylcd_pingrp {                /*!< Function assigned for the core: Cortex-A7[ca7] */
            fsl,pins = <
                MX6UL_PAD_GPIO1_IO08__GPIO1_IO08           0x000010B0
                MX6UL_PAD_LCD_CLK__LCDIF_CLK               0x000010B0
                MX6UL_PAD_LCD_DATA00__LCDIF_DATA00         0x000010B0
                MX6UL_PAD_LCD_DATA01__LCDIF_DATA01         0x000010B0
                MX6UL_PAD_LCD_DATA02__LCDIF_DATA02         0x000010B0
                MX6UL_PAD_LCD_DATA03__LCDIF_DATA03         0x000010B0
                MX6UL_PAD_LCD_DATA04__LCDIF_DATA04         0x000010B0
                MX6UL_PAD_LCD_DATA05__LCDIF_DATA05         0x000010B0
                MX6UL_PAD_LCD_DATA06__LCDIF_DATA06         0x000010B0
                MX6UL_PAD_LCD_DATA07__LCDIF_DATA07         0x000010B0
                MX6UL_PAD_LCD_DATA08__LCDIF_DATA08         0x000010B0
                MX6UL_PAD_LCD_DATA09__LCDIF_DATA09         0x000010B0
                MX6UL_PAD_LCD_DATA10__LCDIF_DATA10         0x000010B0
                MX6UL_PAD_LCD_DATA11__LCDIF_DATA11         0x000010B0
                MX6UL_PAD_LCD_DATA12__LCDIF_DATA12         0x000010B0
                MX6UL_PAD_LCD_DATA13__LCDIF_DATA13         0x000010B0
                MX6UL_PAD_LCD_DATA14__LCDIF_DATA14         0x000010B0
                MX6UL_PAD_LCD_DATA15__LCDIF_DATA15         0x000010B0
                MX6UL_PAD_LCD_DATA16__LCDIF_DATA16         0x000010B0
                MX6UL_PAD_LCD_DATA17__LCDIF_DATA17         0x000010B0
                MX6UL_PAD_LCD_DATA18__LCDIF_DATA18         0x000010B0
                MX6UL_PAD_LCD_DATA19__LCDIF_DATA19         0x000010B0
                MX6UL_PAD_LCD_DATA20__LCDIF_DATA20         0x000010B0
                MX6UL_PAD_LCD_DATA21__LCDIF_DATA21         0x000010B0
                MX6UL_PAD_LCD_DATA22__LCDIF_DATA22         0x000010B0
                MX6UL_PAD_LCD_DATA23__LCDIF_DATA23         0x000010B0
                MX6UL_PAD_LCD_ENABLE__LCDIF_ENABLE         0x000010B0
                MX6UL_PAD_LCD_HSYNC__LCDIF_HSYNC           0x000010B0
                MX6UL_PAD_LCD_VSYNC__LCDIF_VSYNC           0x000010B0
            >;
        };

probe 函数中关于背光:

    /* get gpio from device tree */
    bl_gpio = gpiod_get(&pdev->dev, "backlight", 0);
    /* config bl_gpio as output */
    gpiod_direction_output(bl_gpio, 1);
    /* set val: gpiod_set_value(bl_gpio, status); */

时钟配置

imx6ull.dtsi 中,搜索 lcdif,就可以看到已经预先设置好了时钟,IMX6UL_CLK_DUMMY 这一项是用来占位的,为了兼容驱动程序而已。我们自己重新编写驱动程序和设备树,就可以省略这一条。

    framebuffer-mylcd {
            compatible = "100ask,lcd_drv";
            pinctrl-names = "default";
            pinctrl-0 = <&mylcd_pinctrl>;
            backlight-gpios = <&gpio1 8 GPIO_ACTIVE_HIGH>;
            clocks = <&clks IMX6UL_CLK_LCDIF_PIX>,
                     <&clks IMX6UL_CLK_LCDIF_APB>;
            clock-names = "pix", "axi";
    };

另外 axi 时钟因为是总线时钟,一般已经设置好了,不需要手动设置。

    /* get clk from device tree */
    clk_pix = devm_clk_get(&pdev->dev, "pix");
    clk_axi = devm_clk_get(&pdev->dev, "axi");
    /* set clk rate */
    clk_set_rate(clk_pix, 50000000);
    /* enable clk */
    clk_prepare_enable(clk_pix);
    clk_prepare_enable(clk_axi);

lcd 控制器获得参数

设备树

设备树中,display = <&display0>;, native-mode = <&timing0>; 用于指明具体使用的配置。后续的配置分为两部分,一部分是时序,另外一部分是极性。

时序相关的直接参考规格书里面的时间表格即可。 极性需要对照时序图,确认有效边沿的情况,上升沿是 0,下降沿是 1.

// framebuffer-mylcd
            display = <&displayA>;

            displayA: display {
                bits-per-pixel = <24>;
                bus-width = <24>;

                display-timings {
                    native-mode = <&timingA>;

                     timingA: timing0_1024x600 {
                     clock-frequency = <50000000>;
                     hactive = <1024>;
                     vactive = <600>;
                     hfront-porch = <160>;
                     hback-porch = <140>;
                     hsync-len = <20>;
                     vback-porch = <20>;
                     vfront-porch = <12>;
                     vsync-len = <3>;

                     hsync-active = <0>;
                     vsync-active = <0>;
                     de-active = <1>;
                     pixelclk-active = <0>;
                     };

                };
            }; 

代码

    struct device_node *display_np;
    int ret;
    int width;
    int bits_per_pixel;
    struct display_timings *timings = NULL;

    display_np = of_parse_phandle(pdev->dev.of_node, "display", 0);

    /* get common info */
    ret = of_property_read_u32(display_np, "bus-width", &width);
    ret = of_property_read_u32(display_np, "bits-per-pixel",
                   &bits_per_pixel);

    /* get timming */
    timings = of_get_display_timings(display_np);
  • pdev->dev.of_node 这个指向了设备树中对应的节点。static const struct of_device_id mxsfb_dt_ids[].compatible = "fsl,imx28-lcdif"imx6ull.dtsi 中的 lcdif 相匹配。 在 100ask_imx6ull-14x14.dts 中,&lcdiflcdif 进行了更加详细的配置。 所以,pdev->dev.of_node 实际对应的就是 &lcdif
  • display_np = of_parse_phandle(pdev->dev.of_node, "display", 0); 获得了 display[0] 的 handle
  • timings = of_get_display_timings(display_np); 对 time 进行获取
    • timings_np = of_get_child_by_name(np, "display-timings"); 获得 display-timings 节点
    • entry = of_parse_phandle(timings_np, "native-mode", 0); 获得 native-mode[0] 也就是 timing0 的 handle
    • for_each_child_of_node(timings_np, entry) r = of_parse_display_timing(entry, dt); 对 timing0 进行解析。
    • ret |= parse_timing_property(np, "hback-porch", &dt->hback_porch); 这样的时间保存在 dt 中。
    • 极性保存在 dt->flags 中。
      if (!of_property_read_u32(np, "vsync-active", &val))
      dt->flags |= val ? DISPLAY_FLAGS_VSYNC_HIGH :
      DISPLAY_FLAGS_VSYNC_LOW;

lcd 寄存器配置

根据芯片手册,并参考官方提供的裸机代码,对寄存器配置即可。

  • 根据芯片手册,定义一个寄存器结构体 lcdif,方便后续操作。并把相关寄存器操作放到一个函数里面。
  • 结构体 lcdif 地址应该通过设备树来获取, 设备树中对应的节点,应该有类似于 reg 参数,驱动中才能获取这个 mem 参数。
    framebuffer-mylcd {
            compatible = "100ask,lcd_drv";
            reg = <0x021c8000 0x4000>;
                        ...
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    lcdif = devm_ioremap_resource(&pdev->dev, res);

上机实验

替换驱动和设备树:

  • makefile 中需要注释旧的,增加新的
  • 设备树中,原来的 &lcdif 中的 status 需要改为 disabled
  • 内核源码 make mrproper make 100ask_imx6ull_defconfig make zImage -j4 这样编译内核与驱动; make dtbs 编译设备树。
  • 内核在 arch/arm/boot/zImage, 设备树arch/arm/boot/dts/100ask_imx6ull-14x14_ptz.dtb

重新烧录系统

如果系统有问题,需要重新烧录:

  • 开发板设置为 usb 启动,然后用 usb 连接
  • 使用专门的烧录工具进行烧录,烧录整个系统。 具体可以参考开发板使用手册上面的,烧写系统的章节
  • 烧录完成之后,断电,并设置为 emmc 启动,然后重新上电。

调试

  • 查看 /dev/fb0 有没有这个设备
  • 查看 /sys/firmware/devicetree/base/framebuffer-mylcd, cat compatible 看看是否和设备树文件以及驱动 struct of_device_id mxsfb_dt_ids[] 中的 compatible 一致。
  • 查看 /sys/bus/platform/drivers 下面有没有和 struct platform_driver mxsfb_driver 中的 .driver.name 同名的平台驱动文件夹。
  • 如果既有设备树,也有平台驱动,但是没有 /dev/fb0 这个设备。 唯一的可能就是 probe 没有工作。需要检查开机时候的启动信息。
  • 如果出现引脚被占用,那么需要把引脚在其他地方的 status 改为 disabled.

单 buffer 的缺点与改进方法

缺点

因为 app 和 lcd 控制器,使用同一个 buffer,就可能产生两种情况:

  • app 慢,buffer 中才更新了一般,lcd 已经刷新完成
  • app 快,lcd 才显示一部分,buffer 中的数据已经是下一帧的数据,导致 lcd 下半部分是新的数据。

都会导致画面撕裂,闪烁之类的情况。

改进

就是使用多 buffer 来改进。

  • 驱动 alloc 多 buffer
  • 驱动描述 多 buffer
  • app 获取 多 buffer
  • app 写入数据
  • app 启动切换 buffer
  • 驱动执行切换 buffer

代码

alloc 多 buffer
  • mxsfb.cfb_info->fix.smem_len = SZ_32M;
  • 其他开发板使用的 framesize = width * height * 2 * 2; fb->fb.fix.smem_len = framesize; 双倍 buffer.
  • 然后使用 dma_alloc_writecombine 这样的函数来申请内存空间。
描述 多 buffer
struct fb_var_screeninfo {
    __u32 xres;         /* visible resolution       */
    __u32 yres;
    __u32 xres_virtual;     /* virtual resolution       */
    __u32 yres_virtual;
    __u32 xoffset;          /* offset from virtual to visible */
    __u32 yoffset;          /* resolution           */

假设分辨率是 1024 x 600, 申请的是 3倍 buffer.

  • 申请的多个 buffer 本身是在一起的连续内存。
  • xres_virtualyres_virtual 指的是多个 buffer 在一起的总的尺寸。那么虚拟分辨率就是 1024 x 1800
  • xres yres 可视分辨率还是 1024 x 600
  • 可视分辨率和虚拟分辨率的关系是,在虚拟分辨率上,通过 xoffsetyoffset 的偏移,获得的就是 可视分辨率。
  • 如果现在使用第二个 fb,那么 xoffsetyoffset 就是 (0, 600)
  • 开始时,只能用单 buffer,也就是虚拟与可视一致。

app 获取 多 buffer

  • ioctl(fbfd, FBIOGET_FSCREENINFO, &m_finfo), ioctl(fbfd, FBIOGET_VSCREENINFO, &m_vinfo) 获取 fix 和 var 数据。
  • m_nPhysicalMemNum = m_finfo.smem_len / m_nMemSize; 通过 fix 和 var 中的显存大小和可视分辨率,可以计算出,申请的显存可以容纳多少个 fb; 然后对比 var 中的虚拟分辨率,确认有没有问题,如果有问题,需要使用 ioctl(fbfd, FBIOPUT_VSCREENINFO, &m_vinfo) 来重新设置。

app 写入数据

  • m_pMem = (char *)mmap(0, m_finfo.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fbfd, 0); 映射显存。
  • m_pVirtualMem[i] = m_pMem + i * m_nMemSize; 定位相应的 fb,然后就可以写入了。

app 启动切换 buffer

  • ioctl(fbfd, FBIOPAN_DISPLAY, &m_vinfo) 在修改 yoffset 之后,执行这个,来让驱动执行切换。

驱动执行切换 buffer

  • .fb_pan_display = mxsfb_pan_display, 因为 ops 中设置了这个,所以系统会调用 static int mxsfb_pan_display(struct fb_var_screeninfo *var, struct fb_info *fb_info)
  • writel(fb_info->fix.smem_start + offset, host->base + host->devdata->next_buf); 新的 fb 地址写入到下一帧寄存器
  • writel(CTRL1_CUR_FRAME_DONE_IRQ_EN, host->base + LCDC_CTRL1 + REG_SET); 设置当前帧完成中断使能
  • ret = wait_for_completion_timeout(&host->flip_complete, HZ / 2); 等待中断完成。完成之后,会有个返回给 app.

其他

新的驱动中,一般直接等待中断完成,然后 app 直接 if(ioctl(fbfd, FBIOPAN_DISPLAY, &m_vinfo) == -1) 检查返回值就行。

老的驱动中,没有这个等待中断完成,需要 app 主动等待 vsync 信号:

                m_vinfo.yoffset = nMemMap * m_nPhysicalHeight;
                ret = ioctl(m_fd, FBIOPAN_DISPLAY, &m_vinfo);
                if(ret == 0) {
                    m_nDispMemNo = nMemMap;
                    ioctl(m_fd, FBIO_WAITFORVSYNC, &ret);
                }

编写 多 buffer 应用程序

按照上节的步骤,编写应用程序即可。

调试的时候,需要先移除开发板自身的 gui 程序:

  • /etc/init.d/S99myirhmi2 移除这个自启动文件
  • 然后执行应用程序即可。

如果闪屏比较严重,可以在 app 中使用 nanosleep 来减慢速度,降低显存改变的频率,效果会好很多。

发表评论