Android驱动的系统笔记3


注意:本网站所有内容为自己学习期间记录的笔记,基本都是原作者那边摘抄过来的,不作商业用途也不插播广告。(如有侵权,请联系我删除,15909440083)

北京迅为电子

同款开发板购买链接

【第16期的 最新驱动资料(文档+例程)】
链接:https://pan.baidu.com/s/1Qf5d5_e2u_VklJWJK9wzNQ 提取码:n4q6

第十三篇输入子系统

第十四篇单总线

第155 章单总线简介

155.1 单总线概述

单总线(One-Wire)是一种串行通信协议和硬件总线,用于在电子设备之间传输数据和控制信号。它是由独立的芯片制造商Dallas Semiconductor 开发的,并且在多种应用中得到了广泛应用。与SPI I2C 等串行数据通信方式不同,单总线的特点是只需要一根信号线,既可以传输时钟又可以传输数据,而且数据是双向的。所以单总线具有节省IO 口,结构简单,便于扩展和维护等特点

单总线用于各种应用,包括温度传感器,湿度传感器,EEPROM 存储器,时钟等。它在许多领域中得到了广泛应用,例如工业自动化,家庭自动化,物联网和电子设备监测等。本篇使用温度传感器DS18b20 进行举例学习单总线。

单总线的硬件结构包括以下主要组成部分:

image-20240925093927376

  • 1 信号线:单总线使用一根信号线进行数据传输和通信。这根线被称为数据线,也是提供电源的线路。单总线上的所有设备都连接到这根信号线上。
  • 2 上拉电阻:单总线需要一个上拉电阻连接到信号线和电源之间,以确保在没有设备发送数据时,信号线上的电平保持为高电平(逻辑1)。上拉电阻的值通常在4.7 千欧姆到10 千欧姆之间。
  • 3 设备:单总线支持多个设备连接到信号线上。每个设备都具有唯一的64 位地址,通过这个地址来识别和选择通信的目标设备。设备可以是各种类型的传感器,存储器,时钟等。
  • 4 处理器:处理器是单总线上的控制器,处理器负责发送命令,读取响应和控制单总线上的从设备。

155.2 单总线的通信步骤

单总线是主从结构,当主机呼叫从机时,从机才会应答,所以主机都必须严格遵循单总线的命令时序。如果命令时序不对,则器件不会响应。

单总线的通信步骤通常包括以下几个阶段。

  • 1 初始化:通信开始之前,主设备会发送初始化信号来确保单总线上没有其他设备正在通信。初始化信号是一个特定的序列,通常是将数据线拉低一段时间然后释放。
  • 2 ROM 操作命令
  • 3 功能命令

第156 章DS18B20 介绍

本篇使用温度传感器DS18b20 进行举例学习单总线。所以本章节我们来认识下DS18b20。

156.1 DS18b20 芯片概述

DS18b20 是一种数字温度传感器芯片,提供9 到12bit 分辨率的温度测量,可以通过可编程非易失性存储单元实现温度的下限和上限报警。它是基于单总线通信协议的设备,只需要一根信号线和一根地线。DS18b20 能够以较高的精度测量温度精确到0.0625°C。它具有广泛的测量范围,通常介于-55°C 到+125°C 之间。DS18B20 芯片可以通过单总线从主设备获取供电,也可以通过外部电源进行供电。这使得它在一些低功耗应用中能够灵活选择供电方式。每个DS18b20 都会有一个全球唯一的64 位序列号,可以将多个DS18b20 串联在同一根单总线上进行组网,只需要一个处理器就可以控制分布在大面积区域中的多颗DS18b20。这种组网方式特别适合HVAC 环境控制,建筑,设备,粮情测温和工业测温以及过程监测控制等应用领域。

156.2 DS18b20 基本性能

DS18B20 是一款数字温度传感器芯片,具有以下基本性能特点:

  • 采用单总线接口仅需一个端口引脚进行通信
  • 每颗芯片具有全球唯一的64 位的序列号
  • 具有多点分布式测温功能无需外围元器件
  • 可通过数据线供电,供电电压范围为2.5V~5.5V
  • 测度测量范围为-55°C 到+125°C
  • 在-10°C~ 70°C 范围内精确度为±0.4°C
  • 温度分辨率9-12 位可选
  • 最高12 位精度下,温度转换速度小于750ms
  • 具有用户自定义的非易失性温度报警设置
  • 报警搜索命令识别并标识超过程序设定温度的器件
  • 超强静电保护能力:HBM 8000V MM 800V
  • 可提供贴片的MSOP8,SOP8 封装和3 脚的TO-92、TO-92S 封装。

156.3 DS18b20 应用场景

DS18B20 广泛应用于各种温度监测和控制场景。以下是一些常见的应用场景:

  • 室内温度监测:DS18B20 可以用于监测室内温度,例如住宅、办公室、实验室等。它可以提供精确的温度数据,帮助调整空调、采暖系统或其他温度控制设备。
  • 温室和农业:在温室和农业领域,DS18B20 可以用于监测植物生长环境的温度。这有助于优化种植条件,提高作物的生长效率和产量。
  • 水族箱温度控制:DS18B20 可用于监测水族箱的水温,确保鱼类和其他水生生物的舒适和健康。
  • 工业自动化:在工业自动化中,DS18B20 可以用于监测机器、设备或工艺过程中的温度。这有助于实现温度控制、故障监测和预警等功能。
  • 太阳能热水器:DS18B20 可以用于太阳能热水器系统中的温度监测和控制。它可以测量太阳能收集器和水箱中的温度,以优化热水的供应和节约能源。
  • 冷链物流:在冷链物流中,DS18B20 可用于监测货物运输过程中的温度。它可以确保货物在适宜的温度条件下运输,以保持产品的质量和安全性。
  • 气象站:DS18B20 可以用于气象站中的温度测量。它可以提供准确的环境温度数据,用于天气观测和气候研究。

156.4 DS18b20 引脚配置和封装

DS18B20 芯片引脚配置:

  • VDD:供电引脚,用于提供芯片的正电源。
  • DQ:数据引脚,用于单总线通信和数据传输。
  • GND:地引脚,连接芯片的地(负电源)。

DS18B20 芯片可用于不同的封装类型,其中最常见的封装是TO-92 封装和TO-92-3 封装。这些封装都是具有三个引脚的小型封装,适用于直插式安装和表面贴装。
TO-92 封装是一种常见的小型塑料封装,引脚按照顺序排列,依次为VDD、DQ 和GND。
TO-92-3 封装与TO-92 封装类似,但引脚顺序略有不同。TO-92-3 封装的引脚顺序为GND、DQ 和VDD。

除了这些常见的封装类型,DS18B20 还可以在其他封装类型中使用,例如SOT-23 封装和TO-263 封装等,这些封装类型可能具有不同的引脚排列和尺寸。如下图所示:

image-20240925101807145

image-20240925101812400

156.5 DS18b20 内部结构

DS18b20 是一种数字温度传感器芯片,其内部结构主要包括以下组成部分,如下图所示:

image-20240925101829894

  • 温度传感器:DS18B20 内部集成了温度传感器,用于测量环境的温度。传感器通常基于基准电压的变化来检测温度,并将其转换为数字信号。
  • A/D 转换器:DS18B20 芯片内部包含了一种模数转换器(A/D 转换器),用于将传感器测量到的模拟温度值转换为相应的数字表示。这使得温度数据能够以数字形式进行处理和传输。
  • 存储器:DS18B20 芯片还具有内部存储器,用于存储配置信息和温度测量结果。存储器可以存储唯一的64 位地址、温度分辨率和其他相关设置。
  • 控制逻辑:DS18B20 芯片包含了控制逻辑电路,用于管理温度测量、通信和其他相关功能。控制逻辑协调各个部分的操作,并与主设备进行通信。
  • 单总线接口:DS18B20 采用了单总线通信协议,其内部结构包括一条数据线和一个上拉电阻,用于与主设备进行通信。单总线接口简化了连接和通信的布线,使得多个DS18B20 传感器能够方便地串联在同一条总线上。

这些组成部分相互配合,使得DS18B20 芯片能够准确地测量温度并提供数字输出。其结构设计简单、集成度高,使得DS18B20 在各种温度监测和控制应用中得到广泛应用。

存储器由9 个字节组成,其分配如下表所示。

image-20240925101923270

当温度转换命令发布后,经转换所得的温度值以二字节补码形式存放在高速暂存存储器的第0 和第1 个字节,单片机可通过单线接口读到该数据,读取时低位在前,高位在后,数据格式如下表所示。

image-20240925101956638

156.6 DS18b20 寄存器介绍

DS18B20 芯片具有几个重要的寄存器,用于配置和控制传感器的功能。以下是DS18B20常用的寄存器介绍:

  • 温度寄存器(Temperature Register):温度寄存器存储了最近一次温度测量的结果。它是一个16 位的寄存器,包含了温度值的原始数据。通过读取温度寄存器中的数据,并结合分辨率设置,可以计算出实际的温度值。
  • 配置寄存器(Configuration Register):配置寄存器用于设置DS18B20 的工作模式和温度分辨率。它是一个8 位的寄存器,每个位对应一个配置选项。通过写入配置寄存器,可以选择温度分辨率、触发温度转换和使用电源供电模式等。
  • 精度寄存器(Resolution Register):精度寄存器用于设置温度分辨率。它是一个8 位的寄存器,每个位对应一种分辨率选项。通过写入精度寄存器,可以选择不同的温度分辨率,例如9 位、10 位、11 位或12 位。
  • 唯一地址寄存器(Unique Address Register):唯一地址寄存器存储了DS18B20 芯片的唯一64 位地址。每个DS18B20 芯片都有唯一的地址,通过读取唯一地址寄存器中的数据,可以获取芯片的地址信息。

这些寄存器可以通过单总线通信协议与DS18B20 进行读写操作。通过读取温度寄存器和唯一地址寄存器,可以获取温度测量结果和唯一地址信息。通过写入配置寄存器和精度寄存器,可以设置温度分辨率和其他相关配置。

156.6.1 配置寄存器

配置寄存器用于设置DS18B20 的工作模式和温度分辨率。它是一个8 位的寄存器,每个位对应一个配置选项。如下图所示:

image-20240925102217316

注:上电默认设置R0=1,R1=1(12 位精度)。精度和转换时间之间有直接关系。配置寄存器的位7 和位0 到4 被器件保留,禁止写入。温度分辨率设置表如下所示:

image-20240925102234384

156.6.2 温度测量寄存器

DS18B20 芯片的温度寄存器是一个16 位的寄存器,用于存储最近一次温度测量的原始数据。温度寄存器的位布局如下所示:

image-20240925104208345

温度寄存器的最低有效位(LSB)是2^-4 位,表示温度的最小精度为0.0625°C。其他位依次表示更高的温度精度,分别为2^-3(0.125)、2^-2(0.25)、2^-1(0.5)、2^0(1)、2^1(2)、2^2(4)、2^3(8)。

DS18B20 芯片的温度寄存器中存储的原始数据可以通过以下步骤计算出实际温度值:

  • 1 从温度寄存器读取的16 位数据可以解释为一个有符号整数,其中最高位(MSB)表示符号位。如果符号位为0,表示正温度;如果符号位为1,表示负温度。
  • 2 取出温度寄存器中的低11 位(位4 到位15),这些位表示温度的绝对值,其中位值为1 表示该位对应的温度分辨率有效。
  • 3 将这11 位数据与符号位组合成一个有符号整数。
  • 4 根据所选择的温度分辨率,将有符号整数乘以相应的分辨率因子,以获得实际的温度值。
  • 5 当温度大于0 时,符号位为0,测量到的温度值乘以分辨率因子即可得到实际的温度。当温度小于0 时,符号位为1,测量得到的温度值取反加一再乘以分辨率因子即可得到实际的温度。

举个例子,假设选择了12 位的温度分辨率,并从温度寄存器读取的数据为0x1FFF。
0x1FFF 的二进制表示为:0001 1111 1111 1111。最高位为0,表示正温度。
取出低11 位:111 1111 1111。将这11 位与符号位组合,得到有符号整数为:0111 1111 1111(对应0x07FF)。对于12 位的温度分辨率,分辨率因子为0.0625°C。将有符号整数0x07FF乘以分辨率因子:0x07FF * 0.0625 = 127.9375°C。因此,从温度寄存器读取的数据0x1FFF 对应着约127.94°C 的实际温度值。

156.7 DS18b20 指令介绍

根据DS18B20 的通讯协议,主机(单片机)控制DS18B20 完成温度转换必须经过三个步骤:每一次读写之前都要对DS18B20 进行复位操作,复位成功后发送一条ROM 指令,最后发送RAM 指令,这样才能对DS18B20 进行预定的操作。复位要求主CPU 将数据线下拉500 微秒,然后释放,当DS18B20 收到信号后等待16~60 微秒左右,后发出60~240 微秒的存在低脉冲,主CPU 收到此信号表示复位成功。

ROM 指令表如下所示:

image-20240925105016595

RAM 指令表如下所示:

image-20240925105025941

操作举例:

当单总线上只有一个DS18b20 的时候

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

当单总线上有多个ds18b20 的时候

  • 1 开始进行温度转换:复位信号->发送ROM 指令0x55 匹配指令->发送DS18b20 的地址->发送RAM 指令0x44 进行温度转换。
  • 2 读温度:复位信号->发送ROM 指令0x55 匹配指令->发送DS18b20 的地址->发送RAM 指令0xbe 读暂存器。
  • 3 设置ds18b20:复位信号->发送ROM 指令0x55 匹配指令->发送DS18b20 的地址->发送RAM 指令0x4e 写暂存器->要写的数据。

第157 章DS18B20 驱动框架编写

编写DS18b20 驱动涉及以下几个关键知识点

  • 1 字符设备驱动
  • 2 平台总线
  • 3 设备树
  • 4 解析设备树
  • 5 gpio 子系统
  • 6 pinctrl 子系统
  • 7 单总线协议

157.1 驱动程序的编写

本章节我们编写DS18b20 驱动框架编写,如下所示:
本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\94_ds18b20_01
编写完成的ds18b20.c 代码如下所示。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>


int ds18b20_probe(struct platform_device *dev){
    printk("This is probe \n");
    return 0;
}

const struct of_device_id ds18b20_match_table[] = {
    {.compatible = "ds18b20"},
    {},
};

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


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

    return 0;
}

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

module_init(ds18b20_init);
module_exit(ds18b20_exit);
MODULE_LICENSE("GPL");

158.2 编译驱动程序

在上一小节中的ds18b20.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成ds18b20.ko 目标文件,至此DS18b20 驱动框架编写完成了,并且编译驱动模块成功了。

第158 章DS18B20 编写字符设备驱动框架

接着上个章节的驱动编写,本章节实现DS18b20 编写字符设备驱动框架。

158.1 驱动程序的编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\94_ds18b20_02

编写完成的ds18b20.c 代码如下所示。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/gpio/consumer.h> // 添加此头文件
#include <linux/delay.h>


struct ds18b20_data
{
    dev_t dev_num;
    struct cdev ds18b20_cdev;
    struct class *ds18b20_class;
    struct device *ds18b20_device;
    struct gpio_desc *ds18b20_gpio;
};

struct ds18b20_data *ds18b20;
//复位函数
void ds18b20_reset(void)
{
    //主机发送 复位脉冲
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);   //把GPIO设置为输出
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);          //拉低
    udelay(700);                                        //至少480us(根据手册)

    //主机接收 接收从机发的应答脉冲
    gpiod_set_value(ds18b20->ds18b20_gpio, 1);          //拉高 它的时间就是15-60us(这是主机先拉高,后续从机会拉低)
    gpiod_direction_input(ds18b20->ds18b20_gpio);       //把GPIO设置为输入

    while ( gpiod_get_value(ds18b20->ds18b20_gpio));
    while ( !gpiod_get_value(ds18b20->ds18b20_gpio));
    udelay(480);
}

int ds18b20_open(struct inode *inode, struct file *file)
{
    return 0;
}

ssize_t ds18b20_read(struct file *file, char __user *buf, size_t size, loff_t *offs)
{
    return 0;
}

int ds18b20_release(struct inode *inode, struct file *file)
{
    return 0;
}

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

int ds18b20_probe(struct platform_device *dev)
{
    int ret;
    printk("This is probe \n");
    ds18b20 = kzalloc(sizeof(*ds18b20), GFP_KERNEL);
    if (ds18b20 == NULL)
    {
        printk("kzalloc error\n");
        ret = -ENOMEM;
        goto error_0;
    }
    
    ret = alloc_chrdev_region(&ds18b20->dev_num, 0, 1, "myds18b20");
    if (ret < 0){
        printk("alloc_chrdev_region error\n");
        ret = -EAGAIN;
        goto error_1;
    }

    cdev_init(&ds18b20->ds18b20_cdev, &ds18b20_fops);

    ds18b20->ds18b20_cdev.owner = THIS_MODULE;
    cdev_add(&ds18b20->ds18b20_cdev, ds18b20->dev_num, 1);      //向系统中注册字符设备

    ds18b20->ds18b20_class = class_create(THIS_MODULE, "sensors");      //自动创建设备类
    if (IS_ERR(ds18b20->ds18b20_class)){
        printk("class_create error\n");
        ret = PTR_ERR(ds18b20->ds18b20_class);
        goto error_2;
    }

    ds18b20->ds18b20_device = device_create(ds18b20->ds18b20_class, NULL, ds18b20->dev_num, NULL, "ds18b20");   //在类下面创建设备
    if (IS_ERR(ds18b20->ds18b20_device)){
        printk("device_create error\n");
        ret = PTR_ERR(ds18b20->ds18b20_device);
        goto error_3;
    }

    ds18b20->ds18b20_gpio = gpiod_get_optional(&dev->dev, "ds18b20", 0);
    if (ds18b20->ds18b20_gpio == NULL){
        ret = -EBUSY;
        goto error_4;
    }

    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);

    return 0;
error_4:
    device_destroy(ds18b20->ds18b20_class, ds18b20->dev_num);

error_3:
    class_destroy(ds18b20->ds18b20_class);

error_2:
    cdev_del(&ds18b20->ds18b20_cdev);
    unregister_chrdev_region(ds18b20->dev_num, 1);

error_1:
    kfree(ds18b20);

error_0:
    return ret;
}

const struct of_device_id ds18b20_match_table[] = {
    {.compatible = "ds18b20"},
    {},
};

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

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

static void __exit ds18b20_exit(void)
{
    gpiod_put(ds18b20->ds18b20_gpio);
    device_destroy(ds18b20->ds18b20_class, ds18b20->dev_num);
    class_destroy(ds18b20->ds18b20_class);
    cdev_del(&ds18b20->ds18b20_cdev);
    unregister_chrdev_region(ds18b20->dev_num, 1);
    kfree(ds18b20);
    platform_driver_unregister(&ds18b20_driver);
}

module_init(ds18b20_init);
module_exit(ds18b20_exit);
MODULE_LICENSE("GPL");

158.2 编译驱动程序

在上一小节中的ds18b20.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成ds18b20.ko 目标文件,至此DS18b20 字符设备驱动框架编写完成了,并且编译驱动模块成功了。

第159 章DS18B20 驱动设备树

159.1 DS18b20 硬件连接

为了对核心板进行更好的资源利用,迅为对底板进行了升级。本手册对1.7 版本增加的20pin GPIO 接口进行外设适配。

GPIO 接口原理图如下图所示:

image-20240925110020205

Ds18b20 引脚如下所示:

image-20240925110031079

以下为对应的引脚连接表:

DS18b20 模块引脚编号 DS18b20 模块引脚名称 连接到的开发板的引脚编号 连接到的开发板的引脚名称
1 VCC 2 VCC3.3
2 DQ 1 DVP_PWREN0_H_GPIO0_B0
3 GND 19/20 GND

159.2 DS18B20 驱动设备树配置

打开Linux/linux_sdk/kernel/arch/arm64/boot/dts/rockchip/rk3568-evb1-ddr4-v10.dtsi 文件,在根目录下添加ds18b20 节点,如下所示:

image-20240925110248369

&pinctrl 节点添加如下节点。

image-20240925110259133

修改完成后,重新编译并烧写内核镜像。内核镜像烧写完毕,输入以下命令

cd /proc/device-tree/gpio0_b0/
ls
cat compatible

image-20240925110329258

然后输入以下命令加载上章节编译好的驱动模块,如下图所示:

image-20240925110347158

驱动加载成功之后,输入以下命令

ls /dev/ds18b20 			# 列出位于/dev 目录下的名为ds18b20 的设备文件
ls /sys/class/sensors/ 		 # 列出位于/sys/class/sensors/目录下的文件和子目录
cat /proc/devices 			# 用于查看系统中已加载的设备驱动程序列表。

image-20240925110432235

image-20240925110437254

至此,DS18b20 设备树配置和字符设备驱动框架已经完成了。

第160 章DS18B20 驱动复位时序编写

在上个章节的基础上,本章节我们继续编写DS18b20 驱动,在驱动中实现获取GPIO 和复位时序的驱动编写,并且在加载驱动之后,使用逻辑分析仪对复位时序进行分析。

160.1 DS18B20 驱动获取GPIO 编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\95_ds18b20_02
编写完成的ds18b20.c 代码如下所示,添加的代码已加粗表示。

image-20240925114521174

image-20240925114538293

160.2 复位和应答信号时序分析

在ds18b20 驱动文件中,复位和应答信号的时序分析是通过控制GPIO 引脚的电平和延时来实现的。下面详细解释一下信号时序的分析过程。

image-20240925114923794

如上图所示,复位信号时序分析

  • 在复位的过程中,首先将GPIO 引脚拉低,保持一段时间(通常为480 微秒),这是为了确保发送复位信号的低电平持续足够的时间触发DS18b20 的复位操作。
  • 接下来,释放GPIO 引脚,使其回到高电平状态,此时DS18b20 将检测到引脚状态的变化,并开始响应复位信号。
  • 在释放引脚之后需要等待一段时间,以确保ds18b20 完成复位操作并准备好接收后续的命令。

如上图所示,应答信号时序分析

  • 在发送命令或数据之前,需要先向DS18B20 发送一个启动信号,然后等待DS18B20 的应答信号。启动信号是通过将GPIO 引脚拉低一小段时间(通常为1 微秒)然后立即拉高来发送的。
  • 接下来,驱动程序需要检查引脚的状态,以确定DS18B20 是否发送了应答信号。
  • DS18B20 的应答信号是在启动信号后的一小段时间内由DS18B20 将引脚拉低发送的。驱动程序需要检测引脚状态是否被成功拉低,以确认应答信号的接收。

在驱动程序中,可以使用Linux 内核提供的GPIO API 来控制GPIO 引脚的电平状态和延时。

例如,在复位信号时序分析中,可以使用以下函数来控制GPIO 引脚的状态:

  • gpiod_direction_output():设置GPIO 引脚为输出模式。
  • gpiod_set_value():将GPIO 引脚设置为高电平或低电平。
  • udelay():延时一定的微秒数。

160.3 DS18b20 驱动复位时序编写

我们在160.1 小节驱动的基础上,继续编写复位时序驱动。
本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\95_ds18b20_02
编写完成的ds18b20.c 代码如下所示,添加的代码已加粗表示。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/gpio/consumer.h> // 添加此头文件
#include <linux/delay.h>

struct ds18b20_data
{
    dev_t dev_num;
    struct cdev ds18b20_cdev;
    struct class *ds18b20_class;
    struct device *ds18b20_device;
    struct gpio_desc *ds18b20_gpio;
};

struct ds18b20_data *ds18b20;

void ds18b20_reset(void)
{
    // 设置 GPIO 方向为输出,输出低电平
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);
    udelay(700); // 延迟 700 微秒

    // 设置 GPIO 输出高电平,并将 GPIO 方向设置为输入
    gpiod_set_value(ds18b20->ds18b20_gpio, 1);
    gpiod_direction_input(ds18b20->ds18b20_gpio);

    // 等待直到 GPIO 输入为低电平
    while (gpiod_get_value(ds18b20->ds18b20_gpio))
        ;

    // 等待直到 GPIO 输入为高电平
    while (!gpiod_get_value(ds18b20->ds18b20_gpio))
        ;
    udelay(480); // 延迟 480 微秒
}

/**
 * 向 DS18B20 写入单个位(bit)
 * @param bit 要写入的位(bit),0 或 1
 */
void ds18b20_writebit(unsigned char bit) {
    // 将 GPIO 方向设置为输出
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);

    // 将 GPIO 输出设置为指定的位(bit)
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);

    // 若 bit 为 1,则延时 10 微秒
    if (bit)
    {
        udelay(10);
        // 将 GPIO 方向设置为输出
    	gpiod_direction_output(ds18b20->ds18b20_gpio, 1);
    }
        



    // 延时 65 微秒
    udelay(65);

    // 将 GPIO 方向设置为输出
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);

    // 延时 2 微秒
    udelay(2);
}

/**
 * 向 DS18B20 写入一个字节(byte)数据
 * @param data 要写入的字节数据
 */
void ds18b20_writebyte(int data) {
    int i;

    for (i = 0; i < 8; i++) {
        // 逐位写入数据
        ds18b20_writebit(data & 0x01);
        data = data >> 1;
    }
}

int ds18b20_open(struct inode *inode, struct file *file)
{
    return 0;
}

ssize_t ds18b20_read(struct file *file, char __user *buf, size_t size, loff_t *offs)
{
    return 0;
}

int ds18b20_release(struct inode *inode, struct file *file)
{
    return 0;
}

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

int ds18b20_probe(struct platform_device *dev)
{
    int ret;
    printk("This is probe \n");

    // 分配内存给ds18b20_data结构体
    ds18b20 = kzalloc(sizeof(*ds18b20), GFP_KERNEL);
    if (ds18b20 == NULL)
    {
        printk("kzalloc error\n");
        ret = -ENOMEM;
        goto error_0;
    }

    // 分配字符设备号
    ret = alloc_chrdev_region(&ds18b20->dev_num, 0, 1, "myds18b20");
    if (ret < 0)
    {
        printk("alloc_chrdev_region error\n");
        ret = -EAGAIN;
        goto error_1;
    }

    // 初始化字符设备
    cdev_init(&ds18b20->ds18b20_cdev, &ds18b20_fops);
    ds18b20->ds18b20_cdev.owner = THIS_MODULE;
    cdev_add(&ds18b20->ds18b20_cdev, ds18b20->dev_num, 1);

    // 创建设备类
    ds18b20->ds18b20_class = class_create(THIS_MODULE, "sensors");
    if (IS_ERR(ds18b20->ds18b20_class))
    {
        printk("class_create error\n");
        ret = PTR_ERR(ds18b20->ds18b20_class);
        goto error_2;
    }

    // 创建设备
    ds18b20->ds18b20_device = device_create(ds18b20->ds18b20_class, NULL, ds18b20->dev_num, NULL, "ds18b20");
    if (IS_ERR(ds18b20->ds18b20_device))
    {
        printk("device_create error\n");
        ret = PTR_ERR(ds18b20->ds18b20_device);
        goto error_3;
    }

    // 获取GPIO描述符
    ds18b20->ds18b20_gpio = gpiod_get_optional(&dev->dev, "ds18b20", 0);
    if (ds18b20->ds18b20_gpio == NULL)
    {
        ret = -EBUSY;
        goto error_4;
    }

    // 设置GPIO方向为输出
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);

    return 0;

    error_4:
    device_destroy(ds18b20->ds18b20_class, ds18b20->dev_num);

    error_3:
    class_destroy(ds18b20->ds18b20_class);

    error_2:
    cdev_del(&ds18b20->ds18b20_cdev);
    unregister_chrdev_region(ds18b20->dev_num, 1);

    error_1:
    kfree(ds18b20);

    error_0:
    return ret;
}

const struct of_device_id ds18b20_match_table[] = {
    {.compatible = "ds18b20"},
    {},
};

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

static int __init ds18b20_init(void)
{
    int ret;

    // 注册平台驱动
    ret = platform_driver_register(&ds18b20_driver);
    if (ret < 0)
    {
        printk("platform_driver_register error\n");
        return -1;
    }

    ds18b20_reset(); // 调用复位函数
    ds18b20_writebyte(0xcc);//写入0Xcc字符
    return 0;
}

static void __exit ds18b20_exit(void)
{
    // 释放资源
    gpiod_put(ds18b20->ds18b20_gpio);
    device_destroy(ds18b20->ds18b20_class, ds18b20->dev_num);
    class_destroy(ds18b20->ds18b20_class);
    cdev_del(&ds18b20->ds18b20_cdev);
    unregister_chrdev_region(ds18b20->dev_num, 1);
    kfree(ds18b20);
    platform_driver_unregister(&ds18b20_driver);
}

module_init(ds18b20_init);
module_exit(ds18b20_exit);
MODULE_LICENSE("GPL");

160.4 编译驱动程序

在上一小节中的ds18b20.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成ds18b20.ko 目标文件,至此驱动模块就编译成功了。接下来通过逻辑分析仪验证复位时序。

160.5 通过逻辑分析仪验证复位时序

首先将DS18b20 模块和开发板背面的GPIO 连接起来,然后将逻辑分析仪连接到ds18b20模块上,连接如下图所示:

image-20240925115913263

硬件连接好之后,安装逻辑分析仪的上位机软件,安装完成之后,打开上位机软件,然后设置参数,如下图所示:

image-20240925115938873

image-20240925115945013

image-20240925115949840

设置完毕之后,如下图所示:

image-20240925115959097

然后将160.4 小节编译的驱动模块加载到内核中,如下图所示:

image-20240925120009200

上位机上捕捉到时序如下图所示:

image-20240925120018825

image-20240925120023325

image-20240925120027714

将捕捉到的复位时序和下图的时序图对比分析,可以看出驱动复位时序是没问题的。

image-20240925120040149

至此,DS18b20 驱动复位时序编写完成。

第161 章DS18B20 驱动写时序编写

在上个章节中成功在驱动中添加了ioctl 相关代码,通过ioctl 对DS18B20 采集到的温度分辨率进行设置。而在本章节将继续完善驱动程序,对DS18B20 采集到的温度分辨率进行读取,然后编写相应的应用程序进行分辨率读取测试。

161.1 写时序分析

DS18B20 数据手册中关于写入的时序图如下所示:

image-20240925141849895

关于DS18B20 有写入0 和写入1 两种时序,他们的时序是不同的,接下来首先对前半部分写入0 进行分析

  • 步骤1:主机拉低总线,从高电平变成低电平,且有时间限制,要确保拉低的时间最少为60 微秒,最多为120 微秒
  • 步骤2:从机采样读取,主机拉低总线的15us-60us 从机开始采样,如果读取到的是0,从机就会接收0,从而成功写入0。
  • 步骤3:拉高总线,恢复总线的高电平状态,且两个写操作的间隔必须大于1 微秒。

总结出的写0 操作代码如下所示:

void ds18b20_writebit_0() {
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);// 将GPIO 方向设置为输出
    
    gpiod_set_value(ds18b20->ds18b20_gpio, 0); // 将GPIO 输出设置为指定的位(bit)
    udelay(65);// 延时65 微秒
    
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);// 将GPIO 方向设置为输出
    udelay(2);// 延时2 微秒
}

然后来对写1 的步骤进行分析:

  • 步骤1:主机拉低总线,从高电平变成低电平,且有时间限制,要确保拉低的时间最少为1 微秒,并且不能超过15 微秒
  • 步骤2:拉高总线,恢复总线的高电平状态,
  • 步骤3:从机采样读取,拉高总线之后就会进行采样,如果读取到的是1,从机就会接收1,从而成功写入1.

总结出的写1 操作代码如下所示:

void ds18b20_writebit_1() {
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);// 将GPIO 方向设置为输出
    
    gpiod_set_value(ds18b20->ds18b20_gpio, 0); // 将GPIO 输出设置为指定的位(bit)
    udelay(10);// 延时105 微秒
    
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);// 将GPIO 方向设置为输出
    udelay(2);// 延时2 微秒
}

综合上面写1 和写0 操作的代码,可以将两个代码进行整合在一起,整合之后的代码如下所示:

/**
* 向DS18B20 写入单个位(bit)
* @param bit 要写入的位(bit),0 或1
*/
void ds18b20_writebit(unsigned char bit) {
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);// 将GPIO 方向设置为输出
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);// 将GPIO 拉低
    
    if (bit){
        udelay(10);//延时10 微秒
        gpiod_direction_output(ds18b20->ds18b20_gpio, 1);// 将GPIO 方向设置为输出
    }
    udelay(65);// 延时65 微秒
    
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);// 将GPIO 方向设置为输出
    udelay(2);// 延时2 微秒
}

但这样修改之后的代码仅仅只能发送一个字符,如果要连续写入8 位字符就需要连续使用8 次该函数,而为了更方便,可以重新添加一个函数,从而直接写入一个字节的数据,具体内容如下所示:

/**
* 向DS18B20 写入单个位(bit)
* @param bit 要写入的位(bit),0 或1
*/
void ds18b20_writebit(unsigned char bit) {
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);// 将GPIO 方向设置为输出
    
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);// 将GPIO 拉低
    if (bit){
        udelay(10);//延时10 微秒
        gpiod_direction_output(ds18b20->ds18b20_gpio, 1);// 将GPIO 方向设置为输出
    }
    udelay(65);// 延时65 微秒
    
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);// 将GPIO 方向设置为输出
    udelay(2);// 延时2 微秒
}

/**
* 向DS18B20 写入一个字节(byte)数据
* @param data 要写入的字节数据
*/
void ds18b20_writebyte(int data) {
    int i;
    for (i = 0; i < 8; i++) {
        // 逐位写入数据
        ds18b20_writebit(data & 0x01);
        data = data >> 1;
    }
}

至此,关于DS18B20 的写操作相关函数就编写完成了,会在下个小节编写填加写时序相关函数的驱动。

161.2 DS18b20 驱动写时序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\96_ds18b20_03\02_module。

相较于上一章节的驱动程序,本小节编写的驱动程序只是将写操作的步骤进行了完善,并在入口函数中调用了ds18b20 的复位和写函数进行测试。
编写完成的ds18b20.c 代码如下所示:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/gpio/consumer.h> // 添加此头文件
#include <linux/delay.h>

struct ds18b20_data
{
    dev_t dev_num;
    struct cdev ds18b20_cdev;
    struct class *ds18b20_class;
    struct device *ds18b20_device;
    struct gpio_desc *ds18b20_gpio;
};

struct ds18b20_data *ds18b20;

void ds18b20_reset(void)
{
    // 设置 GPIO 方向为输出,输出低电平
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);
    udelay(700); // 延迟 700 微秒

    // 设置 GPIO 输出高电平,并将 GPIO 方向设置为输入
    gpiod_set_value(ds18b20->ds18b20_gpio, 1);
    gpiod_direction_input(ds18b20->ds18b20_gpio);

    // 等待直到 GPIO 输入为低电平
    while (gpiod_get_value(ds18b20->ds18b20_gpio))
        ;

    // 等待直到 GPIO 输入为高电平
    while (!gpiod_get_value(ds18b20->ds18b20_gpio))
        ;
    udelay(480); // 延迟 480 微秒
}

/**
 * 向 DS18B20 写入单个位(bit)
 * @param bit 要写入的位(bit),0 或 1
 */
void ds18b20_writebit(unsigned char bit) {
    // 将 GPIO 方向设置为输出
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);

    // 将 GPIO 输出设置为指定的位(bit)
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);

    // 若 bit 为 1,则延时 10 微秒
    if (bit)
    {
        udelay(10);
        // 将 GPIO 方向设置为输出
    	gpiod_direction_output(ds18b20->ds18b20_gpio, 1);
    }
        



    // 延时 65 微秒
    udelay(65);

    // 将 GPIO 方向设置为输出
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);

    // 延时 2 微秒
    udelay(2);
}

/**
 * 向 DS18B20 写入一个字节(byte)数据
 * @param data 要写入的字节数据
 */
void ds18b20_writebyte(int data) {
    int i;

    for (i = 0; i < 8; i++) {
        // 逐位写入数据
        ds18b20_writebit(data & 0x01);
        data = data >> 1;
    }
}

int ds18b20_open(struct inode *inode, struct file *file)
{
    return 0;
}

ssize_t ds18b20_read(struct file *file, char __user *buf, size_t size, loff_t *offs)
{
    return 0;
}

int ds18b20_release(struct inode *inode, struct file *file)
{
    return 0;
}

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

int ds18b20_probe(struct platform_device *dev)
{
    int ret;
    printk("This is probe \n");

    // 分配内存给ds18b20_data结构体
    ds18b20 = kzalloc(sizeof(*ds18b20), GFP_KERNEL);
    if (ds18b20 == NULL)
    {
        printk("kzalloc error\n");
        ret = -ENOMEM;
        goto error_0;
    }

    // 分配字符设备号
    ret = alloc_chrdev_region(&ds18b20->dev_num, 0, 1, "myds18b20");
    if (ret < 0)
    {
        printk("alloc_chrdev_region error\n");
        ret = -EAGAIN;
        goto error_1;
    }

    // 初始化字符设备
    cdev_init(&ds18b20->ds18b20_cdev, &ds18b20_fops);
    ds18b20->ds18b20_cdev.owner = THIS_MODULE;
    cdev_add(&ds18b20->ds18b20_cdev, ds18b20->dev_num, 1);

    // 创建设备类
    ds18b20->ds18b20_class = class_create(THIS_MODULE, "sensors");
    if (IS_ERR(ds18b20->ds18b20_class))
    {
        printk("class_create error\n");
        ret = PTR_ERR(ds18b20->ds18b20_class);
        goto error_2;
    }

    // 创建设备
    ds18b20->ds18b20_device = device_create(ds18b20->ds18b20_class, NULL, ds18b20->dev_num, NULL, "ds18b20");
    if (IS_ERR(ds18b20->ds18b20_device))
    {
        printk("device_create error\n");
        ret = PTR_ERR(ds18b20->ds18b20_device);
        goto error_3;
    }

    // 获取GPIO描述符
    ds18b20->ds18b20_gpio = gpiod_get_optional(&dev->dev, "ds18b20", 0);
    if (ds18b20->ds18b20_gpio == NULL)
    {
        ret = -EBUSY;
        goto error_4;
    }

    // 设置GPIO方向为输出
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);

    return 0;

    error_4:
    device_destroy(ds18b20->ds18b20_class, ds18b20->dev_num);

    error_3:
    class_destroy(ds18b20->ds18b20_class);

    error_2:
    cdev_del(&ds18b20->ds18b20_cdev);
    unregister_chrdev_region(ds18b20->dev_num, 1);

    error_1:
    kfree(ds18b20);

    error_0:
    return ret;
}

const struct of_device_id ds18b20_match_table[] = {
    {.compatible = "ds18b20"},
    {},
};

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

static int __init ds18b20_init(void)
{
    int ret;

    // 注册平台驱动
    ret = platform_driver_register(&ds18b20_driver);
    if (ret < 0)
    {
        printk("platform_driver_register error\n");
        return -1;
    }

    ds18b20_reset(); // 调用复位函数
    ds18b20_writebyte(0xcc);//写入0Xcc字符
    return 0;
}

static void __exit ds18b20_exit(void)
{
    // 释放资源
    gpiod_put(ds18b20->ds18b20_gpio);
    device_destroy(ds18b20->ds18b20_class, ds18b20->dev_num);
    class_destroy(ds18b20->ds18b20_class);
    cdev_del(&ds18b20->ds18b20_cdev);
    unregister_chrdev_region(ds18b20->dev_num, 1);
    kfree(ds18b20);
    platform_driver_unregister(&ds18b20_driver);
}

module_init(ds18b20_init);
module_exit(ds18b20_exit);
MODULE_LICENSE("GPL");

161.3 运行测试

161.3.1 编译驱动程序

在上一小节中的ds18b20.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成ds18b20.ko 目标文件,至此驱动模块就编译成功了。

161.3.2 运行测试

首先启动开发板,进入系统之后如下所示:

image-20240925145612796

然后将上一小节编译完成的ds18b20.ko 驱动文件拷贝到开发板上,拷贝完成如下所示:

image-20240925145626868

然后根据前面章节内容连接好逻辑分析仪,并设置好相应的软件,然后使用以下命令进行驱动的加载,如下图所示:

insmod ds18b20.ko

image-20240925145640870

然后来查看逻辑分析仪采集到的波形,第一段波形为复位信号,具体如下所示:

image-20240925145651807

接下来看第二段波形,该波形表示发送的0xcc,具体内容如下所示:

image-20240925145700754

可以根据第一小节的时序图进行对比,发现上图通过逻辑分析仪采集到的波形图是正确的。

第162 章DS18B20 驱动读时序编写

在上个章节中完善了DS18B20 驱动的写时序,在本小节中将完善DS18B20 的读时序。

162.1 读时序分析

DS18B20 数据手册中关于读取的时序图如下所示:

image-20240926150821883

关于DS18B20 有读0 和读1 两种时序,他们的时序是不同的,接下来首先对前半部分读0进行分析。

  • 步骤1:主机拉低总线,从高电平变成低电平,且有时间限制,要确保拉低的时间最少为1 微秒
  • 步骤2:主机释放总线,从机拉低总线,15us 以内主机完成采样工作,如果这时候从机仍旧处于拉低总线的状态,则采集到的就是0.
  • 步骤3:拉高总线,恢复总线的高电平状态,且要求读操作必须大于60 毫秒。

总结出的读取0 操作代码如下所示:

unsigned char ds18b20_readbit(void) {
    unsigned char bit;
    
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);// 将GPIO 方向设置为输出
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);// 将GPIO 输出设置为低电平
    udelay(2);// 延时2 微秒
    
    gpiod_direction_input(ds18b20->ds18b20_gpio);// 将GPIO 方向设置为输入
    udelay(10);// 延时10 微秒
    
    bit = gpiod_get_value(ds18b20->ds18b20_gpio);// 读取GPIO 的值作为位(bit)
    udelay(60);// 延时60 微秒
    
    return bit;
}

然后来对读1 的步骤进行分析:

  • 步骤1:主机拉低总线,从高电平变成低电平,且有时间限制,要确保拉低的时间最少为1 微秒
  • 步骤2:通过电阻进行上拉,15us 以内主机完成采样工作,如果这时候处于电阻上拉的状态,则采集到的就是1

总结出的读1 操作代码如下所示:

unsigned char ds18b20_readbit(void) {
    unsigned char bit;
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);// 将GPIO 方向设置为输出
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);// 将GPIO 输出设置为低电平
    udelay(2);// 延时2 微秒
    
    gpiod_direction_input(ds18b20->ds18b20_gpio);// 将GPIO 方向设置为输入
    bit = gpiod_get_value(ds18b20->ds18b20_gpio);// 读取GPIO 的值作为位(bit)
    
    return bit;
}

综合上面读1 和读0 操作的代码以及时序图,可以将两个代码进行整合在一起,整合之后的代码如下所示,大家可以神奇的发现该函数和读0 中的函数是相同的。

unsigned char ds18b20_readbit(void) {
    unsigned char bit;
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);// 将GPIO 方向设置为输出
    
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);// 将GPIO 输出设置为低电平
    udelay(2);// 延时2 微秒
    
    gpiod_direction_input(ds18b20->ds18b20_gpio);// 将GPIO 方向设置为输入
    udelay(10);// 延时10 微秒
    
    bit = gpiod_get_value(ds18b20->ds18b20_gpio);// 读取GPIO 的值作为位(bit)
    udelay(60);// 延时60 微秒
    
    return bit;
}

但这样修改之后的代码仅仅只能读取一个字符,如果要读取8 位字符就需要连续使用8次该函数,而为了更方便,可以重新添加一个函数,从而直接读取一个字节的数据,具体内容如下所示:

/**
* 从DS18B20 读取单个位(bit)
* @return 读取到的位(bit),0 或1
*/
unsigned char ds18b20_readbit(void) {
    unsigned char bit;
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);// 将GPIO 方向设置为输出
    
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);// 将GPIO 输出设置为低电平
    udelay(2);// 延时2 微秒
    
    gpiod_direction_input(ds18b20->ds18b20_gpio);// 将GPIO 方向设置为输入
    udelay(10);// 延时10 微秒
    
    bit = gpiod_get_value(ds18b20->ds18b20_gpio);// 读取GPIO 的值作为位(bit)
    udelay(60);// 延时60 微秒
    
    return bit;
}

/**
* 从DS18B20 读取一个字节(byte)数据
* @return 读取到的字节数据
*/
int ds18b20_readbyte(void) {
    int data = 0;
    int i;
    for (i = 0; i < 8; i++) {
        // 读取单个位(bit)并根据位的位置进行左移操作
        data |= ds18b20_readbit() << i;
    }
    return data;
}

至此,关于DS18B20 的读操作相关函数就编写完成了,会在下个小节编写填加写时序相关函数的驱动。

162.2 DS18b20 驱动读时序编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\97_ds18b20_04\
编写完成的ds18b20.c 代码如下所示:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/gpio/consumer.h> // 添加此头文件
#include <linux/delay.h>

struct ds18b20_data
{
    dev_t dev_num;
    struct cdev ds18b20_cdev;
    struct class *ds18b20_class;
    struct device *ds18b20_device;
    struct gpio_desc *ds18b20_gpio;
};

struct ds18b20_data *ds18b20;

void ds18b20_reset(void)
{
    // 设置 GPIO 方向为输出,输出低电平
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);
    udelay(700); // 延迟 700 微秒

    // 设置 GPIO 输出高电平,并将 GPIO 方向设置为输入
    gpiod_set_value(ds18b20->ds18b20_gpio, 1);
    gpiod_direction_input(ds18b20->ds18b20_gpio);

    // 等待直到 GPIO 输入为低电平
    while (gpiod_get_value(ds18b20->ds18b20_gpio))
        ;

    // 等待直到 GPIO 输入为高电平
    while (!gpiod_get_value(ds18b20->ds18b20_gpio))
        ;
    udelay(480); // 延迟 480 微秒
}

/**
 * 向 DS18B20 写入单个位(bit)
 * @param bit 要写入的位(bit),0 或 1
 */
void ds18b20_writebit(unsigned char bit) {
    // 将 GPIO 方向设置为输出
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);

    // 将 GPIO 输出设置为指定的位(bit)
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);

    // 若 bit 为 1,则延时 10 微秒
    if (bit)
    {
         udelay(10);
         // 将 GPIO 方向设置为输出
    	gpiod_direction_output(ds18b20->ds18b20_gpio, 1);
    }
        
    // 延时 65 微秒
    udelay(65);

    // 将 GPIO 方向设置为输出
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);

    // 延时 2 微秒
    udelay(2);
}

/**
 * 向 DS18B20 写入一个字节(byte)数据
 * @param data 要写入的字节数据
 */
void ds18b20_writebyte(int data) {
    int i;

    for (i = 0; i < 8; i++) {
        // 逐位写入数据
        ds18b20_writebit(data & 0x01);
        data = data >> 1;
    }
}

/**
 * 从 DS18B20 读取单个位(bit)
 * @return 读取到的位(bit),0 或 1
 */
unsigned char ds18b20_readbit(void) {
    unsigned char bit;        
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);// 将 GPIO 方向设置为输出        
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);// 将 GPIO 输出设置为低电平        
    udelay(2);// 延时 2 微秒        
    
    gpiod_direction_input(ds18b20->ds18b20_gpio);// 将 GPIO 方向设置为输入   释放总线
    udelay(10);// 延时 10 微秒       
    
    bit = gpiod_get_value(ds18b20->ds18b20_gpio);// 读取 GPIO 的值作为位(bit)       
    udelay(60);// 延时 60 微秒
    
    return bit;
}

/**
 * 从 DS18B20 读取一个字节(byte)数据
 * @return 读取到的字节数据
 */
int ds18b20_readbyte(void) {
    int data = 0;
    int i;
    
    for (i = 0; i < 8; i++) {
        // 读取单个位(bit)并根据位的位置进行左移操作
        data |= ds18b20_readbit() << i;
    }
    
    return data;
}

int ds18b20_open(struct inode *inode, struct file *file)
{
    return 0;
}

ssize_t ds18b20_read(struct file *file, char __user *buf, size_t size, loff_t *offs)
{
    return 0;
}

int ds18b20_release(struct inode *inode, struct file *file)
{
    return 0;
}

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

int ds18b20_probe(struct platform_device *dev)
{
    int ret;
    printk("This is probe \n");

    // 分配内存给ds18b20_data结构体
    ds18b20 = kzalloc(sizeof(*ds18b20), GFP_KERNEL);
    if (ds18b20 == NULL)
    {
        printk("kzalloc error\n");
        ret = -ENOMEM;
        goto error_0;
    }

    // 分配字符设备号
    ret = alloc_chrdev_region(&ds18b20->dev_num, 0, 1, "myds18b20");
    if (ret < 0)
    {
        printk("alloc_chrdev_region error\n");
        ret = -EAGAIN;
        goto error_1;
    }

    // 初始化字符设备
    cdev_init(&ds18b20->ds18b20_cdev, &ds18b20_fops);
    ds18b20->ds18b20_cdev.owner = THIS_MODULE;
    cdev_add(&ds18b20->ds18b20_cdev, ds18b20->dev_num, 1);

    // 创建设备类
    ds18b20->ds18b20_class = class_create(THIS_MODULE, "sensors");
    if (IS_ERR(ds18b20->ds18b20_class))
    {
        printk("class_create error\n");
        ret = PTR_ERR(ds18b20->ds18b20_class);
        goto error_2;
    }

    // 创建设备
    ds18b20->ds18b20_device = device_create(ds18b20->ds18b20_class, NULL, ds18b20->dev_num, NULL, "ds18b20");
    if (IS_ERR(ds18b20->ds18b20_device))
    {
        printk("device_create error\n");
        ret = PTR_ERR(ds18b20->ds18b20_device);
        goto error_3;
    }

    // 获取GPIO描述符
    ds18b20->ds18b20_gpio = gpiod_get_optional(&dev->dev, "ds18b20", 0);
    if (ds18b20->ds18b20_gpio == NULL)
    {
        ret = -EBUSY;
        goto error_4;
    }

    // 设置GPIO方向为输出
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);

    return 0;

    error_4:
    device_destroy(ds18b20->ds18b20_class, ds18b20->dev_num);

    error_3:
    class_destroy(ds18b20->ds18b20_class);

    error_2:
    cdev_del(&ds18b20->ds18b20_cdev);
    unregister_chrdev_region(ds18b20->dev_num, 1);

    error_1:
    kfree(ds18b20);

    error_0:
    return ret;
}

const struct of_device_id ds18b20_match_table[] = {
    {.compatible = "ds18b20"},
    {},
};

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

static int __init ds18b20_init(void)
{
    int ret;

    // 注册平台驱动
    ret = platform_driver_register(&ds18b20_driver);
    if (ret < 0)
    {
        printk("platform_driver_register error\n");
        return -1;
    }

    return 0;
}

static void __exit ds18b20_exit(void)
{
    // 释放资源
    gpiod_put(ds18b20->ds18b20_gpio);
    device_destroy(ds18b20->ds18b20_class, ds18b20->dev_num);
    class_destroy(ds18b20->ds18b20_class);
    cdev_del(&ds18b20->ds18b20_cdev);
    unregister_chrdev_region(ds18b20->dev_num, 1);
    kfree(ds18b20);
    platform_driver_unregister(&ds18b20_driver);
}

module_init(ds18b20_init);
module_exit(ds18b20_exit);
MODULE_LICENSE("GPL");

由于读时序的实验需要后续的知识作为支撑,所以会在下个小节的驱动中完善温度读取相关的函数之后,再进行测试。

第163 章DS18B20 驱动温度读取编写

在上个章节中讲解了DS18B20 的读操作相关的时序以及函数代码编写,但是并没有编写实验函数进行验证,那是因为读操作必然要设计到一些其他的知识,导致知识过于混乱,影响理解和记忆,而本章节将会编写DS18B20 温度读取相关的代码及相关的测试应用,最后对
DS18B20 完整的时序逻辑进行讲解。

163.1 DS18B20 驱动读温度逻辑分析

在介绍单总线的时候提到过,单总线的通信步骤都有着严格的步骤,具体步骤如下所示:

  • (1)初始化
  • (2)ROM 操作命令
  • (3)功能命令

初始化涉及到的函数就是我们在前面章节中所编写的ds18b20_reset 函数,而由于在实验中只使用了一个DS18B20 所以只需要写入0xCC,忽略序列号即可。

image-20240926163915218

而第三步中的功能命令我们需要获取到当前的温度,所以需要写入0x44 进行温度转换,如下图所示:

image-20240926164002026

温度的转换是需要进行一定的时间的,并且不同分辨率下转换时间也是不同的,具体的转换时间和分辨率关系如下所示:

image-20240926164018357

而默认情况下分辨率为12 位,所以需要在程序中延时750 毫秒,至此温度转换的操作就完成了,编写完成的代码如下所示:

int ds18b20_readtemp(void) {
    ds18b20_reset();// 复位DS18B20
    ds18b20_writebyte(0xCC);// 发送写入字节命令0xCC(跳过ROM)
    ds18b20_writebyte(0x44);// 发送写入字节命令0x44(启动温度转换)
    mdelay(750);// 延时750 微秒,等待温度转换完成
}

