本报告围绕学生用开发板从VGA接口(HX1006A)升级到HDMI接口(HX1006B)后的教学与工程需求,完成并验证了FPGA端HDMI显示驱动与SoC系统的HDMI显示改造两项数电实验课助教任务。首先,基于DVI兼容模式实现720p@60Hz视频链路:PLL生成74.25MHz像素时钟与371.25MHz高速时钟,完成视频时序生成(hsync/vsync/de)、TMDS编码以及10:1并串与差分输出,并以屏幕彩条Demo验证链路稳定性。
在此基础上,将原VGA显示工程移植到HDMI平台,选取“HDMI乐谱播放器”作为综合示例:通过位图ROM实现512×256乐谱图像渲染,并叠加动态方框指示当前音符位置,同时与蜂鸣器播放保持节拍同步。
其次,在RV32I五级流水线SoC系统中集成HDMI显示外设,采用内存映射I/O方式将字符显存映射为CPU可直接读写的RAM区域,实现CPU“写显存即显示”的字符输出模型;并针对CPU系统时钟域与像素时钟域的跨时钟数据交互(CDC问题的规避)、以及显示流水线延迟下的时序/像素对齐问题给出可靠实现。最终以基于C语言编写的数字时钟Demo进行验证,结果表明FPGA SoC上的HDMI外设可稳定输出720p信号,字符显示清晰无乱码且可连续刷新。
关键词:FPGA-HDMI驱动,RV32I SoC,HDMI驱动移植,CDC问题,流水线时序对齐
本学期笔者担任了黄继业老师的数字系统与处理器实验课的本科生助教,将FPGA实验开发板从HX1006A(VGA视频输出)升级为支持HDMI输出的HX1006B。作为助教编程任务之一,需完成以下两项工作:
设计实现FPGA HDMI驱动,并运用驱动实现HDMI彩条显示的基本Demo;在HDMI成功驱动的基础上改写所有原有的VGA图像显示工程,等效输出在HDMI显示器上。本报告选取较为复杂的HDMI乐谱播放器工程作为介绍内容。
在已有VGA输出接口的RV32I SoC系统上,改写此FPGA SoC系统,使得其支持HDMI显示接口,并实现基于HDMI的字符显示功能,最终设计并运行一个基于SoC-HDMI的数字时钟Demo程序。
HDMI用于传输高清数字视频(以及音频/控制信息)。在本工程中只使用了与DVI兼容的“视频传输”部分,其核心特点是:不以数据包形式发送图像,而是以连续像素流的方式按行、按帧扫描输出。发送端负责生成视频时序(行/场同步与消隐区),接收端依据时序在正确的时间点采样并还原每个像素的RGB数据。
对720p@60Hz而言,一帧图像由“有效显示区+消隐区”组成。有效区内每到来一个像素时钟(pixel_clk),系统输出一个像素的24位RGB(RGB888,8:8:8);hsync/vsync同步信息并不是额外占用一根“同步线”去传输,而是在de=0(消隐区)时由TMDS发送控制符号来携带。de(Data Enable)是发送端根据行/列计数判断“当前像素是否属于有效显示区”得到的标志。
在DVI兼容的视频模式下,三路数据通道D2/D1/D0分别承载R/G/B三个8bit颜色分量(约定为D2→R,D1→G,D0→B),时钟通道CLK提供像素采样参考。有效显示区(de=1)时传输像素数据(TMDS 8b/10b编码后发送);消隐区(de=0)时传输控制符号(携带hsync/vsync等时序边界信息)。
由于每像素每通道发送10bit,单通道串行码率为:\[
R_{\text{TMDS}} = f_{\text{pixel}} \times 10
\]
720p@60Hz像素时钟为\(f_{\text{pixel}}=74.25\text{MHz}\),单通道码率约为742.5Mbps。本工程采用pixel_clk_5x配合DDR方式实现10:1并串:pixel_clk_5x为像素时钟的5倍(371.25MHz),在其上升沿/下降沿各输出1bit,从而每5个高速周期输出10bit。
| 信号名称 | FPGA引脚 | 信号名称 | FPGA引脚 |
|---|---|---|---|
HDMIB_D2_P | B8 | HDMIB_D0_P | F15 |
HDMIB_D2_N | A8 | HDMIB_D0_N | F16 |
HDMIB_D1_P | B9 | HDMIB_CLK_N | G16 |
HDMIB_D1_N | A9 | HDMIB_CLK_P | G15 |
h_cnt/v_cnt → hsync/vsync/de
de=1 像素 / de=0 控制
8b→10b
RGB888(8bit×3)
pixel_clk_5x + DDR
D2/D1/D0/CLK(P/N)
串并 + TMDS解码
HDMI的屏幕色条Demo用于验证HDMI显示链路的完整性与稳定性。通过最简单的“时序生成+颜色图案”组合,可以快速确认像素时钟、视频时序、TMDS编码与差分输出是否工作正常,是后续复杂图像显示的基础。
HDMI_Colorbar工程顶层结构分为四部分:1)时钟管理:pll_clk将50MHz系统时钟变换为74.25MHz像素时钟与371.25MHz TMDS传输时钟;2)视频时序:video_driver产生720p@60Hz的hsync/vsync/de以及像素坐标;3)图案生成:video_display依据横坐标生成8条彩色竖条;4)HDMI输出:dvi_transmitter_top完成TMDS编码与10:1串行化,输出差分信号到HDMI接口。
满足:\(\texttt{H_TOTAL}=\texttt{H_SYNC}+\texttt{H_BACK}+\texttt{H_DISP}+\texttt{H_FRONT}\), \(\texttt{V_TOTAL}=\texttt{V_SYNC}+\texttt{V_BACK}+\texttt{V_DISP}+\texttt{V_FRONT}\)。
| 参数 | 水平(H) | 垂直(V) | 说明 |
|---|---|---|---|
| 同步脉冲 | 40 | 5 | H_SYNC/V_SYNC |
| 后沿 | 220 | 20 | H_BACK/V_BACK |
| 有效显示 | 1280 | 720 | H_DISP/V_DISP |
| 前沿 | 110 | 5 | H_FRONT/V_FRONT |
| 总计 | 1650 | 750 | H_TOTAL/V_TOTAL |
彩条图案生成模块根据像素横坐标将显示区域等分为8段,并输出RGB888颜色数据。颜色顺序与VGA实验一致,可描述为(从右到左):蓝、黑、红、粉、绿、青、黄、白。
工程中应用层色条生成参数仍保留为H_DISP=800(800×600),因此色条宽度BAND_WIDTH=H_DISP/8=100。当实际有效显示宽度为1280像素时,最后一段蓝色会占据\(1280-700=580\)像素,呈现“右侧一大块蓝条”的视觉效果,但不影响驱动链路正确性验证。
parameter H_DISP = 11'd800;
// define 1280 will be better for hdmi 720p. 800 can work too
// uniform band width
localparam BAND_WIDTH = H_DISP / 8;
// generate 8 vertical color bars by pixel_xpos
always @(posedge pixel_clk) begin
if (!sys_rst_n)
pixel_data <= 24'd0;
else if (pixel_xpos < BAND_WIDTH * 1)
pixel_data <= WHITE;
else if (pixel_xpos < BAND_WIDTH * 2)
pixel_data <= YELLOW;
else if (pixel_xpos < BAND_WIDTH * 3)
pixel_data <= CYAN;
else if (pixel_xpos < BAND_WIDTH * 4)
pixel_data <= GREEN;
else if (pixel_xpos < BAND_WIDTH * 5)
pixel_data <= MAGENTA;
else if (pixel_xpos < BAND_WIDTH * 6)
pixel_data <= RED;
else if (pixel_xpos < BAND_WIDTH * 7)
pixel_data <= BLACK;
else
pixel_data <= BLUE;
end
FPGA工程顶层模块将HDMI显示链路与音乐播放链路集成。首先是HDMI显示链路:利用已经完成的FPGA HDMI的720p视频驱动,设计乐谱图像显示与动态方框叠加模块,实现乐谱图像的HDMI显示与当前音符位置指示。再是音乐播放链路:PLL产生1MHz/2kHz,分频得到4Hz节拍,cnt138t输出音符序列索引,music ROM给出音符数据,经f_code译码后驱动speak与DFF产生蜂鸣器音频输出。
下方为乐谱播放器运行录像:画面包含乐谱位图与红色动态方框标记,蜂鸣器按节拍播放音符。浏览器可直接全屏播放,用于直观验证“显示链路 + 音乐链路”联动是否稳定。
HTML介绍/video/music_player.mp4)。
乐谱图像存储在乐谱位图ROM中,分辨率为512×256,1bit黑白数据。视频显示模块在像素域内完成图像读取、边框绘制与动态方框叠加:先在800×600虚拟窗口内完成居中计算,再进行ROM地址映射
\(\texttt{rom_addr}=\{y[7:0],x[8:0]\}=y\times512+x\),并将1bit像素扩展为RGB888(rom_data=0输出黑色,rom_data=1输出粉紫色系)。
动态方框用于标记当前音符位置。计数器cnt138同时驱动音符播放与方框位置:\[
\texttt{cnt138_adj}=\max(\texttt{cnt138}-2,0)
\]
\[
x=(\texttt{IMG_X_START}-1)+(\texttt{cnt138_adj}[3:0]\times32)
\]
\[
y=(\texttt{IMG_Y_START}+1)+(\texttt{cnt138_adj}[6:4]\times32)
\]
其中低4位对应16列,高3位对应8行,实现16×8音符布局;延迟2个音符位置用于补偿音频输出与显示之间的节拍差异。
// image size: 512x256 (1-bit), centered in a 800x600 virtual window
parameter H_DISP = 11'd800;
parameter V_DISP = 11'd600;
parameter IMG_WIDTH = 11'd512;
parameter IMG_HEIGHT = 11'd256;
localparam IMG_X_START = (H_DISP - IMG_WIDTH) / 2;
localparam IMG_Y_START = (V_DISP - IMG_HEIGHT) / 2;
wire [10:0] x = pixel_xpos - IMG_X_START;
wire [10:0] y = pixel_ypos - IMG_Y_START;
wire dis_en = (pixel_xpos >= IMG_X_START && pixel_xpos < IMG_X_START + IMG_WIDTH) &&
(pixel_ypos >= IMG_Y_START && pixel_ypos < IMG_Y_START + IMG_HEIGHT);
// ROM address mapping: rom_addr = {y[7:0], x[8:0]} = y*512 + x
always @(posedge pixel_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
rom_addr <= 17'd0;
else if (dis_en)
rom_addr <= {y[7:0], x[8:0]};
else
rom_addr <= 17'd0;
end
// priority: dynamic box > border > image > background
always @(posedge pixel_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
pixel_data <= 24'd0;
else if (dynamic_box_en)
pixel_data <= RED;
else if (border_en)
pixel_data <= RED;
else if (dis_en) begin
if (rom_data) begin
// expand 1-bit to RGB888 (a purple-ish foreground color)
pixel_data[23:16] <= {rom_data,rom_data,1'b0,1'b0,1'b0,1'b0,1'b0,1'b0}; // R
pixel_data[15:8] <= {1'b0,1'b0,rom_data,rom_data,1'b0,1'b0,1'b0,1'b0}; // G
pixel_data[7:0] <= {rom_data,1'b0,rom_data,1'b0,1'b0,1'b0,1'b0,1'b0}; // B
end else
pixel_data <= BLACK;
end else
pixel_data <= BLACK;
end
该应用层渲染在像素时钟域内完成“读图 + 叠加绘制 + 输出RGB”。本工程采用“动态方框 → 红色边框 → ROM图像 → 黑色背景”的优先级,保证方框不被覆盖,边框也清晰可见。
FPGA上部署的RV32I指令集SoC系统由RV32I处理器核 + 片上总线与仲裁 + 指令/数据存储器 + 外设组成。系统通过内存映射I/O(MMIO)技术,将外设映射到统一地址空间,CPU使用Load/Store指令即可像访问内存一样访问外设;对软件来说,“写显存即显示”的HDMI字符输出也属于MMIO范式的一种具体应用。
RV32I中每条指令固定占用32位(4字节)。指令字段包含:opcode、rd、funct3、rs1、rs2、funct7与立即数等。由于指令定长为4字节,顺序执行时PC按PC=PC+4递增;跳转/分支会改写PC实现控制流转移。
本工程CPU采用经典的五级流水线(IF取指、ID译码、EX执行、MEM访存、WB回写),各级之间通过段寄存器保存中间结果,从而提高吞吐率。
- IF:根据PC取指令,PC顺序+4或被分支/跳转改写。
- ID:译码、读寄存器、生成控制信号与立即数。
- EX:ALU运算/地址计算/分支判断。
- MEM:Load/Store访存与外设访问(MMIO)。
- WB:写回寄存器堆。
当后一条指令需要使用前一条指令刚计算出的结果(尚未写回寄存器堆)时会出现数据冒险。解决方案包括: 1)数据前递(Forwarding):直接从EX/MEM/WB阶段取数送到ALU输入; 2)Load-Use停顿:Load读数返回较晚,紧随其后的依赖指令即使前递也来不及,必须插入1个气泡(stall)。
LW x1, 0(x2) // 读内存到x1(数据稍后才回)
ADD x3, x1, x4 // 立刻用x1(来不及) => 需要1周期停顿
分支/跳转会导致取指阶段可能取错指令。本工程采用默认不跳转的简单预测策略:若EX阶段判断实际发生跳转,则把跳转信号反馈到IF更新PC,同时把IF/ID阶段已取出的错误指令作废(flush为NOP),代价是损失1到2个周期,但保证正确性。
取指主口与数据主口共享片上总线。当数据访存请求未获得授权时(例如m1_hgrant=0),触发总线冲突暂停,冻结流水线,等待数据访问完成后再继续,避免读写顺序错乱。
在MMIO模型中,CPU访问外设与访问内存没有本质区别:执行Load/Store指令时,EX阶段计算地址,MEM阶段通过总线接口模块发起总线事务,总线仲裁器根据优先级授予访问权,再由地址译码将请求路由到对应从设备。对软件来说就是“读某地址得到数据 / 写某地址改变外设状态”。
rs1 + imm
hbusreq/haddr/hwrite/be
hgrant/hready
hrdata
| 从设备 | 地址范围 | 功能 |
|---|---|---|
| Slave0 | 0x0000_0000–0x0000_0FFF | 指令ROM |
| Slave1 | 0x0000_8000–0x0000_8FFF | 指令RAM |
| Slave2 | 0x0001_0000–0x0001_0FFF | 数据RAM |
| Slave3 | 0x0002_0000–0x0002_0FFF | 显示缓冲区RAM(HDMI外设使用) |
| Slave4 | 0x0003_0000–0x0003_0003 | 用户UART |
| Slave5 | 0x0003_1000–0x0003_100F | LED/SEG |
| 对比项 | VGA | HDMI |
|---|---|---|
| 信号类型 | 模拟RGB(3根线) | 数字TMDS差分(4对差分线) |
| 分辨率 | 800×600@60Hz | 1280×720@60Hz |
| 像素时钟 | 40MHz(50MHz分频) | 74.25MHz(需独立PLL) |
| 编码方式 | 直接输出RGB电压 | TMDS 8b/10b编码 + 串行化 |
| 时钟域 | 单时钟域 | 双时钟域(CPU 50MHz + HDMI 74.25MHz) |
将FPGA-HDMI驱动模块集成到SoC系统,本质上需要同时解决两类问题:其一是应用层字符显示逻辑(CPU写RAM如何映射到屏幕字符);其二是驱动层接口实现(50MHz系统时钟域与74.25MHz像素时钟域之间如何安全可靠地进行跨时钟域数据传输,并在多级流水线延迟下保证时序/像素对齐)。
这里的“驱动层难点”主要集中在两个点:跨时钟域(CDC)与时序/像素对齐。HDMI侧像素链路运行在74.25MHz像素时钟域,且编码/取数/字模查询等逻辑引入固定流水线延迟;CPU与总线运行在50MHz系统时钟域。如果直接让两个时钟域共享同一份显存并同时读写,会引入亚稳态与撕裂风险,导致字符乱码或画面不稳定。
可靠做法是把“CPU写显存”和“HDMI读显存”在硬件结构上隔离:CPU侧在50MHz下写显存;HDMI侧在74.25MHz下只读自己的显存镜像。两份显存之间不做异步读写握手,而是由CPU时钟域的复制控制在写入时同时更新两份显存,达到“空间换时序”的效果,从根源上规避CDC风险。
74.25MHz读取镜像
HDMI字符渲染链路通常包含“像素坐标计算 → 显存读取 → 字模ROM查询 → RGB输出”等多级流水线。若只对像素数据做延迟而不同步延迟hsync/vsync/de与坐标信号,就会出现“信号边界与像素内容错位”的现象(例如字符整体偏移、行起始抖动、边缘撕裂)。因此必须把参与RGB生成的所有控制信号按同样级数做移位寄存器延迟,使其与最终像素数据对齐后再输出到TMDS编码链路。
- 需要对齐的典型信号:
hsync、vsync、de、像素坐标pixel_x/pixel_y、字符坐标、显存读出的ASCII码、字模点阵位等。 - 工程化做法:用N级移位寄存器把上述信号统一延迟N拍;N取决于“显存/字模ROM/组合逻辑”总体延迟。
- 替换显示链路:从VGA像素生成与RGB DAC输出,替换为HDMI(DVI兼容)时序 + TMDS编码 + 串行化 + 差分输出。
- 显存外设化:把字符显存作为总线从设备挂载到固定地址窗口(例如
0x00020000起)。 - CDC规避:双显存镜像 + CPU域复制控制同步写入。
- 对齐验证:用已知字符布局(例如行列边界字符)验证坐标映射与流水线延迟是否正确。
- 软件验证:运行数字时钟等持续刷新程序,验证长时间稳定、无乱码、无撕裂。
为了让同学“用C语言像操作内存一样操作外设”,SoC把常见外设都以MMIO方式挂载到总线:指令ROM/指令RAM/数据RAM作为基础存储器外设;用户UART用于输出调试信息;LED与数码管作为最直观的可视化外设。它们与HDMI显存的访问方式完全一致:都是Load/Store到指定地址窗口。
其中存储器类外设一般实现为“4个8bit字节通道并联组成32bit数据口”,配合wr_be/rd_be字节使能支持SB/SH/SW等不同宽度的读写;这也是你TeX报告中强调的“按字节精确修改内存”的关键点。
LED/SEG通常是“写寄存器更新显示”的模型:CPU向LED/SEG映射地址写入数据,外设寄存器在时钟上升沿更新,驱动板上LED或数码管刷新。对同学来说,只需记住地址与写入格式即可。
// 伪代码:写LED寄存器
*(volatile unsigned int*)0x00031000 = 0x000000A5;
UART让软件侧能输出日志(例如每秒输出一个“.”或打印寄存器值),与HDMI屏幕输出互补:一个用于“看屏幕状态”,一个用于“看程序运行轨迹”,更容易定位问题。
SoC存在多个总线主设备(例如调试UART主口、CPU数据主口、CPU取指主口)。当多个主设备同时请求总线时,总线仲裁器按固定优先级授予使用权(常见做法是调试优先、数据访问优先于取指)。如果数据访存没拿到总线授权,就会触发暂停信号,冻结流水线直到事务完成,以保证访存操作的正确性与原子性。
为了验证移植是否成功,编写了一个数字时钟Demo的C语言程序。该Demo在屏幕顶部显示标题,实时显示格式为HH:MM:SS并每秒更新,同时通过UART串口输出信息。测试结果显示:HDMI显示器成功识别720p信号且画面稳定无闪烁,字符清晰无乱码,可连续刷新。
#define VRAM_BASE 0x00020000 // 显存基地址
// 在显存(row,col)位置写入字符
void putchar_at(int row, int col, char c) {
volatile unsigned char *vram = (unsigned char *)VRAM_BASE;
int addr = row * 64 + col;
vram[addr] = c;
}
// 主循环(节选)
int main() {
int hour = 12, min = 0, sec = 0;
// 绘制界面:标题、分隔线等...
while (1) {
putchar_at(3, 0, '0' + hour/10);
putchar_at(3, 1, '0' + hour%10);
putchar_at(3, 2, ':');
putchar_at(3, 3, '0' + min/10);
putchar_at(3, 4, '0' + min%10);
putchar_at(3, 5, ':');
putchar_at(3, 6, '0' + sec/10);
putchar_at(3, 7, '0' + sec%10);
delay_1s();
sec++;
if (sec >= 60) { sec = 0; min++; }
if (min >= 60) { min = 0; hour++; }
if (hour >= 24) { hour = 0; }
}
}
HTML介绍/video/soc_clock.mp4)。
在数字时钟Demo中,软件层面的delay_1s()本质上是“用CPU执行固定次数的指令/循环来近似等待1秒”。因此讨论“能否完全准确走时”,需要区分两点:其一是时间基准的频率是否与真实秒严格一致;其二是系统运行时能否保证每次延时循环消耗的CPU周期数恒定。
五级流水线并不意味着每条指令需要5个周期:流水线填满后多数简单指令可做到1周期吞吐,但单条指令从取指到写回的端到端延迟仍跨越多个阶段。对忙等待而言,关键是“单位时间内累计了多少个CPU时钟周期”(例如50MHz时钟计数),理想情况下循环每次迭代的周期数恒定,循环次数就能换算为时间。
但在SoC里,总线仲裁与访存会引入非确定性停顿:取指口与数据口共享总线时,可能因仲裁/等待导致流水线暂停;Load-Use等相关也会产生气泡。这会让同样的delay_1s()在不同负载下抖动,从而影响软件延时的短期精度。
另外,HDMI显示链路的多级流水线主要影响“写显存到上屏”的显示延迟(并叠加光栅扫描到达目标位置的帧内等待),并不直接构成时间基准本身;即使CPU计时准确,屏幕上看到的更新时间也可能存在最多1帧量级的不确定性。
从“绝对准确”角度,板载晶振(标称50MHz)仍存在温漂/电压/工艺带来的频偏与长期漂移;因此不引入外部标准时基时,很难做到“长期完全无漂移”。工程上若追求更稳定/可校准的走时,通常需要外部参考(高稳晶振/RTC/同步校准机制)并尽量用硬件定时器中断替代忙等待(当前工程仍以CPU软件循环刷新显存为主,未引入定时器中断与DMA)。
本报告完成了课程助教任务中“HDMI驱动实现与应用部署”的核心工作,并给出了从纯FPGA HDMI驱动工程到SoC系统外设从VGA到HDMI改造的完整技术路径。
在FPGA端,基于DVI兼容模式搭建了720p@60Hz HDMI显示链路,形成“时钟生成—视频时序—TMDS编码—10:1串行化—差分输出”的驱动闭环,并通过彩条显示验证了像素时序与链路传输的正确性与稳定性。在应用层面,将既有VGA显示工程迁移到HDMI输出,乐谱播放器案例实现了位图ROM渲染、边框/动态方框叠加以及与音频播放节拍同步的综合功能,证明该HDMI驱动具备支撑复杂显示应用的能力。
在SoC端,将HDMI显示作为内存映射外设融入RV32I五级流水线系统,建立了“CPU写字符显存—HDMI硬件持续读显存并渲染输出”的软件友好接口,并针对跨时钟域数据传输与显示流水线延迟对齐等关键难点完成工程化处理。数字时钟Demo的稳定显示与连续刷新结果表明:CPU与HDMI显示模块可并行工作且无资源冲突,平台具备良好的可用性与可拓展性。
下面嵌入的是TeX导出的PDF全文预览;若浏览器阻止本地PDF预览或显示空白,可直接点击打开:ReportPro.pdf。