单片机之DS18B20(用proteus)
0赞单片机之DS18B20(用proteus)
想用verilog驱动DS18B20。但是想到verilog调试比较困难,首先先用单片机来驱动看看。看看DS18B20是怎么驱动的。
用proteus搭建软件仿真环境,在proteus里面进行仿真。
这里搭建了三个DS18B20。因为DS18B20是可以在一根线上挂载多个的。这里就挂载了三个。不过只用到了前两个。
首先介绍下DS18B20。这个网上介绍比较多,还比较详细,详情可自行百度。这里就简单介绍一下。
DS18B20是一个温度检测芯片,能检测周围环境温度。然后把温度值保存在两个8位寄存器中。
这里保存的值是以补码方式保存的。因为可以检测负温度。
这里是可以改变转换温度的分辨率的。由配置寄存器设置:
可以见,在不同的分辨率,转换时间是不一样的。
这里注意,由温度寄存器发现,只有11位数据,怎么有12位分辨率,那是因为包括了符号位,所以就是12位分辨率。即当正温度,温度寄存器的高寄存器的高5位为0,当负温度,温度寄存器的高寄存器的高5位全为1.
这里其他分辨率,指的是包括小数第几位。9位分辨率,包括小数后1位。芯片默认为是12位分辨率。即转换时间为75ms。
DSB20内部有64位ROM单元和9字节的暂存器单元。
64位ROM存的是DS18B20的唯一的序列号。每个DS18B20有自己唯一的序列号,这样才能挂载多个DS18B20在一根总线上。
序列号的信息
高8位是CRC检验位。对后面的56位数据进行CRC检验后的8位值。使用检验式为
在本次试验中
第一个DS18B20器件的serial number为000000B8C530,系列码为28。那么CRC检验为8E。如下图:
这里要注意检验的数据按ROM单元地址从低到高检验的。所以这里CRC效验为8E。
整个8字节数据从高到低为:
8E_00_00_00_B8_C5_30_28.
效验的时候从低到高。
第二个DS18B20器件的serial number为000000B8C531,系列码为28。那么CRC检验为B9。
9字节的暂存器单元。
第一个字节保存的温度的低字节数据,第二个数据保存的温度的高字节数据。
第三个和第四个是用来警报用到,因为有时候需要当温度超过设定的范围的时候,能够进行报警。这个就是设置上限和下限的。
第五个就是配置寄存器,就是之前说的配置分辨率用的。
后面三个是固定的。最后一个是前8个字节的数据的CRC检验,没有什么用。
可以看到边上有三个单元的EEPROM单元。用来保存设定的温度上限和下限以及配置寄存器值。因为掉电后,暂存器单元数据会丢失,所以就可以将数据保存到EEPROM中,这样就掉电不丢失了。
其实说白了,对于这个芯片,主要就是读取暂存寄存器的前两个字节的值,因为这两个字节的值保存有温度信息。但是怎么能够读取这两个字节的值,就需要根据datasheet的时序规定来操作了。
因为是一根线,而且这根线又是双向口,所以要实现能读和能写,对时序就比较要求严格。在什么时间做什么事,就要做什么事。
对于操作DS13B20,顺序为:
比较简单吧。但是时序要求比较严格的。
1、 初始化:
总线上的所有处理,均从初始化开始,即初始化之后,才是真正的对DS18B20进行读写操作。即在每一次操作之前,首先要初始化,然后才能发数据和读数据。
初始化包括,总线发送一复位脉冲,然后读取存在脉冲。
其时序:
首先单片机拉低总线,保持时间在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个命令:
这个就是读8-bit ROM的值。ROM读取的值是从低字节开始读,然后读到高字节。括号里面的33,指发送字节33,就表示是读ROM。然后后面就可以依次读8字节数据。
这个就是寻址特定的DS18B20器件,因为每个DS18B20器件的序列号不一样,所以要操作特定的DS18B20,就需要首先匹配他的序列号,这样才可以操作。命令码为55.
允许主机不提供64位ROM编码,访问DS18B20。但是注意,这样只能是写数据才可以,读数据就不行了。因为会有冲突。
这个由于不知道DS18B20的序列号,所以就需要搜索一下,识别DS18B20器件的序列号是多少。
流程和搜索ROM流程一样。搜索是否温度测量在警告的情况下。
本次中,主要用到匹配rom和跳过rom。因为rom的值都是已知的,所以不需要搜索和读。本次也没有用到告警功能。
然后就开始数据的读写。
对于写:
上面左是写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.这个硬件单步调试的时候发现的。所以推荐对于复杂的操作,要加括号,避免优先级的问题造成程序逻辑错误。
对于读:
左边是读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,表示开启转换。
从上图可以看出,有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中仿真。
第一个的温度值为5.所以P3端口值为0x05.正确。同时可以看到暂存寄存器中的温度值为0x0050.
第二个温度值为16,所以P1端口值为0x10.正确。同事可以看到暂存寄存器中的温度值为0x0100。
以上就驱动了DS18B20。
对于器件序列号,是怎么更好的,选择DS18B20,右键。
选择第二个选项,然后
在蓝色部分填入值即可。下面的选项要选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; }