PUYA I2C — 双板主机轮询通信 I2C_TwoBoard_CommunicationMaster_Polling

PUYA PY32F403_Firmware V1.4.5 I2C 双板主机轮询通信示例教学

I2C — 双板主机轮询通信 I2C_TwoBoard_CommunicationMaster_Polling

固件库: PY32F403_Firmware V1.4.5
芯片: PY32F403(Cortex-M4)
源文件: PY32F403-STK/Example/I2C/I2C_TwoBoard_CommunicationMaster_Polling/Src/main.c
核心外设: I2C1(主机,100K,轮询)


1. 功能简介(大白话)

两块 PY32F403-STK,一块当 主机(Master),一块当 从机(Slave)。主机按下用户键后,先通过 I2C 给从机发 15 个字节1,2,...,15),发完再把从机回发的 15 个字节收回来。最后主机比较「发出去的是不是等于收回来的」:相等则点亮 LED,不相等就 LED 闪烁报错。

和 SPI 例程不同,I2C 是半双工——这里是「先发后收」两步完成,而不是同时收发。

2. 硬件准备

物品 数量 说明
PY32F403-STK 开发板 2 主机烧 Master_Polling,从机烧 Slave_IT(或对应 Slave 例程)
USB 线 2 供电 + 下载
上拉电阻 4.7kΩ 2 SCL、SDA 各一(开发板通常已带上拉)

接线(两根线交叉对应,务必共地):

1
2
3
4
主机 MASTER              从机 SLAVE
PB6 (I2C1_SCL) ----->  PB6 (I2C1_SCL)
PB7 (I2C1_SDA) ----->  PB7 (I2C1_SDA)
GND            ----->  GND

STK 板上的 I2C1 引脚(PB6/PB7)一般已板载上拉电阻;若自己搭电路务必外接 4.7kΩ 上拉到 VCC。

操作步骤

  1. 主机烧 I2C_TwoBoard_CommunicationMaster_Polling,从机烧 I2C_TwoBoard_CommunicationSlave_IT
  2. 先复位从机,再复位主机
  3. 按主机用户键启动通信;
  4. 主从 LED 由灭变常亮 = 成功;任一 LED 闪烁 = 失败。

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

/* 数据长度、地址、速率定义 */
#define DATA_LENGTH     15                   /* 收发 15 字节 */
#define I2C_ADDRESS     0xA0                 /* 本 demo 使用的地址 */
#define I2C_SPEEDCLOCK  100000               /* 标准速率 100K */
#define I2C_DUTYCYCLE   I2C_DUTYCYCLE_16_9   /* 快速模式占空比(100K 下影响不大)*/

/* I2C 句柄与缓冲 */
I2C_HandleTypeDef I2cHandle;
uint8_t aTxBuffer[15] = {1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};
uint8_t aRxBuffer[15] = {0};

/* 函数声明 */
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)
{
  /* 1) HAL 初始化 */
  HAL_Init();
  /* 2) 系统时钟:HSI 8MHz;APB1 = 8MHz(I2C1 时钟源)*/
  APP_SystemClockConfig();

  /* 3) LED 与按键 */
  BSP_LED_Init(LED_GREEN);
  BSP_PB_Init(BUTTON_KEY, BUTTON_MODE_GPIO);

  /* 4) I2C1 配置 */
  I2cHandle.Instance             = I2C1;
  I2cHandle.Init.ClockSpeed      = I2C_SPEEDCLOCK;          /* 100K */
  I2cHandle.Init.DutyCycle       = I2C_DUTYCYCLE;           /* 占空比 */
  I2cHandle.Init.OwnAddress1     = I2C_ADDRESS;            /* 自身地址 */
  I2cHandle.Init.AddressingMode  = I2C_ADDRESSINGMODE_7BIT;/* 7 位地址 */
  I2cHandle.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE; /* 不用双地址 */
  I2cHandle.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;/* 不响应广播 */
  I2cHandle.Init.NoStretchMode   = I2C_NOSTRETCH_DISABLE;  /* 允许时钟拉伸 */
  if (HAL_I2C_Init(&I2cHandle) != HAL_OK) APP_ErrorHandler(); /* 内部调 MSP 配 PB6/PB7 */

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

  /* 6) 主机轮询发送 15 字节(阻塞,超时 5s)*/
  if (HAL_I2C_Master_Transmit(&I2cHandle, I2C_ADDRESS, aTxBuffer, DATA_LENGTH, 5000) != HAL_OK)
    APP_ErrorHandler();
  while (HAL_I2C_GetState(&I2cHandle) != HAL_I2C_STATE_READY) {}  /* 等回到 READY */

  /* 7) 主机轮询接收 15 字节 */
  if (HAL_I2C_Master_Receive(&I2cHandle, I2C_ADDRESS, aRxBuffer, DATA_LENGTH, 5000) != HAL_OK)
    APP_ErrorHandler();
  while (HAL_I2C_GetState(&I2cHandle) != HAL_I2C_STATE_READY) {}

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

  while (1) { }
}

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

