3、SPI多从机配置:菊花链拓扑与独立片选拓扑

多从机连接,是SPI实战中绕不开的坎。很多初学者觉得SPI就是四根线,接上就能用。但真到了项目里,一个主控要挂三四个传感器、两个ADC、一个LCD,怎么连?连完怎么保证不打架?

我个人习惯把多从机拓扑分成两大类:独立片选和菊花链。这两种方式,从硬件到软件,思路完全不同。今天咱们就把它掰开揉碎了讲清楚。

3.1 独立片选拓扑:最常用的方案

独立片选,说白了就是每个从机独占一条CS线。主控有多少个从机,就得有多少个GPIO口当片选用。

核心特点: 每个从机独立CS,共享SCK、MOSI、MISO。

我最早做的一个数据采集项目,就是用的这种方案。主控是STM32F103,挂了三个传感器:温度、湿度、气压。每个传感器一根CS线,共用了SCK和MOSI。当时觉得挺简单,但调试时发现一个问题——CS线的时序必须严格对齐。

为什么会这样?因为如果CS拉低后SCK还没稳定,从机可能误判起始条件。嗯,这里要注意:CS拉低后,至少要等半个SCK周期再发时钟。

3.1.1 硬件连接要点

  • CS线独立:每个从机一根,主控GPIO直接控制
  • MISO线共享:从机输出必须三态,未选中时输出高阻
  • SCK/MOSI共享:所有从机同时接收,但只有被选中的才响应

我曾经踩过的坑: 某次项目中用了两个不同厂家的SPI Flash,一个CS低有效,一个CS高有效。结果初始化时忘了配置极性,两个芯片同时响应,MISO上数据打架,总线直接崩溃。后来花了半天才定位到问题。所以,务必确认每个从机的CS极性,别想当然。

3.1.2 软件配置流程

  1. 初始化所有CS引脚为推挽输出,默认置高(失能)
  2. 配置SPI外设:模式、极性、相位、速率
  3. 通信前,拉低目标从机的CS
  4. 发送/接收数据
  5. 通信结束,拉高CS
  6. 切换下一个从机,重复步骤3-5
// 独立片选示例:切换从机
void SPI_SelectSlave(uint8_t slave_id) {
    // 先全部失能
    HAL_GPIO_WritePin(CS1_GPIO_Port, CS1_Pin, GPIO_PIN_SET);
    HAL_GPIO_WritePin(CS2_GPIO_Port, CS2_Pin, GPIO_PIN_SET);
    HAL_GPIO_WritePin(CS3_GPIO_Port, CS3_Pin, GPIO_PIN_SET);
    
    // 使能目标从机
    switch(slave_id) {
        case 0: HAL_GPIO_WritePin(CS1_GPIO_Port, CS1_Pin, GPIO_PIN_RESET); break;
        case 1: HAL_GPIO_WritePin(CS2_GPIO_Port, CS2_Pin, GPIO_PIN_RESET); break;
        case 2: HAL_GPIO_WritePin(CS3_GPIO_Port, CS3_Pin, GPIO_PIN_RESET); break;
        default: break;
    }
    
    // 等待CS稳定,至少延时1us
    delay_us(1);
}

3.2 菊花链拓扑:少引脚,高要求

独立片选虽然简单,但GPIO不够用怎么办?比如一个主控只有3个空闲IO,却要控制8个从机。这时候,菊花链就派上用场了。

菊花链的玩法是:所有从机共用一根CS线,数据像链条一样逐级传递。主控发数据给从机1,从机1处理完再转发给从机2,以此类推。

我个人的经验: 菊花链适合写多读少的场景。比如LED驱动、DAC配置这类只需要下发数据的应用。如果是传感器读取,菊花链的延迟会让人抓狂。

3.2.1 硬件连接方式

信号 连接方式 说明
CS 所有从机共用 同时使能/失能所有从机
SCK 所有从机共用 时钟同步
MOSI 主控→从机1→从机2→... 数据逐级传递
MISO 最后一个从机→主控 最终数据返回

你想想看,这种结构下,主控发一个字节,要经过N个从机才能到达目标。每个从机都会延迟一个字节的时间。所以,菊花链的总延迟 = 从机数量 × 字节传输时间。

3.2.2 数据帧结构

菊花链模式下,数据帧必须包含地址信息。每个从机收到数据后,先检查地址是否匹配自己,匹配则处理,不匹配则原样转发。

// 菊花链数据帧格式
// | 地址(1字节) | 命令(1字节) | 数据(N字节) | 校验(1字节) |

#define CHAIN_ADDR_SENSOR1  0x01
#define CHAIN_ADDR_SENSOR2  0x02
#define CHAIN_ADDR_SENSOR3  0x03

uint8_t chain_frame[] = {
    CHAIN_ADDR_SENSOR2,  // 目标地址
    0x10,                // 读命令
    0x00, 0x00,          // 寄存器地址
    0x00                 // 校验和(简化)
};

3.3 多从机地址分配与仲裁机制

不管是独立片选还是菊花链,只要总线上有多个从机,就绕不开地址分配和冲突仲裁。

3.3.1 地址分配策略

我见过三种常见的分配方式:

  • 硬件引脚配置:从机通过外部引脚设置地址(如ADDR0、ADDR1)。优点是上电即用,缺点是占用引脚。
  • 软件初始化分配:主控上电后,逐个使能从机,下发唯一地址。优点是灵活,缺点是需要从机支持。
  • 固定地址:从机出厂地址固定,不可更改。最简单,但容易冲突。

我建议: 如果项目允许,优先用硬件引脚配置。我在一个工业控制项目中用过软件分配,结果某次掉电重启后,从机地址丢失,整个系统瘫痪。从那以后,我对软件分配就多留了个心眼。

3.3.2 总线仲裁机制

SPI本身没有像I2C那样的多主仲裁机制。但多从机场景下,MISO线上的冲突仍然可能发生。常见仲裁方式有两种:

仲裁方式 原理 适用场景
CS互斥 同一时刻只使能一个从机 独立片选拓扑
三态输出 未选中从机MISO输出高阻 所有拓扑
地址过滤 从机只响应匹配地址的帧 菊花链拓扑

嗯,这里要特别强调:三态输出是SPI多从机的基础。如果某个从机的MISO不是三态,而是推挽输出,那它没被选中时也会拉低或拉高总线,直接导致其他从机通信失败。

我曾经遇到过一个案例: 某款国产SPI ADC,数据手册上写着MISO是三态输出。结果实际测试发现,失能后MISO仍然输出低电平。后来加了外部三态缓冲器才解决。所以,关键芯片一定要实测验证,别全信数据手册。

3.4 两种拓扑的对比与选型建议

说了这么多,到底该用哪种?我整理了一个对比表,方便你决策:

对比项 独立片选 菊花链
GPIO占用 多(每个从机1根) 少(所有从机共用1根)
通信速率 高(无额外延迟) 低(逐级延迟)
软件复杂度 低 高(需地址管理)
可靠性 高(故障隔离) 低(单点故障影响全链)
适用场景 传感器、ADC、Flash LED驱动、DAC、移位寄存器

我个人习惯是:GPIO够用就选独立片选。简单、可靠、好调试。只有GPIO实在不够,或者从机数量超过10个,我才会考虑菊花链。

3.5 本章知识结构图

下面这张图,把两种拓扑的核心差异和选型逻辑串起来了。你可以保存下来,做项目时对照着看。

SPI多从机拓扑选型决策图 多从机拓扑 独立片选拓扑 菊花链拓扑 每个从机独立CS线 MISO三态输出 CS互斥仲裁 共用CS线 数据逐级传递 地址过滤仲裁 选型建议 GPIO充足 → 独立片选(简单可靠) GPIO不足且写多读少 → 菊花链

3.6 避坑总结

最后,把我这些年踩过的坑汇总一下,你遇到类似问题可以直接对照排查:

  • CS时序:拉低CS后必须等待至少半个SCK周期再发时钟
  • MISO三态:务必确认从机失能后MISO是高阻,不是推挽
  • 地址冲突:硬件引脚配置最可靠,软件分配要做好掉电保护
  • 菊花链延迟:总延迟 = 从机数 × 字节时间,实时性要求高的别用
  • CS极性:不同从机可能高低有效不同,初始化时逐一确认

嗯,多从机配置这块,说白了就是硬件拓扑决定上限,软件设计决定下限。选对拓扑,配好地址,仲裁机制到位,SPI多从机系统就能稳定跑起来。