别再只用内部Flash了!手把手教你用STM32F103的SPI驱动W25Q128存储图片(附完整代码)
STM32F103实战:用SPI驱动W25Q128实现TFT屏图片存储与显示
当你用STM32驱动TFT屏幕开发显示类项目时,是否遇到过这样的困境:精心设计的UI界面因为内部Flash容量不足而被迫删减?240x240分辨率的彩色图片,在STM32F103的512KB内部Flash中仅能存放4张左右。这种限制让许多开发者头疼不已。本文将带你彻底解决这个痛点,通过外置W25Q128 Flash芯片扩展存储空间,实现图片的高效存取与显示。
1. 硬件方案设计与核心器件选型
1.1 为什么选择W25Q128?
在众多外部存储方案中,W25Q128系列SPI Flash凭借其优异的性价比成为STM32项目的理想选择。与SD卡等NAND Flash相比,NOR Flash结构的W25Q128具有以下优势:
- 可靠性高:不存在坏块问题,数据保存期限可达20年
- 读取速度快:支持104MHz时钟频率,远高于SD卡的SPI模式
- 接口简单:标准4线SPI接口,占用MCU引脚少
- 容量适中:16MB(128Mbit)容量可存储约50张240x240的16位色图片
// W25Q128关键参数定义 #define W25Q128_PAGE_SIZE 256 // 页大小(字节) #define W25Q128_SECTOR_SIZE 4096 // 扇区大小(字节) #define W25Q128_BLOCK_SIZE 65536 // 块大小(字节) #define W25Q128_TOTAL_SIZE 16777216 // 总容量(字节)1.2 硬件连接示意图
典型的STM32F103与W25Q128连接方式如下:
| STM32引脚 | W25Q128引脚 | 功能说明 |
|---|---|---|
| PB12 | /CS | 片选信号(低有效) |
| PB13 | CLK | SPI时钟线 |
| PB14 | DO(MISO) | 主入从出数据线 |
| PB15 | DI(MOSI) | 主出从入数据线 |
| 3.3V | VCC | 电源(2.7-3.6V) |
| GND | GND | 地线 |
提示:WP(写保护)和HOLD引脚可接高电平,若不需要相关功能
2. 图片数据处理与存储方案
2.1 图片格式转换实战
TFT液晶屏通常需要RGB565格式的位图数据,而常见的JPEG/PNG等图片格式需要经过转换才能使用。推荐使用Img2Lcd工具进行转换:
- 打开图片文件,设置输出格式为"16位真彩色"
- 选择"水平扫描"和"高位在前"模式
- 生成C语言数组格式的数据文件
转换后的图片数据示例:
const unsigned char gImage_test[115200] = { /* 0X00,0X10,0X01,0XE0,0X03,0X20,0X01, */ 0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF, 0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF,0XFF, // 更多数据... };2.2 存储空间规划策略
合理的存储规划能显著提升访问效率。针对240x240的16位色图片(115200字节),建议按如下方式组织:
- 每张图片占用28个扇区(28×4096=114688字节)
- 预留512字节存储图片元信息(名称、尺寸等)
- 建立文件分配表(FAT)记录图片存储位置
// 图片信息结构体示例 typedef struct { char name[16]; // 图片名称 uint32_t addr; // 起始地址 uint16_t width; // 宽度 uint16_t height; // 高度 uint32_t size; // 数据大小 } ImageInfo;3. SPI驱动实现与优化技巧
3.1 SPI初始化关键配置
STM32F103的SPI接口配置需要特别注意时钟相位和极性的匹配:
void SPI2_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; // 使能时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE); // 配置SPI引脚 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_15; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_14; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, &GPIO_InitStructure); // SPI配置 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; // 时钟极性 SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; // 时钟相位 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI2, &SPI_InitStructure); SPI_Cmd(SPI2, ENABLE); }3.2 高效读写函数实现
为提高传输效率,我们实现带DMA支持的批量读写函数:
void W25Q128_Read_DMA(uint32_t addr, uint8_t *buf, uint32_t len) { uint8_t cmd[4] = { W25X_ReadData, (uint8_t)(addr >> 16), (uint8_t)(addr >> 8), (uint8_t)addr }; FLASH_CS_LOW(); SPI2_SendBytes(cmd, 4); // 发送读命令和地址 SPI2_ReceiveBytes(buf, len); // DMA方式接收数据 FLASH_CS_HIGH(); } void W25Q128_Write_Page(uint32_t addr, uint8_t *buf) { W25Q128_WriteEnable(); FLASH_CS_LOW(); SPI2_SendByte(W25X_PageProgram); SPI2_SendByte((uint8_t)(addr >> 16)); SPI2_SendByte((uint8_t)(addr >> 8)); SPI2_SendByte((uint8_t)addr); SPI2_SendBytes(buf, W25Q128_PAGE_SIZE); FLASH_CS_HIGH(); W25Q128_WaitBusy(); }注意:写入前必须确保目标扇区已被擦除,Flash写入只能将1变为0
4. 完整图片显示系统实现
4.1 系统架构设计
完整的图片显示系统包含以下组件:
- 存储层:W25Q128负责图片数据存储
- 驱动层:SPI接口通信与Flash控制
- 应用层:图片缓存管理与显示控制
- UI层:TFT液晶屏驱动与刷新
[图片文件] → [格式转换] → [存储到W25Q128] ↓ [TFT显示屏] ← [读取解码] ← [缓存管理]4.2 关键代码实现
图片显示的核心流程代码示例:
void Show_Image(uint16_t x, uint16_t y, const char *name) { ImageInfo info; uint8_t buffer[512]; // 局部缓冲区 // 1. 从Flash读取图片信息 W25Q128_Read(IMAGE_TABLE_ADDR, (uint8_t*)&info, sizeof(ImageInfo)); // 2. 设置TFT显示窗口 LCD_SetWindow(x, y, x+info.width-1, y+info.height-1); // 3. 分段读取并显示图片 uint32_t remain = info.size; uint32_t addr = info.addr; while(remain > 0) { uint32_t chunk = (remain > sizeof(buffer)) ? sizeof(buffer) : remain; W25Q128_Read_DMA(addr, buffer, chunk); // 通过FSMC接口快速写入TFT LCD_WriteDataBuffer((uint16_t*)buffer, chunk/2); addr += chunk; remain -= chunk; } }4.3 性能优化技巧
- 双缓冲技术:在读取下一块数据时显示当前块内容
- 数据预取:提前读取即将显示的图片数据
- SPI时钟优化:将预分频系数调整为2(36MHz SPI时钟)
- 关键代码放RAM:将显示相关函数通过
__attribute__((section(".ramfunc")))放入RAM执行
// RAM执行函数示例 void __attribute__((section(".ramfunc"))) LCD_WriteDataBuffer(uint16_t *buf, uint32_t len) { while(len--) { *(__IO uint16_t*)LCD_DATA_ADDR = *buf++; } }5. 常见问题与解决方案
5.1 数据写入失败排查
若遇到写入失败,可按以下步骤排查:
- 检查写使能指令是否成功发送
uint8_t status = W25Q128_ReadStatusReg1(); if(!(status & 0x02)) { // 写使能失败 } - 确认目标扇区已擦除(Flash只能将1改为0)
- 检查电源电压是否稳定(不低于2.7V)
- 验证SPI时序是否符合规格书要求
5.2 显示出现杂点的处理
图片显示出现杂点通常由以下原因导致:
- SPI时钟干扰:缩短走线长度,增加上拉电阻
- 电源噪声:在VCC与GND间添加0.1μF去耦电容
- 数据对齐问题:确保RGB565数据高位在前
- Flash读取错误:实现简单的CRC校验机制
uint16_t Calc_CRC16(uint8_t *data, uint32_t len) { uint16_t crc = 0xFFFF; while(len--) { crc ^= *data++; for(uint8_t i=0; i<8; i++) { if(crc & 0x0001) { crc >>= 1; crc ^= 0xA001; } else { crc >>= 1; } } } return crc; }6. 进阶应用:实现图片动态切换
6.1 图片索引表设计
为方便管理多张图片,可在Flash开头建立索引表:
#define MAX_IMAGES 50 typedef struct { uint16_t count; // 图片总数 ImageInfo images[MAX_IMAGES]; // 图片信息数组 uint16_t crc; // 校验值 } ImageTable; void Init_ImageTable(void) { ImageTable table; table.count = 0; table.crc = Calc_CRC16((uint8_t*)&table, sizeof(ImageTable)-2); W25Q128_Write(IMAGE_TABLE_ADDR, (uint8_t*)&table, sizeof(ImageTable)); }6.2 平滑过渡效果实现
通过分步刷新实现图片切换动画:
void Transition_Effect(uint16_t x, uint16_t y, const char *new_img) { // 1. 读取新图片到缓冲区 ImageInfo info; W25Q128_Read(IMAGE_TABLE_ADDR, (uint8_t*)&info, sizeof(ImageInfo)); // 2. 分块渐变显示 for(uint16_t i=0; i<info.height; i+=8) { uint16_t chunk_height = (info.height-i) > 8 ? 8 : (info.height-i); W25Q128_Read_DMA(info.addr + i*info.width*2, buffer, info.width*chunk_height*2); LCD_SetWindow(x, y+i, x+info.width-1, y+i+chunk_height-1); LCD_WriteDataBuffer((uint16_t*)buffer, info.width*chunk_height); Delay_ms(20); } }在实际项目中,我发现合理设置SPI时钟分频对系统稳定性影响很大。当使用72MHz主频时,SPI2的预分频设为4(18MHz)既能保证速度又不会导致通信错误。另外,将频繁调用的显示函数放入RAM执行,可使刷新速度提升约30%。
