diff --git a/README.md b/README.md index 50f5561..59d3d44 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,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 pcf8574.py +$ mpremote mip install github:mcauser/micropython-pcf8574 +$ mpremote mip install github:mcauser/micropython-pcf8574/examples +``` + +Using mip directly on a WiFi capable board: + +```python +>>> import mip +>>> mip.install("github:mcauser/micropython-pcf8574") +>>> mip.install("github:mcauser/micropython-pcf8574/examples") ``` +Manual installation: + +Copy `src/pcf8574.py` to the root directory of your device. + + +## Examples + **Basic Usage** ```python @@ -37,7 +54,7 @@ import pcf8574 from machine import I2C, Pin # TinyPICO (ESP32) -i2c = I2C(scl=Pin(22), sda=Pin(21)) +i2c = I2C(0) pcf = pcf8574.PCF8574(i2c, 0x20) # read pin 2 @@ -53,14 +70,22 @@ pcf.pin(4, 0) pcf.toggle(5) # set all pins at once with 8-bit int -pcf.port = 0xff +pcf.port = 0xFF # read all pins at once as 8-bit int pcf.port +# returns 255 (0xFF) ``` For more detailed examples, see [examples](/examples). +If you mip installed them above, you can run them like so: + +```python +import pcf8574.examples.basic +``` + + #### Pins Pin | Type | Description @@ -82,6 +107,7 @@ SCL | I | I2C Serial Clock, needs pull-up GND | PWR | Ground VCC | PWR | Supply voltage 3.3-5V + ## Methods Construct with a reference to I2C and set the device address. @@ -92,6 +118,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. @@ -108,6 +140,11 @@ Valid pin range 0-7. toggle(pin) ``` +Private method for checking the supplied pin number is within the valid range. +```python +_validate_pin() +``` + Private method for loading _port from the device. ```python _read() @@ -118,6 +155,7 @@ Private method for sending _port to the device. _write() ``` + ## Properties Getter reads the port from the device and returns a 8-bit integer. @@ -129,25 +167,30 @@ Setter writes an 8-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 8-bit port to the device once, rather than 8 separate writes. ```python -port = 0xffff +port = 0xFF ``` + ## Ports * P00-P07 - Port A -This chip only has one port. If you need more pins, the [PCF8575](https://github.com/mcauser/micropython-pcf8575) has two ports (another 8 pins). +This chip only has one port (8 pins). If you need more pins, the +[PCF8575](https://github.com/mcauser/micropython-pcf8575) has two ports (16 pins). + ## Interrupts * INT - Active LOW + ## I2C Interface If you are using a module, most contain 10k pull-ups on the SCL and SDA lines. If you are using the PCF8574 chip directly, you'll need to add your own. + ### I2C Address The chip supports I2C addresses 0x20-0x27 and is customisable using address pins A0, A1, A2 @@ -163,31 +206,47 @@ GND | GND | 3V3 | 0x24 GND | 3V3 | 3V3 | 0x26 3V3 | 3V3 | 3V3 | 0x27 + ## Parts -* [TinyPICO](https://www.tinypico.com/) $20.00 USD -* [PCF8574 10x DIP](https://www.aliexpress.com/item/32933373566.html) $3.00 AUD -* [PCF8574 blue board](https://www.aliexpress.com/item/32224660654.html) $0.83 AUD -* [PCF8574 red board](https://www.aliexpress.com/item/32441163158.html) $0.95 AUD -* [PCF8574 purple board](https://www.aliexpress.com/item/32805562508.html) $0.99 AUD -* [PCF8574 as LCD backpack](https://www.aliexpress.com/item/32654514649.html) $0.98 AUD +* [PCF8574 blue chainable board](https://s.click.aliexpress.com/e/_DdzV1ZR) +* [PCF8574 blue chainable board](https://s.click.aliexpress.com/e/_DlQkWZj) +* [PCF8574 red board DIP switch](https://s.click.aliexpress.com/e/_DevQFrx) +* [PCF8574 red board DIP switch](https://s.click.aliexpress.com/e/_Dmeylnb) +* [PCF8574 purple board](https://s.click.aliexpress.com/e/_DlcJB2t) +* [PCF8574 as 1602 LCD backpack](https://s.click.aliexpress.com/e/_DlcJB2t) +* [PCF8574 10x DIP-16](https://s.click.aliexpress.com/e/_Dn3xoYh) +* [PCF8574 5x SOP-16](https://s.click.aliexpress.com/e/_DC1lqxn) +* [TinyPICO](https://www.tinypico.com/) + ## Connections -TinyPICO | PCF8574 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) +``` + +PCF8574 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) * [PCF8574 datasheet](docs/pcf8574.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..12daad5 --- /dev/null +++ b/examples/basic.py @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: 2019 Mike Causer +# SPDX-License-Identifier: MIT + +""" +MicroPython PCF8574 Basic example + +Toggles pins individually, then all in a single call +""" + +import pcf8574 +from machine import I2C + +# TinyPICO (ESP32) +i2c = I2C(0) + +pcf = pcf8574.PCF8574(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 8-bit int +pcf.port = 0xFF + +# read all pins at once as 8-bit int +print(pcf.port) +# returns 255 (0xFF) diff --git a/examples/interrupts.py b/examples/interrupts.py new file mode 100644 index 0000000..c656e45 --- /dev/null +++ b/examples/interrupts.py @@ -0,0 +1,54 @@ +# SPDX-FileCopyrightText: 2019 Mike Causer +# SPDX-License-Identifier: MIT + +""" +MicroPython PCF8574 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. + +Interrupt is cleared when the pins are changed or the port is read. + +Add a push button between pin P07 and GND. Works on any pin. P07 picked at random. + +When pressed P07 will be driven LOW and the interrupt will fire. + +When released another interrupt will fire, if the previous interrupt has been cleared. + +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 pcf8574 +from machine import I2C, Pin + +# TinyPICO (ESP32) +i2c = I2C(0) + +pcf = pcf8574.PCF8574(i2c, 0x20) + +# set all pins as inputs (HIGH) +pcf.port = 0xFF + +# 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(f"INT: {p.value()}, PORT: {pcf.port}") + + +# turn on interrupt handler +p4.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=_handler) + +# turn off interrupt handler +p4.irq(None) diff --git a/examples/package.json b/examples/package.json new file mode 100644 index 0000000..11f67da --- /dev/null +++ b/examples/package.json @@ -0,0 +1,8 @@ +{ + "urls": [ + ["pcf8574/examples/basic.py", "github:mcauser/micropython-pcf8574/examples/basic.py"], + ["pcf8574/examples/interrupts.py", "github:mcauser/micropython-pcf8574/examples/interrupts.py"] + ], + "deps": [], + "version": "1.1.0" +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3e6fe3c --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "urls": [ + ["pcf8574/__init__.py", "github:mcauser/micropython-pcf8574/src/pcf8574.py"] + ], + "deps": [], + "version": "1.1.0" +} diff --git a/pcf8574.py b/pcf8574.py deleted file mode 100644 index ab2639c..0000000 --- a/pcf8574.py +++ /dev/null @@ -1,72 +0,0 @@ -""" -MicroPython PCF8574 8-Bit I2C I/O Expander with Interrupt -https://github.com/mcauser/micropython-pcf8574 - -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 PCF8574: - def __init__(self, i2c, address=0x20): - self._i2c = i2c - self._address = address - self._port = bytearray(1) - if i2c.scan().count(address) == 0: - raise OSError('PCF8574 not found at I2C address {:#x}'.format(address)) - - @property - def port(self): - self._read() - return self._port[0] - - @port.setter - def port(self, value): - self._port[0] = value & 0xff - self._write() - - def pin(self, pin, value=None): - pin = self.validate_pin(pin) - if value is None: - self._read() - return (self._port[0] >> pin) & 1 - else: - if value: - self._port[0] |= (1 << (pin)) - else: - self._port[0] &= ~(1 << (pin)) - self._write() - - def toggle(self, pin): - pin = self.validate_pin(pin) - self._port[0] ^= (1 << (pin)) - self._write() - - def validate_pin(self, pin): - # pin valid range 0..7 - if not 0 <= pin <= 7: - raise ValueError('Invalid pin {}. Use 0-7.'.format(pin)) - return pin - - def _read(self): - self._i2c.readfrom_into(self._address, self._port) - - def _write(self): - self._i2c.writeto(self._address, self._port) diff --git a/src/pcf8574.py b/src/pcf8574.py new file mode 100644 index 0000000..129e3a1 --- /dev/null +++ b/src/pcf8574.py @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: 2019 Mike Causer +# SPDX-License-Identifier: MIT + +""" +MicroPython PCF8574 8-Bit I2C I/O Expander with Interrupt +https://github.com/mcauser/micropython-pcf8574 +""" + +__version__ = "1.1.0" + + +class PCF8574: + def __init__(self, i2c, address=0x20): + self._i2c = i2c + self._address = address + self._port = bytearray(1) + + def check(self): + if self._i2c.scan().count(self._address) == 0: + raise OSError(f"PCF8574 not found at I2C address {self._address:#x}") + return True + + @property + def port(self): + self._read() + return self._port[0] + + @port.setter + def port(self, value): + self._port[0] = value & 0xFF + self._write() + + def pin(self, pin, value=None): + pin = self._validate_pin(pin) + if value is None: + self._read() + return (self._port[0] >> pin) & 1 + if value: + self._port[0] |= 1 << (pin) + else: + self._port[0] &= ~(1 << (pin)) + self._write() + + def toggle(self, pin): + pin = self._validate_pin(pin) + self._port[0] ^= 1 << (pin) + self._write() + + def _validate_pin(self, pin): + # pin valid range 0..7 + if not 0 <= pin <= 7: + raise ValueError(f"Invalid pin {pin}. Use 0-7.") + return pin + + def _read(self): + self._i2c.readfrom_into(self._address, self._port) + + def _write(self): + self._i2c.writeto(self._address, self._port)