1. 基于STM32的软件SPI实现与W25Q128闪存驱动开发
在嵌入式系统开发中,SPI(Serial Peripheral Interface)是一种常用的同步串行通信接口。当硬件SPI资源不足或需要灵活配置时,软件模拟SPI就成为了一个实用的解决方案。本文将详细介绍如何在STM32平台上实现软件SPI,并基于此开发W25Q128闪存芯片的完整驱动。
2. 软件SPI的核心设计与实现
2.1 SPI通信基础与模式选择
SPI通信有四种工作模式,由CPOL(时钟极性)和CPHA(时钟相位)两个参数决定:
- 模式0:CPOL=0,CPHA=0 - 时钟空闲为低电平,数据在第一个边沿(上升沿)采样
- 模式1:CPOL=0,CPHA=1 - 时钟空闲为低电平,数据在第二个边沿(下降沿)采样
- 模式2:CPOL=1,CPHA=0 - 时钟空闲为高电平,数据在第一个边沿(下降沿)采样
- 模式3:CPOL=1,CPHA=1 - 时钟空闲为高电平,数据在第二个边沿(上升沿)采样
在我们的实现中,通过SoftSPI_HandleTypeDef结构体的mode字段来配置这四种模式。
2.2 软件SPI的硬件抽象层设计
我们设计了完整的软件SPI抽象层,核心结构体如下:
c复制typedef struct SoftSPI_Handle {
GPIO_TypeDef *SCK_Port; // 时钟线端口
uint16_t SCK_Pin; // 时钟线引脚
GPIO_TypeDef *MOSI_Port; // 主机输出从机输入端口
uint16_t MOSI_Pin; // MOSI引脚
GPIO_TypeDef *MISO_Port; // 主机输入从机输出端口
uint16_t MISO_Pin; // MISO引脚
GPIO_TypeDef *NSS_Port; // 片选端口
uint16_t NSS_Pin; // 片选引脚
SPI_Mode mode; // SPI模式
SPI_FirstBit firstBit; // 数据传输顺序(MSB/LSB)
SPI_BaudRatePrescaler baudRatePrescaler; // 波特率分频
uint8_t useHardwareDelay; // 是否使用硬件定时器延时
// 函数指针
void (*Init)(struct SoftSPI_Handle *hspi);
void (*TransferBytes)(struct SoftSPI_Handle *hspi, uint8_t *txData, uint8_t *rxData, uint16_t size);
// 其他函数指针...
} SoftSPI_HandleTypeDef;
这种设计提供了高度灵活性,允许开发者:
- 自由配置任意GPIO作为SPI引脚
- 运行时动态修改SPI模式、波特率和数据顺序
- 选择使用软件延时或硬件定时器实现精确时序
2.3 关键时序实现细节
以模式0(CPOL=0, CPHA=0)为例,我们来看字节传输的实现:
c复制static void SoftSPI_Transfer_Mode0(SoftSPI_HandleTypeDef *hspi, uint8_t txData,
uint8_t *rxData, uint32_t half_delay) {
uint8_t rx = 0;
uint8_t tx = txData;
for(int i=0; i<8; i++) {
// 设置MOSI(在时钟边沿前稳定数据)
if(hspi->firstBit == SPI_FIRSTBIT_MSB) {
HAL_GPIO_WritePin(hspi->MOSI_Port, hspi->MOSI_Pin,
(tx & 0x80) ? GPIO_PIN_SET : GPIO_PIN_RESET);
tx <<= 1;
} else {
HAL_GPIO_WritePin(hspi->MOSI_Port, hspi->MOSI_Pin,
(tx & 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET);
tx >>= 1;
}
SoftSPI_Delay(hspi, half_delay);
// 上升沿采样数据
HAL_GPIO_WritePin(hspi->SCK_Port, hspi->SCK_Pin, GPIO_PIN_SET);
// 读取MISO
if(hspi->firstBit == SPI_FIRSTBIT_MSB) {
rx <<= 1;
if(HAL_GPIO_ReadPin(hspi->MISO_Port, hspi->MISO_Pin))
rx |= 0x01;
} else {
rx >>= 1;
if(HAL_GPIO_ReadPin(hspi->MISO_Port, hspi->MISO_Pin))
rx |= 0x80;
}
SoftSPI_Delay(hspi, half_delay);
// 下降沿准备数据变化
HAL_GPIO_WritePin(hspi->SCK_Port, hspi->SCK_Pin, GPIO_PIN_RESET);
}
*rxData = rx;
}
关键点:模式0中,数据在时钟上升沿被采样,在下降沿可以变化。每个时钟周期需要保持足够的稳定时间,这通过half_delay参数控制。
2.4 波特率控制与延时实现
软件SPI的波特率通过控制时钟信号的高低电平持续时间来实现。我们提供了多种预分频选项:
c复制typedef enum {
SPI_BAUDRATE_DIV2 = 0, // ~42MHz (最大理论值)
SPI_BAUDRATE_DIV4, // ~21MHz
SPI_BAUDRATE_DIV8, // ~10.5MHz
// ...其他分频
} SPI_BaudRatePrescaler;
延时可以通过简单的NOP循环实现,也可以使用硬件定时器获得更精确的控制:
c复制static void default_delay_ns(uint32_t ns) {
// 假设系统时钟为180MHz,每个周期约5.56ns
uint32_t cycles = (uint32_t)(ns / 5.56f);
for(volatile uint32_t i=0; i<cycles; i++) {
__NOP();
}
}
对于高精度应用,可以配置硬件定时器延时函数:
c复制void SoftSPI_SetDelayFunction(SoftSPI_HandleTypeDef *hspi,
void (*delay_func)(uint32_t ns)) {
hspi->delay_ns = delay_func;
hspi->useHardwareDelay = 1;
}
3. W25Q128闪存驱动实现
3.1 W25Q128芯片概述
W25Q128是Winbond公司推出的128M-bit串行Flash存储器,主要特性包括:
- 支持标准SPI和双线SPI接口
- 256字节可编程页
- 4KB扇区、32KB块和64KB块擦除
- 最大时钟频率104MHz
- 超过10万次擦写周期
3.2 驱动架构设计
我们设计了面向对象的驱动接口:
c复制typedef struct W25QXX_Handle {
SoftSPI_HandleTypeDef *hspi; // 软件SPI句柄
int8_t (*Init)(struct W25QXX_Handle *, SoftSPI_HandleTypeDef *);
void (*WriteData)(struct W25QXX_Handle *, uint32_t, uint8_t *, uint16_t);
void (*ReadData)(struct W25QXX_Handle *, uint32_t, uint8_t *, uint32_t);
void (*ReadID)(struct W25QXX_Handle *);
} W25QXX_T;
这种设计将SPI通信细节封装在驱动内部,对外提供简洁的存储操作接口。
3.3 关键操作实现
3.3.1 页编程操作
W25Q128的页编程操作需要遵循特定流程:
c复制void W25QXX_WritePageData(W25QXX_T *pW25QXX, uint32_t addr,
uint8_t *p, uint32_t len) {
uint8_t lTxd, lRxd;
W25QXX_WriteEnable(pW25QXX); // 使能写操作
pW25QXX->hspi->ChipSelect(pW25QXX->hspi, 0);
pW25QXX->hspi->delay_us(20);
lTxd = 0x02; // 页写命令
pW25QXX->hspi->TransferBytes(pW25QXX->hspi, &lTxd, &lRxd, 1);
// 发送24位地址
lTxd = (addr >> 16);
pW25QXX->hspi->TransferBytes(pW25QXX->hspi, &lTxd, &lRxd, 1);
lTxd = (addr >> 8);
pW25QXX->hspi->TransferBytes(pW25QXX->hspi, &lTxd, &lRxd, 1);
lTxd = addr;
pW25QXX->hspi->TransferBytes(pW25QXX->hspi, &lTxd, &lRxd, 1);
// 写入数据
pW25QXX->hspi->WriteByte(pW25QXX->hspi, p, len);
pW25QXX->hspi->ChipSelect(pW25QXX->hspi, 1);
pW25QXX->hspi->delay_us(20);
W25QXX_BusyStateWait(pW25QXX); // 等待写操作完成
}
注意:W25Q128的页编程操作最多只能写入256字节,且不能跨页写入。如果写入数据跨越页边界,超出部分会从页开头写入。
3.3.2 扇区擦除实现
Flash存储器必须先擦除后才能写入,擦除操作以扇区(4KB)为单位:
c复制void W25QXX_SectorErase(W25QXX_T *pW25QXX, uint32_t addr, uint8_t cmd) {
uint8_t lTxd, lRxd;
W25QXX_WriteEnable(pW25QXX);
pW25QXX->hspi->ChipSelect(pW25QXX->hspi, 0);
pW25QXX->hspi->delay_us(10);
lTxd = cmd; // 0x20 for sector erase
pW25QXX->hspi->TransferBytes(pW25QXX->hspi, &lTxd, &lRxd, 1);
// 发送24位地址
lTxd = (addr >> 16);
pW25QXX->hspi->TransferBytes(pW25QXX->hspi, &lTxd, &lRxd, 1);
lTxd = (addr >> 8);
pW25QXX->hspi->TransferBytes(pW25QXX->hspi, &lTxd, &lRxd, 1);
lTxd = addr;
pW25QXX->hspi->TransferBytes(pW25QXX->hspi, &lTxd, &lRxd, 1);
pW25QXX->hspi->ChipSelect(pW25QXX->hspi, 1);
pW25QXX->hspi->delay_us(10);
W25QXX_BusyStateWait(pW25QXX); // 等待擦除完成
}
3.3.3 安全写入实现
考虑到Flash存储器的特性,我们实现了安全写入函数,它会自动处理扇区擦除和跨扇区写入:
c复制void W25QXX_WriteData(W25QXX_T *pW25QXX, uint32_t WriteAddr,
uint8_t *pBuffer, uint16_t NumToWrite) {
uint32_t secpos = (WriteAddr - W25Q_FLASH_BASE) / SECTOR_SIZE;
uint16_t secoff = (WriteAddr - W25Q_FLASH_BASE) % SECTOR_SIZE;
uint16_t secremain = SECTOR_SIZE - secoff;
if(NumToWrite <= secremain) secremain = NumToWrite;
while(1) {
// 读取整个扇区内容
W25QXX_ReadData(pW25QXX, secpos*SECTOR_SIZE+W25Q_FLASH_BASE,
W25QXX_Buff, SECTOR_SIZE);
// 检查是否需要擦除
uint8_t need_erase = 0;
for(int i=0; i<secremain; i++) {
if(W25QXX_Buff[secoff+i] != 0XFF) {
need_erase = 1;
break;
}
}
if(need_erase) {
W25QXX_SectorErase(pW25QXX, secpos*SECTOR_SIZE+W25Q_FLASH_BASE, 0x20);
// 更新缓冲区数据
for(int i=0; i<secremain; i++) {
W25QXX_Buff[i+secoff] = pBuffer[i];
}
// 写入整个扇区
W25QXX_WriteDataNoCheck(pW25QXX, secpos*SECTOR_SIZE+W25Q_FLASH_BASE,
W25QXX_Buff, SECTOR_SIZE);
} else {
W25QXX_WriteDataNoCheck(pW25QXX, WriteAddr, pBuffer, secremain);
}
if(NumToWrite == secremain) break;
// 处理剩余数据
secpos++;
secoff = 0;
pBuffer += secremain;
WriteAddr += secremain;
NumToWrite -= secremain;
secremain = (NumToWrite > SECTOR_SIZE) ? SECTOR_SIZE : NumToWrite;
}
}
4. 实际应用与性能优化
4.1 初始化配置示例
以下是典型的初始化配置代码:
c复制// 定义软件SPI句柄
SoftSPI_HandleTypeDef hspi1 = {
.SCK_Port = GPIOF, .SCK_Pin = GPIO_PIN_7,
.MOSI_Port = GPIOF, .MOSI_Pin = GPIO_PIN_9,
.MISO_Port = GPIOF, .MISO_Pin = GPIO_PIN_8,
.NSS_Port = GPIOF, .NSS_Pin = GPIO_PIN_6,
.mode = SPI_MODE0,
.firstBit = SPI_FIRSTBIT_MSB,
.baudRatePrescaler = SPI_BAUDRATE_DIV256,
.useHardwareDelay = 0,
.ChipSelect = SoftSPI_ChipSelect,
.Init = SoftSPI_Init,
.TransferBytes = SoftSPI_TransferBytes
};
// 定义W25Q128驱动句柄
W25QXX_T gDrv_W25Q256 = {
.Init = W25Qxx_Init,
.WriteData = W25QXX_WriteData,
.ReadData = W25QXX_ReadData
};
// 初始化
void Storage_Init(void) {
SoftSPI_Init(&hspi1);
W25Qxx_Init(&gDrv_W25Q256, &hspi1);
}
4.2 性能优化技巧
-
使用硬件定时器延时:软件循环延时的精度受中断影响,使用硬件定时器可以获得更稳定的时序。
-
合理选择波特率:虽然软件SPI可以实现较高波特率,但实际性能受CPU速度和任务调度影响。建议通过实测确定稳定工作的最高波特率。
-
批量传输优化:对于连续读写操作,尽量使用批量传输函数减少函数调用开销。
-
缓存管理:对于频繁读写的小数据,可以在RAM中建立缓存,减少对Flash的实际操作次数。
4.3 常见问题排查
-
通信失败:
- 检查GPIO配置是否正确,特别是输入/输出方向
- 确认SPI模式(CPOL/CPHA)与从设备匹配
- 检查片选信号是否正常控制
-
写入数据异常:
- 确保在写入前已擦除相应扇区
- 检查是否遵守页编程限制(不跨页写入)
- 确认写入操作后等待足够的时间(检查BUSY状态)
-
性能低下:
- 尝试提高SPI时钟频率
- 检查是否有不必要的延时
- 考虑使用DMA加速数据传输(如果是硬件SPI)
5. 扩展应用与进阶开发
基于这个软件SPI实现,我们可以进一步开发:
-
支持双线/四线SPI模式:通过配置多个IO并行传输提高速度
-
实现SPI从机模式:通过GPIO中断检测时钟边沿,实现STM32作为SPI从机
-
添加DMA支持:对于硬件SPI,可以结合DMA实现高效数据传输
-
开发文件系统接口:在驱动层之上实现FATFS或LittleFS等文件系统
在实际项目中,我发现软件SPI虽然灵活性高,但在高速或实时性要求高的场景下,还是应该优先使用硬件SPI。软件SPI更适合低速设备或硬件资源受限的情况。对于W25Q128这类Flash设备,软件SPI通常能够满足需求,特别是在开发初期硬件资源分配尚未最终确定时,软件SPI提供了快速验证方案的途径。
