目录
本文是根据B站小梅哥FPGA设计编写。
一、I2C协议解析
I2C由两条总线构成:SDA(串行数据总线)和SCL(串行时钟总线),属于半双工,任意时刻只能有一个主机。(总线上一主多从)
1.I2C协议基本规定
(1)在时钟(SCL)为高电平的时候,数据总线(SDA)必须保持稳定,所以数据总线(SDA)
在时钟(SCL)为低电平的时候才能改变。
(2)在时钟(SCL)为高电平的时候,数据总线(SDA)由高到低的跳变为总线起始信号,
在时钟(SCL)为高电平的时候,数据总线(SDA)由低到高的跳变为总线停止信号。
(3)应答,当 IIC 主机(不一定是发送端还是接收端)将 8 位数据或命令传出后,会将数据
总线(SDA)释放,即设置为输入,然后等待从机应答(低电平 0 表示应答,1 表示非
应答),此时的时钟仍然是主机提供的。
2.I2C写流程
提醒:器件地址:有的器件地址在出厂时地址就设置好了,用户不可以更改(例如 OV7670 器件地址为固定的 0x42),有的确定了几位,剩下几位由硬件确定(比如常见的 I2C 接口的EEPROM 存储器,留有 3 个控地址的引脚,由用户自己在硬件设计时确定)。
存储器地址:相当于器件里的存储单元,例如,EEPROM 存储器,内部就是顺序编址的一系列存储单元;型号为 OV7670 的 CMOS 摄像头(OV7670 的该接口叫 SCCB 接口,其实质也是一种特殊的 I2C 协议,可以直接兼容 I2C协议),其内部就是一系列编址的可供读写的寄存器。
(1)主机将SDA设置为输出:发起起始信号;
(2)主机传输器件地址字节,其中最低位为 0,表明为写操作;
(3)主机设置 SDA 为三态门输入,读取从机应答信号;
(4)读取应答信号成功,主机设置 SDA 为输出,传输 1 字节地址数据;
(5)主机设置 SDA 为三态门输入,读取从机应答信号;
(6)读取应答信号成功,主机设置 SDA 为输出,传输待写入的数据;
(7)设置 SDA 为三态门输入,读取从机应答信号;
(8)读取应答信号成功,主机产生 STOP 位,终止传输。
2字节存储器地址流程差不多不过多赘述
3.I2C读流程
(1)主机设置 SDA 为输出:主机发起起始信号;
(2)主机传输器件地址字节,其中最低位为 0,表明为写操作;
(3)主机设置 SDA 为三态门输入,读取从机应答信号;
(4)读取应答信号成功,主机设置 SDA 为输出,传输 1 字节地址数据;
(5)主机设置 SDA 为三态门输入,读取从机应答信号;
(6)读取应答信号成功,主机发起起始信号;
(7)主机传输器件地址字节,其中最低位为 1,表明为读操作;
(8)设置 SDA 为三态门输入,读取从机应答信号;
(9)读取应答信号成功,主机设置 SDA 为三态门输入,读取 SDA 总线上的一个字节的
数据;
(10)产生无应答信号(高电平)(无需设置为输出高电平,因为总线会被自动拉高);
(11)主机产生 STOP 位,终止传输。
2字节存储器地址流程差不多不过多赘述
二、 I2C 控制器实现思路解析
无论是读or写,每个步骤可以归结为以上五个步骤:
写2字节操作:①(写器件地址,最后一位为0)->②(写存储器地址)->②(写第一字节数据)->③(写第二字节数据并停止)
读一字节操作:①(写器件地址,最后一位为0)->②(写存储器地址)->①写器件地址,最后一位为1)->⑤(读数据并停止)
其他情况皆适用,这里只例举两个例子。
这一思想到目前你可能无法理解怎么用,当学到本章第四小节I2C顶层控制模块便可理解了。
三、I2C传输逻辑模块: i2c_bit_shift
1.接口设计
module i2c_bit_shift(
clk,
rst_n,
cmd,
go,
rx_data,
tx_data,
trans_done,
ack_o,
i2c_sclk,
i2c_sdat
);
input clk;//模块工作时钟,50M 时钟
input rst_n;//复位,低电平有效
input [5:0] cmd;//控制总线实现各种传输操作的各种命令的组合
input go;//整个模块的启动使能信号
output reg [7:0] rx_data;//总线收到的 8 位数据,读操作时读到的数据由此端口输出
input [7:0] tx_data;//总线要发送的 8 位数据,需要传输的数据经此端口传入该模块
output reg trans_done;//发送或接收 8 位数据完成标志信号,高标志位
output reg ack_o;//从机是否应答标志
output reg i2c_sclk;//i2c 时钟总线
inout i2c_sdat;//i2c 数据总线
2.SCL时钟设计
我们根据 I2C 的协议标准,这里采用的是快速模式(400kb/s)来作为总线的工作时钟,所以我们将 SYS_CLOCK 设置成 50_000_000,SCL_CLOCK 是用来配置时钟(SCL)总线的频率,通过这两个参数就可以计算出参数 SCL_CNT_M 要计数到多少就能产生期望的总线工作时钟(SCL)频率
每个状态的处理都可以分为四步(如果这里无法理解,接下来状态机代码设计里便可理解了)
//系统时钟采用 50MHz
parameter SYS_CLOCK =50_000_000;
//SCL 总线时钟采用 400kHz
parameter SCL_CLOCK =400_000;
//产生时钟 SCL 计数器最大值
localparam SCL_CNT_M = SYS_CLOCK/SCL_CLOCK/4-1;
reg i2c_sdat_o;//发送数据电平,1:z(因为总线有上拉电阻,高阻态就是拉高);0:0。
reg i2c_sdat_oe;//高电平SDA为输出模式,低电平为输入模式
//得到每个时刻的脉冲信号sclk_plus,作为序列使能信号
reg [19:0]div_cnt;
reg en_div_cnt;//只在数据传输过程中计数,只在状态机中赋值
always@(posedge clk or negedge rst_n)
if(!rst_n)
div_cnt<=20'b0;
else if(en_div_cnt)begin
if(div_cnt<SCL_CNT_M)
div_cnt<=div_cnt+1'b1;
else
div_cnt<=0;
end
else
div_cnt<=20'b0;
wire sclk_plus =(div_cnt == SCL_CNT_M);//当到达时刻时,置高电平
assign i2c_sdat = !i2c_sdat_o && i2c_sdat_oe ? 1'b0:1'bz;//先计算!i2c_sdat,再&&,最后?://i2c_sdat_oe高电平为输出模式,i2c_sdat_o==1时i2c_sdat=z(由外部上拉电阻把数据线拉高,i2c_sdat_o==0时i2c_sdat=0)
3.各状态转换(状态机设计)
有部分连线在真实过程中不会出现,画出来是为了保证状态机完整性。
从上面的状态转移图可以看出,这里总共分成了 7 个状态,刚开始复位后的默认状态(IDLE)、产生起始信号状态(GEN_STA)、写数据状态(WR_DATA)、读数据状态(RD_DATA)、检测从机是否应答状态(CHECK_ACK)、给从机应答状态(GEN_ACK)、产生停止位状态(GEN_STO)
//状态机7个状态定义:独热码
localparam
IDLE = 7'b0000001, //空闲状态
GEN_STA = 7'b0000010, //产生起始信号
WR_DATA = 7'b0000100, //写数据状态
RD_DATA = 7'b0001000, //读数据状态
CHECK_ACK = 7'b0010000, //检测应答状态
GEN_ACK = 7'b0100000, //产生应答状态
GEN_STO = 7'b1000000; //产生停止信号
Cmd 的 6 个命令(写请求<WR>、起始位请求<STA>、读请求<RD>、停止位请求<STO>、应答位请求<ACK>、无应答请求<NACK>)
//cmd命令定义
localparam
WR =6'b000001,//写请求
STA =6'b000010,//起始位请求
RD =6'b000100,//读请求
STO =6'b001000,//停止位请求
ACK =6'b010000,//应答位请求
NACK =6'b100000;//无应答请求
开始状态转移吧:
reg [4:0] cnt;//用来记录sclk_plus次数(1-4)
reg [7:0] state;
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
rx_data<=0;
i2c_sdat_oe<=1'b0;
en_div_cnt<=1'b0;
i2c_sdat_o<=1'b1;
trans_done<=1'b0;
ack_o<=1'b0;
state<=IDLE;
cnt<=0;
end
else begin
case(state)
IDLE://写器件地址操作的 Cmd 命令里面的条件会首先跳转到 GEN_STA这个状态,没有go则留在IDLE状态,分频计数器不开启
begin
trans_done <= 1'b0;
i2c_sdat_oe <= 1'd1;
if (go) begin
en_div_cnt <= 1'b1;//开始分频序列机计数
if (cmd & STA)
state <= GEN_STA;
else if (cmd & WR)
state <= WR_DATA;
else if (cmd & RD)
state <= RD_DATA;
else
state <= IDLE;
end
else begin
en_div_cnt <= 1'b0;
state <= IDLE;
end
end
GEN_STA://产生开始信号:SCL高电平时,SDA输出模式下由高拉低
begin
if (sclk_plus) begin//传送1bit信号分为四个时刻(SCL_CLOCK一个周期分为4部分)
if (cnt == 3)
cnt <= 0;
else
cnt <= cnt + 1'b1;
case (cnt)
0:begin i2c_sdat_o <= 1; i2c_sdat_oe <= 1'd1;end//拉高数据总线,sda输入模式
1:begin i2c_sclk <= 1;end//拉高时钟线
2:begin i2c_sdat_o <= 0; i2c_sclk <= 1;end//拉低sda,scl保持高电平
3:begin i2c_sclk <= 0;end//拉低scl
default:begin i2c_sdat_o <= 1; i2c_sclk <= 1;end
endcase
if (cnt == 3) begin//判断开始后是读or写
if (cmd & WR)
state <= WR_DATA;
else if (cmd & RD)
state <= RD_DATA;
end
end
end
WR_DATA:
begin
if (sclk_plus) begin
if (cnt == 31)
cnt <= 0;
else
cnt <= cnt + 1'b1;
case (cnt)
0,4,8,12,16,20,24,28://把 7 位器件地址和最低位(方向位)的 0 直接赋值给 Tx_DATA,从最高位开始传给i2c_sdat_o
begin
i2c_sdat_o <= tx_data[7-cnt[4:2]];//cnt[4:2]即第几个第一时刻
i2c_sdat_oe <= 1'd1;
end
1,5,9,13,17,21,25,29:begin i2c_sclk<=1;end //将总线时钟(SCL,代码里面 i2c_sclk)拉高
2,6,10,14,18,22,26,30:begin i2c_sclk<=1;end //将总线时钟继续保持为高电平
3,7,11,15,19,23,27,31:begin i2c_sclk <=0;end //将时钟总线 i2c_sclk 拉为低电平
default:begin i2c_sdat_o <= 1; i2c_sclk <= 1;end
endcase
if (cnt == 31) begin
state <= CHECK_ACK;
end
end
end
CHECK_ACK:
begin
if(sclk_plus)begin
if (cnt == 3)
cnt <= 0;
else
cnt <= cnt + 1'b1;
case(cnt)
0:begin i2c_sdat_oe <= 1'd0; i2c_sclk <= 0;end//sda数据总线为输入模式
1:begin i2c_sclk <= 1;end//将总线时钟 i2c_sclk 拉高
2:begin ack_o <= i2c_sdat; i2c_sclk <= 1;end//总线时钟继续保持为高电平,将数据总线数据输入到ack_o,此时判断对方产生是否产生应答只需要判断 ack_o 的值是 0(应答)还是 1(无应答)
3:begin i2c_sclk <= 0;end//将时钟总线 i2c_sclk 拉为低电平
default:begin i2c_sdat_o <= 1; i2c_sclk <= 1;end
endcase
if(cnt == 3)begin
if(cmd & STO)
state <= GEN_STO;
else begin
state <= IDLE;//继续写/读
trans_done <= 1'b1;
end
end
end
end
GEN_STO://在时钟(SCL,代码里面是 i2c_sclk)为高电平的时候,数据总线(SDA)由低电平到高电平的跳变就一个停止信号
begin
if(sclk_plus)begin
if(cnt == 3)
cnt <= 0;
else
cnt <= cnt + 1'b1;
case(cnt)
0:begin i2c_sdat_o <= 0; i2c_sdat_oe <= 1'd1;end//将 i2c_sdat_o设置为 0,i2c_sdat_oe 使能
1:begin i2c_sclk <= 1;end//将总线时钟 i2c_sclk 拉高
2:begin i2c_sdat_o <= 1; i2c_sclk <= 1;end//将 i2c_sdat_o 设置为1,让外部上拉电阻将数据总线 i2c_sdat 拉成高电平
3:begin i2c_sclk <= 1;end//将时钟总线 i2c_sclk 拉为高电平
default:begin i2c_sdat_o <= 1; i2c_sclk <= 1;end
endcase
if(cnt == 3)begin
trans_done <= 1'b1;
state <= IDLE;
end
end
end
RD_DATA:
begin
if (sclk_plus) begin
if (cnt == 31)
cnt <= 0;
else
cnt <= cnt + 1'b1;
case (cnt)
0,4,8,12,16,20,24,28:
begin
i2c_sdat_oe <= 1'd0;
i2c_sclk <= 0;
end //将总线设置为输入,即i2c_sdat_oe 设置为 0,i2c_sclk 拉为低电平
1,5,9,13,17,21,25,29:begin i2c_sclk<=1;end //sclkposedge
2,6,10,14,18,22,26,30:
begin
i2c_sclk <= 1;
rx_data <= {rx_data[6:0],i2c_sdat};//读取数据总线 i2c_sdat 的值到 Rx_DATA,每次将读取到的数据放到 Rx_DATA 的最低位,读取一次,左移一次
end //sclk keep high
3,7,11,15,19,23,27,31:begin i2c_sclk<=0;end //sclknegedge
default:begin i2c_sdat_o <= 1; i2c_sclk <= 1;end
endcase
if (cnt == 31) begin
state <= GEN_ACK;
end
end
end
GEN_ACK:
begin
if(sclk_plus)begin
if(cnt == 3)
cnt <= 0;
else
cnt <= cnt + 1'b1;
case(cnt)
0: begin
i2c_sdat_oe <= 1'd1;
i2c_sclk <= 0;
if(cmd & ACK)
i2c_sdat_o <= 1'b0;//拉低(应答)
else if(cmd & NACK)
i2c_sdat_o <= 1'b1;//拉高(不应答)
end
1:begin i2c_sclk <= 1;end
2:begin i2c_sclk <= 1;end
3:begin i2c_sclk <= 0;end
default:begin i2c_sdat_o <= 1; i2c_sclk <= 1;end
endcase
if(cnt == 3)begin
if(cmd & STO)
state <= GEN_STO;
else begin
state <= IDLE;
trans_done <= 1'b1;
end
end
end
end
default:state<=IDLE;
endcase
end
endmodule
以上代码连起来便是完整设计代码,可直接复制粘贴使用
接下来进行波形仿真:本实验采用的是镁光官网提供的 EEPROM 仿真模型,仿真模型 24LC04B.v 文件,在本篇文章顶部可自行下载
`timescale 1ns / 1ps
module i2c_bit_shift_tb;
reg Clk;
reg Rst_n;
reg [5:0] Cmd;
reg Go;
wire [7:0] Rx_DATA;
reg [7:0] Tx_DATA;
wire Trans_Done;
wire ack_o;
wire i2c_sclk;
wire i2c_sdat;
pullup PUP(i2c_sdat); //模拟外部上拉电阻
localparam
WR = 6'b000001, //写请求
STA = 6'b000010, //起始位请求
RD = 6'b000100, //读请求
STO = 6'b001000, //停止位请求
ACK = 6'b010000, //应答位请求
NACK = 6'b100000; //无应答请求
i2c_bit_shift DUT(
.clk(Clk),
.rst_n(Rst_n),
.cmd(Cmd),
.go(Go),
.rx_data(Rx_DATA),
.tx_data(Tx_DATA),
.trans_done(Trans_Done),
.ack_o(ack_o),
.i2c_sclk(i2c_sclk),
.i2c_sdat(i2c_sdat)
);
M24LC04B M24LC04B(
.A0(0),
.A1(0),
.A2(0),
.WP(0),
.SDA(i2c_sdat),
.SCL(i2c_sclk),
.RESET(~Rst_n)
);
always #10 Clk = ~Clk;
initial begin
Clk = 1;
Rst_n = 0;
Cmd = 6'b000000;
Go = 0;
Tx_DATA = 8'd0;
#2001;
Rst_n = 1;
#2000;
//写数据操作,往 EEPROM 器件的 B1 地址写数据
//第一次:起始位+EEPROM 器件地址(7 位)+写方向(1 位)
Cmd = STA | WR;//按位或
Go = 1;
Tx_DATA = 8'hA0 | 8'd0;//最后一位为0:写操作
@ (posedge Trans_Done);//说明字段传输完成,进入下一状态
Go = 0;
#200;
//第二次:写 EEPROM 的 8 位寄存器地址
Cmd = WR;
Go = 1;
Tx_DATA = 8'hB1;//写地址 B1
@ (posedge Trans_Done);
Go = 0;
#200;
//第三次:写 8 位数据 + 停止位
Cmd = WR | STO;
Go = 1;
Tx_DATA = 8'hda;//写数据 DA
@ (posedge Trans_Done);
Go = 0;
#200;
#5000000; //仿真模型的两次操作时间间隔
//读数据操作,从 EEPROM 器件的 B1 地址读数据
//第一次:起始位+EEPROM 器件地址(7 位)+写方向(1 位)
Cmd = STA | WR;
Go = 1;
Tx_DATA = 8'hA0 | 8'd0;//写方向
@ (posedge Trans_Done);
Go = 0;
#200;
//第二次:写 EEPROM 的 8 位寄存器地址
Cmd = WR;
Go = 1;
Tx_DATA = 8'hB1;//写地址 B1
@ (posedge Trans_Done);
Go = 0;
#200;
//第三次:起始位+EEPROM 器件地址(7 位)+读方向(1 位)
Cmd = STA | WR;
Go = 1;
Tx_DATA = 8'hA0 | 8'd1;//读方向
@ (posedge Trans_Done);
Go = 0;
#200;
//第四次:读 8 位数据 + 停止位
Cmd = RD | STO;
Go = 1;
@ (posedge Trans_Done);
Go = 0;
#200;
#2000;
$stop;
end
endmodule
4.波形分析
可以看到头和尾有两块很密集的波形,前面的是写操作后面的读操作,我们先看写(放大)
这张图上state显示出来的部分只有00000100,但不是只有这一个状态,其他状态放大波形可看到,这里不过多赘述了。
我们再来看看读操作部分吧。
四、I2C顶层模块:I2C_control
对于一个单字节读操作,可以分为四步:
(1)起始位+写数据(7 位器件 ID + 1 位读写控制位(这里是 0,表示写)),等待从
机应答。
(2)写数据(8 位 EEPROM 的寄存器地址),等待从机应答。
(3)起始位+写数据(7 位器件 ID + 1 位读写控制位(这里是 1,表示读)),等待从机应答。
(4)读数据(从 EEPROM 中读出 8 位数据)+ 应答位(根据需要给出应答0或者无应答1)+ 停止位。
总的来说就是写了三次 1 字节的数据,读了一次 1 字节的数据。为了让代码更简洁这里用两个 task 一个是用来写字节(write_byte),另一个是读字节(read_byte)。
写字节的 task 里面就将要写的字节数据准备好(赋值给 i2c_bit_shift 模块的 Tx_DATA 端口),同时涉及到的传给 i2c_bit_shift 模块的 Cmd 命令也准备好,同时触发 Go 信号,这样就可以通过
i2c_bit_shift 模块将要写的数据发送到总线上了。
同理,读字节的 task 里面将涉及到的传给i2c_bit_shift 模块的 Cmd 命令也准备好,同时触发 Go 信号,这样就可以通过 i2c_bit_shift 模块将总线上的数据读取出来了。
1.接口设计
module i2c_control(
Clk,
Rst_n,
wrreg_req,
rdreg_req,
addr,
addr_mode,
wrdata,
rddata,
device_id,
RW_Done,
ack,
i2c_sclk,
i2c_sdat
);
input Clk;
input Rst_n;
input wrreg_req;//写请求
input rdreg_req;//读请求
input [15:0] addr;//寄存器地址16位
input addr_mode;//单字节地址和双字节寄存器地址
input [7:0] wrdata;//要写入从机数据,是由Tx_DATA准备好最后再赋值给他
output reg [7:0] rddata;//存储读到的数据,也是读到Rx_DATA后再传给它
input [7:0] device_id;//器件地址
output reg RW_Done;//读写完成信号
output reg ack;//从机应答信号
output i2c_sclk;//时钟总线
inout i2c_sdat;//数据总线
wire [15:0] reg_addr;
assign reg_addr = addr_mode?addr:{addr[7:0],addr[15:8]};//双字节地址先传高八位再传低八位,所以若为单字节地址把地址有效部分【7:0】放到高位
2.模块例化
wire [7:0] Rx_DATA;
reg [7:0] Tx_DATA;
wire Trans_Done;
wire ack_o;
reg Go;
reg [5:0] Cmd;
//模块例化
i2c_bit_shift i2c_bit_shift(
.clk(Clk),
.rst_n(Rst_n),
.cmd(Cmd),
.go(Go),
.rx_data(Rx_DATA),
.tx_data(Tx_DATA),
.trans_done(Trans_Done),
.ack_o(ack_o),
.i2c_sclk(i2c_sclk),
.i2c_sdat(i2c_sdat)
);
3.6个命令(cmd)和7个状态
//六个命令
localparam
WR =6'b000001,//写请求
STA =6'b000010,//起始位请求
RD =6'b000100,//读请求
STO =6'b001000,//停止位请求
ACK =6'b010000,//应答位请求
NACK =6'b100000;//无应答请求
//七个状态
localparam
IDLE = 7'b0000001, //空闲状态
WR_REG = 7'b0000010, //写寄存器状态
WAIT_WR_DONE = 7'b0000100, //等待写寄存器完成状态
WR_REG_DONE = 7'b0001000, //写寄存器完成状态
RD_REG = 7'b0010000, //读寄存器状态
WAIT_RD_DONE = 7'b0100000, //等待读寄存器完成状态
RD_REG_DONE = 7'b1000000; //读寄存器完成状态
4.读和写任务
目的是为了简化后续代码
task read_byte;
input [5:0]Ctrl_Cmd;
begin
Cmd <= Ctrl_Cmd;
Go <= 1'b1;
end
endtask
task write_byte;
input [5:0]Ctrl_Cmd;
input [7:0]Wr_Byte_Data;
begin
Cmd <= Ctrl_Cmd;
Tx_DATA <= Wr_Byte_Data;
Go <= 1'b1;
end
endtask
5.状态机设计
reg [6:0] state;
reg [7:0] cnt;
always@(posedge Clk or negedge Rst_n)
if(!Rst_n)begin
Cmd<=6'b0;
Tx_DATA<=8'b0;
Go<=1'b0;
rddata<=0;
state<=IDLE;
ack<=0;
end
else begin
case(state)
IDLE:
begin
cnt<=0;
ack<=0;
RW_Done<=1'b0;
if(wrreg_req)
state<=WR_REG;
else if(rdreg_req)
state<=RD_REG;
else
state<=IDLE;
end
WR_REG:
begin
state <= WAIT_WR_DONE;//每次进入写请求时都会先进入等待写完成阶段(这一阶段的目的是判断存储器地址字节数并接收从机ACK信号)
case(cnt)
0:write_byte(WR | STA, device_id);
1:write_byte(WR, reg_addr[15:8]);
2:write_byte(WR, reg_addr[7:0]);//若为单字节地址应该跳过这个步骤,在状态WAIT_WR_DONE中执行
3:write_byte(WR | STO, wrdata);
default:;
endcase
end
WAIT_WR_DONE:
begin
Go <= 1'b0;
if(Trans_Done)begin
ack <= ack | ack_o;
case(cnt)
0: begin cnt <= 1; state <= WR_REG;end
1: begin
state <= WR_REG;
if(addr_mode)//addr_mode==1即为双字节地址,从2开始继续传输低8位地址,反之为单字节跳过步骤2,从3开始
cnt <= 2;
else
cnt <= 3;
end
2: begin
cnt <= 3;
state <= WR_REG;
end
3:state <= WR_REG_DONE;
default:state <= IDLE;
endcase
end
end
WR_REG_DONE:
begin
RW_Done <= 1'b1;//完成写操作标志
state <= IDLE;
end
RD_REG:
begin
state <= WAIT_RD_DONE;
case(cnt)
0:write_byte(WR | STA, device_id);
1:write_byte(WR, reg_addr[15:8]);
2:write_byte(WR, reg_addr[7:0]);
3:write_byte(WR | STA, device_id | 8'd1);//最低位是1,读操作命令(一定不要混淆主机发送读命令和主机的写操作,这一步是主机写操作并告诉从机它需要读数据了)
4:read_byte(RD | NACK | STO);
default:;
endcase
end
WAIT_RD_DONE:
begin
Go <= 1'b0;
if(Trans_Done)begin
if(cnt <= 3)
ack <= ack | ack_o;
case(cnt)
0: begin cnt <= 1; state <= RD_REG;end
1: begin
state <= RD_REG;
if(addr_mode)
cnt <= 2;
else
cnt <= 3;
end
2: begin
cnt <= 3;
state <= RD_REG;
end
3:begin
cnt <= 4;
state <= RD_REG;
end
4:state <= RD_REG_DONE;
default:state <= IDLE;
endcase
end
end
RD_REG_DONE:
begin
RW_Done <= 1'b1;
rddata <= Rx_DATA;
state <= IDLE;
end
default:state<=IDLE;
endcase
end
endmodule
以上为所有完整代码。
6.仿真
接下来是波形仿真:依然需要用到仿真模型 24LC04B.v 文件
`timescale 1ns / 1ps
module i2c_control_tb;
reg Clk;
reg Rst_n;
reg wrreg_req;
reg rdreg_req;
reg [15:0]addr;
reg addr_mode;
reg [7:0]wrdata;
wire [7:0]rddata;
reg [7:0]device_id;
wire RW_Done;
wire ack;
wire i2c_sclk;
wire i2c_sdat;
pullup PUP(i2c_sdat);//外部上拉
i2c_control DUT(
.Clk(Clk),
.Rst_n(Rst_n),
.wrreg_req(wrreg_req),
.rdreg_req(rdreg_req),
.addr(addr),
.addr_mode(addr_mode),
.wrdata(wrdata),
.rddata(rddata),
.device_id(device_id),
.RW_Done(RW_Done),
.ack(ack),
.i2c_sclk(i2c_sclk),
.i2c_sdat(i2c_sdat)
);
M24LC04B M24LC04B(
.A0(0),
.A1(0),
.A2(0),
.WP(0),
.SDA(i2c_sdat),
.SCL(i2c_sclk),
.RESET(~Rst_n)
);
initial Clk = 1;
always #10 Clk = ~Clk;
initial begin
Rst_n = 0;
rdreg_req = 0;
wrreg_req = 0;
#2001;
Rst_n = 1;
#2000;
write_one_byte(8'hA0,8'h0A,8'hd1);
write_one_byte(8'hA0,8'h0B,8'hd2);
write_one_byte(8'hA0,8'h0C,8'hd3);
write_one_byte(8'hA0,8'h0D,8'hd4);
read_one_byte(8'hA0,8'h0A);
read_one_byte(8'hA0,8'h0B);
read_one_byte(8'hA0,8'h0C);
read_one_byte(8'hA0,8'h0D);
$stop;
end
task write_one_byte;
input [7:0]id;
input [7:0]mem_address;
input [7:0]data;
begin
addr = {8'd0,mem_address};
device_id = id;
addr_mode = 0;
wrdata = data;
wrreg_req = 1;
#20;
wrreg_req = 0;
@(posedge RW_Done);
#5000000;//一定要等够5ms
end
endtask
task read_one_byte;
input [7:0]id;
input [7:0]mem_address;
begin
addr = {8'd0,mem_address};
device_id = id;
addr_mode = 0;
rdreg_req = 1;
#20;
rdreg_req = 0;
@(posedge RW_Done);
#5000000;//一定要等够5ms
end
endtask
endmodule
插播一条:之前做仿真第二次写操作一直收不到从机ACK,找了好久原因才发现24LC04B 需要 5ms 的写周期时间,之前写的仿真代码周期时间设短了,主机在从机未完成内部写周期时发起新操作,从机可能不响应。