• Linux SPI驱动学习——注册匹配


    @

    博客说明

    撰写日期 2019.10.22
    完稿日期 2019.10.23
    最近维护 暂无
    本文作者 multimicro
    联系方式 multimicro@qq.com
    资料链接 本文无附件资料
    GitHub https://github.com/wifialan/drivers/
    原文链接 https://blog.csdn.net/multimicro/article/details/102685871

    开发环境

    环境说明 详细信息 备注信息
    操作系统 Ubunut 18.04
    开发板 JZ2440-V3
    Linux内核 linux-3.4.2

    1. Linux SPI概述

    鄙人通过查看宋宝华《Linux设备驱动开发详解–基于最新的Linux 4.0内核》 第12章:Linux设备驱动的软件架构思想,初步了解了总线设备驱动这三个名词:
    总线:比如4线SPI的总线是四条线,这四条线就构成了SPI总线,但不知道这样解释合不合适,保留疑问
    设备:对应的是spi_device——外设设备的抽象
    驱动:对应的是spi_drivce——外设端驱动
    以上解释暂保留疑问。

    先知道有这三个名词吧。

    下面的内容只是对SPI驱动的初步实现进行感性的认识,先实现,后谈理论分析。

    1.1 SPI驱动框架

    如下图所示
    在这里插入图片描述
    设备驱动(外设端驱动)抽象出来一个spi_driver,用外设模块所规定的传输协议收发数据,具体实现就是调用主机端的spi收发函数进行排列组合实现外设协议所规定的波形。
    控制器驱动(主机端驱动)抽象出来一个spi_master,用于产生总线上的波形。比如调用spi_transfer函数发送一个16位的数据,那么在总线上就会生成一个16位的SPI波形,主机端只产生波形不干别的。

    2. SPI 注册匹配

    2.1 spi_drive注册

    再看韦东山SPI视频时,他说参考内核中的其他代码进行编写,如sound/soc/codecs/ad1936.c文件中第374-388c行:

    static struct spi_driver ad1836_spi_driver = {
    	.driver = {
    		.name	= "ad1836",
    		.owner	= THIS_MODULE,
    	},
    	.probe		= ad1836_spi_probe,
    	.remove		= __devexit_p(ad1836_spi_remove),
    	.id_table	= ad1836_ids,
    };
    
    static int __init ad1836_init(void)
    {
    	return spi_register_driver(&ad1836_spi_driver);
    }
    module_init(ad1836_init);
    

    Tips:在source inside中采用快捷键ctrl + ?调出Lookup References框框,然后输如spi_driver,在生成的搜索结果里面第一项展开即可直接定位至文件中的spi_driver所在行。
    在这里插入图片描述


    注册spi_driver的步骤为:

    Step 1:
    我仿照编写的spi_driver程序为如下:
    路径:drivers/char/w25q16_spi.c

    static struct spi_driver w25q16_spi_driver =
    {
        .driver     =
        {
            .name   = "w25q16",	/* spi_driver注册成功后,会在/sys/bus/spi/drivers/目录下面显示出该name字段的名字,见下图 */
            .owner  = THIS_MODULE,
        },
        .probe      = w25q16_bus_spi_probe,
        .remove     = __devexit_p(w25q16_bus_spi_remove),
    };
    
    module_init(w25q16_driver_init);
    

    在这里插入图片描述
    该程序所在文件的位置为:drives/char/w25q16_spi.c
    我把这个flash定为字符驱动进行编写了,所以该文件在char这个文件夹里面。
    按照驱动在内核模块中的加载方式,还需要同步修改KconfigMakefile
    Step 2:
    Kconfig中增添信息
    在这里插入图片描述
    Step 3:
    Makefile中增添信息
    ![在这里插入图片描述#pic_center)
    menuconfig菜单中勾选此选项即可,另外,为了开启SPI支持,需要在menuconfig菜单中同步开启如下选项:
    配置内核使用主控驱动 spi-s3c24xx.c

     -> General setup
         [*] Prompt for development and/or incomplete code/drivers

     -> Device Drivers
         -> SPI support
              <*> Samsung S3C24XX series SPI

    2.2 spi_device注册

    spi_device 的注册可以由系统完成,具体是通过内核中spi_match_master_to_boardinfo函数(在spi_register_board_info函数中调用),board_info里含有bus_num, 如果某个spi_master的bus_num跟它一样,则创建一个新的spi_device,代码如下:
    路径:drivers/spi/spi.c

    static void spi_match_master_to_boardinfo(struct spi_master *master,
    				struct spi_board_info *bi)
    {
    	struct spi_device *dev;
    
    	if (master->bus_num != bi->bus_num)
    		return;
    	dev = spi_new_device(master, bi);
    	if (!dev)
    		dev_err(master->dev.parent, "can't create new device for %s
    ",
    			bi->modalias);
    }
    

    可以看到,如果master->bus_num == bi->bus_num时,才会执行spi_new_device函数创建spi_device
    s3c2440有两个spi控制器,那么bus_num就有两个值:0和1,分别对应SPI0和SPI1。

    上述函数中传递的第二个参数是spi_board_info结构体,那么我们就需要构造一个这样的结构体,这个结构体怎么构造呢?首先就要追溯到这个函数的上层函数spi_register_board_info中去,在source inside中按照上面讲的方法搜索该函数,则可以找出很多例子,下面是我仿照其他文件中的方式构造的:

    只有下面这个程序是本节要单独编写的代码

    路径:driver/spi/spi_info_jz2440.c

    #include <linux/module.h>
    #include <linux/device.h>
    #include <linux/platform_device.h>
    #include <linux/spi/spi.h>
    #include <linux/gpio.h>
    #include <mach/regs-gpio.h>
    #include <plat/gpio-cfg.h>
    
    static struct spi_board_info spi_info_jz2440[] = {
    	{
        	 .modalias = "oled",  
        	 .max_speed_hz = 10000000,	
        	 .bus_num = 1,     
        	 .mode    = SPI_MODE_0,
        	 .chip_select   = S3C2410_GPF(1), 
        	 //.platform_data = (const void *)S3C2410_GPG(4) ,
    	 },
    	 {
        	 .modalias = "w25q16",  
        	 .max_speed_hz = 80000000,	/* max spi clock (SCK) speed in HZ */
        	 .bus_num = 0,
        	 .mode    = SPI_MODE_0,
        	 .chip_select   = S3C2410_GPG(2), 
    	 }
    };
    
    static int spi_info_jz2440_init(void)
    {
        printk("spi_info_jz2440_init function..
    ");
        return spi_register_board_info(spi_info_jz2440, ARRAY_SIZE(spi_info_jz2440));
    }
    
    module_init(spi_info_jz2440_init);
    

    可以看到spi_board_info结构体中包含两项(也可以只构造一项),每项都包含名字,最大时钟频率,总线编号,模式和片选等信息。

    从名字可以看出,这个结构体主要和外设模块信息有关,它只规定这个外设模块使用多高的SPI时钟频率,接到那个SPI控制器上,片选用那个引脚,采用什么模式等。其实就是把外设模块的信息汇总抽象生成一个结构体,通过调用该结构体,来注册符合实际外设SPI模块的spi_device


    参考 宋宝华《Linux设备驱动开发详解–基于最新的Linux 4.0内核》 12.4.1节 P322中所述:

      4) 板级逻辑。板级逻辑用来描述主机主机和外设是如何互联的,它相当于一个“路由表”。假设板子上由多个SPI控制器和多个SPI外设,那究竟谁接在谁上面?管理互联关系,既不是主机端的责任,也不是外设端的责任,这属于板级逻辑的责任。这部分通常出现在 arch/arm/mach-xxx 下面或者 arch/arm/boot/dts 下面。


    下面看一下spi_register_board_info函数:
    路径:drivers/spi/spi.c

    int __devinit
    spi_register_board_info(struct spi_board_info const *info, unsigned n)
    {
    	struct boardinfo *bi;
    	int i;
    
    	bi = kzalloc(n * sizeof(*bi), GFP_KERNEL);
    	if (!bi)
    		return -ENOMEM;
    
    	for (i = 0; i < n; i++, bi++, info++) {
    		struct spi_master *master;
    
    		memcpy(&bi->board_info, info, sizeof(*info));		 //把info结构体的内容复制到 bi->board_info 里面
    		mutex_lock(&board_lock);
    		list_add_tail(&bi->list, &board_list);
    		list_for_each_entry(master, &spi_master_list, list)
    			spi_match_master_to_boardinfo(master, &bi->board_info);		//这个函数就是上面2.2节的贴出的第一个函数,现在调到这个函数的实体中去,在看一下
    		mutex_unlock(&board_lock);
    	}
    
    	return 0;
    }
    

    分析玩上面函数以及注释后,可以大概得出这样一个流程:
    如果spi_board_info结构体里面的bus_numspi_master里面的bus_num相等的话,在spi_match_master_to_boardinfo函数中调用spi_new_device创建一个spi_device
    在这里插入图片描述
    /sys/bus/spi/devices/文件夹里面可以看到spi_device的注册信息:
    在这里插入图片描述
    这个spi0.194spi1.161的命名在spi_add_device函数里面:
    路径:drivers/spi/spi.c第357-358行

    dev_set_name(&spi->dev, "%s.%u", dev_name(&spi->master->dev),
    			spi->chip_select);
    

    可以看出,后面的数字是和chip_select有关,这里的chip_select是在spi_board_info里面定义的,通过%u的格式将其打印输出。回过头看spi_board_info结构体里面的chip_select

    static struct spi_board_info spi_info_jz2440[] = {
    	{
    		... ...
    		 .bus_num		 = 1;
        	 .chip_select   = S3C2410_GPF(1), 		//%u 输出是 161
    	 },
    	 {
     		... ...
     		 .bus_num		 = 0;
        	 .chip_select   = S3C2410_GPG(2), 		//%u 输出是 194
    	 }
    };
    

    就明白后面的数字是怎么回事了。

    spi_board_info结构体里面的chip_select变量就是获取的该SPI控制器所调用的片选IO引脚
    .chip_select = S3C2410_GPF(1)表示该SPI控制器选用GPF1作为CS引脚
    .chip_select = S3C2410_GPG(2)表示该SPI控制器选用GPG2作为CS引脚
    之前认为能作为SPI控制器的CS信号引脚的,一定是芯片级支持的,不是随便找一个IO的,但是实际测试发现,S3C2440这个板子可以使用任意一个引脚作为CS片选引脚,对于其他板子,不知道可不可以。

    下面给出流程:
    在这里插入图片描述
    注:spi_register_board_info函数不能被编为模块,否则会出现

    WARNING: "spi_register_board_info" [drivers/spi/spi_info_jz2440.ko] undefined!

    原因就是内核没有将此函数导出来,导致该函数不可被外部程序所调用。


    拓展:
    为了能让函数在其他模块中使用,内核采用了以下方式修饰函数,这样即可将修饰后的函数供模块外使用。
    EXPORT_SYMBOL(符号名);
    EXPORT_SYMBOL_GPL(符号名)

    在内核文件driver/spi/spi.c中使用了大量的EXPORT_SYMBOL_GPL(spi_new_device)使得修饰后的函数供模块外程序调用。

    参考资料:linux模块导出符号 EXPORT_SYMBOL_GPL EXPORT_SYMBOL


    2.3 SPI的device和driver匹配

    device 和 driver 在内核中分别注册后,若其下的name相同,则会调用 xxx_driver中的probe函数进行配对,使device和driver绑定在同一条总线上面

    • 首先看以下spi_driver下的name字段
      路径:drivers/char/w25q16_spi.c
    static struct spi_driver w25q16_spi_driver =
    {
        .driver     =
        {
            .name   = "w25q16",
            .owner  = THIS_MODULE,
        },
        .probe      = w25q16_bus_spi_probe,
        .remove     = __devexit_p(w25q16_bus_spi_remove),
    };
    
    • 在看以下spi_deivce下的name字段(由spi_board_info结构体提供)
      路径:driver/spi/spi_info_jz2440.c
    static struct spi_board_info spi_info_jz2440[] = {
    	{
        	 .modalias = "oled",  
        	 .max_speed_hz = 10000000,	
        	 .bus_num = 1,     
        	 .mode    = SPI_MODE_0,
        	 .chip_select   = S3C2410_GPF(1), 
        	 //.platform_data = (const void *)S3C2410_GPG(4) ,
    	 },
    	 {
        	 .modalias = "w25q16",  
        	 .max_speed_hz = 80000000,	/* max spi clock (SCK) speed in HZ */
        	 .bus_num = 0,
        	 .mode    = SPI_MODE_0,
        	 .chip_select   = S3C2410_GPG(2), 
    	 }
    };
    

    两者name字段都是"w25q16"

    故在driver和device在内核注册后可自动调用spi_driverprobe函数,其实体为w25q16_bus_spi_probe
    路径:drivers/char/w25q16_spi.c

    struct spi_device *spi_w25q16_pdev;
    
    static int __devinit w25q16_bus_spi_probe(struct spi_device *spi)
    { 
        int ret,err;
        dev_t devid;
        spi_w25q16_pdev = spi;
        s3c2410_gpio_cfgpin(spi->chip_select, S3C2410_GPIO_OUTPUT);
    
        if(major) {
           devid = MKDEV(major, 0);
           ret = register_chrdev_region(devid, 1, DRV_NAME);
           printk(DRV_NAME "	Origin Creat node %d
    ",major);
        } else {
            ret = alloc_chrdev_region(&devid, 0, 1, DRV_NAME);
            major = MAJOR(devid);
            printk(DRV_NAME "	Arrage Creat node %d
    ",major);
        }
        if(ret < 0) {
            printk(DRV_NAME "	new device failed
    ");
            //goto fail_malloc;
            return ret;
        }
        
        w25q16_pdev = kzalloc(sizeof(struct w25q16_dev_t), GFP_KERNEL);
        if(!w25q16_pdev) {
           ret = -ENOMEM;
           goto fail_malloc;
        }
        cdev_init(&w25q16_pdev->cdev, &w25q16_ops);
        err = cdev_add(&w25q16_pdev->cdev, devid, 1);
        if(err)
            printk(DRV_NAME "	Error %d adding w25q16 %d
    ",err, 1);
    
        class = class_create(THIS_MODULE, "w25q16");
        device_create(class, NULL, MKDEV(major, minor), NULL, "w25q16");
        printk(DRV_NAME "	creat device node /dev/w25q16 
    ");
    
        return 0;
    
    fail_malloc:
        printk("Failed to allocate memory!
    ");
        return ret;
    
    }
    

    spi_device和spi_driver匹配成功后,在probe函数内实现字符驱动的注册:
    在这里插入图片描述
    spi_driver的注册程序:

    static int __init w25q16_driver_init(void)
    {
        int ret;
        printk("
    ************ driver init begin ************
    
    ");
        ret = spi_register_driver(&w25q16_spi_driver);
        if(ret)
        {
            spi_unregister_driver(&w25q16_spi_driver);
            printk(DRV_NAME "	Failed register spi driver. Error: %d
    ",ret);
        }
        printk("
    ************* driver init end *************
    
    ");
        return ret;
    }
    

    可以看出,一旦注册完spi_driver,那么就会自动寻找同名的spi_device,匹配完成后,则会自动执行probe函数。

    至此,完成了spi_device和spi_driver的匹配注册。总体流程如下图:
    在这里插入图片描述

    附录:

    1. spi_driver程序:https://github.com/wifialan/drivers/blob/master/w25q16_spi.c
    2. spi_device程序:https://github.com/wifialan/drivers/blob/master/spi_info_jz2440.c
  • 相关阅读:
    模块化利器:RequireJS常用知识
    移动端适配:font-size设置的思考
    样式化复选框
    jquery tmpl 详解
    移动前端相关解决方案整理
    常用页面布局方式介绍
    移动端制作的常见问题及解决方法
    手机端页面自适应:rem布局
    React工程化之PWA之serviceWorker
    React之JSX循环遍历方法对比
  • 原文地址:https://www.cnblogs.com/multimicro/p/11726863.html
Copyright © 2020-2023  润新知