/**
  * @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;
}

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

/**
  * @brief  系统时钟:HSI 8MHz;APB1 = 8MHz(I2C1 源)
  */
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 */
  if (HAL_RCC_ClockConfig(&ClkInitstruct, FLASH_LATENCY_0) != HAL_OK) APP_ErrorHandler();
}

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

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

1
2
3
4
5
6
7
/* PB6 = I2C1_SCL, PB7 = I2C1_SDA,均 AF1,开漏 + 上拉 */
GPIO_InitStruct.Pin       = GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode      = GPIO_MODE_AF_OD;        /* 关键:开漏!*/
GPIO_InitStruct.Pull      = GPIO_PULLUP;            /* 内部上拉(外部也建议再加 4.7k)*/
GPIO_InitStruct.Speed     = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF1_I2C1;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

4. 代码分段讲解

4.1 为什么 SCL/SDA 必须是「开漏 + 上拉」?

I2C 是多设备共用一条线的总线,任何一个器件都可能拉低它。若用推挽输出,两个器件一个拉高一个拉低就会短路。开漏保证「谁想拉低谁拉低,平时靠上拉电阻维持高电平」,多个器件不会互相打架——这是 I2C 能「一总线挂多设备」的物理基础。所以引脚模式务必是 GPIO_MODE_AF_OD

4.2 先发后收:半双工的体现

代码里 HAL_I2C_Master_Transmit(发)和 HAL_I2C_Master_Receive(收)是两次独立调用。I2C 总线同一时刻只能单向传,所以主机要先写完、回到 READY,再启动读。每步都检查 HAL_I2C_GetState == READY 确保上一步彻底结束。

4.3 地址 0xA0 是什么?

本 demo 里主机 OwnAddress1 和对外寻址都用了 0xA0。这是示例自定义的地址,主从必须一致才能通信。实际接真实芯片(如 EEPROM AT24C02)时,要换成该芯片手册规定的地址(通常 7 位如 0x50),并注意 HAL 接口传的是 7 位地址。

4.4 时钟拉伸(Clock Stretching)

NoStretchMode = DISABLE 表示允许从机在忙时把 SCL 拉低「拖住」主机,等自己准备好再放开。很多传感器(如 SHT30)靠这招告诉主机「稍等」。保留默认允许即可。

4.5 速率怎么改?

直接改 I2C_SPEEDCLOCK:100000(标准)、400000(快速)。注意提高速率要保证上拉电阻合适、总线不长,否则波形失真。

5. 实验现象

  • 正确接线、先跑从机再按主机按键后:
    • 主机 LED 常亮 = 收发校验成功;
    • 任一板 LED 闪烁 = 通信失败(检查接线、共地、地址、上拉)。

6. 常见问题

现象 原因 解决
LED 一直闪(失败) 没上拉 / 没共地 / 线接反 加 4.7k 上拉、接好 GND、SCL-SCL/SDA-SDA
完全无响应、卡在发送 从机没先启动 先复位从机,再复位/按键启动主机
偶发通信错误 上拉太弱 / 速率过高 / 总线长 减小上拉电阻、降到 100K、缩短走线
接真实 EEPROM 不响应 地址不对(7 位 vs 8 位) 用器件手册的 7 位地址;写后延时再读
读到全 0xFF 从机没回数据 / 上拉缺失 查上拉与从机程序是否匹配
想提速 改 I2C_SPEEDCLOCK 400K 需更优的上拉与时序

7. 小结

  • I2C 是两线、半双工、多主从总线,靠地址选设备,靠开漏+上拉实现多设备共线;
  • 例程用 I2C1(APB1=8MHz)100K,主机轮询「先发 15 字节、再收 15 字节」;
  • 引脚必须 开漏 + 上拉(PB6/PB7,AF1),否则总线拉不高;
  • 主从地址、速率必须一致,且从机要先于主机就绪;
  • 校验方法是「发出去的 == 收回来的」逐字节比较。

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

  • 参数存储:参考 I2C_xxx_DMA_MEM,用 AT24Cxx 保存配置、校准值,断电不丢;
  • 环境传感器:挂 SHT30(温湿度)、BMP280(气压)、MPU6050(姿态),做气象站;
  • 实时时钟:接 DS3231 做带断电走时的电子钟;
  • OLED 显示:SSD1306(I2C 版)小屏,做菜单/状态显示;
  • IO 扩展:PCF8574 扩展按键/LED,省 MCU 引脚;
  • 非阻塞进阶:把轮询换成 I2C_xxx_IT / I2C_xxx_DMA,主循环不卡等,适合复杂项目。

返回:I2C 总线总览

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