前言

之前已经介绍了STM32的ADC、DMA、EXTI、TIME、NVIC、USART以及普通IO模式,此系列笔者还打算写最后三个大的内容,分别是SPI通信、IIC通信以及看门狗,后面就看大家的需求了,需要什么可以留在评论区,本文首先来介绍SPI的有关知识。

SPI总线概述

在这里插入图片描述
在通信协议分类的介绍中,提到过SPI,它是一种同步 串行 全双工(也可半双工)通信协议,是最常用的板级通信总线。为什么要加总线作为它的定语呢,原因就是这个协议可以实现一主多从的通信,多个从机和主机通过SPI所需的信号线连接在一起,就拿之前的串口通信来说,串口通信是一主一从的通信方式,主机TX接从机的RX,主机的RX接从机的TX,除了共地以外还需要两个通信线;而SPI通信除了共地以外需要四个信号线进行传输,根据它同步串行全双工的特点,可以分析出它必然具有同步时钟线,以及两个串行数据线来实现全双工通信,至于第四个信号线,是用来判断具体与哪个从机进行通信的片选线。

名称 功能
MOSI 主输出从输入
MISO 主输入从输出
SCLK 同步时钟线
CS 片选线

SPI通信拓扑图

上面提到过,SPI是一种通信总线,这也意味着在一组信号线上可能存在多个从机,那么具体的连接方式拓扑图是怎么样的呢
1.一主一从
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

注意上图中的MOSI和MOSI分别用了DO和DI来表示,这里需要了解一下生产厂商常用的别称,如下所示:
MISO:SIMO、DOUT、DO、SDO或SO(在主机端);

MOSI:SOMI、DIN、DI、SDI或SI(在主机端);

CS:CE、NSS、CE或SSEL;

SCLK也可以是SCK;
2.一主多从:
如下图所示,就是一个主机与多个主机连接的一种方式,主机与各个从机直接共同连接在SCLK、MOSI、MISO上,除此之外,每个从机还单独有一个SS片选与主机连接,通信时,主机通过拉低对应从机的SS来选择通信对象。
在这里插入图片描述
除此之外还有一个菊花链的拓扑连接方式,这个大家可以去下面两篇博客中查看,关于SPI的详细介绍也可以查看这两篇大佬的分析,很透彻。

  1. SPI协议详解(图文并茂+超详细)http://t.csdn.cn/qaS7h
  2. 一文搞懂SPI通信协议http://t.csdn.cn/gTKQq

STM32的SPI通信

关于STM32的SPI通信,也有两种方案,这个在串口通信的时候我们也提到过,
方案一就是使用STM32集成好的SPI控制器,与之前使用USART一样,按照自己需求配置好对应的寄存器以及通信模式后就可以直接通过SPI控制器来实现收发,不需要编程实现底层的具体发送方式;
方案二使用IO口模拟SPI的时序,参照SPI通信的时序图,操作GPIO口实现数据的收发,模拟SPI的好处在于不必拘束于固定的管脚,随便一组管脚都可以,只是需要自己编写底层的发送函数,实现01010之类的发送。本文笔者会将两种方式都介绍一下。
首先,还是参照之前的模式,先介绍使用控制器实现的过程。

SPI的特性

