泰山派3.1寸mipi屏幕驱动分析(对比正点原子LCD驱动)

前言

前端时间刚刚完成了正点原子IMX6ULL开发板下的LCD和触摸驱动的学习,随后又把泰山派里手机项目的扩展板MIPI屏幕以及触摸驱动调通了一遍。整体完成下来的感觉就是,还是什么都不会!于是就有了这一篇博客,希望能通过梳理总结,从这两种不同开发板,不同屏幕型号的开发中,对linux下屏幕/触摸驱动的开发有一个更加深入的了解。总的来说,两者的开发流程几乎一致,但是其中有一些小细节还是有区别的,接下来我从头梳理并对比两者的开发流程。

硬件部分

1.正点原子

IMX6ULL开发板使用的是一款RGB LCD(Liquid Crystal Display),即液晶显示器。它是一种并行RGB接口。具体型号是啥就不知道了,原子的教程里貌似没有说,不过后面应该能从驱动里获得一些信息。

硬件原理图如下:

其中,I2C2_SCLI2C2_SDA就是触摸芯片的通信引脚,CT_INT是触摸的中断引脚,CT_RST是触摸的中断引脚。BLT_PWM是LCD的背光控制IO。这里我使用的是480x272的屏幕,使用的触摸芯片是gt9147

具体的引脚定义如下:

引脚功能 引脚定义
I2C2_SCL UART5_TXD
I2C2_SDA UART5_RXD
CT_INT GPIO1_IO9
CT_RST SNVS_TAMPER9
BLT_PWM GPIO1_IO8

2.泰山派

下面再来一下泰山派扩展屏的屏幕,这是一款MIPI屏幕,MIPI屏幕的接口一般简称为MIPI DSI。

采用的是一款3.1寸(480x800)的RGB屏幕,型号为D310T9362V1,具体参数特性如下:

除此之外,屏幕上板载了触摸芯片,型号为ft5x06

由于泰山派的MIPI DSI接口和该屏幕的接口并不兼容,所以要设计一个扩展板。下面来分析电路原理图,以及扩展板设计。

2.1 屏幕MIPI接口
其中`BACK_LED+`和`BACK_LED-`分别是**背光的驱动正负极**。

MIPI接口相关定义:

1.采用差分对来进行数据传输,这里MIPI_DSI_0PMIPI_DSI_0NMIPI_DSI_1PMIPI_DSI_1N就是两对差分数据线,P代表正极,N代表负极。两对差分数据线,那么这个就是 2 lanes,lane 越多,总带宽就越大。并且mipi屏幕lanes的多少跟屏幕的分辨率也有直接关系,屏幕分辨率越高lanes数量就越多

2.时钟线,同样是一个差分对MIPI_DSI_CLKP为时钟线正极,MIPI_DSI_CLKN为时钟线负极。

3.复位引脚,MIPI_DSI_RESET为复位IO。

2.2 泰山派MIPI DSI接口

泰山派的MIPI DSI接口有四对差分数据线,也就是 4 lanes,但是我们的屏幕只有 2 lanes,所有有一些引脚可以直接不连,只选用0和1这前两对差分线用作数据传输

MIPI_DSI_VCC_LED+MIPI_DSI_VCC_LED-是泰山派板载的背光驱动输出正负极,常规方案是直接将这对接到BACK_LED+BACK_LED-上,以此实现背光驱动,但它输出的电流有110mA而我们3.1寸屏幕最大能承受的驱动电流是25mA所以不适合直接接到3.1寸屏幕的FPC上。

同时我们发现泰山派的这个接口里没有预留PWM功能的引脚,那是怎么进行背光调节的呢?

这里就和原子的LCD接口不太一样了,原子是直接给了一个有PWM背光引脚,配置好这个引脚就可以实现背光调节了。但是泰山派的接口里只有MIPI_DSI_VCC_LED+MIPI_DSI_VCC_LED-,并没有PWM调节引脚。查看原理图如下:

这就是一个背光驱动电路,采用驱动芯片SY7201ABC是一款高效率的LED驱动器,主要用于控制和调节LED灯的亮度。SY7201ABC通过提供恒定电流来确保LED发光的一致性和稳定性,从而提高了LED使用的寿命和效能。

这里就比较清晰了,并不是说单纯给一个PWM引脚就可以实现背光调节,背光LED的供电也并不是单纯给一个5V,而是要有一个背光驱动电路,驱动芯片的输入包括5V电压和PWM信号,然后从MIPI_DSI_VCC_LED+MIPI_DSI_VCC_LED-正负极引脚输出恒定电流给背光LED,实现背光驱动。

所以猜想一下正点原子为什么只需要提供一个PWM引脚就可以,应该是在LCD上已经板载了一个背光驱动电路,配置的引脚就相当于这上面的PWM5_LCD_BL

所以说,由于电流原因,我们不采用泰山派板载的背光驱动电路了,那么我们就需要在扩展板上重新设计一个符合要求的背光驱动电路。

2.3 触摸接口

左边为泰山派触摸接口,右边为3.1寸屏幕引出来的触摸接口,这个最多就是改一下线序,引脚无非就是I2C,中断,复位这三个。这里用的是I2C1,具体的定义如上如图。

2.4 扩展板背光电路

首先要解决一个问题,就是没有PWM引脚,但是触摸接口是有一个I2C接口的,可以使用一个I2C转PWM芯片来实现PWM的供入。

所以这里的背光电路就包含:背光调节电路,背光驱动电路,背光选择电路

2.4.1 背光选择电路

用来选择使用MIPI_DSI_VCC_LED(泰山派板载驱动)还是用MIPI_DSI_VCC_LED(扩展板板载驱动)。

贴片0殴电阻在R105,R106,那么就是选择扩展板的驱动输入给3.1寸屏幕。

2.4.2 背光调节电路

采用GP7101这个I2C转PWM芯片,输出的PWM信号给到背光驱动电路,实现背光的PWM调节。

2.4.3 背光驱动电路

屏幕驱动

从软件角度来说,泰山派和原子的屏幕驱动都是两个步骤:背光驱动和屏幕参数修改

前面提到了很多次背光这个概念,在屏幕显示中,我们可以将背光驱动简单理解为:1.点亮屏幕,如果没有的话屏幕就是黑屏,因为LCD本身是不发光的,背光LED才是光源;2.调节屏幕亮度。所以说,背光驱动是非常重要的一环。

背光驱动

1.正点原子

正点原子使用的PWM引脚是GPIO1_IO8,接下来我们一步一步来看配置背光调节驱动,哪些内容需要我们修改,哪些内容不需要修改。

首先我们知道GPIO1_IO8这个引脚是要复用为PWM1_OUT的,我们在imx6ull.dtsi文件下找到pwm1的节点定义

/ {
	soc {
		....
        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>;
        };
        ....
	}
}

搜索一下兼容属性compatible的两个值,可以找到pwm的驱动文件drivers/pwm/pwm-imx.c

在这里插入图片描述

这就说明,pwm的驱动是不用我们来写的了,内核里已经写好了

imx6ull.dtsi是soc的公共资源描述,是描述芯片的硬件资源的,如pwm,i2c,gpio等。而用户进行设计的话一般是对板级dts文件如imx6ull-alientek-emmc.dts进行追加或者修改。所以imx6ull.dtsi只是配置了pwm控制器的通用信息,而我们要去指定具体的一个引脚复用为pwm1功能,就需要在板级文件imx6ull-alientek-emmc.dts里追加复用信息。这种分层的设计,就能让用户能够更为灵活的定义pwm功能。

所以,接下来就是在imx6ull-alientek-emmc.dts下定义我们要配置的pwm引脚的复用信息,追加pwm1内容如下:

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

pinctrl_pwm1: pwm1grp {   /* 背光PWM引脚配置 */
    fsl,pins = <
        MX6UL_PAD_GPIO1_IO08__PWM1_OUT   0x110b0
    >;
};

这样就是告诉内核,我们用GPIO1_IO08这个引脚作为PWM1的复用输出。那么由于内核已经写好了pwm驱动,此时该引脚就可以用作pwm输出了,如单独使用的话,就是去看内核的pwm驱动文件里是怎么定义设备操作函数的。

但是我们这里是要将这个引脚作为背光调节的功能,我们需要告诉内核,这个pwm引脚需要用作背光调节的功能。这个接口内核也提供了,方法就是在imx6ull-alientek-emmc.dts的根节点下定义一个背光节点backlight

/ {
	backlight {  /* 背光节点 */
		compatible = "pwm-backlight";
		pwms = <&pwm1 0 5000000>;
		brightness-levels = <0 4 8 16 32 64 128 255>;
		default-brightness-level = <6>;
		status = "okay";
	};
}	

compatible属性值要设置为pwm-backlight,意思已经很明显了,就是内核已经写好了一个背光调节的驱动,只要我们对应上属性即可。

在这里插入图片描述

可以看到内核的背光驱动就是drivers/video/backlight/pwm_bl.c

pwms = <&pwm1 0 5000000>;就是去绑定对应的pwm引脚的,GPIO1_IO08是pwm1的输出通道0,所以这里就是&pwm1 05000000是设置频率,这里对应200Hz。

brightness-levels = <0 4 8 16 32 64 128 255>;描述亮度级别,范围为 0~255,0 表示 PWM 占空比为 0%,也就是亮度最低,255 表示 100%占空比,也就是亮度最高。这里是设置 8 级背光(0~7),分别为 0、4、8、16、32、64、128、255,对应占空比为0%、1.57%、3.13%、6.27%、12.55%、25.1%、50.19%、100%,如果嫌少的话可以自行添加一些其他的背光等级值。

default-brightness-level = <6> 为默认亮度级别,这里设置默认背光等级为 6,也就是 50.19%的亮度。


到这里正点原子LCD的背光驱动就全部完成了,我们发现pwm驱动和背光驱动内核实际上都已经写好了,我们只需要在设备树里指定pwm引脚,绑定背光节点就可以了,并不需要自己写代码。这里就能很好的理解compatible这个属性的作用了,它可以自动帮我们去匹配相应的驱动,用户只需要修改设备树的compatible属性,不需要修改驱动源码,而且同一份驱动可以支持不同硬件,只要 compatible 匹配即可。

2.泰山派

泰山派使用扩展板来外接屏幕,本身是没有PWM接口的,而是用GP7101这个I2C转PWM芯片。所以我们实际上是通过配置I2C,来通过I2C通信给GP7101来控制PWM的输出。因此方式和正点原子的就不一样了, 我们不需要去配置pwm的引脚复用,且内核提供的背光驱动我们也用不了了,因为它是绑定soc的硬件pwm资源来实现输出的。所以,这里要做的就是:1.配置I2C实现与GP7101的通信,以控制PWM;2.自行注册内核的背光功能,相当于自己写背光驱动。

1.设备树配置

前面说了,我们是要自己来注册背光设备的,因此设备树文件tspi-rk3566-dsi-v10.dtsi里的有关backlight节点的就可以注释掉了。

linux_sdk/kernel/arch/arm64/boot/dts/rockchip/tspi-rk3566-user-v10-linux.dts追加i2c1节点内容,添加GP7101子节点。

&i2c1 {
    status = "okay"; // 状态为"okay",表示此节点是可用和配置正确的
    GP7101@58 {      // 定义一个子节点,名字为GP7101,地址为58
        compatible = "gp7101-backlight";   // 该节点与"gp7101-backlight"兼容,
        reg = <0x58>;                      // GP7101地址0x58
        max-brightness-levels = <255>;     // 背光亮度的最大级别是255
        default-brightness-level = <100>;  // 默认的背光亮度级别是100
    };
};

这里我们发现泰山派和正点原子的都定义了default-brightness-level。但是泰山派这里的值意义是亮度值,而正点原子里的是亮度等级。

但是泰山派定义了 max-brightness-levels = <255>,而正点原子定义的是brightness-levels = <0 4 8 16 32 64 128 255>;,泰山派定义的是最大等级,原子是将每个等级都枚举出来。

产生区别的原因应该是驱动的方式不一样,正点原子的写法是遵循内核的背光驱动来的,而我们是自己写背光驱动,这样写可能会更方便。

2.驱动程序
#include "linux/stddef.h"
#include <linux/kernel.h>
#include <linux/hrtimer.h>
#include <linux/i2c.h>
#include <linux/input.h>
#include <linux/module.h>
#include <linux/delay.h>
#include <linux/proc_fs.h>
#include <linux/string.h>
#include <linux/uaccess.h>
#include <linux/vmalloc.h>
#include <linux/interrupt.h>
#include <linux/io.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <linux/slab.h>
#include <linux/timer.h>
#include <linux/input/mt.h>
#include <linux/random.h>
#include <linux/backlight.h>

#if 1
#define MY_DEBUG(fmt,arg...);  printk("gp7101_bl:%s %d "fmt"",__FUNCTION__,__LINE__,##arg);
#else
#define MY_DEBUG(fmt,arg...)
#endif

// I2C 背光控制器寄存器定义 
#define BACKLIGHT_REG_CTRL_8  0x03
#define BACKLIGHT_REG_CTRL_16 0x02

// 设备结构体 
struct gp7101_bl_dev {

    struct i2c_client *client;

};

struct gp7101_bl_dev gp7101bldev;	


static s32 i2c_write_regs(struct i2c_client *client, u8 reg, u8 *buf, u8 len)
{
	u8 b[256];
	struct i2c_msg msg;

	b[0] = reg;					// 寄存器首地址 
	memcpy(&b[1],buf,len);		// 将要写入的数据拷贝到数组b里面 
		
	msg.addr = client->addr;	// 从机地址 
	msg.flags = 0;				// 标记为写数据 

	msg.buf = b;				// 要写入的数据缓冲区 
	msg.len = len + 1;			// 要写入的数据长度 

	return i2c_transfer(client->adapter, &msg, 1);
}

// 设置背光亮度
static int gp7101_backlight_set(struct backlight_device *bl) {

    struct gp7101_bl_dev *dev = bl_get_data(bl);  // 获取背光设备结构体
    struct i2c_client *client = dev->client;      // 获取I2C设备指针

    u8 addr[1] =  {BACKLIGHT_REG_CTRL_8};
    u8 buf[1] = {bl->props.brightness}  // 缓冲区,存储背光亮度

    MY_DEBUG("pwm:%d", bl->props.brightness);  

    i2c_write_regs(client, addr[0], buf, sizeof(buf));

    return 0;

}

// 背光设备操作结构体
static struct backlight_ops gp7101_backlight_ops = {
    .update_status = gp7101_backlight_set,
};


/*
 * @description		: i2c驱动的probe函数,当驱动与
 * 					  设备匹配以后此函数就会执行,进行设备初始化操作
 * @param - dev 	: i2c设备
 * @return 			: 0,成功;其他负值,失败
 */
static int  gp7101_bl_probe(struct i2c_client *client, const struct i2c_device_id *id)
{	

    struct backlight_device *bl;                  // backlight_device结构用于表示背光设备
    struct backlight_properties props;            // 背光设备的属性
    struct device_node *np = client->dev.of_node; // 设备树中的节点

    printk("gp7101_bl driver and device was matched!\r\n");

    gp7101bldev.client = client;

    // 初始化背光属性结构
    memset(&props, 0, sizeof(props));
    props.type = BACKLIGHT_RAW; // 设置背光类型为原始类型

    of_property_read_u32(np, "max-brightness-levels", &props.max_brightness);  // 从设备树中读取最大亮度级别
    of_property_read_u32(np, "default-brightness-level", &props.brightness);  // 从设备树中读取默认亮度级别

    if (props.max_brightness > 255 || props.max_brightness < 0) {
        props.max_brightness = 255;
    }
    if (props.brightness > props.max_brightness || props.brightness < 0) {
        props.brightness = props.max_brightness;
    }

    // 注册背光设备
    bl = devm_backlight_device_register(&client->dev, "backlight", &client->dev, 
                                        &gp7101bldev, &gp7101_backlight_ops, &props);
    
    printk("max_brightness:%d  default_brightness:%d\r\n", props.max_brightness, props.brightness);
    
    backlight_update_status(bl); // 更新背光设备的状态


	return 0;
}

/*
 * @description     : i2c驱动的remove函数,移除i2c驱动的时候此函数会执行
 * @param - client 	: i2c设备
 * @return          : 0,成功;其他负值,失败
 */
static int gp7101_bl_remove(struct i2c_client *client)
{
    MY_DEBUG("locat");
	return 0;
}

// 传统匹配方式ID列表 , 这个必须要有
static const struct i2c_device_id gp7101_bl_id[] = {
	{"gp7101-backlight", 0},  
	{}
};

// 设备树匹配列表
static const struct of_device_id gp7101_bl_of_match[] = {
	{ .compatible = "gp7101-backlight" },
	{ }
};

// i2c驱动结构体  
static struct i2c_driver gp7101_bl_driver = {
	.driver		= {
		.owner = THIS_MODULE,
		.name	= "gp7101-bl",				// 驱动名称,用于和设备匹配 
		.of_match_table	= gp7101_bl_of_match, // 设备树匹配表 		 
	},
	.probe		= gp7101_bl_probe,
	.remove		= gp7101_bl_remove,
	.id_table = gp7101_bl_id,
};

/*
 * @description	: 驱动模块加载函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init gp7101_bl_init(void)
{
	int ret = 0;

	ret = i2c_add_driver(&gp7101_bl_driver);
	return ret;
}

/*
 * @description	: 驱动模块卸载函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit gp7101_bl_exit(void)
{
	i2c_del_driver(&gp7101_bl_driver);
}

module_init(gp7101_bl_init);
module_exit(gp7101_bl_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("lzp");

整体框架简单来说就是:在I2C驱动框架下,注册背光驱动

I2C框架就不用细说了,这个与正点原子的教程里是一模一样的。主要来看一下背光驱动是怎么注册的,如何调节背光的。主要来看probe函数,当匹配到gp7101-backlight属性,probe函数就会执行。

struct backlight_device *bl;                  // backlight_device结构用于表示背光设备
struct backlight_properties props;            // 背光设备的属性

bl是背光设备结构体变量,注册背光设备后会将信息写入该结构体内,然后执行背光操作的函数就是以该结构体变量进行传参。

backlight_properties这个结构体是backlight_device结构体里的成员变量。它用来表示背光设备的属性,具体定义如下:

/* This structure defines all the properties of a backlight */
struct backlight_properties {
	/* Current User requested brightness (0 - max_brightness) */
	int brightness;
	/* Maximal value for brightness (read-only) */
	int max_brightness;
	/* Current FB Power mode (0: full on, 1..3: power saving
	   modes; 4: full off), see FB_BLANK_XXX */
	int power;
	/* FB Blanking active? (values as for power) */
	/* Due to be removed, please use (state & BL_CORE_FBBLANK) */
	int fb_blank;
	/* Backlight type */
	enum backlight_type type;
	/* Flags used to signal drivers of state changes */
	unsigned int state;

#define BL_CORE_SUSPENDED	(1 << 0)	/* backlight is suspended */
#define BL_CORE_FBBLANK		(1 << 1)	/* backlight is under an fb blank event */

};

struct backlight_device {
	/* Backlight properties */
	struct backlight_properties props;

	/* Serialise access to update_status method */
	struct mutex update_lock;

	/* This protects the 'ops' field. If 'ops' is NULL, the driver that
	   registered this device has been unloaded, and if class_get_devdata()
	   points to something in the body of that driver, it is also invalid. */
	struct mutex ops_lock;
	const struct backlight_ops *ops;

	/* The framebuffer notifier block */
	struct notifier_block fb_notif;

	/* list entry of all registered backlight devices */
	struct list_head entry;

	struct device dev;
	/* Backlight cooling device */
	struct thermal_cooling_device *cdev;
	/* Thermally limited max brightness */
	int thermal_brightness_limit;
	/* User brightness request */
	int usr_brightness_req;

	/* Multiple framebuffers may share one backlight device */
	bool fb_bl_on[FB_MAX];

	int use_count;
};

接着来看

struct device_node *np = client->dev.of_node;

device_node 这个结构体就是用来描述设备树节点的,它通过client->dev.of_node来传入,相似的写法我们之前也遇到过,比如:

leddev.node = of_find_node_by_path("/gpioled");
leddev.node = dev->dev.of_node;    // 这里dev的类型是 struct platform_device *dev

client->dev.of_nodei2c_client这个结构体来帮助我们获取i2c节点下的客户端子节点的方式,这里也就是我们前面在设备树里定义的GP7101@58这个节点。

i2c_client这结构体里的成员变量struct device dev,这是设备结构体,它包含了设备的基本信息和操作,然后再device这个结构体里又包含成员变量struct device_node *of_node,这个是设备树节点信息结构体,这两个要区分清楚。

接着我们就可以利用np这个设备树节点,来读取节点里对应属性的信息:

of_property_read_u32(np, "max-brightness-levels", &props.max_brightness);  // 从设备树中读取最大亮度级别
of_property_read_u32(np, "default-brightness-level", &props.brightness);  // 从设备树中读取默认亮度级别

这里就是将两个属性值读取到props结构体变量里。

接下来就是注册背光设备了

// 注册背光设备
bl = devm_backlight_device_register(&client->dev, "backlight", &client->dev, 
                                    &gp7101bldev, &gp7101_backlight_ops, &props);

devm开头的函数都是表示自动注销,无需我们手动注销。

&gp7101bldev是我们传入的私有数据,后面可以用bl_get_data来获取。

gp7101_backlight_ops是背光设备的操作函数,需要我们定义:

// 设置背光亮度
static int gp7101_backlight_set(struct backlight_device *bl) {

    struct gp7101_bl_dev *dev = bl_get_data(bl);  // 获取背光设备结构体
    struct i2c_client *client = dev->client;      // 获取I2C设备指针

    u8 addr[1] =  {BACKLIGHT_REG_CTRL_8};
    u8 buf[1] = {bl->props.brightness}  // 缓冲区,存储背光亮度

    MY_DEBUG("pwm:%d", bl->props.brightness);  

    i2c_write_regs(client, addr[0], buf, sizeof(buf));

    return 0;

}

// 背光设备操作结构体
static struct backlight_ops gp7101_backlight_ops = {
    .update_status = gp7101_backlight_set,
};

这个实际上和文件设备操作函数file_operations是有点像的。backlight_ops的定义如下:

struct backlight_ops {
	unsigned int options;

#define BL_CORE_SUSPENDRESUME	(1 << 0)

	/* Notify the backlight driver some property has changed */
	int (*update_status)(struct backlight_device *);
	/* Return the current backlight brightness (accounting for power,
	   fb_blank etc.) */
	int (*get_brightness)(struct backlight_device *);
	/* Check if given framebuffer device is the one bound to this backlight;
	   return 0 if not, !=0 if it is. If NULL, backlight always matches the fb. */
	int (*check_fb)(struct backlight_device *, struct fb_info *);
};

可以看到,主要就是注册背光的更新、获取、检查,这三种操作。

我们注册一个背光更新的函数,gp7101_backlight_set,内部通过获取背光的属性props里的亮度值,再通过I2C写入GP7101,就实现了背光亮度设置。

backlight_update_status(bl); // 更新背光设备的状态

这个函数会调用update_status也就是gp7101_backlight_set,用来更新背光为我们设置的默认值。

后续应用层调整背光,就是通过写入背光亮度数据,内核就会调用gp7101_backlight_set函数,从而实现背光调节的功能。

屏幕参数修改

1.正点原子

先看imx6ull.dtssoc节点下的lcdif节点

lcdif: lcdif@021c8000 {
    compatible = "fsl,imx6ul-lcdif", "fsl,imx28-lcdif";
    reg = <0x021c8000 0x4000>;
    interrupts = <GIC_SPI 5 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&clks IMX6UL_CLK_LCDIF_PIX>,
         <&clks IMX6UL_CLK_LCDIF_APB>,
         <&clks IMX6UL_CLK_DUMMY>;
    clock-names = "pix", "axi", "disp_axi";
    status = "disabled";
};

这个跟上面的pwm1节点的情况就是一样的,厂商已经为我们提供了统一的lcd资源接口,即配置了soc层面的资源描述,我们只需要在imx6ull-alientek-emmc.dts里根据我们开发板所适配的LCD型号,来追加信息即可。

我们可以根据compatible属性,搜索到内核里写好的lcd的驱动文件(drivers/video/fbdev/mxsfb.c):

在这里插入图片描述

追加信息如下,主要就是引脚配置,lcd属性,时序信息这些,这些应该可以从lcd的数据手册里查到的。

pinctrl_lcdif_dat: lcdifdatgrp {  /* 24根数据线配置 */
    fsl,pins = <
        MX6UL_PAD_LCD_DATA00__LCDIF_DATA00  0x49
        MX6UL_PAD_LCD_DATA01__LCDIF_DATA01  0x49
        MX6UL_PAD_LCD_DATA02__LCDIF_DATA02  0x49
        MX6UL_PAD_LCD_DATA03__LCDIF_DATA03  0x49
        MX6UL_PAD_LCD_DATA04__LCDIF_DATA04  0x49
        MX6UL_PAD_LCD_DATA05__LCDIF_DATA05  0x49
        MX6UL_PAD_LCD_DATA06__LCDIF_DATA06  0x49
        MX6UL_PAD_LCD_DATA07__LCDIF_DATA07  0x49
        MX6UL_PAD_LCD_DATA08__LCDIF_DATA08  0x49
        MX6UL_PAD_LCD_DATA09__LCDIF_DATA09  0x49
        MX6UL_PAD_LCD_DATA10__LCDIF_DATA10  0x49
        MX6UL_PAD_LCD_DATA11__LCDIF_DATA11  0x49
        MX6UL_PAD_LCD_DATA12__LCDIF_DATA12  0x49
        MX6UL_PAD_LCD_DATA13__LCDIF_DATA13  0x49
        MX6UL_PAD_LCD_DATA14__LCDIF_DATA14  0x49
        MX6UL_PAD_LCD_DATA15__LCDIF_DATA15  0x49
        MX6UL_PAD_LCD_DATA16__LCDIF_DATA16  0x49
        MX6UL_PAD_LCD_DATA17__LCDIF_DATA17  0x49
        MX6UL_PAD_LCD_DATA18__LCDIF_DATA18  0x49
        MX6UL_PAD_LCD_DATA19__LCDIF_DATA19  0x49
        MX6UL_PAD_LCD_DATA20__LCDIF_DATA20  0x49
        MX6UL_PAD_LCD_DATA21__LCDIF_DATA21  0x49
        MX6UL_PAD_LCD_DATA22__LCDIF_DATA22  0x49
        MX6UL_PAD_LCD_DATA23__LCDIF_DATA23  0x49
    >;
};
pinctrl_lcdif_ctrl: lcdifctrlgrp { /* 4根控制线配置 */
    fsl,pins = <
        MX6UL_PAD_LCD_CLK__LCDIF_CLK	    0x49
        MX6UL_PAD_LCD_ENABLE__LCDIF_ENABLE  0x49
        MX6UL_PAD_LCD_HSYNC__LCDIF_HSYNC    0x49
        MX6UL_PAD_LCD_VSYNC__LCDIF_VSYNC    0x49
    >;
};
....
....
&lcdif {
	pinctrl-names = "default";
	pinctrl-0 = <&pinctrl_lcdif_dat         /* 使用到的IO */
		     &pinctrl_lcdif_ctrl>;
	display = <&display0>;
	status = "okay";

	display0: display {                     /* LCD属性信息 */
		bits-per-pixel = <24>;              /* 每个像素占用几个bit */
		bus-width = <24>;                   /* 总线宽度 */

		display-timings {
			native-mode = <&timing0>;       /* 时序信息 */
			timing0: timing0 {
			clock-frequency = <9000000>;    /* LCD像素时钟,单位Hz */
			hactive = <480>;                /* LCD X轴像素个数 */
			vactive = <272>;                /* LCD Y轴像素个数 */
			hfront-porch = <5>;             /* LCD hfp参数 */
			hback-porch = <40>;             /* LCD hbp参数 */
			hsync-len = <1>;                /* LCD hspw参数 */
			vback-porch = <8>;              /* LCD vbp参数 */
			vfront-porch = <8>;             /* LCD vfp参数 */
			vsync-len = <1>;                /* LCD vspw参数 */

			hsync-active = <0>;             /* hsync数据线极性 */
			vsync-active = <0>;             /* vsync数据线极性 */
			de-active = <1>;                /* de数据线极性 */
			pixelclk-active = <0>;          /* clk数据线极性 */
			};
		};
	};
};
2.泰山派

