I2C I/O Expander PCB with 32 channels, WeltonCopy board

This PCB peripheral uses two MCP23017 chips to give 32 programmable input/output general purpose connections. These are digital inputs/outputs; pull-up and interrupt options etc. are available. The pin layout involves doubled up digital outputs to go with other existing peripherals such as the relay board and the stepper motor drivers. There will be an equivalent board without the doubling up coming soon i.e. fewer output pins, but the same number of channels. The boards are designed to be daisy-chained allowing huge numbers of digital outputs and soon PWM outputs provided by the Scampton PCB. A maximum of four WeltonCopy boards can be run off of one RPi Pico, providing 128 digital I/O channels per bus; the Pico can have two buses connected. 25mA is available from any output pin, so small devices can be driven, such as off/on LEDs. Between the digital I/O and the PWM option a model train layout could have many off/on lights and signals and also dimmable LED lights. These options can also provide a large number of train track speed and direction controls. Also note that though the Bluetooth Remote Control can be used to control the Digital and PWM outputs, it will be possible to create programs to run on laptops and tablets to allow large numbers of outputs to be controlled by a GUI. An upgraded version of Lincoln Code is planned for this development, which will probably be Visual Studio DotNet to start with. The WeltonCopy board can either regulate 5V down to 3.3V or it can run directly from the external (5V) supply. It does not offer to run the board from MCU I2C cable power at this time, this is because the intent is to supply greater load than most MCUs are rated for.

The unpopulated WeltonCopy peripheral PCB is available for $10NZD plus postage. The lower WeltonCopy peripheral PCB populated as shown is available for $40NZD plus postage. The 5V power supply and the upper parent board are not included. The I2C cable is included.

from machine import Pin, I2C
import mcp23017
import time

#i2c = I2C(scl=Pin(22), sda=Pin(21))
i2c = I2C(id=0, scl=Pin(1), sda=Pin(0))
mcp = mcp23017.MCP23017(i2c, 0x20) # 0x20, 0x21
mcp1 = mcp23017.MCP23017(i2c, 0x21) # 0x20, 0x21

# property interface 16-bit
mcp.mode = 0x0000  # all set to outputs
mcp.gpio = 0x5555  # write to outputs

# property interface 16-bit
mcp1.mode = 0x0000
mcp1.gpio = 0x5555


databyte = 0x5555
while True:
    databyte ^= 0xffff
    mcp.gpio = databyte
    mcp1.gpio = databyte
    print('{0:016b}'.format(databyte)) 
    time.sleep(2)

--------------------------------------------------------------------------------
#Program output & voltage measurements confirm behavior
MPY: soft reboot
1010101010101010
0101010101010101
1010101010101010
0101010101010101
1010101010101010
0101010101010101
1010101010101010
0101010101010101
--------------------------------------------------------------------------------
# iopin.py
# MIT License (MIT)
# Copyright (c) 2025 Microtron Ltd NZ - Stephen Eichler
# https://opensource.org/licenses/MIT

from machine import Pin, I2C
import mcp23017
import time

class IoPin:

    ioaddr=[0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27]
    pinlist=[[],[],[],[],[],[],[],[]]
    pinlist1=[[],[],[],[],[],[],[],[]]
    
    @classmethod
    def updatepinlist(cls, iopin, i2c_channel, i2c_periph_addr_0, i2c_periph_addr_1):

        if i2c_periph_addr_0 not in cls.ioaddr or i2c_periph_addr_1 not in cls.ioaddr:
            raise ValueError("Invalid peripheral address provided")

        pl=cls.pinlist if i2c_channel==0 else cls.pinlist1
        idx0 = cls.ioaddr.index(i2c_periph_addr_0)
        idx1 = cls.ioaddr.index(i2c_periph_addr_1)
        
        if pl[idx0] != pl[idx1]:
            raise ValueError("Pin list mismatch, pin " + str(iopin)) 
        
        if iopin not in pl[idx0] and iopin not in pl[idx1]:
            pl[idx0].append(iopin)
            pl[idx1].append(iopin)
        else:
            raise ValueError("Duplicate pin instance, pin " + str(iopin)) 

    def __init__(self, iopin=None, i2c_channel=0, scl_pin=1, sda_pin=0, i2c_periph_addr_0 = 0x20,
                 i2c_periph_addr_1 = 0x21):
        self._i2c = I2C(id=i2c_channel, scl=Pin(scl_pin), sda=Pin(sda_pin))
        self._mcp = mcp23017.MCP23017(self._i2c, i2c_periph_addr_0) 
        self._mcp1 = mcp23017.MCP23017(self._i2c, i2c_periph_addr_1) 
        self._value=0

        if iopin is None :
            raise TypeError("No index provided")
        elif iopin < 0 or iopin >= 32:
            raise ValueError("Invalid pin ID provided")
        else:
            self._iopin=iopin
                        
        #self._mcp.mode = 0
        #self._mcp1.mode = 0
                 
        self.updatepinlist(iopin, i2c_channel, i2c_periph_addr_0, i2c_periph_addr_1)
        

    @property
    def iopin(self):        
       return self._iopin

    @property
    def value(self):        
       return self._value

    @value.setter
    def value(self, value):
        if value > 1:
            self._value=1
        elif value < 0:
            self._value=0
        else:
            self._value=value
        
        if self._iopin < 16 and  self._iopin >= 0:
            self._mcp[self._iopin].value(self._value)
        elif  self._iopin < 32 and  self._iopin >= 16:
            self._mcp1[self._iopin - 16].value(self._value)
            
    def output(self, value=None):
        if value is None:
            self._value=None
        elif value > 1:
            self._value=1
        elif value < 0:
            self._value=0
        else:
            self._value=value
            
        if self._iopin < 16 and  self._iopin >= 0:
            self._mcp[self._iopin].output(self._value)
        elif  self._iopin < 32 and  self._iopin >= 16:
            self._mcp1[self._iopin - 16].output(self._value)        
            
    def inputmode(self, pull=None):
        if self._iopin < 16 and  self._iopin >= 0:
            self._mcp[self._iopin].input(pull)
        elif  self._iopin < 32 and  self._iopin >= 16:
            self._mcp1[self._iopin - 16].input(pull)
            
    @property
    def mode(self):
        return self._mcp.mode | (self._mcp1.mode << 16)
    @mode.setter
    def mode(self, val):
        self._mcp.mode = val
        self._mcp1.mode = (val >> 16)
        
    @property
    def gpio(self):
        return self._mcp.gpio | (self._mcp1.gpio << 16)
    @gpio.setter
    def gpio(self, val):
        self._mcp.gpio = val
        self._mcp1.gpio = (val >> 16)
    
    @property
    def output_latch(self):
        return self._mcp.output_latch | (self._mcp1.output_latch << 16)

    @output_latch.setter
    def output_latch(self, value):
        if value is None:
            self._value=None
        elif value > 1:
            self._value=1
        elif value < 0:
            self._value=0
        else:
            self._value=value
            
        if self._iopin < 16 and  self._iopin >= 0:
            self._mcp.output_latch=self._value
        elif  self._iopin < 32 and  self._iopin >= 16:
            self._mcp1.output_latch=self._value        


--------------------------------------------------------------------------------
# IoPinMain.py
import iopin
import time

pin1 = iopin.IoPin(iopin=1)
pin2 = iopin.IoPin(iopin=2)

#pin1a = iopin.IoPin(iopin=1)

print(iopin.IoPin.pinlist)
print(iopin.IoPin.pinlist1)

while True:
    pin1.output(1)
    print(pin1.value)
    time.sleep(2)
    pin1.output(0)
    print(pin1.value)
    time.sleep(2)
--------------------------------------------------------------------------------
pin1a = iopin.IoPin(iopin=1) # uncommented. Duplicate pin.

MPY: soft reboot
Traceback (most recent call last):
  File "<stdin>", line 7, in <module>
  File "iopin.py", line 53, in __init__
  File "iopin.py", line 30, in updatepinlist
ValueError: Duplicate pin instance, pin 1
>>> 
# This code runs, but so far have not connected a motor
# latchiopin.py
# MIT License (MIT)
# Copyright (c) 2024 Microtron Ltd NZ - Stephen Eichler
# https://opensource.org/licenses/MIT
# Written 2/7/2025
# import latchiopin
# alatch0=latchiopin.LatchIoPin(0) 
# alatch1=latchiopin.LatchIoPin(1) 
# alatch2=latchiopin.LatchIoPin(2) 
# alatch0.d0.value=1

#from machine import Pin
import iopin

class LatchIoPin:

    def __init__(self, iopinbyte=None, bus=0, scl=1, sda=0, ioaddr0=0x20, ioaddr1=0x21, pwmaddr=0x40, reset=False):
        if iopinbyte is None :
            raise TypeError("No byte index provided")
        elif iopinbyte < 0 or iopinbyte >= 4:
            raise ValueError("Invalid byte index provided")
        else:
            self._iopinbyte=iopinbyte

        self._iopn=[]
        for i in range(8):
            self._iopn.append(iopin.IoPin(iopin=i + (self._iopinbyte * 8), i2c_channel=bus, scl_pin=scl, sda_pin=sda,
                              i2c_periph_addr_0=ioaddr0, i2c_periph_addr_1=ioaddr1))

        self.d0 = Channel(self, 0)
        self.d1 = Channel(self, 1)
        self.d2 = Channel(self, 2)
        self.d3 = Channel(self, 3)
        self.d4 = Channel(self, 4)
        self.d5 = Channel(self, 5)
        self.d6 = Channel(self, 6)
        self.d7 = Channel(self, 7)
        if reset:
            self.d0.output(0)
            self.d1.output(0)
            self.d2.output(0)
            self.d3.output(0)
            self.d4.output(0)
            self.d5.output(0)
            self.d6.output(0)
            self.d7.output(0)
        else:
            self.d0.output(self._iopn[0].value)
            self.d1.output(self._iopn[1].value)
            self.d2.output(self._iopn[2].value)
            self.d3.output(self._iopn[3].value)
            self.d4.output(self._iopn[4].value)
            self.d5.output(self._iopn[5].value)
            self.d6.output(self._iopn[6].value)
            self.d7.output(self._iopn[7].value)                   

    def _set_value(self, channel, val):
        self._iopn[channel.channel_index].value=bool(val)
        
    def _output(self, channel, val):
        self._iopn[channel.channel_index].output(bool(val))

    def reset(self):
        self.d0.output(0)
        self.d1.output(0)
        self.d2.output(0)
        self.d3.output(0)
        self.d4.output(0)
        self.d5.output(0)
        self.d6.output(0)
        self.d7.output(0)

class Channel:
    """An instance of a single channel for a multi-channel address latch.
    **All available channels are created automatically and should not be created by the user**"""


    def __init__(self, alat_instance, index):
        self._alat = alat_instance
        self.channel_index = index
        self._value=0

    @property
    def value(self):
        """The current value for the channel."""
        return self._value

    @value.setter
    def value(self, value):
        self._value=value
        self._alat._set_value(self, value)  

    def output(self, value):
        self._value=value
        self._alat._output(self, value)  


-----------------------------------------------------------------
# latchiopin_main.py
# MIT License (MIT)
# Copyright (c) 2025 Microtron Ltd NZ - Stephen Eichler
# https://opensource.org/licenses/MIT
# Written 24/7/2025
import latchiopin
import time
import sys
#import iopin

latchdrv0=latchiopin.LatchIoPin(0) 
#ipin0=iopin.IoPin(0) # if uncommented causes exception due to duplicate pin

print ("Press enter to continue.")


while 1:
    latchdrv0.d0.value = 1
    time.sleep_ms(500)
    print ("latchdrv0.d0.value ", latchdrv0.d0.value)
    data = sys.stdin.buffer.read(2)
    latchdrv0.d0.value = 0
    print ("latchdrv0.d0.value ", latchdrv0.d0.value)

    latchdrv0.d1.value = 1
    time.sleep_ms(500)
    print ("latchdrv0.d1.value ", latchdrv0.d1.value)
    data = sys.stdin.buffer.read(2)
    latchdrv0.d1.value = 0
    print ("latchdrv0.d1.value ", latchdrv0.d1.value)

    latchdrv0.d2.value = 1
    time.sleep_ms(500)
    print ("latchdrv0.d2.value")
    data = sys.stdin.buffer.read(2)
    latchdrv0.d2.value = 0

    latchdrv0.d3.value = 1
    time.sleep_ms(500)
    print ("latchdrv0.d3.value")
    data = sys.stdin.buffer.read(2)
    latchdrv0.d3.value = 0

    latchdrv0.d4.value = 1
    time.sleep_ms(500)
    print ("latchdrv0.d4.value")
    data = sys.stdin.buffer.read(2)
    latchdrv0.d4.value = 0

    latchdrv0.d5.value = 1
    time.sleep_ms(500)
    print ("latchdrv0.d5.value")
    data = sys.stdin.buffer.read(2)
    latchdrv0.d5.value = 0

    latchdrv0.d6.value = 1
    time.sleep_ms(500)
    print ("latchdrv0.d6.value")
    data = sys.stdin.buffer.read(2)
    latchdrv0.d6.value = 0

    latchdrv0.d7.value = 1
    time.sleep_ms(500)
    print ("latchdrv0.d7.value")
    data = sys.stdin.buffer.read(2)
    latchdrv0.d7.value = 0

-----------------------------------------------------------------
# iopinstepper1.py
# MIT License (MIT)
# Copyright (c) 2025 Microtron Ltd NZ - Stephen Eichler
# https://opensource.org/licenses/MIT
# Written 22/7/2025
# import iopinstepper1
# stpdrv0=iopinstepper1.StepDrv(iopinbyte=1) 
# stpdrv0.DV1.ms1 = 1
# stpdrv0.DV1.dir = 1
# stpdrv0.DV1.step()


import latchiopin
import time

class StepDrv:

    def __init__(self, latchid, bus=0, scl=1, sda=0, ioaddr0=0x20, ioaddr1=0x21, pwmaddr=0x40):
        self.alatchn=latchiopin.LatchIoPin(iopinbyte=latchid, reset=True, bus=bus, scl=scl,
                                           sda=sda, ioaddr0=ioaddr0, ioaddr1=ioaddr1, pwmaddr=pwmaddr)
        self.DV1 = Driver(self, 1)      
       
    @property
    def enable(self):
        return self.alatchn.d7.value

    @enable.setter
    def enable(self, value):
        self.alatchn.d7.value=value

    @property
    def reset(self):
        return self.alatchn.d3.value

    @reset.setter
    def reset(self, value):
        self.alatchn.d3.value=value
        
    @property
    def sleep(self):
        return self.alatchn.D2.value

    @sleep.setter
    def sleep(self, value):
        self.alatchn.d2.value=value        
    
    def _step(self, driver):

        Dbit = self.alatchn.d1
            
        if Dbit != "":
            Dbit.value = 0            
            time.sleep_us(2)
            Dbit.value = 1
            time.sleep_us(2)
            #time.sleep_ms(100) # debugging
            Dbit.value = 0
        

    #def _set_value(self, channel, val):
    def _set_ms1(self, driver, val):
        self._setalatbit(6, val)

    def _set_ms2(self, driver, val):
        self._setalatbit(5, val)

    def _set_ms3(self, driver, val):
        self._setalatbit( 4, val)

    def _set_dir(self, driver, val):
        self._setalatbit(0, val)

    def _setalatbit(self, dbitindex, val):
            
        if dbitindex == 0:
            self.alatchn.d0.value = val
        elif dbitindex == 1:
            self.alatchn.d1.value = val
        elif dbitindex == 2:
            self.alatchn.d2.value = val
        elif dbitindex == 3:
            self.alatchn.d3.value = val
        elif dbitindex == 4:
            self.alatchn.d4.value = val
        elif dbitindex == 5:
            self.alatchn.d5.value = val
        elif dbitindex == 6:
            self.alatchn.d6.value = val
        elif dbitindex == 7:
            self.alatchn.d7.value = val
            
    def resetmode(self):
        self.alatchn.reset()

class Driver:
    def __init__(self, stpdv_instance, index):
        self._stpdv = stpdv_instance
        self.driver_index = index
        self._ms1=0
        self._ms2=0    
        self._ms3=0    
        self._dir=0
        
    @property
    def ms1(self):
        return self._ms1
    @property
    def ms2(self):
        return self._ms2
    @property
    def ms3(self):
        return self._ms3
    @property
    def direction(self):
        return self._direction

    @ms1.setter
    def ms1(self, value):
        self._ms1=value
        self._stpdv._set_ms1(self, value)  
        
    @ms2.setter
    def ms2(self, value):
        self._ms2=value
        self._stpdv._set_ms2(self, value)  

    @ms3.setter
    def ms3(self, value):
        self._ms3=value
        self._stpdv._set_ms3(self, value)  

    @direction.setter
    def direction(self, value):
        self._dir=value
        self._stpdv._set_dir(self, value)  

    def step(self):
        self._stpdv._step(self)
        
        
        
        
