当前位置: 代码迷 >> 综合 >> 嵌入式内核及驱动开发-03字符设备驱动基础(申请设备号,创建设备节点,实现文件操作对象,应用控制驱动,copy_to_user,ioremapled,灯驱动)
  详细解决方案

嵌入式内核及驱动开发-03字符设备驱动基础(申请设备号,创建设备节点,实现文件操作对象,应用控制驱动,copy_to_user,ioremapled,灯驱动)

热度:32   发布时间:2024-01-20 09:26:44.0

文章目录

  • 字符设备、块设备、网络设备基础概念
    • 字符设备
    • 块设备
    • 网络设备
  • 字符设备驱动框架(用户空间和内核空间)
    • 设备号,设备节点
    • 字符设备驱动框架
    • 字符设备驱动
      • 向系统申请—主设备号
        • 示例-主设备号申请
        • 测试
      • 创建设备节点
        • 手动创建
        • 测试
        • 自动创建(通过udev/mdev机制)
          • 创建一个类
          • 创建一个设备文件
          • 销毁动作:
          • 例—代码
          • 测试
      • 在驱动中实现文件io的接口,应用程序可以调用文件io
        • 驱动中实现文件io操作接口:struct file_operations
          • 例—驱动代码
        • 应用程序如何去调用文件io去控制驱动–open,read,…
          • 例—应用程序代码
          • 例—Makefile
          • 实验效果—应用程序控制驱动
      • 应用程序需要传递数据给驱动
      • 控制外设,其实就是控制地址,内核驱动中是通过虚拟地址操作
        • 示例—led闪烁
          • 驱动代码 chr_drv.c
          • 应用程序 chr_test.c
  • 参考

字符设备、块设备、网络设备基础概念

字符设备

  • 举例

  • LCD屏,keyboard,I2C从设备

  • 字符设备的特点
    字符设备产生的数据是字符流
    大部分的数据都是从寄存器中产生,速度较快,量不是很大
    应用空间中读取数据按统一的接口(文件IO)来读取

块设备

  • 块设备举例
    存储设备(硬盘,SD卡,U盘,flash)

  • 块设备的特点
    数据以块为单位
    读取速度相对较慢,数据量很大
    块设备分层管理,比如:文件管理层,缓冲区层等

网络设备

  • 网络设备举例
    以太网,WiFi

  • 网络设备的特点
    网络设备走协议,所以linux中把网络设备全部做成socket套接字设备
    按照socket统一的接口来管理
    分层比较复杂

字符设备驱动框架(用户空间和内核空间)

设备号,设备节点

  • 设备号和驱动相关联
  • 设备号是一个ID,设备节点就是驱动文件
  • 字符设备和块设备是独立的,虽然设备号可能相同,但却是不同的设备
    在这里插入图片描述

字符设备驱动框架

作为字符设备驱动要素:1,必须有一个设备号,用在众多到设备驱动中进行区分2,用户必须知道设备驱动对应到设备节点(设备文件)linux把所有到设备都看成文件crw-r----- 1 root root 13, 64 Mar 28 20:14 event0crw-r----- 1 root root 13, 65 Mar 28 20:14 event1crw-r----- 1 root root 13, 66 Mar 28 20:14 event23,对设备操作其实就是对文件操作,应用空间操作open,read,write的时候实际在驱动代码有对应到open, read,write

字符设备驱动

  1. 注册获取设备号
  2. 初始化设备
  3. 操作设备 file_operations – open release read write ioctl…
  4. 两个宏定义 module_init module_exit 两个命令 insmod rmmod
  5. 注册设备号 register_chrdev_region
  6. cdev_init 初始化字符设备
  7. cdev_add 添加字符设备到系统

驱动是被动调用的,是被应用程序触发的

应用程序的 open  调用到驱动的file_operations 的 open
应用程序的 read  调用到驱动的file_operations 的 read
应用程序的 write 调用到驱动的file_operations 的 write
应用程序的 ioctl 调用到驱动的file_operations 的 ioctl
应用程序的 close 调用到驱动的file_operations 的 release

向系统申请—主设备号

作为驱动必须有一个主设备号–向系统申请

int register_chrdev(unsigned int major, const cha-r * name, const struct file_operations * fops)

参数说明:

  1. 参数1:主设备号
    设备号(32bit–dev_t)==主设备号(12bit) + 次设备号(20bit)
    主设备号:表示一类设备–camera
    次设备号: 表示一类设备中某一个:前置,后置
    给定到方式有两种:
    1,动态–参数1直接填0
    2,静态–指定一个整数,250

  2. 参数2: 描述一个设备信息,可以自定义
    /proc/devices列举出所有到已经注册的设备

  3. 参数3: 文件操作对象–提供open, read,write
    返回值: 正确返回0,错误返回负数

