
简介针对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有效期间把像素数据送到总线屏幕就会点亮但像素时钟、行场前肩/同步/后肩参数必须和屏模组匹配差太多会出现横移、闪动或完全没图像。分辨率典型像素时钟常见颜色深度接口形式480x2729 ~ 11 MHzRGB666 / RGB565并口 RGB800x48033 ~ 40 MHzRGB888 / RGB565并口 RGB1024x60050 ~ 62 MHzRGB888并口 RGB部分屏用LVDS像素时钟的计算公式是像素时钟 H_Total x V_Total x 刷新率其中H_Total H_Active H_FrontPorch H_SyncPulse H_BackPorchV方向同理。以800x48060Hz为例如果H_Total是1056V_Total是525像素时钟就是1056 x 525 x 60 ≈ 33.26MHz。很多教程直接写33.3MHz实际使用时以屏手册给的PCLK范围为准偏差在几个MHz以内通常能出图但同步信号宽度要跟手册走。常用RGB屏的同步极性有正有负DE模式则只要求DE为高时数据有效。不要把单片机驱动MCU屏时常用的“先发命令、再发数据”习惯带进来RGB屏没有命令通道FPGA只需要持续输出像素流。2.2 用两个计数器生成HSYNC/VSYNC/DEVerilog实现行场时序可以统一用两个计数器完成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 16hF800; // 上半屏红色 else lcd_rgb 16h07E0; // 下半屏绿色 end else begin lcd_rgb 16h0000; end end如果准备做FPGA图像处理真正的帧缓存通常放在Block RAM或DDR颗粒里由AXI侧的读写逻辑维护缓存索引和重组LCD控制器只固定频率地读取像素流不要在显示时序模块里同时做数据搬运和缓存索引。这样可以避免调试时分不清问题是出在同步信号还是缓存逻辑。2.3 屏幕接口不止RGBLVDS和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到400kHzXC7A35T普通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 4d0; localparam START 4d1; localparam SEND_REG 4d2; localparam ACK_REG 4d3; localparam RESTART 4d4; localparam SEND_RD 4d5; localparam ACK_RD 4d6; localparam READ_H 4d7; localparam READ_L 4d8; localparam SEND_NACK 4d9; localparam STOP 4d10; // 在每个SCL高电平中间采样SDA wire sample_en (scl_counter CLK_DIV); always (posedge clk or negedge rst_n) begin if (!rst_n) ack_flag 1b1; else if (sample_en) begin ack_flag sda_in; // 0表示ACK1表示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像素时钟和开漏IO4.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 (1b0), // 输出数据固定为0模拟开漏低电平 .T (sda_oe_n), // 1高阻释放总线0输出低电平 .O (sda_in), // 采样到的总线电平 .IO(touch_sda) );当sda_oe_n1时IO引脚为高阻态外部上拉把总线拉高当sda_oe_n0时引脚输出低电平把SDA拉低。这样就和ARM的GPIO开漏行为完全一致。SCL因为是主机输出可以直接用普通输出引脚驱动不需要开漏。5. 调试技巧用ILA同时看LCD时序和触摸I2C先把屏点亮再谈坐标5.1 用ILA抓行场同步和DE的对应关系Vivado的ILA是集成逻辑分析仪调试LCD时序时比示波器方便得多。在综合前给关键信号加上(* mark_debug true *)属性或者在综合后打开网表添加probe。ILA的时钟直接用pixel_clkprobe建议至少包含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是1024LCD_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的问题。本文还有配套的精品资源点击获取