1. 概述

1.1 实验目的

        本实验旨在通过 STM32 微控制器的硬件 SPI 接口,对 W25Q64 外部 NOR Flash 存储芯片进行读写操作。实验演示了芯片ID读取、随机扇区擦除、多页数据写入和扇区数据读取。并提供对应的驱动文件,帮助读者深入理解 STM32 硬件 SPI 通信协议的工作机制,并掌握基于串行 Flash 的数据存储与访问方法。
        通过本实验,读者不仅可以掌握 SPI 接口的基本使用技巧,还能够熟悉 W25Q 系列 Flash 的命令结构、存储特性及应用方法。这对于日后在嵌入式系统开发中涉及外部存储扩展(如固件存储、日志记录、字库管理等)提供了技术积累和实践基础,具有良好的工程指导意义。

1.2 SPI协议介绍

        SPI(Serial Peripheral Interface,串行外设接口)是一种全双工、高速、主从架构的同步串行通信协议。由 Motorola 首创,目前已被广泛应用于微控制器与外设(如 Flash、LCD、传感器、ADC、DAC 等)之间的通信中。SPI 接口结构简单、速率高、实现方便,是嵌入式系统中最常用的通信方式之一。

1.2.1 接口引脚

引脚名称 方向(以主机为参考) 说明
SCLK 主机输出 串行时钟信号
MOSI 主机输出 主出从入(Master Out Slave In)
MISO 从机输出 主入从出(Master In Slave Out)
CS 主机输出 片选信号,低电平有效,用于选中从设备

注: 对于多个从机设备,通常采用多个 CS 引脚来分别控制。

1.2.2 工作模式        

        SPI 定义了 4 种工作模式,具体取决于时钟极性(CPOL)和时钟相位(CPHA):

模式 CPOL CPHA 空闲时钟 采样时刻
0 0 0 第一个边沿(上升沿)
1 0 1 第二个边沿(下降沿)
2 1 0 第一个边沿(下降沿)
3 1 1 第二个边沿(上升沿)

W25Q64 一般工作在 Mode 0(CPOL=0,CPHA=0) 模式下

1.3 W25Q64介绍

        W25Q系列存储芯片凭借其出色的读写速度与大容量存储优势,在嵌入式领域应用广泛,常被用于存储字库信息、LCD显示界面图片、语音播报音频以及固件数据等各类关键内容。

数据类型 示例
程序代码 Bootloader、主程序固件
字体库/图形数据 用于 OLED/LCD 显示的中文字库、位图图标等
参数配置 JSON配置、阈值设置、网络参数等
日志/历史数据 温湿度采集记录、事件发生时间、设备运行状态等
音频数据 播报文件、提示音、报警音

1.3.1 芯片引脚

引脚编号 引脚名称 引脚用途
1 /CS 芯片选择,低电平选中芯片进行通信,高电平芯片待机
2 DO 数据输出,向外部设备输出芯片内部数据
3 /WP 写保护,低电平禁止芯片写入和擦除操作
4 GND 接地,提供电气参考地
5 DI 数据输入,接收外部输入到芯片的命令、地址和数据
6 CLK 串行时钟,由主设备提供时钟信号同步数据传输
7 /HOLD 暂停,低电平芯片暂停操作进入低功耗,高电平恢复
8 VCC 电源,为芯片提供2.7V - 3.6V工作电压

1.3.2 其他型号

W25Q芯片共有如下型号及对应的容量和寻址范围:

型号 容量(Mbit) 容量(MByte) 寻址范围(十六进制)
W25Q40 4 0.5 0x000000 – 0x07FFFF
W25Q80 8 1 0x000000 – 0x0FFFFF
W25Q16 16 2 0x000000 – 0x1FFFFF
W25Q32 32 4 0x000000 – 0x3FFFFF
W25Q64 64 8 0x000000 – 0x7FFFFF
W25Q128 128 16 0x000000 – 0xFFFFFF
W25Q256 256 32 0x000000 – 0x1FFFFFF
W25Q512 512 64 0x000000 – 0x3FFFFFF

1.3.3 存储空间

        W25Q64 的地址空间从 0x000000 开始,到 0x7FFFFF 结束,总计 8 × 1024 × 1024 = 8,388,608 个字节地址(即 8MB)。每一个存储单元(地址)表示一个字节(8 bit)。

层级 大小 数量 总大小
芯片 8 MByte(64 Mbit) 1 8 MByte
块(Block) 64 KB 128 128 块 × 64 KB = 8 MB
扇区(Sector) 4 KB 2048 128 块 × 16 扇区 × 4 KB = 8 MB
页(Page) 256 Bytes 32,768 128 块 × 16 扇区 × 16 页 × 256B = 8 MB

注:每个 块包含 16 个扇区,每个扇区包含 16 页

1.3.4 指令介绍

常用指令

指令码(Hex) 指令名称 功能描述 备注说明
0x06 Write Enable 使能写操作(必须在写/擦前调用) 写入和擦除前必须调用一次
0x04 Write Disable 禁用写操作,防止误操作 可选调用
0x05 Read Status Register 1 读取状态寄存器1(SR1) 包含 WIP(忙标志)、WEL(写使能)等
0x35 Read Status Register 2 读取状态寄存器2(SR2) 包含 QE(Quad Enable)位
0x01 Write Status Registers 写入状态寄存器1 和 2 设置保护区、启用 Quad 模式等
0x9F Read JEDEC ID 读取 JEDEC 厂商/设备 ID 返回厂商 ID(EFh)、容量等
0x90 Read Manufacturer/Device ID 读取制造商和设备 ID 需提供 3 字节 Dummy 地址

读写指令

指令码(Hex) 指令名称 功能描述 备注说明
0x03 Read Data 标准读取(单线 SPI,低速) 最常用读取指令
0x0B Fast Read 快速读取(需一个 Dummy Byte) 速率更高
0x6B Fast Read Quad Output 快速四线读取(Quad SPI 模式) 需要启用 QE 位
0x02 Page Program 页写入(最大 256 字节,页内写) 写前需执行 Write Enable

擦除指令

指令码(Hex) 指令名称 功能描述 备注说明
0x20 Sector Erase (4KB) 擦除 1 个扇区(4KB) 最小擦除单位
0x52 Block Erase 32KB 擦除 1 个 32KB 块 可选中等粒度
0xD8 Block Erase 64KB 擦除 1 个 64KB 块 常用于固件清理
0xC70x60 Chip Erase 擦除整片 Flash 时间较长(几秒)

其他指令

指令码(Hex) 指令名称 功能描述 备注说明
0xB9 Power Down 进入低功耗休眠模式 降低待机功耗
0xAB Release Power Down / Read ID 释放休眠模式并读取 ID 用于唤醒芯片
0xFF Dummy Instruction 空指令(可用于调试或 SPI 同步) SPI 多字节读取常用填充字节
0x9F Read JEDEC ID 厂商ID=0xEF 类型=0x40h 容量=0x17 容量(字节) = 2^(容量ID的十进制值) = 2^23 byte

1.3.5 读写流程

读操作:

(1)写入操作前,必须先进行写使能。
(2)每个数据位只能由1改写为0,不能由0改写为1。
(3)写入数据前必须先檫除,檫除后,所有数据位变为1。
(4)擦除必须按最小擦除单元(扇区)进行。
(5)连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入。
(6)写入操作结束后,芯片进入忙状态,不响应新的读写操作,需进行等待才能进行后续操作。

写操作:

(1)直接调用读取时序,无需读使能,无需额外操作,没有页的限制。
(2)读取操作结束后不会进入忙状态,但不能在忙状态时读取。

1.3.6 地址对齐

分类 大小 对齐要求(起始地址必须满足) 编号计算方式 起始地址计算方式
页(Page) 256 字节 addr % 256 == 0 page = addr / 256 pageStartAddr = page * 256
扇区(Sector) 4KB(4096 字节) addr % 4096 == 0 sector = addr / 4096 sectorStartAddr = sector * 4096
块(Block) 64KB(65536 字节) addr % 65536 == 0 block = addr / 65536 blockStartAddr = block * 65536

2 STM32CubeMx

2.1 SYS设置

2.2 RCC设置

