I2C的引脚包括SDA和SCL两个,是一种同步时序通讯协议

I2C的硬件要求

        在从机的SDA和SCL中都需要一个上拉电阻,其目的在于当引脚处没有其他的电气连接时,默认为高电平,在主控的芯片配置为开漏输出的模式,此时引脚反应为高阻态或低电平。

        当呈现高阻态的时候引脚的的电平由于有主控上拉甚至还有从机上拉的所以实际引脚会呈现高电平,而如果是在低电平的情况下,即使有上拉电阻,引脚也会直接被强制拉至低电平。在此模式下,可以通过主控改变SDA数据线上的输出,搭配SCL电平的变换,可以实现数据和地址的传输。但开漏输出的情况下,不代表不能读取引脚的电平,事实上来讲引脚

I2C的时序图

传输开始条件:SCL处于高电平,SDA下降沿时;

预先写好的宏定义

#define SCL_PIN  GPIO_Pin_10
#define SDA_PIN  GPIO_Pin_11
#define SCL_PORT GPIOB

 代码实现

void MyI2C_Start()
{
	//强制复位至全是高电平
	GPIO_SetBits(SCL_PORT,SDA_PIN);
	GPIO_SetBits(SCL_PORT,SCL_PIN);

	
	//按时序先拉低SDA后拉低SCL
	GPIO_ResetBits(SCL_PORT,SDA_PIN);
	GPIO_ResetBits(SCL_PORT,SCL_PIN);
}

终止条件:SCL高电平期间,SDA从低电平切换到高电平

void MyI2C_Stop()
{
	//先把SCL拉低,后把SDA拉低
	GPIO_ResetBits(SCL_PORT,SDA_PIN);
	
	//再优先把SCL拉高,再把SDA拉高
	GPIO_SetBits(SCL_PORT,SCL_PIN);
	GPIO_SetBits(SCL_PORT,SDA_PIN);
}

        其中要注意的一点是,一般来讲无论是开始条件还是结束条件,优先都是先先复位好SDA引脚,使得再SCL拉高的时候能产生正确的边沿信号,具体来讲就是SDA想产生下降沿,那么就应该先拉高SDA,以满足条件。如果先拉高SCL,再复位SDA,由于先前的SDA的状态其实是未知的,此时拉低引脚可能会产生意想不到的问题。故整个代码的实现顺序不能做任何的改动。

        传输数据:开始传输后,SCL处于高电平时,SDA的数据为所传输的数据;

I2C发送一个字节:

SCL低电平期间,主机将数据位依次放到SDA线上(高位先行),然后释放SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。用人话来讲就是先放数据SDA再拉高SCL取出数据(整个时间很短)再拉低SCL,得益于开始条件和发送字节结束的时候SCL的状态都是低电平的,所以没必要在最一开始拉低SCL了

// 写字节 (修正后)
void MyI2C_WriteByte(uint8_t value)
{
    for(uint8_t i=0; i<8; i++)
    {
        // 设置数据位 (MSB first)
        (value & (0x80 >> i)) ? 
            GPIO_SetBits(SCL_PORT, SDA_PIN) : 
            GPIO_ResetBits(SCL_PORT, SDA_PIN);
        
        // 产生时钟脉冲
        GPIO_SetBits(SCL_PORT, SCL_PIN);
        GPIO_ResetBits(SCL_PORT, SCL_PIN);
    }
}

同理的接受一个字节

uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i, Byte = 0x00;
	GPIO_SetBits(SCL_PORT,SDA_PIN);
	for (i = 0; i < 8; i ++)
	{
		GPIO_SetBits(SCL_PORT,SCL_PIN);
		if (GPIO_ReadInputDataBit(GPIOB, SDA_PIN)== 1){Byte |= (0x80 >> i);}
		GPIO_REsetBits(SCL_PORT,SCL_PIN);
	}
	return Byte;
}

发送应答:

主机在接收完一个字节之后,在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答

void MyI2C_SendAck(uint8_t AckBit)
{
	   GPIO_WriteBit(SCL_PORT,SDA_PIN,(BitAction)AckBit);
		GPIO_SetBits(SCL_PORT,SCL_PIN);
		GPIO_ResetBits(SCL_PORT,SCL_PIN);
}

接收应答:

主机在发送完一个字节之后,在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)

// 接收应答 (修正后)
uint8_t MyI2C_ReceiveAck(void)
{
    GPIO_SetBits(SCL_PORT, SDA_PIN); // 关键:释放SDA总线
    
    GPIO_SetBits(SCL_PORT, SCL_PIN);
    uint8_t ack = GPIO_ReadInputDataBit(SCL_PORT, SDA_PIN);
    GPIO_ResetBits(SCL_PORT, SCL_PIN);
    
    return ack; // 0=ACK, 1=NACK
}

补充:

这里我配置的GPIO都为开漏模式,但要注意的一点是,开漏模式和调用读取引脚的输入值的api是不矛盾的。按一般配置来讲GPIOB使用的时钟是APB2的外设对应频率是72MHZ,震动一次的频率也就是13.8ns。一般来讲电平的转换对应的汇编指令就是一条,所以我们高低电平变换1的时间就只有13.8ns,这个时间能否足够I2C的各个时序呢?理论上答案是不能的,最少的I2C要的时间都需要250ns,故整体代码是要加入延时的,但实际测试下来I2C是能跟上的,这一块还没有搞明白。

全部代码如下:

#include "stm32f10x.h"                  // Device header

#define SCL_PIN  GPIO_Pin_10
#define SDA_PIN  GPIO_Pin_11
#define SCL_PORT GPIOB
void MyI2C_Init()
{
	//ÅäÖÃÒý½ÅΪ¿ªÂ©Êä³öµÄģʽ
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
	
	GPIO_InitTypeDef GPIO_StructInit;
	GPIO_StructInit.GPIO_Mode=GPIO_Mode_Out_OD;
	GPIO_StructInit.GPIO_Pin=GPIO_Pin_10|GPIO_Pin_11;
	GPIO_StructInit.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOB,&GPIO_StructInit);
	
	GPIO_SetBits(GPIOB,GPIO_Pin_10|GPIO_Pin_11);
}

//ÆðʼÌõ¼þ
//SCL¸ßµçƽÆÚ¼ä£¬SDA´Ó¸ßµçƽÇл»µ½µÍµçƽ
void MyI2C_Start()
{
	//Ç¿ÖÆ¸´Î»µ½È«ÊÇ¸ßµçÆ½
	GPIO_SetBits(SCL_PORT,SDA_PIN);
	GPIO_SetBits(SCL_PORT,SCL_PIN);

	
	//°´Ê±ÐòÏÈÀ­µÍSDAºóÀ­µÍSCL
	GPIO_ResetBits(SCL_PORT,SDA_PIN);
	GPIO_ResetBits(SCL_PORT,SCL_PIN);

}

//½áÊøÌõ¼þ
//SCL¸ßµçƽÆ÷¼þ£¬SDA´ÓµÍµçƽÇл»µ½¸ßµçƽ
void MyI2C_Stop()
{
	//ÏȰÑSCLÀ­µÍ£¬ºó°ÑSDAÀ­µÍ
	GPIO_ResetBits(SCL_PORT,SDA_PIN);
	
	//ÔÙÓÅÏȰÑSCLÀ­¸ß£¬ÔÙ°ÑSDAÀ­¸ß
	GPIO_SetBits(SCL_PORT,SCL_PIN);
	GPIO_SetBits(SCL_PORT,SDA_PIN);
}

//I2Cдһ¸ö×Ö½Ú
void MyI2C_WriteByte(uint8_t value)
{
	GPIO_SetBits(SCL_PORT, SDA_PIN);   // ´ËʱSDAµÄ״̬ÊDz»È·¶¨µÄ£¬ËùÒÔÇ¿ÖÆÈÃÖ÷»úÊÍ·ÅSDAµÄ¿ØÖÆÈ¨£¬Èôӻú¿ÉÒÔ¶ÔSDA½øÐÐÐÞ¸Ä
     for(uint8_t i=0; i<8; i++)
    {

        if(value & (0x80 >> i)) {   
            GPIO_SetBits(SCL_PORT, SDA_PIN);  
        } else {
            GPIO_ResetBits(SCL_PORT, SDA_PIN); 
        }
        GPIO_SetBits(SCL_PORT, SCL_PIN);  
        GPIO_ResetBits(SCL_PORT, SCL_PIN);
    }
}

//I2C½ÓÊÜÒ»¸ö×Ö½Ú
uint8_t MyI2C_ReceiveByte(void)
{
	uint8_t i, Byte = 0x00;
	GPIO_SetBits(SCL_PORT,SDA_PIN);
	for (i = 0; i < 8; i ++)
	{
		GPIO_SetBits(SCL_PORT,SCL_PIN);
		if (GPIO_ReadInputDataBit(GPIOB, SDA_PIN)== 1){Byte |= (0x80 >> i);}
		GPIO_ResetBits(SCL_PORT,SCL_PIN);
	}
	return Byte;
}

//·¢ËÍÓ¦´ð
void MyI2C_SendAck(uint8_t AckBit)
{
	   GPIO_WriteBit(SCL_PORT,SDA_PIN,(BitAction)AckBit);
		GPIO_SetBits(SCL_PORT,SCL_PIN);
		GPIO_ResetBits(SCL_PORT,SCL_PIN);
}

//½ÓÊÜÓ¦´ð

uint8_t MyI2C_ReceiveAck(void)
{
    GPIO_SetBits(SCL_PORT, SDA_PIN); 
    
    GPIO_SetBits(SCL_PORT, SCL_PIN);
    uint8_t ack = GPIO_ReadInputDataBit(SCL_PORT, SDA_PIN);
    GPIO_ResetBits(SCL_PORT, SCL_PIN);
    
    return ack; // 0=ACK, 1=NACK
}

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