W25Q32 SPI Flash 芯片读写速度测试 - 对比全片擦除和扇区擦除

打算做个用spi flash 存储的数据记录器,所以要先试一下这东西的最大吞吐量。用来做测试的硬件规格:

  • 4MB Flash:W25Q32JVSIG
  • 2MB Flash:W25Q16CVSIG
  • 单片机:STM32F103RCT6
  • 接口:标准SPI @18MHz

单片机主频72MHz,SPI 最高频率36MHz,不过后面主要是用18MHz 测试的,因为36MHz 没办法正常通信,不确定什么原因,而且频率足够高以后,瓶颈主要就在flash 芯片和软件处理上了。

全片擦除重写

先测试最简单的场景:全片擦除、全片重写,然后读出来校验。提前说结论:不考虑擦除时间的话,4MB 容量写满大概需要16 秒左右,对应256kB/s 的速度;算上擦除时间,擦除+写入,总共按21~25 秒计算,速度也有160kB/s ~ 195kB/s,感觉还可以,能赶上百度网盘。这是SPI 频率18MHz 的结果,SPI 频率继续拉高的话,读取速度能提高,但是写入和擦除的时间应该没什么优化空间。

1、纯Arduino 实现

刚开始是用纯Arduino 库做的,软件轮询硬件SPI 收发。Flash 芯片驱动库用的SparkFun SPI SerialFlash Arduino Library。写的很乱,但是纯测试代码就不讲究了。代码如下:

cpp 复制代码
#include <Arduino.h>
#include <SPI.h>
#include <SparkFun_SPI_SerialFlash.h>

// 引脚定义放在头文件里,省略
#include "app.hpp"

constexpr size_t FLASH_SIZE = 4 * 1024 * 1024;  // 4MB
constexpr size_t SECTOR_SIZE = 4 * 1024;        // 4KB,擦除单位
constexpr size_t PAGE_SIZE = 256;               // 256B,写入单位

constexpr size_t SPI_FREQ = 18'000'000;

SFE_SPI_FLASH flash;

void loop() {
    Serial.println("BEEP");
    delay(2000);
}


uint32_t program_data[PAGE_SIZE / 4];


void storage_test() {
    Serial.println("<< Full chip program >>");

    Serial.println("-> Erase");
    auto start_time = millis();
    flash.erase();
    auto elapsed_time = millis() - start_time;
    Serial.print("Erase done, time: ");
    Serial.print(elapsed_time);
    Serial.println("ms");

    // ============================================
    Serial.println("-> Write");
    for (size_t i = 0; i < PAGE_SIZE / 4; i++) {
        program_data[i] = 0x07080708;
    }

    start_time = millis();
    uint32_t min_page_time = 0xFFFFFFFF;
    uint32_t max_page_time = 0;
    uint64_t total_page_time = 0;
    uint64_t total_transfer_time = 0;
    for (size_t page_idx = 0; page_idx < FLASH_SIZE; page_idx += PAGE_SIZE) {
        auto page_start_time = micros();
        auto r = flash.writeBlock(page_idx, (uint8_t*)program_data, PAGE_SIZE);
        auto transfer_time = micros() - page_start_time;
        total_transfer_time += transfer_time;
        if (!flash.blockingBusyWait(50)) {
            Serial.print("Busy forever. Write failed at page: ");
            Serial.println(page_idx);
            return;
        }
        elapsed_time = micros() - page_start_time;
        if (r != SFE_FLASH_READ_WRITE_SUCCESS) {
            Serial.print("Write failed at page: ");
            Serial.println(page_idx);
            return;
        }
        total_page_time += elapsed_time;
        if (elapsed_time < min_page_time) {
            min_page_time = elapsed_time;
        }
        if (elapsed_time > max_page_time) {
            max_page_time = elapsed_time;
        }
    }
    Serial.print("Min page time: ");
    Serial.print(min_page_time);
    Serial.println("us");
    Serial.print("Max page time: ");
    Serial.print(max_page_time);
    Serial.println("us");
    Serial.print("Average page time: ");
    Serial.print(total_page_time / (FLASH_SIZE / PAGE_SIZE));
    Serial.println("us");
    Serial.print("Average transfer time: ");
    Serial.print(total_transfer_time / (FLASH_SIZE / PAGE_SIZE));
    Serial.println("us");

    Serial.print("Write done, time: ");
    Serial.print(millis() - start_time);
    Serial.println("ms");

    // ============================================
    Serial.println("-> Verify (Fast read full chip)");
    auto ss = SPISettings(SPI_FREQ, MSBFIRST, SPI_MODE0);
    SPI.beginTransaction(ss);
    digitalWrite(FLASH_CS_NUM, LOW);
    SPI.transfer(0x0b);
    SPI.transfer(0);
    SPI.transfer(0);
    SPI.transfer(0);
    SPI.transfer(0x55);  // dummy byte
    uint8_t last_byte = 0x7;
    start_time = millis();
    for (size_t i = 0; i < FLASH_SIZE; i++) {
        auto b = SPI.transfer(0x55);
        if (b + last_byte != 0xf) {
            Serial.print("Verify failed at byte: ");
            Serial.println(i);
					 goto END_TRANSACTION;
        }
        last_byte = b;
    }
    uint32_t verify_time = millis() - start_time;
    Serial.println("Verify done");
    Serial.print("Verify time, ms: ");
    Serial.print(verify_time);
    Serial.println("ms");
END_TRANSACTION:
    digitalWrite(FLASH_CS_NUM, HIGH);
    SPI.endTransaction();
}


extern "C" uint32_t spi_getClkFreqInst(SPI_TypeDef *spi_inst);


void setup() {
    SPI.setSCLK(SCK_NUM);
    SPI.setMOSI(MOSI_NUM);
    SPI.setMISO(MISO_NUM);
    SPI.begin();
    Serial.begin(230400);
    Serial.println("<< Flash test >>");

    Serial.print("SPI Max freq: ");
    Serial.print(spi_getClkFreqInst(SPI1) / 2 / 1000000);
    Serial.println(" MHz");
    Serial.print("SPI Freq: ");
    Serial.print(SPI_FREQ / 1000000);
    Serial.println(" MHz");

    // flash.enableDebugging();
    if (!flash.begin(FLASH_CS_NUM, SPI_FREQ)) {
        Serial.println("Failed to initialize flash");
        while (1);
    }

    Serial.println("Flash ok");
    auto mfgID = flash.getManufacturerID();
    if (mfgID != SFE_FLASH_MFG_UNKNOWN) {
        Serial.print(F("Manufacturer: "));
        Serial.println(flash.manufacturerIDString(mfgID));
    }
    else {
        uint8_t unknownID = flash.getRawManufacturerID();  // Read the raw manufacturer ID
        Serial.print(F("Unknown manufacturer ID: 0x"));
        if (unknownID < 0x10) Serial.print(F("0"));  // Pad the zero
        Serial.println(unknownID, HEX);
    }

    Serial.print(F("Device ID: 0x"));
    Serial.println(flash.getDeviceID(), HEX);

    auto start_time = millis();
    Serial.println("Full chip erase, write, verify");
    Serial.println("Confirm? (y/n)");

    while (millis() - start_time < 30000) {
        if (Serial.available() > 0) {
            char c = Serial.read();
            if (c == 'n' || c == 'N') {
                Serial.println("Test skipped");
                return;
            }
            if (c == 'y' || c == 'Y') {
                storage_test();
                return;
            }
        }
    }

    Serial.println("Timeout 30s, Skip test");
}

先连上串口,STM32F103 系列芯片默认的串口引脚是:PA2 - TX, PA3 - RX。程序运行后,先读取Flash 芯片的信息,识别芯片厂商,从串口输出。然后以防万一,要求输入y 确定接下来要擦除芯片。串口信息如下:

复制代码
<< Flash test >>
SPI Max freq: 36 MHz
SPI Freq: 18 MHz
Flash ok
Manufacturer: Winbond
Device ID: 0x4016
Full chip erase, write, verify
Confirm? (y/n)

串口发送y 以后开始执行测试。步骤:

  1. 全片擦除
  2. 用重复的0x80x7 把flash 写满
  3. 把4M 字节全读出来,校验相邻两个字节之和是否等于0xf
  4. 统计各步骤耗时,计算读写速度

测试执行结果类似下面这样:

复制代码
<< Full chip program >>
-> Erase
Erase done, time: 6472ms

-> Write
Min page time: 2999us
Max page time: 3149us
Average page time: 2999us
Write done, time: 49154ms

-> Verify (Fast read full chip)
Verify done
Verify time, ms: 37992ms

这是刚开始用2MHz 频率测到的结果,相当慢了。解释一下:

  • 全片擦除一共用了6.47 秒
  • 写入一页256 字节加上页编程时间是3 毫秒
  • 全部4MB 写满用了49 秒
  • 全部读出来花了38 秒

意思是不算擦除时间,每次连续写入256 字节后就要等3 毫秒,让flash 处理写入的数据。更多详细信息可以拿着数据问AI。之后就只用18MHz 频率测试。几次测试的结果如下表:

测试条件 擦除时间 (ms) 写页时间最小值 (µs) 写页时间最大值 (µs) 写页时间平均值 (µs) 写总时间 (ms) 读取时间 (ms) 备注
2MHz 6472 2999 3149 2999 49154 37992
18MHz 6490 1925 1999 1998 32770 11837
18MHz 2 4590 1925 1999 1998 32771 11837 另一片芯片,擦除时间明显缩短
18MHz 寄存器 6580 1925 1999 1998 32771 4549 用寄存器读取
18MHz DMA 6600 902 999 998 16387 4199 用DMA 写入
SPI 36MHz --- --- --- --- --- --- 通信失败,无有效数据
W25Q16 2MB 2310 1933 1998 1997 16387 5978 2MB 版本总时间减半

其中,

  • 18MHz 2 那行是用另一片W25Q32 测试的,两片是从立创买的一批,除了擦除时间不一样,别的操作耗时都差不多
  • 寄存器 那行是跳过SPI 库,直接操作寄存器读取SPI 数据,可以看到时间少了不少,后面说这个
  • DMA 那行改用DMA 往Flash 写数据,相比用SPI 库,写入时间缩短了一半,详细的看后面第3 节
  • 也试了W25Q16,容量减半,总时间都减半了,但是写入一页的时间基本没区别,后面就只测W25Q32

2、直接操作寄存器读取

观察一下之前代码读取SPI 数据的部分,这个for 循环每次调用SPI.transfer 读取1 字节数据,总共要循环四百万次,所以调用库函数的开销就不能忽略了,循环体内的代码要尽量精简,才能排除代码逻辑运行时间的干扰,让总时间接近实际的传输时间。

cpp 复制代码
    for (size_t i = 0; i < FLASH_SIZE; i++) {
        auto b = SPI.transfer(0x55);   // 四百万次库函数调用

        if (b + last_byte != 0xf) {
            Serial.print("Verify failed at byte: ");
            Serial.println(i);
            return;
        }
        last_byte = b;
    }

所以后面就把这块代码改成直接操作SPI 寄存器的形式,从上面的表里能看到,读取数据总耗时差不多缩短了三分之二。

cpp 复制代码
    for (size_t i = 0; i < FLASH_SIZE; i++) {
        // auto b = SPI.transfer(0x55);
        // 直接寄存器轮询传输,避免使用库函数的开销
        while (!(SPI1->SR & SPI_SR_TXE));
        SPI1->DR = 0xFF;
        // 等待接收非空(RXNE=1)
        while (!(SPI1->SR & SPI_SR_RXNE));
        auto b = SPI1->DR;

        if (b + last_byte != 0xf) {
            Serial.print("Verify failed at byte: ");
            Serial.println(i);
            return;
        }
        last_byte = b;
    }

根据目前的数据,可以看出:

  • 18MHz 频率下,读取速度能跑到接近1MB/s
  • 写一页256 字节总耗时大概2 毫秒,那理论上可以做到125kB/s 的写入速度
  • 就算把全片擦除时间4~6 秒加到总写入时间里,算下来平均写入速度也有100kB/s 左右
  • 不确定DMA 读取相比软件轮询能快多少,感觉读取速度优化空间不大了,但是写入还有明显的优化空间

3、用DMA 写入

之前代码里用来写入数据的flash.writeBlock 每次调用会写入256 字节,但是writeBlock 内部还是调用SPI.transfer 一字节一字节传输数据的,所以和读取一样,写入时也会重复调用库函数四百万次。在代码里加入计时,查看每次调用flash.writeBlock 传输256 字节的耗时,结果如下:

复制代码
Min page time: 1935us
Max page time: 1999us
Average page time: 1998us
          Average transfer time: 718us
Write done, time: 32771ms

可见,每次传输256 字节平均要花718 微秒,总共会占用十秒左右的时间,用DMA 优化估计会有比较大的提升。不过Flash 芯片在写入时有一些别的限制:

  • 每次写入前必须单独发一条"写使能"指令
  • 每次最多写一页256 字节

4MB 容量对应16384 个256 字节,就算用了DMA,也得循环写入这么多次,而且每次DMA 传输前都得单独发一字节使能指令,不能把这条指令和数据打包到一块发送。

要用DMA,需要:

  • 配置DMA
  • 配置SPI 为DMA 模式
  • 处理DMA 中断

按理说这个地方可以不用中断的,但是HAL 库提供的DMA 传输函数HAL_SPI_Transmit_DMA 就设计成了中断驱动非阻塞的模式,那只好把中断加上了。下面是DMA 配置和DMA 的ISR:

cpp 复制代码
void setup_spi_dma() {
    // 使能 DMA1 时钟
    __HAL_RCC_DMA1_CLK_ENABLE();

    // 配置 DMA 发送通道(通道3,从内存到 SPI1->DR)
    hdma_spi1_tx.Instance = DMA1_Channel3;
    hdma_spi1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_spi1_tx.Init.PeriphInc = DMA_PINC_DISABLE;  // 外设地址不变
    hdma_spi1_tx.Init.MemInc = DMA_MINC_ENABLE;      // 内存地址递增
    hdma_spi1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_spi1_tx.Init.Mode = DMA_NORMAL;  // 单次传输,不循环
    hdma_spi1_tx.Init.Priority = DMA_PRIORITY_HIGH;

    HAL_DMA_Init(&hdma_spi1_tx);
    NVIC_EnableIRQ(DMA1_Channel3_IRQn);  // 使能DMA 中断

    // 将 DMA 通道关联到 SPI1 的 TX
    __HAL_LINKDMA(SPI.getHandle(), hdmatx, hdma_spi1_tx);
}

void enable_spi1_dma_tx() {
    SET_BIT(SPI1->CR2, SPI_CR2_TXDMAEN);
}

void disable_spi1_dma_tx() {
    CLEAR_BIT(SPI1->CR2, SPI_CR2_TXDMAEN);
}

// DMA 终端服务函数
extern "C" void DMA1_Channel3_IRQHandler(void) {
    HAL_DMA_IRQHandler(&hdma_spi1_tx);
}

下面是DMA 写入函数,用这个替代flash.writeBlock

cpp 复制代码
bool spi1_dma_write_page(uint32_t addr) {
		uint8_t *dma_tx_buf = (uint8_t *)program_data;
    dma_tx_buf[0] = 0x02;                 // 页编程命令
    dma_tx_buf[1] = (addr >> 16) & 0xFF;  // 地址[23:16]
    dma_tx_buf[2] = (addr >> 8) & 0xFF;   // 地址[15:8]
    dma_tx_buf[3] = addr & 0xFF;          // 地址[7:0]
    auto* hspi1 = SPI.getHandle();

    // 先发送写入使能指令
    while (__HAL_SPI_GET_FLAG(hspi1, SPI_FLAG_BSY));
    digitalWrite(FLASH_CS_NUM, 0);
    SPI.transfer(0x06);
    digitalWrite(FLASH_CS_NUM, 1);

    // 发送前确保 SPI 处于空闲状态,CS 已拉低
    while (__HAL_SPI_GET_FLAG(hspi1, SPI_FLAG_BSY));
    enable_spi1_dma_tx();
    digitalWrite(FLASH_CS_NUM, 0);

    // 启动 DMA 发送
    auto ret = HAL_SPI_Transmit_DMA(hspi1, dma_tx_buf, PAGE_SIZE + 4);
    if (ret != HAL_OK) {
        Serial.println("SPI DMA transfer failed");
        Serial.print("Code: ");
        Serial.println(ret);
        return false;
    }

    // 轮询等待 DMA 传输完成(或者用 TXC 标志)
    uint32_t timeout = 1000;  // 适当超时值
    while (__HAL_SPI_GET_FLAG(hspi1, SPI_FLAG_BSY)) {
        if (--timeout == 0) return false;
    }
    // 清 TXC 标志
    CLEAR_BIT(SPI1->SR, SPI_SR_TXE | SPI_SR_OVR | SPI_SR_MODF);
    disable_spi1_dma_tx();
    digitalWrite(FLASH_CS_NUM, 1);

    return true;
}

dma_tx_buf[260] 把4 字节写入指令和256 字节数据打包到一起,让DMA 一次发送。

cpp 复制代码
uint32_t program_data[PAGE_SIZE / 4 + 1];

void init_tx_data_buf() {
    // 把后面256字节设置为0x8 和0x7 重复序列
    // 前4 个字节为命令字节
    for (size_t i = 1; i < sizeof(program_data) / sizeof(program_data[0]); i++) {
        program_data[i] = 0x07080708;
    }
}

还有一些其他零碎要改的地方,完整代码如下,结构有点乱,但是懒得改了。

cpp 复制代码
#include <Arduino.h>
#include <SPI.h>
#include <SparkFun_SPI_SerialFlash.h>

// 引脚定义放这个头文件里,省略
#include "app.hpp"

constexpr size_t FLASH_SIZE = 4 * 1024 * 1024;  // 4MB
constexpr size_t SECTOR_SIZE = 4 * 1024;        // 4KB,擦除单位
constexpr size_t PAGE_SIZE = 256;               // 256B,写入单位
constexpr size_t SPI_FREQ = 18'000'000;

SFE_SPI_FLASH flash;

void loop() {
    Serial.println("BEEP");
    delay(2000);
}


uint32_t program_data[PAGE_SIZE / 4 + 1];


void enable_spi1_dma_tx() {
    SET_BIT(SPI1->CR2, SPI_CR2_TXDMAEN);
}


void disable_spi1_dma_tx() {
    CLEAR_BIT(SPI1->CR2, SPI_CR2_TXDMAEN);
}


void init_tx_data_buf() {
    // 把后面256字节设置为0x8 和0x7 重复序列
    // 前4 个字节为命令字节
    for (size_t i = 1; i < sizeof(program_data) / sizeof(program_data[0]); i++) {
        program_data[i] = 0x07080708;
    }
}


bool spi1_dma_write_page(uint32_t addr) {
    uint8_t *dma_tx_buf = (uint8_t *)program_data;
    dma_tx_buf[0] = 0x02;                 // 页编程命令
    dma_tx_buf[1] = (addr >> 16) & 0xFF;  // 地址[23:16]
    dma_tx_buf[2] = (addr >> 8) & 0xFF;   // 地址[15:8]
    dma_tx_buf[3] = addr & 0xFF;          // 地址[7:0]
    auto* hspi1 = SPI.getHandle();

    // 先发送写入使能指令
    while (__HAL_SPI_GET_FLAG(hspi1, SPI_FLAG_BSY));
    digitalWrite(FLASH_CS_NUM, 0);
    SPI.transfer(0x06);
    digitalWrite(FLASH_CS_NUM, 1);

    // 发送前确保 SPI 处于空闲状态,CS 已拉低
    while (__HAL_SPI_GET_FLAG(hspi1, SPI_FLAG_BSY));
    enable_spi1_dma_tx();
    digitalWrite(FLASH_CS_NUM, 0);

    // 启动 DMA 发送
    auto ret = HAL_SPI_Transmit_DMA(hspi1, dma_tx_buf, PAGE_SIZE + 4);
    if (ret != HAL_OK) {
        Serial.println("SPI DMA transfer failed");
        Serial.print("Code: ");
        Serial.println(ret);
        return false;
    }

    // 轮询等待 DMA 传输完成(或者用 TXC 标志)
    uint32_t timeout = 1000;  // 适当超时值
    while (__HAL_SPI_GET_FLAG(hspi1, SPI_FLAG_BSY)) {
        if (--timeout == 0) return false;
    }
    // 清 TXC 标志
    CLEAR_BIT(SPI1->SR, SPI_SR_TXE | SPI_SR_OVR | SPI_SR_MODF);
    disable_spi1_dma_tx();
    digitalWrite(FLASH_CS_NUM, 1);

    return true;
}


void storage_test() {
    Serial.println("<< Full chip program >>");

    Serial.println("-> Erase");
    auto start_time = millis();
    flash.erase();
    auto elapsed_time = millis() - start_time;
    Serial.print("Erase done, time: ");
    Serial.print(elapsed_time);
    Serial.println("ms");

    // ============================================
    Serial.println("-> Write");
    init_tx_data_buf();
    start_time = millis();
    uint32_t min_page_time = 0xFFFFFFFF;
    uint32_t max_page_time = 0;
    uint64_t total_page_time = 0;
    uint64_t total_transfer_time = 0;
    auto ss = SPISettings(SPI_FREQ, MSBFIRST, SPI_MODE0);
    SPI.beginTransaction(ss);
    for (size_t page_idx = 0; page_idx < FLASH_SIZE; page_idx += PAGE_SIZE) {
        auto page_start_time = micros();
        auto r = spi1_dma_write_page(page_idx);
        auto transfer_time = micros() - page_start_time;
        total_transfer_time += transfer_time;
        if (!flash.blockingBusyWait(50)) {
            Serial.print("Busy forever. Write failed at page: ");
            Serial.println(page_idx);
            return;
        }
        elapsed_time = micros() - page_start_time;
        if (!r) {
            Serial.print("Write failed at page: ");
            Serial.println(page_idx);
            return;
        }

        total_page_time += elapsed_time;
        if (elapsed_time < min_page_time) {
            min_page_time = elapsed_time;
        }
        if (elapsed_time > max_page_time) {
            max_page_time = elapsed_time;
        }
    }

    SPI.endTransaction();
    Serial.print("Min page time: ");
    Serial.print(min_page_time);
    Serial.println("us");
    Serial.print("Max page time: ");
    Serial.print(max_page_time);
    Serial.println("us");
    Serial.print("Average page time: ");
    Serial.print(total_page_time / (FLASH_SIZE / PAGE_SIZE));
    Serial.println("us");
    Serial.print("Average transfer time: ");
    Serial.print(total_transfer_time / (FLASH_SIZE / PAGE_SIZE));
    Serial.println("us");

    Serial.print("Write done, time: ");
    Serial.print(millis() - start_time);
    Serial.println("ms");

    // ============================================
    Serial.println("-> Verify (Fast read full chip)");
    SPI.beginTransaction(ss);
    digitalWrite(FLASH_CS_NUM, LOW);
    SPI.transfer(0x0b);
    SPI.transfer(0);
    SPI.transfer(0);
    SPI.transfer(0);
    SPI.transfer(0x55);  // dummy byte
    uint8_t last_byte = 0x7;
    uint32_t verify_time;
    start_time = millis();
    for (size_t i = 0; i < FLASH_SIZE; i++) {
        // 直接寄存器轮询传输,避免使用库函数的开销
        while (!(SPI1->SR & SPI_SR_TXE));
        SPI1->DR = 0xFF;
        // 等待接收非空(RXNE=1)
        while (!(SPI1->SR & SPI_SR_RXNE));
        auto b = SPI1->DR;

        if (b + last_byte != 0xf) {
            Serial.print("Verify failed at byte: ");
            Serial.println(i);
            Serial.print("Last byte: ");
            Serial.println(last_byte);
            Serial.print("Current byte: ");
            Serial.println(b);
            goto END_TRANSACTION;
        }
        last_byte = b;
    }
    verify_time = millis() - start_time;
    Serial.println("Verify done");
    Serial.print("Verify time, ms: ");
    Serial.print(verify_time);
    Serial.println("ms");
END_TRANSACTION:
    digitalWrite(FLASH_CS_NUM, HIGH);
    SPI.endTransaction();
}


extern "C" uint32_t spi_getClkFreqInst(SPI_TypeDef* spi_inst);

DMA_HandleTypeDef hdma_spi1_tx;


void setup_spi_dma() {
    // 使能 DMA1 时钟
    __HAL_RCC_DMA1_CLK_ENABLE();

    // 配置 DMA 发送通道(通道3,从内存到 SPI1->DR)
    hdma_spi1_tx.Instance = DMA1_Channel3;
    hdma_spi1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_spi1_tx.Init.PeriphInc = DMA_PINC_DISABLE;  // 外设地址不变
    hdma_spi1_tx.Init.MemInc = DMA_MINC_ENABLE;      // 内存地址递增
    hdma_spi1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_spi1_tx.Init.Mode = DMA_NORMAL;  // 单次传输,不循环
    hdma_spi1_tx.Init.Priority = DMA_PRIORITY_HIGH;

    HAL_DMA_Init(&hdma_spi1_tx);
    NVIC_EnableIRQ(DMA1_Channel3_IRQn);

    // 将 DMA 通道关联到 SPI1 的 TX
    __HAL_LINKDMA(SPI.getHandle(), hdmatx, hdma_spi1_tx);
}


