声明:本文代码来自bilibili江科大,侵权可私信我删文
创作本文全凭个人与大家学习
/*引脚配置层*/
/**
* 函 数:I2C写SCL引脚电平
* 参 数:BitValue 协议层传入的当前需要写入SCL的电平,范围0~1
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平
*/
void MyI2C_W_SCL(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue); //根据BitValue,设置SCL引脚的电平
Delay_us(10); //延时10us,防止时序频率超过要求
}
void MyI2C_W_SCL(uint8_t BitValue)
:定义了一个函数MyI2C_W_SCL
,它接受一个uint8_t
类型的参数BitValue
。这个参数的值范围是0到1,其中0表示低电平,1表示高电平。GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
:这一行代码使用GPIO_WriteBit
函数来设置SCL引脚(在STM32中通常映射到GPIOB的第10个引脚)的电平。BitAction
是一个枚举类型,用于控制GPIO引脚的输出状态。如果BitValue
为0,SCL引脚将被设置为低电平;如果BitValue
为1,SCL引脚将被设置为高电平。Delay_us(10);
:在设置SCL引脚电平后,代码会执行一个10微秒的延时。这个延时是为了确保I2C通信的时序要求得到满足,防止由于过快的信号切换而导致的通信错误。I2C协议对信号的上升沿和下降沿之间的时间间隔有严格的要求,以保证数据的正确传输。通过这种方式,MyI2C_W_SCL
函数允许你在软件层面上精确控制SCL信号线的电平,这对于实现I2C通信的手动控制非常重要,尤其是在没有使用硬件I2C控制器的情况下,或者当需要更细粒度的控制时。然而,这种方法的精度和可靠性可能不如使用硬件I2C模块,因为软件延时可能受到CPU负载和其他因素的影响。
/**
* 函 数:I2C写SDA引脚电平
* 参 数:BitValue 协议层传入的当前需要写入SDA的电平,范围0~0xFF
* 返 回 值:无
* 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SDA为低电平,当BitValue非0时,需要置SDA为高电平
*/
void MyI2C_W_SDA(uint8_t BitValue)
{
GPIO_WriteBit(GPIOB, GPIO_Pin_11, (BitAction)BitValue); //根据BitValue,设置SDA引脚的电平,BitValue要实现非0即1的特性
Delay_us(10); //延时10us,防止时序频率超过要求
}
void MyI2C_W_SCL(uint8_t BitValue)
:定义了一个函数MyI2C_W_SCL
,它接受一个uint8_t
类型的参数BitValue
。这个参数的值范围是0到1,其中0表示低电平,1表示高电平。GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
:这一行代码使用GPIO_WriteBit
函数来设置SCL引脚(在STM32中通常映射到GPIOB的第10个引脚)的电平。BitAction
是一个枚举类型,用于控制GPIO引脚的输出状态。如果BitValue
为0,SCL引脚将被设置为低电平;如果BitValue
为1,SCL引脚将被设置为高电平。Delay_us(10);
:在设置SCL引脚电平后,代码会执行一个10微秒的延时。这个延时是为了确保I2C通信的时序要求得到满足,防止由于过快的信号切换而导致的通信错误。I2C协议对信号的上升沿和下降沿之间的时间间隔有严格的要求,以保证数据的正确传输。通过这种方式,MyI2C_W_SCL
函数允许你在软件层面上精确控制SCL信号线的电平,这对于实现I2C通信的手动控制非常重要,尤其是在没有使用硬件I2C控制器的情况下,或者当需要更细粒度的控制时。然而,这种方法的精度和可靠性可能不如使用硬件I2C模块,因为软件延时可能受到CPU负载和其他因素的影响。
/**
* 函 数:I2C读SDA引脚电平
* 参 数:无
* 返 回 值:协议层需要得到的当前SDA的电平,范围0~1
* 注意事项:此函数需要用户实现内容,当前SDA为低电平时,返回0,当前SDA为高电平时,返回1
*/
uint8_t MyI2C_R_SDA(void)
{
uint8_t BitValue;
BitValue = GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11); //读取SDA电平
Delay_us(10); //延时10us,防止时序频率超过要求
return BitValue; //返回SDA电平
}
void MyI2C_W_SCL(uint8_t BitValue)
:定义了一个函数MyI2C_W_SCL
,它接受一个uint8_t
类型的参数BitValue
。这个参数的值范围是0到1,其中0表示低电平,1表示高电平。GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);
:这一行代码使用GPIO_WriteBit
函数来设置SCL引脚(在STM32中通常映射到GPIOB的第10个引脚)的电平。BitAction
是一个枚举类型,用于控制GPIO引脚的输出状态。如果BitValue
为0,SCL引脚将被设置为低电平;如果BitValue
为1,SCL引脚将被设置为高电平。Delay_us(10);
:在设置SCL引脚电平后,代码会执行一个10微秒的延时。这个延时是为了确保I2C通信的时序要求得到满足,防止由于过快的信号切换而导致的通信错误。I2C协议对信号的上升沿和下降沿之间的时间间隔有严格的要求,以保证数据的正确传输。通过这种方式,MyI2C_W_SCL
函数允许你在软件层面上精确控制SCL信号线的电平,这对于实现I2C通信的手动控制非常重要,尤其是在没有使用硬件I2C控制器的情况下,或者当需要更细粒度的控制时。然而,这种方法的精度和可靠性可能不如使用硬件I2C模块,因为软件延时可能受到CPU负载和其他因素的影响。
/**
* 函 数: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_10 | GPIO_Pin_11;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); //将PB10和PB11引脚初始化为开漏输出
/*设置默认电平*/
GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11); //设置PB10和PB11引脚初始化后默认为高电平(释放总线状态)
}
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
:这一行代码用于使能GPIOB的时钟。在STM32中,每个外设都需要其时钟被使能才能工作。RCC_APB2PeriphClockCmd
函数用于控制APB2总线上的外设时钟。GPIO_InitTypeDef
类型的结构体GPIO_InitStructure
,用于配置GPIO引脚的模式、速度和所选引脚。GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD;
:设置GPIO引脚的工作模式为开漏输出(Open Drain)。在I2C通信中,SCL和SDA引脚通常配置为开漏输出,以支持总线上的多个设备同时连接而不相互干扰。GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 | GPIO_Pin_11;
:选择要配置的GPIO引脚,这里是PB10(SCL)和PB11(SDA)。GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
:设置GPIO引脚的速度为50MHz,这通常是为了匹配微控制器的最大GPIO速度能力。GPIO_Init(GPIOB, &GPIO_InitStructure);
:调用GPIO_Init
函数,使用之前定义的GPIO_InitStructure
结构体参数初始化GPIOB的引脚。GPIO_SetBits(GPIOB, GPIO_Pin_10 | GPIO_Pin_11);
:在初始化后,将PB10和PB11引脚设置为高电平。在I2C总线空闲时,SCL和SDA线应保持高电平状态,通常通过上拉电阻实现。设置引脚为高电平有助于确保总线在未被使用时处于正确的状态。通过这段代码,你可以在STM32微控制器上初始化用于I2C通信的SCL和SDA引脚,为后续的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也拉低,即为了占用总线,也为了方便总线时序的拼接
}
MyI2C_W_SDA(1);
:首先,确保SDA(数据线)处于高电平状态。这通常意味着释放SDA引脚的输出,让任何外部上拉电阻将其拉高,或者直接设置SDA引脚为输出高电平。MyI2C_W_SCL(1);
:接着,确保SCL(时钟线)也处于高电平状态。同样地,这可以是释放SCL引脚让其被上拉电阻拉高,或者是直接设置SCL引脚为输出高电平。MyI2C_W_SDA(0);
:在SCL保持高电平的情况下,将SDA线拉低。这是开始信号的关键部分,因为在SCL高电平期间SDA从高变低表示开始信号的开始。MyI2C_W_SCL(0);
:最后,在SDA保持低电平的同时,将SCL线也拉低。此时,I2C总线上的所有设备都识别到了开始信号,准备接收接下来的数据。拉低SCL也有助于确保在后续的数据传输中,SDA线的状态不会在时钟的高电平期间发生变化,这符合I2C的数据传输规则。开始信号在最后的时候为什么要把SCL信号置0
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,主机开始发送下一位数据
}
}
void MyI2C_SendByte(uint8_t Byte)
定义了一个函数,它接受一个uint8_t
类型的参数Byte
,这个参数是要通过I2C总线发送的字节数据。for (i = 0; i < 8; i ++)
:循环8次,因为一个字节有8位。每次循环发送一位数据。MyI2C_W_SDA(Byte & (0x80 >> i))
:这一行使用位运算符&
和>>
来从Byte
中提取当前位的数据。0x80
是一个掩码,表示二进制的最高位(第8位)。通过右移i
次,将掩码对准当前需要发送的位。然后与Byte
做位与操作,如果该位是1,结果就是非零值;如果是0,结果就是0。这个结果被传递给MyI2C_W_SDA
函数,该函数负责将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; //返回接收到的一个字节数据
}
uint8_t MyI2C_ReceiveByte(void)
定义了一个函数,它没有输入参数,返回一个uint8_t
类型的值,即接收的一个字节数据。uint8_t i, Byte = 0x00;
:定义了一个循环计数器i
和一个接收数据的变量Byte
,并将其初始化为0x00。这是因为我们将在循环中逐步填充这个字节的每一位。MyI2C_W_SDA(1);
:在接收数据之前,确保SDA线被设置为高电平。这是为了释放SDA线,让从机有机会将数据发送到总线上。for (i = 0; i < 8; i ++)
:循环8次,每次接收数据的一位。MyI2C_W_SCL(1);
:将SCL线设置为高电平。在SCL高电平期间,SDA线上的数据保持稳定,主机可以从SDA线上读取数据。if (MyI2C_R_SDA() == 1){Byte |= (0x80 >> i);}
:读取SDA线上的数据。如果读取到的数据为1,则使用位或运算符|
将Byte
中对应位置的位设置为1。0x80 >> i
是一个位掩码,它随着循环逐渐右移,每次对准一个不同的位。MyI2C_W_SCL(0);
:将SCL线拉低,这标志着当前位的读取完成,允许从机在SCL低电平时改变SDA线上的数据,准备发送下一位。return Byte;
:函数返回接收到的完整字节数据。这个函数按照I2C协议的要求,逐位读取数据,并在SCL高电平期间稳定读取SDA线上的数据,确保了数据的正确接收。
/**
* 函 数: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,开始下一个时序模块
}
void MyI2C_SendAck(uint8_t AckBit)
定义了一个函数,它接受一个uint8_t
类型的参数AckBit
,这个参数可以是0或1,分别代表应答信号(ACK)或非应答信号(NACK)。MyI2C_W_SDA(AckBit);
:将SDA线设置为AckBit
所指示的电平。如果AckBit
是0,那么SDA线将被设置为低电平,表示应答(ACK)。如果AckBit
是1,那么SDA线将被设置为高电平,表示非应答(NACK)。MyI2C_W_SCL(1);
:将SCL线设置为高电平。在SCL高电平期间,SDA线上的数据必须保持稳定,以便从机可以读取应答信号。这是应答信号被有效传输的时刻。MyI2C_W_SCL(0);
:在应答信号被稳定放置在SDA线上一段时间后,将SCL线拉低。这标志着应答信号的传输完成,并为下一个I2C总线操作(如数据发送或接收)做好准备。通过这个函数,主机可以向从机发送应答信号,确认是否正确接收了从机发送的数据。当主机正确接收了数据后,它应该发送ACK(0),否则应该发送NACK(1)。从机在SCL高电平时读取SDA线上的应答信号,以此判断数据是否被主机正确接收。这是I2C协议中数据完整性检查的一部分。
/**
* 函 数: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; //返回定义应答位变量
}
uint8_t MyI2C_ReceiveAck(void)
定义了一个函数,它没有参数,返回一个uint8_t
类型的值,即接收到的应答信号(ACK或NACK)。uint8_t AckBit;
:定义了一个变量AckBit
,用于存储接收到的应答信号。MyI2C_W_SDA(1);
:在接收应答信号之前,确保SDA线被设置为高电平。这是为了释放SDA线,让从机有机会将应答信号发送到总线上。MyI2C_W_SCL(1);
:将SCL线设置为高电平。在SCL高电平期间,SDA线上的数据保持稳定,主机可以从SDA线上读取应答信号。AckBit = MyI2C_R_SDA();
:读取SDA线上的应答信号,并将其存储到AckBit
变量中。如果SDA线为低电平(0),表示从机发送了ACK;如果SDA线为高电平(1),表示从机发送了NACK。MyI2C_W_SCL(0);
:将SCL线拉低,这标志着应答信号的读取完成,并为下一个I2C总线操作做好准备。return AckBit;
:函数返回接收到的应答信号。这个函数的作用是让主机能够读取从机发送的应答信号,以确认数据是否已被正确接收。在I2C通信中,应答信号是通信可靠性的重要组成部分,因为它允许主机检查数据的完整性,并根据需要重新发送数据。