网站高端设计公司哪家好宁波seo排名外包公司
SPI高级特性分析
目录
- 1. SPI基础回顾
- 1.1 SPI通信基本原理
- 1.2 SPI时序与模式
- 1.3 SPI主从交互机制
- 2. 高速SPI变种
- 2.1 DSPI(双线SPI)
- 2.2 QSPI(四线SPI)
- 2.3 性能对比
- 3. QSPI内存映射模式
- 3.1 工作原理
- 3.2 实现机制
- 3.3 性能优化技巧
- 4. SPI闪存下载算法
- 4.1 标准SPI下载算法
- 4.2 XIP(Execute In Place)
- 4.3 基于标准SPI的代码执行
- 4.4 校验与安全机制
- 5. 高级应用案例
- 5.1 嵌入式系统启动加速
- 5.2 资源受限设备的内存扩展
- 6. 总结与展望
1. SPI基础回顾
SPI(Serial Peripheral Interface)是一种同步串行通信接口,由摩托罗拉公司开发,广泛应用于嵌入式系统中的短距离通信。标准SPI接口包含四条信号线:
- SCLK(Serial Clock):时钟信号,由主设备提供
- MOSI(Master Out Slave In):主设备数据输出,从设备数据输入
- MISO(Master In Slave Out):主设备数据输入,从设备数据输出
- CS/SS(Chip Select/Slave Select):片选信号,用于选择特定从设备
标准SPI的局限性在于其传输效率,每个时钟周期仅能传输1位数据。随着处理器速度提升和存储需求增长,标准SPI已难以满足高速数据传输需求,促使了DSPI和QSPI等高速变种的出现。
1.1 SPI通信基本原理
SPI是一种全双工通信协议,采用主从架构,具有如下特性:
- 同步通信:数据传输与时钟信号同步,确保数据稳定性
- 主设备控制:主设备生成时钟信号并控制CS线,从设备不能主动发起通信
- 全双工传输:MOSI和MISO两条数据线允许同时发送和接收数据
- 多从设备支持:通过多条CS线可连接多个从设备
基本通信流程:
- 主设备拉低特定从设备的CS线,选择通信对象
- 主设备通过SCLK提供时钟信号
- 数据通过MOSI线从主设备传输到从设备
- 同时,数据通过MISO线从从设备传输到主设备
- 通信结束后,主设备拉高CS线,释放从设备
典型的SPI传输时序:
CS |‾‾‾‾‾‾|________________________________|‾‾‾‾‾‾‾‾‾‾‾‾‾‾
SCLK |‾‾‾‾‾‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|‾‾‾‾‾‾‾‾‾‾‾
MOSI |‾‾‾‾‾‾|X|D7|D6|D5|D4|D3|D2|D1|D0|X|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
MISO |‾‾‾‾‾‾|X|R7|R6|R5|R4|R3|R2|R1|R0|X|‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
1.2 SPI时序与模式
SPI协议定义了四种工作模式,由两个参数决定:
- CPOL(时钟极性):空闲时时钟电平的状态(0=低电平,1=高电平)
- CPHA(时钟相位):数据在时钟的哪个边沿采样(0=第一边沿,1=第二边沿)
这形成了四种标准模式:
模式 | CPOL | CPHA | 空闲时钟 | 数据采样 |
---|---|---|---|---|
0 | 0 | 0 | 低电平 | 上升沿 |
1 | 0 | 1 | 低电平 | 下降沿 |
2 | 1 | 0 | 高电平 | 下降沿 |
3 | 1 | 1 | 高电平 | 上升沿 |
模式选择需要与从设备要求匹配,不同设备可能要求不同模式,例如:
- 大多数SPI闪存使用模式0或模式3
- 某些传感器可能使用模式1或模式2
时序图示例(模式0, CPOL=0, CPHA=0):
数据采样点 数据采样点 数据采样点↓ ↓ ↓
SCLK ___|‾‾‾|___|‾‾‾|___|‾‾‾|___|‾‾‾|___
MOSI ___X---X---X---X---X---X---X---X___D7 D6 D5 D4
时序图示例(模式3, CPOL=1, CPHA=1):
数据采样点 数据采样点 数据采样点↓ ↓ ↓
SCLK ‾‾‾|___|‾‾‾|___|‾‾‾|___|‾‾‾|___|‾‾‾
MOSI ___X---X---X---X---X---X---X---X___D7 D6 D5 D4
1.3 SPI主从交互机制
SPI主从设备之间的通信通常遵循特定的命令-响应模式,特别是在与存储器、传感器等设备通信时:
命令格式:
- 指令字节:确定操作类型(读、写、擦除等)
- 地址字节:指定操作目标位置(对于存储设备)
- 数据字节:需要写入的数据或读取的结果
主设备发起通信示例:
void spi_transfer(uint8_t *tx_data, uint8_t *rx_data, uint16_t len) {// 激活从设备GPIO_ResetBits(SPI_CS_PORT, SPI_CS_PIN);// 数据交换for(uint16_t i = 0; i < len; i++) {// 发送一个字节SPI_SendData(SPI_PORT, tx_data[i]);// 等待传输完成while(SPI_GetFlagStatus(SPI_PORT, SPI_FLAG_TXE) == RESET);while(SPI_GetFlagStatus(SPI_PORT, SPI_FLAG_RXNE) == RESET);// 接收一个字节rx_data[i] = SPI_ReceiveData(SPI_PORT);}// 释放从设备GPIO_SetBits(SPI_CS_PORT, SPI_CS_PIN);
}
从设备响应机制:
- 主动型从设备(如MCU):在每个时钟周期准备好要发送的数据
- 被动型从设备(如外设芯片):内部状态机处理接收的命令并准备响应
多从设备寻址:
// 选择从设备1
void select_slave1(void) {GPIO_SetBits(SPI_CS_PORT, SPI_CS_PIN2); // 确保其他设备不活跃GPIO_SetBits(SPI_CS_PORT, SPI_CS_PIN3);GPIO_ResetBits(SPI_CS_PORT, SPI_CS_PIN1); // 激活设备1
}// 选择从设备2
void select_slave2(void) {GPIO_SetBits(SPI_CS_PORT, SPI_CS_PIN1); // 确保其他设备不活跃GPIO_SetBits(SPI_CS_PORT, SPI_CS_PIN3);GPIO_ResetBits(SPI_CS_PORT, SPI_CS_PIN2); // 激活设备2
}
主设备通信效率优化:
- 使用DMA减少CPU干预
- 利用硬件CS控制减少软件开销
- 优化时钟频率设置,平衡速度和可靠性
2. 高速SPI变种
2.1 DSPI(双线SPI)
DSPI通过双倍数据线提高了传输效率,每个时钟周期传输2位数据。
技术特点:
- 保留标准SPI的SCLK和CS线
- 将MOSI和MISO改为双向数据线IO0和IO1
- 工作模式包括:
- 标准SPI模式:向后兼容
- 双线读取模式:指令以单线方式发送,数据以双线方式读取
- 双线写入模式:指令和数据均以双线方式传输
时序示例:
[标准SPI模式]
CS |‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|
SCLK |_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|
IO0 |CMD |ADDR |DATA0|DATA1|DATA2|DATA3|DATA4|DATA5|
IO1 |-----|-----|-----|-----|-----|-----|-----|-----|[双线读取模式]
CS |‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|
SCLK |_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|
IO0 |CMD |ADDR |D0(0)|D1(0)|D2(0)|D3(0)|D4(0)|D5(0)|
IO1 |-----|-----|D0(1)|D1(1)|D2(1)|D3(1)|D4(1)|D5(1)|
其中D0(0)表示Data0的第0位,D0(1)表示Data0的第1位,实现了同一时钟下传输两位数据。
2.2 QSPI(四线SPI)
QSPI将数据线扩展到四条,在每个时钟周期可以传输4位数据,理论上达到标准SPI四倍的吞吐量。
技术特点:
- 保留标准SPI的SCLK和CS线
- 使用四条双向数据线:IO0、IO1、IO2和IO3
- 工作模式包括:
- 标准模式:兼容标准SPI
- 双线模式:兼容DSPI
- 四线模式:全速数据传输,每个时钟周期传输4位数据
时序示例:
[四线模式]
CS |‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾|
SCLK |_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|_|‾|
IO0 |CMD |ADDR |D0(0)|D1(0)|D2(0)|D3(0)|D4(0)|D5(0)|
IO1 |-----|-----|D0(1)|D1(1)|D2(1)|D3(1)|D4(1)|D5(1)|
IO2 |-----|-----|D0(2)|D1(2)|D2(2)|D3(2)|D4(2)|D5(2)|
IO3 |-----|-----|D0(3)|D1(3)|D2(3)|D3(3)|D4(3)|D5(3)|
QSPI指令格式:
典型的QSPI闪存指令包含四个阶段:
- 指令阶段:发送操作码(通常是8位)
- 地址阶段:发送地址(24位或32位)
- 等待阶段:可选,某些操作需要额外延时
- 数据阶段:读取或写入数据
2.3 性能对比
参数 | 标准SPI | DSPI | QSPI |
---|---|---|---|
数据线数量 | 2 (MOSI+MISO) | 2 (IO0+IO1) | 4 (IO0~IO3) |
每周期传输位数 | 1 | 2 | 4 |
相对带宽 | 1x | 2x | 4x |
最高时钟频率* | 50MHz | 50MHz | 50MHz |
理论最大吞吐量* | 50Mbps | 100Mbps | 200Mbps |
复杂度 | 低 | 中 | 高 |
*注:实际最高时钟频率取决于控制器和外设能力,上述数值仅作参考比较
3. QSPI内存映射模式
3.1 工作原理
QSPI内存映射是QSPI最强大的特性之一,允许处理器直接从SPI闪存中执行代码或读取数据,无需先将内容加载到RAM中。这种模式通常称为XIP(Execute In Place,原地执行)。
基本原理:
- QSPI控制器将闪存的内容映射到处理器的地址空间
- 处理器使用普通内存访问指令(如LDR、STR)访问闪存内容
- QSPI控制器自动将内存访问转换为SPI闪存命令
映射过程:
- 处理器发出内存读取请求(例如读取地址0x90000000)
- QSPI控制器截获该请求,识别为映射区域内的访问
- 控制器计算闪存中的实际物理地址
- 控制器向闪存发送读取命令(通常是快速读取命令0xEB)
- 闪存返回数据,控制器将数据传送回处理器
3.2 实现机制
硬件支持:
QSPI内存映射模式需要硬件级别的支持,包括:
-
地址转换单元:
- 负责将CPU地址空间映射到闪存物理地址
- 通常支持内存窗口设置,允许将闪存的不同部分映射到不同的地址空间
-
指令缓存控制:
- 当CPU执行从闪存映射区域获取的指令时,控制器需要确保指令正确获取
- 许多实现包含预取缓冲区,减少延迟
-
读取指令配置:
- 可配置QSPI控制器使用的读取指令(通常为0xEB - 四线快速读取)
- 配置指令格式、地址宽度和等待周期
内存映射配置示例(STM32H7系列):
// 配置QSPI内存映射模式
QSPI_MemoryMappedTypeDef memConfig;
memConfig.TimeOutActivation = QSPI_TIMEOUT_COUNTER_DISABLE;
memConfig.TimeOutPeriod = 0;// 配置QSPI命令
QSPI_CommandTypeDef cmdConfig;
cmdConfig.InstructionMode = QSPI_INSTRUCTION_1_LINE;
cmdConfig.Instruction = 0xEB; // 四线快速读取命令
cmdConfig.AddressMode = QSPI_ADDRESS_4_LINES;
cmdConfig.AddressSize = QSPI_ADDRESS_24_BITS;
cmdConfig.AlternateByteMode = QSPI_ALTERNATE_BYTES_NONE;
cmdConfig.DataMode = QSPI_DATA_4_LINES;
cmdConfig.DummyCycles = 6; // 厂商推荐的等待周期
cmdConfig.DdrMode = QSPI_DDR_MODE_DISABLE;
cmdConfig.DdrHoldHalfCycle = QSPI_DDR_HHC_ANALOG_DELAY;
cmdConfig.SIOOMode = QSPI_SIOO_INST_EVERY_CMD;// 启用内存映射模式
HAL_QSPI_MemoryMapped(&hqspi, &cmdConfig, &memConfig);
3.3 性能优化技巧
1. 缓存优化:
- 配置处理器缓存以最佳匹配QSPI访问模式
- 考虑使用预取缓冲区减少延迟
- 对于频繁访问的数据,考虑预加载到RAM
2. 读取命令选择:
- 选择最适合应用的读取命令(权衡速度和兼容性)
- 常用命令对比:
命令码 | 描述 | 速度 | 兼容性 |
---|---|---|---|
0x03 | 标准读取 | 低 | 高 |
0x3B | 双输出快速读取 | 中 | 中高 |
0xEB | 四输出快速读取 | 高 | 中 |
0xED | 四输出DDR快速读取 | 最高 | 低 |
3. 等待周期(Dummy Cycles)优化:
- 等待周期是读取命令和数据传输之间的延迟时钟周期
- 周期过多会导致不必要的延迟,过少会导致数据不可靠
- 根据闪存芯片规格和时钟频率优化等待周期
4. 时钟频率调整:
- 找到QSPI控制器和闪存芯片都支持的最高稳定频率
- 考虑系统电源和温度对稳定性的影响
5. DDR(双数据率)模式:
在支持的系统上,启用DDR模式可进一步提高吞吐量:
cmdConfig.DdrMode = QSPI_DDR_MODE_ENABLE;
cmdConfig.DummyCycles = 8; // DDR模式通常需要更多等待周期
4. SPI闪存下载算法
4.1 标准SPI下载算法
标准SPI下载算法是指通过基本SPI接口将代码或数据写入SPI闪存的过程。与QSPI下载相比,标准SPI下载速度较慢,但兼容性更好,适用于更广泛的设备。
标准SPI闪存读写特性:
- 单线数据传输(每周期1位)
- 简单的命令结构
- 广泛的设备兼容性
- 较低的硬件要求
典型的SPI闪存读写指令:
指令 | 操作码 | 描述 |
---|---|---|
读数据 | 0x03 | 从指定地址读取数据 |
页编程 | 0x02 | 向指定地址写入数据(最多一页) |
扇区擦除 | 0x20 | 擦除指定的4KB扇区 |
块擦除 | 0x52/0xD8 | 擦除32KB/64KB数据块 |
芯片擦除 | 0xC7/0x60 | 擦除整个芯片 |
读状态寄存器 | 0x05 | 读取闪存状态寄存器 |
写状态寄存器 | 0x01 | 写入闪存状态寄存器 |
写使能 | 0x06 | 启用写操作 |
写禁止 | 0x04 | 禁用写操作 |
标准SPI下载算法实现:
- 初始化SPI接口:
void spi_flash_init(void) {// 配置SPI控制器SPI_InitTypeDef SPI_Config;SPI_Config.SPI_Direction = SPI_Direction_2Lines_FullDuplex;SPI_Config.SPI_Mode = SPI_Mode_Master;SPI_Config.SPI_DataSize = SPI_DataSize_8b;SPI_Config.SPI_CPOL = SPI_CPOL_Low; // 模式0SPI_Config.SPI_CPHA = SPI_CPHA_1Edge;SPI_Config.SPI_NSS = SPI_NSS_Soft;SPI_Config.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; // 根据系统频率调整SPI_Config.SPI_FirstBit = SPI_FirstBit_MSB;SPI_Config.SPI_CRCPolynomial = 7;SPI_Init(SPI1, &SPI_Config);// 启用SPISPI_Cmd(SPI1, ENABLE);// 初始化CS引脚(高电平)GPIO_SetBits(FLASH_CS_PORT, FLASH_CS_PIN);
}
- 读取闪存ID和状态:
uint32_t spi_flash_read_id(void) {uint8_t tx_buffer[4] = {0x9F, 0, 0, 0}; // JEDEC ID命令uint8_t rx_buffer[4] = {0};// 传输数据GPIO_ResetBits(FLASH_CS_PORT, FLASH_CS_PIN); // 激活CSspi_transfer(tx_buffer, rx_buffer, 4);GPIO_SetBits(FLASH_CS_PORT, FLASH_CS_PIN); // 释放CSreturn (rx_buffer[1] << 16) | (rx_buffer[2] << 8) | rx_buffer[3];
}uint8_t spi_flash_read_status(void) {uint8_t tx_buffer[2] = {0x05, 0}; // 读状态寄存器命令uint8_t rx_buffer[2] = {0};GPIO_ResetBits(FLASH_CS_PORT, FLASH_CS_PIN);spi_transfer(tx_buffer, rx_buffer, 2);GPIO_SetBits(FLASH_CS_PORT, FLASH_CS_PIN);return rx_buffer[1];
}
- 写使能和等待操作完成:
void spi_flash_write_enable(void) {uint8_t tx_buffer[1] = {0x06}; // 写使能命令uint8_t rx_buffer[1] = {0};GPIO_ResetBits(FLASH_CS_PORT, FLASH_CS_PIN);spi_transfer(tx_buffer, rx_buffer, 1);GPIO_SetBits(FLASH_CS_PORT, FLASH_CS_PIN);
}void spi_flash_wait_busy(void) {// 等待WIP(Write In Progress)位清零while((spi_flash_read_status() & 0x01) == 0x01);
}
- 擦除操作:
void spi_flash_sector_erase(uint32_t address) {uint8_t tx_buffer[4] = {0x20, // 扇区擦除命令(address >> 16) & 0xFF, // 地址高字节(address >> 8) & 0xFF, // 地址中字节address & 0xFF // 地址低字节};uint8_t rx_buffer[4] = {0};spi_flash_write_enable(); // 必须先写使能GPIO_ResetBits(FLASH_CS_PORT, FLASH_CS_PIN);spi_transfer(tx_buffer, rx_buffer, 4);GPIO_SetBits(FLASH_CS_PORT, FLASH_CS_PIN);spi_flash_wait_busy(); // 等待擦除完成
}
- 页编程(写入数据):
void spi_flash_page_program(uint32_t address, uint8_t* data, uint16_t length) {// 检查长度不超过页大小(一般为256字节)if(length > 256) length = 256;// 准备命令和地址uint8_t header[4] = {0x02, // 页编程命令(address >> 16) & 0xFF, // 地址高字节(address >> 8) & 0xFF, // 地址中字节address & 0xFF // 地址低字节};spi_flash_write_enable(); // 必须先写使能// 发送命令和地址GPIO_ResetBits(FLASH_CS_PORT, FLASH_CS_PIN);for(int i = 0; i < 4; i++) {spi_transfer_byte(header[i]);}// 发送数据for(int i = 0; i < length; i++) {spi_transfer_byte(data[i]);}GPIO_SetBits(FLASH_CS_PORT, FLASH_CS_PIN);spi_flash_wait_busy(); // 等待编程完成
}
- 读取数据:
void spi_flash_read_data(uint32_t address, uint8_t* buffer, uint32_t length) {// 准备命令和地址uint8_t header[4] = {0x03, // 读数据命令(address >> 16) & 0xFF, // 地址高字节(address >> 8) & 0xFF, // 地址中字节address & 0xFF // 地址低字节};// 发送命令和地址GPIO_ResetBits(FLASH_CS_PORT, FLASH_CS_PIN);for(int i = 0; i < 4; i++) {spi_transfer_byte(header[i]);}// 读取数据for(int i = 0; i < length; i++) {buffer[i] = spi_transfer_byte(0xFF); // 发送虚拟字节以接收数据}GPIO_SetBits(FLASH_CS_PORT, FLASH_CS_PIN);
}
- 完整的下载流程:
bool spi_flash_download_firmware(uint32_t address, uint8_t* firmware, uint32_t size) {uint32_t current_addr = address;uint32_t remaining = size;uint32_t sector_addr;uint16_t chunk_size;// 按扇区边界对齐地址sector_addr = current_addr & ~0xFFF; // 4KB对齐// 擦除所需的所有扇区while(sector_addr < current_addr + size) {printf("擦除扇区: 0x%08X\r\n", sector_addr);spi_flash_sector_erase(sector_addr);sector_addr += 4096; // 下一个扇区}// 按页编程固件while(remaining > 0) {// 确定当前页要编程的字节数(不超过256)chunk_size = (remaining > 256) ? 256 : remaining;// 确保不跨页边界if((current_addr & 0xFF) + chunk_size > 256) {chunk_size = 256 - (current_addr & 0xFF);}printf("编程地址: 0x%08X, 大小: %d\r\n", current_addr, chunk_size);spi_flash_page_program(current_addr, firmware, chunk_size);// 验证写入的数据uint8_t verify_buffer[256];spi_flash_read_data(current_addr, verify_buffer, chunk_size);if(memcmp(firmware, verify_buffer, chunk_size) != 0) {printf("验证失败: 地址 0x%08X\r\n", current_addr);return false;}// 更新指针和计数器current_addr += chunk_size;firmware += chunk_size;remaining -= chunk_size;}printf("固件下载完成,总大小: %d 字节\r\n", size);return true;
}
标准SPI下载的优化技巧:
-
批量扇区擦除:
- 对于大块数据,使用块擦除(32KB/64KB)而非多次扇区擦除
- 对于整个芯片编程,考虑使用芯片擦除命令
-
DMA传输优化:
void spi_flash_read_data_dma(uint32_t address, uint8_t* buffer, uint32_t length) {// 发送命令和地址(与普通读类似)// ...// 配置DMA接收DMA_InitTypeDef DMA_InitStructure;DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR;DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)buffer;DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;DMA_InitStructure.DMA_BufferSize = length;// 其他DMA配置// ...// 启用SPI的DMA接收SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Rx, ENABLE);// 开始DMA传输DMA_Cmd(DMA1_Channel2, ENABLE);// 等待DMA传输完成while(!DMA_GetFlagStatus(DMA1_FLAG_TC2));// 清理和释放// ...
}
-
快速读取命令:
- 标准读取(0x03)在低速时钟下工作良好
- 对于更高速率,使用快速读取命令(0x0B),带有额外的等待周期
-
断电保护:
- 实现断电检测和恢复机制
- 保存下载进度信息,支持断点续传
-
多重缓冲:
- 实现双缓冲区机制,一个缓冲区传输数据,同时准备下一个缓冲区
- 减少CPU等待时间,提高吞吐量
与高级SPI模式的比较:
特性 | 标准SPI下载 | QSPI下载 |
---|---|---|
数据线数量 | 2 (MOSI+MISO) | 4 (IO0~IO3) |
传输速率 | 较低 | 高达标准SPI的4倍 |
实现复杂度 | 简单 | 较复杂 |
硬件要求 | 低 | 较高,需专用QSPI控制器 |
兼容性 | 几乎所有SPI闪存 | 仅支持QSPI的闪存 |
典型应用 | 引导加载、配置更新 | 代码执行、资源存储 |
4.2 XIP(Execute In Place)
XIP技术允许处理器直接从闪存执行代码,无需复制到RAM,这在资源受限的系统中尤为重要。
XIP实现关键点:
-
引导加载程序配置:
- 系统启动时,引导加载程序初始化QSPI控制器
- 配置内存映射参数,建立地址映射关系
-
代码编译考虑:
- 应用程序需要专门编译为XIP兼容
- 链接脚本需要指定正确的内存区域
- 例如STM32的XIP链接区域示例:
MEMORY {FLASH (rx) : ORIGIN = 0x90000000, LENGTH = 8M /* QSPI映射区域 */RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 128K }
-
性能注意事项:
- XIP执行速度通常低于RAM执行
- 对时间关键型代码,考虑将其复制到RAM中执行
- 闪存读取延迟会影响指令获取性能
4.3 基于标准SPI的代码执行
虽然QSPI因其高速特性和专用内存映射功能而常被用于XIP实现,但值得注意的是,普通MCU也可以通过标准SPI接口直接从外部Flash执行代码,这种技术在资源受限的系统中尤为重要。
4.3.1 标准SPI的XIP实现机制
普通MCU上实现基于标准SPI的XIP通常有以下几种方式:
-
基于硬件的实现:
- 某些MCU集成了专门的硬件支持,即使是标准SPI控制器也提供内存映射功能
- 例如STM32F4系列中的一些型号允许通过Memory-Mapped SPI访问外部Flash
- NXP的部分LPC系列MCU支持从Quad-SPI和标准SPI Flash执行代码
-
基于软件的实现:
- 使用代码抖动(Code Shadowing)技术,将SPI Flash上的代码分块加载到RAM执行
- 通过复杂的跳转表和中断向量重映射实现近似的XIP功能
- 页面交换(Page Swapping)技术,维护一个内部RAM缓存,动态加载代码块
4.3.2 标准SPI执行代码的实现步骤
以ARM Cortex-M系列MCU为例,实现基于标准SPI Flash的代码执行:
- 引导加载程序设计:
// 简化的引导加载程序示例
void bootloader_start(void) {// 初始化SPI接口SPI_Init();// 检测SPI Flashuint32_t flash_id = SPI_Flash_ReadID();if(is_valid_flash(flash_id)) {// 读取程序头信息,包含程序大小和入口点program_header_t header;SPI_Flash_Read(PROGRAM_HEADER_ADDR, (uint8_t*)&header, sizeof(header));// 将程序从SPI Flash加载到RAMSPI_Flash_Read(PROGRAM_START_ADDR, (uint8_t*)RAM_EXEC_ADDR, header.size);// 重置堆栈指针__set_MSP(*(uint32_t*)RAM_EXEC_ADDR);// 跳转到RAM中的程序入口点void (*program_entry)(void) = (void(*)(void))(RAM_EXEC_ADDR + 4);program_entry();} else {// Flash无效,进入恢复模式enter_recovery_mode();}
}
-
代码分区设计:
- 将程序划分为关键部分和非关键部分
- 关键代码(如中断处理程序、性能敏感代码)放在内部Flash或RAM
- 非关键代码(如初始化函数、低频调用函数)放在外部SPI Flash
-
动态代码加载:
// 简化的代码加载管理器
typedef struct {uint32_t flash_addr; // SPI Flash中的地址uint32_t size; // 代码段大小uint8_t is_loaded; // 是否已加载标志
} code_segment_t;// 代码段表
static code_segment_t code_segments[MAX_SEGMENTS];
static uint8_t code_cache[CODE_CACHE_SIZE];
static uint32_t current_segment = 0xFFFFFFFF;// 加载并执行代码段
void* load_and_execute(uint32_t segment_id, void* params) {if(segment_id >= MAX_SEGMENTS)return NULL;// 检查是否需要加载新段if(current_segment != segment_id) {// 从Flash加载代码段到缓存SPI_Flash_Read(code_segments[segment_id].flash_addr, code_cache, code_segments[segment_id].size);current_segment = segment_id;}// 执行加载的代码void* (*func)(void*) = (void*(*)(void*))code_cache;return func(params);
}
4.3.3 普通SPI执行代码的优化技术
即使没有专用的XIP硬件支持,也可以通过多种技术优化基于标准SPI的代码执行:
-
代码压缩:
- 压缩存储在SPI Flash中的代码,加载时解压缩
- 减少数据传输量,提高加载速度
// 简化的解压缩加载示例 void load_compressed_segment(uint32_t flash_addr, uint8_t* dest, uint32_t compressed_size, uint32_t original_size) {uint8_t compressed_buffer[MAX_COMPRESSED_SIZE];// 读取压缩数据SPI_Flash_Read(flash_addr, compressed_buffer, compressed_size);// 解压缩到目标内存decompress(compressed_buffer, compressed_size, dest, original_size); }
-
预取与缓存:
- 实现简单的代码缓存机制,使用LRU(最近最少使用)策略
- 预测性加载下一个可能执行的代码块
// 简化的代码缓存管理 typedef struct {uint32_t flash_addr; // 对应的Flash地址uint32_t access_count; // 访问计数,用于LRU策略uint8_t data[CACHE_BLOCK_SIZE]; } cache_block_t;// 通过缓存访问代码 uint8_t* get_code_from_flash(uint32_t addr) {// 查找缓存int cache_hit = find_in_cache(addr);if(cache_hit >= 0) {// 缓存命中,更新访问计数cache_blocks[cache_hit].access_count++;return cache_blocks[cache_hit].data;}// 缓存未命中,加载新块int lru_block = find_lru_block();SPI_Flash_Read(addr, cache_blocks[lru_block].data, CACHE_BLOCK_SIZE);cache_blocks[lru_block].flash_addr = addr;cache_blocks[lru_block].access_count = 1;return cache_blocks[lru_block].data; }
-
零拷贝执行:
- 对于只读代码,配置MCU的内存保护单元(MPU),允许直接从SPI数据缓冲区执行代码
- 减少RAM到RAM的复制开销
// 配置内存区域为可执行 void set_buffer_executable(uint32_t buffer_addr, uint32_t size) {// 禁用MPUMPU->CTRL &= ~MPU_CTRL_ENABLE_Msk;// 配置MPU区域MPU->RNR = MPU_REGION_NUMBER;MPU->RBAR = buffer_addr;MPU->RASR = MPU_RASR_ENABLE_Msk |(0x07 << MPU_RASR_SIZE_Pos) | // 大小为2^(7+1)=256字节(0x03 << MPU_RASR_AP_Pos); // 读写访问权限// 启用MPUMPU->CTRL |= MPU_CTRL_ENABLE_Msk;// 数据同步屏障,确保内存访问完成__DSB();// 指令同步屏障,确保指令获取使用新设置__ISB(); }
4.3.4 普通SPI与QSPI执行代码的比较
特性 | 标准SPI执行代码 | QSPI内存映射执行 |
---|---|---|
硬件要求 | 基本SPI控制器 | 专用QSPI控制器 |
执行速度 | 较慢,通常需要先加载到RAM | 较快,可直接从Flash执行 |
RAM占用 | 较高,需要代码缓存空间 | 较低,只需关键代码 |
实现复杂度 | 软件复杂度高 | 硬件支持,软件较简单 |
功耗 | 可能更高(频繁RAM访问) | 可能更低(减少RAM操作) |
适用场景 | 低成本MCU,代码量适中 | 高性能MCU,大型应用 |
4.3.5 实际应用案例
案例1:Arduino运行外部SPI Flash上的代码:
Arduino等资源受限的平台可以使用外部SPI Flash扩展程序空间。通过自定义引导加载程序,实现从外部SPI Flash读取代码并执行:
// Arduino上执行SPI Flash代码的简化实现
void setup() {// 初始化SPISPI.begin();pinMode(FLASH_CS_PIN, OUTPUT);digitalWrite(FLASH_CS_PIN, HIGH);// 加载外部函数load_external_function(FUNCTION_ID_1);
}void loop() {// 调用外部函数execute_external_function(FUNCTION_ID_1, NULL);delay(1000);
}// 加载外部函数到RAM缓冲区
void load_external_function(uint8_t function_id) {uint32_t addr = function_table[function_id].address;uint16_t size = function_table[function_id].size;digitalWrite(FLASH_CS_PIN, LOW);SPI.transfer(0x03); // 读取命令SPI.transfer((addr >> 16) & 0xFF);SPI.transfer((addr >> 8) & 0xFF);SPI.transfer(addr & 0xFF);for(uint16_t i = 0; i < size; i++) {function_buffer[i] = SPI.transfer(0);}digitalWrite(FLASH_CS_PIN, HIGH);
}// 执行已加载的函数
void* execute_external_function(uint8_t function_id, void* param) {// 将函数缓冲区转换为函数指针void* (*func)(void*) = (void*(*)(void*))function_buffer;return func(param);
}
案例2:嵌入式监控系统代码隔离:
在工业监控系统中,使用双区域架构实现安全隔离:
- 核心执行引擎位于内部Flash,负责系统关键功能
- 可升级的监控算法存储在外部SPI Flash,按需加载执行
- 进程隔离确保外部代码不会干扰系统核心功能
这种架构提供了几个关键优势:
- 系统核心保持稳定,即使外部算法存在问题
- 监控算法可远程更新,无需更改核心系统
- 通过代码签名和验证增强安全性
4.3.6 注意事项与局限性
基于标准SPI Flash执行代码时需注意以下几点:
-
性能限制:
- SPI时钟频率限制了代码加载速度
- 每次函数调用可能需要额外开销,特别是首次调用时
- 不适合对时间要求严格的实时应用
-
兼容性问题:
- 代码需要位置无关(PIC)或特殊编译,确保可在任意内存位置执行
- 某些处理器体系结构可能限制从数据内存执行代码的能力
- 需注意对齐要求,特别是ARM Thumb指令
-
调试难度:
- 动态加载的代码调试较为困难
- 错误追踪复杂,可能需要特殊的调试工具和技术
-
安全隐患:
- 动态加载的代码可能引入安全风险
- 应实施代码验证、签名和加密等保护措施
4.4 校验与安全机制
校验机制:
-
写入后验证:
- 在每个页编程后进行读回验证
- 发现不匹配时重新编程或报错
-
校验和验证:
- 计算固件的校验和(如CRC32、SHA256)
- 将校验和存储在特定位置,启动时验证
安全下载机制:
-
防篡改保护:
- 实现安全引导(Secure Boot)
- 验证固件签名确保真实性
-
加密固件:
- 存储加密的固件内容
- 只有授权设备能正确解密和执行
// 简化的固件解密示例 void decrypt_and_program(uint32_t address, const uint8_t *encrypted_data, uint32_t len, const uint8_t *key) {uint8_t decrypted[256]; // 一页大小的缓冲区// 分页处理for(uint32_t offset = 0; offset < len; offset += 256) {uint32_t chunk_size = min(256, len - offset);// 解密数据aes_decrypt(encrypted_data + offset, decrypted, chunk_size, key);// 编程当前页program_page(address + offset, decrypted, chunk_size);} }
-
读取保护:
- 配置闪存状态寄存器以启用保护
- 防止未授权读取闪存内容
void enable_read_protection(void) {// 读取当前状态寄存器uint8_t status = read_status_register();// 设置保护位status |= 0x80; // 通常位7或位5用于读取保护// 写回状态寄存器write_status_register(status); }
5. 高级应用案例
5.1 嵌入式系统启动加速
QSPI的高速特性和XIP功能可显著提升嵌入式系统的启动速度:
传统启动流程:
- 引导加载程序从SPI闪存读取固件
- 将固件复制到RAM
- 执行RAM中的代码
QSPI XIP优化启动:
- 引导加载程序配置QSPI内存映射
- 直接从映射区域执行固件
- 仅将关键代码复制到RAM执行
性能提升:
- 启动时间可减少50-80%
- 减少RAM占用,为应用程序释放更多空间
代码示例(STM32 QSPI启动配置):
void configure_qspi_boot(void) {// 初始化QSPI硬件init_qspi_hardware();// 配置闪存以支持XIPqspi_enable_xip_mode();// 重映射启动区域(如果需要)SCB->VTOR = QSPI_BASE_ADDRESS; // 设置向量表偏移// 可选:将关键代码复制到RAMmemcpy((void*)SRAM_CODE_REGION, (void*)QSPI_CRITICAL_CODE_REGION, CRITICAL_CODE_SIZE);// 跳转到应用程序入口点jump_to_application();
}
5.2 资源受限设备的内存扩展
对于RAM有限的微控制器,QSPI闪存可作为扩展存储,存储大型数据集:
应用场景:具有复杂UI的低成本设备
- 图形资源(图标、字体、背景)存储在QSPI闪存
- 通过内存映射直接访问
- 避免将整个资源复制到有限的RAM
实现技术:
- 资源文件系统:在QSPI闪存上实现简单的只读文件系统
- 直接渲染:GPU或图形库直接从QSPI读取像素数据
- 缓存管理:为频繁访问的资源建立RAM缓存
示例应用:
// 直接从QSPI闪存读取图像并显示
void display_image(uint32_t image_offset, uint16_t x, uint16_t y) {// 获取图像头信息image_header_t* header = (image_header_t*)(QSPI_BASE_ADDR + image_offset);uint16_t width = header->width;uint16_t height = header->height;// 计算像素数据的起始地址uint16_t* pixel_data = (uint16_t*)(QSPI_BASE_ADDR + image_offset + sizeof(image_header_t));// 直接将数据传送到显示控制器for(uint16_t row = 0; row < height; row++) {lcd_set_window(x, y + row, x + width - 1, y + row);lcd_write_pixels(pixel_data + row * width, width);}
}
6. 总结与展望
QSPI技术总结
QSPI通过四线传输和内存映射等高级特性,为资源受限的嵌入式系统提供了高速外部存储解决方案。其主要优势包括:
- 高性能:相比标准SPI提供最高4倍的传输速率
- 内存扩展:通过XIP技术扩展有限的片上存储
- 灵活性:支持多种操作模式,可根据需求调整性能和兼容性
- 广泛支持:众多微控制器集成了QSPI控制器,闪存芯片供应充足
未来发展趋势
- 更高速度:QSPI接口频率不断提高,部分控制器已支持133MHz以上
- 八线接口:OSPI(Octo-SPI)提供8条数据线,理论带宽翻倍
- 高级闪存技术:支持更快写入、更长寿命的新型闪存芯片
- 安全特性增强:集成加密引擎,支持安全启动和加密存储
- 统一内存架构:将QSPI内存更紧密地集成到处理器内存系统中
QSPI作为连接微控制器和大容量存储的桥梁,将继续在嵌入式系统中扮演重要角色,尤其是在需要平衡性能、成本和功耗的应用场景中。
参考资料
- ST Microelectronics, “STM32 QSPI接口和内存映射使用指南”
- NXP Semiconductors, “i.MX RT系列参考手册:QSPI控制器章节”
- Micron Technology, “串行NOR闪存设计指南”
- JEDEC标准, “串行闪存规范JESD216D”
- ARM, “内存系统优化技术白皮书”
附录:常见SPI闪存指令集
命令名称 | 操作码 | 格式 | 描述 |
---|---|---|---|
读取数据 | 0x03 | CMD+ADDR+DATA | 标准读取,无额外等待 |
快速读取 | 0x0B | CMD+ADDR+DUMMY+DATA | 高速读取,带额外等待周期 |
页编程 | 0x02 | CMD+ADDR+DATA | 写入单页数据(通常最大256字节) |
扇区擦除(4KB) | 0x20 | CMD+ADDR | 擦除4KB扇区 |
块擦除(32KB) | 0x52 | CMD+ADDR | 擦除32KB数据块 |
块擦除(64KB) | 0xD8 | CMD+ADDR | 擦除64KB数据块 |
芯片擦除 | 0xC7/0x60 | CMD | 擦除整个芯片 |
写使能 | 0x06 | CMD | 启用写操作 |
写禁止 | 0x04 | CMD | 禁用写操作 |
读状态寄存器1 | 0x05 | CMD+DATA | 读取状态寄存器1 |
写状态寄存器1 | 0x01 | CMD+DATA | 写入状态寄存器1 |
读状态寄存器2 | 0x35 | CMD+DATA | 读取状态寄存器2 |
写状态寄存器2 | 0x31 | CMD+DATA | 写入状态寄存器2 |
读JEDEC ID | 0x9F | CMD+DATA | 读取制造商ID和设备ID |
读唯一ID | 0x4B | CMD+DUMMY+DATA | 读取唯一序列号 |
掉电/深度睡眠 | 0xB9 | CMD | 进入低功耗模式 |
唤醒 | 0xAB | CMD | 从低功耗模式唤醒 |