第一課:設備樹的引入與體驗

出自 百问网嵌入式Linux wiki
前往: 導覽搜尋

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

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

第01節_字符設備的三種寫法

怎麼寫驅動?

①看原理圖:

 a.確定引腳;

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

②寫驅動程序;

 起封裝作用;

③寫測試程序;

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

Ldd devicetree chapter1 1 001.jpg


當芯片引腳輸出低電平時,電流從高電平流向低電平,LED燈點亮;

當芯片引腳輸出高電平時,沒有電勢差,沒有電流流過,LED燈不亮;

從原理圖可以看出,控制了芯片引腳,就等於控制了燈。


在Linux里,操作硬件都是統一的接口,比如操作LED燈,需要先open,如果要讀取LED狀態就調用read,如果要操作LED就調用write函數,也可以通過ioctl去實現。

在驅動里,針對前面應用的每個調用函數,都寫一個對應的函數,實現對硬件的操作。

Ldd devicetree chapter1 1 002.jpg


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


如何寫驅動程序?

①分配一個file_operations結構體;

②設置:

 a. .open=led_open;把led引腳設置為輸出引腳

 b. .write=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引腳,則需要修改驅動源碼,重新編譯驅動文件,假如驅動放在內核中,則需要重新編譯內核

Ldd devicetree chapter1 4 001.png
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平台設備文件夾

Ldd devicetree chapter1 5 001.png

Ldd devicetree chapter1 5 002.png

  • 查看 reg 的地址,這裡面是以大字節須來描述這些值的

Ldd devicetree chapter1 5 003.png

  • 這個屬性有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属性 */
		of_property_read_s32(pdev->dev.of_node, "pin", &led_pin);
	}

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

我們通過驅動程序再次驗證了設備樹的屬性完全卻決於寫驅動程序的人
commpatible屬性必須是 jz2440_led 才可以和驅動匹配成功

我們寫驅動的人應該寫一個文檔,告訴寫應用程序的人設備樹的節點應該怎麼編寫

對於內核自帶的驅動文件,對應的設備樹的文檔一般放在Documentation\devicetree\bindings目錄中,裡面有各種架構的說明文檔以及各種協議的說明文檔, 這些驅動都能在 drivers 目錄下找到對應的驅動程序. 比如查看Documentation\devicetree\bindings\arm\samsung\exynos-chipid.txt裡面的內容如下

 SAMSUNG Exynos SoCs Chipid driver.
 Required properties:(必须填写的内容)
 - compatible : Should at least contain "samsung,exynos4210-chipid".
 - reg: offset and length of the register set
 Example:
	chipid@10000000 {
		compatible = "samsung,exynos4210-chipid";
		reg = <0x10000000 0x100>;
	};

我們自己寫的驅動說明文檔自然沒有適配到內核中去,所以只能期盼商家給你提供相應的說明文檔

參考同類型單板的設備樹文件 進入 arch\arm\boot\dts 目錄下裡面是各種單板的設備樹文件 比如

am335x-boneblack.dts和am335x-boneblack-wireless.dts

發現多了wifi的信息,通過對比設備樹文件,我們可以看出怎麼寫wifi設備節點,就知道如何添加設備節點.

網上搜索 實在不行就研究驅動源碼 一個好的驅動程序,它會儘量確定所用資源,只把不能確定的資源留給設備樹,讓設備樹來指定。