文章目录
全系列传送门
Linux嵌入式驱动开发01——第一个驱动Hello World(附源码)
Linux嵌入式驱动开发06——第一个相对完整的驱动实践编写
Linux嵌入式驱动开发07——GPIO驱动过程记录(飞凌开发板)
Linux嵌入式驱动开发11——平台总线模型修改为设备树实例
Linux嵌入式驱动开发12——pinctl和gpio子系统实践操作
Linux嵌入式驱动开发13——ioctl接口(gpio控制使用)
Linux嵌入式驱动开发14——中断的原理以及按键中断的实现(tasklet中断下文)
平台总线模型介绍
-
什么是平台总线模型?
平台总线模型也叫platform总线模型,是
Linux内核虚拟出来的一条总线,不是真正的导线。平台总线模型实际上就是把原来的驱动C文件分成了两个C文件,一个是device.c,一个是driver.c
把稳定不变的放在driver.c里面,需要变动的放在device.c里面
-
为什么会有平台总线模型?
(1). 提高代码的重用性
(2). 减少重复性代码
设备(device.c)
总线
驱动(driver.c) -
平台总线模型的优点
除了上面提到的,还有就是都挂载在了总线上,方便管理。 -
怎么编写以平台总线模型设计的驱动?
一个是device.c,一个是driver.c,然后分别注册device.c和driver.c。平台总线是以名字来匹配的,实际上就是字符串比较
注册platform设备(device)
device.c里面写的是硬件资源,这里的硬件资源是指寄存器的地址,终端号,时钟等硬件资源。
platform_device结构体
在Linux的内核中,我们用一个结构体来描述硬件资源
/include/linux/platform_device.h
platform_device
就是我们要使用的结构体
struct platform_device { // platform总线设备
const char * name; // 平台设备的名字
int id; // ID 是用来区分如果设备名字相同的时候(通过在后面添加一个数字来代表不同的设备,因为有时候有这种需求)
struct device dev; // 内置的device结构体
u32 num_resources; // 资源结构体数量
struct resource * resource; // 指向一个资源结构体数组
const struct platform_device_id *id_entry; // 用来进行与设备驱动匹配用的id_table表
/* arch specific additions */
struct pdev_archdata archdata; // 自留地 添加自己的东西
};
其中 结构体中的成员变量:
- 平台设备的名字,平台总线进行匹配的时候用到的name,/sys/bus…
const char * name;
- 设备ID,一般写-1
int id;
- 内置的device结构体
struct device dev;
- 资源的个数
u32 num_resources;
- device里面的硬件资源
struct resource * resource;
在ioport.h中可以找到resource的结构体定义,主要用来描述硬件资源
struct resource { // 资源结构体
resource_size_t start; // 资源的起始值,如果是地址,那么是物理地址,不是虚拟地址
resource_size_t end; // 资源的结束值,如果是地址,那么是物理地址,不是虚拟地址
const char *name; // 资源名
unsigned long flags; // 资源的标示,用来识别不同的资源
struct resource *parent, *sibling, *child; // 资源指针,可以构成链表
};
在这里还有很多的宏定义来表示地址,常用的有以下几个
- IO的内存
#define IORESOURCE_IO 0x00000100 /* PCI/ISA I/O ports */
- 表述一段物理内存
#define IORESOURCE_MEM 0x00000200
- 表示中断
#define IORESOURCE_IRQ 0x00000400
- dma的地址
#define IORESOURCE_DMA 0x00000800
- 总线号
#define IORESOURCE_BUS 0x00001000
这里使用开发板上的EIM_A17
,通过/arch/arm/boot/dts/imx6q-pinfunc.h
中查找
我们可以知道对应的是GPIO2_IO21
.,然后去数据手册,查找引脚的配置信息
根据数据手册得到了代码
实战代码
Linux嵌入式platform设备模块modules_platform_device
device.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
struct resource beep_resource[] = {
[0] = {
.start = 0x20a0000,
.end = 0x20a0003,
.flags = IORESOURCE_MEM,
.name = "GPIO2_DR"
}
};
struct platform_device beep_device = { // platform总线设备
.name = "beep_device_test", // 平台设备的名字
.id = -1, // ID 是用来区分如果设备名字相同的时候(通过在后面添加一个数字来代表不同的设备,因为有时候有这种需求)
.num_resources = ARRAY_SIZE(beep_resource), // 资源结构体数量
.resource = beep_resource, // 指向一个资源结构体数组
};
static int device_init(void)
{
printk("beep_device ok!!!\n"); // 在内核中无法使用c语言库,所以不用printf
return platform_device_register(&beep_device); // platform_device_register有返回值,所以要判断
}
static void device_exit(void)
{
printk("beep_device byb!!!\n");
platform_device_unregister(&beep_device);
}
module_init(device_init);
module_exit(device_exit);
MODULE_LICENSE("GPL"); //声明模块拥有开源许可
注意: 这里其实是不全的,因为我们的这里只配置了DR寄存器,没有配置电器属性,这个需要在设备树中配置,会放在后面。这里我是配置过了,所以才能后正常的实现代码的功能。
好,我们继续走
验证总线设备
首先安装模块
insmod device.ko
可以看到,加载成功
然后去查看是不是在总线上
ls /sys/bus/platform/devices/
我们可以找到我们代码中设置的平台设备的名字
然后我们测试注销功能
rmmod device.ko
发现有警告,提示需要release
回到代码进行添加修改
重新编译验证,没有了警告,问题解决
修改后的代码device.c:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
struct resource beep_resource[] = {
[0] = {
.start = 0x20a0000,
.end = 0x20a0003,
.flags = IORESOURCE_MEM,
.name = "GPIO2_DR"
}
};
void beep_release(struct device *dev){
printk("beep_release ok!!!\n");
}
struct platform_device beep_device = { // platform总线设备
.name = "beep_device_test", // 平台设备的名字
.id = -1, // ID 是用来区分如果设备名字相同的时候(通过在后面添加一个数字来代表不同的设备,因为有时候有这种需求)
.num_resources = ARRAY_SIZE(beep_resource), // 资源结构体数量
.resource = beep_resource, // 指向一个资源结构体数组
.dev = {
.release = beep_release,
},
};
static int device_init(void)
{
printk("beep_device ok!!!\n"); // 在内核中无法使用c语言库,所以不用printf
return platform_device_register(&beep_device); // platform_device_register有返回值,所以要判断
}
static void device_exit(void)
{
printk("beep_device byb!!!\n");
platform_device_unregister(&beep_device);
}
module_init(device_init);
module_exit(device_exit);
MODULE_LICENSE("GPL"); //声明模块拥有开源许可
注册platform驱动(driver)
思路
首先定义一个platform_driver
结构体变量,然后去实现结构体中的各个成员变量,那么当我们的driver和device匹配成功的时候,就会执行probe函数
,所以匹配成功以后的重点就在于probe函数
的编写
platform_driver
struct platform_driver {
int (*probe)(struct platform_device *); // 这个probe函数其实和 device_driver中的是一样的功能,但是一般是使用device_driver中的那个
int (*remove)(struct platform_device *); // 卸载平台设备驱动的时候会调用这个函数,但是device_driver下面也有,具体调用的是谁这个就得分析了
void (*shutdown)(struct platform_device *);
int (*suspend)(struct platform_device *, pm_message_t state);
int (*resume)(struct platform_device *);
struct device_driver driver; // 内置的device_driver 结构体
const struct platform_device_id *id_table; // 该设备驱动支持的设备的列表 他是通过这个指针去指向 platform_device_id 类型的数组
};
- 当
driver
和device
匹配成功的时候,就会执行probe函数
int (*probe)(struct platform_device *);
- 当
driver
和device
任意一个remove的时候,就会执行这个函数
int (*remove)(struct platform_device *);
- 当设备收到shutdown命令的时候,就会执行这个函数
void (*shutdown)(struct platform_device *);
- 当设备收到suspend命令的时候,就会执行这个函数
int (*suspend)(struct platform_device *, pm_message_t state);
- 当设备收到resume命令的时候,就会执行这个函数
int (*resume)(struct platform_device *);
对于device_driver 结构体,在device.h文件下,我们可以打开查看
linux-4.1.15/include/linux/device.h
struct device_driver {
const char *name;
struct bus_type *bus;
struct module *owner;
const char *mod_name; /* used for built-in modules */
bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
const struct of_device_id *of_match_table;
const struct acpi_device_id *acpi_match_table;
int (*probe) (struct device *dev);
int (*remove) (struct device *dev);
void (*shutdown) (struct device *dev);
int (*suspend) (struct device *dev, pm_message_t state);
int (*resume) (struct device *dev);
const struct attribute_group **groups;
const struct dev_pm_ops *pm;
struct driver_private *p;
};
注意:const char *name
;这个名字是匹配设备时候用到的名字,就是之前在device.c
中的platform_device
里面定义的名字
代码
driver.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
int beep_probe(struct platform_device *pdev){
printk("beep_probe ok!!!\n");
return 0;
}
int beep_remove(struct platform_device *pdev){
printk("beep_remove ok!!!\n");
return 0;
}
const struct platform_device_id beep_id_table = {
.name = "beep_device_test",
};
struct platform_driver beep_device = {
.probe = beep_probe, // 这个probe函数其实和 device_driver中的是一样的功能,但是一般是使用device_driver中的那个
.remove = beep_remove, // 卸载平台设备驱动的时候会调用这个函数,但是device_driver下面也有,具体调用的是谁这个就得分析了
.driver = {
.owner = THIS_MODULE,
.name = "beep_device_test", // 有了id_table,这个driver->name可有可无,优先级低
}, // 内置的device_driver 结构体
.id_table = &beep_id_table, // 该设备驱动支持的设备的列表 他是通过这个指针去指向 platform_device_id 类型的数组
};
static int beep_driver_init(void)
{
int ret = 0;
printk("beep_driver_init ok!!!\n"); // 在内核中无法使用c语言库,所以不用printf
ret = platform_driver_register(&beep_device);
if(ret < 0){
printk("platform_driver_register error!!!\n");
return ret;
}else{
printk("platform_driver_register ok!!!\n");
}
return 0;
}
static void beep_driver_exit(void)
{
printk("beep_driver_exit bye!!!\n");
platform_driver_unregister(&beep_device);
}
module_init(beep_driver_init);
module_exit(beep_driver_exit);
MODULE_LICENSE("GPL"); //声明模块拥有开源许可
结果验证
分别加载我们的
insmod device.ko
和
insmod driver.ko
然后卸载一下
这样我们就可以在driver.c文件中添加我们所需要的功能代码了,驱动蜂鸣器,或者点亮led灯等操作
编写probe函数
第一步:从device.c里面获得硬件资源
方法一:直接获得,不推荐
方法二:使用函数间接获得
打开platform_device.h
/include/linux/platform_device.h
找到函数platform_get_resource
extern struct resource *platform_get_resource(struct platform_device *,
unsigned int, unsigned int);
参数:
第一个参数:形参pdev
第二个参数:类型,在device中
第三个参数:同类参数的第几个,指的是.flags = IORESOURCE_MEM,,同类型的第几个
根据我们的device,第一个参数填写形参pdev,第二个参数填写类型IORESOURCE_MEM,因为IORESOURCE_MEM类型中这是第一个,所以第三个参数是0
platform_get_resource(pdev, IORESOURCE_MEM, 0);
注意:函数有返回值
代码
probe部分的代码
int beep_probe(struct platform_device *pdev){
printk("beep_probe ok!!!\n");
//方法1
// printk("beep_res is %s\n", pdev->resource[0].name);
//方法2
beep_mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if(beep_mem == NULL) {
printk("beep_get_resource is NULL!!!\n");
return -EBUSY;
}
printk("beep_resource start is 0x%x!!!\n", beep_mem->start);
printk("beep_resource end is 0x%x!!!\n", beep_mem->end);
return 0;
}
结果验证
insmod device.ko
insmod driver.ko
第二步:注册杂项/字符设备,完善file_operation,生成设备节点
注册前要先登记
登记到内核request_mem_region
request_mem_region函数和release_mem_region函数在ioport.h文件中
/include/linux/ioport.h
#define request_mem_region(start,n,name) __request_region(&iomem_resource, (start), (n), (name), 0)
#define release_mem_region(start,n) __release_region(&iomem_resource, (start), (n))
登记到内核的原因是,因为需要告诉内核需要用到某个寄存器,别的设备不可以重复使用。
我们这里使用到了GPIO2的DR寄存器,不登记也可以。一般都是要进行登记操作
beep_mem_tmp = request_mem_region(beep_mem->start, beep_mem->end - beep_mem->start + 1, "beep");
if(beep_mem_tmp == NULL){
printk("request_mem_region is error!!!\n");
goto err_region; // 没有注册成功,需要释放掉
}
完善file_operation
配置杂项设备驱动,现在只需要从probe函数中操作就可以了
/********probe编写***********************************************************************/
int beep_probe(struct platform_device *pdev){
int ret = 0;
printk("beep_probe ok!!!\n");
//方法1
// printk("beep_res is %s\n", pdev->resource[0].name);
//方法2
/*********获取硬件资源*********************************************************************************/
beep_mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if(beep_mem == NULL) {
printk("beep_get_resource is NULL!!!\n");
return -EBUSY;
}
printk("beep_resource start is 0x%x!!!\n", beep_mem->start);
printk("beep_resource end is 0x%x!!!\n", beep_mem->end);
/********登记******************************************************************************************/
// beep_mem_tmp = request_mem_region(beep_mem->start, beep_mem->end - beep_mem->start + 1, "beep");
// if(beep_mem_tmp == NULL){
// printk("request_mem_region is error!!!\n");
// goto err_region; // 没有注册成功,需要释放掉
// }
/*******杂项设备驱动ioremap转化成虚拟地址**************************************************************/
vir_gpio5_dr = ioremap(beep_mem->start, 4); // 物理地址到虚拟地址的映射
if(vir_gpio5_dr == NULL){ //如果映射失败
printk("GPIO2_DR ioremap error!!!\n");
return -EBUSY; //EBUSY是Linux的预留参数,前面加个负号
}else{
printk("GPIO2_DR ioremap ok!!!\n");
}
/*******ioremap之后注册杂项设备********************************************************************/
ret = misc_register(&misc_dev);
if(ret < 0){
printk("misc_register failed!!!\n");
return -1;
}
printk("misc_register succeed!!!\n"); // 在内核中无法使用c语言库,所以不用printf
/********注册之后,完善file_operation*******************************************************************/
return 0;
// err_region:
// release_mem_region(beep_mem->start, beep_mem->end - beep_mem->start + 1);
// return -EBUSY;
}
之前的杂项设备注册是在init函数中,现在在probe函数中,因为probe函数是设备和驱动匹配后执行的函数。
结果验证
加载模块
ls /dev/
可以看到我们的杂项设备已经成功注册
运行app.c的可执行文件
源码下载
Linux嵌入式platform驱动模块modules_platform_dirver
Linux嵌入式驱动开发——平台总线实例modules_platform
总结
在probe函数我们可以拿到device里面描述的硬件资源,然后在probe里面注册杂项设备和字符设备,这样就可以完成一个以总线模型设计的驱动
引出设备树
device本质上是描述我们的底板硬件资源,所以,对于不同的soc,不同的底版,就有了不同的device,为了规范这些,所以有了设备树
设备树文件本质上就是我们device部分
设备树是一种描述硬件资源的数据结构,它通过bootloader将硬件资源传递给内核,使得内核和硬件资源描述相对独立。
这样,与开发板底版相关的代码,就不在Linux内核中,减少了内核的臃肿,这些硬件信息都在设备树中。