概述
文章目录
- 前言
- 一、字符设备驱动开发的主要流程
- 二、驱动程序的编写
- 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 "