⑨tiny4412 Linux驱动开发之1-wire子系统(DS18B20)驱动程序

本来这次想做LCD背光灯的调节的,但是没有调通,时间很紧迫,就转向了其它东西,昨天调了一下DHT11,今天又调了一下DS18B20,还算有个安慰,本来是想用1-wire子系统做的,但是时间上有点紧,要看源代码好长时间去领悟,所以,先自己用以前的笨方法先实现.

先来介绍一下DS18B20:

    DS18B20 是由 DALLAS 半导体公司推出的一种的 “一线总线”接口的温度传感器。与传统的热敏电阻等测温元件相比,它是一种新型的体积小、适用电压宽、与微处理器接口简单的数字化温度传感器。一线总线结构具有简洁且经济的特点,可使用户轻松地组建传感器网络,从而为测量系统的构建引入全新概念,测量温度范围为-55~+125℃ ,精度为±0. 5℃。 

现场温度直接以“一线总线”的数字方式传输,大大提高了系统的抗干扰性。它能直接读出被测温度,并且可根据实际要求通过简单的编程实现 9~l2 位的数字值读数方式。它工作在3—5. 5 V 的电压范围,采用多种封装形式,从而使系统设计灵活、方便,设定分辨率及用户设定的报警温度存储在EEPROM 中,掉电后依然保存。其内部结构如图所示:

⑨tiny4412 Linux驱动开发之1-wire子系统(DS18B20)驱动程序_第1张图片

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

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

1).复位脉冲和应答脉冲  

    单总线上的所有通信都是以初始化序列开始。主机输出低电平,保持低电平时间至少 480us,以产生复位脉冲。接着主机释放总线,4.7K 的上拉电阻将单总线拉高,延时 15~60 us,并进入接收模式(Rx)。接着 DS18B20 拉低总线 60~240 us,以产生低电平应答脉冲,若为低电平,再延时 480 us。

2 )  写时序  

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

3 )读时序  
    单总线器件仅在主机发出读时序时,才向主机传输数据,所以,在主机发出读数据命令后,必须马上产生读时序,以便从机能够传输数据。所有读时序至少需要 60us,且在 2 次独立的读时序之间至少需要 1us 的恢复时间。每个读时序都由主机发起,至少拉低总线1us。主机在读时序期间必须释放总线,并且在时序起始后的 15us 之内采样总线状态。典型的读时序过程为:主机输出低电平延时 2us,然后主机转入输入模式延时 12us,然后读取单总线当前的电平,然后延时 50us。  

    在了解了单总线时序之后,我们来看看 DS18B20 的典型温度读取过程, DS18B20 的典型温度读取过程为:

复位-->发 SKIP ROM 命令(0XCC)-->发开始转换命令(0X44)-->延时-->复位-->发送 SKIP ROM 命令(0XCC)->发读存储器命令(0XBE)-->连续读出两个字节数据(即温度)-->结束。DS18B20 的介绍就到这里,更详细的介绍,请大家参考 DS18B20的数据手册

⑨tiny4412 Linux驱动开发之1-wire子系统(DS18B20)驱动程序_第2张图片

下面,来讲一下驱动的设计思路:

1).DS18B20的初始化  

初始化时序图;

⑨tiny4412 Linux驱动开发之1-wire子系统(DS18B20)驱动程序_第3张图片

设计分析:

主机首先发出一个480-960微秒的低电平脉冲,然后释放总线变为高电平,并在随后的480微秒时间内对总线进行检测,如果有低电平出现说明总线上有器件已做出应答。若无低电平出现一直都是高电平说明总线上无器件应答。

     作为从器件的DS18B20在一上电后就一直在检测总线上是否有480-960微秒的低电平出现,如果有,在总线转为高电平后等待15-60微秒后将总线电平拉低60-240微秒做出响应存在脉冲,告诉主机本器件已做好准备.若没有检测到就一直在检测等待。

初始化代码;

/* 1.复位信号 */
void DS18B20_Rst(void)
{
    gpio_request(DQ_IN_GPIO, "DQ_OUT");

    gpio_direction_output(DQ_IN_GPIO, 0);
    udelay(750);		//拉低750us
    
    gpio_direction_output(DQ_IN_GPIO, 1);
    udelay(15);			//拉高15us
    
    gpio_free(gpio_get_value(DQ_IN_GPIO));
}

DS18B20 写操作  

⑨tiny4412 Linux驱动开发之1-wire子系统(DS18B20)驱动程序_第4张图片

     主机发出各种操作命令都是向DS18B20写0和写1组成的命令字节, 接收数据时也是从DS18B20读取0或1的过程。因此首先要搞清主机是如何进行写0、写1、读0和读1的。  

     写周期最少为60微秒,最长不超过120微秒。写周期一开始做为主机先把总线拉低1微秒表示写周期开始。随后若主机想写0,则将总线置为低电平,若主机想写1,则将总线置为高电平,持续时间最少 60微秒直至写周期结束,然后释放总线为高电平至少1微秒给总线恢复。而DS18B20则在检测到总线被拉底后等待15微秒然后从15us到45us开始对总线采样,在采样期内总线为高电平则为1,若采样期内总线为低电平则为0 。

写数据代码:

/* 3.写时序,一个字节 */
void DS18B20_Write_Byte(int dat)
{
    int j,testb;
    
    gpio_request(DQ_IN_GPIO, "DQ_OUT");
    
    for(j = 0; j < 8; j++){
        testb = dat & 0x1;
        dat >>= 1;
        
        if(testb){  // 写1时序
            gpio_direction_output(DQ_IN_GPIO, 0);				
            udelay(2);
        
            gpio_direction_output(DQ_IN_GPIO, 1);
            udelay(60);
        } else {    // 写0时序
            gpio_direction_output(DQ_IN_GPIO, 0);
            udelay(60);

            gpio_direction_output(DQ_IN_GPIO, 1);
            udelay(2);
        }
    }
    
    gpio_free(DQ_IN_GPIO);
}

DS18B20 读操作

⑨tiny4412 Linux驱动开发之1-wire子系统(DS18B20)驱动程序_第5张图片

     对于读数据操作时序也分为读0时序和读1时序两个过程。读周期是从主机把单总线拉低1微秒之后就得释放单总线为高电平,以让DS18B20把数据传输到单总线上。作为从机DS18B20在检测到总线被拉低1微秒后,便开始送出数据,若是要送出0就把总线拉为低电平直到读周期结束。若要送出1则释放总线为高电平。主机在一开始拉低总线1微秒后释放总线,然后在包括前面的拉低总线电平1微秒在内的15微秒时间内完成对总线进行采样检测,采样期内总线为低电平则确认为0 。采样期内总线为高电平则确认为1。完成一个读时序过程,至少需要60微秒才能完成。

/* 4.读一bit数据 */
int DS18B20_Read_Bit(void)
{
    int data;

    gpio_request(DQ_IN_GPIO, "DQ_OUT");
    
    gpio_direction_output(DQ_IN_GPIO, 0);
    udelay(2);
    
    gpio_direction_output(DQ_IN_GPIO, 1);
    udelay(12);
    
    // gpio_free(DQ_IN_GPIO);
    
    // gpio_request(DQ_IN_GPIO, "DQ_IN");
    
    gpio_direction_input(DQ_IN_GPIO);
    
    if(gpio_get_value(DQ_IN_GPIO))
        data = 1;
    else
        data = 0;
    udelay(50);
    
    gpio_free(DQ_IN_GPIO);

    return data;
}

/* 5.读一个字节的数据 */
int DS18B20_Read_Byte(void)
{
    int i, j, dat = 0;
    
    for(i = 0; i < 8; i++)
    {
        j = DS18B20_Read_Bit();
        
        //低位先行,j左移7位,把最低位赋给dat,同时dat左移1位,把低位右移,循环8次你懂的
        dat = (j << 7) | (dat >> 1);
    }

    return dat;
}

DS18B20 的寄存器简介
1.   DS18B20  内部结构主要由四部分组成:  
1), 64  位光刻  ROM光刻ROM中的64位序列号是出厂前被光刻好的,它可以看作是该DS18B20的地址序列码。64 位光刻ROM的排列是:开始8位(地址:28H)是产品类型标号,接着的48位是该DS18B20自身的序列号,并且每个DS18B20的序列号都不相同,因此它可以看作是该DS18B20的地址序列码;最后8位则是前面56位的循环冗余校验码(CRC=X8+X5+X4+1)。由于每一个DS18B20的ROM数据都各不相同,因此微控制器就可以通过单总线对多个DS18B20进行寻址,从而实现一根总线上挂接多个DS18B20的目的。
2), 温度传感器  
3), 非挥发的温度报警触发器TH和TL
4), 配置寄存器。  
 

2. DS18B20的存储器由一个高速暂存RAM和一个非易失性、 电可擦除(E2)RAM组成.

⑨tiny4412 Linux驱动开发之1-wire子系统(DS18B20)驱动程序_第6张图片

我们看一下第4个字节,配置寄存器:

⑨tiny4412 Linux驱动开发之1-wire子系统(DS18B20)驱动程序_第7张图片

⑨tiny4412 Linux驱动开发之1-wire子系统(DS18B20)驱动程序_第8张图片

出厂设置默认R0和R1都配置为1,也就是温度计分辨率是12bit.

⑨tiny4412 Linux驱动开发之1-wire子系统(DS18B20)驱动程序_第9张图片

1.8  读取温度操作  
1.  读取温度操作  
     DS18B20  经转换所得的温度值以二字节补码形式存放在高速暂存存储器的第0和第1个字节。所以当我们只想简单的读取温度值的时候,只用读取暂存器中的第0和第1个字节就可以了。读取一次D18B20温度的操作步骤  如下:  
a)  初始化DS18B20
b)  跳过ROM操作(ROM里面可以读取DS18B20的地址、型号,还有  配置分辨率等,我们只使用一个DS18B2,所以不用读取地址型号,配置直接使用默认的12位分辨率就好了.  
c)  发送温度转换命令。  
d)  跳过ROM操作  
e)  发送读取温度命令  

f)  读取温度值。

2   补码介绍
什么是补码  
a)  正数的补码是正数本身 
b)  负数的补码是原码取反,然后再加 1。DS18B20存储的温度值是以补码的形式存储的,所以读出来的温度值是实际温度值的补码,要把的转换为原码。  

     1)正温度的话,原码就是补码本身,所以在12位分辨率下,温度的计算公式是:

      温度值=读取值*0.0625  

     2)负温度的话,原码是补码减1再取反,所以在12位分辨率下,计算公式为:

      温度值= -(读取值取反再加1)*0.0625

/* 7.处理温度信息 */
int DS18B20_Get_temp(void)
{
    int tem = 0,
        TL  = 0, 
        TH  = 0;
    
    DS18B20_Start();
    DS18B20_Rst();
    DS18B20_CheckAck();
    DS18B20_Write_Byte(0xcc);
    DS18B20_Write_Byte(0xbe);

    TL = DS18B20_Read_Byte();
    TH = DS18B20_Read_Byte();

    if(TH > 7){
        TH = ~TH;
        TL = ~TL;
        temp = 0;
    }else{
        temp = 1;
    }

    tem = TH;
    tem <<= 8;
    tem += TL;
    
	/**
	 * 内核不支持浮点运算,如果在内核里做计算会丢失小数部分
	 * 所以计算留给用户程序来完成,这里直接传输原始数据即可
	*/ 
    if(temp){
        return tem;
    }else
        return ~tem + 1;
}

下面来一个完整的驱动代码:

#include 
#include 
#include 
#include 
#include 
#include 
#include 

#include 
#include 
#include 

#include 


#define DQ_IN_GPIO      EXYNOS4_GPX3(2)     // 操作DS18B20的GPIO

/*********************************************DS18B20************************************************/
/* 1.复位信号 */
void DS18B20_Rst(void)
{
    gpio_request(DQ_IN_GPIO, "DQ_OUT");

    gpio_direction_output(DQ_IN_GPIO, 0);
    udelay(750);		//拉低750us
    
    gpio_direction_output(DQ_IN_GPIO, 1);
    udelay(15);			//拉高15us
    
    gpio_free(gpio_get_value(DQ_IN_GPIO));
}
		
/* 2.应答信号 */
int DS18B20_CheckAck(void)
{
    int retry = 0;
    
    gpio_request(DQ_IN_GPIO, "DQ_IN");
    gpio_direction_input(DQ_IN_GPIO);

    while(gpio_get_value(DQ_IN_GPIO) & (retry < 200))
    {
        retry++;
        udelay(1);
    };

    if(retry >= 200){
        gpio_free(gpio_get_value(DQ_IN_GPIO));
        return -1;
    }

    retry = 0;
    while(!gpio_get_value(DQ_IN_GPIO) & (retry < 240))
    {
        retry++;
        udelay(1);
    }

    if(retry >= 240){
        gpio_free(gpio_get_value(DQ_IN_GPIO));
        return -1;
    }

    gpio_free(gpio_get_value(DQ_IN_GPIO));

    return 0;
}

/* 3.写时序,一个字节 */
void DS18B20_Write_Byte(int dat)
{
    int j,testb;
    
    gpio_request(DQ_IN_GPIO, "DQ_OUT");
    
    for(j = 0; j < 8; j++){
        testb = dat & 0x1;
        dat >>= 1;
        
        if(testb){  // 写1时序
            gpio_direction_output(DQ_IN_GPIO, 0);				
            udelay(2);
        
            gpio_direction_output(DQ_IN_GPIO, 1);
            udelay(60);
        } else {    // 写0时序
            gpio_direction_output(DQ_IN_GPIO, 0);
            udelay(60);

            gpio_direction_output(DQ_IN_GPIO, 1);
            udelay(2);
        }
    }
    
    gpio_free(DQ_IN_GPIO);
}

/* 4.读一bit数据 */
int DS18B20_Read_Bit(void)
{
    int data;

    gpio_request(DQ_IN_GPIO, "DQ_OUT");
    
    gpio_direction_output(DQ_IN_GPIO, 0);
    udelay(2);
    
    gpio_direction_output(DQ_IN_GPIO, 1);
    udelay(12);
    
    // gpio_free(DQ_IN_GPIO);
    
    // gpio_request(DQ_IN_GPIO, "DQ_IN");
    
    gpio_direction_input(DQ_IN_GPIO);
    
    if(gpio_get_value(DQ_IN_GPIO))
        data = 1;
    else
        data = 0;
    udelay(50);
    
    gpio_free(DQ_IN_GPIO);

    return data;
}

/* 5.读一个字节的数据 */
int DS18B20_Read_Byte(void)
{
    int i, j, dat = 0;
    
    for(i = 0; i < 8; i++)
    {
        j = DS18B20_Read_Bit();
        
        //低位先行,j左移7位,把最低位赋给dat,同时dat左移1位,把低位右移,循环8次你懂的
        dat = (j << 7) | (dat >> 1);
    }

    return dat;
}

/* 6.温度转换 */
void DS18B20_Start(void)
{
    DS18B20_Rst();
    DS18B20_CheckAck();
    DS18B20_Write_Byte(0xcc);
    DS18B20_Write_Byte(0x44);
}

/* 7.处理温度信息 */
int DS18B20_Get_temp(void)
{
    int tem = 0,
        TL  = 0, 
        TH  = 0;
    
    DS18B20_Start();
    DS18B20_Rst();
    DS18B20_CheckAck();
    DS18B20_Write_Byte(0xcc);
    DS18B20_Write_Byte(0xbe);

    TL = DS18B20_Read_Byte();
    TH = DS18B20_Read_Byte();

    if(TH > 7){
        TH = ~TH;
        TL = ~TL;
        temp = 0;
    }else{
        temp = 1;
    }

    tem = TH;
    tem <<= 8;
    tem += TL;
    
	/**
	 * 内核不支持浮点运算,如果在内核里做计算会丢失小数部分
	 * 所以计算留给用户程序来完成,这里直接传输原始数据即可
	*/ 
    if(temp){
        return tem;
    }else
        return ~tem + 1;
}
/*****************************************************************************************************/

int 
ds18b20_open(struct inode *inode, struct file *filp)
{
    // 硬件初始化
    /*do nothing*/

    return 0;
}

ssize_t 
ds18b20_read(struct file *filp, char __user *buf, size_t count, loff_t *fpos)
{
    int ret = -1,
        temp_buf;

    temp_buf = DS18B20_Get_temp();
    
    ret = copy_to_user(buf, &temp_buf, count);
    if(ret > 0){
        printk("copy to user failed !\n");
        return -EFAULT;
    }

    return count;
}

int 
ds18b20_release(struct inode *inode, struct file *filp)
{
    // 释放引脚
    gpio_free(DQ_IN_GPIO);

    return 0;
}

const struct file_operations ds18b20_fops = {
    .open   = ds18b20_open,
    .read   = ds18b20_read,
    .release= ds18b20_release,
};

struct miscdevice ds18_misc = {
    .minor  = MISC_DYNAMIC_MINOR,
    .name   = "ds18b20",
    .fops   = &ds18b20_fops,
};

static void __exit
ds18b20_drv_exit(void)
{
    misc_deregister(&ds18_misc);
}

static int __init
ds18b20_drv_init(void)
{
    // 注册杂项设备,因为比直接的普通字符设备少写好多代码
    int ret = misc_register(&ds18_misc);
    if(ret < 0){
        printk("misc register failed !\n");
        return -EINVAL;
    }

    return 0;
}

module_init(ds18b20_drv_init);
module_exit(ds18b20_drv_exit);

MODULE_LICENSE("GPL");

然后是测试代码;

#include 
#include 

#include 
#include 
#include 
#include 



int main(void)
{
    int fd = -1,
        temp= 0,
	    a = 0;

    fd = open("/dev/ds18b20", O_RDWR);
    if(fd < 0){
        perror("open failed");
        exit(1);
    }

    for(;;)
    {
        read(fd, &temp, sizeof(temp));
        
	    a = (float)temp * 0.625;

	    if(a > -551 && a < 1251)
	        printf(" temp: %d.%d C\n", a / 10, a % 10);

        sleep(2);
    }

    if(close(fd) < 0){
        perror("close failed");
        exit(3);
    }

    return 0;
}

然后是Makefile:

#指定内核源码路径
KERNEL_DIR = /home/george/1702/exynos/linux-3.5

#指定当前路径
CUR_DIR = $(shell pwd)


MYAPP = w1_ds18b20_app
MODULE = w1_ds18b20

all:
	make -C $(KERNEL_DIR) M=$(CUR_DIR) modules
	arm-none-linux-gnueabi-gcc -o $(MYAPP) $(MYAPP).c
clean:
	make -C $(KERNEL_DIR) M=$(CUR_DIR) clean
	$(RM) $(MYAPP)
install:
	cp -raf *.ko $(MYAPP) /home/george/1702/exynos/filesystem/1702

#指定编译当前目录下那个源文件
obj-m = $(MODULE).o

附一张测试效果图:

⑨tiny4412 Linux驱动开发之1-wire子系统(DS18B20)驱动程序_第10张图片

第1次显示的85℃是正常的,是初始化信息,其它两次58.3℃和90.8℃是不正常的,但是大多数数据都是OK的,大概的原因可能是我这个传感器是外接的,友善之臂真扣,连个DS18B20都不带,外接的接触不良或者有干扰可能会造成这种情况.好了先写到这里吧.

你可能感兴趣的:(Linux)