stm32 spi dma 双机通信 以及 字节错位问题

我用的芯片是stm32f103zet6,实现两块板主从机全双工通信。

主机发送几个字节给从机,然后主机读从机,从机把刚才收到的数据返回给主机。

两块板都是用的spi1,一块板下载主机代码,一块板下载从机代码,硬件连线连好后,进行全双工读写测试。

连线方式如下:

主机                  从机    

CS  (PA4)       CS (PA4)  

CLK (PA5)       CLK (PA5)

MISO(PA6)      MISO(PA6)

MOSI(PA7)      MOSI(PA7)

GND                GND

大家根据自己所用的芯片的数据手册进行连线即可。主从机的4条SPI线一一对应连线就行了,注意别忘了把两块板子共地。

我一开始没有用dma来做,也实现了主从机全双工通信,只是字节跟字节之前不是连续的,中间有间隔,这样无形之中降低了我的通信速率。我抓的波形如下图(18M通信速率,字节和字节之间间隔有2.5us的样子,后来我把代码精简了,不用库函数,直接用寄存器,也还是有一点几到两个us的间隔),所以应该不是软件问题。

STM32中文参考手册的第468页有一段对于连续和非连续的描述:

我读完也没有很理解,但是打算用DMA试一下。硬件连线方式同上边描述。

主机的片选配置成软件方式,主机发送数据之前,把片选拉低,发完数据之后,把片选拉高。从机的片选配置成硬件方式。主从机的CPOL和COHA要配置成一样。spi主从机通信时,时钟信号是由主机来提供,因此从机的通信速率可以不用管,配不配成和主机一样都可以。

SPI主从机配置代码如下:

void SPI1_master_configuration(void)
{	 
	RCC->APB2ENR |= 1<<2  ;               //PORTA时钟使能   
	RCC->APB2ENR |= 1<<12 ;               //SPI1时钟使能 
	
	GPIOA->CRL &= 0X000FFFFF ; 
	GPIOA->CRL |= 0XBBB00000 ;            //PA567复用    
	GPIOA->ODR |= 7<<5 ;                  //PA567上拉
	
	RCC->APB2RSTR |=  1<<12;              //SPI1 reset    
	RCC->APB2RSTR &= ~(1<<12);            //SPI1 reset ok.
	
	SPI1->CR1 |= 0<<10 ;                  //全双工模式	
	SPI1->CR1 |= 1<<9  ;                  //nss软件管理
	SPI1->CR1 |= 1<<8  ;                  //nss高电平 

	SPI1->CR1 |= 1<<2  ;                  //SPI主机
	SPI1->CR1 |= 0<<11 ;                  //数据格式:8bit	
  
	SPI1->CR1 |= 1<<1  ;                  //CPOL=1:空闲模式下SCK为高电平 
	SPI1->CR1 |= 1<<0  ;                  //CPHA=1:数据采样从第二个时钟沿开始 
	
	SPI1->CR1 |= 1<<3  ;                  //Fsck=Fcpu/4
	SPI1->CR1 |= 0<<7  ;                  //MSBfirst   
	
	SPI1->CR2 |= 1<<1  ;	              //发送缓冲区DMA使能
	SPI1->CR2 |= 1<<0  ;	              //接收缓冲区DMA使能
	
	SPI1->CR1 |= 1<<6  ;                  //SPI设备使能		 
	
	GPIOA->CRL &= 0XFFF0FFFF; 
	GPIOA->CRL |= 0X00030000;	          //PA4推挽输出 
    GPIOA->ODR |= (1<<4);  	              //PA4输出高电平
}


void SPI1_slaver_configuration(void)
{	 
	RCC->APB2ENR |= 1<<2  ;               //PORTA时钟使能   
	RCC->APB2ENR |= 1<<12 ;               //SPI1时钟使能 
	
	GPIOA->CRL &= 0X0000FFFF ; 
	GPIOA->CRL |= 0XBBBB0000 ;            //PA4567复用    
	GPIOA->ODR |= 7<<5 ;                  //PA567上拉	
		
	RCC->APB2RSTR |=  1<<12;              //SPI1 reset    
	RCC->APB2RSTR &= ~(1<<12);            //SPI1 reset ok.
	
	SPI1->CR1 |= 0<<10 ;                  //全双工模式	
    SPI1->CR1 &= ~(1<<9)  ;               //nss hard
	
	SPI1->CR1 &= ~(1<<2)  ;               //SPI slaver
	SPI1->CR1 |= 0<<11 ;                  //数据格式:8bit	
 
    SPI1->CR1 |= 1<<1 ;                   //CPOL=1:空闲模式下SCK为高电平 
	SPI1->CR1 |= 1<<0 ;                   //CPHA=1:数据采样从第二个时钟沿开始 
	SPI1->CR1 |= 1<<3  ;                  //Fsck=Fcpu/4  
	SPI1->CR1 |= 0<<7  ;                  //MSBfirst   
	
	SPI1->CR2 |= 1<<1  ;	              //发送缓冲区DMA使能
	SPI1->CR2 |= 1<<0  ;	              //接收缓冲区DMA使能
	
	SPI1->CR1 |= 1<<6  ;                  //SPI设备使能		 
} 

