一、DS18B20简介
DS18B20是一一款常用的数字温度传感器,使用单个引脚完成主从机的双向通信是这款芯片的特点。
物理连接简单,那自然有特殊的时序需要满足,这也是使用这款芯片时麻烦的一点。
由于DS18B20不支持我们熟悉的IIC和SPI协议,那也就没有现成的库函数来支持我们的读写操作,也就是需要我们自行编写驱动。
但本文并不打算从解读数据手册中的时序图开始说太多废话,只给把代码给出,介绍一下驱动编写时可能遇到的一些问题,结合代码给出解决方案。
根据数据手册,这款芯片通常有三种封装:
三者都是通过DQ这个引脚来进行与主机的通信。所以问题简化为:
控制与DQ连接的引脚的电平变化
实际上,ds18b20
支持六个操作指令:
只需要按照一定电平协议把这些命令发出去,就能够从DQ
读取到想要的数据。而这些指令都刚好只有8位(bit
),也就是一个字节。于是乎,ds18b20
驱动的编写进一步简化为四个小任务:
1、初始化配置
2、通过DQ读取一个字节
3、通过DQ发送一个字节
4、读取温度
二、驱动代码介绍
1、初始化配置
初始化包含两步:
1、`DQ`对应的引脚的GPIO配置
2、通过`DQ`对`ds18b20`芯片的初始化
对应驱动代码的这部分:
/**
* @brief 配置DQ并复位DS18B20
* @param 无
* @retval 无
*/
void DS18B20_Init(void)
{
/* 初始化GPIO */
DS18B20_DQGPIOConfig();
/* 复位 */
DS18B20_Rst();
}
DS18B20_DQGPIOConfig()
使能APB2总线时钟和GPIO时钟,配置DQ
对应的引脚为推挽输出模式即可:
/**
* @brief 配置DQ对应的GPIO引脚
* @param 无
* @retval 无
*/
void DS18B20_DQGPIOConfig(void)
{
/*定义一个GPIO_InitTypeDef类型的结构体*/
GPIO_InitTypeDef GPIO_InitStructure;
/*开启LED相关的GPIO外设时钟*/
RCC_APB2PeriphClockCmd(DQ_GPIO_CLK, ENABLE);
/*选择要控制的GPIO引脚*/
GPIO_InitStructure.GPIO_Pin = PINDQ;
/*设置引脚模式为通用推挽输出*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
/*设置引脚速率为50MHz */
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
/*调用库函数,初始化GPIO*/
GPIO_Init(GPIODQ, &GPIO_InitStructure);
}
上面的代码中包含一些宏定义:
/* DQ引脚相关宏定义 */
#define GPIODQ GPIOB
#define PINDQ GPIO_Pin_8
/* GPIO端口时钟 */
#define DQ_GPIO_CLK RCC_APB2Periph_GPIOB
注:为了不至于看花眼,不会一次性将所有的宏和函数实现贴出来,说到哪儿就贴到那儿。同时也不用担心不好拼凑成完整的驱动程序,因为文末会放出驱动源代码链接。
重点看DS18B20_Rst()
的实现:
/**
* @brief 复位DS18B20
*/
void DS18B20_Rst(void)
{
DS18B20_SetDQMode_OUT();
/* 先将数据线置高电平“1” */
DQ_H;
/* 延时(该时间要求的不是很严格,但是尽可能的短一点) */
Delay_us(1);
/* 数据线拉到低电平“0”。 */
DQ_L;
/* 延时750微秒(该时间的时间范围可以从480到960微秒) */
Delay_us(750);
/* 数据线拉到高电平“1” */
DQ_H;
/* 延时等待(如果初始化成功则在15到60微秒时间之内产生一个由
DS18B20所返回的低电平“0”。据该状态可以来确定它的存在,
但是应注意不能无限的进行等待,不然会使程序进入死循环,
所以要进行超时控制)。 */
/* 超时时间不要设置得太短(500us是足够的),
否则初始化失败,将获取不到环境温度 */
Delay_us(500);
uint8_t dqStatus = GPIO_ReadInputDataBit(GPIODQ,PINDQ);
/* 将数据线再次拉高到高电平“1”后结束。 */
DQ_H;
}
注意看每一步的注释,交代了初始化过程中DQ
的电平变化。
其中最后一个延时函数:
Delay_us(500);
尤为关键,如果这里的等待时间不足,将导致初始化失败,出现温度值不变的怪病。
DS18B20_SetDQMode_OUT()
是把DQ对应的引脚设置为推挽输出模式:
/**
* @brief 设置DQ对应的引脚位推挽输出模式
* @param 无
* @retval 无
*/
static void DS18B20_SetDQMode_OUT()
{
/*定义一个GPIO_InitTypeDef类型的结构体*/
GPIO_InitTypeDef GPIO_InitStructure;
/*选择要控制的GPIO引脚*/
GPIO_InitStructure.GPIO_Pin = PINDQ;
/*设置引脚模式为通用推挽输出*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
/*设置引脚速率为50MHz */
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
/*调用库函数,初始化GPIO*/
GPIO_Init(GPIODQ, &GPIO_InitStructure);
}
DQ_H()
和DQ_L()
分别是将DQ
对应的引脚设置为高电平和低电平:
#define DQ_H GPIO_SetBits(GPIODQ,PINDQ)
#define DQ_L GPIO_ResetBits(GPIODQ,PINDQ)
Delay_us()
的实现如下:
/**
* @brief 延时函数
* @param us_cnt 设定的延时微秒数
* @retval 无
*/
void Delay_us(uint32_t us_cnt)
{
TIM3->CNT = us_cnt-1;
TIM3->CR1 |= TIM_CR1_CEN;
while((TIM3->SR & TIM_FLAG_Update)!=SET);
TIM3->SR = (uint16_t)~TIM_FLAG_Update;
TIM3->CR1 &= ~TIM_CR1_CEN;
}
这个函数需要配置定时器以支持计时:
/**
* @brief 配置延时定时器
* @param us_cnt 设定的延时微秒数
* @retval 无
*/
void Delay_Timer_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);
TIM_TimeBaseStructInit(&TIM_TimeBaseInitStruct);
TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Down;
TIM_TimeBaseInitStruct.TIM_Period = 100-1;
TIM_TimeBaseInitStruct.TIM_Prescaler = (84-1);
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct);
while((TIM3->SR & TIM_FLAG_Update)!=SET);
TIM3->SR = (uint16_t)~TIM_FLAG_Update;
}
2、通过DQ读取一个字节
/**
* @brief 通过DQ从DS18B20读取一个字节(Byte)
* @param 无
* @retval 读取到的数据(uint8_t类型)
*/
uint8_t DS18B20_ReadByte(void)
{
uint8_t data = 0x00;
for(int i=0;i<8;i++)
{
DS18B20_SetDQMode_OUT();
/* 将数据线拉高“1” */
DQ_H;
/* 延时2微秒 */
Delay_us(2);
/* 将数据线拉低“0” */
DQ_L;
/* 延时3微秒 */
Delay_us(3);
data>>=1;
/* 将数据线拉高“1” */
DQ_H;
DS18B20_SetDQMode_IPU();
/* 延时5微秒 */
Delay_us(5);
/* 读数据线的状态得到1个状态位,并进行数据处理 */
if(GPIO_ReadInputDataBit(GPIODQ,PINDQ))
data|=0x80;
/* 延时60微秒 */
Delay_us(60);
}
return data;
}
同样的,注意阅读注释中的描述。
DS18B20_SetDQMode_IPU()
把DQ对应的引脚设置为输入模式:
/**
* @brief 设置DQ对应的引脚为输入模式
* @param 无
* @retval 无
*/
static void DS18B20_SetDQMode_IPU()
{
/*定义一个GPIO_InitTypeDef类型的结构体*/
GPIO_InitTypeDef GPIO_InitStructure;
/*选择要控制的GPIO引脚*/
GPIO_InitStructure.GPIO_Pin = PINDQ;
/*设置引脚模式为通用推挽输出*/
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
/*调用库函数,初始化GPIO*/
GPIO_Init(GPIODQ, &GPIO_InitStructure);
}
3、通过DQ发送一个字节
/**
* @brief 通过DQ向DS18B20发送一个字节(Byte)
* @param
@arg data 待发送的1字节数据
* @retval 无
*/
void DS18B20_WriteByte(uint8_t data)
{
for(int i=0;i<8;i++){
/* 数据线先置低电平“0” */
DQ_L;
/* 延时确定的时间为15微秒 */
Delay_us(15);
/* 按从低位到高位的顺序发送字节(一次只发送一位) */
if(data&0x01){
GPIO_WriteBit(GPIODQ,PINDQ,Bit_SET);
}else{
GPIO_WriteBit(GPIODQ,PINDQ,Bit_RESET);
}
/* 延时时间为45微秒 */
Delay_us(45);
/* 将数据线拉到高电平 */
DQ_H;
data>>=1;
}
}
4、读取温度
/**
* @brief 获取温度寄存器的值并转换为温度值返回
* @param 无
* @retval 无
*/
float DS18B20_GetTemp(void)
{
DS18B20_Rst();
DS18B20_WriteByte(SKIPROMCOMMAND);
DS18B20_WriteByte(CONVERTT);
DS18B20_Rst();
DS18B20_WriteByte(SKIPROMCOMMAND);
DS18B20_WriteByte(READSCRATCHPAD);
uint8_t tempL = DS18B20_ReadByte();
uint8_t tempH = DS18B20_ReadByte();
/* 反码 */
if(tempH>0x7f)
{
tempL = ~tempL;
tempH = ~tempH+1;
}
/* 计算温度值 */
float temp = ((tempH<<4)|(tempL>>4))+
(float)(tempL&0x0f)*0.0625;
return temp;
}
其中的SKIPROMCOMMAND
、CONVERTT
、READSCRATCHPAD
都是 ds18b20
的控制指令:
温度计算的部分需要注意一下:
/* 计算温度值 */
float temp = ((tempH<<4)|(tempL>>4))+
(float)(tempL&0x0f)*0.0625;
数据手册的表述如下:
1、黑色-划掉的是多余的符号位
2、红色-对应着整数位
3、蓝色-下划线部分对应小数
首先,(高8位左移4位
)或上(低八位右移4位
),得到8位的整数部分:
((tempH<<4)|(tempL>>4))
然后,低8位去掉左边4位:
(tempL&0x0f)
乘2^(-4)化成小数:
(float)(tempL&0x0f)*0.0625
整数部分和小数部分加起来得到温度值:
float temp = ((tempH<<4)|(tempL>>4))+
(float)(tempL&0x0f)*0.0625;
好啦,ds18b20
的驱动就介绍完了,源代码的链接放在下面:
timer.x提供时序控制支持
ds18b20.x是驱动内容,供以读取温度数据