Skip to content

Latest commit

 

History

History
779 lines (618 loc) · 28.2 KB

File metadata and controls

779 lines (618 loc) · 28.2 KB

八、串行接口设备驱动

串行外设接口 ( SPI )为(至少)四线总线- 主输入从输出 ( MISO )、主输出从输入 ( MOSI )、串行时钟 ( SCK )和芯片选择 ( CS ),用于连接串行闪存主机总是产生时钟。它的速度可以达到 80 兆赫,即使没有真正的速度限制(也比 I2C 快得多)。CS 线路也是如此,它始终由主机管理。

每个信号名称都有一个同义词:

  • 每当你看到 SIMO、SDI、DI 或 SDA,它们都指的是 MOSI。
  • SOMI、SDO、DO、SDA 将引用 MISO。
  • SCK、CLK、SCL 会提到 SCK。
  • S̅ S̅是从选择线,也称为 CS。也可以使用 CSx(其中 x 是索引,CS0,CS1),EN 和 ENB,表示启用。CS 通常是一个低电平有效信号:

SPI topology (image from wikipedia)

本章将介绍 SPI 驱动的概念,例如:

  • SPI 总线说明
  • 驱动架构和数据结构描述
  • 半双工和全双工数据发送和接收
  • 从 DT 声明 SPI 设备
  • 以半双工和全双工方式从用户空间访问 SPI 设备

驱动架构

Linux 内核中 SPI 素材所需的头文件是<linux/spi/spi.h>。在讨论驱动结构之前,让我们看看 SPI 设备是如何在内核中定义的。SPI 设备在内核中表示为spi_device的一个实例。管理它们的驱动的例子是struct spi_driver结构。

设备结构

struct spi_device结构代表一个 SPI 设备,在include/linux/spi/spi.h中定义:

struct spi_device { 
    struct devicedev; 
    struct spi_master*master; 
    u32 max_speed_hz; 
    u8 chip_select; 
    u8 bits_per_word; 
    u16 mode; 
    int irq; 
    [...] 
    int cs_gpio;        /* chip select gpio */ 
}; 

一些对我们没有意义的字段被删除了。也就是说,以下是结构中元素的含义:

  • master:表示设备连接的 SPI 控制器(总线)。
  • max_speed_hz:这是该芯片要使用的最大时钟速率(在当前板上);该参数可以在驱动中更改。您可以在每次传输时使用spi_transfer.speed_hz覆盖该参数。稍后我们将讨论 SPI 传输。
  • chip_select:这可以让你启用需要通话的芯片,区分主控处理的芯片。chip_select默认为低电平有效。通过添加SPI_CS_HIGH标志,可以在模式中更改此行为。
  • mode:这定义了数据应该如何计时。设备驱动可能会对此进行更改。默认情况下,传输中每个字的数据时钟首先是最高有效位 ( MSB )。可以通过指定SPI_LSB_FIRST来覆盖该行为。
  • irq:这表示中断号(在你的板init文件或通过 DT 注册为设备资源)你应该传递给request_irq()从这个设备接收中断。

关于 SPI 模式的一句话;它们有两个特点:

  • CPOL:这是初始时钟极性:
    • 0:初始时钟状态为低电平,第一个边沿上升
    • 1:初始时钟状态高,第一个状态下降
  • CPHA:这是时钟相位,选择在哪个边沿采样数据:
    • 0:数据在下降沿锁存(从高电平到低电平转换),而输出在上升沿改变
    • 1:数据在上升沿锁存(从低到高转换),并在下降沿输出

这允许四种 SPI 模式,这些模式根据include/linux/spi/spi.h中的以下宏在内核中定义:

#define  SPI_CPHA  0x01 
#define  SPI_CPOL  0x02 

然后,您可以生成以下数组来总结事情:

| 模式 | CPOL | CPHA | 内核宏 | | Zero | Zero | Zero | #define SPI_MODE_0 (0&#124;0) | | one | Zero | one | #define SPI_MODE_1 (0&#124;SPI_CPHA) | | Two | one | Zero | #define SPI_MODE_2 (SPI_CPOL&#124;0) | | three | one | one | #define SPI_MODE_3 (SPI_CPOL&#124;SPI_CPHA) |

下面是前面数组中定义的每个 SPI 模式的表示。也就是说,只表示了 MOSI 线,但是 MISO 的原理是相同的:

常用的模式有SPI_MODE_0SPI_MODE_3

spi_driver 结构

struct spi_driver代表您为管理 SPI 设备而开发的驱动。其结构如下:

struct spi_driver { 
   const struct spi_device_id *id_table; 
   int (*probe)(struct spi_device *spi); 
   int (*remove)(struct spi_device *spi); 
   void (*shutdown)(struct spi_device *spi); 
   struct device_driver    driver; 
}; 

探头()功能

其原型如下:

static int probe(struct spi_device *spi) 

您可以参考第 7 章I2C 客户端驱动来了解在probe功能中要做什么。同样的步骤也适用于此。因此,与不能在运行时更改控制器总线参数(CS 状态、每个字的位、时钟)的 I2C 驱动不同,SPI 驱动可以。您可以根据设备属性设置总线。

典型的 SPI probe函数如下所示:

static int my_probe(struct spi_device *spi) 
{ 
    [...] /* declare your variable/structures */ 

    /* bits_per_word cannot be configured in platform data */ 
    spi->mode = SPI_MODE_0; /* SPI mode */ 
    spi->max_speed_hz = 20000000;   /* Max clock for the device */ 
    spi->bits_per_word = 16;    /* device bit per word */ 
    ret = spi_setup(spi); 
    ret = spi_setup(spi); 
    if (ret < 0) 
        return ret; 

    [...] /* Make some init */ 
    [...] /* Register with apropriate framework */ 

    return ret; 
} 

struct spi_device*是输入参数,由内核赋予probe函数。它代表您正在探测的设备。在您的probe功能中,您可以使用spi_get_device_id(如果是id_table match)获得触发比赛的spi_device_id,并提取驾驶员数据:

const struct spi_device_id *id = spi_get_device_id(spi); 
my_private_data = array_chip_info[id->driver_data]; 

每个设备的数据

probe功能中,跟踪要在模块生命周期内使用的私有(每个设备)数据是一项常见任务。这已经在第 7 章I2C 客户端驱动中讨论过。

以下是用于设置/获取每台设备数据的功能原型:

/* set the data */ 
void spi_set_drvdata(struct *spi_device, void *data); 

/* Get the data back */ 
 void *spi_get_drvdata(const struct *spi_device); 

例如:

struct mc33880 { 
    struct mutex    lock; 
    u8      bar; 
    struct foo chip; 
    struct spi_device *spi; 
}; 

static int mc33880_probe(struct spi_device *spi) 
{ 
    struct mc33880 *mc; 
    [...] /* Device set up */ 

    mc = devm_kzalloc(&spi->dev, sizeof(struct mc33880), 
                      GFP_KERNEL); 
    if (!mc) 
        return -ENOMEM; 

    mutex_init(&mc->lock); 
    spi_set_drvdata(spi, mc); 

    mc->spi = spi; 
    mc->chip.label = DRIVER_NAME, 
    mc->chip.set = mc33880_set; 

    /* Register with appropriate framework */ 
    [...] 
} 

移除()函数

remove功能必须释放在probe功能中抓取的每一个资源。其结构如下:

static int  my_remove(struct spi_device *spi); 

典型的remove功能可能如下所示:

static int mc33880_remove(struct spi_device *spi) 
{ 
    struct mc33880 *mc; 
    mc = spi_get_drvdata(spi); /* Get our data back */ 
    if (!mc) 
        return -ENODEV; 

    /* 
     * unregister from frameworks with which we registered in the 
     * probe function 
     */ 
    [...] 
    mutex_destroy(&mc->lock); 
    return 0; 
} 

驱动初始化和注册

对于坐在总线上的设备,无论是物理总线还是伪平台总线,大多数时候都是在probe功能中完成的。initexit功能只是用于向总线核心注册/注销驱动:

static int __init foo_init(void) 
{ 
    [...] /*My init code */ 
   return spi_register_driver(&foo_driver); 
} 
module_init(foo_init); 

static void __exit foo_cleanup(void) 
{ 
    [...] /* My clean up code */ 
   spi_unregister_driver(&foo_driver); 
} 
module_exit(foo_cleanup); 

也就是说,如果您除了注册/注销驱动之外不做任何其他事情,内核会提供一个宏:

module_spi_driver(foo_driver); 

这将在内部调用spi_register_driverspi_unregister_driver。这和我们在上一章看到的完全一样。

驱动和设备供应

由于 I2C 设备需要i2c_device_id,我们必须将spi_device_id用于 SPI 设备,以便提供一个device_id阵列来匹配我们的设备。在include/linux/mod_devicetable.h中定义:

struct spi_device_id { 
   char name[SPI_NAME_SIZE]; 
   kernel_ulong_t driver_data; /* Data private to the driver */ 
}; 

我们需要将我们的数组嵌入到struct spi_device_id中,以便通知 SPI 内核我们需要在驱动中管理的设备 ID,并在驱动结构上调用MODULE_DEVICE_TABLE宏。当然,宏的第一个参数是设备所在总线的名称。在我们的例子中,它是 SPI:

#define ID_FOR_FOO_DEVICE  0 
#define ID_FOR_BAR_DEVICE  1  

static struct spi_device_id foo_idtable[] = { 
   { "foo", ID_FOR_FOO_DEVICE }, 
   { "bar", ID_FOR_BAR_DEVICE }, 
   { } 
}; 
MODULE_DEVICE_TABLE(spi, foo_idtable); 

static struct spi_driver foo_driver = { 
   .driver = { 
   .name = "KBUILD_MODULE", 
   }, 

   .id_table    = foo_idtable, 
   .probe       = foo_probe, 
   .remove      = foo_remove, 
}; 

module_spi_driver(foo_driver); 

在电路板配置文件中实例化 SPI 器件–旧的和折旧的方式

只有当系统不支持设备树时,才应该在板文件中实例化设备。既然设备树已经出现,这种实例化的方法就不推荐使用了。因此,让我们记住板文件驻留在arch/目录中。用来表示 SPI 设备的结构是struct spi_board_info,而不是我们在驱动中使用的struct spi_device。只有当您使用spi_register_board_info函数填充并注册了struct spi_board_info时,内核才会构建一个struct spi_device(它将被传递给您的驱动并注册到 SPI 内核)。

请随意查看include/linux/spi/spi.h中的struct spi_board_info字段。spi_register_board_info的定义可以在drivers/spi/spi.c中找到。现在,让我们看一下主板文件中的一些 SPI 器件注册:

/** 
 * Our platform data 
 */ 
struct my_platform_data { 
   int foo; 
   bool bar; 
}; 
static struct my_platform_data mpfd = { 
   .foo = 15, 
   .bar = true, 
}; 

static struct spi_board_info 
   my_board_spi_board_info[] __initdata = { 
    { 
       /* the modalias must be same as spi device driver name */ 
        .modalias = "ad7887", /* Name of spi_driver for this device */ 
        .max_speed_hz = 1000000,  /* max spi clock (SCK) speed in HZ */ 
        .bus_num = 0, /* Framework bus number */ 
        .irq = GPIO_IRQ(40), 
        .chip_select = 3, /* Framework chip select */ 
        .platform_data = &mpfd, 
        .mode = SPI_MODE_3, 
   },{ 
        .modalias = "spidev", 
        .chip_select = 0, 
        .max_speed_hz = 1 * 1000 * 1000, 
        .bus_num = 1, 
        .mode = SPI_MODE_3, 
    }, 
}; 

static int __init board_init(void) 
{ 
   [...] 
   spi_register_board_info(my_board_spi_board_info, ARRAY_SIZE(my_board_spi_board_info)); 
   [...] 

   return 0; 
} 
[...] 

SPI 和设备树

与 I2C 器件一样,SPI 器件属于 DT 中的非存储器映射器件系列,但也是可寻址的。这里,地址是指给控制器(主机)的 CS 列表(从 0 开始)中的 CS 索引。例如,我们可能有三个不同的 SPI 设备位于 SPI 总线上,每个设备都有自己的 CS 线路。主机将获得一组 GPIO,每个 GPIO 代表一个 CS 来激活一个设备。如果设备 X 使用第二条 GPIO 线作为 CS,我们必须在reg属性中将其地址设置为 1(因为我们总是从 0 开始)。

以下是 SPI 设备的真实 DT 列表:

ecspi1 { 
    fsl,spi-num-chipselects = <3>; 
    cs-gpios = <&gpio5 17 0>, <&gpio5 17 0>, <&gpio5 17 0>; 
    pinctrl-0 = <&pinctrl_ecspi1 &pinctrl_ecspi1_cs>; 
    #address-cells = <1>; 
    #size-cells = <0>; 
    compatible = "fsl,imx6q-ecspi", "fsl,imx51-ecspi"; 
    reg = <0x02008000 0x4000>; 
    status = "okay"; 

    ad7606r8_0: ad7606r8@0 { 
        compatible = "ad7606-8"; 
        reg = <0>; 
        spi-max-frequency = <1000000>; 
        interrupt-parent = <&gpio4>; 
        interrupts = <30 0x0>; 
   }; 
   label: fake_spi_device@1 { 
        compatible = "packtpub,foobar-device"; 
        reg = <1>; 
        a-string-param = "stringvalue"; 
        spi-cs-high; 
   }; 
   mcp2515can: can@2 { 
        compatible = "microchip,mcp2515"; 
        reg = <2>; 
        spi-max-frequency = <1000000>; 
        clocks = <&clk8m>; 
        interrupt-parent = <&gpio4>; 
        interrupts = <29 IRQ_TYPE_LEVEL_LOW>; 
    }; 
}; 

SPI 设备节点引入了一个新属性:spi-max-frequency。它代表器件的最大 SPI 时钟速度,单位为赫兹。每当您访问该设备时,总线控制器驱动将确保时钟不会超过该限制。其他常用的属性有:

  • spi-cpol:这是一个布尔值(空属性),表示设备需要反时钟极性模式。它与 CPOL 相对应。
  • spi-cpha:这是一个空属性,表示设备需要移位时钟相位模式。它与 CPHA 相对应。
  • spi-cs-high:默认情况下,SPI 器件要求 CS 低电平有效。这是一个布尔属性,表示设备要求 CS 高电平有效。

也就是说,关于 SPI 绑定元素的完整列表,可以参考内核源码中的Documentation/device tree/bindings/SPI/SPI-bus . txt

在设备树中实例化 SPI 设备——新方法

通过在 DT 中适当填充我们的设备节点,内核将为我们构建一个struct spi_device,并将其作为参数提供给我们的 SPI 核心功能。以下只是之前定义的 SPI DT 列表的摘录:

&ecspi1 { 
    status = "okay"; 
    label: fake_spi_device@1 { 
    compatible = "packtpub,foobar-device"; 
    reg = <1>; 
    a-string-param = "stringvalue"; 
    spi-cs-high; 
   }; 
 }; 

定义和注册 SPI 驱动

同样,这一原则与 I2C 司机的原则相同。我们需要定义一个struct of_device_id来匹配 DT 中的设备,并调用MODULE_DEVICE_TABLE宏来注册 OF 核心:

static const struct of_device_id foobar_of_match[] = { 
           { .compatible = "packtpub,foobar-device" }, 
           { .compatible = "packtpub,barfoo-device" }, 
        {} 
}; 
MODULE_DEVICE_TABLE(of, foobar_of_match); 

然后将我们的spi_driver定义如下:

static struct spi_driver foo_driver = { 
    .driver = { 
    .name   = "foo", 
        /* The following line adds Device tree */ 
    .of_match_table = of_match_ptr(foobar_of_match), 
    }, 
    .probe   = my_spi_probe, 
    .id_table = foo_id, 
}; 

然后,您可以这样改进probe功能:

static int my_spi_probe(struct spi_device *spi) 
{ 
    const struct of_device_id *match; 
    match = of_match_device(of_match_ptr(foobar_of_match), &spi->dev); 
    if (match) { 
        /* Device tree code goes here */ 
    } else { 
        /*  
         * Platform data code comes here. 
         * One can use 
         *   pdata = dev_get_platdata(&spi->dev); 
         * 
         * or *id*, which is a pointer on the *spi_device_id* entry that originated 
         * the match, in order to use *id->driver_data* to extract the device 
         * specific data, as described in Chapter 5, Platform Device Drivers. 
         */ 
    } 
    [...] 
} 

访问客户并与之交谈

SPI 输入/输出模型由一组排队的消息组成。我们提交一个或多个struct spi_message结构,同步或异步处理和完成。单个消息由一个或多个structspi_transfer对象组成,每个对象代表一个全双工 SPI 传输。这是驱动和设备之间交换数据的两种主要结构。它们在include/linux/spi/spi.h中都有定义:

SPI message structure

struct spi_transfer代表全双工 SPI 传输:

struct spi_transfer { 
    const void  *tx_buf; 
    void *rx_buf; 
    unsigned len; 

    dma_addr_t tx_dma; 
    dma_addr_t rx_dma; 

    unsigned cs_change:1; 
    unsigned tx_nbits:3; 
    unsigned rx_nbits:3; 
#define  SPI_NBITS_SINGLE   0x01 /* 1bit transfer */ 
#define  SPI_NBITS_DUAL            0x02 /* 2bits transfer */ 
#define  SPI_NBITS_QUAD            0x04 /* 4bits transfer */ 
    u8 bits_per_word; 
    u16 delay_usecs; 
    u32 speed_hz; 
}; 

以下是结构元素的含义:

  • tx_buf:该缓冲区包含要写入的数据。在只读事务的情况下,它应该为空或保持原样。在需要通过直接内存访问 ( DMA )执行 SPI 事务的情况下,应该是dma-安全的。
  • rx_buf:这是一个用于数据读取的缓冲区(属性与tx_buf相同),或者在只写事务中为空。
  • tx_dma:这是tx_buf的 DMA 地址,以防spi_message.is_dma_mapped设置为1。直接存储器存取在第 12 章直接存储器存取中讨论。
  • rx_dma:这个和tx_dma一样,但是对于rx_buf来说。
  • len:这表示rxtx缓冲区的大小(以字节为单位),这意味着如果两者都使用,它们必须具有相同的大小。
  • speed_hz:这将覆盖spi_device.max_speed_hz中指定的默认速度,但仅适用于当前传输。如果是0,则使用默认值(在struct spi_device结构中提供)。
  • bits_per_word:数据传输涉及一个或多个字。一个字是一个数据单位,它的比特大小可以根据需要而变化。这里,bits_per_word表示该 SPI 传输的字的大小(以位为单位)。这将覆盖spi_device.bits_per_word中提供的默认值。如果是0,则使用默认值(来自spi_device)。
  • cs_change:这决定了本次转移完成后chip_select线的状态。
  • delay_usecs:这表示在(可选地)改变chip_select状态,然后开始下一次传输或完成本次spi_message之前,本次传输后的延迟(以微秒计)。

在另一侧,struct spi_message被原子性地用于包装一个或多个 SPI 传输。所使用的 SPI 总线将被驱动占用,直到构成消息的每个传输完成。include/linux/spi/spi.h中也定义了 SPI 消息结构:

    struct spi_message { 
       struct list_head transfers; 
       struct spi_device *spi; 
       unsigned is_dma_mapped:1; 
       /* completion is reported through a callback */ 
       void (*complete)(void *context); 
       void *context; 
       unsigned frame_length; 
       unsigned actual_length; 
       int status; 
    }; 
  • transfers:这是构成消息的传输列表。我们将在后面看到如何向该列表添加转移。
  • is_dma_mapped:这通知控制器是否使用 DMA(或不使用)来执行事务。然后,您的代码负责为每个传输缓冲区提供 DMA 和 CPU 虚拟地址。
  • complete:这是交易完成时调用的回调,context是要给回调的参数。
  • frame_length:这将根据消息中的字节总数自动设置。
  • actual_length:这是所有成功段中传输的字节数。
  • status:报告转账状态。零上成功,否则-errno

spi_transfer elements in a message are processed in a FIFO order. Until the message is completed, you have to make sure not to use transfer buffer, in order to avoid data corruption. You make completion call to make sure one can.

在消息被提交到总线之前,它必须用void spi_message_init(struct spi_message *message),初始化,这将使结构中的每个元素归零,并初始化transfers列表。对于要添加到消息中的每个转接,您应该在该转接上呼叫void spi_message_add_tail(struct spi_transfer *t, struct spi_message *m),这将导致该转接进入transfers列表。完成后,您有两个选择来开始交易:

  • 同时,使用int spi_sync(struct spi_device *spi, struct spi_message *message)功能,该功能可以休眠,并且不在中断上下文中使用。这里不需要完成回调。这个函数是第二个函数(spi_async())的包装器。
  • 异步使用spi_async()函数,该函数也可以在原子上下文中使用,其原型是int spi_async(struct spi_device *spi, struct spi_message *message)。最好在这里提供回调,因为它将在消息完成时执行。

以下是单个传输 SPI 消息事务可能的样子:

char tx_buf[] = { 
        0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 
        0xFF, 0x40, 0x00, 0x00, 0x00, 
        0x00, 0x95, 0xEF, 0xBA, 0xAD, 
        0xF0, 0x0D, 
}; 

char rx_buf[10] = {0,}; 
int ret; 
struct spi_message single_msg; 
struct spi_transfer single_xfer; 

single_xfer.tx_buf = tx_buf; 
single_xfer.rx_buf = rx_buf; 
single_xfer.len    = sizeof(tx_buff); 
single_xfer.bits_per_word = 8; 

spi_message_init(&msg); 
spi_message_add_tail(&xfer, &msg); 
ret = spi_sync(spi, &msg); 

现在让我们编写一个多转移消息事务:

struct { 
    char buffer[10]; 
    char cmd[2] 
    int foo; 
} data; 

struct data my_data[3]; 
initialize_date(my_data, ARRAY_SIZE(my_data)); 

struct spi_transfer   multi_xfer[3]; 
struct spi_message    single_msg; 
int ret; 

multi_xfer[0].rx_buf = data[0].buffer; 
multi_xfer[0].len = 5; 
multi_xfer[0].cs_change = 1; 
/* command A */ 
multi_xfer[1].tx_buf = data[1].cmd; 
multi_xfer[1].len = 2; 
multi_xfer[1].cs_change = 1; 
/* command B */ 
multi_xfer[2].rx_buf = data[2].buffer; 
multi_xfer[2].len = 10; 

spi_message_init(single_msg); 
spi_message_add_tail(&multi_xfer[0], &single_msg); 
spi_message_add_tail(&multi_xfer[1], &single_msg); 
spi_message_add_tail(&multi_xfer[2], &single_msg); 
ret = spi_sync(spi, &single_msg); 

还有其他辅助功能,都是围绕spi_sync()构建的。其中一些是:

int spi_read(struct spi_device *spi, void *buf, size_t len) 
int spi_write(struct spi_device *spi, const void *buf, size_t len) 
int spi_write_then_read(struct spi_device *spi, 
        const void *txbuf, unsigned n_tx, 
void *rxbuf, unsigned n_rx) 

请看include/linux/spi/spi.h查看完整列表。这些包装器应该用于少量数据。

把它们放在一起

编写 SPI 客户端驱动所需的步骤如下:

  1. 声明驱动支持的设备标识。你可以使用spi_device_id来完成。如果支持 DT,也可以使用of_device_id。你可以独家使用 DT。
  2. 调用MODULE_DEVICE_TABLE(spi, my_id_table);向 SPI 内核注册您的设备列表。如果支持 DT,您必须调用MODULE_DEVICE_TABLE(of, your_of_match_table);of核注册您的设备列表。
  3. 根据各自的原型写出proberemove函数。probe功能必须识别您的设备,对其进行配置,定义每设备(私有)数据,使用spi_setup功能在需要时配置总线(SPI 模式等),并向适当的内核框架注册。在remove功能中,只需撤销在probe功能中所做的一切。
  4. 声明并填充一个struct spi_driver结构,用您创建的标识数组设置id_table字段。用你所写的相应函数的名称设置.probe.remove字段。在.driver子结构中,将.owner字段设置为THIS_MODULE,设置驱动名称,最后设置.of_match_table字段,数组为of_device_id,如果支持 DT。
  5. 用你刚才在module_spi_driver(serial_eeprom_spi_driver);之前填充的spi_driver结构调用module_spi_driver函数,以便向内核注册你的驱动。

SPI 用户模式驱动

用户模式 SPI 设备驱动有两种使用方式。要做到这一点,您需要使用spidev驱动启用您的设备。一个例子如下:

spidev@0x00 { 
    compatible = "spidev"; 
    spi-max-frequency = <800000>; /* It depends on your device */ 
    reg = <0>; /* correspond tochipselect 0 */ 
}; 

您可以调用读/写函数或ioctl()。通过调用读/写,您一次只能读或写。如果你需要全双工读写,你必须使用输入输出控制 ( ioctl )命令。提供了两者的示例。这是读/写示例。您可以使用平台的交叉编译器或主板上的本机编译器进行编译:

#include <stdio.h> 
#include <fcntl.h> 
#include <stdlib.h> 

int main(int argc, char **argv)  
{ 
   int i,fd; 
   char wr_buf[]={0xff,0x00,0x1f,0x0f}; 
   char rd_buf[10];  

   if (argc<2) { 
         printf("Usage:\n%s [device]\n", argv[0]); 
         exit(1); 
   } 

   fd = open(argv[1], O_RDWR); 
   if (fd<=0) {  
         printf("Failed to open SPI device %s\n",argv[1]); 
         exit(1); 
   } 

   if (write(fd, wr_buf, sizeof(wr_buf)) != sizeof(wr_buf)) 
         perror("Write Error"); 
   if (read(fd, rd_buf, sizeof(rd_buf)) != sizeof(rd_buf)) 
         perror("Read Error"); 
   else 
         for (i = 0; i < sizeof(rd_buf); i++) 
             printf("0x%02X ", rd_buf[i]); 

   close(fd); 
   return 0; 
} 

使用 IOCTL

使用 IOCTL 的好处是可以全双工工作。你能找到的最好的例子就是documentation/spi/spidev_test.c,当然是在内核源码树中。

也就是说,前面使用读/写的示例没有改变任何 SPI 配置。但是,内核向用户空间公开了一组 IOCTL 命令,您可以使用这些命令来根据需要设置总线,就像在 DT 中所做的那样。以下示例显示了如何更改总线设置:

 #include <stdint.h> 
 #include <unistd.h> 
 #include <stdio.h> 
 #include <stdlib.h> 
 #include <string.h> 
 #include <fcntl.h> 
 #include <sys/ioctl.h> 
 #include <linux/types.h> 
 #include <linux/spi/spidev.h> 
static int pabort(const char *s) 
{ 
    perror(s); 
    return -1; 
} 

static int spi_device_setup(int fd) 
{ 
    int mode, speed, a, b, i; 
    int bits = 8; 

    /* 
     * spi mode: mode 0 
     */ 
    mode = SPI_MODE_0; 
    a = ioctl(fd, SPI_IOC_WR_MODE, &mode); /* write mode */ 
    b = ioctl(fd, SPI_IOC_RD_MODE, &mode); /* read mode */ 
    if ((a < 0) || (b < 0)) { 
        return pabort("can't set spi mode"); 
    } 

    /* 
     * Clock max speed in Hz 
     */ 
    speed = 8000000; /* 8 MHz */ 
    a = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed); /* Write speed */ 
    b = ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed); /* Read speed */ 
    if ((a < 0) || (b < 0)) { 
        return pabort("fail to set max speed hz"); 
    } 

    /* 
     * setting SPI to MSB first.  
     * Here, 0 means "not to use LSB first". 
     * In order to use LSB first, argument should be > 0 
     */ 
    i = 0; 
    a = ioctl(dev, SPI_IOC_WR_LSB_FIRST, &i); 
    b = ioctl(dev, SPI_IOC_RD_LSB_FIRST, &i); 
    if ((a < 0) || (b < 0)) { 
        pabort("Fail to set MSB first\n"); 
    } 

    /* 
     * setting SPI to 8 bits per word 
     */ 
    bits = 8; 
    a = ioctl(dev, SPI_IOC_WR_BITS_PER_WORD, &bits); 
    b = ioctl(dev, SPI_IOC_RD_BITS_PER_WORD, &bits); 
    if ((a < 0) || (b < 0)) { 
        pabort("Fail to set bits per word\n"); 
    } 

    return 0; 
} 

有关 spidev ioctl 命令的更多信息,您可以查看文档/spi/spidev 。当需要通过总线发送数据时,您可以使用SPI_IOC_MESSAGE(N)请求,该请求提供全双工访问,以及无需 chipselect 去激活的复合操作,从而提供多传输支持。它相当于内核spi_sync()。这里的转移被表示为struct spi_ioc_transfer的一个实例,它相当于内核struct spi_transfer,其定义可以在include/uapi/linux/spi/spidev.h中找到。以下是用法示例:

static void do_transfer(int fd) 
{ 
    int ret; 
    char txbuf[] = {0x0B, 0x02, 0xB5}; 
    char rxbuf[3] = {0, }; 
    char cmd_buff = 0x9f; 

    struct spi_ioc_transfer tr[2] = { 
        0 = { 
            .tx_buf = (unsigned long)&cmd_buff, 
            .len = 1, 
            .cs_change = 1; /* We need CS to change */ 
            .delay_usecs = 50, /* wait after this transfer */ 
            .bits_per_word = 8, 
        }, 
        [1] = { 
            .tx_buf = (unsigned long)tx, 
            .rx_buf = (unsigned long)rx, 
            .len = txbuf(tx), 
            .bits_per_word = 8, 
        }, 
    }; 

    ret = ioctl(fd, SPI_IOC_MESSAGE(2), &tr); 
    if (ret == 1){ 
        perror("can't send spi message"); 
        exit(1); 
    } 

    for (ret = 0; ret < sizeof(tx); ret++) 
        printf("%.2X ", rx[ret]); 
    printf("\n"); 
} 

int main(int argc, char **argv) 
{ 
    char *device = "/dev/spidev0.0"; 
    int fd; 
    int error; 

    fd = open(device, O_RDWR); 
    if (fd < 0) 
        return pabort("Can't open device "); 

    error = spi_device_setup(fd); 
    if (error) 
        exit (1); 

    do_transfer(fd); 

    close(fd); 
    return 0; 
} 

摘要

我们刚刚处理了 SPI 驱动,现在可以利用这种更快的串行(和全双工)总线。我们通过 SPI 完成了数据传输,这是最重要的部分。也就是说,您可能需要更多的抽象,以便不去打扰 SPI 或 I2C API。这就是下一章的内容,涉及到 Regmap API,它提供了一个更高和统一的抽象层次,因此 SPI(或 I2C)命令对您来说将变得透明。