本文實作的是一個低級驅動開發案例。簡單通過編寫核心驅動子產品由開發闆(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
紅色是編譯核心驅動的時候要加的指令 黃色是指定交叉編譯鍊 藍色是跳轉到核心源碼裡去 綠色是回到目前目錄。
值得注意的是-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檢視裝置就已經被删除了。
小弟剛入行不久,若有問題請指正。