IIC协议

协议:
协议简单来说就是主机与从机双方约定一组动作,只要一方做了特定的动作,另一方就可以知道你要干什么,然后就可以给出特定的回复动作,多次重复就可以实现通信。

以下是我总结的IIC读写的步骤,仅供参考:
IIC 读数据:
发送起始信号
发送从机地址 - 发送(写入)
发送寄存器地址
起始信号
发送从机地址 - 接收(读取)
接收数据
发送停止信号

IIC 写数据:
发送起始信号
发送从机地址 - 发送(写入)
发送寄存器地址
发送数据
发送停止信号

软件模拟IIC实现与硬件IIC

由于在IIC总线的通信过程中,时钟信号由主设备产生,从设备根据时钟信号进行数据传输(即同步通信)。故可通过代码方便地模拟该协议,也即所谓的模拟IIC协议。
不过,对于异步通信来说,软件就没办法进行模拟了,比如常见的串口通信。

在软件模拟 IICGPIO 一般为开漏模式,支持线与功能;不过由于开漏模式无法输出真正的高电平,所以需要外部上拉(IIC的电平只是通信使用,所以负载强度不大;一般总线上认为,低于0.3Vdd为低电平,高于0.7Vdd为高电平)
推挽输出不能实现线与功能,因为如果两个输出引脚,一个输出高电平P-MOS管导通,一个输出低电平N-MOS管导通,则P-MOS管上方的高电平会经过P-MOS -> N-MOS -> GND,整个通路上没有外接电阻,因此电阻很小相当于高电平直接接到低电平造成了短路。

模拟 I2C是用两条 GPIO管脚的软件模拟的,将一个 GPIO设置为数据线 SDA,另外一个设置为时钟线 SCL
硬件 I2C则是通过一个 I2C控制器实现的,该控制器被建立在微控制器芯片或单独的 I2C芯片中,通过集成的硬件内部逻辑和电路来控制时序和数据格式,实现 I2C总线通信。
软件与硬件的IIC各有优缺点,可根据实际情况选用。

注:在使用软件模拟IIC时,其实IICIO口既可以配置为推挽输出也可以配置为开漏输出,不同之处在于当IO口配置为推挽输出时,发送和接收数据时需要切换IO口的输入输出模式,发送数据时需要将IO口切换为输出模式,接收数据时需要将IO口切换为输入模式。如果配置开漏输出则不需要切换IO口的输入输出模式。我们知道推挽输出不具有线与的功能,但是由于我们使用软件IIC时通常不会有多个设备连接到一个总线上的情况,所以只有一个从设备的话,也就不会有线与的情况发生了,可以使用推挽输出。但是我们需要根据发送数据和接收数据来切换IO口的工作模式。

模拟IIC代码实现

注:有时候器件规格书上标明的设备地址不一定非得按照该地址进行寻址!

具体来说就是,假如:现有一传感器,使用IIC通信;其规格书标明设备地址可选(0x18、0x19),假如根据硬件连接,我们选用的地址为0x18;这并不意味着我们寻址是直接使用#define SLAVE_ADDR 0x18就行,很可能需要进行一番运算,即:#define SLAVE_ADDR (0x18 << 1)。如果IIC使用7位地址,那么<< 1的作用就很明了了:将最低位让出,用做读写位。

