Programming the Microchip ATmega328P in C

Introduction

This document describes how to program the Microchip ATmega328P microcontroller in C using the Avr-GCC toolchain. Students can download the software and install it on their computers by visiting a few websites. Atmel used to manufacture the ATmega328P microcontroller until it was acquired by Microchip Technology in 2016. Some documents on the EE459 website may still refer to it as the Atmel ATmega328P, but it’s the same microcontroller; the only difference is the company name.

Installing the Development Software

The Avr-GCC toolchain must first be installed on the computer. Detailed installation instructions can be found on the EE459 website at the following links:

  • Macs: https://ece-classes.usc.edu/ee459/library/toolchain_mac64
  • Windows: https://ece-classes.usc.edu/ee459/library/toolchain_windows
  • Linux: https://ece-classes.usc.edu/ee459/library/toolchain_linux

Editing Files

You will need a text editing program on your system to edit the text files associated with your project. The main requirement is that it can edit a plain text file without converting it to an RTF (Rich Text Format) file or another type of word-processing document. There are many of these available for free, and which one to use is primarily a matter of personal preference.

For Unix-like systems (Mac OS X, Linux, FreeBSD, and so on), the most common command-line interface text editors are probably “emacs”, “vi”, or “vim” (improved vi). All are available in various forms from various Internet sites. TextWranger and Aquamacs are two examples of GUI text editors that will work.

“Notepad++” is probably the easiest to find for Windows. One requirement for editing text files on Windows is that the editor be able to save files without the use of a filename extension such as “.txt.” If the editor insists on adding the extension, it is usually necessary to rename the file and remove the extension using a Command Prompt window.

Starting a New Project

The simplest way to start a new project is with a couple of template files from the class website. Make a new folder with a name for the new project.

Note: It is strongly advised that folder and file names do not contain spaces. If desired, use underscores (for example, lab 3). When navigating folders in a GUI, names with spaces are great, but not so great (i.e. painful) when using command line interfaces.

Download the “Makefile” and one of the sample AVR programs from the class websites and place them in the project folder.
To create your program, edit the C file. The Makefile will also need to be modified to ensure that the proper steps are taken when building the project.

The Makefile

There are several methods for compiling programs, but one simple and efficient method is to use a utility program called “make.” This program is run via a command line interface and will compile the source code and link the object files to create the executable binary file. The make program operates on the information contained in the text file “Makefile,” and this file is critical to the correct compilation and downloading of programs. It contains all of the information and commands required to compile the program and generate binary data that can be downloaded into the project board’s ATmega328P processor.

One purpose of the Makefile is to describe “dependencies” between the various parts of the project. The make program only compiles the components of the program that need to be compiled by knowing which program modules are dependent on other files and examining the modification times of the various components. For a simple project, this appears to be more trouble than it’s worth, but for a complex project with hundreds of source code files, the make program is invaluable and well worth learning how to use. Once the necessary information is in the Makefile, all of the required program modules can be compiled and linked together by simply typing the command ” make.”

If a Makefile is copied from one project to another, only a few lines at the top may need to be changed in order for compiling, linking, and link to work. Downloading to function properly The majority of the remaining information can usually be left unattended.
It is critical to note that the file’s name must be “Makefile” (or “makefile”). On occasion, your Mac or Windows machine may attempt to append an extension to the file, naming it “Makefile.txt.” This will stop the make program from running. Some systems will add the extension, then make it invisible, so that the file appears to be named Makefile from the GUI, but it is not.

If the make command fails with an error message indicating that no Makefile was found, open a Terminal or Command Prompt window and navigate to the project folder. The ls (Mac or Linux) or dir (Windows) commands should list the files with their full names. To rectify the name, use the following command.

rename makefile . txt makefile    <– on Windows

mv Makefile . txt Makefile         <– on Mac or Linux

Open the Makefile in your text editor and look at the five lines at the top. These describe the kind of processor being used, how to program it, and which files must be compiled in order to build the executable program. Regardless of where the Makefile originated, it is critical to ensure that the correct information is at the top.

DEVICE     = atmega 328 p

CLOCK      = 9830400

PROGRAMMER = -c usbtiny -P usb

OBJECTS    = myprogram . o myfunctions . o

FUSES      = -U hfuse : w:0 xd9 : m -U lfuse : w:0 xe0 : m

The DEVICE line contains the name of the microcontroller and must be correct for the program to compile correctly. For the ATmega328P this should be “atmega328p”. The CLOCK line indicates the frequency of the clock in Hertz. This must be correct in order for any calls to delay functions to function properly, as well as for communication between your computer and the microcontroller.

