我用的芯片是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
#endifmain函数代码:
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