PUYA SPI — 双板全双工主机轮询 SPI_TwoBoards_FullDuplexMaster_Polling

PUYA PY32F403_Firmware V1.4.5 SPI 双板全双工主机轮询示例教学

SPI — 双板全双工主机轮询 SPI_TwoBoards_FullDuplexMaster_Polling

固件库: PY32F403_Firmware V1.4.5
芯片: PY32F403(Cortex-M4)
源文件: PY32F403-STK/Example/SPI/SPI_TwoBoards_FullDuplexMaster_Polling/Src/main.c
核心外设: SPI1(主机,全双工,轮询)


1. 功能简介(大白话)

这是最经典的「SPI 双板对传」例子:拿 两块 PY32F403-STK 开发板,一块当 主机(Master),一块当 从机(Slave)

主机准备了一个发送缓冲 TxBuff = {1,2,...,15},按下用户按键后,通过 SPI 把 15 个字节发给从机;因为是全双工,主机在发的同时也从从机收 15 个字节到 RxBuff。通信结束后,主机比较「自己发出去的数据」和「从机回环/回应过来的数据」是否一致——一致就点亮 LED,不一致就让 LED 闪烁报错。

注意:此例程是「主机↔从机」两块板通信,并非单板 MOSI-MISO 短接回环。回环(loopback)只是调试技巧,真正的 SPI 设备通常是这样一主一从。

2. 硬件准备

物品 数量 说明
PY32F403-STK 开发板 2 一块烧主机程序,一块烧从机程序
USB 线 2 供电 + 下载

接线(主从一一对应相连,箭头为主→从方向):

1
2
3
4
5
6
主机 MASTER              从机 SLAVE
SCK  (PB3)  ----->   SCK  (PB3)
MISO (PB4)  <-----   MISO (PB4)
MOSI (PB5)  ----->   MOSI (PB5)
NSS  (PA15) ----->   NSS  (PA15)
GND         ----->   GND          (务必共地!)

操作步骤

  1. 主机烧 SPI_TwoBoards_FullDuplexMaster_Polling,从机烧 SPI_TwoBoards_FullDuplexSlave_Polling
  2. 先按从机复位键让从机先运行,再按主机用户键启动主机;
  3. 主从 LED 由灭变常亮 = 通信成功;LED 闪烁 = 失败。

为什么从机要先跑?因为 SPI 是主机给时钟、从机跟着移位的同步协议,从机没就绪就发时钟会采到错数据。

3. 完整代码(带中文注释)

  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
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#include "main.h"

/* 要收发的数据长度 */
#define DATA_LENGTH   15

/* SPI 句柄 */
SPI_HandleTypeDef Spi1Handle;

/* 发送缓冲:1~15;接收缓冲:先清零 */
uint8_t TxBuff[15] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
uint8_t RxBuff[15] = {0};

/* 函数声明 */
static void APP_SystemClockConfig(void);
static void APP_WaitAndCheckEndOfTransfer(void);
static uint8_t APP_Buffercmp8(uint8_t* pBuffer1, uint8_t* pBuffer2, uint8_t BufferLength);
static void APP_LedBlinking(void);

int main(void)
{
  /* 1) HAL 初始化(中断、Systick)*/
  HAL_Init();

  /* 2) 系统时钟:HSI 8MHz,APB2 二分频 → SPI1 所在总线 4MHz */
  APP_SystemClockConfig();

  /* 3) LED 与用户按键初始化 */
  BSP_LED_Init(LED_GREEN);
  BSP_PB_Init(BUTTON_KEY, BUTTON_MODE_GPIO);

  /* 4) 配置 SPI1 句柄(先 DeInit 清状态,再 Init)*/
  Spi1Handle.Instance               = SPI1;
  Spi1Handle.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256; /* 4MHz/256 ≈ 15.6kHz */
  Spi1Handle.Init.Direction         = SPI_DIRECTION_2LINES;      /* 双线全双工 */
  Spi1Handle.Init.CLKPolarity       = SPI_POLARITY_LOW;          /* CPOL=0 → Mode 0 */
  Spi1Handle.Init.CLKPhase          = SPI_PHASE_1EDGE;           /* CPHA=0 → Mode 0 */
  Spi1Handle.Init.DataSize          = SPI_DATASIZE_8BIT;         /* 8 位一帧 */
  Spi1Handle.Init.FirstBit          = SPI_FIRSTBIT_MSB;          /* 高位先发 */
  Spi1Handle.Init.NSS               = SPI_NSS_HARD_OUTPUT;       /* 硬件 NSS(自动拉低/拉高 PA15)*/
  Spi1Handle.Init.Mode              = SPI_MODE_MASTER;           /* 主机 */
  Spi1Handle.Init.CRCCalculation    = SPI_CRCCALCULATION_DISABLE;/* 不校验 CRC */
  if (HAL_SPI_DeInit(&Spi1Handle) != HAL_OK)  APP_ErrorHandler();
  if (HAL_SPI_Init(&Spi1Handle)   != HAL_OK)  APP_ErrorHandler(); /* 内部调 MSP 配引脚 */

  /* 5) 等用户按键按下(按键默认高电平,按下变低)*/
  while (BSP_PB_GetState(BUTTON_KEY) == 1) { }

  /* 6) 全双工收发:发 TxBuff 同时收 RxBuff,阻塞最长 5 秒 */
  if (HAL_SPI_TransmitReceive(&Spi1Handle, TxBuff, RxBuff, DATA_LENGTH, 5000) != HAL_OK)
  {
    APP_ErrorHandler();
  }

  /* 7) 等待传输结束并校验数据 */
  APP_WaitAndCheckEndOfTransfer();

  while (1) { }   /* 成功/失败后停在这里 */
}

