做推广网站多少钱,wordpress4.8.3中文,wordpress 如何上传头像,anivia wordpress templates 1.31、概括 前文对IIC的时序做了详细的讲解#xff0c;还有不懂的可以获取TI的IIC数据手册查看原理。通过手册需要知道的是IIC读、写数据都是以字节为单位#xff0c;每次操作后接收方都需要进行应答。主机向从机写入数据后#xff0c;从机接收数据#xff0c;需要把总线拉低来…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系统时钟信号默认100MHzrst_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而状态机位于WDATAW_REG_ADDRRDATA状态时每次读写的单位都是1字节数据、应答位所以bit_cnt计数器最大值为9-1。 计数器byte_cnt用于记录状态机处于WDATAW_REG_ADDRRDATA状态时接收或者发送的数据字节数。当状态机处于上述三个状态且计数器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 7b1010000 //器件地址。
)(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 {1b0,DEVICE_ADDR,1b0};//开始信号有效时把待发送的信号暂存。always(posedge clk or negedge rst_n)beginif(rst_n1b0)begin//初始值为0;wdata_r 0;rw_flag_r 1b0;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_n1b0)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_n1b0)begin//初始值为0;l2h_flag 1b0;h2l_flag 1b0;wr_flag 1b0;rd_flag 1b0;end_div_cnt 1b0;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_n1b0)begin//初始值为0bit_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_n1b0)begin//初始值为0;bit_cnt_num 4d9;end//写器件地址和起始位、读写指示位总共是10位数据所以计数器的最大值为10-1else if((state_c W_DEVICE_ADDR) || (state_c R_DEVICE_ADDR))beginbit_cnt_num 4d10;endelse begin//其余状态下计数器最大值为9。bit_cnt_num 4d9;endend然后是用来记录状态机在写寄存器地址、读写数据阶段读写数据字节数的计数器byte_cnt对应代码如下图所示。当状态机处于这几个状态下计数器bit_cnt计数结束时加1读写数据的最大值在状态机不同状态页不相同与前文设置的parameter参数有关通过byte_cnt_num信号的值控制计数器byte_cnt的最大值。 //发送字节数的计数器用于计数发送数据的字节数据。always(posedge clk or negedge rst_n)beginif(rst_n1b0)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_n1b0)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_n1b0)begin//初始值为0;scl 1b1;end//当拉高条件有效或者状态机处于空闲状态时拉高。else if(l2h_flag || state_c IDLE)beginscl 1b1;end//只有在初始发送起始位时满足拉低条件时不拉低其余情况下满足条件均要拉低else if((((state_c W_DEVICE_ADDR) bit_cnt 0) || (state_c ! W_DEVICE_ADDR)) h2l_flag)beginscl 1b0;endend然后生成串行数据输出信号sda_out初始时该信号为高电平状态机在不同状态输出不同数据即可。状态机处于R_REG_ADDR、RDATA、STOP需要特别注意重复起始位的产生需要在写数据bit_cnt0 wr_flag时拉高然在scl的高电平中间bit_cnt0 rd_flag拉低。读指示位bit_cnt bit_cnt_num-2 wr_flag需要输出高电平。 读数据阶段主机需要在读取完最后一字节数据后输出高电平表示不应答从机如果读取的数据不是最后一字节数据则输出低电平应答从机继续接收从机输出的数据。 然后在发送停止位时需要先在scl为低电平时把sda拉低在scl为高电平时拉高sda从而表示出停止位。 //赋值输出信号always(posedge clk or negedge rst_n)beginif(rst_n1b0)begin//初始值为0;sda_out 1b1;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 1b1;else//产生起始位之后在SCL低电平中间发送器件地址sda_out device_addr[8 - bit_cnt];else if(rd_flag bit_cnt 0)//在SCL高电平的时候拉低SDA发送重复起始位sda_out 1b0;endRDATA : beginif(bit_cnt bit_cnt_num - 1 wr_flag)if(byte_cnt DATA_BYTE_NUM - 1)//如果是读取的最后一字节数据则不应答。sda_out 1b1;else//如果不是最后一字节数据则进行应答。sda_out 1b0;endSTOP : beginif(wr_flag)//停止信号需要先拉低sda_out 1b0;else if(rd_flag)//在SCL高电平的时候拉高表示停止位sda_out 1b1;enddefault : sda_out sda_out;endcaseendend上述生成了串行数据的输出信号接下来就需要生成三态门使能信号sda_out_en把上面生成的数据输出。如果使用iobufr则可以省略该信号。因为一般系统中只会存在一个主机所以主机除了需要从机做出应答的状态其余时间主机全程驱动数据线。 //赋值输出使能信号除了从机应答之外其余全为高电平always(posedge clk or negedge rst_n)beginif(rst_n1b0)begin//初始值为1;sda_out_en 1b1;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 1b1;else if(bit_cnt bit_cnt_num - 1)//当写入最后一位数据后将使能信号拉低释放总线sda_out_en 1b0;endSTOP : beginif(bit_cnt 0)//当计数器为0时总线拉高开始写入下一字节数据sda_out_en 1b1;endRDATA : begin//在读数据阶段主机应答时需要控制总线其余时间释放总线if(bit_cnt 0)sda_out_en 1b0;else if(bit_cnt bit_cnt_num - 1)sda_out_en 1b1;enddefault: ;endcaseendend因此使能信号初始为高电平状态机处于W_DEVICE_ADDR、WDATA、R_DEVICE_ADDR、W_REG_ADDR、STOP状态时在从机应答的时候释放总线。而读数据状态RDATA只有在应答时主机才控制总线所以与其他状态的控制状态刚好相反。STOP状态下bit_cntbit_cnt_num-1不会满足所以使能在该状态下不会被拉低。 然后就是主机接收从机的数据了如下所示为了该用户接口的信号保持稳定则将接收完成的数据打一拍后输出在SCL的中部接收数据先接收的数据位于高字节的高位。 //在读数据阶段读取总线上的数据always(posedge clk or negedge rst_n)beginif(rst_n1b0)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 1b0;else//其余时间拉高表示模块处于空闲状态上游模块可以发起写或者读操作rdy 1b1;end//从机应答失败标志信号高电平表示应答失败每次开始读写操作时清零always(posedge clk or negedge rst_n)beginif(rst_n1b0)begin//初始值为0;ack_flag 1b0;endelse if(start)begin//接收到开始读写请求信号时拉低ack_flag 1b0;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;endend4、参考代码 上述就是该模块的设计是不是也很简单总共加注释也就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 7b1010000 //器件地址。
)(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 7b0000001 ;//状态机空闲状态localparam W_DEVICE_ADDR 7b0000010 ;//状态机写器件地址状态localparam W_REG_ADDR 7b0000100 ;//状态机写寄存器地址状态localparam WDATA 7b0001000 ;//状态机写数据状态localparam R_DEVICE_ADDR 7b0010000 ;//状态机发送读器件地址状态localparam RDATA 7b0100000 ;//状态机读数据状态localparam STOP 7b1000000 ;//状态机停止状态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 : 1bz;//自动计算位宽函数function integer clogb2(input integer depth);beginif(depth 0)clogb2 1;else if(depth ! 0)for(clogb20 ; depth0 ; clogb2clogb21)depthdepth 1;endendfunction//暂存器件地址和起始位还有写指示位。assign device_addr {1b0,DEVICE_ADDR,1b0};//开始信号有效时把待发送的信号暂存。always(posedge clk or negedge rst_n)beginif(rst_n1b0)begin//初始值为0;wdata_r 0;rw_flag_r 1b0;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_n1b0)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_n1b0)begin//初始值为0;l2h_flag 1b0;h2l_flag 1b0;wr_flag 1b0;rd_flag 1b0;end_div_cnt 1b0;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_n1b0)begin//初始值为0bit_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_n1b0)begin//初始值为0;bit_cnt_num 4d9;end//写器件地址和起始位、读写指示位总共是10位数据所以计数器的最大值为10-1else if((state_c W_DEVICE_ADDR) || (state_c R_DEVICE_ADDR))beginbit_cnt_num 4d10;endelse begin//其余状态下计数器最大值为9。bit_cnt_num 4d9;endend//发送字节数的计数器用于计数发送数据的字节数据。always(posedge clk or negedge rst_n)beginif(rst_n1b0)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_n1b0)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_n1b0)begin//初始值为0;scl 1b1;end//当拉高条件有效或者状态机处于空闲状态时拉高。else if(l2h_flag || state_c IDLE)beginscl 1b1;end//只有在初始发送起始位时满足拉低条件时不拉低其余情况下满足条件均要拉低else if((((state_c W_DEVICE_ADDR) bit_cnt 0) || (state_c ! W_DEVICE_ADDR)) h2l_flag)beginscl 1b0;endend//赋值输出信号always(posedge clk or negedge rst_n)beginif(rst_n1b0)begin//初始值为0;sda_out 1b1;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 1b1;else//产生起始位之后在SCL低电平中间发送器件地址sda_out device_addr[8 - bit_cnt];else if(rd_flag bit_cnt 0)//在SCL高电平的时候拉低SDA发送重复起始位sda_out 1b0;endRDATA : beginif(bit_cnt bit_cnt_num - 1 wr_flag)if(byte_cnt DATA_BYTE_NUM - 1)//如果是读取的最后一字节数据则不应答。sda_out 1b1;else//如果不是最后一字节数据则进行应答。sda_out 1b0;endSTOP : beginif(wr_flag)//停止信号需要先拉低sda_out 1b0;else if(rd_flag)//在SCL高电平的时候拉高表示停止位sda_out 1b1;enddefault : sda_out sda_out;endcaseendend//赋值输出使能信号除了从机应答之外其余全为高电平always(posedge clk or negedge rst_n)beginif(rst_n1b0)begin//初始值为1;sda_out_en 1b1;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 1b1;else if(bit_cnt bit_cnt_num - 1)//当写入最后一位数据后将使能信号拉低释放总线sda_out_en 1b0;endSTOP : beginif(bit_cnt 0)//当计数器为0时总线拉高开始写入下一字节数据sda_out_en 1b1;endRDATA : begin//在读数据阶段主机应答时需要控制总线其余时间释放总线if(bit_cnt 0)sda_out_en 1b0;else if(bit_cnt bit_cnt_num - 1)sda_out_en 1b1;enddefault: ;endcaseendend//在读数据阶段读取总线上的数据always(posedge clk or negedge rst_n)beginif(rst_n1b0)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 1b0;else//其余时间拉高表示模块处于空闲状态上游模块可以发起写或者读操作rdy 1b1;end//从机应答失败标志信号高电平表示应答失败每次开始读写操作时清零always(posedge clk or negedge rst_n)beginif(rst_n1b0)begin//初始值为0;ack_flag 1b0;endelse if(start)begin//接收到开始读写请求信号时拉低ack_flag 1b0;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自己的内容涉及的东西也不会少。 您的支持是我更新的最大动力将持续更新工程如果本文对您有帮助还请多多点赞、评论和收藏⭐