一、驱动流程
驱动需要以下几个步骤才能完成对硬件的访问和操作:
- 模块加载函数 module_init
- 注册主次设备号 <应用程序通过设备号找到设备>
- 驱动设备文件 <应用程序访问驱动的方式> 1、手动创建 (mknod)2、程序自动创建
- file_operations <驱动对硬件的读、写、释放等>
- 模块卸载函数 module_exit
二、举例详解
1、第一种向内核注册字符设备
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
#include <linux/fs.h>
#define MYMAJOR 200
#define MYNAME "LED_DEVICE"
//int (*open) (struct inode *, struct file *);
//open函数的格式是上面的格式:
static int led_dev_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "led_dev_open open\n");
}
//release函数的原型是:int (*release) (struct inode *, struct file *);
static int led_dev_close(struct inode *inode, struct file *file)
{
printk(KERN_INFO "led_dev_close close\n");
}
static const struct file_operations led_dev_fops{
.opne = led_dev_open,
.release = led_dev_close,
}
static int __init leddev_init(void)
{
int ret = -1;
printk(KERN_INFO "leddev_init");
ret = register_chrdev(MYMAJOR, MYNAME, &led_dev_fops);
if(ret) {
printk(KERN_ERR "led devices rigister failed");
retunt -EINVAL;
}
printk(KERN_INFO "led regist sucess");
return 0;
}
static int __exit leddev_exit(void)
{
printfk(KERN_INFO "led device exit");
unregister_chrdev(MYMAJOR, NAME);
}
module_init(leddev_init);module_exit(leddev_exit);
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("bhc"); // 描述模块的作者
MODULE_DESCRIPTION("led test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
注:
通过对驱动的流程进行分析,以上代码中缺少对设备节点的创建,也就是说,上边的代码,应用程序是没有方法进行访问和操作的,这时,我们可以通过手动的方式进行处理,即使用mknod
进行创建,
应用调用驱动是通过驱动设备文件来调用驱动的,我们首先要用mknod /dev/xxx c
主设备号 次设备号 命令来创建驱动设备文件
安装好驱动以后,主设备号可以在/proc/devices
文件中查看,但是由于不同的设备主设备号占用的不一样,有时候需要系统来自动分配
主设备号,这个如何实现呢:
我们可以在register_chrdev
函数的major变量传参0进去,因为这个函数的返回值为主设备号,所以我们定义一个全局变量来接受这个值即可
static int mymajor;
//注册的时候
mymajor = register_chrdev(0, MYNAME, &ded_dev_fops); # 返回的是自动分配的主设备号
//释放的时候
unregister_chrdev(mymajor, MYNAME);
这样即可;
register_chrdev(major, name, struct file_openrations) # 注册设备号,缺点是只能注册主设备号
unregister_chrdev(major, name) # 注销设备号
1、第二种向内核注册字符设备
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <plat/map-base.h>
#include <plat/map-s5p.h>
#include <mach/regs-gpio.h>
#include <mach/gpio-bank.h>
#include <linux/ioport.h>
#include <linux/string.h>
#include <asm/io.h>
#include <linux/cdev.h>
//#define MYMAJOR 200
//#define MYNAME "LED_DEVICE"
#define MYDEV 250
#define LED_COUNT 1
#define GPJ0_PA_base 0xE0200240
#define GPJ0CON_PA_OFFSET 0x0
struct cdev my_led_cdev;
unsigned int *pGPJ0CON;
unsigned int *pGPJ0DAT;
static char kbuf[100];
static int mymojor;
static int led_dev_open(struct inode *inode, struct file *file)
{
printk(KERN_INFO "led_dev open\n");
return 0;
}
static int led_dev_release(struct inode *inode, struct file *file)
{
printk(KERN_INFO "led_dev close\n");
return 0;
}
ssize_t led_dev_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
int ret = -1;
ret = copy_to_user(buf, kbuf, sizeof(kbuf));
if(ret) {
printk(KERN_ERR "kernel led read error\n");
}
printk(KERN_INFO "led device read success\n");
}
static ssize_t led_dev_write(struct file *file, const char __user *user_buf, size_t count, loff_t *ppos)
{
int ret = -1;
//首先把kbuf清零
memset(kbuf, 0, sizeof(kbuf));
ret = copy_from_user(kbuf, user_buf, count);
if(ret) {
printk(KERN_ERR "kernel led write error\n");
return -EINVAL;
}
printk(KERN_INFO "led device write success\n");
if (kbuf[0] == '1') {
*pGPJ0CON = 0x11111111;
*(pGPJ0CON + 1) = ((0<<3) | (0<<4) | (0<<5));
}
if (kbuf[0] == '0') {
*(pGPJ0CON + 1) = ((1<<3) | (1<<4) | (1<<5));
}
return 0;
}
static const struct file_operations led_dev_fops = {
.open = led_dev_open,
.write = led_dev_write,
.read = led_dev_read,
.release = led_dev_release,
.owner = THIS_MODULE,
};
// 模块安装函数
static int __init leddev_init(void)
{
int err = 0;
printk(KERN_INFO "led_device init\n");
//在这里进行注册驱动,因为安装驱动实际上执行的就是这个函数;
err = register_chrdev_region(MKDEV(MYDEV,0), LED_COUNT, "MY_LED_DEV");
if(err)
{
printk(KERN_ERR " register_chrdev_region failed\n");
return -EINVAL;
}
printk(KERN_INFO "leddev_dev regist success\n");
cdev_init(&my_led_cdev, &led_dev_fops);
cdev_add(&my_led_cdev, MKDEV(MYDEV,0), LED_COUNT);
if(!request_mem_region(GPJ0_PA_base + GPJ0CON_PA_OFFSET, 8, "GPJ0PABAST")) {
return -EINVAL;
}
pGPJ0CON = ioremap(GPJ0_PA_base + GPJ0CON_PA_OFFSET, 4);
return 0;
}
// 模块下载函数
static void __exit leddev_exit(void)
{
printk(KERN_INFO "leddev_dev exit\n");
//注销led设备驱动
cdev_del(&my_led_cdev);
unregister_chrdev_region(MKDEV(MYDEV,0), LED_COUNT);
iounmap(GPJ0_PA_base + GPJ0CON_PA_OFFSET);
release_mem_region(GPJ0_PA_base + GPJ0CON_PA_OFFSET, 8);
printk(KERN_INFO "leddev_dev unregist success\n");
}
module_init(leddev_init);
module_exit(leddev_exit);
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("bhc"); // 描述模块的作者
MODULE_DESCRIPTION("led test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
注:
(1)第二种注册函数
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
以上是第一种注册字符设备的方式,这种方式无法设置次设备号。本次的设备如下所示:
int register_chrdev_region(dev_t from, unsigned count, const char *name) # 注册设备
void cdev_init(struct cdev *, const struct file_operations *); # 初始化函数
int cdev_add(struct cdev *p, dev_t dev, unsigned count) # 添加设备函数
void cdev_del(struct cdev *p) # 删除设备函数
void unregister_chrdev_region(dev_t from, unsigned count) # 卸载设备
(2)主次设备号注册
linux
内核为我们提供了三个宏来确定from、主设备号、次设备号
MKDEV、MAJOR、NIMOR
,这么是这三个宏在linux
内核中定义的用法:
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
比如说我们的主设备号为250
第一个次设备号为0
,那么from应该赋值的参数为MKDEV(250, 0)
知道dev的话 主设备号为 MAJOR(dev)
、次设备号为:MINOR(dev)
(3)内核和驱动之间的数据交互
static inline long copy_to_user(void __user *to, const void *from, unsigned long n); # 从驱动到用户空间
static inline long copy_from_user(void *to, const void __user * from, unsigned long n) # 从用户空间到驱动
(4)虚拟空间映射
下面我们的低层驱动开始真正的操作硬件了:
在操作硬件的时候,我们会用到硬件的寄存器,因为我们之前在逻辑程序中使用的物理地址来直接写的,而我们在开发板上移植好内核以后
我们的内核程序就是运行在虚拟地址上了,所以我们要看一下我们的linux内核中虚拟地址跟物理地址是如何映射的
动态虚拟地址:当我们要使用这个寄存器的物理地址的时候,不用事先建立好的页表,而是给物理地址动态的分配一个虚拟地址,操作的时候直接使用这个动态分配的虚拟地址
操作物理地址即可,使用完以后取消映射即可;
使用动态虚拟地址映射首先:
1:建立映射
使用request_mem_region向内核申请虚拟地址空间;
#define request_mem_region(start,n,name) __request_region(&iomem_resource, (start), (n), (name), 0)
request_mem_region实际上是一个宏,真正调用的是 __request_region这个函数;
request_mem_region宏需要三个参数:start:启示的物理地址,n长度,name
申请成功则返回0;
ioremap
#define ioremap(cookie,size) __arm_ioremap(cookie, size, MT_DEVICE)
也是一个宏,调用的是内核函数__arm_ioremap
这个宏需要两个参数起始物理地址以及 长度;
2:使用完以后我们首先要消除映射
取消映射iounmap宏
#define iounmap(cookie) __iounmap(cookie)
接受一个参数,起始物理地址;
然后在消除分配的虚拟地址
使用release_mem_region
#define release_mem_region(start,n) __release_region(&iomem_resource, (start), (n))
这个宏只需要两个参数即可一个是起始物理地址,一个长度;