问答 店铺
热搜: ZYNQ FPGA discuz

QQ登录

只需一步,快速开始

微信登录

微信扫码,快速开始

微信扫一扫 分享朋友圈

已有 168 人浏览分享

开启左侧

[MLKPAI-F01-EG4D]FPGA程序设计基础实验连载-18 I2C MASTER控制器驱动设计

[复制链接]
168 0
安路-FPGA课程
安路课程: 基础入门 » 新手入门实验
安路系列: EG4
本帖最后由 UT发布 于 2025-4-19 09:56 编辑

软件版本:TD_5.6.4_Release_97693
操作系统:WIN11 64bit
硬件平台:适用安路(Anlogic)FPGA
登录米联客”FPGA社区-www.uisrc.com视频课程、答疑解惑!
1系统框图

I2C Master控制器主要包含I2C收发数据状态机,SCL时钟分频器、发送移位模块、接收移位模块、空闲控制忙指示模块。SCLSDA的输出逻辑和时序通过SCLI2C状态机控制。

image.jpg

重点介绍下其中的关键信号:

IO_sdaI2C双向数据总线

O_sclI2C时钟

I_wr_cnt写数据字节长度,包含了器件地址,发送I_iic_req前,预设该值

I_rd_cnt读数据字节长度,仅包含读回有效部分,发送I_iic_req前,预设该值

I_wr_data写入的数据

O_rd_data读出的数据,如果是读请求,当O_iic_busy从高变低代表数据读回有效

I_iic_req I2C操作请求,根据I_rd_cnt是否大于0决定是否有读请求

I_iic_mode是否支持随机读写,发送I_iic_req前,预设该值


O_iic_busy总线忙
1.1 程序设计
1.1.1空闲控制忙指示模块

为了方便使用该驱动程序,和前面学习的驱动一样,IIC master也使用I_iic_req以及O_iic_busy用于信号的握手。用户程序通过设置I_iic_req为高,请求/数据;设置O_iic_busy1表示IIC总线正忙,这时用户程序需要等待非忙的时候,请求/写下一个数据。

image.jpg
  1. //总线忙状态
  2. always @(posedge scl_clk or negedge I_rstn )begin
  3.     if(I_rstn == 1'b0)
  4.         O_iic_busy <= 1'b0;
  5.     else begin
  6.         if((I_iic_req == 1'b1 || rd_req == 1'b1 || O_iic_bus_error))//I_iic_req == 1'b1 || rd_req == 1'b1总线进入忙状态
  7.             O_iic_busy <= 1'b1;
  8.         else if(IIC_S == IDLE)
  9.             O_iic_busy <= 1'b0;
  10.     end         
  11. end
复制代码
1.1.2 SCL时钟分频器

下面我们以简单写1个字节来说明关键的时序顺序设计。

所有的控制逻辑以IIC_S状态机的状态,以及内部时钟scl_clk为主要时序来控制。写操作内部同步时序全部以scl_clk的上升沿进行,为了满足数据TsuThd,设O_iic_scl延迟于scl_r半周期的四分之三 OFFSET = CLK_DIV - CLK_DIV/4。这样对于SLAVE接收来说具有足够的TsuThd。对于读操作,每个scl_sck的下降沿采集总线,由于scl完成了相位调整,也是非常容易满足TsuThd。

image.jpg
  1. ocalparam SCL_DIV = CLK_DIV/2;
  2. localparam OFFSET = SCL_DIV - SCL_DIV/4;//设置I2C总线的SCL时钟的偏移,以满足SCL和SDA的时序要求,外部的SCL延迟内部的半周期的四分之三
  3. reg [15:0] clkdiv = 16'd0;   //I2C 时钟分频寄存器
  4. reg scl_r      = 1'b1;       //I2C控制器的SCL内部时钟
  5. reg scl_clk    = 1'b0;       //I2C控制器内部SCL时钟,与外部时钟存在OFFSET参数设置的相位偏移
  6. wire scl_offset;             //scl 时钟偏移控制
  7. //scl 时钟分频器
  8. always@(posedge I_clk)
  9.     if(clkdiv < SCL_DIV)   
  10.         clkdiv <= clkdiv + 1'b1;
  11.     else begin
  12.         clkdiv <= 16'd0;
  13.         scl_clk <= !scl_clk;
  14.     end
  15. assign  scl_offset  = (clkdiv == OFFSET);//设置scl_offset的时间参数
  16. always @(posedge I_clk) O_iic_scl <=  scl_offset ?  scl_r : O_iic_scl; //O_iic_scl延迟scl_offset时间的scl_r
复制代码
1.1.3开始条件/停止条件

SDA:总线空闲状态sda_o保持高电平,sda_o拉低,代表启动传输。

image.jpg
  1. //当进入IIC_S状态为启动、停止设置sda=0,结合scl产生起始位,或者(IIC_S == R_ACK && (rcnt != I_rd_cnt) sda=0,用于产生读操作的ACK
  2. always @(*)
  3.     if(IIC_S == START || IIC_S == STOP1 || (IIC_S == R_ACK && (rcnt != I_rd_cnt)))
  4.         sda_o <= 1'b0;
  5.     else if(IIC_S == W_WAIT)
  6.         sda_o <= sda_r[7];
  7.     else  sda_o <= 1'b1; //否则其他状态都为1,当(IIC_S == R_ACK && (rcnt == I_rd_cnt) 产生一个NACK
复制代码

SCL:设置scl_r开始和结束的条件为高电平,数据传输过程scl_r为内部时钟scl_clk

image.jpg
1.1.4发送移位模块

当总线进行写操作时,将I_wr_data的数据通过发送移位模块传送到IIC总线sda_r上。

  1. ocalparam START   = 4'd1;//I2C 总线启动
  2. localparam W_WAIT  = 4'd2;//I2C 总线等待写完成
  3. localparam W_ACK   = 4'd3;//I2C 总线等待写WACK
  4. reg [2:0] IIC_S = 4'd0; //I2C 状态机
  5. reg [7:0] wcnt = 8'd0;       //发送数据计数器,以byte为单位
  6. //I2C数据发送模块,所有的写数据都通过此模块发送
  7. always @(posedge scl_clk)
  8.     if(IIC_S == W_ACK || IIC_S == START)begin//IIC_S=START和W_ACK,把需要发送的数据,寄存到sda_r
  9.         sda_r <= I_wr_data[(wcnt*8) +: 8];//寄存需要发发送的数据到sda_r
  10.         if( rd_req ) sda_r <= {I_wr_data[7:1],1'b1};//对于读操作,rd_req由内部代码产生,当写完第一个数据(器件地址),后通过判断I_rd_cnt,确认是否数据需要读
  11.     end
  12.     else if(IIC_S == W_WAIT)//当W_WAT状态,通过移位操作,把数据发送到数据总线
  13.         sda_r <= {sda_r[6:0],1'b1};//移位操作
  14.     else
  15.         sda_r <= sda_r;
复制代码
1.1.5接收移位模块

当总线进行读操作时,将IIC总线上的数据sda_i_r通过接收移位模块传送到O_rd_data上。

  1. localparam IDLE    = 4'd0;//I2C 总线空闲状态
  2. localparam R_WAIT  = 4'd4;//I2C 总线等待读完成
  3. localparam R_ACK   = 4'd5;//I2C 总线等待读RACK
  4. reg [7:0] sda_i_r = 8'd0;    //接收寄存器
  5. reg [7:0] rcnt = 8'd0;       //接收数据计数器,以byte为单位
  6. //sda data bus read and hold data to O_rd_data register when IIC_S=R_ACK
  7. //I2C数据接收模块,I2C读期间,把数据通过移位操作,移入O_rd_data
  8. always @(negedge scl_clk)begin
  9.     if(IIC_S == R_WAIT ) //当IIC_S == R_WAIT ||IIC_S == W_ACK(如果读操作,第1个BIT是W_ACK这个状态读)启动移位操作
  10.         sda_i_r <= {sda_i_r[6:0],sda_i};
  11.     else if(IIC_S == R_ACK)//当IIC_S == R_ACK,完成一个BYTE读,把数据保存到O_rd_data
  12.         O_rd_data[((rcnt-1'b1)*8) +: 8] <= sda_i_r[7:0];
  13.     else if(IIC_S == IDLE)//空闲状态,重置sda_i_r
  14.         sda_i_r <= 8'd0;
  15. end
复制代码
1.1.6总线错误指示模块

进行写操作时,I2C总线无法读到正确ACK,拉高O_iic_bus_error

  1. //总线忙状态
  2. always @(negedge scl_clk or negedge I_rstn )begin
  3.     if(I_rstn == 1'b0)
  4.         O_iic_bus_error <= 1'b0;   
  5.     else begin
  6.         if(IIC_S  == W_ACK && sda_i == 1'b1)//I_iic_req == 1'b1 || rd_req == 1'b1总线进入忙状态
  7.             O_iic_bus_error <= 1'b1;
  8.         else if(I_iic_req == 0)
  9.             O_iic_bus_error <= 1'b0;
  10.     end         
  11. end
复制代码
2状态机设计

所有的SDASCL控制依据状态机设计。

image.jpg

IDLE:IDLE状态,当iic_req请求有效代表一次全新的传输进入启动I2C Start启动传输阶段,如果rd_req有效代表目前要进行repeated start,也是进去START状态。

START:START状态对bcnt进行初始化,设置需要发送的bit数量,因为不管是写操作,还是随机读操作,I2C总线协议要求,都要发送器件地址。因此在START后发送7Bit的器件地址和1bit的读/写位。

W_WAIT:在此阶段发送一个完整的8bit数据,发送移位模块也会在此阶段对数据移位。

W_ACK:对于写操作,SLAVE设备响应ACK,如果还有数据需要,则回到W_WAIT;如果还要进行读操作,则回到IDLE产生一次Repeated Start;如果已经完成所有数据发送,也没有数据需要读,则进入STOP1

R_WAIT:在此阶段完成8bits数据接收,接收移位模块工作,之后进入R_ACK

R_ACK:响应NACK,如果还有数据需要接收,则再次进入R_WAIT,否则进入STOP1,完成本次传输。

STOP1:产生停止位,SDA=0 SDA=1,进入STOP1

SOTP2:产生停止位,SDA=1 SDA=1回到IDLE

3程序源码
  1. module uii2c#
  2. (
  3. parameter integer WMEN_LEN = 8'd0,//写长度,以字节为单位,包含器件地址
  4. parameter integer RMEN_LEN = 8'd0,//读长度,以字节为单位,不包含器件地址
  5. parameter integer CLK_DIV  = 16'd499// I2C时钟分频系数
  6. )
  7. (
  8. input  wire I_clk,//系统时钟输入
  9. input  wire I_rstn,//系统复位,低电平有效
  10. output reg  O_iic_scl = 1'b0,//I2C时钟SCL
  11. inout  wire IO_iic_sda,//I2C 数据总线
  12. input  wire [WMEN_LEN*8-1'b1:0]I_wr_data,//写数据寄存器,其中WMEN_LEN设置了最大支持的数据字节数,越大占用的FPGA资源越多
  13. input  wire [7:0]I_wr_cnt,//写数据计数器,代表写了多少个字节
  14. output reg  [RMEN_LEN*8-1'b1:0]O_rd_data = 0,//读数据寄存器,其中RMEN_LEN设置了最大支持的数据字节数,越大占用的FPGA资源越多
  15. input  wire [7:0]I_rd_cnt,//读数据计数器
  16. input  wire I_iic_req,//I_iic_req == 1 使能I2C传输
  17. input  wire I_iic_mode,//I_iic_mode = 1 随机读   I_iic_mode = 0 读当前寄存器或者页读
  18. output reg  O_iic_busy = 1'b0,//I2C控制器忙
  19. output reg  O_iic_bus_error, //I2C总线,无法读到正确ACK出错
  20. output reg  O_iic_sda_dg
  21. );
  22. localparam IDLE    = 4'd0;//I2C 总线空闲状态
  23. localparam START   = 4'd1;//I2C 总线启动
  24. localparam W_WAIT  = 4'd2;//I2C 总线等待写完成
  25. localparam W_ACK   = 4'd3;//I2C 总线等待写WACK
  26. localparam R_WAIT  = 4'd4;//I2C 总线等待读完成
  27. localparam R_ACK   = 4'd5;//I2C 总线等待读RACK
  28. localparam STOP1   = 4'd6;//I2C 总线产生停止位
  29. localparam STOP2   = 4'd7;//I2C 总线产生停止位   
  30. localparam SCL_DIV = CLK_DIV/2;
  31. localparam OFFSET = SCL_DIV - SCL_DIV/4;//设置I2C总线的SCL时钟的偏移,以满足SCL和SDA的时序要求,外部的SCL延迟内部的半周期的四分之三
  32. reg [2:0] IIC_S = 4'd0; //I2C 状态机
  33. //generate  scl
  34. reg [15:0] clkdiv = 16'd0;   //I2C 时钟分频寄存器
  35. reg scl_r      = 1'b1;       //I2C控制器的SCL内部时钟
  36. reg sda_o      = 1'b0;       //I2C控制器的SDA
  37. reg scl_clk    = 1'b0;       //I2C控制器内部SCL时钟,与外部时钟存在OFFSET参数设置的相位偏移
  38. reg [7:0] sda_r = 8'd0;      //发送寄存器
  39. reg [7:0] sda_i_r = 8'd0;    //接收寄存器
  40. reg [7:0] wcnt = 8'd0;       //发送数据计数器,以byte为单位
  41. reg [7:0] rcnt = 8'd0;       //接收数据计数器,以byte为单位
  42. reg [2:0] bcnt = 3'd0;       //bit计数器
  43. reg  rd_req = 1'b0;          //读请求,当判断到需要读数据,内部状态机中设置1
  44. wire sda_i;                  //sda 输入
  45. wire scl_offset;             //scl 时钟偏移控制
  46. assign  sda_i   = (IO_iic_sda == 1'b0) ?  1'b0 : 1'b1;  //读总线
  47. assign  IO_iic_sda = (sda_o == 1'b0) ?  1'b0 : 1'bz;    //写总线,1'bz代表高阻,I2C外部通过上拉电阻,实现总线的高电平
  48. //scl 时钟分频器
  49. always@(posedge I_clk)
  50.     if(clkdiv < SCL_DIV)   
  51.         clkdiv <= clkdiv + 1'b1;
  52.     else begin
  53.         clkdiv <= 16'd0;
  54.         scl_clk <= !scl_clk;
  55.     end
  56. assign  scl_offset  = (clkdiv == OFFSET);//设置scl_offset的时间参数
  57. always @(posedge I_clk) O_iic_scl <=  scl_offset ?  scl_r : O_iic_scl; //O_iic_scl延迟scl_offset时间的scl_r
  58. //采集I2C 数据总线sda
  59. always @(posedge I_clk) O_iic_sda_dg <= sda_i;  
  60. //当IIC_S状态机处于,同时空闲状态,设置SCL为高电平,同时也是空闲,停止状态,用于产生起始位和停止位时序,否则寄存scl_clk时钟
  61. always @(*)
  62.     if(IIC_S == IDLE || IIC_S == STOP1 || IIC_S == STOP2)
  63.         scl_r <= 1'b1;
  64.     else
  65.         scl_r <= scl_clk;
  66.   
  67. //当进入IIC_S状态为启动、停止设置sda=0,结合scl产生起始位,或者(IIC_S == R_ACK && (rcnt != I_rd_cnt) sda=0,用于产生读操作的ACK
  68. always @(*)
  69.     if(IIC_S == START || IIC_S == STOP1 || (IIC_S == R_ACK && (rcnt != I_rd_cnt)))
  70.         sda_o <= 1'b0;
  71.     else if(IIC_S == W_WAIT)
  72.         sda_o <= sda_r[7];
  73.     else  sda_o <= 1'b1; //否则其他状态都为1,当(IIC_S == R_ACK && (rcnt == I_rd_cnt) 产生一个NACK
  74. //I2C数据发送模块,所有的写数据都通过此模块发送
  75. always @(posedge scl_clk)
  76.     if(IIC_S == W_ACK || IIC_S == START)begin//IIC_S=START和W_ACK,把需要发送的数据,寄存到sda_r
  77.         sda_r <= I_wr_data[(wcnt*8) +: 8];//寄存需要发发送的数据到sda_r
  78.         if( rd_req ) sda_r <= {I_wr_data[7:1],1'b1};//对于读操作,rd_req由内部代码产生,当写完第一个数据(器件地址),后通过判断I_rd_cnt,确认是否数据需要读
  79.     end
  80.     else if(IIC_S == W_WAIT)//当W_WAT状态,通过移位操作,把数据发送到数据总线
  81.         sda_r <= {sda_r[6:0],1'b1};//移位操作
  82.     else
  83.         sda_r <= sda_r;
  84. //sda data bus read and hold data to O_rd_data register when IIC_S=R_ACK
  85. //I2C数据接收模块,I2C读期间,把数据通过移位操作,移入O_rd_data
  86. always @(negedge scl_clk)begin
  87.     if(IIC_S == R_WAIT ) //当IIC_S == R_WAIT ||IIC_S == W_ACK(如果读操作,第1个BIT是W_ACK这个状态读)启动移位操作
  88.         sda_i_r <= {sda_i_r[6:0],sda_i};
  89.     else if(IIC_S == R_ACK)//当IIC_S == R_ACK,完成一个BYTE读,把数据保存到O_rd_data
  90.         O_rd_data[((rcnt-1'b1)*8) +: 8] <= sda_i_r[7:0];
  91.     else if(IIC_S == IDLE)//空闲状态,重置sda_i_r
  92.         sda_i_r <= 8'd0;
  93. end
  94. //总线忙状态
  95. always @(posedge scl_clk or negedge I_rstn )begin
  96.     if(I_rstn == 1'b0)
  97.         O_iic_busy <= 1'b0;
  98.     else begin
  99.         if((I_iic_req == 1'b1 || rd_req == 1'b1 || O_iic_bus_error))//I_iic_req == 1'b1 || rd_req == 1'b1总线进入忙状态
  100.             O_iic_busy <= 1'b1;
  101.         else if(IIC_S == IDLE)
  102.             O_iic_busy <= 1'b0;
  103.     end         
  104. end
  105. //总线忙状态
  106. always @(negedge scl_clk or negedge I_rstn )begin
  107.     if(I_rstn == 1'b0)
  108.         O_iic_bus_error <= 1'b0;   
  109.     else begin
  110.         if(IIC_S  == W_ACK && sda_i == 1'b1)//I_iic_req == 1'b1 || rd_req == 1'b1总线进入忙状态
  111.             O_iic_bus_error <= 1'b1;
  112.         else if(I_iic_req == 0)
  113.             O_iic_bus_error <= 1'b0;
  114.     end         
  115. end
  116. //I2C Master控制器状态机
  117. always @(posedge scl_clk or negedge I_rstn )begin
  118.         if(I_rstn == 1'b0)begin //异步复位,复位相关寄存器
  119.            wcnt     <= 8'd0;
  120.            rcnt     <= 8'd0;
  121.            rd_req   <= 1'b0;   
  122.            IIC_S    <= IDLE;
  123.         end
  124.         else begin
  125.         case(IIC_S) //sda = 1 scl =1
  126.         IDLE:begin//在空闲状态,sda=1 scl=1
  127.            if(I_iic_req == 1'b1 || rd_req == 1'b1) //当I_iic_req == 1'b1代表启动传输 当 rd_req == 1'b1 代表读操作需要产生repeated start 重复启动  
  128.               IIC_S  <= START; //进入START状态
  129.            else begin
  130.               wcnt <= 8'd0; //复位计数器
  131.               rcnt <= 8'd0; //复位计数器
  132.            end
  133.         end
  134.         START:begin //这个状态,前面的代码,先设置sda = 0,scl_offset参数设置了scl_clk时钟的偏移,之后 scl_clk =0 即scl =0 产生起始位或者重复起始位
  135.            bcnt <= 3'd7; //设置bcnt的初值         
  136.            IIC_S  <= W_WAIT;//进入发送等待
  137.         end           
  138.         W_WAIT://等待发送完成,这里发送8bits 数据,写器件地址,写寄存器地址,写数据,都在这个状态完成
  139.         begin
  140.            if(bcnt > 3'd0)//如果8bits没发送完,直到发送完
  141.                bcnt  <= bcnt - 1'b1; //bcnt计数器,每发送1bit减1
  142.            else begin //8bits发送完毕
  143.                wcnt <= wcnt + 1'b1; //wcnt计数器,用于记录已经写了多少字节
  144.                IIC_S  <= W_ACK;//进入W_ACK状态
  145.            end
  146.         end
  147.         W_ACK://等待WACK,此阶段,也判断是否有读操作
  148.         begin
  149.            if(wcnt < I_wr_cnt)begin //判断是否所有数据发送(写)完成
  150.               bcnt <= 3'd7; //如果没有写完,重置bcnt
  151.               IIC_S <= W_WAIT;//继续回到W_WAIT等待数据发送(写)完成
  152.            end
  153.            else if(I_rd_cnt > 3'd0)begin//I_rd_cnt > 0代表有数据需要读,I_rd_cnt决定了有多少数据需要读
  154.               if(rd_req == 1'b0 && I_iic_mode == 1'b1)begin //对于第一次写完器件地址,如果I_iic_mode==1代表支持随机读
  155.                   rd_req <= 1'b1;//设置rd_req=1,请求读操作
  156.                   IIC_S <= IDLE; //设置状态进入IDLE,根据rd_req的值会重新产生一次为读操作进行的repeated重复start
  157.               end
  158.               else //如果之前已经完成了repeated重复start,那么读操作进入读数据阶段
  159.                   IIC_S <= R_WAIT;//进入读等待
  160.                   bcnt <= 3'd7;//设置bcnt的初值  
  161.            end
  162.            else //如果所有的发送完成,也没数据需要读,进入停止状态
  163.               IIC_S <= STOP1;
  164.         end  
  165.         R_WAIT://等待读操作完成
  166.         begin
  167.            rd_req <= 1'b0;//重置读请求rd_req=0
  168.            bcnt  <= bcnt - 1'b1; //bit 计数器
  169.            if(bcnt == 3'd0)begin //当8bits数据读完
  170.               rcnt <= (rcnt < I_rd_cnt) ? (rcnt + 1'b1) : rcnt;//判断是否还有数据需要读
  171.               IIC_S  <= R_ACK;//进入R_ACK
  172.            end
  173.         end
  174.         R_ACK://R_ACK状态产生NACK
  175.         begin
  176.            bcnt <= 3'd7;//重置读请求bcnt计数器
  177.            IIC_S <= (rcnt < I_rd_cnt) ? R_WAIT : STOP1; //如果所有数据读完,进入停止状态
  178.         end  
  179.         STOP1:begin//产生停止位 sda = 0 scl = 1
  180.             rd_req  <= 1'b0;              
  181.             IIC_S <= STOP2;
  182.         end
  183.         STOP2://产生停止位  sda = 1 scl = 1
  184.             IIC_S <= IDLE;         
  185.         default:
  186.             IIC_S <= IDLE;
  187.         endcase
  188.     end
  189. end
  190. endmodule
复制代码













































您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

0

关注

0

粉丝

293

主题
精彩推荐
热门资讯
网友晒图
图文推荐

  • 微信公众平台

  • 扫描访问手机版