STM32 SPI通信驱动开发实战指南

2026-06-13阅读 0热度 0
驱动

在嵌入式开发实战中,SPI通信凭借其灵活性和广泛支持,始终是驱动外设的核心接口。无论是操作W25Q系列Flash、读写SD卡、驱动OLED屏幕,还是对接各类传感器,一套健壮且结构清晰的SPI驱动代码能大幅压缩调试周期。本文直接提供一套基于STM32的SPI驱动实现,覆盖阻塞、中断和DMA三种数据传输模式,并附带W25Q64 Flash的读写案例。

基于STM32 SPI通信的驱动代码

一、STM32 SPI通信核心特性深度解析

SPI通信的适配性源于其四种工作模式,核心差异在于时钟极性(CPOL)与时钟相位(CPHA)的配比。数据采样时机一旦偏离设定,接收数据将完全错乱,因此理解以下参数表是首要任务。

模式 CPOL CPHA 数据采样 适用设备
模式0 0 0 第一个边沿 绝大多数SPI从设备
模式1 0 1 第二个边沿 特定外设需求
模式2 1 0 第二个边沿 特定外设需求
模式3 1 1 第一个边沿 特定外设需求

推荐的标准配置如下:
模式:Mode 0 (CPOL=0, CPHA=0)
数据位:8位
传输顺序:MSB First
时钟极性:Low
数据采样:1 Edge

该组合能兼容市场上绝大多数SPI从设备,从Flash到OLED屏均可直接驱动,无需额外调整。

二、硬件连接与引脚规划

1、SPI引脚定义

SPI1挂载于APB2总线,理论最高频率可达36MHz,足以应对日常应用场景。引脚分配如下:
PA4 → NSS (片选)
PA5 → SCK (时钟)
PA6 → MISO(主入从出)
PA7 → MOSI(主出从入)

2、设备片选控制策略

实际项目中,硬件NSS管理使用较少。原因在于,多设备共享总线时,采用GPIO软件控制CS引脚更为灵活可靠。
设备1_CS → PB0
设备2_CS → PB1
设备3_CS → PB2

一个关键细节:通常直接禁用硬件NSS,全部片选逻辑由GPIO接管。

三、CubeMX配置要点

1、SPI配置参数

在CubeMX中完成以下参数设定,即可确保通信稳定:
Mode: Full-Duplex Master
Data Size: 8 bits
First Bit: MSB First
Baud Rate: 9MHz (APB2/4)
CPOL: Low
CPHA: 1 Edge
NSS: Soft
CRC: Disable

2、DMA配置细节

DMA通道分配并不复杂,但中断优先级必须精心调整,否则易引发数据错乱:
SPI_TX: DMA1 Channel3
SPI_RX: DMA1 Channel2
Mode: Normal
Increment: Memory
Data Width: Byte

四、SPI驱动头文件设计

代码首步定义设备枚举与传输状态。这种做法能直接提升代码可读性;后续扩展新设备时,只需在枚举中新增一项,并配置对应的CS引脚与超时参数即可。

// spi_driver.h
#ifndef __SPI_DRIVER_H
#define __SPI_DRIVER_H
#include "stm32f1xx_hal.h"
// SPI外设枚举
typedef enum {
SPI_DEVICE_FLASH = 0, // W25Q64
SPI_DEVICE_SDCARD, // SD卡
SPI_DEVICE_OLED, // OLED屏幕
SPI_DEVICE_MAX
} SPI_Device_t;
// SPI传输状态
typedef enum {
SPI_STATE_READY = 0,
SPI_STATE_BUSY,
SPI_STATE_ERROR
} SPI_State_t;
// SPI传输结构体
typedef struct {
uint8_t *tx_buffer;
uint8_t *rx_buffer;
uint16_t tx_size;
uint16_t rx_size;
uint16_t tx_index;
uint16_t rx_index;
SPI_State_t state;
uint8_t dma_enabled;
} SPI_Transfer_t;
// SPI设备配置结构体
typedef struct {
GPIO_TypeDef *cs_port;
uint16_t cs_pin;
uint32_t timeout;
uint8_t dummy_byte;
} SPI_Device_Config_t;
// 函数声明
void SPI_Init(void);
void SPI_Select_Device(SPI_Device_t device);
void SPI_Deselect_Device(SPI_Device_t device);
uint8_t SPI_Transmit_Receive(uint8_t *tx_data, uint8_t *rx_data, uint16_t size);
uint8_t SPI_Transmit(uint8_t *data, uint16_t size);
uint8_t SPI_Receive(uint8_t *data, uint16_t size);
void SPI_Transmit_IT(uint8_t *data, uint16_t size);
void SPI_Receive_IT(uint8_t *data, uint16_t size);
void SPI_Transmit_Receive_DMA(uint8_t *tx_data, uint8_t *rx_data, uint16_t size);
uint8_t SPI_Is_Busy(void);
void SPI_Wait_Ready(void);
// 设备专用函数
uint8_t W25Q64_Read_ID(void);
void W25Q64_Read_Data(uint32_t addr, uint8_t *data, uint32_t size);
void W25Q64_Write_Page(uint32_t addr, uint8_t *data, uint16_t size);
void W25Q64_Erase_Sector(uint32_t addr);
#endif

