在使用单片机的过程中,I2C 通信可以说是最被广泛使用和采纳的协议之一,采用 I2C 协议可以占用更少的资源,链接多台设备,因此它和 SPI 一样,在数字传感器中备受偏爱。

I²C(Inter-Integrated Circuit)字面上的意思是集成电路之间,它其实是 I²C Bus 简称,所以中文应该叫集成电路总线,它是一种串行通信总线,使用多主从架构,由飞利浦公司在1980年代为了让主板、嵌入式系统或手机用以连接低速周边设备而发展。I²C 的正确读法为“I平方C”(”I-squared-C”),而“I二C”(”I-two-C”)则是另一种错误但被广泛使用的读法。自2006年11月1日起,使用I²C协议已经不需要支付专利费,但制造商仍然需要付费以获取I²C从属设备地址。

I2C 主要用于电压、温度监控,EEPROM数据的读写,光模块的管理等。该总线只有两根线,SCL 和 SDA,SCL 即 Serial Clock,串行参考时钟,SDA 即 Serial Data,串行数据。

原理

IO口

注意使用其他通讯协议时,你可能不需要特别注意 IO 的模式,但如果你使用的是模拟 I2C 的话,则最好关注下 IO 口模式设置。在 I2C 总线中有 2 个口线,SDA 和 SCL。这两个口线对为 OC 输出。

OC就是开漏输出(Open Collector)的简称,有时候也叫OD输出(Open-Drain),OD是对mos管而言,OC是对双极型管而言,在用法上没啥区别。相对于OC输出,另一种输出叫推挽输出(Push-Pull),一般的MCU管脚输出可以设置这两种模式。这里分别介绍下这两种输出的不同点。

  • 推挽输出 : 可以输出高、低电平连接数字器件,推挽结构一般是指两个三极管分别受两互补信号的控制,总是在一个三极管导通的时候另一个截止.
  • 开漏输出 : 输出端相当于三极管的集电极未接任何电平, 要得到高电平状态需要上拉电阻才行,适合于做电流型的驱动,其吸收电流的能力相对强(一般20ma以内)。

对于MCU的开发者来讲,简单的这样理解就可以了。如果管脚设置成推挽输出模式,输出高时,IO口相当于VCC, 输出低时IO口相当于接地。如果管脚设置成开漏输出模式,输出高时,IO口的电平会和与其相连的口线进行与操作,如果都为高,才会被上拉拉成高电平,输出为低时,也相当于接地。

在网上我们看到很多的例程代码都是直接设置IO口的高低电平,这样做其实是不太合理的。因为我们只满足了 I2C 总线在自己这端的时序要求。而没有考虑到连接在总线上的其他器件。如果总线上其他器件的电平和MCU输出的电平一致,这样做是没问题的,如果两边的电平不一致时,这样做就有一定风险造成IO的损坏。当你输出高时,相当于IO口连接到VCC,如果对方这是恰好输出的是低电平,那就相当于短路了。因为 I2C 总线要实现线与的功能,所以 SDA 和 SCL 口线都必须设置为开漏输出模式,这种方式也是最安全的模式。我们使用 MCU 的硬件 I2C 接口时,口线会被自动设置成开漏。但有时候我们会使用 IO 口来模拟 I2C 总线,这个时候我们如何设置口线呢?

因为 I2C 的总线是开漏输出的,总线上接上上拉电阻后,SCL 和 SDA 就变成了高电平,这个时候挂接在总线上的任意一个 I2C 主机口可以把 SDA 拉低,即产生了一个 START 信号,挂接在总线上的其他 I2C 主机检测到这个信号后就不能去操作 I2C 总线了,否则会发生冲突。直到检测到一个 STOP 信号为止。STOP 的信号是在 SCL 口线为高时,SDA 产生一个上升沿。STOP 信号之后,I2C总线恢复到初始状态。

这里要分两种情况,如果MCU的口线支持开漏输出模式,则可以直接把 SDA 和 SCL 设置成开漏输出。例如 Silicon 的 C8051 系列 MCU,它的口线就支持开漏和推挽输出。如果 MCU 不支持开漏输出,例如 MSP430。当然如果软件做的合理,是可以避免这样的事情的,但总线上的很多器件都不是由你能控制的,如何设计出更合理的软件来避免这样的问题发生呢?对于不支持开漏输出MCU,我们最合理的做法是,当设置口线电平为高时,我们把口线设置成输入状态,然后利用口线上的上拉电阻来把口线拉高。这样即使是两边电平不一致时,也不会造成IO口的损坏。

速度

常见的I²C总线依传输速率的不同而有不同的模式:标准模式(100 Kbit/s)、低速模式(10 Kbit/s),但时钟频率可被允许下降至零,这代表可以暂停通信。而新一代的I²C总线可以和更多的节点(支持10比特长度的地址空间)以更快的速率通信:快速模式(400 Kbit/s)、高速模式(3.4 Mbit/s)。

连线

如上图所示,I2C 是 OC 或 OD 输出结构,使用时必须在芯片外部进行上拉,上拉电阻R的取值根据 I2C 总线上所挂器件数量及 I2C 总线的速率有关,一般是标准模式下 R 选择 10kohm,快速模式下 R 选取 1kohm,I2C 总线上挂的 I2C 器件越多,就要求 I2C 的驱动能力越强,R 的取值就要越小,实际设计中,一般是先选取 4.7kohm 上拉电阻,然后在调试的时候根据实测的 I2C 波形再调整 R 的值。

I²C 只有两根通讯线:数据线 SDA 和时钟 SCL,可发送和接收数据。I2C 总线在传送数据过程中共有三种类型信号, 它们分别是:

  • 开始信号: SCL 为高电平时, SDA 由高电平向低电平跳变,开始传送数据。
  • 结束信号: SCL 为高电平时, SDA 由低电平向高电平跳变,结束传送数据。
  • 应答信号:接收数据的 IC 在接收到 8bit 数据后,向发送数据的 IC 发出特定的低电平脉冲,表示已收到数据。 CPU 向受控单元发出一个信号后,等待受控单元发出一个应答信号, CPU 接收到应答信号后,根据实际情况作出是否继续传递信号的判断。若未收到应答信号,由判断为受控单元出现故障。

具体时序如下图所示:

I2C总线的主要时序参数有:开始建立时间 t(SUSTA),开始保持时间 t(HDSTA),数据建立时间 t(SUDAT),数据保持时间 t(SUDAT) ,结束建立时间 t(SUSTO) 。

  • 开始建立时间:SCL 上升至幅度的90%与SDA下降至幅度的90%之间的时间间隔;
  • 开始保持时间:SDA下降至幅度的10%与SCL下降至幅度的10%之间的时间间隔;
  • 数据建立时间:SDA上升至幅度的90%或SDA下降至幅度的10%与SCL上升至幅度的10%之间的时间间隔;
  • 数据保持时间:SCL下降至幅度的10%与SDA上升至幅度的10%或SDA下降至幅度的90%之间的时间间隔;
  • 结束建立时间:SCL上升至幅度的90%与SDA上升至幅度的90%之间的时间间隔;

  • 数据接收方收到传输的一个字节数据后,需要给出响应,此时处在第九个时钟,发送端释放SDA线控制权,将SDA电平拉高,由接收方控制。若希望继续,则给出“应答(ACK)”信号,即SDA为低电平;反之给出“非应答(NACK)”信号,即SDA为高电平。I2C总线传输的特点:I2C总线按字节传输,即每次传输8bits二进制数据,传输完毕后等待接收端的应答信号ACK,收到应答信号后再传输下一字节。等不到ACK信号后,传输终止。空闲情况下,SCL和SDA都处于高电平状态。

每个字节后会跟随一个ACK信号。ACK bit使得接收者通知发送者已经成功接收数据并准备接收下一个数据。所有的时钟脉冲包括ACK信号对应的时钟脉冲都是由master产生的。

ACK信号:发送者在ACK时钟脉冲期间释放SDA线,接收者可以将SDA拉低并在时钟信号为高时保持低电平。

NACK信号:当在第9个时钟脉冲的时候SDA线保持高电平,就被定义为NACK信号。Master要么产生STOP条件来放弃这次传输,或者重复START条件来发起一个新的开始。

  • 判断一次传输的开始:如上图所示,I2C总线传输开始的标志是:SCL信号处于高电平期间,SDA信号出现一个由高电平向低电平的跳变。

  • 判断一次传输的结束:如上图所示,I2C总线传输结束的标志是:SCL信号处于高电平期间,SDA信号出现一个由低电平向高电平的跳变。跟开始标识正好相反。

  • 有效数据:在SCL处于高电平期间,SDA保持状态稳定的数据才是有效数据,只有在SCL处于低电平状态时,SDA才允许状态切换。前面已经讲过了,SCL高电平期间,SDA状态发生改变,是传输开始/.结束的标志。

读写

如上图所示,I2C开始传输时,第一个字节的前7bit是地址信息(7位地址器件),第8bit是操作标识,为“0”时表示写操作,为“1”时表示读操作,第9个时钟周期是应答信号ACK,低有效,高电平表示无应答,传输终止。在上图中还可以看出,正常情况下,写操作是I2C主设备方发起终止操作的,而读操作时,I2C主控制器在接收完最后一个数据后,不对从设备进行应答,传输终止。

I2C数据总线SDA是在时钟为高时有效,在时钟SCL为高期间,SDA如果发生了电平变化就会终止或重启I2C中线,所以我们在数据传输过程中,要在SCL为低的时候去更改SDA的电平。

总线信号时序

再次总结下总线的各种时序状态:

  • 总线空闲状态:SDA 和 SCL 两条信号线都处于高电平,即总线上所有的器件都释放总线,两条信号线各自的上拉电阻把电平拉高;
  • 启动信号 START:信号 SCL 保持高电平,数据信号SDA的电平被拉低(即负跳变)。启动信号必须是跳变信号,而且在建立该信号前必修保证总线处于空闲状态;
  • 停止信号 STOP:时钟信号SCL保持高电平,数据线被释放,使得SDA返回高电平(即正跳变),停止信号也必须是跳变信号。
  • 数据传送:SCL 线呈现高电平期间,SDA 线上的电平必须保持稳定,低电平表示 0 (此时的线电压为地电压),高电平表示1(此时的电压由元器件的VDD决定)。只有在 SCL 线为低电平期间,SDA 上的电平允许变化。
  • 应答信号 ACK:I2C 总线的数据都是以字节( 8 位)的方式传送的,发送器件每发送一个字节之后,在时钟的第9个脉冲期间释放数据总线,由接收器发送一个ACK(把数据总线的电平拉低)来表示数据成功接收。
  • 无应答信号 NACK:在时钟的第 9 个脉冲期间发送器释放数据总线,接收器不拉低数据总线表示一个 NACK,NACK 有两种用途:
    a. 一般表示接收器未成功接收数据字节;
    b. 当接收器是主控器时,它收到最后一个字节后,应发送一个 NACK 信号,以通知被控发送器结束数据发送,并释放总线,以便主控接收器发送一个停止信号 STOP。

起始信号是必需的,结束信号和应答信号,都可以不要。

模拟 I2C

目前大部分 MCU 都带有 I2C 总线接口,但是芯片自带的 I2C 也有两个问题,一个是移植性较差不够通用,另外部分 MCU 不带 I2C 还是得要模拟的方式,以及一些芯片设计的 I2C 据说是存在问题的,如: stm32 的 I2C 不够稳定,efm32 的 I2C 不够节能等等。这边建议,在权衡 I2C 占用的 CPU 资源是否可以承受后,再做选择。

IIC 选用 IO 模式

通过上面可以知道,IIC 在通信过程中,主设备SCL始终是保持发送状态,因此这边我们可以将 SCL 设置为推挽或者开漏输出。推挽的输出能力较强,开漏输出则需要加上拉电阻,而一般 IIC 的推荐电路就是需要人为增加了上拉电阻的,因此在考虑功耗等一些情况下,可能开漏输出更好。

