STM32 零基础可移植教程 18:I2C 入门,先用扫描器找一找总线上有没有设备

STM32 零基础可移植教程 18:I2C 入门,先用扫描器找一找总线上有没有设备

前面我们已经把串口、定时器、PWM、ADC、DMA 这些基础链路跑了一圈。

从这一篇开始,进入常见总线。

先讲 I2C。

很多新手一上来就想读传感器:

bash 复制代码
读温度
读姿态
读 EEPROM
读 OLED ID

然后程序一跑,什么都没有。

这时候很容易怀疑:

bash 复制代码
是不是寄存器地址错了?
是不是 HAL 函数不会用?
是不是模块坏了?

但在真正读寄存器之前,有一个更基础的问题要先确认:

bash 复制代码
I2C 总线上到底有没有设备回应?

所以这一篇不急着读某一个具体芯片。

我们先做一个通用工具:

bash 复制代码
I2C Scanner

它的作用很简单:

bash 复制代码
从 0x03 扫到 0x77
看看哪些 7 位地址有设备 ACK
然后通过串口打印出来

这篇只解决一个明确目标:

bash 复制代码
用 STM32 扫描 I2C 总线地址,确认设备是否在线

但在动手之前,先把 I2C 是什么、怎么工作、以及为什么选它讲清楚。

道理通了,后面写代码才不懵。

I2C 是什么

I2C(Inter-Integrated Circuit,也常写作 IIC)是一种串行通信总线,由 Philips 在 1980 年代设计。

它最大的特点是:

bash 复制代码
只用两根线,就能挂很多设备

这两根线分别是:

bash 复制代码
SCL(Serial Clock):时钟线,由主机产生
SDA(Serial Data):数据线,双向传输数据

在 STM32 开发中,I2C 最常见的应用场景是:

bash 复制代码
一块 STM32 当主机
总线上挂多个传感器 / EEPROM / OLED
每个设备靠不同的地址区分

I2C 的工作原理

1. 主从结构

I2C 总线上只有两种角色:

bash 复制代码
主机(Master):发起通信的一方,负责产生 SCL 时钟
从机(Slave):响应主机的一方,每个从机有一个唯一地址

一条 I2C 总线上可以有多个主机和多个从机。

但对入门来说,只需要记住最常见的用法:

bash 复制代码
STM32 = 主机
传感器 / EEPROM / OLED = 从机

主机永远主动发起通信,从机只能被动回应。

这一点和 UART 完全不同------UART 两端是平等的,谁都可以随时发数据。

2. 地址怎么区分设备

每一个 I2C 从机都有一个 7 位(或 10 位)的设备地址。

主机在通信开始时先广播一个地址:

bash 复制代码
主机:0x68 在不在?

地址是 0x68 的设备拉低 SDA 回应一个 ACK,表示"我在"。

其他地址的设备不理这件事。

这就是 I2C Scanner 的核心原理:

bash 复制代码
对 0x03 到 0x77 的每个地址问一遍
看谁回 ACK

所以扫到地址,说明总线上确实有这个设备。

3. 开漏输出和上拉电阻------为什么必须有上拉

I2C 的 SCL 和 SDA 引脚都是开漏输出(Open Drain)。

简单理解就是:

bash 复制代码
设备可以把线拉低(接地)
但不会主动把线推高(接电源)

总线要回到高电平,靠的是上拉电阻把线拉到 VCC。

为什么这样设计?

假设有三个设备的 SDA 连在同一条线上:

bash 复制代码
如果设备 A 推高、设备 B 推低
就相当于电源和地直接短路

开漏输出天然避免了这个问题:

bash 复制代码
设备只拉低,不推高
谁都不推高,就不会打架
高电平由上拉电阻统一提供

所以记住一句话:

bash 复制代码
没有上拉电阻,I2C 总线不工作

很多模块(MPU6050、OLED 小板)板子上已经焊了 4.7k 或 10k 上拉电阻,这时就不用再加。

但如果你自制的板子忘了加上拉,I2C 一定扫不到设备。

4. 一次完整的 I2C 通信过程

一次完整的 I2C 通信大致是这样:

Start 和 Stop 这两根线的时序变化,就定义了通信的开始和结束。

中间传输数据时,SCL 的每个脉冲对应一个 bit。

ACK 很关键:

bash 复制代码
ACK = 对方收到了
NACK = 没收到,或没这个地址的设备

初学者不需要手写这些时序,HAL 库已经封装好了。

但知道有这些东西,调试时看波形就不会一脸茫然。

5. 7 位地址和 8 位地址的关系

I2C 入门最容易踩的坑就是地址。

很多资料写:

bash 复制代码
AT24C02 地址:0xA0

也有资料写:

bash 复制代码
AT24C02 地址:0x50

到底哪个对?

都对你。

bash 复制代码
0x50 是 7 位设备地址
0xA0 是左移一位之后、末位填 0 的 8 位写地址

I2C 在线上实际发送 8 位:7 位地址 + 1 位读写位。

所以:

bash 复制代码
写地址 = 0x50 << 1 | 0 = 0xA0
读地址 = 0x50 << 1 | 1 = 0xA1

在代码中使用 STM32 HAL 库时,多数函数要求传入左移后的值。

但我们在应用层和串口打印时,统一使用 7 位地址。

打印的是 0x50,传给 HAL 的才是 (uint16_t)(0x50 << 1)

这样和芯片手册上的 7 位地址对应,移植和查资料都更清楚。

为什么用 I2C 而不是 UART 或 SPI

三种总线各有各的适用场景,不是说谁一定比谁好。

先看对比:

|

特性

|

UART

|

SPI

|

I2C

|

| --- | --- | --- | --- |

|

最少信号线

|

2(TX/RX)

|

4(SCK/MOSI/MISO/CS)

|

2(SCL/SDA)

|

|

多设备

|

不支持

|

支持,但每增加一个设备多加一根 CS

|

支持,靠地址区分,不额外占用引脚

|

|

速度

|

通常 ≤ 1 Mbps

|

几 MHz 到几十 MHz

|

标准 100 kHz,快速 400 kHz,高速 3.4 MHz

|

|

全双工

|

|

|

否(半双工)

|

|

硬件复杂度

|

|

|

|

|

典型用途

|

调试、GPS/蓝牙模块

|

显示屏、高速 ADC、Flash

|

传感器、EEPROM、RTC、小 OLED

|

场景 1:只接一个模块,三种都行

比如你只想用蓝牙透传,UART 最简单。

一块 STM32 的 TX 接模块的 RX,RX 接模块的 TX,串口助手就能看到数据。

场景 2:接多个传感器,I2C 优势明显

假设你要接 3 个传感器:温度、湿度、气压。

用 UART:

bash 复制代码
需要 3 个 USART,6 根数据线
而且 STM32 上 USART 数量有限

用 SPI:

bash 复制代码
SCK + MOSI + MISO = 3 根公用
但每个传感器还要一根独立的 CS = 再加 3 根
总共 6 根线

用 I2C:

bash 复制代码
SCL + SDA = 2 根线
3 个传感器全部并在这两根线上
每个传感器有不同地址,靠地址区分

这就是 I2C 最大的优势:

bash 复制代码
设备越多,省线越明显

场景 3:高速传输,SPI 更适合

I2C 跑 100 kHz 或 400 kHz,SPI 可以跑几 MHz 甚至更高。

如果你要驱动一块彩色 TFT 屏幕,每秒钟要刷大量像素数据,SPI 更合适。

I2C 的 OLED 只是 128 × 64 像素的小屏,数据量小很多。

场景 4:距离远,UART 更可靠

I2C 和 SPI 都是板内总线,设计上就没考虑远距离。

UART 可以加 RS-232、RS-485 收发器,跑到几米甚至上千米。

一句话总结

bash 复制代码
接传感器 / EEPROM / RTC / 小 OLED → 首选 I2C,线最少
接 TFT 彩屏 / 高速 ADC → 首选 SPI,速度快
调试口 / GPS / 蓝牙透传 → 首选 UART,简单直接

对于这篇的 Scanner 来说,目标是找到 I2C 总线上的设备,和别的总线没有关系。

但你可能在同一个工程里同时用到 UART(串口打印)+ I2C(传感器)------这非常正常。

本篇目标

最终现象:

如果没有接任何 I2C 设备,串口打印:

bash 复制代码
Scanning I2C bus...
No I2C device found.

如果接了一个常见 I2C 设备,串口可能打印:

bash 复制代码
Scanning I2C bus...
Found 1 device(s): 0x68

或者:

bash 复制代码
Found 1 device(s): 0x3C

或者:

bash 复制代码
Found 1 device(s): 0x50

本篇用到的外设:

bash 复制代码
I2C
USART printf

本篇跑通标准:

  • CubeMX 能正确配置 I2C;

  • Keil 编译通过;

  • 串口能打印扫描结果;

  • 能解释 7 位地址和 8 位地址的区别;

  • 知道 I2C 为什么需要上拉电阻;

  • 总线扫不到设备时,知道先查哪些地方。

准备工作

你需要准备:

|

项目

|

说明

|

| --- | --- |

|

STM32 开发板

|

任意带 I2C 的 STM32 都可以

|

|

下载器

|

ST-LINK/V2 或板载 ST-LINK

|

|

串口工具

|

用来看扫描结果

|

|

一个 I2C 设备

|

EEPROM、MPU6050、OLED、I2C LCD 小板都可以

|

|

杜邦线

|

外接模块时使用

|

|

原理图

|

确认 I2C 的 SCL/SDA 引脚

|

如果你暂时没有 I2C 模块,也可以先把工程跑起来。

没有设备时,扫描结果应该是:

bash 复制代码
No I2C device found.

这说明程序至少跑通了。

但要验证 I2C 通信,最终还是需要接一个 I2C 设备。

常见设备地址举例:

|

设备

|

常见 7 位地址

|

说明

|

| --- | --- | --- |

|

AT24C02 EEPROM

| 0x50 |

有些资料会写 8 位写地址 0xA0

|

|

MPU6050

| 0x68

0x69

|

取决于 AD0 引脚电平

|

|

SSD1306 OLED

| 0x3C

0x3D

|

常见 0.96 寸 OLED

|

|

PCF8574 I2C LCD 小板

| 0x27

0x3F

|

不同小板地址不同

|

硬件连接

I2C 最少需要两根信号线:

bash 复制代码
SCL:时钟线
SDA:数据线

再加上供电和地:

|

I2C 模块

|

STM32 开发板

|

| --- | --- |

|

VCC

|

3.3V 或模块要求的电源

|

|

GND

|

GND

|

|

SCL

|

STM32 I2C SCL

|

|

SDA

|

STM32 I2C SDA

|

注意三件事。

第一,GND 必须共地。

这个和串口、PWM 接回 ADC 一样。没有共地,电平就没有共同参考。

第二,确认模块电压。

很多 I2C 模块可以 3.3V 供电,也有些模块默认接 5V。STM32 的 I2C 引脚不一定都能承受 5V,上拉到 5V 时要特别小心。

入门阶段建议:

bash 复制代码
模块 VCC 接 3.3V
SCL/SDA 上拉也到 3.3V

第三,确认上拉电阻。

前面原理部分讲过,I2C 必须靠上拉电阻才能工作。

很多模块板子上已经焊了 4.7k 或 10k 上拉电阻。

如果不确定,查一下模块原理图,或者用万用表量一下 SCL/SDA 到 VCC 之间的电阻。

裸芯片或自制板子尤其要注意这一点------忘了加上拉,I2C 一定扫不到设备。

CubeMX 配置步骤

1. 复制前面的串口工程

建议从第 07 篇 USART printf 工程复制一份,改名为:

bash 复制代码
18_i2c_scanner

因为这篇需要串口打印扫描结果。

如果你重新建工程,也可以按第一篇流程:

  1. 选择芯片型号;

  2. SYS -> Debug 设置为 Serial Wire

  3. 配置 USART,用于 printf()

  4. 配置 I2C;

  5. 生成 Keil 工程。

2. 配置 I2C

选择你要用的 I2C,比如:

bash 复制代码
I2C1

模式选择:

bash 复制代码
I2C

常见引脚示例:

|

I2C

|

SCL

|

SDA

|

| --- | --- | --- |

|

I2C1

|

PB6

|

PB7

|

|

I2C2

|

PB10

|

PB11

|

具体以你的芯片和开发板原理图为准。

注意:

bash 复制代码
不是任意 GPIO 都能直接当硬件 I2C 的 SCL/SDA

要选芯片复用功能里支持 I2C_SCLI2C_SDA 的引脚。

3. 设置 I2C 参数

入门阶段先用标准模式:

|

配置项

|

推荐值

|

说明

|

| --- | --- | --- |

|

I2C Speed Mode

|

Standard Mode

|

先用 100 kHz,稳定优先

|

|

I2C Clock Speed

|

100000 Hz

|

常见标准速度

|

|

Duty Cycle

|

2

|

标准设置

|

|

Own Address

|

0

|

主机模式下先不用

|

|

Addressing Mode

|

7-bit

|

常见 I2C 设备都是 7 位地址

|

|

General Call

|

Disable

|

先不用

|

|

No Stretch Mode

|

Disable

|

允许从机拉伸时钟

|

这一篇 STM32 当主机。

模块、传感器、EEPROM 当从机。

所以我们不会用 Own Address 做通信,只保持默认即可。

4. GPIO 上拉怎么选

如果 CubeMX 的 GPIO 页面里能看到 I2C 引脚,通常模式会是:

bash 复制代码
Alternate Function Open Drain

这是 I2C 的典型配置。

Pull-up/Pull-down 怎么选?

入门建议:

bash 复制代码
如果外部模块已经有上拉电阻,内部 Pull-up 可以不依赖它
如果不确定,可以先看模块原理图

有些工程会在 CubeMX 里把 I2C 引脚设成 Pull-up。

但 STM32 内部上拉通常比较弱,不建议把它当成唯一可靠上拉。

更推荐硬件上有 4.7k 或 10k 上拉电阻。

5. 配置 USART printf

这篇仍然需要串口输出扫描结果。

如果你已经完成第 07 篇,就直接沿用。

常见参数:

bash 复制代码
115200
8 数据位
无校验
1 停止位

6. 生成 Keil 工程

点击:

bash 复制代码
GENERATE CODE

打开 Keil 后,先编译一次。

如果 CubeMX 原始工程都编译不过,先处理基础工程,不要急着加扫描代码。

Keil 工程生成和编译

本篇新增两个文件:

bash 复制代码
Core/Inc/app_i2c_scanner.h
Core/Src/app_i2c_scanner.c

如果你手动新建 .c 文件,记得在 Keil 工程树里添加:

bash 复制代码
Core/Src/app_i2c_scanner.c

否则会出现:

bash 复制代码
undefined symbol App_I2CScanner_Scan

这不是函数写错了,而是 .c 文件没有参与编译。

完整代码

1. 新建 Core/Inc/app_i2c_scanner.h

bash 复制代码
#ifndef APP_I2C_SCANNER_H
#define APP_I2C_SCANNER_H

#include "main.h"
#include <stdint.h>

#ifndef APP_I2C_SCANNER_MAX_FOUND
#define APP_I2C_SCANNER_MAX_FOUND 16u
#endif

typedef struct
{
    uint8_t address_7bit[APP_I2C_SCANNER_MAX_FOUND];
    uint8_t count;
    uint8_t overflow;
} App_I2CScanner_Result;

void App_I2CScanner_Init(void);
HAL_StatusTypeDef App_I2CScanner_IsReady7Bit(uint8_t address_7bit);
HAL_StatusTypeDef App_I2CScanner_Scan(App_I2CScanner_Result *result);

#endif

2. 新建 Core/Src/app_i2c_scanner.c

bash 复制代码
#include "app_i2c_scanner.h"

/*
 * Default I2C is I2C1.
 * If your project uses I2C2 or another I2C instance, change this macro.
 */
#ifndef APP_I2C_SCANNER_HANDLE
#define APP_I2C_SCANNER_HANDLE hi2c1
#endif

/*
 * STM32 HAL I2C APIs expect the 7-bit device address shifted left by 1.
 * This module exposes 7-bit addresses to the application layer.
 */
#ifndef APP_I2C_SCANNER_TRIALS
#define APP_I2C_SCANNER_TRIALS 2u
#endif

#ifndef APP_I2C_SCANNER_TIMEOUT_MS
#define APP_I2C_SCANNER_TIMEOUT_MS 10u
#endif

#ifndef APP_I2C_SCANNER_ADDR_MIN
#define APP_I2C_SCANNER_ADDR_MIN 0x03u
#endif

