单片机Flash不够用?手把手教你用AT24C256存储30张BMP图片(附完整代码)
单片机Flash不够用?手把手教你用AT24C256存储30张BMP图片(附完整代码)
当我在开发一个需要显示30张BMP图片的OLED项目时,遇到了一个棘手的问题:单片机的Flash存储空间不足。经过一番探索,我发现AT24C256这颗EEPROM芯片完美解决了我的存储难题。下面我将分享整个解决方案的实现过程,包括芯片选型、I2C驱动编写、图片数据分割存储与读取显示等关键步骤。
1. 问题分析与芯片选型
在嵌入式开发中,存储空间往往是限制项目功能扩展的重要因素。以128×64分辨率的BMP图片为例,每张图片需要1024字节的存储空间。30张这样的图片就需要30×1024=30720字节的存储空间。
常见的单片机如STM32F103C8T6仅有64KB Flash,除去程序本身占用的空间,剩余空间往往不足以存储大量图片资源。这时,外部存储方案就显得尤为重要。
我最终选择了AT24C256这颗EEPROM芯片,主要基于以下考虑:
- 容量匹配:AT24C256提供32768字节存储空间,刚好满足30张图片的存储需求
- 接口简单:采用I2C接口,仅需两根信号线即可通信
- 非易失性:数据断电不丢失
- 低功耗:适合电池供电设备
注意:芯片手册标注的256K指的是256Kbit(256×1024bit=32768字节),而非256K字节,这是选型时容易混淆的地方。
2. AT24C256关键特性解析
2.1 存储结构
AT24C256的存储空间被组织为512页,每页64字节。理解这一结构对正确读写数据至关重要:
| 特性 | 参数 |
|---|---|
| 总容量 | 32768字节 |
| 页数 | 512页 |
| 每页大小 | 64字节 |
| 寻址位数 | 15位 |
2.2 寻址方式
AT24C256采用15位地址寻址,其中:
- 低6位(D5-D0):页内字节偏移地址
- 高9位(D14-D6):页地址
例如,要访问第16页第0字节的位置,地址计算如下:
0000 0100 0000 0000 (0x0400)
其中:
- 0000 0100 0 (高9位):第16页地址
- 00 0000 (低6位):页内偏移0
2.3 I2C通信地址
AT24C256的I2C设备地址由芯片上的A2、A1、A0引脚决定:
1 0 1 0 A2 A1 A0 R/W
当A2、A1、A0全部接地时:
- 写地址:0xA0
- 读地址:0xA1
3. 硬件连接与驱动实现
3.1 硬件连接
AT24C256与单片机的连接非常简单:
AT24C256 单片机
----------------------
VCC → 3.3V/5V
GND → GND
SDA → I2C_SDA
SCL → I2C_SCL
A0-A2 → GND (地址全0)
WP → GND (写保护禁用)
3.2 I2C驱动代码
首先需要实现基本的I2C通信函数。以下是基于STM32 HAL库的实现示例:
// I2C初始化
void I2C_Init(void) {
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 400000; // 400kHz
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
HAL_I2C_Init(&hi2c1);
}
// 单字节写入
HAL_StatusTypeDef AT24C256_WriteByte(uint16_t addr, uint8_t data) {
uint8_t buf[3];
buf[0] = (addr >> 8) & 0xFF; // 高字节地址
buf[1] = addr & 0xFF; // 低字节地址
buf[2] = data;
return HAL_I2C_Master_Transmit(&hi2c1, AT24C256_ADDR, buf, 3, HAL_MAX_DELAY);
}
// 单字节读取
HAL_StatusTypeDef AT24C256_ReadByte(uint16_t addr, uint8_t *data) {
uint8_t addr_buf[2];
addr_buf[0] = (addr >> 8) & 0xFF;
addr_buf[1] = addr & 0xFF;
HAL_StatusTypeDef status = HAL_I2C_Master_Transmit(&hi2c1, AT24C256_ADDR, addr_buf, 2, HAL_MAX_DELAY);
if(status != HAL_OK) return status;
return HAL_I2C_Master_Receive(&hi2c1, AT24C256_ADDR | 0x01, data, 1, HAL_MAX_DELAY);
}
4. BMP图片存储与读取方案
4.1 图片数据预处理
BMP图片需要先转换为适合OLED显示的格式。通常我们会:
- 将彩色BMP转换为单色位图
- 调整大小为128×64分辨率
- 将数据转换为字节数组
可以使用Python脚本完成这一转换:
from PIL import Image
def convert_bmp_to_array(filename):
img = Image.open(filename).convert('1') # 转换为单色
width, height = img.size
data = []
for y in range(height):
for x in range(0, width, 8):
byte = 0
for bit in range(8):
if x + bit < width:
pixel = img.getpixel((x + bit, y))
byte |= (0 if pixel else 1) << bit
data.append(byte)
return bytes(data)
4.2 图片存储策略
由于每张图片占用1024字节,我们可以将AT24C256的存储空间划分为30个连续的1024字节区块:
#define IMAGE_SIZE 1024
#define MAX_IMAGES 30
void StoreImage(uint8_t imageIndex, uint8_t *imageData) {
uint32_t startAddr = imageIndex * IMAGE_SIZE;
for(uint16_t i = 0; i < IMAGE_SIZE; i++) {
AT24C256_WriteByte(startAddr + i, imageData[i]);
HAL_Delay(5); // EEPROM写入需要时间
}
}
重要提示:EEPROM写入操作需要一定时间(典型值5ms),连续写入时必须加入适当延时,否则会导致写入失败。
4.3 图片读取与显示
读取图片数据并显示到OLED的代码示例:
void LoadAndDisplayImage(uint8_t imageIndex) {
uint32_t startAddr = imageIndex * IMAGE_SIZE;
uint8_t buffer[IMAGE_SIZE];
// 读取图片数据
for(uint16_t i = 0; i < IMAGE_SIZE; i++) {
AT24C256_ReadByte(startAddr + i, &buffer[i]);
}
// 显示到OLED
SSD1306_Clear();
for(uint8_t y = 0; y < 64; y++) {
for(uint8_t x = 0; x < 128; x += 8) {
uint8_t byte = buffer[y * 16 + x / 8];
for(uint8_t bit = 0; bit < 8; bit++) {
SSD1306_DrawPixel(x + bit, y, (byte & (1 << bit)) ? WHITE : BLACK);
}
}
}
SSD1306_Update();
}
5. 性能优化与常见问题解决
5.1 写入速度优化
AT24C256支持页写入模式,可以一次写入最多64字节,显著提高写入速度:
void AT24C256_WritePage(uint16_t addr, uint8_t *data, uint8_t len) {
uint8_t buf[66]; // 最大64数据+2地址
buf[0] = (addr >> 8) & 0xFF;
buf[1] = addr & 0xFF;
memcpy(&buf[2], data, len);
HAL_I2C_Master_Transmit(&hi2c1, AT24C256_ADDR, buf, len + 2, HAL_MAX_DELAY);
HAL_Delay(5);
}
void StoreImageFast(uint8_t imageIndex, uint8_t *imageData) {
uint32_t startAddr = imageIndex * IMAGE_SIZE;
for(uint16_t i = 0; i < IMAGE_SIZE; i += 64) {
uint8_t chunkSize = (IMAGE_SIZE - i) > 64 ? 64 : (IMAGE_SIZE - i);
AT24C256_WritePage(startAddr + i, &imageData[i], chunkSize);
}
}
5.2 常见问题与解决方案
-
写入失败
- 原因:未遵守写入周期时间
- 解决:确保每次写入后有足够延时(5ms)
-
数据损坏
- 原因:跨页写入时未处理页边界
- 解决:检查写入地址是否跨越页边界(每64字节一页)
-
读取数据错误
- 原因:I2C总线干扰
- 解决:添加上拉电阻(通常4.7kΩ)到SDA和SCL线
-
地址溢出
- 原因:访问超过32768的地址
- 解决:检查地址计算逻辑,确保不超过0x7FFF
6. 完整项目集成
将上述模块整合到一个完整的项目中,主要流程如下:
- 初始化I2C和OLED
- 检查EEPROM中是否已存储图片
- 如果没有,则从外部(如串口)接收图片数据并存储
- 循环显示存储的图片
int main(void) {
// 硬件初始化
HAL_Init();
SystemClock_Config();
I2C_Init();
SSD1306_Init();
// 检查图片是否已存储
if(!CheckImagesStored()) {
// 从串口接收并存储图片
ReceiveAndStoreImages();
}
// 循环显示图片
uint8_t currentImage = 0;
while(1) {
LoadAndDisplayImage(currentImage);
HAL_Delay(2000); // 每2秒切换一张
currentImage = (currentImage + 1) % MAX_IMAGES;
}
}
在实际项目中,我还添加了通过按钮切换图片、通过串口更新图片等功能,使整个方案更加实用。使用AT24C256存储图片后,我的单片机Flash空间从紧张变为充裕,可以添加更多功能代码而不必担心存储不足的问题。
更多推荐


所有评论(0)