Ivan Barajas
Published © MIT

Diving into AVR: Bare-Metal Motor control with ADC-interrupt

High-performance motor control on ATmega328P using Bare-Metal drivers, Timer1 PWM, and interrupt-driven ADC to eliminate library latency.

IntermediateShowcase (no instructions)16
Diving into AVR: Bare-Metal Motor control with ADC-interrupt

Things used in this project

Hardware components

Arduino Nano R3
Arduino Nano R3
The microcontroller in this board is the Atmel ATmega328P
×1
SparkFun Motor Driver - Dual TB6612FNG (1A)
SparkFun Motor Driver - Dual TB6612FNG (1A)
This driver uses MOSFET techonology and has a smaller voltage drop compared to the L298N.
×1
Rotary potentiometer (generic)
Rotary potentiometer (generic)
10k ohm is the standart choice but any value from 5K and above will do just fine
×1
DC Motor, 12 V
DC Motor, 12 V
You can use any type of 12V DC motor!
×1
9V battery (generic)
9V battery (generic)
Any voltage source will do just fine!
×1

Story

Read more

Schematics

Motor control Conection diagram

Connection diagram for the TB6612FNG using Arduino NANO

Code

Bare-Metal motor control wtih ADC interrups

C/C++
This program allows you to control the speed of a DC motor with a potentiometer input.
The pins on the Arduino board are: EN1 = D4, EN2 = D5, Input = A0, PWM = D9.
If using the TB6612fng, connect the STANDBY pin to 5V.
/*
 * Copyright (c) 2026 [Tu Nombre]
 * Licensed under the MIT License.
 * See LICENSE file in the project root for full license information.
 */

// Register addresses extracted from page 276 of the ATmega328P Datasheet
#define DDRB   *((volatile unsigned char *)0x24) // Port B Data Direction Register
#define DDRD   *((volatile unsigned char *)0x2A) // Port D Data Direction Register
#define PORTD  *((volatile unsigned char *)0x2B) // Port D Data Register
#define DDRC   *((volatile unsigned char *)0x27) // Port C Data Direction Register
#define PORTC  *((volatile unsigned char *)0x28) // Port C Data Register
#define ADCMUX *((volatile unsigned char *)0x7C) // ADC Multiplexer Selection Register
#define ADCSRA *((volatile unsigned char *)0x7A) // ADC Control and Status Register A
#define SREG   *((volatile unsigned char *)0x5F) // Status Register - Bit 7: Global Interrupt Enable
#define TCCR1A *((volatile unsigned char *)0x80) // Timer/Counter 1 Control Register A
#define TCCR1B *((volatile unsigned char *)0x81) // Timer/Counter 1 Control Register B
#define ICR1   *((volatile unsigned int  *)0x86) // Input Capture Register 1 (Defines PWM Period/TOP)
#define OCR1A  *((volatile unsigned int  *)0x88) // Output Compare Register 1A (Duty Cycle for D9)
#define OCR1B  *((volatile unsigned int  *)0x8A) // Output Compare Register 1B (Duty Cycle for D10)
#define ADC_16BIT *((volatile unsigned int *)0x78) // 16-bit ADC Data Register (ADCL + ADCH)

// Control and PID Variables
volatile int value = 0, pwm = 0;
volatile bool sensores_listos = 0, value_ready = 0;

void setup() {
    // Reset all peripherals to a known stable state
    reset_pins();
    reset_pins();

    // Peripheral Configuration
    GPIO_config();
    TIM1_config();
    ADC_config(); // ADC_config enables ADIE and Global Interrupts (SREG)

    // Initial Motor Direction Setup
    PORTD = (1<<4)|(1<<7);
    
    // Enable Global Interrupts (Assembler instruction for reliability)
    asm volatile("sei"); 

    // IGNITION: Trigger the first ADC conversion
    ADCSRA |= (1 << 6);
}

void loop() {
    // Polling function to trigger ADC if it's idle
    get_pot_value();

    // Check if the ISR has flagged a new completed conversion
    if (value_ready) {
        value_ready = 0;
        set_motor_speed(value);
    }
    
    // Optional: Small delay if needed for specific timing requirements
    // for(volatile long i=0; i<20000; i++);
}

// --------------------------- CONTROL FUNCTIONS ---------------------------

/**
 * Updates the PWM duty cycle for Timer 1.
 * Maps the 10-bit input (0-1023) to the PWM TOP value (0-1000).
 */
void set_motor_speed(int data){
    pwm = (1000L * data) / 1023;
    OCR1A = pwm;
}

/**
 * Ensures the ADC conversion is triggered. 
 * Only fires a manual trigger if the ADC is not currently busy (Bit 6 ADSC is low).
 */
void get_pot_value(void){
    if (!(ADCSRA & (1 << 6))) {
        ADCSRA |= (1 << 6);
    }
}

// ------------------------ CONFIGURATION FUNCTIONS ------------------------

/**
 * Configures the Analog-to-Digital Converter.
 */
void ADC_config(void){
    // REFS[1:0] = 01 -> Use AVcc as reference.
    // ADLAR = 0 -> Right-adjust result (standard 10-bit representation).
    ADCMUX = (1<<6);

    // ADEN = 1 (Enable ADC), ADIE = 1 (Enable Interrupt).
    // ADPS[2:0] = 111 (Prescaler 128: 16MHz/128 = 125kHz, within the optimal 50-200kHz range).
    ADCSRA = (1<<7)|(7<<0)|(1<<3);

    // Enable global interrupts in the status register
    SREG |= (1<<7);
}

/**
 * Resets all used registers to 0x0 to ensure a clean peripheral state.
 */
void reset_pins(void){
    TCCR1A = 0x0; TCCR1B = 0x0; DDRB = 0x0; ICR1 = 0x0; DDRD = 0x0; PORTD = 0x0;
    OCR1A = 0x0; OCR1B = 0x0; DDRC = 0x0; ADCMUX = 0x0;
}

/**
 * Configures Timer 1 for Fast PWM on pins PB1 (D9) and PB2 (D10).
 * Prescaler: 8 | TOP: 999 | Resulting Frequency: 2kHz.
 */
void TIM1_config(void){
    // COM1A/B[1:0] = 10 -> Non-inverted PWM.
    // WGM1[1:0] = 10 -> Part of Fast PWM mode 14 (ICR1 as TOP).
    TCCR1A = (1<<7)|(1<<5)|(1<<1);

    // WGM1[3:2] = 11 -> Completion of Fast PWM mode 14.
    // CS1[2:0] = 010 -> Clock Source: Prescaler 8.
    TCCR1B = (1<<4)|(1<<3)|(1<<1);

    // Set the TOP value for the timer (Period = ICR1 + 1)
    ICR1 = 999;
}

/**
 * Sets Data Direction Registers (DDR) for the used pins.
 */
void GPIO_config(void)
{
    // Configure PB1(D9), PB2(D10) and PB5(D13 Status LED) as Outputs.
    DDRB = (1<<1)|(1<<2)|(1<<5); 

    // Configure PD4, PD5, PD6, PD7 as Outputs for H-Bridge control.
    DDRD = (1<<4)|(1<<5)|(1<<6)|(1<<7);

    // Configure A1, A2, A3, A5 as Outputs. A0 is left as Input (0).
    DDRC = (1<<1)|(1<<2)|(1<<3)|(1<<4);
}

/**
 * ADC Conversion Complete Interrupt Service Routine.
 * Vector 21 corresponds to the ADC Interrupt on the ATmega328P.
 */
extern "C" void __vector_21 (void) __attribute__ ((signal, used, externally_visible));
void __vector_21 (void) {
    value = ADC_16BIT; // Capture 10-bit result
    value_ready = 1;   // Set flag for the main loop
    PORTB ^= (1 << 5); // Toggle D13 LED for visual heartbeat/debug
}

/**
 * Logic for Left Turn (H-Bridge configuration).
 */
void girar_izq(void){
    PORTD = 0x0;
    PORTD = (1<<4)|(1<<7);
}

/**
 * Logic for Right Turn (H-Bridge configuration).
 */
void girar_der(void){
    PORTD = 0x0;
    PORTD = (1<<5)|(1<<6);
}

Credits

Ivan Barajas
1 project • 0 followers
Mechatronics student with a passion for embedded systems and firmware development at the low level.

Comments