diff --git a/CHANGELOG b/CHANGELOG index c168fc5..c64ad9c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,22 +3,11 @@ Change Log v4.0.0 ------ -- `pifacecommon.core.DigitalPort` now accepts an optional `toggle` and `mask`. `toggle` describes - which bits are toggled before given to the user and `mask` describes which - bits are allowed through. `toggle` defaults to 0x00 and `mask` defaults to - 0xff. -- `pifacecommon.core.DigitalItem` accepts an optional `toggle` argument which - defaults to 0. -- `pifacecommon.core.DigitalInputPort` and `pifacecommon.core.DigitalInputItem` - no longer default to toggling the inputs. The value is dependent on the - toggle and mask provided by the user. - **You now have to manually specify the toggle and mask if you want the bits - negated/masked.** -- `pifacecommon.interrupts.enable_interrupts` can be given a pin mask and a - list of board numbers to enable the interrupts rather than just enabling all - interrupts on all boards. -- Deactivating interrupts now waits for dispatcher process after calling - terminate. Dispatcher processes ignore KeyboardInterrupt (fix for pifacedigital issue #8). +- Ignored "Interrupted system call" error in `watch_port_events`. +- Rewrite main functions into chip specific (MCP23S17) class. +- GPIOInterruptDevice class replacing core GPIO enable/disable functions. +- SPIDevice class replacing spisend function. Can now add in spi_callback + function which is called before each SPI write. - Updated installation instructions. diff --git a/README.md b/README.md index 5cd9163..8383d68 100644 --- a/README.md +++ b/README.md @@ -31,5 +31,3 @@ can be done with the lastest version of `raspi-config`. Run: $ sudo raspi-config Then navigate to `Advanced Options`, `SPI` and select `yes`. - -You may need to reboot. diff --git a/docs/example.rst b/docs/example.rst index aa872c6..ba5f3bd 100644 --- a/docs/example.rst +++ b/docs/example.rst @@ -1,14 +1,17 @@ ####### Example ####### -Here are some examples of how to use pifacecommon:: +Here are some examples of how to use pifacecommon. - $ python3 - >>> import pifacecommon - >>> pifacecommon.core.init() - >>> pifacecommon.core.write(0xAA, pifacecommon.core.GPIOA) - >>> pifacecommon.core.read(pifacecommon.core.GPIOA) - 0xAA - >>> pifacecommon.core.write_bit(1, 0, pifacecommon.core.GPIOA) - >>> pifacecommon.core.read_bit(0, pifacecommon.core.GPIOA) +MCP23S17 +======== + +:: + + >>> import pifacecommon.mcp23s17 + >>> mcp = pifacecommon.mcp23s17.MCP23S17() + >>> mcp.gpioa.value = 0xAA + >>> mcp.gpioa.value + 170 + >>> mcp.gpioa.bits[3].value 1 diff --git a/docs/installation.rst b/docs/installation.rst index 8f61250..db606fa 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -4,15 +4,13 @@ Installation Install ======= - +apt-get +------- Make sure you are using the lastest version of Raspbian:: $ sudo apt-get update $ sudo apt-get upgrade -Automatically -------------- - Install ``pifacecommon`` (for Python 3 and 2) with the following command:: $ sudo apt-get install python{,3}-pifacecommon diff --git a/docs/reference.rst b/docs/reference.rst index 82c3284..c15bd51 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -8,8 +8,22 @@ Core .. automodule:: pifacecommon.core :members: +********** +SPI +********** +.. automodule:: pifacecommon.spi + :members: + ********** Interrupts ********** .. automodule:: pifacecommon.interrupts :members: + +********** +MCP23S17 +********** +Consult the datasheet for more information. + +.. automodule:: pifacecommon.mcp23s17 + :members: diff --git a/pifacecommon/__init__.py b/pifacecommon/__init__.py index fff31af..501293c 100644 --- a/pifacecommon/__init__.py +++ b/pifacecommon/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Provides common I/O methods for interfacing with PiFace Products Copyright (C) 2013 Thomas Preston @@ -15,77 +14,3 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -# import sys - -# constants -# from .core import ( -# MAX_BOARDS, -# IODIRA, -# IODIRB, -# IPOLA, -# IPOLB, -# GPINTENA, -# GPINTENB, -# DEFVALA, -# DEFVALB, -# INTCONA, -# INTCONB, -# IOCON, -# GPPUA, -# GPPUB, -# INTFA, -# INTFB, -# INTCAPA, -# INTCAPB, -# GPIOA, -# GPIOB, -# BANK_OFF, -# BANK_ON, -# INT_MIRROR_ON, -# INT_MIRROR_OFF, -# SEQOP_OFF, -# SEQOP_ON, -# DISSLW_ON, -# DISSLW_OFF, -# HAEN_ON, -# HAEN_OFF, -# ODR_ON, -# ODR_OFF, -# INTPOL_HIGH, -# INTPOL_LOW, -# ) - -# classes -# from .core import ( -# DigitalPort, -# DigitalInputPort, -# DigitalOutputPort, -# DigitalItem, -# DigitalInputItem, -# DigitalOutputItem, -# ) - -# from .interrupts import ( -# InputFunctionMap, -# ) - -# functions -# from .core import ( -# init, -# deinit, -# get_bit_mask, -# get_bit_num, -# read_bit, -# write_bit, -# read, -# write, -# spisend, -# sleep_microseconds, -# ) - -# from .interrupts import ( -# wait_for_interrupt, -# clear_interrupts, -# enable_interrupts, -# disable_interrupts, -# ) diff --git a/pifacecommon/core.py b/pifacecommon/core.py index 3c85ffc..d3730e4 100644 --- a/pifacecommon/core.py +++ b/pifacecommon/core.py @@ -1,283 +1,19 @@ -import sys -import ctypes -import posix -import time -from fcntl import ioctl -from .linux_spi_spidev import spi_ioc_transfer, SPI_IOC_MESSAGE - - -MAX_BOARDS = 4 - -WRITE_CMD = 0 -READ_CMD = 1 - -# Register addresses -IODIRA = 0x0 # I/O direction A -IODIRB = 0x1 # I/O direction B -IPOLA = 0x2 # I/O polarity A -IPOLB = 0x3 # I/O polarity B -GPINTENA = 0x4 # interupt enable A -GPINTENB = 0x5 # interupt enable B -DEFVALA = 0x6 # register default value A (interupts) -DEFVALB = 0x7 # register default value B (interupts) -INTCONA = 0x8 # interupt control A -INTCONB = 0x9 # interupt control B -IOCON = 0xA # I/O config (also 0xB) -GPPUA = 0xC # port A pullups -GPPUB = 0xD # port B pullups -INTFA = 0xE # interupt flag A (where the interupt came from) -INTFB = 0xF # interupt flag B -INTCAPA = 0x10 # interupt capture A (value at interupt is saved here) -INTCAPB = 0x11 # interupt capture B -GPIOA = 0x12 # port A -GPIOB = 0x13 # port B - -# I/O config -BANK_OFF = 0x00 # addressing mode -BANK_ON = 0x80 -INT_MIRROR_ON = 0x40 # interupt mirror (INTa|INTb) -INT_MIRROR_OFF = 0x00 -SEQOP_OFF = 0x20 # incrementing address pointer -SEQOP_ON = 0x00 -DISSLW_ON = 0x10 # slew rate -DISSLW_OFF = 0x00 -HAEN_ON = 0x08 # hardware addressing -HAEN_OFF = 0x00 -ODR_ON = 0x04 # open drain for interupts -ODR_OFF = 0x00 -INTPOL_HIGH = 0x02 # interupt polarity -INTPOL_LOW = 0x00 - -SPIDEV = '/dev/spidev' -SPI_HELP_LINK = "http://piface.github.io/pifacecommon/installation.html" \ - "#enable-the-spi-module" - -spidev_fd = None - - -class RangeError(Exception): - pass - - -class InitError(Exception): - pass - - -class InputDeviceError(Exception): - pass - - -class DigitalPort(object): - """A digital port on a PiFace product. - - :param port: The port. - :type port: pifacecommon.core.GPPUA, pifacecommon.core.GPPUA - :param board_num: The board number. - :type board_num: int - :param bit_mask: The bit mask defines which bits are cleared. - Example: 0xF0 clears lowest nibble. - :type bit_mask: int - :param toggle_mask: The toggle mask defines which bits are toggled. - Example: 0xF0 toggles the upper nibble. - :type toggle_mask: int - """ - - def __init__(self, port, board_num=0, bit_mask=0xFF, toggle_mask=0x00): - self.port = port - if board_num < 0 or board_num >= MAX_BOARDS: - raise RangeError( - "Specified board index (%d) out of range." % board_num - ) - else: - self.board_num = board_num - self.bit_mask = bit_mask - self.toggle_mask = toggle_mask - - @property - def handler(self): - """The module that handles this port (can be useful for - emulator/testing). - """ - return sys.modules[__name__] - - @property - def value(self): - """The value of the digital port.""" - return self.bit_mask & ( - self.toggle_mask ^ self.handler.read(self.port, self.board_num)) - - @value.setter - def value(self, data): - return self.handler.write( - self.bit_mask & (data ^ self.toggle_mask), - self.port, - self.board_num) - - -class DigitalInputPort(DigitalPort): - """An digital input port on a PiFace product. - - .. note:: This hides the fact that inputs are active low. - - >>> input_port = pifacecommon.core.GPIOB - >>> hex(pifacecommon.core.read(input_port)) - '0xFF' - >>> hex(pifacecommon.core.DigitalInputPort(input_port).value) - '0x00' - """ - # def __init__(self, port, board_num=0): - # super(DigitalInputPort, self).__init__(port, board_num) - - # # redefine value property for input - # @property - # def value(self): - # """The value of the digital input port.""" - # return 0xFF ^ self.handler.read(self.port, self.board_num) - - @DigitalPort.value.setter - def value(self, data): - raise InputDeviceError("You cannot set an input's values!") - - -class DigitalOutputPort(DigitalPort): - """An digital output port on a PiFace product""" - # def __init__(self, port, board_num=0): - # super(DigitalOutputPort, self).__init__(port, board_num) - - def all_on(self): - """Turns all outputs on.""" - self.value = 0xFF - - def all_off(self): - """Turns all outputs off.""" - self.value = 0 - - def toggle(self): - """Toggles all outputs.""" - self.value = 0xFF ^ self.value - - -class DigitalItem(DigitalPort): - """A digital item connected to a pin on a PiFace product. - Has most of the same properties of a Digital Port. - - :param port: The port. - :type port: pifacecommon.core.GPPUA, pifacecommon.core.GPPUA - :param board_num: The board number. - :type board_num: int - :param toggle_mask: The toggle mask defines if the value is toggled. - Example: 1 toggles the bit value. - :type toggle_mask: int - """ - - def __init__(self, pin_num, port, board_num=0, toggle_mask=0): - super(DigitalItem, self).__init__(port, board_num) - self.pin_num = pin_num - self.toggle_mask = toggle_mask - - @property - def value(self): - """The value of the digital item.""" - return self.toggle_mask ^ self.handler.read_bit( - self.pin_num, - self.port, - self.board_num) - - @value.setter - def value(self, data): - return self.handler.write_bit( - self.toggle_mask ^ data, - self.pin_num, - self.port, - self.board_num) - - -class DigitalInputItem(DigitalItem): - """An digital input connected to a pin a PiFace product.""" - # def __init__(self, pin_num, port, board_num=0): - # super(DigitalInputItem, self).__init__(pin_num, port, board_num) - - # redefine value property for input - # @property - # def value(self): - # """The inverse value of the digital input item (inputs are active low - # """ - # return 1 ^ self.handler.read_bit( - # self.pin_num, - # self.port, - # self.board_num) - - @DigitalItem.value.setter - def value(self, data): - raise InputDeviceError("You cannot set an input's values!") - - -class DigitalOutputItem(DigitalItem): - """An output connected to a pin a PiFace Digital product.""" - # def __init__(self, pin_num, port, board_num=0): - # super(DigitalOutputItem, self).__init__(pin_num, port, board_num) - - def turn_on(self): - """Sets the digital output item's value to 1.""" - self.value = 1 - - def turn_off(self): - """Sets the digital output item's value to 0.""" - self.value = 0 - - def toggle(self): - """Toggles the digital output item's value.""" - self.value = not self.value - - -def init(bus=0, chip_select=0): - """Initialises the SPI device file descriptor. - - :param bus: The SPI device bus number - :type bus: int - :param chip_select: The SPI device chip_select number - :param chip_select: int - :raises: InitError - """ - spi_device = "%s%d.%d" % (SPIDEV, bus, chip_select) - global spidev_fd - try: - spidev_fd = posix.open(spi_device, posix.O_RDWR) - except OSError as e: - raise InitError( - "I can't see %s. Have you enabled the SPI module? (%s)" - % (spi_device, SPI_HELP_LINK) - ) # from e # from is only available in Python 3 - - -def deinit(): - """Closes the SPI device file descriptor.""" - global spidev_fd - if spidev_fd: - posix.close(spidev_fd) - spidev_fd = None - - def get_bit_mask(bit_num): - """Translates a bit num to bit mask. + """Returns as bit mask with bit_num set. :param bit_num: The bit number. :type bit_num: int :returns: int -- the bit mask :raises: RangeError - >>> pifacecommon.core.get_bit_mask(0) + >>> bin(pifacecommon.core.get_bit_mask(0)) 1 >>> pifacecommon.core.get_bit_mask(1) 2 >>> bin(pifacecommon.core.get_bit_mask(3)) '0b1000' """ - if bit_num > 7 or bit_num < 0: - raise RangeError( - "Specified bit num (%d) out of range (0-7)." % bit_num) - else: - return 1 << (bit_num) + return 1 << (bit_num) def get_bit_num(bit_pattern): @@ -310,138 +46,6 @@ def get_bit_num(bit_pattern): return bit_num -def read_bit(bit_num, address, board_num=0): - """Returns the bit specified from the address. - - :param bit_num: The bit number to read from. - :type bit_num: int - :param address: The address to read from. - :type address: int - :param board_num: The board number to read from. - :type board_num: int - :returns: int -- the bit value from the address - """ - value = read(address, board_num) - bit_mask = get_bit_mask(bit_num) - return 1 if value & bit_mask else 0 - - -def write_bit(value, bit_num, address, board_num=0): - """Writes the value given to the bit in the address specified. - - :param value: The value to write. - :type value: int - :param bit_num: The bit number to write to. - :type bit_num: int - :param address: The address to write to. - :type address: int - :param board_num: The board number to write to. - :type board_num: int - """ - bit_mask = get_bit_mask(bit_num) - old_byte = read(address, board_num) - # generate the new byte - if value: - new_byte = old_byte | bit_mask - else: - new_byte = old_byte & ~bit_mask - write(new_byte, address, board_num) - - -def __get_device_opcode(board_num, read_write_cmd): - """Returns the device opcode (as a byte). - - :param board_num: The board number to generate the opcode from. - :type board_num: int - :param read_write_cmd: Read or write command. - :type read_write_cmd: int - """ - board_addr_pattern = (board_num << 1) & 0xE # 1 -> 0b0010, 3 -> 0b0110 - rw_cmd_pattern = read_write_cmd & 1 # make sure it's just 1 bit long - return 0x40 | board_addr_pattern | rw_cmd_pattern - - -def read(address, board_num=0): - """Returns the value of the address specified. - - :param address: The address to read from. - :type address: int - :param board_num: The board number to read from. - :type board_num: int - """ - return _pyver_read(address, board_num) - - -def _py3read(address, board_num): - devopcode = __get_device_opcode(board_num, READ_CMD) - op, addr, data = spisend(bytes((devopcode, address, 0))) - return data - - -def _py2read(address, board_num): - devopcode = __get_device_opcode(board_num, READ_CMD) - op, addr, data = spisend(chr(devopcode)+chr(address)+chr(0)) - return ord(data) - - -def write(data, address, board_num=0): - """Writes data to the address specified. - - :param data: The data to write. - :type data: int - :param address: The address to write to. - :type address: int - :param board_num: The board number to write to. - :type board_num: int - """ - _pyver_write(data, address, board_num) - - -def _py3write(data, address, board_num): - devopcode = __get_device_opcode(board_num, WRITE_CMD) - spisend(bytes((devopcode, address, data))) - - -def _py2write(data, address, board_num): - devopcode = __get_device_opcode(board_num, WRITE_CMD) - spisend(chr(devopcode)+chr(address)+chr(data)) - - -# Python 2 support -PY3 = sys.version_info.major >= 3 -_pyver_read = _py3read if PY3 else _py2read -_pyver_write = _py3write if PY3 else _py2write - - -def spisend(bytes_to_send): - """Sends bytes via the SPI bus. - - :param bytes_to_send: The bytes to send on the SPI device. - :type bytes_to_send: bytes - :returns: bytes -- returned bytes from SPI device - :raises: InitError - """ - global spidev_fd - if spidev_fd is None: - raise InitError("Before spisend(), call init().") - - # make some buffer space to store reading/writing - wbuffer = ctypes.create_string_buffer(bytes_to_send, len(bytes_to_send)) - rbuffer = ctypes.create_string_buffer(len(bytes_to_send)) - #rbuffer = ctypes.create_string_buffer(size=len(bytes_to_send)) should be - - # create the spi transfer struct - transfer = spi_ioc_transfer( - tx_buf=ctypes.addressof(wbuffer), - rx_buf=ctypes.addressof(rbuffer), - len=ctypes.sizeof(wbuffer) - ) - - # send the spi command - ioctl(spidev_fd, SPI_IOC_MESSAGE(1), transfer) - return ctypes.string_at(rbuffer, ctypes.sizeof(rbuffer)) - - def sleep_microseconds(microseconds): """Sleeps for the given number of microseconds. diff --git a/pifacecommon/interrupts.py b/pifacecommon/interrupts.py index 3bda403..5c4cba2 100644 --- a/pifacecommon/interrupts.py +++ b/pifacecommon/interrupts.py @@ -2,20 +2,8 @@ import multiprocessing import select import time -from .core import ( - MAX_BOARDS, - GPIOA, - GPIOB, - GPINTENA, - GPINTENB, - INTFA, - INTFB, - INTCAPA, - INTCAPB, - get_bit_num, - read, - write, -) +from .core import get_bit_num +import pifacecommon.mcp23s17 # interrupts @@ -46,14 +34,30 @@ class Timeout(Exception): class InterruptEvent(object): - """An interrupt event.""" + """An interrupt event containting the interrupt flag and capture register + values, the chip object from which the interrupt occured and a timestamp. + """ def __init__( - self, interrupt_flag, interrupt_capture, board_num, timestamp): + self, interrupt_flag, interrupt_capture, chip, timestamp): self.interrupt_flag = interrupt_flag self.interrupt_capture = interrupt_capture - self.board_num = board_num + self.chip = chip self.timestamp = timestamp + def __str__(self): + s = "interrupt_flag: {flag}\n" \ + "interrupt_capture: {capture}\n" \ + "pin_num: {pin_num}\n" \ + "direction: {direction}\n" \ + "chip: {chip}\n" \ + "timestamp: {timestamp}" + return s.format(flag=bin(self.interrupt_flag), + capture=bin(self.interrupt_capture), + pin_num=self.pin_num, + direction=self.direction, + chip=self.chip, + timestamp=self.timestamp) + @property def pin_num(self): return get_bit_num(self.interrupt_flag) @@ -79,6 +83,12 @@ def __init__(self, pin_num, direction, callback, settle_time): self.direction = direction super(PinFunctionMap, self).__init__(callback, settle_time) + def __str__(self): + s = "Pin num: {pin_num}\n" \ + "Direction:{direction}\n" + return s.format(pin_num=self.pin_num, + direction=self.direction) + class EventQueue(object): """Stores events in a queue.""" @@ -93,14 +103,22 @@ def add_event(self, event): settle time for that pin/direction. Such events are assumed to be bouncing. """ + # print("Trying to add event:") + # print(event) # find out the pin settle time for pin_function_map in self.pin_function_maps: - if pin_function_map.pin_num == event.pin_num and \ - pin_function_map.direction == event.direction: + if _event_matches_pin_function_map(event, pin_function_map): + # if pin_function_map.pin_num == event.pin_num and ( + # pin_function_map.direction == event.direction or + # pin_function_map.direction == IODIR_BOTH): pin_settle_time = pin_function_map.settle_time + # print("EventQueue: Found event in map.") break else: # Couldn't find event in map, don't bother adding it to the queue + # print("EventQueue: Couldn't find event in map:") + # for pin_function_map in self.pin_function_maps: + # print(pin_function_map) return threshold_time = self.last_event_time[event.pin_num] + pin_settle_time @@ -121,7 +139,7 @@ class PortEventListener(object): >>> def print_flag(event): ... print(event.interrupt_flag) ... - >>> port = pifacecommon.core.GPIOA + >>> port = pifacecommon.mcp23s17.GPIOA >>> listener = pifacecommon.interrupts.PortEventListener(port) >>> listener.register(0, pifacecommon.interrupts.IODIR_ON, print_flag) >>> listener.activate() @@ -129,19 +147,19 @@ class PortEventListener(object): TERMINATE_SIGNAL = "astalavista" - def __init__(self, port, board_num=0, ignore_keyboard_interrupt=True): + def __init__(self, port, chip, return_after_kbdint=True): self.port = port - self.board_num = board_num + self.chip = chip self.pin_function_maps = list() self.event_queue = EventQueue(self.pin_function_maps) self.detector = multiprocessing.Process( target=watch_port_events, args=( self.port, - self.board_num, + self.chip, self.pin_function_maps, self.event_queue, - ignore_keyboard_interrupt)) + return_after_kbdint)) self.dispatcher = threading.Thread( target=handle_events, args=( @@ -184,22 +202,43 @@ def deactivate(self): self.detector.join() +class GPIOInterruptDevice(object): + """A device that interrupts using the GPIO pins.""" + def gpio_interrupts_enable(self): + """Enables GPIO interrupts.""" + try: + bring_gpio_interrupt_into_userspace() + set_gpio_interrupt_edge() + except Timeout as e: + raise InterruptEnableException( + "There was an error bringing gpio%d into userspace. %s" % + (GPIO_INTERRUPT_PIN, e.message) + ) + + def gpio_interrupts_disable(self): + """Disables gpio interrupts.""" + set_gpio_interrupt_edge('none') + deactivate_gpio_interrupt() + + def _event_matches_pin_function_map(event, pin_function_map): + # print("pin num", event.pin_num, pin_function_map.pin_num) + # print("direction", event.direction, pin_function_map.direction) pin_match = event.pin_num == pin_function_map.pin_num direction_match = pin_function_map.direction is None direction_match |= event.direction == pin_function_map.direction return pin_match and direction_match -def watch_port_events(port, board_num, pin_function_maps, event_queue, - ignore_keyboard_interrupt=False): +def watch_port_events(port, chip, pin_function_maps, event_queue, + return_after_kbdint=False): """Waits for a port event. When a port event occurs it is placed onto the event queue. :param port: The port we are waiting for interrupts on (GPIOA/GPIOB). :type port: int - :param board_num: The board we are waiting for interrupts on. - :type board_num: int + :param chip: The chip we are waiting for interrupts on. + :type chip: :class:`pifacecommon.mcp23s17.MCP23S17` :param pin_function_maps: A list of classes that have inheritted from :class:`FunctionMap`\ s describing what to do with events. :type pin_function_maps: list @@ -211,29 +250,36 @@ def watch_port_events(port, board_num, pin_function_maps, event_queue, epoll = select.epoll() epoll.register(gpio25, select.EPOLLIN | select.EPOLLET) - # a bit map showing what caused the interrupt - intflag = INTFA if port == GPIOA else INTFB - # a snapshot of the port when interrupt occured - intcapture = INTCAPA if port == GPIOA else INTCAPB - while True: # wait here until input try: events = epoll.poll() except KeyboardInterrupt as e: - if ignore_keyboard_interrupt: - pass + if return_after_kbdint: + return else: raise e + except IOError as e: + # ignore "Interrupted system call" error. + # I don't really like this solution. Ignoring problems is bad! + if e.errno != errno.EINTR: + raise # find out where the interrupt came from and put it on the event queue - interrupt_flag = read(intflag, board_num) + if port == pifacecommon.mcp23s17.GPIOA: + interrupt_flag = chip.intfa.value + else: + interrupt_flag = chip.intfb.value + if interrupt_flag == 0: continue # The interrupt has not been flagged on this board else: - interrupt_capture = read(intcapture, board_num) + if port == pifacecommon.mcp23s17.GPIOA: + interrupt_capture = chip.intcapa.value + else: + interrupt_capture = chip.intcapb.value event_queue.add_event(InterruptEvent( - interrupt_flag, interrupt_capture, board_num, time.time())) + interrupt_flag, interrupt_capture, chip, time.time())) epoll.close() @@ -255,7 +301,9 @@ def handle_events( causes this function to exit. """ while True: + # print("HANDLE: Waiting for events!") event = event_queue.get() + # print("HANDLE: It's an event!") if event == terminate_signal: return # if matching get the callback function, else function is None @@ -271,44 +319,45 @@ def handle_events( function(event) -def clear_interrupts(port): - """Clears the interrupt flags by 'read'ing the capture register - on all boards. - """ - intcap = INTCAPA if port == GPIOA else INTCAPB - for i in range(MAX_BOARDS): - read(intcap, i) - - -def enable_interrupts(port, pin_map=0xff, board_nums=range(MAX_BOARDS)): - """Enables interrupts on the port specified. A pin map can be given to - only enable interrupts on some pins. A list of board numbers can also be - given to only enable the interrupts on some boards. - - :param port: The port to enable interrupts on - (pifacecommon.core.GPIOA, pifacecommon.core.GPIOB) - :type port: int - :param pin_map: The pins to enable interrupts on - :type pin_map: int - :param board_nums: The boards to enable interrupts on. - :type board_nums: list - """ - # enable interrupts - int_enable_port = GPINTENA if port == GPIOA else GPINTENB - for board_index in board_nums: - write(pin_map, int_enable_port, board_index) - - try: - _bring_gpio_interrupt_into_userspace() - _set_gpio_interrupt_edge() - except Timeout as e: - raise InterruptEnableException( - "There was an error bringing gpio%d into userspace. %s" % - (GPIO_INTERRUPT_PIN, e.message) - ) - - -def _bring_gpio_interrupt_into_userspace(): +# def clear_interrupts(port): +# """Clears the interrupt flags by 'read'ing the capture register +# on all boards. +# """ +# intcap = INTCAPA if port == GPIOA else INTCAPB +# for i in range(MAX_BOARDS): +# read(intcap, i) + + +# def enable_interrupts(port, pin_map=0xff, hardware_addrs=range(MAX_BOARDS)): +# """Enables interrupts on the port specified. A pin map can be given to +# only enable interrupts on some pins. A list of board numbers can also be +# given to only enable the interrupts on some boards. + +# :param port: The port to enable interrupts on +# (pifacecommon.core.GPIOA, pifacecommon.core.GPIOB) +# :type port: int +# :param pin_map: The pins to enable interrupts on +# :type pin_map: int +# :param hardware_addrs: The boards to enable interrupts on. +# :type hardware_addrs: list +# """ +# # enable interrupts +# int_enable_port = GPINTENA if port == GPIOA else GPINTENB +# for board_index in hardware_addrs: +# write(pin_map, int_enable_port, board_index) + +# try: +# bring_gpio_interrupt_into_userspace() +# set_gpio_interrupt_edge() +# except Timeout as e: +# raise InterruptEnableException( +# "There was an error bringing gpio%d into userspace. %s" % +# (GPIO_INTERRUPT_PIN, e.message) +# ) + + +def bring_gpio_interrupt_into_userspace(): # activate gpio interrupt + """Bring the interrupt pin on the GPIO into Linux userspace.""" try: # is it already there? with open(GPIO_INTERRUPT_DEVICE_VALUE): @@ -318,23 +367,39 @@ def _bring_gpio_interrupt_into_userspace(): with open(GPIO_EXPORT_FILE, 'w') as export_file: export_file.write(str(GPIO_INTERRUPT_PIN)) - _wait_until_file_exists(GPIO_INTERRUPT_DEVICE_VALUE) + wait_until_file_exists(GPIO_INTERRUPT_DEVICE_VALUE) + + +def deactivate_gpio_interrupt(): + """Remove the GPIO interrupt pin from Linux userspace.""" + with open(GPIO_UNEXPORT_FILE, 'w') as unexport_file: + unexport_file.write(str(GPIO_INTERRUPT_PIN)) -def _set_gpio_interrupt_edge(): +def set_gpio_interrupt_edge(edge='falling'): + """Set the interrupt edge on the userspace GPIO pin. + + :param edge: The interrupt edge ('none', 'falling', 'rising'). + :type edge: string + """ # we're only interested in the falling edge (1 -> 0) start_time = time.time() time_limit = start_time + FILE_IO_TIMEOUT while time.time() < time_limit: try: with open(GPIO_INTERRUPT_DEVICE_EDGE, 'w') as gpio_edge: - gpio_edge.write('falling') + gpio_edge.write(edge) return except IOError: pass -def _wait_until_file_exists(filename): +def wait_until_file_exists(filename): + """Wait until a file exists. + + :param filename: The name of the file to wait for. + :type filename: string + """ start_time = time.time() time_limit = start_time + FILE_IO_TIMEOUT while time.time() < time_limit: @@ -347,22 +412,22 @@ def _wait_until_file_exists(filename): raise Timeout("Waiting too long for %s." % filename) -def disable_interrupts(port): - """Disables interrupts for all pins on the port specified. +# def disable_interrupts(port): +# """Disables interrupts for all pins on the port specified. - :param port: The port to enable interrupts on - (pifacecommon.core.GPIOA, pifacecommon.core.GPIOB) - :type port: int - """ - # neither edgez - with open(GPIO_INTERRUPT_DEVICE_EDGE, 'w') as gpio25edge: - gpio25edge.write('none') +# :param port: The port to enable interrupts on +# (pifacecommon.core.GPIOA, pifacecommon.core.GPIOB) +# :type port: int +# """ +# # neither edgez +# with open(GPIO_INTERRUPT_DEVICE_EDGE, 'w') as gpio25edge: +# gpio25edge.write('none') - # remove the pin from userspace - with open(GPIO_UNEXPORT_FILE, 'w') as unexport_file: - unexport_file.write(str(GPIO_INTERRUPT_PIN)) +# # remove the pin from userspace +# with open(GPIO_UNEXPORT_FILE, 'w') as unexport_file: +# unexport_file.write(str(GPIO_INTERRUPT_PIN)) - # disable the interrupt - int_enable_port = GPINTENA if port == GPIOA else GPINTENB - for board_index in range(MAX_BOARDS): - write(0, int_enable_port, board_index) +# # disable the interrupt +# int_enable_port = GPINTENA if port == GPIOA else GPINTENB +# for board_index in range(MAX_BOARDS): +# write(0, int_enable_port, board_index) diff --git a/pifacecommon/mcp23s17.py b/pifacecommon/mcp23s17.py new file mode 100644 index 0000000..a4aebc6 --- /dev/null +++ b/pifacecommon/mcp23s17.py @@ -0,0 +1,384 @@ +import os +import sys +from .core import ( + get_bit_mask, + get_bit_num, +) +from .spi import SPIDevice +import pifacecommon.interrupts + + +# Python 2 support +PY3 = sys.version_info[0] >= 3 + +WRITE_CMD = 0 +READ_CMD = 1 + +# Register addresses +IODIRA = 0x0 # I/O direction A +IODIRB = 0x1 # I/O direction B +IPOLA = 0x2 # I/O polarity A +IPOLB = 0x3 # I/O polarity B +GPINTENA = 0x4 # interupt enable A +GPINTENB = 0x5 # interupt enable B +DEFVALA = 0x6 # register default value A (interupts) +DEFVALB = 0x7 # register default value B (interupts) +INTCONA = 0x8 # interupt control A +INTCONB = 0x9 # interupt control B +IOCON = 0xA # I/O config (also 0xB) +GPPUA = 0xC # port A pullups +GPPUB = 0xD # port B pullups +INTFA = 0xE # interupt flag A (where the interupt came from) +INTFB = 0xF # interupt flag B +INTCAPA = 0x10 # interupt capture A (value at interupt is saved here) +INTCAPB = 0x11 # interupt capture B +GPIOA = 0x12 # port A +GPIOB = 0x13 # port B +OLATA = 0x14 # output latch A +OLATB = 0x15 # output latch B + +# I/O config +BANK_OFF = 0x00 # addressing mode +BANK_ON = 0x80 +INT_MIRROR_ON = 0x40 # interupt mirror (INTa|INTb) +INT_MIRROR_OFF = 0x00 +SEQOP_OFF = 0x20 # incrementing address pointer +SEQOP_ON = 0x00 +DISSLW_ON = 0x10 # slew rate +DISSLW_OFF = 0x00 +HAEN_ON = 0x08 # hardware addressing +HAEN_OFF = 0x00 +ODR_ON = 0x04 # open drain for interupts +ODR_OFF = 0x00 +INTPOL_HIGH = 0x02 # interupt polarity +INTPOL_LOW = 0x00 + +LOWER_NIBBLE, UPPER_NIBBLE = range(2) + + +class MCP23S17(SPIDevice): + """Microchip's MCP23S17: A 16-Bit I/O Expander with Serial Interface. + + :attribute: iodira/iodirb -- Controls the direction of the data I/O. + :attribute: ipola/ipolb --This register allows the user to configure + the polarity on the corresponding GPIO port + bits. + :attribute: gpintena/gpintenb -- The GPINTEN register controls the + interrupt-onchange feature for each + pin. + :attribute: defvala/defvalb --The default comparison value is + configured in the DEFVAL register. + :attribute: intcona/intconb --The INTCON register controls how the + associated pin value is compared for + the interrupt-on-change feature. + :attribute: iocon --The IOCON register contains several bits for + configuring the device. + :attribute: gppua/gppub --The GPPU register controls the pull-up + resistors for the port pins. + :attribute: intfa/intfb --The INTF register reflects the interrupt + condition on the port pins of any pin that + is enabled for interrupts via the GPINTEN + register. + :attribute: intcapa/intcapb -- The INTCAP register captures the GPIO + port value at the time the interrupt + occurred. + :attribute: gpioa/gpiob -- The GPIO register reflects the value on + the port. + :attribute: olata/olatb -- The OLAT register provides access to the + output latches. + """ + def __init__(self, hardware_addr=0, bus=0, chip_select=0): + super(MCP23S17, self).__init__(bus, chip_select) + self.hardware_addr = hardware_addr + + self.iodira = MCP23S17Register(IODIRA, self) + self.iodirb = MCP23S17Register(IODIRB, self) + self.ipola = MCP23S17Register(IPOLA, self) + self.ipolb = MCP23S17Register(IPOLB, self) + self.gpintena = MCP23S17Register(GPINTENA, self) + self.gpintenb = MCP23S17Register(GPINTENB, self) + self.defvala = MCP23S17Register(DEFVALA, self) + self.defvalb = MCP23S17Register(DEFVALB, self) + self.intcona = MCP23S17Register(INTCONA, self) + self.intconb = MCP23S17Register(INTCONB, self) + self.iocon = MCP23S17Register(IOCON, self) + self.gppua = MCP23S17Register(GPPUA, self) + self.gppub = MCP23S17Register(GPPUB, self) + self.intfa = MCP23S17Register(INTFA, self) + self.intfb = MCP23S17Register(INTFB, self) + self.intcapa = MCP23S17Register(INTCAPA, self) + self.intcapb = MCP23S17Register(INTCAPB, self) + self.gpioa = MCP23S17Register(GPIOA, self) + self.gpiob = MCP23S17Register(GPIOB, self) + self.olata = MCP23S17Register(OLATA, self) + self.olatb = MCP23S17Register(OLATB, self) + + def _get_spi_control_byte(self, read_write_cmd): + """Returns an SPI control byte. + + The MCP23S17 is a slave SPI device. The slave address contains + four fixed bits and three user-defined hardware address bits + (if enabled via IOCON.HAEN) (pins A2, A1 and A0) with the + read/write bit filling out the control byte:: + + +--------------------+ + |0|1|0|0|A2|A1|A0|R/W| + +--------------------+ + 7 6 5 4 3 2 1 0 + + :param read_write_cmd: Read or write command. + :type read_write_cmd: int + """ + # board_addr_pattern = (self.hardware_addr & 0b111) << 1 + board_addr_pattern = (self.hardware_addr << 1) & 0xE + rw_cmd_pattern = read_write_cmd & 1 # make sure it's just 1 bit long + return 0x40 | board_addr_pattern | rw_cmd_pattern + + def read(self, address): + """Returns the value of the address specified. + + :param address: The address to read from. + :type address: int + """ + return self._pyver_read(address) + + def _py3read(self, address): + ctrl_byte = self._get_spi_control_byte(READ_CMD) + op, addr, data = self.spisend(bytes((ctrl_byte, address, 0))) + return data + + def _py2read(self, address): + ctrl_byte = self._get_spi_control_byte(READ_CMD) + op, addr, data = self.spisend(chr(ctrl_byte)+chr(address)+chr(0)) + return ord(data) + + def write(self, data, address): + """Writes data to the address specified. + + :param data: The data to write. + :type data: int + :param address: The address to write to. + :type address: int + """ + self._pyver_write(data, address) + + def _py3write(self, data, address): + ctrl_byte = self._get_spi_control_byte(WRITE_CMD) + self.spisend(bytes((ctrl_byte, address, data))) + + def _py2write(self, data, address): + ctrl_byte = self._get_spi_control_byte(WRITE_CMD) + self.spisend(chr(ctrl_byte)+chr(address)+chr(data)) + + # Python 2 support + _pyver_read = _py3read if PY3 else _py2read + _pyver_write = _py3write if PY3 else _py2write + + def read_bit(self, bit_num, address): + """Returns the bit specified from the address. + + :param bit_num: The bit number to read from. + :type bit_num: int + :param address: The address to read from. + :type address: int + :returns: int -- the bit value from the address + """ + value = self.read(address) + bit_mask = get_bit_mask(bit_num) + return 1 if value & bit_mask else 0 + + def write_bit(self, value, bit_num, address): + """Writes the value given to the bit in the address specified. + + :param value: The value to write. + :type value: int + :param bit_num: The bit number to write to. + :type bit_num: int + :param address: The address to write to. + :type address: int + """ + bit_mask = get_bit_mask(bit_num) + old_byte = self.read(address) + # generate the new byte + if value: + new_byte = old_byte | bit_mask + else: + new_byte = old_byte & ~bit_mask + self.write(new_byte, address) + + def clear_interrupts(self, port): + """Clears the interrupt flags by 'read'ing the capture register.""" + self.read(INTCAPA if port == GPIOA else INTCAPB) + + +class MCP23S17RegisterBase(object): + """Base class for objects on an 8-bit register inside an MCP23S17.""" + def __init__(self, address, chip): + self.address = address + self.chip = chip + + +class MCP23S17Register(MCP23S17RegisterBase): + """An 8-bit register inside an MCP23S17.""" + def __init__(self, address, chip): + super(MCP23S17Register, self).__init__(address, chip) + self.lower_nibble = MCP23S17RegisterNibble(LOWER_NIBBLE, self.address, + self.chip) + self.upper_nibble = MCP23S17RegisterNibble(UPPER_NIBBLE, self.address, + self.chip) + self.bits = [MCP23S17RegisterBit(i, self.address, self.chip) + for i in range(8)] + + @property + def value(self): + return self.chip.read(self.address) + + @value.setter + def value(self, v): + self.chip.write(v, self.address) + + def all_high(self): + self.value = 0xFF + + def all_low(self): + self.value = 0x00 + + all_on = all_high + all_off = all_low + + def toggle(self): + self.value = 0xFF ^ self.value + + +class MCP23S17RegisterNeg(MCP23S17Register): + """An negated 8-bit register inside an MCP23S17.""" + def __init__(self, address, chip): + super(MCP23S17RegisterNeg, self).__init__(address, chip) + self.lower_nibble = MCP23S17RegisterNibbleNeg(LOWER_NIBBLE, + self.address, + self.chip) + self.upper_nibble = MCP23S17RegisterNibbleNeg(UPPER_NIBBLE, + self.address, + self.chip) + self.bits = [MCP23S17RegisterBitNeg(i, self.address, self.chip) + for i in range(8)] + + @property + def value(self): + return 0xFF ^ self.chip.read(self.address) + + @value.setter + def value(self, v): + self.chip.write(0xFF ^ v, self.address) + + +class MCP23S17RegisterNibble(MCP23S17RegisterBase): + """An 4-bit nibble inside a register inside an MCP23S17.""" + def __init__(self, nibble, address, chip): + super(MCP23S17RegisterNibble, self).__init__(address, chip) + self.nibble = nibble + range_start = 0 if self.nibble == LOWER_NIBBLE else 4 + range_end = 4 if self.nibble == LOWER_NIBBLE else 8 + self.bits = [MCP23S17RegisterBit(i, self.address, self.chip) + for i in range(range_start, range_end)] + + @property + def value(self): + if self.nibble == LOWER_NIBBLE: + return self.chip.read(self.address) & 0xF + elif self.nibble == UPPER_NIBBLE: + return (self.chip.read(self.address) & 0xF0) >> 4 + + @value.setter + def value(self, v): + register_value = self.chip.read(self.address) + if self.nibble == LOWER_NIBBLE: + register_value &= 0xF0 # clear + register_value ^= (v & 0x0F) # set + elif self.nibble == UPPER_NIBBLE: + register_value &= 0x0F # clear + register_value ^= ((v << 4) & 0xF0) # set + self.chip.write(register_value, self.address) + + def all_high(self): + self.value = 0xF + + def all_low(self): + self.value = 0x0 + + all_on = all_high + all_off = all_low + + def toggle(self): + self.value = 0xF ^ self.value + + +class MCP23S17RegisterNibbleNeg(MCP23S17RegisterNibble): + """A negated 4-bit nibble inside a register inside an MCP23S17.""" + def __init__(self, nibble, address, chip): + super(MCP23S17RegisterNibbleNeg, self).__init__(nibble, address, chip) + self.nibble = nibble + range_start = 0 if self.nibble == LOWER_NIBBLE else 4 + range_end = 4 if self.nibble == LOWER_NIBBLE else 8 + self.bits = [MCP23S17RegisterBitNeg(i, self.address, self.chip) + for i in range(range_start, range_end)] + + @property + def value(self): + if self.nibble == LOWER_NIBBLE: + v = self.chip.read(self.address) & 0xF + elif self.nibble == UPPER_NIBBLE: + v = (self.chip.read(self.address) & 0xF0) >> 4 + return 0xF ^ v + + @value.setter + def value(self, v): + register_value = self.chip.read(self.address) + if self.nibble == LOWER_NIBBLE: + register_value &= 0xF0 # clear + register_value ^= (v & 0x0F ^ 0x0F) # set + elif self.nibble == UPPER_NIBBLE: + register_value &= 0x0F # clear + register_value ^= ((v << 4) & 0xF0 ^ 0xF0) # set + self.chip.write(register_value, self.address) + + +class MCP23S17RegisterBit(MCP23S17RegisterBase): + """A bit inside register inside an MCP23S17.""" + def __init__(self, bit_num, address, chip): + super(MCP23S17RegisterBit, self).__init__(address, chip) + self.bit_num = bit_num + + @property + def value(self): + return self.chip.read_bit(self.bit_num, self.address) + + @value.setter + def value(self, v): + self.chip.write_bit(v, self.bit_num, self.address) + + def set_high(self): + self.value = 1 + + def set_low(self): + self.value = 0 + + turn_on = set_high + turn_off = set_low + + def toggle(self): + self.value = 1 ^ self.value + + +class MCP23S17RegisterBitNeg(MCP23S17RegisterBit): + """A negated bit inside register inside an MCP23S17.""" + def __init__(self, bit_num, address, chip): + super(MCP23S17RegisterBitNeg, self).__init__(bit_num, address, chip) + + @property + def value(self): + return 1 ^ self.chip.read_bit(self.bit_num, self.address) + + @value.setter + def value(self, v): + self.chip.write_bit(v ^ 1, self.bit_num, self.address) diff --git a/pifacecommon/spi.py b/pifacecommon/spi.py new file mode 100644 index 0000000..7d938aa --- /dev/null +++ b/pifacecommon/spi.py @@ -0,0 +1,74 @@ +import posix +import ctypes +from fcntl import ioctl +from .linux_spi_spidev import spi_ioc_transfer, SPI_IOC_MESSAGE + + +SPIDEV = '/dev/spidev' +SPI_HELP_LINK = "http://piface.github.io/pifacecommon/installation.html" \ + "#enable-the-spi-module" + + +class SPIInitError(Exception): + pass + + +class SPIDevice(object): + """An SPI Device at /dev/spi..""" + def __init__(self, bus=0, chip_select=0, spi_callback=None): + """Initialises the SPI device file descriptor. + + :param bus: The SPI device bus number + :type bus: int + :param chip_select: The SPI device chip_select number + :param chip_select: int + :raises: InitError + """ + self.bus = bus + self.chip_select = chip_select + self.spi_callback = spi_callback + spi_device = "%s%d.%d" % (SPIDEV, self.bus, self.chip_select) + self.open_fd(spi_device) + + def __del__(self): + if self.fd is not None: + self.close_fd() + + def open_fd(self, spi_device): + try: + self.fd = posix.open(spi_device, posix.O_RDWR) + except OSError as e: + raise SPIInitError( + "I can't see %s. Have you enabled the SPI module? (%s)" + % (spi_device, SPI_HELP_LINK) + ) # from e # from is only available in Python 3 + + def close_fd(self): + posix.close(self.fd) + del self.fd + + def spisend(self, bytes_to_send): + """Sends bytes via the SPI bus. + + :param bytes_to_send: The bytes to send on the SPI device. + :type bytes_to_send: bytes + :returns: bytes -- returned bytes from SPI device + :raises: InitError + """ + # make some buffer space to store reading/writing + wbuffer = ctypes.create_string_buffer(bytes_to_send, + len(bytes_to_send)) + rbuffer = ctypes.create_string_buffer(len(bytes_to_send)) + + # create the spi transfer struct + transfer = spi_ioc_transfer( + tx_buf=ctypes.addressof(wbuffer), + rx_buf=ctypes.addressof(rbuffer), + len=ctypes.sizeof(wbuffer) + ) + + if self.spi_callback is not None: + self.spi_callback(bytes_to_send) + # send the spi command + ioctl(self.fd, SPI_IOC_MESSAGE(1), transfer) + return ctypes.string_at(rbuffer, ctypes.sizeof(rbuffer))