2.3 USART设置

2.4 GPIO设置

用于片选设备引脚,与w25q64的片选CS引脚相连,配置为输出,默认高电平即可

2.5 SPI设置

        黄色感叹号是提示SPI引脚被占用,是因为作者把PB12引脚配置成了通用输出连接w25q64芯片的片选引脚,PB12引脚同时也是复用引脚,用作当STM32作为从机时SPI的NSS片选引脚,相当于w25q64芯片的CS引脚。我们这里是把STM32作为主机,所以这个复用硬件占用不影响实验目的。

2.6 project配置

配置完毕后,点击右上角的GENERATE CODE即可生成代码,并打开工程,在 keil MDK中进行驱动文件的添加和相关工程的配置。

3 keil MDK配置

3.1 debug配置

3.2 新增驱动文件

点击新建后,CTRL+S 进行保存,存放路径和文件名如下图所示,hardware是作者在Core目录下新建的一个文件夹:

3.3 构建工程目录

3.4 构建工程文件

3.5 引入依赖包

这一步是为了引入一个依赖包,在调试过程中,使用串口打印重定向,需要这个依赖包

4 VSCode

4.1 驱动文件

#ifndef __W25Q64_H
#define __W25Q64_H

#include "stm32f1xx_hal.h"
#include "gpio.h"

// ==== SPI 与引脚定义 ====
// 在 main.c 或其他初始化文件中定义实际使用的 SPI 句柄和 CS 引脚
#define W25Q64_SPI         hspi2
#define W25Q64_CS_GPIO     W25Q64_CS_GPIO_Port
#define W25Q64_CS_PIN      W25Q64_CS_Pin

// ==== 指令码定义 ====
#define W25Q64_CMD_WRITE_ENABLE     0x06
#define W25Q64_CMD_WRITE_DISABLE    0x04
#define W25Q64_CMD_READ_STATUS1     0x05
#define W25Q64_CMD_READ_DATA        0x03
#define W25Q64_CMD_PAGE_PROGRAM     0x02
#define W25Q64_CMD_SECTOR_ERASE     0x20
#define W25Q64_CMD_CHIP_ERASE       0xC7
#define W25Q64_CMD_READ_ID          0x9F

// ==== 容量参数 ====
#define W25Q64_SECTOR_SIZE          4096
#define W25Q64_PAGE_SIZE            256
#define W25Q64_TOTAL_SIZE           (8 * 1024 * 1024) // 8MB

// ==== 函数声明 ====
uint32_t W25Q64_ReadID(void);
void     W25Q64_WriteEnable(void);
uint8_t  W25Q64_ReadStatus1(void);
void     W25Q64_WaitBusy(void);
void     W25Q64_ReadData(uint32_t addr, uint8_t* buf, uint32_t len);
void     W25Q64_PageProgram(uint32_t addr, const uint8_t* buf, uint32_t len);
void     W25Q64_SectorErase(uint32_t addr);
void     W25Q64_ChipErase(void);

#endif
#include "w25q64.h"

extern SPI_HandleTypeDef W25Q64_SPI;

// === 片选控制 ===
static void W25Q64_CS_LOW(void) {
    HAL_GPIO_WritePin(W25Q64_CS_GPIO, W25Q64_CS_PIN, GPIO_PIN_RESET);
}
static void W25Q64_CS_HIGH(void) {
    HAL_GPIO_WritePin(W25Q64_CS_GPIO, W25Q64_CS_PIN, GPIO_PIN_SET);
}

// === 写使能 ===
void W25Q64_WriteEnable(void) {
    uint8_t cmd = W25Q64_CMD_WRITE_ENABLE;
    W25Q64_CS_LOW();
    HAL_SPI_Transmit(&W25Q64_SPI, &cmd, 1, HAL_MAX_DELAY);
    W25Q64_CS_HIGH();
}

// === 读取状态寄存器1 ===
uint8_t W25Q64_ReadStatus1(void) {
    uint8_t cmd = W25Q64_CMD_READ_STATUS1;
    uint8_t status;
    W25Q64_CS_LOW();
    HAL_SPI_Transmit(&W25Q64_SPI, &cmd, 1, HAL_MAX_DELAY);
    HAL_SPI_Receive(&W25Q64_SPI, &status, 1, HAL_MAX_DELAY);
    W25Q64_CS_HIGH();
    return status;
}

// === 等待空闲(WIP=0) ===
void W25Q64_WaitBusy(void) {
    while (W25Q64_ReadStatus1() & 0x01);
}

// === 读取 JEDEC ID ===
uint32_t W25Q64_ReadID(void) {
    uint8_t cmd = W25Q64_CMD_READ_ID;
    uint8_t id_buf[3];
    W25Q64_CS_LOW();
    HAL_SPI_Transmit(&W25Q64_SPI, &cmd, 1, HAL_MAX_DELAY);
    HAL_SPI_Receive(&W25Q64_SPI, id_buf, 3, HAL_MAX_DELAY);
    W25Q64_CS_HIGH();
    return (id_buf[0] << 16) | (id_buf[1] << 8) | id_buf[2];
}

// === 读取数据 ===
void W25Q64_ReadData(uint32_t addr, uint8_t* buf, uint32_t len) {
    uint8_t cmd[4];
    cmd[0] = W25Q64_CMD_READ_DATA;
    cmd[1] = (addr >> 16) & 0xFF;
    cmd[2] = (addr >> 8) & 0xFF;
    cmd[3] = addr & 0xFF;
    
    W25Q64_CS_LOW();
    HAL_SPI_Transmit(&W25Q64_SPI, cmd, 4, HAL_MAX_DELAY);
    HAL_SPI_Receive(&W25Q64_SPI, buf, len, HAL_MAX_DELAY);
    W25Q64_CS_HIGH();
}

// === 页编程(最大 256 字节,不能跨页) ===
void W25Q64_PageProgram(uint32_t addr, const uint8_t* buf, uint32_t len) {
    if (len > W25Q64_PAGE_SIZE) len = W25Q64_PAGE_SIZE;

    W25Q64_WriteEnable();

    uint8_t cmd[4];
    cmd[0] = W25Q64_CMD_PAGE_PROGRAM;
    cmd[1] = (addr >> 16) & 0xFF;
    cmd[2] = (addr >> 8) & 0xFF;
    cmd[3] = addr & 0xFF;

    W25Q64_CS_LOW();
    HAL_SPI_Transmit(&W25Q64_SPI, cmd, 4, HAL_MAX_DELAY);
    HAL_SPI_Transmit(&W25Q64_SPI, (uint8_t*)buf, len, HAL_MAX_DELAY);
    W25Q64_CS_HIGH();

    W25Q64_WaitBusy();
}

// === 扇区擦除(按4KB) ===
void W25Q64_SectorErase(uint32_t addr) {
    W25Q64_WriteEnable();

    uint8_t cmd[4];
    cmd[0] = W25Q64_CMD_SECTOR_ERASE;
    cmd[1] = (addr >> 16) & 0xFF;
    cmd[2] = (addr >> 8) & 0xFF;
    cmd[3] = addr & 0xFF;

    W25Q64_CS_LOW();
    HAL_SPI_Transmit(&W25Q64_SPI, cmd, 4, HAL_MAX_DELAY);
    W25Q64_CS_HIGH();

    W25Q64_WaitBusy();
}

// === 整片擦除 ===
void W25Q64_ChipErase(void) {
    W25Q64_WriteEnable();

    uint8_t cmd = W25Q64_CMD_CHIP_ERASE;
    W25Q64_CS_LOW();
    HAL_SPI_Transmit(&W25Q64_SPI, &cmd, 1, HAL_MAX_DELAY);
    W25Q64_CS_HIGH();

    W25Q64_WaitBusy();
}

4.2 主函数

