STM32常用I2C协议代码解读

一,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配置为开漏。

  • 6
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值