天天看點

linux低級字元驅動開發例子詳細分析驅動檔案編寫應用檔案編寫說說Makefileinsmod和mknod

本文實作的是一個低級驅動開發案例。簡單通過編寫核心驅動子產品由開發闆(sp6818)的按鍵key控制闆子led燈

大緻流程如下

編寫驅動檔案keyled.c

編寫應用檔案app.c

編寫makefile

make制作出.ko檔案

将.ko移至開發闆中

insmod 将裝置加入驅動序列中

mknod /dev/keyled c 87 0 将主裝置号與節點相關聯

驅動檔案編寫

//keyled.c
#include <linux/kernel.h>		// printk 
#include <linux/init.h>			// __init/__exit
#include <linux/module.h>   	// module_init 

#include <linux/fs.h>
#include <asm/uaccess.h>

#include <asm/io.h>				// arch/arm/include/asm/io.h
#include <linux/ioport.h>

#include <linux/interrupt.h>
#include <linux/wait.h>			//休眠喚醒機制

#include <linux/kthread.h>

wait_queue_head_t myqueue;
int flag= 1;


struct resource *res = NULL;

unsigned long *ptr = NULL;
unsigned long *ptr_b = NULL;

int keyled_open(struct inode *node, struct file *file)
{
	printk(KERN_INFO "keyled open\n");//KERN_INFO為輸出優先級


	//led init 
	*(ptr+1) |= (0x1<<7);
	*(ptr) |= (0x1<<7);

	//key_init
	*(ptr_b + 1) &= ~(0x1<<30);


	return 0;
}

int keyled_close(struct inode *node, struct file *file)
{
	//keyled_init();
	printk(KERN_INFO "keyled close\n");

	return 0;
}

int kbuffer;

ssize_t keyled_write(struct file *file, const char __user *ubuf, size_t size, loff_t *offset)
{
	printk(KERN_INFO "keyled write\n");

	
	//memcpy(&kbuffer, ubuf, size);	

	//copy_to_user();
	copy_from_user(&kbuffer, ubuf, size);//将使用者空間資料拷貝給核心空間
	printk(KERN_INFO "kbuffer: %d\n", kbuffer);

	if(kbuffer == 0)
	{
		printk(KERN_INFO "led on\n");
		*(ptr) &= ~(0x1<<7);
	}else 
	{
		printk(KERN_INFO "led off\n");
		*(ptr) |= (0x1<<7);
	}


	return 0;
}

int key_status;
ssize_t keyled_read(struct file *file, char __user *ubuf, size_t size, loff_t *offset)
{
	//printk(KERN_INFO "keyled read...\n");

	//線程開始休眠
	//wait_event(myqueue, flag != 1);
	

	key_status = (*(ptr_b + 6) & (0x1<<30)) >> 30;
	//printk(KERN_INFO "key state: %d\n", key_status);

	copy_to_user(ubuf, &key_status, size);//将核心空間資料拷貝給使用者空間


	return 0;
}


struct file_operations keyled_fops = {
	.open 		= keyled_open,
	.release 	= keyled_close,
	.write		= keyled_write,
	.read 		= keyled_read, 
};

static int __init led_drv_init(void)//_init的作用是在insmod加載核心時調用該函數,之後忽略掉這個函數,并釋放記憶體。
{
	printk(KERN_INFO "-----------------led driver init .....................\n");
	int ret;

	//注冊字元裝置驅動
	ret = register_chrdev(87, "key-led", &keyled_fops);//将驅動與主裝置号相關聯
	if(ret < 0)
	{
		printk(KERN_INFO "register_chrdev failed\n");
		return -1;
	}

	res = request_mem_region(0xc001c000, 0x1000,"myled-iomap");//request_mem_region函數并沒有做實際性的映射工作,隻是告訴核心要使用一塊記憶體位址,聲明占有,也友善核心管理這些資源。
	if(res == NULL)
	{
		printk(KERN_INFO "request_mem_region failed\n");
		return -1;
	}	

	printk(KERN_INFO "request_mem_region success\n");

	ptr = (unsigned long *)ioremap(0xc001c000, 0x1000);//ioremap主要是檢查傳入位址的合法性,建立頁表(包括通路權限),完成實體位址到虛拟位址的轉換。
	if(ptr == NULL)
	{
		printk(KERN_INFO "ioremap failed\n");
		return -1;
	}
	printk(KERN_INFO "ioremap success\n");

	
	ptr_b = (unsigned long *)ioremap(0xc001b000, 0x1000);//隻讀的位址似乎不需要request_mem_region
	if(ptr_b == NULL)
	{
		printk(KERN_INFO "key ioremap failed\n");
		return -1;
	}
	printk(KERN_INFO "key ioremap success\n");

	//初始隊列頭結構
	init_waitqueue_head(&myqueue);


	return 0;
}

static void __exit led_drv_exit(void)//remmod時調用
{
	printk(KERN_INFO "-----------------led driver exit .....................\n");


	iounmap(ptr);//釋放映射
	iounmap(ptr_b);//釋放映射
	release_mem_region(0xc001c000, 0x1000);//釋放位址聲明

	unregister_chrdev(87, "key-led");//登出

}
	

module_init(led_drv_init);
module_exit(led_drv_exit);


MODULE_LICENSE("GPL");//版本
MODULE_AUTHOR("zbzyjya ");//作者
MODULE_DESCRIPTION("8250 serial probe module for Accent Async cards");
           

本文會按照驅動程式内部的啟動順序與調用順序來講解

先關注這兩個函數(_init和_exit)

static int __init led_drv_init(void)
static void __exit led_drv_exit(void)
           

先分别說說修飾符_init和_exit的作用:

_init的作用是調用完該函數之後,就忽略掉這個函數,在初始化完成後丢棄該函數并收回所占記憶體,

__exit修飾詞标記函數隻在子產品解除安裝時使用。

_init和_exit的作用

通過

module_init(led_drv_init);
module_exit(led_drv_exit);
           

在核心加載(insmod)和解除安裝(remmod)時會調用對應的函數。

這種函數存在的意義是,将一部分不常用的代碼或者隻調用一次的代碼,動态編譯進核心當中,使用完之後可以立即釋放該函數的記憶體,使得記憶體利用更加合理。

關注一下led_drv_init内的幾個api

//注冊字元裝置驅動
	ret = register_chrdev(87, "key-led", &keyled_fops);//将驅動與主裝置号相關聯
           

這個函數将驅動與裝置号關聯起來,第三個參數傳入的是一個file_operations的結構體,這個結構體十分重要,後文再講解。

res = request_mem_region(0xc001c000, 0x1000,"myled-iomap");
//request_mem_region函數并沒有做實際性的映射工作,隻是告訴核心要使用一塊記憶體位址,聲明占有,也友善核心管理這些資源。
           

request_mem_region函數并沒有做實際性的映射工作,隻是告訴核心要使用一塊記憶體位址,聲明占有,也友善核心管理這些資源。第一個參數為led燈的實體位址,第二個參數為讀取大小。

ioremap主要是檢查傳入位址的合法性,建立頁表(包括通路權限),完成實體位址到虛拟位址的轉換。要操作led燈的話必須要有這兩步聲明與映射。

在調用insmod keyled.ko時驅動會執行led_drv_init,完成上述各種初始化與注冊操作。與其相應的是調用remmod的時候會執行 led_drv_exit,執行該函數内部的驅動登出與映射釋放等操作。

應用檔案編寫

再來看看app.c

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

#define LED_ON 	0x0 
#define LED_OFF	0x1 

int main()
{
	int fd; 
	int wr_data;
	int rd_state;

	
	fd  = open("/dev/keyled", O_RDWR);
	if(fd < 0)
	{
		printf("open failed\n");
		return -1;
	}
	printf("open keyled success\n");


	while(1)
	{
		read(fd, &rd_state, 4);
		if(rd_state == 0)
		{
			usleep(30000);
			read(fd, &rd_state, 4);
			if(rd_state == 0)
			{
				//按鍵彈開
				do {
					read(fd, &rd_state, 4);
				}while(rd_state == 0);

				//
				printf("led change state....\n");
			}
		}
	}
	close(fd);

	return 0;
}

           

這段代碼為應用層代碼,先簡單說說應用層是如何通過API系統調用的,在應用層中,open了一個驅動節點(/dev/keyled)之後,應用層對這個檔案描述符的open、read、write等API操作,最後都會找到這個驅動内部file_operations結構體(參考上面keyled.c)中的.open、.read、.write(均為函數指針)所指向的函數來執行。

struct file_operations keyled_fops = {
	.open 		= keyled_open,
	.release 	= keyled_close,
	.write		= keyled_write,
	.read 		= keyled_read, 
};
           

