LinCoding

【原创】详细解析基于FPGA的IIC程序

0
阅读(9761)

【主题】:详细解析基于FPGA的IIC程序

【作者】:LinCoding

【时间】:2016.12.04

【声明】:转载、引用,请注明出处

今天把IIC搞定了,IIC可以说是太重要了,很多IC都是IIC协议驱动的,IIC就是两根线一根SCL,一根SDA,下面就介绍下IIC的Verilog实现。

先上图:

笔者是将Microchip公司的24LC04这个EEPROM作为IIC slave作为测试,在单字节读写模式下,向24LC04的8'h01寄存器写入8'hBB数据,然后再读出来送到串口做显示。可见下图,数据十分稳定。

blob.png

下图为IIC的写时序,因为板子上没有引出EEPROM的引脚,因此无法用示波器抓取读时序,此外,在数据抖动时,其实是IIC master已经释放总线等待slave的ACK信号。

blob.png

(移植、修改自CrzayBingo的源码程序)

module iic_driver #( parameter CLK_FREQ = 27'd100_000_000, //100Mhz parameter IIC_FREQ = 27'd1_000 //100Khz ( < 400Khz ) ) ( input clk, //global clock input rst_n, //global reset input [1:0] iic_cmd, //read and write command, 10 write, 01 read input [7:0] iic_dev_addr, //IIC device address input [7:0] iic_reg_addr, //IIC register address input [7:0] iic_wr_data, //IIC write data output reg [7:0] iic_rd_data, //IIC read data output device_done, output reg iic_ack, output iic_sclk, //IIC SCLK inout iic_sdat //IIC SDA );

第一部分是输入输出定义,需要注意两点:

1、第一个parameter后需要有一个逗号,第二个parameter后什么也不需要。

2、iic_sdat就是SDA,因此为inout类型。

//----------------------------------- //delay 1ms for IIC slave is steady localparam DELAY_TOP = CLK_FREQ/1000; //delay 1ms //localparam DELAY_TOP = 100; //just for simulation reg [16:0] delay_cnt; always @ ( posedge clk or negedge rst_n ) begin if ( ! rst_n ) delay_cnt <= 17'd0; else if ( delay_cnt < DELAY_TOP ) delay_cnt <= delay_cnt + 1'b1; else delay_cnt <= delay_cnt; end wire delay_done = ( delay_cnt == DELAY_TOP ) ? 1'b1 : 1'b0; assign device_done = delay_done;

第二部分是个计数器,由于一些IIC slave器件在上电后需要一段时间的准备时间,因此,这里定义了一个1ms的计数器,用于满足这些器件的准备时间。并且将delay_done赋值给device_done以表示器件已经准备就绪,可以开始读写操作了。

//----------------------------------- //generate IIC control clock reg [16:0] clk_cnt; always @ ( posedge clk or negedge rst_n ) begin if ( ! rst_n ) clk_cnt <= 17'd0; else if ( delay_done ) begin if ( clk_cnt < ( CLK_FREQ / IIC_FREQ ) ) clk_cnt <= clk_cnt + 1'b1; else clk_cnt <= 17'd1; end else clk_cnt <= 17'd0; end wire iic_ctrl_clk = ( ( clk_cnt >= ( CLK_FREQ/IIC_FREQ ) / 4 + 1'b1 ) && ( clk_cnt < ( ( CLK_FREQ/IIC_FREQ ) / 4 ) * 3 + 1'b1 ) ) ? 1'b1 : 1'b0; wire iic_transfer_en = ( clk_cnt == 16'd1 ) ? 1'b1 : 1'b0; wire iic_capture_en = ( clk_cnt == ( ( CLK_FREQ/IIC_FREQ ) / 4 ) * 2 ) ? 1'b1 : 1'b0;

第三部分则是生成IIC所需要的时钟,还有读写使能信号,需要注意的是:

1、根据模块开头的两个paramter的定义可直接计算所需计数总数,然后生成所需要的时钟,IIC的时钟规定在100Khz至400Khz之间,太快肯定不行,太慢的话有些器件也不行(笔者测试EEPROM在10Khz时无法正常读写)

2、由于IIC协议规定,数据在高电平期间有效,所以如果是写IIC slave,必须在时钟低电平时将数据设置好,高电平时IIC slave会来读数据;如果是读数据,则必须在时钟高电平时读取数据。因此设定,iic_transger_en为低电平正中间,用于写II slave时作为设置数据的使能信号,iic_capture_en为高电平正中间,用于读IIC slave时的采样使能信号。

3、其实,这部分代码与——《详细解析74HC595驱动程序》这篇文章中生成74HC595时钟的那部分代码非常相似,这也就是告我我们,其实Verilog的程序只要掌握了固定的套路,其实并不难。

下图为上述代码所生成的波形:

blob.png

接下来就是一个标准的三段式FSM了,可参考——《详细解析基于三段式状态机的流水灯》、《详细解析基于FPGA的LCD1602驱动控制》:

//----------------------------------- //FSM encode localparam IIC_IDLE = 5'd0; //IIC Write encode localparam IIC_WR_START = 5'd1; localparam IIC_WR_IDADDR = 5'd2; //device address,write(0) localparam IIC_WR_ACK1 = 5'd3; localparam IIC_WR_REGADDR = 5'd4; //register address localparam IIC_WR_ACK2 = 5'd5; localparam IIC_WR_REGDATA = 5'd6; //register data localparam IIC_WR_ACK3 = 5'd7; localparam IIC_WR_STOP = 5'd8; //IIC Read encode localparam IIC_RD_START1 = 5'd9; localparam IIC_RD_IDADDR1 = 5'd10; //device address,write(0) localparam IIC_RD_ACK1 = 5'd11; localparam IIC_RD_REGADDR = 5'd12; //register address localparam IIC_RD_ACK2 = 5'd13; localparam IIC_RD_STOP1 = 5'd14; localparam IIC_RD_IDLE = 5'd15; localparam IIC_RD_START2 = 5'd16; localparam IIC_RD_IDADDR2 = 5'd17; //device address,read(1) localparam IIC_RD_ACK3 = 5'd18; localparam IIC_RD_REGDATA = 5'd19; //register data localparam IIC_RD_NACK = 5'd20; localparam IIC_RD_STOP2 = 5'd21;

第四部分是三段式FSM的状态编码,采用十进制编码方式。

//----------------------------------- //FSM always 1 reg [4:0] current_state; reg [4:0] next_state; always @ ( posedge clk or negedge rst_n ) begin if ( ! rst_n ) current_state <= IIC_IDLE; else if ( iic_transfer_en ) current_state <= next_state; else current_state <= current_state; end

第五部分,三段式FSM第一段,该说的在上文推荐的那两篇文章中已经说得非常详细了。

//----------------------------------- //FSM always 2 reg [3:0] iic_stream_cnt; always @ ( * ) begin next_state = IIC_IDLE; case ( current_state ) IIC_IDLE : //5'd0 if ( iic_cmd == 2'b01 ) //01 read next_state <= IIC_RD_START1; //5'd9 else if ( iic_cmd == 2'b10 ) //10 write next_state <= IIC_WR_START; //5'd1 else next_state <= IIC_IDLE; //IIC Write: { Device_Address(Write) + Register_Address + Write_Data } IIC_WR_START : //5'd1 if ( iic_transfer_en ) next_state <= IIC_WR_IDADDR; else next_state <= IIC_WR_START; IIC_WR_IDADDR : //5'd2 if ( iic_transfer_en && iic_stream_cnt == 4'd8 ) next_state <= IIC_WR_ACK1; else next_state <= IIC_WR_IDADDR; IIC_WR_ACK1 : //5'd3 if ( iic_transfer_en ) next_state <= IIC_WR_REGADDR; else next_state <= IIC_WR_ACK1; IIC_WR_REGADDR : //5'd4 if ( iic_transfer_en && iic_stream_cnt == 4'd8 ) next_state <= IIC_WR_ACK2; else next_state <= IIC_WR_REGADDR; IIC_WR_ACK2 : //5'd5 if ( iic_transfer_en ) next_state <= IIC_WR_REGDATA; else next_state <= IIC_WR_ACK2; IIC_WR_REGDATA : //5'd6 if ( iic_transfer_en && iic_stream_cnt == 4'd8 ) next_state <= IIC_WR_ACK3; else next_state <= IIC_WR_REGDATA; IIC_WR_ACK3 : //5'd7 if ( iic_transfer_en ) next_state <= IIC_WR_STOP; else next_state <= IIC_WR_ACK3; IIC_WR_STOP : //5'd8 if ( iic_transfer_en ) next_state <= IIC_IDLE; else next_state <= IIC_WR_STOP; //IIC Read: { Device_Address(Write) + Regis_Address + Device_Address(Read) + Read_Data } IIC_RD_START1 : //5'd8 if ( iic_transfer_en ) next_state <= IIC_RD_IDADDR1; else next_state <= IIC_RD_START1; IIC_RD_IDADDR1 : if ( iic_transfer_en && iic_stream_cnt == 4'd8 ) next_state <= IIC_RD_ACK1; else next_state <= IIC_RD_IDADDR1; IIC_RD_ACK1 : if ( iic_transfer_en ) next_state <= IIC_RD_REGADDR; else next_state <= IIC_RD_ACK1; IIC_RD_REGADDR : if ( iic_transfer_en && iic_stream_cnt == 4'd8 ) next_state <= IIC_RD_ACK2; else next_state <= IIC_RD_REGADDR; IIC_RD_ACK2 : if ( iic_transfer_en ) next_state <= IIC_RD_STOP1; else next_state <= IIC_RD_ACK2; IIC_RD_STOP1 : if ( iic_transfer_en ) next_state <= IIC_RD_IDLE; else next_state <= IIC_RD_STOP1; IIC_RD_IDLE : if ( iic_transfer_en ) next_state <= IIC_RD_START2; else next_state <= IIC_RD_IDLE; IIC_RD_START2 : if ( iic_transfer_en ) next_state <= IIC_RD_IDADDR2; else next_state <= IIC_RD_START2; IIC_RD_IDADDR2 : if ( iic_transfer_en && iic_stream_cnt == 4'd8 ) next_state <= IIC_RD_ACK3; else next_state <= IIC_RD_IDADDR2; IIC_RD_ACK3 : if ( iic_transfer_en ) next_state <= IIC_RD_REGDATA; else next_state <= IIC_RD_ACK3; IIC_RD_REGDATA : if ( iic_transfer_en && iic_stream_cnt == 4'd8 ) next_state <= IIC_RD_NACK; else next_state <= IIC_RD_REGDATA; IIC_RD_NACK : if ( iic_transfer_en ) next_state <= IIC_RD_STOP2; else next_state <= IIC_RD_NACK; IIC_RD_STOP2 : if ( iic_transfer_en ) next_state <= IIC_IDLE; else next_state <= IIC_RD_STOP2; default: next_state <= IIC_IDLE; endcase end

