Source code for pylibftdi.bitbang
"""
pylibftdi - python wrapper for libftdi
Copyright (c) 2010-2014 Ben Bass <benbass@codedstructure.net>
See LICENSE file for details and (absence of) warranty
pylibftdi: https://github.com/codedstructure/pylibftdi
"""
from ctypes import byref, c_ubyte
from pylibftdi._base import FtdiError
from pylibftdi.device import Device
from pylibftdi.driver import BITMODE_BITBANG
ALL_OUTPUTS = 0xFF
ALL_INPUTS = 0x00
BB_OUTPUT = 1
BB_INPUT = 0
[docs]
class BitBangDevice(Device):
"""
simple subclass to support bit-bang mode
Internally uses async mode at the moment, but provides a 'sync'
flag (defaulting to True) which controls the behaviour of port
reading and writing - if set, the FIFOs are ignored (read) or
cleared (write) so operations will appear synchronous
Adds three read/write properties to the base class:
direction: 8 bit input(0)/output(1) direction control.
port: 8 bit IO port, as defined by direction.
latch: 8 bit output value, allowing e.g. `bb.latch += 1` to make sense
when there is a mix of input and output lines
"""
[docs]
def __init__(
self,
device_id=None,
direction=ALL_OUTPUTS,
lazy_open=False,
sync=True,
bitbang_mode=BITMODE_BITBANG,
interface_select=None,
**kwargs,
):
# initialise the super-class, but don't open yet. We really want
# two-part initialisation here - set up all the instance variables
# here in the super class, then open it after having set more
# of our own variables.
super().__init__(
device_id=device_id,
mode="b",
lazy_open=True,
interface_select=interface_select,
**kwargs,
)
self.direction = direction
self.sync = sync
self.bitbang_mode = bitbang_mode
self._last_set_dir = None
# latch is the latched state of output pins.
# it is initialised to the read value of the pins
# 'and'ed with those bits set to OUTPUT (1)
self._latch = None
if not lazy_open:
self.open()
[docs]
def open(self):
"""open connection to a FTDI device"""
# in case someone sets the direction before we are open()ed,
# we intercept this call...
super().open()
if self.direction != self._last_set_dir:
self.direction = self._direction
return self
[docs]
def read_pins(self):
"""
read the current 'actual' state of the pins
:return: 8-bit binary representation of pin state
:rtype: int
"""
pin_byte = c_ubyte()
res = self.ftdi_fn.ftdi_read_pins(byref(pin_byte))
if res != 0:
raise FtdiError("Could not read device pins")
return pin_byte.value
@property
def latch(self):
"""
latch property - the output latch (in-memory representation
of output pin state)
Note _latch is not masked by direction (except on initialisation),
as otherwise a loop incrementing a mixed input/output port would
not work, as it would 'stop' on input pins. This is the primary
use case for 'latch'. It's basically a `port` which ignores input.
:return: the state of the output latch
"""
if self._latch is None:
self._latch = self.read_pins() & self.direction
return self._latch
@latch.setter
def latch(self, value):
self.port = value # this updates ._latch implicitly
# direction property - 8 bit value determining whether an IO line
# is output (if set to 1) or input (set to 0)
@property
def direction(self):
"""
get or set the direction of each of the IO lines. LSB=D0, MSB=D7
1 for output, 0 for input
"""
return self._direction
@direction.setter
def direction(self, new_dir):
if not (0 <= new_dir <= 255):
raise FtdiError("invalid direction bitmask")
self._direction = new_dir
if not self.closed:
self.ftdi_fn.ftdi_set_bitmode(new_dir, self.bitbang_mode)
self._last_set_dir = new_dir
# port property - 8 bit read/write value
@property
def port(self):
"""
get or set the state of the IO lines. The value of output
lines is persisted in this object for the purposes of reading,
so read-modify-write operations (e.g. drv.port+=1) are valid.
"""
if self._direction == ALL_OUTPUTS:
# a minor optimisation; no point reading from the port if
# we have no input lines set
result = self.latch
else:
if self.sync:
result = self.read_pins()
else:
result = self.read(1)[0]
# replace the 'output' bits with current value of self.latch -
# the last written value. This makes read-modify-write
# operations (e.g. 'drv.port |= 0x10') work as expected
result = (result & ~self._direction) | ( # read input
self.latch & self._direction
) # output latch
return result
@port.setter
def port(self, value):
# restrict to a single byte
value &= 0xFF
self._latch = value
if self.sync:
self.flush_output()
# note to_bytes() gets these as default args in Python3.11+
return super().write(value.to_bytes(1, "big"))