2、點亮LED


 

接着上一章,本章來實現控制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");
View Code

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
View Code

測試文件:

 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 }
View Code

 

需要注意的是,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、中斷分析以及按鍵中斷

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM