STM32F103C8T6+CubeMX生成基于HAL库的工程+CubeIDE进行代码编写通过硬件SPI操作W25Q128实现读写功能(对keysking的stm32教程视频补充)
看前准备(多图警告)
本随笔默认你走完了keysking的stm32视频教程,那个教程里没有SPI,我手头又有w25q128(SPI通信协议),所以我学习w25q128并写成一个笔记来巩固自己刚学的,其中我踩过的坑会说的很细,需要准备的东西有
- stm32核心板(最常见的10块钱一个的就可以),杜邦线以及原理图,没有原理图拿一个万用表
- W25QX系列模块以及对应的数据手册
- stm32f103xxx参考手册(可以不准备)
- 串口助手软件
一、什么是SPI?
先放一张CubeMX的图

看图可知 I2C,SPI,USART,USB 都是通信协议,本次使用的 STM32F103C8T6 片上 SPI 外设有两个硬件 SPI,硬件电路自动输出时钟、收发数据,CPU 只负责读写寄存器,所以使用起来很方便,下面我们将会学习如何使用它。
二、 W25Q128
2.1 引脚定义

从数据手册中截一张图,看不懂没关系,只需要知道我们stm32f103用的是标准 SPI(Standard SPI),只需要四根线进行通信,看下方的Notes,读一读可知标准SPI用不到IO2(引脚2)和IO3(引脚7)。
当然如果是自己画板子想用也是可以的,数据手册再往下翻,可以看到对引脚3和引脚7的描述:

当(/WP)引脚被拉低,整个芯片不能写入数据,所以不用的时候拉高,不能悬空。
当(/HOLD)引脚被拉低时,芯片会暂停通讯,拉高之后,芯片恢复之前的 SPI 通信,继续工作,同样的,不用的时候拉高。
本工程不使用这两个引脚。
2.2 成品模块

可以看到网上售卖的模块本体一共六个输出引脚,两个是供电引脚,剩下四个就是标准SPI通信用到的,芯片上剩下两个不用的引脚直接接入VCC,通过万用表测量以及观察,画出其原理图,因为我们本次使用的是SPI1,所以我直接把如何连接标出来了(图上成品模块的SLK估计是丝印打错了)。

为什么这么连线?
CS:全称Chip Select Not,片选非,拉低 = 选中,拉高 = 不选中,所以直接接一个IO口就可以,我这里选择的是PA4
CLK:全称Serial Clock,串行时钟,主机输出时钟信号,整个 SPI 通信的同步节拍。
DO:全称Data‑In,数据输入,对应 STM32 的 MOSI(主机输出,从机输入)。
DI:全称Data‑Out,数据输出,对应 STM32 的 MISO(主机输入,从机输出)。

先放个图可以观察一下,知道了如何连线后,我们下面新建一个工程
三、新建工程
3.1 打开CubeMX,新建一个stm32f103c8t6的工程

3.2 设置SYS为Serial Wire

不设置为Serial Wire会导致程序下载一遍后第二遍就下载不进去了,解决办法:将Boot0电压拉高到3.3V,Boot1拉低到GND,此时再使用脱机编程器或ST—Link将选中Serial Wire的.hex文件重新烧录一边,断电后就可以正常下载程序了。
3.3 打开串口usart2,模式选Asynchronous,其余全默认,连接电脑用来调试

注意事项,连接电脑需要有CH340芯片的模块,USB ↔ UART,我用的是ESP PROG的模块,用来连接ESP8266的,不过上面也有一个CH340,可以拿来用,RX接PA2,TX接PA3。不知道模块好坏的可以用一根杜邦线,把TXD和RXD短接(回环测试,Loopback),然后打开串口助手,电脑串口助手发送什么,就原样收到什么,如果收不到,那可能就是坏了。
3.4 打开SPI1,本项目使用SPI1进行通信

| 模式选项 | 中文 | 说明 |
|---|---|---|
| Full‑Duplex Master | 全双工主机 | MOSI+MISO,同时收发,W25Q 就用这个 |
| Full‑Duplex Slave | 全双工从机 | 外部提供 SCK 时钟,同时收发 |
| Half‑Duplex Master | 半双工主机 | 只用 1 根数据线,发、收分时复用 |
| Half‑Duplex Slave | 半双工从机 | 半双工的从机 |
| Receive Only Master | 只接收主机 | 只收数据,不发送 |
| Receive Only Slave | 只接收从机 | 只收数据 |
| Transmit Only Master | 只发送主机 | 只管往外发,不接收 |
| Transmit Only Slave | 只发送从机 | 从机只输出数据 |
3.5 将PA4设为OUT_PUT
2.2最后说过,需要一个IO口来输出高低电平进行片选,并且让它默认输出为高电平,即上电时不选中。

3.6 将时钟源设为最大72MHz
先将外部高速时钟选为Crystal/Ceramic Resonator,使用外部晶振。

再去时钟配置里把频率改为72M,出现提示就点OK。

这时候发现左边的引脚定义与配置出错了,点开看看怎么回事。

发现这个Prescaler报错了,点一下看看,它让我们选择4或者超过4的数,这是为什么?

打开stm32F103的参考手册,找到SPI,可以看到有这么一句话,我们去时钟树里找一下这个fPCLK。

有两个fPCLK,是哪一个呢?继续翻我们的参考手册。

打开系统架构这一块,可以看到我们的SPI1就在APB2里面,所以已知APB2为36M,SPI最大为一半,就是18M,刚好和提示对上,我们选择4分频。

我们选择频率的时候不仅要考虑单片机本身的频率,还要考虑我们通信的芯片它所支持的频率,我们再看一下W25Q128数据手册,它最大支持133MHz,Single就是Standard SPI,即标准SPI,所以c8t6跑不满这个芯片的最大频率,设置18M是可以的

3.7 创建工程的最后准备
工程命名W25Qx,勾选单独创建 .c .h 文件,点击生成代码

四、翻看数据手册,查看如何操作
4.1 FUNCTIONAL DESCRIPTIONS(功能描述)
我们先看第6章,翻到最后,有一段描述:通电后,该设备将自动进入写禁止状态,状态寄存器写使能锁存器(WEL)被设置为0。在执行页编程、扇区擦除、块擦除、芯片擦除或写状态寄存器指令之前,必须发出写使能指令。在完成页编程、擦除或写指令后,写使能锁存器(WEL)将自动清空为写禁止状态0。

可以通过这段话大致理解到,w25q128的流程是先上电,等电压稳定后,设备自动进入write-disabled state,写入禁止状态,在每一次页编程、扇区擦除、块擦除、整片擦除、写状态寄存器,执行之前,必须先发写使能指令,指令完成后WEL会自动变为写禁止状态。
4.2 STATUS AND CONFIGURATION REGISTERS(状态寄存器)
截取状态寄存器这一块,看不懂没关系,用翻译软件翻译一下,我们只会用到寄存器里的 BUSY 位和 WEL 位。
BUSY:意思是芯片正在执行擦除或页编程指令时被置1,芯片会忽略除Read Status Register 和 Erase/Program Suspend 以外的指令,直接看英文,可以对照目录查看,是下面这两条指令


WEL:写使能,为 1 的时候允许擦除/编程,为 0 的时候拒绝擦写指令(上电默认为 0,在执行了 occurs upon 后面的动作之后,会自动切为 写禁止状态)


4.3 指令
看完 4.1 和 4.2 的内容后,我们知道了大致的写入流程
现在看看指令都有哪些


有兴趣的可以挨个看一下,下面列出我们要用到的
- Write Enable (06h) | 写使能
- Read Status Register (05h) | 读状态寄存器(查 BUSY)
- Read Data (03h) | 读数据
- Page Program (02h) | 页写入
- Sector Erase (20h) | 扇区擦除
- 32KB Block Erase (52h) | 32KB 块擦除
- Block Erase (D8h) | 64KB 块擦除
- Chip Erase (C7h / 60h) | 整片擦除
- Read JEDEC ID (9Fh) | 读取芯片 ID
4.4 指令描述
4.4.1 写使能指令 06h
写使能指令会把 WEL 置 1,允许对其进行擦写等操作。
过程:将 CS 拉低,在 CLK 上升沿时通过 DI(MOSI) 把 06h 发送出去,发送完毕后 CS 再拉高。
因为 HAL 库已经把 SPI 发送给封装起来了,所以后面也是像 Uart 一样直接调用封装好的方法即可

4.4.2 读状态寄存器 05h
状态寄存器就是 4.2 节说的那个,我们需要用到的就是状态寄存器 1,根据 4.3 的流程图可以知道我们每次写入或擦除都要等 BUSY 位从 1 变 0,其他位置我们不需要,读到 Status Register 1 out 如何取 S0 的值?可以用 & 运算,Status Register 1 out & 0x01,就可以得到状态寄存器 1 S0 的值,并且只要 CS 不拉高,DO 就会不停的 out 状态寄存器 1 的值,以便我们可以一直读,直到 BUSY 等于 0 时停止读取。
& 运算可以看我的另一篇文章《C语言中的位运算符和源码反码补码的浅解》https://www.cnblogs.com/wjl-de-java/p/17303717.html

4.4.3 读数据 03h
直接看翻译吧,描述的很清晰了


4.4.4 页写入 02h
重点:
1, 每次写入前都必须前写使能,即把 WEL 置 1,且一次只能写入256字节。
2, 只能写入先前已擦除的内存位置,值为 FFh,页写入只能在 FFh 的位置上编程,而擦除命令只能把指定地址的值变为高电平,本质上是浮栅 MOS 管。
3, 如果一次要写入一页,请从低八位为 0 的位置开始写,如果没有从低八位开始,并且写入一页时后面的地址不够了,它会从这一页的开头开始写。
4, 每次写完 256 字节后 CS 要被拉高,拉高后数据会被自动写入,并且 BUSY 位会变 1,我们可以读取 BUSY 位直到它变为 0 后进行下一页的写入。
5, 如果在指令传输途中拉高 CS,整条 02h 指令作废,不会执行写入

4.4.5 扇区擦除 20h
直接看翻译


4.4.6 32KB 块擦除 52h
直接看翻译


4.4.7 64KB 块擦除 D8h
直接看翻译


4.4.8 64KB 整片擦除 C7h/60h
直接看翻译


4.4.9 64KB 读取芯片 ID 9Fh
常见芯片型号
| 芯片型号 | JEDEC ID(3字节) | 总比特容量 | 实际字节容量 |
|---|---|---|---|
| W25Q40 | 0xEF 0x40 0x13 | 4Mbit | 512 KB |
| W25Q80 | 0xEF 0x40 0x14 | 8Mbit | 1 MB |
| W25Q16 | 0xEF 0x40 0x15 | 16Mbit | 2 MB |
| W25Q32 | 0xEF 0x40 0x16 | 32Mbit | 4 MB |
| W25Q64JV | 0xEF 0x40 0x17 | 64Mbit | 8 MB |
| W25Q128JV | 0xEF 0x40 0x18 | 128Mbit | 16 MB |
| W25Q256JV | 0xEF 0x40 0x19 | 256Mbit | 32 MB |
| W25Q512JV | 0xEF 0x40 0x20 | 512Mbit | 64 MB |


五、代码编写
终于开始写代码了,为了不当误 copy 的时间,我先把代码放出来,读代码时需要查看框图,代码注释写的很详细了

