I2C详解--以51单片机和MMA8451Q为例

摘要

I2C(Inter-Integrated Circuit)是由NXP(恩智浦)半导体开发的一种简单的双向两线制总线协议标准。它作为电子行业中最常见的串行通讯协议,虽然书面可以写成I2C,但实际它实际写法是\(I^2C\) 读作[I fang C]。

它只需要2个引脚--SDA(双向数据线)和SCL(时钟线),就可以完成芯片间的通信,这种便捷的应答机制的提出让微控制器,电源、显示、传感器等芯片间能够简单快速的互联互通。

本文详细介绍了I2C的几种传输信号,并以51架构的芯片和三轴加速度传感器为例提供了一套模拟I2C的解决方案。

1.I2C协议

参考知乎文章 一文看懂I2C协议

I2C协议可以工作在以下5种速率模式下,不同的器件可能支持不同的速率。

  • 标准模式(Standard):100kbps
  • 快速模式(Fast):400kbps
  • 快速模式+(Fast-Plus):1Mbps
  • 高速模式(High-speed):3.4Mbps
  • 超快模式(Ultra-Fast):5Mbps(单向传输)

对于I2C协议来说其实只有非常基础的几种信号:起始、停止、数据、应答和非应答信号。

  1. 起始信号:I2C协议规定,SCL处于高电平时,SDA由高到低变化,这种信号是起始信号

  2. 停止信号:SCL处于高电平,SDA由低到高变化,这种信号是停止信号

  3. 数据信号:I2C协议对数据的采样发生在SCL高电平期间,除了起始和停止信号,在数据传输期间,SCL为高电平时,SDA必须保持稳定,不允许改变,在SCL低电平时才可以进行变化。而且一次允许传输的是1个字节的数据量。

  4. 应答信号:应答信号出现在1个字节传输完成之后,即第9个SCL时钟周期内,此时主机需要释放SDA总线,把总线控制权交给从机,由于上拉电阻的作用,此时总线为高电平,如果从机正确的收到了主机发来的数据,会把SDA拉低,表示应答响应。

  5. 非应答信号:当第9个SCL时钟周期时,SDA保持高电平,表示非应答信号,通常在读到数据时不需要从机回应,主机可以不放开总线控制权。

2.I2C读写时序图阅读

在芯片手册中通常可以找到芯片的I2C时序图,下面以MMA8451Q为例,其时序图如下

MMA8451Q时序图

左上角的“<>”中代表着是想要进行的操作,如single-byte read,意思是进行单字节读取。下面的master和slave分别代表主机需要进行的操作和从机需要进行的操作。这里主机指的是主控芯片,从机指的是MMA8451Q。

如果想要从从机这读取单个数据,主机需要先发送一个ST + device address + w,即发送一个起始信号+设备地址数据信号+写信号,其中地址信号是7位的,写信号是1位,加起来就是一个字节的数据信号。随后主机放开控制权,从机会返回一个应答信号,表示通信正常。然后主机将需要读取的寄存器信号地址发送给从机,再收到一个应答,这时候主机需要再发送一个起始信号+设备地址信号+读信号,从机在发送应答后会发送数据给主机,这时候主机收到信号不需要应答,发送一个停止信号即可。

这就是一个完整的读取信息通信过程。

3.I2C读写程序编写

在使用单片机时,通常芯片厂商会配置好硬件I2C,只需要根据例程修改即可使用。但有时可能会出现硬件功能无法正常使用等情况。同时为了更好的了解I2C协议,这里使用模拟I2C来实现通信。

首先需要在硬件原理图中找到I2C通信的两个端口,并将它们初始化为开漏输出模式,注意硬件部分的端口需要加一个上拉电阻,否则无法输出高电平。

为了方便起见,这里使用宏定义将引脚换个名字,我以51架构芯片的P44和P45脚为例,这里请根据自己的主控芯片和硬件原理图进行修改。

1
2
#define I2C_SDA P45
#define I2C_SCL P44

MMA8451的读写地址和寄存器位置也可以在芯片手册中找到。

1
2
3
4
5
6
#define  MMA8451Q_WRITE		    0x38		/*Write Cmd*/
#define MMA8451Q_READ 0x39 /*Read Cmd*/

