1.前言
前面看B站中有些小伙伴吐槽F4的SPI+DMA没有硬件可控的CS引脚,那么今天我就来攻破这个问题
我这边暂时没有SPI的从机芯片,并且接收的过程与发送的过程类似,所以这里我就以发送的过程为例了。
2.理论
手册上给出了如下的描述
我们关注一下黑点的两行,这是DMA操作的核心,我们可以理解为TXE与DMA的触发挂钩,这样理解上与程序上都比较好写。
手册上还给出了DMA的触发流程,如下。
我们详细剖析一下TXE与DMA操作关联,可以看到每一次TXE变高,DMA就会进行一次搬运,直到通讯结束,这样一来我们就可以通过等待TXE置位来联动DMA。
除此之外,我们还需监控BSY位,等待TXE=1然后BSY=0后再关闭SPI,进而完成通讯
然后是DMA通道,本次实验我用的是DMA2的数据流3的通道3
3.程序
3.1 SPI初始化
void init_spi1(void)
{
RCC->AHB1ENR|=1<<1; //开启PB时钟
RCC->AHB1ENR|=1<<0; //开启PA时钟
RCC->APB2ENR|=1<<12; //开启SPI1时钟
#if SPI1_NSSMODE==0
init_spi1_nss1();
#else
GPIOA->MODER|=2<<8; //PA4功能复用
GPIOA->OSPEEDR|=2<<8; //端口速度50MHZ
GPIOA->PUPDR|=1<<8; //PA4上拉输出
GPIOA->AFR[0]|=5<<16; //功能复用到SPI1
#endif
GPIOA->MODER|=2<<10; //PA5功能复用
GPIOA->OSPEEDR|=2<<10; //端口速度50MHZ
GPIOA->PUPDR|=1<<10; //PA3上拉输出
GPIOA->AFR[0]|=5<<20; //功能复用到SPI1
GPIOA->MODER|=2<<12; //PA6功能复用
GPIOA->OSPEEDR|=2<<12; //端口速度50MHZ
GPIOA->PUPDR|=1<<12; //PA6上拉输出
GPIOA->AFR[0]|=5<<24; //功能复用到SPI1
GPIOA->MODER|=2<<14; //PA7功能复用
GPIOA->OSPEEDR|=2<<14; //端口速度50MHZ
GPIOA->PUPDR|=1<<14; //PA7上拉输出
GPIOA->AFR[0]|=5<<28; //功能复用到SPI1
SPI1->CR1&=~(1<<10); //全双工模式
#if SPI1_NSSMODE==0
SPI2->CR1|=1<<9; //软件控制nss
SPI2->CR1|=1<<8; //选择芯片上的引脚
#else
SPI1->CR2|=1<<2; //硬件控制NSS引脚
#endif
SPI1->CR1|=1<<2; //作为SPI主机
#if SPI1_DATALENGTH==8
SPI1->CR1&=~(1<<11); //数据长度为8位
#else
SPI1->CR1|=(1<<11); //数据长度为16位
#endif
#if SPI1_DMA_TX_EN==1
SPI1->CR2|=1<<1; //开启DMA传输
#else
SPI1->CR2&=~(1<<1); //开启DMA传输
#endif
SPI1->CR1|=1<<0; //从第二位开始采集数据
SPI1->CR1|=1<<1; //空闲状态下时钟保持高电平
SPI1->CR1|=SPI_SPEED_8<<3; //APB2上84MHz,8分频
SPI1->CR1&=~(1<<7); //先发送MSB,高位先发送
SPI1->I2SCFGR&=~(1<<11); //关闭I2S功能,使用SPI
}
说一下区别吧,很少,就一句话
SPI的CR2的第一位,解释如下
这里注意一下SPI的发送与接收是分开的,我们可以根据需要开启其中的DMA。
3.2 DMA初始化
//初始化DMA2 组3 通道3
//SPI1_TX
void init_DMA2_S3C3(unsigned char *SPIData,unsigned short SPIWEI)
{
DMA2_Stream3 ->CR = 0;//禁止数据流 ,才能写寄存器
//外设地址寄存器
//将所需寄存器的地址放入PAR寄存器
DMA2_Stream3 ->PAR = (unsigned int)(&SPI1->DR);
//数据流地址寄存器
//M1AR仅在双通道模式下有用
//将数据所在地址给M0AR寄存器
DMA2_Stream3 ->M0AR = (unsigned int)(SPIData);
DMA2_Stream3 ->NDTR = SPIWEI; // 一次传输数量
DMA2_Stream3 ->FCR = 0x21; //FIFO所有配置失效
DMA2_Stream3 ->CR |= 1<< 6; //储存器到外设模式
//循环模式:
//当NDTR寄存器减到0时自动重装
//单次模式(普通模式):
//NDTR减到0后停止DMA
DMA2_Stream3 ->CR &=~(1<<8); //非循环模式
DMA2_Stream3 ->CR &=~(3<<11); //外设数据长度:8位
DMA2_Stream3 ->CR &=~(3<<13); //存储器数据长度:8位
DMA2_Stream3 ->CR &= ~(1<<9); //外设非增量模式
DMA2_Stream3 ->CR |= 1<<10; //存储器增量模式,指针增加,可用于传输数组
DMA2_Stream3 ->CR |= 1<<16; //中等优先级
//突发传输
//DMA占用CPU总线时间,此时CPU无法工作
//一个节拍:传输多少次32位变量
//应用场景:从ram里读出字节
DMA2_Stream3 ->CR &= ~(3<<21); //外设突发单次传输
DMA2_Stream3 ->CR &= ~(3<23); //存储器突发单次传输
DMA2_Stream3 ->CR |= 3<<25; //通道3
DMA2_Stream3 ->CR |= 1<<0; //使能数据流
}
没有什么特别的地方,和存储器去寄存器的操作方式一致。
3.3 发送
unsigned char SPI1_WR(unsigned char SPI1MODE,unsigned char SPI1Data)
{
unsigned char temp=0;
switch(SPI1MODE)
{
case SPI1_WRMODE:
//清除全部设置
SPI1->CR1&=~(1<<15);
SPI1->CR1&=~(1<<10);
#if SPI1_NSSMODE==0
#else
SPI1->CR1|=(1<<6); //开启SPI
#endif
while((SPI1->SR&1<<1)==0); //等待发送缓冲为空
SPI1->DR=SPI1Data; //发送数据
while((SPI1->SR&1<<0)==0); //等待接受缓冲为空
temp=SPI1->DR; //接受数据
while((SPI1->SR&1<<7)==1); //等待发送缓冲为空
#if SPI1_NSSMODE==0
;
#else
SPI1->CR1&=~(1<<6); //关闭SPI
#endif
break;
case SPI1_WOMODE:
#if SPI1_NSSMODE==0
#else
SPI1->CR1|=(1<<6); //开启SPI
#endif
SPI1->CR1&=~(1<<15); //清除模式设置
SPI1->CR1&=~(1<<10); //清除模式设置
while((SPI1->SR&1<<1)==0); //等待发送缓冲为空
#if SPI1_DMA_TX_EN==1
while((SPI1->SR&1<<1)==0); //等待发送缓冲为空
#else
while((SPI1->SR&1<<1)==0); //等待发送缓冲为空
SPI1->DR=SPI1Data; //发送数据
while((SPI1->SR&1<<1)==0); //等待发送缓冲为空
#endif
#if SPI1_NSSMODE==0
#else
#endif
while((SPI1->SR&1<<7)==1); //等待总线空闲
SPI1->CR1&=~(1<<6); //关闭SPI
break;
case SPI1_ROMODE:
SPI1->CR1&=~(1<<10);//清除模式设置
SPI1->CR1|=1<<10; //半双工模式只读
temp=SPI1->DR; //接受数据
break;
}
return temp;
}
这里稍微说说区别
核心在于两个TXE的判断
第一个TXE就是手册上的第一个判断
第二个也就是后面的,但是由于DMA的存在,所以下面无需我们再判断,当一个数据搬运完成,就会重新再次搬运直达搬运完所有数据TXE才会拉高,所以这里我们无需进行循环判断
4.测试
最终程序
spi.c
#include "spi.h"
void init_spi1(void)
{
RCC->AHB1ENR|=1<<1; //开启PB时钟
RCC->AHB1ENR|=1<<0; //开启PA时钟
RCC->APB2ENR|=1<<12; //开启SPI1时钟
#if SPI1_NSSMODE==0
init_spi1_nss1();
#else
GPIOA->MODER|=2<<8; //PA4功能复用
GPIOA->OSPEEDR|=2<<8; //端口速度50MHZ
GPIOA->PUPDR|=1<<8; //PA4上拉输出
GPIOA->AFR[0]|=5<<16; //功能复用到SPI1
#endif
GPIOA->MODER|=2<<10; //PA5功能复用
GPIOA->OSPEEDR|=2<<10; //端口速度50MHZ
GPIOA->PUPDR|=1<<10; //PA3上拉输出
GPIOA->AFR[0]|=5<<20; //功能复用到SPI1
GPIOA->MODER|=2<<12; //PA6功能复用
GPIOA->OSPEEDR|=2<<12; //端口速度50MHZ
GPIOA->PUPDR|=1<<12; //PA6上拉输出
GPIOA->AFR[0]|=5<<24; //功能复用到SPI1
GPIOA->MODER|=2<<14; //PA7功能复用
GPIOA->OSPEEDR|=2<<14; //端口速度50MHZ
GPIOA->PUPDR|=1<<14; //PA7上拉输出
GPIOA->AFR[0]|=5<<28; //功能复用到SPI1
SPI1->CR1&=~(1<<10); //全双工模式
#if SPI1_NSSMODE==0
SPI2->CR1|=1<<9; //软件控制nss
SPI2->CR1|=1<<8; //选择芯片上的引脚
#else
SPI1->CR2|=1<<2; //硬件控制NSS引脚
#endif
SPI1->CR1|=1<<2; //作为SPI主机
#if SPI1_DATALENGTH==8
SPI1->CR1&=~(1<<11); //数据长度为8位
#else
SPI1->CR1|=(1<<11); //数据长度为16位
#endif
#if SPI1_DMA_TX_EN==1
SPI1->CR2|=1<<1; //开启DMA传输
#else
SPI1->CR2&=~(1<<1); //开启DMA传输
#endif
SPI1->CR1|=1<<0; //从第二位开始采集数据
SPI1->CR1|=1<<1; //空闲状态下时钟保持高电平
SPI1->CR1|=SPI_SPEED_256<<3; //APB2上84MHz,8分频
SPI1->CR1&=~(1<<7); //先发送MSB,高位先发送
SPI1->I2SCFGR&=~(1<<11); //关闭I2S功能,使用SPI
}
unsigned char SPI1_WR(unsigned char SPI1MODE,unsigned char SPI1Data)
{
unsigned char temp=0;
switch(SPI1MODE)
{
case SPI1_WRMODE:
//清除全部设置
SPI1->CR1&=~(1<<15);
SPI1->CR1&=~(1<<10);
#if SPI1_NSSMODE==0
#else
SPI1->CR1|=(1<<6); //开启SPI
#endif
while((SPI1->SR&1<<1)==0); //等待发送缓冲为空
SPI1->DR=SPI1Data; //发送数据
while((SPI1->SR&1<<0)==0); //等待接受缓冲为空
temp=SPI1->DR; //接受数据
while((SPI1->SR&1<<7)==1); //等待发送缓冲为空
#if SPI1_NSSMODE==0
;
#else
SPI1->CR1&=~(1<<6); //关闭SPI
#endif
break;
case SPI1_WOMODE:
#if SPI1_NSSMODE==0
#else
SPI1->CR1|=(1<<6); //开启SPI
#endif
SPI1->CR1&=~(1<<15); //清除模式设置
SPI1->CR1&=~(1<<10); //清除模式设置
while((SPI1->SR&1<<1)==0); //等待发送缓冲为空
#if SPI1_DMA_TX_EN==1
while((SPI1->SR&1<<1)==0); //等待发送缓冲为空
#else
while((SPI1->SR&1<<1)==0); //等待发送缓冲为空
SPI1->DR=SPI1Data; //发送数据
while((SPI1->SR&1<<1)==0); //等待发送缓冲为空
#endif
#if SPI1_NSSMODE==0
#else
#endif
while((SPI1->SR&1<<7)==1); //等待总线空闲
SPI1->CR1&=~(1<<6); //关闭SPI
break;
case SPI1_ROMODE:
SPI1->CR1&=~(1<<10);//清除模式设置
SPI1->CR1|=1<<10; //半双工模式只读
temp=SPI1->DR; //接受数据
break;
}
return temp;
}
spi.h
#ifndef SPI_H__
#define SPI_H__
#include "stm32f4xx.h"
#define SPI_SPEED_2 0
#define SPI_SPEED_4 1
#define SPI_SPEED_8 2
#define SPI_SPEED_16 3
#define SPI_SPEED_32 4
#define SPI_SPEED_64 5
#define SPI_SPEED_128 6
#define SPI_SPEED_256 7
//定义空闲状态下的时钟状态,为1则是高电平,否则是低电平
#define SPI1_CPOL 1
//定义数据长度
#define SPI1_DATALENGTH 8
#define SPI1_NSS1UP do{GPIOB->ODR|=1<<12;}while(0)
#define SPI1_NSS1DOWN do{GPIOB->ODR&=~(1<<12);}while(0)
//是否软件管理NSS引脚
//0 软件管理
//1 硬件管理
#define SPI1_NSSMODE 1
//是否开启SPI1发送的DMA功能
//0 关闭
//1 开启
#define SPI1_DMA_TX_EN 1
//是否开启SPI1接收的DMA功能
//0 关闭
//1 开启
#define SPI1_DMA_RX_EN 0
//SPI2通信模式
//0 全双工通信
//1 只发送
//2 只接收
#define SPI1_WRMODE 0
#define SPI1_WOMODE 1
#define SPI1_ROMODE 2
#endif
DMA
//初始化DMA2 组3 通道3
//SPI1_TX
void init_DMA2_S3C3(unsigned char *SPIData,unsigned short SPIWEI)
{
DMA2_Stream3 ->CR = 0;//禁止数据流 ,才能写寄存器
//外设地址寄存器
//将所需寄存器的地址放入PAR寄存器
DMA2_Stream3 ->PAR = (unsigned int)(&SPI1->DR);
//数据流地址寄存器
//M1AR仅在双通道模式下有用
//将数据所在地址给M0AR寄存器
DMA2_Stream3 ->M0AR = (unsigned int)(SPIData);
DMA2_Stream3 ->NDTR = SPIWEI; // 一次传输数量
DMA2_Stream3 ->FCR = 0x21; //FIFO所有配置失效
DMA2_Stream3 ->CR |= 1<< 6; //储存器到外设模式
//循环模式:
//当NDTR寄存器减到0时自动重装
//单次模式(普通模式):
//NDTR减到0后停止DMA
DMA2_Stream3 ->CR &=~(1<<8); //非循环模式
DMA2_Stream3 ->CR &=~(3<<11); //外设数据长度:8位
DMA2_Stream3 ->CR &=~(3<<13); //存储器数据长度:8位
DMA2_Stream3 ->CR &= ~(1<<9); //外设非增量模式
DMA2_Stream3 ->CR |= 1<<10; //存储器增量模式,指针增加,可用于传输数组
DMA2_Stream3 ->CR |= 1<<16; //中等优先级
//突发传输
//DMA占用CPU总线时间,此时CPU无法工作
//一个节拍:传输多少次32位变量
//应用场景:从ram里读出字节
DMA2_Stream3 ->CR &= ~(3<<21); //外设突发单次传输
DMA2_Stream3 ->CR &= ~(3<23); //存储器突发单次传输
DMA2_Stream3 ->CR |= 3<<25; //通道3
DMA2_Stream3 ->CR |= 1<<0; //使能数据流
}
我们在主程序里如何使用呢?首先初始化SPI,然后是DMA,最后触发传输即可。这里我传输5个数据0x01,0x02,0x04,0x01最后一位应该是00
unsigned char spi_test_data[5]={0x01,0x02,0x04,0x01};
init_spi1();//初始化SPI1
init_DMA2_S3C3(spi_test_data,5);//初始化DMA
SPI1_WR(SPI1_WOMODE,5);//发送
可以看到效果拔群啊,CS管脚也没问题。
5.结语
至此完整的SPI完全出来了,手册上说这样的效果可以实现SPI的最高速率,但是我没有测试过。刚刚看手册的时候发现DMA有乒乓功能,嗯?难道这样一来速率还能在高?那么还是老样子有问题评论区见,我们下篇文章见。