同步与互斥
- 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(®istration_lock); fb_info = registered_fb[idx]; if (fb_info) atomic_inc(&fb_info->count); mutex_unlock(®istration_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->var
是struct 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_fb
在static int do_register_framebuffer(struct fb_info *fb_info)
中被写入,但是这个是 static 函数,还要继续找调用这个函数的地方。- 找到
do_register_framebuffer
被int 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_TRUECOLOR
中u32 *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_memory
把info->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_driver
中mxsfb_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_driver
中mxsfb_driver->.probe
对应 mxsfb_proberes = 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
设置设备为挂起状态(忽略引用计数)。
- probe 函数中启用运行时电源管理。
ret = mxsfb_init_fbinfo(host);
配置 fbinforet = 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_par
在static 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
中,&lcdif
对lcdif
进行了更加详细的配置。 所以,pdev->dev.of_node
实际对应的就是&lcdif
display_np = of_parse_phandle(pdev->dev.of_node, "display", 0);
获得了 display[0] 的 handletimings = 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 的 handlefor_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.c
中fb_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_virtual
和yres_virtual
指的是多个 buffer 在一起的总的尺寸。那么虚拟分辨率就是 1024 x 1800xres
yres
可视分辨率还是 1024 x 600- 可视分辨率和虚拟分辨率的关系是,在虚拟分辨率上,通过
xoffset
和yoffset
的偏移,获得的就是 可视分辨率。 - 如果现在使用第二个 fb,那么
xoffset
和yoffset
就是 (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
来减慢速度,降低显存改变的频率,效果会好很多。