目录
SPI(高位先行)
1:什么是spi
2:核心特点
3. 四根线
4. 工作原理
5.四种工作模式
模式0:
模式1:
模式2:
模式3:
6.优缺点
7.常见应用场景
8.与UART和I2C的区别
W25Q64芯片介绍
1:内存分块
2:写入操作
3:读取操作
SPI协议配置
1:硬件引脚电平变化层
①:SPI写CS引脚电平
②:SPI写SCK引脚电平
③:SPI写MOSI引脚电平
④:SPI读MISO引脚电平
2:初始化配置
3:协议层
①:起始信号
②:停止信号
③:交换数据
软件SPI读取W25Q64
1:W25Q64写使能
2:W25Q64等待忙
3:W25Q64页编程
4:W25Q64扇区擦除
5:读取厂商ID和设备ID
6:W25Q64读取数据
硬件SPI读取W25Q64
模式选择:
1:初始化
2:交换数据
SPI(高位先行)
1:什么是spi
SPI(Serial Peripheral Interface,串行外设接口)是一种高速、全双工、同步时钟的串行通信总线,由摩托罗拉公司最早提出。它是目前微控制器(MCU)连接外部芯片最常用的方式之一,广泛应用于嵌入式系统中,简单来说,它就像是微控制器(主机)与各种外设(从机)之间的一条“高速公路”,用于传输数据。
2:核心特点
全双工通信:数据可以同时发送和接收(两根数据线)。
高速:比 I2C、UART 快得多,通常可达几十 MHz,甚至上百 MHz。
同步传输:有时钟线(SCK)统一指挥,收发双方步调一致。
主从架构:一个主机(通常是MCU)控制多个从机。
硬件连接简单直接:相比 UART 不需要复杂的波特率匹配,相比 I2C 不需要仲裁机制。
3. 四根线
标准的 SPI 总线包含 4 根信号线:
SCK (Serial Clock):时钟线。由主机产生,用来同步数据传输。
MOSI (Master Out Slave In):主发从收。数据从主机发送到从机。
MISO (Master In Slave Out):主收从发。数据从从机发送到主机。
CS/SS (Chip Select/Slave Select):片选线。由主机控制,拉低表示选中某个从机。每个从机都需要一根独立的 CS/SS/NSS 线。
4. 工作原理
选片:主机拉低对应从机的 CS 引脚,告诉它“我要和你说话了”。
发时钟:主机在 SCK 线上产生时钟脉冲。
交换数据:
主机通过 MOSI 线发送一位数据,同时从机通过 MISO 线返回一位数据。
每产生一个时钟脉冲,双方就交换一位数据。
因为是环形总线,发送和接收是同时进行的(全双工)。
结束:数据传输完成后,主机拉高 CS 引脚,释放从机。
5.四种工作模式
时钟极性 (CPOL) 和 时钟相位 (CPHA),这是 SPI 最容易让人困惑的地方。SPI 有 4种工作模式,由 CPOL 和 CPHA 决定。
重要提示:通信前,必须查阅从机芯片的数据手册(Datasheet),确认它支持哪种模式,否则无法通信。
模式0:
模式1:
模式2:
模式3:
6.优缺点
SPI 协议本身非常灵活,它没有硬性规定一次通信(即一帧数据)必须传输多少位(Bits)。
7.常见应用场景
凡是需要高速传输数据的外设,基本都用 SPI:
显示屏:OLED、TFT LCD 屏幕(刷图快)。
存储卡:SD 卡(部分模式)、Flash 存储芯片(W25Qxx)。
传感器:高精度 ADC/DAC、部分 IMU(惯性测量单元)传感器。
无线通信:LoRa 模块、NRF24L01 无线模块。
实时时钟 (RTC):部分时钟芯片。
8.与UART和I2C的区别
W25Q64芯片介绍
1:内存分块
最小擦除单位为扇(4KB),最小写入单位为页(256Byte),块区(64KB)
2:写入操作
3:读取操作
SPI协议配置
1:硬件引脚电平变化层
①:SPI写CS引脚电平
void MySPI_W_SS(uint8_t BitValue) { GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); //根据BitValue,设置SS引脚的电平 }②:SPI写SCK引脚电平
void MySPI_W_SCK(uint8_t BitValue) { GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue); //根据BitValue,设置SCK引脚的电平 }③:SPI写MOSI引脚电平
void MySPI_W_MOSI(uint8_t BitValue) { GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue); //根据BitValue,设置MOSI引脚的电平,BitValue要实现非0即1的特性 }④:SPI读MISO引脚电平
uint8_t MySPI_R_MISO(void) { return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6); //读取MISO电平并返回 }2:初始化配置
void MySPI_Init(void) { /*开启时钟*/ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟 /*GPIO初始化*/ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA4、PA5和PA7引脚初始化为推挽输出 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA6引脚初始化为上拉输入 /*设置默认电平*/ MySPI_W_SS(1); //SS默认高电平 MySPI_W_SCK(0); //SCK默认低电平 }3:协议层
①:起始信号
void MySPI_Start(void) { MySPI_W_SS(0); //拉低SS,开始时序 }②:停止信号
void MySPI_Stop(void) { MySPI_W_SS(1); //拉高SS,终止时序 }③:交换数据
uint8_t MySPI_SwapByte(uint8_t ByteSend) { uint8_t i, ByteReceive = 0x00; //定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到 for (i = 0; i < 8; i ++) //循环8次,依次交换每一位数据 { /*两个!可以对数据进行两次逻辑取反,作用是把非0值统一转换为1,即:!!(0) = 0,!!(非0) = 1*/ MySPI_W_MOSI(!!(ByteSend & (0x80 >> i))); //使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线 MySPI_W_SCK(1); //拉高SCK,上升沿移出数据 if (MySPI_R_MISO()) { ByteReceive |= (0x80 >> i);//当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0 } //读取MISO数据,并存储到Byte变量 MySPI_W_SCK(0); //拉低SCK,下降沿移入数据 } return ByteReceive; //返回接收到的一个字节数据 }软件SPI读取W25Q64
1:W25Q64写使能
void W25Q64_WriteEnable(void) { MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_WRITE_ENABLE); //交换发送写使能的指令 MySPI_Stop(); //SPI终止 }2:W25Q64等待忙
void W25Q64_WaitBusy(void) { uint32_t Timeout; MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1); //交换发送读状态寄存器1的指令 Timeout = 100000; //给定超时计数时间 while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01) //循环等待忙标志位 { Timeout --; //等待时,计数值自减 if (Timeout == 0) //自减到0后,等待超时 { /*超时的错误处理代码,可以添加到此处*/ break; //跳出等待,不等了 } } MySPI_Stop(); //SPI终止 }3:W25Q64页编程
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count) { uint16_t i; W25Q64_WriteEnable(); //写使能 MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_PAGE_PROGRAM); //交换发送页编程的指令 MySPI_SwapByte(Address >> 16); //交换发送地址23~16位 MySPI_SwapByte(Address >> 8); //交换发送地址15~8位 MySPI_SwapByte(Address); //交换发送地址7~0位 for (i = 0; i < Count; i ++) //循环Count次 { MySPI_SwapByte(DataArray[i]); //依次在起始地址后写入数据 } MySPI_Stop(); //SPI终止 W25Q64_WaitBusy(); //等待忙 }4:W25Q64扇区擦除
void W25Q64_SectorErase(uint32_t Address) { W25Q64_WriteEnable(); //写使能 MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB); //交换发送扇区擦除的指令 MySPI_SwapByte(Address >> 16); //交换发送地址23~16位 MySPI_SwapByte(Address >> 8); //交换发送地址15~8位 MySPI_SwapByte(Address); //交换发送地址7~0位 MySPI_Stop(); //SPI终止 W25Q64_WaitBusy(); //等待忙 }5:读取厂商ID和设备ID
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID) { MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_JEDEC_ID); //交换发送读取ID的指令 *MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收MID,通过输出参数返回 *DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交换接收DID高8位 *DID <<= 8; //高8位移到高位 *DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); //或上交换接收DID的低8位,通过输出参数返回 MySPI_Stop(); //SPI终止 }6:W25Q64读取数据
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count) { uint32_t i; MySPI_Start(); //SPI起始 MySPI_SwapByte(W25Q64_READ_DATA); //交换发送读取数据的指令 MySPI_SwapByte(Address >> 16); //交换发送地址23~16位 MySPI_SwapByte(Address >> 8); //交换发送地址15~8位 MySPI_SwapByte(Address); //交换发送地址7~0位 for (i = 0; i < Count; i ++) //循环Count次 { DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //依次在起始地址后读取数据 } MySPI_Stop(); //SPI终止 }硬件SPI读取W25Q64
模式选择:
连续传输:CS一直保持有效(低电平),一直时钟,一直收发。
非连续传输:每传一次数据,CS都会释放(拉高),一次通信结束。
区别在于:CS(片选)是否中途释放。
1:初始化
void MySPI_Init(void) { /*开启时钟*/ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); //开启SPI1的时钟 /*GPIO初始化*/ GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA4引脚初始化为推挽输出 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA5和PA7引脚初始化为复用推挽输出 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA6引脚初始化为上拉输入 /*SPI初始化*/ SPI_InitTypeDef SPI_InitStructure; //定义结构体变量 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //模式,选择为SPI主模式 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //方向,选择2线全双工 SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //数据宽度,选择为8位 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //先行位,选择高位先行 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; //波特率分频,选择128分频 SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //SPI极性,选择低极性 SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; //SPI相位,选择第一个时钟边沿采样,极性和相位决定选择SPI模式0 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS,选择由软件控制 SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC多项式,暂时用不到,给默认值7 SPI_Init(SPI1, &SPI_InitStructure); //将结构体变量交给SPI_Init,配置SPI1 /*SPI使能*/ SPI_Cmd(SPI1, ENABLE); //使能SPI1,开始运行 /*设置默认电平*/ MySPI_W_SS(1); //SS默认高电平 }2:交换数据
uint8_t MySPI_SwapByte(uint8_t ByteSend) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET); //轮询查询数据寄存器空,空了再写入数据让它发 SPI_I2S_SendData(SPI1, ByteSend); //写入数据到发送数据寄存器,开始产生时序 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET); //轮询查询接收数据寄存器非空,非空表示数据接受完全 return SPI_I2S_ReceiveData(SPI1); //读取接收到的数据并返回 }