feat: add ESP32 BTS7960 motor controller with web interface

- Implement MotorController class with PWM speed control (0-100%)
- Add bidirectional control (forward/reverse) via LEDC PWM at 20kHz
- Include optional current sensing and stall detection
- Create responsive web UI with speed slider and direction buttons
- Configure WiFi with static IP (10.81.2.185)
- Use built-in WebServer library for ESP32 Arduino 3.x compatibility
This commit is contained in:
devdesk
2026-02-05 15:08:47 +02:00
commit 6ccbc7faf5
15 changed files with 646 additions and 0 deletions

182
src/motor.cpp Normal file
View File

@@ -0,0 +1,182 @@
#include "motor.h"
// Set to true to enable current sensing (requires R_IS and L_IS connected)
#define CURRENT_SENSING_ENABLED false
MotorController motor;
void MotorController::begin() {
// Configure enable pins as outputs
pinMode(R_EN_PIN, OUTPUT);
pinMode(L_EN_PIN, OUTPUT);
// Enable the H-bridge
digitalWrite(R_EN_PIN, HIGH);
digitalWrite(L_EN_PIN, HIGH);
// Configure LEDC PWM channels for ESP32
ledcSetup(PWM_CHANNEL_R, PWM_FREQ, PWM_RESOLUTION);
ledcSetup(PWM_CHANNEL_L, PWM_FREQ, PWM_RESOLUTION);
// Attach PWM channels to GPIO pins
ledcAttachPin(RPWM_PIN, PWM_CHANNEL_R);
ledcAttachPin(LPWM_PIN, PWM_CHANNEL_L);
#if CURRENT_SENSING_ENABLED
// Configure current sense pins as analog inputs
// GPIO34 and GPIO35 are input-only pins, perfect for ADC
analogSetAttenuation(ADC_11db); // Full range 0-3.3V
pinMode(R_IS_PIN, INPUT);
pinMode(L_IS_PIN, INPUT);
Serial.println("Current sensing enabled");
#endif
// Start stopped
stop();
Serial.println("Motor controller initialized");
}
void MotorController::setSpeed(int speed) {
_speed = constrain(speed, 0, 100);
_stalled = false; // Reset stall state on new command
_stallStartTime = 0;
applyMotorState();
}
void MotorController::setDirection(int dir) {
_direction = constrain(dir, -1, 1);
_stalled = false; // Reset stall state on new command
_stallStartTime = 0;
applyMotorState();
}
void MotorController::stop() {
_speed = 0;
_direction = 0;
_stalled = false;
_stallStartTime = 0;
ledcWrite(PWM_CHANNEL_R, 0);
ledcWrite(PWM_CHANNEL_L, 0);
}
void MotorController::update() {
#if CURRENT_SENSING_ENABLED
// Read current sensors
_currentRight = readCurrentSense(R_IS_PIN);
_currentLeft = readCurrentSense(L_IS_PIN);
// Check for stall condition
checkStall();
#endif
}
int MotorController::getSpeed() {
return _speed;
}
int MotorController::getDirection() {
return _direction;
}
float MotorController::getCurrentRight() {
return _currentRight;
}
float MotorController::getCurrentLeft() {
return _currentLeft;
}
float MotorController::getCurrentActive() {
if (_direction > 0) {
return _currentRight;
} else if (_direction < 0) {
return _currentLeft;
}
return 0.0f;
}
bool MotorController::isStalled() {
return _stalled;
}
void MotorController::setStallCallback(void (*callback)(float current)) {
_stallCallback = callback;
}
void MotorController::applyMotorState() {
// Convert percentage to 8-bit PWM value
int pwmValue = map(_speed, 0, 100, 0, 255);
if (_direction == 0 || _speed == 0) {
// Stop - both PWM outputs off
ledcWrite(PWM_CHANNEL_R, 0);
ledcWrite(PWM_CHANNEL_L, 0);
} else if (_direction > 0) {
// Forward - RPWM active, LPWM off
ledcWrite(PWM_CHANNEL_R, pwmValue);
ledcWrite(PWM_CHANNEL_L, 0);
} else {
// Reverse - LPWM active, RPWM off
ledcWrite(PWM_CHANNEL_R, 0);
ledcWrite(PWM_CHANNEL_L, pwmValue);
}
Serial.printf("Motor: dir=%d, speed=%d%%, pwm=%d\n", _direction, _speed, pwmValue);
}
float MotorController::readCurrentSense(int pin) {
#if CURRENT_SENSING_ENABLED
// Read ADC value (12-bit, 0-4095)
int adcValue = analogRead(pin);
// Convert to voltage (0-3.3V)
float voltage = (adcValue / ADC_MAX) * ADC_VREF;
// Calculate current
// IS pin outputs: I_sense = I_load / CURRENT_SENSE_RATIO
// With sense resistor: V = I_sense * R_sense
// Therefore: I_load = V * CURRENT_SENSE_RATIO / R_sense
float current = (voltage * CURRENT_SENSE_RATIO) / SENSE_RESISTOR;
return current;
#else
return 0.0f;
#endif
}
void MotorController::checkStall() {
#if CURRENT_SENSING_ENABLED
// Only check stall when motor should be running
if (_direction == 0 || _speed == 0) {
_stalled = false;
_stallStartTime = 0;
return;
}
float activeCurrent = getCurrentActive();
// Check if current exceeds stall threshold
if (activeCurrent > STALL_CURRENT_THRESHOLD) {
if (_stallStartTime == 0) {
// Start timing potential stall
_stallStartTime = millis();
} else if ((millis() - _stallStartTime) > STALL_DETECT_TIME_MS) {
// Stall confirmed
if (!_stalled) {
_stalled = true;
Serial.printf("STALL DETECTED! Current: %.2fA\n", activeCurrent);
// Call callback if registered
if (_stallCallback != nullptr) {
_stallCallback(activeCurrent);
}
}
}
} else {
// Current normal, reset stall detection
_stallStartTime = 0;
_stalled = false;
}
#endif
}