從點一個燈開始學寫Linux字符設備驅動

關注、星標嵌入式客棧,精彩及時送達

[導讀] 前一篇文章,介紹瞭如何將一個hello word模塊編譯進內核或者編譯爲動態加載內核模塊,本篇來介紹一下如何利用Linux驅動模型來完成一個LED燈設備驅動。點一個燈有什麼好談呢?況且Linux下有專門的leds驅動子系統。

點燈有啥好聊呢?

在很多嵌入式系統裏,有可能需要實現數字開關量輸出,比如:

  • LED狀態顯示

  • 閥門/繼電器控制

  • 蜂鳴器

  • ......

嵌入式Linux一般需求千變萬化,也不可能這些需求都有現成設備驅動代碼可供使用,所以如何學會完成一個開關量輸出設備的驅動,一方面點個燈可以比較快了解如何具體寫一個字符類設備驅動,另一方面實際項目中對於開關量輸出設備就可以這樣幹,所以是具有較強的實用價值的。

要完成這樣一個開關量輸出GPIO的驅動程序,需要梳理梳理下面這些概念:

  • 設備編號

  • 設備掛載

  • 關鍵數據結構

設備編號

字符設備是通過文件系統內的設備名稱進行訪問的,其本質是設備文件系統樹的節點。故Linux下設備也是一個文件,Linux下字符設備在/dev目錄下。可以在開發板的控制檯或者編譯的主Linux系統中利用ls -l /dev查看,如下圖:

對於ls -l列出的屬性,做一個比較細的解析:

細心的朋友或許會發現設備號屬性,在有的文件夾下列出來不是這樣,這就對了!普通文件夾下是這樣:

差別在於一個是文件大小,一個是設備號。

再細心一點的朋友或許還會問,這些/dev下的文件時間屬性爲神馬都相差無幾?這是因爲/dev設備樹節點是在內核啓動掛載設備驅動動態生成的,所以時間就是系統開機後按次序生成的,你如不信,不妨重啓一下系統在查看一下。

常見文件類型:

  • d: directory 文件夾

  • l: link  符號鏈接

  • p: FIFO pipe 管道文件,可以用mkfifo命令生成創建

  • s: socket 套接字文件

  • c: char 字符型設備文件

  • b: block 塊設備文件

  • -:常規文件

回到設備號,設備號是一個32位無符號整型數,其中:

  • 12位用來表示主設備號,用於標識設備對應的驅動程序。

  • 20位用來表示次設備號,用於正確確定設備文件所指的設備。

這怎麼理解呢,看下串口類設備就比較清楚了:

主設備號一樣證明這些設備共用了一個驅動程序,而次設備號不一樣,則對應了不同的串口設備。那麼怎麼得到設備號呢?

/*下列定義位於./include/linux/types.h */
typedef u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;

/* 下面宏用於生成主設備號,次設備號       */
/* 下列定義位於./include/linux/Kdev_t.h */
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)

#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

使用舉例:

/* 主設備號 */
MAJOR(dev_t dev); 
/* 次設備號 */
MINOR(dev_t dev);

設備掛載

爲簡化問題,本文描述一下動態加載設備驅動模塊,暫不考慮設備樹。參考<<Linux設備驅動程序>>一書。可參照前文將驅動編譯成模塊,然後利用下面腳步動態加載模塊。由前面描述,知道設備最終需要在/dev目錄下生成一個設備文件,那麼這個設備文件節點是怎麼生成呢,看看下面的腳本:

#!/bin/sh
#-----------------------------------------------------------------------
module="led"
device="led"
mode="664"
group="staff"

# 利用insmod命令加載設備模塊
insmod -f $module.ko $* || exit 1
# 獲取系統分配的主設備號 
major=`cat /proc/devices | awk "\\$2==\"$module\" {print \\$1}"`

# 刪除舊節點
rm -f /dev/${device} 

#創建設備文件節點
mknod /dev/${device} c $major 0
#設置設備文件節點屬性
chgrp $group /dev/${device}
chmod $mode  /dev/${device}

