IIC(Inter-Integrated Circuit)总线是一种由 PHILIPS 公司开发的两线式串行线,用于连接微控制器及其外围设备。它是由数据线 SDA 和时钟 SCL 构成的串行总线,可发送和接收数据。在 CPU 与被控 IC 之间、 IC 与 IC 之间进行双向传送, 高速 IIC 总线一般可达 400kbps 以上。
I2C 通讯协议(Inter-Integrated Circuit),I2C是一种通信协议!!和USART串口和SPI、SDIO等等都是通信协议。由于它引脚少,硬件实现简单,可扩展性强,不需要 USART、 CAN 等通讯协议的外部收发设备,现在被广泛地使用在系统内多个集成电路(IC)间的通讯。
类似的,学习通信协议通常分为物理层和协议层来学习:
一.物理层
1.I2C常用的连接方式,百试不厌
特点主要有:
(1)支持多设备的总线
(2)一个 I2C 总线只使用两条总线线路,一条双向串行数据线(SDA) ,一条串行时钟线(SCL)。数据线即用来表示数据,时钟线用于数据收发同步。
(3)每个连接到总线的设备都有一个独立的地址,主机可以利用这个地址进行不同设备之间的访问。
(4)总线通过上拉电阻接到电源。当 I2C 设备空闲时,会输出高阻态,而当所有设备都空闲,都输出高阻态时,由上拉电阻把总线拉成高电平。
注意::I2C协议中,0的优先级高于3.3v,当不发送数据时都为高阻态。想发送逻辑1时,先查看是否为总线是否为3.3v,若为3.3v则发送,若为0v,表面有其他设备在发送逻辑0。
(5)标准模式传输速率为 100kbit/s ,快速模式为 400kbit/s ,高速模式
下可达 3.4Mbit/s,但目前大多 I2C 设备尚不支持高速模式。
二.协议层
I2C 的协议定义了通讯的起始和停止信号、数据有效性、响应、仲裁、时钟同步和地址广播等环节。
1.基本读写过程
2.起始和停止信号
当 SCL 线是高电平时 SDA 线从高电平向低电平切换,这个情况表示通讯的起始。当 SCL 是高电平时 SDA线由低电平向高电平切换,表示通讯的停止。起始和停止信号一般由主机产生。
在i2c工作中,有效数据即SDA只允许在SCLK低电平期间变化,在SCLK高电平期间不允许改变。所以,对于两类特殊的命令信号都是在SCLK在高电平期间SDA发生改变。
起始位如下图:在SCLK高电平期间SDA由高到低变化将被理解为起始位。
停止位如下图:在SCLK高电平期间SDA由低到高变化将被理解为起始位。
3.IIC 模式的选择 (我们用的 主要是 主发送器 )
在主模式下, I2C 接口会启动数据传输并生成时钟信号。串行数据传输始终是在出现起始位时开始,在出现停止位时结束。起始位和停止位均在主模式下由软件生成。
在从模式下,该接口能够识别其自身地址( 7 或 10 位)以及广播呼叫地址。广播呼叫地址检测可由软件使能或禁止。
通信数据格式为数据和地址均以 8 位字节传输, MSB 在前。起始位后紧随地址字节(7 位地址占据一个字节;10 位地址占据两个字节)。地址始终在主模式下传送。
在字节传输 8 个时钟周期后是第 9 个时钟脉冲,在此期间接收器必须向发送器发送一个应答位
总结一下就是:在通信时在主设备发送起始信号过后,串行数据开始传输,在第九个时钟时从设备要向主设备发送一个应答位。在串行数据传输时均是MSB在前。
对于I2C地址很多人都分不清,i2c的地址组成一般是7+1,其中最后1位为读写位(一般0为写、1为读),注意这里7位很重要,一般数据手册中给出的都是这7位地址,换句话说也就是7前7位表示8位的一个数值。
应答(ACK)”和“非应答(NACK)”两种信号。
作为数据接收端时,当设备(无论主从机)接收到 I2C 传输的一个字节数据或地址后,若希望对方继续发送数据,则需要向对方发送“应答(ACK)”信号,发送方会继续发送下一个数据;若接收端希望结束数据传输,则向对方发送“非应答(NACK)”信号,发送方接收到该信号后会产生一个停止信号,结束信号传输。
看到上图我们很好理解想要发送数据一般步骤为:
总结; (不管事件 中断 EVx )
1.发送 起始位的信号 和 应答信号(应答信号i:希望对方给我发消息)
2.等待回应
3.8位数据的 第一位 和 应答信号(应答信号i:希望对方给我发消息)
4.等待回应
5.8位数据的 第二位 和 应答信号(应答信号i:希望对方给我发消息)
6.等待回应
7.8位数据的 第三位 和 应答信号(应答信号i:希望对方给我发消息)
8.等待回应
9.8位数据的 第四位 和 应答信号(应答信号i:希望对方给我发消息)
10.等待回应
11.8位数据的 第五位 和 应答信号(应答信号i:希望对方给我发消息)
12.等待回应
13.8位数据的 第六位 和 应答信号(应答信号i:希望对方给我发消息)
14.等待回应
15.8位数据的 第七位 和 应答信号(应答信号i:希望对方给我发消息)
16.等待回应
17.8位数据的 第八位 和 应答信号(应答信号i:希望对方给我发消息)
18.等待回应
19.发送停止位
5.代码: IIC
IIC.h
#ifndef __IIC_H
#define __IIC_H
#include "stm32f4xx.h"
#include "sys.h"
#include "delay.h"
#define SDA_OUT {GPIOB->MODER &= ~(3<<(7*2));GPIOB->MODER |= 1<<(7*2);}//先清零,后赋值 PB7引脚配为输出模式
#define SDA_IN {GPIOB->MODER &= ~(3<<(7*2));GPIOB->MODER |= 0<<(7*2);}//先清零,后赋值 PB7引脚配为输入模式
#define IIC_SCL PBout(6) //IIC的时钟线的输出数据寄存器
#define IIC_SDA PBout(7) //IIC的数据线的输出数据寄存器
#define READ_SDA PBin(7) //IIC的数据线的输入数据寄存器
void IIC_init(void);//IIC初始化
//起始信号
void IIC_start(void);
//停止信号
void IIC_stop(void);
//发送一个字节数据
void IIC_Send_Byte(u8 txd);
//接收一个字节数据
u8 IIC_READ_Byte(u8 ack);
//主机向从机发送低电平应答信号
void IIC_ACK(void);
//主机向从机发送高电平非应答信号
void IIC_NACK(void);
//主机等待从机的应答信号
u8 IIC_wait_ack(void);
#endif
IIC.c
#include "iic.h"
//IIC引脚初始化。
void IIC_init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
//时钟初始化
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);
//引脚初始化
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_OUT;
GPIO_InitStruct.GPIO_OType = GPIO_OType_OD;//开漏输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6|GPIO_Pin_7;
GPIO_InitStruct.GPIO_PuPd = GPIO_PuPd_UP;//上拉电阻
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_100MHz;//输出速度
GPIO_Init(GPIOB,&GPIO_InitStruct);
}
//起始信号
void IIC_start(void)
{
//1.数据线切换成输出模式
SDA_OUT;
//2.总线切回空闲状态
IIC_SDA = 1;
IIC_SCL = 1;
delay_us(2);
//3.时钟线为高电平期间,数据线由高到低跳变。
IIC_SDA = 0;
delay_us(2);
//4.时钟线为低电平,数据线的数据允许变化
IIC_SCL = 0;
}
//停止信号
void IIC_stop(void)
{
//1.数据线切换成输出模式
SDA_OUT;
//2.数据线切换成低电平
IIC_SCL = 0;
IIC_SDA = 0;
delay_us(2);
//3.时钟线为高电平期间,数据线由低到高的跳变
IIC_SCL = 1;
delay_us(2);
IIC_SDA = 1;
}
//发送一个字节数据
void IIC_Send_Byte(u8 txd)
{
u8 i;
//1.数据线切换成输出模式
SDA_OUT;
//2.时钟线为低电平,数据线允许改变
IIC_SCL = 0;
for(i = 0;i < 8 ;i++)
{
//IIC_SDA = (txd & 0x80) >> 7;//数据从第八位开始发送。
IIC_SDA = txd >> 7;//数据从第八位开始发送。
txd <<= 1;
//时钟线为高电平期间 数据保持稳定。
IIC_SCL = 1;
delay_us(2);
IIC_SCL = 0;//时钟线为低电平,数据线允许改变
delay_us(2);
}
}
//接收一个字节数据,当ack= 1,发送应答信号,ack= 0,发送非应答信号
u8 IIC_READ_Byte(u8 ack)
{
u8 i,receive = 0;
//1.数据线切成输入模式
SDA_IN;
for(i = 0;i < 8 ;i++)
{
IIC_SCL = 0;//时钟线为低电平,数据线允许改变
delay_us(2);
receive<<=1;
if(READ_SDA) receive++;
//时钟线为高电平期间 数据保持稳定。
IIC_SCL = 1;
delay_us(2);
}
if(ack) IIC_ACK();
else IIC_NACK();
return receive;
}
//主机向从机发送低电平应答信号
void IIC_ACK(void)
{
//1.数据线切换成输出模式
SDA_OUT;
IIC_SCL = 0;//时钟线为低电平,数据线允许改变
delay_us(2);
IIC_SDA = 0;//低电平应答信号
//时钟线为高电平期间 数据保持稳定。
IIC_SCL = 1;
delay_us(2);
IIC_SCL = 0;//时钟线为低电平,数据线允许改变
}
//主机向从机发送高电平非应答信号
void IIC_NACK(void)
{
//1.数据线切换成输出模式
SDA_OUT;
IIC_SCL = 0;//时钟线为低电平,数据线允许改变
delay_us(2);
IIC_SDA = 1;//高电平非应答信号
//时钟线为高电平期间 数据保持稳定。
IIC_SCL = 1;
delay_us(2);
}
//主机等待从机的低电平应答信号
u8 IIC_wait_ack(void)
{
u8 waittime = 0;
//1.数据线切成输入模式
SDA_IN;
//2.总线切回空闲状态
IIC_SDA = 1;delay_us(2);
IIC_SCL = 1;
delay_us(2);
/*
IIC_SCL = 0;
delay_us(2);
*/
//3.等待从机应答。
while(READ_SDA)
{
waittime++;
if(waittime > 250)
{
IIC_stop();
return 1;//超时读取应答失败
}
}
IIC_SCL = 0;//时钟线为低电平,数据线允许改变
return 0;//读取到从机应答。
}
关于芯片 : at24c02
原理图:
代码:
at24cxx.h
#ifndef __24CXX_H
#define __24CXX_H
#include "sys.h"
#define AT24C01 127
#define AT24C02 255
#define AT24C04 511
#define AT24C08 1023
#define AT24C16 2047
#define AT24C32 4095
#define AT24C64 8191
#define AT24C128 16383
#define AT24C256 32767
//蜘蛛侠开发板使用的是24c02,所以定义EE_TYPE为AT24C02
#define EE_TYPE AT24C02
u8 AT24CXX_ReadOneByte(u16 ReadAddr); //指定地址读取一个字节
void AT24CXX_WriteOneByte(u16 WriteAddr,u8 DataToWrite); //指定地址写入一个字节
void AT24CXX_WriteLenByte(u16 WriteAddr,u32 DataToWrite,u8 Len);//指定地址开始写入指定长度的数据
u32 AT24CXX_ReadLenByte(u16 ReadAddr,u8 Len); //指定地址开始读取指定长度数据
void AT24CXX_Write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite); //从指定地址开始写入指定长度的数据
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead); //从指定地址开始读出指定长度的数据
u8 AT24CXX_Check(void); //检查器件
void AT24CXX_Init(void); //初始化IIC
void AT24CXXTest(void);
#endif
at24cxx.c
#include "sys.h"
#include "at24cxx.h"
#include "iic.h"
#include "delay.h"
#include <stdio.h>
/************************************************************
函数名:AT24CXX_Init
功能: 初始化 AT24CXX GPIO 为输出工作模式
输入参数:无
输出参数:无
返回值:无
备注: AT24CXX_PIN --- PIN6|PIN7
************************************************************/
void AT24CXX_Init(void)
{
IIC_init();
}
/************************************************************
函数名:AT24CXX_ReadOneByte
功能: 在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_WriteOneByte
功能: 在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);
}
/************************************************************
函数名:AT24CXX_WriteLenByte
功能: 在AT24CXX里面的指定地址开始写入长度为Len的数据
输入参数:WriteAddr,DataToWrite,Len
输出参数:无
返回值:无
备注:
************************************************************/
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_ReadLenByte
功能: 在AT24CXX里面的指定地址开始读出长度为Len的数据
输入参数:ReadAddr,Len
输出参数:无
返回值:读到的数据
备注:
************************************************************/
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_Check
功能: 检查AT24CXX是否正常
输入参数:无
输出参数:无
返回值:1:检测失败
0:检测成功
备注: 这里用了24XX的最后一个地址(255)来存储标志字.
如果用其他24C系列,这个地址要修改
************************************************************/
u8 AT24CXX_Check(void)
{
u8 temp;
temp=AT24CXX_ReadOneByte(32767);//避免每次开机都写AT24CXX
if(temp==0X55)return 0;
else//排除第一次初始化的情况
{
AT24CXX_WriteOneByte(32767,0X55);
temp=AT24CXX_ReadOneByte(32767);
if(temp==0X55)return 0;
}
return 1;
}
/************************************************************
函数名:AT24CXX_Read
功能: 在AT24CXX里面的指定地址开始读出指定个数的数据
输入参数:ReadAddr,*pBuffer,NumToRead
输出参数:无
返回值:无
备注:
************************************************************/
void AT24CXX_Read(u16 ReadAddr,u8 *pBuffer,u16 NumToRead)
{
while(NumToRead)
{
*pBuffer++=AT24CXX_ReadOneByte(ReadAddr++);
NumToRead--;
}
}
/************************************************************
函数名:AT24CXX_Write
功能: 在AT24CXX里面的指定地址开始写入指定个数的数据
输入参数:WriteAddr,*pBuffer,NumToWrite
输出参数:无
返回值:无
备注:
************************************************************/
void AT24CXX_Write(u16 WriteAddr,u8 *pBuffer,u16 NumToWrite)
{
while(NumToWrite--)
{
AT24CXX_WriteOneByte(WriteAddr,*pBuffer);
WriteAddr++;
pBuffer++;
}
}
/*
测试 AT24C02 蜘蛛侠开发板采用AT24C02 0 ~ 255字节
*/
void AT24CXXTest(void)
{
uint16_t i = 0;
uint8_t temp = 0;
for(i = 0;i <255;i++)
{
AT24CXX_WriteOneByte(i,0x33);
}
for(i = 0;i <255;i++) //1111 1111
{
temp = AT24CXX_ReadOneByte(i);
printf("temp: %x\r\n", temp);
}
}