SPI(串行外围设备接口)
SPI接口主要应用于EEPROM,FLASH,各种传感器,AD转换器。
高速的,全双工,同步的串行通信总线。
物理层;
1个SPI有四根线
SCK :时钟信号线,用于通讯数据同步,它由通讯主机(MCU)产生,决定了通讯的速率。
不同的设备支持最高的时钟频率不一样,如:STM32的SPI时钟频率最大位fpclk/2
MOSI :主设备输出/从设备输入引脚
MISO :主设备输入/从设备输出引脚
SS :片选线或者使能线,也称NSS或者CS
如果主机要连接多条从设备就需要多条片选线
协议层:
1.来一个时钟上升沿信号,主机和从机分别把自己高位的值左移出来(一般高位先行)
2.来一个时钟下降沿信号,主机和从机分别读入数据,存储到移位寄存器的地位。
经过8次同样的操作,就完成了一个字节的交换。
时钟的极性和相位
CPOL:Clock Polarity ,通信的整个过程分为空闲时刻和通信时刻
空闲状态SCK是低电平,CPOL-0;上升沿表示数据有效的开始
CPHA:Clock Phase ,就是时钟的相位
直接决定SPI总线从那个跳变沿开始采样数据
CPHA=0:表示从第一个跳变沿开始采样
CPHA=1:表示从第二个跳变沿开始采样
所以SPI有四种不同的工作模式
W25Q32:使用SPI通讯协议的NOR FLASH存储器
硬件电路设计:
SPI软件模拟实现:
(1)W25Q32此芯片只支持模式0和模式3工作模式
(2)写的时候先擦除,再写入
(3)移位是高位优先
写入操作注意事项:
1.写入操作前,必须使能
2.每个数据位只能由1改写为0,不能由0改写为1(先擦除,此芯片段擦除)
3.连续写入多字节时,最多写入一页数据,超过页尾位置的数据,会回到页首覆盖写入
4.写入操作结束后,芯片进入忙状态,不响应新的读写状态(写周期内不响应其他的读写操作)
读取操作注意事项:
1.直接调用读取时序,无需使能
2读取操作不会进入忙状态,但不能在忙状态时读取
读写指令:
1.Write Enable(06h)
2.Page Program(02h/块号/段号/页号/页内地址/DATA)
3.Read Data(03h/读取地址)
4.Setcor Erase(4KB~段擦除)
5.Read State Register(05h)
读写时序图------模式0

