DS18B20 简介
DS18B20 温度传感器具有线路简单、体积小的特点,用来测量温度非常简单,在一根通信线上可以挂载多个 DS18B20 温度传感器。用户可以通过编程实现9~12 位的温度读数,每个 DS18B20 有唯一的 64 位序列号,保存在 rom 中,因
此一条总线上可以挂载多个 DS18B20。
DS18B20 模块硬件设计
DS18B20 也使用的是“1-Wire 单总线”,只通过一条数据线传输数据,既要控制器发送数据给芯片,又要通过芯片发送数据给控制器,是双向传输数据。DS18B20 的硬件设计电路与前面的 DHT11 基本一致,原理图如下:
DS18B20 模块软件设计
存储器介绍:
DS18B20 内部有个 64 位只读存储器(ROM)和 64 位配置存储器(SCRATCHP)。
64 位只读存储器(ROM)包含序列号等,具体格式如下图:
低八位用于 CRC 校验,中间 48 位是 DS18B20 唯一序列号,高八位是该系列产品系列号(固定为 28h)。因此,根据每个 DS18B20 唯一的序列号,可以实现一条总线上可以挂载多个 DS18B20 时,获取指定 DS18B20 的温度信息。
64 位配置存储器(SCRATCHP)由 9 个 Byte 组成,包含温度数据、配置信息等,具体格式如下图:
⚫ Byte[0:1]:温度值。也就是当我们发出一个测量温度的命令之后,还需要发送一个读内存的命令才能把温度值读取出来。
⚫ Byte[2:3]:TL 是低温阈值设置,TH 是高温阈值设置。当温度低于/超过阈值,就会报警。 TL、TH 存储在 EEPROM 中,数据在掉电时不会丢失;
⚫ Byte4:配置寄存器。用于配置温度精度为 9、10、11 或 12 位。配置寄存器也存储在 EEPROM 中,数据在掉电时不会丢失;
⚫ Byte[5:7]:厂商预留;
⚫ Byte[8]:CRC 校验码。
通信时序:
① 初始化时序
类似前面的 DHT11,主机要跟 DS18B20 通信,首先需要发出一个开始信号。
深黑色线表示由主机驱动信号,浅灰色线表示由 DS18B20 驱动信号。
最开始时引脚是高电平,想要开始传输信号,
a) 必须要拉低至少 480us,这是复位信号;
b) 然后拉高释放总线,等待 15~60us 之后,
c) 如果 GPIO 上连有 DS18B20 芯片,它会拉低 60~240us。
如果主机在最后检查到 60~240us 的低脉冲,则表示 DS18B20 初始化成功。
② 写时序
⚫ 如果写 0,拉低至少 60us(写周期为 60-120us)即可;
⚫ 如果写 1,先拉低至少 1us,然后拉高,整个写周期至少为 60us 即可。
③ 读时序
⚫ 主机先拉低至少 1us,随后读取电平,如果为 0,即读到的数据是 0,如果
为 1,即可读到的数据是 1。
⚫ 整个过程必须在 15us 内完成,15us 后引脚都会被拉高。
常用命令:
现在我们知道怎么发 1 位数据,收 1 位数据。发什么数据才能得到温度值,
这需要用到“命令”。
DS18B20 中有两类命令:ROM 命令、功能命令,列表如下
使用命令流程图:
DS18B20 芯片手册中有 ROM 命令、功能命令的流程图,先贴出来,下一小
节再举例。
① ROM 命令流程图
② 功能命令流程图
命令示例 1:单个 DS18B20 温度转换
总线上只一个 DS18B20 设备时,根据下表发送命令、读取数据。因为只有一个 DS18B20,所以不需要选择设备,发出“Skip ROM”命令。然后发户“ConvertT”命令启动温度转换;
等待温度转换成功后,要读数据前,也要发出“Skip ROM”命令。下表列得很清楚:
命令示例 2:指定 DS18B20 温度转换
总线上有多个 DS18B20 设备时,根据下表发送命令、读取数据。首先肯定是要选中指定设备:使用“Match ROM”命令发出 ROM Code 来选择中设备;然后发户“Convert T”命令启动温度转换;等待温度转换成功后,要读数据前,也要发出“Match ROM”命令、ROM Code。下表列得很清楚:
编程思路:
按照流程图来编程即可:
① 实现复位函数;
② 实现等待回应的函数;
③ 实现发送 1 位数据的函数,进而实现发送 1 字节的函数;
④ 实现读取 1 位数据的函数,进而实现读取 1 字节的函数;
⑤ 按照流程图或是表格,发送、接收数据。
修改设备树:
程序:
ds18b20_drv.c
#include "acpi/acoutput.h"
#include "asm-generic/errno-base.h"
#include "asm-generic/gpio.h"
#include "asm/gpio.h"
#include "asm/uaccess.h"
#include <linux/module.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/timer.h>
// struct gpio_desc{
// int gpio;
// int irq;
// char *name;
// int key;
// struct timer_list ds18b20_timer;
// } ;
// static struct gpio_desc gpios[] = {
// {115, 0, "ds18b20", },
// };
/* 主设备号 */
static int major = 0;
static struct class *ds18b20_class;
static struct gpio_desc *ds18b20_gpio_desc; // GPIO结构体操作指针
/* 自旋锁 */
static spinlock_t ds18b20_spinlock;
/* 延迟指定的微秒数 */
static void ds18b20_udelay(int us)
{
u64 time = ktime_get_ns();
/* 忙等待循环 直到指定时间才退出循环 */
while (ktime_get_ns() - time < us*1000);
}
/**
* 重置DS18B20并等待应答信号
*
* 该函数首先发送一个重置脉冲给DS18B20,然后等待设备发出的应答信号。
* 如果在预定时间内未收到应答信号,则视为通信失败。
*
* @return
* - 0: 重置并等待应答成功
* - -EIO: 重置后未在规定时间内收到应答信号,通信失败
*/
static int ds18b20_reset_and_wait_ack(void)
{
int timeout = 100; // 定义超时时间,用于等待ACK信号
/* 发送reset信号即0 */
gpiod_set_value(ds18b20_gpio_desc, 0);
ds18b20_udelay(480); // 发送重置脉冲的持续时间480us
/* 设置GPIO为输入模式,准备接收应答信号(自动拉高) */
gpiod_direction_input(ds18b20_gpio_desc);
/* 等待ACK(为低电平响应) */
while (gpiod_get_value(ds18b20_gpio_desc) && timeout--)
{
ds18b20_udelay(1);
}
/* 检查是否在规定时间内收到应答信号 */
if (timeout == 0)
return -EIO; // 超时未收到应答,返回错误
/* 等待ACK结束 先是低电平60~240us 然后回到高电平*/
timeout = 300;
while (!gpiod_get_value(ds18b20_gpio_desc) && timeout--)
{
ds18b20_udelay(1);
}
/* 检查是否在规定时间内完成应答信号的接收 */
if (timeout == 0)
return -EIO;
return 0; // 重置并等待应答成功,返回0
}
/**
* 向DS18B20发送命令。
*
* 该函数通过控制GPIO口,以单总线的方式向DS18B20发送一个命令字节。
* 每位数据的发送由高电平开始,根据命令位的值决定高低电平持续的时间,以区分0和1。
*
* @param cmd 要发送的命令字节,每一位都将被转换为单总线上的信号。
*/
static void ds18b20_send_cmd(unsigned char cmd)
{
int i;
/* 初始化总线为高电平,准备发送数据 */
gpiod_direction_output(ds18b20_gpio_desc, 1);
for (i = 0; i < 8; i++)
{
/* 根据当前位的值决定发送0还是1 */
if (cmd & (1<<i))
{
/* 发送1:先拉低电平2微秒,然后拉高电平60微秒 */
/* 发送1 */
gpiod_direction_output(ds18b20_gpio_desc, 0);
ds18b20_udelay(2);
gpiod_direction_output(ds18b20_gpio_desc, 1);
ds18b20_udelay(60);
}
else
{
/* 发送0:先拉低电平60微秒,然后拉高电平2微秒 */
/* 发送0 */
gpiod_direction_output(ds18b20_gpio_desc, 0);
ds18b20_udelay(60);
gpiod_direction_output(ds18b20_gpio_desc, 1);
}
}
}
/**
* 从DS18B20读取一个字节的数据的时序
*
* @param buf 存储读取到的数据的缓冲区,读取到的数据会存放在buf[0]中
*
* 该函数通过控制GPIO与DS18B20进行通信,读取一个字节的数据。
* 具体操作包括设置GPIO为输出高电平,然后逐位读取数据,每读取一位,
* 就将GPIO设置为输出低电平,等待一段时间后,切换为输入模式,再次等待
* 一段时间后,根据GPIO的读取值来确定当前位是0还是1。读取完所有位后,
* 将数据存储在buf中。
*/
static void ds18b20_read_data(unsigned char *buf)
{
int i;
unsigned char data = 0;
// 初始化GPIO为输出高电平,准备开始读取数据
gpiod_direction_output(ds18b20_gpio_desc, 1);
for (i = 0; i < 8; i++)
{
// 发送读取信号,将GPIO设置为输出低电平
gpiod_direction_output(ds18b20_gpio_desc, 0);
ds18b20_udelay(2); // 延迟2us
gpiod_direction_input(ds18b20_gpio_desc); // 将GPIO切换为输入模式,准备读取数据
ds18b20_udelay(15); //整个过程15us内完成
// 根据GPIO的值,确定当前位是0还是1
if (gpiod_get_value(ds18b20_gpio_desc))
{
data |= (1<<i);
}
// 延迟一段时间,为下一次读取做准备
ds18b20_udelay(50);
gpiod_direction_output(ds18b20_gpio_desc, 1); // 将GPIO设置回输出高电平
}
// 将读取到的数据存储在缓冲区中
buf[0] = data;
}
/********************************************************/
/*DS18B20的CRC8校验程序*/
/********************************************************/
/* 参考: https://www.cnblogs.com/yuanguanghui/p/12737740.html */
/**
* 计算一个字节的CRC校验值。
*
* 此函数用于根据特定的CRC算法对一个字节(8位二进制数据)进行校验,计算出该字节的CRC校验值。
* CRC(Cyclic Redundancy Check)校验是一种广泛使用的错误检测方法,通过计算数据的校验和来检测数据传输或存储过程中可能出现的错误。
*
* @param abyte 待校验的字节数据。
* @return 返回计算得到的CRC校验值。
*/
static unsigned char calcrc_1byte(unsigned char abyte)
{
unsigned char i,crc_1byte;
crc_1byte=0; //设定crc_1byte初值为0
for(i = 0; i < 8; i++)
{
if(((crc_1byte^abyte)&0x01)) //最低位是否相同
{
crc_1byte^=0x18; //固定
crc_1byte>>=1; //最高位清零
crc_1byte|=0x80; //最高位为1
}
else
crc_1byte>>=1; //右移一位不改变值
abyte>>=1; //准备迭代
}
return crc_1byte;
}
/* 参考: https://www.cnblogs.com/yuanguanghui/p/12737740.html */
/**
* 计算给定字节序列的CRC校验值。
*
* 此函数通过迭代每个字节,并对其进行CRC计算,最终返回计算得到的CRC校验值。
* CRC(Cyclic Redundancy Check)是一种广泛使用的错误检测码,通过计算数据的CRC值,
* 可以检测出数据在传输过程中是否发生了错误。
*
* @param p 指向待计算CRC校验值的字节序列的指针。
* @param len 字节序列的长度。
* @return 返回计算得到的CRC校验值。
*/
static unsigned char calcrc_bytes(unsigned char *p,unsigned char len)
{
unsigned char crc=0;
while(len--) //len为总共要校验的字节数
{
crc=calcrc_1byte(crc^*p++);
}
return crc; //若最终返回的crc为0,则数据传输正确
}
static int ds18b20_verify_crc(unsigned char *buf)
{
unsigned char crc;
crc = calcrc_bytes(buf, 8);
if (crc == buf[8])
return 0;
else
return -1;
}
/* 根据DS18B20传感器返回的字节数据计算温度值 */
static void ds18b20_calc_val(unsigned char ds18b20_buf[], int result[])
{
unsigned char tempL=0,tempH=0;
unsigned int integer;
unsigned char decimal1,decimal2,decimal;
tempL = ds18b20_buf[0]; //读温度低8位
tempH = ds18b20_buf[1]; //读温度高8位
if (tempH > 0x7f) //最高位为1时温度是负 0111 1111
{
tempL = ~tempL; //补码转换,取反加一
tempH = ~tempH+1;
integer = tempL / 16 + tempH * 16; //整数部分 二进制运算中,除以2相当于向右移一位,除以16相当于向右移4位,乘16左移4位
decimal1 = (tempL&0x0f)*10/16; //小数第一位
decimal2 = (tempL&0x0f)*100/16%10; //小数第二位
decimal = decimal1*10+decimal2; //小数两位
}
else
{
integer = tempL/16+tempH*16; //整数部分
decimal1 = (tempL&0x0f)*10/16; //小数第一位
decimal2 = (tempL&0x0f)*100/16%10; //小数第二位
decimal = decimal1*10+decimal2; //小数两位
}
result[0] = integer;
result[1] = decimal;
}
/* 实现对应的open/read/write等函数,填入file_operations结构体 */
static ssize_t ds18b20_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
unsigned long flags;
int err;
unsigned char kern_buf[9];
int i;
int result_buf[2];
if (size != 8)
return -EINVAL;
/* 1. 启动温度转换 */
/* 1.1 关中断
@在中断环境下获取ds18b20的自旋锁,并保存当前中断状态;
@spin_lock_irqsave: 是一个宏定义,获取自旋锁并保存中断状态;
@flags: 用于保存当前中断状态
*/
spin_lock_irqsave(&ds18b20_spinlock, flags); //确保单线的读取不会被打断
/* 1.2 发出reset信号并等待回应 */
err = ds18b20_reset_and_wait_ack();
if (err)
{
spin_unlock_irqrestore(&ds18b20_spinlock, flags); //释放自旋锁
printk("ds18b20_reset_and_wait_ack err\n");
return err;
}
/* 1.3 发出命令: skip rom, 0xcc (只有一个设备的时候才这样用)*/
ds18b20_send_cmd(0xcc);
/* 1.4 发出命令: 启动温度转换, 0x44 */
ds18b20_send_cmd(0x44);
/* 1.5 恢复中断 */
spin_unlock_irqrestore(&ds18b20_spinlock, flags);
/* 2. 等待温度转换成功 : 可能长达1s */
set_current_state(TASK_INTERRUPTIBLE); //将当前进程的状态设置为可中断的,表示当前进程可以被中断,例如被其他高优先级的进程抢占CPU。
schedule_timeout(msecs_to_jiffies(1000)); //让出CPU,并让当前进程进入睡眠状态等待1000毫秒
/* 3. 读取温度 */
/* 3.1 关中断 */
spin_lock_irqsave(&ds18b20_spinlock, flags);
/* 3.2 发出reset信号并等待回应 */
err = ds18b20_reset_and_wait_ack();
if (err)
{
spin_unlock_irqrestore(&ds18b20_spinlock, flags);
printk("ds18b20_reset_and_wait_ack second err\n");
return err;
}
/* 3.3 发出命令: skip rom, 0xcc */
ds18b20_send_cmd(0xcc);
/* 3.4 发出命令: read scratchpad, 0xbe */
ds18b20_send_cmd(0xbe);
/* 3.5 读9字节数据 */
for (i = 0; i < 9; i++)
{
ds18b20_read_data(&kern_buf[i]);
}
// /* 3.6 恢复中断 */
spin_unlock_irqrestore(&ds18b20_spinlock, flags);
/* 3.7 计算CRC验证数据 */
err = ds18b20_verify_crc(kern_buf);
if (err)
{
printk("ds18b20_verify_crc err\n");
return err;
}
/* 4. copy_to_user */
ds18b20_calc_val(kern_buf, result_buf);
err = copy_to_user(buf, result_buf, 8);
return 8;
}
/* 定义自己的file_operations结构体 */
static struct file_operations ds18b20_drv = {
.owner = THIS_MODULE,
.read = ds18b20_read,
};
static int ds18b20_probe(struct platform_device *pdev)
{
printk("====%s====\n", __FUNCTION__);
// /* 初始化互斥锁 */
spin_lock_init(&ds18b20_spinlock);
// 获得硬件信息
ds18b20_gpio_desc = gpiod_get(&pdev->dev, "ds18b20", GPIOD_OUT_HIGH);
if (IS_ERR(ds18b20_gpio_desc))
{
dev_err(&pdev->dev, "Failed to get GPIO for ds18b20\n");
return PTR_ERR(ds18b20_gpio_desc);
}
gpiod_direction_output(ds18b20_gpio_desc, 1);
/* 注册file_operations */
major = register_chrdev(0, "ds18b20_chrdev", &ds18b20_drv); /* /dev/gpio_desc */
ds18b20_class = class_create(THIS_MODULE, "ds18b20_class");
device_create(ds18b20_class, NULL, MKDEV(major, 0), NULL, "ds18b20"); /* /dev/ds18b20 */
return 0;
}
static int ds18b20_remove(struct platform_device *pdev)
{
printk("======%s=======\n", __FUNCTION__);
device_destroy(ds18b20_class, MKDEV(major, 0));
class_destroy(ds18b20_class);
unregister_chrdev(major, "ds18b20_chrdev");
gpiod_put(ds18b20_gpio_desc); // 释放 GPIO
return 0;
}
/* 定义设备树匹配表,用于识别和支持特定的LED驱动器 */
static const struct of_device_id ds18b20_match_table[] = {
/* 匹配字符串 "fire,ds18b20" 用于标识 */
{.compatible = "fire,ds18b20"},
/* 空项作为匹配表的结束标志 */
{},
};
/* 定义platform_driver */
static struct platform_driver ds18b20_driver = {
.probe = ds18b20_probe, // 设置探测函数,当设备被探测到时调用
.remove = ds18b20_remove, // 设置移除函数,当设备被移除时调用
/* 设置<驱动程序的名称>和<设备树匹配表> */
.driver = {
.name = "ds18b20", // 字符设备名
.of_match_table = ds18b20_match_table, // 设置设备树匹配表,用于设备的匹配
},
};
/* 在入口函数 */
static int __init ds18b20_platform_driver_init(void)
{
int ret = 0;
printk("====%s====\n", __FUNCTION__);
ret = platform_driver_register(&ds18b20_driver); // 注册驱动程序
return ret;
}
/* 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数
*/
static void __exit ds18b20_platform_driver_exit(void)
{
printk("====%s====\n", __FUNCTION__);
platform_driver_unregister(&ds18b20_driver); // 销毁设备信息
}
/* 7. 其他完善:提供设备信息,自动创建设备节点 */
module_init(ds18b20_platform_driver_init);
module_exit(ds18b20_platform_driver_exit);
MODULE_LICENSE("GPL");
ds18b20_test.c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
#include <signal.h>
static int fd;
/*
* ./ds18b20_test /dev/ds18b20
*
*/
int main(int argc, char **argv)
{
int buf[2];
int ret;
int i;
/* 1. 判断参数 */
if (argc != 2)
{
printf("Usage: %s <dev>\n", argv[0]);
return -1;
}
/* 2. 打开文件 */
fd = open(argv[1], O_RDWR | O_NONBLOCK);
if (fd == -1)
{
printf("can not open file %s\n", argv[1]);
return -1;
}
while (1)
{
if (read(fd, buf, 8) == 8)
printf("get ds18b20: %d.%d\n", buf[0], buf[1]);
else
printf("get ds18b20: -1\n");
sleep(5);
}
close(fd);
return 0;
}
Makefile
# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH, 比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH, 比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
# 请参考各开发板的高级用户使用手册
KERN_DIR = /home/book/100ask_imx6ull-sdk/Linux-4.9.88 # 板子所用内核源码的目录
all:
make -C $(KERN_DIR) M=`pwd` modules
$(CROSS_COMPILE)gcc -o ds18b20_test ds18b20_test.c
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order ds18b20_test
# 参考内核源码drivers/char/ipmi/Makefile
# 要想把a.c, b.c编译成ab.ko, 可以这样指定:
# ab-y := a.o b.o
# obj-m += ab.o
obj-m += ds18b20_drv.o