类似的,我们猜想泰山派也一定有一个用来描述soc公共资源的dtsi文件

tspi-rk3566-user-v10-linux.dts是泰山派的板级dts文件,它应该是会包含soc级的dtsi文件的

我们进入rk3566.dtsi文件

我们发现它又包含了rk3568.dtsi文件,在此基础上,又追加了一些信息。那我是不是可以理解为,rk3566的soc资源是在rk3568的基础上的一个扩展?

现在我们来看rk3568.dtsi这个文件,有如下节点:

dsi1: dsi@fe070000 {
    compatible = "rockchip,rk3568-mipi-dsi";
    reg = <0x0 0xfe070000 0x0 0x10000>;
    interrupts = <GIC_SPI 69 IRQ_TYPE_LEVEL_HIGH>;
    clocks = <&cru PCLK_DSITX_1>, <&cru HCLK_VO>, <&video_phy1>;
    clock-names = "pclk", "hclk", "hs_clk";
    resets = <&cru SRST_P_DSITX_1>;
    reset-names = "apb";
    phys = <&video_phy1>;
    phy-names = "mipi_dphy";
    power-domains = <&power RK3568_PD_VO>;
    rockchip,grf = <&grf>;
    #address-cells = <1>;
    #size-cells = <0>;
    status = "disabled";

    ports {
        #address-cells = <1>;
        #size-cells = <0>;

        dsi1_in: port@0 {
            reg = <0>;
            #address-cells = <1>;
            #size-cells = <0>;

            dsi1_in_vp0: endpoint@0 {
                reg = <0>;
                remote-endpoint = <&vp0_out_dsi1>;
                status = "disabled";
            };

            dsi1_in_vp1: endpoint@1 {
                reg = <1>;
                remote-endpoint = <&vp1_out_dsi1>;
                status = "disabled";
            };
        };
    };
};

dsi(MIPI_Display_Interface),这就是我们要找的mipi屏幕节点定义!这里是dsi1,其实还有一个dsi0,这就说明rk3566这块soc是支持两块mipi屏幕的,但这里我们只用一个。

根据compatible属性,我们同样可以找到对应的驱动文件

在这里插入图片描述

从查找的结果来看,基本可以定位drivers/gpu/drm/rockchip/dw-mipi-dsi.c就是mipi屏幕的驱动文件了!

分析完之后,我们来修改dsi1节点的追加信息,在源码的基础上,只需要修改三个地方就可以了:

1.修改lanes数

为什么要改这个,前面的分析也很清楚了,因为3.1寸屏幕有2对差分线,即数据是在两个通道传输的。

dsi,lanes  = <4>;
改为
dsi,lanes  = <2>;

2.配置初始化序列

这个应该是根据不同屏幕来修改的,具体的原理立创也给了教学,内容比较多,这里就不阐述了。

