平台总线
上一章节介绍的总线、设备、驱动框架可以很好的实现设备和驱动的分离,降低设备和驱动之间的耦合性,但是在Linux中存在许多没有实际物理总线的设备,如led、按键等,为了方便这些设备使用总线、设备、驱动框架,Linux就提供了platform(平台)总线,platform(平台)总线是一条虚拟总线,其目的是为了让没有具体物理总线的设备能够使用总线、设备、驱动框架。
Linux内核中平台总线相关的代码在\linux-5.4.31\drivers\base\platform.c文件中,如下分别是定义总线和注册总线的代码:
其中需要重点关注的是platform_match函数,它决定了平台设备如何和平台驱动进行匹配
static int platform_match(struct device *dev, struct device_driver *drv)
{
struct platform_device *pdev = to_platform_device(dev);
struct platform_driver *pdrv = to_platform_driver(drv);
/* 1、如果平台设备的driver_override不为空则用平台设备的driver_override和平台驱动的name进行匹配 */
if (pdev->driver_override)
return !strcmp(pdev->driver_override, drv->name);
/* 2、使用设备树匹配,设备树compatible属性与drv->of_match_table进行匹配 */
if (of_driver_match_device(dev, drv))
return 1;
/* 3、ACPI匹配 */
if (acpi_driver_match_device(dev, drv))
return 1;
/* 4、如果平台驱动的id_table不为空则用平台驱动的id_table与平台设备进行匹配 */
if (pdrv->id_table)
return platform_match_id(pdrv->id_table, pdev) != NULL;
/* 5、用平台设备的名字与平台驱动的名字进行匹配 */
return (strcmp(pdev->name, drv->name) == 0);
}
平台设备
Linux内核中用 struct platform_device 描述一个平台设备,其核心成员如下:
//平台设备名称
const char *name;
//用于设备和驱动的强制匹配
char *driver_override;
//平台设备ID,用于区分不同的平台设备
int id;
//继承于device对象
struct device dev;
//资源数量
u32 num_resources;
//资源列表,使用设备树时会自动将设备树描述的中断资源和寄存器资源转换为资源列表
struct resource *resource;
可以通过下列函数注册、注销平台设备:
//注册平台设备
int platform_device_register(struct platform_device *pdev)
//注销平台设备
void platform_device_unregister(struct platform_device *pdev)
平台资源
平台设备的资源通过 struct resource 对象来描述,其核心成员如下:
//资源起始
resource_size_t start;
//资源结束
resource_size_t end;
//资源名称
const char *name;
//资源类型
//常用的资源类型有:
//IORESOURCE_MEM:内存资源
//IORESOURCE_IRQ:中断资源
//IORESOURCE_DMA:DMA资源
unsigned long flags;
可以通过下列宏定义来初始化资源:
//初始化IO资源
#define DEFINE_RES_IO_NAMED(_start, _size, _name)
#define DEFINE_RES_IO(_start, _size)
//初始化内存资源
#define DEFINE_RES_MEM(_start, _size)
#define DEFINE_RES_MEM_NAMED(_start, _size, _name)
//初始化中断资源
#define DEFINE_RES_IRQ_NAMED(_irq, _name)
#define DEFINE_RES_IRQ(_irq)
//初始化DMA资源
#define DEFINE_RES_DMA_NAMED(_dma, _name)
#define DEFINE_RES_DMA(_dma)
采用的操作资源的函数
//获取资源大小
resource_size_t resource_size(const struct resource *res)
//获取资源类型
unsigned long resource_type(const struct resource *res)
//获取资源
extern struct resource *platform_get_resource(struct platform_device *, unsigned int, unsigned int);
//映射IO资源,devm_表示模块卸载时自动取消映射,要求资源必须是IORESOURCE_MEM类型,index表示第index个IORESOURCE_MEM类型的资源
extern void __iomem *devm_platform_ioremap_resource(struct platform_device *pdev, unsigned int index);
//获取中断资源
extern int platform_get_irq(struct platform_device *, unsigned int);
//获取中断资源的数量
extern int platform_irq_count(struct platform_device *);
//通过名字获取资源
extern int platform_get_irq_byname(struct platform_device *, const char *);
平台驱动
Linux内核用 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 *);
//继承于device_driver对象
struct device_driver driver;
可以通过下列函数注册、注销平台驱动:
//注册平台驱动
#define platform_driver_register(drv)
//注销平台驱动
void platform_driver_unregister(struct platform_driver *);
//生成包含注册、注销平台驱动的模块加载卸载函数
#define module_platform_driver(__platform_driver)
驱动程序实现
驱动程序基于2.4在Linux内核中操作寄存器中的led驱动进行修改,整个驱动分为了平台设备和平台驱动两个部分,平台设备描述硬件的基本信息,如寄存器地址、led编号、设备文件名等,平台驱动则是根据平台设备提供的信息操作相应的寄存器,控制LED状态。
平台设备
平台设备用于描述设备的硬件信息,LED驱动的平台设备主要包括以下内容:
- LED配置参数;程序中提供一个结构体描述了LED的配置参数,包括时钟寄存器在时钟控制器中的偏移、时钟使能位于那个bit位、是哪个LED、设备文件名
//设备私有数据类型
struct led_config {
//时钟控制寄存器相对于时钟寄存器组的偏移
uint32_t rcc_offset;
//GPIO时钟控制位的偏移
uint32_t rcc_shif;
//引脚号
uint32_t pin_num;
//led标签,这里用于生成设备文件名
const char *labe;
};
//描述LED的配置参数
struct led_config led_config = {
.rcc_offset = 0xA28,
.rcc_shif = 8,
.pin_num = 0,
.labe = "test_led",
};
- 寄存器资源;提供资源表描述了时钟控制器和LED控制器的寄存器(内存)资源
//资源列表
static struct resource led_resource[] = {
[0] = DEFINE_RES_MEM(RCC_BASE, 4096),
[1] = DEFINE_RES_MEM(GPIOI_BASE, 128),
};
- 平台设备定义;初始化平台设备的资源列表、名字(目前设备和驱动通过名字进行匹配)、设备私有参数
//卸载时执行
static void led_release(struct device *dev)
{
struct platform_device *pdev = to_platform_device(dev);
printk("%s release\r\n", pdev->name);
}
//平台设备
static struct platform_device led_dev = {
.name = "myled", //设备名称
.id = 0, //ID,用于区分不同的设备
.num_resources = ARRAY_SIZE(led_resource), //资源数量
.resource = led_resource, //资源列表
.dev = {
.release = led_release, //设备卸载函数
.platform_data = &led_config, //设备私有数据
},
};
- 注册、注销平台设备;在模块加载函数中注册平台设备,在模块卸载函数中注销平台设备
static int __init plt_dev_init(void)
{
int err;
//注册平台设备
err = platform_device_register(&led_dev);
return err;
}
static void __exit plt_dev_exit(void)
{
//注销平台设备
platform_device_unregister(&led_dev);
}
平台驱动
平台驱动用于驱动设备,不同的设备个体拥有不同的资源(寄存器或配置参数等),但是他们的驱动程序可能是一样的,因此平台驱动最好同时支持多个设备,(对于多设备的支持我采用的链表来实现的,每匹配上一个设备,就为其分配一个唯一ID作为次设备号,然后为其创建一个设备句柄,将设备句柄放入到一个链表中,后续操作都可通过ID去链表中查找设备句柄),如下是一个同时支持多个LED设备的LED驱动,它主要包括以下部分:
- 模块加载函数;模块加载函数主要完成了设备号注册、cdev对象注册、class对象创建、平台驱动注册,一个主设备号可以同时有多个次设备号,一个cdev对象也可以同时管理多个字符设备,一个class对象下面也可以创建多个设备,所以在模块加载过程中就注册了设备号、cdev对象,创建了class对象,以后与此驱动匹配的设备都共用一个主设备号、cdev对象、class对象
//平台驱动
struct platform_driver led_drv = {
.driver = {
.name = "myled", //平台驱动名称
.owner = THIS_MODULE,
.pm = NULL,
},
.probe = pled_probe, //设备和驱动匹配成功执行
.remove = pled_remove, //设备或驱动卸载时执行
.shutdown = pled_shutdown, //系统关机前执行
.suspend = pled_suspend, //系统休眠前执行
.resume = pled_resume, //系统唤醒后执行
};
//设备操作函数集合
static struct file_operations led_ops = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_release,
.read = led_read,
.write = led_write,
};
static int __init plt_drv_init(void)
{
int result;
printk("%s\r\n", __FUNCTION__);
//根据次设备号起始值动态分配并注册字符设备号
result = alloc_chrdev_region(&led_num, 0, LED_NUMBER, "csdn,led");
if(result < 0)
{
printk("alloc chrdev failed\r\n");
return result;
}
printk("first device major %d, first device minor %d\r\n", MAJOR(led_num), MINOR(led_num));
//初始化CDEV对象
cdev_init(&led_cdev, &led_ops);
//向系统添加CDEV对象
result = cdev_add(&led_cdev, led_num, LED_NUMBER);
if(result < 0)
{
unregister_chrdev_region(led_num, LED_NUMBER);
printk("add cdev failed\r\n");
return result;
}
//创建class对象
led_class = class_create(THIS_MODULE, "led,class");
if(IS_ERR(led_class))
{
cdev_del(&led_cdev);
unregister_chrdev_region(led_num, LED_NUMBER);
printk("class create failed");
return PTR_ERR(led_class);
}
//注册平台驱动
result = platform_driver_register(&led_drv);
if(result < 0)
{
class_destroy(led_class);
cdev_del(&led_cdev);
unregister_chrdev_region(led_num, LED_NUMBER);
printk("add cdev failed\r\n");
return result;
}
return 0;
}
- 模块卸载函数;模块卸载函数主要实现模块加载函数的反操作
static void __exit plt_drv_exit(void)
{
printk("%s\r\n", __FUNCTION__);
//注销平台驱动
platform_driver_unregister(&led_drv);
//销毁class对象
class_destroy(led_class);
//从系统删除CDEV对象
cdev_del(&led_cdev);
//注销字符设备号
unregister_chrdev_region(led_num, LED_NUMBER);
}
- 多设备管理;在struct led_handle中定义了一个链表节点和id,当设备和驱动匹配时会为每个设备创建了一个struct led_handle对象,并分配一个唯一的ID,同时也将这个ID作为次设备号创建设备文件,然后将struct led_handle对象加入到一个链表中,当执行open等操作时便通过次设备号从链表中去查找设备,当设备和驱动解绑时从链表中删除对应的struct led_handle对象
//LED配置参数
struct led_config{
//时钟控制寄存器相对于时钟寄存器组的偏移
uint32_t rcc_offset;
//GPIO时钟控制位的偏移
uint32_t rcc_shif;
//引脚号
uint32_t pin_num;
//led标签,这里用于生成设备文件名
const char *labe;
};
//struct led_handle 对象
struct led_handle {
//IO内存虚拟地址
void __iomem *RCC_ADDR;
void __iomem *GPIOI_ADDR;
//寄存器虚拟地址
void __iomem *RCC_MP_AHB4ENSETR;
void __iomem *GPIOI_MODER;
void __iomem *GPIOI_OTYPER;
void __iomem *GPIOI_OSPEEDR;
void __iomem *GPIOI_PUPDR;
void __iomem *GPIOI_BSRR;
//LED配置参数
struct led_config *led_config;
//LED的ID号,这里作为LED的次设备号
uint32_t id;
//led状态,0灭,1亮
uint32_t state;
//LED链表节点
struct list_head node;
};
//led句柄列表
static struct list_head led_list = LIST_HEAD_INIT(led_list);
//根据ID查找设备句柄
static struct led_handle *find_led_handle(uint32_t id)
{
struct led_handle *pos;
struct led_handle *n;
struct led_handle *led_handle;
led_handle = NULL;
list_for_each_entry_safe(pos, n, &led_list, node)
{
if(pos->id == id)
{
led_handle = pos;
break;
}
}
return led_handle;
}
//分配一个ID
static int32_t alloc_id(void)
{
int32_t id;
//按从小到大顺序生成ID
for(id = 0; find_led_handle(id) && (id < LED_NUMBER); id++)
{
;
}
//ID必须小于注册的最大ID号
if(id >= LED_NUMBER)
return -EINVAL;
return id;
}
//将设备添加到链表
static void add_led(struct led_handle *led_handle)
{
list_add(&led_handle->node, &led_list);
}
//将设备从链表中移除
static void remove_led(struct led_handle *led_handle)
{
list_del(&led_handle->node);
}
- 平台驱动的probe函数;平台驱动的probe函数充当了基本字符设备驱动框架中的模块加载函数的部分功能,它主要实现设备ID分配(这个ID也作为设备文件的次设备号),struct led_handle对象创建,LED控制寄存器映射、LED初始化,将struct led_handle对象添加到链表、创建设备文件等功能
static int pled_probe(struct platform_device *pdev)
{
int result;
int32_t id;
struct device *device;
struct led_handle *led_handle;
printk("led_init\r\n");
//分配一个ID
id = alloc_id();
if(id < 0)
return id;
//设置平台设备ID
pdev->id = id;
//分配设备句柄
led_handle = devm_kzalloc(&pdev->dev, sizeof(struct led_handle), GFP_KERNEL);
if(!led_handle)
{
printk("alloc memory failed\r\n");
return -ENOMEM;
}
//复位LED设备句柄
memset(led_handle, 0, sizeof(struct led_handle));
//绑定ID
led_handle->id = id;
//获取设备私有数据
led_handle->led_config = pdev->dev.platform_data;
//IO内存映射
result = io_map(pdev, led_handle);
if(result != 0)
{
printk("map io mem failed\r\n");
return result;
}
//初始化LED
gpio_init(led_handle);
//添加LED到链表
add_led(led_handle);
//设置平台设备的驱动私有数据
pdev->dev.driver_data = (void*)led_handle;
//创建设备文件,将ID作为此设备的次设备号
printk("device major %d, device minor %d, device file name = %s\r\n",
MAJOR(led_num+led_handle->id), MINOR(led_num+led_handle->id), led_handle->led_config->labe);
device = device_create(led_class, NULL, led_num+led_handle->id, NULL, led_handle->led_config->labe);
if(IS_ERR(device))
{
list_del(&led_handle->node);remove_led(led_handle);
printk("device create failed");
return PTR_ERR(device);
}
return 0;
}
- 平台驱动的remove函数;平台驱动remove函数执行与probe函数相反的功能,主要实现设备卸载时的清理工作
static int pled_remove(struct platform_device *pdev)
{
struct led_handle *led_handle;
printk("led_exit\r\n");
//提取平台设备的驱动私有数据
led_handle = (struct led_handle*)pdev->dev.driver_data;
//删除设备文件
device_destroy(led_class, led_num+led_handle->id);
//从设备句柄链表中删除
remove_led(led_handle);
//反初始化GPIO
gpio_deinit(led_handle);
//取消IO内存映射
io_unmap(led_handle);
return 0;
}
6.其他函数;其他函数的实现参考2.4在Linux内核中操作寄存器
上机实验
- 从这里下载完整代码并进行编译,然后拷贝到目标板跟文件系统的root目录中
- 执行命令insmod plt_dev.ko和insmod plt_drv.ko加载平台驱动和平台设备
- 执行命令echo 0 > /dev/test_led或命令echo 1 > /dev/test_led可以控制LED的状态
- 执行命令cat /dev/test_led可以查看LED状态,因为驱动的read函数每次都返回1,所以cat命令会一直打印,直到强行退出