接下来需要对转换完成之后的温度进行读取,读取操作仍旧需要遵循单总线的三个操作步骤,其中第一步的初始化和ROM 操作命令与温度转换时相同,只有在第三步的功能命令会不同,这里是要进行温度的读取,所以需要发送RAM 指令中的第二条0xbe,如下所示:

image-20240926164056384

在内部存储中的Byte0 和Byte1 用来存储转换之后的温度低位和高位,如下图所示,所以需要使用上一章节中编写的读操作来读取转换之后的温度,具体代码如下所示:

image-20240926164114566

/**
* 从DS18B20 读取温度值
* @return 读取到的温度值
*/
int ds18b20_readtemp(void) {
    int temp_l, temp_h, temp;
    ds18b20_reset();// 复位DS18B20
    ds18b20_writebyte(0xCC);// 发送写入字节命令0xCC(跳过ROM)
    ds18b20_writebyte(0xBE);// 发送写入字节命令0xBE(读取温度值)
    temp_l = ds18b20_readbyte();// 读取温度低位字节
    temp_h = ds18b20_readbyte();// 读取温度高位字节
    temp_h = temp_h << 8;// 将温度高位字节左移8 位
    temp = temp_h | temp_l;// 组合温度值
    
    return temp;
}

可以将上面温度转换的步骤和温度读取的步骤整合在一起,形成一个完整的函数,具体函数内容如下所示:

/**
* 从DS18B20 读取温度值
* @return 读取到的温度值
*/
int ds18b20_readtemp(void) {
    int temp_l, temp_h, temp;
    ds18b20_reset();// 复位DS18B20
    ds18b20_writebyte(0xCC);// 发送写入字节命令0xCC(跳过ROM)
    ds18b20_writebyte(0x44);// 发送写入字节命令0x44(启动温度转换)
    mdelay(750);// 延时750 微秒,等待温度转换完成
    
    ds18b20_reset();// 复位DS18B20
    ds18b20_writebyte(0xCC);// 发送写入字节命令0xCC(跳过ROM)
    ds18b20_writebyte(0xBE);// 发送写入字节命令0xBE(读取温度值)
    
    temp_l = ds18b20_readbyte();// 读取温度低位字节
    temp_h = ds18b20_readbyte();// 读取温度高位字节
    
    temp_h = temp_h << 8;// 将温度高位字节左移8 位
    temp = temp_h | temp_l;// 组合温度值
    return temp;
}

至此,温度读取相关的驱动函数就编写完成了,会在下个小节中将温度读取的函数添加到完整的驱动中。

163.2 DS18B20 驱动读温度代码编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\98_ds18b20_05\01_module
相较于上一章节的驱动代码,添加了读取温度相关的函数,并在read 函数中通过copy_to_user 将获取到的温度传递到用户空间。编写完成的ds18b20.c 代码如下所示:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/gpio/consumer.h> // 添加此头文件
#include <linux/delay.h>
#include <linux/uaccess.h>

struct ds18b20_data
{
    dev_t dev_num;
    struct cdev ds18b20_cdev;
    struct class *ds18b20_class;
    struct device *ds18b20_device;
    struct gpio_desc *ds18b20_gpio;
};

struct ds18b20_data *ds18b20;

void ds18b20_reset(void)
{
    // 设置 GPIO 方向为输出,输出低电平
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);
    udelay(700); // 延迟 700 微秒

    // 设置 GPIO 输出高电平,并将 GPIO 方向设置为输入
    gpiod_set_value(ds18b20->ds18b20_gpio, 1);
    gpiod_direction_input(ds18b20->ds18b20_gpio);

    // 等待直到 GPIO 输入为低电平
    while (gpiod_get_value(ds18b20->ds18b20_gpio))
        ;

    // 等待直到 GPIO 输入为高电平
    while (!gpiod_get_value(ds18b20->ds18b20_gpio))
        ;
    udelay(480); // 延迟 480 微秒
}

/**
 * 向 DS18B20 写入单个位(bit)
 * @param bit 要写入的位(bit),0 或 1
 */
void ds18b20_writebit(unsigned char bit) {
    // 将 GPIO 方向设置为输出
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);

    // 将 GPIO 输出设置为指定的位(bit)
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);

    // 若 bit 为 1,则延时 10 微秒
    if (bit){
        udelay(10);  
        // 将 GPIO 方向设置为输出
    	gpiod_direction_output(ds18b20->ds18b20_gpio, 1);
    }
       

    // 延时 65 微秒
    udelay(65);

    // 将 GPIO 方向设置为输出
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);

    // 延时 2 微秒
    udelay(2);
}

/**
 * 向 DS18B20 写入一个字节(byte)数据
 * @param data 要写入的字节数据
 */
void ds18b20_writebyte(int data) {
    int i;

    for (i = 0; i < 8; i++) {
        // 逐位写入数据
        ds18b20_writebit(data & 0x01);
        data = data >> 1;
    }
}

/**
 * 从 DS18B20 读取单个位(bit)
 * @return 读取到的位(bit),0 或 1
 */
unsigned char ds18b20_readbit(void) {
    unsigned char bit;        
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);// 将 GPIO 方向设置为输出        
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);// 将 GPIO 输出设置为低电平        
    udelay(2);// 延时 2 微秒        
    gpiod_direction_input(ds18b20->ds18b20_gpio);// 将 GPIO 方向设置为输入   
    udelay(10);// 延时 10 微秒       
    bit = gpiod_get_value(ds18b20->ds18b20_gpio);// 读取 GPIO 的值作为位(bit)       
    udelay(60);// 延时 60 微秒
    
    return bit;
}

/**
 * 从 DS18B20 读取一个字节(byte)数据
 * @return 读取到的字节数据
 */
int ds18b20_readbyte(void) {
    int data = 0;
    int i;
    
    for (i = 0; i < 8; i++) {
        // 读取单个位(bit)并根据位的位置进行左移操作
        data |= ds18b20_readbit() << i;
    }
    
    return data;
}

/**
 * 从 DS18B20 读取温度值
 * @return 读取到的温度值
 */
int ds18b20_readtemp(void) {
    int temp_l, temp_h, temp;   
    
    ds18b20_reset();// 复位 DS18B20    
    ds18b20_writebyte(0xCC);// 发送写入字节命令 0xCC(跳过 ROM)   
    ds18b20_writebyte(0x44);// 发送写入字节命令 0x44(启动温度转换)    
    mdelay(750);// 延时 750 微秒,等待温度转换完成
    
    ds18b20_reset();// 复位 DS18B20        
    ds18b20_writebyte(0xCC);// 发送写入字节命令 0xCC(跳过 ROM)       
    ds18b20_writebyte(0xBE);// 发送写入字节命令 0xBE(读取温度值)        
    temp_l = ds18b20_readbyte();// 读取温度低位字节        
    temp_h = ds18b20_readbyte();// 读取温度高位字节        
    temp_h = temp_h << 8;// 将温度高位字节左移 8 位
    temp = temp_h | temp_l;// 组合温度值
    
    return temp;
} 

int ds18b20_open(struct inode *inode, struct file *file)
{
    return 0;
}

ssize_t ds18b20_read(struct file *file, char __user *buf, size_t size, loff_t *offs) {
    int ds18b20_temp;    
    ds18b20_temp = ds18b20_readtemp();// 从 DS18B20 读取温度值    
    // 将温度值复制到用户空间缓冲区
    if (copy_to_user(buf, &ds18b20_temp, sizeof(ds18b20_temp))) {
        return -1; // 复制失败,返回错误代码
    }
    
    return 0; // 成功读取并复制温度值
}

int ds18b20_release(struct inode *inode, struct file *file)
{
    return 0;
}

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

int ds18b20_probe(struct platform_device *dev)
{
    int ret;
    printk("This is probe \n");

    // 分配内存给ds18b20_data结构体
    ds18b20 = kzalloc(sizeof(*ds18b20), GFP_KERNEL);
    if (ds18b20 == NULL)
    {
        printk("kzalloc error\n");
        ret = -ENOMEM;
        goto error_0;
    }

    // 分配字符设备号
    ret = alloc_chrdev_region(&ds18b20->dev_num, 0, 1, "myds18b20");
    if (ret < 0)
    {
        printk("alloc_chrdev_region error\n");
        ret = -EAGAIN;
        goto error_1;
    }

    // 初始化字符设备
    cdev_init(&ds18b20->ds18b20_cdev, &ds18b20_fops);
    ds18b20->ds18b20_cdev.owner = THIS_MODULE;
    cdev_add(&ds18b20->ds18b20_cdev, ds18b20->dev_num, 1);

    // 创建设备类
    ds18b20->ds18b20_class = class_create(THIS_MODULE, "sensors");
    if (IS_ERR(ds18b20->ds18b20_class))
    {
        printk("class_create error\n");
        ret = PTR_ERR(ds18b20->ds18b20_class);
        goto error_2;
    }

    // 创建设备
    ds18b20->ds18b20_device = device_create(ds18b20->ds18b20_class, NULL, ds18b20->dev_num, NULL, "ds18b20");
    if (IS_ERR(ds18b20->ds18b20_device))
    {
        printk("device_create error\n");
        ret = PTR_ERR(ds18b20->ds18b20_device);
        goto error_3;
    }

    // 获取GPIO描述符
    ds18b20->ds18b20_gpio = gpiod_get_optional(&dev->dev, "ds18b20", 0);
    if (ds18b20->ds18b20_gpio == NULL)
    {
        ret = -EBUSY;
        goto error_4;
    }

    // 设置GPIO方向为输出
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);

    return 0;

    error_4:
    device_destroy(ds18b20->ds18b20_class, ds18b20->dev_num);

    error_3:
    class_destroy(ds18b20->ds18b20_class);

    error_2:
    cdev_del(&ds18b20->ds18b20_cdev);
    unregister_chrdev_region(ds18b20->dev_num, 1);

    error_1:
    kfree(ds18b20);

    error_0:
    return ret;
}

const struct of_device_id ds18b20_match_table[] = {
    {.compatible = "ds18b20"},
    {},
};

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

static int __init ds18b20_init(void)
{
    int ret;

    // 注册平台驱动
    ret = platform_driver_register(&ds18b20_driver);
    if (ret < 0)
    {
        printk("platform_driver_register error\n");
        return -1;
    }

    return 0;
}

static void __exit ds18b20_exit(void)
{
    // 释放资源
    gpiod_put(ds18b20->ds18b20_gpio);
    device_destroy(ds18b20->ds18b20_class, ds18b20->dev_num);
    class_destroy(ds18b20->ds18b20_class);
    cdev_del(&ds18b20->ds18b20_cdev);
    unregister_chrdev_region(ds18b20->dev_num, 1);
    kfree(ds18b20);
    platform_driver_unregister(&ds18b20_driver);
}

module_init(ds18b20_init);
module_exit(ds18b20_exit);
MODULE_LICENSE("GPL");

163.3 应用程序编写

上一小节编写完成的驱动程序会将采集到的温度值通过copy_to_user 函数传递到用户空间,而用户空间需要使用系统调用read 函数来读取传递过来的温度数据,需要注意的是这里传递过来的数据还是未经处理的二进制数据,要想得到实际可用的温度值还需要在应用程序中
进行后处理操作。

具体关于温度后处理的内容可以查看156.6.2 温度测量寄存器寄存器相关章节,编写好的测试程序存放位置为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\98_ds18b20_05\02_app

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

void ds18b20_get_temp(int value, char *sig, float *temp) {
    if ((value >> 11) & 0x01) {
        *sig = '-';
        value = ~value + 1;  // 取补码,表示负数
        value &= ~(0xF8 << 8);  // 清除高5位,保留低11位
    } else {
        *sig = '+';
    }
    *temp = value * 0.0625;  // 将温度值乘以0.0625,得到实际温度值
}

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

    fd = open("/dev/ds18b20", O_RDWR);  // 打开设备文件 /dev/ds18b20
    if (fd < 0) {
        printf("打开文件失败!\n");
        return -1;
    }

    while (1) {
        if (read(fd, &data, sizeof(data)) < 0) {  // 从设备文件中读取数据
            printf("读取数据失败!\n");
            return -1;
        }
        ds18b20_get_temp(data, &sig, &temp);  // 将读取的数据转换为温度值
        printf("温度为 %c%.4f\n", sig, temp);  // 打印温度值
    }

    close(fd);  // 关闭设备文件
    return 0;
}

163.4 运行测试

163.4.1 编译驱动程序

在上一小节中的de18b20.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成ds18b20.ko 目标文件,至此驱动模块就编译成功了。

163.4.2 编译应用程序

首先进行应用程序的编译, 因为测试APP 是要在开发板上运行的, 所以需要aarch64-linux-gnu-gcc 来编译,输入以下命令,编译完成以后会生成一个app 的可执行程序,
如下图所示:

aarch64-linux-gnu-gcc app.c -o app

image-20240926165119183

然后将编译完成的可执行程序拷贝到开发板上.

163.4.3 运行测试

首先启动开发板,进入系统之后如下所示:

image-20240926165137165

然后将上两个小节编译完成的ds18b20.ko 驱动和可执行程序app 文件拷贝到开发板上,拷贝完成如下所示:

image-20240926165150754

然后使用以下命令进行驱动的加载,如下图所示:

insmod ds18b20.ko

image-20240926165228393

然后运行可执行程序app,可以看到当前的温度就打印了出来,如下图所示:

image-20240926165238292

当我用手触摸DS18B20 传感器的时候,可以看到温度明显的上升了,接下来查看逻辑分析仪采集到的时序图。
首先采集到的波形图为复位时序图,接下来是0XCC 的ROM 指令,表示跳过ROM 指令,0x44 表示功能命令,进行温度转换。

image-20240926165253121

第二个采集到的波形复位和ROM 指令与第一个波形是相同的,所以这里直接来看后续的功能命令以及采集到的温度,如下所示:

image-20240926165317070

首先发送了0xbe 指令,表示读取暂存寄存器,后面的0xBB 和0x01 分别表示温度的低八位和高8 位,换算成10 进制为443,然后乘以0.0625 可以得到当前的温度为27.6875,至此关于逻辑分析仪采集到的波形就分析完成了。

第164 章DS18B20 驱动使用ioctl 设置分辨率

在上个章节中成功获取到了DS18B20 的温度,默认情况下DS18B20 的分辨率为12 位,可以通过修改配置寄存器从而改变分辨率的大小,总共支持9 位、10 位、11 位、12 位四种分辨率,在本章节中将完善驱动中的ioctl 部分,增加分辨率修改相关的代码,然后编写相应的应用程序进行分辨率修改的测试。

164.1 ioctl 设置分辨率驱动代码编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\99_ds18b20_06\01_module

相较于上一章节的驱动代码,添加了ioctl 设置分辨率相关的函数,除此之外完成了另外两个功能函数,分别为设置DS18B20 温度传感器的分辨率的set_resolution 函数,以及检查参数的有效性的check_args 函数,编写完成的ds18b20.c 代码如下所示:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/gpio/consumer.h> // 添加此头文件
#include <linux/delay.h>
#include <linux/uaccess.h>

#define SET_RESOLUTION _IOW('A', 0, int)
#define SET_RESOLUTION_9 9
#define SET_RESOLUTION_10 10
#define SET_RESOLUTION_11 11
#define SET_RESOLUTION_12 12

struct ds18b20_data
{
    dev_t dev_num;
    struct cdev ds18b20_cdev;
    struct class *ds18b20_class;
    struct device *ds18b20_device;
    struct gpio_desc *ds18b20_gpio;
};

struct ds18b20_data *ds18b20;

void ds18b20_reset(void)
{
    // 设置 GPIO 方向为输出,输出低电平
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);
    udelay(700); // 延迟 700 微秒

    // 设置 GPIO 输出高电平,并将 GPIO 方向设置为输入
    gpiod_set_value(ds18b20->ds18b20_gpio, 1);
    gpiod_direction_input(ds18b20->ds18b20_gpio);

    // 等待直到 GPIO 输入为低电平
    while (gpiod_get_value(ds18b20->ds18b20_gpio))
        ;

    // 等待直到 GPIO 输入为高电平
    while (!gpiod_get_value(ds18b20->ds18b20_gpio))
        ;
    udelay(480); // 延迟 480 微秒
}

/**
 * 向 DS18B20 写入单个位(bit)
 * @param bit 要写入的位(bit),0 或 1
 */
void ds18b20_writebit(unsigned char bit) {
    // 将 GPIO 方向设置为输出
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);

    // 将 GPIO 输出设置为指定的位(bit)
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);

    // 若 bit 为 1,则延时 10 微秒
    if (bit){
        udelay(10);  
        // 将 GPIO 方向设置为输出
    	gpiod_direction_output(ds18b20->ds18b20_gpio, 1);
    }
       

    // 延时 65 微秒
    udelay(65);

    // 将 GPIO 方向设置为输出
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);

    // 延时 2 微秒
    udelay(2);
}

/**
 * 向 DS18B20 写入一个字节(byte)数据
 * @param data 要写入的字节数据
 */
void ds18b20_writebyte(int data) {
    int i;

    for (i = 0; i < 8; i++) {
        // 逐位写入数据
        ds18b20_writebit(data & 0x01);
        data = data >> 1;
    }
}

/**
 * 从 DS18B20 读取单个位(bit)
 * @return 读取到的位(bit),0 或 1
 */
unsigned char ds18b20_readbit(void) {
    unsigned char bit;        
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);// 将 GPIO 方向设置为输出        
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);// 将 GPIO 输出设置为低电平        
    udelay(2);// 延时 2 微秒        
    gpiod_direction_input(ds18b20->ds18b20_gpio);// 将 GPIO 方向设置为输入   
    udelay(10);// 延时 10 微秒       
    bit = gpiod_get_value(ds18b20->ds18b20_gpio);// 读取 GPIO 的值作为位(bit)       
    udelay(60);// 延时 60 微秒
    
    return bit;
}

/**
 * 从 DS18B20 读取一个字节(byte)数据
 * @return 读取到的字节数据
 */
int ds18b20_readbyte(void) {
    int data = 0;
    int i;
    
    for (i = 0; i < 8; i++) {
        // 读取单个位(bit)并根据位的位置进行左移操作
        data |= ds18b20_readbit() << i;
    }
    
    return data;
}

/**
 * 从 DS18B20 读取温度值
 * @return 读取到的温度值
 */
int ds18b20_readtemp(void) {
    int temp_l, temp_h, temp;   
    
    ds18b20_reset();// 复位 DS18B20    
    ds18b20_writebyte(0xCC);// 发送写入字节命令 0xCC(跳过 ROM)   
    ds18b20_writebyte(0x44);// 发送写入字节命令 0x44(启动温度转换)    
    mdelay(750);// 延时 750 微秒,等待温度转换完成
    
    ds18b20_reset();// 复位 DS18B20        
    ds18b20_writebyte(0xCC);// 发送写入字节命令 0xCC(跳过 ROM)       
    ds18b20_writebyte(0xBE);// 发送写入字节命令 0xBE(读取温度值)        
    temp_l = ds18b20_readbyte();// 读取温度低位字节        
    temp_h = ds18b20_readbyte();// 读取温度高位字节        
    temp_h = temp_h << 8;// 将温度高位字节左移 8 位
    temp = temp_h | temp_l;// 组合温度值
    
    return temp;
} 

/**
 * 设置 DS18B20 温度传感器的分辨率
 * @param args 分辨率参数
 */
void set_resolution(int args) {
    ds18b20_reset();  // 复位 DS18B20 温度传感器
    ds18b20_writebyte(0xCC);  // 发送跳过 ROM 命令
    ds18b20_writebyte(0x4E);  // 发送写配置寄存器命令
    ds18b20_writebyte(60);  // 发送配置字节 1,设置温度上限阈值
    ds18b20_writebyte(10);  // 发送配置字节 2,设置温度下限阈值

    switch (args) {
        case SET_RESOLUTION_9:  // 设置分辨率为 9 位
            ds18b20_writebyte(0x1F);  // 发送配置字节 3,设置分辨率为 9 位
            break;
        case SET_RESOLUTION_10:  // 设置分辨率为 10 位
            ds18b20_writebyte(0x3F);  // 发送配置字节 3,设置分辨率为 10 位
            break;
        case SET_RESOLUTION_11:  // 设置分辨率为 11 位
            ds18b20_writebyte(0x5F);  // 发送配置字节 3,设置分辨率为 11 位
            break;
        case SET_RESOLUTION_12:  // 设置分辨率为 12 位
            ds18b20_writebyte(0x7F);  // 发送配置字节 3,设置分辨率为 12 位
            break;
        default:
            break;
    }
}

/**
 * 检查参数的有效性
 * @param args 参数
 * @return 返回执行结果,成功返回 0,失败返回 -1
 */
int check_args(int args) {
    int ret = -1;  // 返回值,默认为失败

    ds18b20_reset();  // 复位传感器
    ds18b20_writebyte(0xCC);  // 发送指令字节 0xCC
    ds18b20_writebyte(0xBE);  // 发送指令字节 0xBE
    ds18b20_readbyte();  // 读取一个字节
    ds18b20_readbyte();  // 读取一个字节
    ds18b20_readbyte();  // 读取一个字节
    ds18b20_readbyte();  // 读取一个字节

    switch (args) {
        case SET_RESOLUTION_9:
            if (ds18b20_readbyte() == 0x1F) {  // 读取一个字节并与 0x1F 进行比较
                ret = 0;  // 设置返回值为成功
            }
            break;
        case SET_RESOLUTION_10:
            if (ds18b20_readbyte() == 0x3F) {  // 读取一个字节并与 0x3F 进行比较
                ret = 0;  // 设置返回值为成功
            }
            break;
        case SET_RESOLUTION_11:
            if (ds18b20_readbyte() == 0x5F) {  // 读取一个字节并与 0x5F 进行比较
                ret = 0;  // 设置返回值为成功
            }
            break;
        case SET_RESOLUTION_12:
            if (ds18b20_readbyte() == 0x7F) {  // 读取一个字节并与 0x7F 进行比较
                ret = 0;  // 设置返回值为成功
            }
            break;
        default:
            break;
    }

    return ret;  // 返回结果
}

int ds18b20_open(struct inode *inode, struct file *file)
{
    return 0;
}

ssize_t ds18b20_read(struct file *file, char __user *buf, size_t size, loff_t *offs) {
    int ds18b20_temp;    
    ds18b20_temp = ds18b20_readtemp();// 从 DS18B20 读取温度值    
    // 将温度值复制到用户空间缓冲区
    if (copy_to_user(buf, &ds18b20_temp, sizeof(ds18b20_temp))) {
        return -1; // 复制失败,返回错误代码
    }
    
    return 0; // 成功读取并复制温度值
}

int ds18b20_release(struct inode *inode, struct file *file)
{
    return 0;
}

/**
 * DS18B20 温度传感器的 ioctl 函数
 * @param file 文件指针
 * @param cmd 命令
 * @param args 参数
 * @return 返回执行结果,成功返回 0,失败返回 -1
 */
long ds18b20_ioctl(struct file *file, unsigned int cmd, unsigned long args) {
    printk("this is ioctl\n");
    if (cmd == SET_RESOLUTION) {  // 判断命令是否为设置分辨率
        if (args >= SET_RESOLUTION_9 && args <= SET_RESOLUTION_12) {  // 判断参数是否在有效的分辨率范围内
            set_resolution(args);  // 调用设置分辨率
	    printk("set_resolution ok \n");
            return 0;  // 返回成功
        }
    }
    // 如果不匹配 SET_RESOLUTION 或者 args 不在有效范围内,不执行任何操作
    return -1;  // 返回失败
}

struct file_operations ds18b20_fops = {
    .open = ds18b20_open,
    .read = ds18b20_read,
    .release = ds18b20_release,
    .unlocked_ioctl = ds18b20_ioctl,
    .owner = THIS_MODULE,
};

int ds18b20_probe(struct platform_device *dev)
{
    int ret;
    printk("This is probe \n");

    // 分配内存给ds18b20_data结构体
    ds18b20 = kzalloc(sizeof(*ds18b20), GFP_KERNEL);
    if (ds18b20 == NULL)
    {
        printk("kzalloc error\n");
        ret = -ENOMEM;
        goto error_0;
    }

    // 分配字符设备号
    ret = alloc_chrdev_region(&ds18b20->dev_num, 0, 1, "myds18b20");
    if (ret < 0)
    {
        printk("alloc_chrdev_region error\n");
        ret = -EAGAIN;
        goto error_1;
    }

    // 初始化字符设备
    cdev_init(&ds18b20->ds18b20_cdev, &ds18b20_fops);
    ds18b20->ds18b20_cdev.owner = THIS_MODULE;
    cdev_add(&ds18b20->ds18b20_cdev, ds18b20->dev_num, 1);

    // 创建设备类
    ds18b20->ds18b20_class = class_create(THIS_MODULE, "sensors");
    if (IS_ERR(ds18b20->ds18b20_class))
    {
        printk("class_create error\n");
        ret = PTR_ERR(ds18b20->ds18b20_class);
        goto error_2;
    }

    // 创建设备
    ds18b20->ds18b20_device = device_create(ds18b20->ds18b20_class, NULL, ds18b20->dev_num, NULL, "ds18b20");
    if (IS_ERR(ds18b20->ds18b20_device))
    {
        printk("device_create error\n");
        ret = PTR_ERR(ds18b20->ds18b20_device);
        goto error_3;
    }

    // 获取GPIO描述符
    ds18b20->ds18b20_gpio = gpiod_get_optional(&dev->dev, "ds18b20", 0);
    if (ds18b20->ds18b20_gpio == NULL)
    {
        ret = -EBUSY;
        goto error_4;
    }

    // 设置GPIO方向为输出
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);

    return 0;

    error_4:
    device_destroy(ds18b20->ds18b20_class, ds18b20->dev_num);

    error_3:
    class_destroy(ds18b20->ds18b20_class);

    error_2:
    cdev_del(&ds18b20->ds18b20_cdev);
    unregister_chrdev_region(ds18b20->dev_num, 1);

    error_1:
    kfree(ds18b20);

    error_0:
    return ret;
}

const struct of_device_id ds18b20_match_table[] = {
    {.compatible = "ds18b20"},
    {},
};

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

static int __init ds18b20_init(void)
{
    int ret;

    // 注册平台驱动
    ret = platform_driver_register(&ds18b20_driver);
    if (ret < 0)
    {
        printk("platform_driver_register error\n");
        return -1;
    }

    return 0;
}

static void __exit ds18b20_exit(void)
{
    // 释放资源
    gpiod_put(ds18b20->ds18b20_gpio);
    device_destroy(ds18b20->ds18b20_class, ds18b20->dev_num);
    class_destroy(ds18b20->ds18b20_class);
    cdev_del(&ds18b20->ds18b20_cdev);
    unregister_chrdev_region(ds18b20->dev_num, 1);
    kfree(ds18b20);
    platform_driver_unregister(&ds18b20_driver);
}

module_init(ds18b20_init);
module_exit(ds18b20_exit);
MODULE_LICENSE("GPL");

164.2 应用程序编写

上一小节编写了ioctl 设置温度采集分辨率的程序,而ioctl 需要跟用户空间的应用程序相配合才能进行使用,所以在本小节将编写测试用的应用程序,编写好的测试程序存放位置为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\99_ds18b20_06\02_app.

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <stdlib.h>

#define SET_RESOLUTION _IOW('A', 0, int)

/**
 * 根据传感器读取的原始数据计算温度值
 * @param value 传感器读取的原始数据
 */
void ds18b20_get_temp(int value) {
    char sig;   // 温度正负号
    float temp; // 温度值

    // 判断温度正负号
    if ((value >> 11) & 0x01) {
        sig = '-';
        value = ~value + 1;
        value &= ~(0xf8 << 8);
    } else {
        sig = '+';
    }

    // 计算温度值
    temp = value * 0.0625;

    // 打印温度信息
    printf("温度为 %c%.4f\n", sig, temp);
}

int main(int argc, char *argv[]) {
    int fd;    // 文件描述符
    int data;  // 读取的数据
    int args;  // 参数值

    // 打开设备文件
    fd = open("/dev/ds18b20", O_RDWR);
    if (fd < 0) {
        printf("打开设备文件出错\n");
        return -1;
    }

    // 获取命令行参数
    args = atoi(argv[1]);
    printf("参数值为 %d\n", args);

    // 检查参数范围
    if (args < 9 || args > 12) {
        printf("错误!参数范围应为 9 - 12\n");
        return -1;
    }

    // 设置分辨率
    ioctl(fd, SET_RESOLUTION, args);

    while (1) {
        // 读取数据
        read(fd, &data, sizeof(data));

        // 处理并打印温度信息
        ds18b20_get_temp(data);
    }

    return 0;
}

164.3 运行测试

164.3.1 编译驱动程序

在上一小节中的de18b20.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成ds18b20.ko 目标文件,至此驱动模块就编译成功了。

164.3.2 编译应用程序

首先进行应用程序的编译, 因为测试APP 是要在开发板上运行的, 所以需要aarch64-linux-gnu-gcc 来编译,输入以下命令,编译完成以后会生成一个app 的可执行程序,
如下图所示:

aarch64-linux-gnu-gcc app.c -o app

image-20240926170321378

然后将编译完成的可执行程序拷贝到开发板上。

164.3.3 运行测试

首先启动开发板,进入系统之后如下所示:

image-20240926170343636

然后将上两个小节编译完成的ds18b20.ko 驱动和可执行程序app 文件拷贝到开发板上,拷贝完成如下所示:

image-20240926172012372

然后使用以下命令进行驱动的加载,如下图所示:

insmod ds18b20.ko

image-20240926172513931

然后使用以下命令运行可执行程序app,设置分辨率为9 位,然后打印采集到的温度,如下图所示:

./app 9

image-20240926172528857

当我用手触摸DS18B20 传感器的时候,可以看到温度明显的上升了,并且温度上升的分辨率位9 位,也可以设置其他例如10、11 位的分辨率,至此,关于通过ioctl 设置ds18b20 温度采集分辨率的实验就完成了。

第165 章DS18B20 驱动使用ioctl 读取分辨率

在上个章节中成功在驱动中添加了ioctl 相关代码,通过ioctl 对DS18B20 采集到的温度分辨率进行设置。而在本章节将继续完善驱动程序,对DS18B20 采集到的温度分辨率进行读取,然后编写相应的应用程序进行分辨率读取测试。

165.1 ioctl 读取分辨率驱动代码编写

本实验对应的网盘路径为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\100_ds18b20_07\01_module。

相较于上一章节的驱动代码,添加了ioctl 查看分辨率相关的函数,除此之外完成了查看温度分辨率函数read_resolution(),编写完成的ds18b20.c 代码如下所示:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/slab.h>
#include <linux/gpio.h>
#include <linux/gpio/consumer.h> // 添加此头文件
#include <linux/delay.h>
#include <linux/uaccess.h>

#define SET_RESOLUTION _IOW('A', 0, int)
#define READ_RESOLUTION _IOR('A', 1, int)

#define SET_RESOLUTION_9 9
#define SET_RESOLUTION_10 10
#define SET_RESOLUTION_11 11
#define SET_RESOLUTION_12 12

struct ds18b20_data
{
    dev_t dev_num;
    struct cdev ds18b20_cdev;
    struct class *ds18b20_class;
    struct device *ds18b20_device;
    struct gpio_desc *ds18b20_gpio;
};

struct ds18b20_data *ds18b20;

void ds18b20_reset(void)
{
    // 设置 GPIO 方向为输出,输出低电平
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);
    udelay(700); // 延迟 700 微秒

    // 设置 GPIO 输出高电平,并将 GPIO 方向设置为输入
    gpiod_set_value(ds18b20->ds18b20_gpio, 1);
    gpiod_direction_input(ds18b20->ds18b20_gpio);

    // 等待直到 GPIO 输入为低电平
    while (gpiod_get_value(ds18b20->ds18b20_gpio))
        ;

    // 等待直到 GPIO 输入为高电平
    while (!gpiod_get_value(ds18b20->ds18b20_gpio))
        ;
    udelay(480); // 延迟 480 微秒
}

/**
 * 向 DS18B20 写入单个位(bit)
 * @param bit 要写入的位(bit),0 或 1
 */
void ds18b20_writebit(unsigned char bit) {
    // 将 GPIO 方向设置为输出
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);

    // 将 GPIO 输出设置为指定的位(bit)
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);

    // 若 bit 为 1,则延时 10 微秒
    if (bit){
        udelay(10);  
        // 将 GPIO 方向设置为输出
    	gpiod_direction_output(ds18b20->ds18b20_gpio, 1);
    }
       

    // 延时 65 微秒
    udelay(65);

    // 将 GPIO 方向设置为输出
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);

    // 延时 2 微秒
    udelay(2);
}

/**
 * 向 DS18B20 写入一个字节(byte)数据
 * @param data 要写入的字节数据
 */
void ds18b20_writebyte(int data) {
    int i;

    for (i = 0; i < 8; i++) {
        // 逐位写入数据
        ds18b20_writebit(data & 0x01);
        data = data >> 1;
    }
}

/**
 * 从 DS18B20 读取单个位(bit)
 * @return 读取到的位(bit),0 或 1
 */
unsigned char ds18b20_readbit(void) {
    unsigned char bit;        
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);// 将 GPIO 方向设置为输出        
    gpiod_set_value(ds18b20->ds18b20_gpio, 0);// 将 GPIO 输出设置为低电平        
    udelay(2);// 延时 2 微秒        
    gpiod_direction_input(ds18b20->ds18b20_gpio);// 将 GPIO 方向设置为输入   
    udelay(10);// 延时 10 微秒       
    bit = gpiod_get_value(ds18b20->ds18b20_gpio);// 读取 GPIO 的值作为位(bit)       
    udelay(60);// 延时 60 微秒
    
    return bit;
}

/**
 * 从 DS18B20 读取一个字节(byte)数据
 * @return 读取到的字节数据
 */
int ds18b20_readbyte(void) {
    int data = 0;
    int i;
    
    for (i = 0; i < 8; i++) {
        // 读取单个位(bit)并根据位的位置进行左移操作
        data |= ds18b20_readbit() << i;
    }
    
    return data;
}

/**
 * 从 DS18B20 读取温度值
 * @return 读取到的温度值
 */
int ds18b20_readtemp(void) {
    int temp_l, temp_h, temp;   
    
    ds18b20_reset();// 复位 DS18B20    
    ds18b20_writebyte(0xCC);// 发送写入字节命令 0xCC(跳过 ROM)   
    ds18b20_writebyte(0x44);// 发送写入字节命令 0x44(启动温度转换)    
    mdelay(750);// 延时 750 微秒,等待温度转换完成
    
    ds18b20_reset();// 复位 DS18B20        
    ds18b20_writebyte(0xCC);// 发送写入字节命令 0xCC(跳过 ROM)       
    ds18b20_writebyte(0xBE);// 发送写入字节命令 0xBE(读取温度值)        
    temp_l = ds18b20_readbyte();// 读取温度低位字节        
    temp_h = ds18b20_readbyte();// 读取温度高位字节        
    temp_h = temp_h << 8;// 将温度高位字节左移 8 位
    temp = temp_h | temp_l;// 组合温度值
    
    return temp;
} 

/**
 * 设置 DS18B20 温度传感器的分辨率
 * @param args 分辨率参数
 */
void set_resolution(int args) {
    ds18b20_reset();  // 复位 DS18B20 温度传感器
    ds18b20_writebyte(0xCC);  // 发送跳过 ROM 命令
    ds18b20_writebyte(0x4E);  // 发送写配置寄存器命令
    ds18b20_writebyte(60);  // 发送配置字节 1,设置温度上限阈值
    ds18b20_writebyte(10);  // 发送配置字节 2,设置温度下限阈值

    switch (args) {
        case SET_RESOLUTION_9:  // 设置分辨率为 9 位
            ds18b20_writebyte(0x1F);  // 发送配置字节 3,设置分辨率为 9 位
            break;
        case SET_RESOLUTION_10:  // 设置分辨率为 10 位
            ds18b20_writebyte(0x3F);  // 发送配置字节 3,设置分辨率为 10 位
            break;
        case SET_RESOLUTION_11:  // 设置分辨率为 11 位
            ds18b20_writebyte(0x5F);  // 发送配置字节 3,设置分辨率为 11 位
            break;
        case SET_RESOLUTION_12:  // 设置分辨率为 12 位
            ds18b20_writebyte(0x7F);  // 发送配置字节 3,设置分辨率为 12 位
            break;
        default:
            break;
    }
}

/**
 * 检查参数的有效性
 * @param args 参数
 * @return 返回执行结果,成功返回 0,失败返回 -1
 */
int check_args(int args) {
    int ret = -1;  // 返回值,默认为失败

    ds18b20_reset();  // 复位传感器
    ds18b20_writebyte(0xCC);  // 发送指令字节 0xCC
    ds18b20_writebyte(0xBE);  // 发送指令字节 0xBE
    ds18b20_readbyte();  // 读取一个字节
    ds18b20_readbyte();  // 读取一个字节
    ds18b20_readbyte();  // 读取一个字节
    ds18b20_readbyte();  // 读取一个字节

    switch (args) {
        case SET_RESOLUTION_9:
            if (ds18b20_readbyte() == 0x1F) {  // 读取一个字节并与 0x1F 进行比较
                ret = 0;  // 设置返回值为成功
            }
            break;
        case SET_RESOLUTION_10:
            if (ds18b20_readbyte() == 0x3F) {  // 读取一个字节并与 0x3F 进行比较
                ret = 0;  // 设置返回值为成功
            }
            break;
        case SET_RESOLUTION_11:
            if (ds18b20_readbyte() == 0x5F) {  // 读取一个字节并与 0x5F 进行比较
                ret = 0;  // 设置返回值为成功
            }
            break;
        case SET_RESOLUTION_12:
            if (ds18b20_readbyte() == 0x7F) {  // 读取一个字节并与 0x7F 进行比较
                ret = 0;  // 设置返回值为成功
            }
            break;
        default:
            break;
    }

    return ret;  // 返回结果
}


/**
 * 读取分辨率。
 *
 * @return 分辨率值
 */
int read_resolution(void) {
    int ret;

    // 复位传感器
    ds18b20_reset();

    // 发送指令字节0xCC,跳过ROM操作,直接与单个设备通信
    ds18b20_writebyte(0xCC);

    // 发送指令字节0xBE,读取当前设备的配置寄存器
    ds18b20_writebyte(0xBE);

    // 读取4个字节的数据,但实际上只有最后一个字节是分辨率信息
    ds18b20_readbyte();
    ds18b20_readbyte();
    ds18b20_readbyte();
    ds18b20_readbyte();

    // 读取最后一个字节,即分辨率信息
    ret = ds18b20_readbyte();

    // 返回分辨率值
    return ret;
}


int ds18b20_open(struct inode *inode, struct file *file)
{
    return 0;
}

ssize_t ds18b20_read(struct file *file, char __user *buf, size_t size, loff_t *offs) {
    int ds18b20_temp;    
    ds18b20_temp = ds18b20_readtemp();// 从 DS18B20 读取温度值    
    // 将温度值复制到用户空间缓冲区
    if (copy_to_user(buf, &ds18b20_temp, sizeof(ds18b20_temp))) {
        return -1; // 复制失败,返回错误代码
    }
    
    return 0; // 成功读取并复制温度值
}

int ds18b20_release(struct inode *inode, struct file *file)
{
    return 0;
}

/**
 * DS18B20 温度传感器的 ioctl 函数
 * @param file 文件指针
 * @param cmd 命令
 * @param args 参数
 * @return 返回执行结果,成功返回 0,失败返回 -1
 */
