0%

FPGA系统设计之常用串行通信接口

本节主要介绍常用的串行通信接口UART、SPI、IIC时序及其Verilog代码实现。

URAT

  • 异步全双工串行通信协议

  • 所谓异步:发送、接收方使用各自的时钟控制数据的收发

  • 所谓串行:将数据每个bit逐个传输

  • 串行通信的传输方向:

    • 单工:数据只能沿着一个方向传输

      image-20250309150828616
    • 半双工:数据可以沿着两个方向传输,但需要分别进行

      image-20250309150856614
    • 全双工:数据可以同时进行双向传输

      image-20250309150920930
  • 发送方:将并行数据转换为串行数据进行传输

  • 接收方:将串行数据转换为并行数据进行传输

1.时序格式

image-20250309151635179
  • 数据格式:
    • 起始位:标志一帧数据的开始
    • 数据位:一帧数据中的有效数据,可以是5~8个bit
    • 校验位:用于检测数据传输是否出错
      • 奇校验
      • 偶校验
      • 无校验位
    • 停止位:标志一帧数据的结束,可以是1(默认)、1.5、2个bit
  • 串口通信速率:
    • 波特率:每秒钟传输二进制数据的位数,单位bit/s(bps)
    • 常用波特率:9600、19200、38400、57600、115200bit/s

2.设计思路

  • 模式选择:

    • 开始位:1bit
    • 数据位:8bit
    • 校验位:无
    • 停止位:1bit
    • 波特率:115200bit/s
  • 收发设计分析:

    • 由于使用的波特率为115200,那么意味着串口发送或者接收1bit数据的时间为1个波特,即$\frac1{115200}s$
    • 使用FPGA系统时钟周期来计数,若其为50MHz(20ns),那么需要$(1/115200)/(20\times 10^{-9})\approx 434$个系统时钟周期对1bit数据计数。这样我们就需要一个至少为 9 位的波特率计数器来计数,实际使用设为 16 位是为了其他波特率的使用
    • 还需要一个4bit的计数器对总共要发送的10bit数据计数
    • 在系统时钟计数器计数到一半时去采集数据,这时候的数据采集是最稳定的
  • 串口接收模块波形图:

    image-20250309154432478

  • 串口发送模块波形图:

    image-20250309154539709

3.uart串口接收代码设计

  • 结合上述时序图,还是很容易看懂下述代码的。主要是通过两个计数器计数,一个计数器是对每个1bit内部计数(baud_cnt),一个计数器是对10个bit计数(rx_cnt),此外,rx_flag这个标志此时处于接收过程中的标志位还是比较妙的。其次,通过对uart_rxd下降沿的判断(有做两级寄存减少亚稳态),得到一个start_en的开始接收数据标志
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
module uart_rx(
input clk , //系统时钟
input rst_n , //系统复位,低有效

input uart_rxd , //UART接收端口
output reg uart_rx_done, //UART接收完成信号
output reg [7:0] uart_rx_data //UART接收到的数据
);

//parameter define
parameter CLK_FREQ = 50000000; //系统时钟频率
parameter UART_BPS = 115200 ; //串口波特率
localparam BAUD_CNT_MAX = CLK_FREQ/UART_BPS; //为得到指定波特率,对系统时钟计数BPS_CNT次

//reg define
reg uart_rxd_d0;
reg uart_rxd_d1;
reg uart_rxd_d2;
reg rx_flag ; //接收过程标志信号
reg [3:0 ] rx_cnt ; //接收数据计数器
reg [15:0] baud_cnt ; //波特率计数器
reg [7:0 ] rx_data_t ; //接收数据寄存器

//wire define
wire start_en;

//*****************************************************
//** main code
//*****************************************************
//捕获接收端口下降沿(起始位),得到一个时钟周期的脉冲信号
assign start_en = uart_rxd_d2 & (~uart_rxd_d1) & (~rx_flag);

//针对异步信号的同步处理
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
uart_rxd_d0 <= 1'b0;
uart_rxd_d1 <= 1'b0;
uart_rxd_d2 <= 1'b0;
end
else begin
uart_rxd_d0 <= uart_rxd;
uart_rxd_d1 <= uart_rxd_d0;
uart_rxd_d2 <= uart_rxd_d1;
end
end

//给接收标志赋值
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
rx_flag <= 1'b0;
else if(start_en) //检测到起始位
rx_flag <= 1'b1; //接收过程中,标志信号rx_flag拉高
//在停止位一半的时候,即接收过程结束,标志信号rx_flag拉低
else if((rx_cnt == 4'd9) && (baud_cnt == BAUD_CNT_MAX/2 - 1'b1))
rx_flag <= 1'b0;
else
rx_flag <= rx_flag;
end

