STM32 I2C通信详解:从机地址与寄存器地址的作用

一、I2C总线基础回顾

I2C总线特点

  • 两根线:SCL(串行时钟线)和SDA(串行数据线)

  • 多主多从:支持多个主设备和从设备

  • 地址寻址:每个从设备有唯一的7位或10位地址

  • 半双工通信:同一时间只能单向传输

I2C通信帧结构

开始信号 + 从机地址(7位) + R/W位 + ACK + 数据 + ACK + ... + 停止信号

二、从机地址:设备的"身份证"

1. 从机地址的作用

从机地址是I2C总线上识别特定设备的唯一标识。当主设备发起通信时,它会先发送从机地址,只有地址匹配的从设备才会响应。

2. 地址格式

7位地址模式(最常用)
复制代码
| 7位地址 | R/W |
|---------|-----|
| A6-A0   | 0/1 |
  • 地址范围:0x08 到 0x77(0x00-0x07和0x78-0x7F为保留地址)

  • R/W位:0表示写操作,1表示读操作

  • 实际发送:左移1位 + R/W位

cs 复制代码
// 示例:从机地址为0x50,写操作
// 实际发送:0x50 << 1 | 0 = 0xA0
uint8_t slave_addr = 0x50;
uint8_t write_addr = (slave_addr << 1) | 0;  // 0xA0
uint8_t read_addr = (slave_addr << 1) | 1;   // 0xA1
10位地址模式

开始信号 + 11110+A9+A8+R/W + ACK + A7-A0 + ACK + 数据...

3. 常见的I2C设备地址

设备类型 典型地址范围 常见地址
EEPROM 0x50-0x57 0x50
实时时钟RTC 0x68 0x68
温度传感器 0x48, 0x4E 0x48
加速度计 0x1D, 0x53 0x1D
显示屏 0x3C, 0x3D 0x3C

4. 地址冲突与解决方法

硬件配置

许多I2C设备有地址选择引脚(A0, A1, A2),通过连接VCC或GND改变地址:

cs 复制代码
// MPU6050地址配置示例
// AD0引脚接地:地址 = 0x68
// AD0引脚接VCC:地址 = 0x69
#define MPU6050_ADDR_AD0_LOW  0x68
#define MPU6050_ADDR_AD0_HIGH 0x69
软件方案
cs 复制代码
// 地址扫描程序
void I2C_ScanDevices(void)
{
    printf("Scanning I2C devices...\n");
    
    for(uint8_t addr = 0x08; addr <= 0x77; addr++)
    {
        HAL_StatusTypeDef status;
        
        // 尝试与设备通信
        status = HAL_I2C_IsDeviceReady(&hi2c1, addr << 1, 3, 10);
        
        if(status == HAL_OK)
        {
            printf("Device found at address: 0x%02X\n", addr);
        }
    }
}

三、寄存器地址:设备内部的"房间号"

1. 寄存器地址的作用

对于大多数I2C设备,内部都有多个寄存器来存储配置参数、状态信息或测量数据。寄存器地址就是访问这些内部存储单元的指针。

2. 寄存器地址类型

单字节地址(8位)

大多数设备使用8位寄存器地址:

cs 复制代码
// 典型的读写时序
// 写操作:开始 + 从机地址(写) + ACK + 寄存器地址 + ACK + 数据 + ACK + 停止
// 读操作:开始 + 从机地址(写) + ACK + 寄存器地址 + ACK + 重复开始 + 从机地址(读) + ACK + 读数据 + NACK + 停止
双字节地址(16位)

某些大容量设备使用16位寄存器地址:

// 需要发送两个字节的地址

// 高字节在前,低字节在后

3. 寄存器地址映射示例

以MPU6050加速度计为例:

寄存器地址 寄存器名称 功能描述
0x3B ACCEL_XOUT_H 加速度计X轴高字节
0x3C ACCEL_XOUT_L 加速度计X轴低字节
0x3D ACCEL_YOUT_H 加速度计Y轴高字节
0x3E ACCEL_YOUT_L 加速度计Y轴低字节
0x6B PWR_MGMT_1 电源管理寄存器1
0x75 WHO_AM_I 设备ID寄存器

四、完整通信流程分析

1. 写入单个寄存器

cs 复制代码
// 向MPU6050的PWR_MGMT_1寄存器(0x6B)写入0x00(唤醒设备)
HAL_StatusTypeDef MPU6050_WriteReg(uint8_t reg_addr, uint8_t data)
{
    uint8_t buffer[2];
    buffer[0] = reg_addr;  // 寄存器地址
    buffer[1] = data;      // 要写入的数据
    
    // 发送:从机地址(写) + 寄存器地址 + 数据
    return HAL_I2C_Master_Transmit(&hi2c1, 
                                   MPU6050_ADDR << 1,  // 从机地址(写模式)
                                   buffer, 
                                   2,                  // 两个字节
                                   100);               // 超时时间
}