這裏要提一下/proc/devices,這是一個文件記錄了字符和塊設備的主設備號,以及分配到這些設備號的設備名稱。比如使用cat命令來列出這個文件內容:

關鍵數據結構

字符設備由什麼關鍵數據結構進行抽象的呢,來看看:

  • file_operations定義在./include/linux/fs.h

  • cdev定義在./include/linux/cdev.h


cdev中與字符設備驅動編程相關兩個數據域:

  • const struct file_operations *ops;

  • dev_t dev;設備編號

文件操作符是一個龐大的數據結構,常規字符設備驅動一般需要實現下面一些函數指針:

  • read:用來實現從設備中讀取數據

  • write:用於實現寫入數據到設備

  • ioctl:實現執行設備特定命令的方法

  • open:用實現打開一個設備文件

  • release:當file結構被釋放時,將調用這個接口函數

點燈設備

先上代碼(可左右滑動顯示):

#include <linux/module.h>
#include <linux/init.h>
#include <linux/errno.h>
#include <linux/kernel.h>  /* printk() */
#include <linux/major.h>
#include <linux/cdev.h>
#include <linux/fs.h>      /* everything... */
#include <linux/gpio.h>
#include <asm/uaccess.h>   /* copy_*_user */

/*這裏具體參考不同開發板的電路 GPIOC24 */
#define LED_CTRL   (2*32+24)  

static const unsigned int led_pad_cfg = LED_CTRL;

struct t_led_dev{
 struct cdev cdev;
 unsigned char value; 
};

struct t_led_dev  led_dev;
static dev_t led_major;
static dev_t led_minor=0; 

static int led_open(struct inode * inode,struct file * filp)
{ 
 filp->private_data = &led_dev; 
 
 printk ("led is opened!\n");          
 return 0;
}

static int led_release(struct inode * inode,
                       struct file * filp)
{
 return 0;
}

static ssize_t led_read(struct file * file, 
                        char __user * buf,
                        size_t count, 
                        loff_t *ppos)
{
 ssize_t ret=1;   
 if(copy_to_user(&(led_dev.value),buf,1))
  return -EFAULT;
 printk ("led is read!\n");
 return ret;
}

static ssize_t led_write(struct file * filp, 
                         const char __user *buf,
                         size_t count,loff_t *ppos)
{
 unsigned char value; 
    ssize_t retval = 0;
 if(copy_from_user(&value,buf,1))
  return -EFAULT;
 
 if(value&0x01)
  gpio_set_value(led_pad_cfg, 1);
 else
  gpio_set_value(led_pad_cfg, 0);

    printk ("led is written!\n");
 return retval;
}

static const struct file_operations led_fops = {
 .owner = THIS_MODULE,
 .read  = led_read,
 .write = led_write,
 .open  = led_open,
 .release = led_release,
};

static void led_setup_cdev(struct t_led_dev * dev, int index)
{ 
    /* 初始化字符設備驅動數據域 */
 int err,devno = MKDEV(led_major,led_minor+index);
 cdev_init(&(dev->cdev),&led_fops);
 dev->cdev.owner = THIS_MODULE;
 dev->cdev.ops = &led_fops;
    /* 字符設備註冊 */
 err = cdev_add(&(dev->cdev),devno,1);
 if(err)
  printk(KERN_NOTICE "Error %d adding led %d",err,index); 
}

static int led_gpio_init(void)
{
 if (gpio_request(LED_CTRL, "led") < 0) {
  printk("Led request gpio failed\n");
  return -1;
 }
 
 printk("Led gpio requested ok\n");
 
 gpio_direction_output(LED_CTRL, 1); 
 gpio_set_value(LED_CTRL, 1);
 
 return 0;
}
/* 註銷設備 */
void led_cleanup(void)
{ 
 dev_t devno = MKDEV(led_major, led_minor); 
 gpio_set_value(LED_CTRL, 0); 
 gpio_free(LED_CTRL);

 cdev_del(&led_dev.cdev); 
 unregister_chrdev_region(devno, 1);    //註銷設備號  
}

/* 註冊設備 */
static int led_init(void)
{ 
 int result;
 dev_t dev = MKDEV( led_major, 0 );
 /* 動態分配設備號 */
 result = alloc_chrdev_region(&dev, 0, 1, "led");      
 if(result<0)
  return result; 
 
 led_major = MAJOR(dev);
 
 memset(&led_dev,0,sizeof(struct t_led_dev));
 led_setup_cdev(&led_dev,0);

 led_gpio_init();
 printk ("led device initialised!\n"); 
 
 return result;
}

module_init(led_init);
module_exit(led_cleanup);

MODULE_DESCRIPTION("Led device demo");
MODULE_AUTHOR("embinn");
MODULE_LICENSE("GPL");

來總結一下要點:

  • init函數,需要用module_init宏包起來,本例中即爲led_init,module_init宏的作用就是選編譯爲模塊或進內核的底層實現,建議剛開始不必深究。一般而言主要實現:

    • 申請分配主設備號alloc_chrdev_region

    • 爲特定設備相關數據結構分配內存

    • 將入口函數(open read write等)與字符設備驅動的cdev抽象數據結構關聯

    • 將主設備與驅動程序cdev相關聯

    • 申請硬件資源,初始化硬件

    • 調用cdev_add註冊設備

  • exit函數,一樣需要用module_exit包起來,主要負責:

    • 釋放硬件資源

    • 調用cdev_del刪除設備

    • 調用unregister_chrdev_region註銷設備號

  • 用戶空間與驅動數據交換

    • copy_to_user,如其名一樣,將內核空間數據信息傳遞到用戶空間

    • copy_from_user,如其名一樣,從用戶空間拷貝數據進內核空間

  • 善用printk進行驅動調試,這是內核打印函數。

  • gpio相關操作函數,這裏就不一一列舉其作用了,比較容易理解。

測試驅動

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define READ_SIZE 10

int main(int argc, char **argv){
 int fd,count;
 float value;
 unsigned char buf[READ_SIZE+1];
 printf( "Cmd argv[0]:%s,argv[1]:%s,argv[2]:%s\n",argv[0],argv[1],argv[2] );
 
 if( argc<2 ){
  printf( "[Usage: test device_name ]\n" );
  exit(0);
 }
    if(strlen(argv[2]!=1)
        printf( "Invalid parameter\n" );
    
 if(( fd = open(argv[1],O_WRONLY  ))<0){
  printf( "Error:can not open the device: %s\n",argv[1] );
  exit(1);
 }
    
    if(argv[2][0] == '1')
        buf[0] = 1;
    else if(argv[2][0] == '0')
        buf[0] = 0;
    else
        printf( "Invalid parameter\n" );

 printf("write: %d\n",buf[0]);
 if( (count = write( fd, buf ,1 ))<0 ){
  perror("write error.\n");
  exit(1);  
 }
 
 close(fd);
 printf("close device %s\n",argv[1] );
 return 0;
}

編譯成可執行文件,調用前面的腳本加載設備後,在/dev下就可以看到led設備了。比如測試代碼編譯成ledTest執行文件,則使用下面命令運行測試程序就可以看到led控制效果了:

/*打開led 具體取決電路是高有效還是低有效*/
./ledTest /dev/led 1
./ledTest /dev/led 0

這樣就實現了用戶空間驅動底層設備了,實際應用代碼就可以這樣去訪問底層的字符型設備。

總結一下

本文總結了簡單字符設備的驅動開發的一些要點,以及如何動態加載,在設備文件系統樹上創建設備節點,並演示了驅動以及驅動使用的基本要點。

本文辛苦原創,如喜歡請點贊/在看/分享支持,不勝感激!

END

往期精彩推薦,點擊即可閱讀

▲Linux內核中I2C總線及設備長啥樣? 

▲學Linux驅動:應先了解總線驅動模型

▲看思維導圖:一文帶你學Verilog HDL語言

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