STM32-I2C通信协议(二十七)
I2C通信协议我们来一起学习 I2C 通信啊。关于 I2C 通信的内容我主要会分为两大块来讲啊。第一块就是介绍协议规则然后用软件模拟的形式来实现协议啊。第二块就是介绍 STM32 的 I2C 外设然后用硬件来实现协议。因为 I2C 是同步时序啊软件模拟协议也是非常方便目前也存在有很多软件模拟 I2C 的代码所以我们先学软件 I2C再学硬件 I2C 啊。至于哪个更方便啊各自的优势和劣势啊等你学完之后应该会自有定论啊。那 I2C 通信的知识点呢我在 51 单片机的视频里也讲过哈。在 51 单片机那个视频里我们使用的是 AT24C02 这个存储器模块来学习 I2C 的哈。在这个视频呢我们会使用 MPU6050 这个陀螺仪加速度传感器来学习 I2C。大家可以两个视频都看看啊对比一下 I2C C 在不同器件的应用有什么异同啊也可以加深大家对 I2C 协议的理解啊。好那我们先看一下本节课程的程序现象哈。本节课程主要有两个代码一个是软件 I2C 读写 MPU6050另一个是硬件 I2C 读写 MPU6050。两个代码实现的效果是一样的哈。那先看一下这个软件 I2C 读写 MPU6050 的程序现象下载。看一下目前这个程序啊我们通过软件 I2C 通信对 MPU6050 芯片内部的寄存器进行读写。写入到配置寄存器呢就可以对外挂的这个模块进行配置。读出数据寄存器呢就可以获取外挂模块的数据。这就是 I2C 通信的目的。最终我们读出的数据会显示在这个 OID 上。其中最上面这个数据是设备的 ID 号。我们这个 MPU6050 的 ID 号固定为 0x68 哈。一般我们可以读出这个 ID 号验证看看是不是 0x68用来测试 I2C 读取数据的功能。是不是正常啊另外我之前还测试了不同批次的芯片啊发现有的芯片 ID 是 0X98 ID 号可能会有些不同啊。不过如果数据读出来没问题的话这也不影响知道一下就行了。然后下面左边三个是加速度传感器的输出数据分别是 X 轴、 Y 轴和 Z 轴的加速度。右边三个是陀螺仪传感器的输出数据分别是 X 轴、 Y 轴和 Z 轴的角速度。我们可以改变 MPU6050 传感器的姿态哈。这六个数据就会对应变化。至于这个加速度和角速度是什么意思呢等我们介绍这个模块的时候再详细解释啊。那这就是目前这个程序的现象。然后回到 PPT 我们来看一下 I2C 通信。在学 I2C 之前呢我们已经学习了串口通信。串口通信啊就是从 TX 引脚向 RX 引脚发送数据流数据流以字节为单位我们可以组合多个字节变成多字节的数据包传出。另外串口通信的设计是啊一条发送线一条接收线没有时钟线的异步全双工的协议。这是我们学习串口通信的时候了解到的。那现在假如有个大公司找到了你啊出资 1000 万要求你给他设计一个通信协议。这个通信协议是用来干啥的呢大概就是像这里这样。这个公司啊开发出了一款芯片可以干很多事情比如 AD 转换啊、温湿度测量啊、姿态测量等等。像我们单片机一样这个芯片里的众多外设啊也都是通过读写寄存器来控制运行的。寄存器本身也是存储器的一种啊这个芯片所有的寄存器也都是被分配到了一个线性的存储空间。如果我们想要读寄存器来控制硬件电路啊我们就至少需要定义两个字节数据一个字节是我们要读写哪个寄存器也就是指定寄存器的地址另一个字节就是这个地址下存储器存的内容。写入内容就是控制电路读出内容就是获取电路状态。这整个流程和我们单片机 CPU 操作外设的原理是一样的哈。那现在问题来了单片机读写自己的寄存器可以直接通过内部的数据总线来实现直接用指针操作就行不需要我们操心。但是现在这个模块的寄存器在单片机的外面你要是直接把单片机内部的数据总线拽出来把两个芯片合为一体那可能不太现实啊。所以现在这个大公司就找到了你要求你给他设计一种通信协议在单片机和外部模块连接少量的几根线实现单片机读写外部模块寄存器的功能。这时你可能会想这不太简单了我们就用这个串口的数据包通信就可以完成任务。比如我们就用 HEX 数据包定义一个三个字节的数据包从单片机向外挂模块发过去。第一个字节表示读写发送 0 呢表示这是一个写数据包发送 1 呢表示这是一个读数据包。第二个字节表示读写的地址第三个字节表示写入的数据。比如我发送数据包为 0x00 0x06 0xAA 这就表示在 0x06 地址下写入 0xAA 模块收到之后就执行这个写入操作啊。如果我发送数据包为 0x01 0x06 0x00这就表示我要读取 0x06 地址下的数据。注意这个读的数据包第三个字节无效啊模块收到之后就要再给我发送一个字节返回 0x06 地址下的数据这样就行了。是不是完美完成任务啊于是你觉得这 1000 万已经到手了啊。但是呢这个大公司对这个通信协议的要求非常多其中要求一目前串口这个设计啊是一个需要两根通信线的全双工协议。但是可以明显的发现啊我们这个操作流程是一种基于对话的形式来进行的我们在整个过程中并不需要同时进行发送和接收发送的时候就不需要接收接收的时候就不需要发送这样就会导致始终存在一根信号线处于空闲状态这就是资源的浪费所以要求一就是删掉一根通信线。只能在同一根线上进行发送和接收也就是把全双工变成半双工这就是第一个要求。然后要求二我们这个协议并没有一个应答机制啊也就是单片机发送了一个数据对方有没有收到单片机是完全不了解的。所以为了安全起见大公司要求增加应答机制要求每发送一个字节对方都要给我个应答每接收一个字节我也要给对方一个应答这是第二个要求。然后第三个要求呢大公司说你这一根线只能接一个模块不给力啊他要求你这一根线线上能同时接多个模块单片机可以指定和任意一个模块通信同时单片机在跟某个模块进行通信时其他模块不能对正常的通信产生干扰。这就是第三个要求。最后第四个要求你这个串口是异步的时序也就是发送方和接收方约定的传输速率是非常严格的始终不能有过大的偏差也不能说是在传输过程中啊单片机有点事竟中断了这个时序能不能暂停一下啊对于异步时序来说这是不行的你单片机一个字节发一半暂停了接收方可是不知道的。它仍然会按照原来那个约定的数据读去这就会导致传输出错。所以异步时序的缺点就是非常依赖硬件外设的支持啊必须要有 UART 电路才能方便的使用。如果没有 UART 硬件电路的支持那么串口是很难用软件来模拟的。虽然说软件模拟串口通信也是行得通的啊但是由于异步时序对时间要求很严格一般我们很少用软件来模拟串口通信啊。所以这个大公司的要求是你要把这个协议改成同步的协议另外加一条时钟线来指导对方读写由于存在时钟线线啊对传输的时间要求就不高了。单片机也可以随时暂停传输去处理其他事情因为暂停传输的同时啊时钟线也暂停了所以传输双方都能定格在暂停的时刻可以过一段时间再来继续不会对传输造成影响。这就是同步时序的好处。使用同步时序就会极大的降低单片机对硬件电路的依赖即使没有硬件电路的支持也可以很方便的用软件手动翻转电瓶来实现通信。比如我们之前 51 单片机里那个单片机就没有 I2C 的硬件外设但是同样不影响 51 单片机进行软件模拟的 I2C 通信。异步时序的好处就是省一根时钟线啊节省资源。缺点就是对时间要求严格对硬件电路的依赖比较严重。同步时序的好处就是反过来对时间要求不严格对硬件电路不怎么依赖。在一些低端单片机没有硬件资源的情况下也很容易使用软件来模拟时序缺点就是多一根时钟线啊。这就是同步和异步的区别。那大公司考虑到这个协议要主打下沉市场所以他需要一个同步的协议。那现在要求就提完了其实通信协议这个东西啊就是一个很灵活的设计方案并没有很严格的要求说它必须是这样。只要你的设计能实现项目要求符合电路原理性能和稳定性好那你的设计就好设计。如果能有大公司帮你推广得到了广泛的认可那以后我们课上就学习你设计的通信协议哈。好讲了这么多这个通信协议的设计背景大家就已经进出了吧那我们进入正题我们来看 I2C 通信再声明一下项目要求啊我们最基本的任务是通过通信线实现单片机读写外挂模块寄存器的功能其中至少要实现在指定的位置写寄存器和在指定的位置读寄存器这两个功能。实现了读写寄存器就实现了对这个外挂模块的完全控制。另外刚才说的大公司提出的四点要求也别忘了啊必须要满足大公司的要求才行。然后我们来看一下首先 I2C 总线它的英文全称是 Inter IC 缩写就是 IIC 或者 I 的平方 C我们一般习惯把它叫做 I 方 C 啊。 I 方 C 总线是由飞利浦公司开发的一种通用数据总线啊目前应用还是非常广泛的已经有很多模块都使用了 I 方 C 的协议标准了哈比如下面第一个图片就是我们套件里的 M P6050 模块可以进行姿态测量啊只用了 I2C 通信协议。第二个图片是我们套件里的 O I E D 模块可以显示字符啊图片等信息也是 I2C 协议。第三个图片是 A T 二四 C 零二存储器模块就是我们五幺单片机教程里学习 I2C 的模块啊。第四个图片是 D S 三二三幺实时时钟模块也是使用 I2C 通信啊。等等还有很多模块都支持 I2C 通信。使用了这个通用的协议啊这对于我们开发者来说就非常方便了是吧同样的协议在不同的硬件上操作方法都是极为相似的学会了其中一个硬件再学其他的硬件就很容易了。那 I2C 的标志性引脚就是两根通信线 SCL 全称 serial clock 串行时钟线 SDA 全称 serial data 串行数据线。从下面的图片中啊我们也可以很明显的看到使用 I2C 通信的器件啊都有 SCL 和 SDA 这两个引脚。那 SCL 时钟线就满足了刚才大公司提出的要求四要使用同步的时序降低对硬件的依赖。同时同步的时序啊稳定性也比一步的持续更高哈。然后只有一根 SDA 数据线就满足了大公司提出的要求一变传双工为半双工一根线兼具发送和接收最大化利用资源哈。所以我们可以看到下一条 I2C 通信是同步半双工的协议。之后 I2C 也是一个带数据应答的协议哈满足了设计要求二。最后 I2C 支持总线挂载多设备满足了设计要求三。并且这个挂载多设备啊支持两种模型一主多从和多主多从。一主多从的意思就是单片机作为主机主导 I2C 总线的运行挂载在 I2C 总线的所有外部模块都是从机从机只有被主机点名之后才能控制 I2C 总线不能在未经允许的情况下去碰 I2C 总线防止冲突啊。这就像是在教室里老师是主机主导课程的进行所有学生都是从机所有从机可以同时被动的听老师讲课但是从机只有在被老师点名之后才能说话不可以在未经允许的情况下说话这样课堂才能有条不紊的进行啊。这是一主多从的模型我们使用 I2C 的绝大多数场景啊都是是一主多从的形式一个单片机作为主机挂载一个或多个模块作为从机。另外 I2C 其实还支持多主多从的模型啊也就是多个主机。多主多从的模型在总线上任何一个模块都可以主动跳出来说接下来我就是主机你们都得听我的。这个算是在教室里老师在那讲课突然一个学生站出来说老师打断一下接下来让我来说所有同学听我指挥。但是同一个时间只能有一个人说话这时就相当于发生了总线冲突。在总线冲突时 I2C 的协议会进行仲裁啊仲裁胜利的一方取得总线控制权失败的一方自动变回从机。当然由于时钟线也是由主机控制的啊所以在多主机的模型下还要进行时钟同步啊。多主机的情况下协议是比较复杂的大家感兴趣的可以自己去了解啊。我们本课程仅使用一主多从的模型啊多主多从的部分不做要求。那到这里有关 I2C 的设计背景和基本功能我们就了解了。接下来我们就来详细分析一下看看 I2C 是怎么实现这么多功能的。作为一个通信协议它必须要在硬件和软件上都做出规定。硬件上的规定啊就是你的电路应该如何连接端口的输入输出模式都是啥样的这些东西。软件上的规定你的时序是怎么定义的字节如何传输高位先行还是低位先行一个完整的时序有哪些部分构成这些东西。硬件的规定和软件的规定配合起来就是一个完整的通信协议啊。接下来我们先看一下 I2C 的硬件规定也就是硬件电路部分这个图就是 I2C 的一个典型电路模型哈。这是一个一主多从的模型左边 CPU 就是我们的单片机哈作为总线的主机主机的权力很大包括对 SCL 线的完全控制任何时候都是主机完全掌控 SCL 线。另外在空闲状态下主机可以主动发起对 SDA 的控制只有在从机发送数据和从机应答的时候主机才会转交 SDA 的控制权给从机这是主机的权利。然后看下面这一系列都是被控 I2C也就是挂载在 I2C 总线上的从机这些从机可以是姿态传感器啊、 OLED 啊、存储器啊、时钟模块等等。从机的权利比较小啊对于 SCL 时钟线在任何时刻都只能被动的读取从机不允许控制 SCL 线啊对于 SDA 数据线从机不允许主动发起对 SDA 的控制只有在主机发动读取从机的命令后或者从机应答的时候从机才能短暂的获取 SDA 的控制权。这是一组多重模型中协议的规定啊。然后看接线这里第一条写的是所有 I2C 设备的 SCL 连在一起 SDA 连在一起这是接线要求。在这个图里也可以看到啊主机 SCL 线一条拽出来所有从机的 SCL 都接在这上面主机 SDA 线也是一样拽出来所有从机的 SDA 接在这上面。这就是 SCL 和 SDA 的接线方式。那到现在我们先不继续往后看了先忽略这两个电阻哈。假设我们就这样连接那如何规定每个设备 SCL 和 SDA 的输入输出模式呢 SCL 应该好规定因为现在是一主多从主机拥有 SCL 的绝对控制权所以主机的 SCL 可以配置成推挽输出所有从机的 SCL 都配置成浮空输入或者上拉输入。数据流向是主机发送所有从机接收这没问题啊。但是到 SDA 线这里就比较麻烦了因为这是半双工的协议。所以主机的 SDA 在发送的时候是输出在接收的时候是输入。同样从机的 SDA 也会在输入和输出之间反复切换。如果你能协调好输入输出的切换时机那其实也没问题。但这样做如果总线时序没协调好极有可能发生两个引脚同时处于输出的状态。如果这时又正好是一个输出高电平一个输出低电平那这个状态就是电源短路啊这个状态是要极力避免的。所以为了避免总线没协调好导致电源短路这个问题 I2C 的设计是禁止所有设备输出强上拉的高电平采用外置弱上拉电阻加开漏输出的电路结构。这两点规定就是上面的这两条啊。设备的 SCL 和 SDA 均要配置成开漏输出模式 SCL 和 SDA 各添加一个上拉电阻阻值一般为 4.7 千欧左右啊。对应下面这个图呢就是这样所有的设备包括 CPU 和被控 I2C它引脚的内部结构都是右图这样的啊。左边这一块是 SCL 的结构这里 SCL K 就是 SCL 的意思啊右边这一块是 SDA 的结构。这里 data 就是 SDA 的意思。首先引脚的信号进来都可以通过一个数据缓冲器或者是施密特触发器并输入因为输入对电路没有任何影响啊所以任何设备在任何时刻都是可以输入的。但是在输出的这部分采用的是开漏输出的配置。正常的推挽输出是这样的啊上面一个开关管接到正极下面一个开关管接到负极。上面导通输出高电平啊下面导通输出低电平。因为这是通过开关管直接接到正极和负极的所以这个是强上拉和强下拉的模式。而开漏输出呢就是去掉这个强上拉的开关管。输出低电平时下管导通是强下拉输出高电平时下管断开但是没有上管了此时引脚处于浮空的状态这就是开漏输出。和这里图示是一样的哈输出低电平这个开关管导通引脚直接接地是强下拉。输出高电平这个开关管断开引脚什么都不接作为浮空状态。这样的话所有的设备都只能输出低电平而不能输出高电平。为了避免高电平造成的引脚浮空这时就需要在总线外面SCL 和 SDA 各外置一个上拉电阻。这是通过一个电阻拉到高电平的所以这是一个弱上拉。用我们之前的弹簧和杆子的模型来解释就是 SCL 或 SDA 就是一根杆子。为了防止有人向上推杆子有人向下拉杆子造成冲突啊我们就规定所有的人不准向上推杆子只能选择向下拉或者放手。然后我们再外置一根弹往上拉你要输出低电平就往下拽这根弹簧肯定拽不赢你啊所以弹簧被拉伸杆子处于低电平状态。你要输出高电平就放手杆子在弹簧的拉力下回弹到高电平这就是一个弱上拉的高电平但是完全不影响数据传输哈。这样做有什么好处呢第一完全杜绝了电源短路现象保证电路的安全。你看所有人无论怎么拉杆子或者放手杆子都不会处于一个被同时强拉和强推的状态。即使有多个人同时向下拉杆子也没问题哈。第二避免了引脚模式的频繁切换。开漏加弱上拉的模式啊同时兼具了输入和输出的功能。你要是想输出啊就去拉杆子或者放手操作杆子变化就行了。你要是想输入啊就直接放手然后观察杆子高低就行了。因为开漏模式下输出高电平就相当于断开引脚所以在输入之前可以直接输出高电平不需要再切换成数模式。第三就是这个模式会有个线与的现象就是只要有任意一个或多个设备输出了低电平总线就处于低电平只有所有的设备都输出高电平总线才处于高电平。 I2C 可以利用这个电路特征啊执行多主机模式下的时钟同步和总线仲裁。所以这里 SCL 虽然在一主多从模式下可以用推挽输出啊但是它仍然采用了开漏加上拉输出的模式因为在多主机模式下会利用到这个特征啊。好以上就是 I2C 的硬件电路设计啊那接下来我们就要来学习软件也就是时序的设计首先我们就要来学习软件也就是时序的设计了。首先我们来学习一下 I2C 规定的一些时序基本单元哈。首先就是起始条件它是指 SCL 高电平期间 SDA 从高电平切换到低电平。看一下左下角这个图啊在 I2C 总线处于空闲状态时 SCL 和 SDA 都处于高电平状态也就是没有任何一个设备去碰 SCL 和 SDASCL 和 SDA 由外挂的上拉电阻拉高至高电平总线处于平静的高电平状态啊。当主机需要进行数据收发时首先就要打破总线的宁静啊产生一个起始条件。这个起始条件就是 SCL 处于高电平不去动它然后把 SDA 拽下来产生一个下降沿。当从机捕获到这个 SCL 高电平 SDA 下降沿信号时就会进行自身的复位哈等待主机的召唤。然后在 SDA 下降沿之后主机要再把 SCL 拽下来拽下 SCL 啊一方面是占用这个总线另一方面也是为了方便我们这基本单元的拼接哈。就是我们之后会保证除了起始和终止条件每个时序单元的 SCL 都是以低电平开始低电平结束啊这样这些单元拼接起来 SCL 才能续得上是吧然后继续看终止条件是 SCL 高电平期间 SDA 从低电平切换到高电平也就是这样 SCL 先放手回弹到高电平SDA 再放手回弹高电平产生一个上升沿这个上升沿触发终止条同时终止条件之后 SCL 和 SDA 都是高电平回归到最初的平静状态。这个起始条件和终止条件就类似串口时序里的起始位和停止位哈。一个完整的数据帧总是以起始条件开始终止条件结束。另外啊起始和终止都是由主机产生的从机不允许产生起始和终止哈。所以在总线空闲状态时从机必须始终双手放开不允许主动跳出来去碰总线。如果允许的话那就是多主机模型了哈不在本节的讨论范围之内。这就是起始条件和终止条件。接着继续看在起始条件之后这时就可以紧跟着一个发送一个字节的时序单元。如何发送一个字节呢就是 SCL 低电平期间主机将数据位依次放到 SDA 线上高位先行啊。然后释放 SCL 从机将在 SCL 高电平期间读取数据位。所以 SCL 高电平期间 SDA 不允许有数据变化依次循环上述过程八次即可发送一个字节。图示就是下面这样哈。起始条件之后第一个字节也必须是主机发送的。主机如何发送呢就是最开始 SCL 低电平主机如果想发送 0 就拉低 SDA 到低电平如果想发送 1 就放手 SDA 回弹到高电平。在 SCL 低电平期间允许改变 SDA 的电平。当这一位放好之后主机就松手时钟线 SCL 回弹到高电平。在高电平期间是从机读取 SDA 的时候所以高电平期间 SDA 不允许变化。SCL 属于高电平之后从机需要尽快的读取 SDA 一般都是在上升沿这个时刻啊从机就已经读取完成了因为始终是主机控制的从机并不知道什么时候就会产生下降沿了。你从机要是磨磨唧唧的主机可不会等你的啊。所以从机在上升沿时就会立刻把数据读走。那主机在放手 SCL 一段时间后就可以继续拉低 SCL 传出下一位了。主机也需要在 SCL 下降沿之后尽快把数据放在 SDA 上但是主机有时钟的主导权啊所以主机并不需要那么着急只需要在低电平的任意时刻把数据放在 SDA 上就行了晚点也没关系啊。数据放完之后主机再松手 SCLSCL 高电平从机读取这一位。就这样的流程主机拉低 SCL 把数据放在 SDA 上主机松开 SCL 从机读取 SDA 的数据。在 SCL 的同步下依次进行主机发送和重机接收循环八次就发送了八位数据哈也就是一个字节。另外注意啊这里是高位先行所以第一位是一个字节的最高位 B 七然后依次是次高位 B 六等等啊最后发送最低位 B 零。这个和串口是不一样的哈串口时序是低位先行这里 I2C 是高位先行这个注意一下。另外由于这里有时钟线进行同步所以如果主机一个字节发送一半突然进中断了不操作 SCL 和 SDA 了那时序就会在中断的位置不断拉长 SCL 和 SDA 电平都暂停变化传输也完全暂停等中断结束后主机回来继续操作传输仍然不会出问题这就是同步时序的好处。最后就是由于这整个时序是主机发送一个字节所以在这个单元里 SCL 和 SDA 全程都由主机掌控从机只能被动读取哈这就是发送一个字节的时序。然后我们接着看接收一个字节基本流程是啊SCL 低电平期间从机将数据位依次放到 SDA 线上高位先行然后释放 SCL 主机将在 SCL 高电平期间读取数据位所以 SCL 高电平期间 SDA 不允许有数据变化依次循环上述过程八次即可接收一个字节。注意啊主机在接收之前需要释放 SDA。 刚才我们说了释放 SDA 其实就相当于切换的输入模式啊或者这样来理解所有设备包括主机都始终处于输入模式当主机需要发送的时候就可以主动去拉低 SDA而主机在被动接收的时候就必须先释放 SDA 不要去动它以免影响别人发送。因为总线是线与的特征啊任何一个设备拉低了总线就是低电平。如果你接收的时候还拽着 SDA 不放手那别人无论发什么数据总线都始终是低电平。你自己给它拽着不放还让别人怎么发送呢是吧所以主机在接收之前需要释放 SDA。 从流程上来看接收一个字节和发送一个字节是非常相似的区别就是发送一个字节是低电平主机放数据高电平重机读数据而接收一个字节是低电平重机放数据高电平主机读数据。来看一下下面的时序啊和上面的基本一样。区别就是 SDA 线主机在接收之前需要释放 SDA然后这时从机就取得了 SDA 的控制权从机需要发送 0 就把 SDA 拉低从机需要发送一就放手 SDA 回弹高电平。然后同样的低电平变换数据高电平读取数据。这里实线部分表示主机控制的电平啊虚线部分表示重机控制的电平。SCL 全程由主机控制 SDA 主机在接收前要释放交由从机控制。之后还是一样哈因为 SCL 始终是由主机控制的所以从机的数据变换基本上都是贴着 SCL 下降沿进行的啊而主机可以在 SCL 高电平的任意时刻读取。这是接收一个字节的时序。那我们再继续看最后两个基本单元就是应答机制的设计。首先应答机制分为发送应答和接收应答它们的时序啊分别和发送一个字节接收一个字节的其中一位是相同的你可以理解成发送一位和接收一位哈这一位就用来作为应答。看一下首先是发送应答是主机在接收完一个字节之后在下一个时钟发送一位数据数据 0 表示应答数据 1 表示非应答。然后是接收应答是主机在发送完一个字节之后在下一个时钟接收一位数据判断从机是否应答数据 0 表示应答数据 1 表示非应答。然后同样主机在接收之前需要释放 SDA 啊这个意思就是我们在调用发送一个字节之后就要紧跟着调用接收应答的时序用来判断从机有没有收到刚才给它的数据。如果从机收到了那在应答位置里主机释放 SDA 的时候从机就应立刻把 SDA 拉下来然后在 SCL 高电平期间主机读取应答位如果应答位为零就说明重机确实收到了。这个场景就是啊主机刚发送一个字节然后说有没有人收到啊我现在把 SDA 放手了哈。如果有人收到的话你就把 SDA 拽下来。然后主机高电平读取数据发现哎确实有人给他拽下来了那就说明有人收到了。如果主机发现我松手了结果这个 SDA 就跟着回弹到高电平了那就说明没有人回应我刚发的一个字节可能没人收到或者他收到了但是没给我回应。应这是发送一个字节接收应答的流程。同理啊在接收一个字节之后我们也要给从机发送一个应答位。发送应答位的目的是告诉从机你是不是还要继续发啊如果从机发送一个数据后得到了主机的应答那从机就还会继续发送。如果从机没得到主机的应答那从机就会认为啊我发送了一个数据但是主机不理我可能主机不想要了吧。这时从机就会乖乖的释放 SDA 交出 SDA 的控制权防止干扰主机之后的操作。这是应答位的执行逻辑啊。好拉到这里我们 I2C 六块拼图就已经集齐了分别是起始条件、终止条件、发送一个字节、接收一个字节、发送应答和接收应答。接下来我们就来拼接这些基本单元组成一个完整的数据帧吧。I2C 的完整时序主要有指定地址写、当前地址读和指定地址读这三种。看一下这里啊我们这个 I2C 是一主多从的模型主机可以访问总线上的任何一个设备那如何发送指令来确定访问的是哪个设备呢这就需要首先把每个从机都确定一个唯一的设备地址从机设备地址就相当于每个设备的名字哈主机在起始条件之后要先发送一个字节叫一下从机的名字所有从机都会收到第一个字节和自己的名字进行比较如果不一一样只认为主机没有叫我之后的时机我就不管了如果一样就说明主机现在在叫我那我就响应之后主机的读写操作。在同一条 I2C 总线里挂载的每个设备地址必须不一样否则主机叫一个地址有多个设备都响应那不就乱套了吗是吧重机设备地址在 I2C 协议标准里分为七位地址和十位地址我们目前只讲七位地址的模式哈因为七位地址比较简单而且应用范围最广那在每个 I2C 设备出厂时厂商都会为它分配一个七位的地址这个地址具体是什么可以在芯片手册里找到哈比如我们 MPU6050 这个芯片的七位地址是 1101000之前我们学习 AT24C02 的七位地址哈是 1010000一般不同型号的芯片地址都是不同的哈相同型号的芯片地址都是一样的如果有相同的芯片挂载在同一条总线怎么办呢这就需要用到地址中的可变部分了一般器件地址的最后几位是可以在电路中改变的比如 MPU6050地址的最后一位就可以由这个板子上的 AD0 引脚确定哈这个引脚接低电平那它的地址就是 1101000这个引脚接高电平那它的地址就是 1101001比如 AT24C02地址的最后三位都可以分别由这个板子上的 A0、A1、A2 引脚确定比如 A0 引脚接低电平地址对应的位就是 0接高电平地址对应的位就是 1A1、A2 也是同理哈一般 I2C 的从机设备地址高位都是由厂商确定的低位可以由引脚来灵活切换这样即使相同型号的芯片挂载在同一个总线上也可以通过切换地址低位的方式保证每个设备的地址都不一样哈这就是 I2C 设备的从机地址。然后我们来看一下 I2C 时序第一个时序是指定地址写它完成的任务是对于指定设备指定设备哈通过实例化一个的从机地址来确定在指定地址下这个指定地址啊就是某个设备内部的 rag address 寄存器地址写入指定数据啊就是要在这个寄存器中写入的 data 数据然后看一下下面的时序这个时序是我在示波器下实际抓的波形哈大家也可以用逻辑分析仪抓这个波形而且逻辑分析仪还自带协议解析的功能哈还是非常方便的我们来一一分析一下在这里上面的线是 SCL下面的线是 SDA 空闲状态它俩都是高电平然后主机需要给从机写入数据的时候首先 SCL 高电平期间拉低 SDA 产生起始条件在起始条件之后紧跟着的时序哈必须是发送一个字节的时序字节的内容必须是从机地址加读写位正好从机地址是 7 位哈读写位是一位加起来是一个字节 8 位发送从机地址啊就是确定通信的对象发送读写位呢就是确我接下来是要写入还是要读出具体发送的时候呢在这里低电平期间 SDA 变换数据高电平期间从机读取 SDA这里我用绿色的线啊来标明了从机读到的数据比如这样的波形那从机收到的第一位就是高电平一然后 SCL 低电平期间主机继续变换数据因为第二位还是一所以这里 SDA 电平并没有变化然后 SCL 高电平从机读到第二位是一之后继续啊低电平变换数据高电平读取数据第三位就是 0这样持续 8 次就发送了一个字节数据其中这个数据的定义是啊高 7 位表示从机地址比如这个波形下主机寻找的从机地址就是 1101000这个就是 MPU6050 的地址哈然后最低位表示读写位0 表示之后的时序主机要进行写入操作1 表示之后的时序主机要进行读出操作这里是 0说明之后我们要进行写入操作那目前啊主机是发送了一个字节字节的内容转化为十六进制高位先行就是 0X D0然后根据协议规定紧跟着的单元就得是接收从机的应答位在这个时刻主机要释放 SDA所以如果单看主机的波形啊应该是这样释放 SDA 之后引脚电平回弹到高电平但是根据协议规定哈从机要在这个位拉低 SDA所以单看从机的波形啊应该是这样该应答的时候从机立刻拽住 SDA然后应答结束之后从机再放开 SDA那现在综合两者的波形结合线与的特性在主机释放 SDA 之后由于 SDA 也被从机拽住了所以主机松手后SDA 并没有回弹高电平这个过程就代表从机产生了应答最终高电平期间主机读取 SDA 发现是 0就说明我进行寻址有人给我应答了传输没问题对吧如果主机读取 SDA 发现是一就说明我进行寻址应答位期间我松手了但是没人拽住他没人给我应答那就直接产生停止条件吧并提示一些信息啊这是应答位然后这个上面延哈就是应答位结束后从机释放 SDA 产生的从机交出了 SDA 的控制权因为从机要在低电平尽快变换数据所以这个上升沿和 SCL 的下降沿几乎是同时发生的然后继续往后由于之前我们读写位给了 0所以应答结束后我们要继续发送一个字节同样的时序再来一遍第二个字节就可以送到指定设备的内部了从机设备可以自己定义第二个字节和后续字节的用途哈一般第二个字节可以是寄存器地址或者是指控制字等比如 MPU6050 定义的第二个字节就是寄存器地址比如 AD 转换器啊第二个字节可能就是指令控制字比如存储器啊第二个字节可能就是存储器地址那图四这里啊主机发送这样一个波形我们一一判数据为 00011001即主机向从机发送了 0X19这个数据在 MPU6050 里就表示我要操作你 0X19 地址下的寄存器了这同样是从机应答主机释放 SDA从机拽住 SDASDA 表现为低电平主机收到应答位为 0表示收到了从机的应答然后继续同样的流程再来一遍主机再发送一个字节这个字节就是主机想要写入到 0X19 地址下寄存器的内容了比如我这里发发送了 0XAA 的波形就表示我要在 0X19 地址下写入 0XAA最后是接收应答位如果主机不需要继续传输了就可以产生停止条件在停止条件之前先拉低 SDA为后续 SDA 的上升沿做准备哈然后释放 SCL再释放 SDA这样就产生了 SCL 高电平期间 SDA 的上升沿这样一个完整的数据帧就拼接完成了那套用上面这句话呢这个数据帧的目的就是对于指定从机地址为 10101000 的设备在其内部 0X19 地址下的寄存器中写入 0XAA 这个数据这就是指定地址写的时序啊接下来我们继续看下一个时序这个时序叫当前地址读完成的任务是对于指定设备在当前地址指针指示的地址下读取从机数据这个当前地址指针是什么意思呢我们等会再解释啊先看一下下面的时序图这就是当前地址读的时序如果主机想读取从机的数据就可以执行这个时序那最开始还是 SCL 高电平期间拉低 SDA 产生起始条件起始条件开始后主机必须首先调用发送一个字节来进行从机的寻址和指定读写标志位比如图四的波形表示本次寻址的目标是 1001000 的设备同时最后一位读写标志为一表示主机接下来想要读取数据紧跟着发送一个字节之后接受一下从机应答位哈从机应答 0 代表从机收到了第一个字节在从机应答之后从这里开始数据的传输方向就要反过来了因为刚才主机发出了读的命令所以这之后主机就不能继续发送了要把 SDA 的控制权交给从机主机调用接收一个字节的时序进行接收操作啊然后在这一块从机就得到了主机的允许可以在 SCL 低电平期间写入 SDA然后主机在 SCL 高电平期间读取 SDA那最终主机在 SCL 高电平期间依次读取 8 位就接收到了从机发送的一个字节数据 00001111也就是 0X0F 啊那现在问题就来了这个 0X0F 是从机哪个寄存器的数据呢我们看到在读的时序中I2C 协议的规定是哈主机进行寻址时一旦读写标志位给一了下一个字节就要立马转为读的时序所以主机还来不及指定我想要读哪个寄存器就得开始接收了所以这里就没有指定地址这个环节那主机并没有指定寄存器的地址从机到底该发哪个寄存器的数据呢这就需要用到我们上面说的当前地址指针了在从机中啊所有的寄存器被分配到了一个线性区中并且会有个单独的指针变量指示着其中一个寄存器这个指针上电默认啊一般指向 0 地址并且每写入一个字节和读出一个字节后这个指针就会自动自增一次移动到下一个位置那么在调用当前地址读的时序时主机没有指定要读哪个地址从机就会返回当前指针指向的寄存器的值那假设我刚刚调用了这个指定地址写的时序在 0X19 的位置写出了 0XAA那么指针就会加一移动到 0X1A 的位置我再调用这个当前地址读的时序返回的就是 0X1A 地址下的字如果再调用一次呢返回的就是 0X1B 地址下的字以此类推哈这就是当前地址读时序的操作逻辑由于当前地址读并不能指定读的地址哈所以这个时序用的不是很多那最后我们就继续来看下一个时序指定地址读这个时序的目的就是啊对于指定设备在指定地址下读取从机数据那这个时序为什么能指定读的地址呢我们看一下指定地址写在这前面一部分就是指定地址指的时序我们把最后面的写数据的这部分给去掉然后把前面这一段设置地址还没有指定写什么数据的时序给它追加到这个当前地址读时序的前面就得到了指定地址读的时序一般我们也把它称作复合格式啊下面的时序在这里分隔一下前面的部分是指定地址写但是只指定了地址还没来得及写啊后面的部分是当前地址读因为我们刚指定了地址所以再调用当前地址读两者加在一起就是指定地址读了所以指定地址读的时序会复杂一下啊我们来详细分析一下看看。首先最开始仍然是启动条件然后发送一个字节进行寻址这里指定从机地址是 1001000读写标志位是 0代表我要进行写的操作经过从机应答之后再发送一个字节第二个字节用来指定地址这个数据就写入到了从机的地址指针里了也就是说从机接收到这个数据之后它的寄存器指针就指向了 0X19 这个位置之后我们要写入的数据啊不给他发而是直接再来个起始条件这个 s r 的意思就是重复起始条件哈相当于另起一个时序因为指定读写标志位只能是跟着起始条件的第一个字节所以如果想切换读写方向只能再来个起始条件然后起始条件后重新寻址并且指定读写标志位此时读写标志位是一代表我要开始读了接着主机接收一个字节这个字节是不是就是 0X19 地址下的数据啊这就是指定地址读另外在这里啊你也可以再加一个停止条件这样也行啊这样的话就是两个完整的时序了先起始写入地址停止因为写写入的地址会存在地址指针里面所以这个地址并不会因为时序的停止而消失啊我们就可以再起始读当前位置停止这样两条时序也可以完成任务啊但是 I2C 协议官方规定的复合格式是一整个数据帧哈就是先起始再重复起始再停止相当于把两条时序拼接成一条了。好这些就是这三个 I2C 完整时序的介绍了其中第一个指定地址写和第三个指定地址读用的比较多哈也是我们本节代码使用的时序然后除了这三个时序啊I2C 还有这些时序的进阶版本大概介绍一下啊就是我们这些时序啊指定地址写只是写一个字节当前地址读和指定地址读啊也都是读一个字节那进阶版本就是指定地址写多个字节当前地址读多个字节和指定地址读多个字节时序上呢和这些都非常相似啊只需要增加一些小细节就行我们看一下在这里指定地址然后写入一个字节如果你只想写一个字节那就停止就行了如果你想写多个字节就可以把这最后一部分哈多重复几次比如这里重复三遍发送一个字节和接受应答这样第一个数据就写入到了指定地址 0X19 的位置然后不要忘了刚才说的哈写入一个数据后地址指针会自动加一变成 0X1A所以这第二个数据就写入到了 0X1A 的位置同理第三个数据就写入的是 0X1B 的地址以此类推呀。这样这个时序就进阶为在指定的位置开始按顺序连续写入多个字节比如你需要连续写入多个寄存器就可以考虑这样来操作哈这样在一条数据帧里就可以同时写入多个字节执行效率就会比较高哈。然后同理当前位置读和指定位置读也可以多次执行这最后一部分时序由于地址指针在读后也会自增所以这样就可以连续读出一片区域的寄存器效率也会非常高。然后这里还要注意一下哈如果你只想读一个字节就停止的话在读完一个字节之后一定要给从机发个非应答非应答就是该主机应答的时候主机不把 SDA 拉低从机读到 SDA 为一就代表主机没有应答从机收到非应答之后就知道主机不想要继续了从机就会释放总线把 SDA 的控制权交还给主机如果主机读完仍然给从机应答了从机就会认为主机还想要数据就会继续发送下一个数据而这时主机如果想产生停止条件SDA 可能就会因为被从机拽住了而不能正常弹回早点这个注意下啊如果主机想连续读取多个字节就需要在最后一个字节给非应答而之前的所有字节都要给应答。简单来说就是主机给应答了从机就会继续发主机给非应答了从机就不会再发了。交出 SDA 的控制权从机控制 SDA 发送一个字节的权利哈开始于读写标志位为一结束于主机给应答位为一哈这是主机给从机发送应答位的作用。好以上就是 I2C 总线的硬件规定和软件规定了有了这些规定我们就可以按照硬件规定来连接线路用软件规定来操作总线依次实现指定位置写寄存器和指定位置读寄存器。有了这两个功能主机就可以完全掌控外挂模块的运行了也就实现了我们设计这个协议的目的。那这就是 I2C 协议的内容啊。下下大家我们来了解 MPU6050 这个芯片看看它是怎样工作的有哪些寄存器以及如何利用寄存器控制硬件电路的运行。那我们下小节再见。