版权声明
本文档所有内容文字资料由凌云实验室郭工编著,主要用于凌云嵌入式Linux教学内部使用,版权归属作者个人所有。任何媒体、网站、或个人未经本人协议授权不得转载、链接、转帖或以其他方式复制发布/ 发表。已经授权的媒体、网站,在下载使用时必须注明来源,违者本人将依法追究责任。
Copyright (C) 2021 凌云物网智科实验室·郭工
Author: GuoWenxue <guowenxue@gmail.com> QQ: 281143292
3.6.1 一线协议——DS18B20传感器驱动
3.6.1.1.DS18B20简介
DS18B20是一种常见的数字温度传感器,其控制命令和数据都是以数字信号的方式输入输出,相比较于模拟温度传感器,具有具有体积小,硬件开销低,抗干扰能力强,精度高的特点。
测温范围:-55°C 到 +125°C 通信接口:1-Wire(单总线)
其它特征:可形成总线结构、内置温度报警功能、可寄生供电(VCC都不用接,直接信号线加GND线就可以通信)
3.6.1.2.DS18B20工作原理
模拟温度传感器:热敏电阻,可通过温度的变化改变电阻值,一般再接一个分压电阻,串联到VCC和GND之间,需要用AD转换芯片将模拟信号转换为数字信号才能供单片机使用。
DS18B20内部集成了模拟温度传感器所需的电路,其内部也相当于有个小芯片,将模拟信号处理成数字信号后存到RAM中,再通过引脚,将信号传给单片机使用。
3.6.1.3.DS18B20操作流程
初始化:从机复位,主机判断从机是否响应
ROM操作:ROM指令+本指令需要的读写操作
功能操作:功能指令+本指令需要的读写操作
ROM指令 |
功能指令 |
---|---|
SEARCH ROM [F0h](搜索ROM) |
CONVERT T [44h](温度变换,启动温度传感器读取温度放到RAM中) |
READ ROM [33h](读ROM) |
WRITE SCRATCHPAD [4Eh](写RAM,加某个地址后,把数据写入到RAM中) |
MATCH ROM [55h](匹配ROM,发送该指令后会紧接着发送ROM地址,用于多个设备) |
READ SCRATCHPAD [BEh](读RAM,可以只读取RAM前两位的温度值,后续的不读取) |
SKIP ROM [CCh](跳过ROM,只有一个设备时使用) |
COPY SCRATCHPAD [48h](复制暂存器,调用该指令后会将RAM Byte2,3,4的值写入到EEPROM中,掉电不丢失) |
ALARM SEARCH [ECh](报警ROM) |
RECALL E2 [B8h](将EEPROM的值读取到RAM对应地址处) |
READ POWER SUPPLY [B4h](判断是否是寄生供电) |
其中第二步执行ROM指令,也就是访问每个DS18B20,搜索64位序列号,读取匹配的序列号值,然后匹配对应的DS18B20,如果我们仅仅使用单个DS18B20,可以直接跳过ROM指令。而跳过ROM指令的字节是0xCC。
3.6.1.4.DS18B20时序
初始化DS18B20
任何器件想要使用,首先就是需要初始化,对于DS18B20单总线设备,首先初始化单总线为高电平,然后总线开始也需要检测这条总线上是否存在DS18B20这个器件。如果这条总线上存在DS18B20,总线会根据时序要求返回一个低电平脉冲,如果不存在的话,也就不会返回脉冲,即总线保持为高电平。
初始化具体时序步骤如下:
1.拉低总线至少480us,产生复位脉冲,然后释放总线(拉高电平)。 2.这时DS8B20检测到请求之后,会拉低信号,大约60~240us表示应答。 3.DS8B20拉低电平的60~240us之间,读取总线的电平,如果是低电平,那么表示初始化成功 4.DS18B20拉低电平60~240us之后,会释放总线。
写0/1 或者 读0/1
1.写时序
总线控制器通过控制单总线高低电平持续时间从而把逻辑1或0写DS18B20中。每次只传输1位数据
想要给DS18B20写入一个0时,需要将引脚拉低,保持低电平时间要在60~120us之间,然后释放总线 想要给DS18B20写入一个1时,需要将引脚拉低,拉低时间需要大于1us,然后在15us内拉高总线.
在写时序起始后15μs到60μs期间,DS18B20处于采样单总线电平状态。如果在此期间总线为高电平,则向DS18B20写入1;如果总线为低电平,则向DSl8B20写入0。
注意:2次写周期之间至少间隔1us
2.读时序
读时隙由主机拉低总线电平至少1μs然后再释放总线,读取DS18B20发送过来的1或者0
DS18B20在检测到总线被拉低1微秒后,便开始送出数据,若是要送出0就把总线拉为低电平直到读周期结束。若要送出1则释放总线为高电平。
3.6.1.5.DS18B20数据帧
温度变换:初始化→跳过ROM →开始温度变换
温度读取:初始化→跳过ROM →读暂存器→连续的读操作
3.6.1.6.温度存储格式
温度数据都是以补码的形式存储的,读取时要进行转换,特别是负数。
3.6.2 DS18B20驱动实验
3.6.2.1.设备树
首先配置设备树,imx6ull/bsp/kernel/linux-imx/arch/arm/boot/dts/overlays下创建w1.dts
/*
* Copyright (C) 2022 LingYun IoT System Studio
* Author: Guo Wenxue<guowenxue@gmail.com>
*/
/dts-v1/;
/plugin/;
#include <dt-bindings/gpio/gpio.h>
#include "../imx6ul-pinfunc.h"
/* W1(DS18B20) on 40Pin Header Pin#7 (GPIO1_IO18) */
&w1 {
compatible = "ds18b20-gpio";
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_w1>;
w1-gpios = <&gpio1 18 GPIO_ACTIVE_HIGH>;
};
&iomuxc {
pinctrl_w1: w1grp {
fsl,pins = <
MX6UL_PAD_UART1_CTS_B__GPIO1_IO18 0x110b0
>;
};
};
在Makefile中加上
dtb-y += w1.dtbo
编译设备树
make dtbs
生成w1.dtbo文件
3.6.2.2.DS18B20驱动实现
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h> //file_operations,用于联系系统调用和驱动程序
#include <linux/errno.h> //ENODEV,ENOMEM存放的头文件
#include <linux/kernel.h> // printk(),内核打印函数
#include <linux/device.h> // 用于自动生成设备节点的函数头文件
#include <linux/gpio.h> // gpio相关函数
#include <linux/gpio/consumer.h> // gpiod相关函数
#include <linux/of_gpio.h> // gpio子系统相关函数
#include <linux/uaccess.h> // copy_to_user函数头文件
#include <linux/timer.h> //定时器相关
#include <linux/delay.h> // 延时函数头文件
#include <linux/cdev.h> //cdev相关函数
#include <linux/platform_device.h>//platform相关结构体
#include <linux/delay.h>
#define DEV_NAME "w1_ds18b20" //最后在/dev路径下的设备名称,应用层open的字符串名
#define DEV_CNT 1
#ifndef DEV_MAJOR
#define DEV_MAJOR 0
#endif
#define LOW 0
#define HIGH 1
/* DS18B20 ROM命令 */
#define Read_ROM 0x33 // This command can only be used when there is one slave on the bus
#define SKIP_ROM 0xCC // without sending out any ROM code information
/* DS18B20 FUNC命令 */
#define Convert_T 0x44 // initiates a single temperature conversion.
#define Read_Data 0xBE // the master to read the contents of the scratchpad
#define CRC_MODEL 0x31
#define CRC_SUCCESS 0
#define CRC_FAIL 1
static int dev_major = DEV_MAJOR; /*主设备号*/
/*存放Led的私有属性*/
struct gpio_w1_priv {
struct cdev cdev; /*cdev结构体*/
struct class *dev_class;/*自动创建设备节点的类*/
struct device *dev;
spinlock_t lock;
};
struct gpio_desc *w1_gpiod; /* gpio descriptor */
/*DS18B20 接收发送*/
#define DS18B20_In_init() gpiod_direction_input(w1_gpiod)//设置GPIO的方向为输出,默认电平为高
#define DS18B20_Out_init() gpiod_direction_output(w1_gpiod, HIGH)//设置GPIO的方向为输出,默认电平为高
#define DS18B20_DQ_IN() gpiod_get_value(w1_gpiod)
#define DS18B20_DQ_OUT(N) gpiod_set_value(w1_gpiod, N)
/* @description: 解析设备树,初始化w1硬件io
*
* @parm : pdev - platform_device 设备指针
* @parm :
* @return: 0 successfully , !0 failure
*/
static int parser_dt_init_w1(struct platform_device *pdev)
{
struct device *dev = &pdev->dev; // 设备指针
struct gpio_w1_priv *priv = NULL; /*存放私有属性*/
struct gpio_desc *gpiod; /*gpiod结构体*/
/*为w1私有属性分配存储空间*/
priv = devm_kzalloc(dev, sizeof(struct gpio_w1_priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
/*获取w1的设备树节点*/
gpiod = gpiod_get(dev, "w1", 0);//请求gpio,有请求一定要有释放,否则模块下一次安装将请求失败
if (IS_ERR(gpiod))
{
printk("gpiod request failure\n");
return PTR_ERR(gpiod);
}
/*将gpiod传给结构体*/
w1_gpiod = gpiod;
dev_info(&pdev->dev, "parser dts got valid w1\n");
platform_set_drvdata(pdev, priv); //设置私有数据,方便其他函数操作
return 0;
}
static int w1_delay_parm = 1;
static void w1_delay(unsigned long tm)
{
udelay(tm * w1_delay_parm);
}
/* @description: 主机发送起始信号,主机拉低 >=480us,释放总线(主机拉高),等待15~60us
* @return: 0 successfully , !0 failure
*/
static int DS18B20_start(void)
{
int rv;
/*主机设置为输出*/
DS18B20_Out_init();//设置GPIO的方向为输出,默认电平为高
/*主机拉低 >=480us*/
DS18B20_DQ_OUT(LOW);
w1_delay(480);
/*主机拉高 15~60us*/
DS18B20_DQ_OUT(HIGH);
w1_delay(70);
/*主机设置为输入*/
DS18B20_In_init();
/*总线变低说明从设备发送了响应信号:60~240us*/
rv = DS18B20_DQ_IN();
if(0 != rv)
{
printk("%s():%d DS18B20_no respond \n", __FUNCTION__, __LINE__);
return -EFAULT;
}
printk("%s():%d DS18B20_respond \n", __FUNCTION__, __LINE__);
w1_delay(10);
DS18B20_Out_init();
DS18B20_DQ_OUT(HIGH);/*释放总线*/
return rv;
}
/* @description: 主机读取一个位,整个读周期最少需要60us,启动读开始信号后必须15us内读取IO电平,否则就会被上拉拉高
*
* @return: uint8_t bit
*/
static uint8_t DS18B20_readBit(void)
{
uint8_t bit = 0;
/*主机再设置成输出模式准备开始读数据*/
DS18B20_Out_init();//设置GPIO的方向为输出,默认电平为高
/*主机拉低 >=1,表示开始读数据*/
DS18B20_DQ_OUT(LOW);
w1_delay(2);
DS18B20_In_init();
w1_delay(10);
/*判断数据是0还是1*/
bit = DS18B20_DQ_IN();
w1_delay(50);
return bit;
}
/* @description: 读取一个字节
*
* @return: uint8_t Bety
*/
static uint8_t DS18B20_readBety(void)
{
uint8_t i,Bety = 0;
uint8_t bit;
for(i=0; i<8; i++)
{
bit = DS18B20_readBit();
if(bit)
Bety |= (0x01 << i);
// Bety |= DS18B20_readBit();/*MSB最先读到的是最高位*/
// Bety <<= 1;
}
printk("%s():%d DS18B20_readBit %x \n", __FUNCTION__, __LINE__, Bety);
return Bety;
}
/* @description: ds18b20 写一个位
*
* @parm : bit - 一个字
* @parm :
* @return:
*/
static void DS18B20_writeBit(unsigned char bit)
{
DS18B20_Out_init();
/* 判断 bit */
bit = bit > 1 ? 1 : bit;
w1_delay(50);
DS18B20_DQ_OUT(LOW);
w1_delay(2);
DS18B20_DQ_OUT(bit);
w1_delay(60);
DS18B20_DQ_OUT(HIGH);
w1_delay(12);
}
/* @description: ds18b20 写一个字节
*
* @parm : bit - 一个字
* @parm :
* @return:
*/
static void DS18B20_writeBety(uint8_t Bety)
{
uint8_t i = 0;
for(i=0; i<8; i++)
{
DS18B20_writeBit((Bety >> i)&0x01);
}
printk("%s():%d DS18B20_writeBety %x \n", __FUNCTION__, __LINE__, Bety);
}
/* @description: 读取温度值
*
* @parm : struct gpio_w1_priv *priv 私有数据结构体
* @parm :
* @return: uint16_t bit
*/
static uint16_t DS18B20_SampleData(struct gpio_w1_priv *priv)
{
unsigned long flags;
uint16_t temp = 0;
uint8_t temp_H = 0;
uint8_t temp_L = 0;
int rv = 0;
spin_lock_irqsave(&priv->lock, flags);
/*主机发起起始信号,并且从设备回应*/
if(0 != DS18B20_start())
{
printk("%s():%d DS18B20_start err \n", __FUNCTION__, __LINE__);
rv = -EFAULT;
goto undo_spin_unlock;
}
DS18B20_writeBety(SKIP_ROM);
DS18B20_writeBety(Convert_T);
spin_unlock_irqrestore(&priv->lock, flags);
msleep(750);//待修改
spin_lock_irqsave(&priv->lock, flags);
if(0 != DS18B20_start())
{
printk("%s():%d DS18B20_start err \n", __FUNCTION__, __LINE__);
rv = -EFAULT;
goto undo_spin_unlock;
}
DS18B20_writeBety(SKIP_ROM);
DS18B20_writeBety(Read_Data);
temp_L = DS18B20_readBety();
temp_H = DS18B20_readBety();
spin_unlock_irqrestore(&priv->lock, flags);
// temp_H = 0xff;
// temp_L = 0x5e;
temp = (temp_H << 8) | temp_L;
//printk("%s():%d temp_H:%x, temp_L:%x ,temp %x\n", __FUNCTION__, __LINE__, temp_H, temp_L, temp);
#if 0
//if(temp&0x8000>0)
/*如果为负数,就是算补码:取反再加一, 这个地方不需要计算,因为直接就可以判断是正还是负数*/
if(temp < 0)
{
temp = ~temp + 1;
}
#endif
return temp;
undo_spin_unlock:
spin_unlock_irqrestore(&priv->lock, flags);
return rv;
}
/* @description: 打开设备
*
* @parm : inode - 传递给驱动的inode
* @parm : filp - 设备文件,file结构体有个叫做private_data的成员变量
* * 一般在open的时候将private_data指向设备结构体。
* @return: 0 successfully , !0 failure
*/
/*字符设备操作函数集*/
static int w1_open(struct inode *inode, struct file *file)
{
struct gpio_w1_priv *priv;
printk("%s():%d w1_open platform driver\n", __FUNCTION__, __LINE__);
priv = container_of(inode->i_cdev, struct gpio_w1_priv, cdev);//找到私有数据的地址
file->private_data = priv;
return 0;
}
/* @description: 从设备读取文件
* @parm : filp - 设备文件,文件描述符
* @parm : buf - 返回给用户空间的数据缓冲区
* @parm : cnt - 要读取的数据长度
* @parm : offt - 相对于文件首地址的偏移
* @return: 读取的字节数,负数 - 读取失败
*/
static ssize_t w1_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
int rv = 0;
uint16_t temp = 0;
struct gpio_w1_priv *priv = filp->private_data;
temp = DS18B20_SampleData(priv);
printk("%s():%d temp %x \n", __FUNCTION__, __LINE__, temp);
rv = copy_to_user(buf, &temp, sizeof(temp));
if(rv)
{
dev_err(priv->dev, "copy to user error.\n");
return -EFAULT;
}
else
{
rv = sizeof(temp);
}
return rv;
}
/* @description: 关闭设备
*
* @parm : inode - 传递给驱动的inode
* @parm : filp - 设备文件,file结构体有个私有数据可以使用
* @return: 0 successfully , !0 failure
*/
static int w1_release(struct inode *inode, struct file *file)
{
printk("%s():%d w1_release w1 platform driver\n", __FUNCTION__, __LINE__);
return 0;
}
/*字符设备操作函数集*/
static struct file_operations w1_fops = {
.owner = THIS_MODULE,
.open = w1_open,
.read = w1_read,
.release = w1_release,
};
/* @description: sysfs - 温度属性显示函数
*
* @parm : devp - 设备指针,创建file时候会指定dev
* @parm : attr - 设备属性,创建时候传入
* @parm : buf - 传出给sysfs中显示的buf
* @return: 显示的字节数
*/
static ssize_t temp_show(struct device *devp, struct device_attribute *attr, char *buf)
{
u_int16_t temp = 0;
struct gpio_w1_priv *priv = dev_get_drvdata(devp);
temp = DS18B20_SampleData(priv);
return sprintf(buf, "t=%d\n", temp*62);//1000倍
}
/* @description: sysfs - echo写入属性函数
* @parm : dev - 设备指针,创建file时候会指定dev
* @parm : attr - 设备属性,创建时候传入
* @parm : buf - 用户空间的buf
* @parm : count - 传入buf的size
* @return: 写入的buf大小
*/
static ssize_t temp_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
return count;
}
/*声明并初始化一个 device_attribute结构体*/
DEVICE_ATTR(temp, 0644, temp_show, temp_store);
/*probe函数实现字符设备的注册和LED灯的初始化*/
static int gpio_w1_probe(struct platform_device *pdev)
{
struct gpio_w1_priv *priv; //临时存放私有属性的结构体
struct device *dev; //设备结构体
dev_t devno; //设备的主次设备号
int rv = 0;
/*解析设备树并初始化w1状态*/
rv = parser_dt_init_w1(pdev);
if( rv < 0 )
return rv;
/*将之前存入的私有属性。放入临时结构体中*/
priv = platform_get_drvdata(pdev);
/*---------------------注册 字符设备部分-----------------*/
/*1.分配主次设备号,这里即支持静态指定,也至此动态申请*/
if (0 != dev_major) /*static*/
{
devno = MKDEV(dev_major, 0);
rv = register_chrdev_region(devno, 1, DEV_NAME); /* /proc/devices/my_w1 */
}
else
{
rv = alloc_chrdev_region(&devno, 0, 1, DEV_NAME); /*动态申请字符设备号*/
dev_major = MAJOR(devno); /*获取主设备号*/
}
/*自动分配设备号失败*/
if (rv < 0)
{
printk("%s driver can't get major %d", DEV_NAME, dev_major);
return rv;
}
printk(" %s driver use major %d\n", DEV_NAME, dev_major);
/*2.分配cdev结构体,绑定主次设备号、fops到cdev结构体中,并注册给Linux内核*/
priv->cdev.owner = THIS_MODULE; /*.owner这表示谁拥有你这个驱动程序*/
cdev_init(&priv->cdev, &w1_fops); /*初始化cdev,把fops添加进去*/
rv = cdev_add(&priv->cdev, devno, DEV_CNT); /*注册给内核,设备数量1个*/
if (0 != rv) {
printk( " %s driver can't register cdev:result=%d\n", DEV_NAME, rv);
goto undo_major;
}
printk( " %s driver can register cdev:result=%d\n", DEV_NAME, rv);
//3.创建类,驱动中进行节点创建
priv->dev_class = class_create(THIS_MODULE, DEV_NAME); /* /sys/class/my_w1 创建类*/
if(IS_ERR(priv->dev_class))
{
printk("%s driver create class failure\n", DEV_NAME);
rv = -ENOMEM;
goto undo_cdev;
}
//4.创建设备
devno = MKDEV(dev_major, 0); //给每一个led设置设备号
dev = device_create(priv->dev_class, NULL, devno, NULL, DEV_NAME); /* /dev/my_w1 注册这个设备节点*/
if(IS_ERR(priv->dev_class))
{
rv = -ENOMEM; //返回错误码,应用空间strerror查看
goto undo_class;
}
/*5. 初始化自旋锁*/
spin_lock_init(&priv->lock);
//6. 创建sys 属性 在platform下
if(device_create_file(dev, &dev_attr_temp))
{
rv = -ENOMEM;
goto undo_device;
}
priv->dev = dev; /*把dev赋给priv结构体*/
//7. 保存私有数据
platform_set_drvdata(pdev, priv);
dev_set_drvdata(priv->dev, priv);
dev_info(priv->dev, "%s driver probe okay.\n", DEV_NAME);
/*class_create->name , device_create->name */
dev_info(&pdev->dev, "%s driver probe okay.\n", DEV_NAME);
/*driver->name, device->name(devic_tree_node->name)*/
undo_device:
device_destroy(priv->dev_class, devno); /*注销每一个设备号*/
undo_class:
class_destroy(priv->dev_class);
undo_cdev:
cdev_del(&priv->cdev);
undo_major:
unregister_chrdev_region(devno, DEV_CNT);
return rv;
}
/* @description: 设备卸载时候执行
*
* @parm : pdev - 设备指针
* @return: 0 successfully , !0 failure
*/
static int gpio_w1_remove(struct platform_device *pdev)
{
struct gpio_w1_priv *priv = platform_get_drvdata(pdev);//临时存放私有属性的结构体
dev_t devno = MKDEV(dev_major, 0);
//删除sys中的属性
device_remove_file(priv->dev, &dev_attr_temp);
device_destroy(priv->dev_class, devno); /*注销每一个设备号*/
class_destroy(priv->dev_class); //注销类
cdev_del(&priv->cdev); //删除cdev
unregister_chrdev_region(MKDEV(dev_major, 0), DEV_CNT);//释放设备号
gpiod_set_value(w1_gpiod, 0); //低电平关闭灯
gpiod_put(w1_gpiod); //释放gpiod
devm_kfree(&pdev->dev, priv); //释放堆
printk("gpio w1 driver removed.\n");
return 0;
}
/* @description: 设备停止执行的函数
*
* @parm : pdev - 设备指针
* @return: void
*/
static void imx_w1_shutdown(struct platform_device *pdev)
{
struct gpio_w1_priv *priv = platform_get_drvdata(pdev);
gpiod_set_value(w1_gpiod, 0);
}
/*------------------第一部分----------------*/
static const struct of_device_id w1_match_table[] = {
{.compatible = "ds18b20-gpio"},
{/* sentinel */},
};
MODULE_DEVICE_TABLE(of, w1_match_table);
/*定义平台驱动结构体*/
static struct platform_driver gpio_w1_driver = {
.probe = gpio_w1_probe, //驱动安装时候会执行的钩子函数
.remove = gpio_w1_remove, //驱动卸载时候
.shutdown = imx_w1_shutdown, //设备停止时候
.driver = { //描述这个驱动的属性
.name = "ds18b20-gpios", //无设备树时,用于设备和驱动间匹配
.owner = THIS_MODULE,
.of_match_table = w1_match_table,//有设备树后,利用设备树匹配表
},
};
/*------------------第二部分----------------*/
/*驱动初始化函数*/
static int __init platdrv_gpio_w1_init(void)
{
int rv = 0;
rv = platform_driver_register(&gpio_w1_driver); //注册platform的led驱动
if(rv)
{
printk(KERN_ERR "%s:%d: Can't register platform driver %d\n", __FUNCTION__, __LINE__, rv);
return rv;
}
printk("Regist w1 Platform Driver successfully!\n");
return 0;
}
/*------------------第三部分----------------*/
/*驱动注销函数*/
static void __exit platdrv_gpio_w1_exit(void)
{
printk("%s():%d remove w1 platform driver\n", __FUNCTION__, __LINE__);
platform_driver_unregister(&gpio_w1_driver); //卸载驱动
}
module_init(platdrv_gpio_w1_init);
module_exit(platdrv_gpio_w1_exit);
MODULE_AUTHOR("Guowenxue <guowenxue@gmail.com>");
MODULE_DESCRIPTION("GPIO W1 driver on i.MX6ULL platform");
MODULE_LICENSE("GPL");
MODULE_ALIAS("platform:w1-dev");
3.6.2.2.1.注册平台设备
驱动入口和出口函数仅仅用于注册、注销平台设备驱动,代码块如下:
/*------------------第一部分----------------*/
static const struct of_device_id w1_match_table[] = {
{.compatible = "ds18b20-gpio"},
{/* sentinel */},
};
MODULE_DEVICE_TABLE(of, w1_match_table);
/*定义平台驱动结构体*/
static struct platform_driver gpio_w1_driver = {
.probe = gpio_w1_probe, //驱动安装时候会执行的钩子函数
.remove = gpio_w1_remove, //驱动卸载时候
.shutdown = imx_w1_shutdown, //设备停止时候
.driver = { //描述这个驱动的属性
.name = "ds18b20-gpios", //无设备树时,用于设备和驱动间匹配
.owner = THIS_MODULE,
.of_match_table = w1_match_table,//有设备树后,利用设备树匹配表
},
};
/*------------------第二部分----------------*/
/*驱动初始化函数*/
static int __init platdrv_gpio_w1_init(void)
{
int rv = 0;
rv = platform_driver_register(&gpio_w1_driver); //注册platform的led驱动
if(rv)
{
printk(KERN_ERR "%s:%d: Can't register platform driver %d\n", __FUNCTION__, __LINE__, rv);
return rv;
}
printk("Regist w1 Platform Driver successfully!\n");
return 0;
}
/*------------------第三部分----------------*/
/*驱动注销函数*/
static void __exit platdrv_gpio_w1_exit(void)
{
printk("%s():%d remove w1 platform driver\n", __FUNCTION__, __LINE__);
platform_driver_unregister(&gpio_w1_driver); //卸载驱动
}
module_init(platdrv_gpio_w1_init);
module_exit(platdrv_gpio_w1_exit);
3.6.2.2.2.解析设备树
获取设备树节点,申请GPIO。
/* @description: 解析设备树,初始化w1硬件io
*
* @parm : pdev - platform_device 设备指针
* @parm :
* @return: 0 successfully , !0 failure
*/
static int parser_dt_init_w1(struct platform_device *pdev)
{
struct device *dev = &pdev->dev; // 设备指针
struct gpio_w1_priv *priv = NULL; /*存放私有属性*/
struct gpio_desc *gpiod; /*gpiod结构体*/
/*为w1私有属性分配存储空间*/
priv = devm_kzalloc(dev, sizeof(struct gpio_w1_priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
/*获取w1的设备树节点*/
gpiod = gpiod_get(dev, "w1", 0);//请求gpio,有请求一定要有释放,否则模块下一次安装将请求失败
if (IS_ERR(gpiod))
{
printk("gpiod request failure\n");
return PTR_ERR(gpiod);
}
/*将gpiod传给结构体*/
w1_gpiod = gpiod;
dev_info(&pdev->dev, "parser dts got valid w1\n");
platform_set_drvdata(pdev, priv); //设置私有数据,方便其他函数操作
return 0;
}
3.6.2.2.3.probe函数和.remove函数实现
在.probe中实现一些初始化工作,设置platform_device 的私有数据,用来传递设备结构体指针。
然后申请设备号,初始化cdev绑定ops,将cdev注册到内核中,并创建class 和 device ,然后创建设备属性文件。
在.remove函数中实现退出之前的清理工作。
prob函数
/*probe函数实现字符设备的注册和初始化*/
static int gpio_w1_probe(struct platform_device *pdev)
{
struct gpio_w1_priv *priv; //临时存放私有属性的结构体
struct device *dev; //设备结构体
dev_t devno; //设备的主次设备号
int rv = 0;
/*解析设备树并初始化w1状态*/
rv = parser_dt_init_w1(pdev);
if( rv < 0 )
return rv;
/*将之前存入的私有属性。放入临时结构体中*/
priv = platform_get_drvdata(pdev);
/*---------------------注册 字符设备部分 省略-----------------*/
/*5. 初始化自旋锁*/
spin_lock_init(&priv->lock);
//6. 创建sys 属性 在platform下
if(device_create_file(dev, &dev_attr_temp))
{
rv = -ENOMEM;
goto undo_device;
}
priv->dev = dev; /*把dev赋给priv结构体*/
//7. 保存私有数据
platform_set_drvdata(pdev, priv);
dev_set_drvdata(priv->dev, priv);
dev_info(priv->dev, "%s driver probe okay.\n", DEV_NAME);
/*class_create->name , device_create->name */
dev_info(&pdev->dev, "%s driver probe okay.\n", DEV_NAME);
/*driver->name, device->name(devic_tree_node->name)*/
return rv;
}
remove函数
/* @description: 设备卸载时候执行
*
* @parm : pdev - 设备指针
* @return: 0 successfully , !0 failure
*/
static int gpio_w1_remove(struct platform_device *pdev)
{
struct gpio_w1_priv *priv = platform_get_drvdata(pdev);//临时存放私有属性的结构体
dev_t devno = MKDEV(dev_major, 0);
//删除sys中的属性
device_remove_file(priv->dev, &dev_attr_temp);
device_destroy(priv->dev_class, devno); /*注销每一个设备号*/
class_destroy(priv->dev_class); //注销类
cdev_del(&priv->cdev); //删除cdev
unregister_chrdev_region(MKDEV(dev_major, 0), DEV_CNT);//释放设备号
gpiod_set_value(w1_gpiod, 0); //低电平关闭灯
gpiod_put(w1_gpiod); //释放gpiod
devm_kfree(&pdev->dev, priv); //释放堆
printk("gpio w1 driver removed.\n");
return 0;
}
/* @description: 设备停止执行的函数
*
* @parm : pdev - 设备指针
* @return: void
*/
static void imx_w1_shutdown(struct platform_device *pdev)
{
struct gpio_w1_priv *priv = platform_get_drvdata(pdev);
gpiod_set_value(w1_gpiod, 0);
}
3.6.2.2.4.实现字符设备操作函数集
在.probe函数中添加了一个字符设备,ds18b20的初始化以及转换结果的读取都在这个字符设备的操作函数中实现,其中最主要是.open和.read函数,下面是这两个函数实现。
.open函数实现,具体代码如下:
/* @description: 打开设备
*
* @parm : inode - 传递给驱动的inode
* @parm : filp - 设备文件,file结构体有个叫做private_data的成员变量
* * 一般在open的时候将private_data指向设备结构体。
* @return: 0 successfully , !0 failure
*/
static int w1_open(struct inode *inode, struct file *file)
{
struct gpio_w1_priv *priv;
printk("%s():%d w1_open platform driver\n", __FUNCTION__, __LINE__);
priv = container_of(inode->i_cdev, struct gpio_w1_priv, cdev);//找到私有数据的地址
file->private_data = priv;
return 0;
}
.read函数实现,调用DS18B20_SampleData函数获取温度值。
/* @description: 从设备读取文件
* @parm : filp - 设备文件,文件描述符
* @parm : buf - 返回给用户空间的数据缓冲区
* @parm : cnt - 要读取的数据长度
* @parm : offt - 相对于文件首地址的偏移
* @return: 读取的字节数,负数 - 读取失败
*/
static ssize_t w1_read(struct file *filp, char __user *buf, size_t cnt, loff_t *off)
{
int rv = 0;
uint16_t temp = 0;
struct gpio_w1_priv *priv = filp->private_data;
temp = DS18B20_SampleData(priv);//获取温度值
printk("%s():%d temp %x \n", __FUNCTION__, __LINE__, temp);
rv = copy_to_user(buf, &temp, sizeof(temp));
if(rv)
{
dev_err(priv->dev, "copy to user error.\n");
return -EFAULT;
}
else
{
rv = sizeof(temp);
}
return rv;
}
/* @description: 关闭设备
*
* @parm : inode - 传递给驱动的inode
* @parm : filp - 设备文件,file结构体有个私有数据可以使用
* @return: 0 successfully , !0 failure
*/
static int w1_release(struct inode *inode, struct file *file)
{
printk("%s():%d w1_release w1 platform driver\n", __FUNCTION__, __LINE__);
return 0;
}
/*字符设备操作函数集*/
static struct file_operations w1_fops = {
.owner = THIS_MODULE,
.open = w1_open,
.read = w1_read,
.release = w1_release,
};
3.6.2.2.5.sysfs - 温度属性显示函数
/* @description: sysfs - 温度属性显示函数
*
* @parm : devp - 设备指针,创建file时候会指定dev
* @parm : attr - 设备属性,创建时候传入
* @parm : buf - 传出给sysfs中显示的buf
* @return: 显示的字节数
*/
static ssize_t temp_show(struct device *devp, struct device_attribute *attr, char *buf)
{
u_int16_t temp = 0;
struct gpio_w1_priv *priv = dev_get_drvdata(devp);
temp = DS18B20_SampleData(priv);
return sprintf(buf, "t=%d\n", temp*62);//1000倍
}
/* @description: sysfs - echo写入属性函数
* @parm : dev - 设备指针,创建file时候会指定dev
* @parm : attr - 设备属性,创建时候传入
* @parm : buf - 用户空间的buf
* @parm : count - 传入buf的size
* @return: 写入的buf大小
*/
static ssize_t temp_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{
return count;
}
/*声明并初始化一个 device_attribute结构体*/
DEVICE_ATTR(temp, 0644, temp_show, temp_store);
使用这个函数时要引用 device_create所返回的device*指针,作用是在/sys/class/下创建一个属性文件,从而通过对这个属性文件进行读写就能完成对应的数据操作。
3.6.2.2.6.DS18B20操作底层函数
1.设置宏来控制gpio,以便后续操作。
/*DS18B20 接收发送*/
#define DS18B20_In_init() gpiod_direction_input(w1_gpiod)//设置GPIO的方向为输出,默认电平为高
#define DS18B20_Out_init() gpiod_direction_output(w1_gpiod, HIGH)//设置GPIO的方向为输出,默认电平为高
#define DS18B20_DQ_IN() gpiod_get_value(w1_gpiod)
#define DS18B20_DQ_OUT(N) gpiod_set_value(w1_gpiod, N)
2.DS18B20起始信号
复位按照时序图操作,先拉低,等待至少480us后再拉高,延迟70us,读取当前电平,如果被拉低,则设备存在,如果被拉高,则没有。而后延迟一段时间后释放总线。
/* @description: 主机发送起始信号,主机拉低 >=480us,释放总线(主机拉高),等待15~60us
* @return: 0 successfully , !0 failure
*/
static int DS18B20_start(void)
{
int rv;
/*主机设置为输出*/
DS18B20_Out_init();//设置GPIO的方向为输出,默认电平为高
/*主机拉低 >=480us*/
DS18B20_DQ_OUT(LOW);
w1_delay(480);
/*主机拉高 15~60us*/
DS18B20_DQ_OUT(HIGH);
w1_delay(70);
/*主机设置为输入*/
DS18B20_In_init();
/*总线变低说明从设备发送了响应信号:60~240us*/
rv = DS18B20_DQ_IN();
if(0 != rv)
{
printk("%s():%d DS18B20_no respond \n", __FUNCTION__, __LINE__);
return -EFAULT;
}
printk("%s():%d DS18B20_respond \n", __FUNCTION__, __LINE__);
w1_delay(10);
DS18B20_Out_init();
DS18B20_DQ_OUT(HIGH);/*释放总线*/
return rv;
}
3.读信号
/* @description: 主机读取一个位,整个读周期最少需要60us,启动读开始信号后必须15us内读取IO电平,否则就会被上拉拉高
*
* @return: uint8_t bit
*/
static uint8_t DS18B20_readBit(void)
{
uint8_t bit = 0;
/*主机再设置成输出模式准备开始读数据*/
DS18B20_Out_init();//设置GPIO的方向为输出,默认电平为高
/*主机拉低 >=1,表示开始读数据*/
DS18B20_DQ_OUT(LOW);
w1_delay(2);
DS18B20_In_init();
w1_delay(10);
/*判断数据是0还是1*/
bit = DS18B20_DQ_IN();
w1_delay(50);
return bit;
}
/* @description: 读取一个字节
*
* @return: uint8_t Bety
*/
static uint8_t DS18B20_readBety(void)
{
uint8_t i,Bety = 0;
uint8_t bit;
for(i=0; i<8; i++)
{
bit = DS18B20_readBit();
if(bit)
Bety |= (0x01 << i);
// Bety |= DS18B20_readBit();/*MSB最先读到的是最高位*/
// Bety <<= 1;
}
printk("%s():%d DS18B20_readBit %x \n", __FUNCTION__, __LINE__, Bety);
return Bety;
}
4.写信号
/* @description: ds18b20 写一个位
*
* @parm : bit - 一个字
* @parm :
* @return:
*/
static void DS18B20_writeBit(unsigned char bit)
{
DS18B20_Out_init();
/* 判断 bit */
bit = bit > 1 ? 1 : bit;
w1_delay(50);
DS18B20_DQ_OUT(LOW);
w1_delay(2);
DS18B20_DQ_OUT(bit);
w1_delay(60);
DS18B20_DQ_OUT(HIGH);
w1_delay(12);
}
/* @description: ds18b20 写一个字节
*
* @parm : bit - 一个字
* @parm :
* @return:
*/
static void DS18B20_writeBety(uint8_t Bety)
{
uint8_t i = 0;
for(i=0; i<8; i++)
{
DS18B20_writeBit((Bety >> i)&0x01);
}
printk("%s():%d DS18B20_writeBety %x \n", __FUNCTION__, __LINE__, Bety);
}
3.6.2.2.7.获取ds18b20温度值
/* @description: 读取温度值
*
* @parm : struct gpio_w1_priv *priv 私有数据结构体
* @parm :
* @return: uint16_t bit
*/
static uint16_t DS18B20_SampleData(struct gpio_w1_priv *priv)
{
unsigned long flags;
uint16_t temp = 0;
uint8_t temp_H = 0;
uint8_t temp_L = 0;
int rv = 0;
spin_lock_irqsave(&priv->lock, flags);
/*主机发起起始信号,并且从设备回应*/
if(0 != DS18B20_start())
{
printk("%s():%d DS18B20_start err \n", __FUNCTION__, __LINE__);
rv = -EFAULT;
goto undo_spin_unlock;
}
DS18B20_writeBety(SKIP_ROM);
DS18B20_writeBety(Convert_T);
spin_unlock_irqrestore(&priv->lock, flags);
msleep(750);
spin_lock_irqsave(&priv->lock, flags);
if(0 != DS18B20_start())
{
printk("%s():%d DS18B20_start err \n", __FUNCTION__, __LINE__);
rv = -EFAULT;
goto undo_spin_unlock;
}
DS18B20_writeBety(SKIP_ROM);
DS18B20_writeBety(Read_Data);
temp_L = DS18B20_readBety();
temp_H = DS18B20_readBety();
spin_unlock_irqrestore(&priv->lock, flags);
temp = (temp_H << 8) | temp_L;
//printk("%s():%d temp_H:%x, temp_L:%x ,temp %x\n", __FUNCTION__, __LINE__, temp_H, temp_L, temp);
return temp;
undo_spin_unlock:
spin_unlock_irqrestore(&priv->lock, flags);
return rv;
}
3.6.2.3.DS18B20测试应用程序
/* 在C语言编程时,一般系统的头文件用<xxx.h>,我们自己写的头文件则用"zzz.h" */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <string.h>
#include <time.h>
#include <errno.h>
/* 在C语言编程中,函数应该先定义再使用,如果函数的定义在函数调用后面,应该前向声明。*/
int ds18b20_get_temperature(float *temp);
int main(int argc, char *argv[])
{
float temp; /* 温度值有小数位,所以使用浮点数 */
/* 1,在Linux下做C语言编程时,函数返回值一般是0表示成功,<0表示失败,我们也遵循这个规约;
* 2,但函数调用只能有一个返回值,所以这里的采样函数只能通过指针来返回采样的温度值;
* 3,因为要在ds18b20_get_temperature()函数中修改main()中temp的值,所以这里传&temp;
*/
if( ds18b20_get_temperature(&temp) < 0 )
{
printf("ERROR: ds18b20 get temprature failure\n");
return 1;
}
/* 打印DS18B20的采样温度值,因为℃是非ASCII打印字符,所以这里用 'C 代替 */
printf("DS18B20 get temperature: %f 'C\n", temp);
return 0;
}
/*
* 函数说明: 该函数用来使用 DS18B20 温度传感器采样获取当前的温度值;
* 参数说明: $temp: 通过指针返回DS18B20的采样温度
* 返回说明: ==0 表示成功, <0 表示失败
*/
int ds18b20_get_temperature(float *temp)
{
const char *w1_path = "/sys/class/w1_ds18b20/w1_ds18b20/";/*my_ds18b20_driver*/
char ds_path[50]; /* DS18B20 采样文件路径 */
char buf[128]; /* read() 读数据存储 buffer */
DIR *dirp; /* opendir()打开的文件夹句柄 */
int fd =-1; /* open()打开文件的文件描述符 */
char *ptr; /* 一个字符指针,用来字符串处理 */
int rv = 0; /* 函数返回值,默认设置为成功返回(0) */
/* 在C语言编程时,进入函数的第一件事应该进行函数参数的合法性检测,检查参数非法输入。
* 否则调用者"不小心"通过 $temp 传入一个空指针,下面的代码就有可能出现段错误。
*/
if( !temp )
{
return -1;
}
/*我的驱动 打开 "/sys/class/w1_ds18b20/w1_ds18b20/" 文件夹,如果打开失败则打印错误信息并退出。*/
if((dirp = opendir(w1_path)) == NULL)
{
printf("opendir error: %s\n", strerror(errno));
return -2;
}
snprintf(ds_path, sizeof(ds_path), "%s/temp", w1_path);/*my_ds18b20_driver*/
/* 接下来打开 DS18B20 的采样文件,如果失败则返回相应的错误码-4。 */
if( (fd=open(ds_path, O_RDONLY)) < 0 )
{
printf("open %s error: %s\n", ds_path, strerror(errno));
return -4;
}
/* 读取文件中的内容将会触发 DS18B20温度传感器采样,这里读取文件内容保存到buf中 */
if(read(fd, buf, sizeof(buf)) < 0)
{
printf("read %s error: %s\n", ds_path, strerror(errno));
/* 1, 这里不能直接调用 return -5 直接返回,否则的话前面open()打开的文件描述符就没有关闭。
* 这里设置 rv 为错误码-5,通过 goto 语句跳转到函数后面统一进行错误处理。
*
* 2, 在C语言编程时我们应该慎用goto语句进行"随意"的跳转,因为它会降低代码的可读性。但这里是
* goto语句的一个非常典型应用,我们经常会用它来对错误进行统一的处理。
*
* 3,goto后面的cleanup为标号,它在下面的代码中定义。
*/
rv = -5;
goto cleanup;
}
/* 采样温度值是在字符串"t="后面,这里我们从buf中找到"t="字符串的位置并保存到ptr指针中 */
ptr = strstr(buf, "t=");
if( !ptr )
{
printf("ERROR: Can not get temperature\n");
rv = -6;
goto cleanup;
}
/* 因为此时ptr是指向 "t="字符串的地址(即't'的地址),那跳过2个字节(t=)后面的就是采样温度值 */
ptr+=2;
/* 接下来我们使用 atof() 函数将采样温度值字符串形式,转化成 float 类型。*/
*temp = atof(ptr)/1000;
/* 1,在这里我们对函数返回进行集中处理,其中 cleanup 为 goto 语句的标号;
* 2,在函数退出时,我们应该考虑清楚在前面的代码中做了哪些事,这些事是否需要进行反向操作。如
* 打开的文件或文件夹是否需要关闭,malloc()分配的内存是否需要free()等。
* 3, 在最开始我们定义rv并赋初值为0(表示成功)是有原因的,如果前面的代码任何一个地方出现错误,
* 则会将rv的值修改为相应的错误码,否则rv的值将始终为0(即没有错误发生),这里将统一返回。
*/
cleanup:
close(fd);
return rv;
}
3.6.2.4.Makefile
KERNAL_DIR := /home/zying/imx6ull/bsp/kernel/linux-imx
CROSS_COMPILE := /opt/gcc-arm-11.2-2022.02/bin/arm-none-linux-gnueabihf-
TFTP_DTR := /tftp/zouying
PWD := $(shell pwd)
obj-m := w1_ds18b20.o
modules:
$(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules
$(CROSS_COMPILE)gcc w1_sys_App.c -o w1_sys_App
@make clear
cp w1_ds18b20.ko w1_sys_App $(TFTP_DTR) -f
clear:
@rm -f *.o *.cmd *.mod *.mod.c
@rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f
@rm -f .*ko.cmd .*.o.cmd .*.o.d
@rm -f *.unsigned
clean:
@rm -f *.ko
3.6.3.程序运行结果
3.6.3.1.加载设备树
在config.txt文件中添加w1.dtbo
# Enable LCD overlay
dtoverlay_lcd=yes
# Enable Camera overlay
#dtoverlay_cam=no
# Enable I2C overlay
dtoverlay_i2c1=yes
# Enable SPI overlay, SPI1 conflict with UART8(NB-IoT/4G module)
dtoverlay_spi1=yes
# Enable UART overlays
dtoverlay_uart=2 3 4 7
# Enable CAN overlays
#dtoverlay_can=1 2
# Enable PWM overlays, PWM8 conflict with UART8(NB-IoT/4G module)
#dtoverlay_pwm=7 8
# Enable extra overlays
#dtoverlay_extra=w1 adc oled_spi
dtoverlay_extra=w1
然后sync一下再reboot开发板,sync就是把刚刚修改的config.txt配置写入uboot,防止修改没有保存成功。
重启开发板后就能看到新增的w1文件启动成功。
3.6.3.2.硬件连接
DS18B20三个引脚VCC和GND连接到相应的位置,DQ连接到设备树配置的地址,这里连接的是GPIO1_IO18。
3.6.3.3.测试结果
在开发板上安装模块
root@igkboard:~/driver/06w1_ds18b20# insmod w1_ds18b20.ko
root@igkboard:~/driver/06w1_ds18b20# ls
w1_App w1_ds18b20.ko
cat /sys/class/w1_ds18b20/w1_ds18b20/ temp 可以看到温度值
在这里第一个w1_ds18b20是类名,第二个w1_ds18b20是设备名,第三个 temp是我们在device下创建的文件名,然后温度值保存在这里。
root@igkboard:~# cd /sys/class/w1_ds18b20/w1_ds18b20/
root@igkboard:/sys/class/w1_ds18b20/w1_ds18b20# cat temp
t=24118
运行app
root@igkboard:~/dirver/06w1_ds18b20# ./w1_App
temperature : 25.3125
temperature : 25.3125