PUYA SPI — 全双工主机(中断方式)(LL 库)

PUYA PY32F0xx_Firmware V1.5.0 SPI 全双工主机(SPI_TwoBoards_FullDuplexMaster_IT_Init)示例教学(LL 库版本)

SPI — 全双工主机(中断方式)(LL 库)

HAL vs LL:PUYA 固件库提供 HAL 和 LL 两套 API。

  • HAL 库:封装度高,可移植性强,适合快速开发
  • LL 库:接近寄存器操作,代码精简效率高,适合性能敏感场景 本文使用 LL 库版本。

固件库: PY32F0xx_Firmware V1.5.0
芯片: PY32F030(PY32F002A / F003 同库可参考)
源文件: PUYA/PY32F0xx_Firmware_V1.5.0/Projects/PY32F030-STK/Example_LL/SPI/SPI_TwoBoards_FullDuplexMaster_IT_Init/Src/main.c


功能简介

本示例演示 SPI 全双工主机(Master),使用中断方式:按键后使能 SPI1,通过 TXE/RXNE 中断逐字节发送 15 字节,同时接收对方回环的数据,结束后比对收发一致性——一致则点亮 LED,否则 LED 闪烁。

说明:PUYA 的 LL 示例库里 SPI 只有 DMA 和中断(IT)两种版本,没有轮询(Polling)版。你点名要的 SPI_TwoBoards_FullDuplexMaster_Polling 在 LL 中不存在,这里用最接近、也最能体现 LL 风格的 ..._IT_Init(中断方式)来替代。HAL 版则有 ..._Master_DMA,可作为对照。

与 HAL 版(HAL_SPI_TransmitReceive)相比,LL 版把“每个字节的收发”交给中断回调函数 APP_SpiTxCallback() / APP_SpiRxCallback() 完成,主循环只需等传输完成——这正是 LL 在“不阻塞 CPU”场景下的典型写法。

硬件准备

  • 两块 PY32F030 开发板(一块主机、一块从机,跑对应 Slave 示例做回环)
  • SPI 连线:主机 PB3(SCK) ↔ 从机 SCK,PB4(MISO) ↔ MISO,PB5(MOSI) ↔ MOSI
  • 主机板载按键(KEY)、绿 LED

完整代码

  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
#include "main.h"
#include "py32f030xx_ll_Start_Kit.h"

uint8_t aTxBuffer[] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
uint8_t ubNbDataToTransmit = sizeof(aTxBuffer);
uint8_t ubTransmitIndex = 0;

uint8_t aRxBuffer[sizeof(aTxBuffer)] = {0};
uint8_t ubNbDataToReceive = sizeof(aTxBuffer);
uint8_t ubReceiveIndex = 0;

static void APP_SystemClockConfig(void);
static void APP_ConfigSPI(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)
{
  APP_SystemClockConfig();
  BSP_LED_Init(LED_GREEN);
  BSP_PB_Init(BUTTON_KEY, BUTTON_MODE_GPIO);

  APP_ConfigSPI();                        /* 配置 SPI1 + 中断 */

  while (BSP_PB_GetState(BUTTON_KEY) == 1);   /* 等按键 */

  LL_SPI_Enable(SPI1);                    /* 使能 SPI1,中断随即开始搬运 */
  APP_WaitAndCheckEndOfTransfer();        /* 等传输完并校验 */

  while (1) {}
}

/**
  * @brief  SPI1 配置(PB3/4/5 复用 + 参数 + 中断)
  */
static void APP_ConfigSPI(void)
{
  LL_SPI_InitTypeDef   SPI_InitStruct = {0};
  LL_GPIO_InitTypeDef GPIO_InitStruct = {0};

  LL_APB1_GRP2_EnableClock(LL_APB1_GRP2_PERIPH_SPI1);
  LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOB);

  /* PB3=SCK, PB4=MISO, PB5=MOSI,全部复用 AF0 */
  GPIO_InitStruct.Pin        = LL_GPIO_PIN_3;
  GPIO_InitStruct.Mode       = LL_GPIO_MODE_ALTERNATE;
  GPIO_InitStruct.Speed      = LL_GPIO_SPEED_FREQ_HIGH;
  GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_PUSHPULL;
  GPIO_InitStruct.Pull       = LL_GPIO_PULL_UP;
  GPIO_InitStruct.Alternate  = LL_GPIO_AF_0;
  LL_GPIO_Init(GPIOB, &GPIO_InitStruct);

  GPIO_InitStruct.Pin = LL_GPIO_PIN_4;
  GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;     /* MISO 无需上拉 */
  LL_GPIO_Init(GPIOB, &GPIO_InitStruct);

  GPIO_InitStruct.Pin = LL_GPIO_PIN_5;
  GPIO_InitStruct.Pull = LL_GPIO_PULL_NO;
  LL_GPIO_Init(GPIOB, &GPIO_InitStruct);

  /* SPI1 中断 */
  NVIC_SetPriority(SPI1_IRQn, 1);
  NVIC_EnableIRQ(SPI1_IRQn);

  /* SPI 参数:全双工、主机、8 位、极性高/相位1边沿、软件 NSS、128 分频 */
  SPI_InitStruct.TransferDirection = LL_SPI_FULL_DUPLEX;
  SPI_InitStruct.Mode              = LL_SPI_MODE_MASTER;
  SPI_InitStruct.DataWidth         = LL_SPI_DATAWIDTH_8BIT;
  SPI_InitStruct.ClockPolarity     = LL_SPI_POLARITY_HIGH;
  SPI_InitStruct.ClockPhase        = LL_SPI_PHASE_1EDGE;
  SPI_InitStruct.NSS               = LL_SPI_NSS_SOFT;
  SPI_InitStruct.BaudRate          = LL_SPI_BAUDRATEPRESCALER_DIV128;
  SPI_InitStruct.BitOrder          = LL_SPI_MSB_FIRST;
  LL_SPI_Init(SPI1, &SPI_InitStruct);

  LL_SPI_SetRxFIFOThreshold(SPI1, LL_SPI_RX_FIFO_TH_QUARTER);  /* RXNE 在收到 1 字节时置位 */

  LL_SPI_EnableIT_TXE(SPI1);   /* 使能“发送空”中断 */
  LL_SPI_EnableIT_RXNE(SPI1);  /* 使能“接收非空”中断 */
  LL_SPI_EnableIT_ERR(SPI1);   /* 使能错误中断 */
}

/* 以下回调由 SPI1 中断服务程序调用(在 stm32py32xx_it.c 中) */
void APP_SpiRxCallback(void)
{
  aRxBuffer[ubReceiveIndex++] = LL_SPI_ReceiveData8(SPI1);   /* 读 DR 同时清 RXNE */
}
void APP_SpiTxCallback(void)
{
  LL_SPI_TransmitData8(SPI1, aTxBuffer[ubTransmitIndex++]);  /* 写 DR 启动一次收发 */
}
void APP_SpiTransferErrorCallback(void)
{
  LL_SPI_DisableIT_RXNE(SPI1);
  LL_SPI_DisableIT_TXE(SPI1);
  APP_LedBlinking();
}

/**
  * @brief  等待收发完成并比对
  */
static void APP_WaitAndCheckEndOfTransfer(void)
{
  while (ubTransmitIndex != ubNbDataToTransmit) {}   /* 等发完 */
  LL_SPI_DisableIT_TXE(SPI1);
  while (ubNbDataToReceive > ubReceiveIndex) {}      /* 等收完 */
  LL_SPI_DisableIT_RXNE(SPI1);

  if (APP_Buffercmp8((uint8_t*)aTxBuffer, (uint8_t*)aRxBuffer, ubNbDataToTransmit))
    APP_LedBlinking();
  else
    BSP_LED_On(LED_GREEN);
}
/* APP_Buffercmp8 / APP_LedBlinking / 时钟与错误处理 略 */

代码分段讲解

1. GPIO 复用与 SPI 参数

PB3/4/5 设为 LL_GPIO_MODE_ALTERNATE + Alternate = LL_GPIO_AF_0 接到 SPI1。SPI 参数结构体 LL_SPI_InitTypeDef 与 HAL 的 SPI_HandleTypeDef.Init 字段几乎一一对应:全双工、主机、8 位、CPOL/CPHA、软件 NSS、波特率分频、MSB 优先。

2. 中断方式的核心:TXE / RXNE 回调

HAL 的 HAL_SPI_TransmitReceive() 内部要么轮询、要么用 DMA;LL 这里走中断

  • 使能 LL_SPI_EnableIT_TXE():发送缓冲区空时进中断 → APP_SpiTxCallback() 写一个字节,SPI 硬件同时自动收一个字节;
  • 使能 LL_SPI_EnableIT_RXNE():收到一个字节进中断 → APP_SpiRxCallback() 读走它。

因为 SPI 是全双工,每发一个必然同时收一个,所以两个索引独立推进,主循环只需等两者都到头。

3. FIFO 阈值

LL_SPI_SetRxFIFOThreshold(..., LL_SPI_RX_FIFO_TH_QUARTER) 让 RXNE 在“收到 1 个字节”时就置位(而不是等 FIFO 半满),保证逐字节中断行为正确。这是 LL 才需要你显式关心的底层细节。

4. 为什么 LL 没有“轮询 SPI”示例

SPI 全双工收发天然适合“发一个收一个”的流式处理,用中断/DMA 比忙等更高效;而 HAL 为了 API 对称提供了 HAL_SPI_TransmitReceive(轮询版)。LL 把“怎么搬”的选择权完全交给你——你可以轻松把本例的 IT 改成轮询(去掉中断、在主循环里查 LL_SPI_IsActiveFlag_TXE)。

实验现象

  1. 主从板 SPI 四线连好,从机跑 Slave 示例。
  2. 主机按下 KEY,SPI1 使能,开始全双工收发 15 字节。
  3. 收发结束,数据比对一致 → 绿 LED 常亮;不一致 → 绿 LED 闪烁。

常见问题

  1. 收发数据对不上?

    • 确认主从 CPOL/CPHA 一致(本例 极性高/相位1边沿)。
    • 确认 NSS 处理:本例用软件 NSS(LL_SPI_NSS_SOFT),硬件 NSS 引脚未参与,接线时从机片选要正确拉低。
  2. 只进 TX 中断不出 RX?

    • 多半是 MISO 线没连通或主从没共地;SPI 收不到字节,RXNE 永远不会置位。
  3. 想用 DMA?

    • 参考 SPI_TwoBoards_FullDuplexMaster_DMA_Init,用 LL_DMA_* 把缓冲区交给 DMA 搬运,CPU 零参与。

小结

  • LL 的 SPI 全双工主机 = 配 GPIO 复用 + 配 SPI 参数 + 选中断/DMA/轮询搬运 + 使能
  • 相比 HAL 的一行 HAL_SPI_TransmitReceive,LL 让你自己决定“字节怎么搬”,中断回调写法最贴近硬件。
  • SPI 没有 LL 轮询示例,但把 IT 版本去掉中断、改轮询标志即可等价实现。

拓展应用

  • 把 IT 改成纯轮询(查 LL_SPI_IsActiveFlag_TXE/RXNE),代码更简单便于理解。
  • 改用 DMA 搬运长帧,提升吞吐(参考 _DMA_Init)。
  • 接真实 SPI 器件(LCD、Flash、传感器),把回环逻辑换成器件寄存器读写。
  • 增加超时判断,避免从机异常时主循环死等。
世界是你们
使用 Hugo 构建
主题 StackJimmy 设计