0% found this document useful (0 votes)
3 views

AVR Programming With GNU Tools

The document provides a comprehensive guide on AVR programming using GNU tools, focusing on the ATmega328P and ATmega2560 microcontrollers. It covers AVR architecture, communication protocols (I2C, SPI, UART), C programming concepts for embedded systems, and practical examples for using the GNU toolchain. The workbook aims to equip learners with the necessary skills to confidently develop embedded code and troubleshoot common issues.

Uploaded by

Shanthosh G
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
3 views

AVR Programming With GNU Tools

The document provides a comprehensive guide on AVR programming using GNU tools, focusing on the ATmega328P and ATmega2560 microcontrollers. It covers AVR architecture, communication protocols (I2C, SPI, UART), C programming concepts for embedded systems, and practical examples for using the GNU toolchain. The workbook aims to equip learners with the necessary skills to confidently develop embedded code and troubleshoot common issues.

Uploaded by

Shanthosh G
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 49

AVR Programming with GNU Tools

Become job ready with essential skills


Contents
1. Introduction ........................................................................... 3
Objectives of the Workbook ...................................................... 3
Prerequisites for Learners ........................................................ 4
2. AVR Architecture .................................................................... 5
Programmer’s Model: ............................................................... 5
ABI (Application Binary Interface): ........................................... 5
Code Examples- Using UART with Register Manipulation: ....... 6
3. Protocols (I2C, SPI, UART) ...................................................... 9
I2C (Inter-Integrated Circuit) .................................................... 9
SPI (Serial Peripheral Interface) ................................................ 9
UART (Universal Asynchronous Receiver/Transmitter)............. 9
Troubleshooting Tips ............................................................. 13
4. C Concepts for Embedded Systems ...................................... 15
1. Pointers ............................................................................. 15
2. Enums ............................................................................... 15
3. Structures ......................................................................... 16
4. Pointers to Structures ........................................................ 16
5. Bit Fields ........................................................................... 17
6. const Keyword ................................................................... 17
7. static Keyword ................................................................... 17
8. volatile Keyword ................................................................. 18
9. Memory Layout of C Programs ........................................... 18
10. Ring Buffer Implementation ............................................. 19
11. Real-World Scenario: Choosing Ring Buffer Size .............. 21
12. Additional Practical Tips: ................................................. 21
Summary ............................................................................... 22
5. Interrupts and ISR Basics .................................................... 23
Introduction to Interrupts ...................................................... 23
Types of Interrupts in AVR ..................................................... 23
Configuring Interrupts in AVR ............................................... 23
1
Summary ............................................................................... 28
6. Endianness .......................................................................... 29
What is Endianness? ............................................................. 29
Why is Endianness Important? .............................................. 29
Handling Endianness in Embedded Programming ................. 29
Example: Converting Big-Endian to Little-Endian .................. 30
Practical Examples and Applications ..................................... 30
Common Pitfalls and Best Practices ....................................... 32
Summary ............................................................................... 32
7. GNU Toolchain Tutorials ...................................................... 33
Make Tutorial ........................................................................ 33
Writing a Simple Makefile ...................................................... 33
Explanation of Important Aspects .......................................... 35
Binutils Tutorial .................................................................... 36
C Library and Startup Code Basics ........................................ 37
Detailed Explanation of Startup Code .................................... 39
Summary ............................................................................... 39
8. Linker Script Basics ............................................................. 40
What is a Linker Script? ........................................................ 40
Key Elements of a Linker Script ............................................. 40
Detailed Explanation of Key Elements .................................... 42
Custom Memory Segments .................................................... 43
Creating Linker Scripts for Memory-Constrained Applications 44
Common Pitfalls and Best Practices ....................................... 45
Summary ............................................................................... 45
9. Conclusion and Next Steps ................................................... 46

2
1. Introduction
The AVR microcontrollers are a family of microcontrollers
developed by Atmel, now a part of Microchip Technology. They are
known for their simplicity, efficiency, and wide adoption in various
embedded applications. The ATmega328P and ATmega2560 are
popular choices due to their robust features and versatility.
ATmega328P: This microcontroller is widely used in the Arduino
UNO board. It features 32 KB of flash memory, 2 KB of SRAM, and
1 KB of EEPROM. It has 23 general-purpose I/O lines, 32 general-
purpose working registers, three flexible timer/counters with
compare modes, internal and external interrupts, serial
programmable USART, a byte-oriented two-wire serial interface,
SPI, 6-channel 10-bit A/D converter, programmable watchdog
timer with internal oscillator, and five software-selectable power-
saving modes.

ATmega2560: This microcontroller is used in the Arduino Mega


2560 board. It boasts 256 KB of flash memory, 8 KB of SRAM, and
4 KB of EEPROM. It includes 86 general-purpose I/O lines, 32
general-purpose working registers, four flexible timer/counters
with compare modes, internal and external interrupts, four
USARTs, byte-oriented two-wire serial interface, SPI, 16-channel
10-bit A/D converter, JTAG interface, programmable watchdog
timer with internal oscillator, and six software-selectable power-
saving modes.
Objectives of the Workbook
Confidence with AVR Controllers: Enable learners to confidently
work with AVR microcontrollers, particularly the ATmega328P and
ATmega2560.
GNU Toolchain Proficiency: Teach the usage of the GNU toolchain,
including Make, avr-libc, avr-gcc, and binutils, to develop
embedded code without relying on IDEs.
Embedded Code Development: Provide learners with the skills to
develop embedded code efficiently, using industry-standard
practices.

3
Personalized Training: Encourage learners to reach out for
personalized, chargeable training to further their skills and career
opportunities.
Prerequisites for Learners
Knowledge of C Language: Understanding of the C programming
language is essential as it is the primary language used in
embedded programming.
Understanding of Embedded Concepts: Basic knowledge of
embedded systems, including microcontroller architecture,
peripherals, and interfacing.
Programming Experience: Some prior experience in programming,
even if not extensive, will help in grasping the concepts taught in
this course.

4
2. AVR Architecture
Focus Microcontrollers: ATmega328P and ATmega2560
In this section, we will delve into the architecture of the
ATmega328P and ATmega2560 microcontrollers, highlighting the
key features and components that make them suitable for a wide
range of embedded applications.
Programmer’s Model:
General-Purpose Registers: The AVR architecture is based on a
modified Harvard architecture, which includes 32 general-purpose
working registers. These registers are directly connected to the
Arithmetic Logic Unit (ALU), allowing two independent registers to
be accessed in a single instruction executed in one clock cycle.
Memory Spaces: The AVR microcontrollers have separate memory
spaces for program and data memory. Program memory is typically
Flash, while data memory includes SRAM and EEPROM.
I/O Ports: Multiple I/O ports are available for interfacing with
external devices.
ABI (Application Binary Interface):
The AVR ABI defines how functions receive parameters and
return values. It also dictates the usage of registers for parameter
passing and stack management.
Registers R0-R17: Used for temporary storage and are not
preserved across function calls.
Registers R18-R27: Used for argument passing and return
values.
Registers R28-R29 (Y Pointer): Typically used as a frame pointer.
Registers R30-R31 (Z Pointer): Used for indirect addressing and
are preserved across function calls.

This diagram highlights the general-purpose registers, memory


spaces, and I/O ports in the AVR architecture.

5
Code Examples- Using UART with Register Manipulation:
ATmega328P:
define F_CPU 16000000UL
define BAUD 9600
define MYUBRR F_CPU/16/BAUD-1

void uart_init(unsigned int ubrr) {


// Set baud rate
UBRR0H = (unsigned char)(ubrr>>8);
UBRR0L = (unsigned char)ubrr;
// Enable receiver and transmitter
UCSR0B = (1<<RXEN0) | (1<<TXEN0);
// Set frame format: 8 data bits, 1 stop bit
UCSR0C = (1<<USBS0) | (3<<UCSZ00);

6
}

void uart_transmit(unsigned char data) {


// Wait for empty transmit buffer
while (!(UCSR0A & (1<<UDRE0)));
// Put data into buffer, sends the data
UDR0 = data;
}

unsigned char uart_receive(void) {


// Wait for data to be received
while (!(UCSR0A & (1<<RXC0)));
// Get and return received data from buffer
return UDR0;
}
ATmega2560:
define F_CPU 16000000UL
define BAUD 9600
define MYUBRR F_CPU/16/BAUD-1

void uart_init(unsigned int ubrr) {


// Set baud rate
UBRR0H = (unsigned char)(ubrr>>8);
UBRR0L = (unsigned char)ubrr;
// Enable receiver and transmitter
UCSR0B = (1<<RXEN0) | (1<<TXEN0);
// Set frame format: 8 data bits, 1 stop bit
UCSR0C = (1<<USBS0) | (3<<UCSZ00);
7
}

void uart_transmit(unsigned char data) {


// Wait for empty transmit buffer
while (!(UCSR0A & (1<<UDRE0)));
// Put data into buffer, sends the data
UDR0 = data;
}

unsigned char uart_receive(void) {


// Wait for data to be received
while (!(UCSR0A & (1<<RXC0)));
// Get and return received data from buffer
return UDR0;
}

8
3. Protocols (I2C, SPI, UART)
In this section, we will cover a brief theoretical overview of the I2C,
SPI, and UART protocols. These protocols are essential for
communication between microcontrollers and peripheral devices.
I2C (Inter-Integrated Circuit)
I2C is a synchronous, multi-master, multi-slave, packet-switched,
single-ended, serial communication bus. It is widely used for
connecting low-speed peripherals to microcontrollers.
Uses two bidirectional open-drain lines, Serial Data Line (SDA) and
Serial Clock Line (SCL), pulled up with resistors. Supports multiple
devices on the same bus.
Typical Use Cases: Sensors, EEPROMs, RTCs.
Reference: https://www.i2c-bus.org/i2c-primer/

SPI (Serial Peripheral Interface)


SPI is a synchronous serial communication interface used for
short-distance communication, primarily in embedded systems. It
is used to communicate with one or more peripheral devices.
Uses four wires Master Out Slave In (MOSI), Master In Slave Out
(MISO), Serial Clock (SCK), and Slave Select (SS). Full-duplex
(SCK), and Slave Select (SS). Full-duplex communication and
higher data transfer rates compared to I2C.
Typical Use Cases: Sensors, SD cards, LCD displays.
Reference: https://www.circuitbasics.com/basics-of-the-spi-
communication-protocol/

UART (Universal Asynchronous Receiver/Transmitter)


UART is an asynchronous serial communication protocol primarily
used for full-duplex communication between devices. Unlike I2C
and SPI, UART does not use a clock signal.
Uses two wires TX (transmit) and RX (receive). Supports different
baud rates, parity bits, and stop bits for communication
configuration.

9
Serial communication between microcontrollers and computers,
GPS modules, Bluetooth modules.
Reference:
https://www.digi.com/resources/documentation/digidocs/9000
1456-13/concepts/c_serial_uart.htm

Usage and Application


This section will focus on how to use and apply these protocols
with register manipulation for ATmega328P and ATmega2560
microcontrollers.
I2C (ATmega328P and ATmega2560):
Register Manipulation: Setting up the TWI (Two-Wire Interface)
registers.
Code Example:
// I2C initialization for ATmega328P and ATmega2560
void i2c_init(void) {
TWSR = 0x00; // Set prescaler to zero
TWBR = ((F_CPU / I2C_FREQ) 16) / 2; // Set bit
rate register
TWCR = (1 << TWEN); // Enable TWI
}

