基于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.rhkb.cn/news/263838.html

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

相关文章

网络安全“三保一评”深度解析

“没有网络安全就没有国家安全”。近几年&#xff0c;我国法律法规陆续发布实施&#xff0c;为承载我国国计民生的重要网络信息系统的安全提供了法律保障&#xff0c;正在实施的“3保1评”为我国重要网络信息系统的安全构筑了四道防线。 什么是“3保1评”&#xff1f; 等保、分…

QlikSense CyberSecurity : Configuring preferred Cipher Suites

You can rank the preferred cipher suites that Qlik License Service uses to encrypt and decrypt the signed key license.您可以对Qlik许可证服务用于加密和解密签名密钥许可证的首选密码套件进行排序。 The Qlik License Service is included in Qlik Sense Enterprise …

react中修改state中的值无效?

// 初始化state state {personArr:[{name:张三,id:1},{name:李四,id:2},{name:王五,id:3}] }componentDidMount(){const newName 赵六const indexUpdate 1const newArr this.state.personArr.map((item,index)>{if(indexUpdate index){return {...item,name:newName}}e…

【C++ QT项目5】——基于HTTP与JSON数据流的天气预报界面设计

【C QT项目5】——基于HTTP与JSON数据流的天气预报界面设计 一、项目概述二、UI设计与stylesheet样式表三、天气预报数据接口四、JSON数据4.1 概述4.2 QT生成JSON数据4.3 QT解析JSON数据4.4 将JSON数据解析到QMap中 五、软件开发网络通信架构5.1 BS架构/CS架构5.2 HTTP基本概念…

leetcode hot100 买卖股票的最佳时机二

注意&#xff0c;本题是针对股票可以进行多次交易&#xff0c;但是下次买入的时候必须保证上次买入的已经卖出才可以。 动态规划可以解决整个股票买卖系列问题。 dp数组含义&#xff1a; dp[i][0]表示第i天不持有股票的最大现金 dp[i][1]表示第i天持有股票的最大现金 递归公…

SpringBoot Admin 详解

SpringBoot Admin 详解 一、Actuator 详解1.Actuator原生端点1.1 监控检查端点&#xff1a;health1.2 应用信息端点&#xff1a;info1.3 http调用记录端点&#xff1a;httptrace1.4 堆栈信息端点&#xff1a;heapdump1.5 线程信息端点&#xff1a;threaddump1.6 获取全量Bean的…

栈的最后表演:逆波兰表达式求值

前言 今天刷题遇到了逆波兰表达式&#xff0c;死亡的记忆突然开始攻击我&#xff0c;好嘛&#xff0c;既然根基不牢&#xff0c;那么就一次性给他搞明白了&#xff01; 一、算术表达式求值 算术表达式又叫中缀表达式&#xff0c;如果直接给出一个中缀表达式让我们求值&#…

Spring Boot 笔记 025 主界面

1.1 路由搭建 1.1.1 安装vue router npm install vue-router4 1.1.2 在src/router/index.js中创建路由器&#xff0c;并导出 import { createRouter, createWebHistory } from vue-router//导入组件 import LoginVue from /views/Login.vue import LayoutVue from /views/La…

cmake 项目。qt5升级 qt6 报错 error: “Qt requires a C++17 compiler 已解决

日常项目开发中。需要对qt5升级到qt6 做cmake兼容配置&#xff0c;在编译中发现&#xff0c;有c 编译环境 报错 2>C:\Qt\6.5.3\msvc2019_64\include\QtCore/qcompilerdetection.h(1226,1): fatal error C1189: #error: "Qt requires a C17 compiler, and a suitable …

JavaSec 基础之 XXE

