該文章主要目的爲了學習並掌握以下幾個方面:
- 熟悉並編寫字符設備驅動框架
- 學習應用層的測試程序編寫
- 內核空間與用戶空間的數據傳輸
- 驅動安裝與卸載、設備文件查看、應用程序運行等相關命令使用。
嵌入式系統框架簡單介紹
嵌入式系統由硬件、驅動、操作系統、應用,這幾部分層次構成。其中,驅動程序是硬件層與系統層之間的交互層,主要作用是操作底層硬件,實現硬件控制,而應用層位於操作系統層之上,應用層以操作系統爲中介,對驅動層的相關主要函數進行調用(如open(),read(),write()等函數),進而實現對硬件控制。
通常,驅動程序是編譯進操作系統內核中的,與內核融爲一體,區別在於,驅動程序可以以模塊化的形式動態編譯進內核中,相當於拼圖一樣,靈活可拆卸。操作系統可以理解爲龐大的函數庫,提供驅動和應用程序的調用。
應用程序的存放在文件系統中,文件系統可以理解爲一個目錄系統,由很多目錄組成,而這些應用程序就可以存放在某目錄下某文件中(相當於Windows電腦下各個盤符和目錄一樣,存在各個應用程序和文件),因此應用程序可以方便被查找和運行。
(以上均爲個人理解o( ̄︶ ̄)o,有不足之處請指教哦。下面正式進入實操)
實驗內容與源碼
編寫驅動程序和測試程序,實現:開發板任意2個按鍵按下,3個led燈反轉,並在shell終端顯示開發板運行的相關信息。
前期條件:開發板燒錄好u-boot,linux內核,文件系統
芯片:S3C440
開發板:韋老大的JZ2440
引腳說明 KEY1:GPP0,KEY2:GPG3 LED1:GPP4,LED2:GPP5,LED3:GPP6
本實驗源碼下載鏈接:”點擊此處”
字符驅動程序框架與編寫
驅動程序很多中,本文章以字符驅動程序爲入門。學完驅動程序後,就感覺還算蠻簡單,主要是熟悉它的整體結構形式,以及底層寄存器的控制。下面爲編寫流程:
1. 新建以驅動源碼文件,名爲:drv_keyled.c ,然後找好一個已經寫好的字符驅動源碼,作爲模板參考。
2. 將需要的頭文件寫進來,可以直接從模板中copy過來(這些頭文件就是系統內核的源碼庫,驅動程序會從中調用),頭文件如下:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/delay.h>
#include <asm/uaccess.h>
#include <asm/irq.h>
#include <asm/io.h>
#include <asm/arch/regs-gpio.h>
#include <asm/hardware.h>
3.定義需要用的寄存器名稱等
typedef unsigned long Uint32;
volatile unsigned long *gpiog_con=NULL; //寄存器
volatile unsigned long *gpiog_data=NULL; //寄存器
volatile unsigned long *gpiof_con=NULL; //寄存器
volatile unsigned long *gpiof_data=NULL;//寄存器
int major;//主設備號
//類和設備類,可以幫助用於自動創建設備文件
static struct class* drv_keyled_class; //定義類
static struct class_device* drv_keyled_class_device; //定義一類設備
4.綁定驅動有關的重要函數到內核中。關聯綁定後,使得應用層(用戶層)調用open(),read(),write()等函數時,系統能找到驅動對應的xxx_open(),xxx_read(),xxx_write()函數,代碼如下:
//關鍵函數綁定(結構體)
static struct file_operations drv_key_fops=
{
.owner = THIS_MODULE,
.write = drv_keyled_write,
.read = drv_keyled_read,
.open = drv_keyled_open,
};
//初始化、卸載函數綁定(宏)
module_init(drv_keyled_init);
module_exit(drv_keyled_exit);
5.關鍵驅動函數編寫——初始化和卸載函數(即drv_keyled_init,drv_keyled_exit)。
初始化函數主要實現功能如下幾點:(卸載函數與初始化函數相反,略述)
- 向內核註冊驅動,即將結構體綁定的關鍵函數告訴內核,同時從內核中獲取主設備號:major,主設備號用於應用程序對具體哪個驅動的識別。
- 創建類和類設備,運行後,系統能自動創建設備文件:/dev/keyled,相當於應用層上手動完成“mknod“命令操作。
- 物理地址映射到虛擬地址VA(ioremap函數),由於系統開啓了MMU,因此程序都是在虛擬地址運行,如果要完成指定寄存器的控制,就需要將該寄存器的物理地址進行映射,通過虛擬地址完成寄存器控制。
初始化和卸載函數調用方法:
- 當應用層運行命令“insmod”進行驅動裝載時,系統自動調用初始化函數。
- 當應用層運行命令“rmmod”進行驅動卸載時,系統自動調用卸載函數。
源碼如下:
/*
函數名:drv_keyled_init
功能:初始化模塊功能(insmod裝載驅動時調用)
*/
static int drv_keyled_init(void)
{
//註冊主設備號,由fops結構體告訴內核綁定的函數
major = register_chrdev(0, "drv_keyled", &drv_key_fops);
//創建類
drv_keyled_class = class_create(THIS_MODULE, "drv_keyled");
//創建類設備
drv_keyled_class_device = class_device_create(drv_keyled_class, NULL, MKDEV(major, 0), NULL, "keyled");// "/dev/keyled"
//虛擬地址VA映射
gpiof_con = (unsigned long*)ioremap(0x56000050, 12); //映射物理地址的起始地址與長度,返回虛擬地址
gpiof_data = gpiof_con+1;//地址在類型長度上加1(即+4地址)
gpiog_con = (unsigned long*)ioremap(0x56000060, 12); //映射物理地址的起始地址與長度,返回虛擬地址
gpiog_data = gpiog_con+1;
return 0;
}
/*
函數名:drv_keyled_exit
功能:卸載模塊功能(rmmod卸載驅動時調用)
*/
static void drv_keyled_exit(void)
{
unregister_chrdev(major, "drv_keyled");
class_device_unregister(drv_keyled_class_device);
class_destroy(drv_keyled_class);
iounmap(gpiof_con);
iounmap(gpiog_con);
}
6.關鍵驅動函數編寫—–xxx_open(),xxx_read()等函數(即drv_keyled_open,drv_keyled_open等)。
該部分的編寫是編寫驅動源碼的核心內容,實現了對寄存器配置與控制操作。當應用層調用open(),read(),系統就得調用這些對應的函數,因此,這些函數裏的內容以及要實現什麼樣的功能,由用戶自行發揮。該實驗將這些函數定義成如下功能:
- drv_keyled_open(): 實現按鍵和LED的引腳配置與初始化操作。
- drv_keyled_read(): 實現對按鍵電平狀態的讀取。
- drv_keyled_write(): 實現對LED燈的亮滅控制。 (注:實驗只用到了open,read,write三個常用函數,系統其實還提供了很多其他函數,可以見file_operations結構體裏的內容)
源碼如下:
/************驅動關鍵函數實現************/
/*
函數名:drv_keyled_open
功能:配置引腳功能
*/
int drv_keyled_open(struct inode *inode, struct file *file)
{
char key_dat=1;
printk("drv_keyled_open2\n");
//GPF4.5.6
*gpiof_con |= (1<<2*4) | (1<<2*5) | (1<<2*6) ; //led 輸出
*gpiof_data &= ((~(1<<4)) & (~(1<<5)) & (~(0<<6))); //初始化2個點亮
//GPG3,GPP0,GPP2
*gpiog_con |= (0x3<<2*11) ; //按鍵
return 0;
}
/*
函數名:drv_keyled_write
功能:控制led亮滅,實現反轉
*/
static ssize_t drv_keyled_write(struct file *file, const char __user *buf, size_t count, loff_t * ppos)
{
unsigned char key_data[2]={1,1};
Uint32 data=*gpiof_data;
//測試:讀取用戶傳入的按鍵值
copy_from_user(key_data, buf, count);
printk("kernel:the key is:%d , %d\n",key_data[0] ,key_data[1] ); //檢驗用戶空間傳輸的按鍵值
//實現反轉led電平(反轉指定位電平同時其他的位不影響)
*gpiof_data |= (1<<4) | (1<<5) |(1<<6);
*gpiof_data &= ~(data & ((1<<4) | (1<<5) |(1<<6))); //
return 0;
}
/*
函數名:drv_keyled_read
功能:讀key電平
*/
static ssize_t drv_keyled_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
unsigned char key_data[2]={1,1};
unsigned char keybuf=0;
if(size != sizeof(key_data))//用戶空間(應用程序)要讀取的字節與內核空間存的字節數一致
{
printk("read size has err\n");
return -EINVAL; //返回錯誤
}
//把讀取的按鍵值發給用戶空間(應用端)
key_data[0] = (*gpiof_data & (1<<0)) ? 1 : 0;
key_data[1] = (*gpiog_data & (1<<3)) ? 1 : 0;
if( key_data[0] == 0)
{
printk("key1 press \n");
while(!keybuf)//按下彈出
{
keybuf= (*gpiof_data & (1<<0)) ? 1 : 0;
}
}
if( key_data[1] == 0)
{
printk("key2 press \n");
while(!keybuf)//按下彈出
{
keybuf= (*gpiog_data & (1<<3)) ? 1 : 0;
}
}
copy_to_user(buf, key_data, sizeof(key_data));//copy_to_user(用戶空間,內核空間,字節數)
return sizeof(key_data);
}
用戶空間與內核空間進行消息傳遞的關鍵函數:copy_from_user()和copy_to_user()。
copy_from_user():通常在xxx_write()函數內調用,實現用戶空間的數據到內核空間的傳遞,傳遞的數據存在buf的形參中。copy_from_user()可以將buf中的數據讀出來。
copy_to_user(): 通常在xxx_read()函數內調用,實現用戶空間讀取內核空間的數據,copy_to_user()是將內核裏的數據存於buf中,提供用戶層read()的讀取。
7.聲明驅動(模塊的許可證聲明),聲明後,某塊才能被正常安裝到系統內核。固定格式如下:
MODULE_LICENSE(“GPL”);
8.編寫驅動的makefile文件
同樣地,找一寫好的驅動makefile模板,修改主機上存儲開發板的linux源碼目錄(第一行),並更改obj-m選項(最後一行),代碼如下:
KERN_DIR = /work/mysystem/linux-2.6.22.6
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += drv_keyled.o
obj-m的主要功能:將驅動代碼編譯成模塊(.ko文件),與obj-m對立的是:obj-y,是將代碼直接編譯到內核中。
應用測試程序編寫
1.新建以應用程序文件,名爲:keyled_test.c ,同樣找一應用程序模板,把必要頭文件包含進來,如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
2.編寫內容,根據驅動提供的幾個關鍵函數功能,用戶層需要調取這些功能函數,完成最終實現。實驗目的是按下按鍵,反轉LED燈,因此需要調用read函數,讀出內核傳遞過來的數據(按鍵值),根據按鍵值是否按下,調用write函數,實現對led控制。
程序開始需要通過設備文件,打開對應的驅動,然後返回一句柄fd,程序可以通過fd完成設備的讀寫操作。
形參:int argc, char **argv的作用:
當系統啓動應用程序時,可以在啓動程序文件的後面加上要傳入的參數,這些參數就會傳進int argc, char **argv的形參中,其中:
(注:文件名的本身也是個參數,因此參數至少1個,啓動程序文件方式見後面操作)
- argc:顯示的是傳入參數的個數
- 指針argv[i]:顯示第i個參數的字符串內容
源碼如下:
int main(int argc, char **argv)
{
int fd;
unsigned char key_data[2]={1,1};
fd = open("/dev/keyled", O_RDWR); //應用端通過識別設備文件來識別驅動
if (fd < 0)
{
printf("can't open!\n");
}
if(argc == 2)//argc[0]爲文件名本身
{
printf("argv=%s\n",argv[1]);
}
else if(argc == 3)
{
printf("argv=%s , %s\n",argv[1],argv[2]);
}
printf("hello word fd=%d\n",fd);
while(1)
{
read(fd,key_data,sizeof(key_data));
if(key_data[0] == 0 || key_data[1] == 0)
{
printf("user:k1=%d k2=%d\n",key_data[0],key_data[1]);//檢驗內核到用戶空間的數據傳輸
write(fd,key_data,sizeof(key_data));
}
}
return 0;
}
3.測試程序的makefile
採用arm-linux-gcc編譯器進行編譯,生成可執行程序文件keyled_test ,內容如下:
CROSS.=arm-linux-
keyled_test : keyled_test.c
$(CROSS)gcc -o $@ $<
clean:
rm keyled_test
裝載驅動程序與運行測試程序
make編譯驅動模塊以及測試程序後,分別生成.ko文件和可執行文件,這兩個文件需要存放在根文件系統上,分爲完成驅動裝載和運行測試程序。
開發板掛載根文件系統,通常採用NFS網絡方式直接掛載到PC主機上,簡單方便,並大大減少程序調試時間,但是筆者由於路由器問題,無法建立主機和開發板的網絡連接,只能採用直接下載根文件系統到開發板上運行勒,很麻煩,每一次調試都得重新下一次,簡直崩潰o(╥﹏╥)o,下面是驅動裝載與測試程序運行具體步驟:
1.在製作好的根文件系統上新建以任意名稱的文件夾:/moduel,將.ko文件和應用程序執行文件存在該目錄中。
2.如果採用NFS掛載方式,可以省略此步。該步主要將文件系統下載在開發板中,因此需要在主機上把製作好的根文件系統轉成映像文件(採用mkyaffs2image工具),然後由dnw工具,通過usb方式 下載到開發板中。(篇幅有限,具體過程略述啦^_^,能用NFS最好咯)。
3.啓動開發板(uboot,kernel,文件系統都下載並裝載好),操作系統啓動後,在終端上進行驅動裝載(關鍵命令:insmod),如下:
insmod爲驅動裝載,lsmod爲查看裝載情況
4.運行測試程序,如下圖所示,圖中可以觀察到如下幾個結論:
- 運行的程序名稱後可加傳入的參數,如圖所示的參數爲:“iu”和“tr”。
- 按下按鍵時,可以終端打印出相關信息,這些信息都是編寫程序時實現的。
- 圖中顯示的kernel:…. 和user:…. 分別是內核空間與用戶空間之間傳遞的數據信息,可以看出它們傳遞的數據是按鍵值,並且結果正確的。
4.開發板測試結果,激動人心的當然是開發板的實際運行效果啦,隨着兩個按鍵的任意按下,3個LED等就會翻轉,看看效果(^▽^):
其它工作:驅動卸載與設備、進程查看等
1.查看設備與設備文件。設備顯示在/proc/devices虛擬文件系統內,設備文件可以在/dev目錄下查看。
可以看出主設備號爲252,次設備號爲0
2.查看當前測試程序的進程和cpu佔有率,此時啓動的應用程序需要在後臺運行(末位加上&字符),才能到前端查看進程和cpu佔有率。
可以看出:進程號爲791,cpu佔有率爲98%。
1.驅動卸載(從內核中卸載掉該驅動)與關閉進程(退出測試程序運行,通過進程號關閉)。