【入门级别】linux内核驱动三种写法之——platform分离机制

前言

关于想追本溯源理清楚linux驱动的写法,前面已经写了三篇链接如下
1.【入门级别】linux驱动的三种写法之前言——裸机程序
2.【入门级别】linux驱动三种写法之——软硬件信息未分离(以字符设备驱动程序为例)
3.字符设备驱动的简化版混杂设备驱动

今天我们来学习下linux内核驱动的platform分离机制,关于这部分的理论知识,我在如下这一篇博文中有详细介绍,就不再赘述。

linux platform分离机制是如何运作的呢

今天主要以实例分析下platform分离机制,以及我在学习实践过程中的思考和遇到的问题及其解决办法。

框架图

在这里插入图片描述

实例代码

为了跟前面的文章在使用内核的gpio相关函数操作硬件保持一致,该案例是以自定义结构体描述硬件信息(没有使用内核定义好的struct resource结构体)

硬件信息部分led_dev.c

//头文件
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio.h>
#include <mach/platform.h>
/*自定义描述led灯的硬件信息的数据结构*/
struct led_resource{
    int gpio;
    char* name;
};
//使用自定义结构体描述4个led灯的硬件信息
struct led_resource led_info[] = {
    {PAD_GPIO_C+12, "LED1"},
    {PAD_GPIO_C+7, "LED2"},
    {PAD_GPIO_C+11, "LED3"},
    {PAD_GPIO_B+26, "LED4"}
};
//仅仅为了去除卸载模块时的警告,后面问题部分有具体说明
void led_release(struct device *dev){}

/*定义初始化led灯的硬件节点*/
static struct platform_device led_pdev ={
    .name = "vpled",
    .id = -1,
    .dev = {
        .platform_data = led_info,
        .release = led_release
    }
};

//入口函数
static int led_dev_init(void){
    /*向内核添加led硬件节点*/
    platform_device_register(&led_pdev);
    return 0;
}

//出口函数
static void led_dev_exit(void){
    /*从内核卸载led硬件节点*/
    platform_device_unregister(&led_pdev);
}

//各种模块修饰和GPL规则
module_init(led_dev_init);
module_exit(led_dev_exit);
MODULE_LICENSE("GPL");

软件信息部分led_drv.c

//头文件
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio.h>
#include <linux/miscdevice.h>
#include <linux/uaccess.h>
#include <mach/platform.h>
#include <linux/fs.h>

#define LED_ON 0X10001
#define LED_OFF 0x10002
//声明自定义描述led灯的硬件信息的数据结构
struct led_resource{
    int gpio;
    char* name;
};
// 定义一个全局指针变量
struct led_resource *led_info1;

//根据用户需求实现硬件操作接口函数
static long led_ioctl(struct file* file, unsigned int cmd, unsigned long arg){
    
    //分配内核缓冲区
    int kindex;
    copy_from_user(&kindex, (int *)arg, sizeof(kindex));
    switch(cmd){
        case LED_ON:
            gpio_set_value(led_info1[kindex-1].gpio, 0);
            printk("%s:开第%d个灯\n",__func__, kindex);
            break;
        case LED_OFF:
            gpio_set_value(led_info1[kindex-1].gpio, 1);
            printk("%s:关第%d个灯\n",__func__, kindex);
            break;
        default:
            printk("无效命令!\n");
            return -1;
    }
    return 0;
}

//定义led灯硬件的操作接口
struct file_operations led_fops = {
    .unlocked_ioctl = led_ioctl,
    .owner = THIS_MODULE
};

//定义初始化led的混杂设备对象
struct miscdevice led_misc = {
    .fops = &led_fops,
    .name = "vaccine_misc",
    .minor = MISC_DYNAMIC_MINOR
};

//led_probe函数,
int led_probe(struct platform_device* pdev){
    /*通过形参led_pdev指针获取匹配成功的硬件信息*/
    /*处理获取的纯硬件信息,(应该有内核处理)*/
    led_info1 = pdev->dev.platform_data;
    int i;
    for(i = 0; i < 4; i++){
        gpio_request(led_info1[i].gpio, led_info1[i].name);
        gpio_direction_output(led_info1[i].gpio, 1);
    }
    //向内核注册混杂设备对象
    misc_register(&led_misc);
    printk("%s\n",__func__);
    return 0;
}

//led_remove函数
int led_remove(struct platform_device* pdev){
    //从内核卸载混杂设备对象
    misc_deregister(&led_misc);
    /*也许还有其他的操作*/
    led_info1 = pdev->dev.platform_data;
    int i;
    for(i = 0; i < 4; i++){
        gpio_set_value(led_info1[i].gpio, 1);
        gpio_free(led_info1[i].gpio);
    }
    printk("%s\n",__func__);
    return 0;
}

//定义初始化led的platform_driver对象
struct platform_driver led_pdrv = {
    .probe = led_probe,
    .remove = led_remove,
    .driver.name = "vpled"
};

//入口函数
int led_drv_init(void){
    //向内核drv链表添加软件节点
    platform_driver_register(&led_pdrv);
    return 0;
}
//出口函数
void led_drv_exit(void){
    //从内核drv链表删除软件节点
    platform_driver_unregister(&led_pdrv);
}

