全部例程

SPI - 读写串行FLASH

W55MH32

更新于 2026年8月13日

本篇总结

使用 SPI + DMA,读写串行 FLASH。

🔑 KEIL MDK、UART调试、SPI、DMA

本示例演示 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 即可观察串口调试输出。

引脚功能方向模式
PA4SPI1_NSS (CS)MCU → FLASH推挽输出(软件控制)
PA5SPI1_SCKMCU → FLASH复用推挽输出 (AF_PP)
PA6SPI1_MISOFLASH → MCU复用推挽输出 (AF_PP)
PA7SPI1_MOSIMCU → 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_LowSPI_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.uvprojxF7 编译,确认 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.
WIZ UartTool 串口输出 — SPI Flash 读写测试启动
WIZ UartTool 串口输出 — SPI Flash 读写测试启动

若需进一步验证 SPI 时序,可用逻辑分析仪抓取 PA4(CS)/PA5(SCK)/PA7(MOSI) 波形:CS 拉低期间 SCK 应持续输出 256×8=2048 个时钟周期,MOSI 上可观察到 0x01~0xFF 的逐位数据流。

WIZ UartTool 串口输出 — SPI DMA 传输数据与接收结果
WIZ UartTool 串口输出 — SPI DMA 传输数据与接收结果

常见问题

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 观察调试信息

下载本章例程

我们提供完整的工程文件以及配套开发板,方便你随时测试,快速完成产品开发:

开发环境: Keil MDK5 配套开发板

Andy

W5500 技术支持专家