STM32基础知识:IIC概述与软件模拟IIC

描述

IIC概述与软件模拟IIC

1 IIC总线概述

1.1 基本概念

内部集成电路(Inter Integrated circuit )的简称叫做IIC,是一种简单的、半双工同步通信的串行通信接口,IIC总线是上世纪80年代(1982年)由飞利浦公司设计出来,当时的目的是为了给MCU和外围芯片提供更简单的交互方式。

1.2 引脚说明

IIC总线只需要两根引脚就可以实现通信,一根是数据线SDA,另一根是时钟线SCL,所有通过IIC接口通信的外围器件都挂载在IIC总线上,通过这种机制就可以实现多机通信。

总线

可以看到,外围器件的时钟线和数据线都是挂载在IIC总线(由主控芯片提供),并且在空闲状态下所有器件的时钟线SCL和数据线SDA都被总线的上拉电阻拉高,这样就可以把SDA引脚和SCL引脚设置为开漏模式即可,好处是防止短路。

每个挂载在IIC总线上的外围器件都有独立的器件地址,主机发送开始信号后,只需要发送想要通信的设备的地址,如果设备收到地址并且匹配正确,则开始进行单独通信。

1.3 通信速率

IIC总线支持不同的通信速率,但是一般常用的标准速率100KHZ,但是有的外围器件可以支持高达400KHZ的通信速率,而由于IIC总线是半双工通信,所以同一时刻只能接收或者发送,也就是说,IIC总线一般是为了控制,不适合作为大量数据传输的接口。

1.4 通信过程

接口可以下述4种模式中的一种运行:

  • 从发送器模式
  • 从接收器模式
  • 主发送器模式
  • 主接收器模式

默认状态下工作于从模式接口在生成起始条件后自动地从从模式切换到主模式 (谁先发送开始信号,谁就作为主机)。当仲裁丢失或产生停止信号时,则从主模式切换到从模式,从而实现多主模式功能。

通信流

  • 主模式时,I2C接口启动数据传输并产生时钟信号。串行数据传输总是以起始条件开始并以停止条件结束。起始条件和停止条件都是在主模式下由软件控制产生。
  • 从模式时,I2C接口能识别它自己的地址(7位或10位)和广播呼叫地址。软件能够控制开启或禁止广播呼叫地址的识别。
  • 数据和地址按8位/字节进行传输,高位在前。跟在起始条件后的1或2个字节是地址(7位模式为1个字节,10位模式为2个字节)。地址只在主模式发送。
  • 在一个字节传输的8个时钟后的第9个时钟期间,接收器必须回送一个应答位(ACK)给发送器。参考下图。

总线

可以看到,在建立通信的时候主机需要发送 开始信号 ,紧接着主机需要发出从器件的 设备地址 (7bit+1bit),从设备的物理地址是7bit,但是由于只有一根数据线,就需要说清楚数据的传输方向,数据的传输方向通过从设备的地址最低位进行表示(最低位是0,表示写操作,最低位是1,表示读操作),IIC总线提供了 应答机制 ,也就是说从机收到了1个字节的数据之后,会在第九个脉冲发送给主机一个应答信号(1bit),如果主机收到从机的应答信号,则主机可以继续发送数据,反之,如果主机没有收到从机发送的应答信号,那主机就不应该继续发送数据,而是应该主动发出一个 停止信号 ,表示停止通信。

2 软件模拟IIC的实现

2.1 IIC初始化

// ---------- software_iic.h ----------
#ifndef __SOFTWARE_IIC_H__
#define __SOFTWARE_IIC_H__

#include "main.h"
#include "tim.h"
#include "gpio.h"

#define DLY_TIM_Handle (&htim1)


// SCL: PB10, SDA: PB11
#define IIC_SCL_PORT GPIOB
#define IIC_SCL_PIN  GPIO_PIN_10
#define IIC_SDA_PORT GPIOB
#define IIC_SDA_PIN  GPIO_PIN_11

#define IIC_SDA_GPIO_CLK_ENABLE()       __HAL_RCC_GPIOB_CLK_ENABLE()
#define IIC_SCL_GPIO_CLK_ENABLE()       __HAL_RCC_GPIOB_CLK_ENABLE()

#define IIC_SCL_WRITE_UP()    HAL_GPIO_WritePin(IIC_SCL_PORT, IIC_SCL_PIN, GPIO_PIN_SET)
#define IIC_SCL_WRITE_DOWN()  HAL_GPIO_WritePin(IIC_SCL_PORT, IIC_SCL_PIN, GPIO_PIN_RESET)
#define IIC_SDA_WRITE_UP()    HAL_GPIO_WritePin(IIC_SDA_PORT, IIC_SDA_PIN, GPIO_PIN_SET)
#define IIC_SDA_WRITE_DOWN()  HAL_GPIO_WritePin(IIC_SDA_PORT, IIC_SDA_PIN, GPIO_PIN_RESET)

#define IIC_SDA_READ()        HAL_GPIO_ReadPin(IIC_SDA_PORT, IIC_SDA_PIN)

void delay_us(uint16_t nus);

void IIC_Init(void);
void IIC_SDA_OutputMode(void);
void IIC_SDA_InputMode(void);
void IIC_StartSignal(void);
void IIC_StopSignal(void);
void IIC_SendBytes(uint8_t data);
uint8_t IIC_ReadBytes(void);
uint8_t IIC_WaitACK(void);
void IIC_MasterACK(uint8_t ack);

#endif
// ---------- software_iic.c ----------
void IIC_Init(void)
{
  // 初始化SCL和SDA为开漏输出
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  IIC_SDA_GPIO_CLK_ENABLE();
  IIC_SCL_GPIO_CLK_ENABLE(); 

  GPIO_InitStruct.Pin = IIC_SCL_PIN;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(IIC_SCL_PORT, &GPIO_InitStruct);

  GPIO_InitStruct.Pin = IIC_SDA_PIN;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(IIC_SDA_PORT, &GPIO_InitStruct);

  // 初始化SCL和SDA为高电平
  IIC_SCL_WRITE_UP();
  IIC_SDA_WRITE_UP();
}

2.2 IIC模式

// SDA输出模式
void IIC_SDA_OutputMode(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  IIC_SDA_GPIO_CLK_ENABLE();

  GPIO_InitStruct.Pin = IIC_SDA_PIN;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
  HAL_GPIO_Init(IIC_SDA_PORT, &GPIO_InitStruct);
}

// SDA输入模式
void IIC_SDA_InputMode(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  IIC_SDA_GPIO_CLK_ENABLE();

  GPIO_InitStruct.Pin = IIC_SDA_PIN;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_PULLUP;

  HAL_GPIO_Init(IIC_SDA_PORT, &GPIO_InitStruct);
}

2.3 开始信号

总线

开始信号由主机发出,表示打算和所有的从器件进行通信,IIC总线规定在SCL时钟线保持高电平期间,把SDA数据线拉低,表示开始信号。

// IIC开始信号
void IIC_StartSignal(void)
{
  IIC_SDA_OutputMode();  // 设置SDA为输出模式

  // 确保SCL和SDA都是高电平 
  IIC_SCL_WRITE_UP();
  IIC_SDA_WRITE_UP();

  // 拉低SDA,产生一个下降沿
  // 一般常用的IIC总线标准速率为100kHz,即每个时钟周期为10us,故SDA低电平应持续5us
  IIC_SDA_WRITE_DOWN();  // SDA拉低
  delay_us(6);           // 为了保证兼容性,这里延时6us

  // 拉低SCL,表示准备通信
  IIC_SCL_WRITE_DOWN();  // SCL拉低
}

如何实现微秒级的延时可以参考下文

STM32基于HAL库实现微秒延时

2.4 停止信号总线

停止信号由主机发出,表示不打算和从器件继续通信,IIC总线规定在SCL时钟线保持高电平期间,把SDA数据线拉高,表示停止信号。

// IIC停止信号
void IIC_StopSignal(void)
{
  IIC_SDA_OutputMode();  // 设置SDA为输出模式

  // 确保SCL和SDA都是低电平
  IIC_SCL_WRITE_DOWN();
  IIC_SDA_WRITE_DOWN();

  // 拉高SCL,产生一个上升沿
  // 一般常用的IIC总线标准速率为100kHz,即每个时钟周期为10us,故SCL高电平应持续5us
  IIC_SCL_WRITE_UP();
  delay_us(5);
  
  IIC_SDA_WRITE_UP(); // 拉高SDA,表示通信结束
  delay_us(5);        // 确保SDA的电平可以被其他器件检测到
}

2.5 数据发送

总线

在主机发送开始信号后,就可以发送数据或者地址,IIC总线规定数据的收发都是 MSB (高位先出),由于只有一个数据线,所以IIC采用串行方式把数据的每个bit位发出去。

由于SCL提供的脉冲周期是有规律的,所以IIC总线规定只能在SCL脉冲周期的高电平期间进行数据的读取或者写入,在SCL脉冲周期的低电平期间可以进行数据的修改。

