17_IIC协议与FPGA驱动AT24C04
实验原理
什么是IIC
IIC即I2C,一种总线结构。IIC 即Inter-Integrated Circuit,这种总线类型是由菲利浦半导体公司在八十年代初设计出来的,主要是用来连接整体电路(ICS) ,IIC是一种多向控制总线,也就是说多个芯片可以连接到同一总线结构下,同时每个芯片都可以作为实施数据传输的控制源。这种方式简化了信号传输总线。例如:内存中的SPD信息,通过IIC,与BX芯片组联系,IIC 存在于英特尔PIIX4结构体系中。
随着大规模集成电路技术的发展,把CPU和一个单独工作系统所必需的ROM、RAM、I/O端口、A/D、D/A等外围电路集成在一个单片内而制成的单片机或微控制器愈来愈方便。目前,世界上许多公司生产单片机,品种很多。其中包括各种字长的CPU,各种容量的ROM、RAM以及功能各异的I/O接口电路等等,但是,单片机的品种规格仍然有限,所以只能选用某种单片机来进行扩展。扩展的方法有两种:一种是并行总线,另一种是串行总线。由于串行总线的连线少,结构简单,往往不用专门的母板和插座而直接用导线连接各个设备。因此,采用串行线可大大简化系统的硬件设计。PHILIPS公司早在十几年前就推出了I2C串行总线,利用该总线可实现多主机系统所需的裁决和高低速设备同步等功能。因此,这是一种高性能的串行总线。
飞利浦电子公司日前推出新型二选一I2C主选择器,可以使两个I2C主设备中的任何一个与共享资源连接,广泛适用于从MP3播放器到服务器等计算、通信和网络应用领域,从而使制造商和终端用户从中获益。PCA9541可以使两个I2C主设备在互不连接的情况下与同一个从设备相连接,从而简化了设计的复杂性。此外,新产品以单器件替代了I2C多个主设备应用中的多个芯片,有效节省了系统成本。
IIC的硬件结构
I2C串行总线一般有两根信号线,一根是双向的数据线SDA,另一根是时钟线SCL。所有接到I2C总线设备上的串行数据SDA都接到总线的SDA上,各设备的时钟线SCL接到总线的SCL上。
为了避免总线信号的混乱,要求各设备连接到总线的输出端时必须是漏极开路(OD)输出或集电极开路(OC)输出。设备上的串行数据线SDA接口电路应该是双向的,输出电路用于向总线上发送数据,输入电路用于接收总线上的数据。而串行时钟线也应是双向的,作为控制总线数据传送的主机,一方面要通过SCL输出电路发送时钟信号,另一方面还要检测总线上的SCL电平,以决定什么时候发送下一个时钟脉冲电平;作为接受主机命令的从机,要按总线上的SCL信号发出或接收SDA上的信号,也可以向SCL线发出低电平信号以延长总线时钟信号周期。总线空闲时,因各设备都是开漏输出,上拉电阻Rp使SDA和SCL线都保持高电平。任一设备输出的低电平都将使相应的总线信号线变低,也就是说:各设备的SDA是"与"关系,SCL也是"与"关系。
总线对设备接口电路的制造工艺和电平都没有特殊的要求(NMOS、CMOS都可以兼容)。在I2C总线上的数据传送率可高达每秒十万位,高速方式时在每秒四十万位以上。另外,总线上允许连接的设备数以其电容量不超过400pF为限。
总线的运行(数据传输)由主机控制。所谓主机是指启动数据的传送(发出启动信号)、发出时钟信号以及传送结束时发出停止信号的设备,通常主机都是微处理器。被主机寻访的设备称为从机。为了进行通讯,每个接到I2C总线的设备都有一个唯一的地址,以便于主机寻访。主机和从机的数据传送,可以由主机发送数据到从机,也可以由从机发到主机。凡是发送数据到总线的设备称为发送器,从总线上接收数据的设备被称为接受器。
I2C总线上允许连接多个微处理器以及各种外围设备,如存储器、LED及LCD驱动器、A/D及D/A转换器等。为了保证数据可靠地传送,任一时刻总线只能由某一台主机控制,各微处理器应该在总线空闲时发送启动数据,为了妥善解决多台微处理器同时发送启动数据的传送(总线控制权)冲突,以及决定由哪一台微处理器控制总线的问题,I2C总线允许连接不同传送速率的设备。多台设备之间时钟信号的同步过程称为同步化。
IIC的数据传输
在I2C总线传输过程中,将两种特定的情况定义为开始和停止条件(见下图):当SCL保持"高"时,SDA由"高"变为"低"为开始条件;当SCL保持"高"且SDA由"低"变为"高"时为停止条件。开始和停止条件均由主控制器产生。使用硬件接口可以很容易地检测到开始和停止条件,没有这种接口的微机必须以每时钟周期至少两次对SDA取样,以检测这种变化。
SDA线上的数据在时钟"高"期间必须是稳定的,只有当SCL线上的时钟信号为低时,数据线上的"高"或"低"状态才可以改变。输出到SDA线上的每个字节必须是8位,每次传输的字节不受限制,但每个字节必须要有一个应答ACK。如果一接收器件在完成其他功能(如一内部中断)前不能接收另一数据的完整字节时,它可以保持时钟线SCL为低,以促使发送器进入等待状态;当接收器准备好接受数据的其它字节并释放时钟SCL后,数据传输继续进行。
数据传送具有应答是必须的。与应答对应的时钟脉冲由主控制器产生,发送器在应答期间必须下拉SDA线。当寻址的被控器件不能应答时,数据保持为高并使主控器产生停止条件而终止传输。在传输的过程中,在用到主控接收器的情况下,主控接收器必须发出一数据结束信号给被控发送器,从而使被控发送器释放数据线,以允许主控器产生停止条件。
I2C总线在开始条件后的首字节决定哪个被控器将被主控器选择,例外的是"通用访问"地址,它可以在所有期间寻址。当主控器输出一地址时,系统中的每一器件都将开始条件后的前7位地址和自己的地址进行比较。如果相同,该器件即认为自己被主控器寻址,而作为被控接收器或被控发送器则取决于R/W位。
基于AT24C04的IIC通信协议
在使用开发板做实验之前,希望大家先对IIC协议有足够的了解。我们的开发板是基于AT24C02的IIC通信协议。如果对IIC的协议不是很熟悉的同学,可以先去看我们附带的文档资料或者上网了解IIC。
在我们实验之前,我们还是先回顾下基于AT24C02的IIC通信协议。下图分别为单字节写时序和随机读时序。
单字节写时序
随机读时序
IIC通信中只涉及两条信号线,即时钟线SCL和数据线SDA。时钟线为高电平时均可所存数据,即时钟线上升沿和下降沿之间。当时钟线SCL为高电平时,如果把数据线SDA从高电平拉到低电平,则表示通信开始;如果把数据线SDA从低电平拉到高电平,则表示通信结束。器件地址(DEVICE ADDRESS)的定义如下图所示。最低位(LSB)R/W表示读或者写状态,1表示读,0表示写。
器件地址字节定义
硬件原理图
实验代码
顶层原理图设计
IIC
/********************************版权声明************************************** ** 大西瓜团队 ** **----------------------------文件信息-------------------------- ** 文件名称: ** 创建日期:2012.10.9 ** 功能描述: ** 操作过程: ** 硬件平台:大西瓜第3代开发板 ** 版权声明:本代码属个人知识产权,本代码仅供交流学习. **---------------------------修改文件的相关信息---------------- ** 修改人: ** 修改日期: ** 修改内容: *******************************************************************************/
module iic_com( clk,rst_n, sw1,sw2, scl,sda, dis_data );
input clk; // 50MHz input rst_n; //复位信号,低有效 input sw1,sw2; //按键1、2,(1按下执行写入操作,2按下执行读操作) output scl; // 24C02的时钟端口 inout sda; // 24C02的数据端口 output[7:0] dis_data; //数码管显示的数据
//按键检测 reg sw1_r,sw2_r; //键值锁存寄存器,每20ms检测一次键值 reg[19:0] cnt_20ms; //20ms计数寄存器
always @ (posedge clk or negedge rst_n) begin if(!rst_n) cnt_20ms <= 20'd0; else cnt_20ms <= cnt_20ms+1'b1; //不断计数 end
always @ (posedge clk or negedge rst_n) begin if(!rst_n) begin sw1_r <= 1'b1; //键值寄存器复位,没有键盘按下时键值都为1 sw2_r <= 1'b1; end else if(cnt_20ms == 20'hfffff) begin sw1_r <= sw1; //按键1值锁存 sw2_r <= sw2; //按键2值锁存 end end //--------------------------------------------- //分频部分 reg[2:0] cnt; // cnt=0:scl上升沿,cnt=1:scl高电平中间,cnt=2:scl下降沿,cnt=3:scl低电平中间 reg[8:0] cnt_delay; //500循环计数,产生iic所需要的时钟 reg scl_r; //时钟脉冲寄存器
always @ (posedge clk or negedge rst_n) begin if(!rst_n) cnt_delay <= 9'd0; else if(cnt_delay == 9'd499) cnt_delay <= 9'd0; //计数到10us为scl的周期,即100KHz else cnt_delay <= cnt_delay+1'b1; //时钟计数 end
always @ (posedge clk or negedge rst_n) begin if(!rst_n) cnt <= 3'd5; else begin case (cnt_delay) 9'd124: cnt <= 3'd1; //cnt=1:scl高电平中间,用于数据采样 9'd249: cnt <= 3'd2; //cnt=2:scl下降沿 9'd374: cnt <= 3'd3; //cnt=3:scl低电平中间,用于数据变化 9'd499: cnt <= 3'd0; //cnt=0:scl上升沿 default: cnt <= 3'd5; endcase end end `define SCL_POS (cnt==3'd0) //cnt=0:scl上升沿 `define SCL_HIG (cnt==3'd1) //cnt=1:scl高电平中间,用于数据采样 `define SCL_NEG (cnt==3'd2) //cnt=2:scl下降沿 `define SCL_LOW (cnt==3'd3) //cnt=3:scl低电平中间,用于数据变化
always @ (posedge clk or negedge rst_n) begin if(!rst_n) scl_r <= 1'b0; else if(cnt==3'd0) scl_r <= 1'b1; //scl信号上升沿 else if(cnt==3'd2) scl_r <= 1'b0; //scl信号下降沿 end assign scl = scl_r; //产生iic所需要的时钟 //--------------------------------------------- //需要写入24C02的地址和数据
`define DEVICE_READ 8'b1010_0001 //被寻址器件地址(读操作) `define DEVICE_WRITE 8'b1010_0000 //被寻址器件地址(写操作) `define WRITE_DATA 8'b1101_0001 //写入EEPROM的数据 `define BYTE_ADDR 8'b0000_0011 //写入/读出EEPROM的地址寄存器 reg[7:0] db_r; //在IIC上传送的数据寄存器 reg[7:0] read_data; //读出EEPROM的数据寄存器
//--------------------------------------------- //读、写时序 parameter IDLE = 4'd0; parameter START1 = 4'd1; parameter ADD1 = 4'd2; parameter ACK1 = 4'd3; parameter ADD2 = 4'd4; parameter ACK2 = 4'd5; parameter START2 = 4'd6; parameter ADD3 = 4'd7; parameter ACK3 = 4'd8; parameter DATA = 4'd9; parameter ACK4 = 4'd10; parameter STOP1 = 4'd11; parameter STOP2 = 4'd12;
reg[3:0] cstate; //状态寄存器 reg sda_r; //输出数据寄存器 reg sda_link; //输出数据sda信号inout方向控制位 reg[3:0] num;
always @ (posedge clk or negedge rst_n) begin if(!rst_n) begin cstate <= IDLE; sda_r <= 1'b1; sda_link <= 1'b0; num <= 4'd0; read_data <= 8'b0000_0000; end else case (cstate) IDLE: begin sda_link <= 1'b1; //数据线sda为output sda_r <= 1'b1; if(!sw1_r || !sw2_r) begin //SW1,SW2键有一个被按下 db_r <= `DEVICE_WRITE; //送器件地址(写操作) cstate <= START1; end else cstate <= IDLE; //没有任何键被按下 end START1: begin if(`SCL_HIG) begin //scl为高电平期间 sda_link <= 1'b1; //数据线sda为output sda_r <= 1'b0; //拉低数据线sda,产生起始位信号 cstate <= ADD1; num <= 4'd0; //num计数清零 end else cstate <= START1; //等待scl高电平中间位置到来 end ADD1: begin if(`SCL_LOW) begin if(num == 4'd8) begin num <= 4'd0; //num计数清零 sda_r <= 1'b1; sda_link <= 1'b0; //sda置为高阻态(input) cstate <= ACK1; end else begin cstate <= ADD1; num <= num+1'b1; case (num) 4'd0: sda_r <= db_r[7]; 4'd1: sda_r <= db_r[6]; 4'd2: sda_r <= db_r[5]; 4'd3: sda_r <= db_r[4]; 4'd4: sda_r <= db_r[3]; 4'd5: sda_r <= db_r[2]; 4'd6: sda_r <= db_r[1]; 4'd7: sda_r <= db_r[0]; default: ; endcase // sda_r <= db_r[4'd7-num]; //送器件地址,从高位开始 end end // else if(`SCL_POS) db_r <= {db_r[6:0],1'b0}; //器件地址左移1bit else cstate <= ADD1; end ACK1: begin if(/*!sda*/`SCL_NEG) begin //注:24C01/02/04/08/16器件可以不考虑应答位 cstate <= ADD2; //从机响应信号 db_r <= `BYTE_ADDR; // 1地址 end else cstate <= ACK1; //等待从机响应 end ADD2: begin if(`SCL_LOW) begin if(num==4'd8) begin num <= 4'd0; //num计数清零 sda_r <= 1'b1; sda_link <= 1'b0; //sda置为高阻态(input) cstate <= ACK2; end else begin sda_link <= 1'b1; //sda作为output num <= num+1'b1; case (num) 4'd0: sda_r <= db_r[7]; 4'd1: sda_r <= db_r[6]; 4'd2: sda_r <= db_r[5]; 4'd3: sda_r <= db_r[4]; 4'd4: sda_r <= db_r[3]; 4'd5: sda_r <= db_r[2]; 4'd6: sda_r <= db_r[1]; 4'd7: sda_r <= db_r[0]; default: ; endcase // sda_r <= db_r[4'd7-num]; //送EEPROM地址(高bit开始) cstate <= ADD2; end end // else if(`SCL_POS) db_r <= {db_r[6:0],1'b0}; //器件地址左移1bit else cstate <= ADD2; end ACK2: begin if(/*!sda*/`SCL_NEG) begin //从机响应信号 if(!sw1_r) begin cstate <= DATA; //写操作 db_r <= `WRITE_DATA; //写入的数据 end else if(!sw2_r) begin db_r <= `DEVICE_READ; //送器件地址(读操作),特定地址读需要执行该步骤以下操作 cstate <= START2; //读操作 end end else cstate <= ACK2; //等待从机响应 end START2: begin //读操作起始位 if(`SCL_LOW) begin sda_link <= 1'b1; //sda作为output sda_r <= 1'b1; //拉高数据线sda cstate <= START2; end else if(`SCL_HIG) begin //scl为高电平中间 sda_r <= 1'b0; //拉低数据线sda,产生起始位信号 cstate <= ADD3; end else cstate <= START2; end ADD3: begin //送读操作地址 if(`SCL_LOW) begin if(num==4'd8) begin num <= 4'd0; //num计数清零 sda_r <= 1'b1; sda_link <= 1'b0; //sda置为高阻态(input) cstate <= ACK3; end else begin num <= num+1'b1; case (num) 4'd0: sda_r <= db_r[7]; 4'd1: sda_r <= db_r[6]; 4'd2: sda_r <= db_r[5]; 4'd3: sda_r <= db_r[4]; 4'd4: sda_r <= db_r[3]; 4'd5: sda_r <= db_r[2]; 4'd6: sda_r <= db_r[1]; 4'd7: sda_r <= db_r[0]; default: ; endcase // sda_r <= db_r[4'd7-num]; //送EEPROM地址(高bit开始) cstate <= ADD3; end end // else if(`SCL_POS) db_r <= {db_r[6:0],1'b0}; //器件地址左移1bit else cstate <= ADD3; end ACK3: begin if(/*!sda*/`SCL_NEG) begin cstate <= DATA; //从机响应信号 sda_link <= 1'b0; end else cstate <= ACK3; //等待从机响应 end DATA: begin if(!sw2_r) begin //读操作 if(num<=4'd7) begin cstate <= DATA; if(`SCL_HIG) begin num <= num+1'b1; case (num) 4'd0: read_data[7] <= sda; 4'd1: read_data[6] <= sda; 4'd2: read_data[5] <= sda; 4'd3: read_data[4] <= sda; 4'd4: read_data[3] <= sda; 4'd5: read_data[2] <= sda; 4'd6: read_data[1] <= sda; 4'd7: read_data[0] <= sda; default: ; endcase // read_data[4'd7-num] <= sda; //读数据(高bit开始) end // else if(`SCL_NEG) read_data <= {read_data[6:0],read_data[7]}; //数据循环右移 end else if((`SCL_LOW) && (num==4'd8)) begin num <= 4'd0; //num计数清零 cstate <= ACK4; end else cstate <= DATA; end else if(!sw1_r) begin //写操作 sda_link <= 1'b1; if(num<=4'd7) begin cstate <= DATA; if(`SCL_LOW) begin sda_link <= 1'b1; //数据线sda作为output num <= num+1'b1; case (num) 4'd0: sda_r <= db_r[7]; 4'd1: sda_r <= db_r[6]; 4'd2: sda_r <= db_r[5]; 4'd3: sda_r <= db_r[4]; 4'd4: sda_r <= db_r[3]; 4'd5: sda_r <= db_r[2]; 4'd6: sda_r <= db_r[1]; 4'd7: sda_r <= db_r[0]; default: ; endcase // sda_r <= db_r[4'd7-num]; //写入数据(高bit开始) end // else if(`SCL_POS) db_r <= {db_r[6:0],1'b0}; //写入数据左移1bit end else if((`SCL_LOW) && (num==4'd8)) begin num <= 4'd0; sda_r <= 1'b1; sda_link <= 1'b0; //sda置为高阻态 cstate <= ACK4; end else cstate <= DATA; end end ACK4: begin if(/*!sda*/`SCL_NEG) begin // sda_r <= 1'b1; cstate <= STOP1; end else cstate <= ACK4; end STOP1: begin if(`SCL_LOW) begin sda_link <= 1'b1; sda_r <= 1'b0; cstate <= STOP1; end else if(`SCL_HIG) begin sda_r <= 1'b1; //scl为高时,sda产生上升沿(结束信号) cstate <= STOP2; end else cstate <= STOP1; end STOP2: begin if(`SCL_LOW) sda_r <= 1'b1; else if(cnt_20ms==20'hffff0) cstate <= IDLE; else cstate <= STOP2; end default: cstate <= IDLE; endcase end assign sda = sda_link ? sda_r:1'bz; assign dis_data = read_data; endmodule |
串口通信的发送机
/********************************版权声明************************************** ** 大西瓜团队 ** **----------------------------文件信息-------------------------- ** 文件名称: uart_txd.v ** 创建日期: ** 功能描述:串口通信的发送机 ** 硬件平台:大西瓜第三代开发板,http://daxiguafpga.taobao.com ** 版权声明:本代码属个人知识产权,本代码仅供交流学习. **---------------------------修改文件的相关信息---------------- ** 修改人: ** 修改日期: ** 修改内容: *******************************************************************************/ module uart_txd(clk,rst,data_bus,host_ready,load_tram_datareg,serial_out); input clk; //时钟信号 input rst; //复位 input[7:0] data_bus; //数据信号,输入数据总线数据,8bit input host_ready; //控制信号,为高电平表示主机数据准备完毕 input load_tram_datareg; //控制信号,为高电平表示输入数据寄存器从数据数据总线取数据 output serial_out; //数据信号,UART发送的数据信号
reg[7:0] tram_datareg; //UART发送数据寄存器 reg[8:0] tram_shiftreg; //UART发送数据移位寄存器 reg load_tram_shiftreg; //装载移位寄存器标志位 reg[1:0] state; reg[1:0] next_state; reg[3:0] count; reg clear; //对count计数器清零 reg shift; //寄存器移位信号 reg start; //开始发送数据信号
//三种状态:空闲、等待、发送状态 parameter[1:0] idle=2'b00; parameter[1:0] waiting=2'b01; parameter[1:0] sending=2'b10;
assign serial_out=tram_shiftreg[0]; //移位寄存器最低位输出
always@(posedge clk) begin if(!rst) next_state<=idle; state=next_state; case(state) idle: begin //空闲 clear=0; shift=0; start=0; if(host_ready) begin load_tram_shiftreg<=1; next_state<=waiting; end end waiting: begin //等待 start<=1; next_state<=sending; end sending: begin //发送 if(count!=9) shift<=1; else begin clear<=1; next_state<=idle; end end default:next_state<=idle; endcase end always@(posedge clk) begin if(!rst) begin tram_shiftreg<=9'b1_1111_1111; //移位寄存器内容复位 count<=0; end else begin if(load_tram_datareg) tram_datareg<=data_bus; //取数据总线 if(load_tram_shiftreg) tram_shiftreg<={tram_datareg,1'b1}; if(start) tram_shiftreg[0]<=0; //开始传输信号,起始位为0 if(clear) count<=0; //计数器清空 else if(shift) count<=count+1; if(shift) tram_shiftreg<={1'b1,tram_shiftreg[8:1]}; end end endmodule |
串口通讯的波特率选择
/********************************版权声明************************************** ** 大西瓜团队 ** **----------------------------文件信息-------------------------- ** 文件名称: speed_select.v ** 创建日期: ** 功能描述:串口通讯的波特率选择,该波特率为9600bit/s ** 操作过程:可以根据通讯所需的波特率修改程序 ** 硬件平台:大西瓜第三代开发板,http://daxiguafpga.taobao.com ** 版权声明:本代码属个人知识产权,本代码仅供交流学习. **---------------------------修改文件的相关信息---------------- ** 修改人: ** 修改日期: ** 修改内容: *******************************************************************************/ module speed_select(clk,sclk,rst); input clk; input rst; output sclk; reg sclk; reg [12:0] count;
always@(posedge clk or negedge rst) begin if(!rst) begin count<=13'b0_0000_0000_0000; sclk<=1'b0; end else begin if(count<=5208) begin count<=count+13'b0_0000_0000_0001; if(count<=2604) sclk<=1'b1; else sclk<=1'b0; end else count<=13'b0_0000_0000_0000; end end endmodule |
实验操作
本实验中,主要学习使用基于AT24C04的IIC通信协议。实验中,我们使用到了两个按键sw1和sw2,当sw1按下时进行的是写入操作,当sw2按下时进行的是读出操作。在实验中我们添加串口通讯的模块,利用串口调试助手,把我们IIC写入的数据读出后,显示于PC机上。
步骤1:烧入程序,打开串口调试助手。
步骤2:按下复位键,再下显示键(实质是让串口发送数据),观察PC机显示结果。
结果显示的数据都为00,因为此时还没有进行写操作 。
步骤3:按下写操作键,再按下读操作键,观看PC显示结果。
结果显示的是0xD1 ,这与我们程序发送的数据一致。同学们可以在程序里修改发送数据再观察是否正确。
大西瓜FPGA-->https://daxiguafpga.taobao.com
配套开发板:https://item.taobao.com/item.htm?spm=a1z10.1-c.w4004-24211932856.3.489d7241aCjspB&id=633897209972
博客资料、代码、图片、文字等属大西瓜FPGA所有,切勿用于商业! 若引用资料、代码、图片、文字等等请注明出处,谢谢!
每日推送不同科技解读,原创深耕解读当下科技,敬请关注微信公众号"科乎"。