目录
1.单字节设备和多字节设备的区别
对于Linux设备而言,一共有3种设备,即字符设备、块设备和网络设备。
首先我们先来说说单个字节设备属于哪一类的设备,就字符设备而言,有分为单字节和多字节设备。顾名思义:单字节设备:就是最大只能对设备进行读取或者写入一个字节的数据,常见的有:led设备、按键设备,这种设备只会呈现出两种状态。而多字节设备:可以写入或者读取多个字节的,常见的有存储设备。
今天的话,主要是针对单字节设备(以LED设备为例)进行分析。
先说明一下开发环境:
Ubuntu 16.04
不涉及任何硬件的相关操作(如果有读者,想要移植到arm-linux开发板上运行,则需要在对应的函数上增加相关硬件操作,以及修改Makefile的编译器即可)
按照上一篇文章的套路,继续来分析一下:
2.单字节设备(LED设备驱动)的驱动套路
2.1 头文件
#include <linux/module.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/cdev.h> //使用的是cdev
#include <linux/uaccess.h> //copy_to_user copy_from_user
#include <linux/slab.h>
//如果要涉及硬件,请包含相关头文件
2.2 定义设备驱动相关的变量
#define LED_MAJOR 0 /*采用动态的方式创建*/
#define DEVICE_NAME "my_led" //设备的名称
static int led_major = LED_MAJOR;
/*led设备结构体*/
typedef struct LED_CDEV
{
struct cdev cdev;//定义一个cdev结构体
unsigned char led_value;/*led 的值,属于私有数据*/
}led_cdev_t;
static led_cdev_t *g_led_cdv;
2.3 编写file operations 相关操作的函数
2.3.1 open函数
/*打开设备*/
static int led_dev_open(struct inode *inode,struct file *filep)
{
printk(KERN_INFO"%s\r\n",__FUNCTION__);
filep->private_data = g_led_cdv;
//如果涉及到硬件操作,在这里申请gpio资源即可
return 0;
}
2.3.2 release函数
/*文件释放*/
static int led_dev_release(struct inode *inode,struct file *filep)
{
printk(KERN_INFO"%s\r\n",__FUNCTION__);
//如果涉及到硬件相关操作,在这里释放掉gpio资源即可
return 0;
}
2.3.3 write函数
/*写函数*/
static ssize_t led_status_write(struct file *filep,const char __user *buf,
size_t size,loff_t *ppos)
{
printk(KERN_INFO"%s\r\n",__FUNCTION__);
int ret = 0;
led_cdev_t *dev = filep->private_data;
if(copy_from_user(&dev->led_value,buf,1))//从用户空间拷贝数据到内核空间
{
ret = -EFAULT;
}
else
{
printk(KERN_INFO"write:dev->led_value=%d\r\n",dev->led_value);
//涉及到硬件,直接在这里操作即可
ret = 1;
}
return ret;
}
使用copy_from_user()可以向内核传递一个数据块,如果只需要传递一个char、short、或者int类型的数据,其实也可以使用get_user()函数
它们函数原型如下所示:
/*
to : 内核空间缓冲区地址
from : 用户空间地址
n : 数据字节数
返回值: 不能被复制的字节数,返回 0 表示全部复制成功
*/
static inline unsigned long __must_check copy_from_user(void *to, const void __user *from, unsigned long n);
/*
x : 内核空间的数据
p : 用户空间的指针
获取成功,返回 0,否则返回-EFAULT
*/
int get_user(x, p);
2.3.4 read函数
static ssize_t led_status_read(struct file *filep,char __user *buf,size_t size,
loff_t *ppos)
{
printk(KERN_INFO"%s\r\n",__FUNCTION__);
int ret = 0;
led_cdev_t *dev = filep->private_data;/*获得设备结构体指针*/
if(copy_to_user(buf,&dev->led_value,1)) /*内核空间到用户空间拷贝*/
{
ret = -EFAULT;
}
else
{
printk(KERN_INFO"read:dev->led_value=%d\r\n",dev->led_value);
//涉及到硬件的操作,直接在这里操作即可
ret = 1;/*返回具体的个数*/
}
return ret;
}
使用copy_to_user()可以向用户空间传递一个数据块,如果只需要传递一个char、short、或者int类型的数据,其实也可以使用put_user()函数
它们函数原型如下所示:
/*
to : 内核空间缓冲区地址
from : 用户空间地址
n : 数据字节数
返回值: 不能被复制的字节数,返回 0 表示全部复制成功
*/
static inline unsigned long __must_check copy_to_user(void *to, const void __user *from, unsigned long n);
/*
x : 内核空间的数据
p : 用户空间的指针。
ret : 传递成功,返回 0,否则返回-EFAULT
*/
int put_user(x,p);
2.3.5 填充file operations结构体
/*填充file operations 结构体*/
static const struct file_operations led_dev_fops =
{
.owner = THIS_MODULE,
.open = led_dev_open,
.release = led_dev_release,
.read = led_status_read,
.write = led_status_write
};
2.4 模块的初始化和卸载
2.4.1 模块的初始化
/*设备驱动模块加载函数*/
static int __init led_module_init(void)
{
printk(KERN_INFO"%s\r\n",__FUNCTION__);
int result = 0;
dev_t dev_no = MKDEV(led_major,0);
/*1.申请设备号*/
if(led_major)
{
result = register_chrdev_region(dev_no,1,DEVICE_NAME);
}
else
{
/*动态申请设备号*/
result = alloc_chrdev_region(&dev_no,0,1,DEVICE_NAME);
led_major = MAJOR(dev_no);
}
if(result < 0)
{
return result;
}
/*2.动态申请设备结构体的内存*/
g_led_cdv = kmalloc(sizeof(led_cdev_t),GFP_KERNEL);
if(!g_led_cdv)
{
/*申请失败*/
result = -ENOMEM;
goto fail_malloc;
}
/*初始化并注册cdev*/
cdev_init(&g_led_cdv->cdev,&led_dev_fops);/*初始化*/
result = cdev_add(&g_led_cdv->cdev,dev_no,1);/*注册*/
if(result)
{
printk(KERN_INFO"Error to add led cdev");
}
return 0;
fail_malloc:
unregister_chrdev_region(dev_no,1);
return result;
}
module_init(led_module_init);
2.4.2 模块的卸载
需要按照相反的顺序进行模块的卸载
cdev_add =====> cdev_del
kmalloc =====> kfree
alloc_chrdev_region =====> unregister_chrdev_region
static void __exit led_module_exit(void)
{
printk(KERN_INFO"%s\r\n",__FUNCTION__);
/*按照相反的顺序卸载*/
cdev_del(&g_led_cdv->cdev);
kfree(g_led_cdv);
unregister_chrdev_region(MKDEV(led_major,0),1);/*释放设备号*/
}
module_exit(led_module_exit);
2.5 声明
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wxb");
MODULE_DESCRIPTION("this is a led drivers!");
2.6完整代码如下:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/cdev.h>
#include <linux/uaccess.h> //copy_to_user copy_from_user
#include <linux/slab.h>
#define LED_MAJOR 0 /*采用动态的方式创建*/
#define DEVICE_NAME "my_led"
static int led_major = LED_MAJOR;
/*led设备结构体*/
typedef struct LED_CDEV
{
struct cdev cdev;
unsigned char led_value;/*led*/
}led_cdev_t;
static led_cdev_t *g_led_cdv;
//1.file operations 函数相关实现
/*1.1 打开设备*/
static int led_dev_open(struct inode *inode,struct file *filep)
{
printk(KERN_INFO"%s\r\n",__FUNCTION__);
filep->private_data = g_led_cdv;
return 0;
}
/*1.2 文件释放*/
static int led_dev_release(struct inode *inode,struct file *filep)
{
printk(KERN_INFO"%s\r\n",__FUNCTION__);
return 0;
}
/*1.3 读函数*/
static ssize_t led_status_read(struct file *filep,char __user *buf,size_t size,
loff_t *ppos)
{
printk(KERN_INFO"%s\r\n",__FUNCTION__);
int ret = 0;
led_cdev_t *dev = filep->private_data;/*获得设备结构体指针*/
/*内核空间到用户空间拷贝*/
if(copy_to_user(buf,&dev->led_value,1))
{
ret = -EFAULT;
}
else
{
printk(KERN_INFO"read:dev->led_value=%d\r\n",dev->led_value);
ret = 1;/*返回具体的个数*/
}
return ret;
}
/*1.4 写函数*/
static ssize_t led_status_write(struct file *filep,const char __user *buf,
size_t size,loff_t *ppos)
{
printk(KERN_INFO"%s\r\n",__FUNCTION__);
int ret = 0;
led_cdev_t *dev = filep->private_data;
if(copy_from_user(&dev->led_value,buf,1))
{
ret = -EFAULT;
}
else
{
printk(KERN_INFO"write:dev->led_value=%d\r\n",dev->led_value);
ret = 1;
}
return ret;
}
/*1.5 填充file operations 结构体*/
static const struct file_operations led_dev_fops =
{
.owner = THIS_MODULE,
.open = led_dev_open,
.release = led_dev_release,
.read = led_status_read,
.write = led_status_write
};
/*2 注册和卸载*/
/*2.1 设备驱动模块加载函数*/
static int __init led_module_init(void)
{
printk(KERN_INFO"%s\r\n",__FUNCTION__);
int result = 0;
dev_t dev_no = MKDEV(led_major,0);//MKDEV是将主设备号和次设备号转换成dev_t类型的一个内核函数。
/*1.申请设备号*/
if(led_major)
{
result = register_chrdev_region(dev_no,1,DEVICE_NAME);
}
else
{
/*动态申请设备号*/
result = alloc_chrdev_region(&dev_no,0,1,DEVICE_NAME);
led_major = MAJOR(dev_no);
}
if(result < 0)
{
return result;
}
/*2.动态申请设备结构体的内存*/
g_led_cdv = kmalloc(sizeof(led_cdev_t),GFP_KERNEL);
if(!g_led_cdv)
{
/*申请失败*/
result = -ENOMEM;
goto fail_malloc;
}
/*初始化并注册cdev*/
cdev_init(&g_led_cdv->cdev,&led_dev_fops);/*初始化*/
result = cdev_add(&g_led_cdv->cdev,dev_no,1);/*注册*/
if(result)
{
printk(KERN_INFO"Error to add led cdev");
}
return 0;
fail_malloc:
unregister_chrdev_region(dev_no,1);
return result;
}
/*2.2 模块卸载函数*/
static void __exit led_module_exit(void)
{
printk(KERN_INFO"%s\r\n",__FUNCTION__);
/*按照相反的顺序卸载*/
cdev_del(&g_led_cdv->cdev);
kfree(g_led_cdv);
unregister_chrdev_region(MKDEV(led_major,0),1);/*释放设备号*/
}
/*2.3 卸载和加载*/
module_init(led_module_init);
module_exit(led_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wxb");
MODULE_DESCRIPTION("this is a led drivers!");
3.Makefile编写
KERN_VER = $(shell uname -r)
#Linux
KERN_DIR = /lib/modules/$(KERN_VER)/build
#ARM-Linux 的源码路径
#KERN_DIR = /home/wxb/study/arm_linux/linux-2.6.35.3
obj-m += led.o
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
rm -r -f *.ko *.mod.c *.mod.o *.o *.order *.symvers
.PHONY:clean
4.测试验证
4.1 测试文件编写led_test.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <errno.h>
#include <fcntl.h>
#define DEV_NAME "/dev/my_led"
int main(int argc, char *argv[])
{
int i;
int fd = 0;
unsigned char ch = 1;
fd = open (DEV_NAME, O_RDWR);
if (fd < 0)
{
perror("Open "DEV_NAME" Failed!\n");
exit(1);
}
/*先写入一个字节数据*/
i = write(fd, &ch, 1);
if (!i)
{
perror("write "DEV_NAME" Failed!\n");
exit(1);
}
/*然后再读出来*/
i = read(fd, &ch, 1);
if (!i)
{
perror("read "DEV_NAME" Failed!\n");
exit(1);
}
else
{
printf("read data is %d\r\n",ch);
}
/*关闭文件*/
close(fd);
return 0;
}
编译得到可执行文件
gcc led_test.c -o led_test
使用make生成模块
make
然后查看设备号
cat /proc/devices
根据该设备的设备号创建一个设备节点
mknod /dev/my_led c 243 0 # c代表char,243与查看的主设备号要一致,0代表第1个从设备
创建完节点之后,我们可以在/dev/的目录下发现该设备
ls /dev/
最后执行命令
./led_test
可得到如下打印信息。