Controlling a Two-Wheel-Drive Robot Vehicle

 

Controlling a Two-Wheel-Drive Robot Vehicle

1. Introduction

Autonomous and semi-autonomous ground vehicles have become a cornerstone of modern robotics, appearing in applications ranging from warehouse automation and agricultural machinery to educational platforms and hobbyist projects. At the heart of many of these systems lies a simple yet effective mechanical configuration: the two-wheel-drive (2WD) robot vehicle. Despite its mechanical simplicity, a 2WD robot provides an excellent foundation for understanding the core principles of embedded motor control, sensor integration, and real-time decision making.

This project presents the design and implementation of a two-wheel-drive robot vehicle controlled using an ATmega328P microcontroller. The system allows directional control of two DC motors through a push-button interface, enabling forward and backward motion, while an LCD display provides real-time feedback on the current operating state of the vehicle. Motor driving is handled by an L293D H-bridge integrated circuit, which allows bidirectional control of both motors independently from a single low-power microcontroller output.


2. Background and Motivation

DC motors are among the most widely used actuators in robotics due to their simplicity, availability, and ease of control. However, a microcontroller cannot drive a DC motor directly — its output pins supply only a few milliamps of current, far below what a motor requires to operate. A motor driver IC such as the L293D bridges this gap by acting as a current amplifier, accepting low-power logic signals from the microcontroller and switching higher-power motor currents accordingly.

The L293D is a dual H-bridge motor driver capable of controlling two DC motors simultaneously in both forward and reverse directions. Each H-bridge channel consists of four transistor switches arranged in an H-shaped configuration. By selectively activating pairs of switches, current is forced through the motor in either direction, producing forward or reverse rotation. Enable pins on the L293D allow each channel to be independently switched on or off, providing an additional layer of control.

The ATmega328P, an 8-bit AVR microcontroller manufactured by Microchip Technology (formerly Atmel), serves as the control unit for this system. It is widely used in embedded applications due to its rich peripheral set, low power consumption, ease of programming, and strong community support through platforms such as Arduino. Operating at 16MHz with an external crystal oscillator, it provides sufficient processing speed for real-time motor control and display management.


3. System Overview

The complete system consists of four main hardware blocks working together:

Microcontroller (ATmega328P) — reads button inputs, executes control logic, drives the motor driver and updates the LCD display. All decision making happens here.

Motor Driver (L293D) — receives direction commands from the microcontroller on its IN1–IN4 input pins and drives the two DC motors through its OUT1–OUT4 output pins. It operates on a separate 12V motor supply, isolating the high-current motor circuit from the sensitive microcontroller logic circuit.

16x2 LCD Display — connected to the microcontroller via a 4-bit parallel interface on PORTC, it displays the current vehicle state (FORWARD, BACKWARD, or STOPPED) in real time, giving the operator immediate visual confirmation of the active command.

Push Button Interface — two momentary push buttons connected to PD2 and PD3 serve as the operator input. The buttons use the microcontroller's internal pull-up resistors, requiring no external components. The control scheme is hold-to-move: the vehicle moves only while a button is held, and stops immediately upon release, mimicking the feel of a basic RC transmitter.

The overall system architecture is shown below:


Looking at your circuit diagram, here's a full breakdown:


Power Supply Section (Left side)

B1 (12V Battery) feeds two rails:

  • Directly to L293D pin 8 (VS) — motor power (12V)
  • Through U2 (7805 voltage regulator) → outputs 5V for ATmega328P and LCD logic

C3 (100µF), C1 (0.33µF) — input filter capacitors for the 7805, smoothing the 12V input C2 (0.1µF), C4 (10µF) — output filter capacitors on the 5V rail, removing ripple


Microcontroller Section (Center)

U1 — ATmega328P running at 16MHz via:

  • X1 — 16MHz crystal oscillator
  • C5, C6 (22pF each) — crystal load capacitors (required for oscillation)
  • R1 (10k) — reset pull-up resistor on PC6/RESET pin

Pin connections visible:

ATmega PinSignal
PC0–PC5LCD data + RS + EN
PB0, PB1EN1, EN2 (L293D enable)
PB2–PB5IN1–IN4 (L293D inputs)
PD2, PD3Push buttons

Motor Driver Section (Right side)

U3 — L293D receives:

  • 5V on pin 16 (Vcc1) — logic power from 7805
  • 12V on pin 8 (VS) — motor power directly from battery
  • EN1, EN2 — enable signals from PB0, PB1
  • IN1–IN4 — direction signals from PB2–PB5
  • OUT1/OUT2 — drive Motor A (left wheel)
  • OUT3/OUT4 — drive Motor B (right wheel)

LCD Section (Top right)

16x2 LCD connected to PORTC:

  • RV1 (10k potentiometer) — contrast adjustment (V0 pin)
  • R2 (10k) — backlight current limiting resistor
  • Currently displaying "RC CAR CTRL / << BACKWARD<<" confirming backward button is pressed in this snapshot

Button Section

Two push buttons connected to PD2 and PD3 with internal pull-ups enabled in software — no external pull-up resistors needed, which is why you don't see any resistors on those lines in the schematic.

The program code:

#ifndef F_CPU
#define F_CPU 16000000UL
#endif

#include <avr/io.h>
#include <util/delay.h>
#include <stdlib.h>

//=============================================================================
// LCD PIN DEFINITIONS (PORTC, 4-bit mode)
//=============================================================================
#define LCD_RS_DDR      DDRC
#define LCD_RS_PORT     PORTC
#define LCD_RS_PIN      PC5

#define LCD_E_DDR       DDRC
#define LCD_E_PORT      PORTC
#define LCD_E_PIN       PC4

#define LCD_DATA_DDR    DDRC
#define LCD_DATA_PORT   PORTC
#define LCD_D4          PC3
#define LCD_D5          PC2
#define LCD_D6          PC1
#define LCD_D7          PC0

//=============================================================================
// L293D MOTOR PIN DEFINITIONS (PORTB)
// Motor A: EN1(PB0), IN1(PB2), IN2(PB3) -> OUT1, OUT2
// Motor B: EN2(PB1), IN3(PB4), IN4(PB5) -> OUT3, OUT4
//=============================================================================
#define MOTOR_PORT  PORTB
#define MOTOR_DDR   DDRB
#define EN1         PB0
#define EN2         PB1
#define IN1         PB2
#define IN2         PB3
#define IN3         PB4
#define IN4         PB5

//=============================================================================
// PUSH BUTTON DEFINITIONS (PORTD, Active LOW with internal pull-up)
//=============================================================================
#define BTN_DDR         DDRD
#define BTN_PORT        PORTD
#define BTN_PIN         PIND
#define BTN_FORWARD     PD2
#define BTN_BACKWARD    PD3

//=============================================================================
// LCD COMMAND DEFINITIONS
//=============================================================================
#define LCD_CMD_CLEAR           0x01
#define LCD_CMD_HOME            0x02
#define LCD_CMD_ENTRY_MODE      0x06
#define LCD_CMD_DISPLAY_ON      0x0C
#define LCD_CMD_FUNCTION_4BIT   0x28
#define LCD_CMD_LINE1           0x80
#define LCD_CMD_LINE2           0xC0

//=============================================================================
// MOTOR STATE
//=============================================================================
typedef enum {
    STATE_STOPPED,
    STATE_FORWARD,
    STATE_BACKWARD
} MotorState;

//=============================================================================
// DELAY FUNCTIONS
//=============================================================================
void delay_ms(unsigned int ms) {
    while(ms--) _delay_ms(1);
}

void delay_us(unsigned int us) {
    while(us--) _delay_us(1);
}

//=============================================================================
// LCD FUNCTIONS
//=============================================================================
void lcd_pulse_enable(void) {
    LCD_E_PORT |=  (1 << LCD_E_PIN);
    delay_us(2);
    LCD_E_PORT &= ~(1 << LCD_E_PIN);
    delay_us(2);
}

