W25Q32FV SPI FLASH与STM32F103通信及调试问题咨询
STM32F103 对接 W25Q32FV SPI Flash 问题排查与解决
嗨,咱们一步步拆解你的问题——你的代码里有几个核心错误,既导致了Flash操作失效,也引发了调试连接的问题,先把这些问题点出来,再解答你的疑问:
你的代码里的致命问题
- CS引脚完全没正确管理:SPI Flash的CS是低有效,每一个独立的指令操作(写使能、页编程、读数据等)都必须遵循「拉低CS → 执行SPI传输 → 拉高CS」的流程。但你的代码全程反复拉低CS,从来没拉高过!这会让Flash一直处于选中状态,总线持续被占用,不仅会让Flash无法正确解析指令,甚至可能干扰MCU的调试接口(SWD/JTAG),导致烧录后连不上调试器——这就是你遇到「NO TARGET CONNECTED」的核心原因。
- 指令不能合并传输:写使能(0x06)是一个独立的操作,必须单独完成「拉低CS→发指令→拉高CS」,之后再执行页编程的操作。你把写使能和页编程指令塞在同一个SPI传输里,Flash根本无法正确识别这两个独立指令。
- 没等待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
相关产品推荐
相关产品推荐

