链接:
一、AXP2101部分信息介绍




AXP2101数据手册 部分内容:


根据提供的资料,我们可以知道CoreS3使用AXP2101电源管理芯片,并且屏幕背光可能由AXP2101的某个电源输出控制。从原理图片段中可以看到,屏幕背光(BL)由AXP2101的DLDO1(或DC1SW)输出控制。 另外,我们还可以看到AXP2101的I2C地址是0x34 ,并且内部I2C连接使用GPIO12和GPIO11(即I2C_SYS_SDA和I2C_SYS_SCL)。
因此,关闭屏幕背光的方法是通过I2C向AXP2101的相应寄存器写入数据,以关闭DLDO1(或调整其输出电压为0V,或者关闭其输出)。
从AXP2101的数据手册中,我们找到以下相关寄存器:
REG90H:LDOS ON/OFF控制0,其中第7位是DLDO1的使能位(0:禁用,1:启用)
REG99H:DLDO1电压设置,可以设置输出电压从0.5V到3.3V(具体设置值见寄存器描述)
因此,我们可以通过以下步骤关闭背光:
禁用DLDO1输出(将REG90H的第7位设为0)。
或者,将DLDO1的输出电压设置为0V(但注意,寄存器设置中最低电压为0.5V,所以不能设置为0V,因此最好采用禁用输出)。
另外,我们还需要确认DLDO1是否确实用于屏幕背光。从原理图片段中可以看到,屏幕背光(BL)连接到AXP2101的DLDO1/DC1SW引脚(第20脚)。因此,控制DLDO1的输出来关闭背光是正确的。
所以,程序步骤为:
初始化I2C,使用GPIO11(SCL)和GPIO12(SDA)。
向AXP2101的REG90H寄存器写入数据,将第7位设为0(禁用DLDO1),同时保持其他位不变。
注意:在修改寄存器时,我们最好先读取当前值,然后只修改需要改变的位,以免影响其他功能。
但是,根据数据手册,REG90H的默认值是由EFUSE决定的,我们不确定默认状态下DLDO1是否已经开启。因此,我们直接写入0x00到REG90H可能会关闭所有在REG90H中控制的LDO(包括ALDO1等)。所以,我们最好先读取REG90H的值,然后仅将第7位清零,再写回去。
但是,由于我们只关心背光,而且背光由DLDO1控制,所以我们可以直接关闭DLDO1。
另外,我们也可以考虑通过控制背光的亮度来关闭,但这里我们直接关闭电源。
下面给出完整的MicroPython代码:
基于您提供的M5Stack CoreS3硬件资料和AXP2101数据手册,我为您提供完整的关闭屏幕背光的MicroPython程序。
1、程序1:
python
import machine
import time
class AXP2101:
def __init__(self, i2c_bus=0, scl_pin=11, sda_pin=12, address=0x34):
"""初始化AXP2101电源管理芯片"""
self.i2c = machine.I2C(i2c_bus, scl=machine.Pin(scl_pin), sda=machine.Pin(sda_pin))
self.address = address
def write_register(self, register, value):
"""向AXP2101寄存器写入数据"""
data = bytearray([register, value])
try:
self.i2c.writeto(self.address, data)
return True
except:
return False
def read_register(self, register):
"""从AXP2101寄存器读取数据"""
try:
self.i2c.writeto(self.address, bytearray([register]))
result = self.i2c.readfrom(self.address, 1)
return result[0]
except:
return None
class CoreS3_Backlight:
def __init__(self):
"""初始化CoreS3背光控制"""
self.axp = AXP2101()
def turn_off_backlight(self):
"""关闭屏幕背光"""
# 方法1: 通过禁用DLDO1来关闭背光(推荐)
# REG90H第7位控制DLDO1使能 (0:禁用, 1:启用)
reg90_value = self.axp.read_register(0x90)
if reg90_value is not None:
# 清除第7位 (DLDO1 enable bit)
new_value = reg90_value & ~(1 << 7)
self.axp.write_register(0x90, new_value)
print("背光已关闭 (通过禁用DLDO1)")
return True
return False
def turn_on_backlight(self):
"""打开屏幕背光"""
# 启用DLDO1
reg90_value = self.axp.read_register(0x90)
if reg90_value is not None:
# 设置第7位 (DLDO1 enable bit)
new_value = reg90_value | (1 << 7)
self.axp.write_register(0x90, new_value)
print("背光已打开")
return True
return False
def set_backlight_voltage(self, voltage):
"""设置背光电压 (0.5V - 3.3V)
参数:
voltage: 电压值,范围0.5-3.3V
"""
if voltage < 0.5 or voltage > 3.3:
print("电压范围应在0.5V-3.3V之间")
return False
# 计算寄存器值: (voltage - 0.5) / 0.1
reg_value = int((voltage - 0.5) / 0.1)
if reg_value > 29: # 最大29 (0.5 + 29*0.1 = 3.4V)
reg_value = 29
# 写入DLDO1电压设置寄存器 (REG99H)
success = self.axp.write_register(0x99, reg_value)
if success:
print(f"背光电压设置为: {voltage}V")
return success
def get_backlight_status(self):
"""获取背光状态"""
reg90_value = self.axp.read_register(0x90)
if reg90_value is not None:
dldo1_enabled = (reg90_value >> 7) & 0x01
return dldo1_enabled == 1
return None
# 使用示例
def main():
# 创建背光控制对象
backlight = CoreS3_Backlight()
# 检查当前背光状态
status = backlight.get_backlight_status()
if status is not None:
print(f"当前背光状态: {'开启' if status else '关闭'}")
# 关闭背光
print("正在关闭背光...")
if backlight.turn_off_backlight():
print("背光关闭成功")
else:
print("背光关闭失败")
time.sleep(2)
# 重新打开背光
print("正在打开背光...")
if backlight.turn_on_backlight():
print("背光打开成功")
else:
print("背光打开失败")
# 可选:调整背光亮度(通过电压)
# backlight.set_backlight_voltage(2.8) # 设置背光电压为2.8V
# 直接运行测试
if __name__ == "__main__":
main()
2、程序2(简化版本):
python
#简化版本(仅关闭背光)
import machine
def turn_off_backlight():
"""关闭CoreS3屏幕背光"""
i2c = machine.I2C(0, scl=machine.Pin(11), sda=machine.Pin(12))
axp_address = 0x34
try:
# 读取REG90H当前值
i2c.writeto(axp_address, bytearray([0x90]))
current_value = i2c.readfrom(axp_address, 1)[0]
# 禁用DLDO1(清除第7位)
new_value = current_value & ~(1 << 7)
i2c.writeto(axp_address, bytearray([0x90, new_value]))
print("屏幕背光已关闭")
return True
except Exception as e:
print(f"关闭背光失败: {e}")
return False
def turn_on_backlight():
"""打开CoreS3屏幕背光"""
i2c = machine.I2C(0, scl=machine.Pin(11), sda=machine.Pin(12))
axp_address = 0x34
try:
# 读取REG90H当前值
i2c.writeto(axp_address, bytearray([0x90]))
current_value = i2c.readfrom(axp_address, 1)[0]
# 启用DLDO1(设置第7位)
new_value = current_value | (1 << 7)
i2c.writeto(axp_address, bytearray([0x90, new_value]))
print("屏幕背光已打开")
return True
except Exception as e:
print(f"打开背光失败: {e}")
return False
# 使用示例
turn_off_backlight() # 关闭背光
# turn_on_backlight() # 打开背光
二、背光系统架构
ESP32-S3 → I2C通信 → AXP2101电源芯片 → DLDO1输出 → 屏幕背光LED
1、详细实现原理
1.1 硬件连接关系
从硬件框图可以看出:
-
AXP2101 的DLDO1 输出连接到屏幕背光(BL)
-
DLDO1 同时也可作为DC1SW(开关功能)
-
背光控制通过I2C总线(GPIO11-SCL, GPIO12-SDA)实现
1.2 寄存器控制详解
python
# 您代码中的关键寄存器操作:
# REG90H - LDO开关控制寄存器
# 位布局:[DLDO1][CPUSLDO][BLDO2][ALDO1][ALDO4][ALDO3][ALDO2][ALDO1]
# 第7位 = DLDO1使能位 (1=开启, 0=关闭)
# 读取当前值
i2c.writeto(axp_address, bytearray([0x90]))
current_value = i2c.readfrom(axp_address, 1)[0]
# 关闭背光:清除第7位
new_value = current_value & ~(1 << 7)
# 打开背光:设置第7位
new_value = current_value | (1 << 7)
1.3 背光控制的本质
您的代码实际上是通过切断背光LED的供电来实现"关闭"的:
正常状态:AXP2101 DLDO1输出3.3V → 背光LED得电 → 屏幕亮起
关闭状态:AXP2101 DLDO1输出0V → 背光LED断电 → 屏幕变黑
完整的背光控制系统
让我展示一个更完整的实现,包括亮度调节:
python
import machine
class CoreS3_Backlight:
def __init__(self):
self.i2c = machine.I2C(0, scl=machine.Pin(11), sda=machine.Pin(12))
self.address = 0x34
def _read_reg(self, reg):
"""读取寄存器"""
self.i2c.writeto(self.address, bytearray([reg]))
return self.i2c.readfrom(self.address, 1)[0]
def _write_reg(self, reg, value):
"""写入寄存器"""
self.i2c.writeto(self.address, bytearray([reg, value]))
def get_backlight_status(self):
"""获取背光状态"""
reg90 = self._read_reg(0x90)
dldo1_enabled = (reg90 >> 7) & 0x01
return "开启" if dldo1_enabled else "关闭"
def turn_off_backlight(self):
"""关闭背光 - 切断电源"""
reg90 = self._read_reg(0x90)
# 清除第7位 (DLDO1使能位)
new_value = reg90 & ~(1 << 7)
self._write_reg(0x90, new_value)
print("背光已关闭 (电源切断)")
return True
def turn_on_backlight(self, brightness=100):
"""打开背光 - 恢复供电并可设置亮度"""
reg90 = self._read_reg(0x90)
# 设置第7位 (DLDO1使能位)
new_value = reg90 | (1 << 7)
self._write_reg(0x90, new_value)
# 可选:设置亮度
if brightness != 100:
self.set_brightness(brightness)
print(f"背光已开启 (亮度: {brightness}%)")
return True
def set_brightness(self, percent):
"""通过调节电压设置背光亮度"""
if percent < 0 or percent > 100:
return False
# 电压范围:0.5V (最暗) - 3.3V (最亮)
voltage = 0.5 + (3.3 - 0.5) * (percent / 100.0)
# 计算寄存器值:REG99H控制DLDO1输出电压
# 公式:(电压 - 0.5) / 0.1
reg_value = int((voltage - 0.5) / 0.1)
if reg_value > 28: # 最大28对应3.3V
reg_value = 28
self._write_reg(0x99, reg_value)
print(f"背光亮度设置为 {percent}% (电压: {voltage:.2f}V)")
return True
# 测试所有功能
backlight = CoreS3_Backlight()
print(f"当前背光状态: {backlight.get_backlight_status()}")
# 演示完整控制流程
backlight.turn_on_backlight(50) # 开启并设置50%亮度
backlight.set_brightness(30) # 调整为30%亮度
backlight.set_brightness(80) # 调整为80%亮度
backlight.turn_off_backlight() # 完全关闭背光
backlight.turn_on_backlight() # 重新开启(默认100%亮度)
三、背光控制的关键因素
1. 电源管理 (AXP2101芯片)
-
负责提供可调的直流电压给背光LED
-
DLDO1是专门用于背光供电的线性稳压器
2. 电压与亮度的关系
高电压 (3.3V) → 大电流 → 背光LED更亮
低电压 (0.5V) → 小电流 → 背光LED更暗
零电压 (0V) → 无电流 → 背光LED熄灭
3. I2C通信协议
-
设备地址:0x34
-
寄存器地址:0x90 (开关), 0x99 (电压/亮度)
-
通过读写寄存器位来控制硬件状态
4. 与屏幕本身的关系
-
此处代码控制的是背光,不是屏幕显示内容
-
即使背光关闭,屏幕像素数据仍然存在
-
重新打开背光后,之前显示的内容立即可见
5. 总结
您的代码实现背光控制的本质是:通过I2C控制AXP2101电源芯片的DLDO1输出,从而控制供给屏幕背光LED的电压/电流,实现亮度的调节和开关。
这种方式是硬件级的电源控制,比软件PWM调节更加节能和稳定。
注:下面是对部分代码的解释。
四、怎么向寄存器写入的?
python
def turn_off_backlight():
"""关闭CoreS3屏幕背光"""
i2c = I2C(0, scl=Pin(11), sda=Pin(12)) # 初始化I2C总线(相当于打开去银行的路)
axp_address = 0x34 # AXP芯片的"门牌号"
try:
# 1. 先告诉AXP芯片:我要操作0x90这个寄存器(去银行说要去3号窗口)
i2c.writeto(axp_address, bytearray([0x90]))
# 2. 读取0x90寄存器的当前值(柜员把3号窗口的当前单据给你)
current_value = i2c.readfrom(axp_address, 1)[0]
# 3. 只修改第7位(关闭DLDO1),其他位保持不变(改单据上的金额,其他信息不变)
new_value = current_value & ~(1 << 7)
# 4. 告诉AXP芯片:把0x90寄存器的值改成new_value(把改好的单据交给3号窗口)
i2c.writeto(axp_address, bytearray([0x90, new_value]))
return True
except Exception as e:
print(f"关闭背光失败: {e}")
return False
要理解这段代码中向 AXP 芯片寄存器写入数据的完整过程 ,需要结合I2C 总线的寄存器操作规范 和代码的逐行逻辑 来拆解。核心是:I2C 设备的寄存器写入需要先指定寄存器地址 ,再发送要写入的数据,且通常会先读取寄存器当前值做位操作(避免覆盖其他位的配置)。
下面先介绍I2C 寄存器写入的底层原理 ,再逐行解析代码中向 0x90 寄存器写入数据的具体步骤。
1、I2C 寄存器写入的底层原理
AXP 电源管理芯片(如 AXP192)是带寄存器的 I2C 从设备,对其寄存器写入数据需遵循 I2C 通信的 **"寄存器地址 + 数据"** 传输规则:
- 起始信号:I2C 主设备(如 ESP32/CoreS3)发送起始信号,启动通信。
- 发送设备地址 :主设备发送 AXP 芯片的 I2C 设备地址(
0x34),并指定写操作(I2C 设备地址的最低位为 0 表示写,1 表示读)。 - 发送寄存器地址 :从设备应答后,主设备发送要操作的寄存器地址(如
0x90),告诉从设备 "要写哪个寄存器"。 - 发送写入数据 :从设备再次应答后,主设备发送要写入寄存器的具体数据(如代码中的
new_value)。 - 停止信号:主设备发送停止信号,完成一次寄存器写入。
如果需要写入多个字节 (如连续寄存器),步骤 4 可连续发送数据;而代码中是单字节写入,只需发送 1 个数据字节。
2、代码中寄存器写入的逐行解析
代码功能是关闭 CoreS3 屏幕背光 ,核心是通过修改 AXP 芯片的 0x90 寄存器,清除第 7 位(禁用 DLDO1 电源通道,该通道为屏幕背光供电)。下面分模块解析写入过程:
2.1 初始化 I2C 总线和设备地址
i2c = I2C(0, scl=Pin(11), sda=Pin(12))
axp_address = 0x34
I2C(0, scl=Pin(11), sda=Pin(12)):初始化 CoreS3 的 I2C 0 总线,指定 SCL(时钟线)为引脚 11,SDA(数据线)为引脚 12,这是 CoreS3 与 AXP 芯片的硬件连接引脚。axp_address = 0x34:AXP192 芯片的默认 I2C 从设备地址(二进制为0110100,最低位为读写位,由 I2C 方法自动处理)。
2.2 先读取 0x90 寄存器的当前值(关键:避免覆盖其他位)
# 发送寄存器地址0x90,告诉AXP芯片要读这个寄存器
i2c.writeto(axp_address, bytearray([0x90]))
# 从AXP芯片读取1个字节的寄存器值,并取第一个元素(因为readfrom返回字节数组)
current_value = i2c.readfrom(axp_address, 1)[0]
- 为什么要先读? :
0x90寄存器的 8 个位分别控制不同的电源通道(如 DLDO1、DLDO2、DLDO3 等),直接写入固定值会覆盖其他位的配置(比如其他电源通道可能正在使用)。因此先读取当前值,仅修改需要的位(第 7 位),保留其他位的原始配置。 i2c.writeto(axp_address, bytearray([0x90])):向 AXP 芯片发送寄存器地址0x90,为后续读取做准备(I2C 读寄存器的前置步骤是先写寄存器地址)。i2c.readfrom(axp_address, 1):从 AXP 芯片的0x90寄存器读取 1 个字节,返回值是字节数组(如bytearray(b'\x80')),[0]取数组的第一个元素(十进制数值,如 128)。
2.3. 计算要写入的新值(修改第 7 位,保留其他位)
new_value = current_value & ~(1 << 7)
这是位操作的核心 ,目的是清除第 7 位(置 0),其他位保持不变:
1 << 7:将数字 1 左移 7 位,得到二进制10000000(十进制 128,十六进制0x80),这是第 7 位的掩码。~(1 << 7):对掩码取反(Python 中按位取反会得到负数,实际参与运算时按 8 位处理),8 位结果为01111111,表示 "只保留第 0~6 位,第 7 位清零"。current_value & ~(1 << 7):按位与运算,将current_value的第 7 位强制置 0,其他位保持与原始值一致。
举例 :如果 current_value 是 0x81(二进制 10000001),运算后 new_value 为 0x01(二进制 00000001),仅第 7 位被清除,其他位不变。
2.4 向 0x90 寄存器写入新值(核心写入步骤)
i2c.writeto(axp_address, bytearray([0x90, new_value]))
这行代码是真正的寄存器写入操作 ,也是整个流程的关键。bytearray([0x90, new_value]) 创建了一个包含两个字节的字节数组:
- 第一个字节
0x90:要操作的寄存器地址(告诉 AXP 芯片 "写这个寄存器")。 - 第二个字节
new_value:要写入寄存器的具体数据(修改后的位配置)。
i2c.writeto 方法会将这个两字节的数组通过 I2C 总线连续发送给 AXP 芯片,底层执行的是前文提到的I2C 寄存器写入 5 步流程:
- 主设备发送起始信号。
- 发送 AXP 设备地址
0x34+ 写位(0),从设备应答。 - 发送寄存器地址
0x90,从设备应答。 - 发送新值
new_value,从设备应答。 - 发送停止信号,写入完成。
此时 AXP 芯片的 0x90 寄存器值被更新为 new_value,第 7 位被置 0,DLDO1 电源通道被禁用(屏幕背光失去供电,从而关闭)。
2.5 异常处理
except Exception as e:
print(f"关闭背光失败: {e}")
return False
捕获 I2C 通信中的异常(如设备未响应、引脚错误、总线占用等),避免程序崩溃,并返回失败状态。
五、补充说明:为什么是第 7 位控制背光?
CoreS3 的屏幕背光由 AXP 芯片的 DLDO1 电源通道供电,而 AXP192 芯片的 0x90 寄存器是DLDO1~DLDO3 输出使能寄存器,各位定义如下(参考 AXP192 数据手册):
| 位序号 | 位名称 | 功能描述 |
|---|---|---|
| 7 | DLDO1_EN | DLDO1 输出使能(1 = 开启,0 = 关闭) |
| 6 | DLDO2_EN | DLDO2 输出使能(1 = 开启,0 = 关闭) |
| 5 | DLDO3_EN | DLDO3 输出使能(1 = 开启,0 = 关闭) |
| 0~4 | 保留 | 无功能,置 0 即可 |
因此,清除第 7 位(DLDO1_EN=0)会关闭 DLDO1 输出,背光失去供电后熄灭;若要开启背光,只需将第 7 位置 1(new_value = current_value | (1 << 7)),再写入寄存器即可。
六、完整的 "读 - 改 - 写" 流程总结
代码中向寄存器写入数据的核心是 **"先读再改后写"**,这是嵌入式开发中修改寄存器特定位的标准流程,步骤如下:
- 读:读取寄存器当前值,保留未修改位的原始配置。
- 改:通过位运算修改目标位(其他位不变),计算新值。
- 写:将新值写入寄存器,完成配置更新。
这种方式避免了直接写入固定值导致的其他功能异常,是操作硬件寄存器的最佳实践。
对于完全没接触过嵌入式 I2C 通信和寄存器操作的你来说,核心困惑其实是三个关键元素的角色关系 和 **"先写再读" 的底层逻辑 **。接下来我会用生活化的比喻 +分层拆解的方式,从基础概念到实际流程彻底讲清楚,同时扩展嵌入式硬件操作的核心知识。
七、理解芯片I2C地址、寄存器地址、写入值之间的关系
把整个 I2C 通信场景比作你去银行办理业务 ,这样能瞬间理清0x34、0x90、"写入值" 的角色:
| 硬件中的元素 | 银行场景的对应角色 / 操作 | 作用说明 |
|---|---|---|
0x34(AXP 芯片的 I2C 地址) |
银行的总行地址(比如 "北京市朝阳区 XX 路 XX 号工商银行") | 总线上有多个设备(如 AXP 电源芯片、屏幕、传感器),0x34是 AXP 芯片的 "唯一门牌号",主设备(CoreS3)通过这个地址找到它,不会和其他设备混淆 |
0x90(寄存器地址) |
银行里的某个业务窗口(比如 "3 号窗口:水电费缴纳窗口") | AXP 芯片内部有几十 / 上百个寄存器 (可以理解为 "功能配置抽屉"),每个寄存器有唯一地址,0x90是其中一个抽屉的编号,专门负责DLDO1 电源通道的开关(控制屏幕背光) |
写入值(如new_value) |
你交给窗口的业务单据(比如 "缴纳 100 元水电费") | 寄存器是用来存储 "配置指令" 的,写入值就是具体的配置内容(比如0x00表示关闭 DLDO1,0x80表示开启 DLDO1),写入后芯片就会按这个值执行对应的功能 |
一句话总结三者关系 :0x34是找到 AXP 芯片的 "门牌号",0x90是找到芯片内部控制背光的 "配置抽屉",写入值是往这个抽屉里放的 "具体开关指令"。
八、为什么要 "先写再读"?拆解 I2C 寄存器的读写逻辑
为什么 "先调用writeto再调用readfrom"?这是I2C 协议对 "寄存器读取" 的强制规定 ,核心原因是:主设备(CoreS3)必须先告诉从设备(AXP 芯片)"你要读哪个抽屉的东西",从设备才知道给你返回什么数据。
继续用银行比喻理解这个流程:
- 先写(
i2c.writeto(axp_address, bytearray([0x90]))) :你走到银行(0x34),对大堂经理说 "我要去 3 号窗口(0x90)办业务"------ 这一步是 **"告诉对方你要操作的目标"**,没有这一步,银行不知道你要哪个窗口的信息。 - 再读(
i2c.readfrom(axp_address, 1)):3 号窗口的柜员听到你的请求后,把 3 号窗口的单据(寄存器里的当前值)递给你 ------ 这一步是 **"获取目标抽屉里的当前内容"**。
如果是寄存器写入 (比如代码中i2c.writeto(axp_address, bytearray([0x90, new_value]))),则是 "一次说清目标 + 指令":你走到银行(0x34),对大堂经理说 "我要去 3 号窗口(0x90),缴纳 100 元水电费(new_value)",柜员直接按你的指令执行,无需再单独读。
更底层的 I2C 协议流程:读 / 写寄存器的实际信号传输
用时序图的文字版,拆解 "读寄存器" 和 "写寄存器" 的完整信号流程,你会更清楚 "先写" 的必要性:
1. 读取0x90寄存器的完整 I2C 信号流程
主设备(CoreS3)→ 发送起始信号(I2C通信的"开始铃")
主设备 → 发送0x34(AXP地址)+ 写位(0)→ 从设备(AXP)应答(表示"我在")
主设备 → 发送0x90(寄存器地址)→ 从设备应答(表示"我知道你要读这个寄存器了")
主设备 → 发送重复起始信号(重新发起通信)
主设备 → 发送0x34(AXP地址)+ 读位(1)→ 从设备应答
从设备 → 发送`0x90`寄存器的当前值 → 主设备应答
主设备 → 发送停止信号(I2C通信的"结束铃")
代码中的writeto对应前 4 步 (告诉从设备目标寄存器),readfrom对应后 3 步(读取寄存器值),只是 MicroPython 的i2c对象把底层的 "起始信号、应答、停止信号" 都封装好了,你只需要调用方法。
2. 写入0x90寄存器的完整 I2C 信号流程
主设备 → 发送起始信号
主设备 → 发送0x34+写位 → 从设备应答
主设备 → 发送0x90(寄存器地址)→ 从设备应答
主设备 → 发送new_value(写入值)→ 从设备应答
主设备 → 发送停止信号
代码中i2c.writeto(axp_address, bytearray([0x90, new_value]))就是一次性执行了所有步骤,因为字节数组里包含了 "寄存器地址 + 写入值",从设备能一次性接收并执行。
九、扩展:寄存器与 I2C 通信的本质
理解了上述流程后,再扩展几个核心概念,能帮你打通嵌入式硬件操作的任督二脉:
1. 寄存器是什么?为什么硬件都用寄存器配置?
- 寄存器是芯片内部的一小块临时存储单元(类似电脑的内存,但容量极小,通常按字节 / 位划分),是 "软件控制硬件的唯一入口"。
- 硬件的所有功能(比如开关背光、调节电压、控制充电),本质上都是通过修改寄存器的数值 来实现的:芯片会实时读取寄存器的值,按值执行对应的硬件动作(比如
0x90寄存器第 7 位为 0,就关闭 DLDO1 电源通道,背光熄灭)。 - 寄存器的位数:常见的是 8 位寄存器(1 字节),所以每个寄存器的值范围是
0x00(0)~0xFF(255),每一位(bit0~bit7)都可以对应一个独立的小功能(比如0x90寄存器的 bit7 控制 DLDO1,bit6 控制 DLDO2)。
2. I2C 总线的核心特点:多设备共享总线
- I2C 总线只有两根线:SCL(时钟线,主设备控制节奏)、SDA(数据线,传输数据),总线上可以挂多个从设备(比如 AXP 芯片
0x34、OLED 屏幕0x3C、温湿度传感器0x48)。 - 每个从设备必须有唯一的 I2C 地址,否则主设备发送数据时会出现 "地址冲突"(比如两个设备都响应
0x34,主设备不知道该和谁通信)。 - I2C 地址的格式:7 位地址(最常见)+1 位读写位(0 = 写,1 = 读),所以代码中写
0x34是 7 位地址,实际发送时会自动拼接读写位(变成 8 位:写是0x68,读是0x69),MicroPython 的i2c对象会自动处理这一步,你无需关心。
3. "读 - 改 - 写":操作寄存器的黄金法则
代码中先读寄存器当前值、再修改目标位、最后写入新值,是嵌入式开发中操作寄存器的标准流程,原因是:
- 一个寄存器的 8 位可能对应 8 个不同的功能(比如
0x90寄存器的 bit7 控制 DLDO1,bit6 控制 DLDO2,bit5 控制 DLDO3)。 - 如果直接写入一个固定值(比如
0x00),会把其他位的配置也覆盖掉(比如原本 DLDO2 是开启的,写入0x00后 DLDO2 也被关闭了,导致其他硬件异常)。 - 位运算 是修改寄存器特定位的核心手段:
- 清除某一位:
current_value & ~(1 << n)(n 是位序号) - 置 1 某一位:
current_value | (1 << n) - 翻转某一位:
current_value ^ (1 << n)
- 清除某一位:
4. 如何查寄存器的功能?
所有寄存器的地址、每一位的功能、写入值的含义,都在芯片的 ** 数据手册(Datasheet)** 里,比如:
- AXP192 的
0x90寄存器是DLDO1/DLDO2/DLDO3 输出使能寄存器,手册里会明确标注每一位的功能、默认值、可配置范围。 - 开发嵌入式硬件的核心能力之一,就是能看懂数据手册的寄存器章节,并通过代码实现配置。
十、加深理解的小实验
你可以在代码中加几个打印语句,直观看到寄存器的读写过程:
python
def turn_off_backlight():
i2c = I2C(0, scl=Pin(11), sda=Pin(12))
axp_address = 0x34
try:
i2c.writeto(axp_address, bytearray([0x90]))
current_value = i2c.readfrom(axp_address, 1)[0]
print(f"0x90寄存器当前值(十进制):{current_value},十六进制:{hex(current_value)}")
new_value = current_value & ~(1 << 7)
print(f"修改后的新值(十进制):{new_value},十六进制:{hex(new_value)}")
i2c.writeto(axp_address, bytearray([0x90, new_value]))
# 再次读取,验证是否修改成功
i2c.writeto(axp_address, bytearray([0x90]))
check_value = i2c.readfrom(axp_address, 1)[0]
print(f"修改后寄存器的值:{hex(check_value)}")
return True
except Exception as e:
print(f"关闭背光失败: {e}")
return False
turn_off_backlight()
运行后你会看到:
- 原始值可能是
0x80(二进制10000000,DLDO1 开启); - 新值是
0x00(二进制00000000,DLDO1 关闭); - 再次读取的结果是
0x00,说明修改成功,背光也会随之熄灭。
总结
- 三者关系 :
0x34是芯片的 I2C 地址(门牌号),0x90是芯片内部的寄存器地址(功能抽屉),写入值是配置指令(抽屉里的开关); - 先写再读:是 I2C 协议的规定,主设备必须先告诉从设备 "操作哪个寄存器",才能读 / 写数据;
- 核心思想:嵌入式硬件控制的本质是 "通过 I2C/SPI 等总线,修改芯片寄存器的数值",而 "读 - 改 - 写" 是操作寄存器的标准方法。
十一、留言
如有问题,敬请指正。
如果对上述还不能理解,下面是更详细的解释(主要关于寄存器位、地址冲突与 I2C 多设备问题 )。
一、0x90 寄存器:不同位对应不同功能,写值就是 "拨开关"
先拿你最熟悉的 "家里的电灯开关面板" 类比:0x90 寄存器就像一个8 位的开关面板(因为寄存器是 1 字节 = 8 位,bit0~bit7),每个开关(位)控制一个独立的小功能,比如 "开关客厅灯""开关卧室灯""开关阳台灯"------ 这就是 "不同位代表不同功能" 的本质。
- 0x90 寄存器的 "开关面板" 实例(来自 AXP2101 文档)
从AXP2101 文档可知,0x90 是 "LDOS ON/OFF control(LDO 电源开关控制寄存器)",它的 8 位对应不同 LDO 电源通道的开关(🔶1-355、🔶1-692),具体如下:
| 寄存器位(bit) | 对应功能("开关" 用途) | 1 和 0 的含义 |
|---|---|---|
| bit7 | DLDO1 电源通道开关(控制屏幕背光) | 1 = 开启背光,0 = 关闭背光 |
| bit6 | DLDO2 电源通道开关 | 1 = 开启 DLDO2,0 = 关闭 DLDO2 |
| bit5 | BLDO1 电源通道开关 | 1 = 开启 BLDO1,0 = 关闭 BLDO1 |
| bit4 | BLDO2 电源通道开关 | 1 = 开启 BLDO2,0 = 关闭 BLDO2 |
| bit3 | ALDO4 电源通道开关 | 1 = 开启 ALDO4,0 = 关闭 ALDO4 |
| bit2 | ALDO3 电源通道开关 | 1 = 开启 ALDO3,0 = 关闭 ALDO3 |
| bit1 | ALDO2 电源通道开关 | 1 = 开启 ALDO2,0 = 关闭 ALDO2 |
| bit0 | ALDO1 电源通道开关 | 1 = 开启 ALDO1,0 = 关闭 ALDO1 |
比如上述的 "关闭背光" 代码,就是把 "bit7 这个开关" 从 1 拨到 0,其他开关保持不变 ------ 这就是 "写值给对应位写 1 或 0" 的实际操作。
- 写值会改变 0x90 的 "数值",但不会改变 "寄存器地址"
-
0x90 是 "寄存器的地址":就像你家的 "门牌号(比如 101 室)",是固定不变的,不管你家里面住的人是谁、家具怎么换,门牌号永远是 101。
-
写值改变的是 "寄存器里存的数" :比如 0x90 寄存器原本存的是
0x81(二进制10000001,表示 bit7 和 bit0 为 1,即背光开启、ALDO1 开启),写新值0x01(二进制00000001)后,寄存器里的数变了,但 "地址还是 0x90",不会和其他寄存器(比如 0x91、0x62)的地址重复。
举个具体例子:
-
0x90 寄存器地址→固定为 0x90(门牌号不变);
-
写入前值:0x81(bit7=1,背光开);
-
写入后值:0x01(bit7=0,背光关);
-
地址始终是 0x90,永远不会变成 0x91 或其他地址,自然不会和别的寄存器地址冲突。
二、两个相同模块的 I2C 地址:可能一样,怎样区分和通信
"两个同样的模块"(比如两个 AXP2101 芯片,或两个相同的传感器),它们的默认 I2C 地址通常是一样的(比如 AXP2101 默认 I2C 地址是 0x34,🔶1-193)。这就像 "两户人家门牌号都是 101",快递员(主设备,比如 CoreS3)没法区分 ------ 这时候需要用 "硬件改地址" 或 "软件切换" 的方法解决。
1. 为什么相同模块默认地址会一样?
芯片厂商为了方便生产,会给同型号芯片设定一个 "默认 I2C 地址"(比如所有 AXP2101 默认都是 0x34),这样用户拿到手不用改地址就能直接用。但如果要在同一根 I2C 总线上接多个相同模块,就必须让它们的地址不一样 ------ 因为 I2C 总线规定:总线上的每个从设备(模块)必须有唯一的地址。
2. 区分相同模块的两种核心方法(新手能上手)
方法 1:硬件改地址(最常用,靠模块上的 "跳帽 / 焊盘")
很多模块(比如传感器、电源芯片)会预留 "地址选择引脚"(通常标为 A0、A1、A2),通过 "接高电平(VCC)、低电平(GND)" 或 "焊接 / 断开焊盘" 来改变地址。举个例子(假设模块有 A0 引脚):
-
当 A0 接 GND 时,模块地址 = 默认地址(比如 0x34);
-
当 A0 接 VCC 时,模块地址 = 默认地址 + 1(比如 0x35);
-
这样两个相同模块,一个 A0 接 GND(0x34),一个 A0 接 VCC(0x35),地址就不一样了,主设备能分别通信。
方法 2:软件切换(靠芯片的 "地址配置寄存器")
有些芯片(比如 AXP2101)支持通过 "寄存器" 修改 I2C 地址 ------ 简单说就是:先通过默认地址和第一个模块通信,给它的 "地址配置寄存器" 写一个新地址(比如 0x35);再用默认地址和第二个模块通信,给它写另一个新地址(比如 0x36);之后就能用新地址分别控制它们。
3. 实际通信例子:两个 AXP2101 模块怎么区分
假设你有两个 AXP2101 模块,要在同一根 I2C 总线上控制它们的背光(都用 0x90 寄存器),步骤如下:
-
硬件改地址:
-
模块 1:A0 接 GND,地址 = 0x34;
-
模块 2:A0 接 VCC,地址 = 0x35;
-
-
主设备分别通信:
from machine import I2C, Pin # 初始化I2C总线 i2c = I2C(0, scl=Pin(11), sda=Pin(12)) # 两个模块的地址(改后不一样) axp1_address = 0x34 # 模块1地址 axp2_address = 0x35 # 模块2地址 backlight_reg = 0x90 # 背光控制寄存器地址 # 1. 控制模块1:关闭背光(bit7置0) # 读模块1的0x90寄存器当前值 i2c.writeto(axp1_address, bytearray([backlight_reg])) axp1_current = i2c.readfrom(axp1_address, 1)[0] # 改值:bit7置0 axp1_new = axp1_current & ~(1 << 7) # 写回模块1的0x90寄存器 i2c.writeto(axp1_address, bytearray([backlight_reg, axp1_new])) print("模块1背光已关闭") # 2. 控制模块2:开启背光(bit7置1) # 读模块2的0x90寄存器当前值 i2c.writeto(axp2_address, bytearray([backlight_reg])) axp2_current = i2c.readfrom(axp2_address, 1)[0] # 改值:bit7置1 axp2_new = axp2_current | (1 << 7) # 写回模块2的0x90寄存器 i2c.writeto(axp2_address, bytearray([backlight_reg, axp2_new])) print("模块2背光已开启")这样主设备通过 "不同的 I2C 地址(0x34 和 0x35)",就能准确找到两个相同模块,分别控制它们的功能,不会混淆。
三、总结:3 个核心知识点,新手不用记,理解就行
-
寄存器的 "位"= 独立开关:0x90 这样的 8 位寄存器,就像 8 个独立开关,每个开关控制一个小功能,写值就是 "拨某个开关的 1/0",不影响其他开关。
-
地址永远不变:寄存器地址(比如 0x90)是固定的 "门牌号",写值只改 "里面存的数",地址不会变,不会和其他寄存器冲突。
-
相同模块分地址:默认地址一样时,要么用硬件改引脚(接 GND/VCC),要么用软件改寄存器,让它们的地址不一样,主设备就能分别通信。
其实硬件操作的逻辑和生活中的 "分类、标识" 特别像 ------ 就像你给两个相同的杯子贴不同的标签,就能分清哪个装水、哪个装茶一样。如果还有哪个点没明白,随时问,咱们再用更简单的例子拆!
下面是关于I2C地址的组成相关的内容
用 "快递填单" 讲懂 I2C 地址:7 位地址 + 1 位读写位怎么变
别担心 "不懂",咱们用 "寄快递" 的生活场景类比,把 "7 位地址 + 1 位读写位" 拆成 "地址栏 + 操作栏",再用数字一步步算,保证你能看明白 "0x34 怎么变成 0x68 和 0x69"。
一、先明确:I2C 地址是 "8 位数据包",分两部分
你可以把 I2C 主设备(比如 CoreS3)发给从设备(比如 AXP2101)的 "地址信号",理解成一张 "快递单",这张单子共 8 个 "格子"(对应 8 位二进制),分成两部分:
| 快递单格子(二进制位) | 第 7 位 | 第 6 位 | 第 5 位 | 第 4 位 | 第 3 位 | 第 2 位 | 第 1 位 | 第 0 位 |
|---|---|---|---|---|---|---|---|---|
| 对应功能 | 7 位地址的最高位 | 7 位地址的第 6 位 | 7 位地址的第 5 位 | 7 位地址的第 4 位 | 7 位地址的第 3 位 | 7 位地址的第 2 位 | 7 位地址的第 1 位 | 1 位读写位(0 = 写,1 = 读) |
简单说:8 位信号 = 7 位固定地址(找设备) + 1 位操作指令(读 / 写) 代码里写的0x34,是 "7 位固定地址";实际通信时,主设备会自动在 "最后 1 位" 加个 0 或 1,凑成 8 位信号 ------ 这一步不用你手动算,MicroPython 的i2c对象会自动处理。
二、关键步骤:0x34(7 位地址)怎么变成 0x68(写)和 0x69(读)?
先把0x34转换成二进制(这是 7 位地址的关键,一定要看二进制!):
- 0x34(十六进制) = 52(十进制) = 0110100(二进制,共 7 位)
接下来,按 "7 位地址 + 1 位读写位" 的规则拼接成 8 位:
1. 写操作:最后 1 位加 0(0 = 写)
-
7 位地址(0110100) + 1 位写位(0) = 8 位二进制 01101000
-
再把 8 位二进制转成十六进制:01101000 = 104(十进制) = 0x68→ 所以 "写操作时,实际发送的 8 位地址是 0x68"
2. 读操作:最后 1 位加 1(1 = 读)
-
7 位地址(0110100) + 1 位读位(1) = 8 位二进制 01101001
-
再把 8 位二进制转成十六进制:01101001 = 105(十进制) = 0x69→ 所以 "读操作时,实际发送的 8 位地址是 0x69"
- 实际是 "在 7 位地址后面加 1 位读写位",相当于 "地址不变,只加操作指令"------ 比如 0x34(7 位)加 0 变成 0x68(8 位),加 1 变成 0x69(8 位),7 位地址本身还是 0x34,没有变。
三、举个更直观的例子:把 7 位地址0x34比作 "你家的门牌号:101 室"(固定不变);
-
写操作 ="给 101 室送快递"(操作指令:送),快递单上写 "101 - 送"(对应 8 位地址 0x68);
-
读操作 ="从 101 室取快递"(操作指令:取),快递单上写 "101 - 取"(对应 8 位地址 0x69);
-
不管是 "送" 还是 "取",你家的门牌号 "101 室"(7 位地址 0x34)永远没变,变的只是 "操作指令"(最后 1 位)。
四、代码里为什么只写 0x34,不用管 0x68 和 0x69?
因为 MicroPython 的i2c模块已经帮你做了 "拼接读写位" 的工作 ------ 你只需要告诉它 "找哪个设备(0x34)" 和 "做什么操作(读 / 写)",它会自动生成对应的 8 位地址:
-
当你调用
i2c.writeto(axp_address, ...)(写操作)时,i2c模块会自动把 0x34(7 位)+0(写位)拼成 0x68(8 位),发给 AXP2101; -
当你调用
i2c.readfrom(axp_address, ...)(读操作)时,i2c模块会自动把 0x34(7 位)+1(读位)拼成 0x69(8 位),发给 AXP2101; -
你不用手动算 0x68 和 0x69,只需要记住 "设备的 7 位地址是 0x34" 就行。
五、总结:3 句话说透核心逻辑
-
0x34是 7 位 "设备门牌号",固定不变,用来找到 AXP2101 芯片; -
写操作时,主设备自动给
0x34(7 位)加 1 个 0,凑成 8 位的 0x68,告诉芯片 "我要给你发数据"; -
读操作时,主设备自动给
0x34(7 位)加 1 个 1,凑成 8 位的 0x69,告诉芯片 "我要从你这拿数据"; -
你在代码里只需要写
0x34,剩下的 "加 0 加 1" 都由i2c模块自动完成,不用你管。