Skip to main content

SPI

本章介绍 SPI 控制器的相关内容,面向使用 SPI 模块做驱动开发或者用户态程序开发或维护人员。具体包括设备能力,驱动和用户态程序接口,配置流程以及核心参考代码。

概述

SPI(Serial Peripheral Interface,串行外设接口)是一种同步串行通信协议,主要用于短距离设备间的高速数据交换。常见应用包括微控制器与传感器、存储器、显示屏等外设之间的通信。

SPI 采用主从架构,通过四根信号线实现全双工通信:

  • SCK:时钟信号(由主设备提供)
  • MOSI:主设备输出,从设备输入
  • MISO:主设备输入,从设备输出
  • CS_N:片选信号(低电平有效)

SPI 不具备寻址机制,通信依赖硬件片选,具备以下特点:

  • 硬件结构简单
  • 无需握手机制
  • 传输速率高(可达数十 MHz 乃至百 MHz 级)
  • 广泛应用于 EEPROM、LCD、ADC、触控芯片等嵌入式场景

关键特性

  • SPI 通道:系统共支持 6 路 SPI(SPI0–SPI5
    • SPI0–SPI4 位于非安全空间,可由普通应用访问,支持通用 SPI 功能
    • SPI5 位于安全空间,仅供安全模块调用,通常用于控制板载关键芯片,如 PMIC、电源芯片、RTC 等
  • 支持主模式:所有 SPI 控制器均支持主模式通信
  • 通信引脚:使用 SCKCS_NMISOMOSI 四根基本信号线
  • 频率范围SCK 时钟频率范围为 0–50 MHz,常见值为 25 MHz 和 50 MHz
  • 数据传输方式:支持中断模式传输(不支持 DMA
  • FIFO 缓冲:每通道具有 256-word(32-bit 宽)FIFO 缓冲区

内核文件

SPI 涉及到的内核文件包含以下几个:

文件路径用途说明
linux-mthreads/drivers/spi/spi-dw-core.cDesignWare SPI 模块的核心处理文件
linux-mthreads/drivers/spi/spi-dw-m1000.cSPI0–SPI4 模块初始化文件
linux-mthreads/drivers/spi/spi-dw-sm-ss.cSPI5 模块初始化文件(仅供测试使用,非必要不建议在内核中使用 SPI5)
linux-mthreads/drivers/spi/spi.cSPI 子系统的通用程序接口
linux-mthreads/drivers/spi/spidev.cSPI 驱动的用户接口,生成字符设备节点供用户态访问
linux-mthreads/tools/spi/spidev_test.cSPI 用户态测试示例程序

以用户态程序为例:

  • 用户的 spidev_test.c 通过 /dev 目录下的 spidev 字符设备节点与内核的 spidev.c 进行通信。
  • 内核中的 spidev.c 调用 spi.c 提供的通用 SPI 接口进行数据读写操作。
  • spi.c 根据初始化时注册的 spi-dw-core.c 中的函数,完成硬件的初始化和调度。
  • spi-dw-core.c 最终调用对应的硬件相关实现文件 spi-dw-m1000.c,执行具体的硬件层面数据读写操作.

API 接口

内核态核心接口

在编写 SPI device 驱动的时候,先保证打开对应的 config 配置文件和设备树相关说明,之后正常调用 Linux SPI 的标准接口即可。

SPI 相关数据结构

结构体/宏描述
struct spi_device代表一个 SPI 设备,包含片选号、最大速率、模式等配置信息。
struct spi_driverSPI 设备驱动结构体,需实现 proberemove 等方法。
struct spi_transfer描述一次 SPI 传输的数据(发送/接收缓冲区、长度、速率等)。
struct spi_message包含多个 spi_transfer 的传输队列,用于组合复杂传输时序。

SPI 相关函数

函数名描述参数说明备注
spi_setup()配置 SPI 设备参数(如模式、速率等)。struct spi_device *spi:SPI 设备指针。probe 函数中调用。
spi_sync()同步传输 SPI 数据(阻塞直到完成)。struct spi_device *spistruct spi_message *message:设备和消息指针。需预先构建 spi_message
spi_async()异步传输 SPI 数据(立即返回,通过回调通知完成)。struct spi_device *spistruct spi_message *message需设置 message->complete 回调函数。
spi_write()向设备写入数据(仅发送)。struct spi_device *spiconst void *bufsize_t len内部封装了 spi_sync
spi_read()从设备读取数据(仅接收)。struct spi_device *spivoid *bufsize_t len同样基于 spi_sync
spi_write_then_read()先写后读(常用于命令+数据的交互)。struct spi_device *spiconst void *txbufu32 n_txvoid *rxbufu32 n_rx高效处理常见双阶段操作。
spi_message_init()初始化 spi_message 结构体。struct spi_message *m:消息指针。必须在添加 spi_transfer 前调用。
spi_message_add_tail()spi_transfer 添加到 spi_message 队列尾部。struct spi_transfer *tstruct spi_message *m:传输和消息指针。支持多段传输组合。
spi_sync_transfer()同步执行一组 SPI 传输(自动构建消息,无需手动初始化)。struct spi_device *spistruct spi_transfer *xfersint num_xfers:设备、传输结构数组、传输数量。封装了消息构建过程,适用于单次或多段连续传输。

用户态核心接口

在打开默认的 spidev 节点之后(注:如想自行配置节点文件,可以参考 章节自行定义内核态接口),用户态便可通过标准系统调用(openreadwriteioctl)和预定义的 ioctl 命令完成配置和数据传输。以下是需要关注的核心接口和结构体:

核心结构体

用户态主要依赖 spi_ioc_transfer 结构体定义 SPI 数据传输参数,定义在 linux/spi/spidev.h 中:

struct spi_ioc_transfer {
__u64 tx_buf; // 发送数据缓冲区地址(用户态指针)
__u64 rx_buf; // 接收数据缓冲区地址(用户态指针)
__u32 len; // 传输数据长度(字节数)
__u32 speed_hz; // 本次传输的时钟频率(可选覆盖设备默认值)
__u16 delay_usecs; // 传输后的延迟(微秒)
__u8 bits_per_word; // 本次传输的每字位数(可选覆盖设备默认值)
__u8 cs_change; // 传输后是否切换片选(1=切换,0=保持)
__u8 tx_nbits; // 发送数据的线宽(如 SPI_NBITS_DUAL)
__u8 rx_nbits; // 接收数据的线宽
__u8 pad[4]; // 填充字段(保留)
};
  • tx_buf / rx_buf:用户态缓冲区地址,需通过 ioctl 传递指针(用户态地址会被自动转换)。

  • len:单次传输的字节数,内核默认限制最大字节为 4096 字节。

  • speed_hz:可覆盖设备默认速度,实现动态速率调整。

  • cs_change:控制片选信号,适用于多设备共享总线时的片选管理。

关键 ioctl 命令

通过 ioctl(fd, request, arg) 控制 SPI 设备,核心命令分为配置类和传输类:

// 配置类
SPI_IOC_RD_MODE // 读取 SPI 模式(CPOL/CPHA)
SPI_IOC_WR_MODE // 设置 SPI 模式
SPI_IOC_RD_LSB_FIRST // 读取字节传输顺序(0 = MSB first)
SPI_IOC_WR_LSB_FIRST // 设置字节传输顺序
SPI_IOC_RD_BITS_PER_WORD // 读取每字位数
SPI_IOC_WR_BITS_PER_WORD // 设置每字位数
SPI_IOC_RD_MAX_SPEED_HZ // 读取最大时钟频率
SPI_IOC_WR_MAX_SPEED_HZ // 设置最大时钟频率

// 传输类
SPI_IOC_MESSAGE(N) // 执行一次或多次全双工传输(N 为传输次数)

配置流程

使用 SPI 时需要打开 SPI 对应的模块,使其编译到内核之中。

E300 074 图示

编译后,请到 drivers/spi 目录下寻找生成的 .ko 文件。根据配置选项,可能包含以下模块:

  • spidev.ko
  • spi-dw-m1000.ko
  • spi-dw-sm-ss.ko

请将这些模块文件替换到镜像文件系统中的对应位置:/lib/modules/6.6.10/kernel/drivers/spi/

DTS 配置说明

以下以 spi2 为例,说明相关配置含义。

节点定义在 m1000-main.dtsi 中,默认禁用,禁止直接修改原文件! 使用时应通过节点引用方式覆盖默认配置,在自己产品对应的 DTS 文件中进行修改。

主要配置参数说明:

spi2: spi2@00721000 {
compatible = "mthreads,spi-dw-m1000"; // 匹配 spi-dw-m1000.c 驱动
status = "disabled"; // 默认禁用该控制器
#address-cells = <1>; // 子设备地址用 1 个 cell 表示
#size-cells = <0>; // 子设备不需要大小说明
reg = <0x0 0x00721000 0x0 0x1000>; // 寄存器物理地址范围
interrupts = <GIC_SPI 35 IRQ_TYPE_LEVEL_HIGH>; // SPI2 使用 GIC 中断 35,高电平触发
resets = <&crg RESET_PERI_SPI2>; // 关联的复位控制器
reset-names = "spi"; // 复位信号名称
reg-io-width = <4>; // 寄存器访问位宽为 32bit(4 字节)
num-cs = <1>; // 支持 1 个片选信号
clocks = <&crg PERI_SPI2_CLK>; // 时钟源配置
};

// ========== SPI2 控制器启用配置 ==========
// 配置对应的管脚,通常需要 4 个管脚,CS 管脚可自定义,具体请查管脚表
pin131: pin131 {
groups = "pin131";
function = "customized";
function-value = <1>;
};

&spi2 {
status = "okay"; // 启用 SPI2 控制器
pinctrl-names = "default";
pinctrl-0 = <&pin131 &pin132 &pin133 &pin134>;

minimal_spidev@0 {
status = "okay";
reg = <0>;
compatible = "minimal_spidev"; // 使用时请修改为自己驱动对应的名字
};
};


开发参考

内核态 device 开发可参考 linux-mthreads/drivers/spi/spidev.c 用户态 APP 开发可参考 linux-mthreads/tools/spi/spidev_test.c 下方给出一个参考示例,示例中使用 spi2 生成了一个字符节点 /dev/minimal_spidev,之后又编写了一个用户态测试脚本,用来通过此节点发送数据给设备。程序运行结果如下所示,自行使用时可修改下方用户态脚本中指出来的位置,替换为自己需要的传递内容。

E300 075 图示

Device Tree Pin Configuration (.dts)

// 配置 SPI2 所对应的管脚,注意管脚号和对应的 function-value
pin131: pin131 {
groups = "pin131";
function = "customized";
function-value = <1>;
};

pin132: pin132 {
groups = "pin132";
function = "customized";
function-value = <1>;
};

pin133: pin133 {
groups = "pin133";
function = "customized";
function-value = <1>;
};

pin134: pin134 {
groups = "pin134";
function = "customized";
function-value = <1>;
};

// 引用定义好的 pinctrl 管脚,增加最简单的 minimal_spidev 节点
&spi2 {
status = "okay";
pinctrl-names = "default";
pinctrl-0 = <&pin131 &pin132 &pin133 &pin134>;

minimal_spidev@0 {
status = "okay";
reg = <0>;
compatible = "minimal_spidev"; // 使用时修改为自己的驱动对应的名字
};
};

Kernel Driver Code (C)

#include <linux/module.h>
#include <linux/spi/spi.h>
#include <linux/mutex.h>
#include <linux/uaccess.h>
#include <linux/ioctl.h>
#include <linux/cdev.h>
#include <linux/device.h>

// 定义 IOCTL 命令
#define MY_SPI_IOCTL_MAGIC 'k'
#define MY_SPI_IOCTL_SETUP _IOW(MY_SPI_IOCTL_MAGIC, 1, struct spi_setup_params)
#define MY_SPI_IOCTL_SYNC_XFER _IOWR(MY_SPI_IOCTL_MAGIC, 2, struct spi_xfer)
#define MY_SPI_IOCTL_ASYNC_XFER _IOW(MY_SPI_IOCTL_MAGIC, 3, struct spi_xfer_async)
#define MY_SPI_IOCTL_SIMPLE_WRITE _IOW(MY_SPI_IOCTL_MAGIC, 4, struct spi_write_args)
#define MY_SPI_IOCTL_SIMPLE_READ _IOR(MY_SPI_IOCTL_MAGIC, 5, struct spi_read_args)
#define MY_SPI_IOCTL_WRITE_THEN_READ _IOWR(MY_SPI_IOCTL_MAGIC, 6, struct spi_wtr_args)
#define MY_SPI_IOCTL_MULTI_XFER _IOW(MY_SPI_IOCTL_MAGIC, 7, struct spi_multi_xfer)

#define BUFFER_SIZE 4096

// 定义数据结构
struct spi_xfer {
unsigned char *tx_buf;
unsigned char *rx_buf;
int len;
};

struct spi_setup_params {
__u32 mode;
__u32 max_speed_hz;
__u8 bits_per_word;
};

struct spi_write_args {
unsigned char *tx_buf;
__u32 len;
};

struct spi_read_args {
unsigned char *rx_buf;
__u32 len;
};

struct spi_rdwr_args {
unsigned char *tx_buf;
__u32 tx_len;
unsigned char *rx_buf;
__u32 rx_len;
};

#define MAX_TRANSFERS 8

struct spi_xfer_async {
void __user *tx_buf;
void __user *rx_buf;
int len;
};

struct spi_wtr_args {
void __user *tx_buf;
void __user *rx_buf;
int tx_len;
int rx_len;
};

struct spi_multi_xfer {
struct spi_transfer __user *transfers;
int num_transfers;
int total_len;
};

struct minimal_spidev {
struct spi_device *spi;
struct mutex lock;
unsigned char *buffer;
struct cdev cdev;
dev_t devt;
struct class *cls;
};

// 设备操作函数
static int minimal_spidev_open(struct inode *inode, struct file *filp)
{
struct minimal_spidev *mdev = container_of(inode->i_cdev, struct minimal_spidev, cdev);
filp->private_data = mdev;
return 0;
}

static int minimal_spidev_release(struct inode *inode, struct file *filp)
{
return 0;
}

// IOCTL 解析函数(部分核心命令展示)
static long minimal_spidev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct minimal_spidev *mdev = filp->private_data;
int ret = 0;

if (_IOC_TYPE(cmd) != MY_SPI_IOCTL_MAGIC)
return -ENOTTY;

mutex_lock(&mdev->lock);

switch (cmd) {
case MY_SPI_IOCTL_SETUP: {
struct spi_setup_params params;
if (copy_from_user(&params, (void __user *)arg, sizeof(params))) {
ret = -EFAULT;
break;
}
mdev->spi->mode = params.mode;
mdev->spi->max_speed_hz = params.max_speed_hz;
mdev->spi->bits_per_word = params.bits_per_word;
ret = spi_setup(mdev->spi);
break;
}

case MY_SPI_IOCTL_SYNC_XFER: {
struct spi_xfer xfer;
struct spi_message msg;
struct spi_transfer t = {0};

if (copy_from_user(&xfer, (void __user *)arg, sizeof(xfer))) {
ret = -EFAULT;
break;
}

if (xfer.len > BUFFER_SIZE) {
ret = -EINVAL;
break;
}

if (xfer.tx_buf && copy_from_user(mdev->buffer, xfer.tx_buf, xfer.len)) {
ret = -EFAULT;
break;
}

t.tx_buf = xfer.tx_buf ? mdev->buffer : NULL;
t.rx_buf = xfer.rx_buf ? mdev->buffer : NULL;
t.len = xfer.len;

spi_message_init(&msg);
spi_message_add_tail(&t, &msg);
ret = spi_sync(mdev->spi, &msg);

if (ret == 0 && xfer.rx_buf) {
if (copy_to_user(xfer.rx_buf, mdev->buffer, xfer.len))
ret = -EFAULT;
}
break;
}

// 其他 ioctl 命令省略(参照你提供的代码完整实现)

default:
ret = -ENOTTY;
break;
}

mutex_unlock(&mdev->lock);
return ret;
}

// 文件操作结构体
static const struct file_operations minimal_spidev_fops = {
.owner = THIS_MODULE,
.open = minimal_spidev_open,
.release = minimal_spidev_release,
.unlocked_ioctl = minimal_spidev_ioctl,
};

// 设备初始化、移除、驱动注册函数省略(完整代码请参考上文)

User-Space SPI Test Program (test_spidev.c)

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

#define BUFFER_SIZE 4096
#define SPI_DEVICE "/dev/spi0_spidev.0" // 标准设备节点路径

int main(int argc, char *argv[])
{
int fd, ret;
static unsigned char tx_buffer[BUFFER_SIZE];
static unsigned char rx_buffer[BUFFER_SIZE];
struct spi_ioc_transfer xfer = {0};

// 打开设备
fd = open(SPI_DEVICE, O_RDWR);
if (fd < 0) {
perror("Failed to open device");
exit(EXIT_FAILURE);
}

// 配置参数
uint32_t mode = SPI_MODE_0;
uint8_t bits = 8;
uint32_t speed = 500000; // 500 kHz

ioctl(fd, SPI_IOC_WR_MODE, &mode);
ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);

printf("\n=== Test Case 1: Normal Transfer ===\n");
const char *test_str = "Hello, SPI Device!";
strncpy((char *)tx_buffer, test_str, sizeof(tx_buffer));

// 传输参数配置
xfer.tx_buf = (unsigned long)tx_buffer;
xfer.rx_buf = (unsigned long)rx_buffer;
xfer.len = strlen(test_str) + 1;
xfer.speed_hz = speed;
xfer.bits_per_word = bits;

// 执行 SPI 传输
ret = ioctl(fd, SPI_IOC_MESSAGE(1), &xfer);
if (ret < 0) {
perror("IOCTL failed");
} else {
printf("Sent %d bytes: %s\n", xfer.len, (char *)tx_buffer);
printf("Received %d bytes: %s\n", xfer.len, (char *)rx_buffer);
}

close(fd);
return 0;
}