第六部分,三段式FSM第二段,别看这么多,都是重复的内容罢了,需要注意三点:

1、在IIC_IDLE态,会根据输入的命令来判定,执行相应的读或者写操作。

2、iic_stream_cnt是计数当前发送了几位数据的计数器。

3、所有的信号转移状态都以iic_transfer_en为使能信号。

//----------------------------------- //FSM always 3 reg iic_sdat_out; reg [7:0] iic_wdata; always @ ( posedge clk or negedge rst_n ) begin if ( ! rst_n ) begin iic_sdat_out <= 1'b1; iic_stream_cnt <= 4'd0; iic_wdata <= 8'd0; end else if ( iic_transfer_en ) case ( next_state ) IIC_IDLE : //5'd0 begin iic_sdat_out <= 1'b1; iic_stream_cnt <= 4'd0; iic_wdata <= 8'd0; end //IIC Write: {Device_Address + REG_Address + Write_Data} IIC_WR_START : //5'd1 begin iic_sdat_out <= 1'b0; iic_stream_cnt <= 4'd0; iic_wdata <= iic_dev_addr; end IIC_WR_IDADDR : //5'd2 begin iic_stream_cnt <= iic_stream_cnt + 1'b1; iic_sdat_out <= iic_wdata[3'd7 - iic_stream_cnt]; end IIC_WR_ACK1 : //5'd3 begin iic_stream_cnt <= 4'd0; iic_wdata <= iic_reg_addr; end IIC_WR_REGADDR : //5'd4 begin iic_stream_cnt <= iic_stream_cnt + 1'b1; iic_sdat_out <= iic_wdata[3'd7 - iic_stream_cnt]; end IIC_WR_ACK2 : //5'd5 begin iic_stream_cnt <= 4'd0; iic_wdata <= iic_wr_data; end IIC_WR_REGDATA : //5'd6 begin iic_stream_cnt <= iic_stream_cnt + 1'b1; iic_sdat_out <= iic_wdata[3'd7 - iic_stream_cnt]; end IIC_WR_ACK3 : //5'd7 begin iic_stream_cnt <= 4'd0; end IIC_WR_STOP : //5'd8 begin iic_sdat_out <= 1'b0; end //IIC Read: {Device_Address + REG_Address} + {Device_Address + R_REG_Data} IIC_RD_START1 : //5'd9 begin iic_sdat_out <= 1'b0; iic_stream_cnt <= 4'd0; iic_wdata <= iic_dev_addr; end IIC_RD_IDADDR1 : //5'd10 begin iic_stream_cnt <= iic_stream_cnt + 1'b1; iic_sdat_out <= iic_wdata[3'd7 - iic_stream_cnt]; end IIC_RD_ACK1 : //5'd11 begin iic_stream_cnt <= 4'd0; iic_wdata <= iic_reg_addr; end IIC_RD_REGADDR : //5'd12 begin iic_stream_cnt <= iic_stream_cnt + 1'b1; iic_sdat_out <= iic_wdata[3'd7 - iic_stream_cnt]; end IIC_RD_ACK2 : //5'd13 begin iic_stream_cnt <= 4'd0; end IIC_RD_STOP1 : //5'd14 begin iic_sdat_out <= 1'b0; end IIC_RD_IDLE : //5'd15 begin iic_sdat_out <= 1'b1; end IIC_RD_START2 : //5'd16 begin iic_sdat_out <= 1'b0; iic_stream_cnt <= 4'd0; iic_wdata <= iic_dev_addr | 8'h01; end IIC_RD_IDADDR2 : //5'd17 begin iic_stream_cnt <= iic_stream_cnt + 1'b1; iic_sdat_out <= iic_wdata[3'd7 - iic_stream_cnt]; end IIC_RD_ACK3 : //5'd18 begin iic_stream_cnt <= 4'd0; end IIC_RD_REGDATA : //5'd19 begin iic_stream_cnt <= iic_stream_cnt + 1'b1; end IIC_RD_NACK : //5'd20 begin iic_sdat_out <= 1'b1; //NACK end IIC_RD_STOP2 : //5'd21 begin iic_sdat_out <= 1'b0; end default: begin iic_sdat_out <= 1'b1; iic_stream_cnt <= 4'd0; iic_wdata <= 8'd0; end endcase else begin iic_stream_cnt <= iic_stream_cnt; iic_sdat_out <= iic_sdat_out; end end