而问题的关键是 SDA 到底设置为什么模式?

网上参考资料众说纷纭,这边以我自己做实验的亲身感受为例,我推荐在作为输出时将这个脚配置为开漏输出模式;在 MCU 需要接收信号的时候,如果 MCU 本身没有限制,或者不需要精简代码的话,则将其设置为输入模式。详解:

将 SDA 直接设置为开漏模式:

这种方法一般用于之前程序的很多单片机中,开漏因为需要接上拉电阻才能改变电平,且具有线与的特性,通过读 IO 口的状态,即便是设置成了开漏输出,但仍然可以读取IO的数据状态,可以说一劳永逸,一种模式两种用途,虽然目前试过大多数MCU通过这种方式也都可以读取,但毕竟是输出模式,万一以后的设计更改了,无法读取了,则有潜在性的问题。

将 SDA 设置位推挽输出模式进行输出,需要读取数据时转换成输入模式:

最近在不少地方看到用这种方式来对 IIC 器件进行读取,感觉较为灵活,之前也将开漏转换成了这种模式。但最近实验过程中发现了不少问题,在 SDA 发送数据结束后,等待应答的这个过程中,使用推挽模式,有明显的短路风险。这段时间虽然时间很短,且接下来会立即将推挽切换为输入模式,但在单步执行测试的过程中,实际会出现很高的电流,疑似推挽的高电平输出引起了断路,用这种模式必须较为谨慎,注意各种延时时间,千万不能让器件断路烧坏。在使用一个 IIC 通信的 AD 芯片过程中,一段时间后出现测量值不准,疑似可能是这种原因引起的。

SCL 使用开漏输出模式,SDA 使用开漏输出+输入模式:

通过上面的讨论,最终个人认为 IIC 通信过程中:SCL 使用开漏输出模式,SDA 使用开漏输出发送数据,使用输入模式读取数据,这种方法较为合理。值得注意:SDA 需要模式切换,IIC 的延迟时间无需过长,否则影响效率,这些根据手册中的标准执行。

