IIC通信协议
模块框图及输入输出信号
框图表示
输入输出信号解释
输入:
时钟信号
复位信号
使能信号
从机地址
从机寄存器地址
需要写入的数据
输出:
scl
sda
O_done_flag是主机(FPGA)发送一个字节完成标志位,发送完成后会产生一个高脉冲;
实现难点
- 有限状态机保证写时序操作状态的有序进行
- 由于IIC时序要求数据线SDA在串行时钟线的高电平保持不变,在串行时钟线的低电平才能变化,所以代码里面必须在串行时钟线低电平的正中间产生一个标志位,写代码的时候在这个标志位处改变SDA的值,这样就可以保证SDA在SCL的高电平期间保持稳定了。同理,由于IIC从机(24LC04)在接收到主机(FPGA)发送的有效数据以后会在SCL高电平期间产生一个有效应答信号0,所以为了保证采到的应答信号准确,必须在SCL高电平期间的正中间判断应答信号是否满足条件(0为有效应答,1为无效应答),因此代码里面还必须在串行时钟线高电平的正中间产生一个标志位,在这个标志下接收应答位并进行校验。产生上升沿正中间标志位检测ACK应答信号,产生下降沿正中间标志为改变SDK数据信号——利用寄存器存入常量
- 有了SCL信号低电平正中间标志位和高电平正中间标志位以后最好还产生一个下降沿的标志位。原因是在发送第一个8-bit数据以后,处理这个8-bit数据应答位的位置在SCL信号高电平的正中间,由于要复用发送8-bit数据的那个状态,所以必须在第二次进入发送8-bit数据的状态时必须提前把数据再次加载好,因此可以在这个下降沿的标志来加载第二次要发送的数据,然后在SCL下降沿的正中间把8-bit数据发出去。这里必须结合代码来理解,这里可以暂时有个印象。产生下降沿标志位,判断是否要进入写入数据的循环——利用寄存器存入常量
- IIC总线的SDA数据线是一个双向IO口,关于双向IO在Verilog代码中如何进行处理双向I/O口的VERILOG的处理——三态门高电平使能输出,寄存器作为输出位
- 进行发送状态的处理——设置STATE_JUMP标志寄存器
代码实现
这个代码是用VIVADO写的,适用于zybo板
module IIC_SEND(
input I_clk , // 系统50MHz时钟!
input I_rst_n , // 系统全局复位
input I_iic_send_en , // IIC发送使能位
// input [6:0] I_dev_addr , // IIC设备的物理地址,单个设备地址确定
input [7:0] I_word_addr , // IIC设备的字地址,即我们想操作的IIC的内部地址
input [7:0] I_write_data , // 往IIC设备的字地址写入的数据
output reg O_done_flag , // 读或写IIC设备结束标志位
// 标准的IIC设备总线
output O_scl , // IIC总线的串行时钟线
inout IO_sda // IIC总线的双向数据线
// output [9:0]R_scl_cnt //检验信号
);
parameter C_DIV_SELECT = 12'b100111000100 ; // 分频系数选择2500,T=20us,f=50000hz
//parameter C_DIV_SELECT = 12'd16;
parameter C_DIV_SELECT0 = (C_DIV_SELECT >> 2) - 1 , // 用来产生IIC总线SCL低电平最中间的标志位
C_DIV_SELECT1 = (C_DIV_SELECT >> 1) - 1 ,
C_DIV_SELECT2 = (C_DIV_SELECT0 + C_DIV_SELECT1) + 1 , // 用来产生IIC总线SCL高电平最中间的标志位
C_DIV_SELECT3 = (C_DIV_SELECT >> 1) + 1 ; // 用来产生IIC总线SCL下降沿标志位
reg [11:0] R_scl_cnt ; // 用来产生IIC总线SCL时钟线的计数器
reg R_scl_en ; // IIC总线SCL时钟线使能信号
reg [3:0] R_state ; //状态
reg R_sda_mode ; // 设置SDA模式,1位输出,0为输入,INOUT模式控制信号
reg R_sda_reg ; // SDA寄存器
reg [7:0] R_load_data ; // 发送/接收过程中加载的数据,比如设备物理地址,字地址和数据等
reg [3:0] R_bit_cnt ; // 发送字节状态中bit个数计数
reg R_ack_flag ; // 应答标志,接受从机应答标志
reg [3:0] R_jump_state ; // 跳转状态,传输一个字节成功并应答以后通过这个变量跳转到导入下一个数据的状态,标志进行到发送的哪一步
wire W_scl_low_mid ; // SCL的低电平中间标志位
wire W_scl_high_mid ; // SCL的高电平中间标志位
wire W_scl_neg ; // SCL的下降沿标志位
assign IO_sda = (R_sda_mode == 1'b1) ? R_sda_reg : 1'bz ;//标志位为1的时候输出!!!实现INOUT口
always @(posedge I_clk or posedge I_rst_n)//低电平复位->高电平复位
begin
if(I_rst_n==1)//低电平锁住时钟寄存器,一直为0
R_scl_cnt <= 12'd0 ;
else //在判断锁屏器之后才开始判断使能信号
if(R_scl_en) //判断时钟使能信号,时钟使能
begin
if(R_scl_cnt == C_DIV_SELECT - 1'b1)//时钟分频
R_scl_cnt <= 12'd0 ;
else
R_scl_cnt <= R_scl_cnt + 1'b1 ; //产生时钟信号
end
else
R_scl_cnt <= 12'd0 ;//时钟没有被释放
end
assign O_scl = (R_scl_cnt <= C_DIV_SELECT1) ? 1'b1 : 1'b0 ; // 产生串行时钟信号O_scl
assign W_scl_low_mid = (R_scl_cnt == C_DIV_SELECT2) ? 1'b1 : 1'b0 ; // 产生scl低电平正中间标志位,改变传输的数据信号
assign W_scl_high_mid = (R_scl_cnt == C_DIV_SELECT0) ? 1'b1 : 1'b0 ; // 产生scl高电平正中间标志位,接受ACK应答信号
assign W_scl_neg = (R_scl_cnt == C_DIV_SELECT3) ? 1'b1 : 1'b0 ; // 产生scl下降沿标志位
always @(posedge I_clk or posedge I_rst_n)//全部为高电平有效
begin
if(I_rst_n==1)//复位信号
begin//给所有寄存器赋初值
R_state <= 4'd0 ;
R_sda_mode <= 1'b1 ;//输出状态
R_sda_reg <= 1'b1 ;
R_bit_cnt <= 4'd0 ;
O_done_flag <= 1'b0 ;
R_jump_state <= 4'd0 ;
R_ack_flag <= 1'b0 ;
end
else
if(I_iic_send_en) // 往IIC设备发送数据,发送位使能
begin
case(R_state)
4'd0 : // 空闲状态设置SCL与SDA均为高
begin
R_sda_mode <= 1'b1 ; // 设置SDA为输出
R_sda_reg <= 1'b1 ; // 设置SDA为高电平
R_scl_en <= 1'b0 ; // 关闭SCL时钟线,不占用时钟,时钟线拉高
R_state <= 4'd1 ; // 下一个状态是加载设备物理地址状态
R_bit_cnt <= 4'd0 ; // 发送字节状态中bit个数计数清零
O_done_flag <= 1'b0 ; //完成标志位
R_jump_state <= 4'd0 ;
end
4'd1 : // 加载IIC设备物理地址
begin
R_load_data <= 8'b10000000 ;//后加0表示写信号
R_state <= 4'd4 ;//起始信号
R_jump_state <= 4'd2 ;//加载完物理地址后进入到发送设备地址单元
end
4'd2 : // 加载IIC设备字地址
begin
R_load_data <= I_word_addr ;
R_state <= 4'd5 ;
R_jump_state <= 4'd3 ;
end
4'd3 : // 加载要发送的数据
begin
R_load_data <= I_write_data ;
R_state <= 4'd5 ;
R_jump_state <= 4'd8 ;
end
4'd4 : // 发送起始信号 发送状态都要打开时钟线和设置sda为输出模式
begin
R_scl_en <= 1'b1 ; // 打开SCL时钟线
R_sda_mode <= 1'b1 ; // 设置SDA为输出
if(W_scl_high_mid)//高电平中间
begin
R_sda_reg <= 1'b0 ; // 在SCL高电平中间把SDA信号拉低,产生起始信号
R_state <= 4'd5 ; //发送完起始信号进入发送字节信号
end
else
R_state <= 4'd4 ; // 如果SCL高电平中间标志没出现就一直在这个状态等着
end
4'd5 : // 发送1个字节,从高位开始发
begin
R_scl_en <= 1'b1 ; // 打开SCL时钟线
R_sda_mode <= 1'b1 ; // 设置SDA为输出
if(W_scl_low_mid)//在时钟信号的低位中间时进行数据位的改变
begin
if(R_bit_cnt == 4'd8)//观察已经发送了多少位bit
begin
R_bit_cnt <= 4'd0 ;
R_state <= 4'd6 ; // 字节发完以后进入应答状态
end
else
begin
R_sda_reg <= R_load_data[7-R_bit_cnt] ; // 先发送高位
R_bit_cnt <= R_bit_cnt + 1'b1 ;
end
end
else
R_state <= 4'd5 ; // 字节没发完时在这个状态一直等待
end
4'd6 : // 接收应答状态的应答位
begin
R_scl_en <= 1'b1 ; // 打开SCL时钟线
R_sda_mode <= 1'b0 ; // 设置SDA为输入!接受应答信号
if(W_scl_high_mid)
begin
R_ack_flag <= IO_sda ;
R_state <= 4'd7 ;
end
else
R_state <= 4'd6 ;
end
4'd7 : // 校验应答位
begin
R_scl_en <= 1'b1 ; // 打开SCL时钟线
if(R_ack_flag == 1'b0) // 校验通过
begin
if(W_scl_neg == 1'b1)
begin
R_state <= R_jump_state ;
R_sda_mode <= 1'b1 ; // 设置SDA的模式为输出
R_sda_reg <= 1'b0 ; // 读取完应答信号以后要把SDA信号设置成输出并拉低,因为如果这个状
// 态后面是停止状态的话,需要SDA信号的上升沿,所以这里提前拉低它
end
else
R_state <= 4'd7 ;
end
else
R_state <= 4'd0 ;
end
4'd8 : // 发送停止信号
begin
R_scl_en <= 1'b1 ; // 打开SCL时钟线
R_sda_mode <= 1'b1 ; // 设置SDA为输出
if(W_scl_high_mid)
begin
R_sda_reg <= 1'b1 ;
R_state <= 4'd9 ;
end
end
4'd9 : // IIC写操作结束
begin
R_scl_en <= 1'b0 ; // 关闭SCL时钟线
R_sda_mode <= 1'b1 ; // 设置SDA为输出
R_sda_reg <= 1'b1 ; // 拉高SDA保持空闲状态情况
O_done_flag <= 1'b1 ;
R_state <= 4'd0 ;
R_ack_flag <= 1'b0 ;
end
default : R_state <= 4'd0 ;
endcase
end
else
begin
R_state <= 4'd0 ;
R_sda_mode <= 1'b1 ;
R_sda_reg <= 1'b1 ;
R_bit_cnt <= 4'd0 ;
O_done_flag <= 1'b0 ;
R_jump_state <= 4'd0 ;
R_ack_flag <= 1'b0 ;
end
end
endmodule