Bootstrap

STM32—SPI详解入门(使用SPI通讯读写W25Q128模块)

目录

一、SPI是什么

二、SPI物理架构

三、SPI工作原理

四、SPI工作模式

五、SPI相关寄存器介绍

六、SPI用到的结构体与函数

1.结构体

2.函数

七、W25Q128芯片

1.W25Q128介绍

2.W25Q128存储架构

3.W25Q128常用指令

4.W25Q128状态寄存器

5.W25Q128常见操作流程

八、实验(使用SPI通讯读写W25Q128模块)

1.接线

2.配置

3.代码

1.main.c文件

2.w25q128.c文件(向工程添加w25q128.c文件)

3.w25q128.h文件(向工程添加w25q128.h文件)

4.spi.c文件编写

5.spi.h文件编写

九、STM32工程添加.c和.h文件


一、SPI是什么

        SPI是串行外设接口(Serial Peripheral Interface)的缩写,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,越来越多的芯片集成了这种通信协议,比如 AT91RM9200 。

二、SPI物理架构

        SPI总线包含四条总线:分别为MOSI、MISO、SCK、NSS(CS)。

(1)MISO:主设备输入/从设备输出引脚。该引脚在从模式下发送数据,在主模式下接收数据。

(2)MOSI:主设备输出/从设备输入引脚。该引脚在主模式下发送数据,在从模式下接收数据。

(3)SCK:时钟信号,可以使主从设备同步输入输出。

(4)NSS(CS): 由主设备控制,用来选择指定的从设备进行通信。(当主设备想要读/写从设备时,首先拉低从设备对应的NSS线)。

三、SPI工作原理

        1.SPI主从模式

         SPI分为主、从两种模式,一个SPI通讯系统需要包含一个(且只能是一个)主设备,一个或多个从设备。提供时钟的为主设备(Master),接收时钟的设备为从设备(Slave),SPI接口的读写操作,都是由主设备发起。当存在多个从设备时,主设备通过从设备各自的片选信号(NSS)来选择从设备。

        2.SPI主、从设备通讯接线

                一个主设备和一个从设备

                 一个主设备和多个从设备        

        3.SPI数据传输

        SPI主设备和从设备都有一个移位寄存器,主机可以通过向它的移位寄存器写入数据来发起一次SPI通讯,主设备的7移到从设备的0上,而从设备的7移到主设备的0上。

1.主设备拉低对应从设备的NSS信号线。(选择从设备进行通信)

2.主设备发送时钟信号,从设备接收时钟信号。(告诉从设备开始进行SPI通讯)

3.数据交换

        主设备(Master)将要发送的数据传输到发送缓存区(Menory),当从设备收到主设备发送的时钟信号,并且在MOSI引脚上出现第一个数据位时,发送过程开始。余下的位被装进移位寄存器,通过MOSI信号线将字节一位一位的发送给从设备。同时主设备通过MISO引脚将数据一位一位的接收到移位寄存器,当数据接收完成时,将数据传输到接收缓冲区。

        从设备同理,将自己发送缓冲区的数据通过移位寄存器和MISO一位一位发送给主设备,同时通过MOSI引脚将数据一位一位的接收到移位寄存器,当数据接收完成时,将数据传输到接收缓冲区 

        SPI只有主模式和从模式之分,没有读和写的说法,数据的写操作和读操作是同步完成的。

                      i.如果只进行写操作,主机只需忽略接收到的字节。           

                      ii.如果只进行读操作,只需发送一个空字节来获取SPI通讯的一个字节。

四、SPI工作模式

1.时钟极性(CPOL)

        控制在没有数据传输时时钟线的空闲状态电平。       

  • 0:SCK在空闲状态保持低电平。

  • 1:SCK在空闲状态保持高电平。

2.时钟相位(CPHA)

        时钟线在第几个时钟边沿采样数据。

  • 0:SCK的第一个(奇数)边沿进行数据位采样,数据在第一个时钟边沿被锁存。

  • 1:SCK的第二个(偶数)边沿进行数据位采样,数据在第二个时钟边沿被锁存。

3.SPI模式时序图

        模式0(常用)(CPOL = 0,CPHA = 0)

             空闲时SCK时钟为低电平,采样时刻为第一个边沿即上升沿。如图所示,黄线进行采样

         模式1(CPOL = 0,CPHA = 1)

             空闲时SCK时钟为低电平,采样时刻为第二个边沿即下降沿。如图所示,黄线进行采样。

         模式2(CPOL = 1,CPHA = 0)

             空闲时SCK时钟为高电平,采样时刻为第一个边沿即下降沿。如图所示,黄线进行采样。

        模式3(常用)(CPOL = 1,CPHA = 1)

            空闲时SCK时钟为高电平,采样时刻为第二个边沿即上升沿。如图所示,黄线进行采样。

五、SPI相关寄存器介绍

六、SPI用到的结构体与函数

1.结构体

(句柄结构体)SPI_HandleTypeDef

typedef struct __SPI_HandleTypeDef
{
  SPI_TypeDef  *Instance;  /* SPIx */
 
  SPI_InitTypeDef  Init;  /* SPI初始化结构体:通信参数 */
 
} SPI_HandleTypeDef;

(初始化结构体)SPI_InitTypeDefSPI

typedef struct
{
  uint32_t Mode;  /* SPI模式(主机模式。从机模式) */
 
  uint32_t Direction;  /* 工作方式(全双工方式、半双工、只读、只写) */
 
  uint32_t DataSize;  /* 数据格式(8bit、16bit) */
 
  uint32_t CLKPolarity;  /* 时钟极性(CPOL) */
 
  uint32_t CLKPhase;  /* 时钟相位(CPHA) */
 
  uint32_t NSS;  /* SS控制方式(软件) */
 
  uint32_t BaudRatePrescaler;  /* SPI波特率预分频值 */
 
  uint32_t FirstBit;  /* 数据传输顺序(MSB、LSB) */
 
  uint32_t TIMode;  /* 数据帧格式(Motorola、TI)*/
 
  uint32_t CRCCalculation;  /* 设置硬件CRC检验 */
 
  uint32_t CRCPolynomial;  /* 设置CRC检验多项式 */
 
} SPI_InitTypeDef;

2.函数

__HAL_RCC_SPI1_CLK_ENABLE()

使能SPI时钟。(使用STM32CubeMX会自动配置)

HAL_SPI_Init()

初始化SPI。(使用STM32CubeMX会自动配置)

HAL_SPI_MspInit()

初始化SPI相关引脚。(使用STM32CubeMX会自动配置)

HAL_SPI_Transmit()  (SPI发送数据)

原型:
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout)

参数:
SPI_HandleTypeDef *hspi:SPI句柄
uint8_t *pData:发送数据的存储地址
uint16_t Size:发送的数据量大小
uint32_t Timeout:超时时间

实例:
uint8_t uint8_t data = 56;

HAL_SPI_TransmitReceive(&hspi1, &data, 1, 1000);

HAL_SPI_Receive()  (SPI接收数据)

原型:
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout)

参数:
SPI_HandleTypeDef *hspi:SPI句柄
uint8_t *pData:接收数据的存储地址
uint16_t Size:接收的数据量大小
uint32_t Timeout:超时时间

实例:
uint8_t uint8_t data;

HAL_SPI_TransmitReceive(&hspi1, &data, 1, 1000);

HAL_SPI_TransmitReceive()  (SPI发送接收数据)

原型:
HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi, uint8_t *pTxData, uint8_t *pRxData, uint16_t Size,uint32_t Timeout)

参数:
SPI_HandleTypeDef *hspi:SPI句柄
uint8_t *pTxData:发送数据的存储地址
uint8_t *pRxData:接收数据的存储地址
uint16_t Size:发送和接收的数据量大小
uint32_t Timeout:超时时间

实例:
uint8_t spi1_read_write_byte(uint8_t data)
{
uint8_t rec_data = 0;

HAL_SPI_TransmitReceive(&hspi1, &data, &rec_data, 1, 1000);

return rec_data;
}

__HAL_SPI_ENABLE()   (使能SPI外设)

__HAL_SPI_DISABLE()    (失能SPI外设)

七、W25Q128芯片

1.W25Q128介绍

  • W25Q128是华邦公司推出的一款SPI接口的NOR FIash芯片,其存储空间为128 Mbit,相当于16M字节。

  • Flash 是常用的用于储存数据的半导体器件,它具有容量大,可重复擦写、按”扇区/块”擦除、掉电后数据可继续保存的特性。

  • Flash 是有一个物理特性:只能写0,不能写1,靠擦除来写1。

  • 支持SPI模式1。

  • 数据格式:8bit,MSB。

2.W25Q128存储架构

  • 一个W25Q128 = 256个块 = 256 * 16个扇区 = 256 * 16 *16个页 = 256 * 16 * 16 * 256个字节,即16777216字节,约16M字节,即寻址范围为0x00 ~ 0xFFFFFF。

  • 16777216 -1 = 0xFFFFFF。

  • 对Flash擦除时一般按扇区(4K = 4096字节)来进行擦除。

3.W25Q128常用指令

        W25Q128 全部指令非常多,但常用的如下几个指令:

1.写使能(0x06)

  • 写使能指令将状态寄存器中的WEL位设置为1。

  • 必须在每个页写、扇区擦除、块擦除、芯片擦除和写状态寄存器指令之前进行写使能。

  • 操作:拉低CS片选->发送指令0x06 ->拉高CS片选。

2.读SR1(0x05)

  • 读取状态寄存器指令允许读取8位状态寄存器的值。

  • 操作:拉低CS片选 ->发送指令0x05 ->定义一个uint8_t数据接收SR1的返回值 ->拉高CS片选 

3.读数据(0x03)

  • 读取数据指令允许从存储器顺序读取一个或多个数据字节。

  • 操作:拉低CS片选 -> 发送指令0x03 -> 发送24位地址 -> 读取数据 -> 拉高CS片选

4.页写(0x02)

  • 页写指令允许在指定地址写入小于256字节的指定长度的数据,在非0xFF处写入的数据会失败。

  • 操作:写使能 -> 拉低CS片选 -> 发送指令0x02 -> 发送24位地址 -> 写入数据 -> 拉高CS片选 -> 等待写入结束(即判断状态寄存器的BUSY位是否置0

5.扇区擦除时序(0x20)

        写入数据前,检查内存空间是否全部都是 0XFF ,不满足需擦除。

        拉低CS片选 → 发送20H→ 发送24位地址 → 拉高CS片选

6.芯片擦除(0xC7)

  • 芯片擦除指令将W25Q128的所有数据都擦除为0xFF。

  • 操作:写使能 -> 等待空闲(即判断状态寄存器的BUSY位是否置0) -> 拉低CS片选 -> 发送指令0xC7 -> 拉高CS片选 -> 等待芯片擦除完成(即判断状态寄存器的BUSY位是否置0)

7.读取W25Q128的芯片ID(0x90)

  • 读取制造商/设备ID指令。

  • 操作:拉低片选信号 -> 发送24位地址,地址为0xFFFFFF -> 定义一个uint16_t数据接收芯片ID -> 拉高片选信号

4.W25Q128状态寄存器

        W25Q128一共有3个状态寄存器,它们的作用时跟踪芯片的状态。其中,状态寄存器1比较常用。

         1.BUSY位(指示当前的状态)        

        BUSY是状态寄存器中的只读位,当设备执行页程序、四页程序、扇区擦除、块擦除、芯片擦除、写状态寄存器或擦除/程序安全寄存器指令时,将其设置为1状态。 在此期间,器件将忽略除读取状态寄存器和擦除/程序挂起指令之外的其他指令。 当编程、擦除或写入状态/安全寄存器指令完成时,忙位将被清除为0状态,表示设备已准备好接受进一步的指令。

0:空闲

1:忙
        2.WEL位(写使能锁定)

        WEL是状态寄存器(S1)中的只读位,在执行写使能指令后被设置为1。 当设备被禁止写入时,WEL状态位被清除为0。 在上电时或在下列任何指令之后发生写禁用状态:写禁用、页程序、四页程序、扇区擦除、块擦除、芯片擦除、写状态寄存器、擦除安全寄存器和程序安全寄存器。

1:可以操作页、扇区、块

0:禁止写入
 

5.W25Q128常见操作流程

写操作:

八、实验(使用SPI通讯读写W25Q128模块)

1.接线

        W25Q128与STM32F103C8T6板子接线,在STM32F103C8T6的产品手册中找到板子上的SPI1的接口。

        PA4作为SPI1的NSS,PA5作为SPI1的CLK,PA6作为SPI1的DO(MISO),PA7作为SPI1的DI(MOSI)。

2.配置

        1.SYS

        2. RCC

 

         3.SPI1

         4.SUART1

          5.使用MicroLIB库

3.代码

1.main.c文件

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2023 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 "spi.h"
#include "usart.h"
#include "gpio.h"
 
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
 
#include "stdio.h"
#include "string.h"
#include "w25q128.h"
 
/* USER CODE END Includes */
 
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
 
/* USER CODE END PTD */
 
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
 
#define TEXT_SIZE 16
#define  FLASH_WriteAddress     0x000000  //数据写入w25q128的地址,地址范围为0x000000 ~ 0xFFFFFF
#define  FLASH_ReadAddress      FLASH_WriteAddress
 
/* USER CODE END PD */
 
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
 
/* USER CODE END PM */
 
/* Private variables ---------------------------------------------------------*/
 
/* USER CODE BEGIN PV */
 
/* 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 */
 
 
//重写stdio.h文件中的prinft()里的fputc()函数
int fputc(int my_data,FILE *p)
{
    unsigned char temp = my_data;
    //改写后,使用printf()函数会将数据通过串口一发送出去
    HAL_UART_Transmit(&huart1,&temp,1,0xffff);  //0xfffff为最大超时时间
    return my_data;
}
 
 
/* USER CODE END 0 */
 
/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */
 
    uint8_t datatemp[TEXT_SIZE];
    
  /* 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_USART1_UART_Init();
  MX_SPI1_Init();
  /* USER CODE BEGIN 2 */
 
    /* w25q128初始化 */
    w25q128_init();
    
    /* 写入测试数据 */
    sprintf((char *)datatemp, "hello jiangxiao");
    w25q128_write(datatemp, FLASH_WriteAddress, TEXT_SIZE);
    printf("数据写入完成!\r\n");
    
    /* 读出测试数据 */
    memset(datatemp, 0, TEXT_SIZE);
    w25q128_read(datatemp, FLASH_ReadAddress, TEXT_SIZE);
    printf("读出数据:%s\r\n", datatemp);
 
  /* USER CODE END 2 */
 
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* 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 */

2.w25q128.c文件(向工程添加w25q128.c文件)

#include "w25q128.h"
#include "spi.h"
#include "stdio.h"
 
 
 
//w25q128初始化
void w25q128_init(void)
{
		uint16_t flash_type;
    spi1_read_write_byte(0xFF); /* 清除DR(数据寄存器),写入一个0xFF */
    W25Q128_CS(1);  //拉高片选信号不进行SPI通信
		flash_type = w25q128_read_id();   /* 读取FLASH ID. */
		if (flash_type == W25Q128){
			printf("检测到W25Q128芯片\r\n");
		}
}
 
 
 
//等待W25Q128空闲
static void w25q128_wait_busy(void)
{
    while ((w25q128_rd_sr1() & 0x01) == 1);   /* 等待状态寄存器的BUSY位清空 */
}
 
 
 
//读取状态寄存器的值
uint8_t w25q128_rd_sr1(void)
{
    uint8_t rec_data = 0;
    
    W25Q128_CS(0);
		spi1_read_write_byte(FLASH_ReadStatusReg1);     // 写入指令0x05:读状态寄存器1
    rec_data = spi1_read_write_byte(0xFF);  //获取状态寄存器1的值
    W25Q128_CS(1);
    
    return rec_data;
}
 
 
 
//W25Q128写使能,即置位WEL为1
void w25q128_write_enable(void)
{
    W25Q128_CS(0);
    spi1_read_write_byte(FLASH_WriteEnable);   /* 发送指令0x06:写使能 */
    W25Q128_CS(1);
}
 
 
 
//发送24位地址
static void w25q128_send_address(uint32_t address)  /*address:地址范围0~16777215字节,即寻址范围为0x00 ~ 0xFFFFFF */
{
    spi1_read_write_byte((uint8_t)((address)>>16));     /* 发送 bit23 ~ bit16 地址 */
    spi1_read_write_byte((uint8_t)((address)>>8));      /* 发送 bit15 ~ bit8  地址 */
    spi1_read_write_byte((uint8_t)address);             /* 发送 bit7  ~ bit0  地址 */
}
 
 
 
//擦除整个芯片
void w25q128_erase_chip(void)
{
    w25q128_write_enable();    /* 写使能 */
    w25q128_wait_busy();       /* 等待空闲 */
    W25Q128_CS(0);
    spi1_read_write_byte(FLASH_ChipErase);  /* 发送指令0xC7:擦除整个芯片 */ 
    W25Q128_CS(1);
    w25q128_wait_busy();       /* 等待芯片擦除结束 */
}
 
 
 
//擦除一个扇区
void w25q128_erase_sector(uint32_t saddr)  /* saddr:该参数是第几个扇区 */
{
    saddr *= 4096;  /* 一个扇区大小为4096字节 */
    w25q128_write_enable();        /* 写使能 */
    w25q128_wait_busy();           /* 等待空闲 */
    W25Q128_CS(0);
    spi1_read_write_byte(FLASH_SectorErase);    /* 发送指令0x20:擦除指定扇区 */
    w25q128_send_address(saddr);   /* 发送擦除的扇区地址 */
    W25Q128_CS(1);
    w25q128_wait_busy();           /* 等待扇区擦除完成 */
}
 
 
 
//读取w25q128芯片ID
uint16_t w25q128_read_id(void)
{
    uint16_t deviceid;
 
    W25Q128_CS(0);  //拉低片选信号进行SPI通信
    spi1_read_write_byte(FLASH_ManufactDeviceID);   /* 发送读取 ID 命令 */
		/* 发送3个0 */
		/*
    spi1_read_write_byte(0);    
    spi1_read_write_byte(0);
    spi1_read_write_byte(0);
		*/
	
		w25q128_send_address(0x000000);
	
    deviceid = spi1_read_write_byte(0xFF) << 8;     /* 读取高8位字节 */
    deviceid |= spi1_read_write_byte(0xFF);         /* 读取低8位字节 */
    W25Q128_CS(1);
 
    return deviceid;
}
 
/*
读取W25Q128的FLASH,在指定地址开始读取指定长度的数据
pubf:需要读取的数据
addr:指定的地址
datalen:指定的数据大小
*/
void w25q128_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint16_t i;
 
    W25Q128_CS(0);
    spi1_read_write_byte(FLASH_ReadData);       /* 发送指令0x03:读取数据 */
    w25q128_send_address(addr);                /* 发送需要读取的数据地址 */
    
    for(i=0;i<datalen;i++)
    {
        pbuf[i] = spi1_read_write_byte(0XFF);   /* 循环读取 */
    }
    
    W25Q128_CS(1);
}
 
 
 
/*
单页写,在指定地址写入小于256字节的指定长度的数据,在非0xFF处写入的数据会失败
pubf:需要写入的数据
addr:指定的地址
datalen:指定的数据大小
*/
static void w25q128_write_page(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint16_t i;
 
    w25q128_write_enable();    /* 写使能 */
 
    W25Q128_CS(0);
    spi1_read_write_byte(FLASH_PageProgram);    /* 发送命令0x02:页写 */
    w25q128_send_address(addr);                /* 发送写入的页地址 */
 
    for(i=0;i<datalen;i++)
    {
        spi1_read_write_byte(pbuf[i]);          /* 循环写入 */
    }
    
    W25Q128_CS(1);
    w25q128_wait_busy();       /* 等待写入结束 */
}
 
 
 
/*
多页写,在指定地址写入指定长度的数据,在非0xFF处写入的数据会失败
pubf:需要写入的数据
addr:指定的地址
datalen:指定的数据大小
*/
static void w25q128_write_nocheck(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint16_t pageremain;
    pageremain = 256 - addr % 256;  /* 获取指定地址那页的剩余字节数 */
 
    if (datalen <= pageremain)      /* 指定地址那页的剩余字节数能装下指定数据大小 */
    {
        pageremain = datalen;  
    }
 
    while (1)
    { 
			/* 当指定地址那页的剩余字节数能装下指定数据大小时,一次性写完 */
            
			/* 当指定数据大小比指定地址那页的剩余字节数要大时, 先写完指定地址那页的剩余字节, 然后根据剩余数据大小进行不同处理 */
        w25q128_write_page(pbuf, addr, pageremain);  //页写
 
        if (datalen == pageremain)   /* 写入结束了 */
        {
            break;  
        }
        else     /* datalen > pageremain */
        {
            pbuf += pageremain;         /* pbuf指针地址偏移,前面已经写了pageremain字节 */
            addr += pageremain;         /* 写地址偏移,前面已经写了pageremain字节 */
            datalen -= pageremain;      /* 写入总长度减去已经写入了的字节数 */
 
            if (datalen > 256)          /* 剩余数据大小还大于一页 */
            {
                pageremain= 256;       /* 一次写入256个字节,即一次写一页 */
            }
            else     /* 剩余数据大小小于一页  */
            {
                pageremain= datalen;   /* 一次性写完 */
            }
        }
    }
}
 
 
/*
//写入W25Q128的FLASH,在指定地址开写入取指定长度的数据
pubf:需要写入的数据
addr:指定的地址
datalen:指定的数据大小
*/
uint8_t g_w25q128_buf[4096];   /* 扇区缓存 */
 
