kernel版本:linux-4.4.94
CPU: X1600
LCD: 3.5 寸TFT(320×240),Model Name LQ035NC111
本⽂以芯⽚x1600, 开发板halley6⽬前使⽤的群创的spi lcd为例
添加⼀款新的lcd驱动可以分为两部分,添加背光驱动和添加lcd驱动。下边分别介绍添加两部分驱动的步骤。
1.kernel中添加pwm背光驱动
添加驱动⼀般需要添加两部分,⼀部分是设备树dts配置, 另⼀部分是驱动程序代码。
1.1 设备树dts配置
代码位置
kernel-4.4.94/arch/mips/boot/dts/ingenic/halley6_v10.dts
kernel-4.4.94/arch/mips/boot/dts/ingenic/halley6_lcd/RD_X1600_HALLEY6_RGB_SPI_LCD_1V0.dtsi
1 &pwm {
2 pinctrl-names = "default";
3 pinctrl-0 = <&pwm0_pc>; //这⾥需要查看原理图, 确定连接的pwm
4 status = "okay";
5 };
1 backlight {
2 compatible = "pwm-backlight";
3 pwms = <&pwm 0 1000000>; //这⾥需要注意pwm是⼏
4 brightness-levels = <0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15>;
5 default-brightness-level = <4>;
6 }
1.2 驱动程序代码
代码位置
drivers/video/backlight/pwm_bl.c
pws背光驱动代码⽆需更改。
1.3 kernel编译配置
1 Symbol: PWM_INGENIC_V3 [=y]
2 │ Type : bool
3 │ Defined at module_drivers/drivers/pwm/Kconfig:24
4 │ Prompt: Ingenic V3 PWM support
5 │ Depends on: SOC_X1600 [=y]
6 │ Location:
7 │ -> Ingenic device-drivers Configurations
8 │ -> [PWM] drivers
正确配置, 可以看到背光点亮。
2. kernel中添加lcd驱动
2.1 设备树dts配置
代码位置
kernel-4.4.94/arch/mips/boot/dts/ingenic/halley6_lcd/RD_X1600_HALLEY6_RGB_SPI_LCD_1V0.dtsi
/ {
display-dbi {
compatible = "simple-bus";
#interrupt-cells = <1>;
#address-cells = <1>;
#size-cells = <1>;
ranges = <>;
panel_lq035 {
compatible = "ingenic,lq035"; // 跟lcd驱动代码中compatible = "ingenic,lq035"; 保持⼀致
status = "okay";
//定义了数据pin, 定义位置:module_drivers/dts/x1600-pinctrl.dtsi
pinctrl-names = "default", "i2c-func", "sda-high", "sda-low", "sck-high", "sck-low";
pinctrl-0 = <&tft_lcd_pa_rgb888>; //lsun
pinctrl-1 = <&i2c1_pb_f2>;
pinctrl-2 = <&fw040_sda_out_high>;
pinctrl-3 = <&fw040_sda_out_low>;
pinctrl-4 = <&fw040_sck_out_high>;
pinctrl-5 = <&fw040_sck_out_low>;
//以下四个gpio根 据原理图
/* ingenic,lcd-pwm-gpio = <&gpc 0 GPIO_ACTIVE_HIGH INGENIC_GPIO_NOBIAS>;*/
ingenic,vdd-en-gpio = <&gpa 31 GPIO_ACTIVE_HIGH INGENIC_GPIO_NOBIAS>;
ingenic,rst-gpio = <&gpb 13 GPIO_ACTIVE_LOW INGENIC_GPIO_NOBIAS>;
ingenic,de-gpio = <&gpa 27 GPIO_ACTIVE_LOW INGENIC_GPIO_NOBIAS>; //lsun
ingenic,lcd-cs-gpio = <&gpb 14 GPIO_ACTIVE_LOW INGENIC_GPIO_NOBIAS>;
port {
panel_lq035_ep: endpoint {
remote-endpoint = <&dpu_out_ep>; //与下边dpu配置相对应
};
};
};
};
backlight {
compatible = "pwm-backlight";
pwms = <&pwm 0 1000000>;
brightness-levels = <0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15>;
default-brightness-level = <4>;
};
};
&dpu {
status = "okay";
port {
dpu_out_ep: endpoint {
remote-endpoint = <&panel_lq035_ep>; //与上边lcd配置相对应
¦ };
};
};
2.2 添加驱动程序代码
代码位置:module_drivers/drivers/video/fbdev/ingenic/fb_stage/displays/panel-lq035.c
2.2.1 lcd初始化代码添加
调试显⽰屏, ⼀般情况下屏⼚不会提供直接可⽤的驱动,lcd驱动需要在代码库中找⼀份lcd驱动进⾏改写, 屏⼚会提供显⽰屏的初始化代码, 根据屏⼚提供的初始化代码, 按照⼀定规则在驱动中填写初始化代码。
lq035这款屏的初始化代码如下:
void initial_3.5tft_spi(void)/*********3.5TFT****************/
{
write_SPI(0x001);
delayms(50);
write_SPI(0x011);
delayms(50);
write_SPI(0x003);
delayms(50);
write_SPI(0x020);
delayms(50);
write_SPI(0x036);
write_SPI(0x100);//0x1c0
write_SPI(0x038);
write_SPI(0x03A);
write_SPI(0x150);
write_SPI(0x0B0);
write_SPI(0x132);
write_SPI(0x0B1);
write_SPI(0x1AC);
write_SPI(0x0B2);
write_SPI(0x141);//0x141
write_SPI(0x0B3);
write_SPI(0x123);//0x123
write_SPI(0x0B4);
write_SPI(0x100);
write_SPI(0x0C2);
write_SPI(0x109); //0x109
write_SPI(0x0C3);
write_SPI(0x1dc);//0x1dc
write_SPI(0x0C4);
write_SPI(0x100);
write_SPI(0x0C5);
write_SPI(0x100);
write_SPI(0x0C9);
write_SPI(0x181);
write_SPI(0x0D4);
write_SPI(0x100);
write_SPI(0x0D5);
write_SPI(0x100);
write_SPI(0x0D6);
write_SPI(0x11c);
write_SPI(0x0D7);
write_SPI(0x117);
write_SPI(0x0D8);
write_SPI(0x100);
write_SPI(0x0D9);
write_SPI(0x104);
delayms(50);
write_SPI(0x0D2);
write_SPI(0x123);
write_SPI(0x129);
write_SPI(0x121);
write_SPI(0x121);
write_SPI(0x100);
write_SPI(0x102);
write_SPI(0x101);
write_SPI(0x12b);
write_SPI(0x136);
write_SPI(0x13f);
write_SPI(0x0D3);
write_SPI(0x125);//0x125
write_SPI(0x029);
delayms(50);
}
void write_SPI(unsigned int par)//9bit spi
{
unsigned char n;
LCD_CSB = 0;
LCD_SCL = 1;
for(n=0; n<9; n++)
{
LCD_SCL = 0;
LCD_SDA = (par>>(8-n))&0x01;
LCD_SCL = 1;
}
LCD_CSB = 1;
}
因为每个屏⼚给的初始化代码没有统⼀固定格式, 所以没有固定的改写⽅式, 这⾥只描述⼀下将初始化代码添加的位置
void write_SPI(unsigned int par) //9bit spi
{
unsigned char n;
//LCD_CSB = 0;
CS(0);
//LCD_SCL = 1;
SCK_HIGH();
for (n = 0; n < 9; n++) {
//LCD_SCL = 0;
SCK_LOW();
//LCD_SDA = (par >> (8 - n)) & 0x01;
if((par >> (8 - n)) & 0x01)
SDA_HIGH();
else
SDA_LOW();
//LCD_SCL = 1;
SCK_HIGH();
}
//LCD_CSB = 1;
CS(1);
}
void Initial_IC(void)
{
write_SPI(0x001);
mdelay(50);
write_SPI(0x011);
mdelay(50);
write_SPI(0x003);
mdelay(50);
write_SPI(0x020);
mdelay(50);
write_SPI(0x036);
write_SPI(0x100);//0x1c0
write_SPI(0x038);
write_SPI(0x03A);
write_SPI(0x150);
write_SPI(0x0B0);
write_SPI(0x132);
write_SPI(0x0B1);
write_SPI(0x1AC);
write_SPI(0x0B2);
write_SPI(0x141);//0x141
write_SPI(0x0B3);
write_SPI(0x123);//0x123
write_SPI(0x0B4);
write_SPI(0x100);
write_SPI(0x0C2);
write_SPI(0x109); //0x109
write_SPI(0x0C3);
write_SPI(0x1dc);//0x1dc
write_SPI(0x0C4);
write_SPI(0x100);
write_SPI(0x0C5);
write_SPI(0x100);
write_SPI(0x0C9);
write_SPI(0x181);
write_SPI(0x0D4);
write_SPI(0x100);
write_SPI(0x0D5);
write_SPI(0x100);
write_SPI(0x0D6);
write_SPI(0x11c);
write_SPI(0x0D7);
write_SPI(0x117);
write_SPI(0x0D8);
write_SPI(0x100);
write_SPI(0x0D9);
write_SPI(0x104);
mdelay(50);
write_SPI(0x0D2);
write_SPI(0x123);
write_SPI(0x129);
write_SPI(0x121);
write_SPI(0x121);
write_SPI(0x100);
write_SPI(0x102);
write_SPI(0x101);
write_SPI(0x12b);
write_SPI(0x136);
write_SPI(0x13f);
write_SPI(0x0D3);
write_SPI(0x125);//0x125
write_SPI(0x029);
mdelay(50);
}
2.2.2 lcd分辨率正确配置
LCD 的参数设定是需要根据LCD的手册来设定kernel-4.4.94/module_drivers/drivers/video/fbdev/ingenic/fb_stage/displays/panel-lq035.c里面的struct fb_videomode panel_modes[]结构体
例如从LQ035NC111的手册可以得到如下一个表
该表描述了该款并行LCD的所有时钟需求,在这里我参照的全是典型值“Typ”一栏
一个很具有参考价值的文档文件是Documentation/fb/framebuffer.txt文件,里面给我们描述了一个架构
对应我们 LQ035NC111 Spec上的时序如下
还有一个很有用的公式
Pixelclock = HTOTAL * VTOTAL * 60 /* HTOTAL是指Hsync Period,VTOTAL是指Vsync Period*/ 单位是HZ
pixclock = 1000000000000 / Pixelclock 单位是皮秒
再结合结构体
static struct fb_videomode panel_modes[] = {
[0] = {
.name = "lq035",
.refresh = 30,
.xres = 320,
.yres = 240,
// .pixclock = KHZ2PICOS(14000),
.pixclock = 125000,
.left_margin = 71,
.right_margin = 18,
.upper_margin = 14,
.lower_margin = 10,
.hsync_len = 2,
.vsync_len = 2,
.sync = FB_SYNC_HOR_HIGH_ACT & FB_SYNC_VERT_HIGH_ACT,
.vmode = FB_VMODE_NONINTERLACED,
.flag = 0,
},
};
static struct tft_config fw040_cfg = {
.pix_clk_inv = 1, //特别注意:PCLK的极性, x1600发出去的和lcd那边需要一致
.de_dl = 0,
.sync_dl = 0,
.vsync_dl = 0,
.color_even = TFT_LCD_COLOR_EVEN_RGB,
.color_odd = TFT_LCD_COLOR_ODD_RGB,
.mode = TFT_LCD_MODE_PARALLEL_888, //lsun
};
struct lcd_panel lcd_panel = {
.name = "lq035",
.num_modes = ARRAY_SIZE(panel_modes),
.modes = panel_modes,
.bpp = 24, //lsun
.width = 320,
.height = 240,
.lcd_type = LCD_TYPE_TFT,
.tft_config = &fw040_cfg,
.dither_enable = 0,
.dither.dither_red = 0,
.dither.dither_green = 0,
.dither.dither_blue = 0,
.ops = &panel_ops,
};
width、height的设定这个就没什么歧义了,对应320和240
bpp:其实我的这个LCD手册上说该屏是支持24位色的,但是这里填写16位,有空可以试试24位
显示一行时序为: HSPW -> HBPD -> 扫描数据 -> HFPD
垂直扫描一帧的时序: VSPW -> VBPD -> 扫描有效行 -> VFPD
其他的参数:其他参数对应第3节的表填写
xres <=> TEP <=> 320
yres <=> Tvd <=> 240
left_margin <=> Thf <=> 20 /* front porch /
right_margin <=> Thb <=> 38 / back porch /
hsync_len <=> THS <=> 30 / hsync width /
upper_margin <=> Tvf <=> 4 / front porch /
lower_margin <=> Tvb <=> 15 / back porch /
vsync_len <=> Tvs <=> 3 / vsync width */
tvp 即 VSPW
tvb 即 VBPD
tvf 即 VFPD
thp 即 HSPW
thb 即 HBPD
thf 即 HFPD
VBPD(vertical back porch):表示在一帧图像開始时,垂直同步信号以后的无效的行数
VFPD(vertical front porch):表示在一帧图像结束后,垂直同步信号曾经的无效的行数
VSPW(vertical sync pulse width):表示垂直同步脉冲的宽度,用行数计算
HBPD(horizontal back porch):表示从水平同步信号開始到一行的有效数据開始之间的VCLK的个数
HFPD(horizontal front porth):表示一行的有效数据结束到下一个水平同步信号開始之间的VCLK的个数
HSPW(horizontal sync pulse width):表示水平同步信号的宽度
按理说,对于一个已知尺寸(即水平显示尺寸HOZVAL和垂直显示尺寸LINEVAL已知)的LCD屏,仅仅要确定了VCLK值,行频和场频就应该知道了。但这样还不行的,由于在每一帧时钟信号中,还会有一些与屏显示无关的时钟出现,这就给确定行频和场频带来了一定的复杂性。如在HSYNC信号先后会有水平同步信号前肩(HFPD)和水平同步信号后肩(HBPD)出现,在VSYNC信号先后会有垂直同步信号前肩(VFPD)和垂直同步信号后肩(VBPD)出现,在这些信号时序内,不会有有效像素信号出现,另外HSYNC和VSYNC信号有效时,其电平要保持一定的时间,它们分别叫做水平同步信号脉宽HSPW和垂直同步信号脉宽VSPW,这段时间也不能有像素信号。因此计算行频和场频时,一定要包含这些信号。HBPD、HFPD和HSPW的单位是一个VCLK的时间,而VSPW、VFPD和VBPD的单位是扫描一行所用的时间。
LCD一般须要三个时序信号:VSYNC、HSYNC和VCLK。
VSYNC是垂直同步信号(帧同步信号),在每进行一个帧(即一个屏)的扫描之前,该信号就有效一次,由该信号能够确定LCD的场频,即每秒屏幕刷新的次数(单位Hz)。
HSYNC是水平同步信号(行同步信号),在每进行一行的扫描之前,该信号就有效一次,由该信号能够确定LCD的行频,即每秒屏幕从左到右扫描一行的次数(单位Hz)。
VCLK是像素时钟信号。
2.2.3 lcd控制pin的上电时序及操作⽅式
主要有哪些控制pin, 如何控制, 不同的屏有不同的⽅式, 需要查看<屏幕规格书>, <屏幕driverIC的⼿册>
驱动中解析dts中的控制pin:
static int of_panel_parse(struct device *dev)
{
struct panel_dev *panel = dev_get_drvdata(dev);
struct device_node *np = dev->of_node;
enum of_gpio_flags flags;
int ret = 0;
panel->vdd_en.gpio = of_get_named_gpio_flags(np, "ingenic,vdd-en-gpio", 0, &flags);
if(gpio_is_valid(panel->vdd_en.gpio)) {
panel->vdd_en.active_level = (flags & OF_GPIO_ACTIVE_LOW) ? 0 : 1;
ret = gpio_request_one(panel->vdd_en.gpio, GPIOF_DIR_OUT, "vdd_en");
if(ret < 0) {
dev_err(dev, "Failed to request vdd_en pin!\n");
return ret;
}
} else {
dev_warn(dev, "invalid gpio vdd_en.gpio: %d\n", panel->vdd_en.gpio);
}
panel->rst.gpio = of_get_named_gpio_flags(np, "ingenic,rst-gpio", 0, &flags);
if(gpio_is_valid(panel->rst.gpio)) {
panel->rst.active_level = (flags & OF_GPIO_ACTIVE_LOW) ? 0 : 1;
ret = gpio_request_one(panel->rst.gpio, GPIOF_DIR_OUT, "rst");
if(ret < 0) {
dev_err(dev, "Failed to request rst pin!\n");
goto err_request_rst;
}
} else {
dev_warn(dev, "invalid gpio rst.gpio: %d\n", panel->rst.gpio);
}
panel->pwm.gpio = of_get_named_gpio_flags(np, "ingenic,lcd-pwm-gpio", 0, &flags);
if(gpio_is_valid(panel->pwm.gpio)) {
panel->pwm.active_level = (flags & OF_GPIO_ACTIVE_LOW) ? 0 : 1;
ret = gpio_request_one(panel->pwm.gpio, GPIOF_DIR_OUT, "pwm");
if(ret < 0) {
dev_err(dev, "Failed to request pwm pin!\n");
goto err_request_pwm;
}
} else {
dev_warn(dev, "invalid gpio pwm.gpio: %d\n", panel->pwm.gpio);
}
#if 1
panel->de.gpio = of_get_named_gpio_flags(np, "ingenic,de-gpio", 0, &flags);
if(gpio_is_valid(panel->de.gpio)) {
panel->de.active_level = (flags & OF_GPIO_ACTIVE_LOW) ? 0 : 1;
ret = gpio_request_one(panel->de.gpio, GPIOF_DIR_OUT, "de");
if(ret < 0) {
dev_err(dev, "Failed to request de pin!\n");
goto err_request_de;
}
} else {
dev_warn(dev, "invalid gpio de.gpio: %d\n", panel->de.gpio);
}
#endif
panel->spi.cs = of_get_named_gpio_flags(np, "ingenic,lcd-cs-gpio", 0, &flags);
if(gpio_is_valid(panel->spi.cs)) {
ret = gpio_request_one(panel->spi.cs, GPIOF_DIR_OUT, "cs");
if(ret < 0) {
dev_err(dev, "Failed to request cs pin!\n");
goto err_request_cs;
}
} else {
dev_warn(dev, "invalid gpio spi.cs: %d\n", panel->spi.cs);
}
state->i2c = pinctrl_lookup_state(p, "i2c-func");
state->sda_high = pinctrl_lookup_state(p, "sda-high");
state->sda_low = pinctrl_lookup_state(p, "sda-low");
state->sck_high = pinctrl_lookup_state(p, "sck-high");
state->sck_low = pinctrl_lookup_state(p, "sck-low");
return 0;
err_request_cs:
if(gpio_is_valid(panel->spi.sck))
gpio_free(panel->spi.sck);
err_request_sck:
if(gpio_is_valid(panel->spi.sdo))
gpio_free(panel->spi.sdo);
err_request_sdo:
if(gpio_is_valid(panel->pwm.gpio))
gpio_free(panel->pwm.gpio);
err_request_pwm:
if(gpio_is_valid(panel->rst.gpio))
gpio_free(panel->rst.gpio);
err_request_de: //lsun
if(gpio_is_valid(panel->de.gpio))
gpio_free(panel->de.gpio);
err_request_rst:
if(gpio_is_valid(panel->vdd_en.gpio))
gpio_free(panel->vdd_en.gpio);
return ret;
}
不同的显⽰屏控制pin也有所不同, 上电时序也有变化, 所以需要具体来看。
驱动中控制pin的操作,上电时序:
#define POWER_IS_ON(pwr) ((pwr) <= FB_BLANK_NORMAL)
static int panel_set_power(struct lcd_device *lcd, int power)
{
struct panel_dev *panel = lcd_get_data(lcd);
struct board_gpio *vdd_en = &panel->vdd_en;
struct board_gpio *rst = &panel->rst;
struct board_gpio *pwm = &panel->pwm;
if(!POWER_IS_ON(power) && POWER_IS_ON(panel->power)) {
gpio_direction_output(vdd_en->gpio, 0);
gpio_direction_output(rst->gpio, 0);
}else{
if(gpio_is_valid(pwm->gpio)){
gpio_direction_output(pwm->gpio, 1);
msleep(180);
}
#if 0
gpio_direction_output(vdd_en->gpio, 0);
gpio_direction_output(rst->gpio, 1);
msleep(100);
gpio_direction_output(vdd_en->gpio, 1);
msleep(180);
gpio_direction_output(rst->gpio, 0);
msleep(20);
gpio_direction_output(rst->gpio, 1);
msleep(20);
#else
gpio_direction_output(vdd_en->gpio, 0);
gpio_direction_output(rst->gpio, 0);
msleep(100);
gpio_direction_output(vdd_en->gpio, 1);
msleep(10);
gpio_direction_output(rst->gpio, 1);
msleep(10);
#endif
Initial_IC();
pinctrl_select_state(p, state->i2c);
gpio_free(panel->spi.cs);
}
panel->power = power;
return 0;
}
以上就是新添加⼀款显⽰屏, ⼤致需要添加的配置和代码, 因为由于屏⼚提供的资料和每款显⽰ 屏的不同, 具体实现时会差别, 以上内容可作为参考。
注意
根据Spec的说明要使用Parallel RGB的565或者888的模式一定要确保硬件上sel2、sel1、sel0是接地的即下图的Pin48、Pin49、Pin50接地
其次,要确定下图中方的Pin52是接地使能,因为Spec中有明确说明
3.添加TP驱动
⽬前市⾯上使⽤居多的是显⽰+触摸⼀体的模组, 接下来介绍⼀下如何添加⼀款tp驱动.但我们这款屏不带触摸所以暂以其他屏做简单说明
tp⼚⼀般提供的资料⽐较全, 会提供完整的驱动, 所以添加tp驱动相对来说简单⼀些。
3.1 设备树dts配置
位置:module_drivers/dts/halley6_lcd/RD_X1600_HALLEY6_RGB_SLCD_1V0.dtsi
1 &i2c1 { //查看原理图, 确定tp使⽤的i2c
2 clock-frequency = <400000>;
3 pinctrl-names = "default";
4 status = "okay";
5 pinctrl-0 = <&i2c1_pb_f2>; //这⾥需要确定i2c pin是哪⼀组, 位置: module_drivers/dts/x1600-pinctrl.dtsi
6
7 goodix@0x38{ //以下是tp的配置, 这个⼀般会跟驱动配到提供, 根据原理图, 需要修改⼀下rst pin, int pin
8 compatible = "goodix,ft6236"; /* do not modify */
9 reg = <0x38>; /* do not modify */ //tp的i2c地址
10 interrupt-parent = <&gpa>; /* INT pin */ //interrupt pin使⽤的是哪⼀组gpio
11 interrupts = <9>;
12 reset-gpios = <&gpb 14 GPIO_ACTIVE_HIGH INGENIC_GPIO_NOBIAS>; /* RST pin */ //rst pin配置
13 irq-gpios = <&gpb 17 IRQ_TYPE_EDGE_FALLING INGENIC_GPIO_NOBIAS>; /* INT pin */ //int pin配置
14 touchscreen-max-x = <480>;
15 touchscreen-max-y = <800>;
16 touchscreen-invert-x = <0>;
17 touchscreen-invert-y = <0>;
18 ft6236,swap-x2y = <0>;
19 irq-flags = <2>; /* 1 rising, 2 falling */
20 };
21 };
以上pin的配置均需要从原理图上获得。
3.2 tp驱动代码添加
⼀般情况, tp驱动不需要做更改, 可以直接放到kernel代码结构中,
位置:module_drivers/drivers/input/touchscreen/
放到touchscreen⽂件夹下, 然后修改touchscreen⽂件下的Kconfig, Makefile,添加编译选项。
以上就是添加⼀款tp驱动的⽅法。