/* 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 "spi.h"
#include "usart.h"
#include "gpio.h"

/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
#include "w25q64.h"
#include <stdlib.h>   // 提供 rand(), srand()
#include <string.h>   // 提供 strlen()
/* 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 */
static uint8_t read_buffer[W25Q64_SECTOR_SIZE];  // 避免栈溢出
/* 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_USART1_UART_Init();
  MX_SPI2_Init();
  /* USER CODE BEGIN 2 */
  

  printf("W25Q64 ID Read Test\r\n");
  // 1.1 读取W25Q64芯片ID
  uint32_t chipID = W25Q64_ReadID();

  // 1.2 解析ID(W25Q64的ID是3字节:0xEF 0x40 0x17)
  uint8_t manufacturer_id = (chipID >> 16) & 0xFF;  // 第1字节:厂商ID(0xEF)
  uint8_t memory_type = (chipID >> 8) & 0xFF;       // 第2字节:内存类型(0x40)
  uint8_t capacity_id = chipID & 0xFF;              // 第3字节:容量ID(0x17)

  // 1.3 计算实际容量(单位:字节)
  uint32_t capacity_bytes = (1 << capacity_id);     // 2^capacity_id(例如 0x17=23 → 2^23=8MB)

  // 1.4 打印结果
  printf("Manufacturer ID: 0x%02X\r\n", manufacturer_id);  // 应为0xEF(Winbond)
  printf("Memory Type: 0x%02X\r\n", memory_type);          // 应为0x40(SPI Flash)
  printf("Capacity ID: 0x%02X\r\n", capacity_id);          // 应为0x17(64Mbit/8MB)
  printf("Full ID: 0x%02X 0x%02X 0x%02X\r\n", 
        manufacturer_id, memory_type, capacity_id);
  printf("Calculated Capacity: %lu bytes (%lu Mbit)\r\n", 
        capacity_bytes, capacity_bytes * 8 / (1024 * 1024));

  
  printf("W25Q64 Flash Sector Test\r\n");
  // 2.1 随机定义扇区编号(0 ~ 2047,因为 W25Q64 有 2048 个扇区)
  uint32_t num = 1223;
  uint32_t sector_num = num % 2048;  // 随机扇区号
  printf("Selected random sector: %lu\r\n", sector_num);

  // 2.2 计算扇区的起始地址(扇区号 * 4KB)
  uint32_t sector_addr = sector_num * W25Q64_SECTOR_SIZE; // 扇区大小(4KB)
  printf("Sector start address: 0x%06lX\r\n", sector_addr);

  // 2.3 擦除目标扇区(4KB)
  printf("Erasing sector at 0x%06lX...\r\n", sector_addr);
  W25Q64_SectorErase(sector_addr);
  W25Q64_WaitBusy();  // 等待擦除完成

  // 2.4 写入测试字符串到扇区起始地址
  printf("Writing test string to sector...\r\n");
  char data_string[W25Q64_PAGE_SIZE] = {0};// 定义可变字符串缓冲区(最大长度只能写一页 即 256 字节)
  snprintf(data_string, sizeof(data_string), 
         "the fire page context: \r\n page 1, this is a w25q64 write data test, i can write 256 bytes in this page \r\n");
  W25Q64_PageProgram(sector_addr, (uint8_t *)data_string, strlen(data_string)); // 写入第一页
  W25Q64_WaitBusy();  // 等待写入完成
  memset(data_string, 0, sizeof(data_string));  // 清零
  snprintf(data_string, sizeof(data_string), 
         "the second page context: \r\n page 2, this is a w25q64 write data test, i can write 256 bytes in this page \r\n");
  W25Q64_PageProgram(sector_addr + W25Q64_PAGE_SIZE, (uint8_t *)data_string, strlen(data_string)); // 写入第二页
  W25Q64_WaitBusy();  // 等待写入完成

  // 2.5 读取整个扇区内容
  // uint8_t read_buffer[W25Q64_SECTOR_SIZE] = {0};
  printf("Reading sector content...\r\n");
  W25Q64_ReadData(sector_addr, read_buffer, W25Q64_SECTOR_SIZE);

  // 2.6 打印读出的数据(以字符串形式,仅打印前 4页内容,即1024字节 避免刷屏)
  printf("Read Data (as string, first 256 bytes):\r\n");
  for (int i = 0; i < 1024; i++) {
      char c = read_buffer[i];
      printf("%c", c);
  }
  printf("\r\n");

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    printf("go on");
    HAL_Delay(3000);
  }
  /* 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 */

5. 实验验证

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