itop-3568开发板驱动学习笔记(21)平台总线模型
《【北京迅为】itop-3568开发板驱动开发指南.pdf》 学习笔记
文章目录
平台总线模型简介
基本概念
平台总线模型也叫 platform 总线模型。平台总线是 Linux 系统虚拟出来的总线(而不像 i2c,usb 等实实在在存在的硬件总线)。
平台总线模型将一个驱动分成了两个部分,分别是 device.c 和 driver.c。device.c 里写设备有关的驱动代码,driver.c 里写关于控制相关的代码。
使用方法
平台总线通过字符比较,将 name 相同的 device.c 和 driver.c 匹配到一起来控制底层硬件。
特点
- 减少驱动重复代码:同一份 driver.c 可以用在多款开发板上
- 提高代码的利用率:更换开发板只需要修改 device.c 的中断和地址等信息
注册 platform 设备
platform 设备驱动 (device.c)里写的是硬件资源(寄存器地址、中断号等)
platform 设备结构体
struct platform_device
struct platform_device 是平台设备中 device 的结构体,其被定义在 include/linux/platform_device.h 中,
struct platform_device
{const char *name; // 设备名,与驱动层匹配int id; // 用于区分设备,一般设为 -1(不设置 id)bool id_auto; // 一般不用struct device dev; // 主要用于传递数据u32 num_resources; // 设备占用资源个数struct resource *resource; // 设备占用资源的首地址struct platform_device_id *id_entry; // 设备 id 入口char *driver_override; struct mfd_cell *mfd_cell;struct pdev_archdata archdata;
}
struct resource *resource
struct platform_device 中最重要的成员为 struct resource *resource,其被定义在 include/linux/ioport.h 中,
struct resource
{resource_size_t start; // 资源起始物理地址resource_size_t end; // 资源结束物理地址const char *name; // 资源名称unsigned long flags; // 资源类型struct resource *parent, *sibling, *child;
};
flags 可取值可以在 include/linux/ioport.h 中查看,比如 IORESOURCE_IO、IORESOURCE_IRQ 分别代表 IO 资源和中断资源。
platform 设备加载函数
int platform_device_register(struct platform_device *pdev);
功能:向内核注册一个平台设备
参数:pdev 表示要注册的 struct platform_device 结构体指针
返回值:注册成功返回 0,注册失败返回负数
int platform_add_devices(struct platform_device **devs, int num)
功能:向内核注册 num 个平台设备
参数:pdevs 表示要注册的 struct platform_device 结构体指针数组,num 为 pdevs 数组的元素个数
返回值:注册成功返回 0,注册失败返回负数
platform 设备卸载函数
void platform_device_unregister(struct platform_device *pdev)
功能:把平台设备 pdev 从内核中删除
参数:pdev 表示要删除的 struct platform_device 结构体指针
platform 设备实验
驱动代码
该代码参考迅为教程视频(B站)
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>static struct resource my_device_resources[] =
{[0] = {.start = 0xFDD60000,.end = 0xFDD60004,.flags = IORESOURCE_MEM,},[1] = {.start = 13,.end = 13,.flags = IORESOURCE_IRQ,}
};void my_device_release(struct device *dev)
{printk("my_device_release.\\n");
}struct platform_device my_device =
{.name = "mydev", .id = -1, // 不设置id.resource = my_device_resources,.num_resources = ARRAY_SIZE(my_device_resources),.dev = {.release = my_device_release,},
};static int __init my_device_init(void) // 驱动入口函数
{printk("my_device_init.\\n");//注册平台设备platform_device_register(&my_device);return 0;
}static void __exit my_device_exit(void) // 驱动出口函数
{printk("my_device_exit\\n");//注销平台设备platform_device_unregister(&my_device);
}module_init(my_device_init); //注册入口函数
module_exit(my_device_exit); //注册出口函数
MODULE_LICENSE("GPL v2"); //同意GPL协议
MODULE_AUTHOR("xiaohui"); //作者信息
Makefile
#目标文件,与驱动源文件同名,编译成模块
obj-m := platform_device.o#架构平台选择
export ARCH=arm64#编译器选择
export CROSS_COMPILE=aarch64-linux-gnu-#内核目录
KDIR := /home/topeet/Linux/rk356x_linux/kernel/#编译模块
all:make -C $(KDIR) M=$(shell pwd) modules#清除编译文件
clean:make -C $(KDIR) M=$(shell pwd) clean
实验结果
安装平台设备驱动文件后,可以在 /sys/bus/platform/device/ 目录下看到我们的 mydev,
注册 platform 驱动
platform 设备驱动(driver.c)里面写的是软件驱动。在 driver.c 文件中首先需要定义一个 platform_driver 结构体。然后去实现这个结构体中的各个成员变量。当 driver.c 和 device.c 匹配成功后,会执行 driver.c 里的 probe() 函数。
platform 驱动结构体
struct platform_driver
{int (*probe)(struct platform_device *); // 探测函数int (*remove)(struct platform_device *); // 移除函数void (*shutdown)(struct platform_device *); // 关闭设备int (*suspend)(struct platform_device *, pm_message_t state); // 挂起函数int (*resume)(struct platform_device *); // 唤醒函数struct device_driver driver; // 其中的 name 成员用于匹配struct platform_device_id *id_table;
};
该结构体的 probe,remove 两个函数成员时必须实现的,struct device_driver 结构体的 name 成员用于与 device.c 匹配,所以 driver 成员也是需要定义的。
platform 驱动注册函数
int platform_driver_register(struct platform_driver *drv);
功能:向内核注册一个平台驱动
参数:drv表示要注册的 struct platform_driver 结构体指针
返回值:注册成功返回 0,注册失败返回负数
platform 驱动注销函数
void platform_driver_unregister(struct platform_driver *drv);
功能:把平台驱动 drv 从内核中删除
参数:drv 表示要删除的 struct platform_driver 结构体指针
platform 驱动实验
驱动代码
该代码参考迅为教程视频(B站)
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>int my_driver_probe(struct platform_device *dev)
{printk("my_driver_probe.\\n");return 0;
}int my_driver_remove(struct platform_device *dev)
{printk("my_driver_remove.\\n");return 0;
}// id 匹配表(可以为数组),里面的 name 比 driver.name 优先级高
struct platform_device_id my_driver_id_table =
{.name = "mydev",
};struct platform_driver my_driver =
{.probe = my_driver_probe,.remove = my_driver_remove,.driver = {.name = "mydev", // 与 device.c 的 name 匹配.owner = THIS_MODULE,},.id_table = &my_driver_id_table,
};static int __init my_driver_init(void) // 驱动入口函数
{printk("my_driver_init.\\n");//注册平台驱动platform_driver_register(&my_driver);return 0;
}static void __exit my_driver_exit(void) // 驱动出口函数
{printk("my_driver_exit\\n");//注销平台驱动platform_driver_unregister(&my_driver);
}module_init(my_driver_init); //注册入口函数
module_exit(my_driver_exit); //注册出口函数
MODULE_LICENSE("GPL v2"); //同意GPL协议
MODULE_AUTHOR("xiaohui"); //作者信息
Makefile
#目标文件,与驱动源文件同名,编译成模块
obj-m := platform_driver.o#架构平台选择
export ARCH=arm64#编译器选择
export CROSS_COMPILE=aarch64-linux-gnu-#内核目录
KDIR := /home/topeet/Linux/rk356x_linux/kernel/#编译模块
all:make -C $(KDIR) M=$(shell pwd) modules#清除编译文件
clean:make -C $(KDIR) M=$(shell pwd) clean
实验结果
先安装 platform_device 驱动,然后安装 platform_driver 驱动,内核会运行 probe 函数,表示设备和驱动匹配成功,当有一方卸载后,platform_driver 里的 remove 函数被执行,表示匹配断开。
编写 probe 函数
平台总线模型中,硬件信息都在设备 device.c 中,要想让驱动 driver.c 得到 device.c 中的硬件资源,需要在 device 和 driver 匹配后,执行 probe() 函数,在该函数中让 driver 得到 device 中的硬件资源。
probe 中获取设备资源的函数
struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num);
功能:从平台设备 dev 中获取资源
参数:
- dev 平台设备结构体指针
- type 资源类型
- num 索引,不同类型资源索引分开计算(索引从 0 开始计算)
返回值:成功返回 resource 资源指针,失败返回 NULL
int platform_get_irq(struct platform_device *dev, unsigned int num);
功能:从平台设备 dev 中获取中断编号
参数:
- dev 平台设备结构体指针
- num 索引,不同类型资源索引分开计算(索引从 0 开始计算)
返回值:成功返回中断号,失败返回 -ENXIO
platform 驱动实验
驱动代码
该代码参考迅为教程视频(B站)
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>struct resource *tmp_resource;
int irq;int my_driver_probe(struct platform_device *dev)
{printk("my_driver_probe.\\n");// 直接获取 device 的资源printk("Mothod 1: IRQ is %lld\\n", dev->resource[1].start);// 通过 API 获取 device 资源tmp_resource = platform_get_resource(dev, IORESOURCE_IRQ, 0);printk("Mothod 2: IRQ is %lld\\n", tmp_resource->start);// 通过 API 获取 device IRQirq = platform_get_irq(dev, 0);printk("Mothod 3: IRQ is %d\\n", irq);// 通过 API 获取 device 资源tmp_resource = platform_get_resource(dev, IORESOURCE_MEM, 0);printk("MEM is %#llx\\n", tmp_resource->start);return 0;
}int my_driver_remove(struct platform_device *dev)
{printk("my_driver_remove.\\n");return 0;
}// id 匹配表(可以为数组),里面的 name 比 driver.name 优先级高
struct platform_device_id my_driver_id_table =
{.name = "mydev",
};struct platform_driver my_driver =
{.probe = my_driver_probe,.remove = my_driver_remove,.driver = {.name = "mydev", // 与 device.c 的 name 匹配.owner = THIS_MODULE,},.id_table = &my_driver_id_table,
};static int __init my_driver_init(void) // 驱动入口函数
{printk("my_driver_init.\\n");//注册平台驱动platform_driver_register(&my_driver);return 0;
}static void __exit my_driver_exit(void) // 驱动出口函数
{printk("my_driver_exit\\n");//注销平台驱动platform_driver_unregister(&my_driver);
}module_init(my_driver_init); //注册入口函数
module_exit(my_driver_exit); //注册出口函数
MODULE_LICENSE("GPL v2"); //同意GPL协议
MODULE_AUTHOR("xiaohui"); //作者信息
Makefile
#目标文件,与驱动源文件同名,编译成模块
obj-m := platform_driver.o#架构平台选择
export ARCH=arm64#编译器选择
export CROSS_COMPILE=aarch64-linux-gnu-#内核目录
KDIR := /home/topeet/Linux/rk356x_linux/kernel/#编译模块
all:make -C $(KDIR) M=$(shell pwd) modules#清除编译文件
clean:make -C $(KDIR) M=$(shell pwd) clean
实验结果
device 和 driver 匹配后,运行 driver 的 probe 函数,该函数从 device 获取硬件资源,然后打印到串口终端。(使用了三种方法获取中断号资源)
led 驱动(平台总线方式)
平台设备代码
设备代码中的 GPIO_DR 和 GPIO_DDR 来自 itop-3568开发板驱动学习笔记(5) 点灯实验,为 LED GPIO 的寄存器地址。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>#define GPIO_DDR 0xFDD60008 // 数据方向寄存器地址
#define GPIO_DR 0xFDD60000 // 数据寄存器地址static struct resource my_device_resources[] =
{[0] = {.start = GPIO_DR,.end = GPIO_DDR,.flags = IORESOURCE_MEM,},[1] = {.start = 13,.end = 13,.flags = IORESOURCE_IRQ,}
};void my_device_release(struct device *dev)
{printk("my_device_release.\\n");
}struct platform_device my_device =
{.name = "mydev", .id = -1, // 不设置id.resource = my_device_resources,.num_resources = ARRAY_SIZE(my_device_resources),.dev = {.release = my_device_release,},
};static int __init my_device_init(void) // 驱动入口函数
{printk("my_device_init.\\n");//注册平台设备platform_device_register(&my_device);return 0;
}static void __exit my_device_exit(void) // 驱动出口函数
{printk("my_device_exit\\n");//注销平台设备platform_device_unregister(&my_device);
}module_init(my_device_init); //注册入口函数
module_exit(my_device_exit); //注册出口函数
MODULE_LICENSE("GPL v2"); //同意GPL协议
MODULE_AUTHOR("xiaohui"); //作者信息
Makefile
#目标文件,与驱动源文件同名,编译成模块
obj-m := platform_device.o#架构平台选择
export ARCH=arm64#编译器选择
export CROSS_COMPILE=aarch64-linux-gnu-#内核目录
KDIR := /home/topeet/Linux/rk356x_linux/kernel/#编译模块
all:make -C $(KDIR) M=$(shell pwd) modules#清除编译文件
clean:make -C $(KDIR) M=$(shell pwd) clean
平台驱动代码
该代码结合了上一份 driver 代码和 itop-3568开发板驱动学习笔记(5) 点灯实验 的 ioremap 点灯实验驱动代码,其中 led 字符驱动的 init 函数内容需要拷贝到 driver.c 的 probe 函数中,原字符驱动 exit 函数内容拷贝到 driver.c 的 remove 函数中。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/io.h>struct resource *tmp_resource;
int irq;// 定义一个私有数据结构体
struct my_device
{dev_t dev_num; // 设备号int major; // 主设备号int minor; // 次设备号struct cdev st_cdev;struct class *st_class;struct device *st_device;char kbuf[32];unsigned int *gpio_ddr; // io 数据方向unsigned int *gpio_dr; // io 高低电平// 地址数据类型千万不能写小了!!!
};// 定义一个全局私有数据结构体
struct my_device dev1;// open()
static int chrdev_open(struct inode *inode , struct file *file )
{file->private_data = &dev1; // 设置私有数据printk("chrdev_open.\\n");return 0;
}// close()
static int chrdev_release(struct inode *inode, struct file *file)
{printk("chrdev_release.\\n");return 0;
}// write()
static ssize_t chrdev_write(struct file *file , const char __user *buf, size_t size, loff_t *off)
{struct my_device *tmp_dev = (struct my_device*)file->private_data;unsigned int tmp;int ret = copy_from_user(tmp_dev->kbuf, buf, size); // 从应用空间读取数据if(ret != 0){printk("copy_from_user error.\\r\\n");return -1;}if(tmp_dev->kbuf[0] == 1) // 如果读到 1,点亮 LED{*(tmp_dev->gpio_dr) |= 0x80008000; // GPIO0_B7 高电平}else if(tmp_dev->kbuf[0] == 0) // 读到 0,熄灭 LED{tmp = *(dev1.gpio_dr) | 0x80000000; // 数据寄存器使能(中间值)*(dev1.gpio_dr) = tmp & ~(0x00008000); // GPIO0_B7 低电平// 实测:数据寄存器写使能必须和 IO 电平值同时写入寄存器,所以这里用到了中间变量}return 0;
}// read()
static ssize_t chrdev_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{printk("chrdev_read.\\n");return 0;
}static struct file_operations chrdev_fops = {.owner = THIS_MODULE, //将 owner 成员指向本模块,可以避免在模块的操作正在被使用时卸载该模块.open = chrdev_open, //将 open 成员指向 chrdev_open()函数.read = chrdev_read, //将 read 成员指向 chrdev_read()函数.write = chrdev_write,//将 write 字段指向 chrdev_write()函数.release = chrdev_release,//将 release 字段指向 chrdev_release()函数
};int my_driver_probe(struct platform_device *dev)
{int ret;printk("my_driver_probe.\\n");// 通过 API 获取 device 资源tmp_resource = platform_get_resource(dev, IORESOURCE_MEM, 0);printk("MEM is %#llx\\n", tmp_resource->start);// 自动获取设备号(只申请一个,次设备号从 0 开始)ret = alloc_chrdev_region(&dev1.dev_num, 0, 1, "chrdev_test");if(ret < 0){goto err_alloc;}printk("alloc chrdev region successfully.\\n");dev1.major = MAJOR(dev1.dev_num); // 获取主设备号dev1.minor = MINOR(dev1.dev_num); // 获取次设备号printk("major is %d.\\nminor is %d\\n", dev1.major, dev1.minor);dev1.st_cdev.owner = THIS_MODULE; // 将 owner 成员指向本模块,可以避免模块 st_cdev 被使用时卸载模块cdev_init(&dev1.st_cdev, &chrdev_fops); // 初始化字符设备 ret = cdev_add(&dev1.st_cdev, dev1.dev_num, 1); // 将字符设备添加到系统if(ret < 0){goto err_cdev_add;}printk("cdev add successfully.\\n");dev1.st_class = class_create(THIS_MODULE, "chrdev_class"); // 创建设备类if(IS_ERR(dev1.st_class)){ret = PTR_ERR(dev1.st_class); // 返回错误码goto err_class_create;}dev1.st_device = device_create(dev1.st_class, NULL, dev1.dev_num, NULL, "chrdev_device"); // 创建设备if(IS_ERR(dev1.st_device)){ret = PTR_ERR(dev1.st_device); // 返回错误码goto err_device_create;}// 将物理地址转化为虚拟地址(寄存器为 32 位,所以映射 4 字节)dev1.gpio_ddr = ioremap(tmp_resource->end, 4); // IO 数据方向if(IS_ERR(dev1.gpio_ddr)){ret = PTR_ERR(dev1.gpio_ddr); // 返回错误码goto err_ddr_ioremap;}// 1000 0000 1000 0000 *(dev1.gpio_ddr) |= 0x80008000; // GPIO0_B7 设置输出模式dev1.gpio_dr = ioremap(tmp_resource->start, 4); // IO 电平if(IS_ERR(dev1.gpio_dr)){ret = PTR_ERR(dev1.gpio_dr); // 返回错误码goto err_dr_ioremap;}return 0;err_dr_ioremap:iounmap(dev1.gpio_dr); // 取消映射err_ddr_ioremap:iounmap(dev1.gpio_ddr); // 取消映射err_device_create:class_destroy(dev1.st_class); // 删除类err_class_create:cdev_del(&dev1.st_cdev); // 删除 cdeverr_cdev_add:unregister_chrdev_region(dev1.dev_num, 1); // 注销设备号err_alloc:return ret; // 返回错误号}int my_driver_remove(struct platform_device *dev)
{printk("my_driver_remove.\\n");iounmap(dev1.gpio_dr); // 取消映射iounmap(dev1.gpio_ddr); // 取消映射device_destroy(dev1.st_class, dev1.dev_num); // 删除设备class_destroy(dev1.st_class); //删除设备类cdev_del(&dev1.st_cdev); // 删除字符设备unregister_chrdev_region(dev1.dev_num, 1); // 注销设备号return 0;
}// id 匹配表(可以为数组),里面的 name 比 driver.name 优先级高
struct platform_device_id my_driver_id_table =
{.name = "mydev",
};struct platform_driver my_driver =
{.probe = my_driver_probe,.remove = my_driver_remove,.driver = {.name = "mydev", // 与 device.c 的 name 匹配.owner = THIS_MODULE,},.id_table = &my_driver_id_table,
};static int __init my_driver_init(void) // 驱动入口函数
{printk("my_driver_init.\\n");//注册平台驱动platform_driver_register(&my_driver);return 0;
}static void __exit my_driver_exit(void) // 驱动出口函数
{printk("my_driver_exit\\n");//注销平台驱动platform_driver_unregister(&my_driver);
}module_init(my_driver_init); //注册入口函数
module_exit(my_driver_exit); //注册出口函数
MODULE_LICENSE("GPL v2"); //同意GPL协议
MODULE_AUTHOR("xiaohui"); //作者信息
Makefile
#目标文件,与驱动源文件同名,编译成模块
obj-m := platform_driver.o#架构平台选择
export ARCH=arm64#编译器选择
export CROSS_COMPILE=aarch64-linux-gnu-#内核目录
KDIR := /home/topeet/Linux/rk356x_linux/kernel/#编译模块
all:make -C $(KDIR) M=$(shell pwd) modules#清除编译文件
clean:make -C $(KDIR) M=$(shell pwd) clean
应用层代码
这份测试代码摘自 itop-3568开发板驱动学习笔记(5) 点灯实验,用于控制 LED 闪烁。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>#define DEV_FILE "/dev/chrdev_device"int main(int argc, char** argv)
{int fd, tmp;int ret = 0;// 打开设备文件fd = open(DEV_FILE, O_RDWR, 0644);if(fd < 0){printf("%s open failed.\\n", DEV_FILE);return 0;}printf("%s open successfully.\\n", DEV_FILE);// 调用 write()while(1){tmp = 1;write(fd, &tmp, 1);sleep(1);tmp = 0;write(fd, &tmp, 1);sleep(1);}// 关闭设备文件close(fd);return 0;
}
测试结果
device 和 driver 配对后,成功运行 driver 的 probe 函数,在该函数里实现 led 字符设备的创建,随后运行 app 来控制 led 闪烁。
led 闪烁: