Linux内核实践(一)驱动DS18B20传感器的完整流程解析(涵盖字符设备、单总线、设备树等)


  

   本节内容涉及知识点:字符设备驱动(使用杂项设备框架)、平台总线、编写设备树、获取设备树资源、gpio子系统、pinctrl子系统、单总线协议等。

一、单总线知识点及概念

   在 Linux 中,“单总线”(One-Wire Bus)指的是一种串行通信总线标准,通常被用于与具有低数据速率需求的设备进行通信。它是一种由 Dallas Semiconductor(现在是 Maxim Integrated 的一部分)开发的通信协议。

1. 特点

(1)单线通信: 单总线协议只使用一条数据线(再加上一个地线)进行通信,既可以用来传输数据,也可以用来提供电源。这使得它非常适合于需要低成本和简单布线的应用场合。
(2)设备地址唯一性: 每个连接到单总线上的设备都有一个唯一的 64 位序列号,便于总线上挂载多个设备而不会发生冲突。
(3)半双工通信: 数据传输是半双工的,即同一时间数据只能在一个方向上传输。
(4)低速通信: 单总线的传输速率较低,通常在 16 kbps 左右。这适合传输速率要求不高的应用场景,如温度传感器数据传输。
(5)多设备连接: 单总线可以支持在同一条线上连接多个设备,这种特性非常适合需要监控多个传感器的应用。

2. 常见应用

   温度传感器: 最著名的单总线设备可能是 DS18B20,它是一个数字温度传感器,广泛应用于各种温度监控系统。
   电子标签: 单总线还常用于简单的电子标签和其他需要低数据速率、简单连接的设备。

3. Linux 中的支持

   在 Linux 系统中,One-Wire 总线设备通常通过 w1 子系统进行管理。w1 子系统提供了对 One-Wire 总线的内核支持,用户可以通过 /sys/bus/w1/devices/ 目录访问 One-Wire 设备。
   Linux 内核模块如 w1-gpio 提供了对使用 GPIO 引脚的 One-Wire 总线的支持,而 w1-therm 模块则提供对温度传感器如 DS18B20 的支持。

二、ds18b20温度传感器

1. 硬件详情

   DS18B20 是一种数字温度传感器,广泛应用于各种温度监测场合。它的主要特点包括高精度、易用性和低成本,适用于嵌入式系统、物联网设备等场景。
   DS18B20 使用 One-Wire(单总线)通信协议,直接输出数字温度数据,而无需模数转换(ADC),这简化了与微控制器的接口设计。
   该传感器可以提供 9 到 12 位分辨率的温度数据,默认分辨率为 12 位,可通过配置更改。它的测量范围为 -55°C 到 +125°C,在 -10°C 到 +85°C 范围内的精度为 ±0.5°C。

2. 工作原理

●当单总线上只有一个DS18B20的时候:
  (1)开始温度转换:复位信号->发送ROM指令0XCC跳过搜索->发送RAM指令0x44进行温度转换。
  (2)读温度:复位信号->发送ROM指令0XCC跳过搜索->发送RAM指令0xbe读暂存器。
  (3)设置DS18B20:复位信号->发送ROM指令0XCC跳过搜索->发送RAM指令0x4e写暂存器->要写的数据。

●当单总线上有多个DS18B20的时候:
  (1)开始进行温度转换:复位信号->发送ROM指令0X55匹配指令->发送DS18B20的地址->发送RAM指令0x44进
行温度转换。
  (2)读温度:复位信号->发送ROM指令0X55匹配指令->发送DS18B20的地址->发送RAM指令0xbe读暂存器。
  (3)设置DS18B20:复位信号->发送ROM指令0X55匹配指令->发送DS18B20的地址->发送RAM指令0x4e写暂存器->要写的数据。

三、驱动DS18B20传感器的完整流程解析(总线上只有一个从机设备)

1. 编写设备树文件。

   我们需要在源码的设备树文件的根节点下添加自定义节点。例如我们使用 gpio0 的b0 端口作为ds18b20的信号线,此时我们需要添加以下节点。文章:设备树知识点详情。节点添加完成后,重新编译源码文件生成最新镜像包,安装到开发板上。

/ {    //根节点
	ds18b20-sensor {  // 用更通用的名称代替
		compatible = "ds18b20"; //要与平台设备的驱动程序中的compatible名称对应,用于匹配。
		ds18b20-gpio = <&gpio0 RK_PB0 1>; //属性为自定义名称,使用gpio0控制器,PB7引脚(代码里为宏定义) 1表示高电平。
		pinctrl-names = "default";
		pinctrl-0 = <&ds18b20_gpio_pinctrl>; //使用这个节点的配置
	};
};


//在源码设备树的pinctrl的总节点下,添加 ds18b20_gpio_pinctrl节点。
&pinctrl{       //相当于在pinctrl下面追加节点。
		ds18b20_gpio_pinctrl: ds18b20-gpio-pinctrl {
		rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>;
		   /* 0: 表示引脚所在的引脚组(或引脚控制器)。
           RK_PB7 : 表示具体的引脚编号。
           RK_FUNC_GPIO: 表示引脚的功能,这里是配置为 GPIO。这里为宏定义,实际值为0。
           &pcfg_pull_none: 表示引脚的配置参数,这里表示没有上拉或下拉电阻。
        */
	};
};

2. 编写字符设备驱动、平台总线驱动的总体框架。

这里为了方便框架的编写,我们使用杂项设备驱动框架来编写字符设备。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

ssize_t my_read(struct file *file, char __user *ubuf, size_t count, loff_t *offset)
{
    return 0;
}

int my_open(struct inode *node, struct file *fp)
{
    return 0;
}

ssize_t my_write(struct file *file, const char __user *ubuf, size_t count, loff_t *offset)
{
    return 0;
}

static const struct file_operations myfops = {
    .read = my_read,
    .write = my_write,
    .open = my_open,
};

static struct miscdevice my_misc = {
    .minor = MISC_DYNAMIC_MINOR,   //自动分配次设备号
    .fops = &myfops,
    .name = "ds18b20_misc"   //在/dev/目录下生成名为ds18b20_misc的设备节点
};

static int ds18b20_probe(struct platform_device *pdev)
{
    int ret;
    ret = misc_register(&my_misc);   
    if (ret < 0) {
        pr_err("misc_register error\n");
        return ret;
    }
    return 0;
}

static const struct of_device_id ds18b20_id_list[] = {
    { .compatible = "ds18b20", },    //与设备树进行匹配的名称
    { }
};

static struct platform_driver ds18b20_driver = {
    .driver = {
        .name = "ds18b20",
        .owner = THIS_MODULE,
        .of_match_table = ds18b20_id_list,
    },
    .probe = ds18b20_probe,
};

static int __init ds18b20_init(void)
{
    int ret;
    ret = platform_driver_register(&ds18b20_driver);
    if (ret < 0) {
        pr_err("platform_driver_register error\n");
        return ret;
    }
    return 0;
}

static void __exit ds18b20_exit(void)
{
    misc_deregister(&my_misc);
    platform_driver_unregister(&ds18b20_driver);
}

module_init(ds18b20_init);
module_exit(ds18b20_exit);

MODULE_LICENSE("GPL");

3. 解析获取设备树资源。

匹配成功后,在probe函数中获取设备树中GPIO资源。使用详情查看:设备树使用详情文章的最后部分。

struct gpio_desc *ds18gpio;

static int ds18b20_probe(struct platform_device *pdev)
{
  ds18gpio=gpiod_get_optional(&pdev->dev, "ds18b20", 0); //获取gpio
  if (!ds18gpio) {
        dev_err(&pdev->dev, "Failed to get GPIO for LED\n");
        return -ENODEV; // 返回错误码,表示无法获取GPIO
    }
    
  return 0;
}

static void __exit ds18b20_exit(void)
{
    gpiod_put(ds18gpio);   //释放掉ds18gpio。
}

4. 根据ds18b20工作时序图,编写相应时序代码。

(1)复位信号
在这里插入图片描述

#include <linux/delay.h>  // For udelay()
#include <gpiod.h>        // For GPIO functions
struct gpio_desc *ds18gpio;

void ds18b20_reset(void)
{
	gpiod_direction_output(ds18gpio, 1);  //初始为高电平
	gpiod_set_value(ds18gpio, 0);
	udelay(480);  //拉低至少480us
	gpiod_set_value(ds18gpio, 1);
	
	gpiod_direction_input(ds18gpio);  //主机接收
	while(gpiod_get_value(ds18gpio));
	while(!gpiod_get_value(ds18gpio));
	udelay(480);  //拉低至少480us
}

(2)写时序
在这里插入图片描述

#include <linux/delay.h>  // For udelay()
#include <gpiod.h>        // For GPIO functions
struct gpio_desc *ds18gpio;

void ds18b20_writebit(unsigned char bit)
{
  gpiod_direction_output(ds18gpio, 1);  //设置初始为高电平
  gpiod_set_value(ds18gpio, 0);  //设置变为低电平

  if (bit) {
      udelay(15);  // 会在15us后采样 
      gpiod_set_value(ds18gpio, 1);
  } 
  udelay(65);
  gpiod_set_value(ds18gpio, 1);
  udelay(2);
}

void ds18b20_writebyte(int data)
{
  int i;
  for (i = 0; i < 8; i++) {
      ds18b20_writebit(data & 0x01);
      data = data >> 1;  //低位先发
  }
}

(3)读时序
在这里插入图片描述

#include <linux/delay.h>  // For udelay()
#include <gpiod.h>        // For GPIO functions
struct gpio_desc *ds18gpio;

unsigned char ds18b20_readbit(void)
{
  unsigned char bit;
  gpiod_direction_output(ds18gpio, 1);  //设置初始为高电平
  gpiod_set_value(ds18gpio, 0);  //设置变为低电平
  udelay(2);  // 初始>1us 
  
  gpiod_direction_input(ds18gpio);  //设置为输入模式
  udelay(10);  // 初始>1us 
  bit= gpiod_get_value(ds18gpio); //获取值
  udelay(60);
  
  return bit;
}

int ds18b20_readbyte(void)
{
  int i;
  unsigned char bit;
  int value = 0;  // Initialize value to 0

  for (i = 0; i < 8; i++) {
      bit = ds18b20_readbit();  // Read the next bit
      value |= (bit << i);  // Add the bit to the correct position in the value
  }

  return value;
}

5. 根据工作原理编写ds18b20测温函数,在内核驱动程序read接口处调用。

  (1)开始温度转换:复位信号->发送ROM指令0XCC跳过搜索->发送RAM指令0x44进行温度转换。
  (2)读温度:复位信号->发送ROM指令0XCC跳过搜索->发送RAM指令0xbe读暂存器。
  (3)设置DS18B20:复位信号->发送ROM指令0XCC跳过搜索->发送RAM指令0x4e写暂存器->要写的数据。

int read_ds18b20_temp(void)
{
    int temp_low,temp_high,temp;
	ds18b20_reset();
	ds18b20_writebyte(0xCC);
	ds18b20_writebyte(0x44);
	mdelay(750);
	
	ds18b20_reset();
	ds18b20_writebyte(0xCC);
	ds18b20_writebyte(0xbe);
	
    temp_low=ds18b20_readbyte();
    temp_high=ds18b20_readbyte();
    
	temp =temp_high << 8|temp_low;
	return temp;
}


ssize_t my_read(struct file *file, char __user *ubuf, size_t count, loff_t *offset)
{
	int temp;
	int ret;
	temp= read_ds18b20_temp();
	ret=copy_to_user(ubuf,&temp,sizeof(temp));
    if (ret != 0) {
        printk(KERN_ERR "copy_to_user\n");
        return -EFAULT;
    }
    return 0;
}

6. 完整项目代码

