树莓派与STM32之间的SPI通信

树莓派的SPI模块

树莓派的GPIO引脚支持UART、SPI和I2C三种通信协议。SPI是双向、同步、全双工的串行协议,是三种协议中通信速率最快的。官方文档介绍了树莓派SPI模块的基本情况:

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

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

Linux内核代码的/tools/spi/目录下有个spidev_test.c文件,是SPI的示例程序。这段代码实现了一个命令行工具,既可以直接使用,也可以借鉴它的代码来实现自己的需求。编译代码生成可执行文件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_Slave;
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