ESC
输入关键词搜索文章标题和内容

SPI子系统:从协议时序到设备驱动开发的完整实战

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

SPI子系统:从协议时序到设备驱动开发的完整实战

SPI是嵌入式里仅次于I2C的高速串行总线,Flash、显示屏、ADC、IMU全靠它,和I2C不同,SPI是全双工、没有应答机制,速度快但线多,写SPI驱动搞不清四种模式、片选逻辑、传输API,出来的就是一堆乱码和超时。本文基于Linux 6.8内核,把SPI协议、子系统架构、设备树、驱动开发、传输API、控制器驱动、调试方法串一遍。

一、SPI协议基础

四线制全双工

SPI典型四根线:

信号线 方向 作用
SCLK 主→从 时钟信号,主机驱动
MOSI 主→从 主出从入,主机发送数据
MISO 从→主 主入从出,从机发送数据
CS(SS) 主→从 片选,低电平选中从机

四线制是典型配置,实际MISO可以不用(只写不读的设备),CS也可以用GPIO软件控制。但SCLK和MOSI必须有。
全双工是SPI和I2C最大的区别:发送和接收同时进行,每个时钟周期MOSI和MISO各传一位。这意味着读操作必须同时写(写dummy字节),写操作必须同时读(丢弃接收数据)。

四种模式:CPOL/CPHA组合

SPI没有统一标准,不同器件的时钟极性和相位可能不同。CPOL决定空闲时SCLK电平,CPHA决定采样边沿。
| 模式 | CPOL | CPHA | 空闲SCLK | 采样边沿 | 典型器件 |
|:-----|:-----|:-----|:---------|:---------|:---------|
| Mode 0 | 0 | 0 | 低电平 | 上升沿采样 | W25Q Flash、多数传感器 |
| Mode 1 | 0 | 1 | 低电平 | 下降沿采样 | 部分ADC |
| Mode 2 | 1 | 0 | 高电平 | 下降沿采样 | 少数器件 |
| Mode 3 | 1 | 1 | 高电平 | 上升沿采样 | SD卡(SPI模式)、部分显示 |

Mode 0用得最多,90%的SPI器件都是Mode 0。主从双方必须配置相同的模式,否则数据全乱。

传输速率

SPI没有协议层面的速率限制,速率取决于时钟频率:

场景 典型频率 说明
低速传感器 1~5MHz IMU、ADC等
SPI Flash 20~80MHz W25Q系列,高速读操作
SPI显示 10~50MHz TFT LCD、OLED
极限速率 100MHz+ 短走线、低电容负载

实际速率受走线长度、负载电容、信号完整性限制,板子上走线超过10cm还想50MHz,基本不现实。

二、SPI子系统架构

Linux SPI子系统分三层,和I2C一样:

flowchart TB A["SPI设备驱动<br/>spi_driver"] -->|"注册/注销<br/>spi_register_driver"| B["SPI核心<br/>spi-core"] B -->|"传输请求<br/>spi_transfer"| C["SPI控制器驱动层<br/>spi_controller"] A --- A1["- 实现probe/remove<br/>- 通过spi_transfer收发数据<br/>- 解析设备树匹配设备"] B --- B1["- 管理spi_driver和spi_device<br/>- 提供spi_sync/spi_async等API<br/>- 设备树解析与匹配<br/>。spi_message组装和调度"] C --- C1["- 操作控制器硬件寄存器<br/>- 实现transfer_one回调<br/>。处理DMA和中断<br/>- SoC厂商提供,一般不需自己写"] style A fill:#E3F2FD style B fill:#FFF8E1 style C fill:#E8F5E9 style A1 fill:#E3F2FD style B1 fill:#FFF8E1 style C1 fill:#E8F5E9

各层职责

SPI设备驱动*(我们写的部分)。
- 实现spi_driver的probe和remove
- 通过spi_sync、spi_async、spi_write等API与从机通信
- 通过of_device_id匹配设备树中的设备节。
SPI核心*(内核提供)。
- 管理spi_driver和spi_device的注册与匹配
- 向上提供统一的spi_sync/spi_async等数据传输API
- 向下调用控制器驱动的transfer_one
- spi_message和spi_transfer的组装与调度

SPI控制器驱动层(SoC厂商提供):

  • 操作控制器硬件寄存器,产生SCLK/MOSI/CS信号
  • 实现transfer_one或transfer_one_message回调
  • 处理DMA、中断、CS控制等底层细。

    6.8变更:spi_master已改名为spi_controller。旧代码里看到的spi_master在新内核里等价于spi_controller,spi_alloc_master对应spi_alloc_host。写新驱动直接用新名字。
    驱动开发者绝大多数时候只写设备驱动层,控制器驱动由SoC厂商已经写好。

三、设备树配置

SPI设备要在设备树里声明,内核才能创建spi_device并匹配驱动。

SPI控制器节。

SoC的设备树通常已经定义好了SPI控制器节点:

/* SoC级设备树(厂商提供) */
spi0: spi@fe610000 {
    compatible = "rockchip,rk3568-spi";
    reg = <0x0 0xfe610000 0x0 0x1000>;
    clocks = <&cru CLK_SPI0>;
    interrupts = <GIC_SPI 52 IRQ_TYPE_LEVEL_HIGH>;
    #address-cells = <1>;
    #size-cells = <0>;
    status = "disabled";
};

SPI设备子节。

在板级设备树中使能控制器并添加从设备。

/* 板级设备*/
&spi0 {
    status = "okay";
    max-frequency = <50000000>;  /* 控制器最50MHz */

    /* W25Q32 SPI Flash,片选 */
    flash@0 {
        compatible = "jedec,spi-nor";
        reg = <0>;                    /* 片选号,对应CS0 */
        spi-max-frequency = <50000000>;  /* 设备最50MHz */
        spi-cpol;                     /* CPOL=1 */
        spi-cpha;                     /* CPHA=1,Mode 3 */
    };

    /* ILI9341显示控制器,片选 */
    display@1 {
        compatible = "ilitek,ili9341";
        reg = <1>;                    /* 片选号,对应CS1 */
        spi-max-frequency = <20000000>;  /* 设备最50MHz */
        spi-cs-high;                  /* CS高电平有效(罕见)*/
        rotation = <90>;
    };
};

关键字段说明。
| 字段 | 说明 |
|:-----|:-----|
| reg | 片选编号,0=CS00=CS1,依此类推 |
| spi-max-frequency | 设备支持的最大时钟频率,Hz |
| spi-cpol | 存在则CPOL=1,不存在则CPOL=0 |
| spi-cpha | 存在则CPHA=1,不存在则CPHA=0 |
| spi-cs-high | 存在则CS高电平有效,默认低电平有效 |
| spi-3wire | 存在则三线制模式(MOSI/MISO合并 |

完整DTS示例

/* spi-board.dtsi - SPI Flash + 显示控制*/
&spi0 {
    status = "okay";
    max-frequency = <50000000>;
    pinctrl-names = "default";
    pinctrl-0 = <&spi0m0_pins>;

    /* W25Q64 SPI NOR Flash,片。,Mode 0 */
    flash@0 {
        compatible = "jedec,spi-nor";
        reg = <0>;
        spi-max-frequency = <80000000>;
        /* Mode 0: 不设spi-cpol和spi-cpha */
    };

    /* ST7789V TFT显示,片。,Mode 0 */
    display@1 {
        compatible = "sitronix,st7789v";
        reg = <1>;
        spi-max-frequency = <30000000>;
        dc-gpios = <&gpio4 22 GPIO_ACTIVE_HIGH>;  /* 数据/命令选择 */
        reset-gpios = <&gpio4 23 GPIO_ACTIVE_LOW>; /* 复位引脚 */
        rotation = <90>;
        width = <240>;
        height = <320>;
    };
};

dc-gpios是SPI显示控制器特有的,用来区分数据字节和命令字节(SPI协议本身没有这个信号,靠额外GPIO实现)。reset-gpios控制芯片硬件复位。

四、SPI设备驱动开销

spi_driver结构建

#include <linux/spi/spi.h>
#include <linux/module.h>

/* 设备私有数据 */
struct w25q_data {
    struct spi_device *spi;
    struct mutex lock;
    u8 cmd_buf[4];  /* 命令缓冲*/
};

spi_device_id + MODULE_DEVICE_TABLE

SPI驱动有两种匹配方式,和I2C一样都要写。

/* 方式1:spi_device_id - 传统非设备树匹配 */
static const struct spi_device_id w25q_id[] = {
    { "w25q32", 0 },
    { "w25q64", 1 },
    { "w25q128", 2 },
    { }
};
MODULE_DEVICE_TABLE(spi, w25q_id);

/* 方式2:of_device_id - 设备树匹配(优先*/
static const struct of_device_id w25q_of_match[] = {
    { .compatible = "jedec,spi-nor" },
    { }
};
MODULE_DEVICE_TABLE(of, w25q_of_match);

MODULE_DEVICE_TABLE宏把匹配表导出到模块信息,modprobe根据它自动加载对应驱动。

probe/remove

static int w25q_probe(struct spi_device *spi)
{
    struct w25q_data *data;

    /* 检查控制器是否支持所需模式 */
    if (spi->mode & SPI_MODE_0)
        dev_info(&spi->dev, "using SPI Mode 0\n");

    data = devm_kzalloc(&spi->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    data->spi = spi;
    mutex_init(&data->lock);
    spi_set_drvdata(spi, data);

    dev_info(&spi->dev, "W25Q SPI Flash probed\n");
    return 0;
}

static void w25q_remove(struct spi_device *spi)
{
    /* devm资源自动释放,无需手动清理 */
    dev_info(&spi->dev, "W25Q SPI Flash removed\n");
}

完整代码示例:SPI Flash读取ID

// w25q_id.c - W25Q SPI Flash读取JEDEC ID(Linux 6.8。#include <linux/spi/spi.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/delay.h>

#define W25Q_CMD_JEDEC_ID  0x9F

struct w25q_data {
    struct spi_device *spi;
    struct mutex lock;
};

/* 读取JEDEC ID:厂商+类型+容量 */
static int w25q_read_jedec_id(struct w25q_data *data, u8 *id)
{
    struct spi_device *spi = data->spi;
    int ret;

    mutex_lock(&data->lock);

    /*
     * spi_write_then_read:先字节命令,再字节ID
     * 这是SPI Flash读ID最常用的方。     */
    ret = spi_write_then_read(spi, &W25Q_CMD_JEDEC_ID, 1, id, 3);

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

/* 读取状态寄存器1 */
static int w25q_read_sr1(struct w25q_data *data)
{
    u8 cmd = 0x05;  /* RDSR命令 */
    u8 val;
    int ret;

    ret = spi_write_then_read(data->spi, &cmd, 1, &val, 1);
    if (ret < 0)
        return ret;

    return val;
}

static int w25q_probe(struct spi_device *spi)
{
    struct w25q_data *data;
    u8 id[3];
    int ret, sr1;

    /* 设置SPI模式(如果设备树没配,代码里也可以改*/
    spi->mode = SPI_MODE_0;
    spi->max_speed_hz = 50000000;  /* 50MHz */
    ret = spi_setup(spi);
    if (ret < 0) {
        dev_err(&spi->dev, "spi_setup failed: %d\n", ret);
        return ret;
    }

    data = devm_kzalloc(&spi->dev, sizeof(*data), GFP_KERNEL);
    if (!data)
        return -ENOMEM;

    data->spi = spi;
    mutex_init(&data->lock);
    spi_set_drvdata(spi, data);

    /* 读取JEDEC ID */
    ret = w25q_read_jedec_id(data, id);
    if (ret < 0) {
        dev_err(&spi->dev, "read JEDEC ID failed: %d\n", ret);
        return ret;
    }

    dev_info(&spi->dev, "JEDEC ID: %02x %02x %02x\n",
             id[0], id[1], id[2]);
    /* Winbond: id[0]=0xEF, id[1]=0x40, id[2]=0x16(W25Q32) */

    /* 读状态寄存器验证 */
    sr1 = w25q_read_sr1(data);
    if (sr1 < 0) {
        dev_err(&spi->dev, "read SR1 failed: %d\n", sr1);
        return sr1;
    }
    dev_info(&spi->dev, "SR1=0x%02x, BUSY=%d, WEL=%d\n",
             sr1, sr1 & 0x01, (sr1 >> 1) & 0x01);

    return 0;
}

static void w25q_remove(struct spi_device *spi)
{
    dev_info(&spi->dev, "W25Q removed\n");
}

static const struct spi_device_id w25q_id[] = {
    { "w25q32", 0 },
    { "w25q64", 1 },
    { "w25q128", 2 },
    { }
};
MODULE_DEVICE_TABLE(spi, w25q_id);

static const struct of_device_id w25q_of_match[] = {
    { .compatible = "jedec,spi-nor" },
    { }
};
MODULE_DEVICE_TABLE(of, w25q_of_match);

static struct spi_driver w25q_driver = {
    .driver = {
        .name = "w25q",
        .of_match_table = w25q_of_match,
    },
    .probe = w25q_probe,
    .remove = w25q_remove,
    .id_table = w25q_id,
};
module_spi_driver(w25q_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("linuxros");
MODULE_DESCRIPTION("W25Q SPI Flash ID reader driver");

module_spi_driver宏展开后自动注册/注销驱动,不用手写module_init和module_exit。

五、SPI传输API

SPI传输的核心概念是spi_message和spi_transfer。一个message包含一个或多个transfer,同一个message里的transfer之间CS保持低电平不释放。

来自 linuxros.cn · linuxROS

spi_transfer结构建

struct spi_transfer {
    const void  *tx_buf;        /* 发送缓冲区 */
    void        *rx_buf;        /* 接收缓冲*/
    unsigned     len;           /* 传输字节数(tx和rx相同*/
    unsigned     speed_hz;      /* 本次传输时钟频率0=使用默认 */
    u16          bits_per_word; /* 每字比特数,0=使用默认8bit */
    u16          delay_usecs;   /* 传输后延迟(微秒*/
    u8           cs_change;     /* 传输后释放CS0=释放0=保持*/
    /* ... */
};

SPI是全双工,tx_buf和rx_buf同时工作。只写时rx_buf设NULL,只读时tx_buf设NULL(控制器会自动发0xFF)。

spi_message + spi_transfer_list_add_tail + spi_sync/spi_async

/* 组装一次完整传输:写命令+读数据*/
static int w25q_read_data(struct w25q_data *data,
                          u32 addr, u8 *buf, int len)
{
    struct spi_device *spi = data->spi;
    struct spi_message msg;
    struct spi_transfer xfer_cmd, xfer_data;
    u8 cmd[4];  /* 命令+24位地址 */
    int ret;

    /* 构造读命令:0x03 + 3字节地址 */
    cmd[0] = 0x03;
    cmd[1] = (addr >> 16) & 0xFF;
    cmd[2] = (addr >> 8) & 0xFF;
    cmd[3] = addr & 0xFF;

    spi_message_init(&msg);

    /* 第一段:发送命令+地址,CS保持 */
    memset(&xfer_cmd, 0, sizeof(xfer_cmd));
    xfer_cmd.tx_buf = cmd;
    xfer_cmd.len = 4;
    xfer_cmd.cs_change = 0;  /* CS保持低电*/
    spi_message_add_tail(&xfer_cmd, &msg);

    /* 第二段:读取数据,CS释放 */
    memset(&xfer_data, 0, sizeof(xfer_data));
    xfer_data.rx_buf = buf;
    xfer_data.len = len;
    xfer_data.cs_change = 1;  /* 传输完释放CS */
    spi_message_add_tail(&xfer_data, &msg);

    /* 同步传输,阻塞等待完*/
    ret = spi_sync(spi, &msg);
    if (ret < 0)
        dev_err(&spi->dev, "read failed: %d\n", ret);

    return ret;
}

spi_sync是同步版本,阻塞等待传输完成。spi_async是异步版本,传输完成后调用完成回调:

static void w25q_async_complete(void *context)
{
    struct w25q_data *data = context;

    /* 传输完成,处理接收数*/
    dev_dbg(&data->spi->dev, "async transfer complete\n");
    complete(&data->done);  /* 通知等待*/
}

/* 异步传输 */
static int w25q_read_async(struct w25q_data *data,
                           u32 addr, u8 *buf, int len)
{
    struct spi_message msg;
    struct spi_transfer xfer;
    u8 cmd[4];

    cmd[0] = 0x03;
    cmd[1] = (addr >> 16) & 0xFF;
    cmd[2] = (addr >> 8) & 0xFF;
    cmd[3] = addr & 0xFF;

    spi_message_init(&msg);
    msg.complete = w25q_async_complete;
    msg.context = data;

    memset(&xfer, 0, sizeof(xfer));
    xfer.tx_buf = cmd;
    xfer.rx_buf = buf;
    xfer.len = 4 + len;
    spi_message_add_tail(&xfer, &msg);

    return spi_async(data->spi, &msg);
}

异步传输适合大数据量场景,避免长时间阻塞当前线程。但大部分驱动用spi_sync就够了。

spi_write / spi_read / spi_write_then_read:简化API

内核封装了几个快捷函数,内部都是组装spi_message再调spi_sync。

/* 纯写:发送buf的len字节 */
int ret = spi_write(spi, buf, len);

/* 纯读:接收len字节到buf */
int ret = spi_read(spi, buf, len);

/* 先写后读:发n_tx字节,再读n_rx字节 */
int ret = spi_write_then_read(spi, tx_buf, n_tx, rx_buf, n_rx);

spi_write_then_read是最常用的,读寄存器、读ID这类"发命令+读结果的操作一行搞定。它内部会做CS保持——写和读之间CS不释放。

传输方式对比

方式 适用场景 优点 缺点
spi_sync + 手动组装 多段传输、CS控制 最灵活,精确控制CS 代码多
spi_async 大数据量、非阻塞 不阻塞调用线程 需要完成回调,复杂
spi_write 纯写操作 一行代码 只能写
spi_read 纯读操作 一行代码 只能读,发dummy字节
spi_write_then_read 发命令读结果 最常用,简单 写和读之间不能插延迟

实际开发中80%的场景用spi_write_then_read搞定,剩下20%用spi_sync手动组装。

六、SPI控制器驱动

spi_controller + spi_controller_mem_ops

6.8内核中,SPI控制器用spi_controller描述(旧名spi_master),核心回调是transfer_one。

struct spi_controller {
    struct device dev;
    struct list_head list;
    u16 bus_num;                    /* 总线编号 */
    u16 num_chipselect;             /* 片选数*/
    u32 mode_bits;                  /* 支持的模式位掩码 */
    u32 min_speed_hz;
    u32 max_speed_hz;
    u16 bits_per_word_mask;

    /* 核心回调 */
    int (*transfer_one)(struct spi_controller *ctlr,
                        struct spi_device *spi,
                        struct spi_transfer *xfer);
    void (*handle_err)(struct spi_controller *ctlr,
                       struct spi_message *msg);

    /* SPI Memory操作集.8新接口) */
    const struct spi_controller_mem_ops *mem_ops;

    /* ... */
};

/* SPI Memory操作,用于SPI NOR/NAND */
struct spi_controller_mem_ops {
    int (*exec_op)(struct spi_controller *ctlr,
                   const struct spi_mem_op *op);
    const char *(*get_name)(struct spi_controller *ctlr);
    int (*dirmap_create)(struct spi_controller *ctlr,
                         const struct spi_mem_dirmap_info *info);
    void (*dirmap_destroy)(struct spi_controller *ctlr,
                           struct spi_mem_dirmap_desc *desc);
    ssize_t (*dirmap_read)(struct spi_controller *ctlr,
                           const struct spi_mem_dirmap_desc *desc,
                           u64 offs, size_t len, void *buf);
    ssize_t (*dirmap_write)(struct spi_controller *ctlr,
                            const struct spi_mem_dirmap_desc *desc,
                            u64 offs, size_t len, const void *buf);
};

spi_controller_mem_ops6.8为SPI Memory(NOR/NAND Flash)新增的优化接口。传统的spi_transfer方式对Flash操作效率低——每次读都要组装message。mem_ops直接执行Flash操作命令,支持直接映射(dirmap),让Flash读取走DMA,省掉了message组装的开销。

何时需要写控制器驱动

*绝大多数情况不需要 SoC厂商已经在主线内核或BSP里提供了控制器驱动:

SoC 控制器驱动文件 compatible
RK3568 drivers/spi/spi-rockchip.c rockchip,rk3568-spi
i.MX6 drivers/spi/spi-imx.c fsl,imx6q-ecspi
STM32MP drivers/spi/spi-stm32.c st,stm32h7-spi
Allwinner drivers/spi/spi-sun6i.c allwinner,sun8i-h3-spi

需要自己写控制器驱动的场景:- 自研FPGA实现SPI控制。- 使用冷门SoC,主线内核没有支。- 需要优化Flash读取性能(实现mem_ops的dirmap。

七、SPI vs I2C对比

维度 SPI I2C
信号线数 4线(SCLK/MOSI/MISO/CS 2线(SDA/SCL
通信方式 全双工 半双工
传输速率 几十MHz 100kbps~5Mbps
寻址方式 硬件片选(CS线) 软件地址。/10位)
多从 每个从机一根CS 共享总线,地址区分
应答机制 ACK/NACK
传输距离 短(<30cm 短(<1m
功能 较高 较低
硬件复杂 线多,布线麻 线少,布线简
适用场景 Flash、显示、高速ADC 传感器、EEPROM、PMIC

选型原则:要速度上SPI,要省线用I2C。一个板子上通常两种都有——Flash和屏走SPI,传感器和电源芯片走I2C。

八、调试技。

spi-tools

SPI版的i2c-tools,用于用户空间调试SPI通信。

# 安装
sudo apt install spi-tools

# 查看SPI总线信息
spi-config --device /dev/spidev0.0 --list-devices

# 配置SPI参数
spi-config --device /dev/spidev0.0 \
    --mode 0 \
    --speed 1000000 \
    --bits-per-word 8

# 发送数据并接收
spi-transfer --device /dev/spidev0.0 \
    --speed 1000000 \
    --mode 0 \
    --tx-data 9F,00,00,00 \
    --rx-size 3
# 返回Flash的JEDEC ID

逻辑分析仪抓。

SPI调试比I2C更依赖逻辑分析仪——因为SPI没有应答机制,数据错了不会NACK告诉你,只能看波形:

  1. Saleae Logic / sigrok:抓取SCLK/MOSI/MISO/CS信号,自动解码SPI协议
  2. *关注。 - CS是否在传输前拉低、传输后拉高
    - SCLK空闲电平是否和CPOL配置一。 - 数据采样边沿是否正确(CPHA=0上升沿采样,CPHA=1下降沿采样)
    - MOSI数据是否和预期一。 - 时钟频率是否在设备支持范围内
    - 两次传输之间CS是否意外释放

spidev用户空间驱动

内核提供了spidev驱动,让用户空间程序直接操作SPI设备。

/* 设备树中启用spidev */
&spi0 {
    status = "okay";

    spidev@0 {
        compatible = "rohm,dh2228fv";  /* spidev兼容字符号*/
        reg = <0>;
        spi-max-frequency = <10000000>;
    };
};

用户空间C代码。

// spidev_test.c - 用户空间SPI读写
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>

int main(void)
{
    int fd = open("/dev/spidev0.0", O_RDWR);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    /* 配置SPI参数 */
    unsigned char mode = SPI_MODE_0;
    unsigned int speed = 10000000;  /* 10MHz */
    unsigned char bits = 8;

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

    /* 全双工传输:发JEDEC ID命令,读3字节 */
    unsigned char tx[] = { 0x9F, 0x00, 0x00, 0x00 };
    unsigned char rx[4] = { 0 };

    struct spi_ioc_transfer xfer = {
        .tx_buf = (unsigned long)tx,
        .rx_buf = (unsigned long)rx,
        .len = 4,
        .speed_hz = speed,
        .bits_per_word = bits,
    };

    ioctl(fd, SPI_IOC_MESSAGE(1), &xfer);

    printf("JEDEC ID: %02x %02x %02x\n", rx[1], rx[2], rx[3]);

    close(fd);
    return 0;
}

spidev适合快速验证硬件通信,不适合生产环境——没有设备模型、没有电源管理、没有并发控制。正式产品还是得写内核驱动。

九、常见问题

Q1:SPI通信数据错位。

数据整体偏移了几位,读出来的值和预期差一个bit位置,这是模式配错:

  1. CPOL/CPHA不匹配——最常见的原因。对照器件手册确认模式,设备树里spi-cpol和spi-cpha要和器件一致。2. MSB/LSB顺序反了**——SPI默认MSB first,少数器件是LSB first,设备树加spi-lsb-first
  2. CS时序不对——有些器件要求CS拉低后等一段时间再发时钟,用spi_transfer.delay_usecs加延。
# 查看当前SPI设备配置
cat /sys/bus/spi/devices/spi0.0/modalias
cat /sys/bus/spi/devices/spi0.0/max_speed_hz

Q2:spi_sync返回-ETIMEDOUT。

超时说明控制器发了时钟但没完成传输:

  1. *CS未正确拉——检查CS引脚是否被其他驱动占用,或者GPIO配置不对
  2. 时钟频率过高——降频试试,有些器件标称50MHz实际只能稳定50MHz
  3. 控制器驱动bug——DMA地址映射错误、中断丢失等,查dmesg
  4. 走线太长——信号完整性差,高频时钟变形,降频或缩短走。
# 查看SPI控制器状态cat /sys/class/spi_master/spi0/statistics/*

Q3:SPI Flash写入失败。

SPI Flash写入有特殊时序要求,不是直接写就行的。
1. 没发写使能命令——Flash每次写操作前必须发WREN(0x06)命令,否则写入被忽略。2. 写入时Busy位未等待——写操作需要时间(典型1~5ms),必须轮询状态寄存器的BUSY位。3. 跨页写入**——Flash按页写(256字节/页),跨页地址会回卷到页首,数据被覆盖

/* Flash写入正确流程 */
static int w25q_write_enable(struct w25q_data *data)
{
    u8 cmd = 0x06;  /* WREN */
    return spi_write(data->spi, &cmd, 1);
}

static int w25q_wait_busy(struct w25q_data *data)
{
    int sr1;
    int retries = 100;

    do {
        sr1 = w25q_read_sr1(data);
        if (sr1 < 0)
            return sr1;
        if (!(sr1 & 0x01))  /* BUSY位清*/
            return 0;
        usleep_range(100, 200);
    } while (retries-- > 0);

    return -ETIMEDOUT;
}

static int w25q_write_page(struct w25q_data *data,
                           u32 addr, const u8 *buf, int len)
{
    int ret;

    /* 1. 写使*/
    ret = w25q_write_enable(data);
    if (ret < 0)
        return ret;

    /* 2. 发页编程命令+数据 */
    /* ... spi_sync组装PP命令(0x02)+地址+数据 ... */

    /* 3. 等待写入完成 */
    return w25q_wait_busy(data);
}

十、总结

SPI驱动开发速查看

环节 关键字
协议 四线全双工,CPOL/CPHA四种模式,Mode 0最常用
子系统架 设备驱动→核心层→控制器驱动,三层解
设备 reg填片选号,spi-max-frequency设速率,spi-cpol/spi-cpha配模块
驱动注册 spi_driver+of_device_id+spi_device_id,module_spi_driver
数据传输 优先spi_write_then_read,复杂时序用spi_sync手动组装
控制器驱动 SoC厂商提供。.8用spi_controller替代spi_master
调试 spi-tools读写验证,逻辑分析仪看波形,spidev用户空间快速测
常见故障 数据错位→CPOL/CPHA配错,超时→CS/频率问题,Flash写失败→缺WREN

本文首发于linuxros.cn,转载请注明出处。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页