#define MMA8451Q_CTRL_REG1 0x2A /*control registers*/
#define MMA8451Q_CTRL_REG2 0x2B
#define MMA8451Q_WHO_AM_I 0x0D

下面是示例代码,注意要根据主控芯片和传感器自行修改。

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
/*****************************************************************************
** \brief I2C_Delay
** \param
**
** \return
** \note
*****************************************************************************/
static void I2C_Delay(void)
{
uint16_t i;
for(i=0;i<1;i++);
}

/*****************************************************************************
** \brief I2C_Start
** \param
**
** \return
** \note
*****************************************************************************/
void I2C_Start(void){
I2C_SDA=1;
I2C_SCL=1;
I2C_Delay();
I2C_SDA=0;
I2C_Delay();
I2C_SCL=0;
I2C_Delay();
}
/*****************************************************************************
** \brief I2C_Stop
** \param
**
** \return
** \note
*****************************************************************************/
void I2C_Stop(void){
I2C_SDA=0;
I2C_SCL=1;
I2C_Delay();
I2C_SDA=1;
}

/*****************************************************************************
** \brief I2C_ReadAck
** \param
**
** \return
** \note
*****************************************************************************/
bit I2C_ReadAck() //主机接收应答
{
bit ack=0;
I2C_SDA=1; //释放总线
I2C_Delay();
I2C_SCL=1;
I2C_Delay();
ack=I2C_SDA;
I2C_SCL=0;

return ack;
}
/*****************************************************************************
** \brief I2C_Sendbyte(u8 byte)
** \param
**
** \return
** \note I2C总线主机发送一个字节数据
*****************************************************************************/
void I2C_Sendbyte(u8 byte)//主机发送一个字节的数据
{
u8 i=0;
I2C_Delay();
for(i=0;i<8;i++)
{
I2C_SDA=byte&(0x80>>i);
I2C_Delay();
I2C_SCL=1;
I2C_Delay();
I2C_SCL=0;
}
}
/*****************************************************************************
** \brief I2C_Readbyte
** \param
**
** \return
** \note I2C总线主机接收一个字节数据
*****************************************************************************/
u8 I2C_Readbyte()//主机接收一个字节数据
{
u8 i;
u8 byte=0;
I2C_SDA = 1;//释放总线

P4TRIS &= ~(0x20);//设置为输入模式

for(i=0;i<8;i++)
{
I2C_SCL = 1;
I2C_Delay();
byte <<= 1;
//读取I2C_SDA上的数据
if(I2C_SDA){
byte++;
}
I2C_SCL=0;
I2C_Delay();
}

P4TRIS |= 0x20; //改回输出模式
P4OD |= 0x20; //改为开漏输出

return byte;
}
/*****************************************************************************
** \brief MMA8451Q_write_byte
** \param addr地址 byte数据
**
** \return
** \note 写入一个字节数据
*****************************************************************************/
void MMA8451Q_write_byte(u8 addr, u8 byte)
{
bit ack=0;
I2C_Start();
I2C_Sendbyte(MMA8451Q_WRITE);
ack=I2C_ReadAck();
I2C_Sendbyte(addr);
ack=I2C_ReadAck();
I2C_Sendbyte(byte);
ack=I2C_ReadAck();
I2C_Stop();
}

/*****************************************************************************
** \brief MMA8451Q_read_byte
** \param addr地址 byte数据
**
** \return
** \note 读取一个字节数据
*****************************************************************************/
u8 MMA8451Q_read_byte(u8 addr)
{
u8 byte=0;
bit ack=0;
u8 i=0;
I2C_Start();
I2C_Sendbyte(MMA8451Q_WRITE);
ack = I2C_ReadAck();
I2C_Sendbyte(addr);
ack = I2C_ReadAck();
I2C_Delay();
I2C_Start();
I2C_Sendbyte(MMA8451Q_READ);
ack=I2C_ReadAck();
byte=I2C_Readbyte();
I2C_Stop();
return byte;
}

写完后可以读取传感器中的只读寄存器进行测试。比如WHO_AM_I寄存器,其他传感器要自行查询芯片手册。