Arduino/AVR tutorial

Ready to build your first robot under 3h?

Download as .zip Download as .tar.gz View on GitHub

Universal Asynchronous Receiver-Transmitter

Contents

So far, you’ve been blinking LEDs in one way or another. It’s time to let your board talk to your PC too!.

See 19. USART0 chapter in Atmega328p Datasheet for very detailed UART tech specification 1.

Introduction

Serial port, UART or Universal Asynchronous Receiver-Transmitter is a way that allows you to connect easily your device with PC and exchange data.

Asynchronous means that receiver/transmitter does not have to run at the same pace. Clocks of both devices can run at different frequencies or be out of phase. UART uses start and stop bits to determine when the transmission is over. This gives a great advantage. The protocol can work with no difficult clock synchronization techniques and technologies. Therefore, UART can be used at longer distances, speaking more of meters (yards) rather than centimeters (inches).

There are two industry wide standards of UART: RS-232 and RS-485. We are going to use a different route, though: TTL (Transistor-to-Transistor Logic) and connection via UART-USB bridge. In other words, you’ll use Atmega integrated circuit, connect it to a 3rd party silicone chip (such as CH340, PL2303, FT232RL and many other) and plug a USB connector to your PC. You can even use another Atmega to host a USB (for instance Atmega16u4 with hardware USB support 2)

Good news is your Arduino board (either Cytron’s or the genuine one) comes with UART-USB converter. From coding perspective, you don’t need to do anything.

A traditional UART tutorial presents and explains a data frame - a sequence of bits that form a single portion of a message… Let’s get over with it now (image source: Wiki - by Cody Hyman):

UART data frame

Isn’t it nice? Well, let me explain it from a perspective of typical parameters one typically uses:

For a more detailed explanation, please refer to external sources such as this one: UART - A hardware communication protocol and other.

If you are going to connect your UART device to another device, make sure that:

TX1 pin is your transmitter and RX0 is the receiver in your board. Take a look at it. Of course, you can simply use a USB cable if you plan to use PC comm only.

Serial Monitors

You can use your serial device with a 3rd party tool or your custom software. If you decide to simply browse or even plot data you can use some of these tools.

Windows:

Linux:

If you are interested in writing your own software, you need a library or follow a Lord’s blessed path and write your own library. It’s up to you. Personally, I worked with the following:

SerDes - Serialization and Deserialization

Data serialization is a process of converting data into a format that can be transmitted over wire/air and decoded at a destination point. The reverse process is called deserialization.

The keyword is format. You can choose two options: text or binary. The text option is usually human readable, supported by formats such as CSV, JSON, HTML, anything custom. The binary format converts data into a series of bytes, sometimes applying compression. You need to use the same serialization/deserialization technique in both receiver and transmitter to successfully deliver a message. Otherwise, the message can be consider as a series of random useless bytes.

Which one is better? Well, it depends. The binary format usually comes with big frameworks that can convert a schema into your C or C++ code. All you need to do is to compile a schema file and link generated files to your program. IMO, the greatest disadvantage of the binary format is difficulty to handle data packets. Since all your data is binary, you don’t really know when the message stops and another message begins. A byte of binary data is really an 8-bit number, which translates to 0-255 (dec). Microcontroller is not aware of what the byte means. The binary formats of your choice usually provides libraries to capture a whole message and deserialize it, such as protobuf or msgpack, ROS Messages. Of course, you can implement something custom too - see the note below.

You can use text format too! It has several advantages: 1) it’s human readable, 2) easy to find the last character, usually a new line character \n. Text format narrows down a range of values we send in a single byte from the binary data perspective. ASCII encodes human readable numbers as characters starting from a value 32 (in decimal, space <<space>>) up until 126 (dec, ~ character). Numbers between 0-31 and 127 are special characters, such as new line character \n 10 (decimal). Thanks to this trick, a microcontroller can listen to a sender and cache raw bytes incoming from another UART device until it receives a byte of value 10 (dec, a new line character). You received a full line, a message that can be processed!

[!NOTE] Long time ago, I was playing with robotics and European Rover Challenge. My team and I didn’t know anything about electronics and coding back then. We needed to control a robotic arm with 8 effectors. We developed a binary format -> one byte to control all 8 devices: AAAV VVVV. AAA - 3 address bits, VVVVV - a value between 0-31. This technique allowed us to control a 6-DOF arm without really any worries about a data package start and stop positions. All we needed to do was to read just one byte to rule it all :)!

In my experience, hobby projects work very nicely with JSON (text) encoding. Simply, read a line of text data and deserialize JSON into individual variables. I recommend ArduinoJson4.

ArduinoJson deserialization example (source: JsonParserExample)

// ArduinoJson - https://arduinojson.org
// Copyright © 2014-2024, Benoit BLANCHON
// MIT License
//
// This example shows how to deserialize a JSON document with ArduinoJson.
//
// https://arduinojson.org/v7/example/parser/

#include <ArduinoJson.h>

void setup() {
  // Initialize serial port
  Serial.begin(9600);
  while (!Serial)
    continue;

  // Allocate the JSON document
  JsonDocument doc;

  // JSON input string.
  const char* json =
      "{\"sensor\":\"gps\",\"time\":1351824120,\"data\":[48.756080,2.302038]}";

  // Deserialize the JSON document
  DeserializationError error = deserializeJson(doc, json);

  // Test if parsing succeeds
  if (error) {
    Serial.print(F("deserializeJson() failed: "));
    Serial.println(error.f_str());
    return;
  }

  // Fetch the values
  //
  // Most of the time, you can rely on the implicit casts.
  // In other case, you can do doc["time"].as<long>();
  const char* sensor = doc["sensor"];
  long time = doc["time"];
  double latitude = doc["data"][0];
  double longitude = doc["data"][1];

  // Print the values
  Serial.println(sensor);
  Serial.println(time);
  Serial.println(latitude, 6);
  Serial.println(longitude, 6);
}

void loop() {
  // not used in this example
}

ArduinoJson will be used by default in subsequent chapters. Make sure to install it! Go to Tools -> Manage Libraries..., type ArduinoJson and click Install:

ArduinoJson - Install steps

Arduino Framework

Arduino allows using serial communication with ease. Make sure you familiarize yourself with documentation5.

Serial initialization

This section shall present you how to write data to your PC. To connect with your microcontroller, you can use Arduino Serial Monitor, PlatformIO Serial Monitor, minicom or really anything you enjoy. The following examples shall use Ardunio Serial Monitor and minicom interchangeably.

Go to Tools -> Serial Monitor:

Arduino Serial Monitor

You’ll see a new section at the bottom of your window. Make sure baudrate, newline setting and USB port are selected as shown in the picture (if you use Windows, port can be something like COM7):

Arduino Serial Monitor - CLI

To start minicom, enter the following:

$ minicom -b 115200 -D /dev/ttyACM0

Minicom opens with baudrate=115200, 8-bits dataframe, 1 stop bit, no parity. Just like the Arduino Serial Monitor and the default Arduino Serial config (SERIAL_8N1)

Minicom - Serial Monitor

You know how to open a serial monitor, now it’s worth knowing how to start UART communication in Arduino.

There are two steps:

  1. Initialize serial with baudrate and other config
  2. Wait until a serial peripheral module starts (busy waiting)

Let’s see some code (source - Arduino Serial Initialize):

void setup() {
  Serial.begin(115200);
  while(!Serial) continue;
}

void loop() {
}

That’s it! Isn’t it simple? You probably have a feeling that these two lines hide some complex register calls. That’s correct! Let’s now just focus on the positives: it takes two lines to enable Serial.

Serial.begin(<<baudrate>>[, <<config, default: SERIAL_8N1>>])6 initializes serial port to a given baudrate. Optionally, you can specify additional config for serial, should you need it. Otherwise, just enjoy simplicity of the API. Note that Serial is effectively a global variable that represents hardware serial. If you own an Arduino Mega, Due or any other that happened to support multiple UARTs, you can use other ports as well: Serial1, Serial2. You just need to make sure you connect necessary cables accordingly.

while (!Serial) continue; performs a busy waiting operation. The application waits here until the serial is initiated successfully. The implementation of Serial provides an overloaded operator bool()7.

Alright, we are ready to finally send some data to PC!

Write data

Write a simple line

Without further edo, this is the code (source: Arduino - UART Hello World):

void setup() {
  Serial.begin(115200);
  while(!Serial) continue;
}

void loop() {
  Serial.println("Hello World!");
  delay(1000);
}

To write a line, simply use Serial.println() function8. It accepts raw strings such as char * and provides an overload for typical data types, such as int.

Arduino Serial Monitor - Hello World

If you want to send an int, you can also choose to print it either as decimal, octal or hexadecimal value, i.e.:

  Serial.println(15, DEC);
  Serial.println(15, BIN);
  Serial.println(15, OCT);
  Serial.println(15, HEX);

  result:
    15
    1111
    17
    F

Of course, there is also a function that does not add new line characters: \r\n. You can use Serial.print.

Both print()/println() functions send data in a text format. Well, at least this is how data should be interpreted at a receiving end. You can also send binary data with Serial::write function9. As this course uses text-based serialization, I’ll leave the binary part to study on your own. You can make it!

Write a line, substitute placeholders

Printing a static text is nice, but this is not how you use programs. You put data in and expect some results returned. How to send variable data? There are several options:

In this section, we’ll try to go with the second approach (source - Arduino - Buffer and sprintf):

#include <string.h>

static const constexpr uint8_t TXT_BUFFER_SIZE {64};
static char const TXT_BUFFER[TXT_BUFFER_SIZE] {'\0'};

static int counter {0};

static const char *HELLO_WORLD_LABELS[] = {
  "Hello World",
  "HELLO WORLD",
  "HeLlO WoRlD!"
};

void setup() {
  Serial.begin(115200);
  while(!Serial) continue;
  randomSeed(analogRead(0));
}

void loop() {
  uint16_t value = random(1000);                    // generate a random number
  char random_char = map(value, 0, 1000, 32, 127);  // random visible ASCII character
  ++counter;

  sprintf(TXT_BUFFER, "%s #%d: value=%d, char=%c",  // define a string and substitutions
    HELLO_WORLD_LABELS[counter%3],                  // substitute %s with a random string
    counter,                                        // substitute first %d with the counter
    value,                                          // substitute second %d with a random number
    random_char);                                   // substitute %c with a character

  Serial.println(TXT_BUFFER);
  delay(1000);
}

There are many things here. Let’s dissect all of them. TXT_BUFFER is a static char buffer of size 64. counter and HELLO_WORLD_LABELS is just data that we want send dynamically. Similarly, randomSeed(analogRead(0)) is a random number generator stuff… We want to send some dynamic data, don’t we? This is it, random data generated on the fly.

Finally, we go to loop(). The first 3 lines is simply data generation. Finally, the application reaches sprintf function10. It’s a C-standard function here! Yes, you can use some of C and C++ libraries that come with AVR toolchain. sprintf(<<buffer>>, <<format>>, <<args[]>>):

Then, you simply write the buffer as your result with Serial.println() function. Effectively, all you need to do is to provide a buffer and come up with a format string. Pretty standard C!

Write JSON

You must install ArduinoJson first within your IDE. Please, address SerDes - Serialization and Deserialization for more details.

The code roughly corresponds to the previous example (source: Arduino Serial: ArduinoJson):

#include <ArduinoJson.h>
//ArduinoJson: 7.0.0

static int counter {0};
static const char *HELLO_WORLD_LABELS[] = {
  "Hello World",
  "HELLO WORLD",
  "HeLlO WoRlD!"
};

void setup() {
  Serial.begin(115200);
  while(!Serial) continue;
  randomSeed(analogRead(0));
}

void loop() {
  float value = random(1000) / 100.0;                    // generate a random number
  ++counter;

  JsonDocument doc;                                 // Dynamic memory allocation with malloc/free
  doc["str"] = HELLO_WORLD_LABELS[counter%3];       // Assign one of 3 strings dynamically
  doc["cntr"] = counter;                            // Assign cntr=counter
  doc["val"] = value;                               // Assign val=value
  //doc["r_char"] = random_char;                    // fail: must be converted to string

  serializeJson(doc, Serial);
  Serial.println();
  
  delay(1000);
}

Clearly, the data generation code is similar to the one presented in the previous section. This time, value can be of float type! For use of random() function, please refer to the previous section and Arduino documentation.

JsonDocument is the main library that help you serialize data into JSON object. Note, the library automatically detect variable type and uses corresponding serialization techniques. From your perspective, it provides great comfort and ease of use!

Internally, JsonDocument uses dynamic allocators that can be somehow tricky to use. Generally, MISRA11 does not support dynamic heap allocations (new/delete, malloc()/free, etc.) in embedded programming. ArduinoJson in version 6.* allows to statically allocate memory for that matter. In version 7.*, you need to implement a custom allocator (github code: ArduinoJson: Allocator). You often want your code to be as deterministic as possible. This is to guarantee SLAs in certain use cases, i.e., aircrafts. This is one of the reasons why dynamic heap allocation can become a problem in huge apps.

Finally, all you need to do is to write a JSON object into Serial. You do it by calling serializeJson(<<doc>>, <<Serial>>). Note, the API does not write it as a line, therefore you need to call `println().

Yes, that’s all! Isn’t it simple? Look at your results!

Arduino Serial - Arduino Json write

Read data

You know how to write data, now it’s time to implement a way to receive a command. Both examples deal with turning on and off LEDs, most of them in your Cytron’s board!

Read a line of data

Note, this example uses Arduino’s String12 that allocates memory dynamically. You can modify this example with readBytesUntil that accepts a static array of characters.

Code (source Arduino - Read line):

#include <math.h>

int pin = 0;
void setup() {

  DDRB = 0xff;        // all pins as output
  DDRC = 0xff;
  DDRD = 0xff;

  Serial.begin(115200);
  while(!Serial) continue;
}

void loop() {
  if (Serial.available() > 0) {
    String result = Serial.readStringUntil('\n');
    pin = result.toInt();                               // accept int only, 0 is an error value
  }
  
  if (pin != 0) {
    uint8_t pin_state = pin > 0 ? HIGH : LOW;
    digitalWrite(abs(pin), pin_state);
    Serial.print("PIN set to : ");
    Serial.println(pin, DEC);
  }
  pin = 0;
}

The program accepts integers, including negatives. For a negative value, the program turns off an LED. If is a positive one, an LED is turned on. The code enables all pins as OUTPUT (see DDRB, DDRC, and DDRD config). It’s a rather greedy way to initialize but it works!

The core of the functionality happens in two lines:

  if (Serial.available() > 0) {
    String result = Serial.readStringUntil('\n');

    pin = result.toInt();                               // accept int only, 0 is an error value

First, the application verifies if UART input is active. If it is, the application can effectively wait until a full payload comes in. It’s a blocking waiting function. No other steps shall be executed in the main loop of the program! Finally, readStringUntil() returns with data.

By design, we know it’s going to be an integer. Anything else shall result in an error value, exactly 0. Remember about SerDes section? This is the moment where you introduce a contract between the microcontroller and a PC application you talk to. We decided to use an integer as an API. Nothing prevents you from applying something more complicated, as you’ll see in the next section.

The application reads data until a new line character is received: \n. Windows uses \r\n as a new line delimiter. Still, it does not play a huge role in this example but it can be a possible issue in your future project. Be aware of it!

By now, the application has all data it needs to run business logic! It turns on and off an LED depending on a pin number you enter! It also sends a feedback back to Serial Monitor! Isn’t it great! Finally, your first useful app. Now imagine these LEDs are light bulbs, a kettle, a coffee machine and so on. You can control them all with some basic knowledge on power electronics (i.e, how to use relays).

Read a line of JSON data

First, let’s construct a JSON contract (schema). Let’s say there are two options: LED_ON and LED_OFF. This is what your microcontroller is going to do after all. These are two possible values. JSON also needs a key. op should be good enough (short, yet descriptive). You want to keep your labels rather short as you work on limited resources, RAM especially. You also need to specify a pin number. Say pin key is good enough and a value is a positive integer.

You schema can look more or less like this:

{"op":"LED_ON|LED_OFF","pin":+int}

example:
  {"op":"LED_ON","pin":3}
  {"op":"LED_OFF","pin":3}

Now, let’s take a look at the code (source - Arduino Serial - Read serial as ArduinoJson):

#include <math.h>
#include <ArduinoJson.h>
#include <string.h>

enum class Operations {
  UNKNOWN_OP,
  ENABLE_PIN,
  DISABLE_PIN
};

void setup() {

  DDRB = 0xff;        // all pins as output
  DDRC = 0xff;
  DDRD = 0xff;

  Serial.begin(115200);
  while(!Serial) continue;
}

void loop() {
  JsonDocument doc;
  if (Serial.available() > 0) {
    auto error = deserializeJson(doc, Serial);
    if (!error) {
      uint8_t pin_state = LOW;
      const char *operation = doc["op"];
      Operations mode = strcmp(operation, "LED_ON") == 0
        ? Operations::ENABLE_PIN
        : strcmp(operation, "LED_OFF") == 0
          ? Operations::DISABLE_PIN
          : Operations::UNKNOWN_OP;

      int pin = doc["pin"];

      switch (mode) {
        case Operations::ENABLE_PIN:
          pin_state = HIGH;
          [[fallthrough]]
        case Operations::DISABLE_PIN:
          digitalWrite(pin, pin_state);
          Serial.print("Executed: ");
          serializeJson(doc, Serial);
          Serial.println();
          break;
        default: break;
      }
    }
  }
}

It looks way more complicated! Well, yes but not really. The code responsible for reading JSON is this line auto error = deserializeJson(doc, Serial);. The rest is just applying business logic to your code.

Line const char *operation = doc["op"]; reads data under op key. The contract says, there are only two acceptable options LED_ON|LED_OFF. There are lots of strcmp operations to convert it into a value that can be accepted by the switch.

Line int pin = doc["pin"]; is fairly simple. It reads a JSON value as int. There are no quote marks "" in the JSON you send to the microcontroller [...],"pin":3}. This is core JSON specification.

The switch handles the operation, trying to be smart and limiting some code duplicates by implementing [[fallthrough]] label. It’s meant to signalize that a missing break statement is missing for purpose. In that way, the code automatically executes the next block of code. Just a trick that saves some bytes in the microcontroller at cost of readability. You will need to make such decisions if you are forced to fight for resources in any of your future projects!

The code eventually leads to the same overall functionality as the previous example. However, thanks to JSON it can support additional features later in future! Simply adding a new key and adding a new business logic. It’s not possible with the previous example in which you are forced to rewrite deserialization part. It’s even worse - you need to create your own standard and implement it. It’s time consuming and error prone.

With JSON you keep to one specification and all tools come included too. All you need to do is to identify use cases. If you ever need extend functionality, you can add a new key/value with no impact on software that talks to your microcontroller.

Let’s take a look at the results:

Arduino Serial - ArduinoJSON read, serial results

Observe your board. LED #3 performed an operation and returned a callback to Serial Monitor!

UART, Atmega, Registers

You can apply principles you learned in the previous section and use it here, with raw Atmega registers and implement all UART comms on your own! A few notes:

In other words, low-level UART means lots of work. If you want to test your prototype quickly it’s pointless to reinvent the wheel. Simply, use a library such as Arduino Framework.

If you really want to take a look at UART/USART from a low level perspective, I suggest to read the documentation first, then google term such atmega328p uart example.

Good luck!

Other communication protocols

We are not limited to only one protocol. There are many others. I prepared a list of the most popular ones:

References