简介:围绕STM32F767驱动LTDC RGB液晶屏的完整工程资料,面向已具备STM32基础、希望深入掌握LTDC显示控制器与RGB屏接口的嵌入式开发者及高校学习者。内容覆盖GPIO引脚复用与时钟使能、LCD时序参数配置、多层混合和层属性管理、帧缓冲区分配、颜色格式匹配、DMA传输、中断处理及显示更新等关键环节,可用于图形界面、工业HMI等场景的学习、调试与移植参考。压缩包共182个文件,以92个.h头文件和81个.c源文件为主体,包含HAL库外设驱动与LTDC相关实现,并配有Keil工程文件、启动汇编、hex输出、批处理脚本和说明文本,整体约1.12MB,目录便于按模块查阅。已有1693人学习下载,适合作为RGB屏点亮、时序调试和工程搭建的对照材料,帮助读者理解从底层寄存器配置到显示内容更新的完整实现思路及排错思路。
1. 一块 800×480 的 RGB 屏点不亮,问题往往不在屏上
屏是新的,FPC 排线也压好了,背光一亮,屏幕白得像张纸——这是很多人第一次用 STM32F767 驱动 RGB 屏时看到的画面。这块屏上没有任何初始化指令可发,它不像 SPI 屏那样要写一长串寄存器序列才能出图,它只认一件事:你有没有把像素数据按正确的节奏推过去。STM32F767 片内的 LTDC 就是干这个的控制器,它把外部 SDRAM 里的一块内存当成整幅画面,用像素时钟一个点一个点往外送,CPU 只在换帧时改一下起始地址。这套方案适合做工业 HMI、仪表盘、摄像头实时预览这类需要整屏连续刷新、又不愿意让 CPU 一直搬数据的场景。真正难的地方从来不是写初始化代码,而是时序参数、显存带宽和 Cache 这三件事,任何一件没对齐,屏幕给你的反馈都是同一个:黑屏或者花屏。
2. 时序先立住:七个参数决定你能不能看见第一帧
RGB 屏和 MCU 屏最大的区别在于,它没有“驱动 IC 初始化”这个环节,屏内部的时序控制器是纯硬件,上电就开始等信号。所以你给的 DE、DCLK、HS、VS 必须和面板手册里写的一模一样,差一个时钟周期都可能整屏错位。这一章把手册上的 Timing 表翻译成 LTDC 的寄存器值,并且把像素时钟算出来,让第一帧有出现的物理基础。
2.1 把面板手册翻到 Timing 表,只抄七个数
拿到一块 800×480 的屏,手册里一定有一张标着 “Timing Characteristics” 或者 “AC Characteristics” 的表,里面动辄二三十行,但你真正要的只有七个:HSW(行同步宽度)、HBP(行同步到有效像素的前肩)、HFP(有效像素到下一个行同步的后肩)、VSW、VBP、VFP,以及 DCLK 频率。前六个的单位是时钟周期数,不是时间,别拿 ns 去填。
| 参数 | 本屏典型值 | 含义 | 配错之后你会看到什么 |
|---|---|---|---|
| HSW | 48 | 一行开始的同步脉冲宽度 | 图像整体左右抖动,或左边缺一条 |
| HBP | 88 | 同步结束到第一个有效像素 | 左侧出现竖向彩条或黑边 |
| HFP | 40 | 最后一个有效像素到下一行同步 | 右侧出现重复列或亮线 |
| VSW | 3 | 一场开始的同步脉冲行数 | 图像上下滚动、场不同步 |
| VBP | 32 | 场同步结束到第一行有效像素 | 顶部出现横条 |
| VFP | 13 | 最后一行有效像素到场同步 | 底部出现横条 |
| DCLK | 33.3MHz | 像素时钟 | 偏低闪屏,偏高直接不出图 |
抄表的时候只信 “Typ” 那一列,Min/Max 是给做时序裕量分析用的,不是给你配寄存器的。手册里同时会给 HS、VS、DE 的有效电平,这个必须一起记下来,它决定后面HSPolarity这几个字段填_AL还是_AH。我见过最冤的一次翻车,是有人把 DE 的有效电平抄反了,屏幕能亮,但整幅图被压到左边三分之一,查了两天才发现是极性。
2.2 用 PLLSAI 把像素时钟配到 33.33MHz
STM32F767 的 LTDC 时钟源是固定的,只能来自 PLLSAI 的 R 分频,再经过 PLLSAIDIVR 二次分频,不能像 SPI 那样随便挑时钟源。这条链路上有三个约束必须同时满足:VCO 输出要在 100~432MHz 之间,R 只能取 2~7,PLLSAIDIVR 只能取 2、4、8、16。常见的 25MHz 晶振板子,最顺的组合是 M=25 得到 1MHz 参考,N=400 得到 400MHz VCO,R=6 得到 66.67MHz,再二分频得到 33.33MHz。
/* 25MHz HSE / M(25) = 1MHz -> x N(400) = 400MHz VCO * 400MHz / R(6) = 66.67MHz -> / PLLSAIDIVR(2) = 33.33MHz */ static void LCD_Clock_Config(void) { RCC_PeriphCLKInitTypeDef pclk = {0}; pclk.PeriphClockSelection = RCC_PERIPHCLK_LTDC; pclk.PLLSAI.PLLSAIN = 400; /* VCO 必须落在 100~432MHz */ pclk.PLLSAI.PLLSAIR = 6; /* R 只能取 2~7,填 8 会被静默截断 */ pclk.PLLSAIDivR = RCC_PLLSAIDIVR_2; /* 只能取 2 / 4 / 8 / 16 */ if (HAL_RCCEx_PeriphCLKConfig(&pclk) != HAL_OK) { Error_Handler(); /* 卡在这里基本都是 VCO 越界,RCC 直接拒绝配置 */ } __HAL_RCC_LTDC_CLK_ENABLE(); }这段代码里最容易出问题的是Error_Handler()真的被触发,而很多人把它写成一个空死循环,于是现象就是“程序跑飞了”,看不出是时钟配错。我的习惯是在这里面点一个 LED 或者打印一行字符串,把“配置被拒绝”和“配置成功但屏不亮”这两种情况彻底区分开,否则后面所有排查都建立在一个错误的前提上。
配完之后可以反算一下实际刷新率:总行数 976、总场数 528,33.33MHz ÷ (976×528) ≈ 64.7Hz。这个值比 60Hz 略高,在绝大多数 60Hz 面板的容忍范围内,但如果你的屏手册明确写了 “Frame Rate: 60Hz ±1”,那就得回头去调 HFP 或者 VFP 把总量凑到 33.33e6/60/976 ≈ 569 行,多出来的 41 行全加到 VFP 上。
2.3 数据线接法:RGB565 到底接高位还是低位
引脚这边,F767 的 LTDC 全部复用在同一组 AF14 上,LTDC_R[7:0]、LTDC_G[7:0]、LTDC_B[7:0]加上 HSYNC/VSYNC/DE/DCLK 一共 28 根线。如果你只跑 RGB565,需要 16 根数据线,这里有个必须点明的问题:面板的 R 通道有 8 根输入线,你只送 5 位数据,该接R[7:3]还是R[4:0]?
答案是接高位R[7:3]、G[7:2]、B[7:3]。原因在于 LTDC 输出的是左对齐的,它把 5 位红数据放在 R[7:3] 上,低三位一直是 0。如果布线时图省事接到了低位,你看到的画面会是整体发暗、灰阶严重缺失,而且怎么调LxCACR的 Alpha 都没用,因为它根本不是亮度问题,是数据位错位。硬件定版之前,拿万用表和原理图对一遍这 16 根线的顺序,比事后改板便宜太多。
DE 信号必须接,这点没有商量余地。HS 和 VS 在纯 DE 模式下面板可以不用,但建议一并接上,因为示波器排查的时候,有 HS 波形你才能确认 DCLK 数目对不对。
3. 把 LTDC 配置正确:累加寄存器、图层窗口和一行多少字节
时钟有了,接下来是控制器本体。LTDC 的寄存器分成两大部分:一部分是全局的时序累加寄存器,它告诉控制器“一行有多长、有效区从哪开始”;另一部分是图层寄存器,它告诉控制器“这块显存在哪、什么格式、怎么和背景混合”。这两部分的坑完全不一样,前者错一点整屏偏,后者错一点花屏或者全黑。
3.1 四个累加寄存器为什么都要减一
LTDC 的时序不是让你分别填 HSW、HBP、HFP,而是填四个“累加位置”:SSCR是行同步宽度,BPCR是同步加前肩的累计值,AWCR是到有效区结束的累计值,TWCR是一整行的累计值。这四个寄存器在硬件里都是“写到第几个时钟”,而计数从 0 开始,所以填进去的每一个值都要比实际周期数少 1。
换算方式很固定:HorizontalSync = HSW - 1,AccumulatedHBP = HSW + HBP - 1,AccumulatedActiveW = HSW + HBP + 宽 - 1,TotalWidth = HSW + HBP + 宽 + HFP - 1。垂直方向同理。我见过最典型的翻车是只给前两个减了 1,后面两个忘了,结果画面整体右移一个像素,右边多出一条竖线,因为总行宽算多了。这一个像素的偏差在纯色测试图上看不出来,一上真实 UI 就露馅。
另外,HAL 里这几个字段名虽然叫AccumulatedActiveW,但它对应的就是LTDC_AWCR,不要被名字误导成“有效宽度”。
3.2 图层窗口、像素格式与 CFBLR 里那个 +3
图层这边,LxWHPCR/LxWVPCR定义窗口,注意窗口的结束坐标是开区间,WindowX1 = 800表示覆盖 0~799,HAL 内部会自己减 1。LxPFCR定像素格式,RGB565 填LTDC_PIXEL_FORMAT_RGB565。真正容易出问题的是LxCFBLR,这个寄存器同时编码了两个东西:低 13 位是行间距(pitch,单位字节),高 13 位是行长度加 3。
加 3 不是笔误,手册里就是这么定义的。以 800 宽的 RGB565 为例,一行 1600 字节,pitch 也是 1600,那么寄存器值应该是((1600 + 3) << 16) | 1600。如果按直觉写成((1600) << 16) | 1600,控制器会认为每行少 3 个字节,于是第二行开始整体左移一个半像素,画面表现为斜向的彩色条纹——这个现象特别有辨识度,看到斜条纹基本可以直接去查这个 +3。
用 HAL 的话这两个值它内部会算,但你得保证传给它的ImageWidth是真实行宽,不是窗口宽度。窗口比图像小的时候(比如做个局部刷新),这两个值必须分开设,否则照样花屏。
3.3 显存放外部 SDRAM:一帧 750KB 塞不进片内
800×480 的 RGB565 一帧是 800×480×2 = 750000 字节,大概 732KB。STM32F767 片内 SRAM 总量 512KB,其中能连续使用的一大块 AXI SRAM 只有 368KB,还经常被以太网描述符、USB 缓冲占掉一部分。结论很直接:帧缓冲必须放外部 SDRAM,通过 FMC 挂在0xC0000000起始的地址空间上。
外部 SDRAM 的初始化本身是另一套东西,这里只说和显示相关的三个约束。第一,位宽要配成 16 位或 32 位,和硬件一致,配错了读写全是错位数据。第二,刷新计数要按 SDRAM 颗数和使用的主频算准,刷新率不够会随机丢数据,现象是屏幕偶尔闪一下白。第三,两帧缓冲之间要留对齐,一般按 1MB 边界对齐,方便后面配 MPU 区域。我一般的做法是第一帧放0xC0000000,第二帧放0xC0100000,中间空着不用,浪费一点换省心。
3.4 一份能点亮的最小初始化代码
把前面几块拼起来,就是下面这段。参数用的是本章开头那张表的典型值,换成你自己的屏只需要改宏定义。
LTDC_HandleTypeDef hltdc; #define HSW 48 #define HBP 88 #define HFP 40 #define VSW 3 #define VBP 32 #define VFP 13 #define LCD_W 800 #define LCD_H 480 void LCD_LTDC_Init(uint32_t fb_addr) { hltdc.Instance = LTDC; /* 极性按面板手册抄,抄反的表现是图像整体偏移,不是黑屏 */ hltdc.Init.HSPolarity = LTDC_HSPOLARITY_AL; hltdc.Init.VSPolarity = LTDC_VSPOLARITY_AL; hltdc.Init.DEPolarity = LTDC_DEPOLARITY_AL; hltdc.Init.PCPolarity = LTDC_PCPOLARITY_IPC; /* 上升沿采样,绝大多数屏是这个 */ /* 四个累加寄存器写的都是“位置 - 1”,漏减就会整体偏一个像素 */ hltdc.Init.HorizontalSync = HSW - 1; hltdc.Init.VerticalSync = VSW - 1; hltdc.Init.AccumulatedHBP = HSW + HBP - 1; hltdc.Init.AccumulatedVBP = VSW + VBP - 1; hltdc.Init.AccumulatedActiveW = HSW + HBP + LCD_W - 1; hltdc.Init.AccumulatedActiveH = VSW + VBP + LCD_H - 1; hltdc.Init.TotalWidth = HSW + HBP + LCD_W + HFP - 1; hltdc.Init.TotalHeight = VSW + VBP + LCD_H + VFP - 1; hltdc.Init.Backcolor.Red = 0; hltdc.Init.Backcolor.Green = 0; hltdc.Init.Backcolor.Blue = 0; HAL_LTDC_Init(&hltdc); /* 这个函数末尾会把 LTDCEN 置位,控制器开始扫描 */ /* 图层 0:整屏一个 RGB565 缓冲,不透明 */ LTDC_LayerCfgTypeDef lay = {0}; lay.WindowX0 = 0; lay.WindowX1 = LCD_W; lay.WindowY0 = 0; lay.WindowY1 = LCD_H; lay.PixelFormat = LTDC_PIXEL_FORMAT_RGB565; lay.Alpha = 255; /* 常量 Alpha,255 表示完全不透明 */ lay.Alpha0 = 0; lay.BlendingFactor1 = LTDC_BLENDING_FACTOR1_CA; lay.BlendingFactor2 = LTDC_BLENDING_FACTOR2_CA; lay.FBStartAdress = fb_addr; lay.ImageWidth = LCD_W; /* 行宽,不是窗口宽 */ lay.ImageHeight = LCD_H; lay.Backcolor.Red = 0; lay.Backcolor.Green = 0; lay.Backcolor.Blue = 0; HAL_LTDC_ConfigLayer(&hltdc, &lay, 0); /* 0 表示图层 1 */ }HL_LTDC_Init之后控制器立刻开始扫描显存,所以调用之前fb_addr那块内存必须已经可读,不能等到后面才去初始化 SDRAM。这个顺序反了的现象是:程序跑到这里直接进 HardFault,因为访问了未配置的 FMC 地址空间。
BlendingFactor1/2都填CA是为了让常量 Alpha 生效。如果只用一个图层,其实填什么都不影响,但一旦要叠 OSD 或者半透明菜单,这两个字段就必须是CA,否则 Alpha 值根本不起作用,你会以为是LxCACR寄存器坏了。
4. 避坑与排查:黑屏、花屏、偏移、撕裂的现场记录
这一章的五条都是我在不同板子上真实踩过的,按“看到什么 → 为什么 → 怎么修”组织。排查顺序建议从上往下,因为上一条的现象会掩盖下一条。
4.1 背光亮、屏全黑:先量 DCLK 有没有波形
现象:上电后背光正常,屏幕纯黑,偶尔能看到一点点微弱的横纹。
原因:九成是 LTDC 根本没输出。常见的有三种:PLLSAI 没使能成功(RCC->CR的PLLSAIRDY一直是 0)、LTDC->GCR的LTDCEN位没置起来、或者LxCR的图层使能位没开。还有一种比较隐蔽的,是FBStartAdress指到了没初始化的内存,控制器读回来的全是 0,而 RGB565 的 0 就是纯黑。
解决:拿示波器先量 DCLK 引脚,有 33MHz 左右的方波说明时钟链路没问题,直接去查LTDC->GCR和LTDC->LxCR的寄存器值;没有波形就回头查 PLLSAI 配置。没有示波器的话,可以在初始化后把RCC->CR、LTDC->GCR、LTDC->L1CR三个值读出来打个断点看,比猜快得多。
4.2 花屏且带斜向条纹:CFBLR 少加了 3
现象:屏幕有图像,但整个画面是斜的彩色条纹,能隐约看出内容轮廓。
原因:LxCFBLR的行长度字段少加了 3,控制器认为每行比实际短 3 个字节,第二行开始就错位,累积下去就成了斜纹。用 HAL 时如果手动改过ImageWidth或者绕过 HAL 直接写寄存器,很容易漏。
解决:按((行字节数 + 3) << 16) | 行字节数重算一遍。800 宽 RGB565 就是(1603 << 16) | 1600。这个现象辨识度极高,看到斜纹不用查别的。
4.3 图像整体右移、右边多一条:累加寄存器多减了一次
现象:画面内容完整,但整体右移一两个像素,最右边出现一条和左边重复的竖条。
原因:AccumulatedActiveW或TotalWidth被多减了一次 1。有人在看到“都要减一”之后,把这几个值先按公式算好,又在代码里统一减了一次,减重了。
解决:把这四个值打印出来和手算结果对一遍。手算的时候记住:每个值都是“从这个时钟数开始算起的位置”,位置从 0 开始,所以减且只减一次。
4.4 画面随机闪、内容是旧的:D-Cache 没刷
现象:CPU 明明往显存里写了新内容,屏幕上是旧的,或者一半新一半旧,刷新几次又对上了。
原因:STM32F767 有 D-Cache,CPU 写显存时数据可能还在 Cache 里没落到 SDRAM,而 LTDC 和 DMA2D 是直接读物理内存的,它们看不到 Cache。这是 F7 上做显示最经典的坑,也是“玄学花屏”这个词的主要来源。
解决:两条路。省事的做法是用 MPU 把帧缓冲所在的 1~2MB 区域配成 Non-cacheable + Bufferable,一劳永逸;讲究性能的做法是每次写完调用SCB_CleanDCache_by_Addr()把对应区间刷下去。项目里我一般选前者,因为后者要在每个绘图函数后面加一行,漏一次就是一个查半天的 bug。
4.5 换帧时上半屏新、下半屏旧:CFBAR 改在了扫描中间
现象:双缓冲切换的瞬间,屏幕上半部分是下一帧,下半部分还是上一帧,出现一条水平的分界线。
原因:直接调用HAL_LTDC_SetAddress()会立刻写LxCFBAR,如果此刻扫描线正好在屏幕中间,上半部分用的还是旧地址读出来的数据,下半部分就换成新的了。
解决:用HAL_LTDC_SetAddress_NoReload()加HAL_LTDC_Reload(&hltdc, LTDC_RELOAD_VERTICAL_BLANKING),让新地址在下一个垂直消隐期生效。这一条在第五章会展开写。
5. DMA2D 与双缓冲:把 CPU 从填色和搬图里解放出来
能出图只是及格线。真正做产品的时候,你会发现 CPU 光是画个背景、贴个图标就把主循环占满了,而且换帧的时候屏幕还在撕。这一章解决两个问题:怎么让搬运不占 CPU,怎么让换帧不撕裂。
5.1 DMA2D 的三种模式,什么时候用哪个
DMA2D 是 F767 片内的 2D 加速器,独立于 LTDC,能直接对显存做填充、拷贝和混合,全程不占 CPU。它有三个模式值得记住:DMA2D_R2M(寄存器到显存),适合纯色填充、清屏、画矩形;DMA2D_M2M(显存到显存),适合贴图和不带透明的位图拷贝;DMA2D_M2M_BLEND(带混合),适合贴带 Alpha 通道的图标、做淡入淡出。
选型上有个经验:能用 R2M 就别用 M2M。同样填一块 800×480 的纯色区域,R2M 只写不读,带宽占用是 M2M 的一半;而 SDRAM 的带宽在 800×480@65Hz 下本来就紧张,LTDC 自己就要吃掉将近 50MB/s。
5.2 填充矩形要自己算行偏移
很多人第一次用 DMA2D 填色,会发现填出来的矩形是斜的,一路歪到屏幕外面去。原因在于 DMA2D 每画完一行要跳到下一行的起点,跳多少由DMA2D_OOR(输出偏移)决定,值是“行宽减矩形宽”。HAL 的OutputOffset字段会把这个值写进 OOR,但目标地址它不管,得你自己按fb + y * 行宽 + x算准。
static DMA2D_HandleTypeDef hdma2d; void LCD_FillRect(uint32_t *fb, uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color) { hdma2d.Instance = DMA2D; hdma2d.Init.Mode = DMA2D_R2M; /* 只写不读,填色最快 */ hdma2d.Init.ColorMode = DMA2D_OUTPUT_RGB565; hdma2d.Init.OutputOffset = LCD_W - w; /* 换行时跳过的像素数,不是字节数 */ HAL_DMA2D_Init(&hdma2d); /* 起始地址必须自己算:fb 是 32 位指针,一个元素一个像素 */ HAL_DMA2D_Start(&hdma2d, (uint32_t)color, (uint32_t)&fb[(uint32_t)y * LCD_W + x], w, h); HAL_DMA2D_PollForTransfer(&hdma2d, 100); /* 正式代码换成回调,别死等 */ }OutputOffset的单位是像素不是字节,这点和 LTDC 的 pitch 不一样,容易混。另外HAL_DMA2D_PollForTransfer会阻塞,在中断里调用必死,正式代码应该用HAL_DMA2D_Start_IT加完成回调。填充整屏这种操作,DMA2D 大概几十微秒就完了,比 CPU 循环写快一个数量级,而且不会和 LTDC 抢 CPU 时间。
5.3 双缓冲加垂直消隐重载,消掉撕裂
双缓冲的思路很简单:准备两块一样大的显存,CPU 往 A 里画,LTDC 从 B 里读;画完了交换。但“交换”这个动作有讲究,必须等 LTDC 扫完一整帧。
/* 不要用 HAL_LTDC_SetAddress,它会立刻改 CFBAR, * 如果此刻扫描线在屏幕中间,就会出现上半屏新、下半屏旧的撕裂 */ void LCD_SwapBuffer(uint32_t next_fb) { HAL_LTDC_SetAddress_NoReload(&hltdc, next_fb, 0); /* 只写影子寄存器 */ HAL_LTDC_Reload(&hltdc, LTDC_RELOAD_VERTICAL_BLANKING); /* 消隐期生效 */ /* 从这一行到下一帧开始,绝不能再去写 next_fb 那块内存, * 否则 LTDC 正在读的内容会被改成半成品 */ }NoReload这个后缀很关键,它表示“只更新影子寄存器,不立即生效”。紧接着的Reload只是置一个标志位,真正的切换发生在下一个垂直消隐期。所以调用完这两行之后,你的代码要立刻转到“画另一块缓冲”上去,两边轮流写。这套机制配合 4.4 里说的 MPU 非缓存配置,撕裂和残影基本就一起消失了。
5.4 MPU 把帧缓冲配成 Non-cacheable
void LCD_MPU_Config(void) { MPU_Region_InitTypeDef mpu = {0}; HAL_MPU_Disable(); mpu.Enable = MPU_REGION_ENABLE; mpu.Number = MPU_REGION_NUMBER0; mpu.BaseAddress = 0xC0000000; /* SDRAM 起始 */ mpu.Size = MPU_REGION_SIZE_2MB; /* 覆盖两帧 750KB 的缓冲 */ mpu.SubRegionDisable = 0x00; mpu.TypeExtField = MPU_TEX_LEVEL1; mpu.AccessPermission = MPU_REGION_FULL_ACCESS; mpu.DisableExec = MPU_INSTRUCTION_ACCESS_DISABLE; mpu.IsShareable = MPU_ACCESS_NOT_SHAREABLE; mpu.IsCacheable = MPU_ACCESS_NOT_CACHEABLE; /* 关键:LTDC / DMA2D 直接读物理内存 */ mpu.IsBufferable = MPU_ACCESS_BUFFERABLE; /* 允许写缓冲,兼顾性能 */ HAL_MPU_ConfigRegion(&mpu); HAL_MPU_Enable(MPU_PRIVILEGED_DEFAULT); }这两个字段的组合是“不缓存但可缓冲”,意思是 CPU 写显存会先进写缓冲再落到 SDRAM,但不会被 Cache 住导致 LTDC 读到旧数据。代价是 CPU 自己回读显存会慢一点,不过显示场景下 CPU 几乎不回读帧缓冲,所以这笔买卖很划算。
配置 MPU 要在main()最开始、使能 Cache 之前完成,顺序反了不生效。另外注意 2MB 区域是从0xC0000000起的,如果你的 SDRAM 只有 8MB,这块区域不会越界,但别把其他外设映射到这段地址上。
6. 进阶:用行中断和示波器把“玄学花屏”变成可测量的问题
前面所有排查都建立在“能观察到现象”之上,但有些问题在屏幕上根本看不出来,比如帧率不稳、偶发丢帧、换帧时机不准。这类问题靠盯着屏幕猜是没有出路的,得让它变成可测量的数字。
第一件事
本文还有配套的精品资源,点击获取