一、MySPI.c(最底层 SPI 驱动)
#include "stm32f10x.h" /************************* 硬件引脚操作层 *************************/ // 控制CS片选引脚(PA4) void MySPI_W_SS(uint8_t BitValue) { GPIO_WriteBit(GPIOA, GPIO_Pin_4, BitValue); } // 控制SCK时钟引脚(PA5) void MySPI_W_SCK(uint8_t BitValue) { GPIO_WriteBit(GPIOA, GPIO_Pin_5, BitValue); } // 控制MOSI引脚(PA7):主机输出,从机输入 void MySPI_W_MOSI(uint8_t BitValue) { GPIO_WriteBit(GPIOA, GPIO_Pin_7, BitValue); } // 读取MISO引脚(PA6):主机输入,从机输出 uint8_t MySPI_R_MISO(void) { return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6); } /************************* SPI初始化函数 *************************/ // 上电只调用一次,配置SPI引脚工作模式 void MySPI_Init(void) { // 开启GPIOA时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef GPIO_InitStructure; // 配置SS、SCK、MOSI为推挽输出 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置MISO为上拉输入 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_Init(GPIOA, &GPIO_InitStructure); // SPI模式0默认状态:SS拉高,SCK拉低 MySPI_W_SS(1); MySPI_W_SCK(0); } /************************* SPI通信控制函数 *************************/ // 开始SPI通信:拉低CS,选中从机 void MySPI_Start(void) { MySPI_W_SS(0); } // 结束SPI通信:拉高CS,取消选中从机 void MySPI_Stop(void) { MySPI_W_SS(1); } /************************* 核心:交换一个字节 *************************/ // SPI全双工通信:发1个字节,同时收1个字节 uint8_t MySPI_SwapByte(uint8_t ByteSend) { uint8_t i, ByteReceive = 0x00; // 循环8次,逐位收发 for(i = 0; i < 8; i++) { // 发送当前位(从最高位开始) MySPI_W_MOSI(!!(ByteSend & (0x80 >> i))); // SCK拉高,产生上升沿 MySPI_W_SCK(1); // 读取当前位 if(MySPI_R_MISO() == 1) { ByteReceive |= (0x80 >> i); } // SCK拉低,产生下降沿 MySPI_W_SCK(0); } return ByteReceive; }
二、W25Q64.c(Flash 芯片驱动)
#include "stm32f10x.h" #include "MySPI.h" #include "W25Q64_Ins.h" /************************* W25Q64初始化 *************************/ void W25Q64_Init(void) { MySPI_Init(); } /************************* 读取芯片ID *************************/ // 验证芯片是否正常连接 void W25Q64_ReadID(uint8_t *MID, uint16_t *DID) { MySPI_Start(); // 发送读ID指令 MySPI_SwapByte(W25Q64_JEDEC_ID); // 接收3个字节的ID *MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); *DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); *DID <<= 8; *DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); MySPI_Stop(); } /************************* 写使能 *************************/ // 所有写入/擦除操作前必须调用 void W25Q64_WriteEnable(void) { MySPI_Start(); MySPI_SwapByte(W25Q64_WRITE_ENABLE); MySPI_Stop(); } /************************* 等待忙 *************************/ // 所有写入/擦除操作后必须调用 void W25Q64_WaitBusy(void) { uint32_t Timeout; MySPI_Start(); MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1); Timeout = 100000; // 循环等待忙标志位清零 while((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01) { Timeout--; if(Timeout == 0) break; } MySPI_Stop(); } /************************* 页编程(写入数据) *************************/ // 最多写入256字节(1页) void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count) { uint16_t i; // 先写使能 W25Q64_WriteEnable(); MySPI_Start(); // 发送页编程指令 MySPI_SwapByte(W25Q64_PAGE_PROGRAM); // 发送24位地址 MySPI_SwapByte(Address >> 16); MySPI_SwapByte(Address >> 8); MySPI_SwapByte(Address); // 循环发送数据 for(i = 0; i < Count; i++) { MySPI_SwapByte(DataArray[i]); } MySPI_Stop(); // 等待写入完成 W25Q64_WaitBusy(); } /************************* 扇区擦除(4KB) *************************/ // 擦除指定地址所在的整个4KB扇区 void W25Q64_SectorErase(uint32_t Address) { W25Q64_WriteEnable(); MySPI_Start(); // 发送扇区擦除指令 MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB); // 发送24位地址 MySPI_SwapByte(Address >> 16); MySPI_SwapByte(Address >> 8); MySPI_SwapByte(Address); MySPI_Stop(); // 等待擦除完成 W25Q64_WaitBusy(); } /************************* 读取数据 *************************/ void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count) { uint32_t i; MySPI_Start(); // 发送读数据指令 MySPI_SwapByte(W25Q64_READ_DATA); // 发送24位地址 MySPI_SwapByte(Address >> 16); MySPI_SwapByte(Address >> 8); MySPI_SwapByte(Address); // 循环读取数据 for(i = 0; i < Count; i++) { DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE); } MySPI_Stop(); }
三、main.c(应用层主函数)
#include "stm32f10x.h" #include "OLED.h" #include "Delay.h" #include "W25Q64.h" // 全局变量 uint8_t MID; // 制造商ID uint16_t DID; // 设备ID uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04}; // 待写入数据 uint8_t ArrayRead[4]; // 读取数据缓存 int main(void) { // 初始化外设 OLED_Init(); W25Q64_Init(); // 显示固定标题 OLED_ShowString(1, 1, "MID: DID:"); OLED_ShowString(2, 1, "W:"); OLED_ShowString(3, 1, "R:"); // 读取并显示芯片ID W25Q64_ReadID(&MID, &DID); OLED_ShowHexNum(1, 5, MID, 2); OLED_ShowHexNum(1, 12, DID, 4); // Flash读写流程:擦除→写入→读取 W25Q64_SectorErase(0x000000); W25Q64_PageProgram(0x000000, ArrayWrite, 4); W25Q64_ReadData(0x000000, ArrayRead, 4); // 主循环:刷新显示 while(1) { // 显示写入的数据 OLED_ShowHexNum(2, 3, ArrayWrite[0], 2); OLED_ShowHexNum(2, 6, ArrayWrite[1], 2); OLED_ShowHexNum(2, 9, ArrayWrite[2], 2); OLED_ShowHexNum(2, 12, ArrayWrite[3], 2); // 显示读取的数据 OLED_ShowHexNum(3, 3, ArrayRead[0], 2); OLED_ShowHexNum(3, 6, ArrayRead[1], 2); OLED_ShowHexNum(3, 9, ArrayRead[2], 2); OLED_ShowHexNum(3, 12, ArrayRead[3], 2); } }
四、我踩过的所有坑(血泪教训汇总)
1. 硬件接线坑(90% 的问题出在这里)
| 错误现象 | 错误原因 | 正确做法 |
|---|
读 ID 全是FFFF | MISO 和 MOSI 接反了 | SPI 同名字相接:MISO 接 MISO,MOSI 接 MOSI,不要交叉 |
读 ID 全是FFFF | 接了 5V 电源 | W25Q64 是 3.3V 芯片,绝对不能接 5V,会直接烧坏 |
读 ID 全是FFFF | 没有共地 | 必须接 GND,两个设备电平参考要一致 |
读 ID 全是FFFF | CS 引脚接错 | CS 必须接 PA4,不能接其他引脚 |
| 偶尔能读到 ID,偶尔读不到 | 杜邦线接触不良 | 换一根杜邦线,或者重新拔插 |
2. MySPI.c 代码坑
| 错误现象 | 错误原因 | 正确做法 |
|---|
读 ID 全是00 | 忘记开 GPIOA 时钟 | 初始化时必须先开时钟 |
读 ID 全是FF | MISO 引脚配置成了输出 / 下拉 / 浮空 | MISO 必须配置成上拉输入 |
读 ID 全是FF | SCK 默认电平是高电平 | SPI 模式 0 要求 SCK 默认低电平 |
| 读 ID 乱码 | MySPI_SwapByte里少加了括号 | 必须写成(ByteSend & (0x80 >> i)),>>优先级比&高 |
| 读 ID 乱码 | 少加了!! | 必须加!!把非 0 值统一变成 1,保证电平标准 |
| 读 ID 乱码 | 从最低位开始发送 | 必须从最高位(第 7 位)开始发送 |
3. W25Q64.c 代码坑
| 错误现象 | 错误原因 | 正确做法 |
|---|
写不进去数据,读出来全是FF | 写使能和MySPI_Start()顺序反了 | 必须先写使能,再拉 CS |
| 程序一直卡着不动 | WaitBusy和MySPI_Stop()顺序反了 | 必须先拉 CS,再等待忙 |
| ID 读出来只有低 8 位 | ReadID里少了 ` | ` | 必须写成 `*DID | = MySPI_SwapByte(...)` |
| 编译报错 “未定义的函数” | 函数名写错了 | 注意下划线:W25Q64_ReadData,不是W25Q64ReadData |
| 读出来的数据和写入的不一样 | 没有先擦除就写入 | Flash 只能把 1 变成 0,写之前必须先擦除 |
| 写入超过 256 字节数据出错 | 页编程最多写 256 字节 | 超过 256 字节要分多次写入 |
4. main.c 代码坑
| 错误现象 | 错误原因 | 正确做法 |
|---|
| 显示乱码 | 显示代码只执行了一次 | 把显示代码放到while(1)里循环刷新 |
| 写入的数据断电后丢失 | 没有等待忙就断电 | 写入 / 擦除后必须等待忙完成 |
五.创建 W25Q64_Ins.h 头文件
#ifndef __W25Q64_INS_H #define __W25Q64_INS_H #define W25Q64_WRITE_ENABLE 0x06 #define W25Q64_WRITE_DISABLE 0x04 #define W25Q64_READ_STATUS_REGISTER_1 0x05 #define W25Q64_READ_STATUS_REGISTER_2 0x35 #define W25Q64_WRITE_STATUS_REGISTER 0x01 #define W25Q64_PAGE_PROGRAM 0x02 #define W25Q64_QUAD_PAGE_PROGRAM 0x32 #define W25Q64_BLOCK_ERASE_64KB 0xD8 #define W25Q64_BLOCK_ERASE_32KB 0x52 #define W25Q64_SECTOR_ERASE_4KB 0x20 #define W25Q64_CHIP_ERASE 0xC7 #define W25Q64_ERASE_SUSPEND 0x75 #define W25Q64_ERASE_RESUME 0x7A #define W25Q64_POWER_DOWN 0xB9 #define W25Q64_HIGH_PERFORMANCE_MODE 0xA3 #define W25Q64_CONTINUOUS_READ_MODE_RESET 0xFF #define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID 0xAB #define W25Q64_MANUFACTURER_DEVICE_ID 0x90 #define W25Q64_READ_UNIQUE_ID 0x4B #define W25Q64_JEDEC_ID 0x9F #define W25Q64_READ_DATA 0x03 #define W25Q64_FAST_READ 0x0B #define W25Q64_FAST_READ_DUAL_OUTPUT 0x3B #define W25Q64_FAST_READ_DUAL_IO 0xBB #define W25Q64_FAST_READ_QUAD_OUTPUT 0x6B #define W25Q64_FAST_READ_QUAD_IO 0xEB #define W25Q64_OCTAL_WORD_READ_QUAD_IO 0xE3 #define W25Q64_DUMMY_BYTE 0xFF #endif
六.总结
- 遇到全是 FFFF,先查接线再改代码
- SPI 交换字节,括号和!!一个都不能少
- 写擦操作顺序:先使能,再拉 CS,最后等忙
- Flash 铁律:写前必须擦,只能 1 变 0
- 函数名别写错,下划线不能少