Linux SPI设备驱动

CPU是主设备,负责托管SPI控制器,也叫SPI主设备。SPI主设备管理托管了SPI从设备的总线,总线由平台驱动程序管理,从设备由SPI设备驱动程序驱动。

struct spi_controller:抽象SPI主设备

C 复制代码
struct spi_controller {
	struct device dev;
	u16 num_chipselect;
	u32 min_speed_hz;
	u32 max_speed_hz;
	int (*setup)(struct spi_device *spi);
	int (*set_cs_timing)(struct spi_device *spi, struct spi_delay *setup, struct spi_delay *hold, struct spi_delay *inactive);
	int (*transfer)(struct spi_device *spi, struct spi_message *mesg);
	bool (*can_dma)(struct spi_controller *ctlr, struct spi_device *spi, struct spi_transfer *xfer);
	struct kthread_worker *kworker;
	struct kthread_work pump_messages;
	spinlock_t queue_lock;
	struct list_head queue;
	struct spi_message *cur_msg;
	bool busy;
	bool running;
	bool rt;
	int (*transfer_one_message)(struct spi_controller *ctlr, struct spi_message *mesg);
	[...]
	void (*set_cs)(struct spi_device *spi, bool enable);
	int (*transfer_one)(struct spi_controller *ctlr, struct spi_device *spi, struct spi_transfer *transfer);
	[...]
	struct dma_chan *dma_tx;
	struct dma_chan *dma_rx;
	void *dummy_rx;
	void *dummy_tx;
};

num_chipselect:指定分配给控制器的cs数量。cs用于区分各个SPI从设备,编号从0开始。

min_speed_hz和max_speed_hz分别是控制器支持的最低和最高传输速度。

set_cs_timing是一个指向函数的指针,可在SPI控制器支持CS定时配置的情况下使用。在这种情况下,客户端驱动程序可以搭配请求的时间来使用spi_set_cs_timing()调用这个指针指向的函数,但这种调用方式在最近的内核版本补丁中被弃用(删除)了。

transfer向控制器的传输队列添加一条消息。

在控制器注册过程中用到的spi_register_controller()会检查该字段是否为NULL。如果为NULL,SPI核心将检查是否设置了transfer_one或transfer_one_message字段,如果设置了,则假定此控制器支持消息队列,并调用spi_controller_initialize_queue()函数。该函数将使用spi_queued_transfer(设置该字段,spi_queued_transfer)是SPI的核心辅助函数,用于将SPI消息排入控制器的消息队列,并在它尚未运行或繁忙时调度消息泵kworker。

❏ 此外,spi_controller_initialize_queue()函数将为控制器创建专用的内核工作线程(kworker元素)和一个结构体(pump_messages元素)​。为了按照FIFO(First InFirst Out,先进先出)顺序处理消息队列,这个内核工作线程会被频繁调度。

❏ 接下来,SPI核心将控制器的queued元素设置为true。

❏ 最后,如果驱动程序在调用注册API之前将控制器的rt元素设置为true,则SPI核心将工作线程的调度策略设置为实时的FIFO策略,优先级为50。

♦ 如果为NULL,并且transfer_one和transfer_one_message也为NULL,则说明出错,因为没有注册控制器。

♦ 如果不为NULL,SPI核心会认为控制器不支持排队,因而不会调用spi_controller_initialize_queue()函数。(如果transfer≠NULL(驱动自己实现了transfer)→ 内核认为不支持排队,不调用spi_controller_initialize_queue())

· transfer_one和transfer_one_message是互斥的。如果两者都被设置,则SPI核心不会调用前者。transfer_one传输单个SPI操作,并且没有spi_message的概念。如果驱动程序提供了transfer_one_message,则必须在spi_message的基础上工作,并负责所有消息的传输。如果控制器驱动程序不用关心消息处理算法,那么只需要设置transfer_one。在这种情况下,SPI核心将把transfer_one_message设置为 spi_transfer_one_message。在调用驱动程序为消息中的每个传输操作提供的transfer_one回调之前,spi_transfer_one_message将处理所有消息逻辑、定时、CS和其他硬件相关属性。CS在整个消息传输过程中都保持活动状态,除非它被具有spi_transfer.cs_change=1语句的传输操作修改。消息传输操作将使用此设备先前应用的时钟和SPI模式参数执行,这些参数是通过setup()函数设置的。

transfer_one:粒度是「单个 spi_transfer」(处理单个包裹);

transfer_one_message:粒度是「整个 spi_message」(处理整个订单);

场景 1:只设置 transfer_one → 驱动 "无 spi_message 概念"

驱动的责任:只需要处理单个 spi_transfer 的硬件收发(比如往控制器寄存器写数据、读数据),完全不用关心 "这个包裹属于哪个订单""订单里有多少个包裹";

内核的责任:内核会把 spi_message 拆解成一个个 spi_transfer,逐个调用 transfer_one,并负责:

✅ 整个消息的时序(比如传输间隔、CS 保持);

✅ CS 引脚的管理(整个消息期间保持有效,除非某个 spi_transfer 的 cs_change=1);

✅ 时钟 / 模式参数(用 setup () 设置的参数);

✅ 消息的完成通知(比如上层等待消息完成)。

简单说:驱动只干 "搬砖"(单个传输的硬件操作),内核干 "统筹"(消息级的逻辑)。

场景 2:设置 transfer_one_message → 驱动 "必须有 spi_message 概念"

驱动的责任:要处理整个 spi_message,包括:

✅ 遍历消息里的所有 spi_transfer;

✅ 管理 CS 引脚、时序、时钟模式;

✅ 处理 cs_change=1 这类特殊标记;

✅ 完成整个消息的传输并通知内核;

为什么 "不关心消息处理算法" 就只设置 transfer_one

核心原因:内核提供了兜底的 spi_transfer_one_message 函数

如果驱动只设置 transfer_one,SPI 核心会自动把 transfer_one_message 赋值为内核自带的 spi_transfer_one_message------ 这个函数已经封装了所有 "消息处理算法",比如:

加锁保护消息处理;

遍历消息中的每个 spi_transfer;

调用驱动的 transfer_one 处理单个传输;

处理 cs_change=1(比如某个传输后临时释放 CS);

管理传输间隔、时钟参数;

整个消息传输完成后,标记消息为 "完成" 并唤醒上层等待的线程。

写法 1:只设置 transfer_one(推荐,不用关心消息算法)

C 复制代码
// 驱动只写"单个传输"的硬件逻辑(搬砖)
static int my_spi_transfer_one(struct spi_controller *ctlr, 
                               struct spi_device *spi, 
                               struct spi_transfer *xfer) {
    // 仅处理:往SPI控制器写xfer->tx_buf的数据,或从控制器读数据到xfer->rx_buf
    // 不用管CS、不用管消息里有多少个transfer、不用管时序
    return 0;
}

// 初始化控制器
ctlr->transfer_one = my_spi_transfer_one; // 只设这个
// 内核自动把ctlr->transfer_one_message = spi_transfer_one_message(兜底)

写法2:设置 transfer_one_message(需要关心消息算法)

C 复制代码
// 驱动必须写"整个消息"的逻辑(统筹+搬砖)
static int my_spi_transfer_one_message(struct spi_controller *ctlr, 
                                       struct spi_message *mesg) {
    struct spi_device *spi = mesg->spi;
    struct spi_transfer *xfer;
    
    // 1. 驱动自己管理CS(激活)
    ctlr->set_cs(spi, true);
    
    // 2. 遍历消息里的所有transfer(自己拆解订单)
    list_for_each_entry(xfer, &mesg->transfers, transfer_list) {
        // 3. 自己处理单个transfer的硬件操作(搬砖)
        my_hw_transfer(spi, xfer);
        // 4. 自己处理cs_change(比如释放CS)
        if (xfer->cs_change) {
            ctlr->set_cs(spi, false);
            // 处理CS空闲延迟
            udelay(xfer->delay_us);
            ctlr->set_cs(spi, true);
        }
    }
    
    // 5. 驱动自己管理CS(释放)
    ctlr->set_cs(spi, false);
    // 6. 驱动自己标记消息完成
    mesg->status = 0;
    spi_finalize_current_message(ctlr);
    return 0;
}

// 初始化控制器
ctlr->transfer_one_message = my_spi_transfer_one_message; // 设这个,transfer_one会被忽略

kworker用于消息泵程序的内核线程

pump_messages

queue_lock表示自旋锁,用于同步访问消息队列

queue表示控制器的消息队列

idling表示控制器设备是否进入空闲状态

cur_msg表示当前传输操作中的SPI消息

busy表示消息泵的繁忙程度

running表示消息泵正在运行

rt表示kworker是否以实时优先级运行消息泵

dma_tx表示DMA发送通道(当控制器支持时)

dma_rx表示DMA接受通道(当控制器支持时)

dummy_rx/tx用于模拟全双工

SPI传输操作总是读取和写入相同的字节数,这意味着即使客户端驱动程序发出半双工传输,SPI核心也会使用dummy_rx和dummy_tx模拟全双工传输以实现此目的。

· dummy_rx:这是一个用于全双工设备的虚拟接收缓冲区。例如,如果传输操作的接收缓冲区为NULL,接收的数据将在被丢弃之前转移到这个虚拟接收缓冲区。

· dummy_tx:这是一个用于全双工设备的虚拟发送缓冲区。例如,如果传输操作的发送缓冲区为NULL,这个虚拟发送缓冲区将被填充零并用作发送缓冲区。

请注意,SPI核心将SPI消息泵工作线程命名为控制器设备名(dev->name),控制器设备名在spi_register_controller()中被设置如下:

稍后,当在消息队列初始化过程中创建工作线程时,即运行spi_controller_initialize_queue()时,工作线程的名称将像下面这样被给定:

想要识别SPI消息泵工作线程,可以运行以下命令:

struct spi_device:抽象SPI从设备

struct spi_device数据结构表示SPI设备,该数据结构定义在include/linux/spi/spi.h中:

controller表示从设备所属的SPI控制器。换句话说,它表示连接设备的SPI控制器(总线)​。

出于兼容性方面的原因,master元素仍然存在,但它很快就会被弃用。这是控制器的原名。

max_speed_hz是与从设备一起使用的最大时钟频率,这个参数可以在驱动程序中修改。我们可以使用spi_transfer.speed_hz覆盖每次SPI传输的最大时钟频率。

chip_select是分配给此设备的CS信号线。默认低电平为有效状态。这种行为可以通过添加SPI_CS_HIGH标志来改变。

mode定义了数据如何随着时钟信号进行传输。SPI设备驱动程序可能会改变这一点。默认情况下,传输操作中每个字的数据时钟信号以最高有效位(Most Significant Bit,MSB)为优先顺序。可以通过指定SPI_LSB_FIRST来覆盖此行为,使数据时钟信号变为以最低有效位(Least Significant Bit,LSB)为优先顺序。

irq表示中断号(在板级初始化文件或设备树中作为设备资源注册)​,你应该将中断号传递给request_irq()以接收来自此设备的中断。

cs_gpio和cs_gpiod都是可选的。前者是CS信号线遗留的基于整数的GPIO编号,后者是推荐的基于GPIO描述符的新接口。

四种SPI模式

可以使用如下两个特征来构建SPI模式。

· CPOL(Clock Polarity,时钟极性)是初始时钟的极性。

❏ 0:初始时钟状态为低电平,第一个边沿是上升的。

❏ 1:初始时钟状态为高电平,第一个状态为下降状态。

· CPHA(Clock Phase,时钟相位)决定了在哪个边沿对数据进行采样。

❏ 0:数据在下降沿(从高到低过渡)被锁存,输出在上升沿发生变化。

❏ 1:数据在上升沿(从低到高过渡)被锁存,输出在下降沿发生变化。

struct spi_driver:从设备驱动程序

SPI设备驱动程序也称为协议驱动程序,负责驱动SPI总线上的设备。它在内核中由数据结构struct spi_driver抽象,该数据结构声明如下:
下面给出了上述数据结构中各个字段的含义。

· id_table:此驱动程序支持的SPI设备列表。

