网上可查到PCF8575 micropython驱动,但PCF8575没有带LED灯的积木,而9555有带16个LED灯的积木,价格10至11元,没发现MPY驱动,I2C总线,找了一天的资料学习,可以用I2C的内存操作指令i2c.writeto_mem直接测试了,如果控制需求简单,就不需要写驱动,以下供参考。

# I2C总线直接写读PCA9555/TCA9555
from machine import Pin,I2C
import time
# 定义寄存器地址
INPUT_PORT_REG = 0x0
OUTPUT_PORT_REG = 0x2
POLARITY_INVERSION_REG = 0x4
CONFIG_REG = 0x6
 
# I2C构造函数 ESP32引脚可变
i2c=I2C(1,freq=100_000,scl=23,sda=22)
print('I2C ADDRESS',hex(i2c.scan()[0]))
 
# OUTPUT_PORT_REG 必须在 CONFIG_REG 配置OUTPUT前清除记忆,否则有“乱动”危害!!!
print('OUTPUT_PORT_REG 记忆值 ', i2c.readfrom_mem(0x20, 0x02, 2))
print('OUTPUT_PORT_REG 清除了 ', i2c.writeto_mem(0x20, 0x02, b'\xff\xff'))
print('OUTPUT_PORT_REG 记忆值 ', i2c.readfrom_mem(0x20, 0x02, 2))
time.sleep(3)
i2c.writeto_mem(0x20, 0x06, b'\x00\x00') # 写入字节对象
print('CONFIG_REG=', i2c.readfrom_mem(0x20, 0x06, 2)) # 读取配置
print('OUTPUT_PORT_REG=', i2c.readfrom_mem(0x20, 0x02, 2)) # 读取配置
time.sleep(3)

# 简单测试一下点灯
i2c.writeto_mem(0x20, 0x02, b'\xff\xff') # 写入字节对象
time.sleep(1)
i2c.writeto_mem(0x20, 0x02, b'\x00\x00') # 写入字节对象
time.sleep(1)
i2c.writeto_mem(0x20, 0x02, b'\xaa\xaa') # 写入字节对象
time.sleep(1)
i2c.writeto_mem(0x20, 0x02, b'\x55\x55') # 写入字节对象
time.sleep(1)

# 复位包括 CONFIG_REG、OUTPUT_PORT_REG
i2c.writeto_mem(0x20, CONFIG_REG, b'\xff\xff') # 字节对象 = 字节数组 作用相同buffer 只是字节对象不可改变而已
i2c.writeto_mem(0x20, OUTPUT_PORT_REG, b'\xff\xff') # 对于共阳极积木,输出高电平才能保证继电器不动作;共阴极则应输出低电平

熟悉一下bytearray、位操作,仿照PCF8575驱动,使用I2C总线的内存操作命令写9555驱动:

"""
MicroPython PCA9555/TCA9555 16-Bit I2C I/O Expander with Interrupt
LIUCAN 20250104 模仿PCF8575编写9555驱动
LIUCAN 20250308 PCF8575是标准总线操作,9555是内存操作,参考9554驱动
"""
# 定义寄存器地址
INPUT_PORT_REG = 0x0
OUTPUT_PORT_REG = 0x2
POLARITY_INVERSION_REG = 0x4
CONFIG_REG = 0x6
INIT_VALUE = b'\x00\xff' # 初始配置值 P0-P7 输出、P10-P17 输入

class TCA9555:
    def __init__(self, i2c, address=0x20):
        self._i2c = i2c
        self._address = address
        self._port = bytearray(2) # 字节数组可变,字节对象不可改变
        self._i2c.writeto_mem(self._address, CONFIG_REG, INIT_VALUE) # IO端口配置初始化

    def check(self):
        if self._i2c.scan().count(self._address) == 0:
            raise OSError(f"TCA9555 not found at I2C address {self._address:#x}")
        return True

    @property
    def port(self):
        self._read() # 读入字节数组
        return self._port[0] | (self._port[1] << 8)

    @port.setter
    def port(self, value):
        self._port[0] = value & 0xFF
        self._port[1] = (value >> 8) & 0xFF
        self._write() # 写入字节数组

    def pin(self, pin, value=None):
        pin = self._validate_pin(pin)
        if value is None:
            self._read()
            return (self._port[pin // 8] >> (pin % 8)) & 1
        if value:
            self._port[pin // 8] |= 1 << (pin % 8)
        else:
            self._port[pin // 8] &= ~(1 << (pin % 8))
        self._write()

    def toggle(self, pin):
        pin = self._validate_pin(pin)
        self._port[pin // 8] ^= 1 << (pin % 8)
        self._write()

    def _validate_pin(self, pin):
        # pin valid range 0..7 and 10-17 (shifted to 8-15)
        # first digit: port (0-1)
        # second digit: io (0-7)
        if not 0 <= pin <= 7 and not 10 <= pin <= 17:
            raise ValueError(f"Invalid pin {pin}. Use 0-7 or 10-17.")
        if pin >= 10:
            pin -= 2
        return pin

    def pin_set_mode(self, pin: int, mode: bool) -> int:
        """
        设置引脚模式 (INPUT/OUTPUT)
        :param pin: 引脚号 (0-7/10-17)
        :param mode: 模式 (True 为 INPUT, False 为 OUTPUT)
        """
        # bytes字节对象是不可变的,先将字节对象转换为字节数组bytearray再处理
        # readfrom_mem 快递不包邮、readfrom_mem_into 包邮更香
        self._port = bytearray(self._i2c.readfrom_mem(self._address, CONFIG_REG, 2))
        print(self._port, self._port[0], self._port[1])
        if mode:  # INPUT
            self._port[pin // 8] |= (1 << (pin % 8))
        else:  # OUTPUT
            self._port[pin // 8] &= ~(1 << (pin % 8))
        print(self._port, self._port[0], self._port[1])
        self._i2c.writeto_mem(self._address, CONFIG_REG, self._port)
        
    def _read(self): # 读取的字节数是self._port的长度 而readfrom_mem是读指定的字节数
        self._i2c.readfrom_mem_into(self._address, INPUT_PORT_REG, self._port)

    def _write(self): # 将字节数组内容写入从站
        self._i2c.writeto_mem(self._address, OUTPUT_PORT_REG, self._port)
        
if __name__=='__main__':
    from machine import Pin, I2C
    import time
    print('class TCA9555 file tca9555')
    # I2C构造函数 ESP32引脚可变
    i2c = I2C(1, freq=100_000, scl=23, sda=22)
    print("I2C ADDRESS", hex(i2c.scan()[0]))
    # TCA9555默认地址0x20
    tca = TCA9555(i2c)
    print(tca.port)
#     for i in range(8):
#         tca.pin(i,0)
#         time.sleep(1)
#     print(tca.port)
    tca.port = 255 # 装饰器@property将方法变属性 @port.setter设置属性值
    tca.toggle(0)
    time.sleep(1)
    tca.toggle(0)
    
    # 临时设置引脚IO模式 发现仅设置为输出模式就自动点灯,这可不行哦!
    tca.pin_set_mode(8,0)
    time.sleep(1)
    tca.pin_set_mode(8,1)
    # 用总线内存操作验证上面的情况属实,临时配置P10-P17为输出端口,结果灯全亮了
    i2c.writeto_mem(0x20, 0x07, b'\x00')
    # 再想想亮灯是因为输出端口=0,TCA9555带16LED的积木设计是共阳极,0=点灯,这就对了
    
#     常见错误
#     tca.port = b'\xff\xff' # TypeError: unsupported types for __and__: 'bytes', 'int'
#     tca.port(255) # TypeError: 'int' object isn't callable

如何引用9555.py参考:

from machine import Pin, I2C
import pca9555
import time

# I2C构造函数 ESP32引脚可变
i2c = I2C(1, freq=100_000, scl=22, sda=21)
print("I2C ADDRESS", hex(i2c.scan()[0]))

# PCA9555默认地址0x20
pca = pca9555.PCA9555(i2c)

pca.port = 0xFFFF # 清场 全部配置为输出 高电平熄灯
print(pca.pin(1), pca.pin(10)) # 1 1 读
time.sleep_ms(3000)
pca.pin(1, 0), pca.pin(10, 0)  # 0 0 写 输出低电平亮灯
time.sleep_ms(3000)
pca.toggle(1), pca.toggle(10)  # 1 1 翻转熄灯
time.sleep_ms(3000)

pca.port = 0x0000  # 设置 CONFIG_REG 0x06 0x07 全亮
print(i2c.readfrom_mem(0x20, 0x02, 2)) # OUTPUT_PORT_REG = 0x2 没作用

# attach an IRQ to any mcu pin that can be pulled high.
# INT is open drain, so the mcu pin needs a pull-up
# when the INT pin activates, it will go LOW
p32 = Pin(32, Pin.IN, Pin.PULL_UP)
# 中断处理函数
def _handler(p):
    print(f"INT: {p.value()}, PORT: {pca.port}")
# 上升沿或下降沿触发中断
p32.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=_handler)

# 实现流水灯
while True:
    for i in range(0, 8, 1):
        pca.pin(i, not pca.pin(i))
        time.sleep_ms(1000)
    print(i2c.readfrom_mem(0x20, 0x02, 2))
    for i in range(10, 18, 1):
        pca.pin(i, not pca.pin(i))
        time.sleep_ms(1000)
    print(i2c.readfrom_mem(0x20, 0x02, 2))


 二、利用TCA9555扩展IO口的电路PCB尝试:
1、32路输入

2、32路输出 寻找低电平有效的高侧开关,找不到,所以用反相器。
TCA9555 3.3V + SN74HC240N 5.0V + TBD62783APG 24V

三、TCA9555芯片内置弱上拉电阻测试
电子积木有2K限流电阻接LED灯,需要知道芯片内部是否有上拉电阻,才好对接SN74HC240N

三、实验经验
完整3.3V电压驱动电路:MCU I2C--TCA9555电子积木--74HC240N反相器--TBD62783达林顿。
1、基于TCA9555电子积木的16路IO口集成了16个2K限流电阻+LED灯,共阳极,低电平有效、LED点亮。但是积木没有集成10K上拉电阻,芯片内置的电阻实测80-120K相当于高阻态,遇到80K的还好点,但多数是高于100K的高阻态,不方便后级CMOS芯片需要接地/接电源不能悬空的情况。
2、TBD62783是与ULN2803对应的八路达林顿阵列,内置续流二极管,非常适合500ma以内24VDC中间继电器负载,二者均是逻辑高电平有效,驱动电流1ma以内,其中TBD62783实测每路驱动电流0.1ma(3.3V TTL电平),八路合计小于1ma驱动电流,低功耗不发热。TBD62783输入端没有强制接地和接VCC的要求,可以悬空,实测和接地的效果一样,输出端会呈现1.4至2.0V不等的“虚电压”,你只需接24VDC中间继电器负载,即可测量电压归零,而且没有漏出来的电流。而输入端逻辑电平高,实测3.2V电压,驱动电流每路仅0.107ma,很小,2路才0.214ma,输出38.2ma电流时,中间继电器线圈电压22.5V,VCC=24V,压降1.5V。TBD62783的芯片货源有三种,价格差估计体现在压降上,这1.5V的压降*40ma电流,会导致芯片内部发热功率60mW,8路480mw,轻微发热。
3、基于以上TCA9555电子积木的逻辑低电平有效、八路达林顿阵列的逻辑高电平有效,中间必须加八路反相器匹配。选择74HC240N,3.3V工作电压,输出0.1ma时逻辑电平高于3.2V,逻辑低电平接近0V。这里不得不提74HCT240,他的工作电压4.5-5.5V,可以兼容TTL电平3.3V,需要电平转换(3.3V转5V)的时候可用,而没有字母T的240,工作电压2-6V电压更宽,但5V工作电压识别MCU的高电平3.3V不可靠。74HC240N驱动TBD62783,八路全开不到1ma,芯片冷静。所幸TBD62783的output on最小驱动电压2.0V,可以采用没有字母T的240工作在3.3V电压。
4、迫于MCU的3.3V电平,TCA9555电子积木也用3.3V供电才能适配I2C总线电平,那么74HC240N也用3.3V工作电压,TBD62783的output on最小驱动电压2.0V,实测3.3V电平驱动完全放心。
5、最后实验教训总结:TCA9555电子积木的IO口必须加10K上拉电阻,否则74HC240N上电后输入端悬空,等TCA9555从默认高阻态配置为输出时,估计已经永久性损坏。
核心问题:TCA9555在复位后默认所有引脚为输入模式(高阻态),此时约100kΩ的上拉电阻太弱,对后级CMOS电路相当于悬空。必须在TCA9555的输出引脚和VCC(3.3V)之间添加外部10kΩ上拉电阻。为什么外部10kΩ优于内部100kΩ?
CMOS输入需求:CMOS输入需要明确的逻辑电平。10kΩ能提供更强的上拉电流(330μA),确保74HC240N输入端被稳定拉高。
响应速度:74HC240N输入端有寄生电容,100kΩ上拉形成的时间常数太大,会导致逻辑状态变化缓慢,影响系统响应。
抗干扰能力:更强的上拉能更好地抵抗噪声。
TCA9555电子积木输出引脚 ---> 10kΩ电阻 ---> 3.3V
                                                 ↓
                                      74HC240N输入
TCA9555必须添加外部上拉电阻这一教训,对所有使用I2C IO扩展器驱动CMOS逻辑电路的场景都具有重要参考价值。

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