驱动的作用
从字面上解释,驱动就是“让硬件动起来",所以驱动是直接和硬件打交道的,是底层硬件和上层软件的桥梁。
有的时候并不一定需要操作系统,比如用单片机进行简单的通断控制,从编程角度来说,直接控制寄存器就可以了,也就是直接和硬件打交道。
有了操作系统以后,编写驱动就变的比较复杂,要基于Linux
的各种驱动框架进行编程。但是当驱动都按照系统给出的框架进行编程以后,就可以提供一个统一的接口给应用程序调用。
Linux
将驱动分为三类。
字符设备:字符设备指那些必须以串行顺序依次进行访问的设备,如鼠标。
网络设备:块设备可以按照任意顺序进行访问,如硬盘。
块设备:网络设备是面向数据包的接收和发送。
我们可以在https://www.kernel.org/下载到最新的Linux内核源码。历史版本可以在 https://www.kernel.org/pub/下载历史版本,一般我们使用的半导体产商提供的源码:NPX(恩智浦),MTK(联发科),ALLWINNER(全智),RK,TI(德州仪器)等等。
编译介绍
Linux
内核源码包含多级目录,形成一个巨大的树状结构,进入源码所在的目录,就是Linux
源码的顶层目录。例举linux-5.15
目录 | 说明 |
---|---|
arch | 架构相关目录,里面存放了许多CPU的架构,如arm,X86,MIPs等。 |
block | 存放块设备相关代码,在Linux 中用block表示块设备。比如硬盘,SD卡,都是块设备 |
certs | 存储了认证 和签名 相关代码 |
crypto | 存放加密算法目录 |
Documentation | 存放官方Linux内核文档 |
drivers | 驱动目录,里面存放了Linux系统支持的硬件设备驱动源码 |
firmware | 存放固件目录 |
fs | 存放支持的文件系统的代码目录,比如fat,ext2,ext3 等。 |
include | 存放公共的头文件目录 |
init | 存放Linux内核启动初始化的代码 |
ipc | 存放进程间通信代码 |
kernel | 存放内核本身的代码文件夹 |
lib | 存放库函数的文件夹 |
mm | 存放内存管理的目录,mm就是memory management的缩写 |
net | 存放网络相关代码,比如TCP/IP协议栈 |
samples | 内核实列代码 |
scripts | 存放脚本的文件夹 |
security | 存放安全相关代码 |
sound | 存放音频相关代码 |
tools | 存放Linux用到的工具文件夹 |
usr | 和Linux内核的启动有关代码 |
virt | 内核虚拟机相关代码 |
一个最简单的Linux
驱动主要由以下几个部分组成:
(1)头文件(必须有)
驱动需要包含内核相关头文件。必须包含
和
(2)驱动加载函数。(必须有)
当加载驱动的时候,驱动加载函数会自动被内核执行。
(3)驱动卸载函数(必须有)
当卸载驱动的时候,驱动卸载函数会自动被内核执行。
(4)许可证声明(必须有)
Linux
内核是开源的,遵守GPL
协议,驱动在加载的时候也要遵守相关的协议,可以接收的License有"GPL"、“GPL v2”、"GPL and additional rights"、"Dual BSD/GPL"、"Dual MIT/GPL"、"Dual MPL/GPL"
。内核驱动中最常见的是GPL v2
。
(5)模块参数(可选)
模块参数是模块被加载的时候传递给内核模块的值。
可以声明驱动的作者信息和代码的版本信息。
第一种编译方法:将驱动放在Linux
内核里面,然后编译Linux
内核。将驱动编译到Linux
内核里面。
第二种编译方法:将驱动编译成内核模块,独立于Linux
内核以外。
内核模块是Linux
系统中一个特殊的机制,可以将一些使用频率很少或者暂时不用的功能编译成内核模块,在需要的时候在动态加载到内核里面。
使用内核模块可以减小内核的体积,加快启动速度。并且可以在系统运行的时候插入或者卸载驱动,无需重启系统。内核模块的后缀是.ko
Makefile
解析:
# 表示把目标文件helloworld.o作为模块进行编译。obj就是object的缩写,-m表示编译成模块。
obj-m += helloworld.o
# 使用绝对路径的方式指定内核源码的路径。
# KDIR:=/lib/modules/$(shell uname -r)/build 在ubuntu 编译,
# 在某版本的linux内核源码编译
KDIR:=/home/fengzc/study/linux-5.15/
# 获取Makefile文件所在的路径
PWD?=$(shell pwd)
# 进到KDIR目录,使用PWD路径下源码和Makefile文件编译驱动模块
make -C $(KDIR) M=$(PWD) modules
# 清除编译文件
rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.order
驱动源码helloworld.c
#include
#include
static int helloworld_init(void){
printk("helloworld init\n");
return 0;
}
static void helloworld_exit(void){
printk("helloworld exit");
}
module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("FZC");
MODULE_VERSION("V1.0");
在驱动源码目录下创建Makefile
文件
obj-m += helloworld.o
KDIR:=/home/fengzc/study/linux-5.15/
PWD?=$(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.order
[Linux 源码下载](Linux 源码下载)的内核源码编译通过,内核不编译通过,无法编译内核模块
设置环境变量(看设置)
#设置架构,x86,arm64等
export ARCH=arm64
export CROSS_COMPILE=xxxxxx
输入make编译,编译成功后驱动源码目录下,生成helloworld.ko
模块加载命令
insmod
命令
功能:载入Linux内核模块
语法:
insmod
模块名
举例:
insmod helloworld.ko
modprobe
命令
功能:加载Linux内核模块,同时这个模块所依赖的模块也同时被加载语法
语法:
modprobe
模块名
举例:
modprobe helloworld.ko
模块卸载命令
rmmode
命令
功能:移除已经载入Linux的内核模块
语法:rmmod
模块名
rmmod helloworld.ko
查看模块信息命令
lsmod
命令
功能:列出已经载入Linux
的内核模块
也可以使用命令cat /proc/modules
来查看模块是否加载成功。
modinfo
命令
功能:查看内核模块信息
语法:modinfo
模块名
举例:
modinfo helloworld.ko
打开图形化配置界面
使用命令export ARCH=arm
设置平台架构,平台架构是arm
,arm64
,还是mips
要根据实际开发板架构选择。然后在内核源码的顶层目录下,输入命令make menuconfig
。
常见错误
"'make menuconfig' requires the ncurses libraries"
,请使用命令sudo apt-get install libncurses5-dev
安装ncurses
。Your display is too small to run Menuconfig!"
这个错误是因为控制终端的窗口太小,放大窗口或者全屏操作即可。"make:*** No rule to make target 'menuconig'. Stop."
这个错误是没有在内核源码的顶层目录输入make menuconfig
图形化配置界面操作
移动
使用键盘的上,下,左,右按键可以移动光标。
搜索功能
输入“/”即可弹出搜索界面,然后输入我们要搜索的内容即可
配置驱动选项状态操作
使用“空格”按键来配置这三种不同的状态。
选项的状态有[],<>,()三种表示状态,其中
[]表示有两种状态,只能设置成选中或者不选中,
<>M表示有三种状态,可以设置成选中,不选中,和编译成模块。
()表示用来存放字符串或者16进制数。
用做菜类比他们之间的关系:
Makefile
文件相当于菜的做法。
Kconfig
文件相当于饭店的菜单
.config
文件相当于我们使用饭店的菜单点完的菜品
Kconfig
文件是图形化配置界面的的源文件,图形化配置界面中的选项由Kconfig
文件决定。当我们执行命令make menuconfig
命令的时候,内核的配置工具会读取内核源码目录下的arch/xxx/Kconfig
。xxx
是命令export ARCH=arm
中的ARCH
的值。然后生成对应的配置界面供开发者使用。
config
文件和.config
文件都是Linux
内核的配置文件,config
文件位于Linux
内核源码的arch/$(ARCH)/configs
目录下,是Linux
系统默认的配置文件。.config
文件位于Linux
内核源码的顶层目录下,编译linux
内核时会使用.config
文件里面的配置来编译内核镜像。
若.config
存在,make menuconfig
界面的默认配置即当前.config
文件的配置,若修改了图形化配置界面的设置并保存,则.config
文件会被更新
若.config
文件不存在,make menuconfig
界面的默认配置则为Kconfig
文件中的默认配置。使用命令make xxx_defconfig
命令会根据arch/$(ARCH)/configs
目录下默认文件生成.config
文件。
可以用menu,endmenu
来生成菜单,menu是菜单开始的标志,endmenu
是菜单结束的标志。这俩个是成对出现的。
menu
config HELLOWORLD
endmenu
使用关键字config
来定义一个新的选项。每个选项都必须指定类型,类型包括bool,tristate,string,hex,int
。最常见的是bool,tristate,string
这三个。其中:
bool
类型有俩种值:y和n
tristat
有三种值:y、m和n
string
:为字符串类型
help
表示帮助信息,当我们在图形化界面按下h
按键,弹出来的就是help的内容。
举例:
config HELLOWORLD
bool "hello world support"
default y
help
hello world
Kconfig
中的依赖关系可以用depends on
和select
。
直接举例说明:
depends on
表示直接依赖关系:
config A
depends on B
#表示选项A依赖选项B,只有当B选项被选中时,A选项才可以被选中。
select
表示反向依赖关系:
config A
select B
#在A选项被选中的情况下,B选项自动被选中。
使用choice
和endchoice
定义可选择项。
直接举例说明:
choice
bool "a“
config b
boot b1
config c
boot c1
...
endchoice
Kconfig
中使用comment
用于注释,不过此注释非彼注释,这个注释是在图形化界面中显示一行注释。
举例:
config TEST_CONFIG
bool "test"
default y
help
just test
comment "just test"
source
用于读取另一个Kconfig
文件,如source "init/Kconfig"
就是读取init
目录下的Kconfig
文件。
例子
1、/home/fengzc/study/linux-5.15/drivers/char
创建helloworld
文件夹
2、/home/fengzc/study/linux-5.15/drivers/char/helloworld
创建Kconfig
config HELLOWORLD
bool "hello world support"
default y
help
hello world
3、/home/fengzc/study/linux-5.15/drivers/char/Kconfig
里增加
source "drivers/char/helloworld/Kconfig"
4、通过make menuconfig
命令,查看Device Drivers ---> Character devices ---> [*] hello world support
,然后save
保存退出
5、编写/home/fengzc/study/linux-5.15/drivers/char/helloworld
驱动
helloworld.c
#include
#include
static int helloworld_init(void){
printk("helloworld init\n");
return 0;
}
static void helloworld_exit(void){
printk("helloworld exit");
}
module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("FZC");
MODULE_VERSION("V1.0");
6、编译写Makefile
obj-$(CONFIG_HELLOWORLD) += helloworld.o
KDIR:=/home/fengzc/study/linux-5.15/
PWD?=$(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.order
7、/home/fengzc/study/linux-5.15/drivers/char/Makefile
修改添加
obj-y += helloworld/
8、查看有没有编译Linux
内核里(烧录镜像有没有helloworld
日志打印)
优势:
不足:
c语言中常用的数据类型内核大部分都支持驱动传参。这里将内核支持的驱动传递参数的类型分成三类:
基本类型:char,bool,int,long,short,byte,ushort,uint。
数组:array
家符串:string
驱动支持的参数类型有基本类型,数组,字符串。这三个类型分别对应函数:
module_param
:传递基本类型函数
函数功能:传递基本类型参数给驱动
函数原型:
module_param(name,type,perm)
函数参数:
name
:要传递给驱动代码中的变量的名字。
type
:参数类型。
perm
:参数的读写权限。
module_param_array
:传递数组类型函数
函数功能:传递数组类型参数给驱动
函数原型:
module_param_array(name,type,nump,perm)
函数参数:
name
:要传递给驱动代码中的变量的名字。
type
:参数类型。
nump
:数组的长度。
perm
:参数的读写权限。
module_param_string
:传递字符串类型函数
函数功能:传递字符串类型参数给驱动
函数原型:
module_param(name,string,len,perm)
函数参数:
name
:要传递给驱动代码中的变量的名字。
string
:驱动程序中变量的名字,要和参数name的名字保持一致。
len
:字符串的大小。
perm
:参数的读写权限。
MODULE_PARM_DESC
函数
函数功能:描述模块参数的信息。
在
include/linux/moduleparam.h
定义函数原型:
MODULE_PARM_DESC(_parm, desc)
函数参数:
_parm
:要描述的参数的参数名称。
desc
:描述信息。
这三个函数在Linux内核源码include/linux/moduleparam.h
中有定义,如下:
#define module_param(name, type, perm) \
module_param_named(name, name, type, perm)
#define module_param_array(name, type, nump, perm) \
module_param_array_named(name, name, type, nump, perm)
#define module_param_string(name, string, len, perm) \
static const struct kparam_string __param_string_##name \
= { len, string }; \
__module_param_call(MODULE_PARAM_PREFIX, name, \
¶m_ops_string, \
.str = &__param_string_##name, perm, -1, 0);\
__MODULE_PARM_TYPE(name, "string")
读写权限在include/linux/stat.h
和include/uapi/linux/stat.h
下有定义,一般使用S_IRUGO,也可以使用数字表示,如444表示S_IRUGO
include/uapi/linux/stat.h
# S_I不管,R:可读,W:可写,X:可执行,U、USR:用户所有者user,G、GRP:用户组,O,TH:其他人
#define S_IRWXU 00700
#define S_IRUSR 00400
#define S_IWUSR 00200
#define S_IXUSR 00100
#define S_IRWXG 00070
#define S_IRGRP 00040
#define S_IWGRP 00020
#define S_IXGRP 00010
#define S_IRWXO 00007
#define S_IROTH 00004
#define S_IWOTH 00002
#define S_IXOTH 00001
include/linux/stat.h
#define S_IRWXUGO (S_IRWXU|S_IRWXG|S_IRWXO)
#define S_IALLUGO (S_ISUID|S_ISGID|S_ISVTX|S_IRWXUGO)
#define S_IRUGO (S_IRUSR|S_IRGRP|S_IROTH)
#define S_IWUGO (S_IWUSR|S_IWGRP|S_IWOTH)
#define S_IXUGO (S_IXUSR|S_IXGRP|S_IXOTH)
#include
#include
#include
static int a = 0;
static int array[5] = {0};
static int array_size;
static char str1[10] = {}
module_param(a, int ,S_IRUGO)
MODULE_PARAM_DESC(a, "e.g. a = 1");
module_param_array(array, int, &array_size, S_IRUGO);
MODULE_PARAM_DESC(array, "e.g. array=1, 2, ");
module_param_string(str, str1, sizeof(str1), S_IRUGO);
MODULE_PARAM_DESC(str, "e.g. str = hellow")
static int helloworld_init(void){
int i = 0;
printk("a is %d\n, a");
for(i = 0;i< array_size;i ++){
printk("array[%d] is %d\n", i,
}
printk("str1 is %s\n", str1);
printk("helloworld init\n");
return 0;
}
static void helloworld_exit(void){
printk("helloworld exit");
}
module_init(helloworld_init);
module_exit(helloworld_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("FZC");
MODULE_VERSION("V1.0");
编译成内核模块 ,通过命令insmod helloworld.ko a=1 array=1,2,3 str=hello
驱动程序可以编译成内核模块,也就是KO
文件。每个KO文件是相互独立的,也就是说模块之间无法互相访问。但是在某些使用场景下要互相访问,如B模块要用A模块中的函数。此时符号表的概念就引入了
所谓“符号"就是内核中的函数名,全局变量名等。符号表就是用来记录这些“符号”的文件。
模块可以使用一下宏EXPORT_SYMBOL
和EXPORT_SYMBOL_GPL
导出符号到内核符号表中。
例:
EXPORT_SYMBOL
(符号名);
EXPORT_SYMBOL_GPL
(符号名);//只适用于包含GPL许可的模块。
导出去的符号可以被其他模块使用。使用前只需要声明一下即可。
a.c
#include
#include
extern int add(int a, int b);
int add(int a, int b){
return a + b;
}
EXPORT_SYMBOL(add);
static int helloA_init(void){
printk("helloA init\n");
return 0;
}
static void helloA_exit(void){
printk("helloA exit");
}
module_init(helloA_init);
module_exit(helloA_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("FZC");
MODULE_VERSION("V1.0");
b.c
#include
#include
extern int add(int a, int b);
static int helloB_init(void){
printk("helloB init\n");
int a = 0;
a = add(1,2);
printk("a value = %d", a);
return 0;
}
static void helloB_exit(void){
printk("helloB exit");
}
module_init(helloB_init);
module_exit(helloB_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("FZC");
MODULE_VERSION("V1.0");
a.c
,得到a.ko
的Module.symvers
,然后将它复制b.c
的同级目录b.c
得到b.ko
insmod a.ko
,然后insmod b.ko
,这样b
模块就可以使用a
模块的函数了rmmod b.ko
,然后rmmod a.ko