Linux驅動之GPIO子系統和pinctrl子系統


前期知識

  1.如何編寫一個簡單的Linux驅動(一)——驅動的基本框架
  2.如何編寫一個簡單的Linux驅動(二)——設備操作集file_operations
  3.如何編寫一個簡單的Linux驅動(三)——完善設備驅動
  4.Linux驅動之設備樹的基礎知識

前言

  在學習單片機(比如51單片機和STM32)的時候,我們可以直接對單片機的寄存器進行操作,進而達到控制pin腳的目的。而Linux系統相較於一個單片機系統,要龐大而復雜得多,因此在Linux系統中我們不能直接對pin腳進行操作。Linux系統講究驅動分層,pinctrl子系統和GPIO子系統就是驅動分層的產物。如果我們要操作pin腳,就必須要借助pinctrl子系統和GPIO子系統。
  pinctrl子系統的作用是pin config(引腳配置)pin mux(引腳復用),而如果pin腳被復用為了GPIO(注意:GPIO功能只是pin腳功能的一種),就需要再借助GPIO子系統對pin腳進行控制了,GPIO子系統提供了一系列關於GPIO的API函數,供我們調用。本章,我們會使用pinctrl子系統和GPIO子系統來完成對GPIO的操作。
  本章的驅動代碼和用戶程序代碼要在"如何編寫一個簡單的Linux驅動(三)——完善設備驅動"這一章所寫的代碼基礎上進行修改。如果要下載"如何編寫一個簡單的Linux驅動(三)——完善設備驅動"這一章的代碼,請點擊這里

1.閱讀幫助文檔

  打開內核設備樹目錄下的文檔kernel/Documentation/devicetree/bindings/pinctrl/samsung-pinctrl.txt,可以看到三星原廠提供的pinctrl子系統的幫助說明。
  首先看下面這一段文檔內容。

Eg: <&gpx2 6 0>
<[phandle of the gpio controller node]
[pin number within the gpio controller]
[flags]>

Values for gpio specifier:
- Pin number: is a value between 0 to 7.
- Flags: 0 - Active High 1 - Active Low

  這段內容舉例了如何使能某個GPIO。以<&gpx2 6 0>為例,第一個參數&gpx2是對應的gpio controller節點,第二個參數6是gpio controller的pin腳編號,第三個參數0是標志位(0表示高電平有效,1表示低電平有效)。
  再看文檔中的這一段內容。

- Pin mux/config groups as child nodes: The pin mux (selecting pin function mode) and pin config (pull up/down, driver strength) settings are represented as child nodes of the pin-controller node. There should be atleast one child node and there is no limit on the count of these child nodes. It is also possible for a child node to consist of several further child nodes to allow grouping multiple pinctrl groups into one. The format of second level child nodes is exactly the same as for first level ones and is described below.

The child node should contain a list of pin(s) on which a particular pin function selection or pin configuration (or both) have to applied. This list of pins is specified using the property name "samsung,pins". There should be atleast one pin specfied for this property and there is no upper limit on the count of pins that can be specified. The pins are specified using pin names which are derived from the hardware manual of the SoC. As an example, the pins in GPA0 bank of the pin controller can be represented as "gpa0-0", "gpa0-1", "gpa0-2" and so on. The names should be in lower case. The format of the pin names should be (as per the hardware manual) "[pin bank name]-[pin number within the bank]".

The pin function selection that should be applied on the pins listed in the child node is specified using the "samsung,pin-function" property. The value of this property that should be applied to each of the pins listed in the "samsung,pins" property should be picked from the hardware manual of the SoC for the specified pin group. This property is optional in the child node if no specific function selection is desired for the pins listed in the child node. The value of this property is used as-is to program the pin-controller function selector register of the pin-bank.

The child node can also optionally specify one or more of the pin configuration that should be applied on all the pins listed in the "samsung,pins" property of the child node. The following pin configuration properties are supported.

- samsung,pin-val: Initial value of pin output buffer.
- samsung,pin-pud: Pull up/down configuration.
- samsung,pin-drv: Drive strength configuration.
- samsung,pin-pud-pdn: Pull up/down configuration in power down mode.
- samsung,pin-drv-pdn: Drive strength configuration in power down mode.

  這部分內容較長,簡而言之就是描述了引用pin腳的寫法pin腳的功能復用屬性pin腳的配置屬性

  1. 引用pin腳的屬性名為samsung,pins,它的值寫法是[pin bank name]-[pin number within the bank],如samsung.pins = gpa0-1;
  2. pin腳功能復用屬性的屬性名為samsung,pin-function,它的值的寫法可以在dt-bindings/pinctrl/samsung.h文件中找到,如samsung,pin-function = EXYNOS_PIN_FUNC_OUTPUT;
  3. pin腳的配置屬性比較多,這里只選兩個本章用得到的:samsung.pin-val是pin腳的默認輸出值(高電平還是低電平),如samsung.pin-val = <1>;,而samsung.pin-pud是設置上拉或者下拉,如samsung.pin-pud = <EXYNOS_PIN_PULL_UP>;

  以上這兩部分說明了pin腳復用為GPIO時該如何寫設備樹代碼。

