上篇提到裸机程序,本文使用的硬件平台相同。详情见linux驱动的三种写法之前言——裸机程序
linux驱动的三种写法之——软硬件信息未分离
前言
今天我们来聊聊linux驱动程序应该是比较原始的写法,就是硬件驱动程序中的软件和硬件信息是写在同一个文件中的,虽然这样的写法看上去简单相对来说好理解,但是很不符合软件程序“低内聚,高耦合”的要求,特别是当硬件信息有变动的时候,比如硬件信息更改或者将驱动程序移植到其他平台上,就会发现工作量非常之大。最要命的是,即使更改一点硬件信息,都需要重新编译。基本上,我在工作上没有见过这样的驱动程序,应该是学习linux驱动程序的基础。其实本人开这一系列的博文的目的就是追本溯源地重新整理下linux下的驱动程序的写法。那么,刚入门嵌入式开发的同学们,我们一起来以LED点亮熄灭的驱动程序为例把linux驱动程序的基础知识打打牢固吧。
GPIO相关函数
先回顾下,在LED点亮熄灭的裸板程序中,我们需要明确的信息有哪些?
- 4个LED灯的硬件原理图(即找到控制LED等的gpio引脚)
- 编程的思路步骤(如何配置这些GPIO引脚)
而在linux系统中,有一些gpio控制的配套函数来大大简便我们对GPIO引脚的配置工作和管理工作。
gpio对linux系统来说,是有限的,是一种宝贵的资源,需要严格管理起来,供给不同的硬件使用,因此有gpio申请函数和释放函数。
1.首先看看GPIO申请函数gpio_request(),其定义如下,由内核实现。
int gpio_request(unsigned gpio, const char *label)
- 函数功能:向linux内核申请GPIO资源
- 参数说明:gpio是GPIO 引脚硬件在 linux 内核中的软件编号,也就是对于任何一个 GPIO引脚,linux 内核都给分配一个唯一的软件编号(类似 GPIO 引脚的身份证号)
在本案使用的S5P6818开发板上四个LED的GPIO引脚与在linux内核中对应编号关系如下
GPIO硬件 | GPIO软件编号 |
---|---|
GPIOC12 | PAD_GPIO_C+12 |
GPIOC11 | PAD_GPIO_C+11 |
GPIOC7 | PAD_GPIO_C+7 |
GPIOB26 | PAD_GPIO_B+26 |
注释:PAD_GPIO_C+12等都是宏定义,对应的应该是gpio的数字编号
——————————————————————————————
label:给申请的硬件 GPIO 引脚指定的名称,根据实际需求取名
2.与gpio_request()函数对应的就是从内核中释放GPIO资源的函数gpio_free()其定义如下,由内核实现。
void gpio_free(unsigned gpio)
- 函数功能:内核程序如果不再使用访问 GPIO 硬件资源, 要将硬件资源归还给 linux 内核
- 参数说明:gpio:要释放的 GPIO 引脚(硬件资源)对应的软件编号
++++++++++++++++++++++++++++++++++++++++++++
以上就是linux内核中对GPIO资源进行管理的两个函数
++++++++++++++++++++++++++++++++++++++++++++
下面,我们来接着看看linux内核中对GPIO引脚配置的函数。
3.配置GPIO引脚为输入的函数gpio_direction_input(),其定义如下
int gpio_direction_input(unsigned gpio)
- 函数功能:配置 GPIO 为输入功能 输入功能由外设控制,(所以就没有输入的值?本人没明白,有人明白的话烦请留言解答,谢谢)
- 参数说明:gpio:要配置为输入的 GPIO 引脚(硬件资源)
4…配置GPIO引脚为输出的函数gpio_direction_output(),其定义如下
int gpio_direction_output(unsigned gpio, int value)
- 函数功能;配置 GPIO 引脚为输出功能,并且输出一个 value 值(1 高电平/0 低电平),
- gpio: GPIO 硬件对应的软件编号 value:输出的值
5.获取GPIO引脚电平状态的函数gpio_get_value(),其定义如下
int gpio_get_value(unsigned gpio)
- 函数功能:获取 GPIO 引脚电平状态,返回值就是引脚的电平状态(返回 1:高电平;返回 0:低电平),此引脚到底是输入还是输出没关系
6.由CPU设置 GPIO 引脚的输出值(1或0)的函数gpio_set_value(),定义如下
int gpio_set_value(unsigned gpio, int value)
- 函数功能:设置 GPIO 引脚的输出值为 value(1:高/0:低)前提是必须首先将 GPIO 配置为输出功能
++++++++++++++++++++++++++++++++++++++++++++++
以上就是linux内核中对GPIO资源进行设置的四个函数
+++++++++++++++++++++++++++++++++++++++++++++++
学习了这6个函数,我们理论上就能都点亮熄灭这些gpio引脚对应的LED了。那么,具体如何写程序呢,linux内核对驱动程序有一定的规则,我们一起来看看linux内核的驱动框架吧
linux驱动框架
框架组成 | 组成说明 |
---|---|
头文件 | 调用linux函数必需 |
添加各种软硬件信息 | 驱动操作硬件必须 |
入口函数 | 内核程序的入口函数(类似 main 函数) |
出口函数 | 内核程序的出口函数(类似 return 0) |
各种修饰规则 | 告知内核此程序遵循 GPL 等linux内核协议 |
我们以在linux内核安装驱动程序的同时开LED灯,卸载驱动程序的同时关LED灯为一个简单的例子来说明解释下如上linux驱动框架
#include <linux/gpio.h>
#include <linux/init.h>
#include <linux/module.h>
#include <mach/platform.h>
//定义描述led硬件信息的数据结构
struct led_resource{
int gpio;
char* name;
};
//定义初始化led灯的硬件信息
struct led_resource led_info[]={
{PAD_GPIO_C+12,"LED1",0},
{PAD_GPIO_C+11,"LED2",0},
{PAD_GPIO_C+7,"LED3",0},
{PAD_GPIO_B+26,"LED4",0}
};
//驱动的入口函数
int led_drv_init(void){
int i;
for(i = 0; i < ARRAY_SIZE(led_info);i++){
gpio_request(led_info[i].gpio,led_info[i].name);//申请gpio
gpio_direction_output(led_info[i].gpio,0);//配置输出同时点亮
}
return 0;
}
//驱动的出口函数
void led_drv_exit(void){
int i;
for(i = 0; i < ARRAY_SIZE(led_info); i++){
gpio_set_value(led_info[i].gpio,1);//熄灭4个LED灯
gpio_free(led_info[i].gpio);//释放gpio资源
}
}
//各种修饰规则
module_init(led_drv_init);
module_exit(led_drv_exit);
MODULE_LICENSE("GPL");
是不是很简单啊,当然了这些只是linux驱动程序基本框架。既然在linux系统中写驱动程序,就不会像写裸板程序那样实现不了复杂的功能了。接下来,我们就添加一些复杂的功能吧,在linux系统下,一切皆是文件。所有的硬件也是文件,这些LED灯也都是文件,linux系统操作文件一样就可以操作这些硬件。那么linux系统里面是如何实现硬件外设文件化的呢?下面我们先以字符设备为例来说明下,linux系统下是如何将硬件外设以文件的形式存放的呢?
linux字符设备驱动相关知识
linux设备驱动分类
linux设备驱动分类 | 简单说明 |
---|---|
字符设备驱动 | 对字符设备的访问按照字节流形式访问,比如LED,按键,蜂鸣器,GPS(UART),GPRS(UART),BT(UART)等等 |
块设备驱动 | 对块设备的访问按照数据块进行,比如一次操作 512 字节,比如 硬盘,U 盘,TF 卡,SD 卡,Nandflash,Norflash,EMMC |
网络设备驱动 | 对网络设备驱动一般按照网络协议进行,比如有线网卡和无线网卡 |
字符设备文件特点属性
字符设备对应的文件称之为字符设备文件。字符设备文件本身代表的就是字符设备硬件本身,字符设备文件存在于根文件系统必要目录的 dev 目录下,当然块设备文件也存于 dev 目录下。
举个例子:查看下位机的 UART 设备的字符设备文件
ls /dev/ttySAC* -lh 得到以下信息:
crw-rw---- 204, 64 /dev/ttySAC0
crw-rw---- 204, 65 /dev/ttySAC1
crw-rw---- 204, 66 /dev/ttySAC2
crw-rw---- 204, 67 /dev/ttySAC3
说明:c:表示此设备文件对应的设备为字符设备
204:表示串口的主设备号
64/65/66/67:分别表示第一个,第二个,第三个,第四个串口的次设备号
ttySAC0:表示第一个串口的设备文件名
ttySAC1:表示第二个串口的设备文件名
ttySAC2:表示第三个串口的设备文件名
ttySAC3:表示第四个串口的设备文件名
注意:一个硬件外设个体有唯一的一个设备文件名
主设备号,次设备号,设备号的说明
- 主设备号:
应用程序根据字符设备文件的主设备号在茫茫的内核驱动中找到对应的唯一的设备驱动,一个设备驱动仅有唯一的主设备号
- 次设备号:
设备驱动根据次设备号能够找到应用程序要访问的硬件外设个体
总结:一个驱动仅有一个主设备号,一个硬件个体仅有一个次设备号,应用根据主设备号找驱动,驱动根据次设备号找硬件个体(如下示意图)
所以:主,次设备号对于 linux 内核是一个宝贵的资源,某个设备驱动必须要向 linux 内核申请主,次设备号,那么如何申请呢?
- 设备号
设备号包含主,次设备号
linux 内核用 dev_t(unsigned int)数据类型描述设备号信息
设备号的高 12 位用来描述主设备号
设备号的低 20 位用来描述次设备号
设备号和主,次设备号之间的转换操作宏:
设备号=MKDEV(已知的主设备号,已知的次设备号);
主设备号=MAJOR(已知的设备号);
次设备号=MINOR(已知的设备号);
详细实现见下内核代码。
include\linux\kdev_t.h from linux kernel 3.18.67
#ifndef _LINUX_KDEV_T_H
#define _LINUX_KDEV_T_H
#include <uapi/linux/kdev_t.h>
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
- 设备号配套函数
一. 设备号申请函数alloc_chrdev_region();定义如下
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, \
unsigned count, const char*name);
- 函数功能:向内核申请设备号
- 参数:1. dev:保存申请的设备号 包括主设备号和起始的次设备号 2. baseminor:希望申请的起始次设备号,一般给 0
3. count:申请的次设备号的个数 如果 baseminor=0,count=2,那么申请的次设备号分别是 0 和 1
4. name:申请设备号指定的名称,随便取,将来通过执行 cat /proc/devices 查看
设备驱动一旦不再使用设备号,记得要将设备号资源归还给 linux 内核,于是相对应的有释放占用的设备号的函数
二. 设备号释放函数unregister_chrdev_region();定义如下
void unregister_chrdev_region(dev_t dev, unsigned count);
- 函数功能:释放申请的设备号资源
- 参数:dev:申请的设备号 count:申请的次设备号的个数
字符设备文件的创建方法
1.手动创建,只需 mknod 命令
语法: mknod /dev/字符设备文件名 c 主设备号 次设备号
例如: mknod /dev/vaccine c 244 0
2.自动创建:见此链接
字符设备文件的访问
明确:访问字符设备文件本质就是在访问字符设备硬件
字符设备文件的访问必须利用系统调用函数
以访问UART0,读写数据为例
.................
//打开 UART0:注意:使用绝对路径!
int fd = open("/dev/ttySAC0", O_RDWR)
if (fd < 0)
return -1;
//从 UART0 读取数据:
char buf[1024] = {0};
read(fd, buf, 1024);
//向 UART0 写入数据:
write(fd, "hello,world\n", 12);
//关闭 UART0:
close(fd);
.................
linux设备驱动的两大核心内容
1.必须有对硬件的操作访问
2.必须有硬件操作接口,用户能够通过这些接口来访问硬件
从上面我们已经字符设备文件有一些特有的属性,那么内核如何去描述字符设备的属性呢?都包括哪些内容呢?由上文,我们不难想到会包括字符设备文件对应的设备号,次设备号的个数,提供给用户的操作接口等。
描述字符设备对象属性的结构体struct cdev及其配套的函数
- linux内核中描述字符设备对象属性的结构体
struct cdev {
struct kobject kobj; //linux设备模型的基础
struct module *owner; // 模块的所有者
const struct file_operations *ops; //硬件的操作接口对象
struct list_head list;
dev_t dev; // 字符设备的设备号
unsigned int count; //次设备号的个数
};
- 字符设备对象的初始化函数cdev_init(),定义如下
void cdev_init(struct cdev *, \
const struct file_operations *);
1.函数功能:(initialize a cdev structure)初始化字符设备驱动对象,其中有一点很重要的是,给字符设备对象添加一个硬件操作接口
2.参数说明:cdev:要初始化的字符设备对象,fops:给用户提供的硬件操作接口
- 向内核添加字符设备对象的函数cdev_add(),定以如下
.int cdev_add(struct cdev *, dev_t , unsigned )
1.函数功能:向内核注册添加一个字符设备对象,一旦添加完毕, 内核就有一个真实的字符设备对象
2.参数说明:cdev:要注册的字符设备对象 dev:申请的设备号 count:申请的次设备号的个数
- 从内核卸载字符设备对象的函数cdev_del(),其定义如下
void cdev_del(struct cdev*);
1.函数功能:从内核中卸载字符设备对象,一旦卸载完毕
内核就不会有这个的字符设备对象
2.参数说明:cdev 要卸载的字符设备对象
用户操作接口对象
到此,字符设备驱动的基本框架就介绍的差不多,还剩下给用户提供的操作接口了,其实内核已经给我们定义了一系列的操作的接口,如下
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 (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, 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 *);
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 (*aio_fsync) (struct kiocb *, 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);
int (*show_fdinfo)(struct seq_file *m, struct file *f);
};
而我们常用的就是下面这几个
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
ssize_t (*write) (struct file *, const char __user *,\
size_t, loff_t *);
ssize_t (*read) (struct file *, char __user *, \
size_t, loff_t *);
long (*unlocked_ioctl) (struct file *, unsigned int,\
unsigned long);
关于这几个用户的操作接口的函数的功能和参数说明,限于本文就不做过多说明。但是有一点需要强调的是,它们与其对应用户空间的函数之间的调用关系需要说明下,我们以write函数说明下。我们只是write函数是linux系统的调用函数,我们使用man 2 write在linux系统终端中查看该函数在用户空间的定义和其调用过程
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
- write系统调用的实现过程:
1.当应用程序(进程)调用 write 系统调用, 首先会跑到 C 库的 write 函数的定义实现的地方
2.C 库的 write 函数将会做两件事:首先保存 write 函数的系统调用号到 R7 寄存器。系统调用号:linux内核给每一个系统调用函数分配唯一的软件编号(类似系统调用函数的身份证号),定义在arch/arm/include/uapi/asm/unistd.h(linux kernel 3.18.67版本)
#ifndef _UAPI__ASM_ARM_UNISTD_H
#define _UAPI__ASM_ARM_UNISTD_H
#define __NR_OABI_SYSCALL_BASE 0x900000
#if defined(__thumb__) || defined(__ARM_EABI__)
#define __NR_SYSCALL_BASE 0
#else
#define __NR_SYSCALL_BASE __NR_OABI_SYSCALL_BASE
#endif
/*
* This file contains the system call numbers.
*/
#define __NR_restart_syscall (__NR_SYSCALL_BASE+ 0)
#define __NR_exit (__NR_SYSCALL_BASE+ 1)
#define __NR_fork (__NR_SYSCALL_BASE+ 2)
#define __NR_read (__NR_SYSCALL_BASE+ 3)
#define __NR_write (__NR_SYSCALL_BASE+ 4)
#define __NR_open (__NR_SYSCALL_BASE+ 5)
#define __NR_close (__NR_SYSCALL_BASE+ 6)
/* 7 was sys_waitpid */
#define __NR_creat (__NR_SYSCALL_BASE+ 8)
#define __NR_link (__NR_SYSCALL_BASE+ 9)
#define __NR_unlink (__NR_SYSCALL_BASE+ 10)
#define __NR_execve (__NR_SYSCALL_BASE+ 11)
#define __NR_chdir (__NR_SYSCALL_BASE+ 12)
.......................................
..............................
我们可以看到write对应的系统调用号为__NR_SYSCALL_BASE+ 4,即是0x900004
然后调用 swi 指令触发一个软中断异常,(新版本的 ARM 核(ARMV6 开始)触发软中断的指令为 svc老版本的 ARM 核触发软中断的指令是 swi,现在的新版编译器同样支持 swi 指令!)
3.一旦触发软中断异常,CPU 核立马要处理软中断异常,CPU核的硬件会自动的做如下工作:
CPU(ARM核)硬件处理软中断异常的工作流程 |
---|
1.备份 CPSR 到 SVC管理模式下的SPSR |
2.设置CPSR, 切换到SVC管理模式,切换到ARM状态,禁止FIQ/IRQ中断 |
3.保存返回地址:(SVC下)LR = PC - 4 (由ARM指令的工作流水决定的) |
4. 设置 PC=0x08,至此 CPU 跑到 0x08 软中断处理的入口地址 |
注释:CPSR:(current program status register)当前程序状态寄存器
SPSR:(saved program status register)备份程序状态寄存器
LR:永远只保存返回地址的寄存器,每个工作模式下都有的
PC:永远只保存取指器要取的那条指令的内存存储地址的寄存器,只有用户和系统工作模式下有
4.linux 内核软中断处理的入口地址相关的代码将做以下工作:
先从 R7 寄存器取出之前保存的 write 函数的系统调用号 0x900004,然后以write系统调用号 0x900004为下标在内核的系统调用表(数组)中 找到对应的一个内核函数 sys_write,一旦找到对应的内核函数,继续调用此函数,调用完毕,最后原路返回到用户空间,至此 write 函数返回!
系统调用表:本质就是一个数组,数组元素就是函数指针, 数组元素的下标就是系统调用号。 其定义在内核源码的 arch/arm/kernel/calls.S
/*
* linux/arch/arm/kernel/calls.S
*
* Copyright (C) 1995-2005 Russell King
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This file is included thrice in entry-common.S
*/
/* 0 */ CALL(sys_restart_syscall)
CALL(sys_exit)
CALL(sys_fork)
CALL(sys_read)
CALL(sys_write)
................
................
以上流程见下如下示意图
总结:系统调用函数的write调用流程概述为:用户空间write ->软中断 ->sys_write -->内核驱动的write函数接口
linux字符设备驱动实例
/*编写字符设备驱动,利用ioctl随意开关上述四个LED等*/
//头文件
#include <linux/init.h>
#include <linux/module.h>
#include <linux/gpio.h>
#include <mach/platform.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/device.h>
//定义描述led灯硬件的数据结构
struct led_resource{
int gpio;
char *name;
};
//定义初始化4个led灯的硬件信息
struct led_resource led_info[] = {
{PAD_GPIO_C+12, "LED1"},
{PAD_GPIO_C+7, "LED2"},
{PAD_GPIO_C+11, "LED3"},
{PAD_GPIO_B+26, "LED4"}
};
//定义设备号
dev_t led_dev;
//定义led设备对象
struct cdev led_cdev;
#define LED_ON 0X100001
#define LED_OFF 0X100002
//调用关系:应用程序ioctl -> 软中断 -> 内核sys_ioctl ->驱动led_ioctl
//例子:int index=1;ioctl(fd, LED_ON, &index);//开第1个灯
//参数关系:
// fd<---->file 亲戚关系
// cmd=LED_ON或者cmd=LED_OFF
// arg=(unsigned long)&index
long led_ioctl(struct file *file, unsigned int cmd, unsigned long arg){
//分配内核缓冲区
int kindex;
copy_from_user(&kindex, (int *)arg, sizeof(kindex));
switch(cmd){
case LED_ON:
gpio_set_value(led_info[kindex-1].gpio, 0);
printk("%s:开第%d个灯\n",__func__, kindex);
break;
case LED_OFF:
gpio_set_value(led_info[kindex - 1].gpio, 1);
printk("%s:关第%d个灯\n",__func__, kindex);
break;
default:
printk("无效命令!\n");
return -1;
}
return 0;
}
//定义led灯的硬件操作接口
struct file_operations led_fops = {
.unlocked_ioctl = led_ioctl
};
//入口函数
int led_drv_init(void){
//申请gpio资源
int i;
for(i = 0; i < ARRAY_SIZE(led_info); i++){
gpio_request(led_info[i].gpio, led_info[i].name);
gpio_direction_output(led_info[i].gpio, 1);
}
//向内核申请设备号资源
alloc_chrdev_region(&led_dev, 0, 1, "vaccine");
/*led设备对象的操作*/
cdev_init(&led_cdev, &led_fops);
cdev_add(&led_cdev, led_dev, 1);
return 0;
}
//出口函数
void led_drv_exit(void){
//释放gpio资源
int i;
for(i = 0; i < ARRAY_SIZE(led_info); i++){
gpio_set_value(led_info[i].gpio, 1);
gpio_free(led_info[i].gpio);
}
//释放设备号资源
unregister_chrdev_region(led_dev, 1);
/*led设备对象的操作*/
cdev_del(&led_cdev);
}
//各种修饰和GPL规则
module_init(led_drv_init);
module_exit(led_drv_exit);
MODULE_LICENSE("GPL");
其对应的应用程序如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#define LED_ON 0x100001 //开灯命令
#define LED_OFF 0x100002 //关灯命令
int main(int argc, char *argv[])
{
int fd;
int index; //分配用户缓冲区,保存操作灯的编号
if(argc != 3) {
printf("用法:%s <on|off> <1|2|3|4>\n", argv[0]);
return -1;
}
//打开设备
fd = open("/dev/myled", O_RDWR);
if (fd < 0) {
printf("打开设备失败!\n");
return -1;
}
//"1"->1 将输入的字符串"1"转换为数字1
index = strtoul(argv[2], NULL, 0);
//应用ioctl->软中断->内核sys_ioctl->驱动led_ioctl
if(!strcmp(argv[1], "on"))
ioctl(fd, LED_ON, &index);
else if(!strcmp(argv[1], "off"))
ioctl(fd, LED_OFF, &index);
//关闭设备
close(fd);
return 0;
}
关于strtoul函数的用法说明,参见linux库函数strtoul简介
总结
若是觉得以上linux字符设备驱动程序稍稍复杂,linux内核还提供了简化版的混杂设备驱动,详情请戳此链接→字符设备驱动的简化版混杂设备驱动