面向读者:1-3 年嵌入式软件工程师,已理解 Port 和 DIO 的基本概念,希望彻底掌握 MCAL 中最复杂的通信驱动之一 — SPI。

前置阅读:建议先阅读 MCAL Port 配置教程,理解引脚功能配置(SPI 的 SCK/MOSI/MISO/CS 需要先通过 Port 配成 ALT 模式)。

目标平台:Infineon AURIX TC3xx (TriCore 架构),SPI 方法论适用于所有 AUTOSAR MCAL 平台。

发布平台:CSDN


目录

  1. SPI 基础:你以为你懂了,但你真的懂了吗?
  2. QSPI 硬件:不只是"一个 SPI 模块"
  3. BACON 序列器:芯片如何替你"自动驾驶"
  4. 三层架构:Channel → Job → Sequence
  5. 配置结构全景图
  6. 同步传输:Level 0 的轮询世界
  7. 异步传输:Level 1/2 的 DMA 世界
  8. 片选信号:硬件自动化 vs 手动 GPIO
  9. 时钟与波特率:从浮点数到寄存器值的旅程
  10. 时序延迟:Leading / Trailing / Idle 的精妙控制
  11. 外部解复用器:一个 CS 控制多个设备
  12. 安全机制与 Init Check
  13. 常见陷阱与避坑指南
  14. 端到端配置实战清单

1. SPI 基础:你以为你懂了,但你真的懂了吗?

1.1 经典四线 SPI 的真相

每个嵌入式工程师都学过 SPI:SCK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。四根线,主设备控制时钟,数据在每个时钟边沿移入移出。

但这只是课本上的简化模型。实际工程中,你会遇到这些问题:

  • 你的 Flash 芯片要求 CS 拉低后等 50ns 才能发第一个时钟 — 谁来保证这 50ns?
  • 你需要在一次 CS 有效期内连续读 256 字节 — 谁来管理 CS 的自动保持和释放?
  • 两个 SPI 设备挂在同一个 QSPI 模块上,一个需要 MSB 优先,另一个需要 LSB 优先 — 怎么无缝切换?
  • 你的系统有 6 个核心,Core2 要发 SPI 的时候 Core0 也在发 — 谁来仲裁?

MCAL SPI 驱动就是为解决这些实际问题而生的。

1.2 SPI 在 AUTOSAR 分层中的位置

┌────────────────────────────────────┐
│      Application (SWC)             │  ← "读 Flash JEDEC ID (命令 0x9F)"
├────────────────────────────────────┤
│      RTE / IoHwAb                  │  ← "Spi_SyncTransmit(Seq_FLASH_READ_ID)"
├────────────────────────────────────┤
│   ★ SPI 驱动 (本教程) ★            │  ← "BACON: CS=0, DL=8, LAST=0 → Tx 0x9F → 
│                                    │      BACON: CS=0, DL=24, LAST=1 → Rx 3 bytes"
├────────────────────────────────────┤
│   Port 驱动                         │  ← "SCK=ALT1, MOSI=ALT1, MISO=ALT1, CS=ALT1"
├────────────────────────────────────┤
│   硬件 QSPI + DMA                   │  ← BACON 序列器自动生成时序
└────────────────────────────────────┘

1.3 SPI vs QSPI:名字的陷阱

你可能会困惑:文档里一会儿叫 SPI,一会儿叫 QSPI,到底什么区别?

  • SPI (Serial Peripheral Interface):通信协议本身,Motorola 发明
  • QSPI (Queued SPI):AURIX TC3xx 上实现 SPI 的硬件模块。"Queued"意味着它有一个硬件队列,可以自动连续执行多个 SPI 传输,而不需要 CPU 逐个字节干预。

💡 类比:SPI 是"开车"这个行为,QSPI 是 AURIX 定制的"自动驾驶系统"。MCAL SPI 驱动则是"自动驾驶的使用说明书"。


2. QSPI 硬件:不只是"一个 SPI 模块"

2.1 TC3xx 上的 QSPI 拓扑

TC39xB 芯片
│
├── QSPI0  (地址 0xF0001C00) → 最多 16 个片选 (SLSO0-15)
├── QSPI1  (地址 0xF0001D00) → 最多 16 个片选
├── QSPI2  (地址 0xF0001E00) → 最多 16 个片选 + 捕获定时器
├── QSPI3  (地址 0xF0001F00) → 最多 16 个片选 + 捕获定时器
├── QSPI4  (地址 0xF0002000) → 最多 16 个片选
└── QSPI5  (地址 0xF0002100) → 最多 16 个片选

每个 QSPI 模块 = 独立的 SPI 主机控制器

这意味着 TC3xx 可以同时驱动 6 路 SPI 总线,每路可以挂最多 16 个从设备(通过 16 个片选线),最多共 96 个 SPI 设备 — 但要共享 IO 引脚。

2.2 QSPI 内部寄存器一览

QSPIx 寄存器块 (256 字节):
┌────────────┬──────────────────────────────────────┐
│ GLOBALCON  │ 全局控制:TQ 时基、回环、使能、主/从  │
│ GLOBALCON1 │ 中断使能:TX/RX/Error/PT1/PT2          │
├────────────┼──────────────────────────────────────┤
│ BACON      │ 当前正在执行的 BACON (只读)            │
│ BACONENTRY │ ★ BACON 入口 (只写) — 整个驱动的核心   │
│ ECON[0-7]  │ 8 个扩展配置 (波特率、时钟极性/相位)    │
├────────────┼──────────────────────────────────────┤
│ DATAENTRY  │ TxFIFO 入口 (8 个寄存器,可连续写入)    │
│ RXEXIT     │ RxFIFO 出口                           │
├────────────┼──────────────────────────────────────┤
│ STATUS     │ 状态:错误标志、FIFO 电平、PHASE       │
│ FLAGSCLEAR │ 清除中断/错误标志                      │
├────────────┼──────────────────────────────────────┤
│ SSOC       │ 片选输出控制:有效电平 + 输出使能        │
│ MC + MCCON │ 移动计数器:跟踪剩余传输次数             │
│ XXLCON     │ 超大数据配置                            │
│ MIXENTRY   │ 混合入口 (TxFIFO + BACON 合并写入)      │
├────────────┼──────────────────────────────────────┤
│ CAPCON     │ 捕获控制 (仅 QSPI2/3)                   │
└────────────┴──────────────────────────────────────┘

