【自学嵌入式:stm32单片机】硬件SPI读写W25Q64(含用DMA读取W25Q64)
硬件SPI读写W25Q64
接线图
和之前软件SPI接线图一样
我和江科大版本的接线不同,我还是把OLED屏幕接在了硬件I2C2接口上面
SPI1的复用引脚和之前软件SPI的引脚是一样的
SPI1的引脚还可以重定义到:
使用重定义的SPI1引脚,需要先 解除JTDI等调试端口的复用,否则不会正常工作,我们只需要修改MYSPI模块即可,其余不用动
我对江科大教程的代码做了扩展,实现了连续交换数据和用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 */
实现效果
关于为什么标准库是先等待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_RX
到READY
),会自动清除 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 ) |
自动清除(状态机内部处理) |
错误处理 | 需手动判断超时 + 额外查错误标志 | 状态机直接返回错误类型,判断更简单 |
核心优点 | 响应快、灵活性高 | 易用性高、无需记硬件细节 |
核心缺点 | 需记硬件参数、易出错 | 略多封装开销、灵活性稍低 |
适用人群 | 硬件熟悉者、追求极致控制者 | 新手、追求快速开发 / 可移植性者 |