版权声明

本文档所有内容文字资料由凌云实验室郭工编著,主要用于凌云嵌入式Linux教学内部使用,版权归属作者个人所有。任何媒体、网站、或个人未经本人协议授权不得转载、链接、转帖或以其他方式复制发布/发表。已经授权的媒体、网站,在下载使用时必须注明来源,违者本人将依法追究责任。

  • Copyright (C) 2022 凌云物网智科实验室·郭工

  • Author: Guo Wenxue Email: guowenxue@gmail.com QQ: 281143292

wechat_pub

2.2 文件IO之温度传感器采样

2.2.1 DS18b20传感器简介

DS18B20 是由 Dallas 半导体公司推出的一种的一线总线(1-Wire)接口的数字温度传感器,它工作在 3v~5.5V的电压范围,其测量温度范围为-55~+125℃ ,精度为±0.5℃。与传统的热敏电阻等测温元件相比,它是一种新型的体积小、适用电压宽、接口简单的数字化温度传感器,并采用多种封装形式,从而使系统设计灵活、方便。

ds18b20

DS18B20根据不同的应用场合而改变其外观,封装后的DS18B20可用于电缆沟测温,高炉水循环测温,锅炉测温,机房测温,农业大棚测温,洁净室测温,弹药库测温等各种非极限温度场合。耐磨耐碰,体积小,使用方便,封装形式多样,适用于各种狭小空间设备数字测温和控制领域。

ds18b20_package

DS18B20温度传感器主要具有以下功能特性:

  • 它的工作电压范围为3.0v~5.0v,另外也可以直接由数据线供电而不需要外部电源供电;

  • 采用一线协议(1-Wire),即仅使用一根数据线(以及地线)与微控制器(MCU)进行通信;

  • 它可以提供9-Bit到12-Bit的测量精度和一个用户可编程的非易失性且具有过温和低温触发报警的报警功能;

  • 该传感器的温度检测范围为-55℃至+125℃,并且在温度范围超过-10℃至85℃之外时还具有+-0.5℃的精度;

  • DS18B20温度转换时间在转换精度为12-Bits时达到最大值750ms;

每个DS18B20芯片在出厂时,都固化烧录了一个唯一的64位产品序列号在其ROM中,它可以看作是该 DS18B20 的地址序列码。 64 位 ROM 的排列是:前 8 位是产品家族码,接着 48 位是DS18B20 的序列号,最后 8 位是前面 56 位的循环冗余校验码(CRC=X8+X5+X4+1)。 ROM 作用是使每一个 DS18B20 都各不相同,这样就可实现一根总线上挂接多个 DS18B20。

接下来,我们将深入学习并了解DS18B20温度传感器的工作原理,需要注意的是在学习的过程中,一定要对着芯片的datasheet来理解点此链接可以下载或在线查阅。

2.2.2 DS18B20工作原理

2.2.2.1 1-Wire协议

一线总线结构具有简洁且经济的特点,可使用户轻松地组建传感器网络,从而为测量系统的构建引入全新概念。现场温度直接以“一线总线”的数字方式传输,大大提高了系统的抗干扰性。它能直接读出被测温度,并且可根据实际要求通过简单的编程实现 9~12 位的数字值读数方式。

所有的单总线器件要求采用严格的信号时序,以保证数据的完整性。 DS18B20 共有 6 种信号类型:复位脉冲、应答脉冲、写 0/ 1、读 0/1。所有这些信号,除了应答脉冲以外,都由主机发出同步信号,并且发送所有的命令和数据都是字节的低位在前(LSB)。

1)复位脉冲和应答脉冲

单总线上的所有通信都是以初始化序列开始。主机(CPU)输出低电平,并保持低电平时间至少 480us,以产生复位脉冲。接着主机释放总线, 4.7K 的上拉电阻将单总线拉高,延时 15~60 us,并进入接收模式(Rx)。DS18B20芯片在收到主机发送过来的这个复位脉冲后,将会拉低总线 60~240 us,以产生低电平应答脉冲,然后释放总线并维持至少480us。CPU在这段期间如果读到低电平,则说明探测到DS18B20芯片,否则DS18B20芯片损坏或芯片并没有连接。

w1_reset

2)写时序

写时序包括写 0 时序和写 1 时序。所有写时序至少需要 60us,且在 2 次独立的写时序之间至少需要 1us 的恢复时间,两种写时序均起始于主机拉低总线:

  • 写 0 时序:主机输出低电平,延时 60us,然后释放总线,延时 2us;

  • 写 1 时序:主机输出低电平,延时 2us,然后释放总线,延时 60us;

w1_write

3)读时序

单总线器件(DS18B20)仅在主机发出读时序时,才向主机传输数据,所以,在主机发出读数据命令后,必须马上产生读时序,以便从机能够传输数据。所有读时序至少需要 60us,且在 2 次独立的读时序之间至少需要 1us 的恢复时间。

当总线控制器把数据线从高电平拉到低电平时,读时序开始,数据线必须至少保持1us,然后总线被释放。DS18B20 通过拉高或拉低总线上来传输”1”或”0”。当传输逻辑”0”结束后,总线将被释放,通过上拉电阻回到上升沿状态,从DS18B20输出的数据在读时序的下降沿出现后15us 内有效。因此,总线控制器在读时序开始后必须停止把I/O口驱动为低电15us,以读取I/O口状态。

w1_read

2.2.2.2 DS18B20工作流程

DS18B20传感器的工作流程为:

  1. 总线初始化;

  2. ROM操作命令;

  3. 存储器操作命令;

  4. 处理数据;

1)总线初始化

单总线上的所有通信都是以初始化序列开始,Master发出初始化信号后等待从设备的应答信号,已确定从设备是否存在并能正常工作。

2)ROM操作命令

总线主机检测到DS18B20的存在后,便可以发出 ROM 操作命令之一,这些命令如下表所示。一般我们不关心ROM中的16位产品序列号,通常会发送0xCC跳过ROM的相关操作。

指令说明

十六进制代码

Read ROM(读 ROM)

[33H]

Match ROM(匹配 ROM)

[55H]

Skip ROM(跳过 ROM]

[CCH]

Search ROM(搜索 ROM)

[F0H]

Alarm search(告警搜索)

[ECH]

3)存储器操作命令

ROM命令操作完成之后,接下来可以发送相应的高速暂存存储器操作命令,这些命令如下表所示。其中0x44命令将通知DS18B20温度传感器开始采样, 而0xBE命令则将开始读出DS18B20的采样值。

指令说明

十六进制代码

Write Scratchpad(写暂存存储器)

[4EH]

Read Scratchpad(读暂存存储器)

[BEH]

Copy Scratchpad(复制暂存存储器)

[48H]

Convert Temperature(温度变换)

[44H]

Recall EPROM(重新调出)

[B8H]

Read Power supply(读电源)

[B4H]

3)数据处理

DS18B20的高速暂存存储器由9个字节组成。当温度转换命令(0x44)发布后,经转换所得的温度值以二字节补码形式存放在高速暂存存储器前两个字节。接着单片机可以发送读暂存存储器命令(0xBH)读出存储器里的值, 存储器里的9个字节的存储结构如下图所示:

memory_map

  • 字节0~1 是温度存储器,用来存储转换好的温度。第0个字节存储温度低8位,第一个字节存储温度高8位;

  • 字节2~3 是用户用来设置最高报警和最低报警值(TH和TL)。

  • 字节4 是配置寄存器,用来配置转换精度,可以设置为9~12 位。

  • 字节5~7 保留位。芯片内部使用

  • 字节8 CRC校验位。是64位ROM中的前56位编码的校验码,由CRC发生器产生。

如果我们只关心采样温度值的话,则只需要读前两个字节即可。其中Byte[0]为温度值的低字节,而Byte[1]为温度值的高字节。这16位数据的格式如下图所示:

reg_format

  • BIT[3:0]为温度值的小数部分;

  • BIT[10:4]为温度值的整数部分;

  • BIT[15:11]则为符号位,如果为0则温度为正值,如果为1则温度为负值。

2.2.3 DS18B20温度采样编程

2.2.3.1 模块硬件连接说明

DS18B20传感器的工作电压范围为3~5.5v,所以其电源连接3.3v和5v都可以(建议连接3.3V)。这样DS18B20在与开发板相连时,主要连接如下三个引脚:

  1. GND,该引脚要连到开发板的GND扩展引脚上;

  2. VDD, 该引脚要连到开发板的 3.3v 或 5v 供电引脚上;

  3. DQ, 是DS18B20的数据通信引脚,该引脚应该连开发板上具有 1-Wire 协议功能的GPIO引脚上;

在IGKBoard开发板上,提供了与树莓派兼容的 40Pin扩展引脚,其定义如下。其中物理引脚 #7 (GPIO1_IO18) 在Linux系统启动时如果启用了 w1 overlay 后,它将会默认作为 DS18B20 传感器的一线协议接口使用。这样,DS18B20的 DQ 引脚应该连接它上。

40pin_hat

如下是 DS18B20 温度传感器连接到 IGKBoard上的实物示意图。

igkboard_ds18b20

实物连接图如下。

Ds18b20_IGKBoard_Read_connect

2.2.3.2 驱动配置使用说明

在前面,我们将DS18B20的DQ引脚连到了IGKBoard开发板扩展接口的 #7 引脚上(GPIO01_IO18),该引脚在系统启动时有可能 默认作为GPIO功能使用。如果想作为DS18B20的通信引脚使用的话,我们需要修改开发板上的DTOverlay配置文件,添加该引脚的 1-Wire 协议支持。

具体方法为修改 eMMC/TF卡 启动介质的 boot 分区下的 config.txt 文件,修改 dtoverlay_w1 选项中添加 w1 支持即可。

root@igkboard:~# vim /run/media/mmcblk1p1/config.txt 

# Eanble 1-Wire overlay
dtoverlay_w1=yes

如果系统默认没有挂载这个分区,则需要先使用 mount 命令来手动挂载。

root@igkboard:~# mount | grep mmcblk
/dev/mmcblk1p2 on / type ext4 (rw,relatime)

root@igkboard:~# mkdir -p /run/media/mmcblk1p1/
root@igkboard:~# mount /dev/mmcblk1p1 /run/media/mmcblk1p1/

修改完成后重启系统,系统启动时将会自动加载 1-wire 协议驱动。待系统启动完成后,我们可以使用 dmesg 命令确认驱动是否成功加载。

root@igkboard:~# dmesg | grep 1-wire
[    1.897063] Driver for 1-wire Dallas network protocol.

接下来,我们应该可以在 /sys/bus/w1/devices/ 路径下,看到 DS18B20 温度传感器的相关文件 28-xxxxxx,事实上它是一个链接到其它文件夹的符号连接,我们暂时不关心。如下命令执行的结果所示,其中 0417012373ff 为现在所使用的DS18B20芯片的产品序列号。需要注意的是,板子上连接不同的DS18B20芯片这个值就不同。

root@igkboard:~# ls /sys/bus/w1/devices/
28-0417012373ff  w1_bus_master1

我们使用 ls 命令看看,这个文件夹下有哪些文件。在这里,最重要的一个文件是 w1_slave 文件。当我们在Linux下开始读该文件时,它将触发Linux内核中的 DS18B20 驱动开始采样。在采样完成之后,Linux内核驱动将会把采样温度值写入到该文件中,这样我们读取该文件里的内容即可。

root@igkboard:~# ls /sys/bus/w1/devices/28-0417012373ff
alarms     eeprom_cmd  hwmon  power       temperature
conv_time  ext_power   id     resolution  uevent
driver     features    name   subsystem   w1_slave

现在,我们可以使用 cat 命令查看 w1_slave 文件里的内容,注意这里只能使用 cat 命令而不能使用 vim 命令查看,因为该文件是只读的。另外在执行这个命令的时候,我们会发现执行 cat 命令之后,结果需要延时一会才会显示出来。这是因为,Linux内核在触发传感器采样,并等待采样完成、写入采样温度值到该文件中还需要一段时间。

root@igkboard:~# cat /sys/bus/w1/devices/28-0417012373ff/w1_slave 
93 01 4b 46 7f ff 0c 10 f6 : crc=f6 YES
93 01 4b 46 7f ff 0c 10 f6 t=25187

在上面的 输出结果中:

crc=f6 : 它是DS18B20采样时传输数据的CRC校验和,主要用来校验传感器将采样数据发送给CPU时是否出错;

t=25187 : 后面的 25187就是采样温度值,其中25是整数部分,187为小数部分,即本次采样温度为 25.187℃。

2.2.2.3 测试程序应用编程

在前面我们知道,DS18B20采样后的温度值存放在 **/sys/bus/w1/devices/28-0621c148b27d/w1_slave **文件中,这里的 28-0621c148b27d 是DS18B20的产品序列号,不同的DS18B20芯片其序列号不同。所以在编写代码时,我们不能直接读这个路径的文件来获取温度,因为传感器或设备换了之后,我们的代码就不能工作了。

这样,我们在编程时就应该能够根据不同的芯片来动态获取这个文件路径。因为DS18B20的温度传感器文件总是在 /sys/bus/w1/devices/ 路径下,并且传感器文件夹名是以 “28-” 开头,所以我们可以通过打开文件夹**/sys/bus/w1/devices/**,并在里面找到以 “28-” 开头的文件。然后再使用相应的字符串处理函数把它们组合成一个完整的路径: **/sys/bus/w1/devices/28-xxxxxxxxxxxx/w1_slave **。

接下来,我们编写 DS18B20传感器采样获取温度的代码如下。

guowenxue@ubuntu20:~/igkboard/apps$ vim ds18b20.c

    /*********************************************************************************
 *      Copyright:  (C) 2023 LingYun IoT System Studio
 *                  All rights reserved.
 *
 *       Filename:  ds18b20.c
 *    Description:  This file is temperature sensor DS18B20 source code
 *
 *        Version:  1.0.0(2023/8/10)
 *         Author:  Guo Wenxue <guowenxue@gmail.com>
 *      ChangeLog:  1, Release initial version on "2023/8/10 12:13:26"
 *
 * Pin connection:
 *
 *               DS18B20 Module          IGKBoard
 *                   VCC      <----->      3.3V
 *                   DQ       <----->      #Pin7(GPIO1_IO18)
 *                   GND      <----->      GND
 *
 * /run/media/mmcblk1p1/config.txt:
 *
 *          # Eanble 1-Wire overlay
 *          dtoverlay_w1=yes
 *
 ********************************************************************************/

/* 在C语言编程时,一般系统的头文件用<xxx.h>,我们自己写的头文件则用"zzz.h" */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <string.h>
#include <time.h>
#include <errno.h>

/* 在C语言编程中,函数应该先定义再使用,如果函数的定义在函数调用后面,应该前向声明。*/
int ds18b20_get_temperature(float *temp);

int main(int argc, char *argv[])
{
    float       temp; /* 温度值有小数位,所以使用浮点数 */

    /* 1,在Linux下做C语言编程时,函数返回值一般是0表示成功,<0表示失败,我们也遵循这个规约;
     * 2,但函数调用只能有一个返回值,所以这里的采样函数只能通过指针来返回采样的温度值;
     * 3,因为要在ds18b20_get_temperature()函数中修改main()中temp的值,所以这里传&temp;
     */
    if( ds18b20_get_temperature(&temp) < 0 )
    {
        printf("ERROR: ds18b20 get temprature failure\n");
        return 1;
    }

    /* 打印DS18B20的采样温度值,因为℃是非ASCII打印字符,所以这里用 'C 代替 */
    printf("DS18B20 get temperature: %f 'C\n", temp);

    return 0;
}

/*
 * 函数说明: 该函数用来使用 DS18B20 温度传感器采样获取当前的温度值;
 * 参数说明: $temp: 通过指针返回DS18B20的采样温度
 * 返回说明: ==0 表示成功, <0 表示失败
 */
int ds18b20_get_temperature(float *temp)
{
    const char     *w1_path = "/sys/bus/w1/devices/";
    char            ds_path[50]; /* DS18B20 采样文件路径 */
    char            chip[20];    /* DS18B20 芯片序列号文件名 */
    char            buf[128];    /* read() 读数据存储 buffer */
    DIR            *dirp;        /* opendir()打开的文件夹句柄 */
    struct dirent  *direntp;     /* readdir()读文件夹内容时的目录项*/
    int             fd =-1;      /* open()打开文件的文件描述符 */
    char           *ptr;         /* 一个字符指针,用来字符串处理 */
    int             found = 0;   /* 是否找到DS18B20的标志,默认设置为没找到(0) */
    int             rv = 0;      /* 函数返回值,默认设置为成功返回(0) */


    /* 在C语言编程时,进入函数的第一件事应该进行函数参数的合法性检测,检查参数非法输入。
     * 否则调用者"不小心"通过 $temp 传入一个空指针,下面的代码就有可能出现段错误。
     */
    if( !temp )
    {
        return -1;
    }

    /* 打开 "/sys/bus/w1/devices/" 文件夹,如果打开失败则打印错误信息并退出。 */
    if((dirp = opendir(w1_path)) == NULL)
    {
        printf("opendir error: %s\n", strerror(errno));
        return -2;
    }

    /* 1, 因为文件夹下可能有很多文件,所以这里使用while()循环读取/sys/bus/w1/devices/
     * 文件夹下的所有目录项,其中 direntp->d_name 就是目录里的每个文件/文件夹的文件名。
     *
     * 2, 接下来我们使用 strstr() 函数判断文件名中是否包含 "28-",如果找到则将完整的
     * 文件名通过strcpy()函数保存到 chip 中;并设置 found 标志为1,跳出循环。
     */
    while((direntp = readdir(dirp)) != NULL)
    {
        if(strstr(direntp->d_name,"28-"))
        {
            /* find and get the chipset SN filename */
            strcpy(chip,direntp->d_name);
            found = 1;
            break;
        }
    }

    /* 文件夹打开用完后,要记得第一时间关闭 */
    closedir(dirp);

    /* found在定义时初始化为0,如果上面的代码没有找到 "28-" 文件名则其值依然为0,否则将会被
     * 设置为1。如果 found 的值为0的话,则打印错误信息并返回相应的错误码-3.
     */
    if( !found )
    {
        printf("Can not find ds18b20 in %s\n", w1_path);
        return -3;
    }

    /* 使用snprintf()函数生成完整路径/sys/bus/w1/devices/28-xxxxx/w1_slave
     * 并保存到 ds_path 中。
     */
    snprintf(ds_path, sizeof(ds_path), "%s/%s/w1_slave", w1_path, chip);

    /* 接下来打开 DS18B20 的采样文件,如果失败则返回相应的错误码-4。 */
    if( (fd=open(ds_path, O_RDONLY)) < 0 )
    {
        printf("open %s error: %s\n", ds_path, strerror(errno));
        return -4;
    }

    /* 读取文件中的内容将会触发 DS18B20温度传感器采样,这里读取文件内容保存到buf中 */
    if(read(fd, buf, sizeof(buf)) < 0)
    {
        printf("read %s error: %s\n", ds_path, strerror(errno));

        /* 1, 这里不能直接调用 return -5 直接返回,否则的话前面open()打开的文件描述符就没有关闭。
         * 这里设置 rv 为错误码-5,通过 goto 语句跳转到函数后面统一进行错误处理。
         *
         * 2, 在C语言编程时我们应该慎用goto语句进行"随意"的跳转,因为它会降低代码的可读性。但这里是
         * goto语句的一个非常典型应用,我们经常会用它来对错误进行统一的处理。
         *
         * 3,goto后面的cleanup为标号,它在下面的代码中定义。
         */
        rv = -5;
        goto cleanup;
    }

    /* 采样温度值是在字符串"t="后面,这里我们从buf中找到"t="字符串的位置并保存到ptr指针中 */
    ptr = strstr(buf, "t=");
    if( !ptr )
    {
        printf("ERROR: Can not get temperature\n");
        rv = -6;
        goto cleanup;
    }

    /* 因为此时ptr是指向 "t="字符串的地址(即't'的地址),那跳过2个字节(t=)后面的就是采样温度值 */
    ptr+=2;

    /* 接下来我们使用 atof() 函数将采样温度值字符串形式,转化成 float 类型。*/
    *temp = atof(ptr)/1000;

    /* 1,在这里我们对函数返回进行集中处理,其中 cleanup 为 goto 语句的标号;
     * 2,在函数退出时,我们应该考虑清楚在前面的代码中做了哪些事,这些事是否需要进行反向操作。如
     *    打开的文件或文件夹是否需要关闭,malloc()分配的内存是否需要free()等。
     * 3, 在最开始我们定义rv并赋初值为0(表示成功)是有原因的,如果前面的代码任何一个地方出现错误,
     *    则会将rv的值修改为相应的错误码,否则rv的值将始终为0(即没有错误发生),这里将统一返回。
     */
cleanup:
    close(fd);
    return rv;
}

2.2.2.4 交叉编译测试运行

程序编写好之后,我们需要使用交叉编译器编译 ds18b20 的测试程序。

guowenxue@ubuntu20:~/igkboard/apps$ arm-linux-gnueabihf-gcc ds18b20.c -o ds18b20

另外,我们可以使用 file 命令查看编译输出的文件信息,确实是ARM开发板Linux系统上运行的程序。

guowenxue@ubuntu20:~/igkboard/apps$ file ds18b20
ds18b20: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, BuildID[sha1]=81a4c52969ec742c357ed45a7d682e04b37a55a2, for GNU/Linux 3.2.0, not stripped

现在我们在开发板上通过 scp 命令 或其它方式将编译生成的测试程序下载到开发板上。

root@igkboard:~# scp -P 2200 guowenxue@192.168.2.2:~/ds18b20 .
guowenxue@192.168.2.2's password: 
ds18b20                                         100% 8752   794.3KB/s   00:00 

接下来,我们给该程序加上执行权限并运行。运行结果显示当前的温度值为 24.875℃。

root@igkboard:~# chmod a+x ds18b20 
root@igkboard:~# ./ds18b20 
DS18B20 get temperature: 24.875000 'C

如果,我们的开发板上没有连接 DS18B20 温度传感器,则程序的运行结果为:

root@igkboard:~# ./ds18b20 
Can not find ds18b20 in /sys/bus/w1/devices/
ERROR: ds18b20 get temprature failure