I2C学习笔记-软件模拟I2C

I2C学习笔记(软件模拟)

介绍

I2C的信号大概有 起始信号、应答信号、停止信号、写数据、读数据、无应答信号等,每个信号都有其不同的特点时序要求。

参考视频思路:https://www.youtube.com/watch?v=6IAkYpmA1DQ

参考资料:正点原子HAL库介绍

GPIO的配置

c 复制代码
/**
 * @brief       初始化IIC
 * @param       无
 * @retval      无
 */
void iic_init(void)
{
    GPIO_InitTypeDef gpio_init_struct;

    IIC_SCL_GPIO_CLK_ENABLE();  /* SCL引脚时钟使能 */
    IIC_SDA_GPIO_CLK_ENABLE();  /* SDA引脚时钟使能 */

    gpio_init_struct.Pin = IIC_SCL_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;        /* 推挽输出 */
    gpio_init_struct.Pull = GPIO_PULLUP;                /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;      /* 高速 */
    HAL_GPIO_Init(IIC_SCL_GPIO_PORT, &gpio_init_struct);/* SCL */

    gpio_init_struct.Pin = IIC_SDA_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_OD;        /* 开漏输出 */
    HAL_GPIO_Init(IIC_SDA_GPIO_PORT, &gpio_init_struct);/* SDA */
    /* SDA引脚模式设置,开漏输出,上拉, 这样就不用再设置IO方向了, 开漏输出的时候(=1), 也可以读取外部信号的高低电平 */

    iic_stop();     /* 停止总线上所有设备 */
}
c 复制代码
/* 引脚 定义 */

#define IIC_SCL_GPIO_PORT               GPIOB
#define IIC_SCL_GPIO_PIN                GPIO_PIN_6
#define IIC_SCL_GPIO_CLK_ENABLE()       do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   /* PB口时钟使能 */

#define IIC_SDA_GPIO_PORT               GPIOB
#define IIC_SDA_GPIO_PIN                GPIO_PIN_7
#define IIC_SDA_GPIO_CLK_ENABLE()       do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)   /* PB口时钟使能 */

/******************************************************************************************/

/* IO操作 */
#define IIC_SCL(x)        do{ x ? \
                              HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT, IIC_SCL_GPIO_PIN, GPIO_PIN_SET) : \
                              HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT, IIC_SCL_GPIO_PIN, GPIO_PIN_RESET); \
                          }while(0)       /* SCL */

#define IIC_SDA(x)        do{ x ? \
                              HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN, GPIO_PIN_SET) : \
                              HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN, GPIO_PIN_RESET); \
                          }while(0)       /* SDA */

#define IIC_READ_SDA     HAL_GPIO_ReadPin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN) /* 读取SDA */

信号的展示

起始信号 与 停止信号

起始信号:当SCL为高电平时,SDA由高电平向低电平跳变

停止信号:当SCL位高电平时,SDA由低电平向高电平跳变

代码实现

c 复制代码
/*SCL SAD都由1跳变到0*/
void I2CStart(void)
{
/* SCL为高电平期间, SDA从高电平往低电平跳变*/
    IIC_SDA ( 1 );	
	IIC_SCL ( 1 );
    iic_delay( );
    
 	IIC_SDA ( 0 );	
 	iic_delay( );
    IIC_SCL ( 0 );	
 	iic_delay( ); 
    /* 钳住总线, 准备发送/接收数据 */

}

抓包数据

c 复制代码
/*SCL SDA 都由0跳变到1*/
void I2CStop(void)
{
 /* SCL为高电平期间, SDA从低电平往高电平跳变*/
    IIC_SDA ( 0 );	
	iic_delay( );
 	IIC_SCL ( 1 );	
 	iic_delay( );

    
    IIC_SDA ( 1 ); 	/* 发送总线停止信号*/
 	iic_delay( );


}

应答信号(非应答信号)

在发送完数据后,SCL为高电平,如果SDA为低电平则为应答信号。因为I2C外部默认上拉,如果为低电平时就说明从机在响应了,如果还是高电平就说明从机没有动作。

数据线为低位时,表示应答

c 复制代码
void iic_ack(void)
{ 
    IIC_SCL (0);	
	iic_delay( );
 	IIC_SDA (0);  /* 数据线为低电平,表示应答 */
 	iic_delay( );
    IIC_SCL (1); 	
 	iic_delay( );
}

数据线为高位时,说明从机没动作,被上拉至高电平,说明没有应答。

c 复制代码
void iic_nack(void)
{ 
    IIC_SCL (0);	
	iic_delay( );
 	IIC_SDA (1);  /* 数据线为高电平,表示非应答 */
 	iic_delay( );
    IIC_SCL (1); 	
 	iic_delay( );
}

检测应答信号

