linux驅動之LED驅動

澆築菜鳥發表於2021-07-16

通過之前的學習,瞭解到linux驅動編寫的流程是:先通過註冊函式註冊我們編寫的入口函式,然後在入口函式中獲取裝置號->註冊字元裝置->自動建立裝置節點->獲取裝置樹資訊,最後通過銷燬函式將出口函式中需要釋放的資源進行釋放,想知道具實現的小夥伴可以檢視我之前的文章。完成之前的學習,這篇文章所涉及的知識就比較簡單了,現在我們開始led驅動的學習。

一、準備材料

開發環境:VMware
作業系統:ubuntu
開發版:湃兔i2S-6UB
庫檔案:linux開發板或ubuntu的核心原始碼

二、GPIO原理圖

我用的是i2C-6ULX-B開發版,想要了解更多開發版的資訊可以檢視i2C-6ULX-B開發套件,外觀如下圖所示:

通過湃兔官方提供的原理圖,可以知道開發板上的兩個LED,有一個是電源指示燈通電就亮,所以我們能使用的只有一個,具體如下圖所示:

從原理圖中可知led是低電平亮,高電平熄,然後接著檢視湃兔核心板的引腳圖,如下圖所示:

最後在檢視湃兔官方提供的引腳定義,具體如下圖所以:

現在不用我多說小夥伴們都知道i2C-6ULX-B開發版上的led燈接的是晶片的gpio5.IO[5],在Linux中的GPIO計算方法是,GPIO_num = (<imx6ul_gpio_port> - 1) * 32 + <imx6ul_gpio_pin>,所以湃兔i2C-6ULX-B開發板的led接的是133引腳。

三、GPIO配置

瞭解led的硬體原理後,需要在裝置樹中進行配置,有需要的小夥伴可以瞭解湃兔官方的GPIO配置教程,好吧說得比較簡單,沒有學習過裝置樹的小夥伴可能看不懂,需要的可以百度搜尋下相關教程。在配置之前我們還需要了解一下什麼是GPIO子系統和pinctrl子系統,需要的朋友可以瞭解一下gpio子系統和pinctrl子系統(上)
好吧,趕緊回來扯遠了,看不明白不要緊,我們主要是先實踐再理論,只學理論知識可能很讓人絕望啊,等用多了,再回頭學習自然就明白了。現在我們開始在裝置樹中配置gpio,開啟'arch/arm/boot/dts'目錄下的'i2c6ulxb-i2s6ull-emmc.dtsi'檔案,在跟節點中新增如下資訊

dtsled{
		compatible = "i2som,gpioled";
		pinctrl-names = "default";
		pinctrl-0 = <&pinctrl_dtsled>;
		led-gpios = <&gpio5 5 GPIO_ACTIVE_LOW>;
		status = "okay";
	};

如下圖所示:

細心的小夥伴可以已經看出來了,去註釋了一行資訊,因為湃兔的這個開發板只有一個led燈,然後被系統用於心跳燈使用,為了更好的驗證,所以我們把系統使用的心跳燈給註釋了,如下圖所示:

最後在'iomuxc_snvs'這個節點中新增如下資訊:

pinctrl_dtsled: dtsled {
			fsl,pins = <
				MX6ULL_PAD_SNVS_TAMPER5__GPIO5_IO05	0x1b0b0
			>;
		};

如下圖所示:

到此我們的裝置樹已經更改完成了,接下來編寫驅動程式。

四、led驅動程式

其他的函式我就不過多介紹了,有需要的小夥伴可以檢視我直接的文章,我使用led的驅動函式是如下所示

/* 獲取GPIO */
int of_get_named_gpio(struct device_node *np, const char *propname, int index)
/* 檢查gpio number是否合法 */
int gpio_to_irq(unsigned gpio)
/* 申請IO */
int gpio_request(unsigned gpio, const char *label)
/* 釋放IO */
void gpio_free(unsigned gpio)
/* 設定gpio 為輸入*/
int gpio_direction_input(unsigned gpio)
/* 設定IO為輸出模式 */
int gpio_direction_output(unsigned gpio, int value)
/* 設定IO輸出電平 */
gpio_set_value(unsigned gpio, int value)
/* 設定gpio的消抖時間 */
int gpio_set_debounce(unsigned gpio, unsigned debounce)
/* 獲取gpio對應的中斷線路 */
int gpio_to_irq(unsigned gpio)
/* gpio中斷 */
int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char * name, void * dev)

相信這些函式都不用過多的介紹,小夥伴們應該都知道怎麼使用了,如想了解具體的引數含義可以檢視Linux 驅動學習筆記 - gpio 子系統 (八)這篇文章,接下來開始編寫原始碼。

五、程式原始碼

驅動dtsled.c檔案

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/gpio.h>
#include <linux/of_gpio.h>

#define DTSLED_NAME     "dtsled"
#define DTSLED_COUNT    1
#define LEDOFF          0
#define LEDON           1

/*裝置結構體*/
struct dtsled_dev{
    dev_t devid;                /* 裝置號 */
    int major;                  /* 主裝置號 */
    int minor;                  /* 次裝置號 */
    struct cdev cdev;           /* 字元裝置 */
    struct class *class;        /* 類結構體 */
    struct device *device;      /* 裝置 */
    struct device_node *nd;     /* 裝置節點 */
    int gpio_number;            /* gpio的編號 */
};

struct dtsled_dev dtsled;

static int dtsled_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &dtsled;
    return 0;
}

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

static ssize_t dtsled_write(struct file *filp, const char __user *buf, size_t count, loff_t *ppos)
{
    int ret = 0;
    unsigned char databuf[1];
    struct dtsled_dev *dev = filp->private_data;

    ret = copy_from_user(databuf, buf, count);
    if (ret < 0) {
        return -EINVAL;
    }

    if (databuf[0] == LEDON) {
        gpio_set_value(dev->gpio_number, 0);
    } else if (databuf[0] == LEDOFF) {
        gpio_set_value(dev->gpio_number, 1);
    }

    return 0;
}

/*
 * 字元裝置操作集合
 */
static const struct file_operations dtsled_fops = {
    .owner = THIS_MODULE,
    .open = dtsled_open,
    .release = dtsled_release,
    .write = dtsled_write,
};

/*
 * 模組入口
 */
static int __init dtsled_init(void)
{
    int ret = 0;

    printk("dtsled_init\r\n");

    /* 申請裝置號 */
    dtsled.major = 0;   /* 設定裝置號由記憶體分配 */
    if (dtsled.major){
    	dtsled.devid = MKDEV(dtsled.major, 0);
    	ret = register_chrdev_region(dtsled.devid, DTSLED_COUNT, DTSLED_NAME);
    } else {
    	ret = alloc_chrdev_region(&dtsled.devid, 0, DTSLED_COUNT, DTSLED_NAME);
    	dtsled.major = MAJOR(dtsled.devid);
    	dtsled.minor = MINOR(dtsled.devid);
    }
    if (ret < 0) {
    	printk("dtsled chrdev_region err!\r\n");
    	goto fail_devid;
    }
    
    /* 註冊字元裝置 */
    dtsled.cdev.owner = dtsled_fops.owner;
    cdev_init(&dtsled.cdev, &dtsled_fops);
    ret = cdev_add(&dtsled.cdev, dtsled.devid, DTSLED_COUNT);
    if (ret < 0) {
        goto fail_cdev;
    }

    /* 自動建立裝置節點 */
    dtsled.class = class_create(dtsled_fops.owner, DTSLED_NAME);
    if (IS_ERR(dtsled.class)) {
        ret = PTR_ERR(dtsled.class);
        goto fail_class;
    }

    dtsled.device = device_create(dtsled.class, NULL, dtsled.devid, NULL, DTSLED_NAME);
    if (IS_ERR(dtsled.device)) {
        ret = PTR_ERR(dtsled.device);
        goto fail_device;
    }

    /* 獲取裝置樹的屬性內容 */
    dtsled.nd = of_find_node_by_path("/dtsled");
    if (dtsled.nd == NULL) {
        ret = -EINVAL;
        goto fail_findnd;
    }

    /* 獲取GPIO */
    dtsled.gpio_number = of_get_named_gpio(dtsled.nd, "led-gpios", 0);
    if (dtsled.gpio_number < 0) {
        printk("can't find led gpio");
        ret = -EINVAL;
        goto fail_rs;
    } else {
        printk("led gpio num = %d\r\n", dtsled.gpio_number);
    }

    /* 申請IO */
    ret = gpio_request(dtsled.gpio_number, "led-gpio");
    if (ret < 0) {
        printk("failde to request the led gpio\r\n");
        ret = -EINVAL;
        goto fail_rs;
    }

    /* 設定IO為輸出模式 */
    ret = gpio_direction_output(dtsled.gpio_number, 1);
    if (ret < 0) {
        ret = -EINVAL;
        goto fail_setoutput;
    }

    /* 設定IO預設輸出低電平 */
    gpio_set_value(dtsled.gpio_number, 0);

    return 0;


fail_setoutput:
    gpio_free(dtsled.gpio_number);
fail_rs:
fail_findnd:
    device_destroy(dtsled.class, dtsled.devid);
fail_device:
    class_destroy(dtsled.class);
fail_class:
    cdev_del(&dtsled.cdev);
fail_cdev:
    unregister_chrdev_region(dtsled.devid, DTSLED_COUNT);
fail_devid:
    return ret;

}

