环境:board:JZ2440 arch:arm CPU:arm920t kernel:linux2.6
本篇作为linux驱动练习第一篇,理应从零开始,优先将基本框架准备好,而后根据需要往框架中填充需要的功能。
驱动作为linux内核功能的补充,在已有的linux内核框架基础上,驱动可以作为模块很容易地加入到内核并发挥其预期的功能。一般驱动可以以build-in和modules的方式加入到内核中,前者在内核编译时完成,与内核一体。而后者则以modules这一模块的方式存在,可以在内核运行时,动态加载或者卸载自身,这种应该算是支持热拔插了吧。以module的方式极大地丰富了linux内核的灵活性,同时也方便了驱动开发者,使得驱动开发不再需要修改内核、编译内核、加载内核这一冗长的流程。
为了实现这一灵活性,驱动和内核之间必然有协商好的约定,从而加载驱动时,内核能够知道这是一个驱动,并作出相应的措施来使能或者初始化加载的驱动。以下为一个驱动最基本需要的框架,虽然没什么用处,但是仍然可以作为一个驱动被内核识别并加载。
#include
#include
/* 驱动加载到内核时的初始化函数,__init标识用于将该函数放入对应的段中(数据段,代码段等,
* 由链接脚本规划,同时加上__init代表完成加载后可以释放的区域)。
* 返回值类型int,用于表示加载是否成功。
*/
static int __init XXX_init()
{
}
/* 基本概念如init没有太大区别,只是__exit标识放置的段会有所差异,同时调用时机也是有差别,
* 在驱动要卸载时调用。
*/
static void __exit XXX_exit()
{
}
module_init(XXX_init); //标识入口,驱动加载到内核时,由内核调用,一般为初始化资源
module_exit(XXX_exit); //标识出口,驱动从内核中卸载时,由内核调用,一般为资源回收
在描述其他实际功能之前,不妨再看看能给这个框架增加信息但是实际上并没什么用处的其他描述(这里没啥用只是针对最终期望实现的功能而言,存在必定存在其意义,只是这里不做展开)。
MODULE_AUTHOR("CryptonymAMS"); //作者名称
MODULE_VERSION("1.0.0"); //版本号
MODULE_DESCRIPTION("hello world!"); //相关描述
MODULE_LICENSE("GPL"); //GPL许可
基本框架有了,现在要开始往这个架子填充相关功能了。首先需要做的是把设备节点申请并注册好,这是驱动自身在内核中存在的一个"证明",同时也是向用户空间暴露入口的机会,linux一切皆文件的背景下,最终的设备节点期望是以/dev/xxx这一文件存在在文件系统中。
通过 ls -l /dev 指令可以看到/dev目录下已有的字符设备驱动,行首的 c 表示该文件是字符型设备,4 和 9x分别为该设备的主设备号和次设备号,英文表示分别为major和minor,这是该驱动在内核中的身份证,而名字ttyxx只是针对用户空间做的一个代名,在内核中并没有实际的作用。
major minor
crw-rw---- 1 root dialout 4, 91 5月 2 17:00 ttyS27
crw-rw---- 1 root dialout 4, 92 5月 2 17:00 ttyS28
crw-rw---- 1 root dialout 4, 93 5月 2 17:00 ttyS29
crw-rw---- 1 root dialout 4, 67 5月 2 17:00 ttyS3
crw-rw---- 1 root dialout 4, 94 5月 2 17:00 ttyS30
crw-rw---- 1 root dialout 4, 95 5月 2 17:00 ttyS31
当用户空间调用C库函数如open和read等对文件进行操作时,操作的是文件名,而系统将根据文件名找到对应的主次设备号,依次经过系统调用层、VFS分派到对应的设备驱动中处理。关于VFS和系统调用这里展开过于庞大,放个TODO,之后另篇分析。
从上述可知,注册设备第一步需要做的是注册主次设备号,占好坑。然后把处理函数绑定到该设备上。
注册设备号并绑定设备根据linux内核版本不同有两个版本,以linux2.6作为分水岭,当然新版本内核会兼容旧版本,但是推荐使用新的方法,一方面新方法提供了更高的灵活性,另一方面是防止日后不兼容。
旧方法涉及两个方法如下,从命名可以看出两者对立,一个用于分配,另一个负责回收,这里尝试将内核中的描述翻译,发现怎么都不对味,因此附上原文,基本上重要的点是:major传入非0时,即我们指定系统分配该major给我们的驱动,系统会进行尝试分配,成功返回0,失败返回对应错误码,不推荐,当传入为0时,由系统自动分配空闲主设备号,此时成功将返回对应分配的设备号。使用该方法申请设备号会将所有的次设备号也占住,且全部指向同一个file_operation,即使用相同的处理函数,如果次设备需要不同处理,需要在对应的函数中做对应的区分。
/**
* register_chrdev() - Register a major number for character devices.
* @major: major device number or 0 for dynamic allocation
* @name: name of this range of devices
* @fops: file operations associated with this devices
*
* If @major == 0 this functions will dynamically allocate a major and return
* its number.
*
* If @major > 0 this function will attempt to reserve a device with the given
* major number and will return zero on success.
*
* Returns a -ve errno on failure.
*
* The name of this device has nothing to do with the name of the device in
* /dev. It only helps to keep track of the different owners of devices. If
* your module name has only one type of devices it's ok to use e.g. the name
* of the module here.
*
* This function registers a range of 256 minor numbers. The first minor number
* is 0.
*/
int register_chrdev(unsigned int major, const char *name,const struct file_operations *fops);
int unregister_chrdev(unsigned int major, const char *name);
这里暂且忽略错误情况,正常情况下需要对每一次返回值进行判断,出现错误时及时回收资源并return,这样内核才能稳定。
int major;
const char* device_name = "xxx_device";
static const struct file_operations fops= {
.owner =THIS_MODULE,
};
static int __init xxx_init(void)
{
major = register_chrdev(0, device_name, &fops);
return 0;
}
static void __exit xxx_exit(void)
{
unregister_chrdev(unsigned int major, device_name);
}
新方法中使用的接口较多,但是实际上将旧方法中的两个接口展开,就会发现,新方法只是将旧方法拆开,细节化而已。同样包含分配和回收两部分,分配分为设备号分配,cdev绑定fops和注册这两步,同样存在动态分配和指定分配的区别,拆开后的好处是同一个主设备号,可以绑定不同的fops到次设备号中,增加了设备容量,同时灵活性up。
/**
* register_chrdev_region() - register a range of device numbers
* @from: the first in the desired range of device numbers; must include
* the major number.
* @count: the number of consecutive device numbers required
* @name: the name of the device or driver.
*
* Return value is zero on success, a negative error code on failure.
*/
int register_chrdev_region(dev_t from, unsigned count, const char *name);
/**
* alloc_chrdev_region() - register a range of char device numbers
* @dev: output parameter for first assigned number
* @baseminor: first of the requested range of minor numbers
* @count: the number of minor numbers required
* @name: the name of the associated device or driver
*
* Allocates a range of char device numbers. The major number will be
* chosen dynamically, and returned (along with the first minor number)
* in @dev. Returns zero or a negative error code.
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name);
/**
* unregister_chrdev_region() - return a range of device numbers
* @from: the first in the range of numbers to unregister
* @count: the number of device numbers to unregister
*
* This function will unregister a range of @count device numbers,
* starting with @from. The caller should normally be the one who
* allocated those numbers in the first place...
*/
void unregister_chrdev_region(dev_t from, unsigned count);
/**
* cdev_alloc() - allocate a cdev structure
*
* Allocates and returns a cdev structure, or NULL on failure.
*/
struct cdev *cdev_alloc(void);
/**
* cdev_init() - initialize a cdev structure
* @cdev: the structure to initialize
* @fops: the file_operations for this device
*
* Initializes @cdev, remembering @fops, making it ready to add to the
* system with cdev_add().
*/
void cdev_init(struct cdev *cdev, const struct file_operations *fops);
/**
* cdev_add() - add a char device to the system
* @p: the cdev structure for the device
* @dev: the first device number for which this device is responsible
* @count: the number of consecutive minor numbers corresponding to this
* device
*
* cdev_add() adds the device represented by @p to the system, making it
* live immediately. A negative error code is returned on failure.
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count);
/**
* cdev_del() - remove a cdev from the system
* @p: the cdev structure to be removed
*
* cdev_del() removes @p from the system, possibly freeing the structure
* itself.
*/
void cdev_del(struct cdev *p);
同样暂时不对错误情况处理。
#include
#include
static dev_t dev;
static struct cdev *cdev_p;
const char* device_name = "xxx_device";
static const struct file_operations fops= {
.owner =THIS_MODULE,
};
static int __init xxx_init(void)
{
alloc_chrdev_region(&dev, 0, 1, device_name);
cdev_p = cdev_alloc();
cdev_init(cdev_p, &fops);
cdev_add(cdev_p, dev, 1);
return 0;
}
static void __exit xxx_exit(void)
{
cdev_del(cdev_p);
unregister_chrdev_region(dev,1);
}
至此,申请好了主次设备号并且绑定好了fops,可以编译并加载进内核看看了。编译使用make,依赖kernel的编译树,编写makefile如下:
kernel_dir = "/work/system/linux-2.6.22.6" #内核所在路径,需要提前编译好
All:
make -C $(kernel_dir) M=`pwd` modules
clean:
make -C $(kernel_dir) M=`pwd` modules clean
rm Module.symvers
obj-m += final_test.o
编译后产物为final_test.ko,将其上传至开发板中,执行加载相关操作
insmod final_test.ko #加载ko模块
lsmod #列出所有已加载的模块
rmmod #卸载已加载模块
通过 cat /proc/devices 可以看到已经将设备创建并加载完成。并分配到了252这一主设备号。但是可以看到/dev目录下并未生成我们需要的供用户空间操作的设备节点。
此时,可以通过mknod指令手动创建该设备节点。
mknod /dev/test c 252 0
但是这样并不方便,因为手动创建必须知道该设备的主次设备号,此时需要mdev机制,将我们hotplug的设备自动创建设备节点出来。这里需要mdev和/sys文件系统的共同作用。需要使用到的接口如下:
#include
/**
* class_create - create a struct class structure
* @owner: pointer to the module that is to "own" this struct class
* @name: pointer to a string for the name of this class.
*
* This is used to create a struct class pointer that can then be used
* in calls to class_device_create().
*
* Note, the pointer created here is to be destroyed when finished by
* making a call to class_destroy().
*/
struct class *class_create(struct module *owner, const char *name);
/**
* class_device_create - creates a class device and registers it with sysfs
* @cls: pointer to the struct class that this device should be registered to.
* @parent: pointer to the parent struct class_device of this new device, if any.
* @devt: the dev_t for the char device to be added.
* @device: a pointer to a struct device that is assiociated with this class device.
* @fmt: string for the class device's name
*
* This function can be used by char device classes. A struct
* class_device will be created in sysfs, registered to the specified
* class.
* A "dev" file will be created, showing the dev_t for the device, if
* the dev_t is not 0,0.
* If a pointer to a parent struct class_device is passed in, the newly
* created struct class_device will be a child of that device in sysfs.
* The pointer to the struct class_device will be returned from the
* call. Any further sysfs files that might be required can be created
* using this pointer.
*
* Note: the struct class passed to this function must have previously
* been created with a call to class_create().
*/
struct class_device *class_device_create(struct class *cls,
struct class_device *parent,
dev_t devt,
struct device *device,
const char *fmt, ...);
void class_device_unregister(struct class_device *class_dev);
/**
* class_destroy - destroys a struct class structure
* @cls: pointer to the struct class that is to be destroyed
*
* Note, the pointer to be destroyed must have been created with a call
* to class_create().
*/
void class_destroy(struct class *cls);
#include
#include
#include
#include
#include
#define BASE_MINOR 0
#define MAX_DEVICE_NUM 1
int major;
static dev_t dev;
static struct cdev *cdev_p;
const char* device_name = "xxx_device";
static struct class* xxx_clsp;
static struct class_device* xxx_cls_devp[MAX_DEVICE_NUM];
static const struct file_operations fops= {
.owner =THIS_MODULE,
};
static int __init xxx_init(void)
{
int i=0;
alloc_chrdev_region(&dev, BASE_MINOR, MAX_DEVICE_NUM, device_name);
cdev_p = cdev_alloc();
cdev_init(cdev_p, &fops);
cdev_add(cdev_p, dev, MAX_DEVICE_NUM);
xxx_clsp = class_create(THIS_MODULE,"xxx_class");
major=MAJOR(dev);
for(i=BASE_MINOR;i < MAX_DEVICE_NUM + BASE_MINOR;++i)
xxx_cls_devp[i] = class_device_create(xxx_clsp ,NULL,MKDEV(major,i), NULL, "xxx%d",i);
return 0;
}
static void __exit xxx_exit(void)
{
int i;
for(i=BASE_MINOR;i < MAX_DEVICE_NUM + BASE_MINOR;++i)
class_device_unregister(xxx_cls_devp[i]);
class_destroy(xxx_clsp);
cdev_del(cdev_p);
unregister_chrdev_region(dev,MAX_DEVICE_NUM);
}
module_init(xxx_init);
module_exit(xxx_exit);
MODULE_AUTHOR("CryptonymAMS");
MODULE_VERSION("1.0.0");
MODULE_DESCRIPTION("hello world!");
MODULE_LICENSE("GPL");
至此,可以看到设备节点已经创建完成。后续将在此基本框架上增加需要的功能。