每次调试OLED显示都要重新烧录程序?SD卡占用宝贵IO口还增加硬件成本?今天带你用STM32串口+W25Q64搭建一套轻量级显示资源管理系统。这个方案最吸引我的地方在于——不需要文件系统支持,却能实现字库、图标的动态更新。
在嵌入式显示项目中,我们常遇到两个头疼问题:一是中文字库占用大量Flash空间(一个16x16的GB2312字库就要近250KB);二是UI图片资源频繁修改导致反复烧录程序。传统解决方案要么挤占单片机内部Flash,要么依赖SD卡模块,而W25Q64这类SPI Flash芯片提供了第三种可能。
三种存储方案对比:
| 方案类型 | 容量限制 | 写入速度 | 硬件成本 | 动态更新便利性 |
|---|---|---|---|---|
| 内部Flash | 有限 | 慢 | 无 | 需重新烧录 |
| SD卡模块 | 大 | 快 | 中 | 即插即用 |
| W25Q64 SPI Flash | 8MB | 中 | 低 | 串口即可更新 |
实际测试发现,通过串口更新W25Q64内容时,115200波特率下传输100KB数据约需9秒,完全在可接受范围内。
接线示意图:
code复制W25Q64 STM32F103 OLED
CS ------> PC0
SCK ------> PA5 SCL ------> PB6
MISO ------> PA6 SDA ------> PB7
MOSI ------> PA7
VCC ------> 3.3V VCC ------> 3.3V
GND ------> GND GND ------> GND
c复制// SPI初始化配置
void SPI1_Init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_GPIOC|RCC_APB2Periph_SPI1, ENABLE);
// CS引脚配置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);
// SPI引脚配置
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA, &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_InitStructure.SPI_CRCPolynomial = 7;
SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
W25Q64_CS_HIGH();
}
python复制# Python转换示例
with open('font.txt', 'r') as f:
hex_data = f.read().replace('0x','').replace(',','').strip()
with open('font.bin', 'wb') as f:
f.write(bytes.fromhex(hex_data))
自定义简单协议帧格式:
code复制[开始标志0xAA][地址高位][地址低位][数据长度][数据...][校验和]
上位机关键代码(C#示例):
csharp复制private void SendFile(string filePath, uint startAddr) {
byte[] fileData = File.ReadAllBytes(filePath);
int packetSize = 256;
for(int i=0; i<fileData.Length; i+=packetSize) {
int remain = Math.Min(packetSize, fileData.Length - i);
byte[] packet = new byte[5 + remain];
packet[0] = 0xAA; // 帧头
packet[1] = (byte)((startAddr >> 8) & 0xFF);
packet[2] = (byte)(startAddr & 0xFF);
packet[3] = (byte)remain;
Array.Copy(fileData, i, packet, 4, remain);
// 计算校验和
byte checksum = 0;
for(int j=0; j<packet.Length-1; j++)
checksum ^= packet[j];
packet[packet.Length-1] = checksum;
serialPort.Write(packet, 0, packet.Length);
startAddr += (uint)remain;
}
}
code复制0x000000 - 0x0FFFFF: 字库区
- 0x000000: 12x12 ASCII
- 0x010000: 16x16 GB2312
- 0x100000: 24x24 GB2312
0x200000 - 0x7FFFFF: 图片资源区
- 每个图片占用固定64KB块
Flash读写关键操作:
c复制// 读取Flash数据到缓冲区
void W25Q64_ReadBuffer(uint8_t* pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead) {
W25Q64_CS_LOW();
SPI1_ReadWriteByte(W25Q64_CMD_READDATA);
SPI1_ReadWriteByte((ReadAddr & 0xFF0000) >> 16);
SPI1_ReadWriteByte((ReadAddr & 0xFF00) >> 8);
SPI1_ReadWriteByte(ReadAddr & 0xFF);
while(NumByteToRead--) {
*pBuffer++ = SPI1_ReadWriteByte(0xFF);
}
W25Q64_CS_HIGH();
}
// 显示指定地址的图片
void Show_Image(uint32_t addr) {
uint8_t buffer[128]; // 根据OLED宽度调整
for(int y=0; y<64; y++) { // 假设OLED高度64像素
W25Q64_ReadBuffer(buffer, addr + y*16, 16);
OLED_DisplayLine(y, buffer);
}
}
c复制// 在SPI传输期间处理其他任务
void SPI1_IRQHandler(void) {
if(SPI_I2S_GetITStatus(SPI1, SPI_I2S_IT_TXE) != RESET) {
SPI_I2S_SendData(SPI1, txBuffer[txIndex++]);
if(txIndex >= txLength) {
// 传输完成处理
}
}
}
现象1:传输数据校验失败
现象2:OLED显示乱码
现象3:Flash写入后读取异常
调试时可先用W25Q64_ReadID()验证芯片通信正常,典型返回值0xEF4017表示W25Q64
这套方案经过简单适配即可用于:
最近在一个智能家居中控项目上,我们利用这个方案实现了: