【WCH蓝牙系列芯片】-基于CH585开发板—SPI外设读写W25Q64

-------------------------------------------------------------------------------------------------------------------------------------

  在CH585 芯片提供 2 个 SPI 接口(SPI0 和 SPI1),CH584 芯片仅提供了 SPI0。SPI是一种高速的,全双工,同步的串行通信接口,总线上连接有一个主机和若干从机,同一时刻,仅有一对主从在通讯(一主多从)。

  通常 SPI 接口由 4 个引脚组成:

  SPI 片选引脚 SCS —————— 从设备使能信号,由主设备控制,一主多从时,SCS是从芯片是否被主芯片选中的控制信号,当片选信号为规定的使能信号时(高电位或低电位),只有被选中的从设备才会响应主机发送的数据和命令。

  SPI 时钟引脚 SCK ——————传输时钟信号,用于主从设备的同步

  SPI 串行数据引脚 MISO(主机输入/从机输出引脚)—————— 主模式时候:主机输入,从机输出 /从模式时候:主机输出。从机输入

  SPI 串行数据引脚 MOSI(主机输出/从机输入引脚)—————— 主模式时候:从机输入,主机输出 / 从模式时候:从机输出。主机输入

  在CH585芯片种SPI0 支持主机模式(Master)和从机模式(Slave),SPI1 只支持主机模式(Master);支持模式 0 和模式 3 数据传输 SPI时钟频率最高可达系统主频 Fsys 的一半。SPI接口主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。

   这次利用CH585的SPI0操作读写FLASH芯片W25Q64,有些情况下由于MCU的FLASH空间有限,在特殊使用场所中会存在FLASH存储不够使用的情况,W25Q64作为一种高性能的串行Flash存储器,就可以用于扩展MCU的存储空间。

   W25Q64由每页256字节组成,每页的256字节用一次页编程指令即可完成。每次可以擦除16页(一个扇区),128页(32KB块),256页(64KB块)和全片擦除。 W25Q64的内存空间结构:一页256字节,4k(4096字节)为一个扇区,16个扇区为一块,总容量为8M字节,共有128个块即2048个扇区。SPI最高支持80MHZ,当使用快读双倍/四倍指令时,相当于双倍输出时最高速率160MHZ,四倍输出时最高速率320MHZ。

1、W25Q64的硬件电路

image

2、W25Q64的引脚功能说明

image

3、W25Q64框图

image

4、SPI操作W25Q64程序说明

  在进行任何数据操作前,需先完成 SPI 接口初始化和 Flash 芯片识别,确保硬件通信正常且芯片型号匹配。

