2019-07-05 18:19:33 +08:00
|
|
|
|
# Linux SPI 子系统(x86平台)
|
2019-07-04 19:08:06 +08:00
|
|
|
|
|
2019-07-05 18:19:33 +08:00
|
|
|
|
## 前言
|
2019-07-04 19:08:06 +08:00
|
|
|
|
|
2019-07-05 18:19:33 +08:00
|
|
|
|
写文在于交流和传播知识,本人才粗学浅,还请多多指教,板砖轻拍。
|
2019-07-04 19:08:06 +08:00
|
|
|
|
|
2019-07-09 06:38:40 +08:00
|
|
|
|
网络上很多 Linux SPI 驱动框架参考资料,但这些资料大部分以讲解源码为主,虽然 Linux 内核源码很清晰美妙,但内核版本众多,细枝末节处差异很大,不利于初学者进行对比学习。另外初学者需要阅读大量的源码才能明晰程序流程,这就增加了系统了解 SPI 子系统的难度,很难快速完成具体开发工作。因此本文主要以文字描述为主,源码为辅,重点在于理清与 SPI 有关的相关概念和 SPI 子系统的初始化的流程。
|
2019-07-04 19:08:06 +08:00
|
|
|
|
|
2019-07-05 18:19:33 +08:00
|
|
|
|
另外,本文主要描述 x86 体系下的 SPI 框架,也可作为 ARM 体系下 SPI 框架的参考,因为两种框架下的概念和原理都是相通的,只是有些地方的具体实现不同。
|
2019-07-04 19:14:49 +08:00
|
|
|
|
|
2019-07-08 18:08:03 +08:00
|
|
|
|
## 总述
|
2019-07-05 18:19:33 +08:00
|
|
|
|
|
2019-07-08 18:08:03 +08:00
|
|
|
|
SPI 是一种总线通讯协议,由总线控制器和从设备构成。Linux SPI 驱动包含两个部分,分别用于驱动 SPI 总线控制器和从设备,内核中的 SPI 子系统为这两种驱动提供了开发框架。
|
2019-07-05 18:19:33 +08:00
|
|
|
|
|
2019-07-08 18:08:03 +08:00
|
|
|
|
在系统探测到设备并挂载相关驱动的过程中,涉及到设备发现(探测或枚举)和驱动匹配(Match),对于总线上的设备,需要知道该设备挂载在哪条总线上,因此就需要知道该设备的总线号。在 SPI 子系统中,这些动作由 SPI Board Info(ACPI 或 Device Tree 中与 SPI 从设备有关的部分)、SPI 控制器驱动以及SPI 从设备驱动共同完成,它们各自的分工如下:
|
|
|
|
|
|
|
|
|
|
1. SPI Board Info(ACPI 或 Device Tree 中与 SPI 从设备有关的部分):用于声明设备的存在,提供 SPI 从设备所在的总线号、片选号以及用于与 SPI 从设备匹配的 modalias 字段(与 SPI 从设备中的 name 字段匹配)。
|
|
|
|
|
2. SPI 控制器驱动:用于驱动 SPI 总线控制器。
|
|
|
|
|
3. SPI 从设备驱动:用于驱动 SPI 从设备。
|
|
|
|
|
|
|
|
|
|
接下来将从 SPI 硬件系统与软件抽象之间的关系,以及 SPI 驱动的探测过程两个方面展开说明。
|
|
|
|
|
|
|
|
|
|
## SPI 硬件系统与软件抽象之间的关系
|
|
|
|
|
|
|
|
|
|
电子系统中有很多外设,有像 GPIO 这样简单的设备,也有像 LCD 控制器这样复杂的。有一类特殊的外设,用于实现总线通信,通常以控制器的角色出现,被称为总线控制器,例如 UART 控制器、以太网控制器,以及本文的主角 SPI 控制器。
|
|
|
|
|
|
|
|
|
|
SPI 控制器用于实现 SPI 总线通讯,该通讯采用主从通讯形式,一个主设备可以挂载多个 SPI 从设备,每个从设备通过片选(CS)信号来选定。核心 CPU 通过 SPI 控制器与从设备进行交互。SPI 控制器常常以某种形式挂载到核心 CPU 上,具体而言,在 x86 平台上,SPI 总线控制器通过 PCI 总线挂接到主 CPU 上;而 ARM 平台上往往是以片载外设的形式出现,直接挂接在 SOC 或 MCU 片内的系统总线上。
|
2019-07-05 18:19:33 +08:00
|
|
|
|
|
|
|
|
|
![图 1 SPI 总线结构](./img/Linux_SPI_子系统_x86平台/001.jpg)
|
|
|
|
|
|
2019-07-09 06:38:40 +08:00
|
|
|
|
SPI 通讯离不开 SPI 总线控制器和从设备,因此在 Linux 系统中就需要为这两种对象开发驱动程序。用于驱动 SPI 总线控制器的驱动称为 SPI 控制器驱动,而用于驱动 SPI 从设备的驱动称为 SPI 设备驱动(个人觉得叫 SPI 从设备驱动似乎更贴切些,为便于区分,后文都将 SPI 设备驱动称作 SPI 从设备驱动)。从另一个角度来说,SPI 从设备驱动的工作就是基于 SPI 通讯来实现对从设备的控制和访问,相当于实现 SPI 通信协议与具体的设备控制/访问协议的相互转换,所以 SPI 从设备驱动又称为 SPI 协议驱动。从名称上可以看出,SPI 控制器驱动属于总线控制器驱动,然而 SPI 从设备是多种多样的,可能是 char 设备(如 spidev),也可能是 block 设备(如 SPI Flash),因此 SPI 从设备驱动也是有多种类型的。一条 SPI 总线(对应一个 SPI 总线控制器)上可以挂接多个 SPI 从设备,与之对应的是:一个 SPI 控制器驱动也可以挂接多个 SPI 从设备驱动。下图,详细描述了 Linux 系统中 SPI 驱动框架,以及各部分之间的相互关系。
|
2019-07-05 18:19:33 +08:00
|
|
|
|
|
|
|
|
|
![图 2 Linux SPI 子系统框架](./img/Linux_SPI_子系统_x86平台/002.jpg)
|
|
|
|
|
|
|
|
|
|
1. SPI 控制器驱动:相当于 master/controller,由 spi_master 描述,用于驱动 SPI 总线控制器,实现其初始化、中断回调等功能,在 /sys 目录下创建节点,不提供 file operation 接口,驱动类型为 SPI 总线控制器驱动。
|
|
|
|
|
|
|
|
|
|
2. SPI 协议驱动(SPI 从设备驱动): 相当于 slave,用于驱动 SPI 总线上所挂接的设备,在 /dev 目录下创建设备节点,提供 open、read、write、ioctl 等 file operation 接口,驱动类型由设备本身所属设备驱动类型描述(char/block/...)。
|
|
|
|
|
|
2019-07-09 06:38:40 +08:00
|
|
|
|
有一些 SPI 从设备并不是由内核来为其提供驱动的,而是交给用户态去处理,此时需要在内核为这些从设备导出用户态操作的接口,Linux 内核为此提供了统一的内核程序——spidev,来完成此项工作。被 spidev 导出的接口以 spidev<总线号>.<子设备号/片选序号> 的文件形式出现在 /dev 系统目录下,如:spidev1.2 便是第二个 SPI 总线上的第三个从设备。之后就可以在用户态通过 open、read、write、ioctl、close 等标准接口操作 spidev 了。spidev 相当于一种特殊的 SPI 从设备驱动,即在 SPI 子系统框架下实现的字符设备,该设备能够从用户态进行直接 SPI 通讯。
|
2019-07-08 18:08:03 +08:00
|
|
|
|
|
2019-07-09 06:38:40 +08:00
|
|
|
|
了解 Linux SPI 子系统中两个主要角色(SPI 控制器驱动和 SPI 从设备驱动)后,就需要知道他们是如何被初始化的,以及如何与具体设备匹配上的,这涉及到 SPI 总线控制器、SPI 从设备的枚举和发现,以及驱动匹配等。
|
2019-07-08 18:08:03 +08:00
|
|
|
|
|
2019-07-09 06:38:40 +08:00
|
|
|
|
## SPI 驱动的 Probe 和 Match 过程
|
2019-07-08 18:08:03 +08:00
|
|
|
|
|
2019-07-09 06:38:40 +08:00
|
|
|
|
有些总线设备是可以自动枚举到的,如 PCI 总线可以通过 BDF(总线号 Bus、设备号 Device 和功能号 Function) 来枚举设备并通过 Device ID 和 Vendor ID 来匹配驱动程序。然而有很多总线不能自动枚举设备并匹配驱动程序。因此内核提供了几种配置表用于声明某些设备的存在,对于 ARM 平台目前使用 Device Tree,而 x86 平台有 ACPI 表,或者干脆以平台设备的形式注册 Board Info。Linux 内核会扫描这些表或已注册的 Board Info,并根据其中的信息触发对应驱动程序的 Probe 流程。这个过程由系统内核框架实现的,不需要设备驱动开发人员关心,只需要写好 Device Tree,提供好 ACPI 表,或注册好 Board Info 即可,而这一般会有 Demo 可以参考。
|
2019-07-08 18:08:03 +08:00
|
|
|
|
|
2019-07-09 06:38:40 +08:00
|
|
|
|
在 x86 平台下,SPI 总线控制器作为 PCI 设备接入,由 PCI 枚举来探测到设备,并通匹配 Device ID 和 Vendor ID 来匹配 SPI 控制器驱动(Linux 内核程序结合硬件机制实现了该功能,具体 PCI 设备枚举和匹配过程可以参考 PCI 方面的专业资料),触发其 Probe 程序;然而 SPI 从设备是无法自动探测到的,需要在 ACPI 表中声明这些从设备(x86 平台),或在平台设备中注册相关的 SPI Board Info,以便内核能够匹配到正确的 SPI 设备驱动并触发其 Probe 程序。
|
2019-07-08 18:08:03 +08:00
|
|
|
|
|
2019-07-09 06:38:40 +08:00
|
|
|
|
以平台设备为例,看下 SPI 从设备的探测流程。由于 SPI 平台设备是通过 SPI Board Info 来声明设备存在的,所以先介绍 spi_board_info 的具体结构。然后再根据 SPI Board Info 一步步分析 SPI 从设备的 Probe 过程。
|
2019-07-08 18:08:03 +08:00
|
|
|
|
|
|
|
|
|
*注:x86 系统的平台设备声明在 arch->x86->platform 下。*
|
2019-07-05 18:19:33 +08:00
|
|
|
|
|
2019-07-08 18:08:03 +08:00
|
|
|
|
### SPI Board Info
|
2019-07-05 18:19:33 +08:00
|
|
|
|
|
2019-07-09 06:38:40 +08:00
|
|
|
|
对于 spi_board_info,需要重点关注 bus_num 和 modalias,bus_num 将设备与对应的总线控制器驱动匹配,modalias 用于与 SPI 从设备匹配。以下是 spi_board_info 的具体结构,注意每个成员后面的注释:
|
2019-07-05 18:19:33 +08:00
|
|
|
|
|
2019-07-08 18:08:03 +08:00
|
|
|
|
```cpp
|
|
|
|
|
struct spi_board_info {
|
|
|
|
|
char modalias[SPI_NAME_SIZE]; // 用于与 SPI 从设备驱动匹配,触发其 Probe 过程.
|
|
|
|
|
const void *platform_data;
|
|
|
|
|
const struct property_entry *properties;
|
|
|
|
|
void *controller_data;
|
|
|
|
|
int irq;
|
|
|
|
|
u32 max_speed_hz;
|
|
|
|
|
u16 bus_num; // 用于与 SPI 控制器中的 bus num 匹配,对应 spidev<总线号>.<子设备号/片选序号> 中的 总线号.
|
|
|
|
|
u16 chip_select; // 指定片选序号,将作为 spidev<总线号>.<子设备号/片选序号> 中的 子设备号/片选序号.
|
|
|
|
|
u16 mode; // SPI 有四种模式,见下图,具体参考 SPI 协议相关资料.
|
|
|
|
|
};
|
|
|
|
|
```
|
2019-07-05 18:19:33 +08:00
|
|
|
|
|
2019-07-08 18:08:03 +08:00
|
|
|
|
![图 3 SPI 四种模式](./img/Linux_SPI_子系统_x86平台/003.jpg)
|
2019-07-05 18:19:33 +08:00
|
|
|
|
|
2019-07-09 06:38:40 +08:00
|
|
|
|
接下来,根据 bus_num 和 modalias 分析下从设备驱动的 Match 和 Probe 流程。
|
2019-07-05 18:19:33 +08:00
|
|
|
|
|
2019-07-09 06:38:40 +08:00
|
|
|
|
### 从设备驱动的 Match 和 Probe 过程
|
2019-07-05 18:19:33 +08:00
|
|
|
|
|
2019-07-09 06:38:40 +08:00
|
|
|
|
系统启动时,先执行 arch_initcall 中的定义的板级初始化程序,由该程序完成 spi_board_info 的注册,并由此形成一张 SPI 从设备列表。之后,在 x86 平台下,当系统进行 PCI 设备枚举时,将发现 SPI 总线控制器,并调用与之对应的 SPI 总线控制器驱动中的 Probe 程序,该 Probe 程序继续调用 regist 函数来注册 SPI master/controller,在这个注册函数中比较当前 SPI 控制器的总线号,如果与 SPI 从设备列表中的总线号对应,则将该从设备挂接到这个控制器上。在 4.19.23 版本内核中,以 pxa2xx 为例,可用以下函数调用关系来描述:
|
2019-07-05 18:19:33 +08:00
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
spi_register_board_info(){ /*Provide bus_num*/} <--+
|
|
|
|
|
|
|
|
|
|
|
pxa2xx_spi_probe() |
|
|
|
|
|
{ |
|
|
|
|
|
devm_spi_register_controller() |
|
|
|
|
|
{ |
|
|
|
|
|
spi_register_controller() |
|
|
|
|
|
{ |
|
|
|
|
|
spi_match_controller_to_boardinfo() |
|
|
|
|
|
{ <---+
|
|
|
|
|
/**
|
|
|
|
|
* controller 的 bus_num 与
|
|
|
|
|
* regist 的 spi_board_info
|
|
|
|
|
* 中的 bus_num 一致则执行
|
|
|
|
|
* spi_new_device()
|
|
|
|
|
* 并在 /sys 目录下创建节点。
|
|
|
|
|
*/
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2019-07-09 06:38:40 +08:00
|
|
|
|
一旦 SPI 从设备挂载到了对应的总线上,系统就会查找有无匹配的 SPI 从设备驱动,并触发其 Probe 过程。判断 SPI 从设备驱动是否与声明的平台设备相匹配,是通过比较 spi_driver 结构体中的 name 字段与 spi_board_info 结构体中的 modalias 字段是否一致来完成的,如果一致,则调用 SPI 从设备的 Probe 程序。SPI 从设备不但要继续完成 Match 和 Probe 过程,创建具体的设备对象,还要实现从设备的控制和访问,并向上为用户态提供设备访问接口,创建 /dev 目录下的设备节点等。下面以 spidev 为例,看看 spi_driver 的基本结构,以及 SPI 从设备驱动的接口和主要工作:
|
2019-07-05 18:19:33 +08:00
|
|
|
|
|
|
|
|
|
```cpp
|
|
|
|
|
/**
|
|
|
|
|
* File operation.
|
|
|
|
|
*/
|
|
|
|
|
static ssize_t spidev_write(...) {...}
|
|
|
|
|
static ssize_t spidev_read(...) {...}
|
|
|
|
|
static long spidev_ioctl(...) {...}
|
|
|
|
|
static int spidev_open(...) {...}
|
|
|
|
|
static int spidev_release(...) {...}
|
|
|
|
|
|
|
|
|
|
static const struct file_operations spidev_fops = {
|
|
|
|
|
.owner = THIS_MODULE,
|
|
|
|
|
.write = spidev_write,
|
|
|
|
|
.read = spidev_read,
|
|
|
|
|
.unlocked_ioctl = spidev_ioctl,
|
|
|
|
|
.open = spidev_open,
|
|
|
|
|
.release = spidev_release,
|
|
|
|
|
...
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Probe and remove.
|
|
|
|
|
*/
|
|
|
|
|
static int spidev_probe(struct spi_device *spi)
|
|
|
|
|
{
|
|
|
|
|
...
|
|
|
|
|
// 形成上文说的 /dev/spidev<总线号>.<子设备号/片选序号> 文件.
|
|
|
|
|
dev = device_create(spidev_class, &spi->dev, spidev->devt,
|
|
|
|
|
spidev, "spidev%d.%d",
|
|
|
|
|
spi->master->bus_num, spi->chip_select);
|
|
|
|
|
...
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int spidev_remove(...) {...}
|
|
|
|
|
|
|
|
|
|
static struct spi_driver spidev_spi_driver = {
|
|
|
|
|
.driver = {
|
|
|
|
|
// 通过 name 来匹配.
|
|
|
|
|
.name = "spidev",
|
|
|
|
|
// 通过 Device Tree 来匹配.
|
|
|
|
|
.of_match_table = of_match_ptr(spidev_dt_ids),
|
|
|
|
|
// 通过 ACPI 表来匹配.
|
|
|
|
|
.acpi_match_table = ACPI_PTR(spidev_acpi_ids),
|
|
|
|
|
},
|
|
|
|
|
.probe = spidev_probe,
|
|
|
|
|
.remove = spidev_remove,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Init and deinit.
|
|
|
|
|
*/
|
|
|
|
|
static int __init spidev_init(void)
|
|
|
|
|
{
|
|
|
|
|
...
|
|
|
|
|
// 注册字符设备
|
|
|
|
|
register_chrdev(SPIDEV_MAJOR, "spi", &spidev_fops);
|
|
|
|
|
class_create(THIS_MODULE, "spidev");
|
|
|
|
|
spi_register_driver(&spidev_spi_driver);
|
|
|
|
|
...
|
|
|
|
|
}
|
|
|
|
|
module_init(spidev_init);
|
|
|
|
|
|
|
|
|
|
static void __exit spidev_exit(void)
|
|
|
|
|
{
|
|
|
|
|
...
|
|
|
|
|
spi_unregister_driver(&spidev_spi_driver);
|
|
|
|
|
class_destroy(spidev_class);
|
|
|
|
|
unregister_chrdev(SPIDEV_MAJOR, spidev_spi_driver.driver.name);
|
|
|
|
|
...
|
|
|
|
|
}
|
|
|
|
|
module_exit(spidev_exit);
|
|
|
|
|
```
|
|
|
|
|
|
2019-07-09 06:38:40 +08:00
|
|
|
|
可以看出,代码首先注册了 File Operation 方法用于实现用户态访问接口,另一方面则通过其 SPI 控制器来访问和控制实际设备。而从 spi_driver 结构体不难看出,spidev 不但可以驱动已注册的 SPI 平台设备,还可以驱动以 Device Tree 或 ACPI 表声明的 SPI 从设备(匹配 Device Tree 或 ACPI 表)。
|
2019-07-05 18:19:33 +08:00
|
|
|
|
|
|
|
|
|
## SPI 核心层
|
|
|
|
|
|
2019-07-09 06:38:40 +08:00
|
|
|
|
有了 SPI 总线控制器驱动和 SPI 从设备驱动,SPI 子系统就可以工作了。但是我们发现,对于 SPI 子系统,有很多核心的代码是完全通用的,把这些共通代码抽取出来,便构建成了 SPI 核心层。
|
2019-07-05 18:19:33 +08:00
|
|
|
|
|
2019-07-05 18:28:48 +08:00
|
|
|
|
## 对于开发的一些简单指导
|
|
|
|
|
|
2019-07-09 06:38:40 +08:00
|
|
|
|
基于当前的 SPI 子系统框架,一般有两种类型的设备驱动需要开发——SPI 控制器驱动和 SPI 从设备驱动。SPI 控制器驱动一般由芯片供应商或开源社区会提供,下游的开发者只需要实现 SPI 从设备驱动即可。对于 SPI 控制器驱动,可以参考 pxa2xx 这个驱动程序;对于 SPI 从设备驱动可以参考 spidev 这个驱动程序。
|
2019-07-05 18:28:48 +08:00
|
|
|
|
|
2019-07-05 18:19:33 +08:00
|
|
|
|
## 总结
|
|
|
|
|
|
|
|
|
|
最后晒一张来自网友的大图(来源见图中水印),系统总结了 SPI 子系统的 Probe 过程和各部分的功能:
|
|
|
|
|
|
2019-07-09 06:38:40 +08:00
|
|
|
|
![图 4 SPI 子系统总结](./img/Linux_SPI_子系统_x86平台/004.jpg)
|
2019-07-05 18:19:33 +08:00
|
|
|
|
|
|
|
|
|
## 参考资料
|
|
|
|
|
|
|
|
|
|
1. [linux设备模型之spi子系统](https://www.cnblogs.com/gdt-a20/archive/2011/05/22/2291983.html)
|
|
|
|
|
2. [PXA2xx SPI on SSP driver HOWTO](https://www.mjmwired.net/kernel/Documentation/spi/pxa2xx)
|
|
|
|
|
3. [Linux设备驱动剖析之SPI(一)](https://www.cnblogs.com/lknlfy/p/3265019.html)
|
|
|
|
|
4. [Linux设备驱动剖析之SPI(二)](https://www.cnblogs.com/lknlfy/p/3265031.html)
|
|
|
|
|
5. [Linux设备驱动剖析之SPI(三)](https://www.cnblogs.com/lknlfy/p/3265054.html)
|