@[toc]

SPI通信模式及其对DMA控制器配置策略的影响

在这里插入图片描述

1. 引言

串行外设接口(SPI)作为一种同步、全双工的串行通信协议,在嵌入式系统中被广泛应用于微控制器与各类外设(如传感器、存储器、显示控制器)之间的数据交换。尽管其物理层定义是全双工的,但在实际应用中,根据数据流向,可分为全双工、半双工及三线单工等多种工作模式。不同的工作模式对底层驱动,特别是DMA(直接内存访问)控制器的配置策略,提出了截然不同的要求。本文旨在深度剖析SPI的各种通信模式,并重点分析为何“阻塞式发送(Polling TX)与DMA接收(DMA RX)”的组合在全双工模式下是不可行的,以及这种配置如何破坏协议的同步性。

2. SPI协议核心:同步与全双工的物理基础

要理解不同模式的派生,必须首先掌握SPI协议的根本机制。

2.1 物理连接

标准的四线SPI总线由以下信号线构成:

  • SCLK (Serial Clock): 主设备产生的时钟信号,同步总线上的所有数据传输。
  • MOSI (Master Out, Slave In): 主设备数据输出,从设备数据输入。
  • MISO (Master In, Slave Out): 主设备数据输入,从设备数据输出。
  • CS/SS (Chip Select / Slave Select): 片选信号,由主设备控制,用于选择与之通信的从设备。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
graph TD
subgraph "Master Device"
M_SCLK[SCLK]
M_MOSI[MOSI]
M_MISO[MISO]
M_CS[CS]
end
subgraph "Slave Device"
S_SCLK[SCLK]
S_MISO[MISO]
S_MOSI[MOSI]
S_CS[CS]
end
M_SCLK --> S_SCLK;
M_MOSI --> S_MOSI;
S_MISO --> M_MISO;
M_CS --> S_CS;
2.2 核心机制:同步数据交换

SPI传输的核心是一个环形的移位寄存器模型。在主设备每产生一个SCLK时钟周期时,会发生一次双向的数据位交换:

  1. 主设备将其发送移位寄存器的最高位(MSB)推到MOSI线上。
  2. 从设备同时将其发送移位寄存器的最高位推到MISO线上。
  3. 在同一个时钟周期内,主设备捕获MISO线上的数据位并移入其接收移位寄存器的最低位(LSB)。
  4. 从设备捕获MOSI线上的数据位并移入其接收移位寄存器的最低位。
    这个过程持续N个时钟周期(N为数据帧长度,通常是8或16),完成一次完整的数据交换。关键在于,发送和接收是同步发生的,发送一个比特必然会接收一个比特。 这就是SPI协议物理层上的“全双工”本质。
1
2
3
4
5
6
7
8
9
10
graph TD
A[SCLK时钟周期 N 启动] --> B{数据交换};
B -- 主设备 --> C[发送1 bit数据至MOSI];
B -- 从设备 --> D[发送1 bit数据至MISO];
C --> E[接收来自MISO的1 bit数据];
D --> F[接收来自MOSI的1 bit数据];
E & F --> G{完成1 bit交换};
G --> H{达到N个时钟周期?};
H -- 否 --> A;
H -- 是 --> I[传输结束];

3. SPI通信模式的分类

基于物理层的全双工特性,上层应用可以演化出多种逻辑上的通信模式。

3.1 全双工模式 (Full-Duplex)

在这种模式下,MOSI和MISO两条数据线上的数据在同一时间点都是有效的。主设备在向从设备发送数据的同时,也在接收从设备发回的数据。

  • 应用场景: 与ADC/DAC模块、其他微控制器或需要同步命令与状态交换的复杂外设通信。
  • 数据流: send_bufrecv_buf 均包含有效数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
graph TD
subgraph "时间轴"
direction LR
T1[开始] --> T2[结束];
end
subgraph "Master -> Slave (MOSI)"
direction LR
D_OUT[发送有效数据]
end
subgraph "Slave -> Master (MISO)"
direction LR
D_IN[接收有效数据]
end
T1 -- 同步发生 --> D_OUT;
T1 -- 同步发生 --> D_IN;
3.2 半双工模式 (Half-Duplex)

虽然物理上数据交换仍在发生,但逻辑上只有一个方向的数据是有效的。

  • 写操作 (Transmit-Only): 主设备通过MOSI发送数据,但忽略MISO上接收到的任何数据。此时MISO线上的数据通常是无效的或不被关心。
  • 读操作 (Receive-Only): 主设备需要从从设备读取数据。为产生必要的SCLK时钟,主设备必须通过MOSI发送数据,但这些数据是无意义的“哑元数据”(Dummy Data)。主设备只关心MISO线上接收到的有效数据。
  • 应用场景: 读写EEPROM/Flash存储器,配置简单的传感器等。
1
2
3
4
graph TD
A[半双工模式] --> B{操作类型?};
B -- 写操作 --> C[MOSI: 有效数据<br>MISO: 忽略];
B -- 读操作 --> D[MOSI: 哑元数据<br>MISO: 有效数据];
3.3 三线单工模式 (3-Wire Simplex)

这是半双工模式的一种硬件简化。在纯粹的单向通信场景中(例如,主设备只向显示驱动器发送数据),MISO线可以被完全省略,从而节省一个GPIO引脚。

  • 应用场景: 驱动数码管、LED灯带、一些简单的显示屏。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
graph TD
subgraph "Master Device"
M_SCLK[SCLK]
M_MOSI[MOSI]
M_CS[CS]
end
subgraph "Slave Device"
S_SCLK[SCLK]
S_MOSI[MOSI]
S_CS[CS]
end
M_SCLK --> S_SCLK;
M_MOSI --> S_MOSI;
M_CS --> S_CS;

4. DMA配置策略与全双工模式的内在冲突

现在,我们来分析为何“阻塞式发送(Polling TX)+ DMA接收(DMA RX)”的组合在全双工模式下存在根本性的逻辑缺陷。

4.1 问题的提出

在一个设想的SPI驱动函数中,为了处理全双工传输,可能会尝试如下的混合编程模型:

  1. 发送: 使用阻塞式(Polling)或中断方式发送数据。CPU循环等待发送缓冲区(TX FIFO)为空,然后写入下一个字节,直到所有数据发送完毕。
  2. 接收: 同时,为接收路径配置一个DMA通道。期望DMA在SPI硬件接收到数据后,自动将数据从接收缓冲区(RX FIFO)搬运到内存。
1
2
3
4
5
6
graph TD
A[启动全双工传输] --> B[CPU执行阻塞式发送循环];
A --> C[配置并启动RX DMA通道];
B --> D{发送完成?};
D -- 是 --> E[结束];
C --> F[DMA等待RXNE事件];
4.2 冲突的根本原因:破坏同步性

这种设计的失败源于它错误地将SPI的同步交换过程分解为了两个独立的、异步的任务,而SPI硬件本身并不支持这种解耦。

  1. 时钟的来源: SPI的SCLK时钟是由主设备发送数据这个行为产生的。当CPU通过阻塞方式向TX FIFO写入数据时,SPI硬件才开始移位操作并产生时钟。
  2. 接收的依赖性: 数据的接收完全依赖于SCLK时钟。没有时钟,就没有数据交换,RX FIFO就永远不会接收到新数据。
  3. 逻辑死锁:
    • RX DMA通道被启动后,它被动地等待SPI硬件发出“接收到新数据”(RXNE)的请求信号。
    • 然而,RXNE信号只有在SCLK时钟驱动下,数据从MISO线被移入接收寄存器后才会产生。
    • SCLK时钟又依赖于CPU执行的发送操作
    • 如果CPU执行一个完整的阻塞式发送函数(例如HAL_SPI_Transmit()),它会一次性发送完所有数据。在这个过程中,确实产生了所有时钟,数据也确实被接收到了RX FIFO。但是,当这个发送函数返回时,DMA可能还没来得及搬运所有数据,或者更糟的是,如果接收数据量大,RX FIFO可能已经发生了溢出(Overrun)。
    • 更根本的问题是,一个设计良好的驱动不会将这两个操作分开调用。它会调用一个统一的全双工传输函数,例如HAL_SPI_TransmitReceive_DMA()。这个函数会同时配置TX DMA和RX DMA,确保发送和接收的启动和数据流是同步管理的。
4.3 一个更清晰的失败场景

设想一个驱动试图实现 transmit_polling_receive_dma(tx_buf, rx_buf, len):

  1. 配置RX DMA: HAL_SPI_Receive_DMA(&hspi, rx_buf, len); 这条语句配置了DMA,并使能了SPI的接收中断/DMA请求。SPI硬件现在开始等待数据。
  2. 执行阻塞TX: HAL_SPI_Transmit(&hspi, tx_buf, len, timeout); CPU进入此函数,开始循环写入数据到TX FIFO。
  3. 竞争与冲突:
    • CPU写入第一个字节到TX FIFO,SPI硬件开始产生时钟。
    • 第一个字节的数据交换发生,一个字节被接收到RX FIFO,触发RX DMA请求,DMA开始搬运数据。
    • CPU可能以极高速度继续填充TX FIFO,而DMA搬运需要时间。如果CPU发送速度快于DMA处理接收数据的速度,RX FIFO就会发生溢出,导致数据丢失。
    • 这种分离的调用方式完全破坏了HAL库设计的原子性操作,使得传输过程中的状态管理和错误处理变得不可靠。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
sequenceDiagram
participant CPU
participant SPI_Hardware
participant DMA
CPU->>DMA: 配置并启动RX DMA
DMA->>SPI_Hardware: 使能RXNE请求
CPU->>SPI_Hardware: 开始阻塞式发送第一个字节 (HAL_SPI_Transmit)
SPI_Hardware-->>SPI_Hardware: 产生时钟, 交换数据
SPI_Hardware->>DMA: 触发RXNE, 请求搬运数据
CPU->>SPI_Hardware: 立即发送第二个字节 (CPU速度 >> DMA)
DMA->>CPU: (正在处理第一个字节)
SPI_Hardware-->>SPI_Hardware: 产生时钟, 交换数据
SPI_Hardware->>DMA: 触发RXNE, 但DMA可能仍在处理上一个请求
Note over SPI_Hardware, DMA: RX FIFO发生溢出 (Overrun)

5. 结论

SPI协议的物理层是内在全双工且同步的,其数据发送与接收操作在时钟的驱动下紧密耦合。虽然上层应用可以根据逻辑需求实现全双工、半双工等多种模式,但底层驱动的实现必须严格尊重其同步性。

将全双工传输分解为“阻塞式发送”和“DMA接收”两个独立任务是不可行的,因为它:

  1. 破坏了同步性: 将由发送行为驱动的接收过程错误地解耦。
  2. 引入了竞态条件: CPU的发送速度与DMA的接收处理速度之间存在竞争,极易导致接收FIFO溢出和数据丢失。
  3. 违背了驱动设计的原子性: 可靠的SPI驱动必须通过统一的接口(如HAL_SPI_TransmitReceive_DMA())来原子地管理全双工传输,确保TX和RX的DMA通道被同步配置和启动,从而维持协议的完整性和数据的一致性。

因此,在设计SPI驱动时,必须根据通信模式选择正确的、匹配的传输策略,避免引入此类根本性的逻辑矛盾。