22 Linux SPI 驱动

这里讨论Linux kernel SPI控制器驱动, 也即使用GPIO bitbang模拟SPI功能的驱动, 类似drivers/spi/spi-rockchip.c或者spi-imx.c等SPI Controller控制器驱动。

实际上在Kernel里已经有这样的驱动, 代码在drivers/spi/spi-gpio.c。 spi-gpio.c 是 Linux 内核中使用 GPIO 模拟 SPI 控制器的驱动。它实现了一种bit-banging 的方式,通过软件直接控制 GPIO 引脚来模拟 SPI 通信协议。这种方式常用于那些没有硬件 SPI 控制器的系统,或者那些需要使用特定 GPIO 引脚而非专用 SPI 总线的场景。该驱动实现了标准的 SPI 主控制器功能,允许 SPI 设备通过 GPIO 引脚进行通信。

22.1 DTS配置

由于是使用GPIO模拟SPI host controller, 故需要先配置DTS。 如下:

        demo_spi: spi@10 {
                compatible = "spi-gpio";
                sck-gpios  = <&gpio1 RK_PA2 0>;   /* SCLK */
                mosi-gpios = <&gpio1 RK_PA4 0>;   /* MOSI */
                miso-gpios = <&gpio2 RK_PB6 0>;   /* MISO */
                cs-gpios   = <&gpio1 RK_PB0 0>;   /* CS */
                pinctrl-names = "default";
                pinctrl-0 = <&demo_spi_pin>; 
                num-chipselects = <1>;
                spi-max-frequency = <1000000>; /* 1 MHz */

                #address-cells = <1>;            /* Define number of address cells (1 for chip select) */
                #size-cells = <0>;               /* Define number of size cells (0 for SPI devices) */

                /* Child node for spidev */
                spidev@0 {
                    compatible = "neardi,spidev";
                    reg = <0>;                     /* Chip Select 0 */
                    spi-max-frequency = <1000000>; /* Max frequency for the device */
                };
        };
