• Linux驱动开发(十二)---树莓派framebuffer学习(改造OLED)


    前文回顾

    《Linux驱动开发(一)—环境搭建与hello world》
    《Linux驱动开发(二)—驱动与设备的分离设计》
    《Linux驱动开发(三)—设备树》
    《Linux驱动开发(四)—树莓派内核编译》
    《Linux驱动开发(五)—树莓派设备树配合驱动开发》
    《Linux驱动开发(六)—树莓派配合硬件进行字符驱动开发》
    《Linux驱动开发(七)—树莓派按键驱动开发》
    《Linux驱动开发(八)—树莓派SR04驱动开发》
    《Linux驱动开发(九)—树莓派I2C设备驱动开发(BME280)》
    《Linux驱动开发(十)—树莓派输入子系统学习(红外接收)》
    《Linux驱动开发(十一)—树莓派SPI驱动学习(OLED)》

    继续宣传一下韦老师的视频

    70天30节Linux驱动开发快速入门系列课程【实战教学、技术讨论、直播答疑】

    在这里插入图片描述

    framebuffer介绍

    framebuffer从字面上理解是“帧缓冲”,一般有如下理解:

    • 单纯的把framebuffer看作一块内存,这部分内存包含了将要scan out显示的数据。
    • 等价于framebuffer driver。通常作为LCD控制器或者其他显示设备的驱动,FrameBuffer驱动是一个字符设备,设备节点是/dev/fbX,主设备号为29,次设备号递增,用户可以将Framebuffer看成是显示内存的一个映像,将其映射到进程地址空间之后,就可以直接进行读写操作,而写操作可以立即反应在屏幕上。这种操作是抽象的,统一的。用户不必关心物理显存的位置、换页机制等等具体细节。这些都是由Framebuffer设备驱动来完成的。Framebuffer设备为上层应用程序提供系统调用,也为下一层的特定硬件驱动提供接口;那些底层硬件驱动需要用到这儿的接口来向系统内核注册它们自己。所以可以看成是一个graphic hardware-independent抽象层,上面对接应用层,下面对接LCD等硬件的驱动。
    • 有一种说法是操纵lcd显示就是操纵framebuffer,表面上来看是这样的。实际上是frambuffer就是linux内核驱动申请的一片内存空间,然后lcd内有一片sram,cpu内部有个lcd控制器,它有个单独的dma用来将frambuffer中的数据拷贝到lcd的sram中去 拷贝到lcd的sram中的数据就会显示在lcd上,LCD驱动和framebuffer驱动没有必然的联系,它只是驱动LCD正常工作的,比如有信号传过来,那么LCD驱动负责把信号转成显示屏上的内容,至于什么内容这就是应用层要处理的。

    我理解的就是它对应一个设备/dev/fbx,通过它就可以直接写屏幕。后面的操作不需要我们操心,系统自动完成。
    在这里插入图片描述

    OLED

    其实我们的oled并不具备这种控制器,所以它没有提供显存的地址,这里用oled测试,也只是注册好fb之后,手动将fb的数据通过SPI驱动,更新到屏幕上去。
    在这里插入图片描述

    驱动中的核心步骤

    其中最关键重点步骤,就是分配,设置,注册fb_info结构体

    static int oled_probe(struct spi_device *spi)
    {	
    	
    	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
    	oled_dev = spi;
    
    	/* 分配/设置/注册 fb_info */
    	oled_fb_info = framebuffer_alloc(0, &spi->dev);
    
    
    	/* 1.2 设置fb_info */
    	/* a. var : LCD分辨率、颜色格式 */
    	oled_fb_info->var.xres_virtual = oled_fb_info->var.xres = 128;//宽度
    	oled_fb_info->var.yres_virtual = oled_fb_info->var.yres = 64;//高度
    	
    	oled_fb_info->var.bits_per_pixel = 1;  //1位表示一个像素
    	
    	/* b. fix */
    	strcpy(oled_fb_info->fix.id, "pgg_oled");
    	oled_fb_info->fix.smem_len = oled_fb_info->var.xres * oled_fb_info->var.yres * oled_fb_info->var.bits_per_pixel / 8;//显存占据的地址大小,要除以8变成字节数
    
    	/* c. 分配显存 */
    	oled_fb_info->screen_base = dma_alloc_wc(&spi->dev, oled_fb_info->fix.smem_len, &phy_addr_fb, GFP_KERNEL);
    	oled_fb_info->fix.smem_start = phy_addr_fb;  /* fb的物理地址 */
    	oled_fb_info->fix.smem_len = oled_fb_info->fix.smem_len;
    	
    	oled_fb_info->fix.type = FB_TYPE_PACKED_PIXELS;//表示像素类型
    	oled_fb_info->fix.visual = FB_VISUAL_MONO10;//表示单色屏幕
    
    	oled_fb_info->fix.line_length = oled_fb_info->var.xres * oled_fb_info->var.bits_per_pixel / 8;//每行的长度
    
    	/* d. fbops */
    	oled_fb_info->fbops = &myfb_ops;
    	oled_fb_info->pseudo_palette = pseudo_palette;	
    
    	register_framebuffer(oled_fb_info);
    
    	/* spi oled init */
    	oled_dc = gpiod_get(&spi->dev, "dc", GPIOD_OUT_HIGH);
    	oled_rst = gpiod_get(&spi->dev, "rst", GPIOD_OUT_HIGH);
    
    	data_buf = dma_alloc_wc(&spi->dev, oled_fb_info->fix.smem_len, &phy_addr_user, GFP_KERNEL);
    	
    	oled_hardware_init();
    
    	OLED_DIsp_Clear();
    
    	/* 创建1个内核线程,用来把Framebuffer的数据通过SPI控制器发送给OLED */
    	oled_kthread = kthread_create(oled_thread, NULL, "pgg_oled_pd");
    	wake_up_process(oled_kthread);
    	
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    在这里插入图片描述

    来详细说一下这个步骤

    申请framebuffer

    oled_fb_info = framebuffer_alloc(0, &spi->dev);
    
    • 1

    设置framebuffer的变量参数,这里根据实际屏幕的真实参数决定。oled是128*64像素,每个像素用1bit表示,只有亮和灭。

    oled_fb_info->var.xres_virtual = oled_fb_info->var.xres = 128;//宽度
    oled_fb_info->var.yres_virtual = oled_fb_info->var.yres = 64;//高度
    oled_fb_info->var.bits_per_pixel = 1;  //1位表示一个像素
    
    • 1
    • 2
    • 3

    再设置固定参数

    strcpy(oled_fb_info->fix.id, "pgg_oled");
    oled_fb_info->fix.smem_len = oled_fb_info->var.xres * oled_fb_info->var.yres * oled_fb_info->var.bits_per_pixel / 8;//显存占据的地址大小,要除以8变成字节数
    
    • 1
    • 2

    分配实际的内存地址

    oled_fb_info->screen_base = dma_alloc_wc(&spi->dev, oled_fb_info->fix.smem_len, &phy_addr_fb, GFP_KERNEL);
    oled_fb_info->fix.smem_start = phy_addr;  /* fb的物理地址 */
    oled_fb_info->fix.smem_len = oled_fb_info->fix.smem_len;
    
    • 1
    • 2
    • 3

    这里注意配置屏幕类型,并且每行的字节数,还是需要除以8,这个和oled的屏幕内存并不一样,oled是竖着存放一个字节表示第一列的8个bit。所以后面刷屏的时候,还有一个变化。

    oled_fb_info->fix.type = FB_TYPE_PACKED_PIXELS;//表示像素类型
    oled_fb_info->fix.visual = FB_VISUAL_MONO10;//表示单色屏幕
    oled_fb_info->fix.line_length = oled_fb_info->var.xres * oled_fb_info->var.bits_per_pixel / 8;//每行的长度
    
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述

    这里注册几个操作函数,是必须要有的,还有后面的调色板。

    oled_fb_info->fbops = &myfb_ops;
    oled_fb_info->pseudo_palette = pseudo_palette;	
    
    • 1
    • 2

    最后注册framebuffer

    register_framebuffer(oled_fb_info);
    
    • 1

    卸载函数,内部需要释放一些内存,然后注销framebuffer。

    static int oled_remove(struct spi_device *spi)
    {
    	printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
    	kthread_stop(oled_kthread);
    
    	dma_free_wc(&spi->dev,oled_fb_info->fix.smem_len,oled_fb_info->screen_base,phy_addr_fb);
    	dma_free_wc(&spi->dev,oled_fb_info->fix.smem_len,data_buf,phy_addr_user);
    	/* 反注册 fb_info */
    	unregister_framebuffer(oled_fb_info);
    
    	gpiod_put(oled_dc);
    	gpiod_put(oled_rst);
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这个framebuffer目前来说还和硬件没有任何关系。我们只是申请到了一块显存。
    在这里插入图片描述

    然后就需要把显存中的数据,通过spi方式,不断的刷新到oled显示屏中

    static int get_pixel(int x, int y)
    {
    	unsigned char byte = *(oled_fb_info->screen_base + y*oled_fb_info->fix.line_length + (x>>3));
    	int bit = x & 0x7;
    	if (byte & (1<< bit))
    		return 1;
    	else 
    		return 0;
    }
    
    static void convert_fb_to_oled(void)
    {
    	unsigned char data;
    	int i = 0;
    	int x, page;
    	int bit;
    
    	for (page = 0; page < 8; page++)
    	{
    		for (x = 0; x < 128; x++)
    		{
    			data = 0;
    			for (bit = 0; bit < 8; bit++)
    			{
    				data |= (get_pixel(x, page*8 + bit) << bit);
    			}
    			data_buf[i++] = data;
    		}		
    	}
    	
    	
    }
    
    static int oled_thread(void *data)
    {
        unsigned char y;
    	while (1)
    	{
    		/* 把Framebuffer的数据刷到OLED上去 */
    		convert_fb_to_oled();
    		for (y = 0; y < 8; y++)
    		{
    			OLED_DIsp_Set_Pos(0, y);
    			oled_write_datas(&data_buf[y*128], 128);
    		}
    		
    		set_current_state(TASK_INTERRUPTIBLE);
    		schedule_timeout(HZ);
    
    		if (kthread_should_stop()) {
    			set_current_state(TASK_RUNNING);
    			break;
    		}
    		
    	}
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    在这里插入图片描述

    用户侧程序

    用户侧程序参考的是韦老师的代码,用来显示一个字符A,需要理解的就是mmap函数的用法。
    mmap函数是一个比较神奇的函数,它可以把文件映射到进程的虚拟内存空间。通过对这段内存的读取和修改,可以实现对文件的读取和修改,而不需要用read和write函数。

    void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
    
    • 1

    在这个函数原型中:

    参数addr:指定映射的起始地址,通常设为NULL,由内核来分配 参数length:代表将文件中映射到内存的部分的长度。
    参数prot:映射区域的保护方式。可以为以下几种方式的组合: PROT_EXEC 映射区域可被执行 PROT_READ 映射区域可被读取
    PROT_WRITE 映射区域可被写入 PROT_NONE 映射区域不能存取 参数flags:映射区的特性标志位,常用的两个选项是:
    MAP_SHARD:写入映射区的数据会复制回文件,且运行其他映射文件的进程共享
    MAP_PRIVATE:对映射区的写入操作会产生一个映射区的复制,对此区域的修改不会写会原文件
    参数fd:要映射到内存中的文件描述符,有open函数打开文件时返回的值。
    参数offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍。
    函数返回值:实际分配的内存的起始地址。

    在用户侧操作Framebuffer的使用步骤

    1. 打开设备文件 /dev/fbx
    2. 获取当前设备信息
    3. mmap做映射
    4. 填充framebuffer
      在这里插入图片描述

    问题

    韦老师的代码中
    在这里插入图片描述
    这个参数为空了,好像会导致申请失败,这里的视频尚未完结,这个bug可能还没解决。
    在这里插入图片描述

    遗留问题

    这个模块的卸载,目前还有问题。
    另外,内核会有一个告警信息
    在这里插入图片描述
    不过没有影响使用,不知道是不是问题。
    这两个问题后续继续解决。
    在这里插入图片描述

    效果

    这里的代码只是写了一个字符A的显示。
    在这里插入图片描述
    代码下载
    《传送门》

    结束语

    今天更新完就学习完了韦老师的视频,驱动知识刚刚入门。后续的学习不能停止,继续搜罗资料,进行进一步的学习,丰富种类,学习块设备,网络设备的驱动。
    在这里插入图片描述
    昨天看了魔女2,动作场面还是挺过瘾,但是就是情节不是很清楚,可能是翻译的不太准确罢了。新的女主也挺好看,旧的女主也出来了一面,不过不如1中可爱了。电影看完就忘了,只记得里面令人尴尬的中国话。
    在这里插入图片描述
    今天看《汉江怪物》,肯定差不了。在这里插入图片描述

  • 相关阅读:
    网络安全-黑客技术(自学笔记)
    巧用字典树算法,轻松实现日志实时聚类分析
    3.1 Android eBPF代码仓解读
    AR眼镜_单目光波导VS双目光波导方案
    【力扣刷题练习】42. 接雨水
    7种系统设计中的数据库范式
    NEFU数字图像处理(2)图像增强
    解密Prompt系列8. 无需训练让LLM支持超长输入:知识库 & unlimiformer & PCW & NBCE
    Android入门第39天-系统设置Configuration类
    css 动画基础知识和案例
  • 原文地址:https://blog.csdn.net/baidu_19348579/article/details/126074010