嵌入式Linux之設備驅動程序
本文檔以一個簡單的字符設備——LED驅動設備爲例,闡述Linux系統下設備驅動程序的基本原理以及設備驅動程序的編程方法。
1. 設備驅動簡介
1.1 linux設備驅動分類
在Linux操作系統中,一個核心的思想就是“一切皆文件”。對於驅動設備來講,也是將其作爲文件,通過open/close/read/write/cioctl(字符設備相關操作除外)進行相應的控制。在Linux系統中,設備驅動程序可以分爲三類:
字符設備:
字符設備是能夠像字節流一樣被訪問的設備,對字符設備發出的讀寫請求,相應的IO操作立即發生。Linux系統中很多設備都是字符設備,例如串口,鍵盤,鼠標等。
塊設備:
在Linux系統中進行IO以塊爲單位的設備被稱作塊設備,塊設備能夠安裝文件系統。塊設備能夠安裝文件系統。塊設備會利用一塊系統內存作爲緩衝區,因此對塊設備的訪問並不一定會立即產生IO操作,Linux環境下常見的塊設備有硬盤,軟驅等。
網絡設備:
網絡設備既可以是網卡這樣的硬件設備,也可是純軟件的設備如迴環設備。網絡設備由Linux的網絡子系統驅動,負責數據包的發送和接收,而不是面向流設備,因此在Linux系統下網絡設備並沒有節點。對網絡設備的訪問是通過socket產生的,而不是通過文件操作如read/write產生。
1.2 設備號和設備節點
Linux系統中的設備都被當做文件來處理,被稱作設備文件或者設備節點,所有的設備文件都放在系統/dev目錄下,如;
#ls /dev
consol null tty ttyS0 zero ……
在看一下設備文件的詳細信息:
ls –al /dev/ttyS0
crw-rw—- 1 root root 4.64 2009-03-27 09:27 /dev/ttyS0
塊設備與普通文件不同,信息的第一個字符c表示這個設備是字符型設備,後面的編號4表示該設備的主設備號,64表示該設備的從設備號。主設備號用於標示設備的驅動程序,從設備號表示該設備文件所指定的設備。
1.3 file_operations結構
Linux操作系統採用文件操作的方式對設備進行管理。在用戶級層面上,這些調用函數通常是open/close/write/read之類。而在設備驅動程序中,驅動對底層硬件的操作也是通過類似的讀寫操作實現的,這些函數通常命名爲xxx_open,xxx_close,xxx_write等等,xxx一般表示設備名。這樣的一組函數通常組織在結構體file_operations中,通過它把系統調用和驅動程序關聯起來。注意,file_operations結構體成員函數屬於內核空間。用戶程序空間並不能直接調用這些函數。整個系統驅動程序調用關係如下圖所示:
2. Linux驅動框架
linux2.6引入了platfrom_device的概念,在驅動的註冊和管理上帶來了變化,使用platform_device描述設備,通過platform_driver描述設備,註冊和銷燬驅動都有一系列接口函數。
2.1 Platform_device
platform_device是在系統中以獨立實體出現的設備,包括傳統的基於端口的設備,主機到外設的總線以及大部分片內集成的控制器等。這些設備的共同點是CPU可以通過總線直接對它們進行訪問。
描述platform_device的結構體定義如下,是對傳統設備的device的封裝:
struct flatfrom_device{
const char *name;
u32 id;
struct device dev;
u32 num_resource;
struct resource *resource;
}
可以看出該結構體內包含的信息,包括:設備名,設備id等等。
2.2 platform_driver結構
platform_driver是對device_driver的封裝,提供了驅動probe和remove的方法,也提供了與電源管理相關的shutdown和suspend的方法。
struct platform_driver{
int (*probe) (struct flatfrom_device *);
int (*remove)( struct flatfrom_device *);
void (*shutdown)( struct flatfrom_device *);
int (*suspend)( struct flatfrom_device*);
……
}
在定義過上述struct flatfrom_device之後,platform_driver結構體主要完成對device的各種操作。
3. LED驅動編程實例
在明白了Linux驅動程序的工作原理及主要數據結構之後,以一個簡單的字符設備,LED驅動設備爲例,闡述Linux環境下設備編程的思路和流程。
整體上來講,驅動程序編程分爲兩大步:
- 設備的申請和註冊
- 定義對設備的相關操作
第一步,主要是透過上面提到的flatfrom_device和platform_driver數據結構來實現的,
主要分爲以下四部:定義flatfrom_device,註冊flatfrom_device,定義platform_driver,註冊platform_driver。
第二步,對設備相關操作的定義,就需要根據設備類型以及需要對設備進行的操作,編寫相對應的操作函數。
3.1 設備的申請和註冊
根據上面的思路,首先需要進行的是設備的申請和註冊,主要是藉助上面提到的兩個結構體進行的。
static struct miscdevice led_miscdev =
{
.minor = MISC_DYNAMIC_MINOR,
.name = DEV_NAME,
.fops = &led_fops,
};
struct platform_device *led_device;
static struct device_driver led_driver = {
.name = DEV_NAME,
.owner = THIS_MODULE,
.bus = &platform_bus_type,
.probe = led_probe,
.remove = led_remove,
};
3.2 設備操作函數的實現
對於一個LED驅動程序來講,我們對其需要的功能應該包括打開,關閉,控制,因此需要有open,close,write,以及ioctl函數,至於read函數則不需要。因而file_operations中的函數應該包括:
static struct file_operations led_fops = {
.owner = THIS_MODULE,
.open = led_open,
.release = led_release,
.write = led_write,
.ioctl = led_ioctl,
};
填充了file_operations結構體之後,接下來要進行的就是針對該結構體內的函數進行相應的編程,這裏僅僅led_open和led_write函數列寫如下。
static int led_open(struct inode *inode, struct file *filp)
{
__raw_writel(_BIT(5), GPIO_P3_OUTP_SET(GPIO_IOBASE));/
try_module_get(THIS_MODULE);
printk( KERN_INFO DEV_NAME ” opened!/n”);
return 0;
}
static ssize_t led_write(struct file *filp, const char __user *buff, size_t count, loff_t *ppos)
{
int i;
unsigned char ctrl=0;
if (count > 1) {
return -EFBIG;
}
if (down_interruptible(&led_sem))
return -ERESTARTSYS;
get_user(ctrl, (u8 *)buff);
i = (ctrl-0x30)&0x03;
if(i==0) {
__raw_writel(_BIT(5), GPIO_P3_OUTP_CLR(GPIO_IOBASE));
} else {
__raw_writel(_BIT(5), GPIO_P3_OUTP_SET(GPIO_IOBASE));
}
up(&led_sem);
return count;
}
完成上面的4個函數之後,我們就可以填充led_fops這個結構體了,下面就是爲了應和platform結構體系,所以我們還要編寫另外的兩個函數probe和remove,實現如下:
static int led_probe(struct device *dev)
{
int ret;
printk(KERN_INFO DEV_NAME ” probing…/n”);
ret = misc_register(&led_miscdev);
if (ret)
printk(KERN_ERR “Failed to register miscdev./n”);
return ret;
}
static int led_remove(struct device *dev)
{
misc_deregister(&led_miscdev);
printk(KERN_INFO DEV_NAME ” removed!/n”);
return 0;
}
驅動都是以模塊的形式來實現的,所以就要有模塊的入口和出口,就是init和exit。
static int __init led_init(void)
{
int rc = 0;
printk(KERN_INFO DEV_NAME ” init…/n”);
__raw_writel(_BIT(5), GPIO_P3_MUX_CLR(GPIO_IOBASE));
__raw_writel(_BIT(5), GPIO_P3_OUTP_SET(GPIO_IOBASE));
led_device = platform_device_register_simple(DEV_NAME, -1, NULL, 0);
if(IS_ERR(led_device)) {
goto out;
}
rc = driver_register(&led_driver);
if (rc < 0) {
platform_device_unregister(led_device);
}
sema_init(&led_sem, 1);
out:
return rc;
}
編譯之後,在相應的文件夾中生成leddrv.ko文件,即爲LED驅動模塊。
4. 驗證
驅動模塊加載如內核之後,需要用戶層的應用程序調用才能得到驗證。應用層程序如下所示:
#include
<stdio.h>
#include<stdlib.h>
#include<fcntl.h>
#include<errno.h>
#include<unistd.h>
#include<sys/ioctl.h>
#include “../leddrv.h”
#define DEV_NAME “/dev/led”
int main(int argc, char *argv[])
{
int fd;
int dat=0;
int i;
fd=open(DEV_NAME, O_RDWR);
if(fd<0) {
perror(“can not open device”);
exit(1);
}
printf(“Test write method……/n”);
for (i=0; i<3; i++) {
dat = 0;
write(fd, &dat, 1);
usleep(300000);
dat = 1;
write(fd, &dat, 1);
usleep(300000);
}
printf(“/nTest write method OK!/n”);
printf(“Test ioctl method start……/n”);
for (i=0; i<3; i++) {
ioctl(fd, SET_LED_ON, NULL);
usleep(300000);
ioctl(fd, SET_LED_OFF, NULL);
usleep(300000);
}
printf(“/n”);
printf(“/nTest ioctl method OK!/n”);
close(fd);
return 0;
}
編譯之後,生成可執行文件main,在驗證時,首先要動態加載LED驅動模塊,然後運行該用戶程序。
insmod leddrv.ko
./main
可以看到開發板上LED燈連續閃爍六次,證明LED驅動程序正確且加載成功。