首先还是来过一下STM32的SPI的特性,
第一,基于三条线的全双工同步传输,也就是常说的四线SPI,这里的三条线是不包括NSS片选线的;注意,还有一种三线SPI,这里的三线是缺少MOSI或者是MISO其中的一根,双方通过一个数据线进行数据交换,同一时间只能主发从收或者主收从发,这种三线SPI是一种半双工模式,有些类似后面会介绍的IIC的通信方式;
第二,STM32 SPI的数据帧可以是八位或者是16位数据,也就是说SPI控制器可以一次性发送16位数据或者8位数据,而我们之前的USART是只能发送8位或者9位数据;
第三,SPI控制器具有主从模式可以设置,也就是说,STM32上集成的SPI既可以做主机也可以作为从机,在使用过程中需要我们进行配置;
第四,SPI的最大通信频率是40MHZ,而fPCLK是84MHZ(由所挂接的时钟线决定),所以最大的通信速率是fPCLK/2,40MHZ的传输速率已经比IIC和USART快很多了。而且SPI控制器还可以使用快速模式的通信方式,将两个数据线同时作为传输,一般用不上,做个了解即可。
第五,SPI控制器有可编程的时钟极性与时钟相位,关于时钟极性与时钟相位,它们两个分别有两个状态,时钟极性有0与1两个状态,决定时钟是高有效还是低有效;而时钟相位决定的是第一个时钟边沿有效还是第二个时钟边沿有效,它们两两组合,构成了SPI的四种模式,
时钟极性CPOL
当时钟极性为0的时候,时钟线空闲状态是低电平状态
当时钟极性为1的时候,时钟线空闲状态是高电平状态
时钟相位CPHA
当时钟相位为0的时候,数据在第一个跳变沿被采样(数据采集)
当时钟相位为1的时候,数据在第二个跳变沿被采样
在这里插入图片描述

模式 CPOL(时钟极性) CPHA(时钟相位) 数据收发
模式0 0 0 空闲时时钟线为低电平,第一个时钟边沿采集数据
模式1 0 1 空闲时时钟线为低电平,第二个时钟边沿采集数据
模式2 1 0 空闲时时钟线为高电平,第一个时钟边沿采集数据
模式3 1 1 空闲时时钟线为高电平,第二个时钟边沿采集数据

常用的是模式3与模式0,而且一般来说支持模式0的器件也支持模式3的通信方式(上升沿写入数据,下降沿读取数据),支持模式1的也支持模式2的通信方式(下降沿读取数据,上升沿写入数据)。
第六,SPI控制器也有对应的状态位可以产生发送完成和接收完成的标志,而且其传输是高位先发还是低位先发都可以进行编程控制。
在这里插入图片描述

SPI控制器的框图

在了解了SPI控制器的相关特性后,接下来就是它的框图介绍了,其整体框图如下图所示,不难发现,这个框图的整体不是很复杂,我们来稍作拆分介绍一下。
在这里插入图片描述

引脚

首先是左边的四个GPIO口,它们各自的功能如下图所示:
在这里插入图片描述
这里注意的NSS片选,我们在使用过程过程中一般禁止此处的NSS,用内部的软件管理来屏蔽这个NSS,用另外一个GPIO来专门操作控制从机,在需要通信时拉低对应的管脚即可。
在这里插入图片描述

数据收发过程

当使用四线SPI实现收发时,主机数据发送过程如下图中绿色线的流程,首先由MCU将数据写入发送缓冲区,然后再将数据由发送缓冲区并行转移到移位寄存器,根据LSB的设置,看是低位先发还是高位先发,然后再由移位寄存器一位一位的将数据发送到MOSI管脚上
主机数据接收过程,如下图红色线所示,由MISO输入数据,然后进入移位寄存器,接收完毕后经过移位寄存器并行转移到接收缓冲区,然后CPU在接收缓冲区中读取数据。
注:

  1. 下图中蓝色框里面还有两个箭头,一个是从MOSI指向MISO的,另一个是从MISO指向MOSI的,而且看起来是受到主控制逻辑的管理的,这里的两个箭头有两个作用:(1)使用三线SPI时,会舍弃一个数据传输脚,使用MOSI或者MISO中的任意一个,此时这一个数据脚既要发送又要接收,就需要使用到蓝色框内的箭头;(2)使用更快的传输模式时,会使用两个数据脚同时由主机向从机或者由从机向主机写数据,这个时候也需要使用到蓝色框内的箭头。
  2. 橙色框的NSS片选本来是SPI控制器内部集成的片选脚,但是我们实际使用过程一般不用,所以在配置过程中会使用软件管理来屏蔽掉它,让它作为一个普通的GPIO口;而实际的片选脚会根据硬件连接的管脚进行配置。
    在这里插入图片描述
    类似下图,假设此时使用三线SPI的半双工模式,只有MOSI一个脚,此时数据数据输入时就需要使用到上图蓝色框内的箭头,变成下图橙色线的流程。而输出方式不变,同样的,只留下MISO亦是如此。
    在这里插入图片描述

