i.MX283開發板第一個Linux驅動-LED驅動改進

上一個博客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

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章