自学内容网 自学内容网

软件I2C-基于江科大源码进行的原理解析和改造升级

一、软件I2C的作用

软件I2C可以不用特定的端口,可以在I2C外设不够的时候使用,虽然没有硬件I2C的速度快,但是在一些要求低的工作中不足为谈

数据有效性:


I2C总线进行数据传送时,时钟信号为高电平期间,数据线上的数据必须保持稳定,只有在时钟线上的信号为低电平期间,数据线上的高电平或低电平状态才允许变化。
即:数据在SCL的上升沿到来之前就需准备好。并在在下降沿到来之
前必须稳定。

改变SCL和SDA线的状态

我选用的是PB0和PB1,给大家用非I2C外设GPIO口实践一下

/**
  * 函    数:I2C写SCL引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SCL的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平
  */
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_0, (BitAction)BitValue);//根据BitValue,设置SCL引脚的电平
Delay_us(10);//延时10us,防止时序频率超过要求
}

/**
  * 函    数:I2C写SDA引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SDA的电平,范围0~0xFF
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SDA为低电平,当BitValue非0时,需要置SDA为高电平
  */
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_1, (BitAction)BitValue);//根据BitValue,设置SDA引脚的电平,BitValue要实现非0即1的特性
Delay_us(10);//延时10us,防止时序频率超过要求
}

读SDA数据线状态函数

/**
  * 函    数:I2C读SDA引脚电平
  * 参    数:无
  * 返 回 值:协议层需要得到的当前SDA的电平,范围0~1
  * 注意事项:此函数需要用户实现内容,当前SDA为低电平时,返回0,当前SDA为高电平时,返回1
  */
uint8_t MyI2C_R_SDA(void)
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1);//读取SDA电平
Delay_us(10);//延时10us,防止时序频率超过要求
return BitValue;//返回SDA电平
}

GPIO初始化

这里最后把两条线的电平都置1了,这是将I2C保持空闲状态了

/**
  * 函    数:I2C初始化
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,实现SCL和SDA引脚的初始化
  */
void MyI2C_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//开启GPIOB的时钟

/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);//将PB0和PB1引脚初始化为开漏输出

/*设置默认电平*/
GPIO_SetBits(GPIOB, GPIO_Pin_0 | GPIO_Pin_1);//设置PB0和PB1引脚初始化后默认为高电平(释放总线状态)
}

起始信号与停止信号

首先要保证,一定是SCL保持高电平期间,发生的跳变,才会被视为起始或者停止信号

/**
  * 函    数:I2C起始
  * 参    数:无
  * 返 回 值:无
  */
void MyI2C_Start(void)
{
MyI2C_W_SDA(1);//释放SDA,确保SDA为高电平
MyI2C_W_SCL(1);//释放SCL,确保SCL为高电平
MyI2C_W_SDA(0);//在SCL高电平期间,拉低SDA,产生起始信号
MyI2C_W_SCL(0);//起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
}
/**
  * 函    数:I2C终止
  * 参    数:无
  * 返 回 值:无
  */
void MyI2C_Stop(void)
{
MyI2C_W_SDA(0);//拉低SDA,确保SDA为低电平
MyI2C_W_SCL(1);//释放SCL,使SCL呈现高电平
MyI2C_W_SDA(1);//在SCL高电平期间,释放SDA,产生终止信号
}

I2C发送字节

发送字节的时候,时钟线为高电平期间,SDA数据线不可以发生变化,要保持稳定

/**
  * 函    数:I2C发送一个字节
  * 参    数:Byte 要发送的一个字节数据,范围:0x00~0xFF
  * 返 回 值:无
  */
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i ++)//循环8次,主机依次发送数据的每一位
{
MyI2C_W_SDA(Byte & (0x80 >> i));//使用掩码的方式取出Byte的指定一位数据并写入到SDA线
MyI2C_W_SCL(1);//释放SCL,从机在SCL高电平期间读取SDA
MyI2C_W_SCL(0);//拉低SCL,主机开始发送下一位数据
}
}

代码详解

  1. 循环初始化
for (i = 0; i < 8; i++)

这个循环将运行8次,对应于一个字节中的8位。

  1. 取出数据位
MyI2C_W_SDA(Byte & (0x80 >> i));
    • 0x80 是二进制 10000000,表示最高位是1。
    • 0x80 >> i0x80 右移 i 位,得到一个新的掩码,这个掩码只有一个位是1,其余位都是0。
    • Byte & (0x80 >> i) 通过与操作来检查 Byte 的第 i 位是否为1。如果 Byte 的第 i 位是1,则结果为1;否则为0。
    • MyI2C_W_SDA(...) 函数根据上述结果设置 SDA 线的状态(1或0)。
  1. 拉高SCL
