第二十三章 DS18B20实验
本章,我们将介绍ESP32-S3如何读取外部温度传感器的温度,来得到较为准确的环境温度。我们将学习单总线技术,通过它来实现ESP32-S3和外部温度传感器DS18B20的通信,并把从温度传感器得到的温度显示在LCD上。 本章分为如下几个小节: 23.1 DS18B20介绍 23.2 硬件设计 23.3 软件设计 23.4 下载验证
23.1 DS18B20介绍 23.1.1 DS18B20简介 DS18B20是由DALLAS半导体公司推出的一种“单总线”接口的温度传感器,实物图如下图所示。 图23.1.1.1 DS18B20实物图 与传统的热敏电阻等测温元件相比,它是一种新型的体积小、适用电压宽、与微处理器接口简单的数字化温度传感器。单总线结构具有简洁且经济的特点,可使用户轻松地组建传感器网络,从而为测量系统的构建引入全新的概念,测试温度范围为-55~+125℃,精度为±0.5℃。现场温度直接以单总线的数字方式传输,大大提高了系统的抗干扰性。它能直接读出被测温度,并且可根据实际要求通过简单的编程实现9~12位的数字值读数方式。它工作在3~5.5V的电压范围,采用多种封装形式,从而使系统设置灵活、方便,设定分辨率以及用户设定的报警温度存储在EEPROM中,掉电后依然保存。 其内部结构如下图所示。 图23.1.1.2 DS18B20内部结构图 ROM中的64位序列号是出厂前被标记好的,它可以看作使该DS18B20的地址序列码,每个DS18B20的64位序列号均不相同。64位ROM的排列是:前8位是产品家族码,接着48位是DS18B20的序列号,最后8位是前面56位的循环冗余校验码(CRC=X8+X5+X4+1)。ROM作用是使每一个DS18B20都各不相同,这样设计可以允许一根总线上挂载多个DS18B20模块同时工作且不会引起冲突。 23.1.2 DS18B20时序介绍 所有单总线器件要求采用严格的信号时序,以保证数据的完整性。DS18B20共有6种信号类型:复位脉冲、应答脉冲、写0、写1、读0和读1。所有这些信号,除了应答脉冲以外,都是由主机发出同步信号。并且发送所有的命令和数据都是字节的低位在前。这里我们简单介绍这几个信号的时序。 1,复位脉冲和应答脉冲 图23.1.2.1 复位脉冲和应答脉冲时序图 单总线上的所有通信都是以初始化序列开始。主机输出低电平,保持低电平时间至少要在480us,以产生复位脉冲。接着主机释放总线,4.7K的上拉电阻将单总线拉高,延时时间要在15~60us,并进入接收模式(Rx)。接着DS18B20拉低总线60~240us,以产生低电平应答脉冲。 2,写时序 图23.1.2.2 写时序图 写时序包括写0时序和写1时序。所有写时序至少需要60us,且在两次独立的写时序之间至少需要1us的恢复时间,两种写时序均起始于主机拉低总线。写1时序:主机输出低电平,延时2us,然后释放总线,延时60us。写0时序:主机输出低电平,延时60us,然后释放总线延时2us。 3,读时序 图23.1.2.3 读时序图 单总线器件仅在主机发出读时序时,才向主机传输数据,所以,在主机发出读数据命令后,必须马上产生读时序,以便从机能够传输数据。所有读时序至少需要60us,且在2次独立的读时序之间至少需要1us的恢复时间。每个读时序都由主机发起,至少拉低总线1us。主机在读时序期间必须释放总线,并且在时序起始后的15us之内采样总线状态。典型的读时序过程为:主机输出低电平延时2us,然后主机转入输入模式延时12us,然后读取单总线当前的电平,然后延时50us。 在了解单总线时序之后,我们来看一下DS18B20的典型温度读取过程,DS18B20的典型温度读取过程为:复位→发SKIP ROM(0xCC)→发开始转换命令(0x44)→延时→复位→发送SKIP ROM命令(0xCC)→发送存储器命令(0xBE)→连续读取两个字节数据(即温度)→结束。 DS18B20的简介,我们就介绍到这里,关于该传感器的详细说明,请大家参考其数据手册。 23.2 硬件设计 1. 例程功能 DS18B20每隔200ms左右读取一次数据,并把温度显示在LCD上。LED闪烁用于提示程序正在运行。 2. 硬件资源 1)LED灯 LED-IO1 2)USART0 U0TXD-IO43 U0RXD-IO44 3)XL9555 IIC_SDA-IO41 IIC_SCL-IO42 4)SPILCD CS-IO21 SCK-IO12 SDA-IO11 DC-IO40(在P5端口,使用跳线帽将IO_SET和LCD_DC相连) PWR- IO1_3(XL9555) RST- IO1_2(XL9555) 5)DS18B20 1WIRE_DQ-IO0(在P5端口,使用跳线帽将IO0和1WIRE_DQ相连) 3. 原理图 DS18B20相关原理图,如下图所示。 图23.2.1 DS18B20原理图 从上图可以看出,DS18B20的数据引脚DQ并没有直接与ESP32-S3的引脚进行相连,需要借助跳线帽将BOOT和1WIRE_DQ连接起来,这时候DQ引脚连接到的就是ESP32-S3的IO0引脚。 图中U4为DHT11(数字温湿度传感器)和DS18B20共用的一个接口,DHT11我们将在下一章介绍。 DS18B20只用到U4的3个引脚(U4的1、2和3脚),将DS18B20传感器插入到这个上面就可以通过ESP32-S3来读取DS18B20的温度了。连接示意图如图35.2.2所示: 图23.2.2 DS18B20连接示意图 从上图可以看出,DS18B20的平面部分(有字的那面)应该朝内,而曲面部分朝外。然后插入如图所示的三个孔内。 23.3 软件设计 23.3.1 程序流程图 下面看看本实验的程序流程图: 图23.3.1 程序流程图 23.3.2 程序解析 1. DS18B20驱动代码这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。DS18B20驱动源码包括两个文件:ds18b20.cpp和ds18b20.h。 本例程,我们直接使用ESP32底层接口函数去完成DS18B20驱动。使用到ESP32底层接口函数需要#include “esp_system.h”。 下面我们先解析ds18b20.h的程序。对DS18B20的数据引脚做了相关定义。 #define DS18B20_DQ_PIN GPIO_NUM_0 我们选择使用IO0作为DS18B20数据引脚。为了后续对DQ引脚便捷的操作,我们为DQ引脚函数做了下面的定义。 #define DS18B20_DQ_OUT(x) gpio_set_level(DS18B20_DQ_PIN, x) #define DS18B20_DQ_IN gpio_get_level(DS18B20_DQ_PIN) #define DS18B20_MODE_IN gpio_set_direction(DS18B20_DQ_PIN, GPIO_MODE_INPUT) #define DS18B20_MODE_OUT gpio_set_direction(DS18B20_DQ_PIN, GPIO_MODE_OUTPUT) gpio_set_level函数设置GPIO输出高低电平;gpio_get_level函数获取GPIO的高低电平;gpio_set_direction函数设置IO的输入输出模式。 DS18B20_DQ_OUT(x)宏函数设置DQ引脚高低电平,x为0,即输出低电平;x为1,即输出高电平。 DS18B20_DQ_IN宏函数获取DQ引脚的电平状态,返回值为0表示低电平,返回值为1表示高电平。 DS18B20_MODE_IN宏函数设置引脚的输入模式。 DS18B20_MODE_OUT宏函数设置引脚的输出模式。 下面我们再解析ds18b20.cpp的程序,首先先来看一下初始化函数ds18b20_init,代码如下: /** * @param 无 * @retval 0:正常,1:不存在/不正常 */ uint8_t ds18b20_init(void) { ds18b20_reset(); return ds18b20_check(); } 该函数主要调用ds18b20_reset函数进行软件复位,复位后,调用ds18b20_check函数进行检测器件是否正常。 下面介绍一下在前面提及的几个信号类型: /** * @brief 复位DS18B20 * @param 无 * @retval 无 */ static void ds18b20_reset(void) { DS18B20_MODE_OUT; DS18B20_DQ_OUT(0); /* 拉低DQ,复位 */ delayMicroseconds(750); /* 拉低750us */ DS18B20_DQ_OUT(1); /* DQ=1, 释放复位 */ delayMicroseconds(15); /* 延迟15US */ } /** * @brief 等待DS18B20的回应 * @param 无 * @retval 0, DS18B20正常 * 1, DS18B20异常/不存在 */ uint8_t ds18b20_check(void) { uint8_t retry = 0; uint8_t rval = 0; DS18B20_MODE_IN; while (DS18B20_DQ_IN && retry < 200) /* 等待DQ变低, 等待200us */ { retry++; delayMicroseconds(1); } if (retry >= 240) { rval = 1; } else { retry = 0; while (!DS18B20_DQ_IN && retry < 240) /* 等待DQ变高, 等待240us */ { retry++; delayMicroseconds(1); } if (retry >= 240) { rval = 1; } } return rval; } 以上两个函数分别代表着前面所说的复位脉冲与应答信号,大家可以对比前面的时序图进行理解。由于复位脉冲比较简单,所以这里不做展开。现在看一下应答信号函数,函数主要是对于DS18B20传感器的回应信号进行检测,对此判断其是否存在。函数的实现也是依据时序图进行逻辑判断,例如当主机发送了复位信号之后,按照时序,DS18B20会拉低数据线60~240us,同时主机接收最小时间为480us,我们就依据这两个硬性条件进行判断,首先需要设置一个时限等待DS18B20响应,后面也设置一个时限等待DS18B20释放数据线拉高,满足这两个条件即DS18B20成功响应。 下面介绍的是写函数,其定义如下: /** * @brief 写一个字节到DS18B20 * @param data: 要写入的字节 * @retval 无 */ static void ds18b20_write_byte(uint8_t data) { uint8_t j; DS18B20_MODE_OUT; for (j = 1; j <= 8; j++) { if (data & 0x01) { DS18B20_DQ_OUT(0); delayMicroseconds(2); DS18B20_DQ_OUT(1); delayMicroseconds(60); } else { DS18B20_DQ_OUT(0); delayMicroseconds(60); DS18B20_DQ_OUT(1); delayMicroseconds(2); } data >>= 1; } } 通过形参决定是写1还是写0,按照前面对写时序的分析,我们可以很清晰知道写函数的逻辑处理。 有写函数肯定就有读函数,下面看一下读函数: /** * @brief 从DS18B20读取一个位 * @param 无 * @retval 读取到的位值: 0 / 1 */ static uint8_t ds18b20_read_bit(void) { uint8_t data = 0; DS18B20_MODE_OUT; DS18B20_DQ_OUT(0); delayMicroseconds(2); DS18B20_DQ_OUT(1); delayMicroseconds(12); DS18B20_MODE_IN; if (DS18B20_DQ_IN) { data = 1; } delayMicroseconds(50); return data; } /** * @brief 从DS18B20读取一个字节 * @param 无 * @retval 读到的数据 */ static uint8_t ds18b20_read_byte(void) { uint8_t i, b, data = 0; for (i = 0; i < 8; i++) { b = ds18b20_read_bit(); /* DS18B20先输出低位数据 ,高位数据后输出 */ data |= b << i; /* 填充data的每一位 */ } return data; } 在这里ds18b20_read_bit函数从DS18B20处读取1位数据,在前面已经对读时序也进行了详细的分析,所以这里也不展开解释了。 下面介绍读取温度函数,其定义如下: /** * @brief 开始温度转换 * @param 无 * @retval 无 */ static void ds18b20_start(void) { ds18b20_reset(); ds18b20_check(); ds18b20_write_byte(0xcc); /* skip rom */ ds18b20_write_byte(0x44); /* convert */ } /** * @brief 从ds18b20得到温度值(精度:0.1C) * @param 无 * @retval 温度值 (-550~1250) * @note 返回的温度值放大了10倍. * 实际使用的时候,要除以10才是实际温度. */ short ds18b20_get_temperature(void) { uint8_t flag = 1; /* 默认温度为正数 */ uint8_t TL, TH; short temp; ds18b20_start(); /* ds1820 start convert */ ds18b20_reset(); ds18b20_check(); ds18b20_write_byte(0xcc); /* skip rom */ ds18b20_write_byte(0xbe); /* convert */ TL = ds18b20_read_byte(); /* LSB */ TH = ds18b20_read_byte(); /* MSB */ if (TH > 7) { /* 温度为负,查看DS18B20的温度表示法与计算机存储正负数据的原理一致: 正数补码为寄存器存储的数据自身,负数补码为寄存器存储值按位取反后+1 所以我们直接取它实际的负数部分,但负数的补码为取反后加一,但考虑到低位 可能+1后有进位和代码冗余,我们这里先暂时没有作+1的处理,这里需要留意 */ TH = ~TH; TL = ~TL; flag = 0; } temp = TH; /* 获得高八位 */ temp <<= 8; temp += TL; /* 获得底八位 */ /* 转换成实际温度 */ if (flag == 0) { /* 将温度转换成负温度,这里的+1参考前面的说明 */ temp = (double)(temp + 1) * 0.625; temp = -temp; } else { temp = (double)temp * 0.625; } return temp; } 在这里简单介绍一下上面用到的RAM指令: 跳过ROM(0xCC),该指令只适合总线只有一个节点,它通过允许总线上的主机不提供64位ROM序列号而直接访问RAM,节省了操作时间。 温度转换(0x44),启动DS18B20进行温度转换,结果存入内部RAM。 读暂存器(0xBE),读暂存器9个字节内容,该指令从RAM的第一个字节(字节0)开始读取,直到九个字节(字节8,CRC值)被读出为止。如果不需要读出所有字节的内容,那么主机可以在任何时候发出复位信号以中止读操作。 2. 17_ds18b20.ino代码在17_ds18b20.ino里面编写如下代码: #include "led.h" #include "uart.h" #include "xl9555.h" #include "spilcd.h" #include "ds18b20.h" short temperature; /* 温度值 */ uint8_t t = 0; /** * @brief 当程序开始执行时,将调用setup()函数,通常用来初始化变量、函数等 * @param 无 * @retval 无 */ void setup() { led_init(); /* LED初始化 */ uart_init(0, 115200); /* 串口0初始化 */ xl9555_init(); /* IO扩展芯片初始化 */ lcd_init(); /* LCD初始化 */ ds18b20_init(); /* DS18B20初始化 */ lcd_show_string(30, 50, 200, 16, LCD_FONT_16, "ESP32-S3", RED); lcd_show_string(30, 70, 200, 16, LCD_FONT_16, "DS18B20 TEST", RED); lcd_show_string(30, 90, 200, 16, LCD_FONT_16, "ATOM@ALIENTEK", RED); lcd_show_string(30, 110, 200, 16, LCD_FONT_16, "Temp: . C", BLUE); } /** * @brief 循环函数,通常放程序的主体或者需要不断刷新的语句 * @param 无 * @retval 无 */ void loop() { if (t % 10 == 0) { temperature = ds18b20_get_temperature(); if (temperature < 0) { lcd_show_char(30 + 40, 110, '-', LCD_FONT_16, 0, BLUE); temperature = -temperature; } else { lcd_show_char(30 + 40, 110, ' ', LCD_FONT_16, 0, BLUE); } lcd_show_num(30 + 40 + 8, 110, temperature / 10, 2, LCD_FONT_16, BLUE); lcd_show_num(30 + 40 + 32, 110, temperature % 10, 1, LCD_FONT_16, BLUE); } delay(10); t++; if (t == 20) { t = 0; LED_TOGGLE(); } } 在setup函数中,调用led_init函数完成LED初始化,uart_init函数完成串口初始化,调用xl9555_init函数完成XL9555初始化,调用lcd_init函数完成LCD屏初始化,调用ds18b20_init函数完成DS18B20初始化,然后LCD显示实验信息。 在loop函数中,间隔100毫秒调用ds18b20_get_temperature函数获取温度传感器数据,然后在LCD显示数据。LED灯每隔200毫秒进行闪烁。 23.4 下载验证 假定DS18B20传感器已经接上去正确的位置,将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了。LCD显示当前的温度值的内容如下图所示。 图23.4.1 DS18B20实验测试图 该程序还可以读取并显示负温度值,具备零下温度条件可以测试一下。
|