【自学嵌入式:stm32单片机】硬件SPI读写W25Q64(含用DMA读取W25Q64)

硬件SPI读写W25Q64

接线图

和之前软件SPI接线图一样
image

我和江科大版本的接线不同,我还是把OLED屏幕接在了硬件I2C2接口上面
image
SPI1的复用引脚和之前软件SPI的引脚是一样的
SPI1的引脚还可以重定义到:
image
使用重定义的SPI1引脚,需要先 解除JTDI等调试端口的复用,否则不会正常工作,我们只需要修改MYSPI模块即可,其余不用动
image
我对江科大教程的代码做了扩展,实现了连续交换数据和用DMA读取数据
这里SPI1的DMA读写通道是通道2和通道3,我一开始没看手册,自认为通道1也可用,其实不行
DMA读是比较实用的

代码实现

标准库实现

已开源到:https://gitee.com/qin-ruiqian/jiangkeda-stm32

MYSPI.h

#ifndef __MYSPI_H
#define __MYSPI_H

void MYSPI_Init(void);
void MYSPI_Start(void);
void MYSPI_Stop(void);
uint8_t MYSPI_SwapByte(uint8_t ByteSend);
void MYSPI_SendBytes(uint8_t* SendBytes, uint32_t BytesCount);
void MYSPI_ReceiveBytes(uint8_t* ReceiveBytes, uint32_t BytesCount, uint8_t DummyByte);

#endif

MYSPI.c

#include "stm32f10x.h"                  // Device header
#include <stddef.h>  // 包含NULL的定义
#include <stdlib.h> //开辟内存空间用
//#include "Serial.h"


//由于SPI速度非常快,操作完引脚后不需要加延时
//SS引脚还是使用软件模拟
#define MYSPI_W_SS(x) GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)(x))

//初始化SPI通信
void MYSPI_Init(void)
{
    //初始化DMA

    //初始化SPI
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; //PA4还是通用推挽输出,因为SS引脚用软件模拟
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
    //初始化SCK和MOSI,复用推挽输出
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //推挽输出
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; 
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
    //配置MISO,上拉输入模式
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; //上拉输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; 
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
    //初始化SPI外设
    SPI_InitTypeDef SPI_InitStructure;
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //SPI模式,选择当前设备是主机还是从机
    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //选择半双工还是全双工 ,选择双线全双工
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //8位数据帧
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //高位先行
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; //波特率预分频器,配置SCK时钟的频率,选一个慢一点128分频,SPI1的外设是72MHz/128,如果SPI2的外设就得是36MHz/128
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; //第一个边沿采样,相当于第一个边沿移入,模式0
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //选择模式0
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //选择软件NSS
    SPI_InitStructure.SPI_CRCPolynomial = 7; //不用CRC循环冗余码校验,写个7就行
    SPI_Init(SPI1, &SPI_InitStructure);
    
    //使能SPI外设
    SPI_Cmd(SPI1, ENABLE);
    //软件模拟SS接口,让SS默认高电平
    MYSPI_W_SS(1);

    //Serial_Init();
}

//起始信号
void MYSPI_Start(void)
{
    //置SS低电平
    MYSPI_W_SS(0);
}

//终止信号
void MYSPI_Stop(void)
{
    //置SS高电平 
    MYSPI_W_SS(1);
}

//延时等待
void MYSPI_WaitFlag(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG)
{
    uint16_t Timeout = 10000;
    //等待发送寄存器为空
    while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG) != SET)
    {
        Timeout--;
        if(Timeout == 0)
        {
            break;
        }
    }
}

//交换一个字节
//ByteSend是要交换的一个字节
//要通过交换一个字节的时序发送出去
//硬件SPI,必须是发送同时接收,要想接收,必须得先发送
//只有给TDR写东西,才会触发时序的生成
//如果你不发送,只调用接收函数,那时序是不会动的
//TXE RXNE软件清除并不是写代码清除
//写入DR时会顺带执行清除TXE的操作,RXNE也是同理,都是自动清除
uint8_t MYSPI_SwapByte(uint8_t ByteSend)
{
    //等待发送寄存器为空
    MYSPI_WaitFlag(SPI1, SPI_I2S_FLAG_TXE);
    SPI_I2S_SendData(SPI1, ByteSend); //ByteSend自动转入TDR,之后ByteSend自动转入移位寄存器,一旦移位寄存器有数据了,时序波形就会自动产生,这个过程自动完成
    //用非连续传输,所以时序产生的这段时间,我们就不必提前把下一个数据放到TDR里了,这段时间直接死等过去就行了
    //发送和接收是同步的,发送完成时,接收也完成了,只需等待RXNE出现就行
    MYSPI_WaitFlag(SPI1, SPI_I2S_FLAG_RXNE);
    //读取DR,从RDR把交换的数据读出来
    return SPI_I2S_ReceiveData(SPI1);
}

//高速版本时序交换数据,连续传输
//Bytes指针,连续传输的数据
//BytesCount,字节数
void MYSPI_SwapBytes(uint8_t* SendBytes, uint32_t BytesCount, uint8_t* ReceiveBytes, uint8_t DummyByte)
{
    //如果就一个字节
    //退化为之前的死等的版本
    //为负数或者0都返回
    //大于1,开始连续传输
    //如果只接收,不发送,SendBytes是NULL,那就每个字节交换DummyByte
    //如果ReceiveBytes为空,只发送,那就随便接收一下,不赋值
    uint32_t i = 0;
    if(BytesCount == 1)
    {
        uint8_t returnValue = MYSPI_SwapByte(SendBytes[0]);
        *ReceiveBytes = returnValue;
        return;
    }
    else if (BytesCount <= 0)
    {
        return;
    }
    else
    {
        //注意到,发送的时候,先发送第一个字节,然后不接收
        //发送第二个字节的时候,传回来的是第一个字节应该接收的
        //结尾,接收最后一个字节也单独放在循环外
        //这样循环体内部就是BytesCount-1次循环,但是从下标1开始
        //先发送第一个字节
        MYSPI_WaitFlag(SPI1, SPI_I2S_FLAG_TXE);
        if(SendBytes != NULL)
        {
            SPI_I2S_SendData(SPI1, SendBytes[0]);
        }
        else
        {
            SPI_I2S_SendData(SPI1, DummyByte);
        }
        for(i = 1 ; i < BytesCount; i++)
        {
            MYSPI_WaitFlag(SPI1, SPI_I2S_FLAG_TXE);
            if(SendBytes != NULL)
            {
                SPI_I2S_SendData(SPI1, SendBytes[i]);
            }
            else
            {
                SPI_I2S_SendData(SPI1, DummyByte);
            }
            MYSPI_WaitFlag(SPI1, SPI_I2S_FLAG_RXNE);
            if(ReceiveBytes != NULL)
            {
                ReceiveBytes[i-1] = SPI_I2S_ReceiveData(SPI1);
            }
            else
            {
                SPI_I2S_ReceiveData(SPI1);
            }
        }
        //Serial_Printf("%d\r\n", i);
        MYSPI_WaitFlag(SPI1, SPI_I2S_FLAG_RXNE);
        if(ReceiveBytes != NULL)
        {
            ReceiveBytes[i-1] = SPI_I2S_ReceiveData(SPI1); //最后i到BytesCount,因为下标从0开始,减1
        }
        else
        {
            SPI_I2S_ReceiveData(SPI1);
        }
    }
}

//封装成发送和接收,要不然填的参数太多了
void MYSPI_SendBytes(uint8_t* SendBytes, uint32_t BytesCount)
{
    MYSPI_SwapBytes(SendBytes, BytesCount, NULL, 0xFF);
}

void MYSPI_ReceiveBytes(uint8_t* ReceiveBytes, uint32_t BytesCount, uint8_t DummyByte)
{
    MYSPI_SwapBytes(NULL, BytesCount, ReceiveBytes, DummyByte);
}

W25Q64.h

#ifndef __W25Q64_H
#define __W25Q64_H

void W25Q64_Init(void);
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID);
void W25Q64_WriteEnable(void);
void W25Q64_WaitBusy(void);
void W25Q64_PageProgram(uint32_t Address, uint8_t* DataArray,  uint16_t DataCount);
void W25Q64_SectorErase(uint32_t Address);
void W25Q64_ReadData(uint32_t Address, uint8_t* DataArray,  uint32_t DataCount);
void W25Q64_DMA_ReadData(uint32_t Address, uint8_t* DataArray,  uint32_t DataCount);

#endif

W25Q64.c

#include "stm32f10x.h"                  // Device header
#include "MYSPI.h"
#include "W25Q64_Ins.h"
#include <string.h>  // 包含 memcpy 函数的声明
//#include "Serial.h"

#define W25Q64_SECTOR_SIZE 4096 //一页大小

uint8_t W25Q64_Buffer[W25Q64_SECTOR_SIZE]; //全局变量,W25Q64读缓冲区,一页
uint8_t W25Q64_TX_Byte=0xFF;  //写一个字节缓冲


//初始化W25Q64 DMA的结构体
typedef struct W25Q64_DMA_InitStructure
{
    uint32_t DMA_CH2_AddrA;
    uint32_t DMA_CH2_AddrB;
    uint16_t DMA_CH2_Size;
    uint32_t DMA_CH3_AddrA;
    uint32_t DMA_CH3_AddrB;
    uint16_t DMA_CH3_Size;
}W25Q64_DMA_Typedef;

//AddrA,源数组首地址
//AddrB,目的数组首地址
//也相反,主要看参数设置
//Size,数组长度,也算传输计数器的值
//DMA通道2收,通道3发,这是芯片手册里规定的
void W25Q64_DMA_Init(W25Q64_DMA_Typedef* WDIS)
{
    //Serial_Init();
    //DMA是AHB的设备
	//RCC_AHBPeriph_DMA1对应stm32f103系列
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //开启DMA的时钟
	DMA_InitTypeDef DMA_InitStructure; //DMA结构体
	DMA_InitStructure.DMA_PeripheralBaseAddr = WDIS->DMA_CH2_AddrA; //外设站点起始地址
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设站点数据宽度,传输的是字节
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设站点是否自增,不自增,只读数据寄存器的内容 
	DMA_InitStructure.DMA_MemoryBaseAddr = WDIS->DMA_CH2_AddrB; //存储器站点起始地址
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器站点数据宽度
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器站点是否自增
	//DMA_DIR_PeripheralDST外设站点作为目的地
	//DMA_DIR_PeripheralSRC外设站点作为源端
	//把DataA放到外设站点,DataB放到存储器站点
	//传输方向就是外设站点到存储器站点
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //传输方向,指定外设站点是源端还是目的地
	DMA_InitStructure.DMA_BufferSize = WDIS->DMA_CH2_Size; //缓存区大小,其实就是传输计数器
	//自动重装不能应用在存储器到存储器的情况下
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //传输模式,是否使用自动重装
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //选择是否是存储器到存储器,其实就是选择硬件触发还是软件触发,现在改成硬件触发
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级,发送中等,接收优先
	//DMA1_Channel1 - DMA1通道1
	DMA_Init(DMA1_Channel2, &DMA_InitStructure);



    //初始化通道2
    DMA_InitStructure.DMA_PeripheralBaseAddr = WDIS->DMA_CH3_AddrA; //外设站点起始地址
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设站点数据宽度,传输的是字节
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设站点是否自增,不自增,只读数据寄存器的内容 
	DMA_InitStructure.DMA_MemoryBaseAddr = WDIS->DMA_CH3_AddrB; //存储器站点起始地址
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器站点数据宽度
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Disable; //存储器站点是否自增,不自增,通道2一般只发一个字节,不自增
	//DMA_DIR_PeripheralDST外设站点作为目的地
	//DMA_DIR_PeripheralSRC外设站点作为源端
	//把DataA放到外设站点,DataB放到存储器站点
	//传输方向就是外设站点到存储器站点
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //传输方向,指定外设站点是源端还是目的地,这次相反
	DMA_InitStructure.DMA_BufferSize = WDIS->DMA_CH3_Size; //缓存区大小,其实就是传输计数器
	//自动重装不能应用在存储器到存储器的情况下
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //传输模式,是否使用自动重装
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //选择是否是存储器到存储器,其实就是选择硬件触发还是软件触发,现在改成硬件触发
	DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh; //优先级,发送中等,接收优先
	//DMA1_Channel1 - DMA1通道1
	DMA_Init(DMA1_Channel3, &DMA_InitStructure);
	//如果初始化立刻工作就写ENABLE参数
	//这里不让DMA初始化之后立刻进行转运
	//关闭DMA,清DMA标记,使能DMA1_CH3的传输完成中断
    DMA_Cmd(DMA1_Channel2, DISABLE);            //关闭发送DMA
    DMA_Cmd(DMA1_Channel3, DISABLE);            //关闭接收DMA
    //初始化SPI协议
    MYSPI_Init();
    //打开SPI1的DMA发送接收请求
    SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Rx, ENABLE);
    SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE);    

    // //使能NVIC中断,写数据要中断
    // NVIC_InitTypeDef NVIC_InitStructure;
    // NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel2_IRQn;
    // NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //抢占优先级
	// NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //响应优先级
    // NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    // NVIC_Init(&NVIC_InitStructure);
}