SPI主从机DMA配置代码如下:

void SPI1_master_DMA_Configuration( void )
{

    RCC->AHBENR |= 1<<0 ;                     //DMA1时钟使能

	/*------------------配置SPI1_RX_DMA通道Channel2---------------------*/

    DMA1_Channel2->CCR &= ~( 1<<14 ) ;        //非存储器到存储器模式
	DMA1_Channel2->CCR |=    2<<12   ;        //通道优先级高
	DMA1_Channel2->CCR &= ~( 3<<10 ) ;        //存储器数据宽度8bit
	DMA1_Channel2->CCR &= ~( 3<<8  ) ;        //外设数据宽度8bit
	DMA1_Channel2->CCR |=    1<<7    ;        //存储器地址增量模式
	DMA1_Channel2->CCR &= ~( 1<<6  ) ;        //不执行外设地址增量模式
	DMA1_Channel2->CCR &= ~( 1<<5  ) ;        //不执行循环操作
	DMA1_Channel2->CCR &= ~( 1<<4  ) ;        //从外设读

	DMA1_Channel2->CNDTR &= 0x0000   ;        //传输数量寄存器清零
	DMA1_Channel2->CNDTR = SPI_MAX_BUF_LEN ;  //传输数量设置为SPI_MAX_RX_LEN个

	DMA1_Channel2->CPAR = SPI1_DR_Addr ;      //设置外设地址
	DMA1_Channel2->CMAR = (u32) spi_rx_buf ;  //设置DMA存储器地址

	/*------------------配置SPI1_TX_DMA通道Channel3---------------------*/

	DMA1_Channel3->CCR &= ~( 1<<14 ) ;        //非存储器到存储器模式
	DMA1_Channel3->CCR |=    0<<12   ;        //通道优先级最低
	DMA1_Channel3->CCR &= ~( 3<<10 ) ;        //存储器数据宽度8bit
	DMA1_Channel3->CCR &= ~( 3<<8 )  ;        //外设数据宽度8bit
	DMA1_Channel3->CCR |=    1<<7    ;        //存储器地址增量模式
	DMA1_Channel3->CCR &= ~( 1<<6 )  ;        //不执行外设地址增量模式
	DMA1_Channel3->CCR &= ~( 1<<5 ) ;         //不执行循环操作
	DMA1_Channel3->CCR |=    1<<4    ;        //从存储器读

	DMA1_Channel3->CNDTR &= 0x0000   ;        //传输数量寄存器清零
	DMA1_Channel3->CNDTR = SPI_MAX_BUF_LEN ;  //传输数量设置为SPI_MAX_RX_LEN个
	
	DMA1_Channel3->CPAR = SPI1_DR_Addr ;      //设置外设地址
	DMA1_Channel3->CMAR = (u32)spi_tx_buf ;   //设置DMA存储器地址			 
}

void SPI1_slaver_DMA_Configuration( void )
{

    RCC->AHBENR |= 1<<0 ;                     //DMA1时钟使能

	/*------------------配置SPI1_RX_DMA通道Channel2---------------------*/

    DMA1_Channel2->CCR &= ~( 1<<14 ) ;        //非存储器到存储器模式
	DMA1_Channel2->CCR |=    2<<12   ;        //通道优先级高
	DMA1_Channel2->CCR &= ~( 3<<10 ) ;        //存储器数据宽度8bit
	DMA1_Channel2->CCR &= ~( 3<<8  ) ;        //外设数据宽度8bit
	DMA1_Channel2->CCR |=    1<<7    ;        //存储器地址增量模式
	DMA1_Channel2->CCR &= ~( 1<<6  ) ;        //不执行外设地址增量模式
	DMA1_Channel2->CCR &= ~( 1<<5  ) ;        //不执行循环操作
	DMA1_Channel2->CCR &= ~( 1<<4  ) ;        //从外设读

	DMA1_Channel2->CNDTR &= 0x0000   ;        //传输数量寄存器清零
	DMA1_Channel2->CNDTR = SPI_MAX_BUF_LEN ;   //传输数量设置为SPI_MAX_RX_LEN个

	DMA1_Channel2->CPAR = SPI1_DR_Addr ;      //设置外设地址,注意PSIZE
	DMA1_Channel2->CMAR = (u32) spi_rx_buf ;//设置DMA存储器地址,注意MSIZE

	/*------------------配置SPI1_TX_DMA通道Channel3---------------------*/

	DMA1_Channel3->CCR &= ~( 1<<14 ) ;        //非存储器到存储器模式
	DMA1_Channel3->CCR |=    0<<12   ;        //通道优先级最低
	DMA1_Channel3->CCR &= ~( 3<<10 ) ;        //存储器数据宽度8bit
	DMA1_Channel3->CCR &= ~( 3<<8 )  ;        //外设数据宽度8bit
	DMA1_Channel3->CCR |=    1<<7    ;        //存储器地址增量模式
	DMA1_Channel3->CCR &= ~( 1<<6 )  ;        //不执行外设地址增量模式
	DMA1_Channel3->CCR &= ~( 1<<5 ) ;         //不执行循环操作
	DMA1_Channel3->CCR |=    1<<4    ;        //从存储器读

	DMA1_Channel3->CNDTR &= 0x0000   ;        //传输数量寄存器清零
	DMA1_Channel3->CNDTR = SPI_MAX_BUF_LEN ;  //传输数量设置为SPI_MAX_RX_LEN个
	DMA1_Channel3->CPAR = SPI1_DR_Addr ;      //设置外设地址
	DMA1_Channel3->CMAR = (u32)spi_tx_buf ;   //设置DMA存储器地址

    DMA1_Channel2->CCR |= 1 << 0 ;            //开启DMA通道2
	DMA1_Channel3->CCR |= 1 << 0 ;            //开启DMA通道3
}

初始化:

void SPI1_DMA_Master_Init(void)
{
    g_spi_slave_flag = 0; 
	SPI1_master_configuration();
	SPI1_master_DMA_Configuration();
}
	
void SPI1_DMA_Slaver_Init(void)
{
    g_spi_slave_flag = 1;
	SPI1_slaver_configuration();
	SPI1_slaver_DMA_Configuration();
}

主机开始传输:

// start once dma transfer , spi master  . return 0:ok, return 1:error.
uint8_t SPI1_DMA_ReadWrite(u8 *tx_buf, u16 len, u8 *rx_buf)
{
	uint8_t ret = 0;
	uint16_t ucErrTime = 0;
	DMA1_Channel2->CCR &= ~(1 << 0) ;           //关闭DMA通道2
	DMA1_Channel3->CCR &= ~(1 << 0) ;           //开启DMA通道3

	GPIOA->ODR &= ~(1<<4);  	                //PA4 LOW  cs = 0;
	
	DMA1->IFCR = 0xfff ;					    //清除DMA传输完成标志

	DMA1_Channel2->CNDTR = len;                 //传输数量设置为LEN个
	DMA1_Channel3->CNDTR = len;                 //传输数量设置为LEN个
	
	DMA1_Channel2->CMAR =  (u32) rx_buf;       
	DMA1_Channel3->CMAR =  (u32) tx_buf;        //设置DMA存储器地址	

	DMA1_Channel2->CCR |= 1 << 0 ;              //开启DMA通道2
	DMA1_Channel3->CCR |= 1 << 0 ;              //开启DMA通道3

	//wait TCIF3 = 1  and TCIF2 = 1
    while( (( DMA1->ISR & (1<<9)  ) == 0 ) ||  (( DMA1->ISR & (1<<5)  ) == 0 ) )   
	{
		ucErrTime++;
		if(ucErrTime>SPI1_EV_TIMEOUT)  
		{
			ret = 1;
		}
		DelayTick_us(10);	
	}
	GPIOA->ODR |= (1<<4);  	                    //PA4输出高电平   cs = 1;
    return ret;
}

uint8_t SPI1_master_dma_transfer(uint8_t* tx_data, uint16_t tx_len, uint8_t* rx_data)
{
	uint8_t ret = 0;
	
	ret = SPI1_DMA_ReadWrite(tx_data, tx_len, rx_data);
	if(ret == 0)
	{
		DelayTick_ms(20);
		memset(rx_data,tx_len,0);
		ret = SPI1_DMA_ReadWrite(tx_data, tx_len, rx_data);
	}
	
    //for test
	if(memcmp(rx_data, tx_data,tx_len) == 0)
    {
	    LED2_ON();
		DelayTick_ms(500);	
		LED2_OFF();
	}
	else
	{
		LED3_ON();
	}
    //for test
	return ret;
}

