· 6 min read

Building an Air Quality Monitoring App with Arduino and ESP32 (Part 3) - Arduino Pitfalls & Gotchas

This article was auto-translated from Chinese. Some nuances may be lost in translation.

This article is the third in the series:

  1. Sensors Introduction - DHT11 and MH-Z14A
  2. Data Communication - UART
  3. Arduino Pitfalls & Gotchas (this article)
  4. (Upcoming) WiFi Edition: To save debugging time, I purchased an ESP32 development board, which comes with built-in WiFi and Bluetooth.
  5. (Upcoming) MQTT Edition: Using the lightweight MQTT protocol to transmit data to other devices.
  6. (Upcoming) Grafana / Web Edition: Once data is stored in a database, it has to be displayed in a cool way! Here, I used Grafana + Prometheus and Svelte to visualize the data.

In this post, I will discuss some of the pitfalls and traps I stumbled into due to my unfamiliarity with Arduino and C/C++.

Different CPU Byte Orders (Big-Endian vs. Little-Endian)

Because CPU instruction set architectures differ, the order in which data is read also varies—this is known as byte order or endianness. Reading starting from the most significant byte is called Big-Endian, while reading starting from the least significant byte is called Little-Endian. The diagram below illustrates this clearly:

big-endian and little-endian difference

Arduino uses Little-Endian byte order; you can find the explanation here. Aside from the memory layout mentioned above, any difference in byte reading order can be described in terms of Big-Endian and Little-Endian.

For ease of memory management, an OS divides memory into fixed-size units. When storing continuous data, if there is not enough space, it spills over into the next slot, and the values are combined when accessed.

I ran into this issue because I tried to be clever by casting ppm directly to an unsigned short pointer. I assumed this would automatically compute the correct number for us without needing to manually write bit-shift operations, like in the example below:

struct Co2Result {
  byte startByte;
  byte command;
  byte high;
  byte low;
  unsigned short ppm;
};
Co2Result *result = malloc(sizeof(Co2Result));
result->startByte = response[0];
result->command = response[1];
result->high = response[2];
result->low = response[3];
result->ppm = &result->high;

Since a short is 2 bytes, pointing to result->high and reading 2 bytes forward should theoretically have the same effect as (result->high << 8) + result->low.

short 指標測試結果

After testing, I got a bizarre number, which I realized was caused by the byte order. Once I dutifully switched to bit shifting, I was able to get the correct number:

result->ppm = (result->high << 8) + result->low;

C Data Types Vary by Platform

For example, an int can be 2 bytes or 4 bytes, and a regular long might be 8 bytes or 4 bytes. In languages like Java, data types don’t behave differently across platforms because their definitions are standardized by the language implementation itself. In C/C++, however, fixed-width types like uint8_t exist so that platforms can define explicit data type sizes as needed.

Although this is second nature to experienced C developers, when calculating the size of a data structure, you should always use sizeof rather than assuming data type sizes are uniform across all platforms.

Making Good Use of the byte Data Type

Arduino provides a data type called byte. Under the hood, it is defined identically to unsigned char. Since it is easy to get confused about the differences between uint8_t, char, and int, let’s clarify them here:

  1. byte and unsigned char are identical; both allocate 8 bits of memory to the variable. The only difference is that unsigned char conceptually feels more general-purpose, whereas byte more clearly conveys that the value is an unsigned number between 0 and 255. Arduino recommends using byte for consistency.
  2. uint8_t, byte, and unsigned char essentially represent the same concept—acting somewhat like aliases. In C/C++, different platforms might define basic types differently, so fixed-width types like uint8_t are defined to provide explicit sizing.

Using Header Files to Modularize Code

When working on Arduino projects, codebases are typically small, so stuffing all functionality into loop and setup doesn’t make things too unreadable. However, as the circuit gets more complex, jamming every implementation detail into a single file quickly becomes messy.

Even though Arduino sketches use the .ino extension, it is essentially C++ under the hood. Thus, it supports C++ syntax, though standard library support is limited. To write modules and use Arduino functions, you simply need to include Arduino.h:

#include "Arduino.h"
#include <string.h>

Also, if you use VS Code for development, you might find that Arduino’s built-in libraries cannot be located by IntelliSense. Even though the code compiles, the editor shows red squiggly error lines. You can resolve this by adding .vscode/c_cpp_properties.json so VS Code can locate the correct header files. An example is shown below (for macOS; on Linux or Windows, you’ll need to locate where Arduino is installed):

{
  "configurations": [
    {
      "name": "Mac",
      "includePath": [
        "${workspaceFolder}/**",
        "/Applications/Arduino.app/Contents/Java/hardware/**",
        "/Applications/Arduino.app/Contents/Java/hardware/arduino/**"
      ],
      "defines": [],
      "macFrameworkPath": [
        "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/System/Library/Frameworks"
      ],
      "compilerPath": "/usr/bin/clang",
      "cStandard": "c11",
      "cppStandard": "c++17",
      "intelliSenseMode": "clang-x64"
    }
  ],
  "version": 4
}

Miscellaneous

  • noInterrupts() is the same as cli(). In Arduino.h, it is defined as:
#define interrupts() sei()
#define noInterrupts() cli()
  • Using std::string directly on Arduino will cause issues. It’s generally better to avoid std on Arduino—all those nice C++ standard library features can’t really be used. Check out the discussion in this forum thread; some community members have tried to port the standard library to Arduino, though I’m not sure what the current progress is.

Through this experimentation, I found that learning low-level programming concepts through Arduino is a great approach. You have a built-in Serial Port for debugging, plugging in via USB and installing the IDE allows you to upload code right away without fuss, and the underlying implementations are readily available on GitHub. It has also motivated me to brush up on my C/C++ knowledge (which I had only touched back in college).

Finally, because of my limited familiarity with Arduino, although I did my best to research and verify everything, there may still be oversights or inaccuracies. Please feel free to share any feedback or corrections!

Related Posts

Explore Other Topics