/*
 * 模組出口
 */
static void __exit dtsled_exit(void)
{
    printk("dtsled_exit\r\n");

    /* 關閉led */
    gpio_set_value(dtsled.gpio_number, 1);

    /* 刪除字元裝置 */
    cdev_del(&dtsled.cdev);

    /* 釋放字元設號 */
    unregister_chrdev_region(dtsled.devid, DTSLED_COUNT);

    /* 摧毀裝置 */
    device_destroy(dtsled.class, dtsled.devid);

    /* 摧毀類 */
    class_destroy(dtsled.class);

    /* 釋放IO */
    gpio_free(dtsled.gpio_number);
}

/*
 * 模組註冊入口
 */
module_init(dtsled_init);
/*
 * 模組註冊出口
 */
module_exit(dtsled_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("jiaozhu");

Makefile檔案

KERNELDIR := /home/xfg/linux/imx_7ull/i2x_6ub/system_file/i2SOM-iMX-Linux
 
CURRENT_PATH := $(shell pwd)
obj-m := dtsled.o

build: kernel_modules

kernel_modules:
    $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules
clean:
    $(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

ledApp.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[]:具體的引數內容,字串形式
 *./ledApp <filename> <0:1> 0表示關,1表示開 
 *./ledApp /dev/dtsled 0    關閉led燈
 *./ledApp /dev/dtsled 1    開啟led燈
 * */

#define LEDOFF      0
#define LEDON       1

int main(int argc, char *argv[])
{
    int ret = 0;
    int fd = 0;
    char *filename;
    unsigned char databuf[1];

    if(argc !=3) {
        printf("Instruction usage error!!!\r\n");
        printf("./ledApp <filename> <0:1> 0表示關,1表示開\r\n");
        printf("./ledApp ./dev/dtsled 1 \r\n");
        return -1;
    }

    filename = argv[1];

    fd = open(filename, O_RDWR);
    if(fd < 0) {
        printf("open file %s failed\r\n", filename);
    }

    databuf[0] = atoi(argv[2]);
    ret = write(fd, databuf, sizeof(databuf));
    if(ret < 0) {
        printf("write file %s failed\r\n", filename);
    }

    ret =close(fd);
    if(ret <0) {
        printf("close file %s falied!\r\n", filename);
    }

    return 0;
}

六、測試

1.裝置樹測試

回到核心原始碼的根目錄下重新編譯裝置樹檔案,編譯命令

make dtbs

編譯完成後將/arch/arm/boot/dts目錄下的i2c6ulxb-i2s6ull-emmc.dtb拷貝至tftp伺服器下,然後重啟開發版即可,我使用的是nfs掛載根檔案系統的方式進行測試的,當然也可以直接更新開發板中的裝置樹,只是這樣比較麻煩,開發中不建議這麼操作。
重新啟動後,進入/proc/device-tree/目錄下檢視我們更改的節點資訊是否存在,如下圖所示:

可知我們新增的dtsled節點資訊是確定的,接下來測試驅動。

1.驅動測試

將應用檔案和驅動檔案編譯後拷貝到開發版的/lib/modules/4.1.43/目錄下,掛載led驅動,如下圖所示:

掛載成功後可以看到我們申請的gpio引腳編號是133,最後通過應用測試led是否能開啟和關閉,如下圖所示:

如果能正常開啟和關閉led燈,說明我們的驅動和應用程式沒有問題,若有寫得不好的地方,望各位大佬指出。

參考文獻

gpio子系統和pinctrl子系統(上):https://www.cnblogs.com/rongpmcu/p/7662751.html
Linux 驅動學習筆記 - gpio 子系統 (八):https://blog.csdn.net/tyustli/article/details/105484666

相關文章