platform 裝置驅動實驗

Bathwind_W發表於2024-06-26

platform 裝置驅動實驗

Linux 驅動的分離與分層

程式碼的重用性非常重要,否則的話就會在 Linux 核心中存在大量無意義的重複程式碼。尤其是驅動程式,因為驅動程式佔用了 Linux核心程式碼量的大頭,如果不對驅動程式加以管理,任由重複的程式碼肆意增加,那麼用不了多久Linux 核心的檔案數量就龐大到無法接受的地步。
三種平臺下的 MPU6050 驅動框架就可以簡化為圖 54.1.1.2 所示:
在這裡插入圖片描述
實際的 I2C 驅動裝置肯定有很多種,不止 MPU6050 這一個,那麼實際的驅動架構如圖54.1.1.3 所示:
在這裡插入圖片描述
一般 I2C 主機控制器驅動已經由半導體廠家編寫好了,而裝置驅動一般也由裝置器件的廠家編寫好了,我們只需要提供裝置資訊即可,比如 I2C 裝置的話提供裝置連線到了哪個 I2C 介面上, I2C 的速度是多少等等。相當於將裝置資訊從裝置驅動中剝離開來,驅動使用標準方法去獲取到裝置資訊(比如從裝置樹中獲取到裝置資訊),然後根據獲取到的裝置資訊來初始化裝置。 這樣就相當於驅動只負責驅動,裝置只負責裝置,想辦法將兩者進行匹配即可。
這個就是 Linux 中的匯流排(bus)、驅動(driver)和裝置(device)模型,也就是常說的驅動分離。匯流排就是驅動和裝置資訊的月老,負責給兩者牽線搭橋,如圖 54.1.1.4 所示:
在這裡插入圖片描述
當我們向系統註冊一個驅動的時候,匯流排就會在右側的裝置中查詢,看看有沒有與之匹配的裝置,如果有的話就將兩者聯絡起來。同樣的,當向系統中註冊一個裝置的時候,匯流排就會在左側的驅動中查詢看有沒有與之匹配的裝置,有的話也聯絡起來。 Linux 核心中大量的驅動程式都採用匯流排、驅動和裝置模式,我們一會要重點講解的 platform 驅動就是這一思想下的產物。
Linux 下的驅動往往也是分層的,分層的目的也是為了在不同的層處理不同的內容。以其他書籍或者資料常常使用到的input(輸入子系統,後面會有專門的章節詳細的講解)為例,簡單介紹一下驅動的分層。 input 子系統負責管理所有跟輸入有關的驅動,包括鍵盤、滑鼠、觸控等,最底層的就是裝置原始驅動,負責獲取輸入裝置的原始值,獲取到的輸入事件上報給 input 核心層。 input 核心層會處理各種 IO 模型,並且提供file_operations 操作集合。我們在編寫輸入裝置驅動的時候只需要處理好輸入事件的上報即可,至於如何處理這些上報的輸入事件那是上層去考慮的,我們不用管。可以看出藉助分層模型可以極大的簡化我們的驅動編寫,對於驅動編寫來說非常的友好。

platform 平臺驅動模型簡介

匯流排(bus)、驅動(driver)和裝置(device)模型,比如 I2C、 SPI、 USB 等匯流排。但是在 SOC 中有些外設是沒有匯流排這個概念的,但是又要使用匯流排、驅動和裝置模型該怎麼辦呢?為了解決此問題, Linux 提出了 platform 這個虛擬匯流排,相應的就有 platform_driver 和 platform_device.

platform 匯流排

Linux系統核心使用bus_type 結構體表示匯流排,此結構體定義在檔案 include/linux/device.h,bus_type 結構體內容如下:

示例程式碼 54.2.1.1 bus_type 結構體程式碼段
1 struct bus_type {
2 const char *name; /* 匯流排名字 */
3 const char *dev_name;
4 struct device *dev_root;
5 struct device_attribute *dev_attrs;
6 const struct attribute_group **bus_groups; /* 匯流排屬性 */
7 const struct attribute_group **dev_groups; /* 裝置屬性 */
8 const struct attribute_group **drv_groups; /* 驅動屬性 */
9
10 int (*match)(struct device *dev, struct device_driver *drv);
11 int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
12 int (*probe)(struct device *dev);
13 int (*remove)(struct device *dev);
14 void (*shutdown)(struct device *dev);
15
16 int (*online)(struct device *dev);
17 int (*offline)(struct device *dev);
18 int (*suspend)(struct device *dev, pm_message_t state);
19 int (*resume)(struct device *dev);
20 const struct dev_pm_ops *pm;
21 const struct iommu_ops *iommu_ops;
22 struct subsys_private *p;
23 struct lock_class_key lock_key;
24 };

第 10 行, match 函式,此函式很重要,單詞 match 的意思就是“匹配、相配”,因此此函式就是完成裝置和驅動之間匹配的,匯流排就是使用 match 函式來根據註冊的裝置來查詢對應的驅動,或者根據註冊的驅動來查詢相應的裝置,因此每一條匯流排都必須實現此函式。 match 函式有兩個引數: dev 和 drv,這兩個引數分別為 device 和 device_driver 型別,也就是裝置和驅動.
platform 匯流排是 bus_type 的一個具體例項,定義在檔案 drivers/base/platform.c, platform 總
線定義如下:

示例程式碼 54.2.1.2 platform 匯流排例項
1 struct bus_type platform_bus_type = {
2 .name = "platform",
3 .dev_groups = platform_dev_groups,
4 .match = platform_match,
5 .uevent = platform_uevent,
6 .pm = &platform_dev_pm_ops,
7 };

platform_bus_type 就是 platform 平臺匯流排,其中 platform_match 就是匹配函式。我們來看一下驅動和裝置是如何匹配的, platform_match 函式定義在檔案 drivers/base/platform.c 中,函式內容如下所示:

示例程式碼 54.2.1.3 platform 匯流排例項
1 static int platform_match(struct device *dev,
struct device_driver *drv)
2 {
3 struct platform_device *pdev = to_platform_device(dev);
4 struct platform_driver *pdrv = to_platform_driver(drv);
5 6
/*When driver_override is set,only bind to the matching driver*/
7 if (pdev->driver_override)
8 return !strcmp(pdev->driver_override, drv->name);
9
10 /* Attempt an OF style match first */
11 if (of_driver_match_device(dev, drv))
12 return 1;
13
14 /* Then try ACPI style match */
15 if (acpi_driver_match_device(dev, drv))
16 return 1;
17
18 /* Then try to match against the id table */
19 if (pdrv->id_table)
20 return platform_match_id(pdrv->id_table, pdev) != NULL;
21
22 /* fall-back to driver name match */
23 return (strcmp(pdev->name, drv->name) == 0);
24 }

第 11~12 行,第一種匹配方式, OF 型別的匹配,也就是裝置樹採用的匹配方式,
of_driver_match_device 函式定義在檔案 include/linux/of_device.h 中。 device_driver 結構體(表示裝置驅動)中有個名為of_match_table的成員變數,此成員變數儲存著驅動的compatible匹配表,裝置樹中的每個裝置節點的 compatible 屬性會和 of_match_table 表中的所有成員比較,檢視是否有相同的條目,如果有的話就表示裝置和此驅動匹配,裝置和驅動匹配成功以後 probe 函式就會執行。
第 15~16 行,第二種匹配方式, ACPI 匹配方式。
第 19~20 行,第三種匹配方式, id_table 匹配,每個 platform_driver 結構體有一個 id_table成員變數,顧名思義,儲存了很多 id 資訊。這些 id 資訊存放著這個 platformd 驅動所支援的驅動型別。
第 23 行,第四種匹配方式,如果第三種匹配方式的 id_table 不存在的話就直接比較驅動和裝置的 name 欄位,看看是不是相等,如果相等的話就匹配成功。一般裝置驅動為了相容性都支援裝置樹和無裝置樹兩種匹配方式。也就是第一種匹配方式一般都會存在,第三種和第四種只要存在一種就可以,一般用的最多的還是第四種,也就是直接比較驅動和裝置的 name 欄位,畢竟這種方式最簡單了。

platform 驅動

platform_driver 結 構 體 表 示 platform 驅 動 , 此 結 構 體 定 義 在 文 件include/linux/platform_device.h 中,內容如下:

示例程式碼 54.2.2.1 platform_driver 結構體
1 struct platform_driver {
2 int (*probe)(struct platform_device *);
3 int (*remove)(struct platform_device *);
4 void (*shutdown)(struct platform_device *);
5 int (*suspend)(struct platform_device *, pm_message_t state);
6 int (*resume)(struct platform_device *);
7 struct device_driver driver;
8 const struct platform_device_id *id_table;
9 bool prevent_deferred_probe;
10 };

第 2 行, probe 函式,當驅動與裝置匹配成功以後 probe 函式就會執行,非常重要的函式!!一般驅動的提供者會編寫,如果自己要編寫一個全新的驅動,那麼 probe 就需要自行實現。
第 7 行, driver 成員,為 device_driver 結構體變數, Linux 核心裡面大量使用到了物件導向的思device_driver 相當於基類,提供了最基礎的驅動框架。 plaform_driver 繼承了這個基類,然後在此基礎上又新增了一些特有的成員變數。
第 8 行, id_table 表,也就是我們上一小節講解 platform 匯流排匹配驅動和裝置的時候採用的第三種方法, id_table 是個表( 也就是陣列) ,每個元素的型別為 platform_device_id,platform_device_id 結構體內容如下:

示例程式碼 54.2.2.2 platform_device_id 結構體
1 struct platform_device_id {
2 char name[PLATFORM_NAME_SIZE];
3 kernel_ulong_t driver_data;
4 };

device_driver 結構體定義在 include/linux/device.h, device_driver 結構體內容如下:

示例程式碼 54.2.2.3 device_driver 結構體
1 struct device_driver {
2 const char *name;
3 struct bus_type *bus;
4 5
struct module *owner;
6 const char *mod_name; /* used for built-in modules */
7 8
bool suppress_bind_attrs; /* disables bind/unbind via sysfs */
9
10 const struct of_device_id *of_match_table;
11 const struct acpi_device_id *acpi_match_table;
12
13 int (*probe) (struct device *dev);
14 int (*remove) (struct device *dev);
15 void (*shutdown) (struct device *dev);
16 int (*suspend) (struct device *dev, pm_message_t state);
17 int (*resume) (struct device *dev);
18 const struct attribute_group **groups;
19
20 const struct dev_pm_ops *pm;
21
22 struct driver_private *p;
23 };

第 10 行, of_match_table 就是採用裝置樹的時候驅動使用的匹配表,同樣是陣列,每個匹配項都為 of_device_id 結構體型別,此結構體定義在檔案 include/linux/mod_devicetable.h 中,內容如下:

示例程式碼 54.2.2.4 of_device_id 結構體
1 struct of_device_id {
2 char name[32];
3 char type[32];
4 char compatible[128];
5 const void *data;
6 };

第 4 行的 compatible 非常重要,因為對於裝置樹而言,就是透過裝置節點的 compatible 屬性值of_match_table 中每個專案的 compatible 成員變數進行比較,如果有相等的就表示裝置和此驅動匹配成功。
在編寫 platform 驅動的時候,首先定義一個 platform_driver 結構體變數,然後實現結構體中的各個成員變數,重點是實現匹配方法以及 probe 函式。當驅動和裝置匹配成功以後 probe函式就會執行,具體的驅動程式在 probe 函式里面編寫,比如字元裝置驅動等等。
當我們定義並初始化好 platform_driver 結構體變數以後,需要在驅動入口函式里面呼叫platform_driver_register 函式向 Linux 核心註冊一個 platform 驅動, platform_driver_register 函式原型如下所示:

int platform_driver_register (struct platform_driver *driver)
函式引數和返回值含義如下:
driver:要註冊的 platform 驅動。
返回值: 負數,失敗; 0,成功。

還需要在驅動解除安裝函式中透過 platform_driver_unregister 函式解除安裝 platform 驅動,

platform_driver_unregister 函式原型如下:
void platform_driver_unregister(struct platform_driver *drv)
函式引數和返回值含義如下:
drv:要解除安裝的 platform 驅動。
返回值: 無。

platform 驅動框架如下所示:

示例程式碼 54.2.2.5 platform 驅動框架
/* 裝置結構體 */
1 struct xxx_dev{
2 struct cdev cdev;
3 /* 裝置結構體其他具體內容 */
4 };
5 6
	struct xxx_dev xxxdev; /* 定義個裝置結構體變數 */
7 8
	static int xxx_open(struct inode *inode, struct file *filp)
9 {
10 /* 函式具體內容 */
11 return 0;
12 }
13
14 static ssize_t xxx_write(struct file *filp, const char __user *buf,size_t cnt, loff_t *offt)
15 {
16 /* 函式具體內容 */
17 return 0;
18 }
19
20 /*
21 * 字元裝置驅動操作集
22 */
23 static struct file_operations xxx_fops = {
24 .owner = THIS_MODULE,
25 .open = xxx_open,
26 .write = xxx_write,
27 };
28
29 /*
30 * platform 驅動的 probe 函式
31 * 驅動與裝置匹配成功以後此函式就會執行
32 */
33 static int xxx_probe(struct platform_device *dev)
34 {
35 ......
36 	cdev_init(&xxxdev.cdev, &xxx_fops); /* 註冊字元裝置驅動 */
37 	/* 函式具體內容 */
38 	return 0;
39 }
40
41 static int xxx_remove(struct platform_device *dev)
42 {
43 ......
44 cdev_del(&xxxdev.cdev);/* 刪除 cdev */
45 /* 函式具體內容 */
46 return 0;
47 }
48
49 /* 匹配列表 */
50 static const struct of_device_id xxx_of_match[] = {
51 { .compatible = "xxx-gpio" },
52 { /* Sentinel */ }
53 };
54
55 /*
56 * platform 平臺驅動結構體
57 */
58 static struct platform_driver xxx_driver = {
59 .driver = {
60 .name = "xxx",
61 .of_match_table = xxx_of_match,
62 },
63 .probe = xxx_probe,
64 .remove = xxx_remove,
65 };
66
67 /* 驅動模組載入 */
68 static int __init xxxdriver_init(void)
69 {
70 		return platform_driver_register(&xxx_driver);
71 }
72
73 /* 驅動模組解除安裝 */
74 static void __exit xxxdriver_exit(void)
75 {
76 		platform_driver_unregister(&xxx_driver);
77 }
78
79 module_init(xxxdriver_init);
80 module_exit(xxxdriver_exit);
81 MODULE_LICENSE("GPL");
82 MODULE_AUTHOR("zuozhongkai");

platform 裝置

platform 驅動已經準備好了,我們還需要 platform 裝置,否則的話單單一個驅動也做不了什麼platform_device 這個結構體表示 platform 裝置,這裡我們要注意,如果核心支援裝置樹的話就不要再使用 platform_device 來描述裝置了,因為改用裝置樹去描述了。當然了,你如果一定要用 platform_device 來描述裝置資訊的話也是可以的。 platform_device 結構體定義在檔案include/linux/platform_device.h 中,結構體內容如下:

示例程式碼 54.2.3.1 platform_device 結構體程式碼段
22 struct platform_device {
23 const char *name;
24 int id;
25 bool id_auto;
26 struct device dev;
27 u32 num_resources;
28 struct resource *resource;
29
30 const struct platform_device_id *id_entry;
31 char *driver_override; /* Driver name to force a match */
32
33 /* MFD cell pointer */
34 struct mfd_cell *mfd_cell;
35
36 /* arch specific additions */
37 struct pdev_archdata archdata;
38 };

第 23 行, name 表示裝置名字,要和所使用的 platform 驅動的 name 欄位相同,否則的話裝置就無法匹配到對應的驅動。比如對應的 platform 驅動的 name 欄位為“xxx-gpio”,那麼此 name欄位也要設定為“xxx-gpio”。
第 27 行, num_resources 表示資源數量,一般為第 28 行 resource 資源的大小。
第 28 行, resource 表示資源,也就是裝置資訊,比如外設暫存器等。 Linux 核心使用 resource結構體表示資源, resource 結構體內容如下:

示例程式碼 54.2.3.2 resource 結構體程式碼段
18 struct resource {
19 resource_size_t start;
20 resource_size_t end;
21 const char *name;
22 unsigned long flags;
23 struct resource *parent, *sibling, *child;
24 };

start 和 end 分別表示資源的起始和終止資訊,對於記憶體類的資源,就表示記憶體起始和終止地址, name 表示資源名字, flags 表示資源型別,可選的資源型別都定義在了檔案include/linux/ioport.h 裡面,如下所示:

示例程式碼 54.2.3.3 資源型別
29 #define IORESOURCE_BITS 0x000000ff /* Bus-specific bits */
30
31 #define IORESOURCE_TYPE_BITS 0x00001f00 /* Resource type */
32 #define IORESOURCE_IO 0x00000100 /* PCI/ISA I/O ports */
33 #define IORESOURCE_MEM 0x00000200
34 #define IORESOURCE_REG 0x00000300 /* Register offsets */
35 #define IORESOURCE_IRQ 0x00000400
36 #define IORESOURCE_DMA 0x00000800
37 #define IORESOURCE_BUS 0x00001000
......
104 /* PCI control bits. Shares IORESOURCE_BITS with above PCI ROM. */
105 #define IORESOURCE_PCI_FIXED (1<<4) /* Do not move resource */

在以前不支援裝置樹的Linux版本中,使用者需要編寫platform_device變數來描述裝置資訊,然後使用 platform_device_register 函式將裝置資訊註冊到 Linux 核心中,此函式原型如下所示:

int platform_device_register(struct platform_device *pdev)
函式引數和返回值含義如下:
pdev:要註冊的 platform 裝置。
返回值: 負數,失敗; 0,成功。

如果不再使用 platform 的話可以透過 platform_device_unregister 函式登出掉相應的 platform裝置, platform_device_unregister 函式原型如下:

void platform_device_unregister(struct platform_device *pdev)
函式引數和返回值含義如下:
pdev:要登出的 platform 裝置。
返回值: 無。

platform 裝置資訊框架如下所示:

示例程式碼 54.2.3.4 platform 裝置框架
1 /* 暫存器地址定義*/
2 #define PERIPH1_REGISTER_BASE (0X20000000) /* 外設 1 暫存器首地址 */
3 #define PERIPH2_REGISTER_BASE (0X020E0068) /* 外設 2 暫存器首地址 */
4 #define REGISTER_LENGTH 4
5 6
/* 資源 */
7 static struct resource xxx_resources[] = {
8 [0] = {
9 .start = PERIPH1_REGISTER_BASE,
10 .end = (PERIPH1_REGISTER_BASE + REGISTER_LENGTH - 1),
11 .flags = IORESOURCE_MEM,
12 },
13 [1] = {
14 .start = PERIPH2_REGISTER_BASE,
15 .end = (PERIPH2_REGISTER_BASE + REGISTER_LENGTH - 1),
16 .flags = IORESOURCE_MEM,
17 },
18 };
19
20 /* platform 裝置結構體 */
21 static struct platform_device xxxdevice = {
22 .name = "xxx-gpio",
23 .id = -1,
24 .num_resources = ARRAY_SIZE(xxx_resources),
25 .resource = xxx_resources,
26 };
27
28 /* 裝置模組載入 */
29 static int __init xxxdevice_init(void)
30 {
31 return platform_device_register(&xxxdevice);
32 }
33
34 /* 裝置模組登出 */
35 static void __exit xxx_resourcesdevice_exit(void)
36 {
37 platform_device_unregister(&xxxdevice);
38 }
39
40 module_init(xxxdevice_init);
41 module_exit(xxxdevice_exit);
42 MODULE_LICENSE("GPL");
43 MODULE_AUTHOR("zuozhongkai");

第 7~18 行,陣列 xxx_resources 表示裝置資源,一共有兩個資源,分別為裝置外設 1 和外設 2 的暫存器資訊。因此 flags 都為 IORESOURCE_MEM,表示資源為記憶體型別的。
第 21~26 行, platform 裝置結構體變數,注意 name 欄位要和所使用的驅動中的 name 欄位一致,否則驅動和裝置無法匹配成功。 num_resources 表示資源大小,其實就是陣列 xxx_resources的元素數量,這裡用 ARRAY_SIZE 來測量一個陣列的元素個數。
第 29~32 行,裝置模組載入函式,在此函式中呼叫 platform_device_register 向 Linux 核心註冊 platform 裝置。
第 35~38 行,裝置模組解除安裝函式,在此函式中呼叫 platform_device_unregister 從 Linux 核心中解除安裝 platform 裝置。

程式編寫

新建名為 leddevice.c 和 leddriver.c 這兩個檔案,這兩個檔案分別為 LED燈的 platform 裝置檔案和 LED 燈的 platform 的驅動檔案。程式如下:

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/fcntl.h>
#include <linux/platform_device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

接下里要定義裝置相關的暫存器:

/* 
 * 暫存器地址定義
 */
#define CCM_CCGR1_BASE				(0X020C406C)	
#define SW_MUX_GPIO1_IO03_BASE		(0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE		(0X020E02F4)
#define GPIO1_DR_BASE				(0X0209C000)
#define GPIO1_GDIR_BASE				(0X0209C004)
#define REGISTER_LENGTH				4

接下來定義release函式以及定義裝置資源資訊。

/* @description		: 釋放flatform裝置模組的時候此函式會執行	
 * @param - dev 	: 要釋放的裝置 
 * @return 			: 無
 */
static void	led_release(struct device *dev)
{
	printk("led device released!\r\n");	
}

/*  
 * 裝置資源資訊,也就是LED0所使用的所有暫存器
 */
static struct resource led_resources[] = {
	[0] = {
		.start 	= CCM_CCGR1_BASE,
		.end 	= (CCM_CCGR1_BASE + REGISTER_LENGTH - 1),
		.flags 	= IORESOURCE_MEM,
	},	
	[1] = {
		.start	= SW_MUX_GPIO1_IO03_BASE,
		.end	= (SW_MUX_GPIO1_IO03_BASE + REGISTER_LENGTH - 1),
		.flags	= IORESOURCE_MEM,
	},
	[2] = {
		.start	= SW_PAD_GPIO1_IO03_BASE,
		.end	= (SW_PAD_GPIO1_IO03_BASE + REGISTER_LENGTH - 1),
		.flags	= IORESOURCE_MEM,
	},
	[3] = {
		.start	= GPIO1_DR_BASE,
		.end	= (GPIO1_DR_BASE + REGISTER_LENGTH - 1),
		.flags	= IORESOURCE_MEM,
	},
	[4] = {
		.start	= GPIO1_GDIR_BASE,
		.end	= (GPIO1_GDIR_BASE + REGISTER_LENGTH - 1),
		.flags	= IORESOURCE_MEM,
	},
};

