You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

W25Q32FV SPI FLASH与STM32F103通信及调试问题咨询

STM32F103 对接 W25Q32FV SPI Flash 问题排查与解决

嗨,咱们一步步拆解你的问题——你的代码里有几个核心错误,既导致了Flash操作失效,也引发了调试连接的问题,先把这些问题点出来,再解答你的疑问:

你的代码里的致命问题

  1. CS引脚完全没正确管理:SPI Flash的CS是低有效,每一个独立的指令操作(写使能、页编程、读数据等)都必须遵循「拉低CS → 执行SPI传输 → 拉高CS」的流程。但你的代码全程反复拉低CS,从来没拉高过!这会让Flash一直处于选中状态,总线持续被占用,不仅会让Flash无法正确解析指令,甚至可能干扰MCU的调试接口(SWD/JTAG),导致烧录后连不上调试器——这就是你遇到「NO TARGET CONNECTED」的核心原因。
  2. 指令不能合并传输:写使能(0x06)是一个独立的操作,必须单独完成「拉低CS→发指令→拉高CS」,之后再执行页编程的操作。你把写使能和页编程指令塞在同一个SPI传输里,Flash根本无法正确识别这两个独立指令。
  3. 没等待Flash忙状态:W25Q32在执行页编程、擦除时,内部会进入忙碌状态,必须等状态寄存器的BUSY位清零后才能执行下一个指令。你用Delay代替忙状态检查,不仅不可靠,还可能在上一个操作没完成时就发新指令,导致指令混乱。

你的疑问解答

1. 是否需要先发送ID来初始化,告知MCU已连接FLASH?

不需要专门的「初始化告知」,但读取ID是非常重要的调试手段:发送读JEDEC ID指令(0x9F),可以读取Flash的制造商ID(0xEF)和设备ID(0x16,对应W25Q32)。这一步能快速验证SPI总线连接是否正常、CS引脚控制是否正确,是排查硬件和通信问题的第一步——建议你在程序开头先做ID读取,确认通信没问题再进行读写操作。

2. 如何发送24位地址?写入FLASH的起始地址应从哪里开始?

  • 24位地址发送:W25Q32的地址是24位,分3个字节,高位在前(大端模式)。比如要写入地址0x000001,你需要依次发送0x00(最高字节)、0x00(中间字节)、0x01(最低字节)——这部分你代码里的地址格式是对的,但要注意是在页编程指令(0x02)之后单独发送这三个字节,再发送数据。
  • 写入起始地址:W25Q32总容量是32Mbit(4MB),地址范围是0x000000到0x3FFFFF,可以从0x000000开始写入,但要注意:页编程不能跨页(每页256字节)。比如你从0x0000FF开始写,最多只能写1个字节(因为0x0000FF是第0页的最后一个地址,下一个地址0x000100属于第1页),跨页的话数据会绕回当前页的起始位置,导致数据错误。

3. 烧录该代码后无法进入调试模式的原因是什么?

主要有两个原因:

  • SPI总线冲突干扰调试接口:你全程拉低CS,Flash一直处于选中状态,SPI总线持续被占用,虽然F103的SPI1引脚(PA5/6/7)和SWD调试引脚(PA13/14)没有复用冲突,但总线持续异常可能导致MCU进入不稳定状态,让调试器无法连接。
  • 错误指令导致MCU跑飞:混乱的SPI指令序列可能触发MCU的总线错误或者HardFault异常,导致程序跑飞,调试器无法正常连接。你现在用的「复位+擦除Flash」的方法是对的,修正代码后重新烧录就能恢复。

修正后的操作流程参考

给你一套标准的W25Q32操作代码,你可以直接参考:

1. 等待Flash忙状态的工具函数

void W25Q_WaitBusy(void)
{
    uint8_t status = 0x01;
    // 拉低CS,发送读状态寄存器指令(0x05)
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET);
    HAL_SPI_Transmit(&hspi1, &(uint8_t){0x05}, 1, 50);
    // 循环读取状态,直到BUSY位(bit0)清零
    do {
        HAL_SPI_Receive(&hspi1, &status, 1, 50);
    } while ((status & 0x01) != 0);
    // 拉高CS结束操作
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET);
}

2. 页编程函数(写入数据)

// 注意:写入前必须确保目标地址所在区域已被擦除(Flash只能从1写为0,擦除才能恢复为1)
// addr: 写入起始地址; data: 待写入数据; len: 写入长度(不能超过256 - addr%256,即不能跨页)
void W25Q_PageProgram(uint32_t addr, uint8_t *data, uint16_t len)
{
    // 第一步:发送写使能指令
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET);
    uint8_t cmd = 0x06;
    HAL_SPI_Transmit(&hspi1, &cmd, 1, 50);
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET);

    // 等待写使能完成(保险起见)
    W25Q_WaitBusy();

    // 第二步:发送页编程指令+地址+数据
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET);
    uint8_t tx_buff[4] = {
        0x02, // 页编程指令
        (addr >> 16) & 0xFF, // 地址高字节
        (addr >> 8) & 0xFF,  // 地址中字节
        addr & 0xFF          // 地址低字节
    };
    // 先发送指令和地址
    HAL_SPI_Transmit(&hspi1, tx_buff, 4, 50);
    // 发送待写入数据
    HAL_SPI_Transmit(&hspi1, data, len, 50);
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET);

    // 等待编程完成
    W25Q_WaitBusy();

    // 可选:发送写禁用指令(Flash编程完成后会自动禁用写,手动发送也可)
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET);
    cmd = 0x04;
    HAL_SPI_Transmit(&hspi1, &cmd, 1, 50);
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET);
}

3. 读数据函数

// addr: 读取起始地址; buff: 存储读取数据的缓冲区; len: 读取长度
void W25Q_ReadData(uint32_t addr, uint8_t *buff, uint16_t len)
{
    W25Q_WaitBusy();

    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET);
    uint8_t tx_buff[4] = {
        0x03, // 读数据指令
        (addr >> 16) & 0xFF,
        (addr >> 8) & 0xFF,
        addr & 0xFF
    };
    // 发送读指令和地址
    HAL_SPI_Transmit(&hspi1, tx_buff, 4, 50);
    // 接收数据
    HAL_SPI_Receive(&hspi1, buff, len, 50);
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET);
}

4. 读取ID验证通信

// 返回W25Q32的JEDEC ID,正常应为0xEF1600
uint32_t W25Q_ReadID(void)
{
    uint32_t id = 0;
    uint8_t rx_buff[3];

    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_RESET);
    uint8_t cmd = 0x9F; // 读JEDEC ID指令
    HAL_SPI_Transmit(&hspi1, &cmd, 1, 50);
    HAL_SPI_Receive(&hspi1, rx_buff, 3, 50);
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_2, GPIO_PIN_SET);

    id = (rx_buff[0] << 16) | (rx_buff[1] << 8) | rx_buff[2];
    return id;
}

测试示例(main函数中)

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_SPI1_Init();

    // 先验证SPI通信是否正常
    uint32_t flash_id = W25Q_ReadID();
    if(flash_id != 0xEF1600) {
        // ID不对,说明硬件或通信有问题,这里可以加LED提示
        while(1);
    }

    uint8_t write_data = 0x11;
    uint32_t write_addr = 0x000001;
    // 注意:如果该地址所在扇区未擦除,需要先执行扇区擦除(指令0x20)
    // W25Q_SectorErase(write_addr); // 可以自己实现这个函数,发送0x20+24位地址,然后等忙

    // 写入数据
    W25Q_PageProgram(write_addr, &write_data, 1);

    // 读取数据验证
    uint8_t read_data = 0;
    W25Q_ReadData(write_addr, &read_data, 1);
    if(read_data == write_data) {
        // 读写成功,比如点亮LED提示
    }

    while (1)
    {
        // 主循环
    }
}

额外提醒:W25Q32的写入必须在擦除后的区域进行(Flash只能将1改为0,擦除才能把0恢复为1),如果目标地址之前有非0xFF的数据,必须先擦除对应的扇区(4KB)、块(32KB/64KB)或者整片Flash,否则写入会失败。

内容的提问来源于stack exchange,提问作者Devjeet Mandal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 08:07:32