简介:针对FPGA开发者和数字系统设计学习者,该工程完整演示了在Xilinx Artix-7 XC7A35T上使用Verilog HDL驱动LCD触摸屏的实现方案,覆盖LCD控制器接口、触摸屏I2C/SPI通信、显示缓冲区管理以及主控状态机等核心模块。压缩包共228个文件,约3.73MB,主要包含Verilog/VHDL源码(v、vh、vhdl)、XDC约束、DCP网表、BIT比特流、Tcl脚本以及仿真与综合报告,可还原从代码编写到上板验证的完整流程。目前已有102人学习下载。开发者可重点研读LCD初始化时序、像素数据传输、中断响应与坐标解码逻辑,并借助BIT文件直接查看最终实现效果。工程结构清晰,适合作为FPGA驱动外部设备的入门参考或课程设计蓝本。
1. XC7A35T驱动LCD触摸屏:两条总线并行,Verilog先把时序放对位置
FPGA驱动LCD触摸屏和单片机点屏不一样:单片机往往借助现成的LCD控制器和硬件SPI/I2C接口,FPGA则要自己用Verilog把像素时钟、行场同步、数据总线以及触摸芯片的坐标回读一点一点对齐。很多从FPGA入门项目里跑过流水灯、数码管的开发者,下一步想做图像显示时,卡住的往往不是代码量,而是“这块屏幕到底该按什么时序送数据”。这里要意识到:屏幕显示和触摸面板是两套独立硬件,RGB接口负责画面,I2C或SPI接口负责坐标,触摸屏模块只是把两个器件物理组装在一起,回读路径完全是另一条总线。
带触摸的LCD在FPGA项目里通常承担交互屏角色,适合做图像处理参数配置、控制面板或调试工具。XC7A35T作为Artix-7系列里逻辑资源比较均衡的型号,BRAM总量有限,放几个小尺寸色条可以,直接缓存整幅800x480 RGB565会明显紧张,所以一般配合实时生成图像或外挂DDR颗粒。真正决定成败的是两个细节:像素时钟频率是否由屏的手册算出,以及DE信号和RGB像素数据是否对齐。下面按显示侧、触摸侧、约束、调试四部分展开,所有代码都能在Vivado里直接综合到XC7A35T工程里。
2. RGB LCD显示侧:确定像素时钟,再用Verilog计数器生成行场时序
2.1 先按屏手册计算像素时钟,常见分辨率参数表
拿到带触摸的LCD模组后,第一步不是写Verilog,而是找屏幕规格书里的“Timing Characteristics”页。RGB接口屏没有内部寄存器可配置,上电后只要在DE有效期间把像素数据送到总线,屏幕就会点亮;但像素时钟、行场前肩/同步/后肩参数必须和屏模组匹配,差太多会出现横移、闪动或完全没图像。
| 分辨率 | 典型像素时钟 | 常见颜色深度 | 接口形式 |
|---|---|---|---|
| 480x272 | 9 ~ 11 MHz | RGB666 / RGB565 | 并口 RGB |
| 800x480 | 33 ~ 40 MHz | RGB888 / RGB565 | 并口 RGB |
| 1024x600 | 50 ~ 62 MHz | RGB888 | 并口 RGB,部分屏用LVDS |
像素时钟的计算公式是像素时钟 = H_Total x V_Total x 刷新率,其中H_Total = H_Active + H_FrontPorch + H_SyncPulse + H_BackPorch,V方向同理。以800x480@60Hz为例,如果H_Total是1056,V_Total是525,像素时钟就是1056 x 525 x 60 ≈ 33.26MHz。很多教程直接写33.3MHz,实际使用时以屏手册给的PCLK范围为准,偏差在几个MHz以内通常能出图,但同步信号宽度要跟手册走。
常用RGB屏的同步极性有正有负,DE模式则只要求DE为高时数据有效。不要把单片机驱动MCU屏时常用的“先发命令、再发数据”习惯带进来,RGB屏没有命令通道,FPGA只需要持续输出像素流。
2.2 用两个计数器生成HSYNC/VSYNC/DE,Verilog实现
行场时序可以统一用两个计数器完成:h_cnt在每个像素时钟加一,走到整行末尾归零并让v_cnt加一;v_cnt走到整帧末尾归零,形成一帧周期。下面这段代码是典型的VGA-like时序生成器,参数都可参数化:
module lcd_rgb_timing #( parameter H_ACTIVE = 800, parameter H_FP = 40, parameter H_SYNC = 48, parameter H_BP = 40, parameter V_ACTIVE = 480, parameter V_FP = 13, parameter V_SYNC = 3, parameter V_BP = 29 )( input wire pclk, input wire rst_n, output reg hs_v, output reg vs_v, output reg de, output wire [10:0] h_cnt, output wire [10:0] v_cnt ); reg [10:0] h_count, v_count; wire h_act = (h_count < H_ACTIVE); wire v_act = (v_count < V_ACTIVE); always @(posedge pclk or negedge rst_n) begin if (!rst_n) begin h_count <= 0; v_count <= 0; end else if (h_count < H_ACTIVE + H_FP + H_SYNC + H_BP - 1) begin h_count <= h_count + 1; end else begin h_count <= 0; if (v_count < V_ACTIVE + V_FP + V_SYNC + V_BP - 1) v_count <= v_count + 1; else v_count <= 0; end end // 同步脉冲位于前肩结束后,后肩开始前 always @* begin hs_v = (h_count >= H_ACTIVE + H_FP) && (h_count < H_ACTIVE + H_FP + H_SYNC); vs_v = (v_count >= V_ACTIVE + V_FP) && (v_count < V_ACTIVE + V_FP + V_SYNC); de = h_act && v_act; end assign h_cnt = h_count; assign v_cnt = v_count; endmodule这里生成的是正极性HSYNC/VSYNC,如果屏手册要求负极性,把输出取反即可。DE模式屏可以不接HS/VS信号,只靠DE高电平期间送像素数据;但代码里仍然保留同步信号,便于调试和兼容需要同步信号的屏幕。注意h_count的宽度至少要能装下H_ACTIVE + H_FP + H_SYNC + H_BP - 1,本代码用[10:0],对1056这种值没有问题。
像素数据输出侧可以接一个简单彩条,先解决“有没有画面”的问题:
always @(posedge pclk) begin if (de) begin if (v_cnt < V_ACTIVE / 2) lcd_rgb <= 16'hF800; // 上半屏红色 else lcd_rgb <= 16'h07E0; // 下半屏绿色 end else begin lcd_rgb <= 16'h0000; end end如果准备做FPGA图像处理,真正的帧缓存通常放在Block RAM或DDR颗粒里,由AXI侧的读写逻辑维护缓存索引和重组,LCD控制器只固定频率地读取像素流,不要在显示时序模块里同时做数据搬运和缓存索引。这样可以避免调试时分不清问题是出在同步信号还是缓存逻辑。
2.3 屏幕接口不止RGB,LVDS和MIPI屏要单独处理
XC7A35T的普通IO可以直接驱动RGB/TTL接口的LCD,这是最简单的接法。LVDS屏需要把RGB并行数据串行化,一般用原语OBUFDS输出差分对,PCB上也要按差分阻抗走线,不是把信号线一对一接到FPGA就能点亮。MIPI DSI屏更复杂,需要有DDR时钟通道、协议打包和Lane均衡,Artix-7没有MIPI DSI硬核,用OSERDES强行实现MIPI物理层可以,但协议层工作量远大于写一个RGB时序控制器。
所以选择“XC7A35T驱动LCD触摸屏”这个方案时,我一般建议优先选并行RGB接口的屏,触摸部分优先选I2C接口的电容屏。触控IC的I2C频率一般在100kHz到400kHz,XC7A35T普通IO就能应付,关键是状态机里要把ACK/NACK处理清楚。
3. 触摸屏Verilog读取:I2C状态机、坐标解析与中值滤波
3.1 先确认触摸IC类型,电容屏通常走I2C
屏幕上的触摸部分常见有两种:电阻屏一般使用XPT2046这类SPI接口ADC,靠电压变化计算坐标;电容屏多用FT5x06、GT911这类自带触控算法的芯片,通过I2C接口读出坐标。带触摸的LCD模组里出现频率更高的是后者,因为电容屏已经是消费级方案默认选择。下面对应的是I2C电容屏。
以某颗FT系列的电容触控IC为例,I2C从设备地址可以通过引脚拉高/拉低选择,通常是0x38或0x39。触摸点数据放在寄存器区,流程是先写寄存器地址,再发起重复起始信号,随后连续读回状态字节和坐标字节。
| 寄存器区域 | 含义 | 说明 |
|---|---|---|
| 0x00 ~ 0x02 | 设备模式、手势寄存器、触点数量 | 触点数大于0说明有手指按下 |
| 0x03 ~ 0x04 | 第一个触摸点X坐标高/低字节 | 12位坐标时高字节只有低4位有效 |
| 0x05 ~ 0x06 | 第一个触摸点Y坐标高/低字节 | 部分IC把XY顺序反过来 |
| 0x07 ~ 0x0A | 第二个触摸点坐标 | 单点交互时可以不读 |
不同IC的寄存器偏移差异很大,GT911甚至需要读0x814E附近的连续坐标缓冲区。写Verilog之前,先确认真实芯片型号,再按手册的寄存器区组织状态机。
3.2 I2C读取状态机:最关键的ACK采样逻辑
在FPGA里实现I2C从设备读取,不需要把整个总线协议封装得很复杂,只要能完成“写寄存器地址 -> 重复起始 -> 读多个字节 -> 停止”这一串动作即可。常见做法是维护一个半位计数器,CLK分频后控制SCL高低电平,SDA在SCL低电平期间变化,在SCL高电平期间采样。
下面是单字节读的关键状态机框架,实际工程可以在此基础上扩展成连续读多个字节:
localparam IDLE = 4'd0; localparam START = 4'd1; localparam SEND_REG = 4'd2; localparam ACK_REG = 4'd3; localparam RESTART = 4'd4; localparam SEND_RD = 4'd5; localparam ACK_RD = 4'd6; localparam READ_H = 4'd7; localparam READ_L = 4'd8; localparam SEND_NACK = 4'd9; localparam STOP = 4'd10; // 在每个SCL高电平中间采样SDA wire sample_en = (scl_counter == CLK_DIV); always @(posedge clk or negedge rst_n) begin if (!rst_n) ack_flag <= 1'b1; else if (sample_en) begin ack_flag <= sda_in; // 0表示ACK,1表示NACK if (!sda_in) // ACK确认后继续 bus_state <= next_state; else bus_state <= STOP; // NACK直接结束 end end这段代码的关键是sample_en,它必须发生在SCL高电平的中间位置,不能刚拉高SCL就采样。I2C的SDA在SCL高电平期间必须保持稳定,所以用CLK分频后,SCL拉高和采样之间至少要留出半拍以上的稳定时间。实际参数设置里,CLK_DIV = CLK_FREQ / (I2C_FREQ * 2),比如50MHz主频配400kHz I2C,每个SCL半周期需要62.5个时钟,取CLK_DIV = 62即可。
很多第一次写触摸驱动的开发者会漏掉重复起始信号。写寄存器地址后不能直接发送读命令,要先在SCL高电平期间把SDA拉高,再拉低,生成一个重复起始条件,然后才能发送读方向地址。少了这个动作,触摸IC会一直等在寄存器地址等待状态,读回来的是寄存器地址本身而不是坐标数据。
3.3 坐标抖动怎么滤:中值滤波的FPGA写法
触摸坐标原始值通常有抖动,尤其是手指轻触边缘时,单点坐标可能在相邻几个像素间跳变。平均值滤波会拖慢响应,还会在手指出现在颜色分界区域时产生过渡坐标。中值滤波更适合触摸信号:能滤掉单点毛刺,又保留坐标阶跃变化。
module median3 #(parameter W = 12) ( input wire [W-1:0] a, b, c, output reg [W-1:0] mid ); wire [W-1:0] hi_ab = (a > b) ? a : b; wire [W-1:0] lo_ab = (a > b) ? b : a; wire [W-1:0] tmp = (c > hi_ab) ? hi_ab : (c < lo_ab ? lo_ab : c); always @* mid = tmp; endmodule把连续三次I2C读到的X坐标分别放进a、b、c,输出mid就是中间值。这个模块是纯组合逻辑,可以被三个寄存器例化三次,得到滤波后的X和Y。注意滤波不能代替手指释放判断:如果寄存器里的触点数量为0,应该直接输出无效坐标,而不是让滤波结果悬空。
4. 集成到XC7A35T工程:XDC引脚约束、MMCM像素时钟和开漏IO
4.1 XDC约束:PACKAGE_PIN、IOSTANDARD与bank电压
把Verilog代码变成能烧进XC7A35T的位流,约束文件比代码更容易出错。XDC里每个LCD信号都要指定物理引脚和电气标准,下面是一个示例片段,引脚位置占位,不能直接照抄:
set_property PACKAGE_PIN N18 [get_ports {lcd_rgb_o[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {lcd_rgb_o[0]}] set_property PACKAGE_PIN M21 [get_ports lcd_de_o] set_property IOSTANDARD LVCMOS33 [get_ports lcd_de_o] set_property PACKAGE_PIN E3 [get_ports touch_sda] set_property IOSTANDARD LVCMOS33 [get_ports touch_sda] set_property PULLUP true [get_ports touch_sda] set_property PACKAGE_PIN E5 [get_ports touch_scl] set_property IOSTANDARD LVCMOS33 [get_ports touch_scl]这里的PULLUP true很关键。触摸板的SDA和SCL是开漏总线,通常模块上已经有上拉电阻,但有些模组为了兼容不同主控把上拉电阻做得很小或省略。FPGA的IO在未设置时没有内部上拉,直接接I2C总线会导致SDA始终为低,状态机卡在ACK判断。引脚所在的Bank电压必须和屏幕电平一致,XC7A35T如果引脚所在Bank的VCCO接3.3V,设置IOSTANDARD LVCMOS33;如果屏幕是2.5V接口,则对应Bank必须接2.5V,否则IO的输入阈值不匹配。
4.2 用Clocking Wizard生成像素时钟,不要用普通计数器分频
RGB LCD对像素时钟的抖动要求虽然不如高速数传严格,但用计数器从50MHz分频得到非整数频率会带来明显相位噪声,画面可能出现缓慢水平漂移。Vivado里打开IP Catalog,搜索Clocking Wizard,输入系统时钟频率50MHz,输出像素时钟频率按屏手册填写,例如33.3MHz或10MHz,相位默认0,其他选项保持默认。生成的MMCM/PLL原语实例化代码如下:
clk_wiz_0 u_clk_wiz ( .clk_in1 (clk_50m), .clk_out1(pixel_clk), .resetn (rst_n) );Clocking Wizard会自动选择VCO频率和分频系数,比手工写PLLE2_BASE更安全。像素时钟生成后要先经过BUFG再送到时序模块,Vivado会在综合时自动插入全局时钟缓冲;不要在普通逻辑里对pixel_clk做门控或分频后直接驱动多个模块。
4.3 FPGA的IO有类似ARM的推挽和开漏模式吗:用IOBUF实现触摸I2C
很多从ARM转到FPGA的开发者会找GPIO配置寄存器里的推挽/开漏选项,Artix-7的普通IO实际上没有ARM那种软件可配的推挽/open-drain模式,但可以通过三态缓冲器实现。I2C要求SDA可被外设拉低,读数据时主机释放总线,用IOBUF原语最直接:
IOBUF #( .IOSTANDARD("LVCMOS33"), .DRIVE(4), .SLEW("SLOW") ) touch_sda_io ( .I (1'b0), // 输出数据固定为0,模拟开漏低电平 .T (sda_oe_n), // 1=高阻释放总线,0=输出低电平 .O (sda_in), // 采样到的总线电平 .IO(touch_sda) );当sda_oe_n=1时,IO引脚为高阻态,外部上拉把总线拉高;当sda_oe_n=0时,引脚输出低电平,把SDA拉低。这样就和ARM的GPIO开漏行为完全一致。SCL因为是主机输出,可以直接用普通输出引脚驱动,不需要开漏。
5. 调试技巧:用ILA同时看LCD时序和触摸I2C,先把屏点亮再谈坐标
5.1 用ILA抓行场同步和DE的对应关系
Vivado的ILA是集成逻辑分析仪,调试LCD时序时比示波器方便得多。在综合前给关键信号加上(* mark_debug = "true" *)属性,或者在综合后打开网表添加probe。ILA的时钟直接用pixel_clk,probe建议至少包含h_cnt、v_cnt、de、hs_v、vs_v和触摸I2C状态机当前状态:
ila_0 u_ila ( .clk (pixel_clk), .probe0(h_cnt), .probe1(v_cnt), .probe2({de, hs_v, vs_v}), .probe3(i2c_state), .probe4(sda_in) );不插线也能看内部信号,这是FPGA调试比单片机方便的地方。触发条件设置为de下降沿,触发位置适中,单次触发捕获一帧中的有效区、前肩和同步区。如果h_cnt在有效区内不是连续递增到预设值,说明行周期参数和屏实际行总周期不一致;如果v_cnt每行的递进位置和h_cnt归零位置错开,说明计数器条件写错,通常是复位时序或比较边界差1。
5.2 用彩条/网格定位触摸坐标换算关系
屏幕点亮后,先把触摸坐标映射到LCD分辨率。电容触摸IC输出的坐标分辨率通常和屏幕物理分辨率不一致,常见的是0到1023范围映射到800x480区域。换算公式是lcd_x = touch_x * LCD_W / TD_MAX_X。如果TD_MAX_X是1024,LCD_W是800,可以约分成lcd_x = touch_x * 25 / 32,乘法后右移5位即可,避免在FPGA里做除法。
更有效的做法是在屏幕上画网格,把每个网格的颜色区分开,手指按下去后用触摸坐标换算成颜色值,再和屏幕实际位置比较。如果整个坐标系有固定偏移,说明触摸IC的原点和LCD显示原点不重合,在顶层做一个减法修正。这个偏移通常来自屏幕玻璃的贴装误差,量产板每一批可能都不一样,最好通过校准参数调整。
5.3 触摸I2C调试时先检查ACK和上拉,再来怀疑状态机
触摸I2C读不到坐标时,先看ILA里的sda_in波形。如果采样到的ACK一直为1,说明从设备没有响应,可能是设备地址错误,也可能是SDA没有上拉。如果ACK是0但坐标全为0xFF,说明重复起始信号之后没有正确切换为读方向,寄存器地址被当成数据发送了。仿真时记得在testbench里给sda加一个pullup,否则高阻态会显示为未定义态,状态机永远等不到正确的ACK,这是触摸调试里最容易被当成代码bug的问题。
本文还有配套的精品资源,点击获取