explain
async def task_control_motor():
global vars
global cfg
global button_press_state_previous
global front_motor
global rear_motor
global throttle_1
global throttle_2
global brake_sensor
while True:
##########################################################################################
# Throttle
# read 1st and 2nd throttle, and use the max value of both
throttle_1_value = throttle_1.value
throttle_2_value = throttle_2.value
throttle_value = max(throttle_1_value, throttle_2_value)
#print(throttle_1.adc_value, throttle_1.value)
#print(throttle_2.adc_value, throttle_2.value)
#print(rear_motor.data.battery_voltage_x10)
#print()
# check to see if throttle is over the suposed max error value,
# if this happens, that probably means there is an issue with ADC and this can be dangerous,
# as this did happen a few times during development and motor keeps running at max target / current / speed!!
# the raise Exception() will reset the system
if throttle_1_value > cfg.throttle_1_adc_over_max_error or \
throttle_2_value > cfg.throttle_2_adc_over_max_error:
# send 3x times the motor current 0, to make sure VESC receives it
# VESC set_motor_current_amps command will release the motor
front_motor.set_motor_current_amps(0)
rear_motor.set_motor_current_amps(0)
front_motor.set_motor_current_amps(0)
rear_motor.set_motor_current_amps(0)
front_motor.set_motor_current_amps(0)
rear_motor.set_motor_current_amps(0)
if throttle_1_value > cfg.throttle_1_adc_over_max_error:
message = f'throttle 1 value: {throttle_1_value} -- is over max, this can be dangerous!'
else:
message = f'throttle 2 value: {throttle_2_value} -- is over max, this can be dangerous!'
raise Exception(message)
pass
# Apply cruise control
throttle_value = cruise_control(
vars,
rear_motor.data.wheel_speed,
throttle_value)
# Calculate target speed
front_motor.data.motor_target_speed = simpleio.map_range(
throttle_value,
0.0,
1000.0,
0.0,
front_motor.data.cfg.motor_erpm_max_speed_limit)
rear_motor.data.motor_target_speed = simpleio.map_range(
throttle_value,
0.0,
1000.0,
0.0,
rear_motor.data.cfg.motor_erpm_max_speed_limit)
# Limit mins and max values
if front_motor.data.motor_target_speed < 500.0:
front_motor.data.motor_target_speed = 0.0
if rear_motor.data.motor_target_speed < 500.0:
rear_motor.data.motor_target_speed = 0.0
if front_motor.data.motor_target_speed > \
front_motor.data.cfg.motor_erpm_max_speed_limit:
front_motor.data.motor_target_speed = front_motor.data.cfg.motor_erpm_max_speed_limit
if rear_motor.data.motor_target_speed > \
rear_motor.data.cfg.motor_erpm_max_speed_limit:
rear_motor.data.motor_target_speed = rear_motor.data.cfg.motor_erpm_max_speed_limit
# Set motor min and max currents
front_motor.set_motor_current_limits(
front_motor.data.motor_target_current_limit_min,
front_motor.data.motor_target_current_limit_max)
rear_motor.set_motor_current_limits(
rear_motor.data.motor_target_current_limit_min,
rear_motor.data.motor_target_current_limit_max)
# Set battery min and max currents
front_motor.set_battery_current_limits(
front_motor.data.battery_target_current_limit_min,
front_motor.data.battery_target_current_limit_max)
rear_motor.set_battery_current_limits(
rear_motor.data.battery_target_current_limit_min,
rear_motor.data.battery_target_current_limit_max)
# Check if brakes are active
vars.brakes_are_active = True if brake_sensor.value else False
# If motor state is disabled, set motor current to 0 to release the motor
if vars.motors_enable_state == False:
front_motor.set_motor_current_amps(0)
rear_motor.set_motor_current_amps(0)
else:
# If brakes are active, set motor speed to 0 to have the highest brake / regen
if vars.brakes_are_active:
front_motor.set_motor_speed_rpm(0)
rear_motor.set_motor_speed_rpm(0)
# If brakes are not active, set the motor speed
else:
front_motor.set_motor_speed_rpm(front_motor.data.motor_target_speed)
rear_motor.set_motor_speed_rpm(rear_motor.data.motor_target_speed)
# We just updated the motor target, so let's feed the watchdog to avoid a system reset
watchdog.feed() # avoid system reset because watchdog timeout
gc.collect() # https://learn.adafruit.com/Memory-saving-tips-for-CircuitPython
await asyncio.sleep(0.02)
This is the “brain” of an electric skateboard / e-bike / similar vehicle.
It runs forever (while True) at 50 Hz (20 ms period) and does only one job: decide how fast each motor should spin and how much current they may draw, while keeping the rider safe.
It runs forever (while True) at 50 Hz (20 ms period) and does only one job: decide how fast each motor should spin and how much current they may draw, while keeping the rider safe.
- Read the rider’s request
- Two independent throttle potentiometers are sampled.
- The bigger of the two values is kept (“dual throttle” safety).
- If either ADC value is higher than a hard-wired maximum that the hardware can never reach under normal conditions, the code assumes the ADC or wiring is fried.
– It instantly sends six “0 A” commands to the two VESC motor controllers (three times each to be sure the packet gets through).
– Then it raises an exception, which reboots the whole microcontroller (watchdog will fire if the exception is somehow missed).
Rebooting is the safest fallback because it guarantees the motors stop.
- Cruise-control overlay
The raw throttle value is passed to a
That helper can “dial back” the throttle automatically if the wheel-speed is already above the target cruise speed (PID-style regulation).
The returned value is still in the 0-1000 range.
cruise_control()
helper.That helper can “dial back” the throttle automatically if the wheel-speed is already above the target cruise speed (PID-style regulation).
The returned value is still in the 0-1000 range.
- Convert throttle → electrical RPM
simpleio.map_range() rescales:
0-1000 → 0 - motor_erpm_max_speed_limit
After that a dead-band is applied: anything below 500 eRPM is clamped to 0 so the motor is completely quiet at very small throttle openings.
Likewise, the value is hard-clamped to the configured maximum so a software bug can never request an overspeed.
Likewise, the value is hard-clamped to the configured maximum so a software bug can never request an overspeed.
- Current limits (battery & motor side)
The VESC controllers let you set four separate limits every cycle:
- Motor phase-current min / max (controls torque)
- Battery current min / max (controls power drawn from the pack)
The code simply pushes the values that were calculated elsewhere (probably from battery SOC, temperature, rider profile, etc.) to both controllers.
- Brake interlock & enable interlock
- A digital brake sensor is read.
- If the master enable flag (
vars.motors_enable_state
) is False the motors are immediately told to draw 0 A (coast). - If brakes are active and the system is enabled, the speed target is set to 0 rpm.
On a VESC this gives maximum regenerative braking (the controller will apply the configured negative current until the wheel stops). - If brakes are not active and the system is enabled, the speed targets calculated in step 3 are sent to the controllers.
- House-keeping
watchdog.feed()
– the hardware watchdog must be patted every cycle or the board reboots.gc.collect()
– CircuitPython runs on a small MCU, so an explicit garbage-collection keeps the heap from fragmenting.await asyncio.sleep(0.02)
– yields control back to the scheduler so other coroutines (BLE, display, logging, etc.) get CPU time.
In one sentence
Every 20 ms the loop translates the rider’s throttle (safely), applies cruise control, respects brake and enable interlocks, sets speed and current limits on dual VESCs, and then feeds the watchdog so the board never locks up.
front_motor.data.motor_target_speed = simpleio.map_range(
throttle_value,
0.0,
1000.0,
0.0,
front_motor.data.cfg.motor_erpm_max_speed_limit)
That line is nothing more than a linear scale / “percentage” converter.
What it literally does
simpleio.map_range(x, in_min, in_max, out_min, out_max)
returns:out_min + (x - in_min) * (out_max - out_min) / (in_max - in_min)
so with the constants you gave:
Copy
front_motor.data.motor_target_speed
= 0 + (throttle_value - 0) * (erpm_max - 0) / (1000 - 0)
= throttle_value * erpm_max / 1000
Therefore:
- throttle_value = 0 → 0 eRPM
- throttle_value = 250 → 25 % of erpm_max
- throttle_value = 1000 → 100 % of erpm_max
Any fractional value is kept (float), so you get smooth speed control rather than coarse integer steps.
After this single line the next few lines clamp the result to a dead-band (0 if < 500) and to the configured maximum, but the
map_range
itself is just “convert 0-1000 throttle counts to 0-max eRPM”.