嵌入式驅動開發流程


嵌入式系統中,操作系統是通過各種驅動程序來駕馭硬件設備的。設備驅動程序是操作系統內核和硬件設備之間的接口,它爲應用程序屏蔽了硬件的細節,這樣在應用程序看來,硬件設備只是一個設備文件,可以像操作普通文件一樣對硬件設備進行操作。設備驅動程序是內核的一部分,完成以下功能:
◇ 驅動程序的註冊和註銷。
◇ 設備的打開和釋放。
◇ 設備的讀寫操作。
◇ 設備的控制操作。
◇ 設備的中斷和輪詢處理。
Linux主要將設備分爲三類:字符設備、塊設備和網絡設備。字符設備是指發送和接收數據以字符的形式進行,沒有緩衝區的設備;塊設備是指發送和接收數據以整個數據緩衝區的形式進行的設備;網絡設備是指網絡設備訪問的BSD socket 接口。下面以字符設備爲例,寫出其驅動編寫框架:
一、 編寫驅動程序初始化函數
  驅動程序的初始化在函數xxx_init()中完成,包括對硬件初始化、中斷函數、向內核註冊驅動程序等。
  首先理解硬件結構,搞清楚其功能,接口寄存器以及CPU怎麼訪問控制這些寄存器等。
  其次向內核註冊驅動程序。設備驅動程序可以直接編譯進內核,在系統啓動的時候初始化,也可以在需要的時候以模塊的方式動態加載到內核中去。每個字符設備或是塊設備都是通過register_chrdev()函數註冊,調用該函數後就可以向系統申請主設備號,操作成功,設備名就會出現在/proc/devices裏。
  此外,在關閉設備時,需要先解除原先設備的註冊,需要有清除函數,在xxx_exit()中通過unregister_chrdev()函數在實現,此後設備就會從/proc/devices裏消失。
  當驅動程序被編譯成模塊時,使用insmod加載模塊,模塊的初始化函數xxx_init()被調用,向內核註冊驅動程序;使用rmmod卸載模塊,模塊的清除函數xxx_exit()被調用。
二、 構造file_operations結構中要用到的各個成員函數
  Linux操作系統將所有的設備都看成文件,以操作文件的方式訪問設備。應用程序不能直接操作硬件,使用統一的接口函數調用硬件驅動程序,這組接口被成爲系統調用。每個系統調用中都有一個與之對應的函數(open、release、read、write、ioctl等),在字符驅動程序中,這些函數集合在一個file_operations類型的數據結構中。以一個鍵盤驅動程序爲例:
struct file_operations Key7279_fops =  
{
.open = Key7279_Open,  
.ioctl = Key7279_Ioctl,  
.release = Key7279_Close,
.read = Key7279_Read,
};
1、 設備的打開和釋放
  打開設備是由open()函數來完成,在大部分設備驅動中open完成如下工作:
  ◇ 遞增計數器
  ◇ 檢查特定設備的特殊情況
  ◇ 初始化設備
  ◇ 識別次設備號
  釋放設備由release()函數來完成。當一個進程釋放設備時,其它進程還能繼續使用該設備,只是該進程暫時停止對該設備的的使用,而當一個進程關閉設備時,其它進程必須重新打開此設備才能使用。Release完成如下工作:
◇ 遞減計數
◇ 在最後一次釋放設備操作時關閉設備
2、 設備的讀寫操作
  讀寫設備的主要任務就是把內核空間的數據複製到用戶空間,或者是從用戶空間複製到內核空間,也就是將內核空間緩衝區裏的數據複製到用戶空間的緩衝區中或者相反。字符設備使用各自的read()函數和write()函數來進行數據讀寫。
3、 設備的控制操作
大部分設備除了讀寫能力,還可進行超出簡單的數據傳輸之外的操作,所以設備驅動也必須具備進行各種硬件控制操作的能力. 這些操作常常通過 ioctl 方法來支持。與讀寫操作不同,ioctl()的用法與具體設備密切相關。以鍵盤Key7279_Ioctl爲例:
static int Key7279_Ioctl(struct inode *inode,struct file *file,unsigned int cmd, unsigned long arg)
{
switch(cmd)  
  {
  case Key7279_GETKEY:
return key7279_getkey();
  default:
  printk("Unkown Keyboard Command ID.\n");
  }
  return 0;
}
  cmd的取值及含義都與具體的設備有關,除了ioctl(),設備驅動程序還可能有其他控制函數,比如llseek()等。
  當應用程序使用open、release等函數打開某個設備時,設備驅動程序的file_operations結構中的相應成員就會被調用。
三、設備的中斷和輪詢處理
  對於不支持中斷的設備,讀寫時需要輪詢設備狀態,以及是否需要繼續進行數據傳輸。例如,打印機。如果設備支持中斷,則可按照中斷方式進行。
