IMX6ULL 移植

1 LCD 显示屏驱动

LCD 显示屏是常用的外设,IMXULL 提供了一个 eLCDIF 接口用于连接 RGB 接口的液晶屏。我们不需要关注液晶屏的原理,只需要明白相关的编程接口即可。不同分辨率的 LCD 屏幕的 eLCDIF 控制器的驱动代码都是一样的,因此我们只需要修改对应屏幕的参数即可。

1.1 分辨率

LCD 显示屏都是由一个一个的像素点组成的,一个像素点就类似一个小灯,这个小灯是由 R,G,B 三个颜色组成的,1080p 的意思就是一个 LCD 屏幕上的像素数量是 1920*1080 个。

1.2 像素格式

一个像素点是一个 RGB 小灯,RGB这三种颜色每个都是 8 bit 的数据。3 个就是 24bit。 这种像素格式称为 RGB888。再加入一个通道透明度,就是 32bit,也就是 ARGB8888。因此一个像素点占用4字节的内存。

1.3 LCD 屏幕接口

显示屏的接口有很多,比如 VGA,HDMI,DP 等等。但是本开发板不支持这些接口。只支持 RGB 接口的 LCD。信号线如下:

  • R: 8 根红线
  • G: 8 根绿线
  • B: 8 根黑线
  • DE: 数据使能
  • VSYNC: 垂直同步信号线
  • HSYNC: 水平同步信号线
  • PCLK: 像素时钟信号线

1.4 LCD 时间参数

HSYNC 是水平同步信号线,当此信号产生的话就表示开始显示新的一行了。VSYNC 是垂直同步信号,当此信号产生的时候就表示开始显示新的一帧图像了。在古早的大屁股显示屏当中,是由电子枪进行Z形运动显示的图像。当显示完一行之后就会发出 HSYNC 信号,此时电子枪就会关闭,然后迅速移动到屏幕左边,HSYNC 信号结束之后就可以显示新的一行了,此时电子枪就会重新打开。在 HSYNC 信号结束之后到电子枪重新打开之间会有一段延迟叫 HBP。当显示完一行之后就会关闭电子枪等待 HSYNC 信号产生,之间会插入一段延时,叫 HFP。同理,当显示完成一帧图片之后电子枪也会关闭,然后等待 VSYNC 信号产生,这段延时叫 VFP,当 VSYNC 结束之后电子枪也会重新打开,中间也有一段延时叫 VBP。在现代的屏幕当中继续存在着 HBP,HFP,VBP,VFP 这四个参数。

1.5 RGB LCD 屏幕时序

  • HSPW: 有些地方叫 THP,是 HSYNC 信号宽度/持续时间
  • HOZVAL: 有些地方叫 THD,表示显示一行数据所需要的时间
  • VSPW: 有些地方叫 TVP,是 VSYNC 的信号宽度
  • LINE: 有些地方叫 TVD,显示一帧有效数据所需要的时间,例如一个图像的分辨率是 1024*600,那么该时间就是 600 行的时间
  • HSYNC,HBP,HFP,VSYNC,VBP,VFP: 前面都介绍过了

alt text

alt text

因此最终的时间计算公式就是:

1
T = (VSPW + VBP + LINE + VFP) * (HSPW + HBP + HOZVAL + HFP)

一帧是 60 Hz。因此我们的像素时钟的计算公式是 T * 60

这里以 ATK7016 为例。

  • x: 1024
  • HSPW: 20
  • HBP: 140
  • HFP: 160
  • y: 600
  • VSPW: 3
  • VBP: 20
  • VFP: 12

那么 T = 853440,最终的像素时钟是 51206400 也就是 51.2M

2 显示框架

2.1 Framebuffer 框架

Framebuffer 框架的目的是将所有跟显示有关的硬件和软件关联起来从而虚拟出一个 fb 设备。当我们编写好 LCD 驱动以后,就会生成一个名为 /dev/fbX 的设备,应用程序通过访问 /dev/fbX 就可以控制 LCD 了。

/dev/fbX 是一个字符设备,其初始化代码如下:

Framebuffer 的公共逻辑代码在 fbmem.c 当中,他的作用只是让 linux 把设备当成一个 Framebuffer 设备来管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// linux-imx-rel_imx_4.1.15_2.1.0_ga/drivers/video/fbdev/core/fbmem.c
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,
#ifdef HAVE_ARCH_FB_UNMAPPED_AREA
.get_unmapped_area = get_fb_unmapped_area,
#endif
#ifdef CONFIG_FB_DEFERRED_IO
.fsync = fb_deferred_io_fsync,
#endif
.llseek = default_llseek,
};

之后是 mxsfb.c 的部分,他是负责让硬件显示的主要代码。整个操作流程如下:

1
应用程序 -> /dev/fb0 -> fbmem.c -> mxsfb.c -> LCDIF寄存器 -> RGB屏

mxsfb.c 主要干这些:

  • 解析设备树时序,分配显存,设置 fb_info,实现 fb_ops,编程 LCDIF 寄存器

