关于 I2C 通信的内容主要分为 两大块。
- 第一块:介绍协议规则,然后用软件模拟的形式来实现协议。
- 第二块:介绍 STM32 的 I2C 外设,然后用硬件来实现协议。
因为 I2C 是同步时序,软件模拟协议也非常方便,目前也存在很多软件模拟 I2C 的代码,所以我们先介绍软件 I2C,再介绍硬件 I2C,至于哪个更方便,各自的优势和劣势,等介绍完后你应该会自有定论。
本节我们会使用 MPU6050 陀螺仪、加速器传感器来学习 I2C,大家可以对比一下 I2C 在不同器件的应用有什么异同,也可以加深大家对 I2C 协议的理解。
I2C 的设计背景
在学 I2C 之前,我们已经学习了串口通信,串口通信,就是从 TX 引脚向 RX 引脚发送数据流,数据流以字节为单位,我们可以组合多个字节,变成多字节的数据包传输。另外串口通信的设计是:一条发送线、一条接收线,没有时钟线的异步全双工的协议。这是我们学习串口通信的时候了解到的。
假如一个公司开发出了一款芯片,可以干很多事情,比如 AD 转换、温湿度测量、姿态测量等等,像我们单片机一样,这个芯片里的众多外设,也都是通过读写寄存器来控制运行的,寄存器本身也是存储器的一种,这个芯片所有的寄存也都是被分配到了一个线性的存储空间,如果我们想要读写寄存器来控制硬件电路,我们就至少需要定义两个字节的数据,一个字节是我们要读写哪个寄存器,也就是指定寄存器的地址,另一个字节就是这个地址下存储器存的内容。写入内容就是控制电路,读出内容就是获取电路状态,这整个流程和我们单片机 CPU 操作外设的原理是一样的。
那现在问题来了,单片机读写自己的寄存器,可以直接通过内部的数据总线来实现,直接用指针操作就行,不需要我们操心;但是现在这个模块的寄存器在单片机的外面,你要是直接把单片机内部的数据总线拽出来,把两个芯片合为一体,那可能不太现实。所以现在公司要求你给它设计一种通信协议,在单片机和外部模块连接少量的几根线,实现单片机读写外部模块寄存器的功能,这时你可能会想,这不太简单了,我们就用串口的数据包通信就可以完成任务,比如我就用 HEX 数据包,定义一个 3 个字节的数据包,从单片机向外挂模块发过去,第一个字节,表示读写,发送 0,表示这是一个写数据包,发送 1,表示这是一个读数据包;第二个字节,表示读写的地址;第三个字节,表示写入的数据。比如我发送数据包为 0x00,0x06,0xAA,这就表示在 0x06 地址下写入 0xAA,模块收到之后,就执行这个写入操作;如果我发送数据包为 0x01,0x06,0x00,这就表示我要读取 0x06 地址下的数据,注意,这个读的数据包第 3 个字节无效。模块收到之后,就要再给我发送一个字节,返回 0x06 地址下的数据,这样就行了,是不是完美完成任务啊。
但是呢,这个公司对这个通信协议的要求非常多。其中,要求 1:目前串口这个设计,是一个需要两根通信线的全双工协议,但是可以明显地发现,我们这个操作流程是一种基于对话的形式来进行的,我们在整个过程中并不需要同时进行发送和接收,发送的时候就不需要接收,接收的时候就不需要发送,这样就会导致,始终存在一条信号线处于空闲状态,这就是资源的浪费。所以要求 1 就是 删掉一根通信线,只能在同一根线上进行发送和接收,也就是把全双工变成半双工。要求 2:我们这个协议并没有一个应答机制,也就是单片机发送了一个数据,对方有没有收到,单片机是完全不了解的,所以为了安全起见,公司要求增加应答机制,要求每发送一个字节,对方都要给我一个应答;每接收一个字节,我也要给对方一个应答。要求 3:公司说你这一根线只能接一个模块,不给力,它要求,你这一根线上能同时接多个模块,单片机可以指定,和任意一个模块进行通信,同时单片机在跟某个模块进行通信时,其他模块不能对正常的通信产生干扰。要求 4:这个串口是异步的时序,也就是发送方和接收方约定的传输速率是非常严格的,时钟不能有过大的偏差,也不能说是,在传输过程中,单片机有点事,进中断了,这个时序能不能暂停一下啊,对于异步时序来说,这是不行的,你单片机一个字节发一半暂停了,接收方可是不知道的,它仍然会按照原来的那个约定的速率读取,这就会导致传输出错。所以异步时序的缺点就是:非常依赖硬件外设的支持,必须要有 USART 电路才能方便的使用,如果没有 USART 硬件电路的支持,那么串口是很难用软件来模拟的,虽然说软件模拟串口通信也是行的通的,但是由于异步时序对时间要求很严格,一般我们很少用软件来模拟串口通信,所以公司的要求是,你要把这个协议改成同步的协议,另外加一条时钟线来指导对方读写,由于存在时钟线,对传输的时间要求就不高了,单片机也可以随时暂停传输,去处理其他事情,因为暂停传输的同时,时钟线也暂停了,所以传输双方都能定格在暂停的时刻,可以过一段时间再来继续,不会对传输造成影响,这就是同步时序的好处。使用同步时序就可以极大地降低单片机对硬件电路的依赖,即使没有硬件电路的支持,也可以很方便的用软件手动翻转电平来实现通信。(比如 51 单片机里没有 I2C 的硬件外设,但是同样不影响 51 单片机进行软件模拟的 I2C 通信)异步时序的好处就是省一根时钟线,节省资源;缺点就是对时间要求严格,对硬件电路的依赖比较严重。同步时序的好处就是反过来,对时间要求不严格,对硬件电路不怎么依赖。在一些低端单片机,没有硬件资源的情况下,也很容易使用软件来模拟时序,缺点就是多一根时钟线。这就是同步和异步的区别。
公司考虑到这个协议要主打下沉市场,所以它需要一个同步的协议。
其实通信协议就是一个很灵活的设计方案,并没有很严格的要求,说它必须是这样;只要你的设计能实现项目要求、符合电路原理、性能和稳定性好,那你的设计就是好设计。
项目要求:
- 最基本的任务是:通过通信线,实现单片机读写外挂模块寄存器的功能,其中至少要实现,在指定的位置写寄存器和在指定的位置读寄存器,这两个功能,实现了读写寄存器,就实现了对这个外挂模块的完全控制。
- 另外刚才说的公司的 4 点要求,也别忘了,必须要满足公司的要求才行。
1. I2C 通信简介
1. 1 I2C 的基本功能
I2C(Inter IC Bus,缩写 IIC / I2C,一般习惯称为 I2C)是由 Philips 公司开发的一种通用数据总线
目前应用还是非常广泛的,已经有很多模块都使用了 I2C 的协议标准了。比如我们套件里的 MPU6050 模块,可以进行姿态测量,使用了 I2C 通信协议;我们套件里的 OLED 模块,可以显示字符、图片等信息,也是 I2C 协议;AT24C02 存储器模块(51 单片机中学习 I2C 的模块);DS3231 实时时钟模块,也是使用 I2C 通信;等等。还有很多模块,都支持 I2C 通信,使用了这个通用的协议,对于我们开发者来说,就非常方便了是吧,同样的协议在不同的硬件上,操作方法都是极为相似的,学会了其中一个硬件,再学其他的硬件就很容易了。
两根通信线:SCL(Serial Clock)、SDA(Serial Data)
那 I2C 的标志性引脚,就是两根通信线,SCL,全称 Serial Clock,串行时钟线;SDA,全称 Serial Data,串行数据线。使用 I2C 通信的器件,都有 SCL 和 SDA 这两个引脚。那 SCL 时钟线,就满足了刚才公司提出的要求 4,要使用同步的时序,降低对硬件的依赖,同时同步的时序稳定性也比异步时序更高。然后只有一根 SDA 数据线,就满足了公司提出的要求 1,变全双工为半双工,一根线兼具发送和接收,最大化利用资源,所以我们可以看到下一条。
同步,半双工
带数据应答
满足了设计要求 2
支持总线挂载多设备(一主多从、多主多从)
满足了设计要求 3。并且这个挂载多设备,支持两种模型,一主多从和多主多从,一主多从的意思就是,单片机作为主机,主导 I2C 总线的运行,挂载在 I2C 总线的所有外设模块都是从机,从机只有被主机点名之后才能控制 I2C 总线,不能在未经允许的情况下去碰 I2C 总线,防止冲突。这就像在教室里,老师是主机,主导课程的进行,所有学生都是从机,所有从机可以同时被动的听老师讲课,但是从机只有在被老师点名之后才能说话,不可以在未经允许的情况下说话,这样课堂才能有条不紊的进行,这就是一主多从的模式。我们使用 I2C 的绝大多数场景都是一主多从的模式,一个单片机作为主机,挂载一个或多个模块作为从机,另外 I2C 其实还支持多主多从的模型,也就是多个主机。多主多从的模型,在总线上任何一个模块都可以主动跳出来,说,接下来我就是主机,你们都得听我的,这就像是在教室里,老师正在讲课,突然有个学生站起来说,老师打断一下,接下来让我来说,所有同学听我指挥,但是,同一个时间只能有一个人说话,这时就相当于发生了总线冲突,在总线冲突时,I2C 协议会进行仲裁,仲裁胜利的一方取得总线控制权,失败的一方自动变回从机。当然由于时钟线也是由主机控制的,所以在多主机的模型下,还要进行时钟同步,多主机的情况下,协议是比较复杂的,大家感兴趣可以自行了。我们本节仅使用一主多从的模型,多主多从的部分不做要求。
1.2 I2C 硬件规定
功能实现原理:
作为一个通信协议,他必须要在硬件和软件上都作出规定。硬件上的规定,就是你的电路应该如何连接,端口的输入输出模式都是啥样的,这些东西;软件上的规定,就是你的时序是怎么定义的,字节如何传输,高位先行还是低位先行,一个完整的时序有哪些部分构成,这些东西。硬件的规定和软件的规定配合起来就是一个完整的通信协议。
也就是硬件电路部分。
这个图就是 I2C 的一个典型的电路模型,是一个一主多从的模型。左边 CPU 就是我们的单片机,作为总线的主机,主机的权力很大,包括对 SCL 线的完全控制,任何时候,都是主机完全掌握 SCL 线,另外在空闲状态下,主机可以主动发起对 SDA 的控制,只有在从机发送数据和从机应答的时候,主机才会转交 SDA 的控制权给从机,这就是主机的权力。
下面这一系列都是被控 IC,也就是挂载在 I2C 总线上的从机,这些从机可以是姿态传感器、OLED、存储器、时钟模块等等,从机的权力比较小,对于 SCL 时钟线,在任何时刻都只能被动的读取,从机不允许控制 SCL 线,对于 SDA 数据线,从机不允许主动发起对 SDA 的控制,只有在主机发送读取从机命令后,或者从机应答的时候,从机才能短暂的取得 SDA 的控制权,这就是一主多从模型中协议的规定。
然后看接线要求:
- 所有 I2C 设备的 SCL 连在一起,SDA 连在一起
在图里,主机 SCL 线一条拽出来,所有从机的 SCL 都接在这上面;主机 SDA 线也是一样,拽出来,所有从机的 SDA 都接在这上面,这就是 SCL 和 SDA 的接线方式。
- 设备的SCL和SDA均要配置成开漏输出模式
- SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右
先忽略两个电阻,假设我们就这样连接,那如何规定每个设备 SCL 和 SDA 的输入输出模式呢?SCL 应该好规定,因为现在是一主多从,主机拥有 SCL 的绝对控制权,所以主机的 SCL 应该配置成推挽输出,所有从机的 SCL 都配置成浮空输入或者上拉输入,数据流向是,主机发送,所有从机接收,这没问题。但是到 SDA 线这里,就比较麻烦了。因为这是半双工的协议,所以主机的 SDA,在发送的时候是输出,在接收的时候是输入,同样,从机的 SDA 也会在输入和输出之间反复切换,如果你能协调好输入输出的切换时机,那其实也没问题;但是这样做,如果总线时序没协调好,极有可能发生两个引脚同时处于输出的状态,如果这时又正好是一个输出高电平,一个输出低电平,那这个状态就是电源短路,这个状态是要极力避免的,所以为了避免总线没协调好导致电源短路这个问题,I2C 的设计是,禁止所有设备输出强上拉的高电平,采用外置弱上拉电阻加开漏输出的电路结构,这两点规定就是上面的这两条,设备的SCL和SDA均要配置成开漏输出模式 和 SCL和SDA各添加一个上拉电阻,阻值一般为4.7KΩ左右。
对应下面这个图呢,就是这样。
所有的设备,包括 CPU 和被控 IC,它引脚的内部结构都是上图这样的,左边这一块是 SCL 的结构,这里的 SCLK 就是 SCL 的意思,右边这一块是 SDA 的机构,这里的 DATA 就是 SDA 的意思。
首先引脚的信号进来,都可以通过一个数据缓冲器或者是施密特触发器,进行输入,因为输入对电路没有任何影响,所以任何设备在任何时刻都是可以输入的;但是在输出的这部分,采用的是开漏输出的配置,正常的推挽输出是上面一个开关管接到正极,下面一个开关管接到负极,上面导通,输出高电平,下面导通,输出低电平,因为这是通过开关管直接接到正极和负极的,所以这个是强上拉和强下拉的模式,而开漏输出呢,就是去掉强上拉的开关管,输出低电平时,下管导通,是强下拉,输出高电平时,下管断开,但是没有上管了,此时引脚处于浮空的状态,这就是开漏输出,和图示是一样的,输出低电平,开关管导通,引脚直接接地,是强下拉;输出高电平,开关管断开,引脚说明都不接,处于浮空状态。这样的话,所有的设备都只能输出低电平而不能输出高电平,为了避免高电平造成的引脚浮空,这时就需要在总线外面,SCL 和 SDA 各外置一个上拉电阻,这是通过一个电阻拉到高电平的,所以这是一个弱上拉,用我们之前的弹簧和杆子的模型来解释就是,SCL 或 SDA 就是一根杆子,为了防止有人向上推杆子,有人向下拉杆子,造成冲突,我们就规定,所有人不准向上推杆子,只能选择向下拉杆子或者放手,然后,我们再外置一根弹簧向上拉,你要输出低电平,就往下拽,这根弹簧肯定拽不赢你,所以弹簧被拉伸,杆子处于低电平状态,你要输出高电平,就放手,杆子在弹簧的压力下,回弹到高电平,这就是一个弱上拉的高电平,但是完全不影响数据传输,这样做有什么好处呢?第一,完全杜绝了电源短路现象,保证电路安全,你看所有人无论怎么拉杆子或者放手,杆子都不会处于一个被同时强拉和强推的状态,即使有多个人同时向下拉杆子,也没问题。第二,避免了引脚模式的频繁切换,开漏加弱上拉的模式,同时还兼具了输入和输出的功能,你要是想输出,就去拉杆子或放手,操作杆子变化就行了,你要是想输入,就直接放手,然后观察杆子高低就行了。因为开漏模式下,输入高电平就相当于断开引脚,所以在输入之前,可以直接输出高电平,不需要再切换成输入模式了。第三,就是这个模式会有一个“线与”的现象,就是只要有任意一个或多个设备输出了低电平,总线就处于低电平,只有所有的设备都输出高电平,总线才处于高电平。I2C 可以利用这个电路特性,执行多主机模式下的时钟同步和总线仲裁,所以这里 SCL 虽然在一主多从模式下可以用推挽输出,但是它仍然采用了开漏加上拉输出的模式,因为在多主机模式下会利用到这个特征,以上就是 I2C 的硬件电路设计。
1.3 I2C 软件
接下来,我们就要来学习软件,也就是时序的设计了。首先我们来学习下 I2C 规定的一些时序基本单元。
首先就是起始条件:SCL高电平期间,SDA从高电平切换到低电平。
看一下上图的左边,在 I2C 总线处于空闲状态时,SCL 和 SDA 都处于高电平状态,也就是没有任何一个设备去碰 SCL 和 SDA,SCL 和 SDA 有外挂的上拉电阻拉高至高电平,总线处于平静的高电平状态。当主机需要进行数据收发时,首先就要打破总线的宁静,产生一个起始条件。这个起始条件就是 SCL 处于高电平不去动它,然后把 SDA 拽下来,产生一个下降沿,当从机捕获到这个 SCL 高电平,SDA 下降沿信号时,就会进行自身的复位,等待主机的召唤,然后在 SDA 下降沿之后,主机要再把 SCL 拽下来,拽下 SCL,一方面是占用这个总线,另一方面也是为了方便我们这些基本单元的拼接,就是我们之后会保证,除了起始和终止条件,每个时序单元的 SCL 都是以低电平开始,低电平结束,这样这些单元拼接起来,SCL 才能续的上是吧。
终止条件:SCL高电平期间,SDA从低电平切换到高电平
然后继续看,终止条件是 …。也就是这样, SCL 先放手,回弹到高电平,SDA 再放手,回弹高电平,产生一个上升沿,这个上升沿触发终止条件,同时终止条件之后,SCL 和 SDA 都是高电平,回归到最初的平静状态,这个起始条件和终止条件就类似串口时序里的起始位和停止位。一个完整的数据帧,总是以起始条件开始、终止条件结束,另外,起始和终止,都是由主机产生的,从机不允许产生起始和终止。所以在总线空闲状态时,从机必须始终双手放开,不允许主动跳出来,去碰总线,如果允许的话,那就是多主机模型了,不在本机的讨论范围之内。
这就是起始条件和终止条件。
接着继续看,在起始条件之后,这时就可以紧跟着一个发送一个字节的时序单元。如何发送一个字节呢?就是 SCL 低电平期间,主机将数据位依次放到 SDA 线上(高位先行),然后释放 SCL,从机将在 SCL 高电平期间读取数据位,所以 SCL 高电平期间 SDA 不允许有数据变化,依次循环上述过程 8 次,即可发送一个字节。图示就是下面这样。
起始条件之后,第一个字节,也必须是主机发送的,主机如何发送呢?就是最开始,SCL 低电平,主机如果想发送 0,就拉低 SDA 到低电平,如果想发送 1,就放手,SDA 回弹到高电平,在 SCL 低电平期间,允许改变 SDA 的电平,当这一位放好之后,主机就松手时钟线,SCL 回弹到高电平,在高电平期间,是从机读取 SDA 的时候,所以高电平期间,SDA 不允许变化,SCL 处于高电平之后,从机需要尽快的读取 SDA,一般都是在上升沿这一时刻,从机就已经读取完成了,因为时钟是主机控制的,从机并不知道什么时候就会产生下降沿了,你从机要是磨磨唧唧的,主机可不会等你的。所以从机在上升沿时,就会立刻把数据读走,那主机在放手 SCL 一段时间后,就可以继续拉低 SCL,传输下一位了,主机也需要在 SCL 下降沿之后尽快把数据放在 SDA 上,但是主机有时钟的主导权,所以主机并不需要那么着急,只需要在低电平的任意时刻把数据放在 SDA 上就行了,晚点也没关系,数据放完之后,主机再松手 SCL,SCL 高电平,从机读取这一位,就这样的流程。主机拉低 SCL,把数据放在 SDA 上,主机松开 SCL,从机读取 SDA 的数据,在 SCL 的同步下,依次进行主机发送和从机接收,循环 8 次,就发送了 8 位数据,也就是一个字节。
另外注意,这里是高位先行,所以第一位是一个字节的最高位 B7,然后依次是次高位 B6,等等。最后发送最低位 B0,这个和串口是不一样的。串口时序是低位先行,这里 I2C 是高位先行,这个注意一下。
另外,由于这里有时钟线进行同步,所以如果主机一个字节发送一半,突然进中断了,不操作 SCL 和 SDA 了,那时序就会在中断的位置不断拉长,SCL 和 SDA 电平都暂停变化,传输也完全暂停,等中断结束后,主机回来继续操作,传输仍然不会出问题,这就是同步时序的好处。
最后就是,由于这整个时序是主机发送一个字节,所以在这个单元里,SCL 和 SDA 全程都由主机掌控,从机只能被动读取。这就是发送一个字节的时序。
然后我们接着看,接收一个字节。
最上面的数据是设备的 ID 号,MPU6050 的 ID 号固定是 0x68,一般我们可以读出这个 ID 号,验证看看是不是 0x68,用来测试 I2C 读取数据的功能是不是正常。另外之前还测试了不同批次的芯片,发现有的芯片 ID 号是 0x98,ID 号可能会有些不同,不过如果数据读出来没问题的话,这也不影响,知道一下就行了。
下面左边 3 个,是加速度传感器的输出数据,分别是 x 轴,y 轴 和 z 轴的加速度,右边 3 个,是陀螺仪传感器的输出数据,分别是 x 轴,y 轴 和 z 轴的角速度。我们可以改变 MPU6050 传感器的姿态,这 6 个数据就会对应变化。