CoreS3 屏幕背光

链接:

1、M5Stack CoreS3 官方资料

2、AXP2101数据手册

一、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(具体设置值见寄存器描述)

因此,我们可以通过以下步骤关闭背光:

  1. 禁用DLDO1输出(将REG90H的第7位设为0)。

  2. 或者,将DLDO1的输出电压设置为0V(但注意,寄存器设置中最低电压为0.5V,所以不能设置为0V,因此最好采用禁用输出)。

另外,我们还需要确认DLDO1是否确实用于屏幕背光。从原理图片段中可以看到,屏幕背光(BL)连接到AXP2101的DLDO1/DC1SW引脚(第20脚)。因此,控制DLDO1的输出来关闭背光是正确的。

所以,程序步骤为:

  1. 初始化I2C,使用GPIO11(SCL)和GPIO12(SDA)。

  2. 向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 硬件连接关系

从硬件框图可以看出:

  • AXP2101DLDO1 输出连接到屏幕背光(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 通信的 **"寄存器地址 + 数据"** 传输规则:

  1. 起始信号:I2C 主设备(如 ESP32/CoreS3)发送起始信号,启动通信。
  2. 发送设备地址 :主设备发送 AXP 芯片的 I2C 设备地址(0x34),并指定写操作(I2C 设备地址的最低位为 0 表示写,1 表示读)。
  3. 发送寄存器地址 :从设备应答后,主设备发送要操作的寄存器地址(如 0x90),告诉从设备 "要写哪个寄存器"。
  4. 发送写入数据 :从设备再次应答后,主设备发送要写入寄存器的具体数据(如代码中的 new_value)。
  5. 停止信号:主设备发送停止信号,完成一次寄存器写入。

如果需要写入多个字节 (如连续寄存器),步骤 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_value0x81(二进制 10000001),运算后 new_value0x01(二进制 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 步流程

  1. 主设备发送起始信号。
  2. 发送 AXP 设备地址 0x34 + 写位(0),从设备应答。
  3. 发送寄存器地址 0x90,从设备应答。
  4. 发送新值 new_value,从设备应答。
  5. 发送停止信号,写入完成。

此时 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)),再写入寄存器即可。

六、完整的 "读 - 改 - 写" 流程总结

代码中向寄存器写入数据的核心是 **"先读再改后写"**,这是嵌入式开发中修改寄存器特定位的标准流程,步骤如下:

  1. :读取寄存器当前值,保留未修改位的原始配置。
  2. :通过位运算修改目标位(其他位不变),计算新值。
  3. :将新值写入寄存器,完成配置更新。

这种方式避免了直接写入固定值导致的其他功能异常,是操作硬件寄存器的最佳实践。

对于完全没接触过嵌入式 I2C 通信和寄存器操作的你来说,核心困惑其实是三个关键元素的角色关系 和 **"先写再读" 的底层逻辑 **。接下来我会用生活化的比喻 +分层拆解的方式,从基础概念到实际流程彻底讲清楚,同时扩展嵌入式硬件操作的核心知识。

七、理解芯片I2C地址、寄存器地址、写入值之间的关系

把整个 I2C 通信场景比作你去银行办理业务 ,这样能瞬间理清0x340x90、"写入值" 的角色:

硬件中的元素 银行场景的对应角色 / 操作 作用说明
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 芯片)"你要读哪个抽屉的东西",从设备才知道给你返回什么数据

继续用银行比喻理解这个流程:

  1. 先写(i2c.writeto(axp_address, bytearray([0x90])) :你走到银行(0x34),对大堂经理说 "我要去 3 号窗口(0x90)办业务"------ 这一步是 **"告诉对方你要操作的目标"**,没有这一步,银行不知道你要哪个窗口的信息。
  2. 再读(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,说明修改成功,背光也会随之熄灭。

总结

  1. 三者关系0x34是芯片的 I2C 地址(门牌号),0x90是芯片内部的寄存器地址(功能抽屉),写入值是配置指令(抽屉里的开关);
  2. 先写再读:是 I2C 协议的规定,主设备必须先告诉从设备 "操作哪个寄存器",才能读 / 写数据;
  3. 核心思想:嵌入式硬件控制的本质是 "通过 I2C/SPI 等总线,修改芯片寄存器的数值",而 "读 - 改 - 写" 是操作寄存器的标准方法。

十一、留言

如有问题,敬请指正。


如果对上述还不能理解,下面是更详细的解释(主要关于寄存器位、地址冲突与 I2C 多设备问题 )。

一、0x90 寄存器:不同位对应不同功能,写值就是 "拨开关"

先拿你最熟悉的 "家里的电灯开关面板" 类比:0x90 寄存器就像一个8 位的开关面板(因为寄存器是 1 字节 = 8 位,bit0~bit7),每个开关(位)控制一个独立的小功能,比如 "开关客厅灯""开关卧室灯""开关阳台灯"------ 这就是 "不同位代表不同功能" 的本质。

  1. 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" 的实际操作。

  1. 写值会改变 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. 硬件改地址

    • 模块 1:A0 接 GND,地址 = 0x34;

    • 模块 2:A0 接 VCC,地址 = 0x35;

  2. 主设备分别通信

    复制代码
    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 个核心知识点,新手不用记,理解就行

  1. 寄存器的 "位"= 独立开关:0x90 这样的 8 位寄存器,就像 8 个独立开关,每个开关控制一个小功能,写值就是 "拨某个开关的 1/0",不影响其他开关。

  2. 地址永远不变:寄存器地址(比如 0x90)是固定的 "门牌号",写值只改 "里面存的数",地址不会变,不会和其他寄存器冲突。

  3. 相同模块分地址:默认地址一样时,要么用硬件改引脚(接 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 句话说透核心逻辑

  1. 0x34是 7 位 "设备门牌号",固定不变,用来找到 AXP2101 芯片;

  2. 写操作时,主设备自动给0x34(7 位)加 1 个 0,凑成 8 位的 0x68,告诉芯片 "我要给你发数据";

  3. 读操作时,主设备自动给0x34(7 位)加 1 个 1,凑成 8 位的 0x69,告诉芯片 "我要从你这拿数据";

  4. 你在代码里只需要写0x34,剩下的 "加 0 加 1" 都由i2c模块自动完成,不用你管。

相关推荐
星期天22 小时前
1.4光敏传感器控制蜂鸣器
stm32·单片机·嵌入式硬件·江科大
沐欣工作室_lvyiyi3 小时前
基于无线互联的电源健康监测与控制系统设计(论文+源码)
stm32·单片机·毕业设计·电源健康监测
@good_good_study4 小时前
STM32 ADC单通道采样函数及实验
stm32·单片机
田甲4 小时前
【STM32】基于TPS61165芯片的LED驱动电路
stm32·单片机·嵌入式硬件
d111111111d4 小时前
STM32得中断服务函数,为什么不能有返回值
笔记·stm32·单片机·嵌入式硬件·学习
CS Beginner4 小时前
【单片机】GPIO位结构图解析
单片机·嵌入式硬件
时光の尘4 小时前
嵌入式面试八股文(十九)·裸机开发与RTOS开发的区别
linux·stm32·单片机·iic·rtos·spi
袖手蹲4 小时前
Arduino UNO Q 烘托圣诞节气氛
人工智能·单片机·嵌入式硬件
电子工程师-C514 小时前
基于51单片机的64位流水灯
单片机·嵌入式硬件·51单片机