&pinctrl {
        demo_spi {
                demo_spi_pin: demo_spi_pin {
                        rockchip,pins = <1 RK_PA2 RK_FUNC_GPIO &pcfg_pull_none>,
                                        <1 RK_PA4 RK_FUNC_GPIO &pcfg_pull_none>,
                                        <2 RK_PB6 RK_FUNC_GPIO &pcfg_pull_none>,
                                        <1 RK_PB0 RK_FUNC_GPIO &pcfg_pull_none>;
                        function = "spi";
                };
};

22.2 Kernel选项

在编译kernel时, 需要勾选如下配置:

  1. 启用 SPI 支持,SPI 子系统是 SPI 设备和控制器驱动工作的基础。
    Device Drivers  ---> 
       <*> SPI support  --->
  2. 勾选 GPIO 模拟 SPI 驱动 (spi-gpio.c)
    Device Drivers  ---> 
       <*> SPI support  --->
           <*>   GPIO-based bitbanging SPI master
  3. 启用 GPIO 支持
    Device Drivers  ---> 
       [*] GPIO Support  --->
           <*> GPIO Generic bitbanged SPI support
  4. 用户空间 SPI 驱动支持 (spidev)
    Device Drivers  ---> 
       <*> SPI support  --->
           <*>   User mode SPI device driver support

22.3 SPI Bit-banging 机制

在标准的 SPI 控制器中,硬件负责生成 SPI 时钟(SCK)并管理数据线(MOSI 和 MISO)。然而,在没有硬件 SPI 控制器的情况下,bit-banging 通过软件来模拟这些信号时序,手动控制 GPIO 引脚的电平以符合 SPI 协议规范

由前2章所描述, 典型的 SPI 通信包括:

  • SCK(Serial Clock):时钟信号,由主设备生成,控制数据传输的时序。

  • MOSI(Master Out Slave In):主设备发送数据给从设备。

  • MISO(Master In Slave Out):从设备发送数据给主设备。

  • CS(Chip Select):选择从设备的信号,低电平有效。

在 bit-banging 中,软件负责根据 SPI 协议的时序控制这些信号。如下代码所示:

static u32 spi_gpio_txrx_word_mode0(struct spi_device *spi,
		unsigned nsecs, u32 word, u8 bits, unsigned flags)
{
	return bitbang_txrx_be_cpha0(spi, nsecs, 0, flags, word, bits);
}

static u32 spi_gpio_txrx_word_mode1(struct spi_device *spi,
		unsigned nsecs, u32 word, u8 bits, unsigned flags)
{
	return bitbang_txrx_be_cpha1(spi, nsecs, 0, flags, word, bits);
}

static u32 spi_gpio_txrx_word_mode2(struct spi_device *spi,
		unsigned nsecs, u32 word, u8 bits, unsigned flags)
{
	return bitbang_txrx_be_cpha0(spi, nsecs, 1, flags, word, bits);
}

static u32 spi_gpio_txrx_word_mode3(struct spi_device *spi,
		unsigned nsecs, u32 word, u8 bits, unsigned flags)
{
	return bitbang_txrx_be_cpha1(spi, nsecs, 1, flags, word, bits);
}

22.4 驱动注册流程

spi-gpio.c 通过 Linux SPI 子系统进行集成,它会注册为一个 SPI 控制器,并通过 GPIO 来模拟 SPI 时钟、数据线和芯片选择线。

22.4.1 驱动注册流程概述

  • 分配 SPI 控制器: 驱动通过调用 devm_spi_alloc_master() 来分配一个 spi_master 结构体,该结构体表示一个 SPI 控制器。

  • 设置 SPI 控制器属性:初始化 spi_master 的各类属性,比如 SPI 模式支持、字长范围、总线号、设备的 setup 和 cleanup 方法等。

  • 配置 GPIO 引脚:驱动调用 spi_gpio_request() 等函数来配置 SPI 所需的 GPIO 引脚(SCK、MOSI、MISO、CS)。

  • 设置 Bit-bang 操作:驱动将具体的 GPIO 操作函数与 spi_bitbang 结构体相关联,定义如何通过软件控制 GPIO 引脚以进行 SPI 数据传输。

  • 初始化 Bit-bang 控制器:调用 spi_bitbang_init() 完成 bit-banging SPI 控制器的初始化。

  • 注册 SPI 控制器:最后调用 spi_register_master() 将模拟的 SPI 控制器注册到 Linux 内核中,使其能够被 SPI 子系统使用。

22.4.2 代码解析

static int spi_gpio_probe(struct platform_device *pdev)
{
	int				status;
	struct spi_master		*master;
	struct spi_gpio			*spi_gpio;
	struct device			*dev = &pdev->dev;
	struct spi_bitbang		*bb;

	master = devm_spi_alloc_master(dev, sizeof(*spi_gpio));
	if (!master)
		return -ENOMEM;

	if (pdev->dev.of_node)
		status = spi_gpio_probe_dt(pdev, master);
	else
		status = spi_gpio_probe_pdata(pdev, master);

	if (status)
		return status;

	spi_gpio = spi_master_get_devdata(master);

	status = spi_gpio_request(dev, spi_gpio);
	if (status)
		return status;

	master->bits_per_word_mask = SPI_BPW_RANGE_MASK(1, 32);
	master->mode_bits = SPI_3WIRE | SPI_3WIRE_HIZ | SPI_CPHA | SPI_CPOL |
			    SPI_CS_HIGH;
	if (!spi_gpio->mosi) {
		/* HW configuration without MOSI pin
		 *
		 * No setting SPI_MASTER_NO_RX here - if there is only
		 * a MOSI pin connected the host can still do RX by
		 * changing the direction of the line.
		 */
		master->flags = SPI_MASTER_NO_TX;
	}

	master->bus_num = pdev->id;
	master->setup = spi_gpio_setup;
	master->cleanup = spi_gpio_cleanup;

	bb = &spi_gpio->bitbang;
	bb->master = master;
	/*
	 * There is some additional business, apart from driving the CS GPIO
	 * line, that we need to do on selection. This makes the local
	 * callback for chipselect always get called.
	 */
	master->flags |= SPI_MASTER_GPIO_SS;
	bb->chipselect = spi_gpio_chipselect;
	bb->set_line_direction = spi_gpio_set_direction;

	if (master->flags & SPI_MASTER_NO_TX) {
		bb->txrx_word[SPI_MODE_0] = spi_gpio_spec_txrx_word_mode0;
		bb->txrx_word[SPI_MODE_1] = spi_gpio_spec_txrx_word_mode1;
		bb->txrx_word[SPI_MODE_2] = spi_gpio_spec_txrx_word_mode2;
		bb->txrx_word[SPI_MODE_3] = spi_gpio_spec_txrx_word_mode3;
	} else {
		bb->txrx_word[SPI_MODE_0] = spi_gpio_txrx_word_mode0;
		bb->txrx_word[SPI_MODE_1] = spi_gpio_txrx_word_mode1;
		bb->txrx_word[SPI_MODE_2] = spi_gpio_txrx_word_mode2;
		bb->txrx_word[SPI_MODE_3] = spi_gpio_txrx_word_mode3;
	}
	bb->setup_transfer = spi_bitbang_setup_transfer;

	status = spi_bitbang_init(&spi_gpio->bitbang);
	if (status)
		return status;

	return devm_spi_register_master(&pdev->dev, master);
}
  • devm_spi_alloc_master():分配 SPI 控制器结构 spi_master,并关联设备。

  • spi_gpio_request():请求 GPIO 资源,为 SPI 控制器的时钟、数据线和片选线分配 GPIO。

  • spi_bitbang_init():初始化 bit-banging,配置通过 GPIO 模拟 SPI 的操作。

  • devm_spi_register_master():将控制器注册到 SPI 核心层,使得内核可以通过该控制器管理 SPI 设备。

    22.5 数据传输与接口

    在 spi-gpio 中,数据传输是通过 bit-banging 实现的,SPI 控制器的 txrx_word 系列函数用于定义如何在不同 SPI 模式下传输数据。这些函数将通过软件模拟 SPI 时钟周期,并控制数据传输方向。

    static inline u32
    bitbang_txrx_be_cpha0(struct spi_device *spi,
                    unsigned nsecs, unsigned cpol, unsigned flags,
                    u32 word, u8 bits)
    {
            /* if (cpol == 0) this is SPI_MODE_0; else this is SPI_MODE_2 */
    
            u32 oldbit = (!(word & (1<<(bits-1)))) << 31;
            /* clock starts at inactive polarity */
            for (word <<= (32 - bits); likely(bits); bits--) {
    
                    /* setup MSB (to slave) on trailing edge */
                    if ((flags & SPI_MASTER_NO_TX) == 0) {
                            if ((word & (1 << 31)) != oldbit) {
                                    setmosi(spi, word & (1 << 31));
                                    oldbit = word & (1 << 31);
                            }
                    }
                    spidelay(nsecs);        /* T(setup) */
    
                    setsck(spi, !cpol);
                    spidelay(nsecs);
    
                    /* sample MSB (from slave) on leading edge */
                    word <<= 1;
                    if ((flags & SPI_MASTER_NO_RX) == 0)
                            word |= getmiso(spi);
                    setsck(spi, cpol);
            }
            return word;
    }

    该函数通过软件控制 GPIO 引脚的电平,模拟 SPI 时钟周期,并在每个周期中发送和接收一位数据。

22.6 SPI 设备控制

当 spi-gpio 被注册为 SPI 控制器后,应用程序可以通过标准的 SPI API 与设备通信。应用层通常会打开 /dev/spidevX.Y 设备接口,并通过 read() 和 write() 函数与 SPI 设备进行数据传输。

22.6.1 用户空间与 SPI 通信流程

  • 打开 SPI 设备: 用户空间应用可以通过打开 /dev/spidevX.Y 设备文件来访问 SPI 设备。
int fd = open("/dev/spidev0.0", O_RDWR);
  • 配置 SPI 参数: 使用 ioctl 设置 SPI 模式、位宽、时钟速度等。
int mode = SPI_MODE_0;
ioctl(fd, SPI_IOC_WR_MODE, &mode);
  • 读写数据: 使用 read() 和 write() 或 ioctl(SPI_IOC_MESSAGE) 与设备通信。
struct spi_ioc_transfer tr = {
    .tx_buf = (unsigned long)tx_buffer,
    .rx_buf = (unsigned long)rx_buffer,
    .len = sizeof(tx_buffer),
};
ioctl(fd, SPI_IOC_MESSAGE(1), &tr);

22.7 测试驱动

22.7.1 验证GPIO配置

重新编译kernel及下载到设备, 则可以查看gpio的配置信息, 有如下:

neardi@LPA3588:~$ sudo cat /sys/kernel/debug/gpio
gpiochip1: GPIOs 32-63, parent: platform/fec20000.gpio, gpio1:
 gpio-34  (                    |sck                 ) out lo
 gpio-36  (                    |mosi                ) out lo
 gpio-40  (                    |spi6 CS0            ) out hi ACTIVE LOW
 gpio-41  (                    |vcc-mipidphy1-regula) out lo
 gpio-42  (                    |vcc-mipidphy0-regula) out lo
 gpio-44  (                    |reset               ) out hi
 gpio-52  (                    |hp-det              ) in  hi ACTIVE LOW
 gpio-54  (                    |work1               ) out hi ACTIVE LOW
 gpio-58  (                    |vcc5v0_host_pwren   ) out hi
 gpio-61  (                    |hdmirx-det          ) in  hi ACTIVE LOW
 gpio-62  (                    |enable              ) out hi
 gpio-63  (                    |reset               ) out hi

gpiochip2: GPIOs 64-95, parent: platform/fec30000.gpio, gpio2:
 gpio-78  (                    |miso                ) in  hi
 gpio-81  (                    |enable              ) out hi

从上面信息可以看见成功地配置了SPI GPIO。

22.7.2 检查/dev/spidevX.Y

驱动运行成功后, 会创建/dev/spidevX.Y设备, X表示SPI bus号, Y表示此bus上面的设备号。 如下:

neardi@LPA3588:~$ ls /dev/spidev6.0 -l
crw------- 1 root root 153, 0 Sep 25 15:55 /dev/spidev6.0

这里的/dev/spidev6.0中的6, 表示SPI bus 6, 是kernel动态创建的, 与注册到kernel SPI核心的顺序有关。

22.7.3 测试App

创建一个测试程序, app.cpp, 代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>

#define DEVICE "/dev/spidev6.0"
#define READ_ID_CMD 0x9F // JEDEC ID read command

// configurate SPI mode, clock and word length.
int configure_spi(int fd, unsigned char mode, unsigned int speed, unsigned char bits_per_word) {
    if (ioctl(fd, SPI_IOC_WR_MODE, &mode) < 0) {
        perror("Unable to set SPI mode");
        return -1;
    }

    if (ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed) < 0) {
        perror("Unable to set SPI speed");
        return -1;
    }

    if (ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits_per_word) < 0) {
        perror("Unable to set SPI bits per word");
        return -1;
    }

    return 0;
}

// read SPI NOR Flash ID
int read_flash_id(int fd, unsigned char *rx_buf, size_t len) {
    unsigned char tx_buf[1];

    // constructure read ID command
    tx_buf[0] = READ_ID_CMD;

struct spi_ioc_transfer tr = {
        .tx_buf = (unsigned long)tx_buf,
        .rx_buf = (unsigned long)rx_buf,
        .len = (__u32)(len + 1), // 1 bye command
        .speed_hz = 0,
        .delay_usecs = 0,
        .bits_per_word = 0,
        .cs_change = 0
    };

    // send reading ID command and receive ID data
    if (ioctl(fd, SPI_IOC_MESSAGE(1), &tr) < 0) {
        perror("SPI transfer failed");
        return -1;
    }

    return 0;
}

int main() {
    int fd;
    unsigned char mode = SPI_MODE_0;
    unsigned int speed = 100000; // 500 kHz
    unsigned char bits = 8;
    unsigned char rx_buf[3] = {0}; // JEDEC ID 3 bytes

    // open SPI device
    fd = open(DEVICE, O_RDWR);
    if (fd < 0) {
        perror("Failed to open SPI device");
        return -1;
    }

    // config SPI 
    if (configure_spi(fd, mode, speed, bits) < 0) {
        close(fd);
        return -1;
    }

    // read SPI NOR Flash ID
    if (read_flash_id(fd, rx_buf, sizeof(rx_buf)) < 0) {
        close(fd);
        return -1;
    }

    // output
    printf("SPI NOR Flash ID:\n");
    printf("Manufacturer ID: 0x%02X\n", rx_buf[0]);
    printf("Memory Type ID: 0x%02X\n", rx_buf[1]);
    printf("Capacity ID: 0x%02X\n", rx_buf[2]);

    // close SPI
    close(fd);
    return 0;
}

编译及运行App, 使用示波器抓sck & mosi管脚波形, 如下:

22.8 总结

spi-gpio.c 驱动通过 bit-banging 模拟了一个 SPI 控制器,允许在没有硬件 SPI 控制器的系统上使用 GPIO 引脚进行 SPI 通信。它的注册流程包括初始化 spi_master 结构、配置 GPIO 引脚、设置传输函数,并最终注册到 SPI 核心层。应用程序通过标准的 /dev/spidev 接口与 SPI 设备通信。bit-banging 方法虽然效率不如硬件 SPI 控制器,但为灵活使用 GPIO 提供了便利的解决方案。

Linx 更改标题为「Linux SPI 驱动