这一篇在干嘛?
前面几篇我们点亮过数码管、点阵和字符液晶,这一次要对付的是一块真正的彩色屏——2.8 寸 TFT 液晶。本实验把扩展彩屏模块插到开发板 JP1 上,用 FPGA 通过 16 位并行接口向屏幕写入颜色数据,最终在屏幕上显示出一组竖向彩条。实验本身逻辑不复杂,重点在于理解 TFT 彩屏的「显存 + 命令」控制方式,以及那一长串引脚约束的含义。
实验目标与原理
实验目的
通过 2.8 寸彩屏显示彩条。彩条就是屏幕上按顺序排列的红、绿、蓝等纯色竖条,它是显示驱动里最经典的「Hello World」:只要每一条颜色都对、边界都整齐,就说明数据线、控制时序、显存写入全部正常,任何一条数据线接错或接反,彩条立刻会花掉,所以它也常被当作排障工具。
彩屏是怎么被「点亮」的
先建立一个整体认知。2.8 寸 TFT 彩屏模组的内部结构大致分三层:
- 玻璃基板与像素阵列:每个像素由红、绿、蓝三个子像素组成,子像素的亮度由加在它上面的电压决定;
- 驱动 IC(控制器):屏上通常绑定了一颗 TFT 控制芯片(这一类 2.8 寸屏常见的是 ILI9341 一类的驱动 IC),它内部有一块显存(GRAM),并负责按扫描节奏把显存内容刷新到玻璃上;
- 对外接口: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 并口驱动要写三段逻辑:
- 初始化状态机:上电后先拉低
tft_reset_n保持一段时间再释放,然后按驱动 IC 手册把初始化命令逐条发出——每条命令是「拉低tft_dcx,把命令码放上总线,打一个tft_wrx写脉冲」,命令后跟的参数则把tft_dcx拉高再写; - 显存写入:初始化完成后发一条「开始写GRAM」的命令(如 2CH),之后
tft_dcx一直保持高电平,连续把像素数据打上总线即可,显存地址指针会由驱动 IC 自动递增; - 彩条生成:根据当前写入的像素列号判断颜色区间,例如 0
39 列给红色79 列给绿色、80~119 列给蓝色……依次循环,就得到等宽彩条。16'hF800、40
彩条颜色用 RGB565 表示时,几个典型值是:纯红 16'hF800、纯绿 16'h07E0、纯蓝 16'h001F、纯白 16'hFFFF、纯黑 16'h0000。判断屏幕显示是否正确时,记住这几个值对应的颜色即可。
常用 RGB565 颜色速查:
| 颜色 | R(5位) | G(6位) | B(5位) | 16 位值 |
|---|---|---|---|---|
| 黑 | 00000 | 000000 | 00000 | 0x0000 |
| 蓝 | 00000 | 000000 | 11111 | 0x001F |
| 绿 | 00000 | 111111 | 00000 | 0x07E0 |
| 红 | 11111 | 000000 | 00000 | 0xF800 |
| 黄 | 11111 | 111111 | 00000 | 0xFFE0 |
| 白 | 11111 | 111111 | 11111 | 0xFFFF |
由于本篇对应的官方例程以工程文件形式提供,原理图即上面的框图,源码在工程包内,这里不再展开完整代码;动手时建议先读例程中的初始化命令列表,对照驱动 IC 手册看每条命令的作用,收获会大得多。
上板流程
- 将 2.8 寸彩屏扩展模块插在开发板 JP1 上,注意方向,不要错位硬插;
- 插好下载线,打开电源开关;
- 在安路 TD 中打开例程工程,编译并下载程序至核心板内;
- 下载完成后背光点亮,屏幕上显示出彩条信号:

现象与总结
正常现象是屏幕出现一组颜色纯净、边界整齐的竖向彩条。如果现象不对,可以按下面的思路排查:
- 屏幕全白或全黑:先看背光
tft_bl是否被驱动为高;再确认tft_reset_n复位时序是否足够长。复位没做完就发命令,驱动 IC 会忽略后面的所有写入; - 颜色错乱、彩条花屏:大概率是数据线顺序接错,逐位核对
tft_d_inout的引脚约束; - 只显示一部分:检查写使能
tft_wrx的脉冲宽度是否满足驱动 IC 的最小写周期要求; - 显示位置偏移:通常是初始化命令里扫描方向(行列地址计数方向)配置与预期不符,改用其他 Gate/Source 扫描方向的命令组合即可。
一个好用的验证技巧:把彩条发生器改成「每个颜色区间只显示一行像素」的网格图案,可以让数据线错位的问题暴露得更明显——单根数据线接错时,网格会出现规律的竖线缺失。
这个实验的价值在于打通了「FPGA → 并口总线 → 带 MCU 接口的外设」这条最常见的驱动路径。掌握了它,再去做摄像头屏、触摸屏、其他型号的 TFT,都只是换一套初始化命令表而已。
自测
自测
tft_dcx信号的作用是什么?FPGA 发命令和发数据时它分别是什么电平?- RGB565 格式下一个像素占多少位?纯红色对应的 16 位数值是多少?
- 本实验中
tft_rdx是否被用到?为什么只写不读通常不影响显示?- 下载程序后屏幕全白,应优先检查哪两个信号?
- 为什么
tft_d_inout引脚约束中把驱动强度设为 8 并加上拉?