The parameters in the PROGRAMMER line are used by the program that downloads data to the microcontroller. This information is the same on all systems (Mac, Windows, or Linux), but it varies depending on the type of programmer used. This should be “-c usbtiny -P usb” for the USBtiny programmers in the black plastic boxes with clear tops. “-c avrispmkii -P usb” should be used for blue AVRISP MkII programmers.

The OBJECTS line contains a list of all object modules that must be linked together. If the program was divided into multiple source files, this line will have more entries. The FUSE line specifies the parameters for programming the high and low fuse bytes (see Sec. 6.16). These regulate various aspects of the microcontroller, such as the clock source type. The above settings should be used to program individual 328Ps on protoboards.
Make any necessary changes to the Makefile in a text editor, and ensure that the OS does not append a “.txt” or another extension to it when you save it.

Writing Code

Because a microcontroller lacks an operating system, writing C code to run on it necessitates managing some of the system’s low-level details. You are writing not only the application that will run on the system but also the necessary portions of the operating system code. The low-level details you must manage include reading and writing internal registers, writing assembly routines, registering interrupt service routines, dealing with memory addressing issues, and some basic optimization issues.

Program Initialization

The Avr-GCC software includes start-up code in the project to do things like implement global variables and set up the processor stack. The programmable fuse settings determine the rest of the initialization tasks. The programmer can simply begin their program, assuming that all housekeeping tasks related to the microcontroller have been completed.

Program Termination

In contrast to writing the code on a larger computer, the microcontroller lacks an operating system that can take control once the program is completed and exits. This means that the program should never terminate. It should always be doing something, such as looping indefinitely.

while (1) {

}

Declaring Variables

In most cases, memory can be allocated just like any other C program. However, because microcontrollers have much less memory than general-purpose computers, it is critical to make efficient use of them. Fixed-point variables, rather than floating-point variables, should be used whenever possible because they take up less memory. Because the 328P processor lacks internal floating point hardware, using floating point can slow down execution and increase program size.

The microcontroller’s native size for operating on data is 8 bits. Variables should be declared as 8-bit values whenever possible to reduce the amount of RAM memory used as well as the amount of code generated to operate on the variables. Variables (and functions) should be declared as “unsigned char” for unsigned 0 to 255 values, or “signed char” for -128 to +127 values unless the values being stored in a variable are known to exceed the limits of 8-bits.

Using the smallest variable type required is also important for improving program execution times. Because the processor can only operate on 8-bit (single byte) quantities, adding two or four bytes requires multiple instructions. Using a variable of two or four bytes for something like a loop index can cause the loop to run much slower than expected due to the amount of work that has to be done each time the index is incremented.

 

Memory Map of 328P RAM

Figure 1: Memory Map of 328P RAM

Table 1 summarizes the sizes and ranges of the fixed-point variables.

Traditional name Portable name # Bytes Min Max
signed char int8_t 1 -128 +127
unsigned char uint8_t 1 0 255
signed int int16_t 2 -32768 32767
unsigned int uint16_t 2 0 65535
signed long int32_t 4 -2147483648 2147483647
unsigned long uint32_t 4 0 4294967295

Table 1: Fixed-Point Variable Ranges

Using RAM Memory

The “stack” is a section of memory that the processor and programs use to store temporary data. The amount of memory available for the stack is limited, and one of the more common reasons that programs do not execute as expected on the 328P is the misuse of the stack space. As shown in Fig. 1, the stack begins at 0x08FF and grows downward towards the area where global variables are stored as data and addresses are pushed onto it. If too many variables are pushed onto the stack, the stack will most likely grow too far downward and eventually overwrite the contents of the global variables. It should be noted that there is no mechanism in the 328P to prevent too much data from being placed on the stack. Stack data can overwrite global variables, and writing to global variables can overwrite stack data, which can cause major issues for the program.

When writing code, the programmer should be aware of how much stack space is being consumed. Several things happen when a function is called. First, all of the function’s arguments are pushed onto the stack. If a function takes four 16-bit “int” arguments, eight bytes are added to the stack. The processor then adds two bytes to the stack for the return address. Finally, stack space is allocated for all local variables declared in the function. When these actions are combined, a significant amount of stack space is consumed each time a function is called. If one function calls another, which calls another, and so on, the amount of stack space used can quickly exceed what is available.
The following guidelines can help you avoid stack problems when writing code.

Avoid allocating large amounts of local variables. For example, if your functions require an 80-byte temporary buffer to perform a task, do not declare an array of 80 unsigned bytes in the function because these will all be allocated on the stack. Instead, consider using an 80-byte global variable array that can be shared with other functions that are not called at the same time. Even though good programming practice suggests not using global variables, using global variables is often the best way to avoid stack problems.

Avoid passing a data structure by value to a function. If the “struct” is passed by value, the calling routine places all of the structure’s values on the stack before proceeding to the function’s start. A better approach would be to pass the structure by reference, rather than the values.

Do not write programs that use recursion. Recursion occurs when a function calls itself, and this practice can result in excessive stack memory usage. If you believe your program requires a recursive operation, look for another way to implement it.

Using Program Memory (ROM)

Constant values, such as arrays of character strings that will not be changed, can be stored in RAM or ROM. Because RAM is so limited, it’s usually a good idea to put these in ROM. This is not as simple as you might think due to the architecture of Microchip AVR microcontrollers. To use the data stored in program memory, it must be declared in a specific way and accessed differently within the program.

The sample program AT328-5.c on the class website shows how to store strings in program memory and then access them within the program. For more information, visit the EE459 reference library website, which includes a link to a tutorial on how to use the program memory.

Using EEPROM Memory

The ATmega328P has 1024 bytes of EEPROM (Electrically Erasable Programmable Read-Only Memory) available for use by programs. EEPROM memory is similar to ROM memory in that the data in it is retained (non-volatile) when the power to the chip is turned off, but the contents can be modified by the program in the same way that RAM is used. To use the EEPROM, a program must include the following preprocessor directive at the beginning of the code.

# include <avr / eeprom .h>

This declares a number of routines that can be called to read or write individual byte, word, long, or float values, as well as additional routines that can be called to read or write data blocks. For more information, visit the EE459 reference library website, which includes a link to a tutorial on how to use the EEPROM memory.
Unless one of the fuse bits is set to retain the contents, the EEPROM contents are erased each time the ATmega328P is reprogrammed. The “EESAVE” bit must be set to zero in order for the data to be retained across programming operations. See Section 6.16 for more information.

Determining Memory Usage

Too much RAM memory is frequently the source of problems with microcontroller software. Examine the output produced each time the program is compiled and linked to get an idea of how much RAM and ROM your program is using. The “AVR-size” command is automatically executed to print memory usage statistics. For example, from the class website, here is the output for the AT328-4 sample program.

% make

avr – gcc – Wall – Os – DF_CPU =9830400 – mmcu = atmega 328 p -c at328 -4. c … avr – gcc – Wall – Os – DF_CPU =9830400 – mmcu = atmega 328 p -o main . elf … rm -f main . hex

avr – objcopy -j . text -j . data -O ihex main . elf main . hex avr – size — format = avr — mcu = atmega 328 p main . elf

AVR Memory Usage

—————-

Device : atmega 328 p

Program :      530 bytes (3.2% Full ) (. text + . data + . bootloader )

Data :           50 bytes (4.9% Full ) (. data + . bss + . noinit )

In this example, 530 bytes of program memory (ROM) are being used, and 50 bytes of RAM. Note that the RAM usage does not include the space that the stack will be used during program execution.

The Linker “.map” File

When your program compiles and is linked successfully, the linker can generate a map file that shows where all of the procedures and variables have been allocated in ROM and RAM. This file’s contents can be very useful in determining whether or not there are potential memory allocation and usage issues. Locate the line in the Makefile that defines the command that will be used to compile to create an a.map file for the program.

COMPILE = avr – gcc – Wall – Os – DF_CPU =$( CLOCK ) – mmcu =$( DEVICE )

and at the end of this line add the following options

-Wl ,- Map = myprog . map -Wl ,– cref

where “myprog.map” can be replaced with any filename you want for the map file. The “-Wl,…” option instructs the compiler to pass the following option to the linker during the build phase of the executable program.
The majority of the .map file is useless gibberish, but there is some useful information in there. Look for a line that begins with “.text” and does not have any spaces before it.

. text           0 x 00000000      0 x1a0

The executable code for a program is referred to as “text” in the linker’s language, and this line specifies where in the program memory the program begins (0x00000000) and how large the text segment is (0x1a0 = 416 bytes). Other entries show the location of functions in the program or ones that were added by the linker to make it all work. The end of the text segment is indicated by a line with “ etext” on it and in this example, it says the end of the text is at address 0x1a0.

0 x 000001 a 0                   _etext = .

The next part of the map file is the “data” segment which is initialized global variables. Like the text segment part it also contains a lot of mostly useless information but some of it is shown below.

 

. data 0 x 00800100

.

0 x10 load address 0 x 000001 a 0
.
.
. data 0 x 00800100 0 x10 at328 -3. o
0 x 00800100 digit_segs
.
.
.
0 x 00800110 _edata = .

 

This says there are 0x10 (16) global variables and that these are located starting at address 0x0100 in RAM (ignore the 0080 at the start of the address) and the data segment ends at address 0x0110.

The address shown for “edata” is most likely the most important thing in the map file. This indicates how far away in memory the global variables extend. As mentioned in Section 6.5, stack memory grows downward toward global variables, so it’s important to keep track of how much space is being used.

Reading and Writing Internal Registers

All of the microcontroller’s internal registers are defined by variables and can be read and written in the same way as any other C variable. When the line is created, the names of the registers and individual register bits are defined.

# include <avr / io.h>

The ATmega328P manual lists the register names and individual bit values.

The input and output registers for ports B, C, and D, for example, are defined by variables PINB, PINC, and PIND, and PORTB, PORTC, and PORTD, respectively. You can operate on these variables in the same way that you would any other.

PORTB = 0 xf4 ;

PORTD = 5 * PINB + 2;

unsigned char my_var = PINC ; PORTC += 1;

Individual register bits can also be accessed, but they cannot be read or written in the same way that full registers can. To convert ones and zeros into bits, use the “|” (OR) and “&” (AND) operators. For example, to set bit 3 in PORTB to one and bit 5 in PORTD to zero, do the following.

PORTB |= (1 << 3);

PORTD &= ~(1 << 5);

The bits in the device registers all have declared names in the “ AVR/io.h” file and these should be used for setting and clearing bits.

TCCR1B |= (1 << WGM12 ); TIMSK1 |= (1 << OCIE1A );

Multiple bits can be set (or cleared) with one statement in the same manner.

TCCR1B |= ((1 << CS12 ) | (1 << CS11 ));

When writing individual bits in the registers, keep in mind that the microcontroller will first read the entire eight-bit byte, modify it, and then write the entire byte back. Because reading a register affects some of the microcontroller’s internal functions, it is critical to be aware of any potential side effects when accessing the registers.

Floating Point

The AVR-GCC compiler is capable of generating code that performs floating-point calculations. This enables programs to be written in a style that is very similar to that of a larger computer. However, because the ATmega328P processor lacks floating-point arithmetic hardware, all floating-point calculations must be implemented using multiple 8-bit integer arithmetic operations. The resulting code may be relatively slow to execute, and the program’s size will be increased due to the need to include routines that perform floating-point operations in the final binary program. Even simple floating point operations (multiply, divide, compare, and so on) can cause a program to grow by about 1900 bytes. This can be a problem if the program is close to exhausting the processor’s ROM space. Unless there are compelling reasons to do so, programs should not use floating points. Integer arithmetic should be used instead whenever possible.

Floating point variables can be declared as “float” or “double,” and both will generate the same code. The compiler can only handle 32-bit floating-point numbers (4 bytes for each variable). Because some library routines are defined with double arguments, it is often easier to declare all floating point variables in a program as double to avoid having to do casts or receiving warning messages when calling library functions.
In some cases, the project’s “Makefile” must be modified to include different libraries that support floating point routines. For example, if floating point values are to be formatted using the “sprintf” routine, the following lines must be added to the Makefile’s COMPILE definition.

-Wl ,-u, vfprintf – lprintf_flt

This instructs the compiler to remove the default library code for vfprintf and replace it with one from the printf flt library. To use a floating point version of sscanf, follow the same steps. As described in the following section, adding these libraries will also increase the size of the program.

Standard C Library

Avr-GCC includes a standard C library with many of the routines that programmers are used to having at their disposal for use in their programs. Routines such as “strlen” and “strcmp” can be used in the same way that they are on larger systems. There are also routines that can operate on floating point numbers, such as “sqrt,” but users should review the information in Sec. 6.11 before using them. Routines that perform I/O operations are ineffective because the system lacks a file system or other I/O capability.

When the program is compiled and linked, the AVR-GCC linker will typically include any required routines from the standard C library in the binary output. Programmers must keep in mind that these can significantly increase the size of their final executable program.
“sprintf” is a common library function for creating formatted strings of data. If this function is only used to format integer values and strings, the following line can be added to the Makefile’s COMPILE definition to reduce program size.

-Wl ,-u, vfprintf – lprintf_min

This instructs the compiler to remove the default library code for vfprintf and replace it with one from the printf min library that only supports integer and string formatting. This will save approximately 400 bytes of program space. If floating point values are to be formatted, see Section 6.11 for changes to the Makefile that must be made for this to work properly.

Including Assembly Language Code in C

There are two ways to write assembly language portions of your program. The first method is to use “inline assembly,” which involves inserting lines of assembly code directly into the C program file. While some compilers make this method relatively simple, the AVR-GCC compiler has a rather complicated syntax that makes it difficult to use. The inline assembler is not recommended for use in class projects.

The other method is to separate the assembly code into “.S” files and assemble these into object files separate from the rest of the program. These object files can be combined with those generated by the compiler from C code files to form the final executable. In comparison to inline assembly, this is a relatively simple process. The C code in the “.c” file looks like normal C code, and the assembly code looks like a portion of a normal assembly language program. It does have the limitation that the assembly code must be a complete function and cannot be just a few lines in the middle of some C code.

The following is a summary of what has to be done with any registers that a function uses.

r0 – Can be used by the assembler function. Does not have to be restored on return.

r1 – Assumed to be zero. If the function uses it, it must clear it to zero before returning.

r2-r17, r28-r29 – Must be saved and later restored if used in the function.

r18-r27, r30-r31 – Can be used freely. The function doesn’t have to save and restore them.

Using Interrupts

If your program uses interrupts, whether external from the INT inputs or internal from the timer, ADC, and so on, you must include the following at the start of the program.

# include <avr / interrupt .h>

In the program, global interrupts are enabled with the statement

sei ();

and disabled with

cli ();

Setting the global interrupt enable to enable interrupts for any of the modules is insufficient. Each module has its own interrupt enable bit in one of the module’s registers, which must be set to enable the module to generate an interrupt. Any interrupt that is enabled must be managed by a corresponding interrupt service routine (ISR). ISRs are written in the same way as any other C function, except that instead of a function name and arguments, the ISRs contain the name of the interrupt vector. An ISR for the “Timer1 Compare Match A” interrupt, for example, would look like this.

ISR ( TIMER 1 _COMPA_vect )

{

// ISR code

}

The names of the interrupt vectors that should be used when writing interrupt service routines in C are listed in Table 2. Please keep in mind that some of the C language vector names below differ slightly from the names of the vectors used in assembly language programs. If in doubt, look at the contents of the interrupts.h file, which defines the functions.

Volatile Variables

Global variables’ values can be changed and/or checked from anywhere in the program, including interrupt service routines. This means that a variable’s value can change during the execution of one part of the program even if no assignment statements affect it at that point.

Interrupt definition Vector name
External Interrupt Request 0

External Interrupt Request 1 Pin Change Interrupt Request 0 Pin Change Interrupt Request 1 Pin Change Interrupt Request 2 Watchdog Time-out Interrupt

Timer/Counter2 Compare Match A Timer/Counter2 Compare Match B Timer/Counter2 Overflow Timer/Counter1 Capture Event Timer/Counter1 Compare Match A Timer/Counter1 Compare Match B Timer/Counter1 Overflow Timer/Counter0 Compare Match A Timer/Counter0 Compare Match B Timer/Counter0 Overflow

SPI Serial Transfer Complete USART Rx Complete USART Data Register Empty USART Tx Complete

ADC Conversion Complete EEPROM Ready

Analog Comparator

Two-wire Serial Interface Store Program Memory Read

INT0_vect

INT1_vect PCINT0_vect PCINT1_vect PCINT2_vect WDT_vect TIMER2_COMPA_vect TIMER2_COMPB_vect TIMER2_OVF_vect TIMER1_CAPT_vect TIMER1_COMPA_vect TIMER1_COMPB_vect TIMER1_OVF_vect TIMER0_COMPA_vect TIMER0_COMPB_vect TIMER0_OVF_vect SPI_STC_vect USART_RX_vect USART_UDRE_vect USART_TX_vect ADC_vect EE_READY_vect ANALOG_COMP_vect TWI_vect SPM_READY_vect

Table 2: Interrupt Vector Names

If an interrupt occurs, the variable can be changed, and when program execution resumes, it will have a different value than before. This can be a problem if the compiler removes what it considers to be unnecessary variable references in order to optimize the code. In the following code, for example, the function “check()” performs a loop while waiting for the variable “flag” to become non-zero. The variable is not altered in check(), but it is altered in the ISR. The function then does something after an interrupt occurs and the ISR sets the variable.

uint8_t flag ;

 

check ()

{

while ( flag == 0);

 

\\ Do something

}

 

ISR ( SOME_INTERRUPT_vect )

{

flag = 1;

}

The problem occurs because the compiler will see that the value of “flag” is never changed in the function so it will simplify the code by replacing it with something like this.

check ()

{

if ( flag == 0) while (1);

else {

\\ Do something

}

}

The prior code only checks the value of flag once and then enters an infinite loop if it is zero. The compiler sees no reason to check the value of the flag each time through the loop because it isn’t changing. To avoid this, all global variables that can be modified outside of the normal flow of the program, such as in an ISR, must be declared with the “volatile” keyword.

volatile uint8_t flag ;

The volatile keyword informs the compiler that the value of the variable can change and that it should never assume that the value it had the last time it was used will be the same the next time it is used. The compiler will generate the correct code for this situation if the flag variable is declared with the volatile keyword.

Fuse Settings

Some of the ATmega328P’s operation is determined by three bytes that the user can program. These are known as fuse bytes and are distinct from program memory. Most of the time, these can be programmed once when a new ATmega328P is installed on the project board and do not need to be reprogrammed each time the chip’s firmware is changed.

The fuses for the 328P include the bits listed in Table 3. For the majority of the things that can be configured, the default settings are adequate. The only changes that are typically made are a couple of bits in the low fuse byte to force the chip to use an external clock source, such as a TTL oscillator, and to not divide the clock by eight. All of the other settings can be left at their default values. These settings produce a high fuse byte value of 0xd9 and a low fuse byte value of 0xe0. These values are entered on the FUSES line in the Makefile.

Building your Application

Enter the command “make” to compile and link your project. This will compile and link any source files that have changed since the last time the command was run. If the compilation and linking processes are successful, a file called “main.hex” will be created in the project folder, containing the binary data that will be loaded into the microcontroller.

The compiler will print any error or warning messages when it runs to compile your code. Check through these to see if anything needs to be fixed. Don’t assume that because the compilation is finished, everything is fine. In many cases, the compiler will create the main.hex file despite errors in the process. Students frequently waste hours or days debugging a program because they failed to read a message generated during the compilation process that tells them where the problem is. Warning messages should not be ignored, even if they are usually for non-fatal code errors. Make every effort to resolve the issues in the code that are causing these messages to be generated. When a program is compiled and generates a large number of warnings, it can be difficult to identify other messages that may be reporting more serious issues.

Programming the ATmega328P

High Fuse Byte
Bit Name Description Default Recommended Value = 0xd9
7 RSTDISBL External Reset Disable 1 1 ⇒ Reset enabled
6 DWEN debugWIRE Enable 1 1 ⇒ debugWIRE disabled
5 SPIEN Enable Serial Program

Downloading

0 0 ⇒ SPI enabled
4 WDTON Watchdog Timer On 1 1 ⇒ Watchdog timer off
3 EESAVE EEPROM memory preserved 1 1 ⇒ Don’t preserve EEPROM
2

1

BOOTSZ1

BOOTSZ0

Select Boot Size 0

0

0 ⇒ 2048 words

0

0 BOOTRST Select Reset Vector 1 1
Low Fuse Byte
Bit Name Description Default Recommended Value = 0xe0
7 CKDIV8 Divide Clock by 8 0 1 ⇒ Don’t divide by 8
6 CKOUT Clock output 1 1 ⇒ Don’t ouput clock on PB0
5

4

SUT1

SUT0

Select start-up time 1

0

1 ⇒ 14 clocks + 65ms

0

3

2

CKSEL3

CKSEL2

Select clock source 0

0

0 ⇒ External clock

0

1 CKSEL1 1 0
0 CKSEL0 0 0
Extended Fuse Byte
Bit Name Description Default Recommended Value = 0xff
7 1 1
6 1 1
5 1 1
4 1 1
3 1 1
2

1

BODLEVEL2

BODLEVEL1

Brown-Out Detector Level 1

1

1 ⇒ BOD disabled

1

0 BODLEVEL0 1 1

Table 3: ATmega328P fuse bits

  1. Connect the USB cable from the programmer to one of the computer’s USB ports. Inside the programmer, a red LED should light up.
  2. Connect the programmer to the project board’s 2 3-pin headers. Because the USBtiny programmers supply +5 power to the board being programmed, you may not need to connect the board’s power wires to the power supply for the board to function.
  3. Type “make flash” into the command line. The avrdude software will now be executed and the data will be downloaded into the 328P program memory.
  4. Enter the command “make fuse” for new chips or if the fuse setting has changed. The fuse data will be programmed into the 328P as a result of this. If you previously performed the “make fuse” operation on this microcontroller chip and are not attempting to change the fuse settings, you can skip this step.

At this point, the microcontroller will be reset and the new program will begin to run. While the operation of the board is being observed and changes to the program source files are made, the programmer can be left connected to the computer and the project board. After the modified program has been compiled and linked and is ready for download, only step 3 (make flash) remains.

Important: If the programmer is connected to your project board, it must also be connected to the computer for the program to run properly. If the programmer is not connected to the computer and thus is not powered on, the program in the microcontroller may not run.

Programming Problems

Several things can go wrong and prevent the microcontroller from being programmed. If the make flash command returns an error message

avrdude : Initialization failed , rc =-1

This indicates that the programmer is unable to communicate with the microcontroller. This could be for any number of reasons.

  • The microcontroller is not plugged securely into the
  • Clock signal not getting to the microcontroller (check with )
  • +5 volt power not present at the microcontroller (check with an oscilloscope.)
  • Wiring errors between the 6-pin header and the microcontroller
  • Microcontroller has become

If the micro was programming fine and then this error started appearing, one of the first three listed above is probably the cause.

Sample Programs

The files listed below are available on the EE 459 class website and may be of use to students writing C programmes for the ATmega328P.

at328-0.cA very simple programme for demonstrating the micro’s functionality. It loops indefinitely, rapidly turning bit zero in port C (PC0) on and off.

at328-1.c This program reads a switch input and turns an LED on and off.

at328-2.c This program counts up and down on a seven-segment display. The program uses a library function to implement the counter delay. See Appendix A for a listing.

at328-3.c Similar to at328-2.c but uses an internal timer and interrupts to implement the delay. See Appendix B for a listing.

at328-4.c An 8-bit interface is used to interface to an LCD display. Displays a brief message on the screen.

at328-5.c Similar to at328-4.c but uses a 4-bit interface to the LCD and stores the strings in the ROM memory instead of RAM.

at328-6.cThis program shows how to control an LCD display using an RS-232 serial interface.

at328-7.cAn IIC (I2C) bus is used to read and write data to an EEPROM.

Appendix A: Sample program at328-2.c

/*************************************************************

at328 -2. c - Demonstrate simple I/ O functions of ATmega 328 P

*

This program will cause a 7 - segment display to either count up in
hexadecimal (0 ,1 ,2 ,... , E,F ,0 ,1 ,...) or count down in decimal
(9 ,8 ,... ,1 ,0 ,9 ,8 ,..) depending on whether or not a switch is
pressed .

*

Port C, bit 1 - input from switch (0 = pressed , 1 = not pressed )
When the switch is not pressed , the 7 - segment display
counts up in hexadecimal . When the switch is pressed ,
the 7 - segment display counts down in decimal .
Port B, bits 0 -1 and Port D, bits 2 -6 - Outputs to data inputs of
the 74 LS374 register .

*                 Bit 6 -> segment A, 5 - >B, ... , 1 - >F, 0 - >G

A low output bit will cause the LED segment to light
Port C, bit 2 - Output to positive edge - triggered clock input
of 74 LS374 register .

*

*************************************************************/

# include  # include 

unsigned  char  digit_segs [16]  =  { 0 x7e ,0 x30 ,0 x6d ,0 x79 ,0 x33 ,0 x5b ,0 x5f ,0 x70 , 0 x7f ,0 x73 ,0 x77 ,0 x1f ,0 x4e ,0 x3d ,0 x4f ,0 x47 };

void display_digit ( unsigned char ); # define SWITCH                 (1 << PC1 )

# define SEG_DATA_B   0 x03   // Bits in Port B for LED display # define SEG_DATA_D   0 xfc   // Bits in Port D for LED display # define SEG_CLOCK (1 << PC2 ) int main ( void ) { unsigned char up; unsigned char cnt = 0; PORTC |= SWITCH ;              // Turn on pull - up on PC1 for switch DDRC |= SEG_CLOCK ;                     // Set PORTC bit 2 for output DDRB |= SEG_DATA_B ;           // Set PORTB bits 0 -1 for output DDRD |= SEG_DATA_D ;                    // Set PORTD bits 2 -6 for output while (1) {                     // Loop forever display_digit ( cnt ); _delay_ms (500);         // wait 500 ms up = PINC & SWITCH ;      // read the button if ( up) {                 // if button is not pressed , up = 1 if (++ cnt > 15)     // and we count up in hex cnt = 0;

}

else {                              // if button is pressed , up = 0 if (-- cnt < 0 || cnt > 9)           // and we count down in decimal

cnt = 9;

}

}

return 0;    /* never reached */

}

void display_digit ( unsigned char digit )