释放设备号
void unregister_chrdev(unsigned int major, const char * name)

参数说明:

参数1:主设备号
参数2: 描述一个设备信息,可以自定义

示例-主设备号申请
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>/*
设备号给定方式有两种:1,动态--参数1直接填02,静态--指定一个整数,250
*/
static unsigned int dev_major = 250;const struct file_operations my_fops = {
};static int __init chr_dev_init(void)
{//装载一般都是申请设备号资源//申请设备号int ret;ret = register_chrdev(dev_major, "chr_dev_test", &my_fops);if(ret == 0){printk("register ok\n");}else{printk("register failed\n");return -EINVAL;}return 0;
}static void __exit chr_dev_exit(void)
{//卸载一般都是释放资源unregister_chrdev(dev_major, "chr_dev_test");
}module_init(chr_dev_init);
module_exit(chr_dev_exit);
MODULE_LICENSE("GPL");
测试

模块装载前观察开发板所有设备号,并没有申请的设备号
在这里插入图片描述

装载chr_drv.ko ,观察开发板设备编号,发现申请设备号250出现了在这里插入图片描述

创建设备节点

手动创建
缺点/dev/目录中文件都是在内存中,断电后/dev/文件就会消失mknod /dev/设备名  类型  主设备号 次设备号(主设备号要和驱动中申请的主设备号保持一致)比如:mknod  /dev/chr0  c  250 0[root@farsight drv_module]# ls /dev/chr0 -lcrw-r--r--    1 0        0         250,   0 Jan  1 00:33 /dev/chr0
测试

在这里插入图片描述

自动创建(通过udev/mdev机制)
创建一个类
struct class *class_create(owner, name)
  1. 参数1: THIS_MODULE
  2. 参数2: 字符串名字,自定义
  3. 返回一个class指针
创建一个设备文件
struct device *device_create(struct class * class, struct device * parent, dev_t devt, void * drvdata, const char * fmt,...)
  1. 参数1: class结构体,class_create调用之后的返回值

  2. 参数2:表示父亲,一般直接填NULL

  3. 参数3: 设备号类型 dev_t
    dev_t devt

     #define MAJOR(dev)	((unsigned int) ((dev) >> MINORBITS)) 		//获取主设备号#define MINOR(dev)	((unsigned int) ((dev) & MINORMASK)) 		//获取次设备号#define MKDEV(ma,mi)	(((ma) << MINORBITS) | (mi))  			//生成设备号
    
  4. 参数4:私有数据,一般直接填NULL

  5. 参数5和6:表示可变参数,字符串,表示设备节点名字

销毁动作:
void device_destroy(devcls,  MKDEV(dev_major, 0));
void class_destroy(devcls);
  1. 参数1: class结构体,class_create调用之后到返回值

  2. 参数2: 设备号类型 dev_t

  3. 参数1: class结构体,class_create调用之后到返回值

例—代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>/*
设备号给定方式有两种:1,动态--参数1直接填02,静态--指定一个整数,250
*/
static unsigned int dev_major = 250;
static unsigned int dev_minor = 0;
static struct class *devcls;
static struct device *dev;const struct file_operations my_fops = {};static int __init chr_dev_init(void)
{//装载一般都是申请设备号资源//申请设备号int ret;ret = register_chrdev(dev_major, "chr_dev_test", &my_fops);if(ret == 0){printk("register ok\n");}else{printk("register failed\n");return -EINVAL;}//自动创建设备节点devcls = class_create(THIS_MODULE, "chr_cls");dev = device_create(devcls,NULL,MKDEV(dev_major,dev_minor),NULL,"chr2");return 0;
}static void __exit chr_dev_exit(void)
{//卸载一般都是释放资源//销毁节点和类device_destroy(devcls,MKDEV(dev_major,dev_minor));class_destroy(devcls);//释放设备号unregister_chrdev(dev_major, "chr_dev_test");
}module_init(chr_dev_init);
module_exit(chr_dev_exit);
MODULE_LICENSE("GPL");
测试

在这里插入图片描述

在驱动中实现文件io的接口,应用程序可以调用文件io

