这一章在干嘛?
这是安路 FPGA 实战的第一课。我们先不碰硬件,而是把 TD 6.2.1 软件跑起来,打开官方 HDMI 播放例程,读懂顶层五大模块,看穿官方已经内置的「双缓冲」设计,最后用一个自己写的 testbench 完成第一次功能仿真。学完这一章,你就拿到了理解整条 TF 卡 → SDRAM → HDMI 链路的地图。
没有板子,也能学会八成
FPGA 开发有一半以上的工作发生在「看不见硬件」的阶段。先把这个阶段的能力边界讲清楚,你就不会觉得没板子就无从下手。
| 能做(现在就能做) | 不能做(等板子到手) |
|---|---|
| 读懂全部 RTL 代码 | JTAG 下载 bitstream 到芯片 |
| 打开 TD 工程、综合、布局布线 | 看 HDMI 屏幕上的真实画面 |
| 功能仿真验证时序逻辑 | 读 SD 卡的物理扇区 |
| 看 LUT / 寄存器 / 时钟资源报告 | 调 PLL 输出与引脚电平 |
先建立信心
本章要带你做的那几件事——打开工程、读懂模块、跑仿真——全部属于「能做」那一列。板子到手后,剩下的只是「下载 → 看画面 → 调参数」这三步。
第一步:打开 TD 工程
我们这套例子用的是安路 TD 6.2.1,配合官方提供的 HDMI 多媒体播放例程(TF 卡读图 + SDRAM 帧缓存 + HDMI 输出)。
- 启动 TD:双击安装目录下的
bin\td.exe(默认在C:\Anlogic\TD_6.2.1_Engineer_6.2.168.116\bin\)。 - 打开工程:
File → Open Project,选中例程里的工程文件lab_ex5_i2s_v1.0.al(文件名虽然叫lab_ex5_i2s,内容其实是 TF 卡读图 + HDMI 显示这一套)。 - 打开后先别急着综合,按顺序看左侧 Design 树,把顶层模块和它的子模块认全,这就是下一步要做的事。
注意命名
官方解压出来的工程文件名是
lab_ex5_i2s_v1.0.al,但它对应的顶层其实是 TF 卡 → HDMI 的显示链路,别被文件名误导。
例程代码结构:顶层五大模块
顶层模块 top_tf_hdmi_audio.v 扮演的是「导演」的角色,真正的演员是下面五个模块。认清这五个分工,整条数据流就通了:
| 模块 | 文件 | 职责 |
|---|---|---|
sd_card_bmp | SD/sd_card_bmp.v | SD 卡 SPI 读 + BMP 解析 + 多图扫描,产出写数据与双缓冲索引 |
frame_read_write | SD/frame_read_write.v | SDRAM 读写仲裁(内含异步 FIFO 跨时钟域) |
sdram | SD/sdram.v | 片内 SDRAM 控制器 |
video_timing_data + video_delay | SD/*.v | 产生 640×480 时序(hs/vs/de),从 SDRAM 读像素输出 |
hdmi_1_4b_transmitter_core_wrapper + hdmi_phy_wrapper | .enc.v | 加密 IP,负责 HDMI TMDS 串行发送 |
数据从左往右流:TF 卡 → sd_card_bmp(解析出像素)→ frame_read_write(写进 SDRAM)→ video_timing_data(按显示时序读出来)→ HDMI 发送核(变成屏幕上的画面)。
加密 IP 是黑盒
sd_card_bmp、HDMI 发送核和 SDRAM 控制器里的核心逻辑是官方给的加密 IP(.enc.v),你只能当黑盒调用。你未来真正要动的是中间那几个明文.v模块——frame_read_write(双缓冲仲裁)、video_timing_data(时序),以及你自己新加的 OSD / 特效模块。
核心洞察:双缓冲官方已经实现
这是本课最重要的发现,它直接决定你后面该往哪儿使劲。
很多初学者第一个想到的优化是「加双缓冲,避免屏幕闪烁」。但官方例程早就做好了。看顶层这几行:
parameter FRAME_PIXELS = 24'd307200; // 640*480
parameter BUF0_ADDR = 24'd0;
parameter BUF1_ADDR = FRAME_PIXELS; // 307200
...
.read_addr_index (disp_buf_idx), // 显示读哪个缓冲
.write_addr_index (write_buf_idx), // 写入写哪个缓冲
原理是一张乒乓图:SDRAM 里开两片 307200 × 32bit 的区域(BUF0 从地址 0 开始,BUF1 从地址 307200 开始)。写侧写满一帧后翻转 write_buf_idx,读侧在 VSYNC 场边界翻转 disp_buf_idx。两个索引各自独立翻转,保证显示器永远读的是「完整的一帧」,绝不会读到写了一半的画面。
所以你的学习重点应该从「如何自研双缓冲」转向「读懂官方这个双缓冲,再想怎么改」。这才是竞赛能拿分的地方:
- 把双缓冲升级成三缓冲;
- 给缓冲切换加淡入淡出过渡;
- 在
video_delay输出之后插入 OSD 叠加层; - 这些增量创新,比重造一个官方已有的轮子有价值得多。
三个时钟域与跨域握手
这个工程里跑着好几个不同频率的时钟,跨时钟域(CDC)是理解它的关键,也是出 bug 的高发区。
| 时钟 | 频率 | 用途 |
|---|---|---|
sd_card_clk | 100 MHz | SD 卡 SPI 读 + 写 SDRAM 的一侧 |
ext_mem_clk | 100 MHz | SDRAM 控制器的工作时钟 |
video_clk | 25.175 MHz | HDMI 读 SDRAM、产生显示时序 |
hdmi_5x_clk | 约 126 MHz | TMDS 串行化 |
不同的时钟域之间不能直接传递多比特信号,否则会亚稳态。官方在这里给了一个教科书级的单比特跨域写法:
// mem_clk 域把 write_finish 单拍转成 toggle,供 sd_card_clk 域可靠同步
always @(posedge ext_mem_clk or posedge rst_all)
if (frame_write_finish)
frame_write_toggle_mem <= ~frame_write_toggle_mem;
它的思想是:不直接跨域传「写完了」这个多比特电平,而是把它变成一个「电平翻转」信号。翻转信号对边沿不敏感,另一侧只要检测到翻转,就知道「这一帧写完了」。这就是单比特跨时钟域的标准做法。
为什么不直接传?
如果直接把
write_finish这根线从 100 MHz 域拉到另一个 100 MHz 域,相位差会带来亚稳态,采样可能采到中间电平。转成 toggle 之后,对方只需要关心「有没有变化」,信号早一拍晚一拍都能正确识别。
功能仿真:第一个 testbench
官方例程没有自带 testbench,而整条 TF → HDMI 链路里,只有一个模块是「纯时序逻辑、不依赖 SDRAM / SD 卡 / 加密 IP」的——那就是 color_bar(8 色彩条发生器)。它就是你第一个仿真对象。
color_bar 内部就是一台精密的计数器机器:h_cnt 数到 800(= 640+16+96+48)回零,v_cnt 数到 525(= 480+10+2+33)回零,在特定计数值翻转 hs / vs,在有效区按 active_x 分 8 段输出白、黄、青、绿、品红、红、蓝、黑。
给它写一个 testbench,只需要一个时钟和一个复位:
// 时钟:25MHz(半周期 20ns,近似真实 25.175MHz)
initial clk = 1'b0;
always #20 clk = ~clk;
// 复位:高电平有效,前 200ns 复位
initial begin
rst = 1'b1;
#200;
rst = 1'b0;
end
// 自动检查:测量一行周期(hs 上升沿间隔)
integer hs_prev_time, hs_period;
initial begin
@(posedge hs);
hs_prev_time = $time;
@(posedge hs);
hs_period = $time - hs_prev_time;
$display("一行周期 = %0d ns(应为 800 * 40 = 32000 ns)", hs_period);
if (hs_period == 32000)
$display("[PASS] 行周期正确:800 像素/行");
else
$display("[FAIL] 行周期异常,请检查时序参数");
end
在 TD 里新建仿真,添加 testbench + color_bar.v + video_define.v,运行后看 hs / vs / de 的波形。testbench 会自动打印行周期是否等于 32000 ns——等于,就说明你把 640×480 时序彻底吃透了。
建议学习路线
没有板子的这段时间,按下面节奏走,约 3 到 5 天:
- 第 1 天:跑通
color_bar仿真,对着波形把h_cnt/v_cnt每个临界值算一遍,彻底搞懂 640×480 时序。 - 第 2 天:读
frame_read_write.v,画出读写两条数据流的 FIFO 与仲裁状态机。 - 第 3 天:读
sd_card_bmp.v,搞懂write_buf_idx/disp_buf_idx到底在哪几个状态翻转。 - 第 4 至 5 天:综合整个工程,看资源报告——LUT 用了多少、距离 19600 的上限还有多大余量,这就是你后面加 OSD / 特效的「预算」。
- 随时可做:用
convert_images_to_bmp.py把图片转成 640×480 24bit BMP,板子一到就能直接播。
官方例程里,BUF1 的起始地址是多少?
307200(即 FRAME_PIXELS,640×480 个像素)
双缓冲里,写侧和读侧各自在什么时机翻转缓冲索引?
写侧写满一帧翻转 write_buf_idx,读侧在 VSYNC 场边界翻转 disp_buf_idx
为什么跨时钟域时要把 write_finish 转成 toggle 信号?
避免多比特/电平信号直接跨域带来的亚稳态,toggle 对边沿不敏感,对方只关心有没有变化
640×480 显示时序里,一行总共多少个像素时钟?
800(640 + 16 前肩 + 96 同步 + 48 后肩)
第一个功能仿真为什么选 color_bar 而不是整个工程?
它是唯一纯时序、不依赖 SDRAM/SD 卡/加密 IP 的模块,仿真不依赖缺失的 IP 模型