Unknown symbol __gnu_mcount_nc (err 0) insmod: can't insert 'test.ko': unknown symbol in module
LDDï¼Linux Device Driverï¼ç¬¬ä¸ç« scull设å¤ä¾åçæ¶åï¼ä¹éå°äºç¸ä¼¼çé®é¢ï¼åæ¥åç°æ¯ç»makeç¼è¯åºæ¥çscull.koçownerågroupå±æ§ä¸å¯¹ï¼èä¸å¼å§å¦ä¹ 第äºç« hello worldçæ¶åå´æ²¡æéå°è¿ä¸ªé®é¢ï¼å°è¯çchownåchgrpä¹åï¼ownågroupé½æ¹ærootï¼ï¼å°±è½å¤é¡ºå©å°ç¨insmodå 载该模åè¿å æ ¸äºã
Linux驱动开发笔记(一):helloworld驱动源码编写、源码makefile编写以及驱动编译基本流程
前言
基于linux的解析驱动开发学习笔记,本篇主要介绍了一个字符驱动的源码基础开发流程,适合有嵌入式开发经验的解析正确源码是什么读者学习驱动开发。
笔者自身情况
我具备硬件基础、源码单片机软硬基础和linux系统基础等,解析但缺乏linux驱动框架基础,源码也未进行过linux系统移植和驱动移植开发。解析因此,源码学习linux系统移植和驱动开发将有助于打通嵌入式整套流程。解析虽然作为技术leader不一定要亲自动手,源码但对产品构架中的解析每一块业务和技术要有基本了解。
推荐
建议参考xun为的源码视频教程,教程过程清晰,适合拥有丰富知识基础的资深研发人员学习。该教程不陷入固有思维误区,也不需要理解imx6的庞杂汇报,直接以实现目标为目的,无需从裸机开始开发学习,所有步骤都解释得清清楚楚。结合多年相关从业经验,确实能够融会贯通。从业多年,首次推荐,因为确实非常好。
驱动
驱动分为四个部分
第一个驱动源码:Hello world!
步骤一:包含头文件
包含宏定义的头文件init.h,包括初始化和宏头文件,如module_init、module_exit等。
#include
包含初始化加载模块的头文件
步骤二:写驱动文件的入口和出口
使用module_init()和module_exit()宏定义入口和出口。
module_init(); module_exit();
步骤三:声明开源信息
告诉内核,本模块驱动有开源许可证。源码内核怎么设置
MODULE_LICENSE("GPL");
步骤四:实现基础功能
入口函数
static int hello_init(void) { printk("Hello, I’m hongPangZi\n"); return 0; }
出口函数
static void hello_exit(void) { printk("bye-bye!!!\n"); }
此时可以修改步骤二的入口出口宏
module_init(hello_init); module_exit(hello_exit);
总结,按照四步法,搭建了基础的驱动代码框架。
Linux驱动编译成模块
将驱动编译成模块,然后加载到内核中。将驱动直接编译到内核中,运行内核则会直接加载驱动。
步骤一:编写makefile
1 生成中间文件的名称
obj-m += helloworld.o
2 内核的路径
内核在哪,实际路径在哪
KDIR:=
3 当前路径
PWD?=$(shell pwd)
4 总的编译命令
all: make -C $(KDIR) M=$(PWD) modules
make进入KDIR路径,当前路径编译成模块。
obj-m = helloworld.o KDIR:= PWD?=$(shell pwd) all: make -C $(KDIR) M=$(PWD) modules
步骤二:编译驱动
编译驱动之前需要注意以下几点:
1 内核源码要编译通过
驱动编译成的目标系统需要与内核源码对应,且内核源码需要编译通过。
2 内核源码版本
开发板或系统运行的内核版本需要与编译内核驱动的内核源码版本一致。
3 编译目标环境
在内核目录下,确认是否为需要的构架:
make menu configure export ARCH=arm
修改构架后,使用menu configure查看标题栏的内核构架。
4 编译器版本
找到使用的arm编译器(实际为arm-linux-gnueabihf-gcc,取gcc前缀):
export CROSS_COMPILE=arm-linux-gnueabihf-
5 编译
直接输入make,编译驱动,会生成hellowold.ko文件,ko文件就是编译好的驱动模块。
步骤三:加载卸载驱动
1 加载驱动
将驱动拷贝到开发板或目标系统,然后使用加载指令:
insmod helloworld.ko
会打印入口加载的printk输出。
2 查看当前加载的驱动
lsmod
可以查看到加载的驱动模块。
3 卸载驱动
rmmod helloworld
可以移除指定驱动模块(PS:卸载驱动不需要.ko后缀),卸载成功会打印之前的printk输出。
总结
学习了驱动的基础框架,为了方便测试,下一篇将使用ubuntu.编译驱动,并做好本篇文章的相关实战测试。
主次设备号动态分配主设备号
在设备管理中,一些主设备号通常是预先静态分配给常见设备的,这些信息可在内核源代码的即时买入指标源码Documentation/device.txt文件中查看。然而,由于大部分号码已被占用,为新设备寻找一个独特的编号变得困难,因为可供选择的设备数量远超主设备号。
幸运的是,主设备号支持动态分配。当使用register_chrdev函数并设置major为0时,它会自动寻找并返回一个未被占用的主设备号。值得注意的是,返回的主设备号总是正数,负数则表示错误。当动态分配主设备号时,register_chrdev的返回值即为分配的号码;而在使用静态指派时,成功注册后函数返回0,而非实际设备号。
对于私有驱动程序,强烈推荐采用动态分配主设备号。然而,如果您的驱动程序需要广泛应用,甚至可能被纳入官方内核,那么可能需要提前分配一个专用的主设备号。
动态分配的不足在于,分配的主设备号可能会变化,导致无法预先创建设备节点(通过mknod命令)。这意味着不能利用"按需加载模块"的高级功能。对于普通用途的驱动程序,这通常不是问题,因为设备号分配后,可以从/proc/devices获取信息。
加载使用动态分配主设备号的驱动程序时,需要编写一个脚本,首先调用insmod,然后从/proc/devices获取主设备号,vwap指标源码查询并创建相应的设备文件,以此来确保驱动程序的正确加载。
linux设备驱动程序——i2c总线的添加与实现
一文看懂linux内核详解
深入了解使用linux查看磁盘io使用情况
在实际驱动开发过程中,i2c总线也是集成在系统中的,驱动开发者不需要关心i2c总线的初始化,只需要将相应的i2c_client和i2c_driver挂载在总线上进行匹配即可。
那么,i2c总线在系统中是如何初始化得来的呢?
答案就在文件i2c-core-base.c中,它的过程是这样的:
在i2c_init函数中,使用bus_register()将i2c总线注册到系统中,那么这个i2c_init()函数是在哪里被调用的呢?
在内核启动的过程中,进入C语言环境的第一个入口函数是start_kernel(),但是i2c_init()并非由start_kernel()间接调用,而是借助于linux内核中的initcall机制被调用,简而言之,就是将这个函数地址在编译阶段放入特定的段中,在内核初始化时由某个启动函数将这些段中的函数一一取出并执行。
i2c总线通过postcore_initcall()将init函数注册到系统中。
当模块被加载进系统时,就会执行i2c_init函数来进行初始化。
在i2c_bus_type结构体中,定义了match(),probe(),remove()和shutdown()函数,match()函数就是当有新的i2c_client或者i2c_driver添加进来时,试图寻找与新设备匹配的项,返回匹配的结果。
remove()和shutdown(),顾名思义,这两个函数是管理的驱动移除行为的。
对于probe函数,在之前的章节中提到:当相应的device和driver经由总线匹配上时,会调用driver部分提供的probe函数。
那么:
带着这两个疑问,我们接着往下看。源码工具哪个好用
在这里我们不免要回顾一下前面章节所说的,作为一个驱动开发者,如果我们要开发某些基于i2c的设备驱动,需要实现的框架流程是怎样的:
但是问题是,为什么device和driver都注册进去之后,就会调用driver部分提供的probe函数呢?
为了解决这些问题,最好的办法就是看源代码,假设我们是以设备树的方式进行匹配,device(即i2c_client)部分已经被注册到系统中,此时我们向系统中注册(insmod由driver部分程序编译的模块)相应的driver部分,接下来我们跟踪driver部分注册到i2c总线的流程,看看它是怎么实现的:
跟踪i2c_add_driver:
经过一系列的代码跟踪,找到了bus_add_driver(),根据名称可以知道,这个函数就是将当前i2c_driver添加到i2c_bus_type(即i2c总线)中。
接着往下看:
从第一部分可以看到,将当前drv链入到bus->p->klist_drivers链表中,那么可以猜到,在注册device部分的时候,就会将device链入到bus->p->klist_devices中。
然后,再看driver_attach(drv):
driver_attach调用bus_for_each_dev,传入当前驱动bus(即i2c_bus_type),当前驱动drv,以及一个函数__driver_attach。
bus_for_each_dev对每个device部分调用__driver_attach。
在__driver_attach中,对每个drv和dev调用driver_match_device(),并根据函数返回值决定是否继续执行调用driver_probe_device()。
从函数名来看,这两个函数大概就是我们在前文中提到的match和probe函数了,我们不妨再跟踪看看,先看看driver_match_device()函数:
果然不出所料,这个函数十分简单,如果当前驱动的所属的bus有相应的match函数,就调用match函数,否则返回1.
当前driver所属的总线为i2c_bus_type,根据上文i2c总线的初始化部分可以知道,i2c总线在初始化时提供了相应的match函数,所以,总线的match函数被调用,并返回匹配的结果。
接下来我们再看看driver_probe_device()函数:
在driver_probe_device中又调用了really_probe,在really_probe()中,先判断当前bus总线中是否注册probe()函数如果有注册,就调用总线probe函数,否则,就调用当前drv注册的probe()函数。
到这里,我们解决了上一节中的一个疑问:总线和driver部分都有probe函数时,程序是怎么处理的?
答案已经显而易见了:优先调用总线probe函数。
而且我们理清了总线的match()函数和probe()函数是如何被调用的。
那么,上一节中还有一个疑问没有解决:总线的match和probe函数执行了一些什么行为?
对于总线probe函数,获取匹配成功的device,再由device获取driver,优先调用driver->probe_new,因为driver中没有设置,直接调用driver的probe函数。
总线probe和driver的probe函数的关系就是:当match返回成功时,优先调用总线probe,总线probe完成一系列的初始化,再调用driver的probe函数,如果总线probe函数不存在,就直接调用driver的probe函数,所以当我们在系统中添加了相应的device和driver部分之后,driver的probe函数就会被调用。
对于总线match函数,我们直接查看在i2c总线初始化时的函数定义:
i2c_device_match就是i2c_driver与i2c_device匹配的部分,在i2c_device_match函数中,可以看到,match函数并不只是提供一种匹配方式:
接下来再深入函数内部,查看匹配的细节部分:
在i2c_of_match_device中,调用了of_match_device()和i2c_of_match_device_sysfs()两个函数,这两个函数代表了两种匹配方式,先来看看of_match_device:
of_match_device调用of_match_node。
of_match_node调用__of_match_node函数。
如果你对设备树有一定的了解,就知道系统在初始化时会将所有设备树子节点转换成由struct device_node描述的节点。
在被调用的__of_match_node函数中,对device_node中的compatible属性和driver部分的of_match_table中的compatible属性进行匹配,由于compatible属性可以设置多个,所以程序中会对其进行逐一匹配。
我们再回头来看设备树匹配方式中的i2c_of_match_device_sysfs()匹配方式:
由i2c_of_match_device_sysfs()的实现可以看出:当设备树中不存在设备节点时,driver部分的of_match_table中的compatible属性试图去匹配i2c_client(device部分)的.driver.name属性.
因为设备树的默认规则,compatible属性一般形式为"vender_id,product_id",当compatible全字符串匹配不成功时,取product_id部分再进行匹配,这一部分主要是兼容之前的不支持设备树的版本。
acpi匹配方式较为少用且较为复杂,这里暂且不做详细讨论
id_table匹配方式中,这种匹配方式一目了然,就是对id_table(driver部分)中的.name属性和i2c_client(device部分)的.name属性进行匹配。那么i2c_client的.name是怎么来的呢?
在非设备树的匹配方式中,i2c_client的.name属性由驱动开发者指定,而在设备树中,i2c_client由系统对设备树进行解析而来,i2c_client的name属性为设备树compatible属性"vender_id,product_id"中的"product_id",所以,在进行匹配时,默认情况下并不会严格地要求 of_match_table中的compatible属性和设备树中compatible属性完全匹配,driver中.drv.name属性和.id.name属性也可以与设备树中的"product_id"进行匹配。
关于linux i2c总线的初始化以及运行机制的讨论就到此为止啦
Linux驱动(驱动程序开发、驱动框架代码编译和测试)
驱动就是对底层硬件设备的操作进行封装,并向上层提供函数接口。
Linux系统将设备分为3类:字符设备、块设备、网络设备。
先看一张图,图中描述了流程,有助了解驱动。
用户态:
内核态:
驱动链表:管理所有设备的驱动,添加或查找, 添加是发生在我们编写完驱动程序,加载到内核。查找是在调用驱动程序,由应用层用户空间去查找使用open函数。驱动插入链表的顺序由设备号检索。
字符设备驱动工作原理:
在Linux的世界里一切皆文件,所有的硬件设备操作到应用层都会被抽象成文件的操作。当应用层要访问硬件设备,它必定要调用到硬件对应的驱动程序。Linux内核有那么多驱动程序,应用怎么才能精确的调用到底层的驱动程序呢?
当open函数打开设备文件时,可以根据设备文件对应的struct inode结构体描述的信息,可以知道接下来要操作的设备类型(字符设备还是块设备),还会分配一个struct file结构体。
根据struct inode结构体里面记录的设备号,可以找到对应的驱动程序。在Linux操作系统中每个字符设备都有一个struct cdev结构体。此结构体描述了字符设备所有信息,其中最重要的一项就是字符设备的操作函数接口。
找到struct cdev结构体后,linux内核就会将struct cdev结构体所在的内存空间首地址记录在struct inode结构体i_cdev成员中,将struct cdev结构体中的记录的函数操作接口地址记录在struct file结构体的f_ops成员中。
任务完成,VFS层会给应用返回一个文件描述符(fd)。这个fd是和struct file结构体对应的。接下来上层应用程序就可以通过fd找到struct file,然后在struct file找到操作字符设备的函数接口file_operation了。
其中,cdev_init和cdev_add在驱动程序的入口函数中就已经被调用,分别完成字符设备与file_operation函数操作接口的绑定,和将字符驱动注册到内核的工作。
驱动程序开发步骤:
Linux 内核就是由各种驱动组成的,内核源码中有大约 %是各种驱动程序的代码。内核中驱动程序种类齐全,可以在同类驱动的基础上进行修改以符合具体单板。
编写驱动程序的难点并不是硬件的具体操作,而是弄清楚现有驱动程序的框架,在这个框架中加入这个硬件。
一般来说,编写一个 linux 设备驱动程序的大致流程如下:
下面以一个简单的字符设备驱动框架代码来进行驱动程序的开发、编译等。
基于驱动框架的代码开发:
上层调用代码
驱动框架代码
驱动开发的重点难点在于读懂框架代码,在里面进行设备的添加和修改。
驱动框架设计流程:
1. 确定主设备号
2. 定义结构体 类型 file_operations
3. 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
4. 实现驱动入口:安装驱动程序时,就会去调用这个入口函数,执行工作:
① 把 file_operations 结构体告诉内核:注册驱动程序register_chrdev.
② 创建类class_create.
③ 创建设备device_create.
5. 实现出口:卸载驱动程序时,就会去调用这个出口函数,执行工作:
① 把 file_operations 结构体从内核注销:unregister_chrdev.
② 销毁类class_create.
③ 销毁设备结点device_destroy.
6. 其他完善:GPL协议、入口加载
驱动模块代码编译和测试:
编译阶段:
驱动模块代码编译(模块的编译需要配置过的内核源码,编译、连接后生成的内核模块后缀为.ko,编译过程首先会到内核源码目录下,读取顶层的Makefile文件,然后再返回模块源码所在目录。)
将该驱动代码拷贝到 linux-rpi-4..y/drivers/char 目录下 文件中(也可选择设备目录下其它文件)
修改该文件夹下Makefile(驱动代码放到哪个目录,就修改该目录下的Makefile),将上面的代码编译生成模块,文件内容如下图所示:(-y表示编译进内核,-m表示生成驱动模块,CONFIG_表示是根据config生成的),所以只需要将obj-m += pin4drive.o添加到Makefile中即可。
回到linux-rpi-4..y/编译驱动文件
使用指令:ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KERNEL=kernel7 make modules进行编译生成驱动模块。
加载内核驱动:
加载内核驱动(相当于通过insmod调用了module_init这个宏,然后将整个结构体加载到驱动链表中)。 加载完成后就可以在dev下面看到名字为pin4的设备驱动(这个和驱动代码里面static char *module_name="pin4"; //模块名这行代码有关),设备号也和代码里面相关。
lsmod查看系统的驱动模块,执行上层代码,赋予权限
查看内核打印的信息,如下图所示:表示驱动调用成功
在装完驱动后可以使用指令:sudo rmmod +驱动名(不需要写ko)将驱动卸载。
驱动调用流程:
上层空间的open去查找dev下的驱动(文件名),文件名背后包含了驱动的主设备号和次设备号。此时用户open触发一个系统调用,系统调用经过vfs(虚拟文件系统),vfs根据文件名背后的设备号去调用sys_open去判断,找到内核中驱动链表的驱动位置,再去调用驱动里面自己的dev_open函数。
为什么生成驱动模块需要在虚拟机上生成?树莓派不行吗?
生成驱动模块需要编译环境(linux源码并且编译,需要下载和系统版本相同的Linux内核源代码)。也可以在树莓派上面编译,但在树莓派里编译,效率会很低,要非常久。
2024-12-28 23:27
2024-12-28 23:09
2024-12-28 23:02
2024-12-28 21:32
2024-12-28 21:29