3. BACON 序列器:芯片如何替你"自动驾驶"

3.1 BACON 是什么?

BACON = BAsic CONfiguration Register。它是一个 32-bit 寄存器,单次写入就能描述一次完整的 SPI 传输所需的一切:

BACON 寄存器 (32 bits)
┌──────┬───┬──────┬───┬──────┬───┬──────┬───┬──┬──┬──┬──┬────────┬──────┐
│  CS  │DL │ MSB  │BYT│ LEAD │LPR│TRAIL │TPR│PA│UI│ID│LA│  IDLE  │ IPRE │
│ 4bit │5bit│ 1bit│1bit│ 3bit│3bit│ 3bit│3bit│1bit│1bit│  │  │  3bit  │ 3bit │
│      │    │      │   │      │   │      │   │   │   │  │  │        │      │
│31..28│27..│ 21   │ 22 │18..16│15..│12..10│9..7│19 │20 │  │0 │  6..4  │ 3..1 │
│      │ 23 │      │   │      │ 13 │      │   │   │   │  │  │        │      │
└──────┴───┴──────┴───┴──────┴───┴──────┴───┴──┴──┴──┴──┴────────┴──────┘

每个字段的含义

字段 名称 作用
CS[3:0] Chip Select 选择 0-15 号片选线中的哪个
DL[4:0] Data Length 数据宽度-1 (DL=7 → 8bit, DL=31 → 32bit)
MSB MSB First 1=高位先出, 0=低位先出
BYTE Byte Mode 字节模式
LEAD[2:0] Leading Delay CS 有效后等多久发时钟
LPRE[2:0] Leading Prescaler 前导延迟的预分频
TRAIL[2:0] Trailing Delay 最后时钟后等多久释放 CS
TPRE[2:0] Trailing Prescaler 尾随延迟的预分频
IDLE[2:0] Idle Delay 连续两帧之间的空闲时间
IPRE[2:0] Idle Prescaler 空闲延迟的预分频
LAST Last Word 1=此帧后自动释放 CS
PARTYP Parity 奇偶校验类型
UI User Interrupt 下一帧 PT1 事件触发用户中断

3.2 BACON 的"自动驾驶"流程

当你写一个 BACON 到 BACONENTRY 寄存器,然后写数据到 DATAENTRY 时,QSPI 硬件自动完成以下全部操作:

时间轴 (每个阶段都由硬件自动完成):
│
├─ [1] 根据 BACON.CS 选择片选线 SLSOx
│      └─ 根据 SSOC 寄存器决定有效电平 (高有效/低有效)
│
├─ [2] 等待 Leading Delay = (LPRE+1) × (LEAD+1) × tQ
│      └─ ★ 这满足了你 Flash 芯片的 tCSS 时序要求
│
├─ [3] 产生 DL+1 个时钟脉冲
│      └─ 每个时钟边沿移出/移入 1 bit 数据
│      └─ 时钟极性/相位由 ECON 寄存器控制
│
├─ [4] 等待 Trailing Delay = (TPRE+1) × (TRAIL+1) × tQ
│      └─ ★ 这满足了你 Flash 芯片的 tCSH 时序要求
│
├─ [5] 如果 BACON.LAST = 1 → 释放 CS
│      如果 BACON.LAST = 0 → 保持 CS,等待下一个 BACON
│
└─ [6] 如果还有下一帧 → 等待 Idle Delay = (IPRE+1) × (IDLE+1) × tQ → 回到 [2]

🎯 这就是 MCAL SPI 驱动最大的价值:你不需要用软件 DIO 操作 CS、不需要用定时器做延迟、不需要手动操作移位寄存器。你只需要配置好 BACON 的参数,硬件就会替你做原始 SPI 做的所有事情 — 而且精确到纳秒级。

3.3 为什么这很重要?

假设你要读一个 SPI Flash 的 JEDEC ID:

传统 GPIO 模拟 (痛苦版):
  Dio_WriteChannel(CS, LOW);     // 手动拉低 CS
  delay_ns(50);                   // 手动等 tCSS
  for (i=0; i<8; i++) {          // 手动移位输出 0x9F 的每个 bit
    Dio_WriteChannel(MOSI, ...);
    Dio_WriteChannel(SCK, HIGH);
    delay_ns(...);
    Dio_WriteChannel(SCK, LOW);
  }
  for (i=0; i<24; i++) {         // 手动读入 24 bit
    Dio_WriteChannel(SCK, HIGH);
    data |= Dio_ReadChannel(MISO) << i;
    Dio_WriteChannel(SCK, LOW);
  }
  delay_ns(50);                   // 手动等 tCSH
  Dio_WriteChannel(CS, HIGH);    // 手动拉高 CS

MCAL SPI + BACON (自动驾驶版):
  // BACON1: CS=0, DL=7(8bit), LAST=0 → 发 0x9F
  // BACON2: CS=0, DL=23(24bit), LAST=1 → 收 3 字节 → 自动释放 CS
  Spi_SyncTransmit(Seq_FLASH_READ_ID);
  // DONE. 一行代码。时序精确到 ns。

4. 三层架构:Channel → Job → Sequence

这是 MCAL SPI 最核心也是最难理解的设计。用日常生活的比喻来引入:

4.1 一个快递公司的比喻

想象你经营一家快递公司,要从仓库发货给客户:

SPI 概念 快递比喻 硬件操作
Channel 一个包裹(装着要发的货 + 一个空箱子收回执) 发送缓冲区 + 接收缓冲区
Job 一张运单(同一个收货人、同一个快递员、包裹连续发出) 同一个片选下的一组 Channel 传输
Sequence 一条配送路线(可能包含多个收货人、多个快递员) 一组可能跨 QSPI 模块的 Job 串联

一次 Spi_SyncTransmit(Sequence) 相当于执行一整条配送路线。

4.2 具体例子:读 SPI Flash 数据

假设你要从 SPI Flash 的地址 0x1000 读取 256 字节:

Sequence: "FLASH_READ_256"
│
├── Job 1: "发送读命令和地址"
│   │  片选: CS0 (Flash 的片选线)
│   │  波特率: 10 MHz
│   │  时钟模式: CPOL=0, CPHA=0 (模式 0)
│   │
│   ├── Channel A: 发送 0x03 (读命令)        [8-bit, MSB first]
│   ├── Channel B: 发送 0x00 0x10 0x00 (地址) [24-bit, MSB first]
│   └── Channel C: 接收 256 字节               [2048-bit, MSB first]
│
└── (Job 1 中所有 Channel 共享同一个 CS 有效周期)

关键理解

  • 同一 Job 内的所有 Channel 在同一个 CS 有效期内完成 — CS 拉低后直到 Job 结束才释放
  • Channel A、B、C 的 BACON 中只有最后一个的 LAST=1
  • 不同 Job 可以有不同的波特率、时钟模式、片选线

4.3 再一个例子:读两个不同设备

Sequence: "DUAL_DEVICE_READ"
│
├── Job 1: "读 Flash JEDEC ID"
│   │  QSPI0, CS0, 10MHz, 模式 0
│   ├── Channel A: 发送 0x9F
│   └── Channel B: 接收 3 字节
│
└── Job 2: "读温度传感器"
    │  QSPI0, CS1, 1MHz, 模式 3  (不同设备!)
    ├── Channel A: 发送 0x00 (温度寄存器地址)
    └── Channel B: 接收 2 字节

💡 Job 1 和 Job 2 使用了同一个 QSPI0 模块但不同的 CS,而且波特率和时钟模式完全不同。硬件在每个 Job 开始时自动重配寄存器。

4.4 一个经典的混淆点

初学者常问:“为什么需要三层?Channel 直接组成 Sequence 不行吗?”

:因为 CS 的有效周期。Channel 是最小传输单元(一次移位),但芯片与芯片之间的交互是以 CS 有效周期 为单位的。Job 封装了"一个 CS 周期"的概念,所以在架构上是必要的:

没有 Job 层的后果:
  Sequence: [Ch A, Ch B, Ch C, Ch D]
  问题:CS 在什么时候释放?Ch B 和 Ch C 之间?还是全部结束?
  无法表达 "A+B 共用 CS, C+D 共用另一个 CS"

有了 Job 层:
  Sequence: [Job1(A,B), Job2(C,D)]
  清晰:Job1 内 CS0 保持, Job1 结束释放 CS0
        Job2 内 CS1 保持, Job2 结束释放 CS1

5. 配置结构全景图

5.1 总览

MCAL SPI 的配置结构体(Spi_ConfigType)是编译生成的常量表。它支持 PB(Post-Build)变体,意味着配置可以在 HEX 生成后独立替换。

Spi_ConfigType
│
├── CoreConfigPtr[6]  ← 每个核独立的配置集
│   └── Spi_CoreConfigType
│       ├── SequenceConfigPtr[]   → Spi_SequenceConfigType 数组
│       ├── JobConfigPtr[]        → Spi_JobConfigType 数组
│       ├── ChannelConfigPtr[]    → Spi_ChannelConfigType 数组
│       ├── ChannelOffsetInfo[]   → 缓冲区偏移查找表
│       └── QSPIHwConfigPtr[]     → Spi_QspiHwConfigType 指针数组
│
├── SequenceLookup[] / JobLookup[] / ChannelLookup[]
│   → 逻辑 ID → 物理索引 的映射表
│
└── SyncTimeout  → 同步传输超时值

5.2 Spi_ChannelConfigType — 通道配置

每个 SPI 传输的最小单元配置:

typedef struct {
  uint32 Defaultdata;          // 当发送缓冲区为 NULL 时发送的默认值
  uint16 NoOfDataElements;     // IB: 内部缓冲元素数; EB: 最大元素数
  uint8  ChannelType;          // SPI_EB_CHANNEL (外部缓冲) 或 SPI_IB_CHANNEL (内部缓冲)
  uint8  DataConfig;           // bit[7]=MSB/LSB, bits[6:0]=数据宽度(2-32)
  Spi_ChannelType ChannelId;   // 唯一通道 ID
} Spi_ChannelConfigType;

IB vs EB

  • IB(内部缓冲):数据存在驱动内部管理的缓冲区中,写入通过 Spi_WriteIB()。适合不变的常量数据(如命令码)。
  • EB(外部缓冲):数据由应用提供,通过 Spi_SetupEB() 指定地址。适合每次变化的动态数据。

5.3 Spi_JobConfigType — 作业配置

typedef struct {
  Spi_JobType    JobId;
  Spi_JobEndNotification JobNotification;  // 异步回调 (Level 1/2)
  uint32         BaudRateAndClockParam;    // 波特率 + 时钟配置 (编码后)
  uint32         IdleLeadTrailDelay;       // 时序延迟 (编码后)
  const Spi_ChannelType *ChnlLinkPtrPhysical; // 通道链表, 0xFF 终止
  uint16         CSPortPin;                // CS 引脚 (或 0xFFFF = HW 控制)
  uint8          JobPriority;              // 0-3 (异步模式优先级)
  uint8          CsPolarity;               // CS 极性
  Spi_HWUnitType HwUnit;                   // QSPI 模块 + SLSO 通道
  uint8          ParitySupport;            // 校验支持
  uint8          FramebasedCs;             // 逐帧 CS 切换
} Spi_JobConfigType;

