百问网驱动大全学习(一)LCD驱动
时间:2022-12-06 16:30:00
百问网驱动大全学习(1)LCD驱动
LCD驱动是由字符设备驱动的,但因为LCD驱动是一个常用的驱动程序,所以内核把框架性的一些东西给封装好了,它就是fbmem.c,它完成了一些字符设备的驱动框架,最终将通过fb_info与硬件相关的驱动程序调用结构体。
因此,我们必须写一个LCD驱动程序,要做的就是
1. 修改设备树 2. 分析驱动程序中的设备树 3. 分配、设置、注册fb_info结构体 4. 硬件相关操作
一、修改设备树
该设备的树节点相对固定,无需记忆stm32mp157c-100ask-512d-lcd-v1.dts
添加的设备节点如下
mylcd {
status = "okay"; compatible = "100ask,mylcd"; pinctrl-names = "default"; pinctrl-0 = <<dc_pins_a>; clocks = <&rcc LTDC_PX>; clock-names = "lcd"; backlight-gpios = <&gpioe 11 GPIO_ACTIVE_LOW>; reg = <0x5a001000 0x400>; display = <&display0>; display0: display {
bits-per-pixel = <16>; bus-width = <24>; display-timings {
native-mode = <&timing0>; timing0: timing0_1024x600 {
clock-frequency = <50000000>; hactive = <1024>; vactive = <600>; hfront-porch = <160>; hback-porch = <140/span>>; hsync-len = <20>; vback-porch = <20>; vfront-porch = <12>; vsync-len = <3>; hsync-active = <0>; vsync-active = <0>; de-active = <1>; pixelclk-active = <0>; }; }; }; };
这里要把原来的pannel节点状态设置为disabled,在文末会给出完整的设备树文件
这里也有需要注意的地方,背光引脚已经被使用了,我们需要禁用使用该引脚的节点
二、在驱动程序中解析设备树
添加了设备树节点,下一步我们要做的就是在驱动程序代码中解析设备树节点,获取硬件相关的信息。
static int mylcd_probe(struct platform_device *pdev)
{
struct resource *res;
struct device_node *display_np;
struct display_timings *timings = NULL;
struct display_timing *timing = NULL;
int size;
int width;
int bits_per_pixel;
display_np = of_parse_phandle(pdev->dev.of_node, "display", 0);
ret = of_property_read_u32(display_np, "bus-width", &width);
ret = of_property_read_u32(display_np, "bits-per-pixel", &bits_per_pixel);
timings = of_get_display_timings(display_np);
timing = timings->timings[timings->native_mode];
}
这样,我们就能够匹配到第一步添加的设备树节点,并可以从display_timing结构体变量中获得设备相关的时序、引脚极性等信息
三、分配、设置、注册一个fb_info结构体
(一)分配fb_info结构体
static struct fb_info *g_ptFbInfo = NULL;
static int mylcd_probe(struct platform_device *pdev)
{
...
g_ptFbInfo = framebuffer_alloc(0, NULL);
...
}
使用上面这几行简洁的代码就可以分配一个fb_info结构体了,并且会把它初始化为0
(二)设置fb_info结构体
设置fb_info结构体就比较重要了,主要有三个方面:
1. var可变参数
2. fix固定参数
3. fbops等其他变量
我们先来看看var可变参数要设置哪些吧
(1)fb_info.var可变参数
首先是LCD的分辨率和bpp,这两个属性我们在上面解析设备树时就已经获取到了,这里直接赋值即可
g_ptFbInfo->var.xres_virtual = g_ptFbInfo->var.xres = timing->hactive.typ;
g_ptFbInfo->var.yres_virtual = g_ptFbInfo->var.yres = timing->vactive.typ;
g_ptFbInfo->var.bits_per_pixel = bits_per_pixel;
这个xres_virtual和yres_virtual如果不设置,在fb-test时会无效参数的错误
然后是红绿蓝三原色的偏移和大小,也比较简单,根据bpp的值来配置
if(bits_per_pixel == 16)
{
g_ptFbInfo->var.red.offset = 11;
g_ptFbInfo->var.red.length = 5;
g_ptFbInfo->var.green.offset = 5;
g_ptFbInfo->var.green.length = 6;
g_ptFbInfo->var.blue.offset = 0;
g_ptFbInfo->var.blue.length = 5;
}
else if(bits_per_pixel == 24 || bits_per_pixel == 32)
{
g_ptFbInfo->var.red.offset = 16;
g_ptFbInfo->var.red.length = 8;
g_ptFbInfo->var.green.offset = 8;
g_ptFbInfo->var.green.length = 8;
g_ptFbInfo->var.blue.offset = 0;
g_ptFbInfo->var.blue.length = 8;
}
else
{
printk("don't support bits_per_pixel\n");
return -1;
}
这样,我们的可变参数就设置完毕了,是不是很简单。
(2)fb_info.fix固定参数
接下来是fix固定参数
fix.id
首先是fix.id,这个参数在register_framebuffer时会打印这个变量,如果不设置的话可能会打印乱码啥的,但是fb_info结构体在分配时清零了,这里不设置的话可能只会打印空字符串。
strcpy(g_ptFbInfo->fix.id, "100ask,lcd_drv");
fix.smem_len
接着是framebuffer的占用内存的长度,这个也很好理解,直接看代码,有一个需要注意的地方是,我们这个framebuffer的内存必须使用一块连续的物理内存
g_ptFbInfo->fix.smem_len = g_ptFbInfo->var.xres *
g_ptFbInfo->var.yres * g_ptFbInfo->var.bits_per_pixel / 8;
if(g_ptFbInfo->var.bits_per_pixel == 24)
g_ptFbInfo->fix.smem_len = g_ptFbInfo->var.xres *
g_ptFbInfo->var.yres * 4;
fix.line_length
然后是line_length,是framebuffer一行的长度,也很好理解,如果不设置这个,在使用fb_test时,也会报无效参数的错误
g_ptFbInfo->fix.line_length = g_ptFbInfo->var.xres * g_ptFbInfo->var.bits_per_pixel / 8;
if(g_ptFbInfo->var.bits_per_pixel == 24)
g_ptFbInfo->fix.line_length = g_ptFbInfo->var.xres * 4;
fix.smem_start
最后是设置framebuffer的物理地址和虚拟地址映射以及一些 类型信息
unsigned map_size;
dma_addr_t phy_dma;
map_size = PAGE_ALIGN(g_ptFbInfo->fix.smem_len);
g_ptFbInfo->screen_base = dma_alloc_wc(&pdev->dev, map_size, &phy_dma,
GFP_KERNEL);// framebuffer虚拟地址
g_ptFbInfo->fix.smem_start = phy_dma; // framebuffer物理地址
g_ptFbInfo->fix.type = FB_TYPE_PACKED_PIXELS;
g_ptFbInfo->fix.visual = FB_VISUAL_TRUECOLOR;
这里使用了一个新的函数dma_alloc_wc
,用这个函数就可以分配一块连续的物理内存,并且把这一块物理内存的起始地址保存在phy_dma变量中,并且返回这一块内存映射的虚拟地址,关于这一个函数具体的作用,我还是存在一些疑问,必应直接搜索这个函数得到的信息不是很多。
(3)fbops等其他变量
最后就是设置fbops等其他变量了
static unsigned int pseudo_palette[16];
static inline u_int chan_to_field(u_int chan, struct fb_bitfield *bf)
{
chan &= 0xffff;
chan >>= 16 - bf->length;
return chan << bf->offset;
}
static int mylcd_setcolreg(unsigned regno,
unsigned red, unsigned green, unsigned blue,
unsigned transp, struct fb_info *info)
{
unsigned int val;
/* dprintk("setcol: regno=%d, rgb=%d,%d,%d\n", regno, red, green, blue); */
switch (info->fix.visual) {
case FB_VISUAL_TRUECOLOR:
/* true-colour, use pseudo-palette */
if (regno < 16) {
u32 *pal = info->pseudo_palette;
val = chan_to_field(red, &info->var.red);
val |= chan_to_field(green, &info->var.green);
val |= chan_to_field(blue, &info->var.blue);
pal[regno] = val;
}
break;
default:
return 1; /* unknown type */
}
return 0;
}
static struct fb_ops lcd_drv_fbops = {
.owner = THIS_MODULE,
.fb_setcolreg = mylcd_setcolreg,
.fb_fillrect = cfb_fillrect,
.fb_copyarea = cfb_copyarea,
.fb_imageblit = cfb_imageblit,
};
g_ptFbInfo->fbops = &lcd_drv_fbops;
g_ptFbInfo->pseudo_palette = pseudo_palette;
fbops里面的mylcd_setcolreg是设置调色板函数,这里直接参考2410的代码复制过来修改的
并且在设置调色板函数里面,会调用到fb_info里的pseudo_palette,所以要给fb_info设置一个pseudo_palette
(三)注册fb_info结构体
搞定了这些,我们就要注册fb_info结构体了
ret = register_framebuffer(g_ptFbInfo);
四、硬件相关操作
在注册了fb_info结构体之后,要做的就是硬件相关的配置了
主要分为四个步骤
- 配置时钟
- 配置引脚
- 根据设备树节点配置寄存器
- 使能LCD控制器
(一)配置时钟
在设备树节点里面我们已经声明了需要使用的时钟
clocks = <&rcc LTDC_PX>;
clock-names = "lcd";
我们需要在驱动程序中使用内核提供的函数来使能这个时钟
static struct clk *g_ptFbClk = NULL;
g_ptFbClk = devm_clk_get(&pdev->dev, "lcd");
clk_set_rate(g_ptFbClk, timing->pixelclock.typ);
clk_prepare_enable(g_ptFbClk);
上面的四行代码所表达的含义已经很清晰了,需要注意的一点是设置时钟频率这里,我们使用的是第二步、解析设备树里面解析得到的display_timing结构体变量里的典型值
(二)配置引脚
这一步其实相对来说比较简单,在我们的设备树节点里使用了下面两行属性
pinctrl-names = "default";
pinctrl-0 = <<dc_pins_a>;
这个就是100ask STM32MP157板子的LCD控制器的引脚配置了,Linux的PinCtrl子系统已经帮我们配置好了
我们要做的就是配置背光引脚,在设备树节点里有背光引脚的声明
backlight-gpios = <&gpioe 11 GPIO_ACTIVE_LOW>;
使用的是PE11引脚,这就需要我们在代码中手动配置了,不过也简单
static struct gpio_desc *g_ptBLGpio = NULL;
g_ptBLGpio = gpiod_get(&pdev->dev, "backlight", GPIOD_OUT_LOW);
// 设置输入输出方向
gpiod_direction_output(g_ptBLGpio, 1);
// 设置输出低电平
gpiod_set_value(g_ptBLGpio, 0);
这样我们的背光引脚就配置好了
(三)根据设备树节点配置寄存器
这里已经是具体单板的寄存器操作了,只要根据解析设备树得到的display_timing结构体变量配置就好
分为两步:
1. 获取寄存器的物理地址,并且把物理地址映射成虚拟地址
2. 根据设备树配置具体的寄存器
(1)获取寄存器地址
我们在设备树节点里有这么一个属性
reg = <0x5a001000 0x400>;
这里保存的是我们157的LCD控制器的起始地址和大小,在驱动程序中通过如下几行代码获取
int size;
struct resource *res;
static struct stm32mp157_lcdif *g_ptLcdIf = NULL;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
size = resource_size(res);
g_ptLcdIf = devm_ioremap_resource(&pdev->dev, res);
通过这几行代码,我们就能够通过结构体变量访问到157LCD控制器里的寄存器了,这里的stm32mp157_lcdif结构体是157LCD控制器的寄存器结构体,会在附录中给出。
(2)根据设备树配置具体的寄存器
这一步我们修改100ask STM32MP157开发板的裸机LCD驱动里面的初始化函数,在probe函数中直接调用即可
lcd_controller_init(g_ptLcdIf, timing, width, bits_per_pixel, phy_dma);
static int lcd_controller_init(struct stm32mp157_lcdif *lcdif, struct display_timing *dt, int lcd_bpp, int fb_bpp, unsigned int fb_phy)
{
int bpp_mode;
int pol_vclk = 0;
int pol_vsync = 0;
int pol_hsync = 0;
int pol_de = 0;
/*[11:0]垂直同步信号宽度tvp,[27:16]水平同步信号宽度thp*/
lcdif->LTDC_SSCR = (dt->vsync_len.typ << 0) | (dt->hsync_len.typ << 16);
/*清空LTDC_BPCR寄存器*/
lcdif->LTDC_BPCR = 0 ;
/*[11:0] VSYNC宽度tvp + 上黑框tvb - 1*/
lcdif->LTDC_BPCR |= (dt->vsync_len.typ + dt->vback_porch.typ - 1) << 0 ;
/*[27:16]HSYNC宽度thp + 左黑框thb - 1*/
lcdif->LTDC_BPCR |= (dt->hsync_len.typ + dt->hback_porch.typ - 1) << 16;
/*清空LTDC_AWCR寄存器*/
lcdif->LTDC_AWCR = 0 ;
/*[11:0] VSYNC宽度tvp + 上黑框tvb + 垂直有效高度yres - 1*/
lcdif->LTDC_AWCR |= (dt->vsync_len.typ + dt->vback_porch.typ + dt->vactive.typ - 1) << 0;
/*[27:16] HSYNC宽度thp + 左黑框thb + 水平有效高度xres - 1*/
lcdif->LTDC_AWCR |= (dt->hsync_len.typ + dt->hback_porch.typ + dt->hactive.typ - 1) << 16;
/*清空LTDC_TWCR寄存器*/
lcdif->LTDC_TWCR = 0;
/*[11:0] VSYNC宽度tvp + 上黑框tvb + 垂直有效高度yres + 下黑框tvf - 1 , 即垂直方向上的总周期*/
lcdif->LTDC_TWCR |= (dt->vsync_len.typ + dt->vback_porch.typ + dt->vactive.typ + dt->vfront_porch.typ - 1) << 0;
/*[27:16] HSYNC宽度thp + 左黑框thb + 垂直有效高度xres + 右黑框thf - 1 , 即水平方向上的总周期*/
lcdif->LTDC_TWCR |= (dt->hsync_len.typ + dt->hback_porch.typ + dt->hactive.typ + dt->hfront_porch.typ - 1) << 16;
// vclk 上升沿有效
if(dt->flags & DISPLAY_FLAGS_PIXDATA_POSEDGE)
pol_vclk = 1;
if(dt->flags & DISPLAY_FLAGS_DE_HIGH)
pol_de = 1;
if(dt->flags & DISPLAY_FLAGS_VSYNC_HIGH)
pol_vsync = 1;
if(dt->flags & DISPLAY_FLAGS_HSYNC_HIGH)
pol_hsync = 1;
/*清空LTDC_GCR寄存器*/
lcdif->LTDC_GCR &= ~(0xF << 28);
/* 1 : DOTCLK下降沿有效 ,根据屏幕配置文件将其设置为1 */
lcdif->LTDC_GCR |= pol_vclk << 28;
/* 1 : ENABLE信号高电平有效,根据屏幕配置文件将其设置为1 */
lcdif->LTDC_GCR |= pol_de << 29;
/* 0 : VSYNC低电平有效 ,根据屏幕配置文件将其设置为0 */
lcdif->LTDC_GCR |= pol_vsync << 30 ;
/* 0 : HSYNC低电平有效 , 根据屏幕配置文件将其设置为0 */
lcdif->LTDC_GCR |= pol_hsync << 31 ;
/*layer 1的相关设置如下*/
lcdif->LTDC_L1WHPCR |= (dt->hsync_len.typ + dt->hback_porch.typ + dt->hactive.typ - 1) << 16 | (dt->hsync_len.typ + dt->hback_porch.typ ) ;
lcdif->LTDC_L1WVPCR |= (dt->vsync_len.typ + dt->vback_porch.typ + dt->vactive.typ - 1) << 16 | (dt->vsync_len.typ + dt->vback_porch.typ ) ;
lcdif->LTDC_L1CFBLR = (dt->hactive.typ * (fb_bpp>>3) + 7) | (dt->hactive.typ * (fb_bpp>>3))<< 16;
lcdif->LTDC_L1CFBLNR = dt->vactive.typ;/*显存总共的行数*/
/*透明度填充值,当选的bpp格式是ARGB8888,ARGB1555等会使用到,如选的是RGB565,RBG888等者不设置也可以*/
lcdif->LTDC_L1CACR = 0xff;
/* *BC = BF1 x C + BF2 x Cs *BF1为LTDC_L1BFCR设置的[10:8]值,设置为100:constant alpha即LTDC_L1CACR设置的值0xff,表示完全不透明 *BF2为LTDC_L1BFCR设置的[2:0]值,设置为101:constant alpha即LTDC_L1CACR设置的值0xff,表示完全不透明 *C为当前层的颜色, *Cs为背景色,不设置,默认值为0,即黑色 *LTDC_L1BFCR寄存器也是针对有透明度的像素格式而设置,如用RGB565等也可不设置 */
lcdif->LTDC_L1BFCR = (4<<8) | (5<<0);
/*当bpp为16时,数据格式为RGB565 , 当bpp为32时,数据格式为ARGB8888*/
switch(fb_bpp)
{
case 16:{
bpp_mode = 0x2;break;}
case 32:{
bpp_mode = 0x0;break;}
default:{
bpp_mode = 0x0;break;}
}
lcdif->LTDC_L1PFCR = 0 ;
lcdif->LTDC_L1PFCR |= bpp_mode; /*设置像素格式*/
lcdif->LTDC_L1CFBAR = fb_phy; /*设置显存地址*/
lcdif->LTDC_L1CR |= 0x1;/*1 layer 使能*/
return 0;
}
(四)使能LCD控制器
这里也使用100ask提供的裸机代码,也很简单
static void Stm32mp157_lcd_controller_enable(struct stm32mp157_lcdif *lcdif)
{
lcdif->LTDC_SRCR |= 1; /*加载LAYER的参数*/
lcdif->LTDC_GCR |= 1<<0; /* 使能STM32MP157的LCD控制器 */
}
在probe函数中直接调用就可以使能LCD控制器了
Stm32mp157_lcd_controller_enable(g_ptLcdIf);
附录一、设备树文件 stm32mp157c-100ask-512d-lcd-v1.dts
// SPDX-License-Identifier: (GPL-2.0+ OR BSD-3-Clause) /* * Copyright (C) 100ASK 2020 - All Rights Reserved * Author: 100ask * support: weidongshan@qq.com */ /dts-v1/; #include "stm32mp157c-100ask-512d-v1.dts" / { model = "100ASK YA157C v2 www.100ask.com"; compatible = "st,stm32mp157c-100ask-512d-v1",