1. SPI总线的电路结构,如图:

• 上图SPI总线可以实现多个设备进行通信。
• MOSI:是主机发送数据,从机接收。
• MISO:是从机发送数据,主机接收。
• SCK:是传输的是时钟信号,可以表示通信速度(快或者慢) 。一个时钟周期,发送和接收一个bit位数据。
• NSS:从机选择引脚,如果主机要选择和那个从机进行通信就需要拉低对应从机的NSS引脚。默认情况下是高电压。
2. 通信流程及波形
• 如图:

• 上图,如果主机要和从机1进行通信,就需要先拉低从机1的NSS引脚,其他从机的NSS引脚保持高电压。
• 一个时钟信号周期内主机和从机能接收和发送一个bit数据
• 主机能发送多少个bit数据,也能同时接收多少个bit数据。
3. 时钟极性和相位
3.1 极性,如图:
• SPI总线不工作(不通信)的时候,SCK的状态(空闲的时候)。
3.2 相位,如图:

• 采样的边沿。
• 例子,如图:

4. SPI的四种工作模式
• 如图:

4.1 根据上图解释
4.1.1 模式0(CPOL(极性) = 0,CPHA(相位) = 0)
• CPOL = 0表示在空闲状态下,SCK为低电平。
• CPHA = 0意味着数据在SCK的上升沿进行采样 。所以在数据采集期间,SCK是从低电平跳变到高电平的上升沿时刻进行数据采集。此时在采集数据的瞬间,SCK是高电平。
4.1.2 模式1(CPOL = 0,CPHA = 1)
• CPOL = 0,SCK空闲时为低电平。
• CPHA = 1表示数据在SCK的下降沿进行采样 。数据采集期间,SCK处于下降沿,此时SCK从高电平跳变到低电平,数据是在这个下降沿的瞬间被采集,在这个瞬间SCK的状态是低电平。
4.1.3 模式2(CPOL = 1,CPHA = 0)
• CPOL = 1,SCK空闲时为高电平。
• CPHA = 0,数据在SCK的下降沿进行采样。当进行数据采集时,SCK从高电平跳变到低电平,在数据采集的瞬间,SCK是低电平。
4.1.4 模式3(CPOL = 1,CPHA = 1)
• CPOL = 1,空闲状态SCK为高电平。
• CPHA = 1,数据在SCK的上升沿进行采样。数据采集是在SCK从低电平跳变到高电平的上升沿时刻,此时采集数据的瞬间SCK是高电平。
5. 传输顺序,是高位先行还是低位先行
• 如图:

6. 数据宽度,每次传输bit的个数,是每次传输8个字节还是16个字节
• 如图:

7. 如何配置SPI
7.1 SPI模块简介,如图:

• 这个型号的单片机有两个SPI,但是挂载到不同的总线上。
• SPI是单片机上的一个片上外设,是一个通信接口。
7.2 W25Q128简介,如图:

• 如图,W25Q64通过SPI总线和STM32进行通信
• W25Q64是一种断电数据不丢失的Flash芯片,类似于电脑的硬盘。
7.3 SPI的引脚位置,接线图:

• NSS引脚是从机选择引脚。
• 对于主机模式(单片机作为主机)用不到这个引脚。
• 对于和从机的NSS/CS引脚连接,主机可以随便选择一个IO和从机的NSS/CS连接,由主机发送低电压来控制从机的NSS,来选择和那个从机通信。
• 引脚位置,如图:

• IO引脚的模式,如图:

• 选择IO引脚的最大输出速度,如图:
• 由于这里时钟频率高达80Mhz,但是如果选择这么高的频率可能导致我们传输数据不稳,所以在这里选择的是1Mhz频率。如图:

7.4 如图配置SPI模块
7.4.1 SPI模块的基本工作原理
• 如图,这是SPI的结构框图:

• MOSI:是主机发送,从机接收的意思,是主机向外发送数据的引脚。
• MISO:是主机接收,从机发送的意思,是主机从外界接收数据的引脚。
• SCK:是主机发送时钟信号的引脚。
• 注:主机一般不会用NSS,去拉低从机的NSS,而是使用普通IO去和从机的NSS连接。
• 通信:
• 要发送的数据的会放到发送数据寄存器里面,然后通过并行传输,传输给发送移位寄存器,然后在发送移位寄存器里面一个bit一个bit通过MOSI向外发送。
• 接收的波形通过MISO传输进来,然后一个bit一个bit进行解析,再然后进入到接收移位寄存器里面,接收满之后,在传输给接收数据寄存器。
• 波特率是通过对时钟进行分频得到的,而分频系数可以设置,也就说可以设置不同的波特率。
• 上图,有三个标志位,可以知道寄存器里面的工作状态。
7.4.2 SPI_Init,如图:

• 上图是SPI初始化要配置的参数。但是配置参数需要考虑从机适合哪一些参数。
• 比如w25q64,如图:

• W25Q64的数据宽度是8位,高位先行,极性和相位都知道。
7.4.3 选择数据通信方向,如图:

• 如图SPI的通信方向有4种:
• 2线全双工就是主机可以发送数据,也可以接收数据,收发是同时进行的。
• 2线只读,接线和2线全双工一样,但是只有主机通过MOSI给从机发送数据,从机不会通过MISO给主机发送。类似于"广播"。
• 1线发送和1线接收的接线如图,这种方法能够实现接收或者发送,但是不能同时实现。
• 
• 
7.4.4 设置波特率,如图:

• SPI的波特率是通过时钟分频得到的,根据需求可以设置不同的波特率。
• W25q64的时钟频率最高可以达到80MHz,所以可以随便选择小于80M就可以,这里选择1MHz左右就行了。
7.4.5 NSS的配置方式
• 虽然STM32作为主机不用NSS去拉低从机的NSS,但是在配置的时候也是要对NSS配置。但是主机拉低从机NSS是用普通的IO。
• 如图,这是真实的SPI接线图:

• 这里的主机的NSS要3.3v,因为要满足适配多主机模式的情况。
• 多主机,这是多主机的接线图,如图:

• 这两个主机的NSS谁被拉低,谁就会变成从机。
• 所以,上图主机NSS配置3.3v是要适配多主机(就算没有使用多主机,在stm32作为spi主机的时候,也要适配多主机模式)。同时防止被拉低,导致主机身份丢失。
• 但是一般来说不会让引脚接3.3v(这是硬件的NSS),一般是使用软件的NSS,这样可以节省引脚
• 软件NSS,如图:

• 可以在选择器那里选择软件NSS,同时通过在内部NSS写1让NSS表现出高电压,0是表示低电压。所以在内部NSS写1会让NSS表现出高电压。
7.5 SPI数据收发的特点,如图:

• 在每一个时钟周期内,主机能够发送一个bit数据,同时也能接收一个bit的数据。
• 特点:SPI数据收发是双向的,同时的,每发送一个bit数据必然会接收一个bit数据。
• 也就说主机发送多少个bit数据,也会接收多少个bit数据。
8. W25Q64的存储结构
• 如图:
9. 代码
#include "stm32f10x.h"
#include "button.h"
Button_TypeDef button1;
void spi1_init();
void spi_master_Transmit_Receive(SPI_TypeDef* SPIx,const uint8_t* pdataTx,uint8_t * pdataRx,uint16_t size);
void w25q64_saveByte(uint8_t byte);
uint8_t w25q64_loadByte();
void led_init();
void button_init();
void button_clicked_cb(uint8_t clicks);
int main(void)
{
spi1_init();
button_init();
led_init();
uint8_t byte = w25q64_loadByte();
if(byte == 0){
GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);
}else {
GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_RESET);
}
while(1)
{
My_Button_Proc(&button1);
}
}
void button_clicked_cb(uint8_t clicks){
if(clicks == 1){//按键一次的情况
if(GPIO_ReadOutputDataBit(GPIOC,GPIO_Pin_13) == Bit_SET){
GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_RESET);
w25q64_saveByte(0x01);//亮
}else {
GPIO_WriteBit(GPIOC,GPIO_Pin_13,Bit_SET);
w25q64_saveByte(0x00);//灭
}
}
}
void button_init(){
Button_InitTypeDef button_initstruct = {0};
button_initstruct.GPIOx = GPIOA;
button_initstruct.GPIO_Pin = GPIO_Pin_0;
button_initstruct.button_clicked_cb = button_clicked_cb;
My_Button_Init(&button1,&button_initstruct);
}
void led_init(){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC,ENABLE);
GPIO_InitTypeDef gpio_initstruct = {0};
gpio_initstruct.GPIO_Mode = GPIO_Mode_Out_OD;
gpio_initstruct.GPIO_Pin = GPIO_Pin_13;
gpio_initstruct.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOC,&gpio_initstruct);
}
void w25q64_saveByte(uint8_t byte){
uint8_t buffer[10];//用于发送指令
//写使能
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_RESET);//选中
buffer[0] = 0x06;//发送写使能指令
spi_master_Transmit_Receive(SPI1,buffer,buffer,1);
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET);//取消选中
//扇区擦除
buffer[0] = 0x20;//扇区擦除指令
buffer[1] = 0x00;//这是24位地址
buffer[2] = 0x00;
buffer[3] = 0x00;
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_RESET);//选中
spi_master_Transmit_Receive(SPI1,buffer,buffer,4);
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET);//取消选中
//等待空闲
while(1){//不断获取状态寄存器里面的值,因为扇区擦除需要花费时间
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_RESET);//选中
buffer[0] = 0x05;//读状态寄存器的指令
spi_master_Transmit_Receive(SPI1,buffer,buffer,1);
buffer[0] = 0xff;//在读取状态寄存器的值的时候,同时也要保证MOSI一直为高电压
spi_master_Transmit_Receive(SPI1,buffer,buffer,1);
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET);//取消选中
//如果状态寄存器里面的BUSY == 0表示空闲了
if((buffer[0] & 0x01) == 0) break;
}
//写使能
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_RESET);//选中
buffer[0] = 0x06;//发送写使能指令
spi_master_Transmit_Receive(SPI1,buffer,buffer,1);
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET);//取消选中
//页编程
buffer[0] = 0x02;//页编程指令
buffer[1] = 0x00;//这是24位地址
buffer[2] = 0x00;
buffer[3] = 0x00;
buffer[4] = byte;//要写入的数据
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_RESET);//选中
spi_master_Transmit_Receive(SPI1,buffer,buffer,5);
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET);//取消选中
//等待空闲
while(1){//不断获取状态寄存器里面的值
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_RESET);//选中
buffer[0] = 0x05;//读状态寄存器的指令
spi_master_Transmit_Receive(SPI1,buffer,buffer,1);
buffer[0] = 0xff;//在读取状态寄存器的值的时候,同时也要保证MOSI一直为高电压
spi_master_Transmit_Receive(SPI1,buffer,buffer,1);
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET);//取消选中
//如果状态寄存器里面的BUSY == 0表示空闲了
if((buffer[0] & 0x01) == 0) break;
}
}
uint8_t w25q64_loadByte(){
uint8_t buffer[10];
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_RESET);//选中
buffer[0] = 0x03;//读取数据指令
buffer[1] = 0x00;//这是24位地址
buffer[2] = 0x00;
buffer[3] = 0x00;
spi_master_Transmit_Receive(SPI1,buffer,buffer,4);
buffer[0] = 0xff;//在读取状态寄存器的值的时候,同时也要保证MOSI一直为高电压
spi_master_Transmit_Receive(SPI1,buffer,buffer,1);
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET);//取消选中
return buffer[0];
}
void spi1_init(){
//重映射SPI1的MOSI,MISO,SCK引脚
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);
GPIO_PinRemapConfig(GPIO_Remap_SPI1,ENABLE);
//重映射PA15
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);
GPIO_InitTypeDef gpio_initstruct = {0};
//PB3 SCK
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
gpio_initstruct.GPIO_Mode = GPIO_Mode_AF_PP;
gpio_initstruct.GPIO_Pin = GPIO_Pin_3;
gpio_initstruct.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOB,&gpio_initstruct);
//PB4 MISO
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
gpio_initstruct.GPIO_Mode = GPIO_Mode_IPU;
gpio_initstruct.GPIO_Pin = GPIO_Pin_4;
//gpio_initstruct.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOB,&gpio_initstruct);
//PB5 MOSI
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
gpio_initstruct.GPIO_Mode = GPIO_Mode_AF_PP;
gpio_initstruct.GPIO_Pin = GPIO_Pin_5;
gpio_initstruct.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOB,&gpio_initstruct);
//PA15 任意IO控制从机的NSS/CS
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
gpio_initstruct.GPIO_Mode = GPIO_Mode_Out_PP;
gpio_initstruct.GPIO_Pin = GPIO_Pin_15;
gpio_initstruct.GPIO_Speed = GPIO_Speed_2MHz;
GPIO_Init(GPIOA,&gpio_initstruct);
GPIO_WriteBit(GPIOA,GPIO_Pin_15,Bit_SET);
//开启SPI时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1,ENABLE);
SPI_InitTypeDef spi_initstruct = {0};
spi_initstruct.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_64;
spi_initstruct.SPI_CPHA = SPI_CPHA_1Edge;
spi_initstruct.SPI_CPOL = SPI_CPOL_Low;
spi_initstruct.SPI_DataSize = SPI_DataSize_8b;
spi_initstruct.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
spi_initstruct.SPI_FirstBit = SPI_FirstBit_MSB;
spi_initstruct.SPI_Mode = SPI_Mode_Master;
spi_initstruct.SPI_NSS = SPI_NSS_Soft;//主机的NSS使用软件管理
SPI_Init(SPI1,&spi_initstruct);
//通过内部NSS写1让NSS表现高电压
SPI_NSSInternalSoftwareConfig(SPI1,SPI_NSSInternalSoft_Set);
}
void spi_master_Transmit_Receive(SPI_TypeDef* SPIx,const uint8_t* pdataTx,uint8_t * pdataRx,uint16_t size){
SPI_Cmd(SPIx,ENABLE);//数据传输开始前要闭合SPI总开关
//向TDR发送第一个数据
SPI_I2S_SendData(SPIx,pdataTx[0]);//为什么要先发第一个,因为要保证后续的TDR里面都有数据发送。
//循环size - 1次发送数据,接收数据
for(uint16_t i = 0;i < size - 1;i++){
//本质上进循环之前,第一个字节的数据收发已经开始了
//因为第一个写进来的数据已经被移动到发送移位寄存器里面了。
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_TXE) == RESET);//判断TDR是否为空
SPI_I2S_SendData(SPIx,pdataTx[i + 1]);//往TDR发送数据,因为要保证后续的TDR里面都有数据发送。
while(SPI_I2S_GetFlagStatus(SPIx,SPI_I2S_FLAG_RXNE) == RESET);//判断RDR是否为值
pdataRx[i] = SPI_I2S_ReceiveData(SPIx);
}
//最后一个数据发送完,在TDR里面,然后开始接收最后一个字节
while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) == RESET);//判断RDR是否为值
pdataRx[size - 1] = SPI_I2S_ReceiveData(SPIx);
SPI_Cmd(SPIx,DISABLE);//数据传输完要断开SPI总开关
//总结:
/*
为什么一开始要写SPI_I2S_SendData(SPI1,pdataTx[0]);这一句代码
因为,这一句代码和第一个miso传进来的数据开始进行接收和发送
然后for里面的让TDR寄存器里面始终都要有一个字节数据等待发送和下一个miso传进来的数据,进行同时收发
但是,如果都写在for,虽然第一句还是会正常同时接收和发送,
但是在第一句同时收发的时候,会卡在while(SPI_I2S_GetFlagStatus(SPI1,SPI_I2S_FLAG_RXNE) == RESET);这里
这会导致TDR里面没有待发送的数据。所以第二个字节的数据可能会造成一点误差。
在这个代码实现同时收发的前提是TDR要保证一直有数据。
*/
}