5.4 Spi_SequenceConfigType — 序列配置

typedef struct {
  Spi_SequenceType       SequenceId;
  Spi_SeqEndNotification SeqNotification;   // 序列完成回调
  const Spi_JobType     *JobLinkPtrPhysical; // Job 链表, 0xFFFF 终止
  const uint8           *SeqSharePtr;        // 异步共享序列列表
  uint16                 NoOfJobInSeq;       // 序列中的 Job 数量
  uint8                  SeqInterruptible;   // 是否可被中断
  uint8                  HwModuleUsed;       // 关联的 QSPI 模块
} Spi_SequenceConfigType;

5.5 Spi_QspiHwConfigType — QSPI 硬件配置

typedef struct {
  uint32 ActiveChipSelectLevel;  // SSOC: 每个 SLSO 的有效电平 + 使能
  uint16 JobQueueLength;         // 异步任务队列深度
  uint8  DMARxChannel;           // DMA 接收通道号
  uint8  DMATxChannel;           // DMA 发送通道号
  uint8  DMATCSCount;            // 每个 Job 的最大 Channel 数 (DMA TCS 分配)
  uint8  ClockSetting;           // CLC: 时钟休眠使能
  uint8  MasterReceivePortPin;   // PISEL: MRIS 引脚选择
  Spi_SequenceType MaxSequence;  // 最大序列号
  uint8  ExternalDemuxEnabled;   // 外部解复用器模式
  uint32 StrobeDelay;            // 解复用器选通延迟
} Spi_QspiHwConfigType;

6. 同步传输:Level 0 的轮询世界

6.1 Level 0 是什么?

Level 0 是 SPI 驱动的最基本模式:同步、轮询、单任务

  • 调用 Spi_SyncTransmit(Sequence) → CPU 等待 → 传输完成 → 返回
  • 没有中断、没有 DMA、没有回调、没有队列
  • 适合简单场景:初始化时读一次芯片 ID、配置寄存器

6.2 同步传输的执行流程

Spi_SyncTransmit(Seq_FLASH_READ)
│
├── 遍历 Sequence 中的每个 Job
│   │
│   ├── Spi_lHwSetJobConfig(Job)
│   │   │  写 GLOBALCON (TQ、回环、解复用器)
│   │   │  写 GLOBALCON1 (中断使能、FIFO模式)
│   │   │  写 ECON (时钟极性/相位、波特率)
│   │   │  写 SSOC (片选有效电平和输出使能)
│   │   │  清除状态标志
│   │   │
│   │   ├── 遍历 Job 中的每个 Channel
│   │   │   ├── 构建 BACON (CS、DL、MSB、LEAD、TRAIL、LAST)
│   │   │   ├── 写 BACONENTRY
│   │   │   ├── 写 DATAENTRY (发送数据)
│   │   │   └── 轮询 STATUS.PHASE (等待硬件完成)
│   │   │       └── 检查 STATUS.ERRORFLAGS → 有错误则返回失败
│   │   │
│   │   └── Job 完成 → CS 释放 (如果 FramebasedCs=1,每个 Channel 都释放)
│   │
│   └── 下一个 Job...
│
└── 返回 E_OK (所有 Job 全部成功)

⚠️ 同步传输期间 CPU 被完全占用。如果传输 256 字节 @ 1MHz = 大约 2ms,在这 2ms 内 CPU 什么都不能做。对于高速大批量传输,要用异步模式。

6.3 SyncTransmit 的超时保护

同步传输有一个 SyncTimeout 超时值。如果硬件卡住(比如从设备没响应),驱动不会永远等待。超时后返回错误。


7. 异步传输:Level 1/2 的 DMA 世界

7.1 三种 Level 的对比

Level 0 ──────────────────────────── Level 1 ────────────── Level 2
同步轮询                              纯异步                  混合双模
│                                      │                       │
├── Spi_SyncTransmit  ✓               ├── Spi_SyncTransmit  ✗  ├── Spi_SyncTransmit  ✓
├── Spi_AsyncTransmit  ✗               ├── Spi_AsyncTransmit ✓  ├── Spi_AsyncTransmit ✓
├── 中断                 ✗               ├── 中断                ✓  ├── 中断                ✓
├── DMA                  ✗               ├── DMA                 ✓  ├── DMA                 ✓
├── Job Queue            ✗               ├── Job Queue           ✓  ├── Job Queue           ✓
└── Spi_SetAsyncMode     ✗               └── Spi_SetAsyncMode    ✗  └── Spi_SetAsyncMode    ✓
                                                               (可切换轮询/中断模式)

7.2 异步传输的执行流程

Spi_AsyncTransmit(Seq_FLASH_READ)
│
├── 参数验证 (与同步相同)
├── 检查 Job Queue 是否有空位
├── 检查 Sequence 是否已经在 PENDING
├── 检查共享 Job 冲突
│
├── 将所有 Job 设为 SPI_JOB_QUEUED
├── 按优先级插入队列 (Spi_lInsertSeqInQueue)
│
├── QSPI 空闲?→ 立即启动第一个 Job:
│   └── Spi_lAsyncStartJob(JobIndex)
│       ├── 设置 Job 状态 = SPI_JOB_PENDING
│       ├── 配置 QSPI 硬件
│       ├── ★ Spi_lSetDmaTcs() → 构建 DMA 链表 ★
│       ├── 写第一个 BACON 到 BACONENTRY
│       └── Dma_ChEnableHardwareTrigger(TX+RX)
│           └── ★ 现在 DMA 开始自动驾驶 ★
│
└── 返回 E_OK (立即返回,不等待!)
    │
    │ ... 时间流逝,DMA + QSPI 默默工作 ...
    │
    ├── DMA RX 完成中断 → Spi_QspiDmaCallout()
    │   └── 加载下一个 Channel 的 BACON
    │   └── 重新触发 DMA
    │
    ├── PT2 中断 (Phase Transition 2, 帧结束)
    │   └── Job 完成 → 检查队列中下一个 Job
    │   └── Spi_lQspiTriggerNextJob() → 启动下一个 Job
    │
    └── 全部 Job 完成 → 用户回调 Spi_SeqEndNotification()

