概要

在嵌入式系统中,SPI(Serial Peripheral Interface)凭借其高速率、全双工、简单可靠的特性,成为微控制器(MCU)与非易失性存储器(如 AT25 系列 EEPROM、NOR FLASH)之间数据交互的首选接口。这类存储器广泛用于存储固件、配置参数和关键数据,SPI 接口则为它们提供了高效的读写通道。

一、SPI 通信系统架构

1. 基本连接方式

微控制器与非易失性存储器的 SPI 连接采用主从架构,典型硬件连接如下:
在这里插入图片描述

核心信号线功能:

SCK:串行时钟线,由主设备产生,控制数据传输时序
MOSI:主设备输出 / 从设备输入线,主设备向存储器发送命令和数据
MISO:主设备输入 / 从设备输出线,存储器向主设备返回数据
CS:片选线,主设备通过拉低该引脚选中特定存储器(多设备时用于地址区分)

2. 多设备扩展

当系统需要多个 SPI 存储器时,通过独立的 CS 线实现设备选择:

二、SPI 通信协议与存储器指令集

1. 基本通信时序

SPI 通信采用同步时钟机制,数据在 SCK 的边沿(上升沿或下降沿)被采样,常见时序模式有四种(由 CPOL 和 CPHA 决定),存储器通常支持其中 1-2 种:

CPOL=0,CPHA=0:SCK 空闲时为低电平,数据在 SCK 上升沿被采样
CPOL=0,CPHA=1:SCK 空闲时为低电平,数据在 SCK 下降沿被采样
CPOL=1,CPHA=0:SCK 空闲时为高电平,数据在 SCK 下降沿被采样
CPOL=1,CPHA=1:SCK 空闲时为高电平,数据在 SCK 上升沿被采样

AT25 系列 EEPROM 和 NOR FLASH 通常采用 CPOL=0,CPHA=0 模式。

2. 标准通信流程

MCU 与 SPI 存储器的通信遵循 “命令 - 地址 - 数据” 的交互模式:

主设备拉低 CS 引脚,选中存储器
主设备通过 MOSI 发送操作命令(1-4 字节,如读、写、擦除等)
主设备发送操作地址(根据存储器容量,通常为 2-4 字节)
数据传输阶段(读操作时从设备通过 MISO 返回数据,写操作时主设备通过 MOSI 发送数据)
操作完成后,主设备拉高 CS 引脚,结束通信

3. 常用存储器指令集

SPI 非易失性存储器定义了标准化的指令集,以下为典型指令:
在这里插入图片描述

三、关键操作实现详解

1. 数据读取操作

以 AT25DF081 NOR FLASH 为例,读取数据的具体实现:

// 从SPI存储器读取数据
// addr: 起始地址
// buf: 接收缓冲区
// len: 读取长度
void spi_memory_read(uint32_t addr, uint8_t *buf, uint32_t len) {
    // 1. 拉低片选,开始通信
    SPI_CS_LOW();
    
    // 2. 发送读命令(0x03)
    spi_send_byte(0x03);
    
    // 3. 发送3字节地址(高位在前)
    spi_send_byte((addr >> 16) & 0xFF);  // 地址高8位
    spi_send_byte((addr >> 8) & 0xFF);   // 地址中8位
    spi_send_byte(addr & 0xFF);          // 地址低8位
    
    // 4. 读取数据
    for (uint32_t i = 0; i < len; i++) {
        buf[i] = spi_receive_byte();
    }
    
    // 5. 拉高片选,结束通信
    SPI_CS_HIGH();
}

// 快速读实现(适用于高速读取,如固件加载)
void spi_memory_fast_read(uint32_t addr, uint8_t *buf, uint32_t len) {
    SPI_CS_LOW();
    
    // 发送快速读命令(0x0B)
    spi_send_byte(0x0B);
    
    // 发送3字节地址
    spi_send_byte((addr >> 16) & 0xFF);
    spi_send_byte((addr >> 8) & 0xFF);
    spi_send_byte(addr & 0xFF);
    
    // 发送1字节dummy数据(为了满足时序要求)
    spi_send_byte(0x00);
    
    // 读取数据
    for (uint32_t i = 0; i < len; i++) {
        buf[i] = spi_receive_byte();
    }
    
    SPI_CS_HIGH();
}

2. 数据写入操作

非易失性存储器的写入需要遵循特定流程(写使能→写入数据→等待完成):

// 等待存储器空闲
void spi_memory_wait_idle(void) {
    uint8_t status;
    do {
        SPI_CS_LOW();
        spi_send_byte(0x05);  // 读状态寄存器命令
        status = spi_receive_byte();
        SPI_CS_HIGH();
    } while (status & 0x01);  // 检查忙标志位(bit0)
}

// 发送写使能命令
void spi_memory_write_enable(void) {
    SPI_CS_LOW();
    spi_send_byte(0x06);  // 写使能命令
    SPI_CS_HIGH();
}

// 页写入操作(注意:不能跨页写入)
// AT25系列EEPROM通常每页256字节
void spi_memory_page_write(uint32_t addr, const uint8_t *buf, uint32_t len) {
    // 1. 检查长度是否超过一页
    if (len > 256) len = 256;
    
    // 2. 发送写使能命令
    spi_memory_write_enable();
    
    // 3. 开始页写入
    SPI_CS_LOW();
    
    // 4. 发送页写命令(0x02)
    spi_send_byte(0x02);
    
    // 5. 发送3字节地址
    spi_send_byte((addr >> 16) & 0xFF);
    spi_send_byte((addr >> 8) & 0xFF);
    spi_send_byte(addr & 0xFF);
    
    // 6. 发送数据
    for (uint32_t i = 0; i < len; i++) {
        spi_send_byte(buf[i]);
    }
    
    // 7. 结束写入
    SPI_CS_HIGH();
    
    // 8. 等待写入完成
    spi_memory_wait_idle();
}

// 多页写入(自动处理跨页情况)
void spi_memory_write(uint32_t addr, const uint8_t *buf, uint32_t len) {
    uint32_t page_remain;  // 当前页剩余空间
    
    while (len > 0) {
        // 计算当前页剩余字节数
        page_remain = 256 - (addr % 256);
        if (page_remain > len) {
            page_remain = len;
        }
        
        // 写入一页数据
        spi_memory_page_write(addr, buf, page_remain);
        
        // 更新地址、缓冲区和剩余长度
        addr += page_remain;
        buf += page_remain;
        len -= page_remain;
    }
}

3. 扇区擦除操作(NOR FLASH 特有)

NOR FLASH 在写入前需要先擦除(擦除后所有位为 1):

// 擦除指定扇区(NOR FLASH)
// 不同型号扇区大小不同,AT25DF081为4KB/扇区
void spi_memory_sector_erase(uint32_t sector_addr) {
    // 1. 发送写使能命令
    spi_memory_write_enable();
    
    // 2. 发送扇区擦除命令(0xD8)
    SPI_CS_LOW();
    spi_send_byte(0xD8);
    
    // 3. 发送扇区地址
    spi_send_byte((sector_addr >> 16) & 0xFF);
    spi_send_byte((sector_addr >> 8) & 0xFF);
    spi_send_byte(sector_addr & 0xFF);
    
    // 4. 结束命令
    SPI_CS_HIGH();
    
    // 5. 等待擦除完成(扇区擦除可能需要几百毫秒)
    spi_memory_wait_idle();
}

// 芯片全擦除
void spi_memory_chip_erase(void) {
    spi_memory_write_enable();
    
    SPI_CS_LOW();
    spi_send_byte(0xC7);  // 全擦除命令
    SPI_CS_HIGH();
    
    spi_memory_wait_idle();  // 全擦除可能需要几秒
}

四、性能优化与可靠性设计

1. 速率优化

SPI 通信速率可根据存储器支持范围调整,优化传输效率:

AT25 系列 EEPROM 通常支持最高 10MHz
现代 NOR FLASH 可支持 50MHz 甚至 100MHz
固件加载等场景使用最高速率,配置参数读写可使用低速率

2. 数据完整性保障

校验机制:重要数据附加 CRC 或校验和,确保读写正确性
重试机制:通信失败时重试(最多 3 次),提高可靠性
写保护:通过硬件引脚或软件指令设置写保护,防止误操作

3. 电源管理

空闲时可通过指令使存储器进入低功耗模式(如 AT25 的 Deep Power-Down 模式)
上电后等待稳定再进行通信(通常延迟 10ms)
掉电检测:检测到电源异常时立即停止写入操作

五、典型应用场景

1. 固件存储与升级

NOR FLASH 常用于存储嵌入式系统固件:

系统启动时,MCU 通过 SPI 从 NOR FLASH 读取固件并加载到 RAM
支持固件在线升级(通过 SPI 写入新固件镜像)
通常采用双分区设计(运行区 + 升级区),保证升级安全性

2. 配置参数存储

EEPROM 适合存储频繁读写的配置参数:

设备 ID、网络配置、用户偏好等
相比 FLASH,EEPROM 无需擦除即可写入,且擦写次数更高(可达 100 万次)
采用磨损均衡算法,延长使用寿命

3. 数据日志记录

在低速率场景下,SPI 存储器可用于记录系统日志:

温度、湿度等环境数据
设备运行状态和错误信息
采用循环存储方式,满了自动覆盖最早数据

六、与其他接口的对比

在这里插入图片描述
SPI 在平衡速率、布线复杂度和可靠性方面表现优异,成为现代嵌入式系统中存储器接口的主流选择。

总结

SPI 接口为微控制器与非易失性存储器(EEPROM、NOR FLASH)提供了高效可靠的数据交互方式,其核心优势在于:

高速率传输,适合固件加载等大数据量场景
简单的硬件实现,仅需 4 根线即可完成通信
灵活的多设备扩展能力,通过片选线区分不同设备
全双工通信,支持同时收发数据

在实际应用中,需根据存储器特性(页大小、擦除粒度、速率支持)设计相应的读写算法,并关注数据完整性和可靠性保障,以构建稳定的嵌入式存储系统。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