协议------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.前言
· 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教程