2.修改設備樹源碼

  本章要實現的效果是讓用戶程序控制開發板上LED燈的亮滅。通過查看開發板原理圖,得知兩個LED燈連的pin腳是gpl2-0gpk1-1
  (1) 打開pinctrl相關的設備樹頭文件kernel/arch/arm/boot/dts/exynos4412-pinctrl.dtsi,可以看到gpkgplpinctrl_1的子節點,見下方代碼。

...
pinctrl_1: pinctrl@11000000 {
    ...
    gpk1: gpk1 {
          gpio-controller;
          #gpio-cells = <2>;

          interrupt-controller;
          #interrupt-cells = <2>;
    };
    ...
    gpl2: gpl2 {
          gpio-controller;
          #gpio-cells = <2>;

          interrupt-controller;
          #interrupt-cells = <2>;
    };
    ...
}
...

  在pinctrl_1節點下添加兩個自定義的節點pinctrl_shanwuyan_leds,如下方代碼。

pinctrl_1: pinctrl@11000000 {
    ...
    gpk1: gpk1 {
          gpio-controller;
          #gpio-cells = <2>;

          interrupt-controller;
          #interrupt-cells = <2>;
    };
    ...
    gpl2: gpl2 {
          gpio-controller;
          #gpio-cells = <2>;

          interrupt-controller;
          #interrupt-cells = <2>;
    };
    ...
    /*自己添加的設備樹節點*/
    pinctrl_shanwuyan_leds: gpio_leds {
          samsung,pins = "gpl2-0","gpk1-1" ;	//LED的pin腳為gpl2-0和gpk1-1
          samsung,pin-function = <EXYNOS_PIN_FUNC_OUTPUT>;	//設置為輸出
          samsung,pin-val = <1>;	//默認輸出為低電平
          samsung,pin-pud = <EXYNOS_PIN_PULL_UP>;	//設置為上拉
    };
    ...
}

  (2) 然后打開設備樹文件kernel/arch/arm/boot/dts/exynos4412-itop-elite.dts,在根節點下添加一個自定義節點shanwuyan_leds。另外,如果設備樹文件中其他的代碼段也使用了這兩個pin腳,記得將它們注釋掉。本文中,gpk1-1在設備樹自帶的led燈設備中被使用了,所以要先注釋掉。如下方代碼。

/ {
    model = "TOPEET iTop 4412 Elite board based on Exynos4412";
    compatible = "topeet,itop4412-elite", "samsung,exynos4412", "samsung,exynos4";
		
    chosen {
          bootargs = "root=/dev/mmcblk0p2 rw rootfstype=ext4 rootdelay=1 rootwait";
          stdout-path = "serial2:115200n8";
    };
		
    memory {
          reg = <0x40000000 0x40000000>;
    };
    leds {	//這是設備樹自帶的led設備節點
          compatible = "gpio-leds";
			
          led2 {
                label = "red:system";
                gpios = <&gpx1 0 GPIO_ACTIVE_HIGH>;
                default-state = "off";
                linux,default-trigger = "heartbeat";
          };
		
          led3 {
                label = "red:user";
          //	gpios = <&gpk1 1 GPIO_ACTIVE_HIGH>;	//和我們自己寫的led設備所用的pin腳產生了沖突,要注釋掉
                default-state = "off";
          };
    };
		...
		...
    /*自己添加的設備樹節點*/
    shanwuyan_leds{
          compatible = "samsung,shanwuyan_leds";
          pinctrl-names = "default";
          pinctrl-0 = <&pinctrl_shanwuyan_leds>;
          led-gpios = <&gpl2 0 GPIO_ACTIVE_HIGH>,<&gpk1 1 GPIO_ACTIVE_HIGH>;
          status = "okay";
	};
};

  (3) 使用命令make dtbs編譯設備樹文件。

  將生成的dtb文件燒寫進開發板中。

  重啟開發板,在命令行輸入ls /proc/device-tree/shanwuyan_leds/,可以查看到我們新添加的節點及其屬性。

3.修改驅動程序

  打開驅動代碼文件。
  (1) 首先添加四個新的頭文件,然后把驅動名稱修改一下,再添加三個宏定義。如下方代碼。

/* 源代碼文件名為"shanwuyan.c"*/
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/cdev.h>
#include <linux/device.h>

/*新添加如下四個新的頭文件*/
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <linux/of.h>
#include <linux/io.h>

#define SHANWUYAN_NAME "shanwuyan_leds"	//修改驅動名稱
/*添加三個新的宏定義*/
#define LEDS_NUM 2	//LED燈的個數為2
#define LEDS_ON	1	//LED燈的開啟狀態
#define LEDS_OFF 0	//lED燈的關閉狀態
...

  (2) 向結構體shanwuyan_dev中添加兩個新的成員變量,如下方代碼。

struct shanwuyan_dev
{
    struct cdev c_dev;		//字符設備
    dev_t dev_id;			//設備號
    struct class *class;	//類
    struct device *device;	//設備
    int major;				//主設備號
    int minor;				//次設備號
	
    /*新添加的成員變量*/
    struct device_node *node;	//用於獲取設備樹節點
    int led_gpios[2];	//用於獲取兩個led的GPIO編號
};

  (3) 我們需要添加一個新的函數leds_init,用以初始化兩個LED占用的GPIO。在該函數中,我們使用GPIO子系統提供的API函數,對pin腳進行操作。在添加之前,我們要先介紹幾個函數。

//位於linux/of.h文件中
static inline struct device_node *of_find_node_by_path(const char *path);	
//通過節點路徑來查找設備樹節點,若查找失敗,則返回NULL
//位於linux/of_gpio.h文件中
static inline int of_get_named_gpio(struct device_node *np, const char *propname, int index);
//通過設備樹節點、屬性名、屬性索引號來獲取GPIO編號,若獲取失敗,則返回一個負數
//位於linux/gpio.h文件中
static inline int gpio_request(unsigned gpio, const char *label);
//申請GPIO,第一個參數是GPIO編號,第二個參數是給GPIO加的標簽(由程序員給定),如果申請成功,則返回0,否則返回一個非零數
static inline void gpio_free(unsigned gpio);
//釋放GPIO,參數是GPIO編號
static inline int gpio_direction_input(unsigned gpio);
//把GPIO設置為輸入模式,參數是GPIO編號,如果設置成功,則返回0,否則返回一個非零數
static inline int gpio_direction_output(unsigned gpio, int value);
//把GPIO設置為輸出模式,第一個參數是GPIO編號,第二個參數是默認輸出值,如果設置成功,則返回0,否則返回一個非零數
static inline void gpio_set_value(unsigned int gpio, int value);
//設置GPIO的輸出值,第一個參數是GPIO編號,第二個參數是輸出值

  接下來我們添加函數led_init,然后在入口函數shanwuyan_init中調用它,在加載驅動的時候就完成GPIO的初始化。相應的,在出口函數shanwuyan_exit中,要釋放掉申請的GPIO。如下方代碼。

...
static int leds_init(struct shanwuyan_dev *leds_dev)//初始化led的GPIO
{
    int ret = 0;
    int i = 0;
    char led_labels[][20] = {"led_gpio_0", "led_gpio_1"};	//定義兩個設備標簽
	
    /*1.根據設備節點在設備樹中的路徑,獲取設備樹中的設備節點*/
    leds_dev->node = of_find_node_by_path("/shanwuyan_leds");
    if(leds_dev->node == NULL)
    {
          ret = -EINVAL;
          printk("cannot find node /shanwuyan_leds\r\n");
          goto fail_find_node;
    }
	
    /*2.獲取led對應的gpio*/
    for(i = 0; i < LEDS_NUM; i++)
    {
          leds_dev->led_gpios[i] = of_get_named_gpio(leds_dev->node, "led-gpios", i);	
          if(leds_dev->led_gpios[i] < 0)	//如果獲取失敗
          {
                printk("cannot get led_gpio_%d\r\n", i);
                ret = -EINVAL;
                goto fail_get_gpio;
          }	
    }		
    for(i = 0; i < LEDS_NUM; i++)	//打印出獲取的gpio編號
          printk("led_gpio_%d = %d\r\n", i, leds_dev->led_gpios[i]);

    /*3.申請gpio*/
    for(i = 0; i < LEDS_NUM; i++)
    {
          ret = gpio_request(leds_dev->led_gpios[i], led_labels[i]);
          if(ret)	//如果申請失敗
          {
                printk("cannot request the led_gpio_%d\r\n", i);
                ret = -EINVAL;
                if(i == 1)
                      goto fail_request_gpio_1;
                else
                      goto fail_request_gpio_0;
          }
    }
	

    /*4.使用gpio:設置為輸出*/
    for(i = 0; i < LEDS_NUM; i++)
    {
          ret = gpio_direction_output(leds_dev->led_gpios[i], 1);
          if(ret)	//如果是指失敗
          {
                printk("failed to set led_gpio_%d\r\n", i);
                ret = -EINVAL;
                goto fail_set_output;
          }
    }
	
    /*5.設置輸出高電平,led會亮*/
    for(i = 0; i < LEDS_NUM; i++)
          gpio_set_value(leds_dev->led_gpios[i], 1);

    return 0;

fail_set_output:
    gpio_free(leds_dev->led_gpios[1]);	//釋放掉led_gpio_1
fail_request_gpio_1:
    gpio_free(leds_dev->led_gpios[0]);//如果led_gpio_1申請失敗,則也要把led_gpio_0也要釋放掉
fail_request_gpio_0:
fail_get_gpio:
fail_find_node:
	return ret;
}

static int __init shanwuyan_init(void)	//驅動入口函數
{
    int ret = 0;
	
    /*1.分配設備號*/
    ...

    /*2.向內核添加字符設備*/
    ...

    /*3.自動創建設備節點*/
    ...

    /*4.初始化GPIO*/
    leds_init(&shanwuyan);

    return 0;
}

static void __exit shanwuyan_exit(void)	//驅動出口函數
{
    int i = 0;
    /*釋放GPIO*/
    for(i = 0; i < LEDS_NUM; i++)
          gpio_free(shanwuyan.led_gpios[i]);
    /*注銷設備號*/
    ...
    /*摧毀設備*/
    ...
    /*摧毀類*/
    ...
}
...

  (4) 然后一下open函數,因為該函數有一個參數我們一直沒有使用,現在我們使用它,close函數無需改造。如下方代碼。

...
/*打開設備*/
static int shanwuyan_open(struct inode *inode, struct file *filp)
{
    printk(KERN_EMERG "shanwuyan_open\r\n");
    filp->private_data = &shanwuyan;	//在設備操作集中,我們盡量使用私有數據來操作對象
    return 0;
}
...

  (5) 最后改造write函數(用戶程序控制GPIO,只需要用到write函數,用不到read函數,所以不用改造read函數)。如下方代碼。

...
/*寫設備*/
static ssize_t shanwuyan_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
    int i = 0;
    int ret = 0;
    char user_data;	//保存用戶數據
	
    struct shanwuyan_dev *led_dev = filp->private_data;	//獲取私有變量
	
    ret = copy_from_user(&user_data, buf, count);	//獲取用戶數據
    if(ret < 0)
          return -EINVAL;

    if(user_data == LEDS_ON)	//如果接到的命令是打開LED
          for(i = 0; i < LEDS_NUM; i++)
                gpio_set_value(led_dev->led_gpios[i], LEDS_ON);
    else if(user_data == LEDS_OFF)	//如果接到的命令是關閉LED
          for(i = 0; i < LEDS_NUM; i++)
                gpio_set_value(led_dev->led_gpios[i], LEDS_OFF);

    return 0;
}
...

4.修改用戶程序

  用戶程序只用得到寫操作,可以把讀操作的代碼刪除。再另外修改一下寫操作的代碼,如下。

//源代碼名稱為 "shanwuyanAPP.c"
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

/*
*argc:應用程序參數個數,包括應用程序本身
*argv[]:具體的參數內容,字符串形式
*./shanwuyanAPP <filename> <0:1> 0表示LED滅,1表示LED亮
*/
int main(int argc, char *argv[])
{
    int ret = 0;
    int fd = 0;
    char *filename;

    char user_data;
	
    user_data = atoi(argv[2]);

    if(argc != 3)
    {
          printf("Error usage!\r\n");
          return -1;
    }
		
    filename = argv[1];	//獲取設備名稱

    fd = open(filename, O_RDWR);
    if(fd < 0)
    {
          printf("cannot open device %s\r\n", filename);
          return -1;
    }

    /*寫操作*/
    ret = write(fd, &user_data, sizeof(char));
    if(ret < 0)
          printf("write error!\r\n");

    /*關閉操作*/
    ret = close(fd);
    if(ret < 0)
    {
          printf("close device %s failed\r\n", filename);
    }

    return 0;
}

5.應用

  編譯驅動,交叉編譯用戶程序,拷貝到開發板中。
  加載驅動,可以看到開發板上的LED燈亮了起來。

  同時可以在終端看到兩個LED的GPIO編號。

  在終端輸入命令./shanwuyanAPP /dev/shanwuyan_leds 0,可以看到兩個LED燈滅掉。

  再在終端輸入命令./shanwuyanAPP /dev/shanwuyan_leds 1,可以看到兩個LED燈又亮起來。

  本文全部代碼在這里


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM