资讯动态

FPGA驱动4.3寸RGB触摸屏:Verilog时序与I2C实战解析

发布时间:2026/10/2 1:19:34 来源:尧图企业网站定制
说实话把一块RGB接口的4.3寸触摸屏点亮说难不难说简单也真不简单。我从流水灯玩到数码管再玩到串口收发一直觉得显示这块是FPGA学习路上最值得啃的硬骨头。RGB屏不像SPI屏那样有现成的控制器芯片帮你刷像素它把时序、同步、像素时钟全抛给你逼着你去真正理解显示设备的工作方式。这篇文章就把我用Verilog驱动4.3寸RGB电容触摸屏的整个思路、代码和调试过程完整复盘一遍适合已经会Verilog基础语法、想挑战一个综合项目的朋友。先说清楚这个项目做到什么程度板子上电后屏幕会显示红、绿、蓝、白四色横条手指点到哪里对应位置就会出现一个黄色十字光标手指移开光标消失。整个工程分两条链路显示链路负责把图像数据送进RGB屏触摸链路通过I2C读取电容屏控制芯片的坐标值再叠加到显示画面上。无论你是想搞FPGA图像处理、做仪器界面还是纯粹练手这个项目都能帮你把“时序”和“状态机”这两件事彻底吃透。1. 项目拆解RGB触摸屏背后要解决的三件事1.1 RGB接口屏和常见TFT屏有什么不一样很多初学者第一次接触屏幕是用STM32或ESP32驱动0.96寸OLED、1.8寸TFT这些屏里面内置了控制器芯片比如ST7735、SSD1306之类。你在单片机上写命令、写像素数据控制器会帮你管理行列扫描、刷新率这些底层细节你只需要关心SPI或I2C接口怎么发数据就行。RGB接口屏完全不同。它的面板上只有行信号、场信号、像素时钟、数据使能以及并行RGB数据线没有控制器帮你生成任何时序。屏幕内部就像一张按行扫描的“像素矩阵”每来一个像素时钟它就把当时数据线上出现的24位颜色数据写到当前行列位置然后自动跳到下一个位置。如果像素时钟中断、行场同步信号异常画面就会出现撕裂、偏移甚至完全黑屏。所以驱动RGB屏的关键不是在“发数据”而是在“生成时序”。你需要用计数器精确模拟屏幕的扫描过程一行一行从左到右扫扫完一行就换行扫完一屏再从左上角重新开始。这个听起来复杂实际拆开就是几个计数器和比较器的事情。1.2 硬件准备与整体框图我的硬件组合比较常规一块Xilinx Artix-7系列的开发板板载50MHz晶振一块4.3寸480×272的RGB电容触摸屏模组。屏幕模组上集成了FT5206电容触摸控制器触摸信号通过I2C接口引出显示接口是RGB888模组自带背光驱动电路只需要一个背光使能脚。整体框图很简单FPGA内部用PLL把50MHz时钟转成9MHz像素时钟送给LCD驱动模块生成行场同步和有效区信号图像生成模块根据当前扫描位置输出颜色数据触摸读取模块通过I2C定时读取FT5206的坐标寄存器把触摸坐标送进图像生成模块画一个光标。最后所有信号通过引脚约束连到屏幕模组。1.3 为什么这套方案推荐用Verilog来写有人可能会问这种RGB屏为什么不用现成的HDMI转RGB芯片、或者直接用带LCD控制器的MCU一个很重要的原因是RGB屏的行场时序和像素时钟千变万化FPGA可以做到精确到单时钟周期的控制。用Verilog写一方面能让你彻底看清显示时序的本质另一方面后续如果要做图像缩放、边缘检测、OSD叠加这些都是在像素流上做文章用硬件描述语言非常自然。而且Verilog写出来的驱动代码可移植性也强。同一个LCD驱动模块换成800×480的屏幕只需要改几个参数换成其他分辨率的RGB屏调整有效区和同步参数就行。这种“一套逻辑适配多块屏”的体验是MCU工程很难给你的。2. 时序与硬件先搞懂像素时钟和引脚连接再写代码2.1 行场同步和有效区到底谁先谁后RGB屏扫描的核心就是“行场同步”。想象屏幕左上角有一个光标它先从左上角开始一个像素一个像素向右移动每移动一格就需要一个像素时钟。等到光标到达这一行最右边屏幕会进入一小段“行消隐”时间这时候HSYNC拉低或者拉高取决于屏的极性通知屏幕“我要换行了”。然后光标回到下一行的最左边继续扫描。一行扫完就轮到VSYNC出场。当最后一行的最右边扫完之后VSYNC拉低通知屏幕“这一屏扫完了”接下来是场消隐时间光标回到左上角开始下一帧。整个过程中DE数据使能信号只在一行中的“有效像素区”为高告诉屏幕“现在数据线上的颜色是真实有效的”。消隐期间DE为低数据线可以随意。我们常说的“前肩”“后肩”“同步脉宽”其实就是同步信号和有效区之间的等待时间。这些等待时间不能省因为你不知道屏幕内部电路需要多少时间才能安全地把光标从右挪到左、从底挪到顶。具体数值以屏幕规格书为准我用的这块4.3寸屏比较常规行同步脉宽41个时钟行前肩2个时钟行后肩2个时钟场同步脉宽10行场前肩2行场后肩2行。2.2 像素时钟9MHz是怎么算出来的计算像素时钟可以套一个很直观的公式像素时钟 ≈ 帧率 × 每行总像素 × 总行数。其中每行总像素等于有效区、前肩、同步、后肩相加总行数同理。以480×27260Hz为例水平总长度4802412525垂直总行数2722102286那么像素时钟就是525×286×60≈9MHz。这个9MHz就是这么来的。有的屏时序参数稍有差异算出来可能是8.5MHz或者9.2MHz问题不大PLL不一定要锁到整数倍只要在这个范围内屏幕就能正常工作。我实际工程里用PLL把50MHz倍频得到9MHz像素时钟。如果你手头没有PLL也可以用一个简单的计数器把50MHz分频成8.33MHz50/6虽然频率低了一点但多数屏也能显示。正式项目我建议还是走PLL让像素时钟精确满足屏的规格。2.3 4.3寸屏引脚与FPGA连接表拿到屏幕模组先别急着接线把模组上的丝印和规格书信号名核对一遍。我这块屏的接口大致如下信号名方向功能说明FPGA侧建议R[7:0] G[7:0] B[7:0]输出RGB颜色数据普通IO口三态配置CLK输出像素时钟9MHz普通IO最好靠近数据线HSYNC输出行同步信号普通IOVSYNC输出场同步信号普通IODE输出数据使能普通IOBL输出背光使能接高电平或PWMSDA双向触摸I2C数据开漏带上拉SCL输出触摸I2C时钟开漏带上拉TP_RST输出触摸复位接高或控制脚TP_INT输入触摸中断可选查询方式可不连接线时注意几个细节RGB数据线数量多最好用杜邦线逐一对应避免交叉I2C的SDA和SCL一定要接上拉电阻一般模组自带了没有的话在板上加4.7kΩ背光是高电平点亮别一上来就接错信号导致屏幕不亮还以为程序有问题。触摸部分的RST可以先直接接高电平让触摸芯片处于正常供电状态。3. 显示链路从0到1LCD驱动、彩条和光标3.1 LCD驱动模块用计数器还原整屏扫描过程显示链路的核心是一个LCD驱动模块。它的任务就是跑两个计数器一个水平计数器h_cnt一个垂直计数器v_cnt然后用组合逻辑判断当前是否处于有效区、同步信号该拉低还是拉高。下面是我用的lcd_driver.v完整代码参数部分可以直接改module lcd_driver ( input wire clk, input wire rst_n, input wire [23:0] pixel_data_in, output reg [23:0] pixel_data_out, output reg lcd_hs, output reg lcd_vs, output reg lcd_de, output reg [9:0] h_cnt, output reg [9:0] v_cnt ); localparam H_ACTIVE 480; localparam H_FP 2; localparam H_SYNC 41; localparam H_BP 2; localparam V_ACTIVE 272; localparam V_FP 2; localparam V_SYNC 10; localparam V_BP 2; // 总长度 localparam H_TOTAL H_ACTIVE H_FP H_SYNC H_BP; // 525 localparam V_TOTAL V_ACTIVE V_FP V_SYNC V_BP; // 286 // 水平、垂直计数 always (posedge clk or negedge rst_n) begin if (!rst_n) begin h_cnt 10d0; v_cnt 10d0; end else begin if (h_cnt H_TOTAL - 1) begin h_cnt 10d0; if (v_cnt V_TOTAL - 1) v_cnt 10d0; else v_cnt v_cnt 1b1; end else begin h_cnt h_cnt 1b1; end end end // HSYNC 和 VSYNC我这里按低有效设计 always (posedge clk or negedge rst_n) begin if (!rst_n) begin lcd_hs 1b1; lcd_vs 1b1; end else begin if (h_cnt H_SYNC) lcd_hs 1b0; else lcd_hs 1b1; if (v_cnt V_SYNC) lcd_vs 1b0; else lcd_vs 1b1; end end // 有效区判断 wire h_active (h_cnt H_SYNC H_BP) (h_cnt H_SYNC H_BP H_ACTIVE); wire v_active (v_cnt V_SYNC V_BP) (v_cnt V_SYNC V_BP V_ACTIVE); // DE 信号 always (posedge clk or negedge rst_n) begin if (!rst_n) lcd_de 1b0; else lcd_de h_active v_active; end // 数据输出只在有效区透传 always (posedge clk or negedge rst_n) begin if (!rst_n) pixel_data_out 24b0; else if (h_active v_active) pixel_data_out pixel_data_in; else pixel_data_out 24b0; end endmodule这里有一个很容易踩的坑h_cnt和v_cnt刚计数到消隐区的时候有效区为假但是DE和数据输出的寄存器是打了一拍的所以HS、VS、DE、DATA之间会存在一个时钟周期的相位差。只要保持所有输出信号都是从reg里出去的相位差是统一的屏幕通常不会出问题。千万别让DE走组合逻辑直接输出、数据却从reg输出那样相位对不上画面会怪怪的。3.2 图像生成模块彩条测试与中心十字光标LCD驱动模块只负责把pixel_data_in搬到屏幕上至于屏幕显示什么由另一个模块决定。我写了一个image_gen模块它根据当前扫描到的行列坐标生成四种颜色横条同时接收触摸模块的坐标在对应位置画十字光标。module image_gen ( input wire clk, input wire rst_n, input wire [9:0] h_cnt, input wire [9:0] v_cnt, input wire touch_valid, input wire [11:0] touch_x, input wire [11:0] touch_y, output reg [23:0] pixel_data ); localparam H_ACTIVE_START 43; // H_SYNC H_BP localparam V_ACTIVE_START 12; // V_SYNC V_BP wire h_valid (h_cnt H_ACTIVE_START) (h_cnt H_ACTIVE_START 480); wire v_valid (v_cnt V_ACTIVE_START) (v_cnt V_ACTIVE_START 272); wire [9:0] px h_cnt - H_ACTIVE_START; wire [9:0] py v_cnt - V_ACTIVE_START; always (posedge clk or negedge rst_n) begin if (!rst_n) begin pixel_data 24b0; end else if (h_valid v_valid) begin // 四色横条方便判断数据线是否接对 if (py 68) pixel_data 24hFF0000; // 红 else if (py 136) pixel_data 24h00FF00; // 绿 else if (py 204) pixel_data 24h0000FF; // 蓝 else pixel_data 24hFFFFFF; // 白 // 在触摸坐标位置叠加十字光标 if (touch_valid) begin if ((px touch_x - 5 px touch_x 5 py touch_y) || (py touch_y - 5 py touch_y 5 px touch_x)) pixel_data 24hFFFF00; // 黄色 end end else begin pixel_data 24b0; end end endmodule仔细看会发现在四色横条的基础上加了十字光标颜色直接覆盖为黄色。这里px和py是相对有效区的坐标范围分别是0到479、0到271正好对应屏幕的480×272。如果触摸坐标需要对X或Y方向做翻转就在这个模块里转换一下再比较不要在触摸底层乱改。3.3 顶层接线与时钟生成顶层模块把上面两个模块和PLL串起来。我用的是Vivado的Clocking Wizard IP输入50MHz输出9MHz名称叫clk_wiz_0。如果你的开发板是其他品牌用对应的PLL IP或者手动分频都行。module top_lcd_touch ( input wire clk_50m, input wire rst_n, output wire lcd_clk, output wire lcd_hs, output wire lcd_vs, output wire lcd_de, output wire lcd_bl, output wire [23:0] lcd_rgb, inout wire tp_sda, output wire tp_scl, output wire tp_rstn ); wire clk_pix; wire locked; wire [9:0] h_cnt; wire [9:0] v_cnt; wire [23:0] pixel_data; wire [11:0] touch_x; wire [11:0] touch_y; wire touch_valid; clk_wiz_0 u_clk_wiz ( .clk_in1 (clk_50m), .reset (~rst_n), .locked (locked), .clk_out1(clk_pix) ); lcd_driver u_lcd_driver ( .clk (clk_pix), .rst_n (rst_n locked), .pixel_data_in (pixel_data), .pixel_data_out(lcd_rgb), .lcd_hs (lcd_hs), .lcd_vs (lcd_vs), .lcd_de (lcd_de), .h_cnt (h_cnt), .v_cnt (v_cnt) ); image_gen u_image_gen ( .clk (clk_pix), .rst_n (rst_n locked), .h_cnt (h_cnt), .v_cnt (v_cnt), .touch_valid (touch_valid), .touch_x (touch_x), .touch_y (touch_y), .pixel_data (pixel_data) ); touch_ctrl u_touch_ctrl ( .clk (clk_50m), .rst_n (rst_n locked), .sda (tp_sda), .scl (tp_scl), .touch_valid (touch_valid), .touch_x (touch_x), .touch_y (touch_y) ); assign lcd_clk clk_pix; assign lcd_bl rst_n; assign tp_rstn rst_n; endmodulePLL锁定之前FPGA的时钟输出还不稳定所以我用locked信号作为所有模块的复位条件之一避免屏幕抓到乱七八糟的数据。这个细节特别重要很多第一次做PLL的哥们儿直接忽略了locked结果屏幕偶尔闪屏查半天查不到。4. 触摸链路从0到1I2C读FT5206坐标4.1 FT5206寄存器初体验0x02、0x03、0x05RGB屏显示正常之后接下来就是把触摸数据读回来。触摸链路说复杂也复杂说简单也简单本质就是一个I2C从机设备的寄存器读取操作。FT5206是这颗触摸控制器的核心它通过I2C接口对外提供触摸状态和坐标。常用的寄存器有这几个寄存器地址含义说明0x00设备ID读取到0xA8左右说明I2C通信正常0x02触摸点状态低4位表示当前有效触摸点数0表示无触摸0x03触摸点1 X高字节12位坐标的高8位0x04触摸点1 X低字节12位坐标的低4位在高4bit0x05触摸点1 Y高字节12位坐标的高8位0x06触摸点1 Y低字节12位坐标的低4位在高4bit设备地址是0x387位地址。I2C通信时写操作发送的8位地址是0x700x38左移1位再或0读操作发送0x71。这也是初学者最容易懵的地方7位地址和8位地址经常搞混。以一次读取为例先发送起始条件接着发送0x70并等待ACK然后发送寄存器地址0x02紧接着重新发送一个起始条件重复起始再发送0x71并等待ACK随后连续读取6个字节分别是0x02、0x03、0x04、0x05、0x06、0x07的寄存器内容。前5个字节读完后要回ACK最后一个字节回NACK最后发送停止条件。4.2 I2C读流程从START到连续读6个字节我习惯把I2C读序列分成几个状态来写代码结构更清晰。下面是核心状态机的伪代码级别描述IDLE等待触摸读取启动信号每隔10ms触发一次。START产生起始条件此时SCL保持高SDA从高拉低。SEND_ADDR_W发送0x70等待从机ACK。SEND_REG发送寄存器地址0x02等待ACK。RESTART再次产生起始条件。SEND_ADDR_R发送0x71等待ACK。READ_BYTE连续读6字节每字节8位。前5字节发ACK最后一字节发NACK。STOP产生停止条件SDA从低拉高SCL保持高。PARSE把读回的寄存器拼接成坐标送入图像生成模块。坐标拼法是X (0x03寄存器值 4) | (0x04寄存器值的高4位)Y同理。这里注意0x04寄存器的低4位是无效的只有高4位有效。代码里可以用按位与和移位处理。4.3 触摸状态机代码片段与完整工程说明因为完整I2C控制器的代码比较长我这里把最关键的读坐标状态机部分整理出来。如果你的目标是赶紧跑起来也可以直接用FPGA厂商自带的I2C IP或者把下面这套状态机扩展成完整模块。// 触摸读取状态机核心结构 localparam IDLE 5d0; localparam START 5d1; localparam ADDR_W 5d2; localparam ACK1 5d3; localparam REG_ADDR 5d4; localparam ACK2 5d5; localparam RESTART 5d6; localparam ADDR_R 5d7; localparam ACK3 5d8; localparam READ_LOOP 5d9; localparam STOP 5d10; localparam DONE 5d11; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; end else begin case (state) IDLE: if (read_trig) state START; START: state ADDR_W; ADDR_W: begin // 逐位发送0x70 if (bit_done) state ACK1; end ACK1: if (ack_done) state REG_ADDR; // ... 类似地推进 DONE: state IDLE; endcase end end真正的I2C位时钟生成建议用计数器做一个100kHz的SCL再在SCL低电平期间改变SDASCL高电平期间采样数据。不要天真地把SCL做成50MHz自由运行的方波否则SDA变化时序完全对不上。完整工程我放在文末资源区里面包含可综合的touch_ctrl.v、引脚约束XDC文件以及一个简单的仿真testbench。工程里触摸读取模块我做了双保险先读一次设备ID寄存器0x00如果读不到0xA8就打印一串ASCII警告方便排查I2C通信问题。5. 调试实录与避坑指南5.1 屏幕没反应先查供电、复位、背光屏幕完全黑屏是最容易让人慌的。其实你按顺序排查就很快先量背光引脚电压如果是高电平还黑说明背光电路有问题大概率是模组供电没接对然后看PLL锁定信号用逻辑分析仪抓一下像素时钟有没有最后看复位按键有没有抬起如果复位一直拉低所有模块都停在复位态自然什么都不显示。我在第一次上板时遇到一个奇怪现象屏幕背光亮了但没有任何颜色。排查了很久发现是顶层把lcd_rgb接到了24位数据但模组实际只接了RGB666低两位悬空。这事本身不影响显示因为高6位数据没错。真正的问题是DE信号没接屏幕不知道什么时候才算有效数据整个画面全黑。所以提醒一下DE不是可有可无的大多数RGB屏都用DE作为数据有效判定务必确认引脚已经连到FPGA。5.2 画面不对从彩条颜色判断数据线接错如果你看到了彩条但颜色顺序不对比如红绿变成了绿红不用怀疑就是RGB数据线接反或者接错位了。用四色横条测试图案的好处就在这看到整屏是红色说明R通道数据正常看到绿色该在第二段却出现在第一段说明最可能G和R两组线交叉。还有一种常见问题是画面整体偏左或者偏右明明图案显示出来了但有半截跑到屏幕外。这个一般是行后肩设置不对。把H_BP调大画面会整体右移调小画面左移。你可以做一个实验在上电后按一下按键让H_BP在2、4、8这三个值之间切换观察画面位置这样就能理解后肩参数对画面偏移的影响。颜色数据位数也要小心。我的代码是24位RGB888但很多4.3寸屏模块是RGB666只有18根数据线。顶层例化的时候让高6位接到屏的R/G/B[5:0]低2位直接绑0这样降低位宽后颜色整体会偏淡一点但不会错乱。不要试图把24位硬塞给18位那样反而会错位。5.3 触摸读不到数据I2C上拉和时序是重灾区触摸部分最常见的坑是I2C没有上拉电阻。FT5206的SDA和SCL都是开漏输出必须外接上拉才能出来高电平。很多模组板上已经贴了上拉电阻但如果你是买的那种裸屏排线转接板很可能没有。测量方法很简单断电后量SDA对地电阻如果接近无穷大大概率没上拉。加上4.7kΩ电阻再试。第二个坑是地址搞错。寄存器读设备ID时如果发现读出来全是0xFF大概率是地址写错了。先确认FT5206的7位地址是0x38还是0x3A有些模组通过地址引脚拉高拉低来切换。我的模组是0x38如果你的读不到试着把设备地址改成0x3A试试。第三个坑和时序有关。I2C的SCL频率虽然规范上可以到400kHz但FT5206不少模组跑100kHz最稳。采样时机也很关键释放SDA后要等到SCL高电平稳定后再采样不能刚释放就立刻读否则采到的是SDA还没翻转完全的电平。我的状态机里专门用一个时钟周期的等待确保采样点落在SCL高电平的中间位置。5.4 坐标方向反了别急着改代码先判断正坐标触摸坐标读回来之后你会发现一个有意思的现象手指往下滑屏幕上的光标反而往上跑。这是因为不同模组的触摸芯片Y轴方向和液晶显示坐标方向不一定一致。FT5206内部的坐标原点通常在左上角但有些模组因为排线方向不同实际上Y轴是反的。解决办法有很多最简单的是在image_gen模块里做坐标翻转例如把touch_y改为271减去touch_y再参与比较。这个操作不需要动触摸底层代码也不影响数据精度。我在调试时喜欢先做一个测试手势手指从左下角划到右上角如果光标方向和手指方向完全一致那就说明不需要翻转如果上下左右都反了就在顶层做一次坐标映射。别小看这一步坐标反了在正式项目里会造成极其糟糕的体验。还有一点FT5206单点模式上报的坐标有时候会轻微抖动尤其是手指停在屏幕上的时候光标会在两三个像素之间跳。如果你后续要做手写识别或者精确点击建议加一个简单的均值滤波连续采3次取中间值或者平均值。这个滤波在Verilog里用移位加加法就能实现不会消耗太多逻辑资源。我在工程里加了一版最简单的三点中值滤波效果很明显。最后再分享一点自己的调试心得整个项目从零到全部跑通我大概花了两天半。第一天卡在PLL的locked信号接入上第二天卡在I2C的ACK等待死循环上第三天下午才真正把触摸坐标和画面光标联动起来。回头来看这个项目最有价值的地方不在于最终效果多炫酷而在于它逼着你一次性接触了PLL时钟、计数时序、组合逻辑有效区判断、状态机通信、开漏总线仲裁这么多知识点。如果你也打算照着做一遍我有三个建议第一先把显示部分单独调通再碰触摸不要一开始就两条链路一起调试第二写代码的时候统一信号的时序确保DE、数据、行场同步都从寄存器输出避免组合逻辑和时序逻辑混搭第三I2C状态机里一定要加超时退出机制否则从机没ACK你的状态机会卡在等待里连仿真都测不下去。后面的扩展方向也很明确把彩色横条换成一张存在ROM里的图片就可以做成开机画面把触摸坐标转化成简单的点击区域判断就能做一个带GUI的菜单再加上文字字模模块就能显示汉字和数字。RGB屏这条路一旦走通后面很多界面类项目都会变得顺手很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