1. 点亮一块RGB屏之前,先弄清楚屏幕到底要什么
1.1 行场同步与DE模式,你的FPGA先要选边站
先说句大实话:第一次拿到4.3寸RGB触摸屏的数据手册时,我盯着引脚定义愣了好一会儿。RGB888接口加触摸I2C加起来五六十个引脚,比之前玩过的SPI屏复杂了不止一个量级。但你只要把原理捋顺,就会发现这批屏幕的驱动思路高度统一,甚至可以说,学会了这一块屏,整个RGB系列你都能上手。
RGB屏和SPI屏最本质的区别在于,它没有"写命令""写数据"这种寄存器操作流程。屏幕上每一个像素的位置,是靠行同步(HSYNC)、场同步(VSYNC)、像素时钟(PCLK)三根线配合来确定位的。数据线上的RGB信号只有在对的时刻、对的位置出现,屏幕才会显示正确的内容。
这里FM的VGA时序大家应该不陌生,RGB屏的时序逻辑和VGA如出一辙,可以说是同一个体系:一帧画面由若干行组成,一行由若干个像素组成,行与行之间有行消隐,帧与帧之间有场消隐,像素数据在有效显示区域内逐点送出去。
实际场景中,4.3寸RGB屏常见的有两种工作模式,需要你在初始化时明确选择:
- SYNC模式(行场同步模式):用HSYNC和VSYNC两根线直接指示行列位置,DE信号可以接固定电平或不用。
- DE模式(数据使能模式):只在DE拉高时,数据线上的RGB信号才有效,行场同步信号也能继续发送,但屏幕以DE为准。
绝大多数4.3寸工业屏默认推荐DE模式,逻辑上比SYNC模式好处理,因为省掉了"消隐期间数据必须为黑"这种约束。但有些屏的数据手册给出的示例时序是SYNC模式,初始化寄存器里可以切换。我自己在DE模式下驱动800x480分辨率的屏幕,时钟、行场、DE四路信号全由FPGA产生,代码结构简洁,出问题也好定位。
1.2 像素时钟、刷新率、时序参数三者怎么配
这是个特别容易翻车的环节。很多人拿到屏幕第一件事就是写代码,结果图像扭曲、闪烁、上下跳动,折腾半天发现是时钟频率不对,或者时序参数和屏幕要求不匹配。
RGB屏的数据手册里通常会给一个时序表,类似下面这样:
| 参数 | 含义 | 800x480典型值 |
|---|---|---|
| HPW | 行同步脉冲宽度 | 1~40 PCLK |
| HBP | 行后沿 | 1~255 PCLK |
| HFP | 行前沿 | 1~255 PCLK |
| VPW | 场同步脉冲宽度 | 1~20 行 |
| VBP | 场后沿 | 1~255 行 |
| VFP | 场前沿 | 1~255 行 |
| DCLK | 像素时钟频率 | 30~33 MHz |
像素时钟的选取逻辑很直接:一帧总行数乘以一行总像素数再乘以刷新率,就得出需要的最低DCLK。以800x480为例,如果一行总计928个PCLK、一帧总计525行、目标刷新率60Hz,那么时钟至少需要928×525×60 ≈ 29.2MHz。数据手册若是给了33MHz的典型值,直接用它就好,不用自己算得太极限。
我当时踩过一个低级坑:用Altera(Intel)的PLL把50MHz晶振倍频到33MHz,但忘了检查PLL输出的占空比规格。后来用示波器看才发现输出正常,问题其实出在时序参数配置和初始化顺序上。这块后面在调试章节细说。
需要留意的是,部分4.3寸屏的兼容性很强,30MHz也能出图,只是刷新率略降。但有一种情况必须严格按手册:带电容触摸的一体式屏幕,触摸IC的上电时序如果和显示屏时序错得太离谱,会造成触摸失灵,这会在第三章展开。
2. Verilog时序生成核心代码拆解
2.1 用计数器把时序"切"出来——行场同步发生器
时序生成器的本质就是计数器。横向一个计数器数PCLK,纵向一个计数器数行数,两个计数器配合,行场同步脉冲、消隐区间、有效显示区间全都能对应到具体的计数值上。
先看行方向的计数器,核心代码大致是这样的:
// 以800x480、33MHz为例 localparam H_TOTAL = 928; // 一行总时钟数 = HFP + HPW + HBP + 800 localparam H_START = 0; // 计数起点 localparam H_SYNC = 40; // 同步脉冲宽度 localparam H_BACK = 40; // 后沿 localparam H_ACT = 800; // 有效像素数 localparam H_FRONT = 48; // 前沿 reg [10:0] h_cnt; always @(posedge pclk or negedge rst_n) begin if (!rst_n) h_cnt <= 0; else if (h_cnt == H_TOTAL - 1) h_cnt <= 0; else h_cnt <= h_cnt + 1; end assign hsync = (h_cnt < H_SYNC) ? 1'b0 : 1'b1; // 低有效或高有效看手册 assign hde = (h_cnt >= H_SYNC + H_BACK) && (h_cnt < H_SYNC + H_BACK + H_ACT);行同步脉冲的极性不是随便定的,这在数据手册里都有标注。有的屏幕是低有效,有的是高有效,务必按手册来。HDE信号就是行方向的数据使能,它相当于一个精度到像素的"窗口"信号。
再来是场方向:
localparam V_TOTAL = 525; // 总行数 localparam V_SYNC = 3; localparam V_BACK = 8; localparam V_ACT = 480; localparam V_FRONT = 34; reg [9:0] v_cnt; always @(posedge pclk or negedge rst_n) begin if (!rst_n) v_cnt <= 0; else if (h_cnt == H_TOTAL - 1) begin if (v_cnt == V_TOTAL - 1) v_cnt <= 0; else v_cnt <= v_cnt + 1; end end assign vsync = (v_cnt < V_SYNC) ? 1'b0 : 1'b1; assign vde = (v_cnt >= V_SYNC + V_BACK) && (v_cnt < V_SYNC + V_BACK + V_ACT); assign de = hde & vde;这样DE信号就是行有效和场有效的逻辑"与",只有同时在水平和垂直有效区间内,数据线上的RGB才对屏幕有意义。这个思路和VGA显示完全一致,如果你之前做过VGA彩条实验,这里基本上是同级复杂度。
但是要提醒一点:别像我最初那样把行场计数器和DE信号分开做两个模块再拼起来,直接在同一个模块里用HDE和VDE组合直接产生最终DE,代码逻辑会更顺,综合后时序路径也更短。
2.2 打拍与FIFO:像素数据流的节奏控制
时序信号生成了,接下来就是数据通道。这一步很多人想简单了,觉得直接把图像数据送到RGB数据线上就行。但实际项目中数据几乎不可能正好和PCLK同拍——要么来自ROM/RAM,要么来自摄像头采集模块,频率和相位都可能有偏差。
最稳妥的做法有两种:
- 寄存器打拍对齐:数据源的时钟和PCLK同源(比如同一PLL输出的不同分频),靠打两拍把数据和DE对齐。适合ROM图片、FPGA内部生成的测试图案。
- 异步FIFO跨时钟:数据源时钟和PCLK完全异步时,用FIFO缓冲,读侧用PCLK和DE配合,保证像素按节奏输出。适合摄像头、HDMI接收等场景。
我这里做了个彩条测试图案,采用打拍方式:
// 测试图案生成:8条彩色竖条 reg [23:0] rgb_data; always @(posedge pclk or negedge rst_n) begin if (!rst_n) rgb_data <= 24'h000000; else if (de) begin case (h_cnt[9:7]) // 取高3位,切成8段 3'd0: rgb_data <= 24'hFF0000; // 红 3'd1: rgb_data <= 24'h00FF00; // 绿 3'd2: rgb_data <= 24'h0000FF; // 蓝 3'd3: rgb_data <= 24'hFFFF00; // 黄 3'd4: rgb_data <= 24'hFF00FF; // 品红 3'd5: rgb_data <= 24'h00FFFF; // 青 3'd6: rgb_data <= 24'hFFFFFF; // 白 default: rgb_data <= 24'h000000; endcase end else rgb_data <= 24'h000000; end // 输出打一拍,消除组合逻辑的毛刺 reg [23:0] rgb_out; reg de_out, hsync_out, vsync_out; always @(posedge pclk or negedge rst_n) begin if (!rst_n) begin rgb_out <= 24'h000000; de_out <= 1'b0; hsync_out <= 1'b1; vsync_out <= 1'b1; end else begin rgb_out <= rgb_data; de_out <= de; hsync_out <= hsync; vsync_out <= vsync; end end输出打一拍这段代码非常关键。时序信号和RGB数据在组合逻辑里生成后直接驱动外部引脚,容易因为布线延迟产生毛刺,有些屏幕会间歇性花屏。打一拍后所有信号统一在PCLK上升沿跳变,源同步接口的质量会好很多。这也是从"能出图"到"稳定出图"之间很重要的一道工序。
2.3 完整驱动模块代码
把上面的思路组合起来,就是一个完整的RGB屏幕驱动模块,以800x480 DE模式为例,代码放到一个文件里管理:
module rgb_lcd_ctrl #( parameter H_ACT = 800, parameter V_ACT = 480, parameter H_SYNC = 40, parameter H_BACK = 40, parameter H_FRONT = 48, parameter V_SYNC = 3, parameter V_BACK = 8, parameter V_FRONT = 34 )( input clk, // 33MHz PCLK input rst_n, output lcd_hs, output lcd_vs, output lcd_de, output [23:0] lcd_rgb, input [23:0] pixel_data_in // 实际图像数据 ); localparam H_TOTAL = H_ACT + H_SYNC + H_BACK + H_FRONT; localparam V_TOTAL = V_ACT + V_SYNC + V_BACK + V_FRONT; // ... 行场计数器与DE生成逻辑见2.1节 ... // 数据窗口内直接把外部图像数据透传 reg [23:0] rgb_data; always @(posedge clk or negedge rst_n) begin if (!rst_n) rgb_data <= 24'h000000; else if (de) rgb_data <= pixel_data_in; else rgb_data <= 24'h000000; end // 打拍输出 reg [23:0] lcd_rgb_r; reg lcd_de_r, lcd_hs_r, lcd_vs_r; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin lcd_rgb_r <= 24'h000000; lcd_de_r <= 1'b0; lcd_hs_r <= 1'b1; lcd_vs_r <= 1'b1; end else begin lcd_rgb_r <= rgb_data; lcd_de_r <= de; lcd_hs_r <= hsync; lcd_vs_r <= vsync; end end assign lcd_hs = lcd_hs_r; assign lcd_vs = lcd_vs_r; assign lcd_de = lcd_de_r; assign lcd_rgb = lcd_rgb_r; endmodule模块的实际使用范围很宽,参数化设置在800x480、640x480、480x272这些常见分辨率之间切换只用改参数,不需要动逻辑。当初我为了适配不同屏幕,把参数都提到顶层,直到现在做新项目时拉出来就改,省了非常多重复劳动。
3. 触摸屏那块玻璃后面藏着的I2C世界
3.1 解析坐标数据包:FT5206是怎么把手指位置报出来的
RGB屏搞定了,接下来是触摸部分。4.3寸屏的电容触摸方案,最常见的是汇顶的GT911系列、敦泰的FT5x06系列,还有一部分用的是台湾奇景的HX8527。这些芯片从协议角度讲并没有本质区别,I2C接口、查询寄存器、读坐标,逻辑上高度统一。我项目里用的是FT5206,拿它举例最合适。
FT5206上电后,FPGA通过I2C接口定期读取它的坐标寄存器。芯片内部的寄存器映射大致分三块:
- 0x00~0x01:设备状态和点数寄存器,0x00的低4位表示当前触摸点数。
- 0x02~0x06:第一个触摸点的X/Y坐标和压力值,X坐标12位,Y坐标12位,分布在连续的字节里。
- 0x08~0x0C:第二个触摸点的数据,布局和第一个点相同。
坐标读取的流程非常固定:先写一个寄存器地址0x02,然后连续读取5个字节(FT5206支持I2C连续读),解析出12位X和12位Y坐标。
| 寄存器 | 位域 | 含义 |
|---|---|---|
| 0x02 | [11:8] XH | 触摸点1 X坐标高4位 |
| 0x03 | [7:0] XL | 触摸点1 X坐标低8位 |
| 0x04 | [11:8] YH | 触摸点1 Y坐标高4位 |
| 0x05 | [7:0] YL | 触摸点1 Y坐标低8位 |
| 0x06 | [7:0] | 触摸点1压力值 |
这会带来一个很隐蔽的坑:X坐标不是单独一个寄存器,而是横跨0x02和0x03两个字节。你要是只读了0x03的低8位,高位永远丢失,坐标在屏幕边缘部分会错得离谱。我第一次调试时就吃了这个亏,触摸点只在屏幕左半边有反应,换到右边直接跳变,折腾了一下午发现是高4位被丢掉了。
3.2 I2C控制器设计:起始、停止、应答
FPGA驱动触摸IC的核心,是一个可靠的I2C主机控制器。某种程度上可以说,读懂I2C时序比读懂触摸屏数据手册更重要——因为FT5206也好、GT911也好,外设逻辑都跑在I2C这条总线上。
I2C的四个关键时序段:
- 起始条件(START):SCL为高时,SDA从高拉低。
- 停止条件(STOP):SCL为高时,SDA从低拉高。
- 数据位:SCL低电平时SDA变化,SCL高电平时SDA必须保持稳定。
- 应答(ACK):接收方在第9个时钟周期拉低SDA表示成功接收。
在FPGA里做I2C控制器,两种状态机风格比较主流:
- 位状态机:每状态处理一个SCL周期内的SDA变化。状态数多,但时序控制精确,适合高频PCLK。
- 字节状态机:每状态处理一个完整字节,内部细分位计数。更直观,适合50MHz以下的系统时钟。
我的FT5206控制器挂在50MHz时钟下,走的字节状态机。核心状态跳转:
localparam IDLE = 4'd0; localparam START = 4'd1; localparam SEND_ADDR = 4'd2; // 写地址或读地址 localparam CHECK_ACK = 4'd3; localparam SEND_REG = 4'd4; // 写寄存器地址 localparam CHECK_ACK2 = 4'd5; localparam RESTART = 4'd6; // 读操作时需要重发起始条件 localparam SEND_RADDR = 4'd7; // 读地址 localparam CHECK_ACK3 = 4'd8; localparam READ_DATA = 4'd9; // 连续读 localparam SEND_NACK = 4'd10; localparam STOP = 4'd11;读寄存器序列实际上包含两次I2C事务:第一次发START→写设备地址→写寄存器地址,第二次发RESTART→读设备地址→读数据→发NACK→STOP。这个"重复起始条件(Restart)"是I2C协议里特别基础的用法,但也是很多新手最容易漏的地方。漏掉RESTART直接发STOP再接START,部分触摸IC也能容错工作,但行为会变得不可预期,时不时的通信超时就成了玄学问题。
SDA是开漏输出,FPGA侧必须有上拉电阻。很多4.3寸触摸屏模组自带上拉,但有些廉价款没有,需要自己在PCB上加2.2K~4.7K上拉电阻到3.3V。这个如果你忽略,表现就是I2C读写偶尔成功偶尔失败,用示波器抓波形发现SDA低电平像斜坡一样缓降。务必记住:I2C总线不是靠推挽输出拉高电平的,而是靠上拉电阻。
3.3 校准不存在的,但你得处理坐标映射
触摸坐标和屏幕像素坐标多数情况下不是一一对应的。以FT5206为例,它输出的坐标范围通常是0~1023(12位),而屏幕分辨率是800x480,不做换算的话点哪里都错位。
换算公式很直接:
pixel_x = touch_x * 800 / 1024 pixel_y = touch_y * 480 / 1024但还有个更隐蔽的问题:触摸屏的X轴方向和屏幕显示可能正好相反。我手上的这块屏,触摸IC输出的X坐标是按模组的物理方向定义的,屏幕显示时是从左到右扫描,如果不加处理,你点在屏幕左上角,系统会认为点在右上角。解决办法就是在代码里加一个方向标志位,要么在算坐标时做镜像,要么在硬件layout时翻转屏的方向。软件方式好用,加两句:
assign pixel_x = (mirror_x) ? (800 - touch_x * 800 / 1024) : (touch_x * 800 / 1024); assign pixel_y = (mirror_y) ? (480 - touch_y * 480 / 1024) : (touch_y * 480 / 1024);坐标滤波则是另一个不能忽略的环节。电容屏的原始数据会有抖动,尤其是边缘区域和手指按压力度不均时,坐标可能在几个像素间跳变。嵌入式Linux里常用中值滤波或滑动平均,Verilog里实现滑动平均更简单:寄存器组存最近4次X、Y坐标,输出取平均值。代价是多几个周期延迟,但触摸手感会稳定很多。具体实现有很多方案,资源足够的话,用移位寄存器加累加器做个4点平均就够用。
4. 调试中的那些坑,逻辑分析仪教我的事
4.1 上电点不亮的常见原因:从电源到时序逐一排查
RGB屏调试和单片机点灯完全是两个难度。屏幕不亮时,问题可能出现在任何一环,而你不会立刻知道是哪一环。我把自己排查顺序整理成了固定流程,每次遇到新屏都按这个顺序走,排查速度快了很多:
第一步:量电源。4.3寸RGB屏模组通常需要3.3V逻辑电源,部分带背光的还需要单独的背光LED电源,甚至有些模组要求5V。上电后用万用表量屏接口处的电压实际值,很多故障其实出在电源适配器压降上——电源标称3.3V,接上屏后实际只有2.9V,IC就死活不工作。
第二步:确认背光。背光不亮和屏幕不显示是两回事。如果背光控制引脚(常标为BL_EN或LED_EN)没有拉高,或者PWM调光引脚悬空,屏幕看起来就是黑的,但这不代表RGB信号有问题。
第三步:逻辑分析仪抓时序。到现在这一步,如果你有逻辑分析仪(哪怕是十来块钱的24MHz USB逻辑分析仪),别犹豫,直接抓HSYNC、VSYNC、DE、PCLK四根线。看它们是否按你代码里设计的周期在翻转。这里有个关键判断点:像素时钟要采样得到,普通逻辑分析仪带宽有限,直接用示波器看更靠谱,但HSYNC和DE这些低频信号,逻辑分析仪完全够用。
如果四根线都有波形,屏还是黑屏,大概率是背光或者电源问题。如果波形频率不对,多半是PLL配置错了,回来检查时钟。
4.2 显示错位与花屏的波形级分析
显示错位这问题,表现形式很典型:屏幕左移了一截,右边多出一条垂直黑带,或者画面上下滚动。这通常是时序参数和屏的实际规格不一致导致的。
举个例子,如果HBP配小了,图像会整体左移,右侧是黑边;如果HFP配小了,图像整体右移。这其实代表了消隐区间的分配出了问题。解决办法是静下心来看数据手册,把HFP、HPW、HBP、VFP、VPW、VBP都核对一遍,尤其是注意单位:有些手册写的是"像素时钟个数",有些写的是"基准时钟个数",换算错了也会出现类似错位。
花屏的情况一般有两种:
- 花屏加闪烁:像素时钟频率不对,或者PCLK的相位/极性配反了。数据手册里会标注PCLK是上升沿采数据还是下降沿采数据,如果代码在时钟下降沿打拍,而屏幕在上升沿采集,显示就会错乱。
- 花屏但画面稳定:数据线连接问题居多。RGB888需要24根数据线,排线虚焊、杜邦线接触不良、PCB走线串扰都会导致某几位固定错误。这时候代码解决不了问题,得用万用表一根一根量通断。排线松了这类看似弱智的问题,我至少遇到三次。
4.3 触摸乱跳、漂移的处理思路
触摸IC调通后,真正闹心的是坐标漂移和触摸乱跳。
电容触摸乱跳,常见原因有三类:
1. 电源噪声干扰。I2C边上的开关电源纹波大,触摸IC参考电压跟着抖,报点坐标就乱跳。处理方式是给触摸IC的电源单独加LC滤波,避开开关电源辐射区域。
2. I2C时序不标准。SCL频率太高或者上升沿过缓,触摸IC采样出错,报出异常坐标。我在FPGA里做I2C时,SCL频率压在100KHz附近,逻辑分析仪看波形发现上升沿差不多50ns,后来在SDA和SCL上加了上拉电阻后稳定了。
3. 触摸IC固件重新握手失败。这个排查起来是最费时间的。某些电容触摸IC在开机时,主控需要主动向它发送配置数据,如果上电时序不对或者I2C通信失败,触摸IC会运行在默认状态,表现就是触摸不响应,或者报点不对。
碰到这种情况,我的建议是:先断开所有其他I2C设备,只留触摸IC,然后反复上电看能否恢复。能恢复的话,问题大概率在握手时序;不能恢复,换一颗同型号触摸IC再试,排除芯片本身损坏的可能。
还有一个很实用的技巧:触摸屏模组自带复位引脚(RESET/RST)的话,可以在FPGA初始化时做一个"上电延迟复位"流程:先拉低复位脚保持10ms以上,拉高后等待至少50ms再开始I2C通信。这个流程能解决相当一部分"摸着摸着突然失灵"的疑难杂症。因为触摸IC内部的固件上电后需要一个稳定的供电和复位过程,等它完全进入工作状态再通信,成功率最高。
5. 这套方案能扩展到哪里去
RGB屏驱动这事儿,最大的价值不在于"点亮一块屏",而在于打通了一条图像数据通路。很多项目只要换了输入源,整块屏的用途就完全不同了。
我在项目里接过一个OV5640摄像头模组,图像数据从摄像头传感器输出,经过色彩插值、格式转换,最后填进一个异步FIFO,读侧用RGB屏的PCLK和DE信号去取数。改造过程非常直观:摄像头模块替代了2.2节的测试图案生成器,FIFO桥接了两个不同时钟域,其余的行场同步、打拍输出逻辑完全复用。
后来又做过一次把图像数据换成HDMI输入的实验,也是同一个套路——解码后的RGB数据经异步FIFO再送到屏控模块。整个工程的可复用性非常高,因为屏幕驱动模块永远只关心三件事:PCLK节奏、DE窗口、RGB数据对齐。至于数据是哪来的,它不在乎。
如果你接下来想做更复杂的图形界面,比如菜单系统、仪表盘、波形显示,建议在屏控模块外面套一层简单的总线协议,让单片机或软核CPU来写显存。显存用双端口RAM实现,一边是CPU写数据,另一边是屏控模块按固定节奏读数据,互不干扰。这种架构再往上走,就是成熟的GPU工作了逻辑雏形,理解这一点之后再去学MIPI、LVDS这些接口,很多概念都会一通百通。
另外,我建议你在工程里预留一组调试用的数码管或LED指示灯,把触摸状态、显示帧同步状态都拉出来亮灯。FPGA调试最怕的就是黑盒,不知道内部跑到哪一步了。多个指示灯,调试效率会明显改善。
我自己在实际项目中养成的习惯是:屏幕驱动模块单独建一个工程目录,参数、模块、仿真testbench全部独立管理,换项目时直接拷过去,只改顶层连线。做了三个项目之后,这个模块已经几乎不再改动,省下来的时间都花在业务逻辑上了。这个做法,比重新从零写一遍驱动值得多。