在SPI.h中编写
c
#ifndef __SPI_H
#define __SPI_H
#include "stm32f10x.h"
#include "delay.h"
// 宏定义,不同引脚输出高低电平
#define CS_HIGH (GPIOC->ODR |= GPIO_ODR_ODR13)
#define CS_LOW (GPIOC->ODR &= ~GPIO_ODR_ODR13)
#define SCK_HIGH (GPIOA->ODR |= GPIO_ODR_ODR5)
#define SCK_LOW (GPIOA->ODR &= ~GPIO_ODR_ODR5)
#define MOSI_HIGH (GPIOA->ODR |= GPIO_ODR_ODR7)
#define MOSI_LOW (GPIOA->ODR &= ~GPIO_ODR_ODR7)
// 读取MISO引脚信号
#define MISO_READ (GPIOA->IDR & GPIO_IDR_IDR6)
// SPI标准延迟时间
#define SPI_DELAY Delay_us(5)
// 初始化
void SPI_Init(void);
// 数据传输的开始和结束
void SPI_Start(void);
void SPI_Stop(void);
// 主从设备交换一个字节的数据
uint8_t SPI_SwapByte(uint8_t byte);
#endif
在SPI.c中编写
c
#include "spi.h"
// 初始化
void SPI_Init(void)
{
// 1. 开启时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
// 2. GPIO工作模式
// PA5、PA7、PC13:通用推挽输出,CNF = 00,MODE = 11
GPIOC->CRH |= GPIO_CRH_MODE13;
GPIOC->CRH &= ~GPIO_CRH_CNF13;
GPIOA->CRL |= GPIO_CRL_MODE5;
GPIOA->CRL &= ~GPIO_CRL_CNF5;
GPIOA->CRL |= GPIO_CRL_MODE7;
GPIOA->CRL &= ~GPIO_CRL_CNF7;
// PA6:MISO,浮空输入,CNF = 01,MODE = 00
GPIOA->CRL &= ~GPIO_CRL_MODE6;
GPIOA->CRL &= ~GPIO_CRL_CNF6_1;
GPIOA->CRL |= GPIO_CRL_CNF6_0;
// 3. 选择SPI的工作模式 0:SCK空闲0
SCK_LOW;
// 4. 片选不使能
CS_HIGH;
// 5. 延时
SPI_DELAY;
}
// 数据传输的开始和结束
void SPI_Start(void)
{
CS_LOW;
}
void SPI_Stop(void)
{
CS_HIGH;
}
// 主从设备交换一个字节的数据
uint8_t SPI_SwapByte(uint8_t byte)
{
// 定义变量保存接收到的字节
uint8_t rByte = 0x00;
// 用一个循环,依次交换8位数据
for (uint8_t i = 0; i < 8; i++)
{
// 1. 先准备要发送的数据(最高位),送到MOSI
//取最高位
if (byte & 0x80)
{
MOSI_HIGH;
}
else
{
MOSI_LOW;
}
// 左移一位
byte <<= 1;
// 2. 拉高时钟信号,形成一个上升沿
SCK_HIGH;
SPI_DELAY;
// 3. 在MISO上采样Flash传来的数据
// 先做左移
rByte <<= 1;
if (MISO_READ)
{
rByte |= 0x01;
}
// 4. 拉低时钟,为下次数据传输做准备
SCK_LOW;
SPI_DELAY;
}
return rByte;
}
FLASH接口函数
在W25Q32.h中编写
c
#ifndef __W25Q32_H
#define __W25Q32_H
#include "spi.h"
// 初始化
void W25Q32_Init(void);
// 读取ID
void W25Q32_ReadID(uint8_t * mid, uint16_t * did);
// 开启写使能
void W25Q32_WriteEnable(void);
// 关闭写使能
void W25Q32_WriteDisable(void);
// 等待状态不为忙(busy)
void W25Q32_WaitNotBusy(void);
// 擦除段(sector erase),地址只需要块号和段号
void W25Q32_EraseSector(uint8_t block, uint8_t sector);
// 写入(页写)
void W25Q32_PageWrite(uint8_t block, uint8_t sector, uint8_t page, uint8_t * data, uint16_t len);
// 读取
void W25Q32_Read(uint8_t block, uint8_t sector, uint8_t page, uint8_t innerAddr, uint8_t * buffer, uint16_t len);
#endif
在W25Q32.c中编写
W25Q32地址组成:
c
/*
* @Author: wushengran
* @Date: 2024-06-12 15:54:23
* @Description:
*
* Copyright (c) 2024 by atguigu, All Rights Reserved.
*/
#include "w25q32.h"
// 初始化
void W25Q32_Init(void)
{
SPI_Init();
}
// 读取ID
void W25Q32_ReadID(uint8_t *mid, uint16_t *did)
{
SPI_Start();
// 1. 发送指令 9fh
SPI_SwapByte(0x9f);
// 2. 获取制造商ID(为了读取数据,发送什么不重要)
*mid = SPI_SwapByte(0xff);
// 3. 获取设备ID
*did = 0;
*did |= SPI_SwapByte(0xff) << 8;
*did |= SPI_SwapByte(0xff) & 0xff;
SPI_Stop();
}
// 开启写使能
void W25Q32_WriteEnable(void)
{
SPI_Start();
SPI_SwapByte(0x06);
SPI_Stop();
}
// 关闭写使能
void W25Q32_WriteDisable(void)
{
SPI_Start();
SPI_SwapByte(0x04);
SPI_Stop();
}
// 等待状态不为忙(busy)
void W25Q32_WaitNotBusy(void)
{
SPI_Start();
// 发送读取状态寄存器指令
SPI_SwapByte(0x05);
// 等待收到的数据末位变成0
while (SPI_SwapByte(0xff) & 0x01)
{
}
SPI_Stop();
}
// 擦除段(sector erase),地址只需要块号和段号
void W25Q32_EraseSector(uint8_t block, uint8_t sector)
{
// 首先等待状态不为忙
W25Q32_WaitNotBusy();
// 开启写使能
W25Q32_WriteEnable();
// 计算要发送的地址(段首地址)
uint32_t addr = (block << 16) + (sector << 12);
SPI_Start();
// 发送指令
SPI_SwapByte(0x20);
SPI_SwapByte(addr >> 16 & 0xff); // 第一个字节
SPI_SwapByte(addr >> 8 & 0xff); // 第二个字节
SPI_SwapByte(addr >> 0 & 0xff); // 第三个字节
SPI_Stop();
W25Q32_WriteDisable();
}
// 写入(页写)
void W25Q32_PageWrite(uint8_t block, uint8_t sector, uint8_t page, uint8_t *data, uint16_t len)
{
// 首先等待状态不为忙
W25Q32_WaitNotBusy();
// 开启写使能
W25Q32_WriteEnable();
// 计算要发送的地址(页首地址)
uint32_t addr = (block << 16) + (sector << 12) + (page << 8);
SPI_Start();
// 发送指令
SPI_SwapByte(0x02);
// 发送24位地址
SPI_SwapByte(addr >> 16 & 0xff); // 第一个字节
SPI_SwapByte(addr >> 8 & 0xff); // 第二个字节
SPI_SwapByte(addr >> 0 & 0xff); // 第三个字节
// 依次发送数据
for (uint16_t i = 0; i < len; i++)
{
SPI_SwapByte(data[i]);
}
SPI_Stop();
W25Q32_WriteDisable();
}
// 读取
void W25Q32_Read(uint8_t block, uint8_t sector, uint8_t page, uint8_t innerAddr, uint8_t *buffer, uint16_t len)
{
// 首先等待状态不为忙
W25Q32_WaitNotBusy();
// 计算要发送的地址
uint32_t addr = (block << 16) + (sector << 12) + (page << 8) + innerAddr;
SPI_Start();
// 发送指令
SPI_SwapByte(0x03);
// 发送24位地址
SPI_SwapByte(addr >> 16 & 0xff); // 第一个字节
SPI_SwapByte(addr >> 8 & 0xff); // 第二个字节
SPI_SwapByte(addr >> 0 & 0xff); // 第三个字节
// 依次读取数据
for (uint16_t i = 0; i < len; i++)
{
buffer[i] = SPI_SwapByte(0xff);
}
SPI_Stop();
}
在main.c中编写
c
#include "usart.h"
#include "w25q32.h"
#include <string.h>
int main(void)
{
// 1. 初始化
USART_Init();
W25Q32_Init();
printf("尚硅谷SPI软件模拟实验开始...\n");
// 2. 读取ID进行测试
uint8_t mid = 0;
uint16_t did = 0;
W25Q32_ReadID(&mid, &did);
printf("mid = %#x, did = %#x\n", mid, did);
// 3. 段擦除
W25Q32_EraseSector(0, 0);
// 4. 页写
W25Q32_PageWrite(0, 0, 0, "12345678", 8);
// 5. 读取
uint8_t buffer[10] = {0};
W25Q32_Read(0, 0, 0, 2, buffer, 6);
printf("buffer = %s\n", buffer);
while (1)
{
}
}
STM32的SPI外设实现代码:
外设总线频率的1/2
SPI1------APB2 SPI2,SPI3------APB1
支持4中外设模式
数据长度8,16位,可以高位先行和低位先行。
支持双线全双工,单线双向,单线模式
波特率发生器:用于生成通信的同步时钟
NSS片选信号,可以硬件控制,也可以软件控制(主设备模式时使用)
SPI案例2:SPI外设读写FLASH

相关寄存器及相关位:
SPI_CR2
SPI_CR1_MSTR,配置工作模式(1主设备)
SPI_CR1_BR_2:0
SPI_CR1_CP0L
SPI_CR1_CPHA
SPI_CR1_DDF(数据帧格式设置)
SPI_CR1_LSBfirst(低位先行)
SPI_CR1_SSM(软件从设备管理置于1)
SPI_CR1_SSI(使NSS引脚作用无效)
SPI_CR1_SPE(启动SPI)
SP1_SR_TXE
SPI_SR_RXNE
SPI_DR
在SPI.h中编写
c
#ifndef __SPI_H
#define __SPI_H
#include "stm32f10x.h"
// 宏定义,不同引脚输出高低电平
#define CS_HIGH (GPIOC->ODR |= GPIO_ODR_ODR13)
#define CS_LOW (GPIOC->ODR &= ~GPIO_ODR_ODR13)
// 初始化
void SPI_Init(void);
// 数据传输的开始和结束
void SPI_Start(void);
void SPI_Stop(void);
// 主从设备交换一个字节的数据
uint8_t SPI_SwapByte(uint8_t byte);
#endif
SPI.c中编写
c
#include "spi.h"
// 初始化
void SPI_Init(void)
{
// 1. 开启时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
// 2. GPIO工作模式
// PC13:通用推挽输出,CNF = 00,MODE = 11
GPIOC->CRH |= GPIO_CRH_MODE13;
GPIOC->CRH &= ~GPIO_CRH_CNF13;
// PA5、PA7: 复用推挽输出,CNF = 10,MODE = 11
GPIOA->CRL |= GPIO_CRL_MODE5;
GPIOA->CRL |= GPIO_CRL_CNF5_1;
GPIOA->CRL &= ~GPIO_CRL_CNF5_0;
GPIOA->CRL |= GPIO_CRL_MODE7;
GPIOA->CRL |= GPIO_CRL_CNF7_1;
GPIOA->CRL &= ~GPIO_CRL_CNF7_0;
// PA6:MISO,浮空输入,CNF = 01,MODE = 00
GPIOA->CRL &= ~GPIO_CRL_MODE6;
GPIOA->CRL &= ~GPIO_CRL_CNF6_1;
GPIOA->CRL |= GPIO_CRL_CNF6_0;
// 3. SPI相关配置
// 3.1 配置SPI为主模式
SPI1->CR1 |= SPI_CR1_MSTR;
// 3.2 使用软件控制片选信号,直接拉高
SPI1->CR1 |= SPI_CR1_SSM;
SPI1->CR1 |= SPI_CR1_SSI;
// 3.3 配置工作模式0,时钟极性和相位
SPI1->CR1 &= ~SPI_CR1_CPOL;
SPI1->CR1 &= ~SPI_CR1_CPHA;
// 3.4 配置时钟分频系数,波特率选择:BR-001
SPI1->CR1 &= ~SPI_CR1_BR;
SPI1->CR1 |= SPI_CR1_BR_0;
// 3.5 设置数据帧格式
SPI1->CR1 &= ~SPI_CR1_DFF;
// 3.6 配置高位先行MSB
SPI1->CR1 &= ~SPI_CR1_LSBFIRST;
// 3.7 SPI模块使能
SPI1->CR1 |= SPI_CR1_SPE;
}
// 数据传输的开始和结束
void SPI_Start(void)
{
CS_LOW;
}
void SPI_Stop(void)
{
CS_HIGH;
}
// 主从设备交换一个字节的数据
uint8_t SPI_SwapByte(uint8_t byte)
{
// 1. 将要发送的数据byte写入发送缓冲区
// 1.1 等待发送缓冲区为空
while ((SPI1->SR & SPI_SR_TXE) == 0)
{}
// 1.2 将数据byte写入DR寄存器
SPI1->DR = byte;
// 2. 读取MISO发来的数据
// 2.1 等待接收缓冲区为非空
while ((SPI1->SR & SPI_SR_RXNE) == 0)
{}
// 2.1 从接收缓冲区读取数据,返回
return (uint8_t)(SPI1->DR & 0xff);
}
在FLASH中代码不改变
在main.c中编写(无变化)
c
/*
* @Author: wushengran
* @Date: 2024-05-23 15:14:48
* @Description:
*
* Copyright (c) 2024 by atguigu, All Rights Reserved.
*/
#include "usart.h"
#include "w25q32.h"
#include <string.h>
int main(void)
{
// 1. 初始化
USART_Init();
W25Q32_Init();
printf("尚硅谷SPI软件模拟实验开始...\n");
// 2. 读取ID进行测试
uint8_t mid = 0;
uint16_t did = 0;
W25Q32_ReadID(&mid, &did);
printf("mid = %#x, did = %#x\n", mid, did);
// 3. 段擦除
W25Q32_EraseSector(0, 0);
// 4. 页写
W25Q32_PageWrite(0, 0, 0, "12345678", 8);
// 5. 读取
uint8_t buffer[10] = {0};
W25Q32_Read(0, 0, 0, 2, buffer, 6);
printf("buffer = %s\n", buffer);
while (1)
{
}
}