我是靠谱客的博主 鲤鱼路灯,最近开发中收集的这篇文章主要介绍基于Jetson Agx Xavier的驱动开发----------一个字符设备驱动(多设备驱动)前言一、字符设备驱动开发的主要流程二、驱动程序的编写三、驱动编译总结,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

文章目录

  • 前言
  • 一、字符设备驱动开发的主要流程
  • 二、驱动程序的编写
    • 1.驱动程序的组成
      • 1.1.驱动入口程序
      • 1.2.驱动出口函数
      • 1.3.设备操作函数
    • 2.驱动入口函数
      • 2.1.设备注册
      • 2.2 创建设备
      • 2.3 自动创建设备节点
    • 3.驱动出口函数
    • 4.设备操作函数
      • 4.1 gpio函数
      • 4.2 file_operation结构体与实现
  • 三、驱动编译
    • 1 编写udev规则文件
    • 2 编写makefile文件
    • 3 编译过程
    • 4 测试驱动是否成功
    • 5 卸载模块
  • 总结


前言

这段时间学习驱动开发,手里只有一块jetson开发板,但是所有驱动开发教程都是针对于特定的学习板,针对于jetson开发板没有,虽然他们的开发流程可以借鉴,但是有许多地方不一样,在此记录一下开发流程和踩到的坑,如需具体理论知识,请参考正点原子的驱动开发教程/PS之前以为正点原子只是在裸板开发上的教程特别好,这段时间看了他们嵌入式linux驱动开发指南,不得不说,是真的好,建议像我这样的小白去学习一下,有钱最好买他们的板子学习/。


一、字符设备驱动开发的主要流程

linux驱动有两种开发方式,一种是将驱动编译到Linux内核中,当Linux启动时,就会自动运行驱动程序,另一种是将驱动编译成模块,然后加载该驱动模块到内核中,至于两种的具体特点这里就不细说了,我这里主要采用的是第二种,首先编写驱动程序,其次进行编译,最后将驱动模块加载到内核中。

二、驱动程序的编写

1.驱动程序的组成

驱动程序一般由以下三个部分组成:

1.1.驱动入口程序

主要工作是写驱动注册函数,创建设备。

static int __init xxx_init(void){
/*入口函数,包括驱动注册等函数*/
}
module_init(xxx_init);

1.2.驱动出口函数

主要工作是写驱动注销函数,销毁设备。

static int __init xxx_exit(void){
/*出口函数,包括驱动注销等函数*/
}
module_exit(xxx_exit);

1.3.设备操作函数

主要工作是写实现file_operations里面的操作函数。

static struct file_operations mydev_fops = { 
	.owner = THIS_MODULE, 
	.open = dev_open, 
	.read = dev_read, 
	.write = dev_write,
	.release = dev_release, 
};

2.驱动入口函数

2.1.设备注册

一些老的教程会提到register_chrdev这个函数进行字符设备的注册,使用该函数进行字符设备的注册时,在驱动模块加载成功后还需要手动使用mknod命令创建命令节点,但是这个函数是老版本驱动使用的函数,现在Linux内核推荐使用新的字符设备驱动API函数:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
int register_chrdev_region(dev_t from, unsigned count, const char *name)

上面的那个函数是没有指定设备号的情况下使用的,下面的函数时在指定设备号的情况下使用的,我主要使用第一个函数,不用查找哪些哪些设备号被使用了,下面主要介绍一下第一个函数的主要参数:参数dev是设备的设备号,其高12位为主设备号,低20位为次设备号,参数baseminor是所需要注册的设备的起始次设备号,参数count是设备个数,参数name是注册设备的设备名。

2.2 创建设备

首先使用cdev_init函数初始化,该函数的原型为:

void cdev_init(struct cdev *cdev, const struct file_operations *fops)

参数cdev上述已说,参数fops就是字符操作函数的集合,这个在第三部分会细讲。
接下来使用cdev_add添加字符设备,该函数的原型为:

int cdev_add(struct cdev *p, dev_t dev, unsigned count)

2.3 自动创建设备节点

如果不添加自动创建设备节点,在加载驱动程序后,还需要使用mknod进行手动创建设备节点,下面介绍自动创建设备节点,在驱动加载成功后,就能在/dev下面创建对应的设备。
首先需要创建一个类,class_create是类创建函数,是个宏定义,该宏在device.h头文件下,内容如下:

