《网蜂A8实战演练》——7.Linux LCD设备驱动

时间:2022-05-02 09:35:33
第9章 Linux LCD 设备驱动

《网蜂A8实战演练》——7.Linux LCD设备驱动

学习本章前,给大家打一下预防针, LCD 设备驱动本身属于字符设备驱动的范畴,难度属于一般难度。 在层次架构方面较输入子系统或平台总线驱动设备模型要简单,但是它有复杂的硬件操作。所以,建议大家在学习本章内容之前,要先学习第一部分的第 11 章---LCD 控制器原理与应用。 因为, Linux 下的 LCD 设备驱动引用了很大一部分裸机下的硬件相关的代码。

9.1 帧缓冲(framebuffer) 

帧缓冲( framebuffer)是 Linux 为显示设备提供的一个接口,把显存抽象后的一种设备,他允许上层应用程序在图形模式下直接对显示缓冲区进行读写操作。这种操作是抽象的,统一的。用户不必关心物理显存的位置、换页机制等等具体细节。这些都是由 Framebuffer 设备驱动来完成的。

Linux FrameBuffer 本质上只是提供了对图形设备的硬件抽象,在开发者看来, FrameBuffer 是一块显示缓存,往显示缓存中写入特定格式的数据就意味着向屏幕输出内容。所以说 FrameBuffer 就是一块白板。例如对于初始化为 16 位色的 FrameBuffer 来说, FrameBuffer 中的两个字节代表屏幕上一个点,从上到下,从左至右,屏幕位置与内存地址是顺序的线性关系。 帧缓存有个地址,是在内存里。我们通过不停的向 frame buffer 中写入数据, 显示控制器就自动的从 frame buffer 中取数据并显示出来。

帧缓冲设备对应的设备文件为/dev/fb*,如果系统有多个显示卡, Linux 下还可支持多个帧缓冲设备,最多可达 32 个,分别为/dev/fb0 到/dev/fb31 ,而/dev/fb则为当前缺省的帧缓冲设备,通常指向/dev/fb0。当然在嵌入式系统中支持一个显示设备就够了。帧缓冲设备为标准字符设备,主设备号为 29,次设备号则从0 到 31 。分别对应/dev/fb0-/dev/fb31 。

9.1.1 帧缓冲与应用程序的交互

对于用户程序而言,它和其他的设备并没有什么区别,用户可以把 fb 看成是一块内存,既可以向内存中写数据,也可以读数据。 fb 的显示缓冲区位于内核空间,应用程序可以把此空间映射到自己的用户空间,在进行操作。 在应用程序中,操作/dev/fbn 的一般步骤如下:

(1) 打开/dev/fbn 设备文件。

(2) 用 ioctl()操作取得当前显示屏幕的参数,如屏幕分辨率、每个像素点的比特数。根据屏幕参数可计算屏幕缓冲区的大小。

(3) 用 mmap()函数,将屏幕缓冲区映射到用户空间。(4) 映射后就可以直接读/写屏幕缓冲区,进行绘图和图片显示了。

9.1. 2 帧缓冲显示原理

帧缓冲类似一个蓄水池,存放来自用户进程的数据,然后把这些数据再输入显示设备中。对于用户而言,帧缓冲就是内存中的一块区域,可以读、写、映射。只要在初始化阶段把显示设备映射到用户进程空间,可以理解为将屏幕中的每一点和帧缓冲的每一点一一对应起来。这样接下来就可以对这块内存区域填充任何已经定义的像素以及颜色,而屏幕也就可以根据刚才写入的像素及颜色呈现出五彩缤纷的画面。

9.1.3 Linux 帧缓冲设备驱动架构
《网蜂A8实战演练》——7.Linux LCD设备驱动

一个帧缓冲区对应一个 struct fb_info 结构,它包括了帧缓冲设备的属性和操作的完整集合。 具体的 lcd 设备驱动(xxxfb.c)通过 register_framrbuffer()向内核注册帧缓冲设备。

帧缓冲设备提供给用户空间的 file_operations 结构体由 fbmem.c 中的file_operations 提供,而特定的帧缓冲设备 fb_info 结构体的注册、注销以及其中的成员的实现,则由具体的 lcd 设备驱动(xxxfb.c)实现。

9.1.3.1 应用系统调用到 LCD 寄存器的流程

《网蜂A8实战演练》——7.Linux LCD设备驱动

如图 9.2 所示,当应用程序通过系统调用(ioctl/mmap/write)打开/dev/fbX时,就 会 调 用 到 主 设 备 号 为 29 , 次 设 备 号 为 X 的 字 符 设 备 驱 动 里 的(fb_ioctl/fb_mmap/fb_write) 函 数 , 而 这 些 函 数 是 由 framebuffer 驱 动 核 心fbmem.c 文件实现的,并且它提供了一个 register_framebuffer()注册接口函数。当有具体的 LCD 设备驱动时,就会使用 register_framebuffer() 函数向内核注册一个帧缓冲设备。 这时候应用程序的系统调用会进一步调用到具体 LCD 设备驱动里的(xxx_lcd_ioctl/ xxx_lcd_mmap/ xxx_lcd_write)函数,最后操作到 LCD 硬件寄存器等。

9.1. 4 fb_info 结构体

无论是帧缓冲设备驱动(fbmem.c)还是具体的 LCD 设备驱动(xxxfb.c)都离不开 fb_info 结构体,它包括了帧缓冲设备属性和操作的完整描述,包括设备的设置参数、状态以及操作函数指针。


/* 参考 include\linux\fb.h */
struct fb_info{
atomic_t count; //原子变量
int node; //用作次设备号索引
int flags;
struct mutex lock; //用于 open/release/ioctl 函数的锁

struct fb_var_screeninfo var;//可变参数,重点关注
struct fb_fix_screeninfo fix;//固定参数,重点关注
struct fb_monspecs monspecs; //显示器标准
struct work_struct queue; //帧缓冲区队列
struct fb_pixmap pixmap; //图像硬件映射
struct fb_pixmap sprite; //光标硬件映射
struct fb_cmap cmap; //当前颜色表
struct list_head modelist; //模式链表
struct fb_videomode *mode; //当前 video 模式
struct fb_ops *fbops;//帧缓冲操作函数集
char __iomem *screen_base;//显存基地址
unsigned long screen_size; //显存大小
void *pseudo_palette;//16 色颜色表
#define FBINFO_STATE_RUNNING 0
#define FBINFO_STATE_SUSPENDED 1
u32 state; //硬件状态,如挂起
void *fbcon_par; //用作私有数据区
void *par; //info->par 指向了额外多申请内存空间的首地址
};


9.1. 4.1 可变参数(fb_var_screeninfo) 

fb_info结构体有个重要的成员,那就是 fb_var_screeninfo可变参数结构体,fb_var_screeninfo 结构体定义了一些在系统运行期间可以改变的信息。例如像素深度、灰度级、颜色格式、时序,屏幕边缘空白区等。


/* 参考 include\uapi\linux\fb.h */
struct fb_var_screeninfo {
__u32 xres; /*物理分辨率的 X坐标 */
__u32 yres; /*物理分辨率的 Y坐标 */
__u32 xres_virtual; /*虚拟分辨率的 X坐标 */
__u32 yres_virtual; /*虚拟分辨率的 Y坐标 */
__u32 xoffset; /*虚拟分辨率与物理分辨率的 X坐标的偏移量*/
__u32 yoffset; /*虚拟分辨率与物理分辨率的 Y坐标的偏移量*/
__u32 bits_per_pixel; /*每个像素占据多少位 */
struct fb_bitfield red; /*红色位域 */
struct fb_bitfield green; /*绿色位域 */
struct fb_bitfield blue; /*蓝色位域 */
__u32 activate; /*活动模式 */
__u32 grayscale; /* 灰度模式 */

struct fb_bitfield transp; /* 透明色 */
__u32 nonstd; /* 非标准像素格式 */
...
__u32 pixclock; /* 像素时钟,单位是皮秒 */
__u32 left_margin; /* 左侧边缘区 */
__u32 right_margin; /* 右侧边缘区 */
__u32 upper_margin; /* 顶部边缘区 */
__u32 lower_margin;
__u32 hsync_len; /* 水平扫描边缘区 */
__u32 vsync_len; /* 垂直扫描边缘区 */
...
};


其中 fb_bitfield 结构体表示位域,其定义如下:


/* 参考 include\uapi\linux\fb.h */
struct fb_bitfield {
__u32 offset; /* 位域偏移值 */
__u32 length; /* 位域长度 */
__u32 msb_right; /* 等于 1 ,表示最重要一位在右 */
};


图 9.3 标出了各种边缘区在整个屏幕上的位置:

《网蜂A8实战演练》——7.Linux LCD设备驱动

9.1.4.2 固定参数(fb_fix_screeninfo) 
fb_fix_screeninfo,该数据结构定义了一些系统运行期间不能改变的信息,例如设备名,屏幕的像素数量,缓冲区的首址和长度等。这类信息一般通过 ioctl函数获得。

/* 参考 include\uapi\linux\fb.h */
struct fb_fix_screeninfo {
char id[16]; /* 设备名 */
unsigned long smem_start; /* 缓冲区物理地址起始地址 */
__u32 smem_len; /* 缓冲区长度 */
__u32 type; /* 设备类型,例如 TFT STN */
...
__u32 visual; /* 色彩类型,真彩色、假彩色或单色 */
...
__u32 line_length; /* 屏幕上每行的字节数 */
unsigned long mmio_start; /* IO 映射区起始地址(物理地址) */
__u32 mmio_len; /* IO 映射区长度 */
...
__u16 reserved[3]; /* 系统保留*/
};


9.1.4.3 fb_ops 

结构体fb_ino结构体里的成员变量 fb_ops 为指向特定的 LCD 底层操作的函数指针,这些函数需要驱动工程师来完成。
/* 参考 include\linux\fb.h */
struct fb_ops {
struct module *owner;
int (*fb_open)(struct fb_info *info, int user);
int (*fb_release)(struct fb_info *info, int user);
...
/* 检测可变参数,并调整到支持的值 */
int (*fb_check_var)(struct fb_var_screeninfo *var,
struct fb_info *info);
/* 根据 info->var 设置 video 模式 */
int (*fb_set_par)(struct fb_info *info);
/* 设置 color 寄存器,设置调色板 */
int (*fb_setcolreg)(unsigned regno, unsigned red, unsigned green,
unsigned blue, unsigned transp, struct fb_info *info);
...
/* 画一个矩形*/
void (*fb_fillrect) (struct fb_info *info,
const struct fb_fillrect *rect);

/* 数据拷贝*/
void (*fb_copyarea) (struct fb_info *info,
const struct fb_copyarea *region);
/* 图像填充*/
void (*fb_imageblit) (struct fb_info *info,
const struct fb_image *image);
...
};


9.2 帧缓冲设备驱动浅析(fbmem.c) 

看标题,注意关键词,是浅析而不是深入研究。 为什么是浅析呢? 因为这个是 Linux 大牛已经帮我们写好的,通用的、核心的驱动。 它并不代表某一个具体的 LCD 设备驱动,但是它作为应用程序与底层 LCD 设备驱动的中转层,起到了很好的衔接作用。 大概知道一下它的工作原理,还是有必要滴。 钻的太深,我担心你爬不出来啊, 兄弟。

9.2.1 入口函数分析一个函数,从它的入口函数开始分析。

/* 参考 drivers\video\fbmem.c */
#define FB_MAJOR 29 /* /dev/fb* framebuffers */
static int __init fbmem_init(void)
{

/* 注册一个主设备号为 29,文件操作函数为 fb_fops 的字符设备 */
if (register_chrdev(FB_MAJOR,"fb",&fb_fops))
printk("unable to get major %d for fb devs\n", FB_MAJOR);
/* 创建一个 graphics 类 */
fb_class = class_create(THIS_MODULE, "graphics");

return 0;
}

入口函数主要做了二件事,一是注册一个主设备号为 29,文件操作函数为fb_fops 的字符设备;二是创建一个 graphics 类。这二个都是我们所非常熟悉的,但是,细心的读者可能会发现,以往还会多一件事,什么事呢?给你一分钟时间思考……那就是在类下面创建设备,这样 udev 机制就会自动创建设备节点,在这里是/dev/fb0--/dev/fb31 。 但是这里并没有这么做,是不是 Linux 大牛们不小心漏了呢? 你再猜猜? O(∩ _∩ )O 哈哈~,好吧,这问题,留到后面再讲。

9.2.2 fb_fops 实例

static const struct file_operations fb_fops= {
.owner = THIS_MODULE,
.read = fb_read,
.write = fb_write,
.unlocked_ioctl = fb_ioctl,
……
.mmap = fb_mmap,
.open = fb_open,
.release = fb_release,
……
.llseek = default_llseek,
};

看到这里,这不就是我们常见的字符设备驱动里的文件操作集实例吗?对,没错,帧缓冲设备驱动本来就属于字符设备驱动的范畴。

9.2.3 fb_open

假设应用程序 open("/dev/fb0", ...), 主设备号: 29, 次设备号: 0。就会调用到fb_open 函数。
/* 参考 drivers\video\fbmem.c */
static int fb_open(struct inode *inode, struct file *file)
__acquires(&info->lock)
__releases(&info->lock)
{
/* 从设备节点中获取次设备号 */
int fbidx = iminor(inode);
struct fb_info *info;
int res = 0;
/* 通过次设备号来获取一个 fb_info 实例 */
info = get_fb_info(fbidx);
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;
}
/* 将 fb_info 实例赋给文件私有数据,以供其他函数使用 */
file->private_data = info;
/* 如果新的 fb_info 实例中的 fbops->fb_open 存在的话就调用它 */
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;
}


9.2.3.1 

get_fb_info从函数名字上看,获得 fb_info 结构体,简单明了。
/* 参考 drivers\video\fbmem.c */
static struct fb_info *get_fb_info(unsigned int idx)
{
struct fb_info *fb_info;
/* 如果次设备号大于 32,出错 */
if (idx >= FB_MAX)
return ERR_PTR(-ENODEV);
mutex_lock(®istration_lock);
/* 以次设备号为下标,在 registered_fb 数组里找到一项 fb_ino 实例 */
fb_info = registered_fb[idx];
if (fb_info)
atomic_inc(&fb_info->count);

mutex_unlock(®istration_lock);
return fb_info;
}


9.2. 4 

fb_read假设打开/dev/fb0 后, 应用程序 read(),就会调用到 fb_read 函数。
/* 参考 drivers\video\fbmem.c */
static ssize_t
fb_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
unsigned long p = *ppos;
/* 以次设备号为下标,在registered_fb 数组里找到一项fb_ino 实例*/
struct fb_info *info = file_fb_info(file);
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;
/* 如果新的fb_info 实例中的fbops->fb_read 存在的话就调用它*/
if (info->fbops->fb_read)
return info->fbops->fb_read(info, buf, count, ppos);
/* 没有默认的读函数,就从显存的虚拟起始
* 地址读数据,大小为虚拟显存大小
*/
total_size = info->screen_size;
/* 如果虚拟显存大小为了,则设置大小为 fb 缓冲区长度 */
if (total_size == 0)
total_size = info->fix.smem_len;
/* 如果读的偏移值大于 total_size,则返回 0 个字节 */
if (p >= total_size)
return 0;

/* 如果读的总量大于 total_size,则调整
* 最多读 total_size 个字节的数据
*/
if (count >= total_size)
count = total_size;
/* 调整读的位置及能读多少字节 */
if (count + p > total_size)
count = total_size - p;
/* 分配内存,最大分配 4K 的大小 */
buffer = kmalloc((count > PAGE_SIZE) ? PAGE_SIZE : count,
GFP_KERNEL);
if (!buffer)
return -ENOMEM;
/* 源虚拟基地址 */
src = (u8 __iomem *) (info->screen_base + p);
/* 如果新的 fb_info 实例中的 fbops->fb_sync 存在的话就调用它 */
if (info->fbops->fb_sync)
info->fbops->fb_sync(info);
while (count) {
/* 读多少计数变量,单位为byte */
c = (count > PAGE_SIZE) ? PAGE_SIZE : count;
/* dst 指向刚分配buffer 内存的首地址*/
dst = buffer;
/* 从源地址里拷贝数据到目的地址*/
fb_memcpy_fromfb(dst, src, c);
/* 调整位置*/
dst += c;
src += c;
/* 从内核刚申请内存的地址buffer 拷贝
* c 长度的数据到用户空间的buf 里去
*/
if (copy_to_user(buf, buffer, c)) {
err = -EFAULT;
break;
}
/* 调整偏移位置*/

*ppos += c;
buf += c;
cnt += c;
/* count 变量减去已经读取的 c 数量,用于判断 while(count)是否为真*/
count -= c;
}
/* 释放内存 */
kfree(buffer);
/* err = 0 时,返回被拷贝成功的数量 cnt */
return (err) ? err : cnt;
}


9.2.4.1 

file_fb_infofile_fb_info 函数的功能与 get_fb_info 函数的功能几乎是一样的。
static struct fb_info *file_fb_info(struct file *file)
{
/* 获取次设备号 */
struct inode *inode = file->f_path.dentry->d_inode;
int fbidx = iminor(inode);
/* 以次设备号为下标,在 registered_fb 数组里找到一项 fb_ino 实例 */
struct fb_info *info = registered_fb[fbidx];
/* 如果获取的 fb_info 实例与 fb_open 函数
* 获取的 fb_info 实例不一致,置为 NULL
*/
if (info != file->private_data)
info = NULL;
return info;
}


9.2.5 
registered_fb

通过分析 fb_open 和 fb_read 函数,都发现有个共同的特点,就是以次设备号为下标找到一项 fb_info 结构体。 从哪里找?从 registered_fb[]数组里找。 那这个 registered_fb[]数组,在哪里被设置? 还记得,在第八章里,内核如何注册平台总线了吗?我不是叫你回忆如何注册的,我是叫你回忆,怎么找到函数调用流程的。这就是使用 Source Insight 的好处啦,使用“ctrl+/” 搜索 registered_fb会发现如下结果:
register_framebuffer
-->do_register_framebuffer
-->registered_fb[i] = fb_info;


9.2.6 register_framebuffer
/* 参考 drivers\video\fbmem.c */
int register_framebuffer(structfb_info *fb_info)
{
int ret;
mutex_lock(®istration_lock);
ret = do_register_framebuffer(fb_info);
mutex_unlock(®istration_lock);
return ret;
}

register_framebuffer 上锁后,调用 do_register_framebuffer 来向内核注册一个帧缓冲设备。

9.2.7 do_register_framebuffer