· probe:用于将驱动程序绑定到SPI设备。此探测函数将在使用该驱动程序的任何设备上调用,并决定该驱动程序是否负责该设备。如果负责该设备,则会发生绑定。

· remove:解除驱动程序与SPI设备的绑定。

· shutdown:此函数在系统状态发生改变时调用,例如关机和停机。

· driver:这是设备和驱动程序模型的底层驱动程序数据结构。除了每个SPI设备驱动程序必须填充并公开一个spi_driver结构体实例,此处并未涉及其他更多信息。

消息传输数据结构

SPI I/O模型由一个消息队列组成,其中的每个消息可以由一个或者多个SPI传输操作组成,即单个消息由一个或多个spi_transfer结构体实例组成,每个SPI传输操作表示一个全双工SPI事务。消息以同步或异步的方式提交和处理。

struct spi_transfer:表示主从设备之间的单个操作


tx_buf是一个指向缓冲区的指针,该缓冲区含有要写入的数据。如果设置为NULL,此传输操作将被视为只读事务的半双工通信。当需要通过DMA执行SPI事务时,应确保它是DMA安全的。

rx_buf指针则指向要从中读取数据的缓冲区(rx_buf与tx_buf具有相同的属性)​,它在只写事务中为NULL。

tx_dma是tx_buf的DMA地址,在spi_message.is_dma_mapped设置为1时生效。

rx_dma与tx_dma类似,但rx_dma用于rx_buf。

len表示rx和tx缓冲区的大小(以字节为单位)​。数据只有当到达len字节时才会被移出或移进)​,并且试图移出部分数据将导致错误。

speed_hz能够取代spi_device.max_speed_hz中指定的默认速度,但仅适用于当前传输。如果它为0,则使用来自spi_device的默认值。

bits_per_word:数据传输涉及一个或多个字。字是一种数据单位,以位为单位的字大小能够根据需要而变化。在这里,bits_per_word表示SPI传输中每个字的位大小。这将覆盖spi_device_bits_per_word提供的默认值。如果它为0,则使用来自spi_device的默认值。

  1. "字" 的定义(针对 SPI)
    这里的 "字(word)" 不是 CPU 架构里的 "4 字节 / 8 字节字",而是 SPI 传输的最小位单元:
    bits_per_word = 8 → 每次传输 1 字节(8 位),这是 SPI 的 "标准操作"(比如传感器、Flash、显示屏的 SPI 通信几乎都用 8 位);
    bits_per_word = 16 → 每次传输 2 字节(16 位),常见于高精度 ADC/DAC、工业控制模块;
    也支持 12 位、24 位、32 位等(由硬件控制器能力决定)。

cs_change用于确定传输完成后CS是否变为不活动状态。所有SPI传输操作都将从适当的CS信号开始。通常,在完成消息的最后一次传输之前,它会一直保持选中状态。当cs_change不为0时,驱动程序可以改变CS信号。

cs_change 是 struct spi_transfer 的标记位(非 0 即 1),它的作用是:告诉内核 "当前这个 transfer 被 transfer_one 处理完后,要不要临时切换 CS 状态",具体流程如下:

触发时机:transfer_one 完成当前这个 transfer 的数据传输后,内核会检查该 transfer 的cs_change;

核心行为:

cs_change=1:内核自动释放 CS(从激活→不活动,比如拉高),若消息中还有下一个 transfer,则等待delay_us后重新激活 CS(拉低);

cs_change=0(默认):不触发 CS 切换,CS 保持激活状态,直到整个消息的最后一个 transfer 处理完才释放;

关键例外:如果当前 transfer 是消息里的最后一个,哪怕cs_change=1,内核也会忽略 ------ 因为消息结束后本就要释放 CS,无需多此一举。

delay_usecs表示此次传输之后的延迟(以微秒为单位)​,其间可以选择更改chip_select状态,然后开始下一次传输或完成当前的spi_message传输。

struct spi_message:原子传输序列

struct spi_message数据结构用于原子性地发出一个传输操作序列,其中的每个传输操作由一个spi_transfer结构体实例表示。原子性是指在传输完这样的序列之前,没有其他的spi_message可以使用SPI总线。但请注意,有些平台可以用一次编程的DMA传输操作处理许多这样的序列。struct spi_message数据结构声明如下:

transfers是构成消息的传输操作序列。稍后我们将看到如何将传输操作添加到这个序列中。在最后一次传输操作中使spi_transfer.cs_change标志生效可能会潜在地节省CS操作和撤销CS操作的成本。

is_dma_mapped通知控制器是否使用DMA来执行事务。你的代码需要为每个传输缓冲区提供DMA和CPU虚拟地址。

complete是事务完成时调用的回调函数

context是要传递给该回调函数的参数。

frame_length将根据消息中的总字节数自动设置。

frame_length(帧长度):传输的 "理论总目标"

actual_length是分段传输成功的总字节数。

actual_length(实际长度):传输的 "实际总结果"

核心含义:是传输过程中(尤其是大消息被分段传输时),所有成功完成的分段传输的字节数总和。它代表 "本次消息实际成功收发的字节数",是真实的 "结果值"。

status报告传输操作的状态,成功时为0,否则为-errno。

消息中的spi_transfer按FIFO顺序处理。在消息完成(即表示传输完成的回调函数被执行)之前,用户必须确保不使用传输缓冲区,以避免损坏数据。向底层提交spi_message(及其spi_transfer)的代码负责管理其内存。在消息提交之后,驱动程序必须忽略该消息(及其传输)​,至少在其回调函数被调用之前应如此。

访问SPI设备

SPI控制器能够与一个或多个从设备(即一个或多个spi_device结构体实例)通信。它们组成了一个微型总线,共享MOSI、MISO和SCK信号,但不共享CS信号。由于这些共享信号在未选择芯片时会被忽略,因此每个设备可以被编程以利用不同的时钟频率。SPI控制器驱动程序通过spi_message事务队列来管理与这些设备的通信,在CPU内存和SPI从设备之间移动数据。spi_message事务队列中每个消息实例的complete回调函数会在事务完成时被调用。

在将消息提交到总线之前,必须使用spi_message_init()函数对其进行初始化,该函数原型如下:

spi_message_init()函数会将spi_message中的每个元素归零,并初始化传输操作序列。对于每个要添加到消息对象中的传输操作,都应该调用spi_message_add_tail()函数进行处理,以便将该传输操作放入消息的传输队列。该函数声明如下:

一旦完成该操作,就有以下两种选择来启动事务。

· 使用int spi_sync(struct spi_device *spi, struct spi_message *message)函数同步传输数据,成功时返回0,否则返回错误码。该函数可能会睡眠,因此不能在中断上下文中使用。但要注意,该函数可能以不可中断的方式进入睡眠状态,并且不允许指定睡眠时间。支持DMA的控制器驱动程序可以利用DMA功能直接从消息缓冲区接收数据或将数据发送到消息缓冲区。SPI设备的片选信号在整个消息传输期间(从第一次传输到最后一次传输)由核心激活,然后通常在消息之间被禁用。一些驱动程序为了尽量减少CS操作的影响(如节省功耗)​,会保持芯片处于选中状态,并预期下一条消息将被推送到同一芯片。

· 使用int spi_async(struct spi_device *spi, struct spi_message *message)函数可以异步地在任何(原子或非原子的)上下文中执行操作。因为该函数只完成提交且处理是异步的,所以,它是与上下文无关的。然而,complete回调函数是无法在睡眠的上下文中调用的。在调用这个回调函数之前,message->status的值是未定义的。调用后,message->status保存了完成状态,它的值要么是0(表示完全成功)​,要么是一个错误码。

这个回调函数返回后,发起传输请求的驱动程序可以释放相关的内存,因为它已经不再被任何SPI核心或控制器驱动程序代码使用。在当前处理消息的complete回调函数返回之前,不会处理设备队列中的后续spi_message,这一规则也适用于同步传输的回调。如果成功,该函数返回0,否则返回错误码。

C 复制代码
static int regmap_spi_gather_write(void *context, const void *reg, size_t reg_len, const void *val, size_t val_len)
{
	struct device *dev = context;
	struct spi_device *spi = to_spi_device(dev);
	struct spi_message m;
	u32 addr;
	struct spi_transfer t[2] = {
		{.tx_buf = &addr, .len = reg_len, .cs_change = 0,},
		{.tx_buf = val, .len = val_len},
	};
	addr = TCAN4X5X_WRITE_CMD | (*((u16*)reg) << 8 ) | val_len >> 2;
	spi_message_init(&m);
	spi_message_add_tail(&t[0], &m);
	spi_message_add_tail(&t[1], &m);
	return spi_sync(spi, &m);

}

上述代码展示了静态初始化会在函数的返回路径上丢弃消息和传输。在某些情况下,驱动程序可能希望在其生命周期内预先分配消息及其传输结构,以避免频繁初始化。在这种情况下,可以使用spi_message_alloc()动态分配内存,并使用spi_message_free()释放内存。它们的原型如下:

其中,ntrans是要为这个新的spi_message分配的传输数量;而flags是新分配内存的标志,这里使用GFP_KERNEL就足够了。如果成功,将返回分配的新消息及其传输结构。可以使用内核的链表相关宏来访问传输元素,如list_first_entry、list_next_entry和list_for_each_entry。下面是一些例子:

C 复制代码
static void my_complete(void *context)
{
	struct spi_message *msg = context;
	[...]
	spi_message_free(msg);
}

static int example_spi_async(struct spi_device *spi, struct my_fake_spi_reg *cmds, unsigned len)
{
	struct spi_transfer *xfer;
	struct spi_message *msg;
	// 步骤1:动态分配内存:1个spi_message + len个spi_transfer(GFP_KERNEL=进程上下文分配)
	msg = spi_message_alloc(len, GFP_KERNEL);
	if (!msg)
		return -ENOMEM;
	
	// 步骤2:设置异步回调和上下文:传输完成后调用my_complete,把msg本身传给回调
	msg->complete = my_complete;
	msg->context = msg;// 关键:把msg指针作为上下文,回调里能拿到它
	
	// 步骤3:遍历所有分配的spi_transfer,逐个配置参数(重点是这个循环)
	list_for_each_entry(xfer, &msg->transfers, transfer_list)
	{
		xfer->tx_buf = (u8 *)cmds; // 每个传输的发送缓冲区指向当前命令
		[...]
		xfer->len = 2;// 每个传输固定发送2字节
		xfer->cs_change = true;// 每个传输后临时切换CS
		cmds++;// 命令指针后移,下一个传输处理下一个命令
	}
	return spi_async(spi, msg);
}

通过上面的代码片段,我们不仅了解了如何动态地分配消息及其传输结构,还知道了如何使用spi_async()函数。这个例子没什么用,因为分配的消息及其传输结构在传输完成后会被立即释放。动态分配的最佳实践是动态分配tx和rx缓冲区,并在驱动程序的生命周期内将它们保持在触手可及的范围内。

C 复制代码
struct regmap_spi_prv {
	struct spi_device *spi;
	struct spi_message *m;
	struct spi_transfer *t;
};

static int regmap_spi_probe(struct spi_device *spi)
{
	struct regmap_spi_priv *priv = devm_kzalloc(&spi->dev, sizeof(*priv), GFP_KERNEL);
	if (!priv) return -ENOMEM;
	priv->m = spi_message_alloc(2, GFP_KERNEL);
	if (!priv->m) return -ENOMEM;
	priv->t = priv->m->transfers;
	priv->spi = spi;
	spi_set_drvdata(spi, priv);
	return 0;
}

static int regmap_spi_gather_write(void *context, const void *reg, size_t reg_len, const void *val, size_t val_len)
{
	struct device *dev = context;
	struct spi_device *spi = to_spi_device(dev);
	struct regmap_spi_priv *priv = spi_get_drvdata(spi);
	u32 addr;
	
	// 复用预先分配的transfer,只需修改关键参数(不用重新初始化)
	priv->t[0].tx_buf = &addr;
	priv->t[0].len = reg_len;
	priv->t[0].cs_change = 0;
	priv->t[1].tx_buf = val;
	priv->t[1].len = val_len;

	// 拼接地址
	addr = TCAN4X5X_WRITE_CMD | (*((u16*)reg) << 8 ) | val_len >> 2;

	// 重新初始化消息(复用只需重置,不用重新分配)
	spi_message_init(priv->m);
	spi_message_add_tail(&priv->t[0], priv->m);
	spi_message_add_tail(&priv->t[1], priv->m);

	// 同步/异步都安全:堆内存不会随函数返回丢弃
	return spi_sync(spi, priv->m);
}

