I’ve manually switched my boards to speed control mode when stuck in an off road situation. It’s worked to great effect to get me out of low traction situations.
Below is a plan to automate that and make it easy.
I’ve manually switched my boards to speed control mode when stuck in an off road situation. It’s worked to great effect to get me out of low traction situations.
Below is a plan to automate that and make it easy.
Bro!! This is suck as fuuuuuck
It writes the whole vesc configuration to the flash memory each time you press the button. It will kill the flash memory after many rewrites
I think that’s what metr does for modes as well… I will blow up the ESC before I rewrite enough to kill the flash memory ![]()
Ooo I had no idea this was something to look out for! Thanks for the catch.
How do other consumer grade electronics handle this problem?
Temporary changes go to ram, but vescuart doesn’t support that.
My solution is to send the throttle to esp32 and then send set.current or speed commands to vesc
I rather wear out my flash memory than have my controls go through an extra component. Are there any other Solutions?
My company’s claude code max plan said this ![]()
I read the whole thread. The core issue: jack.luis built an ESP32 sidecar that rewrites the entire VESC config to flash on every button press (current mode ↔ speed mode). Dimos15 rightly flagged that this kills flash. Dimos’s fix (route throttle through ESP32) jack rejects because it adds a failure point in the throttle path.
Here are three realistic solutions, best to most pragmatic:
My recommendation: option 2 if he wants the minimal change to the existing project, option 1 if he’s building for the long run.
This could be total bullahit and AI slop, or something useful. You tell me ![]()
Option 1 is very appealing because there’s no MCU needed and we don’t use our ADC pins anyways.
I’ve been going back and forth with my clanker and eventually got this. I asked it to explain briefly:
I built a button-and-knob system that toggles my 4WD VESC skateboard between torque control (normal riding) and synchronised speed control (low-traction crawl mode), with a knob to set the speed cap from 0.3 mph to 8 mph. Posting it here because the design might be useful to others, and the safety reasoning is worth sharing.
Standard 4WD VESC build with one master and three slaves on CAN. The only added hardware is on the master VESC’s COMM port:
That’s it. No ESP32, no Arduino, no buck converter, no level shifter.
A small VESC LispBM script runs on the master VESC. LispBM is built into VESC firmware 6.00+ and runs sandboxed alongside the motor control task.
throttle × knob limit, and drives all 4 motors at 50 Hz using set-rpm and canset-rpm. Each VESC runs its own speed PID locally with hall-sensor feedback.The button toggles a Lisp variable. The knob is read every loop tick.
The first version used an ESP32 sidecar that called setMcconf over UART to switch modes. Flagged on another thread that this writes flash on every button press, and Vedder confirmed conf-store writes to flash but conf-set does not. So the LISP version uses conf-set-style RAM-only operations exclusively — actually it doesn’t even need those, since set-rpm and app-ppm-detach are pure runtime state.
Zero flash wear after the one-time script install. Press the button a million times if you want.
The whole design is built around one rule: in current mode, nothing the script or master VESC does can affect the rider.
| Failure | Current mode | Speed mode |
|---|---|---|
| Master loses power | All 4 wheels keep running on PPM | Slaves resume PPM in <0.5 s |
| Master script crashes | All 4 wheels keep running on PPM | Slaves resume PPM in <0.5 s |
| CAN bus disconnects | All 4 wheels keep running on PPM | Slaves resume PPM in <0.5 s |
| Receiver dies | All 4 coast | All 4 coast |
| Slave VESC loses power | That wheel coasts; others continue | That wheel coasts; others continue |
| Knob wire shorts/breaks | No effect (knob unused in current mode) | Speed cap jumps within bounded range |
| Button stuck pressed | One toggle, then debounce holds steady | Same |
The worst case in speed mode is that the board reverts to current-mode coasting within 500 ms. Since speed mode is for low-traction surfaces, this is acceptable. The script never acts in current mode, so a script bug or master crash is invisible to the rider during normal riding.
After installing, I ride at low speed and yank the master’s CAN H wire. All 4 wheels keep going on their own PPM. That’s the proof the architecture is right — if a wheel had stopped, the throttle wiring would be wrong and I’d fix it before continuing.
For my hardware (190 KV motors, 48 V, 5.2:1 gear, 13″ wheels, 7 pole pairs), 8 mph at the wheel = 7,500 ERPM at the motor. That’s the script’s hard cap. The knob scales linearly from 300 ERPM (0.3 mph crawl) to 7,500 ERPM.
Ramp time isn’t set in code — I rely on each VESC’s local speed PID with Speed Kp = 0.004, Ki = 0.006 (Ki slightly higher than typical because hall sensors give clean feedback at zero RPM). The 50 Hz CAN command rate is fast enough that the rider’s throttle stick controls the ERPM target smoothly.
At centre stick the script stops sending CAN commands so the slaves time out and coast — otherwise the speed PID would fight to actively hold zero RPM, which feels wrong on a hill.
VESC has a built-in slave-VESC traction control that targets the slowest wheel. That works for most cases but it doesn’t give you a top-speed limit, and it doesn’t synchronise to a single throttle-controlled RPM target. For pulling the board through wet grass or gravel where I want a guaranteed crawl speed regardless of grip, I needed the explicit RPM control with a knob.
can-cmd is rate-limited to 2 calls/sec per device in VESC LispBM. My script doesn’t use it (uses canset-rpm which has no rate limit), but if you adapt this design to write configuration over CAN you’ll hit that limit fast.Happy to share the full LispBM script and wiring guide if there’s interest. Open to feedback on the safety reasoning — particularly if anyone sees a failure mode I’ve missed.
Below is the same result in the format of instructions.
No microcontroller required — runs entirely on the master VESC
VESC LispBM · VESC Tool · CAN Bus
This guide builds a button-and-knob mode switcher that toggles your 4WD electric skateboard between torque control (current mode) and synchronised speed control (speed mode), using only the VESC’s built-in LispBM interpreter.
The architecture is built around one safety guarantee: in current mode, no failure of the script, the master VESC, or the CAN bus can affect the rider. Every VESC reads the throttle PWM independently and runs its own PPM app. The script does nothing in current mode — it only acts when speed mode is engaged.
In speed mode, the script overrides each VESC’s local control to lock all 4 wheels to the same RPM, with a knob-controlled top speed. If the script or master fails in speed mode, the slave VESCs’ own PPM apps regain control within 0.5 seconds and the board reverts to coasting current-mode behaviour — an acceptable degradation since speed mode is only used in low-traction conditions.
Failure mode summary
All VESCs are configured with Timeout Brake Current = 0, so any loss of control input results in coasting, never braking.
Failure Current mode Speed mode Master loses power All 4 wheels keep running on PPM Slaves resume PPM in <0.5 s Master script crashes All 4 wheels keep running on PPM Slaves resume PPM in <0.5 s CAN disconnects All 4 wheels keep running on PPM Slaves resume PPM in <0.5 s Slave VESC loses power That wheel coasts That wheel coasts Receiver dies All 4 coast All 4 coast Knob wire fails No effect Speed cap jumps within bounded range
| Component | Purpose |
|---|---|
| 4× VESC (firmware 6.00+, FOC, hall sensors) | Motor controllers |
| 10kΩ Potentiometer (linear) | Speed limit knob → master VESC ADC1 |
| Momentary pushbutton | Mode toggle → master VESC ADC2 |
| 10kΩ resistor | Pull-up for the button line |
| 100nF ceramic capacitor | ADC noise filter on the pot wiper |
| Hookup wire + JST-PH connector | Connects to master VESC COMM port |
That is the entire BOM. No external microcontroller, no power supply, no level shifter.
NOTE: Your VESCs need firmware 6.00 or newer for LispBM. Update via VESC Tool first if needed.
vesc-project.com and download the latest VESC Tool for Windows. Run the installer with default options.NOTE: Updating firmware erases motor and app configurations. You will re-run motor detection in Part 3.
Wire everything with the battery fully disconnected.
| Connection | Destination |
|---|---|
| Battery positive | All 4 VESC power inputs |
| Battery negative | All 4 VESC GND inputs |
| PWM receiver signal | All 4 VESC PPM inputs in parallel |
| PWM receiver GND | Common GND with all VESCs |
NOTE: The throttle wires to all 4 VESCs in parallel — the same as a stock 4WD VESC build. This is the safety foundation. Every VESC has its own PPM source and runs its own PPM app. The master’s LispBM script overrides this only in speed mode.
Daisy-chain all 4 VESCs together with twisted pairs.
| Signal | Connection |
|---|---|
| Master VESC CAN H | VESC 2 CAN H → VESC 3 CAN H → VESC 4 CAN H |
| Master VESC CAN L | VESC 2 CAN L → VESC 3 CAN L → VESC 4 CAN L |
TIP: Twist the CAN H and CAN L wires together by hand even without a proper twisted-pair cable. Even a few twists per inch significantly reduces noise.
This is the only wiring beyond a stock 4WD build. Everything attaches to the master VESC’s COMM connector.
| COMM Pin | Function | Wire To |
|---|---|---|
| Pin 1 | GND | Pot outer pin + button leg |
| Pin 2 | +3.3V | Pot outer pin (other side) + 10kΩ pull-up resistor |
| Pin 3 | RX | Not used |
| Pin 4 | TX | Not used |
| Pin 5 | ADC1 | Pot wiper (centre pin) + 100nF cap to GND |
| Pin 6 | ADC2 | Button (other leg) + pull-up resistor (other end) |
Master VESC COMM Port:
+3.3V (Pin 2) ────┬──── 10kΩ ─────┬──── ADC2 (Pin 6) ──── Button ──── GND (Pin 1)
│ │
│ └─── (this junction is the button-read line)
│
└──── 10kΩ POT outer
│
├──── ADC1 (Pin 5) ──┐
│ │
│ [100nF cap]
│ │
└──── GND ──────────┴──── GND (Pin 1)
POT WIPER → ADC1
POT outer pins → 3.3V and GND
How it works:
(get-adc 0).(get-adc 1).NOTE: If your VESC has a different COMM connector pinout (some Stormcore variants differ), check your hardware’s documentation. The function names (ADC1, ADC2, 3.3V, GND) are universal — only the physical pin numbers may change.
Move the USB cable between the 4 VESCs and run setup on each one.
7 for pole pairs. Confirm hall sensor detection succeeds on all 6 positions. Complete the resistance and inductance measurements.Motor Settings > General, set motor current max and battery current max to safe values for your motors. Do not exceed motor or VESC ratings.This is the safety-critical setting that guarantees coast-on-failure behaviour.
App Settings > General.0.0 A.NOTE: With this set to 0, any loss of control input (PPM signal lost, CAN command stream stops, script crash) results in the motor coasting after the timeout period. Any non-zero value would cause active braking on failure, which is not acceptable for an off-road skateboard.
Every VESC runs the same PPM app — current control mode. Do this on each one.
App Settings > General. Set App to Use to PPM.App Settings > PPM.0.3 and Negative Ramping Time to 0.3. This is current-mode feel — not used in speed mode.NOTE: Configure this on the master VESC too, exactly like the slaves. The script will take over the master’s PPM input only when speed mode is engaged.
1. Enable Send Status Over CAN. Write App Configuration.2. Enable Send Status Over CAN. Write.3. Enable Send Status Over CAN. Write.4. Enable Send Status Over CAN. Write.Each VESC runs its own speed PID independently when receiving CAN RPM commands.
Motor Settings > PID Controllers.| Parameter | Value |
|---|---|
| Speed Kp | 0.004 |
| Speed Ki | 0.006 |
| Speed Kd | 0.0 |
TIP: Hall sensors give clean speed feedback all the way to zero RPM. This is why Ki can be slightly higher than the back-EMF-only default without causing oscillation at low speed.
The script runs only on the master VESC. Slaves run nothing.
;;; ─────────────────────────────────────────────────────────────────────────
;;; 4WD VESC Mode Switcher — Master VESC (ID 1)
;;; Coast-safe architecture
;;;
;;; CURRENT MODE (default):
;;; Script does nothing. All 4 VESCs run their own PPM apps independently.
;;; Any failure (script crash, master power loss, CAN disconnect) leaves
;;; every VESC reading its own throttle. Rider feels nothing.
;;;
;;; SPEED MODE (button engaged):
;;; Master detaches its PPM app and computes synchronised RPM target from
;;; throttle * knob limit. Drives itself with set-rpm and slaves with
;;; canset-rpm at 50 Hz. Slave VESCs' PPM apps still receive throttle in
;;; parallel - they're being overridden by the higher-priority CAN command.
;;; If the script stops, slaves time out (Timeout Brake Current = 0 ->
;;; coast), then their PPM apps resume control within 0.5 s.
;;;
;;; Hardware: 190KV motors | 48V | 5.2:1 gear | 13" tires | 7 pole pairs
;;; ─────────────────────────────────────────────────────────────────────────
;; ── Slave VESC IDs ───────────────────────────────────────────────────────
(define slave-1 2)
(define slave-2 3)
(define slave-3 4)
;; ── Speed mode tuning ────────────────────────────────────────────────────
;; ERPM at 8 mph for this hardware = 7,500
(define knob-max-erpm 7500.0)
(define knob-min-erpm 300.0) ; ~0.3 mph crawl when knob fully CCW
;; ── Throttle behaviour ───────────────────────────────────────────────────
(define ppm-deadband 0.05) ; +/-5% deadband around centre stick
;; ── Button handling ──────────────────────────────────────────────────────
;; ADC2 voltage: ~3.3V released, ~0V pressed (10k pull-up)
(define btn-press-v 1.0)
(define btn-release-v 2.0)
(define btn-state 'released)
(define btn-debounce-ticks 0)
;; ── Mode state ───────────────────────────────────────────────────────────
(define mode 'current)
;; ── Knob ─────────────────────────────────────────────────────────────────
(defun knob-fraction ()
(let ((v (get-adc 0)))
(if (< v 0.0) 0.0
(if (> v 3.3) 1.0
(/ v 3.3)))))
(defun current-erpm-limit ()
(+ knob-min-erpm
(* (knob-fraction) (- knob-max-erpm knob-min-erpm))))
;; ── Button (debounced edge detection) ────────────────────────────────────
(defun read-button ()
(let ((v (get-adc 1)))
(if (< v btn-press-v) 'pressed
(if (> v btn-release-v) 'released
btn-state))))
(defun button-pressed-edge ()
(let ((raw (read-button)))
(if (eq raw btn-state)
(progn (setq btn-debounce-ticks 0) nil)
(progn
(setq btn-debounce-ticks (+ btn-debounce-ticks 1))
(if (>= btn-debounce-ticks 3) ; 3 x 20ms = 60ms debounce
(progn
(setq btn-debounce-ticks 0)
(setq btn-state raw)
(eq raw 'pressed))
nil)))))
(defun apply-deadband (x)
(if (< (abs x) ppm-deadband) 0.0 x))
;; ── Mode entry / exit ────────────────────────────────────────────────────
;; Speed mode entry: detach master's PPM app so it stops driving the motor
;; from PPM. We will drive it directly with set-rpm.
(defun enter-speed-mode ()
(app-ppm-detach 1)
(setq mode 'speed)
(print "-> SPEED"))
;; Current mode entry: re-attach master's PPM app. Script becomes inert.
;; Slaves' PPM apps were never detached - they resume immediately when our
;; canset-rpm commands stop arriving (after their 0.5s timeout, coast).
(defun enter-current-mode ()
(app-ppm-detach 0)
(setq mode 'current)
(print "-> CURRENT"))
;; ── Speed mode driver ────────────────────────────────────────────────────
;; PPM x knob limit = ERPM target. All 4 wheels locked to same RPM.
;; At centre stick: stop sending CAN commands so slaves coast (timeout
;; brake = 0) instead of fighting the speed PID at zero.
(defun drive-speed-mode ()
(let ((throttle (apply-deadband (get-ppm)))
(limit (current-erpm-limit)))
(let ((target (* throttle limit)))
(if (= target 0.0)
;; Centre stick - let everything coast.
;; Master: app-ppm-override 0 keeps master coasting
;; Slaves: skip CAN command, they time out and coast
(app-ppm-override 0.0)
(progn
(set-rpm target)
(canset-rpm slave-1 target)
(canset-rpm slave-2 target)
(canset-rpm slave-3 target))))))
;; ── Main loop ────────────────────────────────────────────────────────────
(defun main-loop ()
(loopwhile t
(progn
;; Mode toggle on button edge
(if (button-pressed-edge)
(if (eq mode 'current)
(enter-speed-mode)
(enter-current-mode)))
;; Drive only when in speed mode. In current mode, do nothing -
;; the master's own PPM app is doing all the work.
(if (eq mode 'speed) (drive-speed-mode))
;; 20 ms tick = 50 Hz control loop
(sleep 0.02))))
;; ── Boot ─────────────────────────────────────────────────────────────────
(print "4WD Mode Switcher (coast-safe) starting...")
;; Always start in current mode - confirm PPM is attached
(app-ppm-detach 0)
(print "Default: CURRENT (script idle, all VESCs running own PPM)")
(main-loop)
4WD Mode Switcher (coast-safe) starting...
Default: CURRENT (script idle, all VESCs running own PPM)
-> SPEED. Apply throttle — wheels should now hit the knob-set ERPM limit.-> CURRENT. Confirm full throttle range is restored.After this the script runs automatically on every power-up. You can disconnect USB.
NOTE: Stream puts the script in RAM only — gone at power-off. Upload + Run on Boot puts it in flash so it persists. Both write to flash equally rarely (only when you click Upload), so neither causes per-button-press wear.
Safety requirement — Lift the board off the ground so all 4 wheels spin freely. Do not test on the ground until both modes have been verified.
Default: CURRENT.This is the test that verifies your safety architecture is working.
If either test causes any wheel to stop, brake, or behave erratically, your wiring is wrong — the throttle PWM is probably not reaching all 4 VESCs. Fix this before proceeding.
-> SPEED.-> CURRENT. Full throttle range restored.In speed mode at full throttle, all 4 wheels should be locked to the same RPM. If one wheel spins faster, freer, or stalls:
slave-1, slave-2, or slave-3 in the script (2, 3, or 4)| Parameter | Value |
|---|---|
| Motor KV | 190 KV |
| Battery voltage | 48 V nominal |
| Motor free-spin RPM at 48V | 9,120 RPM |
| Gear ratio | 5.2 : 1 |
| Pole pairs | 7 |
| Tire diameter | 13 inches |
| Max motor ERPM at 48V | 63,840 ERPM |
| Wheel RPM at 8 mph | 207 RPM |
| Motor RPM at 8 mph | 1,076 RPM |
| ERPM at 8 mph | 7,532 → rounded to 7,500 ERPM |
| Knob minimum speed | 300 ERPM ≈ 0.3 mph |
| Speed mode loop rate | 50 Hz (20 ms tick) |
| Pin | Function | Wired To |
|---|---|---|
| 1 | GND | Pot, button, capacitor common |
| 2 | +3.3V | Pot supply, pull-up resistor |
| 5 | ADC1 | Pot wiper (with 100nF to GND) |
| 6 | ADC2 | Button + 10kΩ pull-up to 3.3V |
| Condition | Behaviour |
|---|---|
| Power on | Always boots in current mode |
| Button press | Toggles between current and speed mode |
| Knob turned in speed mode | Limit updates next throttle tick (~20 ms) |
| Master power loss in current mode | All 4 wheels keep running on their own PPM |
| Master power loss in speed mode | Slaves resume their PPM apps within 0.5 s |
| Master script crash | Master’s PPM app is unaffected; in current mode, nothing changes; in speed mode, slaves resume PPM in 0.5 s |
| CAN disconnect in current mode | No effect — every VESC runs independently |
| CAN disconnect in speed mode | Slaves resume PPM in 0.5 s |
| Receiver dies | All 4 lose PPM → all coast |
| Slave VESC loses power | That wheel coasts; others continue |
mode).set-rpm, canset-rpm, app-ppm-detach, app-ppm-override) are RAM-only firmware-internal state — not flash.conf-set, conf-store, or any other flash-writing function.Asking a human who actually would know how to do this to read this and critique it is actual work.
If you are one of those people, DM me with your experience and a quote in dollars for you to write up a “rebuttal”
Thanks