PUYA ESMC — 外部 QSPI Flash 读写(轮询)ESMC_ReadWrite_Polling

PUYA PY32F403_Firmware V1.4.5 ESMC 外部 QSPI Flash 读写轮询示例教学

ESMC — 外部 QSPI Flash 读写(轮询)ESMC_ReadWrite_Polling

固件库: PY32F403_Firmware V1.4.5
芯片: PY32F403(Cortex-M4)⭐ F403 特色外设
源文件: PY32F403-STK/Example/ESMC/ESMC_ReadWrite_Polling/Src/main.c
核心外设: ESMC(驱动板载 P25Q64 QSPI NOR Flash)


1. 功能简介(大白话)

ESMC 是 F403 用来驱动外部串行 Flash 的专用控制器,本例用它操作板载的 P25Q64(8MB 的 QSPI NOR Flash),把 4096 字节测试数据写进去再读出来比对:

  1. 准备 4096 字节测试数据(0,1,2,...,255 循环);
  2. ESMC 初始化(时钟分频、单线指令、关闭双 Flash);
  3. 写使能 → 等 BUSY 清零;
  4. 扇区擦除(4KB);
  5. 开启 QE 位(Quad Enable,让四根数据线生效);
  6. 四线页编程(按 256 字节/页写入);
  7. 四线快读 回读;
  8. 校验一致则点亮绿灯,否则熄灭进入错误循环。

相当于给 MCU 「外挂了一块 8MB 硬盘」,且用的是四线 QSPI,比前面 SPI 例程的单线 Flash 快得多。这是 F403 相对基础系列多出来的招牌能力。

2. 硬件准备

物品 说明
PY32F403-STK 开发板 板载 P25Q64 QSPI Flash + 绿色 LED
USB 线 供电 + 下载

ESMC 与 P25Q64 的引脚连接(STK 板载已接好,AF10):

P25Q64 信号 MCU 引脚
CLK 时钟 PB10
SS0 / CS 片选 PA2
IO0(SI) 数据0 PB1
IO1(SO) 数据1 PB0
IO2(WP) 数据2 PA7
IO3(HOLD) 数据3 PA6

无需串口,结果用 LED 表示:亮 = 成功,不亮/灭 = 失败。

3. 完整代码(带中文注释)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
#include "main.h"
#define BUFFERSIZE 4096

ESMC_HandleTypeDef hesmc;
ESMC_InitTypeDef    ESMC_initTypeDef;
uint8_t aRxBuffer[BUFFERSIZE];
uint8_t aTxBuffer[BUFFERSIZE];
uint8_t flashStatus;

/* 函数声明 */
static void APP_EsmcInit(void);
static void APP_WriteEnable(void);
static void APP_EraseSector(uint32_t address);
static void APP_PageProgram(uint8_t *pdata, uint32_t addr, uint32_t Nbytes);
static void APP_DataRead(uint8_t *pData, uint32_t address, uint32_t nBytes);
static HAL_StatusTypeDef APP_DataCheck(uint8_t *pSrc, uint8_t *pDst, uint32_t length);
static void APP_SetFlashStatus(uint8_t status, uint8_t comm);
static uint8_t APP_GetFlashStatus(uint8_t comm);

