news 2026/8/4 0:41:25

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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位

// 示例:从机地址为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设备地址

设备类型典型地址范围常见地址
EEPROM0x50-0x570x50
实时时钟RTC0x680x68
温度传感器0x48, 0x4E0x48
加速度计0x1D, 0x530x1D
显示屏0x3C, 0x3D0x3C

4. 地址冲突与解决方法

硬件配置

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

// MPU6050地址配置示例 // AD0引脚接地:地址 = 0x68 // AD0引脚接VCC:地址 = 0x69 #define MPU6050_ADDR_AD0_LOW 0x68 #define MPU6050_ADDR_AD0_HIGH 0x69
软件方案
// 地址扫描程序 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位寄存器地址:

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

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

// 需要发送两个字节的地址
// 高字节在前,低字节在后

3. 寄存器地址映射示例

以MPU6050加速度计为例:

寄存器地址寄存器名称功能描述
0x3BACCEL_XOUT_H加速度计X轴高字节
0x3CACCEL_XOUT_L加速度计X轴低字节
0x3DACCEL_YOUT_H加速度计Y轴高字节
0x3EACCEL_YOUT_L加速度计Y轴低字节
0x6BPWR_MGMT_1电源管理寄存器1
0x75WHO_AM_I设备ID寄存器

四、完整通信流程分析

1. 写入单个寄存器

// 向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); // 超时时间 }

通信时序解析:

主设备发送: [开始] [0xD0] [ACK] [0x6B] [ACK] [0x00] [ACK] [停止] 解释: 0xD0 = 0x68 << 1 | 0 = 从机地址(写) 0x6B = 寄存器地址 0x00 = 写入的数据

2. 读取单个寄存器

// 从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. 连续读取多个寄存器

// 从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没有传统意义上的寄存器,而是通过发送命令和数据来控制:

#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字节)

#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通信失败排查

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. 地址确认技巧

// 通过读取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配置

// 典型的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模拟)

// 当硬件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通信的细节和技巧。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/28 5:37:06

Zanzibar vs MySQL Permission System - 实证性能对比研究

完整的实证研究项目: 对比传统MySQL展开存储和Google Zanzibar风格的元组图遍历权限系统 基于真实测试数据: 5,000用户 | 100,000文档 | 50,000客户 | 生产规模验证 仓库地址: https://github.com/d60-Lab/zanzibar 1️⃣ 业务背景 这是一个典型的企业文档协作系统的权限管理场…

作者头像 李华
网站建设 2026/7/29 1:10:34

生成型消费与体验型消费:未来各行各业发展展望

当AI如同阳光、空气般无处不在&#xff0c;消费不再是简单的购买行为&#xff0c;而是一场与机器共舞的创造之旅清晨&#xff0c;你对着智能音箱说出一个模糊的创意&#xff1a;“想要一款带有东方美学元素的家居装饰”。几分钟后&#xff0c;AI生成了十种设计方案供你选择。确…

作者头像 李华
网站建设 2026/8/1 10:54:47

Linux下Miniconda权限问题导致PyTorch安装失败的修复

Linux下Miniconda权限问题导致PyTorch安装失败的修复 在搭建AI开发环境时&#xff0c;你是否遇到过这样的场景&#xff1a;满怀期待地运行 conda install pytorch&#xff0c;结果终端却弹出一串红色错误&#xff1a; CondaError: Permission denied: /opt/miniconda3/pkgs或者…

作者头像 李华
网站建设 2026/8/2 0:38:31

Markdown生成技术文档:Miniconda环境信息一键导出

Miniconda环境信息一键导出&#xff1a;构建可复现的Python开发体系 在数据科学和人工智能项目中&#xff0c;你是否经历过这样的场景&#xff1f;——本地训练好的模型&#xff0c;换一台机器就跑不起来&#xff1b;团队成员各自安装依赖&#xff0c;结果“在我电脑上明明没问…

作者头像 李华
网站建设 2026/7/26 4:02:35

Miniconda-Python3.10镜像发布:轻量级AI开发环境一键部署

Miniconda-Python3.10镜像发布&#xff1a;轻量级AI开发环境一键部署 在高校实验室的深夜&#xff0c;研究生小李正为跑不通师兄留下的代码而焦头烂额——“明明装了同样的库&#xff0c;为什么我的环境报错&#xff1f;”这几乎是每个AI开发者都经历过的噩梦。随着项目增多&am…

作者头像 李华
网站建设 2026/7/31 13:14:23

Docker Run命令实战:运行Miniconda-Python3.10镜像进行大模型训练

Docker Run命令实战&#xff1a;运行Miniconda-Python3.10镜像进行大模型训练 在如今的大模型时代&#xff0c;一个常见的场景是&#xff1a;团队中某位成员在本地成功复现了某个LLM微调实验&#xff0c;信心满满地将代码推送到仓库。结果其他人拉下来一跑——“ImportError: c…

作者头像 李华