第七部分,三段式FSM第三段的part1,需要注意的是:在IIC_RD_START2态,需要将iic_dev_addr | 8'h01,以将读写方向改成读。其他没什么可注意的,很简单。

//--------------------------------------------- //respone from slave for iic data transfer reg iic_ack1; reg iic_ack2; reg iic_ack3; always @ ( posedge clk or negedge rst_n ) begin if( ! rst_n ) begin iic_ack1 <= 1'b1; iic_ack2 <= 1'b1; iic_ack3 <= 1'b1; iic_ack <= 1'b1; iic_rd_data <= 8'd0; end else if( iic_capture_en ) case ( next_state ) IIC_IDLE: begin iic_ack1 <= 1'b1; iic_ack2 <= 1'b1; iic_ack3 <= 1'b1; iic_ack <= 1'b1; end //Write IIC: {ID_Address, REG_Address, W_REG_Data} IIC_WR_ACK1 : iic_ack1 <= iic_sdat; IIC_WR_ACK2 : iic_ack2 <= iic_sdat; IIC_WR_ACK3 : iic_ack3 <= iic_sdat; IIC_WR_STOP : iic_ack <= ( iic_ack1 | iic_ack2 | iic_ack3 ); //IIC Read: {ID_Address + REG_Address} + {ID_Address + R_REG_Data} IIC_RD_ACK1 : iic_ack1 <= iic_sdat; IIC_RD_ACK2 : iic_ack2 <= iic_sdat; IIC_RD_ACK3 : iic_ack3 <= iic_sdat; IIC_RD_STOP2 : iic_ack <= ( iic_ack1 | iic_ack2 | iic_ack3 ); IIC_RD_REGDATA : iic_rd_data<= { iic_rd_data[6:0], iic_sdat }; endcase else begin iic_ack1 <= iic_ack1; iic_ack2 <= iic_ack2; iic_ack3 <= iic_ack3; iic_ack <= iic_ack; end end

第八部分,三段式FSM第三段的part2,这部分主要是用来处理ACK信号,那么问题来了,为什么不把第三段的part1和part2合并起来呢?

原因是part1使能信号是iic_transfer_en,而part2都是用来处理“读”信号,因此使能信号为iic_capture_en,两者的使能信号不一样,必须分开来写;其次,将ACK信号单独写在一个always中更加利于维护,便于阅读。

//----------------------------------- //IIC signal wire read_en = ( current_state == IIC_WR_ACK1 || current_state == IIC_WR_ACK2 || current_state == IIC_WR_ACK3 || current_state == IIC_RD_ACK1 || current_state == IIC_RD_ACK2 || current_state == IIC_RD_ACK3 || current_state == IIC_RD_REGDATA ) ? 1'b1 : 1'b0; //release data bus assign iic_sclk = ( current_state >= IIC_WR_IDADDR && current_state <= IIC_WR_ACK3 || current_state >= IIC_RD_IDADDR1 && current_state <= IIC_RD_ACK2 || current_state >= IIC_RD_IDADDR2 && current_state <= IIC_RD_NACK ) ? iic_ctrl_clk : 1'b1; assign iic_sdat = ( ~ read_en ) ? iic_sdat_out : 1'bz;