void lcd_send_nibble(unsigned char nibble) {
    LCD_DATA_PORT &= ~((1 << LCD_D4) | (1 << LCD_D5) |
                       (1 << LCD_D6) | (1 << LCD_D7));
    if(nibble & 0x01) LCD_DATA_PORT |= (1 << LCD_D4);
    if(nibble & 0x02) LCD_DATA_PORT |= (1 << LCD_D5);
    if(nibble & 0x04) LCD_DATA_PORT |= (1 << LCD_D6);
    if(nibble & 0x08) LCD_DATA_PORT |= (1 << LCD_D7);
    lcd_pulse_enable();
}

void lcd_send_byte(unsigned char data, unsigned char is_data) {
    if(is_data) LCD_RS_PORT |=  (1 << LCD_RS_PIN);
    else        LCD_RS_PORT &= ~(1 << LCD_RS_PIN);
    lcd_send_nibble((data >> 4) & 0x0F);
    lcd_send_nibble(data & 0x0F);
    delay_us(100);
}

void lcd_command(unsigned char cmd) {
    lcd_send_byte(cmd, 0);
    if(cmd == LCD_CMD_CLEAR || cmd == LCD_CMD_HOME)
        delay_ms(2);
}

void lcd_putchar(char c) {
    lcd_send_byte((unsigned char)c, 1);
}

void lcd_putstr(const char* str) {
    while(*str) lcd_putchar(*str++);
}

void lcd_clear(void) {
    lcd_command(LCD_CMD_CLEAR);
    delay_ms(2);
}

void lcd_goto(unsigned char row, unsigned char col) {
    unsigned char address;
    if(row == 0) address = LCD_CMD_LINE1 + col;
    else         address = LCD_CMD_LINE2 + col;
    lcd_command(address);
}

void lcd_init(void) {
    LCD_RS_DDR    |= (1 << LCD_RS_PIN);
    LCD_E_DDR     |= (1 << LCD_E_PIN);
    LCD_DATA_DDR  |= (1 << LCD_D4) | (1 << LCD_D5) |
                     (1 << LCD_D6) | (1 << LCD_D7);

    LCD_RS_PORT   &= ~(1 << LCD_RS_PIN);
    LCD_E_PORT    &= ~(1 << LCD_E_PIN);
    LCD_DATA_PORT &= ~((1 << LCD_D4) | (1 << LCD_D5) |
                       (1 << LCD_D6) | (1 << LCD_D7));
    delay_ms(50);

    // 3-step HD44780 reset sequence
    lcd_send_nibble(0x03); delay_ms(5);
    lcd_send_nibble(0x03); delay_us(200);
    lcd_send_nibble(0x03); delay_us(200);
    lcd_send_nibble(0x02); delay_us(200);  // Switch to 4-bit mode

    lcd_command(LCD_CMD_FUNCTION_4BIT);
    lcd_command(LCD_CMD_DISPLAY_ON);
    lcd_command(LCD_CMD_CLEAR);
    delay_ms(2);
    lcd_command(LCD_CMD_ENTRY_MODE);
    lcd_command(LCD_CMD_HOME);
    delay_ms(2);
}

//=============================================================================
// MOTOR FUNCTIONS
// EN1 and EN2 are explicitly set HIGH in every function to prevent
// any read-modify-write corruption on PORTB from clearing them
//=============================================================================
void motors_init(void) {
    MOTOR_DDR |= (1 << EN1) | (1 << EN2) |
                 (1 << IN1) | (1 << IN2) |
                 (1 << IN3) | (1 << IN4);

    // All IN pins LOW first
    MOTOR_PORT &= ~((1 << IN1) | (1 << IN2) |
                    (1 << IN3) | (1 << IN4));

    // Enable both motor channels
    MOTOR_PORT |= (1 << EN1) | (1 << EN2);
}

void motors_forward(void) {
    // Clear all IN pins first
    MOTOR_PORT &= ~((1 << IN1) | (1 << IN2) |
                    (1 << IN3) | (1 << IN4));
    // Guarantee EN1 and EN2 are HIGH
    MOTOR_PORT |=  (1 << EN1) | (1 << EN2);
    // Motor A: IN1=H, IN2=L  |  Motor B: IN3=H, IN4=L
    MOTOR_PORT |=  (1 << IN1) | (1 << IN3);
}

