美文网首页BB-black开发板[Linux arm-v8]
Linux驱动OLED屏st7735s(framebuffer学

Linux驱动OLED屏st7735s(framebuffer学

作者: applecai | 来源:发表于2020-12-26 15:42 被阅读0次

    一,前言

    我的学习主要聚焦视觉相关的内容。那么为什么之前要学习IMU驱动呢?原因很简单,因为camera比较老的传输接口用的是SCCB(兼容i2c)所以我要学习i2c驱动,另外,camera一般会用IMU来进行防抖应用,所以也是相关项。然后就是spi一般触摸屏会用此接口,有些小屏也会用spi总线。显示屏总和视觉相关了吧!那么,总结一句就是万物皆有关联,哈哈~另外,将来学到算法层的时候惯导算法和视觉算法都加入后,会有比较多的小应用可以玩,我现在等于做了第一步的准备工作了。
    那么今天的主角是谁呢?因为我买六轴陀螺仪的时候看到淘宝店家还有一个0.96存的OLED屏幕是st7735芯片的。我想10年前用过ili9325 LCD屏,还没有用过OLED小屏,那么就一起买了。然后就有了之后的Linux驱动oled屏幕的故事。我当时主要想先学习下framebuffer框架的,因为理解麻雀虽小五脏俱全,有一部分经验是可迁移的,学习的话还是先从简单的来,循序渐进。

    二,理论框架先行

    1.st7735s
    的spec看了下,了解了各个引脚的使用及接口总线和各类寄存器。LUT这个用来做滤镜的查表蛮有意思的。然后就是gamma参数初始化到底要设置为多少我好像不知道,从spec中没看出线索,当然采购屏幕的时候给了初始化代码。唯一比较后悔的是,仔细看了st7735s的spec发生它是3线spi,无法回读,而且DC脚为高电平是写命令,为低电平是写数据。天哪,这个是个没有反馈的芯片(不可读)我就觉得是个不靠谱的产品,而且调试起来一定会比较麻烦。(一周后发现我的预感是对的)
    2.Linux 5.4.61的framebuffer
    我希望直接中drivers/vedio中能搜索出st7735s的驱动,但是没有找到结果在staging的fbtft文件夹中找到了st7735r的驱动。简单的对比了7735r和7735s的spec,他们的分辨率不同,然后刷屏的主频率不同。其它寄存器地址及定义都是一样的。我看到了曙光。那么我就st7735r开始学习framebuffer吧。直接看着代码有点摸不着,需要看看地图定位到st7735r的位置,然后网上搜索了下framebuffer子系统,注册了framebuffer子系统后,fbmen.c中有用户接口会调用到底层具体的芯片接口。然后framebuffer还有一个特点就是mmap内存映射,即用户写ram画完图后就可以将画布存入共享内存进行显示了。
    3.基于st7735r的framebuffer函数流源码分析
    首先通过spi_register_driver来注册它挂在spi总线上,作为st7735r的通信接口。这个和我之前做的input子系统类似,IMU6500可以挂在 spi或i2c总线上,然后注册到input子系统,此系统有用户接口。同理st7735r注册到spi总线后,依然会注册到framebuffer子系统用来提供用户接口。我之前IMU没有白做吧,用来做迁移学习是很容易理解的。
    初始化看完,就是开始看probe函数了。
    fbtft_probe_common->fbtft_register_framebuffer->register_framebuffer
    fbtft_probe_common中的fbtft_framebuffer_alloc把fb_ops结构体对象进行了注册,这个就是将来用户虚拟文件系统要访问的具体底层

        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;
    

    屏幕的spi读写函数也在此

        /* default fbtft operations */
        par->fbtftops.write = fbtft_write_spi;
        par->fbtftops.read = fbtft_read_spi;
        par->fbtftops.write_vmem = fbtft_write_vmem16_bus8;
        par->fbtftops.write_register = fbtft_write_reg8_bus8;
        par->fbtftops.set_addr_win = fbtft_set_addr_win;
        par->fbtftops.reset = fbtft_reset;
        par->fbtftops.mkdirty = fbtft_mkdirty;
        par->fbtftops.update_display = fbtft_update_display;
    

    对于fbtft它在spi操作前,需要操作下dc引脚。在fbtft_probe_dt函数中会从设备数dts中获取io口,在fbtft_request_gpios_dt函数中申请io口,包括spi也支持用io口来模拟。

        pdata = dev->platform_data;
        if (!pdata) {
            pdata = fbtft_probe_dt(dev);
            if (IS_ERR(pdata))
                return PTR_ERR(pdata);
        }
    

    fbtft_register_framebuffer中会先调用ret = par->fbtftops.request_gpios(par);申请io口

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

    fbtft_register_framebuffer再调用ret = par->fbtftops.init_display(par);对屏幕进行初始化寄存器配置。最后调用register_framebuffer,将组织好的fb_info结构体传入framebuffer子系统。
    然后跳到video文件夹下的fbmem.c中搜索file_operations,果然找到了标准的文件操作接口,其中包括fb_mmap
    看了一个write函数,fb_write会调用return info->fbops->fb_write(info, buf, count, ppos);,其实也就是调用fbtft_fb_write底层函数,然后调用res = fb_sys_write(info, buf, count, ppos);

    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);
        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);
        ......
    }
    

    fb_sys_write函数里面比较关键的就是dst = (void __force *) (info->screen_base + p);等于向base地址中直接写值来进行显示。看到这里基本上如何写屏幕都清楚了。我突然有一个疑问就是st7735r支持这样写吗?于是去查spec,只要先写命令0x2C,然后就一直可以写data,除非又写了其它命令。那么我理解只要确保正确初始化的最后一条命令写入0x2C则打开了写数据的通道,可以随意写数据了。但是写数据也要用spi的呀,那么没有看到调用spi的写函数write_register呢?那么搜索0x2C对应的函数,看到了关键了fb_deferred_io_work。里面包括了mmap操作。
    fbtft_framebuffer_alloc中fbtft_deferred_io里面有'par->fbtftops.update_display(info->par,dirty_lines_start, dirty_lines_end);'而update display绑定到fbtft_update_display函数,里面会调用par->fbtftops.set_addr_win(par, 0, start_line,par->info->var.xres - 1, end_line);所以会写spi。
    fbtft_framebuffer_alloc中会调用fb_deferred_io_init(info);绑定一个mmapinfo->fbops->fb_mmap = fb_deferred_io_mmap;以及初始化一个工程线程INIT_DELAYED_WORK(&info->deferred_work, fb_deferred_io_work);这样看来下面的write函数用不到了吧。

    ssize_t fb_sys_write(struct fb_info *info, const char __user *buf,
                 size_t count, loff_t *ppos)
    {
        unsigned long p = *ppos;
        void *dst;
        int err = 0;
        unsigned long total_size;
    ......
    
        dst = (void __force *) (info->screen_base + p);
    
        if (info->fbops->fb_sync)
            info->fbops->fb_sync(info);
    
        if (copy_from_user(dst, buf, count))
            err = -EFAULT;
    
        if  (!err)
            *ppos += count;
    
        return (err) ? err : count;
    }
    

    三,设备树及代码修改

    1. 代码主要是初始化的值按stm32单片机配套代码修改的。而不用他原来的参数。
    2. 设备树修改如下
        oled_pins: pinmux_oled_pins {
            pinctrl-single,pins = <
                AM33XX_PADCONF(AM335X_PIN_GPMC_BEN1, PIN_OUTPUT, MUX_MODE7) /* gpio1 28 dc */
                AM33XX_PADCONF(AM335X_PIN_GPMC_A0, PIN_OUTPUT, MUX_MODE7) /* gpio1 16 blk */
                AM33XX_PADCONF(AM335X_PIN_GPMC_A1,PIN_OUTPUT, MUX_MODE7) /* gpio1 17 reset */
            >;
        };
    &spi1 {
        status = "okay";
        pinctrl-names = "default";
        pinctrl-0 = <&spi1_pins &oled_pins>;
    
        apple_OLED@0 {
            compatible ="sitronix,st7735r";
            reg = <0>;
            spi-max-frequency = <4000000>;
            spi-cpol;
            spi-cpha;
            rotate = <0>;
            fps = <40>;
            buswidth = <8>;
            dc-gpios = <&gpio1 28 GPIO_ACTIVE_HIGH>;  
            reset-gpios = <&gpio1 17 GPIO_ACTIVE_LOW>; 
            led-gpios = <&gpio1 16 GPIO_ACTIVE_LOW>;  /* blk */
            debug = <0x0>;
        };
    };
    #endi
    

    四,遇到的问题

    1. buswidth没有在设备树或代码中定义导致probe后报错,在设备树添加后probe成功。
    2. fb0出现后,发现会打印Console: switching to colour frame buffer device 20x10然后不停的打印graphics fb0: fbtft_fb_imageblit: dx=0, dy=0, width=8, height=8
      解决方法:因为fb关联到了console,等于用echo hello>/dev/tty1都可以显示的。但是屏幕只有背光。CONFIG_FRAMEBUFFER_CONSOLE_DETECT_PRIMARY宏定义在配置中设置为N。先屏蔽此功能。
    3. 屏幕只有背光。
      示波器看波形,发现我设置的spi模式不正确,修改模式后,依然屏幕不亮。打开debug,调试发现console切换。添加了fb设备为什么console会调用,通过修改linux内核配置参数先关闭功能。然后此时spi时钟评率设置为2M,因为我的示波器比较差劲,所以怕频率高采样不准。看波形发送的初始化命令是正确。当然没有全部看,只看了前几个命令值。初始化成功后应该会显示暗屏,难道初始化都没有成功。spi没有pin脚支持回读,调试起来真是困难。
    4. 接下来直接运行app,运行时候调用一次set_addr_win然后就卡住了,我也不知道运行到哪一步了。所以关于deffer io这块是有问题。但是另外一个问题就是初始化寄存器值都是对的为什么屏没有起来呢?
    5. 验证屏幕是否好的
      我特地又去买了块stm32F407ZG的开发板,用来验证屏幕,并且他有接口可以直接调试camera的,学习我还是先挑选简单的。毕竟现在对linux框架已经有一定了解,并且对linux的字符设备驱动也都是比较熟悉了。掌握了一套可以举一反三的方法论。此时的单片机和linux驱动对我来说都一样的。
    6. stm32链接平面后,可以正确显示。并且仅调用初始化的话,可以显示花屏。但是我之前am335驱动连花屏都没有。说明初始化都没有成功,发现示例代码用reset脚拉高拉低来进行第一步平面初始化,我再检查了am335的fb_st7735r链接的fbtft-core也会先控制reset脚。
    7. 难道是io口添加了杜邦线导致电流不够导致屏幕无法驱动?我拿出了电洛铁,将引脚进行焊接,但是焊接后依然屏幕没有启动,只感觉有背光。
    8. 难道是个别时序不对?当然我还可以通过对比时序来解决问题。
      但是我家只有2通道示波器,而且采样时间很短,没有逻辑分析仪。我评估了下fbtft用在linux上的概率比较低,我为了学习framebuffer才去用oled的。所以这款3线spi那么不好用,将来要学习framebuffer还是先用rgb显示屏吧。

    五,总结:

    难得认输一次,对于这款oled将来有条件再玩吧,本次等于第一次接触framebuffer,看了源码学习了下框架及原理。特别是mmap触发page错误然后重新绑定虚拟内存到物理内存,这套逻辑蛮有意思的。

    相关文章

      网友评论

        本文标题:Linux驱动OLED屏st7735s(framebuffer学

        本文链接:https://www.haomeiwen.com/subject/dswdnktx.html