写了一个LED的驱动程序和测试程序
字符设备是3大类设备(字符设备、块设备、网络设备)中较简单的一类设备、其驱动程序中完成的主要工作是初始化、添加和删除 struct cdev 结构体,申请和释放设备号,以及填充 struct file_operations 结构体中断的操作函数,实现 struct file_operations 结构体中的read()、write()和ioctl()等函数是驱动设计的主体工作。
在Linux内核中,使用cdev结构体来描述一个字符设备,cdev结构体的定义如下:
struct cdev {
struct kobject kobj; //内嵌的内核对象.
struct module *owner; //该字符设备所在的内核模块的对象指针.
const struct file_operations *ops; //该结构描述了字符设备所能实现的方法,是极为关键的一个结构体.
struct list_head list; //用来将已经向内核注册的所有字符设备形成链表.
dev_t dev; //字符设备的设备号,由主设备号和次设备号构成.
unsigned int count; //隶属于同一主设备号的次设备号的个数.
};
先说驱动程序,主要实现了一下几个函数test_chrdev_open(),test_chrdev_release(),test_chrdev_read(),test_chrdev_write(),
这几个函数分别对应了上层的open(),close(),read(),write()函数,当上层用户空间调用open(),write()等函数时,实际上在内核空间执行的是对应的test_chrdev_open(),test_chrdev_write()这些函数。
那么大家就会有疑问了,这样的机制是怎么实现的呢?请看下面的代码
static const struct file_operations test_fops = {
.owner = THIS_MODULE, // 惯例,直接写即可
.open = test_chrdev_open, // 将来应用open打开这个设备时实际调用的
.release = test_chrdev_release, // 就是这个.open对应的函数
.write = test_chrdev_write,
.read = test_chrdev_read,
};
file_operations结构体中的成员函数是字符设备驱动程序设计的主体内容,这些函数实际会在应用程序进行Linux的open()、write()、read()、close()等系统调用时最终被内核调用。file_operations结构体目前已经非常庞大,我们在进行驱动开发的时候不必全部实现这些函数。
接下来是模块安装函数static int __init leddev_init(void)
在模块安装函数中主要进行了一下几个步骤:
第1步:注册/分配主次设备号
mydev = MKDEV(MYMAJOR, 0);
retval = register_chrdev_region(mydev, MYCNT, MYNAME);
第2步:注册字符设备驱动
cdev_init(&test_cdev, &test_fops); //cdev和file_operations建立关系
retval = cdev_add(&test_cdev, mydev, MYCNT);
第3步:使用动态映射的方式来操作寄存器
if (!request_mem_region(GPJ2CON_PA, 4, "GPJ2CON"))
return -EINVAL;
if (!request_mem_region(GPJ2DAT_PA, 4, "GPJ2CON"))
return -EINVAL;
pGPJ2CON = (volatile unsigned int *)ioremap(GPJ2CON_PA, 4);
pGPJ2DAT = (volatile unsigned int *)ioremap(GPJ2DAT_PA, 4);
最后是模块卸载函数static void __exit leddev_exit(void)
在模块卸载函数中主要进行了一下几个步骤:
第1步:解除地址映射
iounmap(pGPJ2CON);
iounmap(pGPJ2DAT);
release_mem_region(GPJ2CON_PA, 4);
release_mem_region(GPJ2DAT_PA, 4);
第2步:注销字符设备驱动
cdev_del(&test_cdev); //真正注销字符设备驱动用cdev_del
unregister_chrdev_region(mydev, MYCNT); // 注销申请的主次设备号
下面是驱动源码:
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/string.h>
#include <linux/io.h>
#include <linux/ioport.h>
#include <linux/cdev.h>
#include <asm/irq.h>
#include "asm/io.h"
#include "linux/kernel.h"
#include "linux/delay.h"
#define MYMAJOR 200
#define MYCNT 1
#define MYNAME "testchar"
#define GPJ2CON_PA 0xe0200280
#define GPJ2DAT_PA 0xe0200284
volatile unsigned int *pGPJ2CON;
volatile unsigned int *pGPJ2DAT;
int mymajor;
static dev_t mydev;
static struct cdev test_cdev;
char kbuf[100]; // 内核空间的buf
static int test_chrdev_open(struct inode *inode, struct file *file)
{
// 这个函数中真正应该放置的是打开这个设备的硬件操作代码部分
// 但是现在暂时我们写不了这么多,所以用一个printk打印个信息来做代表。
printk(KERN_INFO "test_chrdev_open\n");
// 配置GPJ2_0,GPJ2_1,GPJ2_2,GPJ2_3为输出
*pGPJ2CON &= ~((0xf<<(0*4)) | (0xf<<(1*4)) | (0xf<<(2*4)) | (0xf<<(3*4)));
*pGPJ2CON |= ((0x1<<(0*4)) | (0x1<<(1*4)) | (0x1<<(2*4)) | (0x1<<(3*4)));
return 0;
}
static int test_chrdev_release(struct inode *inode, struct file *file)
{
*pGPJ2DAT |= ((0x1<<(0*1)) | (0x1<<(1*1)) | (0x1<<(2*1)) | (0x1<<(3*1)));
printk(KERN_INFO "test_chrdev_release\n");
return 0;
}
ssize_t test_chrdev_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "test_chrdev_read\n");
ret = copy_to_user(ubuf, kbuf, count);
if (ret)
{
printk(KERN_ERR "copy_to_user fail\n");
return -EINVAL;
}
printk(KERN_INFO "copy_to_user success..\n");
return 0;
}
// 写函数的本质就是将应用层传递过来的数据先复制到内核中,然后将之以正确的方式写入硬件完成操作。
static ssize_t test_chrdev_write(struct file *file, const char __user *ubuf,
size_t count, loff_t *ppos)
{
int ret = -1;
printk(KERN_INFO "test_chrdev_write\n");
// 使用该函数将应用层传过来的ubuf中的内容拷贝到驱动空间中的一个buf中
//memcpy(kbuf, ubuf); // 不行,因为2个不在一个地址空间中
memset(kbuf, 0, sizeof(kbuf));
ret = copy_from_user(kbuf, ubuf, count);
if (ret)
{
printk(KERN_ERR "copy_from_user fail\n");
return -EINVAL;
}
printk(KERN_INFO "copy_from_user success..\n");
if (kbuf[0] == '1')
{
*pGPJ2DAT &= ~((1<<0) | (1<<1) | (1<<2) | (1<<3));
}
else if (kbuf[0] == '0')
{
*pGPJ2DAT |= (1<<0) | (1<<1) | (1<<2) | (1<<3);
}
return 0;
}
// 自定义一个file_operations结构体变量,并且去填充
static const struct file_operations test_fops = {
.owner = THIS_MODULE, // 惯例,直接写即可
.open = test_chrdev_open, // 将来应用open打开这个设备时实际调用的
.release = test_chrdev_release, // 就是这个.open对应的函数
.write = test_chrdev_write,
.read = test_chrdev_read,
};
// 模块安装函数
static int __init leddev_init(void)
{
int retval;
printk(KERN_INFO "chrdev_init helloworld init\n");
/*
// 在module_init宏调用的函数中去注册字符设备驱动
// major传0进去表示要让内核帮我们自动分配一个合适的空白的没被使用的主设备号
// 内核如果成功分配就会返回分配的主设备好;如果分配失败会返回负数
mymajor = register_chrdev(0, MYNAME, &test_fops);
if (mymajor < 0)
{
printk(KERN_ERR "register_chrdev fail\n");
return -EINVAL;
}
printk(KERN_INFO "register_chrdev success... mymajor = %d.\n", mymajor);
*/
// 使用新的cdev接口来注册字符设备驱动
// 新的接口注册字符设备驱动需要2步
// 第1步:注册/分配主次设备号
mydev = MKDEV(MYMAJOR, 0);
retval = register_chrdev_region(mydev, MYCNT, MYNAME);
if (retval) {
printk(KERN_ERR "Unable to register minors for %s\n", MYNAME);
return -EINVAL;
}
printk(KERN_INFO "register_chrdev_region success\n");
// 第2步:注册字符设备驱动
cdev_init(&test_cdev, &test_fops);//cdev和file_operations建立关系
retval = cdev_add(&test_cdev, mydev, MYCNT);
if (retval) {
printk(KERN_ERR "Unable to cdev_add\n");
return -EINVAL;
}
printk(KERN_INFO "cdev_add success\n");
//使用动态映射的方式来操作寄存器
if (!request_mem_region(GPJ2CON_PA, 4, "GPJ2CON"))
return -EINVAL;
if (!request_mem_region(GPJ2DAT_PA, 4, "GPJ2CON"))
return -EINVAL;
pGPJ2CON = (volatile unsigned int *)ioremap(GPJ2CON_PA, 4);
pGPJ2DAT = (volatile unsigned int *)ioremap(GPJ2DAT_PA, 4);
return 0;
}
// 模块下载函数
static void __exit leddev_exit(void)
{
printk(KERN_INFO "chrdev_exit helloworld exit\n");
*pGPJ2DAT |= (1<<0) | (1<<1) | (1<<2) | (1<<3);
// 解除映射
iounmap(pGPJ2CON);
iounmap(pGPJ2DAT);
release_mem_region(GPJ2CON_PA, 4);
release_mem_region(GPJ2DAT_PA, 4);
/*
// 在module_exit宏调用的函数中去注销字符设备驱动
unregister_chrdev(mymajor, MYNAME);
*/
// 使用新的接口来注销字符设备驱动
// 注销分2步:
// 第一步真正注销字符设备驱动用cdev_del
cdev_del(&test_cdev);
// 第二步去注销申请的主次设备号
unregister_chrdev_region(mydev, MYCNT);
}
module_init(leddev_init);
module_exit(leddev_exit);
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证