接着上一章,本章來實現控制LED的亮滅操作:
一、驅動框架
#include <linux/fs.h> #include <linux/init.h> /* 定義文件內私有結構體 */ struct led_device { struct cdev cdev; int stat; /* 用於保存LED狀態,0為滅,1為亮 */ }; /* LED write()函數 */ static ssize_t led_write(struct file *filep, const char __user * buf, size_t len, loff_t *ppos) { return 0; } /* LED open()函數 */ static int led_open(struct inode *inodep, struct file *filep) { return 0; } /* 把定義的函數接口集合起來,方便系統調用 */ static const struct file_operations led_fops = { .open = led_open, .write = led_write, }; /* 驅動初始化函數 */ static int __init led_init(void) { return 0; } /* 驅動卸載函數 */ static void __exit led_exit(void) { } /* 聲明段屬性 */ module_init(led_init); module_exit(led_exit); MODULE_LICENSE("GPL");
我們在驅動程序實現的write()和open()函數的格式必須遵循struct file_operations里面的函數指針:
struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t); ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long); long (*compat_ioctl) (struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *, fl_owner_t id); int (*release) (struct inode *, struct file *); ... };
通常我們不會實現struct file_operations里面的所有函數,只會實現一些針對某些設備需要用到的函數
驅動中定義的led_init()和led_exit()函數需要實現向上層注冊字符設備、struct file_operations等
這兩個函數所使用到的__init和__exit,在此以__init為例展開:
#define __init __attribute__((".init.text")) \ __attribute__((__cold__)) \ __attribute__((no_instrument_function))
可以看到led_init()函數代碼會被定位到.init.text段中
這個段定義在include/asm-generic/vmlinux.lds.h中
#define INIT_TEXT_SECTION(inittext_align) \ . = ALIGN(inittext_align); \ .init.text : AT(ADDR(.init.text) - LOAD_OFFSET) { \ VMLINUX_SYMBOL(_sinittext) = .; \ INIT_TEXT \ VMLINUX_SYMBOL(_einittext) = .; \ }
在arch/arm/kernel/vmlinux.lds.S中使用
INIT_TEXT_SECTION(8)
驅動程序中調用的module_init()和module_exit()函數用於向上層注冊led_init()和led_exit()
#define module_init(x) __initcall(x) #define __initcall(fn) device_initcall(fn) ... #define core_initcall(fn) __define_initcall("1",fn,1) #define core_initcall_sync(fn) __define_initcall("1s",fn,1s) #define postcore_initcall(fn) __define_initcall("2",fn,2) #define postcore_initcall_sync(fn) __define_initcall("2s",fn,2s) #define arch_initcall(fn) __define_initcall("3",fn,3) #define arch_initcall_sync(fn) __define_initcall("3s",fn,3s) #define subsys_initcall(fn) __define_initcall("4",fn,4) #define subsys_initcall_sync(fn) __define_initcall("4s",fn,4s) #define fs_initcall(fn) __define_initcall("5",fn,5) #define fs_initcall_sync(fn) __define_initcall("5s",fn,5s) #define rootfs_initcall(fn) __define_initcall("rootfs",fn,rootfs) #define device_initcall(fn) __define_initcall("6",fn,6) #define device_initcall_sync(fn) __define_initcall("6s",fn,6s) #define late_initcall(fn) __define_initcall("7",fn,7) #define late_initcall_sync(fn) __define_initcall("7s",fn,7s) ... #define __define_initcall(fn, id) \ static initcall_t __initcall_##fn##id __used \ __attribute__((__section__(".initcall" #id ".init"))) = fn; \ LTO_REFERENCE_INITCALL(__initcall_##fn##id)
最終,led_init()函數的地址會被定位到.initcall6.init段中
那么initcall為什么要分成這么多段呢?
系統的初始化時,所有的東西都必須按照一定的順序初始化
對於驅動注冊,是在上面的initcall6里面實現的。而要實現設備驅動的注冊,必須要在設備驅動模型初始化完之后才能進行,否則如果設備驅動的管理程序都還沒初始化,則驅動的注冊肯定就有問題了。而要想讓初始化階段先初始化驅動的管理程序,如果靠函數依次調用,因為內核的內容太龐大,這明顯不可能實現。所以初始化階段,內核按先后順序分了16個子階段階段
通常越靠前的是越底層越核心的初始化,通常后面的初始化對前面的都有一定的依賴
總結起來就是:
1. __init修飾的函數,表示把該函數放入init.text這個代碼段
2. module_init修飾的函數,表示把init.text代碼段中的函數地址,存到init.data段
3. 內核啟動時,會根據initcall后面的數字大小,分層進行調用初始化
驅動程序中的MODULE_LICENSE("GPL");用於表示許可證,不需要深度了解
現在我們在框架的基礎上完成注冊字符設備、struct file_operations等操作
二、完成init()函數和exit()函數
1 ... 2 3 static int g_major; 4 module_param(g_major, int, S_IRUGO); 5 6 static struct led_device* dev; 7 static struct class* scls; 8 static struct device* sdev; 9 10 ... 11 12 static int __init led_init(void) 13 { 14 int ret; 15 dev_t devt; 16 17 /* 1. 申請設備號 */ 18 if (g_major) { 19 devt = MKDEV(g_major, 0); 20 ret = register_chrdev_region(devt, 1, "led"); 21 } 22 else 23 ret = alloc_chrdev_region(&devt, 0, 1, "led"); 24 if (ret) 25 return ret; 26 27 /* 2. 申請文件內私有結構體 */ 28 dev = kzalloc(sizeof(struct led_device), GFP_KERNEL); 29 if (dev == NULL) { 30 ret = -ENOMEM; 31 goto fail_malloc; 32 } 33 34 /* 3. 注冊字符設備驅動 */ 35 cdev_init(&dev->cdev, &led_fops); /* 初始化cdev並鏈接file_operations和cdev */ 36 ret = cdev_add(&dev->cdev, devt, 1); /* 注冊cdev */ 37 if (ret) 38 return ret; 39 40 /* 4. 創建類設備,insmod后會生成/dev/led設備文件 */ 41 scls = class_create(THIS_MODULE, "led"); 42 sdev = device_create(scls, NULL, devt, NULL, "led"); 43 44 return 0; 45 46 fail_malloc: 47 unregister_chrdev_region(devt, 1); 48 49 return ret; 50 } 51 52 static void __exit led_exit(void) 53 { 54 /* 鏡像注銷 */ 55 dev_t devt = MKDEV(g_major, 0); 56 57 device_destroy(scls, devt); 58 class_destroy(scls); 59 60 cdev_del(&(dev->cdev)); 61 kfree(dev); 62 63 unregister_chrdev_region(devt, 1); 64 } 65 66 ...
代碼中第4行:module_param(g_major, int, S_IRUGO)表示int型變量g_major可以通過外部向內核傳遞值
S_IRUGO表示數值的權限為0444
函數原型如下,此函數用於在加載模塊時或者模塊加載以后傳遞參數給模塊
module_param(name,type,perm);
函數參數:
name:模塊參數的名稱
type:模塊參數的數據類型,如bool、charp(字符指針)、short、int、long、ulong(無符號long)
perm:模塊參數的訪問權限
代碼中第15行:dev_t devt定義了設備號,為32位,其中高12位為主設備號,低20位為次設備號
主設備號用來表示一個特定的驅動程序;次設備號用來表示使用該驅動程序的各設備。例如TINY4412,有4個LED,每個LED都可以獨立的打開或者關閉。那么,這個LED的字符設備驅動程序,可以將其主設備號注冊成5號設備,次設備號分別為1、2、3和4。這里,次設備號就分別對應4個LED
設備文件通常都在/dev目錄下:
如上圖的/dev/tty,它的主設備號是5,次設備號是0
使用以下宏可以從dev_t中獲取主設備號和次設備號:
MAJOR(dev_t dev)
MINOR(dev_t dev)
使用以下宏則可以通過主設備號和次設備號生成dev_t:
MKDEV(int major, int minor)
代碼中第20行和第23行:register_chrdev_region()和alloc_chrdev_region()用於向系統申請設備號,這兩個函數原型為:
int register_chrdev_region(dev_t from, unsigned count, const char *name) int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
函數參數:
from:要分配的設備號范圍的初始值,其中的次設備號通常設置為0
count:連續編號設備個數
name:設備名稱
dev:alloc_chrdev_region()返回的設備號
baseminor:次設備號起始值,通常設置為0
register_chrdev_region()函數用於已知起始設備的設備號情況;而alloc_chrdev_region()函數用於設備號未知的情況,由系統分配並返回分配對的設備號
釋放設備號函數原型為:
void unregister_chrdev_region(dev_t from, unsigned count)
代碼中第28行:kzalloc()用於申請一片內核內存,並清空內存數據,詳細了解可查看:Linux驅動函數解讀第一節
Linux內核提供了一組函數操作cdev結構體:
cdev_init()用於初始化cdev的成員,並建立cdev和file_operations之間的鏈接
cdev_alloc()用於動態申請一個cdev內存,本節代碼使用的申請內存函數為kzalloc()
cdev_add()函數和cdev_del()函數分別向系統添加和刪除一個cdev,完成字符設備的注冊和注銷
代碼中第7行:struct class用於表示一個類,類是一個設備的高層視圖,它抽象出了低層的實現細節,大概意思就是抽象出了一個通用的接口,類似於C++的面向對象的編程方式
代碼中第8行:struct device用於表示一個設備,關於device的注冊過程可以查看Linux驅動函數解讀第二節
我們可以把類當作一個班級,設備當作學生。班級用於容納學生,當老師來上課時,老師只需要講一遍,學生就都可以聽到(函數抽象)
三、完成write()函數、open()函數和release()函數
1 static volatile unsigned long *gpm4con; 2 static volatile unsigned long *gpm4dat; 3 4 /* LED write()函數 */ 5 static ssize_t led_write(struct file *filep, const char __user * buf, size_t len, loff_t *ppos) 6 { 7 struct led_device *dev = filep->private_data; 8 9 if (copy_from_user(&(dev->stat), buf, 1)) 10 return -EFAULT; 11 12 if (dev->stat == 1) 13 *gpm4dat &= ~((1 << 3) | (1 << 2) | 1); 14 else 15 *gpm4dat |= ((1 << 3) | (1 << 2) | 1); 16 17 return 1; 18 } 19 20 /* LED open()函數 */ 21 static int led_open(struct inode *inodep, struct file *filep) 22 { 23 struct led_device *dev; 24 25 dev = container_of(inodep->i_cdev, struct led_device, cdev); 26 // 放入私有數據中 27 filep->private_data = dev; 28 29 // 映射LED 30 gpm4con = ioremap(0x110002E0, 8); 31 gpm4dat = gpm4con + 1; 32 // 設為輸入引腳,滅燈 33 *gpm4con = 0x1111; 34 *gpm4dat |= ((1 << 3) | (1 << 2) | 1); 35 36 return 0; 37 } 38 39 static int led_close(struct inode *inodep, struct file *filep) 40 { 41 iounmap(gpm4con); 42 43 return 0; 44 } 45 46 /* 把定義的函數接口集合起來,方便系統調用 */ 47 static const struct file_operations led_fops = { 48 .owner = THIS_MODULE, 49 .write = led_write, 50 .open = led_open, 51 .release = led_close, 52 };
代碼中第5行:write()函數使用了文件私有數據(filp->private_data)。實際上,大多數Linux驅動遵循一個“潛規則”,那就是將文件的私有數據private_data指向設備結構體,再用read()、write()等函數通過private_data訪問設備結構體
需要注意的是,用戶空間不能直接訪問內核空間的內存,因此在read()函數中一般使用copy_to_user(),在write()函數中一般使用copy_from_user()來完成用戶空間和內核空間的數據復制,兩函數原型為:
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n) unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
函數參數以及返回值:
to:復制到的地址
from:待復制的地址
n:復制字節數
返回值:兩函數均不返回被復制的字節數,成功返回0,失敗返回負值
代碼中第25行:container_of()函數可以參考:Linux驅動函數解讀第三節
在Linux系統中,開啟MMU后,我們就不能直接使用寄存器的硬件地址(或者說我們不知道,寄存器硬件地址被映射到哪塊內存了),所以我們只能使用虛擬地址來操縱寄存器。而目前我們不知道虛擬地址,只知道物理地址
所以內核給我們提供了一個接口函數ioremap()。它會建立一個新的頁表,可以通過寄存器的物理地址得到寄存器的虛擬地址。
void __iomem *ioremap(phys_addr_t offset, unsigned long size)
函數參數以及返回值:
offset:物理地址
size:寄存器大小
返回值:成功返回虛擬地址,失敗返回-1
ioremap()函數對應的釋放函數為iounmap():
void iounmap(void __iomem *addr)
函數參數:
addr:ioremap()函數返回的虛擬地址
四、完整代碼
led源代碼:

1 #include <linux/module.h> 2 #include <linux/fs.h> 3 #include <linux/init.h> 4 #include <linux/cdev.h> 5 #include <linux/slab.h> 6 #include <linux/device.h> 7 8 #include <asm/uaccess.h> 9 #include <asm/io.h> 10 11 /* 定義文件內私有結構體 */ 12 struct led_device { 13 struct cdev cdev; 14 int stat; /* 用於保存LED狀態,0為滅,1為亮 */ 15 }; 16 17 static int g_major; 18 module_param(g_major, int, S_IRUGO); 19 20 static struct led_device* dev; 21 static struct class* scls; 22 static struct device* sdev; 23 24 static volatile unsigned long *gpm4con; 25 static volatile unsigned long *gpm4dat; 26 27 /* LED write()函數 */ 28 static ssize_t led_write(struct file *filep, const char __user * buf, size_t len, loff_t *ppos) 29 { 30 struct led_device *dev = filep->private_data; 31 32 if (copy_from_user(&(dev->stat), buf, 1)) 33 return -EFAULT; 34 35 if (dev->stat == 1) 36 *gpm4dat &= ~((1 << 3) | (1 << 2) | 1); 37 else 38 *gpm4dat |= ((1 << 3) | (1 << 2) | 1); 39 40 return 1; 41 } 42 43 /* LED open()函數 */ 44 static int led_open(struct inode *inodep, struct file *filep) 45 { 46 struct led_device *dev; 47 48 dev = container_of(inodep->i_cdev, struct led_device, cdev); 49 // 放入私有數據中 50 filep->private_data = dev; 51 52 // 映射LED 53 gpm4con = ioremap(0x110002E0, 8); 54 gpm4dat = gpm4con + 1; 55 // 設為輸出引腳,滅燈 56 *gpm4con = 0x1111; 57 *gpm4dat |= ((1 << 3) | (1 << 2) | 1); 58 59 return 0; 60 } 61 62 static int led_close(struct inode *inodep, struct file *filep) 63 { 64 iounmap(gpm4con); 65 66 return 0; 67 } 68 69 /* 把定義的函數接口集合起來,方便系統調用 */ 70 static const struct file_operations led_fops = { 71 .write = led_write, 72 .open = led_open, 73 .release = led_close, 74 }; 75 76 static int __init led_init(void) 77 { 78 int ret; 79 dev_t devt; 80 81 /* 1. 申請設備號 */ 82 if (g_major) { 83 devt = MKDEV(g_major, 0); 84 ret = register_chrdev_region(devt, 1, "led"); 85 } 86 else { 87 ret = alloc_chrdev_region(&devt, 0, 1, "led"); 88 g_major = MAJOR(devt); 89 } 90 if (ret) 91 return ret; 92 93 /* 2. 申請文件內私有結構體 */ 94 dev = kzalloc(sizeof(struct led_device), GFP_KERNEL); 95 if (dev == NULL) { 96 ret = -ENOMEM; 97 goto fail_malloc; 98 } 99 100 /* 3. 注冊字符設備驅動 */ 101 cdev_init(&dev->cdev, &led_fops); /* 初始化cdev並鏈接file_operations和cdev */ 102 ret = cdev_add(&dev->cdev, devt, 1); /* 注冊cdev */ 103 if (ret) 104 return ret; 105 106 /* 4. 創建類設備,insmod后會生成/dev/led設備文件 */ 107 scls = class_create(THIS_MODULE, "led"); 108 sdev = device_create(scls, NULL, devt, NULL, "led"); 109 110 return 0; 111 112 fail_malloc: 113 unregister_chrdev_region(devt, 1); 114 115 return ret; 116 } 117 118 static void __exit led_exit(void) 119 { 120 /* 鏡像注銷 */ 121 dev_t devt = MKDEV(g_major, 0); 122 123 device_destroy(scls, devt); 124 class_destroy(scls); 125 126 cdev_del(&(dev->cdev)); 127 kfree(dev); 128 129 unregister_chrdev_region(devt, 1); 130 } 131 132 /* 聲明段屬性 */ 133 module_init(led_init); 134 module_exit(led_exit); 135 136 MODULE_LICENSE("GPL");
Makefile:

1 KERN_DIR = /work/tiny4412/tools/linux-3.5 2 3 all: 4 make -C $(KERN_DIR) M=`pwd` modules 5 6 clean: 7 make -C $(KERN_DIR) M=`pwd` modules clean 8 rm -rf modules.order 9 10 obj-m += led.o
測試文件:

1 #include <stdio.h> 2 #include <unistd.h> 3 #include <sys/types.h> 4 #include <sys/stat.h> 5 #include <fcntl.h> 6 #include <string.h> 7 8 int main(int argc, char** argv) 9 { 10 if (argc != 2) { 11 printf("Usage: \n"); 12 printf("%s <on|off>\n", argv[0]); 13 return -1; 14 } 15 16 int fd; 17 fd = open("/dev/led", O_RDWR); 18 if (fd < 0) { 19 printf("can't open /dev/led\n"); 20 return -1; 21 } 22 23 char stat; 24 if (0 == strcmp(argv[1], "off")) { 25 stat = 0; 26 write(fd, &stat, 1); 27 } else { 28 stat = 1; 29 write(fd, &stat, 1); 30 } 31 close(fd); 32 33 return 0; 34 }
需要注意的是,Makefile中的KERN_DIR = /work/tiny4412/tools/linux-3.5需要改成自己的linux內核路徑。
執行make命令編譯.ko驅動程序
執行arm-linux-gcc test.c -o test_led
將驅動程序和測試程序復制到文件系統中,完成后如下圖:
啟動開發板,執行:
[root @ lioker / ] #cd /my_driver/dong/01.led/
掛載模塊insmod:
[root @ lioker 01.led ] #insmod led.ko
[root @ lioker 01.led ] #./test_led on
[root @ lioker 01.led ] #./test_led off
卸載模塊rmmod:
[root @ lioker 01.led ] #rmmod led.ko
可看到對應現象
其實源代碼中的讀寫寄存器方式並不是值得推薦的,內核給我們提供了封裝好的函數,如:
1 #define readb(c) ({ u8 __v = readb_relaxed(c); __iormb(); __v; }) 2 #define readw(c) ({ u16 __v = readw_relaxed(c); __iormb(); __v; }) 3 #define readl(c) ({ u32 __v = readl_relaxed(c); __iormb(); __v; }) 4 5 #define writeb(v,c) ({ __iowmb(); writeb_relaxed(v,c); }) 6 #define writew(v,c) ({ __iowmb(); writew_relaxed(v,c); }) 7 #define writel(v,c) ({ __iowmb(); writel_relaxed(v,c); })
函數使用可查看Linux驅動函數解讀第四節
下一章 3、中斷分析以及按鍵中斷