第一課:linux裝置樹的引入與體驗(基於linux4.19核心版本)
- 轉載請註明原文地址:http://wiki.100ask.org/Linux_devicetree
本套視訊面向如下三類學員:
- 有Linux驅動開發基礎的人, 可以挑感興趣的章節觀看;
- 沒有Linux驅動開發基礎但是願意學習的人,請按順序全部觀看,我會以比較簡單的LED驅動為例講解;
- 完全沒有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屬性 */