I2C(Inter- Integrated Circuit)总线是一种是半双工通信方式,由PHILIPS公司开发的两线式串行总线,用于连接微控制器及其外围设备。很多外围器件如存储器、监控芯片等也提供I2C接口。
它是由数据线SDA和时钟SCL构成的串行总线,可发送和接收数据。在CPU与被控IC之间、IC与IC之间进行双向传送,高速IIC总线一般可达400kbps以上,支持多主控,各控制电路虽然挂在同一条总线上,却彼此独立,互不相关。
I2C协议
- 只有在总线空闲时才允许启动数据传送
- 在数据传送过程中,当时钟线为高电平时。数据必须处于稳定状态,不允许有跳变,因为在时钟线为高电平时数据线上的任何变化将被着作总线的起始或停止信号
1. 空闲状态
数据线SDA和时钟SCL都是出于高电平;所有未使用时要拉高可以硬件软件拉高方式。
2. 开始信号和停止信号
- 开始信号
当SCL保持为高电平的期间,SDA由高到低的跳变。(是一种时序) - 停止信号
当SCL保持为高电平的期间,SDA由低到高的跳变。(是一种时序)
3. 控制字节
在起始条件之后,必须是器件的控制字节,其中高四位为器件类型识别符(不同 的芯片类型有不同的定义,EEPROM - -般应为1010),,接着三位为片选,最后一位 为读写位,当为1时为读操作,为0时为写操作。
4. 应答信号
发送器没发送一个字节(8bit),就在时钟9期间释放数据线,由接收器反馈一个应答信号。其中应答信号为低电平表示有效应答,高电平就是无效的;也就是说对于反馈有效应答位ACK的要求是,接收器在第9个时钟脉冲之前的低电平期间将SDA线拉低,并且确保在该时钟的高电平期间为稳定的低电平(在SCL第9个脉冲到来之前,SDA就得拉低,SDA为低电平时间要稳定大于在SCL第9个脉冲为高电平的时间)。 - I2C总线器件作为从器件,接收完一个字节后响应一一个应答信号
- I2C总线器件作为主器件,发送完一一个字节后等待一个应答信号
5. 写数据操作
写操作分为字节写和页面写两种操作:
对于页面写根据芯片的一次装载的字节不同有所不同。对字节位小于2K位的EEPROM,页的大小为8个BYTE,其他为16个BYTE.(页写就只用指定首地址)
在字节写模式下,主器件发送起始命令和从器件类型地址信息(R/W置零)给从器件,在从器件产生应答信号后,主器件发送从器件的字节地址,主器件在收到从器件的另-一个应答信号后,再发送数据到被寻址的存贮单元(数据被存储位置),从器件再次应答,并在主器件产生停止信号后开始内部数据的擦写,在内部擦写过程中,从器件不再应答主器件的任何请求。(一般mcu为主机,其他设备为从机)
6. 读数据操作
立即读:从器件的地址计数器内容为最后操作字节的地址加1。亦就是说如果上次读/写的操作地址为N,则立即读的地址从N+1开始。如果N=E(E=从器件的最大地址)则计数器将翻转到0且继续输出数据。从器件接收到主器件的地址信号后,它首先发送- -个应答信号,然后发送一一个8位字节数据。主器件不需要发送一一个应答信号,但,要产生一个停止信号。
选择性读:操作允许主器件对寄存器的任意字节进行读操作,主器件首先通过发送起始信号,从器件类型地址及它想读取的字节数据的地址进行一个伪写操作。在从器件应答之后,主器件重新发送起始信号和从器件类型地址,此时R/W位置1,从器件响应并发送应答信号,然后输出一一个8位字节数据,主器件不发送应答信号但产生一个停止信号。
连续读:操作可通过立即读或选择性读操作启动。在从器件发送完一个8位字节数据后,主器件产生-一个应答信号来响应告之从器件主器件要求更多的数据,对应每个主机产生的应答信号从机将发送一个8位数据字节,主器件不发送应答信号而发送停止位时结束此操作。
从从器件输出的数据按顺序由N到N+1输出。读操作时地址计数器在整个地址内增加,这样整个寄存器区域可在一一个读操作内全部读出。当读取的字节超过E时计数器将翻转到零并继续输出数据。
7. 数据的有效性
数据0:在SCL为高电平的期间,SDA稳定为低,Tscl << Tsda;才表示有效数据0。
数据1: 在SCL为高电平的期间,SDA稳定为高,Tscl << Tsda;才表示有效数据1。 - 数据传送
在I2C总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在SCL串行时钟的配合下,在SDA上逐位地串行传送每一-位数据。数据位的传输是边沿触发。(一个bit有一个SCL时钟)
代码实现时序
1.使能GPIO时钟和配置GPIO
SCL和SDA推挽输出、输出为高电平
void PB67_Init(void)
{
//挂载时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
//PB6-SCL PB7-SDA
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_7|GPIO_Pin_6;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;//GPIO_Mode_Out_PP推挽输出 GPIO_Mode_IPD上拉输入
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStruct);
GPIO_SetBits(GPIOB,GPIO_Pin_6|GPIO_Pin_7);
}
注:一些宏定义
#define SDA_IN() {GPIOB->CRL&=0X0FFFFFFF;GPIOB->CRL|=(u32)8<<28;}//输入
#define SDA_OUT() {GPIOB->CRL&=0X0FFFFFFF;GPIOB->CRL|=(u32)3<<28;}//输出
#define SDA_1() {GPIO_SetBits(GPIOB,GPIO_Pin_7);}//SDA =1
#define SDA_0() {GPIO_ResetBits(GPIOB,GPIO_Pin_7);}//SAD = 0
#define SCL_1() {GPIO_SetBits(GPIOB,GPIO_Pin_6);} //SCL = 1
#define SCL_0() {GPIO_ResetBits(GPIOB,GPIO_Pin_6);}//SCL = 0
#define READ_SDA GPIO_ReadOutputDataBit(GPIOB,GPIO_Pin_7)//读取SDA的状态
2. 起始信号配置
SDA为输出,把SDA和SCL都设置输出为高,延时4us等待稳定,把SDA设置为低,延时4us等待稳定,把SCL设置为低。
void Start(void)
{
SDA_OUT();
SDA_1;
delay_us(1);
SCL_1;
delay_us(5);
SDA_0;
delay_us(5);
SCL_0;
}
3. 停止信号
SDA为输出,把SDA和SCL都设置输出为低,延时4us等待稳定,先把SCL设置输出为高,在把SDA为高,延时4us等待稳定。
void Stop(void)
{
SDA_OUT();
SCL_0;
SDA_0;
delay_us(5);
SCL_1;
delay_us(2);
SDA_1;
delay_us(5);
}
4. 应答信号
SDA为输入,把SDA和SCL设为高,等待设备应答(SDA为0),延时一段时间还未等到就视为失败,等到为低就成功。
int Wait_Ack(void)
{
u8 ucErrTime=0;
SDA_IN(); //SDA 设置为输入
SDA_1;
delay_us(2);
SCL_1;
delay_us(2);
while(READ_SDA)//读到低电平
{
ucErrTime++;
if(ucErrTime>250)
{
Stop();
return 1;
}
}
SCL_0; //时钟输出 0
return 0;//失败
}
5. 产生ACK应答和不产生应答
此时mcu为外设。
产生应答:SAD一直为低,SCL产生一个脉冲(低-高-低)。
不产生:SAD一直为高,SCL产生一个脉冲(低-高-低)。
//产生 ACK 应答
void IIC_Ack(void)
{
SCL_0;
SDA_OUT();
SDA_0;
delay_us(3);
SCL_1;
delay_us(3);
SCL_0;
}
//不产生
void IIC_NAck(void)
{
SCL_0;
SDA_OUT();
SDA_1;
delay_us(3);
SCL_1;
delay_us(3);
SCL_0;
}
6. 发送一个字节(8bit)
是1bit的发送,SDA输出模式,同时SCL拉低准备,SDA提前进入数据状态(在SCL低变高前就要为1或者0),所以是在SCL变高前处理数据,然后SCL为高,稳定3us,在为低。
void Send_Byte(u8 txd)
{
u8 t;
SDA_OUT();
SCL_0;//拉低时钟开始数据传输
for(t=0;t<8;t++)
{
if((txd&0x80)>>7)
SDA_1;
else
SDA_0;
txd<<=1;
delay_us(3);
SCL_1;
delay_us(3);
SCL_0;
delay_us(3);
}
}
7. 读取一个字节(8bit)
是每次读取一个bit,SDA要为输入;SCL先有一个低到高的跳变,如果SDA为高此时数据为1,作7次左移就把8个bit整合为一位了。单次数据读取就可以不必要发送应答信号ack给0;控制ack就可以读取连续多次,那么久最好用数组存放每次读取的数据。
int Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;
SDA_IN();//SDA设置为输入
for(i=0;i<8;i++)
{
SCL_0;
delay_us(3);
SCL_1;
receive<<=1;
if(READ_SDA)receive++;
delay_us(2);
}
if (!ack)
IIC_NAck(); //发送 nACK
else
IIC_Ack(); //发送 ACK
return receive;
}
8. 单次发送一次数据
主机开始信号;主机发送控制字;从机应答;主机发送地址;从机应答;主机写数据;停止信号。
void write_data(u8 add,u8 val)
{
Start();
Send_Byte(0XA0);//发送写命令
Wait_Ack();//等待应答 1 成功
Send_Byte(add);//发送地址00-ff
Wait_Ack();//等待应答 1 成功
Send_Byte(val);
Wait_Ack();//等待应答 1 成功
Stop();
delay_ms(20);
}
9. 选择性读数据
开始信号;主机发送控制字(写);从机应答;主机发送读地址;从机应答;开始信号;主机发送读控制字;从机应答;读数据;停止型号。
int Read_data(u8 ReadAddr)
{
unsigned char receive_data=0;
Start();
Send_Byte(0XA0);//发送写命令
Wait_Ack();//等待应答 1 成功
Send_Byte(ReadAddr);//发送取的 地址00-ff
Wait_Ack();//等待应答 1 成功
Start();
Send_Byte(0XA1);//发送读命令
Wait_Ack();//等待应答1成功
receive_data = Read_Byte(0);
Stop();
return receive_data;
}
10. 连续性读数据
完整代码
主要使用STM32F103ZE系列新品,PB6-SCL PB7-SDA,用GPIO模拟I2C时序。
#include "stm32f10x.h"
#include "stdio.h"
int fputc(int ch, FILE *f)
{
while(USART_GetFlagStatus(USART1,USART_FLAG_TC)==RESET);
USART_SendData(USART1,(uint8_t)ch);
return ch;
}
/*
1.挂载时钟
2.初始化GPIO(推挽输出、高电平)PB6-SCL PB7-SDA数据
*/
#define SDA_IN() {GPIOB->CRL&=0X0FFFFFFF;GPIOB->CRL|=(u32)8<<28;}//输入
#define SDA_OUT() {GPIOB->CRL&=0X0FFFFFFF;GPIOB->CRL|=(u32)3<<28;}//输出
#define SDA_1 GPIO_SetBits(GPIOB,GPIO_Pin_7)
#define SDA_0 GPIO_ResetBits(GPIOB,GPIO_Pin_7)
#define SCL_1 GPIO_SetBits(GPIOB,GPIO_Pin_6)
#define SCL_0 GPIO_ResetBits(GPIOB,GPIO_Pin_6)
#define READ_SDA GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_7)
//串口初始化
void Usart_Init(void)
{
GPIO_InitTypeDef GPIO_ITDef1;
GPIO_InitTypeDef GPIO_ITDef;
USART_InitTypeDef USART_ITDef;
//挂载时钟(复用PA) 串口时钟使能,GPIO 时钟使能,复用时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA,ENABLE);
//PA9 TXD初始化
GPIO_ITDef.GPIO_Pin = GPIO_Pin_9;//PA9 TXD
GPIO_ITDef.GPIO_Mode = GPIO_Mode_AF_PP;复用推挽输出
GPIO_ITDef.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_ITDef);
//PA10 TXD初始化
GPIO_ITDef1.GPIO_Pin = GPIO_Pin_10;//PA10 RXD
GPIO_ITDef1.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA,&GPIO_ITDef1);
//USART初始化
USART_ITDef.USART_BaudRate = 115200;//波特率
USART_ITDef.USART_WordLength = USART_WordLength_8b;//发送数据长度
USART_ITDef.USART_StopBits = USART_StopBits_1; //一个停止位
USART_ITDef.USART_Parity = USART_Parity_No; //无奇偶校验位
USART_ITDef.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_ITDef.USART_Mode = USART_Mode_Tx| USART_Mode_Rx ;//发送模式
USART_Init(USART1,&USART_ITDef);
/*中断配置*/
USART_ITConfig(USART1,USART_IT_TC,DISABLE);
USART_ITConfig(USART1,USART_IT_RXNE,DISABLE);
USART_ITConfig(USART1,USART_IT_IDLE,ENABLE);
USART_Cmd(USART1, ENABLE);//使能串口
}
void delay_ms(u16 time)
{
u16 i = 0;
while(time--)
{
i = 12000;
while(i--);
}
}
void delay_us(u16 time)
{
u16 i = 0;
while(time--)
{
i=10;
while(i--);
}
}
void PB67_Init(void)
{
//挂载时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_7|GPIO_Pin_6;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;//GPIO_Mode_Out_PP推挽输出
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStruct);
GPIO_SetBits(GPIOB,GPIO_Pin_6|GPIO_Pin_7);
}
//GPIOB->BSRR = GPIO_Pin_X;//高电平
void Start(void)
{
//SDA为输出,把SDA和SCL都设置输出为高,延时4us等待稳定,把SDA设置为低,延时4us等待稳定,把SCL设置为低。
//PB6-SCL PB7-SDA
SDA_OUT();
SDA_1;
delay_us(1);
SCL_1;
delay_us(5);
SDA_0;
delay_us(5);
SCL_0;
}
void Stop(void)
{
//SDA为输出,把SDA和SCL都设置输出为低,延时4us等待稳定,先把SCL设置输出为高,在把SDA为高,延时4us等待稳定。
SDA_OUT();
SCL_0;
SDA_0;
delay_us(5);
SCL_1;
delay_us(2);
SDA_1;
delay_us(5);
}
int Wait_Ack(void)
{
//SDA为输入,把SDA和SCL设为高,等待设备应答(SDA为0),延时一段时间还未等到就视为失败,等到为低就成功。
u8 ucErrTime=0;
SDA_IN(); //SDA 设置为输入
SDA_1;
delay_us(2);
SCL_1;
delay_us(2);
while(READ_SDA)//读到低电平
{
ucErrTime++;
if(ucErrTime>250)
{
Stop();
return 1;
}
}
SCL_0; //时钟输出 0
return 0;//失败
}
//产生 ACK 应答
void IIC_Ack(void)
{
//产生应答:SAD一直为低,SCL产生一个脉冲(低-高-低)。
SCL_0;
SDA_OUT();
SDA_0;
delay_us(3);
SCL_1;
delay_us(3);
SCL_0;
}
void IIC_NAck(void)
{
//不产生:SAD一直为高,SCL产生一个脉冲(低-高-低)。
SCL_0;
SDA_OUT();
SDA_1;
delay_us(3);
SCL_1;
delay_us(3);
SCL_0;
}
void Send_Byte(u8 txd)
{
u8 t;
SDA_OUT();
SCL_0;//拉低时钟开始数据传输
for(t=0;t<8;t++)
{
if((txd&0x80)>>7)
SDA_1;
else
SDA_0;
txd<<=1;
delay_us(3);
SCL_1;
delay_us(3);
SCL_0;
delay_us(3);
}
}
int Read_Byte(unsigned char ack)
{
unsigned char i,receive=0;
SDA_IN();//SDA设置为输入
for(i=0;i<8;i++)
{
SCL_0;
delay_us(3);
SCL_1;
receive<<=1;
if(READ_SDA)receive++;
delay_us(2);
}
if (!ack)
IIC_NAck(); //发送 nACK
else
IIC_Ack(); //发送 ACK
return receive;
}
void write_data(u8 add,u8 val)
{
Start();
Send_Byte(0XA0);//发送写命令
Wait_Ack();//等待应答 1 成功
Send_Byte(add%256);//发送地址00-ff
Wait_Ack();//等待应答 1 成功
Send_Byte(val);
Wait_Ack();//等待应答 1 成功
Stop();
delay_ms(20);
**//这里延时比不可少,不然数据可能是0xff,位置也不能换**
}
int Read_data(u8 ReadAddr)
{
unsigned char receive_data=0;
Start();
Send_Byte(0XA0);//发送写命令
Wait_Ack();//等待应答 1 成功
Send_Byte(ReadAddr%250);//发送取的 地址00-ff
Wait_Ack();//等待应答 1 成功
Start();
Send_Byte(0XA1);//发送读命令
Wait_Ack();//等待应答1成功
receive_data = Read_Byte(0);
Stop();
return receive_data;
}
unsigned char read_data1=0;
int main(void)
{
Usart_Init();
u8 addr =0X00;
PB67_Init();//PB6-SCL PB7-SDA都为高电平
write_data(addr,12);
delay_us(1000);
read_data1 = Read_data(addr);
printf("READ DATA:%d \n",read_data1);
while(1)
{
write_data(0X35,0X41);
read_data1 = Read_data(0X35);
printf("READ DATA:%c \n",read_data1);
delay_ms(300);
}
}
心得
特别要注意时序的时间;就是在发送完数据后和接收数据的中间要加一个延时,不然会出现不合理的现象。每次SCL为高电平的时间为1.7us以上。
常用资料:
STM32F10x_StdPeriph_Lib_V3.5.0(官方固件库)
链接:STM32固件库使用手册的中文翻译版 提取码:4lkx