Linux LED驱动

admin2024-07-03  5

Linux 驱动入门 LED驱动

文中使用是的韦东山老师的IMX6ULL_mini 开发板,下面总结至韦东山老师驱动实验班。
点亮LED时,首先需要知道我们控制哪一个引脚才可以操作LED,通过查看原理图可以发现,LED是由GPIO5_3控制。

Linux LED驱动,在这里插入图片描述,第1张

在 Linux 中,GPIO 的标识和控制通常是通过引脚号来进行的,引脚号是用于唯一标识特定的 GPIO 引脚。

IMX6ULL引脚号获取

在开发板中输入下面指令获取引脚映射表

cat /sys/kernel/debug/gpio

Linux LED驱动,在这里插入图片描述,第2张

i.max6ull开发板的GPIO5就是 gpiochip4(不同厂家开发板可能不同),gpiochip4起始引脚号为128,因此GPIO5_3的引脚号就是128+3 = 131;

GPIO子系统函数

​ Linux规定了,不管你是什么芯片,你只要想跑Linux,就必须统一接口。Linux的GPIO子系统中可以通过如下函数配置GPIO。

int gpio_request(unsigned gpio, const char *label);
void gpio_free(unsigned gpio);
int gpio_direction_input(unsigned gpio);
int gpio_direction_output(unsigned gpio, int value);
int gpio_get_value(unsigned gpio);
void  gpio_set_value(unsigned gpio, int value);

gpio_request(unsigned gpio, const char *label)

(1)作用: 向Linux 内核中用于请求申请一个 GPIO 引脚的函数。如果我们想对一个引脚进行操作,需要最先调用 gpio_request()这个函数。
(2)gpio : 要请求的 GPIO 引脚号。这个引脚号可以自己直接给出(比如上面花了那么多篇幅讲解的)。还可以通过 of_get_named_gpio 函数从设备树获取指定 GPIO 属性信息(设备树的内容,后面会讲解,这里留个影响即可)。
(3)label : 给GPIO起一个名字,因为直接一个引脚号不方便人阅读,所以可以给这个引脚号起一个名字。随便起名字,只要你自己喜欢,不影响。
(4)返回值 : 如何返回0,表示申请GPIO成功。如果返回负数,表示申请GPIO出现错误。

gpio_free(unsigned gpio)

(1)作用 : 如果不使用某个 GPIO 了, 那么就需要调用 gpio_free 函数进行释放。
(2)gpio : 要释放的GPIO引脚号。与gpio_request的GPIO引脚号是同一个东西。
(3)返回参数 : 无

gpio_direction_input(unsigned gpio)

(1)作用 : 将GPIO配置为输入方向。申请完GPIO之后,需要根据需求配置为输入或者输出,这个函数可以将GPIO设置为输入
(2)gpio : 要设置为输入的GPIO 引脚号
(3)返回参数 : 返回 0,表示成功将 GPIO 引脚设置为输入模式。返回负数,表示出错或无法设置 GPIO 引脚。

gpio_direction_output(unsigned gpio, int value)

(1)作用 : 将GPIO配置为输出方向,并且设置默认输出值。申请完GPIO之后,需要根据需求配置为输入或者输出,这个函数可以将GPIO设置为输出
(2)gpio : 设置为输出的GPIO 引脚号
(3)value : GPIO 默认输出值。如果GPIO初始化成功之后,默认输出的电压。
(4)返回参数 : 返回 0,表示成功将 GPIO 引脚设置为输出模式。返回负数,表示出错或无法设置 GPIO 引脚。

gpio_get_value(unsigned gpio)

(1)作用 : 获取指定GPIO的电平信息
(2)gpio : 要获取电平值的GPIO标号
(3)返回参数 : 获取电平信息成功,高电平返回1,低电平返回0。GPIO电平获取失败返回负值。

gpio_set_value(unsigned gpio, int value)

(1)作用 : 设置指定GPIO的电平值
(2)gpio : 要设置指定GPIO的电平值
(3)value : 要设置的电平值,如果传入0,则表示将GPIO设置为低电平。传入一个非0值,表示将GPIO设置为高电平
(4)返回参数 : 无

应用层程序

#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <poll.h>
#include <signal.h>

/* 可执行文件名   | 表示要操作哪一盏灯(<>表示参数不可省略)  | 灯状态
 * ./led_test    |   <0|1|2|..>        | on     |硬件上开灯
 * ./led_test    |   <0|1|2|..>        | off    |硬件上关灯
 * ./led_test    |   <0|1|2|..>        |        |读取led状态
 */
static int fd;
                      
int main(int argc, char **argv)
{
	int ret;
	char buf[2];

	int i;
	
	/* 1. 判断参数 */
	if (argc < 2) 
	{
		printf("Usage: %s <0|1|2|...> [on | off]\n", argv[0]);
		return -1;
	}
	/* 2. 打开文件 */
    /*打开文件,因为在驱动层中,device_create()函数创建的设备节点名字叫做100ask_led,而设备节点都存放在/dev目录下,所以这里是/dev/100ask_led*/
	fd = open("/dev/100ask_led", O_RDWR);
	if (fd == -1)
	{
		printf("can not open file /dev/100ask_led\n");
		return -1;
	}

	if (argc == 3)//传入了三个参数,表示写入
	{
		/* write */
		buf[0] = strtol(argv[1], NULL, 0);
		/*strtol()函数是将字符转换为数字。我们在命令行中输入的1,其实是字符1,而不是数字1*/
		if (strcmp(argv[2], "on") == 0)
        /*strcmp()函数,用于判断两个字符串是否相等,如果相等返回0,如果不相等返回一个非0值 */
			buf[1] = 0;
		else
			buf[1] = 1;
		ret = write(fd, buf, 2);
	}
	else //表示读取电平信息
	{
		buf[0] = strtol(argv[1], NULL, 0);
		ret = read(fd, buf, 2);
		if (ret == 2)
		{
			printf("led %d status is %s\n", buf[0], buf[1] == 0 ? "on" : "off");
		}
	}
	close(fd);
	return 0;
}

驱动层程序

驱动程序编写流程

1.确定主设备号,也可以让内核分配(用于让系统根据设备号判断是哪一个驱动)。
2.编写file_operations结构体,用于管理驱动程序。
3.首先file_operations中对应的drv_open/drv_read/drv_write 等函数,在应用层调用open,write,read等函数时,就是对应调用file_operations中对应函数。
4.把 file_operations结构体告诉内核: register_chrdev(注册)。谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数。
5.有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用 unregister_chrdev。
6.其他完善:提供设备信息,自动创建设备节点: class_create, device_create。

#include "asm-generic/errno-base.h"
#include "asm-generic/gpio.h"
#include "asm/uaccess.h"
#include <linux/module.h>
#include <linux/poll.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 <linux/gpio/consumer.h>
#include <linux/platform_device.h>
#include <linux/of_gpio.h>
#include <linux/of_irq.h>
#include <linux/interrupt.h>
#include <linux/irq.h>
#include <linux/slab.h>
#include <linux/fcntl.h>
#include <linux/timer.h>
//描述引脚
struct gpio_desc{
	int gpio;
	int irq;  //中断,没有使用
    char *name;
    int key; //按键,没有使用
	struct timer_list key_timer;//定时器 ,没有使用
} ;
//存放引脚编号。名字
static struct gpio_desc gpios[2] = {
    {131, 0, "led0", },
    //{132, 0, "led1", },
};

/* 主设备号                                                                 */
static int major = 0;
static struct class *gpio_class;//一个类,用于创建设备节点

/* 实现对应的open/read/write等函数,填入file_operations结构体                   */
static ssize_t gpio_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset)
{
	char tmp_buf[2];//存放驱动层和应用层交互的信息
	int err;
    int count = sizeof(gpios)/sizeof(gpios[0]);//记录定义的最大引脚数量
	// 如果传入的值不是两个返回错误
	if (size != 2)
		return -EINVAL;
	//驱动层从用户层得到数据
    //tmp_buf : 驱动层数据
    //buf : 应用层数据
    // 1:数据长度为1个字节,因为控制LED,一个字节够用
	err = copy_from_user(tmp_buf, buf, 1);
	// 第0项表示要操作哪一个LED,如果操作的LED超出count,表示失败
	if (tmp_buf[0] >= count)
		return -EINVAL;
	//读取引脚电平
	tmp_buf[1] = gpio_get_value(gpios[tmp_buf[0]].gpio);

    // 驱动层发送数据到应用层
    // buf:应用层数据
    // tmp_buf:驱动层数据
	err = copy_to_user(buf, tmp_buf, 2);
	
	return 2;
}

static ssize_t gpio_drv_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
    unsigned char ker_buf[2];
    int err;

    if (size != 2)
        return -EINVAL;

    err = copy_from_user(ker_buf, buf, size);
    
    if (ker_buf[0] >= sizeof(gpios)/sizeof(gpios[0]))
        return -EINVAL;
    
	//设置gpios[ker_buf[0]].gpio引脚电平
    gpio_set_value(gpios[ker_buf[0]].gpio, ker_buf[1]);
    return 2;    
}

/* 定义自己的file_operations结构体  */
static struct file_operations gpio_key_drv = {
	.owner	 = THIS_MODULE,
	.read    = gpio_drv_read,
	.write   = gpio_drv_write,
};


/* 在入口函数 */
static int __init gpio_drv_init(void)
{
    int err;
    int i;
    int count = sizeof(gpios)/sizeof(gpios[0]);
    /*__FILE__ :表示文件
	 *__FUNCTION__ :当前函数名
	 *__LINE__ :在文件的哪一行
	 */
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	
	for (i = 0; i < count; i++)
	{		
		//申请指定GPIO引脚,申请的时候需要用到名字
		err = gpio_request(gpios[i].gpio, gpios[i].name);
        //如果返回值小于0,表示申请失败
		if (err < 0) {
			printk("can not request gpio %s %d\n", gpios[i].name, gpios[i].gpio);
			return -ENODEV;
		}
		/* 设置为输出引脚 */
		gpio_direction_output(gpios[i].gpio, 1);
	}

	/* 注册file_operations 	*/
	major = register_chrdev(0, "100ask_led", &gpio_key_drv);  /* /dev/gpio_desc */
    
	/*创建类,为THIS_MODULE模块创建一个类*/
	gpio_class = class_create(THIS_MODULE, "100ask_led_class");
    //如果返回错误,注销驱动程序
	if (IS_ERR(gpio_class)) {
		printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
		unregister_chrdev(major, "100ask_led_class");
		return PTR_ERR(gpio_class);
	}
	//类创建成功后,创建设备
	/*输入参数是逻辑设备的设备名,即在目录/dev目录下创建的设备名
	 *参数一 : 在gpio_class类下面创建设备
	 *参数二 : 无父设备的指针
	 *参数三 : 主设备号+次设备号
	 *参数四 : 没有私有数据
	*/
	device_create(gpio_class, NULL, MKDEV(major, 0), NULL, "100ask_led"); 
    /* /dev/100ask_gpio */
	
	return err;
}

/* 有入口函数就应该有出口函数:卸载驱动程序时,就会去调用这个出口函数
 */
static void __exit gpio_drv_exit(void)
{
    int i;
    int count = sizeof(gpios)/sizeof(gpios[0]);
    
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	//销毁gpio_class类下面的设备节点
	device_destroy(gpio_class, MKDEV(major, 0));
    //销毁gpio_class类
	class_destroy(gpio_class);
    //注销驱动
	unregister_chrdev(major, "100ask_led");

	for (i = 0; i < count; i++)
	{
        //释放gpio
		gpio_free(gpios[i].gpio);		
	}
}
/* 7. 其他完善:提供设备信息,自动创建设备节点                                     */
module_init(gpio_drv_init); //入口函数
module_exit(gpio_drv_exit); //出口函数
MODULE_LICENSE("GPL");

上机试验

驱动程序和应用程序编写完成后,执行make命令编译,Makefile文件如下

# 1. 使用不同的开发板内核时, 一定要修改KERN_DIR
# 2. KERN_DIR中的内核要事先配置、编译, 为了能编译内核, 要先设置下列环境变量:
# 2.1 ARCH,          比如: export ARCH=arm64
# 2.2 CROSS_COMPILE, 比如: export CROSS_COMPILE=aarch64-linux-gnu-
# 2.3 PATH,          比如: export PATH=$PATH:/home/book/100ask_roc-rk3399-pc/ToolChain-6.3.1/gcc-linaro-6.3.1-2017.05-x86_64_aarch64-linux-gnu/bin 
# 注意: 不同的开发板不同的编译器上述3个环境变量不一定相同,
#       请参考各开发板的高级用户使用手册

KERN_DIR =  /home/book/100ask_imx6ull_mini-sdk/Linux-4.9.88 # 板子所用内核源码的目录
all:
	make -C $(KERN_DIR) M=`pwd` modules 
	$(CROSS_COMPILE)gcc -o led_test led_test.c
clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order  led_test

# 参考内核源码drivers/char/ipmi/Makefile
# 要想把a.c, b.c编译成ab.ko, 可以这样指定:
# ab-y := a.o b.o
# obj-m += ab.o
obj-m += led_drv.o


执行以下命令,将Ubuntu(192.168.5.11)/home/book/nfs_rootfs中程序挂载到开发板的mnt目录下

mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt

然后进入led驱动目录下,执行 insmod led_drv.ko 命令装载驱动
Linux LED驱动,在这里插入图片描述,第3张

如果装载驱动后没有打印信息,可以执行 echo “7 4 1 7” > /proc/sys/kernel/printk 打开内核打印。执行 ls /dev/100ask_led 就可以看到当前设备节点
Linux LED驱动,在这里插入图片描述,第4张

如果驱动使用完毕之后,可以使用 rmmod led_drv.ko 指令卸载驱动。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明原文出处。如若内容造成侵权/违法违规/事实不符,请联系SD编程学习网:675289112@qq.com进行投诉反馈,一经查实,立即删除!