1. 程式人生 > >linux驅動程式介面

linux驅動程式介面

1. Linux驅動程式介面

系統呼叫是作業系統核心與應用程式之間的介面,裝置驅動程式則是作業系統核心與機器硬體的介面。幾乎所有的系統操作最終對映到物理裝置,除了CPU、記憶體 和少數其它裝置,所有的裝置控制操作都由該裝置特殊的可執行程式碼實現,此程式碼就是裝置驅動程式。作業系統核心需要訪問兩類主要裝置:字元裝置和塊裝置。與 此相關主要有兩類裝置驅動程式,字元裝置驅動程式和塊裝置驅動程式。Linux(也是所有UNIX)的基本原理之一是:系統試圖使它對所有各類裝置的輸 入、輸出看起來就好象對普通檔案的輸入、輸出一樣。裝置驅動程式本身具有檔案的外部特徵,它們都能使用象
open(),close(),read(),write()等系統呼叫。為使裝置的存取能象檔案一樣處理,所有裝置在目錄中應有對應的檔名稱,才可使用有關係統呼叫。
通常Linux驅動程式介面分為如下四層:
1).應用程式程序與核心的介面;
2).核心與檔案系統的介面;
3).檔案系統與裝置驅動程式的介面;
4).裝置驅動程式與硬體裝置的介面。
§2. 驅動程式檔案操作資料結構

每個驅動程式都有一個file-operation的資料結構,包含指向驅動程式內部函式的指標。file-operation的資料結構為:
struct file-operation {
int (*lseek)();
int (*read)();
int (*write)();
int (*readdir)();
int (*select)();
int (*ioctl)();
int (*mmap)();
int (*open)();
int (*close)();
int (*release)();
int (*fsync)();
int (*fasync)();
int (*check-media-change)();
int (*revalidate)();
}
核心中有兩個表,一個用於字元裝置驅動程式,一個用於塊裝置驅動程式。這兩個表用於儲存指向file-operation資料結構的指標,驅動程式內部函 數的地址儲存在這一結構。核心用主裝置號作為索引訪問file-operation結構,可以訪問驅動程式子程式地址。SBS617裝置採用了PCI匯流排 字元裝置的驅動程式實現方式。完成了裝置驅動程式,經GNU軟體編譯,連結,產生一可載入模組,可以用於動態裝入Linux作業系統核心,也可以在需要時 從核心中卸除。

§3. file_operations介紹
在結構file_operations裡,指出了裝置驅動程式所提供的入口點位置,分別是:
(1) lseek,移動檔案指標的位置,顯然只能用於可以隨機存取的裝置。
(2) read,進行讀操作,引數buf為存放讀取結果的緩衝區,count為所要讀取的資料長度。返回值為負表示讀取操作發生錯誤,否則返回實際讀取的位元組 數。對於字元型,要求讀取的位元組數和返回的實際讀取位元組數都必須是inode->i_blksize的的倍數。
(3) write,進行寫操作,與read類似。
(4) readdir,取得下一個目錄入口點,只有與檔案系統相關的裝置驅動程式才使用。
(5) selec,進行選擇操作,如果驅動程式沒有提供select入口,select操作將會認為裝置已經準備好進行任何的I/O操作。
(6) ioctl,驅動程式特殊控制入口點,進行讀、寫以外的其它操作,引數cmd為自定義的命令。 這是很有意思的部分,之後我會詳盡介紹;
(7) mmap,用於把裝置的內容對映到地址空間,一般只有塊裝置驅動程式使用。
(8) open,開啟裝置準備進行I/O操作。返回0表示開啟成功,返回負數表
示失敗。如果驅動程式沒有提供open入口,則只要/dev/driver檔案存
在就認為開啟成功。
(9) release,即close操作。
裝置驅動程式所提供的入口點,在裝置驅動程式初始化的時候向系統進行登記,以便系統在適當的時候呼叫。

§4 PCI字元裝置驅動程式

要設計PCI裝置驅動程式,必須進一步結合硬體裝置和PCI匯流排的特性。設計PCI裝置驅動程式的重要任務是找尋相應的硬體並實現對它的訪問。作為外圍設 備的硬體必須響應三種地址空間的訪問,即記憶體,IO,暫存器地址空間。前兩種地址空間可以為PCI總線上的所有裝置共享。暫存器空間佔用實體地址,可以通 過特殊的函式來訪問配置暫存器。一旦可以訪問配置暫存器,裝置驅動程式就可以訪問硬體了。每個裝置的PCI配置暫存器均由256Bytes構成,其中 64Bytes是標準化的,4Bytes標識了一個唯一的函式ID,通過這個ID驅動程式就可以定位該裝置。
存取系統中的字元裝置和存取系統檔案一樣。應用程式使用標準的系統呼叫來開啟、讀寫和關閉裝置,就像使用一個檔案-樣。當字元裝置初始化時,通過向 chrdevs陣列中新增一個入口,裝置驅動程式在系統核心中註冊。 chrdevs陣列由device_struct資料結構組成。裝置的主裝置號用來作為此chrdevs的索引,因為一個裝置的主裝置號是固定的。

LINUX系統裡,通過呼叫register_chrdev向系統註冊字元型裝置驅動程式。register_chrdev定義為:
#include linux/fs.h
#include linux/errno.h
int register_chrdev(unsigned int major, const char *name,
struct file_operations *fops);
其中,major是為裝置驅動程式向系統申請的主裝置號,如果為0則系統為此驅動程式動態地分配一個主裝置號。name是裝置名。fops就是前面所說的 對各個呼叫的入口點的說明。此函式返回0表示成功。返回-EINVAL表示申請的主裝置號非法,一般來說是主裝置號大於系統所允許的最大裝置號。返回- EBUSY表示所申請的主裝置號正在被其它裝置驅動程式使用。如果是動態分配主裝置號成功,此函式將返回所分配的主裝置號。

§5 PCI裝置啟動與檢測

PC主機板BIOS在系統啟動時,可以自動檢測PCI裝置並配置裝置的每一地址區。當驅動程式訪問裝置時,它的記憶體、I/O地址空間已經對映到程序的地址空 間了。在驅動程式init_module()中,通過呼叫函式pcibios_find_device()函式返回裝置在總線上的位置及函式指標,其中的 包含檔案及函式原型為:

#include Linux/pci.h
#include Linux/config.h
#include Linux/bios32.h
int pcibios_find_device(unsigned short vendor,
unsigned short id, unsigned short index,
unsigned char *bus, unsigned short *function)

§6 地址空間訪問

在裝置驅動程式檢測到裝置之後,通常要從三個地址空間讀寫資料,其中暫存器空間的讀寫尤為重要,因為只有通過它驅動程式才可能找到裝置記憶體和I/O空間的對映地址。裝置驅動程式通過呼叫以下函式實現暫存器空間的訪問,其中的包含檔案及函式原型為:

#include Linux/bios32.h
int pcibios_read_config_byte( unsigned char bus,
unsigned char function,
unsigned char where,
unsigned char b*ptr)

int pcibios_write_config_byte(unsigned char bus,
unsigned char function,
unsigned char where,
unsigned char b*ptr)

類似的還有:
pcibios_read_config_word (), pcibios_write_config_word () ,
pcibios_read_config_dword () ,pcibios_write_config_dword() 呼叫。
PCI裝置最多有6個地址區,型別可以為記憶體區或I/O區。介面板可以通過配置暫存器的PCI_BASE_ADDRESS_0 到PCI_BASE_ADDRESS_5來報告各地址區的實際地址位置。記憶體、IO空間的訪問通過inb(),memcpy()等呼叫。當然可以通過 pcibios_read_config_byte(),pcibios_write_config_byte() 來訪問配置暫存器的相應基地址值。

§7 中斷處理


對中斷的處理是屬於系統核心的部分, PC主機板BIOS為多數裝置分配了一個唯一的中斷號,在配置暫存器中儲存, 裝置驅動程式通過pcibios_read_config_byte() 函式讀取相應的值,格式為:

xxx_irq=pcibios_read_config_byte(pci_bus,pci_device_fn,
PCI_INTERRUPT_LINE,
&pci_cofig->int_line)

作業系統中有中斷暫存器,將特定的中斷請求與中斷處理函式聯絡在一起,當中斷髮生時呼叫相應的中斷處理函式處理。Linux作業系統下可用request_irq(),free_irq( )實現中斷的請求,釋放,其中包含檔案及形式為:

#include Linux/sched.h
int request_irq(unsigned int irq,
void (*handler)(int irq,void dev_id,struct pt_regs *regs),
unsigned long flags,
const char *device,
void *dev_id);
void free_irq(unsigned int irq, void *dev_id);
引數irq表示所要申請的硬體中斷號。handler為向系統登記的中斷處理子程式,中斷產生時由系統來呼叫,呼叫時所帶引數irq為中斷號, dev_id為申請時告訴系統的裝置標識,regs為中斷髮生時暫存器內容。device為裝置名,將會出現在/proc/interrupts檔案裡。
flag是申請時的選項,它決定中斷處理 程式的一些特性,有兩種方式寫中斷方式裝置驅動程式:即快中斷方式和定時等待方式。採取快中斷方式需要將request_irq()的第三個type型別 引數設為SA_INTERRUPT。 正常中斷與快中斷的區別在於: 從正常中斷返回時,核心可以利用機會排程更優先的程序執行; 而快中斷不進行排程立即恢復被中斷程式的執行.
在LINUX系統中,中斷可以被不同的中斷處理程式共享,這要求每一個共享此中斷的處理程式在申請中斷時在flags裡設定SA_SHIRQ,這些處理程 序之間以dev_id來區分。如果中斷由某個處理程式獨佔,則dev_id 可以為NULL。request_irq返回0表示成功,返回-INVAL表示irq>15或 handler==NULL,返回-EBUSY表示中斷已經被佔用且不能共享。

中斷處理函式形式為:

void xxx_irq_handler(int xxx_irq,void *dev_id,
struct pt_regs *regs)

§8 特殊控制函式ioctl()

ioctl()具有裝置特殊性,不同於read() , write(),在於它允許應用程式訪問、配置裝置,並進入可能的操作模式。 通常的read()、write()不能使用這些控制操作。ioctl()可以控制I/O通道。裝置驅動的一個特點是要與其它裝置硬體交換讀/寫的資料並 需要同步控制。
多數的ioctl()由一系列的switch語句組成, ioctl()命令及操作選擇考慮到硬體的特性和實際要實現的功能。寫ioctl()程式之前,應選擇相應的命令,不應該簡單使用1-N的數字。選擇ioctl()的命令有以下的考慮:
·首先命令碼在系統中應該唯一,以避免與其它裝置衝突,每個命令碼應由多個位元域構成。
· 參考兩個檔案來幫助選擇ioctl()的命令,include/asm/ioctl.h及Documentation/ioctl_number.txt 有如下定義:
命令碼有四個8位元組,其相應取值的巨集定義及含義如下表:

命令碼取值巨集定義及含義
位元組名稱 取值巨集定義 含義
type _IOC_TYEBITS 表示每個驅動程式唯一的型別標識
number _IOC_NRBITS 表示序列號
direction _IOC_NONE, _IOC_READ,
_IOC_WRITE,_IOC_READ|WRITE 表示資料傳輸的方向
size _IOC_SIZEBITS 表示傳輸資料的大小

在標頭檔案< asm/ioctl.h >中定義了設定命令碼的一些有用的巨集:
_IO(type,nr);
_IOR(type,nr,size);
_IOW(type,nr,size);
_IOC_DIR(nr);
_IOC_TYPE(nr);
_IOC_NR(nr);
_IOC_SIZE(nr);

這些設定與具體的硬體功能有關,可以參考有關的硬體手冊。通過以上方式可以設定命令、獲得裝置引數及實現控制操作, 完成裝置驅動程式的重要功能。
對於裝置驅動程式,ioctl() 函式非常重要,使用者可以通過它來控制裝置函式,獲取狀態資訊,進行資料的讀寫。
ioctl()在使用者空間的形式為:

int (*ioctl)(struct inode *inode , struct file *file ,
unsigned int cmd , unsigned long arg)

其中cmd相當於一個選擇碼,取決於使用的特殊控制命令,cmd命令通常在標頭檔案中宣告。直接的呼叫的格式為:

temp = ioctl( fd, XX_xxxx, param* );

\"fd\"是裝置檔案控制代碼。XX_xxxx 是控制碼。Param是一個引數結構的指標,當呼叫 ioctl()時,需要理解一些特殊引數結構 ,可以參考下面的四個表格。返回值0表示成功,-1失敗。

§9.呼叫Linux核心函式

Linux有許多核心函式可以呼叫。例如;

