Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions andino_firmware/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ lib_deps =
adafruit/Adafruit BusIO
adafruit/Adafruit Unified Sensor

; Base configuration for Espressif ESP32 based boards.
[base_espressif32]
platform = espressif32
framework = arduino
monitor_speed = 57600
test_ignore = desktop/*
lib_deps =
Wire
SPI
adafruit/Adafruit BNO055
adafruit/Adafruit BusIO
adafruit/Adafruit Unified Sensor

; Base configuration for desktop platforms (for unit testing).
[base_desktop]
platform = native
Expand All @@ -53,3 +66,8 @@ board = nanoatmega328
; Environment for desktop platforms (Windows, macOS, Linux, etc).
[env:desktop]
extends = base_build, base_desktop

; Environment for ESP32 DevKit boards.
[env:esp32dev]
extends = base_build, base_espressif32
board = esp32dev
16 changes: 16 additions & 0 deletions andino_firmware/src/andino_firmware.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Arduino IDE / Arduino-core entry points.
*
* This sketch file provides the standard `setup()` and `loop()` symbols that
* the Arduino core (including ESP32) expects, and simply forwards them to the
* existing andino::App implementation.
*
* NOTE: Do not put any application logic here; keep everything in App to
* ensure the serial protocol and higher-level behavior remain unchanged.
*/

#include "app.h"

void setup() { andino::App::setup(); }

void loop() { andino::App::loop(); }
79 changes: 56 additions & 23 deletions andino_firmware/src/app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -75,38 +75,68 @@
#include "digital_out_arduino.h"
#include "encoder.h"
#include "hw.h"
#include "interrupt_in_arduino.h"
#include "motor.h"
#include "pid.h"
#include "pwm_out_arduino.h"
#include "serial_stream_arduino.h"
#include "shell.h"

#if defined(ARDUINO_ARCH_ESP32)
#include "interrupt_in_esp32.h"
#include "pwm_out_esp32_mcpwm.h"
#else
#include "interrupt_in_arduino.h"
#include "pwm_out_arduino.h"
#endif

namespace andino {

SerialStreamArduino App::serial_stream_;

Shell App::shell_;

DigitalOutArduino App::left_motor_enable_digital_out_(Hw::kLeftMotorEnableGpioPin);
#if defined(ARDUINO_ARCH_ESP32)
PwmOutEsp32Mcpwm App::left_motor_forward_pwm_out_(Hw::kLeftMotorForwardGpioPin, MCPWM_UNIT_0,
MCPWM0A);
PwmOutEsp32Mcpwm App::left_motor_backward_pwm_out_(Hw::kLeftMotorBackwardGpioPin, MCPWM_UNIT_0,
MCPWM0B);
#else
PwmOutArduino App::left_motor_forward_pwm_out_(Hw::kLeftMotorForwardGpioPin);
PwmOutArduino App::left_motor_backward_pwm_out_(Hw::kLeftMotorBackwardGpioPin);
#endif
Motor App::left_motor_(&left_motor_enable_digital_out_, &left_motor_forward_pwm_out_,
&left_motor_backward_pwm_out_);

DigitalOutArduino App::right_motor_enable_digital_out_(Hw::kRightMotorEnableGpioPin);
#if defined(ARDUINO_ARCH_ESP32)
PwmOutEsp32Mcpwm App::right_motor_forward_pwm_out_(Hw::kRightMotorForwardGpioPin, MCPWM_UNIT_1,
MCPWM1A);
PwmOutEsp32Mcpwm App::right_motor_backward_pwm_out_(Hw::kRightMotorBackwardGpioPin, MCPWM_UNIT_1,
MCPWM1B);
#else
PwmOutArduino App::right_motor_forward_pwm_out_(Hw::kRightMotorForwardGpioPin);
PwmOutArduino App::right_motor_backward_pwm_out_(Hw::kRightMotorBackwardGpioPin);
#endif
Motor App::right_motor_(&right_motor_enable_digital_out_, &right_motor_forward_pwm_out_,
&right_motor_backward_pwm_out_);

#if defined(ARDUINO_ARCH_ESP32)
InterruptInEsp32 App::left_encoder_channel_a_interrupt_in_(Hw::kLeftEncoderChannelAGpioPin);
InterruptInEsp32 App::left_encoder_channel_b_interrupt_in_(Hw::kLeftEncoderChannelBGpioPin);
#else
InterruptInArduino App::left_encoder_channel_a_interrupt_in_(Hw::kLeftEncoderChannelAGpioPin);
InterruptInArduino App::left_encoder_channel_b_interrupt_in_(Hw::kLeftEncoderChannelBGpioPin);
#endif
Encoder App::left_encoder_(&left_encoder_channel_a_interrupt_in_,
&left_encoder_channel_b_interrupt_in_);

#if defined(ARDUINO_ARCH_ESP32)
InterruptInEsp32 App::right_encoder_channel_a_interrupt_in_(Hw::kRightEncoderChannelAGpioPin);
InterruptInEsp32 App::right_encoder_channel_b_interrupt_in_(Hw::kRightEncoderChannelBGpioPin);
#else
InterruptInArduino App::right_encoder_channel_a_interrupt_in_(Hw::kRightEncoderChannelAGpioPin);
InterruptInArduino App::right_encoder_channel_b_interrupt_in_(Hw::kRightEncoderChannelBGpioPin);
#endif
Encoder App::right_encoder_(&right_encoder_channel_a_interrupt_in_,
&right_encoder_channel_b_interrupt_in_);

Expand Down Expand Up @@ -187,6 +217,7 @@ void App::adjust_motors_speed() {
int right_motor_speed = 0;
left_pid_controller_.compute(left_encoder_.read(), left_motor_speed);
right_pid_controller_.compute(right_encoder_.read(), right_motor_speed);

if (left_pid_controller_.enabled()) {
left_motor_.set_speed(left_motor_speed);
}
Expand Down Expand Up @@ -281,41 +312,43 @@ void App::cmd_set_motors_pwm_cb(int argc, char** argv) {

// Reset the auto stop timer.
last_set_motors_speed_cmd_ = millis();

left_motor_.set_speed(left_motor_pwm);
right_motor_.set_speed(right_motor_pwm);
Serial.println("OK");
}

void App::cmd_set_pid_tuning_gains_cb(int argc, char** argv) {
// TODO(jballoffet): Refactor to expect command multiple arguments.
if (argc < 2) {
if (argc < 5) {
return;
}

static constexpr int kSizePidArgs{4};
int i = 0;
char arg[20];
char* str;
int pid_args[kSizePidArgs]{0, 0, 0, 0};

// Example: "u 30:20:10:50".
strcpy(arg, argv[1]);
char* p = arg;
while ((str = strtok_r(p, ":", &p)) != NULL && i < kSizePidArgs) {
pid_args[i] = atoi(str);
i++;
int kp = 0;
int kd = 0;
int ki = 0;
int ko = 0;

// Space-separated format: "u 30 10 0 10".
kp = atoi(argv[1]);
kd = atoi(argv[2]);
ki = atoi(argv[3]);
ko = atoi(argv[4]);


// Prevent invalid configuration that would cause divide-by-zero inside the PID.
if (ko == 0) {
ko = 1;
}
left_pid_controller_.set_tunings(pid_args[0], pid_args[1], pid_args[2], pid_args[3]);
right_pid_controller_.set_tunings(pid_args[0], pid_args[1], pid_args[2], pid_args[3]);

left_pid_controller_.set_tunings(kp, kd, ki, ko);
right_pid_controller_.set_tunings(kp, kd, ki, ko);
Serial.print("PID Updated: ");
Serial.print(pid_args[0]);
Serial.print(kp);
Serial.print(" ");
Serial.print(pid_args[1]);
Serial.print(kd);
Serial.print(" ");
Serial.print(pid_args[2]);
Serial.print(ki);
Serial.print(" ");
Serial.println(pid_args[3]);
Serial.println(ko);
Serial.println("OK");
}

Expand Down
30 changes: 28 additions & 2 deletions andino_firmware/src/app.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,19 @@

#include "digital_out_arduino.h"
#include "encoder.h"
#include "interrupt_in_arduino.h"
#include "motor.h"
#include "pid.h"
#include "pwm_out_arduino.h"
#include "serial_stream_arduino.h"
#include "shell.h"

#if defined(ARDUINO_ARCH_ESP32)
#include "interrupt_in_esp32.h"
#include "pwm_out_esp32_mcpwm.h"
#else
#include "interrupt_in_arduino.h"
#include "pwm_out_arduino.h"
#endif

namespace andino {

/// @brief This class wraps the MCU main application.
Expand Down Expand Up @@ -99,24 +105,44 @@ class App {

/// Left wheel motor.
static DigitalOutArduino left_motor_enable_digital_out_;
#if defined(ARDUINO_ARCH_ESP32)
static PwmOutEsp32Mcpwm left_motor_forward_pwm_out_;
static PwmOutEsp32Mcpwm left_motor_backward_pwm_out_;
#else
static PwmOutArduino left_motor_forward_pwm_out_;
static PwmOutArduino left_motor_backward_pwm_out_;
#endif
static Motor left_motor_;

/// Right wheel motor.
static DigitalOutArduino right_motor_enable_digital_out_;
#if defined(ARDUINO_ARCH_ESP32)
static PwmOutEsp32Mcpwm right_motor_forward_pwm_out_;
static PwmOutEsp32Mcpwm right_motor_backward_pwm_out_;
#else
static PwmOutArduino right_motor_forward_pwm_out_;
static PwmOutArduino right_motor_backward_pwm_out_;
#endif
static Motor right_motor_;

/// Left wheel encoder.
#if defined(ARDUINO_ARCH_ESP32)
static InterruptInEsp32 left_encoder_channel_a_interrupt_in_;
static InterruptInEsp32 left_encoder_channel_b_interrupt_in_;
#else
static InterruptInArduino left_encoder_channel_a_interrupt_in_;
static InterruptInArduino left_encoder_channel_b_interrupt_in_;
#endif
static Encoder left_encoder_;

/// Right wheel encoder.
#if defined(ARDUINO_ARCH_ESP32)
static InterruptInEsp32 right_encoder_channel_a_interrupt_in_;
static InterruptInEsp32 right_encoder_channel_b_interrupt_in_;
#else
static InterruptInArduino right_encoder_channel_a_interrupt_in_;
static InterruptInArduino right_encoder_channel_b_interrupt_in_;
#endif
static Encoder right_encoder_;

/// PID controllers (one per wheel).
Expand Down
4 changes: 4 additions & 0 deletions andino_firmware/src/constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ struct Constants {
/// @brief Serial port baud rate.
static constexpr long kBaudrate{57600};

// @brief ESP32 MCPWM frequency (Hz). This is the frequency of the PWM signal generated by the ESP32's MCPWM peripheral, which controls the motors. A common choice for motor control is around
// 20 kHz, which is above the audible range and can help reduce noise. Adjust as needed for your specific motors and application.
static constexpr int kMcpwmFrequency{20000};

/// @brief Time window to automatically stop the robot if no command has been received [ms].
static constexpr long kAutoStopWindow{3000};

Expand Down
33 changes: 33 additions & 0 deletions andino_firmware/src/hw.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,38 @@ namespace andino {

/// @brief Hardware configuration.
struct Hw {
#if defined(ARDUINO_ARCH_ESP32)
/// @brief Left encoder channel A pin.
/// @note These pins are chosen for a typical ESP32 DevKit; adjust to match
/// your wiring.
static constexpr int kLeftEncoderChannelAGpioPin{33};
/// @brief Left encoder channel B pin.
static constexpr int kLeftEncoderChannelBGpioPin{32};

/// @brief Right encoder channel A pin.
static constexpr int kRightEncoderChannelAGpioPin{34};
/// @brief Right encoder channel B pin.
static constexpr int kRightEncoderChannelBGpioPin{35};

/// @brief Left motor driver backward pin (L298N IN3).
static constexpr int kLeftMotorBackwardGpioPin{27};
/// @brief Left motor driver forward pin (L298N IN4).
static constexpr int kLeftMotorForwardGpioPin{26};
/// @brief Left motor driver enable pin (L298N ENB).
static constexpr int kLeftMotorEnableGpioPin{25};

/// @brief Right motor driver backward pin (L298N IN1).
static constexpr int kRightMotorBackwardGpioPin{12};
/// @brief Right motor driver forward pin (L298N IN2).
static constexpr int kRightMotorForwardGpioPin{14};
/// @brief Right motor driver enable pin (L298N ENA).
static constexpr int kRightMotorEnableGpioPin{13};

/// @brief IMU sensor I2C SCL pin (default ESP32 SCL).
static constexpr int kImuI2cSclPin{22};
/// @brief IMU sensor I2C SDA pin (default ESP32 SDA).
static constexpr int kImuI2cSdaPin{21};
#else
/// @brief Left encoder channel A pin. Connected to PD2 (digital pin 2).
static constexpr int kLeftEncoderChannelAGpioPin{2};
/// @brief Left encoder channel B pin. Connected to PD3 (digital pin 3).
Expand Down Expand Up @@ -65,6 +97,7 @@ struct Hw {
static constexpr int kImuI2cSclPin{19};
/// @brief IMU sensor I2C SDA pin. Connected to PC4 (digital pin 18, analog pin A4).
static constexpr int kImuI2cSdaPin{18};
#endif
};

} // namespace andino
6 changes: 6 additions & 0 deletions andino_firmware/src/interrupt_in_arduino.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@

#include <Arduino.h>

// This implementation is AVR-specific and must not be compiled on other
// architectures (e.g. ESP32), where a different interrupt backend is used.
#if !defined(ARDUINO_ARCH_ESP32)

/// Holds the attached callbacks.
static andino::InterruptIn::InterruptCallback g_callbacks[3] = {nullptr};

Expand Down Expand Up @@ -86,3 +90,5 @@ void InterruptInArduino::attach(InterruptCallback callback) const {
}

} // namespace andino

#endif // !ARDUINO_ARCH_ESP32
48 changes: 48 additions & 0 deletions andino_firmware/src/interrupt_in_esp32.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// BSD 3-Clause License
//
// Copyright (c) 2026, Ekumen Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "interrupt_in_esp32.h"

#if defined(ARDUINO_ARCH_ESP32)

#include <Arduino.h>

namespace andino {

void InterruptInEsp32::begin() const { pinMode(gpio_pin_, INPUT_PULLUP); }

int InterruptInEsp32::read() const { return digitalRead(gpio_pin_); }

void InterruptInEsp32::attach(InterruptCallback callback) const {
attachInterrupt(digitalPinToInterrupt(gpio_pin_), callback, CHANGE);
}

} // namespace andino

#endif // ARDUINO_ARCH_ESP32
Loading
Loading