// remove函数:释放动态分配的内存(驱动生命周期结束)
static void regmap_spi_remove(struct spi_device *spi)
{
	struct regmap_spi_priv *priv = spi_get_drvdata(spi);
	if (priv->m) spi_message_free(priv->m); // 释放消息+传输
}

但要注意,设备驱动程序负责组织消息,并按照设备最合适的方式进行传输。

  • 考虑何时开始双向读写,以及spi_transfer请求序列是如何安排的。

    传输顺序:比如 SPI Flash 读操作是 "先发读命令→发地址→收数据",驱动要把这三步做成 3 个 spi_transfer,按顺序加入消息;TCAN4X5X 写操作是 "先发命令帧→发数据",驱动就做 2 个 spi_transfer。

    双向读写时机:如果设备支持全双工(比如同时发命令、收状态),驱动要给同一个 spi_transfer 同时设置 tx_buf(发命令)和 rx_buf(收状态);如果是半双工(先写后读),就拆成两个 spi_transfer(一个只写,一个只读)。

  • I/O缓冲区准备是指在每个传输方向都会为每个spi_transfer封装一个缓冲区,支持全双工传输(即使一个指针为NULL,在这种情况下,控制器将使用其虚拟缓冲区之一)​。

    SPI 控制器的硬件逻辑是天生全双工(时钟每跳变一次,同时发 1 位、收 1 位),哪怕驱动只需要 "只写" 或 "只读",也要适配这个特性:

    每个 spi_transfer 可以同时封装 tx_buf(发送缓冲区)和 rx_buf(接收缓冲区):

    只写:rx_buf=NULL,控制器会用 "虚拟缓冲区"(空内存)接收硬件自动收的无效数据,避免硬件状态异常;

    只读:tx_buf=NULL,控制器会用虚拟缓冲区发送无效数据(如 0xFF),满足全双工硬件时序;

    全双工:同时设置 tx_buf 和 rx_buf,控制器同时收发。

    驱动责任:为每个传输方向准备独立的、DMA 安全的缓冲区,哪怕只用到一个方向 ------ 这是 "设备最合适的方式"(避免硬件报错)。

  • 可以选择使用spi_transfer.delay_usecs来定义传输后的时间间隔。

    delay_usecs(内核字段是 delay_us)是驱动给设备的 "处理窗口期":

    不同设备的处理速度不同:比如有的传感器接收命令后,需要 10μs 才能把采样数据加载到输出寄存器,驱动就设 delay_us=10;

    延迟时机:在当前 spi_transfer 完成后、下一个传输 / 消息结束前触发,配合 cs_change 使用时,延迟发生在 CS 切换后,确保设备有足够时间处理命令。

    核心:不是随便设 0,要按设备手册的 "最小延迟要求" 设置,避免数据读取错误。

  • 考虑CS是否应该使用spi_transfer.cs_change标志,以便在传输后的任何时间间隔改变状态(变为不活动)​。

    cs_change 决定传输后 CS 是否临时切换(释放),驱动要结合设备要求和性能优化来选:

    设备要求层面:比如有的设备规定 "每个命令帧后必须释放 CS 确认",驱动就设 cs_change=true;有的设备要求 "整个消息期间 CS 保持激活",就设 false;

    性能优化层面:连续给同一设备发消息时,设 cs_change=true(最后一个传输除外),避免完全释放 CS 后重新激活的开销(比如时序等待、控制器重置)。

SPI设备驱动程序利用spi_async()函数将消息排队,注册一个complete回调函数,唤醒消息泵并立即返回。当传输完成时,complete回调函数将被调用。因为消息队列和消息泵调度都不会被阻塞,所以spi_async()函数被认为是上下文无关的。然而,它要求你等待complete回调函数执行完才能访问你提交的spi_transfer指针所指向的缓冲区。另外,spi_sync()函数会将消息排队并阻塞,直至它们完成,而不需要调用complete回调函数。当spi_sync()函数返回时,就可以安全地访问数据缓冲区了。如果在drivers/spi/spi.c中查看spi_sync()函数的实现,就会看到它利用spi_async()函数让调用线程进入睡眠状态,直至调用complete回调函数。从4.0内核开始,Linux系统对spi_sync()函数做了改进,当队列中没有任何东西时,消息泵将在调用者的上下文中执行,而不是在消息泵线程中执行,从而避免了上下文切换的开销。

SPI设备驱动程序抽象和架构

SPI设备驱动程序由数据结构struct spi_driver组成,该数据结构已用一些驱动函数填充,这些函数允许你探测和控制底层设备。

探测SPI设备

SPI设备由spi_driver.probe回调函数探测。spi_driver.probe回调函数负责确保驱动程序在和设备绑定到一起之前识别出给定的设备。这个回调函数的原型如下:

如果成功,该回调函数必须返回0,否则返回错误码。唯一的参数是要探测的SPI设备,其结构已经由内核根据设备树中的描述预先初始化。

但是,正如我们在描述其结构时所看到的,SPI设备的大多数属性可以被覆盖。如果SPI设备不使用其默认方式工作,则SPI协议驱动程序可能需要更新传输模式。它们可能需要根据初始值更新时钟频率或字长。这要归功于spi_setup()辅助函数,其原型如下:

spi_setup() 不是随便能调用的,有两个核心约束(违反会导致系统崩溃或传输异常):

  1. 调用上下文:必须在 "能独占睡眠" 的上下文(进程上下文)
    函数说明里的 "必须在能够独占睡眠的上下文中调用",翻译过来就是:
    ✅ 允许调用的场景:进程上下文(比如驱动的probe函数、read/write函数)------ 因为spi_setup()可能会睡眠(比如等待控制器释放硬件资源、更新寄存器);
    ❌ 禁止调用的场景:中断上下文、原子上下文(比如spin_lock保护的代码段)------ 这些场景要求 "快进快出",睡眠会导致中断长时间不返回、系统卡死。
  2. 调用时机:设备无消息挂起,且建议在 I/O 请求前调用
    "设备没有消息挂起":调用spi_setup()时,该 SPI 设备不能有未完成的 SPI 消息(比如spi_async提交的消息还没触发回调)------ 否则修改属性会干扰正在进行的传输,导致数据错乱;
    推荐调用时机:驱动的probe函数里(设备初始化阶段),在提交任何 SPI 读写请求前调用,确保后续所有 I/O 操作都使用新配置;也可在运行时动态调用(比如根据场景调整时钟),只要满足 "无消息挂起" 即可。
  • 生效逻辑:不同属性的生效时机不同
    修改属性后,不是所有变化都立刻生效,核心规则分三类:
    SPI_CS_HIGH(CS 极性):立即生效
    CS 极性是硬件引脚的即时状态(高 / 低电平),调用spi_setup()后会立刻修改控制器的 CS 引脚配置,无需等待后续传输。
    其他属性(时钟、字长、传输模式等):下次访问设备时生效
    比如把时钟从 1MHz 改成 2MHz,spi_setup()只会把新值写入spi_device结构体,不会立刻修改控制器的硬件寄存器;等下一次调用spi_sync()/spi_async()提交消息时,内核才会把新配置写入控制器寄存器,让属性真正生效。
    额外行为:返回路径上设备被取消选中
    无论配置是否成功,spi_setup()返回时都会释放该设备的 CS 信号(取消选中)------ 确保属性修改后,下一次访问设备时 CS 是 "干净" 的状态,避免属性不匹配导致的 CS 时序错误。
    返回值:必须检查,避免配置不支持
    返回0:所有修改的属性都被底层 SPI 控制器支持,配置成功;
    返回非 0 错误码(如-EINVAL、-ENOTSUPP):至少有一个属性不被支持(比如控制器只支持 8/16 位字长,你改 9 位就会失败)。
C 复制代码
#define FAMILY_ID	0x57
static int fake_probe(struct spi_device *spi)
{
	int err;
	u8 id;
	spi->max_speed_hz = min(spi->max_speed_hz, DEFAULT_FREQ);
	spi->bits_per_word = 8;
	spi->mode = SPI_MODE_0;
	spi->rt = true;
	err = spi_setup(spi);
	if (err)
		return err;
	err = get_chip_version(spi, &id);
	if (err)
		return -EIO;
	if (id != FAMILY_ID) {
		dev_err(&spi->dev, "chip family: expected 0x%02x but 0x%02x read\n", FAMILY_ID, id);
		return -ENODEV;
	}
	[...]
	return 0;
}

一个真正的探测函数也可能处理一些驱动状态数据结构或者其他单个设备的数据结构。get_chip_version()函数定义如下:

C 复制代码
#define REG_FAMILY_ID	0x2445
#define DEFAULT_FREQ	10000000

static int get_chip_version(spi_device *spi, u8 *id)
{
	struct spi_transfer t[2];
	struct spi_message m;
	u16 cmd;
	int err;
	cmd = REG_FAMILY_ID;
	spi_message_init(&m);
	memset(&t, 0, sizeof(t));
	
	t[0].tx_buf = &cmd;
	t[0].len = sizeof(cmd);
	spi_message_add_tail(&t[0], &m);
	
	t[1].rx_buf = id;
	t[1].len = 1;
	spi_message_add_tail(&t[1], &m);
	
	return spi_sync(spi, &m);
}
C 复制代码
// 第一步:定义"驱动状态数据结构"(单个设备专属)
struct fake_chip_priv {
    struct spi_device *spi;       // 绑定当前SPI设备指针
    u8 chip_id;                   // 保存读取到的芯片ID(比如0x57)
    u32 chip_version;             // 保存芯片版本(扩展字段)
    bool is_initialized;          // 设备初始化状态标志(是否就绪)
    struct mutex lock;            // 并发保护锁(防止多线程操作冲突)
    struct spi_message *msg;      // 预先分配的SPI消息(避免频繁初始化)
    struct spi_transfer *xfer;    // 预先分配的传输结构
    // 还可以加工作队列、中断信息、缓存数据等
};

// 第二步:真正的probe函数会初始化这个数据结构
static int fake_probe(struct spi_device *spi)
{
    int err;
    u8 id;
    // 1. 配置SPI参数(示例里的逻辑)
    spi->max_speed_hz = min(spi->max_speed_hz, DEFAULT_FREQ);
    spi->bits_per_word = 8;
    spi->mode = SPI_MODE_0;
    spi->rt = true;
    err = spi_setup(spi);
    if (err) return err;

    // 2. 读取芯片ID(示例里的逻辑)
    err = get_chip_version(spi, &id);
    if (err) return -EIO;
    if (id != FAMILY_ID) {
        dev_err(&spi->dev, "chip family mismatch\n");
        return -ENODEV;
    }

    // 3. 真正的probe会做的:分配+初始化"驱动状态数据结构"
    struct fake_chip_priv *priv = devm_kzalloc(&spi->dev, sizeof(*priv), GFP_KERNEL);
    if (!priv) return -ENOMEM;
    priv->spi = spi;              // 绑定设备
    priv->chip_id = id;           // 保存芯片ID(后续读写函数能直接用)
    priv->is_initialized = true;  // 标记设备就绪
    mutex_init(&priv->lock);      // 初始化锁
    // 预先分配SPI消息/传输(避免后续频繁分配)
    priv->msg = spi_message_alloc(2, GFP_KERNEL);
    if (!priv->msg) return -ENOMEM;
    priv->xfer = priv->msg->transfers;

    // 4. 把私有数据绑定到SPI设备,供后续函数(read/write/ioctl)使用
    spi_set_drvdata(spi, priv);

    // 还可能初始化中断、工作队列、DMA、设备节点(/dev)等
    return 0;
}

在驱动程序中提供设备信息

提供一个spi_device_id数组来告诉SPI核心,我们的SPI设备驱动程序都支持哪些设备。在这个数组被填充之后,必须把它分配给spi_driver.id_table字段。此外,为了进行设备匹配和模块加载,还需要将相同的数据提供给MODULE_ DEVICE_TABLE宏。在include/linux/mod_devicetable.h中,struct spi_device_id数据结构被声明如下:

