一、OLED简介
本次使用的开发板正点原子Linux阿波罗。屏幕是i2c接口的四针、分辨率为128×64的oled液晶屏。通信接口为i2c。具体的i2c框架使用请参考前面的文章。oled的详细简介请参考其他博主的文章。
网上很多使用linux驱动i2c接口的oled屏幕,大多数都不不涉及framebuffer驱动相关。本次使用oled,将其看作两个驱动相结合去控制oled。
首先,oled是一个屏幕,使用framebuffer框架进行驱动,它的通信方式为i2c,使用i2c框架与oled硬件ssd1306芯片通信。因此,主要流程就是申请framebuffer、i2c设备,进行注册,将它们结合起来。实际中使用并无特别大价值,Linux系统里面也提供了fbtft框架去驱动非标准的液晶屏(相对RGB888、RGB565等屏幕,使用例如SPI通信方式),这里的驱动作为学习使用,仅供参考。
二、查看、配置i2c设备树
i2c设备树在之前写i2c驱动的时候已经配置好,这里放张截图。按照下面截图修改好设备树,编译出来,烧录到板子里面。
从图中看到,oled的设备树节点compatible属性是htq,oled,i2c的地址是<0x3c>(reg属性),依赖于i2c1这个节点(第二张截图未显示),在第一张截图中,有i2c1的信息,oled的两根i2c引脚连接到板子上的U4_RX、U4_TX即可。
使用i2cdetect指令(i2c-tools相关工具),可以看到总线上已经有了oled设备。
查看放入设备树之后的板子信息,可以看到有htq,oled。这个信息将用来匹配设备树的节点。
三、代码
将设备树相关信息修改好之后,就可以写驱动了。首先在入口函数里面注册i2c设备和framebuffer设备。
static int __init oled_init(void)
{
int ret = 0;
ret = i2c_add_driver(&oled_driver);
printk("i2c ret: %d\n",ret);
ret = register_framebuffer(&oled_info);
printk("framebuffer ret: %d\n",ret);
return ret;
}
其中oled_driver、oled_info是对应的结构体,源码如下。
//i2c框架
static const struct i2c_device_id oled_id_table[] = {
{"htq,oled", 0},
};
static const struct of_device_id oled_of_match_table[] = {
{ .compatible = "htq,oled", },
};
static struct i2c_driver oled_driver = {
.probe = oled_probe,
.remove = oled_remove,
.driver = {
.owner = THIS_MODULE,
.name = "oled",
.of_match_table = oled_of_match_table,
},
.id_table = oled_id_table,
};
//frmebuffer框架
static struct fb_ops oled_fbops = {
.fb_open = oled_fb_open,
.fb_release = oled_fb_release,
.fb_mmap = oled_fb_mmap,
.fb_ioctl = oled_fb_ioctl,
};
static struct fb_info oled_info = {
.var ={
.xres = OLED_WIDTH, //屏幕宽
.yres = OLED_HEIGHT, //高
.xres_virtual = OLED_WIDTH, //虚拟屏幕宽、高
.yres_virtual = OLED_HEIGHT,
.bits_per_pixel = 1, //每像素点占用多少位
},
.fix={
.smem_len = OLED_HEIGHT * OLED_WIDTH / 8, //一帧使用多少内存,单位Byte
.line_length = 128, //一行使用多少内存,单位位
},
.fbops = &oled_fbops, //文件句柄
};
注册之后,两个设备驱动的probe函数会自动用,进行匹配。i2c框架根据compatible属性与设备树的compatible进行匹配,得到设备树节点信息。framebuffer框架根据oled_info得到屏幕的相关信息。在oled_fbops结构体中有fb_open、fb_release、fb_mmap、fb_ioctl相关指针。在fb_open里面可以设置显存,这样应用程序写入的数据首先写入到缓存里,之后调用fb_ioctl将显存数据刷新到oled屏幕里面(需要事先做好fb_ioctl相关命令代码)。因此,需要在fb_open函数里面申请显存区域,这样fb_mmap就可以将应用程序的mmap函数得到的指针和显存产生联系,进而使用。
oled_mmap_buffer = kzalloc(4096,GFP_ATOMIC);
if(oled_mmap_buffer == NULL){
printk("oled framebuffer alloc fail!\n");
return -1;
}
显存是互斥访问,因此使用GFP_ATOMIC(原子操作)。每一个应用程序都有自己的显存(实际上一般也只有一个应用程序使用,多线程访问不考虑)。
一般的RGB屏幕使用framebuffer框架,屏幕是映射到相应的内存上的,对内存的可以直接显示到屏幕上,oled本身不支持类似的操作。
fb_mmap函数如下:
static int oled_fb_mmap(struct fb_info *info, struct vm_area_struct *vma)
{
printk("oled_fb_mmap\n");
vma->vm_flags |= VM_IO;//表示对设备 IO 空间的映射
vma->vm_flags |= (VM_DONTEXPAND | VM_DONTDUMP);//标志该内存区不能被换出,在设备驱动中虚拟页和物理页的关系应该是长期的,
//应该保留起来,不能随便被别的虚拟页换出
if(remap_pfn_range(vma,//虚拟内存区域,即设备地址将要映射到这里
vma->vm_start,//虚拟空间的起始地址
virt_to_phys(oled_mmap_buffer)>>PAGE_SHIFT,//与物理内存对应的页帧号,物理地址右移 12 位
vma->vm_end - vma->vm_start,//映射区域大小,一般是页大小的整数倍
vma->vm_page_prot))//保护属性,
{
return -EAGAIN;
}
printk("(drv)映射的长度:%d\n",vma->vm_end - vma->vm_start);
printk("物理地址:0x%X\n",virt_to_phys(oled_mmap_buffer));
/*
开发板的DDR容量: 1G
0x40000000 ~ 0x80000000
0x10000000=256M
*/
printk("oled_fb_mmap ok\n");
return 0;
}
i2c的probe函数如下
static int oled_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
//1. 构建设备号
if(oled_device.major){
oled_device.dev_id = MKDEV(oled_device.major, 0);
register_chrdev_region(oled_device.dev_id, 1, OLED_NAME);
} else {
alloc_chrdev_region(&oled_device.dev_id, 0,1, OLED_NAME);
oled_device.major = MAJOR(oled_device.dev_id);
}
printk("device major: %d\n",oled_device.major);
//2. 字符设备
cdev_init(&oled_device.cdev, &oled_fops);
cdev_add(&oled_device.cdev, oled_device.dev_id, 1);
//3. 创建类
oled_device.class = class_create(THIS_MODULE, OLED_NAME);
if(IS_ERR(oled_device.class)){
return PTR_ERR(oled_device.class);
}
//4. 创建设备
oled_device.device = device_create(oled_device.class,NULL, oled_device.dev_id, NULL, OLED_NAME);
if(IS_ERR(oled_device.device)){
return PTR_ERR(oled_device.device);
}
oled_device.privare_data = client;
printk("oled iic addr:0x%x %d\n",client->addr,client->addr);
//mdelay(100);
//oled_init_reg();
//mdelay(100);
//printk("oled clear 0x0f\n");
return 0;
}
大概操作就是申请设备节点、注册字符设备、创建类之类的操作。
这样,i2c接口的oled驱动主要的内容写好。这样的驱动,应用程序既可以使用i2c框架访问,也可以使用framebuffer框架访问。
最后放几张结果图。
这张图里面可以看到驱动程序加载进去之后和打开显示的相应数据。
这是最开始亮的图片,之后应用程序写的就不放了。
四 总结:
Linux系统中使用框架开发驱动会大大减少开发精力。本次使用的i2c接口的oled只是一个多框架结合的示例。在实际中,并无太大用处。但一个物理设备需要使用多个框架开发功能是常见的,比如现在常用的电容屏,驱动除了要使用framebuffer框架显示,还有触摸驱动在里面进行,检测触摸点的位置,返回给系统或应用。
针对非标准的液晶屏, Linux系统里面也提供了fbtft框架去驱动,这个等以后有时间再更新。本次的驱动也未考虑多线程/多进程并发访问的相关问题,仅作学习使用。
五 环境和参考
硬件:正点原子Linux开发板
环境:ubuntu18
参考:Linux设备驱动开发详解(基于最新的Linux4.0内核) 宋宝华著
Linux设备驱动程序 J & G著