void setup() {
    SPI.setSCLK(SCK_NUM);
    SPI.setMOSI(MOSI_NUM);
    SPI.setMISO(MISO_NUM);
    SPI.begin();
    setup_spi_dma();
    Serial.begin(230400);
    Serial.println("<< Flash test >>");

    Serial.print("SPI Max freq: ");
    Serial.print(spi_getClkFreqInst(SPI1) / 2 / 1000000);
    Serial.println(" MHz");
    Serial.print("SPI Freq: ");
    Serial.print(SPI_FREQ / 1000000);
    Serial.println(" MHz");

    // flash.enableDebugging();
    if (!flash.begin(FLASH_CS_NUM, SPI_FREQ)) {
        Serial.println("Failed to initialize flash");
        while (1);
    }

    Serial.println("Flash ok");
    auto mfgID = flash.getManufacturerID();
    if (mfgID != SFE_FLASH_MFG_UNKNOWN) {
        Serial.print(F("Manufacturer: "));
        Serial.println(flash.manufacturerIDString(mfgID));
    }
    else {
        uint8_t unknownID = flash.getRawManufacturerID();  // Read the raw manufacturer ID
        Serial.print(F("Unknown manufacturer ID: 0x"));
        if (unknownID < 0x10) Serial.print(F("0"));  // Pad the zero
        Serial.println(unknownID, HEX);
    }

    Serial.print(F("Device ID: 0x"));
    Serial.println(flash.getDeviceID(), HEX);

    auto start_time = millis();
    Serial.println("Full chip erase, write, verify");
    Serial.println("Confirm? (y/n)");

    while (millis() - start_time < 30000) {
        if (Serial.available() > 0) {
            char c = Serial.read();
            if (c == 'n' || c == 'N') {
                Serial.println("Test skipped");
                return;
            }
            if (c == 'y' || c == 'Y') {
                storage_test();
                return;
            }
        }
    }

    Serial.println("Timeout 30s, Skip test");
}


extern "C" {
/**
 * @brief  This function handles DMA Tx interrupt request.
 * @param  None
 * @retval None
 */
void DMA1_Channel3_IRQHandler(void) {
    HAL_DMA_IRQHandler(&hdma_spi1_tx);
}
}

改完以后再测试,总写入时间差不多减半,从32 秒减少到了16 秒,相应的写入速度也从125kB/s 提升到了256kB/s。串口输出结果如下:

复制代码
-> Write
Min page time: 902us
Max page time: 999us
Average page time: 998us
Average transfer time: 135us
Write done, time: 16387ms

每页总写入时间1 毫秒,减去传输时间,flash 内部页编程时间大约0.8毫秒。对照W25Q32 的数据手册,上面说页编程时间为0.4~3 毫秒,所以0.8 毫秒还算可以了。

滚动擦除写入

实际对于数据记录场景,都是滚动擦除+写入,不会全片擦除。原理大概是:

  1. 需要写入256 字节时,判断要写入的地方事先擦除过没有
  2. 如果没擦除,就执行扇区擦除命令,一次最少擦除4kB,最多64kB
  3. 擦除之后,在已擦除扇区里持续写入,直到把扇区写满,然后再向后擦除一个扇区

要考虑的问题:

  1. 擦除速度比写入速度慢的多,4kB 擦除典型值45 毫秒,32kB、64kB 擦除典型值分别是120、150 毫秒
  2. 擦除开始后不能接着往里写,必须等它擦完

1、理论写入速度

如果选择4kB 擦除,那么擦除过程中接收到的数据全部要先存在单片机RAM 里,等擦除完了再存进去。按照之前的测量结果,4kB 对应16 个页,总写入时间约16 毫秒,所以4kB 数据量的擦除+写入周期总时间至少61 毫秒,那么写入速度就只有65kB/s。这个速度远低于之前测得全片擦除+写入速度,因为扇区擦除速度比全片擦除慢的多,4MB 容量对应1024 个4kB 扇区,所有扇区都擦一遍,至少需要46 秒。下面是理论上擦除不同扇区大小对应的周期写入速度:

扇区 擦除时间(典型) 写时间 总周期(典型) 写入速度(典型) 擦除时间(最差) 总周期(最差) 写入速度(最差)
4 kB 45 ms 16 ms 61 ms 65 kB/s 400 ms 416 ms 9 kB/s
32 kB 120 ms 128 ms 248 ms 129 kB/s 1600 ms 1728 ms 18 kB/s
64 kB 150 ms 256 ms 406 ms 157 kB/s 2000 ms 2256 ms 28 kB/s

2、实测速度

修改之前的代码,去掉全片擦除,改为按扇区擦除和写入。先要添加一个执行扇区擦除的函数:

cpp 复制代码
bool flash_erase_sector(uint32_t sector_addr) {
    static_assert(SECTOR_SIZE == 4 * 1024 || SECTOR_SIZE == 32 * 1024 || SECTOR_SIZE == 64 * 1024, "Sector size must be 4KB, 32KB, or 64KB");
    uint8_t erase_cmd = 0x20;
    if (SECTOR_SIZE == 32 * 1024) {
        erase_cmd = 0x52;
    }
    else if (SECTOR_SIZE == 64 * 1024) {
        erase_cmd = 0xd8;
    }

    // 必须事先调用beginTransaction
    // SPI.beginTransaction(ss);
    // 先发送写入使能指令
    digitalWrite(FLASH_CS_NUM, LOW);
    SPI.transfer(0x06);
    digitalWrite(FLASH_CS_NUM, HIGH);
    // 发送擦除指令
    digitalWrite(FLASH_CS_NUM, LOW);
    SPI.transfer(erase_cmd);
    SPI.transfer(sector_addr >> 16);
    SPI.transfer(sector_addr >> 8);
    SPI.transfer(sector_addr);
    digitalWrite(FLASH_CS_NUM, HIGH);
    // SPI.endTransaction();
    return true;
}

不同尺寸的擦除指令是不同的,用常量SECTOR_SIZE 设置一次要擦除的量,然后匹配对应的擦除指令。SECTOR_SIZE 可选4k、32k、64k,设置这之外的值会编译失败。

还要改写入部分,写个嵌套for 循环,完整代码放后面,先来看测试结果。运行程序后,串口输出信息如下,这是选择4kB 擦除时的数据:

复制代码
<< Full chip program (by sector) >>
Sector size: 4KB

-> Write
Min page time: 998us
Max page time: 999us
Average page time: 998us
Average transfer time: 135us

Min sector erase time: 29ms
Max sector erase time: 39ms
Average sector erase time: 31ms
Average sector write time: 16ms
Write done, time: 48377ms

-> Verify (Fast read full chip)
Verify done
Verify time, ms: 4199ms

4kB 扇区最小擦除时间29 毫秒,最大39 毫秒,平均31 毫秒,比数据手册的典型值好多了。下面是擦除扇区4k,32k,64k 的测试情况,一共试了两片flash 芯片。

扇区大小 平均写入时间 平均擦除时间 最小擦除时间 最大擦除时间 平均周期 平均写入 最差周期 最差写入
4 kB 16 ms 31 ms 29 ms 39 ms 47 ms 85 kB/s 55 ms 72 kB/s
4 kB (2) 16 ms 25 ms 23 ms 37 ms 41 ms 97 kB/s 53 ms 75 kB/s
32 kB 128 ms 98 ms 95 ms 106 ms 226 ms 141 kB/s 234 ms 136 kB/s
32 kB (2) 128 ms 101 ms 95 ms 133 ms 229 ms 139 kB/s 261 ms 122 kB/s
64 kB 256 ms 113 ms 110 ms 122 ms 369 ms 173 kB/s 378 ms 169 kB/s
64 kB (2) 256 ms 138 ms 131 ms 178 ms 394 ms 162 kB/s 434 ms 147 kB/s

其中,平均周期是平均擦除时间加上写入时间,最差周期是最大擦除时间加上写入时间。

最大擦除时间基本上是在典型值附近,4kB 擦除时间显著优于典型值,算下来写入速度都比上面的理论值要好一点。另一方面,也可以看到擦除时间波动是比较大的,尤其是第2 片flash,擦除时间波动显著更大,它的32kB 和64kB 擦除时间最大、最小差值分别达到了32ms 和40ms,相比平均值,最大波动30%。

可以预见,不同芯片之间参数差异会比较大,同一片芯片在长时间使用中,擦除时间也可能会显著变化。所以实际应用中,应该实时监控擦除和写入时间,作为健康度指标。如果性能不再满足需求,要及时以某种方式发出提示。批量用的时候可能也应该提前筛出参数离群的芯片。

3、完整测试代码(除了app.hpp 头文件)

按扇区擦写测试的完整代码:

cpp 复制代码
#include <Arduino.h>
#include <SPI.h>
#include <SparkFun_SPI_SerialFlash.h>

#include "app.hpp"

// W25Q32 4MB flash
constexpr size_t FLASH_SIZE = 4 * 1024 * 1024;  // 4MB
constexpr size_t SECTOR_SIZE = 64 * 1024;        // 64KB,擦除单位
constexpr size_t PAGE_SIZE = 256;               // 256B,写入单位
constexpr size_t SPI_FREQ = 18'000'000;

SFE_SPI_FLASH flash;

void loop() {
    Serial.println("BEEP");
    delay(2000);
}


uint32_t program_data[PAGE_SIZE / 4 + 1];


void enable_spi1_dma_tx() {
    SET_BIT(SPI1->CR2, SPI_CR2_TXDMAEN);
}


void disable_spi1_dma_tx() {
    CLEAR_BIT(SPI1->CR2, SPI_CR2_TXDMAEN);
}


void init_tx_data_buf() {
    // 把后面256字节设置为0x8 和0x7 重复序列
    // 前4 个字节为命令字节
    for (size_t i = 1; i < sizeof(program_data) / sizeof(program_data[0]); i++) {
        program_data[i] = 0x07080708;
    }
}


bool spi1_dma_write_page(uint32_t addr) {
    uint8_t* dma_tx_buf = (uint8_t*)program_data;
    dma_tx_buf[0] = 0x02;                 // 页编程命令
    dma_tx_buf[1] = (addr >> 16) & 0xFF;  // 地址[23:16]
    dma_tx_buf[2] = (addr >> 8) & 0xFF;   // 地址[15:8]
    dma_tx_buf[3] = addr & 0xFF;          // 地址[7:0]
    auto* hspi1 = SPI.getHandle();

    // 先发送写入使能指令
    // while (__HAL_SPI_GET_FLAG(hspi1, SPI_FLAG_BSY));
    digitalWrite(FLASH_CS_NUM, 0);
    SPI.transfer(0x06);
    digitalWrite(FLASH_CS_NUM, 1);

    // 发送前确保 SPI 处于空闲状态,CS 已拉低
    while (__HAL_SPI_GET_FLAG(hspi1, SPI_FLAG_BSY));
    enable_spi1_dma_tx();
    digitalWrite(FLASH_CS_NUM, 0);

    // 启动 DMA 发送
    auto ret = HAL_SPI_Transmit_DMA(hspi1, dma_tx_buf, PAGE_SIZE + 4);
    if (ret != HAL_OK) {
        Serial.println("SPI DMA transfer failed");
        Serial.print("Code: ");
        Serial.println(ret);
        return false;
    }

    // 轮询等待 DMA 传输完成(或者用 TXC 标志)
    uint32_t timeout = 1000;  // 适当超时值
    while (__HAL_SPI_GET_FLAG(hspi1, SPI_FLAG_BSY)) {
        if (--timeout == 0) return false;
    }
    // 清 TXC 标志
    CLEAR_BIT(SPI1->SR, SPI_SR_TXE | SPI_SR_OVR | SPI_SR_MODF);
    disable_spi1_dma_tx();
    digitalWrite(FLASH_CS_NUM, 1);

    return true;
}


bool flash_erase_sector(uint32_t sector_addr) {
    static_assert(SECTOR_SIZE == 4 * 1024 || SECTOR_SIZE == 32 * 1024 || SECTOR_SIZE == 64 * 1024, "Sector size must be 4KB, 32KB, or 64KB");
    uint8_t erase_cmd = 0x20;
    if (SECTOR_SIZE == 32 * 1024) {
        erase_cmd = 0x52;
    }
    else if (SECTOR_SIZE == 64 * 1024) {
        erase_cmd = 0xd8;
    }

    // 必须事先调用beginTransaction
    // auto ss = SPISettings(SPI_FREQ, MSBFIRST, SPI_MODE0);
    // SPI.beginTransaction(ss);
    // 先发送写入使能指令
    digitalWrite(FLASH_CS_NUM, LOW);
    SPI.transfer(0x06);
    digitalWrite(FLASH_CS_NUM, HIGH);
    // 发送擦除指令
    digitalWrite(FLASH_CS_NUM, LOW);
    SPI.transfer(erase_cmd);
    SPI.transfer(sector_addr >> 16);
    SPI.transfer(sector_addr >> 8);
    SPI.transfer(sector_addr);
    digitalWrite(FLASH_CS_NUM, HIGH);
    // SPI.endTransaction();
    return true;
}


