1. SPI协议概述

1.1 SPI协议定义

SPI(Serial Peripheral Interface)是一种同步串行通信接口,主要用于微控制器和各种外围器件(如传感器、存储器、无线模块等)之间的短距离通信。SPI具有以下特点:

  • 全双工通信(同时发送和接收)
  • 主从架构(一个主设备控制多个从设备)
  • 高速数据传输(可达数十MHz)
  • 灵活的时钟极性和相位配置

1.2 通信原理

SPI使用4条信号线:

  • SCLK(Serial Clock):时钟信号,由主设备产生
  • MOSI(Master Out Slave In):主设备数据输出,从设备数据输入
  • MISO(Master In Slave Out):主设备数据输入,从设备数据输出
  • CS/SS(Chip Select/Slave Select):片选信号,用于选择要通信的从设备

1.3 工作模式

SPI有四种工作模式,由时钟极性(CPOL)和时钟相位(CPHA)决定:

模式 CPOL CPHA 说明
0 0 0 时钟空闲为低,第一个边沿采样
1 0 1 时钟空闲为低,第二个边沿采样
2 1 0 时钟空闲为高,第一个边沿采样
3 1 1 时钟空闲为高,第二个边沿采样

2. W25Q128 Flash概述

2.1 产品特点

W25Q128是一款128M-bit串行Flash存储器,具有以下特点:

  • 存储容量:128M-bit(16MB)
  • 支持标准SPI、双线SPI和四线SPI通信
  • 时钟频率最高可达104MHz
  • 支持扇区擦除(4KB)和块擦除(64KB)
  • 页编程大小:256字节
  • 擦除/编程周期:100,000次
  • 数据保持时间:20年

2.2 存储特性

存储器组织结构:

  • 总容量:16MB(128Mbit)
  • 块大小:64KB
  • 扇区大小:4KB
  • 页大小:256字节

2.3 命令系统

主要指令集:

#define FLASH_WriteEnable        0x06    /* 写使能 */
#define FLASH_ReadStatusReg1     0x05    /* 读状态寄存器1 */
#define FLASH_ReadData          0x03    /* 读数据 */
#define FLASH_PageProgram       0x02    /* 页编程 */
#define FLASH_SectorErase       0x20    /* 扇区擦除 */
#define FLASH_ChipErase         0xC7    /* 芯片擦除 */

3. 硬件接口设计

3.1 引脚定义

在GD32F427ZGT6开发板上的连接:

/* SPI引脚定义 */
#define NORFLASH_CS_PIN     GPIO_PIN_14
#define NORFLASH_CS_PORT    GPIOB
#define NORFLASH_SCK_PIN    GPIO_PIN_3
#define NORFLASH_MISO_PIN   GPIO_PIN_4
#define NORFLASH_MOSI_PIN   GPIO_PIN_5

3.2 时序要求

当前实现的时序配置:

spi_init_struct.clock_polarity_phase = SPI_CK_PL_HIGH_PH_2EDGE;    /* 时钟空闲为高,第二个边沿采样 */
spi_init_struct.prescale = SPI_PSC_256;                /* 默认使用256分频,速度较慢 */

4. 软件实现

4.1 SPI驱动

SPI初始化配置:

void spi0_init(void)
{
    spi_parameter_struct spi_init_struct;
    
    /* GPIO配置 */
    gpio_af_set(GPIOB, GPIO_AF_5, GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5);
    gpio_mode_set(GPIOB, GPIO_MODE_AF, GPIO_PUPD_PULLUP, 
                 GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5);
    
    /* SPI参数配置 */
    spi_init_struct.trans_mode = SPI_TRANSMODE_FULLDUPLEX;   /* 全双工模式 */
    spi_init_struct.device_mode = SPI_MASTER;                /* 主机模式 */
    spi_init_struct.frame_size = SPI_FRAMESIZE_8BIT;        /* 8位数据帧 */
    spi_init_struct.nss = SPI_NSS_SOFT;                     /* 软件控制片选 */
    spi_init_struct.endian = SPI_ENDIAN_MSB;                /* MSB优先 */
    
    spi_init(SPI0, &spi_init_struct);
    spi_enable(SPI0);
}

4.2 W25Q128详细操作函数

4.2.1 基础操作函数

1. 初始化函数

void norflash_init(void)
{
    uint8_t temp;

    rcu_periph_clock_enable(RCU_GPIOB);    /* NORFLASH CS脚 时钟使能 */

    /* 设置CS引脚PB14 推挽输出 */
    gpio_mode_set(GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, GPIO_PIN_14);
    gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_MAX, GPIO_PIN_14);

    NORFLASH_CS(1);                     /* F_CS拉高,取消片选 */

    spi0_init();                        /* 初始化SPI0 */
    spi0_set_speed(SPI_SPEED_4);        /* SPI0 切换到高速状态 25Mhz */
    
    g_norflash_type = norflash_read_id();   /* 读取FLASH ID. */
    
    if (g_norflash_type == W25Q256)     /* SPI FLASH为W25Q256, 必须使能4字节地址模式 */
    {
        temp = norflash_read_sr(3);     /* 读取状态寄存器3,判断地址模式 */

        if ((temp & 0X01) == 0)         /* 如果不是4字节地址模式,则进入4字节地址模式 */
        {
            norflash_write_enable();    /* 写使能 */
            temp |= 1 << 1;             /* ADP=1, 上电4位地址模式 */
            norflash_write_sr(3, temp); /* 写SR3 */
            
            NORFLASH_CS(0);
            spi0_read_write_byte(FLASH_Enable4ByteAddr);    /* 使能4字节地址指令 */
            NORFLASH_CS(1);
        }
    }

    //printf("ID:%x\r\n", g_norflash_type);

}

  • 功能:初始化SPI NOR FLASH,配置GPIO,设置SPI参数,识别Flash型号
  • 参数:无
  • 返回值:无
  • 说明
    • 配置CS引脚为推挽输出
    • 初始化SPI接口
    • 读取Flash ID识别型号
    • 对于W25Q256型号,需要特殊处理4字节地址模式

2. 等待空闲函数

static void norflash_wait_busy(void)
{
    while ((norflash_read_sr(1) & 0x01) == 0x01);   /*  等待BUSY位清空 */
}
  • 功能:等待Flash处于空闲状态
  • 参数:无
  • 返回值:无
  • 说明:通过读取状态寄存器1的BUSY位判断Flash是否处于忙状态

3. 写使能函数

void norflash_write_enable(void)
{
    NORFLASH_CS(0);
    spi0_read_write_byte(FLASH_WriteEnable);   /* 发送写使能 */
    NORFLASH_CS(1);
}
  • 功能:使能Flash写操作
  • 参数:无
  • 返回值:无
  • 说明:在进行写入、擦除等操作前必须先调用此函数

4. 发送地址函数

static void norflash_send_address(uint32_t address)
{
    if (g_norflash_type == W25Q256) /*  只有W25Q256支持4字节地址模式 */
    {
        spi0_read_write_byte((uint8_t)((address)>>24)); /* 发送 bit31 ~ bit24 地址 */
    } 
    spi0_read_write_byte((uint8_t)((address)>>16));     /* 发送 bit23 ~ bit16 地址 */
    spi0_read_write_byte((uint8_t)((address)>>8));      /* 发送 bit15 ~ bit8  地址 */
    spi0_read_write_byte((uint8_t)address);             /* 发送 bit7  ~ bit0  地址 */
}
  • 功能:向Flash发送地址
  • 参数
    • address:要发送的地址(最大32bit)
  • 返回值:无
  • 说明:根据Flash型号发送24位或32位地址
4.2.2 状态寄存器操作

1. 读状态寄存器

uint8_t norflash_read_sr(uint8_t regno)
{
    uint8_t byte = 0, command = 0;

    switch (regno)
    {
        case 1:
            command = FLASH_ReadStatusReg1;  /* 读状态寄存器1指令 */
            break;

        case 2:
            command = FLASH_ReadStatusReg2;  /* 读状态寄存器2指令 */
            break;

        case 3:
            command = FLASH_ReadStatusReg3;  /* 读状态寄存器3指令 */
            break;

        default:
            command = FLASH_ReadStatusReg1;
            break;
    }

    NORFLASH_CS(0);
    spi0_read_write_byte(command);      /* 发送读寄存器命令 */
    byte = spi0_read_write_byte(0Xff);  /* 读取一个字节 */
    NORFLASH_CS(1);
    
    return byte;
}
  • 功能:读取Flash状态寄存器
  • 参数
    • regno:状态寄存器号(1~3)
  • 返回值:状态寄存器值
  • 说明
    • 状态寄存器1:包含BUSY、WEL、写保护等标志
    • 状态寄存器2:包含QE(四线使能)等标志
    • 状态寄存器3:包含地址模式等标志

2. 写状态寄存器

void norflash_write_sr(uint8_t regno, uint8_t sr)
{
    uint8_t command = 0;

    switch (regno)
    {
        case 1:
            command = FLASH_WriteStatusReg1;  /* 写状态寄存器1指令 */
            break;

        case 2:
            command = FLASH_WriteStatusReg2;  /* 写状态寄存器2指令 */
            break;

        case 3:
            command = FLASH_WriteStatusReg3;  /* 写状态寄存器3指令 */
            break;

        default:
            command = FLASH_WriteStatusReg1;
            break;
    }

    NORFLASH_CS(0);
    spi0_read_write_byte(command);  /* 发送写状态寄存器命令 */
    spi0_read_write_byte(sr);       /* 写入一个字节 */
    NORFLASH_CS(1);
}
  • 功能:写Flash状态寄存器
  • 参数
    • regno:状态寄存器号(1~3)
    • sr:要写入的值
  • 返回值:无
  • 说明:用于设置写保护、QE位、地址模式等
4.2.3 ID与数据读取

1. 读取芯片ID

uint16_t norflash_read_id(void)
{
    uint16_t deviceid;

    NORFLASH_CS(0);
    spi0_read_write_byte(FLASH_ManufactDeviceID);   /* 发送读 ID 命令 */
    spi0_read_write_byte(0);    /* 写入一个字节 */
    spi0_read_write_byte(0);
    spi0_read_write_byte(0);
    deviceid = spi0_read_write_byte(0xFF) << 8;     /* 读取高8位字节 */
    deviceid |= spi0_read_write_byte(0xFF);         /* 读取低8位字节 */
    NORFLASH_CS(1);

   return deviceid;
}
  • 功能:读取Flash芯片ID
  • 参数:无
  • 返回值:Flash芯片ID
  • 说明:通过ID可以识别Flash型号,如W25Q128的ID为0xEF17

2. 读取数据

void norflash_read(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint16_t i;

    NORFLASH_CS(0);
    spi0_read_write_byte(FLASH_ReadData);       /* 发送读取命令 */
    norflash_send_address(addr);                /* 发送地址 */
    
    for(i=0;i<datalen;i++)
    {
        pbuf[i] = spi0_read_write_byte(0XFF);   /* 循环读取 */
    }
    
    NORFLASH_CS(1);
}

  • 功能:从Flash读取数据
  • 参数
    • pbuf:数据存储区指针
    • addr:起始地址
    • datalen:要读取的字节数(最大65535)
  • 返回值:无
  • 说明:使用标准读取命令(0x03)读取数据

4.3 读写操作

4.3.1 写入操作

1. 页写入函数

static void norflash_write_page(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint16_t i;

    norflash_write_enable();    /* 写使能 */

    NORFLASH_CS(0);
    spi0_read_write_byte(FLASH_PageProgram);    /* 发送写页命令 */
    norflash_send_address(addr);                /* 发送地址 */

    for(i=0;i<datalen;i++)
    {
        spi0_read_write_byte(pbuf[i]);          /* 循环读取 */
    }
    
    NORFLASH_CS(1);
    norflash_wait_busy();       /* 等待写入结束 */
}
  • 功能:在一页内写入数据
  • 参数
    • pbuf:数据存储区指针
    • addr:起始地址
    • datalen:要写入的字节数(最大256)
  • 返回值:无
  • 说明
    • 一次最多写入256字节
    • 写入前需要先调用写使能函数
    • 写入后需要等待操作完成

2. 无检验写入函数

static void norflash_write_nocheck(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint16_t pageremain;
    pageremain = 256 - addr % 256;  /* 单页剩余的字节数 */

    if (datalen <= pageremain)      /* 不大于256个字节 */
    {
        pageremain = datalen;
    }

    while (1)
    {
        /* 当写入字节比页内剩余地址还少的时候, 一次性写完
         * 当写入直接比页内剩余地址还多的时候, 先写完整个页内剩余地址, 然后根据剩余长度进行不同处理
         */
        norflash_write_page(pbuf, addr, pageremain);

        if (datalen == pageremain)   /* 写入结束了 */
        {
            break;
        }
        else     /* datalen > pageremain */
        {
            pbuf += pageremain;         /* pbuf指针地址偏移,前面已经写了pageremain字节 */
            addr += pageremain;         /* 写地址偏移,前面已经写了pageremain字节 */
            datalen -= pageremain;      /* 写入总长度减去已经写入了的字节数 */

            if (datalen > 256)          /* 剩余数据还大于一页,可以一次写一页 */
            {
                pageremain = 256;       /* 一次可以写入256个字节 */
            }
            else     /* 剩余数据小于一页,可以一次写完 */
            {
                pageremain = datalen;   /* 不够256个字节了 */
            }
        }
    }
}
  • 功能:无需检验直接写入数据
  • 参数
    • pbuf:数据存储区指针
    • addr:起始地址
    • datalen:要写入的字节数(最大65535)
  • 返回值:无
  • 说明
    • 必须确保写入区域已被擦除(全为0xFF)
    • 具有自动换页功能
    • 内部调用norflash_write_page实现

3. 带擦除的写入函数

void norflash_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint32_t secpos;
    uint16_t secoff;
    uint16_t secremain;
    uint16_t i;
    uint8_t *norflash_buf;

    norflash_buf = g_norflash_buf;
    secpos = addr / 4096;       /* 扇区地址 */
    secoff = addr % 4096;       /* 在扇区内的偏移 */
    secremain = 4096 - secoff;  /* 扇区剩余空间大小 */

    //printf("ad:%X,nb:%X\r\n", addr, datalen); /* 测试用 */
    if (datalen <= secremain)
    {
        secremain = datalen;    /* 不大于4096个字节 */
    }

    while (1)
    {
        norflash_read(norflash_buf, secpos * 4096, 4096);   /* 读出整个扇区的内容 */

        for (i = 0; i < secremain; i++)   /* 校验数据 */
        {
            if (norflash_buf[secoff + i] != 0XFF)
            {
                break;      /* 需要擦除, 直接退出for循环 */
            }
        }

        if (i < secremain)   /* 需要擦除 */
        {
            norflash_erase_sector(secpos);  /* 擦除这个扇区 */

            for (i = 0; i < secremain; i++)   /* 复制 */
            {
                norflash_buf[i + secoff] = pbuf[i];
            }

            norflash_write_nocheck(norflash_buf, secpos * 4096, 4096);  /* 写入整个扇区 */
        }
        else        /* 写已经擦除了的,直接写入扇区剩余区间. */
        {
            norflash_write_nocheck(pbuf, addr, secremain);  /* 直接写扇区 */
        }

        if (datalen == secremain)
        {
            break;  /* 写入结束了 */
        }
        else        /* 写入未结束 */
        {
            secpos++;               /* 扇区地址增1 */
            secoff = 0;             /* 偏移位置为0 */

            pbuf += secremain;      /* 指针偏移 */
            addr += secremain;      /* 写地址偏移 */
            datalen -= secremain;   /* 字节数递减 */

            if (datalen > 4096)
            {
                secremain = 4096;   /* 下一个扇区还是写不完 */
            }
            else
            {
                secremain = datalen;/* 下一个扇区可以写完了 */
            }
        }
    }
}
  • 功能:写入数据(带擦除操作)
  • 参数
    • pbuf:数据存储区指针
    • addr:起始地址
    • datalen:要写入的字节数(最大65535)
  • 返回值:无
  • 说明
    • 写入前会检查目标区域是否需要擦除
    • 如需擦除,会先读取整个扇区,修改后再写回
    • 如不需擦除,直接调用norflash_write_nocheck写入

写入操作流程:

  1. 扇区检查:检查目标地址所在扇区是否需要擦除
  2. 扇区擦除:如果需要,执行扇区擦除操作
  3. 页编程:按页(256字节)写入数据
  4. 等待完成:等待写入操作完成
void norflash_write(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint32_t secpos = addr / 4096;       /* 扇区地址 */
    uint16_t secoff = addr % 4096;       /* 扇区内偏移 */
    uint16_t secremain = 4096 - secoff;  /* 扇区剩余空间 */
    uint16_t i;
    
    if (datalen <= secremain) secremain = datalen;
    
    while(1) 
    {
        /* 读取整个扇区 */
        norflash_read(g_norflash_buf, secpos * 4096, 4096);
        
        /* 检查是否需要擦除 */
        for(i = 0; i < secremain; i++)
        {
            if(g_norflash_buf[secoff + i] != 0XFF) break;
        }
        
        /* 需要擦除的情况 */
        if(i < secremain)
        {
            norflash_erase_sector(secpos);  /* 擦除这个扇区 */
            
            /* 复制数据到扇区缓存 */
            for(i = 0; i < secremain; i++)
            {
                g_norflash_buf[secoff + i] = pbuf[i];
            }
            
            /* 写入整个扇区 */
            norflash_write_nocheck(g_norflash_buf, secpos * 4096, 4096);
        }
        else    /* 不需要擦除,直接写入 */
        {
            norflash_write_nocheck(pbuf, addr, secremain);
        }
        
        /* 处理写入结束或继续写入下一个扇区 */
        if(datalen == secremain) break;  /* 写入结束 */
        else  /* 继续写入下一个扇区 */
        {
            secpos++;               /* 扇区地址增1 */
            secoff = 0;             /* 偏移位置为0 */
            pbuf += secremain;      /* 指针偏移 */
            addr += secremain;      /* 写地址偏移 */
            datalen -= secremain;   /* 字节数递减 */
            
            /* 计算下一个扇区需要写入的字节数 */
            if(datalen > 4096) secremain = 4096;
            else secremain = datalen;
        }
    }
}
4.3.2 擦除操作

1. 扇区擦除函数

void norflash_erase_sector(uint32_t saddr)
{
    //printf("fe:%x\r\n", saddr);   /* 监视flash擦除情况,测试用 */
    saddr *= 4096;
    norflash_write_enable();        /* 写使能 */
    norflash_wait_busy();           /* 等待空闲 */

    NORFLASH_CS(0);
    spi0_read_write_byte(FLASH_SectorErase);    /* 发送扇区擦除指令 */
    norflash_send_address(saddr);   /* 发送地址 */
    NORFLASH_CS(1);
    norflash_wait_busy();           /* 等待扇区擦除完成 */
}
  • 功能:擦除一个扇区
  • 参数
    • saddr:扇区地址(注意是扇区号,不是字节地址)
  • 返回值:无
  • 说明
    • 擦除一个4KB扇区
    • 擦除时间约150ms
    • 擦除后扇区内容全为0xFF

2. 芯片擦除函数