//波特率的计数器赋值
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
baud_cnt <= 16'd0;
else if(rx_flag) begin //处于接收过程时,波特率计数器(baud_cnt)进行循环计数
if(baud_cnt < BAUD_CNT_MAX - 1'b1)
baud_cnt <= baud_cnt + 16'b1;
else
baud_cnt <= 16'd0; //计数达到一个波特率周期后清零
end
else
baud_cnt <= 16'd0; //接收过程结束时计数器清零
end

//对接收数据计数器(rx_cnt)进行赋值
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
rx_cnt <= 4'd0;
else if(rx_flag) begin //处于接收过程时rx_cnt才进行计数
if(baud_cnt == BAUD_CNT_MAX - 1'b1) //当波特率计数器计数到一个波特率周期时
rx_cnt <= rx_cnt + 1'b1; //接收数据计数器加1
else
rx_cnt <= rx_cnt;
end
else
rx_cnt <= 4'd0; //接收过程结束时计数器清零
end

//根据rx_cnt来寄存rxd端口的数据
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
rx_data_t <= 8'b0;
else if(rx_flag) begin //系统处于接收过程时
if(baud_cnt == BAUD_CNT_MAX/2 - 1'b1) begin //判断baud_cnt是否计数到数据位的中间
case(rx_cnt)
4'd1 : rx_data_t[0] <= uart_rxd_d2; //寄存数据的最低位
4'd2 : rx_data_t[1] <= uart_rxd_d2;
4'd3 : rx_data_t[2] <= uart_rxd_d2;
4'd4 : rx_data_t[3] <= uart_rxd_d2;
4'd5 : rx_data_t[4] <= uart_rxd_d2;
4'd6 : rx_data_t[5] <= uart_rxd_d2;
4'd7 : rx_data_t[6] <= uart_rxd_d2;
4'd8 : rx_data_t[7] <= uart_rxd_d2; //寄存数据的高低位
default : ;
endcase
end
else
rx_data_t <= rx_data_t;
end
else
rx_data_t <= 8'b0;
end

//给接收完成信号和接收到的数据赋值
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
uart_rx_done <= 1'b0;
uart_rx_data <= 8'b0;
end
//当接收数据计数器计数到停止位,且baud_cnt计数到停止位的中间时
else if(rx_cnt == 4'd9 && baud_cnt == BAUD_CNT_MAX/2 - 1'b1) begin
uart_rx_done <= 1'b1 ; //拉高接收完成信号
uart_rx_data <= rx_data_t; //并对UART接收到的数据进行赋值
end
else begin
uart_rx_done <= 1'b0;
uart_rx_data <= uart_rx_data;
end
end

endmodule

4.uart串口发送代码设计

  • 结合上述时序图,还是很容易看懂下述代码的。发送部分是外部信号给一个发送使能标志
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
module uart_tx(
input clk , //系统时钟
input rst_n , //系统复位,低有效
input uart_tx_en , //UART的发送使能
input [7:0] uart_tx_data, //UART要发送的数据
output reg uart_txd , //UART发送端口
output reg uart_tx_busy //发送忙状态信号
);

//parameter define
parameter CLK_FREQ = 50000000; //系统时钟频率
parameter UART_BPS = 115200 ; //串口波特率
localparam BAUD_CNT_MAX = CLK_FREQ/UART_BPS; //为得到指定波特率,对系统时钟计数BPS_CNT次

//reg define
reg [7:0] tx_data_t; //发送数据寄存器
reg [3:0] tx_cnt ; //发送数据计数器
reg [15:0] baud_cnt ; //波特率计数器

//*****************************************************
//** main code
//*****************************************************

//当uart_tx_en为高时,寄存输入的并行数据,并拉高BUSY信号
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
tx_data_t <= 8'b0;
uart_tx_busy <= 1'b0;
end
//发送使能时,寄存要发送的数据,并拉高BUSY信号
else if(uart_tx_en) begin
tx_data_t <= uart_tx_data;
uart_tx_busy <= 1'b1;
end
//当计数到停止位结束时,停止发送过程
else if(tx_cnt == 4'd9 && baud_cnt == BAUD_CNT_MAX - 1) begin
tx_data_t <= 8'b0; //清空发送数据寄存器
uart_tx_busy <= 1'b0; //并拉低BUSY信号
end
else begin
tx_data_t <= tx_data_t;
uart_tx_busy <= uart_tx_busy;
end
end

//波特率的计数器赋值
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
baud_cnt <= 16'd0;
else if(uart_tx_en)
baud_cnt <= 16'd0;
//当处于发送过程时,波特率计数器(baud_cnt)进行循环计数
else if(uart_tx_busy) begin
if(baud_cnt < BAUD_CNT_MAX - 1'b1)
baud_cnt <= baud_cnt + 16'b1;
else
baud_cnt <= 16'd0; //计数达到一个波特率周期后清零
end
else
baud_cnt <= 16'd0; //发送过程结束时计数器清零
end

//tx_cnt进行赋值
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
tx_cnt <= 4'd0;
else if(uart_tx_en)
tx_cnt <= 16'd0;
else if(uart_tx_busy) begin //处于发送过程时tx_cnt才进行计数
if(baud_cnt == BAUD_CNT_MAX - 1'b1) //当波特率计数器计数到一个波特率周期时
tx_cnt <= tx_cnt + 1'b1; //发送数据计数器加1
else
tx_cnt <= tx_cnt;
end
else
tx_cnt <= 4'd0; //发送过程结束时计数器清零
end

//根据tx_cnt来给uart发送端口赋值
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
uart_txd <= 1'b1;
else if(uart_tx_busy) begin
case(tx_cnt)
4'd0 : uart_txd <= 1'b0 ; //起始位
4'd1 : uart_txd <= tx_data_t[0]; //数据位最低位
4'd2 : uart_txd <= tx_data_t[1];
4'd3 : uart_txd <= tx_data_t[2];
4'd4 : uart_txd <= tx_data_t[3];
4'd5 : uart_txd <= tx_data_t[4];
4'd6 : uart_txd <= tx_data_t[5];
4'd7 : uart_txd <= tx_data_t[6];
4'd8 : uart_txd <= tx_data_t[7]; //数据位最高位
4'd9 : uart_txd <= 1'b1 ; //停止位
default : uart_txd <= 1'b1;
endcase
end
else
uart_txd <= 1'b1; //空闲时发送端口为高电平
end

endmodule

5.uart串口通信回环实验

  • 上位机通过串口调试助手发送数据给启明星开发板, 启明星开发板 PL 端通过USB_UART 串口接收数据并将接收到的数据发送给上位机,完成串口数据环回

  • 系统框图:

    image-20250309163717252
  • 顶层模块:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    module uart_loopback(
    input sys_clk , //外部50MHz时钟
    input sys_rst_n, //系外部复位信号,低有效

    //UART端口
    input uart_rxd , //UART接收端口
    output uart_txd //UART发送端口
    );

    //parameter define
    parameter CLK_FREQ = 50000000; //定义系统时钟频率
    parameter UART_BPS = 115200 ; //定义串口波特率

    //wire define
    wire uart_rx_done; //UART接收完成信号
    wire [7:0] uart_rx_data; //UART接收数据

    //*****************************************************
    //** main code
    //*****************************************************

    //串口接收模块
    uart_rx #(
    .CLK_FREQ (CLK_FREQ),
    .UART_BPS (UART_BPS)
    )
    u_uart_rx(
    .clk (sys_clk ),
    .rst_n (sys_rst_n ),
    .uart_rxd (uart_rxd ),
    .uart_rx_done (uart_rx_done),
    .uart_rx_data (uart_rx_data)
    );

    //串口发送模块
    uart_tx #(
    .CLK_FREQ (CLK_FREQ),
    .UART_BPS (UART_BPS)
    )
    u_uart_tx(
    .clk (sys_clk ),
    .rst_n (sys_rst_n ),
    .uart_tx_en (uart_rx_done),
    .uart_tx_data (uart_rx_data),
    .uart_txd (uart_txd ),
    .uart_tx_busy ( )
    );

    endmodule
  • 上板验证结果:

    image-20250309170521701

SPI

  • 串行外围设备接口,是高速(相比于UART、IIC)、全双工、同步通信总线
  • 应用:EEPROM、FLASH、ADC、DSP、数字信号解码器
  • 优点:全双工、通讯简单,数据传输速率快,灵活的数据传输方式,不限于8位,可以是任意大小的字
  • 缺点:没有指定的流控制、无应答机制、数据可靠性上有一定的缺陷

1.时序格式

  • 通讯模式:主从通讯模式,通讯双方有主从之分,根据从机设备的数量,SPI通讯设备之间的连接方式可分为一主一从和一主多从

    image-20250309174845480
  • 四根数据线作用:

    • SCK:时钟信号线,由主机产生,决定通信速率
    • MOSI(Master Output Slave Input):主机发送数据 or 从机接收数据的线
    • MISO(Master Input Slave Output):主机接收数据 or 从机发送数据的线
    • CS:片选信号,低电平表示选中
  • 工作模式:CPOL、CPHA决定有四种工作模式

    • 时钟极性:
      • CPOL=0:SPI总线空闲时SCK=0
      • CPOL=1:SPI总线空闲时SCK=1
    • 时钟相位:
      • CPHA=0:SCK第一个跳变沿采样
      • CPHA=1:SCK第二个跳变沿采样
    image-20250309180026123
    • 模式0:CPOL=0,CPHA=0。SCK串行时钟线空闲是为低电平,数据在SCK时钟的上升沿被采样,数据在SCK时钟的下降沿切换
    • 模式1:CPOL=0,CPHA=1。SCK串行时钟线空闲是为低电平,数据在SCK时钟的下降沿被采样,数据在SCK时钟的上升沿切换
    • 模式2:CPOL=1,CPHA=0。SCK串行时钟线空闲是为高电平,数据在SCK时钟的下降沿被采样,数据在SCK时钟的上升沿切换
    • 模式3:CPOL=1,CPHA=1。SCK串行时钟线空闲是为高电平,数据在SCK时钟的上升沿被采样,数据在SCK时钟的下降沿切换
  • SPI基本的通讯过程,以模式0为例:

    image-20250309180933950

2.设计思路

  • 通过对主机的系统时钟分频,得到SCK,并通过计数器判断SCK的上升沿和下降沿
  • 设置一个发送计数器控制发送的bit位,同理,设置一个接收计数器控制接收的bit位

3.SPI driver设计代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
`timescale 1ns/1ns		//时间单位/精度	
// 模式0
module spi_drive
(
// 系统接口
input sys_clk , // 全局时钟50MHz
input sys_rst_n , // 复位信号,低电平有效
// 用户接口
input spi_start , // 发送传输开始信号,一个高电平
input spi_end , // 发送传输结束信号,一个高电平
input [7:0] data_send , // 要发送的数据
output reg [7:0] data_rec , // 接收到的数据
output reg send_done , // 主机发送一个字节完毕标志位
output reg rec_done , // 主机接收一个字节完毕标志位
// SPI物理接口
input spi_miso , // SPI串行输入,用来接收从机的数据
output reg spi_sclk , // SPI时钟
output reg spi_cs , // SPI片选信号,低电平有效
output reg spi_mosi // SPI输出,用来给从机发送数据
);

reg [1:0] cnt; //4分频计数器
reg [3:0] bit_cnt_send; //发送计数器
reg [3:0] bit_cnt_rec; //接收计数器
reg spi_end_req; //结束请求

//4分频计数器
always @(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
cnt <= 2'd0;
else if(!spi_cs)begin
if(cnt == 2'd3)
cnt <= 2'd0;
else
cnt <= cnt + 1'b1;
end
else
cnt <= 2'd0;
end
// 生成spi_sclk时钟
always @(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
spi_sclk <= 1'b0; //模式0默认为低电平
else if(!spi_cs)begin //在SPI传输过程中
if(cnt == 2'd0 )
spi_sclk <= 1'b0;
else if (cnt == 2'd2)
spi_sclk <= 1'b1;
else
spi_sclk <= spi_sclk;
end
else
spi_sclk <= 1'b0; //模式0默认为低电平
end
// 生成片选信号spi_cs
always @(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
spi_cs <= 1'b1; //默认为高电平
else if(spi_start) //开始SPI准备传输,拉低片选信号
spi_cs <= 1'b0;
//收到了SPI结束信号,且结束了最近的一个BYTE
else if(spi_end_req && (cnt == 2'd1 && bit_cnt_rec == 4'd0))
spi_cs <= 1'b1; //拉高片选信号,结束SPI传输
end
// 生成结束请求信号(捕捉spi_end信号)
always @(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
spi_end_req <= 1'b0; //默认不使能
else if(spi_cs)
spi_end_req <= 1'b0; //结束SPI传输后拉低请求
else if(spi_end)
spi_end_req <= 1'b1; //接收到SPI结束信号后就把结束请求拉高
end
// 发送数据过程--------------------------------------------------------------------

// 发送数据
always @(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)begin
spi_mosi <= 1'b0; //模式0空闲
bit_cnt_send <= 4'd0;
end
else if(cnt == 2'd0 && !spi_cs)begin //模式0的上升沿
spi_mosi <= data_send[7-bit_cnt_send]; //发送数据移位
if(bit_cnt_send == 4'd7) //发送完8bit
bit_cnt_send <= 4'd0;
else
bit_cnt_send <= bit_cnt_send + 1'b1;
end
else if(spi_cs)begin //非传输时间段
spi_mosi <= 1'b0; //模式0空闲
bit_cnt_send <= 4'd0;
end
else begin
spi_mosi <= spi_mosi;
bit_cnt_send <= bit_cnt_send;
end
end
// 发送数据标志
always @(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
send_done <= 1'b0;
else if(cnt == 2'd0 && bit_cnt_send == 4'd7) //发送完了8bit数据
send_done <= 1'b1; //拉高一个周期,表示发送完成
else
send_done <= 1'b0;
end

// 接收数据过程--------------------------------------------------------------------

// 接收数据spi_miso
always @(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)begin
data_rec <= 8'd0;
bit_cnt_rec <= 4'd0;
end
else if(cnt == 2'd2 && !spi_cs)begin //模式0的上升沿
data_rec[7-bit_cnt_rec] <= spi_miso; //移位接收
if(bit_cnt_rec == 4'd7) //接收完了8bit
bit_cnt_rec <= 4'd0;
else
bit_cnt_rec <= bit_cnt_rec + 1'b1;
end
else if(spi_cs)begin
bit_cnt_rec <= 4'd0;
end
else begin
data_rec <= data_rec;
bit_cnt_rec <= bit_cnt_rec;
end
end
// 接收数据标志
always @(posedge sys_clk or negedge sys_rst_n)begin
if(!sys_rst_n)
rec_done <= 1'b0;
else if(cnt == 2'd2 && bit_cnt_rec == 4'd7) //接收完了8bit
rec_done <= 1'b1; //拉高一个周期,表示接收完成
else
rec_done <= 1'b0;
end

endmodule
  • 仿真结果:

    image-20250309211942611


IIC

  • 两线式串行总线+半双工同步通讯方式

  • 用于主机和从机在数据量不大,传输距离比较短的场合

  • 物理层的基本描述:

    • 从机设备上的SDA都接到总线的SDA上
    • 从机设备上的SCL都接到总线的SCL上
    • 从机设备都有唯一的设备地址
    • SCL和SDA都需要上拉电阻(3.3K~10K)
    • 两条总线:
      • SCL(Serial Clock Line)控制数据接收时序的时钟线
      • SDA(Serial Data)传输数据的线
    image-20250309213526404

1.时序格式

  • 空闲状态:SCL、SDA皆为高电平(上拉电阻拉高)

  • 开始信号:对应SCL=1时,SDA从0变1

  • 停止信号:对应SCL=1时,SDA由1变0

    image-20250309213645301
  • 数据传输时:

    • SCL=1时,SDA保持稳定
    • SCL=0时,SDA可以变化
  • 应答信号:发送完8个bit后,主机释放SDA以使从机发送是否应答信号

    • 从机拉低SDA第9个bit,代表接收成功
    • 从机拉高SDA第9个bit,代表接收失败
    image-20250309214255314
  • IIC器件地址

    • 每个 I2C 器件都有一个器件地址,有些 I2C 器件的器件地址是固定的,而有些 I2C 器件的器件地址由一个固定部分和一个可编程的部分构成,这是因为很可能在一个系统中有几个同样的器件,器件地址的可编程部分能最大数量的使这些器件连接到 I2C 总线上,例如 E2PROM 器件,为了增加系统的 E2PROM 容量,可能需要多个 E2PROM。

    • 器件可编程地址位的数量由它可使用的管脚决定,比如 E2PROM 器件一般会留下 3 个管脚用于可编程地址位。

    • 但有些 I2C 器件在出厂时器件地址就设置好了,用户不可以更改(如实时时钟 PCF8563 的器件地址为固定的 7’h51)。

    • 所以当主机想给某个器件发送数据时,只需向总线上发送接收器件的器件地址即可

    • 进行数据传输时:

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

      image-20250309222131604
  • 存储地址

    • 一般而言,每个兼容 I2C 协议的器件,内部总会有可供读写的寄存器或存储器
    • 当我们对一个器件中的存储单元(包括寄存器)进行读写时,首先要指定存储单元的地址即字地址,然后再向该地址写入或读出内容
    • 该地址为一个或两个字节长度,具体长度由器件内部的存储单元的数量决定
    image-20250309222831876
  • 写时序:(要注意的是, 所有 I2C 设备均支持单字节数据写入操作,但只有部分 I2C 设备支持页写操作,对于AT24C64 的页写,是不能发送超过一页的单元容量的数据的,而 AT24C64 的一页的单元容量为 32Byte,当写完一页的最后一个单元时,地址指针指向该页的开头,如果再写入数据,就会覆盖该页的起始数据 )

    • 单次写

      image-20250309223155209
    • 连续写

      image-20250309223222552
  • 读时序

    • 当前地址读

      image-20250309223731605
    • 随机读

      image-20250309230656096
      • 随机地址读在发送完器件地址和字地址后,竟然又发送起始信号和器件地址,而且第一次发送器件地址时后面的读写控制位为“ 0”,也就是写命令,第二次发送器件地址时后面的读写控制位为“1”,也就是读。为什么会有这样奇怪的操作呢?
      • 这是因为我们需要使从机内的存储单元地址指针指向我们想要读取的存储单元地址处,所以首先发送了一次Dummy Write 也就是虚写操作,只所以称为虚写,是因为我们并不是真的要写数据,而是通过这种虚写操作使地址指针指向虚写操作中字地址的位置
      • 等从机应答后,就可以以当前地址读的方式读数据了
      • 随机地址读是没有发送数据的单次写操作和当前地址读操作的结合体。
    • 连续读:当前地址读和随机读都是一次读取一个字节,连续读是将当前地址读或随机读的主机非应答改成应答,表示继续读取数据

      • 当前地址读下的连续读
      image-20250309232117019
      • 随机读下的连续读
      image-20250309232429673
  • IIC读写操作总结:

    image-20250309232918663

2.设计思路

  • 很明显,读写操作均有先后顺序,故采用状态机实现。大致为:写物理地址->写字地址->写操作/读操作,若为读操作,则在读数据之前,还需写一次物理地址+1(改为读命令)

    image-20250310111418790
  • 状态机的驱动时钟dri_clk必须是scl的4倍以上的频率时,才能正确产生I2C起始信号和停止信号,dri_clk可由FPGA系统时钟分频而来

    image-20250310112128449

3.IIC driver设计代码

  • 结合上述状态机,纯看代码还是比较容易看懂,如果要我自己从0开始写,那也是个大工程,得先把计数器(cnt)控制的时序一点点画出来,再对着写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
module i2c_dri
#(
parameter SLAVE_ADDR = 7'b1010000 , //EEPROM从机地址
parameter CLK_FREQ = 26'd50_000_000, //模块输入的时钟频率
parameter I2C_FREQ = 18'd250_000 //IIC_SCL的时钟频率
)
(
input clk ,
input rst_n ,

//i2c interface
input i2c_exec , //I2C触发执行信号
input bit_ctrl , //字地址位控制(16b/8b)
input i2c_rh_wl , //I2C读写控制信号
input [15:0] i2c_addr , //I2C器件内地址
input [ 7:0] i2c_data_w , //I2C要写的数据
output reg [ 7:0] i2c_data_r , //I2C读出的数据
output reg i2c_done , //I2C一次操作完成
output reg i2c_ack , //I2C应答标志 0:应答 1:未应答
output reg scl , //I2C的SCL时钟信号
inout sda , //I2C的SDA信号

//user interface
output reg dri_clk //驱动I2C操作的驱动时钟
);

//localparam define
localparam st_idle = 8'b0000_0001; //空闲状态
localparam st_sladdr = 8'b0000_0010; //发送器件地址(slave address)
localparam st_addr16 = 8'b0000_0100; //发送16位字地址
localparam st_addr8 = 8'b0000_1000; //发送8位字地址
localparam st_data_wr = 8'b0001_0000; //写数据(8 bit)
localparam st_addr_rd = 8'b0010_0000; //发送器件地址读
localparam st_data_rd = 8'b0100_0000; //读数据(8 bit)
localparam st_stop = 8'b1000_0000; //结束I2C操作

//reg define
reg sda_dir ; //I2C数据(SDA)方向控制
reg sda_out ; //SDA输出信号
reg st_done ; //状态结束
reg wr_flag ; //写标志
reg [ 6:0] cnt ; //计数
reg [ 7:0] cur_state ; //状态机当前状态
reg [ 7:0] next_state; //状态机下一状态
reg [15:0] addr_t ; //地址
reg [ 7:0] data_r ; //读取的数据
reg [ 7:0] data_wr_t ; //I2C需写的数据的临时寄存
reg [ 9:0] clk_cnt ; //分频时钟计数

//wire define
wire sda_in ; //SDA输入信号
wire [8:0] clk_divide ; //模块驱动时钟的分频系数

//*****************************************************
//** main code
//*****************************************************

//SDA控制
assign sda = sda_dir ? sda_out : 1'bz ; //SDA数据输出或高阻
assign sda_in = sda ; //SDA数据输入
assign clk_divide = (CLK_FREQ/I2C_FREQ) >> 2'd2 ; //模块驱动时钟的分频系数

//生成I2C的SCL的四倍频率的驱动时钟用于驱动i2c的操作
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
dri_clk <= 1'b0;
clk_cnt <= 10'd0;
end
else if(clk_cnt == (clk_divide[8:1] - 9'd1)) begin
clk_cnt <= 10'd0;
dri_clk <= ~dri_clk;
end
else
clk_cnt <= clk_cnt + 10'b1;
end

//(三段式状态机)同步时序描述状态转移
always @(posedge dri_clk or negedge rst_n) begin
if(!rst_n)
cur_state <= st_idle;
else
cur_state <= next_state;
end

//组合逻辑判断状态转移条件
always @(*) begin
next_state = st_idle;
case(cur_state)
st_idle: begin //空闲状态
if(i2c_exec) begin
next_state = st_sladdr;
end
else
next_state = st_idle;
end
st_sladdr: begin
if(st_done) begin
if(bit_ctrl) //判断是16位还是8位字地址
next_state = st_addr16;
else
next_state = st_addr8 ;
end
else
next_state = st_sladdr;
end
st_addr16: begin //写16位字地址
if(st_done) begin
next_state = st_addr8;
end
else begin
next_state = st_addr16;
end
end
st_addr8: begin //8位字地址
if(st_done) begin
if(wr_flag==1'b0) //读写判断
next_state = st_data_wr;
else
next_state = st_addr_rd;
end
else begin
next_state = st_addr8;
end
end
st_data_wr: begin //写数据(8 bit)
if(st_done)
next_state = st_stop;
else
next_state = st_data_wr;
end
st_addr_rd: begin //写地址以进行读数据
if(st_done) begin
next_state = st_data_rd;
end
else begin
next_state = st_addr_rd;
end
end
st_data_rd: begin //读取数据(8 bit)
if(st_done)
next_state = st_stop;
else
next_state = st_data_rd;
end
st_stop: begin //结束I2C操作
if(st_done)
next_state = st_idle;
else
next_state = st_stop ;
end
default: next_state= st_idle;
endcase
end

//时序电路描述状态输出
always @(posedge dri_clk or negedge rst_n) begin
//复位初始化
if(!rst_n) begin
scl <= 1'b1;
sda_out <= 1'b1;
sda_dir <= 1'b1;
i2c_done <= 1'b0;
i2c_ack <= 1'b0;
cnt <= 7'b0;
st_done <= 1'b0;
data_r <= 8'b0;
i2c_data_r<= 8'b0;
wr_flag <= 1'b0;
addr_t <= 16'b0;
data_wr_t <= 8'b0;
end
else begin
st_done <= 1'b0 ;
cnt <= cnt +7'b1 ;
case(cur_state)
st_idle: begin //空闲状态
scl <= 1'b1;
sda_out <= 1'b1;
sda_dir <= 1'b1;
i2c_done<= 1'b0;
cnt <= 7'b0;
if(i2c_exec) begin
wr_flag <= i2c_rh_wl ;
addr_t <= i2c_addr ;
data_wr_t <= i2c_data_w;
i2c_ack <= 1'b0;
end
end
st_sladdr: begin //写地址(器件地址和字地址)
case(cnt)
7'd1 : sda_out <= 1'b0; //开始I2C
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= SLAVE_ADDR[6]; //传送器件地址
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= SLAVE_ADDR[5];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= SLAVE_ADDR[4];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= SLAVE_ADDR[3];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= SLAVE_ADDR[2];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= SLAVE_ADDR[1];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= SLAVE_ADDR[0];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: sda_out <= 1'b0; //0:写
7'd33: scl <= 1'b1;
7'd35: scl <= 1'b0;
7'd36: begin
sda_dir <= 1'b0;
sda_out <= 1'b1;
end
7'd37: scl <= 1'b1;
7'd38: begin //从机应答
st_done <= 1'b1;
if(sda_in == 1'b1) //高电平表示未应答
i2c_ack <= 1'b1; //拉高应答标志位
end
7'd39: begin
scl <= 1'b0;
cnt <= 7'b0;
end
default : ;
endcase
end
st_addr16: begin
case(cnt)
7'd0 : begin
sda_dir <= 1'b1 ;
sda_out <= addr_t[15]; //传送字地址
end
7'd1 : scl <= 1'b1;
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= addr_t[14];
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= addr_t[13];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= addr_t[12];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= addr_t[11];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= addr_t[10];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= addr_t[9];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= addr_t[8];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: begin
sda_dir <= 1'b0;
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd34: begin //从机应答
st_done <= 1'b1;
if(sda_in == 1'b1) //高电平表示未应答
i2c_ack <= 1'b1; //拉高应答标志位
end
7'd35: begin
scl <= 1'b0;
cnt <= 7'b0;
end
default : ;
endcase
end
st_addr8: begin
case(cnt)
7'd0: begin
sda_dir <= 1'b1 ;
sda_out <= addr_t[7]; //字地址
end
7'd1 : scl <= 1'b1;
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= addr_t[6];
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= addr_t[5];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= addr_t[4];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= addr_t[3];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= addr_t[2];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= addr_t[1];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= addr_t[0];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: begin
sda_dir <= 1'b0;
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd34: begin //从机应答
st_done <= 1'b1;
if(sda_in == 1'b1) //高电平表示未应答
i2c_ack <= 1'b1; //拉高应答标志位
end
7'd35: begin
scl <= 1'b0;
cnt <= 7'b0;
end
default : ;
endcase
end
st_data_wr: begin //写数据(8 bit)
case(cnt)
7'd0: begin
sda_dir <= 1'b1;
sda_out <= data_wr_t[7]; //I2C写8位数据
end
7'd1 : scl <= 1'b1;
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= data_wr_t[6];
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= data_wr_t[5];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= data_wr_t[4];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= data_wr_t[3];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= data_wr_t[2];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= data_wr_t[1];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= data_wr_t[0];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: begin
sda_dir <= 1'b0;
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd34: begin //从机应答
st_done <= 1'b1;
if(sda_in == 1'b1) //高电平表示未应答
i2c_ack <= 1'b1; //拉高应答标志位
end
7'd35: begin
scl <= 1'b0;
cnt <= 7'b0;
end
default : ;
endcase
end
st_addr_rd: begin //写地址以进行读数据
case(cnt)
7'd0 : begin
sda_dir <= 1'b1;
sda_out <= 1'b1;
end
7'd1 : scl <= 1'b1;
7'd2 : sda_out <= 1'b0; //重新开始
7'd3 : scl <= 1'b0;
7'd4 : sda_out <= SLAVE_ADDR[6]; //传送器件地址
7'd5 : scl <= 1'b1;
7'd7 : scl <= 1'b0;
7'd8 : sda_out <= SLAVE_ADDR[5];
7'd9 : scl <= 1'b1;
7'd11: scl <= 1'b0;
7'd12: sda_out <= SLAVE_ADDR[4];
7'd13: scl <= 1'b1;
7'd15: scl <= 1'b0;
7'd16: sda_out <= SLAVE_ADDR[3];
7'd17: scl <= 1'b1;
7'd19: scl <= 1'b0;
7'd20: sda_out <= SLAVE_ADDR[2];
7'd21: scl <= 1'b1;
7'd23: scl <= 1'b0;
7'd24: sda_out <= SLAVE_ADDR[1];
7'd25: scl <= 1'b1;
7'd27: scl <= 1'b0;
7'd28: sda_out <= SLAVE_ADDR[0];
7'd29: scl <= 1'b1;
7'd31: scl <= 1'b0;
7'd32: sda_out <= 1'b1; //1:读
7'd33: scl <= 1'b1;
7'd35: scl <= 1'b0;
7'd36: begin
sda_dir <= 1'b0;
sda_out <= 1'b1;
end
7'd37: scl <= 1'b1;
7'd38: begin //从机应答
st_done <= 1'b1;
if(sda_in == 1'b1) //高电平表示未应答
i2c_ack <= 1'b1; //拉高应答标志位
end
7'd39: begin
scl <= 1'b0;
cnt <= 7'b0;
end
default : ;
endcase
end
st_data_rd: begin //读取数据(8 bit)
case(cnt)
7'd0: sda_dir <= 1'b0;
7'd1: begin
data_r[7] <= sda_in;
scl <= 1'b1;
end
7'd3: scl <= 1'b0;
7'd5: begin
data_r[6] <= sda_in ;
scl <= 1'b1 ;
end
7'd7: scl <= 1'b0;
7'd9: begin
data_r[5] <= sda_in;
scl <= 1'b1 ;
end
7'd11: scl <= 1'b0;
7'd13: begin
data_r[4] <= sda_in;
scl <= 1'b1 ;
end
7'd15: scl <= 1'b0;
7'd17: begin
data_r[3] <= sda_in;
scl <= 1'b1 ;
end
7'd19: scl <= 1'b0;
7'd21: begin
data_r[2] <= sda_in;
scl <= 1'b1 ;
end
7'd23: scl <= 1'b0;
7'd25: begin
data_r[1] <= sda_in;
scl <= 1'b1 ;
end
7'd27: scl <= 1'b0;
7'd29: begin
data_r[0] <= sda_in;
scl <= 1'b1 ;
end
7'd31: scl <= 1'b0;
7'd32: begin
sda_dir <= 1'b1;
sda_out <= 1'b1;
end
7'd33: scl <= 1'b1;
7'd34: st_done <= 1'b1; //非应答
7'd35: begin
scl <= 1'b0;
cnt <= 7'b0;
i2c_data_r <= data_r;
end
default : ;
endcase
end
st_stop: begin //结束I2C操作
case(cnt)
7'd0: begin
sda_dir <= 1'b1; //结束I2C
sda_out <= 1'b0;
end
7'd1 : scl <= 1'b1;
7'd3 : sda_out <= 1'b1;
7'd15: st_done <= 1'b1;
7'd16: begin
cnt <= 7'b0;
i2c_done <= 1'b1; //向上层模块传递I2C结束信号
end
default : ;
endcase
end
endcase
end
end

endmodule

4.基于IIC协议的EEPROM读写测试

  • 本节的实验任务是先向 E2PROM(AT24C64)的存储器地址 0 至 255 分别写入数据 0255;写完之后再读取存储器地址 0255 中的数据,若读取的值全部正确则 LED 灯常亮,否则 LED 灯闪烁

  • 系统框图:

    image-20250310112831959
  • 顶层模块:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    module top_e2prom(
    input sys_clk , //系统时钟
    input sys_rst_n , //系统复位
    //eeprom interface
    output iic_scl , //eeprom的时钟线scl
    inout iic_sda , //eeprom的数据线sda
    //user interface
    output led //led显示eeprom读写测试结果
    );

    //parameter define
    parameter SLAVE_ADDR = 7'b1010000 ; //器件地址(SLAVE_ADDR)
    parameter BIT_CTRL = 1'b1 ; //字地址位控制参数(16b/8b)
    parameter CLK_FREQ = 26'd50_000_000 ; //i2c_dri模块的驱动时钟频率(CLK_FREQ)
    parameter I2C_FREQ = 18'd250_000 ; //I2C的SCL时钟频率
    parameter L_TIME = 17'd125_000 ; //led闪烁时间参数
    parameter MAX_BYTE = 16'd256 ; //读写测试的字节个数

    //wire define
    wire dri_clk ; //I2C操作时钟
    wire i2c_exec ; //I2C触发控制
    wire [15:0] i2c_addr ; //I2C操作地址
    wire [ 7:0] i2c_data_w; //I2C写入的数据
    wire i2c_done ; //I2C操作结束标志
    wire i2c_ack ; //I2C应答标志 0:应答 1:未应答
    wire i2c_rh_wl ; //I2C读写控制
    wire [ 7:0] i2c_data_r; //I2C读出的数据
    wire rw_done ; //E2PROM读写测试完成
    wire rw_result ; //E2PROM读写测试结果 0:失败 1:成功

    //*****************************************************
    //** main code
    //*****************************************************

    //e2prom读写测试模块
    e2prom_rw #(
    .MAX_BYTE (MAX_BYTE ) //读写测试的字节个数
    ) u_e2prom_rw(
    .clk (dri_clk ), //时钟信号
    .rst_n (sys_rst_n ), //复位信号
    //i2c interface
    .i2c_exec (i2c_exec ), //I2C触发执行信号
    .i2c_rh_wl (i2c_rh_wl ), //I2C读写控制信号
    .i2c_addr (i2c_addr ), //I2C器件内地址
    .i2c_data_w (i2c_data_w), //I2C要写的数据
    .i2c_data_r (i2c_data_r), //I2C读出的数据
    .i2c_done (i2c_done ), //I2C一次操作完成
    .i2c_ack (i2c_ack ), //I2C应答标志
    //user interface
    .rw_done (rw_done ), //E2PROM读写测试完成
    .rw_result (rw_result ) //E2PROM读写测试结果 0:失败 1:成功
    );

    //i2c驱动模块
    i2c_dri #(
    .SLAVE_ADDR (SLAVE_ADDR), //EEPROM从机地址
    .CLK_FREQ (CLK_FREQ ), //模块输入的时钟频率
    .I2C_FREQ (I2C_FREQ ) //IIC_SCL的时钟频率
    ) u_i2c_dri(
    .clk (sys_clk ),
    .rst_n (sys_rst_n ),
    //i2c interface
    .i2c_exec (i2c_exec ), //I2C触发执行信号
    .bit_ctrl (BIT_CTRL ), //器件地址位控制(16b/8b)
    .i2c_rh_wl (i2c_rh_wl ), //I2C读写控制信号
    .i2c_addr (i2c_addr ), //I2C器件内地址
    .i2c_data_w (i2c_data_w), //I2C要写的数据
    .i2c_data_r (i2c_data_r), //I2C读出的数据
    .i2c_done (i2c_done ), //I2C一次操作完成
    .i2c_ack (i2c_ack ), //I2C应答标志
    .scl (iic_scl ), //I2C的SCL时钟信号
    .sda (iic_sda ), //I2C的SDA信号
    //user interface
    .dri_clk (dri_clk ) //I2C操作时钟
    );

    //led指示模块
    rw_result_led #(.L_TIME(L_TIME ) //控制led闪烁时间
    ) u_rw_result_led(
    .clk (dri_clk ),
    .rst_n (sys_rst_n ),

    .rw_done (rw_done ),
    .rw_result (rw_result ),
    .led (led )
    );

    endmodule
  • 上板验证结果:

    image-20250310113544931

Reference

欢迎来到ssy的世界