前言

之前已经介绍了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通信