Perception-Enabled Navigation and General-Utility INtelligence
Hybrid differential legged-wheeled biped rover for embodied AI, computer vision, and autonomous navigation.
- Controller manufactured and partially assembled.
- Soldering and hardware validation in progress.
- Mechanical components currently being printed.
Table of Contents
I built penguin cause I wanted to create an intelligent robot from the ground up.
|
|
|
|
|
|
Put simply, Penguin is a platform for experimentation with intelligent robotics.
The project began as a simpler, maze-solving rover for a course project (MREN 178 - DSA). My team couldn't deliver the rover in time so we scaled down. I had already started with hardware, which pushed me to adopt the project into my own, with the intent to expand it past maze-solving.
After that term (Winter '26), I designed a controller PCB and the necessary compoments for the chassis.
Now its September, and I'm tuning PID.
The controller is powered by a 7.4V LiPo Battery and driven by an ESP32-S3 (N16 1U).
Below is a list of features from power circuits to various peripherals.
- Power-entry circuitry with OC/OV protetction, RPP, PI-filter, and dual-mosfet BMS.
- Regulation circuit featuring buck converter, 5V ORing, and LDO for logic power (3V3)
- ESP32-S3 breakout circuitry with USB differential routing, S3 boot mode selection and reset toggle.
- Controlled power delivery via CMOS circuits for main motor power.
- N20 motors with encoders for wheels driven by a DRV8833 chip.
- DS3218 20kg Servo motors for hip joints, driven by PWM signals from S3.
- OV2640 camera with FPC connector and three-level voltage regulation.
- I2C bus with three ToF sensors, OLED screen, and GPIO expander.
- Audible and visual indicators.
- cool silkscreen penguin.
- Three-layer internal stackup for housing motors, controller board, and routing cables.
- Integrated vents and ports for sensors and devices, air flow, and weight reduction.
- Hybrid locomotion system with wheels and legs.
- Multi-bar leg assembly - pivot arm, mounting arm (hip), leg.
This section provides low-level explanations of the significant circuits of each "module" of the controller.
I encourage you to clone the repo or you use KiCanvas to look at the schematic and board files.
All electronics were designed with KiCad.
Power Entry
The 7.4V battery power enters through a Molex Micro-Fit (3mm Pitch) header, where it is then met with a 12V 7A fuse for over-current protection. A PTC resettable fuse was selected for durability.
The positive battery line is then protected from voltage spikes with a Transient Voltage Supression (TVS) diode placed in paralell for clamping.
- The diode's standoff @
9V - Breakdown @
10V - Clamps @
15.4V
The positive line then goes through a Reverse Polarity Protection (RPP) circuit, using a P-Channel MOSFET for lossless efficiency (lower V drop than standard diodes).
A Zener diode and resistor is placed between the MOSFET's gate and source for gate over-voltage protection.
A bulk electrolytic (100μF) capacitor, ferrite bead, and high-frequency (0.1μF) capacitor are then placed to create a third-order low pass filter (π-filter).
- This is used to smooth voltage spikes and reduce EMI.
The ferrite bead was primariy selected for its saturation current and DCR.
The approximate current draw with safety margin
Minimize heat dissipation.
Battery-Management-System (BMS)
The BMS system uses the negative terminal of the battery and features a HY2120 protection IC with a dual-mosfet to form the system's GND.
The IC uses the balance tap of the battery (VC ~ 3.7V nominal) to monitor the level of Cell 1, while using the main positive terminal (VDD ~ 7.4V nominal) to monitor Cell 2.
This allows the IC to indepdently track each cell's voltage and trigger the dual-MOSFET to cut off power if either cell suffers from overcharge or overdischarge.
Voltage Regulation (I)
The controller uses a cascade regulation system, starting with the TPS5405DR buck converter.
The input voltage is VBATT (7.4V), after the protection and filtering circuitry, and the output is 5V.
The
Switching frequency is governed by the relationship.
I chose a value of 100kΩ for a 271 kHz
For the soft-start capacitor 10nF for a soft-start time of 5ms
The values below were taken from the TPS5405DR Datasheet
The inductor values were chosen using the equation below.
Using our calculated
$$ L= \frac{(5,\mathrm{V})(8.4,\mathrm{V}-5,\mathrm{V})} {(8.4,\mathrm{V})(525,\mathrm{kHz})(0.3,\mathrm{A})}
\approx 10,\mathrm{\mu H} $$
The Schottky diode is rated for the 2A output current and 8.4V
5V ORing
The 5V rails from both USB and the buck converter are combined using an OR-ing circuit (Dual Ideal Diodes) to prevent backfeeding before feeding the second stage of regulation.
In this configuration, the outputs of both LM66100 ideal diode controllers are tied together to form the shared 5V rail. The CE pins of each device are also tied to the output rail, ensuring both controllers are only enabled once the rail is present
Voltage Regulation (II)
The second stage of voltage regulation involes a Low-Dropout Regulator (LDO), the AMS1117
It is fed the combined 5V rail from the OR-ing circuit to produce the controller's logic-level voltage: 3V3
Controlled Motor Power Delivery
The power for the N20 and Servo motors is controlled using a complementary-MOSFET (CMOS) setup, driven by a digital HIGH from the S3.
-
When the control signal is low or floating, the N-channel MOSFET remains off and a pull-up resistor holds the gate of the P-channel MOSFET at the supply voltage, keeping it disabled and disconnecting the load rail.
-
When the control signal is driven high, the N-channel MOSFET turns on and pulls the P-channel gate low, creating sufficient gate-source voltage to enable conduction and connect the supply to the load.
This controlled power line, VMOT = 7.4V powers the supply voltage of the DRV8833 chip, and is converted via local buck converter to produce VSERVO = 5V
Voltage Sensing
A resistor divider circuit is used to produce a signal to be read by the S3's internal ADC.
$R_1 = 100,\mathrm{k\Omega}$ $V_{batt} = 7.4,\mathrm{V}$ $V_{out} = 2.8,\mathrm{V}$
The controller is driven by an ESP32-S3-WROOM-1U-N16 microcontroller.
The U and N16 model was selected for layout flexibility and memory capacity (16MB).
A MCP23017 GPIO expander was used to accomodate space for all digital inputs/outputs (via I2C)
Digital IO
Mode Select and Reset Toggle
The S3 can be put into BOOT mode and RESET with onboard buttons.
BOOT mode determines whether the chip runs existing code from flash memory or waits to download new firmware.
- The ESP32 samples the BOOT pin (
GPIO0) during reset to determine the boot mode. - It is active-low: when pulled to GND during reset, the chip enters UART bootloader mode for flashing firmware.
- The circuit uses a momentary push-button to GND and a
10kΩpull-up resistor to ensure a defined high state during normal operation.
| BOOT0 Level | Boot Mode |
|---|---|
| HIGH | Programming Mode |
| LOW | Memory Mode |
RESET controls the Enable (EN) pin which is the ON/OFF (reset) switch for the ESP32.
- It is active-low: when pulled to GND it turns off the chip, and when released, it restarts.
- The circuit consists of a momentary push-button, a pull-up resistor to ensure the chip is enabled by default, and a
1µFcapacitor to filter out button noise.
The N20 wheel hub motors are driven by a DRV8833 chip, and their encoder signals are routed to digital pins on the S3.
They are connected via 6-Pin Molex Nano-Fit (2.50mm Pitch) headers, and feature decoupling across the motor terminals and on the encoder power supply.
The Servo hip motors are driven by PWM signals from the S3, and are connected by a single 4-Pin Molex Nano-Fit header.
The connector has decoupling on the power supply, and series resistors on the signal lines to improve signal integrity and reduce noise.
The controller interfaces with an OV2640 camera via FPC-connector.
The camera is configured in 8-bit mode using adjacent digital pins for frames.
Power supply is divided into three levels:
DVDD: 1.2VAVDD: 2.8VDOVDD: 3.3V
These conversions are acheived through the use of fixed-output-voltage LDOs (LP5907MFX) with local decoupling.
Series low-resistance resistors were added on the camera's clock signals (PCLK, XCLK) in series for signal integrity and EMI reduction.
- I used only
S3pins, not expander pins, for the data pinsD2-D9andHREF,VSYNCpins.
The controller interfaces with three VL53L0X Time-of-Flight (ToF) sensors for ranging (obstacle detection, SLAM - potentially).
This interface is achieved through I2C.
The sensors are connected through a single 7-Pin Molex Nano-Fit header, connecting the sensor power, I2C clock and data signals, and the shutdown XSHUT pins of each sensor.
Decoupling capacitors and shutdown pull-ups are placed locally.
Additional I2C Devices
- IMU (LSM6DSL)
- Mini OLED Screen
- OV2640 Cam
Indicator Components
- RGB LED
- Passive LEDs for simple indication
- Buzzer for audible indication
- Misoriented cam header -- fixed with an extenison board + B-Type FPC cable
- DRV8833 VM and VINT pins missing GND connection
- Buzzer and RGB LED pins need clock signal but wired to MCP pins.
This section provides insight on the mechanical components of Penguin: the chassis and leg assembly.
In no way am I a CAD-expert, so this was definitely a learning experience with lots of iteration.
Projected geomtery was clutch.
I used Fusion360 to design everything.
The chassis consists of several components attatched with pegs and magnets.
Base Plate
The base plate forms the start of the internal stack-up.
It contains Servo pads for the hip motors to rest on, a port for an ON/OFF switch, and a battery slot with vents.
It acts as the "floor" of the Servo bay and the bottom of the chassis.
It connects to the rest of the chassis through pegs to insert into the inside face of each wall, as well as the nose.
Side Walls
![]() | ![]() |
The side-walls enclose the internal stack-up, and provide mounting points for several chassis components.
Mounting tabs are on the inside of each wall for board mounting.
They have cutouts for the Servo shafts, arm joints, and house components like the external antenna. The top face has short holes for magnets that align with holes on the lid.
Nose
The nose contains ports for the ToF sensors, and an OLED screen.
There is also an open face cable port connecting to the controller bay for routing.
It mounts to each walls with pegs and magnets, as well as to the base plate.
Stern
The stern features a USB-port and snaps to the walls via pegs.
Lid
The lid contains a magnet cutouts for mounting the camera mount, and is magnetically mounted on the top surface of the walls and nose.
Camera Mount
The mount features a porthole for the OV2640 camera lens, and corresponding magnet cutouts for mating with the roof.
The leg assembly consists of the Servo mounting arm, pivot arm, and the wheel hub assembly.
Mounting Arm
This is the contact point between the leg assembly and the hip motor, it's responsible for driving the assembly.
The Servo uses the disc-horn, and fits into a cutout on the mounting arm, and is secured in with the stock machine screw into the shaft.
The end of the arm mounts onto the wheel hub assembly using bearings on each hole, an M3 shoulder bolt, and a nut.
Pivot Arm
The pivot arm maintains proper orientation for the assembly as the suspension moves up and down.
It uses the same mounting as the mounting arm (bearing, shoulder bolt, nut), and is mounted slightly higher at a 40° angle on the wheel hub assembly.
Wheel Hub Assembly
This component houses the N20 motors through a cutout and U-Channel cover to secure them.
The top of the assembly contains the two mounting points for the pivot and mounting arms.
Cables are routed up the back side of it.
Firmware was written in C++ using the Arduino framework via PlatformIO.
All implementations can be found in the firmware/src directory.
The firmware design is divided into three distinct layers:
- HAL Drivers
- Abstraction of interfaces with hardware components.
- Core Engine
- Controllers and state definitions
- Networking
- Telemetry for GUI
Hardware drivers are used to abstract hardware interfaces for motors and sensors.
Each driver exports a class that follows a consistent structure:
class Driver {
public:
Driver(uint8_t pin);
bool begin();
Data read(); // return type unique to hardware
void run();
};Sensory drivers such as ToFs, IMU, and camera export reads.
Actuator drivers export runs, but the DC motor driver exports a read for the encoders.
In terms of integration, drivers are instantiated at the application level (main.cpp), and are invoked by the scheduler for telemetry, and by the locomotion engine for motor control.
The engine layer encapsulates all state machine, control, and behavioural logic into several classes.
- State & System Definitions
- Data types, schema for global data instance
- Controllers and Locomotion
- PID utility class, motor control
- Behaviour
- Motion primitives and routines
This section defines shapes for all system objects, including IMU, ToF, and motor data.
IMU Data Object
struct IMUData
{
float accX; // g
float accY;
float accZ;
float gyX; // dps
float gyY;
float gyZ;
float temp; // C
Rotation angles;
};
This and other data types are placed in a PenguinState struct which acts as the global state for telemetry lookup and velocity controllers.
Additionally, it implements a PenguinCommands field, which is observed by locomotion controllers and updated by mmotion primitives.
- Commands specify desired pitch, RPM, and hip angles.
The locomotion layer manages the low-level movement of the robot.
It is blind to routines; it doesn't know when to move but it knows how to adjust its motion given a command (orchestrated autonomously by the Behaviour layer).
A utility PID class is used as the basis for locomotion controller.
It calculates an error value as the difference between a target setpoint and a mesaured proces variable, then applies a correction based on proportional, integral, and derivative terms, to smoothly drive the controller toward its desired state.
The layer defines two important controllers:
-
Balance Controller
- Produces an optimal RPM based on a target pitch.
- Uses the IMU pitch rate as derivative term in the PID Equation.
-
Velocity Controller
- Produces an optimal PWM based on a target RPM.
- Target RPM is split into differential-drive inputs to be fed into two distinct controllers (L, R).
$$v_{\text{lr}} = v_{\text{fwd}} - v_{\text{turn}} + v_{\text{corr}}$$
The main Locomotion engine then wraps these controllers within an update method to observe the RobotCommands struct and adjusts accordingly.
- Hip motors are also driven here for stance adjustment.
The behaviour layer defines the buidling blocks of Penguin's movement:
- Motion Primitives
- Movement actions.
- Routines
- Sequences of motion primitives.
Primitives directly update RobotCommands, and form key positions and stances.
Routines are simply sequences of motion primitives, and are tracked using a step-counter and a non-blocking timer.
- Transitions are smoothed using Linear Interpolation
The routine class exports an update method for switching between different routines.
This method is used in the Behaviour implementation.
- Currently it autonomously runs a default cycling routine with obstacle avoidance using the ToF sensors and reverse primitives.
This layer leverages an asynchronous HTTP server and Websockets for streaming data to the GUI.
The ESPAsyncWebServer.h is wrapped in a class HTTPServer which implements begin and update methods which initializes the Websocket connection, and sends the telemetry packet.
The firmware data packet is serialized into a JsonDocument before streaming.
The telemetry packet contains:
- Motor Data
- RPM, position, running boolean
- IMU Data
- Pitch, roll, yaw, three-axis acceleration
- ToF Data (For each sensor)
- Proximity in mm
- Obstacle Detection boolean
- Battery Data
- SOC
- Connected boolean
The payload updates are orchestrated by the Scheduler
| Component | Update Frequency | Update Period (Interval) |
|---|---|---|
| IMU | 100 Hz | 10 ms |
| Motors | 50 Hz | 20 ms |
| TOF Sensors | 25 Hz | 40 ms |
| Battery Data | 2 Hz | 500 ms |
The live camera feed is streamed directly over HTTP.
Camera Stream
The camera driver captures frames as JPEG images and exposes them through the controller's HTTP server at:
http://<controller-ip>/stream
The endpoint uses a multipart/x-mixed-replace response with a frame boundary, allowing a browser to display the feed directly as an MJPEG stream. The dashboard uses this endpoint as the source of an <img> element, so no additional decoding or polling loop is required on the client.
The current capture configuration is:
VGAframe size (640 x 480)- JPEG pixel format
- JPEG quality setting of
10 - Two frame buffers using
CAMERA_GRAB_LATEST - Approximately
30 msdelay between released frames to limit stream pressure
Each request owns its stream state and releases the camera frame buffer when the frame has been sent or the client disconnects. The camera must be initialized before the HTTP server is used; the root endpoint at http://<controller-ip>/ returns penguin --online as a simple connectivity check.
The dashboard (GUI) is developed in Next.js and is ran as a local web-app.
It displays:
- A frame view of the live camera feed.
- Three.js & IMU driven board visualization
- Proximity data visuals
- Spedometer view for motor data
The app simply listens to the streamed Websocket, extracts the payload, and populates the according data objects.
Jeevan Sanchez, 2026

































