Rack-and-Pinion Hopping Leg Mechanism

by Angella_B in Circuits > Robots

21 Views, 1 Favorites, 0 Comments

Rack-and-Pinion Hopping Leg Mechanism

View recent photos.jpeg (1).png

In this project, I set out to build a mechanical hopping leg — but with one key twist: no springs allowed. Springs are a common go-to for creating bouncing or jumping motion, so I challenged myself to explore alternative ways to generate vertical movement. The result is a springless hopping mechanism that relies on using an ODrive S1, MK8325s motor and Rack & Pinion Gears.

Supplies

  1. ODrive S1 + M8325s Motor Kit
  2. M3 + M4 nuts & screws
  3. 2 Limit Switches
  4. M2 screws for limit switches
  5. 1.5 in x 1.5 in 80/20 Bars
  6. Arduino Uno
  7. Power Supply
  8. Wires
  9. Soldering Iron
  10. 3D Printer

Configuring the ODrive - Hardware

soldering_thermistors.png

First I followed the S1 M8325S Kit Build Guide for the hardware setup of the ODrive.

The two extra thin black wires connected to the motor are the thermistors, that have to be soldered. Solder two of the wires from the S1 harness kit to the motor thermistor wires and insert them into pins 13 and 14 of the S1 IO connector, marked thermistor+ and thermistor- in the datasheet.

Configuring the ODrive - Software

odrive_param.png
odrive_control_mode.png
odrive_interfaces.png

Then I continued following the S1 M8325S Kit Build Guide for the software setup of the ODrive

I included some images to show what settings I used for the GUI setup

NOTE: It's important to check the UART box in Interfaces tab so that the ODrive can work with the Arduino Uno later on.

3D Printing Base Setup

Test_setup.png
IMG_4595.jpg
initial_setup.png

To securely mount the ODrive motor on a table, I designed a simple base using Onshape. The goal was to create a stable structure that could keep the motor in place during testing.

The base was modeled with mounting holes aligned to the ODrive motor's dimensions and included a flat bottom for easy clamping to the work surface. Once the design was complete, I exported it as an STL file and 3D printed the part using PLA filament at standard settings (0.2 mm layer height, 20% infill).


Here is the link to all the CAD files: https://cad.onshape.com/documents/bcc229f5a417fd2ac0d7f15d/w/16e4feb4103ded03b4a0785d/e/2af4c2973f524ff89682b129?renderMode=0&uiState=682484de5f6e0a08ab15530f


Here is a video using the GUI Setup to Control the Motor

https://drive.google.com/file/d/1vbm8oAnS844S32nZnzW95HT13Gihc0by/view?usp=drive_link

Designing Vertical Test Setup

Mounting Bracket.png

To test the hopping motion in a controlled environment, I designed a vertical test rig using an 80/20 aluminum extrusion bar (1.5" x 1.5"). The goal was to create a bracket that could easily slide up and down the bar while keeping the mechanism aligned and constrained to vertical motion.

Using Onshape, I designed a custom sliding bracket that:

  1. Fits snugly around the 1.5" x 1.5" 80/20 bar with just enough clearance for smooth movement
  2. Has mounting holes for attaching the rack, or any other moving components

The bracket was 3D printed in PLA and tested for fit and movement. I made sure it could move freely without wobble but also wouldn’t tilt or jam under load. Then I attached the bracket to the motor heater plate as shown in the image.

Building a Supportive Base for Vertical Test Setup

sliding t.png
IMG_5467.jpg
IMG_5466.jpg

Since the sliding bracket moves along a vertical 1.5" x 1.5" 80/20 bar, I needed a stable structure to keep the entire test rig upright during operation. To achieve this, I built a square base using spare 80/20 parts I had on hand. This base provides enough lateral support to prevent the setup from tipping or shifting during motion.

To assemble the frame, I:

  1. 3D designed and printed custom T-slot nuts to fit into the aluminum extrusion channels
  2. Used an L-bracket for added reinforcement at the base joints
  3. Connected the 80/20 bars at right angles to form a rigid and balanced platform