------------------------------------------------------------------
# iopinstep1_main.py
# MIT License (MIT)
# Copyright (c) 2025 Microtron Ltd NZ - Stephen Eichler
# https://opensource.org/licenses/MIT
# Written 22/7/2025
import iopinstepper1
import time
import sys

stpdrv0=iopinstepper1.StepDrv(1) 

print ("Press enter to continue.")

while 1:
    stpdrv0.DV1.ms1 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV1.ms1")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV1.ms1 = 0

    stpdrv0.DV1.ms2 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV1.ms2")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV1.ms2 = 0

    stpdrv0.DV1.ms3 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV1.ms3")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV1.ms3 = 0

    stpdrv0.DV1.direction = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV1.direction")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV1.direction = 0

    stpdrv0.enable = 1
    time.sleep_ms(500)
    print ("stpdrv0.enable")
    data = sys.stdin.buffer.read(2)
    stpdrv0.enable = 0

    stpdrv0.reset = 1
    time.sleep_ms(500)
    print ("stpdrv0.reset")
    data = sys.stdin.buffer.read(2)
    stpdrv0.reset = 0

    stpdrv0.sleep = 1
    time.sleep_ms(500)
    print ("stpdrv0.sleep")
    data = sys.stdin.buffer.read(2)
    stpdrv0.sleep = 0

    stpdrv0.DV1.step()

# This code runs, but so far have not connected a motor
# iopinstepper3.py
# MIT License (MIT)
# Copyright (c) 2025 Microtron Ltd NZ - Stephen Eichler
# https://opensource.org/licenses/MIT
# Written 22/7/2025
# import iopinstepper3
# stpdrv0=iopinstepper3.StepDrv() 
# stpdrv0.DV1.ms1 = 1
# stpdrv0.DV2.dir = 1
# stpdrv0.DV3.step()


import latchiopin
import time

class StepDrv:

    def __init__(self, latchid0, latchid1, bus=0, scl=1, sda=0, ioaddr0=0x20,
                 ioaddr1=0x21, pwmaddr=0x40):
        self.alatch1=latchiopin.LatchIoPin(iopinbyte=latchid0, reset=True, bus=bus, scl=scl,
                                           sda=sda, ioaddr0=ioaddr0, ioaddr1=ioaddr1, pwmaddr=pwmaddr)
        self.alatch2=latchiopin.LatchIoPin(iopinbyte=latchid1, reset=True, bus=bus, scl=scl,
                                           sda=sda, ioaddr0=ioaddr0, ioaddr1=ioaddr1, pwmaddr=pwmaddr)
        self.DV1 = Driver(self, 1)
        self.DV2 = Driver(self, 2)
        self.DV3 = Driver(self, 3)
        
    @property
    def spare(self):
        return self.alatch2.d0.value

    @spare.setter
    def spare(self, value):
        self.alatch2.d0.value=value
      
    def _step(self, driver):
        dbitindex = {3:2,2:7,1:4}[driver.driver_index]
        dbyteindex = {3:2,2:2,1:1}[driver.driver_index]

        alatch = None
        if dbyteindex == 1:
            alatch = self.alatch1
        else:
            alatch = self.alatch2
            
        Dbit = None
        if dbitindex == 2:
            Dbit = alatch.d2
        elif dbitindex == 4:
            Dbit = alatch.d4
        elif dbitindex == 7:
            Dbit = alatch.d7
            
        if Dbit is not None:
            Dbit.value = 0            
            time.sleep_us(2)
            Dbit.value = 1
            time.sleep_us(2)
            Dbit.value = 0
        
    def _setalatbit(self, dbyteindex, dbitindex, val):
        alatch = ""
        if dbyteindex == 1:
            alatch = self.alatch1
        else:
            alatch = self.alatch2
            
        if dbitindex == 0:
            alatch.d0.value = val
        elif dbitindex == 1:
            alatch.d1.value = val
        elif dbitindex == 2:
            alatch.d2.value = val
        elif dbitindex == 3:
            alatch.d3.value = val
        elif dbitindex == 4:
            alatch.d4.value = val
        elif dbitindex == 5:
            alatch.d5.value = val
        elif dbitindex == 6:
            alatch.d6.value = val
        elif dbitindex == 7:
            alatch.d7.value = val    def resetmode(self):
        self.alatchn.reset()

    
    def _set_ms1(self, driver, val):
        dbitindex = {3:5,2:2,1:7}[driver.driver_index]
        dbyteindex = {3:2,2:1,1:1}[driver.driver_index]
        self._setalatbit(dbyteindex, dbitindex, val)

    def _set_ms2(self, driver, val):
        dbitindex = {3:4,2:1,1:6}[driver.driver_index]
        dbyteindex = {3:2,2:1,1:1}[driver.driver_index]
        self._setalatbit(dbyteindex, dbitindex, val)

    def _set_ms3(self, driver, val):
        dbitindex = {3:3,2:0,1:5}[driver.driver_index]
        dbyteindex = {3:2,2:1,1:1}[driver.driver_index]
        self._setalatbit(dbyteindex, dbitindex, val)

    def _set_dir(self, driver, val):
        dbitindex = {3:1,2:6,1:3}[driver.driver_index]
        dbyteindex = {3:2,2:2,1:1}[driver.driver_index]
        self._setalatbit(dbyteindex, dbitindex, val)

    def resetmode(self):
        self.alatch1.reset()
        self.alatch2.reset()


class Driver:
    def __init__(self, stpdv_instance, index):
        self._stpdv = stpdv_instance
        self.driver_index = index
        self._ms1=0
        self._ms2=0    
        self._ms3=0    
        self._dir=0
        
    @property
    def ms1(self):
        return self._ms1
    @property
    def ms2(self):
        return self._ms2
    @property
    def ms3(self):
        return self._ms3
    @property
    def direction(self):
        return self._direction

    @ms1.setter
    def ms1(self, value):
        self._ms1=value
        self._stpdv._set_ms1(self, value)  
        
    @ms2.setter
    def ms2(self, value):
        self._ms2=value
        self._stpdv._set_ms2(self, value)  

    @ms3.setter
    def ms3(self, value):
        self._ms3=value
        self._stpdv._set_ms3(self, value)  

    @direction.setter
    def direction(self, value):
        self._dir=value
        self._stpdv._set_dir(self, value)  

    def step(self):
        self._stpdv._step(self)
        
        
-------------------------------------------------------------------------------
# This code runs, but so far have not connected a motor
# iopinstep3_main.py
# MIT License (MIT)
# Copyright (c) 2025 Microtron Ltd NZ - Stephen Eichler
# https://opensource.org/licenses/MIT
# Written 22/7/2025
import iopinstepper3
import time
import sys

stpdrv0=iopinstepper3.StepDrv(0, 1) 
print ("Press enter to continue.")

while 1:
    stpdrv0.DV1.ms1 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV1.ms1")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV1.ms1 = 0

    stpdrv0.DV1.ms2 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV1.ms2")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV1.ms2 = 0

    stpdrv0.DV1.ms3 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV1.ms3")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV1.ms3 = 0

    stpdrv0.DV1.direction = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV1.direction")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV1.direction = 0

    stpdrv0.DV2.ms1 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV2.ms1")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV2.ms1 = 0

    stpdrv0.DV2.ms2 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV2.ms2")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV2.ms2 = 0

    stpdrv0.DV2.ms3 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV2.ms3")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV2.ms3 = 0

    stpdrv0.DV2.direction = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV2.direction")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV2.direction = 0

    stpdrv0.DV3.ms1 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV3.ms1")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV3.ms1 = 0

    stpdrv0.DV3.ms2 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV3.ms2")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV3.ms2 = 0

    stpdrv0.DV3.ms3 = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV3.ms3")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV3.ms3 = 0

    stpdrv0.DV3.direction = 1
    time.sleep_ms(500)
    print ("stpdrv0.DV3.direction")
    data = sys.stdin.buffer.read(2)
    stpdrv0.DV3.direction = 0

    stpdrv0.DV1.step()
    stpdrv0.DV2.step()
    stpdrv0.DV3.step()

    stpdrv0.spare = 1
    time.sleep_ms(500)
    print ("stpdrv0.spare")
    data = sys.stdin.buffer.read(2)
    stpdrv0.spare = 0

 

Stepper driving using A4988 and IoExpander

The iopinstepper1.py software driver has been tested successfully. It only takes 2us to trigger a step so multiple motors of various kinds can be run simultaneously. See Video. See PWM page. The A4988 does not require a PWM signal and so digital IO only is sufficient to drive it.