Developing in C for the ATmega328: A Simple Project

4 minute read

Where I use an example project from avr-gcc to better understand how to program the ATmega328.

Sources

Introduction

In the previous entry, I wrote a C equivilent to the Arduino analogWrite(). The entry demonstrates the fundamental method of getting the ATmega328 to perform, is to program the registers. From here on, we’ll continue to expand on this approach.

While reviewing the avr-gcc manual, I ran across the folder of projects designed to help you understand how to use avr-gcc and program an AVR microcontroller. The one labeled “A simple project” is of great value. While it is simple, however, it utilizes interrupts which will be of great value and can be difficult to program.

Process

I recommend following the project on its page as well as reviewing this page to understand the changes necessary for the ATmega328.

Schematic

I’m using an UNO with a small breadboard, so the only additional connections were a 220ohm resistor wired from Arduino pin 9/PB1 to a connection on the breadboard then an LED from the other end of the resistor to GND (identical to the schematic on the project page, except I use PB1).

The more important connection was from the Labrador CH1 Oscilloscope to the resistor/LED node as well. This allowed me to view the wave changes on a scope and to better understand what the interrupt was doing.

Here’s a video showing how the waveform looks:

It is much easier to view (and understand) using the Labrador Oscilloscope, than viewing the LED (which is fading in and out).

iocompat.h

Originally, I planned to continue to use the file iocompat.h to make changes appropriate to the ATmega328P. The easy way to do this is to add

|| (__AVR_ATmega328P__)

to the end of line 125. This seemed to work from an constant perspective, I was able to compile without any errors. That said, my code didn’t work and I believed there were some initialization errors causing problems.

I decided to delete the reference to iocompat.h and fix the constants by hand. This would also give me an opportunity to ensure everything was setup properly specifically for the ATmega328.

Changes Made

Ultimately the changes I made were the following, numbers refer to line numbers in orginal demo.c file on the gnu.org site:

# I copied the Makefile over from another example and changed the file name on line 48
22 replaced #include for iocompat.h with
#define TIMER1_TOP 1023   /* 10-bit PWM */
28 changed pwm variable name to duty_cycle as that is what is changing
34  "
39  "
44  "
51 changed TIMER1_PWM_INIT to "|=  _BV(COM1A1) | _BV(WGM11) | _BV(WGM10)"
67 changed OCR to OCR1A
70 changed DDROC and DC1 to DDRB and PB1, respectively
73 changed TIMSK to TIMSK1

Essentially, I made the changes very specific to what was needed for the ATmega328P. Given I’m not interested in having this file work for multiple types of AVR processors, the changes work well for me. I also added comments where I thought it was important to understand what the changes meant.

New file

#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>

#define TIMER1_TOP 1023   /* 10-bit PWM */

enum { UP, DOWN };

ISR (TIMER1_OVF_vect)       /* Note [2] */
{
    static uint16_t duty_cycle;    /* Note [3] */
    static uint8_t direction;
    switch (direction)      /* Note [4] */
    {
        case UP:
            if (++duty_cycle == TIMER1_TOP)
                direction = DOWN;
            break;
        case DOWN:
            if (--duty_cycle == 0)
                direction = UP;
            break;
    }
    OCR1A = duty_cycle;          /* Note [5] */
}

void ioinit (void)           /* Note [6] */
{
    /* Timer 1 is 10-bit PWM 
    * TCCR1A = TIMER1_PWM_INIT;
    * COM1A1:0 =10 => Clear OC1A/OC1B on Compare Match when up-counting
    * WGM2:0 =011 => PWM, Phase Correct, 10-bit
    */
    TCCR1A |=  _BV(COM1A1) | _BV(WGM11) | _BV(WGM10);
    /*
     * Start timer 1.
     * CS12:0 = 011 => clkI/O/64 (From prescaler)
     */
    TCCR1B |= _BV(CS11) | _BV(CS10);
    /* Set PWM value to 0. */
    OCR1A = 0;
    /* Enable OC1 as output. */
    DDRB = _BV (PB1);
    /* Enable timer 1 overflow interrupt. */
    TIMSK1 = _BV (TOIE1);
    sei ();
}

int main (void)
{
    ioinit ();

    /* loop forever, the interrupts are doing the rest */
    for (;;)  {          /* Note [7] */
        // sleep_mode();
        sleep_mode();
    }
    return (0);
}

From here, I can use this framework for setting up other programs which need a timer interrupt. Stay tuned!

Created tenthTimer

I made some simple changes to the Simple Project above and created a tenthTimer() interrupt-based timer. In its present state it provides a 100Hz 50% duty cycle, square wave on pin 8. It could be used for long timing, non-blocking timing routines, multi-tasking etc. It was worth working on to understand how simple ISR’s can be!

Comments powered by Talkyard.