姓名:薛紹宏? ? ?學號:19020100016? ? 學院:電子工程學院
【嵌牛導讀】本文介紹了嵌入式Linux驅動程序開發的字符設備驅動的概念和方法
【嵌牛鼻子】嵌入式Linux驅動程序開發
【嵌牛提問】嵌入式Linux字符設備驅動如何開發?
【嵌牛正文】
Linux 中的三大類驅動:字符設備驅動、塊設備驅動和網絡設備驅動。
字符設備驅動:IO口的驅動,比如點燈、 I2C、 SPI、音頻等。
塊設備驅動:存儲器設備的驅動,比如 EMMC、 NAND、 SD 卡和 U 盤等存儲設備
網絡設備驅動:網絡驅動,比如 USB WIFI,其使用 USB 接口,所以屬于字符設備,但是其又能上網,所以也屬于網絡設備驅動。
Linux 下的應用程序是如何調用驅動程序的?
在 Linux 中一切皆為文件,驅動加載成功以后會在“/dev”目錄下生成一個相應的文件,應用程序通過對這個名為“/dev/xxx” (xxx 是具體的驅動文件名字)的文件進行相應的操作即可實現對硬件的操作。
比如, led 燈/dev/led 的驅動文件,應用程序使用open()、close()、write()、read()等函數對文件操作,進而控制硬件。
字符設備驅動簡介
應用程序運行在用戶空間,而 Linux 驅動屬于內核的一部分,因此驅動運行于內核空間。當我們在用戶空間想要實現對內核的操作,比如使用 open 函數打開/dev/led 這個驅動,因為用戶空間不能直接對內核進行操作,因此必須使用一個叫做“系統調用”的方法來實現從用戶空間“陷入” 到內核空間,這樣才能實現對底層驅動的操作。
如上圖,從應用程序到具體驅動程序都有相應的函數與之對應,比如調用open 這個函數。
在內核部分: Linux 內核文件 include/linux/fs.h 中有個叫做 file_operations 的結構體,此結構體就是 Linux 內核驅動操作函數集合。(聽名字就知道是管理驅動文件操作的)
1588 struct file_operations {
1589 struct module *owner;
1590 loff_t (*llseek) (struct file *, loff_t, int);
1591 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
1592 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
1593 ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
1594 ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
1595 int (*iterate) (struct file *, struct dir_context *);
1596 unsigned int (*poll) (struct file *, struct poll_table_struct *);
1597 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
1598 long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
1599 int (*mmap) (struct file *, struct vm_area_struct *);
1600 int (*mremap)(struct file *, struct vm_area_struct *);
1601 int (*open) (struct inode *, struct file *);
1602 int (*flush) (struct file *, fl_owner_t id);
1603 int (*release) (struct inode *, struct file *);
1604 int (*fsync) (struct file *, loff_t, loff_t, int datasync);
1605 int (*aio_fsync) (struct kiocb *, int datasync);
......
1618 #ifndef CONFIG_MMU
1619 unsigned (*mmap_capabilities)(struct file *);
1620 #endif
1621 };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
file_operation 結構體中比較重要的、常用的函數:
第 1589 行, owner 擁有該結構體的模塊的指針,一般設置為 THIS_MODULE。
第 1590 行, llseek 函數用于修改文件當前的讀寫位置。
第 1591 行, read 函數用于讀取設備文件。
第 1592 行, write 函數用于向設備文件寫入(發送)數據。
第 1596 行, poll 是個輪詢函數,用于查詢設備是否可以進行非阻塞的讀寫。
第 1597 行, unlocked_ioctl 函數提供對于設備的控制功能,與應用程序中的 ioctl 函數對應。
第 1598 行, compat_ioctl 函數與 unlocked_ioctl 函數功能一樣,區別在于在 64 位系統上,32 位的應用程序調用將會使用此函數。在 32 位的系統上運行 32 位的應用程序調用的是unlocked_ioctl。
第 1599 行, mmap 函數用于將設備的內存映射到進程空間中(也就是用戶空間),一般幀緩沖設備會使用此函數,比如 LCD 驅動的顯存,將幀緩沖(LCD 顯存)映射到用戶空間中以后應用程序就可以直接操作顯存了,這樣就不用在用戶空間和內核空間之間來回復制。
第 1601 行, open 函數用于打開設備文件。
第 1603 行, release 函數用于釋放(關閉)設備文件,與應用程序中的 close 函數對應。
第 1604 行, fasync 函數用于刷新待處理的數據,用于將緩沖區中的數據刷新到磁盤中。
第 1605 行, aio_fsync 函數與 fasync 函數的功能類似,只是 aio_fsync 是異步刷新待處理的數據。
字符設備驅動開發步驟
在 Linux 驅動開發中肯定也是要初始化相應的外設寄存器, Linux 驅動開發中我們需要按照其規定的框架來編寫驅動,所以說學 Linux 驅動開發重點是學習其驅動框架。
驅動模塊的加載和卸載
Linux 驅動有兩種運行方式:
第一種就是將驅動編譯進 Linux 內核中,這樣當 Linux 內核啟動的時候就會自動運行驅動程序。
第二種就是將驅動編譯成模塊(Linux 下模塊擴展名為.ko),在Linux 內核啟動以后使用“insmod”命令加載驅動模塊。
調試時推薦使用將驅動編譯成模塊,在調試驅動的時候一般都選擇將其編譯為模塊,這樣我們修改驅動以后只需要編譯一下驅動代碼即可,不需要編譯整個 Linux 代碼。而且在調試的時候只需要加載或者卸載驅動模塊即可,不需要重啟整個系統。
驅動開發完成可以將驅動編譯進Linux 內核中。
模塊有加載和卸載兩種操作,
module_init(xxx_init); //注冊模塊加載函數
module_exit(xxx_exit); //注冊模塊卸載函數
1
2
module_init 函數用來向 Linux 內核注冊一個模塊加載函數,參數 xxx_init 就是需要注冊的具體函數,當使用“insmod”命令加載驅動的時候, xxx_init 這個函數就會被調用。
module_exit()函數用來向 Linux 內核注冊一個模塊卸載函數,參數 xxx_exit 就是需要注冊的具體函數,當使用“rmmod”命令卸載具體驅動的時候 xxx_exit 函數就會被調用。
字符設備驅動模塊加載和卸載模板如下所示:
1 /* 驅動入口函數 */
2 static int __init xxx_init(void)
3 {
4 /* 入口函數具體內容 */
5 return 0;
6 }
7 8
/* 驅動出口函數 */
9 static void __exit xxx_exit(void)
10 {
11 /* 出口函數具體內容 */
12 }
13
14 /* 將上面兩個函數指定為驅動的入口和出口函數 */
15 module_init(xxx_init);
16 module_exit(xxx_exit);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
驅動編譯完成以后擴展名為.ko的模塊,終端命令常用insmod、rmmod,另外modprobe命令可以將具有依賴關系的模塊一鍋端的加載和卸載:
//加載驅動模塊
insmod drv.ko
或 modprobe drv.ko
//卸載驅動模塊
rmmod drv.ko
或 modprobe -r drv.ko
1
2
3
4
5
6
字符設備注冊與注銷
對于字符設備驅動而言,當驅動模塊加載成功以后需要注冊字符設備,同樣,卸載驅動模塊的時候也需要注銷掉字符設備。
(就是你不僅要加載自己的驅動模塊,還要向內核注冊申請,這樣才能是合法公民)
字符設備的注冊和注銷函數原型:
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)
major: 主設備號, Linux 下每個設備都有一個設備號,設備號分為主設備號和次設備號兩部分,關于設備號后面會詳細講解。
name:設備名字,指向一串字符串。
fops: 結構體 file_operations 類型指針,指向設備的操作函數集合變量。
1
2
3
4
5
6
注冊函數register_chrdev()位于加載函數調用的驅動入口函數中;
注銷函數unregister_chrdev()位于卸載函數調用的驅動出口函數中。
37 /* 驅動入口函數 */
38 static int __init xxx_init(void)
39 {
40 /* 入口函數具體內容 */
41 int retvalue = 0;
42
43 /* 注冊字符設備驅動 */
44 retvalue = register_chrdev(200, "chrtest", &test_fops);
45 if(retvalue < 0){
46 /* 字符設備注冊失敗,自行處理 */
47 }
48 return 0;
49 }
50
51 /* 驅動出口函數 */
52 static void __exit xxx_exit(void)
53 {
54 /* 注銷字符設備驅動 */
55 unregister_chrdev(200, "chrtest");
56 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
設備號不能和其他驅動文件沖突,可使用命令查看當前已經被使用掉的設備號:
cat /proc/devices
1
實現設備的具體操作函數
比如,對chrtest 設備進行驅動,對file_operations結構體類型的變量 test_fops進行初始化,內部存有驅動文件操作函數,用什么操作函數就添加什么操作函數。
打開和關閉操作:最基本的要求;
讀寫操作:擁有讀寫一段緩沖區(內存)。
1 /* 打開設備 */
2 static int chrtest_open(struct inode *inode, struct file *filp)
3 {
4 /* 用戶實現具體功能 */
5 return 0;
6 }
7
8 /* 從設備讀取 */
9 static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
10 {
11 /* 用戶實現具體功能 */
12 return 0;
13 }
14
15 /* 向設備寫數據 */
16 static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
17 {
18 /* 用戶實現具體功能 */
19 return 0;
20 }
21
22 /* 關閉/釋放設備 */
23 static int chrtest_release(struct inode *inode, struct file *filp)
24 {
25 /* 用戶實現具體功能 */
26 return 0;
27 }
28//初始化操作函數結構體變量
29 static struct file_operations test_fops = {
30 .owner = THIS_MODULE,
31 .open = chrtest_open,
32 .read = chrtest_read,
33 .write = chrtest_write,
34 .release = chrtest_release,
35 };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
添加 LICENSE 和作者信息
LICENSE 是必須添加的,否則的話編譯的時候會報錯,LICENSE 采用 GPL 協議。
作者信息可以添加也可以不添加。
MODULE_LICENSE("GPL") //添加模塊 LICENSE 信息
MODULE_AUTHOR("zxy") //添加模塊作者信息
1
2
總結
一個完整的字符設備驅動模板就上以上4條:
/*1、文件操作函數file_operations結構體 變量初始化*/
......
/*2、驅動入口函數注冊 驅動出口函數注銷*/
......
/*3、加載驅動模塊 卸載驅動模塊*/
......
/*4、license和作者信息*/
(執行流程:3調用2,2調用1)
1
2
3
4
5
6
7
8
Linux 設備號
設備號的組成
Linux 中每個設備都有一個設備號,設備號由主設備號和次設備號兩部分組成,
主設備號表示某一個具體的驅動,
次設備號表示使用這個驅動的各個設備。
設備號由dev_t數據類型表示:
typedef __kernel_dev_t dev_t;
......
typedef __u32 __kernel_dev_t;
......
typedef unsigned int __u32;
......
經過來回的定義,設備號就是c語言中unsigned int的32位數據類型;
其中高 12 位為主設備號, 低 20 位為次設備號。
1
2
3
4
5
6
7
8
文件 include/linux/kdev_t.h 中提供了幾個關于設備號的操作函數(本質是宏函數):
6 #define MINORBITS 20
7 #define MINORMASK ((1U << MINORBITS) - 1)
8
9 #define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
10 #define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
11 #define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
1
2
3
4
5
6
第 6 行,宏 MINORBITS 表示次設備號位數,一共是 20 位。
第 7 行,宏 MINORMASK 表示次設備號掩碼。
第 9 行,宏 MAJOR 用于從 dev_t 中獲取主設備號,將 dev_t 右移 20 位即可。
第 10 行,宏 MINOR 用于從 dev_t 中獲取次設備號,取 dev_t 的低 20 位的值即可。
第 11 行,宏 MKDEV 用于將給定的主設備號和次設備號的值組合成 dev_t 類型的設備號。
設備號的分配
1、靜態分配設備號
使用“cat /proc/devices”命令即可查看當前系統中所有已經使用了的設備號,人為選擇沒有使用的設備號。
2、動態分配設備號
Linux 社區推薦使用動態分配設備號,在注冊字符設備之前先申請一個設備號,系統自動給你一個沒有被使用的設備號,這樣就避免了沖突。
設備號的申請函數如下:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
dev: 保存申請到的設備號。
baseminor: 次設備號起始地址, alloc_chrdev_region 可以申請一段連續的多個設備號,這些設備號的主設備號一樣,但是次設備號不同,次設備號以 baseminor 為起始地址地址開始遞增。一般 baseminor 為 0,也就是說次設備號從 0 開始。
count: 要申請的設備號數量。
name: 設備名字。
1
2
3
4
5
6
設備號釋放函數如下:
void unregister_chrdev_region(dev_t from, unsigned count)
from:要釋放的設備號。
count: 表示從 from 開始,要釋放的設備號數量。
1
2
3
4
chrdevbase 字符設備驅動開發實驗
為了完整的編寫一個字符設備驅動模塊,假設一個虛擬設備: chrdevbase
設備描述:chrdevbase 設備有兩個緩沖區,一個為讀緩沖
區,一個為寫緩沖區,這兩個緩沖區的大小都為 100 字節。
應用程序描述:在應用程序中可以向 chrdevbase 設備的寫緩沖區中寫入數據,從讀緩沖區中讀取數據。
實驗程序編寫
實現功能描述:
應用程序調用 open 函數打開 chrdevbase 這個設備,
使用 write 函數向chrdevbase 的寫緩沖區 writebuf 中寫入數據(不超過 100 個字節),
使用 read 函數讀取讀緩沖區 readbuf 中的數據操作,
應用程序使用 close 函數關閉 chrdevbase 設備。
驅動程序chrdevbase.c
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
/***************************************************************
Copyright ? ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : chrdevbase.c
作者 ? : 左忠凱
版本 ? : V1.0
描述 ? : chrdevbase驅動文件。
其他 ? : 無
論壇 ? : www.openedv.com
日志 ? : 初版V1.0 2019/1/30 左忠凱創建
***************************************************************/
#define CHRDEVBASE_MAJOR 200 /* 主設備號 */
#define CHRDEVBASE_NAME "chrdevbase" /* 設備名? ? */
static char readbuf[100]; /* 讀緩沖區 */
static char writebuf[100]; /* 寫緩沖區 */
static char kerneldata[] = {"kernel data!"};
/*
* @description : 打開設備
* @param - inode : 傳遞給驅動的inode
* @param - filp : 設備文件,file結構體有個叫做private_data的成員變量
* ? 一般在open的時候將private_data指向設備結構體。
* @return : 0 成功;其他 失敗
*/
static int chrdevbase_open(struct inode *inode, struct file *filp)
{
//printk("chrdevbase open!\r\n");
return 0;
}
/*
* @description : 從設備讀取數據
* @param - filp : 要打開的設備文件(文件描述符)
* @param - buf : 返回給用戶空間的數據緩沖區
* @param - cnt : 要讀取的數據長度
* @param - offt : 相對于文件首地址的偏移
* @return : 讀取的字節數,如果為負值,表示讀取失敗
*/
static ssize_t chrdevbase_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue = 0;
/* 向用戶空間發送數據 */
memcpy(readbuf, kerneldata, sizeof(kerneldata));
retvalue = copy_to_user(buf, readbuf, cnt);
if(retvalue == 0){
printk("kernel senddata ok!\r\n");
}else{
printk("kernel senddata failed!\r\n");
}
//printk("chrdevbase read!\r\n");
return 0;
}
/*
* @description : 向設備寫數據
* @param - filp : 設備文件,表示打開的文件描述符
* @param - buf : 要寫給設備寫入的數據
* @param - cnt : 要寫入的數據長度
* @param - offt : 相對于文件首地址的偏移
* @return : 寫入的字節數,如果為負值,表示寫入失敗
*/
static ssize_t chrdevbase_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
int retvalue = 0;
/* 接收用戶空間傳遞給內核的數據并且打印出來 */
retvalue = copy_from_user(writebuf, buf, cnt);
if(retvalue == 0){
printk("kernel recevdata:%s\r\n", writebuf);
}else{
printk("kernel recevdata failed!\r\n");
}
//printk("chrdevbase write!\r\n");
return 0;
}
/*
* @description : 關閉/釋放設備
* @param - filp : 要關閉的設備文件(文件描述符)
* @return : 0 成功;其他 失敗
*/
static int chrdevbase_release(struct inode *inode, struct file *filp)
{
//printk("chrdevbase release!\r\n");
return 0;
}
/*
* 設備操作函數結構體
*/
static struct file_operations chrdevbase_fops = {
.owner = THIS_MODULE,
.open = chrdevbase_open,
.read = chrdevbase_read,
.write = chrdevbase_write,
.release = chrdevbase_release,
};
/*
* @description : 驅動入口函數
* @param : 無
* @return : 0 成功;其他 失敗
*/
static int __init chrdevbase_init(void)
{
int retvalue = 0;
/* 注冊字符設備驅動 */
retvalue = register_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME, &chrdevbase_fops);
if(retvalue < 0){
printk("chrdevbase driver register failed\r\n");
}
printk("chrdevbase init!\r\n");
return 0;
}
/*
* @description : 驅動出口函數
* @param : 無
* @return : 無
*/
static void __exit chrdevbase_exit(void)
{
/* 注銷字符設備驅動 */
unregister_chrdev(CHRDEVBASE_MAJOR, CHRDEVBASE_NAME);
printk("chrdevbase exit!\r\n");
}
/*
* 將上面兩個函數指定為驅動的入口和出口函數
*/
module_init(chrdevbase_init);
module_exit(chrdevbase_exit);
/*
* LICENSE和作者信息
*/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
分析:文件架構
頭文件調用的都是linux內核文件;
4個驅動文件操作函數;
驅動文件操作函數結構體初始化;
驅動入口函數,注冊設備;
驅動出口函數,注銷設備;
加載驅動模塊
卸載驅動模塊
許可證
人名
printk函數 相當于 printf 的孿生兄妹, printf運行在用戶態 printk 運行在內核態,printk 可以根據日志級別對消息進行分類,一共有 8 個消息級別,這 8 個消息級別定義在文件include/linux/kern_levels.h。
#define KERN_SOH "\001"
#define KERN_EMERG KERN_SOH "0" /* 緊急事件,一般是內核崩潰 */
#define KERN_ALERT KERN_SOH "1" /* 必須立即采取行動 */
#define KERN_CRIT KERN_SOH "2" /* 臨界條件,比如嚴重的軟件或硬件錯誤*/
#define KERN_ERR KERN_SOH "3" /* 錯誤狀態,一般設備驅動程序中使用KERN_ERR 報告硬件錯誤 */
#define KERN_WARNING KERN_SOH "4" /* 警告信息,不會對系統造成嚴重影響 */
#define KERN_NOTICE KERN_SOH "5" /* 有必要進行提示的一些信息 */
#define KERN_INFO KERN_SOH "6" /* 提示性的信息 */
#define KERN_DEBUG KERN_SOH "7" /* 調試信息*/
比如:
printk(KERN_EMERG "gsmi: Log Shutdown Reason\n"); //0級別
printk("gsmi: Log Shutdown Reason\n"); //MESSAGE_LOGLEVEL_DEFAULT 默認級別為4
1
2
3
4
5
6
7
8
9
10
11
12
13
參數 filp 有個叫做 private_data 的成員變量, private_data 是個 void 指針,一般在驅動中將private_data 指向設備結構體,設備結構體會存放設備的一些屬性。
用戶程序chrdevbaseApp.c
C 庫文件操作基本函數
編寫測試 APP 就是編寫 Linux 應用,需要用到 C 庫里面和文件操作有關的一些函數,比如open、 read、 write 和 close 這四個函數。
①、 open 函數
int open(const char *pathname, int flags)
pathname:要打開的設備或者文件名。
flags: 文件打開模式,以下三種模式必選其一:
O_RDONLY 只讀模式
O_WRONLY 只寫模式
O_RDWR 讀寫模式
另外,<邏輯或> 來選擇多種模式:
O_APPEND 每次寫操作都寫入文件的末尾
O_CREAT 如果指定文件不存在,則創建這個文件
O_EXCL 如果要創建的文件已存在,則返回 -1,并且修改 errno 的值
O_TRUNC 如果文件存在,并且以只寫/讀寫方式打開,則清空文件全部內容
O_NOCTTY 如果路徑名指向終端設備,不要把這個設備用作控制終端。
O_NONBLOCK 如果路徑名指向 FIFO/塊文件/字符文件,則把文件的打開和后繼I/O 設置為非阻塞
DSYNC 等待物理 I/O 結束后再 write。在不影響讀取新寫入的數據的前提下,不等待文件屬性更新。
O_RSYNC read 等待所有寫入同一區域的寫操作完成后再進行。
O_SYNC 等待物理 I/O 結束后再 write,包括更新文件屬性的 I/O。
返回值:如果文件打開成功的話返回文件的文件描述符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在 Ubuntu 中輸入“man 2 open” 即可查看 open 函數的詳細內容
②、close 函數
int close(int fd);
fd:要關閉的文件描述符。
返回值: 0 表示關閉成功,負值表示關閉失敗。
1
2
3
4
③、read 函數
當然是從文件里讀內容啦~需要open函數返回的文件描述符。
ssize_t read(int fd, void *buf, size_t count)
fd:要讀取的文件描述符,讀取文件之前要先用 open 函數打開文件, open 函數打開文件成功以后會得到文件描述符。
buf: 數據讀取到此 buf 中。
count: 要讀取的數據長度,也就是字節數。
返回值: 讀取成功的話返回讀取到的字節數;如果返回 0 表示讀取到了文件末尾;如果返回負值,表示讀取失敗。
1
2
3
4
5
6
在 Ubuntu 中輸入“man 2 read”命令即可查看 read 函數的詳細內容。
④、write 函數
ssize_t write(int fd, const void *buf, size_t count);
fd:要進行寫操作的文件描述符,寫文件之前要先用 open 函數打開文件, open 函數打開文件成功以后會得到文件描述符。
buf: 要寫入的數據。
count: 要寫入的數據長度,也就是字節數。
返回值: 寫入成功的話返回寫入的字節數;如果返回 0 表示沒有寫入任何數據;如果返回負值,表示寫入失敗。
1
2
3
4
5
6
編寫一個簡單的測試 APP,控制驅動文件,測試 APP 運行在用戶空間,測試 APP 很簡單通過輸入相應的指令來對 chrdevbase 設備執行讀或者寫操作。
#include "stdio.h"
#include "unistd.h"
#include "sys/types.h"
#include "sys/stat.h"
#include "fcntl.h"
#include "stdlib.h"
#include "string.h"
/***************************************************************
Copyright ? ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名 : chrdevbaseApp.c
作者 ? : 左忠凱
版本 ? : V1.0
描述 ? : chrdevbase驅測試APP。
其他 ? : 使用方法:./chrdevbase /dev/chrdevbase <1>|<2>
? argv[2] 1:讀文件
? argv[2] 2:寫文件
論壇 ? : www.openedv.com
日志 ? : 初版V1.0 2019/1/30 左忠凱創建
***************************************************************/
static char usrdata[] = {"usr data!"};? //寫入的用戶數據
/*
* @description : main主程序
* @param - argc : argv數組元素個數
* @param - argv : 具體參數
* @return : 0 成功;其他 失敗
*/
int main(int argc, char *argv[])
{
int fd, retvalue;
char *filename;
char readbuf[100], writebuf[100];
//要求只能傳入3個參數
if(argc != 3){
printf("Error Usage!\r\n");
return -1;
}
filename = argv[1];
/* 打開驅動文件 獲取文件描述符*/
fd? = open(filename, O_RDWR);
if(fd < 0){
printf("Can't open file %s\r\n", filename);
return -1;
}
if(atoi(argv[2]) == 1){ /* 從驅動文件讀取數據 */
retvalue = read(fd, readbuf, 50);
if(retvalue < 0){
printf("read file %s failed!\r\n", filename);
}else{
/*? 讀取成功,打印出讀取成功的數據 */
printf("read data:%s\r\n",readbuf);
}
}
if(atoi(argv[2]) == 2){
/* 向設備驅動寫數據 */
memcpy(writebuf, usrdata, sizeof(usrdata)); //內存復制函數,從一個緩沖區到另一個緩沖區
retvalue = write(fd, writebuf, 50);
if(retvalue < 0){
printf("write file %s failed!\r\n", filename);
}
}
/* 關閉設備 */
retvalue = close(fd);
if(retvalue < 0){
printf("Can't close file %s\r\n", filename);
return -1;
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
分析:程序架構
調用C庫函數;
打開文件;
讀寫文件;
關閉文件。
判斷 argv[2]參數的值是 1 或 2 ,因為輸入命令的時候其參數都是字符串格式的,因此需要借助 atoi 函數將字符串格式的數字轉換為真實的數字。
編譯驅動程序和測試 APP
1、編譯驅動程序chrdevbase.c 這個文件,將其編譯為.ko 模塊,創建Makefile 文件,路徑自己改改:
KERNELDIR := /home/zxy/linux/kernel_lib/linux-imx-rel_imx_4.1.15_2.1.0_ga_alientek
CURRENT_PATH := $(shell pwd)
obj-m := chrdevbase.o
build: kernel_modules
kernel_modules:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean
1
2
3
4
5
6
7
8
9
10
KERNELDIR 表示開發板所使用的 Linux 內核源碼目錄(原子官方linux內核源碼,需要編譯了好久),使用絕對路徑,大家根據自己的實際情況填寫即可。
CURRENT_PATH 表示當前路徑,直接通過運行“pwd”命令來獲取當前所處路徑。
obj-m 表示將 chrdevbase.c 這個文件編譯為 chrdevbase.ko 模塊。
后面的modules 表示編譯模塊,
-C 表示將當前的工作目錄切換到指定目錄中,也就是 KERNERLDIR 目錄。
M 表示模塊源碼目錄,“make modules”命令中加入 M=dir 以后程序會自動到指定的 dir 目錄中讀取模塊的源碼并將其編譯為.ko 文件。
2、編譯 chrdevbaseApp.c生成可執行程序,這里差不多就是單純的C語言程序,用交叉編譯器arm-linux-gnueabihf-gcc編譯即可:
//編譯
arm-linux-gnueabihf-gcc chrdevbaseApp.c -o chrdevbaseApp
//查看文件信息,chrdevbaseAPP 這個可執行文件是 32 位 LSB 格式, ARM 版本的,因此 chrdevbaseAPP 只能在 ARM 芯片下運行。
file chrdevbaseApp
1
2
3
4
3、傳文件到開發板驗證
左老師使用的 TFTP 從網絡啟動,并且使用 NFS 掛載網絡根文件系統,說白了就是從uboot層面網絡啟動linux內核和掛在根文件系統,但是這能保存到DRAM中。
本人這里研究了一下ssh傳輸協議,實現linux系統client和server之間的文件互傳:
1、兩者連接在同一網絡地址;
2、服務器安裝并配置ssh server協議;
3、板子可以直接scp獲取文件啦。
就是這么簡單,有手就行。
具體把驅動文件chrdevbase.ko放到/lib/modules/4.1.15-gb8ddbbc目錄中,就是放到這個一種庫里;
應用程序chrdevApp放到home目錄下或者其他正常的位置都行。
4、驗證操作
insmod chrdevbase.ko //加載驅動模塊
cat /proc/devices //查找能用的設備號
mknod /dev/chrdevbase c 200 0 //創建節點 “ 200”是設備的主設備號,“ 0”是設備的次設備號。
//操作這個設備節點
./chrdevbaseApp /dev/chrdevbase 1
./chrdevbaseApp /dev/chrdevbase 2
rmmod chrdevbase.ko //卸載驅動模塊
lsmod //查看某些模塊還存不存在,
1
2
3
4
5
6
7
8
9
10
11
總結
這種簡單的驅動模塊,整體就傳到板子上驅動文件.ko和應用程序文件。
驅動文件放到/lib下的庫文件中,一直放著就行;
使用insmod命令加載,就是跟內核申請了一塊兒空間;
使用mknod命令創建節點再/dev下,就是跟內核申請營業許可證;
APP可以放到任何正常的目錄下,使用APP應用程序控制/dev下這個節點,就能實現驅動的控制了;
使用rmmod命令卸載,就是跟內核說一聲退還這一塊兒空間,APP就控制不了了。