突然发现好久没写文章了,今天就写一篇关于i2c的通用控制模块。
i2c协议保护起始,数据传输,ACK或NACK,和传输终止信号。以下是对应的时序图:
在SCL为高的情况下,SDA由高跳到低,这是起始信号,之后在时钟为低电平时更新数据,在高电平时数据保持稳定,每一次传输8bit数据之后是ACK信号,在受到ACK信号后可以选择结束通信或者继续传输数据,这是基本的i2c协议。
而eeprom的i2c有些许不一样,所以在设计时应考虑加入一些冗余以兼容eeprom的读写。
eeprom的读操作如下图:
在向SDA写入设备地址和设备内的存贮地址时,待ACK信号后不是继续传输地址,而是发出起始信号,在接受读操作时需要回复NACK信号(ACK信号是接受方在SCL为高电平时写入低电平数据,NACK,则相反),之后便是终止信号。
基于上述内容,设计如下状态机:
这里WAIT_READ和WAIT_WRITE是为满足时序,实现传输8bit后给外部模块相应的信号,WAIT状态将接受外部的信号以确定下一步的动作是继续传输数据还是终止传输,内部保留超时机制,确保状态机不会被卡死,并且存在跳到START的可能,主要是为了兼容eeprom的读操作而特意设计的,W_ACK,R_ACK是为区分上一步的操作是都还是写,WRITE和READ是读写状态,在需要回复ACK信号时,如果回复NACK信号会跳转到STOP状态以结束通信,整个状态跳转机流程基本介绍完毕,以下是对应的实现代码:
module i2c_ctrl(clk,rst_n,din,w_next,r_next,t_done,ena,restart,i2c_scl,i2c_sdl,done,tc,dout,sdl_ena);
input clk;
input rst_n;
input [7:0]din;
input w_next;
input r_next;
input t_done;
input ena;
input restart;
inout i2c_sdl;
output i2c_scl;
output reg done;
output reg tc;
output reg [7:0]dout;
reg [10:0]state;
reg [1:0]scl_cnt;
reg [7:0]wait_cnt;
reg [3:0]bit_cnt;
reg [7:0]d_in_tmp;
reg sdl_data;
output reg sdl_ena;
parameter WAIT_TIMEOUT = 100;
localparam IDLE = 11'b000_0000_0001 ,
START = 11'b000_0000_0010 ,
WRITE = 11'b000_0000_0100 ,
W_ACK = 11'b000_0000_1000 ,
R_ACK = 11'b000_0001_0000 ,
WAIT_WRITE = 11'b000_0010_0000 ,
WAIT_READ = 11'b000_0100_0000 ,
WAIT = 11'b000_1000_0000 ,
READ = 11'b001_0000_0000 ,
NACK = 11'b010_0000_0000 ,
STOP = 11'b100_0000_0000 ;
assign i2c_sdl=(sdl_ena)?sdl_data:1'bz;
assign i2c_scl=(scl_cnt>2'd1)?1'b1:1'b0;
always @(posedge clk,negedge rst_n) begin
if(!rst_n)
scl_cnt <= 0;
else if(state ==IDLE )
scl_cnt <= 2'd2 ; //keep i2c_scl in high when state is IDLE
else if( state !=WAIT && state!=WAIT_READ)
scl_cnt <= scl_cnt +1'b1;
else
scl_cnt <= 0;
end
always @(posedge clk,negedge rst_n) begin
if(!rst_n)
wait_cnt <= 0;
else if(state == WAIT)
wait_cnt <= wait_cnt +1'b1;
else
wait_cnt <= 0;
end
always @(posedge clk,negedge rst_n) begin
if(!rst_n)
bit_cnt <= 0;
else if(state ==READ ||state ==WRITE)
begin
if(scl_cnt == 2'd2)
bit_cnt <= bit_cnt +1'b1;
end
else
bit_cnt <= 0;
end
/**************state change************************/
always @(posedge clk,negedge rst_n) begin
if(!rst_n )
state <= IDLE ;
else case(state)
IDLE : if(ena)
state <= START ;
START : if(scl_cnt== 2'd3)
state <= WRITE ;
WRITE : if(bit_cnt==4'd8 && scl_cnt == 2'd3)
state <= W_ACK ;
W_ACK : if(scl_cnt==2'd2)
begin
if(i2c_sdl == 1'b0)
state <= WAIT_WRITE ;
else
state <= STOP ;
end
R_ACK : if(bit_cnt==2'd3)
state <= WAIT ;
WAIT_WRITE : state <= WAIT ;
WAIT : if(t_done || wait_cnt== WAIT_TIMEOUT)
state <= STOP ;
else if(restart )
state <= START ;
else if(w_next)
state <=WRITE ;
else if(r_next)
state <= READ ;
else
state <=WAIT ;
READ : if(bit_cnt ==4'd8 && scl_cnt==2'd3)
state <= WAIT_READ ;
WAIT_READ : if(r_next)
state <= R_ACK;
else
state <= NACK ;
NACK : if(scl_cnt == 2'd3)
state <= STOP ;
STOP : if(scl_cnt == 2'd3)
state <= IDLE ;
default : state <= IDLE ;
endcase
end
/***************state output*************************/
always @(posedge clk,negedge rst_n) begin
if(!rst_n )
begin
d_in_tmp <= 0;
sdl_ena <= 1'b1;
sdl_data <= 1'b1;
tc <= 1'b0;
dout <= 8'b0;
done <= 1'b0;
end
else case(state)
IDLE : begin
sdl_ena <= 1'b1;
sdl_data <= 1'b1;
if(ena)
d_in_tmp <= din;
end
START : begin
sdl_ena <= 1'b1;
if(scl_cnt < 2'd2)
sdl_data <= 1'b1;
else
sdl_data <= 1'b0;
end
WRITE : begin
sdl_ena <= 1'b1;
if(scl_cnt == 2'd0)
begin
sdl_data <= d_in_tmp[7];
d_in_tmp <= {d_in_tmp[6:0],1'b0};
end
end
W_ACK : if(scl_cnt < 2'd2)
sdl_ena <= 1'b0;
else
begin
done <=1'b1;
sdl_ena <= 1'b0;
end
R_ACK : if(scl_cnt <= 2'd2)
begin
sdl_ena <= 1'b1;
sdl_data <= 1'b0;
end
else
sdl_ena <= 1'b0;
WAIT : begin
if(w_next)
d_in_tmp <= din;
done <= 1'b0;
end
READ : begin
sdl_ena <= 1'b0;
if(scl_cnt==2'd2)
dout <= {dout[6:0],i2c_sdl};
if(bit_cnt == 4'd8 && scl_cnt == 2'd2)
done <= 1'b1;
else
done <= 1'b0;
end
WAIT_READ : ;
WAIT_WRITE : done <= 1'b0;
NACK : begin
sdl_ena <= 1'b1;
sdl_data <= 1'b1;
end
STOP : begin
sdl_ena <= 1'b1;
if(scl_cnt <2'd2)
sdl_data <= 1'b0;
else
begin
if(scl_cnt == 2'd2)
tc <= 1'b1;
else
tc <= 1'b0;
sdl_data <= 1'b1;
end
end
default : ;
endcase
end
endmodule
这里对模块进行简要的说明,clk使用1MHz的信号,并对其进行4分频以产生SCL时钟,w_next,r_next是外部模块在接受到内部模块发出8bit传输完成done信号后,控制模块是否继续写入数据还是继续读数据,t_done信号为是否接受通信的控制信号,信号优先级高于读写信号,restart信号就是为了兼容eeprom读写而特地设计的,done信号是8bit传输完成信号,tc为传输完成信号,对应于一次传输任务完成,外部模块可以根据此信号进行下一次传输,dout是读到的数据,在done为高时有效,din是外部输入数据。
以下是对应的测试代码,使用System Veilog 编写的:
module i2c_ctrl_tb(i2c_sdl);
reg clk;
reg rst_n;
reg [7:0]din;
reg w_next;
reg r_next;
reg t_done;
reg ena;
reg restart;
inout i2c_sdl;
wire i2c_scl;
wire done;
wire tc;
wire [7:0]dout;
wire sdl_ena;
i2c_ctrl inst(.*);
initial clk=0;
always #10 clk=~clk;
byte cnt;
initial begin
rst_n =1'b0;
din=8'h3d;
ena = 1'b1;
w_next = 1'b0;
r_next = 1'b0;
restart = 1'b0;
t_done = 1'b0;
#20 rst_n=1'b1;
end
assign i2c_sdl=(~sdl_ena)?1'b0:1'bz;
always @(posedge clk, negedge rst_n)begin
if(!rst_n)
cnt <= 0;
else if(done)
begin
cnt <= cnt+1'b1;
w_next <= 1'b1;
if (cnt==0)
din <= 8'h27;
else if(cnt==1)
din <= 8'h31;
else if(cnt ==2)
din <= 8'h4f;
else
begin
din <= 0;
w_next <= 1'b0;
t_done <= 1'b1;
ena <= 1'b0;
end
end
end
endmodule
注意,这个测试代码只对写操作进行测试,读没有测试,超时机制也没有测试,eeprom的读所要求的跳转restart也没有测试,主要是太麻烦了,一个人没有这么多的时间。
图片来源于野火fpga书籍,
状态机截图于quartus 软件。
完
2021/11/06