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-05 18:19:33 +08:00
网络上很多 Linux SPI 驱动框架参考资料,但这些资料大部分以讲解源码为主,虽然 Linux 内核源码很清晰美妙,但内核版本众多,细枝末节处差异很大,不利于初学者进行对比学习。另外初学者需要阅读大量的源码才能明晰程序流程,总结出具体的原理,形成总体概念,而无法形成总体概念的情况下,是很难完成具体开发工作的。因此本文主要以文字描述为主,源码为辅,重点在于理清与 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-05 18:19:33 +08:00
## 对 SPI 子系统的一些理解
电子系统中有很多外设,有像 GPIO 这样简单的设备,也有像 LCD 控制器这样复杂的。有一类特殊的外设,用于实现总线通信,通常以控制器的角色出现,被称为总线控制器,例如 UART 控制器、以太网控制器,以及本文的主角 SPI 控制器。这些控制器与特定的总线连接,总线上往往可以挂载多个设备。总线有主从通信,也有无主从关系的。
SPI 总线是这些总线中的一种, 属于主从通讯形式, 可以挂载多个从设备, 每个从设备通过片选( CS) 信号来选定。核心 CPU 常常作为主设备来工作,通过以某种形式挂载到核心 CPU 上的 SPI 总线控制器与从设备通信。具体而言,在 x86 平台上, SPI 总线控制器通过 PCI 总线挂接到主 CPU 上;而 ARM 平台上往往是以外设的形式出现,直接挂接在 SOC 或 MCU 片内的系统总线上。
![图 1 SPI 总线结构 ](./img/Linux_SPI_子系统_x86平台/001.jpg )
SPI 通讯离不开主控制器和从设备,因此在 Linux 系统中就需要为这两种对象开发驱动程序。其中, SPI 控制器驱动用于驱动主设备中的 SPI 总线控制器;而 SPI 设备驱动用于驱动从设备(因此个人觉得叫 SPI 从设备驱动似乎更贴切些,为便于区分,后卫都将 SPI 设备驱动称作 SPI 从设备驱动) , SPI 从设备驱动的工作相当于将 SPI 通信协议与具体的设备控制协议相互转换,所以又称为 SPI 协议驱动。一条 SPI 总线(对应一个 SPI 主控制器)上可以挂接多个 SPI 从设备,与之对应的是:一个 SPI 控制器驱动也可以挂接多个 SPI 从设备驱动。下图,详细描述了 Linux 系统中 SPI 驱动框架,以及各部分之间的相互关系。
![图 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/...)。
在 x86 平台下, SPI 总线控制器可以作为标准 PCI 设备接入,由 PCI 枚举来触发 SPI 控制器驱动的 Probe 过程; SPI 从设备驱动的 Probe 过程则是通过 Kernel 匹配 spi_driver 的 name 字段来完成的,后文讲具体描述这个过程。
有一些 SPI 从设备并不是由内核来为其提供驱动的, 而是交给用户态去处理, 此时需要在内核为这些从设备导出用户态操作的接口, Linux 内核为此提供了统一的内核程序——spidev, 来完成此项工作。被 spidev 导出的接口以 spidev< 总线号 > .< 子设备号 / 片选序号 > 的文件形式出现在 /dev 系统目录下, 如: spidev1.2 便是第二个 SPI 总线上的第三个从设备。spidev 相当于一种特殊的 SPI 从设备驱动,即在 SPI 子系统框架下实现的字符设备。
## 先说说 Probe
不同平台、不同类型设备的 Probe 方法不同。对于 ARM 平台目前使用 Device Tree; 而 x86 平台有 ACPI 表。对于 PCI 设备, 则通过总线号( Bus) 、设备号( Device) 和功能号( Function) 来枚举设备( 简称 BDF) 。设备发现和枚举过程由系统内核框架实现, 一般不需要设备驱动开发人员关心, 只需要写好 Device Tree, 提供好 ACPI 表即可,而这两个表一般会有 Demo 可以参考。
有些设备无法像 PCI 设备那样被自动探测到,那就需要将这些设备写入 Device Tree 或 ACPI 表,也可以作为 Platform 设备出现,通过 board info 来注册。注意,无论是 Device Tree、ACPI 还是 board info, 他们只描述了设备的存在, 用于触发对应设备驱动的 Probe 过程。
## 板级支持文件
通常来说 SPI 从设备是无法被自动探测到的,此时可以作为 Platform 设备存在,在 arch->x86->platform 下( x86 平台)为其创建板级文件,注册 spi_board_info。
系统启动时,先执行 arch_initcall 中的定义的板级初始化程序,由该程序完成 spi_board_info 的注册,并由此形成一张 SPI 从设备列表。在 x86 平台下,当系统进行 PCI 设备枚举时,将发现 SPI 总线控制器,并调用与之对应的 SPI 总线控制器驱动中的 Probe 程序,该 Probe 程序会调用 regist 函数来注册 SPI master/controller, 在这个注册函数中会比较当前 SPI 控制器的总线号,如果与 SPI 从设备列表中的总线号对应,则将该从设备挂接到这个控制器上。在 4.19.23 版本内核中,以 pxa2xx 为例,可用以下函数调用关系来描述:
```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 目录下创建节点。
*/
}
}
}
}
```
spi_board_info 只是描述了有哪些板级 SPI 从设备存在,并没有真正的驱动这些设备。从设备的驱动是由 SPI 驱动来完成的。以 spidev 为例,看下它的框架就知道了:
```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);
```
代码首先注册了 File Operation 方法,在 init 中注册字符设备和 SPI 驱动,当触发 Probe 程序后,创建设备,并在 /dev 目录下形成设备节点。
通过 spi_driver 结构体可以看出,当 spidev 设备注册后,会通过三种方式匹配从而触发自身的 Probe 过程。这三种方式分别是 Device Tree、ACPI 表和 Name。对于 Platform 设备而言,当其 spi_board_info 中的 modalias 属性与 spi_driver 中的 name 属性一致时,就会触发。
下面具体看下 spi_board_info 的结构,注意每个成员后面的注释:
```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 协议相关资料.
};
```
![图 3 SPI 四种模式 ](./img/Linux_SPI_子系统_x86平台/003.jpg )
## SPI 核心层
有了 SPI 总线控制器驱动和 SPI 从设备驱动, SPI 子系统就可以工作了。但是我们发现,对于 SPI 子系统,有很多核心的代码是完全通用的,把这些代码抽出来,便构建成了 SPI 核心层。
2019-07-05 18:28:48 +08:00
## 对于开发的一些简单指导
基于当前的 SPI 子系统框架,一般有两种类型的设备驱动需要开发。一般开源社区或芯片供应商会提供 SPI 控制器驱动,下游的开发者只需要实现 SPI 从设备驱动即可。对于 SPI 控制器驱动,可以参考 pxa2xx 这个 SPI 总线控制器的驱动程序;对于 SPI 从设备驱动可以参考 spidev 这个驱动程序。
2019-07-05 18:19:33 +08:00
## 总结
最后晒一张来自网友的大图(来源见图中水印),系统总结了 SPI 子系统的 Probe 过程和各部分的功能:
![图 3 SPI 子系统总结 ](./img/Linux_SPI_子系统_x86平台/004.jpg )
## 参考资料
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 )