Table of Contents

Adaptive Cruise Control Vehicle

Introduction

What it does: This project consists of a small autonomous vehicle capable of maintaining a constant safe distance from a target object or vehicle in front of it.

Its purpose: The main goal is to implement a real-time closed-loop control system (Adaptive Cruise Control) on an 8-bit microcontroller. It demonstrates bare-metal embedded programming by utilizing hardware interrupts, hardware timers, and custom serial protocols, completely avoiding high-level abstractions like Arduino libraries.

The starting idea: The idea is inspired by modern Advanced Driver Assistance Systems (ADAS) used in the automotive industry to prevent rear-end collisions and improve driving comfort.

Why it is useful: It serves as a highly practical demonstration of low-level hardware control, bridging the gap between theoretical micro-processor architecture and real-world physical applications.

General Description

The system uses an ultrasonic sensor mounted on the front to continuously measure the distance to the obstacle ahead.

Based on the calculated distance, a proportional control algorithm calculates the necessary speed adjustments to maintain a safe gap. The system smoothly interpolates the motor speed using hardware PWM based on the target's distance. If the target is too close (under 25 cm), the vehicle applies the brakes. If the target is within the tracking range (25 cm - 85 cm), it adjusts the speed dynamically to follow it.

Block Diagram:

Hardware Design

List of components:

Electrical Schematic:

Software Design

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>
#include <stdio.h>
 
#define PM_BAUD 9600
 
#define TRIG_PIN PC0
#define ECHO_PIN PC1
 
#define LATCH_PIN PB4
#define CLK_PIN   PD4
#define EN_PIN    PD7
#define DATA_PIN  PB0
 
#define M1_A 2
#define M1_B 3
#define M2_A 1
#define M2_B 4
 
volatile uint16_t timer_val = 0;
volatile uint8_t echo_done = 0;
 
static int _usart0_putchar(char c, FILE *stream) {
    if (c == '\n') _usart0_putchar('\r', stream);
    while (!(UCSR0A & (1<<UDRE0)));
    UDR0 = c;
    return 0;
}
 
static FILE USART0_stdout = FDEV_SETUP_STREAM(_usart0_putchar, NULL, _FDEV_SETUP_WRITE);
 
void usart_init() {
    UBRR0H = (unsigned char)(103>>8); //br9600
    UBRR0L = (unsigned char)103;
    UCSR0B = (1<<RXEN0) | (1<<TXEN0); //act transmisia
    UCSR0C = (1<<USBS0) | (3<<UCSZ00); //8b
    stdout = &USART0_stdout;
}
 
void shift_out_data(uint8_t data) {
    PORTB &= ~(1<<LATCH_PIN); 
 
    for(int i=0; i<8; i++) { //pt fiecare bit
        PORTD &= ~(1<<CLK_PIN);
        if(data & (1 << (7-i))) {
            PORTB |= (1<<DATA_PIN); //punem val d
        } else {
            PORTB &= ~(1<<DATA_PIN);
        }
        PORTD |= (1<<CLK_PIN); //ridicam si coboram pin clock
    }
 
    PORTB |= (1<<LATCH_PIN); 
}
 
void motors_init() {
    DDRB |= (1<<LATCH_PIN) | (1<<DATA_PIN) | (1<<PB3);
    DDRD |= (1<<CLK_PIN) | (1<<EN_PIN) | (1<<PD3);
 
    PORTD &= ~(1<<EN_PIN);
 
    TCCR2A = (1<<COM2A1) | (1<<COM2B1) | (1<<WGM21) | (1<<WGM20);
    TCCR2B = (1<<CS22);
 
    OCR2A = 0; // punem viteza in registrii
    OCR2B = 0;
 
    shift_out_data(0);
}
 
void set_motor_dir(int m1_fwd, int m1_rev, int m2_fwd, int m2_rev) {
    uint8_t shift_data = 0;
    if (m1_fwd) shift_data |= (1<<M1_A);
    if (m1_rev) shift_data |= (1<<M1_B);
    if (m2_fwd) shift_data |= (1<<M2_A);
    if (m2_rev) shift_data |= (1<<M2_B);
    shift_out_data(shift_data);
}
 
void sonar_init() {
    DDRC |= (1<<TRIG_PIN); //pc0 output
    DDRC &= ~(1<<ECHO_PIN); 
 
    TCCR1A = 0;
    TCCR1B = (1<<CS11);
 
    PCICR |= (1<<PCIE1);
    PCMSK1 |= (1<<PCINT9);
}
 
ISR(PCINT1_vect) {
    if (PINC & (1<<ECHO_PIN)) {
        TCNT1 = 0;
    } else {
        timer_val = TCNT1;
        echo_done = 1;
    }
}
 
int get_distance() {
    echo_done = 0;
 
    PORTC &= ~(1<<TRIG_PIN);
    _delay_us(2);
    PORTC |= (1<<TRIG_PIN);
    _delay_us(10);
    PORTC &= ~(1<<TRIG_PIN);
 
    uint32_t timeout = 60000;
    while(!echo_done && timeout > 0) {
        timeout--;
    }
 
    if(timeout == 0) return 400;
 
    return timer_val / 116;
}
 
int map_speed(int x, int in_min, int in_max, int out_min, int out_max) {
    return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
 
int main() {
    usart_init();
    motors_init();
    sonar_init();
 
    sei();
    printf("ACC System Bare Metal Started\n");
 
    int safe_dist = 25;
    int max_dist = 85;
 
    while(1) {
        int dist = get_distance();
        if(dist == 0 || dist > 400) dist = 400;
 
        printf("Dist: %d cm\n", dist);
 
        if (dist > max_dist) {
            set_motor_dir(0, 0, 0, 0);
            OCR2A = 0;
            OCR2B = 0;
        } else if (dist > safe_dist) {
            int speed = map_speed(dist, safe_dist, max_dist, 115, 220);
            set_motor_dir(1, 0, 1, 0);
            OCR2A = speed;
            OCR2B = speed;
        } else {
            set_motor_dir(0, 0, 0, 0);
            OCR2A = 0;
            OCR2B = 0;
        }
 
        _delay_ms(50);
    }
    return 0;
}

Development Environment:

Libraries and 3rd-party sources:

Algorithms and Hardware Features utilized:

Obtained Results

The system reacts accurately and smoothly to dynamic environments.

Final Project Photo:

Conclusions

Implementing this project in a bare-metal C environment provided profound insights into the internal workings of the ATmega328P microcontroller. Stripping away the Arduino framework forced a deeper understanding of memory mapping, the crucial difference between polling and hardware interrupts, and the complexity of generating custom serial signals (bit-banging). The project successfully replicates an industrial ADAS concept on an 8-bit architecture.

Download

Project Archive - Source Code and PlatformIO Configuration

Journal

Bibliography/Resources

Hardware Resources:

Software Resources: