关于spi_message,spi_transfer的再明白

[复制链接]
发表于 昨天 20:12 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
一、 焦点概念明白:spi_message 与 spi_transfer

在 Linux 内核的 SPI 驱动框架中,spi_transfer 和 spi_message 是最焦点的两个数据布局。假如你用前面我们聊过的“分层”“打包”的头脑来明白它们,就会非常直观:

  • spi_transfer: 是真正负责硬件传输的最小原子单元。它对应的是一段一连的、传输特性雷同的读写数据流。
  • spi_message: 是一个传输队列的载体/事件(Transaction)。它自己不包罗具体的传输数据,而是作为一个“打包的容器”,把一个或多个 spi_transfer 串联成一个不可分割的、完备的通讯任务
我们可以用一个形象的实际生存比喻,然后团结代码来彻底理清它们。
1. 形象的比喻:快递总装箱 vs 内里的独立包裹

假设你通过 SPI 接口去读写一个 SPI 闪存(Flash)芯片。要读取某个所在的数据,你通常须要先发送 1字节的读下令 + 3字节的所在,然后紧接着吸收 64字节的数据。
在这个过程中:

  • spi_transfer 就像是单个独立的包裹。 * 包裹 A 装着“写出去的下令和所在”(4字节)。
  • 包裹 B 装着“准备收返来的数据”(64字节)。
  • spi_message 就像是快递公司的大总装箱。
  • 它负责把包裹 A 和包裹 B 塞进同一个大箱子里,封好口,打上标签。
  • 快递员(SPI 控制器驱动)在运送这个大箱子(spi_message)期间,绝对不能停下往复送别人的件,必须一口气把这个箱子里全部的包裹(spi_transfer)按次序送完。这就包管了原子性。
2. 为什么不归并成一个 spi_transfer?

你大概会问:既然都是要发数据,为什么不把下令、所在、数据直接拼成一个大数组,用一个 spi_transfer 发已往呢?
这里有两个焦点缘故原由:

  • 读写方向切换(半双工/全双工混淆): SPI 是全双工的。但在许多实际应用中,前 4 个字节我们只须要发送(TX),后 64 个字节我们只须要吸收(RX)。通过拆分成两个 spi_transfer,一个只填 tx_buf,一个只填 rx_buf,内核驱动就能精准控制硬件的 DMA 或 FIFO,制止内存浪费和紊乱。
  • 硬件参数的动态调解: 每一个独立的 spi_transfer 都可以有自己独立的硬件举动!比方:你可以在第一个 transfer 竣过后让 CS 线保持拉低(cs_change = 0),并在第二个 transfer 改变时钟频率(speed_hz)大概增长一段延长(delay_usecs)。这种颗粒度的控制,只能在 spi_transfer 级别实现。
3. 焦点布局体看关键字段

看看 Linux 内核源码(include/linux/spi/spi.h)中它们的长相,我们只挑最焦点的字段:
struct spi_transfer(原子包裹)
  1. struct spi_transfer {
  2.     const void  *tx_buf;          /* 要发送的数据缓冲区指针(CPU内存) */
  3.     void        *rx_buf;          /* 要接收的数据缓冲区指针 */
  4.     unsigned    len;              /* 本次传输的字节长度 */
  5.     u32         speed_hz;         /* 可选:临时改变本次传输的时钟频率 */
  6.     u16         delay_usecs;      /* 可选:本次传输结束后,延迟多少微秒再进行下一个 transfer */
  7.     u8          bits_per_word;    /* 可选:字长(如 8位、16位) */
  8.     u8          cs_change;        /* 关键:本次传输结束后,是否要改变(拉高)片选CS线 */
  9.    
  10.     struct list_head transfer_list; /* 链表节点,用于将自己挂载到 spi_message 中 */
  11. };
复制代码
struct spi_message(大装箱)
  1. struct spi_message {
  2.     struct list_head transfers;   /* 链表头:用来串联所有挂载进来的 spi_transfer */
  3.     struct spi_device *spi;       /* 目标 SPI 从设备 */
  4.     void (*complete)(void *context); /* 异步传输完成后的回调函数指针 */
  5.     void *context;                /* 传递给回调函数的参数 */
  6.    
  7.     unsigned actual_length;       /* 整个 message 实际传输成功的总字节数 */
  8. };
复制代码
4. 举例分析:在驱动中怎样使用它们?