五、SPI驱动实现细节

1、SPI初始化流程

初始化顺序至关重要:先开启时钟,再配置引脚,然后初始化SPI外设,最后挂接DMA并启用中断。任何环节缺失,传输都无法正常启动。

// spi_driver.c
#include "spi_driver.h"
#include
// SPI句柄
SPI_HandleTypeDef hspi1;
DMA_HandleTypeDef hdma_spi1_tx;
DMA_HandleTypeDef hdma_spi1_rx;
// 传输控制结构体
static SPI_Transfer_t spi_transfer = { 0};
// 设备配置表
static SPI_Device_Config_t spi_devices[SPI_DEVICE_MAX] = {
// W25Q64 Flash
{ GPIOB, GPIO_PIN_0, 100, 0xFF},
// SD卡
{ GPIOB, GPIO_PIN_1, 1000, 0xFF},
// OLED
{ GPIOB, GPIO_PIN_2, 10, 0x00}
};
// SPI初始化
void SPI_Init(void){
GPIO_InitTypeDef GPIO_InitStruct = { 0};
// 1. 使能时钟
__HAL_RCC_SPI1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_DMA1_CLK_ENABLE();
// 2. 配置SPI引脚
// PA5: SCK, PA6: MISO, PA7: MOSI
GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. 配置CS引脚
for (int i = 0; i < SPI_DEVICE_MAX; i ) {
GPIO_InitStruct.Pin = spi_devices[i].cs_pin;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(spi_devices[i].cs_port, &GPIO_InitStruct);
// 默认取消选中
HAL_GPIO_WritePin(spi_devices[i].cs_port,
spi_devices[i].cs_pin, GPIO_PIN_SET);
}
// 4. 初始化SPI
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_4; // 9MHz
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLED;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLED;
hspi1.Init.CRCPolynomial = 10;
HAL_SPI_Init(&hspi1);
// 5. 初始化DMA
// TX DMA (SPI1 -> 外设)
hdma_spi1_tx.Instance = DMA1_Channel3;
hdma_spi1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_spi1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_spi1_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_spi1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_spi1_tx.Init.Mode = DMA_NORMAL;
hdma_spi1_tx.Init.Priority = DMA_PRIORITY_MEDIUM;
HAL_DMA_Init(&hdma_spi1_tx);
// RX DMA (外设 -> SPI1)
hdma_spi1_rx.Instance = DMA1_Channel2;
hdma_spi1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_spi1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_spi1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_spi1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_spi1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_spi1_rx.Init.Mode = DMA_NORMAL;
hdma_spi1_rx.Init.Priority = DMA_PRIORITY_MEDIUM;
HAL_DMA_Init(&hdma_spi1_rx);
// 关联DMA到SPI
__HAL_LINKDMA(&hspi1, hdmatx, hdma_spi1_tx);
__HAL_LINKDMA(&hspi1, hdmarx, hdma_spi1_rx);
// 6. 启用DMA中断
HAL_NVIC_SetPriority(DMA1_Channel2_IRQn, 0, 0);
HAL_NVIC_SetPriority(DMA1_Channel3_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel2_IRQn);
HAL_NVIC_EnableIRQ(DMA1_Channel3_IRQn);
// 7. 启用SPI
__HAL_SPI_ENABLE(&hspi1);
spi_transfer.state = SPI_STATE_READY;
}

2、设备选择函数

片选信号拉低后插入几个NOP延时并非玄学操作,而是为从设备提供充足的准备时间,尤其在高速时钟下效果更为显著。

