模拟I2C/IIC协议

I2C/IIC协议数据格式

  • IIC协议说明书,提取码:vb9f
  • 废话不多说,直接上图,IIC基本传输格式

  • 这里以STM32的GPIO口来模拟,其它都是同理的。
  • 注意:
  1. I2C通信仅在SCL处于高电平有效
  2. I2C发送数据位从高到低(MSB->LSB)

起始信号

  • 起始信号如图所示。在SCL为高电平的时候,SDA从高到低,作为起始信号。
  1. SCL = 1
  2. SDA = 1
  3. SCL = 1(SDA,SCL均高)
  4. SDA = 0 (触发起始信号)
/**
 * IIC起始信号
 * SCL 高电平
 * SDA 高->低
 **/
void IIC_Start()
{
    SDA_OUT(); // SDA输出模式
    IIC_delay_us();
    /* 拉低时钟和总线 */
    SCL(0);
    SDA(0);
    IIC_delay_us();
    /* 拉高总线 和 时钟 */
    SDA(1);
    SCL(1);
    IIC_delay_us();
    /* 拉低总线 */
    SDA(0);
    IIC_delay_us();
    /* 拉低时钟 准备发送数据 */
    SCL(0);
}

在这里插入图片描述

发送数据

I2C Header

建立起始信号后,首先发送 从机地址 + 读/写,从机地址多数为7bit(此外有10bit),读写标志位1bit(写为0/读为1)
在这里插入图片描述)
以SHT30-DIS温湿度传感器为例
SHT30的默认从机地址:0x44
本来按4bit分割: 0x44 = 0b 0100 0100
因为是7bit的地址,所以实际上0x44 = 0b 0100 0100 = 0b 100 0100
最高位的0是不用出现的
I2C一次发生的1字节8bit,从机地址只有7bit,这显然不够,所以最低位就是读写标志位了

  1. 从机地址+写 => 0x44<<1 | 0x00 = 0x80
    0x44 => 0b 0100 0100
    0x44 << 1 => 0b 1000 1000 = 0x80
    在利用或运算:与0x00或运算可以得到最低位为0(注意由于从机地址左移1bit所以最低位不可能是1,放心大胆去做)

  2. 从机地址+读 => 0x44<<1 | 0x01 = 0x89
    利用或运算:与0x01或运算可以得到最低位为1
    0x80 => 0b 1000 1000
    |
    0x01 => 0b 0000 0001
    结果=> 0 b1000 1001 = 0x89
    在这里插入图片描述

可以先提前把地址定义好

Send One Byte

在这里插入图片描述

  • SCL = 0,准备SDA(0/1)数据
  • SCL = 1 ,发送SDA(0/1)数据
  • 一次发送 一个字节 8bit
  1. for(i=0;i<8;i++)
  2. SCL = 0 准备数据
  3. 取出最高位
    例如 TXData = 0xA6 = 0b 1010 0110
    取出最高位MSB:
    0xA6 => 0b 1 010 0110
    (按位与)&
    0x80 => 0b 1 000 0000
    最后结果 0b1000 0000(0x80)
    结果向右移动7位
    0b1000 0000 >> 7 = 0b 0000 0001 = 0x01(取出第最高位付给SDA)
    4.再把要发送的数据左移一位
    例如之前0xA6 取出最高位MSB后,下一次应该发送0xA6中的第MSB-1位
    0b 1010 0110 << 1 = 0b 0100 1100
    这样就把要取的数据放到MSB了
  4. SCL = 1 发送数据
/**
 * 主机发送一字节的数据
 * SCL 高电平 SDA输出模式(SDA 高为1,低为0)
 * SCL 低电平 准备数据
 **/
void IIC_SendOneByte(uint8_t SendData)
{
    /* 拉低时钟,SDA输出模式 */
    SCL(0);
    SDA_OUT();
    IIC_delay_us();
    /* 发送数据 */
    for (size_t i = 0; i < 8; i++)
    {
        /* 准备数据,从最高位发送 */
        if((SendData & 0x80)>>7)
            SDA(1);
        else
            SDA(0);
        SendData <<= 1;
        /* 拉高时钟,发送数据 */
        SCL(1);
        IIC_delay_us();
        /* 拉低时钟,准备数据 */
        SCL(0);
        
    }
    /* 拉低时钟,准备接收ACK*/
    SCL(0);
}

应答信号ACK/NACK

在主机发送数据后,从机接收后需要给主机一个响应信号,让主机确认是否发送数据成功,从机是否接受成功。

ACK/NACK

  • 主机发完数据后,引脚改为输入模式,读取从机响应的应答信号
  • ACK : 拉高SCL ,读取SDA ,SDA为低电平则为有效响应
  • NACK: 拉高SCL ,读取SDA ,SDA为高电平则为无效响应

从机响应ACK/NACK

  1. 发完数据,SCL = 0
  2. 修改SDA输入模式 (准备接受应答信号)
  3. 等待SDA = 1 (注意超时判断)
/**
 * 主机发送命令等待从机ACK回应
 * SCL 高电平
 * SDA输入模式,等待SDA响应
 * (SDA 高无效,低有效)
 **/
uint8_t IIC_Wait_ACK()
{
    /* 先拉低时钟,SDA改为输入模式 */
    SCL(0);
    SDA_INPUT();
    IIC_delay_us();
    /* 拉高SCL,准备接收数据 */
    SCL(1);
    /* 等待从机响应 */
    uint8_t timeout = 0;
    while (SDA_Read)
    {
        if (timeout > 10)
        {
            return ACK_ERROR;
        }
        timeout++;
        IIC_TimeOut();
    }
    SCL(0);
    return ACK_OK;
}

在这里插入图片描述
在这里插入图片描述

主机发送ACK/NACK

主机发送ACK/NACK两种情况:

1.主机发送ACK

主机发送从机读命令,从机响应后需要向主机发送数据,此时主机接收数据后需要回应从机,因此需要主动发送ACK,告诉从机完成接收数据。

  1. 拉低时钟,SCL = 0
  2. SDA输出模式(准备产生SDA信号)
  3. SDA = 0 (应答ACK)
  4. 拉高时钟,SCL =1 ,发送ACK
/**
 * 主机接收从机数据,回应从机ACK
 * SCL 高电平
 * SDA输出模式(SDA 高无效,低有效)
 **/
void IIC_ACK()
{
    /* 先拉低时钟,SDA输出模式 */
    SCL(0);
    SDA_OUT();
    /* 准备ACK 拉低SDA */
    SDA(0);
    IIC_delay_us();
    /* 拉高时钟 */
    SCL(1);
    IIC_delay_us();
    /* 再拉低时钟,结束第九脉冲 */
    SCL(0);
    IIC_delay_us();
}

在这里插入图片描述

2.主机发送NACK

当主机接收完数据,不再打算接收数据,需要回应从机不再发送数据,因此需要主动发送NACK。

  1. 拉低时钟,SCL = 0
  2. SDA输出模式(准备产生SDA信号)
  3. SDA = 1 (应答ACK)
  4. 拉高时钟,SCL =1 ,发送NACK
/**
 * 主机接收从机数据,回应从机NOACK
 * SCL 高电平
 * SDA输出模式(SDA 高无效,低有效)
 **/
void IIC_NACK()
{
    /* 先拉低时钟,SDA输出模式 */
    SCL(0);
    SDA_OUT();
    /* 准备ACK 拉高SDA */
    SDA(1);
    IIC_delay_us();
    /* 拉高时钟 */
    SCL(1);
    IIC_delay_us();
    /* 再拉低时钟,结束第九脉冲 */
    SCL(0);
    IIC_delay_us();
}

在这里插入图片描述

读取数据

数据读取参考数据发送的实现,由于I2C是以发高位到低位(MSB->LSB),可以每次用最低位接收数据,接收数据后再左移1bit,再继续这样接收下去,循环直至接收完整的8bit

  1. 拉低时钟,SCL = 0
  2. 左移1bit (准备接收数据)
  3. 拉高时钟,SCL = 1
  4. 读取SDA的值(与0x01或运算可以实现最低位置1)
    若读到的是0,可以省略与0x10做与运算,左移空出来的就是0
/**
 * 主机接收一字节的数据
 * SCL 高电平 SDA输出模式(SDA 高为1,低为0)
 **/
uint8_t IIC_ReadOneByte()
{
    uint8_t RecData = 0;
    /* 拉低时钟,SDA输入模式 */
    SCL(0);
    SDA_INPUT();
    IIC_delay_us();
    /* 接收数据 */
    for (size_t i = 0; i < 8; i++)
    {
        /* 拉低时钟,准备接收下一位 */
        SCL(0);
        IIC_delay_us();
        /* 拉高时钟,从最高位接收 */
        SCL(1);
        /* 空出最低位,用来接收数据 */
        RecData <<= 1;
        if (SDA_Read)
            RecData |= 0x01;
        IIC_delay_us();

    }
    return RecData;
}

停止信号

在这里插入图片描述
停止信号是在SCL = 1,SDA从到低拉高(0=>1),由主机产生的停止信号

  1. SCL = 0
  2. SDA = 0
  3. SCL = 1
  4. SDA = 1 (触发停止)
/**
 * IIC停止信号
 * SCL 高电平
 * SDA 低->高
 **/
void IIC_Stop()
{
    /* 先拉低时钟,SDA输出模式 */
    SCL(0);
    SDA_OUT();
    IIC_delay_us();
    /* 拉低时钟和总线 */
    SCL(0);
    SDA(0);
    IIC_delay_us();
    /* 先拉高时钟 */
    SCL(1);
    IIC_delay_us();
    /* 拉高总线 等待停止ACK*/
    SDA(1);
    IIC_delay_us();
    SCL(0);
}

在这里插入图片描述

关于I2C通讯时间,电平要求


实现代码

myiic.h

#ifndef MYIIC_H
#define MYIIC_H

#include "main.h"

/**
 *  SDA   PB9
 *  SCL   PB8
  */
#define SDA_PORT GPIOB          
#define SCL_PORT GPIOB

#define SDA_PIN GPIO_PIN_9
#define SCL_PIN GPIO_PIN_8

/**
 * SDA SCL
 * 输出操作(追求效率可以直接操作寄存器)
  */
#define SCL(n)      (n==1?HAL_GPIO_WritePin(SCL_PORT,SCL_PIN,GPIO_PIN_SET):HAL_GPIO_WritePin(SCL_PORT,SCL_PIN,GPIO_PIN_RESET))
#define SDA(n)      (n==1?HAL_GPIO_WritePin(SDA_PORT,SDA_PIN,GPIO_PIN_SET):HAL_GPIO_WritePin(SDA_PORT,SDA_PIN,GPIO_PIN_RESET))

/**
 * SDA 
 * 读取操作
 **/
#define SDA_Read  HAL_GPIO_ReadPin(SDA_PORT,SDA_PIN)

/**
 * ACK 回应状态
 **/
#define ACK_OK 0      
#define ACK_ERROR 1

void IIC_Init();                        //IIC初始化
void IIC_Start();                       //IIC起始信号
void IIC_Stop();                        //IIC停止信号
uint8_t IIC_Wait_ACK();                //IIC主机等待从机响应ACK
void IIC_ACK();                         //IIC主机向从机发送ACK
void IIC_NACK();                        //IIC主机向从机发送NOACK
void IIC_SendOneByte(uint8_t SendData); //IIC主机发送
uint8_t IIC_ReadOneByte();              //IIC从机接收

#endif

myiic.c

#include "myiic.h"

void SDA_OUT();      //SDA输出模式
void SDA_INPUT();    //SDA输入模式
void IIC_TimeOut();  //IIC 超时
void IIC_delay_us(); //IIC delay

/* IIC GPIO PIN 初始化 */
void IIC_Init()
{
    /* 开GPIO模拟口的时钟 */
    __HAL_RCC_GPIOB_CLK_ENABLE();

    /* 初始化 SCL */
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = SCL_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(SCL_PORT, &GPIO_InitStruct);
    /* 初始化 SDA */
    SDA_OUT();
}

/**
 * IIC起始信号
 * SCL 高电平
 * SDA 高->低
 **/
void IIC_Start()
{
    SDA_OUT(); // SDA输出模式
    IIC_delay_us();
    /* 拉低时钟和总线 */
    SCL(0);
    SDA(0);
    IIC_delay_us();
    /* 拉高总线 和 时钟 */
    SDA(1);
    SCL(1);
    IIC_delay_us();
    /* 拉低总线 */
    SDA(0);
    IIC_delay_us();
    /* 拉低时钟 准备发送数据 */
    SCL(0);
}

/**
 * IIC停止信号
 * SCL 高电平
 * SDA 低->高
 **/
void IIC_Stop()
{
    /* 先拉低时钟,SDA输出模式 */
    SCL(0);
    SDA_OUT();
    IIC_delay_us();
    /* 拉低时钟和总线 */
    SCL(0);
    SDA(0);
    IIC_delay_us();
    /* 先拉高时钟 */
    SCL(1);
    IIC_delay_us();
    /* 拉高总线 等待停止ACK*/
    SDA(1);
    IIC_delay_us();
    SCL(0);
}

/**
 * 主机发送命令等待从机ACK回应
 * SCL 高电平
 * SDA输入模式,等待SDA响应
 * (SDA 高无效,低有效)
 **/
uint8_t IIC_Wait_ACK()
{
    /* 先拉低时钟,SDA改为输入模式 */
    SCL(0);
    SDA_INPUT();
    IIC_delay_us();
    /* 拉高SCL,准备接收数据 */
    SCL(1);
    /* 等待从机响应 */
    uint8_t timeout = 0;
    while (SDA_Read)
    {
        if (timeout > 10)
        {
            return ACK_ERROR;
        }
        timeout++;
        IIC_TimeOut();
    }
    SCL(0);
    return ACK_OK;
}

/**
 * 主机接收从机数据,回应从机ACK
 * SCL 高电平
 * SDA输出模式(SDA 高无效,低有效)
 **/
void IIC_ACK()
{
    /* 先拉低时钟,SDA输出模式 */
    SCL(0);
    SDA_OUT();
    /* 准备ACK 拉低SDA */
    SDA(0);
    IIC_delay_us();
    /* 拉高时钟 */
    SCL(1);
    IIC_delay_us();
    /* 再拉低时钟,结束第九脉冲 */
    SCL(0);
    IIC_delay_us();
}

/**
 * 主机接收从机数据,回应从机NOACK
 * SCL 高电平
 * SDA输出模式(SDA 高无效,低有效)
 **/
void IIC_NACK()
{
    /* 先拉低时钟,SDA输出模式 */
    SCL(0);
    SDA_OUT();
    /* 准备ACK 拉高SDA */
    SDA(1);
    IIC_delay_us();
    /* 拉高时钟 */
    SCL(1);
    IIC_delay_us();
    /* 再拉低时钟,结束第九脉冲 */
    SCL(0);
    IIC_delay_us();
}

/**
 * 主机发送一字节的数据
 * SCL 高电平 SDA输出模式(SDA 高为1,低为0)
 * SCL 低电平 准备数据
 **/
void IIC_SendOneByte(uint8_t SendData)
{
    /* 拉低时钟,SDA输出模式 */
    SCL(0);
    SDA_OUT();
    IIC_delay_us();
    /* 发送数据 */
    for (size_t i = 0; i < 8; i++)
    {
        /* 准备数据,从最高位发送 */
        if((SendData & 0x80)>>7)
            SDA(1);
        else
            SDA(0);
        SendData <<= 1;
        /* 拉高时钟,发送数据 */
        SCL(1);
        IIC_delay_us();
        /* 拉低时钟,准备数据 */
        SCL(0);
        
    }
    /* 拉低时钟,准备接收ACK*/
    SCL(0);
}

/**
 * 主机接收一字节的数据
 * SCL 高电平 SDA输出模式(SDA 高为1,低为0)
 **/
uint8_t IIC_ReadOneByte()
{
    uint8_t RecData = 0;
    /* 拉低时钟,SDA输入模式 */
    SCL(0);
    SDA_INPUT();
    IIC_delay_us();
    /* 接收数据 */
    for (size_t i = 0; i < 8; i++)
    {
        /* 拉低时钟,准备接收下一位 */
        SCL(0);
        IIC_delay_us();
        /* 拉高时钟,从最高位接收 */
        SCL(1);
        /* 空出最低位,用来接收数据 */
        RecData <<= 1;
        if (SDA_Read)
            RecData |= 0x01;
        IIC_delay_us();

    }
    return RecData;
}

//SDA输出模式
void SDA_OUT()
{
    /* 初始化 SDA */
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = SDA_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct);
}

//SDA输入模式
void SDA_INPUT()
{
    /* 初始化 SDA  */
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = SDA_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    //上拉是因为通常我们只是用来等待从机拉低SDA,产生ACK
    //当然也可以靠上拉电阻拉高总线
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(SDA_PORT, &GPIO_InitStruct);
}

void IIC_delay_us()
{
    HAL_Delay_us(3);
}

void IIC_TimeOut()
{
    HAL_Delay_us(2);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值