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\n",
			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..\n");
    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 "\tOrigin Creat node %d\n",major);
    } else {
        ret = alloc_chrdev_region(&devid, 0, 1, DRV_NAME);
        major = MAJOR(devid);
        printk(DRV_NAME "\tArrage Creat node %d\n",major);
    }
    if(ret < 0) {
        printk(DRV_NAME "\tnew device failed\n");
        //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 "\tError %d adding w25q16 %d\n",err, 1);

    class = class_create(THIS_MODULE, "w25q16");
    device_create(class, NULL, MKDEV(major, minor), NULL, "w25q16");
    printk(DRV_NAME "\tcreat device node /dev/w25q16 \n");

    return 0;

fail_malloc:
    printk("Failed to allocate memory!\n");
    return ret;

}

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

static int __init w25q16_driver_init(void)
{
    int ret;
    printk("\n************ driver init begin ************\n\n");
    ret = spi_register_driver(&w25q16_spi_driver);
    if(ret)
    {
        spi_unregister_driver(&w25q16_spi_driver);
        printk(DRV_NAME "\tFailed register spi driver. Error: %d\n",ret);
    }
    printk("\n************* driver init end *************\n\n");
    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
posted @ 2019-10-23 16:09  multimicro  阅读(2872)  评论(1编辑  收藏  举报