Get started with Arduino and ESP32-DevKitC: debugging and unit testing

The goal of this tutorial is to demonstrate how simple it is to use PlatformIO IDE for VSCode to develop, run and debug a simple project with Arduino framework for ESP32-DevKitC board.

Requirements:

Setting Up the Project

At first step, we need to create a new project using PlatformIO Home Page (to open this page just press Home icon on the toolbar):

../../../_images/arduino-debugging-unit-testing-1.png

On the next step we need to select ESP32-DevKitC as a development board, Arduino as a framework and a path to the project location (or use the default one):

../../../_images/arduino-debugging-unit-testing-2.png

Processing the selected project may take some amount of time (PlatformIO will download and install all required packages) and after these steps, we have a fully configured project that is ready for developing code with Arduino framework.

Adding Code to the Generated Project

Let’s add some actual code to the project. Firstly, we open a default main file named main.cpp in the src_dir folder and replace its content with next one:

#include <Arduino.h>

void setup()
{
    Serial.begin(9600);
}

void loop()
{
    Serial.println("Hello world!");
    delay(1000);
}
../../../_images/arduino-debugging-unit-testing-3.png

After this step, we created a basic project ready for compiling and uploading.

Compiling and Uploading the Firmware

Now we can build the project. To compile firmware we can use the next options: Build option from the Project Tasks menu, Build button in PlatformIO Toolbar, Task Menu Tasks: Run Task... > PlatformIO: Build or in PlatformIO Toolbar, Command Palette View: Command Palette > PlatformIO: Build or via hotkeys cmd-alt-b / ctrl-alt-b:

../../../_images/arduino-debugging-unit-testing-4.png

If everything went well, we should see a successful result message in the terminal window:

../../../_images/arduino-debugging-unit-testing-5.png

To upload the firmware to the board we can use the next options: Upload option from the Project Tasks menu, Upload button in PlatformIO Toolbar, Command Palette View: Command Palette > PlatformIO: Upload, using Task Menu Tasks: Run Task... > PlatformIO: Upload or via hotkeys cmd-alt-u / ctrl-alt-u:

../../../_images/arduino-debugging-unit-testing-6.png

After successful uploading, we need to check if the firmware is uploaded correctly. To do this we need to open the serial monitor and check that the message from the board is received. To open serial monitor we can use the next options: Monitor option from the Project Tasks menu, Serial Monitor button in PlatformIO Toolbar, Command Palette View: Command Palette > PlatformIO: Monitor, Task Menu Tasks: Run Task... > PlatformIO: Monitor:

../../../_images/arduino-debugging-unit-testing-7.png

If the firmware works as expected, the message from the board can be observed in the terminal window:

../../../_images/arduino-debugging-unit-testing-8.png

Debugging the Firmware

Setting Up the Hardware

In order to use a JTAG probe with ESP32 we need to connect the following pins:

ESP32 pin

JTAG probe pin

3.3V

Pin 1(VTref)

GPIO 9 (EN)

Pin 3 (nTRST)

GND

Pin 4 (GND)

GPIO 12 (TDI)

Pin 5 (TDI)

GPIO 14 (TMS)

Pin 7 (TMS)

GPIO 13 (TCK)

Pin 9 (TCK)

GPIO 15 (TDO)

Pin 13 (TDO)

PIO Unified Debugger offers the easiest way to debug the board. Firstly, we need to specify debug_tool in “platformio.ini” (Project Configuration File). In this tutorial Olimex ARM-USB-OCD-H debug probe is used:

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
debug_tool = olimex-arm-usb-ocd-h

To start the debug session we can use the next options: Debug: Start debugging from the top menu, Start Debugging option from Quick Access menu or hotkey button F5:

../../../_images/arduino-debugging-unit-testing-9.png

We need to wait some time while PlatformIO is initializing debug session and when the first line after the main function is highlighted we are ready to debug:

../../../_images/arduino-debugging-unit-testing-10.png

We can walk through the code using control buttons, set breakpoints, add variables to Watch window:

../../../_images/arduino-debugging-unit-testing-11.png

Writing Unit Tests

Test cases can be added to a single file that may include multiple tests. First of all, in this file, we need to add four default functions: setUp, tearDown, setup and loop. Functions setUp and tearDown are used to initialize and finalize test conditions. Implementations of these functions are not required for running tests but if you need to initialize some variables before you run a test, you use the setUp function and if you need to clean up variables you use tearDown function. In our example we will use these functions to accordingly initialize and deinitialize LED. setup and loop functions act as a simple Arduino program where we describe our test plan.

Let’s create test folder in the root of the project and add a new file test_main.cpp to this folder. Next basic tests for String class will be implemented in this file:

  • test_string_concat tests the concatenation of two strings

  • test_string_substring tests the correctness of the substring extraction

  • test_string_index_of ensures that the string returns the correct index of the specified symbol

  • test_string_equal_ignore_case tests case-insensitive comparison of two strings

  • test_string_to_upper_case tests upper-case conversion of the string

  • test_string_replace tests the correctness of the replacing operation

#include <Arduino.h>
#include <unity.h>

String STR_TO_TEST;

void setUp(void) {
    // set stuff up here
    STR_TO_TEST = "Hello, world!";
}

void tearDown(void) {
    // clean stuff up here
    STR_TO_TEST = "";
}

void test_string_concat(void) {
    String hello = "Hello, ";
    String world = "world!";
    TEST_ASSERT_EQUAL_STRING(STR_TO_TEST.c_str(), (hello + world).c_str());
}

void test_string_substring(void) {
    TEST_ASSERT_EQUAL_STRING("Hello", STR_TO_TEST.substring(0, 5).c_str());
}

void test_string_index_of(void) {
    TEST_ASSERT_EQUAL(7, STR_TO_TEST.indexOf('w'));
}

void test_string_equal_ignore_case(void) {
    TEST_ASSERT_TRUE(STR_TO_TEST.equalsIgnoreCase("HELLO, WORLD!"));
}

void test_string_to_upper_case(void) {
    STR_TO_TEST.toUpperCase();
    TEST_ASSERT_EQUAL_STRING("HELLO, WORLD!", STR_TO_TEST.c_str());
}

void test_string_replace(void) {
    STR_TO_TEST.replace('!', '?');
    TEST_ASSERT_EQUAL_STRING("Hello, world?", STR_TO_TEST.c_str());
}

void setup()
{
    delay(2000); // service delay
    UNITY_BEGIN();

    RUN_TEST(test_string_concat);
    RUN_TEST(test_string_substring);
    RUN_TEST(test_string_index_of);
    RUN_TEST(test_string_equal_ignore_case);
    RUN_TEST(test_string_to_upper_case);
    RUN_TEST(test_string_replace);

    UNITY_END(); // stop unit testing
}

void loop()
{
}

Now we are ready to upload tests to the board. To do this we can use the next options: Test button on PlatformIO Toolbar, Test option from the Project Tasks menu or Tasks: Run Task... > PlatformIO Test from the top menu:

../../../_images/arduino-debugging-unit-testing-12.png

After processing we should see a detailed report about testing results:

../../../_images/arduino-debugging-unit-testing-13.png

As we can see from the report, all our tests were successful!

Adding Bluetooth LE features

Now let’s create a basic application that can interact with other BLE devices (e.g phone) For example, next code declares a BLE characteristic whose value can be printed to the serial port:

#include <Arduino.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>

#define SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"

class MyCallbacks: public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) {
      std::string value = pCharacteristic->getValue();
      if (value.length() > 0) {
        Serial.print("\r\nNew value: ");
        for (int i = 0; i < value.length(); i++)
          Serial.print(value[i]);
        Serial.println();
      }
    }
};

void setup() {
  Serial.begin(9600);

  BLEDevice::init("ESP32 BLE example");
  BLEServer *pServer = BLEDevice::createServer();
  BLEService *pService = pServer->createService(SERVICE_UUID);
  BLECharacteristic *pCharacteristic = pService->createCharacteristic(
                                         CHARACTERISTIC_UUID,
                                         BLECharacteristic::PROPERTY_READ |
                                         BLECharacteristic::PROPERTY_WRITE
                                       );

  pCharacteristic->setCallbacks(new MyCallbacks());

  pCharacteristic->setValue("Hello World");
  pService->start();

  BLEAdvertising *pAdvertising = pServer->getAdvertising();
  pAdvertising->start();
}

void loop() {
  delay(2000);
}

Now we can compile and upload this program to the board as described in the previous sections. To verify that our application works as expected, we can use any Android smartphone with BLE feature and Nordic nRF Connect tool.

At first, we need to scan all advertising BLE devices and connect to the device called ESP32 BLE example. After the successful connection to the board, we should see one “Unknown Service” with one “Unknown Characteristic” field:

../../../_images/arduino-debugging-unit-testing-14.png

To set the value we need to send the new text to the BLE characteristic:

../../../_images/arduino-debugging-unit-testing-15.png

The change of the value is printed to the serial monitor:

../../../_images/arduino-debugging-unit-testing-16.png

Conclusion

Now we have a project template for ESP32-DevKitC board that we can use as boilerplate for the next projects.