在物联网(IoT)应用中,传感器数据采集是实现智能化控制的基础。单总线(1-Wire)作为一种极简通信协议,凭借单根数据线完成数据传输与供电,被广泛应用于温度、湿度等传感器领域。本文将以 RK3568 开发板为载体,结合 DS18B20 温度传感器,详细介绍单总线协议的原理与实战开发流程,提供整合后的完整可运行代码与设备注册逻辑。
一、单总线协议:极简高效的通信方案
1.1 协议概述
单总线协议由 Dallas 公司提出,通过一根信号线实现数据的双向传输与设备供电,极大简化了硬件设计。其核心优势在于: 低成本:仅需一根数据线,大幅减少布线与芯片资源占用。 多设备挂载:单总线上可挂载多个从设备,通过设备唯一 ROM 编码实现寻址。 长距离传输:支持最长 1000 米的通信距离,适用于工业环境。
1.2 通信原理
单总线通信基于主从模式,主设备(如 RK3568)控制总线时序,从设备(DS18B20)响应指令。关键时序包括: 初始化时序:主设备拉低总线至少 480μs,释放后等待从设备回应(低电平 60 - 240μs)。 写时序:主设备通过拉低总线不同时长,区分写 0(60 - 120μs)与写 1(15μs)。 读时序:主设备拉低总线 15μs 后释放,从设备在 60μs 内输出数据。
二、DS18B20:单总线温度传感器的典型应用
DS18B20 是单总线协议的代表性传感器,其特性如下: 高精度测温:测温范围 -55℃~125℃,精度可达 ±0.5℃。 可编程分辨率:支持 9 - 12 位温度分辨率设置。 寄生电源模式:无需外部供电,通过数据线获取能量。 DS18B20 的操作流程为: 初始化总线,确认设备存在。 发送 ROM 命令(如跳过 ROM 匹配0xCC,适用于单设备场景)。 每个单总线从设备(如 DS18B20)内部都集成了一个 64 位 ROM 地址,结构如下:
前 8 位:家族码(Family Code),标识设备类型(如 DS18B20 的家族码为 0x28)。
中间 48 位:唯一序列号(Serial Number),每个设备独一无二。
最后 8 位:CRC 校验码(Cyclic Redundancy Check),用于验证地址的正确性。
发送功能命令(如启动温度转换0x44)。 读取温度数据,解析二进制编码。
三、DS18B20驱动开发
3.1 寄存器介绍
- 配置寄存器
- 温度测量寄存器 LS BYTE(最低有效字节,Least Significant Byte) 和 MS BYTE(最高有效字节,Most Significant Byte) S = SIGN 表示符号位 BIT10-BIT4表示整数部分 BIT3-BIT0表示小数部分
温度计算方法:
温度>0时(高5位符号位为0),测量的温度*0.0625
以温度125举例:
07D0h转换为10进制 = 2000
2000 * 0.0625 = 125
温度<0时(高5位符号位为1),(测量的温度取反+1)*0.0625,最后加上符号位 以温度-10.125举例:
FF5Eh 取反= A1 + 1 = A2
A2转换为10进制 = 162
162 * 0.0625 = 10.125
最后加上符号位 = -10.125
3.2 ROM指令
常用的指令:
| 指令 | 代码 | 功能 |
|---|---|---|
| 搜索 | F0h | 主设备确定从设备的数量及其类型 |
| 读取 | 33h | 总线主控读取从设备的64位ROM代码 |
| 匹配 | 55h | 通过匹配的ROM命令后跟一个64位的ROM代码序列,总线主控器可以寻址多点或单点总线上的特定从设备 |
| 跳过 | CCh | 主设备可以使用此命令同时对总线上的所有设备进行寻址,而无需发送任何ROM代码信息。例如,主设备可以通过发出Skip ROM命令,然后发出Convert T [44h]命令,使总线上的所有DS18B20器件同时执行温度转换。 |
| 报警搜索 | ECh | 此命令的操作与搜索ROM命令相同,但只有设置了报警标志的从设备会响应。该命令使主设备能够判断最近一次温度转换过程中是否有DS18B20经历了报警状态 |
3.3 RAM指令
3.4 读写时序图
3.5 硬件连接
DS18B20 VCC ---> RK3568 VCC3V3_SYS
DS18B20 GND ---> RK3568 GND
DS18B20 DQ ---> RK3568 DVP_PWREN0_H_GPIO0_B0
3.6 设备树(DTS)配置
在 RK3568 的设备树文件kernel/arch/arm64/boot/dts/rockchip/rk3568-evb1-ddr4-v10.dtsi中添加 DS18B20 节点:
ds18b20_gpio: ds18b20-gpio {
compatible = "maxim,ds18b20";
ds18b20-gpio = <&gpio0 RK_PB0 GPIO_ACTIVE_HIGH>;
pinctrl-names = "default";
pinctrl-0 = <&ds18b20_pins>;
};
设置引脚复用功能
ds18b20 {
ds18b20_pins: ds18b20-pins {
rockchip,pins = <0 RK_PB0 RK_FUNC_GPIO &pcfg_pull_up>;
};
};
3.7 内核驱动开发
将单总线核心驱动与 DS18B20 驱动整合在一个文件中:
#include <linux/module.h>
#include <linux/gpio.h>
#include <linux/delay.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include <linux/uaccess.h>
#define DS18B20_DEVICE_NAME "ds18b20"
// 定义ioctl命令
#define DS18B20_IOC_MAGIC 'd'
#define DS18B20_IOC_GET_TEMP _IOR(DS18B20_IOC_MAGIC, 1, int)
static dev_t dev_num;
static struct cdev ds18b20_cdev;
static struct class *ds18b20_class;
// 初始化总线
int ds18b20_reset(int gpio) {
gpio_direction_output(gpio, 1);
gpio_set_value(gpio, 0);
udelay(600);
gpio_set_value(gpio, 1);
gpio_direction_input(gpio);
int presence = 0;
int i;
while(presence = gpio_get_value(gpio));//当释放总线时,5kΩ上拉电阻将单线总线拉高。当DS18B20检测到上升沿时,它会等待15微秒至60微秒。
while(presence = !gpio_get_value(gpio));//然后通过将单线总线拉低60微秒至240微秒来发送存在脉冲。
udelay(480);
printk(KERN_INFO "ds18b20_reset = %d", presence);
return presence;
}
// 写一位数据
void ds18b20_write_bit(int gpio, int bit) {
gpio_direction_output(gpio, 1);
gpio_set_value(gpio, 0);
if(bit)
{
udelay(10);
gpio_direction_output(gpio, 1);
}
udelay(65);
gpio_direction_output(gpio, 1);
udelay(2);
}
// 写1字节 (调用8次write_bit)
static void ds18b20_write_byte(int gpio, u8 byte) {
int i;
for (i = 0; i < 8; i++) {
ds18b20_write_bit(gpio, byte & 0x01);
byte >>= 1;
}
}
/**
* ds18b20_read_bit - 从DS18B20读取1位数据
* 返回: 读取的位 (0 或 1)
*/
static int8_t ds18b20_read_bit(int gpio) {
int8_t bit = 0;
gpio_direction_output(gpio, 1);
gpio_set_value(gpio, 0);
udelay(2); // 典型值2μs
gpio_direction_input(gpio);
udelay(10); // 延时10μs(位于15μs窗口内)
bit = gpio_get_value(gpio);
printk(KERN_INFO "ds18b20_read_bit = %d", bit);
//等待位周期结束(总时间≥60μs)
udelay(60); // 完成60μs的位周期
return bit;
}
/**
* ds18b20_read_byte - 从DS18B20读取1字节数据
* 返回: 读取的字节数据 (8位)
*
* 注意:需确保DS18B20_GPIO_PIN已正确配置
*/
static int8_t ds18b20_read_byte(int gpio) {
int8_t byte = 0;
uint8_t i;
// 逐位读取,低位(LSB)优先
for (i = 0; i < 8; i++) {
// 读取1位数据
int8_t bit = ds18b20_read_bit(gpio);
// 组合字节数据(LSB优先)
byte |= (bit << i);
}
printk(KERN_INFO "ds18b20_read_byte = %d", byte);
return byte;
}
// 启动温度转换
void ds18b20_convert_temp(int gpio) {
ds18b20_reset(gpio);
ds18b20_write_byte(gpio, 0xCC); // 跳过ROM
ds18b20_write_byte(gpio, 0x44); // 启动转换
}
// 读取温度数据
int ds18b20_read_temp(int gpio) {
int8_t lsb, msb;
int raw_temp;
ds18b20_convert_temp(gpio);
mdelay(750); // 等待转换完成
ds18b20_reset(gpio);
ds18b20_write_byte(gpio, 0xCC);
ds18b20_write_byte(gpio, 0xBE); // 读取暂存器
lsb = ds18b20_read_byte(gpio);
msb = ds18b20_read_byte(gpio);
// 组合原始温度值(16位有符号整数)
raw_temp = (msb << 8) | lsb;
printk(KERN_INFO "ds18b20_read_temp = %d", raw_temp);
return raw_temp;
}
// 设备驱动与设备树匹配表
static const struct of_device_id ds18b20_of_match[] = {
{ .compatible = "maxim,ds18b20" },
{},
};
// 驱动探测函数 - 当设备树节点与驱动匹配时调用
static int ds18b20_probe(struct platform_device *pdev) {
struct device *dev = &pdev->dev;
int ret;
// 从设备树获取GPIO配置
int ds18b20_gpio = of_get_named_gpio_flags(dev->of_node,
"ds18b20-gpio", 0, NULL);
if (!gpio_is_valid(ds18b20_gpio)) {
dev_err(dev, "Failed to get GPIO\n");
return -ENODEV;
}
// 申请GPIO
ret = devm_gpio_request(dev, ds18b20_gpio, "ds18b20-gpio");
if (ret) {
dev_err(dev, "Failed to request GPIO\n");
return ret;
}
// 设置GPIO方向为输入
gpio_direction_input(ds18b20_gpio);
/*// 检查DS18B20是否存在
if (ds18b20_reset(ds18b20_gpio)) {
dev_err(dev, "DS18B20 not found on GPIO%d\n", ds18b20_gpio);
return -ENODEV;
}*/
dev_info(dev, "DS18B20 found on GPIO%d\n", ds18b20_gpio);
return 0;
}
// 驱动移除函数
static int ds18b20_remove(struct platform_device *pdev) {
dev_info(&pdev->dev, "DS18B20 driver removed\n");
return 0;
}
// 平台驱动结构
static struct platform_driver ds18b20_platform_driver = {
.probe = ds18b20_probe,
.remove = ds18b20_remove,
.driver = {
.name = "ds18b20",
.of_match_table = ds18b20_of_match,
.owner = THIS_MODULE,
},
};
static int ds18b20_open(struct inode *inode, struct file *filp) {
return 0;
}
static int ds18b20_release(struct inode *inode, struct file *filp) {
return 0;
}
static long ds18b20_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
int gpio = of_get_named_gpio_flags(of_find_compatible_node(NULL, NULL, "maxim,ds18b20"),
"ds18b20-gpio", 0, NULL);
int temperature;
int ret;
if (gpio < 0) {
return -ENODEV;
}
printk(KERN_INFO "ds18b20_ioctl cmd = %d", cmd);
switch (cmd) {
case DS18B20_IOC_GET_TEMP: // 读取温度
temperature = ds18b20_read_temp(gpio);
printk(KERN_INFO "ds18b20_ioctl temperature = %d", temperature);
ret = copy_to_user((int *)arg, &temperature, sizeof(int));
if (ret) {
return -EFAULT;
}
return 0;
default:
return -EINVAL;
}
}
static struct file_operations ds18b20_fops = {
.owner = THIS_MODULE,
.open = ds18b20_open,
.release = ds18b20_release,
.unlocked_ioctl = ds18b20_ioctl,
};
// 驱动初始化函数(修改后)
static int __init ds18b20_driver_init(void) {
int ret;
// 注册平台驱动
ret = platform_driver_register(&ds18b20_platform_driver);
if (ret) {
printk(KERN_ERR "Failed to register platform driver\n");
return ret;
}
// 申请设备号
ret = alloc_chrdev_region(&dev_num, 0, 1, DS18B20_DEVICE_NAME);
if (ret < 0) {
printk(KERN_ERR "Failed to allocate device number\n");
platform_driver_unregister(&ds18b20_platform_driver);
return ret;
}
// 初始化字符设备
cdev_init(&ds18b20_cdev, &ds18b20_fops);
ds18b20_cdev.owner = THIS_MODULE;
// 添加字符设备到系统
ret = cdev_add(&ds18b20_cdev, dev_num, 1);
if (ret < 0) {
printk(KERN_ERR "Failed to add cdev\n");
unregister_chrdev_region(dev_num, 1);
return ret;
}
// 创建设备类
ds18b20_class = class_create(THIS_MODULE, DS18B20_DEVICE_NAME);
if (IS_ERR(ds18b20_class)) {
ret = PTR_ERR(ds18b20_class);
printk(KERN_ERR "Failed to create class\n");
cdev_del(&ds18b20_cdev);
unregister_chrdev_region(dev_num, 1);
return ret;
}
device_create(ds18b20_class, NULL, dev_num, NULL, DS18B20_DEVICE_NAME);
printk(KERN_INFO "ds18b20_driver_init\n");
return 0;
}
// 驱动退出函数(修改后)
static void __exit ds18b20_driver_exit(void) {
// 销毁设备节点
device_destroy(ds18b20_class, dev_num);
// 销毁设备类
class_destroy(ds18b20_class);
// 从系统移除字符设备
cdev_del(&ds18b20_cdev);
// 释放设备号
unregister_chrdev_region(dev_num, 1);
// 注销平台驱动
platform_driver_unregister(&ds18b20_platform_driver);
}
module_init(ds18b20_driver_init);
module_exit(ds18b20_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("cmy");
MODULE_DESCRIPTION("DS18B20 driver for RK3568");
3.8 用户空间测试程序
#define DS18B20_IOC_MAGIC 'd'
#define DS18B20_IOC_GET_TEMP _IOR(DS18B20_IOC_MAGIC, 1, int)
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_led9_MainActivity_ds18b20(JNIEnv *env, jobject thiz) {
int fd;
int temp = 0;
// 打开设备
fd = open("/dev/ds18b20", O_RDWR);
if (fd == -1) {
LOGD("ds18b20 无法打开设备");
return -1;
}
LOGD("ds18b20 DS18B20_IOC_GET_TEMP = %u", DS18B20_IOC_GET_TEMP);
while(true){
int ret = ioctl(fd, DS18B20_IOC_GET_TEMP, &temp);
if ((temp >> 11) & 0x01) {
temp = ~temp + 1;
temp &= ~(0xf8 << 8);
}
float value = temp * 0.0625f;
LOGD("ds18b20 temp = %d,ret = %d,value = %.4f", temp, ret, value);
sleep(2);
}
close(fd);
return 0;
}
四、调试与优化
4.1 常见问题排查
设备无响应:检查硬件连接、上拉电阻与电源,使用示波器观察总线时序。 数据错误:确认 ROM 命令与功能命令是否正确,验证温度分辨率设置。
4.2 性能优化
多设备支持:通过 ROM 匹配命令(如0x55)实现单总线上多个 DS18B20 的独立寻址。 中断优化:使用 RK3568 的 GPIO 中断检测 DS18B20 的响应,减少轮询开销。
五、总结与拓展
通过本文实践,我们在 RK3568 平台上成功实现了 DS18B20 温度传感器的单总线通信。单总线协议的极简设计为物联网设备开发提供了高性价比方案,未来可拓展至湿度传感器、EEPROM 等更多单总线设备。在实际应用中,需结合具体场景优化驱动与通信逻辑,确保数据采集的准确性与实时性。 如果你在开发过程中遇到问题,欢迎在评论区留言交流,也可以尝试将本文的方案应用于智能家居、环境监测等项目中,探索更多可能性!