1. SPI 硬件初始化(FLASH_Port_Init()

  这里采用 SPI0 的 SCS(PA12)、SCK(PA13)、MOSI(PA14)配置为推挽输出,初始化为高电平;MISO(PA15)配置为上拉输入(接收 Flash 返回数据)。

image

  • SPI 控制器配置:通过SPI0_MasterDefInit()初始化 SPI0 为主机模式,SPI0_DataMode(Mode3_HighBitINFront)设置为模式 3(时钟空闲时为高电平,下降沿采样),SPI0_CLKCfg(2)设置时钟分频(控制通信速率)

image

2. Flash 芯片识别(FLASH_IC_Check()

  初始化完成后,需确认连接的芯片是否为目标型号(W25Q64),通过读取芯片 ID (FLASH_ReadID())实现:

  • 拉低 CS 引脚(PIN_FLASH_CS_LOW())选中芯片;

  • 发送 “读取 JEDEC ID” 指令(CMD_FLASH_JEDEC_ID = 0x9F);

  • 连续读取 3 字节数据(制造商 ID、内存类型 ID、容量 ID),拼接为 32 位Flash_ID

  • 拉高 CS 引脚(PIN_FLASH_CS_HIGH())释放芯片。

  • Main.c中,通过判断Flash_ID是否为W25Q64_FLASH_ID1 (0xEF4017)W25Q64_FLASH_ID2 (0xEF6017)确认是否为 W25Q64

  • image

二、擦除操作(以扇区擦除为例)

  W25Q64 的擦除操作需一次擦除大小可以为16页(4KB)、128页(32KB)、256页(64KB)或者全擦除。(每个扇区 4096 字节,定义为SPI_FLASH_SectorSize),擦除后扇区内所有数据变为0xFF

1. 擦除前准备:发送写使能

Flash 芯片默认禁止写入 / 擦除操作,需先发送 “写使能” 指令(CMD_FLASH_WREN = 0x06):

  • 拉低 CS -> 发送CMD_FLASH_WREN -> 拉高 CS(FLASH_WriteEnable()实现)。

2. 执行扇区擦除

  • 拉低 CS 选中芯片;

  • 发送 “扇区擦除” 指令(CMD_FLASH_SECTOR_ERASE = 0x20);

  • 发送 24 位扇区地址(按高 8 位、中 8 位、低 8 位顺序发送,例如地址0x000000对应第一个扇区);

  • 拉高 CS,触发擦除操作。

3. 等待擦除完成

擦除是耗时操作(毫秒级),需通过读取 “状态寄存器” 判断是否完成:

  • 调用FLASH_ReadStatusReg()读取状态寄存器(发送CMD_FLASH_RDSR = 0x05指令后读取 1 字节);

  • 检查状态寄存器的 bit0(忙标志位):bit0=1 表示正在擦除,bit0=0 表示擦除完成;

  • 循环等待直到 bit0=0)。

image

三、写入操作

  W25Q64 的写入需按 “页” 为单位(每页 256 字节,定义为SPI_FLASH_PageSize),且写入前必须确保目标区域已擦除(数据为0xFF)。

1. 写入前的扇区检查与处理

  FLASH_Write()会自动处理跨扇区写入,并确保写入区域已擦除:

  • 计算写入地址所在的扇区(secpos = WriteAddr / 4096)、扇区内偏移(secoff = WriteAddr % 4096)和扇区剩余空间(secremain = 4096 - secoff);

  • 读取该扇区现有数据到缓冲SPI_FLASH_BUF,区检查目标写入区域是否全为0xFF;

    • 若不是(有非0xFF数据):先擦除该扇区(FLASH_Erase_Sector(secpos)),再将现有数据与新数据合并到缓冲区;

    • 若是:直接写入新数据。

2. 页写入(void W25XXX_WR_Page( uint8_t *pbuf, uint32_t address, uint32_t len )

  不管是否跨扇区,最终写入均以页为单位执行:

  • 发送写使能(FLASH_WriteEnable());

  • 拉低 CS -> 发送 “页编程” 指令(CMD_FLASH_BYTE_PROG = 0x02) -> 发送 24 位写入地址;

  • 连续发送数据(长度不超过 256 字节,若超过则截断);

  • 拉高 CS,触发写入操作;

  • 等待写入完成(通过状态寄存器 bit0 判断,同擦除流程)。

3. 跨页 / 跨扇区处理

  若写入数据长度超过 1 页或 1 扇区,FLASH_Write()会自动拆分:

  • 先写满当前页 / 扇区的剩余空间;

  • 地址和数据指针偏移,重复写入下一页 / 扇区,直到所有数据写完。

四、读取操作

  读取操作无需擦除或写使能,可直接按地址连续读取。

image

1. 启动读取

  • 拉低 CS 选中芯片;

  • 发送 “读取” 指令(CMD_FLASH_READ = 0x03);

  • 发送 24 位读取起始地址(高 8 位、中 8 位、低 8 位)。

2. 连续读取数据

  • 循环通过 SPI 读取数据(SPI_FLASH_SendByte(0xFF),发送0xFF作为时钟填充,同时接收 Flash 返回的数据);

  • 数据存入缓冲区(pBuffer),直到读取指定长度(size)。

3. 结束读取

  • 拉高 CS 释放芯片(PIN_FLASH_CS_HIGH())。

五、程序验证

在主函数中,进行“擦除 -> 写入 -> 读取验证的操作,

  1. 擦除块:循环调用FLASH_Erase_Sector()擦除 2 个扇区(组成 8192 字节的块);

  2. 写入块数据:通过FLASH_Write()写入 8192 字节数据(前半部分为0xAA,后半部分为随机值);

  3. 读取验证:调用FLASH_Read()读取整个块数据,通过PrintBuffer()打印整快写入后的数据内容;

  4. 扇区 / 页操作:然后再对单独擦除 1 个扇区、写入扇区数据、擦除 1 页、写入页数据,并且将擦除后的数据,和写入后的数据全部打印出来做验证。

// 测试参数定义
#define BLOCK_SIZE       (2 * SPI_FLASH_SectorSize)  // 测试块大小(2个扇区:8192字节)
#define SECTOR_SIZE      SPI_FLASH_SectorSize        // 扇区大小(4096字节)
#define PAGE_SIZE        SPI_FLASH_PageSize          // 页大小(256字节)
#define TEST_ADDR        0x000000                    // 测试起始地址
#define BLOCK_FILL_CHAR      0xAA  // 块填充字符
#define SECTOR_FILL_CHAR     0x55  // 扇区填充字符
#define PAGE_FILL_CHAR       0x33  // 页填充字符


// 打印缓冲区数据(十六进制和ASCII),每行1024字节
void PrintBuffer(uint8_t *buf, uint16_t len, const char *desc) {
    uint16_t i;
    printf("\r\n===== %s (长度: %d字节) =====\r\n", desc, len);

    for(i = 0; i < len; i++) {
        // 打印当前字节的十六进制(格式:两位十六进制+空格,共3个字符)
        printf("%02X ", buf[i]);

        // 每1024字节换行,并打印对应ASCII字符
        if((i + 1) % 1024 == 0) {
            printf("  ");  // 十六进制与ASCII之间的分隔

            // 打印当前1024字节的ASCII字符(可打印字符直接显示,不可打印用.代替)
            for(int j = i - 1023; j <= i; j++) {
                if(buf[j] >= 0x20 && buf[j] <= 0x7E) {
                    printf("%c", buf[j]);
                } else {
                    printf(".");
                }
            }
            printf("\r\n");  // 换行

            // 每行结束时打印当前累计显示的字节数
            printf("----- 当前已显示 %d 字节 -----\r\n", i + 1);
        }
    }

    // 处理最后一行不足1024字节的情况
    if(i % 1024 != 0) {
        // 补充空格对齐(每个缺失字节对应3个空格,与十六进制格式"XX "匹配)
        for(int j = 0; j < 1024 - (i % 1024); j++) {
            printf("   ");
        }
        printf("  ");  // 分隔符

        // 打印最后一行的ASCII字符
        for(int j = i - (i % 1024); j < i; j++) {
            if(buf[j] >= 0x20 && buf[j] <= 0x7E) {
                printf("%c", buf[j]);
            } else {
                printf(".");
            }
        }
        printf("\r\n");
    }

    // 总字节数不是1024的倍数时,补充显示总长度
    if(len % 1024 != 0) {
        printf("----- 总显示 %d 字节 -----\r\n", len);
    }
    printf("\r\n");
}

void DebugInit(void)           // SPI0与例程惯用串口打印引脚冲突,所以此例程改用UART1_输出打印
{
    GPIOA_SetBits(GPIO_Pin_9);
    GPIOA_ModeCfg(GPIO_Pin_8, GPIO_ModeIN_PU);
    GPIOA_ModeCfg(GPIO_Pin_9, GPIO_ModeOut_PP_5mA);
    UART1_DefInit();
}
int main()
{
    uint8_t block_buf[BLOCK_SIZE];    // 块缓冲区(8192字节)
    uint8_t sector_buf[SECTOR_SIZE];  // 扇区缓冲区(4096字节)
    uint8_t page_buf[PAGE_SIZE];      // 页缓冲区(256字节)
    uint8_t verify_ok = 1;
    uint16_t i;

    HSECFG_Capacitance(HSECap_18p);
    SetSysClock(SYSCLK_FREQ);

    // 初始化调试口
    DebugInit();
    PRINT("Start @ChipID=%02X\n", R8_CHIP_ID);
    printf("W25Q64 块/扇区/页操作测试开始...\r\n");
    printf("块大小: %d字节, 扇区大小: %d字节, 页大小: %d字节\r\n",
           BLOCK_SIZE, SECTOR_SIZE, PAGE_SIZE);

    // 初始化SPI Flash
    FLASH_Port_Init();
    printf("SPI Flash接口初始化完成\r\n");

    // 检测Flash芯片
    FLASH_IC_Check();
    if(Flash_ID == W25Q64_FLASH_ID1 || Flash_ID == W25Q64_FLASH_ID2) {
        printf("检测到W25Q64 Flash, ID: 0x%08X\r\n", Flash_ID);
    } else {
        printf("未检测到W25Q64, ID: 0x%08X, 测试终止!\r\n", Flash_ID);
        while(1);
    }

    // 擦除整块区域并完整打印
    printf("\r\n===== 步骤1: 擦除整块区域(0x%06X - 0x%06X) =====\r\n",
           TEST_ADDR, TEST_ADDR + BLOCK_SIZE - 1);
    for(i = 0; i < BLOCK_SIZE / SECTOR_SIZE; i++) {
        printf("擦除扇区 %d (地址:0x%06X)...\r\n",
               i, TEST_ADDR + i * SECTOR_SIZE);
        FLASH_Erase_Sector(TEST_ADDR + i * SECTOR_SIZE);
    }

    // 读取整块数据并完整打印
        FLASH_Read(block_buf, TEST_ADDR, BLOCK_SIZE);
        PrintBuffer(block_buf, BLOCK_SIZE, "整块擦除后的数据");

        // 写入整块数据并完整打印
        printf("\r\n===== 步骤2: 写入整块数据 =====\r\n");
        // 填充块数据(前半部分用BLOCK_FILL_CHAR,后半部分用递增数据便于区分)
        for(i = 0; i < BLOCK_SIZE; i++) {
            block_buf[i] = (i < BLOCK_SIZE/2) ? BLOCK_FILL_CHAR : (i % 0xFF);
        }
        FLASH_Write(block_buf, TEST_ADDR, BLOCK_SIZE);

        // 读取整块数据并完整打印(验证写入效果)
        FLASH_Read(block_buf, TEST_ADDR, BLOCK_SIZE);
        PrintBuffer(block_buf, BLOCK_SIZE, "整块写入后的数据");

        // 擦除整块中的1个扇区并打印整块(观察变化)
        printf("\r\n===== 步骤3: 擦除整块中的第1个扇区(0x%06X - 0x%06X) =====\r\n",
               TEST_ADDR, TEST_ADDR + SECTOR_SIZE - 1);
        FLASH_Erase_Sector(TEST_ADDR);

        // 读取整块数据(第1个扇区应为0xFF,第2个扇区保持不变)
        FLASH_Read(block_buf, TEST_ADDR, BLOCK_SIZE);
        PrintBuffer(block_buf, BLOCK_SIZE, "擦除1个扇区后的整块数据");

        // 写入被擦除的扇区并打印整块
        printf("\r\n===== 步骤4: 写入被擦除的扇区 =====\r\n");
        // 填充扇区数据(用SECTOR_FILL_CHAR)
        for(i = 0; i < SECTOR_SIZE; i++) {
            sector_buf[i] = SECTOR_FILL_CHAR;
        }
        FLASH_Write(sector_buf, TEST_ADDR, SECTOR_SIZE);

        // 读取整块数据(验证扇区写入效果)
        FLASH_Read(block_buf, TEST_ADDR, BLOCK_SIZE);
        PrintBuffer(block_buf, BLOCK_SIZE, "写入扇区后的整块数据");

        // 擦除该扇区中的1页并打印整个扇区(观察变化)
        printf("\r\n===== 步骤5: 擦除扇区中的第1页(0x%06X - 0x%06X) =====\r\n",
               TEST_ADDR, TEST_ADDR + PAGE_SIZE - 1);
        // 备份扇区内非目标页数据
        FLASH_Read(sector_buf, TEST_ADDR, SECTOR_SIZE);
        // 擦除整个扇区
        FLASH_Erase_Sector(TEST_ADDR);
        // 恢复非目标页数据(只留第1页为0xFF)
        FLASH_Write(sector_buf + PAGE_SIZE, TEST_ADDR + PAGE_SIZE, SECTOR_SIZE - PAGE_SIZE);

        // 读取整个扇区数据(第1页应为0xFF,其他页保持)
        FLASH_Read(sector_buf, TEST_ADDR, SECTOR_SIZE);
        PrintBuffer(sector_buf, SECTOR_SIZE, "擦除1页后的扇区数据");

        // 写入被擦除的页并打印整个扇区
        printf("\r\n===== 步骤6: 写入被擦除的页 =====\r\n");
        // 填充页数据(用PAGE_FILL_CHAR)
        for(i = 0; i < PAGE_SIZE; i++) {
            page_buf[i] = PAGE_FILL_CHAR;
        }
        FLASH_Write(page_buf, TEST_ADDR, PAGE_SIZE);

        // 读取整个扇区数据(验证页写入效果)
        FLASH_Read(sector_buf, TEST_ADDR, SECTOR_SIZE);
        PrintBuffer(sector_buf, SECTOR_SIZE, "写入页后的扇区数据");

        // 最终验证
        FLASH_Read(page_buf, TEST_ADDR, PAGE_SIZE);
        for(i = 0; i < PAGE_SIZE; i++) {
            if(page_buf[i] != PAGE_FILL_CHAR) {
                verify_ok = 0;
                break;
            }
        }

        if(verify_ok) {
            printf("\r\n******* 所有测试通过 *******\r\n");
        } else {
            printf("\r\n******* 测试失败 *******\r\n");
        }

        while(1);
    }

  通过串口观察擦除——>写入——>读取的过程

image-20250926141759636

image-20250926141822537

image-20250926141852202

image-20250926141911311

image-20250926141940201

image-20250926141950928

关键注意事项

1. CS 引脚控制:所有操作(读 / 写 / 擦除 / 读 ID)均需先拉低 CS 选中芯片,操作完成后拉高 CS 释放,否则指令无效;
2. 写使能:擦除和写入前必须发送CMD_FLASH_WREN,否则操作被忽略;
3. 忙等待:擦除和写入是耗时操作,必须通过状态寄存器的忙标志位等待完成,避免后续操作冲突;
4. 地址对齐:扇区擦除地址需对齐扇区起始(address % 4096 == 0),页写入地址需注意不跨页(或通过W25XXX_WR_Block()自动处理)。

 

posted on 2025-09-26 14:45  凡仕  阅读(88)  评论(0)    收藏  举报