第九部分,就是输出sclk和sdat信号了,由于sdat为inout类型,也就是三态门,因此必须指定一个使能信号,来确定sdat何时为输入,何时为输出——详细参见《LinCoding告诉您什么才是IO口》这篇文章。

总之就是,在需要读取时,选择sdat为读入状态,在需要写入时,选择sdat为输出状态。

--------------------------------------------------------------------------------------

这样的话IIC的驱动程序就写完了,由于使用EEPROM作为IIC的测试slave,因此还需写EEPROM的程序。

module eeprom_test ( input clk, //global clock input rst_n, //global reset output reg [1:0] iic_cmd, //read and write command, 10 write, 01 read output reg [7:0] iic_dev_addr, //IIC device address output reg [7:0] iic_reg_addr, //IIC register address output reg [7:0] iic_wr_data, //IIC write data input [7:0] iic_rd_data, //IIC read data input device_done, input iic_ack, output reg [7:0] rxd_data, output reg rxd_flag ); localparam CLK_FREQ = 27'd100_000_000; //100Mhz localparam IIC_FREQ = 27'd1_000; //100Khz ( < 400Khz ) reg [16:0] delay_cnt; always @ ( posedge clk or negedge rst_n ) begin if ( ! rst_n ) delay_cnt <= 17'd0; else if ( delay_cnt < ( CLK_FREQ / IIC_FREQ ) ) delay_cnt <= delay_cnt + 1'b1; else delay_cnt <= 17'd1; end wire delay_done = ( delay_cnt == ( CLK_FREQ / IIC_FREQ ) ) ? 1'b1 : 1'b0; localparam IDLE = 3'd0; localparam WRITE = 3'd1; localparam WAIT_WR_ACK = 3'd2; localparam READ = 3'd3; localparam WAIT_RD_ACK = 3'd4; localparam STOP = 3'd5; reg [2:0] state; always @ ( posedge clk or negedge rst_n ) begin if ( ! rst_n ) begin iic_cmd <= 2'b00; iic_dev_addr <= 8'd0; iic_reg_addr <= 8'd0; iic_wr_data <= 8'd0; state <= IDLE; rxd_data <= 8'd0; rxd_flag <= 1'b0; end else if ( delay_done ) case ( state ) IDLE: if ( device_done ) state <= WRITE; else state <= IDLE; WRITE: begin iic_cmd <= 2'b10; iic_dev_addr <= 8'hA0; iic_reg_addr <= 8'h01; iic_wr_data <= 8'hBB; state <= WAIT_WR_ACK; end WAIT_WR_ACK: if ( ! iic_ack ) begin state <= READ; iic_cmd <= 2'b00; end else begin iic_cmd <= iic_cmd; state <= WAIT_WR_ACK; end READ: begin iic_cmd <= 2'b01; iic_dev_addr <= 8'hA0; iic_reg_addr <= 8'h01; state <= WAIT_RD_ACK; end WAIT_RD_ACK: if ( ! iic_ack ) begin rxd_data <= iic_rd_data; rxd_flag <= 1'b1; iic_cmd <= 2'b00; state <= STOP; end else begin iic_cmd <= iic_cmd; state <= WAIT_RD_ACK; end STOP: begin rxd_flag <= 1'b0; state <= STOP; end default: begin iic_dev_addr <= 8'd0; iic_reg_addr <= 8'd0; iic_wr_data <= 8'd0; state <= IDLE; end endcase end

这部分就不多做解释了,就是使用一个状态机来发送读写命令,很简单的。

最后,笔者在做仿真的时候使用了Microchip公司提供的24LC04的仿真模型,在其官网上可以下载,但问题是,这个仿真模型笔者的程序写入时序没有任何问题,ACK信号也可以有效的返回,但是写入的数据却不能返回,找不到问题所在,但是笔者下载到板子上,板级调试没有任何问题,不知道是仿真模型的问题还是什么问题。如下图所示,如果知道原因的可以和笔者交流。

blob.png

总结:至此就完成了IIC的测试程序,其实本程序可以学的新知识并不多,都是将之前一些小程序的思想运用进来,融会贯通,还是那句话,Verilog是有套路的,掌握了这些套路,写程序其实并不是很难!


Baidu
map