通信时序解析:

cs 复制代码
主设备发送:
[开始] [0xD0] [ACK] [0x6B] [ACK] [0x00] [ACK] [停止]

解释:
0xD0 = 0x68 << 1 | 0 = 从机地址(写)
0x6B = 寄存器地址
0x00 = 写入的数据

2. 读取单个寄存器

cs 复制代码
// 从MPU6050的WHO_AM_I寄存器(0x75)读取设备ID
HAL_StatusTypeDef MPU6050_ReadReg(uint8_t reg_addr, uint8_t *data)
{
    // 第一步:发送寄存器地址(写模式)
    HAL_StatusTypeDef status;
    status = HAL_I2C_Master_Transmit(&hi2c1, 
                                     MPU6050_ADDR << 1, 
                                     &reg_addr, 
                                     1, 
                                     100);
    if(status != HAL_OK) return status;
    
    // 第二步:读取数据(读模式)
    return HAL_I2C_Master_Receive(&hi2c1, 
                                  (MPU6050_ADDR << 1) | 1,  // 从机地址(读模式)
                                  data, 
                                  1, 
                                  100);
}

通信时序解析:

第一阶段(设置寄存器指针):

开始\] \[0xD0\] \[ACK\] \[0x75\] \[ACK\] \[停止

第二阶段(读取数据):

开始\] \[0xD1\] \[ACK\] \[数据\] \[NACK\] \[停止

解释:

0xD0 = 0x68 << 1 | 0 = 从机地址(写)

0x75 = 寄存器地址

0xD1 = 0x68 << 1 | 1 = 从机地址(读)

3. 连续读取多个寄存器

cs 复制代码
// 从MPU6050读取加速度计数据(6个连续寄存器)
HAL_StatusTypeDef MPU6050_ReadAccel(int16_t *accel_data)
{
    uint8_t buffer[6];
    uint8_t reg_addr = 0x3B;  // ACCEL_XOUT_H寄存器地址
    
    // 发送起始寄存器地址
    HAL_I2C_Master_Transmit(&hi2c1, 
                           MPU6050_ADDR << 1, 
                           &reg_addr, 
                           1, 
                           100);
    
    // 连续读取6个字节
    HAL_I2C_Master_Receive(&hi2c1, 
                          (MPU6050_ADDR << 1) | 1, 
                          buffer, 
                          6, 
                          100);
    
    // 合并为16位数据
    accel_data[0] = (buffer[0] << 8) | buffer[1];  // X轴
    accel_data[1] = (buffer[2] << 8) | buffer[3];  // Y轴
    accel_data[2] = (buffer[4] << 8) | buffer[4];  // Z轴
    
    return HAL_OK;
}

五、实际应用:OLED显示屏驱动

1. SSD1306 OLED显示屏

设备地址
  • 通常为0x3C或0x3D

  • 由SA0引脚决定:接地=0x3C,接VCC=0x3D

寄存器/命令结构

SSD1306没有传统意义上的寄存器,而是通过发送命令和数据来控制:

cs 复制代码
#define OLED_ADDRESS    0x3C
#define OLED_CMD_MODE   0x00  // 命令模式
#define OLED_DATA_MODE  0x40  // 数据模式

// 向OLED发送命令
void OLED_WriteCommand(uint8_t cmd)
{
    uint8_t buffer[2];
    buffer[0] = OLED_CMD_MODE;  // 控制字节:Co=0, D/C#=0
    buffer[1] = cmd;            // 命令
    
    HAL_I2C_Master_Transmit(&hi2c1, 
                           OLED_ADDRESS << 1, 
                           buffer, 
                           2, 
                           100);
}

// 向OLED发送数据
void OLED_WriteData(uint8_t data)
{
    uint8_t buffer[2];
    buffer[0] = OLED_DATA_MODE;  // 控制字节:Co=0, D/C#=1
    buffer[1] = data;            // 显示数据
    
    HAL_I2C_Master_Transmit(&hi2c1, 
                           OLED_ADDRESS << 1, 
                           buffer, 
                           2, 
                           100);
}

// 初始化OLED
void OLED_Init(void)
{
    // 一系列初始化命令
    OLED_WriteCommand(0xAE);  // 关闭显示
    OLED_WriteCommand(0xD5);  // 设置时钟分频
    OLED_WriteCommand(0x80);
    OLED_WriteCommand(0xA8);  // 设置复用率
    OLED_WriteCommand(0x3F);
    OLED_WriteCommand(0xD3);  // 设置显示偏移
    OLED_WriteCommand(0x00);
    OLED_WriteCommand(0x40);  // 设置起始行
    OLED_WriteCommand(0x8D);  // 电荷泵设置
    OLED_WriteCommand(0x14);
    OLED_WriteCommand(0x20);  // 内存地址模式
    OLED_WriteCommand(0x00);
    OLED_WriteCommand(0xA1);  // 段重映射
    OLED_WriteCommand(0xC8);  // 扫描方向
    OLED_WriteCommand(0xDA);  // COM引脚配置
    OLED_WriteCommand(0x12);
    OLED_WriteCommand(0x81);  // 对比度设置
    OLED_WriteCommand(0xCF);
    OLED_WriteCommand(0xD9);  // 预充电周期
    OLED_WriteCommand(0xF1);
    OLED_WriteCommand(0xDB);  // VCOMH电平
    OLED_WriteCommand(0x40);
    OLED_WriteCommand(0xA4);  // 显示全部点亮
    OLED_WriteCommand(0xA6);  // 正常显示
    OLED_WriteCommand(0xAF);  // 开启显示
}

六、EEPROM读写示例

AT24C02 EEPROM(256字节)

cs 复制代码
#define EEPROM_ADDR     0x50  // 从机地址

// 向EEPROM写入数据
void EEPROM_WriteByte(uint16_t mem_addr, uint8_t data)
{
    uint8_t buffer[3];
    
    // AT24C02只有256字节,地址范围为0x00-0xFF
    buffer[0] = (uint8_t)(mem_addr & 0xFF);  // 内存地址
    buffer[1] = data;                        // 数据
    
    HAL_I2C_Master_Transmit(&hi2c1, 
                           EEPROM_ADDR << 1, 
                           buffer, 
                           2,  // 注意:只有地址和数据两个字节
                           100);
    
    // EEPROM写入需要时间,等待5ms
    HAL_Delay(5);
}

// 从EEPROM读取数据
uint8_t EEPROM_ReadByte(uint16_t mem_addr)
{
    uint8_t data;
    
    // 先发送要读取的地址
    uint8_t addr_byte = (uint8_t)(mem_addr & 0xFF);
    HAL_I2C_Master_Transmit(&hi2c1, 
                           EEPROM_ADDR << 1, 
                           &addr_byte, 
                           1, 
                           100);
    
    // 然后读取数据
    HAL_I2C_Master_Receive(&hi2c1, 
                          (EEPROM_ADDR << 1) | 1, 
                          &data, 
                          1, 
                          100);
    
    return data;
}

// 连续写入多个字节(页写入)
void EEPROM_WritePage(uint16_t start_addr, uint8_t *data, uint8_t len)
{
    uint8_t buffer[9];  // 1字节地址 + 最多8字节数据(AT24C02页大小为8字节)
    
    if(len > 8) len = 8;  // 确保不超过页大小
    
    buffer[0] = (uint8_t)(start_addr & 0xFF);  // 起始地址
    
    // 复制数据
    for(uint8_t i = 0; i < len; i++)
    {
        buffer[i + 1] = data[i];
    }
    
    HAL_I2C_Master_Transmit(&hi2c1, 
                           EEPROM_ADDR << 1, 
                           buffer, 
                           len + 1, 
                           100);
    
    HAL_Delay(5);  // 等待写入完成
}

七、常见问题与调试技巧

1. I2C通信失败排查

cs 复制代码
void I2C_Debug_Info(void)
{
    printf("I2C状态检查:\n");
    
    // 检查总线是否繁忙
    if(HAL_I2C_GetState(&hi2c1) == HAL_I2C_STATE_BUSY)
    {
        printf("I2C总线繁忙\n");
    }
    
    // 检查设备是否响应
    for(uint8_t i = 0; i < 3; i++)
    {
        HAL_StatusTypeDef status;
        status = HAL_I2C_IsDeviceReady(&hi2c1, 0x50 << 1, 1, 10);
        
        if(status == HAL_OK)
        {
            printf("设备0x50响应正常\n");
            break;
        }
        else
        {
            printf("设备0x50无响应,尝试 %d/3\n", i+1);
        }
    }
    
    // 检查错误标志
    if(__HAL_I2C_GET_FLAG(&hi2c1, I2C_FLAG_BERR))
    {
        printf("总线错误\n");
        __HAL_I2C_CLEAR_FLAG(&hi2c1, I2C_FLAG_BERR);
    }
    
    if(__HAL_I2C_GET_FLAG(&hi2c1, I2C_FLAG_ARLO))
    {
        printf("仲裁丢失\n");
        __HAL_I2C_CLEAR_FLAG(&hi2c1, I2C_FLAG_ARLO);
    }
    
    if(__HAL_I2C_GET_FLAG(&hi2c1, I2C_FLAG_AF))
    {
        printf("应答失败\n");
        __HAL_I2C_CLEAR_FLAG(&hi2c1, I2C_FLAG_AF);
    }
}

2. 上拉电阻的重要性

// I2C总线需要外部上拉电阻

// 典型值:4.7kΩ 到 10kΩ

// SCL和SDA线都需要上拉到VCC

3. 地址确认技巧

cs 复制代码
// 通过读取WHO_AM_I等设备ID寄存器确认设备
uint8_t MPU6050_CheckID(void)
{
    uint8_t id;
    MPU6050_ReadReg(0x75, &id);  // WHO_AM_I寄存器
    
    if(id == 0x68)  // MPU6050的ID是0x68
    {
        printf("MPU6050检测成功,ID=0x%02X\n", id);
        return 1;
    }
    else
    {
        printf("设备ID错误:0x%02X\n", id);
        return 0;
    }
}

八、STM32 I2C硬件配置

使用CubeMX配置

cs 复制代码
// 典型的I2C配置参数
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 100000;       // 100kHz标准模式
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0;           // 主设备地址设为0
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;

软件模拟I2C(GPIO模拟)

cs 复制代码
// 当硬件I2C有问题时,可以使用GPIO模拟
void I2C_Soft_Init(void)
{
    // 配置SCL和SDA为开漏输出
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    
    // SCL引脚
    GPIO_InitStruct.Pin = GPIO_PIN_6;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_OD;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
    
    // SDA引脚
    GPIO_InitStruct.Pin = GPIO_PIN_7;
    HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
}

// 软件I2C起始信号
void I2C_Soft_Start(void)
{
    SDA_HIGH();
    SCL_HIGH();
    Delay_us(5);
    SDA_LOW();
    Delay_us(5);
    SCL_LOW();
}

总结

从机地址 vs 寄存器地址

特性 从机地址 寄存器地址
作用 选择总线上的设备 选择设备内部的存储位置
大小 7位或10位 通常8位,有时16位
发送时机 每次通信开始 读写数据之前
唯一性 总线上唯一 设备内部唯一

关键要点

  1. 从机地址是设备选择的关键:确保总线上没有地址冲突

  2. 寄存器地址是数据访问的指针:理解设备的寄存器映射表

  3. 正确配置R/W位:写操作=0,读操作=1

  4. 注意ACK/NACK:正确响应是通信成功的基础

  5. 时序很重要:特别是重复开始条件和停止条件

最佳实践

  1. 始终检查设备的响应(ACK)

  2. 为每个I2C设备编写专用的驱动程序

  3. 实现设备检测和错误处理机制

  4. 使用逻辑分析仪或示波器调试复杂的I2C通信问题

  5. 注意上拉电阻的阻值选择,平衡速度和功耗

掌握从机地址和寄存器地址的概念,你就掌握了I2C通信的核心。在实际项目中,建议从简单的EEPROM读写开始,逐步尝试更复杂的传感器和显示屏驱动,这样能更好地理解I2C通信的细节和技巧。

相关推荐
坚持就完事了2 小时前
CMD操作的学习
学习
普中科技2 小时前
【普中51单片机开发攻略--基于普中-2&普中-3&普中-4】-- 第 14 章 矩阵按键实验
单片机·嵌入式硬件·51单片机·开发板·按键检测·矩阵按键·普中科技
炽烈小老头2 小时前
【每天学习一点算法 2025/12/30】最大子序和
学习·算法
搞机械的假程序猿2 小时前
普中51单片机学习笔记-LCD1602液晶显示
笔记·学习·51单片机
hetao17338372 小时前
2025-12-30 hetao1733837 的刷题笔记
c++·笔记·算法
HyperAI超神经2 小时前
【vLLM 学习】Reproduciblity
人工智能·深度学习·学习·cpu·gpu·编程语言·vllm
好奇龙猫2 小时前
【大学院-筆記試験練習:数据库(データベース問題訓練) と 软件工程(ソフトウェア)(4)】
学习
YJlio2 小时前
LDMDump 学习笔记(13.9):动态磁盘元数据“黑盒”拆解工具
windows·笔记·学习
DisonTangor2 小时前
腾讯开源混元翻译——HY-MT1.5
学习·自然语言处理·开源·aigc