STM32 SPI软件设计方案

硬件平台:STM32F103,SPI1;开发方式:C++面向接口分层;标准库实现;单SPI总线挂载多个SPI从设备(W25Q‑Flash + SPI‑ADC示例)
核心思想:依赖抽象,不依赖具体硬件;寄存器/标准库全部收敛在驱动层;设备层与MCU完全解耦

一、SPI硬件底层原理

  1. SPI是同步全双工串行总线,SCK时钟由主机产生。
  2. 物理信号:
    • SCK:串行时钟
    • MOSI:主机输出,从机输入
    • MISO:从机输出,主机输入
    • CS(NSS):片选,每个从设备独立一根CS GPIO
  3. 关键特性
    • SCK/MOSI/MISO是共享总线,所有从机并联;CS是设备私有信号。
    • 同一时刻只能拉低1个CS,被选中设备驱动MISO;未选中设备MISO高阻,不干扰总线。
    • SPI硬件是移位寄存器背对背连接:每8个SCK脉冲,同时收发1字节。不存在单纯“读”操作;想要读取从机数据,主机必须发送哑字节(一般0xFF)来产生时钟。
  4. 本示例时序:CPOL=0,CPHA=0(W25Q、MCP3202常用模式)。

CS不属于SPI外设本身,CS是普通GPIO输出。

二、整体分层架构

四层架构,严格隔离,实现可移植,单总线多设备。

层级文件职责是否接触STM32库/寄存器
抽象接口层spi_base.hppgpio_out_base.hpp定义SPI、GPIO输出纯虚接口,只定义行为,无实现❌完全不接触硬件
硬件驱动层spi1_stdlib.hpp/cppstm32_gpio_stdlib.hpp/cpp实现SPI1硬件、GPIO输出;调用标准库;MCU强相关✅唯一操作硬件的层
设备层w25q_flash.hpp/cppspi_adc_demo.hpp/cpp外设业务逻辑,实现W25Q、ADC命令协议;只依赖抽象接口❌无硬件代码,可跨MCU复用
应用层(main)main.cpp对象实例化、依赖注入;业务调用少量硬件实例创建,业务逻辑与硬件解耦

硬件接线

  • SPI1_SCK PA5
  • SPI1_MOSI PA7
  • SPI1_MISO PA6
  • W25Q CS → PA4
  • SPI‑ADC CS → PA3

SCK/MOSI/MISO两个设备并联;各自独立CS引脚。

三、完整全部代码

1. gpio_out_base.hpp 【抽象GPIO输出接口】

#ifndef GPIO_OUT_BASE_HPP
#define GPIO_OUT_BASE_HPP

class GpioOutBase
{
public:
    virtual ~GpioOutBase() = default;
    virtual void setLow()  = 0;
    virtual void setHigh() = 0;
};

#endif

2. spi_base.hpp 【SPI抽象接口,移除csSet】

#ifndef SPI_BASE_HPP
#define SPI_BASE_HPP
#include <cstdint>

class SpiBase
{
public:
    virtual ~SpiBase() = default;
    virtual bool init() = 0;
    virtual uint8_t readWriteByte(uint8_t tx) = 0;
    virtual void readWriteBuffer(const uint8_t* txBuf, uint8_t* rxBuf, uint16_t len) = 0;
};

#endif

3. stm32_gpio_stdlib.hpp 标准库GPIO输出驱动头文件

#ifndef STM32_GPIO_STDLIB_HPP
#define STM32_GPIO_STDLIB_HPP
#include "gpio_out_base.hpp"
#include "stm32f10x.h"

class Stm32GpioOut : public GpioOutBase
{
private:
    GPIO_TypeDef* _port;
    uint16_t _pin;
public:
    Stm32GpioOut(GPIO_TypeDef* port, uint16_t pin);
    void gpioInit();
    void setLow() override;
    void setHigh() override;
};

#endif
stm32_gpio_stdlib.cpp
#include "stm32_gpio_stdlib.hpp"

Stm32GpioOut::Stm32GpioOut(GPIO_TypeDef* port, uint16_t pin)
    : _port(port), _pin(pin)
{}

void Stm32GpioOut::gpioInit()
{
    GPIO_InitTypeDef gpio_cfg;
    if(_port == GPIOA) RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    else if(_port == GPIOB) RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

    gpio_cfg.GPIO_Pin = _pin;
    gpio_cfg.GPIO_Mode = GPIO_Mode_Out_PP;
    gpio_cfg.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(_port, &gpio_cfg);
    GPIO_SetBits(_port, _pin); // CS默认高电平
}

void Stm32GpioOut::setLow()
{
    GPIO_ResetBits(_port, _pin);
}

void Stm32GpioOut::setHigh()
{
    GPIO_SetBits(_port, _pin);
}

4. spi1_stdlib.hpp SPI1标准库驱动头文件

#ifndef SPI1_STDLIB_HPP
#define SPI1_STDLIB_HPP
#include "spi_base.hpp"
#include "stm32f10x.h"

class Spi1StdLib : public SpiBase
{
public:
    bool init() override;
    uint8_t readWriteByte(uint8_t tx) override;
    void readWriteBuffer(const uint8_t* txBuf, uint8_t* rxBuf, uint16_t len) override;
};

#endif
spi1_stdlib.cpp
#include "spi1_stdlib.hpp"

bool Spi1StdLib::init()
{
    GPIO_InitTypeDef gpio_cfg;
    SPI_InitTypeDef spi_cfg;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE);

    // PA5 SCK PA7 MOSI 复用推挽输出
    gpio_cfg.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
    gpio_cfg.GPIO_Mode = GPIO_Mode_AF_PP;
    gpio_cfg.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &gpio_cfg);

    // PA6 MISO 浮空输入
    gpio_cfg.GPIO_Pin = GPIO_Pin_6;
    gpio_cfg.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &gpio_cfg);

    SPI_I2S_DeInit(SPI1);

    spi_cfg.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
    spi_cfg.SPI_Mode = SPI_Mode_Master;
    spi_cfg.SPI_DataSize = SPI_DataSize_8b;
    spi_cfg.SPI_CPOL = SPI_CPOL_Low;
    spi_cfg.SPI_CPHA = SPI_CPHA_1Edge;
    spi_cfg.SPI_NSS = SPI_NSS_Soft;
    spi_cfg.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4;
    spi_cfg.SPI_FirstBit = SPI_FirstBit_MSB;
    spi_cfg.SPI_CRCPolynomial = 7;
    SPI_Init(SPI1, &spi_cfg);

    SPI_Cmd(SPI1, ENABLE);
    return true;
}

uint8_t Spi1StdLib::readWriteByte(uint8_t tx)
{
    while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);
    SPI_I2S_SendData(SPI1, tx);
    while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) == SET);
    return static_cast<uint8_t>(SPI_I2S_ReceiveData(SPI1));
}

void Spi1StdLib::readWriteBuffer(const uint8_t* txBuf, uint8_t* rxBuf, uint16_t len)
{
    for(uint16_t i = 0; i < len; i++)
    {
        uint8_t tx = txBuf ? txBuf[i] : 0xFFU;
        uint8_t rx = readWriteByte(tx);
        if(rxBuf)
        {
            rxBuf[i] = rx;
        }
    }
}

5. w25q_flash.hpp W25Q设备层头文件

#ifndef W25Q_FLASH_HPP
#define W25Q_FLASH_HPP
#include "spi_base.hpp"
#include "gpio_out_base.hpp"
#include <cstdint>

class W25qFlashDevice
{
private:
    SpiBase* _spi;
    GpioOutBase* _cs;
public:
    W25qFlashDevice(SpiBase* spi, GpioOutBase* cs_gpio);
    uint32_t readJedecId();
    void readData(uint32_t addr, uint8_t* buf, uint16_t len);
private:
    static constexpr uint8_t CMD_JEDEC_ID = 0x9F;
    static constexpr uint8_t CMD_READ     = 0x03;
};

#endif
w25q_flash.cpp
#include "w25q_flash.hpp"

W25qFlashDevice::W25qFlashDevice(SpiBase* spi, GpioOutBase* cs_gpio)
    : _spi(spi), _cs(cs_gpio)
{}

uint32_t W25qFlashDevice::readJedecId()
{
    _cs->setLow();
    _spi->readWriteByte(CMD_JEDEC_ID);
    uint8_t mfr  = _spi->readWriteByte(0xFF);
    uint8_t mem  = _spi->readWriteByte(0xFF);
    uint8_t cap  = _spi->readWriteByte(0xFF);
    _cs->setHigh();
    return (static_cast<uint32_t>(mfr) << 16) | (static_cast<uint32_t>(mem) <<8) | cap;
}

void W25qFlashDevice::readData(uint32_t addr, uint8_t* buf, uint16_t len)
{
    _cs->setLow();
    _spi->readWriteByte(CMD_READ);
    _spi->readWriteByte((addr >> 16) & 0xFFU);
    _spi->readWriteByte((addr >> 8)  & 0xFFU);
    _spi->readWriteByte(addr & 0xFFU);
    _spi->readWriteBuffer(nullptr, buf, len);
    _cs->setHigh();
}

6. spi_adc_demo.hpp SPI‑ADC设备层头文件

#ifndef SPI_ADC_DEMO_HPP
#define SPI_ADC_DEMO_HPP
#include "spi_base.hpp"
#include "gpio_out_base.hpp"
#include <cstdint>

