STM32_SPI

1、SPI简介

1.1 什么是SPI

        SPI,即Serial Peripheral Interface,串行外设接口。SPI是一种高速的、全双工、同步的串行通信总线;SPI采用主从方式工作,一般有一个主设备和一个或多个从设备;SPI需要至少4根线,分别是MISO(主设备输入,从设备输出)、MOSI(主设备输出,从设备输入)、SCK(时钟)、SS(从机选择)。

        相较于IIC而言,SPI的传输速度更快,SPI协议并没有严格规定最大传输速度,这个最大传输速度一般取决于芯片厂商的设计需求;而IIC由于上拉电阻的原因,电平由低转高时耗时更多,一般速度最快在400KHz。

        所有设备的SCK、MOSI、MISO分别连接在一起;主机另外引出多条SS控制线,分别连接各从机的SS引脚;输出引脚配置为推挽输出,输入引脚配置为浮空或上拉输入。

1.2 SPI外设

        TM32内部集成了硬件SPI收发电路,可以由硬件自动执行时钟生成、数据收发等功能,减轻CPU的负担。

        可配置8位/16位数据帧、高位先行/低位先行,一般都是8位数据帧、高位先行

        时钟频率: fPCLK / (2, 4, 8, 16, 32, 64, 128, 256)

        支持多主机模型、主或从操作

        可精简为半双工/单工通信

        支持DMA

        兼容I2S协议(音频传输协议)

        STM32F103C8T6 硬件SPI资源:SPI1、SPI2

1.3 极性和相位

        SPI总线有四种不同的工作模式,取决于极性(CPOL)和相位(CPHL)这两个因素。

        CPOL表示CLK空闲时的状态:CPOL=0,空闲时SCK为低电平;反之则为高电平。

        CPHL表示采样时刻:CPHL=0,每个周期的第一个时钟沿采样;CPHL=1,每个周期的第二个时钟沿采样。

2、SPI结构图

        以下结构图来自STM32F103xxx

        这里发送缓冲区就是TDR,接收缓冲区就是RDR,和串口那里一样,TDR和RDR占用同一个地址,统一叫做DR。如果我们需要连续发送一批数据,第一个数据写入到TDR,当移位寄存器没有数据移位时,TDR的数据会立刻转入移位寄存器,开始移位,这个转入时刻,会置状态寄存器的TXE为1,表示发送寄存器空,当我们检查TXE置1后,紧跟着,下一个数据,就可以提前写入到TDR里候着了,一旦上一个数据发完,下一个数据就可以立刻跟进,实现不间断的连续传输。然后移位寄存器这里,一旦有数据过来了,它就会自动产生时钟,将数据移出去,在数据移出的过程中,MISO的数据也会移入,一旦数据移出完成,数据移入也就完成了,这时移入的数据就会整体的从移位寄存器转入到接收缓冲区RDR,这个时刻会置状态寄存器的RXNE为1,表示接收寄存器非空,当我们检查RXNE置1后,就要尽快把数据从RDR读出来,在下一个数据到来之前,读出RDR,就可以实现连续接收。

        基本结构

SPI移位示意图

        从图中可以看出,SPI的数据收发,都是基于字节交换这个基本单元来进行的。当主机需要发送一个字节,并且同时需要接收一个字节时,就可以执行一下字节交换的时序,这要主机要发送的数据就跑到了从机,主机要从从机接收的数据,就跑到了主机,这就完成了发送同时接收的目的。

3、SPI时序

3.1 基本时序单元

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

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

        SS低电平选中,高电平代表未选中,那么在低电平期间就代表正在通信,下降沿是通信的开始,上升沿是通信的结束。

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

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

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

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

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

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

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

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

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

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

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

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

3.2 SPI时序

        发送指令:向SS指定的设备,发送指令(0x06)

         指定地址写:向SS指定的设备,发送写指令(0x02),随后在指定地址(Address[23:0])下,写入指定数据(Data)

        指定地址读:向SS指定的设备,发送读指令(0x03),随后在指定地址(Address[23:0])下,读取从机数据(Data)

4、示例代码

4.1 软件实现SPI

#include "stm32f10x.h"                  // Device header


//对SPI四个引脚的封装
//从机选择,写SS的引脚
void MySPI_W_SS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}

void MySPI_W_SCK(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}

void MySPI_W_MOSI(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
}

uint8_t MySPI_R_MISO(void)
{
	return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}

void MySPI_Init(void)
{
	//打开时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	//配置端口
	//先定义一个结构体变量
	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;	//速度50MHz
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;	//上拉输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;	
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	//速度50MHz
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	//引脚初始化之后的默认电平
	MySPI_W_SS(1);	//默认使用高电平,不选择从机
	MySPI_W_SCK(0);	//计划使用SPI模式0,所以默认是低电平
	
}

//写SPI的三个基本时序单元
//起始信号
void MySPI_Start(void)
{
	MySPI_W_SS(0);	//根据时序图将SS置低电平
}

//终止信号
void MySPI_Stop(void)
{
	MySPI_W_SS(1);	//根据时序图将SS置高电平
}

//交换一个字节
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	//定义一个字节,用于接收
	uint8_t i, ByteReceive = 0x00;
	
	for (i = 0; i < 8; i++)
	{
		//在SS下降沿之后开始交换字节,先有下降沿再有数据移出的动作
		MySPI_W_MOSI(ByteSend & (0x80 >> i));	//这里相当于通过掩码的模式来获取想要的位
		MySPI_W_SCK(1);	//写SCK为1,产生上升沿,上升沿时,从机会自动把MOSI的数据读走,主机的任务就是把从机刚才放到MISO的数据位读进来
		if (MySPI_R_MISO() == 1)
		{
			ByteReceive |= (0x80 >> i);	//这样就把最高位存在ByteReceive中了
		}
		MySPI_W_SCK(0);	//写SCK为0,产生下降沿,下降沿时,主机和从机移出下一位
		
		//该for循环中的内容还可以进行优化,用掩码的方式提取,好处是不会改变ByteSend本身,后面有需要还可以使用
		//用移位数据本身来操作,好处是提高了效率,但是ByteSend这个数据在移位过程中改变了

	}
	
	return ByteReceive;
}

