Custom Device (custom)
Overview
The custom device integrates board-specific hardware or software objects into the BMGR unified device management pipeline. Unlike other device types, custom has no built-in driver logic — BMGR automatically generates a dedicated configuration struct from the fields under config: in the YAML and includes it in the unified lifecycle management.
During initialization, BMGR looks up the registered init entry by device name:
Init entry registered: calls the board-level init function; the user handle returned by the function is retrieved via
esp_board_manager_get_device_handle().No init entry registered: initialization does not report an error, but the handle is
NULL; the configuration struct is still accessible directly viaesp_board_manager_get_device_config().
Typical use cases:
Peripheral chips with custom drivers (PMICs, sensors, actuators, etc.) where board code provides the init function.
Exposing board-level configuration parameters (I2C address, GPIO number, default values, etc.) to the application layer without any additional initialization logic.
Minimal Configuration
custom does not require additional board_peripherals.yaml entries; peripherals are referenced on demand.
board_devices.yaml:
devices:
- name: axp2101_power_manager
chip: axp2101
type: custom
config:
frequency: 400000
i2c_addr: 0x34
peripherals:
- name: i2c_master
Code Generation Output
After running idf.py bmgr -b <board>, BMGR generates a dedicated configuration struct and its initialization values for each type: custom entry in the build artifact components/gen_bmgr_codes/gen_board_device_custom.h.
Naming Convention
The struct is named dev_custom_{sanitized_name}_config_t, where {sanitized_name} is obtained by replacing all illegal C identifier characters (non-letters, non-digits, non-underscores) in the device name with _; an extra _ prefix is added when the name starts with a digit.
Examples:
name: axp2101_power_manager→dev_custom_axp2101_power_manager_config_tname: my-sensor→dev_custom_my_sensor_config_tname: 2channel→dev_custom__2channel_config_t
Fixed Fields
Each generated struct always starts with three descriptor fields whose values come from the top-level YAML attributes:
const char *name; /*!< Device name */
const char *type; /*!< "custom" */
const char *chip; /*!< Chip name, or "unknown" if omitted */
User Configuration Fields
Each key-value pair under config: generates one struct field. The field name is derived from the key using the same sanitization rule; the field type is automatically inferred from the value:
YAML value type |
C type |
Description |
|---|---|---|
|
|
|
Integer |
|
Smallest fitting type selected in order; signed types are preferred |
Floating-point |
|
Any decimal number |
String |
|
Literal stored in rodata |
Dict (nested object) |
Inline sub-struct |
Generated recursively; sub-struct name is |
List (scalar types) |
C array of the corresponding type |
e.g. |
List (dicts) |
Array of sub-structs |
All dict keys are merged into one struct before generating the array |
Other |
|
Complex types degrade to pointer; value is |
Note
The peripherals key inside config: is reserved for the peripheral list and will not be generated as a regular field.
Full Generation Example
The YAML below is taken from the motor_driver entry in test_apps/components/test_board_e/board_devices.yaml. It shows how nested dicts, lists of dicts, and lists of scalars map to C structs:
- name: motor_driver
chip: mx16161
type: custom
config:
motors:
- motor_id: 1
gpio_motor_ina: 26
gpio_motor_inb: 27
- motor_id: 2
gpio_motor_ina: 28
gpio_motor_inb: 29
- motor_id: 3
gpio_motor_ina: 23
gpio_motor_inb: 22
test_bool: true
test_int: 123
test_float: 3.14
test_string: "hello"
test_int_list: [1, 2, 3, 4, 5]
test_dict:
sub_val_1: 10
sub_val_2: "nested"
After running idf.py bmgr -b <board>, the typedefs generated in gen_board_device_custom.h (actual output, excerpted):
typedef struct {
int8_t motor_id;
int8_t gpio_motor_ina;
int8_t gpio_motor_inb;
} dev_custom_motor_driver_motors_t;
typedef struct {
int8_t sub_val_1;
const char *sub_val_2;
} dev_custom_motor_driver_test_dict_t;
typedef struct {
const char *name; /*!< Custom device name */
const char *type; /*!< Device type: "custom" */
const char *chip; /*!< Chip name */
dev_custom_motor_driver_motors_t motors[3];
bool test_bool;
int8_t test_int;
float test_float;
const char *test_string;
int8_t test_int_list[5];
dev_custom_motor_driver_test_dict_t test_dict;
} dev_custom_motor_driver_config_t;
The following rules can be verified: all integers are inferred as int8_t (values within the signed range select signed types first); the motors list of dicts expands to dev_custom_motor_driver_motors_t motors[3]; the test_dict nested dict generates a sub-struct of the same name; since this device declares no peripherals:, no peripheral fields appear in the top-level struct.
Peripheral Fields
When peripherals are declared under the top-level peripherals:, peripheral-related fields are appended to the end of the struct:
Single peripheral: appends
uint8_t peripheral_countandconst char *peripheral_name.Multiple peripherals: appends
uint8_t peripheral_countandconst char *peripheral_names[MAX_PERIPHERALS].
MAX_PERIPHERALS is fixed at 4 (defined in esp_board_manager/devices/dev_custom/dev_custom.h).
Registering an Init Entry
To have BMGR automatically call board-level driver code during initialization, implement init/deinit functions in the board source file and register them with the CUSTOM_DEVICE_IMPLEMENT macro:
#include "dev_custom.h"
#include "gen_board_device_custom.h" /* Generated configuration struct header */
static int my_sensor_init(void *cfg, int cfg_size, void **device_handle)
{
dev_custom_my_sensor_config_t *config = (dev_custom_my_sensor_config_t *)cfg;
/* Initialize hardware using the fields in config */
my_sensor_handle_t *handle = malloc(sizeof(my_sensor_handle_t));
if (!handle) {
return -1;
}
/* ... initialize the driver ... */
*device_handle = handle;
return 0;
}
static int my_sensor_deinit(void *device_handle)
{
free(device_handle);
return 0;
}
/* The first argument must exactly match the device name in board_devices.yaml */
CUSTOM_DEVICE_IMPLEMENT(my_sensor, my_sensor_init, my_sensor_deinit);
CUSTOM_DEVICE_IMPLEMENT uses GCC attributes to place the descriptor into a special linker section (.esp_board_entries_desc). At runtime, BMGR performs a linear scan of this section to find the init/deinit functions by device name.
CMakeLists.txt Requirements
CUSTOM_DEVICE_IMPLEMENT depends on the linker retaining the symbols in its linker section. The requirement varies by file location:
Placed in the board directory (alongside
board_devices.yaml): thegen_bmgr_codescomponent CMakeLists.txt generated by BMGR already setsWHOLE_ARCHIVE, so no manual action is needed.m5stack_cores3/power_manager.cuses this approach.Placed in a standalone application component: that component must set
WHOLE_ARCHIVEinidf_component_register, otherwise the linker will discard the unreferenced linker section contents:idf_component_register( SRCS "my_device.c" INCLUDE_DIRS "." WHOLE_ARCHIVE )
Application-Side Access
After BMGR initialization, access custom devices as follows.
Get configuration struct (available whether or not an init entry is registered):
#include "gen_board_device_custom.h"
dev_custom_axp2101_power_manager_config_t *cfg = NULL;
esp_err_t ret = esp_board_manager_get_device_config(
"axp2101_power_manager", (void **)&cfg);
if (ret == ESP_OK) {
uint32_t freq = cfg->frequency;
uint8_t addr = cfg->i2c_addr;
}
Get user handle (valid only when an init entry is registered and the init function succeeds):
my_sensor_handle_t *handle = NULL;
esp_err_t ret = esp_board_manager_get_device_handle(
"my_sensor", (void **)&handle);
Warning
When no entry with a matching name is registered, esp_board_manager_get_device_handle returns an error rather than NULL, because the internal handle itself is NULL. Config-only usage should use only esp_board_manager_get_device_config() and must not call esp_board_manager_get_device_handle().
All Fields
devices:
- name: my_custom_device # Required; unique identifier. The first argument of the driver registration macro must match this
type: custom # Fixed value
chip: esp32s3 # Optional; chip name. Defaults to "unknown" in the struct when omitted
version: 1.0.0 # Optional; YAML schema identifier. Omitting it is equivalent to default
init_skip: false # Optional; when true, esp_board_manager_init() skips this device
config:
sensor_id: 0x01 # int → int8_t (0 ≤ value ≤ 127)
sample_rate: 100 # int → int8_t
enable_filter: true # bool → bool
filter_cutoff: 50.5 # float → float
timeout_ms: 1000 # int → int16_t
unit: "celsius" # str → const char *
peripherals: # Optional; references declared peripherals
- name: i2c_master
depends_on: [] # Optional; initialization dependencies (list of device names)
dependencies: # Optional; additional component dependencies
esp_driver_i2c:
require: public
version: "^2.0.0"
Component Dependencies
custom has no fixed dependencies. Declare driver components in the device’s dependencies field as needed, such as esp_driver_i2c, esp_driver_gpio, esp_driver_ledc, or esp_driver_adc.
Required Peripherals
peripheral type |
role / format |
Required |
Purpose |
|---|---|---|---|
Any supported peripheral |
Determined by the peripheral itself |
Depends on board implementation |
Board hardware resources referenced in the init function |
None |
None |
Optional |
Config-only usage; no peripheral handle needed |
Reference Code
esp_board_manager/boards/m5stack_cores3/power_manager.c: Complete custom device implementation with init/deinit functions andCUSTOM_DEVICE_IMPLEMENTregistration, demonstrating the full flow of reading I2C address and peripheral handle from the generated struct.esp_board_manager/test_apps/main/test_dev_custom.cesp_board_manager/test_apps/components/test_board_e/board_devices.yamlesp_board_manager/devices/dev_custom/dev_custom.cesp_board_manager/devices/dev_custom/dev_custom.h
Board Reference
esp_board_manager/boards/m5stack_cores3/board_devices.yaml+power_manager.c:axp2101_power_manager, with full init/deinit entry registered, driving the AXP2101 PMIC via fields such asconfig->i2c_addrandconfig->peripheral_name.esp_board_manager/test_apps/components/test_board_e/board_devices.yaml: Complete test case with nested structs, lists, and lists of dicts.
Notes
For common YAML field rules, see YAML Conventions and Syntax.
The first argument of
CUSTOM_DEVICE_IMPLEMENTmust exactly match the devicename, case-sensitively.Components containing
CUSTOM_DEVICE_IMPLEMENTmust setWHOLE_ARCHIVEin CMakeLists.txt, otherwise the linker will discard the descriptor.When no matching init entry is registered, initialization will not fail, but
esp_board_manager_get_device_handle()returns an error;esp_board_manager_get_device_config()still returns a valid configuration struct.The
peripherals:list limit isMAX_PERIPHERALS = 4; behavior is undefined when more peripherals than this limit are declared.After modifying YAML, re-run
idf.py bmgr -b <board>.
Debugging Tips
API Reference
The configuration struct for a custom device is auto-generated by BMGR during code generation based on the config: fields, and is defined in the build artifact components/gen_bmgr_codes/gen_board_device_custom.h. The struct naming rule is dev_custom_{sanitized_name}_config_t; this header must be included before use.
Use esp_board_manager_get_device_config() to retrieve the configuration struct; if an init entry is registered, use esp_board_manager_get_device_handle() to retrieve the user handle.
The registration macro CUSTOM_DEVICE_IMPLEMENT and the base configuration struct dev_custom_base_config_t are defined in esp_board_manager/devices/dev_custom/dev_custom.h.