图29.3.1.1 DS18B20实验程序流程图
29.3.2 DS18B20函数解析
这一章节除了涉及到GPIO的API函数,便没有再涉及到其他API函数。因此,有关GPIO的API函数介绍,请读者回顾此前的第十章的内容。接下来,笔者将直接介绍DS18B20的驱动代码。
29.3.3 DS18B20驱动解析
在IDF版19_ds18b20例程中,作者在19_ds18b20\components\BSP路径下新增了一个DS18B20文件夹,分别用于存放ds18b20.c、ds18b20.h这两个文件。其中,ds18b20.h文件负责声明DS18B20相关的函数和变量,而ds18b20.c文件则实现了DS18B20的驱动代码。下面,我们将详细解析这两个文件的实现内容。
1,ds18b20.h文件
由于数据线会存在输入输出模式的切换以及高低电平的设置,所以为DS18B20_DQ_PIN做了相关宏函数供单总线时序函数调用。
#define DS18B20_DQ_GPIO_PIN GPIO_NUM_0
/* DS18B20引脚高低电平枚举 */
typedef enum
{
DS18B20_PIN_RESET = 0u,
DS18B20_PIN_SET
}DS18B20_GPIO_PinState;
/* IO操作 */
#define DS18B20_DQ_IN gpio_get_level(DS18B20_DQ_GPIO_PIN) /* 数据端口输入 */
/* DS18B20端口定义 */
#define DS18B20_DQ_OUT(x) do{ x ? \
gpio_set_level(DS18B20_DQ_GPIO_PIN,DS18B20_PIN_SET) : \
gpio_set_level(DS18B20_DQ_GPIO_PIN, DS18B20_PIN_RESET); \
}while(0)
gpio_get_level()与gpio_set_level()的介绍在GPIO章节已经介绍过了,在此不做赘述,请参照10.3.2小节的内容。
DS18B20_DQ_OUT(x)宏函数的作用是设置GPIO_NUM_0的电平状态,x为0表示低电平,x为1表示高电平。
DS18B20_DQ_IN宏函数的作用是获取GPIO_NUM_0的电平状态,返回值为0表示低电平,返回值为1表示高电平。
2,ds18b20.c文件
/**
* @retval 0, 正常
* 1, 不存在/不正常
*/
uint8_t ds18b20_init(void)
{
gpio_config_t gpio_init_struct;
gpio_init_struct.intr_type = GPIO_INTR_DISABLE; /* 失能引脚中断 */
gpio_init_struct.mode = GPIO_MODE_INPUT_OUTPUT_OD; /* 开漏模式的输入和输出 */
gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; /* 使能上拉 */
gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; /* 失能下拉 */
/* 设置的引脚的位掩码 */
gpio_init_struct.pin_bit_mask = 1ull << DS18B20_DQ_GPIO_PIN;
gpio_config(&gpio_init_struct); /* 配置DS18B20引脚 */
ds18b20_reset();
return ds18b20_check();
}
在DS18B20初始化函数中,通过调用复位函数和自检函数,可以知道DS18B20是否正常。
下面介绍一下在前面提及的几个信号类型。
/**
* @brief 复位DS18B20
* @param 无
* @retval 无
*/
static void ds18b20_reset(void) {
DS18B20_DQ_OUT(0); /* 拉低DQ,复位 */
esp_rom_delay_us(750); /* 拉低750us */
DS18B20_DQ_OUT(1); /* DQ=1, 释放复位 */
esp_rom_delay_us(15); /* 延迟15US */
}
/**
* @brief 等待DS18B20的回应
* @param 无
* @retval 0, DS18B20正常
* 1, DS18B20异常/不存在
*/
uint8_t ds18b20_check(void)
{
uint8_t retry = 0;
uint8_t rval = 0;
while (DS18B20_DQ_IN && retry < 200) /* 等待DQ变低, 等待200us */
{
retry++;
esp_rom_delay_us(1);
}
if (retry >= 240)
{
rval = 1;
}
else
{
retry = 0;
while (!DS18B20_DQ_IN && retry < 240) /* 等待DQ变高, 等待240us */
{
retry++;
esp_rom_delay_us(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;
for (j = 1; j <= 8; j++)
{
if (data & 0x01)
{
DS18B20_DQ_OUT(0);
esp_rom_delay_us(2);
DS18B20_DQ_OUT(1);
esp_rom_delay_us(60);
}
else
{
DS18B20_DQ_OUT(0);
esp_rom_delay_us(60);
DS18B20_DQ_OUT(1);
esp_rom_delay_us(2);
}
data >>= 1;
}
}
通过形参决定是写1还是写0,按照前面对写时序的分析,我们可以很清晰知道写函数的逻辑处理。
有写函数肯定就有读函数,下面看一下读函数:
/**
* @brief 从DS18B20读取一个位
* @param 无
* @retval 读取到的位值: 0 / 1
*/
static uint8_t ds18b20_read_bit(void)
{
uint8_t data = 0;
DS18B20_DQ_OUT(0);
esp_rom_delay_us(2);
DS18B20_DQ_OUT(1);
esp_rom_delay_us(12);
if (DS18B20_DQ_IN)
{
data = 1;
}
esp_rom_delay_us(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 从ds18b20得到温度值(精度:0.1C)
* @param 无
* @retval 温度值 (-550~1250)
* 实际使用的时候,要除以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值)被读出为止。如果不需要读出所有字节的内容,那么主机可以在任何时候发出复位信号以中止读操作。
29.3.4 CMakeLists.txt文件
打开本实验BSP下的CMakeLists.txt文件,其内容如下所示:
set(src_dirs
DS18B20
IIC
LCD
LED
SPI
XL9555)
set(include_dirs
DS18B20
IIC
LCD
LED
SPI
XL9555)
set(requires
driver)
idf_component_register(SRC_DIRS ${src_dirs}
INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)
上述的红色IIC、XLP555驱动需要由开发者自行添加,以确保IIC_EXIO驱动能够顺利集成到构建系统中。这一步骤是必不可少的,它确保了IIC_EXIO驱动的正确性和可用性,为后续的开发工作提供了坚实的基础。
29.3.5 实验应用代码
打开main/main.c文件,该文件定义了工程入口函数,名为app_main。该函数代码如下。
i2c_obj_t i2c0_master;
/**
* @brief 程序入口
* @param 无
* @retval 无
*/
void app_main(void)
{
uint8_t err;
uint8_t t = 0;
short temperature;
esp_err_t ret;
/* 初始化NVS */
ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
/* 初始化LED */
led_init();
/* 初始化IIC0 */
i2c0_master = iic_init(I2C_NUM_0);
/* 初始化SPI */
spi2_init();
/* 初始化XL9555 */
xl9555_init(i2c0_master);
/* 初始化LCD */
lcd_init();
lcd_show_string(30, 50, 200, 16, 16, "ESP32", RED);
lcd_show_string(30, 70, 200, 16, 16, "DS18B20 TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
/* 初始化DS18B20数字温度传感器 */
err = ds18b20_init();
if (err != 0)
{
while (1)
{
lcd_show_string(30, 110, 200, 16, 16, "DS18B20 Error", RED);
vTaskDelay(200);
lcd_fill(30, 110, 239, 130 + 16, WHITE);
vTaskDelay(200);
}
}
lcd_show_string(30, 110, 200, 16, 16, "DS18B20 OK", RED);
lcd_show_string(30, 130, 200, 16, 16, "Temp: . C", BLUE);
while (1)
{
/* 每100ms读取一次 */
if (t % 10 == 0)
{
temperature = ds18b20_get_temperature();
if (temperature < 0)
{
/* 显示负号 */
lcd_show_char(30 + 40, 130, '-', 16, 0, BLUE);
/* 转为正数 */
temperature = -temperature;
}
else
{
/* 去掉负号 */
lcd_show_char(30 + 40, 130, ' ', 16, 0, BLUE);
}
/* 显示正数部分 */
lcd_show_num(30 + 40 + 8, 130, temperature / 10, 2, 16, BLUE);
/* 显示小数部分 */
lcd_show_num(30 + 40 + 32, 130, temperature % 10, 1, 16, BLUE);
}
vTaskDelay(10);
t++;
if (t == 20)
{
t = 0;
/* LED闪烁 */
LED_TOGGLE();
}
}
}
在main函数中,完成一系列外设初始化之后,调用ds18b20_init函数完成DS18B20初始化,LCD显示实验信息。
在while循环中,间隔100毫秒调用ds18b20_get_temperature函数获取DS18B20传感器的温度数据,然后在LCD进行显示。LED灯每隔200毫秒状态翻转,实现闪烁效果。
29.4 下载验证
假定DS18B20传感器已经接上去正确的位置,将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了。LCD显示当前的温度值的内容如下图所示:
图29.4.1 DS18B20实验测试图
该程序还可以读取并显示负温度值,具备零下温度条件可以测试一下。