文章目录 XMLReaderSAXReaderSAXBuilderDocumentBuilderUnmarshaller**SAXParserFactory**XMLReaderFactoryDigester总结 XMLReader public String XMLReader(RequestBody String content) {try {XMLReader xmlReader XMLReaderFactory.createXMLReader();// 修复&#xff1a…

Spring之AOP源码解析(下)

前言 在上一遍文章中,我们主要讲解了ProxyFactory在Spring完成AOP动态代理的过程中发挥的作用。这一篇我们主要讲解这些注解都是如何注入Advisors,然后分析这些Advisors生效的条件 注解都是如何注入Advisor并匹配的 EnableTransactionManagement注解 我们在之前提到EnableT…

【Java程序设计】【C00290】基于Springboot的网上书城管理系统(有论文)

基于Springboot的网上书城管理系统&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 这是一个基于Springboot的网上书城管理系统 本系统分为系统功能模块、管理员功能模块以及用户功能模块。 系统功能模块&#xff1a;在系统首页可以查看首…

Day07-面向对象-封装课后练习以及参考答案

文章目录 巩固题包练习成员变量基础题第1题&#xff1a;员工第2题&#xff1a;日期类 成员方法基础题第3题&#xff1a;三角形第4题&#xff1a;日期类第5题&#xff1a;数组工具类 拔高题第6题&#xff1a;公民类第7题&#xff1a;数组工具类 巩固题 包练习 每道作业题可以单…

12 个顶级音频转换器软件(免费)

当涉及不受支持的音乐文件时&#xff0c;音频文件转换器软件总是会派上用场。当您希望缩小大量大型音乐文件的大小以节省设备存储空间时&#xff0c;它也很有帮助。您在寻找传输音频的软件吗&#xff1f;好吧&#xff0c;请仔细选择音频转换器&#xff0c;因为最好的音乐转换器…

C语言数据存储

目录 一.数据类型的介绍 &#xff08;1&#xff09;整形家族 &#xff08;2&#xff09;浮点型家族 &#xff08;3&#xff09;构造类型 &#xff08;4&#xff09;其他 二.整形在内存中如何进行存储 &#xff08;1&#xff09;原&#xff0c;反&#xff0c;补 &#xf…

《Docker 简易速速上手小册》第1章 Docker 基础入门(2024 最新版)

文章目录 1.1 Docker 简介与历史1.1.1 Docker 基础知识1.1.2 重点案例&#xff1a;Python Web 应用的 Docker 化1.1.3 拓展案例 1&#xff1a;使用 Docker 进行 Python 数据分析1.1.4 拓展案例 2&#xff1a;Docker 中的 Python 机器学习环境 1.2 安装与配置 Docker1.2.1 重点基…

k8s-hpa控制器 16

hpa可通过metrics-server所提供pod的cpu或者内存的负载情况&#xff0c;从而动态拉伸控制器的副本数&#xff0c;从而达到后端的自动弹缩 官网&#xff1a;https://kubernetes.io/zh/docs/tasks/run-application/horizontal-pod-autoscalewalkthrough/ 上传镜像 创建hpa实例 …

Java语言实现学生成绩排序问题

目录 题目&#xff1a; 代码展示&#xff1a; Student类 TestStudent类 运行结果 ​编辑 题目&#xff1a; 给定一段字符串,里面包含若干个学生上机和笔试成绩如 String str "张三:上机成绩90,笔试成绩78; 李四:上机成绩68,笔试成绩98; …

React18源码: reconcliler启动过程

Reconcliler启动过程 Reconcliler启动过程实际就是React的启动过程位于react-dom包&#xff0c;衔接reconciler运作流程中的输入步骤.在调用入口函数之前&#xff0c;reactElement(<App/>) 和 DOM对象 div#root 之间没有关联&#xff0c;用图片表示如下&#xff1a; 在启…

反序列化字符串逃逸 [安洵杯 2019]easy_serialize_php1

打开题目 $_SESSION是访客与整个网站交互过程中一直存在的公有变量 然后看extract()函数的功能&#xff1a; extract($_POST)就是将post的内容作为这个函数的参数。 extract() 函数从数组中将变量导入到当前的符号表(本题的作用是将_SESSION的两个函数变为post传参) function…