PUYA I2S — 双板主机轮询收发 I2S_TwoBoard_CommunicationMaster_Polling

PUYA PY32F403_Firmware V1.4.5 I2S 双板主机轮询收发示例教学

I2S — 双板主机轮询收发 I2S_TwoBoard_CommunicationMaster_Polling

固件库: PY32F403_Firmware V1.4.5
芯片: PY32F403(Cortex-M4)⭐ F403 特色外设
源文件: PY32F403-STK/Example/I2S/I2S_TwoBoard_CommunicationMaster_Polling/Src/main.c
核心外设: SPI2 复用为 I2S(PB12=WS / PB13=CK / PB15=SD)


1. 功能简介(大白话)

I2S 是数字音频专用的总线。本例把 PY32F403 的 SPI2 外设切换成 I2S 模式,当作主机(Master),和另一块板(从机 Slave)对传音频样本:

  • 主机先以 I2S_MODE_MASTER_TX 把 16 个 16 位数据 {1,2,...,16} 发给从机;
  • 从机收到后回发同样数据;
  • 主机再切换成 I2S_MODE_MASTER_RX 把从机回发的数据收回来;
  • 比较「发出去的是否等于收回来的」:一致点亮 LED,不一致 LED 闪烁。

虽然是「数据回环」式验证,但流程与真实音频链路完全一致:MCU 用 I2S 把 PCM 样本送给 DAC 播出来,或从 ADC/麦克风收样本

2. 硬件准备

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

接线(主从一一对应,务必共地):

1
2
3
4
5
主机 MASTER              从机 SLAVE
SD  (PB15)  <----->  SD  (PB15)
WS  (PB12)  ----->   WS  (PB12)
CK  (PB13)  ----->   CK  (PB13)
GND         <----->  GND

操作

  1. 主机烧 I2S_TwoBoard_CommunicationMaster_Polling,从机烧 I2S_TwoBoard_CommunicationSlave_Polling
  2. 主从都上电并按复位键;
  3. 先按从机用户键让从机先跑,再按主机用户键启动主机;
  4. 主从 LED 由灭变常亮 = 收发成功;任一 LED 闪烁 = 失败。

I2S 是同步总线,主机提供 CK 和 WS,从机跟随。所以 CK/WS 是主机→从机单向,SD 双向传数据。

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

I2S_HandleTypeDef I2sHandle;
uint16_t TxBuff[16] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16};
uint16_t RxBuff[16] = {0};
#define DATA_LENGTH  16

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

int main(void)
{
  HAL_Init();
  APP_SystemClockConfig();          /* 系统时钟:HSI 8MHz */
  BSP_LED_Init(LED_GREEN);
  BSP_PB_Init(BUTTON_KEY, BUTTON_MODE_GPIO);

  while (BSP_PB_GetState(BUTTON_KEY) == 1) {}   /* 等用户键按下 */

  /* ===== 第一步:配置为主机发送(TX)===== */
  I2sHandle.Instance        = SPI2;                 /* I2S 复用 SPI2 */
  I2sHandle.Init.Mode       = I2S_MODE_MASTER_TX;   /* 主机发送 */
  I2sHandle.Init.Standard   = I2S_STANDARD_PHILIPS; /* 飞利浦标准 */
  I2sHandle.Init.DataFormat = I2S_DATAFORMAT_16B;   /* 16 位样本 */
  I2sHandle.Init.MCLKOutput = I2S_MCLKOUTPUT_DISABLE;/* 不用主时钟 MCLK */
  I2sHandle.Init.AudioFreq  = I2S_AUDIOFREQ_8K;     /* 8kHz 采样率 */
  I2sHandle.Init.CPOL       = I2S_CPOL_HIGH;        /* 时钟空闲高 */
  if (HAL_I2S_Init(&I2sHandle) != HAL_OK) APP_ErrorHandler();

  /* 阻塞发送 16 个样本 */
  if (HAL_I2S_Transmit(&I2sHandle, (uint16_t*)TxBuff, DATA_LENGTH, 5000) != HAL_OK)
    APP_ErrorHandler();
  while (HAL_I2S_GetState(&I2sHandle) != HAL_I2S_STATE_READY) {}  /* 等发完 */

  HAL_Delay(1);   /* 给从机一点准备时间 */

  /* ===== 第二步:切换为主机接收(RX)===== */
  I2sHandle.Init.Mode = I2S_MODE_MASTER_RX;         /* 改为主机接收 */
  if (HAL_I2S_Init(&I2sHandle) != HAL_OK) APP_ErrorHandler();

  /* 阻塞接收 16 个样本 */
  if (HAL_I2S_Receive(&I2sHandle, (uint16_t*)RxBuff, DATA_LENGTH, 5000) != HAL_OK)
    APP_ErrorHandler();
  while (HAL_I2S_GetState(&I2sHandle) != HAL_I2S_STATE_READY) {}

  /* ===== 校验 ===== */
  APP_CheckEndOfTransfer();

  while (1) {}
}

/**
  * @brief  比对收发数据:一致亮灯,不一致闪灯
  */
static void APP_CheckEndOfTransfer(void)
{
  if (APP_Buffercmp8((uint8_t*)TxBuff, (uint8_t*)RxBuff, DATA_LENGTH))
    APP_LedBlinking();           /* 失败闪烁 */
  else
    BSP_LED_On(LED_GREEN);       /* 成功常亮 */
}

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;
}

