文章目录
一、I2C读写控制模块说明
前面的文章中设计了I2C最小单元读写模块,能够发送1byte的数据,根据I2C时序图,单次数据写操作需要发送7位器件地址、8位或16位存储单元地址、8位写数据,需要多次调用最小单元读写模块。因此本文设计I2C读写控制模块,将器件地址、存储单元地址、读写数据作为输入输出端口,能够完成一次单字节数据的读写。
二、I2C读写控制模块设计
1、端口说明
端口名称 | 方向 | 说明 |
---|---|---|
Clk | input | 系统时钟 |
Rst_n | input | 复位信号 |
wrreq | input | 写请求信号 |
rdreq | input | 读请求信号 |
device_id [7:0] | input | 7位器件ID |
addr [15:0] | input | 16位存储器地址 |
wrdata [7:0] | input | 8位写数据 |
rddata [7:0] | output | 8位读数据 |
RW_Done | output | 读写完成标志 |
Ack | output | 应答信号 |
i2c_sclk | output | I2C时钟SCLK |
i2c_sdat | inout | I2C数据线SDA |
将之前设计的I2C最小单元读写模块例化到本模块中,通过多次调用实现功能。device_id为7位器件地址和读写操作位的组合。Ack为根据从机多次返回的应答信号确定,若有一次为非应答,则Ack为1,若每次都为应答,则Ack为0。
2.代码设计
(1)8位地址和16位地址确定
在代码中添加wire型信号addr_mode,若addr高八位不等于0,则addr为16位地址,addr_mode为1;addr高八位等于0,则addr为8位地址,addr_mode为0。
wire addr_mode;//0:addr为8位地址 1:addr为16位地址
assign addr_mode = (addr[15:8] != 8'd0) ? 1'b1:1'b0;
(2)最小单元读写任务封装
将最小单元读写操作封装为任务I2C_WR和I2C_RD,在调用时只需要将Cmd和TX_DATA传入任务即可。
task I2C_WR;
input [5:0] cmd;
input [7:0] wr_DATA;
begin
Cmd = cmd;
TX_DATA = wr_DATA;
Go = 1'b1;
end
endtask
task I2C_RD;
input [5:0] cmd;
begin
Cmd = cmd;
Go = 1'b1;
end
endtask
封装成任务后,2字节地址单字节数据写操作简化为以下代码:
I2C_WR(STA|WR, device_id & 8'hfe);
I2C_WR(WR, addr[15:8]);
I2C_WR(WR, addr[7:0]);
I2C_WR(WR|STO, wrdata);
2字节地址单字节数据读操作简化为以下代码:
I2C_WR(STA|WR, device_id & 8'hfe);
I2C_WR(WR, addr[15:8]);
I2C_WR(WR, addr[7:0]);
I2C_WR(STA|WR, device_id | 8'h01);
I2C_RD(RD|NACK|STO);
(3)状态转换
将I2C读写控制逻辑用状态机实现,可分为以下7个状态:
IDLE:空闲状态
WR_REG:写状态
WAIT_WR_DONE:等待写完成
WR_DONE:写完成
RD_REG:读状态
WAIT_RD_DONE:等待读完成
RD_DONE:读完成
状态转换图如下:
IDLE状态时,根据wrreq和rdreq跳入对应的读写状态。读操作需要调用4次最小单元模块,写操作需要调用5次最小单元模块,利用cnt对调用次数进行计数。在写状态WR_REG和读状态RD_REG两个状态中单次调用最小单元模块后,跳入等待读写完成状态WAIT_WR_DONE和WAIT_RD_DONE,等待单次I2C完成标志Trans_Done置1,置1后根据cnt判断是否继续读写操作。
各状态及最小单元模块Cmd参数定义:
localparam
STA = 6'b000001, //起始位请求
WR = 6'b000010, //写请求
RD = 6'b000100, //读请求
STO = 6'b001000, //停止位请求
ACK = 6'b010000, //应答位请求
NACK = 6'b100000; //无应答请求
reg [8:0] state;
localparam
IDLE = 7'b0000001, //空闲状态
WR_REG = 7'b0000010, //写状态
WAIT_WR_DONE = 7'b0000100, //等待写完成
WR_DONE = 7'b0001000, //写完成
RD_REG = 7'b0010000, //读状态
WAIT_RD_DONE = 7'b0100000, //等待读完成
RD_DONE = 7'b1000000; //读完成
(4)空闲状态
空闲状态根据读请求标志wrreq和写请求标志rdreq进行判断,跳入对应的读写状态。
IDLE:begin
cnt <= 8'd0;
RW_Done = 1'b0;
Ack = 1'b0;
if(wrreq)
state <= WR_REG;
else if(rdreq)
state <= RD_REG;
else
state <= IDLE;
end
(5)写状态
写状态中,调用前面封装好的task任务,将一次读操作分为4步:写器件地址、写存储器高8位地址、写存储器低8位地址、写8位数据。cnt在WAIT_WR_DONE中计数。
WR_REG:begin
case(cnt)
0:I2C_WR(STA|WR, device_id & 8'hfe);
1:I2C_WR(WR, addr[15:8]);
2:I2C_WR(WR, addr[7:0]);
3:I2C_WR(WR|STO, wrdata);
default;
endcase
state <= WAIT_WR_DONE;
end
(6)等待写完成
根据Trans_Done信号判断I2C最小单元是否发送完成,若完成,则对Ack信号进行赋值,并对cnt计数。当cnt为0时,器件地址发送完成,下一步是发送存储器地址,根据之前计算的addr_mode信号判断是否为16位地址,addr_mode为1,表示16位地址,则cnt为1,发送高8位;addr_mode为0,表示8位地址,跳过发高8位,cnt为2。
WAIT_WR_DONE:begin
if(Trans_Done) begin
Ack = Ack | Ack_o;
case(cnt)
0:begin
if(addr_mode)
cnt <= 1;
else
cnt <= 2;
state <= WR_REG;
end
1:begin cnt <= 2; state <= WR_REG; end
2:begin cnt <= 3; state <= WR_REG; end
3:begin cnt <= 0; state <= WR_DONE; end
default:state <= IDLE;
endcase
end
else
Go = 1'b0;
end
(7)写完成
写操作完成,将读写完成标志RW_Done置1,跳回空闲状态。
WR_DONE:begin
RW_Done <= 1'b1;
state <= IDLE;
end
(8)读状态
读状态和写状态类似,调用前面封装好的task任务,将一次读操作分为5步:写器件地址+写标志、写存储器高8位地址、写存储器低8位地址、写器件地址+读命令、写8位数据。cnt在WAIT_RD_DONE中计数。
RD_REG:begin
case(cnt)
0:I2C_WR(STA|WR, device_id & 8'hfe);
1:I2C_WR(WR, addr[15:8]);
2:I2C_WR(WR, addr[7:0]);
3:I2C_WR(STA|WR, device_id | 8'h01);
4:I2C_RD(RD|NACK|STO);
default;
endcase
state <= WAIT_RD_DONE;
end
(9)等待读完成
根据Trans_Done信号判断I2C最小单元是否发送完成,并对cnt计数。当cnt为0时,器件地址发送完成,下一步是发送存储器地址,根据之前计算的addr_mode信号判断是否为16位地址,addr_mode为1,表示16位地址,则cnt为1,发送高8位;addr_mode为0,表示8位地址,跳过发高8位,cnt为2。
WAIT_RD_DONE:begin
if(Trans_Done) begin
case(cnt)
0:begin
if(addr_mode)
cnt <= 1;
else
cnt <= 2;
state <= RD_REG;
end
1:begin cnt <= 2; state <= RD_REG; end
2:begin cnt <= 3; state <= RD_REG; end
3:begin cnt <= 4; state <= RD_REG; end
4:begin cnt <= 0; state <= RD_DONE; end
default:state <= IDLE;
endcase
end
else
Go = 1'b0;
end
(10)读完成
读操作完成,将读写完成标志RW_Done置1,读取到的数据存入rddata,跳回空闲状态。
RD_DONE:begin
rddata <= RX_DATA;
RW_Done <= 1'b1;
state <= IDLE;
end
I2C读写控制模块源代码
module i2c_control(
input Clk, //系统时钟
input Rst_n, //复位信号
input wrreq, //写请求信号
input rdreq, //读请求信号
input [7:0] device_id, //7位器件ID
input [15:0] addr, //16位存储器地址
input [7:0] wrdata, //8位写数据
output reg [7:0] rddata, //8位读数据
output reg RW_Done, //读写完成标志
output reg Ack, //应答信号
output wire i2c_sclk, //I2C时钟SCLK
inout i2c_sdat //I2C数据线SDA
);
reg [5:0] Cmd;
reg [8:0] state;
reg Go; //单次I2C起始信号
reg [3:0] cnt; //单次I2C读写操作计数器
reg [7:0] TX_DATA; //单次I2C接收数据
wire Trans_Done; //单次I2C完成标志
wire Ack_o; //单次I2C应答信号
wire [7:0] RX_DATA; //单次I2C读取数据
wire addr_mode;//0:addr为8位地址 1:addr为16位地址
//addr高八位不等于0,则addr为16位地址,addr_mode为1
assign addr_mode = (addr[15:8] != 8'd0) ? 1'b1:1'b0;
localparam
STA = 6'b000001, //起始位请求
WR = 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_DONE = 7'b0001000, //写完成
RD_REG = 7'b0010000, //读状态
WAIT_RD_DONE = 7'b0100000, //等待读完成
RD_DONE = 7'b1000000; //读完成
always@(posedge Clk or negedge Rst_n)
if(!Rst_n) begin
rddata = 8'd0;
RW_Done = 1'b0;
Ack = 1'b0;
cnt = 3'd0;
state <= IDLE;
end
else begin
case(state)
IDLE:begin
cnt <= 8'd0;
RW_Done = 1'b0;
Ack = 1'b0;
if(wrreq)
state <= WR_REG;
else if(rdreq)
state <= RD_REG;
else
state <= IDLE;
end
WR_REG:begin
case(cnt)
0:I2C_WR(STA|WR, device_id & 8'hfe);
1:I2C_WR(WR, addr[15:8]);
2:I2C_WR(WR, addr[7:0]);
3:I2C_WR(WR|STO, wrdata);
default;
endcase
state <= WAIT_WR_DONE;
end
WAIT_WR_DONE:begin
if(Trans_Done) begin
Ack = Ack | Ack_o;
case(cnt)
0:begin
if(addr_mode)
cnt <= 1;
else
cnt <= 2;
state <= WR_REG;
end
1:begin cnt <= 2; state <= WR_REG; end
2:begin cnt <= 3; state <= WR_REG; end
3:begin cnt <= 0; state <= WR_DONE; end
default:state <= IDLE;
endcase
end
else
Go = 1'b0;
end
WR_DONE:begin
RW_Done <= 1'b1;
state <= IDLE;
end
RD_REG:begin
case(cnt)
0:I2C_WR(STA|WR, device_id & 8'hfe);
1:I2C_WR(WR, addr[15:8]);
2:I2C_WR(WR, addr[7:0]);
3:I2C_WR(STA|WR, device_id | 8'h01);
4:I2C_RD(RD|NACK|STO);
default;
endcase
state <= WAIT_RD_DONE;
end
WAIT_RD_DONE:begin
if(Trans_Done) begin
case(cnt)
0:begin
if(addr_mode)
cnt <= 1;
else
cnt <= 2;
state <= RD_REG;
end
1:begin cnt <= 2; state <= RD_REG; end
2:begin cnt <= 3; state <= RD_REG; end
3:begin cnt <= 4; state <= RD_REG; end
4:begin cnt <= 0; state <= RD_DONE; end
default:state <= IDLE;
endcase
end
else
Go = 1'b0;
end
RD_DONE:begin
rddata <= RX_DATA;
RW_Done <= 1'b1;
state <= IDLE;
end
endcase
end
task I2C_WR;
input [5:0] cmd;
input [7:0] wr_DATA;
begin
Cmd = cmd;
TX_DATA = wr_DATA;
Go = 1'b1;
end
endtask
task I2C_RD;
input [5:0] cmd;
begin
Cmd = cmd;
Go = 1'b1;
end
endtask
i2c_bit_shift i2c_bit_shift(
.Clk(Clk), //系统时钟
.Rst_n(Rst_n), //复位信号
.Cmd(Cmd), //I2C读写控制命令
.Go(Go), //I2C读写开始信号
.RX_DATA(RX_DATA), //I2C读取数据
.TX_DATA(TX_DATA), //I2C发送数据
.Trans_Done(Trans_Done),//单次读写操作完成标志
.Ack_o(Ack_o), //应答信号ACK
.i2c_sclk(i2c_sclk), //I2C时钟SCLK
.i2c_sdat(i2c_sdat) //I2C数据线SDA
);
endmodule
三、I2C读写控制模块仿真
1.读写操作任务封装
将单次读写操作封装成task任务,提高代码的简洁性,仿真时只需要将器件地址device_id、存储单元地址addr和写数据wrdata传入任务即可。
task I2C_WRITE;
input [7:0] wr_id;
input [15:0] wr_addr;
input [7:0] wr_data;
begin
device_id = wr_id;
addr = wr_addr;
wrdata = wr_data;
wrreq = 1'b1;#20;
wrreq = 1'b0;#20;
wait(RW_Done);#200;
end
endtask
task I2C_READ;
input [7:0] wr_id;
input [15:0] wr_addr;
begin
device_id = wr_id;
addr = wr_addr;
rdreq = 1'b1;#20;
rdreq = 1'b0;#20;
wait(RW_Done);#200;
end
endtask
2.仿真文件
采用EEPROM仿真模型M24LC04B对本模块进行仿真。
向器件地址A0的存储单元16’h0011写入数据8’hCD,向存储单元16’h0145写入数据8’h58,写入后读取这两个存储单元的数据。
`timescale 1ns/1ns
module i2c_control_tb();
reg Clk,Rst_n;
reg wrreq,rdreq;
reg [7:0] device_id;
reg [15:0] addr;
reg [7:0] wrdata;
wire [7:0] rddata;
wire RW_Done;
wire Ack;
wire i2c_sclk;
wire i2c_sdat;
pullup(i2c_sdat); //模拟外部上拉电阻
i2c_control i2c_control(
.Clk(Clk), //系统时钟
.Rst_n(Rst_n), //复位信号
.wrreq(wrreq), //写请求信号
.rdreq(rdreq), //读请求信号
.device_id(device_id), //7位器件ID
.addr(addr), //16位存储器地址
.wrdata(wrdata), //8位写数据
.rddata(rddata), //8位读数据
.RW_Done(RW_Done), //读写完成标志
.Ack(Ack), //应答信号
.i2c_sclk(i2c_sclk), //I2C时钟SCLK
.i2c_sdat(i2c_sdat) //I2C数据线SDA
);
M24LC04B M24LC04B(
.A0(0),
.A1(0),
.A2(0),
.WP(0),//写保护引脚,高电平不允许写,低电平允许写
.SDA(i2c_sdat),
.SCL(i2c_sclk),
.RESET(~Rst_n)
);
initial Clk = 1'b1;
always #10 Clk = ~Clk;
initial begin
Rst_n = 1'b0;
wrreq = 1'b0;
rdreq = 1'b0;
device_id = 8'd0;
addr = 16'd0;
wrdata = 8'd0;
#201;
Rst_n = 1'b1;
#20;
I2C_WRITE(8'hA0,16'h0011,8'hCD);
I2C_WRITE(8'hA0,16'h0145,8'h58);
#20000;
I2C_READ(8'hA0,16'h0011);
I2C_READ(8'hA0,16'h0145);
#20000;
$stop;
end
task I2C_WRITE;
input [7:0] wr_id;
input [15:0] wr_addr;
input [7:0] wr_data;
begin
device_id = wr_id;
addr = wr_addr;
wrdata = wr_data;
wrreq = 1'b1;#20;
wrreq = 1'b0;#20;
wait(RW_Done);#200;
end
endtask
task I2C_READ;
input [7:0] wr_id;
input [15:0] wr_addr;
begin
device_id = wr_id;
addr = wr_addr;
rdreq = 1'b1;#20;
rdreq = 1'b0;#20;
wait(RW_Done);#200;
end
endtask
endmodule