声明:此文章是我学习(尚硅谷)过程中的笔记。
1.SPI通信的介绍
SPI,(Serial Peripheral Interface),串行外围设备接口。
SPI接口主要应用于EEPROM,Flash,各种传感器,AD转换器等。
SPI是高速的、全双工、同步的串行通信总线。
1.1 物理层
SCK就是时钟线,由通讯主机(MCU)产生,
时钟线最大频率是Fpclk/2(不大于18MHz)。
MOSI:主设备输出/从设备输入:主机的数据从这条线输出,从机由这条线读入主机发送的数据。
MISO:主设备输入/从设备输出 :主机从这条信号线读入数据,从机从这条信号线输出数据到主机。
SS:片选线或使能线。有时候也叫NSS或CS。像I2C中,要向某个从机发信号,必须要先发地址找到从机,而SPI直接通过此线选定从机。
注意:只能主从之间通信,从机与从机之间不能通信。
1.2 协议层
主从机之间的数据交换:
移位寄存器临时储存要交换的数据(来自于发送缓冲区)。
接收缓冲区存储最终得到的值。
来一个上升沿信号:主机就会通过MOSI向从机发送一个数据,从机通过MISO向主机发送数据,他们分别把自己的高位左移出来(一般高位先行)(发送高位信号)。
来一个下降沿信号:主机和从机分别读入数据,存储到移位寄存器的低位。
经过8次同样的操作就完成了一个字节的交换。
如果主机只想发不想收呢?
我们可以让从机随便发数据,主机不管这个接收的数据就行了。
时钟的极性和相位:
时钟的极性CPOL:
通信的整个过程分成通信时刻和空闲时刻。
空闲状态SCK是低电平:CPOL=0;
空闲状态SCK是高电平:CPOL=1;
时钟的相位CPHA:
直接决定SPI总线从哪个跳变沿开始采样数据。
CPHA=0:从第一个跳变沿开始采样数据。
CPHA=1:从第二个跳变沿开始采样数据。
SPI的四种模式:根据相位和极性来选择:
MODE CPOL CPHA MODE0 0 0 MODE1 0 1 MODE2 1 0 MODE3 1 1 MODE0:
MODE1:
MODE2:
MODE3:
记住:这个只是在这个上升沿或者下降沿进行采样数据。
很多设备支持模式1和3.
2.软件模拟SPI协议
2.1 W25Q32简介
在这之前,我们要了解一个设备(W25Q32)。
W25Q32是一种使用SPI通讯协议的NOR FLASH存储器。
这个芯片的框图是这样的:
写入之前的注意事项:
- 写入操作前,必须先进行写使能。
- 每个数据位只能由1改写为0,不能由0改写为1。
- 写入数据前必须先檫除,檫除后,所有数据位变为1。擦除必须按最小擦除单元进行(一段4kb)。
- 连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入。
- 写入操作结束后,芯片进入忙状态,不响应新的读写操作。
接收之前的注意事项:
- 直接调用读取时序,无需读使能,无需额外操作,没有页的限制。
- 读取操作结束后不会进入忙状态,但不能在忙状态时读取。(就是在读取之前判断一次忙的 状态,之后就直接读取就行了)。
2.2 全部程序
SPI.h
#ifndef _SPI_H
#define _SPI_H
#include"stm32f10x.h"
#include"delay.h"
//定义几个宏,可读性高,方便
#define SPI_SCK_HIGH (GPIOA->ODR|=GPIO_ODR_ODR5)
#define SPI_SCK_LOW (GPIOA->ODR&=~GPIO_ODR_ODR5)
#define SPI_MOSI_HIGH (GPIOA->ODR|=GPIO_ODR_ODR7)
#define SPI_MOSI_LOW (GPIOA->ODR&=~GPIO_ODR_ODR7)
#define SPI_READ (GPIOA->IDR & GPIO_IDR_IDR6)
#define SPI_CS_HIGH (GPIOC->ODR|=GPIO_ODR_ODR13)
#define SPI_CS_LOW (GPIOC->ODR&=~GPIO_ODR_ODR13)
void spi_init(void);
void spi_start(void);
void spi_stop(void);
//其实SPI就是一直在交换数据
uint8_t spi_swapdata(uint8_t data);
#endif
SPI.c
#include"spi.h"
void spi_init(void)
{
//开启时钟
RCC->APB2ENR|=RCC_APB2ENR_IOPAEN;
RCC->APB2ENR|=RCC_APB2ENR_IOPCEN;
RCC->APB2ENR|=RCC_APB2ENR_SPI1EN;
//因为是模拟SPI所以配置GPIO PA5 PA7 PC13 通用推挽输出 MODE 11 CNF 00 PA6 浮空输入 MODE 00 CNF 01
GPIOA->CRL|=(GPIO_CRL_MODE5|GPIO_CRL_MODE7);
GPIOA->CRL&=~(GPIO_CRL_CNF5|GPIO_CRL_CNF7);
GPIOC->CRH|=GPIO_CRH_MODE13;
GPIOC->CRH&=~GPIO_CRH_CNF13;
GPIOA->CRL&=~GPIO_CRL_MODE6;
GPIOA->CRL&=~GPIO_CRL_CNF6_1;
GPIOA->CRL|=GPIO_CRL_CNF6_0;
//按照模式0,空闲的时候是低电平
SPI_SCK_LOW;
//高电平就是片选没有选中
SPI_CS_HIGH;
Delay_us(5);
}
void spi_start(void)
{
//片选选中,开启链接
SPI_CS_LOW;
}
void spi_stop(void)
{
SPI_CS_HIGH;
}
//对于SPI的交换数据,就是发一个收一个
uint8_t spi_swapdata(uint8_t data)
{
uint8_t rdata;
for (uint8_t i = 0; i < 8; i++) //一个字节的信号
{
if(data & 0x80) //这个就是高位先行,如果最高位是高,就发送一个高电平
{
SPI_MOSI_HIGH;
}
else
{
SPI_MOSI_LOW;
}
data<<=1; //将数据左移一位,与上面联动起来就是发上面最高位的下面一位。
SPI_SCK_HIGH; //将时钟线拉高,开始读取数据
rdata<<=1; //先左移而不是先读取的原因是,要是左移放在后面的话,结束的时候最后一位肯定是0.
if(SPI_READ)
{
rdata|=0x01;
}
SPI_SCK_LOW;
}
return rdata;
}
W25Q32.h
#ifndef _W25Q32_H
#define _W25Q32_H
#include"stm32f10x.h"
#include"delay.h"
#include"spi.h"
void w25q32_init(void);
//读取芯片的id,相当于检验了
void w25q32_id(uint8_t* mid,uint16_t * did);
//写使能
void w25q32_writeenable(void);
void w25q32_writedisable(void);
//忙状态检测
void w25q32_notbusy(void);
//擦除
void w25q32_erase(uint32_t block,uint32_t secter);
//页写
void w25q32_writepage(uint32_t block,uint32_t secter,uint32_t page,uint8_t * data,uint16_t len);
void w25q32_freedomwrite(uint32_t block,uint32_t secter,uint32_t page,uint32_t inner,uint8_t * data,uint16_t len);
//读取
void w25q32_read(uint32_t block,uint32_t secter,uint32_t page,uint32_t innearpage,uint8_t * data,uint16_t len);
#endif
W25Q32.c
读下列代码必须要知道的:
#include"w25q32.h"
void w25q32_init(void)
{
spi_init();
}
void w25q32_id(uint8_t* mid,uint16_t * did)
{
spi_start();
spi_swapdata(0x9f);
*mid=spi_swapdata(0xff); //这个发出0xff就是交换数据的功能,我不在乎我发啥,我在乎去我收到的数据
*did=0;
*did|=spi_swapdata(0xff)<<8;
*did|=spi_swapdata(0xff);
spi_stop();
}
void w25q32_writeenable(void)
{
spi_start();
spi_swapdata(0x06);
spi_stop();
}
void w25q32_writedisable(void)
{
spi_start();
spi_swapdata(0x04);
spi_stop();
}
void w25q32_notbusy(void)
{
spi_start();
spi_swapdata(0x05); //获取标志状态
while (spi_swapdata(0xff)&0x01) //获取到标志信号的最后一位就是忙的状态,判断一下
{
}
spi_stop();
}
void w25q32_erase(uint32_t block,uint32_t secter) //擦除最小单位是一段,所以参数需要这些。
{
w25q32_notbusy(); //写入之前要判断忙的状态。
w25q32_writeenable();
spi_start();
uint32_t add=(block<<16)+(secter<<12); //16-23位是块,12-16位是段,8-12位是页
spi_swapdata(0x20);
spi_swapdata((add>>16) & 0xff);
spi_swapdata((add>>8) & 0xff);
spi_swapdata(add & 0xff);
spi_stop();
w25q32_writedisable();
}
//数据的写入详细到哪一页,其实还可以详细到从哪一个字节开始写。
void w25q32_writepage(uint32_t block,uint32_t secter,uint32_t page,uint8_t * data,uint16_t len)
{
w25q32_notbusy();
w25q32_writeenable();
spi_start();
spi_swapdata(0x02);
uint32_t addr=(block<<16)+(secter<<12)+(page<<8);
spi_swapdata((addr>>16) & 0xff);
spi_swapdata((addr>>8) & 0xff);
spi_swapdata( addr & 0xff);
for (uint32_t i = 0; i < len; i++)
{
spi_swapdata(data[i]);
}
spi_stop();
w25q32_writedisable();
}
void w25q32_freedomwrite(uint32_t block,uint32_t secter,uint32_t page,uint32_t inner,uint8_t * data,uint16_t len)
{
w25q32_notbusy();
w25q32_writeenable();
spi_start();
spi_swapdata(0x02);
uint32_t addr=(block<<16)+(secter<<12)+(page<<8)+inner;
spi_swapdata((addr>>16) & 0xff);
spi_swapdata((addr>>8) & 0xff);
spi_swapdata( addr & 0xff);
for (uint32_t i = 0; i < len; i++)
{
spi_swapdata(data[i]);
}
spi_stop();
w25q32_writedisable();
}
//这个就是详细到从哪一个字节开始读取
void w25q32_read(uint32_t block,uint32_t secter,uint32_t page,uint32_t innearpage,uint8_t * data,uint16_t len)
{
w25q32_notbusy();
spi_start();
spi_swapdata(0x03);
uint32_t addr=(block<<16)+(secter<<12)+(page<<8)+innearpage;
spi_swapdata((addr>>16) & 0xff);
spi_swapdata((addr>>8) & 0xff);
spi_swapdata( addr & 0xff);
for (uint32_t i = 0; i < len; i++)
{
data[i]=spi_swapdata(0xff);
}
spi_stop();
}
main.c
#include "USART.h"
#include "eeprom.h"
#include "w25q32.h"
#include "spi.h"
#include <string.h>
uint8_t mid=0;
uint16_t did=0;
int main()
{
uint8_t buffer[10]={0};
usart_init();
printf("111222\r\n");
w25q32_init();
w25q32_id(&mid,&did);
printf("%#x %#x\n",mid,did); //获取ID
w25q32_erase(0,0); //擦除
w25q32_writepage(0,0,0,"12345678",8); //从这个页首开始写8个字节
w25q32_freedomwrite(0,0,1,20,"abcdefg",7); //从这个第1页第20个字节开始写abcdefg
w25q32_read(0,0,0,0,buffer,8); //从这个页首读“12345678”的8个字节
printf("buffer=%s\n",buffer);
memset(buffer,0,10); //清除buffer中的数据
w25q32_read(0,0,1,22,buffer,5); //从第1页的第22个字节开始读5个字节
printf("buffer=%s",buffer);
while (1)
{
}
}
3.硬件实现SPI协议
对于硬件实现,需要修改的只有硬件的SPI,而接口层的W25Q32并不需要修改。
3.1 寄存器的介绍
3.2 全部程序
SPI.h
#ifndef _SPI_H
#define _SPI_H
#include"stm32f10x.h"
#include"delay.h"
#define SPI_CS_HIGH (GPIOC->ODR|=GPIO_ODR_ODR13)
#define SPI_CS_LOW (GPIOC->ODR&=~GPIO_ODR_ODR13)
void spi_init(void);
void spi_start(void);
void spi_stop(void);
uint8_t spi_swapdata(uint8_t data);
#endif
SPI.c
#include"spi.h"
void spi_init(void)
{
//开启时钟
RCC->APB2ENR|=RCC_APB2ENR_IOPAEN;
RCC->APB2ENR|=RCC_APB2ENR_IOPCEN;
RCC->APB2ENR|=RCC_APB2ENR_SPI1EN;
//配置GPIO PA5 PA7 复用推挽输出 MODE 11 CNF 10 PA6 浮空输入 MODE 00 CNF 01 PC13 通用推挽输出
GPIOA->CRL|=(GPIO_CRL_MODE5|GPIO_CRL_MODE7);
GPIOA->CRL|=(GPIO_CRL_CNF5_1|GPIO_CRL_CNF7_1);
GPIOA->CRL&=~(GPIO_CRL_CNF5_0|GPIO_CRL_CNF7_0);
GPIOC->CRH|=GPIO_CRH_MODE13;
GPIOC->CRH&=~GPIO_CRH_CNF13;
GPIOA->CRL&=~GPIO_CRL_MODE6;
GPIOA->CRL&=~GPIO_CRL_CNF6_1;
GPIOA->CRL|=GPIO_CRL_CNF6_0;
//配置SPI
//配置为主模式
SPI1->CR1|=SPI_CR1_MSTR;
//配置软件片选
SPI1->CR1|=SPI_CR1_SSM;
SPI1->CR1|=SPI_CR1_SSI;
//分频
SPI1->CR1&=~SPI_CR1_BR;
SPI1->CR1|=SPI_CR1_BR_0;
//模式0
SPI1->CR1&=~SPI_CR1_CPHA;
SPI1->CR1&=~SPI_CR1_CPOL;
//高位先行
SPI1->CR1&=~SPI_CR1_LSBFIRST;
//8位数据帧格式
SPI1->CR1&=~SPI_CR1_DFF;
//使能
SPI1->CR1|=SPI_CR1_SPE;
}
void spi_start(void)
{
SPI_CS_LOW;
}
void spi_stop(void)
{
SPI_CS_HIGH;
}
uint8_t spi_swapdata(uint8_t data)
{
while ((SPI1->SR & SPI_SR_TXE)==0)
{
}
SPI1->DR=data;
while ((SPI1->SR & SPI_SR_RXNE)==0)
{
}
return (uint8_t)(SPI1->DR);
}