Linux SPI驅動學習——註冊匹配

博客說明

撰寫日期 2019.10.22
完稿日期 2019.10.23
最近維護 暫無
本文作者 multimicro
聯繫方式 [email protected]
資料鏈接 本文無附件資料
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菜單中同步開啓如下選項:
d.1 配置內核使用主控驅動 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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章