FPGA开发中的常用通信协议与通信接口区别与联系

协议------HDMI

  之前的博客详细介绍了 VGA 的原理和基本使用,这次换成 HDMI,增加一个技能点。

一、HDMI 基本介绍

  HDMI 是新一代的多媒体接口标准,英文全称是 High-Definition Multimedia Interface,即高清多媒体接口。它能够同时传输视频和音频,简化了设备的接口和连线;同时提供了更高的数据传输带宽,可以传输无压缩的数字音频及高分辨率视频信号。HDMI 1.0 版本于 2002 年发布,最高数据传输速度为 5Gbps;而2017 年发布的 HDMI 2.1 标准的理论带宽可达 48Gbps。

  HDMI 向下兼容 DVI,但是 DVI(数字视频接口)只能用来传输视频,而不能同时传输音频,这是两者最主要的差别。此外,DVI 接口的尺寸明显大于 HDMI 接口,如下图所示:

  HDMI 的引脚定义如下:

  TMDS(Transition Minimized Differential Signaling,最小化传输差分信号)是美国 Silicon Image 公司开发的一项高速数据传输技术,在 DVI 和 HDMI 视频接口中使用差分信号传输高速串行数据。

  TMDS 差分传输技术使用两个引脚(如图中的"数据 2+"和"数据 2-")来传输一路信号,利用这两个引脚间的电压差的正负极性和大小来决定传输数据的数值(0 或 1)。TMDS 传输系统分为两个部分:发送端和接收端。 TMDS 链路包括 3 个传输 RGB 信号的数据通道和 1 个传输时钟信号的通道。TMDS 发送端对这些数据进行编码和并/串转换,再将数据分别分配到独立的传输通道发送出去。接收端接收来自发送端的串行信号,对其进行解码和串/并转换,然后发送到显示器的控制端。与此同时也接收时钟信号,以实现同步。每一个数据通道都通过编码算法,将 8 位数据转换成最小化传输、直流平衡的 10 位数据。这使得数据的传输和恢复更加可靠。最小化传输差分信号是通过异或及异或非等逻辑算法将原始 8 位信号数据转换成 10 位,前 8 为数据由原始信号经运算后获得,第 9 位指示运算的方式,第 10 位用来对应直流平衡。如下图所示:

  一般来说,HDMI 传输的编码格式中要包含视频数据、控制数据和数据包(数据包中包吨音频数据和附加信息数据,例如纠错码等)。 TMDS 每个通道在传输时要包含一个 2bit 的控制数据、 8bit 的视频数据或者 4bit 的数据包即可。在 HDMI 信息传输过程中,可以分为三个阶段:视频数据传输周期、控制数据传输周期和数据岛传输周期,分别对应上述的三种数据类型。

1、传输最小化

  8 位数据经过编码和直流平衡得到 10 位最小化数据,这仿佛增加了冗余位,对传输链路的带宽要求更高,但事实上,通过这种算法得到的 10 位数据在更长的同轴电缆中传输的可靠性增强了。下图是一个例子,说明对一个 8 位的并行 RED 数据编码、并/串转换。

(1)将 8 位并行 RED 数据发送到 TMDS 収送端。

(2)并/串转换。

(3)进行最小化传输处理,加上第 9 位,即编码过程。第 9 位数据称为编码位。

2、直流平衡

  直流平衡(DC-balanced)就是指在编码过程中保证信道中直流偏移为零。方法是在原来的 9 位数据癿后面加上第 10 位数据,返样,传输的数据趋于直流平衡,使信号对传输线的电磁干扰减少,提高信号传输的可靠性。

3. 差分信号

  TMDS 差分传动技术是一种利用2个引脚间电压差来传送信号的技术。传输数据的数值("0"或者"1")由两脚间电压正负极性和大小决定。即,采用 2 根线来传输信号,一根线上传输原来的信号,另一根线上传输与原来信号相反的信号。这样接收端就可以通过让一根线上的信号减去另一根线上的信号的方式来屏蔽电磁干扰,从而得到正确的信号。

  另外,还有一个显示数据通道(DDC),是用于读取表示接收端显示器的清晰度等显示能力的扩展显示标识数据(EDID)的信号线。搭载 HDCP(High-bandwidth Digital Content Protection,高带宽数字内容保护技术)的发送、接收设备之间也利用 DDC 线进行密码键的认证。

二、DVI 和 HDMI

  上图是 TMDS 发送端和接收端的连接示意图。DVI 或 HDMI 视频传输所使用的 TMDS 连接通过四个串行通道实现。对于DVI来说,其中三个通道分别用于传输视频中每个像素点的红、绿、蓝三个颜色分量(RGB4:4:4格式)。HDMI 默认也是使用三个 RGB 通道,但是它同样可以选择传输像素点的亮度和色度信息(YCrCb4:4:4或YCrCb 4:2:2格式)。第四个通道是时钟通道,用于传输像素时钟。独立的 TMDS 时钟通道为接收端提供接收的参考频率,保证数据在接收端能够正确恢复。

  在传输视频图像的过程中,数据通道上传输的是编码后的有效像素字符 。而在每一帧图像的行与行之间,以及视频中不同帧之间的时间间隔(消隐期)内,数据通道上传输的则是控制字符。每个通道上有两位控制信号的输入接口,共对应四种不同的控制字符。这些控制字符提供了视频的行同步(HZYNC)以及帧同步(VSYNC)信息,也可以用来指定所传输数据的边界(用于同步)。对于 DVI 传输,整个视频的消隐期都用来传输控制字符。而 HDMI 传输的消隐期除了控制字符之外,还可以用于传输音频或者其他附加数据(例如字母信息),4-bit 音频和附加数据将通过 TERC4 编码机制转换成 10-bit TERC4 字符,然后再绿色和红色通道上传输。从上图也可以看出这一差别,即"Auxiliary Data"接口标有"HDMI Olny",即它是 HDMI 所独有的接口。如果我们不需要附加数据,只传输视频数据的话,完全可以把 HDMI 接口当做 DVI 接口进行驱动。关于其他区别,可以查看这个表:

  下图是 DVI 编码器示意图:

  每个通道输入的视频像素数据都要使用 DVI 规范中的 TMDS 编码算法进行编码。每个 8-bit 的数据都将被转换成 460 个特定 10-bit 字符中的一个。这个编码机制大致上实现了传输过程中的直流平衡,即一段时间内传输的高电平(数字"1")的个数大致等于低电平(数字"0")的个数。同时,每个编码后的 10-bit 字符中状态跳转("由 1 到 0"或者"由 0 到 1")的次数将被限制在五次以内。除了视频数据之外,每个通道 2-bit 控制信号的状态也要进行编码,编码后分别对应四个不同的 10-bit 控制字符,分别是 10'b1101010100,10'b0010101011,10'b0101010100,和 10'b1010101011。可以看出,每个控制字符都有七次以上的状态跳转。视频字符和控制字符状态跳转次数的不同将会被用于发送和接收设备的同步。

  再重复一遍,HDMI 在输入附加数据的同时,还需要输入 ADE(Aux/Audio Data Enable)信号,其作用和 VDE 是类似的:当 ADE 为高电平时,表明输入端的附加数据或者音频数据有效,DIV 是不能传音频的。想了解更多有关 HDMI的细节,可以参考HDMI 接口规范------《High-Definition Multimedia Interface Specification Version 1.3a》,英语不好的也可以查看文档《HDMI1.4规范中文版》。

三、HDMI 电路原理

  这里直接引用正点原子领航者 ZYNQ 底板的 HDMI 接口原理图来说明:

  HDMI 的三个数据通道 HDMI_D2:0 只和一个时钟通道 HDMI_CLK 直接与 TMDS 差分引脚相连。

  • HDMI_CEC 指的是用户电气控制(Consumer Electronics Control),它用于 HDMI 连接线上的设备之间进行信息交换。当一个设备的状态发生变化时,CEC 可以使用远程控制或自动改变设置来命令连接的关联设备的状态发生相应的变化。例如,如果用户放置一张碟片到蓝光播放器并开始播放,那么高清电视机将会自动打开电源,设置正确的视频输入格式和打开环绕声设备等等,这种关联通信提供了一个更好的客户体验。
  • HDMI_HPD 指的是热拔插检测(Hot Plug Detect),当视频设备与接收设备通过 HDMI 连接时,接收设备将 HPD 置为高电平,通知发送设备。当发送设备检测到 HPD 为低电平时,表明断开连接。
  • HDMI_OUT_EN **信号,**用于设置 HDMI 接口的输入输出模式,当其为高电平时作为输出端,此时由 FPGA 底板输出 HDMI 接口的 5V 电源。同时,HDMI_HPD 将作为输入信号使用。反之,当 HDMI_OUT_EN 为低电平时,HDMI_HPD 将输出高电平,用于指示 HDMI 连接状态。
  • HDMI_SCL 和 HDMI_SDA信号, 是 HDMI 接口的显示数据通道(DDC,Display Data Channel),用于 HDMI 发送端和接收端之间交换一些配置信息,通过 I2C 协议通信。发送端通过 DDC 通道,读取接收端保存在 EEPROM 中的 EDID 数据,获取接收端的信息,确认接收端终端显示的设置和功能,决定跟接收端之间以什么格式传输音/视频数据。

  本次实验只使用到了 TMDS 数据、TMDS 时钟以及 HDMI 输出使能等信号,相关的管脚约束如下所示:

复制代码
set_property IOSTANDARD TMDS_33 [get_ports HDMI_clk_p]
set_property IOSTANDARD TMDS_33 [get_ports HDMI_clk_n]
set_property IOSTANDARD TMDS_33 [get_ports HDMI_d0_p]
set_property IOSTANDARD TMDS_33 [get_ports HDMI_d0_n]
set_property IOSTANDARD TMDS_33 [get_ports HDMI_d1_p]
set_property IOSTANDARD TMDS_33 [get_ports HDMI_d1_n]
set_property IOSTANDARD TMDS_33 [get_ports HDMI_d2_p]
set_property IOSTANDARD TMDS_33 [get_ports HDMI_d2_n]
set_property PACKAGE_PIN C18 [get_ports HDMI_clk_p]
set_property PACKAGE_PIN D20 [get_ports HDMI_d0_p]
set_property PACKAGE_PIN C22 [get_ports HDMI_d1_p]
set_property PACKAGE_PIN B21 [get_ports HDMI_d2_p]

  需要注意的是,TMDS 数据和时钟信号需要在约束文件中指定电平标准为 TMDS_33。另外,对于差分信号我们只需要指定正极的引脚位置,工具会自动对负极进行管脚分配。

四、HDMI 程序设计

  TMDS 连接从逻辑功能上可以划分成两个阶段:编码和并串转换。在编码阶段,编码器将视频源中的像素数据、HDMI 的音频/附加数据、行同步和场同步信号分别编码成 10 位的字符流,然后在并串转换阶段将上述的字符流转换成串行数据流,并将其从三个差分输出通道发送出去。示意图如下所示:

  将其放大,可以得到下面这个框图:

  Encoder 模块负责对数据进行编码,Serializer 模块对编码后的数据进行并串转换,最后通过 OBUFDS 转化成 TMDS 差分信号传输。 图中左下脚 HDMI 的音频/附加数据输入在本次实验中并未用到,因此以虚线表示。

1、encoder:8B/10B

  8B10B 编码是可以从 Xilinx 官网下载到此模块,模块名为 encode,此模块把 8 比特的数据进行从新映射为 10bit 数据,防止连续的 0 和 1 出现导致直流不平衡造成误码率升高。

复制代码
  1 `timescale 1 ps / 1ps
  2 
  3 module encode (
  4   input            clkin,    // pixel clock input
  5   input            rstin,    // async. reset input (active high)
  6   input      [7:0] din,      // data inputs: expect registered
  7   input            c0,       // c0 input
  8   input            c1,       // c1 input
  9   input            de,       // de input
 10   output reg [9:0] dout      // data outputs
 11 );
 12 
 13   ////////////////////////////////////////////////////////////
 14   // Counting number of 1s and 0s for each incoming pixel
 15   // component. Pipe line the result.
 16   // Register Data Input so it matches the pipe lined adder
 17   // output
 18   ////////////////////////////////////////////////////////////
 19   reg [3:0] n1d; //number of 1s in din
 20   reg [7:0] din_q;
 21 
 22   always @ (posedge clkin) begin
 23     n1d <= din[0] + din[1] + din[2] + din[3] + din[4] + din[5] + din[6] + din[7];
 24 
 25     din_q <= din;
 26   end
 27 
 28   ///////////////////////////////////////////////////////
 29   // Stage 1: 8 bit -> 9 bit
 30   // Refer to DVI 1.0 Specification, page 29, Figure 3-5
 31   ///////////////////////////////////////////////////////
 32   wire decision1;
 33 
 34   assign decision1 = (n1d > 4'h4) | ((n1d == 4'h4) & (din_q[0] == 1'b0));
 35 /*
 36   reg [8:0] q_m;
 37   always @ (posedge clkin) begin
 38     q_m[0] <=#1 din_q[0];
 39     q_m[1] <=#1 (decision1) ? (q_m[0] ^~ din_q[1]) : (q_m[0] ^ din_q[1]);
 40     q_m[2] <=#1 (decision1) ? (q_m[1] ^~ din_q[2]) : (q_m[1] ^ din_q[2]);
 41     q_m[3] <=#1 (decision1) ? (q_m[2] ^~ din_q[3]) : (q_m[2] ^ din_q[3]);
 42     q_m[4] <=#1 (decision1) ? (q_m[3] ^~ din_q[4]) : (q_m[3] ^ din_q[4]);
 43     q_m[5] <=#1 (decision1) ? (q_m[4] ^~ din_q[5]) : (q_m[4] ^ din_q[5]);
 44     q_m[6] <=#1 (decision1) ? (q_m[5] ^~ din_q[6]) : (q_m[5] ^ din_q[6]);
 45     q_m[7] <=#1 (decision1) ? (q_m[6] ^~ din_q[7]) : (q_m[6] ^ din_q[7]);
 46     q_m[8] <=#1 (decision1) ? 1'b0 : 1'b1;
 47   end
 48 */
 49   wire [8:0] q_m;
 50   assign q_m[0] = din_q[0];
 51   assign q_m[1] = (decision1) ? (q_m[0] ^~ din_q[1]) : (q_m[0] ^ din_q[1]);
 52   assign q_m[2] = (decision1) ? (q_m[1] ^~ din_q[2]) : (q_m[1] ^ din_q[2]);
 53   assign q_m[3] = (decision1) ? (q_m[2] ^~ din_q[3]) : (q_m[2] ^ din_q[3]);
 54   assign q_m[4] = (decision1) ? (q_m[3] ^~ din_q[4]) : (q_m[3] ^ din_q[4]);
 55   assign q_m[5] = (decision1) ? (q_m[4] ^~ din_q[5]) : (q_m[4] ^ din_q[5]);
 56   assign q_m[6] = (decision1) ? (q_m[5] ^~ din_q[6]) : (q_m[5] ^ din_q[6]);
 57   assign q_m[7] = (decision1) ? (q_m[6] ^~ din_q[7]) : (q_m[6] ^ din_q[7]);
 58   assign q_m[8] = (decision1) ? 1'b0 : 1'b1;
 59 
 60   /////////////////////////////////////////////////////////
 61   // Stage 2: 9 bit -> 10 bit
 62   // Refer to DVI 1.0 Specification, page 29, Figure 3-5
 63   /////////////////////////////////////////////////////////
 64   reg [3:0] n1q_m, n0q_m; // number of 1s and 0s for q_m
 65   always @ (posedge clkin) begin
 66     n1q_m  <= q_m[0] + q_m[1] + q_m[2] + q_m[3] + q_m[4] + q_m[5] + q_m[6] + q_m[7];
 67     n0q_m  <= 4'h8 - (q_m[0] + q_m[1] + q_m[2] + q_m[3] + q_m[4] + q_m[5] + q_m[6] + q_m[7]);
 68   end
 69 
 70   parameter CTRLTOKEN0 = 10'b1101010100;
 71   parameter CTRLTOKEN1 = 10'b0010101011;
 72   parameter CTRLTOKEN2 = 10'b0101010100;
 73   parameter CTRLTOKEN3 = 10'b1010101011;
 74 
 75   reg [4:0] cnt; //disparity counter, MSB is the sign bit
 76   wire decision2, decision3;
 77 
 78   assign decision2 = (cnt == 5'h0) | (n1q_m == n0q_m);
 79   /////////////////////////////////////////////////////////////////////////
 80   // [(cnt > 0) and (N1q_m > N0q_m)] or [(cnt < 0) and (N0q_m > N1q_m)]
 81   /////////////////////////////////////////////////////////////////////////
 82   assign decision3 = (~cnt[4] & (n1q_m > n0q_m)) | (cnt[4] & (n0q_m > n1q_m));
 83 
 84   ////////////////////////////////////
 85   // pipe line alignment
 86   ////////////////////////////////////
 87   reg       de_q, de_reg;
 88   reg       c0_q, c1_q;
 89   reg       c0_reg, c1_reg;
 90   reg [8:0] q_m_reg;
 91 
 92   always @ (posedge clkin) begin
 93     de_q    <= de;
 94     de_reg  <= de_q;
 95     
 96     c0_q    <= c0;
 97     c0_reg  <= c0_q;
 98     c1_q    <= c1;
 99     c1_reg  <= c1_q;
100 
101     q_m_reg <= q_m;
102   end
103 
104   ///////////////////////////////
105   // 10-bit out
106   // disparity counter
107   ///////////////////////////////
108   always @ (posedge clkin or posedge rstin) begin
109     if(rstin) begin
110       dout <= 10'h0;
111       cnt <= 5'h0;
112     end else begin
113       if (de_reg) begin
114         if(decision2) begin
115           dout[9]   <= ~q_m_reg[8]; 
116           dout[8]   <= q_m_reg[8]; 
117           dout[7:0] <= (q_m_reg[8]) ? q_m_reg[7:0] : ~q_m_reg[7:0];
118 
119           cnt <=#1 (~q_m_reg[8]) ? (cnt + n0q_m - n1q_m) : (cnt + n1q_m - n0q_m);
120         end else begin
121           if(decision3) begin
122             dout[9]   <= 1'b1;
123             dout[8]   <= q_m_reg[8];
124             dout[7:0] <= ~q_m_reg[7:0];
125 
126             cnt <=#1 cnt + {q_m_reg[8], 1'b0} + (n0q_m - n1q_m);
127           end else begin
128             dout[9]   <= 1'b0;
129             dout[8]   <= q_m_reg[8];
130             dout[7:0] <= q_m_reg[7:0];
131 
132             cnt <= cnt - {~q_m_reg[8], 1'b0} + (n1q_m - n0q_m);
133           end
134         end
135       end else begin
136         case ({c1_reg, c0_reg})
137           2'b00:   dout <= CTRLTOKEN0;
138           2'b01:   dout <= CTRLTOKEN1;
139           2'b10:   dout <= CTRLTOKEN2;
140           default: dout <= CTRLTOKEN3;
141         endcase
142 
143         cnt <= 5'h0;
144       end
145     end
146   end
147   
148 endmodule

  encoder 模块按照 DVI 接口规范中 TMDS 编码算法对输入的 8 位像素数据以及 2 位行场同步信号进 行编码.该模块是 Xilinx 应用笔记 XAPP460 中所提供的编码模块,其具体实现的编码算法如下图所示:

  TMDS 通过逻辑算法将 8 位字符数据通过最小转换编码为 10 位字符数据,前 8 位数据由原始信号经运算后获得,第 9 位表示运算的方式,1 表示异或 0 表示异或非。经过 DC 平衡后(第 10 位),采用差分信号传输数据。第 10 位实际是一个反转标志位,1 表示进行了反转而 0 表示没有反转,从而达到 DC 平衡。接收端在收到信号后,再进行相反的运算。TMDS 和 LVDS、TTL 相比有较好的电磁兼容性能。这种算法可以减小传输信号过程的上冲和下冲,而 DC 平衡使信号对传输线的电磁干扰减少,可以用低成本的专用电缆实现长距离、高质量的数字信号传输。图中所描述的算法是 DVI 接口规范所定义的,我们不作深入研究,算法中各个参数的含义如下图所示:

|--------|--------------|
| D | 视频信号 |
| C0 C1  | 控制信号 |
| DE | 使能信号 |
| Cnt | 寄存器参数 |
| N0{X} | 输入视频信号"1"的个数 |
| N1{X} | 输入视频信号"0"的个数 |
| q_out | 编码输出 |

  encoder模块的调用代码如下所示,注意 u_encode_blue 里的 c0 和 c1 输入了 hsync 和 vsync,另两个颜色通道则不需要。

复制代码
//==========================================================================
//==    encode 例化,RGB三个通道
//==========================================================================
encode u_encode_red
(
    .clkin              (HDMI_clk1x                 ),
    .rstin              (HDMI_rst                   ),
    .din                (VGA_r                      ),
    .c0                 (1'b0                       ),
    .c1                 (1'b0                       ),
    .de                 (VGA_de                     ),
    .dout               (dout_r                     )
);

encode u_encode_green
(
    .clkin              (HDMI_clk1x                 ),
    .rstin              (HDMI_rst                   ),
    .din                (VGA_g                      ),
    .c0                 (1'b0                       ),
    .c1                 (1'b0                       ),
    .de                 (VGA_de                     ),
    .dout               (dout_g                     )
);

encode u_encode_blue
(
    .clkin              (HDMI_clk1x                 ),
    .rstin              (HDMI_rst                   ),
    .din                (VGA_b                      ),
    .c0                 (VGA_hsync                  ), //hsync
    .c1                 (VGA_vsync                  ), //vsync
    .de                 (VGA_de                     ),
    .dout               (dout_b                     )
);

2、Serializer:并转串

  Serializer10_1 模块通过调用 OSERDESE2 原语来实现 10:1 的并串转换。原语是 Xilinx 器件底层硬件中的功能模块,它使用专用的资源来实现一系列的功能。相比于 IP 核,原语的调用方法更简单,但是一般只用于实现一些简单的功能。OSERDESE2 详细介绍见 Xilinx 官方提供的UG768手册。OSERDESE2 原语模型如下所示:

  一个 OSERDESE2 只能实现最多 8:1 的转换率,在这里我们通过位宽扩展实现了 10:1 的并串转换,如下图所示:

  OSERDESE2原语的文档中介绍有:该原语可以进行8:1、10:1、14:1转换,当进行8:1转换的时候,采用一个原语即可,当采用10:1或者14:1转换的时候,需要采用两个原语级联的方式。我们需要将 10bit 数据进行串行化,因此根据文档说明,我们需要使用两个OSERDESE2原语进行级联,级联后的框图如下图所示。OSERDESE2 原语的 Master 端承接 10bit 数据的低 8 位,OSERDESE2 原语的 Slave 端的 D1、D2 不使用,D3 和 D4 承接10bit数据的高 2bit,(若是14:1则可以继续使用D5,D6,D7,D8,依然不使用D1,D2)。并将 Slave 的输出 SHIFTOUT1, SHIFTOUT2 连接到 master 的 SHIFTIN1, SHIFTIN2,输入的 10bit 数据将会按照由低位到高位的顺序从 DataOut 端输出。

  OSERDESE2原语获取方法如下所示:

  整个系统需要两个输入时钟,一个是视频的像素时钟 div_clk,另外一个时钟 ser_clk 的频率是像素时钟的五倍。由前面的简介部分我们知道,并串转换过程的实现的是 10:1 的转换率,理论上转换器需要一个 10 倍像素频率的时钟。这里我们只需要一个 5 倍的时钟频率,这是因为 OSERDESE2 模块可以实现 DDR 的功能,即它在五倍时钟频率的基础上又实现了双倍数据速率。

  注意,OSERDESE2 模块要求复位信号高电平有效,并且需要将异步复位信号同步到串行时钟域,因此还要加入一个异步复位信号处理代码,完整的 OSERDESE2 模块的调用代码如下所示:

复制代码
//==========================================================================
//==    时钟同步
//==========================================================================
always @(posedge divclk or posedge rst) begin
    if (rst) begin
        ini_rst <= 1'b1;
    end
    else begin
        ini_rst <= 1'b0;
    end
end
//==========================================================================
//==    OSERDESE2 master 原语例化1:0-7
//==========================================================================
OSERDESE2
#(
   .DATA_RATE_OQ    ("DDR"          ), // 最终输出的数据类型:DDR,SDR
   .DATA_RATE_TQ    ("SDR"          ), // 输出的buffer:DDR, BUF, SDR
   .DATA_WIDTH      (10             ), // Parallel data width (2-8,10,14),数据位宽
   .INIT_OQ         (1'b0           ), // Initial value of OQ output (1'b0,1'b1)
   .INIT_TQ         (1'b0           ), // Initial value of TQ output (1'b0,1'b1)
   .SERDES_MODE     ("MASTER"       ), // MASTER, SLAVE,主机模式
   .SRVAL_OQ        (1'b0           ), // OQ output value when SR is used (1'b0,1'b1)
   .SRVAL_TQ        (1'b0           ), // TQ output value when SR is used (1'b0,1'b1)
   .TBYTE_CTL       ("FALSE"        ), // Enable tristate byte operation (FALSE, TRUE),三态控制
   .TBYTE_SRC       ("FALSE"        ), // Tristate byte source (FALSE, TRUE),三态控制
   .TRISTATE_WIDTH  (1              )  // 3-state converter width (1,4)
)
OSERDESE2_inst_master
(
   .OFB             (               ), // 1-bit output: Feedback path for data,未用到,删除即关闭
   .OQ              (do             ), // 1-bit output: Data path output 输出路径
   
   // SHIFTOUT1 / SHIFTOUT2: 1-bit (each) output: Data output expansion (1-bit each)
   .SHIFTOUT1       (               ), // 未用到,删除即关闭
   .SHIFTOUT2       (               ), // 未用到,删除即关闭
   .TBYTEOUT        (               ), // 1-bit output: Byte group tristate,未用到,删除即关闭
   .TFB             (               ), // 1-bit output: 3-state control,未用到,删除即关闭
   .TQ              (               ), // 1-bit output: 3-state control,未用到,删除即关闭
   .CLK             (serclk         ), // 1-bit input: High speed clock
   .CLKDIV          (divclk         ), // 1-bit input: Divided clock
   
   // D1 - D8: 1-bit (each) input: Parallel data inputs (1-bit each),数据
   .D1              (din[0]         ),
   .D2              (din[1]         ),
   .D3              (din[2]         ),
   .D4              (din[3]         ),
   .D5              (din[4]         ),
   .D6              (din[5]         ),
   .D7              (din[6]         ),
   .D8              (din[7]         ),
   .OCE             (1'b1           ), // 1-bit input: Output data clock enable
   .RST             (ini_rst        ), // 1-bit input: Reset,高电平有效的同步时钟
   
   // SHIFTIN1 / SHIFTIN2: 1-bit (each) input: Data input expansion (1-bit each)
   .SHIFTIN1        (cascade_di1    ), // 连接到SLAVE上
   .SHIFTIN2        (cascade_di2    ), // 连接到SLAVE上
   
   // T1 - T4: 1-bit (each) input: Parallel 3-state inputs
   .T1              (1'b0           ),
   .T2              (1'b0           ),
   .T3              (1'b0           ),
   .T4              (1'b0           ),
   .TBYTEIN         (1'b0           ), // 1-bit input: Byte group tristate
   .TCE             (1'b0           )  // 1-bit input: 3-state clock enable
);
//==========================================================================
//==    OSERDESE2 slave 原语例化:8-9
//==========================================================================
OSERDESE2
#(
   .DATA_RATE_OQ    ("DDR"          ), // DDR, SDR
   .DATA_RATE_TQ    ("SDR"          ), // DDR, BUF, SDR
   .DATA_WIDTH      (10             ), // Parallel data width (2-8,10,14)
   .INIT_OQ         (1'b0           ), // Initial value of OQ output (1'b0,1'b1)
   .INIT_TQ         (1'b0           ), // Initial value of TQ output (1'b0,1'b1)
   .SERDES_MODE     ("SLAVE"        ), // MASTER, SLAVE
   .SRVAL_OQ        (1'b0           ), // OQ output value when SR is used (1'b0,1'b1)
   .SRVAL_TQ        (1'b0           ), // TQ output value when SR is used (1'b0,1'b1)
   .TBYTE_CTL       ("FALSE"        ), // Enable tristate byte operation (FALSE, TRUE)
   .TBYTE_SRC       ("FALSE"        ), // Tristate byte source (FALSE, TRUE)
   .TRISTATE_WIDTH  (1              )  // 3-state converter width (1,4)
)
OSERDESE2_inst_slave
(
   .OFB             (               ), // 1-bit output: Feedback path for data
   .OQ              (               ), // 1-bit output: Data path output,主机输出,从机关闭
   
   // SHIFTOUT1 / SHIFTOUT2: 1-bit (each) output: Data output expansion (1-bit each)
   .SHIFTOUT1       (cascade_di1    ),
   .SHIFTOUT2       (cascade_di2    ),
   .TBYTEOUT        (               ), // 1-bit output: Byte group tristate
   .TFB             (               ), // 1-bit output: 3-state control
   .TQ              (               ), // 1-bit output: 3-state control
   .CLK             (serclk         ), // 1-bit input: High speed clock
   .CLKDIV          (divclk         ), // 1-bit input: Divided clock
   
   // D1 - D8: 1-bit (each) input: Parallel data inputs (1-bit each)
   .D1              (               ), //为什么要空?看框图
   .D2              (               ), //为什么要空?看框图
   .D3              (din[8]         ),
   .D4              (din[9]         ),
   .D5              (1'b0           ),
   .D6              (1'b0           ),
   .D7              (1'b0           ),
   .D8              (1'b0           ),
   .OCE             (1'b1           ), // 1-bit input: Output data clock enable
   .RST             (ini_rst        ), // 1-bit input: Reset
   
   // SHIFTIN1 / SHIFTIN2: 1-bit (each) input: Data input expansion (1-bit each)
   .SHIFTIN1        (               ),
   .SHIFTIN2        (               ),
   
   // T1 - T4: 1-bit (each) input: Parallel 3-state inputs
   .T1              (1'b0           ),
   .T2              (1'b0           ),
   .T3              (1'b0           ),
   .T4              (1'b0           ),
   .TBYTEIN         (1'b0           ), // 1-bit input: Byte group tristate
   .TCE             (1'b0           )  // 1-bit input: 3-state clock enable
);

3、OBUFDS:差分输出

  OBUFDS原语用于将三路数据和一路时钟信号转换成差分信号输出,OBUFDS 是差分输出缓冲器,用于将来自 FPGA 内部逻辑的信号转换成差分信号输出,支持 TMDS 电平标准。OBUFDS 原语示意图如下所示:

  OBUFDS获取途径如下所示:

  为了程序简单化,可以将此模块也放入上面 Serializer10_1 模块的 OSERDESE2 调用后面,如下所示:

复制代码
//==========================================================================
//==    OBUFDS 原语例化,将串行数据转为一对差分信号
//==========================================================================
OBUFDS
#(
   .IOSTANDARD      ("DEFAULT"      ), // Specify the output I/O standard
   .SLEW            ("SLOW"         )  // Specify the output slew rate
)
OBUFDS_inst
(
   .O               (do_p           ), // Diff_p output (connect directly to top-level port)
   .OB              (do_n           ), // Diff_n output (connect directly to top-level port)
   .I               (do             )  // Buffer input
);

4、HDMI_trans:模块连接

  创建一个 HDMI_trans 模块,将上述三个模块连接到一起,有几点需要说明:

(1)u_encode_blue 的例化中的 c0 和 c1 包括了 HSYNC 和 VSYNC,而 red 和 blue 中给的是 0,原因见上文。

(2)Serializer10_1 里包含了 OSERDESE2 原语和 OBUFDS 原语。

(3)上面说过,整个系统需要两个输入时钟,一个是视频的像素时钟 HDMI_clk1x(div_clk) ,另外一个时钟 HDMI_clk5x(ser_clk)的频率是像素时钟的五倍。TMDS 连接的时钟通道我们采用与数据通道相同的并转串逻辑来实现。通过对 10 位二进制序列 10'b11111_00000 在 10 倍像素时钟频率下进行并串转换,就可以得到像素时钟频率下的 TMDS 参考时钟 ser_clk。

复制代码
  1 //**************************************************************************
  2 // *** 名称 : HDMI_trans.v
  3 // *** 作者 : xianyu_FPGA
  4 // *** 博客 : https://www.cnblogs.com/xianyufpga/
  5 // *** 日期 : 2019-08-10
  6 // *** 描述 : VGA协议转成HDMI协议输出
  7 //**************************************************************************
  8 module HDMI_trans
  9 //========================< 端口 >==========================================
 10 (
 11 input   wire                HDMI_clk1x              ,
 12 input   wire                HDMI_clk5x              ,
 13 input   wire                HDMI_rst                ,
 14 //VGA -----------------------------------------------
 15 input   wire                VGA_de                  ,
 16 input   wire                VGA_vsync               ,
 17 input   wire                VGA_hsync               ,
 18 input   wire  [7:0]         VGA_r                   ,
 19 input   wire  [7:0]         VGA_g                   ,
 20 input   wire  [7:0]         VGA_b                   ,
 21 //HDMI ----------------------------------------------
 22 output  wire                HDMI_clk_p              ,
 23 output  wire                HDMI_clk_n              ,
 24 output  wire                HDMI_chn0_p             ,
 25 output  wire                HDMI_chn0_n             ,
 26 output  wire                HDMI_chn1_p             ,
 27 output  wire                HDMI_chn1_n             ,
 28 output  wire                HDMI_chn2_p             ,
 29 output  wire                HDMI_chn2_n
 30 );
 31 //========================< 信号 >==========================================
 32 wire    [9:0]               dout_r                  ; //红
 33 wire    [9:0]               dout_g                  ; //绿
 34 wire    [9:0]               dout_b                  ; //蓝
 35 //==========================================================================
 36 //==    encode 例化,RGB三个通道进行8B转10B转换
 37 //==========================================================================
 38 encode u_encode_red
 39 (
 40     .clkin              (HDMI_clk1x                 ),
 41     .rstin              (HDMI_rst                   ),
 42     .din                (VGA_r                      ),
 43     .c0                 (1'b0                       ),
 44     .c1                 (1'b0                       ),
 45     .de                 (VGA_de                     ),
 46     .dout               (dout_r                     )
 47 );
 48 
 49 encode u_encode_green
 50 (
 51     .clkin              (HDMI_clk1x                 ),
 52     .rstin              (HDMI_rst                   ),
 53     .din                (VGA_g                      ),
 54     .c0                 (1'b0                       ),
 55     .c1                 (1'b0                       ),
 56     .de                 (VGA_de                     ),
 57     .dout               (dout_g                     )
 58 );
 59 
 60 encode u_encode_blue
 61 (
 62     .clkin              (HDMI_clk1x                 ),
 63     .rstin              (HDMI_rst                   ),
 64     .din                (VGA_b                      ),
 65     .c0                 (VGA_hsync                  ),
 66     .c1                 (VGA_vsync                  ),
 67     .de                 (VGA_de                     ),
 68     .dout               (dout_b                     )
 69 );
 70 //==========================================================================
 71 //==    并转串模块例化:时钟,给固定值
 72 //==========================================================================
 73 Serializer10_1 u_Serializer_clk
 74 (
 75     .divclk             (HDMI_clk1x                 ),
 76     .serclk             (HDMI_clk5x                 ),
 77     .rst                (HDMI_rst                   ),
 78     .din                (10'b11111_00000            ), //固定值
 79     .do_p               (HDMI_clk_p                 ),
 80     .do_n               (HDMI_clk_n                 )
 81 );
 82 //==========================================================================
 83 //==    并转串模块例化:数据
 84 //==========================================================================
 85 Serializer10_1 u_Serializer_red
 86 (
 87     .divclk             (HDMI_clk1x                 ),
 88     .serclk             (HDMI_clk5x                 ),
 89     .rst                (HDMI_rst                   ),
 90     .din                (dout_r                     ),
 91     .do_p               (HDMI_chn2_p                ),
 92     .do_n               (HDMI_chn2_n                )
 93 );
 94 
 95 Serializer10_1 u_Serializer_green
 96 (
 97     .divclk             (HDMI_clk1x                 ),
 98     .serclk             (HDMI_clk5x                 ),
 99     .rst                (HDMI_rst                   ),
100     .din                (dout_g                     ),
101     .do_p               (HDMI_chn1_p                ),
102     .do_n               (HDMI_chn1_n                )
103 );
104 
105 Serializer10_1 u_Serializer_blue
106 (
107     .divclk             (HDMI_clk1x                 ),
108     .serclk             (HDMI_clk5x                 ),
109     .rst                (HDMI_rst                   ),
110     .din                (dout_b                     ),
111     .do_p               (HDMI_chn0_p                ),
112     .do_n               (HDMI_chn0_n                )
113 );
114 
115 
116 endmodule 

5、HDMI_top:模块完整封装

  可以看出 HDMI 模块还是比较复杂的,因此我们添加 VGA 时序模块,和 HDMI_trans 模块一起封装成 HDMI_top 模块,以后可以直接把这个模块拿来用。HDMI_top 的框图如下所示:

  HDMI_top 模块代码如下所示:

复制代码
 1 `timescale 1ns / 1ps
 2 //**************************************************************************
 3 // *** 名称 : HDMI_top.v
 4 // *** 作者 : xianyu_FPGA
 5 // *** 博客 : https://www.cnblogs.com/xianyufpga/
 6 // *** 日期 : 2019-08-10
 7 // *** 描述 : HDMI顶层文件
 8 //**************************************************************************
 9 module HDMI_top
10 //========================< 端口 >==========================================
11 (
12 //system --------------------------------------------
13 input   wire                HDMI_clk1x              ,
14 input   wire                HDMI_clk5x              ,
15 input   wire                rst                     ,
16 //HDMI in -------------------------------------------
17 output  wire                HDMI_req                ,
18 input   wire    [15:0]      HDMI_data               ,
19 //HDMI out ------------------------------------------
20 output  wire                HDMI_clk_p              ,
21 output  wire                HDMI_clk_n              ,
22 output  wire                HDMI_d0_p               ,
23 output  wire                HDMI_d0_n               ,
24 output  wire                HDMI_d1_p               ,
25 output  wire                HDMI_d1_n               ,
26 output  wire                HDMI_d2_p               ,
27 output  wire                HDMI_d2_n   
28 );
29 //========================< 连线 >==========================================
30 wire                        VGA_req                 ;
31 wire    [15:0]              VGA_data                ;
32 wire                        VGA_de                  ;
33 wire                        VGA_vsync               ;
34 wire                        VGA_hsync               ;
35 wire    [ 7:0]              VGA_r                   ;
36 wire    [ 7:0]              VGA_g                   ;
37 wire    [ 7:0]              VGA_b                   ;
38 //==========================================================================
39 //==                        VGA控制器
40 //==========================================================================
41 VGA_driver u_VGA_driver
42 (
43     .clk                    (HDMI_clk1x             ),
44     .rst                    (rst                    ),
45     .VGA_data               (HDMI_data              ),
46     .VGA_req                (HDMI_req               ),
47     .VGA_de                 (VGA_de                 ),
48     .VGA_vsync              (VGA_vsync              ),
49     .VGA_hsync              (VGA_hsync              ),
50     .VGA_r                  (VGA_r                  ),
51     .VGA_g                  (VGA_g                  ),
52     .VGA_b                  (VGA_b                  )
53 );
54 //==========================================================================
55 //==                        HDMI转换
56 //==========================================================================
57 HDMI_trans u_HDMI_trans
58 (
59     .HDMI_clk1x             (HDMI_clk1x             ),
60     .HDMI_clk5x             (HDMI_clk5x             ),
61     .HDMI_rst               (rst                    ),
62     //-----------------------------------------------
63     .VGA_de                 (VGA_de                 ),
64     .VGA_vsync              (VGA_vsync              ),
65     .VGA_hsync              (VGA_hsync              ),
66     .VGA_r                  (VGA_r                  ),
67     .VGA_g                  (VGA_g                  ),
68     .VGA_b                  (VGA_b                  ),
69     //-----------------------------------------------
70     .HDMI_clk_p             (HDMI_clk_p             ),
71     .HDMI_clk_n             (HDMI_clk_n             ),
72     .HDMI_chn0_p            (HDMI_d0_p              ),
73     .HDMI_chn0_n            (HDMI_d0_n              ),
74     .HDMI_chn1_p            (HDMI_d1_p              ),
75     .HDMI_chn1_n            (HDMI_d1_n              ),
76     .HDMI_chn2_p            (HDMI_d2_p              ),
77     .HDMI_chn2_n            (HDMI_d2_n              )
78 );
79 
80 
81 endmodule

五、上板现象

  将 HDMI_top 模块和 DDR3 控制模块一起建立工程,编译下载,那么 HDMI 接口的屏幕会显示乱序的彩条,表明此次实验设计成功。

AXI_Lite 总线详解

目录:

  · 1.前言

  · 2.AXI总线与ZYNQ的关系

  · 3 AXI 总线和 AXI 接口以及 AXI 协议

      · 3.1 AXI 总线概述

      · 3.2 AXI 接口介绍

      ·3.3 AXI 协议概述

      · 3.4 AXI 协议之握手协议

      · 3.5 突发式读写

   · 4 AXI4-Lite 详解

      ·4.1 AXI4-Lite 源码查看

      · 4.2 AXI-Lite 源码分析

   · 5 观察 AXI4-Lite 总线信号

   · 6 加载到 SDK

   · 7 本章小结

1 前言

  ZYNQ拥有ARM+FPGA这个神奇的架构,那么ARM和FPGA究竟是如何进行通信的呢?本章通过剖析AXI总线源码,来一探其中的秘密。

2 AXI 总线与 ZYNQ 的关系

  AXI(Advanced eXtensible Interface)本是由ARM公司提出的一种总线协议,Xilinx 从 6 系列的 FPGA 开始对 AXI 总线提供支持,此时 AXI 已经发展到了 AXI4 这个版本,所以当你用到 Xilinx 的软件的时候看到的都是"AIX4"的 IP,如 Vivado 打包一个 AXI IP的时候,看到的都是 Create a new AXI4 peripheral。到了 ZYNQ 就更不必说了,AXI 总线更是应用广泛,双击查看 ZYNQ 的 IP 核的内部配置,随处可见 AXI 的身影。

3 AXI 总线和 AXI 接口以及 AXI 协议

  总线、接口和协议,这三个词常常被联系在一起,但是我们心里要明白他们的区别。总线是一组传输通道,是各种逻辑器件构成的传输数据的通道,一般由由数据线、地址线、控制线等构成。接口是一种连接标准,又常常被称之为物理接口。协议就是传输数据的规则。

3.1 AXI 总线概述

  在ZYNQ中有支持三种AXI总线,拥有三种AXI接口,当然用的都是AXI协议。其中三种AXI总线分别为:

    AXI4:(For high-performance memory-mapped requirements.)主要面向高性能地址映射通信的需求,是面向地址映射的接口,允许最大256轮的数据突发传输;

    AXI4-Lite:(For simple, low-throughput memory-mapped communication )是一个轻量级的地址映射单次传输接口,占用很少的逻辑单元。

    AXI4-Stream:(For high-speed streaming data.)面向高速流数据传输;去掉了地址项,允许无限制的数据突发传输规模。

  首先说AXI4总线和AXI4-Lite总线具有相同的组成部分:

  (1)读地址通道,包含ARVALID, ARADDR, ARREADY信号;

  (2)读数据通道,包含RVALID, RDATA, RREADY, RRESP信号;

  (3)写地址通道,包含AWVALID,AWADDR, AWREADY信号;

  (4)写数据通道,包含WVALID, WDATA,WSTRB, WREADY信号;

  (5)写应答通道,包含BVALID, BRESP, BREADY信号;

  (6)系统通道,包含:ACLK,ARESETN信号。

  AXI4总线和AXI4-Lite总线的信号也有他的命名特点:

    读地址信号都是以AR开头(A:address;R:read)

    写地址信号都是以AW开头(A:address;W:write)

    读数据信号都是以R开头(R:read)

    写数据信号都是以W开头(W:write)

    应答型号都是以B开头(B:back(answer back))

  了解到总线的组成部分以及命名特点,那么在后续的实验中您将逐渐看到他们的身影。每个信号的作用暂停不表,放在后面一一介绍。

  而AXI4-Stream总线的组成有:

    (1)ACLK信号:总线时钟,上升沿有效;

    (2)ARESETN信号:总线复位,低电平有效

    (3)TREADY信号:从机告诉主机做好传输准备;

    (4)TDATA信号:数据,可选宽度32,64,128,256bit

    (5)TSTRB信号:每一bit对应TDATA的一个有效字节,宽度为TDATA/8

    (6)TLAST信号:主机告诉从机该次传输为突发传输的结尾;

    (7)TVALID信号:主机告诉从机数据本次传输有效;

    (8)TUSER信号 :用户定义信号,宽度为128bit。

  对于AXI4-Stream总线命名而言,除了总线时钟和总线复位,其他的信号线都是以T字母开头,后面跟上一个有意义的单词,看清这一点后,能帮助读者记忆每个信号线的意义。如TVALID = T+单词Valid(有效),那么读者就应该立刻反应该信号的作用。每个信号的具体作用,在后面分析源码时再做分析。

3.2 AXI 接口介绍

  三种AXI接口分别是:

  AXI-GP接口(4个):是通用的AXI接口,包括两个32位主设备接口和两个32位从设备接口,用过改接口可以访问PS中的片内外设。

  AXI-HP接口(4个):是高性能/带宽的标准的接口,PL模块作为主设备连接(从下图中箭头可以看出)。主要用于PL访问PS上的存储器(DDR和On-Chip RAM)

  AXI-ACP接口(1个):是ARM多核架构下定义的一种接口,中文翻译为加速器一致性端口,用来管理DMA之类的不带缓存的AXI外设,PS端是Slave接口。我们可以双击查看ZYNQ的IP核的内部配置,就能发现上述的三种接口,图中已用红色方框标记出来,我们可以清楚的看出接口连接与总线的走向:

3.3 AXI 协议概述

  讲到协议不可能说是撇开总线单讲协议,因为协议的制定也是要建立在总线构成之上的。虽然说AXI4,AXI4-Lite,AXI4-Stream都是AXI4协议,但是各自细节上还是不同的。

  总的来说,AXI总线协议的两端可以分为分为主(master)、从(slave)两端,他们之间一般需要通过一个AXI Interconnect相连接,作用是提供将一个或多个AXI主设备连接到一个或多个AXI从设备的一种交换机制。当我们添加了zynq以及带AXI的IP后再进行自动连线时vivado会自动帮我们添加上这个IP,大家应该是不陌生了。AXI Interconnect的主要作用是,当存在多个主机以及从机器时,AXIInterconnect负责将它们联系并管理起来。由于AXI支持乱序发送,乱序发送需要主机的ID信号支撑,而不同的主机发送的ID可能相同,而AXI Interconnect解决了这一问题,他会对不同主机的ID信号进行处理让ID变得唯一。

  AXI协议将读地址通道,读数据通道,写地址通道,写数据通道,写响应通道分开,各自通道都有自己的握手协议。每个通道互不干扰却又彼此依赖。这也是AXI高效的原因之一。

3.4 AXI 协议之握手协议

  AXI4 所采用的是一种 READY,VALID 握手通信机制,简单来说主从双方进行数据通信前,有一个握手的过程。传输源产生 VLAID 信号来指明何时数据或控制信息有效。而目地源产生 READY 信号来指明已经准备好接受数据或控制信息。传输发生在 VALID和 READY 信号同时为高的时候。VALID 和 READY 信号的出现有三种关系。

  **(1) VALID 先变高 READY 后变高。**时序图如下:

在箭头处信息传输发生。

  **(2) READY 先变高 VALID 后变高。**时序图如下:

同样在箭头处信息传输发生。

  **(3) VALID 和 READY 信号同时变高。**时序图如下:

  在这种情况下,信息传输立马发生,如图箭头处指明信息传输发生。需要强调的是,AXI的五个通道,每个通道都有握手机制,接下来我们就来分析一下AXI-Lite的源码来更深入的了解AXI机制。

3.5 突发式读写

1、突发式读的时序图如下:

当地址出现在地址总线后,传输的数据将出现在读数据通道上。设备保持 VALID 为低直到读数据有效。为了表明一次突发式读写的完成,设备用 RLAST 信号来表示最后一个被传输的数据。

2、 突发式写时序图如下:

这一过程的开始时,主机发送地址和控制信息到写地址通道中,然后主机发送每一个写数据到写数据通道中。当主机发送最后一个数据时,WLAST 信号就变为高。当设备接收完所有数据之后他将一个写响应发送回主机来表明写事务完成。

4 AXI4-Lite 详解

4.1 AXI4-Lite 源码查看

  Step1:要看到AXI-Lite的源码,我们先要自定义一个AXI-Lite的IP,新建工程之后,选择,菜单栏->Tools->Creat and Package IP:

Step2:选择Next

Step3:选择Create AXI4 Peripheral,然后Next:

Step4:给模块命名,保存,然后Next

Step5:注意这里接口类型选择Lite,选择Next:

Step6:选择Edit IP,点击Finish:

Step7:此后,Vivado会新建一个工程,专门编辑该IP,通过该工程,我们就可以看到Vivado为我们生成的AXI-Lite的操作源码:

4.2 AXI-Lite 源码分析

  当打开顶层文件的时,映入眼帘的是一堆AXI的信号,这些信号是否似曾相识?

复制代码
input wire  s00_axi_aclk,
input wire  s00_axi_aresetn,
input wire [C_S00_AXI_ADDR_WIDTH-1 : 0] s00_axi_awaddr,
input wire [2 : 0] s00_axi_awprot,
input wire  s00_axi_awvalid,
output wire  s00_axi_awready,
input wire [C_S00_AXI_DATA_WIDTH-1 : 0] s00_axi_wdata,
input wire [(C_S00_AXI_DATA_WIDTH/8)-1 : 0] s00_axi_wstrb,
input wire  s00_axi_wvalid,
output wire  s00_axi_wready,
output wire [1 : 0] s00_axi_bresp,
output wire  s00_axi_bvalid,
input wire  s00_axi_bready,
input wire [C_S00_AXI_ADDR_WIDTH-1 : 0] s00_axi_araddr,
input wire [2 : 0] s00_axi_arprot,
input wire  s00_axi_arvalid,
output wire  s00_axi_arready,
output wire [C_S00_AXI_DATA_WIDTH-1 : 0] s00_axi_rdata,
output wire [1 : 0] s00_axi_rresp,
output wire  s00_axi_rvalid,
input wire  s00_axi_rready

没错笔者曾在《AXI总线概述》这节中提到了他们,这次通过源码分析再次隆重介绍它们。

  Vivado为我们生成的AXI-Lite的操作源码,是一个例子,我只需要读懂他,然后稍加修改,就可以为我们所用。我们先来看一段WDATA相关的代码:

复制代码
always @( posedge S_AXI_ACLK )
begin
  if ( S_AXI_ARESETN == 1'b0 )
    begin
      slv_reg0 <= 0;
      slv_reg1 <= 0;
      slv_reg2 <= 0;
      slv_reg3 <= 0;
    end
  else begin
    if (slv_reg_wren)
      begin
        case ( axi_awaddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
          2'h0:
            for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
              if ( S_AXI_WSTRB[byte_index] == 1 ) begin
                // Respective byte enables are asserted as per write strobes
                // Slave register 0
                slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
              end  
          2'h1:
            for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
              if ( S_AXI_WSTRB[byte_index] == 1 ) begin
                // Respective byte enables are asserted as per write strobes
                // Slave register 1
                slv_reg1[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
              end  
          2'h2:
            for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
              if ( S_AXI_WSTRB[byte_index] == 1 ) begin
                // Respective byte enables are asserted as per write strobes
                // Slave register 2
                slv_reg2[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
              end  
          2'h3:
            for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
              if ( S_AXI_WSTRB[byte_index] == 1 ) begin
                // Respective byte enables are asserted as per write strobes
                // Slave register 3
                slv_reg3[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
              end  
          default : begin
                      slv_reg0 <= slv_reg0;
                      slv_reg1 <= slv_reg1;
                      slv_reg2 <= slv_reg2;
                      slv_reg3 <= slv_reg3;
                    end
        endcase
      end
  end
end   

  这段程序的作用是,当PS那边向AXI4-Lite总线写数据时,PS这边负责将数据接收到寄存器slv_reg。而slv_reg寄存器有0~3共4个。至于赋值给哪一个由axi_awaddrADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB决定,根据宏定义其实就是由axi_awaddr3:2 (写地址中不仅包含地址,而且包含控制位,这里的3:2就是控制位)决定赋值给哪个slv_reg。

  PS调用写函数时,如果不做地址偏移的话,axi_awaddr3:2的值默认是为0的,举个例子,如果我们自定义的IP的地址被映射为0x43C00000,那么我们Xil_Out32(0x43C00000,Value)写的就是slv_reg0的值。如果地址偏移4位,如Xil_Out32(0x43C00000 + 4,Value) 写的就是slv_reg1的值,依次类推。

  分析时只关注slv_reg0(其他结构上也是一模一样的):

复制代码
for ( byte_index = 0; byte_index <= (C_S_AXI_DATA_WIDTH/8)-1; byte_index = byte_index+1 )
     if ( S_AXI_WSTRB[byte_index] == 1 ) begin
              slv_reg0[(byte_index*8) +: 8] <= S_AXI_WDATA[(byte_index*8) +: 8];
     end

  其中,C_S_AXI_DATA_WIDTH的宏定义的值为32,也就是数据位宽,S_AXI_WSTRB就是写选通信号,S_AXI_WDATA就是写数据信号。

  存在于for循环中的最关键的一句:

    slv_reg0(byte_index\*8) +: 8 <= S_AXI_WDATA(byte_index\*8) +: 8;

  当byte_index = 0的时候这句话就等价于:

    slv_reg07:0 <= S_AXI_WDATA7:0;

  当byte_index = 1的时候这句话就等价于:

    slv_reg015:8 <= S_AXI_WDATA15:8;

  当byte_index = 2的时候这句话就等价于:

    slv_reg023:16 <= S_AXI_WDATA23:16;

  当byte_index = 3的时候这句话就等价于:

    slv_reg031:24 <= S_AXI_WDATA31:24;

  也就是说,只有当写选通信号为1时,它所对应S_AXI_WDATA的字节才会被读取。

  读懂了这段话之后,我们就知道了,如果我们想得到PS写到总线上的数据,我们只需要读取slv_reg0的值即可。

  那如果,我们想写数据到总线让PS读取该数据,我们该怎么做呢?我们继续来看有关RADTA读数据代码:

复制代码
// Output register or memory read data
always @( posedge S_AXI_ACLK )
begin
  if ( S_AXI_ARESETN == 1'b0 )
    begin
      axi_rdata  <= 0;
    end
  else
    begin    
      // When there is a valid read address (S_AXI_ARVALID) with
      // acceptance of read address by the slave (axi_arready),
      // output the read dada
      if (slv_reg_rden)
        begin
          axi_rdata <= reg_data_out;     // register read data
        end   
    end
end

  观察可知,当PS读取数据时,程序会把reg_data_out复制给axi_rdata(RADTA读数据)。我们继续追踪reg_data_out:

复制代码
always @(*)
begin
      // Address decoding for reading registers
      case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
        2'h0   : reg_data_out <= slv_reg0;
        2'h1   : reg_data_out <= slv_reg1;
        2'h2   : reg_data_out <= slv_reg2;
        2'h3   : reg_data_out <= slv_reg3;
        default : reg_data_out <= 0;
      endcase
end

  和前面分析的一样此时通过判断axi_awaddr3:2的值来判断将那个值给reg_data_out上,同样当PS调用读取函数时,这里axi_awaddr3:2默认是0,所以我们只需要把slv_reg0替换成我们自己数据,就可以让PS通过总线读到我们提供的数据。

  这里可能有的读者会问了,slv_reg0不是总线写过来的数据吗?因为笔者说过这个程序是Vivado为我们提供的例子,它这么做无非是想验证我写出去的值和我读进入的值相等。但是他怎么写确实会对初看代码的人造成困扰。

  最后笔者提出一个问题,为什么写通道要比读通道多了一列应答通道,这是为什么呢?

  首先,你要知道这个应答信号是干什么用的?

  写应答,主要是回复主机你这个写过程是没有问题的,那读为什么不需要这个过程呢?

  这时因为主机在读取数据时,从机可以直接通过读数据通道给主机反馈信息,因此就没有必要再来开辟一个单独的应答通道了。

小结:

  如果我们想读AXI4_Lite总线上的数据时,只需关注slv_reg的数据,我们可自行添加一段代码,如:

复制代码
reg [11:0]rlcd_rgb;
always @( posedge S_AXI_ACLK )
begin
    if ( S_AXI_ARESETN == 1'b0 )
        begin
            rlcd_rgb  <= 12'd0;
        end
    else
    begin
        rlcd_rgb <= slv_reg0[11:0];
    end
end  
assign lcd_rgb = rlcd_rgb;

  如果我们想对AXI4_Lite信号写数据时,我们只需修改对reg_data_out的赋值,如:

复制代码
//写总线测试修改!!!!!!!!!
wire[31:0]wlcd_xy;// = {10'd0,lcd_xy};
assign wlcd_xy = {10'd0,lcd_xy};
assign slv_reg_rden = axi_arready & S_AXI_ARVALID & ~axi_rvalid;
always @(*)
begin
    // Address decoding for reading registers
    case ( axi_araddr[ADDR_LSB+OPT_MEM_ADDR_BITS:ADDR_LSB] )
      2'h0   : reg_data_out <= wlcd_xy;//slv_reg0;   
      2'h1   : reg_data_out <= slv_reg1;
      2'h2   : reg_data_out <= slv_reg2;
      2'h3   : reg_data_out <= slv_reg3;
      default : reg_data_out <= 0;
    endcase
end

  最后强调下如果我们自定义的IP的地址被映射为0x43C00000,那么我们Xil_Out32(0x43C00000,Value)写的就是slv_reg0的值。如果地址偏移4位,如Xil_Out32(0x43C00000 + 4,Value) 写的就是slv_reg1的值,依次类推。

  目前这里只有4个寄存器,那是因为之前选择的是4个,其实我们可以定义的更多:

  在ps的头文件里可以看到我们自定义的IP的地址是有个范围的

复制代码
#define XPAR_ MYIPFREQUENCY_ 0_ S00_ AXI_ BASEADDR 0x43C00000
#define XPAR_ MYIPFREQUENCY_ 0_ S00_ AXI_ HIGHADDR 0x43C0FFFF

  理论上只要基地址 + 偏移量不要超过HIGHADDR即可。

5 观察 AXI4-Lite 总线信号

  在第十章,我们封装了一个 AXI_Lite 的 GPIO,通过本章的分析,我们在第十章工程的基础上通过添加一个 ila 核的方式,来具体看看 AXI_Lite 总线的信号。

  Step1:做好第十章工程的备份,然后直接打开第十章的工程。

  Step2:单击 IP icon

添加 ila CORE

  

  Step3:双击打开 ILA CORE

  

  Step4:双击打开 ILA CORE

    General Options 设置如下

  

  Probe_Ports 设置如下,之后单击 OK

  

  Step5:连接 Probe0 到 GPIO_LED。

  Step6:连接 CLK 接口到 FCLK_CLK0 接口

  Step7:选中 Processing_System7_0_axi_periph 和 GPIO_LITE_ML_0 之间的 S_AXI 总线。

  Step8:右击选择 Mark Debug

  

  Step9:接下来依然是,右键单击Block文件,文件选择Generate the Output Products。

  Step10:继续右键单击Block文件,选择Create a HDL wrapper,根据Block文件内容产生一个HDL 的顶层文件,并选择让vivado自动完成。

  Setp11:单击Run Synthesis,如果有 Save 对话框弹出选择保存。

  Setp12:综合结束后选择Synthesized Design option单击 OK。

  Step13:在如下对话框中找到Unassigned debug nets(如果对话框没有出现选择 菜单->Window > Debug)

  

  Step14:右击 Unassigned Debug Nets 选择Set up Debug... 之后单击 Next

  Step15:删除红色错误的信号然后单击Next 到结束

  

  Step16:生成 Bit 文件。

6 加载到 SDK

  Step1:导出硬件。

  Step2:右击工程,选择 Debug as ->Debug configuration。

  Step3:选中 system Debugger,双击创建一个系统调试。

  

  Step4:设置系统调试。

  

  Step5:回到 VIVADO 单击 Open Target->Auto Connect

  

  Step6:加载完成后的界面

  

  Step7:选择菜单->window->Debugprobes 选择 AXI_WVALID 和 AXI_AWVALID 做为触发信号

  

  Step8:设置触发条件为 1

  

  Step9:设置触发位置为 512

  

  Step10:单击箭头所指向启动触发

  

  Step11:进入等待触发状态

  

  Step12:单击运行

后 VIVADO HW_ILA2 窗口采集到波形输出,可以看到 AXI总线的工作时序。

  

  Step13:HW_ILA1 窗口采集到的数据是 GPIO_LED 的值为 0x02,同时可观察到开发板上的 LED2 亮起。

7 本章小结

  通过本章的学习,我们首先得认识到总线和接口以及协议的区别,其次通过分析AXI4-Lite,AXI4-Stream,AXI4总线的从机代码,对AXI协议有一定的认识,那么在后面学习AXI的一些IP时就不会有恐惧的心理。

  最后,我们再理一理AXI总线和AXI接口的关系。在ZYNQ中,支持AXI4-Lite,AXI4和AXI4-Stream三种总线协议,这前面已经说过了,要注意的是PS与PL之间的接口(AXI-GP接口,AXI-HP接口以及AXI-ACP接口)却只支持AXI-Lite和AXI协议这两种总线协议。也就是说PL这边的AXI-Stream的接口是不能直接与PS对接的,需要经过AXI4或者AXI4-Lite的转换。比如后面将用到的VDMA IP ,它就实现了在PL内部AXI4到AXI-Stream的转换,VDMA利用的接口就是AXI-HP接口。

协议------SPI

  SPI(Serial Peripheral interface)是由摩托罗拉公司定义的一种串行外围设备接口,是一种高速、全双工、同步的通信总线,只需要四根信号线即可,节约引脚,同时有利于PCB的布局。正是出于这种简单易用的特性,现在越来越多的芯片集成了SPI通信协议,如FLASH、SD卡,AD转换器等。SPI的通信原理比较简单,它以主从方式工作,通常有一个主设备和一个或多个从设备。一般其有 4 根线,片选线(CS)、同步时钟线(SCLK)、输入数据线(MOSI)和输出数据线(MISO)。

一、通信模式

  一般而言,SPI通信有4种不同的模式,不同的从设备在出厂时被厂家配置为其中一种模式,模式是不允许用户修改的。主设备和从设备必须在同一模式下进行通信,否则数据会接收错误。SPI的通信模式是由CPOL(时钟极性)和CPHA(时钟相位)来决定的,四种通信模式如下:

复制代码
模式0:CPOL = 0, CPHA = 0;
模式1:CPOL = 0, CPHA = 1;
模式2:CPOL = 1, CPHA = 0;
模式3:CPOL = 1, CPHA = 1;

1.CPOL时钟极性

  CPOL信号控制着SPI_CLK的时钟极性,时钟极性变化规律如下:

复制代码
CPOL = 1:SPI_CLK空闲时为高,第1个时钟沿为下降沿
CPOL = 0:SPI_CLK空闲时为低,第1个时钟沿为上升沿

2.CPHA时钟相位

  CPHA信号控制数据与时钟的对齐模式,其不同模式下的时序图如下所示:

  由上图可知,当CPHA = 1时,时钟的第一个变化沿(上升沿或者下降沿)数据开始改变,那么也就意味着时钟的第2个变化沿(与第一个变化沿相反)锁存数据;当CPHA = 0时,数据在时钟的第一个变化沿之前就已经改变,并且保持稳定,也就意味着在时钟的第一个变化沿锁存数据。

二、总结

协议------SCCB与IIC的区别

  SCCB(Serial Camera Control Bus,串行摄像头控制总线)是由OV(OmniVision的简称)公司定义和发展的三线式串行总线,该总线控制着摄像头大部分的功能,包括图像数据格式、分辨率以及图像处理参数等。结构框图如下所示:

  OV公司为了减少传感器引脚的封装,现在SCCB总线大多采用两线式接口总线。OV7725使用的是两线式接口总线,该接口总线包括SIO_C串行时钟输入线和SIO_D串行双向数据线,分别相当于IIC协议的SCL信号线和SDA信号线。SIO_C的最小时间为10us,即最大频率为100K。一般来说,100K-400K之间都可以。

  由此可见,SCCB就是改编版的IIC,完全可以按照IIC来理解,下面仔细讲解SCCB的时序以及和IIC的不同之处。

一、SCCB起始和结束(与IIC完全一致)

**  起始:SIO_C为高时,SIO_D由高拉低。**

**  停止:SIO_C为高时,SIO_D由低拉高**。

二、SCCB写(与IIC完全一致)

  ID Address(W)里面就已经包括进了IIC中的"读写控制位",所以没有额外写出。

  即:start + phase_1 + phase_2 + phase_3 + stop

  "X"的意思是"don't care",该位是由从机发出应答信号来响应主机表示当前ID Address、Sub-address和Write Data是否传输完成,但是从机有可能不发出应答信号,因此主机(此处指FPGA)可不用判断此处是否有应答,直接默认当前传输完成即可。"X"即IIC中的ACK应答位。

三、SCCB读

  数据手册中的SCCB读只写了上图的Phase3和Phase4,实际上它是和Phase1和Phase2联系在一起的。SCCB不支持连续读,Phase4的主机应答位必须为NA(no ack),即为1,所以SCCB读其实就专指单次读,和IIC单次读几乎一样。

  区别就一点:在IIC读传输协议中,写完寄存器地址后会有restart即重复开始的操作;而SCCB读传输协议中没有重复开始的概念,在写完寄存器地址后,需发起总线停止信号。

  即:start_1 + phase_1 + phase_2 + stop_1 + start_2 + phase_3 + phase_4 + stop_2

四、SCCB和IIC的区别

  1.SCCB的应答位称为X,表示"don't care",而IIC应答位称为ACK。

  2.SCCB只能单次读,而IIC除了单次读还支持连续读。

  3.SCCB读操作中间有stop,而IIC读操作中间可以有stop也可以不需要stop,具体表现如下

复制代码
SCCB读:start_1 + phase_1 + phase_2 + stop_1 + start_2 + phase_3 + phase_4 + stop_2
 IIC读:start_1 + phase_1 + phase_2 +        + start_2 + phase_3 + phase_4 + stop_2

除去上面三点,SCCB和IIC再无区别,因此如果只需要配置寄存器(只用到写),可以直接拿IIC的时序来当做SCCB用,如果需要读,读操作中间必须有一个stop。

五、SCCB控制器Verilog代码

复制代码
  1 //**************************************************************************
  2 // *** 名称 : sccb.v
  3 // *** 作者 : xianyu_FPGA
  4 // *** 博客 : https://www.cnblogs.com/xianyufpga/
  5 // *** 日期 : 2019-08-10
  6 // *** 描述 : SCCB控制器,只支持写
  7 //**************************************************************************
  8 
  9 module sccb
 10 //========================< 参数 >==========================================
 11 #(
 12 parameter DEVICE_ID         = 8'b01010000           , //器件ID
 13 parameter CLK               = 26'd50_000_000        , //本模块的时钟频率
 14 parameter SCL               = 18'd250_000             //输出的SCL时钟频率
 15 )
 16 //========================< 端口 >==========================================
 17 (
 18 input                       clk                     , //时钟
 19 input                       rst_n                   , //复位,低电平有效
 20 //SCCB control -------------------------------------- 
 21 input                       sccb_en                 , //SCCB触发信号
 22 input                       addr16_en               , //16位地址使能
 23 input                       addr8_en                , //8位地址使能
 24 //SCCB input ---------------------------------------- 
 25 input        [15:0]         sccb_addr               , //SCCB器件内地址
 26 input        [ 7:0]         sccb_data               , //SCCB要写的数据
 27 //SCCB output --------------------------------------- 
 28 output  reg                 sccb_done               , //SCCB一次操作完成
 29 output  reg                 sccb_scl                , //SCCB的SCL时钟信号
 30 inout                       sccb_sda                , //SCCB的SDA数据信号
 31 //dri_clk ------------------------------------------- 
 32 output  reg                 sccb_dri_clk              //驱动SCCB操作的驱动时钟,1Mhz
 33 );
 34 //========================< 状态机参数 >====================================
 35 localparam  IDLE            = 6'b00_0001            ; //空闲状态
 36 localparam  DEVICE          = 6'b00_0010            ; //写器件地址
 37 localparam  ADDR_16         = 6'b00_0100            ; //写字地址高8位
 38 localparam  ADDR_8          = 6'b00_1000            ; //写字地址低8位
 39 localparam  DATA            = 6'b01_0000            ; //写数据
 40 localparam  STOP            = 6'b10_0000            ; //结束
 41 //========================< 信号 >==========================================
 42 reg                         sda_dir                 ; //SCCB数据(SDA)方向控制
 43 reg                         sda_out                 ; //SDA输出信号
 44 reg                         state_done              ; //状态结束
 45 reg    [ 6:0]               cnt                     ; //计数
 46 reg    [ 7:0]               state_c                 ; //状态机当前状态
 47 reg    [ 7:0]               state_n                 ; //状态机下一状态
 48 reg    [15:0]               sccb_addr_t             ; //地址寄存
 49 reg    [ 7:0]               sccb_data_t             ; //数据寄存
 50 reg    [ 9:0]               clk_cnt                 ; //分频时钟计数
 51 wire   [ 8:0]               clk_divide              ; //模块驱动时钟的分频系数
 52 
 53 //==========================================================================
 54 //==    sda控制
 55 //==========================================================================
 56 assign  sccb_sda = sda_dir ?  sda_out : 1'bz;         //SDA数据输出或高阻
 57 
 58 //==========================================================================
 59 //==     生成SCL的4倍时钟来驱动后面SCCB的操作,生成1Mhz的sccb_dri_clk
 60 //==========================================================================
 61 assign  clk_divide = (CLK/SCL) >> 3;                  // >>3即除以8
 62 
 63 always @(posedge clk or negedge rst_n) begin
 64     if(!rst_n) begin
 65         sccb_dri_clk <=  1'b1;
 66         clk_cnt <= 10'd0;
 67     end
 68     else if(clk_cnt == clk_divide - 1'd1) begin
 69         clk_cnt <= 10'd0;
 70         sccb_dri_clk <= ~sccb_dri_clk;
 71     end
 72     else
 73         clk_cnt <= clk_cnt + 1'b1;
 74 end
 75 
 76 //==========================================================================
 77 //==    状态机
 78 //==========================================================================
 79 always @(posedge sccb_dri_clk or negedge rst_n) begin
 80     if(!rst_n)
 81         state_c <= IDLE;
 82     else
 83         state_c <= state_n;
 84 end
 85 
 86 always @(*) begin
 87     case(state_c)
 88         IDLE: begin                             //空闲状态
 89            if(sccb_en)
 90                state_n = DEVICE;
 91            else
 92                state_n = IDLE;
 93         end
 94         DEVICE: begin                           //写器件ID
 95             if(state_done) begin
 96                 if(addr16_en)
 97                    state_n = ADDR_16;
 98                 else if(addr8_en)
 99                    state_n = ADDR_8 ;
100             end
101             else
102                 state_n = DEVICE;
103         end
104         ADDR_16: begin                          //写地址高8位
105             if(state_done)
106                 state_n = ADDR_8;
107             else
108                 state_n = ADDR_16;
109         end
110         ADDR_8: begin                           //写地址低8位
111             if(state_done)
112                 state_n = DATA;
113             else
114                 state_n = ADDR_8;
115         end
116         DATA: begin                             //写数据
117             if(state_done)
118                 state_n = STOP;
119             else
120                 state_n = DATA;
121         end
122         STOP: begin                             //结束
123             if(state_done)
124                 state_n = IDLE;
125             else
126                 state_n = STOP ;
127         end
128         default:state_n= IDLE;
129     endcase
130 end
131 
132 //==========================================================================
133 //==    设计各路信号
134 //==========================================================================
135 always @(posedge sccb_dri_clk or negedge rst_n) begin
136     if(!rst_n) begin
137         sccb_scl    <= 1'b1;
138         sda_out     <= 1'b1;
139         sda_dir     <= 1'b1;
140         sccb_done   <= 1'b0;
141         cnt         <= 1'b0;
142         state_done  <= 1'b0;
143         sccb_addr_t <= 1'b0;
144         sccb_data_t <= 1'b0;
145     end
146     else begin
147         state_done  <= 1'b0 ;
148         cnt         <= cnt + 1'b1 ;
149         case(state_c)
150             //--------------------------------------------------- 空闲状态
151             IDLE: begin
152                     sccb_scl  <= 1'b1;
153                     sda_out   <= 1'b1;
154                     sda_dir   <= 1'b1;
155                     sccb_done <= 1'b0;
156                     cnt       <= 7'b0;
157                     if(sccb_en) begin
158                         sccb_addr_t <= sccb_addr;
159                         sccb_data_t <= sccb_data;
160                     end
161             end
162             //--------------------------------------------------- 写器件ID
163             DEVICE: begin
164                 case(cnt)
165                     7'd1 : sda_out  <= 1'b0;
166                     7'd3 : sccb_scl <= 1'b0;
167                     7'd4 : sda_out  <= DEVICE_ID[7];
168                     7'd5 : sccb_scl <= 1'b1;
169                     7'd7 : sccb_scl <= 1'b0;
170                     7'd8 : sda_out  <= DEVICE_ID[6];
171                     7'd9 : sccb_scl <= 1'b1;
172                     7'd11: sccb_scl <= 1'b0;
173                     7'd12: sda_out  <= DEVICE_ID[5];
174                     7'd13: sccb_scl <= 1'b1;
175                     7'd15: sccb_scl <= 1'b0;
176                     7'd16: sda_out  <= DEVICE_ID[4];
177                     7'd17: sccb_scl <= 1'b1;
178                     7'd19: sccb_scl <= 1'b0;
179                     7'd20: sda_out  <= DEVICE_ID[3];
180                     7'd21: sccb_scl <= 1'b1;
181                     7'd23: sccb_scl <= 1'b0;
182                     7'd24: sda_out  <= DEVICE_ID[2];
183                     7'd25: sccb_scl <= 1'b1;
184                     7'd27: sccb_scl <= 1'b0;
185                     7'd28: sda_out  <= DEVICE_ID[1];
186                     7'd29: sccb_scl <= 1'b1;
187                     7'd31: sccb_scl <= 1'b0;
188                     7'd32: sda_out  <= DEVICE_ID[0];
189                     7'd33: sccb_scl <= 1'b1;
190                     7'd35: sccb_scl <= 1'b0;
191                     7'd36: begin
192                             sda_dir <= 1'b0;    //从机应答
193                             sda_out <= 1'b1;
194                     end
195                     7'd37: sccb_scl <= 1'b1;
196                     7'd38: state_done <= 1'b1;  //状态结束
197                     7'd39: begin
198                             sccb_scl <= 1'b0;
199                             cnt <= 1'b0;
200                     end
201                     default :  ;
202                 endcase
203             end
204             //--------------------------------------------------- 写字地址高8位
205             ADDR_16: begin
206                 case(cnt)
207                     7'd0 : begin
208                             sda_dir <= 1'b1 ;
209                             sda_out <= sccb_addr_t[15];
210                     end
211                     7'd1 : sccb_scl <= 1'b1;
212                     7'd3 : sccb_scl <= 1'b0;
213                     7'd4 : sda_out  <= sccb_addr_t[14];
214                     7'd5 : sccb_scl <= 1'b1;
215                     7'd7 : sccb_scl <= 1'b0;
216                     7'd8 : sda_out  <= sccb_addr_t[13];
217                     7'd9 : sccb_scl <= 1'b1;
218                     7'd11: sccb_scl <= 1'b0;
219                     7'd12: sda_out  <= sccb_addr_t[12];
220                     7'd13: sccb_scl <= 1'b1;
221                     7'd15: sccb_scl <= 1'b0;
222                     7'd16: sda_out  <= sccb_addr_t[11];
223                     7'd17: sccb_scl <= 1'b1;
224                     7'd19: sccb_scl <= 1'b0;
225                     7'd20: sda_out  <= sccb_addr_t[10];
226                     7'd21: sccb_scl <= 1'b1;
227                     7'd23: sccb_scl <= 1'b0;
228                     7'd24: sda_out  <= sccb_addr_t[9];
229                     7'd25: sccb_scl <= 1'b1;
230                     7'd27: sccb_scl <= 1'b0;
231                     7'd28: sda_out  <= sccb_addr_t[8];
232                     7'd29: sccb_scl <= 1'b1;
233                     7'd31: sccb_scl <= 1'b0;
234                     7'd32: begin
235                            sda_dir  <= 1'b0;    //从机应答
236                            sda_out  <= 1'b1;
237                     end
238                     7'd33: sccb_scl <= 1'b1;
239                     7'd34: state_done <= 1'b1;  //状态结束
240                     7'd35: begin
241                            sccb_scl <= 1'b0;
242                            cnt <= 1'b0;
243                     end
244                     default :  ;
245                 endcase
246             end
247             //--------------------------------------------------- 写字地址低8位
248             ADDR_8: begin
249                 case(cnt)
250                     7'd0: begin
251                             sda_dir <= 1'b1 ;
252                             sda_out <= sccb_addr_t[7];
253                     end
254                     7'd1 : sccb_scl <= 1'b1;
255                     7'd3 : sccb_scl <= 1'b0;
256                     7'd4 : sda_out  <= sccb_addr_t[6];
257                     7'd5 : sccb_scl <= 1'b1;
258                     7'd7 : sccb_scl <= 1'b0;
259                     7'd8 : sda_out  <= sccb_addr_t[5];
260                     7'd9 : sccb_scl <= 1'b1;
261                     7'd11: sccb_scl <= 1'b0;
262                     7'd12: sda_out  <= sccb_addr_t[4];
263                     7'd13: sccb_scl <= 1'b1;
264                     7'd15: sccb_scl <= 1'b0;
265                     7'd16: sda_out  <= sccb_addr_t[3];
266                     7'd17: sccb_scl <= 1'b1;
267                     7'd19: sccb_scl <= 1'b0;
268                     7'd20: sda_out  <= sccb_addr_t[2];
269                     7'd21: sccb_scl <= 1'b1;
270                     7'd23: sccb_scl <= 1'b0;
271                     7'd24: sda_out  <= sccb_addr_t[1];
272                     7'd25: sccb_scl <= 1'b1;
273                     7'd27: sccb_scl <= 1'b0;
274                     7'd28: sda_out  <= sccb_addr_t[0];
275                     7'd29: sccb_scl <= 1'b1;
276                     7'd31: sccb_scl <= 1'b0;
277                     7'd32: begin
278                            sda_dir  <= 1'b0;    //从机应答
279                            sda_out  <= 1'b1;
280                     end
281                     7'd33: sccb_scl <= 1'b1;
282                     7'd34: state_done <= 1'b1;  //状态结束
283                     7'd35: begin
284                            sccb_scl <= 1'b0;
285                            cnt <= 1'b0;
286                     end
287                     default :  ;
288                 endcase
289             end
290             //--------------------------------------------------- 写数据
291             DATA: begin
292                 case(cnt)
293                     7'd0: begin
294                             sda_out <= sccb_data_t[7];
295                             sda_dir <= 1'b1;
296                     end
297                     7'd1 : sccb_scl <= 1'b1;
298                     7'd3 : sccb_scl <= 1'b0;
299                     7'd4 : sda_out  <= sccb_data_t[6];
300                     7'd5 : sccb_scl <= 1'b1;
301                     7'd7 : sccb_scl <= 1'b0;
302                     7'd8 : sda_out  <= sccb_data_t[5];
303                     7'd9 : sccb_scl <= 1'b1;
304                     7'd11: sccb_scl <= 1'b0;
305                     7'd12: sda_out  <= sccb_data_t[4];
306                     7'd13: sccb_scl <= 1'b1;
307                     7'd15: sccb_scl <= 1'b0;
308                     7'd16: sda_out  <= sccb_data_t[3];
309                     7'd17: sccb_scl <= 1'b1;
310                     7'd19: sccb_scl <= 1'b0;
311                     7'd20: sda_out  <= sccb_data_t[2];
312                     7'd21: sccb_scl <= 1'b1;
313                     7'd23: sccb_scl <= 1'b0;
314                     7'd24: sda_out  <= sccb_data_t[1];
315                     7'd25: sccb_scl <= 1'b1;
316                     7'd27: sccb_scl <= 1'b0;
317                     7'd28: sda_out  <= sccb_data_t[0];
318                     7'd29: sccb_scl <= 1'b1;
319                     7'd31: sccb_scl <= 1'b0;
320                     7'd32: begin
321                            sda_dir  <= 1'b0;    //从机应答
322                            sda_out  <= 1'b1;
323                     end
324                     7'd33: sccb_scl <= 1'b1;
325                     7'd34: state_done <= 1'b1;  //状态结束
326                     7'd35: begin
327                            sccb_scl  <= 1'b0;
328                            cnt  <= 1'b0;
329                     end
330                     default  :  ;
331                 endcase
332             end
333             //--------------------------------------------------- 结束
334             STOP: begin
335                 case(cnt)
336                     7'd0: begin
337                            sda_dir  <= 1'b1;
338                            sda_out  <= 1'b0;
339                     end
340                     7'd1 : sccb_scl <= 1'b1;
341                     7'd3 : sda_out  <= 1'b1;
342                     7'd15: state_done <= 1'b1;  //状态结束
343                     7'd16: begin
344                            cnt <= 1'b0;
345                            sccb_done <= 1'b1;   //sccb配置完成
346                     end
347                     default  : ;
348                 endcase
349             end
350         endcase
351     end
352 end
353 
354 
355 
356 endmodule

参考资料:

1OmniVision Serial Camera Control Bus (SCCB) Functional Specification

2正点原子FPGA教程

3开源骚客.SDRAM那些事儿

协议------IIC

  I²C即Inter-Integrated Circuit(集成电路总线),它是一种串行通信总线,使用多主从架构,由飞利浦公司在1980年代设计出来的一种简单、双向、二线制总线标准。多用于主机和从机在数据量不大且传输距离短的场合下的主从通信。主机启动总线,并产生时钟用于传送数据,此时任何接收数据的器件均被认为是从机。I²C总线由数据线SDA和时钟线SCL构成通信线路,既可用于发送数据,也可接收数据。在主控与被控IC之间可进行双向数据传送,数据的传输速率在标准模式下可达100kbit/s,在快速模式下可达400kbit/s,在高速模式下可达3.4Mbit/s,各种被控器件均并联在总线上,通过器件地址(SLAVE ADDR,具体可查器件手册)识别。I²C总线物理拓扑结构图如下所示:

  图中的IIC_SCL是串行时钟线,IIC_SDA是串行数据线,由于I2C器件一般采用开漏结构与总线相连,所以IIC_SCL和IIC_SDA均需接上拉电阻,也正因此,当总线空闲时,这两条线路都处于高电平状态,当连到总线上的任一器件输出低电平,都将使总线拉低,即各器件的SDA及SCL都是"线与"关系。IIC总线支持多主和主从两种工作方式,通常工作在主从工作方式,我们的开发板就采用主从工作方式。在主从工作方式中,系统中只有一个主机,其它器件都是具有I2C总线的外围从机。在主从工作方式中,主机启动数据的发送(发出启动信号)并产生时钟信号,数据发送完成后,发出停止信号。I2C总线结构虽然简单,使用两线传输,然而要实现器件间的通信,需要通过控制SCL和SDA的时序,使其满足I2C的总线传输协议,方可实现器件间的数据传输。

一、起始和结束

  在I2C器件开始通信(传输数据)之前,串行时钟线SCL和串行数据线SDA线由于上拉的原因处于高电平状态,此时I2C总线处于空闲状态。

  如果主机(此处指FPGA)想开始传输数据,只需在SCL为高电平时将SDA线拉低,产生一个起始信号,从机检测到起始信号后,准备接收数据,当数据传输完成,主机只需产生一个停止信号,告诉从机数据传输结束,停止信号的产生是在SCL为高电平时,SDA从低电平跳变到高电平,从机检测到停止信号后,停止接收数据。起始信号之前为空闲状态,起始信号之后到停止信号之前的这一段为数据传输状态,主机可以向从机写数据,也可以读取从机输出的数据,数据的传输由双向数据线(SDA)完成。停止信号产生后,总线再次处于空闲状态。

  起始:SCL为高时,SDA由高拉低。

**  停止:SCL为高时,SDA由低拉高**。

二、数据传输和应答期

  先看看数据是怎么通过两根线传过来的,除了起始和结束比较特殊,中间的数据传输所遵循的规律如下所示:

  

1.数据传输  

  SCL为低时,SDA运行变化。

**  SCL为高时,SDA数据锁存。**

2.应答期,SDA总线是三态门

  ①在第8个时钟周期末,主机释放SDA以使从机应答

  ②在第9个时钟周期,从机将SDA拉低以应答

  ③若第9个时钟周期,SCL为高电平时,SDA未被检测到为低电平,视为非应答,表明此次数据传输失败。

  ④在第9个时钟周期末,从机释放SDA以使主机继续传输数据,如果主机发送停止信号,此次传输结束。

三、器件地址

  每个I2C器件都有一个器件地址,有些I2C器件的器件地址是固定的,而有些I2C器件的器件地址由一个固定部分和一个可编程的部分构成,这是因为很可能在一个系统中有几个同样的器件,器件地址的可编程部分能最大数量的使这些器件连接到I2C总线上,例如为了增加系统的EEPROM容量,可能需要多个EEPROM。器件可编程地址位的数量由它可使用的管脚决定,比如EEPROM器件一般会留下3个管脚用于可编程地址位,当硬件电路上分别将这3个管脚连接到GND或VCC时,就可以设置不同的可编程地址。但有些I2C器件在出厂时器件地址就设置好了,用户不可以更改(如实时时钟PCF8563的器件地址为固定的7'h51)。所以当主机想给某个器件发送数据时,只需向总线上发送接收器件的器件地址即可。

  进行数据传输时,主机首先向总线上发出开始信号,对应开始位S,然后按照从高到低的位序发送器件地址,一般为7bit,第8bit位为读写控制位R/W,该位为0时表示主机对从机进行写操作,当该位为1时表示主机对从机进行读操作,然后接收从机响应。对于AT24C64来说,其传输器件地址格式如下图所示。

四、存储器地址(字地址)

  一般而言,每个兼容I2C协议的器件,内部总会有可供读写的寄存器或存储器,对于我们本次实验用到的EEPROM存储器,内部就是一系列顺序编址的存储单元。所以,当我们对一个器件中的存储单元(包括寄存器)进行读写时,首先要指定存储单元的地址即字地址,然后再向该地址写入内容。该地址为一个或两个字节长度,具体长度由器件内部的存储单元的数量决定,当存储单元数量不超过一个字节所能表示的最大数量(2^8=256)时,用一个字节表示,超过一个字节所能表示的最大数量时,就需要用两个字节来表示。

1.单字节的字地址

2.双字节的字地址

五、IIC写

  主机发送完字地址,从机正确应答后就把内部的存储单元地址指针指向该单元。如果读写控制位R/W位为"0"即写命令,从机就处于接收数据的状态,此时,主机就开始写数据了。写数据分为单字节写(对于EEPROM而言,称为字节写)和连续写(对于EEPROM而言,称为页写)。

  不管单字节写和连续写,都可概括为:start + 器件地址 + 写命令(0) + 字地址 + 数据 + stop

1.单字节写

  单字节写:发送完一字节数据后发送结束信号。

2.连续写

  连续写:发送完一字节数据后继续发送下一字节数据,最后发送的是结束信号。

六、IIC读

  主机发送完字地址,从机正确应答后就把内部的存储单元地址指针指向该单元。如果读写控制位R/W位为"1"即读命令,主机就处于接收数据的状态,从机从该地址单元输出数据。读数据分为当前地址读、单字节读和连续读。

  不管是单字节读还是连续读,都可以概括为:start + 器件地址 + 写命令(0) + 字地址 + start + 器件地址 + 读命令(1) + 接收从机的数据 + 主机非应答(1) + stop

1.单字节读

  发送完器件地址和字地址后又发送起始信号和器件地址,而且第一次发送器件地址时后面的读写控制位为"0",也就是写命令,第二次发送器件地址时后面的读写控制位为"1",也就是读。为什么会有这样奇怪的操作呢?这是因为我们需要使从机内的存储单元地址指针指向我们想要读取的存储单元地址处,所以首先发送了一次Dummy Write也就是虚写 操作,只所以称为虚写,是因为我们并不是真的要写数据,而是通过这种虚写操作使地址指针指向虚写操作中字地址的位置,等从机应答后,就可以按当前地址读的方式读数据了。因此也可以理解为:没有发送数据的单次写操作 + 当前地址的读操作

单字节读:读取完一字节数据后,主机发送非应答信号。

2.连续读

  连续读:读取完一字节数据后,主机发送应答信号,读取完最后一个字节数据后,主机发送非应答信号。

七、IIC控制器Verilog代码设计

(修改自正点原子FPGA)

复制代码
  1 //**************************************************************************
  2 // *** 名称 : iic.v
  3 // *** 作者 : xianyu_FPGA
  4 // *** 博客 : https://www.cnblogs.com/xianyufpga/
  5 // *** 日期 : 2019-08-10
  6 // *** 描述 : IIC控制器
  7 //**************************************************************************
  8 
  9 module iic
 10 //========================< 参数 >==========================================
 11 #(
 12 parameter DEVICE_ID         = 7'b1010000            ,  //器件ID
 13 parameter CLK               = 26'd50_000_000        ,  //本模块的时钟频率
 14 parameter SCL               = 18'd250_000              //输出的SCL时钟频率
 15 )
 16 //========================< 端口 >==========================================
 17 (
 18 input                       clk                     , //时钟
 19 input                       rst_n                   , //复位,低电平有效
 20 //IIC control ---------------------------------------
 21 input                       iic_en                  , //IIC触发信号
 22 input                       addr16_en               , //16位地址使能
 23 input                       addr8_en                , //8位地址使能
 24 input                       write_en                , //IIC写使能
 25 input                       read_en                 , //IIC读使能
 26 input        [15:0]         iic_addr                , //IIC器件内地址
 27 input        [ 7:0]         iic_data_wr             , //IIC要写的数据
 28 //IIC output ----------------------------------------
 29 output  reg  [ 7:0]         iic_data_rd             , //IIC读出的数据
 30 output  reg                 iic_done                , //IIC一次操作完成
 31 output  reg                 iic_scl                 , //IIC的SCL时钟信号
 32 inout                       iic_sda                 , //IIC的SDA数据信号
 33 //dri_clk -------------------------------------------
 34 output  reg                 iic_dri_clk               //驱动IIC操作的驱动时钟,1Mhz
 35  );
 36 //========================< 参数 >==========================================
 37 localparam  IDLE            = 8'b0000_0001          ; //空闲状态
 38 localparam  DEVICE          = 8'b0000_0010          ; //写器件地址
 39 localparam  ADDR_16         = 8'b0000_0100          ; //写字地址高8位
 40 localparam  ADDR_8          = 8'b0000_1000          ; //写字地址低8位
 41 localparam  DATA_WR         = 8'b0001_0000          ; //写数据
 42 localparam  DEVICE_RD       = 8'b0010_0000          ; //虚写器件地址
 43 localparam  DATA_RD         = 8'b0100_0000          ; //读数据
 44 localparam  STOP            = 8'b1000_0000          ; //结束
 45 //========================< 信号 >==========================================
 46 reg                         sda_dir                 ; //IIC数据(SDA)方向控制
 47 reg                         sda_out                 ; //SDA输出信号
 48 wire                        sda_in                  ; //SDA输入信号
 49 reg                         state_done              ; //状态结束
 50 reg    [ 6:0]               cnt                     ; //计数
 51 reg    [ 7:0]               state_c                 ; //状态机当前状态
 52 reg    [ 7:0]               state_n                 ; //状态机下一状态
 53 reg    [15:0]               iic_addr_t              ; //地址
 54 reg    [ 7:0]               iic_data_rd_t           ; //读取的数据
 55 reg    [ 7:0]               iic_data_wr_t           ; //IIC需写的数据的临时寄存
 56 reg    [ 9:0]               clk_cnt                 ; //分频时钟计数
 57 wire   [ 8:0]               clk_divide              ; //模块驱动时钟的分频系数
 58 
 59 //==========================================================================
 60 //==    sda控制
 61 //==========================================================================
 62 assign iic_sda = sda_dir ? sda_out : 1'bz;            //SDA数据输出或高阻
 63 assign sda_in  = iic_sda ;                            //SDA数据输入
 64 
 65 //==========================================================================
 66 //==     生成SCL的4倍时钟来驱动后面IIC的操作,生成1Mhz的iic_dri_clk
 67 //==========================================================================
 68 assign clk_divide = (CLK/SCL) >> 3;                   // >>3即除以8
 69 
 70 always @(posedge clk or negedge rst_n) begin
 71     if(!rst_n) begin
 72         iic_dri_clk <=  1'b1;
 73         clk_cnt <= 10'd0;
 74     end
 75     else if(clk_cnt == clk_divide - 1'd1) begin
 76         clk_cnt <= 10'd0;
 77         iic_dri_clk <= ~iic_dri_clk;
 78     end
 79     else
 80         clk_cnt <= clk_cnt + 1'b1;
 81 end
 82 
 83 //==========================================================================
 84 //==    状态机
 85 //==========================================================================
 86 always @(posedge iic_dri_clk or negedge rst_n) begin
 87     if(!rst_n)
 88         state_c <= IDLE;
 89     else
 90         state_c <= state_n;
 91 end
 92 
 93 always @(*) begin
 94     case(state_c)
 95         IDLE: begin                             //空闲状态
 96            if(iic_en) begin
 97                state_n = DEVICE;
 98            end
 99            else
100                state_n = IDLE;
101         end
102         DEVICE: begin                           //写器件ID
103             if(state_done) begin
104                 if(addr16_en)
105                    state_n = ADDR_16;
106                 else if(addr8_en)
107                    state_n = ADDR_8 ;
108             end
109             else
110                 state_n = DEVICE;
111         end
112         ADDR_16: begin                          //写地址高8位
113             if(state_done)
114                 state_n = ADDR_8;
115             else
116                 state_n = ADDR_16;
117         end
118         ADDR_8: begin                           //写地址低8位
119             if(state_done) begin
120                 if(write_en)
121                     state_n = DATA_WR;
122                 else if(read_en)
123                     state_n = DEVICE_RD;
124             end
125             else
126                 state_n = ADDR_8;
127         end
128         DATA_WR: begin                          //写数据
129             if(state_done)
130                 state_n = STOP;
131             else
132                 state_n = DATA_WR;
133         end
134         DEVICE_RD: begin                        //虚写器件ID
135             if(state_done)
136                 state_n = DATA_RD;
137             else
138                 state_n = DEVICE_RD;
139         end
140         DATA_RD: begin                          //读数据
141             if(state_done)
142                 state_n = STOP;
143             else
144                 state_n = DATA_RD;
145         end
146         STOP: begin                             //结束
147             if(state_done)
148                 state_n = IDLE;
149             else
150                 state_n = STOP ;
151         end
152         default:state_n= IDLE;
153     endcase
154 end
155 
156 //==========================================================================
157 //==    设计各路信号
158 //==========================================================================
159 always @(posedge iic_dri_clk or negedge rst_n) begin
160     if(!rst_n) begin
161         iic_scl        <= 1'b1;
162         sda_out        <= 1'b1;
163         sda_dir        <= 1'b1;
164         iic_done       <= 1'b0;
165         cnt            <= 1'b0;
166         state_done     <= 1'b0;
167         iic_addr_t     <= 1'b0;
168         iic_data_rd    <= 1'b0;
169         iic_data_rd_t  <= 1'b0;
170         iic_data_wr_t  <= 1'b0; 
171     end
172     else begin
173         state_done <= 1'b0 ;
174         cnt        <= cnt +1'b1 ;
175         case(state_c)
176             //--------------------------------------------------- 空闲状态
177             IDLE: begin
178                     iic_scl  <= 1'b1;
179                     sda_out  <= 1'b1;
180                     sda_dir  <= 1'b1;
181                     iic_done <= 1'b0;
182                     cnt      <= 7'b0;
183                     if(iic_en) begin
184                         iic_addr_t    <= iic_addr;
185                         iic_data_wr_t <= iic_data_wr;
186                     end
187             end
188             //--------------------------------------------------- 写器件ID
189             DEVICE: begin
190                 case(cnt)
191                     7'd1 : sda_out <= 1'b0;
192                     7'd3 : iic_scl <= 1'b0;
193                     7'd4 : sda_out <= DEVICE_ID[6];
194                     7'd5 : iic_scl <= 1'b1;
195                     7'd7 : iic_scl <= 1'b0;
196                     7'd8 : sda_out <= DEVICE_ID[5];
197                     7'd9 : iic_scl <= 1'b1;
198                     7'd11: iic_scl <= 1'b0;
199                     7'd12: sda_out <= DEVICE_ID[4];
200                     7'd13: iic_scl <= 1'b1;
201                     7'd15: iic_scl <= 1'b0;
202                     7'd16: sda_out <= DEVICE_ID[3];
203                     7'd17: iic_scl <= 1'b1;
204                     7'd19: iic_scl <= 1'b0;
205                     7'd20: sda_out <= DEVICE_ID[2];
206                     7'd21: iic_scl <= 1'b1;
207                     7'd23: iic_scl <= 1'b0;
208                     7'd24: sda_out <= DEVICE_ID[1];
209                     7'd25: iic_scl <= 1'b1;
210                     7'd27: iic_scl <= 1'b0;
211                     7'd28: sda_out <= DEVICE_ID[0];
212                     7'd29: iic_scl <= 1'b1;
213                     7'd31: iic_scl <= 1'b0;
214                     7'd32: sda_out <= 1'b0;             //0:写
215                     7'd33: iic_scl <= 1'b1;
216                     7'd35: iic_scl <= 1'b0;
217                     7'd36: begin
218                            sda_dir <= 1'b0;             //从机应答
219                            sda_out <= 1'b1;
220                     end
221                     7'd37: iic_scl <= 1'b1;
222                     7'd38: state_done <= 1'b1;          //状态结束
223                     7'd39: begin
224                            iic_scl <= 1'b0;
225                            cnt <= 1'b0;
226                     end
227                     default :  ;
228                 endcase
229             end
230             //--------------------------------------------------- 写字地址高8位
231             ADDR_16: begin
232                 case(cnt)
233                     7'd0 : begin
234                            sda_dir <= 1'b1 ;
235                            sda_out <= iic_addr_t[15];
236                     end
237                     7'd1 : iic_scl <= 1'b1;
238                     7'd3 : iic_scl <= 1'b0;
239                     7'd4 : sda_out <= iic_addr_t[14];
240                     7'd5 : iic_scl <= 1'b1;
241                     7'd7 : iic_scl <= 1'b0;
242                     7'd8 : sda_out <= iic_addr_t[13];
243                     7'd9 : iic_scl <= 1'b1;
244                     7'd11: iic_scl <= 1'b0;
245                     7'd12: sda_out <= iic_addr_t[12];
246                     7'd13: iic_scl <= 1'b1;
247                     7'd15: iic_scl <= 1'b0;
248                     7'd16: sda_out <= iic_addr_t[11];
249                     7'd17: iic_scl <= 1'b1;
250                     7'd19: iic_scl <= 1'b0;
251                     7'd20: sda_out <= iic_addr_t[10];
252                     7'd21: iic_scl <= 1'b1;
253                     7'd23: iic_scl <= 1'b0;
254                     7'd24: sda_out <= iic_addr_t[9];
255                     7'd25: iic_scl <= 1'b1;
256                     7'd27: iic_scl <= 1'b0;
257                     7'd28: sda_out <= iic_addr_t[8];
258                     7'd29: iic_scl <= 1'b1;
259                     7'd31: iic_scl <= 1'b0;
260                     7'd32: begin
261                            sda_dir <= 1'b0;             //从机应答
262                            sda_out <= 1'b1;
263                     end
264                     7'd33: iic_scl <= 1'b1;
265                     7'd34: state_done <= 1'b1;          //状态结束
266                     7'd35: begin
267                            iic_scl <= 1'b0;
268                            cnt <= 1'b0;
269                     end
270                     default :  ;
271                 endcase
272             end
273             //--------------------------------------------------- 写字地址低8位
274             ADDR_8: begin
275                 case(cnt)
276                     7'd0: begin
277                            sda_dir <= 1'b1 ;
278                            sda_out <= iic_addr_t[7];
279                     end
280                     7'd1 : iic_scl <= 1'b1;
281                     7'd3 : iic_scl <= 1'b0;
282                     7'd4 : sda_out <= iic_addr_t[6];
283                     7'd5 : iic_scl <= 1'b1;
284                     7'd7 : iic_scl <= 1'b0;
285                     7'd8 : sda_out <= iic_addr_t[5];
286                     7'd9 : iic_scl <= 1'b1;
287                     7'd11: iic_scl <= 1'b0;
288                     7'd12: sda_out <= iic_addr_t[4];
289                     7'd13: iic_scl <= 1'b1;
290                     7'd15: iic_scl <= 1'b0;
291                     7'd16: sda_out <= iic_addr_t[3];
292                     7'd17: iic_scl <= 1'b1;
293                     7'd19: iic_scl <= 1'b0;
294                     7'd20: sda_out <= iic_addr_t[2];
295                     7'd21: iic_scl <= 1'b1;
296                     7'd23: iic_scl <= 1'b0;
297                     7'd24: sda_out <= iic_addr_t[1];
298                     7'd25: iic_scl <= 1'b1;
299                     7'd27: iic_scl <= 1'b0;
300                     7'd28: sda_out <= iic_addr_t[0];
301                     7'd29: iic_scl <= 1'b1;
302                     7'd31: iic_scl <= 1'b0;
303                     7'd32: begin
304                            sda_dir <= 1'b0;             //从机应答
305                            sda_out <= 1'b1;
306                     end
307                     7'd33: iic_scl <= 1'b1;
308                     7'd34: state_done <= 1'b1;          //状态结束
309                     7'd35: begin
310                            iic_scl <= 1'b0;
311                            cnt <= 1'b0;
312                     end
313                     default :  ;
314                 endcase
315             end
316             //--------------------------------------------------- 写数据
317             DATA_WR: begin
318                 case(cnt)
319                     7'd0: begin
320                            sda_out <= iic_data_wr_t[7];
321                            sda_dir <= 1'b1;
322                     end
323                     7'd1 : iic_scl <= 1'b1;
324                     7'd3 : iic_scl <= 1'b0;
325                     7'd4 : sda_out <= iic_data_wr_t[6];
326                     7'd5 : iic_scl <= 1'b1;
327                     7'd7 : iic_scl <= 1'b0;
328                     7'd8 : sda_out <= iic_data_wr_t[5];
329                     7'd9 : iic_scl <= 1'b1;
330                     7'd11: iic_scl <= 1'b0;
331                     7'd12: sda_out <= iic_data_wr_t[4];
332                     7'd13: iic_scl <= 1'b1;
333                     7'd15: iic_scl <= 1'b0;
334                     7'd16: sda_out <= iic_data_wr_t[3];
335                     7'd17: iic_scl <= 1'b1;
336                     7'd19: iic_scl <= 1'b0;
337                     7'd20: sda_out <= iic_data_wr_t[2];
338                     7'd21: iic_scl <= 1'b1;
339                     7'd23: iic_scl <= 1'b0;
340                     7'd24: sda_out <= iic_data_wr_t[1];
341                     7'd25: iic_scl <= 1'b1;
342                     7'd27: iic_scl <= 1'b0;
343                     7'd28: sda_out <= iic_data_wr_t[0];
344                     7'd29: iic_scl <= 1'b1;
345                     7'd31: iic_scl <= 1'b0;
346                     7'd32: begin
347                            sda_dir <= 1'b0;             //从机应答
348                            sda_out <= 1'b1;
349                     end
350                     7'd33: iic_scl <= 1'b1;
351                     7'd34: state_done <= 1'b1;          //状态结束
352                     7'd35: begin
353                         iic_scl  <= 1'b0;
354                         cnt  <= 1'b0;
355                     end
356                     default  :  ;
357                 endcase
358             end
359             //--------------------------------------------------- 虚写器件ID
360             DEVICE_RD: begin
361                 case(cnt)
362                     7'd0 : begin
363                            sda_dir <= 1'b1;
364                            sda_out <= 1'b1;
365                     end
366                     7'd1 : iic_scl <= 1'b1;
367                     7'd2 : sda_out <= 1'b0;             //重新开始
368                     7'd3 : iic_scl <= 1'b0;
369                     7'd4 : sda_out <= DEVICE_ID[6];
370                     7'd5 : iic_scl <= 1'b1;
371                     7'd7 : iic_scl <= 1'b0;
372                     7'd8 : sda_out <= DEVICE_ID[5];
373                     7'd9 : iic_scl <= 1'b1;
374                     7'd11: iic_scl <= 1'b0;
375                     7'd12: sda_out <= DEVICE_ID[4];
376                     7'd13: iic_scl <= 1'b1;
377                     7'd15: iic_scl <= 1'b0;
378                     7'd16: sda_out <= DEVICE_ID[3];
379                     7'd17: iic_scl <= 1'b1;
380                     7'd19: iic_scl <= 1'b0;
381                     7'd20: sda_out <= DEVICE_ID[2];
382                     7'd21: iic_scl <= 1'b1;
383                     7'd23: iic_scl <= 1'b0;
384                     7'd24: sda_out <= DEVICE_ID[1];
385                     7'd25: iic_scl <= 1'b1;
386                     7'd27: iic_scl <= 1'b0;
387                     7'd28: sda_out <= DEVICE_ID[0];
388                     7'd29: iic_scl <= 1'b1;
389                     7'd31: iic_scl <= 1'b0;
390                     7'd32: sda_out <= 1'b1;             //1:读
391                     7'd33: iic_scl <= 1'b1;
392                     7'd35: iic_scl <= 1'b0;
393                     7'd36: begin
394                            sda_dir <= 1'b0;             //从机应答
395                            sda_out <= 1'b1;
396                     end
397                     7'd37: iic_scl     <= 1'b1;
398                     7'd38: state_done <= 1'b1;          //状态结束
399                     7'd39: begin
400                            iic_scl <= 1'b0;
401                            cnt <= 1'b0;
402                     end
403                     default : ;
404                 endcase
405             end
406             //--------------------------------------------------- 读数据
407             DATA_RD: begin
408                 case(cnt)
409                     7'd0 : sda_dir <= 1'b0;
410                     7'd1 : begin
411                            iic_data_rd_t[7] <= sda_in;
412                            iic_scl <= 1'b1;
413                     end
414                     7'd3 : iic_scl <= 1'b0;
415                     7'd5 : begin
416                            iic_data_rd_t[6] <= sda_in;
417                            iic_scl <= 1'b1;
418                     end
419                     7'd7 : iic_scl <= 1'b0;
420                     7'd9 : begin
421                            iic_data_rd_t[5] <= sda_in;
422                            iic_scl <= 1'b1;
423                     end
424                     7'd11: iic_scl <= 1'b0;
425                     7'd13: begin
426                            iic_data_rd_t[4] <= sda_in;
427                            iic_scl <= 1'b1;
428                     end
429                     7'd15: iic_scl <= 1'b0;
430                     7'd17: begin
431                            iic_data_rd_t[3] <= sda_in;
432                            iic_scl <= 1'b1;
433                     end
434                     7'd19: iic_scl <= 1'b0;
435                     7'd21: begin
436                            iic_data_rd_t[2] <= sda_in;
437                            iic_scl <= 1'b1;
438                     end
439                     7'd23: iic_scl <= 1'b0;
440                     7'd25: begin
441                            iic_data_rd_t[1] <= sda_in;
442                            iic_scl <= 1'b1;
443                     end
444                     7'd27: iic_scl <= 1'b0;
445                     7'd29: begin
446                            iic_data_rd_t[0] <= sda_in;
447                            iic_scl <= 1'b1  ;
448                     end
449                     7'd31: iic_scl <= 1'b0;
450                     7'd32: begin
451                            sda_dir <= 1'b1;             //非应答
452                            sda_out <= 1'b1;
453                     end
454                     7'd33: iic_scl <= 1'b1;
455                     7'd34: state_done <= 1'b1;          //状态结束
456                     7'd35: begin
457                            iic_scl <= 1'b0;
458                            cnt <= 1'b0;
459                            iic_data_rd <= iic_data_rd_t;
460                     end
461                     default  :  ;
462                 endcase
463             end
464             //--------------------------------------------------- 结束
465             STOP: begin
466                 case(cnt)
467                     7'd0 : begin
468                            sda_dir <= 1'b1;
469                            sda_out <= 1'b0;
470                     end
471                     7'd1 : iic_scl <= 1'b1;
472                     7'd3 : sda_out <= 1'b1;
473                     7'd15: state_done <= 1'b1;          //状态结束
474                     7'd16: begin
475                            cnt <= 1'b0;
476                            iic_done <= 1'b1;            //IIC配置完成
477                     end
478                     default  : ;
479                 endcase
480             end
481         endcase
482     end
483 end
484 
485 
486 
487 
488 endmodule

协议------VGA

  VGA(Video Graphics Array)是IBM在1987年随PS/2机一起推出的一种视频传输标准,具有分辨率高、显示速率快、颜色丰富等优点,在彩色显示器领域得到了广泛的应用。不支持热插拔,不支持音频传输。对于一些嵌入式VGA显示系统,可以在不使用VGA显示卡和计算机的情况下,实现VGA图像的显示和控制。VGA显示器具有成本低、结构简单、应用灵活的优点。对于一名FPGA工程师,尤其是视频图像的方向的学习者,VGA协议是必须要掌握的。

一、外部接口

  由电路图可以看到,VGA并没有特殊的外部芯片,我们需要关注的其实只有5个信号:HS行同步信号,VS场同步信号,R红基色,G绿基色,B蓝基色。下面慢慢解释这些信号。

二、色彩原理

  经过九年义务教育的我们都应该听过三基色,还给老师了的那就在再复习一下。三基色是指通过其他颜色的混合无法得到的"基本色"由于人的肉眼有感知红、绿、蓝三种不同颜色的锥体细胞,因此色彩空间通常可以由三种基本色来表达。这是色度学的最基本原理,即三基色原理。三种基色是相互独立的,任何一种基色都不能有其它两种颜色合成。红绿蓝是三基色,这三种颜色合成的颜色范围最为广泛。我们的RGB信号真是三基色的运用,对这三个信号赋予不同的数值,混合起来便是不同的色彩。

  设计RGB信号时,既可以R信号、G信号和B信号独立的赋值,最后连到端口上,也可以直接用RGB当做一个整体信号,RGB信号在使用时的位宽有三种常见格式,以你的VGA解码芯片的配置有关。

  1. RGB_8,R:G:B = 3:3:2,即RGB332

  2. RGB_16,R:G:B = 5:6:5,即RGB565

  3. RGB_24,R:G:B = 8:8:8,即RGB888

三、扫描方式

  VGA显示器扫描方式分为逐行扫描和隔行扫描:逐行扫描是扫描从屏幕左上角一点开始,从左像右逐点扫描,每扫描完一行,电子束回到屏幕的左边下一行的起始位置,在这期间,CRT对电子束进行消隐,每行结束时,用行同步信号进行同步;当扫描完所有的行,形成一帧,用场同步信号进行场同步,并使扫描回到屏幕左上方,同时进行场消隐,开始下一帧。隔行扫描是指电子束扫描时每隔一行扫一线,完成一屏后在返回来扫描剩下的线,隔行扫描的显示器闪烁的厉害,会让使用者的眼睛疲劳。因此我们一般都采用逐行扫描的方式。

  扫描原理如下所示:

四、行场信号

行场信号共有 4 种模式,即 hsync 和 vsync 的高低状态不同,如下所示:

  一开始看这些时序图可能看不懂,它是把行场信号绘制在同一张图里,说明行场信号的控制是相似的,只是时间参数不一样而已。如果展开的话,其实时序是这样的:

  这样就清楚了,大致是若干个HS信号才组合而成一个VS,如果在一副图片中,那正确的时序表示方式应该如下图这样。

  现在稍稍解释一下这些参数。SYNC是"信号同步",Back proch和Left border常常加在一起称为"显示后沿",Addressable video为"显示区域",Right porder和Front porch常常加在一起称为"显示前沿",一个时序其实就是先拉高一段较短的"信号同步"时间,然后拉低一段很长的时间,这就是一个回合。同时需要注意,其实也可以完全相反。即先拉低一段时间"信号同步"时间,然后拉高一段很长的时间。

  具体这些时间参数是怎么来的呢?且看下文。

五、规格参数

  直接拿数据手册说话!

  以上是 640x480 @60Hz 的参数表,对着这个表即可确定时间。如果为了嫌麻烦,也可以先计算好写在代码里。

复制代码
//**************************************************************************
// *** 名称 : VGA_driver.v
// *** 作者 : xianyu_FPGA
// *** 博客 : https://www.cnblogs.com/xianyufpga/
// *** 日期 : 2019-06-26
// *** 描述 : VGA驱动模块,VGA_req和VGA_x、VGA_y信号一般不同时使用
//**************************************************************************

module VGA_driver
//========================< 端口 >==========================================
(
//system ----------------------------------------
input   wire                clk                 , //时钟,25Mhz
input   wire                rst_n               , //复位,低电平有效
//VGA_display -----------------------------------
input   wire  [15:0]        VGA_din             , //得到图像数据
output  wire                VGA_req             , //请求图像数据
output  wire  [10:0]        VGA_x               , //请求显示区域横坐标
output  wire  [10:0]        VGA_y               , //请求显示区域纵坐标
//VGA output ------------------------------------
output  wire                VGA_clk             , //VGA接口时钟信号
output  wire                VGA_blank           , //VGA接口空白信号,低有效
output  wire                VGA_de                , //VGA接口使能信号,高有效
output  wire                VGA_hsync           , //VGA接口行信号
output  wire                VGA_vsync           , //VGA接口场信号
output  wire  [15:0]        VGA_data              //VGA接口数据信号
);
//========================< 参数 >==========================================
/*                      640x480 @60Hz 25Mhz
//---------------------------------------------------------------
parameter H_TOTAL           = 800                               ; //行扫描周期
parameter H_ADDR            = 640                               ; //行有效数据
parameter H_RIGHT_BORDER    = 8                                 ; 
parameter H_FRONT_PORCH     = 8                                 ;
parameter H_FRONT           = H_RIGHT_BORDER + H_FRONT_PORCH    ; //行显示前沿
parameter H_SYNC            = 96                                ; //行同步
parameter H_BACK_PORCH      = 40                                ;
parameter H_LEFT_BORDER     = 8                                 ;
parameter H_BACK            = H_BACK_PORCH + H_LEFT_BORDER      ; //行显示后沿
//-----------------------
parameter V_TOTAL           = 525                               ; //场扫描周期
parameter V_ADDR            = 480                               ; //场有效数据
parameter V_BOTTOM_BORDER   = 8                                 ;
parameter V_FRONT_PORCH     = 2                                 ;
parameter V_FRONT           = V_BOTTOM_BORDER + V_FRONT_PORCH   ; //场显示前沿
parameter V_SYNC            = 2                                 ; //场同步
parameter V_BACK_PORCH      = 25                                ;
parameter V_TOP_BORDER      = 8                                 ;
parameter V_BACK            = V_BACK_PORCH + V_TOP_BORDER       ; //场显示后沿
//--------------------------------------------------------------- */
//                      1024x768 @60Hz 65Mhz
//---------------------------------------------------------------
parameter H_TOTAL           = 1344                              ; //行扫描周期
parameter H_ADDR            = 1024                              ; //行有效数据
parameter H_RIGHT_BORDER    = 0                                 ; 
parameter H_FRONT_PORCH     = 24                                ;
parameter H_FRONT           = H_RIGHT_BORDER + H_FRONT_PORCH    ; //行显示前沿
parameter H_SYNC            = 136                               ; //行同步
parameter H_BACK_PORCH      = 160                               ;
parameter H_LEFT_BORDER     = 0                                 ;
parameter H_BACK            = H_BACK_PORCH + H_LEFT_BORDER      ; //行显示后沿
//-----------------------
parameter V_TOTAL           = 806                               ; //场扫描周期
parameter V_ADDR            = 768                               ; //场有效数据
parameter V_BOTTOM_BORDER   = 0                                 ;
parameter V_FRONT_PORCH     = 3                                 ;
parameter V_FRONT           = V_BOTTOM_BORDER + V_FRONT_PORCH   ; //场显示前沿
parameter V_SYNC            = 6                                 ; //场同步
parameter V_BACK_PORCH      = 29                                ;
parameter V_TOP_BORDER      = 0                                 ;
parameter V_BACK            = V_BACK_PORCH + V_TOP_BORDER       ; //场显示后沿
//---------------------------------------------------------------
/*                      1280x720 @60Hz 74.25Mhz
//---------------------------------------------------------------
parameter H_TOTAL           = 1650                              ; //行扫描周期
parameter H_ADDR            = 1280                              ; //行有效数据
parameter H_RIGHT_BORDER    = 0                                 ; 
parameter H_FRONT_PORCH     = 110                               ;
parameter H_FRONT           = H_RIGHT_BORDER + H_FRONT_PORCH    ; //行显示前沿
parameter H_SYNC            = 40                                ; //行同步
parameter H_BACK_PORCH      = 220                               ;
parameter H_LEFT_BORDER     = 0                                 ;
parameter H_BACK            = H_BACK_PORCH + H_LEFT_BORDER      ; //行显示后沿
//-----------------------
parameter V_TOTAL           = 750                               ; //场扫描周期
parameter V_ADDR            = 720                               ; //场有效数据
parameter V_BOTTOM_BORDER   = 0                                 ;
parameter V_FRONT_PORCH     = 5                                 ;
parameter V_FRONT           = V_BOTTOM_BORDER + V_FRONT_PORCH   ; //场显示前沿
parameter V_SYNC            = 5                                 ; //场同步
parameter V_BACK_PORCH      = 20                                ;
parameter V_TOP_BORDER      = 0                                 ;
parameter V_BACK            = V_BACK_PORCH + V_TOP_BORDER       ; //场显示后沿
//--------------------------------------------------------------- */
//========================< 信号 >==========================================
reg   [10:0]                cnt_h               ;
wire                        add_cnt_h           ;
wire                        end_cnt_h           ;
reg   [10:0]                cnt_v               ;
wire                        add_cnt_v           ;
wire                        end_cnt_v           ;
//==========================================================================
//==    行、场计数
//==========================================================================
always @(posedge clk or negedge rst_n) begin
    if(!rst_n)
        cnt_h <= 0;
    else if(add_cnt_h) begin
        if(end_cnt_h)
            cnt_h <= 0;
        else
            cnt_h <= cnt_h + 1;
    end
end

assign add_cnt_h = 1;
assign end_cnt_h = add_cnt_h && cnt_h==H_TOTAL-1;

always @(posedge clk or negedge rst_n) begin 
    if(!rst_n)
        cnt_v <= 0;
    else if(add_cnt_v) begin
        if(end_cnt_v)
            cnt_v <= 0;
        else
            cnt_v <= cnt_v + 1;
    end
end

assign add_cnt_v = end_cnt_h;
assign end_cnt_v = add_cnt_v && cnt_v==V_TOTAL-1;
//==========================================================================
//==    数据请求和数据坐标
//==========================================================================
//VGA请求
assign VGA_req = (cnt_h >= H_SYNC + H_BACK - 1) && (cnt_h < H_SYNC + H_BACK + H_ADDR - 1) &&
                 (cnt_v >= V_SYNC + V_BACK    ) && (cnt_v < V_SYNC + V_BACK + V_ADDR    )
                 ? 1 : 0;
//VGA坐标
assign VGA_x = VGA_req ? (cnt_h - (H_SYNC + H_BACK - 1'b1)) : 10'd0;
assign VGA_y = VGA_req ? (cnt_v - (V_SYNC + V_BACK - 1'b1)) : 10'd0;
//==========================================================================
//==    VGA output
//==========================================================================
//时钟
assign VGA_clk = ~clk;

//行场
assign VGA_hsync = (cnt_h < H_SYNC) ? 0 : 1;
assign VGA_vsync = (cnt_v < V_SYNC) ? 0 : 1;

//空白,直接置1也行
assign VGA_blank = VGA_hsync & VGA_vsync;

//使能,高有效,非VGA必须,但大多数LCD屏都需要
assign VGA_de = (cnt_h >= H_SYNC + H_BACK) && (cnt_h < H_SYNC + H_BACK + H_ADDR) &&
                (cnt_v >= V_SYNC + V_BACK) && (cnt_v < V_SYNC + V_BACK + V_ADDR)
                ? 1 : 0;

//数据
assign VGA_data = VGA_de ? VGA_din : 16'b0;




endmodule

  注意一下,VGA 的驱动电路常用的有 2 种:

(1) R-2R 电阻模拟电路设计方案

  该方案更便宜,在 1024x768@60hz 及以下的分辨率条件下稳定运行,多见于 16 位的 VGA 接口中,通常不需要 VGA_clk 和 VGA_blank 信号。

(2)专用视频转换 DAC 芯片实现 VGA电路方案

  各方面都更牛逼,常见于 24 位的 VGA 接口中,通常需要 VGA_clk 和 VGA_blank 信号,有些还有 VGA_sync 信号,但该信号一般在电路上就接地了。

  本代码中对这些信号都进行了设计,要用就用,不用就不连到 FPGA 引脚就行。如果需要 24 位,只需要修改输入输出的位宽即可。

六、实例讲解

  最近使用的开发板带了一个TFT屏,分辨率为480x272,其显示原理和VGA接口完全相同,因此拿这个屏幕编写一段程序看看。

复制代码
  1 //==========================================================================
  2 // --- 名称 : TFT_driver.v
  3 // --- 作者 : xianyu_FPGA
  4 // --- 日期 : 2019-01-03
  5 // --- 描述 : TFT显示屏控制器,分辨率480x272,显示三个竖着的彩条
  6 //==========================================================================
  7 
  8 module TFT_driver
  9 //=====================<端口声明>===========================================
 10 (
 11 //input -------------------------------------
 12 input  wire             clk                 , //时钟,9Mhz
 13 input  wire             rst_n               , //复位,低电平有效
 14 //user interfaces ---------------------------
 15 output wire             TFT_req             , //输出请求信号
 16 input  wire [15:0]      data                , //得到图像数据
 17 //output ------------------------------------
 18 output wire             TFT_clk             , //TFT像素时钟
 19 output wire             TFT_de              , //TFT使能
 20 output wire             TFT_pwm             , //TFT背光控制
 21 output wire             TFT_hsync           , //TFT行同步信号
 22 output wire             TFT_vsync           , //TFT场同步信号
 23 output reg  [15:0]      TFT_rgb               //TFT像素输出
 24 );
 25 //=====================<参数定义>===========================================
 26 //480x272 @60 9Mhz --------------------------
 27 parameter H_TOTAL       = 525               ; //行扫描周期
 28 parameter H_ADDR        = 480               ; //行有效数据
 29 parameter H_FRONT       = 2                 ; //行显示前沿
 30 parameter H_SYNC        = 41                ; //行同步
 31 parameter H_BACK        = 2                 ; //行显示后沿
 32 parameter V_TOTAL       = 286               ; //场扫描周期
 33 parameter V_ADDR        = 272               ; //场有效数据
 34 parameter V_FRONT       = 2                 ; //场显示前沿
 35 parameter V_SYNC        = 10                ; //场同步
 36 parameter V_BACK        = 2                 ; //场显示后沿
 37 
 38 //=====================<信号定义>===========================================
 39 //行场信号
 40 reg  [9:0]              cnt_h               ;
 41 wire                    add_cnt_h           ;
 42 wire                    end_cnt_h           ;
 43 reg  [9:0]              cnt_v               ;
 44 wire                    add_cnt_v           ;
 45 wire                    end_cnt_v           ;
 46 reg                     TFT_en              ;
 47 wire                    red_area            ;
 48 wire                    green_area          ;
 49 wire                    blue_area           ;
 50 
 51 //--------------------------------------------------------------------------
 52 //--   行、场计数
 53 //--------------------------------------------------------------------------
 54 always @(posedge clk or negedge rst_n) begin
 55     if(!rst_n)
 56         cnt_h <= 0;
 57     else if(add_cnt_h) begin
 58         if(end_cnt_h)
 59             cnt_h <= 0;
 60         else
 61             cnt_h <= cnt_h + 1;
 62     end
 63 end
 64 
 65 assign add_cnt_h = 1;
 66 assign end_cnt_h = add_cnt_h && cnt_h==H_TOTAL-1;
 67 
 68 always @(posedge clk or negedge rst_n) begin 
 69     if(!rst_n)
 70         cnt_v <= 0;
 71     else if(add_cnt_v) begin
 72         if(end_cnt_v)
 73             cnt_v <= 0;
 74         else
 75             cnt_v <= cnt_v + 1;
 76     end
 77 end
 78 
 79 assign add_cnt_v = end_cnt_h;
 80 assign end_cnt_v = add_cnt_v && cnt_v==V_TOTAL-1;
 81 
 82 //--------------------------------------------------------------------------
 83 //--    TFT请求信号和使能信号,注意时序的对齐
 84 //--------------------------------------------------------------------------
 85 assign TFT_req = (cnt_h >= H_SYNC + H_BACK - 1) && (cnt_h < H_SYNC + H_BACK + H_ADDR - 1) &&
 86                  (cnt_v >= V_SYNC + V_BACK    ) && (cnt_v < V_SYNC + V_BACK + V_ADDR    )
 87                  ? 1 : 0;
 88 
 89 always @(posedge clk) begin
 90     TFT_en <= TFT_req;
 91 end
 92 
 93 //--------------------------------------------------------------------------
 94 //--   行场信号
 95 //--------------------------------------------------------------------------
 96 assign TFT_hsync = (cnt_h < H_SYNC) ? 0 : 1;
 97 assign TFT_vsync = (cnt_v < V_SYNC) ? 0 : 1;
 98 
 99 //--------------------------------------------------------------------------
100 //--    其他信号
101 //--------------------------------------------------------------------------
102 assign TFT_clk = clk;
103 assign TFT_de  = TFT_en;
104 assign TFT_pwm = rst_n;
105 
106 //--------------------------------------------------------------------------
107 //--   rgb信号
108 //--------------------------------------------------------------------------
109 //assign TFT_rgb = TFT_en ? data : 0;
110 
111 always @(*) begin
112     if(TFT_en) begin
113 
114         if(red_area) begin                      //红色区域
115             TFT_rgb <= 16'b11111_000000_00000;
116         end
117         else if(green_area) begin               //绿色区域
118             TFT_rgb <= 16'b00000_111111_00000;
119         end
120         else if(blue_area) begin                //蓝色区域
121             TFT_rgb <= 16'b00000_000000_11111;
122         end
123         
124     end
125     else begin                                  //非显示区域
126         TFT_rgb <= 0;
127     end
128 end
129 
130 
131 assign red_area   = cnt_h >= (H_SYNC + H_BACK) && cnt_h < (H_SYNC + H_BACK + H_ADDR*1/3) && 
132                     cnt_v >= (V_SYNC + V_BACK) && cnt_v < (V_SYNC + V_BACK + V_ADDR);
133 assign green_area = cnt_h >= (H_SYNC + H_BACK) && cnt_h < (H_SYNC + H_BACK + H_ADDR*2/3) && 
134                     cnt_v >= (V_SYNC + V_BACK) && cnt_v < (V_SYNC + V_BACK + V_ADDR);
135 assign blue_area  = cnt_h >= (H_SYNC + H_BACK) && cnt_h < (H_SYNC + H_BACK + H_ADDR*3/3) && 
136                     cnt_v >= (V_SYNC + V_BACK) && cnt_v < (V_SYNC + V_BACK + V_ADDR);
137 
138 
139                                  
140 
141 endmodule

  这个工程还包括顶层top模块,pll分频模块,这些就不展示了。还一点是接口处的user interfaces的信号没有使用到,而是自己通过代码赋的值。工程最终正常运行,显示出从左到右的三个竖彩条,其效果如下所示:

七、后记

  这样只是简单的使用了VGA,最终还是要以显示视频或图像为目标,这就涉及到模块之间的交互问题,下次再总结吧!

参考资料:

1开源骚客.VGA系列之一:VGA显示驱动篇

2NingHeChuan.基于FPGA的VGA显示静态图片

3威三学院FPGA教程

4袁玉卓, 曾凯锋, 梅雪松. FPGA自学笔记:设计与验证M. 北京航空航天出版社, 2017.

协议------UART(RS232)

一、UART简介

  UART(universal asynchronous receiver-transmitter)是一种采用异步串行通信方式的通用异步收发传输器。一般来说,UART总是和RS232成对出现,那RS232又是什么呢? RS232也就是我们计算机上的串口,它的全称是EIA-RS-232C (简称232,或者是RS232 )。其中EIA(Electronic Industry Association)代表美国电子工业协会,RS是Recommended Standard的缩写,代表推荐标准,232 是标识符,C表示修改次数,它被广泛用于计算机串行接口外设连接。如果你的计算机上还有串口的话,那么你就可以在主机箱后面看到RS232的接口:

  随着时代的发展,这种借口已经很少用了,取而代之的是"USB转串口",功能和原先一样,但接口更高效了。

  串口的主要功能为:在发送数据时将并行数据转换成串行数据进行传输,在接收数据时将接收到的串行数据转换成并行数据。这应该是大多数人接触电子后学习到的第一个通信协议吧。

二、通信格式

  下面来说说串口的具体要点:

1.传输时序

  UART串口通信需要两个信号线来实现,一根用于串口发送,另外一根负责串口接收。一开始高电平,然后拉低表示开始位,接着8个数据位,然后校验位,最后拉高表示停止位,并且进入空闲状态,等待下一次的数据传输。

  很多时候我们的校验位是允许省略的,所以协议就变成了:开始+数据+停止。

2.传输速率:波特率

  串口通信的速率用波特率表示,它表示麦苗传输二进制数据的位数,单位是bps(位/秒)。常用的波特率有9600、19200、35400、57600以及115200等。

  FPGA开发串口时,设计波特率的方法:FPGA的时钟频率/波特率。例如我的FPGA开发板时钟频率为50Mhz,即50_000_000hz,我想使用的波特率为9600bps,因此我需要的计数为:50000000/9600≈5208。

三、串口回环设计

  现在用FPGA开发板做一个串口回环的实验,要求是PC端通过串口助手发送数据给FPGA,FPGA接收到数据后返回给PC端,并在串口助手处显示数值。即串口助手发什么就能收回什么。实验框图如下:

1.uart_rx

复制代码
  1 //**************************************************************************
  2 // *** 名称 : uart_rx.v
  3 // *** 作者 : xianyu_FPGA
  4 // *** 博客 : https://www.cnblogs.com/xianyufpga/
  5 // *** 日期 : 2019-01-10
  6 // *** 描述 : 串口接收模块,计数9.5下,其中停止位0.5下
  7 //            因为串口助手发送本次停止位和下次开始位中间没有留空闲位
  8 //            若计满10下,则才结束本次传输下次数据就来了,会来不及接收
  9 //**************************************************************************
 10 
 11 module uart_rx
 12 //========================< 参数 >==========================================
 13 #(
 14 parameter  CLK              = 50_000_000        , //系统时钟,50Mhz
 15 parameter  BPS              = 9600              , //波特率
 16 parameter  BPS_CNT          = CLK/BPS             //波特率计数
 17 )
 18 //========================< 端口 >==========================================
 19 (
 20 input   wire                clk                 , //时钟,50Mhz
 21 input   wire                rst_n               , //复位,低电平有效
 22 input   wire                din                 , //输入数据
 23 output  reg   [7:0]         dout                , //输出数据
 24 output  reg                 dout_vld              //输出数据的有效指示
 25 );
 26 //========================< 信号 >==========================================
 27 reg                         rx0                 ;
 28 reg                         rx1                 ;
 29 reg                         rx2                 ;
 30 wire                        rx_en               ;
 31 reg                         flag                ;
 32 reg   [15:0]                cnt0                ;
 33 wire                        add_cnt0            ;
 34 wire                        end_cnt0            ;
 35 reg   [ 3:0]                cnt1                ;
 36 wire                        add_cnt1            ;
 37 wire                        end_cnt1            ;
 38 reg   [ 7:0]                data                ;
 39 
 40 //==========================================================================
 41 //==    消除亚稳态 + 下降沿检测
 42 //==========================================================================
 43 always @(posedge clk or negedge rst_n) begin
 44     if(!rst_n) begin
 45         rx0 <= 1;
 46         rx1 <= 1;
 47         rx2 <= 1;
 48     end
 49     else begin
 50         rx0 <= din;
 51         rx1 <= rx0;
 52         rx2 <= rx1;
 53     end
 54 end
 55 
 56 assign rx_en = rx2 && ~rx1;
 57 
 58 //==========================================================================
 59 //==    接收状态指示
 60 //==========================================================================
 61 always @(posedge clk or negedge rst_n) begin
 62     if(!rst_n)
 63         flag <= 0;
 64     else if(rx_en)
 65         flag <= 1;
 66     else if(end_cnt1)
 67         flag <= 0;
 68 end
 69 
 70 //==========================================================================
 71 //==    波特率计数
 72 //==========================================================================
 73 always @(posedge clk or negedge rst_n) begin
 74     if(!rst_n)
 75         cnt0 <= 0;
 76     else if(add_cnt0) begin
 77         if(end_cnt0)
 78             cnt0 <= 0;
 79         else
 80             cnt0 <= cnt0 + 1;
 81     end
 82 end
 83 
 84 assign add_cnt0 = flag;
 85 assign end_cnt0 = cnt0== BPS_CNT-1 || end_cnt1;
 86 
 87 //==========================================================================
 88 //==    开始1位(不接收) + 数据8位 + 停止0.5位(不接收),共10位
 89 //==========================================================================
 90 always @(posedge clk or negedge rst_n) begin 
 91     if(!rst_n)
 92         cnt1 <= 0;
 93     else if(add_cnt1) begin
 94         if(end_cnt1)
 95             cnt1 <= 0;
 96         else
 97             cnt1 <= cnt1 + 1;
 98     end
 99 end
100 
101 assign add_cnt1 = end_cnt0;
102 assign end_cnt1 = cnt1==10-1 && cnt0==BPS_CNT/2-1;
103 
104 //==========================================================================
105 //==    缓存数据
106 //==========================================================================
107 always @ (posedge clk or negedge rst_n)begin
108     if(!rst_n)
109         data <= 8'd0;
110     else if(cnt1>=1 && cnt1<=8 && cnt0==BPS_CNT/2-1) //中间采样
111         data[cnt1-1] <= rx2;                         //或 dout <= {rx2,dout[7:1]};
112 end
113 
114 //==========================================================================
115 //==    输出数据
116 //==========================================================================
117 always @ (posedge clk or negedge rst_n)begin
118     if(!rst_n)
119         dout <= 0;
120     else if(end_cnt1)   
121         dout <= data;
122 end
123 
124 always @ (posedge clk or negedge rst_n)begin
125     if(!rst_n)
126         dout_vld <= 0;
127     else if(end_cnt1)   
128         dout_vld <= 1;  
129     else    
130         dout_vld <= 0;
131 end
132 
133 
134 
135 endmodule

2.uart_tx

复制代码
  1 //**************************************************************************
  2 // *** 名称 : uart_tx.v
  3 // *** 作者 : xianyu_FPGA
  4 // *** 博客 : https://www.cnblogs.com/xianyufpga/
  5 // *** 日期 : 2019-01-10
  6 // *** 描述 : 串口接收模块,计数9.5下,其中停止位0.5下
  7 //            因为极端情况是本次停止位和下次开始位中间没有留空闲位
  8 //            若计满10下,则才结束本次传输下次数据就来了,会来不及发送
  9 //**************************************************************************
 10 
 11 module uart_tx
 12 //========================< 参数 >==========================================
 13 #(
 14 parameter  CLK              = 50_000_000        , //系统时钟,50Mhz
 15 parameter  BPS              = 9600              , //波特率
 16 parameter  BPS_CNT          = CLK/BPS             //波特率计数
 17 )
 18 //========================< 端口 >==========================================
 19 (
 20 input   wire                clk                 , //时钟,50Mhz
 21 input   wire                rst_n               , //复位,低电平有效
 22 input   wire  [7:0]         din                 , //输入数据
 23 input   wire                din_vld             , //输入数据的有效指示
 24 output  reg                 dout                  //输出数据
 25 );
 26 //========================< 信号 >==========================================
 27 reg                         flag                ;
 28 reg   [ 7:0]                din_tmp             ;
 29 reg   [15:0]                cnt0                ;
 30 wire                        add_cnt0            ;
 31 wire                        end_cnt0            ;
 32 reg   [ 3:0]                cnt1                ;
 33 wire                        add_cnt1            ;
 34 wire                        end_cnt1            ;
 35 wire  [ 9:0]                data                ;
 36 
 37 //==========================================================================
 38 //==    数据暂存(din可能会消失,暂存住)
 39 //==========================================================================
 40 always @ (posedge clk or negedge rst_n) begin
 41     if(!rst_n)
 42         din_tmp <=8'd0;
 43     else if(din_vld)
 44         din_tmp <= din;
 45 end
 46 
 47 //==========================================================================
 48 //==    发送状态指示
 49 //==========================================================================
 50 always  @(posedge clk or negedge rst_n)begin
 51     if(!rst_n)
 52         flag <= 0;
 53     else if(din_vld)
 54         flag <= 1;
 55     else if(end_cnt1)
 56         flag <= 0;
 57 end
 58 
 59 //==========================================================================
 60 //==    波特率计数
 61 //==========================================================================
 62 always @(posedge clk or negedge rst_n) begin
 63     if(!rst_n)
 64         cnt0 <= 0;
 65     else if(add_cnt0) begin
 66         if(end_cnt0)
 67             cnt0 <= 0;
 68         else
 69             cnt0 <= cnt0 + 1;
 70     end
 71 end
 72 
 73 assign add_cnt0 = flag;
 74 assign end_cnt0 = cnt0== BPS_CNT-1 || end_cnt1;
 75 
 76 //==========================================================================
 77 //==    开始1位 + 数据8位 + 停止0.5位,共10位
 78 //==========================================================================
 79 always @(posedge clk or negedge rst_n) begin 
 80     if(!rst_n)
 81         cnt1 <= 0;
 82     else if(add_cnt1) begin
 83         if(end_cnt1)
 84             cnt1 <= 0;
 85         else
 86             cnt1 <= cnt1 + 1;
 87     end
 88 end
 89 
 90 assign add_cnt1 = end_cnt0;
 91 assign end_cnt1 = cnt1==10-1 && cnt0==BPS_CNT/2-1;
 92 
 93 //==========================================================================
 94 //==    数据输出(用case语句也行)
 95 //==========================================================================
 96 assign data = {1'b1,din_tmp,1'b0};  //停止,数据,开始
 97 
 98 always @(posedge clk or negedge rst_n) begin
 99     if(!rst_n)
100         dout <= 1'b1;
101     else if(flag)
102         dout <= data[cnt1];
103 end
104 
105 
106 
107 endmodule

3.top层

复制代码
 1 //**************************************************************************
 2 // *** 名称 : uart_top.v
 3 // *** 作者 : xianyu_FPGA
 4 // *** 博客 : https://www.cnblogs.com/xianyufpga/
 5 // *** 日期 : 2019-01-10
 6 // *** 描述 : 串口实验顶层文件
 7 //**************************************************************************
 8 
 9 module uart_top
10 //========================< 端口 >==========================================
11 (
12 input  wire                 clk                 , //时钟,50Mhz
13 input  wire                 rst_n               , //复位,低电平有效
14 input  wire                 uart_rx             , //FPGA通过串口接收的数据
15 output wire                 uart_tx               //FPGA通过串口发送的数据
16 );
17 
18 //========================< 连线 >==========================================
19 wire [7:0]                  data                ;
20 wire                        data_vld            ;
21 
22 //==========================================================================
23 //==    模块例化
24 //==========================================================================
25 uart_rx
26 #(
27     .BPS_CNT                (52                 )    //仿真用
28 )
29 u_uart_rx
30 (
31     .clk                    (clk                ),
32     .rst_n                  (rst_n              ),
33     .din                    (uart_rx            ),
34     .dout                   (data               ),
35     .dout_vld               (data_vld           )
36 );
37 
38 uart_tx
39 #(
40     .BPS_CNT                (52                 )   //仿真用
41 )
42 u_uart_tx
43 (
44     .clk                    (clk                ),
45     .rst_n                  (rst_n              ),
46     .din_vld                (data_vld           ),
47     .din                    (data               ),
48     .dout                   (uart_tx            )
49 );
50 
51 
52 
53 endmodule

四、仿真调试

1、testbench

复制代码
 1 `timescale 1ns/1ps  //时间精度
 2 `define    Clock 20 //时钟周期
 3 
 4 module uart_top_tb;
 5 
 6 //========================< 端口 >==========================================
 7 reg                         clk                 ; //时钟,50Mhz
 8 reg                         rst_n               ; //复位,低电平有效
 9 reg                         uart_rx             ;
10 wire                        uart_tx             ;
11 
12 //==========================================================================
13 //==    模块例化
14 //==========================================================================
15 uart_top u_uart_top
16 (
17     .clk                    (clk                ),
18     .rst_n                  (rst_n              ),
19     .uart_rx                (uart_rx            ),
20     .uart_tx                (uart_tx            )
21 );
22 
23 //==========================================================================
24 //==    时钟信号和复位信号
25 //==========================================================================
26 initial begin
27     clk = 1;
28     forever
29         #(`Clock/2) clk = ~clk;
30 end
31 
32 initial begin
33     rst_n = 0; #(`Clock*20+1);
34     rst_n = 1;
35 end
36 
37 //==========================================================================
38 //==    task任务
39 //==========================================================================
40 reg  [7:0]              mem[15:0]           ; //位宽为8,深度为16个数据
41 integer                 i                   ;
42 integer                 j                   ;
43 
44 //读取外部数据
45 initial $readmemh("./data.txt",mem);
46 
47 //位赋值
48 task rx_bit
49 (
50     input [7:0]         data
51 );
52     begin
53         for(i=0;i<=9;i=i+1) begin   //10个bit为
54             case(i)
55                  0: uart_rx = 1'b0;
56                  1: uart_rx = data[i-1];
57                  2: uart_rx = data[i-1];
58                  3: uart_rx = data[i-1];
59                  4: uart_rx = data[i-1];
60                  5: uart_rx = data[i-1];
61                  6: uart_rx = data[i-1];
62                  7: uart_rx = data[i-1];
63                  8: uart_rx = data[i-1];
64                  9: uart_rx = 1'b1;
65             endcase 
66             #1040; //一个完整波特延时:52*20=1040
67         end        //考虑到空闲位,也可以设置得1040稍大一些
68     end
69 endtask 
70 
71 //字节赋值
72 task rx_byte;
73     begin
74         for(j=0;j<=15;j=j+1) //16个byte数据
75             rx_bit(mem[j]);
76     end
77 endtask
78 
79 //==========================================================================
80 //==    调用task
81 //==========================================================================
82 initial begin
83     #(`Clock*20+1);
84     rx_byte();
85 end
86 
87 initial begin
88     #180000;
89     $stop;
90 end
91 
92 endmodule

2、data.txt

testbench中调用了一个 data.txt 文本文档,里面存储了此次仿真的16个数据,将其放置到 Modelsim 软件的工程目录中(非 work)即可。

复制代码
0
1
2
3
4
5
6
7
8
9
a
b
c
d
e
f

3、仿真波形

  由波形可以看到,本次设计应该是成功的。

五、上板验证

  本次上位机采用友善串口助手,无校验位,停止位为1。当串口助手发送数据给FPGA后,FPGA很快又将原数据返回给上位机。

  经上板验证,本次设计成功!

参考资料:

1明德扬FPGA教程

2正点原子FPGA教程

2威三学院FPGA教程

相关推荐
仲南音1 天前
Xilinx FPGA——时钟IP核MMCM、PLL
fpga开发
I'm a winner2 天前
基于Xilinx FPGA的LVDS高速串行通信系统(三)--数据训练及握手机制【文末源码】
fpga开发
upper20202 天前
coremark移植坑之-nostartfiles -nostdlib参数
学习·fpga开发·coremark移植·riscv设计
Eloudy2 天前
Vivado 纯命令行构建 FPGA 项目教程
fpga开发
Rambo.xia2 天前
AXI跨时钟域与数据一致性——CDC FIFO深度不够、写后读冒险、多Master仲裁、乱序返回,出了Bug根本查不出来
fpga开发·bug
upper20202 天前
FPGA部署卷积神经网络识别MINST图片
fpga开发·卷积神经网络·minst·minst识别·部署神经网络·神经网络识别图片
你是我的解忧王子3 天前
嵌入式 ADC采样 原始数据分析上层(平均值 波动率 正态分布 线性补偿 系统误差)
fpga开发·数据挖掘·数据分析
9527华安3 天前
FPGA纯verilog代码ISP图像处理培训教程,基于IMX214,提供工程源码+视频教程+FPGA开发板
图像处理·fpga开发·isp·imx214·mipi
千寻xun4 天前
二、实战篇-NVME SSD控制之ZYNQ实现(六)-读写NVME SSD硬盘数据
fpga开发·nvme·nvme ssd
千寻xun4 天前
二、实战篇-NVME SSD控制之ZYNQ实现(三)
fpga开发·nvme ssd·nvme协议