
🎬 渡水无言 :个人主页渡水无言
❄专栏传送门 : 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》
❄专栏传送门 : 《freertos专栏》 《STM32 HAL库专栏》《linux裸机开发专栏》
❄专栏传送门 :《产品测评专栏》
⭐️流水不争先,争的是滔滔不绝
📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生
| 省级优秀毕业生获得者 | csdn新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生
在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连
目录
[1.4、CubeMX 完整配置步骤](#1.4、CubeMX 完整配置步骤)
[2.1、SPI HAL 库核心接口函数](#2.1、SPI HAL 库核心接口函数)
[2.1.1、 仅发送数据:HAL_SPI_Transmit](#2.1.1、 仅发送数据:HAL_SPI_Transmit)
2.1.3、全双工通信:HAL_SPI_TransmitReceive
[2、从 Flash 加载 LED 状态:SaveLoadLEDState](#2、从 Flash 加载 LED 状态:SaveLoadLEDState)
[3、调用SaveLEDState 保存LED当前状态](#3、调用SaveLEDState 保存LED当前状态)
前言
上一期博客我们完成了对SPI 总线的介绍,这期博客我们完成一个外部flash实验,具体功能为:将LED的亮灭状态保存在flash中,使得开发板断电再重新上电后,灯的亮灭状态不改变。
一、外部flash实验
实验总体介绍:将LED的亮灭状态保存在flesh中,使得开发板断电再重新上电后,灯的亮灭状态不改变。
1.1、Flash模块介绍
Flesh模块相当于一块移动硬盘,存储在这里的数据,掉电后不会丢失。
我们这个实验使用的器件为W25Qxx。

W25Qxx系列是一种低成本、小型化、使用简单的非易失性存储器,常应用于数据存储、字库存储、固件程序存储等场景。


1.2、硬件连线图

1.3、实验预期效果
将LED的亮灭状态保存在flesh中,使得开发板断电再重新上电后,灯的亮灭状态不改变。
1.4、CubeMX 完整配置步骤
打开上一小节完成的cubemx
1.4.1、配置SPI接口
打开connectivity可以看到有两个SPI接口,随便选一个就行。

比如我们这里选SPI1
然后再选择SPI的工作模式:
常从机编程比主机更加复杂,绝大多数情况下,选择第一种

这个时候,会出现一个新选项:

此时右侧可以看到已经被分配好的三个引脚DI,CLK.DO:

此时我们可以看到PA4要我们手动设置一下,设置为通用推挽模式

我们还要继续配置一下SPI的参数,配置如下所示

Basic Parameters:
帧格式为 Motorola 标准,
数据位长度 8 Bits(与外部 Flash 单字节传输要求一致),
数据传输顺序为 MSB First。
Clock Parameters:波特率预分频为 8,实际通信速率为 1000.0 KBits/s(1 MHz,兼顾稳定性与 Flash 芯片的速度上限);
时钟极性 CPOL 为 High(空闲时 SCK 为高电平),时钟相位 CPHA 为 2 Edge(在第二个时钟边沿采样数据),共同构成 SPI 模式 3,需与 Flash 芯片手册的时序要求严格匹配。
Advanced Parameters:关闭 CRC 校验(Flash 读写场景无需额外校验)
NSS 信号类型为 Software(软件控制片选引脚 PA4,便于手动拉低 / 拉高以选中 / 取消选中 Flash 芯片)。
引脚分配:
PA4 作为 GPIO_Output 手动控制片选,
PA5、PA6、PA7 分别复用为 SPI1_SCK、SPI1_MISO、SPI1_MOSI,完整覆盖 SPI 通信所需的 4 根核心信号线,与外部 Flash 模块的接线一一对应。
1.4.2项目管理与生成
给项目取名、设置位置、选择开发的工具链,再点击GENERATE CODE生成

二、代码编写
2.1、SPI HAL 库核心接口函数
在 STM32 HAL 库中,SPI 通信主要依赖 3 个核心接口,我们将基于这三个接口实现 Flash 的读写操作:
| 函数接口 | 作用 |
|---|---|
HAL_SPI_Transmit(...) |
仅发送数据(主机→从机) |
HAL_SPI_Receive(...) |
仅接收数据(从机→主机) |
HAL_SPI_TransmitReceive(...) |
全双工通信:发送数据的同时接收数据 |
2.1.1、 仅发送数据:HAL_SPI_Transmit
函数原型:
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout)
hspi:SPI 句柄指针(CubeMX 自动生成,如&hspi1);
pData:要发送的数据缓冲区指针;
Size:要发送的数据长度(单位:字节);
Timeout:超时时间(单位:ms),HAL_MAX_DELAY表示无限等待。
示例:向从机 1 发送 2 字节数据

uint8_t dataToSend[] = {0x5a, 0x33}; // 要发送的数据
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); // 拉低NSS,选中从机1
HAL_SPI_Transmit(&hspi1, dataToSend, 2, HAL_MAX_DELAY); // 发送2字节
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 拉高NSS,取消选中
2.1.2、仅接收数据:HAL_SPI_Receive
函数原型:
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi,
uint8_t *pData,
uint16_t Size,
uint32_t Timeout)
hspi:SPI 句柄指针;pData:接收数据的缓冲区指针;Size:要接收的数据长度(单位:字节);Timeout:超时时间。
示例:从从机 1 接收 2 字节数据

uint8_t dataRcvd[] = {0xFF, 0xFF}; // 接收缓冲区,初始化为0xFF(保持MOSI高电平)
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); // 选中从机1
HAL_SPI_Receive(&hspi1, dataRcvd, 2, HAL_MAX_DELAY); // 接收2字节
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 取消选中
2.1.3、全双工通信:HAL_SPI_TransmitReceive
发送数据的同时接收数据,函数原型:
HAL_StatusTypeDef HAL_SPI_TransmitReceive(SPI_HandleTypeDef *hspi,
uint8_t *pTxData,
uint8_t *pRxData,
uint16_t Size,
uint32_t Timeout)
hspi:SPI 句柄指针;pTxData:要发送的数据缓冲区指针;pRxData:接收数据的缓冲区指针;Size:发送 / 接收的数据长度(两者必须相等);Timeout:超时时间。
示例:发送 2 字节的同时接收 2 字节

uint8_t txData[] = {0x5a, 0x33}; // 要发送的数据
uint8_t rxData[2]; // 接收缓冲区
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET); // 选中从机1
HAL_SPI_TransmitReceive(&hspi1, txData, rxData, 2, HAL_MAX_DELAY); // 全双工通信
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET); // 取消选中
此函数完美适配 SPI 全双工特性,在发送指令的同时接收 Flash 返回的数据,是 Flash 读写操作的核心接口。
2.2、flash写入过程

2.3、正式编程思路
1、声明一个函数用来保存LED的亮灭状态
static void SaveLEDState(uint8_t ledstate);
Flash 底层操作:LoadLEDState(读取 LED 状态)、SaveLEDState(保存 LED 状态);
按键消抖与 LED 控制:通过状态对比捕获按键松开瞬间,实现 LED 翻转;
上电初始化:从 Flash 加载 LED 状态,实现掉电记忆。
2、从 Flash 加载 LED 状态:SaveLoadLEDState
从 W25Qxx Flash 的 0x000000 地址读取 1 字节数据(LED 状态:0 = 熄灭,1 = 点亮)。

static void SaveLEDState(uint8_t ledstate)
{
// 1 .写使能
查找芯片手册第20页: 发送数据0x06(参考编程接口)
// 2 .扇区擦除
手册35页:发送数据0x20(参考编程接口)
uint8_t sectorEraseCmd[] = {0x20 , 0x00 , 0x00 , 0x00}
0x20:指令码 , 后面3个0x00为24位扇区首地址
// 3 .延迟100ms
// 4 .写使能
// 5 .页编程
手册33页,指令码为0x02
uint8_t pageProgCmd[5] ;
pageProgCmd[0] = 0x02 ; //指令码
//24位地址
pageProgCmd[1] = pageProgCmd[2] = pageProgCmd[3] = 0 ;
//写入LED亮灭状态
pageProgCmd[4] = ledstate;
再进行发送。
// 6 .延迟10ms
}

3、调用SaveLEDState 保存LED当前状态

