字符设备
字符设备驱动
在内核移植一章中有关于konfig的介绍,我们添加的驱动文件可以根据需求选择添加到menuconfig菜单中。本章节主要记录基本字符设备驱动开发的步骤。
Makefile
#若在PC上测试简单模块,则使用 uname命令获取内核路径
#KERNELDIR=/lib/modules/$(shell uname -r)/build/
KERNELDIR=/home/guo/Downloads/kernel-3.4.39
PWD=$(shell pwd)
all:
#当make的目标为all时,-C $(KERNELDIR) 指明跳转到内核源码目录下读取那里的Makefile;M=$(PWD) 表明然后返回到当前目录继续读入、执行当前的Makefile。
make -C $(KERNELDIR) M=$(PWD) modules
@echo "build on pc"
# M是个变量
clean:
make -C $(KERNELDIR) M=$(PWD) clean
#编译成模块
obj-m:= hello.o
#内核make语法规则相较于标准Makefile规则有所扩展
#---------------------------------------
#多文件编译,例如hello.c add.c
# obj-m:=demo.o
# demo-y += hello.o add.o
驱动相关命令
insmod :加载驱动
modprobe:更加智能,加载驱动时候也会加载依赖的驱动
rmmod:卸载驱动
lsmod:显示已安装的驱动
dmesg:查看消息,可以查看printk的所有消息
内核打印printk
该函数格式为printk(loglevel "XXXXXXX")
//include/linux/printk.h
#define KERN_EMERG "<0>" /* system is unusable */
#define KERN_ALERT "<1>" /* action must be taken immediately */
#define KERN_CRIT "<2>" /* critical conditions */
#define KERN_ERR "<3>" /* error conditions */
#define KERN_WARNING "<4>" /* warning conditions */
#define KERN_NOTICE "<5>" /* normal but significant condition */
#define KERN_INFO "<6>" /* informational */
#define KERN_DEBUG "<7>" /* debug-level messages
// 0 - 7 (high - low)
通过命令cat /proc/sys/kernel/printk 查看打印级别设置
# 4 4 1 7
#终端级别 消息默认级别 最大级别 最小级别
#-----------------------------------
#修改默认级别
echo 4 3 1 7 > /proc/sys/kernel/printk
#修改开发板对应的打印级别
vi rootfs/etc/init.d/rcS
echo 4 3 1 7 > /proc/sys/kernel/printk
获取命令行参数
* Standard types are:
* byte, short, ushort, int, uint, long, ulong (没有找到char!!!!!!!!)
* charp: a character pointer
* bool: a bool, values 0/1, y/n, Y/N.
* invbool: the above, only sense-reversed (N = true).
// 功能:接收命令行传递的参数
// @name :变量的名字
// @type :变量的类型
// @perm :权限 0664 0775
module_param(name, type, perm)
//功能:对变量的功能进行描述
//@_parm:变量
//@desc :描述字段
MODULE_PARM_DESC(_parm, desc)
//hello:
int a = 10;
module_param(a,int,0664);
MODULE_PARM_DESC(a,"this is lcd light(0-255)");
///sys/module/hello/paramters 可以查看参数
//在加载模块时设置参数
// sudo insmod hello.ko a=121
驱动三要素
#include <linux/init.h>
#include <linux/module.h>
//三要素:入口,出口,许可证
//__init将hello_init放到.init.text段中
static int __init hello_init(void)
{
return 0;
}
//__exit将hello_exit放到.exit.text段中
static void __exit hello_exit(void)
{
}
module_init(hello_init);
//告诉内核驱动的入口地址
module_exit(hello_exit);
MODULE_LICENSE("GPL");
模块导出符号表
一个模块中的函数或者变量提供给其他模块使用,此时需要利用EXPORT_SYMBOL_GPL(symbol)导出符号。
- 编译
先编译提供者,完成后会生成Module.symvers文件,将其拷贝至调用者的同级目录
- 加载
先加载提供者,在加载调用者
- 卸载
先卸载调用者再卸载提供者
字符设备驱动框架
- 分配设备号
- 设置
file_operations结构体 - 注册字符设备
- 创建设备节点
设备号
设备是32位无符号数字
| 32 - 20 | 19 - 0 |
|---|---|
| 主设备号 | 次设备号 |
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)
#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))
设备号申请方式
- 手动申请
/*
手动指定设备号
@from :驱动工程师指定的设备号
@count:设备的个数
@name :名字 cat /proc/devices
返回值:成功返回0,失败返回错误码
*/
int register_chrdev_region(dev_t from, unsigned count, const char *name)
- 自动申请设备号
/*
功能:内核申请设备号
@dev :申请到的设备号
@baseminor :次设备号的起始值
@count :设备的个数
@name :名字 cat /proc/devices
返回值:成功返回0,失败返回错误码
*/
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,vconst char *name)
//设备号释放的函数
void unregister_chrdev_region(dev_t from, unsigned count)
file_operations结构体
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
...
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
...
}
用户通过open,read,write等接口调用,来调用驱动中相应的函数。
用户接口通过 系统调用,找到了VFS中相应sys_* 接口,具体过程以后补充
注册字符设备
- 该接口会默认创建255个设备 (待确认)
/*
@major:主设备号
:如果你填写的值大于0,它认为这个就是主设备号
:如果你填写的值为0,操作系统给你分配一个主设备号
@name :名字 cat /proc/devices
@fops :操作方法结构体
返回值:major>0 ,成功返回0,失败返回错误码(负数) vi -t EIO
major=0,成功主设备号,失败返回错误码(负数)
*/
int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
/*
功能:注销一个字符设备驱动
@major:主设备号
@name:名字
*/
void unregister_chrdev(unsigned int major, const char *name)
- 手动注册
//-----------------------------------------------------------------
// 手动注册
//-----------------------------------------------------------------
/*
功能:字符设备驱动的注册
@p :向注册的对象
@dev :设备号
@count:个数
返回值:成功返回0,失败返回错误码
*/
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
void cdev_del(struct cdev *p)
//若使用cdev_add,需要在此之前初始化结构体,如下
/*
功能:cdev结构体的初始化,将fops填充到cdev中
@cdev :对象的地址
@fops :操作方法结构体的地址
*/
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
创建设备节点
- 自动创建设备节点
#include <linux/device.h>
/*
自动创建设备节点:
功能:向用户空间提交目录信息
@owner :THIS_MODULE
@name :目录名字
返回值:成功返回struct class *指针
错误返回错误码,错误是指针,位于最高地址4K,使用宏IS_ERR(cls)转换
*/
struct class * class_create(owner, name)
//void class_destroy(struct class *cls)
/*
功能:向用户空间提交文件信息
@class :录名字
@parent:NULL
@devt :设备号
@drvdata :NULLc
@fmt :文件的名字
返回值:成功返回struct device *指针
失败返回错误码指针
*/
struct device *device_create(struct class *class, struct device *parent,
dev_t devt, void *drvdata, const char *fmt, ...)
//void device_destroy(struct class *class, dev_t devt)
- 手动创建设备节点
sudo mknod hello (路径是任意) c/b 主设备号 次设备号
驱动框架
这里需要 一张图
/*
open() read write close
----------------------------------------
vfs
-----------------------------------------
fops.open fops.read fops.write fops.close
*/
struct inode{
umode_t i_mode; //文件的权限
unsigned long i_ino;//inode号
dev_t i_rdev; //设备号
union { //设备驱动的类型
struct block_device *i_bdev;
struct cdev *i_cdev;
};
};
/*
* 每个文件都有inode号,内核是如何通过inode 号建立联系?
*/
struct cdev {
struct module *owner; //THIS_MODULE
const struct file_operations *ops; //操作方法结构体
dev_t dev; //设备号 32 12|20 2^32=4G
unsigned int count; //设备的个数
};
struct file{
const struct file_operations *f_op;
...
}
问题:如何通过fd来找到对应的驱动
调用open函数的时候产生fd,在进程中保存
struct task_struct{ //进程结构体
struct files_struct *files;
/../ ----> struct file* fd_array[NR_OPEN_DEFAULT];
}
当成功打开文件时候,会在内核中创建struct file结构体,并且把这个结构体放到fd_array的数组中,这个数组的下标就是文件描述符
fd--->fd_array[fd]--->struct file--->fops--->(read write ioctl release)
file结构体中有fops操作方法结构体,这个fops是从inode结构体中拷贝得到的,inode中的fops是从cdev中得到的。
字符设备驱动实例
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <asm/io.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#define DRV_BUF_SIZE 128
#define CLASS_NAME "class_cdev" // class create name
#define DEV_NAME "cdev_demo" // drives create name
char buf[DRV_BUF_SIZE] = {0};
struct cdev *cdev;
struct class *cdev_cls;
struct device *c_device;
dev_t devno; // 设备号
int major = 0; // 主设备号
static int drv_open(struct inode *node, struct file *filp)
{
printk("open cdrv-demo\n");
return 0;
}
static ssize_t drv_write(struct file *file, const char __user *u_buf,size_t size, loff_t *offset)
{
int ret = 0;
if (size > DRV_BUF_SIZE) {
return -EINVAL;
}
ret = copy_from_user(buf, u_buf, size);
if (ret) {
printk("copy from user failed :%d\n", ret);
return -EAGAIN;
}
printk("cdrv_demo write:%s\n",buf);
return size;
}
static ssize_t drv_read(struct file *file,char __user *u_buf, size_t size, loff_t *offset)
{
int ret = 0;
if (size > DRV_BUF_SIZE) {
return -EINVAL;
}
ret = copy_to_user(u_buf, buf, size);
if (ret) {
printk("copy to user failed :%d\n", ret);
return -EAGAIN;
}
printk("cdrv_demo read:%s\n",buf);
return size;
}
static int drv_release(struct inode* node, struct file *filp)
{
printk("cdrv_demo release\n");
return 0;
}
static struct file_operations file_ops = {
.open = drv_open,
.write = drv_write,
.read = drv_read,
.release = drv_release,
};
#define TOTAL_OF_DEV 3
static int __init drv_demo_init(void)
{
int ret = 0;
int i = 0;
//74 - 95行可以直接用 major = register_chrdev(0, DEV_NAME,&file_ops)代替
//1. 分配对象
cdev = cdev_alloc();
if (cdev == NULL) {
ret = -ENOMEM;
goto err1;
}
//2. 初始化
cdev_init(cdev,&file_ops);
//3.设备号请
if (major > 0) {
ret = register_chrdev_region(MKDEV(major, 0),1, DEV_NAME);
} else {
ret = alloc_chrdev_region(&devno, 0, TOTAL_OF_DEV, CLASS_NAME);
}
if(ret < 0) {
goto err2;
}
//4. 注册
ret = cdev_add(cdev, devno, TOTAL_OF_DEV);
if (ret) {
printk("char device register failed\n");
goto err3;
}
cdev_cls = class_create(THIS_MODULE, CLASS_NAME);
if(IS_ERR(cdev_cls)) {
ret = PTR_ERR(cdev_cls);
goto err4;
}
major = MAJOR(devno);
for (i = 0; i < TOTAL_OF_DEV; i++) {
c_device = device_create(cdev_cls,NULL, MKDEV(major, i),NULL, "cdev%d",i); /* /proc/devices*/
if(IS_ERR(c_device)) {
ret = PTR_ERR(c_device);
goto err5;
}
}
printk("init done\n");
return 0; //一定要有
err5:
for (--i; i >= 0; i--) {
device_destroy(cdev_cls,MKDEV(major,i));
}
class_destroy(cdev_cls);
err4:
cdev_del(cdev);
err3:
unregister_chrdev_region(MKDEV(major,0),TOTAL_OF_DEV);
err2:
kfree(cdev);
err1:
return ret;
}
static void __exit drv_demo_exit(void)
{
int i = 0;
for (i = 0; i < TOTAL_OF_DEV; i++) {
device_destroy(cdev_cls, MKDEV(major,i));
}
class_destroy(cdev_cls);
cdev_del(cdev);
unregister_chrdev_region(devno, TOTAL_OF_DEV);
kfree(cdev);
free_irq(73, NULL);
}
module_init(drv_demo_init)
module_exit(drv_demo_exit)
MODULE_LICENSE("GPL");
高级驱动
#include <sys/ioctl.h>
/*
@fd : 打开文件产生的文件描述符
@request: 请求码(读写|第三个参数传递的字节的个数),
:在sys/ioctl.h中有这个请求码的定义方式。
@... :可写、可不写,如果要写,写一个内存的地址
*/
int ioctl(int fd, int request, ...);
//驱动调用
struct file_operations fops {
long (*unlocked_ioctl) (struct file *file, unsigned int request, unsigned long args);
...
}
#define _IO(type,nr)
_IOC(_IOC_NONE,(type),(nr),0)
#define _IOR(type,nr,size)
_IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOW(type,nr,size)
_IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
#define _IOWR(type,nr,size)
_IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
//这些宏是帮助你完成请求码的封装的。
#define _IOC(dir,type,nr,size) \
(((dir) << _IOC_DIRSHIFT) | \
((type) << _IOC_TYPESHIFT) | \
((nr) << _IOC_NRSHIFT) | \
((size) << _IOC_SIZESHIFT))
dir << 30 | size<<16 | type << 8 | nr << 0
2 14 8 8
方向 大小 类型 序号
内核中已经使用的命令码的域在如下文档中已经声明了。vi kernel-3.4.39/Documentation/ioctl$ vi ioctl-number.txt