ESP32-C3 servo
Sweeps an SG90 end to end with a 50 Hz LEDC channel on GPIO 4: the pulse width is the angle, and the hardware keeps the frame while the core sleeps. The whole build, an ESP32-C3 and 1 more part, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Beginner About 15 minutes to build and run. Runs in your browser. Free, and no account needed.
The parts retail for about $15.50. On Mokxi they're free.
What you'll learn
- Servo pulse width sets the angle
- Hardware PWM with LEDC on an ESP32
- Turning microseconds into timer counts
- Clamping an angle to the safe range
- Difficulty: Beginner
- Time: About 15 minutes
- Board: ESP32-C3-DevKitM-1
- Parts: 3
How it works
A hobby servo does not want PWM for dimming. It wants a pulse whose width sets the angle, repeated 50 times a second. 544 microseconds is one end of the travel and 2400 microseconds is the other. The ESP32-C3's LEDC hardware makes that pulse exactly on GPIO 4.
The sketch sets LEDC to 50 Hz with 14 bits, so each 20 ms frame has 16384 counts. It turns a pulse width in microseconds into counts, and an angle into a pulse width.
It starts at 90 degrees, then sweeps from 0 to 180 and back in 5 degree steps, waiting 40 ms between steps, and prints a message at each end.
Parts list
3 parts, plus the jumper wires. They retail for about $15.50; on Mokxi they're free.
- 1 × ESP32-C3-DevKitM-1In Handheld Console Kit, Desk Buddy Kit.
- 1 × Full-size breadboard
- 1 × Hobby servoIn Halloween Prop Kit, Christmas Lights Kit, Starter Projects Kit.
Wiring table
Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.
| Part and pin | Connects to |
|---|---|
| Hobby servo pin PWM | ESP32-C3-DevKitM-1 pin 4 |
| Hobby servo pin VCC | ESP32-C3-DevKitM-1 pin 5V |
| Hobby servo pin GND | ESP32-C3-DevKitM-1 pin GND |
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Servo: an SG90 swept from end to end by the LEDC hardware.
//
// A servo does not want PWM in the dimming sense. It wants a pulse *width*,
// repeated fifty times a second: 544 us for one end of the travel and 2400 us
// for the other, the Arduino Servo library's numbers and the servo part's
// defaults. Between pulses it holds whatever it was last told.
//
// That is still a PWM waveform, so LEDC can make it, and making it in hardware
// is better than making it by hand: the pulse is exact, the core is asleep
// between frames, and nothing an interrupt does can stretch it.
//
// ledcAttach(pin, 50, 14) fifty frames a second, 16384 counts each
// ledcWrite(pin, duty) duty counts of high out of 16384
//
// A frame is 20000 us, so one count is 20000 / 16384 = 1.22 us and a pulse of
// `us` microseconds is `us * 16384 / 20000` counts. The multiplication is done
// in 32 bits with the division last, so 544 us is 445 counts and 2400 us 1966.
//
// Wire the servo's brown wire to GND, red to 5V and orange to GPIO 4.
const uint8_t SERVO_PIN = 4;
const uint32_t FRAME_HZ = 50;
const uint8_t FRAME_BITS = 14;
const uint32_t FRAME_US = 1000000UL / FRAME_HZ; // 20000
const uint32_t FRAME_COUNTS = 1UL << FRAME_BITS; // 16384
// The Arduino Servo library's window, and how far it turns across it.
const uint32_t MIN_US = 544;
const uint32_t MAX_US = 2400;
const int SWEEP_DEG = 180;
uint32_t countsForMicros(uint32_t us) {
return (us * FRAME_COUNTS) / FRAME_US;
}
void writeAngle(int degrees) {
if (degrees < 0) {
degrees = 0;
}
if (degrees > SWEEP_DEG) {
degrees = SWEEP_DEG;
}
uint32_t us = MIN_US + ((MAX_US - MIN_US) * (uint32_t)degrees) / (uint32_t)SWEEP_DEG;
ledcWrite(SERVO_PIN, countsForMicros(us));
}
void setup() {
Serial.begin(115200);
if (!ledcAttach(SERVO_PIN, FRAME_HZ, FRAME_BITS)) {
Serial.println("no LEDC channel free for the servo");
return;
}
writeAngle(90);
delay(500);
}
void loop() {
for (int angle = 0; angle <= SWEEP_DEG; angle += 5) {
writeAngle(angle);
delay(40);
}
Serial.println("at 180");
for (int angle = SWEEP_DEG; angle >= 0; angle -= 5) {
writeAngle(angle);
delay(40);
}
Serial.println("back at 0");
}
Code walkthrough
The key functions of the sketch, and what each one does.
countsForMicros sketch.ino
Converts a pulse width into LEDC counts. A frame is 20000 microseconds and has 16384 counts, so the answer is us * FRAME_COUNTS / FRAME_US. The multiplication comes first and the division last, so no precision is lost in whole-number math.
uint32_t countsForMicros(uint32_t us) {
return (us * FRAME_COUNTS) / FRAME_US;
}writeAngle sketch.ino
Clamps the angle to 0 to 180, then maps it onto the pulse range from MIN_US to MAX_US. It hands the result through countsForMicros to ledcWrite, which sets the pulse on the servo pin.
void writeAngle(int degrees) {
if (degrees < 0) {
degrees = 0;
}
if (degrees > SWEEP_DEG) {
degrees = SWEEP_DEG;
}
uint32_t us = MIN_US + ((MAX_US - MIN_US) * (uint32_t)degrees) / (uint32_t)SWEEP_DEG;
ledcWrite(SERVO_PIN, countsForMicros(us));
}setup sketch.ino
Starts serial and calls ledcAttach for 50 Hz and 14 bits on GPIO 4. If no channel is free, it says so and stops. Otherwise it moves the servo to 90 degrees and waits half a second.
void setup() {
Serial.begin(115200);
if (!ledcAttach(SERVO_PIN, FRAME_HZ, FRAME_BITS)) {
Serial.println("no LEDC channel free for the servo");
return;
}
writeAngle(90);
delay(500);
}loop sketch.ino
Sweeps the angle from 0 up to 180 in steps of 5, with 40 ms between steps, prints "at 180", then sweeps back down and prints "back at 0".
void loop() {
for (int angle = 0; angle <= SWEEP_DEG; angle += 5) {
writeAngle(angle);
delay(40);
}
Serial.println("at 180");
for (int angle = SWEEP_DEG; angle >= 0; angle -= 5) {
writeAngle(angle);
delay(40);
}
Serial.println("back at 0");
}Troubleshooting
The usual mistakes with this circuit, and the fix for each.
- The servo jitters or does not move This project
Check that the signal wire is on GPIO 4, the servo has 5V, and its ground joins the board's ground. - The servo hits its end stops This project
MIN_USandMAX_USset the travel. A real servo may need a narrower range than 544 to 2400 microseconds. - The servo jitters, twitches or resets the board when it moves. Hobby servo
Power it from its own 5 V supply, not the Arduino 5 V pin, and join the grounds. A hobby servo can pull several hundred milliamps. Add a 100 uF to 470 uF capacitor across its supply. - The servo hums at the end of its travel or will not reach full angle. Hobby servo
Stay inside the pulse range, about 1000 to 2000 us for most hobby servos. Pushing past the end stop makes it strain. Calibrate the min and max pulse for your servo. - Upload fails or the board is not found by the computer. ESP32-C3-DevKitM-1
Pick the ESP32-C3 board in your tools and the right port. If it still fails, hold BOOT, tap RESET, then release BOOT to enter download mode and try again. - GPIO pins act strangely or the board resets. ESP32-C3-DevKitM-1
GPIO is 3.3 V only, so never feed it 5 V. GPIO 18 and 19 are USB, and GPIO 2, 8 and 9 are boot strapping pins. Avoid loading them at reset.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.