基于FPGA的I2C接口控制器(包含单字节和多字节读写)

1、概括

  前文对IIC的时序做了详细的讲解,还有不懂的可以获取TI的IIC数据手册查看原理。通过手册需要知道的是IIC读、写数据都是以字节为单位,每次操作后接收方都需要进行应答。主机向从机写入数据后,从机接收数据,需要把总线拉低来告知主机,前面发送的数据已经被接收。主机在读取从机数据后,如果还需要继续读取数据,就要对从机做出应答,否则不应答。

  另一个需要注意的是数据在时钟的低电平中间进行赋值,数据线在时钟线的高电平期间状态不能发生变化。这是因为在时钟线高电平期间,数据线从高电平变为低电平,从机会认为主机发送了起始位,数据线从低电平变为高电平,从机会认为主句发送停止位。

  在起始位和停止位之间,可以存在任意字节长度的操作,也就是说从机寄存器地址和寄存器数据的宽度都没有限制,根据具体的芯片确定。其实很好理解,比如EEPROM支持单字节的读写操作和突发的页读写操作,这就是上述原因的结果。还有部分芯片的寄存器地址可能是3个字节,读写的数据也是几个字节,这也是可以的。

  使用FPGA接口实现IIC的难度会比UART和SPI高那么一点,原因在于双向IO的控制。双向IO一般使用三态门实现,当然xilinx这类器件还可以使用IOBUFR这种原语实现,会比使能简单很多,但是本文设计的是通用模块,没有平台限制的代码,所以不会使用原语。

  网上关于FPGA的IIC控制器代码还是挺多的,但是基本上对寄存器地址、数据长度都有限制,而且不支持突发读写,如果需要这些功能还是需要独立开发,所以本文就打算设计一个支持寄存器地址长度可变、数据长度可变、支持突发读、写的接口模块,且没有平台限制,一次解决所有问题。

  最后在eeprom上验证单字节读写和突发读写。

2、分析设计

  首先通过几个时序图来具体分析一下单字节、多字节读写时序和多字节的地址读写时序,进而总结出设计思路。

  如下图所示,是eeprom芯片的单字节写时序,该时序每次只写入单个存储单元的单字节数据,所以依次发送起始位、器件地址、写指示位、从机应答、写入地址、从机应答、写入数据、从机应答、停止位即可。

在这里插入图片描述

图1 某eeprom的单字节写时序

  下图是该eeprom实现页写的时序,页写与sdram这些的突发写本质是一样的,就是发送起始位值的地址,后面连续输入后续地址的数据即可。与上图的区别是在第一次写入数据,从机应答之后,主机不发送停止位,而是继续写入数据,便可以向从机的下一个地址写入数据,从机应答之后继续写入数据,直到写入指定个数的数据且从机应答之后,发送停止位结束写入。

  页写入表面上看只节省了发送起始位、器件地址、寄存器地址的时间,但其实节省更多的是单字节写入时中间等待的时间。eeprom两次写入间隔有一个时间要求,芯片手册会给出这个数据的最大值,有的芯片是3ms,有的是5ms,有的10ms。这个时间表示芯片接收数据后,把数据存储到内部指定地址所需要的最大时间。eeprom芯片的页写其实节省最大的是这个时间。

在这里插入图片描述

图2 某eeprom的页写时序

  下图是eeprom的单字节读时序,因为可以读取任意存储位置的数据,所以在发送读指令之前,需要告知存储芯片本次读取数据的存储地址是多少。因此下图读时序中会先发送起始位、器件地址、写指示信号、从机应答、寄存器地址、从机应答。

  将需要读取数据的存储地址写入芯片之后,接下来就是读取该地址的数据了。先发送重复起始位(不发送停止位的原因是在多主机系统中避免被别的主机抢占总线控制权),然后发送器件地址、读指示位、从机应答,之后从机将该寄存器的数据输出到总线上,主机在时钟高电平中部读取数据总线上的数据即可,从机输出一字节数据后主机不应答从机,最后主机发送停止位结束本次读取操作。

在这里插入图片描述

图3 某eeprom的单字节读时序

  下图是eeprom的页读时序,与上图的区别在于主机接收从机发送的第一字节数据后,主机把数据总线拉低,对从机做出应答,从机就会输出下一个存储地址的数据,从而实现连续地址的数据读取。主机接收到指定个数的数据后,应答时将数据总线拉高,不应答从机,然后发送停止位,结束本次读取。

在这里插入图片描述

图4 某eeprom的页读时序

  最后在来查看一个IIC接口的温湿度传感器的读时序,下图时序中的指令数据其实与上述的存储地址是一致的。下图中包含2字节的命令,在发送寄存器地址时需要传输两次,先传输高字节数据。后面寄存器的数据也是16位的,并且后面还包含一字节的CRC校验码,所以读取数据时,需要连续读取3字节数据。

  主机读取前两字节数据时,也需要对从机做出应答,在读完3字节数据后,主机不再对从机做出应答,然后发送停止位结束本次读操作。

在这里插入图片描述

图5 某温湿度传感器IIC读时序

  通过上面对几个时序图的分析可知,页读取(图4)与从同一个寄存器读取多个字节数据(图5)的时序原理是一样,就是读前面字节数据后应答从机,最后一字节数据时不应答从机。

  综上,IIC的读写时序中,器件地址的长度一般是固定的,根据不同芯片设计,寄存器地址的长度不固定,读写的数据长度也是不固定的,所以在设计驱动模块时,这两部分需要根据实际情况自动改变。

  最简单的想法就是通过一个计数器来对已经发送的寄存器地址字节数和读写数据的字节数计数。写入的寄存器地址字节数达到要求后在跳转到别的状态,而读写数据时,只有读写指定字节数数据时,主机才能发送停止位。

  在fpga实现时,寄存器的地址字节数、读写数据字节数可以通过parameter常量进行设置,便于使用时修改,且不会产生多余的电路。

3、设计实现

  下表是该模块的端口信号,开始读写信号start必须在模块空闲(rdy为高电平)时才能拉高,拉高一个时钟周期即可。

表1 端口信号列表

信号位宽I/O含义
clk1I系统时钟信号,默认100MHz
rst_n1I系统复位,默认低电平有效;
start1I读、写操作开始信号,高电平有效。
rw_flag1I读、写指示信号,高电平表示读。
reg_addr可变I寄存器地址信号。
wdata可变I写入寄存器的数据。
rdata可变O从寄存器读出的数据。
rdata_vld1O读出数据有效指示信号,高电平有效。
rdy1O模块忙闲指示信号,高电平表示模块空闲。
scl1OIIC的串行时钟线。
sda1IOIIC的双向串行数据线;

  本次设计采用一个状态机嵌套三个计数器作为主体架构实现,状态机包括7个状态。状态转换图如下所示,“将发送1字节加上应答位划分一个状态”,这句话不完全状态。

在这里插入图片描述

图6 状态机状态转化图

  将发送起始位、器件地址、写标志位划分为W_DEVICE_ADDR状态,发送读数据划分为WDATA状态(这个状态可能会读取多个字节数据,根据设置跳转),发送读、写寄存器地址划分为W_REG_ADDR状态(这个状态依旧可能发送多个字节的数据),发送重复起始位、器件地址、读标志位划分为R_DEVICE_ADDR状态,接收数据线的数据划分为RDATA状态(这个状态依旧可能发送多个字节的数据),最后STOP状态发送停止位。

  分频计数器div_cnt在状态机不处于空闲状态时,对系统时钟进行计数,从而产生IIC时钟信号scl,同时将scl的低电平、高电平的中间分别生成wr_flag和rd_flag标志信号,wr_flag位高电平表示可以对IIC数据线赋值,rd_flag高电平表示可以在此时读取IIC数据线上的数据。

  计数器bit_cnt用于记录每次读写数据的位数,当分频计数器计数结束时加1。状态机在不同的状态,bit_cnt计数器的最大值不一样,当状态机处于W_DEVICE_ADDR或R_DEVICE_ADDR状态时,需要发送起始位、器件地址、读写标志位、应答位,所以bit_cnt计数器最大值为10-1,而状态机位于WDATA,W_REG_ADDR,RDATA状态时,每次读写的单位都是1字节数据、应答位,所以bit_cnt计数器最大值为9-1。

  计数器byte_cnt用于记录状态机处于WDATA,W_REG_ADDR,RDATA状态时,接收或者发送的数据字节数。当状态机处于上述三个状态且计数器bit_cnt计数结束时加1,根据需要读写的寄存器地址字节数和读写数据字节数,确定该计数器在各个状态下的最大值。

  状态机的跳转与三个计数器的结束条件有效,比较简单,此处不做过多介绍,看代码即可。

  只需要注意一下下面几个信号的变化即可,首先注意模块有几个parameter常量,包括系统时钟的频率、IIC时钟的频率、IIC的从机器件地址、读写寄存器的字节数、读写数据的字节数,对应代码如下所示。

module iic_drive #(parameter           FCLK                    =   100_000_000         ,//系统时钟频率,默认100MHz。parameter           FSCL                    =   400_000             ,//IIC时钟频率,默认400KHz。parameter           REG_ADDR_BYTE_NUM       =   1                   ,//寄存器地址字节数;parameter			DATA_BYTE_NUM           =   1		            ,//读写数据字节数。parameter			DEVICE_ADDR             =   7'b1010000           //器件地址。
)(input									        clk		            ,//系统时钟信号;input									        rst_n	            ,//系统复位信号,低电平有效;input                                           start               ,//开始进行读写操作;input                                           rw_flag             ,//读写标志信号,高电平表示读操作,低电平表示写操作;input               [REG_ADDR_BYTE_NUM*8-1 : 0] reg_addr            ,//寄存器地址,读写操作时共用的地址信号;input               [DATA_BYTE_NUM*8 - 1 : 0]   wdata               ,//写数据;output  reg         [DATA_BYTE_NUM*8 - 1 : 0]   rdata               ,//读数据信号;output  reg                                     rdata_vld           ,//读数据输出使能信号,高电平有效;output  reg                                     rdy                 ,//模块忙闲指示信号,位高电平时可以接收上游模块的读写使能信号;output  reg                                     scl                 ,//IIC的时钟信号;inout                                           sda                 ,//IIC的双向数据信号;output  reg                                     ack_flag             //高电平表示应答失败;
);

  当接收到上游模块的读写开始信号(start为高电平)时,将寄存器地址、写数据、读写状态信号暂存,便于后续读写过程中使用,读写寄存器的地址和数据信号全部采用参数化设计,不需要人为修改信号位宽。将器件地址和起始位、写指示位拼接,便于后续使用。对应代码如下所示:

    //暂存器件地址和起始位还有写指示位。assign device_addr = {1'b0,DEVICE_ADDR,1'b0};//开始信号有效时,把待发送的信号暂存。always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为0;wdata_r <= 0;rw_flag_r <= 1'b0;reg_addr_r <= 0;endelse if(start)beginwdata_r <= wdata;rw_flag_r <= rw_flag;reg_addr_r <= reg_addr;endend

  下面就是状态机的跳转了,状态机采用三段式,下面代码包含其次态到现态的转换以及次态变化最重要的两段,跳转很简单,也有注释,基本上就是对应数据读写完毕后就跳转,不做多余讲解。

    //状态机,次态到现态的转换;always@(posedge clk or negedge rst_n)beginif(!rst_n)beginstate_c <= IDLE;endelse beginstate_c <= state_n;endend//状态机次态的跳转;always@(*)begincase(state_c)IDLE : beginif(start)begin//开始信号有效时,跳转到发送起始位和器件地址的状态;state_n = W_DEVICE_ADDR;endelse beginstate_n = state_c;endendW_DEVICE_ADDR : beginif(end_bit_cnt)begin//器件地址发送完成后,跳转到写寄存器地址状态;state_n = W_REG_ADDR;endelse beginstate_n = state_c;endendW_REG_ADDR : beginif(end_byte_cnt)begin//寄存器地址写入完成后,if(rw_flag_r)//如果是读操作,则跳转到重复起始位和写器件地址状态;state_n = R_DEVICE_ADDR;else//如果是写操作,跳转到写数据状态;state_n = WDATA;endelse beginstate_n = state_c;endendWDATA : beginif(end_byte_cnt)begin//如果数据全部写入完成,则跳转到停止状态;state_n = STOP;endelse beginstate_n = state_c;endendR_DEVICE_ADDR : beginif(end_bit_cnt)begin//如果重复起始位、器件地址、读指示位写入完毕,则跳转到读数据状态;state_n = RDATA;endelse beginstate_n = state_c;endendRDATA : beginif(end_byte_cnt)begin//读出一次需要读出的所有数据后,跳转到停止状态;state_n = STOP;endelse beginstate_n = state_c;endendSTOP : beginif(end_div_cnt)begin//停止位发送完毕后,跳转到空闲状态;state_n = IDLE;endelse beginstate_n = state_c;endenddefault:begin//state_n = IDLE;endendcaseend

  然后就是分频计数器div_cnt,当状态机不处于空闲状态时,对系统时钟进行计数,从而生成scl时钟信号,对应代码如下所示。生成该计数器相关的四个信号,计数器计数到一半时,需要把IIC时钟线scl拉低,所以生成标志信号l2h_flag表示scl下降沿。同理生成h2l_flag表示scl上升沿,主机在scl低电平中间驱动数据线SDA,所以分频计数器计数到1/4时,把wr_flag拉高,表示主机可以写入数据。主机在高电平中间读取SDA数据,所以分频计数器计数到3/4时,把rd_flag拉高,表示主机可以读取从机的数据。

    //分频计数器,用于生成SCL信号。always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//div_cnt <= 0;endelse if(state_c != IDLE)beginif(end_div_cnt)//状态机不处于空闲状态时,对系统时钟进行计数;div_cnt <= 0;elsediv_cnt <= div_cnt + 1;endend//根据clk_cnt生成各种标志信号,由于计数器从零开始计数,并且下面为时序电路,所以产生条件是为对应值减2。always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为0;l2h_flag <= 1'b0;h2l_flag <= 1'b0;wr_flag  <= 1'b0;rd_flag  <= 1'b0;end_div_cnt <= 1'b0;endelse beginl2h_flag <= (div_cnt == CLK_DIV / 2);//在计数器div_cnt计数到一半时scl拉高;h2l_flag <= (div_cnt == 0);//在计数器div_cnt计数0时scl拉低;end_div_cnt <= (div_cnt == CLK_DIV - 2);//在计数器div_cnt计数结束时scl拉低;wr_flag <= (div_cnt == CLK_DIV / 4);//在计数器div_cnt计数四分之一处SDA写入数据;rd_flag <= (div_cnt == CLK_DIV*3 / 4);//在计数器div_cnt计数四分之三处从SDA读取数据;endend

  接下来是用来记录发送字节数的计数器bit_cnt,对应代码如下所示:当分频计数器计数结束时该计数器加1,表示经过了发送1位数据的时间。根据状态机所处状态不同,每次需要发送或者读取的数据位数不同,使用bit_cnt_num去控制该计数器在状态机不同状态的最大值。状态机在W_DEVICE_ADDR和R_DEVICE_ADDR需要发送起始位、7位器件地址、1位读写指示位、1位应答位,需要持续10个时钟周期,而写寄存器地址、读写数据状态都是8位数据加1位应答位,所以最大值为9。特别注意该计数器在状态机处于空闲状态时需要清零。

    //数据位计数器bit_cnt,初始值为0,当分频计数器计数结束的时候加一。always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为0;bit_cnt <= 0;endelse if(state_c == IDLE)begin//状态机处于空闲状态时清零;bit_cnt <= 0;endelse if(add_bit_cnt)beginif(end_bit_cnt)bit_cnt <= 0;elsebit_cnt <= bit_cnt + 1;endendassign add_bit_cnt = end_div_cnt;//计数器加一条件,当分频计数器计数结束时有效;assign end_bit_cnt = add_bit_cnt && (bit_cnt == bit_cnt_num - 1);//用于表示每个状态每次发送的数据位数,发送器件地址之前需要发送起始位,在加上应答位,需要是个SCL时钟。//其余状态每次发送一字节数据后需要发送应答位,所以计数器最大值为9。always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为0;bit_cnt_num <= 4'd9;end//写器件地址和起始位、读写指示位,总共是10位数据,所以计数器的最大值为10-1;else if((state_c == W_DEVICE_ADDR) || (state_c == R_DEVICE_ADDR))beginbit_cnt_num <= 4'd10;endelse begin//其余状态下计数器最大值为9。bit_cnt_num <= 4'd9;endend

  然后是用来记录状态机在写寄存器地址、读写数据阶段读写数据字节数的计数器byte_cnt,对应代码如下图所示。当状态机处于这几个状态下,计数器bit_cnt计数结束时加1,读写数据的最大值在状态机不同状态页不相同,与前文设置的parameter参数有关,通过byte_cnt_num信号的值控制计数器byte_cnt的最大值。

    //发送字节数的计数器,用于计数发送数据的字节数据。always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为0。byte_cnt <= 0;endelse if(state_c == IDLE)begin//状态机处于空闲状态时清零;byte_cnt <= 0;endelse if(add_byte_cnt)beginif(end_byte_cnt)byte_cnt <= 0;elsebyte_cnt <= byte_cnt + 1;endend//当状态机处于写寄存器地址或写数据或读数据状态且发送数据位计数器计数结束时加1。assign add_byte_cnt = ((state_c == W_REG_ADDR) || (state_c == WDATA) || (state_c == RDATA)) && end_bit_cnt;assign end_byte_cnt = add_byte_cnt && (byte_cnt == byte_cnt_num);//当计数到指定数值时清零。//字节计数器的最大值,初始值为写寄存器地址的长度;always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为0;byte_cnt_num <= REG_ADDR_BYTE_NUM - 1;endelse if(state_c == W_REG_ADDR)beginbyte_cnt_num <= REG_ADDR_BYTE_NUM - 1;endelse if((state_c == WDATA) || (state_c == RDATA))beginbyte_cnt_num <= DATA_BYTE_NUM - 1;endend

  前文就将状态机和三个计数器的主体架构搭建好了,后面就根据这个架构去生成本文需要的输出信号了,是不是页很简单。

  首先生成IIC的时钟信号scl,当状态机不处于空闲状态且l2h_flag有效时拉高。在产生起始位时,时钟信号需要保持一段时间高电平,状态机在W_REG_ADDR状态下,发送第一位数据时,时钟信号需要一直保持高电平,否则只要h2l_flag有效,就把scl拉低,对应代码如下所示。

    //生成串行时钟信号;always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为0;scl <= 1'b1;end//当拉高条件有效或者状态机处于空闲状态时拉高。else if(l2h_flag || state_c == IDLE)beginscl <= 1'b1;end//只有在初始发送起始位时满足拉低条件时不拉低,其余情况下满足条件均要拉低;else if((((state_c == W_DEVICE_ADDR) && bit_cnt > 0) || (state_c != W_DEVICE_ADDR)) && h2l_flag)beginscl <= 1'b0;endend

  然后生成串行数据输出信号sda_out,初始时该信号为高电平,状态机在不同状态输出不同数据即可。状态机处于R_REG_ADDR、RDATA、STOP需要特别注意,重复起始位的产生需要在写数据(bit_cnt==0 && wr_flag)时拉高,然在scl的高电平中间(bit_cnt==0 && rd_flag)拉低。读指示位(bit_cnt == bit_cnt_num-2 && wr_flag)需要输出高电平。

  读数据阶段主机需要在读取完最后一字节数据后输出高电平,表示不应答从机,如果读取的数据不是最后一字节数据,则输出低电平应答从机,继续接收从机输出的数据。

  然后在发送停止位时需要先在scl为低电平时把sda拉低,在scl为高电平时拉高sda,从而表示出停止位。

    //赋值输出信号;always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为0;sda_out <= 1'b1;endelse begincase (state_c)W_DEVICE_ADDR : beginif((~bit_cnt[3]) && wr_flag)//输出器件地址和写指示位;sda_out <= device_addr[8 - bit_cnt];endW_REG_ADDR : beginif((~bit_cnt[3]) && wr_flag)//输出需要写入的寄存器地址;sda_out <= reg_addr_r[REG_ADDR_BYTE_NUM*8 - 1 - byte_cnt*8 - bit_cnt];//reg_addr_r[7 - bit_cnt];endWDATA : begin//输出写数据,先输出高字节数据;if((~bit_cnt[3]) && wr_flag)sda_out <= wdata_r[DATA_BYTE_NUM*8 - 1 - byte_cnt*8 - bit_cnt];endR_DEVICE_ADDR : begin//输出重复开始信号,器件地址和读指示位;if(wr_flag)//当SCL低电平时把SDA拉低,便于后续产生起始位;if(bit_cnt == 0 || bit_cnt == bit_cnt_num - 2)sda_out <= 1'b1;else//产生起始位之后,在SCL低电平中间发送器件地址;sda_out <= device_addr[8 - bit_cnt];else if(rd_flag && bit_cnt == 0)//在SCL高电平的时候拉低SDA,发送重复起始位;sda_out <= 1'b0;endRDATA : beginif(bit_cnt == bit_cnt_num - 1 && wr_flag)if(byte_cnt == DATA_BYTE_NUM - 1)//如果是读取的最后一字节数据,则不应答。sda_out <= 1'b1;else//如果不是最后一字节数据,则进行应答。sda_out <= 1'b0;endSTOP : beginif(wr_flag)//停止信号需要先拉低;sda_out <= 1'b0;else if(rd_flag)//在SCL高电平的时候拉高,表示停止位;sda_out <= 1'b1;enddefault : sda_out <= sda_out;endcaseendend

  上述生成了串行数据的输出信号,接下来就需要生成三态门使能信号sda_out_en,把上面生成的数据输出。如果使用iobufr则可以省略该信号。因为一般系统中只会存在一个主机,所以主机除了需要从机做出应答的状态,其余时间主机全程驱动数据线。

    //赋值输出使能信号,除了从机应答之外,其余全为高电平;always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为1;sda_out_en <= 1'b1;endelse if(wr_flag)begincase (state_c)//在写器件地址、写寄存器地址、写数据、读过程的写器件地址的从机应答状态,都需要释放总线;W_DEVICE_ADDR,WDATA,R_DEVICE_ADDR,W_REG_ADDR : beginif(bit_cnt == 0)//当计数器为0时,总线拉高,开始写入下一字节数据;sda_out_en <= 1'b1;else if(bit_cnt == bit_cnt_num - 1)//当写入最后一位数据后,将使能信号拉低,释放总线;sda_out_en <= 1'b0;endSTOP : beginif(bit_cnt == 0)//当计数器为0时,总线拉高,开始写入下一字节数据;sda_out_en <= 1'b1;endRDATA : begin//在读数据阶段,主机应答时需要控制总线,其余时间释放总线;if(bit_cnt == 0)sda_out_en <= 1'b0;else if(bit_cnt == bit_cnt_num - 1)sda_out_en <= 1'b1;enddefault: ;endcaseendend

  因此使能信号初始为高电平,状态机处于W_DEVICE_ADDR、WDATA、R_DEVICE_ADDR、W_REG_ADDR、STOP状态时,在从机应答的时候释放总线。而读数据状态RDATA只有在应答时主机才控制总线,所以与其他状态的控制状态刚好相反。STOP状态下bit_cnt==bit_cnt_num-1不会满足,所以使能在该状态下不会被拉低。

  然后就是主机接收从机的数据了,如下所示,为了该用户接口的信号保持稳定,则将接收完成的数据打一拍后输出,在SCL的中部接收数据,先接收的数据位于高字节的高位。

    //在读数据阶段,读取总线上的数据;always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为0;rdata_r <= 0;end//当处于读数据阶段时,在SCL高电平中间读取数据总线上的数据;else if(state_c == RDATA && rd_flag)beginrdata_r[DATA_BYTE_NUM*8 - 1 - byte_cnt*8 - bit_cnt] <= sda_in;endend//数据输出有效指示信号,该信号为高电平时,表示读取的数据rdata有效;always@(posedge clk)beginrdata_vld_r <= (state_c == RDATA) && rd_flag && (bit_cnt == bit_cnt_num - 2) && (byte_cnt == byte_cnt_num);end//将读取的数据输出。always@(posedge clk)beginrdata <= rdata_vld_r ? rdata_r : rdata;rdata_vld <= rdata_vld_r;end

  最后就是模块忙闲指示信号和应答失败的指示信号,模块接收到开始信号或状态机不处于空闲状态时,模块处于忙碌状态,rdy拉低,其余时间拉高,表示模块空闲,注意该信号只能采用组合逻辑生成。

  最后应答失败指示信号,在各个状态的应答位中部读取串行数据线sda的状态,低电平表示应答,高电平表示从机不应答。

    //模块忙闲指示信号,当模块接收到开始信号或者状态机不处于空闲状态时拉低,表示模块处于工作状态;always@(*)beginif(start || (state_c != IDLE))rdy = 1'b0;else//其余时间拉高,表示模块处于空闲状态,上游模块可以发起写或者读操作;rdy = 1'b1;end//从机应答失败标志信号,高电平表示应答失败,每次开始读写操作时清零;always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为0;ack_flag <= 1'b0;endelse if(start)begin//接收到开始读写请求信号时拉低;ack_flag <= 1'b0;end//在从机应答状态下,将接收到的应答信号输出,高电平表示应答失败,低电平表示应答成功。else if(((state_c == WDATA) || (state_c == W_DEVICE_ADDR) || (state_c == W_REG_ADDR) || (state_c == R_DEVICE_ADDR)) && rd_flag && (bit_cnt == bit_cnt_num - 1))beginack_flag <= sda_in;endend

4、参考代码

  上述就是该模块的设计,是不是也很简单?总共加注释也就402行代码,没有采用任何缩写,后文通过驱动eeprom对该模块的设计进行仿真和验证,最后可以看综合结果,消耗的资源也是比较少的,寄存器地址和读写数据均为1字节时,也只需要消耗八十多个LUT和触发器资源。

  该模块的完整参考代码如下所示:

module iic_drive #(parameter           FCLK                    =   100_000_000         ,//系统时钟频率,默认100MHz。parameter           FSCL                    =   400_000             ,//IIC时钟频率,默认400KHz。parameter           REG_ADDR_BYTE_NUM       =   1                   ,//寄存器地址字节数;parameter			DATA_BYTE_NUM           =   1		            ,//读写数据字节数。parameter			DEVICE_ADDR             =   7'b1010000           //器件地址。
)(input									        clk		            ,//系统时钟信号;input									        rst_n	            ,//系统复位信号,低电平有效;input                                           start               ,//开始进行读写操作;input                                           rw_flag             ,//读写标志信号,高电平表示读操作,低电平表示写操作;input               [REG_ADDR_BYTE_NUM*8-1 : 0] reg_addr            ,//寄存器地址,读写操作时共用的地址信号;input               [DATA_BYTE_NUM*8 - 1 : 0]   wdata               ,//写数据;output  reg         [DATA_BYTE_NUM*8 - 1 : 0]   rdata               ,//读数据信号;output  reg                                     rdata_vld           ,//读数据输出使能信号,高电平有效;output  reg                                     rdy                 ,//模块忙闲指示信号,位高电平时可以接收上游模块的读写使能信号;output  reg                                     scl                 ,//IIC的时钟信号;inout                                           sda                 ,//IIC的双向数据信号;output  reg                                     ack_flag             //高电平表示应答失败;
);localparam          CLK_DIV             =       FCLK / FSCL         ;//计算计数器div_cnt的结束值;localparam          CLK_DIV_W           =       clogb2(CLK_DIV - 1) ;//计算计数器div_cnt的位宽;//根据比较寄存器地址和读写数据的大小,然后自动计算处byte_cnt计数器位宽。localparam          BYTE_CNT_W          =       (REG_ADDR_BYTE_NUM  > DATA_BYTE_NUM) ? clogb2(REG_ADDR_BYTE_NUM-1) : clogb2(DATA_BYTE_NUM-1);//Four-stage state machine;localparam          IDLE                =       7'b0000001          ;//状态机空闲状态;localparam          W_DEVICE_ADDR       =       7'b0000010          ;//状态机写器件地址状态;localparam          W_REG_ADDR          =       7'b0000100          ;//状态机写寄存器地址状态;localparam          WDATA               =       7'b0001000          ;//状态机写数据状态;localparam          R_DEVICE_ADDR       =       7'b0010000          ;//状态机发送读器件地址状态;localparam          RDATA	            =       7'b0100000          ;//状态机读数据状态;localparam          STOP	            =       7'b1000000          ;//状态机停止状态;reg                                             l2h_flag            ;reg                                             h2l_flag            ;reg                                             wr_flag             ;reg                                             rd_flag             ;reg                                             end_div_cnt         ;reg                                             rw_flag_r           ;//reg                                             sda_out             ;reg                                             sda_out_en          ;reg                 [6 : 0]	                    state_n             ;reg                 [6 : 0]	                    state_c             ;reg                 [3 : 0] 	                bit_cnt             ;//reg                 [3 : 0]                     bit_cnt_num         ;//reg                 [CLK_DIV_W - 1 : 0] 	    div_cnt             ;//reg                 [BYTE_CNT_W - 1 : 0] 	    byte_cnt            ;//reg                 [BYTE_CNT_W - 1 : 0]        byte_cnt_num        ;//reg                 [DATA_BYTE_NUM*8 - 1 : 0]   wdata_r             ;reg                 [REG_ADDR_BYTE_NUM*8 - 1 : 0] reg_addr_r        ;reg                 [DATA_BYTE_NUM*8 - 1 : 0]   rdata_r             ;reg                                             rdata_vld_r         ;wire                     		                add_byte_cnt        ;wire                     		                end_byte_cnt        ;wire                [8 : 0]                     device_addr         ;wire                                            sda_in              ;wire       		                                add_bit_cnt         ;wire       		                                end_bit_cnt         ;// Pullup output (connect directly to top-level port)//PULLUP PULLUP_inst (.O(sda));//双向IO控制;assign sda_in = sda;assign sda = sda_out_en ? sda_out : 1'bz;//自动计算位宽函数;function integer clogb2(input integer depth);beginif(depth == 0)clogb2 = 1;else if(depth != 0)for(clogb2=0 ; depth>0 ; clogb2=clogb2+1)depth=depth >> 1;endendfunction//暂存器件地址和起始位还有写指示位。assign device_addr = {1'b0,DEVICE_ADDR,1'b0};//开始信号有效时,把待发送的信号暂存。always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为0;wdata_r <= 0;rw_flag_r <= 1'b0;reg_addr_r <= 0;endelse if(start)beginwdata_r <= wdata;rw_flag_r <= rw_flag;reg_addr_r <= reg_addr;endend//状态机,次态到现态的转换;always@(posedge clk or negedge rst_n)beginif(!rst_n)beginstate_c <= IDLE;endelse beginstate_c <= state_n;endend//状态机次态的跳转;always@(*)begincase(state_c)IDLE : beginif(start)begin//开始信号有效时,跳转到发送起始位和器件地址的状态;state_n = W_DEVICE_ADDR;endelse beginstate_n = state_c;endendW_DEVICE_ADDR : beginif(end_bit_cnt)begin//器件地址发送完成后,跳转到写寄存器地址状态;state_n = W_REG_ADDR;endelse beginstate_n = state_c;endendW_REG_ADDR : beginif(end_byte_cnt)begin//寄存器地址写入完成后,if(rw_flag_r)//如果是读操作,则跳转到重复起始位和写器件地址状态;state_n = R_DEVICE_ADDR;else//如果是写操作,跳转到写数据状态;state_n = WDATA;endelse beginstate_n = state_c;endendWDATA : beginif(end_byte_cnt)begin//如果数据全部写入完成,则跳转到停止状态;state_n = STOP;endelse beginstate_n = state_c;endendR_DEVICE_ADDR : beginif(end_bit_cnt)begin//如果重复起始位、器件地址、读指示位写入完毕,则跳转到读数据状态;state_n = RDATA;endelse beginstate_n = state_c;endendRDATA : beginif(end_byte_cnt)begin//读出一次需要读出的所有数据后,跳转到停止状态;state_n = STOP;endelse beginstate_n = state_c;endendSTOP : beginif(end_div_cnt)begin//停止位发送完毕后,跳转到空闲状态;state_n = IDLE;endelse beginstate_n = state_c;endenddefault:begin//state_n = IDLE;endendcaseend//分频计数器,用于生成SCL信号。always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//div_cnt <= 0;endelse if(state_c != IDLE)beginif(end_div_cnt)//状态机不处于空闲状态时,对系统时钟进行计数;div_cnt <= 0;elsediv_cnt <= div_cnt + 1;endend//根据clk_cnt生成各种标志信号,由于计数器从零开始计数,并且下面为时序电路,所以产生条件是为对应值减2。always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为0;l2h_flag <= 1'b0;h2l_flag <= 1'b0;wr_flag  <= 1'b0;rd_flag  <= 1'b0;end_div_cnt <= 1'b0;endelse beginl2h_flag <= (div_cnt == CLK_DIV / 2);//在计数器div_cnt计数到一半时scl拉高;h2l_flag <= (div_cnt == 0);//在计数器div_cnt计数0时scl拉低;end_div_cnt <= (div_cnt == CLK_DIV - 2);//在计数器div_cnt计数结束时scl拉低;wr_flag <= (div_cnt == CLK_DIV / 4);//在计数器div_cnt计数四分之一处SDA写入数据;rd_flag <= (div_cnt == CLK_DIV*3 / 4);//在计数器div_cnt计数四分之三处从SDA读取数据;endend//数据位计数器bit_cnt,初始值为0,当分频计数器计数结束的时候加一。always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为0;bit_cnt <= 0;endelse if(state_c == IDLE)begin//状态机处于空闲状态时清零;bit_cnt <= 0;endelse if(add_bit_cnt)beginif(end_bit_cnt)bit_cnt <= 0;elsebit_cnt <= bit_cnt + 1;endendassign add_bit_cnt = end_div_cnt;//计数器加一条件,当分频计数器计数结束时有效;assign end_bit_cnt = add_bit_cnt && (bit_cnt == bit_cnt_num - 1);//用于表示每个状态每次发送的数据位数,发送器件地址之前需要发送起始位,在加上应答位,需要是个SCL时钟。//其余状态每次发送一字节数据后需要发送应答位,所以计数器最大值为9。always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为0;bit_cnt_num <= 4'd9;end//写器件地址和起始位、读写指示位,总共是10位数据,所以计数器的最大值为10-1;else if((state_c == W_DEVICE_ADDR) || (state_c == R_DEVICE_ADDR))beginbit_cnt_num <= 4'd10;endelse begin//其余状态下计数器最大值为9。bit_cnt_num <= 4'd9;endend//发送字节数的计数器,用于计数发送数据的字节数据。always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为0。byte_cnt <= 0;endelse if(state_c == IDLE)begin//状态机处于空闲状态时清零;byte_cnt <= 0;endelse if(add_byte_cnt)beginif(end_byte_cnt)byte_cnt <= 0;elsebyte_cnt <= byte_cnt + 1;endend//当状态机处于写寄存器地址或写数据或读数据状态且发送数据位计数器计数结束时加1。assign add_byte_cnt = ((state_c == W_REG_ADDR) || (state_c == WDATA) || (state_c == RDATA)) && end_bit_cnt;assign end_byte_cnt = add_byte_cnt && (byte_cnt == byte_cnt_num);//当计数到指定数值时清零。//字节计数器的最大值,初始值为写寄存器地址的长度;always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为0;byte_cnt_num <= REG_ADDR_BYTE_NUM - 1;endelse if(state_c == W_REG_ADDR)beginbyte_cnt_num <= REG_ADDR_BYTE_NUM - 1;endelse if((state_c == WDATA) || (state_c == RDATA))beginbyte_cnt_num <= DATA_BYTE_NUM - 1;endend//生成串行时钟信号;always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为0;scl <= 1'b1;end//当拉高条件有效或者状态机处于空闲状态时拉高。else if(l2h_flag || state_c == IDLE)beginscl <= 1'b1;end//只有在初始发送起始位时满足拉低条件时不拉低,其余情况下满足条件均要拉低;else if((((state_c == W_DEVICE_ADDR) && bit_cnt > 0) || (state_c != W_DEVICE_ADDR)) && h2l_flag)beginscl <= 1'b0;endend//赋值输出信号;always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为0;sda_out <= 1'b1;endelse begincase (state_c)W_DEVICE_ADDR : beginif((~bit_cnt[3]) && wr_flag)//输出器件地址和写指示位;sda_out <= device_addr[8 - bit_cnt];endW_REG_ADDR : beginif((~bit_cnt[3]) && wr_flag)//输出需要写入的寄存器地址;sda_out <= reg_addr_r[REG_ADDR_BYTE_NUM*8 - 1 - byte_cnt*8 - bit_cnt];//reg_addr_r[7 - bit_cnt];endWDATA : begin//输出写数据,先输出高字节数据;if((~bit_cnt[3]) && wr_flag)sda_out <= wdata_r[DATA_BYTE_NUM*8 - 1 - byte_cnt*8 - bit_cnt];endR_DEVICE_ADDR : begin//输出重复开始信号,器件地址和读指示位;if(wr_flag)//当SCL低电平时把SDA拉低,便于后续产生起始位;if(bit_cnt == 0 || bit_cnt == bit_cnt_num - 2)sda_out <= 1'b1;else//产生起始位之后,在SCL低电平中间发送器件地址;sda_out <= device_addr[8 - bit_cnt];else if(rd_flag && bit_cnt == 0)//在SCL高电平的时候拉低SDA,发送重复起始位;sda_out <= 1'b0;endRDATA : beginif(bit_cnt == bit_cnt_num - 1 && wr_flag)if(byte_cnt == DATA_BYTE_NUM - 1)//如果是读取的最后一字节数据,则不应答。sda_out <= 1'b1;else//如果不是最后一字节数据,则进行应答。sda_out <= 1'b0;endSTOP : beginif(wr_flag)//停止信号需要先拉低;sda_out <= 1'b0;else if(rd_flag)//在SCL高电平的时候拉高,表示停止位;sda_out <= 1'b1;enddefault : sda_out <= sda_out;endcaseendend//赋值输出使能信号,除了从机应答之外,其余全为高电平;always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为1;sda_out_en <= 1'b1;endelse if(wr_flag)begincase (state_c)//在写器件地址、写寄存器地址、写数据、读过程的写器件地址的从机应答状态,都需要释放总线;W_DEVICE_ADDR,WDATA,R_DEVICE_ADDR,W_REG_ADDR : beginif(bit_cnt == 0)//当计数器为0时,总线拉高,开始写入下一字节数据;sda_out_en <= 1'b1;else if(bit_cnt == bit_cnt_num - 1)//当写入最后一位数据后,将使能信号拉低,释放总线;sda_out_en <= 1'b0;endSTOP : beginif(bit_cnt == 0)//当计数器为0时,总线拉高,开始写入下一字节数据;sda_out_en <= 1'b1;endRDATA : begin//在读数据阶段,主机应答时需要控制总线,其余时间释放总线;if(bit_cnt == 0)sda_out_en <= 1'b0;else if(bit_cnt == bit_cnt_num - 1)sda_out_en <= 1'b1;enddefault: ;endcaseendend//在读数据阶段,读取总线上的数据;always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为0;rdata_r <= 0;end//当处于读数据阶段时,在SCL高电平中间读取数据总线上的数据;else if(state_c == RDATA && rd_flag)beginrdata_r[DATA_BYTE_NUM*8 - 1 - byte_cnt*8 - bit_cnt] <= sda_in;endend//数据输出有效指示信号,该信号为高电平时,表示读取的数据rdata有效;always@(posedge clk)beginrdata_vld_r <= (state_c == RDATA) && rd_flag && (bit_cnt == bit_cnt_num - 2) && (byte_cnt == byte_cnt_num);end//将读取的数据输出。always@(posedge clk)beginrdata <= rdata_vld_r ? rdata_r : rdata;rdata_vld <= rdata_vld_r;end//模块忙闲指示信号,当模块接收到开始信号或者状态机不处于空闲状态时拉低,表示模块处于工作状态;always@(*)beginif(start || (state_c != IDLE))rdy = 1'b0;else//其余时间拉高,表示模块处于空闲状态,上游模块可以发起写或者读操作;rdy = 1'b1;end//从机应答失败标志信号,高电平表示应答失败,每次开始读写操作时清零;always@(posedge clk or negedge rst_n)beginif(rst_n==1'b0)begin//初始值为0;ack_flag <= 1'b0;endelse if(start)begin//接收到开始读写请求信号时拉低;ack_flag <= 1'b0;end//在从机应答状态下,将接收到的应答信号输出,高电平表示应答失败,低电平表示应答成功。else if(((state_c == WDATA) || (state_c == W_DEVICE_ADDR) || (state_c == W_REG_ADDR) || (state_c == R_DEVICE_ADDR)) && rd_flag && (bit_cnt == bit_cnt_num - 1))beginack_flag <= sda_in;endendendmodule

  本文就这么多吧,后文对该模块进行仿真和上板验证,不是说还没有验证,是本文篇幅已经过长了,仿真也包括单字节读、写,页写和页读,还有eeprom自己的内容,涉及的东西也不会少。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/690936.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux常用操作指令

