1)字符设备:就是一个一个字节,按照字节流进行读写操作的设备,读写数据是分先后顺序的。
比如常见的点灯,按键,IIC,SPI,LCD等都是字符设备。
2)Linux中一切皆文件,驱动加载成功后会在“/dev”目录下生成一个相应的文件,应用程序通过对这个名为“/dev/xxx”(xxx是具体驱动文件名)进行
相应操作即可实现对硬件的操作。
3)应用程序运行在用户空间,驱动运行于内核空间,用户空间不能直接对内核进行操作,因此必须使用“系统调用”的方式实现从用户空间陷入到内核空间,
这样才能实现对底层驱动的操作。open/close/write和read等函数是由C库提供的,在Linux系统中,系统调用作为C库的一部分。
4.)Linux驱动两种运行方式:
a).将驱动编译进Linux内核中,这样当Linux内核启动时就会自动运行驱动程序。
b).将驱动编译成模块(Linux下模块扩展名为.ko),在Linux内核启动后使用“insmod”或"modprobe"命令加载驱动模块。
在调试驱动时一般都选择将其编译为模块,这样修改驱动后只需要编译一下驱动代码即可,不需要编译整个Linux代码,
而且在调试时只需要加载或者卸载驱动模块即可,不需要重启整个系统。
5)字符设备驱动开发步骤:
a) 驱动模块的加载和卸载:
a..) module_init(xxx_init); //注册模块加载函数
当使用“insmod”命令加载驱动的时候,xxx_init 这个函数就会被调用。
b..) module_exit(xxx_exit); //注册模块卸载函数(xxx_exit)
当使用“rmmod”命令卸载具体驱动的时候 xxx_exit 函数就会被调用。
c..) 两种加载使用驱动模块命令:“insmod”和 “modprobe”,
“insmod” 是最简单的模块加载命令,此命令用于加载指定的.ko 模块。insmod 命令不能解决模块的依赖关系,只能层层进行依赖模块加载。
“modprobe” 会分析模块的依赖关系,然后会将所有的依赖模块都加载到内核中,modprobe 命令主要智能在提供了模块的依赖性分析、错误检查、
错误报告等功能,推荐使用 modprobe 命令来加载驱动。
“modprobe” 命令默认会去/lib/modules/<kernel-version>目录中查找模块,一般自己制作的根文件系统中是不会有这个目录的,所以需要自己手动创建。
d..) 两种卸载使用驱动模块命令:“rmmod”和“modprobe -r”
使用 modprobe 命令可以卸载掉驱动模块所依赖的其他模块,前提是这些依赖模块已经没有被其他模块所使用,
否则就不能使用 modprobe 来卸载驱动模块。所以对于模块的卸载,还是推荐使用 rmmod 命令。
b) 字符设备注册与注销:
对于字符设备驱动而言,当驱动模块加载成功以后需要注册字符设备,
同样,卸载驱动模块的时候也需要注销掉字符设备。
一般字符设备的注册在驱动模块的入口函数 xxx_init 中进行,
字符设备的注销在驱动模块的出口函数 xxx_exit 中进行。
命令 “cat /proc/devices”可以查看当前已经被使用掉的设备号。
a..) 字符设备的注册函数原型:
static inline int register_chrdev(unsigned int major, const char *name, const struct file_operations *fops)
参数含义:
major:主设备号。
name:设备名字,指向一串字符串。
fops:结构体 file_operations 类型指针,指向设备的操作函数集合变量。
b..) 字符设备的注销函数原型:
static inline void unregister_chrdev(unsigned int major, const char *name)
参数含义:
major:要注销的设备对应的主设备号。
name:要注销的设备对应的设备名。
c) 实现设备的具体操作函数:
file_operations 结构体就是设备的具体操作函数。根据需求,初始化设备的file_operations 结构体变量,即可设置设备对应的操作函数。
d) 添加 LICENSE 和作者信息:
LICENSE 是必须添加的,否则的话编译的时候会报错,作者信息可以添加也可以不添加。
MODULE_LICENSE(“xxxxx”) //添加模块 LICENSE 信息
MODULE_AUTHOR("xxxxx") //添加模块作者信息
5)Linux 设备号:
a) 设备号的组成:
Linux 中每个设备都有一个设备号,设备号由主设备号和次设备号两部分组成,主设备号表示某一个具体的驱动,次设备号表示使用这个驱动的各个设备。
Linux 提供了一个名为 dev_t 的数据类型表示设备号,是一个 32 位的数据类型,构成了主设备号和次设备号两部分,其中高 12 位为主设备号,低 20 位为次设备号。
因此 Linux系统中主设备号范围为 0~4095。
b) 设备号的分配:
a..) 静态分配设备号:由开发者静态指定一个设备号(容易带来冲突问题)。
a..) 动态分配设备号:在注册字符设备之前先申请一个设备号,系统会自动给你一个没有被使用的设备号,卸载驱动的时候释放掉这个设备号即可。
设备号申请函数:int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
参数含义:
dev:保存申请到的设备号。
baseminor:次设备号起始地址,alloc_chrdev_region 可以申请一段连续的多个设备号,这些设备号的主设备号一样,
但是次设备号不同,次设备号以 baseminor 为起始地址地址开始递增。一般 baseminor 为 0。
count:要申请的设备号数量。
name:设备名字。
设备号释放函数:void unregister_chrdev_region(dev_t from, unsigned count)
参数含义:
from:要释放的设备号。
count:表示从 from 开始,要释放的设备号数量。
6)printk 函数:
printk 相当于 printf 的孪生兄妹,printf 运行在用户态,printk 运行在内核态。
在内核中想要向控制台输出或显示一些内容,必须使用printk 这个函数。
printk 可以根据日志级别对消息进行分类,一共有 8 个消息级别(0 的优先级最高,7 的优先级最低)。
#define KERN_EMERG KERN_SOH "0" /* 紧急事件,一般是内核崩溃 */
#define KERN_ALERT KERN_SOH "1" /* 必须立即采取行动 */
#define KERN_CRIT KERN_SOH "2" /* 临界条件,比如严重的软件或硬件错误*/
#define KERN_ERR KERN_SOH "3" /* 错误状态,一般设备驱动程序中使用KERN_ERR 报告硬件错误 */
#define KERN_WARNING KERN_SOH "4" /* 警告信息,不会对系统造成严重影响 */
#define KERN_NOTICE KERN_SOH "5" /* 有必要进行提示的一些信息 */
#define KERN_INFO KERN_SOH "6" /* 提示性的信息 */
#define KERN_DEBUG KERN_SOH "7" /* 调试信息 */
如果要设置消息级别,参考如下示例:
printk(KERN_EMERG "gsmi: Log Shutdown Reason\n");
上述代码就是设置“gsmi: Log Shutdown Reason\n”这行消息的级别为 KERN_EMERG。
如果使用 printk 的 时 候 不 显 式 的 设 置 消 息 级 别 , 那 么 printk 将 会 采 用 默 认 级 别MESSAGE_LOGLEVEL_DEFAULT,
MESSAGE_LOGLEVEL_DEFAULT 默认为 4。
CONSOLE_LOGLEVEL_DEFAULT 控制着哪些级别的消息可以显示在控制台上,此宏默认为 7,意味着只有优先级高于 7 的消息才能显示在控制台上。
7)C 库文件操作基本函数:
a) open 函数
open 函数原型如下:int open(const char *pathname, int flags)
参数含义:
pathname:要打开的设备或者文件名。
flags:文件打开模式,以下三种模式必选其一: O_RDONLY 只读模式
O_WRONLY 只写模式
O_RDWR 读写模式
除了上述三种模式以外还有其他的可选模式,通过逻辑或来选择多种模式:
O_APPEND 每次写操作都写入文件的末尾
O_CREAT 如果指定文件不存在,则创建这个文件
O_EXCL 如果要创建的文件已存在,则返回 -1,并且修改 errno 的值
O_TRUNC 如果文件存在,并且以只写/读写方式打开,则清空文件全部内容
O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端。
O_NONBLOCK 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继I/O 设置为非阻塞
DSYNC 等待物理 I/O 结束后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新。
O_RSYNC r ead 等待所有写入同一区域的写操作完成后再进行。
O_SYNC 等待物理 I/O 结束后再 write,包括更新文件属性的 I/O。
返回值:如果文件打开成功的话返回文件的文件描述符。
b) read 函数
read 函数原型如下:ssize_t read(int fd, void *buf, size_t count)
参数含义:
fd:要读取的文件描述符。
buf:数据读取到此 buf 中。
count:要读取的数据长度,也就是字节数。
返回值:读取成功的话返回读取到的字节数;如果返回 0 表示读取到了文件末尾;如果返回负值,表示读取失败。
c) write 函数
write 函数原型如下:ssize_t write(int fd, const void *buf, size_t count)
参数含义:
fd:要进行写操作的文件描述符。
buf:要写入的数据。
count:要写入的数据长度,也就是字节数。
返回值:写入成功的话返回写入的字节数;如果返回 0 表示没有写入任何数据;如果返回负值,表示写入失败。
d) close 函数
close 函数原型如下:int close(int fd)
参数含义:
fd:要关闭的文件描述符。
返回值:0 表示关闭成功,负值表示关闭失败。
8)创建设备节点文件
驱动加载成功需要在/dev 目录下创建一个与之对应的设备节点文件,
应用程序就是通过操作这个设备节点文件来完成对具体设备的操作。
“mknod” 就是创建节点命令,
参考如下示例:mknod /dev/chrdevbase c 200 0
“/dev/chrdevbase”是要创建的节点文件,
“c”表示这是个字符设备,
“200”是设备的主设备号,
“0”是设备的次设备号。
2、MMU(内存管理单元)
1)MMU 主要完成的功能如下:
①完成虚拟空间到物理空间的映射。
②内存保护,设置存储器的访问权限,设置虚拟存储空间的缓冲特性。
虚拟地址(VA,Virtual Address)、物理地址(PA,PhyscicalAddress)。
2)Linux 内核启动的时候会初始化 MMU,设置好内存映射,设置好以后 CPU 访问的都是虚拟地址 。
物理内存和虚拟内存之间的转换,需要用到两个函数:ioremap 和 iounmap。
a) ioremap 函数:用 于 获 取 指 定 物 理 地 址 空 间 对 应 的 虚 拟 地 址 空 间。
#define ioremap(cookie,size) __arm_ioremap((cookie), (size), MT_DEVICE)
void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size, unsigned int mtype)
ioremap 是个宏,有两个参数:cookie 和 size,真正起作用的是函数__arm_ioremap,此函数有三个参数和一个返回值,
这些参数和返回值的含义如下:
phys_addr:要映射的物理起始地址。
size:要映射的内存空间大小。
mtype:ioremap 的类型,可以选择 MT_DEVICE、MT_DEVICE_NONSHARED、MT_DEVICE_CACHED 和 MT_DEVICE_WC,ioremap 函数选择 MT_DEVICE。
返回值:__iomem 类型的指针,指向映射后的虚拟空间首地址。
b) iounmap函数:卸载驱动的时候需要使用 iounmap 函数释放掉 ioremap 函数所做的映射。
iounmap 函数原型如下:
void iounmap (volatile void __iomem *addr)
iounmap 只有一个参数 addr,此参数就是要取消映射的虚拟地址空间首地址。
3)I/O 内存访问函数
这里说的 I/O 是输入/输出的意思,使用 ioremap 函数将寄存器的物理地址映射到虚拟地址以后,
我们就可以直接通过指针访问这些地址,但是 Linux 内核不建议这么做,而是推荐使用一组操作函数来对映射后的内存进行读写操作。
a) 读操作函数
u8 readb(const volatile void __iomem *addr) //8bit 读操作
u16 readw(const volatile void __iomem *addr) //16bit 读操作
u32 readl(const volatile void __iomem *addr) //32bit 读操作
a) 写操作函数
void writeb(u8 value, volatile void __iomem *addr) //8bit 写操作
void writew(u16 value, volatile void __iomem *addr) //16bit 写操作
void writel(u32 value, volatile void __iomem *addr) //32bit 写操作
3、新字符设备驱动
1) 分配和释放设备号
没有指定设备号的话就使用如下函数来申请设备号:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
给定了设备的主设备号和次设备号就使用如下所示函数来注册设备号即可:
int register_chrdev_region(dev_t from, unsigned count, const char *name)
注销字符设备之后要释放掉设备号,不管是通过alloc_chrdev_region函数还是register_chrdev_region函数申请的设备号,
统一使用如下释放函数:
void unregister_chrdev_region(dev_t from, unsigned count)
2) 新的字符设备注册方法
a) 字符设备结构
在 Linux 中使用 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 结构体变量,这个变量就表示一个字符设备。
b) cdev_init 函数
定义好 cdev 变量以后就要使用 cdev_init 函数对其进行初始化,cdev_init 函数原型如下:
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
参数 cdev 就是要初始化的 cdev 结构体变量,参数 fops 就是字符设备文件操作函数集合。
c) cdev_add 函数
cdev_add 函数用于向 Linux 系统添加字符设备(cdev 结构体变量)。
首先使用 cdev_init 函数完成对 cdev 结构体变量的初始化,然后使用 cdev_add 函数向 Linux 系统添加这个字符设备。
cdev_add 函数原型如下:
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
参数 p 指向要添加的字符设备(cdev 结构体变量),参数 dev 就是设备所使用的设备号,参数 count 是要添加的设备数量。
d) cdev_del 函数
卸载驱动的时候一定要使用 cdev_del 函数从 Linux 内核中删除相应的字符设备,cdev_del函数原型如下:
void cdev_del(struct cdev *p)
参数 p 就是要删除的字符设备。
3) 自动创建设备节点
a) mdev 机制
udev 是一个用户程序,在 Linux 下通过 udev 来实现设备文件的创建与删除,
udev 可以检测系统中硬件设备状态,可以根据系统中硬件设备状态来创建或者删除设备文件。
比如使用modprobe 命令成功加载驱动模块以后就自动在/dev 目录下创建对应的设备节点文件,
使用rmmod 命令卸载驱动模块以后就删除掉/dev 目录下的设备节点文件。
嵌入式 Linux 中我们使用mdev 来实现设备节点文件的自动创建与删除,Linux 系统中的热插拔事件也由 mdev 管理。
b) 创建和删除类
自动创建设备节点的工作是在驱动程序的入口函数中完成的,一般在 cdev_add 函数后面添加自动创建设备节点相关代码。
首先要创建一个 class 类,class 是个结构体,class_create 是类创建函数,class_create 是个宏定义。
struct class *class_create (struct module *owner, const char *name)
class_create 一共有两个参数,参数 owner 一般为 THIS_MODULE,参数 name 是类名字。返回值是个指向结构体 class 的指针,也就是创建的类。
卸载驱动程序的时候需要删除掉类,类删除函数为 class_destroy,函数原型如下:
void class_destroy(struct class *cls);
参数 cls 就是要删除的类。
c) 创建设备
创建好类以后还不能实现自动创建设备节点,还需要在这个类下创建一个设备。
使用 device_create 函数在类下面创建设备,device_create 函数原型如下:
struct device *device_create(struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...)
device_create 是个可变参数函数,参数 class 就是设备要创建哪个类下面;
参数 parent 是父设备,一般为 NULL,也就是没有父设备;参数 devt 是设备号;
参数 drvdata 是设备可能会使用的一些数据,一般为 NULL;
参数 fmt 是设备名字,如果设置 fmt=xxx 的话,就会生成/dev/xxx这个设备文件。返回值就是创建好的设备。
同样的,卸载驱动的时候需要删除掉创建的设备,设备删除函数为 device_destroy,函数原型如下:
void device_destroy(struct class *class, dev_t devt)
参数 class 是要删除的设备所处的类,参数 devt 是要删除的设备号。
d) 设置文件私有数据
每个硬件设备都有一些属性,比如主设备号(dev_t),类(class)、设备(device)、开关状态(state)等,
对于一个设备的所有属性信息最好将其做成一个结构体。编写驱动 open 函数的时候将设备结构体作为私有数据添加到设备文件中,
如下所示:
设备结构体 */
struct test_dev{
dev_t devid; /* 设备号 */
struct cdev cdev; /* cdev */
struct class *class; /* 类 */
struct device *device; /* 设备 */
int major; /* 主设备号 */
int minor; /* 次设备号 */
};
在 open 函数里面设置好私有数据以后,在 write、read、close 等函数中直接读取 private_data即可得到设备结构体。