⚡ Electric Vehicle C Simulator
Trajectory tools and motor strategies for Electric Vehicle
Arc calibration
Lay out a centerline ending at a block and aim the riflescope along that line (+y, toward the block). Start the EV at (0, 0). Use each check-slot tab position for a fixed turn radius; vary run distance so the vehicle stops at different places. Record stop coordinates in cm — x = lateral (positive = left when facing +y), y = distance along the centerline forward from the start. Enter each trial below with its slot number.
# are ignored. Sheet: forward distance y, then slot (e.g. 8th), then lateral x — use a minus sign, or suffix L/R on the magnitude. Alternate: x, y, slot when the middle column is not a slot number.
Data & fitted arcs
Model
Coordinates. Origin at the start; +y along the riflescope / centerline toward the block; +x lateral (left when facing +y).
Shared heading. Let α be the angle from +y toward +x to the initial velocity direction (same for every slot). Initially \( \mathbf{\hat{t}} = (\sin\alpha,\,\cos\alpha) \) in (\(x\), \(y\)).
Circular arc through the origin. For a left (CCW) turn with radius R, the circle center is at \( (R\cos\alpha,\,-R\sin\alpha) \). Every stop satisfies:
\[ x^2 + y^2 = 2R\,(x\cos\alpha - y\sin\alpha) \]
For a right (CW) turn, use \(x^2 + y^2 = 2R\,(y\sin\alpha - x\cos\alpha)\).
Fitting. With α fixed, each slot’s radius is chosen by linear least squares so the above holds for all points in that slot. α is scanned on [−89°, 89°] and refined to minimize the sum of squared residuals of \(x^2+y^2 - 2R_k(\cdots)\).
Trajectory planner
Uses calibration α and per-slot R. For each slot, the fitted arc is rotated in-plane about the start until it passes through the target (0, D) on the centerline (+y). Centers use the branch with cx > 0 so the path bows toward +x. The can reference is fixed at (−100, D/2) cm. The table lists tangent ψ from +y, ψ − α, block lateral x = D tan(ψ − α) at y = D, and lateral clearance from the arc at y = D/2 to that can point.
Can marker on the plot: (−100, D/2) cm (fixed).
Slot radii R (cm)
| Slot | R |
|---|
Sighting & headings
| Slot | Tangent ψ from +y (deg) | ψ − α (deg) | Block x = D tan(ψ − α) (cm) |
Mid clearance to can (cm) |
|---|
Arcs through target
Trajectory geometry
An arc of radius R through (0,0) and (0,D) has center on y = D/2 with \(|cx|=\sqrt{R^2-(D/2)^2}\). We take cx positive so the bulge is toward +x (outer-can side).
Let \( \mathbf{c}_{\mathrm{cal}} \) be the calibrated center for that slot. Rotation \(\phi\) about the origin maps \(\mathbf{c}_{\mathrm{cal}}\) to \(\mathbf{c}_{\mathrm{tgt}}\) (same length \(R\)). The calibrated tangent \((\sin\alpha,\cos\alpha)\) rotates to \(\mathbf{t}'\); \(\psi=\mathrm{atan2}(t'_x,t'_y)\) from +\(y\). The sighting table uses \(\psi-\alpha\) and block lateral \(x = D\tan(\psi-\alpha)\) at \(y=D\). Mid-clearance uses the arc’s \(x\) at \(y=D/2\) on the path from \((0,0)\) to \((0,D)\); the can reference is fixed at \((-100,\,D/2)\), so clearance is \(x_{\mathrm{curve}}(D/2) - (-100) = x_{\mathrm{curve}}(D/2)+100\).
Motor Control Strategy
Use your measured arc length (encoder distance along the path) from calibration runs as the target distance for encoder-based control.
Over ~2 s, ramp power from standstill to 40%.
Prevents wheel slip on launch; keeps encoder readings accurate.
Goal: Reach 1 m left with 4 s remaining.
In a loop: measure current speed and distance; adjust power to hit that point at exactly the right time.
Target speed ≈ 25 cm/s. Compensates for battery sag and friction.
From Phase 2 (without stopping), reduce power and creep slowly until robot is 2 cm from target distance, then cut power.
Account for drift/inertia; measure braking distance and offset stop trigger if needed.
START_RUN:
startTime = now()
rampStartTime = now()
currentPower = 0
PHASE_1_RAMP_UP: // ~2 s, 0→40%
WHILE rampElapsed < 2.0:
currentPower = 40 × (rampElapsed / 2.0)
drive(currentPower)
currentPower = 40
lastSpeedTime = now()
lastCounter = encoderCount
PHASE_2_TARGETING_LOOP: // Goal: 1 m left with 4 s remaining
WHILE distanceRemaining > 100:
distanceTraveled = encoderToCm(encoderCount)
distanceRemaining = arcLength - distanceTraveled
elapsed = now() - startTime
timeLeft = targetTime - elapsed
neededSpeed = distanceRemaining / timeLeft
neededSpeed = clamp(neededSpeed, 5, 150)
every 100 ms:
currentSpeed = (Δencoder / pulsesPerRev) × wheelCircumfrence / Δt
speedError = neededSpeed - currentSpeed
IF speedError > 5: currentPower += 2
ELSE IF speedError < -5: currentPower -= 2
currentPower = clamp(currentPower, 25, 60)
drive(currentPower)
PHASE_3_PRECISION_STOP: // Creep until 2 cm from target
drive(8) // slow creep power
WHILE distanceRemaining > 2:
drive(8)
drive(0) // STOP
DONE: display results
Goal: Reach 6 cm before the end with 0.3 s remaining.
Single phase: no ramp up or ramp down. A loop measures speed and adjusts power proportionally to the speed error (power change = Kp × error). Stops at 6 cm remaining.
Proportional control for precise speed tracking. Kp = 0.5, max change ±6% per 100 ms.
START_RUN:
startTime = now()
lastSpeedTime = now()
lastCounter = encoderCount
currentPower = 40 // initial guess
TARGETING_LOOP: // Goal: 6 cm left with 0.3 s remaining
WHILE distanceRemaining > 6:
distanceTraveled = encoderToCm(encoderCount)
distanceRemaining = arcLength - distanceTraveled
elapsed = now() - startTime
timeLeft = targetTime - elapsed
// Target the (6 cm, 0.3 s) point
distToTarget = distanceRemaining - 6
timeToTarget = timeLeft - 0.3
timeToTarget = max(timeToTarget, 0.05) // avoid div by zero
neededSpeed = distToTarget / timeToTarget
neededSpeed = clamp(neededSpeed, 5, 150)
every 100 ms:
currentSpeed = (Δencoder / pulsesPerRev) × wheelCircumfrence / Δt
speedError = neededSpeed - currentSpeed
powerChange = Kp × speedError // Kp = 0.5, proportional
powerChange = clamp(powerChange, -6, 6) // cap per cycle
currentPower += powerChange
currentPower = clamp(currentPower, 25, 75)
drive(currentPower)
drive(0) // STOP (reached 6 cm)
DONE: display results
Set motor power (0–100%) and run duration (seconds). Press start — motor runs at that power for that time, then stops.
No encoders, no ramps, no feedback. Good for quick tests and calibration.
SETUP_PHASE:
runPower = user_selected_power // 0–100 %
runTimeSec = user_selected_time // seconds
(via dial + Set button)
START_RUN:
runStartTime = now()
drive(runPower)
RUN_LOOP:
WHILE elapsed < runTimeSec:
elapsed = (now() - runStartTime) / 1000
drive(runPower) // hold constant power
drive(0) // STOP
DONE:
display elapsed time
(no encoder, no feedback, no ramps)