GPIO: General-purpose input/output,通用輸入輸出接口。下面以IMX6ULL芯片的GPIO寄存器來展開介紹。

1 GPIO 寄存器的 2 種操作方法

  1. 直接讀寫:讀出、修改對應位、寫入。
a) 要設置 bit n:
  val = data_reg;
  val = val | (1<<n);
  data_reg = val;
b) 要清除 bit n:
  val = data_reg;
  val = val & ~(1<<n);
  data_reg = val;
  1. set-and-clear protocol:(芯片不一定支持)

  set_reg, clr_reg, data_reg三個寄存器對應的是同一個物理寄存器:

  a) 要設置 bit n:set_reg = (1<<n);

  b) 要清除 bit n:clr_reg = (1<<n);

2 GPIO 寄存器配置流程

2.1 CCM時鐘設置

CCM寄存器為GPIO 模塊提供時鐘:

s5p4418 GPIO驅動 ioremap_#include

以IMX6ULL 芯片為列,GPIOn要用 CCM_CCGRx 寄存器中的 2 位來決定該組 GPIO 是否使能。將對應的clk gating enable

s5p4418 GPIO驅動 ioremap_linux_02

00:該 GPIO 模塊全程被關閉
01:該 GPIO 模塊在 CPU run mode 情況下是使能的;在 WAIT 或 STOP 模式下,關閉
10:保留
11:該 GPIO 模塊全程使能

例如:用CCM_CCGR0[bit31:30]使能GPIO2 的時鐘:

s5p4418 GPIO驅動 ioremap_引腳_03

例如:用CCM_CCGR1[bit31:30]使能GPIO5 的時鐘:

例如:用CCM_CCGR1[bit27:26]使能GPIO1 的時鐘:

s5p4418 GPIO驅動 ioremap_引腳_04

例如:用CCM_CCGR2[bit27:26]使能GPIO3的時鐘:

s5p4418 GPIO驅動 ioremap_linux_05

例如:用CCM_CCGR3[bit13:12]使能GPIO4的時鐘:

s5p4418 GPIO驅動 ioremap_引腳_06

2.2 引腳模式電器屬性設置

s5p4418 GPIO驅動 ioremap_引腳_07

MUX seting用來配置pin的模式,比如GPIO。Pad setting用來設置GPIO的電器屬性,比如電平,上下拉情況。

對於某個/某組引腳,IOMUXC中有 2 個寄存器用來設置它:

2.2.1 IOMUX功能

a) `IOMUXC_SW_MUX_CTL_PAD_ <PAD_NAME>`:`Mux pad xxx`,選擇某個引腳的功能

 IOMUXC_SW_MUX_CTL_GRP_<GROUP_NAME>Mux grp xxx,選擇某組引腳的功能

s5p4418 GPIO驅動 ioremap_linux_08

某個引腳,或是某組預設的引腳,都有 8 個可選的模式(alternate (ALT) MUX_MODE),設成ALT5表示選擇GPIO。

s5p4418 GPIO驅動 ioremap_引腳_09

2.2.2 電器屬性功能

a) IOMUXC_SW_PAD_CTL_PAD_<PAD_NAME>:pad pad xxx,設置某個引腳的電器屬性

b) IOMUXC_SW_PAD_CTL_GRP_<GROUP_NAME>pad grp xxx,設 置某組引腳的電器屬性

s5p4418 GPIO驅動 ioremap_#include_10

pad參數有很多不只是上下拉,還有很多屬性如IO驅動能力。

s5p4418 GPIO驅動 ioremap_引腳_11

2.2.2.1 GPIO驅動LED的4種方式

s5p4418 GPIO驅動 ioremap_#include_12

① 使用引腳輸出 3.3V 點亮 LED,輸出 0V 熄滅 LED。

② 使用引腳拉低到 0V 點亮 LED,輸出 3.3V 熄滅 LED。

③有的芯片為了省電等原因,其引腳驅動能力不足,這時可以使用三極管驅動。 使用引腳輸出 1.2V 點亮 LED,輸出 0V 熄滅 LED。

④使用引腳輸出 0V 點亮 LED,輸出 1.2V 熄滅 LED

2.2.3 GPIO方向

當iomux成gpio模式後,就需要配置成gpio輸出。

GPIOx_GDIR:設置引腳方向,每位對應一個引腳,1-output0-input.

s5p4418 GPIO驅動 ioremap_引腳_13

確定每組gpio基地址如下:加4就對應方向寄存器。

s5p4418 GPIO驅動 ioremap_linux_14

2.2.4 GPIO值

GPIOx_DR:(GPIOx的data register)。設置輸出引腳的電平,每位對應一個引腳,1-高電平,0-低電平。

s5p4418 GPIO驅動 ioremap_引腳_15

如果是配成了輸入引腳,GPIOx_PSR:讀取引腳的電平,每位對應一個引腳,1-高電平,0-低電平:

s5p4418 GPIO驅動 ioremap_#include_16

3 字符設備驅動程序框架

s5p4418 GPIO驅動 ioremap_linux_17

s5p4418 GPIO驅動 ioremap_#include_18

字符驅動編寫流程:

/*
1. 確定主設備號,也可以讓內核動態分配.
2. 定義自己的 file_operations 結構體 實現對應的 drv_open/drv_read/drv_write 等函數
填入 file_operations 結構體,把 file_operations 結構體告訴內核。
3. register_chrdev/unregister_chrdev
4. 其他完善:提供設備信息,自動創建設備節點:class_create, device_create
5. 操作硬件:通過 ioremap 映射寄存器的物理地址得到虛擬地址,讀寫虛擬地址
6. 驅動怎麼和 APP 傳輸數據:通過 copy_to_user、copy_from_user 等操作函數。
*/
if (newchrled.major) { /* 定義了設備號,靜態分配 */ 
    newchrled.devid = MKDEV(newchrled.major, 0); 
    register_chrdev_region(newchrled.devid, NEWCHRLED_CNT, NEWCHRLED_NAME); 
} else { /* 沒有定義設備號,動態分配 */ 
    alloc_chrdev_region(&newchrled.devid, 0, NEWCHRLED_CNT, NEWCHRLED_NAME); /* 申請設備號 */ 
    newchrled.major = MAJOR(newchrled.devid); /* 獲取主設備號 */ 
    newchrled.minor = MINOR(newchrled.devid); /* 獲取次設備號 */ 
} 
printk("newcheled major=%d,minor=%d\r\n",newchrled.major, newchrled.minor); 

/* 2、初始化cdev */ 
newchrled.cdev.owner = THIS_MODULE; 
cdev_init(&newchrled.cdev, &newchrled_fops); 

/* 3、添加一個cdev */ 
cdev_add(&newchrled.cdev, newchrled.devid, NEWCHRLED_CNT); 

/* 4、創建類 */ 
newchrled.class = class_create(THIS_MODULE, NEWCHRLED_NAME); 
if (IS_ERR(newchrled.class)) 
    return PTR_ERR(newchrled.class); 

/* 5、創建設備 */
newchrled.device = device_create(newchrled.class, NULL, newchrled.devid, NULL, NEWCHRLED_NAME);
if (IS_ERR(newchrled.device))
    return PTR_ERR(newchrled.device);

3.1 實現通用性驅動模板

3.1.1 led_drv.c

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include "led_opr.h"
  
/* 確定主設備號 */
static int major = 0;
static struct class *led_class;
struct led_operations *p_led_opr;

#define MIN(a, b) (a < b ? a : b)
/* 實現對應的open/read/write等函數,填入file_operations結構體 */
static ssize_t led_drv_read(struct file *file, char __user *buf, size_t size, loff_t *offset)
{
        printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
        return 0;
}
  
/* write(fd, &val, 1); */
static ssize_t led_drv_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
        int err;
        char status;
        struct inode *inode = file_inode(file);
        int minor = iminor(inode);
 
        printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
        err = copy_from_user(&status, buf, 1);
        /* 根據次設備號和status控制LED */
        p_led_opr->ctl(minor, status);
        return 1;
}
  
static int led_drv_open(struct inode *node, struct file *file)
{
        int minor = iminor(node);
         
        printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
        /* 根據次設備號初始化LED */
        p_led_opr->init(minor);
        return 0;
}
  
static int led_drv_close (struct inode *node, struct file *file)
{
        printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
        return 0;
}
  
/* 定義自己的file_operations結構體  */
static struct file_operations led_drv = {
        .owner         = THIS_MODULE,
        .open    = led_drv_open,
        .read    = led_drv_read,
        .write   = led_drv_write,
        .release = led_drv_close,
};
  
/* 把file_operations結構體告訴內核:註冊驅動程序  */
/* 入口函數:安裝驅動程序時,就會去調用這個入口函數 */
static int __init led_init(void)
{
        int err;
        int i;
         
        printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
        major = register_chrdev(0, "100ask_led", &led_drv);
        led_class = class_create(THIS_MODULE, "100ask_led_class");
        err = PTR_ERR(led_class);
        if (IS_ERR(led_class)) {
                printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
                unregister_chrdev(major, "led");
                return -1;
        }
        p_led_opr = get_board_led_opr();
        /* creat device node, eg: /dev/100ask_led0,1,... */
        for (i = 0; i < p_led_opr->num; i++)
                device_create(led_class, NULL, MKDEV(major, i), NULL, "100ask_led%d", i); 
        return 0;
}
/* 有入口函數就應該有出口函數:卸載驅動程序時,就會去調用這個出口函數 */
static void __exit led_exit(void)
{
        int i;
  
        printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
        for (i = 0; i < p_led_opr->num; i++)
                device_destroy(led_class, MKDEV(major, i)); /* /dev/100ask_led0,1,... */
        class_destroy(led_class);
        unregister_chrdev(major, "100ask_led");
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
  1. register_chrdev, 如果傳入主設備號,則靜態註冊,傳入0則動態註冊返回主設備號。
  2. class_create創建類/sys/class/100ask_led_class
  3. get_board_led_opr獲取具體單板的操作operation函數,後面具體單板實現。
  4. 獲取到具體單板的led數量後,device_create為每一個led燈都建立設備節點。

再來看file_operations中的操作:

  1. led_drv_open根據次設備號,調用具體單板的init函數,比如gpio 引腳複用,電器屬性設置等。
  2. led_drv_write就可以根據次設備號, 控制具體單板的led引腳,設置高低電平,從而控制亮滅。

3.2 具體單板led驅動

3.2.1 led_opr.h

#ifndef _LED_OPR_H
#define _LED_OPR_H
struct led_operations {
        int num;
        int (*init) (int which); /* 初始化LED, which-哪個LED */       
        int (*ctl) (int which, char status); /* 控制LED, which-哪個LED, status:1-亮,0-滅 */
};
struct led_operations *get_board_led_opr(void);
#endif

定義一個led_operationsnum表示有幾個led, init表示初始化led(drv_open的時候調用,配置pinmuxio modeenable pin clk等)。

3.2.2 board_100ask_imx6ull-qemu.c分析

現在有一塊board_100ask_imx6ull-qemu板子有4個LED,佔2組GPIO,分別是GPIO5_3GPIO1_3, GPIO1_5, GPIO1_6

s5p4418 GPIO驅動 ioremap_引腳_19

3.2.2.1 CCM時鐘配置

寄存器配置參考2.1。使能時鐘gpio5和gpio1的時鐘,CCM_CCGR1[CG13]CCM_CCGR1[CG15]配置成0x11。

/* 1. enable GPIO1
 * CG13, b[27:26] = 0b11 */
 
*CCM_CCGR1 |= (3<<26);
       
/* 1. enable GPIO5
 * CG15, b[31:30] = 0b11 */
 *CCM_CCGR1 |= (3<<30);
3.2.2.2 IOMUX成gpio

iomux配置4個引腳複用成gpio功能。

3.2.2.2.1 gpio5_3 進行iomux

基地址為0x2290014。用ioremap進行映射到虛擬地址,就可以直接操作寄存器地址了。但是一般建議用writel, writeb等函數族。配成5表示gpio模式。

s5p4418 GPIO驅動 ioremap_linux_20

IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3=ioremap(0x2290014, 4);        
/* 2. set GPIO5_IO03 as GPIO
 * MUX_MODE, b[3:0] = 0b101 */

*IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = 5;
3.2.2.2.2 gpio1_3/gpio1_5/gpio1_6 進行iomux

s5p4418 GPIO驅動 ioremap_linux_21

s5p4418 GPIO驅動 ioremap_linux_22

s5p4418 GPIO驅動 ioremap_linux_23

每次映射4個字節太繁瑣,乾脆對整個gpio的iomux地址進行映射。

struct iomux {
    volatile unsigned int unnames[23];
    volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO00;  /* offset 0x5c*/
    volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO01;
    volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO02;
    volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
    volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04;
    volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO05;
    volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO06;
    volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO07;
    volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO08;
    volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO09;
};

iomux = ioremap(0x20e0000, sizeof(struct iomux));

這裏偷懶用了一個技巧,unnames[23]92(0x5c)字節,剛好IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO00地址就是0x20e0000+0x5c,就不用把所有寄存器都搬進來到struct iomux

同理IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03地址就是0x20e0000+0x68, 因此:

/* MUX_MODE, b[3:0] = 0b101 */
iomux->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = 5;
iomux->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO05 = 5;
iomux->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO06 = 5;
3.2.2.3 gpio配成輸出

s5p4418 GPIO驅動 ioremap_linux_24

struct imx6ull_gpio {
    volatile unsigned int dr;
    volatile unsigned int gdir;
    volatile unsigned int psr;
    volatile unsigned int icr1;
    volatile unsigned int icr2;
    volatile unsigned int imr;
    volatile unsigned int isr;
    volatile unsigned int edge_sel;
};
/* GPIO1 GDIR, b[5] = 0b1*/
 gpio1 = ioremap(0x209C000, sizeof(struct imx6ull_gpio));
 gpio1->gdir |= (1<<3);
 gpio1->gdir |= (1<<5);
 gpio1->gdir |= (1<<6);

offset為0表示data register, offset為4表示方向寄存器。以gpio1_3/gpio1_5/gpio1_6舉例,gdirbit_n置1就表示哪個gpio配成輸出。

3.2.2.4 gpio值設置
if (which == 0) {
        if (status)  /* on : output 0 */
            gpio5->dr &= ~(1<<3);
        else         /* on : output 1 */
            gpio5->dr |= (1<<3);
    } else if (which == 1) {
        if (status)  /* on : output 0 */
            gpio1->dr &= ~(1<<3);
        else         /* on : output 1 */
            gpio1->dr |= (1<<3);
    } else if (which == 2) {
        if (status)  /* on : output 0 */
            gpio1->dr &= ~(1<<5);
        else         /* on : output 1 */
            gpio1->dr |= (1<<5);
    } else if (which == 3) {
        if (status)  /* on : output 0 */
            gpio1->dr &= ~(1<<6);
        else         /* on : output 1 */
            gpio1->dr |= (1<<6);
    }

同理dr就表示數據寄存器。一共4個led:

which等於0表示gpio5_3
 which等於1示gpio1_3
 which等於2示gpio1_5
 which等於3示gpio1_6
3.2.2.5 board_100ask_imx6ull-qemu.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/io.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include "led_opr.h"
  
struct iomux {
    volatile unsigned int unnames[23];
    volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO00;  /* offset 0x5c*/
    volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO01;
    volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO02;
    volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03;
    volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04;
    volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO05;
    volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO06;
    volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO07;
    volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO08;
    volatile unsigned int IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO09;
};
struct imx6ull_gpio {
    volatile unsigned int dr;
    volatile unsigned int gdir;
    volatile unsigned int psr;
    volatile unsigned int icr1;
    volatile unsigned int icr2;
    volatile unsigned int imr;
    volatile unsigned int isr;
    volatile unsigned int edge_sel;
};
  
/* enable GPIO1,GPIO5 */
static volatile unsigned int *CCM_CCGR1;

static volatile unsigned int *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3;
static struct iomux *iomux;

static struct imx6ull_gpio *gpio1;
static struct imx6ull_gpio *gpio5;

static struct led_operations board_demo_led_opr = {
    .num  = 4,
    .init = board_demo_led_init,
    .ctl  = board_demo_led_ctl,
};

static int board_demo_led_init(int which) {
    if (!CCM_CCGR1) {
        CCM_CCGR1 = ioremap(0x20C406C, 4);
        IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = ioremap(0x2290014, 4);
        iomux = ioremap(0x20e0000, sizeof(struct iomux));
        gpio1 = ioremap(0x209C000, sizeof(struct imx6ull_gpio));
        gpio5 = ioremap(0x20AC000, sizeof(struct imx6ull_gpio));
    }
  
    if (which == 0) {
        *CCM_CCGR1 |= (3<<30);
        *IOMUXC_SNVS_SW_MUX_CTL_PAD_SNVS_TAMPER3 = 5;
        gpio5->gdir |= (1<<3);
    } else if(which == 1) {
        *CCM_CCGR1 |= (3<<26);
        iomux->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO03 = 5;
        gpio1->gdir |= (1<<3);
    } else if(which == 2) {
        *CCM_CCGR1 |= (3<<26);
        iomux->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO05 = 5;
        gpio1->gdir |= (1<<5);
    } else if(which == 3) {
        *CCM_CCGR1 |= (3<<26);
        iomux->IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO06 = 5;
        gpio1->gdir |= (1<<6);
    }
    return 0;
}
static int board_demo_led_ctl(int which, char status) /* 控制LED, which-哪個LED, status:1-亮,0-滅 */
{
    if (which == 0) {
        if (status)
            gpio5->dr &= ~(1<<3);
        else
            gpio5->dr |= (1<<3);
    } else if (which == 1) {
        if (status)
            gpio1->dr &= ~(1<<3);
        else 
            gpio1->dr |= (1<<3);
    } else if (which == 2) {
        if (status)
            gpio1->dr &= ~(1<<5);
        else
            gpio1->dr |= (1<<5);
    } else if (which == 3) {
        if (status)
            gpio1->dr &= ~(1<<6);
        else
            gpio1->dr |= (1<<6);
    }
    return 0;
}

struct led_operations *get_board_led_opr(void) {
    return &board_demo_led_opr;
}

open的時候調用get_board_led_opr得到具體單板的操作函數集。進一步調用board_demo_led_init初始化led。

write的時候調用具體單板的操作函數集,進一步調用board_demo_led_ctl操控led。

4 字符設備驅動基礎概念

4.1 EXPORT_SYMBOL

EXPORT_SYMBOL:導出函數,讓別的module也能使用。

s5p4418 GPIO驅動 ioremap_linux_25

EXPORT_SYMBOL_GPL:

s5p4418 GPIO驅動 ioremap_引腳_26

4.2 MODULE_INFO

MODULE_INFO(intree, "Y");的作用是將可加載內核模塊標記為in-tree

加載樹外 LKM 會導致內核打印警告:這是從module.c中的檢查引起的:

s5p4418 GPIO驅動 ioremap_#include_27

module: loading out-of-tree module taints kernel.

4.2 module_param

module_param(name,type,perm);

功能:指定模塊參數,用於在加載模塊時或者模塊加載以後傳遞參數給模塊。

module_param_array( name, type, nump, perm);

可用sysfs進行查看修改:

s5p4418 GPIO驅動 ioremap_#include_28

講到module_param,把其他的也一筆帶入:

MODULE_DESCRIPTION("Freescale PM rpmsg driver");
MODULE_AUTHOR("Anson Huang <Anson.Huang@nxp.com>");
MODULE_LICENSE("GPL");
MODULE_VERSION("v2.0");

4.2.1 type

type: 數據類型:

bool : 布爾型
    inbool : 布爾反值
    charp: 字符指針(相當於char *,不超過1024字節的字符串)
    short: 短整型
    ushort : 無符號短整型
    int : 整型
    uint : 無符號整型
    long : 長整型
    ulong: 無符號長整型

4.2.2 perm

perm表示此參數在sysfs文件系統中所對應的文件節點的屬性,其權限在include/linux/stat.h中有定義:

s5p4418 GPIO驅動 ioremap_#include_29

#define S_IRUSR 00400 //文件所有者可讀
#define S_IWUSR 00200 //文件所有者可寫
#define S_IXUSR 00100 //文件所有者可執行

#define S_IRGRP 00040 //與文件所有者同組的用户可讀
#define S_IWGRP 00020
#define S_IXGRP 00010

#define S_IROTH 00004 //與文件所有者不同組的用户可讀
#define S_IWOTH 00002
#define S_IXOTH 00001

s5p4418 GPIO驅動 ioremap_#include_30

static char *alg = NULL;
static u32 type;
static u32 mask;
static int mode;

module_param(alg, charp, 0);
module_param(type, uint, 0);
module_param(mask, uint, 0);
module_param(mode, int, 0);

static int fish[10];
static int nr_fish;
module_param_array(fish, int, &nr_fish, 0664);
static char media[8];
module_param_string(media, media, sizeof(media), 0);

可以用sysfs設置fish數組,或者insmod時伴隨設置。

4.3 設備節點

cat /proc/devices

s5p4418 GPIO驅動 ioremap_引腳_31

4.3.1 手動建立設備節點

手動建立設備節點命令是mknod, 由於這裏的字符設備都是用的misc雜項設備方式,因此主設備號都為10:

/mnt/Athena2_FPGA_SDK_Veriry/demo/workspace/ko # ls -l /dev/mmcblk0p1
brw-rw----    1 root     root      179,   1 Jan  1 00:05 /dev/mmcblk0p1
/mnt/Athena2_FPGA_SDK_Veriry/demo/workspace/ko # ls -l /dev/mmcblk0
brw-rw----    1 root     root      179,   0 Jan  1 00:05 /dev/mmcblk0

/dev # ls -l xxx-*
crw-rw----    1 root     root       10,   0 Jan  1 00:05 /dev/xxx-base
crw-rw----    1 root     root       10,  61 Jan  1 00:05 /dev/xxx-dwa
crw-rw----    1 root     root       10,  58 Jan  1 00:30 /dev/xxx-ldc
crw-rw----    1 root     root       10,  60 Jan  1 00:04 /dev/xxx-stitch
crw-rw----    1 root     root       10,  62 Jan  1 00:05 /dev/xxx-sys
crw-rw----    1 root     root       10,  59 Jan  1 00:04 /dev/xxx-vpss

mknod /dev/mmcblk0 b 179 0
mknod /dev/mmcblk0p1 b 179 1

mknod /dev/xxx-base c 10 0
mknod /dev/xxx-sys c 10 62
mknod /dev/xxx-dwa c 10 61
mknod /dev/xxx-ldc c 10 58
mknod /dev/xxx-stitch c 10 60
mknod /dev/xxx-vpss c 10 59
crw-rw---- 1 root root 10, 0 Jan 1 00:08 /dev/xxx-base
crw-rw---- 1 root root 10, 61 Jan 1 00:08 /dev/xxx-dwa
crw-rw---- 1 root root 10, 59 Jan 1 00:07 /dev/xxx-ldc
crw-rw---- 1 root root 10, 60 Jan 1 00:07 /dev/xxx-stitch
crw-rw---- 1 root root 10, 62 Jan 1 00:08 /dev/xxx-sys

mknod /dev/xxx-base c 10 0
mknod /dev/xxx-sys c 10 62
mknod /dev/xxx-dwa c 10 61
mknod /dev/xxx-ldc c 10 59
mknod /dev/xxx-stitch c 10 60

4.3.2 自動創建設備節點

4.3.2.1 mdev機制

udev是一個用户程序,在 Linux下通過 udev來實現設備文件的創建與刪除, udev可以檢測系統中硬件設備狀態,可以根據系統中硬件設備狀態來創建或者刪除設備文件。比如使用modprobe命令成功加載驅動模塊以後就自動在/dev目錄下創建對應的設備節點文件 ,使用rmmod命令卸載驅動模塊以後就 刪除掉/dev目錄下的設備節點文件。 使用busybox構建根文件系統的時候, busybox會創建一個 udev的簡化版本mdev,所以在嵌入式 Linux中我們使用mdev來實現設備節點文件的自動創建與刪除, Linux系統中的熱插拔事件也由 mdev管理:

echo /sbin/mdev > /proc/sys/kernel/hotplug

4.4 設置文件私有數據

一般open函數裏面設置好私有數據以後,在write、 read、 close等函數中直接讀取private_data即可得到設備結構體。

s5p4418 GPIO驅動 ioremap_linux_32

4.5 設備號

include\linux\kdev_t.h

s5p4418 GPIO驅動 ioremap_linux_33

MINORBITS 表示次設備號位數,一共是 20 位;
MINORMASK 表示次設備號掩碼;
MAJOR 用於從 dev_t 中獲取主設備號,將 dev_t 右移 20 位即可
MINOR 用於從 dev_t 中獲取次設備號,取 dev_t 的低 20 位的值即可
MKDEV 用於將給定的主設備號和次設備號的值組合成 dev_t 類型的設備號

s5p4418 GPIO驅動 ioremap_linux_34

定義了major主設備就用靜態註冊,否則動態分配設備號註冊字符設備。

4.5.1 靜態分配和釋放一個設備號

#include <linux/fs.h>
register_chrdev_region()
unregister_chrdev_region()
#include <linux/module.h> 
#include <linux/cdev.h>
#include <linux/fs.h>

#define MY_MAJOR_NUM 202 //主設備號
static const struct file_operations my_dev_fops = {
    .owner = THIS_MODULE,
    .open = my_dev_open,
    .release = my_dev_close,
    .unlocked_ioctl = my_dev_ioctl,
};
static int __init hello_init(void){
    int ret;
    dev_t dev = MKDEV(MY_MAJOR_NUM, 0);
 
    /* Allocate device numbers */
    ret = register_chrdev_region(dev, 1, "my_char_device");
    if (ret < 0){
        pr_info("Unable to allocate mayor number %d\n", MY_MAJOR_NUM);
        return ret;
    }
    /* Initialize the cdev structure and add it to the kernel space */
    cdev_init(&my_dev, &my_dev_fops);
    ret= cdev_add(&my_dev, dev, 1);
    if (ret < 0){
        unregister_chrdev_region(dev, 1);
        pr_info("Unable to add cdev\n");
        return ret;
    }
    return 0;
}
static void __exit hello_exit(void) {
    cdev_del(&my_dev);
    unregister_chrdev_region(MKDEV(MY_MAJOR_NUM, 0), 1);
}

4.5.2 動態分配和釋放一個設備號

#include <linux/fs.h>
alloc_chrdev_region()
unregister_chrdev_region()
static struct class*  helloClass;
static struct cdev my_dev;
dev_t dev;
static int __init hello_init(void) {
    int ret;
    dev_t dev_no;
    int Major;
    struct device* helloDevice;
    ret = alloc_chrdev_region(&dev_no, 0, 1, DEVICE_NAME);
    if (ret < 0){
        pr_info("Unable to allocate Mayor number \n");
        return ret;
    }
    Major = MAJOR(dev_no);
    dev = MKDEV(Major,0);
    cdev_init(&my_dev, &my_dev_fops);
    ret = cdev_add(&my_dev, dev, 1);
    if (ret < 0){
        unregister_chrdev_region(dev, 1);
        pr_info("Unable to add cdev\n");
        return ret;
    }
    helloClass = class_create(THIS_MODULE, CLASS_NAME);
    if (IS_ERR(helloClass)){
        unregister_chrdev_region(dev, 1);
        cdev_del(&my_dev);
        pr_info("Failed to register device class\n");
        return PTR_ERR(helloClass);
    }
    helloDevice = device_create(helloClass, NULL, dev, NULL, DEVICE_NAME);
    if (IS_ERR(helloDevice)){
        class_destroy(helloClass);
        cdev_del(&my_dev);
        unregister_chrdev_region(dev, 1);
        pr_info("Failed to create the device\n");
        return PTR_ERR(helloDevice);
    }
    return 0;
}
static void __exit hello_exit(void) {
    device_destroy(helloClass, dev);     /* remove the device */
    class_destroy(helloClass);           /* remove the device class */
    cdev_del(&my_dev);
    unregister_chrdev_region(dev, 1);    /* unregister the device numbers */
}

4.6 添加設備和類

struct class *class; /* 類 */ 
struct device *device; /* 設備 */ 
dev_t devid; /* 設備號 */
static int __init led_init(void) { 
    class = class_create(THIS_MODULE, "xxx"); 
    device = device_create(class, NULL, devid, NULL, "xxx"); 
    return 0; 
} 
static void __exit led_exit(void) { 
	device_destroy(newchrled.class, newchrled.devid); 
    class_destroy(newchrled.class); 
} 
module_init(led_init); 
module_exit(led_exit);

5 內核源碼樹添加一個字符設備驅動

5.1 準備驅動源碼

這裏以misc device為例, 進入drivers/misc目錄,新建目錄hello_drv。放入驅動源碼MakefileKconfig

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>

static int major = 0;
static struct cdev hello_cdev;
static char kernel_buf[1024];
static struct class *hello_class;

static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset){
    int err;
    err = copy_to_user(buf, kernel_buf, min(1024, size));
    return min(1024, size);
}
static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset){
    int err;
    err = copy_from_user(kernel_buf, buf, min(1024, size));
    return min(1024, size);
} 
static int hello_drv_open (struct inode *node, struct file *file){
    return 0;
}
static int hello_drv_close (struct inode *node, struct file *file){
    return 0;
}
static struct file_operations hello_drv = {
    .owner     = THIS_MODULE,
    .open    = hello_drv_open,
    .read    = hello_drv_read,
    .write   = hello_drv_write,
    .release = hello_drv_close,
};
 