Linux常用操作指令 ls 指令 语法&#xff1a;ls [选项][目录或文件] 功能&#xff1a;对于目录&#xff0c;该命令列出该目录下的所有子目录与文件。对于文件&#xff0c;将列出文件名以及其他信息。 常用选项&#xff1a; -a 列出目录下的所有文件&#xff0c;包括以 . 开头…

【LeetCode每日一题】单调栈 581. 最短无序连续子数组

581. 最短无序连续子数组 给你一个整数数组 nums &#xff0c;你需要找出一个 连续子数组 &#xff0c;如果对这个子数组进行升序排序&#xff0c;那么整个数组都会变为升序排序。 请你找出符合题意的 最短 子数组&#xff0c;并输出它的长度。 示例 1&#xff1a; 输入&am…

【lesson59】线程池问题解答和读者写者问题

文章目录 线程池问题解答什么是单例模式什么是设计模式单例模式的特点饿汉和懒汉模式的理解STL中的容器是否是线程安全的?智能指针是否是线程安全的&#xff1f;其他常见的各种锁 读者写者问题 线程池问题解答 什么是单例模式 单例模式是一种 “经典的, 常用的, 常考的” 设…

【软考】软件质量模型

目录 一、说明二、ISO/IEC 9126软件质量模型2.1 说明2.2 功能性2.3 可靠性2.4 易使用性2.5 效率2.6 可维护性2.7 可移植性 三、Mc Call软件质量模型 一、说明 1.软件质量是指反映软件系统或软件产品满足规定或隐含需求的能力的特征和特性全体。软件质量管理是指对软件开发过程进…

