(原创)巩固理解基于DS18B20的1-wire协议(MCU,经验)


1.Abstract

    如前篇随笔所写,将以前遇到最难懂的两个部分重拾一下。前一篇写的是I2C协议(http://www.cnblogs.com/hechengfei/p/4117840.html),这一篇就来写基于DS18B20的1-wire协议。以前用到它的时候是借助别人写的文章和配套程序,整了一个一知半解;现在重新学习一遍,我想参考的资料就只有唯一一份了——芯片手册,最为权威和齐全的。在平静下来写这个的时候,着实花了很大精力将手册从头看到尾,部分的时序图在草稿纸上画了画,理解也更加深刻了一些。

    总线的结构都是差不多的,相对于上篇中的I2C总线,1-wire总线结构从逻辑上来说还简单一点,要说复杂的地方,那就是比较严格的时序了——规定的时间内必须完成某事,否则容易出岔子。关于1-wire总线的优劣势,在仔细分析完一个具体的器件以后,再来做结论比较适合,下面开始正文。

2.Content

    挂在1-wire总线上的,一般都只有一个主器件,一个或者多个从器件;而且通常情况下单个器件的情况居多。构成主器件的,都是微控制器或者可编程逻辑器件(CPLD/FPGA),从器件就是各个厂商生产的具体功能芯片了(尤其是DALLAS Semiconductor 公司)。下面以一个具体的实例为例,分析下它是如何工作的。器件使用典型的DS18B20温度传感器,在1-wire总线结构中,它只能作从器件。

  2.1 协议分析

    和分析I2C的协议一样,把挂在总线上的器件分为两种;一种是主器件,它具有控制总线的权利,主动与从器件打交道;另外一种是从器件,它受主器件的指挥;一般都是站在主器件的角度去看总线(因为从器件是受控对象,它的功能已经实现好了,只需要给它特定的指令,它就会自动地去做某件事儿)。

    和其他总线通信一样,主器件需要知道从器件的一些基本信息——器件地址和器件内部的控制信息。对于DS18B20来说,解决它的器件地址还真不是一件容易的事儿,看看它的地址信息是怎么分配的。

FIG2.1 DS18B20内部ROM信息

    DS18B20的内部ROM有64位,分为8个字节(8 * 8 = 64bit);第一个字节是器件家族的固定编码,DS18B20为28H;中间的6个字节48位为器件的序列号,也就是器件地址(最为关注的就是它了);最后一个字节是前面 8 + 48 = 56 位的循环冗余校验码(CRC CODE),若是主器件想读取该器件的ROM信息,可以将前56位编码做一个循环冗余校验,生成一个8bit的循环冗余校验码,然后和最后的这一个字节进行对比,若相同,则说明读取信息是正确的,否则,主器件的读取是有误的。

    从上面的分析中,找到了关键的地址信息——中间的这个48bit序列号,按照数学的逻辑组合,它可以分配2的48次方281,474,976,710,656个地址,以亿为单位算,有281474亿个地址!实在是太多了,所以器件生产商将每一个器件都做了一个固定的序列号;不过这样有一个麻烦的是,拿到手中的器件的地址信息最开始是不知道的,要想获得某个器件的地址,必须得用主器件将它的这个序列号读取出来;然而实际上,在1-wire总线上挂了多个DS18B20的器件,要将它们的地址都获取出来,是非常麻烦的;芯片手册里给出了一种算法,将不同的器件识别出来,这里的识别指的不是准确的获取它们的序列信息。所以通常情况下,在未知器件序列号的情况下,都是采用单个从器件跟主器件连接的。要想主器件上一根I/O口线连接多个从器件,那么就得想想其他办法了。

    这里提出一种简单的方法供参考。鉴于DS18B20的数据输出口传输的是数字信号,故MCU的数据端口也应该配置的数据输入输出端口;将它们抽象起来看,就是一个数字端口能分时的与多个数字端口连接,常采用方法就是用选择器,n个地址的选择器就可以分2的n次方个连接端口,下图是以8选1的集成芯片74151做例子解释。

FIG2.2 74151多路选择器

    本图的重要目的是为了解释接口的关系,图纸并不完全。左边是DS18B20的插座,数据接入选择器的输入端,输出端和输入端口选择的地址接在MCU输入输出端子上。如果还需要接更多的DS18B20器件,可以采用选择器路数更多的功能芯片,如16选1,32选1等;如果还需要接有更多的从器件,按照这种逻辑可以使用CPLD/FPGA器件,它们的引脚数比较众多,是做数字设计的好帮手。这样用多路选择器的方法可以解决多个DS18B20分时共用一个数据端口的问题。

    由上述可知,1-wire总线上在同一个时间点上主器件只和一个从器件相连,构成一对一的关系,所以主器件知道从器件的地址信息就不再有意义了。在1-wire通信中,主器件需要了解从器件的控制信息显得更为重要一些了。

    对应于主器件,和从器件通信只有两件事儿,其一是向从器件写数据,另外一个是从从器件读数据。基于DS18B20的1-wire通信协议将器件的控制信息全部列举出来,主器件只需要发送这些控制命令,就可以执行相应的操作了。

FIG2.3 DS18B20的指令对照表

    由于DS18B20内部的寄存器不多(只有七个,其中有三个作为保留),所以对器件进行读写的控制就比较简单,发送相应的读写命令指令以后,器件就会按照协议进行数据交换。整体的读写操作流程如下图所示。

FIG2.4 主器件向从器件写数据

FIG2.5 主器件从从器件读数据

    和其他的通信协议一样,主器件的写操作分为五大步骤,建立通信、写ROM操作指令、写控制操作指令、数据交换、结束通信。

    1-wire协议对它们进行了详细地规定;建立通信是一段480us~960us的低电平复位,如下图所示。

FIG2.5 DS18B20的初始化

    操作首先是主控给一段480-960us的低电平,然后释放总线(将总线拉高);紧接着DS18B20等待15-60us以后,输出一个时常为60-240us的低电平,表示收到主器件发来的初始化信息;之后将总线释放掉。主器件可以通过从器件返回的这个低电平判断器件是否正常连接(相当于一个应答)。

    写ROM操作指令。具体主器件是如何将数据一位一位地通过1-wire传送到从器件,这个在后边讲述;这里将它抽象出来,作为一个指令来叙述。 写ROM的操作指令有5个,如下表所示。

表2.1 ROM指令对照表

ROM指令 指令功能
33H READ ROM
55H MATCH ROM
F0H SERACH ROM
ECH ALARM SERACH ROM
CCH SKIP ROM

    虽然器件给出了五种ROM的操作指令,但是在实际应用中,主器件与从器件的连接是1对1的,所以一般情况下是不需要对ROM数据进行太多的处理的,所以CCH(SKIP ROM)这条ROM指令用得最多。

    写一个控制操作指令。在主器件进行完写ROM操作指令以后,紧接着就是要写一个控制指令了,控制指令表由图FIG2.3给出了,这里只将控制指令部分和指令功能部分列成表给出。

表2.2 控制指令对照表

控制指令 指令功能
44H Convert T
BEH Read Scratchpad
4EH Write Scratchpad
48H Copy Scratchpad
B8H Recall E2
B4H Read Power Supply

    简单的解释一下吧,44H(Convert T)指令是让从器件进行温度转换的指令,在正确读取温度数据之前,需要对当前的温度进行转换;BEH(Read Scratchpad)指令是将从器件中的9个RAM数据读取出来,DS18B20的内部RAM结构图如下图所示。

FIG2.6 DS18B20内部RAM一览表

    前两个数据是温度转换数据,紧接着的两个是温度上下限控制数据,第五个是配置寄存器,第6到第8个是保留寄存器,第9个是前边数据的CRC校验数据。与掉电数据丢失的SCRATCHPAD RAM块相比,器件内部设置了一个掉电不丢失的电可擦除E2RAM,用于保存温度上下限控制数据和配置寄存器数据。

    这些数据是按BYTE0 到BUTE8的顺序逐步读出的。若需要读取E2RAM块的数据,则需要先执行将E2RAM的数据写入到SCRATCHPAD的控制指令。下面简要的说明一下其他控制指令的功能。

    BEH(READ SCRATCHPAD)指令是读取SCRATCHPAD的指令,执行此条指令以后,从器件会将内部SCRAPTCHPAD的数据逐步的发送至1-wire上。当然在读的这个过程中,主器件随时可以中断读操作,只对特定感兴趣的数据读取。

   4EH(WRITE SCRATCHPAD)指令是写SCRATCHPAD指令,执行此条指令以后,主器件需要完整地将温控上下限数据(TH 与TL)和配置数据(CONFIG)依次发送到1-wire上。值得注意的是此条指令的写只是将数据存放到SCRATCHPAD中,而不是E2RAM中,它是会掉电丢失的。

   48H(COPY SCRACTCHPAD)指令是将SCRATCHPAD中的温控上下限数据(TH与TL)和配置数据(CONFIG)写到E2RAM中,用于掉电数据保持。执行此条指令以后后续不需要再写数据。

    B8H(RECALL SCRATCHPAD)指令是将E2RAM的数据读到SCRATCHPAD中,一般是用于读取E2RAM的数据进行校验的场合。执行此条指令以后后续不需要再写数据。

    B4H(READ POWER SUPPLY)指令是读取芯片的供电方式。一种是独立的电源供电,返回1;另外一种是用DQ引脚端复合供电,返回0。执行此条指令以后后续不需要再写数据。

    了解总体的数据读写操作流程以后,剩下的就是具体的读写操作过程了。因为读写的操作过程数据线上只有0和1的变化,没有时钟的辅助,所以DS18B20对时序的控制非常严格。还是站在主器件的角度去看。

FIG2.7 主器件写一位0或者1数据时序操作

    主器件的写是一个特定时间内完成特定数据的变化的过程。主器件首先要发送一个大于1us的低电平,后续就发送相应的数据位,整个1位写的时常至少为60us。如上图左边写0的过程,主器件首先发送一个大于1us的低电平电平,由于数据是0,所以后续的电平为低电平,直至整个位长时间结束(至少60 - 1us)。写完1位数据以后,需要释放数据线,否则数据线低电平时间超过480us,则导致器件开始初始化操作。右边写1的过程也是一样,主器件首先发送一个大于1us的低电平,由于数据是1,所以发送完低电平以后就需要释放数据线,数据线高电平时间一直保持到整个位长时间结束(至少60-1us);与发送1相比,最后可以不需要释放总线(因为数据线本就是高电平)。图中灰色的方框内给出了典型的操作时间;在前15个us以内,写操作主控需要将数据线拉低至少1us,然后将要发送的数据电平发送到数据上并保持稳定,这整段的时间最好在15us左右,余下的15+30 = 45us是从器件DS18B20读取数据(电平采样)的过程;采样完成以后,需要释放数据线,两位数据读取的时间要大于1us。

FIG2.8 主器件读取一位0或者1数据的时序操作

    理解了主器件写一位数据的操作流程,那么来理解主器件读一位数据的操作流程就比较简单了,它们的格式都是一样的。主器件读取一位数据之前,也需要发送一个大于1us的低电平,然后等待从器件将相应的数据放到数据线上直到稳定;主器件等待一段时间以后对数据线进行采样,如果数据线为高电平,表示从器件发送了一个1;相反,如果数据线为低电平,则表示从器件发送一个0数据。整个读取一位数据的时长至少为60us,1位数据读取完毕以后,从器件会自动的将数据线释放掉,做好发送下一位数据的准备。

    插一段理解。纵观通信特点,读和写都的过程都是差不多的;每一次读或写的开始,主器件都需要发送一个至少1us时常的低电平,可以认为是数据传输的开始,紧接着是将数据放到数据线并稳定下来,这整个的时常大约为15us,余下的时间就是等待主器件或者从器件的采样操作,最后就是将数据线释放掉,准备下一次数据的传输操作。

  2.2 基于MCU的协议实现

    用MCS-51来实现一下,采用的是传统51单片机,12M晶振,故指令时间最短为1us。

   2.2.1 DS18B20 复位

bit Init_DS18B20(void)
{
 bit dat=0;          // DQ复位
 DQ = 1;            
 DelayUs2x(5);    // 稍做延时,高电平维持一小段时间
 DQ = 0;         //单片机将DQ拉低
 DelayUs2x(200); //精确延时 大于 480us 小于960us
 DelayUs2x(200);
 DQ = 1;        // 拉高总线
 DelayUs2x(50); //15~60us 后 接收60-240us的存在脉冲
 dat=DQ;        //如果x=0则初始化成功, x=1则初始化失败
 DelayUs2x(25); //稍作延时返回
 return dat;
}

    2.2.2 MCU读取一个字节

unsigned char ReadOneChar(void)
{
unsigned char i=0;
unsigned char dat = 0;
for (i=8;i>0;i--)
 {
  DQ = 0; // 给低电平信号
  dat>>=1;
  _Nop();
  _Nop();  // 维持一段时间
  DQ = 1; // 给脉冲信号
  if(DQ)
   dat|=0x80;
  DelayUs2x(25); // MCU采样
 }
 return(dat);
}

   2.2.3 MCU写一个字节

void WriteOneChar(unsigned char dat)
{
 unsigned char i=0;
 for (i=8; i>0; i--)
 {
  DQ = 0;            // 先发送一个低电平
  _Nop();
  _Nop();             // 低电平时常大于1us 
  DQ = dat&0x01;     // 将数据发送到总线上
  DelayUs2x(25);     // 保持数据一段时间
  DQ = 1;             // 释放数据线
  dat>>=1;
 }
DelayUs2x(25);         // 时序缓冲处理,可省略
}

   2.2.4 读取温度转换值

unsigned int ReadTemperature(void)
{
unsigned char a=0;
unsigned int b=0;
unsigned int t=0;
Init_DS18B20();
WriteOneChar(0xCC); // 跳过读序号列号的操作
WriteOneChar(0x44); // 启动温度转换
DelayMs(750);        // 等待数据转换完毕
Init_DS18B20();        // 初始化DS18B20
WriteOneChar(0xCC); //跳过读序号列号的操作 
WriteOneChar(0xBE); //读取温度寄存器等(共可读9个寄存器) 前两个就是温度
a=ReadOneChar();   //低位
b=ReadOneChar();   //高位

b<<=8;
t=a+b;                // 数据合并

return(t);
}

    值得注意的是,主器件每次对从器件的操作,都需要符合固定的通信格式,详细地可以参照2.1节协议分析。

  2.3 协议验证

    限于手上只有一个DS18B20温度芯片,所以直接将它与MCU相连就可以了,采用默认的12位温度转换方式。不考虑扩展的情况。测试的工具是一个量程为0置100度的液体温度计,温度计的分辨率为0.1度。下面是一个室内室外早晚的测试结果表。

表2.2 温度测试表

测量次数 温度计测量值(度) DS18B20测量值(度)
1 12.5 12.496
2 16.5 16.496
3 18.7 18.695
4 14.8 14.795

     对照上述的结果表,可以看出测量结果还是比较精确的。产生误差的原因有两个方面,一个是传感器量化的误差,另外一个是温度计的测量精度有限,人眼来读取数值时难免会有些误差。

    从上述的结果分析中可以看出,主器件MCU与从器件DS18B20之间按照1-wire的通信方式是成功的。

3.Conclusion

    总体上来说,1-wire协议还是不复杂的,但是对时序的要求还是比较高;相对其他的两线或者三线协议,1线协议要实现单根线上挂多个器件是非常麻烦,这或许也是1-wire协议用得不是很广泛的原因吧。

4.Reference

1). AT24C02 datasheet

mcu