void w25q128_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint32_t secpos;
    uint16_t secoff;
    uint16_t secremain;
    uint16_t i;
    uint8_t *w25q128_buf;
 
    w25q128_buf = g_w25q128_buf;
    secpos = addr / 4096;       /* 获取指定地址在哪片扇区 */
    secoff = addr % 4096;       /* 指定数据在在扇区内的偏移数据大小 */
    secremain = 4096 - secoff;  /* 扇区剩余字节数 */
 
    if (datalen <= secremain)  /* 指定地址那片扇区的剩余字节数能装下指定数据大小 */
    {
        secremain = datalen;    
    }
 
    while (1)
    {
        w25q128_read(w25q128_buf, secpos * 4096, 4096);   /* 读出指定地址那片扇区的全部内容 */
 
        for (i = 0; i < secremain; i++)   /* 校验数据,防止数据出现非0xFF */
        {
            if (w25q128_buf[secoff + i] != 0xFF)  //扇区数据有一个数据不是0xFF
            {
                break;      /* 需要擦除, 直接退出for循环 */
            }
        }
 
        if (i < secremain)   /* 需要擦除 */
        {
            w25q128_erase_sector(secpos);  /* 擦除这个扇区 */
 
            for (i = 0; i < secremain; i++)   /* 复制 */
            {
                w25q128_buf[i + secoff] = pbuf[i];
            }
 
            w25q128_write_nocheck(w25q128_buf, secpos * 4096, 4096);  /* 写入整个扇区 */
        }
        else        /* 写已经擦除了的,直接写入扇区剩余区间. */
        {
            w25q128_write_nocheck(pbuf, addr, secremain);  /* 直接写扇区 */
        }
 
        if (datalen == secremain)
        {
            break;  /* 写入结束了 */
        }
        else        /* 写入未结束 */
        {
            secpos++;               /* 扇区地址增1,新的一个扇区 */
            secoff = 0;             /* 偏移位置为0 */
 
            pbuf += secremain;      /* 指针偏移 */
            addr += secremain;      /* 写地址偏移 */
            datalen -= secremain;   /* 字节数递减 */
 
            if (datalen > 4096)
            {
                secremain = 4096;   /* 一次写入一个扇区 */
            }
            else
            {
                secremain = datalen;/* 一次性写完 */
            }
        }
    }
}
 

3.w25q128.h文件(向工程添加w25q128.h文件)

#include "stdint.h"
 
/* W25Q128片选引脚定义 */
#define W25Q128_CS_GPIO_PORT           GPIOA
#define W25Q128_CS_GPIO_PIN            GPIO_PIN_4
 
/* W25Q128片选信号 */
#define W25Q128_CS(x)      do{ x ? \
                                  HAL_GPIO_WritePin(W25Q128_CS_GPIO_PORT, W25Q128_CS_GPIO_PIN, GPIO_PIN_SET) : \
                                  HAL_GPIO_WritePin(W25Q128_CS_GPIO_PORT, W25Q128_CS_GPIO_PIN, GPIO_PIN_RESET); \
                            }while(0)
 
/* FLASH芯片列表 */
#define W25Q128     0XEF17          /* W25Q128  芯片ID */
 
/* 指令表 */
#define FLASH_WriteEnable                        0x06 
#define FLASH_ReadStatusReg1                0x05 
#define FLASH_ReadData                            0x03 
#define FLASH_PageProgram                        0x02 
#define FLASH_SectorErase                        0x20 
#define FLASH_ChipErase                            0xC7 
#define FLASH_ManufactDeviceID            0x90 
 
/* 静态函数 */
static void w25q128_wait_busy(void);                                                                                                //等待W25Q128空闲       
static void w25q128_send_address(uint32_t address);                                                                      //发送24位地址
static void w25q128_write_page(uint8_t *pbuf, uint32_t addr, uint16_t datalen);         //单页写,在指定地址写入小于256字节的指定长度的数据,在非0xFF处写入的数据会失败
static void w25q128_write_nocheck(uint8_t *pbuf, uint32_t addr, uint16_t datalen);        //多页写,在指定地址写入指定长度的数据,在非0xFF处写入的数据会失败
 
/* 普通函数 */
void w25q128_init(void);                            //w25q128初始化  
uint16_t w25q128_read_id(void);       //读取w25q128芯片ID   
void w25q128_write_enable(void);      //W25Q128写使能,即置位WEL为1    
uint8_t w25q128_rd_sr1(void);                 //读取状态寄存器的值        
 
void w25q128_erase_chip(void);               //擦除整个芯片      
void w25q128_erase_sector(uint32_t saddr);   //擦除一个扇区
void w25q128_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen);     //读取W25Q128的FLASH,在指定地址开始读取指定长度的数据
void w25q128_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen);    //写入W25Q128的FLASH,在指定地址开写入取指定长度的数据
 
 
 
 

4.spi.c文件编写

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file    spi.c
  * @brief   This file provides code for the configuration
  *          of the SPI instances.
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2023 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 "spi.h"
 
/* USER CODE BEGIN 0 */
 
/* USER CODE END 0 */
 
SPI_HandleTypeDef hspi1;  /* SPI句柄 */
 
/* SPI1 init function */
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;  /* 设置SPI模式主机模式 */
  hspi1.Init.Direction = SPI_DIRECTION_2LINES;  /* 设置SPI工作方式:全双工 */
  hspi1.Init.DataSize = SPI_DATASIZE_8BIT;  /* 设置数据格式:8bit */
    
    /* SPI模式1 */
  hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;  /* 设置时钟极性:CPOL = 0 */
  hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;  /* 设置时钟相位:CPHA = 1 */
  
    hspi1.Init.NSS = SPI_NSS_SOFT;  /* 设置片选方式:软件NSS */
  hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;  /* 设置SPI时钟波特率分频:256分频 */
  hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;  /* 设置数据大小端:MSB */
  hspi1.Init.TIMode = SPI_TIMODE_DISABLE;  /* 设置数据帧格式:Motolora */
  hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;  /* 设置CRC校验:关闭CRC检验 */
  hspi1.Init.CRCPolynomial = 10;  /* 设置CRC校验多项式:1~65535 */
  if (HAL_SPI_Init(&hspi1) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN SPI1_Init 2 */
 
  /* USER CODE END SPI1_Init 2 */
 
}
 