fbmem.c 负责如下:

它不关心是 IMX6ULL,STM32 还是别的控制器,它只管 framebuffer 框架公共逻辑。

也就是说 fbmem.c 主要干这些:

  • 建立 /dev/fb0,管理 framebuffer 设备注册,给用户态提供统一接口,通知 fbcon

流程一般如下:

  1. mxsfb_probe()
  2. framebuffer_alloc()
  3. 设置 fb_info 和 fbops
  4. 初始化 LCDIF 硬件
  5. 调用 register_framebuffer()
  6. fbmem.c 把它登记成 /dev/fb0
  7. fbcon 或用户程序通过 /dev/fb0 使用它

IMX6ULL 已经为 eLCDIF 控制器编写好了代码,往后我们挂上显示屏一般只需要调整对应的参数即可。

2.2 DRM 框架

TODO

3 LCD 驱动实验

IMX6ULL 集成了 eLCDIF 控制器,支持 MPU,VSYNC,DOTCLK 三种工作模式。

原理图如下:

alt text

NXP 原版的 lcdif 节点如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
&lcdif {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_lcdif_dat
&pinctrl_lcdif_ctrl
&pinctrl_lcdif_reset>;
display = <&display0>;
status = "okay";

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

display-timings {
native-mode = <&timing0>;
timing0: timing0 {
clock-frequency = <9200000>;
hactive = <480>;
vactive = <272>;
hfront-porch = <8>;
hback-porch = <4>;
hsync-len = <41>;
vback-porch = <2>;
vfront-porch = <4>;
vsync-len = <10>;

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

这里我们的显示屏没有复位 IO。因此不需要 pinctrl_lcdif_reset。我们的显示屏模式是 RGB888,因此是 24bit。

LCD 屏幕参数调节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
&lcdif {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_lcdif_dat
&pinctrl_lcdif_ctrl>;
display = <&display0>;
status = "okay";

display0: display {
bits-per-pixel = <24>; /* 3*8 */
bus-width = <24>;

display-timings {
native-mode = <&timing0>;
timing0: timing0 {
clock-frequency = <51200000>; /* 根据上述的计算公式 */
hactive = <1024>; /* LCD x轴像素个数 */
vactive = <600>; /* LCD y轴像素个数 */
hfront-porch = <160>; /* hfp */
hback-porch = <140>; /* hbp */
hsync-len = <20>; /* hspw */
vback-porch = <20>; /* vbp */
vfront-porch = <12>; /* vfp */
vsync-len = <3>; /* vspw */

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

LCD 背光调节:

背光控制 IO 连接到了开发板上的 GPIO1_IO08 上。我们需要将该引脚复用为 PWM1_OUT 来控制 LCD 屏幕的背光亮度。但是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
		pinctrl_pwm1: pwm1grp {
fsl,pins = <
MX6UL_PAD_GPIO1_IO08__PWM1_OUT 0x110b0
>;
};

......

pwm1: pwm@02080000 {
compatible = "fsl,imx6ul-pwm", "fsl,imx27-pwm";
reg = <0x02080000 0x4000>;
interrupts = <GIC_SPI 83 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_PWM1>,
<&clks IMX6UL_CLK_PWM1>;
clock-names = "ipg", "per";
#pwm-cells = <2>;
};

......

&pwm1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_pwm1>;
status = "okay";
};

......
// Documentation/devicetree/indings/video/backlight/pwm-backlight.txt
backlight {
compatible = "pwm-backlight";
pwms = <&pwm1 0 5000000>;
brightness-levels = <0 4 8 16 32 64 128 255>;
default-brightness-level = <6>;
status = "okay";
};

观察结果。

alt text

4 终端输出显示屏

首先需要内核启动终端输出的相关配置。

首先 Framebuffer 框架必须生效。其次需要打开一些内核的配置。

1
2
3
4
5
6
7
8
9
CONFIG_TTY=y
CONFIG_VT=y
CONFIG_VT_CONSOLE=y
CONFIG_HW_CONSOLE=y
CONFIG_VT_HW_CONSOLE_BINDING=y
CONFIG_DUMMY_CONSOLE=y
CONFIG_FRAMEBUFFER_CONSOLE=y
CONFIG_FRAMEBUFFER_CONSOLE_DETECT_PRIMARY=y
CONFIG_UNIX98_PTYS=y

/etc/inittab 当中加入配置。这表示在第一个终端上启动 shell。

1
tty1::askfirst:-/bin/sh

然后设置启动参数。

下面设置了两个终端 ttymxc0tty0

1
setenv mmcargs 'setenv bootargs console=ttymxc0,115200 console=tty0 root=${mmcroot}'

之后启动,可以看到成功显示在了 LCD 屏幕上。并且 console=tty0 实际落地到了 tty1 上。

1
2
[imx6ull:/]# cat /sys/class/tty/tty0/active
tty1

alt text

References

  1. 野火的 DRM 框架解释: https://doc.embedfire.com/linux/stm32mp1/driver/zh/latest/linux_driver/framework_drm.html#id1