class SpiAdcDevice
{
private:
    SpiBase* _spi;
    GpioOutBase* _cs;
public:
    SpiAdcDevice(SpiBase* spi, GpioOutBase* cs_gpio);
    uint16_t readChannel(uint8_t ch);
};

#endif
spi_adc_demo.cpp
#include "spi_adc_demo.hpp"

SpiAdcDevice::SpiAdcDevice(SpiBase* spi, GpioOutBase* cs_gpio)
    : _spi(spi), _cs(cs_gpio)
{}

uint16_t SpiAdcDevice::readChannel(uint8_t ch)
{
    _cs->setLow();
    uint8_t cmd = static_cast<uint8_t>(0x04 | (ch << 1));
    _spi->readWriteByte(cmd);
    uint8_t high = _spi->readWriteByte(0xFF);
    uint8_t low  = _spi->readWriteByte(0xFF);
    _cs->setHigh();
    uint16_t result = (static_cast<uint16_t>(high & 0x0F) << 8) | low;
    return result;
}

7. main.cpp 应用层,对象组装

#include "spi1_stdlib.hpp"
#include "stm32_gpio_stdlib.hpp"
#include "w25q_flash.hpp"
#include "spi_adc_demo.hpp"

int main(void)
{
    // 1.初始化唯一SPI总线
    Spi1StdLib spi1_bus;
    spi1_bus.init();

    // 2.每个设备独立CS GPIO实例
    Stm32GpioOut cs_w25q(GPIOA, GPIO_Pin_4);
    Stm32GpioOut cs_adc(GPIOA, GPIO_Pin_3);
    cs_w25q.gpioInit();
    cs_adc.gpioInit();

    // 3.设备注入:共享同一个SPI总线,各自独立CS
    W25qFlashDevice flash(&spi1_bus, &cs_w25q);
    SpiAdcDevice adc(&spi1_bus, &cs_adc);

    uint32_t flash_id = flash.readJedecId();
    uint16_t adc_val  = adc.readChannel(0);

    while(1)
    {
        // 用户业务循环
    }
}

四、关键接口说明

SpiBase

virtual bool init() = 0;
virtual uint8_t readWriteByte(uint8_t tx) = 0;
virtual void readWriteBuffer(const uint8_t* txBuf, uint8_t* rxBuf, uint16_t len) = 0;
  • readWriteByte(tx):全双工交换1字节;只想读传入哑字节0xFF
  • readWriteBuffer(txBuf, rxBuf, len):批量收发;txBuf=nullptr自动填充0xFF哑字节。

GpioOutBase

virtual void setLow() = 0;
virtual void setHigh() = 0;

只描述引脚高低语义,无硬件信息。

W25Q readJedecId时序说明

_cs->setLow();
_spi->readWriteByte(0x9F);   // MOSI发送JEDEC标准命令0x9F
mfr = _spi->readWriteByte(0xFF); // 发哑字节产生SCK,读取厂商ID
mem = _spi->readWriteByte(0xFF); // 读取存储类型
cap = _spi->readWriteByte(0xFF); // 读取容量ID
_cs->setHigh();

JEDEC为电子器件行业标准化组织;0x9F是SPI‑NOR Flash标准读ID命令;后面三次0xFF为哑字节,MOSI输出会被FLASH忽略,仅用来产生时钟读取MISO数据。

五、新增SPI外设步骤(例SPI‑OLED)

  1. 编写设备类,构造入参:SpiBase* spiGpioOutBase* cs_gpio
  2. 设备内部调用_cs->setLow()/setHigh()_spi->readWriteByte()实现协议;
  3. main中新建Stm32GpioOut作为该设备CS并初始化;
  4. 构建设备对象,传入全局spi总线指针与CS指针;

SPI驱动代码无需修改。

六、Keil MDK编译约束

  1. 文件后缀使用.cpp,工程开启C++编译;
  2. 禁止全局域实例化对象,全部在main函数内实例;
  3. 若使用new,保证启动文件Heap_Size配置足够;
  4. 通信结束务必拉高CS,否则MISO持续驱动总线干扰其他设备。

七、常见踩坑清单

  1. SPI没有纯读操作,读必须发送哑字节,惯例用0xFF,不要用0x00
  2. 多设备禁止同时拉低两个CS,会造成MISO总线冲突;
  3. BSY标志位为1代表移位还在进行;TXE只是发送缓冲为空,不等于收发完成;
  4. CS硬件初始化默认输出高电平;
  5. CPOL、CPHA必须和外设datasheet保持一致。

八、移植特性

移植其他MCU(GD32等):

  • 无需改动:spi_base.hppgpio_out_base.hpp、全部设备层h/cpp;
  • 需要重写:SPI驱动、GPIO输出驱动;
  • main仅修改硬件驱动实例创建部分。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值