void norflash_erase_chip(void)
{
    norflash_write_enable();    /* 写使能 */
    norflash_wait_busy();       /* 等待空闲 */
    NORFLASH_CS(0);
    spi0_read_write_byte(FLASH_ChipErase);  /* 发送芯片擦除命令 */ 
    NORFLASH_CS(1);
    norflash_wait_busy();       /* 等待芯片擦除结束 */
}
  • 功能:擦除整个芯片
  • 参数:无
  • 返回值:无
  • 说明
    • 擦除整个芯片内容
    • 擦除时间非常长(数十秒)
    • 擦除后所有内容全为0xFF

5. 性能优化

5.1 当前实现分析

当前实现存在以下可优化点:

  1. SPI速度配置

    • 默认使用256分频,速度较慢
    • W25Q128支持最高104MHz时钟频率
  2. 数据传输效率

    • 每次只传输一个字节
    • 未使用DMA传输
    • 未使用双线/四线模式
  3. 写入操作

    • 每次写入都要检查整个扇区
    • 小数据量写入也需要读取整个扇区

5.2 优化建议

  1. 提高SPI时钟频率
/* 根据实际情况选择合适的分频系数 */
void spi0_set_speed(uint8_t speed)
{
    /* 建议从高速开始尝试,如果不稳定再降低速度 */
    spi0_set_speed(SPI_SPEED_2);    /* 50MHz */
}
  1. 使用DMA传输
/* 添加DMA传输支持 */
void norflash_read_dma(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    /* 配置DMA传输 */
    dma_config();
    
    /* 启动传输 */
    NORFLASH_CS(0);
    spi0_read_write_byte(FLASH_ReadData);
    norflash_send_address(addr);
    
    /* 使用DMA接收数据 */
    dma_transfer(pbuf, datalen);
    
    NORFLASH_CS(1);
}
  1. 实现双线/四线模式
/* 添加快速读取命令支持 */
#define FLASH_FastRead          0x0B    /* 快速读取 */
#define FLASH_FastReadDual      0x3B    /* 双线快速读取 */
#define FLASH_FastReadQuad      0x6B    /* 四线快速读取 */
  1. 写入优化
/* 添加缓存机制 */
#define SECTOR_CACHE_SIZE    4   /* 缓存4个扇区 */
struct sector_cache {
    uint32_t addr;              /* 扇区地址 */
    uint8_t data[4096];        /* 扇区数据 */
    uint8_t dirty;             /* 是否被修改 */
};
struct sector_cache sector_caches[SECTOR_CACHE_SIZE];
  1. 批量操作优化
/* 添加批量擦除支持 */
void norflash_erase_block(uint32_t block_addr)
{
    /* 64KB块擦除,比多次执行4KB扇区擦除更快 */
    norflash_write_enable();
    NORFLASH_CS(0);
    spi0_read_write_byte(FLASH_BlockErase);
    norflash_send_address(block_addr);
    NORFLASH_CS(1);
    norflash_wait_busy();
}
  1. 状态检查优化
/* 使用轮询替代忙等待 */
void norflash_wait_busy_with_timeout(uint32_t timeout)
{
    uint32_t start = get_systick();
    while ((norflash_read_sr(1) & 0x01) == 0x01)
    {
        if (get_systick() - start > timeout)
        {
            /* 超时处理 */
            break;
        }
        /* 可以添加延时或让出CPU */
        delay_us(100);
    }
}

这些优化建议可以根据实际应用场景选择性实施。在实施优化时需要注意:

  1. 可靠性测试:提高SPI速度后需要充分测试系统稳定性
  2. 资源平衡:DMA和缓存机制会占用更多系统资源
  3. 兼容性:部分优化可能需要修改上层应用代码
  4. 功耗考虑:更高的时钟频率会增加功耗

建议根据实际应用需求,选择合适的优化方案:

  • 对于追求性能的应用:实现DMA传输和多线模式
  • 对于小容量随机写入:实现扇区缓存机制
  • 对于大块数据传输:使用块擦除和批量传输
  • 对于低功耗要求:保持较低的时钟频率,实现休眠机制
Logo

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

更多推荐