SPI - 读写串行FLASH
更新于 2026年8月13日
本篇总结
使用 SPI + DMA,读写串行 FLASH。
本示例演示 W55MH32L 的 SPI1 外设配合 DMA 实现对串行 FLASH 的高速数据写入。SPI 以主模式、模式 3(CPOL=1/CPHA=1)运行,片选信号 PA4 由软件控制;DMA1 通道 3 负责将内存中 256 字节数据自动搬运到 SPI1 数据寄存器,全程无需 CPU 逐字节干预,大幅提升传输效率。
SPI 简介
SPI(Serial Peripheral Interface)是由摩托罗拉公司提出的串行外围设备接口,是一种高速全双工的通信总线,广泛用于 ADC、LCD、FLASH 等设备与 MCU 之间要求通讯速率较高的场合。
SPI 通讯使用 3 条总线及 1 条片选线:SCK(时钟,由主机产生)、MOSI(主机输出/从机输入)、MISO(主机输入/从机输出)、NSS/CS(片选,低电平有效选中从机)。SPI 一共有四种通讯模式,由时钟极性 CPOL 和时钟相位 CPHA 决定,主机与从机需工作在相同模式下。本例程采用最常用的「模式 3」:CPOL=1(空闲高电平)、CPHA=1(偶数边沿采样)。
前置工具准备
开始本例程前,请确认以下工具已安装:
- WIZ UartTool V1.0:串口日志观察工具:点击下载
- 逻辑分析仪(可选):抓取 SCK/MOSI/CS 波形,验证 SPI 时序
硬件连接
W55MH32L 的 SPI1 引脚(PA4/PA5/PA6/PA7)连接到板载串行 FLASH 芯片。仅需一根 Micro USB 线连接开发板 DEBUG USB 口到 PC 即可观察串口调试输出。
| 引脚 | 功能 | 方向 | 模式 |
|---|---|---|---|
| PA4 | SPI1_NSS (CS) | MCU → FLASH | 推挽输出(软件控制) |
| PA5 | SPI1_SCK | MCU → FLASH | 复用推挽输出 (AF_PP) |
| PA6 | SPI1_MISO | FLASH → MCU | 复用推挽输出 (AF_PP) |
| PA7 | SPI1_MOSI | MCU → FLASH | 复用推挽输出 (AF_PP) |
片选控制说明
实际应用中一般不使用 SPI 硬件 NSS 信号线,而是用普通 GPIO 软件控制 PA4 电平:拉低选中 FLASH、拉高释放。这样更灵活,也便于扩展多个从设备。
工程结构
工程位于 SDK 内 1.SDK\ModuleDemo\SPI\SPI_FLASH\,核心文件如下:
SPI_FLASH/
├── USER/
│ ├── main.c ← SPI初始化 + DMA配置 + 数据搬运主循环 + printf重定向
│ ├── w55mh32_conf.h ← 外设头文件集中包含
│ ├── w55mh32_it.c/h ← 中断服务函数
│ ├── system_w55mh32.c/h← 系统初始化(时钟配置)
│ └── startup_w55mh32.s ← 启动文件
└── SYSTEM/
└── delay/
└── delay.c/h ← SysTick 延时函数
步骤1:SPI 初始化
文件:USER/main.c | 函数:SPI_Configuration()
void SPI_Configuration(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
/* 开启 SPI1 和 GPIOA 时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1 | RCC_APB2Periph_GPIOA, ENABLE);
/* PA4: 片选 CS,推挽输出,初始拉高(非选中状态) */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_SetBits(GPIOA, GPIO_Pin_4);
/* PA5/SCK, PA6/MISO, PA7/MOSI: 复用推挽输出 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/* SPI1 参数:主模式 / 模式3 (CPOL=High, CPHA=2Edge) / 8位帧 / MSB先行 */
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; /* 空闲高电平 */
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; /* 偶数边沿采样 */
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; /* 软件控制片选 */
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; /* 低速,便于调试 */
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
}五步完成:开时钟 → PA4 配置为推挽输出作 CS(初始拉高)→ PA5/PA6/PA7 配置为复用推挽 → SPI 参数(主模式、模式 3、8 位、MSB、软件 NSS)→ 使能 SPI1。
如果你想自定义
- 改通讯速率 → 修改
SPI_BaudRatePrescaler,如SPI_BaudRatePrescaler_4(更高速)。注意从设备支持的最高时钟频率 - 换用 SPI2 → 同步修改 GPIO 引脚(PB12-PB15)和时钟使能宏
RCC_APB1Periph_SPI2,并查阅数据手册确认引脚映射 - 改为模式 0 → 将
SPI_CPOL_High改为SPI_CPOL_Low、SPI_CPHA_2Edge改为SPI_CPHA_1Edge
步骤2:DMA 配置
函数:DMA_Configuration() — 配置 DMA1 通道 3(SPI1 发送)将内存缓冲区 256 字节自动搬运到 SPI1 数据寄存器;通道 2(接收)一并配置但本例程未启用。
void DMA_Configuration(void)
{
DMA_InitTypeDef DMA_InitStructure;
/* 开启 DMA1 时钟 */
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
/* DMA1 通道 3:SPI1 发送(内存 → 外设) */
DMA_DeInit(DMA1_Channel3);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR; /* 外设地址:SPI 数据寄存器 */
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)SPI_TX_BUFF; /* 内存地址:发送缓冲区 */
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; /* 内存 → 外设(发送) */
DMA_InitStructure.DMA_BufferSize = SPI_BUFF_SIZE; /* 传输大小:256 字节 */
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; /* 外设地址不变 */
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; /* 内存地址递增 */
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; /* 普通模式(非循环) */
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel3, &DMA_InitStructure);
SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE); /* 使能 SPI 发送 DMA 请求 */
/* DMA1 通道 2:SPI1 接收(外设 → 内存),本例程未启用 */
DMA_DeInit(DMA1_Channel2);
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)SPI_RX_BUFF;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; /* 外设 → 内存(接收) */
DMA_Init(DMA1_Channel2, &DMA_InitStructure);
SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Rx, ENABLE);
}
关键两点:DMA_DIR_PeripheralDST 表示方向为"内存→外设"(发送);外设地址固定为 &SPI1->DR,内存地址递增遍历整个缓冲区。每轮传输完成后需软件重新配置 DMA_BufferSize 才能再次启动。
如果你想自定义
- 改缓冲区大小 → 修改
SPI_BUFF_SIZE宏定义,然后同步调整DMA_BufferSize - 启用接收通道 → 在
main()中拉低 CS 后调用DMA_Cmd(DMA1_Channel2, ENABLE)并等待DMA1_FLAG_TC2
步骤3:主函数逻辑
文件:USER/main.c | 函数:main() — 初始化外设 → 填充发送缓冲区 → 片选有效 → 启动 DMA 发送 → 等待完成 → 片选释放。
#define SPI_BUFF_SIZE 256
uint8_t SPI_TX_BUFF[SPI_BUFF_SIZE]; /* 发送缓冲区 */
uint8_t SPI_RX_BUFF[SPI_BUFF_SIZE]; /* 接收缓冲区(本例程未使用) */
int main(void)
{
uint32_t i;
RCC_ClocksTypeDef clocks;
delay_init();
UART_Configuration(115200);
SPI_Configuration();
DMA_Configuration();
/* 打印系统时钟信息 */
RCC_GetClocksFreq(&clocks);
printf("\n");
printf("SYSCLK: %3.1fMhz, HCLK: %3.1fMhz, PCLK1: %3.1fMhz, "
"PCLK2: %3.1fMhz, ADCCLK: %3.1fMhz\n",
(float)clocks.SYSCLK_Frequency / 1000000,
(float)clocks.HCLK_Frequency / 1000000,
(float)clocks.PCLK1_Frequency / 1000000,
(float)clocks.PCLK2_Frequency / 1000000,
(float)clocks.ADCCLK_Frequency / 1000000);
printf("SPI FLASH DMA Test.\n");
/* 填充发送缓冲区:0x01, 0x02, ..., 0xFF, 0x00 */
for (i = 0; i < SPI_BUFF_SIZE; i++)
{
SPI_TX_BUFF[i] = i + 1;
}
/* ---- 启动 SPI DMA 发送 ---- */
GPIO_ResetBits(GPIOA, GPIO_Pin_4); /* 拉低片选 CS,选中 FLASH */
DMA_Cmd(DMA1_Channel3, ENABLE); /* 启用 DMA 发送通道 */
while (DMA_GetFlagStatus(DMA1_FLAG_TC3) == RESET); /* 等待传输完成 */
DMA_ClearFlag(DMA1_FLAG_TC3); /* 清除传输完成标志 */
DMA_Cmd(DMA1_Channel3, DISABLE); /* 禁用 DMA 通道 */
GPIO_SetBits(GPIOA, GPIO_Pin_4); /* 拉高片选 CS,释放 FLASH */
printf("SPI DMA Send Complete, %d bytes sent.\n", SPI_BUFF_SIZE);
while (1);
}
发送流程严格遵循 SPI 通讯时序:先拉低 CS 选中从机 → 启动 DMA 让其自动搬运 256 字节 → 轮询 DMA1_FLAG_TC3 等待传输完成 → 清标志、关 DMA → 拉高 CS 结束通讯。
如果你想自定义
- 避免永久阻塞 → 在
while(DMA_GetFlagStatus(...))循环中加超时计数器,超时则跳过本次发送 - 改为中断通知而非轮询 → 启用 DMA TC 中断(
DMA_ITConfig(DMA1_Channel3, DMA_IT_TC, ENABLE)),在w55mh32_it.c的 DMA1_Channel3_IRQHandler 中处理完成回调 - 循环发送 → 在
while(1)中重复「拉低 CS → 重配 DMA_BufferSize → 启动 → 等待 → 拉高 CS」,注意每轮需重新设置DMA_BufferSize
步骤4:printf 重定向
重写 fputc() 将 printf 输出重定向到 USART1,并自动在 \n 前补 \r 确保串口工具正确换行。
int fputc(int c, FILE *f)
{
if (c == '\n')
SER_PutChar('\r'); // 补回车,确保串口工具正确换行
return (SER_PutChar(c));
}
int SER_PutChar(int ch)
{
while (!USART_GetFlagStatus(USART_TEST, USART_FLAG_TC));
USART_SendData(USART_TEST, (uint8_t)ch);
return ch;
}编译与下载
Keil MDK 中打开 SPI_FLASH.uvprojx,F7 编译,确认 0 Error(s)。连接 DEBUG USB 口到 PC,F8 下载。下载成功后按 RESET 键启动新固件。
运行验证
打开 WIZ UartTool V1.0 ,选择开发板对应 COM 口,波特率 115200 / 8N1,打开串口。按 RESET 启动后,预期串口打印系统时钟信息及发送完成提示:
SYSCLK: 72.0Mhz, HCLK: 72.0Mhz, PCLK1: 36.0Mhz, PCLK2: 72.0Mhz, ADCCLK: 36.0Mhz
SPI FLASH DMA Test.
SPI DMA Send Complete, 256 bytes sent.
若需进一步验证 SPI 时序,可用逻辑分析仪抓取 PA4(CS)/PA5(SCK)/PA7(MOSI) 波形:CS 拉低期间 SCK 应持续输出 256×8=2048 个时钟周期,MOSI 上可观察到 0x01~0xFF 的逐位数据流。
常见问题
Q: 编译报错 "cannot open source file w55mh32.h"
检查 Project → Options → C/C++ → Include Paths,确保 SDK 的 Libraries 目录已添加到头文件搜索路径。
Q: 程序卡在 while(DMA_GetFlagStatus(DMA1_FLAG_TC3)) 等待处不动
确认 SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE) 已调用;检查 DMA 通道映射是否正确(SPI1_TX 固定对应 DMA1_Channel3);若 CS 未拉低从机不会回数据,但本例程仅发送不受影响。
Q: SPI 通讯速率太慢/太快导致从机无法响应
调整 SPI_BaudRatePrescaler。分频系数越大速率越慢,本例程默认 256 分频便于调试;正式应用可改 4 或 2 分频提速,但需确保不超过 FLASH 数据手册标称的最大 SCK 频率(通常 33~104MHz)。
Q: 从机读取的数据全部是 0xFF 或 0x00
优先检查 SPI 模式是否匹配(CPOL/CPHA 组合),主机从机必须一致;其次确认 CS 在整个传输期间保持低电平;最后用示波器/逻辑分析仪抓 SCK 与 MOSI 看数据是否真正发出。
Q: 想接收 FLASH 返回的数据怎么办
在 main() 中拉低 CS 后,同时启用 DMA1_Channel2(SPI1_RX)并等待 DMA1_FLAG_TC2。由于 SPI 全双工特性,发送 dummy 字节(如 0xFF)才能在 MISO 上读到从机返回数据。
总结
本文基于 W55MH32L 的 SPI1 + DMA 实现了对串行 FLASH 的高速数据写入:
- SPI1 配置为主模式、模式 3(CPOL=High/CPHA=2Edge)、8 位帧、MSB 先行,波特率预分频 256
- 引脚分配:PA4(CS, 软件控制) / PA5(SCK) / PA6(MISO) / PA7(MOSI),CS 初始拉高,传输时拉低
- DMA1 Channel3 负责发送(内存→SPI1->DR),Normal 模式下每轮需重新配置
DMA_BufferSize;Channel2 接收通道已配置但未启用 - 主流程:拉低 CS → 启动 DMA TX → 等待 TC 标志 → 清标志关 DMA → 拉高 CS,借助 printf 观察调试信息