- 写一个有实际应用功能的驱动程序:
在驱动程序中,初始化 GPIO 设备,自动创建设备节点;
在应用程序中,打开 GPIO 设备,并发送控制指令设置 GPIO 口的状态;
1. 简述目的
- 我们目标是编写一个驱动程序模块:
mygpio.ko
。当这个驱动模块被加载的时候,在系统中创建一个mygpio 类
设备,并且在/dev
目录下,创建 N个设备节点:(N取决于程序中设置的数值)。应用程序中,可以打开某个GPIO
设备,通过发送控制指令,来设置GPIO
的状态
$ ls /dev/my*
/dev/mygpio0
/dev/mygpio1
/dev/mygpio...
/dev/mygpioN
2.驱动程序
- 以下所有操作的工作目录为:
xxxx@gp_developer_server:~/cv25_linux_sdk_2.5.5/ambarella/kernel/linux-4.14/drivers/
2.1 创建驱动目录(mygpio)
$ cd linux-4.14/drivers/
$ mkdir mygpio_driver
$ cd mygpio_driver
$ touch mygpio.c
2.2 创建驱动程序(mygpio.c)
2.2.1 init_func(加载函数)和exit_func(卸载函数)
- 两个加载和卸载驱动相关的函数
static int __init init_func(void);
static void __exit exit_func(void);
- 这两个函数的名称可以由用户自己定义,必须遵守上面的返回值和参数类型。
2.2.2 static关键字
- 修复的函数只能在当前文件中有效并使用,外部不可用。
2.2.3 __init关键字
- 告诉编译器,该函数代码在初始化完毕后被忽略。
2.2.4 __exit关键字
- 告诉编译器,该函数代码仅在卸载模块的时候被调用。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/ctype.h>
#include <linux/device.h>
#include <linux/cdev.h>
// 设备名称
#define MYGPIO_NAME "mygpio"
// 一共有4个 GPIO 口
#define MYGPIO_NUMBER 4
// gpio状态
enum{
GPIO_STATE_CLOSE = 0,
GPIO_STATE_OPEN = 1,
};
// 设备类
static struct class* gpio_class;
// 用来保存设备也可以通过动态创建alloc
struct cdev gpio_cdev[MYGPIO_NUMBER];
// 用来保存设备号
int gpio_major = 0;
int gpio_minor = 0;
// 当应用程序打开设备的时候被调用
static int gpio_open(struct inode *inode, struct file *file)
{
printk("gpio_open is called...\n");
return 0;
}
// 当应用程序控制GPIO的时候被调用
static long gpio_ioctl(struct file* file, unsigned int val, unsigned long gpio_no)
{
printk("gpio_ioctlis called...\n");
// 检查设置的状态值是否合法
if (GPIO_STATE_CLOSE != val && GPIO_STATE_OPEN != val)
{
printk("val is NOT valid! \n");
return 0;
}
// 检查设备范围是否合法
if (gpio_no >= MYGPIO_NUMBER)
{
printk("dev_no is invalid! \n");
return 0;
}
printk("set gpio_no=[%d] val=[%d] \n", gpio_no, val);
return 0;
}
// 文件操作opt
static const struct file_operations gpio_ops={
.owner = THIS_MODULE,
.open = gpio_open,
.unlocked_ioctl = gpio_ioctl
};
// 初始化init函数
static int __init gpio_driver_init(void)
{
int i, devno;
dev_t num_dev;
printk("gpio_driver_init is called... \n");
// 动态申请设备号(严谨点的话,应该检查函数返回值)
int ret = alloc_chrdev_region(&num_dev, gpio_minor, MYGPIO_NUMBER, MYGPIO_NAME);
if (ret != 0)
{
printk("alloc_chrdev_region is error... \n");
return -1;
}
// 获取主设备号
gpio_major = MAJOR(num_dev);
printk("gpio_major = %d. \n", gpio_major);
// 创建设备类
gpio_class = class_create(THIS_MODULE, MYGPIO_NAME);
// 创建设备节点
for (i = 0; i < MYGPIO_NUMBER; ++i)
{
// 设备号
devno = MKDEV(gpio_major, gpio_minor + i);
// 初始化 cdev 结构
cdev_init(&gpio_cdev[i], &gpio_ops);
// 向系统注册字符设备
cdev_add(&gpio_cdev[i], devno, 1);
// 系统创建设备节点,此时可以在/dev/目录下可以查看到对象的/dev/mygpio*
device_create(gpio_class, NULL, devno, NULL, MYGPIO_NAME"%d", i);
}
return 0;
}
static void __exit gpio_driver_exit(void)
{
int i;
printk("gpio_driver_exit is called... \n");
// 删除设备和设备节点
for (i = 0; i < MYGPIO_NUMBER; ++i)
{
// 从系统内核中重删除gpio设备
cdev_del(&gpio_cdev[i]);
// 删除gpio设备节点
device_destroy(gpio_class, MKDEV(gpio_major, gpio_minor + i));
}
// 释放设备类
class_destroy(gpio_class);
// 注销设备号
unregister_chrdev_region(MKDEV(gpio_major, gpio_minor), MYGPIO_NUMBER);
}
MODULE_LICENSE("GPL");
module_init(gpio_driver_init);
module_exit(gpio_driver_exit);
- 如果涉及到硬件的初始化、释放、状态设置等相关的操作,可以分别在
gpio_driver_init()
中、gpio_driver_exit()
中、gpio_ioctl()
中增加gpio_hw_init
()、gpio_hw_release
()、gpio_hw_set
()等自己撰写的硬件处理函数接口,实现硬件响应的设置; - 从代码中可以看出:驱动程序使用
alloc_chrdev_region
函数,来动态
注册设备号,并且利用了Linux 应用层
中的udev 服务
,自动在/dev
目录下创建
了设备节点
。 - 示例代码中,对设备的操作函数只实现了
open
和ioctl
这两个函数,这是根据实际的使用场景来决定的,你也可以使用read
函数,来读取某个GPIO
口的状态。使用write
也可以达到目的,只是ioctl
更灵活一些。
2.3 创建 Makefile 文件
$ touch Makefile
在Makefile文件中输入
ifneq ($(KERNELRELEASE),)
obj-m := mygpio.o
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KERNEL_PATH) M=$(PWD) clean
endif
- 第一行中检查是否定义了
KERNELRELEASE
环境变量,如果定义
了,则表示该模块是内核代码中一部分
,直接把模块名称添加到obj-m环境变量
即可。如果未定义
,表示内核代码外编译
,通过设置KERNELDIR
和PWD
环境变量,然后通过内核脚本编译该当前文件
,生产内核模块
文件。
2.4 编译驱动模块
$ make
在mygpio目录下会得到驱动程序: mygpio.ko 。
2.5 加载驱动模块
2.5.1 查看设备节点(/dev/*)-执行完cdev_add()之后
- 在加载驱动模块之前,先来检查一下系统中,几个与驱动设备相关的地方。先看一下
/dev
目录下,目前还没有设备节点(/dev/mygpio[0-3]
)。
$ ls -l /dev/mygpio*
ls: cannot access '/dev/mygpio*': No such file or directory
2.5.2 查看设备号(/proc/devices/*)-执行完register_chrdev()之后
- 当使用
register_chrdev()
函数成功注册一个字符设备后,会在/proc/devices
生产对应的设备信息,再来查看一下/proc/devices
目录下,也没有mygpio
设备的设备号。
2.5.3 主次设备号(MAGOR、MINOR)
- 在linux系统中,所有的资源都是作为文件管理的,设备驱动也不例外。
设备驱动
通常作为一种特殊的文件
存放在/dev/
目录下,内核中使用主设备号标识一个设备
,次设备号
提供给设备驱动使用
,在打开
一个设备时,内核会根据设备的主设备号找到对应的
驱动,然后把次设备号传递给驱动
。 - 在使用一个设备之前,需要使用linux系统提供的
mknod命令
建立设备文件,mknod命令格式如下:
mknod [option]... NAME TYPE [MAGOR MINOR]
option: 选项 -m执行取消权限
NAME:设备文件名字
TYPE:设备文件类型 c:字符设备 b:块设备
MAGOR MINOR:主次设备号
$ cat /proc/devices
2.5.4 清理内核打印信息(dmesg -c)
$ sudo dmesg -c
2.5.5 加载驱动模块(insmod mygpio.ko\modprobe mygpio.ko)
2.5.5.1 insmod
- 不检查内核模块的符号是否已经在内核中定义
2.5.5.2 modprobe
- 检查内核模块的符号是否已经在内核中定义,模块依赖关系
$ sudo insmod mygpio.ko
- 当驱动程序被加载的时候,通过
module_init( )
注册的函数gpio_driver_init()
将会被执行,那么其中的打印信息就会输出。还是通过dmesg 指令
来查看驱动模块的打印信息: - 可以看到:操作系统为这个设备分配的
主设备号
是244
,并且也打印了GPIO硬件的初始化函数的调用信息。此时,驱动模块已经被加载了!来查看一下/proc/devices 目录
下显示的设备号
$ cat /proc/devices
- 设备已经注册了,主设备号是: 244 。
2.5.5.3 自动创建设备节点
由于在驱动程序的初始化函数中,使用 cdev_add
和 device_create
这两个函数,自动创建
设备节点。
所以,此时我们在 /dev
目录下,就可以看到下面这4个设备节点:
现在,设备的驱动程序已经加载
了,设备节点创建好
了,应用程序就可以来控制 GPIO 硬件设备
了。
3.应用程序
创建应用程序目录
$ mkdir ~/tmp/App/app_mygpio
$ cd ~/tmp/App/app_mygpio
$ touch app_mygpio.c
- 编写app_mygpio.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#define MY_GPIO_NUMBER 4
// gpio状态
enum{
GPIO_STATE_CLOSE = 0,
GPIO_STATE_OPEN = 1,
};
// 4个设备节点
char gpio_name[MY_GPIO_NUMBER][16] = {
"/dev/mygpio0",
"/dev/mygpio1",
"/dev/mygpio2",
"/dev/mygpio3"
};
int main(int argc, char *argv[])
{
int fd, gpio_no, val;
// 参数个数检查
if (3 != argc)
{
printf("Usage: ./app_gpio gpio_no value \n");
return -1;
}
gpio_no = atoi(argv[1]);
val = atoi(argv[2]);
// 参数合法性检查
assert(gpio_no < MY_GPIO_NUMBER);
assert(GPIO_STATE_CLOSE == val || GPIO_STATE_OPEN == val);
// 打开 GPIO 设备
if((fd = open(gpio_name[gpio_no], O_RDWR | O_NDELAY)) < 0)
{
printf("%s: open failed! \n", gpio_name[gpio_no]);
return -1;
}
printf("%s: open success! \n", gpio_name[gpio_no]);
// 控制 GPIO 设备状态
ioctl(fd, val, gpio_no);
// 关闭设备
close(fd);
}
3.1 编译应用程序
$ gcc app_mygpio.c -o app_mygpio
- 执行应用程序的时候,需要携带
2个参数
:GPIO 设备编号(0 ~ 3)
,设置的状态值(0 或者 1)。这里设置一下/dev/mygpio0这个设备,状态设置为1:
3.2 测试应用程序
$ sudo ./app_mygpio 0 1
[sudo] password for xxx: <输入用户密码>
/dev/mygpio0: open success!
3.2 查看测试结果
如何确认/dev/mygpio0
这个GPIO的状态确实被设置为1了呢?当然是看 dmesg 指令
的打印信息:
$ dmesg
GPIO口的状态正确执行
4. 卸载驱动
4.1 卸载指令(rmmod mygpio)
$ sudo rmmod mygpio
此时,/proc/devices
下主设备号 244
的 mygpio 已经不存在了