/**
  * @brief  等待 SPI 就绪并比对收发数据
  */
static void APP_WaitAndCheckEndOfTransfer(void)
{
  while (Spi1Handle.State != HAL_SPI_STATE_READY) { }  /* 等状态机回到 READY */

  if (APP_Buffercmp8(TxBuff, RxBuff, DATA_LENGTH))    /* 发出去的 == 收到的?*/
  {
    APP_LedBlinking();        /* 不一致 → LED 闪(失败)*/
  }
  else
  {
    BSP_LED_On(LED_GREEN);    /* 一致 → LED 常亮(成功)*/
  }
}

/**
  * @brief  逐字节比较两缓冲,全同返回 0,有异返回 1
  */
static uint8_t APP_Buffercmp8(uint8_t* pBuffer1, uint8_t* pBuffer2, uint8_t BufferLength)
{
  while (BufferLength--)
  {
    if (*pBuffer1 != *pBuffer2) return 1;
    pBuffer1++; pBuffer2++;
  }
  return 0;
}

/**
  * @brief  失败指示:LED 每 500ms 翻转一次
  */
static void APP_LedBlinking(void)
{
  while (1) { BSP_LED_Toggle(LED_GREEN); HAL_Delay(500); }
}

/**
  * @brief  系统时钟:HSI 8MHz;APB2 二分频 = 4MHz(SPI1 时钟源)
  */
static void APP_SystemClockConfig(void)
{
  RCC_OscInitTypeDef  OscInitstruct = {0};
  RCC_ClkInitTypeDef  ClkInitstruct = {0};

  OscInitstruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
  OscInitstruct.HSIState       = RCC_HSI_ON;
  OscInitstruct.HSI48MState    = RCC_HSI48M_OFF;
  OscInitstruct.LSEState       = RCC_LSE_OFF;
  OscInitstruct.LSIState       = RCC_LSI_OFF;
  OscInitstruct.PLL.PLLState   = RCC_PLL_OFF;
  if (HAL_RCC_OscConfig(&OscInitstruct) != HAL_OK) APP_ErrorHandler();

  ClkInitstruct.ClockType      = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK |
                                 RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
  ClkInitstruct.SYSCLKSource   = RCC_SYSCLKSOURCE_HSI;  /* 8MHz */
  ClkInitstruct.AHBCLKDivider  = RCC_SYSCLK_DIV1;       /* HCLK = 8MHz */
  ClkInitstruct.APB1CLKDivider = RCC_HCLK_DIV1;         /* APB1 = 8MHz */
  ClkInitstruct.APB2CLKDivider = RCC_HCLK_DIV2;         /* APB2 = 4MHz(SPI1 源)*/
  if (HAL_RCC_ClockConfig(&ClkInitstruct, FLASH_LATENCY_0) != HAL_OK) APP_ErrorHandler();
}

void APP_ErrorHandler(void) { while (1) {} }

附:MSP 里的引脚配置(HAL_SPI_Init 自动调用)

 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
