← SHEET 02 · ASSEMBLIES EL-002
Folding-Stair Actuator Controller
| PART NO | EL-002 |
|---|---|
| MATL / SYSTEM | ESP32 · BTS7960 · 12V LINEAR ACTUATOR |
| TOOLS | Arduino IDE · Wokwi · C++ |
The drive electronics for HW-002, the folding parallelogram staircase already welded, installed, and in daily use in my split-level bedroom. That project ended with two manual latches; this one replaces them with an ESP32 + BTS7960 H-bridge driving a self-locking 12V linear actuator, with limit switches for the two end-stops and current sensing to soft-stop the ramp if the mechanism meets an obstruction instead of grinding through it.
OVERVIEW & MOTIVATION
HW-002’s build notes flagged motorized deployment as planned future work: “size a self-locking 12V linear actuator for motorized deployment against the ~1251 mm rail geometry and 900 mm rise.” This project is that sizing exercise carried through to a working control design — a five-state firmware (STOWED / DEPLOYING / DEPLOYED / STOWING / FAULT), soft-start/soft-stop PWM ramping so the linkage never gets a hard jolt of torque, limit switches that define the two valid end positions, and current-threshold obstruction detection so a foot, a dropped object, or a binding hinge stops the actuator instead of stalling the motor or bending the linkage. The self-locking actuator choice matters as much as the electronics: it holds the stair’s position with zero holding current, so a power cut mid-stroke can’t let a partially-deployed stair free-fall.
COMPONENTS & BOM
| REF | COMPONENT | SPEC | ROLE |
|---|---|---|---|
| U1 | ESP32 DevKit-C | ESP32-WROOM-32, Arduino core 3.x | runs the state machine, PWM ramp, ADC |
| U2 | BTS7960 module | 43A dual half-bridge, RPWM/LPWM/R_EN+L_EN | drives the 12V linear actuator |
| M1 | 12V linear actuator | self-locking (leadscrew/worm), stroke sized to rail travel | motorizes HW-002’s deploy/stow swing |
| U3 | ACS712 current sensor | ratiometric Hall-effect, in series with 12V B+ | obstruction/stall detection for soft-stop |
| SW1 | Limit switch — deployed | normally-open, rail end-stop | marks full-deploy travel limit |
| SW2 | Limit switch — stowed | normally-open, rail end-stop | marks full-stow travel limit |
| SW3 | Pushbutton — deploy | momentary, INPUT_PULLUP | commands DEPLOYING |
| SW4 | Pushbutton — stow | momentary, INPUT_PULLUP | commands STOWING |
| D1 | Status LED | 5 mm + 220R series resistor | state indicator (off / slow-blink / solid / fast-blink) |
| PSU1 | 12V supply | sized to actuator stall current + margin, fused | actuator power rail, routed through U3 |
WIRING
| ESP32 PIN | NET | PERIPHERAL PIN |
|---|---|---|
| GPIO25 (D25) | RPWM | BTS7960 RPWM (forward/extend PWM) |
| GPIO26 (D26) | LPWM | BTS7960 LPWM (reverse/retract PWM) |
| GPIO27 (D27) | DRV_EN | BTS7960 R_EN + L_EN (tied together) |
| GPIO32 (D32) | LIM_DEPLOYED | limit switch, deployed end-stop |
| GPIO33 (D33) | LIM_STOWED | limit switch, stowed end-stop |
| GPIO18 (D18) | BTN_DEPLOY | deploy pushbutton |
| GPIO19 (D19) | BTN_STOW | stow pushbutton |
| GPIO2 (D2) | STATUS_LED | status LED (via 220R) |
| GPIO34 (D34) | CURRENT_SENSE | ACS712 OUT (analog) |
| 3V3 | — | ACS712 VCC (kept at 3.3V so its output stays inside the ESP32 ADC range) |
| VIN (5V) | — | BTS7960 VCC (logic supply) |
| GND | — | common return — ESP32, BTS7960, ACS712, switches, 12V supply |
CONTROL LOGIC
Five states — STOWED, DEPLOYING, DEPLOYED, STOWING, FAULT — driven by two
buttons and two limit switches, with the actuator PWM ramped rather than switched, and an
obstruction watchdog running only once the ramp is fully up to cruise speed (so ramp-up
inrush current never false-triggers a stall):
void serviceRamp(unsigned long now) {
if (activeDir == Dir::NONE) return;
if (now - lastRampStepMs < RAMP_STEP_MS) return;
lastRampStepMs = now;
if (currentDuty < targetDuty) {
uint16_t next = (uint16_t)currentDuty + RAMP_UP_STEP_DUTY;
currentDuty = (next >= targetDuty) ? targetDuty : (uint8_t)next;
} else {
uint16_t drop = min<uint16_t>(currentDuty, RAMP_DOWN_STEP_DUTY);
currentDuty -= drop;
}
pwmWrite(activeDir, currentDuty);
}
Obstruction detection reads the ACS712 through a design-derived, not bench-measured,
scale factor (3.3V-powered ACS712-05B, ~122 mV/A) and only counts a hit once it has held
above threshold for OBSTRUCTION_DEBOUNCE_MS:
float readCurrentAmps() {
float volts = (analogRead(PIN_CURRENT) / 4095.0f) * 3.3f;
return fabsf((volts - 1.65f) / (122.0f / 1000.0f));
}
A confirmed obstruction ramps the drive to zero, fires a brief reverse “relief” pulse to
unbind the linkage, then retries the same direction — up to MAX_RETRIES (2) before
the controller gives up and enters FAULT, de-energizing DRV_EN entirely. FAULT only
clears on a deliberate 2-second hold of both buttons together, and resumes from whichever
limit switch actually reads active rather than assuming a position.
SIMULATION
Wokwi has no BTS7960, ACS712, or linear-actuator part, so diagram.json approximates the
rig rather than simulating it literally — the same approach already used for the L298N in
this site’s line-follower project and the INA219 in the power-monitor project:
- BTS7960 — not a Wokwi part. Its three control lines (RPWM, LPWM, R_EN+L_EN) are each broken out to a labeled LED through a 220R resistor, so the ramp and enable logic are visible as brightness/blink without an actual H-bridge behind them.
- 12V linear actuator — no linear-actuator or plain DC-motor part exists in Wokwi either; its motion is represented indirectly through the RPWM/LPWM LED brightness and by manually toggling the limit-switch pushbuttons to simulate the rail reaching an end-stop.
- ACS712 — substituted with a potentiometer labeled
I stand-in (no ACS712 in Wokwi), wired intoGPIO34exactly like the real sensor’s analog output, so dragging it exercises the samereadCurrentAmps()/ obstruction-threshold code path. - Limit switches and pushbuttons — real
wokwi-pushbuttonparts, not stand-ins; a limit switch is electrically a momentary contact to ground, identical to a pushbutton.
Steps: open wokwi.com, New Project → ESP32, paste in sketch.ino and
diagram.json, press Play. Clicking btnDeploy should ramp ledRpwm up smoothly rather
than snapping on; clicking the deployed limit-switch button ramps it back down and the
status LED goes solid. Dragging the current-sense potentiometer above threshold and holding
it there triggers the retry sequence on the serial monitor, and past MAX_RETRIES trips
FAULT (fast LED blink), clearable by holding both buttons for 2 seconds.
STATUS
HW-002 itself is built, welded, and in daily manual use — two hand latches hold it deployed
or stowed today. This controller is design and firmware-simulation stage only: the
state machine, PWM ramp, and obstruction logic are written and exercised in Wokwi against
the substitutions above, but no BTS7960, ACS712, or actuator has been bench-tested, and
nothing here is wired to the physical stair yet. OBSTRUCTION_THRESHOLD_A is a datasheet-
derived placeholder pending a real bench measurement of the actuator’s no-load and stall
current.
USE CASES & APPLICATIONS
The same pattern — limit-switched travel, soft-start/soft-stop ramping, and current-sensed obstruction cutoff — covers most small linear-actuator automation: motorized attic hatches and loft ladders, automatic gate and barrier operators, hospital-bed and recliner positioning actuators, and retractable equipment covers or ramps. Anywhere a linear actuator moves through a space people or objects can be in, current-sensing soft-stop is the cheap, effective substitute for a proper force/torque sensor.
FILES
sketch.ino— full ESP32 firmware: state machine, non-blocking PWM ramp, debounced limits/buttons, current-threshold obstruction detection with retry-then-fault logic. Header comment carries the full pinout.diagram.json— Wokwi wiring diagram, pin-identical to the firmware, with the honest part substitutions listed above.README.md— run-in-Wokwi steps, real-build 12V supply sizing notes, and why a self-locking actuator means power-off holding with zero standing current.