时钟以及控制部分

首先来看时钟部分,作为主机使用时,STM32的SPI控制器需要提供Sclk时钟信号,其产生方式就是下图的波特率发生器,对于这个波特率发生器在框图中可以看出留给我们操作的只有CR1寄存器的BR[2:0]共三位,按照前面的介绍以及以前的经验,这三位肯定是用来空时分频系数的,也就是决定通信速率的,在特性那里我们提到了,SPI的最大通信速率是40Mhz,而MCU的晶振频率大于40MHZ,也就是说,此处的分频估计最少也是一个2分频。
在这里插入图片描述
然后是主控制逻辑,主控制逻辑部分就是关于模式,单个数据线还是两个数据线,主模式还是从机模式、是否只读这些进行选择。具体的在编程手册查看寄存器SPI_CR1寄存器介绍。
最后是通信控制,通信控制部分首先是最右边橙色框内有一个二选一数据选择器,其中一路输入来自上面提到得NSS,另外的一路输入来自SSI,而控制脚是SSM。当SSM置一时,系统会选择SSI,也就是内部从器件选择,从而屏蔽控制器绑定的指定CS GPIO。
在这里插入图片描述
然后通讯控制还有输出上方蓝色框的各种标志位,发送完成以及接收完成这些,主要用于中断或者DMA关于这个详细的在SPI_CR2寄存器的介绍。这里仅需要知道有这个东西即可。

SPI寄存器简介

1.SPI 控制寄存器 1 (SPI_CR1)(不用于 I2S 模式)
写法:SPIX->CR1
位 0 CPHA:时钟相位 (Clock phase)
位1 CPOL:时钟极性 (Clock polarity)
位 2 MSTR:主模式选择 (Master selection) 选择主模式
位 5:3 BR[2:0]:波特率控制 (Baud rate control) 选2分频
位 6 SPE:SPI 使能 (SPI enable) 放在最后
位 7 LSBFIRST:帧格式 (Frame format) 一般选择高位先发
位8 SSI: 内部从器件选择
位 9 SSM:软件从器件管理 (Software slave management)
位9:8 设置软件管理。什么是软件管理?
NSS引脚属于硬件SPI 片选引脚。把NSS引脚设置为软件管理之后,这个引脚相当于当作普通IO口使用。为什么要把它设置成普通IO口使用。因为当SPI总线外接多个从机时,是通过片选引脚进行选择。把NSS引脚设置为软件管理之后,片选引脚就可以接任意一个IO口都可以,这就方便很多了。
把位9 和 位8 都 设置为1,就是把NSS设置为软件管理。

位 10 RXONLY:只接收 (Receive only) 全双工
在这里插入图片描述
位 11 DFF:数据帧格式 数据位
位 15 BIDIMODE:双向通信数据模式使能 (Bidirectional data mode enable)
选择3线制SPI 还是4线制的SPI 结合硬件
2.SPI 控制寄存器 2 (SPI_CR2)
写法:SPIx->CR2
主要是中断使能和DMA使能

3.SPI 状态寄存器 (SPI_SR)
写法:SPIx->SR
位 0 RXNE:接收缓冲区非空 (Receive buffer not empty)
位 1 TXE:发送缓冲区为空 (Transmit buffer empty)

4.SPI 数据寄存器 (SPI_DR)
写法:SPIx->DR
发送数据SPI1->DR=data
接收数据data = SPI1->DR
如果是8位数据位,则数据寄存器的低8位为有效位
如果使用16为数据为,则整个DR为有效位

SPI初始化代码流程

根据上面的介绍即可总结出SPI控制器的初始化流程
伪代码:

SPI初始化
{
  /*IO口控制器配置*/
	//端口时钟使能
	//端口模式配置
	//具体复用功能配置
 	//输出类型配置
    //输出速度配置
	/*SPI1控制器配置*/
	//SPI1模块时钟使能
	//CR1
	//双向单向 全双工
	//8位数据帧
	//全双工
	//软件从器件管理//
	//主机模式
	//先发高位
	//4分频    
	//主配置
	//0.0模式
	//CR2
	//MOT模式,一般都选择摩托罗拉的模式,而不选择TI的模式
	//CFGR
	//SPI模式  选择为SPI模式,而非I2S
	//SPI使能
}

SPI数据收发函数
{
  //等待发送

  发送数据
 
  //等待接收
  接收数据
}

SPI初始化代码

/*******************************************
*函数名    :spi1_init
*函数功能  :SPI1初始化配置
*函数参数  :无
*函数返回值:无
*函数描述  :
SCK------PB3   //复用输出 
MISO-----PB4   //复用输出
MOSI-----PB5   //复用输出
*********************************************/
void Spi1_Init(void)
{
	/*IO口控制器配置*/
	//端口时钟使能
	RCC->AHB1ENR |= (1<<1);   //B组时钟使能
	//端口模式配置
	GPIOB->MODER &= ~((3<<6)|(3<<8)|(3<<10));
	GPIOB->MODER |= ((2<<6) |(2<<8)| (2<<10));
	//具体复用功能配置
	GPIOB->AFR[0] &= ~((15<<12)|  (15<<16) |  (15<<20));
	GPIOB->AFR[0] |= ((5<<12)|  (5<<16)|(5<<20));
  //输出类型配置
	GPIOB->OTYPER &= ~((1<<3) | (1<<4) | (1<<5));
  //输出速度配置
	GPIOB->OSPEEDR &= ~((3<<6)| (3<<8) | (3<<10));
	GPIOB->OSPEEDR |= ((2<<6)| (2<<8) | (2<<10));   //50M
	
	/*SPI1控制器配置*/
	//SPI1模块时钟使能
	RCC->APB2ENR |= (1<<12);//SPI1时钟使能
	//CR1
	SPI1->CR1 &= ~(1<<15);    //双线单向
//	SPI1->CR1 |= (1<<15);    //单线双向
	SPI1->CR1 &= ~(1<<11);    //8位数据帧
	SPI1->CR1 &= ~(1<<10);    //全双工
	SPI1->CR1 |= (1<<9);      //软件从器件管理//
	SPI1->CR1 |= (1<<8);      //主机模式
	SPI1->CR1 &= ~(1<<7);     //先发高位
	SPI1->CR1 |= (1<<3);      //4分频    
	SPI1->CR1 |= (1<<2);      //主机模式
	SPI1->CR1 &= ~(3<<0);     //清零(0.0模式)模式0
	SPI1->CR1 |= (3<<0);     //1.1模式模式3
	//CR2
	SPI1->CR2 &= ~(1<<4);     //MOT模式,一般都选择摩托罗拉的模式,而不选择TI的模式
	//CFGR
	SPI1->I2SCFGR &= ~(1<<11);  //选择为SPI模式,而非I2S
	
	//SPI使能
	SPI1->CR1 |= (1<<6);
}


/*******************************************
*函数名    :Spi_Send_Data
*函数功能  :SPI1传输一个字节函数
*函数参数  :u8 data
*函数返回值:u8
*函数描述  :
			发送数据时候只需要关注参数
            接收数据的时候,关注返回值,
								参数随便传一个数值
*********************************************/
u8 Spi_Send_Data(u8 data)
{
	u8 val;
	
	/*发送*/
	//等待之前的数据发送完成
	while(!(SPI1->SR & (1<<1)));
	//将要发送的数据赋值给DR
	SPI1->DR = data;

	/*接收*/
	//等待有数据就接收
	while(!(SPI1->SR & (1<<0)));
	//将DR数据赋值给变量
	val = SPI1->DR;
	
	return val;
}

SPI使用IO模拟的代码思路

找到对应的硬件设备接口
LCD_SPI2_MOSI   ---- 发送数据  PB15
LCD_SPI2_SCLK                 PB13
LCD_SPI2_MISO--------并不是屏幕上面没有这根线,而是没有使用到所以没有接
//注意使用的是IO口模拟来实现的功能,所以GPIO口需要配置为推挽输出。
SPI的初始化
{
  //打开时钟   PB
  //配置IO控制器
  //PB13   PB15通用推挽输出
}

SPI数据收发函数
{
  u8 buff;
  时钟线拉低
  数据线输出拉高
  
  for(循环八次)
  {
     //如何发送
时钟线拉低;   //控制发送,下降沿发送
     If(data & 0x80>>i)
     {
       数据线输出拉高
     }
     Else
     {
        数据输出拉低
}
 时钟线拉高;   //接收    上升沿读取 
}
}

由于笔者使用的屏幕不需要给主机反馈数据,所以这里少配置了一个MISO的管脚,且这里的GPIO初始化使用了GPIO的库函数来实现。


/*******************************************
*函数名    :Spi_Gpio_Init
*函数功能  :GPIO模拟SPI的初始化配置
*函数参数  :无
*函数返回值:无
*函数描述  :将GPIO配置为通用推挽输出,模拟产生SPI的时序
*********************************************/
void Spi_Gpio_Init(void)
{
	RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB,ENABLE);
	
	//IO控制器
	GPIO_InitTypeDef gpio_InitTypeDef; //定义了一个结构体变量
	
	gpio_InitTypeDef.GPIO_Mode = GPIO_Mode_OUT;           //通用输出模式
	gpio_InitTypeDef.GPIO_OType = GPIO_OType_PP;          //推挽输出
	gpio_InitTypeDef.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_15;  //一起使用的前提条件 必须是同一个端口
	gpio_InitTypeDef.GPIO_PuPd = GPIO_PuPd_NOPULL;        //无上下拉
	gpio_InitTypeDef.GPIO_Speed = GPIO_Medium_Speed;      //中速
	
	GPIO_Init(GPIOB,&gpio_InitTypeDef);
	

}

/*******************************************
*函数名    :LCD_GSend_Byte
*函数功能  :使用模拟SPI发送一个八位的数据到LCD
*函数参数  :无
*函数返回值:无
*函数描述  :模式0或者模式3
*********************************************/
void LCD_GSend_Byte(u8 data)
{
	u8 i;
	
	//空闲状态
	LCD_SCL_L;
	LCD_MOSI_H;

	//具体的发送过程
	for(i=0;i<8;i++)
	{
		LCD_SCL_L;   //发送数据
		if(data & (0x80>>i))
		{
			LCD_MOSI_H;  //发送数据1
		}
		else
		{
			LCD_MOSI_L;
		}
		LCD_SCL_H;   //拉高接收数据
	}
	//开始读取返回数据
}

总结

关于SPI的控制器实现以及GPIO,模拟实现的介绍就记录到这里,具体的实际使用,笔者抽空再单开一片,应该是LCD的显示或者是对W25Q64烧录GB2312的字库。想要那个大家可以私信或者留在评论区。然后文中如有不足欢迎批评指正。

M4系列目录

1.嵌入式学习笔记——概述
2.嵌入式学习笔记——基于Cortex-M的单片机介绍
3.嵌入式学习笔记——STM32单片机开发前的准备
4.嵌入式学习笔记——STM32硬件基础知识
5.嵌入式学习笔记——认识STM32的 GPIO口
6.嵌入式学习笔记——使用寄存器编程操作GPIO
7.嵌入式学习笔记——寄存器实现控制LED小灯
8.嵌入式学习笔记——使用寄存器编程实现按键输入功能
9.嵌入式学习笔记——STM32的USART通信概述
10.嵌入式学习笔记——STM32的USART相关寄存器介绍及其配置
11.嵌入式学习笔记——STM32的USART收发字符串及串口中断
12.嵌入式学习笔记——STM32的中断控制体系
13.嵌入式学习笔记——STM32寄存器编程实现外部中断
14.嵌入式学习笔记——STM32的时钟树
15.嵌入式学习笔记——SysTick(系统滴答)
16.嵌入式学习笔记——M4的基本定时器
17.嵌入式学习笔记——通用定时器
18.嵌入式学习笔记——PWM与输入捕获(上)
19.嵌入式学习笔记——PWM与输入捕获(下)
20.嵌入式学习笔记——ADC模数转换器
21.嵌入式学习笔记——DMA
22.嵌入式学习笔记——SPI通信
23.嵌入式学习笔记——SPI通信的应用
24嵌入式学习笔记——IIC通信

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