// 选择设备
void SPI_Select_Device(SPI_Device_t device){
if (device >= SPI_DEVICE_MAX) return;
HAL_GPIO_WritePin(spi_devices[device].cs_port,
spi_devices[device].cs_pin, GPIO_PIN_RESET);
// 短暂延时,确保设备就绪
__NOP(); __NOP(); __NOP(); __NOP();
}
// 取消选择设备
void SPI_Deselect_Device(SPI_Device_t device){
if (device >= SPI_DEVICE_MAX) return;
// 等待传输完成
SPI_Wait_Ready();
HAL_GPIO_WritePin(spi_devices[device].cs_port,
spi_devices[device].cs_pin, GPIO_PIN_SET);
// 短暂延时
__NOP(); __NOP(); __NOP(); __NOP();
}

3、阻塞式传输(基础稳定)

阻塞式传输是最原始但最可靠的模式,适用于初始化阶段或数据量极小的场景。接收数据时必须发送一个虚拟字节来驱动时钟,这是SPI全双工通信的固有逻辑。

// 阻塞式发送接收
uint8_t SPI_Transmit_Receive(uint8_t *tx_data, uint8_t *rx_data, uint16_t size){
HAL_StatusTypeDef status;
if (spi_transfer.state != SPI_STATE_READY) {
return 0;
}
spi_transfer.state = SPI_STATE_BUSY;
// 使用HAL库的阻塞传输
status = HAL_SPI_TransmitReceive(&hspi1, tx_data, rx_data, size, 1000);
spi_transfer.state = SPI_STATE_READY;
return (status == HAL_OK);
}
// 阻塞式发送
uint8_t SPI_Transmit(uint8_t *data, uint16_t size){
return SPI_Transmit_Receive(data, NULL, size);
}
// 阻塞式接收
uint8_t SPI_Receive(uint8_t *data, uint16_t size){
// 接收需发送虚拟数据
uint8_t dummy;
uint8_t *dummy_ptr = (data != NULL) ? data : &dummy;
return SPI_Transmit_Receive(NULL, dummy_ptr, size);
}

4、中断式传输(非阻塞)

中断模式适用于中等数据量场景——CPU无需死等,但每传输一个字节都会触发一次中断,对系统调度有一定负载。

// 中断发送
void SPI_Transmit_IT(uint8_t *data, uint16_t size){
if (spi_transfer.state != SPI_STATE_READY) {
return;
}
spi_transfer.state = SPI_STATE_BUSY;
spi_transfer.tx_buffer = data;
spi_transfer.tx_size = size;
spi_transfer.dma_enabled = 0;
HAL_SPI_Transmit_IT(&hspi1, data, size);
}
// 中断接收
void SPI_Receive_IT(uint8_t *data, uint16_t size){
if (spi_transfer.state != SPI_STATE_READY) {
return;
}
spi_transfer.state = SPI_STATE_BUSY;
spi_transfer.rx_buffer = data;
spi_transfer.rx_size = size;
spi_transfer.dma_enabled = 0;
HAL_SPI_Receive_IT(&hspi1, data, size);
}

5、DMA传输(高性能首选)

DMA模式是大数据量场景下的最优解。CPU只需发出启动指令,后续传输全部由DMA引擎自主完成,传输完成后通过中断通知CPU。这样CPU可以并行处理其他任务,大幅提升系统吞吐量。

// DMA发送接收
void SPI_Transmit_Receive_DMA(uint8_t *tx_data, uint8_t *rx_data, uint16_t size){
if (spi_transfer.state != SPI_STATE_READY) {
return;
}
spi_transfer.state = SPI_STATE_BUSY;
spi_transfer.tx_buffer = tx_data;
spi_transfer.rx_buffer = rx_data;
spi_transfer.tx_size = size;
spi_transfer.rx_size = size;
spi_transfer.dma_enabled = 1;
// 启动DMA传输
HAL_SPI_TransmitReceive_DMA(&hspi1, tx_data, rx_data, size);
}
// 检查SPI是否繁忙
uint8_t SPI_Is_Busy(void){
return (spi_transfer.state == SPI_STATE_BUSY);
}
// 等待SPI就绪
void SPI_Wait_Ready(void){
while (SPI_Is_Busy()) {
__NOP();
}
}

6、SPI中断处理机制

中断回调是传输流程闭环的核心。状态标志位更新、错误检测与恢复逻辑均在这里实现,确保系统在异常情况下仍能保持可靠运行。

