Using an I/O expander with a library that wants to write to normal pins

I have a project which needs to control several stepper motors, among other things. I ran out of GPIO pins on my MKR WIFI 1010 and bought a MCP23017 (https://www.amazon.com/dp/B08DFNR2JW?psc=1&ref=ppx_yo2ov_dt_b_product_details) to add more. I want to use the MCP pins as outputs for the stepper direction signals.

The trouble is the stepper library I'm using (SpeedyStepper) wants to write to GPIO pins, not have a conversation with an I2C device.

I thought this this would be a common question but I haven't been able to find much useful discussion so far. The DeepAI chatbot thinks I should modify the SpeedyStepper library to accept a reference to the MCP object (I'm using the Adafruit MCP23x17 library). I don't know if this is a good answer or not, nor would I know how to do it. As you can probably tell, I'm not a very expert programmer yet.

How can I get my motors turning? Tricky programming? Different libraries? Chicken out and just buy a board with more GPIO? I have to say, that last one is starting to look pretty good...

That's the easiest solution if your controller can serve the many motors in time. Otherwise I'd distribute the motors to (slave) Arduinos.

Sort of. Don't trust AI in things like these.

I've had a brief look at SpeedyStepper and it seems feasible to convert it to use MCP. I've done something similar when I just started out with Arduino, in my case running 7-segment displays over TM1637. Anyway, SpeedyStepper is written in such a way that it (1) uses regular digitalWrites for stepper control and (2) the digitalWrites are limited to only two functions due to the clear/logical structure of the code.

You could make a fork (or just a local copy for your own use) of the library and modify the constructor as well as the two functions (an initializer and the function that actually drives the stepper) to go through the MCP. I think the additional I2C timing overhead won't be much of a problem.

phase in @MicroBahner, may be he has something in his MobaTools.h

Isn't this the same as GPT said? :slight_smile:

Not quite. The AI answer was (1) brief and (2) not based on an actual analysis of feasibility.
The conclusion might be the same. That doesn't make it "the same as".

2 Likes

Is something stopping you from using normal pins for the SpeedyStepper and the MCP for other things that currently use normal pins?

The MoToStepper class has the ability to drive up to four 4-pin steppers via SPI. But it is useless here, because MobaTools don't support the MKR WiFi 1010.
Maybe Accelstepper is worth a closer look. it has the ability to include own functions to create the step output.
It would be important to know what kind of steppers @ian1971 wants to control and what speed he wants to achive. Creating the step output via I2C reduces the possible step speed a lot.

The suggestion from @sterretje is probably a better solution

1 Like

Wow, great responses so far! Seems like you folks have a pretty good forum here.

Yes. The steppers are but one ache for my poor head. There are also two h-bridges (PWM out x4, MCP has no hardware support), 4 pots (ADC in x4, MCP is digital only), an optical encoder (interrupt x2) and 3 home switches (with the same problem of support being part of the SpeedyStepper library). Add step signals for 4 steppers, a relay board, and some switches and I'm 3 pins shy (5 actually, after losing 2 more to SDA and SCL for the expander), and most of the stuff needs to/should be connected through GPIO. Of course, the relay board and switches CAN use the MCP.

I was not aware of this one. Looks a bit clunky but an option worth considering. There's even another library that adds MCP support! At a glance, SpeedyStepper seems have it beat on ease of use (maybe not in this case) and good documentation though.

The steppers: NEMA23 282oz/in 3A Stepper Motor ¼” Dual shaft (KL23H276-30-8B) |. These run at different max speeds, 6000 to 25,000 PPS during testing under load, with 10x microstepping on all drives (Gecko g251; not configurable). Note that I only want the direction pins on the MCP for exactly the reason you cited. These don't need to switch rapidly.

I think I want to pursue this option for now as it preserves what I've already done and is no doubt the most educationally profitable. At least I can now be confident it's a reasonable choice. If anyone wants to go into more detail on how to go about it or suggest some other resource I'd love to read it, otherwise I'll start researching and check back in when I run into trouble. Shouldn't be long, in other words. :grin:

Thanks everybody.

Well that didn't take long. This is beyond my skill set but here's an overview of what I think I need to do:

In SpeedyStepper.h:

public:

declare a reference to the MCP object:

int& r_mcp;

add a parameter to the constructor and initialize it with the address stored in r_mcp:

SpeedyStepper(int& r_mcp):mcp_ref(r_mcp);

declare a function to get and store the MCP pin number:

int get_dir_pin_number();

private:

declare a variable to hold the MCP pin number the stepper object will write to:

int mcp_pin;

In SpeedyStepper.cpp:

define a function to store the MCP pin number this motor will use:

void set_mcp_dir_pin(dir_pin){
    mcp_pin = dir_pin;
}

and change the calls to digitalWrite() to

mcp_ref.digitalWrite(mcp_pin, whatever);

when they are writing to the direction pin.

In main:

create the stepper object:

SpeedyStepper x_motor;

and configure it as usual...

...except the step to set the pin number on the MCP for the motor direction signal:

x_motor.set_mcp_dir_pin(dir_pin_x);

where dir_pin_x is defined in main

command x_motor as usual

I haven't been able to try any of this yet but am I getting close?

Conceptually, but the devil is in the details.

The SpeedyStepper would have to be aware of the MCP object to be able to use it. For this, you could store a reference to the MCP object by adding to SpeedyStepper/src/SpeedyStepper.h at master · Stan-Reifel/SpeedyStepper · GitHub in the "private" section (starting at line 95) something like this:
Adafruit_MCP23X17 * mcp;

But you also need to #include the MCP library, so at about line 39 of the .h file linked to above add
#include <Adafruit_MCP23X17.h>

I'd pass a reference to the MCP object to the constructor of the SpeedyStepper class. So do something like this, in here SpeedyStepper/src/SpeedyStepper.cpp at master · Stan-Reifel/SpeedyStepper · GitHub line 202:
SpeedyStepper::SpeedyStepper(Adafruit_MCP23X17 *_mcp);
You will also have to modify the SpeedyStepper .h file so that the declaration of the constructor matches the modification of the .cpp file!

In the constructor, copy the reference to a local version for the SpeedyStepper object. This means you need to add a line of code where you copy the passed reference to the locally held copy:
mcp = _mcp;

As to the 'connectToPins' function, all it needs to change is not write to a GPIO directly, but use the MCP. So e.g. line 237 in the .cpp file would change to something like this:
mcp->pinMode(stepPin, OUTPUT);
(note that we use "mcp->" instead of "mcp." because we're using a reference to the mcp object).
You'll have to modify the other pinMode and digitalWrite lines in a similar fashion.

This is really all that should be necessary to modify the library.

In your main sketch, you will need to first initialize the Adafruit MCP object, and then create the SpeedyStepper object. The order is important, because the MCP needs to exist before you pass its reference to the SpeedyStepper!

So somewhere at the top of your sketch you will add something like this:

Adafruit_MCP23X17 mcp;
SpeedyStepper(&mcp);

Note that we pass the mcp object to SpeedyStepper as a reference, using "&mcp" instead of "mcp".

From that point onwards, you should be able to use the SpeedyStepper object as you'd use it normally, with the only distinction that the pins you pass to connectToPins() are the GPIOs on the MCP.

To prevent confusion between the original SpeedyStepper and the mcp-enabled version, I'd rename the whole thing to something like SpeedyStepperMCP or so. I did not include this in the example snippets above; you'll have to consistently change the name throughout the .h and .cpp files.

Disclaimer: I did this as a theoretical exercise and have not actually tested if it compiles, let alone if it works. I may have overlooked something; you'll have to do some troubleshooting as you make the changes I propose above.

1 Like

rsmls:

Thanks so much for taking the time to post this. I haven't been able to get it to compile yet, but the number of compiler errors is moving in the right direction! I'm sure you've saved me a lot of fumbling in the dark.

Honestly, having never worked with classes, references, pointers, etc., I find this confusing as hell.

I'll post when I either get it working or become irredeemably stuck.

I can confirm...
It gets less confusing as you learn the concepts, but frankly, the business around pointers etc. sometimes has me scratching my head as well, even if I understand the basics.

Or, modify the library such that the functionality for custom "pinMode" and "digitalWrite" functions is passed to the constructor. Then, the main program could specify either the standard Arduino functions or custom ones on a per-instance basis.

2 Likes

IT WORKS!

I got a motor turning both directions with the MCP controlling the direction! Or, more like I followed rsmls's advice exactly, since the solution in post #11 was correct and complete. I was reading a few things into it which aren't there and that's where the compiler errors were coming from...

Hurrah! Happy to hear this, and well done!

I like this idea! I might give it a shot now that I understand how.

Thanks to all who contributed to the thread!

Post the code that you come up with.

Hi there.

I am actually doing something pretty similar but with the 74HC595 shift registers. I'm using a teensy 4.1 for this project and decided to use the teensystep4 library.

My wiring was such:
TS4.1 pin 11 to data
TS4.1 pin 13 to clock
TS4.1 pin 17 to latch

I am currently testing with x2 stepper motors:
STP1 Pul,Dir = 1,2 (register's outputs)
STP2 Pul,Dir = 3,4 (register's outputs)

Similarly to the solution posted above, I used a library called Shifty to create a shift register object and pass it by reference to the stepper motor constructor.

From there, my method calls in place of digitalWriteFast was instead shiftReg->writeBit(dir or pul pin, HIGH or LOW)

Had no compile time errors, but I'm getting no response from my stepper motors. Double checked my wiring and made sure continuity was consistent. As clarification, I am currently waiting for my 74HCT595 chips to arrive, so I for now have a separate logic level convert to shift the 3.3v signals on the teensy to 5v signals. I know for a fact this works since I got the steppers running just from the teensy itself with the 5v logic shift.

Here's my fork of the teensystep4 library: GitHub - HackinSpock/TeensyStep4_74HC595: Will implement a method to replace a typical digitalWriteFast() command with the Shift libraries 74HC595 writeBit(). This will allow the user to chain as many shift registers to expand # of used steppers.

This is my test file:

#include "Arduino.h"
#include "teensystep4.h"
#include <Shifty.h>

using namespace TS4;

//declare shift register
Shifty shift;

Stepper s1(&shift, 1, 2);
Stepper s2(&shift, 3, 4);

void setup()
{
    pinMode(LED_BUILTIN, OUTPUT);
    // Set the number of bits you have (multiples of 8)
    shift.setBitCount(8);

  // Set the clock, data, and latch pins you are using
  // This also sets the pinMode for these pins
    shift.setPins(13, 11, 17); 

    TS4::begin();

    s1
        // .setMaxSpeed(10'000)
        // .setAcceleration(50'000);
        .setMaxSpeed(1'000)
        .setAcceleration(5'000);

    s2
        // .setMaxSpeed(8'000)
        // .setAcceleration(25'000);
        .setMaxSpeed(1'000)
        .setAcceleration(5'000);
    // move two steppers simultaneously (not synchronized)
    s1.moveAbsAsync(1000);
    s2.moveAbsAsync(4000);

    // wait until both steppers are done
    while (s1.isMoving || s2.isMoving)
    {
        delay(10);
    }
    delay(100);

    // define and move a groups of steppers synchronized
    StepperGroup g1{s1, s2};

    s1.setTargetAbs(-1000);
    s2.setTargetAbs(-3000);
    g1.move();
    delay(100);

    s1.setTargetAbs(0);
    StepperGroup({s1, s2}).move();
}

void loop()
{
    digitalToggleFast(LED_BUILTIN);
    delay(200);
}