4.2 硬件SPI读写

#include "stm32f10x.h"                  // Device header


//对SPI四个引脚的封装
//从机选择,写SS的引脚
//这里还是使用软件模拟
void MySPI_W_SS(uint8_t BitValue)
{
	GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}
	
	//以上代码替换成SPI外设的初始化
	//第一步,开启时钟和GPIO的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
	
	//第二步,初始化GPIO口,其中SCK和MOSI是由硬件外设控制的输出信号,所以配置为复用推挽输出,MISo是硬件外设的输入信号,设置为上拉拉输入,
		//因为输入设备可以有多个,所以不存在复用输入这个东西,SS是软件控制的输出信号,所以配置为通用推挽输出
	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;	//速度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_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	//速度50MHz
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;	//上拉输入
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;	//速度50MHz
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	//第三步,配置SPI外设
	SPI_InitTypeDef SPI_InitStructure;
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;	//选择SPI的模式,决定当前设备是SPI的主机还是从机
	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;	//时钟极性,这里选择的是模式0,空闲时低电平
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;	//时钟相位,这里选择第一个边沿采样,这个和SPI的四种模式有关
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;	//这里选择软件NSS
	SPI_InitStructure.SPI_CRCPolynomial = 7;	//CRC校验的多项式,这里填多少都可以,反正我们不用
	SPI_Init(SPI1, &SPI_InitStructure);
	
	//第四步,开关控制,调用SPI_Cmd,给SPI使能即可
	SPI_Cmd(SPI1, ENABLE);
	
	//开启spi之后,还要调用一下MySPI_W_SS,默认给SS输出高电平,默认是不选中从机的
	MySPI_W_SS(1);
}

//写SPI的三个基本时序单元
//起始信号
void MySPI_Start(void)
{
	MySPI_W_SS(0);	//根据时序图将SS置低电平
}

//终止信号
void MySPI_Stop(void)
{
	MySPI_W_SS(1);	//根据时序图将SS置高电平
}

//交换一个字节
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{
	//当调用这个交换字节的函数时,硬件的SPI外设就要自动控制SCK、MOSI、MISO这三个引脚来生成时序了
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET);	//等待TXE的标志位为1
	//写入数据至SPD的DR,就是TDR,要发送的数据
	//ByteSend传入到TDR中,之后ByteSend会自动转入移位寄存器,一旦移位寄存器有数据了,就会自动产生时序波形
	SPI_I2S_SendData(SPI1, ByteSend);
	//发送和接收是同步的,到接收完成的时候也就代表发送移位完成了,接收移位完成时,会收到一个字节数据,这时会置标志位RXNE
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET);	//等待RXNE的标志位为1,表示收到一个字节,同时也表示发送时序产生完成了
	//读取DR,从RDR中,把交换接收的数据读出来
	return SPI_I2S_ReceiveData(SPI1);
	
}
//注意事项1:这里的硬件SPI,必须是发送,同时接收,要想接收必须得先发送,因为只有你给TDR写数据,才会触发时序的生成
	//如果不发送只接收,那时序是不会动的
//注意事项2:TXE和RXNE标志位,在写入SPI_DR时,TXE标志被清除,在读SPI数据寄存器可以清除RXNE标志位,所以按照上面程序的步骤就不用手动清除标志位了

相关推荐

  1. 15 STM32 - SPI

    2024-06-06 15:16:17       52 阅读
  2. stm32 - SPI

    2024-06-06 15:16:17       58 阅读
  3. <span style='color:red;'>STM</span><span style='color:red;'>32</span> <span style='color:red;'>SPI</span>

    STM32 SPI

    2024-06-06 15:16:17      41 阅读
  4. <span style='color:red;'>STM</span><span style='color:red;'>32</span>_<span style='color:red;'>SPI</span>

    STM32_SPI

    2024-06-06 15:16:17      24 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-06-06 15:16:17       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-06-06 15:16:17       100 阅读
  3. 在Django里面运行非项目文件

    2024-06-06 15:16:17       82 阅读
  4. Python语言-面向对象

    2024-06-06 15:16:17       91 阅读

热门阅读

  1. 什么是接地电阻柜呢?

    2024-06-06 15:16:17       29 阅读
  2. 解决QT QMessageBox 弹出需点击两次才能关闭问题

    2024-06-06 15:16:17       33 阅读
  3. delphi 3层源码

    2024-06-06 15:16:17       29 阅读
  4. react+vite创建

    2024-06-06 15:16:17       27 阅读
  5. 三生随记——理发店诡事

    2024-06-06 15:16:17       28 阅读
  6. 深入探索 Linux 命令:usermod

    2024-06-06 15:16:17       30 阅读
  7. ASP.NET第四章 Response、Request和Server对象

    2024-06-06 15:16:17       29 阅读
  8. linux学习:进程通信 管道

    2024-06-06 15:16:17       25 阅读
  9. LeetCode # 1070. 产品销售分析 III

    2024-06-06 15:16:17       31 阅读
  10. golang结构与接口方法实现与交互使用示例

    2024-06-06 15:16:17       31 阅读
  11. 设计模式之观察者模式

    2024-06-06 15:16:17       33 阅读
  12. Go语言 一些问题了解

    2024-06-06 15:16:17       31 阅读
  13. BMC压力测试脚本

    2024-06-06 15:16:17       32 阅读