复制代码
检测应答信号,是在SCL为高电平的时候检测的,所以首先SDA为高电平,上拉电阻的存在,表示是释放了SDA,然后将SCL拉高,延时等待SDA是否为低电平,如果SDA为低电平,表示是从机发来的应答信号,为高电平则说明从机没有应答。
c 复制代码
uint8_t iic_wait_ack (void) /* return 1:fail 0:succeed*/
{	
	IIC_SDA (1);  /* 主机释放SDA线 */
	iic_delay( );
	IIC_SCL (1);  /* 从机返回ACK*/ 	
 	iic_delay( );
	if ( IIC_READ_SDA ) /* SCL高电平读取SDA状态*/ 
	{
		iic_stop();	    /* SDA高电平表示从机nack */ 
		return 1;
	}
	IIC_SCL(0);	 /* SCL低电平表示结束ACK检查 */ 
 	iic_delay( );
	return 0;
}

发送一个字节数据

一个字节数据默认为8位,首先先发高位,

0x80就是 1000 0000,所以经过8次循环,每一次取得最高位然后左移7 发送出去,发送出去后数据位左移动一位,用于下一次循环的发送。

当SCL为高电平时,数据位有效,所以

c 复制代码
void iic_send_byte(uint8_t data)
{
	for (uint8_t t = 0; t < 8; t++)
	{	/* 高位先发 */
		IIC_SDA((data & 0x80) >> 7);
 		iic_delay( );
 		IIC_SCL ( 1 );	
 		iic_delay( );
 		IIC_SCL ( 0 );
		data <<= 1; /* 左移1位, 用于下一次发送 */
	}
	IIC_SDA ( 1 ); 	/* 发送完成,主机释放SDA线 */ 
}

波形图分析:

接收一个字节数据

接收数据,当数据在SCL为高电平时,说明数据有效。也会是先把SCL拉高,延时等待,让后去读取SDA的电平作为接收的数据,每次在SDL为高电平的时候接收,每接收一位数据就左移一位,最后组成8位数据。

如果是0 的话,就+0 如果是1的话就+1 让后接收完左移1

如 0xaa 1010 1010

第0次 rec = 0

第一次 rec=1 0000 0001 <<1 0000 0010

第二次 rec=0 0000 0010 <<1 0000 0100

第三次 rec=1 0000 0101 <<1 0000 1010

第四次 rec=0 0000 1010 <<1 0001 0100

第五次 rec=1 0001 0101 <<1 0010 1010

第六次 rec=0 0010 1010 <<1 0101 0100

第七次 rec=1 0101 0101 <<1 1010 1010

c 复制代码
uint8_t iic_read_byte (uint8_t ack) /* 1:ack 0:nack*/
{ 
	uint8_t receive = 0 ;
	for (uint8_t t = 0; t < 8; t++)
	{	/* 高位先输出,先收到的数据位要左移 */ 
		receive <<= 1;		
		IIC_SCL ( 1 );	
 		iic_delay( );
		if ( IIC_READ_SDA ) receive++;
 		IIC_SCL ( 0 );
		 iic_delay( );
	}
	if ( !ack ) iic_nack();
	else iic_ack();
	return receive; 	
}

硬件配置

为什么IIC总线SDA建议用开漏模式?

IIC的SDA脚即要作为输出,又要作为输入,用开漏输出模式,很好实现输出输入共用,避免IO模式频繁切换带来的麻烦。

**输出时:**主机(MCU)输出0,可以拉低信号,来实现低电平发送,主机输出1(实际不起作用),由外部上拉电阻上拉,实现高电平发送。

**输入时:**主机(MCU)设置输出1状态,此时因为MCU无法输出1,相当于释放了SDA脚,此时外部器件可以主动拉低SDA脚/释放SDA脚(同样由上拉电阻提供"输出1的功能"),实现SDA脚的高低电平变化。

由于开漏输出模式下,MCU还是可以读取IDR状态寄存器,来获取引脚高低电平,因此MCU读取IDR,即可获得SDA脚的高低电平状态,从而实现输入检测。


实物测试

实物采用USB转I2C调试器和调试工具,接逻辑分析仪和I2C从机AT24C02来读取和写入EEPROM中地址0的数据。

开始抓包 读取AT24C02地址为0xA0的寄存器0的数据

写入数据

相关推荐
KaifuZeng36 分钟前
模电面试问题汇总一
单片机·嵌入式硬件·面试·模电
栈溢出了1 小时前
Redis 分片集群与哈希槽笔记
java·redis·笔记·spring
2401_854151551 小时前
STM32 看门狗:IWDG 和 WWDG 怎么用
stm32·单片机·嵌入式硬件
我命由我123451 小时前
复利极简理解
经验分享·笔记·学习·职场和发展·求职招聘·职场发展·学习方法
AOwhisky2 小时前
Python 学习笔记(第三期)——流程控制核心知识点自测与详解
开发语言·笔记·python·学习·云原生·运维开发·流程控制
tyqtyq222 小时前
药品说明书解读 —— AI 安全用药助手,鸿蒙原生应用深度解析
人工智能·学习·华为·生活·harmonyos
zjxtxdy3 小时前
TIM编码器接口
stm32·单片机·嵌入式硬件
小心亦新3 小时前
STM32学习13 定时器1中断
stm32·嵌入式硬件·学习
我命由我123453 小时前
72 法则极简理解
经验分享·笔记·学习·职场和发展·求职招聘·职场发展·学习方法
qq_263_tohua4 小时前
第112期 CNN学习,不错的文章
学习