STM32 零基础可移植教程 19:I2C 读写寄存器,先读一个设备 ID

STM32 零基础可移植教程 19:I2C 读写寄存器,先读一个地址

上一篇我们写了 I2C Scanner。

它只做一件事:

bash 复制代码
扫描 I2C 总线

看看哪些地址有设备回应 ACK

如果串口能打印:

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

说明至少三件事是通的:

bash 复制代码
I2C 引脚基本没错

SCL/SDA 总线上有回应

设备地址大概率找到了

但扫描到地址,只能说明"设备在总线上"。

下一步才是真正和设备说话:

bash 复制代码
读寄存器

写寄存器

这一篇的目标非常明确:

bash 复制代码
用 I2C 读一个设备 ID 寄存器,再写一个简单的配置寄存器

正文用 MPU6050 举例。

常见情况是:

bash 复制代码
MPU6050 7 位地址:0x68

WHO_AM_I 寄存器:0x75

读出来数值:0x68

没有 MPU6050 也不影响。

这一篇真正在教的是:

bash 复制代码
设备地址、寄存器地址、寄存器值三者怎么对应

HAL_I2C_Mem_Read() 和 HAL_I2C_Mem_Write() 怎么用

换设备时要改什么

本篇目标

最终行为:

串口打印类似:

bash 复制代码
I2C register read/write test

Device 0x68 is ready.

WHO_AM_I = 0x68

Write PWR_MGMT_1 = 0x00 OK.

用到的外设:

bash 复制代码
I2C

USART printf

验收标准:

  • 能用第 18 篇的 Scanner 扫到设备地址;

  • 能读取一个寄存器的值;

  • 能说出 7 位设备地址和寄存器地址不是一回事;

  • 能说出 HAL_I2C_Mem_Read()DevAddressMemAddresspData 分别对应什么;

  • 换个 I2C 设备时,知道改设备地址和寄存器地址。

准备工作

你需要:

|

物品

|

说明

|

| --- | --- |

|

STM32 开发板

|

任意带 I2C 的 STM32

|

|

I2C 设备模块

|

本篇以 MPU6050 为例

|

|

调试器

|

ST-LINK/V2 或板载 ST-LINK

|

|

串口工具

|

查看寄存器读取结果

|

|

杜邦线

|

连接外接模块

|

|

设备数据手册

|

查设备地址和寄存器地址

|

如果没有 MPU6050,可以用其他 I2C 设备。

但要注意:有些 I2C 设备可能没有"设备 ID 寄存器"。

比如:

|

设备

|

适合本篇吗

|

| --- | --- |

|

MPU6050

|

非常适合,有 WHO_AM_I

|

|

QMC5883 / HMC5883

|

可以用,但要查手册确认寄存器地址和值

|

|

部分温湿度传感器

|

可以用,但读取方式可能和标准寄存器不一样

|

|

AT24C02 EEPROM

|

适合读写实践,只是读的是存储地址,不是 ID

|

|

PCF8574 I2C LCD 背板

|

不太合适------它更像是 GPIO 扩展器,没有标准 ID 寄存器

|

为了让新手最容易理解,本篇用 MPU6050 举例。

硬件连接

接线和第 18 篇一样:

|

MPU6050 模块

|

STM32 开发板

|

| --- | --- |

|

VCC

|

3.3V

|

|

GND

|

GND

|

|

SCL

|

STM32 I2C SCL

|

|

SDA

|

STM32 I2C SDA

|

如果你的 MPU6050 模块有 AD0 引脚:

|

AD0 电平

|

7 位地址

|

| --- | --- |

|

接 GND

| 0x68 |

|

接 VCC

| 0x69 |

所以如果你扫出来:

bash 复制代码
0x69

不一定是错的------可能只是 AD0 接了高电平。

先用第 18 篇的 Scanner 确认地址。

本篇示例默认:

bash 复制代码
设备 7 位地址 = 0x68

先把三个地址概念分清楚

读 I2C 寄存器时,最容易混淆三样东西。

1. 设备地址

设备地址表示:

bash 复制代码
我要和总线上的哪一个设备说话

比如 MPU6050 常见:

bash 复制代码
0x68

这就是第 18 篇 Scanner 扫到的地址。

2. 寄存器地址

寄存器地址表示:

bash 复制代码
我要访问这个设备内部的哪一个位置

比如 MPU6050 的 WHO_AM_I 寄存器地址是:

bash 复制代码
0x75

PWR_MGMT_1 寄存器地址是:

bash 复制代码
0x6B

3. 寄存器值

寄存器值就是:

bash 复制代码
这个寄存器里存的具体数据

比如读 WHO_AM_I 时:

bash 复制代码
设备地址:0x68

寄存器地址:0x75

读到的值:0x68

这三个 0x68 / 0x75 / 0x68 看起来很相似,但含义完全不同。

用一句话记住:

bash 复制代码
设备地址找人

寄存器地址找抽屉

寄存器值是抽屉里的东西

HAL_I2C_Mem_Read() 参数怎么看

STM32 HAL 里读寄存器,最常用的函数就是:

bash 复制代码
HAL_I2C_Mem_Read()

它的函数签名大概是:

bash 复制代码
HAL_I2C_Mem_Read(&hi2c1,

                 DevAddress,

                 MemAddress,

                 MemAddSize,

                 pData,

                 Size,

                 Timeout);

对应到 MPU6050 读 WHO_AM_I

|

参数

|

示例值

|

含义

|

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

| &hi2c1 | &hi2c1 |

用哪个 I2C 外设

|

| DevAddress | 0x68 << 1 |

设备 7 位地址左移 1 位

|

| MemAddress | 0x75 |

要读的寄存器地址

|

| MemAddSize | I2C_MEMADD_SIZE_8BIT |

寄存器地址宽度是 8 位

|

| pData | &value |

读出来的数据放哪里

|

| Size | 1 |

读几个字节

|

| Timeout | 100 |

超时时间

|

为什么要将 DevAddress 左移?

这在第 18 篇已经讲过。

应用层使用 7 位地址:

bash 复制代码
0x68

传给 HAL 时,通常要变成:

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

所以本篇代码坚持这个风格:

bash 复制代码
函数对外接收 7 位地址

函数内部左移 1 位再传给 HAL

这样文章和代码不会混淆。

CubeMX 配置步骤

1. 复制第 18 篇工程

建议直接复制第 18 篇 I2C Scanner 工程,重命名为:

bash 复制代码
19_i2c_register

因为第 18 篇已经配置好了:

bash 复制代码
I2C

USART printf

SCL/SDA 接线

7 位地址扫描

本篇只是在扫描成功的基础上,增加寄存器读写代码。

2. I2C 配置不变

I2C 参数可以继续用:

|

配置项

|

建议值

|

| --- | --- |

|

Speed Mode

|

Standard Mode

|

|

Clock Speed

|

100000 Hz

|

|

Addressing Mode

|

7-bit

|

|

SCL/SDA GPIO

|

Alternate Function Open Drain

|

先用 100 kHz。

跑稳之后,后面再考虑 400 kHz。

3. USART printf 不变

本篇仍然用串口输出结果。

完成第 07 篇的同学继续用:

bash 复制代码
115200

8 数据位

无校验

1 停止位

4. 先用 Scanner 确认地址

在写寄存器代码之前,建议先跑一遍第 18 篇的 Scanner。

确认串口能打印:

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

或:

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

如果 Scanner 都扫不到东西,先别急着写 HAL_I2C_Mem_Read()

回头检查:

bash 复制代码
电源

GND

SCL/SDA

上拉电阻

I2C 实例

设备地址

Keil 工程生成与编译

本篇会增加两个新文件:

bash 复制代码
Core/Inc/app_i2c_reg.h

Core/Src/app_i2c_reg.c

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

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

否则会出现:

bash 复制代码
undefined symbol App_I2CReg_ReadReg8

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

完整代码

1. 新建 Core/Inc/app_i2c_reg.h

bash 复制代码
#ifndef APP_I2C_REG_H

#define APP_I2C_REG_H


#include "main.h"

#include <stdint.h>


void App_I2CReg_Init(void)
;

HAL_StatusTypeDef App_I2CReg_IsReady7Bit(uint8_t device_addr_7bit)
;

HAL_StatusTypeDef App_I2CReg_ReadReg8(uint8_t device_addr_7bit,                                      uint8_t reg_addr,                                      uint8_t *value)
;

HAL_StatusTypeDef App_I2CReg_WriteReg8(uint8_t device_addr_7bit,                                       uint8_t reg_addr,                                       uint8_t value)
;

HAL_StatusTypeDef App_I2CReg_ReadBytes(uint8_t device_addr_7bit,                                       uint8_t reg_addr,                                       uint8_t *buffer,                                       uint16_t length)
;

HAL_StatusTypeDef App_I2CReg_WriteBytes(uint8_t device_addr_7bit,                                        uint8_t reg_addr,                                        const uint8_t *buffer,                                        uint16_t length)
;


#endif

2. 新建 Core/Src/app_i2c_reg.c

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


/* * Default I2C is I2C1. * If your project uses I2C2 or another I2C instance, change this macro. */

#ifndef APP_I2C_REG_HANDLE

#define APP_I2C_REG_HANDLE hi2c1

#endif


#ifndef APP_I2C_REG_TRIALS

