ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

NT35310 RGB屏驱动实战:从白屏到UI切换的完整调试方案

NT35310 RGB屏驱动实战:从白屏到UI切换的完整调试方案 去年底我接手一个项目做一块基于NT35310的LCD模组显示方案。网上中文资料少得可怜驱动IC又冷门第一反应就是查手册、抄寄存器表、反复调白屏。前后折腾了两天多才从纯白屏一步步调到画点、画线、显示中文、切换UI界面。这条路走下来最大的感受是资料越少的屏越要理解底层的工作原理不然初始化序列抄对了也可能翻车。这篇文章就是把这套完整的实战过程整理出来从硬件接线、上电时序、寄存器初始化到画点函数、图形库、UI页面设计一次性讲透。如果你也在跟这种资料少、查起来费劲的LCD打交道照着这篇文章走能少走很多弯路。1. 先搞清楚NT35310是什么拿到屏幕先别急着接线1.1 认识NT35310这颗驱动ICNT35310是联咏Novatek出品的一颗TFT-LCD驱动控制芯片常见于中小尺寸的液晶模组很多工控屏、仪器仪表屏、带触摸的消费类屏幕背后都印着它。和ILI9341、ST7789这些资料一搜一大把的网红IC不一样NT35310的中文资料很少搜索引擎翻来翻去就那么几篇帖子所以很多人一上来就卡在初始化这一关。不过型号冷门不代表它难搞关键是先搞明白它的接口形态。NT35310这颗IC的寄存器多、配置灵活同一颗IC在不同模组上可能引出完全不同的接口有的是RGB并口需要外部控制器持续送像素时序有的走MIPI DSI适合手机、平板这类带DSI控制器的平台。所以你拿到屏之后第一件事不是上网搜NT35310例程而是看你手上这块模组的转接板、排线丝印、背面标签确认它到底是什么接口、什么分辨率。我遇到的这块NT35310屏是RGB并口形态这也是MCU类项目里最典型的一种。RGB并口的NT35310模组屏幕本身没有大容量GRAM整屏图像必须由MCU或专用控制器不断“喂”数据。这个特性决定了它跟自带GRAM的SPI屏在玩法上完全不一样很多人栽跟头就栽在没意识到这个区别上。1.2 RGB接口和自GRAM屏到底差在哪自带GRAM的屏幕比如常见的SPI接口ILI9341内部有一整块显存你通过SPI把整帧图像写进去之后屏幕会自己持续刷新那块显存MCU可以转头去睡觉。这种屏的优点是接线少、时序要求低、随便一个MCU都能跑缺点是刷全屏要算着SPI速度三五百毫秒刷一帧很正常。NT35310这种RGB并口屏则完全不同。它没有给你一个大到能装一整帧画面的GRAM或者说GRAM只是行缓存级别的缓冲显示内容必须由外部控制器通过PCLK像素时钟、HSYNC、VSYNC、DE这些信号实时扫描上去。MCU这边如果停了时钟画面要么闪、要么直接熄灭。这意味着两件事。第一初始化它只是万里长征第一步驱动难点在于持续生成正确的RGB时序第二你得给它配好储存图像数据的“显存”也就是MCU的RAM里要开一块帧缓冲或者外挂一个SDRAM。很多第一次玩RGB屏的人都是在这一点上转不过弯为什么初始化成功了还是黑屏因为你只发了初始化命令根本没往显存里写像素也没有启动时序发生器。好在很多采用RGB并行接口的NT35310模组虽然数据面走RGB但初始化命令仍然可以通过SPI或者8080总线来发这跟LTDC或DMA传输像素数据是两条路。这个“命令通道”和“数据通道”分离的设计是后面所有代码的最关键前提。1.3 拿到屏幕后要确认的5个参数在写第一行代码之前我建议拿一张纸把下面5个参数先记下来全部确认后再动手接线分辨率看屏幕标签或排线丝印常见的有480x272、480x800、480x854等。确认后要记住x和y的方向因为初始化时设置扫描方向不同长宽是能互换的。接口类型是纯RGB接口还是带SPI/8080命令通道的RGB接口还是MIPI DSI。这决定了你用什么外设去驱动。像素格式屏支持RGB565还是RGB666、RGB888。这决定数据线的引脚数量和颜色格式配置。RGB565用16根数据线RGB888要24根差别很大。背光参数看LED灯串是几串几并、额定电流和电压。有些模组自带升压电路直接供5V就能点亮背光有些要靠板外的恒流升压驱动。驱动IC版本NT35310可能有后缀A/B之类的版本差异具体寄存器值会有小差别。看模组背面标签最好搜一下对应版本的驱动例程。这5个参数确认清楚后面的工作全是体力活。特别是第4个背光参数很多人迫不及待接好数据线结果背光没亮就以为屏坏了其实只是背光供电方式没搞对。2. 硬件接线与引脚功能别让初始化死在硬件上2.1 一组引脚弄清楚电源、控制、数据、背光NT35310模组的引脚看起来多但功能分类很清晰。我把常用引脚归成四组接线时按组来理不容易乱。电源类引脚有VCC、IOVCC、GND。VCC一般是面板模拟电源常见2.8V到3.3VIOVCC是接口逻辑电源也常见3.3V。有些模组会把VCC和IOVCC分开引出接线时别拿一根3.3V全怼上去先看丝印旁边的电压标识。如果丝印不清默认都用3.3V供电一般也能工作但芯片温热的话就要查一查是否超压了。控制类引脚最关键的是RESET和DISP。RESET是硬件复位低电平有效必须由MCU控制不能悬空。DISP是显示开关有的屏也叫BLEN或DISPLAY_ON这个引脚控制的是面板显示状态跟背光不是一回事很多人在这一步混淆。DISP拉低屏幕就是黑的拉高显示开启。数据时序类引脚RGB接口的模组常见的有PCLK、HSYNC、VSYNC、DE以及D0到D15到D23这种并行数据线。如果初始化走SPI命令通道还会有SPI_SCL、SPI_SDA、SPI_CS、SPI_DC这几个引脚。背光类引脚一般是LED_A和LED_K或者BL、BL-这部分单独接千万别跟逻辑电源混在一起。另外有个概念值得说明很多这种模组采用GOAGate on Array技术也就是把栅极驱动电路直接做在液晶面板的玻璃基板上所以屏幕边框上少了很多绑定的驱动芯片看起来更简洁。这意味着驱动IC只管源极信号的输出栅极扫描信号由面板内部电路自己转递。搞清楚这个概念有助于理解为什么初始化序列里会有那么多跟时序、功耗相关的寄存器设置因为GOA电路的充电时序需要和源极输出严格配合。2.2 LTDC和GPIO模拟两条路线怎么选RGB并口屏接MCU主流有两条路。第一种是使用带LTDCLCD控制器外设的MCU比如STM32F429、H750、H743这类。LTDC能自己产生PCLK、HSYNC、VSYNC、DE信号你只要把帧缓冲地址和分辨率配好它会按时钟自动把数据推到屏上。这种方式刷新稳定、不耗CPU缺点是占用引脚多而且对MCU型号有要求。第二种是用普通GPIO软件模拟时序配合DMA搬运。这种方式灵活几乎任何MCU都能用但CPU负载大而且时序容易受中断影响。如果是用ESP32、树莓派Pico这类带DMA但没LTDC的芯片可以走这条路但建议把像素时钟调低一点比如把系统频率拉低或者用彩屏刷新不要太频繁不然刷新过程中一卡顿屏幕上就会出现横纹。我这次用的是STM32H750加LTDC的方案。原因是这块模组分辨率达到了480x800GPIO模拟的方式即使DMA拉满也很难稳定维持刷新而且UI有切换动画需求必须把刷新这件事交给硬件外设去做。LTDC配置里需要把像素时钟PCLK设置在屏幕规格书允许的范围内一般是5MHz到20MHz之间分辨率越大越需要高的PCLK。这里提醒一下LTDC的时序参数里HBP、HFP、VBP、VFP不是随便填的最好从模组规格书里的时序图去查填得不对大概率花屏。2.3 背光和供电的坑背光是这块NT35310屏最容易被坑的地方。我第一块屏就是因为背光点不亮误以为模组坏了折腾了半天才发现问题出在供电方式上。很多RGB接口的模组背光是几颗LED串联的灯串工作电压可能是9V、12V甚至更高。模组上不一定有升压电路有些为了散热和灵活设计把升压部分做成了外置。所以你在板上看到LED_A和LED_K引出来的时候先量一量这两个焊盘点之间有没有电压差或者查模组原理图看是不是自带升压。如果没带升压你得加一个小升压恒流电路电流按模组标注来一般25mA到40mA每串。背光亮度调节用PWM这个大家都熟但PWM频率有讲究。我建议至少20kHz以上低了会听到电感或者背光驱动芯片发出的吱吱声而且手机摄像头拍屏幕时会出现明显的水波纹。我实际项目里用的PWM频率是24kHz亮度线性度还不错。另外背光的PWM信号和屏的RESET最好分开控制线调试初期先用固定100%亮度点亮背光排除调光的变量等画面正常了再去做PWM亮度调节。3. 初始化流程拆解一上电为什么是白屏3.1 上电时序与复位时序NT35310这种驱动IC对上电时序很敏感。很多白屏问题不是初始化数组写错而是电源和复位信号的先后顺序不对。规矩的做法是先给逻辑电源VCC和IOVCC上电等电源稳定后再拉高DISP和背光然后对RESET引脚产生一个低电平脉冲。脉冲持续的时间要足够长一般至少10微秒我习惯给1到10毫秒。复位释放后要等驱动IC内部晶振和电源稳定一般延时50到120毫秒。这时候再开始发初始化命令序列。一个反面教训是有人为了省事把RESET直接接到了电源上电复位芯片上让RC电路自己产生复位脉冲。看起来没问题但RC时间常数不匹配导致驱动IC还没有完全初始化就开始收命令结果就是屏幕白屏。所以调试初期RESET一定要用MCU的GPIO控制软件上写清楚上电、复位、延时的顺序。下面这段是我常用的初始化入口函数结构void NT35310_Init(void) { LCD_PWR_Set(1); // VCC 上电 delay_ms(10); // 等待电源稳定 LCD_DISP_Set(0); // 先关闭显示输出 LCD_RST_Clr(); delay_ms(10); LCD_RST_Set(); delay_ms(120); // 等待驱动IC内部就绪 LCD_WriteCmd(0x11); // Sleep Out delay_ms(120); // 这里插入屏厂提供的初始化寄存器序列 NT35310_WriteInitSeq(); LCD_WriteCmd(0x29); // Display ON LCD_DISP_Set(1); delay_ms(50); LCD_BackLight_Set(100); // 最后再开背光 }注意最后一步才开背光。这么做的好处是初始化过程中就算屏显示异常你不会被刺眼的背光晃着排查问题也更容易。而调试中用“先背光后初始化”的做法往往会因为屏幕上没有任何内容让你分不清是背光问题还是驱动问题。3.2 寄存器初始化序列怎么读屏厂或者模组商家给的例程核心就是一长串寄存器初始化数组很多人看不懂就直接抄。我的建议是不要急着全抄先搞明白两类命令的区别。一类是标准命令比如0x11是Sleep Out0x3A是设置像素格式0x36是设置扫描方向0x29是Display ON。这类命令在NT35310的很多同门IC里是通用的因为MIPI DCS标准定义过这些命令码很多面板驱动IC都兼容。标准命令的语义清楚出问题了容易排查。另一类是厂商私有寄存器通常以0xE0到0xFF这段地址为多用来配置伽马曲线、GOA时序、电源电压、VCOM等。这些寄存器值几乎只能靠屏厂给的初始化代码原样照搬想自己调明白很难。我一开始试着只配标准命令跳过了厂商私有序列结果是屏幕亮了但画面整体发白、条纹感严重。最后把屏厂例程里的厂商寄存器段完整补进去画面才正常。初始化数组在工程里最常见的组织方式就是结构体数组这样把命令、数据、延时都装在一起后续增删方便。这里我给出一个实用的结构体定义typedef struct { uint8_t cmd; uint8_t len; uint16_t delay_ms; uint8_t data[16]; } LCD_InitItem; static const LCD_InitItem nt35310_init_seq[] { {0x11, 0, 120, {0}}, // Sleep Out {0x3A, 1, 0, {0x05}}, // RGB565 {0x36, 1, 0, {0x00}}, // 扫描方向 {0x35, 1, 0, {0x00}}, // 使能TE信号可选 // 厂商私有序列从屏厂例程中拷贝长度和延时按原样保留 {0xE0, 5, 0, {0x00, 0x03, 0x09, 0x07, 0x01}}, // ... 省略部分 {0x29, 0, 0, {0}}, // Display ON };这段代码里的厂商序列我是省略的真实项目中你需要把屏厂给的初始化代码完整装进这个结构体。值得留意的是有些屏厂的初始化代码里命令和数据是揉在一个数组里的比如{0xE0, 0x00, 0x03, 0x09, 0x07, 0x01}这样第一个字节是命令后面是数据。你按这个结构体重新整理时一定要把顺序和延时条数核对清楚漏一条都可能出问题。3.3 验证初始化成功的三个办法初始化完了怎么确认屏幕真的工作正常我有三个从易到难的验证办法。第一个办法是读ID。通过命令通道发读ID命令不同IC命令码不一样很多Novatek驱动IC支持0x04等读命令如果能正确读出厂商ID和版本号说明命令通道、时序、复位都通了。我记得第一次成功读出ID的时候心里悬着的石头一下就落地了因为命令链路通了后面的事就都好办。第二个办法是往屏幕填写纯色。初始化完成后先不启用LTDC直接通过命令通道用0x2C命令往指定窗口写数据。比如把整个分辨率窗口设成0x00到最大然后连续写同一颜色值如果屏幕出现了对应颜色说明显示通路已经通了。这个方法尤其适合验证RGB并口屏里“写GRAM”这条链路。第三个办法是接上LTDC后在帧缓冲里填充一个渐变色观察画面是否有明显条纹、是否颜色正确。渐变色比纯色更能暴露数据线顺序问题和时序问题。如果渐变色看起来平滑那基本可以断定初始化已经成功后面的问题多半出在图形层或应用层。4. 图形显示从画一个点到搭出整套图形库4.1 画点函数是一切的基础图形库里所有高级功能画线、矩形、圆、图片、文字最终都会落到画点和批量写像素这两个操作上。所以画点函数质量高不高直接决定了整套图形库的性能。画一个点本质是两件事把显示区域窗口设置成这一个点覆盖的范围然后往这个窗口写一个像素数据。窗口设置通常用0x2A设置列地址、0x2B设置行地址、0x2C开始写入数据这套命令在NT35310这类驱动IC上是通用的。下面是很常见的画点实现void LCD_SetWindow(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1) { LCD_WriteCmd(0x2A); LCD_WriteData(x0 8); LCD_WriteData(x0 0xFF); LCD_WriteData(x1 8); LCD_WriteData(x1 0xFF); LCD_WriteCmd(0x2B); LCD_WriteData(y0 8); LCD_WriteData(y0 0xFF); LCD_WriteData(y1 8); LCD_WriteData(y1 0xFF); LCD_WriteCmd(0x2C); } void LCD_DrawPoint(uint16_t x, uint16_t y, uint16_t color) { if (x LCD_WIDTH || y LCD_HEIGHT) return; LCD_SetWindow(x, y, x, y); LCD_WriteData16(color); }注意两处细节。一是画点函数一定要做边界裁剪不做裁剪时越界坐标会产生不可预料的窗口设置可能导致整屏花掉。二是画点之前设置窗口之后马上写数据窗口不要留到下一次调用再设置因为驱动IC的窗口状态是保持的绘制其他图形时会把这次的小窗口带进去。实际项目里我不建议逐点调用画点函数来画复杂图形。比如画一个800x480的矩形区域如果逐点调用DrawPoint会执行近40万次窗口设置光是命令字节就要发上百万个速度惨不忍睹。正确做法是开一次窗口然后连续写一堆像素。4.2 画线、矩形、圆与填充的工程化写法画线最常用的是Bresenham算法它只用整数加减运算画线速度极快非常适合MCU环境。我之前在STM32上用这个算法画一个任意斜率的线段耗时集中在每次LCD_WriteData16的等待上算法本身几乎可以忽略不计。void LCD_DrawLine(int x1, int y1, int x2, int y2, uint16_t color) { int dx abs(x2 - x1); int dy abs(y2 - y1); int sx (x1 x2) ? 1 : -1; int sy (y1 y2) ? 1 : -1; int err dx - dy; while (1) { LCD_DrawPoint(x1, y1, color); if (x1 x2 y1 y2) break; int e2 2 * err; if (e2 -dy) { err - dy; x1 sx; } if (e2 dx) { err dx; y1 sy; } } }画矩形和填充矩形如果处理得好效率可以非常高。空心矩形本质是四条直线上横线、下横线、左竖线、右竖线。填充矩形则可以开一次窗口把整个矩形区域设置好然后连续写N行像素。比如填充一个100x50的矩形开一次窗口后连续写5000个像素即可比一行一行画再逐像素写快得多。画圆用中点画圆法这里不多展开。我要强调的是画圆、画三角形、画任意多边形这类复杂图形在MCU上不要追求简单粗暴的实现优先考虑是否能用矩形填充代替或者是否只画边框不填充。因为填充算法的运算量在低主频芯片上是很宝贵的资源。4.3 让屏幕显示ASCII字符和中文字库字符显示是把字模数据翻译到屏幕的过程。ASCII英文和数字常用8x16的字库每个字符占16字节。中文字符至少16x16点阵每个汉字占32字节如果字号更大比如24x24就是72字节。我遇到中文显示需求时常被两个问题卡住。第一个问题是取模方式不统一。取模工具有PCtoLCD2002、Img2Lcd等最常用的是“纵向取模、高位在前”。你和你的队友如果用了不同的取模设置显示效果就会上下颠倒、左右翻转或者完全花掉。这个没有仲裁办法只能用同一套设置并且在代码注释里写清楚。第二个问题是中文编码。C语言字符串里中文字符串是以GB2312/GBK编码存储的很多Keil工程默认GBK显示时要把编码转换成字库里的索引。GB2312的区位码计算公式是// str指向中文字符串第一个字节是区码第二个是位码 uint8_t area str[i] - 0xA1; uint8_t pos str[i1] - 0xA1; uint32_t index (area * 94 pos) * 32; // 16x16点阵每字32字节然后把这个索引对应的字模字节取出来依次点亮对应位置的像素。这样显示中文的核心代码并不复杂麻烦在于字库数据怎么存。如果是小容量MCU可以把字库放在外部Flash或者SD卡里如果是大容量MCU直接把GB2312全字库的数组编进固件也行一个16x16全字库大概几百KBH750这种片子完全放得下。我这里给一个简化版的中文显示函数主体void LCD_ShowChinese(uint16_t x, uint16_t y, const uint8_t *str, uint16_t color, uint16_t bg_color) { uint16_t x0 x; while (*str) { if (*str 0x80) { // ASCII字符查8x16字库 x 8; str; } else { // 中文查16x16字库 uint32_t index; index (((str[0] - 0xA1) * 94) (str[1] - 0xA1)) * 32; for (int row 0; row 16; row) { uint8_t lineByte font16[index row * 2]; for (int col 0; col 8; col) { if (lineByte (0x80 col)) { LCD_DrawPoint(x col, y row, color); } } lineByte font16[index row * 2 1]; for (int col 8; col 16; col) { if (lineByte (0x80 (col - 8))) { LCD_DrawPoint(x col, y row, color); } } } x 16; str 2; } } }上面这个版本是逐点画在大分辨率屏上显示大量中文时会偏慢。工程优化版是把目标区域先建成一个离屏缓冲等所有字符都渲染到缓冲里再一次性刷到屏幕。速度能快好几倍。另外一个偷懒做法是用画窗填充代替逐点把一行的8个像素打包成一个字节用0x2C连续写16行数据这样效率也会高很多。4.4 性能优化从每秒几帧到丝滑刷新RGB并口屏的刷新性能很大程度取决于你用什么方式搬运像素数据。先说一个必须建立的意识显示性能的瓶颈不在驱动IC而在数据搬运通道。以480x800分辨率为例RGB565格式一帧数据是4808002768000字节也就是750KB。如果PCLK跑10MHz换算下来刷新一帧约需38ms大约26帧每秒如果PCLK能跑25MHz就能到65帧每秒。看起来不错但如果用GPIO一位一位翻转去模拟时序速度连1MHz都难稳定刷一帧可能要好几百毫秒肉眼可见的卡顿。所以优化第一件事是把像素数据送到LCD的通道做成DMA或者LTDC直驱。LTDC方案里MCU只需要把帧缓冲地址告诉LTDC它自己按PCLK把数据打出去CPU全程不参与像素搬运这是效率最高的方式。第二件事是合理选择像素数据在内存中的存放格式。RGB565里如果你用大端字节序存放那么连续写一个16位像素时要先发高字节再发低字节。这个顺序和屏幕要求的字节序必须一致否则颜色会左右互换具体来说是红蓝通道错乱。第三件事是局部刷新。UI不需要每帧都全屏更新的话尽量设置小窗口只刷新变化区域。比如进度条涨了2像素就只刷新那一条窄带区域而不是整屏重绘。这一点在MCU上提升体感特别明显整屏重绘和局部刷新的流畅度差距是“能看”和“很顺”的区别。5. 从图形到UI设计MCU上做界面的实战思路5.1 MCU UI设计跟手机UI完全不同很多人一听到UI设计下意识就想往手机那套设计规范上靠这在MCU上是大忌。手机UI有强大的GPU、几百MB内存、流畅的触摸事件流而MCU环境的资源可能是内存几百KB、主频几百MHz、屏幕还是RGB并口能维持30帧刷新就已经很吃力。所以MCU的UI设计首先要做减法。一屏只放几个关键信息主色不超过三种控件尽量用矩形和色块来表现减少圆角、阴影、渐变这类需要大量计算的效果。说实话工业屏上那种“白底黑字一个红色状态块一个绿色进度条”的界面反而比花里胡哨的渐变UI更耐看也更容易维护。另外一个重要认知是UI设计在MCU上不只是画图而是内存布局设计。你需要在RAM里规划出帧缓冲再决定是单缓冲还是双缓冲。双缓冲能避免画面撕裂但代价是内存占用翻倍比如480x800的RGB565双缓冲要1.5MB普通MCU的RAM直接爆掉得外挂SDRAM。所以很多MCU UI项目实际用的是单缓冲加局部刷新用“只更新变化区域”来弥补没有双缓冲的缺陷。5.2 页面状态机与事件处理MCU界面的骨架我习惯用一个简单的页面状态机。整个UI由若干页面组成每个页面有自己的绘制函数和按键处理函数。用户按键或者触摸产生事件后状态机根据当前页面分发到对应处理函数必要时跳转到另一个页面。enum { PAGE_HOME, PAGE_SETTINGS, PAGE_STATUS, PAGE_MAX }; typedef struct { void (*init)(void); void (*draw)(void); void (*handle_key)(uint8_t key); } PageHandler; static const PageHandler pages[PAGE_MAX] { {Home_Init, Home_Draw, Home_HandleKey}, {Settings_Init, Settings_Draw, Settings_HandleKey}, {Status_Init, Status_Draw, Status_HandleKey}, }; uint8_t current_page PAGE_HOME; void UI_ProcessKey(uint8_t key) { pages[current_page].handle_key(key); } void UI_DrawPage(uint8_t page) { pages[page].init(); pages[page].draw(); }这个结构的好处是新加页面只需要三步加枚举、实现三个函数、注册到pages数组里。界面逻辑和绘制逻辑分离改动页面内容时不会影响其他页面。页面切换时要注意的是不要一上来就整屏重绘。很多新手写页面切换直接全屏清成白色再画新页面视觉上就是闪一下白屏。我的做法是先判断新旧页面是否有相同的静态背景如果没有差别只重绘差异区域即使需要整屏重绘也是先把背景颜色一次性刷好再叠控件减少中间状态的可见时间。5.3 控件实现进度条、按键、列表MCU上最常用的三个控件是进度条、按键、列表它们的实现其实都是基于矩形填充和局部刷新。进度条最容易被做复杂。最简单可靠的进度条是先画一个外框然后用填充矩形的方式从起始位置开始按百分比填充不同宽度。填充时记得在每次百分比变化后只刷新进度条所在的那一小块窗口而不是全屏。我之前在公司项目里就是靠这个“只刷进度条”的招数把下载进度动画做得非常流畅其实MCU主频只有168MHz屏幕还是RGB并口。按键控件的关键是状态区分。按下和弹起时外观要立刻变化比如按下时填充色变深弹起时恢复浅色。由于按键通常只有几十个像素大小局部刷新成本极低所以按键响应能做到“手指按下画面立刻反馈”。如果按键后要跳到新页面也别在按键按下状态还没恢复时就切页先恢复按键外观再延时几十毫秒做页面切换手感更好。列表控件的实现重点在于滚动。更准确地说是滚动时不要整屏重绘而是算出滚动偏移量后只重绘需要移动和更新的条目。在MCU上我一般不用平滑滚动动画直接用“按键一次跳一项”的整页跳转视觉上更稳定代码也更简单。如果你想做平滑滚动需要把列表区域做成一块离屏缓冲滚动时用DMA把缓冲整体搬到屏幕否则会撕裂。6. 实操心法常见问题与排查速查表6.1 白屏、亮屏无内容、花屏的问题定位我把调试NT35310这类RGB屏时遇到的现象分成三类白屏、亮屏但无内容、花屏。这三类问题的排查方向完全不同。白屏优先查背光和电源。先看背光是否正常亮起如果背光亮但屏幕纯白多半是DISP引脚没有拉高或者初始化序列里的Display ON没执行。如果背光不亮查背光供电和升压电路。这类问题用万用表就能定位不用急着动代码。亮屏但无内容也就是说背光正常、屏幕不是纯白而是黑色但画面什么也不显示优先查RGB数据和时序。确认LTDC有没有启动帧缓冲有没有内容可发。这时候我会先把LTDC层1的背景色设置成红色如果屏幕能显示整片红色说明LTDC配置OK问题在帧缓冲数据或者窗口设置如果屏幕毫无反应问题就在LTDC时序和引脚配置。花屏则复杂一点常见的是PCLK极性反了、HSYNC/VSYNC极性错、DE模式不对、数据线接错位。排查时先确认规格书里PCLK采样沿是上升沿还是下降沿LTDC里的参数要严格匹配。另外数据线的接线顺序非常容易错比如D0和D1焊反颜色就会错乱这时降低帧率看画面是否稳定能帮助区分是数据位错还是时序错。6.2 颜色错乱分析RGB、BGR和字节序颜色错乱这个问题我每次都会被它折磨一遍。常见的三种错乱原因要分开排查。第一种是RGB和BGR顺序反了。驱动IC里一般有寄存器可以切换RGB顺序LTDC也通常有RGB和BGR交换选项。不一致时红色会变蓝色画面蓝红颠倒。解决办法是让驱动IC设置和LTDC设置保持一致通常把颜色交换功能关掉或者两边同时开启。第二种是字节序问题也就是高字节和低字节交换。RGB565的一个像素占两个字节先发高字节还是先发低字节必须和屏幕要求一致。如果顺序反了颜色会从0xF800红色变成0x00F8这样的错位颜色看起来就像整体色相偏移了。这个调试时可以用一个已知颜色值测试往屏幕上写0xF800如果显示红色字节序和RGB顺序都是对的。第三种是数据线权重接反。比如RGB565的高位数据线接到了屏幕的低位引脚上。这种错误最难查因为画面看起来是花花的彩色噪点。排查办法是用纯色测试画面然后对照颜色值变化去反推接线哪几位错了。我遇到过组装厂把8根数据线接错两对的情况最后是拿示波器一根一根对的。6.3 刷新慢、闪烁、残影的解决办法刷新慢的根源通常是窗口设置太碎、像素搬运通道太慢或者逐点绘制函数用得太频繁。优化思路前面说过核心是把大量像素集中成一个窗口来刷且尽量用DMA或LTDC搬运数据。还有一种情况是用了软件SPI的初始化通道来发送大量图像数据这条通道带宽极低只适合发命令不适合搬图像。闪烁最常见的原因是刷新过程中没有做撕裂保护。RGB并口屏的刷新是一个持续过程如果MCU在屏幕正在扫描时修改帧缓冲画面就会出现一条扫描线也就是撕裂。解决撕裂要么做双缓冲在VSYNC信号触发时切换缓冲要么在非显示区域内修改缓冲内容等VSYNC到来后再把内容刷上去。如果驱动IC支持TE引脚Tearing Effect接上并开启TE信号效果会好很多。残影分两种。一种是像素残留这是液晶面板自身的响应时间问题和驱动关系不大试着提高刷新率或者使用过驱动ODOverDrive功能在驱动IC寄存器里一般有相关配置。另一种是软件层面的“残影”实际上是上次刷新窗口没有覆盖到位某些区域沿用了旧的GRAM数据这种情况检查窗口边界计算就能解决。6.4 调试前期建议从最小系统开始最后分享一个调试顺序的建议。不要一上来就把全部功能接好那是排查问题的灾难。先把屏幕接成最小系统MCU供电、RESET控制、SPI命令通道、背光再跑通纯色显示确认OK后再接入RGB接口和LTDC数据线确认画面正常后再加触摸或其他外设。这个过程听起来多花时间实际上是最快的一条路。我第一次做这块NT35310屏时直接把所有线接齐结果白屏又怀疑时序又怀疑接线最后花了一天多才发现是其中一根数据线虚焊。后来我学乖了永远从最小系统开始每一步验证清楚再往下一步走。很多看起来玄学的显示问题最后查出来都是接触不良、电压不稳、初始化序列漏了延时这类低级原因而最小系统法能在第一时间把它们暴露出来。这块屏稳定点亮之后我又把同样的流程跑了一遍NT35510的模组发现原理几乎一致只是寄存器厂商段不一样。这也是我一直坚持“吃透原理比抄资料更重要”的原因。资料少的屏多了去了但只要你手里有数据手册、有示波器再加上这套“确认接口-理清接线-初始化-验证-叠加功能”的方法再冷门的LCD也能顺手调出来。
返回列表