Linux驱动之LCD驱动编写

时间:2021-06-03 10:02:57

Linux驱动之内核自带的S3C2440的LCD驱动分析这篇博客中已经分析了编写LCD驱动的步骤,接下来就按照这个步骤来字尝试字节编写LCD驱动。用的LCD屏幕为tft屏,每个像素点为16bit。对应与红绿蓝分别为565。

1、分配一个fb_info结构

2、设置fb_info结构

3、硬件相关的操作,配置LCD时钟、配置IO端口、配置LCD寄存器。

4、最终注册fbinfo结构到registered_fb数组

要理解LCD的工作原理,需要了解LCD的时钟,在TFT的LCD中有如下的时钟。这个几个时钟数据在配置LCD寄存器时都说需要设置的。

1、VCLK:两个像素之间的时钟,即两个像素隔多长时间才能显示下一个像素

2、HSYNC:水平同步时钟,即第一行像素点显示完成之后隔多长时间才能开始下一行的显示

3、VSYNC:垂直方向的同步时钟,也叫帧同步信号,即一帧数据显示完成之后(一帧数据表示一个屏幕显示完成,即一个显存的数据全部取完),过多长下一帧数据才开始显示

Linux驱动之LCD驱动编写

本节需要用到的函数:

void *dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp);  //分配DMA缓存区给显存
//返回值为:申请到的DMA缓冲区的虚拟地址,若为NULL,表示分配失败,则需要使用dma_free_writecombine()释放内存,避免内存泄漏
//参数如下: //*dev:指针,这里填0,表示这个申请的缓冲区里没有内容 //size:分配的地址大小(字节单位) //*handle:申请到的物理起始地址 //gfp:分配出来的内存参数,标志定义在<linux/gfp.h>,常用标志如下:
//GFP_ATOMIC 用来从中断处理和进程上下文之外的其他代码中分配内存. 从不睡眠.
//GFP_KERNEL 内核内存的正常分配. 可能睡眠.
//GFP_USER 用来为用户空间页来分配内存; 它可能睡眠.

分配一段DMA缓存区,分配出来的内存会禁止cache缓存(因为DMA传输不需要CPU)

它和 dma_alloc_coherent ()函数相似,不过 dma_alloc_coherent ()函数是分配出来的内存会禁止cache缓存以及禁止写入缓冲区

dma_free_writecombine(dev,size,cpu_addr,handle);   //释放缓存
//cpu_addr:虚拟地址,
//handle:物理地址

释放DMA缓冲区, dev和size参数和上面的一样

struct fb_info *framebuffer_alloc(size_t size, struct device *dev);      //申请一个fb_info结构体,
//size:额外的内存,
//*dev:指针, 这里填0,表示这个申请的结构体里没有内容
int register_framebuffer(struct fb_info *fb_info);  

                      //向内核中注册fb_info结构体,若内存不够,注册失败会返回负数

int unregister_framebuffer(struct fb_info *fb_info) ;

                      //注销内核中fb_info结构体

本节需要用到的结构体:

fb_info结构体如下:

struct fb_info {
... ...
struct fb_var_screeninfo var; //可变的参数
struct fb_fix_screeninfo fix; //固定的参数
... ...
struct fb_ops *fbops; //操作函数
... ...
char __iomem *screen_base; //显存虚拟起始地址
unsigned long screen_size; //显存虚拟地址长度 void *pseudo_palette;
//假的16色调色板,里面存放了16色的数据,可以通过8bpp数据来找到调色板里面的16色颜色索引值,模拟出16色颜色来,节省内存,不需要的话就指向一个不用的数组即可
... ...
};

其中操作函数fb_info-> fbops 结构体写法如下:

static struct fb_ops s3c_lcdfb_ops = {
.owner = THIS_MODULE, .fb_setcolreg = my_lcdfb_setcolreg,//设置调色板fb_info-> pseudo_palette,自己构造该函数 .fb_fillrect = cfb_fillrect, //填充矩形,用/drivers/video/ cfbfillrect.c里的函数即可 .fb_copyarea = cfb_copyarea, //复制数据, 用/drivers/video/cfbcopyarea.c里的函数即可 .fb_imageblit = cfb_imageblit, //绘画图形, 用/drivers/video/imageblit.c里的函数即可
};

