首页 > 代码库 > (原创) 巩固理解I2C协议(MCU,经验)

(原创) 巩固理解I2C协议(MCU,经验)

    题外话:这几天天气突然转冷了。今天已是11月23日了,查查黄历,昨天(11月22日)刚好是小雪,一夜温度骤降,果然老祖先的经验有灵验!冬天来了,还是多加加衣服,注意保暖!

1.Abstract

    前些天借用他人的一块MCS-51开发板来做实验,不想这块板子与我刚开始接触MCS-51的板子一样,实在是太亲切了!现在回过来看这块板子,功能算不上是太强大,麻雀虽小五脏俱全,该有的功能都有。于是又忍不住捣腾这块板子,倒不是写小程序一块,看着电路图,到处连线测试一下功能,从中体会下最初的学习兴奋感觉。

    最初板子里边最难学会的有两处,一处是由I2C上挂上的一些器件,另外一处是基于DS18B20的一线传输协议,当初花了好大气力去学习,仍旧一头雾水的情景仍记忆犹新。现在回过来看,觉得当时最大的问题是不注重对时序的分析,不能完全理解总线传输协议,毕竟理解是随着时间的增加而逐步深刻的。看到这两大部分,虽然是几个小小的芯片,但心底有不少的辛酸感。现在的理解也不能算完全,我想尽可能的用现在的所学将这两大部分分别写下来,权当是一个巩固学习的过程。

2.Content

  2.1 协议分析

    先想想两个陌生人是怎么进行沟通的,为了显得更有主次关系,选取老板和新员工进行沟通的场景,老板一般占有主动权,而且手中有新员工的基本信息,比如姓名,年龄,性别等。沟通开始:

    老板:“XXX,欢迎你加入我们公司,为公司注入新的血液!”伸出握手姿势              —— 主握手

    新员工:“承蒙厚爱,有幸加入我们公司,我觉得是一种光荣!”握手                      —— 握手成功

    老板:“想必已经读过员工手册了吧,说说你对前两条的理解。”                            —— 执行沟通

    新员工:“第一条……,第二条……”                                                                    —— 从应答

    ……

    老板:“回答的很好,确实是我们迫切想招募的人,以后看你的精彩表现了!”        —— 主要求沟通结束

    新员工:“一定不负厚望,必努力工作!”                                                            —— 从做好结束准备

    老板离开                                                                                                             —— 沟通结束

    为了跟后边的描述更贴切,在实景对话的后边做了一定的注释,将沟通的过程抽象出来。从上面的对话可以看出,沟通分为四大步骤,握手、数据交换、准备结束、正式结束。I2C的通信也氛围这样几大步骤,值得注意的是,在通信的时候,总线上必须要有一个为主器件,其他的为从器件。有些器件它既可以是主器件,又可以是从器件,但是它们在总线上的特定时候只有一种模式,要么是主器件模式,要么是从器件模式,否则就混乱了。

    把那些在特定时候器件作为主器件,即在总线上表现为主控的器件叫做主发送器/接收器(MASTER TRANSMITTER/RECEIVER),有些主器件只执行发送,不执行接收的,命名为主发送器(MASTER TRANSMITTER);那些在同时候作为从器件,即在总线上表现为被控的器件叫做从发送器/接收器(SLAVE RECEIVE/RTRANSMITTER),有些从器件只执行接收,不执行发送的,命名为从接受器(SLAVE RECEIVER)。它们在同一个总线上的连接可以如FIG2.1所示。