模塊在使用中斷前要先請求一箇中斷通道(或者 IRQ中斷請求),並在使用後釋放它。通過request_irq()函數來註冊中斷,free_irq()函數來釋放。
四、驅動程序的測試
  對驅動程序的調試可以通過打印的方式來進行,就是通過在驅動程序中添加printk()打印函數,來跟蹤驅動程序的執行過程,以此來判斷問題。
  以上是我根據自己的學習總結的,可能寫的比較簡單,對於比較複雜的驅動函數,會添加更多的函數,但是大體的框架就是這樣了。
 
 
基於操作系統的驅動就是在無操作系統下的硬件接口函數加上操作系統外套
實現一個嵌入式Linux設備驅動程序的大致流程如下:
(l)查看原理圖,理解設備的工作原理。
(2)定義主設備號。設備由一個主設備號和一個次設備號來標識。主設備號唯一標識了設
備類型,即設備驅動程序類型,它是塊設備表或字符設備表中設備表項的索引。次設備號僅
由設備驅動程序解釋,區分被一個設備驅動控制下的某個獨立的設備。
(3)實現初始化函數。在驅動程序中實現驅動的註冊和卸載。
(4)設計所要實現的文件操作,定義file--operations結構。
(5)實現所需的文件操作調用,如read,write等。
(6)實現中斷服務,並用request--irq向內核註冊,中斷並不是每個設備驅動所必需的。
(7)編譯該驅動程序到內核中,或者用insmod命令加載模塊。
(8)測試該設備,編寫應用程序,對驅動程序進行測試。
典型字符設備驅動編寫框架:
1 編寫硬件接口函數
2 建立文件系統與設備驅動程序間的接口,如:struct file_operations結構體
3 註冊設備到chrdevfs全局數組中,註冊或註銷設備可以在任何時候,但一般在模塊加載時註冊設備,在模塊退出時註銷設備。(module_init();module_exit();)
4 以模塊方式編譯驅動源碼,並將其加載到內核中
5 創建設備節點,mknode

6 編寫應用程序訪問底層設備



二、實例剖析

  我們來寫一個最簡單的字符設備驅動程序。雖然它什麼也不做,但是通過它可以瞭解Linux的設備驅動程序的工作原理.把下面的C代碼輸入機器,你就會獲得一個真正的設備驅動程序.不過我的kernel是2.0.34,在低版本的kernel上可能會出現問題,我還沒測試過.//xixi

  #define __NO_VERSION__ 
  #include <linux/modules.h> 
  #include <linux/version.h>

  char kernel_version [] = UTS_RELEASE;

  這一段定義了一些版本信息,雖然用處不是很大,但也必不可少.Johnsonm說所有的驅動程序的開頭都要包含<linux/config.h>,但我看倒是未必.

  由於用戶進程是通過設備文件同硬件打交道,對設備文件的操作方式不外乎就是一些系統調用,如 open,read,write,close...., 注意,不是fopen, fread,但是如何把系統調用和驅動程序關聯起來呢?這需要了解一個非常關鍵的數據結構:

struct file_operations {

int (*seek) (struct inode * ,struct file *, off_t ,int); 
int (*read) (struct inode * ,struct file *, char ,int); 
int (*write) (struct inode * ,struct file *, off_t ,int); 
int (*readdir) (struct inode * ,struct file *, struct dirent * ,int); 
int (*select) (struct inode * ,struct file *, int ,select_table *); 
int (*ioctl) (struct inode * ,struct file *, unsined int ,unsigned long); 
int (*mmap) (struct inode * ,struct file *, struct vm_area_struct *); 
int (*open) (struct inode * ,struct file *); 
int (*release) (struct inode * ,struct file *); 
int (*fsync) (struct inode * ,struct file *); 
int (*fasync) (struct inode * ,struct file *,int); 
int (*check_media_change) (struct inode * ,struct file *); 
int (*revalidate) (dev_t dev); 

 

  這個結構的每一個成員的名字都對應着一個系統調用.用戶進程利用系統調用在對設備文件進行諸如read/write操作時,系統調用通過設備文件的主設備號找到相應的設備驅動程序,然後讀取這個數據結構相應的函數指針,接着把控制權交給該函數.這是linux的設備驅動程序工作的基本原理.既然是這樣,則編寫設備驅動程序的主要工作就是編寫子函數,並填充file_operations的各個域.

  相當簡單,不是嗎?

  下面就開始寫子程序.

#include <linux/types.h> 
#include <linux/fs.h> 
#include <linux/mm.h> 
#include <linux/errno.h> 
#include <asm/segment.h> 
unsigned int test_major = 0;

static int read_test(struct inode *node,struct file *file, 
char *buf,int count) 
{

int left;

if (verify_area(VERIFY_WRITE,buf,count) == -EFAULT ) 
return -EFAULT;

for(left = count ; left > 0 ; left--) 

__put_user(1,buf,1); 
buf++; 

return count; 

 

  這個函數是爲read調用準備的.當調用read時,read_test()被調用,它把用戶的緩衝區全部寫1.buf 是read調用的一個參數.它是用戶進程空間的一個地址.但是在read_test被調用時,系統進入核心態.所以不能使用buf這個地址,必須用__put_user(),這是kernel提供的一個函數,用於向用戶傳送數據.另外還有很多類似功能的函數.請參考.在向用戶空間拷貝數據之前,必須驗證buf是否可用。

 
 這就用到函數verify_area.

static int write_tibet(struct inode *inode,struct file *file, 
const char *buf,int count) 

return count; 
}

static int open_tibet(struct inode *inode,struct file *file ) 

MOD_INC_USE_COUNT; 
return 0; 
}

static void release_tibet(struct inode *inode,struct file *file ) 

MOD_DEC_USE_COUNT; 

 

  這幾個函數都是空操作.實際調用發生時什麼也不做,他們僅僅爲下面的結構提供函數指針。

struct file_operations test_fops = { 
NULL, 
read_test, 
write_test, 
NULL, /* test_readdir */ 
NULL, 
NULL, /* test_ioctl */ 
NULL, /* test_mmap */ 
open_test, 
release_test, NULL, /* test_fsync */ 
NULL, /* test_fasync */ 
/* nothing more, fill with NULLs */ 
}; 

  設備驅動程序的主體可以說是寫好了。現在要把驅動程序嵌入內核。驅動程序可以按照兩種方式編譯。一種是編譯進kernel,另一種是編譯成模塊(modules),如果編譯進內核的話,會增加內核的大小,還要改動內核的源文件,而且不能動態的卸載,不利於調試,所以推薦使用模塊方式。

int init_module(void) 

int result;

result = register_chrdev(0, "test", &test_fops);

if (result < 0) { 
printk(KERN_INFO "test: can't get major number\n"); 
return result; 
}

if (test_major == 0) test_major = result; /* dynamic */ 
return 0; 

 

  在用insmod命令將編譯好的模塊調入內存時,init_module 函數被調用。在這裏,init_module只做了一件事,就是向系統的字符設備表登記了一個字符設備。register_chrdev需要三個參數,參數一是希望獲得的設備號,如果是零的話,系統將選擇一個沒有被佔用的設備號返回。參數二是設備文件名,參數三用來登記驅動程序實際執行操作的函數的指針。

  如果登記成功,返回設備的主設備號,不成功,返回一個負值。

void cleanup_module(void) 

unregister_chrdev(test_major, "test"); 

  在用rmmod卸載模塊時,cleanup_module函數被調用,它釋放字符設備test在系統字符設備表中佔有的表項。

  一個極其簡單的字符設備可以說寫好了,文件名就叫test.c吧。

  下面編譯

  $ gcc -O2 -DMODULE -D__KERNEL__ -c test.c

  得到文件test.o就是一個設備驅動程序。

  如果設備驅動程序有多個文件,把每個文件按上面的命令行編譯,然後

  ld -r file1.o file2.o -o modulename.

  驅動程序已經編譯好了,現在把它安裝到系統中去。

  $ insmod -f test.o

  如果安裝成功,在/proc/devices文件中就可以看到設備test,並可以看到它的主設備號。

要卸載的話,運行


  $ rmmod test

  下一步要創建設備文件。

  mknod /dev/test c major minor

  c 是指字符設備,major是主設備號,就是在/proc/devices裏看到的。

  用shell命令

  $ cat /proc/devices | awk "}"

  就可以獲得主設備號,可以把上面的命令行加入你的shell script中去。

  minor是從設備號,設置成0就可以了。

  我們現在可以通過設備文件來訪問我們的驅動程序。寫一個小小的測試程序。

#include <stdio.h> 
#include <sys/types.h> 
#include <sys/stat.h> 
#include <fcntl.h>

main() 

int testdev; 
int i; 
char buf[10];

testdev = open("/dev/test",O_RDWR);

if ( testdev == -1 ) 

printf("Cann't open file \n"); 
exit(0); 
}

read(testdev,buf,10);

for (i = 0; i < 10;i++) 
printf("%d\n",buf[i]);

close(testdev); 

 


  編譯運行,看看是不是打印出全1 ?

  以上只是一個簡單的演示。真正實用的驅動程序要複雜的多,要處理如中斷,DMA,I/O port等問題。這些纔是真正的難點。請看下節,實際情況的處理。

  如何編寫Linux操作系統下的設備驅動程序

  三、設備驅動程序中的一些具體問題

  1. I/O Port.

  和硬件打交道離不開I/O Port,老的ISA設備經常是佔用實際的I/O端口,在linux下,操作系統沒有對I/O口屏蔽,也就是說,任何驅動程序都可對任意的I/O口操作,這樣就很容易引起混亂。每個驅動程序應該自己避免誤用端口。

  有兩個重要的kernel函數可以保證驅動程序做到這一點。

  1)check_region(int io_port, int off_set)

  這個函數察看系統的I/O表,看是否有別的驅動程序佔用某一段I/O口。

  參數1:io端口的基地址,

  參數2:io端口占用的範圍。

  返回值:0 沒有佔用, 非0,已經被佔用。

  2)request_region(int io_port, int off_set,char *devname)

  如果這段I/O端口沒有被佔用,在我們的驅動程序中就可以使用它。在使用之前,必須向系統登記,以防止被其他程序佔用。登記後,在/proc/ioports文件中可以看到你登記的io口。

  參數1:io端口的基地址。

  參數2:io端口占用的範圍。

  參數3:使用這段io地址的設備名。

  在對I/O口登記後,就可以放心地用inb(), outb()之類的函來訪問了。

  在一些pci設備中,I/O端口被映射到一段內存中去,要訪問這些端口就相當於訪問一段內存。經常性的,我們要獲得一塊內存的物理地址。在dos環境下,(之所以不說是dos操作系統是因爲我認爲DOS根本就不是一個操作系統,它實在是太簡單,太不安全了)只要用段:偏移就可以了。在window95中,95ddk提供了一個vmm 調用 _MapLinearToPhys,用以把線性地址轉化爲物理地址。但在Linux中是怎樣做的呢?

  2.內存操作


  在設備驅動程序中動態開闢內存,不是用malloc,而是kmalloc,或者用get_free_pages直接申請頁。釋放內存用的是kfree,或free_pages. 請注意,kmalloc等函數返回的是物理地址!而malloc等返回的是線性地址!關於kmalloc返回的是物理地址這一點本人有點不太明白:既然從線性地址到物理地址的轉換是由386cpu硬件完成的,那樣彙編指令的操作數應該是線性地址,驅動程序同樣也不能直接使用物理地址而是線性地址。但是事實上kmalloc返回的確實是物理地址,而且也可以直接通過它訪問實際的RAM,我想這樣可以由兩種解釋,一種是在覈心態禁止分頁,但是這好像不太現實;另一種是linux的頁目錄和頁表項設計得正好使得物理地址等同於線性地址。我的想法不知對不對,還請高手指教。

  言歸正傳,要注意kmalloc最大隻能開闢128k-16,16個字節是被頁描述符結構佔用了。kmalloc用法參見khg.

  內存映射的I/O口,寄存器或者是硬件設備的RAM(如顯存)一般佔用F0000000以上的地址空間。在驅動程序中不能直接訪問,要通過kernel函數vremap獲得重新映射以後的地址。

  另外,很多硬件需要一塊比較大的連續內存用作DMA傳送。這塊內存需要一直駐留在內存,不能被交換到文件中去。但是kmalloc最多隻能開闢128k的內存。

  這可以通過犧牲一些系統內存的方法來解決。

  具體做法是:比如說你的機器由32M的內存,在lilo.conf的啓動參數中加上mem=30M,這樣linux就認爲你的機器只有30M的內存,剩下的2M內存在vremap之後就可以爲DMA所用了。

  請記住,用vremap映射後的內存,不用時應用unremap釋放,否則會浪費頁表。

  3.中斷處理

  同處理I/O端口一樣,要使用一箇中斷,必須先向系統登記。

int request_irq(unsigned int irq ,

void(*handle)(int,void *,struct pt_regs *),

unsigned int long flags,

const char *device);

irq: 是要申請的中斷。

handle:中斷處理函數指針。

flags:SA_INTERRUPT 請求一個快速中斷,0 正常中斷。

device:設備名。
 


  如果登記成功,返回0,這時在/proc/interrupts文件中可以看你請求的中斷。

  4.一些常見的問題。

  對硬件操作,有時時序很重要。但是如果用C語言寫一些低級的硬件操作的話,gcc往往會對你的程序進行優化,這樣時序就錯掉了。如果用匯編寫呢,gcc同樣會對彙編代碼進行優化,除非你用volatile關鍵字修飾。最保險的辦法是禁止優化。這當然只能對一部分你自己編寫的代碼。如果對所有的代碼都不優化,你會發現驅動程序根本無法裝載。這是因爲在編譯驅動程序時要用到gcc的一些擴展特性,而這些擴展特性必須在加了優化選項之後才能體現出來。

 


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