Linux下的C編程實戰(五)
――驅動程式設計
1.引言
裝置驅動程式是作業系統核心和機器硬體之間的介面,它為應用程式屏蔽硬體的細節,一般來說,Linux的裝置驅動程式需要完成如下功能:
(1)初始化裝置;
(2)提供各類裝置服務;
(3)負責核心和裝置之間的資料交換;
(4)檢測和處理裝置工作過程中出現的錯誤。
妙不可言的是,Linux下的裝置驅動程式被組織為一組完成不同任務的函數的集合,通過這些函數使得Windows的裝置操作猶如檔案一般。在應
用程式看來,硬體裝置只是一個裝置檔案,應用程式可以象操作普通檔案一樣對硬體裝置進行操作。本系列文章的第2章檔案系統編程中已經看
到了這些函數的真面目,它們就是open ()、close ()、read ()、write () 等。
Linux主要將裝置分為二類:字元裝置和塊裝置(當然網路裝置及USB等其它裝置的驅動編寫方法又稍有不同)。這兩類裝置的不同點在於:在
對字元裝置發出讀/寫請求時,實際的硬體I/O一般就緊接著發生了,而塊裝置則不然,它利用一塊系統記憶體作緩衝區,當使用者進程對裝置請求
能滿足使用者的要求,就返回請求的資料,如果不能,就調用請求函數來進行實際的I/O操作。塊裝置主要針對磁碟等慢速裝置。以字元裝置的驅
動較為簡單,因此本章主要闡述字元裝置的驅動編寫。
2.驅動模組函數
init 函數用來完成對所控裝置的初始化工作,並調用register_chrdev() 函數註冊字元裝置。假設有一字元裝置“exampledev”,則其init
函數為:
void exampledev_init(void)
{
if (register_chrdev(MAJOR_NUM, " exampledev ", &exampledev_fops))
TRACE_TXT("Device exampledev driver registered error");
else
TRACE_TXT("Device exampledev driver registered successfully");
…//裝置初始化
}
其中,register_chrdev函數中的參數MAJOR_NUM為主裝置號,“exampledev”為裝置名稱,exampledev_fops為包含基本函數進入點的結構體,類
型為file_operations。當執行exampledev_init時,它將調用核心功能register_chrdev,把驅動程式的基本進入點指標存放在核心的字元裝置
地址表中,在使用者進程對該裝置執行系統調用時提供入口地址。
file_operations結構體定義為:
struct file_operations
{
int (*lseek)();
int (*read)();
int (*write)();
int (*readdir)();
int (*select)();
int (*ioctl)();
int (*mmap)();
int (*open)();
void(*release)();
int (*fsync)();
int (*fasync)();
int (*check_media_change)();
void(*revalidate)();
};
大多數的驅動程式只是利用了其中的一部分,對於驅動程式中無需提供的功能,只需要把相應位置的值設為NULL。對於字元裝置來說,要提供
的主要入口有:open ()、release ()、read ()、write ()、ioctl ()。
open()函數 對裝置特殊檔案進行open()系統調用時,將調用驅動程式的open () 函數:
int open(struct inode * inode ,struct file * file);
其中參數inode為裝置特殊檔案的inode (索引結點) 結構的指標,參數file是指向這一裝置的檔案結構的指標。open()的主要任務是確定硬體
處在就緒狀態、驗證次裝置號的合法性(次裝置號可以用MINOR(inode-> i - rdev) 取得)、控制使用裝置的進程數、根據執行情況返回狀態代碼
(0表示成功,負數表示存在錯誤) 等;
release()函數 當最後一個開啟裝置的使用者進程執行close ()系統調用時,核心將調用驅動程式的release () 函數:
void release (struct inode * inode ,struct file * file) ;
release 函數的主要任務是清理未結束的輸入/輸出操作、釋放資源、使用者自訂排他標誌的複位等。
read()函數 當對裝置特殊檔案進行read() 系統調用時,將調用驅動程式read() 函數:
void read(struct inode * inode ,struct file * file ,char * buf ,int count) ;
參數buf是指向使用者空間緩衝區的指標,由使用者進程給出,count 為使用者進程要求讀取的位元組數,也由使用者給出。
read() 函數的功能就是從硬裝置或核心記憶體中讀取或複製count個位元組到buf 指定的緩衝區中。在複製資料時要注意,驅動程式運行在核心中
,而buf指定的緩衝區在使用者記憶體區中,是不能直接在核心中訪問使用的,因此,必須使用特殊的複製函數來完成複製工作,這些函數在<asm/
segment.h>中定義:
void put_user_byte (char data_byte ,char * u_addr) ;
void put_user_word (short data_word ,short * u_addr) ;
void put_user_long(long data_long ,long * u_addr) ;
void memcpy_tofs (void * u_addr ,void * k_addr ,unsigned long cnt) ;
參數u_addr為使用者空間地址,k_addr 為核心空間地址,cnt為位元組數。
write( ) 函數 當裝置特殊檔案進行write () 系統調用時,將調用驅動程式的write () 函數:
void write (struct inode * inode ,struct file * file ,char * buf ,int count) ;
write ()的功能是將參數buf 指定的緩衝區中的count 個位元組內容複寫到硬體或核心記憶體中,和read() 一樣,複製工作也需要由特殊函數來完
成:
unsigned char_get_user_byte (char * u_addr) ;
unsigned char_get_user_word (short * u_addr) ;
unsigned char_get_user_long(long * u_addr) ;
unsigned memcpy_fromfs(void * k_addr ,void * u_addr ,unsigned long cnt) ;
ioctl() 函數 該函數是特殊的控制函數,可以通過它向裝置傳遞控制資訊或從裝置取得狀態資訊,函數原型為:
int ioctl (struct inode * inode ,struct file * file ,unsigned int cmd ,unsigned long arg);
參數cmd為裝置驅動程式要執行的命令的代碼,由使用者自訂,參數arg 為相應的命令提供參數,類型可以是整型、指標等。
同樣,在驅動程式中,這些函數的定義也必須符合命名規則,按照本文約定,裝置“exampledev”的驅動程式的這些函數應分別命名為
exampledev_open、exampledev_ release、exampledev_read、exampledev_write、exampledev_ioctl,因此裝置“exampledev”的基本進入點
結構變數exampledev_fops 賦值如下:
struct file_operations exampledev_fops {
NULL ,
exampledev_read ,
exampledev_write ,
NULL ,
NULL ,
exampledev_ioctl ,
NULL ,
exampledev_open ,
exampledev_release ,
NULL ,
NULL ,
NULL ,
NULL
} ;
3.記憶體配置
由於Linux驅動程式在核心中運行,因此在裝置驅動程式需要申請/釋放記憶體時,不能使用使用者級的malloc/free函數,而需由核心級的函數
kmalloc/kfree () 來實現,kmalloc()函數的原型為:
void kmalloc (size_t size ,int priority);
參數size為申請分配記憶體的位元組數;參數priority說明若kmalloc()不能馬上分配記憶體時使用者進程要採用的動作:GFP_KERNEL 表示等待,即等
kmalloc()函數將一些記憶體安排到交換區來滿足你的記憶體需要,GFP_ATOMIC 表示不等待,如不能立即分配到記憶體則返回0 值;函數的傳回值指
向已指派記憶體的起始地址,出錯時,返回0。
kmalloc ()分配的記憶體需用kfree()函數來釋放,kfree ()被定義為:
# define kfree (n) kfree_s( (n) ,0)
其中kfree_s () 函數原型為:
void kfree_s (void * ptr ,int size);
參數ptr為kmalloc()返回的已指派記憶體的指標,size是要釋放記憶體的位元組數,若為0 時,由核心自動確定記憶體的大小。
4.中斷
許多裝置涉及到中斷操作,因此,在這樣的裝置的驅動程式中需要對硬體產生的插斷要求提供中斷服務程式。與註冊基本進入點一樣,驅動程
序也要請求核心將特定的插斷要求和中斷服務程式聯絡在一起。在Linux中,用request_irq()函數來實現請求:
int request_irq (unsigned int irq ,void( * handler) int ,unsigned long type ,char * name);
參數irq為要插斷要求號,參數handler為指向中斷服務程式的指標,參數type 用來確定是正常中斷還是快速中斷(正常中斷指中斷服務子程式
返回後,核心可以執行發送器來確定將運行哪一個進程;而快速中斷是指中斷服務子程式返回後,立即執行被中斷程式,正常中斷type 取值
為0 ,快速中斷type 取值為SA_INTERRUPT),參數name是裝置驅動程式的名稱。
5.執行個體
筆者最近設計了一塊採用三星S3C2410 ARM處理器的電路板(ARM處理器廣泛應用於手機、PDA等嵌入式系統),板上包含四個使用者可程式化的發光
二極體(LED),這些LED串連在ARM處理器的可程式化I/O口(GPIO)上。給出了ARM中央處理器與LED的串連原理:
<!--[if !vml]--><!--[endif]--> <!--[if !vml]--><!--[endif]-->
我們在ARM處理器上移植Linux作業系統,現在來編寫這些LED的驅動:
#include <linux/config.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/miscdevice.h>
#include <linux/sched.h>
#include <linux/delay.h>
#include <linux/poll.h>
#include <linux/spinlock.h>
#include <linux/irq.h>
#include <linux/delay.h>
#include <asm/hardware.h>
#define DEVICE_NAME "leds" /*定義led 裝置的名字*/
#define LED_MAJOR 231 /*定義led 裝置的主裝置號*/
static unsigned long led_table[] =
{
/*I/O 方式led 裝置對應的硬體資源*/
GPIO_B10, GPIO_B8, GPIO_B5, GPIO_B6,
};
/*使用ioctl 控制led*/
static int leds_ioctl(struct inode *inode, struct file *file, unsigned int cmd,
unsigned long arg)
{
switch (cmd)
{
case 0:
case 1:
if (arg > 4)
{
return -EINVAL;
}
write_gpio_bit(led_table[arg], !cmd);
default:
return -EINVAL;
}
}
static struct file_operations leds_fops =
{
owner: THIS_MODULE, ioctl: leds_ioctl,
};
static devfs_handle_t devfs_handle;
static int __init leds_init(void)
{
int ret;
int i;
/*在核心中註冊裝置*/
ret = register_chrdev(LED_MAJOR, DEVICE_NAME, &leds_fops);
if (ret < 0)
{
printk(DEVICE_NAME " can't register major number"n");
return ret;
}
devfs_handle = devfs_register(NULL, DEVICE_NAME, DEVFS_FL_DEFAULT, LED_MAJOR,
0, S_IFCHR | S_IRUSR | S_IWUSR, &leds_fops, NULL);
/*使用宏進行連接埠初始化,set_gpio_ctrl 和write_gpio_bit 均為宏定義*/
for (i = 0; i < 8; i++)
{
set_gpio_ctrl(led_table[i] | GPIO_PULLUP_EN | GPIO_MODE_OUT);
write_gpio_bit(led_table[i], 1);
}
printk(DEVICE_NAME " initialized"n");
return 0;
}
static void __exit leds_exit(void)
{
devfs_unregister(devfs_handle);
unregister_chrdev(LED_MAJOR, DEVICE_NAME);
}
module_init(leds_init);
module_exit(leds_exit);
使用命令方式編譯led 驅動模組:
#arm-linux-gcc -D__KERNEL__ -I/arm/kernel/include
-DKBUILD_BASENAME=leds -DMODULE -c -o leds.o leds.c
以上命令將產生leds.o 檔案,把該檔案複製到板子的/lib目錄下,使用以下命令就可以安裝leds驅動模組:
#insmod /lib/ leds.o
刪除該模組的命令是:
#rmmod leds
6.小結
本章講述了Linux裝置驅動程式的入口函數及驅動程式中的記憶體申請、中斷等,並給出了一個通過ARM處理器的GPIO口控制LED的驅動執行個體。