Skip to main content
 首页 » 操作系统

Linux SPI(1)——Documentation/spi/spi_summary.txt翻译

2022年07月19日26del

Linux内核SPI支持概述
====================================

02 - 2012

1.什么是SPI?
------------
  "Serial Peripheral Interface" (SPI) 是同步四线串行接口,用于将微控制器连接到传感器,存储器和外围设备的链路。这是一个简单的“事实上的”标准,并不足以获得标准化机构。 SPI使用主/从配置

  三条信号线中有一个是时钟(SCK,通常在10 MHz的数量级),以及具有"Master Out, Slave In" (MOSI) 或 "Master In, Slave Out" (MISO) 信号的并行数据线。(也使用其他名称。)有四种时钟模式可以交换数据; mode-0 和mode-3 是最常用的。 每个时钟周期将数据移出并移入数据; 除非有数据位需要移位,否则时钟不会输出。
但并非所有数据位都被使用; 并非每个协议都使用这些全双工功能。

  SPI主控器使用第四条 "chip select" 线来激活给定的SPI从器件,因此这三条信号线(CLK,DI,DO)可以并联连接到多个芯片。所有SPI从设备都要支持芯片选择功能; 它们通常是低电平有效信号,为从设备'x'的片选线指定名字为nCSx(例如nCS0)。某些设备有其他信号,通常包括对主设备的中断。

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

    - SPI可用于请求/响应式设备协议,如触摸屏传感器和存储器芯片。

    - 它也可以用于在任一单方向传输数据(半双工)或两者双向同时(全双工)传输数据。

    - 某些设备可能使用8位字。 其它设备可能使用不同的字长,例如12位或20位数字样本的流。

    - 一个字节通常首先发送最高有效位(MSB),但也有时最先发送的是最低有效位(LSB)。

    - 有时SPI用于环形设备,如移位寄存器。

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

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

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

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

2. Who uses it? On what kinds of systems?
---------------------------------------
  使用SPI的Linux开发人员可能正在为嵌入式系统板编写设备驱动程序。SPI用于控制外部芯片,它也是每个MMC或SD存储卡支持的协议。(早期的“DataFlash”卡,使用相同的连接器和卡形状MMC卡,仅支持SPI。)某些PC硬件使用SPI闪存作为BIOS代码。

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

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

  许多可以运行Linux的微控制器将一个或多个I/O接口与SPI模式集成在一起。 在支持SPI的情况下,他们可以使用MMC或SD卡而无需专用的 MMC / SD / SDIO 控制器。

 

3. 我困惑了。 这四种SPI“时钟模式”是什么?
-----------------------------------------------------
  这里很容易混淆,你找到的供应商文档不一定有用。两个模式位组成了这四种模式:

    - CPOL表示初始时钟极性。 CPOL = 0表示时钟从低电平开始,因此第一个(前沿)边沿上升,第二个(后沿)边沿下降。 CPOL = 1表示时钟从高电平开始,因此第一个(前沿)边沿下降。

    - CPHA表示用于采样数据的时钟相位; CPHA = 0表示前沿的样本,CPHA = 1表示后沿。

  由于信号需要在采样之前稳定,因此CPHA = 0意味着其数据在第一个时钟沿之前写入半个时钟。 chipselect 可能使它变得可用。

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

  在SPI模式编号中,CPOL是高位,CPHA是低位。 因此,当芯片的时序图显示时钟从低电平开始(CPOL = 0)并且数据在后沿时钟边沿(CPHA = 1)期间稳定采样时,那就是SPI模式1。

  请注意,只要chipselect有效,时钟模式就会相关(同一SPI总线上连接的不同的从设备的SPI的模式可能不同,因为SPI主控制其的SPI模式应该也需要跟谁这片选线而变化)。 因此,主器器必须在选择从器件之前将时钟设置为无效,并且从器件可以通过在其选择线变为有效时对时钟电平进行采样来告知所选极性。 这就是为什么许多设备支持例如模式0和3的原因:它们不关心极性,并且总是在时钟上升沿上移入/移出数据。

4. 这些驱动程序编程接口如何工作?
------------------------------------------------
<linux/spi/spi.h>头文件包含kerneldoc,主源代码也是如此,你当然应该阅读内核API文档的那一章。 这只是一个概述,因此您可以在这些细节之前了解全局。

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

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

- 控制器驱动:Controllers 可以内置到 Soc 中,并且通常支持主从两个角色。 这些驱动程序触及硬件寄存器,可能使用DMA。 或者它们可以是PIO bitbangers,只需要GPIO引脚。其实也就是SPI主机控制器驱动。

- 协议驱动程序:它们通过控制器驱动程序传递消息,以便与SPI链路另一侧的从设备或主设备通信。其实也就是SPI设备驱动。

  因此,例如,一个spi设备驱动程序可能与MTD层通信以将数据导出到存储在SPI闪存上的文件系统,如DataFlash; 其他spi设备驱动可能控制音频接口,将触摸屏传感器作为输入接口,或在工业处理过程中监控温度和电压水平。 这些设备驱动可能共享相同的SPI主机控制器驱动程序。

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

  SPI核心的编程接口很少,侧重于使用驱动程序模型使用由板特定初始化代码提供的设备表来连接控制器驱动和设备驱动程序。SPI在几个位置的sysfs中显示:

/sys/devices/.../CTLR     给定SPI控制器的物理节点 
 
/sys/devices/.../CTLR/spiB.C    总线“B”上的spi_device,chipselect是C,通过CTLR访问。 
 
/sys/bus/spi/devices/spiB.C    符号链接到该物理设备/CTLR/spiB.C 
 
/sys/devices/.../CTLR/spiB.C/modalias    标识应该与此设备一起使用的驱动程序(用于hotplug/coldplug) 
 
/sys/bus/spi/drivers/D     一个或多个设备spi*.*的驱动程序 
 
/sys/class/spi_master/spiB    符号链接(或实际设备节点)到逻辑节点,该逻辑节点可以保持SPI主控制器管理 
总线“B”的类相关状态。 所有spiB.* 器件共享一个具有SCLK,MOSI和MISO的物理SPI总线。 
 
/sys/devices/.../CTLR/slave    用于为从控制器注册(或取消注册)一个从属设备的虚拟文件。 将SPI从设备驱动程序的 
名称写入此文件会注册从设备; 写 "(null)" 取消注册从设备。 从该文件读,显示从设备的名称(如果未注册,则为"(null)")。 
 
/sys/class/spi_slave/spiB    符号链接(或实际设备节点)到逻辑节点,该逻辑节点可以在总线“B”上保持SPI从控制器的类相 
关状态。 注册时,此处存在单个spiB.*设备,可能与其他SPI从设备共享物理SPI总线。

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

5. 板特定的init代码如何声明SPI设备?
------------------------------------------------------
Linux需要多种信息才能正确配置SPI设备。 该信息通常由特定于板的代码提供(引入设备树后就在设备树中提供),即使对于支持某些自动发现/枚举的芯片也是如此。

DECLARE CONTROLLERS

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

  平台通常会抽象 "register SPI controller" 操作,可能将它与代码耦合以初始化引脚配置,这样几个电路板的 arch/.../mach-*/board-*.c 文件都可以共享相同的基本控制器设置代码。 这是因为大多数SOC都有几个支持SPI的控制器,通常只能设置和注册实际可用于给定板的控制器。

So for example arch/.../mach-*/board-*.c files might have code like: 
 
    #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); 
        ... 
    } 
 
And SOC-specific utility code might look something like: 
 
    #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. 
             */ 
        } 
        ... 
    }

请注意,即使使用相同的SOC控制器,板的 platform_data 也可能不同。 例如,在一块板上,SPI可能使用外部时钟,而另一块板则从某些主时钟的当前设置中获取SPI时钟。

DECLARE SLAVE DEVICES

第二种信息是目标板上存在的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 位,因为在基础结构知道如何取消选择它之前,不可能与一个 "backwards" 解释chipselect的设备共享总线。

然后,您的电路板初始化代码将使用SPI基础结构注册该表,以便稍后在SPI主控制器驱动程序注册时可用:

spi_register_board_info(spi_board_info, ARRAY_SIZE(spi_board_info));

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

  广泛使用的 "card" 式计算机将内存,CPU和其他小东西捆绑到可能只有三十平方厘米的卡上。 在这样的系统上,你的 arch/.../mach-.../board-*.c 文件主要提供有关插入这种卡的主板上的设备的信息。 这肯定包括通过卡连接器连接的SPI设备!

NON-STATIC CONFIGURATIONS

  开发板通常采用与产品板不同的规则,一个例子是可能需要热插拔SPI器件和/或控制器。

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

  当Linux通过SPI支持 MMC / SD / SDIO / DataFlash 卡时,这些配置也将是动态的。 幸运的是,这些设备都支持基本的设备识别的probes,因此它们应该正常热插拔。

6. 如何编写“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, 
    };

驱动程序核心将自动尝试将此驱动程序绑定到其 board_info.modalias 等于 "CHIP" 的任何SPI设备。 除非您正在创建一个管理总线的设备(出现在 /sys/class/spi_master 下),否则您的 probe() 代码可能如下所示。

    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 请求的顺序;

    + 使用哪些 I/O 缓冲区... 每个 spi_transfer 为每个传输方向包装一个缓冲区,支持全双工(两个指针,在两种情况下可能是相同的)和半双工(一个指针为NULL)传输;

    + 可选择在传输后定义短延迟... 使用 spi_transfer.delay_usecs 设置(如果缓冲区长度为零,此延迟可能是唯一的协议效果);

    + 在传输和延迟后片选引脚是否变为非active状态... 通过 spi_transfer.cs_change 标志来设置; ##############

    + 暗示下一条消息是否可能传到同一设备... 在该原子组的最后一次传输中使用 spi_transfer.cs_change 标志,并可能节省芯片取消选择和选择操作的成本。

译者注:spi_message 是对 一个或多个 spi_transfer 的管理者,spi_message 里面有有一个链表,挂接多个 spi_transfer。一个 spi_message 是一次原子的硬件操作(CS拉低,传输数据,CS拉高)

  - 遵循标准内核规则,并在消息中提供DMA安全缓冲区。 这样,除非硬件需要,否则使用DMA的控制器驱动程序不会被强制执行额外的拷贝(例如,使用强制使用反弹缓冲的硬件勘误表)。

    如果标准的 dma_map_single() 处理这些缓冲区不合适的话,则可以使用 spi_message.is_dma_mapped 告诉控制器驱动程序您已经提供了相关的DMA地址。

  - 基本I / O原语是 spi_async()。 可以在任何上下文(irq处理程序,任务等)中发出异步请求,并使用随消息提供的回调来报告完成。 在检测到任何错误之后,取消选择芯片并中止该 spi_message 的处理。  

  - 还有像 spi_sync() 这样的同步包装器,以及 spi_read(),spi_write() 和 spi_write_then_read() 等包装器。这些只能在可以睡眠的上下文中调用,并且它们都是对 spi_async() 的包装。

  - spi_write_then_read() 调用及其周围的便利包装器应仅用于少量数据,其中可忽略额外拷贝操作的成本。它旨在支持常见的 RP C样式请求,例如写入8位命令和读取16位响应 - spi_w8r16()是其包装器之一,正是这样做的。

   某些驱动程序可能需要修改 spi_device 特性,如传输模式,字大小或时钟速率。这是通过 spi_setup() 完成的,通常在第一次 I/O 到设备完成之前在 probe() 中调用。 但是,也可以在任何时候调用,只要没有到该设备的pending的消息。

  虽然 “spi_device” 将是驱动程序的底部边界,但上边界可能包括sysfs(特别是传感器读数),input layer, ALSA, networking, MTD, 字符设备框架或其他Linux子系统。

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

    - I/O 缓冲区使用通常的Linux规则,并且必须是DMA安全的。 您通常会从堆或空闲页池中分配它们。 不要使用堆栈或任何声明为 "static" 的东西。

    - spi_message 和 spi_transfer 元数据被用于将这些 I/O 缓冲区粘合到一组协议事务中。这些可以在任何方便的地方分配,包括作为其他一次分配驱动程序数据结构的一部分。将这些结构初始化为零。

如果你愿意,spi_message_alloc() 和 spi_message_free()方便例程可用于分配和初始化具有多个传输的 spi_message。

 7. 如何编写“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协议(设备)驱动程序交互的三种方法。它还将初始化自己的内部状态。(见下面关于总线编号和那些方法。)

#define spi_master        spi_controller

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

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

BUS NUMBERING

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

  如果您没有这种硬件分配的总线编号,并且由于某种原因您不能分配它们,那么请提供负的总线编号。然后将由动态分配的号码替换。 然后,您需要将其视为非静态配置(请参见上文)。

SPI MASTER METHODS

master->setup(struct spi_device * spi)

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

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

** BUERT ALERT:由于某种原因,许多 spi_master 驱动程序的第一个版本似乎弄错了。 当您对 setup() 进行编码时,ASSUME 控制器正在主动处理另一个设备的传输。

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)

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

返回值:失败返回负的错误码,成功返回 0:传输完成,1:转移仍在进行中

DEPRECATED METHODS

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

  这一定不能睡眠。 它的职责是安排传输发生并发出 complete() 回调(也就意味着complete()回调不能休眠)。这两个通常会在其他传输完成后发生,如果控制器空闲,则需要启动。此方法不在排队控制器上使用,如果实现transfer_one_message() 和 (un)prepare_transfer_hardware(),则必须为NULL。

SPI MESSAGE QUEUE

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

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

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

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


本文参考链接:https://www.cnblogs.com/hellokitty2/p/11020568.html