linux驅動由淺入深系列:驅動程式的基本結構概覽之一(第一個驅動程式)
本系列導航:
提到linux驅動程式,首先應該知道它是linux的核心模組。那麼想要編寫驅動程式,就要首先認識一下linux的核心模組機制。Linux核心模組是使得複雜而龐大的linux核心條理清晰、可裁剪、高相容性的重要特性。
Linux核心模組的特點:
1, 模組本身不被編譯進核心映象,能夠控制核心的大小。
2, 模組可以在需要的時候中被動態載入,一旦載入完成就和核心其它部分完全一樣。
下面便是linux核心模組的helloworld程式,結構十分固定。編譯完成後生成hello.ko,通過insmod hello.ko進行載入,載入時輸出” hello module has been mount!”,使用rmmod hello進行解除安裝,解除安裝時輸出” hellomodule has been remove!”。
#include <linux/init.h> #include <linux/module.h> MODULE_LICENSE("Dual BSD/GPL"); MODULE_AUTHOR("Radia"); static int hello_init() { printk(KERN_EMERG "hello module has been mount!\n"); return 0; } static void hello_exit() { printk(KERN_EMERG "hello module has been remove!\n"); } module_init(hello_init); module_exit(hello_exit);
在Linux下可以通過兩種方式載入驅動程式:靜態載入和動態載入。
靜態載入就是把驅動程式直接編譯進核心,系統啟動後可以直接呼叫。靜態載入的缺點是除錯起來比較麻煩,每次修改一個地方都要重新編譯和下載核心,效率較低。若採用靜態載入的驅動較多,會導致核心容量很大,浪費儲存空間。
動態載入利用了Linux的module特性,可以在系統啟動後用insmod命令新增模組(.ko),在不需要的時候用rmmod命令解除安裝模組,採用這種動態載入的方式便於驅動程式的除錯,同時可以針對產品的功能需求,進行核心的裁剪,將不需要的驅動去除,大大減小了核心的儲存容量。
在臺式機上,一般採用動態載入的方式;在嵌入式產品裡,可以先採用動態載入的方式進行除錯,除錯成功後再編譯進核心。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/miscdevice.h>
#include <linux/fs.h>
#define DRIVER_NAME "hello"
#define DEVICE_NAME "hello"
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Radia");
static int hello_open(struct inode *inode, struct file *file){
printk(KERN_EMERG "hello open\n");
return 0;
}
static int hello_release(struct inode *inode, struct file *file){
printk(KERN_EMERG "hello release\n");
return 0;
}
static long hello_ioctl(struct file *file, unsigned int cmd, unsigned long arg){
printk("cmd is %d, arg is %d\n", cmd, arg);
return 0;
}
static struct file_operations hello_fops = {
.owner = THIS_MODULE,
.open = hello_open,
.release = hello_release,
.unlocked_ioctl = hello_ioctl,
};
static struct miscdevice hello_dev = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEVICE_NAME,
.fops = &hello_fops,
};
static int hello_probe(struct platform_device *pdv)
{
printk(KERN_EMERG "hello probe\n");
misc_register(&hello_dev);
return 0;
}
static int hello_remove(struct platform_device *pdv)
{
printk(KERN_EMERG "hello remove\n");
misc_deregister(&hello_dev);
return 0;
}
static void hello_shutdown(struct platform_device *pdv)
{
}
static int hello_suspend(struct platform_device *pdv, pm_message_t pmt)
{
return 0;
}
static int hello_resume(struct platform_device *pdv)
{
return 0;
}
static struct platform_driver hello_driver = {
.probe = hello_probe,
.remove = hello_remove,
.shutdown = hello_shutdown,
.suspend = hello_suspend,
.resume = hello_resume,
.driver = {
.name = DRIVER_NAME,
.owner = THIS_MODULE,
}
};
static int hello_init(void)
{
int driver_state;
printk(KERN_EMERG "hello module has been mount!\n");
driver_state = platform_driver_register(&hello_driver);
printk(KERN_EMERG "platform_driver_register driver_state is %d\n", driver_state);
platform_device_register_simple(DRIVER_NAME, -1, NULL, 0);
printk(KERN_EMERG "platform_device_register_simple end\n");
return 0;
}
static void hello_exit(void)
{
printk(KERN_EMERG "hello module has been remove!\n");
platform_driver_unregister(&hello_driver);
}
module_init(hello_init);
module_exit(hello_exit);
程式分析:
1, 可以看出這個驅動模版也是基於我們上一個核心模組模版的。
2, 驅動模組一般在開機時逐次載入,載入成功後就會呼叫hello_init函式使用platform_driver_register向核心中註冊一個驅動。搞定,就是如此簡單!
3, 相關函式
字元裝置申請裝置號函式register_chrdev、register_chrdev_region
register_chrdev註冊指定主次裝置號的裝置,手動在/dev下建立該裝置的裝置節點
register_chrdev_region 註冊一個指定主次裝置號的裝置,後利用class類在/dev/目錄下自動建立一個該裝置的節點。
字元設備註冊函式cdev_add。
本例項為簡潔起見使用了misc_register函式,該函式已包含了上面兩個步驟。
字元裝置驅動註冊函式platform_driver_register。
、
4, probe函式的呼叫
當裝置和驅動的名字匹配,BUS就會呼叫驅動的probe函式。這分兩種情況:
a 先註冊裝置,後註冊驅動
此種方式最為常見,大多數裝置先於驅動註冊到核心中。
在核心原始碼中,platform 裝置的初始化(註冊)用arch_initcall()呼叫,它的initcall 的level為3;而驅動的註冊用module_init()呼叫,即device_initcall(),它的initcall 的level為6。kernel 初始化時([email protected]/main.c),按照核心連結檔案中(arm系統:kernel/arch/arm/vmlinux.lds)的__initcall_start段的序列依次執行,這樣level小的初始化函式先於level大的初始化函式被呼叫。
所以platform裝置先被註冊,驅動載入時會呼叫驅動程式中的probe(),掃描系統中已註冊的裝置,找到匹配裝置後將驅動和裝置繫結。
當設備註冊的時候,由於驅動尚未註冊,所以執行"__device_attach"時直接返回,未執行"driver_probe_device"進行裝置和驅動匹配;後來驅動註冊的時候,執行"__driver_attach"的時候裝置已經註冊,所以進入"driver_probe_device",接著進入"driver_probe_device",然後"really_probe"完成裝置和驅動的繫結。
b 先註冊驅動,再註冊裝置
當驅動註冊的時候,由於裝置尚未註冊,所以執行"__driver_attach"時直接返回,未執行"driver_probe_device"進行驅動和裝置匹配;後來設備註冊的時候,執行"__device_attach"的時候驅動已經註冊,所以進入"driver_probe_device",接著進入"driver_probe_device",然後"really_probe"完成驅動和裝置的繫結。
本示例使用了最為簡潔明瞭的展示方法,在核心模組的載入函式hello_init中依次註冊了裝置與驅動。可知註冊後者時將會觸發hello_probe函式的呼叫,其中呼叫misc_register使用了主裝置號10,動態分配了次裝置號等,完成設備註冊。
5, 驗證
編譯驅動進核心,重新啟動在開機過程中看到hello模組成功掛載,probe函式呼叫。在/dev目錄下生成了hello裝置節點,主裝置號10,次裝置號56。
編寫應用層程式進行測試:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
int main(int argc, char *argv[])
{
int fd;
printf("enter driver test %s %s \r\n", argv[1], argv[2]);
char *hello = "/dev/hello";
if((fd = open(hello, O_RDWR | O_NOCTTY | O_NDELAY)) < 0)
{
printf("open %s failed\n", hello);
}
else
{
printf("%s fd is %d \r\n", hello, fd);
ioctl(fd, atoi(argv[1]), atoi(argv[2]));
}
close(fd);
return 1;
}
編譯後執行結果如下:
Shell 列印
Kernel log