element14 Community
element14 Community
    Register Log In
  • Site
  • Search
  • Log In Register
  • Community Hub
    Community Hub
    • What's New on element14
    • Feedback and Support
    • Benefits of Membership
    • Personal Blogs
    • Members Area
    • Achievement Levels
  • Learn
    Learn
    • Ask an Expert
    • eBooks
    • element14 presents
    • Learning Center
    • Tech Spotlight
    • STEM Academy
    • Webinars, Training and Events
    • Learning Groups
  • Technologies
    Technologies
    • 3D Printing
    • FPGA
    • Industrial Automation
    • Internet of Things
    • Power & Energy
    • Sensors
    • Technology Groups
  • Challenges & Projects
    Challenges & Projects
    • Design Challenges
    • element14 presents Projects
    • Project14
    • Arduino Projects
    • Raspberry Pi Projects
    • Project Groups
  • Products
    Products
    • Arduino
    • Avnet Boards Community
    • Dev Tools
    • Manufacturers
    • Multicomp Pro
    • Product Groups
    • Raspberry Pi
    • RoadTests & Reviews
  • Store
    Store
    • Visit Your Store
    • Choose another store...
      • Europe
      •  Austria (German)
      •  Belgium (Dutch, French)
      •  Bulgaria (Bulgarian)
      •  Czech Republic (Czech)
      •  Denmark (Danish)
      •  Estonia (Estonian)
      •  Finland (Finnish)
      •  France (French)
      •  Germany (German)
      •  Hungary (Hungarian)
      •  Ireland
      •  Israel
      •  Italy (Italian)
      •  Latvia (Latvian)
      •  
      •  Lithuania (Lithuanian)
      •  Netherlands (Dutch)
      •  Norway (Norwegian)
      •  Poland (Polish)
      •  Portugal (Portuguese)
      •  Romania (Romanian)
      •  Russia (Russian)
      •  Slovakia (Slovak)
      •  Slovenia (Slovenian)
      •  Spain (Spanish)
      •  Sweden (Swedish)
      •  Switzerland(German, French)
      •  Turkey (Turkish)
      •  United Kingdom
      • Asia Pacific
      •  Australia
      •  China
      •  Hong Kong
      •  India
      •  Korea (Korean)
      •  Malaysia
      •  New Zealand
      •  Philippines
      •  Singapore
      •  Taiwan
      •  Thailand (Thai)
      • Americas
      •  Brazil (Portuguese)
      •  Canada
      •  Mexico (Spanish)
      •  United States
      Can't find the country/region you're looking for? Visit our export site or find a local distributor.
  • Translate
  • Profile
  • Settings
Raspberry Pi
  • Products
  • More
Raspberry Pi
Blog Raspberry PIO stepper library documentation - 3: control multiple motors with 1 or more PIOs
  • Blog
  • Forum
  • Documents
  • Quiz
  • Events
  • Polls
  • Files
  • Members
  • Mentions
  • Sub-Groups
  • Tags
  • More
  • Cancel
  • New
Raspberry Pi requires membership for participation - click to join
Featured Articles
Announcing Pi
Technical Specifications
Raspberry Pi FAQs
Win a Pi
GPIO Pinout
Raspberry Pi Wishlist
Comparison Chart
Quiz
  • Share
  • More
  • Cancel
Group Actions
  • Group RSS
  • More
  • Cancel
Engagement
  • Author Author: Jan Cumps
  • Date Created: 21 Apr 2025 11:58 AM Date Created
  • Views 350 views
  • Likes 5 likes
  • Comments 4 comments
Related
Recommended
  • raspberry
  • pico_pio_stepper_lib
  • pico
  • PIO
  • stepper-motor
  • c++

Raspberry PIO stepper library documentation - 3: control multiple motors with 1 or more PIOs

Jan Cumps
Jan Cumps
21 Apr 2025