void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if (hspi->Instance == SPI1)
  {
    __HAL_RCC_GPIOB_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_SPI1_CLK_ENABLE();

    /* PB3 = SCK(AF3,推挽复用,CLK 空闲低时下拉)*/
    GPIO_InitStruct.Pin       = GPIO_PIN_3;
    GPIO_InitStruct.Pull      = GPIO_PULLDOWN;
    GPIO_InitStruct.Mode      = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed     = GPIO_SPEED_FREQ_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF3_SPI1;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

    /* PA15 = NSS(AF3,开漏+上拉,硬件方式由外设自动控制)*/
    GPIO_InitStruct.Pin       = GPIO_PIN_15;
    GPIO_InitStruct.Mode      = GPIO_MODE_AF_OD;
    GPIO_InitStruct.Pull      = GPIO_PULLUP;
    GPIO_InitStruct.Speed     = GPIO_SPEED_FREQ_VERY_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF3_SPI1;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    /* PB4=MISO, PB5=MOSI(AF3,推挽复用)*/
    GPIO_InitStruct.Pin       = GPIO_PIN_4 | GPIO_PIN_5;
    GPIO_InitStruct.Mode      = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed     = GPIO_SPEED_FREQ_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF3_SPI1;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
  }
}

4. 代码分段讲解

4.1 全双工为什么要「收发一起」?

SPI 的移位寄存器机制决定:每发送 1 位,同时也接收 1 位。所以不存在「只发不收」或「只收不发」——哪怕你只想发,硬件也在同时收;反之亦然。HAL_SPI_TransmitReceive 正是把这两个动作合并成一次调用(长度相同)。如果你只想发,可以传一个无关接收缓冲;只想收,则发缓冲填 0(dummy)即可。

4.2 波特率怎么算?

1
2
3
SCK 频率 = SPI 所在总线时钟 / BaudRatePrescaler
         = APB2(4MHz) / 256
         ≈ 15.6 kHz

分频系数可选 2/4/8/16/32/64/128/256。实际项目里按从机允许的最高速率来选,越快越好(但别超从机上限)。

4.3 Mode 0 是什么?

CPOL=LOW + CPHA=1EDGESPI Mode 0:时钟空闲为低电平,数据在第一个时钟边沿(上升沿)被采样。只要从机也设成 Mode 0 就能正常通信。

4.4 硬件 NSS 省心在哪?

SPI_NSS_HARD_OUTPUT 让 SPI 外设自动在传输开始时把 PA15 拉低(选中从机)、结束后拉高。比起软件手动 GPIO_ResetBits 拉 CS,硬件 NSS 时序更精准、不会漏。注意:硬件 NSS 方式下也必须把 PA15 连到从机 NSS。

4.5 校验逻辑

主机发出 1~15,从机(对应 Slave 例程)会回发同样数据;主机收到后逐字节比较,全相等才点亮 LED。这验证了「物理连线 + 时序参数」都正确。

5. 实验现象

  • 正确接线、先跑从机再跑主机后:
    • 主机 LED 常亮 = 收发数据校验成功;
    • 若任一板 LED 闪烁 = 通信失败(检查接线/共地/主从模式是否匹配)。
  • 实际板对板回环里,RxBuff 最终等于 TxBuff(即 1,2,...,15)。

6. 常见问题

现象 原因 解决
LED 一直闪(校验失败) 没共地 / 线序接错 接好 GND,确认 SCK-MISO-MOSI-NSS 一一对应
主机发、从机收不到 从机没先启动 先按从机复位,再按主机按键
数据错位/乱码 主从 CPOL/CPHA 不一致 双方都设成 Mode 0(或统一为同模式)
完全无反应 SPI 时钟被分频太低/太高外设异常 确认例程分频 256,总线 4MHz
想接单个 SPI 设备 用不到双板 单板可把 MOSI 焊线接 MISO 做回环自测
速率上不去 总线时钟只有 4MHz 打开 PLL 把系统/APB2 提到更高频再分频

7. 小结

  • SPI 是高速、全双工、同步总线,靠 SCK 边沿移位,主从 CPOL/CPHA 必须一致;
  • 例程用 SPI1(APB2=4MHz)分频 256 ≈ 15.6kHz 做低速演示,跑通后再提速;
  • 数据收发用 HAL_SPI_TransmitReceive 一体完成;
  • 硬件 NSS 让片选自动管理,省心且时序准;
  • 验证方法是「发什么收什么」逐字节比较。

8. 拓展应用(学了能做什么项目)

  • SPI Flash 存储:参考 SPI_FullDuplex_ExternalFLASH,给 MCU 扩几十 MB 固件/字库空间;
  • TFT 彩屏驱动:用 SPI 刷 LCD(ST7735/ILI9341),做小游戏机;
  • SD 卡读写:SPI 模式挂 TF 卡,做数据记录仪;
  • 高速采集:配合 SPI_xxx_DMA 示例,用 DMA 搬运大块数据不占 CPU;
  • 无线模块:驱动 nRF24L01、SX1278 等射频芯片;
  • 多从机组网:用不同 GPIO 当多个 CS,一个主机轮询多个 SPI 从设备。

返回:SPI 总线总览

世界是你们
使用 Hugo 构建
主题 StackJimmy 设计