基于linux内核的驱动开发学习
1 驱动
定义:驱使硬件动起来的程序
种类:裸机驱动:需求分析--》查原理图--》查芯片手册--》code
系统驱动:需求分析--》查原理图--》查芯片手册--》设备树--》code
--》安装到内核中
裸机开发&系统开发的优缺点?
裸机开发:成本低 运行效率高 安全性低 单任务
系统开发:成本高 运行效率低 安全性低 多任务
应用程序和驱动程序的区别?
应用程序 驱动程序
1 加载方式 主动加载 被动加载
2 运行空间 用户空间 kernel空间
3 执行权限 低 高
4 影响力 局部 全局
5 函数来源 自定义/库/系统调用 内核函数/自定义
2 模块--》驱动模块
模块:能够单独命名并且独立完成一定功能的程序语句的集合(程序代码和数据结构)
驱动模块:能够单独命名并且独立完成特定外设功能驱动的程序语句的集合
注:一个驱动模块就是一个完整的外设驱动程序,驱动程序被安装到操作系统内核中,
当该驱动程序对应的外设要工作时,该驱动模块被调用。
2.1如何写一个驱动模块?
1 模块初始化函数 int 函数名1(void)
2 模块清除函数 void 函数名2(void)
3 模块加载函数 module_init(函数名1)--》sudo insmod hello.ko
4 模块卸载函数 module_exit(函数名2)--》sudo rmmod hello.ko
5 声明该驱动遵守GPL--》MOUDULE_LICENSE("GPL”)
《include/linux/init.h》
#define module_init(initfn) \
static inline initcall_t __inittest(void) \
{ return initfn; }
#define module_exit(exitfn) \
static inline exitcall_t __exittest(void) \
{ return exitfn; }
2.2如何编译驱动模块
hello.c-->hello.ko
test.c-->a.out
需要实现一个Makefile:
Makefile:
ifeq ($(KERNELRELEASE),)
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
//进入/lib/modules/3.5.0-23-generic/build下执行Makefile,
将PWD路径下的代码编译成一个hello.o
else
obj-m := hello.o //将hello.o链接成hello.ko
endif
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
Module* modules*
3.3 将驱动模块安装到Linux内核中
sudo insmod hello.ko-->将驱动模块hello.ko安装到linux内核中
lsmod-->查看当前系统中所有已加载的驱动模块
dmesg |tail-->查看内核缓存区后10行打印信息
dmesg |tail -20-->查看内核缓存区后20行打印信息
modinfo hello.ko-->查看模块信息描述
sudo rmmod hello.ko-->将hello.ko从内核中移除
3.4将驱动代码分成两个文件
hello.c-->hello_init.c&hello_exit.c-->hello_init.o&hello_exit.o-->hello.ko
需要实现一个Makefile:
Makefile:
ifeq ($(KERNELRELEASE),)
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
//进入/lib/modules/3.5.0-23-generic/build下执行Makefile,
将PWD路径下的代码编译成一个hello.o
else
obj-m := hello.o //将hello.o链接成hello.ko
hello-objs=hello_init.o hello_exit.o//将两个.o文件连接成一个hello.o文件
endif
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
Module* modules*
3 模块传参
应用程序:
传参:./a.out 1.txt 2.txt
接收传参:int main(int argc,char *argv[])
驱动程序:
传参:安装驱动模块时传 sudo insmod hello.ko gtest=100
接收传参:1 用全局变量 int gtest;
2 声明该全局变量接收传参
如何声明一个全局变量可以接收shell终端的参数传递?用内核函数声明
module_param(name, type, perm)
name:参数名,既是内部参数名,又是外部参数名
type:参数的数据类型
perm:访问权限,0644。0表示该参数在文件系统中不可见
module_param_string(name, string, len, perm)
name:外部参数名
string:内部参数
len:数组的大小
perm:访问权限,0644。0表示该参数在文件系统中不可见
module_param_array(name, type, nump, perm)
name:数组参数名,既是内部参数名,又是外部参数名
type:参数的数据类型(数组成员的数据类型)
nump:用来存放终端传给数组的实际元素个数
perm:访问权限,0644。0表示该参数在文件系统中不可见
测试步骤:
1 sudo insmod hello.ko gtest=100
2 dmesg |tail-->查看gtest的值是否是100?
3 cd /sys/module/hello/paramters
ls -l--->gtest
cat gtest-->100?
sudo chmod 777 gtest
echo 200 > gtest
cat gtest-->200?
4 sudo rmmod hello.ko
5 dmesg |tail-->gtest=200?
4 符号导出
1 什么是符号?在内核和驱动中主要是指全局变量和函数
2 为什么要导出符号?
linux内核是以模块化形式管理内核代码的。内核中的每个模块之间是相互独立
的,也就是说A模块中的全局变量和函数,B模块是无法访问的,若B模块想要使用
A模块已有的符号,那么必须将A模块中的符号做导出,导出到模块符号表中,然后
B模块才能使用符号表里的符号。
3 如何做符号导出?
linux内核给我们提供了两个宏:
EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符号名);
EXPORT_SYMBOL_GPL(gtest);
EXPORT_SYMBOL_GPL(func);
例如:模块A-->符号导出的模块
hello.c-->EXPORT_SYMBOL_GPL(gtest);
EXPORT_SYMBOL_GPL(func);
make成功后会生成一个存放符号的本地符号表,本地符号表中存放的就是
代码里导出的符号
Module.symvers:
Addr------------符号名------------模块名-------------导出符号的宏
0x8eaf8fe3 gtest /home/farsight/2022/22101/driver/day1/module_symbol/module_exportA/hello EXPORT_SYMBOL_GPL
0xd1a68ac8 Func /home/farsight/2022/22091/driver/day1/module_symbol/module_exportA/hello EXPORT_SYMBOL_GPL
模块B--》使用导出符号的模块
使用条件:1 将A模块导出的符号表拷贝到B模块中
2 在B模块的代码里外部声明使用哪个符号,然后才能使用
测试步骤:
1 sudo insmod hello.ko-->符号导出模块
2 sudo insmod world.ko--》使用符号的模块
3 dmesg |tail -->
4 lsmod
5 sudo rmmod world.ko
6 sudo rmmod hello.ko
linux内核提供了两类符号表:
第一种:用户自定义模块导出的符号表,称为本地符号表Module.symvers
第二种:内核全局符号表 /proc/kallsyms
sudo cat /proc/kallsyms |grep printk
c15cd833 T printk
函数指针=0xc15cd833;
int printf(const char *format,...)