这一篇在干嘛?

前面几篇我们点亮过数码管、点阵和字符液晶,这一次要对付的是一块真正的彩色屏——2.8 寸 TFT 液晶。本实验把扩展彩屏模块插到开发板 JP1 上,用 FPGA 通过 16 位并行接口向屏幕写入颜色数据,最终在屏幕上显示出一组竖向彩条。实验本身逻辑不复杂,重点在于理解 TFT 彩屏的「显存 + 命令」控制方式,以及那一长串引脚约束的含义。

实验目标与原理

实验目的

通过 2.8 寸彩屏显示彩条。彩条就是屏幕上按顺序排列的红、绿、蓝等纯色竖条,它是显示驱动里最经典的「Hello World」:只要每一条颜色都对、边界都整齐,就说明数据线、控制时序、显存写入全部正常,任何一条数据线接错或接反,彩条立刻会花掉,所以它也常被当作排障工具。

彩屏是怎么被「点亮」的

先建立一个整体认知。2.8 寸 TFT 彩屏模组的内部结构大致分三层:

  1. 玻璃基板与像素阵列:每个像素由红、绿、蓝三个子像素组成,子像素的亮度由加在它上面的电压决定;
  2. 驱动 IC(控制器):屏上通常绑定了一颗 TFT 控制芯片(这一类 2.8 寸屏常见的是 ILI9341 一类的驱动 IC),它内部有一块显存(GRAM),并负责按扫描节奏把显存内容刷新到玻璃上;
  3. 对外接口:FPGA 只需要和驱动 IC 打交道,往它的显存里写数据、给它发命令即可。

理解了这一点,就能明白 FPGA 端的分工:FPGA 不直接控制像素,而是充当驱动 IC 的「写入器」。初始化时发一串配置命令(设定扫描方向、色彩格式、显示开关等),之后只管把像素颜色连续写进显存,屏幕自己会刷新。

接口信号怎么看

从引脚约束里可以反推出本实验用的接口类型。信号名里有 tft_d_inout[15:0],也就是 16 位双向数据总线;再加上 tft_wrx(写选通)、tft_rdx(读选通)、tft_dcx(数据/命令选择)、tft_csx(片选)、tft_reset_n(复位),这正是典型的 16 位 8080 系并行接口(也叫 i80 接口):

  • tft_dcx:低电平表示总线上是命令,高电平表示是数据。驱动 IC 内部有命令寄存器和数据寄存器,靠这一根线区分;
  • tft_wrx:写选通。数据在写选通的边沿被驱动 IC 采样进内部寄存器;
  • tft_rdx:读选通,FPGA 想回读显存或状态时使用,本实验只写不读;
  • tft_csx:片选,低电平期间驱动 IC 才监听总线;
  • tft_reset_n:硬件复位,上电后先复位一遍驱动 IC;
  • tft_bl:背光使能,拉高后背光点亮,否则屏上什么都看不见;
  • tft_d_inout[15:0]:16 位数据总线,一次传输一个 16 位的颜色值。

为什么用 16 位数据线?

同样是 8080 接口,驱动 IC 也支持 8 位模式,但那样写一个 RGB565 像素要分两次。16 位模式下一次写入正好是一个完整的 RGB565 颜色(红 5 位、绿 6 位、蓝 5 位),传输效率翻倍,对 FPGA 来说也只是把总线拉宽而已,所以彩屏模组把 16 根数据线都引了出来。

实验原理框图如下:

引脚分配

彩屏扩展模块要插在核心板的 JP1 扩展座上,占用了大量 IO。系统级信号有三个:时钟 clk、复位 reset_n 和背光 tft_bl;其余全部是 TFT 接口信号。完整的引脚约束脚本如下(先给出系统信号的约束,再给出 TFT 总线的约束):

set_pin_assignment { clk } { LOCATION = R7; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; } set_pin_assignment { reset_n } { LOCATION = A2; IOSTANDARD = LVCMOS33; PULLTYPE = PULLUP; } set_pin_assignment { tft_bl } { LOCATION = M10; IOSTANDARD = LVCMOS33; DRIVESTRENGTH = 8; PULLTYPE = NONE; }

