简介压缩包内含基于Altera现场可编程门阵列驱动红绿蓝三原色接口液晶屏的完整工程示例面向使用Quartus开发环境的硬件工程师与嵌入式学习者无论是消费电子还是工业显示面板均可借助该示例快速验证驱动方案特别适合需要实现二十四位真彩色显示、像素时钟与行场同步信号控制的场景。工程覆盖从寄存器传输级设计、逻辑综合、适配布线到生成可下载比特流的全过程包含七个Verilog源文件、Quartus编译中间文件cdb、hdb、ddb、rdb、工程配置文件qsf、qpf以及最终下载用的sof文件另附txt说明文档与html报告共一百二十六个文件压缩包仅3.43MB结构清晰便于按需查阅。目前已有1719人学习或下载数据表明其实用性获得一定认可。借助该工程可以系统理解RGB液晶屏驱动的整体架构掌握颜色空间转换、帧缓冲、时钟同步等核心逻辑的实现方法同时可通过工程目录映射Quartus各个处理阶段的产物为后续设计类似显示控制系统提供扎实的参考模板。1. tft_rgb_colorbar.rar 是什么一条色带看穿整个 RGB 链路拿到 tft_rgb_colorbar.rar 这类包你手里基本上就是一套 TFT 屏调试用的“标准测试图案 读写工程”。RGB colorbar 说白了就是一张把红、绿、蓝三通道和灰阶全部铺开的测试图常见做法是压缩包里放着 BMP 格式的色带图、读图脚本以及一个极简的刷屏例程。它的价值不在于好看而在于如果屏幕点不亮、偏色、刷新错位这张图能直接告诉你坏在像素格式、窗口坐标、扫描方向还是 Gamma 上比对着白屏盲猜快得多。适合刚接手 MIPI/SPI 屏驱动、做板级验证的工程师也适合在换屏后快速确认整个显示链路是否正常。2. 读懂 colorbar 的编码逻辑先弄明白你在往屏幕里写什么2.1 色带里的三类信息灰阶、分通道、渐变RGB colorbar 不是随便画几条彩色竖条就完事的。标准做法是横向排布从左到右分成几组信息区灰阶块区、R/G/B 分通道渐变区、以及灰阶渐变区。灰阶块用来验证亮度层次分通道渐变用来验证每个数据位有没有被搞错灰阶渐变则用来暴露传输过程中的丢位和 Gamma 畸变。在动手刷屏之前先把这张图想象成一个像素数组每一个像素都是一个 (R, G, B) 三元组。TFT 屏控制器的核心工作就是把这个三元组按照你配置的像素格式RGB565 还是 RGB888、扫描方向、窗口起始坐标写进对应坐标的显存里。colorbar 之所以比普通照片好用是因为它的像素值是有明确规律的——你可以推算出任何一个坐标点的期望 RGB然后用读回或截图去比对偏差一眼就能定位是哪个环节出了问题。2.2 生成一张分辨率精确匹配的 colorbarPython 脚本我一般会用 Python Pillow 库生成测试图而不是直接从网上下现成的。原因很简单普通图片的分辨率很少精确等于你屏的物理分辨率强行拉伸会让色带边缘出现插值混色导致你以为驱动有问题其实是一张图坏了事。from PIL import Image, ImageDraw # 目标分辨率改成你的屏实际分辨率 W, H 1080, 2280 img Image.new(RGB, (W, H), (0, 0, 0)) draw ImageDraw.Draw(img) # 1. 左侧 1/48 级灰阶跳变块 block_h H // 8 for i in range(8): g i * 255 // 7 draw.rectangle([0, i*block_h, W//4, (i1)*block_h], fill(g, g, g)) # 2. 中间 3/4R/G/B 分通道渐变每通道宽度各占 1/4 seg_w W // 4 # 左侧第一段R 从 0 渐变到 255 for x in range(seg_w): r int(x / (seg_w - 1) * 255) draw.line([(W//4 x, 0), (W//4 x, H)], fill(r, 0, 0)) # 第二段G 渐变 for x in range(seg_w): g int(x / (seg_w - 1) * 255) draw.line([(W//4 seg_w x, 0), (W//4 seg_w x, H)], fill(0, g, 0)) # 第三段B 渐变 for x in range(seg_w): b int(x / (seg_w - 1) * 255) draw.line([(W//4 2*seg_w x, 0), (W//4 2*seg_w x, H)], fill(0, 0, b)) # 3. 最右侧 1/4灰阶渐变从 0 到 255 线性渐变 for x in range(seg_w): g int(x / (seg_w - 1) * 255) draw.line([(W//4*3 x, 0), (W//4*3 x, H)], fill(g, g, g)) img.save(colorbar.bmp, BMP)这里每个像素点的 RGB 值都是可预测的。比如图里坐标(W//4 100, 任意y)处的颜色应该是一个亮红色R 约为100/(seg_w-1)*255。在做屏端读回验证时这些已知值就是比对基准。参数里需要改的是W和H别用 1920x1080 的逻辑分辨率换算要用屏显存里真实的行列数否则窗口设置和读回坐标会全部错位。2.3 三个必调参数色带数、灰阶步长、渐变/跳变色带数量不是越多越好。做灰阶跳变块时我通常用 8 级或 16 级8 级适合快速看个大概16 级能分辨出暗部压缩问题再往上比如 32 级如果驱动 IC 的 Gamma 没调好相邻两块几乎分不清反而不容易肉眼判断。灰阶步长的选择要结合屏的 bit 深度。如果你的 TFT 驱动 IC 支持 8bitRGB888g i * 255 // 7就很自然如果屏工作在 RGB565 模式下R 和 B 只有 5bitG 只有 6bit你把灰阶步长设为 1写进去后相邻色阶可能完全一样因为你把细节丢在了格式转换时。所以调试早期优先用大步长比如每级差 32先确认链路通再回来验灰阶精度。渐变和跳变的选择取决于你要解决什么问题。跳变色带用于确认像素写入位置和扫描方向因为边界是硬切错位现象非常明显。渐变色带则用来检查数据位传输是否丢位——如果某个数据位被拉低渐变带中间会出现规则的横向细条纹。判断经验是跳变块出问题先查坐标和窗口渐变块出问题先查像素格式和引线。3. 把色带写进 TFT 的最小工程文件、驱动参数与刷屏命令3.1 文件格式为什么选 BMP 不选 JPEG/PNGcolorbar 的载体格式直接决定刷屏工程的复杂度。我们做嵌入式屏调试最省事的是 24bit BMP。BMP 格式没有任何压缩像素数据按行顺序保存解析时只需要跳过一个 54 字节的文件头剩下就是像素数组。JPEG 是压缩格式解码需要 JPEG 库且是有损压缩PNG 虽然无损但用 zlib 压缩解码也要带 inflate 逻辑在 MCU 上跑纯粹是给自己找麻烦。BMP 头里只需要关心三个关键偏移量用十六进制编辑器就能确认偏移长度含义colorbar.bmp 常见值0x002字节文件标记0x4D42 (BM)0x0A4字节像素数据起始偏移54无调色板时0x124字节图像宽度与屏宽一致0x164字节图像高度可为负值正数为自底向上0x1C2字节位深24这里有个容易翻车的点BMP 默认行序是从最后一行开始存的也就是说文件里第一行像素是图像最底部的行。如果你的刷屏代码按文件顺序直接送数据且驱动配置的扫描方向是从上到下整个画面会上下镜像。我通常写一个小工具把 BMP 行序翻转为自顶向下或者直接在刷屏时把 Y 坐标反过来两种做法都可以但一定要在代码里固定下来避免后续换图时又踩一次。3.2 从松散的图片文件到紧凑的像素数组转换脚本24bit BMP 里每个像素占 3 字节顺序是 B、G、R文件里有调色板或者行对齐填充时需要处理。我们用 Pillow 生成时不会有多余内容但从别处拿来的 BMP 建议做一次归一化from PIL import Image import numpy as np img Image.open(raw_input.bmp).convert(RGB) # 如果宽度不是 4 的倍数BMP 会每行末尾补 0 到 4 字节对齐 # 这里统一转成 numpy 数组去掉行 padding 的影响 arr np.array(img) # 形状 (H, W, 3)通道顺序为 R, G, B # 翻转行序BMP 存储是自底向上转成自顶向下 arr np.flipud(arr) # 打平成 24bit 像素流注意转成 BGR 字节序 flat arr[:, :, ::-1].flatten() # RGB - BGR # 把 RGB 三个字节打包成 int方便后续通过 SPI/MIPI 发送 packed np.packbits(flat.reshape(-1, 8), axis1) print(f像素字节数: {flat.nbytes})转换脚本的逻辑就是把图像数据变成和驱动 IC 显存布局完全一致的字节流。为什么要在电脑上先转好而不是在 MCU 里转因为 MCU 内存小RGB888 每像素 3 字节一张 1080x2280 的图裸数据要 6MB 多绝大多数 MCU 放不下。常见做法是只把转换结果做成 C 数组刷屏时逐段读取或直接从外部 Flash 流式发送。如果屏是 RGB565那转换脚本就要多做一步丢弃 R 和 B 的低 3 位、G 的低 2 位然后打包成 2 字节。这一步能在 PC 上做就绝不在 MCU 上做因为你每省一个 MCU 周期刷屏速度就能快一截。3.3 写窗口与显存填充两条刷屏命令就够无论你的 TFT 驱动 IC 是 JD9365DA-H3 还是更老的 ST7789刷色带最小的命令集就三个设置列地址范围、设置行地址范围、开始写显存。以 MIPI DSI 加常见驱动 IC 为例命令序列如下// 0x2A: Column Address Set, 列地址范围 uint8_t col_addr[] {0x2A, (uint8_t)(x_start 8), (uint8_t)(x_start 0xFF), (uint8_t)(x_end 8), (uint8_t)(x_end 0xFF)}; spi_send_cmd(col_addr, sizeof(col_addr)); // 0x2B: Page Address Set, 行地址范围 uint8_t page_addr[] {0x2B, (uint8_t)(y_start 8), (uint8_t)(y_start 0xFF), (uint8_t)(y_end 8), (uint8_t)(y_end 0xFF)}; spi_send_cmd(page_addr, sizeof(page_addr)); // 0x2C: Memory Write, 开始连续写入像素数据 uint8_t write_cmd[] {0x2C}; spi_send_cmd(write_cmd, sizeof(write_cmd)); // 之后开始一个像素一个像素地送 RGB 数据 for (int i 0; i pixel_count; i) { spi_send_data(pixel_stream i * bytes_per_pixel, bytes_per_pixel); }这段代码的逻辑很简单先圈定你要写的矩形区域然后连续灌数据。驱动 IC 会按你设置的扫描方向自动换行你不用关心换行逻辑。但这里有两个参数必须和驱动的“扫描方向”寄存器MADCTL, 0x36配合起来。如果你设置窗口为x_start0, x_endW-1, y_start0, y_endH-1但 MADCTL 配置成右旋 90 度那么你看到的图像是侧躺的。调试 colorbar 时先别管旋转问题把 MADCTL 设成 0也就是不旋转不镜像然后再确认坐标方向这样定位问题最简单。3.4 先回答驱动 IC 两个问题RGB565 还是 RGB888、扫描方向是哪个RGB colorbar 是验证像素格式最好的工具但前提是你先确认驱动 IC 当前被配置成什么格式。这一点怎么确认看驱动 IC 的 Data Format Select 寄存器名字大致是0x3ADCS 标准或各家自定义的COLMOD。常见取值0x3A 0x70表示 RGB66618bit0x3A 0x60表示 RGB666 通过 24bit 接口传输0x3A 0x50表示 RGB5650x3A 0x77表示 RGB888。// 设置像素格式: 这里以 RGB888 为例 uint8_t pixfmt[] {0x3A, 0x77}; spi_send_cmd(pixfmt, sizeof(pixfmt));和你期望的像素格式不一致时刷出来是什么效果如果 colorbar 在电脑上看是 RGB888 三通道各 8bit但屏工作在 RGB565那么亮色区域会出现明显色偏红色渐变带上会有细密噪点式条纹因为低 3bit 被驱动 IC 直接扔掉了。所以刷屏之前确认两件事一是你生成的图是什么格式二是驱动 IC 的0x3A配置成什么。两边的 bit 深度必须匹配否则后面所有偏色排查都是白费功夫。扫描方向寄存器我习惯一上来就写死成0x00也就是默认自上而下、从左到右。等 colorbar 能完整显示且方向正确后再去调旋转这样能减少变量。记住一个时刻只改一个参数colorbar 才能发挥出它的定位价值。4. python 读取图片 rgb 值与屏端读回用数据代替肉眼判断4.1 用 PIL 读取期望色值getpixel 性能不够时就 load肉眼判断 colorbar 偏色只能分辨“有没有问题”不能量化“差了多少”。这时候就需要把期望图像的 RGB 值和实际显示结果做逐点比对。Python 端我一般用 Pillow 的getpixel但如果你要扫全屏几千个点getpixel太慢更聪明的做法是用load()把像素数组一次性拉进内存from PIL import Image img Image.open(colorbar.bmp).convert(RGB) # 转成像素访问对象比 getpixel 快很多 pixels img.load() w, h img.size # 在屏幕上选几个关键坐标做采样比如每个色带的中线 probe_points [ (w // 8, h // 2), # 灰阶跳变区中点 (w // 4 50, h // 2), # R 渐变区 (w // 2 50, h // 2), # G 渐变区 (w * 3 // 4 50, h // 2), # B 渐变区 (w * 7 // 8, h // 2), # 灰阶渐变区 ] for pt in probe_points: r, g, b pixels[pt] print(f坐标 {pt}: R{r} G{g} B{b})如果你在屏端读取到的 RGB 和这里的期望值相差超过 ±5就要小心了。±5 以内的差异通常是背光或 Gamma 引入的属于正常范围差几十以上基本是像素格式、通道顺序或者数据位传输的问题。这个方法同样可以用来读一张真实照片拍下来的屏幕画面——把照片裁剪成和 colorbar 同样分辨率再对同一坐标取色比对逻辑完全一样。4.2 驱动读回0x2E GRAM Read 的假读时序与坐标陷阱比拍照更可靠的办法是直接通过驱动 IC 的读回命令把显存内容抓出来。标准 DCS 命令里有一个 0x2EMemory Read配合地址设置命令就能把指定坐标的 RGB 值读回。听起来很直接但有三个坑等着你踩。第一个坑是假读。很多驱动 IC包括 JD9365DA-H3 这类常见型号在发送 Memory Read 命令之后需要你额外读一次数据丢掉第一个字节后面才是有效数据。不同 IC 假读的字节数还不一样有的要 1 字节有的要 2 字节这个只能查 datasheet没有统一规律。第二个坑是坐标陷阱。读回前必须重新设置 Column Address 和 Page Address而且有些 IC 读回时地址是自增的读到行尾后会翻到下一行。如果你只设置了一次窗口然后连续读出来的是整块区域的数据而不是你想要的单点。所以要读单个坐标就得每次重新设置一次窗口读 3 字节RGB888 各占 1 字节就停。第三个坑是读回命令对传输模式有要求。SPI 接口的屏读回通常要求在命令后切换总线方向MIPI DSI 接口则要看 IC 是否支持 DSI 读回很多 DSI 屏只支持写读回只在诊断模式下通过 I2C 或 SPI 完成。如果你发现读回命令发出去后总线没有响应先别怀疑时序去查一下这款屏的读回通道到底走的是哪条路线。// 读回单点像素, 假设是 SPI 接口 RGB888 uint8_t cmd[] {0x2A, (uint8_t)(x 8), (uint8_t)(x 0xFF), (uint8_t)(x 8), (uint8_t)(x 0xFF)}; spi_send_cmd(cmd, sizeof(cmd)); cmd[0] 0x2B; cmd[1] (uint8_t)(y 8); cmd[2] (uint8_t)(y 0xFF); cmd[3] (uint8_t)(y 8); cmd[4] (uint8_t)(y 0xFF); spi_send_cmd(cmd, sizeof(cmd)); uint8_t read_cmd[] {0x2E}; spi_send_cmd(read_cmd, sizeof(read_cmd)); uint8_t dummy, r, g, b; spi_receive_data(dummy, 1); // 假读丢弃 spi_receive_data(r, 1); spi_receive_data(g, 1); spi_receive_data(b, 1); printf(RGB(%d,%d) %02X %02X %02X\n, x, y, r, g, b);这段代码每次只读一个点。如果你要扫全屏可以扩大窗口一次性读一大块但要注意缓冲区和传输速度一次读太多数据超过 MCU 接收能力会翻车。读回值和期望值做逐字节比较如果R和B的值互换说明颜色通道顺序不是 RGB 而是 BGR如果低几位全是 0说明驱动 IC 内部被配置成了 RGB565 而你在往它送 RGB888。4.3 动态照明联动把 colorbar 色值同步推给 RGB 外设灯光colorbar 还有一个常被忽略的用途——如果系统里有氛围灯、RGB 外设灯光这类设备它可以作为统一的颜色标定基准。做动态照明方案时常见做法是让 MCU 解析 colorbar 的色值再通过 PWM 或 I2C 驱动外接的 RGB LED确保屏幕和灯光显示同一颜色而不是各调各的。// 从 colorbar 的 R 渐变区取一个点, 推给 RGB LED uint16_t r_val spi_read_reg(0x2E, x, y); // 假读省略 uint8_t led_pwm[3] { r_val, 0, 0 }; i2c_write_led(0x18, led_pwm, 3); // 驱动 RGB LED 模块这种做法在做联动灯光时很有用屏幕和灯光的白平衡如果能对齐视觉上会协调很多。调试技巧是先把 colorbar 的灰阶块作为基准屏幕显示某种灰阶时把 LED 的 PWM 调到肉眼看起来亮度一致然后记录下这个 PWM 表。后续做动态照明时查表就能让总体灯光跟随屏幕内容不需要每次调光都用色度计去测。5. TFT 彩屏 colorbar 避坑指南现象、原因、解决5.1 色带只显示上半屏下半屏还是上一帧残留现象刷 colorbar 后只有上半部分更新了下半部分保持着上一次的图像或者黑屏。原因这是窗口寄存器配置错误或者驱动 IC 的行地址设置超出了实际分辨率。常见翻车点是初始化时把0x2BPage Address Set设成了比实际屏高多一行或者屏的 POR 值里SSSource Driver Shift和NLNumber of Lines没配对。解决把 Column Address 和 Page Address 的终值改成分辨率-1如果你屏是 2280 行行地址最大值就写0x08EB2280 的十六进制。还有一个细节部分驱动 IC 的 GRAM 地址是反的初始化时通过 MADCTL 的ROW_SCAN和COL_SCAN位来纠正这个位如果设置错出现的就是显示区域错位而不是简单的上下镜像。5.2 整条色带偏红或偏蓝但灰阶块看起来正常现象RGB 三个渐变区的颜色明显不对R 渐变区算出的是红色没错但亮度和期望值差很远G 和 B 区也异常。灰阶块却大致正常。原因这是颜色通道顺序问题。很多驱动 IC 支持 RGB 和 BGR 两种模式寄存器通常叫BGR在0x36MADCTL 里面。如果你的数据是按 RGB 顺序发的但 IC 被设成 BGR则红蓝互换。灰阶块因为 RGB 三个通道值相等所以看不出问题。解决改0x36寄存器的BGR位或者改发送字节序只改一处不要两个都改。改完用 R 渐变区中点做验证期望是(255,0,0)读回如果是(0,0,255)说明修正生效。5.3 渐变色带出现规律性横纹像“搓衣板”现象纯色渐变区不是平滑过渡而是每隔几行出现一条更亮或更暗的横纹。跳变灰阶块没有这个问题。原因多数是像素格式错配后的数据位丢失。比如你架构里的 LCDC 配置成 RGB666 但发的是 RGB888 数据故障率高的地方在低位数据线上悬空或短路。如果渐变带上有固定间隔的跳变数一下行间距往往能算出是哪一根数据线的问题。解决先用0x3A确认当前像素格式再对照电路图重点量悬空的数据线。软件上可以改用低 bit 深度的 RGB565 模式再刷一次如果横纹消失说明数据高位正常、低位线有问题为主板硬件故障而非驱动配置错误。5.4 色带暗部糊成一团深灰和黑完全分不开现象灰阶块区域靠左边的几块看起来全是黑的亮的部分正常。原因这是两个常见原因叠加一是背光亮度偏低导致暗部对比度不足二是驱动 IC 的 Gamma 寄存器没调过默认曲线把暗部压缩了。colorbar 本身没有问题问题在显示链路末端的 D/A 转换曲线。解决先调背光 PWM 占空比排除亮度因素。然后查驱动 IC 的 Positive/Negative Gamma 寄存器组通常是0xE0到0xE7一组找厂商给出的推荐值写进去不要自己裸调 Gamma容易越调越花。确认 Gamma 无误后再用 16 级灰阶色带重新验证暗部层次。5.5 纯色区域的边缘出现彩色毛边现象colorbar 里灰色的跳变块边缘出现红绿蓝杂色像是被描了一圈边。原因这个一般不在驱动 IC 本身而在屏的像素排列和扫描时序。如果是旋转屏颜色子像素排列从 RGB 条纹变成了 PenTile 或其他排列方式子像素渲染逻辑不对就会出现彩色边缘。解决先确认 MADCTL 的旋转方向每个方向对应的子像素排列可能不同。如果是接口时序造成的采样偏移检查 pixel clock 和行同步参数在屏规格书的时序表里找到tHBP和tVBP的推荐区间。付费经验是这种错位问题最容易被怀疑成驱动代码实际往往出在初始化时序参数配置表里。6. colorbar 的进阶玩法用固定色块做坏点定位与灰阶响应6.1 坏点扫描固定坐标原色像素对比常规色带图是按区域铺整体渐变的但你做产线屏检时需要另一种形态的 colorbar每个像素都是固定颜色比如 R255 全屏红色再切全绿、全蓝、全白、全黑。把这些纯色帧按顺序循环播放人眼或自动光学检测设备逐帧拍照任何一个不亮的点都会在某些帧暴露出来。坏点定位的思路是在拍照后做像素减法期望图像是纯色实际拍到的图中某个点的像素值和周围差异超过阈值就记为可疑坏点。import cv2, numpy as np img cv2.imread(screen_white.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 每个像素与周围 3x3 均值做差, 差异过大就是坏点候选 blur cv2.GaussianBlur(gray, (3, 3), 0) diff cv2.absdiff(gray, blur) bad np.argwhere(diff 40) print(坏点坐标列表:, bad[:50])阈值 40 是个经验值实际还要配合光圈和曝光来校准。如果整屏所有像素灰度值整体偏低说明曝光不足先调相机参数再判坏点否则会误报一片。6.2 灰阶响应时间用照片序列测拖影常规 dynamic 应用场景里屏的响应速度比色准更影响体验。你可以在 colorbar 里加入渐变的灰阶块快速从 0 跳到 255 再跳回来用高速相机连拍几十帧观察中间帧的亮度是否符合预期过渡。没有高速相机时用手机慢动作视频逐帧提取也行虽然精度差一些但判断严重拖影足够了。这个方法的优点是全部复用已有工程生成图、刷屏代码、Python 读帧脚本都是调试中已经写好的东西不用额外引入仪器。我自己的习惯是每次调完驱动先把纯色循环和灰阶跳变各跑一遍再进入上层应用开发避免屏幕问题掩盖 UI 问题。这条流程帮我在后来遇到显示异常时能立刻分清是驱动层问题还是应用层数据问题少翻了很多次车——希望帮到你。本文还有配套的精品资源点击获取