1. 项目背景与核心目标
最近拿到一块STM32L562E-DK开发板,这块板子搭载了STM32L5系列超低功耗MCU,特别吸引我的是它支持Octo-SPI(OSPI)接口。作为硬件开发者,我们拿到新板子后第一件事就是要验证基础外设功能是否正常。这次我决定重点测试板载的Flash存储芯片,因为在实际项目中,外部存储器经常用于存储固件、配置文件或日志数据。
OSPI是ST近年来主推的高速串行存储接口,相比传统QSPI,它支持八线数据传输模式,理论带宽翻倍。STM32L562E-DK板载的MX25LM51245G Flash芯片正好支持OSPI模式,最大时钟频率可达133MHz。通过这个测试,我们不仅能验证硬件连接是否正常,还能熟悉CubeMX配置OSPI外设的完整流程。
2. 硬件环境解析
2.1 开发板关键组件
STM32L562E-DK开发板的核心配置如下:
- 主控芯片:STM32L562RET6U(Cortex-M33内核,110MHz主频)
- 板载Flash:MX25LM51245G(512Mbit容量,支持OSPI/DTR模式)
- 调试接口:ST-LINK/V2-1(支持虚拟串口功能)
特别需要注意的是板子的OSPI接口布线情况。查看原理图发现,OSPI接口使用了如下引脚连接:
code复制OSPI_CLK -> PE10
OSPI_NCS -> PE11
OSPI_IO0 -> PE12
OSPI_IO1 -> PE13
OSPI_IO2 -> PE14
OSPI_IO3 -> PE15
OSPI_IO4 -> PD6
OSPI_IO5 -> PD7
OSPI_IO6 -> PE2
OSPI_IO7 -> PE7
2.2 Flash芯片特性
MX25LM51245G是Macronix推出的Octal SPI Flash,关键参数如下:
- 容量:512Mbit(64MB)
- 工作电压:1.8V/3.3V双电压支持
- 接口模式:
- 传统SPI(1/2/4线)
- OSPI(8线)
- DTR(双倍数据速率)
- 最大时钟频率:
- STR模式:133MHz
- DTR模式:66MHz x2
注意:开发板上Flash工作在3.3V电压下,与STM32L5的IO电平匹配。如果自行设计电路,需要确认电压兼容性。
3. 软件环境搭建
3.1 开发工具准备
进行OSPI测试需要以下软件环境:
- IDE:STM32CubeIDE 1.9.0(内置CubeMX配置工具)
- 固件库:STM32L5xx HAL库(v1.0.4)
- Flash驱动:Macronix提供的MX25LM51245G驱动代码
- 调试工具:STM32CubeProgrammer(用于验证Flash读写)
建议在开始前先更新ST-LINK固件,避免调试时出现连接问题。我在Windows 11环境下测试时,遇到过一次ST-LINK无法识别的问题,更新驱动后解决。
3.2 CubeMX基础配置
新建工程时选择STM32L562RET6U芯片,关键配置步骤如下:
-
时钟配置:
- 设置HSI作为PLL源
- 配置PLL使CPU运行在110MHz
- OSPI时钟分频设置为2,得到55MHz时钟(初始测试保守值)
-
OSPI外设配置:
c复制hospi1.Instance = OCTOSPI1; hospi1.Init.FifoThreshold = 4; hospi1.Init.DualQuad = HAL_OSPI_DUALQUAD_DISABLE; hospi1.Init.MemoryType = HAL_OSPI_MEMTYPE_MACRONIX; hospi1.Init.DeviceSize = 26; // 2^26 = 64MB hospi1.Init.ChipSelectHighTime = 2; hospi1.Init.FreeRunningClock = HAL_OSPI_FREERUNCLK_DISABLE; hospi1.Init.ClockMode = HAL_OSPI_CLOCK_MODE_0; hospi1.Init.WrapSize = HAL_OSPI_WRAP_NOT_SUPPORTED; hospi1.Init.ClockPrescaler = 1; // 后续可调整 -
GPIO设置:
所有OSPI相关引脚自动配置为高速模式(High speed),特别注意PE2和PE7需要手动映射到OSPI功能。
实测发现:如果不启用ICache,OSPI读取速度会明显下降。建议在main.c中早期位置添加
SCB_EnableICache()。
4. Flash驱动实现
4.1 初始化序列
MX25LM51245G上电后默认处于SPI模式,需要发送命令切换到OSPI模式。完整初始化流程如下:
c复制uint8_t ospi_init(void)
{
OSPI_RegularCmdTypeDef sCommand = {0};
uint8_t reg;
// 1. 进入Memory Mapped模式前的配置
sCommand.OperationType = HAL_OSPI_OPTYPE_COMMON_CFG;
sCommand.FlashId = HAL_OSPI_FLASH_ID_1;
sCommand.InstructionMode = HAL_OSPI_INSTRUCTION_1_LINE;
sCommand.InstructionSize = HAL_OSPI_INSTRUCTION_8_BITS;
sCommand.InstructionDtrMode = HAL_OSPI_INSTRUCTION_DTR_DISABLE;
sCommand.AddressSize = HAL_OSPI_ADDRESS_32_BITS;
sCommand.AddressDtrMode = HAL_OSPI_ADDRESS_DTR_DISABLE;
sCommand.AlternateBytesMode = HAL_OSPI_ALTERNATE_BYTES_NONE;
sCommand.DataDtrMode = HAL_OSPI_DATA_DTR_DISABLE;
sCommand.DQSMode = HAL_OSPI_DQS_DISABLE;
sCommand.SIOOMode = HAL_OSPI_SIOO_INST_EVERY_CMD;
// 2. 读取Flash ID验证连接
sCommand.Instruction = 0x9F; // RDID命令
sCommand.DataMode = HAL_OSPI_DATA_1_LINE;
sCommand.NbData = 3;
if (HAL_OSPI_Command(&hospi1, &sCommand, HAL_OSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 1;
uint8_t id[3];
if (HAL_OSPI_Receive(&hospi1, id, HAL_OSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 2;
// 3. 启用OSPI模式
sCommand.Instruction = 0x35; // RDSR命令
sCommand.NbData = 1;
if (HAL_OSPI_Command(&hospi1, &sCommand, HAL_OSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 3;
if (HAL_OSPI_Receive(&hospi1, ®, HAL_OSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 4;
if (!(reg & 0x02)) { // 检查OSPI使能位
sCommand.Instruction = 0x06; // WREN命令
sCommand.NbData = 0;
if (HAL_OSPI_Command(&hospi1, &sCommand, HAL_OSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 5;
sCommand.Instruction = 0x71; // WRSR命令
sCommand.DataMode = HAL_OSPI_DATA_1_LINE;
sCommand.NbData = 1;
reg |= 0x02; // 设置OSPI使能位
if (HAL_OSPI_Command(&hospi1, &sCommand, HAL_OSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 6;
if (HAL_OSPI_Transmit(&hospi1, ®, HAL_OSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 7;
}
// 4. 配置Memory Mapped模式
sCommand.OperationType = HAL_OSPI_OPTYPE_READ_CFG;
sCommand.Instruction = 0xEC; // Fast Read Octal命令
sCommand.InstructionMode = HAL_OSPI_INSTRUCTION_8_LINES;
sCommand.AddressMode = HAL_OSPI_ADDRESS_8_LINES;
sCommand.DataMode = HAL_OSPI_DATA_8_LINES;
sCommand.DummyCycles = 8;
if (HAL_OSPI_Command(&hospi1, &sCommand, HAL_OSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 8;
if (HAL_OSPI_MemoryMapped(&hospi1, HAL_OSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 9;
return 0;
}
4.2 读写测试实现
完成初始化后,我们可以进行实际的读写测试。建议先测试小数据块,再逐步扩大测试范围:
c复制#define TEST_ADDR 0x00000000
#define TEST_SIZE 256
uint8_t write_buf[TEST_SIZE];
uint8_t read_buf[TEST_SIZE];
void fill_pattern(uint8_t *buf, uint32_t len) {
for(uint32_t i=0; i<len; i++) {
buf[i] = i % 256;
}
}
uint8_t ospi_test(void) {
OSPI_RegularCmdTypeDef sCommand = {0};
// 1. 准备测试数据
fill_pattern(write_buf, TEST_SIZE);
// 2. 擦除目标扇区(4KB)
sCommand.OperationType = HAL_OSPI_OPTYPE_COMMON_CFG;
sCommand.Instruction = 0x20; // Sector Erase命令
sCommand.AddressMode = HAL_OSPI_ADDRESS_1_LINE;
sCommand.Address = TEST_ADDR;
sCommand.DataMode = HAL_OSPI_DATA_NONE;
sCommand.DummyCycles = 0;
if (HAL_OSPI_Command(&hospi1, &sCommand, HAL_OSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 1;
// 等待擦除完成
if(ospi_wait_ready() != 0)
return 2;
// 3. 写入测试数据
sCommand.Instruction = 0x12; // Page Program命令
sCommand.DataMode = HAL_OSPI_DATA_1_LINE;
sCommand.NbData = TEST_SIZE;
if (HAL_OSPI_Command(&hospi1, &sCommand, HAL_OSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 3;
if (HAL_OSPI_Transmit(&hospi1, write_buf, HAL_OSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 4;
// 4. 读取验证
sCommand.Instruction = 0xEC; // Fast Read Octal命令
sCommand.InstructionMode = HAL_OSPI_INSTRUCTION_8_LINES;
sCommand.AddressMode = HAL_OSPI_ADDRESS_8_LINES;
sCommand.DataMode = HAL_OSPI_DATA_8_LINES;
sCommand.DummyCycles = 8;
if (HAL_OSPI_Command(&hospi1, &sCommand, HAL_OSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 5;
if (HAL_OSPI_Receive(&hospi1, read_buf, HAL_OSPI_TIMEOUT_DEFAULT_VALUE) != HAL_OK)
return 6;
// 5. 数据比对
for(uint32_t i=0; i<TEST_SIZE; i++) {
if(read_buf[i] != write_buf[i]) {
return 7;
}
}
return 0;
}
5. 性能优化技巧
5.1 时钟配置优化
默认配置使用55MHz时钟,实际可以逐步提高频率测试稳定性:
- 修改ClockPrescaler为1,得到110MHz时钟
- 启用DTR模式(Double Transfer Rate)
- 调整DummyCycles值(通常6-10个周期)
实测不同配置下的读取速度对比:
| 模式 | 时钟频率 | 实际带宽 | 稳定性 |
|---|---|---|---|
| OSPI STR | 55MHz | 55MB/s | 优秀 |
| OSPI STR | 110MHz | 110MB/s | 良好 |
| OSPI DTR | 66MHz | 132MB/s | 中等 |
| OSPI DTR | 80MHz | 160MB/s | 不稳定 |
经验分享:在STM32L5上,110MHz STR模式是最佳平衡点。DTR模式虽然理论带宽高,但受限于PCB布线质量,容易出现数据错误。
5.2 内存映射模式优化
对于需要频繁访问的只读数据,可以配置内存映射模式,将Flash地址映射到MCU地址空间:
c复制void setup_memory_map(void) {
OSPI_RegularCmdTypeDef sCommand = {0};
sCommand.OperationType = HAL_OSPI_OPTYPE_READ_CFG;
sCommand.Instruction = 0xEC; // Fast Read Octal命令
sCommand.InstructionMode = HAL_OSPI_INSTRUCTION_8_LINES;
sCommand.AddressMode = HAL_OSPI_ADDRESS_8_LINES;
sCommand.AddressSize = HAL_OSPI_ADDRESS_32_BITS;
sCommand.DataMode = HAL_OSPI_DATA_8_LINES;
sCommand.DummyCycles = 8;
sCommand.DQSMode = HAL_OSPI_DQS_ENABLE;
HAL_OSPI_Command(&hospi1, &sCommand, HAL_OSPI_TIMEOUT_DEFAULT_VALUE);
HAL_OSPI_MemoryMapped(&hospi1, HAL_OSPI_TIMEOUT_DEFAULT_VALUE);
}
启用后,可以通过指针直接访问Flash内容:
c复制uint8_t *flash_ptr = (uint8_t *)0x90000000; // OSPI映射基地址
uint32_t data = *(uint32_t *)(flash_ptr + offset);
6. 常见问题排查
6.1 初始化失败
现象:HAL_OSPI_Init()返回HAL_ERROR
可能原因:
- 时钟配置错误(检查RCC配置)
- GPIO复用功能未正确映射(特别是PE2/PE7)
- 硬件连接问题(检查焊接和连接器)
解决方案:
- 使用CubeMX重新生成时钟配置
- 检查GPIO初始化代码,确保OSPI引脚正确配置
- 用万用表测量OSPI_CLK信号是否输出
6.2 数据校验错误
现象:读取的数据与写入不一致
可能原因:
- 时序配置不当(DummyCycles不足)
- 信号完整性问题(时钟频率过高)
- 未正确等待写操作完成
解决方案:
c复制uint8_t ospi_wait_ready(void) {
OSPI_RegularCmdTypeDef sCommand = {0};
uint8_t status;
do {
sCommand.OperationType = HAL_OSPI_OPTYPE_COMMON_CFG;
sCommand.Instruction = 0x05; // RDSR命令
sCommand.DataMode = HAL_OSPI_DATA_1_LINE;
sCommand.NbData = 1;
HAL_OSPI_Command(&hospi1, &sCommand, HAL_OSPI_TIMEOUT_DEFAULT_VALUE);
HAL_OSPI_Receive(&hospi1, &status, HAL_OSPI_TIMEOUT_DEFAULT_VALUE);
} while(status & 0x01); // 检查BUSY位
return 0;
}
6.3 性能低于预期
现象:实际传输速度远低于理论值
可能原因:
- ICache未启用
- 使用了软件轮询而非DMA
- 内存访问未对齐
优化建议:
- 在main()开头启用ICache:
SCB_EnableICache(); - 使用DMA传输:
c复制// 在OSPI初始化中添加
hospi1.hdma = &hdma_ospi1;
HAL_DMA_Init(hospi1.hdma);
// 异步传输示例
HAL_OSPI_Receive_DMA(&hospi1, buffer, length);
- 确保访问地址和长度为8字节对齐
7. 实际应用建议
经过完整测试后,这里分享几个实际项目中的应用技巧:
-
固件存储方案:
- 将应用程序分为Bootloader和App两部分
- Bootloader存放在内部Flash
- App存放在OSPI Flash,启动时加载到RAM执行
- 使用CRC校验确保固件完整性
-
数据存储优化:
c复制// 使用缓存减少擦写次数 #define CACHE_SIZE 512 uint8_t cache_buf[CACHE_SIZE]; uint32_t cache_addr = 0xFFFFFFFF; void flash_write_cached(uint32_t addr, uint8_t *data, uint32_t len) { if((addr & ~(CACHE_SIZE-1)) != cache_addr) { flash_flush_cache(); // 写入当前缓存 cache_addr = addr & ~(CACHE_SIZE-1); ospi_read(cache_addr, cache_buf, CACHE_SIZE); // 读取新块 } memcpy(&cache_buf[addr & (CACHE_SIZE-1)], data, len); } -
电源管理配合:
- 在低功耗模式下禁用OSPI时钟
- 唤醒后重新初始化OSPI接口
- 使用HOLD#引脚保持Flash状态
通过这次完整的OSPI Flash测试,验证了STM32L562E-DK开发板的存储子系统工作正常。实测在110MHz STR模式下,数据传输稳定可靠,完全可以满足大多数嵌入式应用的需求。对于需要更高性能的场景,可以尝试DTR模式,但要注意PCB布局和信号完整性的要求。