linux驱动的分类

  1. 字符设备驱动
    • IO的传输过程是以字符为单位的,没有缓冲。
    • 如I2C,SPI都是字符设备
  2. 快设备驱动
    • IO的传输过程是以快为单位的。跟存储相关的,都属于快设备,比如,tf卡
  3. 网络设备驱动
    • 与前两个不一样,是以套接字来访问的

ps:其中,理解和掌握字符设备驱动的概念最重要,因为在工作中遇到的大部分是字符设备驱动

驱动程序的内容

  1. 头文件
  2. 驱动模块的入口和出口
  3. 声明信息
  4. 功能实现

写一个驱动代码的流程

第一步:包含头文件

1
2
3
4
//包含宏定义的头文件
#include <linux/init.h>
// 包含初始化加载模块的头文件
#include <linux/module.h>

第二步:驱动模块的入口和出口

1
2
module_init();
module_exit();

第三步:声明模块拥有开源许可证

1
MODULE_LICENSE("GPL");

*第四步:模块功能的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

//包含宏定义的头文件
#include <linux/init.h>
// 包含初始化加载模块的头文件
#include <linux/module.h>

static int hello_init(void)
{
printk("hello world\n");
return 0;
}
static void hello_exit(void)
{
printk("Say goodbye");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

驱动的编译

两种方法:

  1. 把驱动编译成模块,然后使用命令把驱动加载到内核里面
  2. 直接吧驱动编译到内核

编译成模块

第一步:先写一个makefile

1
2
3
4
5
6
7
8
obj-m +=helloworld.o

KDIR:=/home/ccchenji/Desktop/linux-imx-rel_imx_4.1.15_2.1.0_ga

PWD ?=$(shell pwd)

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

第二步:编译驱动

编译驱动之前需要注意的问题:

  1. 内核源码一定要先编译通过
  2. 编译驱动用的内核源码一定要和板子上运行的内核镜像是同一套
  3. 看一下Ubuntu的环境是不是arm:进入linux源码目录,命令行输入!echo $ARCH,如果环境不是arm需要设置

设置环境变量:

1
2
export CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH=arm

编译成功以后,就可以看见ko文件了,这个ko文件就是编译好的驱动。

第三步:加载驱动

将编译好的驱动拷贝到板子里,使用insmod命令加载驱动
命令:insmod
格式:insmod xxx.ko

查看加载的驱动:
命令:lsmod

卸载驱动:
命令:rmmod
格式:rmmod xxx(注意:这里没有.ko的后缀)

make menuconfig图形化界面

进入到图形界面

方法:进入到内核源码的路径下,然后输入make menuconfig即可打开这个界面

make menuconfig 图形化界面的操作

1.搜索功能

输入 “\” 既可弹出搜索界面,然后输入想要搜索的内容既可。

2.配置驱动的状态

  1. 把驱动编译成模块,用 M 来表示
  2. 把驱动编译到内核里面,用”*”来表示
  3. 不编译。空格里没有东西

可以使用“空格”按键来配置这三种不同的状态。

3.退出

退出分为保存退出和不保存退出。

4.和make menuconfig有关的文件

  1. Makefile 里面是编译规则,告诉我们在make的时候要怎么编译
  2. Kconfig 内核配置的选项, 界面显示的内容
  3. .config 配置完内核以后生成的配置选项,会被顶层Makefile调用用于编译内核
  • make menuconfig读取那个目录的文件:arch/$ARCH/目录下的Kconfig
  • arch/$ARCH/configs 下面有很多的配置文件。是很多写好的默认配置

5.复制默认的配置文件要复制成.config

因为内核会默认读取Linux内核目录下的.config作为默认的配置选项,所以不能改名字。

6.复制的默认配置不符合要求怎么办

通过make menuconfig来配置自己的内核。配置完成以后会自动更新到.config里面。

7.怎么和Makefile文件建立关系

当make menuconfig 保存退出以后,在编译内核时,Linux会将所有的配置选项以宏定义的形式保存在include/generated/下面的autoconf.h里面。用于给内核c源码使用

Linux下把驱动编译进内核

kconfig的例子:

1
2
3
4
5
6
source "drivers/redled/Kconfig"
config LED_4412
tristate "Led Support for GPIO Led"
depends on LEDS_CLASS
help
This option enable support for led

说明:

  1. source “drivers/redled/Kconfig”,他会包含 drivers/redled/这个路径下的驱动文件,方便我们对菜单进行管理
  2. config LED__4412 配置选项的名称
  3. tristate 表示的驱动的状态,三种状态是把驱动编译成模块,把驱动编译到内核,不编译。与之对应的还 有 bool 分别是编译到内核,不编译
  4. “Led Support for GPIO Led” make menuconfig 显示的名字
  5. A depends on B 表示只有在选择 B 的时候才可以选择 A
  6. select,反向依赖,该选项被选中时,后面的定义也会被选中
  7. help:This option enable support for led(帮助信息)

例子:

说明:
将上面提到的hello.c文件编译进内核

步骤:

第一步:
进入linux源码目录下的/driver/char目录

1
$ cd driver/char/

第二步:
在当前目录新建hello文件夹,并进入

1
2
$ mkdir hello
$ cd hello/

第三步
在hello目录新建Makefile文件和Kconfig文件

1
2
$ touch Makefile
$ touch Kconfig

文件内容:

  1. Makefile文件:
    1
    obj-$(CONFIG_HELLO) += hello.o
  2. Kconfig文件内容:
    1
    2
    3
    4
    config HELLO
    tristate "hello world"
    help
    test first driver

第四步
返回上一级目录,将Makefile和Kconfig文件和上一级目录的Makefile和Kconfig文件关联

在上一级目录中的Makefile添加:

1
obj-$(CONFIG_HELLO)             += hello/

在上一级目录中的Kconfig添加:

1
source "drivers/char/hello/Kconfig"

第五步:

make menuconfig图形界面中就会出现hello驱动的选项,正常选择驱动,然后编译内核,就可以将驱动加载到内核。

应用层和内核层数据传输

应用层函数和驱动层函数的对应

  1. 应用层使用read设备节点时,会触发驱动里read这个函数
    • ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
  2. 应用层使用write设备节点时,会触发驱动里write这个函数
    • ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
  3. 应用层使用poll/select设备节点时,会触发驱动里poll这个函数
    • unsigned int (*poll) (struct file *, struct poll_table_struct *);
  4. 应用层使用ioctl设备节点时,会触发驱动里ioctl这个函数
    • long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
  5. 应用层使用open设备节点时,会触发驱动里open这个函数
    • int (*open) (struct inode *, struct file *);
  6. 应用层使用close设备节点时,会触发驱动里close这个函数
    • int (*release) (struct inode *, struct file *);

应用层 通过 设备节点(如:”/dev/ttyS5”) 来访问 底层驱动,设备节点就是连接上层应用和底层驱动的桥梁

应用层和内核层传递数据

应用层和内核层是不能直接进行数据传输的。想进行数据传输,要借助下面的这两个函数:

1
2
3
#include <linux/uaccess.h>
static inline long copy_from_user(void *to, const void __user * from, unsigned long n)
static inline long copy_to_user(void __user *to, const void *from,unsigned long n)

用户空间到内核空间

用户空间-->内核空间

内容 说明
头文件 #include
函数 copy_from_user(void to, const void __user from, unsigned long n)
参数 to 目标地址(内核空间)
参数 from 源地址(用户空间)
参数n 将要拷贝数据的字节
返回值 成功返回0,失败返回没有拷贝成功的数据字节
功能 将用户空间数据拷贝到内核空间

内核空间到用户空间

内核空间-->用户空间

内容 说明
头文件 #include
函数 copy_to_user(void _userto, const void from, unsigned long n)
参数 to 目标地址(用户空间)
参数 from 源地址(内核空间)
参数n 将要拷贝数据的字节
返回值 成功返回0,失败返回没有拷贝成功的数据字节
功能 将用户空间数据拷贝到内核空间

Linux物理地址到虚拟地址映射

目前,大多数嵌入式微控制器(如 ARM、PowerPC 等)中并不提供 I/O 空间,而仅存在内存空间。内存空间可以直接通过地址、指针来访问,程序及在程序运行中使用的变量和其他数据都存在于内存空间中。 内存地址可以直接由 C 语言指针操作,如:

1
2
unsigned int *p = (void*)0x12345678;
*p=0x12345678;

高性能处理器一般会提供一个内存管理单元(MMU),该单元辅助操作系统进行内存管理,提供虚 拟地址和物理地址的映射、内存访问权限保护和 Cache 缓存控制等硬件支持。操作系统内核借助 MMU 可以 让用户感觉到程序好像可以使用非常大的内存空间,从而使得编程人员在写程序时不用考虑计算机中物理 内存的实际容量。

MMU 具有虚拟地址和物理地址转换、内存访问权限保护等功能,这将使得 Linux 操作系统能单独为系 统的每个用户进程分配独立的内存空间并保证用户空间不能访问内核空间的地址,为操作系统的虚拟内存 管理模块提供硬件基础。上层应用看到的内存都是虚拟内存,应用就不能直接访问硬件,所以这样就保证了系统安全。

对于包含 MMU 的处理器而言,Linux 系统提供了复杂的存储管理系统,使得进程所能访问的内存达到 4GB。在 Linux 系统中,进程的 4GB 内存空间被分为两个部分——用户空间与内核空间。用户空间的地址一 般分布为 0~3GB(即 PAGE_OFFSET,在 0x86 中它等于 0xC0000000),这样,剩下的 3~4GB 为内核空间,如 下图所示。用户进程通常只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址。用户进程只有通 过系统调用(代表用户进程在内核态执行)等方式才可以访问到内核空间。

每个进程的用户空间都是完全独立、互不相干的,用户进程各自有不同的页表。而内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。内核空间的虚拟地址到物理地址映射是被所有进程共享的, 内核的虚拟空间独立于其他程序。

使用函数完成物理地址到虚拟地址的转换,函数定义在include/asm-generic/io.h

1
2
3
#include <linux/io.h>
ioremap:把物理地址转换为虚拟地址
iounmap:释放掉ioremap映射的地址
功能 说明
头文件 #include
函数 static inline void __iomem *ioremap(phys_addr_t offset, size_t size)
参数 phys_addr_t offset 映射物理地址的起始地址
参数size_t size 要映射多大的内存空间
返回值 成功返回虚拟地址的首地址,失败返回NULL
功能 把物理地址转换成虚拟地址
功能 说明
头文件 #include
函数 static inline void iounmap(void __iomem *addr)
参数*addr 要取消映射的虚拟地址的首地址
功能 释放掉 ioremap 映射的地址

注意: 物理地址只能被映射一次,多次映射会失败。可以使用以下命令查看那些物理地址被映射过了:

1
cat /proc/iomem