//本文主要参考《野火Linux实战开发指南》
上次跟大家分享了设备模型的一些东西,包括总线、设备、驱动等的一些概念,还有他们之间的联系。今天要分享的是platform总线驱动,platform总线是总线的一种,这是相对于物理总线来说的,这是一种虚拟的总线。
为什么要有platform总线呢?因为在Linux当中,对于I2C、SPI、USB这些常见类型的物理总线来说,Linux内核会自动创建与之相应的驱动总线,因此I2C设备、SPI设备、 USB设备自然是注册挂载在相应的总线上。但是,实际项目开发中还有很多结构简单的设备,对它们进行控制并不需要特殊的时序。它们也就没有相应的物理总线,比如led、rtc时钟、蜂鸣器、按键等等,Linux内核将不会为它们创建相应的驱动总线。
为了使这部分设备的驱动开发也能够遵循设备驱动模型,Linux内核引入了一种虚拟的总线——平台总线(platform bus)。平台总线用于管理、挂载那些没有相应物理总线的设备,这些设备被称为平台设备,对应的设备驱动则被称为平台驱动。
平台总线也是基于上一节当中的设备模型,在上一节里,介绍了创建总线,也提到了在实际当中,并不需要我们去创建新的总线,而是一般直接用现成的,平台总线就是这样一种现成的总线。这也意味着我们不需要去实现总线里的各种函数,包括match函数这种,可以直接不用管这部分。
今天的重点就是和大家分享一下我是如何分析在平台总线下led的设备文件和驱动文件是怎么写的。
先来看设备文件。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#define CCM_CCGR1 0x20C406C //时钟控制寄存器
#define IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04 0x20E006C //GPIO1_04复用功能选择寄存器
#define IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO04 0x20E02F8 //PAD属性设置寄存器
#define GPIO1_GDIR 0x0209C004 //GPIO方向设置寄存器(输入或输出)
#define GPIO1_DR 0x0209C000 //GPIO输出状态寄存器
#define CCM_CCGR3 0x020C4074
#define GPIO4_GDIR 0x020A8004
#define GPIO4_DR 0x020A8000
#define IOMUXC_SW_MUX_CTL_PAD_GPIO4_IO020 0x020E01E0
#define IOMUXC_SW_PAD_CTL_PAD_GPIO4_IO020 0x020E046C
#define IOMUXC_SW_MUX_CTL_PAD_GPIO4_IO019 0x020E01DC
#define IOMUXC_SW_PAD_CTL_PAD_GPIO4_IO019 0x020E0468
static struct resource rled_resource[] = {
[0] = DEFINE_RES_MEM(GPIO1_DR, 4),
[1] = DEFINE_RES_MEM(GPIO1_GDIR, 4),
[2] = DEFINE_RES_MEM(IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04, 4),
[3] = DEFINE_RES_MEM(CCM_CCGR1, 4),
[4] = DEFINE_RES_MEM(IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO04, 4),
};
static struct resource gled_resource[] = {
[0] = DEFINE_RES_MEM(GPIO4_DR, 4),
[1] = DEFINE_RES_MEM(GPIO4_GDIR, 4),
[2] = DEFINE_RES_MEM(IOMUXC_SW_MUX_CTL_PAD_GPIO4_IO020, 4),
[3] = DEFINE_RES_MEM(CCM_CCGR3, 4),
[4] = DEFINE_RES_MEM(IOMUXC_SW_PAD_CTL_PAD_GPIO4_IO020, 4),
};
static struct resource bled_resource[] = {
[0] = DEFINE_RES_MEM(GPIO4_DR, 4),
[1] = DEFINE_RES_MEM(GPIO4_GDIR, 4),
[2] = DEFINE_RES_MEM(IOMUXC_SW_MUX_CTL_PAD_GPIO4_IO019, 4),
[3] = DEFINE_RES_MEM(CCM_CCGR3, 4),
[4] = DEFINE_RES_MEM(IOMUXC_SW_PAD_CTL_PAD_GPIO4_IO019, 4),
};
/* not used */
static void led_release(struct device *dev)
{
}
/* led hardware information */
unsigned int rled_hwinfo[2] = { 4, 26 };
unsigned int gled_hwinfo[2] = { 20, 12 };
unsigned int bled_hwinfo[2] = { 19, 12 };
/* red led device */
static struct platform_device rled_pdev = {
.name = "led_pdev",
.id = 0,
.num_resources = ARRAY_SIZE(rled_resource),
.resource = rled_resource,
.dev = {
.release = led_release,
.platform_data = rled_hwinfo,
},
};
/* green led device */
static struct platform_device gled_pdev = {
.name = "led_pdev",
.id = 1,
.num_resources = ARRAY_SIZE(gled_resource),
.resource = gled_resource,
.dev = {
.release = led_release,
.platform_data = gled_hwinfo,
},
};
/* blue led device */
static struct platform_device bled_pdev = {
.name = "led_pdev",
.id = 2,
.num_resources = ARRAY_SIZE(bled_resource),
.resource = bled_resource,
.dev = {
.release = led_release,
.platform_data = bled_hwinfo,
},
};
static __init int led_pdev_init(void)
{
printk("pdev init\n");
platform_device_register(&rled_pdev);
platform_device_register(&gled_pdev);
platform_device_register(&bled_pdev);
return 0;
}
module_init(led_pdev_init);
static __exit void led_pdev_exit(void)
{
printk("pdev exit\n");
platform_device_unregister(&rled_pdev);
platform_device_unregister(&gled_pdev);
platform_device_unregister(&bled_pdev);
}
module_exit(led_pdev_exit);
MODULE_AUTHOR("embedfire");
MODULE_LICENSE("GPL");
对于编译成模块的文件,先从init部分开始看,可以发现,就做了一件事情——向内核注册一个平台设备。
int platform_device_register(struct platform_device *pdev)
所以,其实非常容易,只要创建一个platform_device类型的对象,然后对对象进行初始化,把它作为参数放进去就行了(这其实是面向对象的表达方式,所谓“对象”就是变量)。所以,我们要做的无非就是如何来对对象初始化(说白了就是如何填充结构体)。
那我们得先看一下这个结构体长什么样子。这里我把结构体的包含关系给画出来了:
我们不需要对每个成员进行填充,只要对几个重要的成员进行填充就行了。我们来看一下代码里相应的部分:
name就是设备的名字,这个要和驱动里面一致,因为总线是通过名字来匹配的。
id是相同类型设备的区分,比如都是led设备,名字可以一样,只要id不同就行。
resource是平台设备提供给驱动的资源。通常用一个数组来存放,上面的num_resource就是数组元素的个数。
那么这个resource到底是什么东西呢?在Linux里面用了几个宏来定义资源
在嵌入式中,基本上没有IO地址空间,所以通常使用IORESOURCE_MEM。代码中是这样定义的。
我们把宏一层层展开,就得到下面这样子:
可以看到,IORESOURCE_MEM这个宏最终是帮我们填充了resource结构体。可以看到,是把所有相关的寄存器的数据给放进去了。具体是怎么填充的,大家只要像我上面那样把宏给一层层展开就知道了。
最后再说一下.dev成员里面的.platform_data成员。platform_data是用来保存设备的私有数据的,platform_data是void *类型的万能指针,无论你想要提供的是什么内容,只需要把数据的地址赋值给platform_data即可。
在这里,我们是给platform_data传了一个数组名(数组名就是int *,可以用void*来接收),数组里存放了两个数据,第一个数据是led的引脚号,第二个是引脚对应的时钟寄存器偏移量。
这些数据待会儿都会通过总线传给驱动。
到这里,也就把所有内容都分析完了,可以看到,虽然看起来很复杂,但是其实做的事情不多,就是定义一个platform device类型的对象,对对象进行初始化(填充数据),然后调用register函数注册即可。
设备文件主要就是提供硬件资源信息,这和上一节里讲的内容完全是一致的。
接下来看一下驱动文件是如何写的:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/mod_devicetable.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#define DEV_MAJOR 243
#define DEV_NAME "led"
static struct class *my_led_class;
struct led_data {
unsigned int led_pin;
unsigned int clk_regshift;
unsigned int __iomem *va_dr;
unsigned int __iomem *va_gdir;
unsigned int __iomem *va_iomuxc_mux;
unsigned int __iomem *va_ccm_ccgrx;
unsigned int __iomem *va_iomux_pad;
struct cdev led_cdev;
};
static int led_cdev_open(struct inode *inode, struct file *filp)
{
printk("%s\n", __func__);
struct led_data *cur_led = container_of(inode->i_cdev, struct led_data, led_cdev);
unsigned int val = 0;
val = readl(cur_led->va_ccm_ccgrx);
val &= ~(3 << cur_led->clk_regshift);
val |= (3 << cur_led->clk_regshift);
writel(val, cur_led->va_ccm_ccgrx);
writel(5, cur_led->va_iomuxc_mux);
writel(0x1F838, cur_led->va_iomux_pad);
val = readl(cur_led->va_gdir);
val &= ~(1 << cur_led->led_pin);
val |= (1 << cur_led->led_pin);
writel(val, cur_led->va_gdir);
val = readl(cur_led->va_dr);
val |= (0x01 << cur_led->led_pin);
writel(val, cur_led->va_dr);
filp->private_data = cur_led;
return 0;
}
static int led_cdev_release(struct inode *inode, struct file *filp)
{
return 0;
}
static ssize_t led_cdev_write(struct file *filp, const char __user * buf,
size_t count, loff_t * ppos)
{
unsigned long val = 0;
unsigned long ret = 0;
int tmp = count;
struct led_data *cur_led = (struct led_data *)filp->private_data;
kstrtoul_from_user(buf, tmp, 10, &ret);
val = readl(cur_led->va_dr);
if (ret == 0)
val &= ~(0x01 << cur_led->led_pin);
else
val |= (0x01 << cur_led->led_pin);
writel(val, cur_led->va_dr);
*ppos += tmp;
return tmp;
}
static struct file_operations led_cdev_fops = {
.open = led_cdev_open,
.release = led_cdev_release,
.write = led_cdev_write,
};
static int led_pdrv_probe(struct platform_device *pdev)
{
struct led_data *cur_led;
unsigned int *led_hwinfo;
struct resource *mem_dr;
struct resource *mem_gdir;
struct resource *mem_iomuxc_mux;
struct resource *mem_ccm_ccgrx;
struct resource *mem_iomux_pad;
dev_t cur_dev;
int ret = 0;
printk("led platform driver probe\n");
cur_led = devm_kzalloc(&pdev->dev, sizeof(struct led_data), GFP_KERNEL);
if(!cur_led)
return -ENOMEM;
led_hwinfo = devm_kzalloc(&pdev->dev, sizeof(unsigned int)*2, GFP_KERNEL);
if(!led_hwinfo)
return -ENOMEM;
/* get the pin for led and the reg's shift */
led_hwinfo = dev_get_platdata(&pdev->dev);
cur_led->led_pin = led_hwinfo[0];
cur_led->clk_regshift = led_hwinfo[1];
/* get platform resource */
mem_dr = platform_get_resource(pdev, IORESOURCE_MEM, 0);
mem_gdir = platform_get_resource(pdev, IORESOURCE_MEM, 1);
mem_iomuxc_mux = platform_get_resource(pdev, IORESOURCE_MEM, 2);
mem_ccm_ccgrx = platform_get_resource(pdev, IORESOURCE_MEM, 3);
mem_iomux_pad = platform_get_resource(pdev, IORESOURCE_MEM, 4);
cur_led->va_dr =
devm_ioremap(&pdev->dev, mem_dr->start, resource_size(mem_dr));
cur_led->va_gdir =
devm_ioremap(&pdev->dev, mem_gdir->start, resource_size(mem_gdir));
cur_led->va_iomuxc_mux =
devm_ioremap(&pdev->dev, mem_iomuxc_mux->start,
resource_size(mem_iomuxc_mux));
cur_led->va_ccm_ccgrx =
devm_ioremap(&pdev->dev, mem_ccm_ccgrx->start,
resource_size(mem_ccm_ccgrx));
cur_led->va_iomux_pad =
devm_ioremap(&pdev->dev, mem_iomux_pad->start,
resource_size(mem_iomux_pad));
cur_dev = MKDEV(DEV_MAJOR, pdev->id);
register_chrdev_region(cur_dev, 1, "led_cdev");
cdev_init(&cur_led->led_cdev, &led_cdev_fops);
ret = cdev_add(&cur_led->led_cdev, cur_dev, 1);
if(ret < 0)
{
printk("fail to add cdev\n");
goto add_err;
}
device_create(my_led_class, NULL, cur_dev, NULL, DEV_NAME "%d", pdev->id);
/* save as drvdata */
platform_set_drvdata(pdev, cur_led);
return 0;
add_err:
unregister_chrdev_region(cur_dev, 1);
return ret;
}
static int led_pdrv_remove(struct platform_device *pdev)
{
dev_t cur_dev;
struct led_data *cur_data = platform_get_drvdata(pdev);
printk("led platform driver remove\n");
cur_dev = MKDEV(DEV_MAJOR, pdev->id);
cdev_del(&cur_data->led_cdev);
device_destroy(my_led_class, cur_dev);
unregister_chrdev_region(cur_dev, 1);
return 0;
}
static struct platform_device_id led_pdev_ids[] = {
{.name = "led_pdev"},
{}
};
MODULE_DEVICE_TABLE(platform, led_pdev_ids);
static struct platform_driver led_pdrv = {
.probe = led_pdrv_probe,
.remove = led_pdrv_remove,
.driver.name = "led_pdev",
.id_table = led_pdev_ids,
};
static __init int led_pdrv_init(void)
{
printk("led platform driver init\n");
my_led_class = class_create(THIS_MODULE, "my_leds");
platform_driver_register(&led_pdrv);
return 0;
}
module_init(led_pdrv_init);
static __exit void led_pdrv_exit(void)
{
printk("led platform driver exit\n");
platform_driver_unregister(&led_pdrv);
class_destroy(my_led_class);
}
module_exit(led_pdrv_exit);
MODULE_AUTHOR("Embedfire");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("the example for platform driver");
驱动文件稍微复杂一些,但是总结一下其实也并不难。可以发现,不管是设备文件也好,驱动文件也好,核心都是调用register函数来注册,其他的工作都是由这个引申出来的。
来看一下函数原型:
#define platform_driver_register(drv) \
__platform_driver_register(drv, THIS_MODULE)
int __platform_driver_register(struct platform_driver *drv,
struct module *owner)
所以我们只要创建一个platform_driver类型的对象,并且进行填充就行了。
这里我也把结构体的包含关系给大家画出来:
可以看到,我们主要是要实现probe函数和remove函数,另外要对device_driver结构体进行填充,还有一个platform_device_id。
这里解释一下为什么有两个name,platform总线提供了四种匹配方式,并且这四种方式存在着优先级:设备树机制>ACPI匹配模式>id_table方式>字符串比较。也就是说总线会先比较id_table里的名字,然后再比较.driver.name里的名字。实际上我们只要提供一种就行了。
重点还是在probe函数里面,上一节也说过,probe函数是当驱动和设备相匹配的时候会自动执行的。搞清楚了这个怎么写,驱动也就基本结束了。
probe函数里要做什么事情呢?总结如下:
1、获取平台资源,对资源进行处理
先从平台获得资源,这个资源就是寄存器信息,然后对寄存器进行地址映射。
struct resource *platform_get_resource(struct platform_device *dev,
unsigned int type, unsigned int num)
void __iomem *devm_ioremap(struct device *dev, resource_size_t offset,
resource_size_t size)
devm_ioremap将获取到的寄存器地址转化为虚拟地址。
static inline void *dev_get_platdata(const struct device *dev)
这个是获取设备的私有数据,在上面也说过了,里面保存了led 的引脚编号和时钟寄存器的偏移量。
2、注册字符设备
使用MKDEV宏定义来创建一个设备编号,再调用register_chrdev_region、cdev_init、cdev_add等函数来注册字符设备。使用platform_set_drvdata函数,将LED数据信息存入在平台驱动结构体中pdev->dev->driver_data中。
这些函数没有太多要注意的,只要根据函数的参数类型往里面放数据就行了,而数据在刚刚的第一步里面都已经得到了。
只有cdev_init这个函数要特别注意一下,因为这里涉及到了file_operation结构体的填充。其实主要就是open和write函数接口的编写。实际上,这部分才是真正操控硬件的部分,从实质上来说是最核心的部分,其他的只是一些“壳子”而已。
我们只要把open和write函数搞定了,驱动也就搞定了。其实也不难。
open里面主要是对硬件进行初始化,这和裸机里面基本是一样的,比如开时钟,配置引脚模式等等,唯一的不同就是,在裸机里面我们是直接操作寄存器,使用的是寄存器的物理地址,而在这里,我们并没有直接操作寄存器,而是操作从平台设备获取的“资源”,这些资源本质上还是寄存器信息,在第一步里面获取到的,并且进行了地址映射。
write是对寄存器进行“写入”,从用户空间获得数据,然后写入寄存器。
int __must_check kstrtoul_from_user(const char __user *s, size_t count,
unsigned int base, unsigned long *res);
从逻辑上来说和裸机是一样的,不同的就是数据来源不一样。
这样,我们就把驱动都分析完了,最后编写Makefile来编译程序,生产.ko文件,安装模块,就可以在/dev目录下看到注册的led设备文件,往设备文件里进行读写就可以操控硬件了。