#define class_create(owner, name)		
({						
	static struct lock_class_key __key;	
	__class_create(owner, name, &__key);	
})

接下来需要创建一个设备,该函数原型如下:

struct device *device_create(struct class *class, struct device *parent,
			     dev_t devt, void *drvdata, const char *fmt, ...)

到这里驱动入口函数就结束了。
最后贴出驱动入口函数的代码,该函数将注册多个gpio设备(这里用到了几个函数,比如gpio_request等,这里再设备操作函数会讲,这也是其他教程中都没有的):

#define GPIO_OP0 251	 // PIN29
#define GPIO_OP1 250	 // PIN31
/*定义主设备号和次设备号*/
#define DEV_MAJOR 0
#define DEV_MINOR 0
#define REG_GPIO_NAME "Jetson GPIO"
/*定义gpio数量,为了方便加入iic和spi设备驱动,所以加了一个宏定义*/
#define NUM_DEV_GPIO 2
#define NUM_DEV_TOTAL NUM_DEV_GPIO
/*设备名*/
#define DEVNAME_GPIO "mygpio"
#define DRIVER_NAME "mygpios"
/*创建gpios类,方便自动创建设备节点*/
static struct class *class_opgpio = NULL;
static struct mutex lock;
static int _major_opgpio = DEV_MAJOR;
static int _minor_opgpio = DEV_MINOR;
/*设备号数组*/
static struct cdev *cdev_array = NULL;
/*设备号索引*/
static volatile int cdev_index = 0;
/*gpio注册函数*/
static int mygpio_register_dev(void)
{
	int retval;
	dev_t dev;
	dev_t devno;
	int i;
	/*没有定义设备号,申请设备号*/
	retval = alloc_chrdev_region(&dev, DEV_MINOR, NUM_DEV_GPIO, DEVNAME_GPIO);

	if (retval < 0) {
		printk(KERN_ERR "%s: alloc_chrdev_region failed.n", __func__);
		return retval;
	}
	/*主设备号*/
	_major_opgpio = MAJOR(dev);
	/*创建设备*/
	class_opgpio = class_create(THIS_MODULE, DEVNAME_GPIO);
	if (IS_ERR(class_opgpio)) {
		return PTR_ERR(class_opgpio);
	}
	/*存在多个gpio次设备,循环进行初始化cdev,添加dev*/
	for (i = 0; i < NUM_DEV_GPIO; i++) {
		devno = MKDEV(_major_opgpio, _minor_opgpio + i);
		//初始化cdev
		cdev_init(&(cdev_array[cdev_index]), &mygpios_fops);
		cdev_array[cdev_index].owner = THIS_MODULE;
		//添加一个cdev
		if (cdev_add(&(cdev_array[cdev_index]), devno, 1) < 0) {
			printk(KERN_ERR "%s: cdev_add failed minor = %dn",
			       __func__, _minor_opgpio + i);
		} else {
			device_create(class_opgpio, NULL, devno, NULL,
				      DEVNAME_GPIO "%u", _minor_opgpio + i);
		}
		cdev_index++;
	}
	return 0;
}

static int __init mygpio_init(void)
{
	int retval = 0;
	int registered_devices = 0;
	size_t size;
	printk(KERN_INFO "%s: loading %d devices...n", DRIVER_NAME,
	       NUM_DEV_TOTAL);
	mutex_init(&lock);
	if (!gpio_is_valid(GPIO_OP0)) {
		printk(KERN_INFO "GPIO: invalid LED0 GPIOn");
		return -ENODEV;
	}
	if (!gpio_is_valid(GPIO_OP1)) {
		printk(KERN_INFO "GPIO: invalid LED1 GPIOn");
		return -ENODEV;
	}
	//请求gpio
	retval = gpio_request(GPIO_OP0, "sysfs");
	retval = gpio_request(GPIO_OP1, "sysfs");
    //设置gpio为输出
	retval = gpio_direction_output(GPIO_OP0, 0);
	retval = gpio_export(GPIO_OP0, 0);
	retval = gpio_direction_output(GPIO_OP1, 0);
	retval = gpio_export(GPIO_OP1, 0);
	size = sizeof(struct cdev) * NUM_DEV_TOTAL;
	cdev_array = (struct cdev *)kmalloc(size, GFP_KERNEL);
	retval = mygpio_register_dev();
	if (retval != 0) {
		printk(KERN_ALERT "%s: led driver register failed.n",
		       DRIVER_NAME);
		return retval;
	}
	printk(KERN_INFO "%s: %d devices loaded.n", DRIVER_NAME,
	       registered_devices + NUM_DEV_TOTAL);
	
	return 0;
}