所以如果使用IIC时,根据规格书上的地址寻不到从设备且检查并无其他问题,那么就可以考虑一下该情况!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
/**
* 函 数: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,防止时序频率超过要求
}

/**
* 函 数: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,防止时序频率超过要求
}

/**
* 函 数: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电平
}

/**
* 函 数: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引脚初始化后默认为高电平(释放总线状态)
}

/**
* 函 数: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_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() == 1)
{
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; // 返回定义应答位变量
}

/**
* 函 数:读取从机数据
* 参数1: 从机地址
* 参数2: 指向存放读取数据的地址
*参数3: 读取长度
*/
uint8_t I2C_Read_Data(uint8_t Address, uint8_t *Buffer, uint16_t Length)
{
My_I2C_Start();
My_I2C_WriteByte(Address); // 器件地址
if (My_I2C_GetACK())
{
My_I2C_Stop();
return 1;
}

My_I2C_WriteByte(0x00); // 寄存器地址
if (My_I2C_GetACK())
{
My_I2C_Stop();
return 1;
}

My_I2C_Start();
My_I2C_WriteByte(Address + 1);
if (My_I2C_GetACK())
{
My_I2C_Stop();
return 1;
}

while (1)
{
*Buffer++ = My_I2C_ReadByte();
if (--Length == 0)
{
My_I2C_PutACK(1);
break;
}
My_I2C_PutACK(0);
}

My_I2C_Stop();
return 0;
}

硬件IIC通信示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
void My_IIC_Init(void)
{
rcu_periph_clock_enable(RCU_I2C1);
rcu_periph_clock_enable(RCU_GPIOB);

gpio_af_set(GPIOB, GPIO_AF_1, GPIO_PIN_10);
gpio_af_set(GPIOB, GPIO_AF_1, GPIO_PIN_11);

gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_10); // SCL
gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_10); // 复用开漏
gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, GPIO_PIN_11); // SDA
gpio_output_options_set(GPIOB, GPIO_OTYPE_OD, GPIO_OSPEED_50MHZ, GPIO_PIN_11);

/* configure I2C clock */
i2c_clock_config(I2C1, 100000, I2C_DTCY_2);
/* configure I2C address */
i2c_mode_addr_config(I2C1, I2C_I2CMODE_ENABLE, I2C_ADDFORMAT_7BITS, OWN_ADDRESS7); // 主机地址

/* enable I2C1 */
i2c_enable(I2C1);
/* enable acknowledge */
i2c_ack_config(I2C1, I2C_ACK_ENABLE);
}

void I2C_Read_Data(void)
{
uint16_t timeout = 500;
/* 等IIC总线空闲 */
while (i2c_flag_get(I2C1, I2C_FLAG_I2CBSY));
/* 发送起始信号 */
i2c_start_on_bus(I2C1);
/* 等到起始信号发送完毕 */
while (!i2c_flag_get(I2C1, I2C_FLAG_SBSEND));

/* 发送从机地址 - 发送(写入) */
i2c_master_addressing(I2C1, SLA_ADDRESS7, I2C_TRANSMITTER); // 从机地址
/* 发送从机地址 - 若IIC从机在这儿会卡死,需添加返回操作 */
while (!i2c_flag_get(I2C1, I2C_FLAG_ADDSEND))
{
timeout--;
if (timeout == 0)
{
/* 发送结束信号 */
i2c_stop_on_bus(I2C1);
printf("No I2C Device\r\n");
return;
}
}
/* 清除 I2C_FLAG_ADDSEND 标志位 */
i2c_flag_clear(I2C1, I2C_FLAG_ADDSEND);

/* 发送寄存器地址 */
i2c_data_transmit(I2C1, REG_ADDRESS7); // 寄存器地址
while (!i2c_flag_get(I2C1, I2C_FLAG_BTC));

/* 发送起始信号 */
i2c_start_on_bus(I2C1);
/* 等到起始信号发送完毕 */
while (!i2c_flag_get(I2C1, I2C_FLAG_SBSEND));

/* 发送从机地址 - 接收(读取) */
i2c_master_addressing(I2C1, SLA_ADDRESS7, I2C_RECEIVER);
/* 匹配从机地址 */
while (!i2c_flag_get(I2C1, I2C_FLAG_ADDSEND));
/* 清除 ADDSEND 标志位 */
i2c_flag_clear(I2C1, I2C_FLAG_ADDSEND);

/* 读取数据 - 9个字节 */
for (int index = 0; index < 9; index++)
{
if (index == 8)
{
/* wait until the last data byte is received into the shift register */
while (!i2c_flag_get(I2C1, I2C_FLAG_BTC));
/* 失能应答 */
i2c_ack_config(I2C1, I2C_ACK_DISABLE);
}

/* wait until the RBNE bit is set */
while (!i2c_flag_get(I2C1, I2C_FLAG_RBNE));
/* read a data from I2C_DATA */
i2c_recv_buf[index] = i2c_data_receive(I2C1);
}

/* 发送结束信号 */
i2c_stop_on_bus(I2C1);
/* 等到结束信号发送完毕 */
while (I2C_CTL0(I2C1) & I2C_CTL0_STOP);

/* 使能应答 */
i2c_ack_config(I2C1, I2C_ACK_ENABLE);
}

注意

  1. 根据传输的数据位来拉SCLSDA线电平:若为逻辑1,则拉高SCLSDA线电平,若为逻辑0就拉低
  2. SCLSDA需默认拉高,以释放总线状态,此时为准备好数据传输状态
  3. IIC起始信号:SCL高电平,SDA下降沿(注:起始后需把SCL也拉低,使主机占用总线准备发送数据)IIC结束信号:SCL高电平,SDA上升沿;
  4. 主机发送:每次发送1位,循环8次即可发送一个字节;在发送数据时应注意:SCL高电平时,此时SDA上数据有效,不可被更改;SCL低电平时,SDA上数据可被更改,也只有这时数据可被更改
  5. 从机接收:在接收前,应先拉高SDA,避免主机抢占SDA,导致数据出错;为存储接收到的数据,需拉高SCL,使主机在SCL高电平期间读取SDA,每读取一位就存入提前申请好的变量中;读完后即可得到一个字节的数据。然后拉低SCL,使从机可写入SDA,这样即可实现从机发送。
  6. 应答信号:主机把应答位数据放到SDA线(即:如需应答则拉低SDA,如无需应答则拉高SDA);然后先拉高SCL,以读取应答位;再拉低SCL,开始下一个时序模块;

补充

推挽输出

推挽(Push-Pull)通常用于需要强驱动能力的场景。
推挽输出既可以向负载灌电流,也可以从负载抽取电流。推拉式输出既提高电路的负载能力,又提高开关速度。
下面是一个典型的推挽输出电路:上面的三极管是NPN型三极管,下面的三极管是PNP型三极管,分别有以下两种情况,请留意控制端、输入端和输出端。

输出低电平:从负载拉电流
推挽输出_低
Vin电压为V-时,下面的三极管有电流流出,Q4导通,Q3截止,于是电流从上往下流过。经过下面的P型三极管提供电流给负载(Rload),这就叫「挽」。

输出高电平:向负载灌电流
推挽输出_高
Vin电压为V+时,上面的N型三极管控制端有电流输入,Q3导通,Q4截止,于是电流从上往下通过,提供电流给负载。经过上面的N型三极管提供电流给负载(Rload),这就叫「推」。

开漏输出(开集输出)

开漏输出指的是场效应管(可以类比晶体管来理解,对于晶体管来说,也就变成了开集电极输出)的漏极开路输出,只能输出低电平和高阻态(只有接上拉电阻才能输出高电平)。

以下图为例,当内部输出为1时,经过非门变为0,也就是场效应管的栅极电压为0,此时场效应管截止(相当于开路),那么单片机IO的输出是什么呢?是没办法确定的,注意并没有上拉电路。如果有上拉电阻呢,当然就是VCC,也就是逻辑1。当内部输出0时,栅极电压为1,此时场效应管导通,单片机IO与地连接,输出为0。
开漏输出

开漏结构(OD)对比推挽结构:开漏结构只有一个三极管或者MOS管,推挽结构则有两个。
之所以叫开漏,是因为MOS管分为三极:源极、栅极、漏极。漏极开路输出,所以叫开漏输出;
如果是三极管:基极、集电极、发射极,集电极开路输出,就叫开集输出(OC)。

浮空输入

顾名思义,浮空就是浮在空中,既不上拉也不下拉;
通俗讲就是让管脚什么都不接,浮空着,呈高阻态。
浮空最大的特点就是电压的不确定性,它可能是0V,也可能是VCC,还可能是介于两者之间的某个值(最有可能)完全由外部输入决定,引脚悬空的情况下,该端口的电平是不确定的。