我们以读取一个 SPI 传感器的寄存器为例(须要先写 1 字节寄存器所在,再读 2 字节数据)。
场景:读取传感器数据
  1. #include <linux/spi/spi.h>
  2. int read_sensor_register(struct spi_device *spi, u8 reg_addr, u8 *res_buf)
  3. {
  4.     struct spi_message msg;
  5.     struct spi_transfer xfers[2]; // 我们需要两个阶段(两个包裹)
  6.     u8 tx_data = reg_addr;
  7.     int status;
  8.     // 步骤 1: 初始化 spi_message 容器
  9.     spi_message_init(&msg);
  10.     // 步骤 2: 填充第一个包裹 —— 发送寄存器地址
  11.     memset(xfers, 0, sizeof(xfers));
  12.     xfers[0].tx_buf = &tx_data;   // 只写
  13.     xfers[0].len = 1;             // 1 字节
  14.     // 注意:默认情况下,cs_change=0,意味着发完这个字节后,CS片选线保持拉低,不释放
  15.     spi_message_add_tail(&xfers[0], &msg); // 塞入大箱子
  16.     // 步骤 3: 填充第二个包裹 —— 接收传感器返回的数据
  17.     xfers[1].rx_buf = res_buf;    // 只读
  18.     xfers[1].len = 2;             // 2 字节
  19.     spi_message_add_tail(&xfers[1], &msg); // 塞入大箱子
  20.     // 步骤 4: 把大箱子交给 SPI 控制器,同步阻塞传输
  21.     // 控制器驱动会严格保证:拉低CS -> 执行xfers[0] -> 执行xfers[1] -> 拉高CS
  22.     status = spi_sync(spi, &msg);
  23.    
  24.     if (status < 0) {
  25.         dev_err(&spi->dev, "SPI transfer failed: %d\n", status);
  26.         return status;
  27.     }
  28.     return 0;
  29. }
复制代码
快捷封装:内核提供的“懒人工具”

由于“先写后读”大概“只写/只读”的场景太常见了,内核开辟者在 spi.h 里使用 spi_message 和 spi_transfer 封装了许多好用的简化函数,省去了你手动去 init 和 add_tail 的贫苦。
比方上面的代码,在实际开辟中每每可以直接用一行内核 API 更换:
  1. // 内核内部会自动帮你创建 2 个 transfer 和 1 个 message 并调用 spi_sync
  2. status = spi_write_then_read(spi, &reg_addr, 1, res_buf, 2);
复制代码
总结

在 Linux SPI 驱动的天下里:

  • 面临硬件举动和内存分布的复杂性,内核用 spi_transfer 提供了风雅到“单次一连脉冲”的控制本事。
  • 面临高并发和事件原子性的需求,内核用 spi_message 提供了一个“大包大揽、一次通关”的容器,确保多线程下对 SPI 总线竞争时,一个完备的协议事件不会被其他装备的哀求无情打断。
二、 多线程并发与总线竞争处置惩罚机制

在 Linux 内核中,当一个 spi_message 正在总线上被处置惩罚时,假如有其他驱动步伐或线程也想通过同一个 SPI 总线发送消息,Linux 的 SPI 焦点框架(SPI Core)和主控驱动(Master/Host Driver)已经操持了一套完满的队列与互斥机制来处置惩罚这种竞争。
简单来说,它的处置惩罚原则是:串行化、列队、绝不打断。
具体是怎样协作的,我们可以从以下几个维度来明白:
1. 焦点机制:基于队列的串行化(Serialization)

Linux 内核的 SPI 子体系(特殊是当代内核版本)内部实现了一个基于工作队列(Workqueue)的内核线程。
全部的 SPI 传输哀求,无论是来自传感器 A、闪存 B 还是屏幕 C,终极都会被放入 SPI 控制器(spi_controller / spi_master)维护的一个硬件消息队列中。
当一个 spi_message 正在总线上被实行时:

  • 新消息的处置惩罚: 别的的哀求不会直接冲到硬件总线上,而是被作为新的节点挂载到这个 spi_controller->queue 队列的末端。
  • 按次序调治: 内核的 SPI 工作线程(Worker Thread)会像食堂列队打饭一样,一个接一个地从队列头部取出 spi_message,交给底层硬件驱动去实行。只有前一个 spi_message 里的全部 spi_transfer 全部传输完毕、片选开释,下一个 spi_message 才会得到总线控制权。
2. 发起哀求的线程会怎样?(同步 vs 异步)

另一个消息“须要总线”时,发起这个哀求的软件线程会处于什么状态,取决于它调用的是同步接口还是异步接口
场景 A:调用 spi_sync()(同步壅闭)—— 最常见

假如别的的线程调用了 spi_sync(spi, &new_msg):

  • spi_sync 会把 new_msg 放入控制器的列队队列中。
  • 放入队列后,调用线程会立即进入休眠状态(Sleep),让出 CPU 给其他任务。
  • 当轮到 new_msg 并在硬件上全部传输完成后,SPI 子体系会触发一个完成信号(Completion),唤醒这个休眠的线程。
  • 线程醒来,spi_sync() 函数返回 0(乐成),接着往下实行。
场景 B:调用 spi_async()(异步非壅闭)

假如别的的线程(比如在停止处置惩罚函数或高频定时器中)调用了 spi_async(spi, &new_msg):

  • spi_async 同样把 new_msg 放入列队队列。
  • 它不会期待,而是立即返回 0。 发起哀求的线程可以继续去干别的事变。
  • 当总线空闲并轮到 new_msg 传输完成后,内核会自动调用你在 new_msg.complete 字段里注册的回调函数,关照你“数据已经发完了”。
3. 完满的硬件级物理隔离:CS 片选线

在物理层上,SPI 是通过硬连线的 片选线(CS/SS) 来区分差别装备的。
纵然队列里堆满了来自差别装备(装备 A 和装备 B)的 spi_message,硬件上也绝对不会发生“数据串线”或“相互污染”,由于:

  • 当处置惩罚装备 A 的 spi_message 时,控制器的硬件驱动会只拉低装备 A 的 CS 线,此时装备 B 的 CS 线保持高电平。
  • 纵然总线上的 CLK、MOSI 信号在剧烈跳变,装备 B 的硬件接口由于没有被片选使能,会完全无视这些信号(处于高阻态)。
  • 当 A 的 spi_message 实行完,A 的 CS 被拉高;轮到 B 时,B 的 CS 才会被拉低。
4. 特殊机制:抢占与独占总线(Bus Locking)

在少少数极其看重实时性大概须要一连霸占总线的场景下,内核还提供了两种高级机制:
机制一:SPI 消息的优先级

假如内核设置了实时调治,SPI 的工作队列线程可以运行在很高的实时优先级(如 SCHED_FIFO)。固然它不能“掐断”当前正在传输的字节,但它能确保一旦当前 spi_message 竣事,高优先级的消息能立即插队处置惩罚。
机制二:总线锁(spi_bus_lock / spi_bus_unlock)

假如你有某些非常特殊的利用(比方:必须一连向一个装备发送好几个 spi_message,期间绝对不答应其他装备的 spi_message 插队打断,否则该装备就会复位),你可以使用总线锁:
  1. spi_bus_lock(spi->controller); // 锁住整个 SPI 总线
  2. // 此时其他任何设备调用 spi_sync/spi_async 都会在这里阻塞排队
  3. spi_sync(spi, &msg1);
  4. spi_sync(spi, &msg2);
  5. spi_bus_unlock(spi->controller); // 释放总线锁,队列里的其他消息开始处理
复制代码
总结

在 Linux 内核里,SPI 总线是一个受到严酷羁系的单窗口独占资源
任何“别的的消息”想要使用总线,都必须通过 spi_message 提交给内核队列。正在发送的 spi_message 拥有绝对的原子实行权,厥后的消息只会在队列中静静列队(或让调用线程休眠),直到前人优雅退场。这种机制完满确保了 Linux 在多线程并发驱动外设时的稳固与安全
三、 spi_message 传输期间的当火线程调治与就寝状态

在 spi_message 还没有竣事的时间,发起哀求的当火线程完全有大概,乃至这正是最常见的环境,会被内核调治出去。
许多初学者轻易把“总线事件的原子性(不被其他 SPI 装备打断)”和“线程的可调治性(当前 CPU 线程是否会被切换)”肴杂。实际上,spi_message 在总线上跑的时间,发起调用的这个线程大概率已经被内核切换出去睡觉了,等硬件传完了它才会被重新唤醒。
这背后的焦点逻辑,取决于底层 SPI 控制器驱动是用 “停止/DMA(异步关照)” 还是 “轮询(Polling)” 方式来实现的。
1. 常见环境:使用 停止/DMA 驱动(线程会被调治出去)

在绝大多数嵌入式平台的主控驱动中,SPI 数据的发送和吸收都是靠 硬件停止DMA 完成的。假如你在线程中调用了同步接口 spi_sync(),整个事故的发展脉络是如许的:
  1. 当前线程 (CPU)               SPI 内核工作队列              硬件控制器 (SPI Controller)
  2.      |                             |                               |
  3. 1. 调用 spi_sync()                |                               |
  4.      |---------------------------->|                               |
  5. 2. 线程[进入休眠],让出CPU         | 3. 配置硬件、启动 DMA 传输     |
  6.      |                             |------------------------------>|
  7.      |                             |                               | 4. 硬件疯狂干活...
  8.      |                             X 此时 CPU 空闲,跑其他线程      |    (传输 spi_message)
  9.      |                             X                               |
  10.      |                             X                               | 5. 传输完毕,触发硬件中断
  11.      |                             |<------------------------------|
  12.      |                             | 6. 中断处理函数 (ISR)
  13.      |                             |    发出 Completion 信号
  14.      |<----------------------------|
  15. 7. 线程[被唤醒],重新进入就绪队列
  16.      |
  17. 8. spi_sync() 返回,继续执行
复制代码
2. 线程[进入休眠],让出 CPU(SPI 焦点框架视角的代码分析)

当我们作为驱动开辟者在自己的线程里调用 spi_sync(spi, &msg) 时,内核使用了内核非常经典的期待队列(Wait Queue)完成量(Completion)机制来让我们“睡觉”:
  1. /* 伪代码:源自各 SoC 厂商的 SPI 主控驱动 (如 spi-fsl-dspi.c 或 spi-imx.c) */
  2. static int platform_spi_one_transfer(struct spi_controller *host,
  3.                                      struct spi_device *spi,
  4.                                      struct spi_transfer *xfer)
  5. {
  6.     unsigned long flags;
  7.     u32 dma_ctrl;
  8.     // 1. 设置硬件参数:波特率、时钟极性等
  9.     platform_spi_config_hardware(host, xfer->speed_hz, xfer->bits_per_word);
  10.     // 2. 映射内存缓冲区,准备给 DMA 使用
  11.     // 将 CPU 的虚拟地址 xfer->tx_buf 和 xfer->rx_buf 转换为 DMA 能认的物理地址
  12.     platform_dma_map_buffers(host, xfer);
  13.     // 3. 配置控制器的 DMA 寄存器
  14.     dma_ctrl = readl(host->regs + REG_DMA_CTRL);
  15.     dma_ctrl |= (DMA_CTRL_TX_EN | DMA_CTRL_RX_EN); // 开启 TX/RX DMA 通道
  16.     writel(dma_ctrl, host->regs + REG_DMA_CTRL);
  17.     // 4. 触发 DMA 引擎开始搬运数据(配置源地址、目的地址、长度)
  18.     // 此时,SPI 硬件控制器开始在物理总线上疯狂产生时钟并发送比特流
  19.     dmaengine_submit(host->tx_desc);
  20.     dma_async_issue_pending(host->tx_chan);
  21.     // 5. 硬件已经跑起来了,当前函数可以返回了
  22.     // 注意:这里只是“启动”了硬件,数据并没传完,硬件自己靠 DMA 在后台跑
  23.     return 1;
  24. }
复制代码
假如再往 Linux 内核的调治层(kernel/sched/completion.c)看一眼,wait_for_completion 本质上在做这件事:
  1. /* 源码路径:drivers/spi/spi.c (简化版核心逻辑) */
  2. int spi_sync(struct spi_device *spi, struct spi_message *message)
  3. {
  4.     DECLARE_COMPLETION_ONSTACK(done); // 在栈上定义一个“完成量”结构体 (内部包含一个等待队列)
  5.     int status;
  6.     // 1. 将这个完成量绑定 to 当前要发送的 spi_message 上
  7.     message->complete = spi_sync_complete; // 注册完成后的回调函数
  8.     message->context = &done;              // 把完成量指针作为上下文参数传入
  9.     // 2. 把消息丢进 SPI 核心框架的硬件队列中 (触发上面第3步的硬件启动)
  10.     status = __spi_async(spi, message);
  11.     if (status == 0) {
  12.         /* * 【关键点】代码执行到这里,硬件已经启动了。
  13.          * 接下来,当前线程调用 wait_for_completion()。
  14.          * 这个函数会把当前线程的状态设置为 TASK_UNINTERRUPTIBLE(不可中断休眠),
  15.          * Then 调用 schedule() 主动触发内核调度器,把当前 CPU 让给别的线程跑!
  16.          */
  17.         wait_for_completion(&done);
  18.         
  19.         // --- 线程在此处“断层/冬眠” ----------------------------------------
  20.         // --- 直到硬件中断触发 complete(),线程被唤醒,才会从这里醒来往下走 ---
  21.         status = message->status; // 获取硬件层返回的最终传输状态
  22.     }
  23.     return status;
  24. }
复制代码
3. 完备闭环总结


  • 我们的驱动线程调用 spi_sync(),内部创建了一个闹钟(struct completion done)。
  • 内核把任务派发给 SPI 硬件(设置寄存器、启动 DMA 搬运)。
  • 派发完后,线程立即调用 wait_for_completion() 闭上眼睛睡觉(设置任务状态,调用 schedule() 让出 CPU)。
  • SPI 硬件在背景渐渐传数据。传完末了一个字节,硬件触发SPI/DMA 停止
  • 停止处置惩罚函数(ISR)实行,调用 complete(&done) 拍醒(唤醒)正在睡觉的线程。
  • 我们的线程重新进入 CPU 的停当队列,抢到 CPU 后从 wait_for_completion 反面醒来,拿走数据,完满收工。

免责声明:如果侵犯了您的权益,请联系站长及时删除侵权内容,谢谢合作!qidao123.com:ToB企服之家,中国第一个企服评测及软件市场,开放入驻,技术点评得现金.
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表