@
博客說明
撰寫日期 | 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
這個文件夾里面。
按照驅動在內核模塊中的加載方式,還需要同步修改Kconfig
和Makefile
Step 2:
在Kconfig
中增添信息
Step 3:
在Makefile
中增添信息
在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_num
和spi_master
里面的bus_num
相等的話,在spi_match_master_to_boardinfo
函數中調用spi_new_device
創建一個spi_device
在/sys/bus/spi/devices/
文件夾里面可以看到spi_device
的注冊信息:
這個spi0.194
和spi1.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_driver
的probe
函數,其實體為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的匹配注冊。總體流程如下圖: