AVR Architecture Explained: ATmega328P, ATmega16 and ATmega32 Compared
The AVR family from Atmel (now part of Microchip) is one of the most widely taught 8-bit microcontroller families in Indian engineering colleges. The ATmega16 and ATmega32 appear in many lab manuals and project kits, while the ATmega328P is the chip on the Arduino Uno and Nano. All of them share the same AVR CPU core, so once you understand one, moving to another is mostly a matter of checking pin numbers and register names.
This tutorial explains how the AVR core is organised, how its memories are laid out, how peripherals are controlled through I/O registers, and how the three popular chips compare. The rest of this series uses plain C with avr-gcc and avr-libc, without the Arduino library, so knowing what sits behind each register name will make every later program easier to read.
The AVR CPU Core
AVR is a modified Harvard architecture: program memory (flash) and data memory (SRAM) have separate buses. While one instruction executes, the next one is fetched from flash. This simple two-stage pipeline lets most instructions complete in a single clock cycle, so an ATmega328P running at 16 MHz executes close to 16 million instructions per second.
At the heart of the core is a register file of 32 general-purpose 8-bit registers, named R0 to R31. The ALU can take two of these registers as operands and write the result back in one cycle. The last six registers pair up into three 16-bit pointer registers, X (R27:R26), Y (R29:R28) and Z (R31:R30), used for indirect addressing of data memory. The Z pointer can also read constant tables from flash with the LPM instruction, which is how pgm_read_byte() from avr-libc works.
Status register and stack
The SREG status register holds the flags set by the ALU: carry (C), zero (Z), negative (N), overflow (V), sign (S), half carry (H), the T bit, and the global interrupt enable bit I. The sei() and cli() macros in C simply set and clear this I bit. The stack lives in SRAM and grows downwards from the top of RAM; the stack pointer SPH:SPL is initialised by the C startup code, so you rarely touch it yourself.
Memory Organisation
Every AVR has three separate memories. Flash holds your program and constant data and is organised in 16-bit words because every instruction is 16 or 32 bits wide. SRAM holds variables and the stack. EEPROM is a small non-volatile area for settings that must survive a power cut.
Data memory is a single linear address space. The first 32 addresses map to R0 to R31. Next come the 64 standard I/O registers, and on the ATmega328P a further block of extended I/O registers. Internal SRAM starts after that. This is why a register such as PORTB can be accessed both as a memory location and with the fast IN/OUT instructions.
ATmega328P data memory ATmega16 / ATmega32 data memory 0x0000-0x001F R0..R31 0x0000-0x001F R0..R31 0x0020-0x005F 64 I/O regs 0x0020-0x005F 64 I/O regs 0x0060-0x00FF 160 ext I/O 0x0060-... internal SRAM 0x0100-0x08FF 2 KB SRAM
How Peripherals Are Controlled
Each peripheral is controlled by a handful of 8-bit registers, and each bit inside a register has a name. For example, the ADC is configured with ADMUX and ADCSRA, and the bit that starts a conversion is called ADSC. The header <avr/io.h> defines all register and bit names for the chip selected with the -mmcu compiler option, so the same source code style works across devices.
Bit names are bit positions, not masks. You therefore write ADCSRA |= (1 << ADSC); to set a bit and ADCSRA &= ~(1 << ADSC); to clear it. avr-libc also offers the _BV(bit) macro, which expands to (1 << bit).
#include <avr/io.h>
int main(void)
{
/* Make PB5 an output (on an Uno this is the onboard LED) */
DDRB |= (1 << DDB5);
/* Set PB5 high, leaving the other PORTB bits untouched */
PORTB |= (1 << PORTB5);
/* Read the whole of port D at once */
uint8_t inputs = PIND;
/* Test a single bit: is PD2 high? */
if (inputs & (1 << PIND2)) {
PORTB &= ~(1 << PORTB5); /* clear PB5 */
}
while (1) {
/* main loop */
}
}
ATmega328P vs ATmega16 vs ATmega32
The table below compares the three chips you are most likely to meet. The figures are for the standard DIP packages.
| Feature | ATmega328P | ATmega16 | ATmega32 |
|---|---|---|---|
| Package (DIP) | 28 pins | 40 pins | 40 pins |
| Flash | 32 KB | 16 KB | 32 KB |
| SRAM | 2 KB | 1 KB | 2 KB |
| EEPROM | 1 KB | 512 bytes | 1 KB |
| I/O lines | 23 (ports B, C, D) | 32 (ports A to D) | 32 (ports A to D) |
| ADC | 10-bit, 6 channels on DIP | 10-bit, 8 channels | 10-bit, 8 channels |
| Timers | Timer0 8-bit, Timer1 16-bit, Timer2 8-bit | Same three timers | Same three timers |
| Serial | USART0, SPI, TWI (I2C) | USART, SPI, TWI | USART, SPI, TWI |
| Maximum clock | 20 MHz | 16 MHz | 16 MHz |
| On-chip debug | debugWIRE | JTAG | JTAG |
The ATmega16 and ATmega32 are pin compatible, so a board designed for one accepts the other. The ATmega328P is a newer design with more flexible timers (two compare outputs on every timer), pin change interrupts on every I/O pin, and a picoPower process that lowers sleep current.
Register naming differences
Code is not always portable between the older and newer chips because some registers were split or renamed. On the ATmega16/32 Timer0 has one control register, TCCR0, and one compare register, OCR0. On the ATmega328P it has TCCR0A, TCCR0B, OCR0A and OCR0B. Similarly, the USART registers on the 328P carry a 0 suffix (UDR0, UBRR0, UCSR0A), while the ATmega16 uses UDR, UBRRL and UCSRA. Always keep the datasheet of your exact chip open when writing register-level code.
Common Beginner Mistakes
- Compiling with the wrong
-mmcuvalue. Register addresses differ between chips, so a program built foratmega16will not behave correctly on anatmega328p. - Forgetting that a new ATmega328P runs from its internal oscillator divided by 8, which gives 1 MHz, not 16 MHz. Delays and baud rates will be wrong until
F_CPUand the fuse settings agree. - Leaving JTAG enabled on an ATmega16/32. The JTAGEN fuse is programmed from the factory, so pins PC2 to PC5 do not work as normal I/O until it is disabled.
- Using
inteverywhere. On AVR anintis 16 bits and every 16-bit operation takes extra instructions; useuint8_tfrom<stdint.h>when values fit in a byte. - Running out of SRAM without noticing. There is no memory protection, so a stack overflow silently corrupts variables.
Summary
The AVR core combines a Harvard bus structure, a two-stage pipeline and 32 working registers to execute most instructions in one clock. Flash, SRAM and EEPROM are separate memories, and every peripheral is controlled through named I/O registers defined in <avr/io.h>. The ATmega328P, ATmega16 and ATmega32 share this core but differ in memory size, pin count and some register names. In the next tutorial you will install avr-gcc and avrdude and program a real chip with a USBasp.