【genius_platform软件平台开发】第六十七讲:linux系统驱动开发之-应用程序发送信号给驱动程序

在这里插入图片描述

  • 写一个有实际应用功能的驱动程序:
    在驱动程序中,初始化 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 目录下创建设备节点
  • 示例代码中,对设备的操作函数只实现了 openioctl 这两个函数,这是根据实际的使用场景来决定的,你也可以使用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环境变量即可。如果未定义,表示内核代码外编译,通过设置KERNELDIRPWD环境变量,然后通过内核脚本编译该当前文件生产内核模块文件。

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_adddevice_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 已经不存在了
在这里插入图片描述

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

隨意的風

如果你觉得有帮助,期待你的打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值