一、SPI原理
SPI 通讯是有主从模式,所以对于主从两个设备来说,通信时钟(SCLK)必须要保持一致,所以引入时钟极性和时钟相位的概念。
所谓时钟极性和时钟相位所指的就是 SCLK 的特性,通过设置这两个值保证主从设备时钟的特性一致,这样才能保证 SPI 能够正常通信。
CPHA:时钟相位。表示 SCLK 的边沿,当 CPHA=0,表示第一个边沿,CPHA=1,表示第二个边沿,看不懂可以理解为CPHA=0就是上升沿(0到1的跳变),CPHA=1以此类推。(如果空闲时钟信号为高电平CPHA=0就是下降沿(1到0的跳变),CPHA=1以此类推)
CPOL:时钟极性。表示 SCLK 在空闲时段(IDLE)是是低电平。当 CPOL=0,idle 是低电平,CPOL=1,idle 是高电平,说白了就是高电平采样(0)还是低电平采样(1)。
所以就能够组成四种模式,有这个概念四种模式原理也很容易推理出来,这边举两个例子说明一下:
模式0(CPHA=0,CPOL=0):空闲时为低电平,上升沿(高电平)采样,低电平改变数据,这个模式可以说就是和I2C一样了(如果是用这个模式应该可以用I2C通讯)。
模式3(CPHA=1,CPOL=1):空闲时为高电平,上升沿(高电平)采样,低电平改变数据。
二、硬件连接(W25Q80 Flash)
W25Q80 引脚 | 连接至 G0001 |
---|---|
VCC | 3.3V |
GND | GND |
CS | PA15(软件控制) |
CLK | PA5 |
DO (MISO) | PA6 |
DI (MOSI) | PA7 |
WP/HOLD | 接 VCC(禁用写保护/挂起) |
三、SPI 初始化详解
1. 使能时钟
RCC_AHBPeriphClockCmd(RCC_AHBENR_GPIOA, ENABLE); // GPIOA
RCC_APB1PeriphClockCmd(RCC_APB1PERIPH_SPI1, ENABLE); // SPI1(MM32G0 中 SPI1 在 APB1)
2. 配置 GPIO 为复用功能
// SCK(PA5)、MOSI(PA7)、NSS(PA15) → 复用推挽输出
GPIO_Init_structure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_Init_structure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7 | GPIO_Pin_15;
GPIO_Init(GPIOA, &GPIO_Init_structure);
// MISO(PA6) → 上拉输入(或浮空输入)
GPIO_Init_structure.GPIO_Mode = GPIO_Mode_IPU; // 内部上拉
GPIO_Init_structure.GPIO_Pin = GPIO_Pin_6;
GPIO_Init(GPIOA, &GPIO_Init_structure);
// 配置复用功能为 AF0(SPI1)
GPIO_PinAFConfig(GPIOA, GPIO_PinSource5, GPIO_AF_0);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource6, GPIO_AF_0);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource7, GPIO_AF_0);
GPIO_PinAFConfig(GPIOA, GPIO_PinSource15, GPIO_AF_0);
3. 配置 SPI 参数(模式 0)
CPOL=0, CPHA=0
SPI_InitTypeDef SPI_Init_structure;
SPI_Init_structure.SPI_Mode = SPI_Mode_Master;
SPI_Init_structure.SPI_NSS = SPI_NSS_Soft; // 软件控制 CS
SPI_Init_structure.SPI_BaudRatePrescaler = SPI_BaudratePrescaler_2; // 最高速(系统时钟/2)
SPI_Init_structure.SPI_CPOL = SPI_CPOL_Low; // 时钟空闲低电平
SPI_Init_structure.SPI_CPHA = SPI_CPHA_1Edge; // 数据在第一个边沿采样
SPI_Init_structure.SPI_DataSize = SPI_DataSize_8b;
SPI_Init_structure.SPI_FirstBit = SPI_FirstBit_MSB;
SPI_Init(SPI1, &SPI_Init_structure);
SPI_Cmd(SPI1, ENABLE);
四、SPI 数据交换函数
SPI通讯的核心原理就是交换,我们可以通过上面的函数直接就可以完成数据的接收与发送,如果你需要接收数据就可以通过一个变量去接收对应的返回值(因为SPI通讯的核心就是交换,要完成接收就得给一个东西去交换,建议用0xFF或0x00这两个数去换)
uint8_t SPI1_Exchang(uint8_t cmd)
{
while (SPI_GetFlagStatus(SPI1, SPI_FLAG_TXEPT) != SET); // 等待发送缓冲空
SPI_SendData(SPI1, cmd);
while (SPI_GetFlagStatus(SPI1, SPI_FLAG_RXAVL) != SET); // 等待接收数据可用
return SPI_ReceiveData(SPI1);
}
此函数是与 W25Q 通信的核心!
五、W25Q Flash 的存储结构与基本操作单位
在使用 W25Q 系列 SPI Flash(如 W25Q64、W25Q128 等)时,必须清楚其物理存储结构和操作限制,否则极易出现“写入失败”“数据异常”等问题。
W25Q Flash 的存储组织遵循典型的 “块(Block)→ 扇区(Sector)→ 页(Page)” 三级结构,且 擦除、写入、读取的操作单位各不相同:
操作类型 | 最小单位 | 典型大小 | 说明 |
---|---|---|---|
读取(Read) | 字节(Byte) | 1 字节 | 可从任意地址开始,连续读取任意长度(受限于地址空间) |
写入(Program) | 页(Page) | 256 字节 | 一次最多写入 256 字节,且只能将 1 写为 0,不能反向 |
擦除(Erase) | 扇区(Sector) / 块(Block) | 扇区:4KB 块:32KB / 64KB | 擦除后所有位变为 1(即 0xFF),这是写入的前提 |
为什么必须“先擦除再写入”?
Flash 的物理特性决定了:
- 出厂或擦除后:每个 bit 为 1(即字节值为 0xFF)
- 写入操作:只能将 1 → 0,不能将 0 → 1
- 因此,若某字节已写为
0x55
(二进制01010101
),你无法直接将其改为0xAA
(10101010
),因为部分 bit 需要从 0 变回 1 —— 这是不允许的!
六、主函数测试:写入 0~255,再读回验证
int main(void)
{
uint8_t write_buf[256], read_buf[256];
GPIO_init(); // LED 控制
uart_init(); // 调试输出
spi_init(); // SPI 初始化
// 填充测试数据
for (int i = 0; i < 256; i++) write_buf[i] = i;
// 擦除扇区(地址 0)
W25QX_Sector_Erase(0);
// 写入一页(256 字节)
W25Q_WriteData_Page(write_buf, 0, 256);
// 读回验证
W25Q_ReadData(read_buf, 0, 256);
for (int i = 0; i < 256; i++) {
printf("%d ", read_buf[i]); // 输出 0 1 2 ... 255
}
// 再次擦除后读取(应全为 0xFF)
W25QX_Sector_Erase(0);
W25Q_ReadData(read_buf, 0, 256);
printf("\nAfter erase:\n");
for (int i = 0; i < 256; i++) {
printf("%d ", read_buf[i]);
}
while (1);
}
七、结果(这一次视频有两个)
1、读写ID
2、读写数据

