weiqi7777

单片机之DS18B20(用proteus)

0
阅读(6881)

单片机之DS18B20(用proteus)

想用verilog驱动DS18B20。但是想到verilog调试比较困难,首先先用单片机来驱动看看。看看DS18B20是怎么驱动的。

用proteus搭建软件仿真环境,在proteus里面进行仿真。

clip_image002

这里搭建了三个DS18B20。因为DS18B20是可以在一根线上挂载多个的。这里就挂载了三个。不过只用到了前两个。

首先介绍下DS18B20。这个网上介绍比较多,还比较详细,详情可自行百度。这里就简单介绍一下。

DS18B20是一个温度检测芯片,能检测周围环境温度。然后把温度值保存在两个8位寄存器中。

clip_image004

这里保存的值是以补码方式保存的。因为可以检测负温度。

这里是可以改变转换温度的分辨率的。由配置寄存器设置:

clip_image006

clip_image007

可以见,在不同的分辨率,转换时间是不一样的。

这里注意,由温度寄存器发现,只有11位数据,怎么有12位分辨率,那是因为包括了符号位,所以就是12位分辨率。即当正温度,温度寄存器的高寄存器的高5位为0,当负温度,温度寄存器的高寄存器的高5位全为1.

这里其他分辨率,指的是包括小数第几位。9位分辨率,包括小数后1位。芯片默认为是12位分辨率。即转换时间为75ms。

DSB20内部有64位ROM单元和9字节的暂存器单元。

64位ROM存的是DS18B20的唯一的序列号。每个DS18B20有自己唯一的序列号,这样才能挂载多个DS18B20在一根总线上。

序列号的信息

clip_image009

高8位是CRC检验位。对后面的56位数据进行CRC检验后的8位值。使用检验式为

clip_image010

在本次试验中

第一个DS18B20器件的serial number为000000B8C530,系列码为28。那么CRC检验为8E。如下图:

clip_image012

这里要注意检验的数据按ROM单元地址从低到高检验的。所以这里CRC效验为8E。

整个8字节数据从高到低为:

8E_00_00_00_B8_C5_30_28.

效验的时候从低到高。

第二个DS18B20器件的serial number为000000B8C531,系列码为28。那么CRC检验为B9。

clip_image014

9字节的暂存器单元。

clip_image016

第一个字节保存的温度的低字节数据,第二个数据保存的温度的高字节数据。

第三个和第四个是用来警报用到,因为有时候需要当温度超过设定的范围的时候,能够进行报警。这个就是设置上限和下限的。

第五个就是配置寄存器,就是之前说的配置分辨率用的。

后面三个是固定的。最后一个是前8个字节的数据的CRC检验,没有什么用。

可以看到边上有三个单元的EEPROM单元。用来保存设定的温度上限和下限以及配置寄存器值。因为掉电后,暂存器单元数据会丢失,所以就可以将数据保存到EEPROM中,这样就掉电不丢失了。

其实说白了,对于这个芯片,主要就是读取暂存寄存器的前两个字节的值,因为这两个字节的值保存有温度信息。但是怎么能够读取这两个字节的值,就需要根据datasheet的时序规定来操作了。

因为是一根线,而且这根线又是双向口,所以要实现能读和能写,对时序就比较要求严格。在什么时间做什么事,就要做什么事。

对于操作DS13B20,顺序为:

clip_image017

比较简单吧。但是时序要求比较严格的。

1、 初始化:

总线上的所有处理,均从初始化开始,即初始化之后,才是真正的对DS18B20进行读写操作。即在每一次操作之前,首先要初始化,然后才能发数据和读数据。

初始化包括,总线发送一复位脉冲,然后读取存在脉冲。

其时序:

clip_image019

首先单片机拉低总线,保持时间在480us到960us之间,然后释放总线,等待15-60us,读取总线数据,如果为0,表示接收到存在脉冲,即总线上有DS18B20器件。DS18B20器件给60-240us低电平后会释放总线。这里要注意,从单片机释放总线,要等待最少480us后,才能有后续操作。

代码为:

bit reset_and_getack()
{
   bit ack;

   DQ = 1;
   delay_20us(1);
   DQ = 0;
   delay_20us(60); //delay 610us
   DQ = 1;  //release bus
   delay_20us(6);  //delay 70us;
   ack = DQ;
   while(!DQ);  //wait ds18b20  release bus 
   delay_20us(50); // delay 510 us
   return ack;
} 

这里定义了一个bit变量ack,用来保存读取的响应信号值,为0的话,说明有响应,1表示没有响应。DQ就是指DS18B20的数据总线。

这里的延时,是通过软件仿真仿真出来的。

对于delay_20us(k) 总延时大致为10+(k-1)*10 us的时间。

这里,让总线为高电平,保持20us。然后拉低总线,延时610us,610us在规定的480us到960us之间,符合要求。然后释放总线,延时70us,超过等待最大响应的60us,读取总线上值,保存到ack中。然后一个while循环,等待总线拉高,即DS18B20释放总线。总线高后,延时510us,满足时序中规定单片机释放总线后,要小要等待480us。然后把读到的响应信号值返回。

以上就是复位和读取存在脉冲过程。这样就对DS18B20进行了初始化,然后就可以操作了。初始化之后,发送的第一个数据,是认为是rom操作命令的,这个其他总线的规则是一样的。

2、 rom操作

一旦总线检测到存在脉冲,即有DS18B20挂在总线上后,然后就可以对器件ROM进行操作。这里对ROM操作,可认为是发命令。

总共有5个命令:

clip_image020

这个就是读8-bit ROM的值。ROM读取的值是从低字节开始读,然后读到高字节。括号里面的33,指发送字节33,就表示是读ROM。然后后面就可以依次读8字节数据。

clip_image021

这个就是寻址特定的DS18B20器件,因为每个DS18B20器件的序列号不一样,所以要操作特定的DS18B20,就需要首先匹配他的序列号,这样才可以操作。命令码为55.

clip_image022

允许主机不提供64位ROM编码,访问DS18B20。但是注意,这样只能是写数据才可以,读数据就不行了。因为会有冲突。

clip_image023

这个由于不知道DS18B20的序列号,所以就需要搜索一下,识别DS18B20器件的序列号是多少。

clip_image024

流程和搜索ROM流程一样。搜索是否温度测量在警告的情况下。

本次中,主要用到匹配rom和跳过rom。因为rom的值都是已知的,所以不需要搜索和读。本次也没有用到告警功能。

然后就开始数据的读写。

对于写:

clip_image026

上面左是写0,右是写1.可以看出写0,是有时间限制的,为60us到120us。而写1是没有时间限制的。但是有最开始单片机要写总线0的最小1us时间限制,而且写0时间不能超过15us,然后就要释放总线。

以上都是写一位的时序,需要写多位的时候,重复即可。

以下是写的程序:

void write_byte(uint8_t dat)
{
   uint8_t mask;
   DQ = 1;
   delay_20us(1);	//delay 20us
   for(mask=0x01; mask!=0; mask<<=1)
   {
	 DQ = 0;
	 _nop_();
	 _nop_();
	 if((mask & dat) == 0x00)
	    DQ = 0;
	 else
	    DQ = 1;
	 delay_20us(8); //delay 90us
	 DQ = 1; //release bus
   }
}

分析一下:

首先单片机对总线拉高,保持20us,然后拉低总线,保持两个_nop_();对于_nop_()这个函数,是一个空指令,但是还是要耗费一个指令周期,即1us。这里就相当于拉低后,延时2us。然后判断发送数据是0还是1.如果是0.那就继续发0.如果是发1,那么就发1.然后在延时80us,释放总线。这里80us是根据写0的时序决定的,因为写0规定时间在60us到120us内。这里选取80us。

这样就实现了一位数据的写。这里需要注意,DS18B20写数据是从低位开始发送的,然后在发高位的值。所以这里mask是从0x01然后向左移位的。

这里要注意if((mask & dat) == 0x00) 中间(mask & dat)是需要括号的。如果是

if(mask & dat== 0x00)的话,==的优先级是要高于&的。所以就变成了mask&(dat==0x00)。所以结果都是0.就造成了数据都是一直发1.这个硬件单步调试的时候发现的。所以推荐对于复杂的操作,要加括号,避免优先级的问题造成程序逻辑错误。

对于读:

clip_image028

左边是读0,右边是读1.时序可以看出,在开始单片机要对总线写0,并保持至少1us时间。然后释放总线,在15us之前的时间读取总线值。读0操作,至少需要60us,而读1操作至少只需要15us。

下面是程序:


uint8_t read_byte(void)
{
   uint8_t temp=0;
   uint8_t mask;
   DQ = 1;
   delay_20us(1);
   for(mask=0x01; mask!=0; mask<<=1)
   {
	  DQ = 0;
	  _nop_();
	  _nop_();
	  DQ = 1; //release bus

      _nop_();
	  _nop_();
	  _nop_();
	  _nop_();
	  if(DQ == 1)
	     temp |= mask;
	  else
	     temp &= ~mask;
	  delay_20us(8); //delay 90us
   }
   DQ = 1;
   delay_20us(3);
   return temp;
}


分析一下:

首先单片机对总线写1,保持20us。然后开始依次读每一位,读的数据也是从低位开始读的,最后读的才是高位。单片机拉低总线,保持2us。然后释放总线,延迟4us。读取总线值,判断是0还是1,然后对temp值赋值。这里定义了一个temp变量用来存储读取的8位值。

这里要注意位操作。|1操作是将某一位置1,&0操作是将某一位置0.

最后在延时90us。为了满足读0的60u时间限制。

这里读取总线值是在9us左右读取总线值的。一个_nop_()需要1us,而一个DQ = 1大致为3us。所以这里大致为9us。满足15us之前读值时间限制。

然后将读和写函数进行拼接,即可完成程序了。

第一个函数,开启转换:


bit start18b20()
{
  bit ack;
  ack = reset_and_getack();
  if(ack == 0)
  {
     write_byte(0xcc);  //skip rom
	 write_byte(0x44);	//start convert		   
  }
  return ~ack;
}


先对总线进行复位和读取存在脉冲,当有存在脉冲响应后,发送命令0xcc,即不检测rom。直接对总线上所有DS18B20器件进行操作。然后才发命令0x44,表示开启转换。

clip_image030

从上图可以看出,有6个命令。其中44表示启动温度转换。芯片对温度检测不是自己一直转换的,这和普通AD不一样。是需要外部给命令,然后才转换的。而且给一次命令,只会一次转换,所以要不断的给命令,这样才能读取实时温度值。

最后返回响应信号的取反。这样就启动DS18B20的温度转换。然后就要读取温度值了。

第二个函数,读取温度值。


uint16_t get18b20temp(uint8_t serial_number[8])
{
    uint8_t  i;
   	uint16_t  temp;
	uint8_t  msb,lsb;
	bit ack;
	ack = reset_and_getack();
	if(ack == 0)
	{
	  write_byte(0x55);
	  for(i=0; i<8; i++)
	  {
	     write_byte(serial_number[i]);
	  }
	 write_byte(0xBE);	//read register		 
	 lsb = read_byte();
	 msb = read_byte();
	}  
	temp = ((uint16_t)msb<<8) | (uint16_t)lsb;
	return temp;
}


首先是复位和读取存在脉冲。每一次操作之前,都需要这个过程。然后发送命令55,即匹配ROM。因为总线上挂载有多个DS18B20,所以需要进行选择。然后发送的64位序列号。这里序列号是存在一个二维数组中.

uint8_t serial_number[3][8] = {

{0x28,0x30,0xc5,0xb8,0x00,0x00,0x00,0x8e},

{0x28,0x31,0xc5,0xb8,0x00,0x00,0x00,0xb9},

{0x28,0x32,0xc5,0xb8,0x00,0x00,0x00,0xe0},

};

这里用了一个二维数组。每一维存的是对应的DS18B20的序列号。如第一个serial_number[0]存放的就是总线上第一个DS18B20器件的序列号,即64bit rom的值。这里注意存放顺序是低字节在前,高字节数据在后。

然后就依次的把序列号数据发送。发送完毕后,会和总线上的一个DS18B20器件匹配,这个时候就相当于总线归选中的器件独占。然后发命令0xBE。从命令集中,可以看出,这个命令是读取暂存寄存器值的。

之前说过,暂存寄存器总共有9个。读取的时候是从低依次读到高,所以最开始读到的温度的低8位数据,然后读到的是温度的高8位数据。因为只需要知道温度,其他寄存器的值不关心,这里就没有读取了。

对读到的值lsb和msb进行下处理,放到16位变量temp中,并返回这个值。这样外部调用这个函数就直接可以得到温度的值。

将上述两个函数封装,得到整个读取温度值函数,这个函数就供外部使用。对于这个函数有一个输入的序列号,即要对那个器件操作。返回值是该器件读取的温度值。


uint16_t get_temp_from_18b20(uint8_t serial_number[8])
{
   uint16_t temp;
   bit ack;
   ack = start18b20();
   if(ack == 0)
       return 0;
   delay_ms(1000);	 //wait convert
   temp = get18b20temp(serial_number);
   return temp;
}


上面中间有个延时1000ms。这个是不能省掉的。因为之前说过,转换是需要时间的,这里12位分辨率是需要750ms时间转换的,这里就延时了1000ms。等待转换结束。

这样,整个一个DS18B20驱动就搞定了,主函数只需要调用上述函数即可读取值了。

主函数


uint8_t serial_number[3][8] = {
  {0x28,0x30,0xc5,0xb8,0x00,0x00,0x00,0x8e},
  {0x28,0x31,0xc5,0xb8,0x00,0x00,0x00,0xb9},
  {0x28,0x32,0xc5,0xb8,0x00,0x00,0x00,0xe0},
};

void main()
{
  	uint16_t temp;

	ds18b02_init();
	while(1)
	{
	   temp = get_temp_from_18b20(serial_number[0]);
	   temp >>= 4;
	   P3 = temp&0xff;
	   temp = get_temp_from_18b20(serial_number[1]);
	   temp >>= 4;
	   P1 = temp&0xff;
	}
}


最开始定义了一个二维数组,存储总线上挂接的DS18B20器件的rom地址。在一个无线循环中,不断的调用get_temp_from_18b20函数获取温度值,第一个是读取第一个DS18B20的16温度值。对这个16值处理下,去掉小数部分,主要整数部分,在将整数部分的低8位赋值给P3。

第二个读取的值也处理下,赋值给P1。

这样,我们只要看P3和P1端口的值就知道读取的值是否正确了。

在proteus中仿真。

clip_image032

第一个的温度值为5.所以P3端口值为0x05.正确。同时可以看到暂存寄存器中的温度值为0x0050.

第二个温度值为16,所以P1端口值为0x10.正确。同事可以看到暂存寄存器中的温度值为0x0100。

以上就驱动了DS18B20。

对于器件序列号,是怎么更好的,选择DS18B20,右键。

clip_image033

选择第二个选项,然后

clip_image034

在蓝色部分填入值即可。下面的选项要选no。这里写了b8c530.那么对于这个器件来说,serial number为000000b8c530.因为是6个字节,所以高位要补0.

有了这个,就可以得到序列号了。CRC效验需要自己算,自己网上找软件进行计算。家族编号,这样是统一的0x28。

如果总线只有一个器件的话,就不需要序列号了,直接发命令CC,跳过rom检测,因为只有一个器件,所以读取是不会有冲突的。

即读取函数改为:


uint16_t get18b20temp(uint8_t serial_number[8])
{
uint8_t i;
uint16_t temp;
uint8_t msb,lsb;
bit ack;
ack = reset_and_getack();
if(ack == 0)
{
write_byte(0xcc);
write_byte(0xBE); //read register
lsb = read_byte();
msb = read_byte();
}
temp = ((uint16_t)msb<<8) | (uint16_t)lsb;
return temp;
}