void i2c_start(void) {
TWCR = (1 << TWSTA) | (1 << TWEN) | (1 <<
TWINT); // Send start condition
while (!(TWCR & (1 << TWINT))); // Wait for
TWINT flag set
}

void i2c_stop(void) {

10
TWCR = (1 << TWSTO) | (1 << TWEN) | (1 <<
TWINT); // Send stop condition
}

void i2c_write(uint8_t data) {


TWDR = data; // Load data into data register
TWCR = (1 << TWEN) | (1 << TWINT); // Start
transmission
while (!(TWCR & (1 << TWINT))); // Wait for
TWINT flag set
}

uint8_t i2c_read_ack(void) {
TWCR = (1 << TWEN) | (1 << TWINT) | (1 << TWEA);
// Enable TWI, clear TWINT, enable ACK
while (!(TWCR & (1 << TWINT))); // Wait for
TWINT flag set
return TWDR; // Return received data
}

uint8_t i2c_read_nack(void) {
TWCR = (1 << TWEN) | (1 << TWINT); // Enable
TWI, clear TWINT
while (!(TWCR & (1 << TWINT))); // Wait for
TWINT flag set
return TWDR; // Return received data
}

SPI (ATmega328P and ATmega2560):


Register Manipulation: Setting up the SPI registers.

11
Code Example:
// SPI initialization for ATmega328P and ATmega2560
void spi_init(void) {
DDRB = (1 << PB3) | (1 << PB5) | (1 << PB2);
// Set MOSI, SCK, and SS as output
SPCR = (1 << SPE) | (1 << MSTR) | (1 << SPR0);
// Enable SPI, set as master, set clock rate fck/16
}

void spi_transmit(uint8_t data) {


SPDR = data; // Start transmission
while (!(SPSR & (1 << SPIF))); // Wait for
transmission complete
}

uint8_t spi_receive(void) {
while (!(SPSR & (1 << SPIF))); // Wait for
reception complete
return SPDR; // Return data register
}

UART (ATmega328P and ATmega2560):


Register Manipulation: Setting up the UART registers.
Code Example:
define F_CPU 16000000UL
define BAUD 9600
define MYUBRR F_CPU/16/BAUD-1

void uart_init(unsigned int ubrr) {

12
// Set baud rate
UBRR0H = (unsigned char)(ubrr >> 8);
UBRR0L = (unsigned char)ubrr;
// Enable receiver and transmitter
UCSR0B = (1 << RXEN0) | (1 << TXEN0) | (1 <<
RXCIE0); // Enable receiver, transmitter, and RX
interrupt
UCSR0C = (1 << USBS0) | (3 << UCSZ00); // 8
data bits, 1 stop bit
}
ISR(USART_RX_vect) {
unsigned char received_byte = UDR0;
// Handle received byte
}
int main(void) {
uart_init(MYUBRR);
sei(); // Enable global interrupts
while (1) {
// Main loop
}
}
Troubleshooting Tips
Here are some common issues and troubleshooting tips for these
protocols:
I2C:
Common Issues:
 Address conflicts: Ensure each device on the I2C bus has a
unique address.
 Pull-up resistors: Ensure proper pull-up resistors are used
on SDA and SCL lines.

13
 Clock stretching: Some devices may hold the clock line low to
delay communication. Ensure the master can handle clock
stretching.
Troubleshooting:
 Check connections and continuity on SDA and SCL lines.
 Use an oscilloscope to monitor I2C signals and verify correct
timing and levels.
 Verify device addresses and ensure no conflicts.
SPI:
Common Issues:
 Clock polarity and phase: Ensure CPOL and CPHA settings
match between master and slave devices.
 Signal integrity: Ensure clean signals with minimal noise and
proper termination if needed.
Troubleshooting:
 Verify SPI settings (CPOL, CPHA, clock speed) in both master
and slave devices.
 Use an oscilloscope to check signal integrity and timing.
 Check for loose or incorrect connections.
UART:
Common Issues:
 Baud rate mismatch: Ensure both devices are set to the same
baud rate.
 Framing errors: Ensure correct configuration of data bits,
stop bits, and parity bits.
 Buffer overflows: Ensure the receive buffer is read in time to
avoid overflows.
Troubleshooting:
 Verify baud rate, data bits, stop bits, and parity settings on
both devices.
 Use a logic analyzer or oscilloscope to monitor UART signals.
 Implement proper flow control (e.g., RTS/CTS) if needed.

14
4. C Concepts for Embedded Systems
In this section, we will delve into various C language concepts that
are crucial for embedded systems programming. As an embedded
developer with over two decades of experience and having
interviewed hundreds of candidates, I will emphasize the most
critical aspects and common pitfalls that you need to master.

1. Pointers
Pointers are variables that store the memory address of another
variable. They are crucial in embedded systems for direct memory
access and efficient resource management.
Dereferencing null or uninitialized pointers, pointer arithmetic
errors, and memory leaks.
Code Example:

int var = 10;


int *ptr = &var; // Pointer to an integer variable

// Accessing value using the pointer


printf("Value of var: %d\n", *ptr); // Output: 10

// Changing value using the pointer


*ptr = 20;
printf("New value of var: %d\n", var); // Output:
20

2. Enums
Enumerations provide a way to define and group together sets of
named integer constants. They improve code readability and
maintainability.
Code Example:

15
enum State {INIT, RUNNING, ERROR};
enum State currentState = INIT;

if (currentState == INIT) {
// Initialization code
}
3. Structures
Structures allow the grouping of variables under a single name,
making it easier to manage related data. They are widely used to
represent hardware registers and complex data types.
Code Example:
struct SensorData {
int temperature;
int humidity;
};

