STM32之SPI

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要保证一直有数据。
    */
}
相关推荐
帅次15 小时前
系统分析师-信息物理系统分析与设计
stm32·单片机·嵌入式硬件·mcu·物联网·iot·rtdbs
澜莲Alice15 小时前
STM32 MPLAB X IDE 软件安装-玩转单片机-英文版沉浸式安装
stm32·单片机·嵌入式硬件
良许Linux15 小时前
IIC总线的硬件部分的两个关键点:开漏输出+上拉电阻
单片机·嵌入式硬件
✎ ﹏梦醒͜ღ҉繁华落℘16 小时前
单片机基础知识 -- ADC分辨率
单片机·嵌入式硬件
Q_219327645516 小时前
车灯控制与报警系统设计
人工智能·嵌入式硬件·无人机
雾削木17 小时前
树莓派部署 HomeAssistant 教程
stm32·单片机·嵌入式硬件
Q_219327645517 小时前
基于单片机的破壁机自动控制系统设计
单片机·嵌入式硬件
我是一棵无人问荆的小草17 小时前
stm32f103芯片多个IO配置成外部中断
stm32·单片机·嵌入式硬件
wjykp17 小时前
ESP32xxx烧录
stm32·单片机·嵌入式硬件
早起huo杯黑咖啡18 小时前
【NOR Flash】关于芯片的高耐久性分区的编程/擦除周期和最小保留时间的数据
单片机·嵌入式硬件