MCAL SPI 配置完全指南
面向读者:1-3 年嵌入式软件工程师,已理解 Port 和 DIO 的基本概念,希望彻底掌握 MCAL 中最复杂的通信驱动之一 — SPI。
前置阅读:建议先阅读 MCAL Port 配置教程,理解引脚功能配置(SPI 的 SCK/MOSI/MISO/CS 需要先通过 Port 配成 ALT 模式)。
目标平台:Infineon AURIX TC3xx (TriCore 架构),SPI 方法论适用于所有 AUTOSAR MCAL 平台。
发布平台:CSDN
目录
- SPI 基础:你以为你懂了,但你真的懂了吗?
- QSPI 硬件:不只是"一个 SPI 模块"
- BACON 序列器:芯片如何替你"自动驾驶"
- 三层架构:Channel → Job → Sequence
- 配置结构全景图
- 同步传输:Level 0 的轮询世界
- 异步传输:Level 1/2 的 DMA 世界
- 片选信号:硬件自动化 vs 手动 GPIO
- 时钟与波特率:从浮点数到寄存器值的旅程
- 时序延迟:Leading / Trailing / Idle 的精妙控制
- 外部解复用器:一个 CS 控制多个设备
- 安全机制与 Init Check
- 常见陷阱与避坑指南
- 端到端配置实战清单
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 模块都有 SpiDmaTxChannel 和 SpiDmaRxChannel 配置,并且对应的 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= 1SpiMaxSequence= 1
SpiChannel #0(命令通道):
SpiChannelId= 0SpiChannelType= IB(命令码从内部缓冲发出)SpiDataWidth= 8SpiTransferStart= MSBSpiDefaultData= 0x03(读命令)SpiIbNBuffers= 1
SpiChannel #1(数据通道):
SpiChannelId= 1SpiChannelType= EB(数据由应用缓冲区提供)SpiDataWidth= 8SpiEbMaxLength= 256SpiTransferStart= MSB
SpiExternalDevice(对应 Job):
SpiBaudrate= 20000000.0SpiCsIdentifier= CHANNEL0SpiCsPolarity= LOWSpiCsSelection= CS_VIA_PERIPHERAL_ENGINESpiClkPolarity= 0,SpiClkPhase= 0SpiParity= NONESpiLeadDelay: LPRE=0, LEAD=4 → 50nsSpiTrailDelay: TPRE=0, TRAIL=2 → 30ns
SpiJob #0:
SpiJobId= 0- 链接 Channel 0 → Channel 1
SpiSequence #0:
SpiSequenceId= 0- 链接 Job 0
SpiHwConfiguration(QSPI0):
SpiHwConfigKernel= QSPI0SpiClockSetting= 时钟使能ActiveChipSelectLevel= SLSO0 低有效
SpiGeneral:
SpiLevelDelivered= 0SpiDevErrorDetect= ONSpiVersionInfoApi= 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 版本为准。
更多推荐




所有评论(0)