4. 最简单的Framebuffer驱动实战:基于tiny4412的LCD驱动编写

说实话,很多做Qt的朋友一听到「写驱动」就头大。其实Framebuffer驱动没那么玄乎。说白了,它就是告诉内核:我的LCD屏长什么样,怎么把数据发过去。

我当年第一次调tiny4412的LCD时,也踩了不少坑。这次我就带着你,一步步把这块屏点亮。

4.1 先搞清楚硬件连接

tiny4412用的是Exynos4412处理器,自带LCD控制器。我们接的是一块4.3寸、分辨率480x272的RGB接口屏。

硬件上需要关注这几个信号:

  • VSYNC — 帧同步,一帧开始时的脉冲
  • HSYNC — 行同步,一行开始时的脉冲
  • VCLK — 像素时钟,每个时钟传一个像素
  • VD[23:0] — 数据线,24位RGB
  • DEN — 数据使能,有效数据区间

我的经验: 拿到一块新屏,第一件事就是找数据手册里的时序图。别急着写代码,先把VSYNC、HSYNC的极性搞清楚。我曾经因为极性反了,折腾了两天才发现。

4.2 设备树配置

在Linux 3.5以上的内核里,LCD参数都放在设备树里。tiny4412的设备树文件是exynos4412-tiny4412.dts。

我们需要在fimd节点下添加display-timings:

&fimd {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&lcd_clk &lcd_data24 &lcd_pwm>;
    display-timings {
        native-mode = <&timing0>;
        timing0: timing {
            clock-frequency = <9000000>;  /* 9MHz像素时钟 */
            hactive = <480>;              /* 水平有效像素 */
            vactive = <272>;              /* 垂直有效像素 */
            hfront-porch = <2>;           /* 行前肩 */
            hback-porch = <2>;            /* 行后肩 */
            hsync-len = <41>;             /* 行同步脉宽 */
            vfront-porch = <2>;           /* 帧前肩 */
            vback-porch = <2>;            /* 帧后肩 */
            vsync-len = <10>;             /* 帧同步脉宽 */
            hsync-active = <0>;           /* 低电平有效 */
            vsync-active = <0>;           /* 低电平有效 */
            de-active = <1>;              /* 高电平有效 */
            pixelclk-active = <0>;        /* 下降沿采样 */
        };
    };
};

注意: clock-frequency的单位是Hz。9MHz对应的是480x272@60fps的典型值。如果算出来不对,用这个公式验算:freq = (hactive + hbp + hfp + hsync) * (vactive + vbp + vfp + vsync) * fps

4.3 Framebuffer驱动核心结构

驱动代码主要在drivers/video/fbdev/s3c-fb.c里。核心是填充struct fb_info和struct fb_var_screeninfo。

我习惯把关键参数列个表,方便对照:

参数 含义 我们的值
xres 水平分辨率 480
yres 垂直分辨率 272
bits_per_pixel 每像素位数 32
red.offset 红色偏移 16
green.offset 绿色偏移 8
blue.offset 蓝色偏移 0
transp.offset 透明度偏移 24

驱动初始化时,会调用s3c_fb_probe,流程大致是:

  1. 从设备树解析timing参数
  2. 申请Framebuffer内存(dma_alloc_coherent)
  3. 初始化LCD控制器寄存器
  4. 注册struct fb_info到内核

4.4 核心代码片段解析

咱们看看最关键的s3c_fb_set_par函数,它负责把参数写入硬件寄存器:

static int s3c_fb_set_par(struct fb_info *info)
{
    struct s3c_fb *sfb = info->par;
    struct fb_var_screeninfo *var = &info->var;
    u32 reg;

    /* 设置分辨率 */
    reg = (var->xres - 1) << 16 | (var->yres - 1);
    writel(reg, sfb->regs + VIDTCON2);

    /* 设置行场同步参数 */
    reg = (var->upper_margin - 1) << 24 |
          (var->lower_margin - 1) << 16 |
          (var->vsync_len - 1) << 8;
    writel(reg, sfb->regs + VIDTCON0);

    /* 设置像素时钟分频 */
    reg = (var->pixclock >> 12) & 0xFF;
    writel(reg, sfb->regs + VIDCON1);

    /* 设置数据格式为RGB888 */
    reg = readl(sfb->regs + WINCON0);
    reg &= ~0xF;
    reg |= 0xB;  /* 24位RGB */
    writel(reg, sfb->regs + WINCON0);

    return 0;
}

嗯,这里要注意:pixclock的单位是皮秒。我们设备树里配的9MHz,换算过来就是1000000000 / 9000000 = 111111ps。内核里会做进一步分频。

4.5 编译与测试

驱动写好后,编译内核:

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

把zImage和dtb烧到SD卡,启动后看看:

# ls /dev/fb*
/dev/fb0

# fbset -i
mode "480x272-60"
    # D: 9.000 MHz, H: 17.857 kHz, V: 60.000 Hz
    geometry 480 272 480 544 32
    timings 111111 2 2 2 2 41 10
    accel true
    rgba 8/16,8/8,8/0,8/24
endmode

看到这个输出,说明驱动已经跑起来了。 接下来可以用cat /dev/urandom > /dev/fb0看看屏幕有没有雪花点。有雪花点,说明数据通路通了。

4.6 常见问题与避坑

  • 屏幕全黑: 先量背光供电。我遇到过背光使能引脚没拉高的情况。
  • 画面偏移: 调整hfront-porch和hback-porch。这两个值屏厂给的往往偏大,可以试着减小。
  • 颜色不对: 检查RGB顺序。有些屏是BGR排列,需要在驱动里交换偏移值。
  • 闪烁: 像素时钟频率不对。用示波器量VCLK,确认是否接近目标值。

我曾经: 调一块7寸屏时,画面一直偏左20个像素。查了三天,最后发现是hfront-porch和hback-porch搞反了。从那以后,我每次都会对着数据手册的时序图,一个一个数。

4.7 整体流程回顾

下面这张图总结了从设备树到Framebuffer设备节点的完整链路:

Framebuffer驱动初始化流程 设备树解析 提取timing参数 dma_alloc_coherent申请FB内存 配置LCD控制器寄存器 register_framebuffer 关键数据结构 • struct fb_info • struct fb_var_screeninfo • struct fb_fix_screeninfo 硬件寄存器 • VIDTCON0/1/2 • VIDCON0/1 • WINCON0

驱动注册成功后,/dev/fb0就诞生了。上层Qt应用通过mmap映射这块内存,直接往里写像素数据,LCD控制器会自动把数据刷到屏幕上。

说白了,Framebuffer驱动就是搭了一座桥。桥的一端是内核内存,另一端是LCD屏。我们写驱动,就是确保这座桥的宽度、时序、协议都对得上。