C Refresher

Structs & Enums

Structs group related data together — you'll see them everywhere in ESP-IDF for configuration, events, and data passing.


Structs (Structures)

A struct allows you to combine multiple variables (of different types) under a single name. Think of it like a simplified Class in Python or C++, but it only holds data, not functions.

c
1#include <stdio.h>
2#include <stdbool.h>
3
4// Define a struct type using typedef
5typedef struct {
6 int gpio_num;
7 int brightness;
8 bool is_on;
9} led_config_t;
10
11int main() {
12 // Initialize the struct using designated initializers
13 led_config_t led = {
14 .gpio_num = 2,
15 .brightness = 128,
16 .is_on = true
17 };
18
19 // Access members using the dot (.) operator
20 printf("GPIO: %d, Brightness: %d\n", led.gpio_num, led.brightness);
21
22 return 0;
23}

Passing Structs to Functions

You rarely pass an entire struct into a function by value (which makes a full copy). You almost always pass a pointer to the struct, and use the -> operator to access its members.

c
1void configure_led(const led_config_t *config) {
2 // gpio_set_direction(config->gpio_num, GPIO_MODE_OUTPUT);
3 if (config->is_on) {
4 // gpio_set_level(config->gpio_num, 1);
5 }
6}

Nested Structs

Structs can contain other structs. This is incredibly common in ESP-IDF for organizing complex telemetry or configuration data.

c
1#include <stdio.h>
2#include <stdint.h>
3
4typedef struct {
5 float temperature;
6 float humidity;
7} dht_reading_t;
8
9typedef struct {
10 dht_reading_t sensor;
11 char device_id[16];
12 uint32_t timestamp;
13} telemetry_t;
14
15int main() {
16 telemetry_t data = {
17 .sensor = { .temperature = 25.5, .humidity = 60.0 },
18 .device_id = "XIAO-S3-01",
19 .timestamp = 1713427200
20 };
21
22 printf("Temp: %.1f\n", data.sensor.temperature);
23 return 0;
24}

Enums (Enumerations)

An enum is a way to create a set of named integer constants. It makes your code highly readable because you can use descriptive words instead of arbitrary numbers.

In ESP-IDF, almost all error codes, hardware modes, and states are defined as enums.

c
1#include <stdio.h>
2
3typedef enum {
4 ESP_OK = 0, // Success
5 ESP_FAIL = -1, // Generic failure
6 ESP_ERR_NO_MEM = 0x101, // Out of memory
7 ESP_ERR_TIMEOUT = 0x102, // Operation timed out
8} esp_err_t;
9
10int main() {
11 esp_err_t result = ESP_FAIL; // Simulating a failure
12
13 if (result != ESP_OK) {
14 printf("Operation failed with code: 0x%x\n", result);
15 }
16
17 return 0;
18}

#define vs const vs enum

You will see all three patterns used for constants in ESP-IDF. Here is when to use each:

1. #define — Preprocessor Macro

c
1#define LED_GPIO 2
2#define MAX_RETRIES 3
3#define TIMEOUT_MS 5000
  • How it works: Text replacement before compilation — the compiler never sees LED_GPIO, only 2.
  • Pros: Works everywhere (array sizes, switch cases, #if conditions). It is the ESP-IDF convention for all menuconfig values.
  • Cons: No type checking, does not show up as a named symbol in a debugger.

2. const — Typed Constant

c
1const uint8_t led_gpio = 2;
2const int max_retries = 3;
  • How it works: A real variable that the compiler type-checks, but guarantees cannot be modified.
  • Pros: Type-safe, shows up in the debugger, compiler catches misuse.
  • Cons: Cannot be used in switch/case labels or array sizes in standard C.

3. enum — Named Integer Constants

c
1typedef enum {
2 LED_GPIO = 2,
3 MAX_RETRIES = 3,
4 TIMEOUT_MS = 5000,
5} app_config_t;
  • How it works: Integer constants grouped together.
  • Pros: Type-safe (with typedef), debugger shows names, works perfectly in switch/case.
  • Cons: Only supports integers (no floats or strings).

When in doubt, follow ESP-IDF's lead

Use #define for numeric constants and enum for related integer groups. ESP-IDF uses static const char *TAG = "MAIN"; for logging strings.

Previous
Pointers & Memory