一,I2C简介
IIC(Inter-Integrated Circuit)协议,也被称为I2C(Inter-IC)协议,是一种串行通信协议,用于连接集成电路(IC)之间的通信。IIC协议由飞利浦(Philips)公司在1980年代初开发,旨在简化IC之间的通信。IIC协议使用两根线进行通信:串行数据线(SDA)和串行时钟线(SCL)。SDA线用于传输数据,而SCL线用于同步数据传输的时钟信号。这两根线都通过上拉电阻连接到供电电压。在IIC协议中,通信由一个主设备(master)和一个或多个从设备(slave)组成。主设备负责发起通信和控制整个通信过程,而从设备则被动地响应主设备的指令。主设备通过发送起始条件(Start)信号来启动通信,并通过发送从设备地址来选择要与之通信的从设备。然后,主设备发送或接收数据,并通过发送停止条件(Stop)信号来结束通信。IIC协议支持多种数据传输模式,包括标准模式、快速模式和高速模式。标准模式下,数据传输速率最高为100 kbps;快速模式下,数据传输速率最高为400 kbps;高速模式下,数据传输速率最高可达3.4 Mbps。IIC协议在许多应用中得到广泛应用,例如连接传感器、存储器、显示器、扩展模块等。它的简单性和可扩展性使得IIC协议成为一种常见的串行通信协议。
二,I2C硬件连接示例
手册给的示例图:
简化:
三,时序图
手册给的示例图:
简化:
四,代码解读
起始信号:
/*
函数名称 :I2cStart
作用 :实现I2C通信的起始信号电平变化
参数 :无
返回值 :无
*/
void I2cStart(void)
{
/* SCL为高电平期间, SDA从高电平往低电平跳变*/
IIC_SDA(1);
IIC_SCL(1);
MyDelayUs(2);
IIC_SDA(0);
MyDelayUs(2);
/* 钳住总线, 准备发送/接收数据 */
IIC_SCL(0);
MyDelayUs(2);
}
看时序图可知发送开始信号两条通信线的电平变化要求,开始时两条通信线电平皆为高,然后SDA线从高电平到低电平变化,即SCL为高时,SDA产生下降沿为开始信号。因为主机电平变化后会需要维持一定的时间(一般为微秒级),从机才读为有效信号,故需要延时函数 MyDelayUs()来实现微秒延时。注意发完起始信号,等待下一步动作时,应将SCL拉低,这样无论SDA无论如何变化都是无效的,这一点可以在手册中的数据有效性的章节看到《3.1.3数据有效性》,SDA有效时,必是在SCL为高电平时。
函数说明:(这些函数都在头文件中有相应的定义,实现非常简单,可自己看下面的头文件)
IIC_SDA(1) 使单片机链接SDA的管脚输出高电平,写0即输出低电平
IIC_SCL(1) 使单片机链接SCL的管脚输出高电平,写0即输出低电平
MyDelayUs(2) 利用滴答定时器实现2微秒的延时 IIC_READ_SDA 是读SDA管脚的宏
写信号:
/*
函数名称 :I2cSendByte
作用 :实现I2C通信的发送一个字节信号电平变化
参数 data:需要发送的字节数据
返回值 :无
*/
void I2cSendByte(uint8_t data)
{
for (uint8_t t=0;t<8;t++)
{
/* 高位先发 */
IIC_SDA((data&0x80)>>7);
MyDelayUs(2);
IIC_SCL(1);
MyDelayUs(2);
IIC_SCL(0);
data<<= 1; /* 左移1位, 用于下一次发送 */
}
IIC_SDA(1); /* 发送完成,主机释放SDA线 */
}
该时序图写信号画出的是发送字节第一位为1时的SCL和SDA时序变化,SCL每一个时钟脉冲,SDA为高或低来表示0或1,读写都是八个时钟脉冲,完成这八个时钟脉冲动作即为写或读信号,第九个是应答的时钟脉冲。
注意在写完后要将SDA拉高,以释放SDA线进入等待的状态,来进行等待从机返回确认收到或未收到发送的数据,这点在时序图可能看不出来,时序图是发送完信息下一个是应答信号,其实它们间应该还有一个等待应答的状态,但在手册的《3.1.6确认(ACK)和不确认(NACK)》章节有提到,就是说发完信息号后需要进入等待确认信号的状态。
等待确认函数:
/*
函数名称 :I2cWaitAck
作用 :实现I2C通信的等待应答信号电平变化
参数 :无
返回值 :0表示从机nack 1表示结束ACK检查
*/
uint8_t I2cWaitAck(void) /* return 1:fail 0:succeed*/
{
IIC_SDA(1); /* 主机释放SDA线 */
MyDelayUs(2);
IIC_SCL(1); /* 从机返回ACK*/
MyDelayUs(2);
if (IIC_READ_SDA) /* SCL高电平读取SDA状态*/
{
I2cStop(); /* SDA高电平表示从机nack */
return 1;
}
IIC_SCL(0); /* SCL低电平表示结束ACK检查 */
MyDelayUs(2);
return 0;
}
这个等待我们已经在写函数中封装为什么又单独写一个函数呢?因为在一开时初始化时我们可以使用这个等待信号,并返回相应的值,来确认相应的从机是否存在。
读函数:
/*
函数名称 :I2cReadByte
作用 :实现I2C通信的读取一个字节信号电平变化
参数 ack:本次读取完成 0接下来发送非应答信号 1发送应答信号
返回值 receive:读取的一个字节数据
*/
uint8_t I2cReadByte(uint8_t ack)
{
uint8_t receive = 0 ;
for(uint8_t t = 0; t < 8; t++)
{
/* 高位先输出,先收到的数据位要左移 */
receive<<= 1;
IIC_SCL(1);
MyDelayUs(2);
if (IIC_READ_SDA)receive++;
IIC_SCL(0);
MyDelayUs(2);
}
if(!ack)I2cNack();
else I2cAck();
return receive;
}
这显示的是读一个字节的第一为0的SCL和SDA的情况。
你会发现读和写的时序是一样的,为方便连读这里加入了应答函数和非应答函数。
应答函数:
/*
函数名称 :I2cAck
作用 :实现I2C通信的应答信号电平变化
参数 :无
返回值 :无
*/
void I2cAck(void)
{
IIC_SCL(0);
MyDelayUs(2);
/* 数据线为低电平,表示应答 */
IIC_SDA(0);
MyDelayUs(2);
IIC_SCL(1);
MyDelayUs(2);
}
在SCL的完成一个上升沿脉冲期间SDA为低,即表示应答。
非应答:
/*
函数名称 :I2cNack
作用 :实现I2C通信的非应答信号电平变化
参数 :无
返回值 :无
*/
void I2cNack(void)
{
IIC_SCL(0);
MyDelayUs(2);
/* 数据线为高电平,表示非应答 */
IIC_SDA(1);
MyDelayUs(2);
IIC_SCL(1);
MyDelayUs(2);
}
应答信号其实是第九个时钟脉冲时如果SDA是高就是非应答,低就是应答 。
停止函数:
/*
函数名称 :I2cStop
作用 :实现I2C通信的停止信号电平变化
参数 :无
返回值 :无
*/
void I2cStop(void)
{
/* SCL为高电平期间, SDA从低电平往高电平跳变*/
IIC_SDA(0);
MyDelayUs(2);
IIC_SCL(1);
MyDelayUs(2);
IIC_SDA(1);
MyDelayUs(2);
}
和起始信号相反,SCL高电平期间SDA从低到高即为停止信号。应答的时序和数据的读写时序是一样的,但实际我们并没有这么写,应答后SCL并没有下拉。
初始化函数:
/*
函数名称 :I2CInit
作用 :实现I2C通信的初始化
参数 :无
返回值 :无
*/
void I2cInit(void)
{
IIC_SDA(1);
IIC_SCL(1);
I2cStop();
}
五,完整代码及使用方法
i2c.h文件
#ifndef __I2C_H
#define __I2C_H
#include "main.h"
/*--------------------------------IO操作宏定义-----------------------------------------------*/
#define IIC_SCL(x) do{ x ? \
HAL_GPIO_WritePin(SCL_GPIO_Port,SCL_Pin,GPIO_PIN_SET) : \
HAL_GPIO_WritePin(SCL_GPIO_Port,SCL_Pin,GPIO_PIN_RESET); \
}while(0) /* SCL */
#define IIC_SDA(x) do{ x ? \
HAL_GPIO_WritePin(SDA_GPIO_Port,SDA_Pin,GPIO_PIN_SET) : \
HAL_GPIO_WritePin(SDA_GPIO_Port,SDA_Pin,GPIO_PIN_RESET); \
}while(0) /* SDA */
#define IIC_READ_SDA HAL_GPIO_ReadPin(SDA_GPIO_Port, SDA_Pin) /* 读取SDA */
/*--------------------------------函数声明---------------------------------------------------*/
void I2cInit(void);
void MyDelayUs(uint32_t us);
void I2cStart(void);
void I2cStop(void);
uint8_t I2cWaitAck(void);
void I2cAck(void);
void I2cNack(void);
void I2cSendByte(uint8_t data);
uint8_t I2cReadByte(uint8_t ack);
#endif
i2c.c文件
#include "i2c.h"
/*------------------------------------移植说明----------------------------------------*/
//需要在cubemx中将相应的管脚配重命名为SDA和SCL,SDA配置为开漏输出,上拉,SCL的配置为推挽输出,上拉,速度都为高
//延时函数延时时间根据自己器件的高低电平最小持续时间进行更改
/*------------------------------------I2C相关函数实现----------------------------------------*/
/*
函数名称 :MyDelayUs
作用 :实现微秒的软件延时
参数 us:要延时的微秒数
返回值 :无
*/
void MyDelayUs(uint32_t us)
{
__IO uint32_t currentTicks = SysTick->VAL;
const uint32_t tickPerMs = SysTick->LOAD + 1;
const uint32_t nbTicks = ((us - ((us > 0) ? 1 : 0)) * tickPerMs) / 1000;
uint32_t elapsedTicks = 0;
__IO uint32_t oldTicks = currentTicks;
do {
currentTicks = SysTick->VAL;
elapsedTicks += (oldTicks < currentTicks) ? tickPerMs + oldTicks - currentTicks :
oldTicks - currentTicks;
oldTicks = currentTicks;
} while (nbTicks > elapsedTicks);
}
/*
函数名称 :I2cStart
作用 :实现I2C通信的起始信号电平变化
参数 :无
返回值 :无
*/
void I2cStart(void)
{
/* SCL为高电平期间, SDA从高电平往低电平跳变*/
IIC_SDA(1);
IIC_SCL(1);
MyDelayUs(2);
IIC_SDA(0);
MyDelayUs(2);
/* 钳住总线, 准备发送/接收数据 */
IIC_SCL(0);
MyDelayUs(2);
}
/*
函数名称 :I2cStop
作用 :实现I2C通信的停止信号电平变化
参数 :无
返回值 :无
*/
void I2cStop(void)
{
/* SCL为高电平期间, SDA从低电平往高电平跳变*/
IIC_SDA(0);
MyDelayUs(2);
IIC_SCL(1);
MyDelayUs(2);
IIC_SDA(1);
MyDelayUs(2);
}
/*
函数名称 :I2CInit
作用 :实现I2C通信的初始化
参数 :无
返回值 :无
*/
void I2cInit(void)
{
IIC_SDA(1);
IIC_SCL(1);
I2cStop();
}
/*
函数名称 :I2cWaitAck
作用 :实现I2C通信的等待应答信号电平变化
参数 :无
返回值 :0表示从机nack 1表示结束ACK检查
*/
uint8_t I2cWaitAck(void) /* return 1:fail 0:succeed*/
{
IIC_SDA(1); /* 主机释放SDA线 */
MyDelayUs(2);
IIC_SCL(1); /* 从机返回ACK*/
MyDelayUs(2);
if (IIC_READ_SDA) /* SCL高电平读取SDA状态*/
{
I2cStop(); /* SDA高电平表示从机nack */
return 1;
}
IIC_SCL(0); /* SCL低电平表示结束ACK检查 */
MyDelayUs(2);
return 0;
}
/*
函数名称 :I2cAck
作用 :实现I2C通信的应答信号电平变化
参数 :无
返回值 :无
*/
void I2cAck(void)
{
IIC_SCL(0);
MyDelayUs(2);
/* 数据线为低电平,表示应答 */
IIC_SDA(0);
MyDelayUs(2);
IIC_SCL(1);
MyDelayUs(2);
}
/*
函数名称 :I2cNack
作用 :实现I2C通信的非应答信号电平变化
参数 :无
返回值 :无
*/
void I2cNack(void)
{
IIC_SCL(0);
MyDelayUs(2);
/* 数据线为高电平,表示非应答 */
IIC_SDA(1);
MyDelayUs(2);
IIC_SCL(1);
MyDelayUs(2);
}
/*
函数名称 :I2cSendByte
作用 :实现I2C通信的发送一个字节信号电平变化
参数 data:需要发送的字节数据
返回值 :无
*/
void I2cSendByte(uint8_t data)
{
for (uint8_t t=0;t<8;t++)
{
/* 高位先发 */
IIC_SDA((data&0x80)>>7);
MyDelayUs(2);
IIC_SCL(1);
MyDelayUs(2);
IIC_SCL(0);
data<<= 1; /* 左移1位, 用于下一次发送 */
}
IIC_SDA(1); /* 发送完成,主机释放SDA线 */
}
/*
函数名称 :I2cReadByte
作用 :实现I2C通信的读取一个字节信号电平变化
参数 ack:本次读取完成 0接下来发送非应答信号 1发送应答信号
返回值 receive:读取的一个字节数据
*/
uint8_t I2cReadByte(uint8_t ack)
{
uint8_t receive = 0 ;
for(uint8_t t = 0; t < 8; t++)
{
/* 高位先输出,先收到的数据位要左移 */
receive<<= 1;
IIC_SCL(1);
MyDelayUs(2);
if (IIC_READ_SDA)receive++;
IIC_SCL(0);
MyDelayUs(2);
}
if(!ack)I2cNack();
else I2cAck();
return receive;
}
使用说明:
你需要在stm32cubem进行相应的管脚配置及改名如:
然后将.c和.h文件加入你的工程即可。注意要将SDA配置为开漏。