diff --git a/README.md b/README.md index 876ded0..ff9630b 100644 --- a/README.md +++ b/README.md @@ -28,14 +28,31 @@ At power on, all pins are driven HIGH and can be immediately used as inputs. Operating voltage: 2.5V - 5.5V -## Example -Copy the file to your device, using ampy, rshell, webrepl or compiling and deploying. eg. +## Installation + +Using mip via mpremote: ```bash -$ ampy put pcf8575.py +$ mpremote mip install github:mcauser/micropython-pcf8575 +$ mpremote mip install github:mcauser/micropython-pcf8575/examples +``` + +Using mip directly on a WiFi capable board: + +```python +>>> import mip +>>> mip.install("github:mcauser/micropython-pcf8575") +>>> mip.install("github:mcauser/micropython-pcf8575/examples") ``` +Manual installation: + +Copy `src/pcf8575.py` to the root directory of your device. + + +## Examples + **Basic Usage** ```python @@ -43,7 +60,7 @@ import pcf8575 from machine import I2C, Pin # TinyPICO (ESP32) -i2c = I2C(scl=Pin(22), sda=Pin(21)) +i2c = I2C(0) pcf = pcf8575.PCF8575(i2c, 0x20) # read pin 2 @@ -59,14 +76,21 @@ pcf.pin(4, 0) pcf.toggle(5) # set all pins at once with 16-bit int -pcf.port = 0xff00 +pcf.port = 0xFF00 # read all pins at once as 16-bit int pcf.port +# returns 65280 (0xFF00) ``` For more detailed examples, see [examples](/examples). +If you mip installed them above, you can run them like so: + +```python +import pcf8575.examples.basic +``` + ## Methods Construct with a reference to I2C and set the device address. @@ -77,6 +101,12 @@ See below for address selection. __init__(i2c, address=0x20) ``` +Scans the I2C bus for the provided address and returns True if a device is present +otherwise raises an OSError. +```python +check() +``` + Method for getting or setting a single pin. If no value is provided, the port will be read and value of specified pin returned. If a value is provided, the port will be updated and device written to. @@ -93,6 +123,11 @@ Valid pin range 0-7 and 10-17. toggle(pin) ``` +Private method for checking the supplied pin number is within valid ranges. +```python +_validate_pin() +``` + Private method for loading _port from the device. ```python _read() @@ -103,6 +138,7 @@ Private method for sending _port to the device. _write() ``` + ## Properties Getter reads the port from the device and returns a 16 bit integer. @@ -114,9 +150,10 @@ Setter writes a 16-bit integer representing the port to the device. If you are setting multiple pins at once, use this instead of the pin() method as this writes the entire 16-bit port to the device once, rather than 16 separate writes. ```python -port = 0xffff +port = 0xFFFF ``` + ## Ports * P00-P07 - Port A @@ -126,9 +163,16 @@ Why is there no P08 and P09? Because they skipped them when naming the pins, so that the lest significant digit is 0-7 for both ports. There's still only 16 bits. Port B pins are just labelled +2. +This chip only has two ports (16 pins). If you only need 8 pins, the +[PCF8574](https://github.com/mcauser/micropython-pcf8574) has a single port (8 pins). + + ## Interrupts -* INT - Shared by both ports A and B +* INT - Active LOW + +Shared by both ports A and B. Triggered by either. + ## I2C Interface @@ -152,28 +196,44 @@ GND | GND | 3V3 | 0x24 GND | 3V3 | 3V3 | 0x26 3V3 | 3V3 | 3V3 | 0x27 + ## Parts -* [TinyPICO](https://www.tinypico.com/) $20.00 USD -* [PCF8575](https://www.aliexpress.com/item/4000019921959.html) $2.85 AUD -* [PCF8575](https://www.aliexpress.com/item/32811949623.html) $3.12 AUD +* [PCF8575 blue module](https://s.click.aliexpress.com/e/_DdXNOaN) +* [PCF8575 blue module](https://s.click.aliexpress.com/e/_DFBcPc5) +* [PCF8575 blue module](https://s.click.aliexpress.com/e/_DDFiuqV) +* [PCF8575 red module](https://s.click.aliexpress.com/e/_DmRWVFx) +* [PCF8575 10x SSOP-24](https://s.click.aliexpress.com/e/_DDBKEJP) +* [TinyPICO](https://www.tinypico.com/) + ## Connections -TinyPICO | PCF8575 Module --------- | ---------- -21 SDA | SDA -22 SCL | SCL -3V3 | VCC -G | GND -4 | INT (optional) +### TinyPICO ESP32 + +```python +from machine import SoftI2C, Pin +i2c = SoftI2C(scl=Pin(22), sda=Pin(21)) + +from machine import I2C, Pin +i2c = I2C(0) +``` + +PCF8575 Module | TinyPICO (ESP32) +-------------- | ---------------- +SDA | 21 (SDA) +SCL | 22 (SCL) +VCC | 3V3 +GND | GND +INT (optional) | 4 + ## Links -* [TinyPICO Getting Started](https://www.tinypico.com/gettingstarted) * [micropython.org](http://micropython.org) * [PCF8575 datasheet](docs/pcf8575.pdf) -* [Adafruit Ampy](https://learn.adafruit.com/micropython-basics-load-files-and-run-code/install-ampy) +* [TinyPICO Getting Started](https://www.tinypico.com/gettingstarted) + ## License diff --git a/examples/basic.py b/examples/basic.py new file mode 100644 index 0000000..8c1cc8b --- /dev/null +++ b/examples/basic.py @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: 2019 Mike Causer +# SPDX-License-Identifier: MIT + +""" +MicroPython PCF8575 Basic example + +Toggles pins individually, then all in a single call +""" + +import pcf8575 +from machine import I2C + +# TinyPICO (ESP32) +i2c = I2C(0) + +pcf = pcf8575.PCF8575(i2c, 0x20) + +# read pin 2 +pcf.pin(2) + +# set pin 3 HIGH +pcf.pin(3, 1) + +# set pin 4 LOW +pcf.pin(4, 0) + +# toggle pin 5 +pcf.toggle(5) + +# set all pins at once with 16-bit int +pcf.port = 0xFF00 + +# read all pins at once as 16-bit int +print(pcf.port) +# returns 65280 (0xFF00) diff --git a/examples/interrupt/readme.md b/examples/interrupts.py similarity index 71% rename from examples/interrupt/readme.md rename to examples/interrupts.py index eb269f1..6682495 100644 --- a/examples/interrupt/readme.md +++ b/examples/interrupts.py @@ -1,4 +1,8 @@ -# Using Interrupts +# SPDX-FileCopyrightText: 2019 Mike Causer +# SPDX-License-Identifier: MIT + +""" +MicroPython PCF8575 Interrupts example Any pin that is set HIGH is in input mode and will fire an interrupt on the INT pin on any rising or falling edge. @@ -11,38 +15,42 @@ When released another interrupt will fire, if the previous interrupt has been cleared. -```python +Debouncing +---------- + +In some cases, debouncing isn't required. Depends on the hardware. + +If you add a 100nF capacitor across the push button, it will add a +bit of a buffer and block rapid fires. 100nF blocks for around a second. +A 10nF capacitor blocks for roughly 1/10th of a second. +""" + import pcf8575 -from machine import Pin, I2C +from machine import I2C, Pin -# TinyPICO -i2c = I2C(scl=Pin(22), sda=Pin(21)) +# TinyPICO (ESP32) +i2c = I2C(0) pcf = pcf8575.PCF8575(i2c, 0x20) # set all pins as inputs (HIGH) -pcf.port = 0xffff +pcf.port = 0xFFFF # 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 p4 = Pin(4, Pin.IN, Pin.PULL_UP) + # a simple interrupt handler def _handler(p): - print('INT: {}, PORT: {}'.format(p.value(), pcf.port)) + print(f"INT: {p.value()}, PORT: {pcf.port}") + # turn on interrupt handler -p4.irq(trigger=Pin.IRQ_RISING|Pin.IRQ_FALLING, handler=_handler) +p4.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=_handler) + +# connect pin 7 to GND, interrupt handler will fire # turn off interrupt handler p4.irq(None) -``` - -## Debouncing - -In some cases, debouncing isn't required. Depends on the hardware. - -If you add a 100nF capacitor across the push button, it will add a -bit of a buffer and block rapid fires. 100nF blocks for around a second. -A 10nF capacitor blocks for roughly 1/10th of a second. diff --git a/examples/leds/readme.md b/examples/leds/readme.md deleted file mode 100644 index a654fc9..0000000 --- a/examples/leds/readme.md +++ /dev/null @@ -1,64 +0,0 @@ -# LEDs - -## Sourcing current - -Connect a LED between P00 and GND. - -LED positive anode (long leg) connected to P00. - -LED negative cathode (short leg) connected to GND. - -Driving a pin HIGH will illuminate the LED. - -The device has latched outputs with high current drive capability -for directly driving LEDs. - -Can sink 25mA. - -```python -import pcf8575 -from machine import Pin, I2C - -# TinyPICO -i2c = I2C(scl=Pin(22), sda=Pin(21)) - -pcf = pcf8575.PCF8575(i2c, 0x20) - -# set P00 HIGH and all other pins LOW -# turn LED on -pcf.port = 0x0001 - -# turn LED off -pcf.port = 0x0000 -``` - -## Sinking current - -Connect a LED between P01 and 3V3, via a resistor. - -LED positive anode (long leg) connected to 3V3. - -LED negative cathode (short leg) connected to P01. - -Driving a pin LOW will illuminate the LED. - -For a red or blue LED, use a 330K and green LED 220K current limiting resistor. - -Can source 100mA. - -```python -import pcf8575 -from machine import Pin, I2C - -# TinyPICO -i2c = I2C(scl=Pin(22), sda=Pin(21)) - -pcf = pcf8575.PCF8575(i2c, 0x20) - -# set P01 LOW and all other pins HIGH -# turn LED on -pcf.port = 0xfffd - -# turn LED off -pcf.port = 0xffff -``` diff --git a/examples/leds_sinking.py b/examples/leds_sinking.py new file mode 100644 index 0000000..7f51445 --- /dev/null +++ b/examples/leds_sinking.py @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2019 Mike Causer +# SPDX-License-Identifier: MIT + +""" +MicroPython PCF8575 LEDs sinking current example + +Connect a LED between P01 and 3V3, via a current limiting resistor. +* LED positive anode (long leg) connected to 3V3. +* LED negative cathode (short leg) connected to P01. + +Driving a pin LOW will illuminate the LED. + +eg. for a red or blue LED, use a 330K and green LED 220K current limiting resistor. +""" + +import pcf8575 +from machine import I2C + +# TinyPICO (ESP32) +i2c = I2C(0) + +pcf = pcf8575.PCF8575(i2c, 0x20) + +# set P01 LOW and all other pins HIGH +# turn LED on +pcf.port = 0xFFFE + +# turn LED off +pcf.port = 0xFFFF diff --git a/examples/leds_sourcing.py b/examples/leds_sourcing.py new file mode 100644 index 0000000..2c05c63 --- /dev/null +++ b/examples/leds_sourcing.py @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2019 Mike Causer +# SPDX-License-Identifier: MIT + +""" +MicroPython PCF8575 LEDs sourcing current example + +Connect a LED between P00 and GND. +* LED positive anode (long leg) connected to P00. +* LED negative cathode (short leg) connected to GND. + +Driving a pin HIGH will illuminate the LED. + +The device has latched outputs with high current drive capability +for directly driving LEDs. +""" + +import pcf8575 +from machine import I2C + +# TinyPICO (ESP32) +i2c = I2C(0) + +pcf = pcf8575.PCF8575(i2c, 0x20) + +# set P00 HIGH and all other pins LOW +# turn LED on +pcf.port = 0x0001 + +# turn LED off +pcf.port = 0x0000 diff --git a/examples/package.json b/examples/package.json new file mode 100644 index 0000000..8e0dcda --- /dev/null +++ b/examples/package.json @@ -0,0 +1,10 @@ +{ + "urls": [ + ["pcf8575/examples/basic.py", "github:mcauser/micropython-pcf8575/examples/basic.py"], + ["pcf8575/examples/interrupts.py", "github:mcauser/micropython-pcf8575/examples/interrupts.py"], + ["pcf8575/examples/leds_sinking.py", "github:mcauser/micropython-pcf8575/examples/leds_sinking.py"], + ["pcf8575/examples/leds_sourcing.py", "github:mcauser/micropython-pcf8575/examples/leds_sourcing.py"] + ], + "deps": [], + "version": "1.1.0" +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..965c97f --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "urls": [ + ["pcf8575/__init__.py", "github:mcauser/micropython-pcf8575/src/pcf8575.py"] + ], + "deps": [], + "version": "1.1.0" +} diff --git a/pcf8575.py b/pcf8575.py deleted file mode 100644 index fb35f9b..0000000 --- a/pcf8575.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -MicroPython PCF8575 16-Bit I2C I/O Expander with Interrupt -https://github.com/mcauser/micropython-pcf8575 - -MIT License -Copyright (c) 2019 Mike Causer - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -class PCF8575: - def __init__(self, i2c, address=0x20): - self._i2c = i2c - self._address = address - self._port = bytearray(2) - if i2c.scan().count(address) == 0: - raise OSError('PCF8575 not found at I2C address {:#x}'.format(address)) - - @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 - else: - if value: - self._port[pin // 8] |= (1 << (pin % 8)) - else: - self._port[pin // 8] &= ~(1 << (pin % 8)) - self._write() - - def toggle(self, pin): - # pin valid range 0..7 and 10-17 (shifted to 8-15) - pin = self.validate_pin(pin) - self._port[pin // 8] ^= (1 << (pin % 8)) - self._write() - - def validate_pin(self, pin): - if not 0 <= pin <= 7 and not 10 <= pin <= 17: - raise ValueError('Invalid pin {}. Use 0-7 or 10-17.'.format(pin)) - if pin >= 10: - pin -= 2 - return pin - - def _read(self): - self._i2c.readfrom_into(self._address, self._port) - - def _write(self): - self._i2c.writeto(self._address, self._port) \ No newline at end of file diff --git a/src/pcf8575.py b/src/pcf8575.py new file mode 100644 index 0000000..e82e5cb --- /dev/null +++ b/src/pcf8575.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: 2019 Mike Causer +# SPDX-License-Identifier: MIT + +""" +MicroPython PCF8575 16-Bit I2C I/O Expander with Interrupt +https://github.com/mcauser/micropython-pcf8575 +""" + +__version__ = "1.1.0" + + +class PCF8575: + def __init__(self, i2c, address=0x20): + self._i2c = i2c + self._address = address + self._port = bytearray(2) + + def check(self): + if self._i2c.scan().count(self._address) == 0: + raise OSError(f"PCF8575 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 _read(self): + self._i2c.readfrom_into(self._address, self._port) + + def _write(self): + self._i2c.writeto(self._address, self._port)