void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle)
{
 
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(spiHandle->Instance==SPI1)
  {
  /* USER CODE BEGIN SPI1_MspInit 0 */
 
  /* USER CODE END SPI1_MspInit 0 */
    /* SPI1 clock enable */
    __HAL_RCC_SPI1_CLK_ENABLE();
 
    __HAL_RCC_GPIOA_CLK_ENABLE();
    /**SPI1 GPIO Configuration
    PA5     ------> SPI1_SCK
    PA6     ------> SPI1_MISO
    PA7     ------> SPI1_MOSI
    */
    GPIO_InitStruct.Pin = GPIO_PIN_5|GPIO_PIN_7;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
 
    GPIO_InitStruct.Pin = GPIO_PIN_6;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
 
  /* USER CODE BEGIN SPI1_MspInit 1 */
 
  /* USER CODE END SPI1_MspInit 1 */
  }
}
 
void HAL_SPI_MspDeInit(SPI_HandleTypeDef* spiHandle)
{
 
  if(spiHandle->Instance==SPI1)
  {
  /* USER CODE BEGIN SPI1_MspDeInit 0 */
 
  /* USER CODE END SPI1_MspDeInit 0 */
    /* Peripheral clock disable */
    __HAL_RCC_SPI1_CLK_DISABLE();
 
    /**SPI1 GPIO Configuration
    PA5     ------> SPI1_SCK
    PA6     ------> SPI1_MISO
    PA7     ------> SPI1_MOSI
    */
    HAL_GPIO_DeInit(GPIOA, GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7);
 
  /* USER CODE BEGIN SPI1_MspDeInit 1 */
 
  /* USER CODE END SPI1_MspDeInit 1 */
  }
}
 
/* USER CODE BEGIN 1 */
 
/*通过SPI1同时读写一个字节数据
主机只向从机进行写操作,调用此函数时忽略返回值
主机只向从机进行读操作,调用此函数时随便传入一个字符,尽量是0xFF
*/
uint8_t spi1_read_write_byte(uint8_t data)  
{
    uint8_t rec_data = 0;
    
    HAL_SPI_TransmitReceive(&hspi1, &data, &rec_data, 1, 1000);  //spi读写数据函数,参数2存放用来发送的数据,参数3存放用来接收的数据
   
    return rec_data;  
}
 
/* USER CODE END 1 */

5.spi.h文件编写

/* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file    spi.h
  * @brief   This file contains all the function prototypes for
  *          the spi.c file
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2023 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 */
/* Define to prevent recursive inclusion -------------------------------------*/
#ifndef __SPI_H__
#define __SPI_H__
 
#ifdef __cplusplus
extern "C" {
#endif
 
/* Includes ------------------------------------------------------------------*/
#include "main.h"
 
/* USER CODE BEGIN Includes */
 
/* USER CODE END Includes */
 
extern SPI_HandleTypeDef hspi1;
 
/* USER CODE BEGIN Private defines */
 
/* USER CODE END Private defines */
 
void MX_SPI1_Init(void);
 
/* USER CODE BEGIN Prototypes */
 
uint8_t spi1_read_write_byte(uint8_t data);
 
/* USER CODE END Prototypes */
 
#ifdef __cplusplus
}
#endif
 
#endif /* __SPI_H__ */
 

九、STM32工程添加.c和.h文件

        1.在创建好的STM32工程中找到Core的文件夹

         2.向文件夹里添加新的xxx.c文件或xxx.h文件

        3.在keil5中导入工程后,将这两个文件添加到工程列表中

 

        4.添加头文件

        只需在需要添加头文件的c文件中写上需添加的头文件再编译就能自动添加

        例如下图

 

;