
硬件平台STM32F103SPI1开发方式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)来产生时钟。本示例时序CPOL0CPHA0W25Q、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 PA5SPI1_MOSI PA7SPI1_MISO PA6W25Q CS → PA4SPI‑ADC CS → PA3SCK/MOSI/MISO两个设备并联各自独立CS引脚。三、完整全部代码1. gpio_out_base.hpp 【抽象GPIO输出接口】#ifndefGPIO_OUT_BASE_HPP#defineGPIO_OUT_BASE_HPPclassGpioOutBase{public:virtual~GpioOutBase()default;virtualvoidsetLow()0;virtualvoidsetHigh()0;};#endif2. spi_base.hpp 【SPI抽象接口移除csSet】#ifndefSPI_BASE_HPP#defineSPI_BASE_HPP#includecstdintclassSpiBase{public:virtual~SpiBase()default;virtualboolinit()0;virtualuint8_treadWriteByte(uint8_ttx)0;virtualvoidreadWriteBuffer(constuint8_t*txBuf,uint8_t*rxBuf,uint16_tlen)0;};#endif3. stm32_gpio_stdlib.hpp 标准库GPIO输出驱动头文件#ifndefSTM32_GPIO_STDLIB_HPP#defineSTM32_GPIO_STDLIB_HPP#includegpio_out_base.hpp#includestm32f10x.hclassStm32GpioOut:publicGpioOutBase{private:GPIO_TypeDef*_port;uint16_t_pin;public:Stm32GpioOut(GPIO_TypeDef*port,uint16_tpin);voidgpioInit();voidsetLow()override;voidsetHigh()override;};#endifstm32_gpio_stdlib.cpp#includestm32_gpio_stdlib.hppStm32GpioOut::Stm32GpioOut(GPIO_TypeDef*port,uint16_tpin):_port(port),_pin(pin){}voidStm32GpioOut::gpioInit(){GPIO_InitTypeDef gpio_cfg;if(_portGPIOA)RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);elseif(_portGPIOB)RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);gpio_cfg.GPIO_Pin_pin;gpio_cfg.GPIO_ModeGPIO_Mode_Out_PP;gpio_cfg.GPIO_SpeedGPIO_Speed_50MHz;GPIO_Init(_port,gpio_cfg);GPIO_SetBits(_port,_pin);// CS默认高电平}voidStm32GpioOut::setLow(){GPIO_ResetBits(_port,_pin);}voidStm32GpioOut::setHigh(){GPIO_SetBits(_port,_pin);}4. spi1_stdlib.hpp SPI1标准库驱动头文件#ifndefSPI1_STDLIB_HPP#defineSPI1_STDLIB_HPP#includespi_base.hpp#includestm32f10x.hclassSpi1StdLib:publicSpiBase{public:boolinit()override;uint8_treadWriteByte(uint8_ttx)override;voidreadWriteBuffer(constuint8_t*txBuf,uint8_t*rxBuf,uint16_tlen)override;};#endifspi1_stdlib.cpp#includespi1_stdlib.hppboolSpi1StdLib::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_PinGPIO_Pin_5|GPIO_Pin_7;gpio_cfg.GPIO_ModeGPIO_Mode_AF_PP;gpio_cfg.GPIO_SpeedGPIO_Speed_50MHz;GPIO_Init(GPIOA,gpio_cfg);// PA6 MISO 浮空输入gpio_cfg.GPIO_PinGPIO_Pin_6;gpio_cfg.GPIO_ModeGPIO_Mode_IN_FLOATING;GPIO_Init(GPIOA,gpio_cfg);SPI_I2S_DeInit(SPI1);spi_cfg.SPI_DirectionSPI_Direction_2Lines_FullDuplex;spi_cfg.SPI_ModeSPI_Mode_Master;spi_cfg.SPI_DataSizeSPI_DataSize_8b;spi_cfg.SPI_CPOLSPI_CPOL_Low;spi_cfg.SPI_CPHASPI_CPHA_1Edge;spi_cfg.SPI_NSSSPI_NSS_Soft;spi_cfg.SPI_BaudRatePrescalerSPI_BaudRatePrescaler_4;spi_cfg.SPI_FirstBitSPI_FirstBit_MSB;spi_cfg.SPI_CRCPolynomial7;SPI_Init(SPI1,spi_cfg);SPI_Cmd(SPI1,ENABLE);returntrue;}uint8_tSpi1StdLib::readWriteByte(uint8_ttx){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);returnstatic_castuint8_t(SPI_I2S_ReceiveData(SPI1));}voidSpi1StdLib::readWriteBuffer(constuint8_t*txBuf,uint8_t*rxBuf,uint16_tlen){for(uint16_ti0;ilen;i){uint8_ttxtxBuf?txBuf[i]:0xFFU;uint8_trxreadWriteByte(tx);if(rxBuf){rxBuf[i]rx;}}}5. w25q_flash.hpp W25Q设备层头文件#ifndefW25Q_FLASH_HPP#defineW25Q_FLASH_HPP#includespi_base.hpp#includegpio_out_base.hpp#includecstdintclassW25qFlashDevice{private:SpiBase*_spi;GpioOutBase*_cs;public:W25qFlashDevice(SpiBase*spi,GpioOutBase*cs_gpio);uint32_treadJedecId();voidreadData(uint32_taddr,uint8_t*buf,uint16_tlen);private:staticconstexpruint8_tCMD_JEDEC_ID0x9F;staticconstexpruint8_tCMD_READ0x03;};#endifw25q_flash.cpp#includew25q_flash.hppW25qFlashDevice::W25qFlashDevice(SpiBase*spi,GpioOutBase*cs_gpio):_spi(spi),_cs(cs_gpio){}uint32_tW25qFlashDevice::readJedecId(){_cs-setLow();_spi-readWriteByte(CMD_JEDEC_ID);uint8_tmfr_spi-readWriteByte(0xFF);uint8_tmem_spi-readWriteByte(0xFF);uint8_tcap_spi-readWriteByte(0xFF);_cs-setHigh();return(static_castuint32_t(mfr)16)|(static_castuint32_t(mem)8)|cap;}voidW25qFlashDevice::readData(uint32_taddr,uint8_t*buf,uint16_tlen){_cs-setLow();_spi-readWriteByte(CMD_READ);_spi-readWriteByte((addr16)0xFFU);_spi-readWriteByte((addr8)0xFFU);_spi-readWriteByte(addr0xFFU);_spi-readWriteBuffer(nullptr,buf,len);_cs-setHigh();}6. spi_adc_demo.hpp SPI‑ADC设备层头文件#ifndefSPI_ADC_DEMO_HPP#defineSPI_ADC_DEMO_HPP#includespi_base.hpp#includegpio_out_base.hpp#includecstdintclassSpiAdcDevice{private:SpiBase*_spi;GpioOutBase*_cs;public:SpiAdcDevice(SpiBase*spi,GpioOutBase*cs_gpio);uint16_treadChannel(uint8_tch);};#endifspi_adc_demo.cpp#includespi_adc_demo.hppSpiAdcDevice::SpiAdcDevice(SpiBase*spi,GpioOutBase*cs_gpio):_spi(spi),_cs(cs_gpio){}uint16_tSpiAdcDevice::readChannel(uint8_tch){_cs-setLow();uint8_tcmdstatic_castuint8_t(0x04|(ch1));_spi-readWriteByte(cmd);uint8_thigh_spi-readWriteByte(0xFF);uint8_tlow_spi-readWriteByte(0xFF);_cs-setHigh();uint16_tresult(static_castuint16_t(high0x0F)8)|low;returnresult;}7. main.cpp 应用层对象组装#includespi1_stdlib.hpp#includestm32_gpio_stdlib.hpp#includew25q_flash.hpp#includespi_adc_demo.hppintmain(void){// 1.初始化唯一SPI总线Spi1StdLib spi1_bus;spi1_bus.init();// 2.每个设备独立CS GPIO实例Stm32GpioOutcs_w25q(GPIOA,GPIO_Pin_4);Stm32GpioOutcs_adc(GPIOA,GPIO_Pin_3);cs_w25q.gpioInit();cs_adc.gpioInit();// 3.设备注入共享同一个SPI总线各自独立CSW25qFlashDeviceflash(spi1_bus,cs_w25q);SpiAdcDeviceadc(spi1_bus,cs_adc);uint32_tflash_idflash.readJedecId();uint16_tadc_valadc.readChannel(0);while(1){// 用户业务循环}}四、关键接口说明SpiBasevirtualboolinit()0;virtualuint8_treadWriteByte(uint8_ttx)0;virtualvoidreadWriteBuffer(constuint8_t*txBuf,uint8_t*rxBuf,uint16_tlen)0;readWriteByte(tx)全双工交换1字节只想读传入哑字节0xFF。readWriteBuffer(txBuf, rxBuf, len)批量收发txBufnullptr自动填充0xFF哑字节。GpioOutBasevirtualvoidsetLow()0;virtualvoidsetHigh()0;只描述引脚高低语义无硬件信息。W25Q readJedecId时序说明_cs-setLow();_spi-readWriteByte(0x9F);// MOSI发送JEDEC标准命令0x9Fmfr_spi-readWriteByte(0xFF);// 发哑字节产生SCK读取厂商IDmem_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保持一致。八、移植特性移植其他MCUGD32等无需改动spi_base.hpp、gpio_out_base.hpp、全部设备层h/cpp需要重写SPI驱动、GPIO输出驱动main仅修改硬件驱动实例创建部分。