以下以 EFM32 为例,给出参考代码,这边的代码只需要修改部分宏定义,就可以直接移植到 STM32 等其他单片机上。

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
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
/* iic相关引脚定义 */
#define IIC_SDA_PORT gpioPortA
#define IIC_SDA_PIN 0
#define IIC_SCL_PORT gpioPortA
#define IIC_SCL_PIN 1
/* iic相关功能函数定义 */
//I2C_SDA PC0 --> IIC_SDA=1;
#define IIC_SDA_HIGH() GPIO_PinOutSet(IIC_SDA_PORT, IIC_SDA_PIN)
//I2C_SCL PC1 --> IIC_SCL=1;
#define IIC_SCL_HIGH() GPIO_PinOutSet(IIC_SCL_PORT, IIC_SCL_PIN)
//I2C_SDA PC0 --> IIC_SDA=0;
#define IIC_SDA_LOW() GPIO_PinOutClear(IIC_SDA_PORT, IIC_SDA_PIN)
//I2C_SCL PC1 --> IIC_SCL=0;
#define IIC_SCL_LOW() GPIO_PinOutClear(IIC_SCL_PORT, IIC_SCL_PIN)
//设置SDA为输入
#define IIC_SDA_DISABLE() GPIO_PinModeSet(IIC_SDA_PORT, IIC_SDA_PIN, gpioModeDisabled, 0)
#define IIC_SCL_DISABLE() GPIO_PinModeSet(IIC_SCL_PORT, IIC_SCL_PIN, gpioModeDisabled, 0)
#define IIC_SCL_SET_OUT() GPIO_PinModeSet(IIC_SCL_PORT, IIC_SCL_PIN, gpioModePushPull, 0)
//设置为推挽输出模式
//#define IIC_SDA_SET_OUT() GPIO_PinModeSet(IIC_SDA_PORT, IIC_SDA_PIN, gpioModePushPull, 0)
//设置为推开漏出模式
#define IIC_SDA_SET_OUT() GPIO_PinModeSet(IIC_SDA_PORT, IIC_SDA_PIN, gpioModeWiredAndDrive, 0)
#define IIC_SDA_SET_IN() GPIO_PinModeSet(IIC_SDA_PORT, IIC_SDA_PIN, gpioModeInput, 0)
#define IIC_SDA_INPUT() GPIO_PinInGet(IIC_SDA_PORT, IIC_SDA_PIN)
/* 初始化IIC */
void IIC_Init(void)
{
//设置为推挽输出模式
IIC_SDA_SET_OUT();
IIC_SCL_SET_OUT();
//I2C_SDA PC0 --> IIC_SDA=1;
IIC_SDA_HIGH();
//I2C_SCL PC1 --> IIC_SCL=1;
IIC_SCL_HIGH();
}
/* 产生IIC起始信号 */
void IIC_Start(void)
{
u8 i;
//IIC_SDA设置为推挽输出
IIC_SDA_SET_OUT();
//I2C_SDA PC0 --> IIC_SDA=1;
IIC_SDA_HIGH();
//I2C_SCL PC1 --> IIC_SCL=1;
IIC_SCL_HIGH();
i = 5;
while(i--); //setup time for stop 4us
//I2C_SDA PC0 --> IIC_SDA=0;
IIC_SDA_LOW();
i = 5;
while(i--); //setup time for stop 4us
//I2C_SCL PC1 --> IIC_SCL=0;
IIC_SCL_LOW();
}
//产生IIC停止信号
void IIC_Stop(void)
{
u8 i;
//IIC_SDA设置为推挽输出
IIC_SDA_SET_OUT();
//I2C_SCL PC1 --> IIC_SCL=0;
IIC_SCL_LOW();
//I2C_SDA PC0 --> IIC_SDA=0;
IIC_SDA_LOW();
i = 5;
while(i--); //setup time for stop 4us
//I2C_SCL PC1 --> IIC_SCL=1;
IIC_SCL_HIGH();
//I2C_SDA PC0 --> IIC_SDA=1;
IIC_SDA_HIGH();
i = 5;
while(i--); //setup time for stop 4us
}
//主机接收从机应答信号
//等待应答信号到来
//返回值:1,接收应答失败
// 0,接收应答成功
u8 IIC_Wait_Ack(void)
{
u8 ucErrTime=0;
//SDA设置为输入
IIC_SDA_SET_IN();
//I2C_SDA PC0 --> IIC_SDA=1;
IIC_SDA_HIGH();
__nop();
//I2C_SCL PC1 --> IIC_SCL=1;
IIC_SCL_HIGH();
__nop();
while(IIC_SDA_INPUT())
{
ucErrTime++;
if(ucErrTime>250)
{
IIC_Stop();
return 1;
}
}
//I2C_SCL PC1 --> IIC_SCL=0;
IIC_SCL_LOW();
return 0;
}
//主机发送给从机应答信号
//产生ACK应答
void IIC_Ack(void)
{
//I2C_SCL PC1 --> IIC_SCL=0;
IIC_SCL_LOW();
//IIC_SDA设置为推挽输出
IIC_SDA_SET_OUT();
//I2C_SDA PC0 --> IIC_SDA=0;
IIC_SDA_LOW();
__nop();
//I2C_SCL PC1 --> IIC_SCL=1;
IIC_SCL_HIGH();
__nop();
//I2C_SCL PC1 --> IIC_SCL=0;
IIC_SCL_LOW();
}
//主机发送给从机应答信号
//不产生ACK应答
void IIC_NAck(void)
{
//I2C_SCL PC1 --> IIC_SCL=0;
IIC_SCL_LOW();
//IIC_SDA设置为推挽输出
IIC_SDA_SET_OUT();
//I2C_SDA PC0 --> IIC_SDA=1;
IIC_SDA_HIGH();
__nop();
//I2C_SCL PC1 --> IIC_SCL=1;
IIC_SCL_HIGH();
__nop();
//I2C_SCL PC1 --> IIC_SCL=0;
IIC_SCL_LOW();
}
//IIC发送一个字节
//返回从机有无应答
//1,有应答
//0,无应答
void IIC_Send_Byte(u8 txd)
{
u8 t;
//IIC_SDA设置为推挽输出
IIC_SDA_SET_OUT();
//I2C_SCL PC1 --> IIC_SCL=0;
IIC_SCL_LOW();
for(t=0;t<8;t++)
{
//I2C_SDA PC0 --> IIC_SDA输出数据
if (((txd&0x80)>>7) & 1)
{
IIC_SDA_HIGH();
}
else
{
IIC_SDA_LOW();
}
txd<<=1;
__nop(); //对TEA5767这三个延时都是必须的
//I2C_SCL PC1 --> IIC_SCL=1;
IIC_SCL_HIGH();
__nop();
//I2C_SCL PC1 --> IIC_SCL=0;
IIC_SCL_LOW();
__nop();
}
}
//读1个字节,ack=1时,发送ACK,ack=0,发送nACK
u8 IIC_Read_Byte(u8 ack)
{
u8 i, receive=0;
//SDA设置为输入
IIC_SDA_SET_IN();
for(i = 0; i < 8; i++)
{
//I2C_SCL PC1 --> IIC_SCL=0;
IIC_SCL_LOW();
__nop();
//I2C_SCL PC1 --> IIC_SCL=1;
IIC_SCL_HIGH();
receive <<= 1;
if(IIC_SDA_INPUT())
receive++;
__nop();
}
__nop();
ack ? IIC_Ack() : IIC_NAck();
return receive;
}