這個陣列定義了LED裝置所需的所有硬體資源。在Linux核心中,裝置資源通常指的是記憶體區域、中斷號等,這些資源在裝置驅動中用於正確配置和控制硬體。每個條目都是struct resource型別,定義了一個資源的起始地址、結束地址和標誌。
資源詳解
陣列中的每個元素代表了LED裝置使用的一個暫存器或暫存器組的記憶體對映區域。這些暫存器可能用於控制LED的開關、配置等。
.start和.end定義了暫存器的實體地址範圍。
.flags設定為IORESOURCE_MEM,表示這是一個記憶體型別的資源。
具體資源
[0]: 控制時鐘相關的暫存器(CCM_CCGR1_BASE)。
[1]: 控制特定GPIO功能選擇的多路複用暫存器(SW_MUX_GPIO1_IO03_BASE)。
[2]: 控制GPIO的電氣特性的配置暫存器(SW_PAD_GPIO1_IO03_BASE)。
[3]: 控制GPIO資料暫存器,用於讀寫GPIO引腳的值(GPIO1_DR_BASE)。
[4]: 控制GPIO方向暫存器,用於設定GPIO引腳是輸入還是輸出(GPIO1_GDIR_BASE)。

/*
 * platform裝置結構體 
 */
static struct platform_device leddevice = {
	.name = "imx6ul-led",
	.id = -1,
	.dev = {
		.release = &led_release,
	},
	.num_resources = ARRAY_SIZE(led_resources),
	.resource = led_resources,
};

這段程式碼定義了一個平臺裝置leddevice,它是Linux核心中平臺裝置註冊的一種方式。平臺裝置通常是與處理器緊密整合的裝置,不透過標準的匯流排(如PCI)列舉,因此必須在系統啟動時或驅動初始化時手動註冊。這裡定義的leddevice是一個LED控制裝置,具體細節如下:

struct platform_device 解釋

platform_device結構體是用來描述一個平臺裝置的。它包含了裝置的名稱、ID、裝置特有的資料、資源數量和資源指標等資訊。這些資訊用於裝置的註冊和管理。
程式碼詳解

static struct platform_device leddevice = {
	.name = "imx6ul-led",
	.id = -1,
	.dev = {
		.release = &led_release,
	},
	.num_resources = ARRAY_SIZE(led_resources),
	.resource = led_resources,
};
  • .name = "imx6ul-led": 裝置的名稱,這是一個識別符號,用於在系統中唯一標識該裝置。通常與驅動程式中用來匹配裝置的名稱相對應。

  • .id = -1: 裝置的ID。當設定為-1時,表示該裝置不需要使用編號來區分同類多個裝置。如果有多個相同型別的裝置,則需要分配不同的ID。

  • .dev = { .release = &led_release }: 裝置的釋放函式,這裡將其設定為前面定義的led_release函式。這個函式在裝置登出時被呼叫,用於執行裝置資源的清理工作。儘管在這個例子中,led_release函式只是列印了一個訊息,但這是一個實現裝置資源釋放的鉤子。

  • .num_resources = ARRAY_SIZE(led_resources): 這指定了led_resources陣列中資源的數量。ARRAY_SIZE是一個宏,用於計算陣列元素的個數。這確保了平臺裝置結構體知道有多少資源需要被管理。

  • .resource = led_resources: 這是指向定義的資源陣列的指標。這個陣列包含了裝置所需的所有資源資訊(如記憶體區域、GPIO等),確保裝置能夠正確地訪問其硬體資源。
    這段程式碼的作用是在Linux核心中註冊一個名為imx6ul-led的LED裝置,這個裝置使用了特定的資源,並且定義了一個釋放函式。這樣,當核心或其他驅動需要與這個LED裝置互動時,可以透過這些定義來正確地識別和操作裝置。這是裝置驅動程式設計中常見的初始化和註冊過程的一部分。

最後就是裝置初始化和退出函式程式:

/*
 * @description	: 裝置模組載入 
 * @param 		: 無
 * @return 		: 無
 */
static int __init leddevice_init(void)
{
	return platform_device_register(&leddevice);
}

/*
 * @description	: 裝置模組登出
 * @param 		: 無
 * @return 		: 無
 */
static void __exit leddevice_exit(void)
{
	platform_device_unregister(&leddevice);
}

