FPGA 开发有一半以上的工作发生在「看不见硬件」的阶段。先把这个阶段的能力边界讲清楚,你就不会觉得没板子就无从下手。
| 能做(现在就能做) | 不能做(等板子到手) |
|---|---|
| 读懂全部 RTL 代码 | JTAG 下载 bitstream 到芯片 |
| 打开 TD 工程、综合、布局布线 | 看 HDMI 屏幕上的真实画面 |
| 功能仿真验证时序逻辑 | 读 SD 卡的物理扇区 |
| 看 LUT / 寄存器 / 时钟资源报告 | 调 PLL 输出与引脚电平 |
我们这套例子用的是安路 TD 6.2.1,配合官方提供的 HDMI 多媒体播放例程(TF 卡读图 + SDRAM 帧缓存 + HDMI 输出)。
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 显示这一套)。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 发送核(变成屏幕上的画面)。
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,而整条 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 天:
color_bar 仿真,对着波形把 h_cnt / v_cnt 每个临界值算一遍,彻底搞懂 640×480 时序。frame_read_write.v,画出读写两条数据流的 FIFO 与仲裁状态机。sd_card_bmp.v,搞懂 write_buf_idx / disp_buf_idx 到底在哪几个状态翻转。convert_images_to_bmp.py 把图片转成 640×480 24bit BMP,板子一到就能直接播。