Linux驱动-单总线-DS18b20-写时序编写

提示:Linux驱动-单总线-DS18b20-写时序编写

文章目录

  • 前言
  • 一、参考资料
  • 二、目标
  • 三、时序图-分析-代码实现
  • 四、写时序驱动-实现
  • 五、知识点补充-基础补充
    • [1、核心原理:1-Wire 写操作的本质](#1、核心原理:1-Wire 写操作的本质)
    • 时序要求对比-总结表
    • [2、写时序、读时序 到底是干嘛的?](#2、写时序、读时序 到底是干嘛的?)
      • [写时序 = 主机 → DS18B20(发命令 / 发数据)](#写时序 = 主机 → DS18B20(发命令 / 发数据))
      • [读时序 = DS18B20 → 主机(回传数据)](#读时序 = DS18B20 → 主机(回传数据))
      • [写时序 vs 读时序](#写时序 vs 读时序)
    • [3、T_REC 是什么](#3、T_REC 是什么)
      • 定义
      • [为什么需要 T_REC?](#为什么需要 T_REC?)
    • [4、怎么理解:采样判断-DS18B20 在 15~60μs 的采样窗口内,看到的是低电平,判定主机发送的是 0](#4、怎么理解:采样判断-DS18B20 在 15~60μs 的采样窗口内,看到的是低电平,判定主机发送的是 0)
    • [5、0xCC 到底是怎么发出去的](#5、0xCC 到底是怎么发出去的)
    • [6、为什么 方法 ds18b20_writebyte 参数类型明明是16进制的,但是 实际方法实现 void ds18b20_writebyte(int data) 方法的参数确实int 类型的](#6、为什么 方法 ds18b20_writebyte 参数类型明明是16进制的,但是 实际方法实现 void ds18b20_writebyte(int data) 方法的参数确实int 类型的)
      • 参数类型理解
      • [那为什么不写成 unsigned char /uint8_t?](#那为什么不写成 unsigned char /uint8_t?)
    • [7、data & 0x01 这个运算是在做什么 。](#7、data & 0x01 这个运算是在做什么 。)
    • [8、ds18b20_writebyte(0xcc);-但是内部实现要for 循环8次](#8、ds18b20_writebyte(0xcc);-但是内部实现要for 循环8次)
      • 核心答案(一句话讲透)
      • [最基础的知识点:字节 vs 位](#最基础的知识点:字节 vs 位)
      • [0xCC 到底是什么?](#0xCC 到底是什么?)
      • [循环 8 次到底在干嘛?](#循环 8 次到底在干嘛?)
  • 总结

前言

这里还是结合前面的知识点,前面文章已经讲解、总结、归纳了驱动框架、字符驱动框架,里面还及到平台总线知识点。接下来就是单总线协议,通过对gpio 的控制结合单总线协议,来实现基本功能。 这里讲解的是复位时序问题。

一、参考资料

这里我们把以前的相关知识点,拿到这里规整一下

温度传感器-DS18B20驱动框架编写
温度传感器-DS18B20字符设备驱动框架

Linux驱动-单总线-DS18b20-驱动设备树配置-GPIO复用

DS18B20 电路及驱动笔记

Linux驱动-单总线-DS18b20-复位时序编写

其实, 之前是对基础知识的补充,这里其实核心是对协议的对接,前面也有对复位时序的编写,这里主要以写时序来编写相关代码。

二、目标

如果作为一个初学者,对时序、gpio、读写操作 比较陌生,对于外设比如这里的单总线时序比较陌生就很难理解以时序图来实现代码编写转化为功能,也就是通信协议的实现。

所以:目标还是看懂时序图,然后转化为代码。

三、时序图-分析-代码实现

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

分写入 0 进行分析

写0分析-代码演示

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

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

java 复制代码
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 操作代码如下所示:

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

写时序代码提炼-规整-封装

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

所示:

java 复制代码
/**
* 向 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 次该函数, 而为了更方便, 可以重新添加一个函数, 从而直接写入一个字节的数据, 具体内容如下所示:

java 复制代码
/**
* 向 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 方向设置为输出
} u
delay(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_writebyte(0xcc);//写入0Xcc字符

发送的 0xcc 就是跳过搜索的指令。

java 复制代码
#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");

五、知识点补充-基础补充

1、核心原理:1-Wire 写操作的本质

1-Wire 是半双工通信,主机(MCU)是总线的主导者,所有写操作都由主机发起:

  • 写操作的单位是时隙(time slot),每个时隙传输 1 bit(0 或 1)
  • 主机通过拉低总线来启动一个时隙,从机(DS18B20)只会在每个时隙的固定窗口采样电平,来判断主机发送的是 0 还是 1
  • 关键区别:
java 复制代码
写 0:主机拉低总线的时间很长(持续拉低),让从机采样时看到低电平
写 1:主机拉低总线的时间很短,立即释放,让从机采样时看到高电平

时序要求对比-总结表

操作 主机拉低时长 从机采样电平 时隙周期 恢复时间
写 0 60~120μs 低电平 ≥60μs ≥1μs
写 1 1~15μs 高电平 ≥60μs ≥1μs

2、写时序、读时序 到底是干嘛的?

写时序 = 主机 → DS18B20(发命令 / 发数据)

  • 主机告诉 DS18B20:
java 复制代码
开始转换温度
读取温度
匹配 ROM
跳过 ROM
等等...
  • 一句话:写时序 = 主机说话

读时序 = DS18B20 → 主机(回传数据)

  • DS18B20 把温度数据、寄存器数据传给主机
  • 一句话:读时序 = 主机听 DS18B20 说话

写时序 vs 读时序

类型 谁控制总线 目的 关键动作
写时序 主机控制 发命令 长拉低 = 0,短拉低 = 1
读时序 主机启动,从机回复 读温度 主机拉低→释放→快速采样

3、T_REC 是什么

T_REC 是 Recovery Time(恢复时间),也就是总线从低电平恢复到稳定高电平的时间。

定义

  • 在写 "1" 时隙中:
java 复制代码
主机拉低总线很短时间(1~15μs),然后释放总线
总线由上拉电阻拉回高电平,这个从低到高、稳定下来的过程,就叫 恢复时间 T_REC
  • 图中标注的 1μs < T_REC < ∞,意思是:
java 复制代码
恢复时间必须 大于 1μs
没有上限,只要大于 1μs 都可以

为什么需要 T_REC?

因为总线是有电容的(线缆寄生电容 + 设备输入电容),不是拉一下就能立刻变高的:

  • 电容放电需要时间,电平是慢慢爬升的(图里那个斜线 / 阴影部分就是这个过程)
  • 如果恢复时间太短,总线电平还没稳定到高,下一个时隙就开始了,就会导致从机采样错误
    所以 T_REC > 1μs 是为了保证:总线完全回到高电平、稳定下来,再开始下一个操作

4、怎么理解:采样判断-DS18B20 在 15~60μs 的采样窗口内,看到的是低电平,判定主机发送的是 0

我们把这句话拆成原理、时序图、硬件行为三层来讲,你就彻底懂了。

你说的这句话,对应的是 DS18B20 写时序的核心规则:

这样讲:DS18B20 只在「主机拉低总线后的第 15μs ~ 第 60μs 之间」读取总线电平,来判断主机发的是 0 还是 1。这是 1-Wire 协议的硬性规定,不是随便说的。

为什么是 15~60μs?(硬件角度的设计逻辑)

这个窗口是专门为了区分「写 0」和「写 1」设计的:
1. 主机写「0」时的总线状态

  • 主机拉低总线,并且持续拉低 60~120μs
  • 所以在 15~60μs 这个时间段内,总线一直被主机拉在低电平
  • DS18B20 采样时,读到的是低电平 → 判定为「0」
    2. 主机写「1」时的总线状态
  • 主机只拉低总线 1~15μs,然后立刻释放
  • 总线被上拉电阻拉回高电平(图里的斜线就是电平爬升过程)
  • 所以在 15~60μs 这个时间段内,总线已经稳定在高电平了
  • DS18B20 采样时,读到的是高电平 → 判定为「1」

用一个时间线,把整个过程串起来

假设现在是主机发送 0:

时间点 主机动作 总线电平 DS18B20 在做什么
0μs 拉低总线 收到下降沿,准备采样
1~15μs 持续拉低 等待采样窗口开启
15~60μs 持续拉低 采样!读到低电平 → 判定为 0
60~120μs 持续拉低 已经完成采样,不关心电平了
>120μs 释放总线 总线恢复,准备下一个时隙

再对比主机发送 1:

时间点 主机动作 总线电平 DS18B20 在做什么
0μs 拉低总线 收到下降沿,准备采样
1~15μs 拉高释放 从低变高 总线正在恢复高电平
15~60μs 释放状态 稳定高电平 采样!读到高电平 → 判定为 1
>60μs 释放状态 已经完成采样,不关心电平了

关键问题:为什么主机不拉一下就完事?

你可能会想:"为什么不直接拉一下代表 0,不拉代表 1?"这是因为:

  • 总线有寄生电容:线缆、芯片引脚都会有电容,电平变化不是瞬间完成的,有一个爬升 / 下降的过程(就是时序图里的斜线)
  • 为了抗干扰:给主机和从机都留了足够的时间裕量,不会因为一点点电平抖动就误判
  • 统一时序标准:所有 1-Wire 设备都遵守这个 15~60μs 的采样窗口,这样不同厂家的主机和从机才能兼容通信

5、0xCC 到底是怎么发出去的

0xCC 二进制 = 1100 1100

DS18B20 要求 低位先发送,所以发送顺序是:

java 复制代码
发送顺序:0 0 1 1 0 0 1 1

writebyte 循环 8 次,每次发最低位:

java 复制代码
发 0
发 0
发 1
发 1
发 0
发 0
发 1
发 1
每一位都通过 writebit 按照时序发送

6、为什么 方法 ds18b20_writebyte 参数类型明明是16进制的,但是 实际方法实现 void ds18b20_writebyte(int data) 方法的参数确实int 类型的

参数类型理解

0xCC 根本不是 "16 进制类型",它只是整数的一种写法!

在 C 语言里:

  • 0xCC = 十进制 204 = 二进制 11001100
  • 它们全都是整数(int)
  • 所以函数参数写 int data 完全正确、完全匹配

它们类型完全一样,都是 int!只是写法不同,方便人看,机器不区分。

ds18b20_writebyte(0xcc); 等价于 ds18b20_writebyte(204); 函数接收的都是 int 类型整数。所以函数定义写成:void ds18b20_writebyte(int data) 完全正确、完全匹配、完全没问题!

那为什么不写成 unsigned char /uint8_t?

其实更标准的写法是: void ds18b20_writebyte(unsigned char data) // 1字节,最标准 或者 void ds18b20_writebyte(uint8_t data)

但写成 int 也完全可以运行,原因:

  • DS18B20 只需要 8 bit 数据
  • int 一般是 4 字节,但我们只用到低 8 位
  • data & 0x01 只会取最低位,高位自动忽略

7、data & 0x01 这个运算是在做什么 。

data & 0x01 的作用只有一个: data & 0x01 = 取最后一位

  • 取出 data 的【最低位(最后一位)】
  • 只拿最后 1 个 bit,其他位全部丢掉!

两个基础知识:与运算与二进制

  • & 是 按位与运算
  • 0x01 就是 二进制 0000 0001

按位与规则:

java 复制代码
1 & 1 = 1
1 & 0 = 0
0 & 1 = 0
0 & 0 = 0

只有两个都是 1,结果才是 1,否则都是 0

java 复制代码
0xCC = 二进制 1100 1100

data      = 1100 1100
0x01      = 0000 0001
------------------------
& 运算后  = 0000 0000

结果 = 0也就是说:0xCC & 0x01 = 0,拿到了最低位:0

java 复制代码
data & 0x01
只看最后一位
= 最后一位是 1 → 返回 1
= 最后一位是 0 → 返回 0

为什么 DS18B20 代码必须这么写?

因为 DS18B20 规定:通信必须 低位先传(LSB First)

每次只能发 1 位,而且必须从 最低位 开始发!

所以:

java 复制代码
data & 0x01 → 拿最低位
发出去
data >>= 1 → 把下一位移到最后
循环 8 次,发完 1 个字节

8、ds18b20_writebyte(0xcc);-但是内部实现要for 循环8次

核心答案(一句话讲透)

因为:1 个字节 = 8 个位(bit),DS18B20 只能一位一位地收数据,所以你必须循环 8 次,一位一位地发

我给你用最通俗、最直观的方式讲清楚

最基础的知识点:字节 vs 位

  • 1 个字节(Byte) = 8 个位(bit)
  • 你发的 0xCC 是一个字节
  • 但 DS18B20 的通信方式是:一次只能收 1 位

所以:

  • 你不能一口气把 0xCC 丢给它
  • 必须拆成 8 个 bit
  • 一个一个发过去

0xCC 到底是什么?

0xCC 是十六进制,二进制 = 1100 1100一共 8 位!

java 复制代码
二进制:1  1  0  0  1  1  0  0
        ─────────────────────
              8 位

DS18B20 只能一位一位接收,所以必须循环 8 次。

循环 8 次到底在干嘛?

我给你一步步模拟:初始 data = 0xCC = 1100 1100

java 复制代码
第 1 次循环
取最低位:0
发送 0
数据右移 1 位 → 0110 0110
第 2 次循环
取最低位:0
发送 0
右移 → 0011 0011
第 3 次循环
取最低位:1
发送 1
右移 → 0001 1001
第 4 次循环
取最低位:1
发送 1
右移 → 0000 1100
第 5 次循环
发 0
第 6 次循环
发 0
第 7 次循环
发 1
第 8 次循环
发 1

总结

  • 会看时序图,将时序图代码化
  • 对基础知识一个补充,加深基础知识技能,理解为什么代码这么写,为什么要循环8次,字节和位的转化,低位 等基础知识点加深理解
相关推荐
ItJavawfc20 天前
温度传感器-DS18B20驱动框架编写
ds18b20驱动·平台总线驱动框架·驱动框架骨架