驱动开发入门总结

64 阅读12分钟

最近移植ATV 14代码涉及到驱动部分,特意学习了一下,以此记录供后续学习参考
一、概念简介
所谓驱动,就是内核与外部设备的媒介,下面介绍一下有关驱动需要知道的一些知识。
1.1 设备驱动程序的主要功能

  • 对设备初始化和释放
  • 内核与硬件的数据交互
  • 应用程序和硬件的数据交互
  • 硬件的错误检测

1.2 驱动程序的主要类型

字符设备
  • 提供连续的数据流,应用程序可以顺序读取,通常不支持随机存取。此类设备支持按字节/字符来读写数据。
  • 可以使用自己制定的数据大小,通常以字节为单位输入输出
块设备
  • 以块为单位输入输出,应用程序可以随机访问设备数据,程序可自行确定读取数据的位置。
  • 对块设备读写时,利用系统内存作缓冲区,当用户进程对设备请求能满足用户的要求就返回请求的数据
网络设备

1.3 设备文件
可以查看一下dev目录:

ls -l /dev

可以看到很多的设备文件节点

crw-------  1 root        root        507,   0 1969-12-31 16:00 unifykeys
crw-rw-rw-  1 root        root          1,   9 1969-12-31 16:00 urandom

文件类型

  • 上面格式的第一个字符c代表了这个设备文件的文件类型为字符设备,b就是块设备,网络设备没有设备文件。

主设备号

  • 设备类型和主设备号唯一确定设备文件的驱动程序和界面。在上述格式中507, 1代表了主设备号。

次设备号

  • 说明目标设备是同类设备的第几个,在上述格式中0, 9就是代表了次设备号。
    例如下面两个字符设备同属于一种设备,但不是一个设备。

    crw-rw-rw-  1 media       system      496,   7 1969-12-31 16:00 media.audio
    crw-rw-rw-  1 media       system      496,   6 1969-12-31 16:00 media.codec_mm
    

1.4 sys文件系统:
统一管理查看内核功能参数和设备模型。sysfs也是一个存在于内存中的“伪”文件系统。sys文件系统提供了另一个从用户空间通往内核空间的入口,Linux系统启动时把它挂载到/sys目录,通过访问这个目录下面的文件,可以获得各种的系统内核信息,例如设备、内核模块、文件系统等等。

/sys/block # 所有块设备
/sys/bus # 按总线类型分层放置的目录结构
/sys/class # 按设备功能放置
/sys/class/mem # mem目录包含各个设备的链接,指向devices各个具体设备
/sys/devices # 分层次放置
/sys/dev # 字符设备和块设备的主次号
/sys/fs # 描述所有文件系统
/sys/kernel # 内核所有可调整参数位置
/sys/module # 所有模块信息
/sys/power # 系统电源选项

二、基础编程
驱动程序通常是以内核模块的方式编写,并且插入到系统内核进行执行,所以我们得先了解什么是内核模块。

2.1 内核模块
Linux是一个单体内核系统,整个内核在一个地址空间。Linux提供了模块机制,来为其增加设备;只需编译模块,再插入内核就可以完成设备增加。而内核模块就是可以在系统运行期间动态安装和拆卸的内核功能单元。

2.1.1 设备驱动的编译和加载方式
直接编译进内核,随同Linux启动时加载
编译成可加载删除模块,insmod加载,rmmod删除
2.1.2 一个模块被插入时的主要工作
打开要安装的模块(·ko文件),读进用户空间。
链接其他函数到内核。即把外部函数的地址填入访问指令和数据结构中
在内核创建module数据结构,申请系统空间
将完成链接的模块映像装入内核空间,并在内核登记模块相关的数据结构(里面有相关操作的函数指针)

3.2 内核编程
要编写一个内核模块就要先了解一下基本函数。
首先,内核与用户之间数据是不互通的,要互相使用数据得经过系统调用,系统调用中有着一些基本函数,用来完成基本任务。
比如:

copy_to_user
    主要用于将内核段中的数据拷贝到用户段的内存中去
copy_from_user
    主要用于将用户段内存中的数据拷贝到内核中

这些函数在用户态是无法使用的,也就是说,在外部写的.c程序库中是不包含这两个函数的。所以编写内核程序是与编写普通c程序是有所区别的。

3.2.1 内核模块编程模板
下面贴出一个简单的helloworld内核程序,我们在具体程序中进行解释。

#include<linux/init.h>  // 定义了module_init等函数
#include<linux/module.h> // 最基本的头文件,其中定义了MODULE_LICENSE等宏

// 当插入内核模块时,系统将调用下面的module_init宏,然后通过module_init调用此函数
static int hello_init(void){
    printk(KERN_CRIT "HELLO WORLD!!!\n"); 
    return 0;
}
// 与hello_init对应,在移除该内核模块时调用module_exit宏,然后调用此函数
static void hello_exit(void){
    // KERN_WARNING级别数字为4
    printk(KERN_WARNING "bye bye!!\n");
    return;
}
// 下面都是宏,在加载卸载模块时调用
module_init(hello_init);
module_exit(hello_exit);

// 下面的内容是必须的,用于表明该模块的信息,用modinfo *.ko即可查看
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xxxxxx");
MODULE_DESCRIPTION("一个简单的内核模块测试");

接下来编写Makefile文件

obj-m:=hello_module.o
PWD:=$(shell pwd)
default:
    $(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
     
clean:
    $(MAKE) -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

测试:

将驱动代码.c和Makefile放到源码指定目录,使用make即可编译出ko文件
insmod hello_module.ko 安装模块
dmesg查看内核log
rmmod hello_module.ko  卸载模块

第二部分

一、defconfig和.config
在Linux内核里,编译内核文件时,先要配置.config文件,然后Makefile在编译时通过读取.config文件的配置来选择要编译的文件,选择驱动的加载方式。

xxx_defconfig 一般在arch/xxx/configs/目录下,是一个没有展开的内核配置,需要配合Kconfig展开成.config
需要注意的是,从defconfig到.config不是简单的复制操作,
而是通过make ARCH=xxx defconfig
.confg也不是直接拷贝成defconfig,而是使用make ARCH=xxx savedefconfig
正确使用和保存deconfig的流程:
比如:要修改在arch/arm64/configs下的文件xxx_defconfig

1. make ARCH=arm64 xxx_defconfig 会生成.config文件
2. make ARCH=arm64 menuconfig 修改配置后保存
3. make ARCH=arm64 savedefconfig 生成defconfg文件
4. cp defconfig arch/arm64/configs/xxx_defconfig 保存

这样保存的defconfig文件,配置最小化,而且后续能恢复成.config,方便调试(tips:其实直接修改xxx_defconfig也能达到同样的效果,但如果要使用make menuconfig进行配置,就需要进行defconfig和.config的转换)
因为代码没有保存.config文件,所以不把defconfig转换成.config的话,使用make menuconfig展现出来的配置就会是Kconfig里面配置的默认值,这显然和当前系统使用的配置是不一样的。

二、make menuconfig的使用
menuconfig中操作相关的几个键盘按键,
主要是;Enter、ESC、四个方向箭头按键。

向上和向下箭头,主要用来在选择项菜单中目录浏览时上下翻.
回车,主要作用是选中并且执行select/exit/help/save/load
ESC,主要作用是返回上一层

image.png

向左和向右箭头,主要作用是在菜单选项(select、exit、help)间切换
按键Y、N、M三个按键的作用分别是将选中模块编入、去除、模块化
[ ]不可以模块化,<>的才可以模块化。
*表示编入,空白表示去除,M表示模块化

image.png

配置项说明:

if AMLOGIC_LED

config AMLOGIC_LED_SYS
    bool "System LED Support"  #bool表示System LED Support是不可编译成模块的,配置选项中显示[]
    depends on LEDS_CLASS #依赖于LEDS_CLASS的编译
    default n #默认值是n 不编译
    help
      This option enables support for system led drivers.

      Enable this option to allow the userspace to control
      the system led.

config AMLOGIC_LEDS_TLC59116
    tristate "LED Support for the TLC59116"#tristate 表示LED Support for the TLC59116是可编译成模块的,配置选项中显示<>
    depends on LEDS_CLASS#依赖于LEDS_CLASS的编译
    default n #默认值是n 不编译
    help
      Choose this option if you want to use the LED on
      TLC59116.
endif

对应图形化显示如下:

image.png

三、以S905Y2平台为例,梳理defconfig-->.config-->menuconfig修改配置-->.config-->defconfigd 流程
1、执行make ARCH=arm64 meson64_defconfig生成.config文件

image.png

2、执行make ARCH=arm64 menuconfig,配置中去掉System LED Support的编译,save->exit

image.png

image.png

3、执行make ARCH=arm64 savedefconfig,可以看到生成了defconfig文件

image.png

4、执行cp defconfig arch/arm64/configs/meson64_defconfig

image.png

其实最终就是拿掉了CONFIG_AMLOGIC_LED_SYS=y的配置,使其不编译

第三部分 module_init

在驱动代码中经常能看到module_init,module_exit。它们声明了模块的入口函数和出口函数,那入口函数是怎么被调用的呢?为什么这样声明就可以实现呢?

module_init(hello_init);
module_exit(hello_exit);

下面以module_init(hello_init)为例,介绍一下module_init的调用逻辑

一、module_init宏定义
module_init实际上是在include/linux/module.h里面定义的一个宏,老版本是在include/linux/init.h,可以看到如下代码:

#ifndef MODULE
// 省略
#define module_init(x)  __initcall(x);
// 省略
#else /* MODULE */
 
#define module_init(initfn) \
    int init_module(void) __copy(initfn) __attribute__((alias(#initfn)));
// 省略
#endif

MODULE 是由 Makefile 控制的。上面部分用于将模块静态编译连接进内核,下面部分用于编译可动态加载的模块,以静态加载为例
代码梳理:可在kernel源码下搜到相关的宏定义

#define module_init(x)  __initcall(x);
|
--> #define __initcall(fn) device_initcall(fn)
    |
    --> #define device_initcall(fn)     __define_initcall(fn, 6)
        |
        --> #define __define_initcall(fn, id) \
                  static initcall_t __initcall_name(fn, id) __used \
                       __attribute__((__section__(".initcall" #id ".init"))) = fn;

module_init(hello_init)可展开为:

static initcall_t __initcall_hello_init6 __used \
    __attribute__((__section__(".initcall6.init"))) = hello_init

initcall_t 是函数指针类型,如下:

typedef int (*initcall_t)(void);

attribute用来指定变量或结构位域的特殊属性,其后的双括弧中的内容是属性说明,它的语法格式为:attribute ((attribute-list))。它有位置的约束,通常放于声明的尾部且“ ;” 之前。
这里的 attribute-list为 section(“.initcall6.init”),section 属性就是用来指定将一个函数、变量存放在特定的段中。
所以这里的意思就是:定义一个名为 __initcall_hello_init6_used 的函数指针变量,并初始化为 hello_init(指向hello_init);并且该函数指针变量存放于 .initcall6.init 代码段中。
通过查看链接脚本(arch/arm/kernel/vmlinux.lds.S)来了解 .initcall6.init 段。

    .init.data : {
        INIT_DATA
        INIT_SETUP(16)
        INIT_CALLS
        CON_INITCALL
        SECURITY_INITCALL
        INIT_RAM_FS
        *(.init.rodata.* .init.bss)    /* from the EFI stub */
    }

可以看到,.init 段中包含 INIT_CALLS,它定义在 include/asm-generic/vmlinux.lds.h

#define INIT_CALLS_LEVEL(level)                        \
        VMLINUX_SYMBOL(__initcall##level##_start) = .;        \
        KEEP(*(.initcall##level##.init))            \
        KEEP(*(.initcall##level##s.init))            \

#define INIT_CALLS                            \
        VMLINUX_SYMBOL(__initcall_start) = .;            \
        KEEP(*(.initcallearly.init))                \
        INIT_CALLS_LEVEL(0)                    \
        INIT_CALLS_LEVEL(1)                    \
        INIT_CALLS_LEVEL(2)                    \
        INIT_CALLS_LEVEL(3)                    \
        INIT_CALLS_LEVEL(4)                    \
        INIT_CALLS_LEVEL(5)                    \
        INIT_CALLS_LEVEL(rootfs)                \
        INIT_CALLS_LEVEL(6)                    \
        INIT_CALLS_LEVEL(7)                    \
        VMLINUX_SYMBOL(__initcall_end) = .;

普通驱动程序的优先级是6。其它模块优先级列出如下:值越小,越先加载

#define pure_initcall(fn)           __define_initcall("0",fn,0)   
#define core_initcall(fn)           __define_initcall("1",fn,1)  
#define core_initcall_sync(fn)      __define_initcall("1s",fn,1s)    
#define postcore_initcall(fn)       __define_initcall("2",fn,2)    
#define postcore_initcall_sync(fn)  __define_initcall("2s",fn,2s)    
#define arch_initcall(fn)           __define_initcall("3",fn,3)    
#define arch_initcall_sync(fn)      __define_initcall("3s",fn,3s)    
#define subsys_initcall(fn)         __define_initcall("4",fn,4)    
#define subsys_initcall_sync(fn)    __define_initcall("4s",fn,4s)    
#define fs_initcall(fn)             __define_initcall("5",fn,5)    
#define fs_initcall_sync(fn)        __define_initcall("5s",fn,5s)    
#define rootfs_initcall(fn)         __define_initcall("rootfs",fn,rootfs)    
#define device_initcall(fn)         __define_initcall("6",fn,6)    
#define device_initcall_sync(fn)    __define_initcall("6s",fn,6s)    
#define late_initcall(fn)           __define_initcall("7",fn,7)    
#define late_initcall_sync(fn)      __define_initcall("7s",fn,7s)

通过上下文可以展开为:

        __initcall_start = .;           \
        *(.initcallearly.init)          \
        __initcall0_start = .;          \
        *(.initcall0.init)              \
        *(.initcall0s.init)             \
        // 省略1、2、3、4、5
        __initcallrootfs_start = .;     \
        *(.initcallrootfs.init)         \
        *(.initcallrootfss.init)            \
        __initcall6_start = .;          \
        *(.initcall6.init)              \
        *(.initcall6s.init)             \
        __initcall7_start = .;          \
        *(.initcall7.init)              \
        *(.initcall7s.init)             \
        __initcall_end = .;

上面这些代码段最终在 kernel 中按先后顺序组织,也就决定了位于其中的一些函数的执行先后顺序(__initcall_hello_init6 位于 .initcall6.init 段中)。.init 或者 .initcalls 段的特点就是,当内核启动完毕后,这个段中的内存会被释放掉。

二、kernel初始化,静态加载的模块初始化函数调用
存放于 .initcall6.init 段中的 __initcall_hello_init6 是怎么样被调用的呢?可以从kernel的初始化入手init/main.c,代码梳理如下:

start_kernel
|
--> rest_init
    |
    --> kernel_thread
        |
        --> kernel_init
            |
            --> kernel_init_freeable
                |
                --> do_basic_setup
                    |
                    --> do_initcalls
                        |
                        --> do_initcall_level(level)
                            |
                            --> do_one_initcall(initcall_t fn)

直接干到do_initcalls中:

static void __init do_initcalls(void)
{
    int level;

    for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
        do_initcall_level(level);
}

循环的对象是initcall_levels数组,该数组用于描述初始化调用的级别,定义如下:

extern initcall_t __initcall_start[];
extern initcall_t __initcall0_start[];
extern initcall_t __initcall1_start[];
extern initcall_t __initcall2_start[];
extern initcall_t __initcall3_start[];
extern initcall_t __initcall4_start[];
extern initcall_t __initcall5_start[];
extern initcall_t __initcall6_start[];
extern initcall_t __initcall7_start[];
extern initcall_t __initcall_end[];

static initcall_t *initcall_levels[] __initdata = {
    __initcall0_start,
    __initcall1_start,
    __initcall2_start,
    __initcall3_start,
    __initcall4_start,
    __initcall5_start,
    __initcall6_start,
    __initcall7_start,
    __initcall_end,
};

do_initcalls()函数核心操作是:按顺序从initcall0到initcall_end结束的节段中取出存在不同段之间的函数,并调用do_initcall_level()去执行,hello_init处于initcall6。

static void __init do_initcall_level(int level)
{
    // 省略
    for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
        do_one_initcall(*fn);
}

循环的操作对象为函数指针,且会将对应的函数指针传递到do_one_initcall中,在该函数则会执行函数指针所指向的函数,即hello_init,至此模块初始化函数hello_init得到调用

int __init_or_module do_one_initcall(initcall_t fn)
{
    int ret;
    // 省略
    ret = fn();
    return ret;
}