image FIG2.1 I2C总线的器件连接

    值得强调的一下是 同一时刻,总线上只有一个器件配置为主器件,而其他的只能作为从器件!

    I2C协议是典型的二线通信,确切的讲,应该是三根(SDA串行数据线,SCL串行时钟线,GND共地线),如上图所示。

    那么,一条I2C总线最多能挂多少个器件呢?一般来说,串行数据都是以一个字节一个字节的方式来衡量的,前几位用来表示地址(上述对话中的员工名字),地址的最后一位为读写操作位(READ/WRITTE位,简写为R/W,逻辑1表示读,逻辑0表示写),以最开始的一个字节作为地址来算的话,那么除去字节的最后读写标志位,就剩 8 – 1 = 7bit了,所以理论上以1个字节为地址来算的话,就可以分配2的7次方128个地址,就可以挂上128个器件(极端情况,假设这128个器件都是从机的话,就还需要挂一个主器件,合计就是128 + 1 = 129个器件了)。要是想挂更多的器件,那么就须得将地址位扩展,比如将前两个字节作为地址,去掉最后的一位读写标志位,就剩下 8 + 8 – 1 = 15 bit了,所以以2个字节为地址来算的话,就可以分配2的15次方32768个器件(当然,极端情况下可以多挂一个主器件,合计就是32768 + 1 = 32769个器件了)。要是还想挂更多的器件,方法就如上述了,将前几个字节作为地址,最后一位作为读写标志位,具体的算就不展开了。

    以上是理论的算法,在实际的器件中,都是以第一个字节作为地址的,而且大部分的器件的地址高四位已经被根据不同功能的芯片分配了不同的编码(例如,AD/DA转换芯片PCF8591的前四位为1001,E2PROM芯片AT24C02的前四位为1010,具体的芯片就得查查手册了,这里只说明原理),那么同一种功能芯片(地址前4位都相同)最多只有 8 – 4 – 1 = 3位用来分配地址了,也就是最多可以挂2的3次方8个同种功能芯片。用一个问题来深化理解一下。

    一条I2C总线上最多可以分别挂多少个PCF8591芯片和多少个AT24C02芯片呢?它们能同时挂在总线上吗?若能,请将它们的地址全部列出来。

    算算,1)查手册得知,PCF8591的前四位固定编码为1001,除去最后一位读写标志位,则剩下8 – 4 – 1 = 3位地址编码,由数学逻辑可知,3位二进制码可以分配2的3次方8个地址,故一条I2C总线上最多可以挂8个PCF8591;同理,AT24C02的前四位固定编码为1010,其他的跟PCF8591一样,故一条I2C总线上最多可以挂8个AT24C02。2)它们是可以同时挂在一条总线上的,因为它们对应的地址都不相同,主器件可以全部访问到它们。地址如下表

表2.1 器件地址表

器件名称 器件地址
PCF8591 1001 000X
PCF8591 1001 001X
PCF8591 1001 010X
PCF8591 1001 011X
PCF8591 1001 100X
PCF8591 1001 101X
PCF8591 1001 110X
PCF8591 1001 111X
AT24C02 1010 000X
AT24C02 1010 001X
AT24C02 1010 010X
AT24C02 1010 011X
AT24C02 1010 100X
AT24C02 1010 101X
AT24C02 1010 110X
AT24C02 1010 111X

    关于总线挂器件的问题,应该是明晰许多了。下面开始看看它的四大通信步骤。

  2.1.1 握手与结束

    握手与结束它们都是属于控制总线一块儿的,抽象出来说,它是做通信的控制的,跟数据是如何交换的区分开来。

image FIG2.2 I2C总线开始和结束

    I2C协议有规定,在SCL和SDA均为高电平的前提下,检测到SDA有下降沿信号,则建立I2C的通信开始;同样的,在SCL为高电平,SDA为低电平的前提下,检测到SDA有上升沿信号,则I2C通信正式结束。

    对上面的一段话还真需要有深刻的理解,甚至有必要将它背下来,熟记于心。首先看看总线闲,也就是总线上没有通信,这两根信号线的电平状态;从图中可以看到,通信的开始之前,SCL和SDA均为高电平;再看看通信正式结束后,SCL和SDA信号均为高电平;也就是说,总线闲的时候,两根信号线都是高电平的。再看看通信的建立和正式结束的时候,这两根信号线的电平变化特点。由图中虚线框中引出的,不管是通信建立和通信结束阶段,SCL都是高电平,SDA的变化控制着通信的建立与结束;这一点尤为重要,或许换一种说法更为适合,在SCL为高电平的情况下,SDA信号的转变就对通信起着强制性作用,要么通信建立,要么通信正式结束,有且仅有这两种情况!也就是说,在数据交换的过程中,要对SCL这根信号线尤为注意,在数据变化的时候,一定要保证SCL是为低电平!让数据的变化在SCL的“安全”状态下进行,所以需要牢记一点,数据变化,时钟线低电平先行,如下图所示

