├── LICENSE.txt ├── README.md ├── docs ├── demo.jpg └── pcf8574.pdf ├── examples ├── basic.py ├── interrupts.py └── package.json ├── package.json └── src └── pcf8574.py /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mike Causer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MicroPython PCF8574 2 | 3 | A MicroPython library for PCF8574 8-Bit I2C I/O Expander with Interrupt. 4 | 5 | ![demo](docs/demo.jpg) 6 | 7 | The PCF8574 consists of a 8-bit quasi-bidirectional port and an I2C-bus interface. 8 | 9 | The device includes latched outputs with high current drive capability for directly driving LEDs. 10 | 11 | The interrupt has an open-drain output, which means you need a pull-up on your microcontroller 12 | to detect when the PCF8574 drives it LOW. 13 | 14 | When setting a pin HIGH, it acts as both output AND input. The pin internally uses a weak 15 | current-source pull-up to latch output HIGH. 16 | When driven LOW, for example, with a push button, the pin will read as LOW. 17 | 18 | An interrupt fires on any rising or falling edge of the pins in input mode (HIGH). 19 | Interrupt is cleared when the pins are changed or the port is read. 20 | 21 | At power on, all pins are driven HIGH and can be immediately used as inputs. 22 | 23 | Operating voltage: 2.5V - 5.5V 24 | 25 | 26 | ## Installation 27 | 28 | Using mip via mpremote: 29 | 30 | ```bash 31 | $ mpremote mip install github:mcauser/micropython-pcf8574 32 | $ mpremote mip install github:mcauser/micropython-pcf8574/examples 33 | ``` 34 | 35 | Using mip directly on a WiFi capable board: 36 | 37 | ```python 38 | >>> import mip 39 | >>> mip.install("github:mcauser/micropython-pcf8574") 40 | >>> mip.install("github:mcauser/micropython-pcf8574/examples") 41 | ``` 42 | 43 | Manual installation: 44 | 45 | Copy `src/pcf8574.py` to the root directory of your device. 46 | 47 | 48 | ## Examples 49 | 50 | **Basic Usage** 51 | 52 | ```python 53 | import pcf8574 54 | from machine import I2C, Pin 55 | 56 | # TinyPICO (ESP32) 57 | i2c = I2C(0) 58 | pcf = pcf8574.PCF8574(i2c, 0x20) 59 | 60 | # read pin 2 61 | pcf.pin(2) 62 | 63 | # set pin 3 HIGH 64 | pcf.pin(3, 1) 65 | 66 | # set pin 4 LOW 67 | pcf.pin(4, 0) 68 | 69 | # toggle pin 5 70 | pcf.toggle(5) 71 | 72 | # set all pins at once with 8-bit int 73 | pcf.port = 0xFF 74 | 75 | # read all pins at once as 8-bit int 76 | pcf.port 77 | # returns 255 (0xFF) 78 | ``` 79 | 80 | For more detailed examples, see [examples](/examples). 81 | 82 | If you mip installed them above, you can run them like so: 83 | 84 | ```python 85 | import pcf8574.examples.basic 86 | ``` 87 | 88 | 89 | #### Pins 90 | 91 | Pin | Type | Description 92 | :---:|:----:|:---------------------------------------- 93 | A0 | I | Address select 1, connect to VCC or GND 94 | A1 | I | Address select 2, connect to VCC or GND 95 | A2 | I | Address select 3, connect to VCC or GND 96 | INT | O | Interrupt output, open drain, active LOW 97 | P00 | I/O | Port A, Pin 0 98 | P01 | I/O | Port A, Pin 1 99 | P02 | I/O | Port A, Pin 2 100 | P03 | I/O | Port A, Pin 3 101 | P04 | I/O | Port A, Pin 4 102 | P05 | I/O | Port A, Pin 5 103 | P06 | I/O | Port A, Pin 6 104 | P07 | I/O | Port A, Pin 7 105 | SDA | I/O | I2C Serial Data, needs pull-up 106 | SCL | I | I2C Serial Clock, needs pull-up 107 | GND | PWR | Ground 108 | VCC | PWR | Supply voltage 3.3-5V 109 | 110 | 111 | ## Methods 112 | 113 | Construct with a reference to I2C and set the device address. 114 | Valid address range 0x20-0x27. 115 | If are you not sure what it is, run an `i2c.scan()`. 116 | See below for address selection. 117 | ```python 118 | __init__(i2c, address=0x20) 119 | ``` 120 | 121 | Scans the I2C bus for the provided address and returns True if a device is present 122 | otherwise raises an OSError. 123 | ```python 124 | check() 125 | ``` 126 | 127 | Method for getting or setting a single pin. 128 | If no value is provided, the port will be read and value of specified pin returned. 129 | If a value is provided, the port will be updated and device written to. 130 | The port is written to after each call. If you intend to toggle many pins at once, use the 131 | port property instead. See below. 132 | Valid pin range 0-7. 133 | ```python 134 | pin(pin, value=None) 135 | ``` 136 | 137 | Method for flipping the value of a single pin. 138 | Valid pin range 0-7. 139 | ```python 140 | toggle(pin) 141 | ``` 142 | 143 | Private method for checking the supplied pin number is within the valid range. 144 | ```python 145 | _validate_pin() 146 | ``` 147 | 148 | Private method for loading _port from the device. 149 | ```python 150 | _read() 151 | ``` 152 | 153 | Private method for sending _port to the device. 154 | ```python 155 | _write() 156 | ``` 157 | 158 | 159 | ## Properties 160 | 161 | Getter reads the port from the device and returns a 8-bit integer. 162 | ```python 163 | port 164 | ``` 165 | 166 | Setter writes an 8-bit integer representing the port to the device. 167 | If you are setting multiple pins at once, use this instead of the pin() method as 168 | this writes the entire 8-bit port to the device once, rather than 8 separate writes. 169 | ```python 170 | port = 0xFF 171 | ``` 172 | 173 | 174 | ## Ports 175 | 176 | * P00-P07 - Port A 177 | 178 | This chip only has one port (8 pins). If you need more pins, the 179 | [PCF8575](https://github.com/mcauser/micropython-pcf8575) has two ports (16 pins). 180 | 181 | 182 | ## Interrupts 183 | 184 | * INT - Active LOW 185 | 186 | 187 | ## I2C Interface 188 | 189 | If you are using a module, most contain 10k pull-ups on the SCL and SDA lines. 190 | 191 | If you are using the PCF8574 chip directly, you'll need to add your own. 192 | 193 | 194 | ### I2C Address 195 | 196 | The chip supports I2C addresses 0x20-0x27 and is customisable using address pins A0, A1, A2 197 | 198 | A0 | A1 | A2 | I2C Address 199 | ----|-----|-----|------------ 200 | GND | GND | GND | 0x20 (default) 201 | 3V3 | GND | GND | 0x21 202 | GND | 3V3 | GND | 0x22 203 | 3V3 | 3V3 | GND | 0x23 204 | GND | GND | 3V3 | 0x24 205 | 3V3 | GND | 3V3 | 0x25 206 | GND | 3V3 | 3V3 | 0x26 207 | 3V3 | 3V3 | 3V3 | 0x27 208 | 209 | 210 | ## Parts 211 | 212 | * [PCF8574 blue chainable board](https://s.click.aliexpress.com/e/_DdzV1ZR) 213 | * [PCF8574 blue chainable board](https://s.click.aliexpress.com/e/_DlQkWZj) 214 | * [PCF8574 red board DIP switch](https://s.click.aliexpress.com/e/_DevQFrx) 215 | * [PCF8574 red board DIP switch](https://s.click.aliexpress.com/e/_Dmeylnb) 216 | * [PCF8574 purple board](https://s.click.aliexpress.com/e/_DlcJB2t) 217 | * [PCF8574 as 1602 LCD backpack](https://s.click.aliexpress.com/e/_DlcJB2t) 218 | * [PCF8574 10x DIP-16](https://s.click.aliexpress.com/e/_Dn3xoYh) 219 | * [PCF8574 5x SOP-16](https://s.click.aliexpress.com/e/_DC1lqxn) 220 | * [TinyPICO](https://www.tinypico.com/) 221 | 222 | 223 | ## Connections 224 | 225 | ### TinyPICO ESP32 226 | 227 | ```python 228 | from machine import SoftI2C, Pin 229 | i2c = SoftI2C(scl=Pin(22), sda=Pin(21)) 230 | 231 | from machine import I2C, Pin 232 | i2c = I2C(0) 233 | ``` 234 | 235 | PCF8574 Module | TinyPICO (ESP32) 236 | -------------- | ---------------- 237 | SDA | 21 (SDA) 238 | SCL | 22 (SCL) 239 | VCC | 3V3 240 | GND | GND 241 | INT (optional) | 4 242 | 243 | 244 | ## Links 245 | 246 | * [micropython.org](http://micropython.org) 247 | * [PCF8574 datasheet](docs/pcf8574.pdf) 248 | * [TinyPICO Getting Started](https://www.tinypico.com/gettingstarted) 249 | 250 | 251 | ## License 252 | 253 | Licensed under the [MIT License](http://opensource.org/licenses/MIT). 254 | 255 | Copyright (c) 2019 Mike Causer 256 | -------------------------------------------------------------------------------- /docs/demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcauser/micropython-pcf8574/b6123a8894a72a56dfba439982e2f62c836f4598/docs/demo.jpg -------------------------------------------------------------------------------- /docs/pcf8574.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mcauser/micropython-pcf8574/b6123a8894a72a56dfba439982e2f62c836f4598/docs/pcf8574.pdf -------------------------------------------------------------------------------- /examples/basic.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Mike Causer 2 | # SPDX-License-Identifier: MIT 3 | 4 | """ 5 | MicroPython PCF8574 Basic example 6 | 7 | Toggles pins individually, then all in a single call 8 | """ 9 | 10 | import pcf8574 11 | from machine import I2C 12 | 13 | # TinyPICO (ESP32) 14 | i2c = I2C(0) 15 | 16 | pcf = pcf8574.PCF8574(i2c, 0x20) 17 | 18 | # read pin 2 19 | pcf.pin(2) 20 | 21 | # set pin 3 HIGH 22 | pcf.pin(3, 1) 23 | 24 | # set pin 4 LOW 25 | pcf.pin(4, 0) 26 | 27 | # toggle pin 5 28 | pcf.toggle(5) 29 | 30 | # set all pins at once with 8-bit int 31 | pcf.port = 0xFF 32 | 33 | # read all pins at once as 8-bit int 34 | print(pcf.port) 35 | # returns 255 (0xFF) 36 | -------------------------------------------------------------------------------- /examples/interrupts.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Mike Causer 2 | # SPDX-License-Identifier: MIT 3 | 4 | """ 5 | MicroPython PCF8574 Interrupts example 6 | 7 | Any pin that is set HIGH is in input mode and will fire an interrupt on the INT pin 8 | on any rising or falling edge. 9 | 10 | Interrupt is cleared when the pins are changed or the port is read. 11 | 12 | Add a push button between pin P07 and GND. Works on any pin. P07 picked at random. 13 | 14 | When pressed P07 will be driven LOW and the interrupt will fire. 15 | 16 | When released another interrupt will fire, if the previous interrupt has been cleared. 17 | 18 | Debouncing 19 | ---------- 20 | 21 | In some cases, debouncing isn't required. Depends on the hardware. 22 | 23 | If you add a 100nF capacitor across the push button, it will add a 24 | bit of a buffer and block rapid fires. 100nF blocks for around a second. 25 | A 10nF capacitor blocks for roughly 1/10th of a second. 26 | """ 27 | 28 | import pcf8574 29 | from machine import I2C, Pin 30 | 31 | # TinyPICO (ESP32) 32 | i2c = I2C(0) 33 | 34 | pcf = pcf8574.PCF8574(i2c, 0x20) 35 | 36 | # set all pins as inputs (HIGH) 37 | pcf.port = 0xFF 38 | 39 | # attach an IRQ to any mcu pin that can be pulled high. 40 | # INT is open drain, so the mcu pin needs a pull-up 41 | # when the INT pin activates, it will go LOW 42 | p4 = Pin(4, Pin.IN, Pin.PULL_UP) 43 | 44 | 45 | # a simple interrupt handler 46 | def _handler(p): 47 | print(f"INT: {p.value()}, PORT: {pcf.port}") 48 | 49 | 50 | # turn on interrupt handler 51 | p4.irq(trigger=Pin.IRQ_RISING | Pin.IRQ_FALLING, handler=_handler) 52 | 53 | # turn off interrupt handler 54 | p4.irq(None) 55 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "urls": [ 3 | ["pcf8574/examples/basic.py", "github:mcauser/micropython-pcf8574/examples/basic.py"], 4 | ["pcf8574/examples/interrupts.py", "github:mcauser/micropython-pcf8574/examples/interrupts.py"] 5 | ], 6 | "deps": [], 7 | "version": "1.1.0" 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "urls": [ 3 | ["pcf8574/__init__.py", "github:mcauser/micropython-pcf8574/src/pcf8574.py"] 4 | ], 5 | "deps": [], 6 | "version": "1.1.0" 7 | } 8 | -------------------------------------------------------------------------------- /src/pcf8574.py: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2019 Mike Causer 2 | # SPDX-License-Identifier: MIT 3 | 4 | """ 5 | MicroPython PCF8574 8-Bit I2C I/O Expander with Interrupt 6 | https://github.com/mcauser/micropython-pcf8574 7 | """ 8 | 9 | __version__ = "1.1.0" 10 | 11 | 12 | class PCF8574: 13 | def __init__(self, i2c, address=0x20): 14 | self._i2c = i2c 15 | self._address = address 16 | self._port = bytearray(1) 17 | 18 | def check(self): 19 | if self._i2c.scan().count(self._address) == 0: 20 | raise OSError(f"PCF8574 not found at I2C address {self._address:#x}") 21 | return True 22 | 23 | @property 24 | def port(self): 25 | self._read() 26 | return self._port[0] 27 | 28 | @port.setter 29 | def port(self, value): 30 | self._port[0] = value & 0xFF 31 | self._write() 32 | 33 | def pin(self, pin, value=None): 34 | pin = self._validate_pin(pin) 35 | if value is None: 36 | self._read() 37 | return (self._port[0] >> pin) & 1 38 | if value: 39 | self._port[0] |= 1 << (pin) 40 | else: 41 | self._port[0] &= ~(1 << (pin)) 42 | self._write() 43 | 44 | def toggle(self, pin): 45 | pin = self._validate_pin(pin) 46 | self._port[0] ^= 1 << (pin) 47 | self._write() 48 | 49 | def _validate_pin(self, pin): 50 | # pin valid range 0..7 51 | if not 0 <= pin <= 7: 52 | raise ValueError(f"Invalid pin {pin}. Use 0-7.") 53 | return pin 54 | 55 | def _read(self): 56 | self._i2c.readfrom_into(self._address, self._port) 57 | 58 | def _write(self): 59 | self._i2c.writeto(self._address, self._port) 60 | --------------------------------------------------------------------------------