MyI2C_W_SCL(1);
    • 将 SCL 线拉高。此时,从机会读取 SDA 线上的状态。如果 SDA 为高电平,则读取到的数据位是1;如果 SDA 为低电平,则读取到的数据位是0。
  1. 拉低SCL
MyI2C_W_SCL(0);
    • 将 SCL 线拉低,表示当前数据位传输完成。此时可以安全地改变 SDA 线的状态,以准备传输下一个数据位。

I2C接收一个字节

/**
  * 函    数:I2C接收一个字节
  * 参    数:无
  * 返 回 值:接收到的一个字节数据,范围:0x00~0xFF
  */
uint8_t MyI2C_ReceiveByte(void)
{
uint8_t i, Byte = 0x00;//定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
MyI2C_W_SDA(1);//接收前,主机先确保释放SDA,避免干扰从机的数据发送
for (i = 0; i < 8; i ++)//循环8次,主机依次接收数据的每一位
{
MyI2C_W_SCL(1);//释放SCL,主机机在SCL高电平期间读取SDA
if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}//读取SDA数据,并存储到Byte变量
//当SDA为1时,置变量指定位为1,当SDA为0时,不做处理,指定位为默认的初值0
MyI2C_W_SCL(0);//拉低SCL,从机在SCL低电平期间写入SDA
}
return Byte;//返回接收到的一个字节数据
}

代码详解

  1. 变量初始化
uint8_t i, Byte = 0x00;
    • i 是循环计数器。
    • Byte 用于存储接收到的数据,并初始化为 0x00。这是因为我们需要一个干净的起点来构建最终的字节。
  1. 释放SDA
MyI2C_W_SDA(1);
    • 在开始接收数据之前,主机将 SDA 线拉高。这是为了确保 SDA 线处于高阻态,不会干扰从机的数据发送。
  1. 循环接收每一位
for (i = 0; i < 8; i++)
    • 这个循环会运行8次,对应于一个字节中的8位。
  1. 拉高SCL
MyI2C_W_SCL(1);
    • 将 SCL 线拉高。此时,从机会将数据写入 SDA 线上。
  1. 读取SDA并更新Byte
if (MyI2C_R_SDA() == 1) {  // 读取SDA数据
    Byte |= (0x80 >> i);  // 如果SDA为1,则将Byte的第i位置1
}
    • MyI2C_R_SDA() 函数读取 SDA 线的状态。
    • 如果 SDA 为高电平(逻辑1),则使用按位或操作将 Byte 的第 i 位置1。
    • 如果 SDA 为低电平(逻辑0),则 Byte 的第 i 位保持不变(即保持为0,因为 Byte 已经被初始化为 0x00)。
  1. 拉低SCL
MyI2C_W_SCL(0);
    • 将 SCL 线拉低,表示当前数据位已经接收完成。此时可以安全地改变 SDA 线的状态,以准备接收下一个数据位。
  1. 返回接收到的数据
return Byte;
    • 在所有8位都接收完成后,函数返回最终构建的字节 Byte

发送应答位

应答位一般会在发送数据的第八位后的第九个时钟期间产生

代码解析

1. 设置SDA线
MyI2C_W_SDA(AckBit);
  • MyI2C_W_SDA 是一个假设的函数,用于设置 SDA 线的状态。
  • AckBit 是要发送的应答位,可以是0或1。
    • 如果 AckBit 为0,表示应答(ACK),SDA 线会被拉低。
    • 如果 AckBit 为1,表示非应答(NACK),SDA 线会保持高电平。
2. 拉高SCL线
MyI2C_W_SCL(1);
  • MyI2C_W_SCL 是一个假设的函数,用于设置 SCL 线的状态。
  • 将 SCL 线拉高,使从机能够在 SCL 高电平期间读取 SDA 线上的状态。
  • 在 I2C 协议中,接收方(从机)会在 SCL 的上升沿(即 SCL 从低电平变为高电平)时读取 SDA 线上的状态。
3. 拉低SCL线
MyI2C_W_SCL(0);
  • 将 SCL 线拉低,结束当前的应答位传输,并准备进行下一个时序模块。
  • 这一步确保了 SCL 线在下一次数据传输之前处于低电平状态,以便正确地进行后续的操作。

工作原理

在 I2C 通信中,每个字节的数据传输完成后,接收方需要发送一个应答位来确认数据是否被成功接收。这个应答位是在 SCL 为高电平时通过 SDA 线传输的。

  1. 应答位(ACK)
    • 当接收方成功接收到一个字节后,它会在下一个 SCL 高电平期间将 SDA 拉低,表示应答。
    • 应答位为0(低电平)。
  1. 非应答位(NACK)
    • 如果接收方没有准备好接收更多数据,或者检测到错误,它会在下一个 SCL 高电平期间保持 SDA 为高电平,表示非应答。
    • 非应答位为1(高电平)。

接收应答位

/**
  * 函    数:I2C接收应答位
  * 参    数:无
  * 返 回 值:接收到的应答位,范围:0~1,0表示应答,1表示非应答
  */
uint8_t MyI2C_ReceiveAck(void)
{
uint8_t AckBit;//定义应答位变量
MyI2C_W_SDA(1);//接收前,主机先确保释放SDA,避免干扰从机的数据发送
MyI2C_W_SCL(1);//释放SCL,主机机在SCL高电平期间读取SDA
AckBit = MyI2C_R_SDA();//将应答位存储到变量里
MyI2C_W_SCL(0);//拉低SCL,开始下一个时序模块
return AckBit;//返回定义应答位变量
}

代码解析

1. 定义应答位变量
uint8_t AckBit;
  • AckBit 用于存储接收到的应答位,初始化为未定义状态。
2. 释放SDA线
MyI2C_W_SDA(1);
  • MyI2C_W_SDA 是一个假设的函数,用于设置 SDA 线的状态。
  • 在接收应答位之前,主机将 SDA 线拉高,以避免干扰从机的数据发送。
  • 这一步确保了 SDA 线处于高阻态,允许从机控制 SDA 线的状态。
3. 拉高SCL线
MyI2C_W_SCL(1);
  • MyI2C_W_SCL 是一个假设的函数,用于设置 SCL 线的状态。
  • 将 SCL 线拉高,使从机能够在 SCL 高电平期间将应答位写入 SDA 线。
  • 在 I2C 协议中,从机会在 SCL 的上升沿(即 SCL 从低电平变为高电平)时将应答位放到 SDA 线上。
4. 读取SDA线
AckBit = MyI2C_R_SDA();
  • MyI2C_R_SDA 是一个假设的函数,用于读取 SDA 线的状态。
  • 读取 SDA 线上的状态并存储到 AckBit 变量中。
  • 如果 SDA 为低电平(逻辑0),则表示应答(ACK);如果 SDA 为高电平(逻辑1),则表示非应答(NACK)。
5. 拉低SCL线
MyI2C_W_SCL(0);
  • 将 SCL 线拉低,结束当前的应答位接收,并准备进行下一个时序模块。
  • 这一步确保了 SCL 线在下一次数据传输之前处于低电平状态,以便正确地进行后续的操作。
6. 返回应答位
return AckBit;
  • 返回接收到的应答位 AckBit

工作原理

在 I2C 通信中,每个字节的数据传输完成后,从机需要发送一个应答位来确认数据是否被成功接收。这个应答位是在 SCL 为高电平时通过 SDA 线传输的。

  1. 应答位(ACK)
    • 当从机成功接收到一个字节后,它会在下一个 SCL 高电平期间将 SDA 拉低,表示应答。
    • 应答位为0(低电平)。
  1. 非应答位(NACK)
    • 如果从机没有准备好接收更多数据,或者检测到错误,它会在下一个 SCL 高电平期间保持 SDA 为高电平,表示非应答。
    • 非应答位为1(高电平)。

源码

myI2C.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

/*引脚配置层*/

/**
  * 函    数:I2C写SCL引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SCL的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平
  */
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_0, (BitAction)BitValue);//根据BitValue,设置SCL引脚的电平
Delay_us(10);//延时10us,防止时序频率超过要求
}

/**
  * 函    数:I2C写SDA引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SDA的电平,范围0~0xFF
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SDA为低电平,当BitValue非0时,需要置SDA为高电平
  */
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_1, (BitAction)BitValue);//根据BitValue,设置SDA引脚的电平,BitValue要实现非0即1的特性
Delay_us(10);//延时10us,防止时序频率超过要求
}

/**
  * 函    数:I2C读SDA引脚电平
  * 参    数:无
  * 返 回 值:协议层需要得到的当前SDA的电平,范围0~1
  * 注意事项:此函数需要用户实现内容,当前SDA为低电平时,返回0,当前SDA为高电平时,返回1
  */
uint8_t MyI2C_R_SDA(void)
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1);//读取SDA电平
Delay_us(10);//延时10us,防止时序频率超过要求
return BitValue;//返回SDA电平
}

/**
  * 函    数:I2C初始化
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,实现SCL和SDA引脚的初始化
  */
void MyI2C_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//开启GPIOB的时钟

/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);//将PB10和PB11引脚初始化为开漏输出

/*设置默认电平*/
GPIO_SetBits(GPIOB, GPIO_Pin_0 | GPIO_Pin_1);//设置PB10和PB11引脚初始化后默认为高电平(释放总线状态)
}

/*协议层*/

/**
  * 函    数:I2C起始
  * 参    数:无
  * 返 回 值:无
  */
void MyI2C_Start(void)
{
MyI2C_W_SDA(1);//释放SDA,确保SDA为高电平
MyI2C_W_SCL(1);//释放SCL,确保SCL为高电平
MyI2C_W_SDA(0);//在SCL高电平期间,拉低SDA,产生起始信号
MyI2C_W_SCL(0);//起始后把SCL也拉低,即为了占用总线,也为了方便总线时序的拼接
}

/**
  * 函    数:I2C终止
  * 参    数:无
  * 返 回 值:无
  */
void MyI2C_Stop(void)
{
MyI2C_W_SDA(0);//拉低SDA,确保SDA为低电平
MyI2C_W_SCL(1);//释放SCL,使SCL呈现高电平
MyI2C_W_SDA(1);//在SCL高电平期间,释放SDA,产生终止信号
}

/**
  * 函    数:I2C发送一个字节
  * 参    数:Byte 要发送的一个字节数据,范围:0x00~0xFF
  * 返 回 值:无
  */
void MyI2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i ++)//循环8次,主机依次发送数据的每一位
{
MyI2C_W_SDA(Byte & (0x80 >> i));//使用掩码的方式取出Byte的指定一位数据并写入到SDA线
MyI2C_W_SCL(1);//释放SCL,从机在SCL高电平期间读取SDA
MyI2C_W_SCL(0);//拉低SCL,主机开始发送下一位数据
}
}

/**
  * 函    数:I2C接收一个字节
  * 参    数:无
  * 返 回 值:接收到的一个字节数据,范围:0x00~0xFF
  */
uint8_t MyI2C_ReceiveByte(void)
{
uint8_t i, Byte = 0x00;//定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到
MyI2C_W_SDA(1);//接收前,主机先确保释放SDA,避免干扰从机的数据发送
for (i = 0; i < 8; i ++)//循环8次,主机依次接收数据的每一位
{
MyI2C_W_SCL(1);//释放SCL,主机机在SCL高电平期间读取SDA
if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}//读取SDA数据,并存储到Byte变量
//当SDA为1时,置变量指定位为1,当SDA为0时,不做处理,指定位为默认的初值0
MyI2C_W_SCL(0);//拉低SCL,从机在SCL低电平期间写入SDA
}
return Byte;//返回接收到的一个字节数据
}

/**
  * 函    数:I2C发送应答位
  * 参    数:Byte 要发送的应答位,范围:0~1,0表示应答,1表示非应答
  * 返 回 值:无
  */
void MyI2C_SendAck(uint8_t AckBit)
{
MyI2C_W_SDA(AckBit);//主机把应答位数据放到SDA线
MyI2C_W_SCL(1);//释放SCL,从机在SCL高电平期间,读取应答位
MyI2C_W_SCL(0);//拉低SCL,开始下一个时序模块
}

/**
  * 函    数:I2C接收应答位
  * 参    数:无
  * 返 回 值:接收到的应答位,范围:0~1,0表示应答,1表示非应答
  */
uint8_t MyI2C_ReceiveAck(void)
{
uint8_t AckBit;//定义应答位变量
MyI2C_W_SDA(1);//接收前,主机先确保释放SDA,避免干扰从机的数据发送
MyI2C_W_SCL(1);//释放SCL,主机机在SCL高电平期间读取SDA
AckBit = MyI2C_R_SDA();//将应答位存储到变量里
MyI2C_W_SCL(0);//拉低SCL,开始下一个时序模块
return AckBit;//返回定义应答位变量
}

myI2C.h

#ifndef __MYI2C_H
#define __MYI2C_H

void MyI2C_Init(void);
void MyI2C_Start(void);
void MyI2C_Stop(void);
void MyI2C_SendByte(uint8_t Byte);
uint8_t MyI2C_ReceiveByte(void);
void MyI2C_SendAck(uint8_t AckBit);
uint8_t MyI2C_ReceiveAck(void);

#endif

原文地址:https://blog.csdn.net/weixin_54210362/article/details/142714092

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!