This discussion has been locked.
You can no longer post new replies to this discussion. If you have a question you can start a new discussion

Creation of custom DTS file for describing custom device connections


I need to create a custom dts for a custom board where I need to define the pins used for interfacing to some external devices. The generic "gpio-leds"/"gpio-keys", to my best understanding, do not feed my needs as I need more values per entry.

I would like to be able to create something like:

// Just for illustration, everything is hypothetical.
/ {
    model = "Huge bird detector NRF52832";
    compatible = "savanna,struthio-camelus-detector";
    detectors {
		compatible = "sc-detector";
		// Definition of first channel
		det0: det_0 {
		    irqpin = <&gpio0 13 (GPIO_PULL_UP | GPIO_ACTIVE_LOW)>;
		    // ainpin and dettype are integer values, ainpin is an index
		    // into an ADC channel, dettype is just a flag.
			ainpin = <1>;
			dettype = <1>
			label = "S-C detector channel 1";
	// ...etc...

Where can I find detailed information covering this topic and, possibly, some examples?

I'm using nRF Connect SDK version 1.9.1 and Visual Studio Code.

Thanks a lot in advance.


  • Hello,

    We do not have any documentation showing you connecting custom board with external device. You have to implement this on source code.


    Best Regards,

    Kazi Afroza Sultana

  • Hello Kazi, thanks for your replay.

    Maybe I'll need to clarify my question.

    The question is not related to how to [physically] interface with all the devices I'm integrating in the hardware and how to control them or how to write some source code.

    What I need your support for is for pointing me to where I can find information/manuals/samples about creating and using custom DTS' (device tree source files), so I can create different "boards", with their associated DTS', to address properly the hardware differences that I can have in different board variants.

    If I, for instance, include the hypothetical device tree source from my original question in my board's dts file, the devicetree_unfixed.h file created by the toolchain does not contain all the information, e.g. it does not include any information for irqpin.

    That clearly means I'm missing some crucial steps in the workflow, especially the YAML file for bindings.

    I've created my custom boards in a different root directory, outside of the nRF Connect SDK root folder.

    1) Where, relative to the custom board's root directory, should I locate the customised YAML file required by my custom DTS file?

    2) Is there any example/template available for such YAML file, besides taking as reference the ones available in the zephyr tree for other devices/manufacturers?

    3) If I want to create driver files for the devices I want to integrate, where should I place them, relative to the board's root directory? I want to keep my code as isolated as possible from the SDK code to be able to switch between SDK versions the smoothest as possible.

    4) How do I do it for creating configuration options for the inclusion or not of those drivers in the project during compile, like with CONFIG_PUT_MY_STUFF=y in the prj.conf file?

    Thanks in advance.


  • Hi,

    I didn't find much information about this, so I'll share here what I've found out for others with similar questions.

    For creating a custom DTS file format it is necessary to create the bindings file, YAML. The custom bindings file must be located in route dts\bindings\, relative to the custom board's root path. This is the case when you want to keep all the custom board specifics independent from the nRF Connect SDK for being able to use different sdk versions.

    The format for the YAML file is described in Zephyr's documentation, and here's an example of minimum definition we need for supporting the dts source from my original post:

    description: The parent node, a detectors list
    compatible: "savanna,struthio-camelus-detector"
        description: Struthio Camelus Detector, child node
                  type: array
                  required: true
              required: true
              type: string
              description: Human readable string describing the device

    The type: array in property irq-pins establishes that this property will be expanded to an array of values.

    For simplifying things, I introduced a label "n" in the detectors node, and added another child node, like here below. The label can have any convenient value, I just used n since I was tired of typing useless code.

    n: detectors {
    	model = "Huge birds detector NRF52832";
        compatible = "savanna,struthio-camelus-detector";
    	// Definition of first channel
    	det0: det_0 {
    	    irq-pins = <13 (GPIO_PULL_UP)>;// | GPIO_ACTIVE_LOW)>;
    		label = "S-C detector channel 1";
    	// Definition of second channel
    	det1: det_1 {
    	    irq-pins = <17 (GPIO_PULL_UP)>;// | GPIO_ACTIVE_LOW)>;
    		label = "S-C detector channel 2";

    The text "savanna," will be interpreted a manufacturer id by Zephyr, and the tools will complain for unknown manufacturer. The file named vendor-prefixes.txt, located under zephyr\dts\bindings, contains the list of known manufacturer id's. You may want to add an entry there for your manufacturer id and name.

    Using the values generated by Zephyr's dts files compiler is not exactly straight forward when starting from zero, and the documentation provides only a limited set of examples.

    One intermediate generated file that becomes very handy for understanding what's behind scenes if the header file named devicetree_unfixed.h. This file is created under path zephyr\include\generated, relative to build directory, after making a pristine build, or when rebuilding after modifying the dts file.

    As an example. Here is an excerpt containing some of the macros generated for the first child node, det_0. I'll not dive into it any deeper as it is more or less self-explicative.

    /* Generic property macros: */
    #define DT_N_S_detectors_S_det_0_P_irq_pins {13 /* 0xd */, 16 /* 0x10 */}
    #define DT_N_S_detectors_S_det_0_P_irq_pins_IDX_0 13
    #define DT_N_S_detectors_S_det_0_P_irq_pins_IDX_0_EXISTS 1
    #define DT_N_S_detectors_S_det_0_P_irq_pins_IDX_1 16
    #define DT_N_S_detectors_S_det_0_P_irq_pins_IDX_1_EXISTS 1
    #define DT_N_S_detectors_S_det_0_P_irq_pins_FOREACH_PROP_ELEM(fn) fn(DT_N_S_detectors_S_det_0, irq_pins, 0) \
    	fn(DT_N_S_detectors_S_det_0, irq_pins, 1)
    #define DT_N_S_detectors_S_det_0_P_irq_pins_FOREACH_PROP_ELEM_VARGS(fn, ...) fn(DT_N_S_detectors_S_det_0, irq_pins, 0, __VA_ARGS__) \
    	fn(DT_N_S_detectors_S_det_0, irq_pins, 1, __VA_ARGS__)
    #define DT_N_S_detectors_S_det_0_P_irq_pins_LEN 2
    #define DT_N_S_detectors_S_det_0_P_irq_pins_EXISTS 1
    #define DT_N_S_detectors_S_det_0_P_label "S-C detector channel 1"

    Now in the code, there are several macros defined in Zephy's header file devicetree.h for accessing the values. I'll not dive into them, the documentation is vague for my taste and most code in Zephyr's implementation only uses very simple cases, but it can hint in the right direction if read carefully and with near-photographic memory.

    Using the values in an individualised way is nearly straightforward, like initialising a variable with the first item in the list of values of property irq-pin of node det0, which would be:

    // this will expand to macro DT_N_S_detectors_S_det_0_P_irq_pins, which has value 13
    static int irq_pin = DT_PROP_BY_IDX(DT_NODELABEL(det0), irq_pins, 0);

    Using the child nodes in the form of a list requires some more keystrokes.

    This code creates an array of string pointers with the values of the label property of child nodes, which is directly taken from example code available in Zephyr's DT_FOREACH_CHILD macro documentation:

    #define LABEL_AND_COMMA(node_id) DT_LABEL(node_id),
    const char *child_labels[] = {

    For creating an array of structured elements things go a bit further, and here is where I didn't receive any help or hint neither nor from

    Here is the relevant part of the sample code:

    struct detector_data {
    	int pin;
    	int flags;
    #define NODE_INFO_AND_COMMA(node_id) \
    	{ \
    		.pin=DT_PROP_BY_IDX(node_id, irq_pins, 0),\
    		.flags=DT_PROP_BY_IDX(node_id, irq_pins, 1),\
    const struct detector_data _det_data[] = {

    There are even more subtle things when trying to take full advantage of device trees, like referencing one device/node from others.

    The tool that came very handy for understanding what was occurring behind the scenes, surprisingly, was not available in the context commands list in VS Code, it only appears in the lightbulb when you first click in a macro, then double-click it. And I think there is some bug in VS Code as it does not show-up always, sometimes I need to repeat the procedure one or two more times before the bulb pops-up. Clicking in the bulb will show one single command, "Insert macro", that will replace the macro name with its expansion.

    BTW, the debugger integrated in the nRF Connect SDK for VS Code, if you integrate the previous example code and make no use of _det_data in your code, will not say the constant array was optimised out and will display a group of wrong values when hovering over the constant definition with the mouse. That silly behaviour made me lost a lot of time.

    I hope this is useful for others.