{

unsigned char x;

x = digit_segs [ digit ] ^ 0 xff ;   // invert bits ( active low outputs ) PORTB |= x & SEG_DATA_B ;               // put low two bits in B

PORTB &= ( x | ~ SEG_DATA_B );

PORTD |= x & SEG_DATA_D ;     // put high five bits in D PORTD &= ( x | ~ SEG_DATA_D );

PORTC |= SEG_CLOCK ;           // toggle the clock bit to 1 PORTC &= ~ SEG_CLOCK ;                    // toggle the clock bit to 0

}

Appendix B:   Sample program at328-3.c

/*************************************************************
*	at328 -3. c - Demonstrate simple I/ O functions of ATmega 328 P
*	and the use of an internal timer and interrupt
*
*	This program will cause a 7 - segment display to either count up in
*	hexadecimal (0 ,1 ,2 ,... , E,F ,0 ,1 ,...) or count down in decimal
*	(9 ,8 ,... ,1 ,0 ,9 ,8 ,..) depending on whether or not a switch is
*	pressed .
*
*	Port C, bit 1 - input from switch (0 = pressed , 1 = not pressed )
*	When the switch is not pressed , the 7 - segment display
*	counts up in hexadecimal . When the switch is pressed ,
*	the 7 - segment display counts down in decimal .
*	Port B, bits 0 -1 and Port D, bits 2 -6 - Outputs to data inputs of
*	the 74 LS374 register .
*	Bit 6 -> segment A, 5 - >B, ... , 1 - >F, 0 - >G
*	A low output bit will cause the LED segment to light up.
*	Port C, bit 2 - Output to positive edge - triggered clock input
*	of 74 LS374 register .
*
*************************************************************/

# include  # include 

uint8_t   digit_segs   [16]   =    { 0 x7e ,0 x30 ,0 x6d ,0 x79 ,0 x33 ,0 x5b ,0 x5f ,0 x70 , 0 x7f ,0 x73 ,0 x77 ,0 x1f ,0 x4e ,0 x3d ,0 x4f ,0 x47 };
uint8_t cnt ;
void  display_digit ( uint8_t ); # define SWITCH	(1 << PC1 )
# define SEG_DATA_B   0 x03   // Bits in Port B for LED display # define SEG_DATA_D   0 xfc   // Bits in Port D for LED display # define SEG_CLOCK (1 << PC2 )

int main ( void ) {
PORTC |= SWITCH ;	// Turn on pull - up on PC1 for switch DDRC |= SEG_CLOCK ;	// Set PORTC bit 2 for output
DDRB |= SEG_DATA_B ;	// Set PORTB bits 0 -1 for output DDRD |= SEG_DATA_D ;	// Set PORTD bits 2 -6 for output

/*
The demo board has a 9.8304 MHz clock .	We want the timer to interrupt every half second (2 Hz) so we need to count clocks to 9.8304 MHz /2 Hz = 4 ,915 ,200.	This is too big for the 16 bit counter register so use the prescaler to divide the clock by 256 and then count that clock to 19 ,200.
*/
 

// Reset clears register bits to zero so only set the 1 ’ s
TCCR1B |= (1 << WGM12 );	// Set for CTC mode .	OCR1A = modulus TIMSK1 |= (1 << OCIE1A );	// Enable CTC interrupt
sei ();	// Enable global interrupts
OCR1A = 19200;	// Set the counter modulus
TCCR1B |= (1 << CS12 ); // Set prescaler for divide by 256 , // also starts timer display_digit ( cnt ); while (1) { // Loop forever while interrupts occur } return 0; /* never reached */ } void display_digit ( uint8_t digit ) { uint8_t x; x = digit_segs [ digit ] ^ 0 xff ; // invert bits ( active low outputs ) PORTB |= x & SEG_DATA_B ; // put low two bits in B PORTB &= ( x | ~ SEG_DATA_B ); PORTD |= x & SEG_DATA_D ; // put high five bits in D PORTD &= ( x | ~ SEG_DATA_D ); PORTC |= SEG_CLOCK ; // toggle the clock bit to 1 PORTC &= ~ SEG_CLOCK ; // toggle the clock bit to 0 } ISR ( TIMER 1 _COMPA_vect ) { uint8_t up; up = PINC & SWITCH ; // read the button if ( up) { // if button is not pressed , up = 1 if (++ cnt > 15)	// and we count up in hex
cnt = 0;
}
else {	// if button is pressed , up = 0
if (-- cnt < 0 || cnt > 9)	// and we count down in decimal cnt = 9;
}

display_digit ( cnt );
}


About The Author

Muhammad Bilal

I am a highly skilled and motivated individual with a Master's degree in Computer Science. I have extensive experience in technical writing and a deep understanding of SEO practices.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top