STM32之SPI通信

声明:此文章是我学习(尚硅谷)过程中的笔记。

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的四种模式:根据相位和极性来选择:

MODECPOLCPHA
MODE000
MODE101
MODE210
MODE311

 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);
    
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值