Myo-20: Professional Motor Control for Robotics
Myo-20 is a compact, powerful motor control board designed for robotics research, student projects, and startup applications. Bring professional-grade motor control to your robotics platform with minimal setup.
QUICK LINKS:
Features
Multi-Protocol Communication
- USB (GUI control, CAN Config, Python script)
- CAN Bus (multi-motor, daisy-chainable)
- UART (Motor Pilot compatible)
- SWD (hardware debugging)
Three Control Modes
- Speed control (RPM, max 144 rpm)
- Torque control (current)
- Position control (angle (rad))
- 8 kHz control loop
Production-Ready Protection
- ESD protection on all external data lines
- Reverse polarity protection
- Inrush current limiting (soft start)
- Over-current thermal monitoring
- Encoder loss detection
FOC Algorithm
- Field-Oriented Control at 24 kHz
- <5% speed overshoot
- <5% torque ripple
- Full algorithm visibility for research
Flexible Encoder Support
- On-board quadrature encoder (up to 1024 PPR)
- External SPI encoder (AS5047p compatible)
- ABI external encoder support
Pinout
This is looking at the JST when the USB is to the left of the JST, where the output shaft is at the top.

Teensy 4.1 Wiring
The library hardcodes UART to use Serial1 and CAN to use CAN3. To connect just for Serial and CAN, you need these pins, and also a CAN transceiver (we use these ones):
| Teensy 4.1 Pin # |
Myo-20 or CAN Transceiver Pin # |
| GND |
any GND (2, 5, 8, 16) and CAN Transceiver Module GND |
| 0 |
11 |
| 1 |
13 |
| any 3.3 V |
CAN Transceiver Module 3.3 V |
| 30 |
CAN RX |
| 31 |
CAN TX |
| CANL from transceiver |
1 or 4 |
| CANH from transceiver |
3 or 6 |
Twist the CANL and CANH wires together, and you can run a GND wire next to it for some amount of shielding. There are two sets of CAN on the Myo-20 for daisy-chaining purposes.
Power Connections
XT30, the + and - on the XT30 are accurate. We used a 6S LiPo for most of our testing, but the bus voltage can go up to 48V if you want more RPM.

CAN Termination Switch
CAN Termination: If the Myo-20 is the end of the CAN bus, slide the switch to toggle a split 120Ω resistor between CANH and CANL. Slide the switch towards the middle of the board to toggle termination resistors, away to disable

