This project is a custom-built remote-controlled (RC) car, designed from the ground up with custom electronics, PCB design, and embedded firmware for both the car and its remote.
- Project Video (YouTube): https://youtu.be/qdBbDeDglmY
- Bidirectional Motor Driver (H-Bridge): https://en.wikipedia.org/wiki/H-bridge
- Steering Servo PWM Control: https://en.wikipedia.org/wiki/Servo_control
- nRF24L01 Wireless Transceiver Module: https://www.instructables.com/NRF24L01-Tutorial-Arduino-Wireless-Communication/
- VSCode with PlatformIO extension: https://docs.platformio.org/en/latest/what-is-platformio.html
- AVRDUDE (Flash Uploader): https://github.com/avrdudes/avrdude
- KiCad (PCB Design): https://www.kicad.org
- Tinkercad (Controller and parts Design): https://www.tinkercad.com/
This board is the car's real-time control core. It receives wireless throttle/steering commands from the handheld transmitter, converts steering targets into stable hardware PWM for the MG90S servo, and drives the rear motor through a protected H-bridge path with direction dead-time and command deadbands for smoother behavior. The result is a compact control system designed for responsive handling, predictable failsafe behavior, and reliable power delivery under changing load.
The car uses an nRF24L01+ module with the RF24 library over SPI to receive drive and steering commands from the handheld controller. In firmware, the radio object is created as RF24 radio(CE_PIN, CSN_PIN) with CE on PB0 and CSN on PB1.
The RF stack is configured in configureRFRadio() with:
radio.begin()hardware check (halts if radio is not detected)radio.setPALevel(RF24_PA_LOW)to reduce TX power/currentradio.enableDynamicPayloads()radio.setAutoAck(false)- Pipe/address setup using
"jag-1"and"jag-2" radio.startListening()so this node stays in RX mode
An important part of the RF path is Timer0 timing configuration. Because this firmware includes #include <RF24.h>, the RF24 stack depends on millis()/micros() timing for operations such as CE pulse timing, transmit timing windows, retry timing, and transaction spacing.
The RF24 library itself does not directly configure Timer0. Instead, those timing primitives are provided by the Arduino core (millis()/micros()), and the project sets Timer0 for a 50 us timing base:
TCCR0A = (1 << COM0B1) | (1 << WGM01) | (1 << WGM00);
TCCR0B = (1 << WGM02) | (1 << CS01); // prescaler = 8
OCR0A = 49;At 8 MHz CPU clock with prescaler 8:
- Timer clock = 8 MHz / 8 = 1 MHz
- Timer tick = 1 us
- Period = (49 + 1) x 1 us = 50 us
So the Timer0 event period is 50 us (20 kHz). Timer0 is intentionally kept running at this rate so the millis()/micros() time base used by RF24 remains available and stable.
The receiver expects a MotorControlPayload packet with two fields:
int16_t ocrMotorfor signed throttle/drive commanduint16_t ocrSteeringfor servo pulse width command in microseconds
This payload is 4 bytes total (2 + 2), and after reception the car firmware applies it directly:
OCR1B = payload.ocrSteeringfor steering positionpayload.ocrMotordrives H-bridge direction/speed logic with a deadband of -15 to +15
Reliability/safety behavior tied to RF reception:
- On first valid packet, watchdog is enabled (
WDTO_2S) - Each additional valid packet resets the watchdog
- If RF packets stop arriving, watchdog timeout forces a reset/failsafe
Steering is driven by an MG90S servo using Timer1 hardware PWM on OC1B (PD4). The car firmware configures Timer1 in Fast PWM mode 14 with ICR1 as TOP, prescaler = 8, and ICR1 = 20000. With an 8 MHz clock, that gives a 1 us timer tick and about a 50 Hz control period (20 ms), which matches standard hobby servo timing.
The commanded steering pulse is written directly to OCR1B. Steering values are transmitted as absolute pulse widths in microseconds. Instead of assuming a fixed joystick center, the controller now supports calibration by entering measured joystick potentiometer voltages for minimum, maximum, and center. Those measured values are used to map ADC input to the steering pulse range and produce a more accurate neutral point (typically near 1900 us). On the car side, OCR1B is updated from the payload each loop:
- Neutral: calibrated from measured joystick center voltage (typically ~1900 us)
- Left/Right command range: ~1520 us to ~2280 us
- PWM period: ~20 ms (50 Hz)
- Steering deadzone: +/-90 us around calibrated center snaps to neutral to reduce twitch
This keeps steering response deterministic because the servo waveform is generated in hardware (Timer1), not by software toggling.
The drive motor uses a discrete H-bridge driven by a hybrid approach: latch lines select direction, while PWM is generated in software via Timer2 interrupts.
H-bridge signal assignment:
- PC0: Forward latch
- PC1: Forward PWM
- PC2: Reverse latch
- PC3: Reverse PWM
Timer2 is configured for Fast PWM with OCR2A as TOP. With OCR2A = 199 and prescaler = 8 at 8 MHz, PWM frequency is about 5 kHz. Instead of using OC2A/OC2B output pins directly, compare interrupts are used:
- TIMER2_COMPA_vect sets the active direction PWM pin high (PC1 for forward or PC3 for reverse)
- TIMER2_COMPB_vect clears that same active pin low
This effectively creates direction-dependent PWM on one of two output pins while the opposite side remains off.
Direction changes are intentionally staged to protect the bridge and drivetrain:
- Disable Timer2 PWM (stop switching)
- Force all H-bridge outputs low
- Wait 150 ms dead-time
- Assert new direction latch (PC0 forward or PC2 reverse)
- Re-enable Timer2 PWM
That delay prevents hard instant reversals and helps ensure MOSFETs on the same side of the H-bridge are not on at the same time during direction transitions, reducing shoot-through/braking stress.
Additional control logic from the car firmware:
- Speed deadband: commands between -15 and +15 stop the motor to avoid joystick/noise creep
- Steering deadzone: commands within +/-90 us of the calibrated center are snapped to neutral
- Direction is only changed when needed (or from stopped state)
- A watchdog is enabled after first RF packet and reset on each valid packet; if RF traffic is lost, the MCU resets after the watchdog timeout (2 s)
Using Timer2 compare interrupts for every PWM cycle means the MCU is servicing frequent interrupts continuously while also polling and reading nRF24 payloads over SPI. That interrupt load can steal time from RF handling and make robust payload reception harder than a pure hardware-PWM motor path.
In practice, the current tuning (about 5 kHz PWM, deadband around zero, staged direction changes) was a workable sweet spot between motor smoothness and reliable radio updates. A strictly hardware PWM implementation for both drive channels would reduce ISR overhead and leave more CPU time for RF processing.
This power system was sized around worst-case drive events (motor stall plus active steering), not just average cruising current.
Estimated load envelope:
- 9V drive motor stall current at 7.2V: ~600 mA
- MG90S steering servo working current: ~600-700 mA
- Control circuitry (MCU, RF, logic, support electronics): ~20 mA
Worst-case instantaneous estimate:
- Total peak demand:
$0.60 + 0.70 + 0.02 \approx 1.32,A$
Battery capability:
- 2S 200 mAh pack, 20C rating
- Maximum continuous supply:
$0.2,Ah \times 20 = 4,A$
Power-stage device headroom:
- IRLML6244 continuous drain current: 6.3 A (comfortably above motor stall current)
- IRLML2246 continuous source current: -1.3 A (above the ~600 mA motor stall requirement in this bridge path)
Conclusion: the battery and MOSFET current limits provide practical headroom over the expected peak load, so the system can supply drive, steering, and control electronics simultaneously under normal and transient operation.
For safe charging practice on the car side, do not charge the car while it is powered on. Turn the car off first, then connect the charger.
|
|
The handheld controller samples two joystick axes, converts them into throttle and steering commands, and transmits those values over 2.4 GHz to the car at a steady update rate. The design goal is low-latency control with stable center behavior so the car tracks driver input without twitching or drift.
The remote uses an nRF24L01+ module with RF24 over SPI to transmit MotorControlPayload packets containing ocrMotor and ocrSteering. During initialization, firmware performs a radio hardware check, configures dynamic payload mode, and sets TX/RX addresses for the paired node. Timer0 is also configured with a 50 us base so the millis()/micros() timing used by RF24 remains stable while packets are sent continuously.
Joystick values are read through the ADC and mapped into the exact command domains expected by the vehicle firmware. This preprocessing keeps control semantics consistent across the RF link: signed throttle around zero and steering in absolute pulse-width units centered at a calibrated neutral.
Throttle is sampled on ADC channel 0, inverted to match intuitive stick direction, scaled into a signed drive command, and centered around zero. Positive values represent forward demand, negative values represent reverse demand, and near-zero commands cooperate with the car-side deadband to prevent idle creep.
Steering is sampled on ADC channel 1 and converted using a calibration profile where measured joystick potentiometer minimum, maximum, and center voltages are entered in firmware. The mapping uses those measured endpoints and center value to generate steering pulses, then clamps to the valid steering window before transmission. This makes neutral alignment and left/right travel more accurate than a fixed center assumption. A center deadzone is applied so small stick jitter and ADC noise snap back to neutral, reducing servo chatter and improving straight-line stability.
To measure and enter steering calibration voltages:
- Power the controller on.
- Connect the multimeter to ground and to the X-axis test point by the left controller.
- Measure the voltage with the analog stick in its center position (center voltage).
- Measure the voltage with the stick moved all the way to the right (minimum voltage for this mapping).
- Measure the voltage with the stick moved all the way to the left (maximum voltage for this mapping).
The remote has a built-in LiPo charging circuit. When a charger is connected, the controller may automatically power on while charging. You can place the power switch in the OFF position before charging if you prefer it to stay off.
|
|
ATmega164A/P pin information is from the Microchip datasheet: https://ww1.microchip.com/downloads/en/devicedoc/atmel-8272-8-bit-avr-microcontroller-atmega164a_pa-324a_pa-644a_pa-1284_p_datasheet.pdf
| Signal | MCU Pin | Notes |
|---|---|---|
| nRF24 CE | PB0 | Defined as CE_PIN 0 in car RF firmware |
| nRF24 CSN | PB1 | Defined as CSN_PIN 1 in car RF firmware |
| nRF24 SPI SCK | PB7 | Hardware SPI clock |
| nRF24 SPI MISO | PB6 | Hardware SPI MISO |
| nRF24 SPI MOSI | PB5 | Hardware SPI MOSI |
| nRF24 SPI SS | PB4 | Hardware SPI SS |
| Steering PWM (OC1B) | PD4 | Servo output (Timer1 OC1B) |
| Motor Forward Latch | PC0 | H-bridge control |
| Motor Forward PWM | PC1 | H-bridge control |
| Motor Reverse Latch | PC2 | H-bridge control |
| Motor Reverse PWM | PC3 | H-bridge control |
| Status LED | PD5 | Set during startup/status |
| Signal | MCU Pin | Notes |
|---|---|---|
| nRF24 CE | PB0 | Defined as CE_PIN 0 in remote RF firmware |
| nRF24 CSN | PB1 | Defined as CSN_PIN 1 in remote RF firmware |
| nRF24 SPI SCK | PB7 | Hardware SPI clock |
| nRF24 SPI MISO | PB6 | Hardware SPI MISO |
| nRF24 SPI MOSI | PB5 | Hardware SPI MOSI |
| nRF24 SPI SS | PB4 | Hardware SPI SS |
| Throttle Joystick (Y) ADC | PA0 / ADC0 | Read with readADC(0) |
| Steering Joystick (X) ADC | PA1 / ADC1 | Read with readADC(1) |
See LICENSE for details.