STM32主从通信:使用SPI协议实现
目录
前言
这是一篇学习笔记,记录自己学习SPI通信。方便之后运用的时候回顾。参考《STM32中文参考手册》
一、理论部分
SPI简介
SPI 协议是由摩托罗拉公司提出的通讯协议(Serial Peripheral Interface),即串行外围设
备接口,是一种高速全双工的通信总线。它被广泛地使用在
ADC
、
LCD
等设备与
MCU
间,
要求通讯速率较高的场合。
SPI特征
●
3
线全双工同步传输
● 带或不带第三根双向数据线的双线单工同步传输
●
8
或
16
位传输帧格式选择
● 主或从操作
● 支持多主模式
●
8
个主模式波特率预分频系数
(
最大为
f
PCLK
/2)
● 从模式频率
(
最大为
f
PCLK
/2)
● 主模式和从模式的快速通信
● 主模式和从模式下均可以由软件或硬件进行
NSS
管理:主
/
从操作模式的动态改变
● 可编程的时钟极性和相位
● 可编程的数据顺序,
MSB
在前或
LSB
在前
● 可触发中断的专用发送和接收标志
●
SPI
总线忙状态标志
● 支持可靠通信的硬件
CRC
─ 在发送模式下,
CRC
值可以被作为最后一个字节发送
─ 在全双工模式中对接收到的最后一个字节自动进行
CRC
校验
● 可触发中断的主模式故障、过载以及
CRC
错误标志
● 支持
DMA
功能的
1
字节发送和接收缓冲器:产生发送和接受请求
SPI物理层
SPI通信设备连接图
SPI框图
通常
SPI
通过
4
个引脚与外部器件相连:
●
MISO
:主设备输入
/
从设备输出引脚。该引脚在从模式下发送数据,在主模式下接收数据。
●
MOSI
:主设备输出
/
从设备输入引脚。该引脚在主模式下发送数据,在从模式下接收数据。
●
SCK
:串口时钟,作为主设备的输出,从设备的输入
●
NSS
:从设备选择。这是一个可选的引脚,用来选择主
/
从设备。它的功能是用来作为“片
选引脚”,让主设备可以单独地与特定从设备通讯,避免数据线上的冲突。从设备的
NSS
引脚可以由主设备的一个标准
I/O
引脚来驱动。一旦被使能
(SSOE
位
)
,
NSS
引脚也可以作为
输出引脚,并在
SPI
处于主模式时拉低;此时,所有的
SPI
设备,如果它们的
NSS
引脚连接
到主设备的
NSS
引脚,则会检测到低电平,如果它们被设置为
NSS
硬件模式,就会自动进
入从设备状态。当配置为主设备、
NSS
配置为输入引脚
(MSTR=1
,
SSOE=0)
时,如果
NSS
被拉低,则这个
SPI
设备进入主模式失败状态:即
MSTR
位被自动清除,此设备进入从模式
SPI协议层
数据时钟时序图
SPI配置过程
SPI主模式配置
1. 通过SPI_CR1寄存器的BR[2:0]位定义串行时钟波特率。
2. 选择CPOL和CPHA位,定义数据传输和串行时钟间的相位关系。
3. 设置DFF位来定义8位或16位数据帧格式。
4. 配置SPI_CR1寄存器的LSBFIRST位定义帧格式。
5. 如果需要NSS引脚工作在输入模式,硬件模式下,在整个数据帧传输期间应把NSS脚连接到高电平;在软件模式下,需设置SPI_CR1寄存器的SSM位和SSI位。如果NSS引脚工作在输出模式,则只需设置SSOE位。
6. 必须设置MSTR位和SPE位(只当NSS脚被连到高电平,这些位才能保持置位)。 在这个配置中,MOSI引脚是数据输出,而MISO引脚是数据输入。
SPI从模式配置
1.
设置
DFF
位以定义数据帧格式为
8
位或
16
位。
2.
选择
CPOL
和
CPHA
位来定义数据传输和串行时钟之间的相位关系。
为保证正确 的数据传输,从设备和主设备的CPOL
和
CPHA
位必须配置成相同的方式。
3.
帧格式
(SPI_CR1
寄存器中的
LSBFIRST
位定义的
”MSB
在前
”
还是
”LSB
在前
”)
必须与主设备相同。
4.
硬件模式下
(
参考从选择
(NSS)
脚管理部分
)
,在完整的数据帧
(8
位或
16
位
)
传输过程中,NSS引脚必须为低电平。在
NSS
软件模式下,设置
SPI_CR1
寄存器中的
SSM
位并清除
SSI 位。
5.
清除
MSTR
位、设置
SPE
位
(SPI_CR1
寄存器
)
,使相应引脚工作于
SPI
模式下。 在这个配置中,MOSI
引脚是数据输入,
MISO
引脚是数据输出。
SPI数据发送与接收过程
主模式通讯过程
从模式通讯过程
接收与发送缓冲器
在接收时,接收到的数据被存放在一个内部的接收缓冲器中;在发送时,在被发送之前,数据
将首先被存放在一个内部的发送缓冲器中。 对SPI_DR
寄存器的读操作,将返回接收缓冲器的内容;写入
SPI_DR
寄存器的数据将被写入发 送缓冲器中。
主模式下开始传输
● 全双工模式
─
当写入数据到
SPI_DR
寄存器
(
发送缓冲器
)
后,传输开始;
─
在传送第一位数据的同时,数据被并行地从发送缓冲器传送到
8
位的移位寄存器中,
然后按顺序被串行地移位送到
MOSI
引脚上;
─
与此同时,在
MISO
引脚上接收到的数据,按顺序被串行地移位进入
8
位的移位寄存器
中,然后被并行地传送到
SPI_DR
寄存器
(
接收缓冲器
)
中。
从模式下开始传输
● 全双工模式
─
当从设备接收到时钟信号并且第一个数据位出现在它的
MOSI
时,数据传输开始,随
后的数据位依次移动进入移位寄存器;
─
与此同时,在传输第一个数据位时,发送缓冲器中的数据被并行地传送到
8
位的移位
寄存器,随后被串行地发送到
MISO
引脚上。软件必须保证在
SPI
主设备开始数据传
输之前在发送寄存器中写入要发送的数据。
主或从模式下
全双工发送和接收过程模式
软件必须遵循下述过程,发送和接收数据
1.
设置
SPE
位为
’1’
,使能
SPI
模块;
2.
在
SPI_DR
寄存器中写入第一个要发送的数据,这个操作会清除
TXE
标志;
3.
等待
TXE=1
,然后写入第二个要发送的数据。等待
RXNE=1
,然后读出
SPI_DR
寄存器并获得第一个接收到的数据,读SPI_DR
的同时清除了
RXNE
位。重复这些操作,发送后续的数据同时接n-1
个数据;
4.
等待
RXNE=1
,然后接收最后一个数据;
5.
等待
TXE=1
,在
BSY=0
之后关闭
SPI
模块。
也可以在响应
RXNE
或
TXE
标志的上升沿产生的中断的处理程序中实现这个过程。
二、代码部分
我的想法是在串口助手上发送给主机什么,主机就发什么给从机。之后从机把接收的数据再返回给主机,最终再串口助手上打印出来。验证SPI的全双工通讯。主机和从机都采用中断的方式发送和接收。
主机代码
SPI配置代码
void SPI2_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
/* SPI的IO口和SPI外设打开时钟 */
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE );
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12; //PB12推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
/* SPI的IO口设置 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //PB13/14/15复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_SetBits(GPIOB,GPIO_Pin_12);
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //设置SPI工作模式:设置为主SPI
SPI_InitStructure.SPI_DataSize = SPI_DataSize_16b; //设置SPI的数据大小:SPI发送接收8位帧结构
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; //串行同步时钟的空闲状态为高电平
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; //串行同步时钟的第二个跳变沿(上升或下降)数据被采样
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; //定义波特率预分频的值:
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC值计算的多项式
SPI_Init(SPI2, &SPI_InitStructure); //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器
SPI_Cmd(SPI2, ENABLE); //使能SPI外设
}
串口中断部分
void USART1_IRQHandler(void) //串口1中断服务程序
{
u16 r,i;
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断
{
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
r =USART_ReceiveData(USART1);//(USART1->DR); //读取接收到的数据
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET);//等待发送区空
SPI_I2S_SendData(SPI2, r);
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET); //等待接收完一个byte
i = SPI_I2S_ReceiveData(SPI2);
GPIO_SetBits(GPIOB,GPIO_Pin_12);
USART_senddate(USART1,i) ;
}
}
从机代码
void SPI2_Init(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
SPI_InitTypeDef SPI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
/* SPI的IO口和SPI外设打开时钟 */
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB, ENABLE );
RCC_APB1PeriphClockCmd(RCC_APB1Periph_SPI2, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12; //PB12推挽输出
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //下拉输入
GPIO_Init(GPIOB, &GPIO_InitStructure);
/* SPI的IO口设置 */
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //PB13/14/15复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工
SPI_InitStructure.SPI_Mode = SPI_Mode_Slave; //设置SPI工作模式:设置为主SPI
SPI_InitStructure.SPI_DataSize = SPI_DataSize_16b; //设置SPI的数据大小:SPI发送接收8位帧结构
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; //串行同步时钟的空闲状态为高电平
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; //串行同步时钟的第二个跳变沿(上升或下降)数据被采样
SPI_InitStructure.SPI_NSS = SPI_NSS_Hard; //NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; //定义波特率预分频的值:波特率预分频值为256
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC值计算的多项式
SPI_Init(SPI2, &SPI_InitStructure); //根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器
SPI_Cmd(SPI2, ENABLE); //使能SPI外设
SPI_I2S_ITConfig(SPI2,SPI_I2S_IT_RXNE,ENABLE);//开启相关中断
//Usart1 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = SPI2_IRQn;//SPI2中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0;//抢占优先级0
NVIC_InitStructure.NVIC_IRQChannelSubPriority =2; //子优先级2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器、
}
u16 i;
void SPI2_IRQHandler(void)
{
//接收数据
if(SPI_I2S_GetITStatus(SPI2,SPI_I2S_IT_RXNE) != RESET)
{
i = SPI_I2S_ReceiveData(SPI2);
while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET);
SPI_I2S_SendData(SPI2,i);
USART_senddate(USART1, i);
}
}