如當應用層執行fd = open("/dev/keyled", O_RDWR);時,驅動就會找到keyled_open來執行。file_operations結構體是驅動開發中很重要的結構體,注意在字元驅動注冊時要作為第三個參數傳入。

那麼同理應用層調用write和read時對應會調用

ssize_t keyled_write(struct file *file, const char __user *ubuf, size_t size, loff_t *offset)
{
	printk(KERN_INFO "keyled write\n");

	
	//memcpy(&kbuffer, ubuf, size);	

	//copy_to_user();
	copy_from_user(&kbuffer, ubuf, size);//将使用者空間資料拷貝給核心空間
	printk(KERN_INFO "kbuffer: %d\n", kbuffer);

	if(kbuffer == 0)
	{
		printk(KERN_INFO "led on\n");
		*(ptr) &= ~(0x1<<7);
	}else 
	{
		printk(KERN_INFO "led off\n");
		*(ptr) |= (0x1<<7);
	}


	return 0;
}

int key_status;
ssize_t keyled_read(struct file *file, char __user *ubuf, size_t size, loff_t *offset)
{
	//printk(KERN_INFO "keyled read...\n");

	//線程開始休眠
	//wait_event(myqueue, flag != 1);
	

	key_status = (*(ptr_b + 6) & (0x1<<30)) >> 30;
	//printk(KERN_INFO "key state: %d\n", key_status);

	copy_to_user(ubuf, &key_status, size);//将核心空間資料拷貝給使用者空間


	return 0;
}
           

app.c中的write是如何把内容寫進fd裡的呢?

該函數的第二個傳參ubuf便是使用者寫入的内容,通過

将使用者空間資料拷貝給核心空間,把ubuf裡的内容傳入kbuffer中。

keyled_read同理,用copy_to_user将核心空間資料拷貝給使用者空間,完成讀寫。有一道面試題問的是,核心空間和使用者空間如何通訊,其答案之一就是通過copy_from_user和copy_to_user來實作。

有了這個功能,就能通過讀取開關狀态來判斷是否點亮led燈。

說說Makefile

如何用makefile制作出對應的keyled.ko和app呢?

這裡給出參考與部落客自己的了解

obj-m    += keyled.o


KERNEL_DIR = /opt/wkspace/build_plat/kernel-3.4.39/

all: 
	make modules CROSS_COMPILE=/usr/local/arm/arm-eabi-4.8/bin/arm-eabi- -C $(KERNEL_DIR) M=`pwd`	
	arm-linux-gcc app.c -o app 
	cp keyled.ko app /nfs 
clean:
	make modules clean M=`pwd` -C  $(KERNEL_DIR) CROSS_COMPILE=/usr/local/arm/arm-eabi-4.8/bin/arm-eabi-
           

把keyled.c編譯成keyled.ko檔案

驅動開發makefile

linux低級字元驅動開發例子詳細分析驅動檔案編寫應用檔案編寫說說Makefileinsmod和mknod

紅色是編譯核心驅動的時候要加的指令 黃色是指定交叉編譯鍊 藍色是跳轉到核心源碼裡去 綠色是回到目前目錄。

值得注意的是-C是跳轉到指定路徑當中去 M是回來。由于驅動開發需要依賴核心一些庫,KERNEL_DIR指定的是核心源碼所在的路徑。

制作出.ko和app之後,就能把他們拷到開發闆上。

insmod和mknod

拷到開發闆之後,在.ko目前檔案夾下,指令行輸入

insmod keyled.ko
           

實作将裝置加入驅動序列中。通過cat /proc/devices或者lsmod可以檢視驅動是否加入成功。除了insmod之外還可以用modprobe keyled.ko來将裝置加入,modprode的功能更多,有興趣的讀者可以自己了解。

裝置加入驅動序列之後,還需要

mknod /dev/keyled c 87 0    将主裝置号與節點相關聯 87是主裝置号 0是副裝置号
           

通過mknod将主裝置号與節點相關聯。可以這樣了解,insmod之後驅動中隻有一個裝置号和裝置相關聯,但應用層裡找不到這樣一個裝置。mknod的作用是給這個裝置生成一個/dev檔案夾下的節點,類似于給應用層代碼插一個指路的路牌,這樣就把節點-裝置-裝置号相關聯了起來,能讓應用層代碼順利找到該裝置。

完成這一步之後就能./app執行可執行檔案了。執行完之後可以remmod掉.ko,再次lsmod檢視裝置就已經被删除了。

小弟剛入行不久,若有問題請指正。

繼續閱讀