Raspberry PIO stepper library (pio_stepper_lib) is a C++ library that runs stepper motors in PIO state machines. It's intended to be easy to integrate and use in Pico projects.

Check  Raspberry PIO stepper library documentation - 1: intro, set up project and simple example  to get a motor running. and Raspberry PIO stepper library documentation - 2: advanced example with notification for tighter execution control . This exercise builds upon that example.

This exercise uses the library to control 2 motors. A few scenarios are shown:

  • all commands run in sequence
  • each motor picking its commands and run them all in parallel.

image

you don't need stepper motors or driver ICs to test this. If you have an oscilloscope, you can see what's happening..

Probe these pins:

  • GPIO4: motor 1 DIR
  • GPIO5: motor 1 STEP (the pulse train)
  • GPIO6: motor 2 DIR
  • GPIO7: motor 2 STEP (the pulse train)

In this exercise, I probed GPIO5 and GPIO7.

Design Approach

This example uses the same classes as exercise 2. All changes are in the example code.

initialisation:

I created a container to hold the two motors. This is convenient when the same action has to be done on both motors. And it's also a starting point for running more than 2 steppers.

// TODO: adapt to your  board design
const uint motor1_dir = 4U; // implies that step is gpio 5
const uint motor2_dir = 6U; // implies that step is gpio 7

using motor_t = stepper::stepper_callback_controller;
std::array<motor_t, 2> motors {{{pio0, 0}, {pio0, 1}}};

I used PIO 0, state machine 0 and 1. Any combination works, as long as you have one motor per available state machine.

Each PIO that's used, needs to be programmed, and state machines set up:

void init_pio() {
    // program the pio used for the motors
    // do this only once per used pio
    motor_t::pio_program(pio0); // not needed if all sms run on pio1
    // motor_t::pio_program(pio1); // not needed if all sms run on pio0

    // individual settings
    motors[0].pio_init(motor1_dir, clock_divider);
    motors[1].pio_init(motor2_dir, clock_divider);
    // common settings
    // initialise and enable the motor state machine
    for (auto &m: motors) {
        m.register_pio_interrupt(0, true);
        m.enable(true);
        // and the notification, for demo purpose
        m.on_complete_callback(on_complete);
    }
}

Notification, and flag that a job has finished:

I created a container that gets the count of steps to do per motor. In the callback, I decrease the counter of a motor every time it flags a job done.
The same callback is used for both motors.

std::array<volatile size_t, motors.size()> commands_per_motor;
void on_complete(const motor_t &stepper) {
    size_t index = 0U;
    for (auto &m: motors) {
        if (&m == &stepper) {
            commands_per_motor[index] =  commands_per_motor[index] - 1;
            printf("motor %d executed command\n", index + 1);
            break;
        }
        index++;
    }
}

Perform set of commands:

// struct to hold command and motor
struct motor_command {
    stepper::command cmd;
    size_t motor;
    motor_command(stepper::command cmd, size_t motor) : cmd(cmd), motor(motor) {}
};

using commands_t = std::span<motor_command>;	

void run_commands(const commands_t& cmd, uint delay0, uint delay1) {
    motors[0].set_delay(delay0);
    motors[1].set_delay(delay1);

    for (auto& count: commands_per_motor) { count = 0; }

    for (auto& c : cmd) { 
        // increment commands expected for the motor
        commands_per_motor[c.motor] = commands_per_motor[c.motor] + 1;
    }

    for (auto& c : cmd) { 
        if (commands_per_motor[c.motor] > 0U) {
            motors[c.motor].take_steps(c.cmd);
        }
    }

    size_t todo = 0U;
    do {
        todo = std::accumulate(commands_per_motor.begin(),commands_per_motor.end(),0);
    } while (todo > 0U);

    sleep_ms(100); // pause for demo purpose    
}