w25qx.h(跳转此处复制粘贴)
#ifndef INC_W25QX_H_
#define INC_W25QX_H_
#include "spi.h"
#define W25QX_READ_ONCE 1 // 单次读取
#define W25QX_READ_CONTINUOUS 0 // 连续读取
// ==== SPI 与 CS 片选引脚定义 ====
// 可以方便的改为自己所用的 SPI 是 SPI1 或 SPI2,以及自定义的 IO 口
#define W25QX_SPI hspi1
#define W25QX_CS_GPIO GPIOA
#define W25QX_CS_PIN GPIO_PIN_4
// ==== 指令定义 ====
#define W25QX_CMD_WRITE_ENABLE 0x06 // 写使能
#define W25QX_CMD_READ_STATUS1 0x05 // 读状态寄存器 1
#define W25QX_CMD_READ_DATA 0x03 // 读数据
#define W25QX_CMD_PAGE_PROGRAM 0x02 // 页写入
#define W25QX_CMD_SECTOR_ERASE 0x20 // 扇区擦除
#define W25QX_CMD_32KB_BLOCK_ERASE 0x52 // 32K 块擦除
#define W25QX_CMD_64KB_BLOCK_ERASE 0xD8 // 64K 块擦除
#define W25QX_CMD_CHIP_ERASE 0xC7 // 整片擦除
#define W25QX_CMD_READ_ID 0x9F // 读取芯片 ID
// ==== 容量参数 ====
#define W25QX_SECTOR_COUNT 16 // 一块中的扇区数量
#define W25QX_BLOCK_COUNT 256 // 块数量
#define W25QX_PAGE_SIZE 256 // 页编程最大
#define W25QX_SECTOR_SIZE 4096 // 扇区大小
#define W25QX_BLOCK_SIZE 65535 // 块大小
// ==== 函数声明 ====
void W25QX_WriteEnable(void);
uint8_t W25QX_ReadStatus1(uint8_t read_count);
void W25QX_WaitBusy(void);
void W25QX_ReadData(uint32_t addr, uint8_t* buf, uint32_t len);
void W25QX_PageProgram(uint32_t addr, const uint8_t* buf, uint32_t len);
void W25QX_SectorErase(uint32_t addr);
void W25QX_32KbBlockErase(uint32_t addr);
void W25QX_64KbBlockErase(uint32_t addr);
void W25QX_ChipErase(void);
uint32_t W25QX_ReadID(void);
#endif /* INC_W25QX_H_ */
w25qx.c(跳转此处复制粘贴)
#include "w25qx.h"
// 片选控制,加上 static 只让该方法在 w25qx.c 文件中调用,防止别的程序调用此方法导致 CS 不稳定,影响通信
static void W25QX_CS_LOW(void) {
HAL_GPIO_WritePin(W25QX_CS_GPIO, W25QX_CS_PIN, GPIO_PIN_RESET);
}
static void W25QX_CS_HIGH(void) {
HAL_GPIO_WritePin(W25QX_CS_GPIO, W25QX_CS_PIN, GPIO_PIN_SET);
}
// 写使能
void W25QX_WriteEnable(void) {
uint8_t cmd = W25QX_CMD_WRITE_ENABLE;
W25QX_CS_LOW();
HAL_SPI_Transmit(&W25QX_SPI, &cmd, 1, HAL_MAX_DELAY);
W25QX_CS_HIGH();
}
/*
* 读取状态寄存器 1 的值
* 参数:read_count 要读取次数
* W25QX_READ_ONCE = 1 单次读取
* W25QX_READ_CONTINUOUS = 0 连续读取
* */
uint8_t W25QX_ReadStatus1(uint8_t read_count) {
uint8_t cmd = W25QX_CMD_READ_STATUS1;
uint8_t status; //接收返回的状态寄存器 1 的值
W25QX_CS_LOW();
HAL_SPI_Transmit(&W25QX_SPI, &cmd, 1, HAL_MAX_DELAY);
HAL_SPI_Receive(&W25QX_SPI, &status, 1, HAL_MAX_DELAY);
//if(read_count == W25QX_READ_ONCE) // 0 为连续读取,1 为只读取一次
if(read_count)
W25QX_CS_HIGH();
return status;
}
// 等待 BUSY 位变为0
void W25QX_WaitBusy(void) {
/*
* BUSY 位在状态寄存器 1 的第 0 位,我们需要一直读取等它从 1 变为 0,用 & 0x01 就可以获取它的值
* status registers 1 out = xxxxxxx1 or xxxxxxx0
* 我们 & 上 0x01
* xxxxxxx1 xxxxxxx0
* & 都为 1 时为 1,其它时候全为 0
* 00000001 00000001
* =0x1 =0x0
* */
//while ((W25QX_ReadStatus1(W25QX_READ_CONTINUOUS) & 0x01) == 0);
while (W25QX_ReadStatus1(W25QX_READ_CONTINUOUS) & 0x01);// 为 0 跳出循环
W25QX_CS_HIGH(); // 因为是循环接收,别忘了上拉 cs,否则接收的一直都是状态寄存器的值
}
// 读数据
void W25QX_ReadData(uint32_t addr, uint8_t* buf, uint32_t len) {
uint8_t cmd[4];
cmd[0] = W25QX_CMD_READ_DATA;
// 因为SPI一次只能发送 8 bit,地址为24bit,所以把地址拆开
cmd[1] = (addr >> 16) & 0xFF;
cmd[2] = (addr >> 8) & 0xFF;
cmd[3] = addr & 0xFF;
W25QX_CS_LOW();
HAL_SPI_Transmit(&W25QX_SPI, cmd, 4, HAL_MAX_DELAY);
HAL_SPI_Receive(&W25QX_SPI, buf, len, HAL_MAX_DELAY);
W25QX_CS_HIGH();
}
// 页编程,最大只能写入 256 字节,且如果要写入 256 字节时,起始地址低八位要为 0,
// 如果超过256字节,地址低 8 位走到 0xFF,下一字节会折回本页起始位置覆盖。
void W25QX_PageProgram(uint32_t addr, const uint8_t* buf, uint32_t len) {
if(len > W25QX_PAGE_SIZE) // 最多写入256个
len = W25QX_PAGE_SIZE;
W25QX_WriteEnable();
uint8_t cmd[4];
cmd[0] = W25QX_CMD_PAGE_PROGRAM;
cmd[1] = (addr >> 16) & 0xFF;
cmd[2] = (addr >> 8) & 0xFF;
cmd[3] = addr & 0xFF;
W25QX_CS_LOW();
HAL_SPI_Transmit(&W25QX_SPI, cmd, 4, HAL_MAX_DELAY);
HAL_SPI_Transmit(&W25QX_SPI, (uint8_t*)buf, len, HAL_MAX_DELAY);
W25QX_CS_HIGH();
W25QX_WaitBusy();
}
// 扇区擦除 (4K字节)
void W25QX_SectorErase(uint32_t addr) {
W25QX_WriteEnable();
uint8_t cmd[4];
cmd[0] = W25QX_CMD_SECTOR_ERASE;
cmd[1] = (addr >> 16) & 0xFF;
cmd[2] = (addr >> 8) & 0xFF;
cmd[3] = addr & 0xFF;
W25QX_CS_LOW();
HAL_SPI_Transmit(&W25QX_SPI, cmd, 4, HAL_MAX_DELAY);
W25QX_CS_HIGH();
W25QX_WaitBusy();
}
// 32Kb 块擦除 (32K字节)
void W25QX_32KbBlockErase(uint32_t addr) {
W25QX_WriteEnable();
uint8_t cmd[4];
cmd[0] = W25QX_CMD_32KB_BLOCK_ERASE;
cmd[1] = (addr >> 16) & 0xFF;
cmd[2] = (addr >> 8) & 0xFF;
cmd[3] = addr & 0xFF;
W25QX_CS_LOW();
HAL_SPI_Transmit(&W25QX_SPI, cmd, 4, HAL_MAX_DELAY);
W25QX_CS_HIGH();
W25QX_WaitBusy();
}
// 64Kb 块擦除 (64K字节)
void W25QX_64KbBlockErase(uint32_t addr) {
W25QX_WriteEnable();
uint8_t cmd[4];
cmd[0] = W25QX_CMD_64KB_BLOCK_ERASE;
cmd[1] = (addr >> 16) & 0xFF;
cmd[2] = (addr >> 8) & 0xFF;
cmd[3] = addr & 0xFF;
W25QX_CS_LOW();
HAL_SPI_Transmit(&W25QX_SPI, cmd, 4, HAL_MAX_DELAY);
W25QX_CS_HIGH();
W25QX_WaitBusy();
}
// 整片擦除
void W25QX_ChipErase(void) {
W25QX_WriteEnable();
uint8_t cmd = W25QX_CMD_CHIP_ERASE;
W25QX_CS_LOW();
HAL_SPI_Transmit(&W25QX_SPI, &cmd, 1, HAL_MAX_DELAY);
W25QX_CS_HIGH();
W25QX_WaitBusy();
}
// 读取 JEDEC ID
uint32_t W25QX_ReadID(void) {
uint8_t cmd = W25QX_CMD_READ_ID;
uint8_t id_buf[3];
W25QX_CS_LOW();
HAL_SPI_Transmit(&W25QX_SPI, &cmd, 1, HAL_MAX_DELAY);
HAL_SPI_Receive(&W25QX_SPI, id_buf, 3, HAL_MAX_DELAY);
W25QX_CS_HIGH();
return (id_buf[0] << 16) | (id_buf[1] << 8) | id_buf[2];
}
mian.c(跳转此处复制粘贴)
/* USER CODE BEGIN Header */
/**
******************************************************************************
* @file : main.c
* @brief : Main program body
******************************************************************************
* @attention
*
* Copyright (c) 2026 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 "w25qx.h"
/* USER CODE END Includes */
/* Private typedef -----------------------------------------------------------*/
/* USER CODE BEGIN PTD */
/* USER CODE END PTD */
/* Private define ------------------------------------------------------------*/
/* USER CODE BEGIN PD */
// 重定义 printf,每次打印必须跟上换行符\r,否则数据在缓冲区里不进行打印
int _write(int file, char *ptr, int len){
HAL_UART_Transmit(&huart2, (uint8_t *)ptr, len, HAL_MAX_DELAY);
return len;
}
/* 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 */
/* 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_SPI1_Init();
MX_USART2_UART_Init();
/* USER CODE BEGIN 2 */
// 获取芯片JEDEC ID
uint32_t jeded_id = W25QX_ReadID();
uint8_t manufacturer_id = (jeded_id >> 16) & 0xFF; // 厂商 ID
uint8_t memory_type = (jeded_id >> 8) & 0xFF; // 高 8 位:内存类型(0x40)
uint8_t capacity_id = jeded_id & 0xFF; // 低 8 位:容量ID(0x18)
/*
* 容量ID和容量的关系
* 1 << 容量 ID = 总字节数
* 容量 ID 为 0x13 ~ 19 都适用,19后应该是1A的,不过厂家给的是 20,导致失效,不过我们可以减 6(被省略的A~F)
* */
//if (capacity_id >= 0x20) capacity_id -= 6; // w25q512用
uint32_t total_capacity_bytes = (1U << capacity_id); // 总字节数 1 << 0x18 = 1 << 24 = 16,777,216
/*
* 1 MB = 1024 KB 1 兆等于 1024k
* 1 KB = 1024 Byte 1k 等于 1024 字节
* 1 Byte = 8 Bit 1 字节 等于 8 比特
* 总 MB = 总 Byte / 1024 /1024
* 总 M bit = 总 MB * 8
* */
uint8_t total_capacity_mbytes = (total_capacity_bytes / 1024 /1024); // 总兆字节
uint8_t total_capacity_mbits = (total_capacity_mbytes * 8); // 总兆比特
printf("w25q%d容量大小:%dMB\r\n",total_capacity_mbits,total_capacity_mbytes);
/*
* ==== 写入数据 ====
* 写入数据前必须先擦除,擦除有四个方法,分别为擦除4kb,32kb,64kb,整片擦除
* w25q128总共有256个block,一个block包含16个扇区,总扇区:256*16=4096个
* 一个扇区大小4KB,4096Byte;W25Q单次页写最大256Byte,4096/256=16,写满 1 个扇区需要16次页编程,全部写满需要65536次
* */
// ==== Sector 扇区 地址 ====
// 第0个扇区 0000h~0FFFh
// 第1个扇区 1000h~1FFFh
// 第2个扇区 2000h~2FFFh
// ......
// 第15个扇区 F000h~FFFFh
// ==== Block 块 地址 ====
// 第0个块 000000h~00FFFFh
// 第1个块 010000h~01FFFFh
// 第2个块 020000h~02FFFFh
// ......
// 第255个块 FF0000h~FFFFFFh
/*
* 我们注意到,地址的 17~24Bit 有规律的表示了 0~255 块,13~16bit 有规律的表示了 0~15 扇区,1~12bit 有规律的表示了每一页
* 由于最小擦除单位为扇区,在知道了规律后我们可以精确的算出每一扇区的起始地址。
* */
// 扇区起始地址 = 第N个扇区的起始地址 = N - 1 * 扇区大小(4KB),N的取值范围[1~4096]
// 我们写入第二个扇区,起始地址是1000h
uint32_t sector_n = 2 - 1;
uint32_t start_addr = sector_n * W25QX_SECTOR_SIZE;
// 擦除目标扇区(4KB)
W25QX_SectorErase(start_addr);
W25QX_WaitBusy(); // 等待擦除完成
// 一次写入256字节的英语和中文进行测试
uint8_t page_buf[W25QX_PAGE_SIZE] = {0};// 定义字符串缓冲区,一页最大256字节
// 第一页写入256个字母
strncpy(page_buf, "2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabcdefghijklmnopqrstuvwxyz", sizeof(page_buf));
W25QX_PageProgram(start_addr, (uint8_t *)page_buf, strlen(page_buf));
W25QX_WaitBusy();
memset(page_buf, 0, sizeof(page_buf)); // 发送完后初始化为0
//第二页写入UTF-8中文百家姓,一个中文1.5个字节,写入85个中文加一个英文,正好256个字节
strncpy(page_buf, "赵钱孙李周吴郑王冯陈褚卫蒋沈韩杨朱秦尤许何吕施张孔曹严华金魏陶姜戚谢邹喻柏水窦章云苏潘葛奚范彭郎鲁韦昌马苗凤花方俞任袁柳酆鲍史唐费廉岑薛雷贺倪汤滕殷罗毕郝邬安常乐于时傅皮~", sizeof(page_buf));
W25QX_PageProgram(start_addr + W25QX_PAGE_SIZE, (uint8_t *)page_buf, strlen(page_buf));
W25QX_WaitBusy();
memset(page_buf, 0, sizeof(page_buf)); // 发送完后初始化为0
//第三页随便写点
strncpy(page_buf, "测试语句三", sizeof(page_buf));
W25QX_PageProgram(start_addr + 2* W25QX_PAGE_SIZE, (uint8_t *)page_buf, strlen(page_buf));
W25QX_WaitBusy();
// 读取前三页的内容
uint8_t read_buf[3 * W25QX_PAGE_SIZE] = {0};
W25QX_ReadData(start_addr, read_buf, 3 * W25QX_PAGE_SIZE);
// 打印读出数据,只打印前三页,跳过0xFF空白字节
int i,j;
for(i = 1; i <= 3; i++)
{
for(j = 0; j < W25QX_PAGE_SIZE; j++){
uint8_t c = read_buf[(i-1)*W25QX_PAGE_SIZE + j];
if(c == 0xFF) // 空白内容无意义,直接跳过
{
continue;
}
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 */
}
/* 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.1 串口输出截图

六、标准 SPI 的四种工作模式

大家再看一下写使能的时序图,有没有好奇怎么有 Mode 0 和 Mode 3,这两个是干嘛的,这其实是 SPI 的工作模式,我们打开 cubeMX,找到 SPI,看一下 Parameter Settings 界面。

cubeMX 启用 SPI 外设时默认就是模式 0,那 CPOL 和 CPHA 是什么意思呢?
CPOL: 时钟极性,当为 0 也就是 Low 的时候,意思是 SPI 在空闲状态下的时钟为低电平,当为 1 也就是 High 的时候,正好相反,SPI 在空闲状态下时钟输出为高电平。
CPHA:时钟相位,当为 0 也就是 1 Edge 时,在时钟的第一个跳变沿锁存采样数据,当为 1 也就是 2 Edge 时,在时钟的第二个跳变沿锁存采样数据。
| 模式编号 | CPOL | CPHA | 空闲 SCLK 电平 | 数据采样边沿 |
|---|---|---|---|---|
| Mode 0 | 0 | 0 | 低电平 | 上升沿 |
| Mode 1 | 0 | 1 | 低电平 | 下降沿 |
| Mode 2 | 1 | 0 | 高电平 | 下降沿 |
| Mode 3 | 1 | 1 | 高电平 | 上升沿 |
下面画 Mode 0 和 Mode 3 两个图举个例子
Mode 0
Mode 3


浙公网安备 33010602011771号