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

news/2024/10/5 18:48:13 标签: 单片机, stm32, 嵌入式硬件, 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

http://www.niftyadmin.cn/n/5691155.html

相关文章

Java编码方式:Base64编码与解码

1、Base64 算法介绍 Base64 是一种基于 64 个可打印字符来表示二进制数据的表示方法。它主要用于在不支持二进制数据的场合&#xff08;如电子邮件、URL、文件系统名等&#xff09;传输二进制数据。严格来说 Base64 并不是一种加密/解密算法&#xff0c;而是一种编码方式。Bas…

CSS相关属性和显示模式

1. CSS相关属性 1.1 常见控制属性 属性名 作用 案例 width 宽度 width : 100px; height 高度 height : 100px; background-color 背景色 background-color : red; 1.2 文字控制属性 属性名 作用 案例 font-size 字体大小 font-size:30px; font-weight 字体…

算法(食物链)

240. 食物链 题目 动物王国中有三类动物 A,B,C&#x1d434;,&#x1d435;,&#x1d436;&#xff0c;这三类动物的食物链构成了有趣的环形。 A&#x1d434; 吃 B&#x1d435;&#xff0c;B&#x1d435; 吃 C&#x1d436;&#xff0c;C&#x1d436; 吃 A&#x1d434;。…

数据科学基础复习(简)

可视化、数据可视化 在狭义上&#xff0c;数据可视化是与信息可视化&#xff0c;科学可视化和可视分析学平行的概念&#xff0c;而在广义上数据可视化可以包含这3类可视化技术。 数据科学的主要任务 数据科学研究目的与任务 大数据及其运动规律的揭示从数据到智慧的转化数据…

Unity如何用代码让Project窗口聚焦到指定路径/文件/文件夹

前言&#xff1a; 当项目文件夹 路径越来越多越来越复杂越来越深的时候&#xff0c;要切换到某个目录总要点好多次&#xff1b;而且经常会有在几个频繁访问的目录之间跳来跳去的需求场景。 此时&#xff0c;可以考虑在顶部菜单栏加上快捷访问按钮&#xff0c;能够快速地在Proje…

DenseNet算法:口腔癌识别

本文为为&#x1f517;365天深度学习训练营内部文章 原作者&#xff1a;K同学啊 一 DenseNet算法结构 其基本思路与ResNet一致&#xff0c;但是它建立的是前面所有层和后面层的密集连接&#xff0c;它的另一大特色是通过特征在channel上的连接来实现特征重用。 二 设计理念 三…

Linux 生产者消费者模型

前言 生产者消费者模型&#xff08;CP模型&#xff09;是一种十分经典的设计&#xff0c;常常用于多执行流的并发问题中&#xff01;很多书上都说他很高效&#xff0c;但高效体现在哪里并没有说明&#xff01;本博客将详解&#xff01; 目录 前言 一、生产者消费者模型 1.…

SM2无证书及隐式证书公钥机制签名和加密过程详解(三)

在对隐式证书ASN.1模板和生成过程进行说明后&#xff08;SM2无证书及隐式证书公钥机制签名和加密过程详解(二)-CSDN博客&#xff09;&#xff0c;进一步介绍用于隐式证书编码的COER。 &#xff08;3&#xff09;COER编码 ASN.1模板可采用多种编码形式&#xff0c;如比较熟悉的…