void motors_backward(void) {
    // Clear all IN pins first
    MOTOR_PORT &= ~((1 << IN1) | (1 << IN2) |
                    (1 << IN3) | (1 << IN4));
    // Guarantee EN1 and EN2 are HIGH
    MOTOR_PORT |=  (1 << EN1) | (1 << EN2);
    // Motor A: IN1=L, IN2=H  |  Motor B: IN3=L, IN4=H
    MOTOR_PORT |=  (1 << IN2) | (1 << IN4);
}

void motors_stop(void) {
    // Clear all IN pins
    MOTOR_PORT &= ~((1 << IN1) | (1 << IN2) |
                    (1 << IN3) | (1 << IN4));
    // Keep EN1 and EN2 HIGH (ready for next command)
    MOTOR_PORT |=  (1 << EN1) | (1 << EN2);
}

//=============================================================================
// BUTTON FUNCTIONS (Active LOW, internal pull-up)
//=============================================================================
void buttons_init(void) {
    BTN_DDR  &= ~((1 << BTN_FORWARD) | (1 << BTN_BACKWARD));
    BTN_PORT |=  (1 << BTN_FORWARD)  | (1 << BTN_BACKWARD);
}

unsigned char btn_forward_pressed(void) {
    return !(BTN_PIN & (1 << BTN_FORWARD));
}

unsigned char btn_backward_pressed(void) {
    return !(BTN_PIN & (1 << BTN_BACKWARD));
}

//=============================================================================
// LCD STATUS DISPLAY
//=============================================================================
void display_status(MotorState state) {
    lcd_goto(0, 0);
    lcd_putstr("  RC CAR CTRL   ");
    lcd_goto(1, 0);
    switch(state) {
        case STATE_FORWARD:
            lcd_putstr("  >> FORWARD >> ");
            break;
        case STATE_BACKWARD:
            lcd_putstr("  << BACKWARD<< ");
            break;
        case STATE_STOPPED:
        default:
            lcd_putstr("  [[ STOPPED ]] ");
            break;
    }
}

//=============================================================================
// MAIN
//=============================================================================
int main(void) {
    MotorState current_state = STATE_STOPPED;
    MotorState last_state    = STATE_STOPPED;

    lcd_init();
    motors_init();
    buttons_init();

    // Sanity check: force EN pins HIGH after all inits are done
    // This prevents any accidental PORTB corruption during lcd_init()
    MOTOR_PORT |= (1 << EN1) | (1 << EN2);
    delay_ms(10);

    // Startup splash
    lcd_clear();
    lcd_goto(0, 0);
    lcd_putstr("  RC CAR CTRL   ");
    lcd_goto(1, 0);
    lcd_putstr(" Initializing...");
    delay_ms(2000);

    display_status(STATE_STOPPED);

    while(1) {
        unsigned char fwd = btn_forward_pressed();
        unsigned char bwd = btn_backward_pressed();

        // Determine state from button input
        if(fwd && bwd) {
            // Both pressed simultaneously = stop (safety)
            current_state = STATE_STOPPED;
        } else if(fwd) {
            current_state = STATE_FORWARD;
        } else if(bwd) {
            current_state = STATE_BACKWARD;
        } else {
            // Neither pressed = stop (hold-to-move behavior)
            current_state = STATE_STOPPED;
        }

        // Only update motors and LCD when state actually changes
        // prevents unnecessary PORTB writes and LCD flicker
        if(current_state != last_state) {
            switch(current_state) {
                case STATE_FORWARD:  motors_forward();  break;
                case STATE_BACKWARD: motors_backward(); break;
                case STATE_STOPPED:  motors_stop();     break;
            }
            display_status(current_state);
            last_state = current_state;
        }

        delay_ms(20);  // Debounce + polling interval
    }

    return 0;
}

You can download the project file using the following links:









Post a Comment

Previous Post Next Post