想必看过我很多次博客的同学,都知道了编写驱动的流程!
这里我们还是按照以前的习惯来一步一步讲解!
单总线驱动,在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);
}
else
printf("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);
else
gpio_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 */
else
ds18b20_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);
else
byte &= ~(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); // >=480us
ds18b20_set_input(dev); // 释放总线(高阻)
/* 在 15~300us 窗口内轮询检测存在脉冲(低电平) */
for (i = 0; i < 60; i++) { // 60 * 5us = 300us
udelay(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);
}
else
printf("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.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
在全部完成之后呢,我又把单总线的GPIO口换成别的地方了,是STM32MP157的PZ6。
需要做以下改动;
在pinctrl-z下添加:
c
&pinctrl_z {
ds18b20_pins: ds18b20-0 {
pins1 {
pinmux = <STM32_PINMUX('Z', 6, GPIO)>; // PZ6 设为 GPIO
drive-open-drain; // 开漏输出(释放=高阻)
bias-pull-up; // 上拉(仍建议外部4.7k)
slew-rate = <0>;
};
};
在根节点"/"下追加:
c
ds18b20@0 {
compatible = "alientek,ds18b20";
pinctrl-names = "default";
pinctrl-0 = <&ds18b20_pins>;
ds18b20-gpio = <&gpioz 6 GPIO_ACTIVE_HIGH>;
status = "okay";
即可,配置电气属性,大家如果想换成别的IO口,就需要知道IO口有没有被占用,被占用就要解除占用噢!