【Vuforia+Unity】01实现单张多张图片识别产生对应数字内容

1.官网注册 Home | Engine Developer Portal 2.下载插件SDK&#xff0c;导入Unity 3.官网创建数据库上传图片&#xff0c;官网处理成数据 下载好导入Unity&#xff01; 下载好导入Unity&#xff01; 下载好导入Unity&#xff01; 下载好导入Unity&#xff01; 4.在Unity设…

数据结构排序:插入排序、希尔排序、选择排序、冒泡排序、堆排序、快速排序

文章目录 插入排序希尔排序选择排序冒泡排序堆排序快速排序 插入排序 基本思想&#xff1a; 直接插入排序是一种简单的插入排序法&#xff0c;其基本思想是&#xff1a; 把待排序的值按其关键码值的大小逐个插入到一个已经排好序的有序序列中&#xff0c;直到所有的记录插入完…

MongoDB文档插入

文章目录 MongoDB文档插入对比增删改查文档插入 MongoDB写安全机制非确认式写入 MongoDB文档查询参数说明查询操作符比较查询操作符逻辑查询操作符元素查询操作符数组查询操作符 模糊查询区别:$regex操作符中的option选项 MongoDB游标介绍游标函数手动迭代游标示例游标介绍 Mon…

有事休假店铺无人看守怎么办?智能远程视频监控系统保卫店铺安全

在春节期间&#xff0c;很多自营店主也得到了久违的假期&#xff0c;虽然很多店主都是长期在店铺中看守&#xff0c;但遇到春节这样的日子&#xff0c;多数人还是选择回乡休假。面对店主休假或有事不能管理店铺时&#xff0c;传统的监控虽然可以做到单一的监控&#xff0c;却仍…

J-Flash J-Link解锁GD32单片机

目录 前言一、使用J-Flash工具解锁单片机1.打开J-Flash软件2.创建工程3.连接 J-Link4.解锁Flash 二、使用J-Link STM32 Unlock解锁GD321.打开J-Link STM32 Unlock工具2.命令行输入3.解锁成功4.验证 三、附录总结&#x1f600;*授人鱼,更要授人以渔&#xff0c;希望猿一的本篇博…

Jakarta Bean Validation

Validation 官网 https://beanvalidation.org/ 常见注解 Bean Validation中定义的注解&#xff1a; 注解详细信息Null被注释的元素必须为 nullNotNull被注释的元素必须不为 nullAssertTrue被注释的元素必须为 trueAssertFalse被注释的元素必须为 falseMin(value)被注释的元素…

不买服务器也可以将本地服务放到互联网(ngrok内网穿透)

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 不买服务器也可以将本地服务放到互联网 前言ngrok基础&#xff1a;穿越网络边界的魔法使用场景&#xff1a;突破网络限制的利器实战 前言 在网络的世界里&#xff0c;有时候你的服务像是困在一座数字…

IO进程线程作业day2

使用fread和fwrite完成两个图片文件的拷贝 #include <myhead.h> #define high 541 #define wide 541 int main(int argc, const char *argv[]) {//以只读的方式打开图片文件1.bmpFILE *fp NULL;if((fp fopen("./1.bmp", "r")) NULL){perror(&qu…

【RPG Maker MV 仿新仙剑 战斗场景UI (二)】

RPG Maker MV 仿新仙剑 战斗场景UI 二 战斗指令菜单原仙剑战斗指令图RMMV战斗指令对应代码战斗指令菜单代码效果 战斗指令菜单 原仙剑战斗指令菜单是使用方向键控制&#xff0c;同时按照使用情况正好对应四个指令和四个方向&#xff0c;同时没有选中的菜单用黑色透明图片覆盖&…

左右联动布局效果

效果图&#xff1a; <template><el-dialog :modelValue"modelValue" :before-close"close" fullscreen :close-on-click-modal"false"><div class"farmer_detail"><div class"info_content"><di…

基于Springboot+Vue的超市管理系统源码

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 随着社会经济的发展和…

力扣OJ题——相交链表

题目&#xff1a;160. 相交链表 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点&#xff0c;返回 null 思路一&#xff08;暴力求解&#xff09;&#xff1a; A链表的每个节点依次跟B链表中节点进行…

stm32 DCMI的知识点

1.DCMI的简介 DCMI全称Digital camera interface&#xff08;数字摄像头接口&#xff09;&#xff0c;是一种可以采集摄像头数据的一种接口。此接口适用于黑白摄像头、X24 和 X5 摄像头&#xff0c;并可以假定所有预处理&#xff08;如调整大小&#xff09;都可以在该摄像头模…

【动态规划专栏】专题一:斐波那契数列模型--------1.第N个泰波那契数

本专栏内容为&#xff1a;算法学习专栏&#xff0c;分为优选算法专栏&#xff0c;贪心算法专栏&#xff0c;动态规划专栏以及递归&#xff0c;搜索与回溯算法专栏四部分。 通过本专栏的深入学习&#xff0c;你可以了解并掌握算法。 &#x1f493;博主csdn个人主页&#xff1a;小…

【Go语言】Go语言的数据类型

GO 语言的数据类型 Go 语言内置对以下这些基本数据类型的支持&#xff1a; 布尔类型&#xff1a;bool 整型&#xff1a;int8、byte、int16、int、uint、uintptr 等 浮点类型&#xff1a;float32、float64 复数类型&#xff1a;complex64、complex128 字符串&#xff1a;st…

层级关联,审批人功能

一个需求要求选择一级&#xff0c;下方展示一级的效果 后端给了审批人数据&#xff0c;但是数据需要单独处理 <template><div class"box"><el-form :model"ruleForm" :rules"rules" ref"ruleForm" label-width"…