嵌入式Linux——LCD驱动

2019-07-12 14:51发布

声明:本文以韦东山老师的视频为模本进行编写,开发板为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屏为真彩 {MOD} 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, /* 调 {MOD}板 */ .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; //调 {MOD}板 上面对fb_info设置完后就该到第三步:硬件相关的设置 ,而硬件首先要配置的就是用于LCD的GPIO接口,GPIO的图为:
这里写图片描述这里写图片描述
这里写图片描述
而相应的代码为: 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); 而垂直方向的时间参数发生了变化:
这里写图片描述
所以代码为: /* *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); 水平方向的时间参数:这里写图片描述
所以代码为: /* *水平方向时间参数 *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); 然后我们完善程序,如调 {MOD}板: 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驱动程序:这篇文章发现的比较晚,发现这篇文章是最接近视频的,他的很多地方都使用视频的截图加以说明,使得更容易回顾老师所讲的内容。