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

SPI通信详解:原理、硬件连接与STM32实现

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

SPI通信详解:原理、硬件连接与STM32实现

引用
CSDN
1.
https://blog.csdn.net/weixin_46024065/article/details/136906008

SPI(Serial Peripheral Interface)是一种由Motorola公司开发的通用数据总线,广泛应用于嵌入式系统中。它通过四根通信线(SCK、MOSI、MISO、SS)实现全双工同步通信,支持一主多从的设备连接方式。本文将详细介绍SPI通信的原理、硬件连接方式以及在STM32微控制器中的软件实现方法。

SPI通信概述

SPI(Serial Peripheral Interface)是由Motorola公司开发的一种通用数据总线。它使用四根通信线进行数据传输:

  • SCK(Serial Clock):串行时钟线,由主机产生。
  • MOSI(Master Output Slave Input):主机输出从机输入数据线。
  • MISO(Master Input Slave Output):主机输入从机输出数据线。
  • SS(Slave Select):从机选择线,用于选择特定的从设备进行通信。

SPI通信具有以下特点:

  • 同步通信:使用时钟信号进行数据同步。
  • 全双工通信:主机和从机可以同时发送和接收数据。
  • 支持总线挂载多设备:通过SS线实现一主多从的设备连接方式。
  • 没有应答机制:数据传输不包含确认机制。

硬件连接方式如下:

  • 所有SPI设备的SCK、MOSI、MISO线分别连在一起。
  • 主机另外引出多条SS控制线,分别接到各从机的SS引脚,设置为推挽输出。
  • 输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入。
  • SCK对主机来说是输出,对从机是输入,也设置成推挽输出。
  • 当从机的SS引脚为高电平时,其MISO必须切换为高阻态,以防止电平冲突。

(I2C开漏外加弱上拉电阻的电路结构,使得通信线高电平恶的驱动能力比较弱。当SDA由低到高的时候上升沿耗时长。所以I2C标准模式只有100khz的时钟频率,快速模式只有400khz。I2C之后又通过改进电路的方式,设计出了高速模式。速度达到3.4Mhz,但是普及率不太高)

SPI移位示意图

SPI通信采用高位先行的方式。数据传输过程可以理解为:每当时钟边沿到来时,数据被放到通信线上,下一个边沿时数据从线上被移入寄存器。这种机制对应了数据的移出和移入操作。

SPI时序

SPI通信有多种模式,其中模式0是最常见的。在模式0中:

  • 当SS下降沿时数据紧接着就移出了。
  • SCK的第一个上升沿就可以把数据移入寄存器。
  • 在代码实现时,需要先设置上升下降沿,再对数据进行操作(并不是严格的同时)。
  • 最后一个SCK下降沿,从机发送的是下一个字节的B7位。
  • 如果想连续交换多个字节,可以在不拉高SS的情况下重复上述时序。

在SS的上升沿时,MOSI可以置高低电平,但SPI并没有规定MOSI默认电平,因此没有必要特别设置。

软件代码实现

以下是SPI通信的软件代码实现示例,使用C语言编写,主要展示了引脚配置和数据传输功能。

/* 引脚配置层 */
/**
 * 函    数:SPI写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引脚的电平
}

/**
 * 函    数:SPI写SCK引脚电平
 * 参    数:BitValue 协议层传入的当前需要写入SCK的电平,范围0~1
 * 返 回 值:无
 * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置SCK为低电平,当BitValue为1时,需要置SCK为高电平
 */
void MySPI_W_SCK(uint8_t BitValue)
{
    GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);  // 根据BitValue,设置SCK引脚的电平
}

/**
 * 函    数:SPI写MOSI引脚电平
 * 参    数:BitValue 协议层传入的当前需要写入MOSI的电平,范围0~0xFF
 * 返 回 值:无
 * 注意事项:此函数需要用户实现内容,当BitValue为0时,需要置MOSI为低电平,当BitValue非0时,需要置MOSI为高电平
 */
void MySPI_W_MOSI(uint8_t BitValue)
{
    GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);  // 根据BitValue,设置MOSI引脚的电平,BitValue要实现非0即1的特性
}

/**
 * 函    数:I2C读MISO引脚电平
 * 参    数:无
 * 返 回 值:协议层需要得到的当前MISO的电平,范围0~1
 * 注意事项:此函数需要用户实现内容,当前MISO为低电平时,返回0,当前MISO为高电平时,返回1
 */
uint8_t MySPI_R_MISO(void)
{
    return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);        // 读取MISO电平并返回
}

/**
 * 函    数:SPI初始化
 * 参    数:无
 * 返 回 值:无
 * 注意事项:此函数需要用户实现内容,实现SS、SCK、MOSI和MISO引脚的初始化
 */
void MySPI_Init(void)
{
    /* 开启时钟 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);   // 开启GPIOA的时钟

    /* GPIO初始化 */
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);                  // 将PA4、PA5和PA7引脚初始化为推挽输出

    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStructure);                  // 将PA6引脚初始化为上拉输入

    /* 设置默认电平 */
    MySPI_W_SS(1);                                          // SS默认高电平
    MySPI_W_SCK(0);                                         // SCK默认低电平
}

/* 协议层 */
/**
 * 函    数: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_SwapByte(uint8_t ByteSend)
{
    uint8_t i, ByteReceive = 0x00;                          // 定义接收的数据,并赋初值0x00,此处必须赋初值0x00,后面会用到

    for (i = 0; i < 8; i++)                                 // 循环8次,依次交换每一位数据
    {
        MySPI_W_MOSI(ByteSend & (0x80 >> i));               // 使用掩码的方式取出ByteSend的指定一位数据并写入到MOSI线
        MySPI_W_SCK(1);                                     // 拉高SCK,上升沿移出数据
        if (MySPI_R_MISO() == 1) { ByteReceive |= (0x80 >> i); }  // 读取MISO数据,并存储到Byte变量
                                                            // 当MISO为1时,置变量指定位为1,当MISO为0时,不做处理,指定位为默认的初值0
        MySPI_W_SCK(0);                                     // 拉低SCK,下降沿移入数据
    }

    return ByteReceive;                                     // 返回接收到的一个字节数据
}

STM32硬件SPI实现

STM32微控制器内部集成了硬件SPI收发电路,可以自动执行时钟生成、数据收发等功能,从而减轻CPU的负担。硬件SPI支持以下特性:

  • 可配置8位/16位数据帧
  • 高位先行/低位先行选择
  • 时钟频率可配置为fPCLK的1/2、1/4、1/8、1/16、1/32、1/64、1/128、1/256
  • 支持多主机模型和主从操作
  • 可精简为半双工/单工通信
  • 支持DMA传输
  • 兼容I2S协议

STM32F103C8T6芯片提供了SPI1和SPI2两个硬件SPI接口。SPI1挂载在APB2总线上,时钟频率为72MHz;SPI2挂载在APB1总线上,时钟频率为36MHz。

硬件SPI的寄存器配置主要包括:

  • LSB控制位:用于选择高位/低位先行
  • 接收/发送缓冲区(RDR/TDR):与串口共享同一地址DR
  • TXE标志位:表示发送缓冲区空
  • RXNE标志位:表示接收缓冲区非空

以下是使用STM32硬件SPI的代码示例:

#include "stm32f10x.h"                  // Device header

void MySPI_W_SS(uint8_t BitValue)
{
    GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}

void MySPI_Init(void)
{
    /* 第一步:开启SPI GPIO时钟 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);

    /* 第二步:初始化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;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* 第三步:配置SPI外设 */
    SPI_InitTypeDef SPI_InitStructure;
    SPI_InitStructure.SPI_Mode = SPI_Mode_Master;          // 指定当前设备为从机
    SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  // 双线全双工
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;      // 8位数据帧
    SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;     // 高位先行
    SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128;  // SCK的时钟频率 72Mhz/128
    SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;
    SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;           // 第一个边沿开始采样,可以理解为1Edge=0 2Edge=1
    SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;              // 软件实现NSS
    SPI_InitStructure.SPI_CRCPolynomial = 7;               // CRC校验,基本不用填默认值7

    SPI_Init(SPI1, &SPI_InitStructure);
    SPI_Cmd(SPI1, ENABLE);

    MySPI_W_SS(1);
}

void MySPI_Start(void)
{
    MySPI_W_SS(0);
}

void MySPI_Stop(void)
{
    MySPI_W_SS(1);
}

uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
    /* 第一步:等待TXE为1(发送寄存器为空) */
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);

    /* 第二步:软件写入至DR */
    SPI_I2S_SendData(SPI1, ByteSend);  // 自动转入移位寄存器自动发送

    /* 非连续发送,所以不必在TXE==1时立刻写入下一个数据到DR */

    /* 第三步:等待标志位RXNE */
    while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);

    /* 第四步:读取DR */
    return SPI_I2S_ReceiveData(SPI1);
    // 标志位TXE在写入DR时会自动被清除,读DR时RXNE会自动被清除
}

通过以上代码,可以实现STM32微控制器中SPI通信的硬件配置和数据传输功能。硬件SPI相比软件实现具有更高的性能和效率,适用于对通信速度要求较高的应用场景。

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