STM32标准库学习笔记(十)SPI
前言
学习永无止境!本篇是嵌入式开发之片上外设SPI,了解基本硬件原理以及通信协议。
注:本文章为学习笔记,部分图片与文字来源于网络/江协科技课程/手册,如侵权请联系!谢谢!
一、SPI通信概述
1.1 SPI基本概念
SPI(Serial Peripheral Interface)是由摩托罗拉公司提出的通信协议,是一种高速全双工、同步串行通信总线,主要应用在Flash、ADC、编码器等要求速率较高的场合。
1.2 SPI物理层
①信号线:
- SCLK(Serial Clock):时钟线,用于数据同步,由主机产生,决定了通信速率,不同设备的之间的通信速率受限与低速设备;
- MOSI(Master Output,Slave Input):主出从进,主设备发送数据,从设备接收数据线,数据方向由主机向从机;
- MISO(Master Input,Slave Output):主进从出,主设备接收数据,从设备发送数据线,数据方向由从机到主机;
- CS(Chip Select):片选线,也叫SS(Slave Select),用来寻址,指定要通信设备,一般有几个从设备,就要有几根CS。
②电平:
TTL电平,时钟极性(CPOL)与时钟相位(CPHA)的的选择,决定数据是在时钟上升沿采样还是下降沿采样,共四种模式,下面具体介绍。
1.3 SPI协议层
①基本时序:
- 起始信号:CS信号由高变低,表示选中从机,相应从机检测到起始信号,知道被主机选中;
- 数据发送与接收:数据高位先行(MSB),双方数据在时钟上升沿或下降沿发送(接收);
- 停止信号:CS信号由低变高,表示通信结束。
②SPI模式决定因素:
- CPOL:时钟极性,SCLK时钟线在空闲状态是高电平还是低电平由此决定,当CPOL=0时,SCLK空闲低电平。当CPOL=1时,SCLK空闲状态高电平;
- CPHA:时钟相位,数据的采样时刻,当CPHA=0时,MOSI或MISO数据线上的信号将会在时钟线的奇数边沿被采样。当CPHA=1时,MOSI或MISO数据线上的信号将会在时钟线的偶数边沿被采样。
③SPI四种通信模式:
- CPHA=0,CPOL=0:数据在时钟奇数边沿采样,时钟默认空闲低电平;
- CPHA=0,CPOL=1:数据在时钟奇数边沿采样,时钟默认空闲高电平;
- CPHA=1,CPOL=0:数据在时钟偶数边沿采样,时钟默认空闲低电平;
- CPHA=1,CPOL=1:数据在时钟偶数边沿采样,时钟默认空闲高电平;
二、STM32的SPI外设
2.1 基本简介
- 主从机选择:可作为SPI通讯主机,也可作为从机;
- 时钟频率:最大SCLK的频率为fpclk/2(f103的fpclk1为36MHZ,fpclk2为76MHZ);
- 数据格式:可选MSB先行,也可选LSB先行;
- 模式:除了支持正常双线全双工的四种模式,还有双向单线(同时向一个方向传输,速度提高一倍)以及单线模式(减少硬件接线);
2.2 硬件基本结构框图
- ①外部引脚:四个通信口与GPIO口的对应关系,需要查表;
- ②时钟控制:根据控制寄存器CR1BR[0:2],对fpclk进行分频;
- ③数据控制:数据移位寄存器以发送缓冲区作为数据源,把数据一位一位发送,当从外部接收数据时,数据移位寄存器把数据线数据采样一位位存储到接收缓冲区。数据帧长度设定GFF位可为8或16位,配置LSBFIRST,可选择MSB先行或LSB先行;
- ④主控:通过改变CR1/2的参数,可以配置SPI模式、波特率、LSB/MSB、主从模式、单双向模式,状态寄存器SR实时反馈SPI工作状态,除此之外还有中断、DMA请求等控制。
2.3 主机发送与接收
- ①CS片选:当要进行通信,首先选择从机地址,CS由高电平置为低电平;
- ②SCLK:CS片选同时时钟进行工作;
- ③数据发送:MOSI把发送缓冲区数据一位位传输出去,当发送完一帧数据,SR置TXE标志1(表示发送缓冲区已清空),若要再次发送数据,在TXE为1时,写入DR数据即可;
- ④数据接收:MISO移位寄存器把数据线数据采样一位位存储到接收缓冲区,当接收完一帧数据,SR置RXNE标志1(表示接收缓冲区非空),即可读取DR数据,每次等待RXNE标志位1,即可读取接收数据。
三、应用
3.1 软件模拟SPI
①本次示例说明:使用软件模拟CS、SCLK、MOSI、MISO,利用I/O高低电平翻转实现SPI时序。
配置CS\SCLK\MOSI为推挽输出(PA4\5\7),MISO为上拉输入(PA6),选用模式1(SCLK初始为低电平,数据在奇数边沿采样)。
②配置步骤:
- 开启RCC时钟:开启GPIOA外设时钟;
- 配置GPIO:CS(GPIOA_Pin4)、SCLK(GPIOA_Pin5)、MOSI(GPIOA_Pin7)推挽输出,MISO(GPIOA_Pin6)上拉输出;
- 起始信号;
- 停止信号;
- 发送字bit;
- 接收bit;
- 发送与接收字节:通过循环移位进行写入与读取,将数据从高位依次取出放在发送地址,每取一位,置SCLK位1,并读取MISO电平信号,读取完毕,置SCLK为0。
③代码实战:
MSPI.c:
#include "stm32f10x.h" // Device header
/*设置CS电平*/
void MySPI_W_CS(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}
/*写SCLK*/
void MySPI_W_SCK(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue);
}
/*发送MOSI电平*/
void MySPI_W_MOSI(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue);
}
/*接收MISO电平*/
uint8_t MySPI_R_MISO(void)
{
return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6);
}
/*初始化引脚配置*/
void MySPI_Init(void)
{
/*开启GPIOA外设时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//将PA4、PA5和PA7引脚初始化为推挽输出
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);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;//将PA6引脚初始化为上拉输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/*设置默认电平*/
MySPI_W_CS(1);//CS默认高电平
MySPI_W_SCK(0);//SCLK默认低电平
}
/*协议层*/
/*起始信号*/
void MySPI_Start(void)
{
MySPI_W_CS(0);
}
/*结束信号*/
void MySPI_Stop(void)
{
MySPI_W_CS(1);
}
/*发送接收数据字节*/
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);//拉低SCLK,下降沿移入数据
}
return ByteReceive;//返回接收到的一个字节数据
}
3.2 硬件SPI
①本次示例说明:使用STM32硬件SPI配置,选用SPI1外设,配置时钟极性为低电平,相位为上升沿采样。
②配置步骤:
- 开启RCC时钟:开启GPIOA、SPI1外设时钟;
- 配置GPIO:CS(GPIOA_Pin4)为推挽输出,SCLK(GPIOA_Pin5)、MOSI(GPIOA_Pin7)复用推挽输出,MISO(GPIOA_Pin6)上拉输出;
- 起始信号;
- 停止信号;
- 发送与接收字节:等待TXE为空,写入字节,等待RXNE非空,读取字节。
③代码实战:
MSPI.c:
#include "stm32f10x.h" // Device header
/*设置CS*/
void MySPI_W_CS(uint8_t BitValue)
{
GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue);
}
/*初始化SPI*/
void MySPI_Init(void)
{
/*开启GPIOA\SPI1的外设时钟*/
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;//将PA4引脚初始化为推挽输出
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;//将PA5和PA7引脚初始化为复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;//将PA6引脚初始化为上拉输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
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);
/*设置默认电平*/
MySPI_W_CS(1);
}
/*起始信号*/
void MySPI_Start(void)
{
MySPI_W_CS(0);
}
/*结束信号*/
void MySPI_Stop(void)
{
MySPI_W_CS(1);
}
/**
* 函 数:SPI交换传输一个字节,使用SPI模式0
* 参 数:ByteSend 要发送的一个字节
* 返 回 值:接收的一个字节
*/
uint8_t MySPI_SwapByte(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);//读取接收到的数据并返回
}
待续...
原文地址:https://blog.csdn.net/m0_70655689/article/details/145065748
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!