当前位置: 首页 > news >正文

基于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自己的内容,涉及的东西也不会少。

  您的支持是我更新的最大动力!将持续更新工程,如果本文对您有帮助,还请多多点赞👍、评论💬和收藏⭐!

相关文章:

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

1、概括 前文对IIC的时序做了详细的讲解&#xff0c;还有不懂的可以获取TI的IIC数据手册查看原理。通过手册需要知道的是IIC读、写数据都是以字节为单位&#xff0c;每次操作后接收方都需要进行应答。主机向从机写入数据后&#xff0c;从机接收数据&#xff0c;需要把总线拉低来…...

使用sql判断两段时间是否重叠

使用sql判断两段时间是否重叠 1. 时间点重叠判断a)时间重叠有以下4种情况a)时间不重叠只有以下2种情况 判断条件, 不重叠的判断判断条件, 重叠的判断 假设现在有时间 [startTime, endTime], 数据库存在字段 sql_start_time 和 sql_end_time, 分别表示要判断的时间段和数据库的时…...

C++模板从入门到入土

1. 泛型编程 如果我们需要实现一个不同类型的交换函数&#xff0c;如果是学的C语言&#xff0c;你要交换哪些类型&#xff0c;不同的类型就需要重新写一个来实现&#xff0c;所以这是很麻烦的&#xff0c;虽然可以cv一下&#xff0c;有了模板就可以减轻负担。 下面写一个适…...

Kotlin 中注解 @JvmOverloads 的作用

JvmOverloads 注解的作用就是&#xff1a;在有默认参数值的方法加上 JvmOverloads 注解&#xff0c;则 Kotlin 就会暴露多个重载方法。 例如&#xff0c;没有加注解&#xff0c;默认参数没有起到任何作用。 fun f(a: String, b: Int 0, c: String "abc") {}那相当…...

EI级 | Matlab实现TCN-GRU-MATT、TCN-GRU、TCN、GRU多变量时间序列预测对比

EI级 | Matlab实现TCN-GRU-MATT、TCN-GRU、TCN、GRU多变量时间序列预测对比 目录 EI级 | Matlab实现TCN-GRU-MATT、TCN-GRU、TCN、GRU多变量时间序列预测对比预测效果基本介绍程序设计参考资料 预测效果 基本介绍 【EI级】Matlab实现TCN-GRU-MATT、TCN-GRU、TCN、GRU多变量时间…...

MongoDB文档插入

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

涵盖5大领域的机器学习工具介绍

随着数据的产生及其使用量的不断增加&#xff0c;对机器学习模型的需求也在成倍增加。由于ML系统包含了算法和丰富的ML库&#xff0c;它有助于分析数据和做出决策。难怪机器学习的知名度越来越高&#xff0c;因为ML应用几乎主导了现代世界的每一个方面。随着企业对这项技术的探…...

git修改及合并commit提交

在开发过程中&#xff0c;保持代码记录清晰会更方便追踪&#xff0c;对代码审核人员也更有便宜。 修改commit提交 比如我们刚提交了一个commit&#xff0c;但之后要追加代码到已经推送到远程仓库的提交中&#xff0c;这时我们可以选择修改commit提交&#xff0c;使新的更改也推…...

大型语言模型的语义搜索(一):关键词搜索

关键词搜索(Keyword Search)是文本搜索种一种常用的技术&#xff0c;很多知名的应用app比如Spotify、YouTube 或 Google map等都会使用关键词搜索的算法来实现用户的搜索任务&#xff0c;关键词搜索是构建搜索系统最常用的方法&#xff0c;最常用的搜索算法是Okapi BM25&#x…...

无需统考可获双证的中国社科院-美国杜兰大学金融硕士

无需统考可获双证的中国社科院-美国杜兰大学金融硕士 中国社会科学院作为党和国家的思想库、智囊团&#xff0c;一直致力于金融财经领域政策的研究和咨询工作&#xff0c;在这个方面我们已经形成了深厚的积累。通过长期的研究和实践&#xff0c;我们能够深刻感受中国金融人才培…...