3.驱动出口函数

驱动出口函数主要是负责对设备进行注销和创建的类进行销毁,这里和驱动入口函数比较类似,就不细讲了,需要注意的是,这里和c++中类的析构类似,注意注销和销毁顺序,由于是先注册后创建类,这里最好先销毁类再注销设备,所用的函数如下:

//注销设备
void unregister_chrdev_region(dev_t from, unsigned count)
//销毁类
void cdev_del(struct cdev *p)

最后贴出驱动出口函数的代码

static void __exit mygpio_exit(void)
{
	int i;
	dev_t devno;
	dev_t devno_top;

	printk(KERN_DEBUG "%s: removing %d cdev(s).n", DRIVER_NAME,
	       NUM_DEV_TOTAL);
	for (i = 0; i < NUM_DEV_TOTAL; i++) {
		cdev_del(&(cdev_array[i]));
	}
	devno_top = MKDEV(_major_opgpio, _minor_opgpio);
	for (i = 0; i < NUM_DEV_GPIO; i++) {
		devno = MKDEV(_major_opgpio, _minor_opgpio + i);
		device_destroy(class_opgpio, devno);
	}
	unregister_chrdev_region(devno_top, NUM_DEV_GPIO);
	class_destroy(class_opgpio);
	kfree(cdev_array);
	mutex_destroy(&lock);
	/* GPIO unmap */
	/* set all gpio as low */
	gpio_set_value(GPIO_OP0, 0);
	gpio_set_value(GPIO_OP1, 0);
	/* sysfs: reverses the effect of exporting to userspace */
	gpio_unexport(GPIO_OP0);
	gpio_unexport(GPIO_OP1);
	/* reverse gpio_export() */
	gpio_free(GPIO_OP0);
	gpio_free(GPIO_OP1);
	printk("module being removed at %lun", jiffies);
}

4.设备操作函数

4.1 gpio函数

在说明设备操作函数之前需要特别说明一下前面遗留的问题,关于gpio的一些控制函数,不同于网上的一些教程,目前来说,网上的一些教程,都是采用了寄存器进行控制gpio,比如和jetson开发板比较类似的树莓派,他们的做法是,通过芯片手册查到指定引脚的控制寄存器和输出寄存器的物理地址,再通过ioremap函数将物理地址映射到cpu可以访问的虚拟地址上,关于这一方面的理论只是请看mmu。但是英伟达官方并没有提供jetson开发板关于寄存器的技术手册,因此这一方面肯定是行不通的,因此需要另外找方法来控制对应的gpio,经过查找资料,发现有一个gpio.h的头文件,里面有许多关于gpio的操作函数,关于这些操作函数的具体请自己阅读该头文件代码。这里主要说一下在本驱动上所用到的函数:
1.测试gpio端口是否合法

static inline bool gpio_is_valid(int number)

2.申请使用gpio,其中参数label为字符串,用于标志此GPIO,申请成功后,可以通过/sys/kernel/debug/gpio文件查看到该GPIO的状态

static inline int gpio_request(unsigned gpio, const char *label)

3.设置该GPIO为输出

static inline int gpio_direction_output(unsigned gpio, int value)

4.导出gpio端口到用户空间,参数direction_may_change表示用户程序是否允许修改gpio的方向,假如可以,则参数direction_may_change为真。

static inline int gpio_export(unsigned gpio, bool direction_may_change)

5.设置gpio引脚的值

static inline void gpio_set_value(unsigned gpio, int value)

6.撤销gpio的导出

static inline void gpio_unexport(unsigned gpio)

7.释放已经申请的gpio

static inline void gpio_free(unsigned gpio)

使用流程图如下:
在这里插入图片描述

4.2 file_operation结构体与实现

file_operation就是设备的具体操作函数,他定义在fs.h的头文件中,其所定义的结构体如下:

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iterate) (struct file *, struct dir_context *);
	int (*iterate_shared) (struct file *, struct dir_context *);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
	ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
			loff_t, size_t, unsigned int);
	int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
			u64);
	ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *,
			u64);
};

可以看到,其定义的成员函数特别多,并不是都需要实现的,首先我们需要实现最基本的open和release函数,其余的函数我们就根据设备的需求进行实现即可,比如在我这个gpio设备中,我只需要让他们输出即可,所以我只实现了write函数。
所实现的open函数

static int dev_open(struct inode *inode, struct file *filep)
{
	int *minor = (int *)kmalloc(sizeof(int), GFP_KERNEL);
	//得到次设备号
	// int major = MAJOR(inode->i_rdev);
	*minor = MINOR(inode->i_rdev);
	// printk(KERN_INFO "open request major:%d minor: %d n", major,
	// *minor);
	//将次设备号保存设置为私有数据
	filep->private_data = (void *)minor;
	return 0;
}

所实现的release函数

static int dev_release(struct inode *inode, struct file *filep)
{
	kfree(filep->private_data);
	return 0;
}

所实现的write函数

/*gpio输出1*/
static int dev_put(int ledno)
{
	switch (ledno) {
	case 0:
		gpio_set_value(GPIO_OP0, 1);
		printk("gpio29输出1!n");
		break;
	case 1:
		gpio_set_value(GPIO_OP1, 1);
		printk("gpio31输出1!n");
		break;
	}
	return 0;
}
/*gpio输出0*/
static int dev_del(int ledno)
{
	switch (ledno) {
	case 0:
		gpio_set_value(GPIO_OP0, 0);
		printk("gpio29输出0!n");
		break;
	case 1:
		gpio_set_value(GPIO_OP1, 0);
		printk("gpio29输出0!n");
		break;
	}

	return 0;
}
/*write操作函数*/
static ssize_t dev_write(struct file *filep, const char __user *buf,
			 size_t count, loff_t *f_pos)
{
	//cval为内核空间里的字符
	char cval;
	int ret;
	int minor = *((int *)filep->private_data);
	//将用户空间输出的字符buf复制到内核空间里的cval
	if (count > 0) {
		if (copy_from_user(&cval, buf, sizeof(char))) {
			return -EFAULT;
		}
		switch (cval) {
		case '1':
			ret = dev_put(minor);
			break;
		case '0':
			ret = dev_del(minor);
			break;
		}
		return sizeof(char);
	}
	return 0;
}

最后就是file_operation结构体

static struct file_operations mygpios_fops = {
    .open = dev_open,
    .release = dev_release,
    .write = dev_write,
};

三、驱动编译

1 编写udev规则文件

规则文件是 udev 里最重要的部分,默认是存放在 /etc/udev/rules.d/下。所有的规则文件必须以“.rules”为后缀名。详细理论知识请参考链接,例如我创建了一个文件为50-mygpios.rules,在里面写上:

KERNEL=="mygpio*",ACTION=="add",MODE="0666"

意思就是:匹配名为mygpio的设备,当添加后,给他0666的权限,也就是授予设备权限。

2 编写makefile文件

在驱动文件目录下写编译为ko的makefile,注意这里没有用交叉编译,所以不用像教程里面的还需要添加交叉编译的路径。

MODULE:= mygpios
obj-m:= $(MODULE).o
clean-files := *.o *.ko *.mod.[co] *~

LINUX_SRC_DIR:=/lib/modules/$(shell uname -r)/build
VERBOSE:=0
ccflags-y += -std=gnu99 -Wall -Wno-declaration-after-statement

mygpios.ko: mygpios.c
	make -C $(LINUX_SRC_DIR) M=$(shell pwd) V=$(VERBOSE) modules

clean:
	make -C $(LINUX_SRC_DIR) M=$(shell pwd) V=$(VERBOSE) clean

在主目录下编写,加载驱动和清理驱动等的makefile文件,这里面有多个操作,输入make build就是生成.ko文件,输入make clean就是清理所有的生成文件,输入make install就是生成.ko文件并加载驱动模块,输入make uninstall就是卸载模块,输入make insmod也是生成.ko文件并加载驱动模块,不过在加载驱动模块的指令不同,建议使用make insmod,输入make rmmod也是卸载模块,输入make _dmesg是打印内核信息。具体makefile的语法,可以百度。

MAKEFILE_DIR := $(shell cd $(dir $(lastword $(MAKEFILE_LIST))); pwd)

