第三十二章 HDMI方块移动实验
本章在讲什么
上一章我们用 HDMI 成功点亮了显示器,显示的是静态彩条。这一章就一个目标:让画面动起来——在屏幕上显示一个 40×40 的黑色小方块,它以 100Hz 的频率在屏幕上移动,碰到四周 40 像素宽的蓝色边框就反弹。
硬件设计、引脚约束与 HDMI 彩条实验完全相同(TMDS 差分输出、74.25MHz 像素时钟),所以本章只改一个模块:video_display。这正体现了 FPGA 工程里"分层复用"的好处——传输链路(PLL、video_driver、dvi_transmitter_top)原封不动,只替换"画什么"。
核心知识点就两个:
- 顶点坐标移动法:用
(block_x, block_y)记录方块左上角顶点,移动就是改坐标; - 分频产生移动节拍:像素时钟 74.25MHz 太快,用计数器分频出 100Hz 的
move_en脉冲控制坐标加减。
核心概念拆解
32.1 系统架构:只换"画笔",不动"画布"
系统框图和彩条实验一致:
sys_clk → PLL → pixel_clk (74.25MHz) / pixel_clk_5x (371.25MHz)
↓
video_driver(时序+坐标) → video_display(方块绘制) ← 本次唯一修改点
↓
dvi_transmitter_top → TMDS 差分 → HDMI 显示器
video_display 的接口与彩条版完全一样:输入像素坐标 pixel_xpos/pixel_ypos,输出该点颜色 pixel_data。也就是说,显示内容的变化被完全封装在模块内部。
32.2 顶点坐标移动法:把"方块移动"降维成"坐标移动"
一个 40×40 的方块由无数像素点组成,如果每个点单独算运动,代码没法写。技巧是:方块大小固定时,只要知道左上角顶点坐标,就能画出整个方块。于是把"方块移动"简化为"顶点 (block_x, block_y) 的移动"。
参数定义(都在 video_display 内):
parameter H_DISP = 11'd1280; // 行分辨率
parameter V_DISP = 11'd720; // 列分辨率
parameter DIV_CNT = 22'd750000; // 分频计数器(决定移动速度)
localparam SIDE_W = 11'd40; // 屏幕边框宽度
localparam BLOCK_W = 11'd40; // 方块宽度
localparam BLUE = 24'h0000ff; // 边框蓝色
localparam WHITE = 24'hffffff; // 背景白色
localparam BLACK = 24'h000000; // 方块黑色
想改方块大小、边框宽度、移动速度、颜色,只动这几个参数即可。
32.3 100Hz 移动脉冲:像素时钟太快,分频来凑
屏幕刷新是 60Hz,但方块如果每个像素时钟都动一步,瞬间就飞出屏幕了。解决办法:用计数器 div_cnt 数到 DIV_CNT = 750000(74.25MHz ÷ 750000 = 99Hz ≈ 100Hz)时拉高一个周期的 move_en:
assign move_en = (div_cnt == DIV_CNT) ? 1'b1 : 1'b0;
always @(posedge pixel_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
div_cnt <= 22'd0;
else begin
if (div_cnt < DIV_CNT)
div_cnt <= div_cnt + 22'd1;
else
div_cnt <= 22'd0; // 计数达 10ms 后清零
end
end
move_en 每 10ms 出现一次,方块每秒移动 100 个像素点。DIV_CNT 越大,方块移动越慢——这就是调节移动速度的旋钮。
32.4 方向翻转:撞墙反弹的实现
反弹逻辑用两个方向寄存器 h_direct(1=右移,0=左移)和 v_direct(1=下移,0=上移)实现:
// 当方块移动到边界时,改变移动方向
always @(posedge pixel_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
h_direct <= 1'b1; // 初始水平向右
v_direct <= 1'b1; // 初始竖直向下
end
else begin
if (block_x == SIDE_W + 11'd1) // 到达左边界,向右
h_direct <= 1'b1;
else if (block_x == H_DISP - SIDE_W - BLOCK_W + 11'd1) // 到达右边界,向左
h_direct <= 1'b0;
else
h_direct <= h_direct;
if (block_y == SIDE_W + 11'd1) // 到达上边界,向下
v_direct <= 1'b1;
else if (block_y == V_DISP - SIDE_W - BLOCK_W + 11'd1) // 到达下边界,向上
v_direct <= 1'b0;
else
v_direct <= v_direct;
end
end
以水平方向为例:屏幕宽 1280,两侧各留 40 像素边框,方块宽 40。右边界临界值为 1280 - 40 - 40 + 1 = 1201——顶点到达 1201 时方块最右缘刚好贴住右边框内侧,此时翻转方向向左。仿真验证了这一点:block_x == 41 时 h_direct 拉高(向右),block_x == 1201 时拉低(向左);竖直方向同理(41 翻转向下,641 翻转向上)。
32.5 坐标更新与区域上色:move_en 有效时才动,按坐标范围画三块区域
坐标更新只在 move_en 有效时执行一次,方向寄存器决定加减:
always @(posedge pixel_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
block_x <= SIDE_W + 11'd1; // 初始位置在左上角
block_y <= SIDE_W + 11'd1;
end
else if (move_en) begin
if (h_direct) block_x <= block_x + 11'd1; // 右移
else block_x <= block_x - 11'd1; // 左移
if (v_direct) block_y <= block_y + 11'd1; // 下移
else block_y <= block_y - 11'd1; // 上移
end
end
最后按当前像素坐标判断落在哪个区域,涂对应颜色——这是整个绘制逻辑的"调色板":
// 给不同的区域绘制不同的颜色
always @(posedge pixel_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
pixel_data <= BLACK;
else begin
if ( (pixel_xpos < SIDE_W) || (pixel_xpos >= H_DISP - SIDE_W)
|| (pixel_ypos <= SIDE_W) || (pixel_ypos > V_DISP - SIDE_W) )
pixel_data <= BLUE; // 边框:蓝色
else if ( (pixel_xpos >= block_x - 11'd1) && (pixel_xpos < block_x + BLOCK_W - 11'd1)
&& (pixel_ypos >= block_y) && (pixel_ypos < block_y + BLOCK_W) )
pixel_data <= BLACK; // 方块:黑色
else
pixel_data <= WHITE; // 背景:白色
end
end
判断顺序是边框 → 方块 → 背景,先判最外层。注意方块区域判断里出现的 block_x - 1'd1 偏移,与方向翻转里 +1 的临界值是配套的,几处坐标必须自洽,改任何一处都要同步检查其余几处。
32.6 顶层与仿真:架构照搬,defparam 缩短等待
顶层 top_hdmi_block_move 与彩条实验几乎完全相同:PLL 输出 74.25MHz 像素时钟和 5 倍频 371.25MHz,依次例化 video_driver、video_display、dvi_transmitter_top,唯一的区别是 PLL 时钟频率从 1080p 彩条配置换成了 720p 的 74.25MHz。
仿真时有个现实问题:DIV_CNT = 750000,如果照实仿真,要等非常久才能看到方块动一步。TB 里用 defparam 把参数改小:
defparam top_hdmi_block_move_inst.u_video_display.DIV_CNT = 75;
这样 div_cnt 数到 75 就拉高 move_en,几微秒就能看到一次移动,大幅缩短仿真时间。下载到板上时用的还是真实参数 750000,defparam 只作用于仿真。
图片对照
以下图片配合上文食用效果更佳(路径对应 /images/zynq/ch32/):

图:HDMI 系统总体框图——只有 video_display 是本章新改动的模块

图:显示模块波形图——div_cnt 计数、move_en 脉冲、block_x/block_y 移动与方向翻转的关系一目了然

图:方块垂直移动方向仿真波形——block_y 到达边界时 v_direct 翻转

图:上板效果图——黑色方块在蓝色边框内弹跳
初学者容易踩的坑
- 边界临界值搞混:翻转条件是
block_x == SIDE_W + 1(左)和block_x == H_DISP - SIDE_W - BLOCK_W + 1(右),这几个+1来自坐标偏移的约定(方块区域判断用了block_x - 1)。不要凭直觉写成 40 或 1200,否则方块会"陷进"边框或提前反弹。 - 方块不动:检查
move_en是否正确产生。DIV_CNT = 750000对应 22 位计数器,如果寄存器位宽不够(比如只给了 20 位),计数永远到不了目标值,方块就永远静止。 - 仿真等太久:不修改
DIV_CNT直接仿真,一个移动节拍要 750000 个时钟周期,波形窗口里几乎看不到变化。务必用defparam把参数改小再仿真。 - 改了参数没同步:把
BLOCK_W改成 80 时,方向翻转公式H_DISP - SIDE_W - BLOCK_W + 1里用的是参数名,会自动跟着变;但如果你手写了 1201 这类常数就会错。坚持用参数表达式而非硬编码数字。 - 以为要改传输链路:本章只改
video_display。如果动了dvi_transmitter_top或 PLL 反而把好的改坏了——先确认"屏幕上没有方块"是显示内容问题,而不是链路问题。
小结 & 下一步
本章在 HDMI 彩条实验基础上,仅修改 video_display 模块就实现了动态图案显示,掌握了三个可迁移的套路:
- 顶点坐标法:固定大小的图形,用左上角顶点坐标代表整个图形,移动即改坐标;
- 分频节拍法:用计数器从高速时钟里分出低频使能脉冲,控制"动画帧率"(
DIV_CNT越大越慢); - 区域上色法:按像素坐标的范围判断涂色,绘制边框、运动物体和背景。
这套"坐标 + 节拍 + 上色"的思路正是所有屏幕动画的雏形,之后做贪吃蛇、打砖块小游戏都是它的扩展。
下一步:第三十二章结束,接下来进入第三十三章 I2C 协议读写 EEPROM 实验——这是第一次接触真正的板间串行通信协议,将学习 I2C 的起始/停止条件、应答机制以及状态机驱动的读写控制。