USB Usage
Needs the CP2102N USB to UART driver. To install it, follow these instructions
We used the CP210x Universal Windows Driver.
After that's installed and the device is detecting on the Ports (COM & LPT) in the Device Manager, you can run the python script to configure CAN (COMING SOON), or run basic controls (COMING SOON).
To use the Motor Pilot GUI, you need a windows computer. Go to download STM32 Motor Control Software Development Kit, hover/click the tools tab at the top, and then motor pilot. Or, just search for motor pilot and open it directly. From there, press ctrl + o to open up PositionControlApp. You can then press connect when the USB is plugged in, and control it directly. This method does not allow torque and position control, but it is fun to play around with.
Teensy Setup
We used PlatformIO, if you have never used it before check out a tutorial to setup a basic tutorial for a Teensy 4.1.
In the generated .ini file, add libdeps like this:
[env:teensy41]
platform = teensy
board = teensy41
framework = arduino
monitor_speed = 115200
lib_deps =
https://github.com/tonton81/FlexCAN_T4.git
Even if you're UART only, you still need the libdep because the library includes it, and won't compile otherwise.
Either download and drag the Myo.cpp and Myo.h into the src and include folders respectively, or make files with those names and copy paste the code in there. You can look at our example code, which works with both CAN and UART for reference. Here's a TLDR if you want to make your own code:
Need these:
#include <Arduino.h>
#include "Myo.h"
// true if using CAN, false if using UART
#define USE_CAN true
Myo motor(0x321, // outgoing CAN ID -> controller's CAN_NodeID filter
500000, // CAN baudrate [bit/s]
1843200, // ASPEP UART baudrate [bit/s]
0, // MCP motor index
0x322); // incoming CAN ID -> controller's CAN_ReplyID
In your void setup(),
Serial.begin(115200);
while (!Serial && millis() < 4000)
;
if (!motor.begin(USE_CAN))
{
Serial.println("WARNING: link did not come up cleanly.");
}
Recommended:
motor.onTelemetry(
[](const Myo::MotorTelemetry &telemetry)
{
Serial.printf(
"[RX] TELEMETRY | State: %d | Output: %.1f RPM | Rotor: %d RPM\n",
telemetry.state,
telemetry.rpm,
telemetry.rotorRpm);
});
motor.onAck(
[](const Myo::MotorAck &ack)
{
Serial.printf(
"[RX] ACK | Command: 0x%02X | Status: %s\n",
static_cast<uint8_t>(ack.command),
ack.success ? "OK" : "FAILED");
});
motor.onError(
[](const Myo::MotorError &error)
{
Serial.printf(
"[RX] ERROR | Bad Command: 0x%02X\n",
error.badCommand);
});
These basically decode what messages the Myo-20 sends back for you.
Teensy UART Usage
The Teensy Setup section configures everything, so as long as you have that, you're able to go to API functions and start controlling the Myo-20! Just make sure you define USE_CAN as false.
When you run the file, you can open up serial monitor with the shortcut ctrl + shift + p, and then typing in "serial". It should pop up.
Teensy CAN Usage
If you have configured CAN on the Myo-20, change the Myo motor() function to use your CAN ID and baudrate (default 0x321 and 500000). Otherwise, like the UART Usage, you're ready! Just make sure to define USE_CAN as true.
When you run the file, you can open up serial monitor with the shortcut ctrl + shift + p, and then typing in "serial". It should pop up.
API functions
Set a target first, then start the motor. Call motor.update() in your loop() or you won't see any responses.
void loop()
{
motor.update(); // decodes incoming ACKs, telemetry, and errors
}
Start / Stop
motor.start(); // begins running the last target you set
motor.stop(); // stops the motor, targets are kept
Use start only after you've set a speed, torque, or position ramp!
Speed Ramp
RPM is at the output shaft, ±144 max. Anything higher gets clamped, not rejected.
motor.setSpeed(rpm, rampMs);
motor.setSpeed(100.0f, 500); // ramp to 100 rpm over 500 ms
Torque Ramp
Amps of Iq current. Set rampMs to 0 for direct torque.
motor.setTorque(amps, rampMs);
motor.setTorque(1.0f, 500); // ramp to 1 A over 500 ms
motor.setTorque(1.0f, 0); // straight to 1 A
Position Control
Radians at the rotor — divide by 8 for the output shaft, so a full output turn is 6.28f * 8. Don't set durationSec to 0 (the PID acts weird and continues moving even after reaching the position). Over CAN the duration caps at 65.535 s.
motor.setPosition(radians, durationSec);
motor.setPosition(0.0f, 0.5f); // return to 0 rad over 0.5 s
motor.setPosition(50.27f, 2.0f); // one output revolution over 2 s
Telemetry
Ask for it, then read it in the callback (or poll it).
motor.requestTelemetry();
Myo::MotorTelemetry t;
if (motor.getTelemetry(t))
{
Serial.println(t.rpm); // output shaft RPM
}
Waiting on a Response
Blocks until the Myo-20 answers or the timeout runs out. Useful in setup() where there's no loop() running yet.
motor.setSpeed(100.0f, 500);
motor.receiveResponse(); // default 50 ms timeout
motor.receiveResponse(200); // or set your own
CAN Configuration
Both of these go out over UART, so UART must be wired even if you're using CAN. They change the setting on the Myo-20 and update the Teensy to match. The board remembers these, so you only need to do it once. We recommend making a separate file that you just flash once to configure the Myo-20, so you can unplug UART afterwards. We are also working on a python script that should be out in the next few days (as of writing 8/24) to configure the CAN, so that should be easier.
motor.setCanBaud(500000); // 500000 or 1000000
motor.setCanId(0x321); // the ID the Myo-20 listens on
You should manually change the motor object too in the code.
Myo motor(0x321, // outgoing CAN ID -> controller's CAN_NodeID filter
500000, // CAN baudrate [bit/s]
1843200, // ASPEP UART baudrate [bit/s]
0, // MCP motor index
0x322); // incoming CAN ID -> controller's CAN_ReplyID
setCanId() writes the value literally — set it to 333 and the board only answers to 0x333, not 0x321. Replies always come back on 0x322.
Keepalive (UART)
motor.ping(); // useful to make sure the Myo-20 is still alive
Future Documentation and Additions
We have a lot we can still do!
Future Documentation
- External encoder support (code already exists, just need to write docs for it)
- How to write custom FOC code (pinouts exist for it, no guide yet)
- Troubleshooting common problems guide
- More example code
Future Additions
- Switching between CAN FD and Classic 2.0B
- Increasing control loop frequency
- Features that you guys want (email us!)
Direct Support
Contributing
We welcome contributions!
- Found a bug?
- Have an example?
- Improve docs?
- Hardware improvement?
Open a support ticket with steps to recreate, or any of the above!
Specifications Summary
| Category |
Details |
| Power Supply |
24-48V DC |
| Speed Range |
±144 RPM |
| Control Methods |
Speed, Torque, Direct Torque, Position |
| FOC Loop Rate |
24 kHz (Switching Frequency) |
| Control Loop Rate |
8 kHz (Speed, Torque, Position) |
| Encoder |
1024 PPR (on-board or SPI) |
| Communication |
USB, CAN, UART |
| Protection |
ESD, Reverse polarity, Over-current, Thermal monitoring |
| Board Size |
82.2mm * 62.29mm |
| On Board Connector |
S20B-PHDSS (20-pin main) |
| Mating Receptacle |
PHDR-20VS, SPHD-001T-P0.5 Crimped Ends #26 - 22 AWG |