软件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
相关推荐
网易独家音乐人Mike Zhou3 小时前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
zy张起灵3 小时前
48v72v-100v转12v 10A大功率转换电源方案CSM3100SK
经验分享·嵌入式硬件·硬件工程
PegasusYu6 小时前
STM32CUBEIDE FreeRTOS操作教程(九):eventgroup事件标志组
stm32·教程·rtos·stm32cubeide·free-rtos·eventgroup·时间标志组
lantiandianzi10 小时前
基于单片机的多功能跑步机控制系统
单片机·嵌入式硬件
文弱书生65610 小时前
输出比较简介
stm32
哔哥哔特商务网10 小时前
高集成的MCU方案已成电机应用趋势?
单片机·嵌入式硬件
跟着杰哥学嵌入式10 小时前
单片机进阶硬件部分_day2_项目实践
单片机·嵌入式硬件
电子科技圈11 小时前
IAR与鸿轩科技共同推进汽车未来
科技·嵌入式硬件·mcu·汽车
东芝、铠侠总代1361006839312 小时前
浅谈TLP184小型平面光耦
单片机·嵌入式硬件·物联网·平面
lantiandianzi12 小时前
基于单片机中医药柜管理系统的设计
单片机·嵌入式硬件