stm32—I2C底层代码详解—江科大

声明:本文代码来自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,防止时序频率超过要求
}
  1. 函数声明
    • void MyI2C_W_SCL(uint8_t BitValue):定义了一个函数MyI2C_W_SCL,它接受一个uint8_t类型的参数BitValue。这个参数的值范围是0到1,其中0表示低电平,1表示高电平。
  1. 设置SCL引脚电平
    • GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);:这一行代码使用GPIO_WriteBit函数来设置SCL引脚(在STM32中通常映射到GPIOB的第10个引脚)的电平。BitAction是一个枚举类型,用于控制GPIO引脚的输出状态。如果BitValue为0,SCL引脚将被设置为低电平;如果BitValue为1,SCL引脚将被设置为高电平。
  1. 延时
    • 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,防止时序频率超过要求
}
  1. 函数声明
    • void MyI2C_W_SCL(uint8_t BitValue):定义了一个函数MyI2C_W_SCL,它接受一个uint8_t类型的参数BitValue。这个参数的值范围是0到1,其中0表示低电平,1表示高电平。
  1. 设置SCL引脚电平
    • GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);:这一行代码使用GPIO_WriteBit函数来设置SCL引脚(在STM32中通常映射到GPIOB的第10个引脚)的电平。BitAction是一个枚举类型,用于控制GPIO引脚的输出状态。如果BitValue为0,SCL引脚将被设置为低电平;如果BitValue为1,SCL引脚将被设置为高电平。
  1. 延时
    • 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电平 
}
  1. 函数声明
    • void MyI2C_W_SCL(uint8_t BitValue):定义了一个函数MyI2C_W_SCL,它接受一个uint8_t类型的参数BitValue。这个参数的值范围是0到1,其中0表示低电平,1表示高电平。
  1. 设置SCL引脚电平
    • GPIO_WriteBit(GPIOB, GPIO_Pin_10, (BitAction)BitValue);:这一行代码使用GPIO_WriteBit函数来设置SCL引脚(在STM32中通常映射到GPIOB的第10个引脚)的电平。BitAction是一个枚举类型,用于控制GPIO引脚的输出状态。如果BitValue为0,SCL引脚将被设置为低电平;如果BitValue为1,SCL引脚将被设置为高电平。
  1. 延时
    • 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引脚初始化后默认为高电平(释放总线状态)
}
  1. 开启时钟
    • RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);:这一行代码用于使能GPIOB的时钟。在STM32中,每个外设都需要其时钟被使能才能工作。RCC_APB2PeriphClockCmd函数用于控制APB2总线上的外设时钟。
  1. GPIO初始化
    • 首先,创建一个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的引脚。
  1. 设置默认电平
    • 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

  1. 数据稳定性:在SCL高电平期间,SDA线上的状态必须保持不变,以确保数据位能够被正确采样。这是因为数据位是在SCL的每个上升沿被接收设备读取的。
  2. 数据改变窗口:当SCL被拉低时,SDA线进入一个“数据改变窗口”。在此期间,SDA线上的数据状态可以被改变,以准备传输下一个数据位。这是因为接收设备不会在SCL为低电平时采样SDA线上的数据。
  3. 时序规则:I2C协议的时序规则确保了数据的可靠传输。在SCL的低电平期间改变SDA线状态,然后在SCL上升沿时,数据位被稳定并准备好被采样,这样的时序安排避免了数据的错误读取。

void MyI2C_Stop(void)
{
	MyI2C_W_SDA(0);							//拉低SDA,确保SDA为低电平
	MyI2C_W_SCL(1);							//释放SCL,使SCL呈现高电平
	MyI2C_W_SDA(1);							//在SCL高电平期间,释放SDA,产生终止信号
}

具体来说,停止信号的产生过程如下:

  1. SCL保持高电平:在停止信号开始之前,SCL线必须处于高电平状态。这是停止信号的一个前提条件。
  2. SDA从低变高:在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,主机开始发送下一位数据
	}
}
  1. 函数声明
    • void MyI2C_SendByte(uint8_t Byte) 定义了一个函数,它接受一个uint8_t类型的参数Byte,这个参数是要通过I2C总线发送的字节数据。
  1. 循环发送每一位数据
    • for (i = 0; i < 8; i ++):循环8次,因为一个字节有8位。每次循环发送一位数据。
  1. 设置SDA线
    • MyI2C_W_SDA(Byte & (0x80 >> i)):这一行使用位运算符&>>来从Byte中提取当前位的数据。0x80是一个掩码,表示二进制的最高位(第8位)。通过右移i次,将掩码对准当前需要发送的位。然后与Byte做位与操作,如果该位是1,结果就是非零值;如果是0,结果就是0。这个结果被传递给MyI2C_W_SDA函数,该函数负责将SDA线设置为相应电平。
  1. 设置SCL线(时钟)
    • 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;							//返回接收到的一个字节数据
}
  1. 函数声明
    • uint8_t MyI2C_ReceiveByte(void) 定义了一个函数,它没有输入参数,返回一个uint8_t类型的值,即接收的一个字节数据。
  1. 初始化接收字节
    • uint8_t i, Byte = 0x00;:定义了一个循环计数器i和一个接收数据的变量Byte,并将其初始化为0x00。这是因为我们将在循环中逐步填充这个字节的每一位。
  1. 准备接收数据
    • MyI2C_W_SDA(1);:在接收数据之前,确保SDA线被设置为高电平。这是为了释放SDA线,让从机有机会将数据发送到总线上。
  1. 循环接收每一位数据
    • 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线上的数据,准备发送下一位。
  1. 返回接收的字节
    • 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,开始下一个时序模块
}
  1. 函数声明
    • void MyI2C_SendAck(uint8_t AckBit) 定义了一个函数,它接受一个uint8_t类型的参数AckBit,这个参数可以是0或1,分别代表应答信号(ACK)或非应答信号(NACK)。
  1. 设置SDA线
    • MyI2C_W_SDA(AckBit);:将SDA线设置为AckBit所指示的电平。如果AckBit是0,那么SDA线将被设置为低电平,表示应答(ACK)。如果AckBit是1,那么SDA线将被设置为高电平,表示非应答(NACK)。
  1. 设置SCL线(时钟)
    • 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;							//返回定义应答位变量
}
  1. 函数声明
    • uint8_t MyI2C_ReceiveAck(void) 定义了一个函数,它没有参数,返回一个uint8_t类型的值,即接收到的应答信号(ACK或NACK)。
  1. 初始化变量
    • uint8_t AckBit;:定义了一个变量AckBit,用于存储接收到的应答信号。
  1. 准备接收应答信号
    • MyI2C_W_SDA(1);:在接收应答信号之前,确保SDA线被设置为高电平。这是为了释放SDA线,让从机有机会将应答信号发送到总线上。
  1. 设置SCL线(时钟)
    • MyI2C_W_SCL(1);:将SCL线设置为高电平。在SCL高电平期间,SDA线上的数据保持稳定,主机可以从SDA线上读取应答信号。
  1. 读取应答信号
    • AckBit = MyI2C_R_SDA();:读取SDA线上的应答信号,并将其存储到AckBit变量中。如果SDA线为低电平(0),表示从机发送了ACK;如果SDA线为高电平(1),表示从机发送了NACK。
  1. 设置SCL线(时钟)
    • MyI2C_W_SCL(0);:将SCL线拉低,这标志着应答信号的读取完成,并为下一个I2C总线操作做好准备。
  1. 返回应答信号
    • return AckBit;:函数返回接收到的应答信号。

这个函数的作用是让主机能够读取从机发送的应答信号,以确认数据是否已被正确接收。在I2C通信中,应答信号是通信可靠性的重要组成部分,因为它允许主机检查数据的完整性,并根据需要重新发送数据。


 

你可能感兴趣的:(Stm32代码模块,stm32,单片机,嵌入式硬件)