I2C(Inter-Integrated Circuit)协议是一种用于同步、半双工、串行总线(由SCL时钟线、SDA线组成)上的协议。规定了总线空闲状态、起始条件、停止条件、数据有效性、字节格式、响应Ack信号、从设备地址选择、数据方向。有主从机之分,主机master就是产生时钟的一方,并且起始信号和停止信号由主机发送。
协议说明(之前有博客介绍I2C协议)
总线空闲时钟:SCL、SDA均为高电平,此时主从设备都不控制总线(主从设备的SDA和SCL引脚为输入或者开漏输出),由外部上拉电阻将总线拉高。(I2C的开漏输出是总线的关键)
起始条件:SCL在高电平的时候,SDA出现下降沿,如下图:
停止信号:SCL在高电平的时候,SDA出现上升沿,如下图:
数据有效性:什么时候读取数据(SDA)有效,在SCL为高电平的时候,读数据有效,什么时候写数据(改变SDA),在SCL为低电平时,写数据有效。
字节格式:发送到SDA线上的每个字节必须为8位,首先传输的是字节的高八位(MSB) .
响应(ACK):为确保发送数据被对方可靠的接收,接收方必须发出ACK信号表示已成功接收数据。这个ACK信号就是接收方在接收完一个字节数据后,SCL的第一个下降沿处拉低SDA。发送方如何接受这个ACK信号?在发送完一个字节数据之后,SCL的第一个高电平处读取SDA线(SDA引脚处于输入状态不会使SDA拉低),如果读出为低,则表示对方成功接收到数据,在发送方读完ACK后,接收方需要释放SDA,即在SCL的第二个下降沿处将SDA设置为开漏输出,释放SDA总线。
从设备地址选择和数据方向:I2C是根据设备地址进行寻址的,在发送完一个起始信号之后,紧接着发送一个字节,有7位和10位地址之分,一般用7位地址,最后一位表示数据传输方向,组成一个字节,0表示写方向,1表示读方向。
模拟IO设置
#ifndef __BSP_I2C_GPIO_H
#define __BSP_I2C_GPIO_H
#include "stm32f10x.h"
#define EEPROM_I2C_SDA_PORT GPIOB
#define EEPROM_I2C_SDA_PIN GPIO_Pin_7
#define EEPROM_I2C_SDA_CLK RCC_APB2Periph_GPIOB
#define EEPROM_I2C_SCL_PORT GPIOB
#define EEPROM_I2C_SCL_PIN GPIO_Pin_6
#define EEPROM_I2C_SCL_CLK RCC_APB2Periph_GPIOB
#define EEPROM_I2Cx I2C1
#define EEPROM_I2Cx_CLK RCC_APB1Periph_I2C1
#define EEPROM_I2C_WR 0xFE
#define EEPROM_I2C_RD 0x01
#define STM32_OWN_ADDRESS 0x0A
#define EEPROM_ADDRESS 0xA0
#define I2C_SDA_HIGH() GPIO_SetBits(EEPROM_I2C_SDA_PORT,EEPROM_I2C_SDA_PIN);
#define I2C_SDA_LOW() GPIO_ResetBits(EEPROM_I2C_SDA_PORT,EEPROM_I2C_SDA_PIN);
#define I2C_SCL_HIGH() GPIO_SetBits(EEPROM_I2C_SCL_PORT,EEPROM_I2C_SCL_PIN);
#define I2C_SCL_LOW() GPIO_ResetBits(EEPROM_I2C_SCL_PORT,EEPROM_I2C_SCL_PIN);
#define I2C_SDA_READ() GPIO_ReadInputDataBit(EEPROM_I2C_SDA_PORT,EEPROM_I2C_SDA_PIN)
void I2C_Ack(void);
uint8_t I2C_CheckDevice(void);
void I2C_GPIO_Config(void);
void I2C_NAck(void);
void I2C_NAck(void);
void I2C_SendByte(uint8_t data);
uint8_t I2C_ReadByte(void);
void I2C_Start(void);
void I2C_Stop(void);
uint8_t I2C_WaitAck(void);
#endif
GPIO配置开漏输出
void I2C_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
RCC_APB2PeriphClockCmd(EEPROM_I2C_SDA_CLK|EEPROM_I2C_SCL_CLK,ENABLE);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStruct.GPIO_Pin = EEPROM_I2C_SDA_PIN;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(EEPROM_I2C_SDA_PORT,&GPIO_InitStruct);
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStruct.GPIO_Pin = EEPROM_I2C_SCL_PIN;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(EEPROM_I2C_SCL_PORT,&GPIO_InitStruct);
I2C_SCL_HIGH();
I2C_SDA_HIGH(); //总线空闲
}
延时函数(随便,我这是软件延时,不准确,只要不超出400kbit/s即可,I2C最大速率3.4Mbit/s,一般都只支持快速模式)
static void I2C_Delay(void)
{
uint16_t i;
for(i=0;i<10;i++);
}
起始信号
void I2C_Start(void)
{
I2C_SDA_HIGH();
I2C_SCL_HIGH();
I2C_Delay();
I2C_SDA_LOW();
I2C_Delay();
I2C_SCL_LOW(); //一般低电平结束表示一个时钟周期
I2C_Delay();
}
停止信号
void I2C_Stop(void)
{
I2C_SDA_LOW();
I2C_SCL_HIGH();
I2C_Delay();
I2C_SDA_HIGH();
}
应答信号
void I2C_Ack(void)
{
I2C_SDA_LOW();
I2C_Delay();
I2C_SCL_HIGH();
I2C_Delay();
I2C_SCL_LOW();
I2C_Delay();
I2C_SDA_HIGH(); //前面提到的,应答之后需要释放SDA总线
}
非应答信号(这就不需要释放总线)
void I2C_NAck(void)
{
I2C_SDA_HIGH();
I2C_Delay();
I2C_SCL_HIGH();
I2C_Delay();
I2C_SCL_LOW();
I2C_Delay();
}
等待应答信号
uint8_t I2C_WaitAck(void)
{
uint8_t r;
I2C_SDA_HIGH(); //等待应答,主机需要释放SDA总线,由从机产生应答
I2C_Delay();
I2C_SCL_HIGH();
I2C_Delay();
if(I2C_SDA_READ())
{
r=1;
}
else
{
r=0;
}
I2C_SCL_LOW();
I2C_Delay();
return r;
}
读取字节函数
uint8_t I2C_ReadByte(void)
{
uint8_t i=0;
uint8_t value=0;
for(i=0;i<8;i++)
{
value<<=1;
I2C_SCL_HIGH();
I2C_Delay();
if(I2C_SDA_READ())
{
value++;
}
I2C_SCL_LOW();
I2C_Delay();
}
return value;
}
写入字节函数
void I2C_SendByte(uint8_t data)
{
uint8_t i = 0;
for(i=0;i<8;i++)
{
if(data&0x80)
{
I2C_SDA_HIGH();
}
else
{
I2C_SDA_LOW();
}
I2C_Delay();
I2C_SCL_HIGH();
I2C_Delay();
I2C_SCL_LOW();
if(i==7)
{
I2C_SDA_HIGH(); //数据发送完,释放SDA总线
}
data<<=1;
I2C_Delay();
}
}
写到这里,可以先进行测试,直接发送设备地址,看EEPROM是否有应答产生,能否进行后续更为复杂的读写测试
uint8_t I2C_CheckDevice(void)
{
uint8_t Ack ;
I2C_GPIO_Config();
I2C_Start();
I2C_SendByte(EEPROM_ADDRESS&EEPROM_I2C_WR);
Ack = I2C_WaitAck();
I2C_Stop();
return Ack;
}
读取任意字节函数
uint8_t I2C_BufferRead(uint8_t *pBuffer,uint8_t addr,uint16_t numToRead) { uint16_t i; I2C_Start(); //起始信号 I2C_SendByte(EEPROM_ADDRESS&EEPROM_I2C_WR); //发送设备地址 if(I2C_WaitAck()) //等待应答 { I2C_Stop(); return 0; } I2C_SendByte(addr); //发送要读取的内存地址 if(I2C_WaitAck()) //等待应答 { I2C_Stop(); return 0; } I2C_Start(); //起始信号 I2C_SendByte(EEPROM_ADDRESS|EEPROM_I2C_RD); //设备地址 if(I2C_WaitAck()) { I2C_Stop(); return 0; } for(i=0;i<numToRead;i++) //读取数据 { pBuffer[i] = I2C_ReadByte(); if(i!=(numToRead-1)) //最后一个字节要发送非应答信号 { I2C_Ack(); } else { I2C_NAck(); } } I2C_Stop(); return 1; }
页写入函数
uint8_t I2C_PageWrite(uint8_t *pBuffer,uint8_t addr,uint16_t numToWrite)
{
uint16_t i;
//I2C_Stop();
I2C_Start();
I2C_SendByte(EEPROM_ADDRESS&EEPROM_I2C_WR);
if(I2C_WaitAck())
{
I2C_Stop();
return 0;
}
I2C_SendByte(addr);
if(I2C_WaitAck())
{
I2C_Stop();
return 0;
}
while(numToWrite--)
{
I2C_SendByte(*pBuffer);
if(I2C_WaitAck())
{
I2C_Stop();
return 0;
}
pBuffer++;
}
I2C_Stop();
return 1;
}
任意字节读取函数
uint8_t I2C_BufferWrite(uint8_t *pBuffer,uint8_t addr,uint16_t numToWrite)
{
uint16_t count,numPage,numSingle,usAddr;
usAddr = addr%8;
count = 8-usAddr;
numPage = numToWrite/8;
numSingle=numToWrite%8;
if(usAddr==0)
{
if(numPage==0)
{
I2C_PageWrite(pBuffer,addr,numToWrite);
Delay(0xFFFF);
}
else
{
while(numPage--)
{
I2C_PageWrite(pBuffer,addr,8);
Delay(0xFFFF);
pBuffer+=8;
addr+=8;
}
if(numSingle!=0)
{
I2C_PageWrite(pBuffer,addr,numSingle);
Delay(0xFFFF);
}
}
}
else
{
if(numPage==0)
{
if(numSingle>count)
{
I2C_PageWrite(pBuffer,addr,count);
Delay(0xFFFF);
pBuffer+=count;
addr+=count;
I2C_PageWrite(pBuffer,addr,(numSingle-count));
Delay(0xFFFF);
}
else
{
I2C_PageWrite(pBuffer,addr,numSingle);
Delay(0xFFFF);
}
}
else
{
numToWrite -= count;
numPage=numToWrite/8;
numSingle=numToWrite%8;
I2C_PageWrite(pBuffer,addr,count);
Delay(0xFFFF);
pBuffer+=count;
addr+=count;
while(numPage--)
{
I2C_PageWrite(pBuffer,addr,8);
Delay(0xFFFF);
pBuffer+=8;
addr+=8;
}
if(numSingle!=0)
{
I2C_PageWrite(pBuffer,addr,numSingle);
Delay(0xFFFF);
}
}
}
return 1;
}
参考野火的STM32例程,这里把页写入和任意字节写入归为一个函数
uint8_t I2C_Write(uint8_t *pBuffer,uint8_t addr,uint16_t numToWrite)
{
uint16_t i,m;
uint16_t usAddr;
usAddr = addr;
for(i=0;i<numToWrite;i++)
{
if((i==0)||((usAddr%8)==0)) //每当是第一个字节或者新的一页,要重新发起始信号
{
I2C_Stop(); //停止信号,内部时序
for(m=0;m<1000;m++) //用于超时处理
{
I2C_Start(); //起始信号
I2C_SendByte(EEPROM_ADDRESS&EEPROM_I2C_WR); //设备地址
if(I2C_WaitAck() == 0) //应答则跳出循环
{
break;
}
}
if(m==1000) //无应答,跳出函数
{
I2C_Stop();
return 0;
}
I2C_SendByte(usAddr); //要写入的内存地址
if(I2C_WaitAck()!=0)
{
I2C_Stop();
return 0;
}
}
I2C_SendByte(pBuffer[i]); //写入数据
if(I2C_WaitAck()!=0)
{
I2C_Stop();
return 0;
}
usAddr++; //地址递增
}
I2C_Stop();
return 1;
}
效果如下: