单片机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显示的格式。通常我们会:

  1. 将彩色BMP转换为单色位图
  2. 调整大小为128×64分辨率
  3. 将数据转换为字节数组

可以使用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 常见问题与解决方案

  1. 写入失败

    • 原因:未遵守写入周期时间
    • 解决:确保每次写入后有足够延时(5ms)
  2. 数据损坏

    • 原因:跨页写入时未处理页边界
    • 解决:检查写入地址是否跨越页边界(每64字节一页)
  3. 读取数据错误

    • 原因:I2C总线干扰
    • 解决:添加上拉电阻(通常4.7kΩ)到SDA和SCL线
  4. 地址溢出

    • 原因:访问超过32768的地址
    • 解决:检查地址计算逻辑,确保不超过0x7FFF

6. 完整项目集成

将上述模块整合到一个完整的项目中,主要流程如下:

  1. 初始化I2C和OLED
  2. 检查EEPROM中是否已存储图片
  3. 如果没有,则从外部(如串口)接收图片数据并存储
  4. 循环显示存储的图片
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空间从紧张变为充裕,可以添加更多功能代码而不必担心存储不足的问题。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