(1)驱动层代码

ds18b20_driver.c

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/kernel.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/delay.h>  // For udelay()
#include <linux/gpio.h>        // For GPIO functions

struct gpio_desc *ds18gpio;

extern void ds18b20_reset(void);
extern void ds18b20_writebit(unsigned char bit);
extern void ds18b20_writebyte(int data);
extern unsigned char ds18b20_readbit(void);
extern int ds18b20_readbyte(void);
extern int read_ds18b20_temp(void);


ssize_t my_read(struct file *file, char __user *ubuf, size_t count, loff_t *offset)
{
	int temp;
	int ret;
	temp= read_ds18b20_temp();
	ret=copy_to_user(ubuf,&temp,sizeof(temp));
    if (ret != 0) {
        printk(KERN_ERR "copy_to_user\n");
        return -EFAULT;
    }
   return sizeof(temp);
}

int my_open(struct inode *node, struct file *fp)
{
    return 0;
}

ssize_t my_write(struct file *file, const char __user *ubuf, size_t count, loff_t *offset)
{
    return 0;
}

static const struct file_operations myfops = {
    .read = my_read,
    .write = my_write,
    .open = my_open,
};

static struct miscdevice my_misc = {
    .minor = MISC_DYNAMIC_MINOR,   //自动分配次设备号
    .fops = &myfops,
    .name = "ds18b20_misc"   //在/dev/目录下生成名为ds18b20_misc的设备节点。只有匹配成功后,才会生成节点。
};

static int ds18b20_probe(struct platform_device *pdev)
{
    int ret;
    ret = misc_register(&my_misc);   
    if (ret < 0) {
        pr_err("misc_register error\n");
        return ret;
    }
    
   ds18gpio=gpiod_get_optional(&pdev->dev, "ds18b20", 0); //获取gpio
   if (!ds18gpio) {
        dev_err(&pdev->dev, "Failed to get GPIO for LED\n");
        return -ENODEV; // 返回错误码,表示无法获取GPIO
    }
    return 0;
}

static const struct of_device_id ds18b20_id_list[] = {
    { .compatible = "ds18b20", },    //与设备树进行匹配的名称
    { }
};

static struct platform_driver ds18b20_driver = {
    .driver = {
        .name = "ds18b20",
        .owner = THIS_MODULE,
        .of_match_table = ds18b20_id_list,
    },
    .probe = ds18b20_probe,
};

static int __init ds18b20_init(void)
{
    int ret;
    ret = platform_driver_register(&ds18b20_driver);
    if (ret < 0) {
        pr_err("platform_driver_register error\n");
        return ret;
    }
    return 0;
}

static void __exit ds18b20_exit(void)
{
    misc_deregister(&my_misc);
    gpiod_put(ds18gpio);   //释放掉ds18gpio。
    platform_driver_unregister(&ds18b20_driver);
}

module_init(ds18b20_init);
module_exit(ds18b20_exit);

MODULE_LICENSE("GPL");

ds18b20_temp.c

#include <linux/delay.h>  // For udelay()
#include <linux/gpio.h>        // For GPIO functions

extern struct gpio_desc *ds18gpio;

//复位信号
void ds18b20_reset(void)
{
	gpiod_direction_output(ds18gpio, 1);  //初始为高电平
	gpiod_set_value(ds18gpio, 0);
	udelay(480);  //拉低至少480us
	gpiod_set_value(ds18gpio, 1);
	
	gpiod_direction_input(ds18gpio);  //主机接收
	while(gpiod_get_value(ds18gpio));
	while(!gpiod_get_value(ds18gpio));
	udelay(480);  //拉低至少480us
}

//写bit
void ds18b20_writebit(unsigned char bit)
{
    gpiod_direction_output(ds18gpio, 1);  //设置初始为高电平
    gpiod_set_value(ds18gpio, 0);  //设置变为低电平

    if (bit) {
        udelay(15);  // 会在15us后采样 
        gpiod_set_value(ds18gpio, 1);
    } 
    udelay(65);
    gpiod_set_value(ds18gpio, 1);
    udelay(2);
}
//写一个字节
void ds18b20_writebyte(int data)
{
    int i;
    for (i = 0; i < 8; i++) {
        ds18b20_writebit(data & 0x01);
        data = data >> 1;  //低位先发
    }
}
//读bit
unsigned char ds18b20_readbit(void)
{
	unsigned char bit;
	gpiod_direction_output(ds18gpio, 1);  //设置初始为高电平
	gpiod_set_value(ds18gpio, 0);  //设置变为低电平
	udelay(2);  // 初始>1us 
	
	gpiod_direction_input(ds18gpio);  //设置为输入模式
	udelay(10);  // 初始>1us 
	bit= gpiod_get_value(ds18gpio); //获取值
	udelay(60);
	
	return bit;
}
//读一个字节
int ds18b20_readbyte(void)
{
    int i;
    unsigned char bit;
    int value = 0;  // Initialize value to 0

    for (i = 0; i < 8; i++) {
        bit = ds18b20_readbit();  // Read the next bit
        value |= (bit << i);  // Add the bit to the correct position in the value
    }

    return value;
}

//读取温度值
int read_ds18b20_temp(void)
{
    int temp_low,temp_high,temp;
	ds18b20_reset();
	ds18b20_writebyte(0xCC);
	ds18b20_writebyte(0x44);
	mdelay(750);
	
	ds18b20_reset();
	ds18b20_writebyte(0xCC);
	ds18b20_writebyte(0xbe);
	
    temp_low=ds18b20_readbyte();
    temp_high=ds18b20_readbyte();
    
	temp =temp_high << 8|temp_low;
	return temp;
}

(2)应用层代码

app.c

测量值转为温度公式:
  当温度>0时,高5位(符号位)为0,测量到的温度值乘以0.0625即可得到实际的温度。
  当温度<0时,高5位(符号位)为1,测量到的温度值取反加1在乘以0.0625即可得到实际温度。

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdint.h>

void ds18b20_get_temp(int value)
{
    char sig;
    float temp;

    if ((value >> 11) & 0x01) {  //寄存器为16位寄存器
        sig = '-';
        value = ~value + 1;  // Convert two's complement to positive
    } else {
        sig = '+';
    }

    value &= 0x07FF;  //只保留低11位,因为高5位是符号位,不需要参与计算。
    temp = value * 0.0625;  // Multiply by resolution (0.0625°C)

    printf("Temp is %c%.4f°C\n", sig, temp);
}

int main(int argc, char *argv[])
{
    int fd;
    int data;

    // Open the device file
    fd = open("/dev/ds18b20_misc", O_RDWR);
    if (fd < 0) {
        printf("Open error\n");
        return -1;
    }

    while (1) {
			read(fd, &data, sizeof(data));
			ds18b20_get_temp(data);
			sleep(1);  // 1 second delay
    }

    close(fd);
    return 0;
}

(3)Makefile

#多个c文件生成一个ko文件。
obj-m +=ds18b20_test.o                 #名字自定义,但是不要和.c文件的o文件名字一样!!
ds18b20_test-objs =ds18b20_driver.o  ds18b20_temp.o       # -objs前的名字要和定义的名字一致

# KDIR 内核源码路径,根据自己需要设置
KDIR:=/lib/modules/4.4.0-210-generic/build/


all:
#ARCH: 指当前编译的驱动模块的架构
#CROSS_COMPILE:指明交叉编译器的前缀
#C: 指定去$(KDIR)目录下执行Makefile
#M:告知Makefile,需要的编译文件在哪
#modules: 这个规则是用于编译驱动模块的
	@make -C $(KDIR) M=$(PWD) modules
	@rm -fr .tmp_versions *.o *.mod.o *.mod.c *.bak *.symvers *.markers *.un    signed *.order *~ .*.*.cmd .*.*.*.cmd
	
clean:
	@make -C $(KDIR) M=$(PWD) modules clean

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值