image FIG2.3 数据时序规则

   2.1.2 数据交换

    为了更好地理解I2C数据交换的过程,真是花了不少的功夫,或许真正难懂的地方就在这里,需要准确无误的理解。

    I2C总线上时常是挂着许多器件的,但是某个通信时刻下,只存在一个主器件,一个从器件的一对一通信机制;这也是要分配地址编码的真正原因了,主器件将数据传送到总线上,虽然所有的器件都能接收到数据,但是只有对应地址的器件能做出应答反应。理解这一点就好办了;从主器件这一端看过去,需要和某个从机打交道,那么它应该具备的信息有:1) 从器件的地址 2)从器件内部的控制信息;对应于人的逻辑思维应该是 对方的姓名和要求他做特定的事儿。每次通信开始前,主器件都需要将这两个信息交代清楚。

    还是站在主器件的角度来看;综合一下,主器件主要做哪些事儿呢?是的,无非两件,一、写数据,二、读数据,除此二者之外,别无其他。这对应人的思维应该就是说和听了。引出了主器件写数据和读数据的概念,再就看它们是如何工作的了。

    主器件向从器件写数据。如上所述,主器件向要向从器件写数据,首先得知道从器件的两个基本信息,从器件地址和从器件内部控制信息。写这些信息有特定的顺序,第一个字节是从器件的地址,第二个字节是从器件内部控制信息,第三个字节是所要写的第一个数据,第四个字节是所要写的第二个数据……完整写的通信过程就应该是如下图所示了。

image FIG 2.4 主器件写操作流程

    紫色部分表示通信控制,橙色表示写必要的通信信息,绿色部分表示要写的数据。

    实际的I2C通信大概流程就是如此了,但是它做了一定的优化,即在每次写数据的时候,每写一个字节数据(不论是必要的从器件地址数据、从器件控制数据,还是需要写到从机里的数据),从器件都有一个应答(从器件的应答实现是将SDA线拉成低电平,所以在写完第8位数据以后,切记要把SCL的电平拉成低电平,等一小段时间以后,在将数据线拉高去读SDA的数据,至于原因,2.1.1节描述得很清晰了),用下面的情景模式解释一下或许更容易理解。

    对话已经开始                                                                                              —— 通信开始

    老板:“XXX,你来一下!”                                                                         —— 写从器件地址

    员工:“是!”                                                                                           —— 从器件应答

    老板:“你管业务这一块的吧?”                                                                 —— 写从器件控制信息

    员工:“是!”                                                                                           —— 从器件应答

    老板:“分配两个任务,其一,今天下午2:30你们组开会!”                          —— 写数据1

    员工:“是!”                                                                                           —— 从器件应答

    老板:“其二,下班后大家一起出去吃饭!”                                                 —— 写数据2

    员工:“是!”                                                                                           —— 从器件应答

    老板:“好了,就这么多了,去忙吧!”                                                       —— 写数据3

    员工:“是!”                                                                                           —— 从器件应答

    对话结束                                                                                                     —— 通信正式结束

    也就是说,老板每说一段话,员工都必须做出是的回答。转换成I2C协议里边,就是主器件每发送一个字节数据,从机都要做一个应答(用ACK表示,简写为A)。