panel-init-sequence = [
    // init code
    05 78 01 01
    05 78 01 11
    39 00 06 FF 77 01 00 00 11
    15 00 02 D1 11
    15 00 02 55 B0 // 80 90 b0
    39 00 06 FF 77 01 00 00 10
    39 00 03 C0 63 00
    39 00 03 C1 09 02
    39 00 03 C2 37 08
    15 00 02 C7 00 // x-dir rotate 0:0x00,rotate 180:0x04
    15 00 02 CC 38
    39 00 11 B0 00 11 19 0C 10 06 07 0A 09 22 04 10 0E 28 30 1C
    39 00 11 B1 00 12 19 0D 10 04 06 07 08 23 04 12 11 28 30 1C
    39 00 06 FF 77 01 00 00 11 // enable  bk fun of  command 2  BK1
    15 00 02 B0 4D
    15 00 02 B1 60 // 0x56  0x4a  0x5b
    15 00 02 B2 07
    15 00 02 B3 80
    15 00 02 B5 47
    15 00 02 B7 8A
    15 00 02 B8 21
    15 00 02 C1 78
    15 00 02 C2 78
    15 64 02 D0 88
    39 00 04 E0 00 00 02
    39 00 0C E1 01 A0 03 A0 02 A0 04 A0 00 44 44
    39 00 0D E2 00 00 00 00 00 00 00 00 00 00 00 00
    39 00 05 E3 00 00 33 33
    39 00 03 E4 44 44
    39 00 11 E5 01 26 A0 A0 03 28 A0 A0 05 2A A0 A0 07 2C A0 A0
    39 00 05 E6 00 00 33 33
    39 00 03 E7 44 44
    39 00 11 E8 02 26 A0 A0 04 28 A0 A0 06 2A A0 A0 08 2C A0 A0
    39 00 08 EB 00 01 E4 E4 44 00 40
    39 00 11 ED FF F7 65 4F 0B A1 CF FF FF FC 1A B0 F4 56 7F FF
    39 00 06 FF 77 01 00 00 00
    15 00 02 36 00 //U&D  Y-DIR rotate 0:0x00,rotate 180:0x10
    15 00 02 3A 55
    05 78 01 11
    05 14 01 29
];

3.配置屏幕时序参数

这里和正点原子的是比较像的,主要是配置时序信息,可以参考屏幕的数据手册或直接问厂商要。

disp_timings1: display-timings {
    native-mode = <&dsi1_timing0>;
    dsi1_timing0: timing0 {
        clock-frequency = <27000000>;
        hactive = <480>;       //与 LCDTiming.LCDH 对应
        vactive = <800>;       //与 LCDTiming.LCDV 对应
        hfront-porch = <32>;   //与 LCDTiming.HFPD 对应
        hsync-len = <4>;       //与 LCDTiming.HSPW 对应
        hback-porch = <32>;    //与 LCDTiming.HBPD 对应
        vfront-porch = <9>;    //与 LCDTiming.VEPD 对应
        vsync-len = <4>;       //与 LCDTiming.VsPW 对应
        vback-porch = <3>;     //与 LCDTiming.VBPD 对应
        hsync-active = <0>;
        vsync-active = <0>;
        de-active = <0>;
        pixelclk-active = <0>;
    };
};

屏幕驱动框架区别

总结完这两款不同soc,不同开发板,不同型号屏幕的驱动之后,博主发现还是有很多区别的。博主对其中的一些细节进行了思考,并查阅了相关的资料,发现了最大的区别是框架上的区别。

之前跟着正点原子的教程学习,提到屏幕显示,自然而然就会想到FrameBuffer框架,即fb,原子的lcd显示驱动,就是基于这一框架的,它的驱动文件是drivers/video/fbdev/mxsfb.c。驱动里就是去注册fb设备,从而在/dev下生成一个fb0设备(也可能是其他fbx),应用层就是通过操作fb0设备来实现对屏幕的控制显示的。

但是博主发现在rk3566中,屏幕的驱动文件是drivers/gpu/drm/rockchip/dw-mipi-dsi.c,这个目录和原子的有很大区别,它是在gpu/drm/这样的目录下的,经过查资料,博主才知道,这是一种更为先进的DRM框架。

DRM框架可以和GPU驱动直接结合,支持硬件加速,还有一些其他的特点,总结来说,fb框架在未来基本会被DRM框架替代了,所以博主认为在接下来的时间去研究一下DRM相关的内容还是有必要的,比如去看一下dw-mipi-dsi.c的源码。

还有一个问题,就是fb框架注册设备后会在/dev下生成一个fb设备,那么DRM框架是不是也是类似的呢?不过之前博主是不知道有DRM框架这个东西的,就直接移植了原子的fb框架下的显示应用程序,发现也是能够通过操控fb设备来实现屏幕显示的,获取是做了一些兼容操作?这些博主后面会继续研究。

下一部分,博主会继续分析触摸驱动的框架,如果有哪里写的不对的地方,还请批评指正!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