驱动中实现文件io操作接口:struct 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);ssize_t 		(*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);int 			(*iterate) (struct file *, struct dir_context *);unsigned int 	(*poll) (struct file *, struct poll_table_struct *);long 			(*unlocked_ioctl) (struct file *, unsigned int, unsigned long);long 			(*compat_ioctl) (struct file *, unsigned int, unsigned long);int 			(*mmap) (struct file *, struct vm_area_struct *);int 			(*open) (struct inode *, struct file *);int 			(*flush) (struct file *, fl_owner_t id);int 			(*release) (struct inode *, struct file *);int 			(*fsync) (struct file *, loff_t, loff_t, int datasync);int 			(*aio_fsync) (struct kiocb *, int datasync);int 			(*fasync) (int, struct file *, int);int 			(*lock) (struct file *, int, struct file_lock *);ssize_t 		(*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);unsigned long 	(*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);int 			(*check_flags)(int);int 			(*flock) (struct file *, int, struct file_lock *);ssize_t 		(*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);ssize_t 		(*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);int 			(*setlease)(struct file *, long, struct file_lock **);long 			(*fallocate)(struct file *file, int mode, loff_t offset, loff_t len);int 			(*show_fdinfo)(struct seq_file *m, struct file *f);}; //函数指针的集合,其实就是接口,我们写驱动到时候需要去实现const struct file_operations my_fops = {.open = chr_drv_open,.read = chr_drv_read,.write = chr_drv_write,.release = chr_drv_close,};
例—驱动代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>/*
设备号给定方式有两种:1,动态--参数1直接填02,静态--指定一个整数,250
*/
static unsigned int dev_major = 250;
static unsigned int dev_minor = 0;
static struct class *devcls;
static struct device *dev;ssize_t chr_drv_read(struct file *filep, char __user *buf, size_t count, loff_t *fpos)
{printk("---------%s-------------\n",__FUNCTION__);return 0;
}
ssize_t chr_drv_write(struct file *filep, const char __user *buf, size_t count, loff_t *fops)
{printk("---------%s-------------\n",__FUNCTION__);return 0;
}
int chr_drv_open(struct inode *inode, struct file *filep)
{printk("---------%s-------------\n",__FUNCTION__);return 0;
}
int chr_drv_close(struct inode *inode, struct file *filep)
{printk("---------%s-------------\n",__FUNCTION__);return 0;
}const struct file_operations my_fops = {.open = chr_drv_open,.read = chr_drv_read,.write = chr_drv_write,.release = chr_drv_close,
};static int __init chr_dev_init(void)
{//装载一般都是申请设备号资源//申请设备号int ret;ret = register_chrdev(dev_major, "chr_dev_test", &my_fops);if(ret == 0){printk("register ok\n");}else{printk("register failed\n");return -EINVAL;}//自动创建设备节点devcls = class_create(THIS_MODULE, "chr_cls");dev = device_create(devcls,NULL,MKDEV(dev_major,dev_minor),NULL,"chr2");return 0;
}static void __exit chr_dev_exit(void)
{//卸载一般都是释放资源//销毁节点和类device_destroy(devcls,MKDEV(dev_major,dev_minor));class_destroy(devcls);//释放设备号unregister_chrdev(dev_major, "chr_dev_test");
}module_init(chr_dev_init);
module_exit(chr_dev_exit);
MODULE_LICENSE("GPL");
应用程序如何去调用文件io去控制驱动–open,read,…
例—应用程序代码

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
//调用驱动
int fd;
int value = 0;

if((fd = open("/dev/chr2", O_RDWR)) < 0);
{perror("chr_test open");exit(1);
}read(fd,&value,4);
write(fd,&value,4);close(fd);return 0;

}

例—Makefile
ROOTFS_DIR = /nfs/rootfs#根文件系统路径APP_NAME = chr_test
CROSS_COMPILE = arm-none-linux-gnueabi-
CC = $(CROSS_COMPILE)gccifeq ($(KERNELRELEASE),)KERNEL_DIR = /home/linux/linux-3.14.79  		#编译过的内核源码的路径
CPU_DIR = $(shell pwd) 	#当前路径all:make -C $(KERNEL_DIR) M=$(CPU_DIR) modules  #把当前路径编成modules@#make -C 进入到内核路径@#M 指定当前路径$(CC) $(APP_NAME).c -o $(APP_NAME)clean:make -C $(KERNEL_DIR) M=$(CPU_DIR) cleanrm $(APP_NAME)install:sudo cp -raf *.ko $(APP_NAME) $(ROOTFS_DIR)/drv_module 	#把当前的所有.ko文件考到根文件系统的drv_module目录elseobj-m += chr_drv.o  #指定内核要将哪个文件编译成koendif
实验效果—应用程序控制驱动

在这里插入图片描述

应用程序需要传递数据给驱动

//将数据从内核空间拷贝到用户空间,一般是在驱动中chr_drv_read()用
int copy_to_user(void __user * to, const void * from, unsigned long n)

参数1 to:to:目标地址(用户空间)应用驱动中的一个buffer
参数2 from:源地址(内核空间)
参数3 n :将要拷贝数据的字节数
返回:成功返回0,失败返回没有拷贝成功的数据字节数

//将数据从用户空间拷贝到内核空间,一般是在驱动中chr_drv_write()用
int copy_from_user(void * to, const void __user * from, unsigned long n)

参数1 to :目标地址(内核空间)
参数2 from:源地址(用户空间)
参数3 n:将要拷贝数据的字节数
返回:成功返回0,失败返回没有拷贝成功的数据字节数

控制外设,其实就是控制地址,内核驱动中是通过虚拟地址操作

在这里插入图片描述

//映射虚拟地址
void *ioremap(cookie, size)

参数1: 物理地址
参数2: 长度(连续映射一定长度的地址空间)
返回值: 虚拟地址

//去映射--解除映射
void iounmap(void __iomem *addr)

参数1: 映射之后到虚拟地址

示例—led闪烁
驱动代码 chr_drv.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <asm/io.h>//物理地址
//#define GPX2_CON 	(*(volatile unsigned int *)0x11000C40)  //此处是裸机驱动开发时的用法,此处不能用
#define GPX2_CON 0x11000C40
//GPX2_DATA就是GPX2_CON + 4
#define GPX2_SIZE 8//存放虚拟地址的指针
volatile unsigned long *gpx2_config;
volatile unsigned long *gpx2_data;/*
设备号给定方式有两种:1,动态--参数1直接填02,静态--指定一个整数,250
*/
static unsigned int dev_major = 250;
static unsigned int dev_minor = 0;
static struct class *devcls;
static struct device *dev;static int kernel_val = 555;ssize_t chr_drv_read(struct file *filep, char __user *buf, size_t count, loff_t *fpos)
{int ret;printk("---------%s-------------\n",__FUNCTION__);//从内核空间拷贝数据到用户空间ret = copy_to_user(buf, &kernel_val, count);if(ret > 0){printk("copy_to_user error\n");return -EFAULT;			}return 0;
}
ssize_t chr_drv_write(struct file *filep, const char __user *buf, size_t count, loff_t *fops)
{int ret;int value;printk("---------%s-------------\n",__FUNCTION__);//从用户空间拷贝数据到内核空间ret = copy_from_user(&value, buf, count);if(ret > 0){printk("copy_from_user error\n");return -EFAULT;			}//控制GPX2_7 I/O口电平变化if(value){*gpx2_data |= (0x01 << 7);}else{*gpx2_data &= ~(0x01 << 7);}return 0;
}
int chr_drv_open(struct inode *inode, struct file *filep)
{printk("---------%s-------------\n",__FUNCTION__);return 0;
}
int chr_drv_close(struct inode *inode, struct file *filep)
{printk("---------%s-------------\n",__FUNCTION__);return 0;
}const struct file_operations my_fops = {.open = chr_drv_open,.read = chr_drv_read,.write = chr_drv_write,.release = chr_drv_close,
};static int __init chr_dev_init(void)
{int ret;printk("---------%s-------------\n",__FUNCTION__);//装载一般都是申请设备号资源//申请设备号ret = register_chrdev(dev_major, "chr_dev_test", &my_fops);if(ret == 0){printk("register ok\n");}else{printk("register failed\n");return -EINVAL;}//自动创建设备节点devcls = class_create(THIS_MODULE, "chr_cls");dev = device_create(devcls,NULL,MKDEV(dev_major,dev_minor),NULL,"chr2");//地址映射gpx2_config = ioremap(GPX2_CON, GPX2_SIZE);gpx2_data = gpx2_config + 1;//配置GPIO的功能为输出*gpx2_config = (*gpx2_config & ~(0x0f << 28)) | (0x01 << 28);return 0;
}static void __exit chr_dev_exit(void)
{printk("---------%s-------------\n",__FUNCTION__);//卸载一般都是释放资源//去映射iounmap(gpx2_config);//销毁节点和类device_destroy(devcls,MKDEV(dev_major,dev_minor));class_destroy(devcls);//释放设备号unregister_chrdev(dev_major, "chr_dev_test");
}module_init
(chr_dev_init);
module_exit(chr_dev_exit);
MODULE_LICENSE("GPL");
应用程序 chr_test.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>int main(int argc, char *argv[])
{//调用驱动int fd;int value = 0;fd = open("/dev/chr2", O_RDWR);if(fd < 0){perror("chr_test open");exit(1);}read(fd,&value,4);printf("__USER__ : value = %d\n",value);while(1){value = 1;write(fd, &value, 4);sleep(1);value = 0;write(fd, &value, 4);sleep(1);}close(fd);return 0;
}

参考

https://blog.csdn.net/m0_37542524/article/details/86533273#233ioio_296