通常不直接操作寄存器,而是使用各种驱动框架进行开发。
驱动表示为/dev/下的文件
驱动获取设备传感器数据
查看函数缺少哪个头文件,在终端使用man命令查看,安装sudo apt-get install manpages-dev,例如
- man 2 read 得到#include <unistd.h>
- man atoi 得到 #include <stdlib.h>
- man memcpy 得到#include <string.h>
模块加载和卸载函数
- module_init(xxx_init);
- module_exit(xxx_exit);
模块加载
- insmod xxx.ko
- modprobe xxx.ko (推荐)第一次加载驱动时需要运行命令:depmod
模块卸载
- rmmod xxx.ko (推荐)
- modprobe -r xxx.ko
旧版本注册和注销字符设备
旧版本注册字符设备和注销字符设备,需要我们事先确定好哪些主设备号没有使用;会将一个主设备号下的所有次设备号都使用掉。
- static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
- static inline void unregister_chrdev(unsigned int major, const char *name)
查看使用的设备号
- cat /proc/devices
设备号
- 32位unsigned int类型,主设备号高12位,次设备号低20位,主设备号范围0-4095
内核空间打开设备
- static int xxx_open(struct inode *inode, struct file *filp){}
- inode:传递给驱动的inode,offt:相对于文件首地址的偏移
内核空间从设备读取
- static ssize_t xxx_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt){}
- filp:文件描述符,buf:读取用户空间的数据,cnt:读取数据的长度,offt:相对于文件首地址的偏移
- 内核中read,copy_to_user(目的to, 源from, 长度count)内核把数据发送给应用
- copy_to_user(用户buf, 内核buf, 数据长度count) ,返回0表示成功
内核空间向设备写数据
- static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt){}
- filp:文件描述符,buf:写入设备的数据,cnt:写入数据的长度,offt:相对于文件首地址的偏移
- 内核中write,copy_from_user(目的to, 源from, 长度count)内核接受应用的数据
- copy_from_user(内核buf, 用户buf, 数据长度count),返回0表示成功
内核空间关闭设备
- static int xxx_release(struct inode *inode, struct file *filp){}
- inode:传递给驱动的inode,offt:相对于文件首地址的偏移
内核输出信息
- printk
- 8个级别,0优先级最高,7优先级最低,默认为4
用户空间打开
- int open(const char *pathname, int flags);
- pathname:设备名,flags:打开模式(O_RDONLY只读模式,O_WRONLY只写模式,O_RDWR读写模式,等)
用户空间读取
- ssize_t read(int fd, void *buf, size_t count);
- fd:文件描述符,buf:数据缓存区,count:读取数据的长度
用户空间写入
- ssize_t write(int fd, const void *buf, size_t count);
- fd:文件描述符,buf:数据缓存区,count:写入数据的长度
用户空间关闭
- int close(int fd);
- fd:文件描述符
手动创建设备节点
- mknod /dev/xxx c major minor
- c:表示字符设备,major:主设备号,minor:次设备号
- 驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,应用程序就是通过操作这个设备节点文件来完成对具体设备的操作
内存管理单元MMU(Memory Manage Unit)
MMU功能
- 完成虚拟空间到物理空间的映射
- 内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。
地址映射
Linux驱动开发不可以直接对寄存器的物理地址进行读写操作,需要得到物理地址在Linux系统中对应的虚拟地址。Linux内核启动时会初始化MMU,设置好内存映射后CPU访问的都是虚拟地址。使用函数 ioremap 和 iounmap 。
ioremap
- 获取指定物理地址空间对应的虚拟地址空间
- ioremap(phys_addr, size)
- phys_addr:映射的物理地址的起始地址,size:映射的内存字节大小,返回虚拟地址
iounmap
- 释放ioremap所做的映射
- iounmap(virt_addr)
- virt_addr:虚拟地址的起始地址
内存访问函数
Linux使用操作函数来对映射后的内存进行读写操作
- 读操作函数readb(addr)、readw(addr)、readl(addr)分别对应8位、16位、32位读操作,addr:读取的内存地址,返回值为读取到的数据。
- 写操作函数writeb(value, addr)、writew(value, addr)、writel(value, addr)分别对应8位、16位、32位写操作,value:写入值,addr:写入的内存地址
新版本注册和注销字符设备
新版本注册和注销设备号
新版本注册字符设备和注销字符设备,需要几个就申请几个,由 Linux 内核分配设备可以使用的设备号
如果没有指定设备号的话就使用如下函数来申请设备号
- int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
- dev:要申请的设备号(指针类型),baseminor:次设备号(通常为0),count:要申请的数量,一 般都是一个,name:设备名,返回值大于0表示成功
- 设备号申请成功后使用 MAJOR 和 MINOR 来提取出主设备号和次设备号
如果给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可
- int register_chrdev_region(dev_t from, unsigned count, const char *name)
- from:要申请的起始设备号,也就是给定的设备号,count:要申请的数量,一 般都是一个,name:设备名,返回值大于0表示成功
int major; // 主设备号
int minor; // 次设备号
dev_t devid; // 设备号
if (major) { //定义了主设备号
devid = MKDEV(major, 0); // 大部分驱动次设备号都选择 0
register_chrdev_region(devid, 1, "test");
} else { // 没有定义设备号
alloc_chrdev_region(&devid, 0, 1, "test"); // 申请设备号
major = MAJOR(devid); // 获取分配号的主设备号
minor = MINOR(devid); // 获取分配号的次设备号
}
注销字符设备
-
void unregister_chrdev_region(dev_t from, unsigned count)
-
from:设备号,count:数量
新版本注册和注销字符设备
Linux使用 cdev 结构体表示一个字符设备,cdev中有两个重要的成员变量:ops和dev分别对应字符设备操作函数集合file_operations和设备号dev_t。定义一个cdev结构体变量表示一个字符设备。其
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
使用cdev_init函数对cdev结构体变量初始化。
- void cdev_init(struct cdev *cdev, const struct file_operations *fops)
- cdev:要初始化的cdev结构体变量,fops:字符设备操作函数集合。无返回值
使用cdev_add函数向Linux系统内核添加字符设备。
- int cdev_add(struct cdev *p, dev_t dev, unsigned count)
- p:cdev结构体变量(指针),dev:设备号,count:添加设备数量。返回值大于0添加成功
使用cdev_del函数删除字符设备。
- void cdev_del(struct cdev *p)
- p:要删除的字符设备。无返回值
struct cdev test_cdev; // cdev结构体变量表示字符设备
// 字符设备操作函数集合
static const struct file_operations test_fops = {
.owner = THIS_MODULE,
.open = xxx_open,
.release = xxx_release,
};
test_cdev.owner = THIS_MODULE;
cdev_init(&test_cdev, &test_fops); // 初始化cdev结构体变量
cdev_add(&test_cdev, devid, 1); // 添加1个字符设备
cdev_del(&test_cdev); // 删除cdev
自动创建设备节点
Linux 下通过 udev 来实现设备文件的创建与删除,udev 可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。在嵌入式Linux中使用udev的简化版本mdev实现自动创建和删除设备节点。
自动创建设备节点在驱动程序的入口函数完成,在cdev_add函数后添加自动创建设备节点代码。
创建类
- struct class *class_create (struct module *owner, const char *name);
- owner:一般为THIS_MODULE,name:类名。返回创建的类(指向结构体class的指针)
删除类
- void class_destroy(struct class *cls)
- cls:删除的类
创建设备
- struct device *device_create(struct class *class, struct device *parent, dev_t devt, void *drvdata, const char *fmt, ...)
- class:创建设备所处的类,parent:父设备,一般为NULL,devt:设备号,drvdata:设备可能使用的数据,一般为NULL,fmt:设备名字(/dev/设备名)。返回创建的设备
删除设备
先删除设备,再删除类
- void device_destroy(struct class *class, dev_t devt)
- class:删除设备所处的类,devt:删除的设备号
struct class *class; // 类
struct device *device; // 设备
dev_t devid // 设备号
// 创建类
class = class_create(THIS_MODULE, "xxx_class_name");
// 创建设备
device = device_create(class, NULL, devid, NULL, "xxx_device_name");
// 删除设备
device_destroy(class, devid);
// 删除类
class_destroy(class);
设置文件私有数据
将设备的设备号、类、设备等属性信息做成一个设备结构体,在驱动的xxx_open函数中将设备结构体作为私有数据添加到设备文件中,在xxx_write、xxx_read、xxx_release等函数中可以直接读取私有数据private_data得到设备结构体。
/* 设备结构体 */
struct test_dev{
dev_t devid; // 设备号
struct cdev cdev; // cdev
struct class *class; // 类
struct device *device; // 设备
int major; // 主设备号
int minor; // 次设备号
};
struct test_dev test; // test设备
static int xxx_open(struct inode *inode, struct file *filp){
filp->private_data = &test; // 设置私有数据
return 0;
}
static int xxx_release(struct inode *inode, struct file *filp){
struct test_dev *dev = (struct test_dev*)filp->private_data; // 提取私有数据
}
goto
在某处申请资源出错,需要将前面申请的资源逐次释放掉。goto命令跳转到释放命令。
goto fail_xxx_1; // 错误点1
goto fail_xxx_2; // 错误点2
goto fail_xxx_3; // 错误点3
goto fail_xxx_4; // 错误点4
fail_xxx_4:
// 释放错误4~3之前的资源
fail_xxx_3:
// 释放错误3~2之前的资源
fail_xxx_2:
// 释放错误2~1之前的资源
fail_xxx_1:
// 释放错误1之前的资源和返回错误代码