上一個博客i.MX283開發板第一個Linux驅動講的是最簡單的LED驅動的編寫,但是其中還有一些不足。
首先是在利用insmod加載驅動時,需要用指令mknod /dev/imx283_led c 200 0手動創建設備節點,否則在/dev下是不會有我們的設備的,應用程序中open("/dev/imx283_led",O_RDWR)必然會失敗。
其次是利用register_chrdev函數註冊設備會造成設備號的浪費,這個是早期2.4版本內核的註冊方法,在Linux2.6及以後的版本都不是很推薦這種方式。
下面就針對上一個LED驅動作下改進。
-
udev、mdev機制
Linux有udev、mdev的機制,而我們的ARM開發板上移植的busybox有mdev機制,然後mdev機制會通過class類來找到相應類的驅動設備來自動創建設備節點 (前提需要有mdev)
udev 是一個用戶程序,在 Linux 下通過 udev 來實現設備文件的創建與刪除,udev 可以檢測系統中硬件設備狀態,可以根據系統中硬件設備狀態來創建或者刪除設備文件。比如使用modprobe 命令成功加載驅動模塊以後就自動在/dev 目錄下創建對應的設備節點文件,使用rmmod 命令卸載驅動模塊以後就刪除掉/dev 目錄下的設備節點文件。
1.首先要創建一個 cdev結構體變量、class 類和device設備。
static struct cdev LED_cdev;//定義一個cdev結構體
static struct class *led_class;//創建一個LED類
static struct device *led_device;//創建一個LED設備 該設備是需要掛在LED類下面的
static int major;//主設備號
static dev_t dev_id;//設備號 由主設備號和次設備號組合而成
注意:dev_id是爲了動態申請設備號創建的變量,申請成功後可以利用MAJOR(dev_id)和MINOR(dev_id)取出主設備號和次設備號。
2.改進註冊與註銷字符設備函數
register_chrdev()的弊端在於它僅僅由一個主設備號就確定了一個設備驅動,因爲register_chrdev()的入口參數只有主設備號major和fops結構體,Linux內核最多支持255個字符設備,假設我有255個不同的字符設備需要控制,那麼就需要255個主設備號,一下子用光了所有的設備號,這是很不合理的!
int __register_chrdev(unsigned int major, //主設備號
unsigned int baseminor,
unsigned int count,
const char *name,//設備名稱
const struct file_operations *fops)
因此就需要一種合理的方式分配和釋放設備號,Linux 2.6及以後的內核就提供了這種方式,下面這個函數就是用來申請設備號:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
/*
dev:需要申請的設備號
baseminor:次設備號起始位置
count:需要申請次設備號的個數
name:設備名稱
*/
這樣就保證了每個設備只對應一個主設備號和一個次設備號 。同樣的,有申請設備號必然有註銷設備號,註銷設備號函數如下:
void unregister_chrdev_region(dev_t from, unsigned count)
/*
from:需要註銷設備號的起始位置
count:個數
*/
申請完設備號,就需要向內核註冊字符設備,這裏需要首先需要使用cdev_init()初始化一個cdev結構體。
struct cdev {
struct kobject kobj;
struct module *owner;//一般爲THIS_MODULE
const struct file_operations *ops;
struct list_head list;
dev_t dev;//設備號
unsigned int count;//設備號數量
};
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
/*
cdev:初始化一個cdev結構體
fops:將設備驅動file_operations結構體賦給cdev的ops變量
*/
接下來需要向剛剛初始化好的cdev結構體中添加字符設備:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
/*
p:初始化好的cdev結構體
dev:設備號
count:設備數量
*/
-
改進後註冊設備函數:
static int __init led_init(void)
{
/*1.申請或者指定主設備號 此處由內核動態分配1個設備號*/
alloc_chrdev_region(&dev_id, 0, 1, DEVICE_NAME);
/*2.初始化 LED_DEV結構體*/
cdev_init(&LED_cdev, &led_fops);
/*3.向LED_cdev添加1個設備*/
cdev_add(&LED_cdev,dev_id, 1);
/*4.創建一個LED類*/
led_class = class_create(THIS_MODULE,"led_class");
/*5.在LED類下面創建一個LED設備 然後mdev通過這個自動創建/dev/"DEVICE_NAME"*/
led_device = device_create(led_class,NULL,dev_id,NULL,DEVICE_NAME);
printk("module init ok\n");
return 0;
}
class_create是類創建函數,需要引用頭文件#include <linux/device.h>
#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
/*
owner:THIS_MODULE
name:類名字,可以隨意
*/
device_create用於在指定類下面創建一個設備
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
{
va_list vargs;
struct device *dev;
va_start(vargs, fmt);
dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);
va_end(vargs);
return dev;
}
/*
class:指定類,這裏指定上一步創建的類
parent:父設備,一般爲NULL
devt:設備號,可以通過MKDEV構建出來
drvdata:是設備可能會使用的一些數據,一般爲 NULL
fmt:設備名字,如果設置 fmt=xxx 的話,就會生成/dev/xxx這個設備文件。返回值就是創建好的設備。
MKDEV是個宏定義,作用是根據已有的主設備號和次設備號構建一個dev_t類型的設備號。
*/
-
改進後註銷設備函數:
static void __exit led_exit(void)
{
/*1.刪除LED_cdev結構體*/
cdev_del(&LED_cdev);
/*2.註銷設備 cat /proc/devices下不可見*/
unregister_chrdev_region(dev_id,1);
/*3.刪除LED設備*/
device_destroy(led_class, dev_id);
/*4.刪除LED類*/
class_destroy(led_class);
printk("module exit ok\n");
}
device_destroy函數是從指定類下面刪除一個設備
void device_destroy(struct class *class, dev_t devt)
{
struct device *dev;
dev = class_find_device(class, NULL, &devt, __match_devt);
if (dev) {
put_device(dev);
device_unregister(dev);
}
}
//class:指定類 devt:設備號
void class_destroy(struct class *cls)
{
if ((cls == NULL) || (IS_ERR(cls)))
return;
class_unregister(cls);
}
//cls: 需要刪除的class
下面給出完整的驅動函數:
/*
GPIO Driver driver for EasyARM-iMX283
*/
#include <linux/module.h>//模塊加載卸載函數
#include <linux/kernel.h>//內核頭文件
#include <linux/types.h>//數據類型定義
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/fs.h>//file_operations結構體
#include <linux/device.h>//class_create等函數
#include <linux/ioctl.h>
#include <linux/delay.h>
#include <linux/bcd.h>
#include <linux/capability.h>
#include <linux/rtc.h>
#include <linux/cdev.h>
#include <linux/gpio.h>//gpio_request gpio_free函數
#include <../arch/arm/mach-mx28/mx28_pins.h>
#define DEVICE_NAME "imx283_led"//驅動名稱
#define LED_GPIO MXS_PIN_TO_GPIO(PINID_LCD_D23) //for 283 287A/B
static int led_open(struct inode *inode ,struct file *flip)
{
int ret = -1;
gpio_free(LED_GPIO);
ret = gpio_request(LED_GPIO, "LED1");
printk("gpio_request = %d\r\n",ret);
return 0;
}
static int led_release(struct inode *inode ,struct file *flip)
{
gpio_free(LED_GPIO);
return 0;
}
static int led_write(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
int ret = -1;
unsigned char databuf[1];
ret = copy_from_user(databuf,buf,1);
if(ret < 0 )
{
printk("kernel write error \n");
return ret;
}
gpio_direction_output(LED_GPIO, databuf[0]);
return ret;
}
static ssize_t led_read(struct file *filp, const char __user *buf, size_t count,
loff_t *f_pos)
{
return 0;
}
static struct file_operations led_fops={
.owner = THIS_MODULE,
.open = led_open,
.write = led_write,
.read = led_read,
.release = led_release,
};
static struct cdev LED_cdev;//定義一個cdev結構體
static struct class *led_class;//創建一個LED類
static struct device *led_device;//創建一個LED設備 該設備是需要掛在LED類下面的
static int major;//主設備號
static dev_t dev_id;
static int __init led_init(void)
{
/*1.申請或者指定主設備號 此處由內核動態分配設備號*/
alloc_chrdev_region(&dev_id, 0, 1, DEVICE_NAME);
/*2.初始化 LED_DEV結構體*/
cdev_init(&LED_cdev, &led_fops);
/*3.向LED_cdev添加設備*/
cdev_add(&LED_cdev,dev_id, 1);
//major = register_chrdev(0,DEVICE_NAME,&led_fops);
//if(major < 0)
//{
// printk("register_chrdev error %d\n",major);
// return major;
//}
//4.創建一個LED類
led_class = class_create(THIS_MODULE,"led_class");
//5.在LED類下面創建一個LED設備 然後mdev通過這個自動創建/dev/"DEVICE_NAME"
led_device = device_create(led_class,NULL,dev_id,NULL,DEVICE_NAME);
printk("module init ok\n");
return 0;
}
static void __exit led_exit(void)
{
/*1.刪除LED_cdev結構體*/
cdev_del(&LED_cdev);
/*2.註銷設備 cat /proc/devices下不可見*/
//unregister_chrdev(major, DEVICE_NAME);
unregister_chrdev_region(dev_id,1);
/*3.刪除LED設備*/
device_destroy(led_class, dev_id);
/*4.刪除LED類*/
class_destroy(led_class);
printk("module exit ok\n");
}
module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xzx2020");
3.測試
將上述驅動文件,編譯生成.ko文件在開發板上測試。
首先看下/dev下有麼有LED設備節點
此時沒有任何LED設備節點,然後再加載驅動。
驅動加載成功,我們再到/dev下看看
已經自動生成了設備設備節點,主設備號250,次設備號0.
最後,我們再卸載掉這個驅動,並再次查看/dev和/proc/devices
都沒有“imx283_led”這個設備存在,說明已經被刪除掉了。
4.總結
1.實際上新的設備註冊方法就是將以前的拆分罷了。
register_chrdev()原型如下:
2.如果有多個同一類型的設備怎麼註冊?比如說有4個LED需要控制?
有兩種方法,第一種當然是有幾個設備就註冊幾個設備號,這樣肯定沒問題,但是如果設備數量很多,這種方法就不可取了。
第二種就是利用次設備號,這些設備共享一個主設備號,用次設備號區分彼此。
驅動程序中註冊和註銷函數就要作如下修改,這裏以4個LED爲例:
static int major;//主設備號
static struct device *led_device[4];//4個設備
static int __init led_init(void)
{
unsigned char i;
/*1.申請或者指定主設備號 此處由內核動態分配設備號*/
alloc_chrdev_region(&dev_id, 0, 4, DEVICE_NAME);//需要分配4個次設備號,0,1,2,3
major = MAJOR(dev_id);//取出主設備號
/*2.初始化 LED_DEV結構體*/
cdev_init(&LED_cdev, &led_fops);
/*3.向LED_cdev添加設備*/
cdev_add(&LED_cdev,dev_id, 4);//添加4個設備
//4.創建一個LED類
led_class = class_create(THIS_MODULE,"led_class");
//5.在LED類下面創建一個LED設備 然後mdev通過這個自動創建/dev/"DEVICE_NAME"
for(i=0;i<4;i++)//創建4個設備
{
led_device[i] = device_create(led_class,NULL,MKDEV(major, i),NULL,"imx283_led%d",i);
}
printk("module init ok\n");
return 0;
}
static void __exit led_exit(void)
{
unsigned char i;
/*1.刪除LED_cdev結構體*/
cdev_del(&LED_cdev);
/*2.註銷設備 cat /proc/devices下不可見*/
unregister_chrdev_region(dev_id,4);
/*3.刪除LED設備*/
for(i=0;i<4;i++)
{
device_destroy(led_class, MKDEV(major, i));
}
/*4.刪除LED類*/
class_destroy(led_class);
printk("module exit ok\n");
}
編譯生成.ko文件,用insmod加載,查看cat /proc/devices
只生成了1個LED設備,原因是這4個LED共享一個主設備號,同時也共享一個file_operations結構體,它們只是名字和次設備號不同,再查看下/dev目錄:
那麼它們既然共享同一個file_operations結構體,那該如何單獨控制每一個LED呢?
file_operations結構體的write函數、read函數和open函數的入口參數都可以取出LED的設備號
//write函數
static int led_write(struct file *filp,
const char __user *buf,
size_t count,
loff_t *f_pos)
//read函數
static ssize_t led_read(struct file *filp,
const char __user *buf,
size_t count,
loff_t *f_pos)
int minor =MINOR(filp->f_path.dentry->d_inode->i_rdev);//取出次設備號
static int led_open(struct inode *inode ,struct file *flip)//open 函數
int minor = MINOR(inode->i_rdev);//取出次設備號
//int minor = iminor(inode);//取出次設備號
這樣在應用程序中,調用open(/dev/led1)時,上面取出的minor=1,open(/dev/led3),上面的minor=3.write調用也是類似的情況,這樣驅動程序只要根據不同的次設備號對相應的LED進行操作,就可以實現同一套驅動控制多個同類設備,且沒有佔用多個主設備號。
本文參考:
1.《嵌入式Linux應用完全開發手冊》
2.《【正點原子】I.MX6U嵌入式Linux驅動開發指南V1.0》
3.分析Linux驅動函數register_chrdev_region