void storage_test() {
    Serial.println("<< Full chip program (by sector) >>");
    Serial.print("Sector size: ");
    Serial.print(SECTOR_SIZE / 1024);
    Serial.println("KB");
    
    // ============================================
    Serial.println("-> Write");
    init_tx_data_buf();
    auto ss = SPISettings(SPI_FREQ, MSBFIRST, SPI_MODE0);
    SPI.beginTransaction(ss);
    auto start_time = millis();
    uint32_t elapsed_time = 0;
    uint32_t min_page_time = 0xFFFFFFFF;
    uint32_t max_page_time = 0;
    uint64_t total_page_time = 0;
    uint64_t total_transfer_time = 0;
    uint32_t min_sector_erase_time = 0xFFFFFFFF;
    uint32_t max_sector_erase_time = 0;
    uint64_t total_sector_erase_time = 0;
    uint64_t total_sector_write_time = 0;

    for (uint32_t sector_addr = 0; sector_addr < FLASH_SIZE; sector_addr += SECTOR_SIZE) {
        auto sector_start_time = millis();
        flash_erase_sector(sector_addr);
        if (!flash.blockingBusyWait(500)) {
            Serial.print("Busy forever. Write failed at sector: ");
            Serial.println(sector_addr);
            return;
        }
        auto sector_erase_time = millis() - sector_start_time;
        total_sector_erase_time += sector_erase_time;
        if (sector_erase_time < min_sector_erase_time) {
            min_sector_erase_time = sector_erase_time;
        }
        if (sector_erase_time > max_sector_erase_time) {
            max_sector_erase_time = sector_erase_time;
        }

        auto sector_write_start_time = millis();
        for (size_t page_addr = sector_addr; page_addr < sector_addr + SECTOR_SIZE; page_addr += PAGE_SIZE) {
            auto page_start_time = micros();
            auto r = spi1_dma_write_page(page_addr);
            auto transfer_time = micros() - page_start_time;
            total_transfer_time += transfer_time;
            if (!flash.blockingBusyWait(50)) {
                Serial.print("Busy forever. Write failed at page: ");
                Serial.println(page_addr);
                return;
            }
            elapsed_time = micros() - page_start_time;
            if (!r) {
                Serial.print("Write failed at page: ");
                Serial.println(page_addr);
                return;
            }

            total_page_time += elapsed_time;
            if (elapsed_time < min_page_time) {
                min_page_time = elapsed_time;
            }
            if (elapsed_time > max_page_time) {
                max_page_time = elapsed_time;
            }
        }
        auto sector_write_time = millis() - sector_write_start_time;
        total_sector_write_time += sector_write_time;
    }

    SPI.endTransaction();
    auto write_time = millis() - start_time;
    Serial.print("Min page time: ");
    Serial.print(min_page_time);
    Serial.println("us");
    Serial.print("Max page time: ");
    Serial.print(max_page_time);
    Serial.println("us");
    Serial.print("Average page time: ");
    Serial.print(total_page_time / (FLASH_SIZE / PAGE_SIZE));
    Serial.println("us");
    Serial.print("Average transfer time: ");
    Serial.print(total_transfer_time / (FLASH_SIZE / PAGE_SIZE));
    Serial.println("us");
    // 扇区擦除时间
    Serial.print("Min sector erase time: ");
    Serial.print(min_sector_erase_time);
    Serial.println("ms");
    Serial.print("Max sector erase time: ");
    Serial.print(max_sector_erase_time);
    Serial.println("ms");
    Serial.print("Average sector erase time: ");
    Serial.print(total_sector_erase_time / (FLASH_SIZE / SECTOR_SIZE));
    Serial.println("ms");
    // 扇区写入时间
    Serial.print("Average sector write time: ");
    Serial.print(total_sector_write_time / (FLASH_SIZE / SECTOR_SIZE));
    Serial.println("ms");

    Serial.print("Write done, time: ");
    Serial.print(write_time);
    Serial.println("ms");

    // ============================================
    Serial.println("-> Verify (Fast read full chip)");
    SPI.beginTransaction(ss);
    digitalWrite(FLASH_CS_NUM, LOW);
    SPI.transfer(0x0b);
    SPI.transfer(0);
    SPI.transfer(0);
    SPI.transfer(0);
    SPI.transfer(0x55);  // dummy byte
    uint8_t last_byte = 0x7;
    uint32_t verify_time;
    start_time = millis();
    for (size_t i = 0; i < FLASH_SIZE; i++) {
        // 直接寄存器轮询传输,避免使用库函数的开销
        while (!(SPI1->SR & SPI_SR_TXE));
        SPI1->DR = 0xFF;
        // 等待接收非空(RXNE=1)
        while (!(SPI1->SR & SPI_SR_RXNE));
        auto b = SPI1->DR;

        if (b + last_byte != 0xf) {
            Serial.print("Verify failed at byte: ");
            Serial.println(i);
            Serial.print("Last byte: ");
            Serial.println(last_byte);
            Serial.print("Current byte: ");
            Serial.println(b);
            goto END_TRANSACTION;
        }
        last_byte = b;
    }
    verify_time = millis() - start_time;
    Serial.println("Verify done");
    Serial.print("Verify time, ms: ");
    Serial.print(verify_time);
    Serial.println("ms");
END_TRANSACTION:
    digitalWrite(FLASH_CS_NUM, HIGH);
    SPI.endTransaction();
}


extern "C" uint32_t spi_getClkFreqInst(SPI_TypeDef* spi_inst);

DMA_HandleTypeDef hdma_spi1_tx;


void setup_spi_dma() {
    // 使能 DMA1 时钟
    __HAL_RCC_DMA1_CLK_ENABLE();

    // 配置 DMA 发送通道(通道3,从内存到 SPI1->DR)
    hdma_spi1_tx.Instance = DMA1_Channel3;
    hdma_spi1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_spi1_tx.Init.PeriphInc = DMA_PINC_DISABLE;  // 外设地址不变
    hdma_spi1_tx.Init.MemInc = DMA_MINC_ENABLE;      // 内存地址递增
    hdma_spi1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_spi1_tx.Init.Mode = DMA_NORMAL;  // 单次传输,不循环
    hdma_spi1_tx.Init.Priority = DMA_PRIORITY_HIGH;

    HAL_DMA_Init(&hdma_spi1_tx);
    NVIC_EnableIRQ(DMA1_Channel3_IRQn);

    // 将 DMA 通道关联到 SPI1 的 TX
    __HAL_LINKDMA(SPI.getHandle(), hdmatx, hdma_spi1_tx);
}


void setup() {
    SPI.setSCLK(SCK_NUM);
    SPI.setMOSI(MOSI_NUM);
    SPI.setMISO(MISO_NUM);
    SPI.begin();
    setup_spi_dma();
    Serial.begin(230400);
    Serial.println("<< Flash test >>");

    Serial.print("SPI Max freq: ");
    Serial.print(spi_getClkFreqInst(SPI1) / 2 / 1000000);
    Serial.println(" MHz");
    Serial.print("SPI Freq: ");
    Serial.print(SPI_FREQ / 1000000);
    Serial.println(" MHz");

    // flash.enableDebugging();
    if (!flash.begin(FLASH_CS_NUM, SPI_FREQ)) {
        Serial.println("Failed to initialize flash");
        while (1);
    }

    Serial.println("Flash ok");
    auto mfgID = flash.getManufacturerID();
    if (mfgID != SFE_FLASH_MFG_UNKNOWN) {
        Serial.print(F("Manufacturer: "));
        Serial.println(flash.manufacturerIDString(mfgID));
    }
    else {
        uint8_t unknownID = flash.getRawManufacturerID();  // Read the raw manufacturer ID
        Serial.print(F("Unknown manufacturer ID: 0x"));
        if (unknownID < 0x10) Serial.print(F("0"));  // Pad the zero
        Serial.println(unknownID, HEX);
    }

    Serial.print(F("Device ID: 0x"));
    Serial.println(flash.getDeviceID(), HEX);

    auto start_time = millis();
    Serial.println("Full chip erase, write, verify");
    Serial.println("Confirm? (y/n)");

    while (millis() - start_time < 30000) {
        if (Serial.available() > 0) {
            char c = Serial.read();
            if (c == 'n' || c == 'N') {
                Serial.println("Test skipped");
                return;
            }
            if (c == 'y' || c == 'Y') {
                storage_test();
                return;
            }
        }
    }

    Serial.println("Timeout 30s, Skip test");
}


extern "C" {
/**
 * @brief  This function handles DMA Tx interrupt request.
 * @param  None
 * @retval None
 */
void DMA1_Channel3_IRQHandler(void) {
    HAL_DMA_IRQHandler(&hdma_spi1_tx);
}
}

总结

由以上对比可知:如果选择4kB 擦除,每次需要等待的时间相对短一点,单片机RAM 里也可以完整放下4kB 缓冲区;如果选64kB 擦除,总体平均写入速度更快,但是单次擦除时间更长。在循环记录数据时,擦除单位越大,数据损失也越大。

我觉得32kB 擦除相对更平衡一点。考虑记录串口数据的场景,如果波特率是230400,格式8N1,一帧包含:1bit 起始位、1bit 结束位、8bit 数据,所以数据速度是22.5 kB/s。按照上面实测的最差情况,在一个擦除+写入周期261 毫秒内,只有约5.9kB 数据需要缓冲在RAM 里。把周期放宽到400 毫秒,数据量增加到9kB,单片机RAM 里准备10kB 空间作为环形缓冲就可以搞定了。此外,如果搭配实时数据压缩的话,所需的缓冲空间还可能降低。

以上是用全扇区写入时间作为保守的估计值来计算的。实测的扇区擦除时间最大是178 毫秒,按照230400 波特率,这期间能到达的数据只有4kB,16 毫秒就能写完。

64kB 擦除时,或许更能耐受短时间高速通信,因为只要当前扇区没满,就能以最大速度写入。选64kB 也可能降低擦除过程中掉电的风险,因为擦除频率较低。然后如果擦除刚好撞上了掉电,那用4kB 扇区或许可以让不确定性最小化。

顺便,考虑最大用10kB 缓冲区的情况,DS 计算的最大持续接收能力是这样的:

有 10KB 缓冲区时,瓶颈从"总周期吞吐"变为 "擦除期间缓冲区装不装得下"

约束条件:擦除期间到达的数据必须全部塞进缓冲区,即 速率 × 擦除时间 ≤ 10KB

按实测最差情况(第 2 片芯片的最大擦除时间):

扇区 擦除时间 缓冲约束 吞吐约束 实际最大速率 对应波特率(8N1)
4KB 39ms 256 KB/s 73 KB/s ~73 KB/s ~584k
32KB 133ms 77 KB/s 123 KB/s ~77 KB/s ~616k
64KB 178ms 56 KB/s 147 KB/s ~56 KB/s ~448k

结论反转了:

  • 64KB 扇区反而最慢------擦除那 178ms 只能收约 10KB,缓冲一满数据就得丢,跟后续能写多快没关系。
  • 4KB 和 32KB 基本持平,差距在工程误差范围内(73 vs 77 KB/s)。
  • 三者都轻松覆盖 230400 波特(约22.5KB/s),距离瓶颈还有 2.5 到3.4 倍裕量。

所以 10KB 缓冲前提下,4KB 扇区综合最优:速度没吃亏,掉电损失面最小,擦除时间抖动也最低。原文对 32KB 的推荐在这条件下仍然成立,但 64KB 可以排除。

相关推荐
夜猫子ing2 小时前
《嵌入式 Linux 控制服务从零搭建(一):项目立意与架构总览》
linux·嵌入式硬件
森旺电子2 小时前
candence操作
单片机·嵌入式硬件·cadence
czwxkn3 小时前
pcb设计-电路:基准电压电路(TL431)
单片机·嵌入式硬件
三佛科技-134163842123 小时前
LED阅读灯方案开发,LED护眼读书灯单片机选择(FT60F010A,FT61F023,FT62F211,FT62F0MBA,FT32F103)
单片机·嵌入式硬件·智能家居·pcb工艺
上海合宙LuatOS3 小时前
合宙Air1601 MCU模组-硬件开发手册
单片机·嵌入式硬件·物联网·luatos
W.W.H.4 小时前
STM32实现LED闪烁和串口打印案例
stm32·单片机·嵌入式硬件·usart·gd32·dap-link
LCG元4 小时前
STM32实战:基于STM32F103的智能语音识别系统(LD3320)
stm32·嵌入式硬件·语音识别
Jason_zhao_MR4 小时前
RK3576 MIPI Camera ISP调试:客观标定与环境准备(上)
人工智能·嵌入式硬件·机器人·嵌入式·接口隔离原则
深圳市晶科鑫实业有限公司4 小时前
RTC模块vs. 32.768KHz晶振:深度对比与选型指南
stm32·单片机·嵌入式硬件·实时音视频·rtc