do_register_framebuffer 函数主要做了三件事:第一、 在 graphics 类下创建设备,设备节点为/dev/fb0--/dev/fb31 。第二、 简单初始化 fb_info 结构体部分成员。第三、 将初始化好的 fb_info 结构体赋给 registered_fb[]。
/* 参考 drivers\video\fbmem.c */
static int do_register_framebuffer(struct fb_info *fb_info)
{
int i;
struct fb_event event;
struct fb_videomode mode;
......
if (num_registered_fb == FB_MAX)
return -ENXIO;
num_registered_fb++;
for (i = 0 ; i < FB_MAX; i++)
if (!registered_fb[i])
break;

fb_info->node = i;
atomic_set(&fb_info->count, 1);
mutex_init(&fb_info->lock);
mutex_init(&fb_info->mm_lock);
/* 在 graphics 类下创建设备,设备节点为/dev/fb0--/dev/fb31 */
fb_info->dev = device_create(fb_class, fb_info->device,
MKDEV(FB_MAJOR, i), NULL, "fb%d", i);
...
/* 简单初始化 fb_info 结构体部分成员 */
fb_init_device(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);
fb_var_to_videomode(&mode, &fb_info->var);
fb_add_videomode(&mode, &fb_info->modelist);
/* 将初始化好的fb_info 结构体赋给registered_fb[] */
registered_fb[i] = fb_info;
event.info = fb_info;
if (!lock_fb_info(fb_info))
return -ENODEV;

fb_notifier_call_chain(FB_EVENT_FB_REGISTERED, &event);
unlock_fb_info(fb_info);
return 0;
}

好吧,看到这里, 9.2.1 小 节 留 下 的问 题 就 自然 而 然 的就 解 答 了 。registered_fb[]数组是在 register_framebuffer()函数里设置。

9.2. 8 fb_mmap

file_operations 中的 mmap 成员函数非常重要, 正是它,将显示缓冲区映射到用户空间,从而使得用户空间可以直接操作显示缓冲区而省去一次从用户空间到内核空间的数据拷贝过程,明显提高了高效。而在 fbmem.c 里 mmap 对应的函数是 fb_mmap。
static int fb_mmap(struct file *file, struct vm_area_struct * vma)
{
/* 以次设备号为下标找到一项 fb_info 结构体 */
struct fb_info *info = file_fb_info(file);
struct fb_ops *fb;
unsigned long off;
unsigned long start;
u32 len;
if (!info)
return -ENODEV;
if (vma->vm_pgoff > (~0UL >> PAGE_SHIFT))
return -EINVAL;
off = vma->vm_pgoff << PAGE_SHIFT;
fb = info->fbops;
if (!fb)
return -ENODEV;
mutex_lock(&info->mm_lock);
/* 如果新的 fb_info 实例中的 fbops->fb_mmap 存在的话就调用它 */
if (fb->fb_mmap) {
int res;
res = fb->fb_mmap(info, vma);
mutex_unlock(&info->mm_lock);
return res;
}
/* 源:fb 缓冲内存的开始位置(物理地址) */
start = info->fix.smem_start;

/* 长度 */
len = PAGE_ALIGN((start & ~PAGE_MASK) + info->fix.smem_len);
/* 如果 off 大于 len,则要调整源起始地址、长度 */
if (off >= len) {
/* memory mapped io */
off -= len;
if (info->var.accel_flags) {
mutex_unlock(&info->mm_lock);
return -EINVAL;
}
start = info->fix.mmio_start;
len = PAGE_ALIGN((start & ~PAGE_MASK) + info->fix.mmio_len);
}
mutex_unlock(&info->mm_lock);
start &= PAGE_MASK;
if ((vma->vm_end - vma->vm_start + off) > len)
return -EINVAL;
off += start;
vma->vm_pgoff = off >> PAGE_SHIFT;
vma->vm_page_prot = vm_get_page_prot(vma->vm_flags);
fb_pgprotect(file, vma, off);
/* 正式映射物理内存到用户空间虚拟地址 */
if (io_remap_pfn_range(vma, vma->vm_start, off >> PAGE_SHIFT,
vma->vm_end - vma->vm_start, vma->vm_page_prot))
return -EAGAIN;
return 0;
}


9.2.9 fb_ioctl

fb_ioctl()函数最终实现对用户 I/O 控制命令的执行,比如获取可变的屏幕参数 命 令 (FBIOGET_VSCREENINFO) , 设 置 可 变 的 屏 幕 参 数 命 令(FBIOPUT_VSCREENINFO) , 获 取 固 定 的 屏 幕 参 数 命 令(FBIOGET_FSCREENINFO)等等。
static long fb_ioctl(struct file *file, unsigned intcmd,unsigned longarg)
{
struct fb_info *info = file_fb_info(file);
if (!info)
return -ENODEV;
return do_fb_ioctl(info,cmd,arg);
}

fb_ioctl()函数的实现,其实是通过调用 do_fb_ioctl()函数实现的,它才是资本执行者呀~~对于 do_fb_ioctl()函数的分析,这里就不再深入分析下去了,它主要是根据cmd 命令,来判断要执行哪部分功能,比如: cmd = FBIOGET_VSCREENINFO,它就会转到 FBIOGET_VSCREENINFO 标号去,将 fb_info 成员设置好后,通过 copy_to_user()函数拷贝回用户空间;而用户空间要设置显示设备的话,则是通过 copy_from_user()函数到达内核空间的。

