当前位置: 首页 > news >正文

别再只用内部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片选信号(低有效)
PB13CLKSPI时钟线
PB14DO(MISO)主入从出数据线
PB15DI(MOSI)主出从入数据线
3.3VVCC电源(2.7-3.6V)
GNDGND地线

提示:WP(写保护)和HOLD引脚可接高电平,若不需要相关功能

2. 图片数据处理与存储方案

2.1 图片格式转换实战

TFT液晶屏通常需要RGB565格式的位图数据,而常见的JPEG/PNG等图片格式需要经过转换才能使用。推荐使用Img2Lcd工具进行转换:

  1. 打开图片文件,设置输出格式为"16位真彩色"
  2. 选择"水平扫描"和"高位在前"模式
  3. 生成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 系统架构设计

完整的图片显示系统包含以下组件:

  1. 存储层:W25Q128负责图片数据存储
  2. 驱动层:SPI接口通信与Flash控制
  3. 应用层:图片缓存管理与显示控制
  4. 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 数据写入失败排查

若遇到写入失败,可按以下步骤排查:

  1. 检查写使能指令是否成功发送
    uint8_t status = W25Q128_ReadStatusReg1(); if(!(status & 0x02)) { // 写使能失败 }
  2. 确认目标扇区已擦除(Flash只能将1改为0)
  3. 检查电源电压是否稳定(不低于2.7V)
  4. 验证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%。

http://www.cnnetsun.cn/news/2062413.html

相关文章:

  • 从HEX到BIN:解析嵌入式固件格式差异与Keil实战生成指南
  • 终极隐私安全方案:3步部署免费本地语音转文字工具,实现高效实时语音识别
  • TranslucentTB开机自启动3步完美解决方案:告别启动失败的烦恼
  • Legacy-iOS-Kit终极指南:如何为旧款iPhone和iPad设备降级并提升性能
  • torch.set_grad_enabled(False) 和 torch.no_grad() 到底该用哪个?一份给PyTorch开发者的选择指南
  • 终极指南:4步让旧Mac免费运行最新macOS系统
  • 如何快速掌握Maya到glTF转换:5步解决跨平台兼容难题
  • # 软考软件设计师 · 每日一练 2026-04-24
  • DLSS Swapper终极指南:3分钟掌握游戏性能优化神器
  • YOLOv4性能提升的秘密武器:深入拆解CSP Darknet53中的Mish激活函数与CSP结构
  • Stable Diffusion ControlNet Reference实战:从“垫图”到商业级应用,我的避坑参数全记录
  • MacOS Qt 5开发环境配置实战:从安装到疑难问题排查
  • TrollInstallerX完整技术指南:在iOS 14.0-16.6.1设备上安装TrollStore的解决方案
  • 告别会员费!用Docker小雅+阿里云盘打造家庭影院,实测VidHub/Reex哪个更好用?
  • 为什么电力系统优化研究,很多人还在用Matlab的YALMIP+CPLEX?
  • 【限时技术解禁】Docker 27边缘编排内核级优化白皮书:仅开放72小时,含6类硬件适配参数表与压测基准数据
  • 用STM32的PWM让蜂鸣器唱首歌:从《小星星》到自定义音乐盒的实战教程
  • FPGA课程设计避坑指南:搞定MIPS模型机功能测试的完整流程(含代码与仿真)
  • 别再为CSS渐变圆角边框发愁了!5种方法优缺点实测,mask遮罩法才是真香
  • 老Mac重生指南:用OpenCore Legacy Patcher解锁最新macOS系统
  • CVPR2024投稿全攻略:从CCF_A顶会视角解析计算机视觉论文成功之道
  • Win10隐私保护小技巧:彻底关闭文件资源管理器里的‘最近浏览’记录
  • SAP SD核心主数据全解析:从客户、物料到定价的实战配置
  • Elsevier Tracker终极指南:3分钟免费搞定Elsevier期刊审稿状态追踪
  • Flux2-Klein-9B-True-V2部署教程:解决‘模型加载失败’常见错误五步法
  • 互联网大厂 Java 求职面试:从音视频场景探讨微服务架构
  • 高效构建REFramework游戏Mod开发环境:专业开发者实战指南
  • 在M1/M2 Mac上玩转虚拟机:VMware Fusion Player安装与Windows 11 ARM版避坑指南
  • 当游戏日常成为负担:如何用智能助手重获游戏乐趣
  • 别再手动填0了!用TI Hex6x工具链高效生成DSP可执行文件(bin/dat)