硬件平台:STM32F103,SPI1;开发方式:C++面向接口分层;标准库实现;单SPI总线挂载多个SPI从设备(W25Q‑Flash + SPI‑ADC示例)
核心思想:依赖抽象,不依赖具体硬件;寄存器/标准库全部收敛在驱动层;设备层与MCU完全解耦
一、SPI硬件底层原理
- SPI是同步全双工串行总线,SCK时钟由主机产生。
- 物理信号:
- SCK:串行时钟
- MOSI:主机输出,从机输入
- MISO:从机输出,主机输入
- CS(NSS):片选,每个从设备独立一根CS GPIO
- 关键特性
- SCK/MOSI/MISO是共享总线,所有从机并联;CS是设备私有信号。
- 同一时刻只能拉低1个CS,被选中设备驱动MISO;未选中设备MISO高阻,不干扰总线。
- SPI硬件是移位寄存器背对背连接:每8个SCK脉冲,同时收发1字节。不存在单纯“读”操作;想要读取从机数据,主机必须发送哑字节(一般
0xFF)来产生时钟。
- 本示例时序:CPOL=0,CPHA=0(W25Q、MCP3202常用模式)。
CS不属于SPI外设本身,CS是普通GPIO输出。
二、整体分层架构
四层架构,严格隔离,实现可移植,单总线多设备。
| 层级 | 文件 | 职责 | 是否接触STM32库/寄存器 |
|---|---|---|---|
| 抽象接口层 | spi_base.hpp、gpio_out_base.hpp | 定义SPI、GPIO输出纯虚接口,只定义行为,无实现 | ❌完全不接触硬件 |
| 硬件驱动层 | spi1_stdlib.hpp/cpp、stm32_gpio_stdlib.hpp/cpp | 实现SPI1硬件、GPIO输出;调用标准库;MCU强相关 | ✅唯一操作硬件的层 |
| 设备层 | w25q_flash.hpp/cpp、spi_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)
- 编写设备类,构造入参:
SpiBase* spi、GpioOutBase* cs_gpio; - 设备内部调用
_cs->setLow()/setHigh()、_spi->readWriteByte()实现协议; - main中新建
Stm32GpioOut作为该设备CS并初始化; - 构建设备对象,传入全局spi总线指针与CS指针;
SPI驱动代码无需修改。
六、Keil MDK编译约束
- 文件后缀使用
.cpp,工程开启C++编译; - 禁止全局域实例化对象,全部在main函数内实例;
- 若使用new,保证启动文件
Heap_Size配置足够; - 通信结束务必拉高CS,否则MISO持续驱动总线干扰其他设备。
七、常见踩坑清单
- SPI没有纯读操作,读必须发送哑字节,惯例用
0xFF,不要用0x00; - 多设备禁止同时拉低两个CS,会造成MISO总线冲突;
- BSY标志位为1代表移位还在进行;TXE只是发送缓冲为空,不等于收发完成;
- CS硬件初始化默认输出高电平;
- CPOL、CPHA必须和外设datasheet保持一致。
八、移植特性
移植其他MCU(GD32等):
- 无需改动:
spi_base.hpp、gpio_out_base.hpp、全部设备层h/cpp; - 需要重写:SPI驱动、GPIO输出驱动;
- main仅修改硬件驱动实例创建部分。

6412

被折叠的 条评论
为什么被折叠?