#define APP_I2C_REG_TRIALS 2u

#endif


#ifndef APP_I2C_REG_TIMEOUT_MS

#define APP_I2C_REG_TIMEOUT_MS 100u

#endif


extern
 I2C_HandleTypeDef APP_I2C_REG_HANDLE;


static uint16_t App_I2CReg_DevAddr(uint8_t device_addr_7bit)
{

    
return
 (
uint16_t
)(device_addr_7bit << 
1
);

}


void App_I2CReg_Init(void)
{

}


HAL_StatusTypeDef App_I2CReg_IsReady7Bit(uint8_t device_addr_7bit)
{

    
return
 HAL_I2C_IsDeviceReady(&APP_I2C_REG_HANDLE,

                                 App_I2CReg_DevAddr(device_addr_7bit),

                                 APP_I2C_REG_TRIALS,

                                 APP_I2C_REG_TIMEOUT_MS);

}


HAL_StatusTypeDef App_I2CReg_ReadReg8(uint8_t device_addr_7bit,                                      uint8_t reg_addr,                                      uint8_t *value)
{

    
if
 (value == 
0
)

    {

        
return
 HAL_ERROR;

    }


    
return
 HAL_I2C_Mem_Read(&APP_I2C_REG_HANDLE,

                            App_I2CReg_DevAddr(device_addr_7bit),

                            reg_addr,

                            I2C_MEMADD_SIZE_8BIT,

                            value,

                            
1u
,

                            APP_I2C_REG_TIMEOUT_MS);

}


HAL_StatusTypeDef App_I2CReg_WriteReg8(uint8_t device_addr_7bit,                                       uint8_t reg_addr,                                       uint8_t value)
{

    
return
 HAL_I2C_Mem_Write(&APP_I2C_REG_HANDLE,

                             App_I2CReg_DevAddr(device_addr_7bit),

                             reg_addr,

                             I2C_MEMADD_SIZE_8BIT,

                             &value,

                             
1u
,

                             APP_I2C_REG_TIMEOUT_MS);

}


HAL_StatusTypeDef App_I2CReg_ReadBytes(uint8_t device_addr_7bit,                                       uint8_t reg_addr,                                       uint8_t *buffer,                                       uint16_t length)
{

    
if
 ((buffer == 
0
) || (length == 
0u
))

    {

        
return
 HAL_ERROR;

    }


    
return
 HAL_I2C_Mem_Read(&APP_I2C_REG_HANDLE,

                            App_I2CReg_DevAddr(device_addr_7bit),

                            reg_addr,

                            I2C_MEMADD_SIZE_8BIT,

                            buffer,

                            length,

                            APP_I2C_REG_TIMEOUT_MS);

}


HAL_StatusTypeDef App_I2CReg_WriteBytes(uint8_t device_addr_7bit,                                        uint8_t reg_addr,                                        const uint8_t *buffer,                                        uint16_t length)
{

    
if
 ((buffer == 
0
) || (length == 
0u
))

    {

        
return
 HAL_ERROR;

    }


    
return
 HAL_I2C_Mem_Write(&APP_I2C_REG_HANDLE,

                             App_I2CReg_DevAddr(device_addr_7bit),

                             reg_addr,

                             I2C_MEMADD_SIZE_8BIT,

                             (
uint8_t
 *)buffer,

                             length,

                             APP_I2C_REG_TIMEOUT_MS);

}

本篇只处理:

bash 复制代码
8 位寄存器地址

也就是 I2C_MEMADD_SIZE_8BIT

很多传感器都用这个。

但 EEPROM 或者某些芯片内部地址可能是 16 位------这个可以后面单独讲。

main.c 调用方法

1. 添加头文件

main.c 顶部添加:

bash 复制代码
/* USER CODE BEGIN Includes */

#include "app_i2c_reg.h"

#include <stdio.h>

/* USER CODE END Includes */

2. 添加演示宏

USER CODE BEGIN PD 中添加:

bash 复制代码
/* USER CODE BEGIN PD */

#define DEMO_I2C_ADDR_7BIT       0x68u

#define DEMO_MPU6050_WHO_AM_I    0x75u

#define DEMO_MPU6050_PWR_MGMT_1  0x6Bu

/* USER CODE END PD */

如果你的扫描结果是 0x69,把:

bash 复制代码
#define DEMO_I2C_ADDR_7BIT 0x68u

改成:

bash 复制代码
#define DEMO_I2C_ADDR_7BIT 0x69u

3. 在初始化后读寄存器

确认 CubeMX 已经生成:

bash 复制代码
MX_GPIO_Init();

MX_I2C1_Init();

MX_USART1_UART_Init();

然后在 USER CODE BEGIN 2 中添加:

bash 复制代码
/* USER CODE BEGIN 2 */

uint8_t
 who_am_i = 
0u
;


App_I2CReg_Init();


printf
(
"\r\nI2C register read/write test\r\n"
);


if
 (App_I2CReg_IsReady7Bit(DEMO_I2C_ADDR_7BIT) == HAL_OK)

{

    
printf
(
"Device 0x%02X is ready.\r\n"
, DEMO_I2C_ADDR_7BIT);


    
if
 (App_I2CReg_ReadReg8(DEMO_I2C_ADDR_7BIT, DEMO_MPU6050_WHO_AM_I, &who_am_i) == HAL_OK)

    {

        
printf
(
"WHO_AM_I = 0x%02X\r\n"
, who_am_i);

    }

    
else

    {

        
printf
(
"Read WHO_AM_I failed.\r\n"
);

    }


    
if
 (App_I2CReg_WriteReg8(DEMO_I2C_ADDR_7BIT, DEMO_MPU6050_PWR_MGMT_1, 
0x00
u) == HAL_OK)

    {

        
printf
(
"Write PWR_MGMT_1 = 0x00 OK.\r\n"
);

    }

    
else

    {

        
printf
(
"Write PWR_MGMT_1 failed.\r\n"
);

    }

}

else

{

    
printf
(
"Device 0x%02X is not ready.\r\n"
, DEMO_I2C_ADDR_7BIT);

}

/* USER CODE END 2 */

为什么要先调用:

bash 复制代码
App_I2CReg_IsReady7Bit()

因为读写寄存器之前,先确认设备确实还在总线上。

这一步不是必须的,但对新手排查问题非常有帮助。

4. while 循环

本篇先只在上电后读一次寄存器,while 里可以空着。

如果想每 1 秒读一次,放到 USER CODE BEGIN 3

但入门阶段建议不要刷太快,避免串口刷屏。

编译、下载、验证

加完代码之后:

  1. Keil 编译;

  2. 下载程序;

  3. 打开串口助手;

  4. 复位板子;

  5. 查看输出。

正常输出大致是:

bash 复制代码
I2C register read/write test

Device 0x68 is ready.

WHO_AM_I = 0x68

Write PWR_MGMT_1 = 0x00 OK.

如果看到:

bash 复制代码
Device 0x68 is not ready.

说明设备地址或 I2C 总线还不通。

回退到第 18 篇 Scanner 确认能否扫到地址。

如果看到:

bash 复制代码
Device 0x68 is ready.

Read WHO_AM_I failed.

说明设备有回应,但寄存器读失败。

优先检查:

bash 复制代码
寄存器地址有没有写错

设备到底是不是 MPU6050

I2C 速度是不是太高

设备是否需要先上电延时

移植到其他 I2C 设备

虽然本篇用 MPU6050 举例,但代码不限于 MPU6050。

换设备时,重点关注改三类。

|

要改什么

|

原因

|

改动位置

|

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

|

I2C 实例

|

可能用的是 I2C1、I2C2

| APP_I2C_REG_HANDLE |

|

设备 7 位地址

|

不同设备地址不同

| DEMO_I2C_ADDR_7BIT |

|

寄存器地址

|

不同设备寄存器表不同

| DEMO_MPU6050_WHO_AM_I

等宏

|

|

寄存器地址宽度

|

部分设备内部地址是 16 位

| I2C_MEMADD_SIZE_8BIT

可能需要改

|

|

超时时间

|

慢设备可以适当加长

| APP_I2C_REG_TIMEOUT_MS |

|

设备初始化步骤

|

有些设备需要先写配置才能读

|

按手册写 Init 函数

|


补充实战:AT24C02 EEPROM 写读示例

以下内容是对本篇 I2C 寄存器读写知识的具体应用。AT24C02 没有"寄存器"的概念,但 app_i2c_reg 的封装完全适用于它------"寄存器地址"对应 EEPROM 内部的存储地址。掌握 MPU6050 的寄存器读写后,用同样的 app_i2c_reg 驱动 AT24C02 就是一个很好的巩固练习。

AT24C02 芯片背景

AT24C02 是一颗 2Kbit(256 字节)的 I2C 接口 EEPROM 芯片。

|

特性

|

参数

|

| --- | --- |

|

容量

|

256 × 8bit = 2Kbit

|

|

I2C 7-bit 地址

| 0x50

(A0/A1/A2 接地时)

|

|

页大小

|

8 字节

|

|

写周期

|

最大 5ms

|

|

时钟频率

|

标准 100KHz / 快速 400KHz

|

