【STM32F429开发板用户手册】第32章 STM32F429的SPI总线应用之驱动W25QXX(支持查询,中断和DMA)
最新教程下载:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255
第32章 STM32F429的SPI总线应用之驱动W25QXX(支持查询,中断和DMA)
本章节为大家讲解标准SPI接线方式驱动W25QXX,实现了查询,中断和DMA三种方式。
32.1 初学者重要提示
32.2 W25QXX硬件设计
32.4 W25QXX关键知识点整理(重要)
32.5 W25QXX驱动设计
32.6 SPI总线板级支持包(bsp_spi_bus.c)
32.7 W25QXX板级支持包(bsp_spi_flash.c)
32.8 使用例程设计框架
32.9 实验例程说明(MDK)
32.10 实验例程说明(IAR)
32.11 总结
32.1 初学者重要提示
- 学习本章节前,务必优先学习第31章。
- W25Q64FV属于NOR型Flash存储芯片。
- W25Q64FV手册下载地址:链接 (这是一个超链接),当前章节配套例子的Doc文件件里面也有存放。
- 本章第3小节整理的知识点比较重要,务必要了解下,特别是页编程和页回卷。
- 对SPI Flash W25QXX的不同接线方式(1线,2线或者4线,这里的线是指的数据线),编程命令是不同的。
- W25Q64FV最高支持104MHz,但最高读命令03H速度是50MHz。
- 文件bsp_spi_bus.c文件公共的总线驱动文件,支持串行FLASH、TSC2046、VS1053、AD7705、ADS1256等SPI设备的配置。
- 函数sf_WriteBuffer不需要用户做擦除,会自动执行擦除功能,支持任意大小,任意地址,不超过芯片容量即可。
32.2 W25QXX硬件设计
STM32F4驱动W25QXX的硬件设计如下:
关于这个原理图,要了解到以下几个知识:
- 当前V6开发板实际外接的芯片是W25Q64FV。
- CS片选最好接上拉电阻,防止意外操作。
- 这里的PB3,PB4和PB5引脚可以复用SPI1,SPI3。实际应用中是复用的SPI1。
- W25Q64的WP引脚用于写保护,低电平有效性,当前是直接高电平。
- HOLD引脚也是低电平有效,当前是将其接到高电平。此引脚的作用是CS片选低电平时,DO引脚输出高阻,忽略CLK和DI引脚上的信号。
32.3 W25QXX关键知识点整理(重要)
驱动W25QXX前要先了解下这个芯片的相关信息。
32.3.1 W25QXX基础信息
- W25Q64FV的容量是8MB(256Mbit)。
- W25Q64FV支持标准SPI(单线SPI),用到引脚CLK、CS,DI和DO引脚。
支持两线SPI,用到引脚CLK、CS、IO0、IO1 。
支持四线SPI,用到引脚CLK、CS、IO0、IO1,IO2、IO3。
(注:这里几线的意思是几个数据线)。
- W25Q64FV支持的最高时钟是133MHz。
- 每个扇区最少支持10万次擦写,可以保存20年数据。
- 页大小是256字节,支持页编程,也就是一次编写256个字节,也可以一个一个编写。
- 支持4KB为单位的扇区擦除,也可以32KB或者64KB为单位的擦除。
整体框图如下:
W25Q64FV:
- 有128个Block,每个Block大小64KB。
- 每个Block有16个Sector,每个Sector大小4KB。
- 每个Sector有16个Page,每个Page大小是256字节。
32.3.2 W25QXX命令
使用W25Q的接线方式不同,使用的命令也有所不同,使用的时候务必要注意,当前我们使用的标准SPI,即单线SPI,使用的命令如下:
当前主要用到如下几个命令:
#define CMD_EWRSR 0x50 /* 允许写状态寄存器的命令 */ #define CMD_WRSR 0x01 /* 写状态寄存器命令 */ #define CMD_WREN 0x06 /* 写使能命令 */ #define CMD_READ 0x03 /* 读数据区命令 */ #define CMD_RDSR 0x05 /* 读状态寄存器命令 */ #define CMD_RDID 0x9F /* 读器件ID命令 */ #define CMD_SE 0x20 /* 擦除扇区命令 */ #define CMD_BE 0xC7 /* 批量擦除命令 */ #define WIP_FLAG 0x01 /* 状态寄存器中的正在编程标志(WIP) */
32.3.3 W25QXX页编程和页回卷
SPI Flash仅支持页编程(页大小256字节),所有其它大批量数据的写入都是以页为单位。这里注意所说的页编程含义,页编程分为以下三步(伪代码):
bsp_spiWrite1(0x02); ----------第1步发送页编程命令 bsp_spiWrite1((_uiWriteAddr & 0xFF0000) >> 16); ----------第2步发送地址 bsp_spiWrite1((_uiWriteAddr & 0xFF00) >> 8); bsp_spiWrite1(_uiWriteAddr & 0xFF); for (i = 0; i < _usSize; i++) { bsp_spiWrite1(*_pBuf++); ----------第3步写数据,此时就可以连续写入数据了, 不需要再重新设置地址,地址会自增。这样可以大大加快写入速度。 }
页编程的含义恰恰就体现在第3步了,如果用户设置的“起始地址+数据长度”所确定的地址范围超过了此起始地址所在的页,地址自增不会超过页范围,而是重新回到了此页的首地进行编写。这一点要特别的注意。如果用户不需要使用地址自增效果,那么直接指定地址进行编写即可。可以任意指定地址进行编写,编写前一定要进行擦除。
比如下面就是页内操作(使用前已经进行了扇区擦除,每次擦除最少擦除一个扇区4KB):
uint8_t tempbuf[10] = {0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88,0x99,0x00}; uint8_t temp1 = 0x10, temp2 = 0x29, temp3 = 0x48;
- 从250地址开始写入10个字节数据 PageWrite(tempbuf, 250, 10);(因为一旦写入超过地址255,就会从0地址开始重新写)。
- 向地址20写入1个字节数据:PageWrite(&temp1, 20, 1);
- 向地址30写入1个字节数据:PageWrite(&temp2, 30, 1);
- 向地址510写入1个字节数据:PageWrite(&temp3, 510, 1) (这里已经是写到下一页了)
下面是将从0地址到511地址读取出来的512个字节数据,一行32字节。
32.3.4 W25QXX扇区擦除
SPI Flash的擦除支持扇区擦除(4KB),块擦除(32KB或者64KB)以及整个芯片擦除。对于扇区擦除和块擦除,使用的时候要注意一点,一般情况下,只需用户给出扇区或者块的首地址即可。
如果给的不是扇区或者块的首地址也没有关系的,只要此地址是在扇区或者块的范围内,此扇区或者块也可以被正确擦除。不过建议使用时给首地址,方便管理。
32.3.5 W25QXX规格参数
这里我们主要了解擦写耗时和支持的时钟速度,下面是擦写时间参数:
- 页编程时间:典型值0.45ms,最大值3ms。
- 扇区擦除时间(4KB):典型值45-60ms,最大值400ms。
- 块擦除时间(32KB):典型值120ms,最大值1600ms。
- 块擦除时间(64KB):典型值150ms,最大值2000ms。
- 整个芯片擦除时间:典型值20s,最大值100s。
支持的速度参数如下:
可以看到最高支持的读时钟(使用命令03H)速度是50MHz,其它命令速度可以做到104MHz。
32.4 W25QXX驱动设计
W25QXX的程序驱动框架设计如下:
有了这个框图,程序设计就比较好理解了。
32.4.1 第1步:SPI总线配置
spi总线配置通过如下两个函数实现:
/* ********************************************************************************************************* * 函 数 名: bsp_InitSPIBus * 功能说明: 配置SPI总线。 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void bsp_InitSPIBus(void) { g_spi_busy = 0; bsp_InitSPIParam(SPI_BAUDRATEPRESCALER_8, SPI_PHASE_1EDGE, SPI_POLARITY_LOW); } /* ********************************************************************************************************* * 函 数 名: bsp_InitSPIParam * 功能说明: 配置SPI总线参数,时钟分频,时钟相位和时钟极性。 * 形 参: _BaudRatePrescaler SPI总线时钟分频设置,支持的参数如下: * SPI_BAUDRATEPRESCALER_2 2分频 * SPI_BAUDRATEPRESCALER_4 4分频 * SPI_BAUDRATEPRESCALER_8 8分频 * SPI_BAUDRATEPRESCALER_16 16分频 * SPI_BAUDRATEPRESCALER_32 32分频 * SPI_BAUDRATEPRESCALER_64 64分频 * SPI_BAUDRATEPRESCALER_128 128分频 * SPI_BAUDRATEPRESCALER_256 256分频 * * _CLKPhase 时钟相位,支持的参数如下: * SPI_PHASE_1EDGE SCK引脚的第1个边沿捕获传输的第1个数据 * SPI_PHASE_2EDGE SCK引脚的第2个边沿捕获传输的第1个数据 * * _CLKPolarity 时钟极性,支持的参数如下: * SPI_POLARITY_LOW SCK引脚在空闲状态处于低电平 * SPI_POLARITY_HIGH SCK引脚在空闲状态处于高电平 * * 返 回 值: 无 ********************************************************************************************************* */ void bsp_InitSPIParam(uint32_t _BaudRatePrescaler, uint32_t _CLKPhase, uint32_t _CLKPolarity) { /* 提高执行效率,只有在SPI硬件参数发生变化时,才执行HAL_Init */ if (s_BaudRatePrescaler == _BaudRatePrescaler && s_CLKPhase == _CLKPhase && s_CLKPolarity == _CLKPolarity) { return; } s_BaudRatePrescaler = _BaudRatePrescaler; s_CLKPhase = _CLKPhase; s_CLKPolarity = _CLKPolarity; /* 设置SPI参数 */ hspi.Instance = SPIx; /* 例化SPI */ hspi.Init.BaudRatePrescaler = _BaudRatePrescaler; /* 设置波特率 */ hspi.Init.Direction = SPI_DIRECTION_2LINES; /* 全双工 */ hspi.Init.CLKPhase = _CLKPhase; /* 配置时钟相位 */ hspi.Init.CLKPolarity = _CLKPolarity; /* 配置时钟极性 */ hspi.Init.DataSize = SPI_DATASIZE_8BIT; /* 设置数据宽度 */ hspi.Init.FirstBit = SPI_FIRSTBIT_MSB; /* 数据传输先传高位 */ hspi.Init.TIMode = SPI_TIMODE_DISABLE; /* 禁止TI模式 */ hspi.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; /* 禁止CRC */ hspi.Init.CRCPolynomial = 7; /* 禁止CRC后,此位无效 */ hspi.Init.NSS = SPI_NSS_SOFT; /* 使用软件方式管理片选引脚 */ hspi.Init.Mode = SPI_MODE_MASTER; /* SPI工作在主控模式 */ /* 复位SPI */ if(HAL_SPI_DeInit(&hspi) != HAL_OK) { Error_Handler(__FILE__, __LINE__); } if (HAL_SPI_Init(&hspi) != HAL_OK) { Error_Handler(__FILE__, __LINE__); } }
关于这两个函数有以下两点要做个说明:
- 函数bsp_InitSPIBus里面的配置是个初始设置。实际驱动芯片时,会通过函数bsp_InitSPIParam做再配置。
- 函数bsp_InitSPIParam提供了时钟分频,时钟相位和时钟极性配置。驱动不同外设芯片时,基本上调整这三个参数就够。当SPI接口上接了多个不同类型的芯片时,通过此函数可以方便的切换配置。
32.4.2 第2步:SPI总线的查询,中断和DMA方式设置
SPI驱动的查询,中断和DMA方式主要通过函数bsp_spiTransfer实现数据传输:
/* ********************************************************************************************************* * 选择DMA,中断或者查询方式 ********************************************************************************************************* */ //#define USE_SPI_DMA /* DMA方式 */ //#define USE_SPI_INT /* 中断方式 */ #define USE_SPI_POLL /* 查询方式 */ uint8_t g_spiTxBuf[SPI_BUFFER_SIZE]; uint8_t g_spiRxBuf[SPI_BUFFER_SIZE]; /* ********************************************************************************************************* * 函 数 名: bsp_spiTransfer * 功能说明: 启动数据传输 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void bsp_spiTransfer(void) { if (g_spiLen > SPI_BUFFER_SIZE) { return; } /* DMA方式传输 */ #ifdef USE_SPI_DMA wTransferState = TRANSFER_WAIT; if(HAL_SPI_TransmitReceive_DMA(&hspi, (uint8_t*)g_spiTxBuf, (uint8_t *)g_spiRxBuf, g_spiLen) != HAL_OK) { Error_Handler(__FILE__, __LINE__); } while (wTransferState == TRANSFER_WAIT) { ; } #endif /* 中断方式传输 */ #ifdef USE_SPI_INT wTransferState = TRANSFER_WAIT; if(HAL_SPI_TransmitReceive_IT(&hspi, (uint8_t*)g_spiTxBuf, (uint8_t *)g_spiRxBuf, g_spiLen) != HAL_OK) { Error_Handler(__FILE__, __LINE__); } while (wTransferState == TRANSFER_WAIT) { ; } #endif /* 查询方式传输 */ #ifdef USE_SPI_POLL if(HAL_SPI_TransmitReceive(&hspi, (uint8_t*)g_spiTxBuf, (uint8_t *)g_spiRxBuf, g_spiLen, 1000000) != HAL_OK) { Error_Handler(__FILE__, __LINE__); } #endif }
通过开头宏定义可以方便的切换中断,查询和DMA方式。
32.4.3 第3步:W25QXX的时钟极性和时钟相位配置
首先回忆下STM32F4支持的4种时序配置。
- 当CPOL = 1, CPHA = 1时
SCK引脚在空闲状态处于高电平,SCK引脚的第2个边沿捕获传输的第1个数据。
- 当CPOL = 0, CPHA = 1时
SCK引脚在空闲状态处于低电平,SCK引脚的第2个边沿捕获传输的第1个数据。
- 当CPOL = 1, CPHA = 0时
SCK引脚在空闲状态处于高电平,SCK引脚的第1个边沿捕获传输的第1个数据。
- 当CPOL = 0, CPHA = 0时
SCK引脚在空闲状态处于低电平,SCK引脚的第1个边沿捕获传输的第1个数据。
有了F4支持的时序配置,再来看下W25Q的时序图:
Mode0 : 空闲状态的sck是低电平。
Mode1 : 空闲状态的sck是高电平。
首先W25Q是上升沿做数据采集,所以STM32F4的可选的配置就是:
CHOL = 1, CPHA = 1
CHOL = 0, CPHA = 0
对于这两种情况,具体选择哪种,继续往下看。W25Q有两种SCK模式,分别是Mode0和Mode3,也就是空闲状态下,SCK既可以是高电平也可以是低电平。这样的话,这两种情况都可以使用,经过实际测试,STM32F4使用这两个配置均可以配置驱动W25Q。
32.4.4 第4步:单SPI接口管理多个SPI设备的切换机制
单SPI接口管理多个SPI设备最麻烦的地方是不同设备的时钟分配,时钟极性和时钟相位并不相同。对此的解决解决办法是在片选阶段配置切换,比如SPI Flash的片选:
/* ********************************************************************************************************* * 函 数 名: sf_SetCS * 功能说明: 串行FALSH片选控制函数 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void sf_SetCS(uint8_t _Level) { if (_Level == 0) { bsp_SpiBusEnter(); bsp_InitSPIParam(SPI_BAUDRATEPRESCALER_2, SPI_PHASE_1EDGE, SPI_POLARITY_LOW); SF_CS_0(); } else { SF_CS_1(); bsp_SpiBusExit(); } }
通过这种方式就有效的解决了单SPI接口管理多设备的问题。因为给每个设备都配了一个独立的片选引脚,这样就可以为每个设备都配置这么一个片选配置。
但是频繁配置也比较繁琐,所以函数bsp_InitSPIParam里面做了特别处理。当前配置与之前配置相同的情况下无需重复配置。
32.4.5 第5步:W25QXX的读取实现
W25QXX的读取功能比较好实现,发送03H命令后,设置任意地址都可以读取数据,只要不超过芯片容量即可。
/* ********************************************************************************************************* * 函 数 名: sf_ReadBuffer * 功能说明: 连续读取若干字节,字节个数不能超出芯片容量。 * 形 参: _pBuf : 数据源缓冲区; * _uiReadAddr :首地址 * _usSize :数据个数, 不能超出芯片总容量 * 返 回 值: 无 ********************************************************************************************************* */ void sf_ReadBuffer(uint8_t * _pBuf, uint32_t _uiReadAddr, uint32_t _uiSize) { uint16_t rem; uint16_t i; /* 如果读取的数据长度为0或者超出串行Flash地址空间,则直接返回 */ if ((_uiSize == 0) ||(_uiReadAddr + _uiSize) > g_tSF.TotalSize) { return; } /* 擦除扇区操作 */ sf_SetCS(0); /* 使能片选 */ g_spiLen = 0; g_spiTxBuf[g_spiLen++] = (CMD_READ); /* 发送读命令 */ g_spiTxBuf[g_spiLen++] = ((_uiReadAddr & 0xFF0000) >> 16); /* 发送扇区地址的高8bit */ g_spiTxBuf[g_spiLen++] = ((_uiReadAddr & 0xFF00) >> 8); /* 发送扇区地址中间8bit */ g_spiTxBuf[g_spiLen++] = (_uiReadAddr & 0xFF); /* 发送扇区地址低8bit */ bsp_spiTransfer(); /* 开始读数据,因为底层DMA缓冲区有限,必须分包读 */ for (i = 0; i < _uiSize / SPI_BUFFER_SIZE; i++) { g_spiLen = SPI_BUFFER_SIZE; bsp_spiTransfer(); memcpy(_pBuf, g_spiRxBuf, SPI_BUFFER_SIZE); _pBuf += SPI_BUFFER_SIZE; } rem = _uiSize % SPI_BUFFER_SIZE; /* 剩余字节 */ if (rem > 0) { g_spiLen = rem; bsp_spiTransfer(); memcpy(_pBuf, g_spiRxBuf, rem); } sf_SetCS(1); /* 禁能片选 */ }
这个函数对DMA传输做了特别处理,方便分包进行。
32.4.6 第6步:W25QXX的扇区擦除实现
扇区擦除的实现也比较简单,发送“扇区擦除命令+扇区地址”即可完成相应扇区的擦除。擦除的扇区大小是4KB。
/* ********************************************************************************************************* * 函 数 名: sf_EraseSector * 功能说明: 擦除指定的扇区 * 形 参: _uiSectorAddr : 扇区地址 * 返 回 值: 无 ********************************************************************************************************* */ void sf_EraseSector(uint32_t _uiSectorAddr) { sf_WriteEnable(); /* 发送写使能命令 */ /* 擦除扇区操作 */ sf_SetCS(0); /* 使能片选 */ g_spiLen = 0; g_spiTxBuf[g_spiLen++] = CMD_SE; /* 发送擦除命令 */ g_spiTxBuf[g_spiLen++] = ((_uiSectorAddr & 0xFF0000) >> 16); /* 发送扇区地址的高8bit */ g_spiTxBuf[g_spiLen++] = ((_uiSectorAddr & 0xFF00) >> 8); /* 发送扇区地址中间8bit */ g_spiTxBuf[g_spiLen++] = (_uiSectorAddr & 0xFF); /* 发送扇区地址低8bit */ bsp_spiTransfer(); sf_SetCS(1); /* 禁能片选 */ sf_WaitForWriteEnd(); /* 等待串行Flash内部写操作完成 */ }
整个芯片的擦除更省事些,仅发送整个芯片擦除命令即可:
/* ********************************************************************************************************* * 函 数 名: sf_EraseChip * 功能说明: 擦除整个芯片 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void sf_EraseChip(void) { sf_WriteEnable(); /* 发送写使能命令 */ /* 擦除扇区操作 */ sf_SetCS(0); /* 使能片选 */ g_spiLen = 0; g_spiTxBuf[g_spiLen++] = CMD_BE; /* 发送整片擦除命令 */ bsp_spiTransfer(); sf_SetCS(1); /* 禁能片选 */ sf_WaitForWriteEnd(); /* 等待串行Flash内部写操作完成 */ }
32.4.7 第7步:W25QXX的编程实现
W25QXX的编程实现略复杂,因为做了自动擦除支持,大家可以在任意地址,写任意大小的数据,只要不超过芯片容量即可。我们这里就不做展开讨论了,大家有兴趣可以研究下:
/* ********************************************************************************************************* * 函 数 名: sf_WriteBuffer * 功能说明: 写1个扇区并校验,如果不正确则再重写两次,本函数自动完成擦除操作。 * 形 参: _pBuf : 数据源缓冲区; * _uiWrAddr :目标区域首地址 * _usSize :数据个数,任意大小,但不能超过芯片容量。 * 返 回 值: 1 : 成功, 0 : 失败 ********************************************************************************************************* */ uint8_t sf_WriteBuffer(uint8_t* _pBuf, uint32_t _uiWriteAddr, uint32_t _usWriteSize) { uint32_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0; Addr = _uiWriteAddr % g_tSF.SectorSize; count = g_tSF.SectorSize - Addr; NumOfPage = _usWriteSize / g_tSF.SectorSize; NumOfSingle = _usWriteSize % g_tSF.SectorSize; if (Addr == 0) /* 起始地址是扇区首地址 */ { if (NumOfPage == 0) /* 数据长度小于扇区大小 */ { if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, _usWriteSize) == 0) { return 0; } } else /* 数据长度大于等于扇区大小 */ { while (NumOfPage--) { if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, g_tSF.SectorSize) == 0) { return 0; } _uiWriteAddr += g_tSF.SectorSize; _pBuf += g_tSF.SectorSize; } if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, NumOfSingle) == 0) { return 0; } } } else /* 起始地址不是扇区首地址 */ { if (NumOfPage == 0) /* 数据长度小于扇区大小 */ { if (NumOfSingle > count) /* (_usWriteSize + _uiWriteAddr) > SPI_FLASH_PAGESIZE */ { temp = NumOfSingle - count; if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, count) == 0) { return 0; } _uiWriteAddr += count; _pBuf += count; if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, temp) == 0) { return 0; } } else { if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, _usWriteSize) == 0) { return 0; } } } else /* 数据长度大于等于扇区大小 */ { _usWriteSize -= count; NumOfPage = _usWriteSize / g_tSF.SectorSize; NumOfSingle = _usWriteSize % g_tSF.SectorSize; if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, count) == 0) { return 0; } _uiWriteAddr += count; _pBuf += count; while (NumOfPage--) { if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, g_tSF.SectorSize) == 0) { return 0; } _uiWriteAddr += g_tSF.SectorSize; _pBuf += g_tSF.SectorSize; } if (NumOfSingle != 0) { if (sf_AutoWriteSector(_pBuf, _uiWriteAddr, NumOfSingle) == 0) { return 0; } } } } return 1; /* 成功 */ }
32.5 SPI总线板级支持包(bsp_spi_bus.c)
SPI总线驱动文件bsp_spi_bus.c主要实现了如下几个API供用户调用:
- bsp_InitSPIBus
- bsp_InitSPIParam
- bsp_spiTransfer
32.5.1 函数bsp_InitSPIBus
函数原型:
void bsp_InitSPIBus(void)
函数描述:
此函数主要用于SPI总线的初始化,在bsp.c文件调用一次即可。
32.5.2 函数bsp_InitSPIParam
函数原型:
void bsp_InitSPIParam(uint32_t _BaudRatePrescaler, uint32_t _CLKPhase, uint32_t _CLKPolarity)
函数描述:
此函数用于SPI总线的配置。
函数参数:
- 第1个参数SPI总线的分频设置,支持的参数如下:
SPI_BAUDRATEPRESCALER_2 2分频
SPI_BAUDRATEPRESCALER_4 4分频
SPI_BAUDRATEPRESCALER_8 8分频
SPI_BAUDRATEPRESCALER_16 16分频
SPI_BAUDRATEPRESCALER_32 32分频
SPI_BAUDRATEPRESCALER_64 64分频
SPI_BAUDRATEPRESCALER_128 128分频
SPI_BAUDRATEPRESCALER_256 256分频
- 第2个参数用于时钟相位配置,支持的参数如下:
SPI_PHASE_1EDGE SCK引脚的第1个边沿捕获传输的第1个数据
SPI_PHASE_2EDGE SCK引脚的第2个边沿捕获传输的第1个数据
- 第3个参数是时钟极性配置,支持的参数如下:
SPI_POLARITY_LOW SCK引脚在空闲状态处于低电平
SPI_POLARITY_HIGH SCK引脚在空闲状态处于高电平
32.5.3 函数bsp_spiTransfer
函数原型:
void bsp_spiTransfer(void)
函数描述:
此函数用于启动SPI数据传输,支持查询,中断和DMA方式传输。
32.6 W25QXX板级支持包(bsp_spi_flash.c)
W25QXX驱动文件bsp_spi_flash.c主要实现了如下几个API供用户调用:
- sf_ReadBuffer
- sf_WriteBuffer
- sf_EraseSector
- sf_EraseChip
- sf_EraseSector
32.6.1 函数sf_ReadBuffer
函数原型:
void sf_ReadBuffer(uint8_t * _pBuf, uint32_t _uiReadAddr, uint32_t _uiSize)
函数描述:
此函数主要用于从SPI Flash读取数据,支持任意大小,任意地址,不超过芯片容量即可。
函数参数:
- 第1个参数用于存储从SPI Flash读取的数据。
- 第2个参数是读取地址,不可以超过芯片容量。
- 第3个参数是读取的数据大小,读取范围不可以超过芯片容量。
32.6.2 函数sf_WriteBuffer(自动执行擦除)
函数原型:
uint8_t sf_WriteBuffer(uint8_t* _pBuf, uint32_t _uiWriteAddr, uint32_t _usWriteSize)
函数描述:
此函数主要用于SPI Flash读取数据,支持任意大小,任意地址,不超过芯片容量即可。特别注意,此函数会自动执行擦除,无需用户处理。
函数参数:
- 第1个参数是源数据缓冲区。
- 第2个参数是目标区域首地址。
- 第3个参数是数据个数,支持任意大小,但不能超过芯片容量,单位字节个数。
- 返回值,返回1表示成功,返回0表示失败。
32.6.3 函数sf_EraseSector
函数原型:
void sf_EraseSector(uint32_t _uiSectorAddr)
函数描述:
此函数主要用于扇区擦除,一个扇区大小是4KB。
函数参数:
- 第1个参数是扇区地址,比如擦除扇区0,此处填0x0000,擦除扇区1,此处填0x1000,擦除扇区2,此处填0x2000,以此类推。
32.6.4 函数sf_EraseChip
函数原型:
void sf_EraseChip(void)
函数描述:
此函数主要用于整个芯片擦除。
32.6.5 函数sf_PageWrite(不推荐)
函数原型:
void sf_PageWrite(uint8_t * _pBuf, uint32_t _uiWriteAddr, uint16_t _usSize)
函数描述:
此函数主要用于页编程,一次可以编程多个页,只要不超过芯片容量即可。不推荐大家调用此函数,因为调用这个函数前,需要大家调用函数sf_EraseSector进行扇区擦除。
函数参数:
- 第1个参数是数据源缓冲区。
- 第2个参数目标区域首地址,比如编程页0,此处填0x0000,编程页1,此处填0x0100,编程页2,此处填0x0200,以此类推。
- 第3个参数是编程的数据大小,务必是256字节的整数倍,单位字节个数。
32.7 W25QXX驱动移植和使用
W25QXX移植步骤如下:
- 第1步:复制bsp_spi_bus.c,bsp_spi_bus.h,bsp_spi_flash.c,bsp_spi_flash.h到自己的工程目录,并添加到工程里面。
- 第2步:根据使用的第几个SPI,SPI时钟,SPI引脚和DMA通道等,修改bsp_spi_bus.c文件开头的宏定义。
/* ********************************************************************************************************* * 时钟,引脚,DMA,中断等宏定义 ********************************************************************************************************* */ #define SPIx SPI1 #define SPIx_CLK_ENABLE() __HAL_RCC_SPI1_CLK_ENABLE() #define DMAx_CLK_ENABLE() __HAL_RCC_DMA2_CLK_ENABLE() #define SPIx_FORCE_RESET() __HAL_RCC_SPI1_FORCE_RESET() #define SPIx_RELEASE_RESET() __HAL_RCC_SPI1_RELEASE_RESET() #define SPIx_SCK_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE() #define SPIx_SCK_GPIO GPIOB #define SPIx_SCK_PIN GPIO_PIN_3 #define SPIx_SCK_AF GPIO_AF5_SPI1 #define SPIx_MISO_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE() #define SPIx_MISO_GPIO GPIOB #define SPIx_MISO_PIN GPIO_PIN_4 #define SPIx_MISO_AF GPIO_AF5_SPI1 #define SPIx_MOSI_CLK_ENABLE() __HAL_RCC_GPIOB_CLK_ENABLE() #define SPIx_MOSI_GPIO GPIOB #define SPIx_MOSI_PIN GPIO_PIN_5 #define SPIx_MOSI_AF GPIO_AF5_SPI1 #define SPIx_TX_DMA_CHANNEL DMA_CHANNEL_3 #define SPIx_TX_DMA_STREAM DMA2_Stream3 #define SPIx_RX_DMA_CHANNEL DMA_CHANNEL_3 #define SPIx_RX_DMA_STREAM DMA2_Stream0 #define SPIx_IRQn SPI1_IRQn #define SPIx_IRQHandler SPI1_IRQHandler #define SPIx_DMA_TX_IRQn DMA2_Stream3_IRQn #define SPIx_DMA_RX_IRQn DMA2_Stream0_IRQn #define SPIx_DMA_TX_IRQHandler DMA2_Stream3_IRQHandler #define SPIx_DMA_RX_IRQHandler DMA2_Stream0_IRQHandler
- 第3步:根据使用的SPI ID,添加定义到文件bsp_spi_flash.h。
/* 定义串行Flash ID */ enum { SST25VF016B_ID = 0xBF2541, MX25L1606E_ID = 0xC22015, W25Q64BV_ID = 0xEF4017, /* BV, JV, FV */ W25Q128_ID = 0xEF4018 };
- 第4步:添加相应型号到bsp_spi_flash.c文件的函数sf_ReadInfo里面。
/* ********************************************************************************************************* * 函 数 名: sf_ReadInfo * 功能说明: 读取器件ID,并填充器件参数 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void sf_ReadInfo(void) { /* 自动识别串行Flash型号 */ { g_tSF.ChipID = sf_ReadID(); /* 芯片ID */ switch (g_tSF.ChipID) { case SST25VF016B_ID: strcpy(g_tSF.ChipName, "SST25VF016B"); g_tSF.TotalSize = 2 * 1024 * 1024; /* 总容量 = 2M */ g_tSF.SectorSize = 4 * 1024; /* 扇区大小 = 4K */ break; case MX25L1606E_ID: strcpy(g_tSF.ChipName, "MX25L1606E"); g_tSF.TotalSize = 2 * 1024 * 1024; /* 总容量 = 2M */ g_tSF.SectorSize = 4 * 1024; /* 扇区大小 = 4K */ break; case W25Q64BV_ID: strcpy(g_tSF.ChipName, "W25Q64"); g_tSF.TotalSize = 8 * 1024 * 1024; /* 总容量 = 8M */ g_tSF.SectorSize = 4 * 1024; /* 扇区大小 = 4K */ break; case W25Q128_ID: strcpy(g_tSF.ChipName, "W25Q128"); g_tSF.TotalSize = 16 * 1024 * 1024; /* 总容量 = 8M */ g_tSF.SectorSize = 4 * 1024; /* 扇区大小 = 4K */ break; default: strcpy(g_tSF.ChipName, "Unknow Flash"); g_tSF.TotalSize = 2 * 1024 * 1024; g_tSF.SectorSize = 4 * 1024; break; } } }
- 第5步:根据芯片支持的时钟速度,时钟相位和时钟极性配置函数sf_SetCS。
/* ********************************************************************************************************* * 函 数 名: sf_SetCS * 功能说明: 串行FALSH片选控制函数 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void sf_SetCS(uint8_t _Level) { if (_Level == 0) { bsp_SpiBusEnter(); bsp_InitSPIParam(SPI_BAUDRATEPRESCALER_2, SPI_PHASE_1EDGE, SPI_POLARITY_LOW); SF_CS_0(); } else { SF_CS_1(); bsp_SpiBusExit(); } }
- 第6步:根据使用的SPI Flash片选引脚修改bsp_spi_bus.c文件开头的宏定义。
/* 串行Flash的片选GPIO端口, PD13 */ #define SF_CS_CLK_ENABLE() __HAL_RCC_GPIOD_CLK_ENABLE() #define SF_CS_GPIO GPIOD #define SF_CS_PIN GPIO_PIN_13 #define SF_CS_0() SF_CS_GPIO->BSRR = ((uint32_t)SF_CS_PIN << 16U) #define SF_CS_1() SF_CS_GPIO->BSRR = SF_CS_PIN
- 第7步:初始化SPI。
/* 针对不同的应用程序,添加需要的底层驱动模块初始化函数 */ bsp_InitSPIBus(); /* 配置SPI总线 */ bsp_InitSFlash(); /* 初始化SPI 串行Flash */
- 第8步:SPI Flash驱动主要用到HAL库的SPI驱动文件,简单省事些可以添加所有HAL库C源文件进来。
- 第9步:应用方法看本章节配套例子即可。
32.8 实验例程设计框架
通过程序设计框架,让大家先对配套例程有一个全面的认识,然后再理解细节,本次实验例程的设计框架如下:
第1阶段,上电启动阶段:
- 这部分在第14章进行了详细说明。
第2阶段,进入main函数:
- 第1部分,硬件初始化,主要是MPU,Cache,HAL库,系统时钟,滴答定时器和LED。
- 第2部分,应用程序设计部分,实现SPI Flash的中断,查询和DMA方式操作。
32.9 实验例程说明(MDK)
配套例子:
V6-011_串行SPI Flash W25QXX读写例程(查询方式)
V6-012_串行SPI Flash W25QXX读写例程(中断方式)
V6-013_串行SPI Flash W25QXX读写例程(DMA方式)
实验目的:
- 学习SPI Flash的读写实现,支持查询,中断和DMA方式。
实验操作:
- 支持以下7个功能,用户通过电脑端串口软件发送命令给开发板即可
- printf("请选择操作命令:\r\n");
- printf("【1 - 读串行Flash, 地址:0x%X,长度:%d字节】\r\n", TEST_ADDR, TEST_SIZE);
- printf("【2 - 写串行Flash, 地址:0x%X,长度:%d字节】\r\n", TEST_ADDR, TEST_SIZE);
- printf("【3 - 擦除整个串行Flash】\r\n");
- printf("【4 - 写整个串行Flash, 全0x55】\r\n");
- printf("【5 - 读整个串行Flash, 测试读速度】\r\n");
- printf("【Z - 读取前1K,地址自动减少】\r\n");
- printf("【X - 读取后1K,地址自动增加】\r\n");
- printf("其他任意键 - 显示命令提示\r\n");
上电后串口打印的信息:
波特率 115200,数据位 8,奇偶校验位无,停止位 1。
程序设计:
系统栈大小分配:
硬件外设初始化
硬件外设的初始化是在 bsp.c 文件实现:
/* ********************************************************************************************************* * 函 数 名: bsp_Init * 功能说明: 初始化所有的硬件设备。该函数配置CPU寄存器和外设的寄存器并初始化一些全局变量。只需要调用一次 * 形 参:无0 * 返 回 值: 无 ********************************************************************************************************* */ void bsp_Init(void) { /* STM32H429 HAL 库初始化,此时系统用的还是F429自带的16MHz,HSI时钟: - 调用函数HAL_InitTick,初始化滴答时钟中断1ms。 - 设置NVIV优先级分组为4。 */ HAL_Init(); /* 配置系统时钟到168MHz - 切换使用HSE。 - 此函数会更新全局变量SystemCoreClock,并重新配置HAL_InitTick。 */ SystemClock_Config(); /* Event Recorder: - 可用于代码执行时间测量,MDK5.25及其以上版本才支持,IAR不支持。 - 默认不开启,如果要使能此选项,务必看V5开发板用户手册第8章 */ #if Enable_EventRecorder == 1 /* 初始化EventRecorder并开启 */ EventRecorderInitialize(EventRecordAll, 1U); EventRecorderStart(); #endif bsp_InitKey(); /* 按键初始化,要放在滴答定时器之前,因为按钮检测是通过滴答定时器扫描 */ bsp_InitTimer(); /* 初始化滴答定时器 */ bsp_InitUart(); /* 初始化串口 */ bsp_InitExtIO(); /* 初始化扩展IO */ bsp_InitLed(); /* 初始化LED */ BEEP_InitHard(); /* 初始化蜂鸣器 */ /* 针对不同的应用程序,添加需要的底层驱动模块初始化函数 */ bsp_InitSPIBus(); /* 配置SPI总线 */ bsp_InitSFlash(); /* 初始化SPI 串行Flash */ }
主功能:
主程序实现如下操作:
- 启动一个自动重装软件定时器,每100ms翻转一次LED2。
- 支持以下7个功能,用户通过电脑端串口软件发送命令给开发板即可
- 请选择操作命令:
- 1 - 读串行Flash
- 2 - 写串行Flash
- 3 - 擦除整个串行Flash
- 4 - 写整个串行Flash
- 5 - 读整个串行Flash
- Z - 读取前1K
- X - 读取后1K
/* ********************************************************************************************************* * 函 数 名: DemoSpiFlash * 功能说明: 串行EEPROM读写例程 * 形 参:无 * 返 回 值: 无 ********************************************************************************************************* */ void DemoSpiFlash(void) { uint8_t cmd; uint32_t uiReadPageNo = 0; /* 检测串行Flash OK */ printf("检测到串行Flash, ID = %08X, 型号: %s \r\n", g_tSF.ChipID , g_tSF.ChipName); printf(" 容量 : %dM字节, 扇区大小 : %d字节\r\n", g_tSF.TotalSize/(1024*1024), g_tSF.SectorSize); sfDispMenu(); /* 打印命令提示 */ bsp_StartAutoTimer(0, 100); /* 启动1个100ms的自动重装的定时器 */ while(1) { bsp_Idle(); /* 这个函数在bsp.c文件。用户可以修改这个函数实现CPU休眠和喂狗 */ /* 判断定时器超时时间 */ if (bsp_CheckTimer(0)) { /* 每隔100ms 进来一次 */ bsp_LedToggle(2); } if (comGetChar(COM1, &cmd)) /* 从串口读入一个字符(非阻塞方式) */ { switch (cmd) { case '1': printf("\r\n【1 - 读串行Flash, 地址:0x%X,长度:%d字节】\r\n", TEST_ADDR, TEST_SIZE); sfReadTest(); /* 读串行Flash数据,并打印出来数据内容 */ break; case '2': printf("\r\n【2 - 写串行Flash, 地址:0x%X,长度:%d字节】\r\n", TEST_ADDR, TEST_SIZE); sfWriteTest(); /* 写串行Flash数据,并打印写入速度 */ break; case '3': printf("\r\n【3 - 擦除整个串行Flash】\r\n"); printf("整个Flash擦除完毕大概需要20秒左右,请耐心等待"); sfErase(); /* 擦除串行Flash数据,实际上就是写入全0xFF */ break; case '4': printf("\r\n【4 - 写整个串行Flash, 全0x55】\r\n"); printf("整个Flash写入完毕大概需要20秒左右,请耐心等待"); sfWriteAll(0x55);/* 擦除串行Flash数据,实际上就是写入全0xFF */ break; case '5': printf("\r\n【5 - 读整个串行Flash, %dM字节】\r\n", g_tSF.TotalSize/(1024*1024)); sfTestReadSpeed(); /* 读整个串行Flash数据,测试速度 */ break; case 'z': case 'Z': /* 读取前1K */ if (uiReadPageNo > 0) { uiReadPageNo--; } else { printf("已经是最前\r\n"); } sfViewData(uiReadPageNo * 1024); break; case 'x': case 'X': /* 读取后1K */ if (uiReadPageNo < g_tSF.TotalSize / 1024 - 1) { uiReadPageNo++; } else { printf("已经是最后\r\n"); } sfViewData(uiReadPageNo * 1024); break; default: sfDispMenu(); /* 无效命令,重新打印命令提示 */ break; } } } }
32.10 实验例程说明(IAR)
配套例子:
V6-011_串行SPI Flash W25QXX读写例程(查询方式)
V6-012_串行SPI Flash W25QXX读写例程(中断方式)
V6-013_串行SPI Flash W25QXX读写例程(DMA方式)
实验目的:
- 学习SPI Flash的读写实现,支持查询,中断和DMA方式。
实验操作:
- 支持以下7个功能,用户通过电脑端串口软件发送命令给开发板即可
- printf("请选择操作命令:\r\n");
- printf("【1 - 读串行Flash, 地址:0x%X,长度:%d字节】\r\n", TEST_ADDR, TEST_SIZE);
- printf("【2 - 写串行Flash, 地址:0x%X,长度:%d字节】\r\n", TEST_ADDR, TEST_SIZE);
- printf("【3 - 擦除整个串行Flash】\r\n");
- printf("【4 - 写整个串行Flash, 全0x55】\r\n");
- printf("【5 - 读整个串行Flash, 测试读速度】\r\n");
- printf("【Z - 读取前1K,地址自动减少】\r\n");
- printf("【X - 读取后1K,地址自动增加】\r\n");
- printf("其他任意键 - 显示命令提示\r\n");
上电后串口打印的信息:
波特率 115200,数据位 8,奇偶校验位无,停止位 1。
程序设计:
系统栈大小分配:
硬件外设初始化
硬件外设的初始化是在 bsp.c 文件实现:
/* ********************************************************************************************************* * 函 数 名: bsp_Init * 功能说明: 初始化所有的硬件设备。该函数配置CPU寄存器和外设的寄存器并初始化一些全局变量。只需要调用一次 * 形 参:无0 * 返 回 值: 无 ********************************************************************************************************* */ void bsp_Init(void) { /* STM32H429 HAL 库初始化,此时系统用的还是F429自带的16MHz,HSI时钟: - 调用函数HAL_InitTick,初始化滴答时钟中断1ms。 - 设置NVIV优先级分组为4。 */ HAL_Init(); /* 配置系统时钟到168MHz - 切换使用HSE。 - 此函数会更新全局变量SystemCoreClock,并重新配置HAL_InitTick。 */ SystemClock_Config(); /* Event Recorder: - 可用于代码执行时间测量,MDK5.25及其以上版本才支持,IAR不支持。 - 默认不开启,如果要使能此选项,务必看V5开发板用户手册第8章 */ #if Enable_EventRecorder == 1 /* 初始化EventRecorder并开启 */ EventRecorderInitialize(EventRecordAll, 1U); EventRecorderStart(); #endif bsp_InitKey(); /* 按键初始化,要放在滴答定时器之前,因为按钮检测是通过滴答定时器扫描 */ bsp_InitTimer(); /* 初始化滴答定时器 */ bsp_InitUart(); /* 初始化串口 */ bsp_InitExtIO(); /* 初始化扩展IO */ bsp_InitLed(); /* 初始化LED */ BEEP_InitHard(); /* 初始化蜂鸣器 */ }
主功能:
主程序实现如下操作:
- 启动一个自动重装软件定时器,每100ms翻转一次LED2。
- 支持以下7个功能,用户通过电脑端串口软件发送命令给开发板即可
- 请选择操作命令:
- 1 - 读串行Flash
- 2 - 写串行Flash
- 3 - 擦除整个串行Flash
- 4 - 写整个串行Flash
- 5 - 读整个串行Flash
- Z - 读取前1K
- X - 读取后1K
/* ********************************************************************************************************* * 函 数 名: DemoSpiFlash * 功能说明: 串行EEPROM读写例程 * 形 参:无 * 返 回 值: 无 ********************************************************************************************************* */ void DemoSpiFlash(void) { uint8_t cmd; uint32_t uiReadPageNo = 0; /* 检测串行Flash OK */ printf("检测到串行Flash, ID = %08X, 型号: %s \r\n", g_tSF.ChipID , g_tSF.ChipName); printf(" 容量 : %dM字节, 扇区大小 : %d字节\r\n", g_tSF.TotalSize/(1024*1024), g_tSF.SectorSize); sfDispMenu(); /* 打印命令提示 */ bsp_StartAutoTimer(0, 100); /* 启动1个100ms的自动重装的定时器 */ while(1) { bsp_Idle(); /* 这个函数在bsp.c文件。用户可以修改这个函数实现CPU休眠和喂狗 */ /* 判断定时器超时时间 */ if (bsp_CheckTimer(0)) { /* 每隔100ms 进来一次 */ bsp_LedToggle(2); } if (comGetChar(COM1, &cmd)) /* 从串口读入一个字符(非阻塞方式) */ { switch (cmd) { case '1': printf("\r\n【1 - 读串行Flash, 地址:0x%X,长度:%d字节】\r\n", TEST_ADDR, TEST_SIZE); sfReadTest(); /* 读串行Flash数据,并打印出来数据内容 */ break; case '2': printf("\r\n【2 - 写串行Flash, 地址:0x%X,长度:%d字节】\r\n", TEST_ADDR, TEST_SIZE); sfWriteTest(); /* 写串行Flash数据,并打印写入速度 */ break; case '3': printf("\r\n【3 - 擦除整个串行Flash】\r\n"); printf("整个Flash擦除完毕大概需要20秒左右,请耐心等待"); sfErase(); /* 擦除串行Flash数据,实际上就是写入全0xFF */ break; case '4': printf("\r\n【4 - 写整个串行Flash, 全0x55】\r\n"); printf("整个Flash写入完毕大概需要20秒左右,请耐心等待"); sfWriteAll(0x55);/* 擦除串行Flash数据,实际上就是写入全0xFF */ break; case '5': printf("\r\n【5 - 读整个串行Flash, %dM字节】\r\n", g_tSF.TotalSize/(1024*1024)); sfTestReadSpeed(); /* 读整个串行Flash数据,测试速度 */ break; case 'z': case 'Z': /* 读取前1K */ if (uiReadPageNo > 0) { uiReadPageNo--; } else { printf("已经是最前\r\n"); } sfViewData(uiReadPageNo * 1024); break; case 'x': case 'X': /* 读取后1K */ if (uiReadPageNo < g_tSF.TotalSize / 1024 - 1) { uiReadPageNo++; } else { printf("已经是最后\r\n"); } sfViewData(uiReadPageNo * 1024); break; default: sfDispMenu(); /* 无效命令,重新打印命令提示 */ break; } } } }
32.11 总结
本章节就为大家讲解这么多,实际应用中根据需要选择DMA,中断和查询方式。