字符设备驱动程序——点亮、熄灭LED操作
2019獨角獸企業重金招聘Python工程師標準>>>
字符設備:是指只能一個字節一個字節讀寫的設備,不能隨機讀取設備內存中的某一數據,讀取數據需要按照先后數據。字符設備是面向流的設備,常見的字符設備有鼠標、鍵盤、串口、控制臺和LED設備等。
每一個字符設備都在/dev目錄下對應一個設備文件。linux用戶程序通過設備文件(或稱設備節點)來使用驅動程序操作字符設備。
Linux操作系統將所有的設備看成文件,以操作文件的方式訪問設備。應用程序不能直接操作硬件,而是使用統一的接口函數調用硬件的驅動程序。這組接口被稱為系統調用,在庫函數中定義。可以在glibc的fcntl.h、unistd.h、sys/ioctl.h等文件中看到如下的定義,這個些文件也可以在交叉編譯工具鏈的/usr/local/arm/3.4.1/include目錄下找到。
extern int open (__const char *__file, int __oflag, ...) __nonnull ((1));
extern ssize_t read (int __fd, void *__buf, size_t __nbytes);
extern ssize_t write (int __fd, __const void *__buf, size_t __n);
extern int ioctl (int __fd, unsigned long int __request, ...) __THROW;
.......
對于上述每個系統調用,驅動程序中都有一個與之對應的函數。對于字符設備驅動程序,這些函數集合在一個file_operations類型的數據結構中。file_operations結構在Linux內核的include/linux/fs.h文件中定義。
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 *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
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 *);
int (*fsync) (struct file *, struct dentry *, int datasync);
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*dir_notify)(struct file *filp, unsigned long arg);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
};
當應用程序使用open函數打開某個設備時,設備驅動程序的file_operations結構中的open成員就會被調用;當應用程序使用read、write、ioctl等函數讀寫、控制設備時,驅動程序的file_operations結構中的相應成員(read、write、ioctl等)就會被調用。從這個角度來說,編寫字符設備驅動程序就是為具體硬件的file_operations結構編寫各個函數(并不需要全部實現file_operations結構中的成員)。
那么,當應用程序通過open、read、write等系統調用訪問某個設備文件時,Linux系統怎么知道去調用那個驅動程序的file_operations結構中的open、read、write等成員呢?
(1)、設備文件主/次設備號
設備文件分為字符設備、塊設備,比如PC機上的串口屬于字符設備,硬盤屬于塊設備。在PC機上運行命令“ls /dev/ttyS0 /dev/hdal -l”可以看到:
brw-rw---- 1 root disk ??3, 1 ?5月 21 08:29 /dev/hdal
crw-rw---- 1 root uucp ?4, 64 ?5月 21 08:29 /dev/ttyS0
“brw-rw----”中的“b”表示/dev/hdal是個塊設備,他的主設備號是3,次設備號是1;“crw-rw----”中的“c”表示/dev/ttyS0是個塊設備,它的主設備號位4,次設備號為64。
(2)、模塊初始化時,將主設備號與file_operations結構一起向內核注冊
驅動程序有一個初始化函數,在安裝驅動程序時會調用它。在初始化函數中,會將驅動程序的file_operations結構連同主設備號一起向內核進行注冊。對于字符設備使用如下以下函數進行注冊:
extern int register_chrdev(unsigned int, const char *,const struct file_operations *);
這樣,應用程序操作設備文件時,Linux系統就將會根據設備文件的類型(是字符設備還是塊設備)、主設備號找到在內核中注冊的file_operations結構(對于塊設備位block_device_operations結構),次設備號供驅動程序自身用來分辨它是同類設備中的第幾個。
編寫字符驅動程序的過程大概如下。
(1)、編寫驅動程序初始化函數
進行必要的初始化,包括硬件初始化、向內核注冊驅動程序等。
(2)、構造file_operations結構中要用到的各個成員函數
實際的驅動程序當然比上述兩個步驟復雜,但這兩個步驟已經可以讓我們編寫比較簡單的驅動程序,比如LED控制。其他比較高級的技術,比如中斷、select機制、fasync異步通知機制,將在其他章節的例子中介紹。
LED驅動程序源碼分析:
本節以一個簡單的LED驅動程序作為例子,讓讀者初步了解驅動程序的開發。
本例子的開發板使用引腳GPF4~6外接3個LED,它們的操作方法以前已經做了詳細的說明。
(1)、引腳功能為輸出;
(2)、要點亮LED,令引腳輸出0;
(3)、要熄滅LED,令引腳輸出1。
硬件連接方式如下圖:
1、LED驅動程序代碼分析
下面按照函數調用的順序進行講解。
模塊的初始化函數和卸載函數如下:
/*
* 執行“insmod first_drv.ko”命令時就會調用這個函數
*/
static int __init first_drv_init(void)
{
/* 注冊字符設備驅動程序
?* 參數為主設備號、設備名字、file_operations結構;本例子中寫0,
?* 內核就會自動分配一個主設備號,使用major變量存儲。
?* 這樣,主設備號和具體的file_operations結構就聯系起來了
?* 操作主設備號為major的設備文件時,就會調用first_drv_fops中的相關成員函數
*/
99 : major = register_chrdev( 0, "first_drv", &first_drv_fops);
?
/* 創建一個struct class的指針
?* 第一個參數默認“THIS_MODULE”
?* 第二個參數和register_chrdev()函數中的驅動設備名字相同
*/
firstdrv_class = class_create(THIS_MODULE, "first_drv");
/* 創建/dev/xxx設備
?* 第一個參數為上面創建的struct class類型的變量
?* 第二個參數為NULL,第三個參數為MKDEV(major, 0)
?* 第四個參數為NULL,第五個參數為設備名字
*/
firstdrv_class_dev = class_device_create(firstdrv_class, NULL, MKDEV(major, 0), NULL, "xxx");
?
/* 將物理端口進行映射
*/
GPFCON = (volatile unsigned long *)ioremap(0x56000050, 16);
GPFDATA = GPFCON+1;
return 0;
}
?
/* 執行rmmod first_drv命令時就會調用這個函數
?*
*/
static void __exit first_drv_exit(void)
{
/* 卸載驅動程序 */
unregister_chrdev(major, "first_drv");
/* 卸載/dev/目錄下的設備 */
class_device_unregister(firstdrv_class_dev);
/* 銷毀class_create()創建的類 */
class_destroy(firstdrv_class);
/* 取消端口映射 */
iounmap(GPFCON);
}
/* 這兩行指定驅動程序的初始化和卸載函數 */
119 : module_init(first_drv_init);
120 : module_exit(first_drv_exit);
/* 驅動模塊的許可證聲明 */
MODULE_LICENSE("GPL");
第119、120兩行用來指明裝載、卸載模塊時所調用的函數。也可以不使用這兩行,但是需要將這兩個函數的名字改為init_module、cleanup_module。
執行“insmod first_drv.ko”命令時就會調用first_drv_init函數,這個函數核心的代碼只有第99行。它調用register_chrdev函數向內核注冊驅動程序;內核會自動分配主設備號,并保存在major變量中,將主設備號位major與file_operations結構first_drv_fops聯系起來。以后應用程序操作主設備號位major的設備文件時,比如open、read、write、ioctl,first_drv_fops中的相應成員函數就會被調用。但是,first_drv_fops中并不需要全部實現這些函數,用到哪個就實現哪個。
執行“rmmod first_drv”命令時就會調用first_drv_exit函數,它進而調用unregister_chrdev函數卸載驅動程序,它的功能與register_chrdev函數相反。
first_drv_init、first_drv_exit函數前的“__init”、“__exit”只有在將驅動程序靜態鏈接進內核時才有意義。前者表示first_drv_init函數的代碼被放在“.init.text”段中,這個段在使用一次后被釋放(這可以節省內存);后者表示first_drv_exit函數的代碼段被放在“.exit.data”段中,在連接內核時這個段沒有使用,因為不能卸載靜態鏈接的驅動程序。
下面看看first_drv_fops結構的組成。
/* 這個結構是字符設備驅動程序的核心
?* 當應用程序操作設備文件時所調用的open、read、write等函數
?* 最終會調用這個結構中的對應函數
*/
static struct file_operations first_drv_fops = {
???81 .owner = THIS_MODULE,/* 這是一個宏,指向編譯模塊時自動創建的__this_module變量 */
????.open = ?first_drv_open, ????
????.write = first_drv_write, ???
};
第81行的宏THIS_MODULE在include/linux/module.h中定義如下,__this_module變量在編譯模塊時自動創建,無需關注這點。
# define THIS_MODULE (&__this_module)
file_operations類型的first_drv_fops結構是驅動中最重要的數據結構,編寫字符設備驅動程序的主要工作也是填充其中的各個成員。比如本驅動程序中用到open、write成員被設為first_drv_open、first_drv_write函數,前者用來初始化LED所用的GPIO引腳,后者用來根據用戶傳入的參數設置GPIO的輸出電平。
first_drv_open函數的代碼如下:
/* 應用程序對設備文件/dev/xxx執行open()時,
* 就會調用first_drv_open 函數
*/
static int first_drv_open(struct inode *inode, struct file *file)
{
/* 設置GPIO引腳的功能,本驅動中LED所涉及的GPIO引腳設為輸出功能 */
/*配置GPF4、5、6為輸出*/
*GPFCON ?&= ?~( (0X3<<(4*2)) | (0X3<<(5*2)) | (0X3<<(6*2))); //首先清零
*GPFCON ?|= ?( (0X1<<(4*2)) | (0X1<<(5*2)) | (0X1<<(6*2))); ?//后置1
return 0;
}
在應用程序執行“open(“/dev/xxx”)”系統調用時,first_drv_open函數將被調用。它用來將LED所涉及的GPIO引腳設為輸出功能。不在模塊的初始化函數中進行這些設置的原因是:雖然加載了模塊,但是這個模塊卻不一定會被用到,就是說這些引腳不一定用于這些用途,它們可能在其他模塊中另作他用。所以,在使用時才去設置它,我們把對引腳的初始化放在open操作中。
first_drv_write函數的代碼如下:
/* 應用程序對設備文件/dev/xxx執行write()函數時
* 就會調用first_drv_write函數
*/
static ssize_t first_drv_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
int val;
/* 拷貝從應用程序傳遞過來的值到val變量中 */
copy_from_user(&val, buf, count);
if(val == 1){
//點燈
*GPFDATA &= ~((0X1<<(4)) | (0X1<<(5)) | (0X1<<(6)));
}else{
//滅燈
*GPFDATA |= ((0X1<<(4)) | (0X1<<(5)) | (0X1<<(6)));
}
return 0;
}
應用程序執行系統調用write()時,first_drv_write函數將被調用,copy_from_user(&val, buf, count);作用是將用戶傳遞的輸出取出,根據用戶傳遞的值來設置LED燈的亮滅。
注意:應用程序執行的open、write等系統調用,它們的參數和驅動程序中相應函數的參數不是一一對應的,其中經過了內核文件系統層的轉換。
系統調用的原型如下:
int open(const char *pathname, int flags);
ssize_t write(int fd, const void *buf, size_t count);
int ioctl(int fd, int request, ...);
ssize_t read(int fd, void *buf, size_t count);
file_operations結構中的成員如下:
int (*open) (struct inode *, struct file *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
可以看到,這些參數有很大一部分非常相似。
(1)、系統調用open傳入的參數已經被內核文件系統層處理了,在驅動程序中看不出原來的參數了。
(2)、系統調用ioctl的參數個數可變,一般最多傳入3個:后面兩個參數與file_operations結構中ioctl成員的后兩個參數對應。
(3)、系統調用read傳入的buf、count參數,對應file_operations結構中read成員的buf、count參數。而參數offp表示用戶在文件中進行存取操作的位置,當執行完讀寫操作后由驅動程序調用。
(4)、系統調用write與file_operations結構中write成員的參數關系,與第(3)點相似。
驅動程序的最后,有如下描述信息,它們不是必須的。
/* 描述驅動程序的一些信息,不是必須的 */
MODULE_AUTHOR(“http:\\www.100ask.net”); //驅動程序作者
MODULE_DESCRIPTION(“S3C2410 LED Driver”); //一些描述信息
MODULE_LICENSE(“GPL”); //遵循的協議
驅動程序的編譯:
在次文件的同一目錄下,制作Makefile文件,文件內容如下:
KERN_DIR = /work/linux/linux-2.6.22.6 ?#對應內核的所在目錄
all:
make -C $(KERN_DIR) M=`pwd` modules
clean: ???#執行make clean時刪除生成的文件
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += first_drv.o ?#執行make命令生成的模塊名字為“first_drv.o”
驅動程序測試:
首先要編譯測試驅動程序firstdrv_test.c,它的代碼很簡單,關鍵部分如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
/*
* firstdrvtest
*/
int main(int argc, char **argv)
{
int fd;
int val = 1;
fd = open("/dev/xxx", O_RDWR); //打開設備
if (fd < 0)
{
printf("can't open!\n");
}
if(argc != 2){
printf("Usage:\n");
printf("%s <on | off>\n",argv[0]);
return 0;
}
if(strcmp(argv[1], "on") == 0){ ?//判斷用戶運行程序時,
????????????????//添加的參數時on還是off
val = 1; ?//寫入1,既點亮LED
}else{
val = 0; ?//寫入0,既熄滅LED
}
write(fd, &val, 4); //寫入數據
return 0;
}
其中的open、write函數最終會調用驅動程序中的first_drv_open、first_drv_write函數。
現在就可以參照firstdrv_test.c的使用說明,(直接運行firstdrv_test命令即可看到)操作LED了,以下兩條命令點亮、熄滅LED。
./firstdrv_test on
./firstdrv_test off
?
轉載于:https://my.oschina.net/cht2000/blog/906223
新人創作打卡挑戰賽發博客就能抽獎!定制產品紅包拿不停!總結
以上是生活随笔為你收集整理的字符设备驱动程序——点亮、熄灭LED操作的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用正则表达式小心换行和回车
- 下一篇: VINS(五)非线性优化与在线标定调整