// 主机发送数据
void IIC_SendBytes(uint8_t Data)
{
  uint8_t i = 0;

  IIC_SDA_OutputMode();  // 设置SDA为输出模式
      
  // 确保SCL和SDA都是低电平
  IIC_SCL_WRITE_DOWN();
  IIC_SDA_WRITE_DOWN();

  // 开始发送8位数据
  for (i = 0; i < 8; i++)
  {
    // SCL低电平期间主机准备数据
    if (Data & (1 < < (7 - i))) // 判断数据的第7-i位是否为1
    {
      IIC_SDA_WRITE_UP();  // 如果为1,SDA拉高
    }
    else
    {
      IIC_SDA_WRITE_DOWN();// 如果为0,SDA拉低
    }
    delay_us(5); // 至此,数据准备完毕

    // 拉高SCL,主机发送数据
    IIC_SCL_WRITE_UP();    
    delay_us(5); // 至此,数据发送完毕

    // 拉低SCL,准备发送下一个数据
    IIC_SCL_WRITE_DOWN();
    delay_us(5);
  }
}

2.6 数据接收

总线

在主机发送开始信号后,就可以发送数据或者地址,IIC总线规定数据的收发都是MSB(高位先出),由于只有一个数据线,所以IIC采用串行方式把数据的每个bit位发出去。

由于SCL提供的脉冲周期是有规律的,所以IIC总线规定只能在SCL脉冲周期的高电平期间进行数据的读取或者写入,在SCL脉冲周期的低电平期间可以进行数据的修改。

// 主机接收数据
uint8_t IIC_ReadBytes(void)
{
  uint8_t i = 0;
  uint8_t Data = 0; // 用于存储接收到的数据

  IIC_SDA_InputMode();  // 设置SDA为输入模式

  IIC_SCL_WRITE_DOWN(); // 确保SCL为低电平

  // 开始接收8位数据
  for (i = 0; i < 8; i++)
  {
    // 拉高SCL,主机准备接收数据
    IIC_SCL_WRITE_UP();
    delay_us(5);  // 至此,从机数据准备完毕,主机开始接收

    if (IIC_SDA_READ() == 1)   // 主机收到1
    {
      Data |= (1 < < (7 - i));  // 将收到的1存储到Data的第7-i位
    }

    /* 由于Data初始化为0000 0000,所以不需要else语句
    else // 收到0
    {
      Data &= ~(1 < < (7 - i)); // 将收到的0存储到Data的第7-i位
    }
    */
    
    // 拉低SCL,主机准备接收下一个数据
    IIC_SCL_WRITE_DOWN();
    delay_us(5);
  }
  return Data;  // 返回接收到的数据
}

2.7 应答信号

总线

IIC总线增加了应答机制,在主机发送一个字节数据之后,从机在第9个脉冲周期进行应答,如果SDA为0,则表示应答,如果SDA=1,则表示无应答,如果从机没有应答,则主机应该发送停止信号,表示停止通信。这里分为两种情况:

第一种:主机发送数据,从机进行应答

// 主机发送数据,从机进行应答
uint8_t IIC_WaitACK(void)
{
  uint8_t ack;
  IIC_SDA_InputMode();  // 设置SDA为输入模式

  IIC_SCL_WRITE_DOWN(); // 确保SCL是低电平
  delay_us(5); 

  IIC_SCL_WRITE_UP();  // 拉高SCL,主机准备接收从机的应答信号
  delay_us(5);         // 至此,从机应答信号准备完毕,主机开始接收

  // 如果从机应答信号为0,表示从机接收到数据
  if (IIC_SDA_READ() == 0)
  {
    ack = 0;
  }
  else // 如果从机应答信号为1,表示从机没有接收到数据
  {
    ack = 1;
  }
  
  IIC_SCL_WRITE_DOWN(); // 拉低SCL,主机忽略数据
  delay_us(5);

  return ack;  // 返回从机的应答信号
}

第二种:从机发送数据,主机进行应答

// 从机发送数据,主机进行应答,0表示应答,1表示不应答
void IIC_MasterACK(uint8_t ack)
{
  IIC_SDA_OutputMode();  // 设置SDA为输出模式

  // 确保SCL和SDA都是低电平
  IIC_SCL_WRITE_DOWN();
  IIC_SDA_WRITE_DOWN();

  if (ack == 0) // 如果ack为0,表示主机应答
  {
    IIC_SDA_WRITE_DOWN(); // SDA拉低
  }
  else // 如果ack为1,表示主机不应答
  {
    IIC_SDA_WRITE_UP();   // SDA拉高
  }
  delay_us(5);  //  至此,应答信号准备完毕

  // 拉高SCL,主机发出应答信号
  IIC_SCL_WRITE_UP();
  delay_us(5);  // 至此,应答信号发送完毕

  // 拉低SCL,从机忽略数据
  IIC_SCL_WRITE_DOWN();
  delay_us(5);
}
打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分