static int __init hello_init(void){
    int err;
    int rc;
    dev_t devid;
#if 0
    //major = register_chrdev(0, "hello", &hello_drv);  /* /dev/hello */
#else
    rc = alloc_chrdev_region(&devid, 0, 1, "hello");
    major = MAJOR(devid);
    cdev_init(&hello_cdev, &hello_drv);
    cdev_add(&hello_cdev, devid, 1);
#endif
    hello_class = class_create(THIS_MODULE, "hello_class");
    err = PTR_ERR(hello_class);
    if (IS_ERR(hello_class)) {
        printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
        unregister_chrdev(major, "hello");
        return -1;
    }
    device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello"); /* /dev/hello */
    return 0;
}
 
static void __exit hello_exit(void){
    device_destroy(hello_class, MKDEV(major, 0));
    class_destroy(hello_class);
#if 0
    //unregister_chrdev(major, "hello");
#else
    cdev_del(&hello_cdev);
    unregister_chrdev_region(MKDEV(major,0), 1);   
#endif
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

5.2 MakeFile

s5p4418 GPIO驅動 ioremap_引腳_35

userprogs-always-y += hello_test
userccflags += -I usr/include

這裏表示用userspace方式去編譯應用程序,hello_test就是用户程序。

假如我們多個文件hello1.c hello2.c, 如何得到hello.ohello.ko呢?如下參考:

s5p4418 GPIO驅動 ioremap_linux_36

5.3 Kconfig

s5p4418 GPIO驅動 ioremap_linux_37

5.4 修改上一級Makefile和Kconfig

s5p4418 GPIO驅動 ioremap_linux_38

s5p4418 GPIO驅動 ioremap_#include_39

hello_drv目錄中的Kconfig也能被內核識別,輸入make menuconfig,即可選擇將其編譯成內核模塊還是直接編譯進內核鏡像,默認default n,也就是CONFIG_HELLO等於n, hello_drv目錄是obj-n, 不編譯;選擇y則表示編譯進內核鏡像,選擇m表示編譯成內核模塊。

編譯成內核模塊,則會在.config中產生CONFIG_HELLO=m的一項配置,編譯產生hello.ko

s5p4418 GPIO驅動 ioremap_linux_40

編譯成內核鏡像,則會在.config中產生CONFIG_HELLO=y的一項配置,編譯產生built-in.a,最終該 built-in.a會合入vmlinux。