help:
	@echo "the Jetson gpios device driver installer, liu ya fei"
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "33[36m%-30s33[0m %sn", $$1, $$2}'

build: ## build the Jetson gpios kernel module, mygpios.ko
	cd $(MAKEFILE_DIR)/drivers/mygpios && make mygpios.ko

clean: ## clean the object files created while building the kernel module
	cd $(MAKEFILE_DIR)/drivers/mygpios && make clean

install: build ## install mygpios.ko and set auto load
	cp 50-mygpios.rules /etc/udev/rules.d/
	cp $(MAKEFILE_DIR)/drivers/mygpios/mygpios.ko /lib/modules/`uname -r`/kernel/drivers/misc/
	depmod -A
	modprobe mygpios
	echo mygpios | sudo tee /etc/modules-load.d/mygpios.conf > /dev/null

uninstall: ## remove mygpios.ko and un-set auto load
	-modprobe -r mygpios
	rm /etc/udev/rules.d/50-mygpios.rules
	rm /etc/modules-load.d/mygpios.conf
	rm /lib/modules/`uname -r`/kernel/drivers/misc/mygpios.ko

insmod: build ## insmod mygpios.ko
	sudo insmod $(MAKEFILE_DIR)/drivers/mygpios/mygpios.ko
	sleep 1
	-sudo chmod 666 /dev/mygpio*
	

rmmod: ## rmmod mygpios.ko
	sudo rmmod mygpios

_dmesg:
	dmesg -x --color -l emerg,alert,crit,err,info,debug

3 编译过程

1.生成ko文件,make build
在这里插入图片描述
2.加载内核,sudo make install
在这里插入图片描述
使用lsmod查看已加载的驱动,可以看到mygpios
在这里插入图片描述
查看/dev下是否有驱动,可以看到两个gpio都已经加载成功
在这里插入图片描述

4 测试驱动是否成功

测试代码

#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(void){
        int fd1 = open("/dev/mygpio0", O_RDWR);
		int fd2 = open("/dev/mygpio1", O_RDWR);
		int flag = 10;
        if(fd1 < 0)
        {
        	printf("open pin29 failn");
        }
		if(fd2 < 0){
			printf("open pin31 failn");

		}
        while(flag){
	        char cmd1 = '1';
			char cmd2 = '0';
			int ret;
			if(flag % 2 == 0){
				ret = write(fd1,&cmd1,1);
				if (ret < 0)
				{
					printf("write failn");
				}else{
					printf("pin29输出%c", cmd1);
				}
				ret= write(fd2,&cmd2,1);
	        	if (ret < 0)
	        	{
	        		printf("write failn");
	        	}else{
					printf("pin31输出%c", cmd2);
				}

			}
	        else{
				ret = write(fd1,&cmd2,1);
				if (ret < 0)
				{
					printf("write failn");
				}else{
					printf("pin29输出%c", cmd2);
				}
				ret= write(fd2,&cmd1,1);
	        	if (ret < 0)
	        	{
	        		printf("write failn");
	        	}else{
					printf("pin31输出%c", cmd1);
				}
			
			}
			sleep(2);
			flag--;
	        
        }
		int ret = 0;
		ret = close(fd1);
		if(ret < 0){
			printf("mygpio0关闭失败!n");
		}
		ret = close(fd2);
		if(ret < 0){
			printf("mygpio1关闭失败!n");
		}
        return 0;
 
}

在这里插入图片描述
查看内核打印信息
在这里插入图片描述

5 卸载模块

make uninstall
在这里插入图片描述
lsmod查看驱动是否存在,已经没有mygpios这个驱动文件了。
在这里插入图片描述


总结

上述就是对一个简单的字符设备的开发流程,所有代码已上传到github,后面还会在此基础上开发i2c和spi设备的驱动。

最后

以上就是鲤鱼路灯为你收集整理的基于Jetson Agx Xavier的驱动开发----------一个字符设备驱动(多设备驱动)前言一、字符设备驱动开发的主要流程二、驱动程序的编写三、驱动编译总结的全部内容,希望文章能够帮你解决基于Jetson Agx Xavier的驱动开发----------一个字符设备驱动(多设备驱动)前言一、字符设备驱动开发的主要流程二、驱动程序的编写三、驱动编译总结所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(66)

评论列表共有 0 条评论

立即
投稿
返回
顶部