1)memcpy_fromfs( *toptr, *fromptr, sizeof());
// 用於從檔案系統傳輸資料
2) memcpy_tofs ( *toptr, *fromptr, sizeof());
// 用於將資料傳輸到檔案系統
#include asm/segment.h
void memcpy_fromfs(void * toptr,const void * fromptr,unsigned long n);
void memcpy_tofs(void * toptr,const void * fromptr,unsigned long n);
在使用者程式呼叫read 、write時,因為程序的執行狀態由使用者態變為核心 態,地址空間也變為核心地址空間。而read、write中引數buf是指向使用者程 序的私有地址空間的,所以不能直接訪問,必須通過上述兩個系統函式來訪問使用者程式的私有地址空間。memcpy_fromfs由使用者程式地址空間往核心地 址空間複製,memcpy_tofs則反之。引數toptr為複製的目的指標,fromptr為源指標,n 為要複製的位元組數。

3) ptr = vmalloc( sizeof() );// 動態分配記憶體
4) vfree( ptr ); // 動態釋放記憶體
5) vremap( xxx_mapping[ chn ].pci_addr,
xxx_mapping[ chn ].len );
// 對映PCI地址,
chn = current_map_chn.

6)作為系統核心的一部分,裝置驅動程式在申請和釋放記憶體時不是呼叫malloc
和free,而呼叫kmalloc和kfree,定義為:
#include linux/kernel.h
void * kmalloc(unsigned int len, int priority);
void kfree(void * ptr);
引數len為希望申請的位元組數,ptr為要釋放的記憶體指標。priority為分配記憶體操作的優先順序,即在沒有足夠空閒記憶體時如何操作,一般用GFP_KERNEL。
7)與中斷和記憶體不同,使用一個沒有申請的I/O埠不會使CPU產生異常, 也
就不會導致諸如\"segmentation fault\"一類的錯誤發生。任何程序都可以訪問
任何一個I/O埠。此時系統無法保證對I/O埠的操作不會發生衝突,甚至會因此而使系統崩潰。因此,在使用I/O埠前,也應該檢查此I/O埠是否已有 別的程式在使用,若沒有,再把此埠標記為正在使用,在使用完以後釋放它。
這樣需要用到如下幾個函式:
int check_region(unsigned int from, unsigned int extent);
void request_region(unsigned int from, unsigned int extent,
const char *name);
void release_region(unsigned int from, unsigned int extent);
呼叫這些函式時的引數為:from表示所申請的I/O埠的起始地址;
extent為所要申請的從from開始的埠數;name為裝置名,將會出現在
/proc/ioports檔案裡。check_region返回0表示I/O埠空閒,否則為正在
被使用。
在申請了I/O埠之後,就可以如下幾個函式來訪問I/O埠:
#include asm/io.h
inline unsigned int inb(unsigned short port);
inline unsigned int inb_p(unsigned short port);
inline void outb(char value, unsigned short port);
inline void outb_p(char value, unsigned short port);
其中inb_p和outb_p插入了一定的延時以適應某些慢的I/O埠。
9)在裝置驅動程式裡,一般都需要用到計時機制。在LINUX系統中,時鐘是
由系統接管,裝置驅動程式可以向系統申請時鐘。與時鐘有關的系統呼叫有:
#include asm/param.h
#include linux/timer.h
void add_timer(struct timer_list * timer);
int del_timer(struct timer_list * timer);
inline void init_timer(struct timer_list * timer);
struct timer_list的定義為:
struct timer_list {
struct timer_list *next;
struct timer_list *prev;
unsigned long expires;
unsigned long data;
void (*function)(unsigned long d);
};
其中expires是要執行function的時間。系統核心有一個全域性變數JIFFIES
表示當前時間,一般在呼叫add_timer時jiffies=JIFFIES+num,表示在num個
系統最小時間間隔後執行function。系統最小時間間隔與所用的硬體平臺有關,在核心裡定義了常數HZ表示一秒內最小時間間隔的數目,則num*HZ 表示num 秒。系統計時到預定時間就呼叫function,並把此子程式從定時佇列裡刪除,因此如果想要每隔一定時間間隔執行一次的話,就必須在function裡 再一次呼叫add_timer。function的引數d即為timer裡面的data項。
10)在裝置驅動程式裡,還可能會用到如下的一些系統函式:
#include asm/system.h
#define cli() __asm__ __volatile__ (\"cli\"::)
#define sti() __asm__ __volatile__ (\"sti\"::)
這兩個函式負責開啟和關閉中斷允許。
11)在裝置驅動程式裡,可以呼叫printk來列印一些除錯資訊,用法與printf 類似。
printk列印的資訊不僅出現在螢幕上,同時還記錄在檔案syslog裡。