This guide can't cover everything, but it covers the rules we enforce on PR reviews.
Formatting is handled automatically by .clang-format — run it before committing.
3rd party libraries and managed components are not our concern.
This set is not final. If you want to change something, open a ticket.
- Code is public. Keep it simple, keep it readable.
- If you need a comment to explain a line, the line is too clever.
- Leave references to datasheets or standards you are implementing.
- Every literal with domain meaning must be a named
#define. Exceptions:0,1,NULL,true,false.
Public functions use the module name as prefix. Static functions drop it.
subghz_receiver_start() // public
cc1101_set_frequency() // public
process_pulse() // static
build_histogram() // static
| Scope | Rule | Example |
|---|---|---|
| Local | snake_case |
pulse_duration_us |
| Static | s_ prefix |
s_is_running |
| Global (avoid) | g_ prefix |
g_system_state |
| Boolean | is_, has_, can_ |
is_running, has_data |
| Output parameter | out_ prefix |
out_result, out_buffer |
Names must say what they hold. Single-letter variables only as loop counters in short loops.
All types use snake_case with _t suffix, prefixed by the module name.
subghz_data_t
cc1101_preset_t
subghz_spectrum_line_t
Callback typedefs use _cb_t suffix.
subghz_rx_done_cb_t
Values are UPPER_SNAKE_CASE with module prefix. Include a _COUNT sentinel when iterable.
typedef enum {
SUBGHZ_MODE_SCAN = 0,
SUBGHZ_MODE_RAW,
SUBGHZ_MODE_COUNT
} subghz_mode_t;UPPER_SNAKE_CASE. GPIO pins follow the pattern GPIO_NAME_PIN.
#define RMT_RESOLUTION_HZ 1000000
#define GPIO_CS_PIN 3
#define GPIO_SCL_PIN 9
UPPER_SNAKE_CASE with a _COUNT companion macro.
static const uint32_t HOP_FREQUENCIES[] = {
433920000,
868350000,
315000000,
};
#define HOP_FREQUENCIES_COUNT (sizeof(HOP_FREQUENCIES) / sizeof(HOP_FREQUENCIES[0]))- Directories:
^[0-9a-z_]+$ - Source files:
^[0-9a-z_]+\.[ch]$ - File name is a prefix for its content:
subghz_receiver.c→subghz_receiver_start()
| Suffix | Meaning |
|---|---|
_init |
Initialize module/peripheral. Does not allocate. |
_alloc |
Allocate and init an instance. Returns pointer. |
_free |
Deinit and release an instance. |
_start |
Begin an async operation or task. |
_stop |
Request graceful shutdown. |
_get |
Read a value. No side effects. |
_set |
Write a value. |
Use <stdint.h> fixed-width types for hardware-related code.
| Use | Type |
|---|---|
| Register values, byte buffers | uint8_t |
| Frequencies, addresses | uint32_t |
| Signed pulse timings | int32_t |
| Buffer sizes, array indices | size_t |
| GPIO pins | gpio_num_t |
| Booleans | bool |
| Error codes | esp_err_t |
Never rely on implicit int. Always be explicit about types.
All structs use typedef with _t suffix. Internal structs (file-scope) follow the same rule, prefixed with the module.
- Each function does one thing. A long function that does one thing is fine. A short function that does three things is not.
- Maximum 4 parameters. Group into a config struct if you need more.
- If two functions share more than 70% of their body, unify them with a parameter.
-
Public functions return
esp_err_twhen they can fail. Usevoidonly when failure is impossible. -
Every
malloc/heap_caps_malloc/heap_caps_aligned_allocmust be checked forNULL, logged withESP_LOGE, and handled (return error orgoto cleanup). -
Always use
free()to deallocate memory, regardless of which allocation function was used (malloc,heap_caps_malloc,heap_caps_aligned_alloc). Theheap_caps_aligned_freeis deprecated since ESP-IDF v5.5. -
Use
goto cleanupfor functions with multiple resources:
esp_err_t do_work(void) {
esp_err_t ret = ESP_OK;
uint8_t *buf_a = NULL;
uint8_t *buf_b = NULL;
buf_a = malloc(BUF_A_SIZE);
if (buf_a == NULL) {
ESP_LOGE(TAG, "Failed to allocate buf_a");
ret = ESP_ERR_NO_MEM;
goto cleanup;
}
buf_b = malloc(BUF_B_SIZE);
if (buf_b == NULL) {
ESP_LOGE(TAG, "Failed to allocate buf_b");
ret = ESP_ERR_NO_MEM;
goto cleanup;
}
// work
cleanup:
free(buf_b);
free(buf_a);
return ret;
}- Always validate array indices before access.
array[i + 1]requiresi < size - 1. - Never use
strcatin a loop. Usesnprintfwith offset tracking. - Use
breakto exit loops. Never assign the loop counter to force exit. - Use explicit
== NULLfor pointer checks.!valueis only for booleans.
- Any variable accessed by more than one task must be protected by a mutex or atomic.
volatilealone is not thread-safe. - Document buffer ownership: who allocates, who frees, when.
- Never free a buffer while a peripheral (RMT, SPI, DMA) may still be using it.
- All shared state must be initialized before
xTaskCreate. - Tasks must release all resources before
vTaskDelete. Set handles toNULLafter deletion.
- All headers use
#ifndefinclude guards. - All headers wrap declarations in
extern "C"for C++ compatibility. - Include order, separated by blank lines:
- Corresponding header
- C standard library
- ESP-IDF / FreeRTOS
- Project headers
- Every
.cfile definesstatic const char *TAG = "MODULE_NAME";at the top. - All debug output goes through
ESP_LOGx. Never useprintf.
| Level | When |
|---|---|
ESP_LOGE |
Errors that break functionality |
ESP_LOGW |
Recoverable issues |
ESP_LOGI |
Lifecycle events (init, start, stop) |
ESP_LOGD |
Debug details |
components/
module_name/
include/
module_name.h
module_name_types.h
module_name.c
CMakeLists.txt
1. License header
2. Corresponding header include
3. C standard library includes
4. ESP-IDF / FreeRTOS includes
5. Project includes
6. #define constants
7. Static types
8. Static variables
9. Static function forward declarations
10. Public function implementations
11. Static function implementations
// Copyright (c) 2025 HIGH CODE LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// ...
#ifndef MODULE_NAME_H
#define MODULE_NAME_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include "esp_err.h"
#define MODULE_NAME_MAX_ITEMS 64
#define MODULE_NAME_TIMEOUT_MS 5000
/**
* @brief Operating modes for the module.
*/
typedef enum {
MODULE_NAME_MODE_IDLE = 0,
MODULE_NAME_MODE_ACTIVE,
MODULE_NAME_MODE_COUNT
} module_name_mode_t;
/**
* @brief Configuration for module initialization.
*/
typedef struct {
uint32_t frequency;
uint8_t channel;
module_name_mode_t mode;
bool is_looping;
} module_name_config_t;
/**
* @brief Result data returned after processing.
*/
typedef struct {
uint32_t value;
uint8_t status;
const char *label;
} module_name_result_t;
/**
* @brief Callback invoked when processing is complete.
*
* @param result Pointer to the result data. Valid only during callback scope.
* @param ctx User context passed during registration.
*/
typedef void (*module_name_done_cb_t)(const module_name_result_t *result, void *ctx);
/**
* @brief Initialize the module.
*
* @param config Pointer to configuration. Caller retains ownership.
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_ARG if config is NULL
* - ESP_ERR_NO_MEM if allocation fails
*/
esp_err_t module_name_init(const module_name_config_t *config);
/**
* @brief Start the module processing task.
*
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_STATE if already running
*/
esp_err_t module_name_start(void);
/**
* @brief Stop the module and release resources.
*/
void module_name_stop(void);
/**
* @brief Check if the module is currently running.
*
* @return true if running, false otherwise.
*/
bool module_name_is_running(void);
/**
* @brief Get the latest result.
*
* @param[out] out_result Pointer to store the result. Must not be NULL.
* @return
* - ESP_OK on success
* - ESP_ERR_INVALID_STATE if no result available
*/
esp_err_t module_name_get_result(module_name_result_t *out_result);
/**
* @brief Set a value in the module.
*
* @param value The value to set.
*/
void module_name_set_value(uint32_t value);
/**
* @brief Allocate a new module instance.
*
* @return Pointer to instance, or NULL on failure.
*/
module_name_result_t *module_name_result_alloc(void);
/**
* @brief Free a previously allocated instance.
*
* @param result Pointer to instance. NULL is safe.
*/
void module_name_result_free(module_name_result_t *result);
#ifdef __cplusplus
}
#endif
#endif // MODULE_NAME_H// Copyright (c) 2025 HIGH CODE LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// ...
#include "module_name.h"
#include <string.h>
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "other_module.h"
static const char *TAG = "MODULE_NAME";
#define TASK_STACK_SIZE 4096
#define TASK_PRIORITY 5
#define TASK_CORE 1
#define PROCESS_DELAY_MS 100
typedef struct {
int32_t *items;
size_t count;
} module_name_buffer_t;
static TaskHandle_t s_task_handle = NULL;
static SemaphoreHandle_t s_mutex = NULL;
static volatile bool s_is_running = false;
static module_name_config_t s_config = {0};
static module_name_result_t s_result = {0};
static void process_task(void *pvParameters);
static esp_err_t process_data(const int32_t *data, size_t count, module_name_result_t *out_result);
static void reset_state(void);
esp_err_t module_name_init(const module_name_config_t *config) {
if (config == NULL) {
return ESP_ERR_INVALID_ARG;
}
s_config = *config;
if (s_mutex == NULL) {
s_mutex = xSemaphoreCreateMutex();
if (s_mutex == NULL) {
ESP_LOGE(TAG, "Failed to create mutex");
return ESP_ERR_NO_MEM;
}
}
ESP_LOGI(TAG, "Initialized — mode: %d, freq: %lu",
(int)config->mode, (unsigned long)config->frequency);
return ESP_OK;
}
esp_err_t module_name_start(void) {
if (s_is_running) {
return ESP_ERR_INVALID_STATE;
}
BaseType_t ret = xTaskCreatePinnedToCore(
process_task, "module_name", TASK_STACK_SIZE, NULL,
TASK_PRIORITY, &s_task_handle, TASK_CORE);
if (ret != pdPASS) {
ESP_LOGE(TAG, "Failed to create task");
return ESP_ERR_NO_MEM;
}
return ESP_OK;
}
void module_name_stop(void) {
s_is_running = false;
}
bool module_name_is_running(void) {
return s_is_running;
}
esp_err_t module_name_get_result(module_name_result_t *out_result) {
if (out_result == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(PROCESS_DELAY_MS)) != pdTRUE) {
return ESP_ERR_TIMEOUT;
}
*out_result = s_result;
xSemaphoreGive(s_mutex);
return ESP_OK;
}
void module_name_set_value(uint32_t value) {
if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(PROCESS_DELAY_MS)) == pdTRUE) {
s_result.value = value;
xSemaphoreGive(s_mutex);
}
}
module_name_result_t *module_name_result_alloc(void) {
module_name_result_t *result = malloc(sizeof(module_name_result_t));
if (result == NULL) {
ESP_LOGE(TAG, "Failed to allocate result");
return NULL;
}
memset(result, 0, sizeof(module_name_result_t));
return result;
}
void module_name_result_free(module_name_result_t *result) {
free(result);
}
static void process_task(void *pvParameters) {
s_is_running = true;
ESP_LOGI(TAG, "Task started");
int32_t *buffer = malloc(MODULE_NAME_MAX_ITEMS * sizeof(int32_t));
if (buffer == NULL) {
ESP_LOGE(TAG, "Failed to allocate buffer");
goto cleanup;
}
while (s_is_running) {
// Simulate filling buffer
size_t count = MODULE_NAME_MAX_ITEMS;
module_name_result_t result = {0};
if (process_data(buffer, count, &result) == ESP_OK) {
if (xSemaphoreTake(s_mutex, pdMS_TO_TICKS(PROCESS_DELAY_MS)) == pdTRUE) {
s_result = result;
xSemaphoreGive(s_mutex);
}
}
vTaskDelay(pdMS_TO_TICKS(PROCESS_DELAY_MS));
}
cleanup:
free(buffer);
reset_state();
vTaskDelete(NULL);
}
static esp_err_t process_data(const int32_t *data, size_t count, module_name_result_t *out_result) {
if (data == NULL || count == 0 || out_result == NULL) {
return ESP_ERR_INVALID_ARG;
}
uint32_t sum = 0;
for (size_t i = 0; i < count; i++) {
sum += (uint32_t)abs(data[i]);
}
out_result->value = sum / count;
out_result->status = 1;
out_result->label = "processed";
ESP_LOGD(TAG, "Processed %d items, avg: %lu", (int)count, (unsigned long)out_result->value);
return ESP_OK;
}
static void reset_state(void) {
s_task_handle = NULL;
s_is_running = false;
ESP_LOGI(TAG, "Task stopped");
}