第五阶段: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);// 栈上分配一个 completioncompletion 是一个"信号量"——用来让一个线程等待另一个线程完成某事。
类比:你快递寄出去了,回家等。快递员送到后按门铃。你听到门铃去开门。——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 = -EINPROGRESS:wait_for_completion醒来后会读message->status。传输完成时spi_finalize_current_message把它改成 0(成功)或负数(失败)。现在是"进行中"。
⑤ list_add_tail:知识点 3——消息挂到master->queue链表尾部。FIFO 队列,先来先发。
⑥ !master->busy && need_pump:busy为 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_transfer和spi_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_sync中wait_for_completion(&done)在睡觉等待。谁在什么时候唤醒它?message->complete和message->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_sync在wait_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 |
|---|---|
| msg1 | spi_sync调用者线程手动触发 |
| msg2 | spi_finalize_current_message(msg1)里的kthread_queue_work |
| msg3 | spi_finalize_current_message(msg2)里的kthread_queue_work |
从 msg2 开始,每处理完一个消息,自动触发下一个。不需要调用者再手动触发。调用者在第二步__spi_pump_messages发现cur_msg != NULL(快递员正忙),直接去睡觉。醒来时消息已经被自动泵处理完了。