long ds18b20_ioctl(struct file *file, unsigned int cmd, unsigned long args) {
    int resolution;
    if (cmd == SET_RESOLUTION) {  // 判断命令是否为设置分辨率
        if (args >= SET_RESOLUTION_9 && args <= SET_RESOLUTION_12) {  // 判断参数是否在有效的分辨率范围内
            set_resolution(args);  // 调用设置分辨率的函数
            return 0;  // 返回成功
        }
    }
    else if (cmd == READ_RESOLUTION) {
        // 读取分辨率
        resolution = read_resolution();
        // 将分辨率的值复制给用户空间的args
        if (copy_to_user((int *)args, &resolution, sizeof(resolution))) {
            // 复制失败,返回-1表示失败
            return -1;
        }
    }
    // 如果不匹配 SET_RESOLUTION 或者 args 不在有效范围内,不执行任何操作
    return -1;  // 返回失败
}

struct file_operations ds18b20_fops = {
    .open = ds18b20_open,
    .read = ds18b20_read,
    .release = ds18b20_release,
    .unlocked_ioctl = ds18b20_ioctl,
    .owner = THIS_MODULE,
};

int ds18b20_probe(struct platform_device *dev)
{
    int ret;
    printk("This is probe \n");

    // 分配内存给ds18b20_data结构体
    ds18b20 = kzalloc(sizeof(*ds18b20), GFP_KERNEL);
    if (ds18b20 == NULL)
    {
        printk("kzalloc error\n");
        ret = -ENOMEM;
        goto error_0;
    }

    // 分配字符设备号
    ret = alloc_chrdev_region(&ds18b20->dev_num, 0, 1, "myds18b20");
    if (ret < 0)
    {
        printk("alloc_chrdev_region error\n");
        ret = -EAGAIN;
        goto error_1;
    }

    // 初始化字符设备
    cdev_init(&ds18b20->ds18b20_cdev, &ds18b20_fops);
    ds18b20->ds18b20_cdev.owner = THIS_MODULE;
    cdev_add(&ds18b20->ds18b20_cdev, ds18b20->dev_num, 1);

    // 创建设备类
    ds18b20->ds18b20_class = class_create(THIS_MODULE, "sensors");
    if (IS_ERR(ds18b20->ds18b20_class))
    {
        printk("class_create error\n");
        ret = PTR_ERR(ds18b20->ds18b20_class);
        goto error_2;
    }

    // 创建设备
    ds18b20->ds18b20_device = device_create(ds18b20->ds18b20_class, NULL, ds18b20->dev_num, NULL, "ds18b20");
    if (IS_ERR(ds18b20->ds18b20_device))
    {
        printk("device_create error\n");
        ret = PTR_ERR(ds18b20->ds18b20_device);
        goto error_3;
    }

    // 获取GPIO描述符
    ds18b20->ds18b20_gpio = gpiod_get_optional(&dev->dev, "ds18b20", 0);
    if (ds18b20->ds18b20_gpio == NULL)
    {
        ret = -EBUSY;
        goto error_4;
    }

    // 设置GPIO方向为输出
    gpiod_direction_output(ds18b20->ds18b20_gpio, 1);

    return 0;

    error_4:
    device_destroy(ds18b20->ds18b20_class, ds18b20->dev_num);

    error_3:
    class_destroy(ds18b20->ds18b20_class);

    error_2:
    cdev_del(&ds18b20->ds18b20_cdev);
    unregister_chrdev_region(ds18b20->dev_num, 1);

    error_1:
    kfree(ds18b20);

    error_0:
    return ret;
}

const struct of_device_id ds18b20_match_table[] = {
    {.compatible = "ds18b20"},
    {},
};

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

static int __init ds18b20_init(void)
{
    int ret;

    // 注册平台驱动
    ret = platform_driver_register(&ds18b20_driver);
    if (ret < 0)
    {
        printk("platform_driver_register error\n");
        return -1;
    }

    return 0;
}

static void __exit ds18b20_exit(void)
{
    // 释放资源
    gpiod_put(ds18b20->ds18b20_gpio);
    device_destroy(ds18b20->ds18b20_class, ds18b20->dev_num);
    class_destroy(ds18b20->ds18b20_class);
    cdev_del(&ds18b20->ds18b20_cdev);
    unregister_chrdev_region(ds18b20->dev_num, 1);
    kfree(ds18b20);
    platform_driver_unregister(&ds18b20_driver);
}

module_init(ds18b20_init);
module_exit(ds18b20_exit);
MODULE_LICENSE("GPL");

165.2 应用程序编写

在上一小节中编写了ioctl 通过ioctl 设置DS18B20 获取采集温度分辨率的程序,而ioctl需要跟用户空间的应用程序相配合才能进行使用,所以在本小节将编写测试用的应用程序,编写好的测试程序存放位置为:iTOP-RK3568 开发板【底板V1.7 版本】\03_【iTOP-RK3568 开发板】指南教程\02_Linux 驱动配套资料\04_Linux 驱动例程\100_ds18b20_07\02_app

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <stdlib.h>

#define SET_RESOLUTION _IOW('A', 0, int)
#define READ_RESOLUTION _IOR('A', 1, int)

/**
 * 根据传感器读取的原始数据计算温度值
 * @param value 传感器读取的原始数据
 */
void ds18b20_get_temp(int value) {
    char sig;   // 温度正负号
    float temp; // 温度值

    // 判断温度正负号
    if ((value >> 11) & 0x01) {
        sig = '-';
        value = ~value + 1;
        value &= ~(0xf8 << 8);
    } else {
        sig = '+';
    }

    // 计算温度值
    temp = value * 0.0625;

    // 打印温度信息
    printf("温度为 %c%.4f\n", sig, temp);
}


/**
 * 根据传感器分辨率值打印分辨率信息
 * @param value 传感器分辨率值
 */
void ds18b20_get_resolution(int value) {
    switch (value) {
        case 0x1F:
            printf("分辨率为 9 位\n");
            break;
        case 0x3F:
            printf("分辨率为 10 位\n");
            break;
        case 0x5F:
            printf("分辨率为 11 位\n");
            break;
        case 0x7F:
            printf("分辨率为 12 位\n");
            break;
        default:
            break;
    }
}

int main(int argc, char *argv[]) {
    int fd;    // 文件描述符
    int data;  // 读取的数据
    int args;  // 参数值
    int resolution;  // 返回的分辨率的值

    // 打开设备文件
    fd = open("/dev/ds18b20", O_RDWR);
    if (fd < 0) {
        printf("打开设备文件出错\n");
        return -1;
    }

    // 获取命令行参数
    args = atoi(argv[1]);
    printf("参数值为 %d\n", args);

    // 检查参数范围
    if (args < 9 || args > 12) {
        printf("错误!参数范围应为 9 - 12\n");
        return -1;
    }

    // 设置分辨率
    ioctl(fd, SET_RESOLUTION, args);

    // 读取分辨率
    ioctl(fd, READ_RESOLUTION, &resolution);
    ds18b20_get_resolution(resolution);
    while (1) {
        // 读取数据
        read(fd, &data, sizeof(data));

        // 处理并打印温度信息
        ds18b20_get_temp(data);
    }

    return 0;
}

165.3 运行测试

165.3.1 编译驱动程序

在上一小节中的de18b20.c 代码同一目录下创建Makefile 文件,Makefile 文件内容如下所示:然后使用命令“make”进行驱动的编译,编译完生成ds18b20.ko 目标文件,至此驱动模块就编译成功了。

165.3.2 编译应用程序

首先进行应用程序的编译, 因为测试APP 是要在开发板上运行的, 所以需要aarch64-linux-gnu-gcc 来编译,输入以下命令,编译完成以后会生成一个app 的可执行程序,如下图所示:

aarch64-linux-gnu-gcc app.c -o app

image-20240926172814651

然后将编译完成的可执行程序拷贝到开发板上。

165.3.3 运行测试

首先启动开发板,进入系统之后如下所示:
然后将上两个小节编译完成的ds18b20.ko 驱动和可执行程序app 文件拷贝到开发板上,拷贝完成如下所示:

image-20240926172837504

然后使用以下命令进行驱动的加载,如下图所示:

insmod ds18b20.ko

image-20240926172850676

然后使用以下命令运行可执行程序app,设置分辨率为9 位,然后打印采集到的温度,如下图所示:

./app 9

image-20240926172903755

第一个打印表示我们的传入参数9,第二个打印为通过ioctl 获取得到的ds18b20 的温度分辨率,可以看到设置和和通过ioctl 获取的分辨率相同,且后面得到的温度确实为9 位分辨率,证明试验成功,至此,关于通过ioctl 获取ds18b20 温度采集分辨率的实验就完成了。

第十五篇I2C

第166 章初步认识I2C

166.1 I2C 总线的由来

在一些复杂的电子设备内部电路中,通常需要使用大量的集成电路(IC)来实现各种功能,包括主控制器以及众多外围设备如PLL 合成器、非易失性存储器、音频处理器、视频处理器和屏幕显示器等。这些IC 器件之间需要相互传递数据信息,于是就需要使用大量的导线将它们连接
起来,如下图所示(本图为电子数字积分计算器ENIAC I,来自维基百科):

image-20240926172947513

这种众多IC 器件之间的复杂互连,必然会导致芯片引脚数量庞大、PCB 走线错综复杂,以及连接导线数量激增。这不仅会增加IC 芯片的体积和功耗,同时也大大提高了制造成本,给IC 设计制造厂商带来不利影响。同时,也给IC 应用厂商和工程师们带来了极大的不便。

而为了解决这一问题,在1982 年,荷兰著名电子公司飞利浦(Philips)公司发明了一种名为I2C(Inter-Integrated Circuit)的集成电路互连通信协议。该协议巧妙地利用仅两条线路就可以实现芯片之间的互连通信,大幅简化了硬件电路设计,提高了硬件资源的利用效率,给芯片设计制
造商和应用商带来了极大的好处。

I2C 协议的诞生,为复杂电子设备内部集成电路之间的互连通信提供了一种简单高效的解决方案,极大地促进了现代电子技术的发展。

166.2 I2C 硬件连接

I2C 总线在硬件上的连接非常简单,由SDA、SCL 和上拉电阻构成,具体连接如下图所示:

image-20240926174936257

在空闲状态下,SDA 和SCL 一般被上拉电阻拉高,保持高电平状态,在需要进行数据传输的时候,通过SCL 和SDA 的高低电平来产生I2C 总线所需要的信号进行数据传递。

166.3 I2C 基础知识

  • 1.12C总线是同步,串行,半双工通信总线。
  • 2.12C总线由串行数据线SDA和 串行时钟线SCL两根信号线构成。并且都有上拉电阻。确保总线空闲状态为高电平。
  • 3.12C总线支持多设备连接,允许多主机存在,但同一时刻只允许一台主机。通常我们把带有I2C总线接口的 CPU模块作为主机,把挂载在12C总线上的其他设备看作从机。无论同一个总线上有多少设备,也都是只使用俩根线。
  • 4.每一个12C外设都会对应一个唯一的地址(这个地址可以从12C外设器件的数据手册中得知),主机和从机之间的通信就是通过这个地址来确定主机要和哪个从机进行通信的。
  • 5.12C总线上挂载的设备的数量受总线最大电容 400pF 限制。如果挂载的是相同型号的器件,还受到器件的地址限制。
  • 6.12C总线在标准模式下传输速率可达 100kbit/s,在快速模式下可达 400kbit/s,在高速模式下可达 3.4Mbit/s。
  • 7.12C总线上的主机和从机之间以字节为单位进行数据传输。
  • 8.12C有硬件 I2C和软件 I2C(通过GPIO做读写时序的程序与硬件通信)。

(1)总线拓扑结构:

I2C 总线采用主从式架构,由一个主设备(Master)和一个或多个从设备(Slave)组成。主设备负责发起数据传输,从设备则响应主设备的请求。

(2)物理层接口:

I2C 总线使用两根线路进行通信:

  • SCL(Serial Clock Line)时钟线,由主设备提供时钟信号。
  • SDA(Serial Data Line)数据线,用于双向传输数据。
  • 这两根线通常需要上拉电阻来保持信号的高电平状态。

(3)通信协议:

I2C 采用同步串行通信方式,主设备发起通信并提供时钟。主设备首先发送一个”启动”信号,然后发送从设备地址和数据传输方向(读或写)从设备在收到自己的地址后,会发送应答信号,表示已准备好接收或发送数据。之后主设备和从设备就可以开始传输数据。通信结束时,主设
备发送”停止”信号

(4)时钟频率:
I2C 总线支持多种通信速率,常见的有:

  • 标准模式(Standard mode):100kbps
  • 快速模式(Fast mode):400kbps
  • 高速模式(High-speed mode):3.4Mbps

(5)寻址机制:

I2C 使用7 位地址空间,最多可寻址127 个从设备。地址空间的前7 位用于指定从设备,最后1 位用于表示读/写方向。
每一个12C 外设都会对应一个唯一的地址(这个地址可以从I2C 外设器件的数据手册中得到),主机和从机之间的通信就是通过这个地址来确定主机要和哪个从机进行通信的。

(6)多主机支持:

I2C 总线支持多个主设备共享同一总线,通过仲裁机制避免冲突。当多个主设备同时试图占用总线时,优先级最高的主设备将获得总线控制权。

(7)其他特点:

总线最大电容限制为400pF
以字节为单位传输数据。
存在硬件I2C 和软件I2C 两种实现方式。


文章作者: 葛杨文
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 葛杨文 !
评论
  目录