// SPI中断回调
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi){
if (hspi->Instance == SPI1) {
spi_transfer.state = SPI_STATE_READY;
// 可在此添加传输完成回调
if (spi_transfer.dma_enabled) {
// DMA传输完成处理
} else {
// 中断传输完成处理
}
}
}
// SPI错误回调
void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi){
if (hspi->Instance == SPI1) {
spi_transfer.state = SPI_STATE_ERROR;
// 错误处理
uint32_t error = HAL_SPI_GetError(hspi);
if (error & HAL_SPI_ERROR_MODF) {
// 模式错误
}
if (error & HAL_SPI_ERROR_CRC) {
// CRC错误
}
if (error & HAL_SPI_ERROR_OVR) {
// 溢出错误
}
if (error & HAL_SPI_ERROR_FRE) {
// 帧格式错误
}
if (error & HAL_SPI_ERROR_DMA) {
// DMA错误
}
// 重置SPI
__HAL_SPI_DISABLE(hspi);
__HAL_SPI_ENABLE(hspi);
}
}

7、DMA中断处理

DMA中断向量必须与HAL库的中断处理函数对接准确,否则DMA传输完成后内核收不到信号,传输将会卡死。

// DMA中断服务函数
void DMA1_Channel2_IRQHandler(void){
HAL_DMA_IRQHandler(&hdma_spi1_rx);
}
void DMA1_Channel3_IRQHandler(void){
HAL_DMA_IRQHandler(&hdma_spi1_tx);
}
// DMA传输完成回调
void HAL_SPI_TxHalfCpltCallback(SPI_HandleTypeDef *hspi) {
// 半传输完成
}
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi){
// 接收完成
}
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi){
// 发送完成
}

六、W25Q64 Flash驱动示例

W25Q64是最典型的SPI Flash芯片之一,以其作为演示对象最具说服力。命令定义、状态寄存器读取、写使能、页编程、扇区擦除——这些操作完整覆盖了SPI Flash通信的标准流程。

// w25q64.c
#include "spi_driver.h"
#include
// W25Q64命令定义
#define W25Q64_CMD_WRITE_ENABLE 0x06
#define W25Q64_CMD_WRITE_DISABLE 0x04
#define W25Q64_CMD_READ_STATUS_REG1 0x05
#define W25Q64_CMD_READ_STATUS_REG2 0x35
#define W25Q64_CMD_WRITE_STATUS_REG 0x01
#define W25Q64_CMD_PAGE_PROGRAM 0x02
#define W25Q64_CMD_QUAD_PAGE_PROGRAM 0x32
#define W25Q64_CMD_BLOCK_ERASE 0xD8
#define W25Q64_CMD_SECTOR_ERASE 0x20
#define W25Q64_CMD_CHIP_ERASE 0xC7
#define W25Q64_CMD_ERASE_SUSPEND 0x75
#define W25Q64_CMD_ERASE_RESUME 0x7A
#define W25Q64_CMD_POWER_DOWN 0xB9
#define W25Q64_CMD_HIGH_PERFORMANCE 0xA3
#define W25Q64_CMD_READ_DATA 0x03
#define W25Q64_CMD_FAST_READ 0x0B
#define W25Q64_CMD_READ_JEDEC_ID 0x9F
#define W25Q64_CMD_RELEASE_POWER_DOWN 0xAB
// 状态寄存器位
#define W25Q64_STATUS_BUSY 0x01
#define W25Q64_STATUS_WRITE_ENABLE 0x02
// 读取设备ID
uint8_t W25Q64_Read_ID(void){
uint8_t tx_buffer[4] = { 0};
uint8_t rx_buffer[4] = { 0};
tx_buffer[0] = W25Q64_CMD_READ_JEDEC_ID;
SPI_Select_Device(SPI_DEVICE_FLASH);
SPI_Transmit_Receive(tx_buffer, rx_buffer, 4);
SPI_Deselect_Device(SPI_DEVICE_FLASH);
// rx_buffer[1] = Manufacturer ID
// rx_buffer[2] = Memory Type
// rx_buffer[3] = Capacity
return rx_buffer[3];
}
// 读取状态寄存器
static uint8_t W25Q64_Read_Status_Reg1(void){
uint8_t tx_buffer[2] = { W25Q64_CMD_READ_STATUS_REG1, 0x00};
uint8_t rx_buffer[2] = { 0};
SPI_Select_Device(SPI_DEVICE_FLASH);
SPI_Transmit_Receive(tx_buffer, rx_buffer, 2);
SPI_Deselect_Device(SPI_DEVICE_FLASH);
return rx_buffer[1];
}
// 等待Flash就绪
static void W25Q64_Wait_Busy(void){
while (W25Q64_Read_Status_Reg1() & W25Q64_STATUS_BUSY) {
// 等待
}
}
// 写使能
static void W25Q64_Write_Enable(void){
uint8_t cmd = W25Q64_CMD_WRITE_ENABLE;
SPI_Select_Device(SPI_DEVICE_FLASH);
SPI_Transmit(&cmd, 1);
SPI_Deselect_Device(SPI_DEVICE_FLASH);
}
// 读取数据
void W25Q64_Read_Data(uint32_t addr, uint8_t *data, uint32_t size){
uint8_t tx_buffer[5];
// 构建读取命令
tx_buffer[0] = W25Q64_CMD_READ_DATA;
tx_buffer[1] = (addr >> 16) & 0xFF; // 地址高字节
tx_buffer[2] = (addr >> 8) & 0xFF; // 地址中字节
tx_buffer[3] = addr & 0xFF; // 地址低字节
SPI_Select_Device(SPI_DEVICE_FLASH);
// 发送命令和地址
SPI_Transmit(tx_buffer, 4);
// 接收数据
if (data != NULL) {
SPI_Receive(data, size);
} else {
// 如果data为NULL,只接收不存储
uint8_t dummy;
for (uint32_t i = 0; i < size; i ) {
SPI_Receive(&dummy, 1);
}
}
SPI_Deselect_Device(SPI_DEVICE_FLASH);
}
// 写入一页数据(最大256字节)
void W25Q64_Write_Page(uint32_t addr, uint8_t *data, uint16_t size){
uint8_t tx_buffer[4];
if (size > 256) size = 256; // 限制页大小
// 等待Flash就绪
W25Q64_Wait_Busy();
// 写使能
W25Q64_Write_Enable();
// 构建页编程命令
tx_buffer[0] = W25Q64_CMD_PAGE_PROGRAM;
tx_buffer[1] = (addr >> 16) & 0xFF;
tx_buffer[2] = (addr >> 8) & 0xFF;
tx_buffer[3] = addr & 0xFF;
SPI_Select_Device(SPI_DEVICE_FLASH);
// 发送命令和地址
SPI_Transmit(tx_buffer, 4);
// 发送数据
SPI_Transmit(data, size);
SPI_Deselect_Device(SPI_DEVICE_FLASH);
// 等待写入完成
W25Q64_Wait_Busy();
}
// 擦除扇区(4KB)
void W25Q64_Erase_Sector(uint32_t addr){
uint8_t tx_buffer[4];
// 等待Flash就绪
W25Q64_Wait_Busy();
// 写使能
W25Q64_Write_Enable();
// 构建扇区擦除命令
tx_buffer[0] = W25Q64_CMD_SECTOR_ERASE;
tx_buffer[1] = (addr >> 16) & 0xFF;
tx_buffer[2] = (addr >> 8) & 0xFF;
tx_buffer[3] = addr & 0xFF;
SPI_Select_Device(SPI_DEVICE_FLASH);
SPI_Transmit(tx_buffer, 4);
SPI_Deselect_Device(SPI_DEVICE_FLASH);
// 等待擦除完成
W25Q64_Wait_Busy();
}

七、主程序示例

主程序的测试流程具备典型性:读取Flash ID、擦除扇区、写入一页数据、读出对比验证,最后用DMA再执行一次传输流程。这套逻辑可直接移植到实际项目中。

// main.c
#include "main.h"
#include "spi_driver.h"
#include
int main(void){
HAL_Init();
SystemClock_Config();
// 初始化SPI
SPI_Init();
// 1. 测试Flash ID
uint8_t flash_id = W25Q64_Read_ID();
printf("Flash ID: 0xXrn", flash_id);
// 2. 测试数据读写
uint8_t write_buffer[256];
uint8_t read_buffer[256];
// 填充测试数据
for (int i = 0; i < 256; i ) {
write_buffer[i] = i;
}
// 擦除扇区
printf("Erasing sector...rn");
W25Q64_Erase_Sector(0x000000);
// 写入数据
printf("Writing data...rn");
W25Q64_Write_Page(0x000000, write_buffer, 256);
// 读取数据
printf("Reading data...rn");
W25Q64_Read_Data(0x000000, read_buffer, 256);
// 验证数据
if (memcmp(write_buffer, read_buffer, 256) == 0) {
printf("SPI Flash test PASS!rn");
} else {
printf("SPI Flash test FAIL!rn");
}
// 3. 测试DMA传输
uint8_t dma_tx_buffer[1024];
uint8_t dma_rx_buffer[1024];
for (int i = 0; i < 1024; i ) {
dma_tx_buffer[i] = 0xAA;
}
// 使用DMA传输
printf("Starting DMA transfer...rn");
SPI_Select_Device(SPI_DEVICE_FLASH);
SPI_Transmit_Receive_DMA(dma_tx_buffer, dma_rx_buffer, 1024);
// 等待DMA传输完成
while (SPI_Is_Busy()) {
// 可在此处理其他任务
}
SPI_Deselect_Device(SPI_DEVICE_FLASH);
printf("DMA transfer complete!rn");
while (1) {
// 主循环
HAL_Delay(1000);
}
}

八、性能优化策略

1. 提升SPI时钟

时钟频率上限取决于从设备规格与PCB走线质量。在72MHz系统时钟下,APB2=72MHz,分频设置对应的实际频率一目了然:
// SPI_BAUDRATEPRESCALER_2 = 36MHz
// SPI_BAUDRATEPRESCALER_4 = 18MHz
// SPI_BAUDRATEPRESCALER_8 = 9MHz

2. 启用快速读取

W25Q64的快速读取命令相较普通读取多了一个虚拟字节,但能支持更高的时钟频率,特别适合大数据量转存场景:
// W25Q64支持快速读取
void W25Q64_Fast_Read(uint32_t addr, uint8_t *data, uint32_t size){
uint8_t tx_buffer[5] = { W25Q64_CMD_FAST_READ,
(addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF, 0xFF}; // 虚拟字节
SPI_Select_Device(SPI_DEVICE_FLASH);
SPI_Transmit_Receive(tx_buffer, data, size 5);
SPI_Deselect_Device(SPI_DEVICE_FLASH);
}

3. 批量传输优化

写入大文件时,务必避免跨页边界,否则数据会回卷覆盖。批量写入时按页分割,并在每个扇区擦除后适当延时,防止芯片过热:
// 批量写入
void W25Q64_Write_Multi_Page(uint32_t addr, uint8_t *data, uint32_t size){
uint32_t offset = 0;
while (size > 0) {
uint16_t write_size = (size > 256) ? 256 : size;
W25Q64_Write_Page(addr offset, data offset, write_size);
offset = write_size;
size -= write_size;
// 延时避免Flash过热
if (offset % 4096 == 0) {
HAL_Delay(1);
}
}
}

九、调试技巧

1、逻辑分析仪调试

逻辑分析仪抓取波形是最直接的调试手段。若手头设备不足,可利用一个GPIO作为调试信号输出,在关键操作前后拉高拉低,借助示波器或逻辑分析仪观察时序:
// 添加调试GPIO
#define DEBUG_PIN_SET() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET)
#define DEBUG_PIN_CLR() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET)
// 在关键位置添加
DEBUG_PIN_SET();
SPI_Transmit(data, size);
DEBUG_PIN_CLR();

2、错误检测

SPI通信过程中可能遭遇多种错误。定期检查错误码并执行复位恢复,是提升系统鲁棒性的有效手段:
// 检查SPI错误
void SPI_Check_Error(void){
if (hspi1.ErrorCode != HAL_SPI_ERROR_NONE) {
printf("SPI Error: 0xlXrn", hspi1.ErrorCode);
// 重置SPI
__HAL_SPI_DISABLE(&hspi1);
HAL_SPI_DeInit(&hspi1);
HAL_SPI_Init(&hspi1);
}
}

十、常见问题排查

最后整理一份常见问题的快速排查表,实际开发时可直接对照检查,有效减少弯路:

问题 原因 解决
无响应 CS引脚电平或时序异常 检查CS引脚电平和时序
数据错误 时钟相位不匹配 调整CPOL/CPHA
DMA卡死 DMA未正确初始化 检查DMA配置和中断
速度慢 SPI时钟过低 提高时钟频率
干扰大 未加滤波电容 靠近芯片加0.1uF电容
免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

相关阅读

更多
欢迎回来 登录或注册后,可保存提示词和历史记录
登录后可同步收藏、历史记录和常用模板
注册即表示同意服务条款与隐私政策