LECTURE 03

Programming a Microcontroller

Duration: 121 min of teaching Level: undergraduate, year 2 - recommended after Lecture 02 Discipline: Microcontrollers and Microprocessors PDF: download the notes RO versiunea română

The student who presses the upload button in the Arduino IDE experiences a moment of magic: a few seconds later, an LED blinks. Between the press and the blink, however, a dozen distinct operations take place, carried out by just as many separate programs, none visible on screen. Today we open this black box: what does it actually mean to program a microcontroller, what forms does the text we write pass through until it becomes bits in Flash memory, what does the file that reaches the programmer look like, and why do the error messages in a first project almost never point at the real cause of the problem.

1Purpose and structure of the course7 min

Programming a microcontroller means, in the most concrete sense, changing the state of some cells of non-volatile memory inside the chip, so that, on the next power-up, the central unit finds there a sequence of instructions it can execute. Nothing more. The microcontroller does not "understand" C, does not know what a function is and has no notion of a file - it has a program counter pointing to an address in Flash, reads a 16-bit word from there, interprets it as an instruction, executes it and moves on.

The first major difference from a personal computer On a PC, the program stays a file on disk; the operating system loads it into RAM, resolves its links to libraries, gives it a virtual address space and starts it. The microcontroller has no operating system, disk, or loader - the program must already sit in non-volatile memory, at the exact physical address where the central unit will look for it. That is why the process is called programming, not installing: we do not copy a file somewhere, we permanently write a pattern of bits into a memory.

The second difference concerns where the work happens. The program is written, compiled and transformed on a host computer, of a different architecture than the target. A compiler that runs on x86 and produces code for an 8-bit AVR is called a cross-compiler, and the set of tools that go with it forms a toolchain - for AVR, it is called avr-gcc.

The two halves of the journey
The first is purely software and happens entirely on the host computer: writing, compiling, linking, all the way to a file in the format the programmer understands. The second is a transfer and physical write operation into memory, followed by the code executing on the microcontroller. The course follows exactly this split.
Recap from Lecture 02
  • Flash memory holds the program and stays written after power is cut, unlike SRAM, which empties out (Lecture 02)
  • The central unit reads instructions from Flash through the program counter, on a bus separate from the data one - the Harvard architecture (Lecture 02)
  • The oscillator frequency determines the duration of an instruction cycle; today we will see what happens when the program assumes a different frequency than the real one (Lecture 02)
  • List, in order, the five transformations the source code goes through on its way to bits in Flash, with the tool and resulting file for each
  • Explain why an .elf file cannot be sent directly to the programmer
  • Read a line of an Intel HEX file and check its checksum
  • Compare programming via bootloader with ISP, in terms of the hardware needed and the space consumed
  • Estimate the Flash and SRAM usage of a program and recognize the symptoms of each running out
  • Identify the real cause of a program that "runs twice as slowly" or resets on its own

2The stages of programming: from source to ELF12 min

main.csourcepreprocessor.icompiler.sassembler.olinker.elfobjcopy.hexavrdudeFlasheach box is a separate program, invisible in the IDEbelow each box: the file it produces
The toolchain, from the text written by the programmer to the bits in Flash memory. The IDE displays a single command, but runs all these programs in turn, deleting the intermediate files at the end.

An IDE displays a single command, "Compile" or "Upload". Behind it, five successive transformations run in order, each producing an intermediate file with a precise role. IDEs delete the intermediate files at the end - which does not mean they never existed, just that the student never gets to see them.

1. Preprocessing

