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
ifndef _CUSTOM_LOGGING
define _CUSTOM_LOGGING
include "freertos/FreeRTOS.h"
include "freertos/task.h"
static xQueueHandle log_string_queue;
endif /* _CUSTOM_BLUETOOTH_HELPER */
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(https://www.geeksforgeeks.org/static-variables-in-c/) It doesn't mean that the value is accessible across files. I got the best explanation from here https://www.youtube.com/watch?v=ySY_FlA7EvA . 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);
xSemaphoreGive(mutex_bluetooth_scan_running);
extern SemaphoreHandle_t mutex_bluetooth_scan_running; // included from .h file
xSemaphoreTake(mutex_bluetooth_scan_running, portMAX_DELAY);
xSemaphoreGive(mutex_bluetooth_scan_running);
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.