I2C_MASTER模块设计
I2C协议简介
I2C的数据传输中,其实重要的只有4点:起始位,数据发送,应答位,结束位。
-
起始位:SCL为高时,使SDA产生下降沿
-
数据发送:SCL为低时,数据允许变化,SDA为高时,数据必须保持稳定
-
应答位:每一相数据发完后,数据接收方发出ACK表示已经收到数据,数据发送方根据这一位判断是否继续发送数据。
-
结束位:SCL为高时,使SDA产生上升沿
知道这几点后,已经足够我们设计出状态机了,而更细节的部分直接看这篇文章就可以了:I2C协议靠这16张图彻底搞懂(超详细)。
I2C接口设计
接口信号及解释
对于这一模块,我们为了能够方便的使用,采用命令+请求信号相组合的方式来驱动这一模块,于是就有了如下的接口设计:
module i2c_master
(
input clk ,
input rst_n ,
input i2c_start ,
input [7:0] i2c_data_in ,
input [3:0] i2c_cmd ,
output i2c_scl ,
input i2c_sda_in ,
output i2c_sda_out ,
output i2c_sda_en ,
output [7:0] dout ,
output i2c_end
);
-
i2c_start:每次使用时,都需要给予一个周期的start信号
-
i2c_cmd:每次使用时,都需要在i2c_start前给予希望执行的操作
-
i2c_data_in:这一信号只针对写操作,如果发送了写命令,则还需要在i2c_start前给予要写的数据
-
dout:这一信号只针对写操作,如果发送了读命令,这一信号就会返回读出的数据
-
i2c_end:这是告诉控制模块此次I2C传输完成的信号,如果发送了读命令,这也是数据有效信号
I2C状态机设计
I2C状态机原理图
状态机简介
首先分析I2C协议中需要接口模块做什么:
-
发起始位
-
读/写操作
-
读/写响应
-
发停止位
明确需要进行的操作后,就可以确定需要几个状态了。因为接口是基于命令控制来设计的,那么状态机的状态跳转条件也要与命令相关。为了便于理解,此处贴出状态跳转的代码:
//命令
localparam START_CMD = 4'b0001,
WRITE_CMD = 4'b0010,
READ_CMD = 4'b0100,
STOP_CMD = 4'b1000;
//状态跳转条件
assign idle_start = (state_c == IDLE ) && (start && (i2c_cmd & START_CMD));
assign start_write = (state_c == START ) && (end_cnt_time && (i2c_cmd & WRITE_CMD));
assign start_read = (state_c == START ) && (end_cnt_time && (i2c_cmd & READ_CMD));
assign write_wack = (state_c == WRITE ) && (end_cnt_bit);
assign read_rack = (state_c == READ ) && (end_cnt_bit);
assign wack_stop = (state_c == WACK ) && (end_cnt_time && ((i2c_cmd & STOP_CMD) || slave_ack_r));
assign rack_stop = (state_c == RACK ) && (end_cnt_time && (i2c_cmd & STOP_CMD));
assign stop_idle = (state_c == STOP ) && (end_cnt_time);
assign idle_write = (state_c == IDLE ) && (start && (i2c_cmd & WRITE_CMD));
assign idle_read = (state_c == IDLE ) && (start && (i2c_cmd & READ_CMD));
assign wack_idle = (state_c == WACK ) && (end_cnt_time);
assign rack_idle = (state_c == RACK ) && (end_cnt_time);
//其他注释
//cnt_time是用于产生SCL的计数器,end_cnt_time表示一个SCL时钟周期的结束
//cnt_bit是用于计数发送的bit数的计数器,end_cnt_bit表示8个bit发完
模块设计时的特殊处理
START信号
在控制模块中,为了使用方便,i2c_start信号一般持续为高电平。但是在i2c_end信号发出后,控制模块还没来得及更新命令及数据,状态机就会因为i2c_start持续为高而再次启动。这显然不是我们希望的,因为这会导致一些命令发送两次,一些命令完全不发。
为了应对这种情况,我们对i2c_start进行了一些处理:
reg start;
always@(posedge clk or negedge rst_n) begin
if(!rst_n)
start <= 'd0;
else if(state_c == IDLE && i2c_start)
start <= 'd1;
else if(state_c != IDLE)
start <= 'd0;
end
这种写法达成了两个效果:
-
通过将i2c_start延时一个周期,给了控制模块改变数据的时间
-
保证start是一个脉冲信号,而不是hold型的信号
经过修改后,i2c_start有了更高的自由度,可兼容脉冲和hold信号。
I2C_end信号
assign i2c_end = wack_idle || rack_idle || stop_idle;
先来分析一下原本的写法:如果发送的命令为{READ_CMD|STOP_CMD}
,那么状态跳转条件中的:
//部分状态跳转条件
assign rack_stop = (state_c == RACK ) && (end_cnt_time && (i2c_cmd & STOP_CMD));
assign rack_idle = (state_c == RACK ) && (end_cnt_time);
assign stop_idle = (state_c == STOP ) && (end_cnt_time);
rack_stop和rack_idle会同时拉高,之后stop_idle也会拉高,这会造成一次传输中i2c_end拉高两次。这虽然可以在控制模块的状态机设计中做出错误规避,但这依旧是危险的。
因此,我们对输出的i2c_end做一些处理:
reg i2c_end_r ;
always@(*) begin
if(i2c_cmd & STOP_CMD)
if(stop_idle)
i2c_end_r = 1'b1;
else
i2c_end_r = 1'b0;
else
i2c_end_r = wack_idle || rack_idle;
end
assign i2c_end = i2c_end_r;
这种写法约束了三种结束信号的优先级:
-
当有STOP命令时,只有stop_idle才能发出i2c_end
-
没有STOP命令时,wack_idle和rack_idle才能发出i2c_end
模块完整代码
module i2c_master
(
input clk ,
input rst_n ,
input i2c_start ,
input [7:0] i2c_data_in ,
input [3:0] i2c_cmd ,
output i2c_scl ,
input i2c_sda_in ,
output i2c_sda_out ,
output i2c_sda_en ,
output [7:0] dout ,
output i2c_end
);
//输出寄存
reg scl_r ;
reg sda_out_r ;
reg sda_out_en_r ;
reg slave_ack_r ;
reg [7:0] dout_r ;
reg i2c_end_r ;
//参数定义
parameter SCL_TIME = 250,//200Khz
DATA_WIDTH = 8;
localparam SCL_HALF = SCL_TIME / 2,
SCL_LOW = SCL_TIME / 4,
SCL_HIGH = SCL_TIME - SCL_LOW;
localparam START_CMD = 4'b0001,
WRITE_CMD = 4'b0010,
READ_CMD = 4'b0100,
STOP_CMD = 4'b1000;
//计数器定义
reg [7:0] cnt_time;
wire add_cnt_time,
end_cnt_time;
reg [2:0] cnt_bit;
wire add_cnt_bit,
end_cnt_bit;
//其他信号
reg start;
//状态机参数定义
reg [6:0] state_c,
state_n;
localparam IDLE = 7'b0000_001,
START = 7'b0000_010,
WRITE = 7'b0000_100,
WACK = 7'b0001_000,
READ = 7'b0010_000,
RACK = 7'b0100_000,
STOP = 7'b1000_000;
wire idle_start ,
start_write ,
start_read ,
write_wack ,
read_rack ,
wack_stop ,
rack_stop ,
stop_idle ,
idle_write ,
idle_read ,
wack_idle ,
rack_idle ;
//状态机状态转移
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
state_c <= IDLE;
else
state_c <= state_n;
end
always@(*)begin
case (state_c)
IDLE :begin
if(idle_start)
state_n = START;
else if(idle_write)
state_n = WRITE;
else if(idle_read)
state_n = READ;
else
state_n = state_c;
end
START:begin
if(start_write)
state_n = WRITE;
else if(start_read)
state_n = READ;
else
state_n = state_c;
end
WRITE:begin
if(write_wack)
state_n = WACK;
else
state_n = state_c;
end
WACK :begin
if(wack_stop)
state_n = STOP;
else if(wack_idle)
state_n = IDLE;
else
state_n = state_c;
end
READ :begin
if(read_rack)
state_n = RACK;
else
state_n = state_c;
end
RACK :begin
if(rack_stop)
state_n = STOP;
else if(rack_idle)
state_n = IDLE;
else
state_n = state_c;
end
STOP :begin
if(stop_idle)
state_n = IDLE;
else
state_n = state_c;
end
default: state_n = IDLE;
endcase
end
assign idle_start = (state_c == IDLE ) && (start && (i2c_cmd & START_CMD));
assign start_write = (state_c == START ) && (end_cnt_time && (i2c_cmd & WRITE_CMD));
assign start_read = (state_c == START ) && (end_cnt_time && (i2c_cmd & READ_CMD));
assign write_wack = (state_c == WRITE ) && (end_cnt_bit);
assign read_rack = (state_c == READ ) && (end_cnt_bit);
assign wack_stop = (state_c == WACK ) && (end_cnt_time && ((i2c_cmd & STOP_CMD) || slave_ack_r));
assign rack_stop = (state_c == RACK ) && (end_cnt_time && (i2c_cmd & STOP_CMD));
assign stop_idle = (state_c == STOP ) && (end_cnt_time);
assign idle_write = (state_c == IDLE ) && (start && (i2c_cmd & WRITE_CMD));
assign idle_read = (state_c == IDLE ) && (start && (i2c_cmd & READ_CMD));
assign wack_idle = (state_c == WACK ) && (end_cnt_time);
assign rack_idle = (state_c == RACK ) && (end_cnt_time);
//计数器
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
cnt_time <= 1'b0;
else if(add_cnt_time)
if(end_cnt_time)
cnt_time <= 1'b0;
else
cnt_time <= cnt_time + 1'b1;
else
cnt_time <= cnt_time;
end
assign add_cnt_time = (state_c != IDLE);
assign end_cnt_time = add_cnt_time && cnt_time >= SCL_TIME - 1'b1;
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
cnt_bit <= 1'b0;
else if(add_cnt_bit)
if(end_cnt_bit)
cnt_bit <= 1'b0;
else
cnt_bit <= cnt_bit + 1'b1;
else
cnt_bit <= cnt_bit;
end
assign add_cnt_bit = ((state_c == WRITE) || (state_c == READ)) && end_cnt_time;
assign end_cnt_bit = add_cnt_bit && cnt_bit >= DATA_WIDTH - 1'b1;
//启动缓存
always@(posedge clk or negedge rst_n) begin
if(!rst_n)
start <= 'd0;
else if(state_c == IDLE && i2c_start)
start <= 'd1;
else if(state_c != IDLE)
start <= 'd0;
end
//SCL输出定义
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
scl_r <= 1'b1;
else if(add_cnt_time && (cnt_time <= SCL_HALF))
scl_r <= 1'b0;
else if(add_cnt_time && (cnt_time > SCL_HALF))
scl_r <= 1'b1;
else
scl_r <= 1'b1;
end
assign i2c_scl = scl_r;
//sda_out_en输出定义
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
sda_out_en_r <= 'b0;
else if((state_c == START) || (state_c == WRITE) || (state_c == RACK) || (state_c == STOP))
sda_out_en_r <= 'b1;
else
sda_out_en_r <= 'b0;
end
assign i2c_sda_en = sda_out_en_r;
//sda_out输出定义
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
sda_out_r <= 'b1;
else if(state_c == START)begin
if(cnt_time <= SCL_HIGH)
sda_out_r <= 'b1;
else
sda_out_r <= 'b0;
end
else if(state_c == WRITE)begin
if(cnt_time == SCL_LOW)
sda_out_r <= i2c_data_in[DATA_WIDTH-1-cnt_bit];
else
sda_out_r <= sda_out_r;
end
else if(state_c == RACK)begin
if(cnt_time <= SCL_LOW)
sda_out_r <= sda_out_r;
else if(i2c_cmd & STOP_CMD)
sda_out_r <= 'b1;
else
sda_out_r <= 'b0;
end
else if(state_c == STOP)begin
if(cnt_time <= SCL_HIGH-19)
sda_out_r <= 'b0;
else
sda_out_r <= 'b1;
end
else
sda_out_r <= 'b1;
end
assign i2c_sda_out = sda_out_r;
//slave_ack输出定义
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
slave_ack_r <= 'd0;
else if((state_c == WACK) && (cnt_time == SCL_HIGH))
slave_ack_r <= i2c_sda_in;
else
slave_ack_r <= slave_ack_r;
end
//dout输出定义
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
dout_r <= 'd0;
else if((state_c == READ) && (cnt_time == SCL_HIGH))
dout_r[DATA_WIDTH-1-cnt_bit] <= i2c_sda_in;
else
dout_r <= dout_r;
end
assign dout = i2c_end ? dout_r : dout;
always@(*) begin
if(i2c_cmd & STOP_CMD)
if(stop_idle)
i2c_end_r = 1'b1;
else
i2c_end_r = 1'b0;
else
i2c_end_r = wack_idle || rack_idle;
end
assign i2c_end = i2c_end_r;
endmodule
其他补充
这一模块只是一个I2C接口,本身不能达成什么功能,需要搭配相应的控制模块才可以正常工作。控制模块根据不同的器件有不同的要求和写法,之后以OV5640的初始化来进行实例展示。