0%

树莓派与STM32之间的SPI通信

树莓派的SPI模块

树莓派的GPIO引脚支持三种通信协议:UART、SPI和I2C。SPI是一种双向、同步、全双工的串行协议,是三个协议中通信速率最快的。树莓派的官方文档详细介绍了SPI模块的基本特性,主要是如下几个方面:

  1. 硬件引脚分配
  2. 在树莓派的Linux系统中,SPI硬件抽象成/dev/spidev0.0文件
  3. 树莓派的SPI只支持主模式(Master Mode)
  4. 配置参数:除了通信速率、CPOL、CPHA这三个参数外,其他的参数一般都不会去修改
  5. 提供了一段回环测试程序,可用于检测SPI模块是否正常工作

接下来的问题是,我们如何通过SPI接受和发送数据。看到/dev/spidev0.0文件,熟悉Linux系统编程的人马上会想起通用I/O模型中的几个函数。的确,可以在该文件上执行open和close操作,就跟操作普通文件一样。同样地,read和write函数可以读写数据,但只能进行基本的半双工数据传输,片选信号在读写操作之间会失效。使用ioctl()函数的SPI_IOC_MESSAGE(N)请求可实现全双工数据传输,而且整个过程中片选信号不会失效。

Linux内核代码提供了一段SPI的示例程序,是/tools/spi/目录下的spidev_test.c文件。这段代码实现了一个命令行工具,既可以直接使用,也可以借鉴它的代码实现自己的功能。将代码编译后生成的可执行文件命名为spidev_test,下面的命令使用默认的配置将一个文件的内容通过SPI发送了出去,并把从SPI接收的数据保存到另一个文件中:

1
$ ./spidev_test -D /dev/spidev0.0 -i send.txt -o receive.txt -v

通过-s-O-H选项可以分别修改通信速率、CPOL和CPHA这三个参数,-v选项可以打印发送和接收的内容。正如上面提到的,这段代码就是使用ioctl()函数实现全双工数据传输,详情可以阅读源文件中的transfer函数。

STM32端的代码

首先将树莓派和STM32对应的SPI引脚连接好。为了验证树莓派和STM32之间的SPI通信是正确的,我将实现如下功能:

  1. 树莓派发送一个32字节大小的文件给STM32,STM32收完之后用串口发送出去,在PC端的串口助手上查看数据
  2. STM32同时也给树莓派发送数据,树莓派上可通过-v选项查看STM32发送过来的数据

STM32的SPI配置成全双工、从模式(Slave Mode),CPHA和CPOL参数都为0。我使用固件库开发,部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* 省略GPIO配置、其他初始化和NVIC配置代码
*/
// 主模式,双线全双工通信,软件控制片选信号
SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
SPI_InitStructure.SPI_Mode = SPI_Mode_Master;
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;
SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; // CPOL = 0
SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; // CPHA = 0
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;

// 使能TXE和RXNE中断
SPI_I2S_ITConfig(SPI_MASTER, SPI_I2S_IT_TXE, ENABLE);
SPI_I2S_ITConfig(SPI_MASTER, SPI_I2S_IT_RXNE, ENABLE);

STM32接收和发送数据都在中断中进行。发送缓冲区为空时会触发TXE中断,这时没有可发的数据,调用SendData函数发送数据;接收缓冲区不为空时说明有新的数据到了,此时会触发RXNE中断,调用ReceiveData函数读出数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#define TX_SIZE 32
#define RX_SIZE 32

// 发给树莓派的数据
uint8_t spi_tx[TX_SIZE] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C,
0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12,
0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E,
0x1F, 0x20};
uint32_t tx_idx = 0;

uint8_t spi_rx[RX_SIZE];
uint32_t rx_idx = 0;

void SPI1_IRQHandler(void)
{
int i;
if (SPI_I2S_GetITStatus(SPI1, SPI_I2S_IT_TXE) != RESET) {
SPI_I2S_SendData(SPI1, spi_tx[tx_idx++]);
// 发送32个数据后禁止TXE中断
if (tx_idx == TX_SIZE) {
SPI_I2S_ITConfig(SPI1, SPI_I2S_IT_TXE, DISABLE);
}
}
// 从SPI读取数据
if (SPI_I2S_GetITStatus(SPI1, SPI_I2S_IT_RXNE) != RESET) {
spi_rx[rx_idx++] = SPI_I2S_ReceiveData(SPI1);
// 收完32个字节后将收到的内容通过串口发送出去
if (rx_idx == RX_SIZE) {
for (i = 0; i < RX_SIZE; i++) {
USART_SendData(USART1, (uint16_t)spi_rx[i]);
while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET);
}
}
}
}

将STM32串口线连接到PC上,打开串口助手并打开对应的COM口,然后在树莓派端运行命令向STM32发送文件的内容。如果串口助手中接受到的数据和文件内容完全符合,而且树莓派上显示的也是STM32发送的内容,就证明双向的通信均是正确的。

参考链接

  1. SPI - Raspberry Pi Documentation
  2. Can Raspberry PI function as SPI slave?
  3. spidev - kernel.org
  4. SPLibrary-STM32F103-SPI-FULLDUBLEX-MASTER-SLAVE_COMMUNICATION