adding queues, c++ — more esp32-s3-devkitc-1 programming notes

In this post I’m going to show how I added freeRTOS queues to last post’s code. In addition, I moved the source from C to C++, hopefully using the C++17 standard. I discovered that if I renamed the source file extension from .c to .cpp then the ESP IDF framework would automatically switch from compiling the source as C to the C++. After the switch I was able to do a bit of contemporary C++ coding to clean up my original code, which I’ll show later in the post. To better help explain things, I’ve broken the single source file into four sections to describe what’s happening.

The primary motivation for adding queues came from discovering that you can’t use ESP_LOGx calls in critical sections such as threads. If you do then the application will abort and start over, continuously. So that led me to find a way to transfer information from the threads back to app_main, which can then be logged without causing the application to crash.

#include <stdio.h>#include "freertos/FreeRTOS.h"#include "freertos/queue.h"#include "freertos/task.h"#include "driver/gpio.h"#include "esp_log.h"#include "led_strip.h"#include "sdkconfig.h"QueueHandle_t   qtask = NULL;enum MsgType {corenum = 1,tick};struct TaskMessage {uint16_ttaskid;MsgType msgtype;uint32_tmsgdata;};

This first code section lists the necessary include files, the global definition of the queue handle (QueueHandle_t qtask), a simple enumeration, and a simple message structure to be used by the tasks with the queues.

// Task 1.//static void task_blink_neo_pixel(void * pvParameters) {static led_strip_t *pStrip_a;pStrip_a = led_strip_init(CONFIG_BLINK_LED_RMT_CHANNEL, CONFIG_BLINK_GPIO, 1);pStrip_a->clear(pStrip_a, 50);TaskMessage task_message, *ptask_message;ptask_message = &task_message;task_message.taskid = *reinterpret_cast<uint16_t*>(pvParameters);task_message.msgtype = MsgType::corenum;task_message.msgdata = xPortGetCoreID();xQueueSend(qtask, (void *)&ptask_message, 0);uint32_t count = 0;static int led_colors[][4] = {{0, 32, 0, 0},  // red{0, 0, 32, 0},  // green{0, 0, 0, 32},  // blue{0, 32, 0, 16}, // violet{0, 0, 32, 32}  // cyan};// Stay in an endless loop. Don't return from this task.//while(true) {for (auto color : led_colors) {pStrip_a->set_pixel(pStrip_a, color[0], color[1], color[2], color[3]);pStrip_a->refresh(pStrip_a, 100);vTaskDelay(500 / portTICK_PERIOD_MS);// Set NeoPixel LED dark by clearing all its pixels.pStrip_a->clear(pStrip_a, 50);vTaskDelay(500 / portTICK_PERIOD_MS);task_message.msgtype = MsgType::tick;task_message.msgdata = count++;xQueueSend(qtask, (void *)&ptask_message, 0);}}}

This is the first task which blinks the built-in NeoPixel with five different colors (led_colors) repeatedly. Switching to C++ allowed me to use a C++ style for-each construct (line 58) and eliminate the verbose code I was using before, such as defining the size of the array of colors in one variable and then using a classic C for loop to iterate through led_colors. Cleaner code, fewer lines and two less variables.

However, the interesting bits are lines 39,44, and 68. It’s on those lines the message structure is set up, and it’s lines 44 and 68 that send out a message on the queue. The really annoying part is how to set up the pointer to message that is unique to each task. It took me a while while reading various code examples that the argument in xQueueSend was a void pointer to pointer, not a simple void pointer. Once I got that part straight then I was able to refine the messaging a bit to what it is right now.

Two messages are sent by tasks 1 and 2; the CPU core they’re running on, followed by a continuous data message that’s sending out a simple counter incremented every time an LED flashes.

// Task 2.//static void task_blink_led(void * pvParameters) {const gpio_num_t BLINK_GPIO46_LED = (gpio_num_t)46;gpio_reset_pin(BLINK_GPIO46_LED);// Set the GPIO as a push/pull outputgpio_set_direction(BLINK_GPIO46_LED, GPIO_MODE_OUTPUT);TaskMessage task_message, *ptask_message;ptask_message = &task_message;task_message.taskid = *reinterpret_cast<uint16_t*>(pvParameters);task_message.msgtype = MsgType::corenum;task_message.msgdata = xPortGetCoreID();xQueueSend(qtask, (void *)&ptask_message, 0);uint32_t count = 0;// Stay in an endless loop. Don't return from this task.//while (true) {gpio_set_level(BLINK_GPIO46_LED, true);   // LED onvTaskDelay(100 / portTICK_PERIOD_MS);gpio_set_level(BLINK_GPIO46_LED, false);  // LED offvTaskDelay(100 / portTICK_PERIOD_MS);gpio_set_level(BLINK_GPIO46_LED, true);   // LED onvTaskDelay(100 / portTICK_PERIOD_MS);gpio_set_level(BLINK_GPIO46_LED, false);  // LED offvTaskDelay(2700 / portTICK_PERIOD_MS);task_message.msgtype = MsgType::tick;task_message.msgdata = count++;xQueueSend(qtask, (void *)&ptask_message, 0);}}