This function gets a set of commands, and the speed for each motor. What happens?

  • speed is set
  • the count of steps to do per motor is retrieved, by looking into the command batch
  • commands are sent to each motor. They start immediately
    in parallel, the callback will start decrementing the relevant command counters each time a notification from PIO arrives
  • code waits until the total amount of steps has been taken

Demo preparation: batch of commands

This is the set of 7 commands used in the demo. The first and last are run on the first motor. The others on the other one.

    std::array<motor_command, 7> cmd{{
        { {200 * microstep_x, true}, 0},
        { {200 * microstep_x, true}, 1},
        { {300 * microstep_x, false}, 1},
        { {200 * microstep_x, false}, 1},
        { {10 * microstep_x, true}, 1},
        { {200 * microstep_x, false}, 1},
        { {20 * microstep_x, true}, 0}
    }};

Demo: 7 commands in parallel 

In the first demo, each motor picks up its own commands, and executes them right after each other. Motors run independently, at different speed.
The first motor will immediately execute the first and last command, and the second one immediately runs the middle 5.
The demo finishes when both motors have run the commands.

void full_demo(const commands_t & cmd) {
    // TODO: stepper driver IC should wake up here, if not yet
    // sleep_ms(1); 

    // demo : run all in parallel
    run_commands(cmd, 7000, 4300);

    // ...

    // TODO: stepper driver IC can go back to sleep here
}

image

Demo: 7 commands in sequence

In the second demo, each command is sent after the previous one is completed.
There is not a lot that you need to different. Just send the commands from the batch one by one...

void full_demo(const commands_t & cmd) {
    // TODO: stepper driver IC should wake up here, if not yet
    // sleep_ms(1); 

    // demo: run all in sequence
    for (auto &c: cmd) {
        std::array<motor_command,1> single_cmd {{c}};
        run_commands(single_cmd, 9000, 7000);
    }
    
    // ...


    // TODO: stepper driver IC can go back to sleep here
}

image

Both strategies can be intermixed. The gaps in the traces are for demo purposes, to have visual markers.

You don't have permission to edit metadata of this video.
Edit media
x
image
Upload Preview
image

video: 1 PIO controls 4 stepper motors

What changes do you need to make for your design?

DIR and STEP pins: the two pins for each motor have to be next to each other. DIR is the first, and STEP has to be the next pin in the GPIO bank.
The example uses GPIO4 for motor 1 DIR, and GPIO6 for motor 2 DIR. And that implies that the STEP pins are GPIO5 and GPIO7.
To change this, alter these lines:

// TODO: adapt to your board design
const uint motor1_dir = 4U; // implies that step is gpio 5
const uint motor2_dir = 6U; // implies that step is gpio 7

STEPPER DRIVER IC: the example doesn't contain stepper IC management. If you need to configure, enable or disable the IC during execution, add that logic.
There are placeholders in the code marked with:

// TODO: stepper driver IC <instructions>

I've created an example for

  • Texas Instruments DRV8711: https://github.com/jancumps/pio_drv8711_stepper.
  • Allegro A4988: https://github.com/jancumps/pio_a4988_stepper

Build instructions

A CMake file is available as a Gist, together with the example source.
Copy pico_sdk_import.cmake from the Picos C SDK root, or from their GitHub, to your project root.
Then build as you'd do with any Pico project.

The pio_stepper_lib is hosted on GitHub. You don't need to clone or build it, to use the library in your project. CMake does that for you.
The project with all necessary files: pico_stepper_2motors_20250421.zip. (includes .uf2 binary that you can drop on a Pico 1).

next post:  Raspberry PIO stepper library documentation - 4: understand the step frequency 

view all posts

  • Sign in to reply
  • Jan Cumps
    Jan Cumps 12 days ago in reply to Jan Cumps

    This example uses 4 physical Allegro A4988 stepper motor drivers, but the software only uses one driver object.

    I've linked all ~enable and ms1..3 pins of these drivers. These are all controlled by that single driver object. So they all get enabled at the same time, and use the same micro-step settings.

    The DIR and STEP signals are directly controlled from PIO, and don't use the driver object.

    This is similar to real world applications. You 'll see that even the 3 micro step pins are often hard-wired on the PCB.
    If some motors only have to work sometimes, it makes sense to give them their own driver to control the ~enable pin - or just use GPIO to control their ~enable pin ...

    #include "hardware/pio.h" // some constant definitions used
    
    #include "pico/stdlib.h"  // for demo section (printf)
    #include <array>          // for demo section (commands container)
    #include <iterator>       // for demo section (commands container)
    #include <span>           // for demo section (commands container)
    #include <algorithm>      // for_each
    #include <numeric>        // for demo section (accumulate)
    
    import stepper;           // PIO stepper lib
    
    // TODO: adapt to your  board design
    // TODO: driver IC dependent. see dummy_driver.cpp
    import a4988_pico;
    using driver_t = a4988_pico::a4988_pico;
    
    const uint n_enable = 9U;
    const uint ms1 = 6U;
    const uint ms2 = 7U;
    const uint ms3 = 8U;
    
    std::array<driver_t, 1> drivers {{{n_enable, ms1, ms2, ms3}}}; // 1 driver shared accross the motors
    
    // #define MICROSTEP_8
    #undef MICROSTEP_8
    #ifdef MICROSTEP_8
    const float clock_divider = 3; // works well for 8 microsteps
    const uint microstep_x = 8;
    #else
    const float clock_divider = 16; // works well for no microsteps
    const uint microstep_x = 1;
    #endif
    
    
    // TODO: adapt to your  board design
    const uint motor0_dir =  4U; // implies that step is gpio 5
    const uint motor1_dir = 10U; // implies that step is gpio 11
    const uint motor2_dir = 12U; // implies that step is gpio 13
    const uint motor3_dir = 14U; // implies that step is gpio 15
    
    using motor_t = stepper::stepper_callback_controller;
    std::array<motor_t, 4> motors {{{pio0, 0}, {pio0, 1}, {pio0, 2}, {pio0, 3}}};
    
    std::array<volatile size_t, motors.size()> commands_per_motor;
    void on_complete(const motor_t &stepper) {
        size_t index = 0U;
        for (auto &m: motors) {
            if (&m == &stepper) {
                commands_per_motor[index] =  commands_per_motor[index] - 1;
                printf("motor %d executed command\n", index + 1);
                break;
            }
            index++;
        }
    }
    
    void init_pio() {
        // program the pio used for the motors
        // do this only once per used pio
        motor_t::pio_program(pio0); // not needed if all sms run on pio1
        // motor_t::pio_program(pio1); // not needed if all sms run on pio0
    
        // individual settings
        motors[0].pio_init(motor0_dir, clock_divider);
        motors[1].pio_init(motor1_dir, clock_divider);
        motors[2].pio_init(motor2_dir, clock_divider);
        motors[3].pio_init(motor3_dir, clock_divider);
        // common settings
        // initialise and enable the motor state machine
        for (auto &m: motors) {
            m.register_pio_interrupt(0, true);
            m.enable(true);
            // and the notification, for demo purpose
            m.on_complete_callback(on_complete);
        }
    }
    
    void init_everything() {
        stdio_init_all();
    
        // TODO: driver IC dependent. see dummy_driver.cpp
        for (auto &d: drivers) {
            d.init();
        }    
    
        init_pio();
    }
    
    // stepper demo: execute a series of commands ================================
    
    // struct to hold command and motor
    struct motor_command {
        stepper::command cmd;
        size_t motor;
        motor_command(stepper::command cmd, size_t motor) : cmd(cmd), motor(motor) {}
    };
    
    using commands_t = std::span<motor_command>;	
    
    void run_commands(const commands_t& cmd, uint delay0, uint delay1) {
        for (auto &m: motors) {
            m.set_delay(delay0);
        }
    
        for (auto& count: commands_per_motor) { count = 0; }
    
        for (auto& c : cmd) { 
            // increment commands expected for the motor
            commands_per_motor[c.motor] = commands_per_motor[c.motor] + 1;
        }
    
        for (auto& c : cmd) { 
            if (commands_per_motor[c.motor] > 0U) {
                motors[c.motor].take_steps(c.cmd);
            }
        }
    
        size_t todo = 0U;
        do {
            todo = std::accumulate(commands_per_motor.begin(),commands_per_motor.end(),0);
        } while (todo > 0U);
    
        // sleep_ms(100); // pause for demo purpose    
    }
    
    void full_demo(const commands_t & cmd) {
        // stepper driver IC should wake up here, if not yet
        // TODO: driver IC dependent. see dummy_driver.cpp
        for (auto &d: drivers) {
            d.enable(true);
        }    
        // sleep_ms(1); // some drivers require time between enable and step
    
        // demo 1 :run all in sequence
        for (auto &c: cmd) {
            std::array<motor_command,1> single_cmd {{c}};
            run_commands(single_cmd, 9000, 7000);
        }
    
        // demo 2: run all in parallel
        run_commands(cmd, 7000, 4300);
    
        // TODO: stepper driver IC can go back to sleep here
        // TODO: driver IC dependent. see dummy_driver.cpp
        for (auto &d: drivers) {
            d.enable(false);
        }    
    }
    
    int main() {
        init_everything();
    
        std::array<motor_command, 7> cmd{{
            { {250 * microstep_x, true}, 0},
            { {200 * microstep_x, true}, 1},
            { {300 * microstep_x, false}, 2},
            { {200 * microstep_x, false}, 3},
            { {100 * microstep_x, true}, 2},
            { {200 * microstep_x, false}, 1},
            { {150 * microstep_x, true}, 0}
        }};
    
     
        while (true) {
            full_demo(cmd);
            // sleep_ms(200); // pause for demo purpose
        }
        return 0;
    }

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • Jan Cumps
    Jan Cumps 12 days ago

    4 motors on 1 PIO block -

    1. running commands in sequence
    2. running all commands concurrently

    You don't have permission to edit metadata of this video.
    Edit media
    x
    image
    Upload Preview
    image

    Commands:

            { {200 * microstep_x, true}, 0},
            { {200 * microstep_x, true}, 1},
            { {300 * microstep_x, false}, 2},
            { {200 * microstep_x, false}, 3},
            { {10 * microstep_x, true}, 2},
            { {200 * microstep_x, false}, 1},
            { {20 * microstep_x, true}, 0}

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • DAB
    DAB 27 days ago

    Nice post Jan.

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
  • Jan Cumps
    Jan Cumps 28 days ago

    > The gaps in the traces are for demo purposes, to have a visual markers.

    Here is a capture with the "demo sleeps" removed.
    The gaps you see here, are when I ask the firmware to run steps sequential, and one motor waits for the other one to finish its step(s):

    image

    • Cancel
    • Vote Up 0 Vote Down
    • Sign in to reply
    • More
    • Cancel
element14 Community

element14 is the first online community specifically for engineers. Connect with your peers and get expert answers to your questions.

  • Members
  • Learn
  • Technologies
  • Challenges & Projects
  • Products
  • Store
  • About Us
  • Feedback & Support
  • FAQs
  • Terms of Use
  • Privacy Policy
  • Legal and Copyright Notices
  • Sitemap
  • Cookies

An Avnet Company © 2025 Premier Farnell Limited. All Rights Reserved.

Premier Farnell Ltd, registered in England and Wales (no 00876412), registered office: Farnell House, Forge Lane, Leeds LS12 2NE.

ICP 备案号 10220084.

Follow element14

  • X
  • Facebook
  • linkedin
  • YouTube