问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

STM32 SPI通信详解:硬件电路与代码实现

创作时间:
作者:
@小白创作中心

STM32 SPI通信详解:硬件电路与代码实现

引用
CSDN
1.
https://blog.csdn.net/weixin_61921209/article/details/139202796

SPI(Serial Peripheral Interface)是一种常用的同步串行外设接口,广泛应用于嵌入式系统中。本文将详细介绍SPI通信的基本概念、硬件电路连接、移位示意图、时序以及在STM32微控制器上的硬件和软件实现方法。

一、SPI通信

  1. SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线
  2. 四根通信线:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO(Master Input Slave Output)、SS(Slave Select)
  • 同步,全双工
  • 支持总线挂载多设备(一主多从)

二、硬件电路

  1. 所有SPI设备的SCK、MOSI、MISO分别连在一起
  2. 主机另外引出多条SS控制线,分别接到各从机的SS引脚
  3. 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入
  4. 片选信号SS是低电平有效,哪个SS被置为低电平,总线上的设备就哪个起作用,有且至多仅有一个从设备与主机通信

PS:

  1. 推挽输出均有很强的驱动能力,使得上升沿和下降沿的速度很快。
  2. SS有高电平时(从机未被选中),此时从机的MISO切换成高阻态。

三、移位示意图

例如:主机的数据要发送到从机,从机的数据要发送主机(采用模式1)

  1. 时钟产生一个上升沿的电平,主机的高位移到MOSI(移位寄存器)线上,从机的高位移到MISO(移位寄存器)上。(SPI高位先行)
  2. 时钟产生一个下降沿时,主机会对MISO线上(移位寄存器)进行采样,从机会对MOSI线上(移位寄存器)进行采样
  3. 时钟继续产生一个上升沿的电平,主机的高位移到MOSI线上(移位寄存器),从机的高位移到MISO上(移位寄存器)。(SPI高位先行)
  4. 时钟继续产生一个下降沿时,主机会对MISO线上(移位寄存器)进行采样,从机会对MOSI线上(移位寄存器)进行采样
  5. 这样不断的循环直到主机把从机的数据置换过来

SPI通信最终的原理是主机与从机的字节交换,当主机没有数据给从机(主机只接收),而主机又需要从机的数据,主机就可以写一个垃圾数据(0XFF、0X00)给从机,就可以把主机想要的从机数据置换过来。当主机只发送的时候,从机也会随便写一个垃圾数据(0XFF、0XFF)给主机,将主机的数据置换出去

四、SPI时序

起始条件:SS从高电平切换到低电平
终止条件:SS从低电平切换到高电平

  • 交换一个字节(模式0)

  • CPOL=0:空闲状态时,SCK为低电平

  • CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

  • 由于SCK第一个边沿就要采样数据,那么从机和主机应该提前把数据移出来,所以在SS下降沿的时候主机和从机就移出数据了,如果一个字节交换结束之后还要继续交换数据,SCK的最后一个下降沿就会移出第二字节的最高位数据,如果主机和从机不继续交换数据,主机和从机的最高位也会冒一个头

  • 交换一个字节(模式1)

  • CPOL=0:空闲状态时,SCK为低电平

  • CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据

  • 交换一个字节(模式2)

  • CPOL=1:空闲状态时,SCK为高电平

  • CPHA=0:SCK第一个边沿移入数据,第二个边沿移出数据

  • 交换一个字节(模式3)

  • CPOL=1:空闲状态时,SCK为高电平

  • CPHA=1:SCK第一个边沿移出数据,第二个边沿移入数据

五、硬件SPI代码

#include "stm32f10x.h"                  // Device header
#include "mySPI.h"
//PA3 RTS
/**
  * 函    数:SPI写SS引脚电平,SS仍由软件模拟
  * 参    数:BitValue 协议层传入的当前需要写入SS的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SS为低电平,当BitValue为1时,需要置SS为高电平
  */
void MySPI_W_SS(uint8_t BitValue)
{
    GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);		//根据BitValue,设置SS引脚的电平
}
/**
  * 函    数:RST由软件模拟
  * 参    数:BitValue 协议层传入的当前需要写入SS的电平,范围0~1
  * 返 回 值:无
  * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置RST为低电平,当BitValue为1时,需要置RST为高电平
  */
void SPI_RST(uint8_t number)
{
    GPIO_WriteBit(GPIOA,GPIO_Pin_3,(BitAction)number);
}
/**
  * 函    数:SPI初始化
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Init(void)
{
    /*开启时钟*/
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);	//开启SPI1的时钟
    
    /*GPIO初始化*/
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		//SPI1 NSS/SDA
    GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA4引脚初始化为推挽输出
    
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;	//SPI1 MOSI/--7
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		//SPI1 SCK/--5
    GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA5和PA7引脚初始化为复用推挽输出
    
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		//SPI1 MISO/
    GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA6引脚初始化为上拉输入
    //PA3是RC522的复位引脚
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;				 //
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 		 //
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;		 PA3 RTS
    GPIO_Init(GPIOA, &GPIO_InitStructure);					 //
    
    /*SPI初始化*/
    SPI_InitTypeDef SPI_InitStructure;						//定义结构体变量
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;			//模式,选择为SPI主模式
    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;	//方向,选择2线全双工
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;		//数据宽度,选择为8位
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;		//先行位,选择高位先行
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;	//波特率分频,选择128分频
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;				//SPI极性,选择低极性
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;			//SPI相位,选择第一个时钟边沿采样,极性和相位决定选择SPI模式0
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;				//NSS,选择由软件控制
    SPI_InitStructure.SPI_CRCPolynomial = 7;				//CRC多项式,暂时用不到,给默认值7
    SPI_Init(SPI1, &SPI_InitStructure);						//将结构体变量交给SPI_Init,配置SPI1
    
    /*SPI使能*/
    SPI_Cmd(SPI1, ENABLE);									//使能SPI1,开始运行
    
    /*设置默认电平*/
    MySPI_W_SS(1);											//SS默认高电平
    SPI_RST(1);
}
/**
  * 函    数:SPI起始
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Start(void)
{
    MySPI_W_SS(0);				//拉低SS,开始时序
}
/**
  * 函    数:SPI终止
  * 参    数:无
  * 返 回 值:无
  */
void MySPI_Stop(void)
{
    MySPI_W_SS(1);				//拉高SS,终止时序
}
/**
  * 函    数:SPI交换传输一个字节,使用SPI模式0
  * 参    数:ByteSend 要发送的一个字节
  * 返 回 值:接收的一个字节
  */
uint8_t MySPI_RW_Byte(uint8_t ByteSend)
{
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);	//等待发送数据寄存器空
    
    SPI_I2S_SendData(SPI1, ByteSend);								//写入数据到发送数据寄存器,开始产生时序
    
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);	//等待接收数据寄存器非空
    
    return SPI_I2S_ReceiveData(SPI1);								//读取接收到的数据并返回
}

六、软件SPI代码

#include "stm32f10x.h"
#include "softspi.h"
/**********************************
*该程序是软件SPI的测试 用SPI零模式来操作
*SS---PA4
*SCLK---PA5
*PA6--MISO
*PA7--MOSI
*对于主机来说片选信号和时钟和主机输入都是输出引脚
*输出引脚配置为推挽输出(有独立的输入输出线,不需要考虑输入扫描时,输入线有输出的电平造成短路现象),输入引脚配置为浮空或上拉输入
***********************************/
//片选信号写操作
void MySPI_W_SS(uint8_t value)
{
     GPIO_WriteBit(A, Pin_SS, (BitAction)value);
}
//主机输出从机输入写操作
void MySPI_W_MOSI(uint8_t value)
{
     GPIO_WriteBit(A, Pin_MOSI, (BitAction)value);
}
//时钟信号写操作
void MySPI_W_SCLK(uint8_t value)
{
     GPIO_WriteBit(A, Pin_SCLK, (BitAction)value);
}
//读取MISO操作
uint8_t MySPI_R_MISO(void)
{
     return GPIO_ReadInputDataBit(A,  Pin_MISO);
}
//初始化SPI引脚
void MySoftSPI_Init(void)
{
    GPIO_InitTypeDef Init_GPIO; 
    //初始化时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    //初始化输出引脚
    Init_GPIO.GPIO_Pin = Pin_SS | Pin_SCLK | Pin_MOSI;		//设置引脚
    Init_GPIO.GPIO_Speed = GPIO_Speed_50MHz;	//设置输出的速度
    Init_GPIO.GPIO_Mode = GPIO_Mode_Out_PP;	//设置输出模式为推挽输出
    GPIO_Init(A, &Init_GPIO);//真正的初始化到引脚结构体中
    
    //初始化输入引脚
    Init_GPIO.GPIO_Pin = Pin_MISO;		//设置引脚
    Init_GPIO.GPIO_Speed = GPIO_Speed_50MHz;	//设置输出的速度
    Init_GPIO.GPIO_Mode = GPIO_Mode_IPU;	    //设置输入模式为上拉输入
    GPIO_Init(A, &Init_GPIO);//真正的初始化到引脚结构体中
    MySPI_W_SS(1);//片选拉高
    //SoftW25Q64_W_MOSI(Bit_SET);//没有明确规定 可以不需理会
    MySPI_W_SCLK(0);//0模式初始化是低电平
}
//SPI起始信号
void MySPI_Start(void)
{
    MySPI_W_SS(0);//片选拉低
}
//SPI停止信号
void MySPI_Stop(void)
{
    MySPI_W_SS(1);//片选拉高
}
//SPI字节交换(传输数据),0模式
uint8_t MySPI_RW_Byte(uint8_t byte)
{
    uint8_t i,ReciveData = 0x00;
    for(i = 0;i < 8;i++)
    {
        MySPI_W_MOSI(byte & (0x80 >>i));//SS拉低之后把byte的每一位依次传到MOSI上(MOSI引脚上输出对应的电平)
        MySPI_W_SCLK(1);//这时自动将MOSI数据进行采样
        if(MySPI_R_MISO() == 1){ReciveData |= (0x80>>i);}//MISO上的引脚进行读取电平
        
        MySPI_W_SCLK(0);//传出下一位到MOSI上
    }
    return ReciveData;
}

SPI有连续输出和非连续输出,一般采用非连续输出

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号