基于 STM32F407 Discovery 向 W25Q16 SPI Flash 烧录固件

本文介绍如何基于 STM32F407 Discovery 开发板,通过 SPI 接口 将固件数据写入 W25Q16 SPI Flash,并结合 USB CDC(虚拟串口) 实现上位机数据传输与验证。

1. 工程创建与外设配置

使用 STM32CubeMX 创建一个基于 STM32F407VG 的工程,开发板选择 STM32F407 Discovery

1.1 SPI 配置

  • 启用 SPI2 作为 Flash 通信接口

    • 模式:Full-Duplex Master
    • Prescaler:8(降低 SPI 时钟,提高通信稳定性)
  • 引脚配置

    将 SPI2 引脚调整为同一侧排布,方便硬件连接:

  • GPIO 速度配置

    在 GPIO -> SPI2 中, 将 Maximum Output Speed 设置为 Low, 以降低信号频率,增强通信稳定性:

1.2 USB CDC 配置

USB_OTG-FS 选择 Device Only 模式

在 Middleware 中启用:

  • USB Device
  • Class:Communication Device Class (CDC)

作用:

  • 将开发板虚拟为串口设备
  • 用于 PC ↔ MCU 数据传输(烧录固件)

2. W25Q Flash 驱动实现

cpp 复制代码
// --- W25Q16 底层驱动实现 ---
#define W25Q_CS_LOW()  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET)
#define W25Q_CS_HIGH() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET)

uint8_t SPI_RW(uint8_t byte) {
    uint8_t d_read = 0;
    HAL_SPI_TransmitReceive(&hspi2, &byte, &d_read, 1, 100);
    return d_read;
}

void W25Q_WriteEnable(void) {
    W25Q_CS_LOW();
    SPI_RW(0x06);
    W25Q_CS_HIGH();
}

void W25Q_WaitBusy(void) {
    uint8_t status = 0;
    do {
        W25Q_CS_LOW();
        SPI_RW(0x05);
        status = SPI_RW(0xFF);
        W25Q_CS_HIGH();
    } while (status & 0x01); // 只要 BUSY 位为 1 就循环
}

void W25Q_EraseChip(void) {
    W25Q_WriteEnable();
    W25Q_CS_LOW();
    SPI_RW(0xC7); // 全片擦除
    W25Q_CS_HIGH();
    W25Q_WaitBusy(); // 此处会阻塞直到擦除完成(约10秒)
}

void W25Q_WritePage(uint32_t Addr, uint8_t* pBuf, uint16_t Len) {
    W25Q_WriteEnable();
    W25Q_CS_LOW();
    SPI_RW(0x02); // 页编程
    SPI_RW((uint8_t)(Addr >> 16));
    SPI_RW((uint8_t)(Addr >> 8));
    SPI_RW((uint8_t)Addr);
    HAL_SPI_Transmit(&hspi2, pBuf, Len, 100);
    W25Q_CS_HIGH();
    W25Q_WaitBusy();
}

void W25Q_ReadBytes(uint32_t Addr, uint8_t *pBuf, uint32_t Len)
{
  /* 与 SPI_RW / Page Program 相同:逐字节全双工,避免 HAL_Transmit+Receive 与 MISO
   * 上"命令阶段即开始出数据"的时序错位,以及 DR 未按节拍读空导致的错字节 */
  W25Q_CS_LOW();
  (void)SPI_RW(0x03U);
  (void)SPI_RW((uint8_t)(Addr >> 16));
  (void)SPI_RW((uint8_t)(Addr >> 8));
  (void)SPI_RW((uint8_t)Addr);
  for (uint32_t i = 0; i < Len; i++) {
    pBuf[i] = SPI_RW(0xFFU);
  }
  W25Q_CS_HIGH();
}

void W25Q_DumpHexToCdc(uint32_t addr, uint32_t len)
{
  uint8_t row[16];
  char line[96];

  for (uint32_t off = 0; off < len; off += 16U)
  {
    uint32_t n = 16U;
    if (off + n > len) {
      n = len - off;
    }
    uint32_t a = addr + off;
    W25Q_ReadBytes(a, row, n);

    int p = snprintf(line, sizeof(line), "%08lX:", (unsigned long)a);
    if (p < 0 || (size_t)p >= sizeof(line)) {
      p = 0;
    }
    for (uint32_t i = 0; i < n; i++)
    {
      int q = snprintf(line + (size_t)p, sizeof(line) - (size_t)p, " %02X", row[i]);
      if (q < 0) {
        break;
      }
      p += q;
    }
    if ((size_t)p + 2U < sizeof(line))
    {
      line[p++] = '\r';
      line[p++] = '\n';
      line[p] = '\0';
    }
    CDC_TransmitBlocking((const uint8_t *)line, (uint16_t)p);
  }
}

3. 主循环处理逻辑

cpp 复制代码
#define W25Q_HEX_DUMP_LEN  (65536u)
extern USBD_HandleTypeDef hUsbDeviceFS; // 显式声明外部 USB 句柄
uint8_t Page_Buffer[512];    // 刚好一页的缓冲区
volatile uint32_t Buf_Idx = 0;
volatile uint32_t Data_Ready = 0;
volatile uint8_t HexDump_Requested = 0;
uint32_t Current_Addr = 0;

void W25Q_WriteEnable(void);
void W25Q_WaitBusy(void);
void W25Q_EraseChip(void);
void W25Q_WritePage(uint32_t Addr, uint8_t* pBuf, uint16_t Len);
cpp 复制代码
static void FlushRemainderToFlash(void)
{
  static const uint8_t ack[] = "OK\r\n";
  __disable_irq();
  uint32_t tail = Buf_Idx;
  Buf_Idx = 0;
  __enable_irq();
  if (tail > 0U) {
    W25Q_WritePage(Current_Addr, Page_Buffer, (uint16_t)tail);
    Current_Addr += tail;
  }
  (void)CDC_Transmit_FS((uint8_t *)ack, 4);
  (void)USBD_CDC_ReceivePacket(&hUsbDeviceFS);
}

static void CDC_TransmitBlocking(const uint8_t *data, uint16_t len)
{
  uint32_t t0 = HAL_GetTick();
  while (CDC_Transmit_FS((uint8_t *)data, len) == USBD_BUSY) {
    if ((HAL_GetTick() - t0) > 5000U) {
      break;
    }
  }
}

while(1)里加上:

cpp 复制代码
if (Data_Ready)
{
    // 写入 256 字节到 Flash
    W25Q_WritePage(Current_Addr, Page_Buffer, 256);
    Current_Addr += 256;

    // 状态重置
    if(Buf_Idx>256)
    {
        memcpy(Page_Buffer, &Page_Buffer[256], Buf_Idx - 256);
    }
    Buf_Idx -= 256;
    Data_Ready = 0;

    // 回传 OK 信号,告知上位机发送下一包
    CDC_Transmit_FS(ack, 4);

    // 重新开启 USB 接收中断
    USBD_CDC_ReceivePacket(&hUsbDeviceFS);
}

if (HexDump_Requested)
{
    HexDump_Requested = 0;
    const char hdr[] = "\r\n--- W25Q HEX DUMP ---\r\n";
    CDC_TransmitBlocking((const uint8_t *)hdr, (uint16_t)(sizeof(hdr) - 1U));
    W25Q_DumpHexToCdc(0, W25Q_HEX_DUMP_LEN);
    const char ftr[] = "--- END ---\r\n";
    CDC_TransmitBlocking((const uint8_t *)ftr, (uint16_t)(sizeof(ftr) - 1U));
    (void)USBD_CDC_ReceivePacket(&hUsbDeviceFS);
}

/* PA0 用户键(B1):按下为高电平,松手后把缓冲区剩余数据写入 Flash */
if (HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin) == GPIO_PIN_SET)
{
    HAL_Delay(40);
    
    if (HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin) == GPIO_PIN_SET)
    {
        while (HAL_GPIO_ReadPin(B1_GPIO_Port, B1_Pin) == GPIO_PIN_SET)
        {
            HAL_Delay(2);
        }
        
        if (Data_Ready == 0U && Buf_Idx > 0U)
        {
            FlushRemainderToFlash();
        }
    }
}

4. USB CDC 接收回调

cpp 复制代码
extern void W25Q_EraseChip(void);
extern uint8_t CDC_Transmit_FS(uint8_t* Buf, uint16_t Len);
cpp 复制代码
static int8_t CDC_Receive_FS(uint8_t* Buf, uint32_t *Len)
{
  /* USER CODE BEGIN 6 */
	static uint16_t debug_buf_idx_array[1024];
	static int	array_idx = 0;
	  extern uint8_t Page_Buffer[];
	  extern volatile uint32_t Buf_Idx;
	  extern volatile uint32_t Data_Ready;

#if ENABLE_HEXDUMP
	  /* 单字节 'H':请求通过 USB 以十六进制转储 Flash(在 main 中执行,避免阻塞 USB) */
	  if (*Len == 1U && Buf[0] == (uint8_t)'H') {
	      HexDump_Requested = 1U;
	      USBD_CDC_ReceivePacket(&hUsbDeviceFS);
	      return (USBD_OK);
	  }
#endif

#if ENABLE_ERASE
	  // 如果收到 'E',执行全片擦除(用于手动触发)
	  if (Buf[0] == 'E' && *Len == 1) {
	      W25Q_EraseChip();
	      uint8_t msg[] = "Erased\r\n";
	      CDC_Transmit_FS(msg, 8);
	      USBD_CDC_ReceivePacket(&hUsbDeviceFS);
	      return (USBD_OK);
	  }
#endif

	  // 累加数据到页缓冲区
	  memcpy(&Page_Buffer[Buf_Idx], Buf, *Len);
	  Buf_Idx += *Len;

	  // 凑满一页(256字节)交给 main 处理
	  if (Buf_Idx >= 256) {
	      Data_Ready = 1;
	      debug_buf_idx_array[array_idx % 1024] = Buf_Idx;
	      array_idx ++;
	      // 注意:这里暂时不调用 ReceivePacket,等 main 写完再开
	  } else {
	      USBD_CDC_ReceivePacket(&hUsbDeviceFS);
	  }
	  return (USBD_OK);
  /* USER CODE END 6 */
}

5. 烧录固件

运行程序后,按下述步骤在 PC 端发送固件:

  • 在串口终端(如 Tera Term)中选择名为"USB CDC"的虚拟串口并打开连接:

  • 在终端菜单选择 文件 → 发送文件,选择要烧录的二进制文件并开始发送:

  • 文件发送完成后,在开发板上按下用户键 B1,将缓冲区中剩余的数据写入 Flash:

6. 验证烧录结果

烧录完成后,定义宏 ENABLE_HEXDUMP 可启用十六进制转储功能:在串口发送单字节 'H' 将请求转储 Flash 内容以便验证。转储长度由 W25Q_HEX_DUMP_LEN 控制,可按需调整。

相关推荐
白掰虾2 小时前
STM32CubeMX2教程——STM32C5 UART
stm32·单片机·嵌入式硬件·mcu·usart·stm32cubemx2·stm32c542
我不是程序猿儿3 小时前
【嵌入式】面向 STM32 的 ADC 与 DMA 学习路线
linux·stm32·单片机·嵌入式硬件·学习
一支闲人3 小时前
研究STM32库函数 如何操作寄存器
stm32·单片机
rit84324994 小时前
基于STM32的触控USB鼠标设计
stm32·嵌入式硬件·计算机外设
天狼IoT4 小时前
STM32-keil+CubeMX快速开发:GPIO功能
stm32·单片机·嵌入式硬件
ytttr8734 小时前
STM32 独立看门狗(IWDG)程序设计与实现
stm32·单片机·嵌入式硬件
济6174 小时前
FreeRTOS 通信任务设计(2)----UART+DMA 环形缓冲 + 空闲中断+ 流缓冲区--- 高效接收方案详解
stm32·单片机·嵌入式·freertos
济6175 小时前
FreeRTOS 通信任务设计(1)---STM32 串口 DMA + 协议帧解析 + CRC 校验全流程详解
stm32·嵌入式·freertos
Tomhex5 小时前
stm32将JTAG/SWD接口误设GPIO模式后无法调试
stm32