首页 / 资讯中心 / 文章详情

LiteOS驱动RGB888 OLED:双系统显示外设归属与DCLK参数详解

LiteOS驱动RGB888 OLED:双系统显示外设归属与DCLK参数详解 ★ FEATURED ARTICLE
做到系列第7篇前面的内容基本把HI3556V200上Linux和LiteOS双系统怎么启动、两个核之间怎么通信、内存视图怎么划分都讲完了剩下的都是硬骨头。这篇换换口味聊一个实际得不能再实际的需求在LiteOS这一侧直接驱动一块RGB888接口的OLED显示屏。很多刚接触双系统的人会有个固有印象显示这种活不就应该交给Linux吗LiteOS摆弄点实时控制、收收传感器数据就完事了。但实际上当你真正做产品的时候Linux核心可能已经忙着跑网络协议栈、跑业务逻辑、跑各种中间件这时候如果UI刷新或者仪表显示还要来抢占Linux的调度资源体验会非常差。把显示屏这件事从Linux侧剥离出来放到LiteOS侧去单独管反而是更合理的设计。这篇就把我在HI3556V200上把RGB888 OLED对接起来的过程、踩过的坑、算过的参数完整记录下来算是给这个系列补上一块重要的拼图。如果你正在做双系统项目或者正打算在LiteOS这种轻量RTOS里驱动一块RGB并口屏这篇文章应该能帮你省下两个星期的摸索时间。里面关于DCLK计算、双缓冲设计、VOP寄存器配置的思路换到其他芯片上也一样能套用。1. 项目背景与整体设计1.1 这块屏为什么要归LiteOS管HI3556V200的双系统部署方式通常是一个核心跑Linux做主控另一个核心跑LiteOS做实时业务。AMP架构下两个系统各有各的地址空间、各有各的外设访问权限但显示控制器这类外设是物理上共享的必须明确划给某一方。这次项目里Linux侧要处理网络推流、设备管理、文件存储负载已经很重。而显示屏上要实时刷新的内容是设备运行状态、传感器读数、调试日志这类变化频繁但逻辑简单的信息。如果把这些刷新丢给Linux一个用户态进程要经过系统调用、驱动层、帧缓冲映射刷新链路长一旦CPU忙起来还可能出现显示卡顿。放进LiteOS之后刷新逻辑变成裸机级别的循环任务想什么时候刷就什么时候刷延迟完全可以控制在微秒级。另外还有一个更深层的原因LiteOS跑在AMP架构的副核上时它几乎没有文件系统、没有网络协议栈跑起来非常轻理论上很适合做“显示从机”这种职责单一的角色。相当于你把一个复杂系统的显示模块单独拆出来交给一个专门的“小前台”主系统只负责通过核间通信把要显示的数据丢过来显示这件事再也不用来回折腾。1.2 接口与方案选型为什么盯着RGB888不放市面上小尺寸OLED屏接口五花八门SPI、DBI、RGB并口都有。SPI屏驱动简单但刷屏带宽太低一个320×240的屏刷一帧就要传好几Mbit的数据中低频率下掉帧明显。DBI并口通常走MCU模式适合小屏低分辨率。这次选的屏是RGB并口而且是24位色深的RGB888。RGB888接口意味着每像素用24bit表示数据线一共要接24根D0~D23再加上DCLK、HSYNC、VSYNC、DE这些同步信号接线看起来吓人但换来的是16.7M色和真正的视频流式刷新。OLED屏本身色彩艳丽RGB888能发挥出它的显示优势不会出现RGB565那种明显的色阶断层。至于为什么不干脆用RGB565省带宽原因有三项目UI里有大面积的渐变背景和实时曲线RGB565的6bit绿色通道很难看渐变色带肉眼可见HI3556V200的VO模块本身就有RGB888输出模式直接按24bit配置并不会增加太多软件复杂度实测下来DDR带宽完全够用没必要为了省这点带宽牺牲显示质量1.3 整个对接工作的三层拆解把整个任务拆开看其实就三层底层是VO外设的寄存器配置中间层是LiteOS的驱动框架集成上层是显示内容的组织与刷新策略。底层这块HI3556V200的VO外设支持多种输出模式我们要把它从默认的MIPI或BT.1120模式切成RGB888并口模式配置好时钟源、分频系数、行场同步极性和时序参数。这一层如果只靠Linux下的现成驱动其实是帮你藏起来了现在在LiteOS里没有那些封装必须对着寄存器手册一个一个写。中间层是LiteOS侧要有一个像样的显示驱动模块不能裸到直接在主循环里操作寄存器。我参考Linux的简单Framebuffer思想在LiteOS里也做了一个极简的帧缓冲抽象上层申请几块DMA缓冲区底层把申请到的物理地址配置给VO作为显示源地址刷新的时候切换源地址即可。这样内核里的业务逻辑只需要关心要显示的数据长什么样不用关心VO怎么把数据搬运出去。上层则是双缓冲和局部刷新策略。这一层看着简单实际直接决定了显示会不会撕裂、会不会闪屏。2. LiteOS侧显示驱动框架搭建2.1 LiteOS环境下驱动的写法习惯如果你之前在Linux里写过字符设备驱动换到LiteOS会有一个明显感受框架概念变少了。LiteOS下面没有platform_driver、没有of_match_table驱动更像是一堆功能函数的集合再通过一个简单的ops结构体暴露给业务层。我习惯先定义一个lcd_ops结构体把操作屏幕的接口统一收口typedef struct { int32_t (*init)(lcd_info_t *lcd); int32_t (*deinit)(lcd_info_t *lcd); int32_t (*set_buffer)(lcd_info_t *lcd, uint32_t paddr); int32_t (*flush)(lcd_info_t *lcd); void (*show_rgb888)(lcd_info_t *lcd, uint16_t x, uint16_t y, uint16_t w, uint16_t h, void *buf); } lcd_ops_t;LCD屏本身的差异初始化序列、扫描方向、Gamma全部收敛到init函数里LiteOS业务侧只需要调用show_rgb888就能把画面刷出去。这种设计的好处是换屏的时候业务代码一行不用动坏处是驱动里的细节全得自己看手册抠没有现成轮子。挂载方式也别搞太复杂LiteOS环境下用全局指针注册驱动实例就够了。我在系统初始化阶段直接调用一次lcd_device_register把上面这个ops结构体挂到全局链表上后续任务通过lcd_get_device拿到驱动句柄。相比Linux那套设备树匹配这种做法在RTOS场景下反而更直接出问题也好查。2.2 显存与DMA搬运的设计LiteOS侧没有MMU带来的虚拟地址概念驱动里用的都是物理地址。这一点很重要因为VO外设DMA搬运的时候要的是物理地址。我在系统内存划分阶段就预留了两块DMA缓冲区每块大小按屏幕分辨率×3字节计算并保证32字节对齐。/* 480x272 RGB888 buffer */ #define LCD_W 480 #define LCD_H 272 #define LCD_PIXEL_BYTES 3 #define LCD_BUF_SIZE (LCD_W * LCD_H * LCD_PIXEL_BYTES) /* 约375KB */ static uint8_t lcd_buffer0[LCD_BUF_SIZE] __attribute__((aligned(32))); static uint8_t lcd_buffer1[LCD_BUF_SIZE] __attribute__((aligned(32)));为什么强调32字节对齐因为HI3556V200的VO DMA模块在做突发传输时对源地址的burst对齐有要求。不对齐的情况下可能偶尔出现画面某几个像素颜色错乱而且这种问题非常难查不是必现的拿示波器也未必看得出来。搬运方式我最后选了DMA搬运而不是CPU逐像素拷贝。480×272的RGB888一帧是375KB如果用CPU去memcpy虽然也不慢但会白白占用LiteOS这个核的整数周期而DMA搬运基本不占CPU把源地址和目标地址配置好之后等着完成中断就行。实测下来DMA的方式比CPU拷贝能省出约几百微秒的任务执行时间对实时任务调度非常友好。2.3 屏幕初始化序列与上电时序RGB888 OLED屏首次上电不能直接刷数据要先按要求把电源和复位时序走完。我踩过最狠的一次坑就是上电后立刻初始化命令导致屏一直灰屏无响应后来排查发现是复位脉宽不够。一般正常的流程是先给屏的VCI和VDDIO供电然后拉高复位引脚等待10ms以上再拉低复位引脚保持至少10ms最后拉高释放。释放之后还要等120ms左右让屏幕内部稳压完成才能发送初始化命令。这个时序不同驱动IC要求略有差异但大方向固定。我建议写一个带延时的初始化函数并且把延时参数做成宏方便换屏时统一调整#define LCD_RESET_HIGH_DELAY 10 /* ms */ #define LCD_RESET_LOW_DELAY 10 /* ms */ #define LCD_RESET_RELEASE_DELAY 120 /* ms */ static void lcd_power_on_sequence(void) { lcd_gpio_set(LCD_PWR_EN, 1); LOS_Mdelay(10); lcd_gpio_set(LCD_RESET, 1); LOS_Mdelay(LCD_RESET_HIGH_DELAY); lcd_gpio_set(LCD_RESET, 0); LOS_Mdelay(LCD_RESET_LOW_DELAY); lcd_gpio_set(LCD_RESET, 1); LOS_Mdelay(LCD_RESET_RELEASE_DELAY); }初始化命令序列按屏幕控制器手册配置基本都是SPI/并口命令写寄存器常见的就是退出睡眠模式、设置像素格式、设置伽马曲线、开启显示。需要注意有的屏默认像素格式是RGB666如果不显式设置成RGB888最后显示出来颜色会偏淡这是一个典型的隐蔽坑。3. 关键参数计算与RGB888刷屏实现3.1 DCLK时钟频率算清楚RGB并口屏对时钟信号的要求是严格的DCLK太高屏跟不上太低刷新率不够会闪。算DCLK的公式不复杂DCLK 行总周期 × 帧总周期 × 刷新率行总周期不是单纯的屏幕宽度而要加上水平消隐时间HFP、HBP、HSYNC宽度帧总周期也要加上垂直消隐时间VFP、VBP、VSYNC宽度。以这次480×272的屏为例我查到的时序参数大致如下参数数值含义屏幕宽度480有效显示列数HFP12行前肩HBP12行后肩HSYNC脉宽16行同步宽度屏幕高度272有效显示行数VFP4帧前肩VBP4帧后肩VSYNC脉宽4帧同步宽度刷新率60Hz目标帧率那么行总周期 480 12 12 16 520帧总周期 272 4 4 4 284DCLK 520 × 284 × 60 ≈ 8.86MHz。实际配置的时候我留了余量把DCLK定到9MHz左右。为什么留余量因为如果时钟源分频做不到恰好8.86MHz稍微偏高一点问题不大但偏低太多就会导致帧率下降肉眼可见地闪烁。HI3556V200的时钟分频器通常能通过寄存器输出任意频率但一定要在手册支持的范围内选择分频系数不要想当然地给个值就完事。3.2 双缓冲解决撕裂问题RGB并口屏幕是连续扫描刷新的如果一边让VO从buffer0读数据另一边往buffer0里写新一帧画面上就会出现撕裂——上半屏是新内容下半屏还是旧内容那条分界线还会上下跳动。这个问题在视频场景尤其明显。解决办法就是双缓冲加VSYNC同步。我维护了一个前台缓冲索引凡是VO当前正在读取的buffer叫“前台”另一个应用正在写入的叫“后台”。每次准备刷新时只写后台等VSYNC中断来的一瞬间交换前后台指针之后再让VO从新的前台地址继续读。伪代码大致是这样volatile uint32_t front_buf_idx 0; volatile uint32_t frame_updating 0; /* VSYNC中断处理函数 */ void vo_vsync_irq_handler(void) { if (frame_updating) { front_buf_idx ^ 1; /* 交换前后台 */ vo_set_display_addr(lcd_buffers[front_buf_idx]); frame_updating 0; } } int32_t lcd_flush(uint8_t *new_content) { uint32_t back_idx front_buf_idx ^ 1; memcpy(lcd_buffers[back_idx], new_content, LCD_BUF_SIZE); frame_updating 1; /* 标记有更新等下个VSYNC切换 */ return 0; }这套逻辑的核心是不怕应用层写得多慢只要写的时候不碰到前台就行。VSYNC中断里只做指针交换不做任何可能阻塞的操作。按照LiteOS的编码习惯中断服务函数里连LOS_Mdelay都不能用所以一定要把耗时的操作全部放到任务上下文。3.3 帧率与带宽的平衡480×27260Hz、RGB888的裸带宽是480×272×3×60 ≈ 23.5MB/s这个数字对DDR来说完全不是问题但加上VO本身从显存读数据的开销、LiteOS其他任务的DMA访问总带宽还是要心里有数。我实测跑起来系统整体带宽占用率大概在5%左右对业务没有丝毫影响。不过如果刷新率提到90Hz甚至更高带宽就会逼近35MB/s。这个场景下我就建议改用局部刷新策略了。UI大部分区域其实是不变的没必要每次都整帧搬运。可以把刷新区域收敛成一个矩形列表只有当矩形区域的内容变化时才更新。LiteOS侧实现一个简化的脏矩形模块并不复杂关键是刷新命令变成了区域坐标加区域数据VO层的DMA搬运地址也相应调整。实际产品如果对功耗敏感这个优化能少搬不少数据。4. 实测中的坑与排查技巧4.1 黑屏先查上电时序再查寄存器黑屏是最麻烦的故障因为完全没反馈。我调试时建议大家按顺序排查不要一上来就翻寄存器。第一确认屏的电源引脚的电压确实到了用万用表量一下VCI和VDDIO。第二确认复位时序符合手册很多屏要求复位释放后至少等几十毫秒才能发命令。第三确认DCLK引脚上有波形用示波器看时钟频率是否符合预期。如果DCLK都没有问题一定在VO时钟配置如果DCLK有但屏幕还是黑再回头查初始化命令序列是否完整、命令是否真的写进去了。我遇到过一次比较隐蔽的情况初始化命令已经正常发出但屏幕依然是灰蒙蒙的仔细看像是所有像素都变成了一种极低亮度的灰度。后来发现是像素格式寄存器没设置好屏幕拿RGB888数据按RGB666解析低两位被丢掉后颜色严重偏差。所以初始化命令里像素格式设置那几步一定不能省。4.2 花屏优先怀疑DMA对齐与同步极性花屏和黑屏不一样说明数据通路基本通了但某个环节出了问题。最常见的两个原因一个是DMA源地址没有按要求的边界对齐另一个是VO输出的同步信号极性和屏幕控制器期望的不匹配。对齐问题前面提过把缓冲区定义成aligned(32)基本能解决。同步极性问题就要拿示波器或逻辑分析仪对比HSYNC、VSYNC、DE在有效数据期间的极性关系了。很多时候屏手册会画一张时序图明确标注HSYNC高有效还是低有效、VSYNC高有效还是低有效。VO模块的寄存器里通常都有极性配置位把默认值和屏要求的不一致改过来就好。4.3 偏色用三基色测试条快速定位RGB888数据线有24根焊接或者排线时稍微错一根就会导致偏色。为了快速定位是哪根线接错我写了一个最简单的测试程序全屏刷纯红色、纯绿色、纯蓝色每次只让一种颜色分量有值。正常情况下屏上就应该显示对应颜色如果刷红色显示出来的是黄色说明绿色分量通道上串进了红色数据。这个测试小工具看起来土但实际排查效率极高。我甚至建议以后每个项目都保留这个测试代码屏幕上电后第一步先刷三基色确认接线没问题再去做业务显示。4.4 Linux与LiteOS争抢VO外设的问题双系统项目还有一个特别容易踩的坑Linux启动的时候也会去初始化VO外设。如果Linux的设备树里把VO节点使能了Linux驱动会在内核启动阶段往VO寄存器写入它的配置等LiteOS侧再去设置VO时两个系统写出的寄存器值可能互相覆盖最终屏幕状态谁也说不清楚。解决办法是Linux侧把VO节点在设备树里标记为disabled或passthrough让Linux内核完全不去触碰这个外设把VO的占有权明确交给LiteOS。这一步必须在项目初期就定好否则后面联调的时候两边互相踩脚排查起来非常痛苦。另外还要注意物理地址映射问题。LiteOS访问VO寄存器时用的物理地址必须和Linux侧看到的寄存器基地址一致但LiteOS端没有设备树地址只能做成宏定义放在共享头文件里两个系统共用同一个头文件才能保证不会各写一套地址。RGB888 OLED对接这件事做完了回头看难点反而不在代码量而在外围细节时钟参数算清楚了没有、时序有没有偏差、两个系统的外设归属定好没有。只要这些地基打牢LiteOS侧跑一个稳定的显示驱动是很自然的结果。我整个调试过程最深的感受是别急着改代码先拿示波器把时钟和同步信号摸清楚能少走很多弯路。
阅读完成 · 觉得有帮助?
咨询建站