用途:

  • 浮空可用来做ADC输入,这样可以减少上下拉电阻对结果的影响。
  • 用于外部按键输入。

高阻态

高阻状态是三态门电路的一种状态,三态为高电平、低电平、高阻态。
当处于高阻态时,无论该门的输入如何变化,都不会对其输出有影响。
高阻态近似为开路状态,控制信号无法控制引脚的电平,引脚测量电压可能为任意的电压值。

上拉 & 下拉

上拉就是将引脚通过一个电阻连接到VCC上;
上拉电阻的功能主要是为集电极开路输出型电路和开漏输出提供输出电流通道,将不确定的信号通过一个电阻钳位在高电平(可以结合上面的开漏输出来理解),电阻同时起限流作用。
所谓的强上拉、弱上拉,只是上拉电阻的阻值不同。

下拉就是将引脚通过一个电阻连接到GND上,将不确定的信号通过一个电阻钳位在低电平。

三极管 & 场效应管

三个极:
三极管是半导体基本元器件之一,具有电流放大作用;
共有三个极:发射极(Emitter)、基极(Base)和集电极(Collector);中间部分是基区,两侧部分是发射区和集电区,排列方式有PNPNPN两种。
场效应管有三个极:源极(S - Source),栅极(G - Gate),漏极(D - Drain),对应于晶体管的发射极(E),基极(B),以及集电极(C),排列方式也有P沟道和N沟道两种。

MOS管的源极和漏极是可以对调的,他们都是在Pbackgate中形成的N型区。在多数情况下,这个两个区是一样的,即使两端对调也不会影响器件的性能,这样的器件被认为是对称的。

N-MOS的特性,Vgs大于一定的值就会导通,适合用于源极接地时的情况(低端驱动)。
P-MOS的特性,Vgs小于一定的值就会导通,适合用于源极接 VCC时的情况(高端驱动)。

三极管与场效应管的基本工作原理:

  • 三极管‌:三极管是一种电流控制元件,其工作原理是通过控制输入电流来改变输出电流。三极管有NPN型和PNP型两种类型,电流方向不同‌。
  • 场效应管‌:场效应管是一种电压控制元件,通过控制输入电压来改变输出电流。场效应管分为结型场效应管和金属氧化物场效应管(MOS管),其中MOS管是最常见的类型‌。

三极管与场效应管的主要区别:

  1. 控制方式‌:三极管是电流控制元件,需要输入电流才能产生输出电流;而场效应管是电压控制元件,输入端电流极小,适用于低功耗应用‌。
  2. 输入阻抗‌:三极管的输入阻抗较小,而场效应管的输入阻抗较大,适用于高输入电阻的场合‌。
  3. 噪声性能‌:场效应管的噪声系数较低,适用于低噪声放大器的前置级‌。
  4. 温度特性‌:三极管是负温度系数器件,而场效应管是正温度系数器件,场效应管在高温下功耗增加,可能导致更高的温度‌。
  5. 开关频率‌:场效应管的开关频率高于三极管,适用于高速开关应用‌。

应用场景:

  • 三极管‌:适用于低频放大、开关电路和功率放大等应用。
  • 场效应管‌:适用于高频放大、低噪声放大、开关电路和低功耗电路等‌。

线与

线与:所有GPIO输出高就是高,只要有一个输出低,整条线上面的都是低,这就是“与”的意思。

参考资料

I2C推挽结构和开漏结构 - 电子发烧友

从IIC看推挽输出和开漏输出 - 知乎

推挽输出和开漏输出 - 博客园

推挽、开漏等概念总结 - STM32社区

三极管 - CSDN

认识场效应管MOSFET - 知乎

管脚复用功能之 上拉、下拉、模拟、浮空输入;推挽、开漏、复用输出 - 博客园

嵌入式软件学习之单片机篇 GPIO - 知乎