编程笔记 Golang基础 024 映射

编程笔记 Golang基础 024 映射 一、映射二、映射的定义与初始化三、基本操作四、综合示例程序 Go语言中的映射&#xff08;map&#xff09;是一种关联数组或哈希表数据结构&#xff0c;它存储键值对&#xff0c;其中每个键都是唯一的。在Go中&#xff0c;你可以使用 map[keyTy…...

基于springboot+vue的中小型医院网站(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…...

Spring boot 实现监听 Redis key 失效事件

一. 开启Redis key过期提醒 方式一&#xff1a;修改配置文件 redis.conf # 默认 notify-keyspace-events "" notify-keyspace-events Ex方式二&#xff1a;命令行开启 CONFIG SET notify-keyspace-events Ex CONFIG GET notify-keyspace-events二. notify-keyspace-e…...

振动样品磁强计

振动样品磁强计是基于电磁感应原理的高灵敏度磁矩测量仪。检测线圈中的振动产生的感应电压与样品的磁矩&#xff0c;振幅和振动频率成正比。在确保振幅和振动频率的不便的基础上&#xff0c;使用锁相放大器测量该电压&#xff0c;然后可以计算出待测样品的磁矩。 振动样品磁强计…...

C语言标准库介绍:<string.h>

在C语言中&#xff0c;<string.h>头文件是标准库中的一个重要部分&#xff0c;它定义了一系列操作字符串和字符数组的函数。本文将详细介绍<string.h>头文件中包含的22个函数&#xff0c;并提供每个函数的完整示例代码。 简介 <string.h>头文件定义了一个变…...

大语言模型LangChain本地知识库:向量数据库与文件处理技术的深度整合

文章目录 大语言模型LangChain本地知识库&#xff1a;向量数据库与文件处理技术的深度整合引言向量数据库在LangChain知识库中的应用文件处理技术在知识库中的角色向量数据库与文件处理技术的整合实践挑战与展望结论 大语言模型LangChain本地知识库&#xff1a;向量数据库与文件…...

展厅设计中都包含哪些分区与展示内容

1、欢迎区 欢迎区是展厅的入口处&#xff0c;通常展示企业品牌、企业标志和企业形象等内容。这个区域通常会有一个欢迎台&#xff0c;展示企业的宣传片、简介和最新资讯等。 2、产品展示区 产品展示区是展示企业产品的区域&#xff0c;展示的产品包括企业主营产品、新产品和重点…...

【k8s核心概念与专业术语】

k8s架构 1、服务的分类 服务分类按如下图根据数据服务支撑&#xff0c;分为无状态和有状态 无状态引用如下所示&#xff0c;如果一个nginx服务&#xff0c;删除后重新部署有可以访问&#xff0c;这个属于无状态&#xff0c;不涉及到数据存储。 有状态服务&#xff0c;如redis&a…...

【stm32】hal库学习笔记-UART/USART串口通信(超详细!)

【stm32】hal库学习笔记-UART/USART串口通信 hal库驱动函数 CubeMX图形化配置 导入LCD.ioc RTC设置 时钟树配置 设置LSE为RTC时钟源 USART设置 中断设置 程序编写 编写主函数 /* USER CODE BEGIN 2 */lcd_init();lcd_show_str(10, 10, 16, "Demo12_1:USART1-CH340&q…...

通俗易懂理解GhostNetV1轻量级神经网络模型

一、参考资料 原始论文&#xff1a;[1] PyTorch代码链接&#xff1a;Efficient-AI-Backbones MindSpore代码&#xff1a;ghostnet_d 解读模型压缩5&#xff1a;减少冗余特征的Ghost模块&#xff1a;华为Ghost网络系列解读 GhostNet论文解析&#xff1a;Ghost Module CVPR…...

P8630 [蓝桥杯 2015 国 B] 密文搜索

P8630 [蓝桥杯 2015 国 B] 密文搜索 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)https://www.luogu.com.cn/problem/P8630 题目分析 基本上是hash的板子&#xff0c;但实际上对于密码串&#xff0c;只要判断主串中任意连续的八个位置是否存在密码串即可&#xff1b;那么我们…...

Electron实战之环境搭建

工欲善其事必先利其器&#xff0c;在进行实战开发的时候&#xff0c;我们最终的步骤是搞好一个舒服的开发环境&#xff0c;目前支持 Vue 的 Electron 工程化工具主要有 electron-vue、Vue CLI Plugin Electron Builder、electron-vite。 接下来我们将分别介绍基于 Vue CLI Plu…...

【0259】inval.h/inval.c的理解

1. inval.h/inval.c inval.h、inval.c是缓存无效消息(invalidation message)调度程序定义。 2. inval.h/inval.c特性 inval.h/inval.c的实现是一个非常微妙的东西,所以需要注意: 当一个元组被更新或删除时,我们的标准可见性规则(standard visibility rules)认为只要我…...

力扣爆刷第77天--动态规划一网打尽打家劫舍问题

力扣爆刷第77天–动态规划一网打尽打家劫舍问题 文章目录 力扣爆刷第77天--动态规划一网打尽打家劫舍问题一、198.打家劫舍二、213.打家劫舍II三、337.打家劫舍 III 一、198.打家劫舍 题目链接&#xff1a;https://leetcode.cn/problems/house-robber/ 思路&#xff1a;小偷不…...

深入理解C语言(5):程序环境和预处理详解

文章主题&#xff1a;程序环境和预处理详解&#x1f30f;所属专栏&#xff1a;深入理解C语言&#x1f4d4;作者简介&#xff1a;更新有关深入理解C语言知识的博主一枚&#xff0c;记录分享自己对C语言的深入解读。&#x1f606;个人主页&#xff1a;[₽]的个人主页&#x1f3c4…...

ESP8266智能家居(3)——单片机数据发送到mqtt服务器

1.主要思想 前期已学习如何用ESP8266连接WIFI&#xff0c;并发送数据到服务器。现在只需要在单片机与nodeMCU之间建立起串口通信&#xff0c;这样单片机就可以将传感器测到的数据&#xff1a;光照&#xff0c;温度&#xff0c;湿度等等传递给8266了&#xff0c;然后8266再对数据…...

lvm逻辑卷创建raid阵列(不常用)—— 筑梦之路

RAID卷介绍 逻辑卷管理器(LVM)不仅仅可以将多个磁盘和分区聚合到一个逻辑卷中&#xff0c;以此提高单个分区的存储容量&#xff0c;还可以创建和管理独立磁盘的冗余阵列(RAID)卷&#xff0c;防止磁盘故障并提高性能。它支持常用的RAID级别&#xff0c;支持的RAID的级别有 0、1…...

LayUI发送Ajax请求

页面初始化操作 var processData null $(function () {initView();initTable();// test(); })function initView() {layui.use([laydate, form], function () {var laydate layui.laydate;laydate.render({elem: #applyDateTimeRange,type: datetime,range: true});}); }初始…...

平时积累的FPGA知识点(10)

平时在FPGA群聊等积累的FPGA知识点&#xff0c;第10期&#xff1a; 41 ZYNQ系列芯片的PL中使用PS端送过来的时钟&#xff0c;这些时钟名字是自动生成的吗&#xff1f; 解释&#xff1a;是的。PS端设置的是ps_clk&#xff0c;用report_clocks查出来的时钟名变成了clk_fpga_0&a…...

使用Streamlit构建纯LLM Chatbot WebUI傻瓜教程

文章目录 使用Streamlit构建纯LLM Chatbot WebUI傻瓜教程开发环境hello Streatelit显示DataFrame数据显示地图WebUI左右布局设置st.sidebar左侧布局st.columns右侧布局 大语言模型LLM Chatbot WebUI设置Chatbot页面布局showdataframe()显示dataframeshowLineChart()显示折线图s…...