1. 程式人生 > >第一課:linux裝置樹的引入與體驗(基於linux4.19核心版本)

第一課:linux裝置樹的引入與體驗(基於linux4.19核心版本)

本套視訊面向如下三類學員:

  1. 有Linux驅動開發基礎的人, 可以挑感興趣的章節觀看;
  2. 沒有Linux驅動開發基礎但是願意學習的人,請按順序全部觀看,我會以比較簡單的LED驅動為例講解;
  3. 完全沒有Linux驅動知識,又不想深入學習的人, 比如應用開發人員,不得已要改改驅動, 等全部錄完後,我會更新本文件,那時再列出您需要觀看的章節。

第01節_字元裝置的三種寫法

怎麼寫驅動?

①看原理圖:

a.確定引腳;

b.看晶片手冊,確定如何操作引腳;

②寫驅動程式;

起封裝作用;

③寫測試程式;

如下原理圖,VCC經過一個限流電阻到達LED的一端,再通向晶片的引腳上。

當晶片引腳輸出低電平時,電流從高電平流向低電平,LED燈點亮;
當晶片引腳輸出高電平時,沒有電勢差,沒有電流流過,LED燈不亮;
從原理圖可以看出,控制了晶片引腳,就等於控制了燈。

在Linux裡,操作硬體都是統一的介面,比如操作LED燈,需要先open,如果要讀取LED狀態就呼叫read,如果要操作LED就呼叫write函式,也可以通過ioctl去實現。
在驅動裡,針對前面應用的每個呼叫函式,都寫一個對應的函式,實現對硬體的操作。

可以看出驅動程式起封裝作用,它讓應用程式訪問硬體變得簡單,遮蔽了硬體更加複雜的操作。

如何寫驅動程式?

①分配一個file_operations結構體;
②設定:
 a. .open=led_open;把led引腳設定為輸出引腳
 b. .read=led_write;根據APP傳入的值設定引腳狀態

③註冊(告訴核心),register_chrdev(主裝置號,file_operations,name)
④入口函式
⑤出口函式

在驅動中如何指定LED引腳?

有如下三種方法:
①傳統方法:在程式碼led_drv.c中寫死;
②匯流排裝置驅動模型:
 a. 在led_drv.c裡分配、註冊、入口、出口等
 b. 在led_dev.c裡指定引腳
③使用裝置樹指定引腳
 a. 在led_drv.c裡分配、註冊、入口、出口等
 b. 在jz2440.dts裡指定引腳

可以看到,’’‘無論何種方法,驅動寫法的核心不變,差別在於如何指定硬體資源’’’。
對比下三種方法的優缺點。
假設這樣一個情況,某公司用同一個晶片做了兩款產品,其中一款是TV(電視盒子),使用Pin1作為LED的指示燈控制引腳,其中一款是Cam(監控攝像頭),使用Pin2作為LED的指示燈控制引腳。

TV裝置 Cam裝置 優缺點
1.傳統方法 led_drv.c
①分配一個file_operations結構體;
②設定:
 a .open=led_open;設定Pin1為輸出引腳
 b .read=led_read;根據APP傳入的值設定引腳狀態
③註冊(告訴核心)
④入口函式
⑤出口函式
led_drv.c

①分配一個file_operations結構體;
②設定:
 a. .open=led_open;設定Pin2為輸出引腳
 b. .read=led_read;根據APP傳入的值設定引腳狀態
③註冊(告訴核心)
④入口函式
⑤出口函式
優點:簡單

缺點:不易擴充套件,需要重新編譯
2.匯流排裝置驅動模型 led_drv.c
①分配/設定/註冊 platform_driver;
② .probe:
 a 分配一個file_operations結構體;
b .open=led_open;設定平臺裝置總指定的引腳為輸出引腳
  .read=led_read;根據APP傳入的值設定引腳狀態
c註冊
③ .driver{ .name }

led_dev.c
①分配/設定/註冊 platform_device;
② .resource:指定引腳;,name為Pin1

led_dev.c

①分配/設定/註冊 platform_driver;
② .resource:指定引腳;,name為Pin2
優點:易擴充套件

缺點:稍複雜,冗餘程式碼太多,需要重新編譯
3.裝置樹 led_drv.c
①分配/設定/註冊 platform_driver;
② .probe:
 a 分配一個file_operations結構體;
b .open=led_open;設定平臺裝置總指定的引腳為輸出引腳
  .read=led_read;根據APP傳入的值設定引腳狀態
c註冊
③ .driver{ .name }

.dts指定資源
核心根據dts生成的dtb檔案分配/設定/註冊platform_device
.dts指定資源

核心根據dts生成的dtb檔案分配/設定/註冊platform_device
優點:易擴充套件

缺點:稍複雜,冗餘程式碼太多,需要重新編譯

第02節_字元裝置驅動的傳統寫法

在上一節視訊裡我們介紹了三種編寫驅動的方法,也對比了它們的優缺點,後面我們將使用比較快速的方法寫出驅動程式,因為寫驅動程式不是我們這套視訊的重點,所以儘快的把驅動程式寫出來,給大家展示一下。

這節視訊我們使用傳統的方法編寫字元驅動程式,以最簡單的點燈驅動程式為示例。
先回顧下寫字元裝置驅動的五個步驟:
1.2.3.分配/設定/註冊file_operations
4.入口
5.出口
所謂分配file_operations,我們可以定義一個file_operations結構體,就不需要分配了。

static struct file_operations myled_oprs = {
	.owner = THIS_MODULE, //表示這個模組本身
	.open  = led_open,
	.write = led_write,
	.release = led_release,
};

定義好了file_operations結構體,再去入口函式註冊結構體。

static int myled_init(void)
{
	major = register_chrdev(0, "myled", &myled_oprs);

	return 0;
}

第一個引數:主裝置號寫0,讓系統為我們分配;
第二個引數:設定名字,沒有特殊要求;
第三個引數:file_operations結構體;
對應的出口操作進行相反向操作:

static void myled_exit(void)
{
	unregister_chrdev(major, "myled");
}

然後用巨集module_init對入口、出口函式進行修飾,表示它們和普通函式不一樣:

module_init(myled_init);
module_exit(myled_exit);

module_init(myled_init)實際就是int init_module(void) attribute((alias(“myled_init”))),表示myled_init的別名是init_module,以後就可以使用init_module來引用myled_init

此外,還要加上GPL協議:

MODULE_LICENSE("GPL");

寫到這裡,驅動程式的框架已經搭建起來了,接下來實現具體的硬體操作函式:led_open()和led_write()。
在led_open()裡把對應的引腳配置為輸出引腳,在led_write()根據應用程式傳入的資料點燈,讓其輸出高電平或低電平。
為了讓程式更具有擴充套件性,把GPIO的暫存器放在一個數組裡:

static unsigned int gpio_base[] = {
	0x56000000, /* GPACON */
	0x56000010, /* GPBCON */
	0x56000020, /* GPCCON */
	0x56000030, /* GPDCON */
	0x56000040, /* GPECON */
	0x56000050, /* GPFCON */
	0x56000060, /* GPGCON */
	0x56000070, /* GPHCON */
	0,          /* GPICON */
	0x560000D0, /* GPJCON */
};

定義好了引腳的組,還得確定使用該組的哪個引腳,使用巨集來確定哪個引腳:

#define S3C2440_GPA(n)  (0<<16 | n)
#define S3C2440_GPB(n)  (1<<16 | n)
#define S3C2440_GPC(n)  (2<<16 | n)
#define S3C2440_GPD(n)  (3<<16 | n)
#define S3C2440_GPE(n)  (4<<16 | n)
#define S3C2440_GPF(n)  (5<<16 | n)
#define S3C2440_GPG(n)  (6<<16 | n)
#define S3C2440_GPH(n)  (7<<16 | n)
#define S3C2440_GPI(n)  (8<<16 | n)
#define S3C2440_GPJ(n)  (9<<16 | n)

後面就可以向對應巨集傳入對應位,得到對應組的對應引腳。
檢視原理圖,知道我們要使用的引腳是GPF5,因此定義 led_pin = s3c2440_GPF(5)。

static int led_open (struct inode *node, struct file *filp)
{
	/* 把LED引腳配置為輸出引腳 */
	/* GPF5 - 0x56000050 */
	int bank = led_pin >> 16;
	int base = gpio_base[bank];

	int pin = led_pin & 0xffff;
	gpio_con = ioremap(base, 8);  
	if (gpio_con) {
		printk("ioremap(0x%x) = 0x%x\n", base, gpio_con);
	}
	else {
		return -EINVAL;
	}
	
	gpio_dat = gpio_con + 1;

	*gpio_con &= ~(3<<(pin * 2));
	*gpio_con |= (1<<(pin * 2));  

	return 0;
}

在Linux中,不能直接操作基地址,需要使用ioremap()對映。

對於基地址,定義全域性指標來表示,gpio_con表示控制暫存器,gpio_dat表示資料暫存器。

這裡將GPF5的第二個引腳先清空,再設定為1,表示輸出引腳。

接下來是寫函式:

static ssize_t led_write (struct file *filp, const char __user *buf, size_t size, loff_t *off)
{
	/* 根據APP傳入的值來設定LED引腳 */
	unsigned char val;
	int pin = led_pin & 0xffff;
	
	copy_from_user(&val, buf, 1);

	if (val)
	{
		/* 點燈 */
		*gpio_dat &= ~(1<<pin);
	}
	else
	{
		/* 滅燈 */
		*gpio_dat |= (1<<pin);
	}

	return 1; /* 已寫入1個數據 */
}

注意這裡的__user巨集起強調作用,告訴你buf來自應用空間,在核心裡不能直接使用。
使用copy_from_user()將使用者空間的資料拷貝到核心空間。
再根據傳入的值,設定gpio_dat的值,來點亮或者熄滅pin所對應的燈。
至此,這個驅動程式已經具備操作硬體的功能,但我們還要增加一些內容,比如我們先註冊驅動後,自動建立節點資訊。
在入口函式裡,使用class_create()建立class,並且使用device_create()建立裝置。

static int myled_init(void)
{
	major = register_chrdev(0, "myled", &myled_oprs);

	led_class = class_create(THIS_MODULE, "myled");
	device_create(led_class, NULL, MKDEV(major, 0), NULL, "led"); /* /dev/led */

	return 0;
}

出口函式需要進行相反操作:

static void myled_exit(void)
{
	unregister_chrdev(major, "myled");
	device_destroy(led_class,  MKDEV(major, 0));
	class_destroy(led_class);
}

還有在release函式裡,釋放前面的iormap()的資源

static int led_release (struct inode *node, struct file *filp)
{
	printk("iounmap(0x%x)\n", gpio_con);
	iounmap(gpio_con);
	return 0;
}

最後把以前的測試程式拷貝過來,簡單修改一下,見網盤led_driver/001_led_drv_traditional/ledtest.c
可以看出,這種傳統寫驅動程式的方法把硬體資源寫在了程式碼裡,換個LED,換個引腳,就得去修改 led_pin = s3c2440_GPF(5),然後重新編譯,載入。

第03節_字元裝置驅動的編譯測試

這節課來講解一下測試和編譯的過程。
驅動程式的編譯依賴於核心,在驅動程式裡的一堆標頭檔案,是來自於核心的,因此我們需要先編譯核心
接下來我們要編譯驅動程式,編譯測試程式,並在單板上測試一樣。

首先從網盤下載:

doc_and_sources_for_device_tree/source_and_images/source_and_images下的核心原始碼和補丁;
doc_and_sources_for_device_tree/source_and_images/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabi.tar.xz編譯核心和驅動的交叉編譯工具鏈;
doc_and_sources_for_device_tree/source_and_images/arm-linux-gcc-4.3.2.tar.bz2編譯測試程式的交叉編譯工具鏈;
doc_and_sources_for_device_tree/source_and_images/readme.txt介紹了一些編譯器、工具的使用、uboot等筆記,需要時可以看一看;

1.編譯核心
將核心原始碼、補丁、編譯核心的交叉工具鏈上傳到Ubuntu,然後解壓、打補丁。
再解壓工具鏈,設定工具鏈環境,最後編譯。
編譯中遇到錯誤提示,嘗試百度搜索,一般都能找到解決方法。

2.編譯驅動
待核心編譯完後,修改Makefile,編譯驅動。
3.編譯應用程式
解壓編譯應用程式的交叉編譯工具鏈,修改環境變數,編譯應用程式。
4.載入驅動和執行測試程式
使用nfs掛載該目錄,載入驅動,執行測試程式。

第04節_匯流排裝置驅動模型

匯流排驅動模型是為了解決什麼問題呢?

  • 使用之前的驅動模型,編寫一個led驅動程式,如果需要修改gpio引腳,則需要修改驅動原始碼,重新編譯驅動檔案,假如驅動放在核心中,則需要重新編譯核心

    bus匯流排是虛擬的概念,並非硬體,dev註冊設定某個結構體,這個裝置也就是平臺裝置
    struct platform_device {
	const char	*name;
	int		id;
	bool		id_auto;
	struct device	dev;
	u32		num_resources;
	/*resource 裡面確定使用那些資源*/
	struct resource	*resource;

	const struct platform_device_id	*id_entry;
	char *driver_override; /* Driver name to force a match */

	/* MFD cell pointer */
	struct mfd_cell *mfd_cell;

	/* arch specific additions */
	struct pdev_archdata	archdata;
	};

drv那面定義platform_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;
    };

裝置和驅動如何進行通訊呢

*通過bus進行匹配 platform_match函式確定(dev,drv)若匹配則呼叫drv中的probe函式

    struct bus_type platform_bus_type = {
    	.name		= "platform",
    	.dev_groups	= platform_dev_groups,
    	.match		= platform_match,
    	.uevent		= platform_uevent,
    	.pm		= &platform_dev_pm_ops,
    };

這種模型只是一種程式設計技巧一種機制
並不是驅動程式的核心

platform_match是如何判斷dev drv是匹配的?

判斷方法是比較dev 和drv 各自的name來進行匹配

  • 平臺裝置platform_device這面有name
  • platform_driver這面有 driver (裡面含有name) 還有id_table(包含 name driver_data)
  • id_table裡面的內容表示所支援一個或多個的裝置名
static int platform_match(struct device *dev, struct device_driver *drv)
{		
	/*省略部分無用程式碼*/
	/* Then try to match against the id table */
	if (pdrv->id_table)
		return platform_match_id(pdrv->id_table, pdev) != NULL;

	/* fall-back to driver name match */
	return (strcmp(pdev->name, drv->name) == 0);
}

也就是優先比較 id_table中名字,如果沒有則對比driver中名字

  • 根據二期視訊led程式碼進行修改
/* 分配/設定/註冊一個platform_device */
/*設定資源*/
static struct resource led_resource[] = {
    [0] = {
		/*指明瞭使用那個引腳*/
        .start = S3C2440_GPF(5),
		/*end並不重要,可以隨意指定*/
        .end   = S3C2440_GPF(5),
        .flags = IORESOURCE_MEM,
    },
};

static void led_release(struct device * dev)
{
}


static struct platform_device led_dev = {
    .name         = "myled",
    .id       = -1,
    .num_resources    = ARRAY_SIZE(led_resource),
    .resource     = led_resource,
    .dev = { 
    	.release = led_release, 
	},
};

/*入口函式去註冊平臺裝置*/
static int led_dev_init(void)
{
	platform_device_register(&led_dev);
	return 0;
}
/*出口函式去釋放這個平臺裝置*/
static void led_dev_exit(void)
{
	platform_device_unregister(&led_dev);
}

module_init(led_dev_init);
module_exit(led_dev_exit);
  • led_drv驅動檔案
static int led_probe(struct platform_device *pdev)
{
	struct resource		*res;

	/* 根據platform_device的資源進行ioremap */
	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	led_pin = res->start;

	major = register_chrdev(0, "myled", &myled_oprs);

	led_class = class_create(THIS_MODULE, "myled");
	device_create(led_class, NULL, MKDEV(major, 0), NULL, "led"); /* /dev/led */
	
	return 0;
}


struct platform_driver led_drv = {
	.probe		= led_probe,
	.remove		= led_remove,
	.driver		= {
		.name	= "myled",
	}
};


static int myled_init(void)
{
	platform_driver_register(&led_drv);
	return 0;
}

static void myled_exit(void)
{
	platform_driver_unregister(&led_drv);
}

Makefile檔案

KERN_DIR = /work/system/linux-4.19-rc3

all:
	make -C $(KERN_DIR) M=`pwd` modules 

clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order

obj-m	+= led_drv.o
obj-m	+= led_dev.o

執行測試程式

如果我需要更換一個led
則只需要修改 led_dev led_resource結構體中的引腳即可

static struct resource led_resource[] = {
    [0] = {
        .start = S3C2440_GPF(6),
        .end   = S3C2440_GPF(6),
        .flags = IORESOURCE_MEM,
    },
};

裝置和驅動的匹配是如何完成的?

  • dev這面有裝置連結串列
  • drv這面也有驅動的結構體連結串列
  • 通過match函式進行對比,如果相同,則呼叫drv中的probe函式

第05節_使用裝置樹時對應的驅動程式設計

  • 本節介紹怎麼使用裝置樹怎麼編寫對應的驅動程式

  • 只是平臺裝置的構建區別,以前構造平臺裝置是在.c檔案中,使用裝置樹構造裝置節點原本不存在,需要在dts檔案中構造節點,節點中含有資源

  • dts被編譯成dtb檔案傳給核心,核心會處理解析dtb檔案得到device_node結構體,之後變成platform_device結構體,裡面含有資源(資源來自dts檔案)

  • 我們定義的led裝置節點

    	led {
    		compatible = "jz2440_led";
    		reg = <S3C2410_GPF(5) 1>;
    	};
  • 以後就使用compatible找到核心支援這個裝置節點的平臺driver reg = <S3C2410_GPF(5) 1>; 就是暫存器地址的對映

修改好後編譯 裝置樹檔案 make dtb

拷貝到tftp資料夾,開發板啟動

  • 進入 /sys/devices/platform 目錄檢視是否有5005.led平臺裝置資料夾

  • 檢視 reg 的地址,這裡面是以大位元組須來描述這些值的

這個屬性有8個位元組,對應兩個數值

  • 第一個值S3C2410_GPF(5)是我們的起始地址,對應 #define S3C2410_GPF(_nr) ((5<<16) + (_nr))
  • 第二個值1 本意是指暫存器的大小

如何去寫平臺驅動?
通過bus匯流排去匹配裝置驅動
在 platform_match函式中,通過

	/* Attempt an OF style match first */
	if (of_driver_match_device(dev, drv))
		return 1;

進入 of_device.h中

/**
 * of_driver_match_device - Tell if a driver's of_match_table matches a device.
 * @drv: the device_driver structure to test
 * @dev: the device structure to match against
 */
static inline int of_driver_match_device(struct device *dev,
					 const struct device_driver *drv)
{
	return of_match_device(drv->of_match_table, dev) != NULL;
}

of_match_table結構體
include\linux\mod_devicetable.h

/*
 * Struct used for matching a device
 */
struct of_device_id {
	char	name[32];
	char	type[32];
	char	compatible[128];
	const void *data;
};
  • compatible 也就是從dts得到的platform_device裡有compatible 屬性,兩者進行對比,一樣就表示匹配
  • 寫led驅動,修改led_drv.c
  • 新增
static const struct of_device_id of_match_leds[] = {
	{ .compatible = "jz2440_led", .data = NULL },
	{ /* sentinel */ }
};

*修改

struct platform_driver led_drv = {
	.probe		= led_probe,
	.remove		= led_remove,
	.driver		= {
		.name	= "myled",
		.of_match_table = of_match_leds, /* 能支援哪些來自於dts的platform_device */
	}
};

*修改Makefile並編譯

  • 如果修改燈怎麼辦?
    • 直接修改裝置樹中的led裝置節點
    	led {
    		compatible = "jz2440_led";
    		reg = <S3C2410_GPF(6) 1>;
    	};

上傳編譯,直接使用新的dtb檔案
我們使用另外一種方法指定引腳

	led {
		compatible = "jz2440_led";
		pin = <S3C2410_GPF(5)>;
	};

修改led_drv中的probe函式

在of.h中找到獲取of屬性的函式 of_property_read_s32

static int led_probe(struct platform_device *pdev)
{
	struct resource		*res;

	/* 根據platform_device的資源進行ioremap */
	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (res) {
		led_pin = res->start;
	}
	else {
		/* 獲得pin屬性 */
		of_property_read_s32(pdev->dev.of_node, "pin", &led_pin);
	}

	if (!led_pin) 
	{
		printk("can not get pin for led\n");
		return -EINVAL;
	}
		

	major = register_chrdev(0, "myled", &myled_oprs);

	led_class = class_create(THIS_MODULE, "myled");
	device_create(led_class, NULL, MKDEV(major, 0), NULL, "led"); /* /dev/led */
	
	return 0;
}
  • 從新編譯裝置樹 和led驅動檔案
    在platform_device結構體中的struct device dev;中對於dts生成的platform_device這裡含有of_node

of_node中含有屬性,這取決於裝置樹,比如compatible屬性
讓後註冊/配置/file_operation

第06節_只想使用裝置樹不想深入研究怎麼辦

寄希望於寫驅動程式的人,提供了文件/示例/程式寫得好適配性強

根據之前寫的裝置樹
led {
compatible = “jz2440_led”;
reg = <S3C2410_GPF(6) 1>;
};
led {
compatible = “jz2440_led”;
pin = <S3C2410_GPF(5)>;
};

可以通過reg指定引腳也可以通過pin指定引腳,我們在裝置樹中如何指定引腳完全取決於驅動程式
既可以獲取pin屬性值也可以獲取reg屬性值

	/* 根據platform_device的資源進行ioremap */
	res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (res) {
		led_pin = res->start;
	}
	else {
		/* 獲得pin屬性 */