想必看过我很多次博客的同学,都知道了编写驱动的流程!
这里我们还是按照以前的习惯来一步一步讲解!
单总线驱动,在F103和51单片机的裸机开发中是经常见的。
linux驱动代码编写实际上就是,端对端的编程!
就是 硬件-连接-软件
一开始是主芯片的设备树和镜像,配置硬件,该执行哪条总线,端口。
二就是编写外设写入或者读取数据的文件,还有类似QT的代码执行。
三就是需要把需要驱动的硬件目标和软件操作文件进行匹配。也就是将第一步和第二步相匹配。
1、配置设备树
打开 stm32mp157d-atk.dts 文件,把以下的内容添加到此文件中
与之前大多数不同的是,这里并不是节点追加的方式。是新创建的。
这里就不用配置镜像了,因为没有用到追加节点。就用到一个GPIO口。
一般来说下面我们这个代码用到了platform框架,那么就需要用到pinctrl用来配置电气属性的,但是这里正点原子并没有加pinctrl,可能是因为该引脚复位后默认就是 GPIO 功能,就无需 pinctrl 配置 “复用为 GPIO”,但是这样并不规范!
编译:
make dtbs
复制到开发板中:
sudo cp arch/arm/boot/dts/stm32mp157d-atk.dtb /home/chensir/linux/tftpboot/ -f
发现可以在设备树下发现我们刚刚创建的节点;
2、DS18B20驱动编写
之前的博客也是跟大家按照肌肉记忆来编写程序!一步一步按照思路来编写!
总代码会放在最后。
为了让大家更能明白,可以先对着总代码,进行对我的写代码流程更加详细得当!
2.1、头文件
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/miscdevice.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/errno.h>
可以看出头文件用到了miscdevice.h,说明这个例程代码用到了MISC子系统。
2.2、驱动注册和注销
因为我这个DS18B20外设的单总线是在linux没有总线概念的,所以用platform总线来实现总线驱动框架,这个是前面已经讲过的!这个仅仅为了简化代码量!
注册和注销一体化:这个意思是init和exit不用开发人员写了!
可以看以下举例代码:
2.2.1、编写platform_driver驱动结构体
其中流程是:
16行代码:设备树中的compatible值会与ds18b20_of_match下的compatible相匹配。
如下图ds18b20_of_match的代码:
其中MODULE_DEVICE_TABLE是声明一下而已!
15行代码:会在driver目录下生成ds18b20。这个是驱动开发者自己编写的!
和设备树中的compatible没有关系。
18~19行代码:compatible值一旦匹配成功,就会执行probe和remove。(这些其实之前的驱动程序讲解已经讲过很多遍了)
2.2.2、编写probe和remove函数
2.2.3、注册和注销字符驱动设备
我们这里用到的MISC子系统和platform框架,所以可以回顾。
我们用这个的同时需要在设备结构体中定义MISC设备。
虽然说MISC子系统帮开发人员自动设置了主设备号为10,但是子设备号、设备名、字符操作集还是得开发人员自己创建。
1、定义设备结构体
struct miscdevice mdev;
这里可以放在probe函数内,但是如果需要适应创建多个MISC设备,那么就放在设备结构体中;
这里我们要强调一下;
这两种写法要好好记一下!
2、配置probe和remove函数
这里我们先编译测试一下;
发现并没有问题!
接下来继续完善probe和remove函数:
可以看到我们添加了名字、次设备号、字符操作集函数。
接下来注册和注销MISC设备:
==重点来了!!!==我们发现,只有在probe函数内,才动态分配了内存,在remove是没有分配,不能傻傻的再用devm_kmallo函数了,在 probe
函数中动态分配的,remove
函数无法直接访问,需要通过 platform_set_drvdata
和 platform_get_drvdata
传递指针。
接下来继续完善:
在probe内使用 platform_set_drvdata(pdev, ds18b20_dev);
在remove内使用 ds18b20_dev = platform_get_drvdata(pdev);
3、配置字符操作集
目前这个单总线DS18B20,功能实现只需要读数据就行,所以字符操作集只涉及,在本模块下执行、open、release、read即可!
在完成写完字符操作集之前,我们先来回顾DS18B20的时序,需要严格特定的时序,还有数据判定。然后再上传数据。
4、获取设备节点(设备树属性)
4.1、配置设备树结构体
4.2、GPIO初始化
使用了 devm_gpio_request
(带 devm_
前缀的资源申请函数)—— 这类函数申请的 GPIO 会与「设备生命周期」自动绑定,无需手动调用 gpio_free
,内核会在设备卸载时自动释放 GPIO。
这里知道了有关驱动的gpio信息,仅仅是能知道信息,并没有驱动能力,所以要向内核申请权限来驱动gpio口。
5、配置DS18B20时序
熟悉DS18B20配置的同学就知道,需要严格的时序,还有高低电平转换!所以接下来我们需要配置高低电平、定时器、还有可以把这个读写时序放到队列里面!
5.1、配置输入输出
需要配置输入输出!
因为需要获取DS18B20的温度数据,所以需要判定GPIO的值。
5.2、获取GPIO的值
5.3、设置定时器
5.4、配置工作队列
其中:
struct ds18b20_dev *ds18b20_dev = container_of(work, struct ds18b20_dev, work);container_of需要放在处理函数中,通过工作队列(work)反向找到设备结构体(匹配设备)
5.5、完善定时任务
5.6、完成DS18B20的时序
5.6.1、初始化DS18B20
5.6.2、写入一位数据
5.6.3、读取一位数据
5.6.4、写一个字节到DS18B20
5.6.5、从DS18B20读取一个字节
5.7、完善字符操作集
5.8、完善工作队列
编译生成ko文件:
make
复制到
sudo cp ds18b20.ko /home/chensir/linux/nfs/rootfs/lib/modules/5.4.31/
6、编写测试 APP
这里其实也简单,就是传递2个数据!
核心代码:
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"int main()
{int fd, ret;unsigned char result[2];int TH, TL;short tmp = 0;float temperature;int flag = 0;fd = open("/dev/ds18b20", 0);if(fd < 0){perror("open device failed\n");exit(1);}elseprintf("Open success!\n");while(1){ret = read(fd, &result, sizeof(result)); if(ret == 0) { /* 读取到数据 */TL = result[0];TH = result[1];if((TH == 0XFF) && (TL == 0XFF))/* 如果读取到数据为0XFFFF就跳出本次循序 */continue;if(TH > 7) { /* 负数处理 */TH = ~TH;TL = ~TL;flag = 1; /* 标记为负数 */}tmp = TH;tmp <<= 8;tmp += TL;if(flag == 1) {temperature = (float)(tmp+1)*0.0625; /* 计算负数的温度 */temperature = -temperature;}else {temperature = (float)tmp *0.0625; /* 计算正数的温度 */} if(temperature < 125 && temperature > -55) { /* 温度范围 */printf("Current Temperature: %f\n", temperature);}}else if(ret == -1){perror("read"); break;}flag = 0;sleep(1);}close(fd); /* 关闭文件 */
}
编译:
arm-none-linux-gnueabihf-gcc ds18b20App.c -o ds18b20App
复制到
sudo cp ds18b20App /home/chensir/linux/nfs/rootfs/lib/modules/5.4.31/
8、效果
9、总代码:
ds18b20.c:
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/miscdevice.h>
#include <linux/delay.h>
/* #include <linux/ide.h> */
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/errno.h>
#include <linux/uaccess.h>
#include <linux/timer.h>
#include <linux/workqueue.h>/*ds18b20设备结构体*/
struct ds18b20_dev{struct miscdevice mdev; /* MISC设备 */struct device_node *nd; //设备树节点指针int ds18b20_gpio; //GPIO编号unsigned char data[2]; /* 接收原始数据的BUFF */struct timer_list timer; /* 定时器 */struct work_struct work; /* 工作队列 */
};#define HIGH 1
#define LOW 0/** @description : 设置GPIO的输出值* @param - value: 输出value的值 * @return : 无*/
static void ds18b20_set_output(struct ds18b20_dev *dev, int value)
{if(value)gpio_direction_output(dev->ds18b20_gpio, 1);elsegpio_direction_output(dev->ds18b20_gpio, 0);
}/** @description : 设置GPIO为输入模式* @param : 无* @return : 无*/
static void ds18b20_set_input(struct ds18b20_dev *dev)
{gpio_direction_input(dev->ds18b20_gpio);
}/** @description : 获取GPIO的值* @param : 无 * @return : GPIO的电平*/
static int ds18b20_get_io(struct ds18b20_dev *dev)
{return gpio_get_value(dev->ds18b20_gpio);
}/** @description : 写一位数据* @param bit : 要写入的位数* @return : 无*/
static void ds18b20_write_bit(struct ds18b20_dev *dev, int bit)
{local_irq_disable();if(bit) {ds18b20_set_output(dev, LOW);udelay(5);ds18b20_set_input(dev); /* 释放为高阻 */udelay(55); /* 补足到 ~60us */} else {ds18b20_set_output(dev, LOW);udelay(60); /* 写0保持低 ~60us */ds18b20_set_input(dev); /* 释放为高阻 */udelay(5);}local_irq_enable();
}/** @description : 读一位数据* @param : 无* @return : 返回读取一位的数据*/
static int ds18b20_read_bit(struct ds18b20_dev *dev)
{u8 bit = 0;local_irq_disable();ds18b20_set_output(dev, LOW);udelay(2);ds18b20_set_input(dev);udelay(12);bit = ds18b20_get_io(dev) ? 1 : 0;udelay(45);local_irq_enable();return bit;
}/** @description : 写一个字节到DS18B20* @param byte : 要写入的字节* @return : 无*/
static void ds18b20_write_byte(struct ds18b20_dev *dev, u8 byte)
{int i;for(i = 0; i < 8; i++) {if(byte & 0x01)ds18b20_write_bit(dev,1); /* write 1 */elseds18b20_write_bit(dev,0); /* write 0 */byte >>= 1; /* 右移一位获取高一位的数据 */}
}/** @description : 读取一个字节的数据* @param : 无* @return : 读取到的数据*/
static char ds18b20_read_byte(struct ds18b20_dev *dev)
{int i;u8 byte = 0;for(i = 0; i < 8; i++) { /* DS18B20先输出低位数据 ,高位数据后输出 */if(ds18b20_read_bit(dev))byte |= (1 << i);elsebyte &= ~(1 << i);}return byte;
}/** @description : GPIO的初始化函数* @param pdev : platform设备 * @return : 0表示转换成功,其它值表示转换失败*/
static int ds18b20_request_gpio(struct platform_device *pdev)
{struct device *dev = &pdev->dev;struct ds18b20_dev *ds18b20_dev = platform_get_drvdata(pdev);int ret;ds18b20_dev->nd = dev->of_node;if (!ds18b20_dev->nd)return -EINVAL;ds18b20_dev->ds18b20_gpio = of_get_named_gpio(ds18b20_dev->nd, "ds18b20-gpio", 0);if (!gpio_is_valid(ds18b20_dev->ds18b20_gpio))return -EINVAL;ret = devm_gpio_request(dev, ds18b20_dev->ds18b20_gpio, "DS18B20 Gpio");if (ret)return ret;ds18b20_set_input(ds18b20_dev);return 0;
}/** @description : 初始化DS18B20* @param : 无* @return : 0,初始化成功,1,失败*/
static int ds18b20_init(struct ds18b20_dev *dev)
{int ret = 1; // 默认失败int i;ds18b20_set_input(dev);udelay(10); // 总线稳定时间ds18b20_set_output(dev, LOW); // 拉低复位udelay(500); // >=480usds18b20_set_input(dev); // 释放总线(高阻)/* 在 15~300us 窗口内轮询检测存在脉冲(低电平) */for (i = 0; i < 60; i++) { // 60 * 5us = 300usudelay(5);if (ds18b20_get_io(dev) == LOW) {ret = 0; // 初始化成功,检测到存在脉冲break;}}/* 等待存在脉冲结束 */udelay(240);ds18b20_set_input(dev); // 保持释放return ret;
}/** @description : 打开设备* @param - inode : 传递给驱动的inode* @param - filp : 设备文件,file结构体有个叫做pr似有ate_data的成员变量* 一般在open的时候将private_data似有向设备结构体。* @return : 0 成功;其他 失败*/
static int ds18b20_open(struct inode *inode, struct file *filp)
{struct miscdevice *mdev = filp->private_data; /* 由 misc_open 预先设置 */struct ds18b20_dev *ds18b20 = dev_get_drvdata(mdev->this_device);filp->private_data = ds18b20;return 0;
}
/** @description : 从设备读取数据 * @param - filp : 要打开的设备文件(文件描述符)* @param - buf : 返回给用户空间的数据缓冲区* @param - cnt : 要读取的数据长度* @param - offt : 相对于文件首地址的偏移* @return : 读取的字节数,如果为负值,表示读取失败*/
static ssize_t ds18b20_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{struct ds18b20_dev *ds18b20 = filp->private_data;size_t n = 2;if (!ds18b20)return -ENODEV;if (cnt < n)n = cnt;if (copy_to_user(buf, &ds18b20->data[0], n))return -EFAULT;return 0;
}static int ds18b20_release(struct inode *inode, struct file *filp)
{return 0;
}static struct file_operations ds18b20_fops = {.owner = THIS_MODULE,.open = ds18b20_open,.read = ds18b20_read,.release = ds18b20_release,
};/** @description : 使用内核的工作队列,获取温度的原始数据* @param - work : work的结构体* @return : 无*/
static void ds18b20_work_callback(struct work_struct *work)
{int ret;struct ds18b20_dev *dev = container_of(work, struct ds18b20_dev, work);ret = ds18b20_init(dev);if (ret)return;ds18b20_write_byte(dev, 0XCC);ds18b20_write_byte(dev, 0X44);msleep(750);ds18b20_set_input(dev);ret = ds18b20_init(dev);if (ret)return;ds18b20_write_byte(dev, 0XCC);ds18b20_write_byte(dev, 0XBE);dev->data[0] = ds18b20_read_byte(dev);dev->data[1] = ds18b20_read_byte(dev);
}/** @description : 定时器的操作函数,每1s去获取一次数据* @param - asg : 定时器的结构体* @return : 无*/
/* 定时器回调:每秒触发一次采集 */
static void ds18b20_timer_callback(struct timer_list *arg)
{struct ds18b20_dev *dev = from_timer(dev, arg, timer);schedule_work(&dev->work);mod_timer(&dev->timer, jiffies + msecs_to_jiffies(1000));
}/*驱动的probe函数,当驱动与设备匹配以后此函数就会执行*/
static int ds18b20_probe(struct platform_device *pdev)
{int ret;struct miscdevice *mdev;struct ds18b20_dev *ds18b20_dev;dev_info(&pdev->dev, "ds18b20 device and driver matched successfully!\n");ds18b20_dev = devm_kzalloc(&pdev->dev, sizeof(*ds18b20_dev), GFP_KERNEL);if (!ds18b20_dev) {return -ENOMEM;}platform_set_drvdata(pdev, ds18b20_dev);/* GPIO的初始化 */ret = ds18b20_request_gpio(pdev);if(ret) {return ret;}mdev = &ds18b20_dev->mdev;mdev->name = "ds18b20";mdev->minor = MISC_DYNAMIC_MINOR;mdev->fops = &ds18b20_fops;ret=misc_register(mdev);if(ret < 0){dev_info(&pdev->dev, "ds18b20 MISC match fail!\n");return -ENODEV;}/* 绑定 drvdata,供 open 通过 mdev->this_device 找回 */if (mdev->this_device)dev_set_drvdata(mdev->this_device, ds18b20_dev);/* 初始化定时器 */timer_setup(&ds18b20_dev->timer, ds18b20_timer_callback, 0);ds18b20_dev->timer.expires=jiffies + msecs_to_jiffies(1000);add_timer(&ds18b20_dev->timer);/* 初始化工作队列 */INIT_WORK(&ds18b20_dev->work, ds18b20_work_callback);return 0;
}/*驱动的remove函数,移除驱动的时候此函数会执行*/
static int ds18b20_remove(struct platform_device *pdev)
{int ret;struct miscdevice *mdev;struct ds18b20_dev *ds18b20_dev;dev_info(&pdev->dev, "DS18B20 driver has been removed!\n");ds18b20_dev = platform_get_drvdata(pdev);mdev = &ds18b20_dev->mdev;misc_deregister(mdev); /* 卸载定时器 */del_timer(&ds18b20_dev->timer); /* 卸载工作队列 */cancel_work_sync(&ds18b20_dev->work);return 0;
}static const struct of_device_id ds18b20_of_match[] = {{ .compatible = "alientek,ds18b20" },{ /* Sentinel */ }
};
MODULE_DEVICE_TABLE(of,ds18b20_of_match);/*platform驱动结构体*/
static struct platform_driver ds18b20_driver = {.driver = {.name = "ds18b20",.of_match_table = ds18b20_of_match,},.probe = ds18b20_probe,.remove = ds18b20_remove,
};/*注册和注销一体化*/
module_platform_driver(ds18b20_driver);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("chensir");
MODULE_INFO(intree, "Y");
ds18b20App.c:
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"int main()
{int fd, ret;unsigned char result[2];int TH, TL;short tmp = 0;float temperature;int flag = 0;fd = open("/dev/ds18b20", 0);if(fd < 0){perror("open device failed\n");exit(1);}elseprintf("Open success!\n");while(1){ret = read(fd, &result, sizeof(result)); if(ret == 0) { /* 读取到数据 */TL = result[0];TH = result[1];if((TH == 0XFF) && (TL == 0XFF))/* 如果读取到数据为0XFFFF就跳出本次循序 */continue;if(TH > 7) { /* 负数处理 */TH = ~TH;TL = ~TL;flag = 1; /* 标记为负数 */}tmp = TH;tmp <<= 8;tmp += TL;if(flag == 1) {temperature = (float)(tmp+1)*0.0625; /* 计算负数的温度 */temperature = -temperature;}else {temperature = (float)tmp *0.0625; /* 计算正数的温度 */} if(temperature < 125 && temperature > -55) { /* 温度范围 */printf("Current Temperature: %f\n", temperature);}}else if(ret == -1){perror("read"); break;}flag = 0;sleep(1);}close(fd); /* 关闭文件 */
}
makefile:
KERNELDIR := /home/chensir/linux/atk-mp1/linux/my_linux/linux-5.4.31
CURRENT_PATH := $(shell pwd)
obj-m := ds18b20.obuild: kernel_moduleskernel_modules:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
在全部完成之后呢,我又把单总线的GPIO口换成别的地方了,是STM32MP157的PZ6。
需要做以下改动;
在pinctrl-z下添加:
&pinctrl_z {ds18b20_pins: ds18b20-0 {pins1 {pinmux = <STM32_PINMUX('Z', 6, GPIO)>; // PZ6 设为 GPIOdrive-open-drain; // 开漏输出(释放=高阻)bias-pull-up; // 上拉(仍建议外部4.7k)slew-rate = <0>;};};
在根节点“/”下追加:
ds18b20@0 {compatible = "alientek,ds18b20";pinctrl-names = "default";pinctrl-0 = <&ds18b20_pins>;ds18b20-gpio = <&gpioz 6 GPIO_ACTIVE_HIGH>;status = "okay";
即可,配置电气属性,大家如果想换成别的IO口,就需要知道IO口有没有被占用,被占用就要解除占用噢!