简介:这是一份面向赛灵思Zynq-7000系列器件的IIC寄存器配置例程,硬件平台为米联客MZ7035FA开发板,对应芯片型号xc7z045ffg676,开发工具采用Vivado 2017.4与SDK。工程核心是利用处理器系统侧的IIC控制器,通过EMIO接口把IIC信号引到可编程逻辑端,对ADV7611单输入HDMI接收芯片完成寄存器配置,从而使Zynq能够接收外部输入的HDMI信号,目标分辨率为1920×1080@60Hz。可编程逻辑部分负责接收并解析视频分辨率参数,同时将输入HDMI环出输出以作回环验证;处理器系统部分负责IIC配置,并通过GPIO拉高done引脚提示配置完成。ADV7611支持HDMI 1.4a规定的强制性3D电视格式,以及最高UXGA 60Hz、8位分辨率,例程中寄存器配置注释清晰,可迁移到同类HDMI桥接芯片调试中。资源包共1577个文件,大小45.16MB,主要文件类型包括Verilog/VHDL硬件源码、C语言软件代码、XDC约束文件、Tcl脚本以及Vivado/SDK工程文件,覆盖工程创建、逻辑设计、约束编译与调试全流程。目前已有1560人学习下载,适合希望掌握Zynq软硬件协同、IIC外设配置及HDMI视频通路设计的FPGA工程师作为工程参考。
1. ZYNQ配置IIC寄存器驱动ADV7611:这步走通HDMI输入就成了一半
接手MZ7035FA这块板子时,最容易被卡住的地方不是逻辑代码,而是ADV7611的IIC配置。这颗HDMI接收芯片的寄存器非常多,手册快三百页,但真正要动的手脚其实集中在几个页面上。更关键的是,ZYNQ这边怎么把IIC总线引出去——用PS的IIC控制器走EMIO,还是用PL侧逻辑模拟IIC,两条路我都在项目里试过,结论很明确:只要能调PS的资源,就不要自己写IIC时序。PS侧IIC控制器带硬件仲裁、超时检测和FIFO,SDK里调Xilinx提供的驱动函数就行,省掉一大截调试时间。本文以xc7z045ffg676跑1080p60输入为例,把ADV7611寄存器初始化、PL侧回环和分辨率解析三个环节串起来讲,适合正在做视频采集或HDMI分发项目的开发者参考。
2. ADV7611寄存器寻址与初始化序列构造
2.1 IIC地址映射与分页机制
ADV7611虽然是单颗芯片,但内部寄存器被拆成好几个映射块,每个映射块都有自己的IIC从机地址。默认配置下,主映射(Main Map)挂在0x98(8位地址格式),CP映射在0x9A,HDMI映射在0x9C。ZYNQ的IIC控制器如果配置成7位地址模式,对应的就是0x4C、0x4D、0x4E。
很多人第一次写配置程序时,只往0x98这个地址写寄存器,发现读出来全是0xFF或者写不进去,原因就是没有切换MAP页。ADV7611的寄存器寻址是分页的,访问HDMI相关寄存器前必须先在主映射的0xFD寄存器里写入目标页号。比如要访问HDMI映射的寄存器,就先写0x98、0xFD、0x03,后续的读写操作才会落到HDMI页上。
#define ADV7611_IIC_ADDR 0x4C // 7-bit地址模式下的主映射 #define REG_MAP_ADDR 0xFD // MAP页选择寄存器 static int adv7611_write_reg(uint8_t map, uint8_t reg, uint8_t val) { uint8_t buf[2] = {reg, val}; /* 先切页,再写寄存器 */ uint8_t map_sel[2] = {REG_MAP_ADDR, map}; XIic_Send(BASEADDR, ADV7611_IIC_ADDR, map_sel, 2, XIIC_STOP); return XIic_Send(BASEADDR, ADV7611_IIC_ADDR, buf, 2, XIIC_STOP); }这段代码先通过主映射的0xFD寄存器切换页面,再写入目标寄存器的地址和数据。XIic_Send的最后一个参数XIIC_STOP表示发送完成后产生停止条件,这是IIC时序里必须的操作,漏掉的话从机可能不认这次传输。如果用的是带FIFO的IIC控制器,也可以连续发送多个字节,但切页操作建议单独发一次,方便跟踪时序异常。
2.2 核心寄存器配置表与初始化序列
ADV7611上电后默认是不工作的,必须写一段初始化序列才能让它正常接收HDMI信号。不同开发板给出的例程序列可能有差别,核心的配置项集中在输入格式、EDID RAM和电源管理三块。我整理了一份精简的初始化序列,实测在1080p60输入下稳定跑通。
| 映射页 | 寄存器地址 | 写入值 | 作用说明 |
|---|---|---|---|
| 0x00 主映射 | 0x0D | 0x40 | 禁用时钟输出,先静默配置 |
| 0x00 主映射 | 0x0F | 0x00 | 关闭HDMI输出,避免配置过程中闪屏 |
| 0x00 主映射 | 0x98 | 0x03 | 设置ADI公司ID校验,必须匹配 |
| 0x00 主映射 | 0x9A | 0xE0 | 设置IIC地址的MSB |
| 0x00 主映射 | 0x9C | 0x30 | 设置IIC地址的LSB |
| 0x01 CP映射 | 0x17 | 0x41 | 配置色彩空间转换,YCbCr转RGB |
| 0x01 CP映射 | 0x18 | 0x16 | 视频时序相关参数 |
| 0x01 CP映射 | 0x19 | 0x02 | 使能自动输入格式检测 |
| 0x03 HDMI映射 | 0x00 | 0x00 | HDMI模式使能 |
| 0x03 HDMI映射 | 0x01 | 0x00 | 禁用HDCP,纯直通模式 |
| 0x03 HDMI映射 | 0x02 | 0x00 | 通道极性配置 |
| 0x03 HDMI映射 | 0x07 | 0x10 | 深色彩模式关闭,8bit输出 |
| 0x03 HDMI映射 | 0x0A | 0x80 | 使能HDMI输入 |
| 0x04 EDID映射 | 0x00~0x7F | 自定义 | 写入128字节EDID 1.3 |
配置顺序有讲究,先配主映射和电源管理,再切到CP映射配置视频处理链路,HDMI映射的配置放最后,因为一旦使能输入,芯片就开始解析信号,后续寄存器写入可能会被打断。EDID映射建议在芯片处于非工作状态时写入,写完后再使能HDMI输入。
2.3 为什么需要写EDID
HDMI源端设备(比如电脑显卡、摄像机)在输出信号之前,会通过DDC通道读取显示端的EDID,确认支持的分辨率、刷新率和色彩格式。如果EDID没写或者内容不对,源端可能直接不输出信号,或者输出一个奇怪的分辨率。MZ7035FA板卡上ADV7611的DDC引脚直接连到了HDMI座子,所以EDID必须由我们通过IIC写入。
EDID 1.3标准长度是128字节,其中前16字节是头部和厂商信息,必须严格符合规范。项目里如果只是接收固定1080p60的信号源,可以简化EDID内容,但校验和必须算对,否则源端不认。计算方式是前127字节求和,取低8位的补码填入第127字节。
static void adv7611_edid_fix_checksum(uint8_t *edid) { uint8_t sum = 0; /* 前127字节求和,目标是让总和为0x00 */ for (int i = 0; i < 127; i++) { sum += edid[i]; } edid[127] = (uint8_t)(0x00 - sum); }这段校验和逻辑其实是把EDID配置中"最后做补码求和"这个步骤拆出来了。0x00 - sum的运算等价于取sum的补码,这样前127字节加上第127字节的和刚好是0x00,符合EDID规范的校验要求。写EDID时建议一页一页连续写,因为ADV7611的EDID RAM是双端口,一边读一边写可能产生不一致。
3. PL端HDMI回环逻辑与分辨率参数解析
3.1 硬件管脚与时钟规划
ADV7611输出的像素接口是并行RGB或者YCbCr,加上行场同步、时钟和数据使能信号。MZ7035FA上ADV7611与FPGA之间的连接走的是PL侧普通IO,没有走硬核收发器,这点和HDMI的TMDS物理层不同。ADV7611已经把TMDS信号解码成并行数据了,FPGA这边只需要处理并行总线和同步信号。
常见的接法是把ADV7611的像素时钟PIX_CLK连接到PL的全局时钟引脚,数据总线接到普通IO bank。ZYNQ的IO bank电压要注意,ADV7611的IO电压通常是1.8V或2.5V,必须检查板级原理图确认bank电压匹配,否则数据采进来全是乱的。
回环验证的思路很简单:把ADV7611输出的并行数据和同步信号,直接接到HDMI发送端芯片的输入。如果板上HDMI发送端也是并行接口,回环路径就是纯逻辑直通,不经过DDR缓存。这样做的好处是延迟极低,坏处是如果输入分辨率变化,输出不做帧率转换,会跟着变。
3.2 基于FIFO的跨时钟回环实现
ADV7611输出的像素时钟和ZYNQ PL侧逻辑的时钟域可能不同步,直接打拍拼接大概率出问题。我一般会在回环路径上放一个异步FIFO,写入端用ADV7611的像素时钟,读出端用HDMI发送芯片的像素时钟。两个时钟频率接近但相位不同步时,FIFO能起到缓冲作用,但要注意FIFO深度设计,太大增加延迟,太小容易溢出。
module hdmi_loopback_fifo #( parameter DATA_WIDTH = 24, parameter FIFO_DEPTH = 1024 )( input wire wr_clk, input wire wr_en, input wire [DATA_WIDTH-1:0] wr_data, input wire rd_clk, input wire rd_en, output wire [DATA_WIDTH-1:0] rd_data, output wire fifo_full, output wire fifo_empty ); /* 调用Xilinx FIFO IP核,原生模式 */ fifo_generator_i0 fifo_inst ( .rst (1'b0), .wr_clk (wr_clk), .rd_clk (rd_clk), .din (wr_data), .wr_en (wr_en), .rd_en (rd_en), .dout (rd_data), .full (fifo_full), .empty (fifo_empty), .wr_rst_busy (), .rd_rst_busy () ); /* 写使能由外部同步信号控制 */ assign wr_en = vsync_active && hsync_active && data_valid; endmodule这里在回环路径上加了同步使能条件,写使能只有在行场有效期间才拉高,避免消隐期间写入无效数据。FIFO的wr_rst_busy和rd_rst_busy信号没接出来,实际使用时建议引到LED调试,FIFO复位的释放时序如果不对,首帧数据会出现错位。
3.3 分辨率参数解析策略
PL侧解析分辨率的核心是测量行场同步信号的周期。像素时钟下,用计数器统计一个完整行周期内的像素数,再统计一个完整场周期内的行数,两个数值就能算出分辨率。这个方法不需要查表,输入信号变化时自动适应。
reg [15:0] h_cnt; reg [15:0] v_cnt; reg h_sync_d; reg v_sync_d; always @(posedge pix_clk) begin h_sync_d <= h_sync; v_sync_d <= v_sync; /* 行同步上升沿,说明新的一行开始 */ if (h_sync && !h_sync_d) begin v_cnt <= v_sync ? v_cnt + 1'b1 : 16'd0; h_cnt <= 16'd1; end else begin h_cnt <= h_cnt + 1'b1; end /* 场同步上升沿,一帧结束,锁存统计结果 */ if (v_sync && !v_sync_d) begin reg_h_total <= h_cnt; reg_v_total <= v_cnt; end end这段逻辑在行同步上升沿累加行计数器,在场同步上升沿锁存结果。注意v_cnt的复位条件用了v_sync的实时值而不是延时值,这是为了防止场同步恰好和行同步同时出现时计数出错。锁存出来的reg_h_total和reg_v_total就是实际的行像素数和行数,与1080p的1920和1080做比较就能确认输入格式。解析结果可以在SDK里通过AXI GPIO读取,也可以直接在ILA里观察。
3.4 像素时钟与数据对齐问题
ADV7611输出的像素时钟默认是上升沿采样数据,但走线长度不同会导致数据和时钟的相位偏移。遇到图像有彩色噪点或左右移动的花屏,优先检查数据采样沿。Xilinx的IDELAY原语可以调整单根数据线的延迟,但更简单的做法是先看ADV7611的输出格式寄存器设置,确认是否把时钟极性反转了。
/* 使用IDELAYE2调整单bit延迟,每个tap约78ps @ 200MHz参考时钟 */ IDELAYE2 #( .IDELAY_TYPE("FIXED"), .DELAY_SRC ("DATAIN"), .IDELAY_VALUE(12) ) idelay_d0 ( .IDATAIN(pix_data[0]), .DATAOUT(data_delayed[0]), .C(pix_clk), .CE(1'b0), .INC(1'b0), .CINVCTRL(1'b0), .CNTVALUEIN(8'd0), .CNTVALUEOUT(), .LD(1'b0), .LDPIPEEN(1'b0), .REGRST(1'b0) );这段IDELAYE2的配置把数据线延迟了12个tap,对应大约936ps。这个值是经验值,实际项目中可以用IO约束里的INPUT_DELAY配合时序报告来调整。如果所有数据线都延迟,不如直接在约束文件里把PACKAGE_PIN对应的输入延迟统一设置,代码里只针对特殊长度的信号做微调。
4. SDK工程搭建与软件配置流程
4.1 Vivado硬件工程中的关键配置
Vivado 2017.4下创建ZYNQ硬件工程,PS端配置要打开IIC0或IIC1控制器。MZ7035FA板级原理图上ADV7611的IIC总线接在PS的MIO14和MIO15上,对应IIC1控制器。如果板子把IIC引到了EMIO,需要在ZYNQ IP配置里把IIC接口改成EMIO,并分配PL侧引脚。
# ZYNQ PS配置IIC1为MIO14/MIO15 set_property -dict [list \ CONFIG.PCW_USE_IIC1 {1} \ CONFIG.PCW_IIC1_I2C_FREQ {100000} \ ] [get_bd_cells processing_system7_0]这段Tcl命令设置了IIC1的时钟频率为100kHz。ADV7611的IIC最高支持400kHz,但PCB走线质量和上拉电阻决定实际能跑多快,调试阶段用100kHz更稳。上拉电阻的值对IIC信号质量影响很大,MZ7035FA板上用的是2.2kΩ上拉到3.3V,如果自己画板,建议按总线上设备数量和总线电容来算,电容超过100pF就把上拉降到1.5kΩ左右。
4.2 BSP与驱动初始化顺序
SDK里创建应用工程时,BSP默认会把XIic驱动注册进系统。初始化顺序有讲究:先复位ADV7611,再初始化IIC控制器,最后写寄存器序列。芯片不复位直接配置,可能因为内部状态机未初始化导致某些寄存器写不进去。
#include "xiic.h" #include "xgpiops.h" #define IIC_DEVICE_ID XPAR_XIICPS_1_DEVICE_ID #define GPIO_DEVICE_ID XPAR_XGPIOPS_0_DEVICE_ID #define ADV7611_RST_PIN 78 /* MIO78接到ADV7611复位脚 */ int adv7611_init(void) { XIicPs iic_inst; XGpioPs gpio_inst; XIicPs_Config *iic_cfg; /* 先拉低复位,至少保持10ms */ XGpioPs_WritePin(&gpio_inst, ADV7611_RST_PIN, 0); usleep(10000); XGpioPs_WritePin(&gpio_inst, ADV7611_RST_PIN, 1); usleep(20000); /* PS端IIC控制器初始化 */ iic_cfg = XIicPs_LookupConfig(IIC_DEVICE_ID); XIicPs_CfgInitialize(&iic_inst, iic_cfg, iic_cfg->BaseAddress); XIicPs_SetSClk(&iic_inst, 100000); /* 写入寄存器初始化序列 */ for (int i = 0; i < init_table_len; i++) { adv7611_write_reg(init_table[i].map, init_table[i].reg, init_table[i].val); } return 0; }初始化序列里的init_table和init_table_len来自第2.2节的寄存器配置表,建议把配置表独立成数组放在单独的头文件里,方便不同分辨率或不同板卡之间切换。复位管脚在MIO78上,需要先确认板级原理图,别盲写管脚号,曾经遇到一块定制板把复位接到了PL侧IO上,结果软件怎么复位都没反应。
4.3 IIC读回校验与版本确认
写完寄存器序列不能直接跑,得读回ADV7611的ID寄存器确认芯片响应正常。ID寄存器在任意映射页都能读到,写法是先切页到目标映射,再直接读0x00或0x01。读回的数据和预期不符时,先用示波器看IIC波形,确认SDA和SCL上有没有正常的ACK。
static int adv7611_verify(void) { uint8_t val = 0; /* 切到主映射,读芯片ID寄存器0x00 */ adv7611_write_reg(0x00, REG_MAP_ADDR, 0x00); XIic_Recv(BASEADDR, DEV_ADDR, &val, 1, XIIC_STOP); xil_printf("ADV7611 ID: 0x%02x\r\n", val); /* 正确ID应为0x00或0x01 */ if (val == 0x00 || val == 0x01) { return 0; } return -1; }这里读ID用了一次空写来设置寄存器地址,再发起读操作。有些IIC控制器支持重复起始条件,可以先把寄存器地址发出去,然后在同一个传输事务里直接读,但Xilinx的XIic驱动里分开调用反而更安全。ID读出来是0x00或0x01对应的都是ADV7611,0xFF说明芯片没在总线上,0x55或其它值说明地址配错了。
4.4 done信号与GPIO输出
PS侧初始化完成后,把结果通过MIO或EMIO输出到PL侧的LED,方便观察。这里用GPIO输出拉高作为done信号,PL侧可以把done信号接到逻辑里做输入锁定,也可以直接驱动LED。MZ7035FA板上有几个用户LED,通过MIO连接到PS端,直接用XGpioPs驱动即可。
/* 配置完成,拉高LED */ XGpioPs_SetDirection(&gpio_inst, LED_PIN_MASK, 1); XGpioPs_WritePin(&gpio_inst, LED_PIN, 1);拉高LED这个操作放在配置序列和读回校验都成功之后,不然LED一亮就以为配置成功了,实际可能只是部分寄存器写进去了。实际项目里我习惯再加一个心跳闪烁,跑一个1s周期的翻转任务,看到闪烁说明PS的调度活着,纯延时点灯不能证明系统状态。
5. 用ILA抓IIC时序反推配置问题
配置完发现HDMI没有图像,第一件事不是改代码,而是抓IIC波形。把ILA的探针挂在SDA和SCL上,采样时钟用200MHz,触发条件设置成SCL上升沿。这个抓法能看到完整的IIC事务,包括起始条件、地址字节、寄存器地址和数据字节。重点看ACK位:ADV7611在第九个时钟周期拉低SDA表示ACK,如果一直保持高电平,说明设备没响应或者地址不对。
抓完波形后对着数据手册查每个字节。很多人配置不成功的原因是寄存器写顺序不对,比如先使能了HDMI输入再写EDID,结果EDID被芯片内部状态机覆盖。ILA里能看到写入顺序时,就能定位问题在哪个环节。
我常做的验证流程是:先用SDK读回ID和几个关键寄存器的值,确认IIC通路没问题;再用ILA抓HDMI输入侧的数据有效信号,看是否有行场同步输出;最后才去查图像质量。如果行场同步都没有,问题大概率在ADV7611配置或者输入信号本身,用示波器量HDMI座子的TMDS信号就能区分。
把IIC寄存器配置做成一个通用的初始化数组,把EDID写入和寄存器初始化拆成两个独立函数,后续换分辨率或者换HDMI芯片时,只需要改数组内容,不用动整体流程。这套方法在MZ7035FA上验证过,跑1080p60输入回环输出稳定,也能用到其它带PS侧IIC的ZYNQ平台上。
本文还有配套的精品资源,点击获取