PUYA I2C — 主机轮询通信(LL 库)

PUYA PY32F0xx_Firmware V1.5.0 I2C 主机轮询通信(I2C_TwoBoard_CommunicationMaster_Polling_Init)示例教学(LL 库版本)

I2C — 主机轮询通信(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/I2C/I2C_TwoBoard_CommunicationMaster_Polling_Init/Src/main.c


功能简介

本示例演示 I2C 主机(Master)轮询模式:按键后,主机先向从机地址 0xA0 发送 15 字节,再从中机读回 15 字节(对端跑 Slave 示例做回环),最后比对收发数据——一致则点亮 LED,不一致则 LED 闪烁报错。

与 HAL 版(HAL_I2C_Master_Transmit / HAL_I2C_Master_Receive 一行搞定)相比,LL 版把 I2C 的完整状态机摊开在你面前:起始条件 → 地址 → 数据 → 停止条件,每一步都靠轮询标志位(SB/ADDR/TXE/BTF/RXNE)推进。代码长,但能让你真正搞懂 I2C“为什么这么写”。

硬件准备

  • 两块 PY32F030 开发板(一块作主、一块作从)
  • 主从板 SCL(PA9) 与 SDA(PA10) 交叉相连,并各自接上拉电阻(示例里已配置内部上拉)
  • 主机板载按键(KEY)、绿 LED
  • 串口/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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#include "main.h"
#include "py32f030xx_ll_Start_Kit.h"

#define I2C_ADDRESS        0xA0     /* 从机地址 */
#define I2C_SPEEDCLOCK     100000   /* 100K 标准速率 */
#define I2C_STATE_READY    0
#define I2C_STATE_BUSY_TX  1
#define I2C_STATE_BUSY_RX  2

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

uint8_t         *pBuffPtr  = NULL;
__IO uint16_t   XferCount  = 0;
__IO uint32_t   Devaddress = 0;
__IO uint32_t   State      = I2C_STATE_READY;

static void APP_SystemClockConfig(void);
static void APP_ConfigI2cMaster(void);
static void APP_MasterTransmit(uint16_t DevAddress, uint8_t *pData, uint16_t Size);
static void APP_MasterReceive(uint16_t DevAddress, uint8_t *pData, uint16_t Size);
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)
{
  APP_SystemClockConfig();
  BSP_LED_Init(LED_GREEN);
  BSP_PB_Init(BUTTON_KEY, BUTTON_MODE_GPIO);

  APP_ConfigI2cMaster();

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

  APP_MasterTransmit(I2C_ADDRESS, (uint8_t *)aTxBuffer, sizeof(aTxBuffer));
  while (State != I2C_STATE_READY);           /* 等发送完 */
  APP_MasterReceive(I2C_ADDRESS, (uint8_t *)aRxBuffer, sizeof(aRxBuffer));
  while (State != I2C_STATE_READY);           /* 等接收完 */

  APP_CheckEndOfTransfer();                   /* 比对结果 */
  while (1) {}
}

/**
  * @brief  I2C 主机 GPIO + 外设配置
  */
static void APP_ConfigI2cMaster(void)
{
  LL_IOP_GRP1_EnableClock(LL_IOP_GRP1_PERIPH_GPIOA);
  LL_APB1_GRP1_EnableClock(LL_APB1_GRP1_PERIPH_I2C1);

  /* SCL=PA9, SDA=PA10:复用、开漏、上拉、AF6 */
  LL_GPIO_InitTypeDef GPIO_InitStruct = {0};
  GPIO_InitStruct.Pin        = LL_GPIO_PIN_9;
  GPIO_InitStruct.Mode       = LL_GPIO_MODE_ALTERNATE;
  GPIO_InitStruct.Speed      = LL_GPIO_SPEED_FREQ_HIGH;
  GPIO_InitStruct.OutputType = LL_GPIO_OUTPUT_OPENDRAIN;   /* I2C 必须开漏 */
  GPIO_InitStruct.Pull       = LL_GPIO_PULL_UP;
  GPIO_InitStruct.Alternate  = LL_GPIO_AF_6;
  LL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  GPIO_InitStruct.Pin = LL_GPIO_PIN_10;
  LL_GPIO_Init(GPIOA, &GPIO_InitStruct);

  /* 复位 I2C 外设 */
  LL_APB1_GRP1_ForceReset(LL_APB1_GRP1_PERIPH_I2C1);
  LL_APB1_GRP1_ReleaseReset(LL_APB1_GRP1_PERIPH_I2C1);

  LL_I2C_InitTypeDef I2C_InitStruct = {0};
  I2C_InitStruct.ClockSpeed      = I2C_SPEEDCLOCK;
  I2C_InitStruct.DutyCycle       = LL_I2C_DUTYCYCLE_16_9;
  I2C_InitStruct.OwnAddress1     = I2C_ADDRESS;
  I2C_InitStruct.TypeAcknowledge = LL_I2C_NACK;
  LL_I2C_Init(I2C1, &I2C_InitStruct);
}

/**
  * @brief  LL 主机发送:起始→地址→数据→停止,全程轮询标志
  */
static void APP_MasterTransmit(uint16_t DevAddress, uint8_t *pData, uint16_t Size)
{
  pBuffPtr = pData; XferCount = Size; Devaddress = DevAddress;
  State = I2C_STATE_BUSY_TX;

  LL_I2C_GenerateStartCondition(I2C1);
  while (LL_I2C_IsActiveFlag_SB(I2C1) != 1);           /* 等起始条件发送完 */

  LL_I2C_TransmitData8(I2C1, (Devaddress & (uint8_t)(~0x01)));  /* 发写地址(末位0) */
  while (LL_I2C_IsActiveFlag_ADDR(I2C1) != 1);
  LL_I2C_ClearFlag_ADDR(I2C1);                          /* 清地址标志 */

  while (XferCount > 0)
  {
    while (LL_I2C_IsActiveFlag_TXE(I2C1) != 1);         /* 等发送寄存器空 */
    LL_I2C_TransmitData8(I2C1, *pBuffPtr);              /* 发一字节 */
    pBuffPtr++; XferCount--;
    while (LL_I2C_IsActiveFlag_BTF(I2C1) != 1);         /* 等字节发完(BTF) */
  }

  LL_I2C_GenerateStopCondition(I2C1);                   /* 发停止条件 */
  State = I2C_STATE_READY;
}

/**
  * @brief  LL 主机接收:起始→地址(读)→接收 N 字节→停止
  */
static void APP_MasterReceive(uint16_t DevAddress, uint8_t *pData, uint16_t Size)
{
  pBuffPtr = pData; XferCount = Size; Devaddress = DevAddress;
  State = I2C_STATE_BUSY_RX;

  LL_I2C_AcknowledgeNextData(I2C1, LL_I2C_ACK);        /* 先允许 ACK */
  LL_I2C_GenerateStartCondition(I2C1);
  while (LL_I2C_IsActiveFlag_SB(I2C1) != 1);
  LL_I2C_TransmitData8(I2C1, (Devaddress | 0x1));      /* 发读地址(末位1) */
  while (LL_I2C_IsActiveFlag_ADDR(I2C1) != 1);
  LL_I2C_ClearFlag_ADDR(I2C1);

  while (XferCount > 0)
  {
    while (LL_I2C_IsActiveFlag_RXNE(I2C1) != 1);        /* 等收到一字节 */
    *pBuffPtr = LL_I2C_ReceiveData8(I2C1);              /* 读一字节 */
    pBuffPtr++; XferCount--;
  }
  State = I2C_STATE_READY;
}

/* APP_CheckEndOfTransfer / APP_Buffercmp8 / APP_LedBlinking / 时钟与错误处理
   与前述示例同构,略 */

代码分段讲解

1. 开漏 + 上拉:I2C 物理层铁律

I2C 总线靠“线与”工作,SCL/SDA 必须开漏LL_GPIO_OUTPUT_OPENDRAIN)+ 外部/内部上拉。示例用内部上拉(LL_GPIO_PULL_UP)。HAL 版在 HAL_I2C_Init 里通过 I2C_HandleTypeDef 同样隐含了这些设置,但 LL 让你看得一清二楚。

2. 发送状态机(核心)

LL 的发送就是“手搓 I2C 协议帧”:

  1. LL_I2C_GenerateStartCondition() 拉出 START;
  2. SB 标志 → 写从机地址(写=地址&0xFE);
  3. ADDR 标志并清除 → 进入数据传输;
  4. 每字节等 TXE(发送寄存器空)写数据,再等 BTF(字节传输完);
  5. 最后 LL_I2C_GenerateStopCondition() 发 STOP。

对比 HAL:HAL_I2C_Master_Transmit() 把以上 5 步全藏在库里,你只传“地址+缓冲区+长度”。

3. 接收状态机

接收时地址末位置 1(读=地址|0x01),之后轮询 RXNE 逐字节读取。示例为简化只演示了“固定长度、一直 ACK”的情形;HAL 内部还处理了“最后一字节发 NACK + STOP”的时序细节(源码里那些 XferCount<=3 的分支就是干这个的)。

4. 没有超时!

注意这些 while(标志 != 1); 都是死等。真实总线若从机无响应(地址错、没接上拉、线断),程序会卡死。真实项目务必加超时或改用中断(I2C_TwoBoard_CommunicationMaster_IT_Init)。

实验现象

  1. 主从两块板 SCL/SDA 连接好,从机先跑 Slave 示例。
  2. 主机按下 KEY,先向 0xA0 发 15 字节。
  3. 主机再读回 15 字节,与发送缓冲比对。
  4. 一致 → 绿 LED 常亮;不一致 → 绿 LED 闪烁。

常见问题

  1. 卡在等 ADDR / 一直不进数据?

    • 最常见:从机地址不对、SCL/SDA 没接上拉、主从没共地。
    • 确认从机确实在跑对应的 Slave 示例并监听 0xA0
  2. 死等卡死?

    • LL 轮询无超时。用逻辑分析仪看波形,确认 START/地址是否真的出现在总线上。
  3. 想用中断而不是轮询?

    • 参考 I2C_TwoBoard_CommunicationMaster_IT_Init,用 LL_I2C_EnableIT_xxx + 回调处理。

小结

  • LL 的 I2C 主机 = 手搓协议帧:START → 地址 → 数据 → STOP,每步轮询标志。
  • 相比 HAL 的“一行收发”,LL 代码长但完全透明,没有隐藏时序。
  • 物理层(开漏+上拉)和“无超时死等”是两个最需要警惕的点。

拓展应用

  • 把轮询改成中断/IT 版本,避免阻塞主循环。
  • 接入真实传感器(如 EEPROM、温湿度芯片),把 0xA0 换成器件真实地址。
  • 用 DMA 搬运长数据(参考 I2C_TwoBoard_CommunicationMaster_DMA_Init)。
  • 增加超时退出,提升总线异常时的鲁棒性。
世界是你们
使用 Hugo 构建
主题 StackJimmy 设计