固定的参数fb_info-> fix 结构体如下:

struct fb_fix_screeninfo {
char id[]; //id名字
unsigned long smem_start; //framebuffer物理起始地址
__u32 smem_len; //framebuffer长度,字节为单位
__u32 type; //lcd类型,默认值0即可
__u32 type_aux; //附加类型,为0
__u32 visual; //画面设置,常用参数如下
// FB_VISUAL_MONO01 0   单色,0:白色,1:黑色
// FB_VISUAL_MONO10 1   单色,1:白色,0:黑色
// FB_VISUAL_TRUECOLOR 2 真彩(TFT:真彩)
// FB_VISUAL_PSEUDOCOLOR     3 伪彩
// FB_VISUAL_DIRECTCOLOR 4 直彩     __u16 xpanstep; /*如果没有硬件panning就赋值为0 */
    __u16 ypanstep; /*如果没有硬件panning就赋值为0 */
    __u16 ywrapstep; /*如果没有硬件ywrap就赋值为0 */     __u32 line_length; /*一行的字节数 ,例:(RGB565)240*320,那么这里就等于240*16/8 */     /*以下成员都可以不需要*/
    unsigned long mmio_start; /*内存映射IO的起始地址,用于应用层直接访问寄存器,可以不需要*/
__u32 mmio_len; /* 内存映射IO的长度,可以不需要*/
__u32 accel;
__u16 reserved[]; };

可变的参数fb_info-> var 结构体如下:

structfb_var_screeninfo{
   __u32xres; /*可见屏幕一行有多少个像素点*/
__u32 yres; /*可见屏幕一列有多少个像素点*/
__u32 xres_virtual; /*虚拟屏幕一行有多少个像素点 */
__u32 yres_virtual; /*虚拟屏幕一列有多少个像素点*/
__u32 xoffset; /*虚拟到可见屏幕之间的行偏移,若可见和虚拟的分辨率一样,就直接设为0*/
__u32 yoffset; /*虚拟到可见屏幕之间的列偏移*/
__u32 bits_per_pixel; /*每个像素的位数即BPP,比如:RGB565则填入16*/
__u32 grayscale; /*非0时,指的是灰度,真彩直接填0即可*/ struct fb_bitfield red; //fb缓存的R位域, fb_bitfield结构体成员如下:
//__u32 offset; 区域偏移值,比如RGB565中的R,就在第11位
//__u32 length; 区域长度,比如RGB565的R,共有5位
//__u32 msb_right; msb_right ==0,表示数据左边最大, msb_right!=0,表示数据右边最大 struct fb_bitfield green; /*fb缓存的G位域*/
struct fb_bitfield blue; /*fb缓存的B位域*/    /*以下参数都可以不填,默认为0*/
struct fb_bitfield transp; /*透明度,不需要填0即可*/ __u32nonstd; /* != 0表示非标准像素格式*/
__u32 activate; /*设为0即可*/
__u32height; /*外设高度(单位mm),一般不需要填*/
__u32width; /*外设宽度(单位mm),一般不需要填*/
__u32 accel_flags; /*过时的参数,不需要填*/ /* 除了pixclock本身外,其他的都以像素时钟为 单位*/
__u32pixclock; /*像素时钟(皮秒)*/
__u32 left_margin; /*行切换,从同步到绘图之间的延迟*/
__u32right_margin; /*行切换,从绘图到同步之间的延迟*/
__u32upper_margin; /*帧切换,从同步到绘图之间的延迟*/
__u32lower_margin; /*帧切换,从绘图到同步之间的延迟*/
__u32hsync_len; /*水平同步的长度*/
__u32 vsync_len; /*垂直同步的长度*/
__u32 sync;
__u32 vmode;
__u32 rotate;
__u32reserved[]; /*保留*/ }

1.写驱动程序:

(驱动设置:参考自带的LCD平台驱动drivers/video/s3c2410fb.c )

(LCD控制寄存器设置:参考之前的LCD裸机驱动:http://www.cnblogs.com/lifexy/p/7144890.html)

1.1 步骤如下:

在驱动init入口函数中:

1)分配一个fb_info结构体

2)设置fb_info

  2.1)设置固定的参数fb_info-> fix

  2.2) 设置可变的参数fb_info-> var

  2.3) 设置操作函数fb_info-> fbops

  2.4) 设置fb_info 其它的成员

3)设置硬件相关的操作

  3.1)配置LCD引脚

  3.2)根据LCD手册设置LCD控制器

  3.3)分配显存(framebuffer),把地址告诉LCD控制器和fb_info

4)开启LCD,并注册fb_info: register_framebuffer()

  4.1) 直接在init函数中开启LCD(后面讲到电源管理,再来优化)

    控制LCDCON5允许PWREN信号,

    然后控制LCDCON1输出PWREN信号,

    输出GPB0高电平来开背光,

  4.2) 注册fb_info

在驱动exit出口函数中:

1)卸载内核中的fb_info

2) 控制LCDCON1关闭PWREN信号,关背光,iounmap注销地址

3)释放DMA缓存地址dma_free_writecombine()

4)释放注册的fb_info

1.2 具体代码如下:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <asm/io.h> //含有iomap函数iounmap函数
#include <asm/uaccess.h>//含有copy_from_user函数
#include <linux/device.h>//含有类相关的处理函数
#include <linux/fb.h> //含有fb_info结构体定义
//#include <asm/dma-mapping.h> //含有dma_free_writecombine宏定义
#include <linux/dma-mapping.h> //含有dma_free_writecombine宏定义
#include <linux/platform_device.h>//含有平台设备总线模型相关变量
#include <linux/mm.h>
#include <linux/slab.h> //#include <linux/module.h>
//#include <linux/kernel.h>
//#include <linux/errno.h>
//#include <linux/string.h>
//#include <linux/mm.h>
//#include <linux/slab.h>
//#include <linux/delay.h>
//#include <linux/fb.h>
//#include <linux/init.h>
//#include <linux/dma-mapping.h>
//#include <linux/interrupt.h>
//#include <linux/workqueue.h>
//#include <linux/wait.h>
//#include <linux/platform_device.h>
//#include <linux/clk.h> //#include <asm/io.h>
//#include <asm/uaccess.h>
//#include <asm/div64.h> //#include <asm/mach/map.h>
//#include <asm/arch/regs-lcd.h>
//#include <asm/arch/regs-gpio.h>
//#include <asm/arch/fb.h> /*lcd控制寄存器放在一个结构体里面*/
struct lcd_regs {
unsigned long lcdcon1;
unsigned long lcdcon2;
unsigned long lcdcon3;
unsigned long lcdcon4;
unsigned long lcdcon5;
unsigned long lcdsaddr1;
unsigned long lcdsaddr2;
unsigned long lcdsaddr3;
unsigned long redlut;
unsigned long greenlut;
unsigned long bluelut;
unsigned long reserved[];
unsigned long dithmode;
unsigned long tpal;
unsigned long lcdintpnd;
unsigned long lcdsrcpnd;
unsigned long lcdintmsk;
unsigned long lpcsel;
}; static struct fb_info *s3c_mylcdfb_info;//fb_info结构体
static volatile unsigned long *gpbcon;//GPB0用于lcd背光的控制
static volatile unsigned long *gpbdat;//GPB0用于lcd背光的控制
static volatile unsigned long *gpccon;
static volatile unsigned long *gpdcon;
static volatile unsigned long *gpgcon;//GPG4用于lcd电源
static volatile struct lcd_regs* lcd_regs;//lcd寄存器
static u32 pseudo_palette[]; //调色板内存 /* from pxafb.c */
static inline unsigned int chan_to_field(unsigned int chan, struct fb_bitfield *bf)
{
chan &= 0xffff; //取出16bit的数据
chan >>= - bf->length; //
return chan << bf->offset;
} static int s3c_mylcdfb_setcolreg(unsigned int regno, unsigned int red,
unsigned int green, unsigned int blue,
unsigned int transp, struct fb_info *info)
{
unsigned int val; if (regno > )
return ; /* 用red,green,blue三原色构造出val */
val = chan_to_field(red, &info->var.red);
val |= chan_to_field(green, &info->var.green);
val |= chan_to_field(blue, &info->var.blue); //((u32 *)(info->pseudo_palette))[regno] = val;
pseudo_palette[regno] = val;
return ;
} static struct fb_ops s3c_mylcdfb_ops = { //操作函数结构体
.owner = THIS_MODULE,
.fb_setcolreg = s3c_mylcdfb_setcolreg,//待会设置,这个是调色板,如果使用小于16bit的像素需要用到
.fb_fillrect = cfb_fillrect,
.fb_copyarea = cfb_copyarea,
.fb_imageblit = cfb_imageblit,
}; static int lcd_drv_init(void)
{
/*1、分配一个fb_info*/
s3c_mylcdfb_info = framebuffer_alloc(,NULL);//size为额外分配的大小,这里不需要,所以设为0
if(s3c_mylcdfb_info==NULL)
{
printk("unframebuffer_alloc\n");
return ;
} /*2、设置*/
/*2.1 设置固定的参数*/
strcpy(s3c_mylcdfb_info->fix.id, "mylcd");//名字
//s3c_mylcdfb_info->fix.smem_start = ;//显存的物理起始地址,后面设置
s3c_mylcdfb_info->fix.smem_len = **/;//单位为字节,每个像素点占用16bit :565,显存的大小
s3c_mylcdfb_info->fix.type = FB_TYPE_PACKED_PIXELS;//LCD类型,填充像素的类型 tft
//s3c_mylcdfb_info->fix.type_aux= //附加的LCD类型,不需要设置
s3c_mylcdfb_info->fix.visual = FB_VISUAL_TRUECOLOR;//视觉类型,选择真彩色
s3c_mylcdfb_info->fix.line_length = */; //一行的长度,单位为字节
// s3c_mylcdfb_info->fix.mmio_start = //控制lcd的寄存器的物理地址
// s3c_mylcdfb_info->fix.mmio_len = //控制lcd的寄存器的大小 /*2.2 设置可变的参数*/
s3c_mylcdfb_info->var.xres = ;//x方向的分辨率
s3c_mylcdfb_info->var.yres = ;//y方向的分辨率
s3c_mylcdfb_info->var.xres_virtual = ;//x方向的虚拟分辨率
s3c_mylcdfb_info->var.yres_virtual = ;//y方向的虚拟分辨率
s3c_mylcdfb_info->var.bits_per_pixel = ;//每个像素的大小,单位为bit
s3c_mylcdfb_info->var.grayscale = ;//灰度值 s3c_mylcdfb_info->var.red.length = ;//红色像素占用的长度,单位bit
s3c_mylcdfb_info->var.green.length = ;//绿色像素占用的长度,单位bit
s3c_mylcdfb_info->var.blue.length = ;//蓝色像素占用的长度,单位bit s3c_mylcdfb_info->var.red.offset= ;//红色像素在16bit中的偏移值
s3c_mylcdfb_info->var.green.offset= ;//绿色像素在16bit中的偏移值
s3c_mylcdfb_info->var.blue.offset=;//蓝色像素在16bit中的偏移值 s3c_mylcdfb_info->var.red.msb_right= ;//低位在前还是高位在前,一般高位在前,也就是小端模式
s3c_mylcdfb_info->var.green.msb_right= ;
s3c_mylcdfb_info->var.blue.msb_right=; s3c_mylcdfb_info->var.activate = FB_ACTIVATE_NOW;//使用默认参数,显存立刻生效 /*2.3 设置操作函数*/
s3c_mylcdfb_info->fbops = &s3c_mylcdfb_ops; /*2.4 其它的一些设置 */
s3c_mylcdfb_info->pseudo_palette = pseudo_palette;//调色板的地址
//s3c_mylcdfb_info->screen_base = ;//显存的虚拟基地址
s3c_mylcdfb_info->screen_size = **/;//单位为字节,每个像素点占用16bit :565,显存的大小 /*3、硬件相关的操作 */
/*3.1、配置GPIO用于LCD*/
gpbcon = ioremap(0x56000010, );//将实际的寄存器地址转换为虚拟地址
gpccon = ioremap(0x56000020 , );
gpdcon = ioremap(0x56000030 , );
gpgcon = ioremap(0x56000060 , );
gpbdat = gpbcon + ; *gpccon = 0xaaaaaaaa; /* GPIO管脚用于VD[7:0],LCDVF[2:0],VM,VFRAME,VLINE,VCLK,LEND */
*gpdcon = 0xaaaaaaaa; /* GPIO管脚用于VD[23:8] */ *gpbcon &= ~(); /* GPB0设置为输出引脚 */
*gpbcon |= ; *gpbdat &= ~; /* 输出低电平关闭LCD背光 */ *gpgcon |= (<<); /* GPG4用作LCD_PWREN 电源*/ /*3.2、根据LCD手册设置LCD控制器,比如VCLK的频率等 */
lcd_regs = ioremap(0X4D000000 , sizeof(struct lcd_regs)); /*
* bit[17:8] : VCLK = HCLK / [(CLKVAL+1) x 2]
* 10M = 100M/[(CLKVAL+1) x 2]
* CLKVAL = 4
*
* bit[6:5] :PNRMODE = 11显示模式,选择TFT模式
*
* bit[4:1] :BPPMODE = 1100;像素=16bit 565
*
* bit[0] :ENVID = 0;先关闭LCD控制器
*/
lcd_regs->lcdcon1 = (<<) | (<<) | (0x0c<<);// /*
* [31:24] : VBPD = 帧同步信号发出后,过多长时间开始显示数据,单位为行,理解为1行的时间
* 看LCD手册tvb = VBPD + 1 = 2;所以VBPD = 1
*
* [23:14]:LINEVAL + 1= 272;,所以LINEVAL = 271;垂直方向尺寸,多少行
*
* [13:6]:VFPD = 一帧的数据传输完成之后,过多长时间开始下一帧数据的帧同步信号,单位为行,理解为1行的时间
* 看LCD手册tvf = VFPD + 1 = 2;所以VFPD = 1
*
* [5:0]:VSPW = 帧同步信号的脉冲宽度,单位为行
* 看LCD手册tvp = VSPW + 1 =10;所以VSPW = 9
*/
lcd_regs->lcdcon2 = (<<) | (<<) | (<<) | (<<); /*
* [25:19]:HBPD = 行同步信号发出后,经过多少个VCLK,才发送像素的数据,单位为VCLK
* 看LCD手册thb = HBPD + 1 = 2;所以HBPD=1
*
* [18:8]:HOZVAL + 1 = 480,所以 HOZVAL = 479;水平方向尺寸,多少列
*
*[7:0]:HFPD = 一行的像素数据传输完成之后,经过多长时间,才能发送下一个行同步信号,单位为VCLK
*看LCD手册thf = HFPD + 1 = 2;所以HFPD = 1;
*/
lcd_regs->lcdcon3 = (<<) | (<<) | (<<); /*
* [7:0]:HSPW = 行同步信号的脉冲宽度,单位为VCLK
* 看LCD手册thp = HSPW + 1 = 41;所以HSPW = 40
*
*/
lcd_regs->lcdcon4 = (<<); /*
* [11] :FRM565 = 1;16位模式的格式 R:G:B = 5:6:5
* [10] :INVVCLK = 0;VCLK在哪个边沿取数据 = 0表示下降沿取数据
* [9] :INVVLINE = 1;行同步信号是否需要反转= 1需要反转
* [8] :INVVFRAME = 1;帧同步信号是否需要反转= 1需要反转
* [7] :INVVD = 0; 数据是否需要反转
* [6] :INVVDEN = 0; 数据使能信号是否需要反转
* [5] :INVPWREN = 0;电源使能信号是否需要反转
* [4] :INVLEND = 0;行结束信号是否需要反转
* [3] :PWREN = 0;电源使能信号,先不使能
* [2] :ENLEND = 1;//行结束信号先使能
* [1:0] :BSWP 、HWSWP = 0 1;字节内部不需要交换,字节间需要交换
*/
lcd_regs->lcdcon5= (<<) | (<<) | (<<) | (<<); /*3.3、显存和调色板设置 */ /*
*利用dma_alloc_writecombine分配一块连续的显存
*/
s3c_mylcdfb_info->screen_base = dma_alloc_writecombine(NULL,s3c_mylcdfb_info->screen_size,(&(s3c_mylcdfb_info->fix.smem_start)),GFP_KERNEL);//返回虚拟地址
if(s3c_mylcdfb_info->screen_base==NULL) //如果显存分配失败,直接返回
{
printk("undma_alloc_writecombine\n");
return ;
} /*
*将显存的地址告诉LCD控制器(物理地址)
*/
lcd_regs->lcdsaddr1 = (s3c_mylcdfb_info->fix.smem_start >> ) & (~(<<));//起始地址
lcd_regs->lcdsaddr2 = ((s3c_mylcdfb_info->fix.smem_start + s3c_mylcdfb_info->screen_size) >> ) & 0x1fffff;//结束地址
lcd_regs->lcdsaddr3 = (*/); /* 一行的长度(单位: 2字节) */ //s3c_lcd->fix.smem_start = xxx; /* 显存的物理地址 */
/* 启动LCD */
lcd_regs->lcdcon1 |= (<<); /* 使能LCD控制器 */
lcd_regs->lcdcon5 |= (<<); /* 使能LCD本身电源 */
*gpbdat |= ; /* 输出高电平, 使能背光 */ /*4、注册LCD*/
register_framebuffer(s3c_mylcdfb_info);
printk("register_framebuffer\n");
return ;
} static void lcd_drv_exit(void)
{
unregister_framebuffer(s3c_mylcdfb_info);
lcd_regs->lcdcon1 &= ~(<<); /* 关闭LCD本身 */
*gpbdat &= ~; /* 关闭背光 */
dma_free_writecombine(NULL, s3c_mylcdfb_info->fix.smem_len, s3c_mylcdfb_info->screen_base, s3c_mylcdfb_info->fix.smem_start);
iounmap(lcd_regs);
iounmap(gpbcon);
iounmap(gpccon);
iounmap(gpdcon);
iounmap(gpgcon);
framebuffer_release(s3c_mylcdfb_info);
} module_init(lcd_drv_init);
module_exit(lcd_drv_exit);
MODULE_LICENSE("GPL");

2.重新编译内核,去掉默认的LCD

make menuconfig ,进入menu菜单重新设置内核参数:

进入Device Drivers-> Graphics support:
<M> S3C2410 LCD framebuffer support //将自带的LCD驱动设为模块, 不编进内核中

然后make uImage 编译内核

make modules 编译模块

为什么要编译模块?

因为LCD驱动相关的文件也没有编进内核,而fb_ops里的成员fb_fillrect(), fb_copyarea(), fb_imageblit()用的都是drivers/video下面的3个文件,所以需要这3个的.ko模块,如下图所示:

Linux驱动之LCD驱动编写

3.挂载驱动

将编译好的LCD驱动模块 和drivers/video里的3个.ko模块 放入nfs文件系统目录中

然后烧写内核, 先装载3个/drivers/video下编译好的模块,再来装载LCD驱动模块

挂载LCD驱动后, 如下图,可以通过  ls -l /dev/fb*   命令查看已挂载的LCD设备节点:

Linux驱动之LCD驱动编写

4.测试运行

测试有两种:

(echo和cat命令详解入口地址: http://www.cnblogs.com/lifexy/p/7601122.html)

echo hello> /dev/tty1     // LCD上便显示hello字段

cat Makefile>/dev/tty1    // LCD上便显示Makeflie文件的内容

4.1使用上节的键盘驱动在LCD终端打印命令行

vi  /etc/inittab         //修改inittab, inittab:配置文件,用于启动init进程时,读取inittab

添加->tty1::askfirst:-/bin/sh   //将sh进程(命令行)输出到tty1里,也就是使LCD输出信息

然后重启,insmod装载3个/drivers/video下编译好的模块,再来insmod装载LCD驱动模块,tty1设备便有了,就能看到提示信息:

Linux驱动之LCD驱动编写

如下图,我们insmod上一节的键盘驱动后,按下enter键,便能在LCD终端上操作linux了

(上一节的键盘驱动详解入口地址: http://www.cnblogs.com/lifexy/p/7553861.html)

Linux驱动之LCD驱动编写

从上图可以看到按下enter键,它就启动了一个进程号772的-sh进程,如下图发现这个-sh的描述符都指向了tty1:

Linux驱动之LCD驱动编写

以上内容转载自16.Linux-LCD驱动(详解)