目录
一,字符设备驱动开发框架
开发字符设备驱动的要素:
- 必须有一个设备号,用于在众多设备驱动中进行区分。
- 用户必须知道设备驱动对应的设备结点(设备文件,在Linux中所有设备都是文件)。
- 对设备操作其实就是对文件进行操作,应用空间中操作open(),read()…的时候,实际在驱动代码中有对应的open(),read()…
二,申请设备号和创建文件结点
1.申请设备号
申请设备号函数:register_chrdev(unsigned int major, const char * name, const struct file_operations * fops)
头文件包含:#include <linux/fs.h>
参数:
参数1:主设备号
设备号(32bit) = 主设备号(高12bit) + 次设备号 (低20bit)
主设备号的给定方式有两种:
1. 动态分配 ------> 参数1直接给定0
2. 静态分配 -------> 指定一个整数(通常需要用命令cat /proc/devices 查看被占用的主设备号,避免定义重复的主设备号)
参数2:描述一个设备信息,可自定义
参数3:文件操作对象file_operation指针
返回值:
正确返回0,错误返回负数
注销设备号函数:unregister_chrdev(unsigned int major, const char * name)
------------- 参数同上
2.创建设备结点
- 手动创建 ------- 缺点:/dev/目录中的文件都是在内存中,断电后这些文件会消失
手动创建文件结点:mknod /dev/设备名 类型 主设备号 次设备号
比如:mknod /dev/chr0 c 250 0
--------可以通过ls /dev/chr0 -l
查看
- 自动创建(通过udev/mdev机制):创建类 + 创建设备
创建类函数:struct class *class_create(owner, name)
参数:
参数1:THIS_MODULE,类似于C++中的this指针
参数2:类的名字,字符串格式,自定义
返回值:
返回一个class指针
销毁类函数:void class_destroy(struct class * cls)
创建设备文件:
创建设备函数:device_create(struct class * cls, struct device * parent, dev_t devt, void * drvdata, const char * fmt,...)
头文件包含:#include <linux/device.h>
参数:
参数1:class结构体,class_create();调用之后的返回值
参数2:表示父类,一般填NULL
参数3:设备号类型,一般由MKDEV(ma,mi)宏来定义,ma为主设备号,mi为次设备号
参数4:私有数据,一般填NULL
参数5和参数6:格式化可变参数,字符串格式,表示设备结点中的名字
销毁设备函数:device_destroy(struct class * class, dev_t devt)
3.在驱动中实现文件IO的接口,让应用程序可以调用文件IO
file_operations结构体:
文件操作对象,实质就是一些函数指针的集合,需要我们自己去实现。
先填充结构体:
函数名自定义,要与实现的函数名对应即可。
const struct file_operations my_fops = {
.open = chr_dev_open,
.read = chr_dev_read,
.write = chr_dev_write,
.release = chr_dev_close,
};
根据需求实现函数:
举例:(没有意义,只是举例说明)
int chr_dev_open (struct inode *inode, struct file *filp)
{
printk("-----%s-----\n",__FUNCTION__);
return 0;
}
int chr_dev_close (struct inode *inode, struct file *filp)
{
printk("-----%s-----\n",__FUNCTION__);
return 0;
}
三,用户控制驱动和驱动控制硬件方式
1.用户空间和内核空间的数据交互
copy_to_user(void __user * to, const void * from, unsigned long n)
将数据从内核空间拷贝到用户空间,一般在驱动中的xxx_read()中用。
参数:
参数1:应用空间的buffer
参数2:内核空间的buffer
参数3:个数
返回值:
大于0,表示出错,剩下多少个没拷贝成功
等于0,表示正确
copy_from_user(void * to, const void __user * from, unsigned long n)
将数据从用户空间拷贝到内核空间,一般在驱动中的xxx_write()中用。
参数:
参数1:内核空间的buffer
参数2:应用空间的buffer
参数3:个数
头文件包含:#include <asm/uaccess.h>
2.用户操作驱动与控制外设的关系
内核驱动中是通过虚拟地址转换操作寄存器:
void* ioremap(cookie, size); //映射函数
void iounmap(void __iomen*addr); //去映射函数
映射函数参数:
参数1:物理地址
参数2:长度
返回值:虚拟地址
去映射函数参数:映射成功的虚拟地址
头文件包含:#include <asm/io.h>
四,编写字符设备驱动的步骤和规范
1.驱动编写步骤
1.1实现模块的加载和卸载入口函数
module_init(led_dev_init);
module_exit(led_dev_exit);
MODULE_LICENSE("GPL"); // 描述模块的许可证
1.2在模块加载函数的入口函数中实现下面步骤
[ 1 ] 申请主设备号(内核中用于区分和管理不同的字符设备)
register_chrdev(0, "led_dev_test", &my_fops); //动态分配主设备号,且分配成功返回主设备号
[ 2 ] 创建设备节点(为用户提供一个可操作的文件接口)
struct class* class_create(THIS_MODULE, "dev_class");
struct devices* device_create(led_dev->myclass, NULL, MKDEV(led_dev->dev_major,0), NULL, "led%d",0);
[ 3 ] 硬件的初始化
- 地址的映射
ioremap(GPJ0CON, GPJ0_SIZE);
- 中断的申请
- 实现硬件的寄存器的初始化
[ 4 ] 实现file_operations
const struct file_operations my_fops = {
.open = led_dev_open,
.read = led_dev_read,
.write = led_dev_write,
.release = led_dev_close,
};
2.驱动编写规范
2.1 面向对象的编程思想
设计一个结构体类型,描述一个设备的所有信息
struct led_desc{
unsigned int dev_major; //主设备号
struct class* myclass;
struct device* mydev; //创建设备文件的类和设备
void *reg_virt_base; //表示进行虚拟映射后寄存器地址的基准值
};
struct led_desc *led_dev; //声明全局的设备对象
在模块加载函数static int __init led_dev_init(void) //一般都是申请系统资源
中实例化全局的设备对象
//实例化全局的设备对象----分配空间
led_dev = kmalloc(sizeof(struct led_desc), GFP_KERNEL); //GFP_KERNEL表示当前内存不够用的时候,该函数就会一直阻塞
if(led_dev == NULL)
{
printk(KERN_ERR "malloc error.\n");
return -ENOMEM;
}
2.2出错处理
在某个位置出错了,要将之前申请的资源全部释放。
2.3操作寄存器地址的方式
- 传统方式
volatile unsigned long* gpjocon;
*gpjocon &= ~(0xf << 12);
- 内核推荐方式readl();/writel();
readl()作用:从给定的地址中读取地址空间的值,即解引用
u32 readl(const volatile void__iomen* addr);
writel()作用:把value的值写到给定的addr地址中
void writel(unsigned long value ,const volatile void__iomen* addr);
五,编写规范LED驱动实例
1.先看原理图和有关寄存器
2.驱动代码:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/slab.h>
struct led_desc{
unsigned int dev_major; //主设备号
struct class* myclass;
struct device* mydev; //创建设备文件的类和设备
void *reg_virt_base; //表示进行虚拟映射后寄存器地址的基准值
}*led_dev; //声明全局的设备对象
#define GPJ0CON 0xE0200240 // GPJ0CON寄存器物理地址
#define GPJ0_SIZE 8
static int kernel_val = 555; //内核空间定义的一个值,可以看成是一段4字节的空间,模拟和用户空间进行数据交互
ssize_t led_dev_read (struct file *filp, char __user *buf, size_t count, loff_t *fpos)
{
printk("-----%s-----\n",__FUNCTION__);
int ret;
ret = copy_to_user(buf,&kernel_val, count);
if(ret > 0)
{
printk("copy_to_user error.\n");
return -EFAULT;
}
return 0;
}
ssize_t led_dev_write (struct file *filp, const char __user *buf, size_t count, loff_t *fpos)
{
printk("-----%s-----\n",__FUNCTION__);
int ret;
int value;
ret = copy_from_user(&value,buf,count);
if(ret > 0)
{
printk("copy_from_user error.\n");
return -EFAULT;
}
if(value) //根据用户空间传过来的value实现LED的亮灭
{
writel(readl(led_dev->reg_virt_base + 4) & ~(1<<3),led_dev->reg_virt_base + 4); //led亮
}
else
{
writel(readl(led_dev->reg_virt_base + 4) | (1<<3),led_dev->reg_virt_base + 4); //led灭
}
return 0;
}
int led_dev_open (struct inode *inode, struct file *filp)
{
printk("-----%s-----\n",__FUNCTION__);
return 0;
}
int led_dev_close (struct inode *inode, struct file *filp)
{
printk("-----%s-----\n",__FUNCTION__);
return 0;
}
const struct file_operations my_fops = {
.open = led_dev_open,
.read = led_dev_read,
.write = led_dev_write,
.release = led_dev_close,
};
static int __init led_dev_init(void) //一般都是申请系统资源
{
int ret;
//0-实例化全局的设备对象----分配空间
led_dev = kmalloc(sizeof(struct led_desc), GFP_KERNEL);
if(led_dev == NULL)
{
printk(KERN_ERR "malloc error.\n");
return -ENOMEM;
}
// 1-申请设备号
led_dev->dev_major = register_chrdev(0, "led_dev_test", &my_fops); //动态分配主设备号,且分配成功返回主设备号
if(led_dev->dev_major < 0)
{
printk(KERN_ERR "register_chrdev error.\n");
ret = -ENODEV;
goto err_0;
}
// 2-创建设备结点
led_dev->myclass = class_create(THIS_MODULE, "dev_class");
if(IS_ERR(led_dev->myclass)) //IS_ERR判断指针是否出错
{
printk(KERN_ERR "class_create error.\n");
ret = PTR_ERR(led_dev->myclass); //PTR_ERR将指针的错误原因转换成错误码
goto err_1;
}
led_dev->mydev = device_create(led_dev->myclass, NULL, MKDEV(led_dev->dev_major,0), NULL, "led%d",0);
if(IS_ERR(led_dev->mydev))
{
printk(KERN_ERR "device_create error.\n");
ret = PTR_ERR(led_dev->mydev);
goto err_2;
}
// 3-硬件初始化
//对地址进行映射
led_dev->reg_virt_base = ioremap(GPJ0CON, GPJ0_SIZE);
if(led_dev->reg_virt_base == NULL)
{
printk(KERN_ERR "ioremap error.\n");
ret = -ENOMEM;
goto err_3;
}
//gpio的输出功能的配置
u32 value = readl(led_dev->reg_virt_base);
value &= ~(0xf<<12); //先清零
value |= (0x1<<12);
writel(value,led_dev->reg_virt_base); //设置GPJ0的[15:12]为输出模式
return 0;
//错误处理
err_3:
device_destroy(led_dev->myclass, MKDEV(led_dev->dev_major,0));
err_2:
class_destroy(led_dev->myclass);
err_1:
unregister_chrdev(led_dev->dev_major,"led_dev_test");
err_0:
kfree(led_dev);
}
static void __exit led_dev_exit(void)
{
//一般都是释放资源
iounmap(led_dev->reg_virt_base);
device_destroy(led_dev->myclass, MKDEV(led_dev->dev_major,0));
class_destroy(led_dev->myclass);
unregister_chrdev(led_dev->dev_major,"led_dev_test");
kfree(led_dev);
printk("--------%s-------\n",__FUNCTION__);
}
module_init(led_dev_init);
module_exit(led_dev_exit);
MODULE_LICENSE("GPL"); // 描述模块的许可证
3.应用层代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
int fd;
int value;
fd = open("/dev/led0",O_RDWR); //根据 device_create的最后一个参数确定设备结点的名字
if(fd < 0)
{
perror("open");
exit(-1);
}
read(fd,&value,4);
printf("---USER----:%d\n",value);
//应用程序去控制LED的亮灭
while(1)
{
value = 0;
write(fd,&value,4);
sleep(1);
value = 1;
write(fd,&value,4);
sleep(1);
}
close(fd);
return 0;
}
最终能实现LED的闪烁。