9.3 从零编写 LCD 设备驱动

在 Source Insight 里使用“ctrl+/”搜索 register_framebuffer,会发现有一大堆文件会调用这个函数,有趣的是,这些文件大部分都位于/driver/video 目录下。对,没错, 如图 9.4 所示, 这些都是具体厂商的 LCD 设备驱动。
《网蜂A8实战演练》——7.Linux LCD设备驱动
一眼扫过去, s3c2410fb.c 貌似看起来相对比较熟悉。 进去咋一看,确实这是三星公司提供的基于 S3C2410/S3C2440 架构来编写的 LCD 设备驱动。 它属于我们第八章讲的平台总线驱动设备模型,入口函数注册了一个平台驱动,如果系统中存在有相同名字的平台设备,就会调用 platform_driver 的 probe 函数。

由于 S5PV210 的 LCD 控制器的寄存器与 S3C2410/S3C2440 的 LCD 控制器的寄存器有太大的差别了,基于 s3c2410fb.c 来移植到 S5PV210 架构上实在是太麻烦了,要修改的地方太多。 所以,我们直接自己从零编写一个 LCD 设备驱动。
这里仅仅大概讲一下 s3c24xxfb_probe 函数做了哪些事情,有兴趣的读者请自行分析 drivers/video/s3c2410fb.c
(1) 获取平台数据,这里平台数据应该存放板相关的 fb_info 结构体实例。 (2) 通过平台设备 platform_device 获得 IRQ。 (3) 使用 framebuffer_alloc()函数分配一个 fb_info 结构体。
(4) 通过平台设备 platform_device 获得内存资源。
(5) 申请以 res->start 地址开始大小为 size 的 I/O 内存。 (6) 使用 ioremap()函数映射 I/O 地址。 (7) 禁止 Video output (8) 设置 fb_info 结构体通用的固定参数 fb_fix_screeninfo 结构体。 (9) 设置 fb_info 结构体通用的可变参数 fb_var_screeninfo 结构体。 (10) 设置 fb_ops 结构体 (11) 设置假调色板 (12) 申请中断 (13) 获取 lcd 时钟 (14) 分配显存 (15) 初始化 LCD 相关的寄存器 (16) 检查可变参数 (17) 注册 fb_info 结构体

9.3.1 图说 LCD 设备驱动编写流程

1. 分配一个 fb_info 结构体 2. 设置 fb_info 结构体成员 3. 硬件相关的操作 4. 注册

《网蜂A8实战演练》——7.Linux LCD设备驱动

9.3.2 从零编写 LCD 设备驱动分析

LCD 设备驱动源码路径为: webee210_drivers\8th_lcd\webee210_lcd.c考虑到本驱动大量移植了裸机下的 LCD 源码,特意将裸机下的 LCD 源码也放在这里,方面大家参考。
LCD 裸机源码路径为: webee210_drivers\8th_lcd\210_code\11.lcd

9.3.2.1 入口函数

本驱动并没有使用平台总线驱动设备模型,直接在一个文件上使用了 LCD设备驱动, 入口函数完成了图 9.5 的大部分工作。
1.分配一个 fb_info
/* 第一个参数为 0,表示只需要分配结构体本身大小,无需分配额外空间
* 第二个参数为 NULL,表示无 device 结构
*/
webee_fbinfo = framebuffer_alloc(0 , NULL);


2.1 设置固定的参数

/* 关于这些参数的含义,请参考 9.1.4.2 小节 */
strcpy(webee_fbinfo->fix.id, "webee210_lcd");
webee_fbinfo->fix.smem_len = 800 * 480 * 32/8;
webee_fbinfo->fix.type = FB_TYPE_PACKED_PIXELS;
webee_fbinfo->fix.visual = FB_VISUAL_TRUECOLOR;
webee_fbinfo->fix.line_length = 800 * 32/8;

2.2 设置可变的参数
/* 关于这些参数的含义,请参考 9.1.4.1 小节 */
webee_fbinfo->var.xres = 800;
webee_fbinfo->var.yres = 480;
webee_fbinfo->var.xres_virtual = 800;
webee_fbinfo->var.yres_virtual = 480;
webee_fbinfo->var.bits_per_pixel = 32;
/*RGB:888*/
webee_fbinfo->var.red.offset = 16;
webee_fbinfo->var.red.length = 8;
webee_fbinfo->var.green.offset = 8;
webee_fbinfo->var.green.length = 8;
webee_fbinfo->var.blue.offset = 0;
webee_fbinfo->var.blue.length = 8;
webee_fbinfo->var.activate = FB_ACTIVATE_NOW

2.3 设置操作函数
/* 后面分析这个 fops */
webee_fbinfo->fbops = &webee210_lcdfb_ops;

2.4.1 设置显存的大小
webee_fbinfo->screen_size = 800 * 480 * 32/8;

2.4.2 设置调色板
static u32 pseudo_palette[16];
……
/* pseudo_palette 是一个 u32 型的数组,在 webee210_lcdfb_setcolreg
* 函数中被设置, 如何设置看后面 webee210_lcdfb_setcolreg 函数分析。
*/
webee_fbinfo->pseudo_palette = pseudo_palette;

2.4.3 设置显存的虚拟起始地址
webee_fbinfo->screen_base = dma_alloc_writecombine(NULL,webee_fbinfo->fix.smem_len,(u32*)&(webee_fbinfo->fix.smem_start), GFP_KERNEL);
/* 函数原型如下:
* 第一个参数为: device 结构体,设置为 NULL,表示无 device 结构
* 第二个参数为: 需分配帧缓冲大小,这里为 800 * 480 * 32/8
* 第三个参数为: 返回的帧缓冲的物理起始地址,表示 DMA 可用
* 第四个参数为:分配标志,如 GFP_KERNEL
*/
static inline void *dma_alloc_writecombine(struct device *dev,size_t size,dma_addr_t *dma_handle, gfp_t flag)


3. 硬件相关的操作3.1 获取 lcd 时钟,使能时钟

lcd_clk = clk_get(NULL, "lcd");
if (!lcd_clk || IS_ERR(lcd_clk)) {
printk(KERN_INFO "failed to get lcd clock source\n");
}
clk_enable(lcd_clk);3.2 配置 GPIO 用于 LCD
gpf0con = ioremap(0xE0200120, 4);
gpf1con = ioremap(0xE0200140, 4);
gpf2con = ioremap(0xE0200160, 4);
gpf3con = ioremap(0xE0200180, 4);
gpd0con = ioremap(0xE02000A0, 4);
gpd0dat = ioremap(0xE02000A4, 4);
display_control = ioremap(0xe0107008, 4);
/* 设置相关 GPIO 引脚用于 LCD */
*gpf0con = 0x22222222;
*gpf1con = 0x22222222;
*gpf2con = 0x22222222;
*gpf3con = 0x22222222;
/* 使能 LCD 本身 */
*gpd0con |= 1<<4;
*gpd0dat |= 1<<1;
/* 显示路径的选择, 0b10: RGB=FIMD I80=FIMD ITU=FIMD */
*display_control = 2<<0;

关于这些引脚的含义,这里就不详细分析了, 我们要的是层次和架构,对于LCD 硬件的设置,大家可以参考第一部分的第 11 章《S5PV210 LCD 控制器原理与应用》,或者自己对照着手册来看看是什么含义。

3.3 映射 LCD 控制器对应寄存器

lcd_regs = ioremap(0xF8000000, sizeof(struct s5pv210_lcd_regs));
vidw00add0b0 = ioremap(0xF80000A0, 4);
vidw00add1b0 = ioremap(0xF80000D0, 4);
lcd_regs->vidcon0 &= ~((3<<26) | (1<<18) | (0xff<<6) | (1<<2));
lcd_regs->vidcon0 |= ((5<<6) | (1<<4) );
/* 在 vclk 的下降沿获取数据 */
lcd_regs->vidcon1 &= ~(1<<7);
/* HSYNC 极性反转, VSYNC 极性反转 */
lcd_regs->vidcon1 |= ((1<<6) | (1<<5));
lcd_regs->vidtcon0 = (VBPD << 16) | (VFPD << 8) | (VSPW << 0);
lcd_regs->vidtcon1 = (HBPD << 16) | (HFPD << 8) | (HSPW << 0);
lcd_regs->vidtcon2 = (LINEVAL << 11) | (HOZVAL << 0);
lcd_regs->wincon0 &= ~(0xf << 2);
lcd_regs->wincon0 |= (0xB<<2)|(1<<15);
lcd_regs->vidosd0a = (LeftTopX<<11) | (LeftTopY << 0);
lcd_regs->vidosd0b = (RightBotX<<11) | (RightBotY << 0);
lcd_regs->vidosd0c = (LINEVAL + 1) * (HOZVAL + 1);
*vidw00add0b0 = webee_fbinfo->fix.smem_start;
*vidw00add1b0 =
webee_fbinfo->fix.smem_start + webee_fbinfo->fix.smem_len;
lcd_regs->shadowcon = 0x1; /* 使能通道 0 */
lcd_regs->vidcon0 |= 0x3; /* 开启总控制器 */
lcd_regs->wincon0 |= 1; /* 开启窗口 0 */

4.注册
register_framebuffer(webee_fbinfo);


9.3.2.2 出口函数

出口函数还是一如既往的, Webee 总喜欢把它形容为“聪明的跟屁虫”,为什么这么说呢?因为出口函数总喜欢做入口函数相反的事情,如果非要文艺点形容的话,还是那句话,做人要有始有终嘛~~
static void __exit webee210_lcd_exit(void)
{
unregister_framebuffer(webee_fbinfo);
dma_free_writecombine(NULL,webee_fbinfo->fix.smem_len,webee_fbinfo->screen_base, webee_fbinfo->fix.smem_start);
iounmap(gpf0con);
iounmap(gpf1con);

iounmap(gpf2con);
iounmap(gpf3con);
iounmap(gpd0con);
iounmap(gpd0dat);
iounmap(display_control);
iounmap(lcd_regs);
iounmap(vidw00add0b0);
iounmap(vidw00add1b0);
framebuffer_release(webee_fbinfo);
}


9.3.2.3 webee210_lcdfb_ops

一看到这个,又回到了字符设备驱动的那套了,对吗?对不?细心的读者可能会发现,这个 fops 貌似与字符设备驱动的 fops 有点不一样哟~~
static struct fb_ops webee210_lcdfb_ops=
{
.owner = THIS_MODULE,
.fb_setcolreg = webee210_lcdfb_setcolreg,
.fb_fillrect = cfb_fillrect,
.fb_copyarea = cfb_copyarea,
.fb_imageblit = cfb_imageblit,
};

看了源码后,发现有什么不大一样了吗? 以前的 fops 类型是 file_operations型的,而这里的 fops 是 fb_ops 型的,与 9.1.4.3 小节讲的 fops 是一致的。 其中,webee210_lcdfb_ops 实例里有四个成员函数,但是只需要我们自己实现fb_setcolreg 成员函数。而 fb_fillrect、 fb_copyarea、 fb_imageblit 成员函数默认为由内核大牛们写好的。 注意了,我们并没有实现这三个成员,所以我们在加载
webee210_lcd.ko 时,需要先加载这三个函数对应的模块。


9.3.2.4 webee210_lcdfb_setcolreg

static int webee210_lcdfb_setcolreg(unsigned regno,
unsigned red, unsigned green, unsignedblue,
unsigned transp, struct fb_info *info)
{
unsigned int val;
if (regno > 16)
return 1;
/* 用 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);
pseudo_palette[regno] = val;
return 0;
}

webee210_lcdfb_setcolreg()函数使用应用程序提供的 red、 green、 bule 构成出颜色映射值 val,然后将这个 val 写入假调色板中( pseudo_palette[])

9.3.2.5 chan_to_field

怎么构造出颜色映射值 val 呢? 使用 chan_to_field()函数。
static unsigned int chan_to_field(unsigned int chan,struct fb_bitfield *bf)
{
chan &= 0xffff;
chan >>= 16 - bf->length;
return chan << bf->offset;
}

很简单,它根据 fb_bitfield 位域的 length 和 offset 来构造。 对于 Webe210的 LCD,使用的 RGB 格式是 888 的。 把值代进去一算,你就知道如何构造啦。

9.3. 3 从零编写 LCD 设备驱动的测试

修改文件系统的 etc 配置文件,将新的 webee210_lcd.ko 配置上。
/* 下面的操作都在虚拟机下, Webee 的文件系统路径可能与你们的不一样 */
[root@localhost etc]# cd /home/webee210v2/rootfs/etc
[root@localhost etc]# vim profile
/* etc/profile 文件部分内容 */
export PS1 HOSTNAME
insmod ko/cfbimgblt.ko
insmod ko/cfbfillrect.ko
insmod ko/cfbcopyarea.ko
#insmod ko/lcd.ko
insmod ko/webee210_lcd.ko
insmod ko/bus.ko
insmod ko/dev.ko
insmod ko/drv.ko

……

将原来的 insmod ko/lcd.ko 屏蔽掉,替换成自己从零编写的 LCD 设备驱动,也就是增加这条命令:insmod ko/webee210_lcd.ko当然,前提是已经将编译好的 webee210_lcd.ko 拷贝到文件系统的 ko 文件夹下了。
同样在 etc/profile 文件里,如果已经屏蔽了网蜂物联网 QT 界面的话,还需要将最后那行命令由:
#qt4 &改为:
qt4 &

改好后,无论你使用 NFS 启动还是以 yaffs2 启动,如果 LCD 上能够正常显示出网蜂物联网的那个 QT 界面,表示此 LCD 设备驱动成功了。 当然,如果你能够触摸的话,表示触摸屏驱动也成功了。但是,注意了,触摸屏和 LCD 屏是两种不同的设备了,不要混淆了哦。

9.4 本章小结

本章重点讲解了 Linux 下的帧缓冲设备 Linux 驱动的架构与编程方法,介绍了帧缓冲设备驱动的整体结构,还分析了帧缓冲设备驱动的几个重要的函数。通过学习帧缓冲设备驱动,能够更好的理解如何编写 LCD 设备驱动。 接着,我们还讲解了如何从零编写一个 LCD 设备驱动。有图有真相哟~~~ 我们重点讲解的是它的编程框架,对于寄存器的设置我们并没有详细分析了,读者可参考以前的裸机源码。 这表明什么?表明学习 Linux 驱动,主要抓层次抓框架,而不是细节。