Linux 驱动学习笔记 ——(1)字符设备驱动

这篇具有很好参考价值的文章主要介绍了Linux 驱动学习笔记 ——(1)字符设备驱动。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

《【正点原子】I.MX6U嵌入式Linux驱动开发指南》学习笔记

字符设备驱动简介

字符设备是 Linux 驱动中最基本的一类设备驱动,字节设备就是按照字节流来读写的设备,常见的字符设备包括:LED、蜂鸣器、按键、I2C 以及 SPI 等。

Linux 中一切皆文件,字符设备驱动加载成功后会在 /dev 目录下生成相应的设备文件,应用程序可以通过 open() 函数来打开这个设备文件,然后可以通过 write() 和 read() 对这个设备进行读写操作。

上面提到的 open()、write() 等函数在驱动中都对应一个函数,驱动中这类函数有很多,它们都在结构体 file_operations 当中(kernel/include/linux/fs.h),该结构体就是 Linux 内核驱动文件操作函数的集合,结构体定义如下:

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 (*readdir) (struct file *, void *, filldir_t);
	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);
	/* get_lower_file is for stackable file system */
	struct file* (*get_lower_file)(struct file *f);
};

file_operations 结构体函数成员太多,这里介绍几个常用的:

  1. llseek() 函数用于修改文件指针偏移量
  2. read() 函数用于读取设备文件
  3. write() 函数用于向设备文件写入数据
  4. poll() 函数用于轮询监听设备状态
  5. unlocked_ioctl() 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应
  6. compat_ioctl() 函数与 unlocked_ioctl() 函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl()
  7. mmap() 函数用于将设备的内存映射到进程空间中(也就是用户空间)
  8. open() 函数用于打开设备文件
  9. release() 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应
  10. fasync() 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中
  11. aio_fsync() 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据

字符设备驱动开发步骤

驱动模块的加载和卸载

Linux 驱动的运行方式有两种,编译进内核和编译成模块,调试时常用的是第二种,这样修改驱动时不需要编译内核源码,但是编译成驱动模块后,还需要用 insmod 命令将驱动模块加载进系统。

在我们使用加载和卸载命令时,驱动会调用下面两个函数:

module_init(xxx_init);  // 模块加载函数,使用 insmod 命令时该函数被调用
module_exit(xxx_exit);  // 模块卸载函数,使用 rmmod 命令时该函数被调用

模块加载和卸载模板如下:

Linux 驱动学习笔记 ——(1)字符设备驱动

驱动文件的扩展名为 .ko,我们可以用两种命令来安装驱动,insmod 和 modeprobe,modeprobe 比 insmode 更加智能,modeprobe 能提供模块的依赖性分析、错误检查、错误报告,它默认会去 /lib/modules/<kernel-version> 目录中查找(内核中编译时指定了编译成动态模块的驱动文件会放在这个目录)。但是我平时很少使用 modeprobe,虽然正点原子文档上推荐我们使用这个命令。

使用 insmod 安装驱动:

insmod test.ko

使用 modprobe 安装驱动

modprobe test.ko
或者 
modpobe test

使用 rmmod 卸载驱动

rmmod test.ko

使用 modprobe -r 卸载驱动

modprobe -r test.ko
或者 
modpobe -r test

字符设备注册与注销

注册函数

函数原型:

#include <linux/fs.h>
static inline int register_chrdev(unsigned int major, const char *name,
const struct file_operations *fops)

功能:注册一个字符设备

参数:

  • major: 主设备号(填 0 时,会自动分配)
  • name: 设备名
  • fops: 文件操作方法结构体指针

返回值:
成功返回分配的主设备号,失败返回负数。

注销函数

函数原型:

#include <linux/fs.h>
static inline void unregister_chrdev(unsigned int major, const char *name)

功能:注销一个已经存在字符设备

参数:

  • major: 主设备号
  • name: 设备名

示例代码(模板):

Linux 驱动学习笔记 ——(1)字符设备驱动

上面一个模板,test_fops 只是被定义,它的成员(如 open、release)还没有被初始化(定义)。

在 linux 命令行,使用 cat /proc/devices 可以查看当前系统已经占用的设备号。下图是我的虚拟机上 Ubuntu 已被使用的部分设备号:

Linux 驱动学习笔记 ——(1)字符设备驱动

设备具体操作函数

上面的设备驱动注册函数中有一个参数 fops,它是 file_operations 结构体变量,这个结构体前文已经介绍了,有很多成员函数,我们需要实现需求定义部分函数,这里我们就拿最常见的 open、clease、read 和 write 写一个简单的示例:(模板,不包含头文件)


/* 打开设备 */
static int test_open(struct inode *inode, struct file * filp)
{
	printk("Chrdev was opened.\n");
	return 0;
}

/* 读设备 */
static ssize_t test_read(struct file *filp, char __user *buf,
							size_t cnt, loff_t *offt)
{
	return 0;
}

/* 写设备 */
static ssize_t test_write(struct file *filp, const char __user *buf,
							size_t cnt, loff_t *offt)
{
	return 0;
}

/* 释放设备 */
static int test_release(struct inode *inode, struct file *filp)
{
	printk("Chrdev was closed.\n");	
	return 0;
}

// 定义文件操作结构体变量
static struct file_operations test_fops = {
	.owner = THIS_MODULE,
	.open = test_open,
	.read = test_read,
	.write = test_write,
	.release = test_release,
};

/* 驱动入口函数 */
static int __init test_init(void)
{
	int ret = 0;

	/* 注册字符设备驱动 */
	ret = register_chrdev(100, "chrdev_test", &test_fops);
	if(ret < 0)
	{
		printk("Chrdev register failed.\n");
	}
	printk("Driver installed\n");
	return 0;
}

/* 驱动出口函数 */
static void __exit test_exit(void)
{
	/* 注销字符设备驱动 */
	unregister_chrdev(100, "chrdev_test");
	printk("Driver uninstalled\n");
}

/* 指定入口函数和出口函数 */
module_init(test_init);
module_exit(test_exit);

上面编写了四个操作函数 test_open()、test_read()、test_write() 及 test_release(),当应用层使用 open()、read()、write() 或 close() 时,就会调用驱动中对应的函数。

添加 LICENSE 和作者信息

驱动中还需要加入 LICENSE 信息和作者信息,前者是必须加的,不然编译不能通过。添加方式如下:

MODULE_LICENSE() //添加模块 LICENSE 信息
MODULE_AUTHOR()  //添加模块作者信息

这两两行代码放驱动的最后两行即可。

到这里,一个字符设备驱动的所有组成部分就全部介绍完了。

Linux 设备号

设备号的组成

Linux 里每个设备都有一个设备号,设备号由主设备号和次设备号组成,主设备号代表一个驱动,次设备号对应单个驱动中的各个设备。

设备号是一个 32 位(unsigned int)数据,主设备为高 12位,次设备为低 20 位,所以主设备号的范围为 0 ~ 4095(0~212)。

设备号的分配

静态分配

上文介绍字符设备注册函数时用到的就是静态分配。可以先使用 cat /proc/devices 查看哪些设备号已经被占用。

动态分配

静态分配很容易带来设备号冲突问题,所以推荐使用动态分配设备号,设备号申请函数如下:

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

参数介绍:

  • dev: 申请到的设备号
  • baseminor: 次设备号起始地址,一般填 0,
  • count: 要申请的设备号数量
  • name: 设备名

有申请,就会有对应的注销,下面是 alloc_chrdev_region() 对应的注销函数:

void unregister_chrdev_region(dev_t from, unsigned count)

参数介绍:

  • from: 要释放的设备号
  • count: 要释放的设备数量

前面介绍字符设备注册函数时,设备号填 0,也能实现动态分配!

chrdevbase 字符设备驱动实验

字符设备驱动介绍完了,写一个简单的驱动来作为总结,驱动主要实现的功能:应用层和驱动层互相传输数据。

驱动层代码(驱动代码)

#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/kernel.h>
//#include <linux/delay.h>
#include <linux/ide.h>
//#include <linux/types.h>

static char rxbuff[255];
static char txbuff[255] = {"\"user read test.\""};

/* 打开设备 */
static int test_open(struct inode *inode, struct file * filp)
{
	printk("Chrdev was opened.\n");
	return 0;
}

/* 读设备 */
static ssize_t test_read(struct file *filp, char __user *buf,
							size_t cnt, loff_t *offt)
{
	int ret = 0;
	// 将数据发送到用户空间
	ret = copy_to_user(buf, txbuff, cnt); 
	if(ret != 0)
	{
		printk("Send failed.\n");
	}
	return 0;
}

/* 写设备 */
static ssize_t test_write(struct file *filp, const char __user *buf,
							size_t cnt, loff_t *offt)
{
	int ret = 0;
	// 从用户空间获取数据
	ret = copy_from_user(rxbuff, buf, cnt); 
	if(ret != 0)
	{
		printk("Receive failed.\n");
	}
	else
	{
		printk("The data received from user is %s\n", rxbuff);
	}
	return 0;
}

/* 释放设备 */
static int test_release(struct inode *inode, struct file *filp)
{
	printk("Chrdev was closed.\n");	
	return 0;
}

// 定义文件操作结构体变量
static struct file_operations test_fops = {
	.owner = THIS_MODULE,
	.open = test_open,
	.read = test_read,
	.write = test_write,
	.release = test_release,
};

/* 驱动入口函数 */
static int __init test_init(void)
{
	int ret = 0;

	/* 注册字符设备驱动 */
	ret = register_chrdev(0, "chrdev_test", &test_fops);
	if(ret < 0)
	{
		printk("Chrdev register failed.\n");
	}
	printk("Driver installed\n");
	return 0;
}


/* 驱动出口函数 */
static void __exit test_exit(void)
{
	/* 注销字符设备驱动 */
	unregister_chrdev(0, "chrdev_test");
	printk("Driver uninstalled\n");
}

/* 指定入口函数和出口函数 */
module_init(test_init);
module_exit(test_exit);

/* LICENSE 和 AUTHOR */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xiaohui");

应用层代码(应用程序)

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>


#define DEV_PATH "/dev/chrdev_test"

static char txbuff[] = "\"user write test\"";

int main()
{
	int fd;
	int ret = 0;
	char rxbuff[255];

	// 打开驱动文件
	fd = open(DEV_PATH, O_RDWR);
	if(fd < 0)
	{
		printf("Cannot open device file.\n");
		exit(-1);
	}

	// 从设备读数据
	ret = read(fd, rxbuff, sizeof(rxbuff));
	if(ret < 0)
	{
		printf("Read data failed.\n");
	}
	else
	{
		printf("The data received from kernel is %s\n", rxbuff);
	}

	// 向设备写数据
	ret = write(fd, txbuff, sizeof(txbuff));
	if(ret < 0)
	{
		printf("Write data failed.\n");
	}
	else
	{
		printf("Write data success.\n");	
	}
	
	// 关闭设备
	close(fd);

	return 0;
}

Makfile 文件

我是在 X86 平台测试的,所以用了以下的 Makefile,如果想编译成 arm 平台的驱动,只需指定 arm 交叉编译器和内核路径即可(如下面注释掉的那部分内容)。

obj-m := test_drv.o

#export ARCH=arm
#export CROSS_COMPILE=arm-linux-gnueabihf-

KDIR := /lib/modules/$(shell uname -r)/build
#KDIR := /home/alientek/alpha/alientek-alpha/kernel-alientek

all:
	make -C $(KDIR) M=$(shell pwd) modules

clean:
	rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.order

测试结果

首先编译驱动程序和应用程序。

驱动程序的编译是在 Makefile 目录下使用 make 命令,编译成功后会生成 .ko 文件,

Linux 驱动学习笔记 ——(1)字符设备驱动

接着编译应用程序,我测试的平台是 x86,所以直接用系统的 gcc 命令即可,

Linux 驱动学习笔记 ——(1)字符设备驱动

使用 insmod 安装驱动:

sudo insmod test_drv.ko 

使用 dmesg 查看内核打印信息(不如开发板方便)

Linux 驱动学习笔记 ——(1)字符设备驱动
驱动虽然安装成功了,但是没有在 /dev 下生成设备文件(因为注册字符设备后并不会自己生成设备节点,需要在驱动里添加相关代码,或者手动创建,原教程使用了手动创建的方式)

使用 cat /proc/devices 查看驱动申请到的节点号,

Linux 驱动学习笔记 ——(1)字符设备驱动

然后使用 mknod 命令手动生成设备节点,

Linux 驱动学习笔记 ——(1)字符设备驱动

最后就是运行 app (应用程序)来测试了,下图是应用层的打印信息:

Linux 驱动学习笔记 ——(1)字符设备驱动

这是内核驱动层打印信息:

Linux 驱动学习笔记 ——(1)字符设备驱动文章来源地址https://www.toymoban.com/news/detail-477665.html

到了这里,关于Linux 驱动学习笔记 ——(1)字符设备驱动的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Linux设备驱动——第三章字符驱动

    当对幸福的憧憬过于急切,那痛苦就在人的心灵深处升起。——加缪 本章的目的是编写一个完整的字符设备驱动。我们开发一个字符驱动是因为这一类适合大部分简单的硬件设备。字符驱动也比块驱动易于理解。本章的最终目的是编写一个模块化的字符驱动,但是我们不会在

    2024年02月08日
    浏览(80)
  • 【嵌入式Linux学习笔记】platform设备驱动和input子系统

    对于Linux这种庞大的操作系统,代码重用性非常重要,所以需要有相关的机制来提升效率,去除重复无意义的代码,尤其是对于驱动程序,所以就有了platform和INPUT子系统这两种工作机制。 学习视频地址:【正点原子】STM32MP157开发板 platform 驱动框架分为总线、设备和驱动。总

    2024年02月07日
    浏览(57)
  • Linux 驱动之字符设备

    什么是设备号 Linux 规定每一个字符设备或者块设备都必须有一个专属的设备号。一个设备号由主设备号和次设备号组成。主设备号用来表示某一类驱动,如鼠标,键盘都可以归类到 USB 驱动中。而次设备号是用来表示这个驱动下的各个设备。比如第几个鼠标,第几个键盘等。

    2024年02月16日
    浏览(38)
  • Linux 驱动之高级字符设备

    什么是IO呢? IO 的英文全称是 input 和output,翻译过来就是输入和输出。 在冯.诺依曼结构中,将计算机分成分为5个部分: 运算器、控制器、存储器、输入设备、输出设备 。其中输入设备指的是向计算机输入数据或者信息,如鼠标,键盘都是输入设备。输出设备指的是用于接收

    2023年04月14日
    浏览(41)
  • Linux -- 字符设备驱动--LED的驱动开发(初级框架)

    看原理图确定引脚,确定引脚输出什么电平才能点亮 / 熄灭 LED 看主芯片手册,确定寄存器操作方法:哪些寄存器?哪些位?地址是? 编写驱动:先写框架,再写硬件操作的代码 注意 :在芯片手册中确定的寄存器地址被称为 物理地址 ,在 Linux 内核中无法直接使用。 需要使

    2024年04月28日
    浏览(31)
  • Linux下字符设备驱动开发以及流程介绍

    首先我们介绍一下什么是字符设备,然后讲解一下字符设备开发的具体的流程,分别详细介绍每一个流程中涉及到的结构体以及知识点,最后我们编写代码实现字符设备的开发以及测试。 Linux内核设计哲学是把所有的东西都抽象成文件进行访问,这样对设备的访问都是通过文

    2024年02月01日
    浏览(40)
  • Linux字符设备驱动(设备文件,用户空间与内核空间进行数据交互,ioctl接口)

    在Linux系统中“一切皆文件”,上一篇讲述了cdev结构体就描述了一个字符设备驱动,主要包括设备号和操作函数集合。但是要怎么操作这个驱动呢?例如,使用open()该打开谁,read()该从哪读取数据等等。所以就需要创建一个设备文件来代表设备驱动。 应用程序要操纵外部硬件

    2024年02月12日
    浏览(38)
  • Linux驱动开发笔记(四):设备驱动介绍、熟悉杂项设备驱动和ubuntu开发杂项设备Demo

    若该文为原创文章,转载请注明原文出处 本文章博客地址:https://hpzwl.blog.csdn.net/article/details/134533533 红胖子网络科技博文大全:开发技术集合(包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软硬结合等等)持续更新中… 上一篇:《Linux驱动开发笔记(三

    2024年02月05日
    浏览(50)
  • 【linux驱动开发】在linux内核中注册一个杂项设备与字符设备以及内核传参的详细教程

    开发环境: windows + ubuntu18.04 + 迅为rk3568开发板 相较于字符设备,杂项设备有以下两个优点: 节省主设备号:杂项设备的主设备号固定为 10,在系统中注册多个 misc 设备驱动时,只需使用子设备号进行区分即可。 使用简单:相比如普通的字符设备驱动, misc驱动只需要将基本信

    2024年01月21日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包