ESP-IDF is the official extension for the ESP32 MCU family. ESP-IDF uses FreeRTOS
These are my notes on the ESP-IDF. I followed these docs:
ESP-IDF Documentation FreeRTOS Documentation
What is FreeRTOS?
freeRTOS is a real-time operating system kernel for embedded devices, like the ESP32
- a kernel has a few responsibilities:
- scheduling, deciding which task runs, for how long, on which core
- memory managing: allocating RAM resource to provide virtual memory to ensure tasks don’t interfere with each other.
- syscalls: provides a secure low-level interface for users to request services from the kernel.
- it is a preemptive scheduler, where the scheduler has the ability to interrupt a running task if a more important one needs the cpu.
Using vTaskDelay()
vTaskDelay doesn’t count in milliseconds. it counts in ticks.
- a “tick” is like a single heartbeat of a processor’s scheduler
to make it easy, we use a macro, called
pdMS_TO_TICKSto translate milliseconds to ticks, where ticks is what the esp32 understands.
Here, the cpu will wait for exactly 1 second:
vTaskDelay(pdMS_TO_TICKS(1000));Using xTaskCreatePinnedToCore()
this is a more “serious” version of xTaskCreate(). It is literally just xTaskCreate() but with one extra parameter at the end. The pinned to core version allows the task to be specified which core should be running the task.
- if using esp-now, then all wifi/tcp comms run on core 0, since the esp32 has two cores.
- thus, we need to create user-based tasks like a servo moving on core 1
xTaskCreatePinnedToCore(
my_espnow_task, // Function
"ESPNOW_Task", // Name (debug purposes)
4096, // Stack
NULL, // Params to pass
5, // Priority, 0 is lowest
NULL, // Handle (delete or pause)
1 // pinned to core 1
);tasks need their own stack since the CPU constantly switches between different tasks, so the CPU needs to know some background information per task to be able to handle them all.
When creating tasks, they will always terminate once they reach the closing brace.
- it is important to note that
app_mainis a task itself, so once it reaches the closing brace the task will gracefully cleanup and end.
FreeRTOS tasks
syntax:
void task_name(void *params)
{
while (true) {
// mandatory delay to avoid wasting resources
vTaskDelay(pdMS_TO_TICKS(1000));
}
}void *paramsis a pointer to any data (structs, arrays, int, etc that we want the task to have access to.
ESP-IDF API
ESP-IDF main function
the main function in an esp-idf project is:
app_main(void)the key important thing to note though, is that app_main is a task, which means that any other tasks started by app_main will keep running, even if we return from app_main
LEDC API
The LEDC library is the most common way to control servos. It is more commonly used to control LED brightness but can be used to control servos/motors via PWM signal.
Setting up a channel is done in 3 steps:
- setting the timer configuration
ledc_timer_config_t dc_motor_config = {
.speed_mode = LEDC_HIGH_SPEED_MODE,
.duty_resolution = LEDC_TIMER_10_BIT,
.timer_num = LEDC_TIMER_1,
.freq_hz = 5000,
};
ESP_ERROR_CHECK(ledc_timer_config(&dc_motor_config));duty_resolution is the range of possible speeds. the formula for resolution depends on the bit resolution. The formula to find out the range is:
Example: take the 10 bit resolution from .duty_resolution from 1), the range would be from 0-1023
- setting the channel config
ledc_channel_config_t dc_motor_a = {
.gpio_num = DC_MOTOR_A_PWMA_PIN,
.speed_mode = LEDC_HIGH_SPEED_MODE,
.channel = LEDC_CHANNEL_3,
.timer_sel = LEDC_TIMER_1,
.duty = 0, // init the motor at 0 avg speed
};
ESP_ERROR_CHECK(ledc_channel_config(&dc_motor_a));- setting PWM using duty cycle
Log Levels
esp_log_level_set("ESPNOW", ESP_LOG_WARN);this api function is saying only show warnings or errors in the serial output, ignore all fail/success messages.
- this is because ESPNOW errors will fill the serial output, since it is trying to send packets but will call the callback function a lot, and if we are trying to see other output we will need to suppress the errors with the log level function.
static and volatile in C
Static
if you put static before a global variable or function, it limits the scope to just that specific file.
volatile
by marking a variable as volatile, you tell the compiler: do NOT optimize this code and re-read the value from memory every time it’s used since the hardware or another task might have changed it behind your back.
Using Non-Volatile storage (critical for wifi/ESP-NOW)
NVS is a library from the ESP-IDF sdk that is designed to store data in a ESP32’s flash memory so that information is preserved even if we pull power.
- we can think about it like a hard drive. it saves important information like calibration or mac addresses
usually, boilerplate code is run FIRST in app_main():
// Initialize NVS
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK( nvs_flash_erase() );
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
// do setups here...
// setup wifi..
// setup esp-now...ESP_ERROR_CHECK Macro
Some notes for when we need to use ESP_ERROR_CHECK:
ESP_ERROR_CHECK()is used onesp_err_ttypes, such asESP_OKorESP_FAIL. (these are just placeholders for 0 and 1 internally)ESP_ERROR_CHECK()triggers a system reboot and internally calls the abort() function.- if
ESP_ERROR_CHECK()fails and is called in a any function, it dies mid-execution we can see thatESP_ERROR_CHECKis very destructive in a way. Only useESP_ERROR_CHECKwhen setting up important things, like wifi, or NVS initializations. Wrappingesp_now_sendin the check will be poor design, since we don’t want the entire system to crash but rather retry sending a message.
- if
Importance of Macros
Macros are defined by the syntax #define.
they have the following properties.
- preprocessed by the compiler before compilation
- macros are expanded during compile time, this allows for no function call overheads, no return addresses, and no stack frame
- macro constants like
#define MAX 10don’t take up any RAM, unlikeconst uint8_t
Unicast VS Broadcast
in networking, the difference between broadcast and unicast is the following:
- unicast is a one-to-one connection, and the receiver sends an ACK (acknowledgement) saying “recieved” which triggers the status in the send callback function.
- Broadcast is like shouting in a crowded room, and are sending messages to EVERYONE within a range (meters).
- using a universal MAC address, there is no acknowledgement, since the ESP32 that is sending the packets doesn’t wait for any “thanks”
- there is no encryption for broadcasting, since it uses a generic MAC address, like
FF:FF:FF:FF:FF
Using Static Variables
Static memory is different from the stack memory, it is persistent.
- if we declare a variable static, it lives in the static section of RAM until the program terminates.
Rules/characteristics of static:
- a static variable/function is private to that specific file, which provides encapsulation
- it persists in RAM
- static variables are zeroed by the compiler at initialization.
Don’t put any static variables in a .h file, only use them in .c files. This is because if we include the .h somewhere else, then that file that includes the .h library will get its own separate static variables.
Example in a .h, like motor_speed.h:
static int motor_speedif we #include motor_speed.h in main.c AND motor_speed.c, the compiler will make two separate copies of motor_speed in both .c files, when the expected result is actually wanting only 1 motor_speed variable!
static variable declaration inside a function
a static variable stays alive between calls.
Using extern
extern is useful if we want to use a variable declared in a file in another variable somewhere else.
Rules:
- definition of an extern variable goes in
.c - declaration of extern variable goes in
.hExample: in main.h:
extern int motor_speed; // declarationin main.c:
int motor_speed = 0 // definition (memory allocation occurs here)in another file, like motor_speed.c, we can use motor_speed however we like, such as modifying it or using it inside a function.
do NOTE that extern variables are a type of global variable, and globals are bad! This is because globals can read/write from anywhere
FreeRTOS Queues
FreeRTOSDocumentation Reference
Queues in FreeRTOS are useful for producer and consumer code. It can be thought of as the queue data structure.
Normal Queues
A normal queue dynamically allocates its size during runtime of a program.
- A normal queue is dynamic because memory is not hard-coded. if we no longer need the queue, we have the choice of deleting it via
vQueueDelete()
NOTE: since a normal queue is dynamic, there is a potential chance of fragmentation, where RAM is split into tiny blocks after multiple allocations and frees and cannot allocate a size of 200 bytes when 200 bytes are “apparent” in memory but are split in pieces.
Static Queues
static queues require the use of a buffer, since static queues take up some chunk of RAM during compile time and don’t require on the OS to allocate memory during runtime.
We need the following:
- the packet struct
- queue length (ie how many items will we let the queue hold?)
- array of
uin8_t’s to store the raw bytes (note:uin8_ttype is raw bytes) - queue handler - pointer to
QueueHandle_t
once created, a static queue works like a normal queue, and we can use the same API calls:
xQueueSend()- and
xQueueRecieve()
how to use a queue
the queue handler, denoted as QueueHandle_t is used to hide the implementation of the actual queue. Use the queue handler to handle all the things related to the queue we are using.
Callback Functions
there are two callback functions that are useful for debugging and confirming.
- send (
esp_now_register_send_cb) - receive callback (
esp_now_register_recv_cb) note that callbacks functions are a type of interrupt-style events. the system triggers them, so no need to create new tasks (threads) for them since they will get called when data is received/sent
ESP_IDF data types and macros
Using size_t
size_t is a special container that is an UNSIGNED integer to represent size of objects in memory. it is portable, since each device might have different sizes for size_t.
the size_t variable itself takes up 4 bytes, but it can “hold” any number from 0 to 4 billion.
using pdPASS
pdPASS is a defined number that states that something has been recieved in a queue. note that pdPASS only works when using xQueueRecieve.
alternatively, pdFAIL is the opposite, ie there is no data to be grabbed from the queue.
using portMAX_DELAY
portMAX_DELAY means that it will wait indefinitely (i.e. block) until an item arrives in a queue. this means that in a task, it will block until some item has been pushed into the queue.
idf.py and CMake
Since the idf.py tool is installed somewhere globally, I am using the oh-my-zsh alias get_idf in zsh terminal to grab the tools needed to compile, build, and flash via CLI. if idf.py doesn’t show up as a valid command in the current directory, run the following:
idf.py fullclean
idf.py build flash monitoruse this if there are errors to fully wipe build and build it from scratch.
- this will only work if idf.py is initiated inside the project repository.