Using extern storage classifier to share variable across files in Embedded C

So, I was working on an ESP32 WROOM 32 chip for a hobby project. Specifically, working with connecting Xbox Controllers to the chip.

The said chip is dual core and runs on FreeRTOS for scheduling tasks.

I had never used any RTOS, and just jumped in on an example project. I split the modules of the work into multiple tasks, as modularization is one of my favourite things to do.

I split the app into many sections, the relevant ones to this post are

  • Logging task
  • Bluetooth HID task

The Bluetooth HID task deals with finding and connecting to controllers. After connection, the task is supposed to inform the logging unit that outputs the said event to an OLED screen.

Now, I am not that knowledgeable on multi-threading, but I do know race conditions occur if you try to write to global vars and read from the same in 2 separate tasks.

I looked up the docs and found that xQueueCreate will create a queue that is thread safe. I went and implemented the same and happily went along my work.

Till this point all the tasks that I defined, were present in main.c in the main component, the same place where I put

log_string_queue = xQueueCreate(10, sizeof(char*));

Bluetooth HID was an interesting library, interesting and complicated. Suffice to say that a whole lot of stuff is abstracted away there. I wanted to log device connection event which was in another file.

Here I thought, aha, queues will make this seamless. I just have to make this variable static and put it in an include file like so


include "freertos/FreeRTOS.h"
include "freertos/task.h"

static  xQueueHandle log_string_queue;


Including logging.h to both main.c and bluetooth_helper.c should do the trick.

Alas, when i tried to send a message with this in bluetooth_helper.c

asprintf(&lg, "Dev Connected"); xQueueSend(log_string_queue, &lg, 0); 

I got a chip restart with this error.

assertion "pxQueue" failed: file "IDF/components/freertos/queue.c", line 764, function: xQueueGenericSend
 abort() was called at PC 0x4015b1c7 on core 1
 0x4015b1c7: __assert_func at /builds/idf/crosstool-NG/.build/xtensa-esp32-elf/src/newlib/newlib/libc/stdlib/assert.c:62 (discriminator 8)
 Backtrace:0x400d5297:0x3ffd41100x40090f21:0x3ffd4130 0x400965e2:0x3ffd4150 0x4015b1c7:0x3ffd41c0 0x4009170e:0x3ffd41f0 0x400db515:0x3ffd4230 0x40171a96:0x3ffd42e0 0x40171ed3:0x3ffd4320 0x40171ff4:0x3ffd4370 0x400940d9:0x3ffd4390
 0x400d5297: panic_abort at /home/edwin/esp/esp-idf/components/esp_system/panic.c:354
 0x40090f21: esp_system_abort at /home/edwin/esp/esp-idf/components/esp_system/esp_system.c:126
 0x400965e2: abort at /home/edwin/esp/esp-idf/components/newlib/abort.c:46
 0x4015b1c7: __assert_func at /builds/idf/crosstool-NG/.build/xtensa-esp32-elf/src/newlib/newlib/libc/stdlib/assert.c:62 (discriminator 8)
 0x4009170e: xQueueGenericSend at /home/edwin/esp/esp-idf/components/freertos/queue.c:764 (discriminator 1)
 0x400db515: hidh_callback at /home/edwin/coding/xbox-switch-controller-bridge/esp32/build/../main/bluetooth_helper.c:126 (discriminator 13)
 0x40171a96: handler_execute at /home/edwin/esp/esp-idf/components/esp_event/esp_event.c:145
 0x40171ed3: esp_event_loop_run at /home/edwin/esp/esp-idf/components/esp_event/esp_event.c:582 (discriminator 3)
 0x40171ff4: esp_event_loop_run_task at /home/edwin/esp/esp-idf/components/esp_event/esp_event.c:115 (discriminator 15)
 0x400940d9: vPortTaskWrapper at /home/edwin/esp/esp-idf/components/freertos/port/xtensa/port.c:168

Well shit...

This took a while to get the fix to. To be honest, it was a moment of serendipity that I recalled about extern

Turns out, even if static means that it is not destroyed when out of scope( It doesn't mean that the value is accessible across files. I got the best explanation from here . In short, static keyword limit- the scope of that variable to that very translation unit(that file).

This is because each C file is compiled separately and then linked together. The code that I used was essentially using 2 different instances of the variables for each file.

The solution was to set the files up like this:

xQueueHandle log_string_queue;
log_string_queue = xQueueCreate(10, sizeof(char*));
extern xQueueHandle log_string_queue;
asprintf(&lg, "Dev Connected"); xQueueSend(log_string_queue, &lg, 0); 

extern Keyword

In a normal C program, every symbol needs to be recognised by the compiler when producing .obj files. That is, all the variables should have corresponding declarations before they are used, whether they are in an included file or in the .c source itself.

This implies that truly global variables(declare in only one file) is not possible. Extern is a way around it. With it, you are declaring that this file that has extern is not the owner and hence doesn't need to declare it. That way, the compiler knows to postpone the resolution of that variable/function to the linking stage of compliation. At that point, the linker will lookup other obj files to find the declaration of the variable.

One other use-case of extern is to avoid cyclic dependencies. For example consider the following three files:

extern SemaphoreHandle_t mutex_bluetooth_scan_running; // included from .h file

xSemaphoreTake(mutex_bluetooth_scan_running, portMAX_DELAY);
extern SemaphoreHandle_t mutex_bluetooth_scan_running; // included from .h file

xSemaphoreTake(mutex_bluetooth_scan_running, portMAX_DELAY);
SemaphoreHandle_t mutex_bluetooth_scan_running; // declared here

mutex_bluetooth_scan_running = xSemaphoreCreateMutex(); // Initialized here

Now, the variable is owned by main.c, but needs to be used by both hid_scanner.c and gpio.c

Using extern allows C to support this scenario without causing cyclic dependencies.

Related Posts

Leave a Reply

%d bloggers like this: