《【正点原子】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 结构体函数成员太多,这里介绍几个常用的:
- llseek() 函数用于修改文件指针偏移量
- read() 函数用于读取设备文件
- write() 函数用于向设备文件写入数据
- poll() 函数用于轮询监听设备状态
- unlocked_ioctl() 函数提供对于设备的控制功能,与应用程序中的 ioctl 函数对应
- compat_ioctl() 函数与 unlocked_ioctl() 函数功能一样,区别在于在 64 位系统上,32 位的应用程序调用将会使用此函数。在 32 位的系统上运行 32 位的应用程序调用的是unlocked_ioctl()
- mmap() 函数用于将设备的内存映射到进程空间中(也就是用户空间)
- open() 函数用于打开设备文件
- release() 函数用于释放(关闭)设备文件,与应用程序中的 close 函数对应
- fasync() 函数用于刷新待处理的数据,用于将缓冲区中的数据刷新到磁盘中
- aio_fsync() 函数与 fasync 函数的功能类似,只是 aio_fsync 是异步刷新待处理的数据
字符设备驱动开发步骤
驱动模块的加载和卸载
Linux 驱动的运行方式有两种,编译进内核和编译成模块,调试时常用的是第二种,这样修改驱动时不需要编译内核源码,但是编译成驱动模块后,还需要用 insmod 命令将驱动模块加载进系统。
在我们使用加载和卸载命令时,驱动会调用下面两个函数:
module_init(xxx_init); // 模块加载函数,使用 insmod 命令时该函数被调用
module_exit(xxx_exit); // 模块卸载函数,使用 rmmod 命令时该函数被调用
模块加载和卸载模板如下:
驱动文件的扩展名为 .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: 设备名
示例代码(模板):
上面一个模板,test_fops 只是被定义,它的成员(如 open、release)还没有被初始化(定义)。
在 linux 命令行,使用 cat /proc/devices
可以查看当前系统已经占用的设备号。下图是我的虚拟机上 Ubuntu 已被使用的部分设备号:
设备具体操作函数
上面的设备驱动注册函数中有一个参数 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 文件,
接着编译应用程序,我测试的平台是 x86,所以直接用系统的 gcc 命令即可,
使用 insmod 安装驱动:
sudo insmod test_drv.ko
使用 dmesg 查看内核打印信息(不如开发板方便)
驱动虽然安装成功了,但是没有在 /dev 下生成设备文件(因为注册字符设备后并不会自己生成设备节点,需要在驱动里添加相关代码,或者手动创建,原教程使用了手动创建的方式)
使用 cat /proc/devices
查看驱动申请到的节点号,
然后使用 mknod
命令手动生成设备节点,
最后就是运行 app (应用程序)来测试了,下图是应用层的打印信息:
这是内核驱动层打印信息:文章来源:https://www.toymoban.com/news/detail-477665.html
文章来源地址https://www.toymoban.com/news/detail-477665.html
到了这里,关于Linux 驱动学习笔记 ——(1)字符设备驱动的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!