linux驱动移植-LCD驱动基础
一、LCD基础知识
1.1 LCD硬件原理
在Mini2440裸机开发之LCD基础我们介绍了LCD的硬件原理,有兴趣的可以去看看,这里我们仅仅简述一下LCD的原理。
下图是LCD示意图,里面的每个点就是一个像素点。它里面有一个电子枪,一边移动,一边发出各种颜色的光。用动态图表示如下:
电子枪是如何移动的?
- 有一条CLK时钟线与LCD相连,每发出一次CLK(高低电平),电子枪就移动一个像素。
颜色如何确定?
- 由连接LCD的三组线RGB三原色混合而成:R(Red)、G(Green)、B(Blue)确定。
电子枪如何得知应跳到下一行?
- 有一条HSYNC信号线与LCD相连,每发出一次脉冲(高低电平),电子枪就跳到下一行,该信号叫做行同步信号。
电子枪如何得知应跳到原点?
- 有一条VSYNC信号线与LCD相连,每发出一次脉冲(高低电平),电子枪就跳到原点,该信号叫做帧同步信号。
RGB线上的数据从何而来?
- 内存里面划分一块显存(framebuffer),里面存放了要显示的数据,LCD控制器从里面将数据读出来,通过RGB三组线传给电子枪,电子枪再依次打到显示屏上。
前面的信号由谁发给LCD?
- 由S3C2440里面的LCD控制器来控制发出信号。
工作原理:
- LCD屏可以看作是由许多像素构成的,比如320*240就是由320*240个像素构成的,每个像素由RGB三色调和,每种颜色又由多个位组成。比如我们的开发板上的LCD,有320*240个像素,每个像素由RGB三色调和,RGB三色位数分别为:565。
- S3C2440内集成了LCD控制器,LCD控制器外接LCD,每来一个VLCK,就会从左到右在LCD屏幕上显示一个像素的颜色,而这一个个像素的颜色就存放在显存里,在嵌入式领域,一般不会佩戴专门的显存,而是从内存SDRAM中划分出一部分充当显存;
- HSYNC引脚每发出一个脉冲,表示一行的数据开始发送;
- VSYNC引脚每发出一个脉冲,表示一帧的数据开始发送。
1.2 frambuffer设备
我们在Mini2440裸机开发之LCD编程(GB2312、ASCII字库制作) 中介绍了如何在LCD显示屏中显示一张图片,其核心步骤就是向framebuffer中写入图片数据。
在linux中,如果我们的系统想使用GUI(图形界面接口),这时LCD设备驱动程序就应该编写成frambuffer接口,而不是像裸机中那样只编写操作底层的LCD控制器接口。
framebuffer是linux系统为显示设备提供的一个用户接口,它将显示缓冲区抽象,屏蔽图像硬件的底层差异,允许上层应用程序在图形模式下直接对显示缓冲区进行操作,用户应用程序可以通过framebuffer透明地访问不同类型的显示设备。
linux抽象出framebuffer这个帧缓冲区可以供用户应用程序直接读写,通过更改framebuffer中的内容,就可以立刻显示在LCD显示屏上。
framebuffer是一个标准的字符设备,主设备号是29,次设备号根据缓冲区的数目而定。framebuffer对应/dev/fb%d设备文件。
对用户程序而言,framebuffer设备它和/dev下面的其它设备没有什么区别,用户可以把frameBuffer看成一块内存,既可以写,又可以读。显示器将根据内存数据显示对应的图像界面,这一切都由framebuffer设备驱动来完成。
注意:如果“framebuffer设备”你理解起来比较变扭,你就把它当做LCD设备即可,实际上我认为他们就是一个意思,只是叫法不一样。
1.3 实现思路
我们在介绍linux framebuffer驱动实现框架之前,我们试想一下,如果让我们按照之前所学的知识来实现framebuffer设备驱动我们会怎么做呢?
- 首先我们会动态注册设备编号;
- 初始化一个字符设备,并注册设备的file_operations;
- 创建字符设备节点;
- 编写file_operations成员函数open、write等函数;
这么做当然也是可以的,不过这么存在一个问题,如果我们需要同时注册多个framebuffer设备,并且每个framebuffer设备具有很多共性的东西,他们的设备操作函数基本相同,只是LCD的屏幕尺寸、LCD控制器时序参数等信息存在差异,那我们应该如何进行抽象,屏蔽不同framebuffer之间的差异呢。
实际上如果采用面向对象编程的话,我们可以这么干,我们抽象出frambebufer设备接口,然后实现一个抽象类继承该接口,在抽象类中实现一个些共性属性和方法,大致如下:
实际上linux内核也是这么干的,只不过由于C不支持面向对象,在linux系统中,当系统接入若干个不同型号的LCD设备时,linux内核做了一层抽象,使用fb_info结构体来表示每一个LCD设备的信息以及设备操作方法。
二、framebuffer设备驱动框架图
framebuffer设备驱动在linux系统框架如下图:
从上图可以看出frambebuffer设备驱动主要由两部分组成:
- fbmem.c:为应用程序提供了对frambebuffer设备操作的系统调用,同时为硬件层提供了注册framebuffer设备的接口,比如register_frambebuffer;
- xxxfb.c:主要实现了framebuffer设备的注册,实际上就是填充fb_info结构体,关于该结构的含义我们后面介绍。
通过引入fb_info的形式,将硬件相关的部分与文件设备操作分离开,增加了内核代码的稳定性。我们只需调用register_framebuffer函数注册一个新的fb_info结构体,即可向内核新增一个framebuffer设备。
三、基础数据结构
3.1 fb_info结构体
struct fb_info定义在include/linux/fb.h文件中,用于保存我们framebuffer设备信息,其内部提供了对framebuffer设备操作的函数指针:
struct fb_info { atomic_t count; int node; int flags; /* * -1 by default, set to a FB_ROTATE_* value by the driver, if it knows * a lcd is not mounted upright and fbcon should rotate to compensate. */ int fbcon_rotate_hint; struct mutex lock; /* Lock for open/release/ioctl funcs */ struct mutex mm_lock; /* Lock for fb_mmap and smem_* fields */ 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 */ #if IS_ENABLED(CONFIG_FB_BACKLIGHT) /* assigned backlight device */ /* set before framebuffer registration, remove after unregister */ struct backlight_device *bl_dev; /* Backlight level curve */ struct mutex bl_curve_mutex; u8 bl_curve[FB_BACKLIGHT_LEVELS]; #endif #ifdef CONFIG_FB_DEFERRED_IO struct delayed_work deferred_work; struct fb_deferred_io *fbdefio; #endif 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 */ #ifdef CONFIG_FB_TILEBLITTING struct fb_tile_ops *tileops; /* Tile Blitting */ #endif union { char __iomem *screen_base; /* Virtual address */ char *screen_buffer; }; 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; /* we need the PCI or similar aperture base/size not smem_start/size as smem_start may just be an object allocated inside the aperture so may not actually overlap */ struct apertures_struct { unsigned int count; struct aperture { resource_size_t base; resource_size_t size; } ranges[0]; } *apertures; bool skip_vt_switch; /* no VT switch on suspend/resume required */ };
部分参数含义如下:
- count:fb_info的引用计数,fb_open时使其+1,release时使其-1,为0时销毁;
- node:全局变量registered_fb中的索引值,注册的时候分配,通过node可以索引fb_info;
- flags:一些标志位,有关于硬件加速的,大小端,fb的内存位置(设备或者内存),具体硬件加速的方法,表明哪个使用了硬件加速;
- var:描述的是LCD屏幕的可变参数,包括可见的分辨率,Bpp(bits_per_pixel),还有具体的时钟信号,包括bp,fp,vsync,hsync等,可以通过应用层设置也可以驱动层配置,相关设置时序的工具有fbset,还有相关的一些调色板配置;
- fix:描述的是LCD屏幕的不可参数,不能在用户层更改。包括framebuffer缓冲区的区里起始位置(一般是显示控制器DMA起始地址,smem_start),framebuffer的长度(单位为字节,smem_len);
- monspecs:描述的是显示器的一些参数,时序,生产日期等,一般这种信息描述在显示器中的EDID中,通过解析EDID来填充此参数;
- queue:事件队列;
- pixmap,sprite(光标)都是像素图,注册framebuffer的时候会默认申请;
- cmap:设备独立的 colormap 信息,可以通过 ioctl 的 FBIOGETCMAP 和 FBIOPUTCMAP 命令设置 colormap;
- modelist:将var参数转化成video mode,然后存入这个链表;
- mode:一些时序,刷新率扫描方式(vmode)(隔行,逐行),极性(sync);
- CONFIG_FB_BACKLIGHT:有关于背光曲线以及背光设备注册,需要注意的是需要在注册framebuffer之前就对其初始化;
- CONFIG_FB_DEFERRED_IO:延迟IO,使用缺页中断的原理操作,减少FBIOPAN_DISPLAY带来的系统调用开支;
- screen_base/screen_buffer:framebuffer缓冲区虚拟地址,u8类型,对应的物理地址保存在fix.smem_start中;
- screen_size:framebuffer缓冲区大小;
- fbops:提供具体的fb操作函数,主要是通过fbmem.c中提供的文件操作函数,间接调用fb_ops,主要的操作有fb_check_var,fb_pan_display,fb_mmap,等,以下三个函数提供了绘图的操作,可以使用系统中的绘图函数,也可以重写硬件加速的绘图函数;
- device:fb_info的设备父节点,对应即/sys/device/xxx/fb_info;
- dev:设备指针,注册framebuffer时创建;
- pseudo_palette:伪调色板;
- state:硬件状态,在fbmem中会设置成suspend以及resume;
- skip_vt_switch:关于VT switch,是与console切换以及PM相关的;
3.2 fb_info标志位
fb_info标志位定义如下:
/* FBINFO_* = fb_info.flags bit flags */ #define FBINFO_DEFAULT 0 #define FBINFO_HWACCEL_DISABLED 0x0002 /* When FBINFO_HWACCEL_DISABLED is set: * Hardware acceleration is turned off. Software implementations * of required functions (copyarea(), fillrect(), and imageblit()) * takes over; acceleration engine should be in a quiescent state */ /* hints */ #define FBINFO_VIRTFB 0x0004 /* FB is System RAM, not device. */ #define FBINFO_PARTIAL_PAN_OK 0x0040 /* otw use pan only for double-buffering */ #define FBINFO_READS_FAST 0x0080 /* soft-copy faster than rendering */ /* hardware supported ops */ /* semantics: when a bit is set, it indicates that the operation is * accelerated by hardware. * required functions will still work even if the bit is not set. * optional functions may not even exist if the flag bit is not set. */ #define FBINFO_HWACCEL_NONE 0x0000 #define FBINFO_HWACCEL_COPYAREA 0x0100 /* required */ #define FBINFO_HWACCEL_FILLRECT 0x0200 /* required */ #define FBINFO_HWACCEL_IMAGEBLIT 0x0400 /* required */ #define FBINFO_HWACCEL_ROTATE 0x0800 /* optional */ #define FBINFO_HWACCEL_XPAN 0x1000 /* optional */ #define FBINFO_HWACCEL_YPAN 0x2000 /* optional */ #define FBINFO_HWACCEL_YWRAP 0x4000 /* optional */ #define FBINFO_MISC_USEREVENT 0x10000 /* event request from userspace */ #define FBINFO_MISC_TILEBLITTING 0x20000 /* use tile blitting */ /* A driver may set this flag to indicate that it does want a set_par to be * called every time when fbcon_switch is executed. The advantage is that with * this flag set you can really be sure that set_par is always called before * any of the functions dependent on the correct hardware state or altering * that state, even if you are using some broken X releases. The disadvantage * is that it introduces unwanted delays to every console switch if set_par * is slow. It is a good idea to try this flag in the drivers initialization * code whenever there is a bug report related to switching between X and the * framebuffer console. */ #define FBINFO_MISC_ALWAYS_SETPAR 0x40000 /* where the fb is a firmware driver, and can be replaced with a proper one */ #define FBINFO_MISC_FIRMWARE 0x80000 /* * Host and GPU endianness differ. */ #define FBINFO_FOREIGN_ENDIAN 0x100000 /* * Big endian math. This is the same flags as above, but with different * meaning, it is set by the fb subsystem depending FOREIGN_ENDIAN flag * and host endianness. Drivers should not use this flag. */ #define FBINFO_BE_MATH 0x100000 /* * Hide smem_start in the FBIOGET_FSCREENINFO IOCTL. This is used by modern DRM * drivers to stop userspace from trying to share buffers behind the kernel's * back. Instead dma-buf based buffer sharing should be used. */ #define FBINFO_HIDE_SMEM_START 0x200000
3.3 fb_ops
fb_ops里存放时的framebuffer设备操作函数:
/* * Frame buffer operations * * LOCKING NOTE: those functions must _ALL_ be called with the console * semaphore held, this is the only suitable locking mechanism we have * in 2.6. Some may be called at interrupt time at this point though. * * The exception to this is the debug related hooks. Putting the fb * into a debug state (e.g. flipping to the kernel console) and restoring * it must be done in a lock-free manner, so low level drivers should * keep track of the initial console (if applicable) and may need to * perform direct, unlocked hardware writes in these hooks. */ struct fb_ops { /* open/release and usage marking */ struct module *owner; int (*fb_open)(struct fb_info *info, int user); int (*fb_release)(struct fb_info *info, int user); /* For framebuffers with strange non linear layouts or that do not * work with normal memory mapped access */ ssize_t (*fb_read)(struct fb_info *info, char __user *buf, size_t count, loff_t *ppos); ssize_t (*fb_write)(struct fb_info *info, const char __user *buf, size_t count, loff_t *ppos); /* checks var and eventually tweaks it to something supported, * DO NOT MODIFY PAR */ int (*fb_check_var)(struct fb_var_screeninfo *var, struct fb_info *info); /* set the video mode according to info->var */ int (*fb_set_par)(struct fb_info *info); /* set color register */ int (*fb_setcolreg)(unsigned regno, unsigned red, unsigned green, unsigned blue, unsigned transp, struct fb_info *info); /* set color registers in batch */ int (*fb_setcmap)(struct fb_cmap *cmap, struct fb_info *info); /* blank display */ int (*fb_blank)(int blank, struct fb_info *info); /* pan display */ int (*fb_pan_display)(struct fb_var_screeninfo *var, struct fb_info *info); /* Draws a rectangle */ void (*fb_fillrect) (struct fb_info *info, const struct fb_fillrect *rect); /* Copy data from area to another */ void (*fb_copyarea) (struct fb_info *info, const struct fb_copyarea *region); /* Draws a image to the display */ void (*fb_imageblit) (struct fb_info *info, const struct fb_image *image); /* Draws cursor */ int (*fb_cursor) (struct fb_info *info, struct fb_cursor *cursor); /* wait for blit idle, optional */ int (*fb_sync)(struct fb_info *info); /* perform fb specific ioctl (optional) */ int (*fb_ioctl)(struct fb_info *info, unsigned int cmd, unsigned long arg); /* Handle 32bit compat ioctl (optional) */ int (*fb_compat_ioctl)(struct fb_info *info, unsigned cmd, unsigned long arg); /* perform fb specific mmap */ int (*fb_mmap)(struct fb_info *info, struct vm_area_struct *vma); /* get capability given var */ void (*fb_get_caps)(struct fb_info *info, struct fb_blit_caps *caps, struct fb_var_screeninfo *var); /* teardown any resources to do with this framebuffer */ void (*fb_destroy)(struct fb_info *info); /* called at KDB enter and leave time to prepare the console */ int (*fb_debug_enter)(struct fb_info *info); int (*fb_debug_leave)(struct fb_info *info); };
四、fbmem.c源码分析
fbmem.c是framebuffer设备驱动的核心,它向上给应用程序提供了系统调用的接口,向下对特定的硬件提供了设备注册接口。
fbmem.c位于drivers/video/fbdev/core路径下。我们可以在该文件定位到驱动模块的入口和出口:
module_init(fbmem_init);
module_exit(fbmem_exit);
4.1 入口函数
我们定位到fbmem.c的入口函数,也就是fbmem_init:
/** * fbmem_init - init frame buffer subsystem * * Initialize the frame buffer subsystem. * * NOTE: This function is _only_ to be called by drivers/char/mem.c. * */ static int __init fbmem_init(void) { int ret; if (!proc_create_seq("fb", 0, NULL, &proc_fb_seq_ops)) return -ENOMEM; ret = register_chrdev(FB_MAJOR, "fb", &fb_fops); if (ret) { printk("unable to get major %d for fb devs\n", FB_MAJOR); goto err_chrdev; } fb_class = class_create(THIS_MODULE, "graphics"); if (IS_ERR(fb_class)) { ret = PTR_ERR(fb_class); pr_warn("Unable to create fb class; errno = %d\n", ret); fb_class = NULL; goto err_class; } fb_console_init(); return 0; err_class: unregister_chrdev(FB_MAJOR, "fb"); err_chrdev: remove_proc_entry("fb", NULL); return ret; }
简要分析一下该函数执行流程:
-
创建/proc/fb文件;
-
创建字符设备fb,主设备编号为FB_MAJOR(29),注册file_operations结构体fb_fops;
- 调用class_create在/sys/class目录下创建graphics这个类,但是此时并没有调用device_create在/dev下创建设备节点 ;
可以通过如下命令查看字符设备:
root@zhengyang:/work/sambashare/linux-5.2.8# cat /proc/devices Character devices: 1 mem 4 /dev/vc/0 4 tty 4 ttyS 5 /dev/tty 5 /dev/console 5 /dev/ptmx 5 ttyprintk 6 lp 7 vcs 10 misc 13 input 14 sound/midi 14 sound/dmmidi 21 sg 29 fb // 这个名字来自register_chrdev函数第二个参数 89 i2c
可以看到,确实是创建了主设备号为29的"fb"字符设备,而这里还没有创建设备节点,后面会提到,内核将该工作放到register_framebuffer函数里了。
4.2 fb_fops
我们再来看看file_operations结构体fb_fops:
static const struct file_operations fb_fops = { .owner = THIS_MODULE, .read = fb_read, .write = fb_write, .unlocked_ioctl = fb_ioctl, #ifdef CONFIG_COMPAT .compat_ioctl = fb_compat_ioctl, #endif .mmap = fb_mmap, .open = fb_open, .release = fb_release, #if defined(HAVE_ARCH_FB_UNMAPPED_AREA) || \ (defined(CONFIG_FB_PROVIDE_GET_FB_UNMAPPED_AREA) && \ !defined(CONFIG_MMU)) .get_unmapped_area = get_fb_unmapped_area, #endif #ifdef CONFIG_FB_DEFERRED_IO .fsync = fb_deferred_io_fsync, #endif .llseek = default_llseek, };
下面我们来一一分析这些成员函数。
4.3 fb_open
static int fb_open(struct inode *inode, struct file *file) __acquires(&info->lock) __releases(&info->lock) { int fbidx = iminor(inode); // 获取设备节点的次设备号 struct fb_info *info; // 定义fb_info指针 int res = 0; info = get_fb_info(fbidx); // 根据次设备编号获取fb_info if (!info) { request_module("fb%d", fbidx); info = get_fb_info(fbidx); if (!info) return -ENODEV; } if (IS_ERR(info)) return PTR_ERR(info); mutex_lock(&info->lock); // 获取互斥锁 if (!try_module_get(info->fbops->owner)) { res = -ENODEV; goto out; } file->private_data = info; if (info->fbops->fb_open) { res = info->fbops->fb_open(info,1); if (res) module_put(info->fbops->owner); } #ifdef CONFIG_FB_DEFERRED_IO if (info->fbdefio) fb_deferred_io_open(info, inode, file); #endif out: mutex_unlock(&info->lock); // 释放互斥锁 if (res) put_fb_info(info); return res; }
这里我们来看一下get_fb_info函数的实现:
static struct fb_info *get_fb_info(unsigned int idx) { struct fb_info *fb_info; if (idx >= FB_MAX) return ERR_PTR(-ENODEV); mutex_lock(®istration_lock); fb_info = registered_fb[idx]; if (fb_info) atomic_inc(&fb_info->count); mutex_unlock(®istration_lock); return fb_info; }
可以看到get_fb_info函数将registered_fb数组的第idx个元素赋值给了fb_info,registered_fb是一个struct fb_info结构类型的全局数组:
struct fb_info *registered_fb[FB_MAX] __read_mostly;
这和数组会在register_framebuffer函数中赋值。
经过分析,我们最终会发现fb_open执行的是fbops的操作函数中的fbopen函数。
4.4 fb_read
static ssize_t fb_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { unsigned long p = *ppos; // 读取起始位置 struct fb_info *info = file_fb_info(file); // 获取fb_info u8 *buffer, *dst; u8 __iomem *src; int c, cnt = 0, err = 0; unsigned long total_size; if (!info || ! info->screen_base) return -ENODEV; if (info->state != FBINFO_STATE_RUNNING) return -EPERM; if (info->fbops->fb_read) return info->fbops->fb_read(info, buf, count, ppos); //执行fbops操作函数里的fb_read函数 total_size = info->screen_size; // 屏幕尺寸 假设屏幕大小240*320,每个像素占n字节数 则屏幕尺寸为240*320*n if (total_size == 0) total_size = info->fix.smem_len; //frmebuffer缓冲区大小 if (p >= total_size) return 0; if (count >= total_size) // 最多把整个屏幕数据全部读取了 count = total_size; if (count + p > total_size) count = total_size - p; buffer = kmalloc((count > PAGE_SIZE) ? PAGE_SIZE : count, // 分配缓冲器 大于一页的话,按页大小读取 GFP_KERNEL); if (!buffer) return -ENOMEM; src = (u8 __iomem *) (info->screen_base + p); // 获取读取起始地址(虚拟地址) if (info->fbops->fb_sync) info->fbops->fb_sync(info); while (count) { c = (count > PAGE_SIZE) ? PAGE_SIZE : count; dst = buffer; fb_memcpy_fromfb(dst, src, c); // 一次从src读取c个字节到dst dst += c; src += c; if (copy_to_user(buf, buffer, c)) { // 写回用户空间buf中,长度为c err = -EFAULT; break; } *ppos += c; buf += c; cnt += c; count -= c; } kfree(buffer); // 释放内存 return (err) ? err : cnt; }
可以看到如果提供了fbops操作函数里的fb_read函数,则直接调用info->fbops->fb_read(info, buf, count, ppos)从framebuff缓冲区读取数据。
否则直接从info->screen_base + ppos地址读取count字节数据到buf缓冲区。
4.5 fb_write
static ssize_t fb_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { unsigned long p = *ppos; // 写入起始位置 struct fb_info *info = file_fb_info(file); // 获取fb_info u8 *buffer, *src; u8 __iomem *dst; int c, cnt = 0, err = 0; unsigned long total_size; if (!info || !info->screen_base) return -ENODEV; if (info->state != FBINFO_STATE_RUNNING) return -EPERM; if (info->fbops->fb_write) return info->fbops->fb_write(info, buf, count, ppos); // 执行fbops操作函数里的fb_write函数 total_size = info->screen_size; // 屏幕尺寸 假设屏幕大小240*320,每个像素占n字节数 则屏幕尺寸为240*320*n if (total_size == 0) total_size = info->fix.smem_len; //frmebuffer缓冲区大小 if (p > total_size) return -EFBIG; if (count > total_size) { err = -EFBIG; count = total_size; } if (count + p > total_size) { if (!err) err = -ENOSPC; count = total_size - p; } buffer = kmalloc((count > PAGE_SIZE) ? PAGE_SIZE : count, GFP_KERNEL); // 分配缓冲器 大于一页的话,按页大小写入 if (!buffer) return -ENOMEM; dst = (u8 __iomem *) (info->screen_base + p); // 获取写入起始地址(虚拟地址) if (info->fbops->fb_sync) info->fbops->fb_sync(info); while (count) { c = (count > PAGE_SIZE) ? PAGE_SIZE : count; src = buffer; if (copy_from_user(src, buf, c)) { // 从用户空间获取数据到内容 err = -EFAULT; break; } fb_memcpy_tofb(dst, src, c); // 一次将src中c个字节写到dst dst += c; src += c; *ppos += c; buf += c; cnt += c; count -= c; } kfree(buffer); // 释放内存 return (cnt) ? cnt : err; }
可以看到如果提供了fbops操作函数里的fb_write函数,则直接调用iinfo->fbops->fb_write(info, buf, count, ppos)向framebuffer缓冲区写入数据。
否则直接向从buf中读取count个字节写入nfo->screen_base + ppos地址处。
4.6 fb_mmap
static int fb_mmap(struct file *file, struct vm_area_struct * vma) { struct fb_info *info = file_fb_info(file); // 获取fb_info struct fb_ops *fb; unsigned long mmio_pgoff; unsigned long start; u32 len; if (!info) return -ENODEV; fb = info->fbops; if (!fb) return -ENODEV; mutex_lock(&info->mm_lock); if (fb->fb_mmap) { int res; /* * The framebuffer needs to be accessed decrypted, be sure * SME protection is removed ahead of the call */ vma->vm_page_prot = pgprot_decrypted(vma->vm_page_prot); res = fb->fb_mmap(info, vma); mutex_unlock(&info->mm_lock); return res; } /* * Ugh. This can be either the frame buffer mapping, or * if pgoff points past it, the mmio mapping. */ start = info->fix.smem_start; // framebuffer缓冲区起始地址(物理地址) len = info->fix.smem_len; // framebuffer缓冲区大小 mmio_pgoff = PAGE_ALIGN((start & ~PAGE_MASK) + len) >> PAGE_SHIFT; if (vma->vm_pgoff >= mmio_pgoff) { if (info->var.accel_flags) { mutex_unlock(&info->mm_lock); return -EINVAL; } vma->vm_pgoff -= mmio_pgoff; start = info->fix.mmio_start; len = info->fix.mmio_len; } mutex_unlock(&info->mm_lock); vma->vm_page_prot = vm_get_page_prot(vma->vm_flags); /* * The framebuffer needs to be accessed decrypted, be sure * SME protection is removed */ vma->vm_page_prot = pgprot_decrypted(vma->vm_page_prot); fb_pgprotect(file, vma, start); return vm_iomap_memory(vma, start, len); }
framebuffer的显示缓冲区位于linux的内核态地址空间。而在linux中,每个应用程序都有自己的虚拟地址空间,在应用程序中是不能直接访问物理缓冲区的。
为此,linux在文件操作file_operations结构中提供了mmap函数,可将文件的内容映射到用户空间。
对应帧缓冲设备,则可以通过映射操作,将屏幕缓冲区的物理地址映射到用户空间的一段虚拟地址中,之后用户就可以通过读写这段虚拟地址访问屏幕缓冲区,在屏幕上绘图。
4.7 register_framebuffer
register_framebuffer函数用于向内核注册framebuffer设备:
/** * register_framebuffer - registers a frame buffer device * @fb_info: frame buffer info structure * * Registers a frame buffer device @fb_info. * * Returns negative errno on error, or zero for success. * */ int register_framebuffer(struct fb_info *fb_info) { int ret; mutex_lock(®istration_lock); ret = do_register_framebuffer(fb_info); mutex_unlock(®istration_lock); return ret; }
在do_register_farmebuffer之前前后加入了互斥锁,可以判断出该操作是线程安全的,我们定位到 do_register_framebuffer函数:
static int do_register_framebuffer(struct fb_info *fb_info) { int i, ret; struct fb_event event; struct fb_videomode mode; if (fb_check_foreignness(fb_info)) return -ENOSYS; ret = do_remove_conflicting_framebuffers(fb_info->apertures, fb_info->fix.id, fb_is_primary_device(fb_info)); if (ret) return ret; if (num_registered_fb == FB_MAX) // 已达到最大注册设备数 return -ENXIO; num_registered_fb++; // 已注册设备计数+1 for (i = 0 ; i < FB_MAX; i++) // 查找空的数组项 if (!registered_fb[i]) break; fb_info->node = i; // 设置fb_info在registered_fb数组中的索引号 atomic_set(&fb_info->count, 1); // 引用计数设置为1 mutex_init(&fb_info->lock); // 初始化互斥锁 mutex_init(&fb_info->mm_lock); // 初始化互斥锁 fb_info->dev = device_create(fb_class, fb_info->device, // 设备创建是在这里完成的,设备名称为fb%d 可以在/dev下看到fb%d设备 次设备号为i MKDEV(FB_MAJOR, i), NULL, "fb%d", i); if (IS_ERR(fb_info->dev)) { /* Not fatal */ printk(KERN_WARNING "Unable to create device for framebuffer %d; errno = %ld\n", i, PTR_ERR(fb_info->dev)); fb_info->dev = NULL; } else fb_init_device(fb_info); // 初始化fb_info部分参数 if (fb_info->pixmap.addr == NULL) { fb_info->pixmap.addr = kmalloc(FBPIXMAPSIZE, GFP_KERNEL); if (fb_info->pixmap.addr) { fb_info->pixmap.size = FBPIXMAPSIZE; fb_info->pixmap.buf_align = 1; fb_info->pixmap.scan_align = 1; fb_info->pixmap.access_align = 32; fb_info->pixmap.flags = FB_PIXMAP_DEFAULT; } } fb_info->pixmap.offset = 0; if (!fb_info->pixmap.blit_x) fb_info->pixmap.blit_x = ~(u32)0; if (!fb_info->pixmap.blit_y) fb_info->pixmap.blit_y = ~(u32)0; if (!fb_info->modelist.prev || !fb_info->modelist.next) INIT_LIST_HEAD(&fb_info->modelist); // 初始化双向链表 if (fb_info->skip_vt_switch) pm_vt_switch_required(fb_info->dev, false); else pm_vt_switch_required(fb_info->dev, true); fb_var_to_videomode(&mode, &fb_info->var); fb_add_videomode(&mode, &fb_info->modelist); registered_fb[i] = fb_info; // 设置数组第i个元素 event.info = fb_info; if (!lockless_register_fb) console_lock(); else atomic_inc(&ignore_console_lock_warning); if (!lock_fb_info(fb_info)) { ret = -ENODEV; goto unlock_console; } ret = 0; fb_notifier_call_chain(FB_EVENT_FB_REGISTERED, &event); unlock_fb_info(fb_info); unlock_console: if (!lockless_register_fb) console_unlock(); else atomic_dec(&ignore_console_lock_warning); return ret; }
do_register_farmebuffer函数首先从registered_fb数组中查找空的数组项,然后填充fb_info结构体,赋给这个空的数组项中。在这里还创建了设备节点(前面创建字符设备未完成的工作)。
从这里我们可以看出,register_framebuffer()函数通过注册各种各样的fb_info,来让内核支持多种framebuffer设备,并且以/dev/fb%d的形式命名。
五、platform设备注册(s3c2410-lcd)
5.1 LCD相关结构体
我们定位到arch/arm/plat-samsung/include/plat/fb-s3c2410.h头文件:
struct s3c2410fb_hw { unsigned long lcdcon1; unsigned long lcdcon2; unsigned long lcdcon3; unsigned long lcdcon4; unsigned long lcdcon5; }; /* LCD description */ struct s3c2410fb_display { /* LCD type */ unsigned type; /* Screen size */ unsigned short width; unsigned short height; /* Screen info */ unsigned short xres; unsigned short yres; unsigned short bpp; unsigned pixclock; /* pixclock in picoseconds */ unsigned short left_margin; /* value in pixels (TFT) or HCLKs (STN) */ unsigned short right_margin; /* value in pixels (TFT) or HCLKs (STN) */ unsigned short hsync_len; /* value in pixels (TFT) or HCLKs (STN) */ unsigned short upper_margin; /* value in lines (TFT) or 0 (STN) */ unsigned short lower_margin; /* value in lines (TFT) or 0 (STN) */ unsigned short vsync_len; /* value in lines (TFT) or 0 (STN) */ /* lcd configuration registers */ unsigned long lcdcon5; }; struct s3c2410fb_mach_info { struct s3c2410fb_display *displays; /* attached displays info */ unsigned num_displays; /* number of defined displays */ unsigned default_display; /* GPIOs */ unsigned long gpcup; unsigned long gpcup_mask; unsigned long gpccon; unsigned long gpccon_mask; unsigned long gpdup; unsigned long gpdup_mask; unsigned long gpdcon; unsigned long gpdcon_mask; /* lpc3600 control register */ unsigned long lpcsel; };
这里定义了s3c24xx系列SOC关于LCD相关配置的结构体:
- s3c2410fb_hw:定义了s3c2440 LCD控制寄存器需要配置的值;
- s3c2410fb_display:定义了开发板所使用的的LCD描述信息,比如LCD的时序参数、LCD屏的宽、高等;
- s3c2410fb_mach_info :包含了s3c2410fb_display以及s3c2440 GPIO相关信息;
5.2 结构体全局变量
我们定位到 arch/arm/mach-s3c24xx/mach-smdk2440.c文件,在这个里面我们可以看到LCD芯片时序相关的信息定义:
/* LCD driver info */ static struct s3c2410fb_display smdk2440_lcd_cfg __initdata = { .lcdcon5 = S3C2410_LCDCON5_FRM565 | S3C2410_LCDCON5_INVVLINE | S3C2410_LCDCON5_INVVFRAME | S3C2410_LCDCON5_PWREN | S3C2410_LCDCON5_HWSWP, .type = S3C2410_LCDCON1_TFT, .width = 240, .height = 320, .pixclock = 166667, /* 每个像素时长,10^12/VCLK */ .xres = 240, .yres = 320, .bpp = 16, .left_margin = 20, .right_margin = 8, .hsync_len = 4, .upper_margin = 8, .lower_margin = 7, .vsync_len = 4, }; static struct s3c2410fb_mach_info smdk2440_fb_info __initdata = { .displays = &smdk2440_lcd_cfg, .num_displays = 1, .default_display = 0, #if 0 /* currently setup by downloader */ .gpccon = 0xaa940659, .gpccon_mask = 0xffffffff, .gpcup = 0x0000ffff, .gpcup_mask = 0xffffffff, .gpdcon = 0xaa84aaa0, .gpdcon_mask = 0xffffffff, .gpdup = 0x0000faff, .gpdup_mask = 0xffffffff, #endif .lpcsel = ((0xCE6) & ~7) | 1<<4, };
可以看到这里声明了全局变量smdk2440_lcd_cfg、smdk2440_fb_info并进行了初始化,如果我们想支持我们LCD的话,实际上只要修改这些配置信息即可。
5.3 smdk2440_machine_init
linux内核启动的时候会根据uboot中设置的机器id执行相应的初始化工作,比如.init_machine、.init_irq:
static void __init smdk2440_machine_init(void) { s3c24xx_fb_set_platdata(&smdk2440_fb_info); // 设置s3c_device_lcd->dev.platform_data=&smdk2440_fb_info s3c_i2c0_set_platdata(NULL); platform_add_devices(smdk2440_devices, ARRAY_SIZE(smdk2440_devices)); // s3c2440若干个platform设备注册 usb host controller、lcd、wdt等 smdk_machine_init(); // s3c24x0系列若干个platform设备注册(通用) } MACHINE_START(S3C2440, "SMDK2440") /* Maintainer: Ben Dooks <ben-linux@fluff.org> */ .atag_offset = 0x100, .init_irq = s3c2440_init_irq, .map_io = smdk2440_map_io, .init_machine = smdk2440_machine_init, .init_time = smdk2440_init_time, MACHINE_END
5.3.1 s3c24xx_fb_set_platdata
这里我们只关注smdk2440_fb_info相关的代码,我们定位到s3c24xx_fb_set_platdata函数,位于 arch/arm/plat-samsung/devs.c文件中,实际上在这个文件里根据我们内核编译配置的宏,注册不同的platform设备,比如这里我们定义了名字为"s3c2410-lcd"的platform设备:
/* LCD Controller */ #ifdef CONFIG_PLAT_S3C24XX static struct resource s3c_lcd_resource[] = { [0] = DEFINE_RES_MEM(S3C24XX_PA_LCD, S3C24XX_SZ_LCD), // 定义内存资源 起始地址0x4D000000(LCD相关寄存器基地址)、大小为1M [1] = DEFINE_RES_IRQ(IRQ_LCD), }; struct platform_device s3c_device_lcd = { // 定义platform设备 .name = "s3c2410-lcd", .id = -1, .num_resources = ARRAY_SIZE(s3c_lcd_resource), .resource = s3c_lcd_resource, .dev = { .dma_mask = &samsung_device_dma_mask, .coherent_dma_mask = DMA_BIT_MASK(32), } }; void __init s3c24xx_fb_set_platdata(struct s3c2410fb_mach_info *pd) // pd = &smdk2440_fb_info { struct s3c2410fb_mach_info *npd; npd = s3c_set_platdata(pd, sizeof(*npd), &s3c_device_lcd); // 设置s3c_device_lcd->dev.platform_data=&smdk2440_fb_info if (npd) { npd->displays = kmemdup(pd->displays, sizeof(struct s3c2410fb_display) * npd->num_displays, GFP_KERNEL); if (!npd->displays) printk(KERN_ERR "no memory for LCD display data\n"); } else { printk(KERN_ERR "no memory for LCD platform data\n"); } } #endif /* CONFIG_PLAT_S3C24XX */
在s3c24xx_fb_set_platdata这里我们调用了s3c_set_platdata函数,该函数设置s3c_device_lcd->dev.platform_data=&smdk2440_fb_info。
5.3.2 s3c_set_platdata
s3c_set_platdata定义在arch/arm/plat-samsung/platformdata.c文件中:
void __init *s3c_set_platdata(void *pd, size_t pdsize, // pd = &smdk2440_fb_info , pdev = &s3c_device_lcd struct platform_device *pdev) { void *npd; if (!pd) { /* too early to use dev_name(), may not be registered */ printk(KERN_ERR "%s: no platform data supplied\n", pdev->name); return NULL; } npd = kmemdup(pd, pdsize, GFP_KERNEL); if (!npd) return NULL; pdev->dev.platform_data = npd; return npd; }
这个函数主要是用来设置pdev->dev的platform_data成员,是个void *类型,可以给平台driver提供各种数据(比如:GPIO引脚等等)。
5.4 platform设备注册
我们已经定义了LCD相关的platform_device设备s3c_device_lcd,并进行了初始化,那platform设备啥时候注册的呢?
我们定位到smdk2440_machine_init中的如下函数:
platform_add_devices(smdk2440_devices, ARRAY_SIZE(smdk2440_devices));
这里利用platform_add_devices进行若干个platform设备的注册,该函数还是通过调用platform_device_register实现platform设备注册:
/** * platform_add_devices - add a numbers of platform devices * @devs: array of platform devices to add * @num: number of platform devices in array */ int platform_add_devices(struct platform_device **devs, int num) { int i, ret = 0; for (i = 0; i < num; i++) { ret = platform_device_register(devs[i]); if (ret) { while (--i >= 0) platform_device_unregister(devs[i]); break; } } return ret; }
smdk2440_devices中就包含了s3c_device_lcd:
static struct platform_device *smdk2440_devices[] __initdata = { &s3c_device_ohci, &s3c_device_lcd, &s3c_device_wdt, &s3c_device_i2c0, &s3c_device_iis, &smdk2440_device_eth, };
六、platform驱动注册(s3c2410-lcd)
既然注册了名字为"s3c2410-lcd"的platform设备,那么名字为"s3c2410-lcd"的platform驱动在哪里注册的呢?
其相关代码为drivers/video/fbdev/s3c2410fb.c,在该文件里构建了fb_info结构体。我们可以在该文件定位到驱动模块的入口和出口:
module_init(s3c2410fb_init);
module_exit(s3c2410fb_cleanup);
6.1 入口函数
我们定位到fbmem.c的入口函数,也就是s3c2410fb_init:
int __init s3c2410fb_init(void) { int ret = platform_driver_register(&s3c2410fb_driver); if (ret == 0) ret = platform_driver_register(&s3c2412fb_driver); return ret; }
看到这里是不是有点意外,这里是通过platform_driver_register函数注册了一个platform驱动。
在plaftrom总线设备驱动模型中,我们知道当内核中有platform设备的.name名称和platform驱动s3c2410fb_driver里driver的name相同,会调用到platform_driver里的成员.probe,在这里就是s3c2410fb_probe函数。
static struct platform_driver s3c2410fb_driver = { .probe = s3c2410fb_probe, .remove = s3c2410fb_remove, .suspend = s3c2410fb_suspend, .resume = s3c2410fb_resume, .driver = { .name = "s3c2410-lcd", }, };
6.2 s3c2410fb_probe
static int s3c2410fb_probe(struct platform_device *pdev) { return s3c24xxfb_probe(pdev, DRV_S3C2410); }
定位到s3c24xxfb_probe:smdk2440_fb_info
static int s3c24xxfb_probe(struct platform_device *pdev, enum s3c_drv_type drv_type) { struct s3c2410fb_info *info; struct s3c2410fb_display *display; struct fb_info *fbinfo; struct s3c2410fb_mach_info *mach_info; struct resource *res; int ret; int irq; int i; int size; u32 lcdcon1; mach_info = dev_get_platdata(&pdev->dev); // 这里实际获取到的就是smdk2440_fb_info,类型为s3c2440fb_mach_info if (mach_info == NULL) { dev_err(&pdev->dev, "no platform data for lcd, cannot attach\n"); return -EINVAL; } if (mach_info->default_display >= mach_info->num_displays) { // 默认使用的LCD索引号 >= 支持的LCD总数 dev_err(&pdev->dev, "default is %d but only %d displays\n", mach_info->default_display, mach_info->num_displays); return -EINVAL; } display = mach_info->displays + mach_info->default_display; // 获取使用的LCD描述信息 irq = platform_get_irq(pdev, 0); if (irq < 0) { dev_err(&pdev->dev, "no irq for device\n"); return -ENOENT; } fbinfo = framebuffer_alloc(sizeof(struct s3c2410fb_info), &pdev->dev); // 分配一个fb_info结构体,额外分配s3c2410fb_info大小的内存,初始化fbinfo->device = &pdev->dev if (!fbinfo) return -ENOMEM; platform_set_drvdata(pdev, fbinfo); // 设置pdev->dev.driver_data = fbinfo info = fbinfo->par; // 获取成员par info->dev = &pdev->dev; // 初始化par成员 info->drv_type = drv_type; res = platform_get_resource(pdev, IORESOURCE_MEM, 0); // 获取第一个内存资源,地址范围0x4D000000~(0x4D000000+1MB) if (res == NULL) { dev_err(&pdev->dev, "failed to get memory registers\n"); ret = -ENXIO; goto dealloc_fb; } size = resource_size(res); // 1MB info->mem = request_mem_region(res->start, size, pdev->name); // 动态申请内存 if (info->mem == NULL) { // 内存已经被使用 dev_err(&pdev->dev, "failed to get memory region\n"); ret = -ENOENT; goto dealloc_fb; } info->io = ioremap(res->start, size); // 将LCD相关寄存器起始物理地址映射到虚拟地址,并返回虚拟地址 if (info->io == NULL) { dev_err(&pdev->dev, "ioremap() of registers failed\n"); ret = -ENXIO; goto release_mem; } if (drv_type == DRV_S3C2412) info->irq_base = info->io + S3C2412_LCDINTBASE; else info->irq_base = info->io + S3C2410_LCDINTBASE; dprintk("devinit\n"); strcpy(fbinfo->fix.id, driver_name); // 设置fb_info成员id为s3c2410fb /* Stop the video */ lcdcon1 = readl(info->io + S3C2410_LCDCON1); // S3C2410_LCDCON1=0,从而得到LCDCON1寄存器地址 读取寄存器值 writel(lcdcon1 & ~S3C2410_LCDCON1_ENVID, info->io + S3C2410_LCDCON1); // 输出使能位设置为禁止 // 设置LCD不可变参数 fbinfo->fix.type = FB_TYPE_PACKED_PIXELS; fbinfo->fix.type_aux = 0; fbinfo->fix.xpanstep = 0; fbinfo->fix.ypanstep = 0; fbinfo->fix.ywrapstep = 0; fbinfo->fix.accel = FB_ACCEL_NONE; // 设置LCD可变参数 fbinfo->var.nonstd = 0; fbinfo->var.activate = FB_ACTIVATE_NOW; fbinfo->var.accel_flags = 0; fbinfo->var.vmode = FB_VMODE_NONINTERLACED; // 设置LCD操作函数 fbinfo->fbops = &s3c2410fb_ops; fbinfo->flags = FBINFO_FLAG_DEFAULT; fbinfo->pseudo_palette = &info->pseudo_pal; // 清空调色板数组 for (i = 0; i < 256; i++) info->palette_buffer[i] = PALETTE_BUFF_CLEAR; ret = request_irq(irq, s3c2410fb_irq, 0, pdev->name, info); // 申请中断 if (ret) { dev_err(&pdev->dev, "cannot get irq %d - err %d\n", irq, ret); ret = -EBUSY; goto release_regs; } info->clk = clk_get(NULL, "lcd"); // 获取lcd时钟 if (IS_ERR(info->clk)) { dev_err(&pdev->dev, "failed to get lcd clock source\n"); ret = PTR_ERR(info->clk); goto release_irq; }
clk_prepare_enable(info->clk); // 使能时钟 dprintk("got and enabled clock\n"); usleep_range(1000, 1100); info->clk_rate = clk_get_rate(info->clk); // 获取时钟频率 /* find maximum required memory size for display */ for (i = 0; i < mach_info->num_displays; i++) { unsigned long smem_len = mach_info->displays[i].xres; smem_len *= mach_info->displays[i].yres; smem_len *= mach_info->displays[i].bpp; smem_len >>= 3; if (fbinfo->fix.smem_len < smem_len) fbinfo->fix.smem_len = smem_len; } /* Initialize video memory */ ret = s3c2410fb_map_video_memory(fbinfo); //为framgebuffer缓冲区动态申请内存空间,物理地址为fbinfo->fix.smem_start,虚拟地址为fbinfo->screen_base,大小为页对齐(fbinbfo->fix.smem_len) if (ret) { dev_err(&pdev->dev, "Failed to allocate video RAM: %d\n", ret); ret = -ENOMEM; goto release_clock; } dprintk("got video memory\n"); fbinfo->var.xres = display->xres; fbinfo->var.yres = display->yres; fbinfo->var.bits_per_pixel = display->bpp; s3c2410fb_init_registers(fbinfo); // 设置GPIO,配置GPCUP、GPCCON、GPDUP、GPDCON为LCD功能 实际上就是给寄存器赋值,值来自smdk2440_fb_info中设置的值 s3c2410fb_check_var(&fbinfo->var, fbinfo); ret = s3c2410fb_cpufreq_register(info); // 根据LCD可变参数、lefrt_margin、right_margin、以及LCD控制器时钟频率(HCLK),计算LCD控制器时序参数,并设置相应控制寄存器值 s3c2410fb_calculate_tft_lcd_regs if (ret < 0) { dev_err(&pdev->dev, "Failed to register cpufreq\n"); goto free_video_memory; } ret = register_framebuffer(fbinfo); // 注册设备 if (ret < 0) { dev_err(&pdev->dev, "Failed to register framebuffer device: %d\n", ret); goto free_cpufreq; } /* create device files */ ret = device_create_file(&pdev->dev, &dev_attr_debug); if (ret) dev_err(&pdev->dev, "failed to add debug attribute\n"); dev_info(&pdev->dev, "fb%d: %s frame buffer device\n", fbinfo->node, fbinfo->fix.id); return 0; free_cpufreq: s3c2410fb_cpufreq_deregister(info); free_video_memory: s3c2410fb_unmap_video_memory(fbinfo); release_clock: clk_disable_unprepare(info->clk); clk_put(info->clk); release_irq: free_irq(irq, info); release_regs: iounmap(info->io); release_mem: release_mem_region(res->start, size); dealloc_fb: framebuffer_release(fbinfo); return ret; }
这段代码是在太长了,我直接挑重点说:
- 分配一个struct fb_info结构体变量fbinfo;
- 设置fbinfo:
- 设置LCD不可变参数;fbinfo->fix;
- 设置LCD可变参数;fbinfo->var;
- 设置LCD操作函数;fbinfo->fbops;
- 设置其他成员,fbinfo->flags、fbinfo->pseudo_palette、fbinfo->clk_rate;
- 硬件相关的操作:
- 将LCD相关寄存器起始物理地址映射到虚拟地址,并返回虚拟地址;
- 设置中断:通过request_irq注册中断,中断类型为IRQ_LCD,中断处理函数为s3c2410fb_irq;
- 设置framebuffer缓冲器地址:通过s3c2410fb_map_video_memory申请framebuffer显存,然后设置LCDSADDR1、LCDSADDR2、LCDSADDR3寄存器;
- 配置引脚:通过s3c2410fb_init_registers设置GPIO端口C和GPIO端口D用于LCD;
- 设置LCD控制器时序参数:通过s3c2410fb_cpufreq_register设置LCDCON1、LCDCON2、LCDCON3、LCDCON4、LCDCON5寄存器;
- 注册fb_info结构体;
需要注意的是这里并没有打开背光灯,开不开背光灯影响不大。
6.2.1 struct s3c2410fb_info
struct s3c2410fb_info定义在drivers/video/fbdev/s3c2410fb.h:
struct s3c2410fb_info { struct device *dev; // 设备基类 struct clk *clk; // lcd时钟 struct resource *mem; // i/o 内存地址(虚拟地址) void __iomem *io; // lcd控制器寄存器基地址(虚拟地址) void __iomem *irq_base; enum s3c_drv_type drv_type; struct s3c2410fb_hw regs; // lcd控制寄存器 unsigned long clk_rate; // 时钟频率 unsigned int palette_ready; #ifdef CONFIG_ARM_S3C24XX_CPUFREQ struct notifier_block freq_transition; #endif /* keep these registers in case we need to re-write palette */ u32 palette_buffer[256]; u32 pseudo_pal[16]; };
6.3 s3c2410fb_cpufreq_register
该函数主要用来初始化LCD控制器时序参数:
static inline int s3c2410fb_cpufreq_register(struct s3c2410fb_info *info) { info->freq_transition.notifier_call = s3c2410fb_cpufreq_transition; return cpufreq_register_notifier(&info->freq_transition, CPUFREQ_TRANSITION_NOTIFIER); }
定位到函数s3c2410fb_cpufreq_transition:
static int s3c2410fb_cpufreq_transition(struct notifier_block *nb, unsigned long val, void *data) { struct s3c2410fb_info *info; struct fb_info *fbinfo; long delta_f; info = container_of(nb, struct s3c2410fb_info, freq_transition); fbinfo = dev_get_drvdata(info->dev); /* work out change, <0 for speed-up */ delta_f = info->clk_rate - clk_get_rate(info->clk); if ((val == CPUFREQ_POSTCHANGE && delta_f > 0) || (val == CPUFREQ_PRECHANGE && delta_f < 0)) { info->clk_rate = clk_get_rate(info->clk); s3c2410fb_activate_var(fbinfo); } return 0; }
最终定位到s3c2410fb_activate_var:
/* s3c2410fb_activate_var * * activate (set) the controller from the given framebuffer * information */ static void s3c2410fb_activate_var(struct fb_info *info) { struct s3c2410fb_info *fbi = info->par; void __iomem *regs = fbi->io; int type = fbi->regs.lcdcon1 & S3C2410_LCDCON1_TFT; struct fb_var_screeninfo *var = &info->var; int clkdiv; clkdiv = DIV_ROUND_UP(s3c2410fb_calc_pixclk(fbi, var->pixclock), 2); dprintk("%s: var->xres = %d\n", __func__, var->xres); dprintk("%s: var->yres = %d\n", __func__, var->yres); dprintk("%s: var->bpp = %d\n", __func__, var->bits_per_pixel); if (type == S3C2410_LCDCON1_TFT) { // 走这里 TFT真彩 s3c2410fb_calculate_tft_lcd_regs(info, &fbi->regs); // 计算LCD控制器控制寄存器的值 --clkdiv; if (clkdiv < 0) clkdiv = 0; } else { s3c2410fb_calculate_stn_lcd_regs(info, &fbi->regs); if (clkdiv < 2) clkdiv = 2; } fbi->regs.lcdcon1 |= S3C2410_LCDCON1_CLKVAL(clkdiv); /* write new registers */ dprintk("new register set:\n"); dprintk("lcdcon[1] = 0x%08lx\n", fbi->regs.lcdcon1); dprintk("lcdcon[2] = 0x%08lx\n", fbi->regs.lcdcon2); dprintk("lcdcon[3] = 0x%08lx\n", fbi->regs.lcdcon3); dprintk("lcdcon[4] = 0x%08lx\n", fbi->regs.lcdcon4); dprintk("lcdcon[5] = 0x%08lx\n", fbi->regs.lcdcon5); writel(fbi->regs.lcdcon1 & ~S3C2410_LCDCON1_ENVID, // 向LCD控制寄存器写入对应值 regs + S3C2410_LCDCON1); // LCD控制器寄存器基地址 + 控制寄存器偏移 (虚拟地址) writel(fbi->regs.lcdcon2, regs + S3C2410_LCDCON2); writel(fbi->regs.lcdcon3, regs + S3C2410_LCDCON3); writel(fbi->regs.lcdcon4, regs + S3C2410_LCDCON4); writel(fbi->regs.lcdcon5, regs + S3C2410_LCDCON5); /* set lcd address pointers */ s3c2410fb_set_lcdaddr(info); fbi->regs.lcdcon1 |= S3C2410_LCDCON1_ENVID, // 使能LCD writel(fbi->regs.lcdcon1, regs + S3C2410_LCDCON1); }
这里我们简单介绍一下CLKVAL的计算方法,这里计算方式如下:
$$clkdiv =\frac{clk\_rate * pixclock} {2^{12}*2}-1 $$
clk_rate为HCLK频率,也就是100HMz,而 pixclock为每个像素时长,由于我们采用T35屏幕像素时钟信号$VCLK=6.4MHz$,对应每像素时长1/(6.4*10^6)*10^12=156250皮秒。
所以:
$$VCLK=\frac{10^{12}}{pixclock}$$
因此可以计算出:
$$CLKVAL=HCLK/VCLK/2−1=\frac{HCLK*pixclock}{2*10^{12}}-1$$
6.4 s3c2410fb_calculate_tft_lcd_regs
s3c2410fb_calculate_tft_lcd_regs函数用于计算LCD控制寄存器的值,并保存到regs对应成员变量中。
/* s3c2410fb_calculate_tft_lcd_regs * * calculate register values from var settings */ static void s3c2410fb_calculate_tft_lcd_regs(const struct fb_info *info, struct s3c2410fb_hw *regs) { const struct s3c2410fb_info *fbi = info->par; const struct fb_var_screeninfo *var = &info->var; switch (var->bits_per_pixel) { case 1: regs->lcdcon1 |= S3C2410_LCDCON1_TFT1BPP; break; case 2: regs->lcdcon1 |= S3C2410_LCDCON1_TFT2BPP; break; case 4: regs->lcdcon1 |= S3C2410_LCDCON1_TFT4BPP; break; case 8: regs->lcdcon1 |= S3C2410_LCDCON1_TFT8BPP; regs->lcdcon5 |= S3C2410_LCDCON5_BSWP | S3C2410_LCDCON5_FRM565; regs->lcdcon5 &= ~S3C2410_LCDCON5_HWSWP; break; case 16: regs->lcdcon1 |= S3C2410_LCDCON1_TFT16BPP; regs->lcdcon5 &= ~S3C2410_LCDCON5_BSWP; regs->lcdcon5 |= S3C2410_LCDCON5_HWSWP; break; case 32: regs->lcdcon1 |= S3C2410_LCDCON1_TFT24BPP; regs->lcdcon5 &= ~(S3C2410_LCDCON5_BSWP | S3C2410_LCDCON5_HWSWP | S3C2410_LCDCON5_BPP24BL); break; default: /* invalid pixel depth */ dev_err(fbi->dev, "invalid bpp %d\n", var->bits_per_pixel); } /* update X/Y info */ dprintk("setting vert: up=%d, low=%d, sync=%d\n", var->upper_margin, var->lower_margin, var->vsync_len); dprintk("setting horz: lft=%d, rt=%d, sync=%d\n", var->left_margin, var->right_margin, var->hsync_len); regs->lcdcon2 = S3C2410_LCDCON2_LINEVAL(var->yres - 1) | S3C2410_LCDCON2_VBPD(var->upper_margin - 1) | S3C2410_LCDCON2_VFPD(var->lower_margin - 1) | S3C2410_LCDCON2_VSPW(var->vsync_len - 1); regs->lcdcon3 = S3C2410_LCDCON3_HBPD(var->right_margin - 1) | S3C2410_LCDCON3_HFPD(var->left_margin - 1) | S3C2410_LCDCON3_HOZVAL(var->xres - 1); regs->lcdcon4 = S3C2410_LCDCON4_HSPW(var->hsync_len - 1); }
七、platform总线设备驱动(s3c2410-lcd)
我们已经介绍了:
- name为s3c2410-lcd的platform设备的注册;
- name为s3c2410-lcd的platform驱动的注册;
我们把这两部分代码整理成如下框框,方便查看:
从这张图我们可以学习linux内核platform总线设备驱动的编写,以及如何将各部分代码进行拆分放到linux各个目录结构下。
八、修改内核自带的LCD驱动
8.1 修改smdk2440_lcd_cfg
这里主要就是设置LCD时序相关参数,我们在Mini2440裸机开发之LCD基础中介绍过型号为LCD-P35(LQ035Q1DG04和ZQ3506_V0手册通用)的时序参数设置。这里我们以另外一款LCD为例,型号为LCD-T35(TD035STEB4),参考TD035STEB4 LCD数据手册上的参数性能,见下表:
LCD 大小为240×320,16BPP数据格式,则:$$HOZVAL=240-1,LINEVAL=240-1$$
水平同步信号的脉宽、前肩和后肩分别取10、10和20,则:$$HSPW=10-1=9,HFPD=10−1=9,HBPD=20−1=19$$
垂直同步信号的脉宽、前肩和后肩分别取2、2和2,则:$$VSPW=2-1=1,VFPD=2−1=1,VBPD=2−1=1$$
HCLK的频率为100MHz,要想驱动像素时钟信号为6.4MHz的LCD屏,则通过上式计算CLKVAL值:
$$CLKVAL=HCLK/VCLK/2−1=100/6.4/2−1=6.8$$
结果CLKVAL为6.8MHZ,取整后(值为7)放入寄存器LCDCON1中相应的位置即可。由于CLKVAL进行了取整,因此我们把取整后的值代入上式,重新计算VCLK,得到$VCLK=6.25MHz$。
修改arch/arm/mach-s3c24xx/mach-smdk2440.c文件,定义宏:
/* LCD T35参数设定 */ #define LCD_WIDTH 240 /* LCD面板的行宽 */ #define LCD_HEIGHT 320 /* LCD面板的列宽 */ #define VSPW 1 /* 通过计算无效行数垂直同步脉冲宽度决定VSYNC脉冲的高电平宽度 */ #define VBPD 1 /* 垂直同步周期后的无效行数 */ #define LINEVAL (LCD_HEIGHT-1) /* LCD的垂直宽度-1 */ #define VFPD 1 /* 垂直同步周期前的的无效行数 */ #define CLKVAL 7 /* VCLK = HCLK / [(CLKVAL + 1) × 2] */ #define HSPW 9 /* 通过计算VCLK的数水平同步脉冲宽度决定HSYNC脉冲的高电平宽度 */ #define HBPD 19 /* 描述水平后沿为HSYNC的下降沿与有效数据的开始之间的VCLK周期数 */ #define HOZVAL (LCD_WIDTH-1) /* LCD的水平宽度-1 */ #define HFPD 9 /* 水平后沿为有效数据的结束与HSYNC的上升沿之间的VCLK周期数 */
修改中smdk2440_lcd_cfg:
static struct s3c2410fb_display smdk2440_lcd_cfg __initdata = { .lcdcon5 = S3C2410_LCDCON5_FRM565 | S3C2410_LCDCON5_INVVLINE | S3C2410_LCDCON5_INVVFRAME | S3C2410_LCDCON5_PWREN | S3C2410_LCDCON5_HWSWP, .type = S3C2410_LCDCON1_TFT, .width = LCD_WIDTH, .height = LCD_HEIGHT, .pixclock = 156250, /* 每个像素时长,10^12/VCLK */ .xres = LCD_WIDTH, .yres = LCD_HEIGHT, .bpp = 16, .left_margin = HFPD, // HFPD .right_margin = HBPD, // HBPD .hsync_len = HSPW, // HSPW .upper_margin = VBPD, // VBPD .lower_margin = VFPD, // VFPD .vsync_len = VSPW, // VSPW };
8.2 修改smdk2440_fb_info
修改arch/arm/mach-s3c24xx/mach-smdk2440.c中smdk2440_lcd_cfg:
static struct s3c2410fb_mach_info smdk2440_fb_info __initdata = { .displays = &smdk2440_lcd_cfg, .num_displays = 1, .default_display = 0, #if 1 /* currently setup by downloader */ .gpccon = 0xaaaaaaaa, .gpccon_mask = 0xffffffff, .gpcup = 0xffffffff, .gpcup_mask = 0xffffffff, .gpdcon = 0xaaaaaaaa, .gpdcon_mask = 0xffffffff, .gpdup = 0xffffffff, .gpdup_mask = 0xffffffff, #endif .lpcsel = ((0xCE6) & ~7) | 1<<1, // 第一位设置为1 选择输出分片率类型0:320 * 240 1:240*320 };
8.3 配置启动logo
执行如下命令:
root@zhengyang:/work/sambashare/linux-5.2.8# make menuconfig
配置内核,显示启动logo:
Device Drivers ---> Graphics support ---> [*] Bootup logo ---> Frame buffer Devices ---> <*> Support for frame buffer devices ---> <*> S3C2410 LCD framebuffer support // 支持S3C2410、S3C2440
保存文件,输入文件名s3c2440_defconfig,在当前路径下生成s3c2440_defconfig:存档:
mv s3c2440_defconfig ./arch/arm/configs/
8.4 编译内核
此时重新执行:
make distclean make s3c2440_defconfig make uImage V=1
将uImage复制到tftp服务器路径下:
cp /work/sambashare/linux-5.2.8/arch/arm/boot/uImage /work/tftpboot/
8.5 烧录内核
开发板uboot启动完成后,内核启动前,按下任意键,进入uboot,可以通过print查看uboot中已经设置的环境变量。
设置开发板ip地址,从而可以使用网络服务:
SMDK2440 # set ipaddr 192.168.0.105 SMDK2440 # save Saving Environment to NAND... Erasing NAND... Erasing at 0x40000 -- 100% complete. Writing to NAND... OK SMDK2440 # ping 192.168.0.200 dm9000 i/o: 0x20000000, id: 0x90000a46 DM9000: running in 16 bit mode MAC: 08:00:3e:26:0a:5b operating at unknown: 0 mode Using dm9000 device host 192.168.0.200 is alive
设置tftp服务器地址,也就是我们ubuntu服务器地址:
set serverip 192.168.0.200 save
下载内核到内存,并写NAND FLASH:
tftp 30000000 uImage nand erase.part kernel nand write 30000000 kernel
运行结果如下:
SMDK2440 # tftp 30000000 uImage dm9000 i/o: 0x20000000, id: 0x90000a46 DM9000: running in 16 bit mode MAC: 08:00:3e:26:0a:5b operating at unknown: 0 mode Using dm9000 device TFTP from server 192.168.0.200; our IP address is 192.168.0.188 Filename 'uImage'. Load address: 0x30000000 Loading: *################################################################# ################################################################# ################################################################# ############################################################## 429.7 KiB/s done Bytes transferred = 3766128 (397770 hex) SMDK2440 # nand erase.part kernel NAND erase.part: device 0 offset 0x60000, size 0x400000 Erasing at 0x60000 -- 3% complete. Erasing at 0x80000 -- 6% complete. Erasing at 0xa0000 -- 9% complete. Erasing at 0xc0000 -- 12% complete. Erasing at 0xe0000 -- 15% complete. Erasing at 0x100000 -- 18% complete. Erasing at 0x120000 -- 21% complete. Erasing at 0x140000 -- 25% complete. Erasing at 0x160000 -- 28% complete. Erasing at 0x180000 -- 31% complete. Erasing at 0x1a0000 -- 34% complete. Erasing at 0x1c0000 -- 37% complete. Erasing at 0x1e0000 -- 40% complete. Erasing at 0x200000 -- 43% complete. Erasing at 0x220000 -- 46% complete. Erasing at 0x240000 -- 50% complete. Erasing at 0x260000 -- 53% complete. Erasing at 0x280000 -- 56% complete. Erasing at 0x2a0000 -- 59% complete. Erasing at 0x2c0000 -- 62% complete. Erasing at 0x2e0000 -- 65% complete. Erasing at 0x300000 -- 68% complete. Erasing at 0x320000 -- 71% complete. Erasing at 0x340000 -- 75% complete. Erasing at 0x360000 -- 78% complete. Erasing at 0x380000 -- 81% complete. Erasing at 0x3a0000 -- 84% complete. Erasing at 0x3c0000 -- 87% complete. Erasing at 0x3e0000 -- 90% complete. Erasing at 0x400000 -- 93% complete. Erasing at 0x420000 -- 96% complete. Erasing at 0x440000 -- 100% complete. OK SMDK2440 # nand write 30000000 kernel NAND write: device 0 offset 0x60000, size 0x400000 4194304 bytes written: OK
在LCD参数设置过程中,为了查看寄存器设置的参数,我在drivers/video/fbdev/s3c2410fb.c中多处地方输出了寄存器的值信息,这样就可以在linux启动过程中看到寄存器的值,从而知道寄存器值有没有设置成功.
比如在s3c24xxfb_probe函数最后加入:
printk("lcdcon[1] = 0x%08lx\n", info->regs.lcdcon1); printk("lcdcon[2] = 0x%08lx\n", info->regs.lcdcon2); printk("lcdcon[3] = 0x%08lx\n", info->regs.lcdcon3); printk("lcdcon[4] = 0x%08lx\n", info->regs.lcdcon4); printk("lcdcon[5] = 0x%08lx\n", info->regs.lcdcon5);
截取部分内核启动输出信息如下:
map_video_memory: clear (ptrval):00026000 map_video_memory: dma=339c0000 cpu=(ptrval) size=00026000 got video memory gpcup = 0xffffffff gpcup = 0xaaaaaaaa gpcup = 0xffffffff gpcup = 0xaaaaaaaa gpcup = 0xffffffff gpccon = 0xaaaaaaaa cpdup = 0xffffffff gpdcon = 0xaaaaaaaa s3c2410fb_activate_var: var->xres = 240 s3c2410fb_activate_var: var->yres = 320 s3c2410fb_activate_var: var->bpp = 16 LCDSADDR1 = 0x19ce0000 LCDSADDR2 = 0x19cf2c00 LCDSADDR3 = 0x000000f0 Console: switching to colour frame buffer device 30x40 s3c2410-lcd s3c2410-lcd: fb0: s3c2410fb frame buffer device lcdcon[1] = 0x00000779 lcdcon[2] = 0x014fc041 lcdcon[3] = 0x0098ef09 lcdcon[4] = 0x00000009 lcdcon[5] = 0x00000f09
8.6 演示
修改根文件系统inittab文件:
root@zhengyang:/work/nfs_root/rootfs# cd /work/nfs_root/rootfs
root@zhengyang:/work/nfs_root/rootfs# vim etc/inittab
添加如下代码:
tty1::askfirst:-/bin/sh #重启一个sh终端,并将信息输出到tty1设备
重新启动开发板,LCD被点亮,并有“Please press Enter to activate this console.”提示字样。
运行如下命令:
echo hello > /dev/tty1
此时在LCD可以看到由hello显示出来。
安装linux驱动移植-输入子系统示例中介绍的案例驱动:
[root@zy:/]# insmod button_dev.ko button_dev: loading out-of-tree module taints kernel. button driver init input: Unspecified device as /devices/virtual/input/input0 register irq
随便按下K1、K2、...,可以在LCD看到输出信息:
此外我们可以重定位控制台到LCD设备,重新启动开发板,在uboot运行过程中按下任意键,然后设定启动参数:
set bootargs "noinitrd console=tty1 root=/dev/nfs rw nfsroot=192.168.0.200:/work/nfs_root/rootfs ip=192.168.0.105:192.168.0.200:192.168.0.1:255.255.255.0::eth0:off" save
重启开发板,此时启动输出信息就会输出到LCD显示屏上:
参考文章