1.实验目的
使用树莓派向 STM32 发送数据,STM32 收到数据后通过串口的方式将数据打印到电脑上,同时返回给树莓派数据。树莓派接收到数据后打印在控制台上。
SPI 的配置为
树莓派主机
STM32 从机
全双工
8 bit 传输
工作模式 0 :CPOL|CPHA = 00
MSB 优先
禁止 CRC 校验
2.SPI 简介
SPI(Serial Peripheral Interface,串行外设接口)是 Motorola 公司提出的一种同步串行数据传输标准
2.1 接口
SPI 接口经常被称为 4 线串行总线,以主/从方式工作,数据传输过程由主机初始化,其使用的 4 条信号线分别为:
- SCLK:串行时钟,用来同步数据传输,由主机输出,从机不用配置时钟
- MOSI:主机输出从机输入数据线,通常先传输 MSB ;
- MISO:主机输入从机输出数据线,通常先传输 LSB ;
- CS:片选线,低电平有效,由主机输出。
在 SPI 总线上,某一时刻可以出现多个从机,但只能存在一个主机,主机通过片选线来确定要通信的从机。连接是对应相连,如下所示
SCLK-----SCLK
MOSI-----MOSI
MISO-----MISO
CS-----CS
2.2 数据传输
在一个 SPI 时钟周期内,会完成如下操作:
- 主机通过 MOSI 线发送 1 位数据,从机通过该线读取这 1 位数据;
- 从机通过 MISO 线发送 1 位数据,主机通过该线读取这 1 位数据。
这是通过移位寄存器来实现的。如下图所示,主机和从机各有一个移位寄存器,且二者连接成环。随着时钟脉冲,数据按照从高位到低位的方式依次移出主机寄存器和从机寄存器,并且依次移入从机寄存器和主机寄存器。当寄存器中的内容全部移出时,相当于完成了两个寄存器内容的交换。
2.3 时钟极性和时钟相位
在 SPI 操作中,最重要的两项设置就是时钟极性( CPOL 或 UCCKPL )和时钟相位( CPHA 或 UCCKPH )。
- CPOL 表示时钟信号的初始电平的状态(就是空闲状态),CPOL 为 0 表示时钟信号初始状态为低电平,为 1 表示时钟信号的初始电平是高电平。
- CPHA 表示在哪个时钟沿采样数据,CPHA 为 0 表示在首个时钟变化沿采样数据,而 CPHA 为 1 则表示在第二个时钟变化沿采样数据。
主机和从机的发送数据是同时完成的,两者的接收数据也是同时完成的。所以为了保证主从机正确通信,应使得它们的SPI具有相同的时钟极性和时钟相位。
2.4 SPI的4种工作模式
由于 CPOL 和 CPHA 都有两种不同状态,所以 SPI 分成了 4 种模式。我们在开发的时候,使用比较多的是模式 0 和模式 3 ,如下表所示
SPI工作模式 | CPOL | CPHA | SCL空闲状态 | 采样边沿 | 采样时刻 |
---|---|---|---|---|---|
0 | 0 | 0 | 低电平 | 上升沿 | 奇数边沿 |
1 | 0 | 1 | 低电平 | 下降沿 | 偶数边沿 |
2 | 1 | 0 | 高电平 | 下降沿 | 奇数边沿 |
3 | 1 | 1 | 高电平 | 上升沿 | 偶数边沿 |
2.5 优缺点
优点:
- 支持全双工操作
- 操作简单
- 数据传输速率较高
缺点:
- 需要占用主机较多的口线(每个从机都需要一根片选线)
- 只支持单个主机
3.树莓派部分
3.1 开启SPI
树莓派默认是没有开启 SPI 的功能的,我们需要手动去开启一下
在终端输入
raspi-config
然后跟着下面的图进行操作(小键盘上下左右是选择,回车是确定)
输入命令
ls /dev/spi*
出现如下图的样子,就是成功打开了
3.2 安装vscode(可选)
标题是超链接,点击跳转
3.3 下载bcm支持包
4.STM32部分(只讲解SPI和主函数部分)
4.1 spi.c 中 spi 的初始化代码
/* SPI1 引脚初始化,会在HAL_SPI_Init的时候调用*/
void HAL_SPI_MspInit(SPI_HandleTypeDef* spiHandle)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
if(spiHandle->Instance==SPI1)
{
__HAL_RCC_SPI1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
/**SPI1 引脚配置
PA4 ------> SPI1_NSS
PA5 ------> SPI1_SCK
PA6 ------> SPI1_MISO
PA7 ------> SPI1_MOSI
*/
GPIO_InitStruct.Pin = GPIO_PIN_4|GPIO_PIN_5|GPIO_PIN_6|GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF5_SPI1;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
}
/* SPI1 配置初始化 */
void MX_SPI1_Init(void)
{
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_SLAVE;// 从机
hspi1.Init.Direction = SPI_DIRECTION_2LINES;// 全双工
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;//8 bit 传输
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;//CPOL = 0
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;//CPHA = 0
hspi1.Init.NSS = SPI_NSS_HARD_INPUT;//片选硬件输入
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;//MSB 优先
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;//禁止 CRC 校验
hspi1.Init.CRCPolynomial = 10;
if (HAL_SPI_Init(&hspi1) != HAL_OK)//判断是否初始化成功
{
Error_Handler();
}
}
4.2 main.c
#include "main.h"
#include "spi.h"
#include "usart.h"
#include "gpio.h"
#include "stdio.h"
void SystemClock_Config(void);//函数声明,实现在下面
/* 输出和输入重定向,主要用于 printf 串口输出 */
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
int fgetc(FILE *f)
{
uint8_t ch = 0;
HAL_UART_Receive(&huart1, &ch, 1, 0xffff);
return ch;
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();//串口初始化
MX_SPI1_Init();//SPI1初始化
HAL_Delay(1000);
uint8_t data2[10]={0};//接收数据
uint8_t send_data2[10]={0xff,0xfe,0xfd,0xfc,0xfb,0xfa,0xf9,0xf8,0xf7,0xf6};//发送数据
HAL_SPI_TransmitReceive(&hspi1, (uint8_t *)send_data2, (uint8_t *)data2,10,0xff);//提前将数据放进发送缓存区
while (1)
{
HAL_GPIO_WritePin(Start_GPIO_Port,Start_Pin,GPIO_PIN_RESET);
HAL_Delay(100);
HAL_GPIO_WritePin(Start_GPIO_Port,Start_Pin,GPIO_PIN_SET);
/* 接收10个字节的同时,发送10个字节出去,等待时间为 65535ms ,超时则不继续等待 */
HAL_SPI_TransmitReceive(&hspi1, (uint8_t *)send_data2, (uint8_t *)data2,10,65535);
/* 通过串口发送给电脑 */
for(int i=0;i<10;i++)
{
printf("0x%02X\n",data2[i]);//指定为以16进制的形式格式化输出
}
HAL_Delay(2000);//延时2s
}
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 4;
RCC_OscInitStruct.PLL.PLLN = 168;
RCC_OscInitStruct.PLL.PLLP = RCC_PLLP_DIV2;
RCC_OscInitStruct.PLL.PLLQ = 4;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
{
Error_Handler();
}
}
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
__disable_irq();
while (1)
{
}
}
源码见本文末尾
5.验证结果
下面是树莓派的引脚图
按照下面的方式连接树莓派与 STM32 的引脚
树莓派--------STM32
MOSI/#10--------PA7
MISO/#9----------PA6
SCLK/#11--------PA5
GND---------------GND
cd “/home/pi/SPI_test/bcm2835-1.71/examples/spi/” && gcc *.c -o spi && "/home/pi/SPI_test/bcm2835-1.71/examples/spi/"spi