//初始化W25Q64
void W25Q64_Init(void)
{
    W25Q64_DMA_Typedef W25Q64_DMA_InitStructure;
    W25Q64_DMA_InitStructure.DMA_CH2_AddrA = (uint32_t)(&(SPI1->DR));
    W25Q64_DMA_InitStructure.DMA_CH2_AddrB = (uint32_t)W25Q64_Buffer;
    W25Q64_DMA_InitStructure.DMA_CH2_Size = 0;
    W25Q64_DMA_InitStructure.DMA_CH3_AddrA = (uint32_t)(&(SPI1->DR));
    W25Q64_DMA_InitStructure.DMA_CH3_AddrB = (uint32_t)(&W25Q64_TX_Byte);
    W25Q64_DMA_InitStructure.DMA_CH3_Size = 0;
    W25Q64_DMA_Init(&W25Q64_DMA_InitStructure);
}

//获取设备ID
//MID是厂商ID
//DID是16位设备ID
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{
    MYSPI_Start();
    MYSPI_SwapByte(W25Q64_JEDEC_ID); //返回值不要了,没有意义,0x9F读ID号指令
    //下一次交换就会返回ID号
    //为了交换,发送0xFF
    *MID = MYSPI_SwapByte(W25Q64_DUMMY_BYTE);
    //接下来两个字节分别是设备ID高8位和低8位
    *DID = MYSPI_SwapByte(W25Q64_DUMMY_BYTE);
    *DID <<= 8;
    *DID |= MYSPI_SwapByte(W25Q64_DUMMY_BYTE);
    MYSPI_Stop();
}

//写使能
void W25Q64_WriteEnable(void)
{
    MYSPI_Start();
    MYSPI_SwapByte(W25Q64_WRITE_ENABLE);
    MYSPI_Stop();
}

//等待W25Q64的忙状态
void W25Q64_WaitBusy(void)
{
    uint32_t Timeout = 100000; //防止死循环卡死的超时处理
    MYSPI_Start();
    MYSPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
    //用掩码取最低位读BUSY
    while((MYSPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)
    {
        Timeout--;
        if(Timeout == 0)
        {
            //可加错误处理函数
            break;
        }
    }
    MYSPI_Stop();
}

//等待DMA的忙状态
void W25Q64_DMA_WaitBusy(uint32_t FLAG1, uint32_t FLAG2)
{
    uint32_t Timeout = 100000; //防止死循环卡死的超时处理
    //最开始这里写成逻辑与是错误的
    /*
    SPI 是全双工通信,DMA1_Channel2(接收)和DMA1_Channel3(发送)必须都完成传输,才能保证 4 个字节都读 / 发完。
    当前条件是 “TC2没完成 AND TC3没完成才等待”—— 只要有一个通道先完成(比如 TC2 先置 1),循环就退出,此时另一个通道(TC3)还在传输,导致后面的字节没传完,自然读不到。
    */
    while(DMA_GetFlagStatus(FLAG1) == RESET || DMA_GetFlagStatus(FLAG2) == RESET) //没完成就一直循环等待
    {
        Timeout--;
        if(Timeout == 0)
        {
            //可加错误处理函数
            break;
        }
    }
}

//页编程
//24位地址,没有24位的变量,就用32位的无符号整数
//Address写数据的地址
//待写入的数据数组DataArray
//要传输多少字节DataCount
//由于页编程最大一次性传256个字节
//DataCount指定义为8位的,只能存0-255
//这样写入256个数据时就会出问题
void W25Q64_PageProgram(uint32_t Address, uint8_t* DataArray,  uint16_t DataCount)
{
    uint8_t send1[] = {W25Q64_PAGE_PROGRAM , (Address >> 16), (Address >> 8), (Address)};
    //uint16_t i = 0;
    //写入操作前,必须先进行写使能
    W25Q64_WriteEnable();
    MYSPI_Start();
    // MYSPI_SwapByte(W25Q64_PAGE_PROGRAM);
    // //接下来发送3个字节的地址
    // //依次从高位到低位,只能接收8位数据
    // //所以获取到对应的高8位,次高8位,低8位
    // //其余位置的二进制都舍弃了
    // MYSPI_SwapByte(Address >> 16);
    // MYSPI_SwapByte(Address >> 8);
    // MYSPI_SwapByte(Address);
    MYSPI_SendBytes(send1, 4);
    //地址发完,开始写入数据
    // for(i = 0; i < DataCount; i++)
    // {
    //     MYSPI_SwapByte(DataArray[i]);
    // }
    MYSPI_SendBytes(DataArray, DataCount);
    MYSPI_Stop();
    //写入后事后等待BUSY位
    //也可以事前等待,写入之前等待BUSY位
    //事后等待最保险,在函数之外的地方,芯片肯定是不忙的状态
    //事后等待只需要在写入之后调用,而事前等待,在写入操作和读取操作之前,都得调用
    W25Q64_WaitBusy();
}

//擦除指定地址的扇区
void W25Q64_SectorErase(uint32_t Address)
{
    uint8_t send1[] = {W25Q64_SECTOR_ERASE_4KB , (Address >> 16), (Address >> 8), (Address)};
    //写入操作前,必须先进行写使能
    W25Q64_WriteEnable();
    MYSPI_Start();
    // MYSPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
    // MYSPI_SwapByte(Address >> 16);
    // MYSPI_SwapByte(Address >> 8);
    // MYSPI_SwapByte(Address);
    MYSPI_SendBytes(send1, 4);
    MYSPI_Stop();
    W25Q64_WaitBusy();
}

//读数据
//通过数组输出DataArray
//读取数据可以非常大,16位无符号整数不够用
//所以改成32位整数DataCount类型
void W25Q64_ReadData(uint32_t Address, uint8_t* DataArray,  uint32_t DataCount)
{
    // uint32_t i = 0;
    uint8_t send1[] = {W25Q64_READ_DATA , (Address >> 16), (Address >> 8), (Address)};
    MYSPI_Start();
    // MYSPI_SwapByte(W25Q64_READ_DATA);
    // //接下来发送3个字节的地址
    // //依次从高位到低位,只能接收8位数据
    // //所以获取到对应的高8位,次高8位,低8位
    // //其余位置的二进制都舍弃了
    // MYSPI_SwapByte(Address >> 16);
    // MYSPI_SwapByte(Address >> 8);
    // MYSPI_SwapByte(Address);
    //先发读命令,再发地址
    MYSPI_SendBytes(send1, 4);
    //直接发送数组
    MYSPI_ReceiveBytes(DataArray, DataCount, W25Q64_DUMMY_BYTE);
    // for(i = 0; i < DataCount; i++)
    // {
    //     DataArray[i] = MYSPI_SwapByte(W25Q64_DUMMY_BYTE);
    // }
    MYSPI_Stop();
}

//通过DMA读数据
//最多读一个扇区
void W25Q64_DMA_ReadData(uint32_t Address, uint8_t* DataArray,  uint32_t DataCount)
{
    uint8_t send1[] = {W25Q64_READ_DATA , (Address >> 16), (Address >> 8), (Address)};
    MYSPI_Start();
    MYSPI_SendBytes(send1, 4); //把指令和地址发过去
    //重新给传输计数器赋值,赋值前必须先给DMA失能再赋值,最后使能
	DMA_Cmd(DMA1_Channel2, DISABLE);
    DMA_Cmd(DMA1_Channel3, DISABLE);
	DMA_SetCurrDataCounter(DMA1_Channel2, DataCount);
    DMA_SetCurrDataCounter(DMA1_Channel3, DataCount);
	DMA_Cmd(DMA1_Channel2, ENABLE);
    DMA_Cmd(DMA1_Channel3, ENABLE);
    W25Q64_DMA_WaitBusy(DMA1_FLAG_TC2, DMA1_FLAG_TC3);
    //Serial_Printf("等过去了\r\n");
    //将 W25Q64_Buffer 中的数据复制到外部传入的 DataArray 缓冲区
    //等DMA传完数据再拷贝
    memcpy(DataArray, W25Q64_Buffer, DataCount);
    // 清除接收通道标志(TC2 + GL2)
    DMA_ClearFlag(DMA1_FLAG_TC2 | DMA1_FLAG_GL2);
    // 清除发送通道标志(TC3 + GL3)
    DMA_ClearFlag(DMA1_FLAG_TC3 | DMA1_FLAG_GL3);
    MYSPI_Stop();
}

main.c

#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "MYOLED.h"
#include "W25Q64.h"
//#include "Serial.h"

uint8_t MID; //厂商ID
uint16_t DID; //设备ID

uint8_t ArrayWrite[] = {0xA1, 0x02, 0x03, 0x04};
uint8_t ArrayRead[4];

int main(void)
{
	MYOLED_Init();
	//Serial_Init();
    W25Q64_Init();
	//Serial_Printf("W25Q64初始化完成\r\n");
	//Serial_Printf("MYOLED初始化完成\r\n");
    MYOLED_ShowString(0,0,"MID:  ;DID:   ");
    MYOLED_ShowString(0,1,"W:");
    MYOLED_ShowString(0,2,"R:");
    W25Q64_ReadID(&MID, &DID);
	MYOLED_ShowHexNum(4,0,MID,2);
    MYOLED_ShowHexNum(11,0,DID,4);
    //向0x00 00 00地址写数据,写之前擦扇区
    //最好对齐扇区的起始地址,也就是末尾3位都是0
    W25Q64_SectorErase(0X000000);
    W25Q64_PageProgram(0x000000, ArrayWrite, 4);
    W25Q64_DMA_ReadData(0x000000, ArrayRead, 4);
    MYOLED_ShowHexNum(2,1,ArrayWrite[0],2);
    MYOLED_ShowHexNum(5,1,ArrayWrite[1],2);
    MYOLED_ShowHexNum(8,1,ArrayWrite[2],2);
    MYOLED_ShowHexNum(11,1,ArrayWrite[3],2);
    MYOLED_ShowHexNum(2,2,ArrayRead[0],2);
    MYOLED_ShowHexNum(5,2,ArrayRead[1],2);
    MYOLED_ShowHexNum(8,2,ArrayRead[2],2);
    MYOLED_ShowHexNum(11,2,ArrayRead[3],2);
    while (1) 
    {
        Delay_ms(1000);
        ArrayWrite[0]++;ArrayWrite[1]++;ArrayWrite[2]++;ArrayWrite[3]++;
        W25Q64_SectorErase(0X000000);
        W25Q64_PageProgram(0x000000, ArrayWrite, 4);
        W25Q64_DMA_ReadData(0x000000, ArrayRead, 4);
        MYOLED_ShowHexNum(2,1,ArrayWrite[0],2);
        MYOLED_ShowHexNum(5,1,ArrayWrite[1],2);
        MYOLED_ShowHexNum(8,1,ArrayWrite[2],2);
        MYOLED_ShowHexNum(11,1,ArrayWrite[3],2);
        MYOLED_ShowHexNum(2,2,ArrayRead[0],2);
        MYOLED_ShowHexNum(5,2,ArrayRead[1],2);
        MYOLED_ShowHexNum(8,2,ArrayRead[2],2);
        MYOLED_ShowHexNum(11,2,ArrayRead[3],2);
    }
}

HAL库

已开源到:https://gitee.com/qin-ruiqian/jiangkeda-stm32-hal
关于IDE配置可参考开源仓库对应的.ioc文件

MYSPI.h

/*
 * MYSPI.h
 *
 *  Created on: Aug 23, 2025
 *      Author: Administrator
 */

#ifndef HARDWARE_MYSPI_H_
#define HARDWARE_MYSPI_H_

#include "stm32f1xx_hal.h"

extern SPI_HandleTypeDef* MYSPI_pHspi1;
extern DMA_HandleTypeDef* MYSPI_pHdma_spi1_rx;
extern DMA_HandleTypeDef* MYSPI_pHdma_spi1_tx;

void MYSPI_Init(SPI_HandleTypeDef* pshtd);
void MYSPI_Start(void);
void MYSPI_Stop(void);
uint8_t MYSPI_SwapByte(uint8_t ByteSend);
void MYSPI_SendBytes(uint8_t* SendBytes, uint16_t BytesCount);
void MYSPI_ReceiveBytes(uint8_t* ReceiveBytes, uint16_t BytesCount);

#endif /* HARDWARE_MYSPI_H_ */

MYSPI.c

/*
 * MYSPI.c
 *
 *  Created on: Aug 23, 2025
 *      Author: Administrator
 */
#include "stm32f1xx_hal.h"

SPI_HandleTypeDef* MYSPI_pHspi1;

//由于SPI速度非常快,操作完引脚后不需要加延时
#define MYSPI_W_SS(x) HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, x)

//初始化SPI通信
void MYSPI_Init(SPI_HandleTypeDef* pshtd)
{
	MYSPI_pHspi1 = pshtd;
	MYSPI_W_SS(1);
}

//起始信号
void MYSPI_Start(void)
{
    //置SS低电平
    MYSPI_W_SS(0);
}

//终止信号
void MYSPI_Stop(void)
{
    //置SS高电平
    MYSPI_W_SS(1);
}

//交换一个字节
//ByteSend是要交换的一个字节
//要通过交换一个字节的时序发送出去
uint8_t MYSPI_SwapByte(uint8_t ByteSend)
{
	uint8_t ByteReceive = 0;
	// HAL库函数:阻塞式发送并接收一个字节(自动处理TXE和RXNE标志等待)
	// 参数:SPI句柄、发送缓冲区、接收缓冲区、数据长度、超时时间(ms)
	HAL_SPI_TransmitReceive(MYSPI_pHspi1, &ByteSend, &ByteReceive, 1, HAL_MAX_DELAY);
    return ByteReceive;
}

//HAL库集成了连续发送多个字节的功能,就是把刚才调用的函数的参数改一改就好了
void MYSPI_SwapBytes(uint8_t* SendBytes, uint16_t BytesCount, uint8_t* ReceiveBytes, uint8_t DummyByte)
{
	HAL_SPI_TransmitReceive(MYSPI_pHspi1, SendBytes, ReceiveBytes, BytesCount, HAL_MAX_DELAY);
}

//发送和接收,HAL库也有单独的函数(HAL_SPI_Transmit,HAL_SPI_Receive),所以不用刚才的逻辑也可以
void MYSPI_SendBytes(uint8_t* SendBytes, uint16_t BytesCount)
{
	HAL_SPI_Transmit(MYSPI_pHspi1, SendBytes, BytesCount, HAL_MAX_DELAY);
}

void MYSPI_ReceiveBytes(uint8_t* ReceiveBytes, uint16_t BytesCount)
{
	HAL_SPI_Receive(MYSPI_pHspi1, ReceiveBytes, BytesCount, HAL_MAX_DELAY);
}


W25Q64.h

/*
 * W25Q64.h
 *
 *  Created on: Aug 23, 2025
 *      Author: Administrator
 */

#ifndef HARDWARE_W25Q64_H_
#define HARDWARE_W25Q64_H_

////初始化W25Q64 DMA的结构体
//typedef struct W25Q64_DMA_InitStructure
//{
//    uint32_t DMA_CH2_AddrA;
//    uint32_t DMA_CH2_AddrB;
//    uint16_t DMA_CH2_Size;
//    uint32_t DMA_CH3_AddrA;
//    uint32_t DMA_CH3_AddrB;
//    uint16_t DMA_CH3_Size;
//}W25Q64_DMA_Typedef;

void W25Q64_Init(SPI_HandleTypeDef* pshtd);
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID);
void W25Q64_WriteEnable(void);
void W25Q64_WaitBusy(void);
void W25Q64_PageProgram(uint32_t Address, uint8_t* DataArray,  uint16_t DataCount);
void W25Q64_SectorErase(uint32_t Address);
void W25Q64_ReadData(uint32_t Address, uint8_t* DataArray,  uint32_t DataCount);
void W25Q64_DMA_ReadData(uint32_t Address, uint8_t* DataArray,  uint32_t DataCount);

#endif /* HARDWARE_W25Q64_H_ */

W25Q64.c

/*
 * W25Q64.c
 *
 *  Created on: Aug 23, 2025
 *      Author: Administrator
 */


#include "stm32f1xx_hal.h"
#include "MYSPI.h"
#include "W25Q64_Ins.h"
//#include "W25Q64.h"

#define W25Q64_SECTOR_SIZE 4096 //一页大小

//uint8_t W25Q64_Buffer[W25Q64_SECTOR_SIZE]; //全局变量,W25Q64读缓冲区,一页
//uint8_t W25Q64_TX_Byte=0xFF;  //写一个字节缓冲 //HAL库里甚至不需要自己搞发送和接收的缓冲
SPI_HandleTypeDef* W25Q64_pSHTD;

DMA_HandleTypeDef* W25Q64_pHdma_spi1_rx;
DMA_HandleTypeDef* W25Q64_pHdma_spi1_tx;

//初始化W25Q64
void W25Q64_Init(SPI_HandleTypeDef* pshtd)
{
	W25Q64_pSHTD = pshtd;
    MYSPI_Init(pshtd);
//    //初始化DMA的一些信息
//    W25Q64_pHdma_spi1_rx = hdma_spi1_rx;
//    W25Q64_pHdma_spi1_tx = hdma_spi1_tx;
//    // 禁用 DMA(初始化后不立即启动)
//    HAL_DMA_Abort (phdma_spi1_rx);
//    HAL_DMA_Abort (hdma_spi1_tx);
}

//获取设备ID
//MID是厂商ID
//DID是16位设备ID
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{
    MYSPI_Start();
    MYSPI_SwapByte(W25Q64_JEDEC_ID); //返回值不要了,没有意义,0x9F读ID号指令
    //下一次交换就会返回ID号
    //为了交换,发送0xFF
    *MID = MYSPI_SwapByte(W25Q64_DUMMY_BYTE);
    //接下来两个字节分别是设备ID高8位和低8位
    *DID = MYSPI_SwapByte(W25Q64_DUMMY_BYTE);
    *DID <<= 8;
    *DID |= MYSPI_SwapByte(W25Q64_DUMMY_BYTE);
    MYSPI_Stop();
}

//写使能
void W25Q64_WriteEnable(void)
{
    MYSPI_Start();
    MYSPI_SwapByte(W25Q64_WRITE_ENABLE);
    MYSPI_Stop();
}

//等待W25Q64的忙状态
void W25Q64_WaitBusy(void)
{
    uint32_t Timeout = 100000; //防止死循环卡死的超时处理
    MYSPI_Start();
    MYSPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1);
    //用掩码取最低位读BUSY
    while((MYSPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01)
    {
        Timeout--;
        if(Timeout == 0)
        {
            //可加错误处理函数
            break;
        }
    }
    MYSPI_Stop();
}

//页编程
//24位地址,没有24位的变量,就用32位的无符号整数
//Address写数据的地址
//待写入的数据数组DataArray
//要传输多少字节DataCount
//由于页编程最大一次性传256个字节
//DataCount指定义为8位的,只能存0-255
//这样写入256个数据时就会出问题
void W25Q64_PageProgram(uint32_t Address, uint8_t* DataArray,  uint16_t DataCount)
{
	uint8_t send1[] = {W25Q64_PAGE_PROGRAM , (Address >> 16), (Address >> 8), (Address)};
    //uint16_t i = 0;
    //写入操作前,必须先进行写使能
    W25Q64_WriteEnable();
    MYSPI_Start();
//    MYSPI_SwapByte(W25Q64_PAGE_PROGRAM);
//    //接下来发送3个字节的地址
//    //依次从高位到低位,只能接收8位数据
//    //所以获取到对应的高8位,次高8位,低8位
//    //其余位置的二进制都舍弃了
//    MYSPI_SwapByte(Address >> 16);
//    MYSPI_SwapByte(Address >> 8);
//    MYSPI_SwapByte(Address);
    MYSPI_SendBytes(send1, 4);
    //地址发完,开始写入数据
//    for(i = 0; i < DataCount; i++)
//    {
//        MYSPI_SwapByte(DataArray[i]);
//    }
    MYSPI_SendBytes(DataArray, DataCount);
    MYSPI_Stop();
    //写入后事后等待BUSY位
    //也可以事前等待,写入之前等待BUSY位
    //事后等待最保险,在函数之外的地方,芯片肯定是不忙的状态
    //事后等待只需要在写入之后调用,而事前等待,在写入操作和读取操作之前,都得调用
    W25Q64_WaitBusy();
}

//擦除指定地址的扇区
void W25Q64_SectorErase(uint32_t Address)
{
	uint8_t send1[] = {W25Q64_SECTOR_ERASE_4KB , (Address >> 16), (Address >> 8), (Address)};
    //写入操作前,必须先进行写使能
    W25Q64_WriteEnable();
    MYSPI_Start();
//    MYSPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);
//    MYSPI_SwapByte(Address >> 16);
//    MYSPI_SwapByte(Address >> 8);
//    MYSPI_SwapByte(Address);
    MYSPI_SendBytes(send1, 4);
    MYSPI_Stop();
    W25Q64_WaitBusy();
}

//读数据
//通过数组输出DataArray
//读取数据可以非常大,16位无符号整数不够用
//所以改成32位整数DataCount类型
void W25Q64_ReadData(uint32_t Address, uint8_t* DataArray,  uint32_t DataCount)
{
    //uint32_t i = 0;
	uint8_t send1[] = {W25Q64_READ_DATA , (Address >> 16), (Address >> 8), (Address)};
    MYSPI_Start();
//    MYSPI_SwapByte(W25Q64_READ_DATA);
//    //接下来发送3个字节的地址
//    //依次从高位到低位,只能接收8位数据
//    //所以获取到对应的高8位,次高8位,低8位
//    //其余位置的二进制都舍弃了
//    MYSPI_SwapByte(Address >> 16);
//    MYSPI_SwapByte(Address >> 8);
//    MYSPI_SwapByte(Address);
    MYSPI_SendBytes(send1, 4);
//    for(i = 0; i < DataCount; i++)
//    {
//        DataArray[i] = MYSPI_SwapByte(W25Q64_DUMMY_BYTE);
//    }
    MYSPI_ReceiveBytes(DataArray, DataCount);
    MYSPI_Stop();
}

/**
  * @brief  以轮询方式等待SPI+DMA传输完成,确保SPI外设回到就绪状态
  * @note   针对W25Q64的DMA读场景:等待DMA接收完成后,SPI外设才会释放(避免后续操作冲突)
  *         若超时,会强制终止DMA传输并释放W25Q64的片选(防止硬件卡死)
  */
void W25Q64_DMA_WaitSPIBusy(void)
{
	//等待 DMA 接收完成(polling 方式)
	uint32_t timeout = 0;
	while (HAL_SPI_GetState(W25Q64_pSHTD) != HAL_SPI_STATE_READY) {
		timeout++;
		if (timeout > 100000) { // 超时保护(根据 DataCount 调整,比如 100us/字节)
			HAL_SPI_Abort(W25Q64_pSHTD); // 终止 DMA 传输
			MYSPI_Stop();
			return;
		}
	}
}
void W25Q64_DMA_WaitSPIBusy(void)
{
	//等待 DMA 接收完成(polling 方式)
	uint32_t timeout = 0;
	while (HAL_SPI_GetState(W25Q64_pSHTD) != HAL_SPI_STATE_READY) {
		timeout++;
		if (timeout > 100000) { // 超时保护(根据 DataCount 调整,比如 100us/字节)
			HAL_SPI_Abort(W25Q64_pSHTD); // 终止 DMA 传输
			MYSPI_Stop();
			return;
		}
	}
}

//通过DMA读数据
//HAL库封装好了,很方便
void W25Q64_DMA_ReadData(uint32_t Address, uint8_t* DataArray,  uint32_t DataCount)
{
	uint8_t send1[] = {W25Q64_READ_DATA , (Address >> 16), (Address >> 8), (Address)};
	MYSPI_Start();
	MYSPI_SendBytes(send1, 4); //把指令和地址发过去
	HAL_SPI_Receive_DMA(W25Q64_pSHTD, DataArray, DataCount);
	//在 HAL_SPI_Receive_DMA 后,通过 HAL_SPI_GetState 等待 DMA 传输完成,确保 CS 不提前释放,且下次传输前 DMA 已空闲
	W25Q64_DMA_WaitSPIBusy();

	MYSPI_Stop();
}

main.c

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2025 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "dma.h"
#include "i2c.h"
#include "spi.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "MYOLED.h"
#include "W25Q64.h"
/* USER CODE END Includes */

/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */

/* USER CODE END PTD */

/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */

/* USER CODE END PD */

/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */

/* USER CODE END PM */

/* Private variables ---------------------------------------------------------*/

/* USER CODE BEGIN PV */
uint8_t MID; //厂商ID
uint16_t DID; //设备ID

uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04};
uint8_t ArrayRead[4];
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

/* USER CODE END PFP */

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_SPI1_Init();
  MX_I2C2_Init();
  /* USER CODE BEGIN 2 */
	MYOLED_SetI2CHandleBeforeInit(&hi2c2);
	MYOLED_Init();
	W25Q64_Init(&hspi1);
	MYOLED_ShowString(0,0,"MID:  ;DID:   ");
	MYOLED_ShowString(0,1,"W:");
	MYOLED_ShowString(0,2,"R:");
	W25Q64_ReadID(&MID, &DID);
	MYOLED_ShowHexNum(4,0,MID,2);
	MYOLED_ShowHexNum(11,0,DID,4);
	//向0x00 00 00地址写数据,写之前擦扇区
	//最好对齐扇区的起始地址,也就是末尾3位都是0
	W25Q64_SectorErase(0X000000);
	W25Q64_PageProgram(0x000000, ArrayWrite, 4);
	W25Q64_DMA_ReadData(0x000000, ArrayRead, 4);
	MYOLED_ShowHexNum(2,1,ArrayWrite[0],2);
	MYOLED_ShowHexNum(5,1,ArrayWrite[1],2);
	MYOLED_ShowHexNum(8,1,ArrayWrite[2],2);
	MYOLED_ShowHexNum(11,1,ArrayWrite[3],2);
	MYOLED_ShowHexNum(2,2,ArrayRead[0],2);
	MYOLED_ShowHexNum(5,2,ArrayRead[1],2);
	MYOLED_ShowHexNum(8,2,ArrayRead[2],2);
	MYOLED_ShowHexNum(11,2,ArrayRead[3],2);
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
	  HAL_Delay(1000);
	  ArrayWrite[0]++;ArrayWrite[1]++;ArrayWrite[2]++;ArrayWrite[3]++;
	  W25Q64_SectorErase(0X000000);
	  W25Q64_PageProgram(0x000000, ArrayWrite, 4);
	  W25Q64_DMA_ReadData(0x000000, ArrayRead, 4);
	  MYOLED_ShowHexNum(2,1,ArrayWrite[0],2);
	  MYOLED_ShowHexNum(5,1,ArrayWrite[1],2);
	  MYOLED_ShowHexNum(8,1,ArrayWrite[2],2);
	  MYOLED_ShowHexNum(11,1,ArrayWrite[3],2);
	  MYOLED_ShowHexNum(2,2,ArrayRead[0],2);
	  MYOLED_ShowHexNum(5,2,ArrayRead[1],2);
	  MYOLED_ShowHexNum(8,2,ArrayRead[2],2);
	  MYOLED_ShowHexNum(11,2,ArrayRead[3],2);
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2) != HAL_OK)
  {
    Error_Handler();
  }
}

/* USER CODE BEGIN 4 */

/* USER CODE END 4 */

/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}

#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

实现效果

image

关于为什么标准库是先等待DMA硬件标志后传数据而HAL库是先传数据后等待SPI状态机标志

细心的你会发现,我这代码有两处一样功能的函数,但是在标注库和HAL库上实现的方式不一样

标注库代码

//等待DMA的忙状态
void W25Q64_DMA_WaitBusy(uint32_t FLAG1, uint32_t FLAG2)
{
    uint32_t Timeout = 100000; //防止死循环卡死的超时处理
    //最开始这里写成逻辑与是错误的
    /*
    SPI 是全双工通信,DMA1_Channel2(接收)和DMA1_Channel3(发送)必须都完成传输,才能保证 4 个字节都读 / 发完。
    当前条件是 “TC2没完成 AND TC3没完成才等待”—— 只要有一个通道先完成(比如 TC2 先置 1),循环就退出,此时另一个通道(TC3)还在传输,导致后面的字节没传完,自然读不到。
    */
    while(DMA_GetFlagStatus(FLAG1) == RESET || DMA_GetFlagStatus(FLAG2) == RESET) //没完成就一直循环等待
    {
        Timeout--;
        if(Timeout == 0)
        {
            //可加错误处理函数
            break;
        }
    }
}

void W25Q64_DMA_ReadData(uint32_t Address, uint8_t* DataArray,  uint32_t DataCount)
{
    uint8_t send1[] = {W25Q64_READ_DATA , (Address >> 16), (Address >> 8), (Address)};
    MYSPI_Start();
    MYSPI_SendBytes(send1, 4); //把指令和地址发过去
    //重新给传输计数器赋值,赋值前必须先给DMA失能再赋值,最后使能
	DMA_Cmd(DMA1_Channel2, DISABLE);
    DMA_Cmd(DMA1_Channel3, DISABLE);
	DMA_SetCurrDataCounter(DMA1_Channel2, DataCount);
    DMA_SetCurrDataCounter(DMA1_Channel3, DataCount);
	DMA_Cmd(DMA1_Channel2, ENABLE);
    DMA_Cmd(DMA1_Channel3, ENABLE);
    W25Q64_DMA_WaitBusy(DMA1_FLAG_TC2, DMA1_FLAG_TC3);
    //Serial_Printf("等过去了\r\n");
    //将 W25Q64_Buffer 中的数据复制到外部传入的 DataArray 缓冲区
    //等DMA传完数据再拷贝
    memcpy(DataArray, W25Q64_Buffer, DataCount);
    // 清除接收通道标志(TC2 + GL2)
    DMA_ClearFlag(DMA1_FLAG_TC2 | DMA1_FLAG_GL2);
    // 清除发送通道标志(TC3 + GL3)
    DMA_ClearFlag(DMA1_FLAG_TC3 | DMA1_FLAG_GL3);
    MYSPI_Stop();
}

HAL库代码

/**
  * @brief  以轮询方式等待SPI+DMA传输完成,确保SPI外设回到就绪状态
  * @note   针对W25Q64的DMA读场景:等待DMA接收完成后,SPI外设才会释放(避免后续操作冲突)
  *         若超时,会强制终止DMA传输并释放W25Q64的片选(防止硬件卡死)
  */
void W25Q64_DMA_ReadData(uint32_t Address, uint8_t* DataArray,  uint32_t DataCount)
{
	uint8_t send1[] = {W25Q64_READ_DATA , (Address >> 16), (Address >> 8), (Address)};
	MYSPI_Start();
	MYSPI_SendBytes(send1, 4); //把指令和地址发过去
	HAL_SPI_Receive_DMA(W25Q64_pSHTD, DataArray, DataCount);
	//在 HAL_SPI_Receive_DMA 后,通过 HAL_SPI_GetState 等待 DMA 传输完成,确保 CS 不提前释放,且下次传输前 DMA 已空闲
	W25Q64_DMA_WaitSPIBusy();

	MYSPI_Stop();
}

总结(AI)

我对这个现象也感兴趣,问了AI,总结如下:
(基于STM32 DMA读取W25Q64场景,通俗易懂且专业)

一、核心前提:两者的“等待时机”完全一致

无论是标准库还是HAL库,DMA等待的核心目的都一样——「启动DMA异步传输后,等待传输完成,再进行后续操作(如释放片选、拷贝数据)」,避免读到不完整数据或破坏SPI时序。
两者的等待都发生在「启动DMA传输之后」(比如DMA_Cmd/HAL_SPI_Receive_DMA调用后),因为DMA传输需要时间,必须等结束才能下一步。

二、核心区别:从5个关键维度对比

1. 等待对象:盯“硬件信号灯” vs 盯“状态牌”
标准库:直接盯DMA的“硬件信号灯”(底层寄存器标志)
  • 本质:直接检查DMA控制器的硬件标志位(比如传输完成标志TCx),相当于“直接看DMA硬件的‘完成灯’亮了没”。
  • 特点:必须知道具体的DMA通道(如通道2、通道3)和对应的标志(如DMA1_FLAG_TC2),依赖对硬件细节的了解。
HAL库:盯SPI的“状态牌”(上层封装的状态机)
  • 本质:不直接看DMA,而是看HAL库封装的SPI状态机(比如HAL_SPI_STATE_READY表示空闲,HAL_SPI_STATE_BUSY_RX表示DMA接收中),相当于“看SPI整体的‘工作状态牌’,牌显示‘空闲’就代表DMA传完了”。
  • 特点:不用管具体DMA通道,只需操作SPI句柄(如W25Q64_pSHTD),HAL库内部已封装DMA状态判断。
2. 抽象层次:“直接操作硬件” vs “用封装好的工具”
标准库:低层次,直接对话硬件
  • 相当于“直接手动拧螺丝”:需要自己找对应的DMA寄存器(如DMA1->ISR),自己判断标志位,操作更直接但门槛高。
  • 优点:响应快(无中间封装)、灵活性高(可自定义判断逻辑);
  • 缺点:需记硬件细节(如通道号、标志位名称),容易因记错参数出错。
HAL库:高层次,用封装好的“工具”
  • 相当于“用电动螺丝刀”:HAL库把复杂的硬件操作(DMA标志检查、SPI状态判断)封装成简单的函数(如HAL_SPI_GetState),不用关心底层寄存器。
  • 优点:门槛低(不用记硬件细节)、不易出错(状态机已做完整性判断);
  • 缺点:略多一层封装(响应速度稍慢,日常场景可忽略)、灵活性稍低(受HAL状态机限制)。
3. 代码实现示例:直观对比
标准库:直接检查DMA硬件标志
// 标准库:等待DMA通道2、3的传输完成标志(TCx)
void W25Q64_DMA_WaitBusy(uint32_t FLAG1, uint32_t FLAG2)
{
    uint32_t Timeout = 100000; // 超时保护,防止死循环
    // 等待两个DMA通道的“传输完成标志”都置位(亮灯)
    while(DMA_GetFlagStatus(FLAG1) == RESET || DMA_GetFlagStatus(FLAG2) == RESET)
    {
        Timeout--;
        if(Timeout == 0)
        {
            // 超时处理:可加错误提示(如串口打印)
            break;
        }
    }
}

// 调用场景:启动DMA后等待
DMA_Cmd(DMA1_Channel2, ENABLE); // 启动接收DMA
DMA_Cmd(DMA1_Channel3, ENABLE); // 启动发送DMA
W25Q64_DMA_WaitBusy(DMA1_FLAG_TC2, DMA1_FLAG_TC3); // 等DMA标志
HAL 库:检查 SPI 状态机
// HAL库:等待SPI状态机回到“就绪”状态
void W25Q64_DMA_WaitSPIBusy(void)
{
    uint32_t timeout = 0; // 超时保护
    // 等待SPI状态机显示“空闲”(代表DMA传输完成+无错误)
    while (HAL_SPI_GetState(W25Q64_pSHTD) != HAL_SPI_STATE_READY)
    {
        timeout++;
        if (timeout > 100000)
        {
            HAL_SPI_Abort(W25Q64_pSHTD); // 强制终止异常传输
            MYSPI_Stop();                 // 释放W25Q64片选
            return;
        }
    }
}

// 调用场景:启动DMA后等待
HAL_SPI_Receive_DMA(W25Q64_pSHTD, DataArray, DataCount); // 启动DMA接收
W25Q64_DMA_WaitSPIBusy(); // 等SPI状态机就绪
4. 标志位处理:“手动擦灯” vs “自动擦灯”
标准库:必须手动 “擦灯”(清除标志位)

DMA 的硬件标志(如TCx)被置位后,不会自动清除,就像 “信号灯亮了不会自己灭”,必须手动调用DMA_ClearFlag清除,否则下次判断会误以为 “又完成了”:

// 标准库:传输完成后必须手动清标志
DMA_ClearFlag(DMA1_FLAG_TC2 | DMA1_FLAG_GL2); // 清接收通道标志(TC2+全局标志GL2)
DMA_ClearFlag(DMA1_FLAG_TC3 | DMA1_FLAG_GL3); // 清发送通道标志(TC3+全局标志GL3)
HAL 库:自动 “擦灯”(内部封装清除逻辑)

HAL 库在状态机切换时(比如从BUSY_RXREADY),会自动清除 DMA 和 SPI 的标志位,不用用户手动处理,相当于 “工具自己擦信号灯”:

// HAL库:无需手动清标志,状态机切换时内部自动处理
while (HAL_SPI_GetState(W25Q64_pSHTD) != HAL_SPI_STATE_READY); // 等就绪后,标志已自动清
5. 错误处理:“自己判断异常” vs “状态机提示异常”
标准库:需手动判断超时,其他错误需额外检查

标准库的等待函数只判断 “标志位是否置位”,超时需要自己加计数器,其他错误(如 DMA 传输错误TE)需额外调用DMA_GetFlagStatus检查:

// 标准库:仅处理超时,其他错误需额外加逻辑
if(Timeout == 0)
{
    // 若要查“传输错误”,需额外判断DMA错误标志
    if(DMA_GetFlagStatus(DMA1_FLAG_TE2) == SET)
    {
        DMA_ClearFlag(DMA1_FLAG_TE2); // 清错误标志
        // 加错误提示(如串口打印“DMA传输错误”)
    }
    break;
}
HAL 库:状态机包含错误状态,判断更全面

HAL 库的HAL_SPI_GetState会返回多种错误状态(如HAL_SPI_STATE_ERROR_RX表示接收错误),等待时可直接判断是否处于错误状态,不用额外查硬件标志:

// HAL库:同时判断“超时”和“错误状态”
while (1)
{
    if (HAL_SPI_GetState(W25Q64_pSHTD) == HAL_SPI_STATE_READY)
        break; // 正常完成
    if (HAL_SPI_GetState(W25Q64_pSHTD) == HAL_SPI_STATE_ERROR_RX)
    {
        // 直接知道是接收错误,不用查底层标志
        HAL_SPI_ClearErrorFlags(W25Q64_pSHTD);
        break;
    }
    if(timeout++ > 100000)
    {
        // 超时处理:强制终止传输
        HAL_SPI_Abort(W25Q64_pSHTD);
        break;
    }
}

三、适用场景:选哪个更合适?

场景类型 推荐用标准库 推荐用 HAL 库
开发者对硬件的熟悉程度 熟悉 STM32 DMA/SPI 寄存器,想精细控制 不熟悉硬件细节,想快速开发
项目需求 追求极致性能(无封装开销)、自定义逻辑多 追求开发效率、代码可移植性(换芯片少改)
代码维护成本 维护成本高(需备注硬件细节) 维护成本低(依赖库封装,新人易上手)

四、整体对比表格(汇总版)

对比维度 标准库实现 HAL 库实现
等待对象 DMA 通道的硬件标志位(如TCx SPI 状态机(如HAL_SPI_STATE_READY
抽象层次 低(直接操作硬件寄存器) 高(封装成状态机)
标志位处理 手动清除(DMA_ClearFlag 自动清除(状态机内部处理)
错误处理 需手动判断超时 + 额外查错误标志 状态机直接返回错误类型,判断更简单
核心优点 响应快、灵活性高 易用性高、无需记硬件细节
核心缺点 需记硬件参数、易出错 略多封装开销、灵活性稍低
适用人群 硬件熟悉者、追求极致控制者 新手、追求快速开发 / 可移植性者
posted @ 2025-08-24 19:45  秦瑞迁  阅读(136)  评论(0)    收藏  举报