一、概述

    无论是新手还是大佬,基于STM32单片机的开发,使用STM32CubeMX都是可以极大提升开发效率的,并且其界面化的开发,也大大降低了新手对STM32单片机的开发门槛。
    本文主要讲述STM32芯片硬件IIC功能的配置及其相关知识。

二、软件说明

    STM32CubeMX是ST官方出的一款针对ST的MCU/MPU跨平台的图形化工具,支持在Linux、MacOS、Window系统下开发,其对接的底层接口是HAL库,另外习惯于寄存器开发的同学们,也可以使用LL库。STM32CubeMX除了集成MCU/MPU的硬件抽象层,另外还集成了像RTOS,文件系统,USB,网络,显示,嵌入式AI等中间件,这样开发者就能够很轻松的完成MCU/MPU的底层驱动的配置,留出更多精力开发上层功能逻辑,能够更进一步提高了嵌入式开发效率。
    演示版本 6.1.0

三、IIC与EEPROM功能简介

    这个在另外两篇文章中有详解,这里就不多赘述了。
【知识分享】IIC协议详解
【知识分享】EEPROM相关知识

四、配置说明

4.1 硬件IIC框图

    之前网上流传着一个说法,说ST芯片的硬件IIC存在Bug(但他就是不说是什么Bug,这种人就很坑),所以大部分资料使用EEPROM都是用的模拟IIC。然而一次偶然的机会,发现一位大佬的代码备注里写着:硬件IIC配置太复杂,使用模拟IIC比较简单。这跟硬件IIC资料偏少,可能有着某种联系。即使有Bug,也要用起来看下是什么问题,毕竟在嵌入式领域,资源有限是永久的话题,既然芯片厂家提供了硬件IIC这个资源,那不用白不用。

在这里插入图片描述
    这是STM32H750XB芯片上硬件IIC控制器模块的架构框图,用于实现IIC总线通信功能,可从信号输入、核心功能模块、信号处理流程、对外接口 4 部分理解:

4.1.1 信号输入

I2c_ker_ck(I2CCLK):I²C 内核时钟,为模块内部逻辑(如数据传输、时钟控制)提供时序基准,决定 I²C 通信速率的基础。
I2c_pdk(PCLK):外设时钟,用于寄存器访问、模块配置等辅助逻辑,保障与系统总线(APB bus)交互的时序。

4.1.2 核心功能模块(灰色区域)

Data control(数据控制)
Shift register(移位寄存器):负责数据的串并转换。发送时,并行数据转为串行通过 I2C_SDA 输出;接收时,串行数据转为并行供系统处理。
SMBUS PEC generation/check:实现 SMBus(I²C 扩展协议)的 数据包错误校验(PEC),自动生成或校验数据校验码,检测通信错误。
Clock control(时钟控制)
Master clock generation:作为 I²C 主机时,生成 I2C_SCL 时钟信号,控制通信节奏。
Slave clock stretching:作为从机时,可主动拉低 I2C_SCL 延长时钟(时钟拉伸),用于从机等待数据准备或处理延时。
SMBus Timeout check:监控 SMBus 通信超时,避免因从机无响应导致总线挂死。
Wakeup on address match:地址匹配唤醒功能。若总线上的地址与模块配置地址匹配,可触发系统唤醒(低功耗场景常用)。
SMBus Alert control & status:处理 SMBus 报警信号(I2C_SMBA),管理从机主动报警的状态与控制逻辑。

4.1.3 信号处理流程(I2C_SDA/I2C_SCL 通路)

Analog noise filter(模拟噪声滤波):先对 I2C_SDA/I2C_SCL 输入信号做模拟滤波,去除电压毛刺、干扰。
Digital noise filter(数字噪声滤波):进一步做数字滤波,消抖、过滤高频噪声,保障信号干净。
GPIO logic:实现引脚复用(GPIO 功能与 I²C 功能切换),让硬件引脚可灵活配置为普通 GPIO 或 I²C 总线。

4.1.4 对外接口

I2C_SDA/I2C_SCL:标准 I²C 总线引脚,分别用于数据传输、时钟同步,连接外部 I²C 设备(传感器、EEPROM 等)。
I2C_SMBA:SMBus 报警引脚,支持从机主动向主机上报异常。
Registers(寄存器):软件可通过 APB bus 访问这些寄存器,配置 I²C 通信参数(如从机地址、速率)、读写数据、查询状态。
APB bus:模块与系统 CPU / 内存的通信总线,软件通过它配置 I²C 控制器、收发数据。
总结
这个框图展示了 “硬件化的 I²C 通信引擎”:输入时钟保障时序,核心模块处理数据 / 时钟逻辑,滤波电路抗干扰,寄存器和 APB 总线实现软件可控,让系统无需纯软件模拟 I²C,高效、稳定地与外部设备通信 。