调试方法

本章节提供一些常见的调试方向,以应对 SPI 通信或驱动开发过程中遇到的问题。

硬件检查

  • 物理连接验证

    • 确认 CS(片选)、SCLK(时钟)、MOSI(主发从收)、MISO(从发主收)连线正确,无短路/断路。
    • 检查设备供电电压是否稳定,SPI 电平是否与控制器匹配(如 3.3V vs 1.8V)。
  • SPI 模式匹配

    • 核对外设支持的 SPI 模式(CPOLCPHA),确保驱动中配置一致(模式 0/1/2/3)。
    • 检查配置的时钟频率是否在设备允许范围内。建议从较低频率开始测试,逐步提升。
  • 波形查看

    • 使用示波器或逻辑分析仪,观察 SPI 总线的电平波形,确认传输逻辑符合预期。
    • 重点查看 SCLKCSMOSIMISO 四根信号是否同步、抖动、时序错误等问题。

软件调试

  • 确认设备树(DTS)配置正确。

  • 在关键函数或节点增加打印(如 printk),查看是否可以正常输出和运行。

  • 可以暂时将驱动编译为模块(M 模式),方便与其他模块隔离,独立观察影响。

常见问题

Q:如果不使用默认的 CS 管脚,想使用自定义的 CS 管脚应该如何做?

A: 需要修改 spi-dw-m1000.c 文件,参考 spi-dw-sm-ss.c 中的片选配置方式,将 CS 控制由硬件配置修改为程序控制(GPIO 控制)。

实现步骤:

  1. spi-dw-m1000.c 中增加以下函数,实现自定义 GPIO 控制:
static void dw_spi_set_cs_gpio(struct spi_device *spi)
{
// 可以在此处实现 GPIO 配置逻辑,借助内核框架调用 GPIO 接口。
// 相比默认硬件控制,此方式可以提前几毫秒拉低 CS,更灵活。
// 若不在此实现逻辑,也可在数据传输前后手动控制拉低/拉高 CS。
return;
}
  1. 在初始化 SPI 控制器时,将该函数赋值给 dws->set_cs
dws->set_cs = dw_spi_set_cs_gpio;

这样配置后,驱动框架会自动调用 dw_spi_set_cs_gpio() 控制 CS 引脚,实现软控制方式。