4、加载LED状态
static uint8_t LoadLEDState(void); //申明加载状态函数,写在代码54行即上次声明的函数下面。
//实现函数:手册23页
读取数据的指令码为0x03
static uint8_t LoadLEDState(void)
{
uint8_t readDataCmd[]={0x03,0x00,0x00,0x00};
uint8_t ledstate= 0xff;//用来接收读出的数据,即LED状态
//拉低NSS
HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_RESET);
HAL_SPI_Transmit(&hspi1 , readDataCmd , 4 , HAL_MAX_DELAY); //发送数据
HAL_SPI_Transmit(&hspi1 , &ledstate , 1 , HAL_MAX_DELAY);
//拉高NSS
HAL_GPIO_WritePin(GPIOA , GPIO_PIN_4 , GPIO_PIN_SET);
return ledstate;// 返回读取到的数据
}
main.c总代码
cpp
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
* 功能:按键控制LED翻转,LED状态保存到W25Qxx SPI Flash,掉电不丢失
******************************************************************************
* @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" // HAL库核心头文件
#include "spi.h" // SPI外设驱动头文件
#include "gpio.h" // GPIO外设驱动头文件
/* Private includes ----------------------------------------------------------*/
/* USER CODE BEGIN Includes */
// 无额外头文件需要引入
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
// 无自定义类型
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
// 硬件相关宏定义(可选,增强代码可读性)
#define NSS_PIN GPIO_PIN_4 // Flash片选引脚:PA4
#define NSS_PORT GPIOA // 片选引脚端口
#define KEY_PIN GPIO_PIN_0 // 按键引脚:PA0
#define KEY_PORT GPIOA // 按键引脚端口
#define LED_PIN GPIO_PIN_13 // LED引脚:PC13
#define LED_PORT GPIOC // LED引脚端口
/* 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); // 系统时钟配置函数声明(HAL库自动生成)
/* USER CODE BEGIN PFP */
static void SaveLEDState(uint8_t ledstate); // 声明:保存LED状态到Flash
static uint8_t LoadLEDState(void); // 声明:从Flash加载LED状态
/* USER CODE END PFP */
/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
// 重复声明(兼容代码格式,实际可删除)
static uint8_t LoadLEDState(void);
/**
* @brief 从W25Qxx Flash读取LED状态
* @note 读取地址:0x000000,仅读取1字节数据
* @param 无
* @retval uint8_t: 读取到的LED状态(0=熄灭,1=点亮,0xFF=读取异常)
*/
static uint8_t LoadLEDState(void)
{
// 读数据指令:0x03 + 24位地址(0x000000),参考W25Qxx手册23页
uint8_t readDataCmd[]={0x03,0x00,0x00,0x00};
uint8_t ledstate= 0xff; // 接收缓冲区,初始值0xFF(SPI接收时MOSI保持高电平)
// 1. 拉低NSS引脚,选中Flash芯片
HAL_GPIO_WritePin(NSS_PORT, NSS_PIN, GPIO_PIN_RESET);
// 2. 发送读指令+地址(4字节)
HAL_SPI_Transmit(&hspi1 , readDataCmd , 4 , HAL_MAX_DELAY);
// 3. 接收1字节数据(LED状态):HAL库无纯接收,通过发送0xFF实现
HAL_SPI_Transmit(&hspi1 , &ledstate , 1 , HAL_MAX_DELAY);
// 4. 拉高NSS引脚,取消选中Flash
HAL_GPIO_WritePin(NSS_PORT, NSS_PIN, GPIO_PIN_SET);
// 5. 返回读取到的LED状态
return ledstate;
}
/**
* @brief 将LED状态写入W25Qxx Flash
* @note 写入地址:0x000000,流程:写使能→扇区擦除→写使能→页编程
* @param ledstate: 要保存的LED状态(0=熄灭,1=点亮)
* @retval 无
*/
static void SaveLEDState(uint8_t ledstate)
{
// 步骤1:写使能(Flash写入/擦除前必须执行,指令0x06)
uint8_t writeEnableCmd[] = {0x06};
HAL_GPIO_WritePin(NSS_PORT, NSS_PIN, GPIO_PIN_RESET); // 选中Flash
HAL_SPI_Transmit(&hspi1 , writeEnableCmd , 1 , HAL_MAX_DELAY); // 发送写使能指令
HAL_GPIO_WritePin(NSS_PORT, NSS_PIN, GPIO_PIN_SET); // 取消选中
// 步骤2:扇区擦除(指令0x20 + 24位地址,擦除4KB扇区)
uint8_t sectorEraseCmd[] = {0x20,0x00,0x00,0x00};
HAL_GPIO_WritePin(NSS_PORT, NSS_PIN, GPIO_PIN_RESET); // 选中Flash
HAL_SPI_Transmit(&hspi1 , sectorEraseCmd , 4 , HAL_MAX_DELAY); // 发送擦除指令
HAL_GPIO_WritePin(NSS_PORT, NSS_PIN, GPIO_PIN_SET); // 取消选中
HAL_Delay(100); // 擦除耗时约100ms,必须等待完成
// 步骤3:再次写使能(擦除后写使能位自动清零)
HAL_GPIO_WritePin(NSS_PORT, NSS_PIN, GPIO_PIN_RESET); // 选中Flash
HAL_SPI_Transmit(&hspi1 , writeEnableCmd , 1 , HAL_MAX_DELAY); // 发送写使能指令
HAL_GPIO_WritePin(NSS_PORT, NSS_PIN, GPIO_PIN_SET); // 取消选中
// 步骤4:页编程(指令0x02 + 24位地址 + 数据,写入1字节)
uint8_t pageProgCmd[5] ;
pageProgCmd[0] = 0x02 ; // 页编程指令码
pageProgCmd[1] = 0x00 ; // 地址高位
pageProgCmd[2] = 0x00 ; // 地址中位
pageProgCmd[3] = 0x00 ; // 地址低位
pageProgCmd[4] = ledstate; // 要保存的LED状态
HAL_GPIO_WritePin(NSS_PORT, NSS_PIN, GPIO_PIN_RESET); // 选中Flash
HAL_SPI_Transmit(&hspi1 , pageProgCmd, 5 , HAL_MAX_DELAY); // 发送编程指令+数据
HAL_GPIO_WritePin(NSS_PORT, NSS_PIN, GPIO_PIN_SET); // 取消选中
HAL_Delay(10); // 写入耗时约10ms,等待完成
}
/* USER CODE END 0 */
/**
* @brief 应用程序入口函数
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
// 无初始化前置操作
/* USER CODE END 1 */
/* MCU初始化:复位外设、初始化Flash、SysTick */
HAL_Init();
/* USER CODE BEGIN Init */
// 无自定义初始化
/* USER CODE END Init */
/* 配置系统时钟(HSI时钟,主频8MHz) */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
// 无系统初始化扩展
/* USER CODE END SysInit */
/* 初始化所有配置的外设:GPIO、SPI1 */
MX_GPIO_Init();
MX_SPI1_Init();
/* USER CODE BEGIN 2 */
uint8_t pre = 1, cur = 1; // 按键状态变量:pre=前状态,cur=当前状态(1=松开,0=按下)
uint8_t ledstate = 0; // LED状态变量(0=熄灭,1=点亮)
// 上电后从Flash加载LED状态,实现掉电记忆
ledstate = LoadLEDState();
if (ledstate == 1)
{
// 加载状态为1:点亮LED(PC13低电平点亮)
HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET);
}
else
{
// 加载状态为0/异常:熄灭LED(PC13高电平熄灭)
HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_SET);
}
/* USER CODE END 2 */
/* 主循环 */
/* USER CODE BEGIN WHILE */
while (1)
{
// 步骤1:更新按键前状态
pre = cur ;
// 步骤2:读取按键引脚电平,更新当前状态
if( HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) == GPIO_PIN_SET)
{
cur = 1; // 按键松开(PA0上拉输入,高电平)
}
else
{
cur = 0; // 按键按下(PA0低电平)
}
// 步骤3:检测到按键状态变化(按下/松开)
if (pre != cur)
{
HAL_Delay(10); // 按键消抖:过滤机械抖动(10ms)
if (cur == 0) // 按键按下瞬间:无操作
{ }
else // 按键松开瞬间:翻转LED状态
{
if(ledstate == 1) // 当前LED点亮→熄灭
{
HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_SET);
ledstate = 0; // 更新LED状态变量
}
else // 当前LED熄灭→点亮
{
HAL_GPIO_WritePin(LED_PORT, LED_PIN, GPIO_PIN_RESET);
ledstate = 1; // 更新LED状态变量
}
SaveLEDState(ledstate); // 将新状态保存到Flash
}
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
/**
* @brief 系统时钟配置函数
* @note 配置HSI为系统时钟,主频8MHz
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
// 初始化RCC振荡器:HSI开启,无PLL
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
RCC_OscInitStruct.HSIState = RCC_HSI_ON;
RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
// 初始化CPU、AHB、APB总线时钟
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK)
{
Error_Handler();
}
}
/* USER CODE BEGIN 4 */
// 无扩展函数
/* USER CODE END 4 */
/**
* @brief 错误处理函数
* @note 发生HAL库错误时进入死循环
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
__disable_irq();
while (1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief 断言失败处理函数
* @param file: 源文件名指针
* @param line: 断言失败的行号
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* 可添加打印:printf("Assert failed: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
总结
上一期博客我们完成了对SPI 总线的介绍,这期博客我们完成一个外部flash实验,具体功能为:将LED的亮灭状态保存在flash中,使得开发板断电再重新上电后,灯的亮灭状态不改变。