网上可查到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))
待续 输入端口 检测。