The first tool to touch the source code is not the compiler, but the preprocessor - it does not know C, for it the file is just text. It executes the directives starting with a hash sign (#include, #define, #ifdef) and hands the compiler a text from which these have disappeared. This explains why a fifteen-line program grows, after preprocessing, to tens of thousands of lines: the header <avr/io.h> pulls in the device-specific file, with the definitions of all registers and their bits. When we write PORTB, the preprocessor replaces it with the physical address of the register; the compiler never sees the name, only access to an address.

2. Actual compilation

The compiler receives the preprocessed text and produces assembly for the target architecture. Here the high-level structure is permanently lost: loops, functions, data types become AVR instructions, and variables are placed in the 32 general registers or, if they do not fit, in SRAM.

The optimizer and the volatile trap

If a variable is modified in an interrupt routine, but the main program only reads it in a loop, the compiler, which only analyzes the loop, notices that nobody modifies it and reads it just once, keeping it in a register - the loop becomes infinite, even though the variable changes in memory. volatile forbids exactly this optimization: it is not a trick, but information the programmer has and the compiler does not.

3. Assembling

The assembler translates the mnemonics one-to-one into binary machine code - no optimization decisions, just a fixed correspondence. The result, the .o file, is not executable code: it is organized into sections (.text for instructions, .data for non-zero-initialized global variables, .bss for zero-initialized ones, .rodata for constants) and is incomplete - if it calls a function from another file, the assembler does not know its address and leaves a gap, noted in the relocation table.

4. Linking

The linker receives all the object files and libraries, with three tasks: resolving symbols (finds the definition of every name undefined locally - hence "undefined reference" and "multiple definition" errors), allocating addresses (concatenates sections of the same kind and places them in the address space, according to a linker script - for the ATmega328P, .text starts at address 0 in Flash, .data/.bss in SRAM) and relocation (fills in the gaps left by the assembler, now that the addresses are known).

The startup code you never wrote The linker adds the startup code (crt), which occupies the first addresses in Flash, installs the table of interrupt vectors, initializes the stack pointer, copies the contents of the .data section from Flash into SRAM, fills .bss with zero and only then calls main. The initial values of global variables therefore take up space both in Flash and in SRAM - an observation that comes back in the discussion of SRAM exhaustion.

The result is an ELF executable file - it contains the fully relocated code, plus debugging information (symbol names, address-to-line correspondence, variable types), essential for any debugger and the reason the .elf file must not be discarded.

The toolchain, step by step: put the stages in order

3From ELF to Flash memory9 min

A chip programmer does not need symbols or debugging information - it needs an answer to a single question: what byte goes at what address. From the ELF, only the sections that actually end up in Flash are extracted (.text and the initial image of .data) and transcribed into the Intel HEX format, with the utility avr-objcopy.

The conversion compiles nothing

The machine code is already ready in the ELF - the conversion is a simple repackaging, from a complex container into a minimal text format. From the same ELF, the content destined for EEPROM is extracted separately, into the .eep file.

ExtensionProduced byContent
.ipreprocessorsource with includes resolved
.scompilerAVR assembly code
.oassemblermachine code by section, addresses unresolved
.elflinkerrelocated executable, with symbols
.hexobjcopyaddress-byte pairs, for the programmer
.maplinkerthe address of every symbol

The transfer into memory

The last stage is the only one that involves hardware. A program on the host computer (avrdude - AVR Downloader Uploader) reads the .hex file, splits it into pages and sends them, through a serial protocol, to the write logic in the microcontroller's Flash.

Writing to Flash is done by page, not by byte

For the ATmega328P, a page is 64 words (128 bytes), and the 32 KB of Flash are split into 256 pages. Flash technology allows flipping a bit from 1 to 0 by writing, but not the other way around - bringing it back to 1 can only be done by erasing, again at the page or whole-chip level. The correct sequence is therefore: erase, then load the page into a buffer register, then the write command. The number of erase-write cycles is finite, on the order of tens of thousands - enough for development, but relevant if someone wanted to use Flash as frequently rewritten data memory.

After writing, verification is recommended: the programmer reads back the content and compares it with the source. A mismatch indicates insufficient power, too high a programming frequency, or worn-out memory.

A useful distinction when debugging The stages up to generating the .hex file do not touch the microcontroller and do not depend on it - they can run without the board being connected. Only the last stage needs hardware. If the error message appears before the upload progress bar, the problem is in the code or the configuration, not in the cable, board or port.
Match the file with what it contains

4The Intel HEX file format10 min

The .hex file is the last object a human can inspect before the program disappears into the chip - and the only one in the chain simple enough to be read with the naked eye. The format, defined in the 1970s for transfer over slow and unreliable serial links, is pure ASCII text, each line stands on its own and carries its own address, and each line has a checksum.

The structure of a line: :BBaaAATTDD...DDCC
BB - the number of data bytes on the line (usually 16, i.e. 0x10).
aaAA - the address from which the bytes are written, most significant byte first.
TT - the record type.
DD...DD - the actual data bytes, exactly the machine code generated by the compiler.
CC - the checksum of the line.
TypeNameRole
00Dataprogram bytes, to be written at the given address
01End of filethe last line, contains no data
04Extended linear addressthe upper address bytes, above 64 KB (does not appear for the ATmega328P)
Worked exercise - checking the checksum

Given the line :0A000000182800000000000000288E. Identify the fields and check the checksum.

See the solution

The byte count is 0A (10 data bytes). The address is 0000. The type is 00 (program data). The 10 data bytes follow: 18 28 00 00 00 00 00 00 00 28. The last byte, 8E, is the checksum.

All bytes are added, excluding the checksum: 0A+00+00+00+18+28+00×7+28 = 72₁₆ = 114₁₀.

The two's complement of 72₁₆ = 0111 0010₂: flip the bits (1000 1101) and add one: 1000 1110 = 8E₁₆. This matches the byte in the file - the line is correct.

A faster alternative check: 72+8E = 100₁₆, whose low byte is zero - exactly what the checksum property requires: the sum of all bytes on the line, including the checksum, gives zero over eight bits.

The last line of any Intel HEX file is always :00000001FF - zero data bytes, type 01, checksum FF (the complement of 01). Its absence makes the programmer refuse the file, treating it as truncated.

The first line of an AVR program - the interrupt vector table

A typical first line: count 10 (16 bytes), address 0000, type 00, data 0C 94 34 00 followed by 0C 94 3E 00 repeated three times.

See the explanation

The bytes are read two at a time, in reverse order (AVR stores words with the low byte first): the pair 0C 94 is the word 940C, the opcode of the absolute jump instruction jmp; the next pair gives the destination address. What we see at the start of memory is therefore the interrupt vector table - a list of jumps, one for each interrupt source. The first entry, at address 0, is the reset vector and jumps to the startup code; the others, unused in this program, all jump to the default routine, which just resets the microcontroller.

5The structure of Flash memory and the bootloader10 min

Who writes to Flash? The memory is internal to the chip, and its pins do not expose the memory bus directly. There are two answers: either an external device takes control of the chip through a dedicated serial interface, or the microcontroller programs itself, running a program already present in it, using the spm (store program memory) instruction, which writes a page into Flash. The program that does this is called a bootloader.

The two sections of Flash memory

For safety, the Flash of the ATmega328P is split into an application section (the lower part, from address zero, the user's program) and a boot section (the upper part, the bootloader). The distinction is enforced by hardware: the spm instruction has effect only when the PC is inside the boot section - an ordinary application program cannot rewrite Flash either by mistake or on purpose.

The size of the boot section is not fixed It is chosen from four values (256, 512, 1024 or 2048 words), through two permanent configuration bits (fuses) BOOTSZ1/BOOTSZ0. A third fuse, BOOTRST, decides whether execution starts at address zero (directly into the application) or in the boot section (into the bootloader) - fuses that cannot be changed by the bootloader, only by an external programmer.

On reset, the bootloader initializes the communication interface and waits a few hundred milliseconds for a synchronization message. If it arrives, it receives code pages, writes them with spm and jumps to address zero. If it does not arrive, it jumps straight to the application - the explanation for the one-second delay observed when powering up an Arduino board.

What is lost if the bootloader disappears

The Optiboot bootloader, used on the Arduino Uno, takes up 512 bytes - which is why the Arduino environment reports 32256 bytes available, not 32768. If the bootloader is erased (for example by an ISP programming session that performs a full erase), the microcontroller keeps executing the application perfectly, but loses the ability to be programmed serially - any later change requires an external programmer, until the bootloader is rewritten. This is what the "Burn Bootloader" command does: it programs the bootloader image via ISP and sets the BOOTRST and BOOTSZ fuses accordingly.

A brand-new chip from the distributor
Has no bootloader and cannot be programmed serially. To use it on an Arduino-type board, it must first be programmed once, via ISP, with the right bootloader and fuses - the same reason a chip replaced after a failure does not respond to a USB upload until this initial operation is done.

6Programming Arduino boards8 min

On an Arduino Uno board, besides the ATmega328P, a second chip handles the USB-to-serial conversion - the host computer sees it as a virtual serial port, whose transmit/receive lines are wired to the RXD/TXD pins of the main microcontroller. This chip only carries bytes, it does not program anything.

Automatic reset

The bootloader only listens right after a reset. On old boards, the user had to press reset at exactly the right moment. The current solution: the DTR line of the virtual serial port is wired to the reset pin through a small capacitor. When the program on the computer opens the port, it toggles DTR, and the capacitor passes only the edge, generating a short reset pulse - the board resets itself at exactly the right moment.

Why the serial monitor restarts the program Opening the serial monitor also opens the virtual serial port, toggling the same DTR line - that is why the board resets and the program starts over. It is not a bug, it is the effect of the automatic reset mechanism.
The avrdude command generated by the Arduino environment
avrdude -C avrdude.conf -p atmega328p -c arduino \
        -P /dev/ttyUSB0 -b 115200 -D \
        -U flash:w:prog.hex:i

-p gives the target microcontroller, -c the programming protocol, -P the port, -b the speed, -U describes the operation (flash memory, write, the file, the Intel HEX format). -D disables a full chip erase - precisely so as not to destroy the bootloader.

The dialog protocol is called STK500: the host requests synchronization, asks for the chip's signature, sends the address and content of each page, requests the write, moves to the next page. The Optiboot bootloader implements only a subset, enough for writing to Flash - it cannot write the fuses or read EEPROM, precisely to stay within the 512 bytes allocated to it.

The "not in sync" error

The communication speed must match exactly between the two ends, being fixed in the bootloader's code (Optiboot: 115200 b/s; older bootloaders: 57600). The most frequent cause of the error is a wrong board type selected in the development environment compared to the board actually connected.

7Programming via ISP9 min

In-system programming (ISP, or ICSP) is the basic method, which does not depend on any code already present in the microcontroller. It uses the chip's synchronous serial SPI interface, in a special mode activated by holding the reset pin active - as long as the chip is held in reset, the central unit executes nothing, and the programming logic takes over control through MOSI/MISO/SCK.

The dialog: enable programming, read the chip's signature (three bytes: manufacturer and model), erase, load the pages, verify.

The frequency constraint

The SCK clock applied by the programmer cannot exceed a quarter of the microcontroller's clock frequency, because the programming logic is internally synchronized with the system clock. A brand-new chip, on the divided internal oscillator (1 MHz clock), accepts at most 250 kHz on SCK - a programmer configured for a higher frequency will report finding no device, even though the wiring is correct. Most programmers have a "slow clock" option for this situation.

CriterionBootloader (serial)ISP (SPI)
Hardware neededjust the USB cablededicated programmer
Code present on the chipa bootloader already writtennone; works on a brand-new chip
Flash space consumed512 bytes or morenone
Access to fuses / lock bitsnoyes
Can write the bootloadernoyes
Startup delayyes (the listening window)no
Typical useongoing development, updatesfirst programming, repair, production
The two modes do not exclude each other, they complement each other Ordinary development uses the bootloader, because it is sufficient and fast. The ISP programmer becomes indispensable when first powering up a new chip, when the clock or protection configuration must be changed, and when the application needs every byte of Flash and an instant startup - in which case the bootloader is deliberately removed. An Arduino board can itself act as an ISP programmer for a second chip, through the ArduinoISP sketch - the same setup used to repair a board whose bootloader was erased by mistake.

8Programming languages and the cost of libraries11 min

A microcontroller can, in principle, be programmed in any language with a compiler for that architecture. In practice, the choice is constrained by resources: with 32 KB of Flash and 2 KB of SRAM, there is no room for a virtual machine, a garbage collector or a rich standard library.

Assembly, C, C++

Assembly gives the most compact and fastest code possible, with perfectly known resource use - essential in loops with a strict deadline or interrupts with guaranteed timing. It becomes, however, hard to read and unportable beyond a few dozen lines; today it is used sparingly, in short, critical routines.

C holds the dominant position, for solid reasons: the types match the processor's natural units, bitwise operators allow direct manipulation of registers, pointers allow access to physical addresses. A good compiler generates, for carefully written code, a result comparable to hand-written assembly. The price: a steep learning curve and no safety net - C allows, without complaint, writing past the end of an array or an uninitialized pointer.

C++ adds classes and encapsulation - the Arduino libraries (Serial, Servo) benefit visibly. The cost: dynamic allocation, exception handling and runtime type identification consume memory that 2 KB of SRAM cannot sustain, which is why embedded practice uses a disciplined subset, without these mechanisms.

What the Arduino libraries hide

The Arduino environment hides the registers behind functions with readable names - a good pedagogical choice at first, costly later.

/* variant with Arduino functions */
void setup() { pinMode(13, OUTPUT); }
void loop() { digitalWrite(13, HIGH); digitalWrite(13, LOW); }

/* the same operation, directly on registers */
#include <avr/io.h>
int main(void) {
    DDRB |= (1 << PB5);
    while (1) {
        PORTB |= (1 << PB5);
        PORTB &= ~(1 << PB5);
    }
}

The difference in behavior is nil - on the Uno board, pin 13 is exactly PB5. The difference in cost is large: PORTB |= (1<<PB5) generates the sbi instruction, two cycles. digitalWrite(13, HIGH) translates the pin number into a port and bit by reading two tables from Flash, checks whether the pin is attached to a PWM channel and, if so, detaches it, disables interrupts, modifies the register and re-enables them - dozens of cycles, instead of two.

Worked exercise - the maximum frequency of a signal generated through the program

ATmega328P at 16 MHz. Estimate the maximum frequency of a square-wave signal obtained by toggling a pin in a loop, with digitalWrite (~50 cycles/call) versus direct register access (sbi/cbi, 2 cycles).

See the solution

T_clk = 1/(16×10⁶) = 62.5 ns. A full period requires one toggle up and one down.

With the library: 2×50 = 100 cycles, T ≈ 100×62.5 ns = 6.25 µs, f ≈ 160 kHz.

With direct access: 2×2 = 4 cycles, T ≈ 4×62.5 ns = 250 ns, f ≈ 4 MHz.

The ratio is ~25, exactly the ratio of the cycle counts. For turning on an LED or reading a button, the difference is completely irrelevant - a human does not perceive microseconds. For generating a signal, communication implemented in software, or an interrupt that must finish quickly, the difference decides whether the application works.

Not a reason to avoid libraries, but an explicit trade-off Arduino libraries offer safety and convenience in exchange for speed and memory. A well- written program uses library functions in the non-critical parts, where readability matters, and drops down to registers where every cycle matters.

9Debugging a microcontroller9 min

Debugging a microcontroller is structurally harder than on a computer: it has no screen, console or file system for a log; it interacts with the physical world in real time (a motor does not stop for a breakpoint); many faults sit at the code-hardware boundary (a poorly soldered wire, a power supply that sags), invisible to a software debugger; and the resources needed for debugging - program memory, pins - are exactly the resources in short supply.

Hardware-level debugging

For small AVRs, the mechanism is called debugWIRE - it uses a single wire, the reset pin itself, turned into a bidirectional command link: it stops the processor, reads/writes registers and memory, executes step by step, sets breakpoints.

The DWEN fuse trap

debugWIRE is enabled through the DWEN fuse - once enabled, the reset pin no longer works as an ordinary reset, so programming via ISP, which depends on it, becomes impossible. Getting out requires a special debugger command; if the connection is lost before that, the chip requires high-voltage parallel programming, with specialized equipment. Arduino Uno boards do not conveniently expose this mode, because reset is already wired to the automatic reset circuit.

JTAG (four or five pins, on larger AVRs and most ARM chips) and SWD (the reduced two-pin variant, specific to ARM) offer the same basic functions - breakpoints, step-by-step execution, inspecting variables and the stack - working because the debugging device reads the .elf file and knows which address corresponds to which line of code.

Why serial messages remain the main tool

They need no extra hardware (the link already exists, the same one used for uploading), do not stop execution (the system keeps running while it reports), allow observing evolution over time - a value that drifts slowly shows up in a log, not in a single break.

The limits of printing over serial

It takes time - at 9600 b/s, a thirty-character message takes over thirty milliseconds, an eternity for an interrupt. It consumes SRAM (next section). And, most insidiously, it can change the program's behavior: a fault caused by tight timing can disappear when we add prints and reappear when we remove them. When timing is suspected, a better alternative is toggling a free pin, observed on an oscilloscope or logic analyzer.

The cheapest diagnostic tool Before suspecting the code, check the power supply and that the microcontroller is actually running. A loop that blinks an LED, placed at the start of the program, answers in three seconds whether the chip starts up, whether the clock is the one assumed, and whether the program actually reached Flash.

10Flash full, SRAM exhausted: the memory budget11 min

Flash memory full

The simplest error, clearly reported by the development environment. The usual cause is not the code written by the student, but the included libraries - a graphics library, a networking one, or using floating-point arithmetic can each add thousands of bytes. Out of the 32768 bytes, the bootloader already consumes 512.

Solutions, in order of effort: removing unused libraries, giving up floating point where integers suffice, moving constant tables into Flash with PROGMEM, and, as a last resort, removing the bootloader and programming via ISP.

SRAM memory exhausted

Much more insidious - it produces no message. The program compiles, uploads, starts, works for a while, then produces absurd results or resets on its own.

The mechanism, in detail

SRAM holds three things: global/static variables (the .data and .bss sections, known at compile time), dynamic allocation (if used), and the stack, which grows from the highest address toward lower ones. The compiler reports the size of the first category, but cannot know how deep the stack will grow at run time. When the stack drops below the global variables area, it starts overwriting them, and vice versa - with no protection unit stopping it.

Worked exercise - the hidden cost of constant strings

A program uses twenty constant messages of thirty characters each, printed over serial. Estimate the SRAM consumption.

See the solution

A literal such as "Temperature measured: " is, by default, copied from Flash into SRAM by the startup code - AVR has separate address spaces for program and data, and ordinary pointers point into SRAM. Twenty messages of thirty characters: 20×30 = 600 bytes, almost a third of the available memory, without the programmer having declared a single variable.

The fix: the F() macro from the Arduino library (respectively the PROGMEM attribute and the _P functions in plain C) keeps the string in Flash and reads it character by character, as needed, with the lpm instruction.

/* consumes SRAM */
Serial.println("Temperature measured: ");
/* keeps the string in Flash */
Serial.println(F("Temperature measured: "));
The practical warning sign If global variables take up more than ~75% of SRAM, the space left for the stack becomes risky, and random behavior is to be expected.
The memory budget: does the program fit on the ATmega328P?

11The clock and other confusions7 min

The clock configured differently than the code assumes

Shows up as wrong delays and garbled serial communication. The code does not measure time, it counts cycles: _delay_ms calculates the number of iterations starting from the F_CPU macro, defined at compile time; the same F_CPU is also used to calculate the speed divider of the serial interface.

Worked exercise

A program compiled with F_CPU = 16000000 contains _delay_ms(500). The microcontroller, however, runs on the internal oscillator at 1 MHz. How long does it actually take?

See the solution

The compiler calculated the cycles assuming the declared frequency: N = 0.5 s × 16×10⁶ = 8×10⁶ cycles - a fixed number, baked into the code.

At run time, the cycles are consumed at 1 MHz: t = 8×10⁶ / 1×10⁶ = 8 s, sixteen times longer than the intended 0.5 s - exactly the ratio of the frequencies (16 MHz / 1 MHz = 16).

Diagnostic method: measure a known delay and calculate the real frequency as the product of the declared frequency and the ratio between the expected and measured duration.

The situation typically arises on a new chip

Programmed via ISP, where nobody set the fuses for the external oscillator, or when a board with a different crystal than the one physically mounted is selected in the development environment. A simple check: a loop that turns the LED on for one second and off for one second, timed - if the observed period is eight or sixteen times larger than expected, the cause is the clock, not the program.

Other frequent confusions

A wrongly selected board type leads to the wrong communication speed with the bootloader. The serial port occupied by another application makes the upload fail without a clear explanation. A pin used simultaneously by the application and the ISP interface (PB3, PB4, PB5) can prevent programming if an external circuit loads the lines too heavily. Omitting volatile on variables shared with interrupts produces a program that works at zero optimization and hangs at the default level - wrongly suggesting a compiler bug.

The common thread: the symptom is far from the cause The method that helps the most is reduction: start from the smallest program known to work reliably (blinking an LED) and add one element at a time, checking after each step. It is slower than searching directly for the bug, but it is the only method that always converges.

12Frequent mistakes4 min

  • "The program compiled without errors, so it is correct." Compiling only checks syntax and types. A program that fits in Flash, but exhausts SRAM at run time, compiles flawlessly and behaves randomly on the board. Read the memory report shown at the end of compilation, not just the "Done compiling" line, and watch the percentage of SRAM used by global variables.
  • "I changed the code, but the board behaves the same - it is probably a hardware problem." Most often the upload never happened: the wrong port is selected, the board chosen in the menu does not match, or the transfer failed, and the previous program remained in Flash. Check the message at the end of the upload (the number of bytes written and verified) before suspecting the hardware.
  • "I can copy the .elf file directly onto the microcontroller, it is just an executable." False - ELF contains addresses, sections and debugging information the programmer cannot interpret; the microcontroller has no file system and no loader. The Intel HEX file, obtained through conversion with avr-objcopy, is sent to the programmer; the .elf is kept only for debugging.
  • "I set F_CPU in the code, so the microcontroller will run at that frequency." F_CPU controls nothing in hardware - it is just a constant the compiler uses to calculate delay loops. The real frequency comes from the oscillator and the configuration fuses. Match F_CPU to the real oscillator; if delays come out twice as long or twice as short, that is where the cause is, not in the code.

13Summary and glossary5 min

Programming means writing to a non-volatile memory at exact physical addresses, not installing a file. The transformation chain runs from the source code through preprocessing, compiling, assembling and linking to an ELF executable, then through conversion to the Intel HEX text format, written page by page into Flash by avrdude. The ATmega328P's Flash splits into the application section and the boot section, where the bootloader lives - a program that writes itself, through spm, enabled only in its own zone, at the cost of 512 bytes and a startup delay. ISP is the universal alternative, which does not depend on any code present on the chip and has access to the fuses. C dominates microcontroller programming for its closeness to the machine; Arduino libraries trade speed and memory for readability. Debugging is structurally harder than on a computer - serial messages remain the main tool, and the most insidious faults (exhausted SRAM, mismatched clock) produce no error message.

Toolchain
the set of tools that turns source code into executable code for a different architecture.
Linker
resolves symbols, allocates addresses and relocates the sections from the object files.
Intel HEX
a text format for transferring code to a programmer, with a checksum on every line.
Bootloader
a program that runs on the microcontroller and reprograms it by itself, through the spm instruction.
ISP
in-system programming, over SPI, with the microcontroller held in reset.
debugWIRE
hardware debugging over a single wire, through the reset pin.

14Self-check questions7 min

  1. Describe, in order, the transformations a C source file goes through until the code is written into Flash, naming the tool and resulting file for each stage.
  2. Explain why the .elf file cannot be sent directly to the programmer and what information is lost when converting to Intel HEX.
  3. What are the three tasks of the linker? What does relocation mean?
  4. Why can an ordinary application program not write to its own Flash memory, and what hardware condition must be met for spm to have any effect?
  5. Compare programming via bootloader with ISP from the point of view of the hardware needed, the space consumed and access to the configuration bits.
  6. Explain the mechanism by which an Arduino board resets itself at the start of an upload, and connect it to the fact that opening the serial monitor restarts the program.
  7. A program uses twenty constant messages of forty characters each. Estimate the SRAM consumption and explain how the F() macro reduces it.

15Directions for further study2 min

The next lecture moves down from the toolchain to the simplest and most used peripheral of a microcontroller: the input-output ports - configuring them bit by bit, reading and writing directly through registers, and the electrical pitfalls of a digital pin (floating, pull-up, multiplexing).