第五阶段:SPI 数据通路

第五阶段:SPI 数据通路

分三个小节:数据结构 → 入队 → 消息泵。

5.1 数据结构:spi_transfer 和 spi_message

spi_transfer —— 单个传输段

structspi_transfer{constvoid*tx_buf;// 发什么(NULL = 发全零)void*rx_buf;// 收到哪(NULL = 丢掉收到的数据)unsignedlen;// 多少字节u32 speed_hz;// 本次传输的时钟(0 = 用设备的默认值)u8 bits_per_word;// 字长(0 = 用设备的默认值)unsignedcs_change:1;// 传输完后是否翻转 CSu16 delay_usecs;// 传输完后延时多少微秒structlist_headtransfer_list;// 知识点3:挂在 spi_message.transfers 链表上};

spi_message —— 一组 transfer 的"原子包裹"

structspi_message{structlist_headtransfers;// 知识点3:transfer 链表锚点structspi_device*spi;// 发给哪个设备void(*complete)(void*context);// 知识点2:完成后的回调函数void*context;// 回调的参数unsignedframe_length;// 预计传输总字节数unsignedactual_length;// 实际传输了多少字节intstatus;// 0=成功,负数=失败structlist_headqueue;// 知识点3:挂在 master->queue 上的挂钩};

两者的关系

一个spi_message包含多个spi_transfer。CS 在一个 message 的所有 transfer 之间保持拉低(原子操作)。

spi_message ┌──────────────────────────────────────────┐ │ transfers (锚点) │ │ ├── spi_transfer[0] │ │ │ tx_buf = 命令(1字节) │ │ │ rx_buf = NULL │ │ │ cs_change = 0 ← CS 不变 │ │ │ │ │ ├── spi_transfer[1] │ │ │ tx_buf = 地址(3字节) │ │ │ rx_buf = NULL │ │ │ cs_change = 0 ← CS 不变 │ │ │ │ │ └── spi_transfer[2] │ │ tx_buf = NULL │ │ rx_buf = 数据(256字节) │ │ cs_change = 1 ← 传完翻转CS │ │ │ │ spi → spi0.0 (Flash) │ │ complete = 完成回调 │ │ status = -EINPROGRESS → 0 │ └──────────────────────────────────────────┘ CLK 波形: CS: ‾‾‾\_________________________/‾‾‾ transfer[0] transfer[1] transfer[2] 发送命令 发送地址 接收数据

类比:spi_message 是一个快递包裹。spi_transfer 是包裹里的每件物品。CS 是"正在送货中"的标志——所有物品搬完之前,标志不撤。

5.2 入队路径:spi_sync → 消息挂到 master->queue

完整路径

spi_sync(spi,message)└──__spi_sync(spi,message)├── 构造 completion(信号量) ├── message->complete=spi_complete ← 完成回调:醒来的时候调这个 ├── message->context=&done ← 回调的参数 ├──__spi_queued_transfer(spi,msg)← 消息入队 ├──__spi_pump_messages(master)← 马上尝试发送 └──wait_for_completion(&done)← 阻塞!等传输完成 │ │...消息泵处理中...│ └── spi_finalize_current_message └── message->complete(context)=spi_complete(&done)└──complete(&done)→ 唤醒 wait_for_completion

逐行解读

staticint__spi_sync(structspi_device*spi,structspi_message*message){DECLARE_COMPLETION_ONSTACK(done);intstatus;structspi_master*master=spi->master;unsignedlongflags;status=__spi_validate(spi,message);if(status!=0)returnstatus;message->complete=spi_complete;message->context=&done;message->spi=spi;SPI_STATISTICS_INCREMENT_FIELD(&master->statistics,spi_sync);SPI_STATISTICS_INCREMENT_FIELD(&spi->statistics,spi_sync);/* If we're not using the legacy transfer method then we will * try to transfer in the calling context so special case. * This code would be less tricky if we could remove the * support for driver implemented message queues. */if(master->transfer==spi_queued_transfer){spin_lock_irqsave(&master->bus_lock_spinlock,flags);trace_spi_message_submit(message);status=__spi_queued_transfer(spi,message,false);spin_unlock_irqrestore(&master->bus_lock_spinlock,flags);}else{status=spi_async_locked(spi,message);}if(status==0){/* Push out the messages in the calling context if we * can. */if(master->transfer==spi_queued_transfer){SPI_STATISTICS_INCREMENT_FIELD(&master->statistics,spi_sync_immediate);SPI_STATISTICS_INCREMENT_FIELD(&spi->statistics,spi_sync_immediate);__spi_pump_messages(master,false);}wait_for_completion(&done);status=message->status;}message->context=NULL;returnstatus;

}

第 1 步:构造信号量

DECLARE_COMPLETION_ONSTACK(done);// 栈上分配一个 completion

completion 是一个"信号量"——用来让一个线程等待另一个线程完成某事。

类比:你快递寄出去了,回家等。快递员送到后按门铃。你听到门铃去开门。——completion 就是这门铃。

第 2 步:设置完成回调

message->complete=spi_complete;// 知识点2:函数指针message->context=&done;// 传进回调的参数

spi_complete内部就一行:

staticvoidspi_complete(void*arg){complete(arg);// 唤醒在当前 completion 上等待的线程}

第 3 步:消息入队

__spi_queued_transfer(spi,message,false);// ↑ need_pump = false,下面手动触发

第二阶段学过的函数——list_add_tail(&msg->queue, &master->queue)

第 4 步:立即尝试发送

__spi_pump_messages(master,false);

不用等 kthread_worker 被唤醒——直接在调用者线程上下文中尝试发一次。如果传送带闲着(!master->busy),马上开始传输。如果正忙,它入队了就行。

第 5 步:阻塞等待

wait_for_completion(&done);

调用者在这里睡觉。等到消息泵处理完消息,调用spi_complete,才能醒来继续往下执行。

// 醒来后:status=message->status;// 0 = 成功,负数 = 失败message->context=NULL;returnstatus;

同步 vs 异步

spi_sync: spi_async: 入队 入队 立即尝试发送 不等待 wait_for_completion(阻塞) 直接返回 传输完成 ← 醒来 传输完成 → 回调通知 返回结果

spi_sync 内部调了 spi_async 的入队逻辑,加了一个 completion 等待。异步是地基,同步是异步 + 睡觉。

__spi_queued_transfer —— 把消息挂到货架上

staticint__spi_queued_transfer(structspi_device*spi,structspi_message*msg,bool need_pump){structspi_master*master=spi->master;spin_lock_irqsave(&master->queue_lock,flags);// ① 拿锁,关中断if(!master->running){// ② 队列停了?spin_unlock_irqrestore(&master->queue_lock,flags);return-ESHUTDOWN;// 快递站关门了}msg->actual_length=0;// ③ 清零实际传输字节数msg->status=-EINPROGRESS;// ④ 标记:传输进行中list_add_tail(&msg->queue,&master->queue);// ⑤ 包裹放货架!if(!master->busy&&need_pump)// ⑥ 快递员闲着?kthread_queue_work(&master->kworker,// ⑦ 拍醒他!&master->pump_messages);spin_unlock_irqrestore(&master->queue_lock,flags);// ⑧ 放锁return0;}

逐行解读

① spin_lock_irqsave:拿自旋锁 + 关本地 CPU 中断。和之前 Q3 讲的一样——中断上下文可能同时调这个函数,必须用 spinlock。

② !master->running:队列被关了(比如控制器正在注销)。拒绝新消息。

③ actual_length = 0:还没传任何字节。

④ status = -EINPROGRESSwait_for_completion醒来后会读message->status。传输完成时spi_finalize_current_message把它改成 0(成功)或负数(失败)。现在是"进行中"。

⑤ list_add_tail:知识点 3——消息挂到master->queue链表尾部。FIFO 队列,先来先发。

⑥ !master->busy && need_pumpbusy为 true 说明当前正在传输另一个消息。这时不需要唤醒快递员——他正在送别的包裹,送完会在__spi_pump_messages里回头检查货架,自动发现你刚放的新消息。

⑦ kthread_queue_work:提交pump_messages任务到kworker,唤醒kworker_task线程。

__spi_pump_messages —— 发动机

段落 A:入队检查(lines 1104-1162)

spin_lock_irqsave(&master->queue_lock,flags);// 检查1:正在处理消息?直接返回if(master->cur_msg){spin_unlock_irqrestore(&master->queue_lock,flags);return;// "上一个包裹还没送完,这次不用管"}// 检查2:正在空闲清理中?延后到 kthread 处理if(master->idling){kthread_queue_work(&master->kworker,&master->pump_messages);return;}// 检查3:队列空了?if(list_empty(&master->queue)||!master->running){if(!master->busy){return;// "货架空着,我也闲着 → 直接走"}// busy=true 但队列空了 → 传输刚完成,做收尾工作master->busy=false;// ... unprepare_transfer_hardware(关掉时钟、释放硬件资源)return;}

关键逻辑busy和队列状态是两件事。busy=true队列空 = 最后一个消息刚传完,需要收尾。


段落 B:从货架上取货(lines 1164-1173)

// 取队列头(最早挂上去的消息)master->cur_msg=list_first_entry(&master->queue,structspi_message,queue);// 知识点1+3:从 list_head 反推出 spi_message// 从队列中移除list_del_init(&master->cur_msg->queue);// 标记:传送带正在使用中if(master->busy)was_busy=true;// 上一个消息还在处理?(不太可能,但兼容)elsemaster->busy=true;// "传送带我占了!"spin_unlock_irqrestore(&master->queue_lock,flags);

段落 C:发送消息(lines 1175-1236)

mutex_lock(&master->io_mutex);// 拿 IO 锁,保护传输过程// ① 第一次传输:准备硬件if(!was_busy&&master->prepare_transfer_hardware)master->prepare_transfer_hardware(master);// 知识点2:函数指针// 类比:快递员出发前检查车胎、打火// ② 控制器驱动的准备回调if(master->prepare_message)master->prepare_message(master,master->cur_msg);// 知识点2:控制器驱动可以在这里做消息级别的准备// (比如配置 DMA 描述符、设置 CS)// ③ DMA 映射ret=spi_map_msg(master,master->cur_msg);// 把 tx_buf/rx_buf 映射成 DMA 地址// 如果有设备不支持 DMA,走 PIO 路径(跳过这个)// ④ 传输!核心中的核心!ret=master->transfer_one_message(master,master->cur_msg);// 知识点2:函数指针调用// 默认实现 spi_transfer_one_message:// → 遍历 msg->transfers 链表// → 对每个 transfer 调用 master->transfer_one// → spi_imx_transfer → DMA/PIO → spi_imx_isr → spi_finalize_current_messageout:mutex_unlock(&master->io_mutex);

关键:传输完成后谁再次调用消息泵

master->transfer_one_message内部(比如spi_imx_transfer)在传输完成后调用spi_finalize_current_message

voidspi_finalize_current_message(structspi_master*master){msg=master->cur_msg;master->cur_msg=NULL;master->busy=false;// ← 传送带空闲了msg->status=0;// 成功msg->actual_length=xxx;// 实际传输字节数msg->complete(msg->context);// ← 知识点2:调用 spi_complete → 唤醒等待者// ← 这里再次调用 __spi_pump_messages!检查货架上有没有新包裹spi_idle_runtime_pm(master);// 内部可能调 __spi_pump_messages}

这就是之前的 Q3 答案——快递员送完一个包裹,回头检查货架,发现有新包裹就继续送。货架空了才下班。


完整时序

线程1(调用者) kworker_task(快递员) ─────────── ──────────── spi_sync(msg) ├── __spi_queued_transfer → 入队 ├── __spi_pump_messages spi_pump_messages │ └── 取 msg → busy=true ← 被唤醒 │ └── transfer_one_message ├── spi_imx_transfer │ └── DMA开始 │ └── 写寄存器 → 等中断 │ │ └── wait_for_completion 睡觉 ← CPU 切走,线程1睡眠 中断来了 → spi_imx_isr └── spi_finalize_current_message ├── msg->complete 唤醒线程1 └── __spi_pump_messages 检查下一条 队列空 → busy=false → 下班睡觉 线程1醒来:message->status = 0 返回 spi_sync → 数据已传输完成

Q1(L1)spi_transferspi_message是什么关系?一个spi_message包含多个spi_transfer时,CS 片选信号在 transfer 之间怎么变化?哪个字段决定了这个行为?

spi_message包含多个spi_transfer;CS 在一个 message 的所有 transfer 之间保持拉低(原子操作)。cs_change:1;决定这个行为

cs_change = 0(默认):CS 在一个 message 的所有 transfer 之间保持拉低cs_change = 1:当前 transfer 传完后,CS短暂拉高再拉低,再传下一个 transfer。

cs_change只在 transfer 结束时检查——在spi_transfer_one_message的第 1045 行:

if(xfer->cs_change){if(list_is_last(&xfer->transfer_list,&msg->transfers))keep_cs=true;// 最后一个 transfer,CS 保持拉低elsespi_set_cs(msg->spi,false);// CS 拉高udelay(10);spi_set_cs(msg->spi,true);// CS 拉低}

Q2(L2)spi_syncwait_for_completion(&done)在睡觉等待。谁在什么时候唤醒它?message->completemessage->context在这场"睡觉和醒来"中各自扮演什么角色?

在传数完数据后,通过spi_finalize_current_message()的mesg->complete(mesg->context);来唤醒它;

“这两个字段是谁设的”。答案在第 ② 步:

// __spi_sync 中设置message->complete=spi_complete;// ← 函数指针:唤醒函数message->context=&done;// ← 传给唤醒函数的参数

complete是"怎么叫醒"(门铃),context是"叫醒哪个"(哪个 completion 对象)。spi_complete被调用时拿到&done,调用complete(&done)——这个done正是spi_syncwait_for_completion(&done)等的那个。

Q3(L3)__spi_pump_messages发送完当前消息后,什么时候会去取下一个消息?假如队列里有 3 个消息在排队,画出发送这 3 个消息的完整时序图(标注每次上下文切换、每次__spi_pump_messages调用是谁触发的)。

时间 → msg1 到达(kworker) msg2 到达 msg3 到达 │ │ │ ├─ __spi_pump_messages │ │ │ └─ transfer_one(msg1) │ │ │ └─ DMA开始... │ │ │ │ │ │ DMA完成 → ISR │ │ │ └─ spi_finalize(msg1) │ │ │ ├─ ⑤ 提交 pump → __spi_pump_messages ← 第1次自动触发 │ │ ├─ transfer_one(msg2) │ │ │ └─ 发 msg2... │ │ │ │ └─ ⑥ complete(&done1) → 唤醒线程A │ 线程A醒来:msg1 done │ │ │ │ 线程B: spi_sync(msg3) │ │ ├─ 入队 msg3 │ │ ├─ __spi_pump_messages │ │ │ cur_msg != NULL! → return(不做事) │ │ └─ wait_for_completion(&done3) 睡觉 │ │ │ │ DMA完成 → ISR │ │ └─ spi_finalize(msg2) │ │ ├─ ⑤ 提交 pump → __spi_pump_messages ← 第2次自动触发 │ │ │ ├─ 取 msg3! │ │ │ └─ transfer_one(msg3) │ │ └─ ⑥ complete(&done2) → 唤醒线程B? NO │ │ done2 是谁?msg2 的等待者 │ │ │ DMA完成 → ISR │ └─ spi_finalize(msg3) │ ├─ ⑤ 提交 pump → 队列空,busy=false,下班 │ └─ ⑥ complete(&done3) → 唤醒线程B 线程B醒来:msg3 done

关键点

消息谁触发的__spi_pump_messages
msg1spi_sync调用者线程手动触发
msg2spi_finalize_current_message(msg1)里的kthread_queue_work
msg3spi_finalize_current_message(msg2)里的kthread_queue_work

从 msg2 开始,每处理完一个消息,自动触发下一个。不需要调用者再手动触发。调用者在第二步__spi_pump_messages发现cur_msg != NULL(快递员正忙),直接去睡觉。醒来时消息已经被自动泵处理完了。