芯片引脚
bash 复制代码
          ┌───┴───┐

   A0   1 │       │ 8  VCC  (1.8V ~ 5.5V)

   A1   2 │ AT24  │ 7  WP   (写保护,接 GND 允许写入)

   A2   3 │  C02  │ 6  SCL  (I2C 时钟线)

   GND  4 │       │ 5  SDA  (I2C 数据线)

          └───────┘
  • A0/A1/A2 :片选地址引脚。三根引脚接 GND 或 VCC,组合出 8 个不同地址(0x50~0x57),一条 I2C 总线上最多挂 8 颗 AT24C02。

  • WP:写保护。接 VCC 时整颗芯片只读不写;接 GND 时允许正常写入。

  • SCL/SDA:标准 I2C 总线,需要外接 4.7KΩ 上拉电阻到 VCC。

存储布局

AT24C02 共 256 字节,按 8 字节一页划分为 32 页。页是写入操作的最小跨距单位------连续写入不能跨越页边界。

bash 复制代码
AT24C02 Memory Map (256 bytes = 32 pages × 8 bytes)


Page   Address Range

 0    0x00 ┌──┬──┬──┬──┬──┬──┬──┬──┐

           │  │  │  │  │  │  │  │  │

           └──┴──┴──┴──┴──┴──┴──┴──┘

 1    0x08 ┌──┬──┬──┬──┬──┬──┬──┬──┐

           │  │  │  │  │  │  │  │  │

           └──┴──┴──┴──┴──┴──┴──┴──┘

...         ...

31    0xF8 ┌──┬──┬──┬──┬──┬──┬──┬──┐

           │  │  │  │  │  │  │  │  │

           └──┴──┴──┴──┴──┴──┴──┴──┘

AT24C02 与 MPU6050 的关键区别

MPU6050 的操作模型是"寄存器读写"------每个寄存器有固定功能和地址。AT24C02 则是一个存储阵列------"寄存器地址"就是存储单元的地址(0~255)。同一个 app_i2c_reg 封装可以驱动两者:

| |

MPU6050

|

AT24C02

|

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

|

I2C 7-bit 地址

| 0x68

/ 0x69

| 0x50 |

|

"寄存器地址"

|

功能寄存器(如 0x75 = WHO_AM_I)

|

存储单元地址(0x00~0xFF

|

|

写操作特点

|

写完立即生效

|

写完后需等待 5ms 内部烧录

|

|

连续写入限制

|

无页限制(取决于寄存器布局)

|

8 字节页,超出一页会回卷

|

|

读操作特点

|

直接读

|

先"伪写"地址,再重复起始条件读

|

设计要点

为什么写完要等 5ms?

AT24C02 收到写入命令后,会把数据从内部缓冲器烧录到 EEPROM 单元。在这段内部写周期(最长 5ms)内:

  • 芯片不响应任何 I2C 命令

  • 如果立即去读,会收到 NACK,读取失败

  • 如果立即去写下一页,数据会丢失

所以 HAL_Delay(5) 不是可选的------它是时序协议的一部分

bash 复制代码
写周期时序示意:


SDA  ──┬──┬──┬──┬──────────┬──┬──┬──────

       │S │  │  │ ......   │P │  │

       │  │  │  │  数据    │  │  │

       └──┴──┴──┴──────────┴──┴──┴──────

                                 │

    ┌────────────────────────────┘

    │  STOP 之后进入内部写周期 (Twr ≤ 5ms)

    │  芯片在此期间不响应任何 I2C 命令

    ▼

  ╔══════════════╗

  ║  内部烧录中   ║── Twr ──▶ 烧录完成,芯片恢复响应

  ╚══════════════╝
为什么一次只写 8 字节?

AT24C02 内部有一个 8 字节的页缓冲器。如果一次写入超过 8 字节:

bash 复制代码
向地址 0x00 连续写入 10 字节:


  页0 (0x00~0x07)

  ┌────┬────┬────┬────┬────┬────┬────┬────┐

  │ D0 │ D1 │ D2 │ D3 │ D4 │ D5 │ D6 │ D7 │  ← 先填满 8 个位置

  ├────┼────┼────┼────┼────┼────┼────┼────┤

  │ D8 │ D9 │ ?? │ ?? │ ?? │ ?? │ ?? │ ?? │  ← 地址回卷!

  └────┴────┴────┴────┴────┴────┴────┴────┘

    ↑

    D8 覆盖了 D0 的位置,D9 覆盖了 D1 的位置

    最终存入的是 {D8, D9, D2, D3, D4, D5, D6, D7} ------ 前两字节丢了!

地址低 3 位回卷归零,超过 8 字节的部分不会自动跨到下一页,而是回到当前页的开头继续写。

如果需要写超过 8 字节,应分页写入:

bash 复制代码
跨页写入的正确方式:


  第1次写入          
wait
 5ms      第2次写入

  地址 0x00~0x07 ──────────────▶ 地址 0x08~0x0F

  ┌──┬──┬──┬──┬──┬──┬──┬──┐     ┌──┬──┬──┬──┬──┬──┬──┬──┐

  │D0│D1│D2│D3│D4│D5│D6│D7│     │D8│D9│DA│DB│DC│DD│DE│DF│

  └──┴──┴──┴──┴──┴──┴──┴──┘     └──┴──┴──┴──┴──┴──┴──┴──┘

       Page 0                         Page 1

主逻辑代码(替换 main.c 中 USER CODE BEGIN 2)

先在 USER CODE BEGIN PD 中添加宏:

bash 复制代码
/* USER CODE BEGIN PD */

#define AT24C02_ADDR_7BIT    0x50u

#define EEPROM_START_ADDR    0x00u

#define TEST_DATA_LEN        8u

/* USER CODE END PD */

然后在 USER CODE BEGIN 2 中替换为 AT24C02 写读逻辑:

bash 复制代码
/* USER CODE BEGIN 2 */

uint8_t
 tx_data[
8
] = {
0xAA
, 
0x55
, 
0x12
, 
0x34
, 
0xAB
, 
0xCD
, 
0xEF
, 
0x00
};

uint8_t
 rx_data[
8
] = {
0
};

uint8_t
 i;


App_I2CReg_Init();


printf
(
"\r\n=== AT24C02 EEPROM Write-Read Demo ===\r\n"
);


/* 1. 检测 AT24C02 是否在线 */

if
 (App_I2CReg_IsReady7Bit(AT24C02_ADDR_7BIT) != HAL_OK)

{

    
printf
(
"ERROR: AT24C02 (0x%02X) not found!\r\n"
, AT24C02_ADDR_7BIT);

    
while
 (
1
) {}

}

printf
(
"AT24C02 (0x%02X) detected.\r\n"
, AT24C02_ADDR_7BIT);


/* 2. 写入 8 字节测试数据 */

printf
(
"\r\nWriting %d bytes to EEPROM addr 0x%02X ...\r\n"
,

       TEST_DATA_LEN, EEPROM_START_ADDR);

for
 (i = 
0
; i < TEST_DATA_LEN; i++)

{

    
printf
(
"  [%d] = 0x%02X\r\n"
, i, tx_data[i]);

}


if
 (App_I2CReg_WriteBytes(AT24C02_ADDR_7BIT, EEPROM_START_ADDR,

                           tx_data, TEST_DATA_LEN) != HAL_OK)

{

    
printf
(
"ERROR: Write failed!\r\n"
);

    
while
 (
1
) {}

}

printf
(
"Write OK.\r\n"
);


/* 3. 等待内部写周期(AT24C02 最长 5ms) */

HAL_Delay(
5
);


/* 4. 读回数据 */

printf
(
"\r\nReading %d bytes from EEPROM addr 0x%02X ...\r\n"
,

       TEST_DATA_LEN, EEPROM_START_ADDR);


if
 (App_I2CReg_ReadBytes(AT24C02_ADDR_7BIT, EEPROM_START_ADDR,

                          rx_data, TEST_DATA_LEN) != HAL_OK)

{

    
printf
(
"ERROR: Read failed!\r\n"
);

    
while
 (
1
) {}

}


/* 5. 逐字节比对并输出表格 */

printf
(
"\r\nByte  | Write  | Read   | Status\r\n"
);

printf
(
"------+--------+--------+-------\r\n"
);


uint8_t
 mismatch = 
0
;

for
 (i = 
0
; i < TEST_DATA_LEN; i++)

{

    
printf
(
" [%d]  | 0x%02X   | 0x%02X   | %s\r\n"
,

           i, tx_data[i], rx_data[i],

           (tx_data[i] == rx_data[i]) ? 
"OK"
 : 
"FAIL"
);

    
if
 (tx_data[i] != rx_data[i])

    {

        mismatch = 
1
;

    }

}


if
 (mismatch)

{

    
printf
(
"\r\n*** VERIFY FAILED ***\r\n"
);

}

else

{

    
printf
(
"\r\n*** All bytes match - test PASSED ***\r\n"
);

}


printf
(
"\r\nDemo complete.\r\n"
);

/* USER CODE END 2 */

I2C 写时序

AT24C02 的写操作与 MPU6050 的寄存器写使用完全相同的 HAL 函数,I2C 总线上的时序如下:

bash 复制代码
┌──┐ ┌──────────┐ ┌──┐ ┌──────────┐ ┌──┐ ┌──────────┐ ┌──┐       ┌──┐

│S │ │ DevAddr  │ │A │ │ MemAddr  │ │A │ │  Data0   │ │A │  ...  │P │

│  │ │ +W(0xA0) │ │C │ │  (0x00)  │ │C │ │  (0xAA)  │ │C │       │  │

└──┘ └──────────┘ └──┘ └──────────┘ └──┘ └──────────┘ └──┘       └──┘

                                                  │

                                   重复 7 次(共 8 字节数据)


S  = START condition  (SCL=H 时 SDA: H→L)

P  = STOP condition   (SCL=H 时 SDA: L→H)

ACK= 从机拉低 SDA 确认

对于 AT24C02,"MemAddr"就是 EEPROM 内部的存储地址(0~255),等同于 MPU6050 的寄存器地址概念。

I2C 读时序(随机读)

bash 复制代码
┌──┐ ┌──────────┐ ┌──┐ ┌──────────┐ ┌──┐ ┌──┐ ┌──────────┐ ┌──┐ ┌──────────┐ ┌──┐ ┌──┐

│S │ │ DevAddr  │ │A │ │ MemAddr  │ │A │ │Sr│ │ DevAddr  │ │A │ │  Data0   │ │N │ │P │

│  │ │ +W(0xA0) │ │C │ │  (0x00)  │ │C │ │  │ │ +R(0xA1) │ │C │ │  (
read
)  │ │A │ │  │

└──┘ └──────────┘ └──┘ └──────────┘ └──┘ └──┘ └──────────┘ └──┘ └──────────┘ └──┘ └──┘

                                                  │                       │

                                     REPEAT START  │    最后字节回复 NACK │


S  = START    Sr = REPEAT START    P  = STOP

ACK= 从机应答  NACK = 主机不应答(告诉从机
"不要再发了"
)


关键区别:

  写方向 DevAddr = 0xA0(最低位 = 0)

  读方向 DevAddr = 0xA1(最低位 = 1)

  区别就在最低 1 位,由 app_i2c_reg 内部左移后自动处理。

注意:和读 MPU6050 寄存器一样,读 EEPROM 也是 ①先"伪写"地址告诉芯片从哪开始读 → ②重复起始条件切到读模式读出数据。

AT24C02 接线示意图

bash 复制代码
     STM32F1                          AT24C02

   ┌──────────┐                    ┌──────────┐

   │          │                    │          │

   │   PB6 ───┼────────────────────┼── SCL    │

   │   PB7 ───┼────────────────────┼── SDA    │

   │          │      4.7KΩ         │          │

   │          │   ┌─[R]── VCC      │   A0 ────┼── GND

   │          │   │         │      │   A1 ────┼── GND

   │          │   │         │      │   A2 ────┼── GND

   │          │   ├─[R]──┐  │      │   WP ────┼── GND

   │          │   │      │  │      │          │

   │        VCC ──┘      │  └────── VCC       │

   │        GND ─────────┻───────── GND       │

   └──────────┘                    └──────────┘


   注意:

   - SCL/SDA 各需要一个 4.7KΩ 上拉电阻到 VCC

   - A0/A1/A2 接地 → 7-bit 地址 = 0x50

   - WP 接地 → 允许写入

AT24C02 常见问题

**Q1:读回来的全是 0xFF?**EEPROM 出厂时所有字节都是 0xFF。如果读回 0xFF 说明数据没写进去,检查:

  • 写完有没有等 5ms?

  • I2C 总线上拉电阻接了吗?(通常 4.7KΩ)

  • AT24C02 的 WP(写保护)引脚是否接地?悬空或接 VCC 会禁止写入。

**Q2:写入超过 8 字节时前几个字节被覆盖?**见上文"为什么一次只写 8 字节"。解决办法:每 8 字节写一页,每页之间等 5ms,地址递增 8。

**Q3:可以不用 HAL_Delay 而用轮询方式等待吗?**可以。AT24C02 在写周期内不响应 I2C,所以可以循环发 IsReady 直到 ACK 返回。HAL_Delay(5) 是最简单可靠的做法。


常见问题排查

1. Scanner 能扫到地址,但 ID 读失败

这说明"设备在线"和"寄存器读正确"是两件事。

优先检查:

|

检查项

|

说明

|

| --- | --- |

|

设备型号

|

你的模块到底是不是 MPU6050

|

|

设备地址

|

你扫出来的是 0x68 还是 0x69

|

|

寄存器地址

| WHO_AM_I

真的是 0x75

|

|

地址移位

|

代码已经内部左移了,不要再传 0xD0 进来

|

|

上电延时

|

有些模块上电后需要一点时间才能读

|

|

I2C 速度

|

先用 100 kHz,别一上来就 400 kHz

|

2. 读出来的值不是 0x68

可能原因:

  • 你用的不是 MPU6050;

  • 模块上的芯片是兼容型号,ID 值不同;

  • 寄存器读错了;

  • 设备地址选错了;

  • I2C 总线不稳;

  • 模块供电或上拉不合适。

不要纠结"必须等于 0x68"。

先看你的芯片数据手册,确认它预期的 WHO_AM_I 值是什么。

3. Write PWR_MGMT_1 失败

先确认读操作成功。

如果读也失败,先不要急着调试写。

读成功但写失败,检查:

  • 这个寄存器是否可写;

  • 写入的值是否符合数据手册;

  • 设备是否处于允许写的状态;

  • 是否需要先退出睡眠或复位;

  • 超时时间是否太短。

MPU6050 常见的初始化写入是:

bash 复制代码
PWR_MGMT_1 = 0x00

把设备从睡眠唤醒。

但不同芯片不要盲目照抄这个寄存器。

4. 编译提示 hi2c1 未定义

你的工程没有打开 I2C1,或者实际用的是 I2C2。

解决办法:

  1. 打开 i2c.c

  2. 看句柄是 hi2c1 还是 hi2c2

  3. 修改:

bash 复制代码
#define APP_I2C_REG_HANDLE hi2c1

如果实际是 I2C2,改成:

bash 复制代码
#define APP_I2C_REG_HANDLE hi2c2

5. 编译提示 undefined symbol App_I2CReg_ReadReg8

一般是:

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

解决办法:

  1. 右键 Application/User/Core

  2. 选择 Add Existing Files to Group

  3. 添加 Core/Src/app_i2c_reg.c

  4. 重新编译。

6. 传 0xD0 而不是 0x68 会怎样?

本系列的函数要求传:

bash 复制代码
7 位地址

也就是:

bash 复制代码
0x68

函数内部会左移:

bash 复制代码
device_addr_7bit << 
1

如果你传 0xD0 进来,内部移位后地址就错了。

所以记住:

bash 复制代码
应用层传 7 位地址

HAL 层内部左移

7. 不同设备的寄存器地址宽度不同

本篇用的是:

bash 复制代码
I2C_MEMADD_SIZE_8BIT

很多传感器都适用。

但有些 EEPROM 或者外设内部地址是 16 位的。

那种情况要改成:

bash 复制代码
I2C_MEMADD_SIZE_16BIT

而且函数参数要从 uint8_t reg_addr 改成 uint16_t reg_addr

这不是 I2C 总线变了,是设备内部寄存器地址宽度变了。

总结

这一篇从 I2C Scanner 往前走了一步,完成了 I2C 寄存器读写。

现在你应该知道:

  • 扫到地址只是证明设备在线;

  • 读寄存器时要分清设备地址、寄存器地址、寄存器值;

  • 应用层继续使用 7 位地址;

  • 调 STM32 HAL 时,设备地址一般要左移 1 位;

  • HAL_I2C_Mem_Read() 适合读寄存器;

  • HAL_I2C_Mem_Write() 适合写寄存器;

  • 换设备时不要盲目照抄 MPU6050 的寄存器地址,要看对应数据手册。

  • 同样的 app_i2c_reg 封装可以驱动 MPU6050(寄存器型设备),也可以驱动 AT24C02(存储型设备),区别只在于设备地址和地址空间含义。

下一篇继续 I2C 的内容:

STM32 I2C 故障排查:为什么总是 NACK 或者卡死。

I2C 的坑很多,但排查顺序其实比较固定。下一篇会把电源、上拉、地址、时序、错误码、总线恢复一起讲清楚。

相关推荐
minglie11 小时前
zynq用普通网口在局域网同步
单片机
weixin_467182281 小时前
Arduino进阶二|自定义类库保姆级教程(从零手写属于自己的传感器类库+完整源码)
c语言·c++·单片机·嵌入式硬件·arduino·c++面向对象·diy库文件
清风6666662 小时前
基于单片机的64位多模式流水灯控制系统设计
单片机·毕业设计·课程设计·期末大作业
进击的横打2 小时前
【车载开发系列】热敏电阻与上下拉电阻
单片机·嵌入式硬件
XINVRY-FPGA2 小时前
XCKU035-2FBVA676I AMD Xilinx Kintex UltraScale FPGA
arm开发·嵌入式硬件·网络安全·fpga开发·硬件工程·信号处理·fpga
崇山峻岭之间2 小时前
单片机USB虚拟串口实验
单片机·嵌入式硬件
崇山峻岭之间3 小时前
单片机USB U盘实验
单片机·嵌入式硬件
点灯小铭3 小时前
基于单片机的锅炉压力与温度监测报警系统设计
数据库·单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
环境倒逼我学习3 小时前
无人机地面站之第13章 Mission Planner 入门与界面总览
单片机·嵌入式硬件·无人机