module_init(leddevice_init);
module_exit(leddevice_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wyw");

leddevice.c 檔案編寫完成以後就編寫 leddriver.c 這個 platform 驅動檔案,在 leddriver.c 裡面輸入如下內容:

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/fcntl.h>
#include <linux/platform_device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

這段程式碼是Linux核心裝置驅動程式的一部分,用於定義與LED控制相關的一些常量和變數。下面分別解釋每部分程式碼的作用和意義:

定義常量

  1. #define LEDDEV_CNT 1

    • 這行程式碼定義了一個常量LEDDEV_CNT,其值為1。這個常量通常用於指定裝置號的長度,即在建立裝置時分配給裝置的裝置號數量。在這個上下文中,它可能被用來指示驅動程式只使用一個裝置號。
  2. #define LEDDEV_NAME "platled"

    • 這行程式碼定義了裝置的名稱為platled。這是裝置在系統中的識別符號,用於在/dev目錄下建立相應的裝置檔案,使得使用者空間程式可以透過這個名稱訪問裝置。
  3. #define LEDOFF 0

    • 這定義了一個常量LEDOFF,值為0,用於表示LED燈的關閉狀態。
  4. #define LEDON 1

    • 這定義了一個常量LEDON,值為1,用於表示LED燈的開啟狀態。

定義暫存器指標

接下來的幾行定義了幾個型別為static void __iomem *的變數,這些變數將用於指向對映到核心地址空間的物理暫存器地址。這些暫存器用於控制特定的硬體功能,如GPIO的配置和資料暫存器。這些指標透過核心函式(如ioremap)對映後,允許驅動程式透過這些指標讀寫這些暫存器,從而控制硬體。每個變數的作用如下:

  1. static void __iomem *IMX6U_CCM_CCGR1;

    • 指向CCM(時鐘控制模組)的CCGR1暫存器,通常用於控制時鐘門控,即啟用或禁用與GPIO相關的時鐘。
  2. static void __iomem *SW_MUX_GPIO1_IO03;

    • 指向用於配置GPIO1_IO03引腳功能的多路複用暫存器。此暫存器用於選擇該引腳的功能(如GPIO、I2C等)。
  3. static void __iomem *SW_PAD_GPIO1_IO03;

    • 指向用於配置GPIO1_IO03引腳電氣屬性(如驅動力、上拉/下拉等)的暫存器。
  4. static void __iomem *GPIO1_DR;

    • 指向GPIO1資料暫存器,用於讀取或設定GPIO1埠的輸出。
  5. static void __iomem *GPIO1_GDIR;

    • 指向GPIO1方向暫存器,用於配置GPIO1埠的引腳是作為輸入還是輸出。

接下來就和之前編寫驅動程式中的裝置結構體是一樣的:

/* leddev裝置結構體 */
struct leddev_dev{
	dev_t devid;			/* 裝置號	*/
	struct cdev cdev;		/* cdev		*/
	struct class *class;	/* 類 		*/
	struct device *device;	/* 裝置		*/
	int major;				/* 主裝置號	*/		
};

struct leddev_dev leddev; 	/* led裝置 */

接下來重點看probe函式。由於裝置和驅動的名稱完全匹配,當您的裝置和驅動都註冊到核心後,核心將自動呼叫led_probe函式來初始化裝置。這種基於名稱的匹配機制使得裝置和驅動的關聯變得非常直觀和簡單。
在這裡插入圖片描述
具體led_probe函式如下:

static int led_probe(struct platform_device *dev)
{	
	int i = 0;
	int ressize[5];
	u32 val = 0;
	struct resource *ledsource[5];

	printk("led driver and device has matched!\r\n");
	/* 1、獲取資源 */
	for (i = 0; i < 5; i++) {
		ledsource[i] = platform_get_resource(dev, IORESOURCE_MEM, i); /* 依次MEM型別資源 */
		if (!ledsource[i]) {
			dev_err(&dev->dev, "No MEM resource for always on\n");
			return -ENXIO;
		}
		ressize[i] = resource_size(ledsource[i]);	
	}	

	/* 2、初始化LED */
	/* 暫存器地址對映 */
 	IMX6U_CCM_CCGR1 = ioremap(ledsource[0]->start, ressize[0]);
	SW_MUX_GPIO1_IO03 = ioremap(ledsource[1]->start, ressize[1]);
  	SW_PAD_GPIO1_IO03 = ioremap(ledsource[2]->start, ressize[2]);
	GPIO1_DR = ioremap(ledsource[3]->start, ressize[3]);
	GPIO1_GDIR = ioremap(ledsource[4]->start, ressize[4]);
	
	val = readl(IMX6U_CCM_CCGR1);
	val &= ~(3 << 26);				/* 清除以前的設定 */
	val |= (3 << 26);				/* 設定新值 */
	writel(val, IMX6U_CCM_CCGR1);

	/* 設定GPIO1_IO03複用功能,將其複用為GPIO1_IO03 */
	writel(5, SW_MUX_GPIO1_IO03);
	writel(0x10B0, SW_PAD_GPIO1_IO03);

	/* 設定GPIO1_IO03為輸出功能 */
	val = readl(GPIO1_GDIR);
	val &= ~(1 << 3);			/* 清除以前的設定 */
	val |= (1 << 3);			/* 設定為輸出 */
	writel(val, GPIO1_GDIR);

	/* 預設關閉LED1 */
	val = readl(GPIO1_DR);
	val |= (1 << 3) ;	
	writel(val, GPIO1_DR);
	
	/* 註冊字元裝置驅動 */
	/*1、建立裝置號 */
	if (leddev.major) {		/*  定義了裝置號 */
		leddev.devid = MKDEV(leddev.major, 0);
		register_chrdev_region(leddev.devid, LEDDEV_CNT, LEDDEV_NAME);
	} else {						/* 沒有定義裝置號 */
		alloc_chrdev_region(&leddev.devid, 0, LEDDEV_CNT, LEDDEV_NAME);	/* 申請裝置號 */
		leddev.major = MAJOR(leddev.devid);	/* 獲取分配號的主裝置號 */
	}
	
	/* 2、初始化cdev */
	leddev.cdev.owner = THIS_MODULE;
	cdev_init(&leddev.cdev, &led_fops);
	
	/* 3、新增一個cdev */
	cdev_add(&leddev.cdev, leddev.devid, LEDDEV_CNT);

	/* 4、建立類 */
	leddev.class = class_create(THIS_MODULE, LEDDEV_NAME);
	if (IS_ERR(leddev.class)) {
		return PTR_ERR(leddev.class);
	}

	/* 5、建立裝置 */
	leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, LEDDEV_NAME);
	if (IS_ERR(leddev.device)) {
		return PTR_ERR(leddev.device);
	}

	return 0;
}

接下來和之前的驅動程式編寫是完全一樣的。Open和Write函式如下:

void led0_switch(u8 sta)
{
	u32 val = 0;
	if(sta == LEDON){
		val = readl(GPIO1_DR);
		val &= ~(1 << 3);	
		writel(val, GPIO1_DR);
	}else if(sta == LEDOFF){
		val = readl(GPIO1_DR);
		val|= (1 << 3);	
		writel(val, GPIO1_DR);
	}	
}

/*
 * @description		: 開啟裝置
 * @param - inode 	: 傳遞給驅動的inode
 * @param - filp 	: 裝置檔案,file結構體有個叫做private_data的成員變數
 * 					  一般在open的時候將private_data指向裝置結構體。
 * @return 			: 0 成功;其他 失敗
 */
static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &leddev; /* 設定私有資料  */
	return 0;
}

/*
 * @description		: 向裝置寫資料 
 * @param - filp 	: 裝置檔案,表示開啟的檔案描述符
 * @param - buf 	: 要寫給裝置寫入的資料
 * @param - cnt 	: 要寫入的資料長度
 * @param - offt 	: 相對於檔案首地址的偏移
 * @return 			: 寫入的位元組數,如果為負值,表示寫入失敗
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;

	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {
		return -EFAULT;
	}

	ledstat = databuf[0];		/* 獲取狀態值 */
	if(ledstat == LEDON) {
		led0_switch(LEDON);		/* 開啟LED燈 */
	}else if(ledstat == LEDOFF) {
		led0_switch(LEDOFF);	/* 關閉LED燈 */
	}
	return 0;
}

/* 裝置操作函式 */
static struct file_operations led_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.write = led_write,
};

總體的驅動程式如下:

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/timer.h>
#include <linux/irq.h>
#include <linux/wait.h>
#include <linux/poll.h>
#include <linux/fs.h>
#include <linux/fcntl.h>
#include <linux/platform_device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
檔名		: leddriver.c
作者	  	: 左忠凱
版本	   	: V1.0
描述	   	: platform驅動
其他	   	: 無
論壇 	   	: www.openedv.com
日誌	   	: 初版V1.0 2019/8/13 左忠凱建立
***************************************************************/

#define LEDDEV_CNT		1			/* 裝置號長度 	*/
#define LEDDEV_NAME		"platled"	/* 裝置名字 	*/
#define LEDOFF 			0
#define LEDON 			1

/* 暫存器名 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

/* leddev裝置結構體 */
struct leddev_dev{
	dev_t devid;			/* 裝置號	*/
	struct cdev cdev;		/* cdev		*/
	struct class *class;	/* 類 		*/
	struct device *device;	/* 裝置		*/
	int major;				/* 主裝置號	*/		
};

struct leddev_dev leddev; 	/* led裝置 */

/*
 * @description		: LED開啟/關閉
 * @param - sta 	: LEDON(0) 開啟LED,LEDOFF(1) 關閉LED
 * @return 			: 無
 */
void led0_switch(u8 sta)
{
	u32 val = 0;
	if(sta == LEDON){
		val = readl(GPIO1_DR);
		val &= ~(1 << 3);	
		writel(val, GPIO1_DR);
	}else if(sta == LEDOFF){
		val = readl(GPIO1_DR);
		val|= (1 << 3);	
		writel(val, GPIO1_DR);
	}	
}

/*
 * @description		: 開啟裝置
 * @param - inode 	: 傳遞給驅動的inode
 * @param - filp 	: 裝置檔案,file結構體有個叫做private_data的成員變數
 * 					  一般在open的時候將private_data指向裝置結構體。
 * @return 			: 0 成功;其他 失敗
 */
static int led_open(struct inode *inode, struct file *filp)
{
	filp->private_data = &leddev; /* 設定私有資料  */
	return 0;
}

/*
 * @description		: 向裝置寫資料 
 * @param - filp 	: 裝置檔案,表示開啟的檔案描述符
 * @param - buf 	: 要寫給裝置寫入的資料
 * @param - cnt 	: 要寫入的資料長度
 * @param - offt 	: 相對於檔案首地址的偏移
 * @return 			: 寫入的位元組數,如果為負值,表示寫入失敗
 */
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	int retvalue;
	unsigned char databuf[1];
	unsigned char ledstat;

	retvalue = copy_from_user(databuf, buf, cnt);
	if(retvalue < 0) {
		return -EFAULT;
	}

	ledstat = databuf[0];		/* 獲取狀態值 */
	if(ledstat == LEDON) {
		led0_switch(LEDON);		/* 開啟LED燈 */
	}else if(ledstat == LEDOFF) {
		led0_switch(LEDOFF);	/* 關閉LED燈 */
	}
	return 0;
}

/* 裝置操作函式 */
static struct file_operations led_fops = {
	.owner = THIS_MODULE,
	.open = led_open,
	.write = led_write,
};

/*
 * @description		: flatform驅動的probe函式,當驅動與
 * 					  裝置匹配以後此函式就會執行
 * @param - dev 	: platform裝置
 * @return 			: 0,成功;其他負值,失敗
 */
static int led_probe(struct platform_device *dev)
{	
	int i = 0;
	int ressize[5];
	u32 val = 0;
	struct resource *ledsource[5];

	printk("led driver and device has matched!\r\n");
	/* 1、獲取資源 */
	for (i = 0; i < 5; i++) {
		ledsource[i] = platform_get_resource(dev, IORESOURCE_MEM, i); /* 依次MEM型別資源 */
		if (!ledsource[i]) {
			dev_err(&dev->dev, "No MEM resource for always on\n");
			return -ENXIO;
		}
		ressize[i] = resource_size(ledsource[i]);	
	}	

	/* 2、初始化LED */
	/* 暫存器地址對映 */
 	IMX6U_CCM_CCGR1 = ioremap(ledsource[0]->start, ressize[0]);
	SW_MUX_GPIO1_IO03 = ioremap(ledsource[1]->start, ressize[1]);
  	SW_PAD_GPIO1_IO03 = ioremap(ledsource[2]->start, ressize[2]);
	GPIO1_DR = ioremap(ledsource[3]->start, ressize[3]);
	GPIO1_GDIR = ioremap(ledsource[4]->start, ressize[4]);
	
	val = readl(IMX6U_CCM_CCGR1);
	val &= ~(3 << 26);				/* 清除以前的設定 */
	val |= (3 << 26);				/* 設定新值 */
	writel(val, IMX6U_CCM_CCGR1);

	/* 設定GPIO1_IO03複用功能,將其複用為GPIO1_IO03 */
	writel(5, SW_MUX_GPIO1_IO03);
	writel(0x10B0, SW_PAD_GPIO1_IO03);

	/* 設定GPIO1_IO03為輸出功能 */
	val = readl(GPIO1_GDIR);
	val &= ~(1 << 3);			/* 清除以前的設定 */
	val |= (1 << 3);			/* 設定為輸出 */
	writel(val, GPIO1_GDIR);

	/* 預設關閉LED1 */
	val = readl(GPIO1_DR);
	val |= (1 << 3) ;	
	writel(val, GPIO1_DR);
	
	/* 註冊字元裝置驅動 */
	/*1、建立裝置號 */
	if (leddev.major) {		/*  定義了裝置號 */
		leddev.devid = MKDEV(leddev.major, 0);
		register_chrdev_region(leddev.devid, LEDDEV_CNT, LEDDEV_NAME);
	} else {						/* 沒有定義裝置號 */
		alloc_chrdev_region(&leddev.devid, 0, LEDDEV_CNT, LEDDEV_NAME);	/* 申請裝置號 */
		leddev.major = MAJOR(leddev.devid);	/* 獲取分配號的主裝置號 */
	}
	
	/* 2、初始化cdev */
	leddev.cdev.owner = THIS_MODULE;
	cdev_init(&leddev.cdev, &led_fops);
	
	/* 3、新增一個cdev */
	cdev_add(&leddev.cdev, leddev.devid, LEDDEV_CNT);

	/* 4、建立類 */
	leddev.class = class_create(THIS_MODULE, LEDDEV_NAME);
	if (IS_ERR(leddev.class)) {
		return PTR_ERR(leddev.class);
	}

	/* 5、建立裝置 */
	leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, LEDDEV_NAME);
	if (IS_ERR(leddev.device)) {
		return PTR_ERR(leddev.device);
	}

	return 0;
}

/*
 * @description		: platform驅動的remove函式,移除platform驅動的時候此函式會執行
 * @param - dev 	: platform裝置
 * @return 			: 0,成功;其他負值,失敗
 */
static int led_remove(struct platform_device *dev)
{
	iounmap(IMX6U_CCM_CCGR1);
	iounmap(SW_MUX_GPIO1_IO03);
	iounmap(SW_PAD_GPIO1_IO03);
	iounmap(GPIO1_DR);
	iounmap(GPIO1_GDIR);

	cdev_del(&leddev.cdev);/*  刪除cdev */
	unregister_chrdev_region(leddev.devid, LEDDEV_CNT); /* 登出裝置號 */
	device_destroy(leddev.class, leddev.devid);
	class_destroy(leddev.class);
	return 0;
}

/* platform驅動結構體 */
static struct platform_driver led_driver = {
	.driver		= {
		.name	= "imx6ul-led",			/* 驅動名字,用於和裝置匹配 */
	},
	.probe		= led_probe,
	.remove		= led_remove,
};
		
/*
 * @description	: 驅動模組載入函式
 * @param 		: 無
 * @return 		: 無
 */
static int __init leddriver_init(void)
{
	return platform_driver_register(&led_driver);
}

/*
 * @description	: 驅動模組解除安裝函式
 * @param 		: 無
 * @return 		: 無
 */
static void __exit leddriver_exit(void)
{
	platform_driver_unregister(&led_driver);
}

module_init(leddriver_init);
module_exit(leddriver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wyw");

最後編寫測試APP的程式碼:

#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
檔名		: ledApp.c
作者	  	: 左忠凱
版本	   	: V1.0
描述	   	: platform驅動驅測試APP。
其他	   	: 無
使用方法	 :./ledApp /dev/platled  0 關閉LED
		     ./ledApp /dev/platled  1 開啟LED		
論壇 	   	: www.openedv.com
日誌	   	: 初版V1.0 2019/8/16 左忠凱建立
***************************************************************/
#define LEDOFF 	0
#define LEDON 	1

/*
 * @description		: main主程式
 * @param - argc 	: argv陣列元素個數
 * @param - argv 	: 具體引數
 * @return 			: 0 成功;其他 失敗
 */
int main(int argc, char *argv[])
{
	int fd, retvalue;
	char *filename;
	unsigned char databuf[2];
	
	if(argc != 3){
		printf("Error Usage!\r\n");
		return -1;
	}

	filename = argv[1];

	/* 開啟led驅動 */
	fd = open(filename, O_RDWR);
	if(fd < 0){
		printf("file %s open failed!\r\n", argv[1]);
		return -1;
	}
	
	databuf[0] = atoi(argv[2]);	/* 要執行的操作:開啟或關閉 */
	retvalue = write(fd, databuf, sizeof(databuf));
	if(retvalue < 0){
		printf("LED Control Failed!\r\n");
		close(fd);
		return -1;
	}

	retvalue = close(fd); /* 關閉檔案 */
	if(retvalue < 0){
		printf("file %s close failed!\r\n", argv[1]);
		return -1;
	}
	return 0;
}

相關文章