Blink an arduino’s LED without IDE -embedded tutorials 1

Tobias Aguiar
9 min readFeb 26, 2023

--

Photo by Harrison Broadbent on Unsplash

If you want to get in embedded software development and want to be serious and professional, you need to forget about those abstraction layers. This steals you the opportunity to learn new low-level programming skills, which are fundamental in the embedded software engineering path.

If you are a beginner and want to seriously get into embedded software development, you need to acquire a end-to-end global system view, and this also includes understand the hardware-software interface, which most of the time is hidden by low-level-abstraction layers.

Most of people recommend you to start with an ARM processor, but I allow myself to disagree. Here’s what I wrote about that here.

But anyway, let’s get started !

Tools you’re going to need

I’m supposing you’re using an OS linux, because that’s how I did. So you’re going to need to install those tools on the terminal :

sudo apt-get install gcc-avr avr-libc binutils-avr avrdude

Why those tools ?

  1. gcc-avr : This is the GNU Compiler Collection for the AVR microcontroller architecture. It provides a set of C and C++ compilers, as well as other tools like assemblers and linkers, that allow you to write and compile programs for the AVR microcontroller.
  2. avr-libc : This is the standard C library for the AVR microcontroller. It provides a set of common C functions and macros that are optimized for use on the AVR microcontroller, including functions for working with I/O ports, timers, and other hardware peripherals.
  3. binutils-avr : This is a set of binary utilities for the AVR microcontroller, including an assembler, linker, and object file converter. These tools are used during the compilation process to convert your program source code into a format that can be run on the AVR microcontroller.
  4. avrdude : This is a utility for programming AVR microcontrollers using a variety of hardware programmers. It provides a simple command-line interface for flashing programs onto the microcontroller and also supports various programming protocols.

With all these tools installed, we’re good to start!

Editing the main.c file

When you’re going to blink an arduino’s LED and you don’t use IDE or libraries/hardware abstractions, where can you look at to know what exactly to program?

If you thought about datasheet, you are right! The datasheet is the most reliable “library” if you’re going to program an embedded device from scratch!

Before showing it, create a file with a simple file editor and let’s take a look at the main.c file :

#include <avr/io.h>
#include <util/delay.h>

int main(void) {
DDRB |= 1 << DDB5;

while(1) {
PORTB ^= 1 << PORTB5;
_delay_ms(500);
}

return 0;
}

Pretty small, right? but let’s detail each of the lines, because I like to question the why of each thing.

#include <avr/io.h>
#include <util/delay.h>

These lines are preprocessor directives that include header files containing definitions for the AVR microcontroller's input/output (I/O) registers and delay functions. The avr/io.h header file contains definitions for the AVR microcontroller's I/O registers, while the util/delay.h header file contains delay functions.

int main(void) {

This is the main function of the program. Every C program needs a main function as it is the starting point of the program. I talked a little bit more in details about that on this article.

DDRB |= 1 << DDB5;

This line sets bit 5 of the DDRB register to 1, which sets pin 5 of port B as an output. In AVR microcontrollers, each I/O pin has a corresponding bit in a special register called the Data Direction Register (DDR). Setting a bit in the DDR to 1 makes the corresponding pin an output. And how do I know this : datasheet ! You can find this information everywhere on internet, but the only way to find out if it’s telling you the truth is the datasheet!

datasheet
while(1) {

This is an infinite loop, which means that the code inside the loop will run forever, because in the context of embedded systems we always develop products for repetitive and specific tasks!

PORTB ^= 1 << PORTB5;

This line toggles the state of pin 5 of port B. It uses the XOR operator to toggle the state of bit 5 of the PORTB register. When the bit is set to 1, the corresponding pin is turned on, and when it's set to 0, the pin is turned off. I talk a little bit more about bit operations on this article.


_delay_ms(500);

This line introduces a delay of 500 milliseconds using the _delay_ms() macro provided by the util/delay.h header file. This delay is necessary to slow down the blinking of the LED to a visible rate.

Compiling and flashing on the board

Here’s the commands you’re going to need to compile and flash your program onto the board :

 avr-gcc -Os -DF_CPU=160000000U -mmcu=atmega328p -c -o main.elf main.c
This command is used to compile our source file.Here's a breakdown of each argument in this command :1. avr-gcc : This is the command to invoke the AVR-GCC compiler.
2. -Os : This is a compiler flag that specifies the optimization level. -Os stands for "optimize for size", which tells the compiler to optimize the code to minimize the size of the resulting binary. This is useful for embedded systems with limited flash memory.
3. -DF_CPU : This is a preprocessor flag that defines a macro called F_CPU with a value of 160000000. This macro is used by the AVR-Libc library and other AVR-GCC header files to calculate timing constants, such as those used in the _delay_ms() macro. The U sufix indicates that the value is an unsigned integer.
4. -mmcu :
5. -o main.elf : This is a compiler flag that specifies the output file name. In this case, it's main.elf, which is an ELF (Executable and Linkable Format) binary file that contains the compiled object code.
6. -c : This is a compiler flag that tells the compiler to compile the code into an object file without linking it. This is useful for large projects where you want to compile each source file separately and then link them together later.
But wait! We can't load this elf file right away onto the board, you know why? This file contains a lot of additional information such as debugging symbols, function and variable names, and other metadata that the mcu can't understand, although it's pretty useful for us developers.To load the program onto the AVR microcontroller, we need to convert the elf file into a format that can be understood by the AVR microcontroller. This is where the command avr-objcopy comes into play!
avr-objcopy -O ihex -R .eeprom main.elf main.hex
This command is used to transform the elf file in a machine code format.Here's a break down of each argument in this command :1. -O ihex : This argument specifies the output file format. In this case, the format is Intel HEX, which is a commonly used file format for storing binary data in a human-readable text format.
2. -R .eeprom : This argument tells avr-objcopy to remove the EEPROM section from the output file. The EEPROM is a type of non-volatile memory in AVR microcontrollers that can be used to store data that needs to be retained even when the power is turned off. By excluding this section from the output file, the resulting main.hex file will only contain the program code and not any data stored in EEPROM.
3. main.hex : This argument specifies the name of the output file that the command will create and it will contain the machine code for the program in Intel HEX format.
Finally, we're good to load the machine code onto the board! For that, we will need another tool! Why is that? When you compile and assemble your code, you get an executable file in a binary format that contains the machine code that needs to be loaded onto the microcontroller's flash memory. However, loading this binary file onto the microcontroller is not as simple as copying the file onto the board's storage device, like you would do with a USB stick. Microcontrollers don't have a traditional operating system like desktop computers, and their memory organization is often quite different.To load the machine code onto the microcontroller, you need a special program that understands the microcontroller's architecture and programming protocol. This program is often referred to as a "programmer". The programmer connects to the microcontroller via a set of pins (such as SPI or JTAG), and it communicates with the microcontroller to erase and program its flash memory. The process of loading machine code onto the microcontroller is referred to as "flashing".The tool we're going to use for this end is avrdude.
avrdude -c arduino -p atmega328p -U flash:w:main.hex -P /dev/ttyACM0
This command is used to load the machine code onto the board.Here is a detailed explanation of each argument of the command:1. avrdude : This is the command to invoke the avrdude program, which is used to program AVR microcontrollers.
2. -c arduino : This specifies the programming protocol to be used. In this case, it is the protocol used by the Arduino board.
3. -p atmega328p : This specifies the AVR microcontroller to be programmed. In this case, it is the ATmega328P microcontroller, which is used on the Arduino Uno board.
4. -U flash:w:main.hex : This is the actual programming command. The U option specifies the memory space to be programmed, which in this case is the flash memory (that's why flash:). The w option specifies the operation to be performed, which is to write to the specified memory space. The main.hex argument specifies the name of the file that contains the machine code to be programmed onto the microcontroller.
5. -P /dev/ttyACM0 : This specifies the port to which the Arduino board is connected. In this case, /dev/ttyACM0 port.

But how do you know which device port you will communicate with? That’s the perfect time to present you a tool (command line) that will be useful for the rest of your life if you want to work in embedded software in linux environment : dmesg !

dmesg is a command in Linux and Unix operating systems that displays the kernel’s message buffer. It shows system messages, including errors, warnings, and other information about hardware, drivers, and system events, that have been generated since the last boot. In other worlds, it’s a portal to know everything that’s happening in the lower layers of your PC.

Here’s how use dmesg to discover which device port is connected to your board :

If everything goes fine, you should have this log message in yout terminal :

avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.00s

avrdude: Device signature = 0x1e950f (probably m328p)
avrdude: NOTE: "flash" memory has been specified, an erase cycle will be performed
To disable this feature, specify the -D option.
avrdude: erasing chip
avrdude: reading input file "main.hex"
avrdude: input file main.hex auto detected as Intel Hex
avrdude: writing flash (30 bytes):

Writing | ################################################## | 100% 0.02s

avrdude: 30 bytes of flash written
avrdude: verifying flash memory against main.hex:
avrdude: load data flash data from input file main.hex:
avrdude: input file main.hex auto detected as Intel Hex
avrdude: input file main.hex contains 30 bytes
avrdude: reading on-chip flash data:

Reading | ################################################## | 100% 0.02s

avrdude: verifying ...
avrdude: 30 bytes of flash verified

avrdude: safemode: Fuses OK (E:00, H:00, L:00)

avrdude done. Thank you.

Now you should have your LED blinking with a 500ms period. That’s all! It’s a bit complicate in the beginning and it’s a lot of information, but soon it will be as natural as reading a menu in a restaurant, trust me!

As always, thank you for taking then time to read me!

Other articles

1.A harsh truth to deal with if you are starting embedded software development

2.Communications protocols in embedded software development

--

--

Tobias Aguiar

Software developer | Trying to make complex concepts look easy | Want help or discuss about embedded software development? Email me! tobi.aguiar01@gmail.com