4.2 CubeMX配置界面

在这里插入图片描述
    这是STM32H750XB单片机IIC外设的配置界面,用于设置 I²C 总线的工作模式、通信参数和功能特性,以下拆解核心配置项:

4.2.1 模式选择(Mode)

下拉框提供 4 种模式,决定 I²C 控制器的工作协议:
Disable:关闭 I²C 外设,不启用总线功能。
I2C:标准 I²C 总线模式,支持传统主从通信(一般选用这个)。
SMBus-Alert-mode:SMBus(System Management Bus,I²C 扩展协议)报警模式,用于从机主动向主机上报异常(如传感器故障)。
SMBus-two-wire-Interface:SMBus 双线模式,兼容 SMBus 协议的常规通信(与标准 I²C 接近,但增加了超时、报警等管理)。

4.2.2 核心参数(Parameter Settings)

  • Timing configuration(时序配置)

Custom Timing:Disabled 表示使用工具自动计算的时序;若 Enabled,需手动填写寄存器值(高级调试用)。
I2C Speed Mode:Standard Mode(标准模式,100KHz),还可支持 Fast Mode(400KHz)、Fast Mode Plus(1MHz)等,决定通信速率。
I2C Speed Frequency (KHz):100 表示总线时钟频率 100KHz(与模式对应,标准模式下通常设 100)。
Rise Time/Fall Time (ns):配置 SDA/SCL 信号的上升沿、下降沿时间(影响总线抗干扰性,需匹配硬件电平切换速度)。
Coefficient of Digital Filter:数字滤波器系数,过滤总线上的高频噪声(值越大滤波越强,但可能延迟信号)。
Analog Filter:Enabled 启用模拟滤波(硬件消抖,减少电压毛刺干扰)。
Timing:工具自动生成的时序寄存器值(无需手动改,模式 / 速率变化时自动更新)。

  • Slave Features(从机特性,仅当设备作为 I²C 从机时生效)

Clock No Stretch Mode:Disabled 表示允许从机拉伸时钟(从机可拉低 SCL 暂停通信,用于处理数据);Enabled 则禁止时钟拉伸。
General Call Address Detection:Disabled 关闭广播地址检测(0x00 地址呼叫时不响应);Enabled 则响应广播。
Primary Address Length selection:7-bit 表示从机地址为 7 位(最常用,兼容多数设备),还可设 10-bit(地址空间更大)。
Dual Address Acknowledged:Disabled 表示仅响应 1 个从机地址;Enabled 可配置双地址,响应两个不同地址。
Primary slave address:0 需改为实际从机地址(如 0x50,取决于外接设备的 I²C 地址)。

五、应用配置

    这里用的是正点原子的ALIENTEK北极星STM32H750开发板,查看开发板原理图,开发板上EEPROM是接在PH4、PH5脚上。
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
    因为开发板EEPROM连的是PH4、PH5引脚,所以这里先直接找到PH4、PH5引脚,选择想要复用的功能,这两个引脚对应的是IIC2。找到IIC2后,选择标准IIC模式。因为默认的配置中,频率设置的是100kHz,低于标称400kHz,按默认配置即可,如果对性能有要求的,可以提高至400kHz。从机设备地址我们用的是7位的,这里也按默认7位配置即可。其他均使用默认配置,不需要更改。

六、接口说明

    因为我们用的是标准IIC的主机功能,且初始化部分的工作已经由CubeMX自动生成,这里我们只需要关注过程中的收发功能,即HAL_I2C_Master_TransmitHAL_I2C_Master_Receive接口。

6.1 发送接口(HAL_I2C_Master_Transmit)

6.1.1 核心功能

    在 I2C 通信中,让 STM32 以 主设备身份,向挂载在 I2C 总线上的从设备(通过设备地址区分)发送一段连续数据,完成 “主 → 从” 的单向数据传输,比如给 EEPROM 发送写入命令 + 数据、给传感器发送配置指令等。

6.1.2 函数原型与参数解析

HAL_StatusTypeDef HAL_I2C_Master_Transmit(
    I2C_HandleTypeDef *hi2c,   // I2C 外设句柄,关联硬件配置
    uint16_t DevAddress,       // 从设备地址(7 位或 10 位,需左移 + 读写位)
    uint8_t *pData,            // 要发送的数据缓冲区指针
    uint16_t Size,             // 发送数据的字节数
    uint32_t Timeout           // 超时时间(毫秒),超时则返回错误
);

I2C_HandleTypeDef *hi2c
指向 I2C 外设的配置结构体(如 hi2c1/hi2c2 ),包含 I2C 时钟、模式、引脚等初始化参数,是 HAL 库操作硬件的 “桥梁”。

uint16_t DevAddress
从设备的 I2C 地址,需遵循 I2C 协议规则:
7 位地址:实际地址左移 1 位(腾出最低位作 “读写位” ),写操作时最低位为 0(如设备地址 0x50,传入 0xA0 );
10 位地址:直接传入完整地址(如 0x0378 ),需确保 MCU 支持 10 位地址模式。

uint8_t *pData
指向待发送数据的缓冲区,数据存储遵循 大端模式(先发送数组低索引数据,如 pData[0] → pData[1] → … )。

uint16_t Size
要发送的字节总数,决定函数会从 pData 中连续取多少字节发往从设备。

uint32_t Timeout
超时阈值(毫秒),若 I2C 总线因从设备无响应、总线冲突等问题,导致传输未完成且超过该时间,函数会返回超时错误(HAL_TIMEOUT )。

6.1.3 执行流程(简化版)

总线忙检测:检查 I2C 总线是否被占用(BUSY 标志位),若忙则等待或返回 HAL_BUSY。
锁定外设:通过 __HAL_LOCK(hi2c) 防止多任务同时操作 I2C,保证传输原子性。
发送起始信号:生成 I2C 起始条件(SCL 高电平时,SDA 从高变低 ),启动通信。
发送从地址:将 DevAddress 发往从设备,等待从设备应答(ACK);若无应答,返回 HAL_ERROR。
循环发送数据:逐个字节发送 pData 缓冲区内容,每次发完 1 字节后,等待从设备应答;若超时或无应答,返回错误。
发送停止信号:数据发完后,生成 I2C 停止条件(SCL 高电平时,SDA 从低变高 ),结束通信。
释放外设:解锁 I2C,更新状态为 HAL_I2C_STATE_READY,返回 HAL_OK(成功)或对应错误码。

6.1.4 典型应用场景

向无内部地址的从设备发指令:如简单传感器只需接收 “开始测量” 命令,直接用该函数发 1 字节指令。
EEPROM 写入(前半段):给 EEPROM 写入数据时,需先通过此函数发送 “写入命令 + 目标内存地址”,再配合其他操作发数据(或用 HAL_I2C_Mem_Write 简化流程 )。
配置从设备寄存器:向传感器、模块的配置寄存器写参数(如设置 I2C 从设备的采样率 ),将 “寄存器地址 + 配置值” 打包到 pData 发送。

6.1.5 与类似函数的区别

函数名 区别核心点 典型场景
HAL_I2C_Master_Transmit 仅主 → 从单向发数据,不涉及从设备内部地址 发指令、无地址从设备通信
HAL_I2C_Mem_Write 主 → 从发数据 + 指定从设备内部寄存器地址 EEPROM 写、带寄存器从设备配置
HAL_I2C_Master_Receive 主 → 从发地址后,主 ← 从接收数据 读传感器数据、EEPROM 读

    简单说,HAL_I2C_Master_Transmit 是 I2C 主设备 “单纯发数据” 的工具,适合无需指定从设备内部地址的场景,是 STM32 I2C 通信中最基础的 主模式写操作 函数 。

6.2 接收接口(HAL_I2C_Master_Receive)

6.2.1 核心功能

    在 I2C 通信中,让 STM32 以 主设备身份,主动从指定从设备(通过地址区分)读取一段连续数据,完成 “从 → 主” 的单向数据接收,比如从 EEPROM 读存储内容、从传感器读测量数据等。

6.2.2 函数原型与参数解析

HAL_StatusTypeDef HAL_I2C_Master_Receive(
    I2C_HandleTypeDef *hi2c,   // I2C 外设句柄,关联硬件配置
    uint16_t DevAddress,       // 从设备地址(7 位或 10 位,需左移 + 读写位)
    uint8_t *pData,            // 存储接收数据的缓冲区指针
    uint16_t Size,             // 要接收的字节数
    uint32_t Timeout           // 超时时间(毫秒),超时则返回错误
);

I2C_HandleTypeDef *hi2c
指向 I2C 外设的配置结构体(如 hi2c1/hi2c2 ),包含 I2C 时钟、模式、引脚等初始化参数,是 HAL 库操作硬件的 “桥梁”。

uint16_t DevAddress
从设备的 I2C 地址,需遵循 I2C 协议规则:
7 位地址:实际地址左移 1 位(腾出最低位作 “读写位” ),读操作时最低位为 1(如设备地址 0x50,读时传入 0xA1 );
10 位地址:直接传入完整地址(如 0x0378 ),需确保 MCU 支持 10 位地址模式。

uint8_t *pData
指向用于 存储接收数据 的缓冲区,函数会将从设备返回的数据按顺序存入该缓冲区(大端模式,先存低索引位置 )。

uint16_t Size
要接收的字节总数,决定函数会从从设备读取多少字节并存入 pData。

uint32_t Timeout
超时阈值(毫秒),若 I2C 总线因从设备无响应、总线冲突等问题,导致接收未完成且超过该时间,函数会返回超时错误(HAL_TIMEOUT )。

6.2.3 执行流程(简化版)

总线忙检测:检查 I2C 总线是否被占用(BUSY 标志位),若忙则等待或返回 HAL_BUSY。
锁定外设:通过 __HAL_LOCK(hi2c) 防止多任务同时操作 I2C,保证传输原子性。
发送起始信号:生成 I2C 起始条件(SCL 高电平时,SDA 从高变低 ),启动通信。
发送从地址(读模式):将 DevAddress(读模式,最低位为 1 )发往从设备,等待从设备应答(ACK);若无应答,返回 HAL_ERROR。
循环接收数据:从设备开始发送数据,主设备逐个字节接收并存入 pData;主设备每收 1 字节,需发送应答(ACK)给从设备,告知继续发送;当接收完 Size 字节后,主设备发送 非应答(NACK),通知从设备停止发送。
发送停止信号:数据接收完毕,生成 I2C 停止条件(SCL 高电平时,SDA 从低变高 ),结束通信。
释放外设:解锁 I2C,更新状态为 HAL_I2C_STATE_READY,返回 HAL_OK(成功)或对应错误码。

6.2.4 典型应用场景

从传感器读数据:如温湿度传感器(SHT30)、加速度计(MPU6050),主设备发送读请求后,用此函数接收测量数据。
从 EEPROM 读内容:先通过 HAL_I2C_Master_Transmit 发送 “读地址命令”,再用此函数读取 EEPROM 对应地址的数据(或直接用 HAL_I2C_Mem_Read 简化流程 )。
读取从设备状态:查询从设备的状态寄存器(如设备 ID、工作模式 ),接收返回的状态字节。

6.2.5 与类似函数的区别

函数名 区别核心点 典型场景
HAL_I2C_Master_Receive 主 ← 从单向读数据,需先确保从设备准备好数据 读传感器、EEPROM 数据
HAL_I2C_Mem_Read 主 ← 从读数据 + 指定从设备内部寄存器地址 带寄存器的从设备(如传感器配置读 )
HAL_I2C_Master_Transmit 主 → 从单向发数据,不涉及从设备内部地址 发指令、无地址从设备通信

    简单说,HAL_I2C_Master_Receive 是 I2C 主设备 “单纯收数据” 的工具,适合从设备已准备好数据、只需读取的场景,是 STM32 I2C 通信中最基础的 主模式读操作 函数 。使用时需注意:若从设备需要先指定 “内部寄存器地址” 才能读数据(如 EEPROM 读操作需先写地址 ),需先用 HAL_I2C_Master_Transmit 发地址,再调用此函数读数据。

七、应用代码实现

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* EEPROM页大小 */
#define EEPROM_PAGE_SIZE        (8)

/* 地址字节数 */
#define EEPROM_ADDR_SIZE        (1)

/* EEPROM写周期ms */
#define EEPROM_WRITE_CYCLE      (10)

/* EEPROM地址定义 */
#define EEPROM_ADDRESS          0xA0    // 7位地址 + R/W位

/* 函数返回值类型 */
typedef enum
{
    EEPROM_OK = 0,
    EEPROM_ERROR = 1,
    EEPROM_TIMEOUT = 2,
    EEPROM_BUSY = 3
} EEPROM_StatusTypeDef;


/* EEPROM任意长度数据读取函数 */
EEPROM_StatusTypeDef EEPROM_Read(uint16_t addr, uint8_t *pData, uint16_t size)
{
    HAL_StatusTypeDef status;
    uint8_t addr_buf[EEPROM_ADDR_SIZE];

    /* 处理8/16位地址 */
    for (uint8_t i = 0; i < EEPROM_ADDR_SIZE; i++)
    {
        addr_buf[i] = (uint8_t)((addr >> (8 * i)) & 0xFF);
    }

    /* 发送EEPROM地址 */
    status = HAL_I2C_Master_Transmit(&hi2c2, EEPROM_ADDRESS, addr_buf, EEPROM_ADDR_SIZE, 100);
    if (status != HAL_OK)
    {
        return EEPROM_ERROR;
    }

    /* 读取数据 */
    status = HAL_I2C_Master_Receive(&hi2c2, EEPROM_ADDRESS | 0x01, pData, size, 100);
    if (status != HAL_OK)
    {
        return EEPROM_ERROR;
    }

    return EEPROM_OK;
}

/* EEPROM任意长度数据写入函数 */
EEPROM_StatusTypeDef EEPROM_Write(uint16_t addr, uint8_t *pData, uint16_t size)
{
    HAL_StatusTypeDef status;
    uint16_t bytes_remaining = size;
    uint16_t current_addr = addr;
    uint8_t *current_data = pData;
    uint16_t bytes_to_write;
    uint8_t tx_buffer[EEPROM_PAGE_SIZE + EEPROM_ADDR_SIZE];  // 最大页大小 + 1字节地址

    /* 按页写入数据(页大小为8字节) */
    while (bytes_remaining > 0)
    {
        /* 计算当前页剩余空间 */
        bytes_to_write = EEPROM_PAGE_SIZE - (current_addr % EEPROM_PAGE_SIZE);
        if (bytes_to_write > bytes_remaining)
        {
            bytes_to_write = bytes_remaining;
        }

        /* 准备发送缓冲区(地址 + 数据) */
        for (uint8_t i = 0; i < EEPROM_ADDR_SIZE; i++)
        {
            tx_buffer[i] = (uint8_t)((current_addr >> (8 * i)) & 0xFF);
        }
        memcpy(&tx_buffer[EEPROM_ADDR_SIZE], current_data, bytes_to_write);

        /* 发送数据 */
        status = HAL_I2C_Master_Transmit(&hi2c2, EEPROM_ADDRESS, tx_buffer, bytes_to_write + EEPROM_ADDR_SIZE, 100);
        if (status != HAL_OK)
        {
            return EEPROM_ERROR;
        }

        /* 等待EEPROM写入完成 */
        HAL_Delay(EEPROM_WRITE_CYCLE);

        /* 更新计数器 */
        bytes_remaining -= bytes_to_write;
        current_addr += bytes_to_write;
        current_data += bytes_to_write;
    }

    return EEPROM_OK;
}

/* 定义测试数据 */
uint8_t test_data_write[16] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
                               0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10};
uint8_t test_data_read[16];

/* USER CODE END 0 */

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_I2C2_Init();
  /* USER CODE BEGIN 2 */
  /* 写入测试数据到地址0x0000 */
  if (EEPROM_Write(0x0005, test_data_write, 16) == EEPROM_OK)
  {
      /* 读取数据验证 */
      if (EEPROM_Read(0x0005, test_data_read, 16) == EEPROM_OK)
      {
          /* 验证读取的数据 */
          for (int i = 0; i < 16; i++)
          {
              if (test_data_read[i] != test_data_write[i])
              {
                  /* 数据不匹配,处理错误 */
              }
          }
      }
  }

  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

八、效果演示

效果演示

九、注意事项

1、每次写入数据后,需要延时5~10ms时间,才可以进行下一次的读写,具体延时时间由EEPROM芯片规格书来定。该时间跟EEPROM供电电压有关系。
2、EEPROM读写操作是可以被中断的,也就是IIC通信进行到一半时,单片机暂停去做其他任务,完成后再回来继续操作,是不会对EEPROM的读写产生影响的,但要注意这过程中不会被重入,或者总线被干扰。
3、每次初始化时,最好是对EEPROM进行一次初始化,将可能处于写入或读取状态的EEPROM释放出来。

十、相关链接

    对于刚入门的小伙伴可以先看下STM32CubeMX的基础使用及Keil的基础使用。
【工具使用】STM32CubeMX-基础使用篇
【工具使用】Keil5软件使用-基础使用篇
【知识分享】IIC协议详解
【知识分享】EEPROM相关知识

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