struct SensorData data;


data.temperature = 25;
data.humidity = 60;

4. Pointers to Structures
Pointers to structures enable dynamic memory allocation and
efficient manipulation to the reader for dynamic memory allocation
and efficient manipulation of complex data types.
Code Example:
struct SensorData {
int temperature;
int humidity;
};

16
struct SensorData *ptr = &data;
ptr->temperature = 25;
ptr->humidity = 60;

5. Bit Fields
Bit fields within structures allow the allocation of a specified
number of bits to structure members, which is useful for memory-
mapped hardware registers.
Code Example:
struct {
unsigned int flag1: 1;
unsigned int flag2: 1;
unsigned int flag3: 1;
} flags;

6. const Keyword
The `const` keyword is used to declare variables whose values
cannot be modified after initialization. It is often used for defining
constant values and protecting read-only data.
Code Example:
const int maxSize = 100;

7. static Keyword
The `static` keyword has different meanings based on its context.
For local variables, it preserves the value of the variable across
function calls. For global variables, it restricts the visibility to the
file scope.
Code Example:
void counter() {

17
static int count = 0;
count++;
printf("Count: %d\n", count);
}
8. volatile Keyword
The `volatile` keyword tells the compiler that the value of the
variable may change at any time without any action taken by the
code. This is crucial for variables representing hardware registers.
Code Example:
volatile int flag = 0;

9. Memory Layout of C Programs


Understanding how a C program is laid out in memory helps in
optimizing and debugging embedded systems.
Segments:
 Text Segment: Contains the executable code.
 Data Segment: Contains initialized global and static
variables.
 BSS Segment: Contains uninitialized global and static
variables.
 Heap: Used for dynamic memory allocation.
 Stack: Used for function calls and local variables.
Diagram:
text
+--+
| Text Segment |
+--+
| Data Segment |
+--+
| BSS Segment |

18
+--+
| Heap |
+--+
| Stack |
+--+
10. Ring Buffer Implementation
A ring buffer (or circular buffer) is a data structure that uses a
single, fixed-size buffer as if it were connected end-to-end. This is
particularly useful for buffering data streams.
Calculation of Ring Buffer Size:
The size of the ring buffer should be chosen based on the data rate
of the incoming data and the rate at which the data will be
processed. A larger buffer can handle more burst data but will
consume more memory.
Formula:
(Buffer Size = Data Rate times Max Latency)
Example: If the data rate is 100 bytes/second and the
maximum latency before the buffer is processed is 2 seconds, the
buffer size should be at least 200 bytes.
Code Example:
define BUFFER_SIZE 256
typedef struct {
uint8_t buffer[BUFFER_SIZE];
int head;
int tail;
} RingBuffer;
void buffer_init(RingBuffer *rb) {
rb->head = 0;
rb->tail = 0;
}
19
int buffer_write(RingBuffer *rb, uint8_t data) {
int next = (rb->head + 1) % BUFFER_SIZE;
if (next == rb->tail) {
// Buffer is full
return -1;
}
rb->buffer[rb->head] = data;
rb->head = next;
return 0;
}
int buffer_read(RingBuffer *rb, uint8_t *data) {
if (rb->head == rb->tail) {
// Buffer is empty
return -1;
}
*data = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) % BUFFER_SIZE;
return 0;
}
Handling Overflow:
To handle overflow, ensure that the buffer does not write data if it
is full. This can be managed by checking the head and tail
pointers.
Code Addition:
int buffer_write(RingBuffer *rb, uint8_t data)
{
int next = (rb->head + 1) % BUFFER_SIZE;
if (next == rb->tail) {

20
// Buffer is full, handle overflow
// Option 1: Overwrite the oldest data
rb->tail = (rb->tail + 1) % BUFFER_SIZE;
// Option 2: Discard new data
return -1;
}
rb->buffer[rb->head] = data;
rb->head = next;
return 0;
}

11. Real-World Scenario: Choosing Ring Buffer Size


In a real-time data acquisition system, data from sensors needs to
be buffered before processing.
Example Calculation:
Data rate: 1000 bytes/second.
Maximum processing latency: 0.5 seconds.
Buffer size: (1000 times 0.5 = 500) bytes.
If memory is limited, consider the maximum expected burst
data and the average processing rate.

12. Additional Practical Tips:


 Memory Alignment: Ensure that structures and arrays are
properly aligned in memory to avoid inefficient access and
potential bugs.
 Boundary Conditions: Always test boundary conditions for
buffer overflows, underflows, and pointer dereferencing.
 Compiler Warnings: Enable and heed compiler warnings.
Many subtle bugs can be caught by paying attention to these
warnings.

21
Summary
Understanding these C language concepts is fundamental for
embedded systems programming. Mastery of pointers, structures,
and memory management will not only improve your ability to
write efficient and bug-free code but also prepare you for technical
interviews and real-world embedded system challenges.

22
5. Interrupts and ISR Basics
In this section, we will delve into the fundamentals of interrupts
and Interrupt Service Routines (ISRs) for AVR microcontrollers.
Interrupts are a crucial aspect of embedded systems, allowing the
microcontroller to respond to asynchronous events efficiently. As
an experienced embedded systems developer with extensive
knowledge of AVR controllers, I'll provide detailed explanations,
practical examples, and common pitfalls to avoid.
Introduction to Interrupts
Interrupts are signals that temporarily halt the execution of the
main program to execute a specific task or routine called an
Interrupt Service Routine (ISR). After the ISR is executed, the
microcontroller resumes its normal operation from where it left off.
Types of Interrupts in AVR
 External Interrupts: Triggered by external events on specific