static void APP_LedBlinking(void)
{
  while (1) { BSP_LED_Toggle(LED_GREEN); HAL_Delay(500); }
}

/**
  * @brief  系统时钟:HSI 8MHz(I2S 时钟由 SPI2/APB1 派生,HAL 内部按 AudioFreq 分频)
  */
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(SPI2 源)*/
  ClkInitstruct.APB2CLKDivider = RCC_HCLK_DIV2;         /* APB2 = 4MHz */
  if (HAL_RCC_ClockConfig(&ClkInitstruct, FLASH_LATENCY_0) != HAL_OK) APP_ErrorHandler();
}

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void HAL_I2S_MspInit(I2S_HandleTypeDef *hi2s)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  __HAL_RCC_SPI2_CLK_ENABLE();     /* I2S 复用 SPI2 时钟 */
  __HAL_RCC_GPIOB_CLK_ENABLE();

  /* PB12=WS, PB13=CK, PB15=SD,均 AF14 */
  GPIO_InitStruct.Pin       = GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_15;
  GPIO_InitStruct.Mode      = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Pull      = GPIO_NOPULL;
  GPIO_InitStruct.Speed     = GPIO_SPEED_FREQ_HIGH;
  GPIO_InitStruct.Alternate = GPIO_AF14_I2S2;
  HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}

4. 代码分段讲解

4.1 为什么用 SPI2 当 I2S?

PY32F403 的 I2S 功能复用在 SPI2 这个外设上(硬件层面共享)。所以代码里 Instance = SPI2,但调用的是 HAL_I2S_* 函数——HAL 会根据你选的 I2S_* 模式把 SPI 模块切到 I2S 工作方式,引脚走 I2S 的 AF14 复用。

4.2 采样率与时钟关系

I2S_AUDIOFREQ_8K 设定目标采样率 8kHz。HAL 内部会用 SPI2(APB1=8MHz)的时钟去分频,产生:

  • WS 频率 ≈ 8kHz(每 8k 次采样切换一次左右声道);
  • CK 频率 = 8k × 16bit × 2声道 = 256kHz

想换 44.1k/48k 音乐采样率,把 AudioFreq 改掉即可(前提是主时钟能分频得到,必要时打开 MCLK 给 CODEC 作基准)。

4.3 主机先发后收:同一句柄切 Mode

同一 I2sHandle,先 I2S_MODE_MASTER_TX 发完,再 I2S_MODE_MASTER_RX 收,重新 HAL_I2S_Init 即可切换方向。真实音频链路里方向通常固定(DAC 端只发、ADC 端只收),本例为演示才来回切。

4.4 标准格式必须主从一致

I2S_STANDARD_PHILIPS + I2S_CPOL_HIGH 是从机示例默认的格式。只要主从任何一方格式不同(如改成 MSB 对齐),数据就会错位。这是接 CODEC 时最常踩的坑。

4.5 轮询的局限

HAL_I2S_Transmit/Receive 会阻塞到完成或超时(5s)。教学够用,但连续音频流不能这么干——要用 *_DMA 版把样本流持续搬进/出缓冲。这正是 I2S_..._DMA 示例的用途。

5. 实验现象

  • 正确接线、先跑从机再按主机键后:
    • 主机 LED 常亮 = 收发校验成功(收到 {1..16});
    • 任一板 LED 闪烁 = 失败(查接线、共地、主从格式是否一致)。
  • 若用逻辑分析仪抓 CK/WS/SD,可见 WS 以 8kHz 翻转、SD 上逐位送出样本。

6. 常见问题

现象 原因 解决
LED 一直闪(失败) 没共地 / 线序错 / 从机没先跑 接好 GND,CK/WS 主机→从机,SD 交叉;先按从机键
收到全 0 或错乱 主从 Standard/CPOL 不一致 双方都用 PHILIPS + CPOL_HIGH
音频有杂音/采样率偏 系统时钟与 AudioFreq 不匹配 确认主时钟;必要时开 MCLK 输出
想连续播放音乐 轮询占 CPU 改用 I2S_..._DMA + 双缓冲
找不到 I2S 引脚 误用 SPI 引脚 I2S2 = PB12/13/15(AF14),不是普通 SPI2 MOSI/MISO
只有一路声道 只发了单声道 I2S 默认立体声;单声道数据需按格式填充

7. 小结

  • I2S 是音频专用同步总线,由 SPI2 复用实现(引脚 PB12/13/15,AF14);
  • 配置四要素:标准格式、位宽、采样率、主/从模式,且主从必须完全一致
  • 例程用 8kHz、16 位,主机先 Master_TX 发 16 样本、再 Master_RX 收回校验;
  • 轮询简单但占 CPU;真实音频流用 DMA/IT 版(I2S_..._DMA)。

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

  • 播放提示音/WAV:I2S DAC(MAX98357A)接 SD/CK/WS,DMA 送 PCM 数据;
  • 录音/声控:I2S MEMS 麦克风做采集,配合 FFT 做频谱;
  • 双向音频:CODEC(WM8978)接 I2S,做通话/采集;
  • 不间断音频流I2S_..._DMA + 双缓冲,从 Flash/SD 卡连续取数据;
  • 数字功放/语音识别前端:I2S 是标准接口,方便对接各类音频芯片。

返回:I2S 音频总线总览

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