Linux内核SPI支持概述

SPI是什么?

“串行外设接口”(SPI)是一种同步四线串行链路,用于连接微控制器到传感器、内存和外设。这是一个简单的“de facto”标准,还没有复杂到需要一个标准化机构。SPI使用一个主/从配置。

这三根信号线包含一个时钟(SCK,通常在10兆赫兹的量级),以及带有“主输出,从输出”(MOSI)或“主输入,从输出”(MISO)信号的并行数据线。(也可以使用其他名称。)数据交换有四种时钟模式;模式0和模式3是最常用的。每个时钟周期将数据输出和数据输入;时钟不循环,除非有数据位移位。并不是所有的数据位都被使用;并非所有协议都使用这些全双工功能。

SPI主机使用第四“芯片选择”线来激活给定的SPI从设备,因此这三根信号线可以并联到多个芯片。所有SPI slave支持芯片选择;它们通常是有效的低信号,标记为从“x”的nCSx(例如nCS0)。一些设备有其他信号,通常包括到主机的中断。

与USB或SMBus等串行总线不同,即使是用于SPI slave功能的低级协议通常也不能在供应商之间互操作(SPI内存芯片除外)。

  • SPI可以用于请求/响应类型的设备协议,如触摸屏传感器和内存芯片。
  • 它也可以用于任意方向的数据流(半双工),或两者同时(全双工)。
  • 有些设备可能使用八位字。其他可能使用不同的字长,如12位或20位数字样本流。
  • Words 通常以其最高有效位(MSB)首先发送,但有时是最低有效位(LSB)先发送。
  • 有时SPI被用于 daisy-chain设备,如移位寄存器。

同样,SPI从服务器很少支持任何类型的自动发现/枚举协议。从一个给定的SPI主机访问的从设备树通常使用配置表手动设置。

SPI只是这种四线协议使用的名称之一,大多数控制器在处理“MicroWire”(将其视为半双工SPI,用于请求/响应协议)、SSP(“同步串行协议”)、PSP(“可编程串行协议”)和其他相关协议时都没有问题。

一些芯片通过结合MOSI和MISO来消除信号线,并在硬件级别上限制自己为半双工。事实上,一些SPI芯片有这种信号模式作为捆扎选项。可以使用与SPI相同的编程接口访问这些数据,但它们当然不会处理全双工传输。你可能会发现这样的芯片被描述为使用“三线”信号:SCK, data, nCSx。(这条数据线有时被称为MOMI或SISO。)

微控制器通常同时支持SPI协议的主从端。本文档(和Linux)同时支持SPI交互的主端和从端。

谁使用它?在什么系统上?

使用SPI的Linux开发人员可能正在为嵌入式系统板编写设备驱动程序。SPI是用来控制外部芯片的,也是每个MMC或SD存储卡都支持的协议。(较老的“DataFlash”卡,在MMC卡之前,但使用相同的连接器和卡形状,只支持SPI。) 一些PC硬件使用SPI flash存放BIOS代码。

SPI slave芯片范围从用于模拟传感器和编解码器的数字/模拟转换器,到存储器,到外围设备,如USB控制器或以太网适配器等等。

大多数使用SPI的系统将在主板上集成一些设备。有些在扩展连接器上提供SPI链接;在没有专用SPI控制器存在的情况下,GPIO引脚可以用来创建低速“bitbanging”适配器。很少有系统会“热插拔”SPI控制器;使用SPI的原因集中在低成本和简单的操作,如果动态重新配置是重要的,USB通常会是一个更合适的low-pincount外围总线。

许多可以运行Linux的微控制器集成了一个或多个带有SPI模式的I/O接口。由于SPI支持,他们可以使用MMC或SD卡,而不需要特殊用途的MMC/SD/SDIO控制器。

四种SPI“时钟模式”是什么?

这里很容易混淆,您将发现供应商文档不一定有帮助。四种模式结合两个模式位:

  • CPOL表示初始时钟极性。CPOL=0意味着时钟开始时是低电平,所以第一个(leading)跳变沿是上升沿,第二个(trailing)跳变沿是下降沿。CPOL=1意味着时钟开始时是高电平,所以第一个(leading)跳变沿是下降沿。
  • CPHA 表示用于采样数据的时钟相位;CPHA =0表示在第一个(leading)跳变沿取样,CPHA =1表示在第二个(trailing)跳变沿取样。

由于信号在采样之前需要稳定,所以CPHA=0意味着它的数据在第一个时钟边缘之前提前半个时钟写入。chipselect 可能使它变得可能。

芯片规格并不总是说“使用SPI模式X”,但它们的时序图将使CPOL和CPHA模式清晰。

SPI模式号中,CPOL为高阶位,CHPA为低阶位。因此,当芯片的时序图显示时钟开始时为低(CPOL=0)和数据稳定采样期间为第二个(trailing)跳变沿(CPHA=1),这是SPI模式1。

请注意,一旦chipselect被激活,时钟模式就与之相关。因此,在选择一个从服务器之前,主服务器必须将时钟设置为非活动状态,而从服务器可以在选择线激活时通过采样时钟电平来得知所选择的极性。

这就是为什么许多设备同时支持模式0和模式3:他们不关心极性,并且总是在上升的时钟边缘上输入/输出时钟数据。

这些驱动程序编程接口是如何工作的?

<linux/spi/spi.h>头文件包括kerneldoc,主源代码也是如此,您当然应该阅读内核API文档的那一章。这只是一个概述,所以在细节之前,你会有一个大的图景。

SPI请求总是进入I/O队列。对给定SPI设备的请求总是按照FIFO顺序执行,并通过完成回调异步完成。还有一些用于这些调用的简单同步包装器,包括用于常见事务类型的包装器,比如编写命令然后读取其响应。

有两种类型的SPI驱动程序,这里称为:

Controller驱动程序:

控制器可以内置于片上系统处理器中,并且通常同时支持主从角色。这些驱动程序涉及硬件寄存器并可能使用DMA。或者他们可能是PIO的bitbangers,只需要GPIO引脚。

Protocol驱动程序:

这些消息通过控制器驱动程序与SPI链路另一端的从设备或主设备通信。

 例如,一个protocol驱动程序可能与MTD层通信,将数据导出到存储在像DataFlash这样的SPI flash上的文件系统;还有一些可能控制音频接口,将触摸屏传感器作为输入接口,或在工业处理过程中监控温度和电压水平。它们可能共享同一个控制器驱动程序。

“struct spi_device”封装了这两种类型的驱动程序之间的控制器端接口。

SPI编程接口有一个最小的核心,重点是使用驱动程序模型来连接控制器和协议驱动程序,使用单板特定初始化代码提供的设备表。SPI在sysfs中出现在以下几个位置:

/sys/devices/.../CTLR ... physical node for a given SPI controller

/sys/devices/.../CTLR/spiB.C ... spi_device on bus "B",
     chipselect C, accessed through CTLR.

/sys/bus/spi/devices/spiB.C ... symlink to that physical
     .../CTLR/spiB.C device

/sys/devices/.../CTLR/spiB.C/modalias ... identifies the driver
     that should be used with this device (for hotplug/coldplug)

/sys/bus/spi/drivers/D ... driver for one or more spi*.* devices

/sys/class/spi_master/spiB ... symlink (or actual device node) to
     a logical node which could hold class related state for the SPI
     master controller managing bus "B".  All spiB.* devices share one
     physical SPI bus segment, with SCLK, MOSI, and MISO.

/sys/devices/.../CTLR/slave ... virtual file for (un)registering the
     slave device for an SPI slave controller.
     Writing the driver name of an SPI slave handler to this file
     registers the slave device; writing "(null)" unregisters the slave
     device.
     Reading from this file shows the name of the slave device ("(null)"
     if not registered).

/sys/class/spi_slave/spiB ... symlink (or actual device node) to
     a logical node which could hold class related state for the SPI
     slave controller on bus "B".  When registered, a single spiB.*
     device is present here, possible sharing the physical SPI bus
     segment with other SPI slave devices.

注意,控制器类状态的实际位置取决于是否启用了CONFIG_SYSFS_DEPRECATED。此时,唯一特定于类的状态是总线号(“spiB”中的“B”),因此那些/sys/class条目只对快速识别总线有用。

特定于板的初始化代码如何声明SPI设备?

Linux需要几种信息来正确配置SPI设备。这些信息通常是由特定于电路板的代码提供的,即使对于那些确实支持自动发现/枚举的芯片也是如此。

声明Controllers

第一种信息是存在的SPI控制器的列表。对于基于片上系统(SOC)的板,这些通常是平台设备,控制器可能需要一些platform_data才能正常操作。“struct platform_device”将包括一些资源,比如控制器的第一个寄存器的物理地址及其IRQ。

平台通常会抽象“注册SPI控制器”操作,可能会将其与代码耦合起来初始化引脚配置,以便arch/…/mach-/board-.c文件对于几块板都可以共享相同的基本控制器设置代码。这是因为大多数soc都有几个支持spi的控制器,通常只应该设置和注册给定板上实际可用的控制器。

例如arch/…/mach-/board-.c文件可能有如下代码:

#include <mach/spi.h>   /* for mysoc_spi_data */

/* if your mach-* infrastructure doesn't support kernels that can
 * run on multiple boards, pdata wouldn't benefit from "__init".
 */
static struct mysoc_spi_data pdata __initdata = { ... };

static __init board_init(void)
{
        ...
        /* this board only uses SPI controller #2 */
        mysoc_register_spi(2, &pdata);
        ...
}

特定于soc的实用程序代码可能如下所示:

#include <mach/spi.h>

static struct platform_device spi2 = { ... };

void mysoc_register_spi(unsigned n, struct mysoc_spi_data *pdata)
{
        struct mysoc_spi_data *pdata2;

        pdata2 = kmalloc(sizeof *pdata2, GFP_KERNEL);
        *pdata2 = pdata;
        ...
        if (n == 2) {
                spi2->dev.platform_data = pdata2;
                register_platform_device(&spi2);

                /* also: set up pin modes so the spi2 signals are
                 * visible on the relevant pins ... bootloaders on
                 * production boards may already have done this, but
                 * developer boards will often need Linux to do it.
                 */
        }
        ...
}

注意板子的platform_data可能是不同的,即使使用相同的SOC控制器。例如,在一个板上,SPI可能使用外部时钟,而另一个板从一些主时钟的当前设置派生SPI时钟。

声明Slave设备

第二种信息是目标板上存在的SPI从设备的列表,通常带有驱动程序正确工作所需的特定于板的数据。

通常,arch/…/mach-/board-.c文件会提供一个小表,列出每个板上的SPI设备。(这通常只是一小部分。)它可能看起来像:

static struct ads7846_platform_data ads_info = {
        .vref_delay_usecs       = 100,
        .x_plate_ohms           = 580,
        .y_plate_ohms           = 410,
};

static struct spi_board_info spi_board_info[] __initdata = {
    {
        .modalias       = "ads7846",
        .platform_data  = &ads_info,
        .mode           = SPI_MODE_0,
        .irq            = GPIO_IRQ(31),
        .max_speed_hz   = 120000 /* max sample rate at 3V */ * 16,
        .bus_num        = 1,
        .chip_select    = 0,
    },
};

再一次,注意板特定信息是如何提供的;每个芯片可能需要几种类型。这个例子显示了通用的约束,如最快的SPI时钟允许(在这种情况下,板电压的功能)或IRQ引脚是如何连接的,加上特定芯片的约束,比如一个重要的延迟是由一个引脚上的电容改变的。

还有“controller_data”,可能对控制器驱动有用的信息。一个例子是特定于外围设备的DMA调优数据或chipselect回调。稍后将存储在spi_device中。

board_info应该提供足够的信息,让系统在不加载芯片驱动的情况下工作。最麻烦的方面可能是spi_device.mode字段中的SPI_CS_HIGH位,在基础结构知道如何取消选择之前,与解释chipselect “backwards”的设备共享总线是不可能的。

然后你的板初始化代码会将该表注册到SPI基础结构中,这样当SPI主控制器驱动程序注册时它就可用了:

spi_register_board_info(spi_board_info, ARRAY_SIZE(spi_board_info));

与其他静态板特定的设置一样,您不会取消这些注册。

广泛使用的“卡片”式计算机将内存、中央处理器和其他东西捆绑在一张可能只有30平方厘米的卡片上。在这样的系统上,你的arch/…/mach-…/board-*.c文件将主要提供关于主板上的设备的信息,这样的卡被插到主板上。这当然包括通过卡连接器连接的SPI设备!

非静态配置

开发板通常遵循与产品板不同的规则,一个例子就是对热插拔SPI设备和/或控制器的潜在需求。

对于这些情况,您可能需要使用 spi_busnum_to_master() 来查找spi总线主机,并且可能需要 spi_new_device() 来提供基于被热插拔的板的板信息。当然,当该板被删除时,您至少会调用 spi_unregister_device()。

struct spi_controller * spi_busnum_to_master(u16 bus_num);
struct spi_device * spi_new_device(struct spi_controller *ctlr, struct spi_board_info *chip);
void spi_unregister_device(struct spi_device *spi);

当Linux通过SPI包含对MMC/SD/SDIO/DataFlash卡的支持时,这些配置也将是动态的。幸运的是,这些设备都支持基本的设备标识probes,所以它们应该能够正常地热插拔。

如何写一个“SPI协议驱动程序”?

大多数SPI驱动程序目前都是内核驱动程序,但也支持用户空间驱动程序。这里我们只讨论内核驱动程序。

SPI协议驱动程序有点类似于平台设备驱动程序:

static struct spi_driver CHIP_driver = {
    .driver = {
        .name           = "CHIP",
        .owner          = THIS_MODULE,
        .pm             = &CHIP_pm_ops,
    },

    .probe          = CHIP_probe,
    .remove         = CHIP_remove,
};

驱动核心将自动尝试将该驱动绑定到任何SPI设备,其board_info提供的modalias为“CHIP”。您的probe()代码可能看起来像这样,除非您正在创建一个管理总线的设备(出现在/sys/class/spi_master下)。

static int CHIP_probe(struct spi_device *spi)
{
        struct CHIP                     *chip;
        struct CHIP_platform_data       *pdata;

        /* assuming the driver requires board-specific data: */
        pdata = &spi->dev.platform_data;
        if (!pdata)
                return -ENODEV;

        /* get memory for driver's per-chip state */
        chip = kzalloc(sizeof *chip, GFP_KERNEL);
        if (!chip)
                return -ENOMEM;
        spi_set_drvdata(spi, chip);

        ... etc
        return 0;
}

一旦进入probe(),驱动程序就可以使用“struct spi_message”向SPI设备发出I/O请求。当remove()返回时,或者probe()失败后,驱动程序保证它不会提交任何此类消息。

  • spi_message是一个协议操作序列,作为一个原子序列执行。SPI驱动控制包括:
    • 当双向读和写开始…通过它的spi_transfer请求序列的安排;
    • 每个spi_transfer为每个传输方向包装一个缓冲区,支持全双工(两个指针,可能在两种情况下都是同一个)和半双工(一个指针是NULL)传输;
    • 使用spi_transfer.delay.value设置可选地定义传输后的短延迟(如果缓冲区长度为零,此延迟可能是唯一的协议影响)…当指定此延迟时,默认的spi_transfer.delay.unit为微秒,但是如果需要,可以将其调整为时钟周期或纳秒;
    • 通过使用spi_transfer.cs_change标志,chipselect是否在传输和延迟之后变为非活动状态;
    • 在原子组的最后一个传输中使用spi_transfer.cs_change标志来提示下一个消息是否可能到达同一设备,并潜在地节省芯片deselect和select操作的成本。
  • 遵循标准内核规则,并在消息中提供dma安全缓冲区。这样,除非硬件需要,否则使用DMA的控制器驱动程序就不会被迫做额外的拷贝(例如,解决硬件勘误表强制使用反弹缓冲)。如果这些缓冲区的标准dma_map_single()处理不合适,可以使用spi_message.is_dma_mapped来告诉控制器驱动程序您已经提供了相关的DMA地址。
  • 基本的I/O原语是spi_async()。Async请求可以在任何上下文中发出(irq处理程序,任务等),并使用消息提供的回调来报告完成情况。在检测到任何错误后,该芯片将被取消选择,并中止对该spi_message的处理。
  • 还有像spi_sync()这样的同步包装器,以及像spi_read()、spi_write()和spi_write_then_read()这样的包装器。这些可能只在可能处于休眠状态的上下文中发布,而且它们都是spi_async()上的干净(小,且“可选”)层。
  • spi_write_then_read()调用以及围绕它的方便包装器应该只用于少量数据,因为可能会忽略额外副本的成本。它被设计为支持常见的rpc样式的请求,比如编写一个8位命令并读取16位响应——spi_w8r16()是它的一个包装器,正是这样做的。

一些驱动程序可能需要修改spi_device特征,如传输模式、字长或时钟速率。这是通过spi_setup()完成的,通常在对设备进行第一次I/O之前从probe()调用spi_setup()。然而,也可以在该设备没有消息挂起的任何时候调用。

虽然“spi_device”是驱动程序的底部边界,但上面的边界可能包括sysfs(特别是对于传感器读数)、输入层、ALSA、网络、MTD、字符设备框架或其他Linux子系统。

注意,作为与SPI设备交互的一部分,驱动程序必须管理两种类型的内存。

  • I/O缓冲区使用通常的Linux规则,并且必须是dma安全的。通常从堆或空闲页池中分配它们。不要使用栈,或者任何声明为“static”的东西。
  • spi_message和spi_transfer元数据用于将这些I/O缓冲区粘合到一组协议事务中。这些可以被分配到任何方便的地方,包括作为其他一次分配驱动程序数据结构的一部分。Zero-init这些。

如果您愿意,可以使用spi_message_alloc()和spi_message_free()方便例程来分配和初始化一个spi_message并进行多次传输。

如何写一个“SPI主控制器驱动程序”?

SPI控制器可能会在platform_bus上注册;写一个驱动程序绑定到设备上,无论哪个总线涉及。

这类驱动程序的主要任务是提供一个“spi_master”。使用spi_alloc_master()来分配master,使用spi_master_get_devdata()来获取分配给该设备的驱动程序私有数据。

struct spi_master       *master;
struct CONTROLLER       *c;

master = spi_alloc_master(dev, sizeof *c);
if (!master)
        return -ENODEV;

c = spi_master_get_devdata(master);

驱动程序将初始化spi_master的字段,包括总线号(可能与平台设备ID相同)和用于与SPI核心和SPI协议驱动程序交互的三个方法。它还将初始化自己的内部状态。(参见下面关于总线编号和那些方法的内容。)

初始化spi_master之后,使用 spi_register_master() 将其发布到系统的其他部分。那时,控制器的设备节点和任何预先声明的spi设备都将可用,驱动模型核心将负责将它们绑定到驱动。

如果需要删除SPI控制器驱动程序,则spi_unregister_master()将逆转spi_register_master()的效果。

总线号

总线编号很重要,因为这是Linux识别给定SPI总线(共享SCK、MOSI、MISO)的方式。有效的总线编号从零开始。在SOC系统上,总线号应该与芯片制造商定义的数字相匹配。例如,硬件控制器SPI2将是总线号2,连接到它的设备的spi_board_info将使用该编号。

如果你没有这样的硬件分配的总线号,并且由于某些原因你不能直接分配它们,那么提供一个负的总线号。这将被一个动态分配的数字所取代。然后需要将其视为非静态配置(参见上面)。

SPI Master 方法

master->setup(struct spi_device *spi)

这设置了设备时钟速率、SPI模式和字大小。驱动程序可能会改变board_info提供的默认值,然后调用spi_setup(spi)来调用这个例程。它可能会睡眠。

除非每个SPI slave有它自己的配置寄存器,不要立即更改它们…否则驱动程序可能会破坏正在进行的其他SPI设备的I/O。

BUG警告:由于某些原因,许多spi_master驱动的第一个版本似乎在这一点上是错误的。在编写setup()代码时,假设控制器正在积极地处理另一个设备的传输。

master->cleanup(struct spi_device *spi)

  您的控制器驱动程序可能使用 spi_device.controller_state 保存与该设备动态关联的状态。如果您这样做,请确保提供cleanup()方法来释放该状态。

master->prepare_transfer_hardware(struct spi_master *master)

  这将被队列机制调用,以向驱动程序发出消息即将到来的信号,因此子系统请求驱动程序通过发出这个调用来准备传输硬件。这可能睡眠。

master->unprepare_transfer_hardware(struct spi_master *master)

  这将被队列机制调用,以向驱动程序发出信号,表明队列中没有更多的消息挂起,它可能会放松硬件(例如通过电源管理调用)。这可能睡眠。

master->transfer_one_message(struct spi_master *master, struct spi_message *mesg)

 

  子系统调用驱动程序来传输单个消息,同时对同时到达的传输进行排队。当驱动程序完成此消息时,它必须调用spi_finalize_current_message(),以便子系统可以发出下一条消息。这可能睡眠。

master->transfer_one(struct spi_master *master, struct spi_device *spi, struct spi_transfer *transfer)

  子系统调用驱动程序来传输单个传输,同时对同时到达的传输进行排队。当驱动程序完成这个传输时,它必须调用spi_finalize_current_transfer(),以便子系统可以发出下一次传输。这可能睡眠。注:transfer_one和transfer_one_message互斥;当两者都设置了时,通用子系统不会调用transfer_one回调。

  返回值:

  • negative errno: 错误
  • 0: 表示传输结束
  • 1: 传输仍在进行中

master->set_cs_timing(struct spi_device *spi, u8 setup_clk_cycles, u8 hold_clk_cycles, u8 inactive_clk_cycles)

 

  这个方法允许SPI客户端驱动程序请求SPI主控制器来配置设备特定的CS设置,保持和非活动的时间要求。

弃用的方法

master->transfer(struct spi_device *spi, struct spi_message *message)

  这不能睡眠。它的责任是安排传输的发生和它的complete()回调的发出。这两种情况通常会在其他传输完成后发生,如果控制器是空闲的,则需要启动它。该方法不用于队列控制器,如果实现了transfer_one_message()和(un)prepare_transfer_hardware()则必须为NULL。

SPI 消息队列

如果您对SPI子系统提供的标准排队机制感到满意,那么只需实现上面指定的排队方法。使用消息队列的好处是可以集中大量代码,并提供方法的纯流程上下文执行。在高优先级SPI通信中,消息队列也可以提升到实时优先级。

除非选择了SPI子系统中的队列机制,否则驱动程序的大部分将管理由现在已弃用的函数transfer()提供的I/O队列。

这个队列可能纯粹是概念性的。例如,仅用于低频传感器访问的驱动程序可以使用同步PIO。

但是队列可能非常真实,使用message->queue、PIO,通常是DMA(特别是根文件系统在SPI flash中),以及执行上下文,如IRQ处理程序、tasklets或工作队列(如keventd)。您的驱动程序可以很花哨,也可以很简单,只要您需要。这样的transfer()方法通常只会将消息添加到队列中,然后启动一些异步传输引擎(除非它已经在运行)。

posted @ 2021-08-23 15:32  闹闹爸爸  阅读(1027)  评论(0编辑  收藏  举报