pins.
 Timer Interrupts: Generated by internal timers.
 Peripheral Interrupts: Triggered by peripheral events (e.g.,
ADC completion, UART reception).
Configuring Interrupts in AVR
Configuring interrupts involves enabling the interrupt, setting the
appropriate interrupt vector, and writing the ISR.
Example: External Interrupt on ATmega328P
1. Enabling the Interrupt:
Registers Involved: EIMSK (External Interrupt Mask Register),
EICRA (External Interrupt Control Register A)
Code:
include <avr/io.h>
include <avr/interrupt.h>

void external_interrupt_init(void) {
EIMSK |= (1 << INT0); // Enable INT0

23
EICRA |= (1 << ISC01); // Trigger on falling
edge
}
ISR(INT0_vect) {
// Interrupt Service Routine for INT0
// Your code here
}
int main(void) {
external_interrupt_init();
sei(); // Enable global interrupts
while (1) {
// Main loop
}
}

Example: Timer Interrupt on ATmega328P


1. Enabling the Timer Interrupt:
Registers Involved: TIMSK1 (Timer/Counter1 Interrupt Mask
Register), TCCR1B (Timer/Counter1 Control Register B), OCR1A
(Output Compare Register A)
Code:
include <avr/io.h>
include <avr/interrupt.h>

void timer1_init(void) {
TCCR1B |= (1 << WGM12); // CTC mode
OCR1A = 15624; // Set compare value
for 1Hz (assuming 16MHz clock)

24
TIMSK1 |= (1 << OCIE1A); // Enable Timer1
compare interrupt
TCCR1B |= (1 << CS12) | (1 << CS10); //
Prescaler 1024
}
ISR(TIMER1_COMPA_vect) {
// Interrupt Service Routine for Timer1
compare match
// Your code here
}
int main (void)
{
timer1_init();
sei(); // Enable global interrupts
while (1) {
// Main loop
}
}
Example: UART Interrupt on ATmega2560
1. Enabling the UART Receive Interrupt:
Registers Involved: UCSR0B (USART Control and Status Register
0 B), UDR0 (USART I/O Data Register)
Code:
include <avr/io.h>
include <avr/interrupt.h>

void uart_init(unsigned int ubrr) {


UBRR0H = (unsigned char)(ubrr >> 8);
UBRR0L = (unsigned char)ubrr;

25
UCSR0B = (1 << RXEN0) | (1 << TXEN0) | (1 <<
RXCIE0); // Enable receiver, transmitter, and RX
interrupt
UCSR0C = (1 << USBS0) | (3 << UCSZ00); // 8
data bits, 1 stop bit
}

ISR(USART_RX_vect) {
unsigned char received_byte = UDR0;
// Handle received byte
}

int main(void) {
uart_init(MYUBRR);
sei(); // Enable global interrupts
while (1) {
// Main loop
}
}
Common Pitfalls and Best Practices
 Avoid Long ISRs: Keep ISRs short to ensure other interrupts
can be serviced promptly.
 Use Volatile Variables: Variables shared between main code
and ISRs should be declared as `volatile` to prevent compiler
optimizations that could cause issues.
 Atomic Operations: When accessing shared variables in ISRs
and the main code, ensure atomicity to prevent data
corruption.
 Interrupt Priorities: Be aware of interrupt priorities and
nesting. Higher priority interrupts can preempt lower priority
ones.

26
Example: Handling Shared Variables
1. Using Volatile Variables:
volatile uint8_t flag = 0;

ISR(TIMER1_COMPA_vect) {
flag = 1; // Set flag in ISR
}

int main(void) {
timer1_init();
sei(); // Enable global interrupts
while (1) {
if (flag) {
flag = 0; // Clear flag in main loop
// Handle the interrupt event
}
}
}
2. Atomic Access:
include <avr/interrupt.h>

volatile uint8_t shared_var = 0;


ISR(TIMER1_COMPA_vect) {
shared_var++;
}
int main(void) {
timer1_init();
27
sei(); // Enable global interrupts
while (1) {
uint8_t temp;
cli(); // Disable interrupts
temp = shared_var;
sei(); // Enable interrupts
// Use temp safely
}
}
Practical Applications
 Real-Time Clock: Using timer interrupts to maintain a real-
time clock.
 Debouncing Buttons: Using external interrupts to handle
button presses with debouncing logic.
 UART Communication: Using UART interrupts for efficient
serial communication without polling.
Summary
Understanding and effectively using interrupts is essential for
developing responsive and efficient embedded systems. By
following best practices and avoiding common pitfalls, you can
ensure your interrupt-driven code is reliable and performant.

28
6. Endianness
In this section, we will explore the concept of endianness and its
importance in embedded systems. Endianness is a fundamental
aspect of computer architecture that affects how data is stored and
accessed in memory.
What is Endianness?
Endianness refers to the order in which bytes are arranged in
memory. There are two primary types of endianness:
 Big-Endian: The most significant byte (MSB) is stored at the
lowest memory address.
 Little-Endian: The least significant byte (LSB) is stored at the
lowest memory address.
Why is Endianness Important?
Understanding endianness is crucial for embedded systems
developers because different microcontrollers and processors use
different endianness conventions. Misunderstanding or ignoring
endianness can lead to data corruption and bugs, especially when
dealing with data exchange between systems with different
endianness.

Examples of Endianness
Big-Endian:
Memory Address: 0x00 0x01 0x02 0x03
Value (0x12345678): 0x12 0x34 0x56 0x78
Little-Endian:
Memory Address: 0x00 0x01 0x02 0x03
Value (0x12345678): 0x78 0x56 0x34 0x12

Handling Endianness in Embedded Programming


When developing embedded systems, it is often necessary to
handle data that may be stored in either big-endian or little-endian
format. The AVR microcontrollers are little-endian, so we need to

29
ensure that data exchanged with other systems is correctly
interpreted.

Example: Converting Big-Endian to Little-Endian


Here's an example of converting a 32-bit integer from big-endian
to little-endian on an AVR microcontroller:
include <stdint.h>

uint32_t big_to_little_endian(uint32_t
big_endian_value) {
return ((big_endian_value & 0xFF000000) >> 24) |
((big_endian_value & 0x00FF0000) >> 8) |
((big_endian_value & 0x0000FF00) << 8) |
((big_endian_value & 0x000000FF) << 24);
}
int main(void) {
uint32_t big_endian_value = 0x12345678;
uint32_t little_endian_value =
big_to_little_endian(big_endian_value);

// Now little_endian_value contains 0x78563412


while (1) {
// Main loop
}
}
Practical Examples and Applications
1. Data Exchange Between Systems:
When exchanging data between systems with different
endianness, it is crucial to convert the data to the appropriate
format. For example, network protocols often use big-endian

30
format (network byte order), so data must be converted when
sending or receiving from a little-endian system.
2. Reading Data from Sensors:
Some sensors might send data in big-endian format. In such
cases, the received data must be converted to little-endian format
before processing.
3. File I/O:
When reading from or writing to files that store data in a specific
endianness, conversions might be necessary to ensure correct data
interpretation.

Example: Data Exchange with Big-Endian System


Consider a scenario where an AVR microcontroller communicates
with a network server that uses big-endian format. The
microcontroller must convert data to big-endian format before
sending it to the server.

include <stdint.h>
uint16_t little_to_big_endian_16(uint16_t
little_endian_value) {
return (little_endian_value >> 8) |
(little_endian_value << 8);
}
void send_data_to_server(uint16_t data) {
uint16_t big_endian_data =
little_to_big_endian_16(data);
// Send big_endian_data to server
}
int main(void) {
uint16_t data = 0x1234;
send_data_to_server(data);

31
while (1) {
// Main loop
}
}
Common Pitfalls and Best Practices
 Consistent Endianness Handling: Always be consistent in
handling endianness across your codebase. Mixing big-
endian and little-endian handling without proper conversion
can lead to data corruption.
 Testing and Validation: Test your code thoroughly to ensure
that data is correctly converted between endianness formats.
Use tools like debuggers and memory inspection to verify the
correctness.
 Documentation: Document the endianness expectations of
your code, especially when dealing with interfaces or
protocols that involve data exchange.
Summary
Understanding and handling endianness is a critical skill for
embedded systems developers. By ensuring that data is correctly
interpreted and converted between different endianness formats,
you can prevent bugs and data corruption in your embedded
applications.

32
7. GNU Toolchain Tutorials
In this section, we will explore the GNU toolchain, which is
essential for developing and debugging embedded applications on
AVR microcontrollers. The GNU toolchain includes tools like Make,
avr-gcc, avr-libc, and binutils. These tools provide a robust and
flexible environment for compiling, linking, and debugging
embedded code.
Make Tutorial
Make is a build automation tool that automatically builds
executable programs and libraries from source code by reading
files called Makefiles. Here, we will cover the basics of writing
Makefiles, common commands, and some common pitfalls.
Writing a Simple Makefile
A Makefile typically contains rules that specify how to derive the
target program from source files. Here’s a simple Makefile for an
AVR project:

Makefile for AVR Project


Target MCU
MCU = atmega328p

Target file
TARGET = main

Source files
SRC = main.c uart.c

Compiler and flags


CC = avr-gcc
CFLAGS = -mmcu=$(MCU) -Os -Wall

33
Linker flags
LDFLAGS = -mmcu=$(MCU) -Wl,-Map,$(TARGET).map

Object files
OBJ = $(SRC:.c=.o)

Default rule
all: $(TARGET).hex

Compile source files into object files


%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@

Link object files into an ELF binary


$(TARGET).elf: $(OBJ)
$(CC) $(LDFLAGS) $(OBJ) -o $@

Convert ELF binary to HEX format


$(TARGET).hex: $(TARGET).elf
avr-objcopy -O ihex $< $@

Clean up build files


clean:
rm -f $(OBJ) $(TARGET).elf $(TARGET).hex
$(TARGET).map

34
Explanation of Important Aspects
Variables: Variables like `MCU`, `TARGET`, `SRC`, `CC`, `CFLAGS`,
`LDFLAGS`, and `OBJ` are defined at the beginning for easy
customization.
Rules: Each rule consists of a target, prerequisites, and a recipe.
For example, the rule to create object files `%.o: %.c` specifies how
to compile `.c` files into `.o` files.
Phony Targets: `clean` is a phony target used to remove build
artifacts.

Common Pitfalls and Tips for Writing Effective Makefiles


 Incorrect File Paths: Ensure that file paths in the Makefile are
correct and relative to the Makefile location.
 Misconfigured Compiler Flags: Use appropriate compiler flags
for optimization and warnings. For example, `-Os` optimizes
for size, which is crucial for embedded systems with limited
memory.
 Unintended Variable Overrides: Be cautious with variable
names to avoid unintentional overrides.
 Missing Dependencies: Ensure all source files and headers
are listed as dependencies.
 Incorrect Rule Syntax: Make sure the syntax for rules and
commands is correct. Commands must be indented with a
tab, not spaces.

Step-by-Step Guide for a Sample Project


For simplicity, this guide does not include a full project but
ensures you understand the steps to create and build a simple
AVR project using Make.
 Create Project Directory: Create a directory for your project
and navigate to it.
 Create Source Files: Create your source files, e.g., `main.c`
and `uart.c`.

35
 Write Makefile: Create a Makefile in the project directory with
the content provided above.
 Build the Project: Run `make` in the terminal to build the
project. This will compile the source files and generate the
HEX file for programming the microcontroller.
 Clean the Project: Run `make clean` to remove build artifacts.

Binutils Tutorial
Binutils is a collection of binary tools provided by the GNU project.
For AVR development, common tools include `avr-objcopy`, `avr-
size`, and `avr-addr2line`.

Overview of Tools
1. avr-objcopy: Converts object files between different formats.
Example Usage:
avr-objcopy -O ihex main.elf main.hex
2. avr-size: Displays the size of sections and the total size of the
binary.
Example Usage:
avr-size main.elf
3. avr-addr2line: Converts addresses into file names and line
numbers.
Example Usage:
avr-addr2line -e main.elf 0x0802

Detailed Usage Examples


avr-objcopy: This tool is often used to convert ELF files generated
by the linker into HEX files for programming the microcontroller.
avr-objcopy -O ihex main.elf main.hex

36
avr-size: Use this tool to get a quick overview of the memory usage
of your program.
avr-size main.elf
avr-addr2line: This tool is useful for debugging. It converts
addresses in your program to the corresponding file and line
number.
avr-addr2line -e main.elf 0x0802

C Library and Startup Code Basics


The AVR C library (`avr-libc`) provides a comprehensive set of
functions for AVR development. It includes standard library
functions as well as AVR-specific functions for peripheral control
and low-level operations.

Mention of `avr-libc`
`avr-libc` is a highly optimized C library for AVR microcontrollers.
It includes functions for delay, port manipulation, interrupt
handling, and more. Documentation and resources for `avr-libc`
can be found on http://www.nongnu.org/avr-libc/user-manual/
In-Depth Explanation of Startup Code
Startup code is the code that runs before the main function. It
initializes the microcontroller’s hardware and prepares the
environment for the application code.

Reset Vector: The startup code begins at the reset vector, which is
the address the microcontroller jumps to after a reset.
Initialization of Data and BSS Segments: The startup code
initializes global variables and static variables.
Calling main(): After initialization, the startup code calls the `main`
function.

37
Example Startup Code for ATmega2560
.global __start
__start:
cli ; Disable interrupts
clr r1 ; Clear register r1 (used as
zero register)

ldi r28, lo8(__data_start)


ldi r29, hi8(__data_start)
ldi r30, lo8(__data_load_start)
ldi r31, hi8(__data_load_start)
ldi r24, lo8(__data_size)
ldi r25, hi8(__data_size)
1: ld r0, Z+ ; Load data byte
st X+, r0 ; Store data byte
sbiw r24, 1 ; Decrement counter
brne 1b ; Loop until counter is zero

ldi r28, lo8(__bss_start)


ldi r29, hi8(__bss_start)
ldi r24, lo8(__bss_size)
ldi r25, hi8(__bss_size)
clr r0
1: st X+, r0 ; Clear BSS byte
sbiw r24, 1 ; Decrement counter
brne 1b ; Loop until counter is zero

sei ; Enable interrupts

38
rjmp main ; Jump to main
Detailed Explanation of Startup Code
cli: Disable interrupts to ensure the startup code runs without
interruption.
clr r1: Clear register r1, which is used as the zero register
throughout the program.
Initialize Data Segment: Load the initialized data from flash
memory to the data segment in SRAM.
Initialize BSS Segment: Clear the BSS segment to zero.
sei: Enable interrupts after initialization.
rjmp main: Jump to the main function to start the application.

Summary
The GNU toolchain provides powerful tools for AVR development,
enabling efficient compilation, linking, and debugging of embedded
applications. Understanding how to use these tools effectively will
enhance your development workflow and improve the quality of
your embedded systems projects.

39
8. Linker Script Basics
In this section, we will explore the basics of linker scripts, which
are essential for defining how a program's sections are placed in
memory. Linker scripts are particularly important in embedded
systems programming, where precise control over memory layout
is required to ensure efficient and reliable operation.

What is a Linker Script?


A linker script is a file used by the linker to control the memory
layout of the output file. It specifies how the sections of the
program (such as code, data, and BSS) are to be arranged in the
target memory. This is crucial for embedded systems, where
memory resources are limited and specific regions of memory may
be reserved for particular purposes.

Key Elements of a Linker Script


1. Memory Regions: Define the physical memory layout of the
target device.
2. Sections: Specify how different sections of the program are
mapped to memory regions.
3. Entry Point: Define the starting address of the program.

Example Linker Script for ATmega328P


Below is a basic linker script for an ATmega328P microcontroller:
/* Define memory regions */
MEMORY
{
FLASH (rx) : ORIGIN = 0x0000, LENGTH = 32K
RAM (rwx) : ORIGIN = 0x0100, LENGTH = 2K
EEPROM (rw) : ORIGIN = 0x0000, LENGTH = 1K
}

40
/* Define sections and their placement in memory */
SECTIONS
{
.text :
{
*(.text*) /* All .text sections (code)
*/
*(.rodata*) /* Read-only data (constants)
*/
_etext = .; /* End of .text section */
} > FLASH

.data :
{
_data = .;
*(.data*) /* All .data sections
(initialized data) */
_edata = .;
} > RAM AT > FLASH

.bss :
{
_bss = .;
*(.bss*) /* All .bss sections
(uninitialized data) */
*(COMMON)
_ebss = .;
} > RAM

41
.eeprom :
{
*(.eeprom*) /* EEPROM data */
} > EEPROM

.stack (NOLOAD) :
{
_stack = .;
*(.stack*)
_estack = .;
} > RAM

/* Define entry point */


_start = 0x0000;
PROVIDE(_start = 0x0000);
}

Detailed Explanation of Key Elements


1. Memory Regions:
FLASH: This region is for storing the program code and read-only
data. The `rx` attribute indicates that this region is readable and
executable.
RAM: This region is for storing initialized and uninitialized data.
The `rwx` attribute indicates that this region is readable, writable,
and executable.
EEPROM: This region is for storing non-volatile data. The `rw`
attribute indicates that this region is readable and writable.

42
2. Sections:
.text: Contains the executable code and read-only data. It is
placed in the FLASH memory region.
.data: Contains initialized data. It is loaded into RAM but initially
stored in FLASH.
.bss: Contains uninitialized data. It is placed in RAM.
.eeprom: Contains EEPROM data. It is placed in the EEPROM
memory region.
.stack: Defines the stack region. `NOLOAD` indicates that this
section should not be loaded into the final output file.
3. Entry Point:
`_start`: Defines the entry point of the program, which is the
address where the microcontroller starts execution after a reset.
For AVR microcontrollers, this is typically set to `0x0000`.

Custom Memory Segments


In some cases, you may need to define custom memory segments
for specific purposes, such as bootloaders or special data storage.

Bootloader Segment:
MEMORY
{
BOOT (rx) : ORIGIN = 0x7000, LENGTH = 8K
FLASH (rx) : ORIGIN = 0x0000, LENGTH = 24K
RAM (rwx) : ORIGIN = 0x0100, LENGTH = 2K
}

SECTIONS
{
.boot :

43
{
*(.boot*)
} > BOOT

.text :
{
*(.text*)
} > FLASH

.data :
{
*(.data*)
} > RAM AT > FLASH
}

This example creates a separate memory region for a bootloader,


placed at the end of the FLASH memory.

Creating Linker Scripts for Memory-Constrained Applications


When working with memory-constrained applications, it is
essential to carefully allocate memory to avoid overlaps and ensure
efficient use of available resources. Here are some tips:

1. Minimize the Size of Initialized Data: Use the `.data` section


sparingly and prefer using `.bss` for large uninitialized arrays.
2. Optimize Stack and Heap Usage: Carefully estimate the stack
and heap sizes required by your application to avoid excessive
allocation.
3. Use Compiler Optimization: Enable compiler optimizations (e.g.,
`-Os` for size optimization) to reduce the size of the generated code.

44
Common Pitfalls and Best Practices
1. Overlapping Sections: Ensure that memory regions and sections
do not overlap, as this can cause runtime errors and unpredictable
behavior.
2. Uninitialized Data: Always initialize global and static variables
to avoid undefined behavior.
3. Alignment Issues: Ensure proper alignment of sections to
prevent access violations and improve performance.
4. Documentation: Document your linker scripts thoroughly to
make it easier for others (and your future self) to understand the
memory layout.

Summary
Linker scripts are a powerful tool for controlling the memory layout
of embedded applications. By understanding the key elements of
linker scripts and following best practices, you can ensure efficient
and reliable operation of your embedded systems.

45
9. Conclusion and Next Steps
In this workbook, we have covered a range of essential topics for
AVR microcontroller programming using GNU tools. By now, you
should have a solid understanding of the following:

 AVR Architecture: Understanding the key features and


programmer’s model of ATmega328P and ATmega2560
microcontrollers.
 Protocols (I2C, SPI, UART): Practical usage and application of
these communication protocols through register
manipulation.
 C Concepts for Embedded Systems: Key C language concepts
such as pointers, structures, bit fields, and memory layout,
along with practical examples.
 Interrupts and ISR Basics: Configuring and handling
interrupts, writing efficient ISRs, and avoiding common
pitfalls.
 Endianness: Understanding endianness, handling data
exchange between systems with different endianness, and
practical examples.
 GNU Toolchain Tutorials: Using Make, avr-gcc, avr-libc, and
binutils for compiling, linking, and debugging embedded
applications.
 Linker Script Basics: Defining memory regions, sections, and
entry points in linker scripts for efficient memory
management.

While this workbook provides a comprehensive foundation,


mastering embedded systems programming often requires hands-
on experience and personalized guidance. If you are looking to
further enhance your skills and accelerate your career growth,
consider reaching out for personalized training. Here's how you
can benefit from personalized training sessions:
 Tailored Learning: Customized training plans that focus on
your specific needs and goals.

46
 Expert Guidance: Learn directly from an experienced
embedded systems developer with decades of industry
experience.
 Real-World Projects: Work on practical projects that simulate
real-world challenges and scenarios.
 Career Advancement: Gain the confidence and skills required
to excel in technical interviews and secure high-paying jobs
in the embedded systems domain.

"Embark on a journey of mastery in embedded systems


programming. Unlock your potential, gain expert insights,
and transform your career. Don't miss this opportunity to
become a confident and skilled embedded developer. The
future is yours to shape!"

Contact Information:
For personalized training and mentoring, visit [Embedded System
Coach] https://www.embeddedsystemcoach.com or contact me at
[email protected]

Additional Resources and References


Here are some additional resources to further your knowledge and
skills in AVR programming and embedded systems:
 AVR Freaks Community: https://www.avrfreaks.net/ A
vibrant community of AVR developers sharing tips, projects,
and solutions.
 Microchip Technology Documentation:
https://www.microchip.com/en-us/documentation Official
documentation for AVR microcontrollers and tools.
 GNU Project: https://www.gnu.org/ Comprehensive
resources on GNU tools and software.
 AVR Libc Documentation: https://www.nongnu.org/avr-
libc/user-manual/ Detailed documentation for the AVR C
library.

47
Mastering embedded systems requires continuous learning and
practice. By building on the foundation provided in this workbook,
you can tackle increasingly complex projects and advance your
career in embedded systems. Remember, the key to success is
persistence, curiosity, and a willingness to explore new challenges.

Thank you for using this workbook, and best of luck on your
journey to becoming an expert embedded systems developer!

48

You might also like