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

ops/2024/10/9 10:53:23/

一、软件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高电平期间读取SDAMyI2C_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高电平期间读取SDAif (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}	//读取SDA数据,并存储到Byte变量//当SDA为1时,置变量指定位为1,当SDA为0时,不做处理,指定位为默认的初值0MyI2C_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高电平期间读取SDAAckBit = 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高电平期间读取SDAMyI2C_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高电平期间读取SDAif (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}	//读取SDA数据,并存储到Byte变量//当SDA为1时,置变量指定位为1,当SDA为0时,不做处理,指定位为默认的初值0MyI2C_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高电平期间读取SDAAckBit = MyI2C_R_SDA();					//将应答位存储到变量里MyI2C_W_SCL(0);							//拉低SCL,开始下一个时序模块return AckBit;							//返回定义应答位变量
}

myI2C.h

#ifndef __MYI2C_H
#define __MYI2C_Hvoid 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.ppmy.cn/ops/123141.html

相关文章

第二章:处理机管理:第二节——处理机调度

&#x1f308;个人主页&#xff1a;小新_- &#x1f388;个人座右铭&#xff1a;“成功者不是从不失败的人&#xff0c;而是从不放弃的人&#xff01;”&#x1f388; &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd; &#x1f3c6;所属专栏&#xff1…

HarmonyOS应用六之应用程序进阶二

目录&#xff1a; 一、进度条通知二、闹钟提醒2.1、在module.json5配置文件中开启权限2.2、导入后台代理提醒reminderAgentManager模块&#xff0c;将此模块命名为reminderAgentManager2.3、如果是新增提醒&#xff0c;实现步骤如下&#xff1a; 3、Native C交互4、第三方库的基…

序列化与反序列化基础及反序列化漏洞(附案例)

参考文章&#xff1a; [web安全原理]PHP反序列化漏洞 - 笑花大王 - 博客园 (cnblogs.com) 一、概念 为了能有效的存储数据而不丢失数据的类型和内容&#xff0c;经常需要通过序列化对数据进行处理&#xff0c;将数据进行序列化后&#xff0c;会生成一个字符串&#xff0c;字符…

王春城 | 如何处理班组管理中出现的人际冲突?

在日常工作中&#xff0c;由于性格差异、工作压力、沟通不畅等多种原因&#xff0c;班组管理中难免会出现人际冲突。这些冲突若不能得到妥善处理&#xff0c;不仅会影响团队的凝聚力&#xff0c;还可能阻碍工作进度&#xff0c;降低整体绩效。因此&#xff0c;掌握有效的策略来…

C++头文件

C中的头文件&#xff08;header file&#xff09;通常用于声明函数、类、变量或其他代码组件&#xff0c;以便在不同的源文件之间共享这些声明。头文件的扩展名通常是.h或者.hpp。在C项目中&#xff0c;头文件的主要作用是将接口与实现分离&#xff0c;从而使代码更加模块化和易…

音频剪辑在线工具 —— 让声音更精彩

你是否曾梦想过拥有自己的声音创作空间&#xff0c;却苦于复杂的音频编辑软件&#xff1f;接下来&#xff0c;让我们一同揭开这些音频剪辑在线工具的神秘面纱&#xff0c;看看它们如何帮助你实现从录音到发布的无缝衔接。 1.福昕音频剪辑 链接直达>>https://www.foxits…

各省常住人口及人口密度面板数据(2000-2022年)

常住人口指在某地区居住超过一定时间&#xff08;通常为半年以上&#xff09;的人口&#xff0c;而人口密度则指每平方千米或每公顷内的常住人口数。数据集的主要指标包括&#xff1a; 省份年份常住人口&#xff08;万人&#xff09;人口密度&#xff08;人/平方公里&#xff…

系统架构设计师教程 第12章 12.2 信息系统架构 笔记

12.2 信息系统架构 ★★★☆☆ 12.2.1 架构风格 信息系统架构风格是描述某一特定应用领域中系统组织方式的惯用模式。架构风格定义了一个系统家族&#xff0c;即一个架构定义一个词汇表和一组约束。 词汇表中包含一些构件和连接件类型&#xff0c; 约束指出系统是如何将这些…