4. Qt 图形架构与渲染管线
说实话,很多做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,流程大致是:
- 从设备树解析timing参数
- 申请Framebuffer内存(dma_alloc_coherent)
- 初始化LCD控制器寄存器
- 注册
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设备节点的完整链路:
驱动注册成功后,/dev/fb0就诞生了。上层Qt应用通过mmap映射这块内存,直接往里写像素数据,LCD控制器会自动把数据刷到屏幕上。
说白了,Framebuffer驱动就是搭了一座桥。桥的一端是内核内存,另一端是LCD屏。我们写驱动,就是确保这座桥的宽度、时序、协议都对得上。