int main(void)
{
  uint32_t i = 0, cnt = 0, progAddr = 0;
  uint8_t *pData = NULL;
  uint32_t address = 0;

  HAL_Init();
  BSP_LED_Init(LED_GREEN);

  /* 准备测试数据 0,1,2,...,255 循环 */
  for (i = 0; i < BUFFERSIZE; i++) aTxBuffer[i] = i % 256;

  APP_EsmcInit();          /* 1. ESMC 初始化 */

  APP_WriteEnable();        /* 2. 写使能 */
  while (APP_GetFlashStatus(0x05) & 0x01) {}   /* 等 BUSY 清 */

  APP_EraseSector(address); /* 3. 扇区擦除(4KB)*/
  while (APP_GetFlashStatus(P25Q64_READ_STATUS_REG_CMD) & 0x01) {}

  /* 4. 开启 QE 位(状态寄存器1 的 bit1)*/
  flashStatus = APP_GetFlashStatus(P25Q64_READ_STATUS_REG_1_CMD);
  flashStatus |= 0x02;
  APP_WriteEnable();
  while (APP_GetFlashStatus(0x05) & 0x01) {}
  APP_SetFlashStatus(flashStatus, P25Q64_WRITE_STATUS_REG_1_CMD);
  while (APP_GetFlashStatus(P25Q64_READ_STATUS_REG_CMD) & 0x01) {}

  /* 5. 分页编程(每页 256 字节)*/
  cnt = BUFFERSIZE; progAddr = address; pData = aTxBuffer;
  while (cnt / P25Q64_PAGE_SIZE > 0)
  {
    APP_PageProgram(pData, progAddr, P25Q64_PAGE_SIZE);
    while (APP_GetFlashStatus(P25Q64_READ_STATUS_REG_CMD) & 0x01) {}
    cnt      -= P25Q64_PAGE_SIZE;
    progAddr += P25Q64_PAGE_SIZE;
    pData    += P25Q64_PAGE_SIZE;
  }
  if (cnt > 0) {
    APP_PageProgram(pData, progAddr, cnt);
    while (APP_GetFlashStatus(P25Q64_READ_STATUS_REG_CMD) & 0x01) {}
  }

  /* 6. 读回并校验 */
  APP_DataRead(aRxBuffer, address, BUFFERSIZE);
  if (HAL_OK != APP_DataCheck(aRxBuffer, aTxBuffer, BUFFERSIZE))
  {
    BSP_LED_Off(LED_GREEN);
    APP_ErrorHandler();
  }
  BSP_LED_On(LED_GREEN);   /* 成功亮灯 */
  while (1) {}
}

/* ---------- ESMC 初始化 ---------- */
static void APP_EsmcInit(void)
{
  __HAL_RCC_ESMC_CLK_ENABLE();
  hesmc.Instance = ESMC;
  ESMC_initTypeDef.ClockPrescaler = 0x8;             /* 时钟分频(决定 QSPI 速率)*/
  ESMC_initTypeDef.ClockMode      = ESMC_CLOCK_MODE_0; /* CPOL=0, CPHA=0 */
  ESMC_initTypeDef.DualFlash      = ESMC_DUALFLASH_DISABLE;
  hesmc.Init = ESMC_initTypeDef;
  HAL_ESMC_Init(&hesmc);
}

/* ---------- 读状态寄存器 ---------- */
static uint8_t APP_GetFlashStatus(uint8_t command)
{
  ESMC_CommandTypeDef cmdStc = {0};
  uint8_t status;
  cmdStc.TransferFormat    = ESMC_TRANSFER_FORMAT_SINGLE;
  cmdStc.InstructionMode   = ESMC_INSTRUCTION_SINGLE_LINE;
  cmdStc.Instruction       = command;                /* 如 0x05 读状态 */
  cmdStc.AddressMode       = ESMC_ADDRESS_NONE;
  cmdStc.DummyCycles       = 0x0;
  cmdStc.DdrMode           = ESMC_DDR_DISABLE;
  cmdStc.DataMode          = ESMC_DATA_READ;
  cmdStc.NbData            = 1;
  cmdStc.CSPinSel          = ESMC_SELECT_PIN_CS0;     /* 选 CS0(PA2) */
  HAL_ESMC_Command(&hesmc, &cmdStc, 0x0000FFFF);
  HAL_ESMC_Receive(&hesmc, &status, 0x0000FFFF);
  return status;
}

/* ---------- 写状态寄存器 ---------- */
static void APP_SetFlashStatus(uint8_t status, uint8_t command)
{
  ESMC_CommandTypeDef cmdStc = {0};
  cmdStc.TransferFormat  = ESMC_TRANSFER_FORMAT_SINGLE;
  cmdStc.InstructionMode = ESMC_INSTRUCTION_SINGLE_LINE;
  cmdStc.Instruction     = command;                  /* 如 0x31 写状态1 */
  cmdStc.AddressMode     = ESMC_ADDRESS_NONE;
  cmdStc.DataMode        = ESMC_DATA_WRITE;
  cmdStc.NbData          = 1;
  cmdStc.CSPinSel        = ESMC_SELECT_PIN_CS0;
  HAL_ESMC_Command(&hesmc, &cmdStc, 0x0000FFFF);
  HAL_ESMC_Transmit(&hesmc, &status, 0x0000FFFF);
}

/* ---------- 写使能(06h)---------- */
static void APP_WriteEnable(void)
{
  ESMC_CommandTypeDef commandStc = {0};
  commandStc.TransferFormat  = ESMC_TRANSFER_FORMAT_SINGLE;
  commandStc.InstructionMode = ESMC_INSTRUCTION_SINGLE_LINE;
  commandStc.Instruction     = P25Q64_WRITE_ENABLE_CMD; /* 0x06 */
  commandStc.AddressMode     = ESMC_ADDRESS_NONE;
  commandStc.DataMode        = ESMC_DATA_NONE;
  commandStc.CSPinSel        = ESMC_SELECT_PIN_CS0;
  HAL_ESMC_Command(&hesmc, &commandStc, 0x0000FFFF);
}

/* ---------- 扇区擦除(20h,4KB)---------- */
static void APP_EraseSector(uint32_t address)
{
  ESMC_CommandTypeDef cmdStc = {0};
  APP_WriteEnable();
  cmdStc.TransferFormat  = ESMC_TRANSFER_FORMAT_SINGLE;
  cmdStc.InstructionMode = ESMC_INSTRUCTION_SINGLE_LINE;
  cmdStc.Instruction     = P25Q64_SECTOR_ERASE_4K_CMD; /* 0x20 */
  cmdStc.AddressMode     = ESMC_ADDRESS_SINGLE_LINE;
  cmdStc.AddressSize     = ESMC_ADDRESS_24_BITS;
  cmdStc.Address         = address;
  cmdStc.DataMode        = ESMC_DATA_NONE;
  cmdStc.CSPinSel        = ESMC_SELECT_PIN_CS0;
  HAL_ESMC_Command(&hesmc, &cmdStc, 0x0000FFFF);
}

/* ---------- 四线页编程(32h)---------- */
static void APP_PageProgram(uint8_t *pData, uint32_t addr, uint32_t nBytes)
{
  ESMC_CommandTypeDef cmdStc = {0};
  APP_WriteEnable();
  cmdStc.TransferFormat  = ESMC_TRANSFER_FORMAT_QUAD;   /* 四线传输数据 */
  cmdStc.InstructionMode = ESMC_INSTRUCTION_SINGLE_LINE;/* 指令仍单线 */
  cmdStc.Instruction     = P25Q64_QUAD_INPUT_PAGE_PROG_CMD; /* 0x32 */
  cmdStc.AddressMode     = ESMC_ADDRESS_SINGLE_LINE;
  cmdStc.AddressSize     = ESMC_ADDRESS_24_BITS;
  cmdStc.Address         = addr;
  cmdStc.DummyCycles     = 0x0;
  cmdStc.DataMode        = ESMC_DATA_WRITE;
  cmdStc.NbData          = nBytes;
  cmdStc.CSPinSel        = ESMC_SELECT_PIN_CS0;
  HAL_ESMC_Command(&hesmc, &cmdStc, 0x0000FFFF);
  HAL_ESMC_Transmit(&hesmc, pData, 0x0000FFFF);
}

/* ---------- 四线快读(6Bh)---------- */
static void APP_DataRead(uint8_t *pData, uint32_t address, uint32_t nBytes)
{
  ESMC_CommandTypeDef cmdStc = {0};
  cmdStc.TransferFormat  = ESMC_TRANSFER_FORMAT_QUAD;   /* 四线读数据 */
  cmdStc.InstructionMode = ESMC_INSTRUCTION_SINGLE_LINE;
  cmdStc.Instruction     = P25Q64_1I4O_FAST_READ_CMD;  /* 0x6B:指令单线,数据四线 */
  cmdStc.AddressMode     = ESMC_ADDRESS_SINGLE_LINE;
  cmdStc.AddressSize     = ESMC_ADDRESS_24_BITS;
  cmdStc.Address         = address;
  cmdStc.DummyCycles     = DUMMY_CLOCK_CYCLES_READ;     /* 8 个哑周期 */
  cmdStc.DataMode        = ESMC_DATA_READ;
  cmdStc.NbData          = nBytes;
  cmdStc.CSPinSel        = ESMC_SELECT_PIN_CS0;
  HAL_ESMC_Command(&hesmc, &cmdStc, 0x00FFFFFF);
  HAL_ESMC_Receive(&hesmc, pData, 0x00FFFFFF);
}

/* ---------- 数据比对 ---------- */
static HAL_StatusTypeDef APP_DataCheck(uint8_t *pSrc, uint8_t *pDst, uint32_t length)
{
  for (uint32_t i = 0; i < length; i++)
    if (pSrc[i] != pDst[i]) return HAL_ERROR;
  return HAL_OK;
}

void APP_ErrorHandler(void) { while (1) {} }

附:MSP 里的引脚配置(HAL_ESMC_Init 自动调用)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void HAL_ESMC_MspInit(ESMC_HandleTypeDef *hesmc)
{
  __HAL_RCC_ESMC_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();
  __HAL_RCC_GPIOC_CLK_ENABLE();

  GPIO_InitTypeDef GPIO_InitStructure = {0};
  GPIO_InitStructure.Mode      = GPIO_MODE_AF_PP;
  GPIO_InitStructure.Alternate = GPIO_AF10_ESMC;       /* 全部 AF10 */
  GPIO_InitStructure.Speed     = GPIO_SPEED_FREQ_VERY_HIGH;

  /* CLK=PB10, IO0=PB1, IO1=PB0 */
  GPIO_InitStructure.Pin = GPIO_PIN_10; GPIO_InitStructure.Pull = GPIO_NOPULL;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
  GPIO_InitStructure.Pin = GPIO_PIN_1;  HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
  GPIO_InitStructure.Pin = GPIO_PIN_0;  HAL_GPIO_Init(GPIOB, &GPIO_InitStructure);
  /* IO2=PA7, IO3=PA6, SS0=PA2(上拉)*/
  GPIO_InitStructure.Pin = GPIO_PIN_7; GPIO_InitStructure.Pull = GPIO_PULLUP;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStructure);
  GPIO_InitStructure.Pin = GPIO_PIN_6; HAL_GPIO_Init(GPIOA, &GPIO_InitStructure);
  GPIO_InitStructure.Pin = GPIO_PIN_2; HAL_GPIO_Init(GPIOA, &GPIO_InitStructure);
}

4. 代码分段讲解

4.1 ESMC 的「命令描述符」模型

ESMC 把一次 Flash 访问拆成 两步

  1. HAL_ESMC_Command():用 ESMC_CommandTypeDef 描述本次操作——指令码、指令线数、是否带地址、地址位数、数据方向(读/写)、数据线数(单/双/四)、数据长度、CS 引脚
  2. HAL_ESMC_Transmit / Receive:按描述真正搬数据。

这种「描述符 + 传输」模型非常灵活,一套 API 通吃各种 QSPI 器件,不用手写位时序。

4.2 单线指令 + 四线数据

注意一个关键设计:指令阶段几乎都用单线INSTRUCTION_SINGLE_LINE,成本低兼容好),而数据阶段才用四线(TRANSFER_FORMAT_QUAD)。例如:

  • 写:0x32(QUAD_INPUT_PAGE_PROG)指令单线发,后面数据四线写;
  • 读:0x6B(1I4O_FAST_READ)指令单线发,读回数据四线收,中间插 8 个哑周期(Dummy)。

4.3 四线为什么必须开 QE?

四线要用 IO2/IO3 两根线,但这两根线默认是 WP/HOLD 功能。必须在**状态寄存器 1 的 bit1(QE 位)**置 1,Flash 才把它们切换成数据线。本例在擦除后专门读-改-写状态寄存器开启 QE——没开 QE 就四线读写会失败

4.4 写使能与 BUSY 轮询(铁律)

任何擦/写前必须发 Write Enable(0x06);操作期间状态寄存器 BUSY 位=1,代码用 while (GetFlashStatus & 0x01) 轮询等到清零,才能下一步。这是所有 SPI/QSPI NOR Flash 的通用约定。

4.5 页编程与跨页

P25Q64 每页 256 字节,一次最多写一页。本例 4096 字节用 while (cnt / 256 > 0) 循环分 16 页写,余数单独补一页。且每次写前地址所在扇区必须已擦除(4KB 扇区擦除已覆盖这 4096 字节)。

4.6 时钟分频

ClockPrescaler = 0x8 决定 QSPI 接口时钟频率(由 ESMC 输入时钟分频而来)。想提速可减小分频,但要保证在 Flash 手册允许范围内。

5. 实验现象

  • 烧录运行后:绿色 LED 常亮 = 擦→写→读→校验全部成功;
  • 若失败(如型号不匹配、QE 未开、接线错):LED 不亮或熄灭,卡在 APP_ErrorHandler

6. 常见问题

现象 原因 解决
LED 不亮(校验失败) Flash 型号不是 P25Q64 指令宏 P25Q64_* 与芯片匹配;换型号需改指令/页大小
四线读写失败 QE 位没成功开启 检查 APP_SetFlashStatus 流程,确认状态1 bit1=1
写不进去 没 WriteEnable / 没擦除 写前使能且目标区已擦除(全 0xFF)
BUSY 一直为 1 操作未完成/异常 检查指令与 CS 引脚选择(CS0=PA2)
速率上不去 分频太大 减小 ClockPrescaler,勿超 Flash 上限
大块数据慢 轮询阻塞 参考 ESMC_ReadWrite_DMAESMC_ReadWriteQuad_IT

7. 小结

  • ESMC 是 F403 特色外设,用「命令描述符 + 传输」统一驱动 QSPI/Octo 串行存储器;
  • QSPI Flash 写流程铁律:写使能 → 等 BUSY → 擦除 → 写使能 → 页编程 → 等 BUSY → 读 → 校验
  • 指令通常单线发,开 QE 后数据用四线搬,速度翻数倍;
  • 引脚全部 AF10:CLK=PB10、CS=PA2、IO0~3=PB1/PB0/PA7/PA6;
  • 本例擦 4KB、分 16 页写 4096 字节、四线读回,LED 亮即成功。

8. 拓展应用(学了能做什么项目)

  • 字库/图片/UI 资源库:把字模、位图存外部 Flash,内部 Flash 只放代码;
  • 数据记录仪:日志、采样缓存写外部 Flash,断电不丢;
  • 固件 OTA:新固件先存外部 Flash,校验后切换启动;
  • 内存映射 XIP:参考 ESMC_MemoryMapping,把外部 Flash 映射成地址空间直接执行代码,突破内部容量;
  • 双 Flash 并行:参考 ESMC_ReadWrite_TwoFlash_Polling,翻倍带宽;
  • 大块资源搬运:参考 ESMC_ReadWrite_DMA,用 DMA 把音频/视频资源送到 LCD/Codec。

返回:ESMC 总览

世界是你们
使用 Hugo 构建
主题 StackJimmy 设计