DS18B20
简介
DS18B20是一款具有高精度温度测量的芯片,测温范围是-55摄氏度到125摄氏度。
DS18B20 使用单总线协议,总线通讯通过一根控制信号线实现。控制线需要一个弱上拉电阻,这样所有的器件都通过三态或者开漏极端口(就是DS18B20 的 DQ 引脚)连接到总线上。在这个总线系统中,单片机(主机)通过每个器件的唯一 64 位编码识别并寻址总线上的器件。因为每个器件都有唯一的编码,实际上挂在总线上并可以被寻址的设备数量是无限的。
DS18B20 的另一个特点是其可以不需要额外供电运行。这种情况下供电是总线为高的时候,通过单总线在 DQ 引脚上的上拉电阻提供给器件的。总线高信号对一个内部电容充电,然后在总线低的时候,内部电容就会维持对器件供电。这种从单总线获取电源的方法被称为“寄生供电”。 但是一般都是通过 VDD 由外部供电。
DS18B20上电的默认分辨率是12位,对应的温度分度是0.0625(即将采集到的数据乘以0.0625得到的就是实际的温度)。还有9、10、11位的分辨率可供选择,他们对应的温度分度分别为0.5、0.25、0.125。
温度寄存器
这里补充一个知识点:二进制补码,
比如short类型数据占两个字节,范围为-32768~32767如果一个 short a=0xfc90;那么a为 -(0x10000 - 0xfc90)=-(65536-64656)=-880;该传感器的负温度值就是这样计算的。
温度数据存储为 16 位符号扩展温度寄存器中的二进制补码(见下图)。
符号位(S,高5位bint11~bit15)指示温度为正或负:对于正数 S = 0,对于负数 S = 1。
如果DS18B20 配置为 12 位分辨率,温度寄存器中的所有位都将包含有效数据。对于 11 位分辨率,位 0 未定义。对于 10 位分辨率,位 1 和 0 未定义,对于 9 位分辨率位 2,1 和 0 未定义。
下图是在12位分辨率的情况下输出数据和温度读数:
单总线时序
DS18B20 的单总线通讯协议定义了几种信令类型:复位脉冲,存在脉冲,写 0,写 1, 读 0,读 1。除了存在脉冲之外,所有信令都由总线主机发起。
初始化时序——复位和存在脉冲
DS18B20的初始化是主机发送一个复位脉冲(主机通过将单总线拉低至少480 µs),总线主机随后释放总线进入接收模式。当总线被释放后,5kΩ 上拉电阻会把总线拉高。当 DS18B20 检测到这个上升沿,它等待15µs 到 60µs 然后发出存在脉冲(DS18B20把单总线拉低 60µs 到240µs )。
//复位DS18B20
void DS18B20_Rst(void)
{
DS18B20_IO_OUT(); //设置为输出
DS18B20_DQ_OUT=0; //拉低DQ
MY_Delay_us(750); //拉低750us
DS18B20_DQ_OUT=1; //DQ=1
MY_Delay_us(15); //15US
}
//等待DS18B20的回应
//DS18B20复位后会拉低总线60—240us来表示存在
//返回1:未检测到DS18B20的存在
//返回0:存在
uint8_t DS18B20_Check(void)
{
uint8_t retry=0;
DS18B20_IO_IN(); //设置为输入
while (DS18B20_DQ_IN&&retry<200)
{
retry++;
MY_Delay_us(1);
}
if(retry>=200)return 1;
else retry=0;
while (!DS18B20_DQ_IN&&retry<240)
{
retry++;
MY_Delay_us(1);
}
if(retry>=240)return 1;
return 0;
}
/*初始化DS18B20
等待DS18B20的回应
DS18B20复位后会拉低总线60—240us来表示存在
返回1:未检测到DS18B20的存在
返回0:存在*/
void DS18B20_Init(void)
{
DS18B20_Rst();
DS18B20_Check();
}
读时序
DS18B20 只能在主机发布读时隙期间可以传送数据到主机。例如主机可以在发布 Convert T[44h](温度转换指令)后读取温度数据。
所有读时隙必须持续至少 60µs,并且两个写时隙之间恢复时间不少于 1µs。读时隙的产生是通过主机拉低单总线至少 1µs 然后释放总线来实现。主机发起读时隙之后,DS18B20 会开始在总线上传输 1 或 0。DS18B20 通过保持总线高发送 1 并通过拉低总线发送 0。
当传输 0 的时候,DS18B20 会在时隙结束时释放总线,之后总线会被上拉电阻拉回高空闲状态。DS18B20 的输出数据在启动时隙的下降沿后 15µs 之内有效。所以,主机必须在时隙启动之后 15µs 之内释放总线并采样总线状态。
//从DS18B20读取一个位
//返回值:1/0
uint8_t DS18B20_Read_Bit(void)
{
uint8_t data;
DS18B20_IO_OUT(); //设置为输出
DS18B20_DQ_OUT=0;
MY_Delay_us(2); //两个读时隙之间不少于1us
DS18B20_DQ_OUT=1;
DS18B20_IO_IN(); //设置为输入
MY_Delay_us(12); //以上这个过程必须在15us内完成。然后判断数据是0还是1
if(DS18B20_DQ_IN)data=1;
else data=0;
MY_Delay_us(50); //所有读时隙必须持续至少60us
return data;
}
//从DS18B20读取一个字节
//返回值:读到的数据
uint8_t DS18B20_Read_Byte(void)
{
uint8_t i,j,dat;
dat=0;
for (i=1;i<=8;i++)
{
j=DS18B20_Read_Bit();
dat=(j<<7)|(dat>>1);
}
return dat;
}
写时序
有两种写时隙:“写 1”时隙和“写 0”时隙。总线主机通过写1 时隙把一个逻辑1写入DS18B20,通过写 0 时隙把一个逻辑 0 写入 DS18B20。所有写时隙必须持续最少 60µs,并且两个写时隙之间至少有 1µs 的恢复时间。两种写时隙都是通过主机把单总线拉低来发起。
写“1”,把单总线拉低之后,总线主机必须在 15µs 内释放单总线。总线被释放后,5kΩ 上拉电阻会把总线拉高,(这里由主机控制总线输出1也可)。
写 “0”,把单总线拉低之后,总线主机必须在整个时隙期间持续保持总线低(至少 60µs)。
DS18B20 在主机发起写时隙后,会在至少 15µs到 60µs 的时间窗口内采样单总线。如果在这个采样时间窗口总线为高,一个 1 就被写入 DS18B20。如果总线是低,一个 0 会被写入 DS18B20。
//写一个字节到DS18B20
//dat:要写入的字节
void DS18B20_Write_Byte(uint8_t dat)
{
uint8_t j;
uint8_t testb;
DS18B20_IO_OUT(); //设置为输出
for (j=1;j<=8;j++)
{
testb=dat&0x01;
dat=dat>>1;
if(testb) // 写1
{
DS18B20_DQ_OUT=0;
MY_Delay_us(2); //两个写时隙之间至少有 1μs 的恢复时间
DS18B20_DQ_OUT=1;
MY_Delay_us(60); //写时隙必须持续最少 60μs
}
else //写0
{
DS18B20_DQ_OUT=0;
MY_Delay_us(60); //写时隙必须持续最少 60μs
DS18B20_DQ_OUT=1;
MY_Delay_us(2); //两个写时隙之间至少有 1μs 的恢复时间
}
}
}
开启温度转换
程序流程:
1、初始化(复位、存在脉冲)
2、发送Skip ROM(忽略ROM) 指令(0xcc)
3、发送Convert T(温度转换)指令(0x44)
4、初始化(复位、存在脉冲)
5、发送Skip ROM(忽略ROM) 指令(0xcc)
6、发送Read Scratchpad(读取暂存器)指令(0xbe)
7、先获取低8位数据
8、再获取高8位数据
9、判断温度是正还是负
10、温度数据转换
//从ds18b20得到温度值
//精度:0.1C
//返回值:温度值 (-550~1250)
short DS18B20_Get_Temp(void)
{
uint8_t temp;
uint8_t TL,TH;
short tem;
DS18B20_Start (); //开始转换
DS18B20_Init();
DS18B20_Write_Byte(0xcc); // skip rom
DS18B20_Write_Byte(0xbe); // convert
TL=DS18B20_Read_Byte(); // LSB
TH=DS18B20_Read_Byte(); // MSB
if(TH>7) //高5位为1温度值为负数
{
TH=~TH;
TL=~TL;
temp=0;//温度为负
}
else //高5位为0温度值为正数
temp=1;//温度为正
tem=TH; //获得高八位
tem<<=8;
tem+=TL;//获得底八位
tem=((double)tem*0.0625)*100; //将采集到的数据*0.0625转换成实际温度
//*100保留两位为小数
if(temp)
return tem; //返回温度值
else
return -tem;
}
STM32的IO口设置
我在用STM32CubeMX配置工程时发现,STM32的IO口要么只能设置成输入要么只能设置成输出,但是我们使用的是单总线,就是一个IO口纪要输出也要输入。这里提供两个方法:1、通过设置GPIO的寄存器来改变对应IO口的方向;2、就是把IO口配置成开漏输出模式,还要再在IO口接一个上拉电阻,并且在进行数据读取前要先把IO置1。第一个方法我实现过了的是可行的。
STM32的IO口有8中工作模式:
1、输入浮空
2、输入上拉
3、输入下拉
4、模拟输入
5、开漏输出
6、推挽输出
7、推挽式复用功能
8、开漏复用功能
STM32 的每个 IO 端口都有 7 个寄存器来控制。他们分别是:配置模式的 2 个 32 位的端口配置寄存器 CRL 和 CRH(一个控制GPIO的低8位,一个控制GPIO的高8位);2 个 32 位的数据寄存器 IDR 和 ODR(一个控制输入,一个控制输出);1 个 32 位的置位/复位寄存器BSRR;一个 16 位的复位寄存器 BRR;1 个 32 位的锁存寄存器 LCKR;
我们只了解一下CRL、CRH、IDR、ODR。
CRL
具体参考正点原子的寄存器开发手册。
//IO口操作宏定义
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))
#define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))
//IO口地址映射
#define GPIOA_ODR_Addr (GPIOA_BASE+12) //0x4001080C
#define GPIOB_ODR_Addr (GPIOB_BASE+12) //0x40010C0C
#define GPIOC_ODR_Addr (GPIOC_BASE+12) //0x4001100C
#define GPIOA_IDR_Addr (GPIOA_BASE+8) //0x40010808
#define GPIOB_IDR_Addr (GPIOB_BASE+8) //0x40010C08
#define GPIOC_IDR_Addr (GPIOC_BASE+8) //0x40011008
//IO口操作,只对单一的IO口!
//确保n的值小于16!
#define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //输出
#define PAin(n) BIT_ADDR(GPIOA_IDR_Addr,n) //输入
#define PBout(n) BIT_ADDR(GPIOB_ODR_Addr,n) //输出
#define PBin(n) BIT_ADDR(GPIOB_IDR_Addr,n) //输入
#define PCout(n) BIT_ADDR(GPIOC_ODR_Addr,n) //输出
#define PCin(n) BIT_ADDR(GPIOC_IDR_Addr,n) //输入
#define DS18B20_Pin GPIO_PIN_2
#define DS18B20_GPIO_Port GPIOB
//IO方向设置
#define DS18B20_IO_IN() {DS18B20_GPIO_Port->CRL&=0XFFFFF0FF;DS18B20_GPIO_Port->CRL|=8<<8;}
#define DS18B20_IO_OUT() {DS18B20_GPIO_Port->CRL&=0XFFFFF0FF;DS18B20_GPIO_Port->CRL|=3<<8;}
//IO操作函数
#define DS18B20_DQ_OUT PBout(2) //数据端口 PB2
#define DS18B20_DQ_IN PBin(2) //数据端口 PB2
微秒级延时
由于DS18B20要用到微秒延时,但是STM32CubeMX生成的工程中提供的延时函数只有一个毫秒级延时函数,所以需要我们自己编写一个。我这里采用的方法是配置一个定时器用于产生微秒级延时。
因为我选择的芯片是STM32F103C8T6,主频是72MHz,所以这里选择72分频,定时器得到的频率就是1MHz,即每1us记一次数。
添加代码:
//记得添加tim.h头文件
#define US_TIM htim1
void MY_Delay_us(uint32_t us)
{
__HAL_TIM_SetCounter(&US_TIM,0); //先清零
HAL_TIM_Base_Start(&US_TIM); //开启定时器
while(__HAL_TIM_GetCounter(&US_TIM) < us);
HAL_TIM_Base_Stop(&US_TIM);
}
__HAL_TIM_GetCounter(&US_TIM)函数是获取当前计数值。
附上整个代码
ds18b20.c
#include "ds18b20.h"
#include "mydelay.h"
//复位DS18B20
void DS18B20_Rst(void)
{
DS18B20_IO_OUT(); //设置为输出
DS18B20_DQ_OUT=0; //拉低DQ
MY_Delay_us(750); //拉低750us
DS18B20_DQ_OUT=1; //DQ=1
MY_Delay_us(15); //15US
}
//等待DS18B20的回应
//DS18B20复位后会拉低总线60—240us来表示存在
//返回1:未检测到DS18B20的存在
//返回0:存在
uint8_t DS18B20_Check(void)
{
uint8_t retry=0;
DS18B20_IO_IN(); //设置为输入
while (DS18B20_DQ_IN&&retry<200)
{
retry++;
MY_Delay_us(1);
}
if(retry>=200)return 1;
else retry=0;
while (!DS18B20_DQ_IN&&retry<240)
{
retry++;
MY_Delay_us(1);
}
if(retry>=240)return 1;
return 0;
}
/*初始化DS18B20
等待DS18B20的回应
DS18B20复位后会拉低总线60—240us来表示存在
返回1:未检测到DS18B20的存在
返回0:存在*/
void DS18B20_Init(void)
{
DS18B20_Rst();
DS18B20_Check();
}
//从DS18B20读取一个位
//返回值:1/0
uint8_t DS18B20_Read_Bit(void)
{
uint8_t data;
DS18B20_IO_OUT(); //设置为输出
DS18B20_DQ_OUT=0;
MY_Delay_us(2); //两个读时隙之间不少于1us
DS18B20_DQ_OUT=1;
DS18B20_IO_IN(); //设置为输入
MY_Delay_us(12); //以上这个过程必须在15us内完成。然后判断数据是0还是1
if(DS18B20_DQ_IN)data=1;
else data=0;
MY_Delay_us(50); //所有读时隙必须持续至少60us
return data;
}
//从DS18B20读取一个字节
//返回值:读到的数据
uint8_t DS18B20_Read_Byte(void)
{
uint8_t i,j,dat;
dat=0;
for (i=1;i<=8;i++)
{
j=DS18B20_Read_Bit();
dat=(j<<7)|(dat>>1);
}
return dat;
}
//写一个字节到DS18B20
//dat:要写入的字节
void DS18B20_Write_Byte(uint8_t dat)
{
uint8_t j;
uint8_t testb;
DS18B20_IO_OUT(); //设置为输出
for (j=1;j<=8;j++)
{
testb=dat&0x01;
dat=dat>>1;
if(testb) // 写1
{
DS18B20_DQ_OUT=0;
MY_Delay_us(2); //两个写时隙之间至少有 1μs 的恢复时间
DS18B20_DQ_OUT=1;
MY_Delay_us(60); //写时隙必须持续最少 60μs
}
else //写0
{
DS18B20_DQ_OUT=0;
MY_Delay_us(60); //写时隙必须持续最少 60μs
DS18B20_DQ_OUT=1;
MY_Delay_us(2); //两个写时隙之间至少有 1μs 的恢复时间
}
}
}
//开始温度转换
void DS18B20_Start(void)
{
DS18B20_Init();
DS18B20_Write_Byte(0xcc);// skip rom
DS18B20_Write_Byte(0x44);// convert
}
//从ds18b20得到温度值
//精度:0.1C
//返回值:温度值 (-550~1250)
short DS18B20_Get_Temp(void)
{
uint8_t temp;
uint8_t TL,TH;
short tem; //两个字节
DS18B20_Start (); //开始转换
DS18B20_Init();
DS18B20_Write_Byte(0xcc); // skip rom
DS18B20_Write_Byte(0xbe); // convert
TL=DS18B20_Read_Byte(); // LSB
TH=DS18B20_Read_Byte(); // MSB
if(TH>7) //高5位为1温度值为负数
{
TH=~TH;
TL=~TL;
temp=0;//温度为负
}
else //高5位为0温度值为正数
temp=1;//温度为正
tem=TH; //获得高八位
tem<<=8;
tem+=TL;//获得底八位
tem=((double)tem*0.0625)*100; //将采集到的数据*0.0625转换成实际温度
//*100保留两位为小数
if(temp)
return tem; //返回温度值
else
return -tem;
}
ds18b20.h
#ifndef __DS18B20_H
#define __DS18B20_H
#include "main.h"
//IO方向设置
#define DS18B20_IO_IN() {DS18B20_GPIO_Port->CRL&=0XFFFFF0FF;DS18B20_GPIO_Port->CRL|=8<<8;}
#define DS18B20_IO_OUT() {DS18B20_GPIO_Port->CRL&=0XFFFFF0FF;DS18B20_GPIO_Port->CRL|=3<<8;}
//IO操作函数
#define DS18B20_DQ_OUT PBout(2) //数据端口 PB2
#define DS18B20_DQ_IN PBin(2) //数据端口 PB2
void DS18B20_Rst(void);
uint8_t DS18B20_Check(void); //检测是否存在DS18B20
void DS18B20_Init(void); //初始化DS18B20
short DS18B20_Get_Temp(void); //获取温度
void DS18B20_Start(void); //开始温度转换
void DS18B20_Write_Byte(uint8_t dat);//写入一个字节
uint8_t DS18B20_Read_Byte(void); //读出一个字节
uint8_t DS18B20_Read_Bit(void); //读出一个位
记得在main.h里添加寄存器配置代码
/* USER CODE BEGIN Private defines */
//位带操作,实现51类似的GPIO控制功能
//具体实现思想,参考<<CM3权威指南>>第五章(87页~92页).
//IO口操作宏定义
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))
#define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))
//IO口地址映射
#define GPIOA_ODR_Addr (GPIOA_BASE+12) //0x4001080C
#define GPIOB_ODR_Addr (GPIOB_BASE+12) //0x40010C0C
#define GPIOC_ODR_Addr (GPIOC_BASE+12) //0x4001100C
#define GPIOA_IDR_Addr (GPIOA_BASE+8) //0x40010808
#define GPIOB_IDR_Addr (GPIOB_BASE+8) //0x40010C08
#define GPIOC_IDR_Addr (GPIOC_BASE+8) //0x40011008
//IO口操作,只对单一的IO口!
//确保n的值小于16!
#define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //输出
#define PAin(n) BIT_ADDR(GPIOA_IDR_Addr,n) //输入
#define PBout(n) BIT_ADDR(GPIOB_ODR_Addr,n) //输出
#define PBin(n) BIT_ADDR(GPIOB_IDR_Addr,n) //输入
#define PCout(n) BIT_ADDR(GPIOC_ODR_Addr,n) //输出
#define PCin(n) BIT_ADDR(GPIOC_IDR_Addr,n) //输入
/* USER CODE END Private defines */