自带 I2C

这边仅以 stm32 平台通过 I2C 总线读取 EEPROM 为例,说明下如何通过芯片自带 I2C 功能来通信。

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
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
#define EEP_Firstpage 0x00
u8 I2c_Buf_Write[256];
u8 I2c_Buf_Read[256];
static void I2C_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* 使能与 I2C1 有关的时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1,ENABLE);
/* PB6-I2C1_SCL、PB7-I2C1_SDA*/
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 开漏输出
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
static void I2C_Mode_Configu(void)
{
I2C_InitTypeDef I2C_InitStructure;
/* I2C 配置 */
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C;
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2;
I2C_InitStructure.I2C_OwnAddress1 =I2C1_OWN_ADDRESS7;
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable ;
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
I2C_InitStructure.I2C_ClockSpeed = I2C_Speed;
/* 使能 I2C1 */
I2C_Cmd(I2C1, ENABLE);
/* I2C1 初始化 */
I2C_Init(I2C1, &I2C_InitStructure);
}
void I2C_EE_Init(void)
{
I2C_GPIO_Config();
I2C_Mode_Configu();
/* 选择 EEPROM Block0 来写入 */
EEPROM_ADDRESS = EEPROM_Block0_ADDRESS;
}
void I2C_EE_BufferWrite(u8* pBuffer, u8 WriteAddr, u16 NumByteToWrite)
{
u8 NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0;
Addr = WriteAddr % I2C_PageSize;
count = I2C_PageSize - Addr;
NumOfPage = NumByteToWrite / I2C_PageSize;
NumOfSingle = NumByteToWrite % I2C_PageSize;
I2C_EE_PageWrite(pBuffer, WriteAddr, NumOfSingle);
I2C_EE_WaitEepromStandbyState();
}
void I2C_EE_PageWrite(u8* pBuffer, u8 WriteAddr, u8 NumByteToWrite)
{
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); // Added by Najoua 27/08/2008
/* Send START condition */
I2C_GenerateSTART(I2C1, ENABLE);
/* Test on EV5 and clear it */
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
/* Send EEPROM address for write */
I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);
/* Test on EV6 and clear it */
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
/* Send the EEPROM's internal address to write to */
I2C_SendData(I2C1, WriteAddr);
/* Test on EV8 and clear it */
while(! I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
/* While there is data to be written */
while(NumByteToWrite--)
{
/* Send the current byte */
I2C_SendData(I2C1, *pBuffer);
/* Point to the next byte to be written */
pBuffer++;
/* Test on EV8 and clear it */
while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
}
/* Send STOP condition */
I2C_GenerateSTOP(I2C1, ENABLE);
}
void I2C_EE_WaitEepromStandbyState(void)
{
vu16 SR1_Tmp = 0;
do
{
/* Send START condition */
I2C_GenerateSTART(I2C1, ENABLE);
/* Read I2C1 SR1 register */
SR1_Tmp = I2C_ReadRegister(I2C1, I2C_Register_SR1);
/* Send EEPROM address for write */
I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);
}while(!(I2C_ReadRegister(I2C1, I2C_Register_SR1) & 0x0002));
/* Clear AF flag */
I2C_ClearFlag(I2C1, I2C_FLAG_AF);
/* STOP condition */
I2C_GenerateSTOP(I2C1, ENABLE);
}
void I2C_EE_BufferRead(u8* pBuffer, u8 ReadAddr, u16 NumByteToRead)
{
//*((u8 *)0x4001080c) |=0x80;
while(I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY)); // Added by Najoua 27/08/2008
/* Send START condition */
I2C_GenerateSTART(I2C1, ENABLE);
//*((u8 *)0x4001080c) &=~0x80;
/* Test on EV5 and clear it */
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
/* Send EEPROM address for write */
I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Transmitter);
/* Test on EV6 and clear it */
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
/* Clear EV6 by setting again the PE bit */
I2C_Cmd(I2C1, ENABLE);
/* Send the EEPROM's internal address to write to */
I2C_SendData(I2C1, ReadAddr);
/* Test on EV8 and clear it */
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
/* Send STRAT condition a second time */
I2C_GenerateSTART(I2C1, ENABLE);
/* Test on EV5 and clear it */
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));
/* Send EEPROM address for read */
I2C_Send7bitAddress(I2C1, EEPROM_ADDRESS, I2C_Direction_Receiver);
/* Test on EV6 and clear it */
while(!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
/* While there is data to be read */
while(NumByteToRead)
{
if(NumByteToRead == 1)
{
/* Disable Acknowledgement */
I2C_AcknowledgeConfig(I2C1, DISABLE);
/* Send STOP Condition */
I2C_GenerateSTOP(I2C1, ENABLE);
}
/* Test on EV7 and clear it */
if(I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED))
{
/* Read a byte from the EEPROM */
*pBuffer = I2C_ReceiveData(I2C1);
/* Point to the next location where the byte read will be saved */
pBuffer++;
/* Decrement the read bytes counter */
NumByteToRead--;
}
}
/* Enable Acknowledgement to be ready for another reception */
I2C_AcknowledgeConfig(I2C1, ENABLE);
}
void I2C_Test(void)
{
u16 i;
printf("写入的数据\n\r");
for ( i=0; i<=255; i++ ) //填充缓冲
{
I2c_Buf_Write[i] = i;
}
//将I2c_Buf_Write中顺序递增的数据写入EERPOM中
I2C_EE_BufferWrite(I2c_Buf_Write, EEP_Firstpage, 256);
//将EEPROM读出数据顺序保持到I2c_Buf_Read中
I2C_EE_BufferRead(I2c_Buf_Read, EEP_Firstpage, 256);
}

小结

我们在根据芯片手册来写 I2C 驱动时,尤其需要注意以下几点:

  • 传输数据的大小端模式,这边的大小端是位大小端而不是字节。
  • 时钟周期,如果通讯出现问题,试着用示波器看 CLK 波形得出通讯速度,以及看下通讯设备能够支持的速度。
  • 看下应答信号是否设置成功。

参考链接:
https://blog.csdn.net/luckywang1103/article/details/17549739
https://zhuanlan.zhihu.com/p/26579936
http://wenku.baidu.com/view/2a0a7f9869dc5022abea001d.html
http://blog.csdn.net/zmq5411/article/details/6085740
https://zh.wikipedia.org/wiki/I%C2%B2C
http://blog.sina.com.cn/s/blog_626998030102vfjx.html