set_pin_assignment { tft_csx } { LOCATION = R14; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { tft_d_inout[0] } { LOCATION = P16; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = PULLUP; }
set_pin_assignment { tft_d_inout[10] } { LOCATION = K15; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = PULLUP; }
set_pin_assignment { tft_d_inout[11] } { LOCATION = G14; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = PULLUP; }
set_pin_assignment { tft_d_inout[12] } { LOCATION = G16; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = PULLUP; }
set_pin_assignment { tft_d_inout[13] } { LOCATION = F15; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = PULLUP; }
set_pin_assignment { tft_d_inout[14] } { LOCATION = F14; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = PULLUP; }
set_pin_assignment { tft_d_inout[15] } { LOCATION = E15; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = PULLUP; }
set_pin_assignment { tft_d_inout[1] } { LOCATION = N14; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = PULLUP; }
set_pin_assignment { tft_d_inout[2] } { LOCATION = N16; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = PULLUP; }
set_pin_assignment { tft_d_inout[3] } { LOCATION = L16; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = PULLUP; }
set_pin_assignment { tft_d_inout[4] } { LOCATION = M15; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = PULLUP; }
set_pin_assignment { tft_d_inout[5] } { LOCATION = M16; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = PULLUP; }
set_pin_assignment { tft_d_inout[6] } { LOCATION = K16; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = PULLUP; }
set_pin_assignment { tft_d_inout[7] } { LOCATION = H16; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = PULLUP; }
set_pin_assignment { tft_d_inout[8] } { LOCATION = H15; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = PULLUP; }
set_pin_assignment { tft_d_inout[9] } { LOCATION = J16; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = PULLUP; }
set_pin_assignment { tft_dcx } { LOCATION = T15; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { tft_rdx } { LOCATION = R16; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { tft_reset_n } { LOCATION = P15; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }
set_pin_assignment { tft_wrx } { LOCATION = R15; IOSTANDARD = LVCMOS33;
DRIVESTRENGTH = 8; PULLTYPE = NONE; }

注意数据总线全部设置成了 DRIVESTRENGTH = 8(最大驱动强度)并带上拉,因为 16 根数据线要同时翻转,驱动能力不足会造成边沿变缓、采样出错。所有 IO 电平标准都是 LVCMOS33,与扩展座供电一致。

检查约束与工程端口名一一对应

这一批引脚数量多,约束脚本里任何一个信号名拼错(比如把 tft_d_inout[11] 写成 [10]),综合器都不会报错,但上板一定显示异常。建议直接使用官方例程自带的 tcl 约束脚本,不要手抄。

Verilog 实现与驱动思路

本实验的重点不在代码量,而在「先把时序想清楚」。一个典型的 TFT 并口驱动要写三段逻辑:

  1. 初始化状态机:上电后先拉低 tft_reset_n 保持一段时间再释放,然后按驱动 IC 手册把初始化命令逐条发出——每条命令是「拉低 tft_dcx,把命令码放上总线,打一个 tft_wrx 写脉冲」,命令后跟的参数则把 tft_dcx 拉高再写;
  2. 显存写入:初始化完成后发一条「开始写GRAM」的命令(如 2CH),之后 tft_dcx 一直保持高电平,连续把像素数据打上总线即可,显存地址指针会由驱动 IC 自动递增;
  3. 彩条生成:根据当前写入的像素列号判断颜色区间,例如 039 列给红色 16'hF800、4079 列给绿色、80~119 列给蓝色……依次循环,就得到等宽彩条。

彩条颜色用 RGB565 表示时,几个典型值是:纯红 16'hF800、纯绿 16'h07E0、纯蓝 16'h001F、纯白 16'hFFFF、纯黑 16'h0000。判断屏幕显示是否正确时,记住这几个值对应的颜色即可。

常用 RGB565 颜色速查:

颜色R(5位)G(6位)B(5位)16 位值
00000000000000000x0000
00000000000111110x001F
绿00000111111000000x07E0
11111000000000000xF800
11111111111000000xFFE0
11111111111111110xFFFF

由于本篇对应的官方例程以工程文件形式提供,原理图即上面的框图,源码在工程包内,这里不再展开完整代码;动手时建议先读例程中的初始化命令列表,对照驱动 IC 手册看每条命令的作用,收获会大得多。

上板流程

  1. 将 2.8 寸彩屏扩展模块插在开发板 JP1 上,注意方向,不要错位硬插;
  2. 插好下载线,打开电源开关;
  3. 在安路 TD 中打开例程工程,编译并下载程序至核心板内;
  4. 下载完成后背光点亮,屏幕上显示出彩条信号:

现象与总结

正常现象是屏幕出现一组颜色纯净、边界整齐的竖向彩条。如果现象不对,可以按下面的思路排查:

  • 屏幕全白或全黑:先看背光 tft_bl 是否被驱动为高;再确认 tft_reset_n 复位时序是否足够长。复位没做完就发命令,驱动 IC 会忽略后面的所有写入;
  • 颜色错乱、彩条花屏:大概率是数据线顺序接错,逐位核对 tft_d_inout 的引脚约束;
  • 只显示一部分:检查写使能 tft_wrx 的脉冲宽度是否满足驱动 IC 的最小写周期要求;
  • 显示位置偏移:通常是初始化命令里扫描方向(行列地址计数方向)配置与预期不符,改用其他 Gate/Source 扫描方向的命令组合即可。

一个好用的验证技巧:把彩条发生器改成「每个颜色区间只显示一行像素」的网格图案,可以让数据线错位的问题暴露得更明显——单根数据线接错时,网格会出现规律的竖线缺失。

这个实验的价值在于打通了「FPGA → 并口总线 → 带 MCU 接口的外设」这条最常见的驱动路径。掌握了它,再去做摄像头屏、触摸屏、其他型号的 TFT,都只是换一套初始化命令表而已。

自测