7.3 DMA 链表机制

这是异步模式最精妙的设计。Spi_lSetDmaTcs() 为每个 Channel 创建两个 DMA TCS(Transaction Control Set)

  • RX TCS:目标地址 → Channel 的接收缓冲区
  • TX TCS:源地址 → Channel 的发送缓冲区

这些 TCS 通过硬件 Linked List 串联:

Channel A RX → Channel A TX → Channel B RX → Channel B TX → Channel C RX → Channel C TX
      ↑                                                           │
      └────────────────── SHADR 链表 ────────────────────────────┘

DMA 引擎自动遍历这个链表,无需 CPU 干预。
每个节点触发时,对应的 BACON (从 BaconChannelArray[] 中预取) 被加载到 BACONENTRY。

🎯 这就是"自动驾驶"的终极形态:应用调用 Spi_AsyncTransmit() 后立即返回。DMA 引擎按照预先构建的链表自动执行每个 Channel 的收发,QSPI 硬件按照 BACON 自动控制 CS 和时序。CPU 完全解放,去做别的任务。

7.4 异步模式特有 API

API 用途 可用级别
Spi_GetJobResult(Job) 查询 Job 状态:OK / PENDING / FAILED / QUEUED Level 1/2
Spi_GetSequenceResult(Seq) 查询 Sequence 状态:OK / PENDING / FAILED / CANCELED Level 1/2
Spi_Cancel(Seq) 取消一个队列中的 Sequence Level 1/2
Spi_SetAsyncMode(Mode) 切换轮询/中断模式 Level 2 only

7.5 Level 2 的轮询模式

Level 2 之所以叫"混合双模",是因为它允许运行时通过 Spi_SetAsyncMode() 在两种模式间切换:

  • SPI_POLLING_MODE:禁用错误中断,由 Spi_MainFunction_Handling() 周期性轮询状态(适合 OS 任务调度)
  • SPI_INTERRUPT_MODE:启用所有中断,完全由硬件事件驱动(更低延迟,更适合高频传输)

8. 片选信号:硬件自动化 vs 手动 GPIO

8.1 CS_VIA_PERIPHERAL_ENGINE — 全自动片选

这是推荐的标准模式。QSPI 硬件的 SLSO(Slave Select Output)引脚自动控制 CS:

配置: CsSelection = CS_VIA_PERIPHERAL_ENGINE
      CsPolarity = LOW
      CsIdentifier = CHANNEL0  (映射到 SLSO0)

效果:
  1. Job 开始 → QSPI 自动拉低 SLSO0
  2. Job 执行中 → SLSO0 保持低
  3. Job 结束 → QSPI 自动释放 SLSO0 (回到高)
  4. Leading/Trailing/Idle 延迟全部由硬件自动保证

优点:时序精确(硬件保证纳秒级)、无 CPU 开销、不会因中断延迟导致 CS 时序抖动

8.2 CS_VIA_GPIO — 手动片选

当没有空闲的 SLSO 引脚,或硬件 CS 时序不满足要求时,可以用普通 GPIO 手动控制:

配置: CsSelection = CS_VIA_GPIO
      CSPortPin = 0x214  (P33.4 的编码: (33<<4)|4)

效果:
  1. Job 开始 → 驱动通过 Dio_WriteChannel 拉低指定 GPIO
  2. Job 执行中 → GPIO 保持低
  3. Job 结束 → 驱动通过 Dio_WriteChannel 释放 GPIO

缺点:CS 的时序精度受中断延迟影响,CPU 需要参与 CS 切换

8.3 片选有效电平

SSOC 寄存器控制每个 SLSO 的有效输出电平。ActiveChipSelectLevel 是一个编码值,每个 SLSO 线可以独立配置:

SLSO0: AOL=0 → 低有效 (常见)
SLSO1: AOL=1 → 高有效 (某些特殊芯片)
...
SLSO15: OEN=0 → 该 SLSO 未使能

9. 时钟与波特率:从浮点数到寄存器值的旅程

9.1 波特率如何编码

你在 Tresos 中输入一个浮点数(如 100000000.0 即 10 MHz),代码生成器会把它编码成一个 32-bit 的 BaudRateAndClockParam

BaudRateAndClockParam (32 bits)
┌─────────────────────────────────────────────────────────────┐
│ 高 16 位 (GLOBALCON):                                       │
│   bits [23:16] = TQ  (Time Quantum,时基分频)                │
│   bit  [30]    = LB  (LoopBack,回环)                        │
├─────────────────────────────────────────────────────────────┤
│ 低 16 位 (ECON):                                            │
│   bits [5:0]   = Q   (预分频)                                │
│   bits [7:6]   = A   (位段 1)                                │
│   bits [9:8]   = B   (位段 2)                                │
│   bits [11:10] = C   (位段 3)                                │
│   bit  [12]    = CPH (时钟相位)                               │
│   bit  [13]    = CPOL (时钟极性)                              │
│   bit  [14]    = PAREN (校验使能)                             │
└─────────────────────────────────────────────────────────────┘

波特率计算公式(简化):

f_SCLK = f_QSPI / (2 × (TQ+1) × (Q+1) × (A+B+C+3))

💡 Tresos 会自动算出这些值。你输入目标波特率、选择时钟模式、代码生成器完成编码。你不需要手算 — 但你理解编码原理有助于在出问题时用寄存器值反推实际波特率。

9.2 SPI 四种时钟模式

CPOL(时钟极性)和 CPH(时钟相位)的组合产生 SPI 的四种经典模式:

CPOL=0, CPHA=0 (模式 0):  CPOL=0, CPHA=1 (模式 1):
  SCK  ─┐   ┌──┐   ┌──      SCK  ┌──┐   ┌──┐   ┌
        └───┘  └───┘             ┘  └───┘  └───
  数据采样 ↑   ↑                  采样    ↑   ↑
  数据变化   ↑   ↑                变化    ↑   ↑

CPOL=1, CPHA=0 (模式 2):  CPOL=1, CPHA=1 (模式 3):
  SCK  ┌──┐   ┌──┐   ┌      SCK  ─┐   ┌──┐   ┌──
       ┘  └───┘  └───              └───┘  └───┘
  数据采样   ↑   ↑                采样 ↑   ↑
  数据变化 ↑   ↑                  变化   ↑   ↑

⚠️ 模式不匹配是 SPI 通信失败的头号原因。“数据发出去了但读到的是乱码” → 99% 的概率是 CPOL/CPHA 配错了。翻出从设备的数据手册,确认到底用的是哪个模式。


10. 时序延迟:Leading / Trailing / Idle 的精妙控制

10.1 三个延迟的物理含义

CS  ─┐                                    ┌──
      │← Lead ──→│                 ← Trail│
      └────────────────────────────────────┘
SCK  ─────────────────┐   ┌──┐   ┌─────────
                      └───┘  └───┘
                         │← Idle →│ (如果有下一个 BACON)
延迟 BACON 字段 物理含义 典型值
Leading LPRE + LEAD CS 有效后 → 第一个时钟沿 Flash tCSS ≈ 5-50ns
Trailing TPRE + TRAIL 最后时钟沿后 → CS 释放 Flash tCSH ≈ 5-50ns
Idle IPRE + IDLE 连续两帧之间的空闲时间 通常设为 0 或最小

10.2 延迟计算公式

Delay = (PRE + 1) × (LENGTH + 1) × tQ

其中 tQ = (TQ + 1) / f_QSPI
     f_QSPI 通常 = f_SPB (系统外设总线频率) = 100 MHz

示例计算

假设 TQ = 0, f_SPB = 100 MHz → tQ = 10 ns

你需要 50ns Leading Delay:
  50ns = (LPRE + 1) × (LEAD + 1) × 10ns
  (LPRE + 1) × (LEAD + 1) = 5
  可选: LPRE=0, LEAD=4 → (1)×(5)=5 ✓
  可选: LPRE=4, LEAD=0 → (5)×(1)=5 ✓

💡 代码生成器会自动完成这个计算,但了解原理能帮你在"我的 SPI 设备需要 120ns 的前导延迟,为什么 Tresos 不让我输入这个值"时自己调试。

10.3 一个容易被忽略的细节:tQ

tQ(Time Quantum)是 QSPI 所有时序的基础时钟单位。它在 GLOBALCON.TQ 中配置,影响所有延迟和波特率。

  • TQ=0 → tQ = 1/f_SPB ≈ 10ns (100MHz SPB)
  • TQ=1 → tQ = 2/f_SPB ≈ 20ns
  • TQ=7 → tQ = 8/f_SPB ≈ 80ns

选择 TQ 的权衡:TQ 越小 → 时序分辨率越高 → 但最大延迟越短。


11. 外部解复用器:一个 CS 控制多个设备

11.1 什么场景需要外部解复用器?

假设你有一个 QSPI 模块挂了 4 个相同的 SPI Flash 芯片。你有足够的数据线(MOSI/MISO/SCK 共享),但只有一个 SLSO0 片选可用。

解决方案:外接一个 2-4 解码器(如 74HC139),用 SLSO0 做"选通信号",用两个普通 GPIO 做"地址选择":

QSPI0.SLSO0 ────────── 解码器的 E (Enable)
QSPI0.SCLK ─┬─────── 所有 Flash 共享
QSPI0.MTSR ─┤
QSPI0.MRST ─┤
GPIO_A     ── 解码器的 A0
GPIO_B     ── 解码器的 A1

解码器输出:
  A0=0, A1=0 → Y0 → Flash 0 的 CS
  A0=1, A1=0 → Y1 → Flash 1 的 CS
  ...

11.2 选通延迟 (Strobe Delay)

当启用外部解复用器 (ExternalDemuxEnabled = 1) 时,QSPI 使用 SLSO0 的延迟模式

正常的 SLSO0(不延迟):与第一个数据位同时变化
延迟的 SLSO0(STROBE 模式):比数据位延迟 N 个 tQ 才变化

这样解码器在数据稳定后才锁存地址选择信号,
避免了 CS 和数据之间的竞争条件。

12. 安全机制与 Init Check

12.1 Spi_InitCheck 的验证链路

SPI_INIT_CHECK_API == STD_ON 时,Spi_InitCheck() 会验证:

Spi_InitCheck()
│
├── Spi_lCheckKernelVar()     → 运行时内核变量完整性
├── Spi_lCheckCoreVar()       → 每核变量完整性
├── Spi_lCheckRegisters()     → QSPI 寄存器复位值验证
└── Spi_lCheckBusStatus()     → 总线空闲状态确认

所有检查通过 → E_OK。任一失败 → E_NOT_OK + SPI_E_INIT_FAILED

12.2 安全运行时检查

SPI_SAFETY_ENABLE == STD_ON 时:

  • 每次 API 调用都验证参数有效性
  • 虚假中断检测SPI_E_SAFETY_SPURIOUS_INTERRUPT — 中断触发了但 QSPI 没有等待中断
  • 多核违规检测 (当 SPI_MULTICORE_ERROR_DETECT == STD_ON):从错误的 CPU 核心调用了 SPI API

12.3 Move Counter 限制

异步模式下,每个 Job 的元素总数不能超过 8190。这是 QSPI 硬件 Move Counter 的限制。如果超过,驱动会报 SPI_E_MOVECOUNTER_LIMIT_EXCEEDED

元素总数 = Σ(每个 Channel 的数据元素数)


13. 常见陷阱与避坑指南

陷阱 1:忘了 Port 配置

症状Spi_SyncTransmit() 返回 OK,但示波器上没有任何信号。

根因:Port 中没有把 SCK/MOSI/MISO/CS 引脚配成对应的 ALT 模式。

解决:先确认 Port 配置。SCK → ALT1(通常是),MOSI → ALT1,MISO → ALT1(输入),CS → ALT1 或 GPIO。

陷阱 2:Channel 数据宽度理解错误

症状:发送 0x03(8-bit 命令),但从设备收到 0x06

根因DataWidth 不是字节数,是位数DataWidth=8 = 8 位,不是 8 字节。

解决:检查 SpiDataWidth 参数:8-bit 数据选 8,16-bit 选 16,32-bit 选 32。

陷阱 3:同一个 Job 内混合 MSB 和 LSB

症状:有些 Channel 传输正常,有些数据位颠倒。

根因:Job 的 BACON.MSB 位是每个 Channel 独立配置的,但 ECON 中的 BE(Big Endian)是 Job 级的。如果 Job 级配置错误,Channel 级的 MSB 设置可能被覆盖。

解决:统一一个 Job 内所有 Channel 的字节序。如果需要混合 MSB/LSB,拆成两个 Job。

陷阱 4:异步模式忘了检查结果

症状Spi_AsyncTransmit() 返回 OK,但数据没发出去。

根因:异步模式下 Sequence 只是被入队,还没有真正执行完。你没有等结果就去读缓冲区。

解决

Spi_AsyncTransmit(Seq);
// ... 做别的事 ...
while (Spi_GetSequenceResult(Seq) == SPI_SEQ_PENDING) {
    // 等待完成或做其他工作
}
// 现在可以安全地读接收缓冲区

陷阱 5:同步传输时被高优先级任务抢占

症状Spi_SyncTransmit() 有时超时。

根因:同步模式轮询等待硬件完成,如果被高优先级 ISR 或任务长时间抢占,可能导致传输间隔过长、从设备超时。

解决:如果系统有长 ISR,用异步模式(Level 1/2),让 DMA 管理时序。

陷阱 6:IB vs EB 混淆

症状:写入了 Spi_WriteIB() 的值,但传输出来的还是旧数据。

根因:IB(内部缓冲)的数据在 Spi_WriteIB() 时更新,但如果没有在传输前调用,传输的仍然是缓冲区的旧值。

解决:传输前先 Spi_WriteIB(),或者用 EB(外部缓冲)直接指向你的数据数组。

陷阱 7:DMA 通道配置遗漏

症状:异步模式编译通过,但运行时第一个 Job 就卡住。

根因SpiHwConfiguration 中没有配置 DMA 通道号,或 DMA 驱动没有初始化。

解决:在 Tresos 中确保每个 QSPI 模块都有 SpiDmaTxChannelSpiDmaRxChannel 配置,并且对应的 DMA 驱动已初始化。

陷阱 8:ChannelGroup 不能跨端口(但 SPI 里没有 Group!)

这其实是一个正面陷阱 — SPI 没有 ChannelGroup 概念。如果你是从 DIO 教程过来,别找 SPI 的 Group 功能了 — SPI 用 Sequence+Job+Channel 的三层模型替代了 Group。


14. 端到端配置实战清单

场景设定

我们要配置一个 SPI Flash (W25Q 兼容) 的读取操作:

需求
QSPI 模块 QSPI0
波特率 20 MHz
时钟模式 模式 0 (CPOL=0, CPHA=0)
片选 SLSO0, 低有效
字节序 MSB first
前导延迟 50 ns
尾随延迟 30 ns
驱动级别 Level 0 (同步)

Step 1:Port 配置

引脚 方向 模式 上下拉 驱动
P20.11 (SCK) OUT ALT2 NO_PULL STRONG_SHARP (20MHz!)
P20.14 (MOSI) OUT ALT2 NO_PULL STRONG_SHARP
P20.12 (MISO) IN ALT2 NO_PULL
P20.10 (CS0) OUT ALT2 NO_PULL MEDIUM

⚠️ 20MHz 的 SCK 和 MOSI 必须用 STRONG_SHARP 驱动强度!MEDIUM 的边沿斜率不够陡,高频下会被从设备误判。

Step 2:SPI 配置 (Tresos)

SpiDriver:
  • SpiMaxChannel = 2(命令通道 + 数据通道)
  • SpiMaxJob = 1
  • SpiMaxSequence = 1
SpiChannel #0(命令通道):
  • SpiChannelId = 0
  • SpiChannelType = IB(命令码从内部缓冲发出)
  • SpiDataWidth = 8
  • SpiTransferStart = MSB
  • SpiDefaultData = 0x03(读命令)
  • SpiIbNBuffers = 1
SpiChannel #1(数据通道):
  • SpiChannelId = 1
  • SpiChannelType = EB(数据由应用缓冲区提供)
  • SpiDataWidth = 8
  • SpiEbMaxLength = 256
  • SpiTransferStart = MSB
SpiExternalDevice(对应 Job):
  • SpiBaudrate = 20000000.0
  • SpiCsIdentifier = CHANNEL0
  • SpiCsPolarity = LOW
  • SpiCsSelection = CS_VIA_PERIPHERAL_ENGINE
  • SpiClkPolarity = 0, SpiClkPhase = 0
  • SpiParity = NONE
  • SpiLeadDelay: LPRE=0, LEAD=4 → 50ns
  • SpiTrailDelay: TPRE=0, TRAIL=2 → 30ns
