LCD触控屏移植之LCD显示驱动篇

1 前言

  关于framebuffer的移植,这是我第一次接触稍微高端点的驱动。以前都是学习使用spi和i2c驱动,这些驱动的整体框架已经很熟悉了,且网上资料都烂大街了,所以遇到问题基本都能轻松解决。到了这个驱动,就遇到了很多新问题;首先这个框架甚至都是我第一次听说,因此官方高度优化的代码读起来很费力!且不得不吐槽的一点是官方为了兼容性设置了一大堆宏,加上各种变量的重赋值,综合下来代码简直没法往下读。
  没办法,只能去网上查找framebuffer框架的资料先学习一下;幸运的是我找到了拱卒大佬的framebuffer的驱动,也是基于ili9488写的;按道理讲用大佬的驱动,只要适配好设备树基本上屏幕也就能顺利点亮了。基于此我就改起了自己的代码。
  然后就卡在设备树的第一道难关上了。我结合拱卒大佬的设备树和官方基于st7789v的lcd屏幕的设备树,编写迭代了自己多个版本的设备树;虽说结合驱动完成了屏幕的各项配置工作,但屏幕始终就是点不亮。怀疑各种问题,各种尝试,就是失败。后面实在是没思路了,就想着先用官方st7789v的屏幕把framebuffer驱动点亮屏幕这条路走通,加深理解后再回头点亮自己的屏幕。
  结果不试不知道,一试才发现官方示例的设备树也是错误的。后面顺理成章地在官方论坛上提问,官方给出了正确的设备树。从而爬出了第一个深坑。(官方设备树在原有的基础上关闭了一个spi的节点)
  接着又踏进第二个深坑,内核申请的framebuffer空间地址怎么都和应用层通信不成功。一开始怀疑是自己得问题,是自己写得fb_mmap函数有问题,但百度各种mmap函数的写法,代码魔改无数次都失败了;于是怀疑是内核版本的问题,专门实现了操作方法结构体ops中的open函数和fb_mmap函数做对比,看到底问题出在哪里,结果发现open函数被成功调用,但fb_mmap函数一样地实现方式,它就没被成功调用。分析我这个fb_mmap函数的实现方式,这是从网上多个教程交叉验证得到地可行方法啊!应该没问题!既然自己的驱动有问题,那就看官方驱动的调用流程,再反向分析自己的代码。
  分析官方st7789v的framebuffer驱动,对比差异查找调用失败原因。发现官方使用得ops中从头到尾也没用到mmap函数,但在应用层竟然能获取到地址,这是让我产生了更深的疑问!因为我无论是从百度还是csdn等各种地方查找的资料都显示:想要在应用层调用mmap的函数,那驱动层必须要在操作方法结构体ops中实现自己的mmap函数。而我在最新的驱动文件中并未发现这个函数,且之前自己写的驱动中实现过这个函数,但系统未调用(实现了fb_open函数和fb_mmap函数,但只调用了open函数,怀疑是mmap函数的参数不对,导致没调用)。
  基于上面两点提出了最终的问题,这个函数在哪,怎么被调用的?层层分析,最终抽丝剥茧找到/video/fbdev/core/fbmem.c文件,发现了framebuffer子系统框架对于mmap函数的处理调用逻辑,也明白了这个子系统对于操作方法结构体有自己一套默认的实现函数,在用户驱动未实现自己的操作方法结构体时,系统默认调用这个函数。
  至此整个驱动的调用流程就全通了,理所应当屏幕也顺利点亮了。

2 spi驱动简单分析

  官方的触摸屏项目来源于树莓派的示例,所以他的spi驱动代码已经很成熟简洁了;整体思路也比较清晰明了,稍微用心看看就能看懂。但缺点就是我上个项目提过的,全部引脚都用的sysfs文件系统在应用层调用;这样的方法感觉虽然简单提高了驱动的兼容性,但也增加了应用层代码的复杂程度。
  这里简单梳理一下从应用层到驱动的调用流程,通过第3、4两步可以看出引脚的状态控制是通过sysfs文件系统在应用层控制的,而spi传输数据则是通过ioctl方式将数据发送给驱动,由驱动处理数据后将数据通过spi子系统传输给硬件的!



  如下图是sysfs文件系统中设备树与应用层的调用流程。



  如下是节选的BL引脚的sysfs的初始化设备树写法。

/ {
    model = "Luckfox Pico Plus";
    compatible = "rockchip,rv1103g-38x38-ipc-v10", "rockchip,rv1103";
    /*LCD_BL*/
    gpio0pa4:gpio0pa4 {
        compatible = "regulator-fixed";
        pinctrl-names = "default";
        pinctrl-0 = <&gpio0_pa4>;
        regulator-name = "gpio0_pa4";
        regulator-always-on;
    };
}
&pinctrl {
    /*LCD_BL*/
    gpio0-pa4 {
        gpio0_pa4:gpio0-pa4 {
            rockchip,pins = <0 RK_PA4 RK_FUNC_GPIO &pcfg_pull_none>;
        };
    };
}

  综上所述可以看出官方demo其实核心思路就是调用spi子系统传输数据,其他代码都是围绕这个核心编写的,把握这个思路,整体读下来就很清晰明了了。

3 framebuffer驱动

  想要读懂framebuffer驱动的代码主要看两个模块,一个是初始化函数,一个是操作方法结构体。通过prob入口函数可以了解所有变量及函数的注册及初始化流程;通过ops操作方法结构体可以找到所有调用read、write等接口函数与应用层交互的逻辑;二者结合很容易就能找到思路读懂整个驱动。
  整体读下来官方的framebuffer驱动,总结其核心函数就是fb_mmap函数;与硬件交互的核心还是spi子系统那套函数;剩余的关键点就是初始化lcd屏幕得那套逻辑了。总得看写法很复杂,但东西还是那些东西。

3.1 framebuffer子系统

3.1.1 fb_mmap函数

  通过我上面的吐槽也能看出,关于framebuffer的问题最终都指向了fb_mmap这个函数。
  如下是frammebuffer子系统在/video/fbdev/core/fbmem.c文件中对fb_mmap的函数实现。通过这个函数,内核实现了对空间的分配,以及将地址传递给应用层接口的功能。

3.1.1.1 函数逻辑分析

  这个函数在代码初始阶段就使用file_fb_info(file)函数获取fb_info的结构体。若在驱动中已经实现了自己的操作方法结构体,则会返回指针,在下面做进一步处理。没有则执行官方自己的逻辑。
  按道理我自己在fb_ops的操作方法结构体中也实现了自己的fb_mmap函数,应该会在执行阶段调用我的函数,却没调用成功;最后怀疑是自己实现函数的参数和类型不对,导致在后续执行时发生了分配空间的错误。

static int
fb_mmap(struct file *file, struct vm_area_struct * vma)
{
        //获取驱动的操作方法结构体,并赋给info
        struct fb_info *info = file_fb_info(file);
        int (*fb_mmap_fn)(struct fb_info *info, struct vm_area_struct *vma);
        unsigned long mmio_pgoff;
        unsigned long start;
        u32 len;
        //判断,info为空,返回错误
        if (!info)
                return -ENODEV;
        mutex_lock(&info->mm_lock);
        //无论驱动中是否实现了fb_mmap,直接赋值,为空也没问题。
        fb_mmap_fn = info->fbops->fb_mmap;
//优化选项,若在config文件中配置,则会使用内核的mmap函数。
#if IS_ENABLED(CONFIG_FB_DEFERRED_IO)
        if (info->fbdefio)
                fb_mmap_fn = fb_deferred_io_mmap;
#endif
        //这里对赋值的fb_mmap_fn函数进行判断,若写了自己的mmap函数,则直接进入,否则执行下一步   
        if (fb_mmap_fn) {
                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_mmap_fn(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.
         */
         //对于没实现自己mmap的函数,则正常执行系统自带的mmap的逻辑
        start = info->fix.smem_start;
        len = info->fix.smem_len;
        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);
}

3.1.1.2 函数功能实现

  搞懂内核对mmap函数的处理逻辑后,对函数做进一步分析,了解关键分配内存空间的流程。

3.1.1.2.1 内存权限分配

  vma->vm_page_prot是用来描述映射区域的内存页权限。这里通过调用 pgprot_decrypted()函数,确保映射区域不包含SME(Secure Memory Encryption)保护。
  SME是一种内存加密技术,旨在通过硬件级的内存加密来保护系统中的敏感数据。某些平台(例如 AMD EPYC 处理器)支持SME。
  pgprot_decrypted()函数会移除SME保护,使得内存可以在未加密的情况下访问。这一步确保映射的 Framebuffer内存是可解密访问的。

vma->vm_page_prot = pgprot_decrypted(vma->vm_page_prot);
3.1.1.2.2 映射地址空间

  这段代码是 Linux 内核中 fb_mmap 函数的一部分,用于处理 framebuffer 显存(framebuffer memory)和 MMIO(内存映射 I/O)区域的内存映射。它决定了用户空间请求 mmap 时,如何将 framebuffer 或设备的 MMIO 映射到用户进程的地址空间。

//1.提取smem_start和smem_len
start = info->fix.smem_start;
len = info->fix.smem_len;
//info->fix.smem_start:这是 framebuffer的显存起始地址,通常表示设备上显存的物理地址。
//info->fix.smem_len:显存的长度(即显存的大小)。
//这些信息来自于fb_info->fix结构体,通常是在初始化framebuffer时由驱动设置的。

//2.计算mmio_pgoff(MMIO 偏移量)
mmio_pgoff = PAGE_ALIGN((start & ~PAGE_MASK) + len) >> PAGE_SHIFT;
//这里的PAGE_ALIGN是用来将起始地址对齐到页面大小(通常是 4KB)。
//start&~PAGE_MASK会移除低位地址中的页面偏移量(即只保留页面对齐的部分)。
//将显存地址start和长度len对齐后,计算对应的页面偏移量,并通过>>PAGE_SHIFT将字节偏移量转化为页偏移量。
//mmio_pgoff表示MMIO区域的页偏移量。MMIO 是内存映射I/O,用于访问硬件寄存器或其他非标准内存区域。

//3.判断是否为 MMIO 区域
if (vma->vm_pgoff >= mmio_pgoff) {
//vma->vm_pgoff:这个是用户请求 mmap 时传递的页面偏移量,表示用户想映射的内存区域的起始地址。
//如果用户请求的页面偏移量大于等于 mmio_pgoff,意味着用户想要访问的内存超出了 framebuffer 显存区域,可能在请求 MMIO 区域。

//4.处理 MMIO 区域的映射
if (info->var.accel_flags) {
    mutex_unlock(&info->mm_lock);
    return -EINVAL;
}
//如果设备有硬件加速功能(info->var.accel_flags 表示加速标志位),则不允许映射 MMIO 区域,返回错误 -EINVAL。
vma->vm_pgoff -= mmio_pgoff;
start = info->fix.mmio_start;
len = info->fix.mmio_len;

//如果没有硬件加速,则处理 MMIO 区域的映射。
//vma->vm_pgoff -= mmio_pgoff:调整偏移量,减去显存区域的页偏移量,以便接下来映射 MMIO 区域。
//将 start 设置为 info->fix.mmio_start(MMIO 的起始地址),将 len 设置为 info->fix.mmio_len(MMIO 的长度),从而准备映射 MMIO 内存。

//5.解锁互斥锁
mutex_unlock(&info->mm_lock);
//之前在函数开始时锁定了 info->mm_lock,现在需要解锁。该锁确保对 info 结构的访问是线程安全的。

//6.设置内存页权限
vma->vm_page_prot = vm_get_page_prot(vma->vm_flags);
vma->vm_page_prot = pgprot_decrypted(vma->vm_page_prot);
//vm_get_page_prot(vma->vm_flags):根据 vma 的标志位,获取合适的内存页面权限。
//pgprot_decrypted(vma->vm_page_prot):确保 SME(Secure Memory Encryption)保护被移除,以允许未加密访问。这是为了适配某些硬件的加密机制,确保内存可以直接访问。

//7.保护 framebuffer 映射
fb_pgprotect(file, vma, start);
//该函数确保映射的 framebuffer 或 MMIO 区域具备适当的保护措施(如只读或可读写权限)。fb_pgprotect 是一个辅助函数,用于调整页表的权限。

//8.将内存映射到用户空间
return vm_iomap_memory(vma, start, len);
//vm_iomap_memory:将物理内存(start 到 start+len)映射到用户进程的地址空间。这是一个通用的内核函数,处理将物理内存页映射到虚拟内存。

3.1.2 驱动接口注册

  当framebuffer驱动被加载时,驱动会调用 register_framebuffer()函数来注册framebuffer设备。在这个过程中,/dev/fb0 设备节点也会自动创建。

int register_framebuffer(struct fb_info *fb_info)

3.1.3 应用层接口示例

  在驱动成功注册fb0接口后,应用层即可利用此接口获取文件描述符,进一步利用mmap在应用层的接口获取分配的内存空间的地址,传输图像数据给内核。如下是应用层测试用例:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <linux/fb.h>
#include <sys/ioctl.h>
#include <stdint.h>
#include <string.h>

// 定义颜色
#define RED     0x00FF0000
#define GREEN   0x0000FF00
#define BLUE    0x000000FF
#define WHITE   0x00FFFFFF
#define BLACK   0x00000000

// framebuffer 结构
struct framebuffer_info {
    uint32_t width;   // 屏幕宽度
    uint32_t height;  // 屏幕高度
    uint32_t bpp;     // 每像素位数
    size_t screensize; // 映射内存的大小
    uint32_t *fbp;    // framebuffer指针
};

struct framebuffer_info get_framebuffer_info(int fb_fd) {
    struct framebuffer_info fb_info;
    struct fb_var_screeninfo vinfo;
    struct fb_fix_screeninfo finfo;

    // 获取固定的屏幕信息
    ioctl(fb_fd, FBIOGET_FSCREENINFO, &finfo);
    // 获取可变的屏幕信息
    ioctl(fb_fd, FBIOGET_VSCREENINFO, &vinfo);

    fb_info.width = vinfo.xres;
    fb_info.height = vinfo.yres;
    fb_info.bpp = vinfo.bits_per_pixel;
    fb_info.screensize = finfo.smem_len;

    return fb_info;
}

void draw_color(struct framebuffer_info *fb_info, uint32_t color) {
    size_t pixels = fb_info->width * fb_info->height;
    for (size_t i = 0; i < pixels; ++i) {
        fb_info->fbp[i] = color;
    }
}

int main() {
    int fb_fd = open("/dev/fb0", O_RDWR);
    if (fb_fd == -1) {
        perror("Error: cannot open framebuffer device");
        exit(1);
    }

    struct framebuffer_info fb_info = get_framebuffer_info(fb_fd);

    // 使用 mmap 映射 framebuffer 到内存
    fb_info.fbp = (uint32_t*)mmap(0, fb_info.screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fb_fd, 0);
    if ((intptr_t)fb_info.fbp == -1) {
        perror("Error: failed to mmap");
        close(fb_fd);
        exit(1);
    }

    // 显示红色
    draw_color(&fb_info, RED);
    sleep(2);  // 显示 2 秒

    // 显示绿色
    draw_color(&fb_info, GREEN);
    sleep(2);  // 显示 2 秒

    // 显示蓝色
    draw_color(&fb_info, BLUE);
    sleep(2);  // 显示 2 秒

    // 清除屏幕,显示黑色
    draw_color(&fb_info, BLACK);
    sleep(2);  // 显示 2 秒

    // 取消映射并关闭 framebuffer 设备
    munmap(fb_info.fbp, fb_info.screensize);
    close(fb_fd);

    return 0;
}

3.2 framebuffer驱动的颜色显示模块

  应用层到驱动的路通了以后,接着分析驱动到硬件的路。
  传输数据前首要的工作就是对应用层的数据进行处理,而这段处理程序恰恰也是整个驱动中最琐碎的代码。不难但混乱,读代码时要频繁的跳转寻找其调用逻辑,来理解驱动作者编写的思路。

3.2.1 驱动中的结构体

  理解代码的第一步,分析驱动的数据结构体。如下是驱动自身编写的结构体。

    struct fbtft_par {
    struct spi_device *spi;//spi指针
    struct platform_device *pdev;
    struct fb_info *info;//内核的fb的信息结构体
    struct fbtft_platform_data *pdata;
    u32 pseudo_palette[16];
    struct {
        void *buf;
        size_t len;
    } txbuf;
    u8 *buf;
    struct fbtft_ops fbtftops;//操作方法结构体指针
    struct {
        struct mutex lock;
        u32 *curves;
        int num_values;
        int num_curves;
    } gamma;
    bool bgr;//红蓝反置标志位
    void *extra;
    bool polarity;
};

  如下是内核frammebuffer子系统的结构体,存储着子系统的各类信息。

struct fb_info {
    int node;                           // framebuffer 的编号,例如 /dev/fb0 则 node 为 0
    int flags;                          // 标志位,控制设备行为
    struct mutex lock;                  // 保护 fb_info 的锁
    struct mutex mm_lock;               // 映射操作 (mmap) 的锁
    struct fb_var_screeninfo var;       // 可变屏幕信息,如分辨率、色深等
    struct fb_fix_screeninfo fix;       // 固定屏幕信息,如显存地址等
    struct fb_monspecs monspecs;        // 显示器的规格信息
    struct fb_pixmap pixmap;            // 一些图片缓冲相关信息
    struct fb_ops *fbops;               // 指向 framebuffer 操作函数的指针(包括 mmap、read、write 等)
    struct device *device;              // 指向该 framebuffer 设备的指针
    struct fb_deferred_io *fbdefio;     // 延迟 IO 相关结构体(如果支持的话)
    void *pseudo_palette;               // 用于伪调色板的存储
    void *par;                          // 驱动程序的私有数据
    unsigned char *screen_base;         // 显存的起始地址
    unsigned long screen_size;          // 显存的大小
    // ... 其他成员
};

3.2.2 驱动的初始化

  驱动的prob入口是匹配成功后进入的主函数,因此一系列初始化操作均在这个函数中进行。如下是截取的关键函数部分:

int fbtft_probe_common(struct fbtft_display *display,
               struct spi_device *sdev,
               struct platform_device *pdev)
{
    struct device *dev;
    struct fb_info *info;
    struct fbtft_par *par;
    struct fbtft_platform_data *pdata;
    info = fbtft_framebuffer_alloc(display, dev, pdata); //创建一个新的帧缓冲区信息结构,分配空间等操作

  由此进入初始化info的结构体,初始化各项参数

struct fb_info *fbtft_framebuffer_alloc(struct fbtft_display *display,
                    struct device *dev,
                    struct fbtft_platform_data *pdata)
{
    struct fb_info *info;
    struct fbtft_par *par;
    struct fb_ops *fbops = NULL;//分配帧缓冲设备
    info = framebuffer_alloc(sizeof(struct fbtft_par), dev);

    fbops->owner        =      dev->driver->owner;
    fbops->fb_read      =      fb_sys_read;
    fbops->fb_write     =      fbtft_fb_write;
    fbops->fb_fillrect  =      fbtft_fb_fillrect;
    fbops->fb_copyarea  =      fbtft_fb_copyarea;
    fbops->fb_imageblit =      fbtft_fb_imageblit;
    fbops->fb_setcolreg =      fbtft_fb_setcolreg;
    fbops->fb_blank     =      fbtft_fb_blank;

    fbdefio->delay =           HZ / fps;
    fbdefio->deferred_io =     fbtft_deferred_io;
    fb_deferred_io_init(info);
//初始化结构体的固定参数
    snprintf(info->fix.id, sizeof(info->fix.id), "%s", dev->driver->name);
    info->fix.type =           FB_TYPE_PACKED_PIXELS;
    info->fix.visual =         FB_VISUAL_TRUECOLOR;
    info->fix.xpanstep =       0;
    //...大部分初始化参数省略

3.2.3 配置颜色相关结构体与函数

  配置颜色涉及到了核心的fbtft_framebuffer_alloc函数,通过对此函数调用流程的分析,了解系统对颜色数据的处理操作。

3.2.3.1 初始化结构体函数指针

//在fbtft_framebuffer_alloc函数中初始化函数指针
    fbops->fb_setcolreg =      fbtft_fb_setcolreg;
//配置颜色的数据类型
    /* RGB565 *///红蓝反了
    info->var.red.offset =     11;
    info->var.red.length =     5;
    info->var.green.offset =   5;
    info->var.green.length =   6;
    info->var.blue.offset =    0;
    info->var.blue.length =    5;
    info->var.transp.offset =  0;
    info->var.transp.length =  0;
    info->flags =              FBINFO_FLAG_DEFAULT | FBINFO_VIRTFB;

3.2.3.2 设置颜色寄存器(调色板)的函数

  到这其实基本就可以停止分析了,因为分析到这我发现驱动压根就没使用这个显示颜色的方式。从下面代码可以看出,pal[regno]在这赋值后,压根就没有函数调用这个接口。驱动是直接使用的"rgb565"16位的颜色数据。

//设置颜色寄存器的函数
static int fbtft_fb_setcolreg(unsigned int regno, unsigned int red,
                  unsigned int green, unsigned int blue,
                  unsigned int transp, struct fb_info *info)
{
    unsigned int val;
    int ret = 1;

    dev_dbg(info->dev,
        "%s(regno=%u, red=0x%X, green=0x%X, blue=0x%X, trans=0x%X)\n",
        __func__, regno, red, green, blue, transp);

    switch (info->fix.visual) {
    case FB_VISUAL_TRUECOLOR:
    //配置的基础色一共16个
        if (regno < 16) {
            u32 *pal = 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);
            //根据值确定最后的颜色
            pal[regno] = val;
            ret = 0;
        }
        break;
    }
    return ret;
}

  将通道值转换成一个帧缓冲的16位字段值

/* from pxafb.c */
//将通道值转换成一个帧缓冲的16位字段值
static unsigned int chan_to_field(unsigned int chan, struct fb_bitfield *bf)
{
    chan &= 0xffff;
    chan >>= 16 - bf->length;
    return chan << bf->offset;
}

3.2.3.3 8位真彩的显示逻辑

  假设驱动使用此方式显示颜色,继续往下分析。因此需要先了解8位真彩以及调色板的概念。
  调色板的概念:假如LCD的数据位为16位,那么framebuffer应该每个像素占据16bit,但是为了节省空间,使用8bit表示一个像素,这时候需要引入调色板才能正确传输16位的数据给LCD(正确传输每个像素的数据)。调色板其实就是一片内存,这里面每一格存放16bit的数据。当LCD控制器从framebuffer中取出8bit的数据后,不是直接传给LCD,而是用这个8bit作为索引,从调色板中取出16bit的数据,然后发给LCD。所以,在使用8BPP(每个像素点的位数)格式时,framebuffer中存放的是伪彩色。16BPP或者24BPP格式时,framebuffer中存放的才是真彩色。所以在使用8BPP格式时,首先要设置调色板。如下图是调用逻辑:



  综上所述我们了解了整个8位真彩显示的调用流程,驱动代码里缺失的部分就是从应用层获取8位的颜色编码后,通过编码调用调色板的逻辑,因为没用过就没写这个逻辑,大概了解就行了。

3.2.3.4 应用层调用调色板示例

  如下代码是简单的调用ioctl接口实现8位真彩显示颜色的示例;如果想用mmap的接口,只需把颜色数据处理部分放到驱动中,应用层与驱动层配置好传输协议可以了。

#include <linux/fb.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>

int set_color(int fb_fd, unsigned int regno, unsigned int red, unsigned int green, unsigned int blue) {
    struct fb_cmap cmap;
    cmap.start = regno;           // 设置颜色寄存器号
    cmap.len = 1;                 // 只设置一个颜色
    cmap.red = &red;              // 红色值
    cmap.green = &green;          // 绿色值
    cmap.blue = &blue;            // 蓝色值
    cmap.transp = NULL;           // 不透明度

    return ioctl(fb_fd, FBIOPUTCMAP, &cmap);
}

int main() {
    int fb_fd = open("/dev/fb0", O_RDWR);
    if (fb_fd < 0) {
        return -1;
    }

    // 设置寄存器 0 对应的颜色为红色
    set_color(fb_fd, 0, 0xFFFF, 0x0000, 0x0000);

    close(fb_fd);
    return 0;
}

3.2.3.5 实际颜色显示方式

  而在本驱动中并未使用上文显示颜色的逻辑;因为使用的是16位rgb565颜色数据,frammebuffer直接接收颜色数据发送给屏幕即可,不需要像这种8位真彩一样进行数据处理。实际的显示颜色的方式看上文mmap函数的整体调用流程。

3.3 framebuffer中的spi子系统

  驱动不论怎么写,但凡涉及到spi接口的数据传输,肯定会使用spi子系统。唯一的区别在于开发人员会因势利导对子系统代码的调用做更合适本驱动的优化罢了!

3.3.1 初始化驱动自带的结构体

  通过prob函数初始化fbtft_framebuffer_alloc函数中的par驱动私有结构体。

//初始化par结构体的各项参数,如调试标志,缓冲区指针,旋转角度,gamma曲线
    par = info->par;
    par->info = info;
    par->pdata = pdata;
    par->debug = display->debug;
    par->buf = buf;
    spin_lock_init(&par->dirty_lock);
    //par->bgr = pdata->bgr;
    par->bgr = 1;//红蓝反置参数
    par->fbtftops.write = fbtft_write_spi;//spi子系统的写函数
    par->fbtftops.write_vmem = fbtft_write_vmem16_bus8;//spi子系统写函数的调用函数
    //...大部分初始化参数省略

3.3.2 实际初始化逻辑

  分析fbtft_write_vmem16_bus8函数;在上述结构体中虽然赋值了,但在prob函数中又重新进行了赋值判断,所以上面的赋值无效。真正的赋值逻辑如下所示,需要判断总线宽度的值,来确定传输函数到底是哪个?

    if (display->buswidth == 8)
        par->fbtftops.write_vmem = fbtft_write_vmem16_bus8;
    else if (display->buswidth == 9)
        par->fbtftops.write_vmem = fbtft_write_vmem16_bus9;
    else if (display->buswidth == 16)
        par->fbtftops.write_vmem = fbtft_write_vmem16_bus16;

3.3.3 buildwith的赋值

//从设备的属性中读取信息,填充到fbtft_platform_data结构体中
static struct fbtft_platform_data *fbtft_properties_read(struct device *dev)
{
    struct fbtft_platform_data *pdata;
    pdata->display.buswidth = fbtft_property_value(dev, "buswidth");
    pdata->bgr = device_property_read_bool(dev, "bgr");

3.3.4 设备树的配置

  配置设备树中的buswidth属性值。

   ili9488@0{
       compatible = "ilitek,ili9488";
       buswidth = <8>;
       bgr = <1>;
   };

3.3.5 调用fbtft_write_vmem16_bus8的流程

  在确定这个是写函数后,那就要确定其调用流程,逐层分析调用代码逻辑。具体调用流程如下:
1.将写函数集成进刷新屏幕的函数中,将多个函数抽象为刷新屏幕功能。

//更新显示屏的内容,根据起始行和结束行的,将对应的显存内容写入到显示屏
static void fbtft_update_display(struct fbtft_par *par, unsigned int start_line,
                 unsigned int end_line)
{
    ret = par->fbtftops.write_vmem(par, offset, len);

2.为结构体中的函数指针赋值。

par->fbtftops.update_display = fbtft_update_display;

3.注册阶段调用更新屏幕函数
  注册阶段需要调用写函数传输初始化命令等操作。在register中调用更新屏幕的逻辑间接调用写函数。

int fbtft_register_framebuffer(struct fb_info *fb_info)
{
par->fbtftops.update_display(par, 0, par->info->var.yres - 1);

  最后由prob函数中调用注册函数,向写函数中传输数据。

int fbtft_probe_common(struct fbtft_display *display,
               struct spi_device *sdev,
               struct platform_device *pdev)
{
ret = fbtft_register_framebuffer(info);

5.正常刷新屏幕
  除了注册阶段要写数据,刷新屏幕也要写数据。此部分驱动地思路是使用定时器固定时间步长调用刷新函数传输数据。具体地逻辑如下:

//延时io函数
static void fbtft_deferred_io(struct fb_info *info, struct list_head *pagelist)
{
    par->fbtftops.update_display(info->par,
                    dirty_lines_start, dirty_lines_end);

  将驱动的延时I/O函数填充到函数指针中;并初始化定时器函数。

    fbdefio->deferred_io =     fbtft_deferred_io;
    fb_deferred_io_init(info);

  正常追函数到这里又断掉了,分析发现fb_deferred_io_init是frammebuffer子系统自带的延时I/O机制函数。内部使用定时器实现了定时调用写函数的逻辑,将缓冲区内的数据传输到lcd屏幕上。函数的实现在drivers/video/fbdev/core/fb_defio.c文件里。

3.3.6 处理数据的逻辑

  分析到此处可以看出驱动调用的函数是fbtft_write_vmem16_bus8。整体思路就是将数据存放到vmem16的数组中,然后利用系统的转换字节序的接口进行数据处理,然后发给下面的spi子系统。

/* 16 bit pixel over 8-bit databus */
int fbtft_write_vmem16_bus8(struct fbtft_par *par, size_t offset, size_t len)
{
    u16 *vmem16;
    __be16 *txbuf16 = par->txbuf.buf;
    size_t remain;
    size_t to_copy;
    size_t tx_array_size;
    int i;
    int ret = 0;
    size_t startbyte_size = 0;

    fbtft_par_dbg(DEBUG_WRITE_VMEM, par, "%s(offset=%zu, len=%zu)\n",
              __func__, offset, len);

    remain = len / 2;
    vmem16 = (u16 *)(par->info->screen_buffer + offset);

    gpiod_set_value(par->gpio.dc, 1);

    /* non buffered write */
    if (!par->txbuf.buf)
        return par->fbtftops.write(par, vmem16, len);

    /* buffered write */
    tx_array_size = par->txbuf.len / 2;

    if (par->startbyte) {
        txbuf16 = par->txbuf.buf + 1;
        tx_array_size -= 2;
        *(u8 *)(par->txbuf.buf) = par->startbyte | 0x2;
        startbyte_size = 1;
    }

    while (remain) {
        to_copy = min(tx_array_size, remain);
        dev_dbg(par->info->device, "to_copy=%zu, remain=%zu\n",
            to_copy, remain - to_copy);

        for (i = 0; i < to_copy; i++)
        {
            txbuf16[i] = cpu_to_be16(vmem16[i]);
//            printk("转换字节序aaaaaaaaa\n");

        }

        vmem16 = vmem16 + to_copy;
        ret = par->fbtftops.write(par, par->txbuf.buf,
                        startbyte_size + to_copy * 2);
        if (ret < 0)
            return ret;
        remain -= to_copy;
    }

    return ret;
}

3.3.7 spi子系统传输数据

  调用spi子系统par->fbtftops.write。

int fbtft_write_spi(struct fbtft_par *par, void *buf, size_t len)
{
    struct spi_transfer t = {
        .tx_buf = buf,
        .len = len,
    };
    struct spi_message m;

    fbtft_par_dbg_hex(DEBUG_WRITE, par, par->info->device, u8, buf, len,
              "%s(len=%zu): ", __func__, len);

    if (!par->spi) {
        dev_err(par->info->device,
            "%s: par->spi is unexpectedly NULL\n", __func__);
        return -1;
    }
    spi_message_init(&m);
    spi_message_add_tail(&t, &m);
    return spi_sync(par->spi, &m);
}

  驱动中spi子系统的调用流程完成。

3.4 小结

  完成这个驱动后,总结发现framebuffer驱动对比原有spi驱动的优点和核心就是使用了fb_mmap函数。它是framebuffer设备相关的一个内存映射函数。它的主要作用是将framebuffer或MMIOMemory-Mapped I/O)区域映射到用户空间,使用户可以通过内存访问设备数据。最终效果就是提高了应用层和内核数据的传输效率。
  至此驱动的主线逻辑就已经捋清楚了,其他部分代码逻辑简单清晰,没啥讲解地必要。另外有一部分初始化地逻辑在硬件篇已讲过,驱动篇就不再赘述了!

4 设备树

4.1 关闭设备节点

  设备树也是一个坎,其他部分内容在网上都有参考依据,代码调试调试都能改出来。但下面这个关闭spidev节点是真的想不到,这个东西如果官方不放出来的话,很难自己搞出来;因为根本就和设备树不搭边啊!设置各种属性是知道的,但单独关闭一个spi的节点,这是怎么能想到的呢?

spidev@0 {
    status = "disabled";
};

  此外,如果还是点亮不成功,可以考虑在设备树里加一下这个参数,用于调整spi得时钟相位和时钟极性。这个参数是我在开发板群里向其他大佬请教时获取得一组参数,适用的屏幕应该是st7789v芯片屏幕,具体情况还是要看开发手册。

spi-cpol;
spi-cpha;

4.2 设备树编写

  看了半天感觉设备树没啥讲的,就是初始化几个节点,具体配置如下:

&spi0 {
    status = "okay";
    pinctrl-names = "default";
    pinctrl-0 = <&spi0m0_pins &tp_irq>;
    // cs-gpios = <&gpio1 RK_PC0 1>;
    // cs-gpios = <&gpio1 26 1>;
    #address-cells = <1>;
    #size-cells = <0>;
    spidev@0 {
              status = "disabled";
      };
   ili9488@0{
       status = "okay";
       compatible = "ilitek,ili9488";
       reg = <0>;
       spi-max-frequency = <20000000>;
       fps = <30>;
       bpp = <24>;
       buswidth = <8>;
       debug = <0x7>;
       led-gpios = <&gpio0 RK_PA4 GPIO_ACTIVE_LOW>;//BL
       dc = <&gpio1 RK_PA2 GPIO_ACTIVE_HIGH>;      //DC
       reset = <&gpio1 RK_PD1 GPIO_ACTIVE_LOW>;    //RES-ili9488
    //   reset = <&gpio1 RK_PC3 GPIO_ACTIVE_LOW>;    //RES-st7789v
   };

};
&pinctrl {
    spi0 {
        /omit-if-no-ref/
        spi0m0_pins: spi0m0-pins {
            rockchip,pins =
                /* spi0_clk_m0 */
                <1 RK_PC1 4 &pcfg_pull_none>,
                /* spie_miso_m0 */
                <1 RK_PC3 6 &pcfg_pull_none>, 
                /* spi_mosi_m0 */
                <1 RK_PC2 6 &pcfg_pull_none>;
        };
    };
};

  注:还有一部分引脚初始化的代码我就不放了,都是千篇一律的东西,想了解的看上面spi章节的截图,自己照葫芦画瓢修改就行了。

4.3 小结

  至此,我们可以看出frammebuffer驱动核心内容也就三大模块,frammmebuffer子系统、spi子系统、颜色配置,搞懂这三大块剩余部分都是细枝末节,慢慢读都可以看懂。其实很想吐槽得一点就是,官方为了兼容性,把驱动写得过于全面和复杂,我为了方便读代码,专门把官方的驱动整合到了一个.c文件中,整理过程中发现很多函数和代码压根就没被调用,精简完以后,代码少了很大一块。

5 drm的简介

  我因为屏幕限制,没搞drm这么高级的驱动,就查询整理了一些资料分享给大家。
  DRM(Direct Rendering Manager)驱动和Framebuffer驱动是Linux操作系统中用于管理图形硬件的两种不同的图形子系统,它们在架构和功能上存在显著的差异。

5.1 设计目标

  Framebuffer 驱动:

最初设计用于简单的图形输出设备。
目标是提供基本的、直接的帧缓冲访问接口,允许用户空间应用程序直接写入显存,绘制图像。
它不支持现代图形硬件的高级特性,比如硬件加速、显示管道管理、VSync、双缓冲等。
适用于简单的嵌入式系统和终端设备,性能需求较低。

  DRM 驱动:

设计目标是支持现代 GPU 和复杂的图形硬件,主要用于桌面系统、图形界面和需要硬件加速的应用程序(例如游戏、图形密集型应用)。
支持硬件加速、渲染、多显示器、双缓冲等高级功能。
DRM 驱动与 KMS(Kernel Mode Setting,内核模式设置)紧密结合,允许内核控制显示模式、连接器等显示资源管理。

5.2 架构

  Framebuffer 驱动:

简单的架构,核心是通过 /dev/fb0 等设备节点与显存直接交互。
通过 mmap 映射显存,用户空间应用可以直接操作显存中的每个像素。
没有 GPU 硬件加速支持,也不处理复杂的显示任务。
显示的分辨率和颜色深度等设置在驱动加载时完成,无法动态切换。

  DRM 驱动:

DRM 驱动是 Linux 内核中的一个子系统,包含两个主要部分:渲染和显示。
DRM 通过与 GPU 硬件的交互,实现硬件加速渲染,同时管理显示输出。
KMS 是 DRM 的一部分,用于管理显示管道,设置显示模式(如分辨率、刷新率)等。
DRM 驱动通过与用户空间图形栈(如 Mesa、Xorg 或 Wayland)协作,提供复杂的图形操作和渲染支持。
支持多缓冲(如双缓冲、三缓冲)和 VSync,以确保平滑的图像更新。

5.3 功能和性能

  Framebuffer 驱动:

功能简单,适合直接访问显存进行像素级别的绘制操作。
不支持硬件加速,所有的图形操作(如图形绘制、窗口管理等)都依赖于 CPU,这对于复杂的图形应用程序性能较差。
不具备多显示器管理、硬件加速渲染等功能。
典型使用场景:嵌入式系统中的控制台、简单图形应用。

  DRM 驱动:

支持 GPU 加速,可以使用 GPU 来执行复杂的图形计算任务,如 3D 渲染、图像处理等。
具备多显示器支持,可以通过内核动态调整显示器的分辨率、刷新率、颜色格式等。
通过 VSync 同步,防止图像撕裂,支持更高的帧率和显示效果。
典型使用场景:桌面环境、游戏、3D 图形应用等。

5.4 用户空间接口

  Framebuffer 驱动:

提供简单的用户空间接口 /dev/fbX,可以通过文件读写和 ioctl 调用与驱动进行交互。
主要用于绘制像素数据,但不支持复杂的 3D 渲染。

  DRM 驱动:

通过 /dev/dri/cardX 提供接口,允许用户空间通过 DRM IOCTL 与 GPU 驱动进行交互。
支持用户空间的图形库,如 Mesa3D、X11、Wayland、EGL 等。
现代图形栈(如 Xorg、Wayland)都依赖 DRM 驱动来提供硬件加速支持。

5.5 显示管理

  Framebuffer 驱动:

依赖于固定分辨率的模式设置,难以动态调整显示输出。
缺乏多显示器支持,不能同时管理多个显示设备。

  DRM 驱动:

通过 KMS 可以动态设置和管理显示模式,如分辨率、刷新率等。
具备多显示器支持,能够管理多个显示输出(如 HDMI、DisplayPort、VGA 等)。

5.6 使用场景

  Framebuffer 驱动:

简单图形输出,不需要 GPU 加速的应用。
主要用于嵌入式设备或不需要复杂图形操作的系统。
终端控制台、早期的 Linux 系统控制台、一些嵌入式系统。

  DRM 驱动:

桌面系统(如 GNOME、KDE)使用 DRM 驱动来提供图形加速。
游戏、视频播放和需要高性能渲染的应用程序都依赖于 DRM 驱动。
现代桌面系统的显示服务器(如 Wayland、Xorg)通过 DRM 驱动来控制显示输出和渲染操作。

5.7 小结

  Framebuffer 是一种老旧的显示管理方式,主要提供直接的显存访问,不支持硬件加速,功能较为简单。
DRM 则是现代 Linux 图形栈的重要部分,支持 GPU 硬件加速和复杂的显示管理,是当前桌面系统和图形应用的核心组件。

6 总结

  至此,lcd屏幕的显示部分驱动就移植完成了,这个文章的着重点其实就是我在移植屏幕时遇到的问题。整体移植完成回来看时,我觉得framebuffer子系统没啥太难的部分;都是因为不熟悉驱动框架,才导致我不断的踏进各种坑里又不断地爬出来。值得庆幸的是最终我完成了驱动地移植工作。
  最后,这个是我个人在移植屏幕时遇到地问题,如何阅读文章地各位遇到其他问题欢迎评论区一起讨论!!!!

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