This approach let me quickly prototype a solid base without needing additional metal hardware, and the 3D printed T-slot nuts held up well.


Here is a link to the CAD files for the T-slot Nuts + L Bracket

https://cad.onshape.com/documents/929a30d47d67571c0ba3c844/w/9c520307652e9d741a354022/e/4906d4952693d01db1bfb2cf?renderMode=0&uiState=6824d1e16f29a65e94b6e931

Designing Rack & Pinion Mechanism

rackholder.png

To convert the rotational motion of the ODrive motor into the linear motion needed for hopping, I designed a rack and pinion mechanism. The idea was to have the motor drive a pinion gear, which then moves a rack (a straight gear) up and down — simulating a spring-like push-off without actually using any springs.

Using Onshape, I adapted a pinion gear from McMaster Carr to attach directly to the motor and ensured proper gear meshing with the rack. I paid close attention to:

  1. Alignment guides to keep the rack constrained to vertical movement
  2. Clearance between moving parts to prevent jamming or binding

However, after testing some movement with this mechanism, it was clear that the rack needed more support so I added reinforcements.

Rack Design

rack_CAD.png
rack cad.png
IMG_5805.jpg

Here is a closeup of the rack design. I adapted a McMaster-Carr gear rack design into 6-inch modular segments to accommodate 3D printer bed size limitations, and created a custom connector that mechanically joins the rack segments. Each rack segment includes integrated hexagonal slots at the ends to hold nuts, and the connector features matching screw holes that align with these slots, allowing the segments to be securely fastened together using machine screws. This design ensures strong mechanical connections and maintains accurate tooth alignment for seamless gear engagement across multiple segments. For now, I have just connected two segments but more can be connected!

Continuation of Designing Rack & Pinion Mechanism

setup_vertical.png
connector.png

In this CAD design I added reinforcements on the other side so that the rack holder would not deflect as the gear teeth are in contact. Similar to the rack design I connected these two parts using the same connecter with nuts and screws.

3D Printing + Assembling Parts - Bracket + Rack Holder

View recent photos.jpeg.png

Here is the full CAD assembly on Onshape, it includes the Bracket, Rack Holder, the Support, Rack & Pinion Gears, etc.

https://cad.onshape.com/documents/3e80a7851d16672e080ff565/w/67f7b51e919d92a93418a3aa/e/ab6dfdd811309465646ad821?renderMode=0&uiState=6824ceb82c3f6304d8053535

Assembling Parts - Foot

IMG_5819.jpg

After 3D printing the foot, in order to attach the foot to the rack, I press-fit a 3/16" dowel pin into the 3D-printed foot, allowing it to connect to the rack via a friction-fit interface. This design acts as a mechanical fuse—if the motor makes sudden or forceful movements, the dowel allows the foot to detach without damaging the rack or other components, making the system more modular

In addition to the dowel pin, I mounted two limit switches by using the switches themselves as drilling templates to mark and drill precise mounting holes. I then threaded M2 screws directly through the PLA to secure the switches in place. One switch is positioned at the top to make contact with the rack holder, serving as an upper limit, and the other is mounted at the bottom of the foot to detect when it contacts the ground, providing a lower limit. This setup ensures the switches stay firmly in place and accurately detect end positions during motion.

Setting Up ODrive With Arduino Uno - Hardware + Software

IMG_5820.jpg

I followed these steps to connect the Arduino to the ODrive, it involved some more soldering.

https://docs.odriverobotics.com/v/latest/guides/arduino-uart-guide.html

I attached the Arduino Uno to the back of the sliding bracket using double-sided foam tape, allowing it to move along with the motor assembly. I also used zip-ties to tie the wires together. This prevents the wires from getting caught or strained during motion, keeps the electronics compact and mobile, and simplifies cable management.

Then in Arduino, make sure to download the ODrive library.

Testing Arduino Sample Code With Rack Mechanism

test_arduino_code.png

Once you open up Arduino, you can head to Files -> Examples -> ODriveArduino -> SineWaveUART

Make sure in the SineWaveUART file you change the baud rate to match what you configure in the ODrive, in this case in the beginning we set it to 115200.

Here is a video showing the movement of the rack with the Arduino ODrive Sample code where the position moves in a sinusoidal function.

https://drive.google.com/file/d/13eq2fgwSOlo1XbgSIoTTkx7ppik9NrXs/view?usp=drive_link

Arduino Code - Using Limit Switch to Control Position

limit switch.png

Now I will integrate the limit switches to the Arduino Code, to start download the library EZButton in Arduino. Then connect two wires to the limit switch, connect the yellow wire to Digital Pin 7 and the white wire to GND in the Arduino.

IMPORTANT NOTE: Connect the ODrive to your computer to add some initialization parameters so that position can be changed. I followed the these steps to create an "Absolute Encoder Reference Frame". This is essential for the homing sequence later on

https://docs.odriverobotics.com/v/latest/manual/control.html#custom-user-reference-frame


Here is a video showing how the motor moves when the top limit switch is pressed!

https://drive.google.com/file/d/1hWuqpaj8jvUjNWAvqE04MLmNuJJtLe04/view?usp=drive_link


Here is the Arduino Code to test the limit switch:

#include <ODriveUART.h>
#include <SoftwareSerial.h>
#include <ezButton.h>

// Documentation for this example can be found here:
// https://docs.odriverobotics.com/v/latest/guides/arduino-uart-guide.html


////////////////////////////////
// Set up serial pins to the ODrive
////////////////////////////////

// Below are some sample configurations.
// You can comment out the default one and uncomment the one you wish to use.
// You can of course use something different if you like
// Don't forget to also connect ODrive ISOVDD and ISOGND to Arduino 3.3V/5V and GND.

// Arduino without spare serial ports (such as Arduino UNO) have to use software serial.
// Note that this is implemented poorly and can lead to wrong data sent or read.
// pin 8: RX - connect to ODrive TX
// pin 9: TX - connect to ODrive RX
SoftwareSerial odrive_serial(8, 9);
unsigned long baudrate = 115200; // Must match what you configure on the ODrive (see docs for details)
ezButton lowlimitSwitch(7);
ezButton footlimitSwitch(6);


int STATE = 0;
// Teensy 3 and 4 (all versions) - Serial1
// pin 0: RX - connect to ODrive TX
// pin 1: TX - connect to ODrive RX
// See https://www.pjrc.com/teensy/td_uart.html for other options on Teensy
//HardwareSerial& odrive_serial = Serial1;
//int baudrate = 115200; // Must match what you configure on the ODrive (see docs for details)

// Arduino Mega or Due - Serial1
// pin 19: RX - connect to ODrive TX
// pin 18: TX - connect to ODrive RX
// See https://www.arduino.cc/reference/en/language/functions/communication/serial/ for other options
// HardwareSerial& odrive_serial = Serial1;
// int baudrate = 115200; // Must match what you configure on the ODrive (see docs for details)


ODriveUART odrive(odrive_serial);

float pos_val = 0;

//float pos_val = odrive.getPosition();

void setup() {
int incomingByte = 1;
odrive_serial.begin(baudrate);
Serial.begin(115200); // Serial to PC
odrive.setParameter("axis0.pos_estimate", 0);

//odrive.setPosition(pos_val);

// LIMIT SWITCH SETUP
lowlimitSwitch.setDebounceTime(50);
footlimitSwitch.setDebounceTime(50);
//pos_val = odrive.getPosition();
}

void loop() {

lowlimitSwitch.loop();
//Serial.println(odrive.getPosition());
// }
// In this part, if the limit switch is pressed
// Then the motor should increase its position by 1
if (lowlimitSwitch.isReleased()){
pos_val += 1;
delay(10);
odrive.setPosition(pos_val,0);
Serial.println(pos_val);
}

}

Arduino Code - Homing Sequence

homing_sequence.png

Now I am configuring the limit switch to act as a collision detection sensor, allowing the motor to recognize when it is about to collide with the foot. When triggered, the limit switch sends a signal to the Arduino, declare that position as 0 and then make the motor move away from the limit switch. Here I introduce STATES, STATE = 0, is the homing sequence.


Here is a video of the homing sequence with the rack in action!

I tested it without the foot first:

https://drive.google.com/file/d/1DzOEt6328DQ8gOyD4z-eLsxyG4nFN_Uq/view?usp=sharing

And then with the foot:

https://drive.google.com/file/d/129548ees27mhqt2HS2N1NHHH4AVsGpBU/view?usp=sharing


Arduino Code:

#include <ODriveUART.h>
#include <SoftwareSerial.h>
#include <ezButton.h>

// Documentation for this example can be found here:
// https://docs.odriverobotics.com/v/latest/guides/arduino-uart-guide.html


////////////////////////////////
// Set up serial pins to the ODrive
////////////////////////////////

// Below are some sample configurations.
// You can comment out the default one and uncomment the one you wish to use.
// You can of course use something different if you like
// Don't forget to also connect ODrive ISOVDD and ISOGND to Arduino 3.3V/5V and GND.

// Arduino without spare serial ports (such as Arduino UNO) have to use software serial.
// Note that this is implemented poorly and can lead to wrong data sent or read.
// pin 8: RX - connect to ODrive TX
// pin 9: TX - connect to ODrive RX
SoftwareSerial odrive_serial(8, 9);
unsigned long baudrate = 115200; // Must match what you configure on the ODrive (see docs for details)
ezButton lowlimitSwitch(7);
ezButton footlimitSwitch(6);


int STATE = 0;
// Teensy 3 and 4 (all versions) - Serial1
// pin 0: RX - connect to ODrive TX
// pin 1: TX - connect to ODrive RX
// See https://www.pjrc.com/teensy/td_uart.html for other options on Teensy
//HardwareSerial& odrive_serial = Serial1;
//int baudrate = 115200; // Must match what you configure on the ODrive (see docs for details)

// Arduino Mega or Due - Serial1
// pin 19: RX - connect to ODrive TX
// pin 18: TX - connect to ODrive RX
// See https://www.arduino.cc/reference/en/language/functions/communication/serial/ for other options
// HardwareSerial& odrive_serial = Serial1;
// int baudrate = 115200; // Must match what you configure on the ODrive (see docs for details)

ODriveUART odrive(odrive_serial);

float pos_val = 0;


//float pos_val = odrive.getPosition();

void setup() {
int incomingByte = 1;
odrive_serial.begin(baudrate);
//odrive.setParameter("axis0.controller.config.control_mode", CONTROL_MODE_POSITION_CONTROL);
odrive.setParameter("axis0.pos_estimate", 0);
//odrive.setParameter("axis0.controller.config.input_mode",INPUT_MODE_POS_FILTER);
Serial.begin(115200); // Serial to PC

// LIMIT SWITCH SETUP
lowlimitSwitch.setDebounceTime(50);
footlimitSwitch.setDebounceTime(50);
//pos_val = odrive.getPosition();
}

void loop() {

// MOTION GOING DOWN IS -
// MOTION GOING UP IS +

while(STATE == 0){
lowlimitSwitch.loop();
// If the limit switch is pressed it will change states
// And also change its "Zero"
// Then it will move 0.5 away from the limit switch
if (lowlimitSwitch.isReleased()){
STATE = 1;
odrive.setParameter("axis0.pos_estimate", 0);
pos_val = 0;
odrive.setPosition(pos_val + 0.5);
pos_val = 0.5;
Serial.println("Stop");
//delay(10);
//odrive.setParameter("axis0.controller.input_pos",10);
//odrive.setPosition(pos_val);
//Serial.println(pos_val);
}
// The motor will move down to reach the limit switch initially
odrive.setPosition(pos_val);
delay(100);
pos_val -= 0.1;
}
}


Arduino Code - Small Hopping Motion!

motion.png

Now to try and create a hopping motion, I created two more states.

STATE = 0 - Homing Sequence

STATE = 1 - To manually test the top position of the rack

STATE = 2 - To move between two points


Here is a video where I make the delay time between the positions equal, the foot just stays on the ground

https://drive.google.com/file/d/1HzWkHnKpdtFeXFSptnhvMHf-J9F-Ju8K/view?usp=drive_link


Here is a video where I change the delay time as shown in the Arduino Code, the foot lifts up a little!

https://drive.google.com/file/d/1srPWZupnLcUV_Msb81ls0RtoNmsI97lT/view?usp=sharing


Arduino Code:

#include <ODriveUART.h>
#include <SoftwareSerial.h>
#include <ezButton.h>

// Documentation for this example can be found here:
// https://docs.odriverobotics.com/v/latest/guides/arduino-uart-guide.html


////////////////////////////////
// Set up serial pins to the ODrive
////////////////////////////////

// Below are some sample configurations.
// You can comment out the default one and uncomment the one you wish to use.
// You can of course use something different if you like
// Don't forget to also connect ODrive ISOVDD and ISOGND to Arduino 3.3V/5V and GND.

// Arduino without spare serial ports (such as Arduino UNO) have to use software serial.
// Note that this is implemented poorly and can lead to wrong data sent or read.
// pin 8: RX - connect to ODrive TX
// pin 9: TX - connect to ODrive RX
SoftwareSerial odrive_serial(8, 9);
unsigned long baudrate = 115200; // Must match what you configure on the ODrive (see docs for details)
ezButton lowlimitSwitch(7);
ezButton footlimitSwitch(6);

int STATE = 0;
// Teensy 3 and 4 (all versions) - Serial1
// pin 0: RX - connect to ODrive TX
// pin 1: TX - connect to ODrive RX
// See https://www.pjrc.com/teensy/td_uart.html for other options on Teensy
//HardwareSerial& odrive_serial = Serial1;
//int baudrate = 115200; // Must match what you configure on the ODrive (see docs for details)

// Arduino Mega or Due - Serial1
// pin 19: RX - connect to ODrive TX
// pin 18: TX - connect to ODrive RX
// See https://www.arduino.cc/reference/en/language/functions/communication/serial/ for other options
// HardwareSerial& odrive_serial = Serial1;
// int baudrate = 115200; // Must match what you configure on the ODrive (see docs for details)

ODriveUART odrive(odrive_serial);

float pos_val = 0;

void setup() {
int incomingByte = 1;
odrive_serial.begin(baudrate);
//odrive.setParameter("axis0.controller.config.control_mode", CONTROL_MODE_POSITION_CONTROL);
odrive.setParameter("axis0.pos_estimate", 0);
//odrive.setParameter("axis0.controller.config.input_mode",INPUT_MODE_POS_FILTER);
Serial.begin(115200); // Serial to PC

// LIMIT SWITCH SETUP
lowlimitSwitch.setDebounceTime(50);
footlimitSwitch.setDebounceTime(50);
}

void loop() {

// MOTION GOING DOWN IS -
// MOTION GOING UP IS +

while(STATE == 0){
lowlimitSwitch.loop();
if (lowlimitSwitch.isReleased()){
STATE = 1;
odrive.setParameter("axis0.pos_estimate", 0);
pos_val = 0;
odrive.setPosition(pos_val + 0.5);
pos_val = 0.5;
//Serial.println("Stop");
}
odrive.setPosition(pos_val);
delay(100);
pos_val -= 0.1;
}

while(STATE == 1){
pos_val += 0.1;
delay(100);
if(pos_val < 3.5){
odrive.setPosition(pos_val);
}
else{
STATE = 2;
}
}

while(STATE == 2){
odrive.setPosition(0.75);
delay(1000);
odrive.setPosition(3.2);
delay(500);
}
Serial.println(pos_val);
//Serial.println(odrive.getPosition());
}

Future Work

One area for future work in this project is the use of limit switches. Currently, only one limit switch is implemented in the code, primarily to detect when the foot makes contact with the rack holder to determine position 0. However, integrating another limit switch to detect the ground would be helpful.

The addition of a third limit switch at the top of the rack would also allow for better positional awareness, especially during the STATE 2 sequence, where the foot’s position was manually set. With a second switch, this step could be automated and included as part of a proper homing routine, ensuring repeatability and reducing the need for manual calibration.