//各种修饰和GPL规则
module_init(led_drv_init);
module_exit(led_drv_exit);
MODULE_LICENSE("GPL");

然后编写应用程序,在下位机安装模块后,成功操作如下

/tmp/vaccine # ls
led_dev.ko  led_drv.ko  led_test
/tmp/vaccine # insmod led_dev.ko
/tmp/vaccine # insmod led_drv.ko
[   36.171000] led_probe
/tmp/vaccine # ./led_test on 2
[   60.121000] led_ioctl:开第2个灯
/tmp/vaccine # ./led_test on 3
[   63.553000] led_ioctl:开第3个灯
/tmp/vaccine # ./led_test on 4
[   66.870000] led_ioctl:开第4个灯
/tmp/vaccine # ./led_test off 4
[   71.845000] led_ioctl:关第4个灯
/tmp/vaccine # ./led_test off 3
[   76.462000] led_ioctl:关第3个灯
/tmp/vaccine # ./led_test off 2
[   80.019000] led_ioctl:关第2个灯
/tmp/vaccine # rmmod led_drv.ko
[   91.974000] led_remove
/tmp/vaccine # rmmod led_dev.ko
/tmp/vaccine # 

可以仔细对比框架图和实例代码,多练几次,就会掌握该框架。

遇到问题

  • 1.因为没有初始化硬件信息部分led_dev.c中led_pdev.dev.release,导致在卸载模块时候,内核报出警告如下
/tmp/vaccine # insmod led_drv.ko
/tmp/vaccine # insmod led_dev.ko
[  369.641000] led_probe
/tmp/vaccine # rmmod led_dev.ko
[  379.348000] led_remove
[  379.350000] ------------[ cut here ]------------
[  379.351000] WARNING: at drivers/base/core.c:196 device_release+0x78/0x84()
[  379.352000] Device 'platform_led' does not have a release() function, it is broken and must be fixed.
[  379.354000] Modules linked in: led_dev(O-) led_drv(O) [last unloaded: led_dev]
[  379.357000] [<c0014e80>] (unwind_backtrace+0x0/0x138) from [<c0042814>] (warn_slowpath_common+0x4c/0x64)
[  379.362000] [<c0042814>] (warn_slowpath_common+0x4c/0x64) from [<c00428c0>] (warn_slowpath_fmt+0x30/0x40)
[  379.363000] [<c00428c0>] (warn_slowpath_fmt+0x30/0x40) from [<c02ca144>] (device_release+0x78/0x84)
[  379.364000] [<c02ca144>] (device_release+0x78/0x84) from [<c027a374>] (kobject_release+0x90/0x1b4)
[  379.365000] [<c027a374>] (kobject_release+0x90/0x1b4) from [<c0092cbc>] (sys_delete_module+0x168/0x2a8)
[  379.366000] [<c0092cbc>] (sys_delete_module+0x168/0x2a8) from [<c000ec80>] (ret_fast_syscall+0x0/0x30)
[  379.367000] ---[ end trace f48197ea5bf2619b ]---

这个时候,我们仅仅为了去除内核的警告,给初始化一个空的led_release()函数,见硬件信息部分led_dev.c代码。

  • 2.上位机中的启动串口工具kermit时候报错如下 ?SET SPEED has no effect without prior SET LINE
    -l or -j or -X required
vaccine@vaccine-ThinkPad-X201:~/Vaccine_200614/led_drv/platform_led/1.0_gpio_my$ sudo kermit -c
?SET SPEED has no effect without prior SET LINE
-l or -j or -X required
vaccine@vaccine-ThinkPad-X201:~/Vaccine_200614/led_drv/platform_led/1.0_gpio_my$ ls /dev/ttyUSB* -lh
crw-rw---- 1 root dialout 188, 0 626 09:16 /dev/ttyUSB0
vaccine@vaccine-ThinkPad-X201:~/Vaccine_200614/led_drv/platform_led/1.0_gpio_my$ cat ~/.kermrc
set line /dev/ttyUSB1
set speed 115200
set carrier-watch off
set handshake none
set flow-control none
robust
set file type bin
set file name lit
set rec pack 1000
set send pack 1000
set window 5

解决办法:将 kermit的配置文件~/.kermrc中的set line /dev/ttyUSB1 -> set line /dev/ttyUSB0,保存就好了,疑问:为什么串口号ttyUSB0和ttyUSB1会经常的变来变去呢?有没有什么设置可以固定下呢

3.安装驱动模块时,报错[ 36.833000] led_drv: Unknown symbol led_info1 (err 0)
insmod: can’t insert ‘led_drv.ko’: unknown symbol in module, or unknown parameter
详情见bash如下显示

tmp/vaccine # insmod led_dev.ko
/tmp/vaccine # insmod led_drv.ko
[   36.833000] led_drv: Unknown symbol led_info1 (err 0)
insmod: can't insert 'led_drv.ko': unknown symbol in module, or unknown parameter
/tmp/vaccine # cat /proc/kallsyms |grep "led_info1"
/tmp/vaccine # cat /proc/kallsyms |grep "led_info1"
/tmp/vaccine # 

然后,我更改了下代码,这个错误就消失了,但是我并没有明白具体机制,只是直觉上觉得led_drv.c的代码中的led_info1指针变量写法有问题,下面是前后代码关于led_info1的指针变量的更改对比。
在这里插入图片描述
在这里插入图片描述
OLD代码中的思路是这样的:既然led_probe函数和led_remove函数中要使用pdev指针变量来获取LED灯的硬件信息,于是在该函数中定义一个描述LED灯硬件信息的结构体指针struct led_resource *led_info1,以为了获取led_dev.c中定义初始化的LED的硬件信息。

然而,在给用户提供的硬件操作接口led_ioctl()函数中,恰好需要使用到获取到的LED的硬件信息,于是想着使用extern声明下led_info1指针变量,就可以了。上位机编译的过程没有问题,但是在下位机安装模块时,报出如上错误。

网上搜索一番,使用cat /proc/kallsyms | grep “led_info1” 在没有出现相关打印,说明内核中,没有包含led_info1。难道是编译没有编译进去?但是使用cat /proc/kallsyms | grep “led_info” ,却能够出现相关打印如下,说明内核中已经包含led_info。

/tmp/vaccine # ls
led_dev.ko  led_drv.ko  led_test
/tmp/vaccine # insmod led_dev.ko
/tmp/vaccine # insmod led_drv.ko
[   39.167000] led_drv: Unknown symbol led_info1 (err 0)
insmod: can't insert 'led_drv.ko': unknown symbol in module, or unknown parameter
/tmp/vaccine # cat /proc/kallsyms |grep "led_info"
bf000268 d led_info	[led_dev]
/tmp/vaccine # 

然后我就把struct led_resource *led_info1定义为一个全局变量,这样就可以在不同的函数中被使用了。参见上面对比截图中的NEW一侧。结果就ok了,问题解决了,使用cat /proc/kallsyms | grep “led_info” 也能找到led_info1。但是背后的具体原因原理还没有搞明白

/tmp/vaccine # ls
led_dev.ko  led_drv.ko  led_test
/tmp/vaccine # insmod led_dev.ko
/tmp/vaccine # insmod led_drv.ko
[   84.375000] led_probe
/tmp/vaccine # cat /proc/kallsyms |grep "led_info"
bf004528 b led_info1	[led_drv]
bf000268 d led_info	[led_dev]
/tmp/vaccine # 

补充linux内核符号表kallsyms

转载于旅途@KryptosX » linux内核符号表kallsyms简介
简介:在2.6版的内核中,为了更方便的调试内核代码,开发者考虑将内核代码中所有函数以及所有非栈变量的地址抽取出来,形成是一个简单的数据块(data blob:符号和地址对应),并将此链接进 vmlinux 中去。

在需要的时候,内核就可以将符号地址信息以及符号名称都显示出来,方便开发者对内核代码的调试。完成这一地址抽取+数据快组织封装功能的相关子系统就称之为 kallsyms。

反之,如果没有 kallsyms 的帮助,内核只能将十六进制的符号地址呈现给外界,因为它能理解的只有符号地址,并不能显示各种函数名等符号。

kallsyms抽取了内核用到的所有函数地址(全局的、静态的)和非栈数据变量地址,生成一个数据块,作为只读数据链接进kernel image,相当于内核中存了一个System.map

开启kallsyms

要在一个内核中启用 kallsyms 功能。须设置 CONFIG_KALLSYMS 选项为y;如果要在 kallsyms 中包含全部符号信息,须设置CONFIG_KALLSYMS_ALL 为y
查看kallsyms表:less /proc/kallsyms
得益于/proc文件系统,我们可以直接读取这个表(按q退出)

..................
c000a608 t vfp_single_fcmpez
c000a614 t vfp_single_fcmpz
c000a620 t vfp_single_fcmpe
c000a628 t vfp_single_fcmp
c000a630 T __vfp_single_normaliseround
c000a834 t vfp_single_fdiv
c000ab70 t vfp_single_fnmul
c000acb8 t vfp_single_multiply_accumulate.isra.2
c000ae90 t vfp_single_fnmsc
c000aeac t vfp_single_fnmac
c000aec8 t vfp_single_fmsc
c000aee4 t vfp_single_fmac
c000af00 t vfp_single_fadd
c000b034 t vfp_single_fsub
c000b03c t vfp_single_fmul
...........

这个应该可以很容易看出,第一列为符号地址,第二列为类型,第三列为符号名。

注意:如果发现符号地址均为0,那是因为系统保护。使用root权限查看即可。

第二列的类型:

有的符号是大写的,有的是小写。大写的符号是全局的。
b 符号在未初始化数据区(BSS)
c 普通符号,是未初始化区域
d 符号在初始化数据区
g 符号针对小object,在初始化数据区
i 非直接引用其他符号的符号
n 调试符号
r 符号在只读数据区
s 符号针对小object,在未初始化数据区
t 符号在代码段
u 符号未定义

猜你喜欢

转载自blog.csdn.net/weixin_43326587/article/details/106967878
今日推荐