一、项目背景
1.1 本专栏背景:
(1)STM32 CUBE MX 软件的选用:
对于毕业以后很久没有碰过STM32代码编写的码农来说,如果按照传统的HAL库编写单片机程序费时费力。哪怕是仅仅用裸机跑功能,不仅仅需要重新阅读数据手册和开发板厂家的教程,而且还基于厂家例程改写引脚、外设参数等等。出于项目周期、任务达成两方面考量,为快速完成可供芯片功能、性能测试用途的STM32 demo工程代码,可考虑使用STM32 CUBE MX 软件进行快速代码生成,配合开发板相关手册、网上教程进行排障。(STM32CUBE MX官方下载地址)
(2)RTOS的应用
基于项目多任务实时处理的需求,在项目中添加FREE_RTOS操作系统。借助STM32 CUBE MX 软件和操作系统配套官方配件包(配件包官方下载地址)进行快速移植、配置。
一方面,在公司做项目不会有充裕的时间像在学校那样慢慢啃视频教程或者教程书籍,主要的特点是快速完成一个demo工程实现基本的功能如点灯,在此基础上增加新的功能和需求,遇到新的问题通过论坛或者请教他人解决;另一方面,之前学生时代简单上手STM32工程进行实验,没有深究其中的众多机制(书到用时方恨少啊…),故在此整理从0开始完成功能开发的过程,不仅供日后编者自己查阅,也供有类似需求的开发者参考,抛砖引玉,如有误请大佬指正。
如果读者参考本文的操作遇到其他问题或者对文中的操作有疑问和建议欢迎在评论区与我交流。
文中所用的软件大多通过ST官网或者野火/正点原子等开发板厂家可以获得,我尽量将软件贴在文章中免费下载,如有需要的读者可以联系我免费获取(编者自己也是厌倦了各种会员和关注微信下载的套路,故所有资源无门槛无套路联系我即可获得,愿与各位一起进步!)
(无广)需要相关芯片资料可通过该网站查找下载半导小芯。
1.2 本项目背景
现有一项目,需使用F429芯片通过SPI通信协议对某一芯片(称为B芯片)进行读/写操作,编者的任务是编写该单片机控制代码实现对B芯片的通信、控制。SPI通信协议是在嵌入式开发中常见、常用的通信协议。掌握单片机的SPI控制代码快速编写和debug是一项基本技能。(编者上周被同事吐槽说在学校学习的基本功不扎实…)
本测试demo的调试即使借助STM32Cube MX、参考了野火等网上例程,自己仍然花了数天时间定位问题。我认为这些努力是值得的:
(1)在编写驱动中去加深对SPI通信机制、芯片工作原理以及HAL标准接口的理解;
(2)尽管有的人觉得不需要掌握STM32Cube MX也能轻松编写基于HAL库的FreeRTOS工程,但是认为掌握STM32Cube MX能够使你在没有模板和代码时,仅仅借助自动生成和数据手册编写基本的芯片驱动。
二、本次开发的环境准备
2.1 硬件环境
本次开发使用F103开发板以及配套的JTAG debugger。开发板通过debugger和USB串口线连接电脑,开发板上主控芯片F103VET6通过四个引脚连接W25Q64芯片。连接示意图如下
关于SPI协议和FLASH芯片,网上讲解的资料有很多,在此不再赘述。
2.2 软件环境
如本专栏之前文章所述,使用STM32 Cube MX进行代码生成,使用Keil进行代码修改和编译。串口调试工具选择具有基本功能的即可,无特别限制。
本文章代码参考野火指南者开发板中的例程,所需的代码会在文中展示。
三、开发过程
3.1 生成代码
初次生成代码的基本操作参考编者文章串口工程的快速生成
在完成以上操作基础上,增加以下操作:
配置SPI1相关的引脚以及通信参数。
3.1.1 配置SPI1的时钟线、数据线
开发注意事项:
(1)开发者在拿到一个没有接触过的芯片时,如果有芯片厂家或是其他渠道获得的参考例程,请仔细按照例程配置Parameter Settings中的各项参数,或者是在生成代码后,找到spi.c以及spi.h,对SPI外设初始化参数进行仔细核对,以下是野火提供的驱动代码
void MX_SPI1_Init(void)
{
/* USER CODE BEGIN SPI1_Init 0 */
/* USER CODE END SPI1_Init 0 */
/* USER CODE BEGIN SPI1_Init 1 */
/* USER CODE END SPI1_Init 1 */
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH;
hspi1.Init.CLKPhase = SPI_PHASE_2EDGE;
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi1.Init.CRCPolynomial = 10;
if (HAL_SPI_Init(&hspi1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN SPI1_Init 2 */
/* USER CODE END SPI1_Init 2 */
}
在main.c中需要调用void MX_SPI1_Init(void)函数对外设进行初始化。
(2)如果开发者只有芯片手册(比如你没有参考代码,手上只有W25Q64芯片手册),需要重点关注几方面:芯片所支持的读写频率、芯片与单片机的硬件连接、芯片手册上提供的读写协议及其时序。在开发中遇到问题请务必再三检查以上的参数是否配置正确,其中读写频率是容易踩坑的参数,网上有部分文章和视频有提到,部分芯片在进行SPI通信时,即时配置通信波特率稍稍低于理论最高工作频率但仍在理论许可范围内,可能会出现错误,所以当出现类似问题时可以尝试降低通信波特率。
3.1.2 配置SPI的片选信号线
前面提到开发板上的片选信号线CS引脚对应单片机的PC0引脚,在可视化配置界面中找到PA4引脚并配置为输出模式。
在system core->GPIO中配置输出引脚的参数
3.1.3 创建SPI线程,任务函数为设置为弱定义。
3.1.4 最后检查工程的时钟树和系统嘀嗒定时器
3.2 在keil中编写W25Q64芯片的驱动文件
3.2.1 从简单的FLASH ID读取开始
在工程目录中创建BSP文件夹,在文件夹中创建bsp_spi_flash.c和bsp_spi_flash.h文件。为了方便修改,将SPI线程的任务函数编写在驱动中,所以freertos.c需要引用驱动头文件:
#include "bsp_spi_flash.h"
首先在前者中添加如下任务函数,目的是进行简单的FLASH ID读取:
#include "bsp_spi_flash.h"
typedef enum { FAILED = 0, PASSED = !FAILED} TestStatus;
__IO uint32_t DeviceID = 0;
__IO uint32_t FlashID = 0;
__IO TestStatus TransferStatus1 = FAILED;
static __IO uint32_t SPITimeout = SPIT_LONG_TIMEOUT;
/**
* @brief 使用SPI发送一个字节的数据
* @param byte:要发送的数据
* @retval 返回接收到的数据
*/
uint8_t SPI_FLASH_SendByte(uint8_t byte)
{
SPITimeout = SPIT_FLAG_TIMEOUT;
/* 等待发送缓冲区为空,TXE事件 */
while (__HAL_SPI_GET_FLAG( &hspi1, SPI_FLAG_TXE ) == RESET)
{
if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0);
}
/* 写入数据寄存器,把要写入的数据写入发送缓冲区 */
WRITE_REG(hspi1.Instance->DR, byte);
SPITimeout = SPIT_FLAG_TIMEOUT;
/* 等待接收缓冲区非空,RXNE事件 */
while (__HAL_SPI_GET_FLAG( &hspi1, SPI_FLAG_RXNE ) == RESET)
{
if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(1);
}
/* 读取数据寄存器,获取接收缓冲区数据 */
return READ_REG(hspi1.Instance->DR);
}
/**
* @brief 读取FLASH ID
* @param 无
* @retval FLASH ID
*/
uint32_t SPI_FLASH_ReadID(void)
{
uint32_t Temp = 0, Temp0 = 0, Temp1 = 0, Temp2 = 0;
/* 开始通讯:CS低电平 */
SPI_FLASH_CS_LOW();
/* 发送JEDEC指令,读取ID */
SPI_FLASH_SendByte(W25X_JedecDeviceID);
/* 读取一个字节数据 */
Temp0 = SPI_FLASH_SendByte(Dummy_Byte);
/* 读取一个字节数据 */
Temp1 = SPI_FLASH_SendByte(Dummy_Byte);
/* 读取一个字节数据 */
Temp2 = SPI_FLASH_SendByte(Dummy_Byte);
/* 停止通讯:CS高电平 */
SPI_FLASH_CS_HIGH();
/*把数据组合起来,作为函数的返回值*/
Temp = (Temp0 << 16) | (Temp1 << 8) | Temp2;
return Temp;
}
/**
* @brief 读取FLASH Device ID
* @param 无
* @retval FLASH Device ID
*/
uint32_t SPI_FLASH_ReadDeviceID(void)
{
uint32_t Temp = 0;
/* Select the FLASH: Chip Select low */
SPI_FLASH_CS_LOW();
/* Send "RDID " instruction */
SPI_FLASH_SendByte(W25X_DeviceID);
SPI_FLASH_SendByte(Dummy_Byte);
SPI_FLASH_SendByte(Dummy_Byte);
SPI_FLASH_SendByte(Dummy_Byte);
/* Read a byte from the FLASH */
Temp = SPI_FLASH_SendByte(Dummy_Byte);
/* Deselect the FLASH: Chip Select high */
SPI_FLASH_CS_HIGH();
return Temp;
}
void SPI_Flash_PowerDown(void)
{
/* 选择 FLASH: CS 低 */
SPI_FLASH_CS_LOW();
/* 发送 掉电 命令 */
SPI_FLASH_SendByte(W25X_PowerDown);
/* 停止信号 FLASH: CS 高 */
SPI_FLASH_CS_HIGH();
}
/**
* @brief 等待超时回调函数
* @param None.
* @retval None.
*/
static uint16_t SPI_TIMEOUT_UserCallback(uint8_t errorCode)
{
/* 等待超时后的处理,输出错误信息 */
FLASH_ERROR("SPI 等待超时!errorCode = %d",errorCode);
return 0;
}
void StartTask_SPI(void const * argument)
{
/* USER CODE BEGIN StartTask_SPI */
printf("\r\nthis is a 16M flash(W25Q64)SPI TEST \r\n");
/* 获取 Flash Device ID */
DeviceID = SPI_FLASH_ReadDeviceID();
osDelay(200);
/* 获取 SPI Flash ID */
FlashID = SPI_FLASH_ReadID();
printf("\r\nFlashID is 0x%X, Manufacturer Device ID is 0x%X\r\n", FlashID, DeviceID);
/* 检验 SPI Flash ID */
if (FlashID == sFLASH_ID)
{
printf("\r\ncheck SPI FLASH W25Q64 successfully!\r\n");
}// if (FlashID == sFLASH_ID)
else
{
printf("\r\n获取不到 W25Q64 ID!\n\r");
}
SPI_Flash_PowerDown();
/* Infinite loop */
for(;;)
{
osDelay(1);
}
/* USER CODE END StartTask_SPI */
}
/*********************************************END OF FILE**********************/
在后者(头文件)中添加以下代码:
#ifndef __SPI_FLASH_H
#define __SPI_FLASH_H
#include "stm32f1xx.h"
#include <stdio.h>
#include "main.h"
#include "cmsis_os.h"
/* Private typedef -----------------------------------------------------------*/
//#define sFLASH_ID 0xEF3015 //W25X16
//#define sFLASH_ID 0xEF4015 //W25Q16
#define sFLASH_ID 0XEF4017 //W25Q64
//#define sFLASH_ID 0XEF4018 //W25Q128
//#define SPI_FLASH_PageSize 4096
#define SPI_FLASH_PageSize 256
#define SPI_FLASH_PerWritePageSize 256
/* Private define ------------------------------------------------------------*/
/*命令定义-开头*******************************/
#define W25X_WriteEnable 0x06
#define W25X_WriteDisable 0x04
#define W25X_ReadStatusReg 0x05
#define W25X_WriteStatusReg 0x01
#define W25X_ReadData 0x03
#define W25X_FastReadData 0x0B
#define W25X_FastReadDual 0x3B
#define W25X_PageProgram 0x02
#define W25X_BlockErase 0xD8
#define W25X_SectorErase 0x20
#define W25X_ChipErase 0xC7
#define W25X_PowerDown 0xB9
#define W25X_ReleasePowerDown 0xAB
#define W25X_DeviceID 0xAB
#define W25X_ManufactDeviceID 0x90
#define W25X_JedecDeviceID 0x9F
#define WIP_Flag 0x01 /* Write In Progress (WIP) flag */
#define Dummy_Byte 0xFF
/*命令定义-结尾*******************************/
#define FLASH_CS_PIN GPIO_PIN_0
#define FLASH_CS_GPIO_PORT GPIOC
#define digitalHi(p,i) {p->BSRR=i;} //设置为高电平
#define digitalLo(p,i) {p->BSRR=(uint32_t)i << 16;} //输出低电平
#define SPI_FLASH_CS_LOW() digitalLo(FLASH_CS_GPIO_PORT,FLASH_CS_PIN )
#define SPI_FLASH_CS_HIGH() digitalHi(FLASH_CS_GPIO_PORT,FLASH_CS_PIN )
/*SPI接口定义-结尾****************************/
/*等待超时时间*/
#define SPIT_FLAG_TIMEOUT ((uint32_t)0x1000)
#define SPIT_LONG_TIMEOUT ((uint32_t)(10 * SPIT_FLAG_TIMEOUT))
/*信息输出*/
#define FLASH_DEBUG_ON 1
#define FLASH_INFO(fmt,arg...) printf("<<-FLASH-INFO->> "fmt"\n",##arg)
#define FLASH_ERROR(fmt,arg...) printf("<<-FLASH-ERROR->> "fmt"\n",##arg)
#define FLASH_DEBUG(fmt,arg...) do{\
if(FLASH_DEBUG_ON)\
printf("<<-FLASH-DEBUG->> [%d]"fmt"\n",__LINE__, ##arg);\
}while(0)
#define MAX_TIMEOUT 200 //SPI轮询操作时的最大等待时间,ms
//HAL_StatusTypeDef SPI_TransmitOneByte(uint8_t byteData);
//uint8_t SPI_ReceiveOneByte();
uint32_t SPI_FLASH_ReadID(void);
uint32_t SPI_FLASH_ReadDeviceID(void);
//uint8_t SPI_FLASH_SendByte(uint8_t byte);
void SPI_Flash_PowerDown(void);
void StartTask_SPI(void const * argument);
#endif /* __SPI_FLASH_H */
3.2.2 故障1
编写简单的ID读取功能代码,处理一些简单的报错后,编写通过。烧录到开发板上读取失败。这块开发板是五年前买的,所以不排除是芯片或者其他电路损坏。为了验证这一猜想,烧录开发板自带的例程可以成功读取,排除是开发板的问题。
经过对开发板例程代码和自己生成代码的反复对比、烧录测试,定位到两处问题:
(1)STM32Cube MX生成的FreeRTOS工程代码,初始化函数是需要开发者自己写使能函数的!!!
问题就出在spi.c中的void MX_SPI1_Init(void)函数,调用该函数能够初始化指定的SPI外设,但是不会帮你配置SPI外设对应的CR1寄存器SPE位(即“SPI使能位”),看图就清楚了。图中右下SPI_CR1寄存器SPE位控制SPI使能。
因此,需要开发者自行添加使能代码到void MX_SPI1_Init(void)中:
void MX_SPI1_Init(void)
{
/* USER CODE BEGIN SPI1_Init 0 */
/* USER CODE END SPI1_Init 0 */
/* USER CODE BEGIN SPI1_Init 1 */
/* USER CODE END SPI1_Init 1 */
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_MASTER;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_HIGH;
hspi1.Init.CLKPhase = SPI_PHASE_2EDGE;
hspi1.Init.NSS = SPI_NSS_SOFT;
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi1.Init.CRCPolynomial = 10;
if (HAL_SPI_Init(&hspi1) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN SPI1_Init 2 */
__HAL_SPI_ENABLE(&hspi1);//这一行是需要开发者自行添加的使能代码
/* USER CODE END SPI1_Init 2 */
}
(2)在驱动文件中使用的结构体变量hspi1应当直接调用spi.h中进行了外部声明的SPI_HandleTypeDef 变量hspi1,打开spi.h文件可以看到自动生成的代码已经对其进行了外部声明,驱动文件直接引用该头文件即可。(对于extern外部声明用法不清楚的可以自行查阅相关资料加深理解)
#include "spi.h //在驱动头文件中添加
至此,驱动代码实现了在线程中查询并向上位机发送W25Q64芯片ID信息。
3.2.3 改写SPI发送接口函数
不难注意到,开发板例程的代码中对芯片的读写都是通过uint8_t SPI_FLASH_SendByte(uint8_t byte)函数进行了,阅读该函数的实现发现其依赖于对底层寄存器的操作,如果没有特殊的考量,为了代码的可读性更强应当尽量使用HAL库的接口实现读写。为此将收发功能分别用HAL库中的收发接口函数HAL_SPI_Transmit、HAL_SPI_Receive实现,改写后如下:
HAL_StatusTypeDef SPI_FLASH_SendByte(uint8_t byteData)
{
return HAL_SPI_Transmit(&hspi1, &byteData, 1, MAX_TIMEOUT);
}
uint8_t SPI_FLASH_ReceiveByte()
{
uint8_t byteData = 0;
HAL_SPI_Receive(&hspi1, &byteData, 1, MAX_TIMEOUT);
return byteData;
}
经过测试,同样可以成功读取芯片ID
3.2.4 对芯片进行读写测试
对线程回调函数进行扩写,参考开发板例程增加读写测试的代码
#include "bsp_spi_flash.h"
/* 获取缓冲区的长度 */
#define TxBufferSize1 (countof(TxBuffer1) - 1)
#define RxBufferSize1 (countof(TxBuffer1) - 1)
#define countof(a) (sizeof(a) / sizeof(*(a)))
#define BufferSize (countof(Tx_Buffer)-1)
#define FLASH_WriteAddress 0x00000
#define FLASH_ReadAddress FLASH_WriteAddress
#define FLASH_SectorToErase FLASH_WriteAddress
/* 发送缓冲区初始化 */
//uint8_t Tx_Buffer[] = "this is a string a test data\r\nI am Xue Dingwei.\r\n";
uint8_t Tx_Buffer[] = "this is a string a test data\r\n";
uint8_t Rx_Buffer[BufferSize];
//#define DEBUG
typedef enum { FAILED = 0, PASSED = !FAILED} TestStatus;
__IO uint32_t DeviceID = 0;
__IO uint32_t FlashID = 0;
__IO TestStatus TransferStatus1 = FAILED;
static __IO uint32_t SPITimeout = SPIT_LONG_TIMEOUT;
static uint16_t SPI_TIMEOUT_UserCallback(uint8_t errorCode);
/**
* @brief 使用SPI发送一个字节的数据
* @param byte:要发送的数据
* @retval 返回接收到的数据
*/
//uint8_t SPI_FLASH_SendByte(uint8_t byte)
//{
// SPITimeout = SPIT_FLAG_TIMEOUT;
// /* 等待发送缓冲区为空,TXE事件 */
// while (__HAL_SPI_GET_FLAG( &hspi1, SPI_FLAG_TXE ) == RESET)
// {
// if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(0);
// }
// /* 写入数据寄存器,把要写入的数据写入发送缓冲区 */
// WRITE_REG(hspi1.Instance->DR, byte);
// SPITimeout = SPIT_FLAG_TIMEOUT;
// /* 等待接收缓冲区非空,RXNE事件 */
// while (__HAL_SPI_GET_FLAG( &hspi1, SPI_FLAG_RXNE ) == RESET)
// {
// if((SPITimeout--) == 0) return SPI_TIMEOUT_UserCallback(1);
// }
// /* 读取数据寄存器,获取接收缓冲区数据 */
// return READ_REG(hspi1.Instance->DR);
//}
HAL_StatusTypeDef SPI_FLASH_SendByte(uint8_t byteData)
{
return HAL_SPI_Transmit(&hspi1, &byteData, 1, MAX_TIMEOUT);
}
//SPI接口接收一个字节, 返回接收的一个字节数据
uint8_t SPI_FLASH_ReceiveByte()
{
uint8_t byteData = 0;
HAL_SPI_Receive(&hspi1, &byteData, 1, MAX_TIMEOUT);
return byteData;
}
/**
* @brief 向FLASH发送 写使能 命令
* @param none
* @retval none
*/
void SPI_FLASH_WriteEnable(void)
{
/* 通讯开始:CS低 */
SPI_FLASH_CS_LOW();
/* 发送写使能命令*/
SPI_FLASH_SendByte(W25X_WriteEnable);
/*通讯结束:CS高 */
SPI_FLASH_CS_HIGH();
}
/**
* @brief 等待WIP(BUSY)标志被置0,即等待到FLASH内部数据写入完毕
* @param none
* @retval none
*/
void SPI_FLASH_WaitForWriteEnd(void)
{
uint8_t FLASH_Status = 0;
/* 选择 FLASH: CS 低 */
SPI_FLASH_CS_LOW();
/* 发送 读状态寄存器 命令 */
SPI_FLASH_SendByte(W25X_ReadStatusReg);
SPITimeout = SPIT_FLAG_TIMEOUT;
/* 若FLASH忙碌,则等待 */
do
{
/* 读取FLASH芯片的状态寄存器 */
FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte);
{
if((SPITimeout--) == 0)
{
SPI_TIMEOUT_UserCallback(4);
return;
}
}
}
while ((FLASH_Status & WIP_Flag) == SET); /* 正在写入标志 */
/* 停止信号 FLASH: CS 高 */
SPI_FLASH_CS_HIGH();
}
/**
* @brief 对FLASH按页写入数据,调用本函数写入数据前需要先擦除扇区
* @param pBuffer,要写入数据的指针
* @param WriteAddr,写入地址
* @param NumByteToWrite,写入数据长度,必须小于等于SPI_FLASH_PerWritePageSize
* @retval 无
*/
void SPI_FLASH_PageWrite(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
/* 发送FLASH写使能命令 */
SPI_FLASH_WriteEnable();
/* 选择FLASH: CS低电平 */
SPI_FLASH_CS_LOW();
/* 写页写指令*/
SPI_FLASH_SendByte(W25X_PageProgram);
/*发送写地址的高位*/
SPI_FLASH_SendByte((WriteAddr & 0xFF0000) >> 16);
/*发送写地址的中位*/
SPI_FLASH_SendByte((WriteAddr & 0xFF00) >> 8);
/*发送写地址的低位*/
SPI_FLASH_SendByte(WriteAddr & 0xFF);
if(NumByteToWrite > SPI_FLASH_PerWritePageSize)
{
NumByteToWrite = SPI_FLASH_PerWritePageSize;
FLASH_ERROR("SPI_FLASH_PageWrite too large!");
}
/* 写入数据*/
while (NumByteToWrite--)
{
/* 发送当前要写入的字节数据 */
SPI_FLASH_SendByte(*pBuffer);
/* 指向下一字节数据 */
pBuffer++;
}
/* 停止信号 FLASH: CS 高电平 */
SPI_FLASH_CS_HIGH();
/* 等待写入完毕*/
SPI_FLASH_WaitForWriteEnd();
}
/**
* @brief 对FLASH写入数据,调用本函数写入数据前需要先擦除扇区
* @param pBuffer,要写入数据的指针
* @param WriteAddr,写入地址
* @param NumByteToWrite,写入数据长度
* @retval 无
*/
void SPI_FLASH_BufferWrite(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;
/*mod运算求余,若writeAddr是SPI_FLASH_PageSize整数倍,运算结果Addr值为0*/
Addr = WriteAddr % SPI_FLASH_PageSize;
/*差count个数据值,刚好可以对齐到页地址*/
count = SPI_FLASH_PageSize - Addr;
/*计算出要写多少整数页*/
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
/*mod运算求余,计算出剩余不满一页的字节数*/
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
/* Addr=0,则WriteAddr 刚好按页对齐 aligned */
if (Addr == 0)
{
/* NumByteToWrite < SPI_FLASH_PageSize */
if (NumOfPage == 0)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
}
else /* NumByteToWrite > SPI_FLASH_PageSize */
{
/*先把整数页都写了*/
while (NumOfPage--)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
WriteAddr += SPI_FLASH_PageSize;
pBuffer += SPI_FLASH_PageSize;
}
/*若有多余的不满一页的数据,把它写完*/
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}
}
/* 若地址与 SPI_FLASH_PageSize 不对齐 */
else
{
/* NumByteToWrite < SPI_FLASH_PageSize */
if (NumOfPage == 0)
{
/*当前页剩余的count个位置比NumOfSingle小,写不完*/
if (NumOfSingle > count)
{
temp = NumOfSingle - count;
/*先写满当前页*/
SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
WriteAddr += count;
pBuffer += count;
/*再写剩余的数据*/
SPI_FLASH_PageWrite(pBuffer, WriteAddr, temp);
}
else /*当前页剩余的count个位置能写完NumOfSingle个数据*/
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumByteToWrite);
}
}
else /* NumByteToWrite > SPI_FLASH_PageSize */
{
/*地址不对齐多出的count分开处理,不加入这个运算*/
NumByteToWrite -= count;
NumOfPage = NumByteToWrite / SPI_FLASH_PageSize;
NumOfSingle = NumByteToWrite % SPI_FLASH_PageSize;
SPI_FLASH_PageWrite(pBuffer, WriteAddr, count);
WriteAddr += count;
pBuffer += count;
/*把整数页都写了*/
while (NumOfPage--)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, SPI_FLASH_PageSize);
WriteAddr += SPI_FLASH_PageSize;
pBuffer += SPI_FLASH_PageSize;
}
/*若有多余的不满一页的数据,把它写完*/
if (NumOfSingle != 0)
{
SPI_FLASH_PageWrite(pBuffer, WriteAddr, NumOfSingle);
}
}
}
}
/**
* @brief 读取FLASH数据
* @param pBuffer,存储读出数据的指针
* @param ReadAddr,读取地址
* @param NumByteToRead,读取数据长度
* @retval 无
*/
void SPI_FLASH_BufferRead(uint8_t* pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead)
{
/* 选择FLASH: CS低电平 */
SPI_FLASH_CS_LOW();
/* 发送 读 指令 */
SPI_FLASH_SendByte(W25X_ReadData);
/* 发送 读 地址高位 */
SPI_FLASH_SendByte((ReadAddr & 0xFF0000) >> 16);
/* 发送 读 地址中位 */
SPI_FLASH_SendByte((ReadAddr& 0xFF00) >> 8);
/* 发送 读 地址低位 */
SPI_FLASH_SendByte(ReadAddr & 0xFF);
/* 读取数据 */
while (NumByteToRead--)
{
/* 读取一个字节*/
*pBuffer = SPI_FLASH_ReceiveByte();
/* 指向下一个字节缓冲区 */
pBuffer++;
}
/* 停止信号 FLASH: CS 高电平 */
SPI_FLASH_CS_HIGH();
}
/**
* @brief 读取FLASH ID
* @param 无
* @retval FLASH ID
*/
uint32_t SPI_FLASH_ReadID(void)
{
uint32_t Temp = 0, Temp0 = 0, Temp1 = 0, Temp2 = 0;
#ifndef DEBUG
/* 开始通讯:CS低电平 */
SPI_FLASH_CS_LOW();
/* 发送JEDEC指令,读取ID */
SPI_FLASH_SendByte(W25X_JedecDeviceID);
/* 读取一个字节数据 */
Temp0 = SPI_FLASH_ReceiveByte();
/* 读取一个字节数据 */
Temp1 = SPI_FLASH_ReceiveByte();
/* 读取一个字节数据 */
Temp2 = SPI_FLASH_ReceiveByte();
/* 停止通讯:CS高电平 */
SPI_FLASH_CS_HIGH();
#else
/* 开始通讯:CS低电平 */
SPI_FLASH_CS_LOW();
/* 发送JEDEC指令,读取ID */
SPI_FLASH_SendByte(W25X_JedecDeviceID);
/* 读取一个字节数据 */
Temp0 = SPI_FLASH_ReceiveByte();
/* 读取一个字节数据 */
Temp1 = SPI_FLASH_ReceiveByte();
/* 读取一个字节数据 */
Temp2 = SPI_FLASH_ReceiveByte();
/* 停止通讯:CS高电平 */
SPI_FLASH_CS_HIGH();
#endif
/*把数据组合起来,作为函数的返回值*/
Temp = (Temp0 << 16) | (Temp1 << 8) | Temp2;
return Temp;
}
/**
* @brief 读取FLASH Device ID
* @param 无
* @retval FLASH Device ID
*/
uint32_t SPI_FLASH_ReadDeviceID(void)
{
uint32_t Temp = 0;
/* Select the FLASH: Chip Select low */
SPI_FLASH_CS_LOW();
#ifndef DEBUG
/* Send "RDID " instruction */
SPI_FLASH_SendByte(W25X_DeviceID);
SPI_FLASH_SendByte(Dummy_Byte);
SPI_FLASH_SendByte(Dummy_Byte);
SPI_FLASH_SendByte(Dummy_Byte);
/* Read a byte from the FLASH */
Temp = SPI_FLASH_ReceiveByte();
#else
SPI_FLASH_SendByte(W25X_DeviceID);
SPI_FLASH_SendByte(Dummy_Byte);
SPI_FLASH_SendByte(Dummy_Byte);
SPI_FLASH_SendByte(Dummy_Byte);
/* Read a byte from the FLASH */
Temp = SPI_FLASH_ReceiveByte();
#endif
/* Deselect the FLASH: Chip Select high */
SPI_FLASH_CS_HIGH();
return Temp;
}
void SPI_FLASH_SectorErase(uint32_t SectorAddr)
{
/* 发送FLASH写使能命令 */
SPI_FLASH_WriteEnable();
SPI_FLASH_WaitForWriteEnd();
/* 擦除扇区 */
/* 选择FLASH: CS低电平 */
SPI_FLASH_CS_LOW();
/* 发送扇区擦除指令*/
SPI_FLASH_SendByte(W25X_SectorErase);
/*发送擦除扇区地址的高位*/
SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16);
/* 发送擦除扇区地址的中位 */
SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8);
/* 发送擦除扇区地址的低位 */
SPI_FLASH_SendByte(SectorAddr & 0xFF);
/* 停止信号 FLASH: CS 高电平 */
SPI_FLASH_CS_HIGH();
/* 等待擦除完毕*/
SPI_FLASH_WaitForWriteEnd();
}
void SPI_Flash_PowerDown(void)
{
/* 选择 FLASH: CS 低 */
SPI_FLASH_CS_LOW();
/* 发送 掉电 命令 */
SPI_FLASH_SendByte(W25X_PowerDown);
/* 停止信号 FLASH: CS 高 */
SPI_FLASH_CS_HIGH();
}
/**
* @brief 等待超时回调函数
* @param None.
* @retval None.
*/
static uint16_t SPI_TIMEOUT_UserCallback(uint8_t errorCode)
{
/* 等待超时后的处理,输出错误信息 */
FLASH_ERROR("SPI 等待超时!errorCode = %d",errorCode);
return 0;
}
/*
* 函数名:Buffercmp
* 描述 :比较两个缓冲区中的数据是否相等
* 输入 :-pBuffer1 src缓冲区指针
* -pBuffer2 dst缓冲区指针
* -BufferLength 缓冲区长度
* 输出 :无
* 返回 :-PASSED pBuffer1 等于 pBuffer2
* -FAILED pBuffer1 不同于 pBuffer2
*/
TestStatus Buffercmp(uint8_t* pBuffer1, uint8_t* pBuffer2, uint16_t BufferLength)
{
while(BufferLength--)
{
if(*pBuffer1 != *pBuffer2)
{
return FAILED;
}
pBuffer1++;
pBuffer2++;
}
return PASSED;
}
void StartTask_SPI(void const * argument)
{
/* USER CODE BEGIN StartTask_SPI */
printf("\r\nthis is a 16M flash(W25Q64)SPI TEST \r\n");
/* 获取 Flash Device ID */
DeviceID = SPI_FLASH_ReadDeviceID();
osDelay(200);
/* 获取 SPI Flash ID */
FlashID = SPI_FLASH_ReadID();
printf("\r\nFlashID is 0x%X, Manufacturer Device ID is 0x%X\r\n", FlashID, DeviceID);
/* 检验 SPI Flash ID */
if (FlashID == sFLASH_ID)
{
printf("\r\ncheck SPI FLASH W25Q64 successfully!\r\n");
/* 擦除将要写入的 SPI FLASH 扇区,FLASH写入前要先擦除 */
SPI_FLASH_SectorErase(FLASH_SectorToErase);
/* 将发送缓冲区的数据写到flash中 */
SPI_FLASH_BufferWrite(Tx_Buffer, FLASH_WriteAddress, BufferSize);
printf("\r\nwritten data:\r\n%s", Tx_Buffer);
for(int i = 0; i < sizeof(Tx_Buffer)/sizeof(Tx_Buffer[0]); i++)
{
printf("%02X , ", Tx_Buffer[i]);
}
/* 将刚刚写入的数据读出来放到接收缓冲区中 */
SPI_FLASH_BufferRead(Rx_Buffer, FLASH_ReadAddress, BufferSize);
printf("\r\n read data:\r\n%s", Rx_Buffer);
for(int i = 0; i < sizeof(Rx_Buffer)/sizeof(Rx_Buffer[0]); i++)
{
printf("%02X ,", Rx_Buffer[i]);
}
/* 检查写入的数据与读出的数据是否相等 */
TransferStatus1 = Buffercmp(Tx_Buffer, Rx_Buffer, BufferSize);
if( PASSED == TransferStatus1 )
{
printf("\r\n16M flash(W25Q64) test sucessfully!\n\r");
}
else
{
printf("\r\n16M flash(W25Q64)test failed!\n\r");
}
}// if (FlashID == sFLASH_ID)
else
{
printf("\r\ncan not found W25Q64 ID!\n\r");
}
SPI_Flash_PowerDown();
/* Infinite loop */
for(;;)
{
osDelay(1);
}
/* USER CODE END StartTask_SPI */
}
3.3.5 故障2
代码解决简单的编译错误,暂不表。
编译成功后烧录,发现无论写入什么数据,回读的数据全是0xFF。
经过反复对比、烧录测试,最终定位到芯片状态读取操作上。在读取芯片状态的函数void SPI_FLASH_WaitForWriteEnd(void)中,轮询接收芯片状态需要在每次轮询前发送0xFF。
据此修改函数void SPI_FLASH_WaitForWriteEnd(void):
void SPI_FLASH_WaitForWriteEnd(void)
{
uint8_t FLASH_Status = 0;
/* 选择 FLASH: CS 低 */
SPI_FLASH_CS_LOW();
/* 发送 读状态寄存器 命令 */
SPI_FLASH_SendByte(W25X_ReadStatusReg);
SPITimeout = SPIT_FLAG_TIMEOUT;
/* 若FLASH忙碌,则等待 */
do
{
/* 读取FLASH芯片的状态寄存器 */
SPI_FLASH_SendByte(Dummy_Byte);
FLASH_Status = SPI_FLASH_ReceiveByte();
//FLASH_Status = SPI_FLASH_SendByte(Dummy_Byte);
{
if((SPITimeout--) == 0)
{
SPI_TIMEOUT_UserCallback(4);
return;
}
}
}
while ((FLASH_Status & WIP_Flag) == SET); /* 正在写入标志 */
/* 停止信号 FLASH: CS 高 */
SPI_FLASH_CS_HIGH();
}
四、 总结
至此,经过测试,能够完成对W25Q64芯片多个寄存器的读写,效果如图:
本次目标是基于STM32Cube MX生成的代码,根据W25Q64芯片手册编写驱动代码,完成对该芯片的寄存器读写操作,目标达成。
如有帮助,欢迎点赞、收藏文章。需要相关代码可在站内联系作者本人,一起交流、进步!
(项目代码近期上传后,会在文章中更新,站内免费下载无需会员、积分)