嵌入式Linux之設備驅動程序

嵌入式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驅動程序正確且加載成功。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章