Task 2 is a shorter version of Task 1 because it’s only toggling a single color LED on one of its GPIO pins. The set up code for the queue message is the same between both tasks, so there’s a bit of a code smell there. I suppose I should come up with a simple class that encapsulates queue message block initialization. Perhaps the struct TaskMessage can be evolved in that direction. I have yet to decide. Once again the task sends one message with the CPU core it’s running on, followed by a continuous stream of data messages sending the counter results.

extern "C" void app_main(void) {static const char *TAG = "DUAL_BLINK_QUEUE";int core = xPortGetCoreID();ESP_LOGI(TAG, "app_main running on core %i", core);ESP_LOGI(TAG, "CONFIG_BLINK_GPIO %i", CONFIG_BLINK_GPIO);TaskHandle_t xHandle1 = NULL;static uint16_t ucParameterToPass1 = 1;TaskHandle_t xHandle2 = NULL;static uint16_t ucParameterToPass2 = 2;qtask = xQueueCreate(10, sizeof(TaskMessage *));if (qtask != NULL) {ESP_LOGI(TAG, "queue created");// Create task 1.//xTaskCreate(task_blink_neo_pixel,"BlinkNeoPixel",// human readable task name.2048,   // stack size in bytes.&ucParameterToPass1,tskIDLE_PRIORITY,&xHandle1);configASSERT(xHandle1);// Create task 2.//xTaskCreate(task_blink_led,"BlinkLED", // human-readable task name.2048,   // stack size in bytes.&ucParameterToPass2,tskIDLE_PRIORITY,&xHandle2);configASSERT(xHandle2);TaskMessage *ptask_message;// Stay in an endless loop. Don't return from this task.//vTaskDelay(1000 / portTICK_PERIOD_MS);while (true) {// vTaskDelay(1000 / portTICK_PERIOD_MS);xQueueReceive(qtask, &(ptask_message), (1000/portTICK_PERIOD_MS));uint16_t tid = ptask_message->taskid;MsgType msgtype = ptask_message->msgtype;uint32_t msgdata = ptask_message->msgdata;ESP_LOGI(TAG, "Task ID %i, MsgType %i, msg data %i", tid, msgtype, msgdata);}}}

Finally we get to app_main, the entry point for ESP32. Note that I had to declare app_main as extern "C". This occurred when I switch the source to C++. The library that the ESP IDF wants to link with my code expects a standard C app_main signature. Compiling as C++ “mangles” the name, thus the need to declare external C symbol naming.

The majority of app_main is setting up the queue and starting the two tasks. The work begins in line 149 where the code waits for messages to come in from each task. Note that the message block is owned by the tasks, and only a pointer to those message blocks is sent on the message queue. This keeps the amount of data being sent to a minimum.

With all that running, here’s what the initial output looks like:

...I (277) cpu_start: Starting scheduler on PRO CPU.I (0) cpu_start: Starting scheduler on APP CPU.I (297) DUAL_BLINK_QUEUE: app_main running on core 0I (307) DUAL_BLINK_QUEUE: queue createdI (327) gpio: GPIO[46]| InputEn: 0| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 I (1307) DUAL_BLINK_QUEUE: Task ID 2, MsgType 1, msg data 0I (1307) DUAL_BLINK_QUEUE: Task ID 1, MsgType 1, msg data 0I (1377) DUAL_BLINK_QUEUE: Task ID 1, MsgType 2, msg data 0I (2377) DUAL_BLINK_QUEUE: Task ID 1, MsgType 2, msg data 0I (2427) DUAL_BLINK_QUEUE: Task ID 1, MsgType 2, msg data 1I (3367) DUAL_BLINK_QUEUE: Task ID 2, MsgType 2, msg data 0I (3477) DUAL_BLINK_QUEUE: Task ID 1, MsgType 2, msg data 2I (4477) DUAL_BLINK_QUEUE: Task ID 1, MsgType 2, msg data 2I (4527) DUAL_BLINK_QUEUE: Task ID 1, MsgType 2, msg data 3I (5527) DUAL_BLINK_QUEUE: Task ID 1, MsgType 2, msg data 3I (5577) DUAL_BLINK_QUEUE: Task ID 1, MsgType 2, msg data 4I (6417) DUAL_BLINK_QUEUE: Task ID 2, MsgType 2, msg data 1I (6627) DUAL_BLINK_QUEUE: Task ID 1, MsgType 2, msg data 5I (7627) DUAL_BLINK_QUEUE: Task ID 1, MsgType 2, msg data 5...

The first two messages received from the two tasks are the CPU core (MsgType 1), followed by a continuous stream of counter values. Because of the task 1’s faster blink cycle, there are far more messages from task 1 than task 2.

I’m working on setting up an interrupt handler, and breaking up the code into smaller source modules. So until the next post, happy coding!

more esp32-s3-devkitc-1 programming notes

This is a follow on to this post: /2022/03/23/simple-programming-of-the-esp32-s3-devkitc-1-using-esp-idf/

These are a collection of notes more than anything else.

  • I cleaned up the code a bit, then added a few more comments to make things a bit clearer to me for future development. When you write for an audience of one, you tend to forget to leave important bread crumbs for the future when the code isn’t as fresh as when you first wrote it. Overall it grew a few lines, but all in the name of clarity and possible future growth.
  • I tried to use the Espressif Visual Studio Code plugin to develop with the ESP32-S3 board, but it wouldn’t see my initial Espressif supplied ESP-IDF installation.
  • I tried to use PlatformIO in Visual Studio Code, but it failed to connect with my existing ESP-IDF installation. I will admit PlatformIO looks quite powerful, and I’ve seen a few YouTube videos showing how to use it when writing code for older ESP32 chips and boards. But it won’t work with ESP-IDF 4.4 installed on my computer.

What I now have is a simpler development environment. I use Visual Studio to open and manage my ESP32-S3 projects. There’s no auto-complete, no C++ language checking, just syntax code highlighting. I have a shell opened in a vertical window on the right where I run the idf.py tool with necessary command line arguments, and watch the results. You can see the screen cap of a run at the top of the post. Primitive? Perhaps. But effective enough for my modest needs. I will probably give PlatformIO another run in the very near future.

As for the code, it’s a bit cleaner and I changed a few camel-case names to snake-case. The only camel-case names left are all from FreeRTOS, which makes spotting them a lot easier. Snake-case is the coding standard used in ESP-IDF, so I’ll be using that in my own code examples as well.

#include <stdio.h>#include "freertos/FreeRTOS.h"#include "freertos/task.h"#include "driver/gpio.h"#include "esp_log.h"#include "led_strip.h"#include "sdkconfig.h"// Task 1.//static void task_blink_neo_pixel(void * pvParameters) {static led_strip_t *pStrip_a;pStrip_a = led_strip_init(CONFIG_BLINK_LED_RMT_CHANNEL, CONFIG_BLINK_GPIO, 1);pStrip_a->clear(pStrip_a, 50);static int led_colors[][4] = {{0, 32, 0, 0},  // red{0, 0, 32, 0},  // green{0, 0, 0, 32},  // blue{0, 32, 0, 16}, // violet{0, 0, 32, 32}  // cyan};static uint NUM_COLORS = sizeof(led_colors)/sizeof(led_colors[0]);// Stay in an endless loop. Don't return from this task.//while(true) {for( int color_select = 0; color_select < NUM_COLORS; ++color_select ) {// Set NeoPixel LED to a color using RGB from 0 (0%) to 255 (100%)// for each color.//pStrip_a->set_pixel(pStrip_a, led_colors[color_select][0],  led_colors[color_select][1],  led_colors[color_select][2],  led_colors[color_select][3]);// Refresh the strip to send data//pStrip_a->refresh(pStrip_a, 100);vTaskDelay(500 / portTICK_PERIOD_MS);// Set NeoPixel LED dark by clearing all its pixels.pStrip_a->clear(pStrip_a, 50);vTaskDelay(500 / portTICK_PERIOD_MS);}}}// Task 2.//static void task_blink_led(void * pvParameters) {#define BLINK_GPIO46_LED 46gpio_reset_pin(BLINK_GPIO46_LED);// Set the GPIO as a push/pull outputgpio_set_direction(BLINK_GPIO46_LED, GPIO_MODE_OUTPUT);// Stay in an endless loop. Don't return from this task.//while (true) {gpio_set_level(BLINK_GPIO46_LED, true);   // LED onvTaskDelay(100 / portTICK_PERIOD_MS);gpio_set_level(BLINK_GPIO46_LED, false);  // LED offvTaskDelay(100 / portTICK_PERIOD_MS);gpio_set_level(BLINK_GPIO46_LED, true);   // LED onvTaskDelay(100 / portTICK_PERIOD_MS);gpio_set_level(BLINK_GPIO46_LED, false);  // LED offvTaskDelay(2700 / portTICK_PERIOD_MS);}}void app_main(void) {static const char *TAG = "DUAL_BLINK";int core = xPortGetCoreID();ESP_LOGI(TAG, "app_main running on core %i", core);ESP_LOGI(TAG, "CONFIG_BLINK_GPIO %i", CONFIG_BLINK_GPIO);// Create task 1.//TaskHandle_t xHandle1 = NULL;static uint8_t ucParameterToPass1 = 1;xTaskCreate(task_blink_neo_pixel,"BlinkNeoPixel",// human readable task name.2048,   // stack size in bytes.&ucParameterToPass1,tskIDLE_PRIORITY,&xHandle1);configASSERT(xHandle1);// Create task 2.//TaskHandle_t xHandle2 = NULL;static uint8_t ucParameterToPass2 = 1;xTaskCreate(task_blink_led,"BlinkLED", // human-readable task name.2048,   // stack size in bytes.&ucParameterToPass2,tskIDLE_PRIORITY,&xHandle2);configASSERT(xHandle2);// Stay in an endless loop. Don't return from this task.//while (true) {vTaskDelay(1000 / portTICK_PERIOD_MS);}}