一、I2C协议简介
I2C(IIC,Inter-Integrated Circuit),两线式串行总线,由飞利浦(PHILIPS)公司开发用于连接微控制器及其外围设备。它是由数据线SDA和时钟SCL构成的半双工串行总线,可发送和接收数据。在CPU与被控IC之间、IC与IC之间进行双向传送,高速IIC总线一般可达400kbps以上。
1.功能特点
1)它只使用两条总线线路:一条双向串行数据线(SDA),一条串行时钟线(SCL)。
2)每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问。
3)多主机同时使用总线时,为了防止数据冲突,会利用仲裁方式决定由哪个设备占用总线。
4)具有三种传输模式:标准模式的传输速率为100 Kbit/s", 快速模式为400 Kbit’s ,高速模式下可达3.4 Mbit/s,但目前大多I2C设备尚不支持高速模式。
5)片上的滤波器可以滤去总线数据线上的毛刺波以保证数据完整。
6)连接到相同总线的IC数量受到总线的最大电容400 pF限制。
2.信号时序
I2C 总线在传送数据中有空闲状态、开始信号、停止信号、应答信号、数据的有效性判断、数据传输等信号机过程。
1)空闲状态
I2C总线总线的SDA和SCL两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。
2)起始信号与停止信号
⚫ 起始信号:当SCL为高期间,SDA由高到低的跳变;启动信号是一种电平跳变时序信号,而不是一个电平信号。
⚫ 停止信号:当SCL为高期间,SDA由低到高的跳变;停止信号也是一种电平跳变时序信号,而不是一个电平信号。
3)应答信号ACK
发送器每发送一个字节,就在时钟脉冲9期间释放数据线,由接收器反馈一个应答信号。 应答信号为低电平时,规定为有效应答位(ACK简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。对于反馈有效应答位ACK的要求是,接收器在第9个时钟脉冲之前的低电平期间将SDA线拉低,并且确保在该时钟的高电平期间为稳定的低电平。 如果接收器是主控器,则在它收到最后一个字节后,发送一个NACK信号,以通知被控发送器结束数据发送,并释放SDA线,以便主控接收器发送一个停止信号P。
4)数据有效性
I2C总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。即:数据在SCL的上升沿到来之前就需准备好。并在在下降沿到来之前必须稳定。
5)数据的传送
在I2C总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在SCL串行时钟的配合下,在SDA上逐位地串行传送每一位数据。数据位的传输是边沿触发。
4)地址及数据方向
I2C 总线上的每个设备都有自己的独立地址,主机发起通讯时,通过 SDA 信号线发送设备地址(SLAVE_ADDRESS)来查找从机。I2C 协议规定设备地址可以是 7 或 10 位,实际中 7 位的地址应用比较广泛。紧跟设备地址的一个数据位用来表示数据传输方向,它是数据方向位(R/W),第 8位或第 11位。数据方向位为“1”时表示主机由从机读数据,该位为“0”时表示主机向从机写数据。
3.工作流程
I2C的协议包括起始和停止条件、数据有效性、响应、仲裁、时钟同步和地址广播等环节。
上图表示的是主机和从机通信时SDA线的数据包序列。
其中S表示由主机的IC接口产生的传输起始信号(S), 这时连接到I2C总线上的所有从机都会接收到这个信号。
起始信号产生后,所有从机就开始等待主机紧接下来广播的从机地址信号(SLAVE_ADDRESS),在I2C总线上,每个设备的地址都是唯一的。当主机广播的地址与某个设备地址相同时,这个设备就被选中了,没被选中的设备将会忽略之后的数据信号。根据I2C协议,这个从机地址可以是7位或10位。
在地址位之后,是传输方向的选择位,该位为0时,表示后面的数据传输方向是由主机传输至从机。该位为1时,则相反。
从机接收到匹配的地址后,主机或从机会返回一一个应答(A)或非应答(A)信号,只有接收到应答信号后,主机才能继续发送或接收数据。
若配置的方向传输位为写数据,广播完地址,接收到应答信号后,主机开始正式向从机传输数据(DATA),数据包的大小为8位。主机每发送完-一个数据,都要等待从机的应答信号(A,重复这个过程,可以向从机传输N个数据,这个N没有大小限制。当数据传输结束时,主机向从机发送一个停止传输信号(P),表示不再传输数据。
若配置的方向传输位为读数据,广播完地址,接收到应答信号后,从机开始向主机返回数据(DATA),数据包大小也为8位。从机每发送完一个数据,都会等待主机的应答信号(A),重复这个过程,可以返回N个数据,这个N也没有大小限制。当主机希望停止接收数据时,就向从机返回一个非应答信号(A),则从机自动停止数据传输。
二、STM32的I2C
1.I2C接口特性
1) STM32的中等容量和大容量型号的芯片均有多达两个的rc总线接口。
2)能够工作于多主模式或从模式,分别为主接收器、主发送器、从接收器及从发送器。
3)支持标准模式100 Kbit/s和快速模式400 Kbit/s,不支持高速模式。
4)支持7位或10位寻址。
5)内置了硬件CRC发生器/校验器。
6) I2C 的接收和发送都可以使用DMA操作。
7)支持系统管理总线(SMBus) 2.0 版。
2.I2C架构
上图中我们可以看到,I2C的所有硬件架构就是根据SCL线和SDA线展开的(其中SMBALERT线用于SMBus)。
SCL线的时序即为I2C协议中的时钟信号,它由IC接口根据时钟控制寄存器(CCR)控制,控制的参数主要为时钟频率。
而SDA线的信号则通过一系列数据控制架构,在将要发送的数据的基础上,根据协议添加各种起始信号、应答信号、地址信号,实现以I2C协议的方式发送出去。读取数据时则从SDA线上的信号中取出接收到的数据值。发送和接收的数据都被保存在数据寄存器(DR)上。
三、I2C读取EEPROM
1.发送读写流程
(1)主发送器发送流程:
⚫ 控制产生起始信号(S),当发生起始信号后,它产生事件“EV5”,并会对 SR1 寄存器的“SB”位置 1,表示起始信号已发送;
⚫ 接着发送设备地址并等待应答信号,若有从机应答,则产生事件“EV6”及“EV8”,这时 SR1 寄存器的“ADDR”位及“TXE”位被置 1,ADDR 为 1表示地址已经发送,TXE 为 1表示数据寄存器为空;
⚫ 以上步骤正常执行并对 ADDR 位清零后,我们往 I2C 的“数据寄存器 DR”写入要发送的数据,这时TXE位会被重置0,表示数据寄存器非空,I2C外设通过SDA信号线一位位把数据发送出去后,又会产生“EV8”事件,即 TXE 位被置 1,重复这个过程,就可以发送多个字节数据了;
⚫ 当我们发送数据完成后,控制 I2C 设备产生一个停止信号( P ),这个时候会产生EV8_2 事件,SR1 的 TXE位及 BTF位都被置 1,表示通讯结束。
⚫假如我们使能了 I2C 中断,以上所有事件产生时,都会产生 I2C 中断信号,进入同一个中断服务函数,到 I2C中断服务程序后,再通过检查寄存器位来判断是哪一个事件。
(2)主接收器接收流程:
⚫ 同主发送流程,起始信号(S)是由主机端产生的,控制发生起始信号后,它产生事件“EV5”,并会对 SR1寄存器的“SB”位置 1,表示起始信号已经发送;
⚫ 紧接着发送设备地址并等待应答信号,若有从机应答,则产生事件“EV6”这时SR1 寄存的“ADDR”位被置 1,表示地址已经发送。
⚫ 从机端接收到地址后,开始向主机端发送数据。当主机接收到这些数据后,会产生“EV7”事件,SR1寄存器的 RXNE被置 1,表示接收数据寄存器非空,我们读取该寄存器后,可对数据寄存器清空,以便接收下一次数据。此时我们可以控制I2C 发送应答信号(ACK)或非应答信号(NACK),若应答,则重复以上步骤接收数据,若非应答,则停止传输;
⚫ 发送非应答信号后,产生停止信号( P ),结束传输。
在发送和接收过程中,有的事件不只是标志了我们上面提到的状态位,还可能同时标志主机状态之类的状态位,而且读了之后还需要清除标志位,比较复杂。我们可使用STM32标准库函数来直接检测这些事件的复合标志,降低编程难度。
2.I2C初始化函数
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_OwnAddress1 = I2C1_OWN_ADDRESS7;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable;
I2C_InitStructure.I2C_AcknowledgeAddress = I2C_AcknowledgeAddress_7bit;
I2C_InitStructure.I2C_ClockSpeed = I2C_Speed;
I2C_Init(I2C1,ENABLE);
(1)I2C_ Mode :
本成员是选择I2C的使用方式,有I2C模式(I2C_ Mode_I2C)和SMBus模式(I2C_ Mode_SMBusDevice、I2C Mode_ SMBus_Host )。
(2)I2C_DutyCycle:
本成员设置的是I2C的SCL线时钟的占空比。SCL线的时钟信号的高电平时间与低电平时间不必相同,由于SDA线在SCL线维持在高电平时读取或写入数据,而在SCL的低电平期间SDA的数据发生变化,所以高电平时间较长就不容易出现数据错误。根据I2C协议,在快速模式和高速模式下SCL的高低电平时间可以不同。在STM32的rC占空比配置中有两个选择,分别为高电平时间和低电平时间之比为16:9 (I2C_DutyCycle_16_9)和2: 1 ( I2C_DutyCycle_2)。
(3) I2C_OwnAddress1 :
本成员配置的是STM32的IC设备自己的地址,每个连接到I2C总线上的设备都要有一个自己的地址,作为主机也不例外。就如同前面介绍的连接到IFC的EEPROM的设备地址一样,只不过STM32的I2C地址可软件编程。这个地址可以被配置为7位和10位地址。
(4)I2C_AckEnable:
本成员关于FC应答设置,设置为使能则每接收到一个字节就返回一个应答信号。本实验配置为允许应答(I2C_Ack_ Enable), 这是绝大多数遵循I2C标准的设备通信的要求,改为禁止应答(I2C_Ack_Disable) 往往会导致通信错误。
(5)I2C_AcknowledgeAddress :
本成员选择I2C的寻址模式是7位还是10位地址。这需要根据实际连接到IC总线上设备的地址进行选择。本实验是与EEPROM进行通信,使用的为7位寻址模式(I2C_AcknowledgedAddress_7bit)。
(6)I2C_ClockSpeed :
本成员设置的是FC的传输速率,在调用初始化函数时,函数会根据我们输入的数值经过运算后把分频值写人到FC的时钟控制寄存器。而我们写人的这个参数值不得高于400KHZ。本实验向这个成员写人参数为400000 (自定义密I2C Speed)。实际上由于IC使用的APB1时钟为36 MHz,不是10 MHz的整数倍,因此最终分频后输出的SCL线时钟并不是精确的400KHz。
对结构体成员赋值完成后,我们调用库函数I2C_ Init()根据我们的配置对IFC进行初始化,并调用库函数I2C_Cmd()使能I2C外设。
3.实验程序
这里我们不使用STM32的硬件IIC来读写24C02,而是通过软件模拟IIC来读写24C02。STM32的硬件IIC非常复杂,更重要的是不稳定,故不推荐使用。
(1)IIC初始化函数
//初始化 IIC
void IIC_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
//RCC->APB2ENR|=1<<4;//先使能外设 IO PORTC 时钟
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOC, ENABLE );
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12|GPIO_Pin_11;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP ; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
IIC_SCL=1;
IIC_SDA=1;
}
//产生 IIC 起始信号
void IIC_Start(void)
{
SDA_OUT(); //sda 线输出
IIC_SDA=1;
IIC_SCL=1;
delay_us(4);
IIC_SDA=0;//START:when CLK is high,DATA change form high to low
delay_us(4);
IIC_SCL=0;//钳住 I2C 总线,准备发送或接收数据
}
//产生 IIC 停止信号
void IIC_Stop(void)
{
SDA_OUT();//sda 线输出
IIC_SCL=0;
IIC_SDA=0;//STOP:when CLK is high DATA change form low to high
delay_us(4);
IIC_SCL=1;
IIC_SDA=1;//发送 I2C 总线结束信号
delay_us(4);
}
//等待应答信号到来
//返回值:1,接收应答失败
// 0,接收应答成功
u8 IIC_Wait_Ack(void)
{
u8 ucErrTime=0;
SDA_IN(); //SDA 设置为输入
IIC_SDA=1;delay_us(1);
IIC_SCL=1;delay_us(1);
while(READ_SDA)
{
ucErrTime++;
if(ucErrTime>250)
{
IIC_Stop();
return 1;
}
}
IIC_SCL=0;//时钟输出 0
return 0;
}
//产生 ACK 应答
void IIC_Ack(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=0;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
}
//不产生 ACK 应答
void IIC_NAck(void)
{
IIC_SCL=0;
SDA_OUT();
IIC_SDA=1;
delay_us(2);
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
}
//IIC 发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答
void IIC_Send_Byte(u8 txd)
{
u8 t;
SDA_OUT();
IIC_SCL=0;//拉低时钟开始数据传输
for(t=0;t<8;t++)
{
IIC_SDA=(txd&0x80)>>7;
txd<<=1;
delay_us(2); //对 TEA5767 这三个延时都是必须的
IIC_SCL=1;
delay_us(2);
IIC_SCL=0;
delay_us(2);
}
}
//读 1 个字节,ack=1 时,发送 ACK,ack=0,发送 nACK
u8 IIC_Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;
SDA_IN();//SDA 设置为输入
for(i=0;i<8;i++ )
{
IIC_SCL=0;
delay_us(2);
IIC_SCL=1;
receive<<=1;
if(READ_SDA)receive++;
delay_us(1);
}
if (!ack)
IIC_NAck();//发送 nACK
else
IIC_Ack(); //发送 ACK
return receive;
}
该部分为IIC初始化代码,实现包括IIC的初始化(IO口)、IIC开始、IIC结束、ACK、IIC读写等功能,在其他函数里面,只需要调用相关的IIC函数就可以和外部IIC器件通信了,这里并不局限于24C02,该段代码可以用在任何IIC设备上。
(2)IIC.h
#define SDA_IN() {GPIOC->CRH&=0XFFFF0FFF;GPIOC->CRH|=8<<12;}
#define SDA_OUT() {GPIOC->CRH&=0XFFFF0FFF;GPIOC->CRH|=3<<12;}
(3)24C02代码
#include "24cxx.h"
#include "delay.h"
//初始化 IIC 接口
void AT24CXX_Init(void)
{
IIC_Init();
}
//在 AT24CXX 指定地址读出一个数据
//ReadAddr:开始读数的地址
//返回值 :读到的数据
u8 AT24CXX_ReadOneByte(u16 ReadAddr)
{
u8 temp=0;
IIC_Start();
if(EE_TYPE>AT24C16)
{
IIC_Send_Byte(0XA0); //发送写命令
IIC_Wait_Ack();
IIC_Send_Byte(ReadAddr>>8);//发送高地址
}else IIC_Send_Byte(0XA0+((ReadAddr/256)<<1)); //发送器件地址 0XA0,写数据
IIC_Wait_Ack();
IIC_Send_Byte(ReadAddr%256); //发送低地址
IIC_Wait_Ack();
IIC_Start();
IIC_Send_Byte(0XA1); //进入接收模式
IIC_Wait_Ack();
temp=IIC_Read_Byte(0);
IIC_Stop();//产生一个停止条件
return temp;
}
//在 AT24CXX 指定地址写入一个数据
//WriteAddr :写入数据的目的地址
//DataToWrite:要写入的数据
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite)
{
IIC_Start();
if(EE_TYPE>AT24C16)
{
IIC_Send_Byte(0XA0); //发送写命令
IIC_Wait_Ack();
IIC_Send_Byte(WriteAddr>>8);//发送高地址
}else IIC_Send_Byte(0XA0+((WriteAddr/256)<<1)); //发送器件地址 0XA0,写数据
IIC_Wait_Ack();
IIC_Send_Byte(WriteAddr%256); //发送低地址
IIC_Wait_Ack();
IIC_Send_Byte(DataToWrite); //发送字节
IIC_Wait_Ack();
IIC_Stop(); //产生一个停止条件
delay_ms(10); //对于 EEPROM 器件,每写一次要等待一段时间,否则写失败!
}
//在 AT24CXX 里面的指定地址开始写入长度为 Len 的数据
//该函数用于写入 16bit 或者 32bit 的数据.
//WriteAddr :开始写入的地址
//DataToWrite:数据数组首地址
//Len :要写入数据的长度 2,4
void AT24CXX_WriteLenByte(u16 WriteAddr,u32 DataToWrite,u8 Len)
{
u8 t;
for(t=0;t<Len;t++) AT24CXX_WriteOneByte(WriteAddr+t,(DataToWrite>>(8*t))&0xff);
}
//在 AT24CXX 里面的指定地址开始读出长度为 Len 的数据
//该函数用于读出 16bit 或者 32bit 的数据.
//ReadAddr :开始读出的地址
//返回值 :数据
//Len :要读出数据的长度 2,4
u32 AT24CXX_ReadLenByte(u16 ReadAddr,u8 Len)
{
u8 t; u32 temp=0;
for(t=0;t<Len;t++)
{
temp<<=8;
temp+=AT24CXX_ReadOneByte(ReadAddr+Len-t-1);
}
return temp;
}
//检查 AT24CXX 是否正常
//这里用了 24XX 的最后一个地址(255)来存储标志字.
//如果用其他 24C 系列,这个地址要修改
//返回 1:检测失败
//返回 0:检测成功
u8 AT24CXX_Check(void)
{
u8 temp;
temp=AT24CXX_ReadOneByte(255);//避免每次开机都写 AT24CXX
if(temp==0X55)return 0;
else//排除第一次初始化的情况
{
AT24CXX_WriteOneByte(255,0X55);
temp=AT24CXX_ReadOneByte(255);
if(temp==0X55)return 0;
}
return 1;
}
//在 AT24CXX 里面的指定地址开始读出指定个数的数据
//ReadAddr :开始读出的地址 对 24c02 为 0~255
//pBuffer :数据数组首地址
//NumToRead:要读出数据的个数
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead)
{
while(NumToRead)
{
*pBuffer++=AT24CXX_ReadOneByte(ReadAddr++);
NumToRead--;
}
}
//在 AT24CXX 里面的指定地址开始写入指定个数的数据
//WriteAddr :开始写入的地址 对 24c02 为 0~255
//pBuffer :数据数组首地址
//NumToWrite:要写入数据的个数
void AT24CXX_Write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite)
{
while(NumToWrite--)
{
AT24CXX_WriteOneByte(WriteAddr,*pBuffer);
WriteAddr++;
pBuffer++;
}
}
(4)main.c
//要写入到 24c02 的字符串数组
const u8 TEXT_Buffer[]={"MiniSTM32 IIC TEST"};
#define SIZE sizeof(TEXT_Buffer)
int main(void)
{
u8 key;
u16 i=0;
u8 datatemp[SIZE];
delay_init(); //延时函数初始化
uart_init(9600); //串口初始化为 9600
LED_Init(); //初始化与 LED 连接的硬件接口
LCD_Init();
KEY_Init(); //按键初始化
AT24CXX_Init(); //IIC 初始化
POINT_COLOR=RED;//设置字体为红色
LCD_ShowString(60,50,200,16,16,"Mini STM32");
LCD_ShowString(60,70,200,16,16,"IIC TEST");
LCD_ShowString(60,90,200,16,16,"ATOM@ALIENTEK");
LCD_ShowString(60,110,200,16,16,"2014/3/9");
LCD_ShowString(60,130,200,16,16,"WK_UP:Write KEY0:Read"); //显示提示信息
while(AT24CXX_Check())//检测不到 24c02
{
LCD_ShowString(60,150,200,16,16,"24C02 Check Failed!");
delay_ms(500);
LCD_ShowString(60,150,200,16,16,"Please Check! ");
delay_ms(500);
LED0=!LED0;//DS0 闪烁
}
LCD_ShowString(60,150,200,16,16,"24C02 Ready!");
POINT_COLOR=BLUE;//设置字体为蓝色
while(1)
{
key=KEY_Scan(0);
if(key==WKUP_PRES)//WK_UP 按下,写入 24C02
{
LCD_Fill(0,170,239,319,WHITE);//清除半屏
LCD_ShowString(60,170,200,16,16,"Start Write 24C02....");
AT24CXX_Write(0,(u8*)TEXT_Buffer,SIZE);
LCD_ShowString(60,170,200,16,16,"24C02 Write Finished!");//提示传送完成
}
if(key==KEY0_PRES)//KEY0 按下,读取字符串并显示
{
LCD_ShowString(60,170,200,16,16,"Start Read 24C02.... ");
AT24CXX_Read(0,datatemp,SIZE);
LCD_ShowString(60,170,200,16,16,"The Data Readed Is: ");//提示传送完成
LCD_ShowString(60,190,200,16,16,datatemp);//显示读到的字符串
}
i++;
delay_ms(10);
if(i==20)
{
LED0=!LED0;//提示系统正在运行
i=0;
}
}
}
通过KEY_UP按键来控制24C02的写入,通过另外一个按键KEY0来控制24C02的读取。并在LCD模块上面显示相关信息。