linux设备驱动的实现与理解
linux設備驅動的實現與理解
????在linux中對字符設備的驅動編寫,驅動插入以及使用驅動文件進行邏輯控制,其中這份代碼寫在嵌入式板中,通過控制io來實現燈的亮滅,但是設備驅動的實現流程與燈無關,大致的流程都體現在代碼中。我感覺這份博客我自己不會看,太難看了,算是自己對設備驅動理解的記錄吧。
一、???程序解讀
1、????? 系統調用
open() 打開文件
ioctl()設備驅動程序中對設備的I/O通道進行管理的函數
exit() 使進程停止運行,exit()函數在調用exit系統調用之前要檢查文件的打開情況,把文件緩沖區中的內容寫回文件,就是”清理I/O緩沖”。
2、????? 寄存器地址
#define FS4412_GPF3CON? 0x114001E0
通過數據手冊可以找到每個端口的物理地址。
gpf3con = ioremap(FS4412_GPF3CON, 4);
在系統運行時,外設的I/O內存資源的物理地址是已知的,由硬件的設計決定。但是CPU通常并沒有為這些已知的外設I/O內存資源的物理地址預定義虛擬地址范圍,驅動程序并不能直接通過物理地址訪問I/O內存資源,而必須將它們映射到虛地址空間內,然后才能根據映射所得到的虛地址范圍,通過訪問內存指令訪問這些I/O內存資源。通過ioremap來進行靜態映射,4表示映射一個寄存器的地址,也就是4個字節,返回虛地址保存在本地全局的指針變量中。
3、????? 寄存器讀寫代碼
writel((readl(gpf3con) & ~(0xff << 16)) | (0x11 << 16),gpf3con);
?? writel(readl(gpx2dat) &~(0x3<<4), gpf3dat);
這是初始化端口時的f端口控制寄存器的初始化以及數據寄存器的初試化,其他的端口初始化同樣的方法:
(1)首先根據數據手冊初始化控制寄存器,配置固定的位,將需要的引腳的狀態改為輸出狀態。
(2)通過寫數據寄存器初始化引腳為低電平,即使led燈初始狀態為熄滅。
(3)在收到測試文件的命令后,根據命令相應輸出引腳為低電平或高電平
(4)先將端口數據寄存器32個引腳數據全部讀出,然后只將需要的位置1或清零,然后再寫回端口數據寄存器,可以防止修改沒有用到的引腳。
二、? ?原理
在這里將實驗的原理描述為兩個部分,第一部分為驅動的加載部分,第二部分為驅動加載完成后,驅動的使用時的調用過程。
驅動加載:
1、將驅動源代碼編譯后,生成ko文件,這是將要加載的驅動模塊。2、調用命令insmod加載模塊,首先會找代碼里邊固定的宏moudle_init()來找到驅動中的初始化函數這里是s5pv210_led_init()和退出函數s5pv210_led_exit()。
3、調用s5pv210_led_init()來進行設備號注冊,和設備添加。MKDEV是一個宏,可以通過移位把主設備號和次設備號進行處理,生成一個32位的數據。調用register_chrdev_region()注冊設備號,linux驅動根據散列hash表來建立設備描述cdev結構體的索引,當hash表的index沖突時,采用鏈表的方式避免沖突,這樣可以通過設備號快速找到cdev結構體的地址。
4、調用cdev_init()來初始化結構體cdev,最重要的是將file_operations保存在cdev中,file_oerations里邊有本地實現的open release ioctl等具體功能的函數指針,這樣可以在使用驅動的時候找到相應的實現函數。
5、調用cdev_add()來添加設備結構體cdev到hash表中,根據參數設備號可以找到注冊設備號時在hash表中的位置,然后將cdev結構體地址添加進去
6、映射io端口,即映射io端口的物理地址為虛擬地址,因為在系統運行時,外設的I/O內存資源的物理地址是已知的,由硬件的設計決定,可以在數據手冊中找到。但是CPU通常并沒有為這些已知的外設I/O內存資源的物理地址預定義虛擬地址范圍,驅動程序并不能直接通過物理地址訪問I/O內存資源,而必須將它們映射到核心虛地址空間內,然后才能根據映射所得到的核心虛地址范圍,通過訪問內存指令訪問這些I/O內存資源。
7、使用命令mknod 添加/dev目錄下設備描述文件,其實主要就是描述了我們輸入的三個參數,首先c代表字符型設備,500代表主設備號,0代表次設備號。三個參數的用法在下邊的流程描述。
8、調用rmmod命令后,卸載驅動,找到驅動中moudle_exit()宏來找到卸載驅動的退出函數,在這個里邊調用cdev_del()和unregister_chrdev_region()刪除設備并且去掉設備號的注冊,相反的過程,不用贅言。
驅動使用:
1、在驅動測試文件中,首先打開了/dev目錄下的設備文件,但是這個文件只是設備基本信息的描述,沒有實質的動作,具體的作用可以看作為設備的索引,通過打開文件可以找到設備驅動的位置。這里分析我們輸入的三個參數,c表示字符型設備open系統調用中拿到c就知道要去找字符型設備的結構體。主設備號和次設備號用來索引hash散列表,找到cdev結構體的地址,而cdev中保存有file_operations的地址,就可以找到驅動的具體實現函數,就是驅動加載的逆過程。而open執行完之后,返回一個文件描述符,這個描述符中就帶有找到的cdev地址。
2、Open函數根據得到的cdev找到file_operations 中的.open對應的的函數指針,調用這個函數來初始化驅動,這里可以做io端口控制寄存器的設置,將相關的端口設置為輸出。
3、調用ioctl通信,ioctl是io管道管理函數,是linux系統封裝用來給驅動用的通信函數,方便用戶使用,不用關心通信的實現方式,也不用考慮通信是否跨線程或者跨進程,可以看作是一個通道,在使用時塞入數據,在驅動里邊寫好拿出數據并做相應的處理及可以,感覺非常像socket套接字。
4、測試文件中調用ioctl傳入數據,ioctl根據傳入的文件描述符參數中的cdev找到file_operations 中的. unlocked_ioctl對應的的函數指針,在這個函數里邊調用copy_from_user(),可以取出傳入的參數,根據參數做驅動對應的動作。
5、退出驅動后,跟2-4同樣的道理找到.release執行,釋放驅動。
三、??? 代碼的理解
實驗過程中仔細研究了代碼的流程,感覺有幾點不足:
1、端口的控制寄存器和數據寄存器初始化放在驅動模塊加載的函數中,這從實際的情況來講并不合適。
2、io端口映射也放在驅動模塊加載的過程中,也不太合適,端口映射是在消耗系統資源,而加載之后一直不使用驅動時,端口映射一直存在系統中,只有在驅動卸載之后才釋放,從實際情況來講也不合適。
3、對于cdev結構體的聲明和定義放在驅動文件中有過疑惑,因為之前我以為cdev是一個鏈表,通過系統中的全局變量保存鏈表頭,然后根據設備號去索引鏈表,如果放在本地,發生意外修改了結構體中指向下一個結構體的指針,會導致鏈表斷裂,這是個很嚴重的問題。后來研究發現是用hash散列表實現的,故不存在這樣的問題。
4、測試文件中只有open函數,沒有調用close函數,當然這可以理解為需要在實驗過程中自己實現,比如不用ctl c退出而是用讀取輸入字符判斷退出函數,然后退出之前調用close。
四、和裸機平臺設備驅動的不同:
第一次接觸帶有操作系統的驅動編程,之前感覺系統驅動比較神秘的面紗也被揭開了,跟裸機平臺的設備驅動相比,區別就是
1、系統給驅動的編程增加了一個框架,需要依照系統對于驅動的統一管理來實現框架的內容,比如增加moudle_init,在init中注冊設備號 添加設備等, 這都是在告訴系統,我們寫的驅動具體實現的東西在哪里。
2、具體實際的動作,跟裸機驅動是一致的,因為驅動的本質就是直接操作硬件,而對于硬件的操作方法是硬件的數據手冊里邊統一定義的,手冊沒有改,對硬件的操作同樣不會變化,
3、系統實現控制驅動的通信方法,裸機驅動調用控制驅動的方法直接調用就可以,而系統中需要通過ioctl來實現應用驅動的程序跟驅動程序的通信控制。
源碼:
1、Makefile
ifeq ($(KERNELRELEASE),)
KERNELDIR ?= /home/linux/linux-3.14-fs4412/
#KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD)?
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions Module* modules*
.PHONY: modules modules_install clean
else
? ? obj-m := fs4412_led.o
endif
2、頭文件
#ifndef S5pV210_LED_HH
#define S5pV210_LED_HH
#define LED_MAGIC 'L'
/*
?* need arg = 1/2?
?*/
#define LED_ON _IOW(LED_MAGIC, 0, int)
#define LED_OFF _IOW(LED_MAGIC, 1, int)
?
#endif
3、實現文件
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/uaccess.h>
#include "fs4412_led.h"
MODULE_LICENSE("Dual BSD/GPL");
#define LED_MA 500
#define LED_MI 0
#define LED_NUM 1
#define FS4412_GPF3CON 0x114001E0
#define FS4412_GPF3DAT 0x114001E4
#define FS4412_GPX1CON 0x11000C20
#define FS4412_GPX1DAT 0x11000C24
#define FS4412_GPX2CON 0x11000C40
#define FS4412_GPX2DAT 0x11000C44
static unsigned int *gpf3con;
static unsigned int *gpf3dat;
static unsigned int *gpx1con;
static unsigned int *gpx1dat;
static unsigned int *gpx2con;
static unsigned int *gpx2dat;
struct cdev cdev;
void fs4412_led_on(int nr)
{
switch(nr) {
case 1:?
writel(readl(gpx2dat) | 1 << 7, gpx2dat);
break;
case 2:?
writel(readl(gpx1dat) | 1 << 0, gpx1dat);
break;
case 3:?
writel(readl(gpf3dat) | 1 << 4, gpf3dat);
break;
case 4:?
writel(readl(gpf3dat) | 1 << 5, gpf3dat);
break;
}
}
void fs4412_led_off(int nr)
{
switch(nr) {
case 1:?
writel(readl(gpx2dat) & ~(1 << 7), gpx2dat);
break;
case 2:?
writel(readl(gpx1dat) & ~(1 << 0), gpx1dat);
break;
case 3:?
writel(readl(gpf3dat) & ~(1 << 4), gpf3dat);
break;
case 4:?
writel(readl(gpf3dat) & ~(1 << 5), gpf3dat);
break;
}
}
static int s5pv210_led_open(struct inode *inode, struct file *file)
{
return 0;
}
static int s5pv210_led_release(struct inode *inode, struct file *file)
{
return 0;
}
static long s5pv210_led_unlocked_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
int nr;
if(copy_from_user((void *)&nr, (void *)arg, sizeof(nr)))
return -EFAULT;
if (nr < 1 || nr > 4)
return -EINVAL;
switch (cmd) {
case LED_ON:
fs4412_led_on(nr);
break;
case LED_OFF:
fs4412_led_off(nr);
break;
default:
printk("Invalid argument");
return -EINVAL;
}
return 0;
}
int fs4412_led_ioremap(void)
{
int ret;
gpf3con = ioremap(FS4412_GPF3CON, 4);
if (gpf3con == NULL) {
printk("ioremap gpf3con\n");
ret = -ENOMEM;
return ret;
}
gpf3dat = ioremap(FS4412_GPF3DAT, 4);
if (gpf3dat == NULL) {
printk("ioremap gpx2dat\n");
ret = -ENOMEM;
return ret;
}
gpx1con = ioremap(FS4412_GPX1CON, 4);
if (gpx1con == NULL) {
printk("ioremap gpx2con\n");
ret = -ENOMEM;
return ret;
}
gpx1dat = ioremap(FS4412_GPX1DAT, 4);
if (gpx1dat == NULL) {
printk("ioremap gpx2dat\n");
ret = -ENOMEM;
return ret;
}
gpx2con = ioremap(FS4412_GPX2CON, 4);
if (gpx2con == NULL) {
printk("ioremap gpx2con\n");
ret = -ENOMEM;
return ret;
}
gpx2dat = ioremap(FS4412_GPX2DAT, 4);
if (gpx2dat == NULL) {
printk("ioremap gpx2dat\n");
ret = -ENOMEM;
return ret;
}
return 0;
}
void fs4412_led_iounmap(void)
{
iounmap(gpf3con);
iounmap(gpf3dat);
iounmap(gpx1con);
iounmap(gpx1dat);
iounmap(gpx2con);
iounmap(gpx2dat);
}
void fs4412_led_io_init(void)
{
writel((readl(gpf3con) & ~(0xff << 16)) | (0x11 << 16), gpf3con);
writel(readl(gpx2dat) & ~(0x3<<4), gpf3dat);
writel((readl(gpx1con) & ~(0xf << 0)) | (0x1 << 0), gpx1con);
writel(readl(gpx1dat) & ~(0x1<<0), gpx1dat);
writel((readl(gpx2con) & ~(0xf << 28)) | (0x1 << 28), gpx2con);
writel(readl(gpx2dat) & ~(0x1<<7), gpx2dat);
}
struct file_operations s5pv210_led_fops = {
.owner = THIS_MODULE,
.open = s5pv210_led_open,
.release = s5pv210_led_release,
.unlocked_ioctl = s5pv210_led_unlocked_ioctl,
};
static int s5pv210_led_init(void)
{
dev_t devno = MKDEV(LED_MA, LED_MI);? ? ? ?//構建設備號
int ret;
ret = register_chrdev_region(devno, LED_NUM, "newled");? //分配設備號 ,這里用靜態申請
if (ret < 0) {
printk("register_chrdev_region\n");
return ret;
}
cdev_init(&cdev, &s5pv210_led_fops);? ? ?//cdev結構體初始化,最關鍵的是將file_operations結構體的地址關聯到cdev中
cdev.owner = THIS_MODULE;
ret = cdev_add(&cdev, devno, LED_NUM);? ?//增加設備,將主設備號和次設備號關聯到cdev中
if (ret < 0) {
printk("cdev_add\n");
goto err1;
}
ret = fs4412_led_ioremap();? ? ? ? //映射io端口的物理地址為虛擬地址,因為般來說,在系統運行時,外設的I/O內存資源的物理地址是已知的,由硬件的設計決定。但是CPU通常并沒有為這些已知的外設I/O內存資源的物理地址預定義虛擬地址范圍,驅動程序并不能直接通過物理地址訪問I/O內存資源,而必須將它們映射到核心虛地址空間內(通過頁表),然后才能根據映射所得到的核心虛地址范圍,通過訪問內存指令訪問這些I/O內存資源。
if (ret < 0)
goto err2;
fs4412_led_io_init();
printk("Led init\n");
return 0;
err2:
cdev_del(&cdev);
err1:
unregister_chrdev_region(devno, LED_NUM);? ? ?//注銷設備號
return ret;
}
static void s5pv210_led_exit(void)
{
dev_t devno = MKDEV(LED_MA, LED_MI);
fs4412_led_iounmap();
cdev_del(&cdev);
unregister_chrdev_region(devno, LED_NUM);
printk("Led exit\n");
}
module_init(s5pv210_led_init);
module_exit(s5pv210_led_exit);
總結
以上是生活随笔為你收集整理的linux设备驱动的实现与理解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 软件测试环境有几种,什么是软件测试环境_
- 下一篇: linux 其他常用命令