欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

嵌入式Linux——LCD驱动

程序员文章站 2022-07-14 09:37:56
...

声明:本文以韦东山老师的视频为模本进行编写,开发板为s3c2440,LCD为A043-24-TT-11,此LCD为480*272 的4.3寸屏幕。与老师所讲的略有不同。同时本文为复习视频所学的内容,如有巧合,敬请谅解;
要写LCD驱动就要先从内核中找到支持LCD的软件相关的部分,也就是fbmem.c文件。fbmem.c作为LCD的软件部分为其提供了代码不变的部分,即在入口函数中分配好了主设备号:29,file_operations结构体和register_chrdev函数,详细代码为:register_chrdev(FB_MAJOR,"fb",&fb_fops)
。而fbmem.c会根据不同的设备通过register_fb数组找到不同的设置代码进行调用。
我们所要编写的将是硬件相关的部分,就是将设备的fb_info结构体设置好然后放到register_fb中,让fbmem.c调用,这部分与硬件相关,相对变化较大。而在编写代码之前需要先了解fbmem.c做了什么工作,而我们自己编写的驱动又该做什么样的工作。首先我们分析入口函数,通过上面的代码我们知道了fbmem.c已经为我们将软件的框架搭好,而假设当我们使用应用程序打开这个设备时,我们将调用file_operations中的open函数.open = fb_open:

int fbidx = iminor(inode); //获得次设备号
struct fb_info *info;      //分配一个fb_info结构体
info = registered_fb[fbidx]; //根据次设备号从registered_fb中找到相应的fb_info结构体
if (info->fbops->fb_open)      //如果已经在info中定义了fb_open
    res = info->fbops->fb_open(info,1); //调用info中的fb_open

通过上面的分析我们可以看出,我们是通过register_fb来获得fb_info结构体的,而register_fb数组又是由什么决定那?我们通过从fbmem.c 中查找发现,在register_framebuffer函数中为register_fb赋值:

for (i = 0 ; i < FB_MAX; i++)   //找到一个没有占用的次设备
    if (!registered_fb[i])
        break;
fb_info->dev = device_create(fb_class, fb_info->device,
    MKDEV(FB_MAJOR, i), "fb%d", i); //使用vdev机制自动生成设备
registered_fb[i] = fb_info; //将这个fb_info结构体根据获得次设备号放入register_fb中

有上面的分析知道我们要写一个与我们的LCD硬件相关的fb_info结构体,并通过register_framebuffer函数将这个fb_info结构体通过次设备号放入register_fb中,而fbmem.c 就可以通过次设备号调用register_fb中的fb_info,进而驱动这个硬件。所以我们所要写的LCD驱动可以分为以下四步:
1. 分配一个fb_info结构体
2. 设置这个结构体
3. 做硬件相关的操作
4. 通过register_framebuffer函数注册fb_info

有了上面的步骤,我们按着上面的步骤写程序:而一些细节的部分我会在程序中说明:
下面我们写第一步:
既然要分配一个fb_info结构体,我们就应该先定义这个结构体:

static struct fb_info *s3c_lcd;

然后是为其分配:

s3c_lcd =framebuffer_alloc(0,NULL); 

需要说明:

/**
 * framebuffer_alloc 函数就是创造一个新的frame buffer info结构体
 * @size: 是设备私有数据,可以是0
 * @dev: 指向fb的设备,这里可以为 NULL
 * Returns: 返回一个新的fb_info结构体或者NULL(如果出错).
 */
struct fb_info *framebuffer_alloc(size_t size, struct device *dev)

下面就该进入第二步,设置fb_info结构体:


struct fb_info {
    int node;
    int flags;
    struct fb_var_screeninfo var;   /* Current var */
    struct fb_fix_screeninfo fix;   /* Current fix */
    struct fb_monspecs monspecs;    /* Current Monitor specs */
    struct work_struct queue;   /* Framebuffer event queue */
    struct fb_pixmap pixmap;    /* Image hardware mapper */
    struct fb_pixmap sprite;    /* Cursor hardware mapper */
    struct fb_cmap cmap;        /* Current cmap */
    struct list_head modelist;      /* mode list */
    struct fb_videomode *mode;  /* current mode */
    struct fb_ops *fbops;
    struct device *device;      /* This is the parent */
    struct device *dev;     /* This is this fb device */
    int class_flag;        /* private sysfs flags */
    char __iomem *screen_base;  /* Virtual address */
    unsigned long screen_size;  /* Amount of ioremapped VRAM or 0 */ 

    void *pseudo_palette;       /* Fake palette of 16 colors */ 
#define FBINFO_STATE_RUNNING    0
#define FBINFO_STATE_SUSPENDED  1
    u32 state;          /* Hardware state i.e suspend */
    void *fbcon_par;                /* fbcon use-only private area */
    /* From here on everything is device dependent */
    void *par;  
};

fb_info中有很多设置的选项,我们只设置与我们LCD相关的选项, 而其中重要的设置又分为四部分:
* 设置fb_info的固定参数:struct fb_fix_screeninfo fix;
* 设置fb_info的可变参数:struct fb_var_screeninfo var;
* 设置fb_info操作函数:struct fb_ops *fbops;
* fb_info的其他设置:char __iomem *screen_base;
unsigned long screen_size;
void *pseudo_palette;

下面我们先设置fb_info的固定参数(固定参数多为硬件相关而不会变化的,如屏幕的尺寸,显存物理地址,和屏幕类型等):

strcpy(s3c_lcd->fix.id,"mylcd");
//s3c_lcd->fix.smem_start   //LCD显存的物理地址,在3.3中设置
s3c_lcd->fix.smem_len    = 480*272*16/8;     //显存的长度
s3c_lcd->fix.type        = FB_TYPE_PACKED_PIXELS;
s3c_lcd->fix.visual      = FB_VISUAL_TRUECOLOR; //TFT屏为真彩色
s3c_lcd->fix.line_length = 480*16/8; //一行的长度(单位:type

再设置fb_info的可变参数(而可变的参数是可以根据不同情况而进行不同设置的,如:x,y方向虚拟分辨率,多少字节每像素,以及RGB所占有的比例等):

    s3c_lcd->var.xres          = 480;        //x方向真实的分辨率
    s3c_lcd->var.yres          = 272;        //y方向真实的分辨率
    s3c_lcd->var.xres_virtual  = 480;        //x方向虚拟的分辨率
    s3c_lcd->var.yres_virtual  = 272;        //y方向虚拟的分辨率
    s3c_lcd->var.xoffset       = 0;          //x方向真实分辨率与虚拟分辨率的差值
    s3c_lcd->var.yoffset       = 0;          //y方向真实分辨率与虚拟分辨率的差值

    s3c_lcd->var.bits_per_pixel = 16;        //设置16个字节每像素

    /*RGB:565*/ 
    s3c_lcd->var.red.offset   = 11;
    s3c_lcd->var.red.length   = 5;
    s3c_lcd->var.green.offset = 5;
    s3c_lcd->var.green.length = 6;
    s3c_lcd->var.blue.offset  = 0;
    s3c_lcd->var.blue.length  = 5;

    s3c_lcd->var.activate     = FB_ACTIVATE_NOW;

之后设置操作函数:

s3c_lcd->fbops = &s3c_lcdfb_ops;
static struct fb_ops s3c_lcdfb_ops = {
    .owner      = THIS_MODULE,
    .fb_setcolreg   = s3c_lcdfb_setcolreg,   /* 调色板 */
    .fb_fillrect    = cfb_fillrect,
    .fb_copyarea    = cfb_copyarea,
    .fb_imageblit   = cfb_imageblit,
};

最后就是对fb_info的其他设置:

//s3c_lcd->screen_base             //显存的虚拟地址
s3c_lcd->screen_size = 480*272*2;     //屏幕的尺寸
s3c_lcd->pseudo_palette = pseudo_palette;  //调色板

上面对fb_info设置完后就该到第三步:硬件相关的设置 ,而硬件首先要配置的就是用于LCD的GPIO接口,GPIO的图为:
嵌入式Linux——LCD驱动嵌入式Linux——LCD驱动
嵌入式Linux——LCD驱动
而相应的代码为:

gpbcon = ioremap(0x56000010, 8);
gpbdat = gpbcon + 1;
gpccon = ioremap(0x56000020, 4);  
gpdcon = ioremap(0x56000030, 4); 
gpgcon = ioremap(0x56000060, 4); 

/* 设置背光灯 */
*gpbcon &= ~(3);   //清零
*gpbcon |= (1);    //设置输出模式
*gpbdat &= ~(1);   //设置低电平

/* 设置RGB数据接口 */
*gpccon = 0xaaaaaaaa;
*gpdcon = 0xaaaaaaaa;

/* 设置PWREN */
*gpgcon &= ~(3<<4);   //清零
*gpgcon |= (3<<4);    //设置LCD模式

下面将是对LCD控制器的设置,以使其可以支持相应的LCD,在这之前我们先构造一个lcd_reg的结构体用于存放LCD控制器的各种寄存器:

/* s3c2440 lcd registers */
struct lcd_reg{
    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[9];
    unsigned long DITHMODE;
    unsigned long TPAL;
    unsigned long LCDINTPND;
    unsigned long LCDSRCPND;
    unsigned long LCDINTMSK;
    unsigned long TCONSEL;
};

由于我开发板上的LCD与视频中的LCD不是同一种类型的,所以这部分代码我做了相应的改动所以从lcdcon1到lcdcon4,我要另做说明:


/*
*LCDCON1
*bit[17:8] CLKVAL :TFT: VCLK = HCLK / [(CLKVAL+1) * 2] ( CLKVAL> 0 )
*                   LCD手册:VCLK =9MHZ ,而HCLK =100MHZ
*                   所以 CLKVAL=5
*bit[6:5] PNRMODE :0b11 = TFT LCD panel
*bit[4:1] BPPMODE :0b1100 = 16 bpp for TFT
*bit [0]  ENVID   :0 = Disable the video output and the LCD control signal.
*/
lcd_regs->lcdcon1 = (5<<8) | (3<<5) | (12<<1) | (0<<0);

而垂直方向的时间参数发生了变化:
嵌入式Linux——LCD驱动
所以代码为:

/*
*LCDCON2 :垂直方向时间参数
*bit[31:24] VBPD    : VSYNC 后再过多长时间才能发出第一个数据
*                     =1
*bit[23:14] LINEVAL : 多少行
*                     =271
*bit [13:6] VFPD    : 最后一行数据后再过多久发VSYNC信号
*                     =1
*bit [5:0]  VSPW    : VSYNC脉冲宽度
*                     =9
*/
lcd_regs->lcdcon2 = (1<<24) | (271<<14) | (1<<6) | (9<<0);

水平方向的时间参数:嵌入式Linux——LCD驱动
所以代码为:

/*
*水平方向时间参数
*LCDCON3:
*bit[25:19] HBPD    : HSYNC 后再过多长时间才能发出第一个数据
*                     =2
*bit[18:8] HOZVAL   : 多少列
*                     =479
*bit[7:0] HFPD      : 一行中发出最后一个像素数据后再过多久发HSYNC信号
*                     =2
*
*LCDCON4 :
*bit[7:0] HSPW     : HSYNC脉冲宽度
*                     =40
*/
lcd_regs->lcdcon3 = (2<<19) | (479<<8) | (2<<0);
lcd_regs->lcdcon4 = (40<<0);

信号的极性并没有发生变化,所以代码为:

/*
*信号极性
*LCDCON5:
*
*bit[11] FRM565  : 16bpp输出形式
*                  1 = 5:6:5 Format
*bit[10] INVVCLK : 表示是上升沿读取数据还是下降沿读取数据
*                  0 = 下降沿读取数据
*bit[9] INVVLINE : 水平方向同步信号是否反转
*                  1 = 反转
*bit[8] INVVFRAME: 垂直方向同步信号是否反转
*                  1 = 反转
*bit[7] INVVD    : 数据脉冲是否反转
*                  0 = Normal(不反转)
*bit[6] INVVDEN  : 数据使能位是否反转
*                  0 = normal
*bit[5] INVPWREN : PWREN(LCD电源)位是否反转
*                  0 = normal
*bit[3] PWREN    : PWREN(LCD电源)位是否使能
*                  0 = Disable PWREN signal(不使能)
*bit[1] BSWP     :字节转换位
*                  0 = Swap Disable
*bit[0] HWSWP    : 半字转换控制位
*                  1 = Swap Enable(转换)
*/

lcd_regs->lcdcon5 = (1<<11) | (1<<9) | (1<<8) | (1<<0);

上面的工作做完我们的对LCD的设置就基本完成了,下面就是对显存的设置了,

/*分配显存:
//s3c_lcd->fix.smem_start
//s3c_lcd->fix.smem_len    = 480*272*16/8;
//s3c_lcd->screen_base
*/
s3c_lcd->screen_base = dma_alloc_writecombine(NULL,272*480*2,&(s3c_lcd->fix.smem_start),GFP_KERNEL);

/*
*把地址告诉LCD控制器:
*LCDSADDR1:
*bit[29:21] LCDBANK   :A[30:22] of the bank location for the video buffer in the system memory
*bit[20:0]  LCDBASEU  :A[21:1] of the start address of the LCD frame buffer
*
*LCDSADDR2:
*bit[20:0] LCDBASEL   :A[21:1] of the end address of the LCD frame buffer
*                       LCDBASEL = ((the frame end address) >>1) + 1
*                                   = LCDBASEU + (PAGEWIDTH+OFFSIZE) x (LINEVAL+1)
*
*LCDSADDR3:
*bit[10:0] PAGEWIDTH  : Virtual screen page width (the number of half words).
*
*/
lcd_regs->lcdsaddr1 = (s3c_lcd->fix.smem_start >> 1) & ~(3<<30);
lcd_regs->lcdsaddr2 = ((s3c_lcd->fix.smem_start + s3c_lcd->fix.smem_len) >>1) & 0x1fffff;
lcd_regs->lcdsaddr3 = 480*16/16;

而在这里我需要讲一下dma_alloc_writecombine函数:

void *
dma_alloc_writecombine(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp)

此函数为分配显存的函数,而且这一显存的物理地址为连续的地址, 其中第一个参数dev为设备,第二个参数size表示要分配的地址的大小,第三个参数handle为物理地址,而第四个参数为标记。而此函数的返回值为分配内存的虚拟地址。

上面的工作做完后,我们就基本完成入口函数的程序,只差最后一步将fb_info结构体注册了:

register_framebuffer(s3c_lcd);

然后我们完善程序,如调色板:

static u32 pseudo_palette[16];
s3c_lcd->pseudo_palette = pseudo_palette;
//在操作函数中
.fb_setcolreg   = s3c_lcdfb_setcolreg,

static int s3c_lcdfb_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 > 16) {
        return 1;
    }
    u32 *pseudo_palette = 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);

    pseudo_palette[regno] = val;

    return 0;
}
/* from pxafb.c */
static inline unsigned int chan_to_field(unsigned int chan, struct fb_bitfield *bf)
{
    chan &= 0xffff;
    chan >>= 16 - bf->length;
    return chan << bf->offset;
}

出口函数:

unregister_framebuffer(s3c_lcd);

lcd_regs->lcdcon1 &= ~1;         //关LCD控制器
lcd_regs->lcdcon5 &= ~(1<<3);    //关LCD本身,给LCD断电
*gpbdat           &= ~1;         //关背光灯

dma_free_writecombine(NULL,s3c_lcd->fix.smem_len,s3c_lcd->screen_base,s3c_lcd->fix.smem_start);

iounmap(gpbcon);
iounmap(gpccon);  
iounmap(gpdcon); 
iounmap(gpgcon); 
iounmap(lcd_regs);

framebuffer_release(s3c_lcd);

写完出口函数,我们的LCD驱动程序就写完了。
下面就是测试了:测试的步骤为:

  1. 进入内核目录:make menuconfig ,去掉原来的LCD驱动程序
  2. make uImage :生成没有LCD驱动的内核
  3. cp arch/arm/boot/uImage /work/nfs_root/uImage_nolcds
  4. make modules :得到cfd_fillrect,cfb_copyarea,cfb_imageblit函数对应的模块
  5. 使用新的uImage_nolcds 启动:nfs 30000000 192.168.1.111:/work/nfs_root/uImage_nolcds bootm 30000000
  6. 编译自己写的LCD驱动程序,并将其考到根文件系统
  7. 在开发板上使用安装驱动:insmod cfbcopyarea.ko insmod cfdfillrect.ko insmod cfbimageblit.ko insmod lcd.ko
  8. echo hollo >/dev/tty1 ,可以在LCD上可以看到hello
  9. cat lcd.ko >/dev/fb0 ,花屏
  10. 修改/etc/inittab,加一行tty1::askfirst:-/bin/sh
  11. 使用新的uImage_nolcds 重新启动:nfs 30000000 192.168.1.111:/work/nfs_root/uImage_nolcds bootm 30000000
  12. insmod input.ko
  13. 可以按键s2,s3,s4,屏幕会显示ls,和ls命令后的内容

    我的文章中可能有些概念或道理讲的不详细,下面是我看到的几篇我认为比较好的文章我在这里转载:

    linux LCD驱动(二)–FrameBuffer:这一篇对framebuffer做了比较详尽的描写,可以让你有跟深入的认识。
    嵌入式Linux之我行——S3C2440上LCD驱动(FrameBuffer)实例开发讲解:这一篇确实像其所说的是个实例,可以让你对LCD驱动的整个过程有一个全面的了解。
    嵌入式Linux驱动笔记(三)——LCD驱动程序:这篇文章中使用的是480*272 的TFT屏,并附加了全部的驱动程序。
    linux驱动LCD 驱动程序代码编写:这篇文章中对LCD驱动的测试有很详尽的描写
    10-S3C2440驱动学习(四)嵌入式linux-LCD驱动程序:这篇文章发现的比较晚,发现这篇文章是最接近视频的,他的很多地方都使用视频的截图加以说明,使得更容易回顾老师所讲的内容。