#ifndef APP_I2C_SCANNER_ADDR_MAX
#define APP_I2C_SCANNER_ADDR_MAX 0x77u
#endif

extern I2C_HandleTypeDef APP_I2C_SCANNER_HANDLE;

void App_I2CScanner_Init(void)
{
}

HAL_StatusTypeDef App_I2CScanner_IsReady7Bit(uint8_t address_7bit)
{
    return HAL_I2C_IsDeviceReady(&APP_I2C_SCANNER_HANDLE,
                                 (uint16_t)(address_7bit << 1),
                                 APP_I2C_SCANNER_TRIALS,
                                 APP_I2C_SCANNER_TIMEOUT_MS);
}

HAL_StatusTypeDef App_I2CScanner_Scan(App_I2CScanner_Result *result)
{
    uint8_t addr;
    HAL_StatusTypeDef status;

    if (result == 0)
    {
        return HAL_ERROR;
    }

    result->count = 0u;
    result->overflow = 0u;

    for (addr = APP_I2C_SCANNER_ADDR_MIN; addr <= APP_I2C_SCANNER_ADDR_MAX; addr++)
    {
        status = App_I2CScanner_IsReady7Bit(addr);
        if (status == HAL_OK)
        {
            if (result->count < APP_I2C_SCANNER_MAX_FOUND)
            {
                result->address_7bit[result->count] = addr;
                result->count++;
            }
            else
            {
                result->overflow = 1u;
            }
        }
    }

    return HAL_OK;
}

核心函数是:

bash 复制代码
HAL_I2C_IsDeviceReady()

它会向指定地址发起一次测试。

如果设备回应 ACK,返回:

bash 复制代码
HAL_OK

如果没有设备回应,或者总线异常,就不会返回 HAL_OK

注意这一行:

bash 复制代码
(uint16_t)(address_7bit << 1)

我们对外使用 7 位地址。

传给 HAL 时,左移 1 位。

这样串口打印出来的地址和大多数芯片手册里的 7 位地址保持一致。

main.c 调用方式

1. 添加头文件

main.c 顶部添加:

bash 复制代码
/* USER CODE BEGIN Includes */
#include "app_i2c_scanner.h"
#include <stdio.h>
/* USER CODE END Includes */

2. 初始化后打印提示

确认 CubeMX 已生成:

bash 复制代码
MX_GPIO_Init();
MX_I2C1_Init();
MX_USART1_UART_Init();

然后在 USER CODE BEGIN 2 中添加:

bash 复制代码
/* USER CODE BEGIN 2 */
App_I2CScanner_Init();

printf("\r\nI2C scanner test\r\n");
printf("Use 7-bit address format.\r\n");
/* USER CODE END 2 */

3. while 循环里扫描并打印

bash 复制代码
/* USER CODE BEGIN WHILE */
while (1)
{
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */
  App_I2CScanner_Result result;
  uint8_t i;

  printf("\r\nScanning I2C bus...\r\n");

  if (App_I2CScanner_Scan(&result) == HAL_OK)
  {
      if (result.count == 0u)
      {
          printf("No I2C device found.\r\n");
      }
      else
      {
          printf("Found %u device(s): ", result.count);
          for (i = 0u; i < result.count; i++)
          {
              printf("0x%02X ", result.address_7bit[i]);
          }
          printf("\r\n");

          if (result.overflow != 0u)
          {
              printf("Warning: too many devices, result list overflow.\r\n");
          }
      }
  }
  else
  {
      printf("I2C scan failed.\r\n");
  }

  HAL_Delay(3000);
  /* USER CODE END 3 */
}

这里每 3 秒扫描一次。

你可以先不接设备,看它打印:

bash 复制代码
No I2C device found.

再接上设备,看地址是否出现。

这样排查会很清楚。

编译、下载和验证

1. 不接 I2C 设备先验证程序

先只下载程序,不接任何 I2C 模块。

串口应该打印:

bash 复制代码
I2C scanner test
Use 7-bit address format.

Scanning I2C bus...
No I2C device found.

如果这一步都没有输出,先回去查 USART,不要急着查 I2C。

2. 接上一个 I2C 模块

接线示例:

bash 复制代码
模块 VCC -> 3.3V
模块 GND -> GND
模块 SCL -> STM32 I2C SCL
模块 SDA -> STM32 I2C SDA

然后复位开发板。

如果接的是 MPU6050,可能看到:

bash 复制代码
Found 1 device(s): 0x68

如果接的是 EEPROM,可能看到:

bash 复制代码
Found 1 device(s): 0x50

如果接的是 OLED,可能看到:

bash 复制代码
Found 1 device(s): 0x3C

3. 拔掉 SDA 或 SCL 做反向验证

如果你想确认扫描结果真的来自 I2C 模块,可以做一个小测试:

  1. 正常接线,扫描到地址;

  2. 拔掉 SDA;

  3. 复位或等下一次扫描;

  4. 地址应该消失;

  5. 再接回 SDA,地址应该恢复。

这个测试很适合新手建立感觉:

bash 复制代码
不是程序随便打印了一个地址
而是真的总线上有设备回应

移植到其他板子的修改点

|

要改的地方

|

为什么要改

|

在哪里改

|

| --- | --- | --- |

|

I2C 实例

|

可能用 I2C1、I2C2 或 I2C3

|

CubeMX,APP_I2C_SCANNER_HANDLE

|

|

SCL/SDA 引脚

|

不同板子引脚不同

|

CubeMX Pinout 和原理图

|

|

I2C 速度

|

有些设备只适合 100 kHz,有些支持 400 kHz

|

CubeMX I2C 参数

|

|

上拉电阻

|

I2C 必须有上拉

|

硬件原理图或外接电阻

|

|

设备供电电压

|

影响 SCL/SDA 上拉电平

|

模块说明和开发板电平

|

|

地址范围

|

有些特殊设备地址可能不在常用范围

| APP_I2C_SCANNER_ADDR_MIN/MAX |

|

扫描结果容量

|

总线上设备很多时可能超过 16 个

| APP_I2C_SCANNER_MAX_FOUND |

|

HAL 地址格式

|

HAL 通常要 7 位地址左移 1 位

| App_I2CScanner_IsReady7Bit() |

如果你用 I2C2,把代码里的默认句柄改成:

bash 复制代码
#define APP_I2C_SCANNER_HANDLE hi2c2

如果你用 I2C3,就改成:

bash 复制代码
#define APP_I2C_SCANNER_HANDLE hi2c3

常见问题排查

1. 串口没有任何输出

这不是 I2C 问题。

先按第 07 篇排查 USART:

  • TX/RX/GND 是否接对;

  • 波特率是否一致;

  • printf() 是否已经重定向;

  • Keil 是否勾选 MicroLIB;

  • MX_USARTx_UART_Init() 是否执行;

  • 串口助手是否选对端口。

2. 一直显示 No I2C device found

优先按这个顺序查:

|

检查项

|

说明

|

| --- | --- |

|

VCC/GND

|

模块是否供电,GND 是否共地

|

|

SCL/SDA

|

是否接反,是否接到 CubeMX 配置的那两个引脚

|

|

上拉电阻

|

模块或板子上是否有上拉

|

|

模块电压

|

3.3V 模块不要接错,5V 模块注意电平

|

|

I2C 实例

|

代码默认 hi2c1,实际是否用 I2C2

|

|

地址左移

|

代码内部已经左移,不要传 8 位地址再左移

|

I2C 扫不到设备时,硬件问题比代码问题更常见。

3. SCL/SDA 接反了会怎样

通常扫不到设备。

如果你不确定模块引脚顺序,先不要凭感觉接。

回去看模块丝印或原理图。

有些模块的引脚顺序是:

bash 复制代码
VCC GND SCL SDA

有些是:

bash 复制代码
GND VCC SCL SDA

看错一排针脚,很正常,也很耽误时间。

4. 扫出来的地址和资料不一样

先判断资料写的是 7 位地址还是 8 位地址。

比如资料写:

bash 复制代码
0xA0

扫描器打印:

bash 复制代码
0x50

这是正常的。

因为:

bash 复制代码
0xA0 >> 1 = 0x50

再比如资料写:

bash 复制代码
0xD0

扫描器打印:

bash 复制代码
0x68

也是正常的:

bash 复制代码
0xD0 >> 1 = 0x68

5. 程序卡住或者扫描很慢

本篇每个地址超时默认是:

bash 复制代码
#define APP_I2C_SCANNER_TIMEOUT_MS 10u

扫描 0x03 到 0x77,如果很多地址都没有回应,整个扫描会花一点时间。

如果你觉得太慢,可以适当减小超时,比如:

bash 复制代码
#define APP_I2C_SCANNER_TIMEOUT_MS 3u

但太短也可能导致慢设备来不及回应。

入门阶段先用 10 ms,稳定优先。

6. I2C 总线卡死

有时从机异常、程序下载中断、SCL/SDA 被拉低,可能导致总线卡死。

常见现象:

bash 复制代码
SDA 一直低
SCL 一直低
扫描不到任何设备
复位 STM32 也没用

先做简单处理:

  1. 给 I2C 模块断电再上电;

  2. 复位 STM32;

  3. 检查是否有短路;

  4. 降低 I2C 速度到 100 kHz;

  5. 确认上拉电阻。

更完整的"总线恢复"后面 I2C 排坑篇再讲。

7. 编译报 hi2c1 未定义

说明你的工程里 I2C 句柄不叫 hi2c1,或者没有开启 I2C1。

解决方法:

  1. 看 CubeMX 里开启的是 I2C1 还是 I2C2;

  2. 打开 i2c.c,确认句柄名;

  3. 修改:

bash 复制代码
#define APP_I2C_SCANNER_HANDLE hi2c1

比如实际是 I2C2,就改成:

bash 复制代码
#define APP_I2C_SCANNER_HANDLE hi2c2

8. 编译报 undefined symbol App_I2CScanner_Scan

通常是:

bash 复制代码
app_i2c_scanner.c 没有加入 Keil 工程

解决方法:

  1. 右键 Application/User/Core

  2. 选择 Add Existing Files to Group

  3. 添加 Core/Src/app_i2c_scanner.c

  4. 重新编译。

本篇小结

这一篇我们没有急着读某个具体传感器,而是先做了一个通用 I2C Scanner。

你现在应该知道:

  • I2C 至少有 SCL 和 SDA 两根信号线;

  • I2C 总线需要上拉电阻;

  • STM32 当主机,模块通常当从机;

  • 扫描器的本质是看某个地址有没有设备 ACK;

  • 应用层建议统一显示 7 位地址;

  • 传给 STM32 HAL 时,通常要把 7 位地址左移 1 位;

  • 扫不到设备时,优先查供电、共地、SCL/SDA、上拉、电平和 I2C 实例。

下一篇继续 I2C:

STM32 I2C 读写寄存器:先读一个设备 ID 或配置寄存器。

到时候我们会在扫描到地址的基础上,再讲 HAL_I2C_Mem_Read()HAL_I2C_Mem_Write() 怎么用。

相关推荐
天涯铭5 小时前
深入浅出:单片机I/O口串联电阻选型
单片机·嵌入式硬件·io口串联电阻
国科安芯5 小时前
ASP7A84AS——航天级低噪声高PSRR线性稳压器
网络·单片机·嵌入式硬件·架构·安全性测试
普中科技6 小时前
【普中STM32F1xx开发攻略--标准库版】-- 第 42 章 STM32 内部 FLASH 实验
stm32·单片机·嵌入式硬件·开发板·普中科技·内部flash
John_ToDebug7 小时前
WeakPtr 与 Raw 指针:UAF 如何识别、如何处理、以及 Chromium 的设计哲学
c++·chrome·ai
不做无法实现的梦~7 小时前
CLion+pyocd配置教程
嵌入式硬件
破晓单片机8 小时前
012、STM32项目分享:智能台灯系统
stm32·单片机·嵌入式硬件
悠哉悠哉愿意8 小时前
【单片机复习笔记】十五届国赛复盘
笔记·单片机·嵌入式硬件·学习
是温不嗜温8 小时前
芯茂微 LP7012 双重过流保护机制拆解:DESAT 单次锁存 vs OCP 连续 5 次锁存有何区别?
嵌入式硬件·开闭原则·电源管理·电源芯片·ac-dc
HPT_Lt8 小时前
ZCC5146 支持100V宽压多功能同步降压控制器,兼容LM5146
嵌入式硬件