Linux系統的驅動框架主要就是三個主要部分組成,驅動、總線、設備。現在常見的嵌入式SOC已經不是單純的CPU的概念了,它們都會在片上集成很多外設電路,這些外設都掛接在SOC內部的總線上,不同與IIC、SPI和USB等這一類實際存在外部PCB走線總線,他是系統內的總線實際是CPU的內部走線,所以Linux為了統一驅動模型在系統在啟動引導時初始化了一條虛擬總線作為一個抽象的總線稱之為platform總線,實現在drivers/base/platform.c中。今天就來學習這一類驅動的框架結構。
總線的具體實現
struct bus_type platform_bus_type = { .name = "platform", .dev_groups = platform_dev_groups,(屬性) .match = platform_match, .uevent = platform_uevent, .pm = &platform_dev_pm_ops, };
結合前面的分析,Linux下的設備都應該(但不是必須)有所屬的bus_type(dev.bus)這個bus_type就抽象了他們共通的一些“屬性”和“方法”。platform設備他包含一個普通的device的基礎上由增加了一些平台設備需要的數據如下
struct platform_device { const char *name; int id;
bool id_auto; struct device dev; u32 num_resources; struct resource *resource; const struct platform_device_id *id_entry;/*記錄和驅動的匹配表id_table中匹配的哪一個表項指針*/ /* MFD cell pointer */ struct mfd_cell *mfd_cell; /* arch specific additions */ struct pdev_archdata archdata; 這個參數一般都指向這個結構體實體本身地址 };
值得一提的是其中ID參數如果是-1則包含的設備名就是 platform_device .name的值,如果為-2則會自動分配platform設備ID具體是通過platform.c中的一個函數實現具體參考源碼,否則其他參數則就按"%s.%d", pdev->name, pdev->id 格式格式化platform設備名。一般注冊平台設備需要初始化的內容主要有name、 resource,有時還需要指定內涵dev的platform_data,這一部分數據常常被驅動使用,這也是Linux 驅動和設備分離的一部分體現。
platform_device
注冊添加
這里只是簡單羅列函數調用過程,這一部分實際上是Device注冊的過程的一個封裝,具體內部操作可以參考Linux設備注冊。這一部分如果前面device的注冊理解的比較透徹這一部分就很好理解了。
platform_device_register
1、device_initialize(&pdev->dev);
2、arch_setup_pdev_archdata(空函數)
3、platform_driver_add
1、pdev->dev.parent = &platform_bus;指定父設備為platform設備總線
2、pdev->dev.bus = &platform_bus_type;
3、設定設備名稱三種情況(-1 -2(申請ID) other)
4、設備資源管理
5、調用device_add(pdev->dev)
添加過程
int platform_device_add(struct platform_device *pdev) { int i, ret; if (!pdev) return -EINVAL; if (!pdev->dev.parent) pdev->dev.parent = &platform_bus; pdev->dev.bus = &platform_bus_type; switch (pdev->id) { default: dev_set_name(&pdev->dev, "%s.%d", pdev->name, pdev->id); break; case PLATFORM_DEVID_NONE: dev_set_name(&pdev->dev, "%s", pdev->name); break; case PLATFORM_DEVID_AUTO: /* * Automatically allocated device ID. We mark it as such so * that we remember it must be freed, and we append a suffix * to avoid namespace collision with explicit IDs. */ ret = ida_simple_get(&platform_devid_ida, 0, 0, GFP_KERNEL); if (ret < 0) goto err_out; pdev->id = ret; pdev->id_auto = true; dev_set_name(&pdev->dev, "%s.%d.auto", pdev->name, pdev->id); break; } for (i = 0; i < pdev->num_resources; i++) { struct resource *p, *r = &pdev->resource[i]; if (r->name == NULL) r->name = dev_name(&pdev->dev); p = r->parent; if (!p) { if (resource_type(r) == IORESOURCE_MEM) p = &iomem_resource; else if (resource_type(r) == IORESOURCE_IO) p = &ioport_resource; } if (p && insert_resource(p, r)) { dev_err(&pdev->dev, "failed to claim resource %d\n", i); ret = -EBUSY; goto failed; } } pr_debug("Registering platform device '%s'. Parent at %s\n", dev_name(&pdev->dev), dev_name(pdev->dev.parent)); ret = device_add(&pdev->dev); if (ret == 0) return ret; failed: if (pdev->id_auto) { ida_simple_remove(&platform_devid_ida, pdev->id); pdev->id = PLATFORM_DEVID_AUTO; } while (--i >= 0) { struct resource *r = &pdev->resource[i]; if (r->parent) release_resource(r); } err_out: return ret; }
處理過程是給設備指定父設備即依托的總系即platform_bus,指定bus這一步很關鍵涉及到后面的驅動匹配(因為設備添加過程會拿這個設備所屬的總線總線上由注冊的驅動list),然后就是根據ID的不同值以不同的策略初始化設備name字段。然后就是資源的保存添加,其中最關鍵的就是device_add的操作過程這一部分參考我的Linux設備章節,就可以知道設備添加的細節。
device卸載過程
這一部分內容也是上面的操作的一個逆向操作,其實核心的內容還是設備刪除的操作同樣可以參考上面給出的聯接查看設備的注銷過程,就能明白platform框架只是在原有的驅動和設備驅動模型上的更高一層的封裝。所以這里還是簡單的羅列一下調用過程。
platform_device_unregister
1、platform_device_del
1、釋放platform_device id
2、device_del(pdev->dev)
2、platform_device_put
3、put_device
platform_driver
同理樣platform_driver 也是一個包含了device_driver 的結構體如下:
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; const struct platform_device_id *id_table; bool prevent_deferred_probe; };
從結構體可以看出平台設備驅動提供了一些操作接口和一個platform_device_id 類型的兼容性匹配表(后面分析)。其次是結構體中的操作接口函數其在內部的device_driver結構體內也是有一套類似的操作接口;其他的電源管理現在已經很少用平台設備驅動中的接口了而轉而使用內涵的device_driver驅動中的dev_pm_ops結構體中的接口來實現。
driver注冊添加
__platform_driver_register(drv, THIS_MODULE)
1、drv->driver.bus = platform_bus_type;
2、如果platform驅動中的xx_probe或xx_remove等為空則指定drv->driver.同名接口指針為platform驅動默認行為(僅支持acpi方式匹配)。
3、driver_register
__platform_driver_register
int __platform_driver_register(struct platform_driver *drv, struct module *owner) { drv->driver.owner = owner; drv->driver.bus = &platform_bus_type; if (drv->probe) drv->driver.probe = platform_drv_probe; if (drv->remove) drv->driver.remove = platform_drv_remove; if (drv->shutdown) drv->driver.shutdown = platform_drv_shutdown; return driver_register(&drv->driver); }
通過上面的代碼我們很清楚的看到platform_driver實際上也是對通用驅動的注冊流程的一個高層級的封裝,具體的驅動注冊過程還是需要參考前面的驅動注冊過程。
driver注冊移除
移除過程同樣很簡單就是設備驅動的刪除操作同上參考設備驅動的注銷過程,這里也是僅僅簡單的羅列API的調用層級和過程。
platform_device_unregister
1、platform_driver_unregister
1、driver_unregister
platform驅動和設備的匹配
無論上面的注冊設備還是注冊驅動,最后都是要調用總線類型的mach函數進行驅動和設備的匹配,這也是platform 驅動框架中比較重要核心的部分所以這里從源碼分析一下。
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); /* Attempt an OF style match first */ /* 采用設備樹的兼容方式匹配驅動和設備 */ if (of_driver_match_device(dev, drv)) return 1; /* 采用ACPI的方式匹配驅動和設備 */ if (acpi_driver_match_device(dev, drv)) return 1; /* 通過驅動和設備的mach表來匹配驅動和設備 */ if (pdrv->id_table) return platform_match_id(pdrv->id_table, pdev) != NULL; /* 最后就是按驅動和設備名稱是否相同來判斷當前驅動是否支持這個設備 */ return (strcmp(pdev->name, drv->name) == 0); }
從這個函數我們可以知道platform的driver和device的匹配就是通過以上四種規則來進行匹配的,前兩種方式暫時不深究學到再來看;除此之外這個函數還告訴我們一個內核機制
如果驅動指定了mach_id_table則驅動將放棄名稱相同匹配機制這一點需要重點記住。具體這個mach函數是在何時調用的參考Linux device的分析。其中兼容ID的匹配表格式是
struct platform_device_id { char name[PLATFORM_NAME_SIZE]; kernel_ulong_t driver_data; };
具體的匹配規則也很簡單就是使用ID表內的名稱來和設備名比較具體看代碼,比較簡單,需要注意的是這里還將匹配的id 表的句柄保存在platform device的id_entry項上,id_table里常常帶一個long型的driver_data數據保存驅動數據。
static const struct platform_device_id *platform_match_id( const struct platform_device_id *id, struct platform_device *pdev) { while (id->name[0]) { if (strcmp(pdev->name, id->name) == 0) { pdev->id_entry = id; return id; } id++; } return NULL; }
具體實例分析
下面通過自己實現一個platform device 來匹配內核的一個三星的led驅動,內核代碼是3-16-57版本驅動在drivers\leds\leds-s3c24xx.c。
static struct platform_driver s3c24xx_led_driver = { .probe = s3c24xx_led_probe, .remove = s3c24xx_led_remove, .driver = { .name = "s3c24xx_led", .owner = THIS_MODULE, }, };
主要分析其s3c24xx_led_probe函數的執行過程就能明白對應的設備應該如何添加。通過驅動的聲明我得出結論,這個驅動除了設備樹和ACPI的方式匹配設備外就只能通過名稱來匹配設備了,所以先定義設備如下然后慢慢填充。
static struct platform_device tiny210_device_led []= { .name = "s3c24xx_led", .id = 0, };
然后在看s3c24xx_led_probe函數都是怎樣處理的
static int s3c24xx_led_probe(struct platform_device *dev) { struct s3c24xx_led_platdata *pdata = dev_get_platdata(&dev->dev); /* 首先獲取 platform_data 這個我還沒定義所以后面需要定義 */ struct s3c24xx_gpio_led *led; int ret; /* 申請驅動私有數據結構體 */ led = devm_kzalloc(&dev->dev, sizeof(struct s3c24xx_gpio_led), GFP_KERNEL); if (!led) return -ENOMEM; /* 將私有數據結構體綁定到device的driver_data成員上方便使用 */ platform_set_drvdata(dev, led); /* 這里涉及LED class 子系統的內容 可以暫時當作黑盒 */ led->cdev.brightness_set = s3c24xx_led_set; led->cdev.default_trigger = pdata->def_trigger; led->cdev.name = pdata->name; led->cdev.flags |= LED_CORE_SUSPENDRESUME; /* 綁定platform_data 到私有數據結構 */ led->pdata = pdata; ret = devm_gpio_request(&dev->dev, pdata->gpio, "S3C24XX_LED"); if (ret < 0) return ret; /* no point in having a pull-up if we are always driving */ /* GPIO 子系統內容 配置對應的GPIO */ s3c_gpio_setpull(pdata->gpio, S3C_GPIO_PULL_NONE); /* 如果設備定義時指定了這個標志則會執行下面的設置將GPIO配置為輸入方向 */ if (pdata->flags & S3C24XX_LEDF_TRISTATE) /* GPIO 子系統內容 配置對應的GPIO方向為輸入 一般底層由芯片廠商實現 */ gpio_direction_input(pdata->gpio); else /* 第二個參數是保證LED在默認狀態下是不點亮的 */ gpio_direction_output(pdata->gpio, pdata->flags & S3C24XX_LEDF_ACTLOW ? 1 : 0); /* register our new led device */ /* 這里涉及LED class 子系統的內容 可以暫時當作黑盒 */ ret = led_classdev_register(&dev->dev, &led->cdev); if (ret < 0) dev_err(&dev->dev, "led_classdev_register failed\n"); return ret; }
到此LED驅動的匹配操作就完了,除了中間涉及Linux 的led class 和 gpio 子系統的內容外還是很簡單的所以接下來完善我的LED設備
增加驅動所需的數據
struct s3c24xx_led_platdata led_data { .gpio = S5PV210_GPJ2(0),(gpio 子系統) .flags = S3C24XX_LEDF_ACTLOW,(驅動的私有標志,指明LED開啟時的IO電平) .name = "led", .def_trigger = "", }; static struct platform_device tiny210_device_led []= { .name = "s3c24xx_led", .id = 0, .dev ={ .platform_data = &led_data, }, };
flags 的內容是后來補上的他的意思就是led在低電平時點亮,不要這個標志LED默認狀態是開啟的這和具體的硬件有關。
最后將設備以模塊的形式加入。
然后在/sys/class/leds/ 下將看到一個led0文件他是一個符合鏈接指向/devices/platform/latform/s3c24xx_led.0/leds/led0
進入 會看到
brightness max_brightness subsystem uevent
device power trigger
這些就是ledclass的內容的,通過向brightness寫數據就可以控制LED的開啟和關閉了。也可以直接使用腳本
echo 0 > brightness led燈就亮了 echo 1 >brightness led燈就滅了。
綜上Linux下的platform總線出現的意義就是統一linux下的驅動模型,即設備、驅動、總線其中總線負責設備和驅動的匹配。一般Linux下的復雜驅動都是基於platform總線的
通過驅動的probe函數進行調用其他子系統的接口實習更加復雜的驅動模型。而platform_driver和platform_device都是在Linux device和device_driver之上封裝的所以需要在明白
Linux device和device_driver 的相關機制之上來理解就更加容易了。
附設備添加源碼,不過源碼后來我又添加了三個LED。

#include <linux/kernel.h> #include <linux/types.h> #include <linux/init.h> #include <linux/device.h> #include <linux/fb.h> #include <linux/gpio.h> #include <linux/delay.h> #include <asm/mach/arch.h> #include <asm/mach/map.h> #include <asm/setup.h> #include <asm/mach-types.h> #include <mach/map.h> #include <plat/gpio-cfg.h> #include <plat/devs.h> #include <plat/cpu.h> #include <linux/platform_data/leds-s3c24xx.h> static struct s3c24xx_led_platdata led_data[] = { [0]={ .gpio = S5PV210_GPJ2(0), .flags = S3C24XX_LEDF_ACTLOW, .name = "led0", .def_trigger = "", }, [1]={ .gpio = S5PV210_GPJ2(1), .name = "led1", .flags = S3C24XX_LEDF_ACTLOW, .def_trigger = "", }, [2]={ .gpio = S5PV210_GPJ2(2), .name = "led2", .flags = S3C24XX_LEDF_ACTLOW, .def_trigger = "", }, [3]={ .gpio = S5PV210_GPJ2(3), .name = "led3", .flags = S3C24XX_LEDF_ACTLOW, .def_trigger = "", }, }; static struct platform_device tiny210_device_led []= { [0]={ .name = "s3c24xx_led", .id = 0, .dev ={ .platform_data = &led_data[0], .devt = MAJOR(22), }, }, [1]={ .name = "s3c24xx_led", .id = 1, .dev ={ .platform_data = &led_data[1], .devt = MAJOR(22), }, }, [2]={ .name = "s3c24xx_led", .id = 2, .dev ={ .platform_data = &led_data[2], .devt = MAJOR(22), }, }, [3]={ .name = "s3c24xx_led", .id = 3, .dev ={ .platform_data = &led_data[3], .devt = MAJOR(22), }, } }; static int __init platform_led_init(void) { int i; for(i=0;i<ARRAY_SIZE(tiny210_device_led);i++){ if(platform_device_register(&tiny210_device_led[i])<0) { printk(KERN_ERR "tiny210_device_led %d Fail\n",i); return -1; } } printk(KERN_INFO "tiny210_device_led Succse\n"); return 0; } static void __exit platform_led_exit(void) { int i; for(i=0;i<ARRAY_SIZE(tiny210_device_led);i++){ platform_device_unregister(&tiny210_device_led[i]); } } module_init(platform_led_init); module_exit(platform_led_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("smile@shanghai");