IIC协议

本文详细介绍了IIC通信协议的概念,包括一主多从和多主多从模式,硬件连线要求,如SCL和SDA的连接方式及开漏输出模式,以及软件时序规则,如起始信号、数据传输、应答处理等。最后提供了示例的软件模拟代码段。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、概念

两根通信线:SCL和SDA

同步,半双工

带数据应答

支持总线挂载多设备(一主多从、多主多从)

一主多从:单片机作为主机,挂载在IIC总线上的所有模块都是从机,主机可以控制和哪一个从机进行通信。任何时候,都是主机完全掌控SCL时钟线,在SDA空闲状态下,主机可以获得SDA总线的控制,只有在从机发送数据和应答位时,主机才会将SDA的控制权移交给从机。

多主多从:所有设备都可以充当主机,当一个设备是主机,但是突然出现一个设备打断,说他想当主机(由于IIC总线只能由一个设备进行控制),因此会进行仲裁,获胜的一方是主机,失败则是从机

         时钟频率与数据传输率是如何对等的喃。因为一个SCL时钟周期只能传输1bit数据(并且只能在SCL的低电平期间进行SDA数据电平的变化),假如SCL的时钟频率为100KHz,那么1s中就能产生100K个SCL时钟周期,也就是说1s中可以传输100Kbit的数据。此时,可以看出时钟频率100KHz与数据传输率100Kbit/s就是一样的了。

         IIC传输位速率在标准模式下可达100Kbit/s,快速模式下可达400Kbit/s,高速模式下可达3.4Mbit/s;也可以理解为时钟频率在标准模式下可达100kHz,快速模式下可达400kHz,高速模式下可达3.4MHz。(时钟频率(Hz)与数据传输速率(bit/s)两者是相同的概念。)

二、硬件连线

(1)所有IIC设备的SCL线连在一起,SDA线连在一起

(2)设备的SCL和SDA均要配置成开漏输出模式(因为SDA时半双工通信,所以主机和从机的SDA都会输入或者输出高低电平,但如果总线时序没有调节好,造成了主机和从机的SDA同时输出高电平,此时就会造成电路短路,所以IIC协议禁止SDA配置推挽输出模式;然后SCL配置为开漏输出原因时多主多从模式下需要使用此特性)

(3)SCL和SDA添加一个上拉电阻,阻值时4.7KΩ(开漏输出只能输出低电平,输出高电平时引脚初始浮空状态,所以提供一格上拉电阻,可以输出一个弱高电平)

三、IIC软件时序

(1)起始和终止条件

(2)发送一个字节

(3)接收一个字节

 

 (4)发送和接收应答

(5)指定地址写一个字节

开始信号+从机地址&W+ack+寄存器地址+ack+数据+uack+结束信号

(6)当前地址读一个字节

对于指定设备,在当前地址指针指示的地址下,读取从机数据

时序:开始信号+从机地址&R+ack+数据+uack+结束信号

(但是从机并不知道应该读哪个寄存器的地址,所以此时采取读当前指针指向的寄存器的值,在从机中,所以寄存器的都被分到了同一个线性区域,并且会有一个单独的指针变量指示着其中的一个寄存器,其值在上电默认,一般指向0地址,并且每写入一个字节或者读出一个字节之后,这个指针会自动加一,移动到下一个寄存器,所以如果主机没有指定要读取那个寄存器的地址,那么就会返回当前指针指向的值,如果从机指定了要读的寄存器的地址,那么指针变量会指向要读的寄存器的地址,读写该寄存器之后指针变量会加一)

(7)指定地址读一个字节

开始时序+从机地址&W+ack+寄存器地址+ack+开始时序+从机地址&R+ack+数据+uack+结束信号

(8)指定地址写多个字节

开始信号+从机地址&W+ack+寄存器地址+ack+数据1+ack+数据2+ack+...+数据n+uack+结束信号

(9)指定地址读多个字节

开始时序+从机地址&W+ack+寄存器地址+ack+开始时序+从机地址&R+ack+数据1+ack+数据2+ack+...+数据n+uack+结束信号

四、软件模拟代码(GD32F303)

#include "MyIIC.h"

/*引脚配置层*/
#define MYIIC_SCL_PORT (GPIOB)
#define MYIIC_SCL_PIN (GPIO_PIN_10)
#define MYIIC_SDA_PORT (GPIOB)
#define MYIIC_SDA_PIN (GPIO_PIN_11)

void MyI2C_delay()
{
	int i = 400;
	while(i--);
}

/**
  * 函    数:I2C写SCL引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SCL的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCL为低电平,当BitValue为1时,需要置SCL为高电平
  */
void MyI2C_W_SCL(uint8_t BitValue)
{
	gpio_bit_write(MYIIC_SCL_PORT, MYIIC_SCL_PIN, (bit_status)BitValue);		//根据BitValue,设置SCL引脚的电平
	MyI2C_delay();												
}

/**
  * 函    数:I2C写SDA引脚电平
  * 参    数:BitValue 协议层传入的当前需要写入SDA的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SDA为低电平,当BitValue为1时,需要置SDA为高电平
  */
void MyI2C_W_SDA(uint8_t BitValue)
{
	gpio_bit_write(MYIIC_SDA_PORT, MYIIC_SDA_PIN, (bit_status)BitValue);		//根据BitValue,设置SDA引脚的电平,BitValue要实现非0即1的特性
	MyI2C_delay();												//延时10us,防止时序频率超过要求
}

/**
  * 函    数:I2C读SDA引脚电平
  * 参    数:无
  * 返 回 值:协议层需要得到的当前SDA的电平,范围0~1
  * 注意事项:此函数需要用户实现内容,当前SDA为低电平时,返回0,当前SDA为高电平时,返回1
  */
uint8_t MyI2C_R_SDA(void)
{
	uint8_t BitValue;
	BitValue = gpio_output_bit_get(MYIIC_SDA_PORT, MYIIC_SDA_PIN);		//读取SDA电平
	MyI2C_delay();												//延时10us,防止时序频率超过要求
	return BitValue;											//返回SDA电平
}

/**
  * 函    数:I2C初始化
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,实现SCL和SDA引脚的初始化
  */
void MyI2C_Init(void)
{
	/*开启时钟*/
	rcu_periph_clock_enable(RCU_GPIOB);//开启GPIOB的时钟
	
	/*GPIO初始化*/
	gpio_init(MYIIC_SCL_PORT, GPIO_MODE_OUT_OD, GPIO_OSPEED_50MHZ,  MYIIC_SCL_PIN);
	gpio_init(MYIIC_SDA_PORT, GPIO_MODE_OUT_OD, GPIO_OSPEED_50MHZ,  MYIIC_SDA_PIN);
	
	/*设置默认电平*/
	MyI2C_W_SCL(1);		//(释放总线状态)
	MyI2C_W_SDA(1);
}

/*协议层*/

/**
  * 函    数: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_SCL(0);						//拉低SCL,主机开始发送下一位数据
		/*两个!可以对数据进行两次逻辑取反,作用是把非0值统一转换为1,即:!!(0) = 0,!!(非0) = 1*/
		MyI2C_W_SDA(!!(Byte & (0x80 >> i)));//使用掩码的方式取出Byte的指定一位数据并写入到SDA线
		MyI2C_W_SCL(1);						//释放SCL,从机在SCL高电平期间读取SDA
	}
	MyI2C_W_SCL(0);						//拉低SCL,等待从机发送应答位
	
	
//	uint8_t i;
//	for (i = 0; i < 8; i ++)				//循环8次,主机依次发送数据的每一位
//	{
//		/*两个!可以对数据进行两次逻辑取反,作用是把非0值统一转换为1,即:!!(0) = 0,!!(非0) = 1*/
//		MyI2C_W_SDA(!!(Byte & (0x80 >> i)));//使用掩码的方式取出Byte的指定一位数据并写入到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()){Byte |= (0x80 >> i);}	//读取SDA数据,并存储到Byte变量
													//当SDA为1时,置变量指定位为1,当SDA为0时,不做处理,指定位为默认的初值0
		MyI2C_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高电平期间读取SDA
	AckBit = MyI2C_R_SDA();					//将应答位存储到变量里
	MyI2C_W_SCL(0);							//拉低SCL,开始下一个时序模块
	return AckBit;							//返回定义应答位变量
}
#ifndef __MYI2C_H
#define __MYI2C_H

#include "gd32f30x.h"
#include "string.h"

void 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

### STM32 HAL 库中的I2C应答回调机制 在STM32 HAL库中,为了处理I2C通信过程中的发送和接收应答,HAL提供了特定的功能函数来简化这一操作。对于应答的控制,在初始化结构体`I2C_HandleTypeDef`内有一个成员变量`Init.NoAcknowledgeAddressSpecific`用于配置是否启用地址特异性无应答功能[^1]。 当涉及到具体的数据传输时,可以利用如下两个要接口: - `HAL_I2C_Master_Transmit()` 和 `HAL_I2C_Master_Receive()`: 这些API允许机模式下的数据发送与接收,并且可以通过参数传递的方式指定是否期望从设备回应ACK信号。 下面展示了一个简单的例子,说明如何使用这些API以及如何处理应答情况: ```c // 初始化I2C句柄 I2C_HandleTypeDef hi2c1; void I2C_Init(void){ // 配置I2C外设... hi2c1.Instance = I2C1; hi2c1.Init.Timing = 0x20909CEC; // 设置定时器值以适应所需的频率 hi2c1.Init.OwnAddress1 = 0; hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(&hi2c1) != HAL_OK){ Error_Handler(); } } uint8_t data_to_send[] = {0xAA, 0xBB}; uint8_t received_data[2]; // 机发送带应答请求的例子 if(HAL_I2C_Master_Transmit(&hi2c1, SLAVE_ADDR<<1, data_to_send, sizeof(data_to_send), HAL_MAX_DELAY)!= HAL_OK){ // 错误处理逻辑 } else{ // 成功发送后等待接收方响应 if(HAL_I2C_Master_Receive(&hi2c1, SLAVE_ADDR<<1, received_data, sizeof(received_data), HAL_MAX_DELAY)== HAL_OK){ // 数据成功接收到这里处理 } else { // 处理接收失败的情况 } } ``` 上述代码片段展示了基本的发送流程,其中包含了对发送完成后尝试接收来自目标节点回复的操作。值得注意的是,每次执行发送或接收命令前都需要确保之前的事务已经完成;此外,还需要考虑超时设置等问题以便更好地管理总线访问冲突等情况。 针对更复杂的场景,比如需要手动控制NACK(不确认),可以在回调函数里做相应的调整。例如,通过重写默认的行为或者修改硬件层面上的相关寄存器位来改变系统的反应方式[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值