题外话:这几每天气忽然转冷了。今天已经是11月23日了,查查黄历,昨天(11月22日)恰好是小雪,一晚上温度骤降,果真老祖先的经验有灵验!冬天来了,仍是多加加衣服,注意保暖!算法
1.Abstract小程序
前些天借用他人的一块MCS-51开发板来作实验,不想这块板子与我刚开始接触MCS-51的板子同样,实在是太亲切了!如今回过来看这块板子,功能算不上是太强大,麻雀虽小五脏俱全,该有的功能都有。因而又忍不住捣腾这块板子,倒不是写小程序一块,看着电路图,处处连线测试一下功能,从中体会下最初的学习兴奋感受。安全
最初板子里边最难学会的有两处,一处是由I2C上挂上的一些器件,另一处是基于DS18B20的一线传输协议,当初花了好大气力去学习,仍旧一头雾水的情景仍记忆犹新。如今回过来看,以为当时最大的问题是不注重对时序的分析,不能彻底理解总线传输协议,毕竟理解是随着时间的增长而逐步深入的。看到这两大部分,虽然是几个小小的芯片,但心底有很多的辛酸感。如今的理解也不能算彻底,我想尽量的用如今的所学将这两大部分分别写下来,权当是一个巩固学习的过程。学习
2.Content测试
2.1 协议分析优化
先想一想两个陌生人是怎么进行沟通的,为了显得更有主次关系,选取老板和新员工进行沟通的场景,老板通常占有主动权,并且手中有新员工的基本信息,好比姓名,年龄,性别等。沟通开始:网站
老板:“XXX,欢迎你加入咱们公司,为公司注入新的血液!”伸出握手姿式 —— 主握手编码
新员工:“承蒙厚爱,有幸加入咱们公司,我以为是一种光荣!”握手 —— 握手成功spa
老板:“想必已经读过员工手册了吧,说说你对前两条的理解。” —— 执行沟通.net
新员工:“第一条……,第二条……” —— 从应答
……
老板:“回答的很好,确实是咱们迫切想招募的人,之后看你的精彩表现了!” —— 主要求沟通结束
新员工:“必定不负厚望,必努力工做!” —— 从作好结束准备
老板离开 —— 沟通结束
为了跟后边的描述更贴切,在实景对话的后边作了必定的注释,将沟通的过程抽象出来。从上面的对话能够看出,沟通分为四大步骤,握手、数据交换、准备结束、正式结束。I2C的通讯也氛围这样几大步骤,值得注意的是,在通讯的时候,总线上必需要有一个为主器件,其余的为从器件。有些器件它既能够是主器件,又能够是从器件,可是它们在总线上的特定时候只有一种模式,要么是主器件模式,要么是从器件模式,不然就混乱了。
把那些在特定时候器件做为主器件,即在总线上表现为主控的器件叫作主发送器/接收器(MASTER TRANSMITTER/RECEIVER),有些主器件只执行发送,不执行接收的,命名为主发送器(MASTER TRANSMITTER);那些在同时候做为从器件,即在总线上表现为被控的器件叫作从发送器/接收器(SLAVE RECEIVE/RTRANSMITTER),有些从器件只执行接收,不执行发送的,命名为从接受器(SLAVE RECEIVER)。它们在同一个总线上的链接能够如FIG2.1所示。
值得强调的一下是 同一时刻,总线上只有一个器件配置为主器件,而其余的只能做为从器件!
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 握手与结束
握手与结束它们都是属于控制总线一起的,抽象出来讲,它是作通讯的控制的,跟数据是如何交换的区分开来。
I2C协议有规定,在SCL和SDA均为高电平的前提下,检测到SDA有降低沿信号,则创建I2C的通讯开始;一样的,在SCL为高电平,SDA为低电平的前提下,检测到SDA有上升沿信号,则I2C通讯正式结束。
对上面的一段话还真须要有深入的理解,甚至有必要将它背下来,熟记于心。首先看看总线闲,也就是总线上没有通讯,这两根信号线的电平状态;从图中能够看到,通讯的开始以前,SCL和SDA均为高电平;再看看通讯正式结束后,SCL和SDA信号均为高电平;也就是说,总线闲的时候,两根信号线都是高电平的。再看看通讯的创建和正式结束的时候,这两根信号线的电平变化特色。由图中虚线框中引出的,无论是通讯创建和通讯结束阶段,SCL都是高电平,SDA的变化控制着通讯的创建与结束;这一点尤其重要,或许换一种说法更为适合,在SCL为高电平的状况下,SDA信号的转变就对通讯起着强制性做用,要么通讯创建,要么通讯正式结束,有且仅有这两种状况!也就是说,在数据交换的过程当中,要对SCL这根信号线尤其注意,在数据变化的时候,必定要保证SCL是为低电平!让数据的变化在SCL的“安全”状态下进行,因此须要牢记一点,数据变化,时钟线低电平先行,以下图所示。
2.1.2 数据交换
为了更好地理解I2C数据交换的过程,真是花了很多的功夫,或许真正难懂的地方就在这里,须要准确无误的理解。
I2C总线上时常是挂着许多器件的,可是某个通讯时刻下,只存在一个主器件,一个从器件的一对一通讯机制;这也是要分配地址编码的真正缘由了,主器件将数据传送到总线上,虽然全部的器件都能接收到数据,可是只有对应地址的器件能作出应答反应。理解这一点就好办了;从主器件这一端看过去,须要和某个从机打交道,那么它应该具有的信息有:1) 从器件的地址 2)从器件内部的控制信息;对应于人的逻辑思惟应该是 对方的姓名和要求他作特定的事儿。每次通讯开始前,主器件都须要将这两个信息交代清楚。
仍是站在主器件的角度来看;综合一下,主器件主要作哪些事儿呢?是的,无非两件,1、写数据,2、读数据,除此两者以外,别无其余。这对应人的思惟应该就是说和听了。引出了主器件写数据和读数据的概念,再就看它们是如何工做的了。
主器件向从器件写数据。如上所述,主器件向要向从器件写数据,首先得知道从器件的两个基本信息,从器件地址和从器件内部控制信息。写这些信息有特定的顺序,第一个字节是从器件的地址,第二个字节是从器件内部控制信息,第三个字节是所要写的第一个数据,第四个字节是所要写的第二个数据……完整写的通讯过程就应该是以下图所示了。
紫色部分表示通讯控制,橙色表示写必要的通讯信息,绿色部分表示要写的数据。
实际的I2C通讯大概流程就是如此了,可是它作了必定的优化,即在每次写数据的时候,每写一个字节数据(不管是必要的从器件地址数据、从器件控制数据,仍是须要写到从机里的数据),从器件都有一个应答(从器件的应答实现是将SDA线拉成低电平,因此在写完第8位数据之后,切记要把SCL的电平拉成低电平,等一小段时间之后,在将数据线拉高去读SDA的数据,至于缘由,2.1.1节描述得很清晰了),用下面的情景模式解释一下或许更容易理解。
对话已经开始 —— 通讯开始
老板:“XXX,你来一下!” —— 写从器件地址
员工:“是!” —— 从器件应答
老板:“你管业务这一块的吧?” —— 写从器件控制信息
员工:“是!” —— 从器件应答
老板:“分配两个任务,其一,今天下午2:30大家组开会!” —— 写数据1
员工:“是!” —— 从器件应答
老板:“其二,下班后你们一块儿出去吃饭!” —— 写数据2
员工:“是!” —— 从器件应答
老板:“好了,就这么多了,去忙吧!” —— 写数据3
员工:“是!” —— 从器件应答
对话结束 —— 通讯正式结束
也就是说,老板每说一段话,员工都必须作出是的回答。转换成I2C协议里边,就是主器件每发送一个字节数据,从机都要作一个应答(用ACK表示,简写为A)。
这个是以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采样完成之后,就会把数据放到指定的地方,并且只有一个字节数据,因此对它的读操做就不须要控制信息了,主器件只须要有它的器件地址就能够完成读的操做了。这样就简化了流程,主器件只须要发送从器件的地址信息加上读操做位,就能够对信息的读取了。完整的流程以下。
主器件首先要创建通讯,而后向总线发送 器件地址+ 读标志位,从器件就能够将数据一位一位将数据发到总线上,要注意的是,虽然主器件不能控制SDA信号了,可是它能够控制SCL信号,主机能够经过改变SCL的电平,从而来接收来自于SDA的信息。和写操做同样,在每发送1个字节数据之后,须要有一个应答信号;这下应答信号的产生就来自于主器件了。能够这样来看,发送方式从器件,而接收方是主器件,发送方要确认接收方已经接收到数据了,这种发送方和接收方的理解也适用于上述的读操做。
比较特殊的是,主器件的应答方式有两种。一种是将SDA线成低电平,而后输出一个脉冲,从器件根据接收到这个低电平信号,能够判断主器件正常地接收到了数据,并且准备好了接收下一个数据,这与上述的写操做从机应答是同样的;还有一种就是主器件将SDA释放为高电平,而后输出一个脉冲,从器件根据接收到的这个高电平信号,能够判断主器件正常地接收到了数据,可是不许备接收下下一个数据,意味着数据通讯即将结束。这种应答方式能够用下面的情景模式辅助理解一下。
对话开始 —— 创建通讯
老板:“XXX,你过来一下,把今天的完成的进度汇报一下!” —— 发送从机地址并加读信号标志位
员工:“今天完成的任务有四项,第一项是……” —— 从器件发送数据1
老板:“嗯,继续” —— 主器件肯定应答
员工:“第二项是……” —— 从器件发送数据2
老板:“好,就这样吧,还汇报一个任务你就去忙吧!” —— 主器件取消应答
员工:“第三项是……” —— 从器件发送数据3
对话结束 —— 通讯正式结束
也就是说,员工汇报完每项任务之后,老板都须要作一个应答,应答的方式有两种,一种是作好准备听下下一个任务,另一种是结束听下下一个任务,从而结束对话。转换到I2C协议中来,就是从器件自接收地址数据要求作数据输出时,从器件开始向总线逐个的发送数据;主器件也须要对从器件发送的数据进行应答;应答的方式有两种,一种是发送应答信号(ACK),表示主器件已经准备好接收下下一个数据,另一种就是非应答信号(NO ACK),表示主器件不打算接收下下一个数据,要求通讯结束。PCF8591完整的读操做过程以下所示。
仍是从左往右看,最开始是创建通讯开始S;而后主器件向总线上发送从器件地址,并将读标志位置1(地址的最后一位置1,表示读操做);随后,从器件将第一个数据发送到I2C总线上;若是主器件准备再读取下下一个数据,则置应答位为0(ACK),若是主器件不想再读下下一个数据,就置应答位为1(NO ACK),表示通讯即将结束;最后就是通讯结束S,表示通讯正式结束。
PCF8591的读操做流程仍是很是简单的,由于所要读的数据很少,数据位置已经固定好了,因此没有控制信息。再来看一下一个具备控制信息的器件该怎么读,以E2PROM存储器AT24C02为例。
针对存储器,首先要知道的就是它的器件地址,其次就是要读的地址信息(也就是控制信息)。因此,在读数据以前就要先将地址信息写过去,而后才能准确地读数据。例子就不举了,跟PCF8591大同小异,多了一个在读以前要写控制信息这部分。直接看看完整的读操做流程。
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(); }
这里有几个位置须要说明一下,就是时序上的冲突问题。
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转换数据处理,将信息能够显示在液晶屏幕上。由于这套板子的程序已经彻底作好了,在出厂以前就已经测试好了;为了保险起见,我也将程序烧录其中作了一下测试,其实是能够通得过的。这里就再也不插图看了,直接将测量结果以表格的方式给出来,避免打广告的嫌疑。
表2.2 数据测量表
测量次数 | 万用表测试值(V) | AD转换后对应的值(V) |
1 | 0.00 | 0.00 |
2 | 1.56 | 1.60 |
3 | 2.45 | 2.52 |
4 | 4.87 | 4.98 |
万用表的型号是LINI-T UT51。从上述的测试值对比,转换的值仍是相差挺大的,虽然A/D转换器存在量化偏差,可是不至于这么大。分析了下缘由,实际上是受参考电压的影响,A/D转换计算是采用5.00V的参考电压,而实际的参考电压为4.87V,根据转换的公式,能够知道计算出来的电压与实际电压有一个比值系数 k = 5.00/4.87,有了这个关系,把AD转换后的值除以这个系数,就能够获得实际的电压了,以第二次测试结果为例说明一下。
1.60 / k = 1.60 * 4.87 / 5.00 = 1.5584,取小数后最后两位,第三位小数位进行四舍五入处理,那么实际的转换值为1.56,这与万用表的测试值是相吻合的。
关于器件的量化偏差,与本节的内容就不大相关了,这里就不分析这部分了。上述实验证实,经过MCU与PCF8591的I2C通讯是成功的。
3.Conclusion
整体来讲,I2C的协议仍是不复杂的,不过学好它确实有点不容易,须要从多个角度去思考和体会。如今规范的协议不算太多,可以很好的掌握I2C协议就至关于上了一个新的台阶。
就我的来讲,对I2C的协议理解仍是不够深入的,之前学了一门课程,计算机接口与技术,这门课是专门讲接口的,I2C就是一个很经常使用的串行接口。不过这本书的主要目的是讲硬件方面,即这些接口是怎样用物理的方法实现的。对于使用者,确确实实只要了解这个接口怎么用就能够了,但想从电路设计的角度去深入理解它,我想要学的还有不少。
4.Reference
1). PCF8591 datasheet
2). AT24C02 datasheet