image FIG2.5 I2C通信中主器件完整的通信过程

    这个是以PCF8591为例子的,其他的器件类似。从左往右看,最开始的是通信建立S;然后再写从器件地址(注意,器件地址的最后位为0,表示的是写操作);紧接着器件回复了一个应答信号A;然后主器件写从器件的控制信息CONTROL BYTE;从器件回复一个应答信号A;之后主器件开始写数据DATA BYTE;写完一个数据以后,从器件回复一个应答信号A;当然,可以写多个数据,每写完一个数据,从器件都回复一个应答信号A;写完所有数据之后,可以选择停止通信操作P,也可以建立新的开始通信S。主器件的写就讲述完了,再来看主器件的读。

    主器件向从器件读数据。主器件向从器件读数据的过程和上述主器件向从器件写数据的过程类似,也得首先知道从器件的地址信息,对于控制信息,有些器件需要,有些器件则不需要,这得查看手册信息了。具备这些信息以后,就可以开始读器件的数据了。现在以一个最基本读流程开始,后边再介绍一个稍微复杂的读流程。

    以PCF8591为例,对照它的数据手册,可知它的功能是一个8位4路A/D转换和1路D/A转换的集成芯片。在做D/A转换时,主要用到的是对它进行写的操作;而用到读取它的数据,则是A/D转换的操作;因为每次A/D采样完成以后,就会把数据放到指定的地方,而且只有一个字节数据,所以对它的读操作就不需要控制信息了,主器件只需要有它的器件地址就可以完成读的操作了。这样就简化了流程,主器件只需要发送从器件的地址信息加上读操作位,就可以对信息的读取了。完整的流程如下。

image FIG 2.6 主器件的读操作流程

    主器件首先要建立通信,然后向总线发送 器件地址+ 读标志位,从器件就可以将数据一位一位将数据发到总线上,要注意的是,虽然主器件不能控制SDA信号了,但是它可以控制SCL信号,主机可以通过改变SCL的电平,从而来接收来自于SDA的信息。和写操作一样,在每发送1个字节数据以后,需要有一个应答信号;这下应答信号的产生就来自于主器件了。可以这样来看,发送方式从器件,而接收方是主器件,发送方要确认接收方已经接收到数据了,这种发送方和接收方的理解也适用于上述的读操作。

    比较特殊的是,主器件的应答方式有两种。一种是将SDA线成低电平,然后输出一个脉冲,从器件根据接收到这个低电平信号,可以判断主器件正常地接收到了数据,而且准备好了接收下一个数据,这与上述的写操作从机应答是一样的;还有一种就是主器件将SDA释放为高电平,然后输出一个脉冲,从器件根据接收到的这个高电平信号,可以判断主器件正常地接收到了数据,但是不准备接收下下一个数据,意味着数据通信即将结束。这种应答方式可以用下面的情景模式辅助理解一下。

    对话开始                                                                                    —— 建立通信

    老板:“XXX,你过来一下,把今天的完成的进度汇报一下!”       —— 发送从机地址并加读信号标志位

    员工:“今天完成的任务有四项,第一项是……”                         —— 从器件发送数据1

    老板:“嗯,继续”                                                                    —— 主器件确定应答

    员工:“第二项是……”                                                             —— 从器件发送数据2

    老板:“好,就这样吧,还汇报一个任务你就去忙吧!”               —— 主器件取消应答

    员工:“第三项是……”                                                             —— 从器件发送数据3

    对话结束                                                                                    —— 通信正式结束

    也就是说,员工汇报完每项任务以后,老板都需要做一个应答,应答的方式有两种,一种是做好准备听下下一个任务,另外一种是结束听下下一个任务,从而结束对话。转换到I2C协议中来,就是从器件自接收地址数据要求做数据输出时,从器件开始向总线逐个的发送数据;主器件也需要对从器件发送的数据进行应答;应答的方式有两种,一种是发送应答信号(ACK),表示主器件已经准备好接收下下一个数据,另外一种就是非应答信号(NO ACK),表示主器件不打算接收下下一个数据,要求通信结束。PCF8591完整的读操作过程如下所示。

image FIG2.7 针对PCF8591芯片主器件完整的读操作流程

    还是从左往右看,最开始是建立通信开始S;然后主器件向总线上发送从器件地址,并将读标志位置1(地址的最后一位置1,表示读操作);随后,从器件将第一个数据发送到I2C总线上;如果主器件准备再读取下下一个数据,则置应答位为0(ACK),如果主器件不想再读下下一个数据,就置应答位为1(NO ACK),表示通信即将结束;最后就是通信结束S,表示通信正式结束。

    PCF8591的读操作流程还是非常简单的,因为所要读的数据不多,数据位置已经固定好了,所以没有控制信息。再来看一下一个具有控制信息的器件该怎么读,以E2PROM存储器AT24C02为例。

    针对存储器,首先要知道的就是它的器件地址,其次就是要读的地址信息(也就是控制信息)。所以,在读数据之前就要先将地址信息写过去,然后才能准确地读数据。例子就不举了,跟PCF8591大同小异,多了一个在读之前要写控制信息这部分。直接看看完整的读操作流程。

image FIG 2.8 针对AT24C02芯片主器件完整的读操作流程

    还是从左往右看,要想读取特定地址的数据,首先要将地址写入到指定器件。按照操作流程,最开始是建立通信S,然后写从器件地址,并将读写操作位置0(地址的最后1位为0,表示写操作);紧接着是从器件回复应答信号(ACK);然后写入要读出的数据信息位置(控制信息);从器件回复应答信号(ACK);然后再开始进行读操作,读操作之前,也先将从器件的地址发送到I2C总线上,并将读写标志位置1(地址最后一位置1,表示读操作);然后从器件回复一个应答信号(ACK);紧接着从器件将指定位置的数据发送到I2C总线上,主器件接收到数据以后,发送一个非应答信号(NO ACK),表示不再准备读取下下一个数据,准备通信结束;最后就是通信结束S,表示通信正式结束。

    上述描述了主从器件之间如何握手与通信结束,如何向从机写数据,如何从从机读数据等基本协议分析。下面就是进行用程序来实现这个通信协议。

2.2 协议的程序实现

    协议的程序实现部分,我自己就不再写代码了,上述的协议图已经分析的很透彻了,这里我参考别人写好的程序,其实实现的方式也是大同小异,因为通信的格式已经固定了,具体的代码都得按照这个格式来书写。

    我的这份代码摘自于doflye网站:www.doflye.net 它是以PCF8591为例,其他的类推就可以了。

  2.2.1 基本配置和宏定义

#include "i2c.h"#include "delay.h"#define  _Nop()  _nop_()  //定义空指令                         bit ack;                  //应答标志位sbit SDA=P2^1;sbit SCL=P2^0;

  2.2.1 I2C通信建立

void Start_I2c(){  SDA=1;   //发送起始条件的数据信号  _Nop();  SCL=1;  _Nop();    //起始条件建立时间大于4.7us,延时  _Nop();  _Nop();  _Nop();  _Nop();      SDA=0;     //发送起始信号  _Nop();    //起始条件锁定时间大于4μ  _Nop();  _Nop();  _Nop();  _Nop();         SCL=0;    //钳住I2C总线,准备发送或接收数据  _Nop();  _Nop();}

    这里有几个位置需要说明一下,就是时序上的冲突问题。

image FIG2.9  PCF8591的操作时序

imageFIG2.10 PCF8591 交流特性表

    FIG2.9 和 FIG2.10是需要合起来一起看,而且一般是要找出几种极限的参数,比如时序建立时间、保持时间、有效数据时间等。对照图和表,查出时序建立时间Tsu至少为4.7us,保持时间Thd至少为4.0us,数据有效时间建立不低于3.4us。

    若MCS-51选取12M的晶振,每一个指令周期的时间就是 12 / ( 6 * 12) = 1us,故达到了器件工作频率的上限值,所以需要采取一定的空机器周期用来解决时序冲突。下面也有这样的做法,就不再赘述了。

  2.2.2 I2C通信结束

void Stop_I2c(){  SDA=0;    //发送结束条件的数据信号  _Nop();   //发送结束条件的时钟信号  SCL=1;    //结束条件建立时间大于4μ  _Nop();  _Nop();  _Nop();  _Nop();  _Nop();  SDA=1;    //发送I2C总线结束信号  _Nop();  _Nop();  _Nop();  _Nop();}

  2.2.3 主器件向从器件写一个字节数据(带应答信号检验)

void  SendByte(unsigned char c){ unsigned char BitCnt;  for(BitCnt=0;BitCnt<8;BitCnt++)  //要传送的数据长度为8位    {     if((c<<BitCnt)&0x80)SDA=1;   //判断发送位       else  SDA=0;                     _Nop();     SCL=1;               //置时钟线为高,通知被控器开始接收数据位      _Nop();       _Nop();             //保证时钟高电平周期大于4μ      _Nop();      _Nop();      _Nop();              SCL=0;     }        _Nop();    _Nop();    SDA=1;               //8位发送完后释放数据线,准备接收应答位    _Nop();    _Nop();       SCL=1;    _Nop();    _Nop();    _Nop();    if(SDA==1)ack=0;            else ack=1;        //判断是否接收到应答信号    SCL=0;    _Nop();    _Nop();}

  2.2.4 主器件从从器件接收一个数据

unsigned char  RcvByte(){  unsigned char retc;  unsigned char BitCnt;    retc=0;   SDA=1;             //置数据线为输入方式  for(BitCnt=0;BitCnt<8;BitCnt++)      {        _Nop();                   SCL=0;       //置时钟线为低,准备接收数据位        _Nop();        _Nop();      //时钟低电平周期大于4.7us        _Nop();        _Nop();        _Nop();        SCL=1;       //置时钟线为高使数据线上数据有效        _Nop();        _Nop();        retc=retc<<1;        if(SDA==1)retc=retc+1; //读数据位,接收的数据位放入retc中        _Nop();        _Nop();       }  SCL=0;      _Nop();  _Nop();  return(retc);}

  2.2.5 主器件回复应答信号0(ACK)

void Ack_I2c(void){    SDA=0;       _Nop();  _Nop();  _Nop();        SCL=1;  _Nop();  _Nop();              //时钟低电平周期大于4μ  _Nop();  _Nop();  _Nop();    SCL=0;               //清时钟线,钳住I2C总线以便继续接收  _Nop();  _Nop();    }

  2.2.6 主器件回复应答信号1(NO ACK)

void NoAck_I2c(void){    SDA=1;  _Nop();  _Nop();  _Nop();        SCL=1;  _Nop();  _Nop();              //时钟低电平周期大于4μ  _Nop();  _Nop();  _Nop();    SCL=0;                //清时钟线,钳住I2C总线以便继续接收  _Nop();  _Nop();    }

  2.2.7 主器件向从器件写入数据

bit WriteDAC(unsigned char dat){   Start_I2c();               //启动总线   SendByte(AddWr);             //发送器件地址     if(ack==0)return(0);   SendByte(0x40);            //发送控制信息
     if(ack==0)return(0);   SendByte(dat);             //发送数据     if(ack==0)return(0);   Stop_I2c();  }

  2.2.8 主器件从从器件读入数据

unsigned char ReadADC(unsigned char Chl) {   unsigned char Val;   Start_I2c();               //启动总线   SendByte(AddWr);             //发送器件地址     if(ack==0)return(0);   SendByte(0x00|Chl);            //发送控制信息  AD采样通道     if(ack==0)return(0);   Start_I2c();   SendByte(AddWr+1);    //  将最后一位读写标志位设置为1,表示读      if(ack==0)return(0);   Val=RcvByte();   NoAck_I2c();                 //发送非应位   Stop_I2c();                  //结束总线  return(Val); }

  2.3  实际程序验证

    最后加入一些液晶显示模块和AD转换数据处理,将信息可以显示在液晶屏幕上。因为这套板子的程序已经完全做好了,在出厂之前就已经测试好了;为了保险起见,我也将程序烧录其中做了一下测试,实际上是可以通得过的。这里就不再插图看了,避免打广告的嫌疑。

3.Conclusion

    总体来说,I2C的协议还是不复杂的,不过学好它确实有点不容易,需要从多个角度去思考和体会。现在规范的协议不算太多,能够很好的掌握I2C协议就相当于上了一个新的台阶。

    就个人来说,对I2C的协议理解还是不够深刻的,以前学了一门课程,计算机接口与技术,这门课是专门讲接口的,I2C就是一个很常用的串行接口。不过这本书的主要目的是讲硬件方面,即这些接口是怎样用物理的方法实现的。对于使用者,确确实实只要了解这个接口怎么用就可以了,但想从电路设计的角度去深刻理解它,我想要学的还有很多。

4.Reference

1). PCF8591 datasheet

2). AT24C02 datasheet

(原创) 巩固理解I2C协议(MCU,经验)