SpiJob #0:
  • SpiJobId = 0
  • 链接 Channel 0 → Channel 1
SpiSequence #0:
  • SpiSequenceId = 0
  • 链接 Job 0
SpiHwConfiguration(QSPI0):
  • SpiHwConfigKernel = QSPI0
  • SpiClockSetting = 时钟使能
  • ActiveChipSelectLevel = SLSO0 低有效
SpiGeneral:
  • SpiLevelDelivered = 0
  • SpiDevErrorDetect = ON
  • SpiVersionInfoApi = ON

Step 3:应用代码流程

初始化阶段:
  Port_Init()     ← 配置 SPI 引脚功能
  Spi_Init()      ← 配置 QSPI 硬件
  Spi_InitCheck() ← (安全项目) 验证寄存器

运行阶段 (读 Flash @ 地址 0x100000):
  // 1. 向命令通道写入"读数据"命令
  Spi_WriteIB(CMD_CH, &cmd_0x03);

  // 2. 设置数据通道的外部缓冲区 (地址 + 接收区)
  Spi_SetupEB(DATA_CH, &address_24bit, &rx_buf, 259);

  // 3. 执行传输 (同步模式)
  Spi_SyncTransmit(SEQ_FLASH_READ);

  // 4. 读取结果 (rx_buf[256] 中已收到 Flash 数据)

注意: 上面的 Channel 组合需要仔细设计 -
Channel 0 只发 0x03 (1 byte)
Channel 1 发 3-byte 地址 (24-bit) + 收 256 bytes
或者拆成多个 Channel 来区分"发地址"和"收数据"阶段

Step 4:验证清单

  • 示波器量 SCK → 20 MHz 方波,无振铃
  • 示波器量 CS → 传输前拉低,传输后释放,领先/尾随时序符合 Flash 手册
  • 示波器量 MOSI → MSB first,数据正确
  • 逻辑分析仪抓取完整 SPI 帧 → 命令+地址+数据 全部正确
  • Spi_GetStatus() 返回 SPI_IDLE
  • (安全项目)Spi_InitCheck() 返回 E_OK

附录 A:SPI API 速查表

API 作用 可用级别
Spi_Init 初始化驱动 All
Spi_DeInit 反初始化 All
Spi_WriteIB 写内部缓冲 All (IB 使能时)
Spi_ReadIB 读内部缓冲 All (IB 使能时)
Spi_SetupEB 设置外部缓冲区地址和长度 All (EB 使能时)
Spi_SyncTransmit 同步传输一个 Sequence Level 0 / Level 2
Spi_AsyncTransmit 异步传输一个 Sequence Level 1 / Level 2
Spi_GetStatus 获取驱动状态 All
Spi_GetJobResult 查询 Job 结果 All
Spi_GetSequenceResult 查询 Sequence 结果 All
Spi_GetHWUnitStatus 查询 QSPI 硬件单元状态 All (API 使能时)
Spi_Cancel 取消队列中的 Sequence Level 1/2 (API 使能时)
Spi_SetAsyncMode 切换轮询/中断模式 Level 2 only
Spi_ControlLoopBack 使能/禁用回环自测 All (API 使能时)
Spi_InitCheck 验证初始化完整性 All (API 使能时)
Spi_GetVersionInfo 获取版本信息 All (API 使能时)

附录 B:Level 选择决策树

你需要异步 (不阻塞调用者)?
├─ 是 → 需要运行时切换轮询/中断模式?
│        ├─ 是 → Level 2
│        └─ 否 → Level 1
│
└─ 否 → 传输量大 (>100 bytes/次)?
         ├─ 是 → 考虑 Level 1/2 (用 DMA 释放 CPU)
         └─ 否 → Level 0 (一致就够)

附录 C:SPI vs QSPI vs DSPI vs MSC 名词辨析

名词 全称 含义
SPI Serial Peripheral Interface 通信协议
QSPI Queued SPI AURIX 上带硬件队列的 SPI 控制器
Queued SPI 也用 QSPI 缩写,通常指支持 XIP 的 Flash 专用 SPI
DSPI Deserial SPI 将串行数据解串为并行
MSC Micro Second Channel AURIX 另一种串行协议,用于快速 GPIO 扩展

附录 D:本文术语中英对照

英文 中文 说明
BACON 基本配置寄存器 QSPI 硬件描述单次 SPI 传输的 32-bit 配置字
Channel 通道 最小的独立数据收发单元
Job 作业 同一 CS 周期内的一组 Channel
Sequence 序列 一组可能跨总线/设备的 Job
SLSO 从设备选择输出 QSPI 硬件的片选信号线
MTSR 主发从收 Master Transmit Slave Receive = MOSI
MRST 主收从发 Master Receive Slave Transmit = MISO
TQ 时基 Time Quantum,QSPI 所有时序的最小单位
Leading Delay 前导延迟 CS 有效到第一个时钟之间的等待
Trailing Delay 尾随延迟 最后时钟到 CS 释放之间的等待
IB / EB 内部/外部缓冲 Internal/External Buffer
TCS 事务控制集 DMA 的传输描述符 (Transaction Control Set)
PT2 相位转换 2 QSPI 在帧结束时产生的中断事件
Move Counter 移动计数器 QSPI 硬件跟踪剩余传输次数的计数器
External Demux 外部解复用器 用 CS + GPIO 扩展更多片选

结语:MCAL SPI 驱动是 QSPI 硬件强大能力的软件体现。BACON 序列器让原本需要 CPU 位操作的 SPI 变得像"DMA 自动驾驶",而 Channel → Job → Sequence 的三层架构为复杂 SPI 场景提供了清晰的抽象。理解这三个层次的关系和 BACON 的工作原理,你就真正掌握了 AUTOSAR SPI 的精髓。

特别提示:本文所涉及的技术细节仅供学习参考,实际产品开发中请以芯片厂商提供的最新数据手册和 MCAL 版本为准。

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