从机开始传输:

uint8_t SPI1_slaver_dma_transfer( void )
{
	uint8_t ret = 1;
	u16 tmp_len = 0;
	u16 rx_len = 0;
	uint16_t ucErrTime = 0;
	
	if(g_spi_slave_flag == 1)
	{
		tmp_len = DMA1_Channel2->CNDTR;
		
		if(tmp_len != SPI_MAX_RX_LEN)
		{
			DelayTick_ms(10);
			if (tmp_len == DMA1_Channel2->CNDTR)
			{
				rx_len = SPI_MAX_RX_LEN - tmp_len;
				memcpy(spi_tx_buf, spi_rx_buf, rx_len);
				
				#ifdef SPI_DEBUG
				USART2_Write(spi_rx_buffer.data, rx_len);
				#endif
				
				if(rx_len == SPI_MAX_RX_LEN)
				{
					//wait TCIF3 = 1  and TCIF2 = 1
                    while( (( DMA1->ISR & (1<<9)  ) == 0 ) ||  (( DMA1->ISR & (1<<5)  ) == 0 ) )      
					{
						ucErrTime++;
						if(ucErrTime>SPI1_EV_TIMEOUT)  
						{
							ret = 1;
						}
						DelayTick_us(10);	
					}
					ucErrTime = 0;
				}
				
				while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) != RESET)
				{
					ucErrTime++;
					if(ucErrTime>SPI1_EV_TIMEOUT)  
					{
						ret = 1;
					}
					DelayTick_us(10);	
				}
				
				DMA1->IFCR = 0xfff ;					     //清除DMA传输完成标志
				
				DMA1_Channel2->CCR &= ~(1 << 0) ;            //关闭DMA通道2
				DMA1_Channel3->CCR &= ~(1 << 0) ;            //关闭DMA通道3
				
				DMA1_Channel2->CNDTR = SPI_MAX_RX_LEN ;      //传输数量设置SPI_MAX_RX_LEN
				DMA1_Channel3->CNDTR = SPI_MAX_RX_LEN ;      //传输数量设置SPI_MAX_RX_LEN

				DMA1_Channel2->CMAR = (u32) spi_rx_buf;
				DMA1_Channel3->CMAR = (u32) (spi_tx_buf + 1); //设置DMA存储器地址					
				SPI1->DR = spi_tx_buf[0];	
				
				DMA1_Channel2->CCR |= 1 << 0 ;                //开启DMA通道2
				DMA1_Channel3->CCR |= 1 << 0 ;                //开启DMA通道3
				ret = 0;
				return ret;
			}
		}
  }
  return ret;
}

全局变量定义:

uint8_t g_spi_slave_flag = 0; 
u8 spi_rx_buf[10] = {0};
u8 spi_tx_buf[10] = {0x21,0x43,0x65,0x87,0xa9,0xcb,0xed,0x0f,0xfa,0xeb};

.h文件代码如下:

下载主机代码时,只要把#define SPI1_DMA_SLAVER注释掉,编译下载即可。

下载从机代码时,是要把#define SPI1_DMA_SLAVER定义上,编译下载即可。

#ifndef __SPI1_DMA_H
#define __SPI1_DMA_H
#include "sys.h"
	
#define SPI1_DMA

#define SPI1_DMA_SLAVER


#define SPI_MAX_BUF_LEN 256
#define SPI1_EV_TIMEOUT 1000

#define SPI1_DR_Addr ( (u32)0x4001300C )


void SPI1_DMA_Master_Init(void);
void SPI1_DMA_Slaver_Init(void);

uint8_t SPI1_DMA_ReadWrite(u8 *tx_buf, u16 len, u8 *rx_buf);  //spi master
uint8_t SPI1_master_dma_transfer(uint8_t* tx_data, uint16_t tx_len, uint8_t* rx_data);

uint8_t SPI1_slaver_dma_transfer( void ); //spi slaver rx

#endif

main函数代码:

u8 tx_buf[6] = {0x11,0x22,0x33,0x44,0x55,0x66} ;
u8 rx_buf[6] = {0} ;

int main(void)
{	
	staSysInit();
	
#ifndef SPI1_DMA_SLAVER	
	SPI1_DMA_Master_Init();
#else
	SPI1_DMA_Slaver_Init();
#endif
	while(1)
	{
		#ifndef SPI1_DMA_SLAVER
		SPI1_master_dma_transfer(tx_buf, 6, rx_buf);
		DelayTick_ms(1000);	
		#else
		SPI1_slaver_dma_transfer(); 
		#endif
	}
}