C 复制代码
#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 const struct of_device_id foobar_of_match[] = {
	{.compatible = "packtpub,foobar-device"},
	{.compatible = "packtpub,barfoo-device"},
	{},
};
MODULE_DEVICE_TABLE(of, foobar_of_match);

static struct spi_driver foo_driver = {
	.driver = {
		.name = "foo",
		.of_match_table = of_match_ptr(foobar_of_match),
	},
	.probe = my_spi_probe,
	.id_table = foo_idtable,
};

spi_driver.remove

必须使用spi_driver.remove回调函数来释放占用的每个资源,并撤销探测函数执行的所有操作。这个回调函数的原型如下:

其中,spi是SPI设备的数据结构,与探测函数使用的参数相同,这简化了从探测到删除设备期间的设备状态数据结构的跟踪过程。成功时返回0,失败时返回错误码。你还必须确保设备处于一致且稳定的状态。下面是一个例子:

驱动程序初始化和注册

驱动程序实现到这一步,代码几乎已经准备好了,接下来要做的就是通知SPI设备驱动程序的SPI核心。对于SPI设备驱动程序,SPI核心提供了spi_register_driver()和spi_unregister_driver()函数来注册和注销SPI设备驱动程序与SPI核心。这两个函数的原型如下:

在这两个函数中,sdrv是先前设置的SPI设备驱动程序数据结构。注册成功时返回0,失败时返回错误码。驱动程序的注册和注销通常是在模块初始化函数和模块退出函数中完成的。以下是注册SPI设备驱动程序的典型示例:

如果你在初始化模块时除了注册/注销驱动程序什么也不做,则可以使用module_spi_driver()宏来分解代码,如下所示:

这个宏将填充模块初始化函数和清理函数,并在其中调用spi_register_driver()和spi_unregister_driver()函数。

实例化SPI设备

SPI从设备节点必须是SPI控制器节点的子节点。在主模式下,可以存在一个或多个从设备节点(不超过CS的数量)​。

必需的属性如下。

· compatible:在驱动程序中定义的用于设备匹配的兼容字符串。

· reg:设备相对于控制器的CS索引。

· spi-max-frequency:设备的最大SPI时钟频率,单位为Hz。

所有从设备节点都可以包含以下可选属性。

· spi-cpol:布尔属性,只要写了这个属性(无需赋值),表示设备需要CPOL=1(时钟空闲时为高电平);默认是 CPOL=0(空闲低)。

· spi- cpha:布尔属性,只要写了这个属性,表示设备需要CPHA=1(第二个时钟边沿采样数据);默认是 CPHA=0(第一个边沿采样)。

· spi-cs-hi-h:空属性,表示设备需要CS高电平有效。

· spi-3wire:布尔属性,表示设备需要三线模式才能正常工作。

· spi-lsb-first:布尔属性,表示设备需要LSB优先模式才能正常工作。

· spi-tx-bus-width:MOSI 总线的宽度(数据线数量),默认 1(单根线);比如设为 2 就是 2 根 MOSI 线传输,提升速率(需控制器支持)。

spi-rx-bus-width:MISO 总线的宽度,默认 1;同理,设为 2 就是 2 根 MISO 线。

· spi-rx-delay-s:读取传输完成后,延迟 N 微秒再进行下一步操作(给设备处理数据的时间)。

· spi-tx-delay-us:写入传输完成后,延迟 N 微秒再进行下一步操作。

如何在用户空间直接驱动SPI设备

处理SPI设备的常用方法是编写内核代码来驱动SPI设备。如今,spidev接口使得无须编写内核代码就可以处理此类设备。然而,这个接口的使用应该被限制在简单的用例中,例如与(slave microcontroller)从微控制器通信或用于原型设计。使用此接口,你将无法处理设备可能支持的各种中断(IRQ),并且无法利用其他内核框架。

spidev接口以/dev/spidevX.Y的形式公开了一个字符设备节点。其中X代表设备所在的总线,Y代表设备树中分配给设备节点的CS索引(相对于控制器)​。例如,/dev/spidev1.0表示SPI总线1上的设备0。这同样适用于sysfs目录项,形式是/sys/class/spidev/spidevX.Y。

在字符设备出现于用户空间之前,设备节点必须在设备树中声明为SPI控制器节点的子节点。示例如下:

其中,spidev@0对应于SPI设备节点。reg = <0>则告诉控制器此设备正在使用第一个CS信号线(索引从0开始)​。compatible="semtech,sx1301"用于匹配spidev驱动程序中的条目。不建议使用"spidev"作为兼容字符串,否则将会发出警告。最后,spi-max-frequency = <20000000>设置了设备运行时的默认时钟频率(这里是20MHz)​,除非使用适当的API对它进行更改。

从用户空间来看,处理spidev接口所需的头文件如下:

因为是字符设备,所以只允许使用基本的系统调用,例如open()、read()、write()、ioctl()和close()系统调用。下面的例子展示了一些基本用法,其中只包含read()和write()系统调用:

C 复制代码
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h?
#include <linux/types.h>
#include <linux/spi/spidev.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv)
{
	int i, fd;
	char *device = "/dev/spidev0.0";
	char wr_buf[] = {0xff, 0x00, 0x1f, 0x0f};
	char rd_buf[10];
	fd = open(device, O_RDWR);
	if (fd <= 0) {
		printf("Failed to open SPI device %s\n", device);
		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;
}

在上面的代码中,你应该注意到标准的read()和write()系统调用是半双工的,并且CS是无效的。

为了做到全双工,你别无选择,只能使用ioctl()系统调用。在这里,你可以根据需要传递输入和输出缓冲区。此外,通过 ioctl()系统调用,你可以使用一组SPI_IOC_RD_*和SPI_IOC_WR_*命令来获取RD并设置WR以覆盖设备当前的设置。完整的列表和文档可以在内核源代码的Documentation/spi/spidev目录中找到。

SPI_IOC_MESSAGE (N):执行 "复合操作" 且不释放 CS

SPI_IOC_MESSAGE(N)是ioctl的核心请求(N 表示要执行的transfer数量),解决了你之前遇到的 "CS 无效" 问题:

把 N 个struct spi_ioc_transfer(用户态传输单元)打包成一个 "消息",内核会按顺序执行这些 transfer,整个过程中 CS 保持激活(不中途释放),直到所有 transfer 执行完才释放 ------ 这就是 "在不停用 CS 的情况下执行复合操作"。

struct spi_ioc_transfer:用户态的 "传输单元"(对应内核的 spi_transfer)

这个结构体是用户空间和内核struct spi_transfer的 "等价映射"------ 内核会把用户态的spi_ioc_transfer转换成内核态的spi_transfer执行,核心字段完全对应:

ioctl()系统调用允许在不停用CS的情况下执行复合操作,并使用SPI_IOC_MESSAGE(N)请求。这将产生一个新的数据结构,即struct spi_ioc_transfer,它是用户空间中与struct spi_transfer等价的数据结构。使用ioctl命令的例子如下:

C 复制代码
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/spi/spidev.h>

// 错误处理函数:打印错误并返回-1
static int pabort(const char *s)
{
    perror(s);
    return -1;
}

// SPI设备配置函数:设置模式、速度、字节序、字长
static int spi_device_setup(int fd)
{
    int mode, speed, wr_ret, rd_ret, lsb_first;
    int bits = 8;

    // 1. 设置SPI模式为MODE0
    mode = SPI_MODE_0;
    wr_ret = ioctl(fd, SPI_IOC_WR_MODE, &mode);
    rd_ret = ioctl(fd, SPI_IOC_RD_MODE, &mode);
    if ((wr_ret < 0) || (rd_ret < 0)) {
        return pabort("can't set spi mode");
    }

    // 2. 设置最大时钟频率为8MHz
    speed = 8000000;
    wr_ret = ioctl(fd, SPI_IOC_WR_MAX_SPEED_HZ, &speed);
    rd_ret = ioctl(fd, SPI_IOC_RD_MAX_SPEED_HZ, &speed);
    if ((wr_ret < 0) || (rd_ret < 0)) {
        return pabort("fail to set max speed hz");
    }

    // 3. 设置MSB优先(lsb_first=0)
    lsb_first = 0;
    wr_ret = ioctl(fd, SPI_IOC_WR_LSB_FIRST, &lsb_first);
    rd_ret = ioctl(fd, SPI_IOC_RD_LSB_FIRST, &lsb_first);
    if ((wr_ret < 0) || (rd_ret < 0)) {
        return pabort("Fail to set MSB first"); // 错误时返回,不再继续
    }

    // 4. 设置字长为8位
    bits = 8;
    wr_ret = ioctl(fd, SPI_IOC_WR_BITS_PER_WORD, &bits);
    rd_ret = ioctl(fd, SPI_IOC_RD_BITS_PER_WORD, &bits);
    if ((wr_ret < 0) || (rd_ret < 0)) {
        return pabort("Fail to set bits per word");
    }

    return 0;
}

// SPI传输函数:先发命令0x9f,再收发数据
static int do_transfer(int fd)
{
    int ret;
    char txbuf[] = {0x0B, 0x02, 0xB5};
    char rxbuf[3] = {0};
    char cmd_buff = 0x9f;

    // 定义2个transfer:先发命令,再全双工收发数据
    struct spi_ioc_transfer tr[2] = {
        [0] = {
            .tx_buf = (unsigned long)&cmd_buff,
            .len = 1,
            .cs_change = 0,          // 关键:不释放CS,保持激活
            .delay_us = 50,          // 修正:用户态字段是delay_us
            .bits_per_word = 8,
        },
        [1] = {
            .tx_buf = (unsigned long)txbuf,
            .rx_buf = (unsigned long)rxbuf,
            .len = sizeof(txbuf),    // 修正:分号→逗号,变量名txbuf
            .bits_per_word = 8,
        },
    };
    
    // 提交2个transfer组成的消息
    ret = ioctl(fd, SPI_IOC_MESSAGE(2), tr);
    if (ret < 0) { // 修正:失败返回-1,成功返回总字节数(4)
        perror("can't send spi message");
        return -1; // 返回错误,不直接exit
    }

    // 打印接收的字节(加空格,更易读)
    printf("Received data: ");
    for (int i = 0; i < sizeof(rxbuf); i++) {
        printf("%.2x ", rxbuf[i]);
    }
    printf("\n");

    return 0;
}

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

    // 打开SPI设备
    fd = open(device, O_RDWR);
    if (fd < 0) {
        pabort("Can't open device");
        exit(1); // 规范退出
    }
    
    // 配置SPI设备
    error = spi_device_setup(fd);
    if (error) {
        close(fd); // 先关闭fd再退出
        exit(1);
    }
    
    // 执行SPI传输
    error = do_transfer(fd);
    if (error) {
        close(fd);
        exit(1);
    }
    
    // 关闭设备
    close(fd);
    return 0;
}
相关推荐
春日见2 小时前
在虚拟机上面无法正启动机械臂的控制launch文件
linux·运维·服务器·人工智能·驱动开发·ubuntu
松涛和鸣2 小时前
Linux Makefile : From Basic Syntax to Multi-File Project Compilation
linux·运维·服务器·前端·windows·哈希算法
Predestination王瀞潞3 小时前
JDK安装及环境变量配置
java·linux·开发语言
LF3_3 小时前
配置ssh免密登录
运维·ssh
再睡一夏就好3 小时前
深入Linux线程:从轻量级进程到双TCB架构
linux·运维·服务器·c++·学习·架构·线程
小小药3 小时前
09-vmware配置虚机连接互联网-nat模式
linux·运维·centos
广东大榕树信息科技有限公司3 小时前
如何通过国产信创动环监控系统优化工厂环境管理?
运维·网络·物联网·国产动环监控系统·动环监控系统
Bright Xu4 小时前
Qemu 安装 LoongArch架构 Fedora Remix F42 Linux系统
linux·loongarch·国产cpu
莫白媛4 小时前
Linux创作笔记综合汇总篇
linux·运维·笔记