从mian函数可以看出,测试时,主机每隔1s往从机发送6个字节,分别是0x11,0x22,0x33,0x44,0x55,0x66。先调用函数SPI1_DMA_ReadWrite(tx_data, tx_len, rx_data)把这6个字节发给从机,再调用该函数SPI1_DMA_ReadWrite(tx_data, tx_len, rx_data)把从机收到的数据接收回来。第二次调用SPI1_DMA_ReadWrite函数是为了给从机提供6个字节的时钟,好让从机把收到的6个字节传回给主机。

测试时,从机先判断是否接收完成,接收完成后,通过rx_len = SPI_MAX_RX_LEN - tmp_len;来计算本次接收到的数据总数。然后通过memcpy(spi_tx_buf, spi_rx_buf, rx_len);把接收到的数据拷贝到从机的发送buf中,等主机再来读取的时候(即主机第二次调用SPI1_DMA_ReadWrite时),这时候从机就把之前接收到的数据发送出去了。

这里大家注意两句代码:

DMA1_Channel3->CMAR = (u32) (spi_tx_buf + 1); //设置DMA存储器地址                    
SPI1->DR = spi_tx_buf[0];

至于为何要赋值spi_tx_buf[0],以及(u32) (spi_tx_buf + 1);为何要地址偏移等会说字节错位问题时再说。下面先看spi主从机传输的波形:

这是整体波形,18M通信速率:

这是局部放大后的波形,可见字节和字节之间已经没有文章开头所说的间隔,实现了连续传输。

至于波形上的不均匀,是因为我的逻辑分析仪采样率太低导致,大家忽略即可。

到此,本次主从机通信已完成。下面说一下字节错位问题:

正常的波形图如下:

刚才描述了,主机是每隔1s给从机收发一组数据,即上边说的0x11,0x22,0x33,0x44,0x55,0x66这6个字节。下图大红框中是主机发送了3组数据。小红框中是主机每给从机收发一组数据,实际上调用了两次SPI1_DMA_ReadWrite函数,第一次是把数据发给从机,第二次是从从机读从机接收到的数据。

把小红框放大后如下,红框中为第二次SPI1_DMA_ReadWrite函数波形:

先看第二次SPI1_DMA_ReadWrite函数波形,即把上图中红框波形放大,如下图:,即从机返回的为0x11,0x22,0x33,0x44,0x55,0x66.

而如果把从机传输的代码改成下图这样:

主从机收发一组数据时的两次波形如下:

把第一个红框波形放大:即主机第一次调用SPI1_DMA_ReadWrite函数,把数据发送给从机时,从机相应的回复是初始化里的数据,从机初始化数据为:u8 spi_tx_buf[10] = {0x21,0x43,0x65,0x87,0xa9,0xcb,0xed,0x0f,0xfa,0xeb};至于为何从机回复的不是0x21,0x43,0x65,0x87,0xa9,0xcb,而回复的是,0x43,0x65,0x87,0xa9,0xcb,0xed,我也不知道为何会这样。有知道的朋友可以告诉一下。

把第二个红框波形放大:即主机第二次调用SPI1_DMA_ReadWrite函数,从从机读取从机收到的数据,可以看到从机回复的是0x0f,0x11,0x22,0x33,0x44,0x55,而不是从主机收到的0x11,0x22,0x33,0x44,0x55,0x66。

这是因为,主机第一次调用SPI1_DMA_ReadWrite函数给从机发数据时,当最后一个字节通信完成后,即主机的MOSI发送的0x66字节,同时从机的MISO回复的是0xed字节。此时从机的发送缓存是空的,即TXE=1,当TXE=1时,从机会立刻往SPI->DR里放入新的字节,即发送buf中0xed的下一个字节,即0x0f。所以当主机第二次调用SPI1_DMA_ReadWrite函数给从机提供时钟时,从机先把SPI->DR中的0x0f发送出去了,然后再接着发送当前spi_tx_buf中的前5个字节出去,即0x11,0x22,0x33,0x44,0x55。即上图的情况。

因此,我在从机的传输函数里,对SPI->DR重新赋值,同时把存储器取值地址偏移了一位,如下:

DMA1_Channel3->CMAR = (u32) (spi_tx_buf + 1); //设置DMA存储器地址                    
SPI1->DR = spi_tx_buf[0];

本次调试参考文章链接如下:对作者表示感谢。

http://openedv.com/forum.php?mod=viewthread&tid=3159&highlight=spi%2Bdma%2B%D7%D4%D1%A7

https://blog.csdn.net/weixin_40185666/article/details/89315019

 


版权声明:本文为RYOMARYOMA原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。