Beware that this post is related to an SDK in maintenance mode
More Info: Consider nRF Connect SDK for new designs
This post is older than 2 years and might not be relevant anymore
More Info: Consider searching for newer posts

Custom Board definition

I am starting an experimentation effort with Nordic Mesh SDK. I am using Segger Embedded Studio.

I can open/compile/download the Mesh SDK examples.

I wish to start to modify the supplied examples, and I have a few question related on how to and "best practice" to achieve the following.

  1. Create a copy of an existing example, so I can mess it up without affecting the whole MeshSDK and the linked nRF5_SDK. Can you tell me if it is enough to copy the example folder and rename it? (may be because all the files are linked with relative paths with no exceptions?). Or there is a way to export a project and re-import it along with some sort of refactoring to change its root name?
  2. I need to work using different board definitions (NOT only with a single new one named/referenced as BOARD_CUSTOM). Of course I can create all the correct "boardnames.h" files (deriving them from the closest one from your SDK: pca10056.h for instance) in the nRF5_SDK_15.2.0_9412b96\components\boards folder, but then what? Do I need to modify the board.h and board.c files to add the elif switches and potentially add some specific code to the board.c to support custom functionalities? I really feel not at ease when messing with the SDK files, I rather wish to keep my own custom code completely separated from the SDK... Can you comment/suggest something?
  3. Once I will have the custom board definitions ready and available by means of some #define like BOARD_PROJECT_A, BOARD_PROJECT_B, which is the correct way to select the board? I read that one can have the #define inherited by MANUALLY messing inside the project definition XML file (light_switch_dimming_server_nrf52840_xxAA_s140_6_1_0.emProject for instance)?? Is that the preferred way to work? Can you comment/suggest something?
  4. Same question as #3: how to switch a project from nRF52840 chip to nRF52832 and vice versa? Again need I to manually modify the XML project file?

I tried to find how to proceed browsing the developer network and the internet, but I found partial answers and usually not recently posted.

Can you give me the right hints and/or point me to the right documentation for these "basic" topics?

Many thanks in advnace and

 Best Regards

Davide De Nardis

Parents
  • It sounds a bit strange that you would have to do this manually. What change is this, I might be able to find a way to do that from within SES. If it is only what preprocessor define to use for the board then you could just change it in the "Common" options for the project. You should find "Preprocessor Definitions" there under "Code" -> "Preprocessor".

    This is poor answer. Why do you (Nordic) not give detailed, clear direction on how to start out with designing for custom boards? The SDK docs give the briefest treatment:

    Adding support for a custom board

    To add support for a custom board, you must create a custom board support file with the name custom_board.h. This file must be located in a directory in the Include path. You can then select to use the custom board by adding the define statement #define BOARD_CUSTOM.

    The easiest way to create the custom_board.h file is to start with an existing platform definition file (for example pca10040.h) and adapt it to your needs.

    Link: https://infocenter.nordicsemi.com/index.jsp?topic=%2Fcom.nordic.infocenter.sdk5.v15.0.0%2Fnrf51_getting_started.html&cp=4_0_0_1

    That's it?

    Is that really all there is to say?!

    Detailed application notes would be extremely helpful. For each peripheral and for helping users adapt the SDK to their designs.

    How on earth you are successful as a company with this kind of support is beyond me.

  • Hi,

    Thank you for your valuable feedback. While we strive to provide the documentation, guides and support needed for reaching your goals, we are aware that we have blind spots. Pointing out those blind spots is appreciated!

    Are there particular points in the "Adding support for a custom board" documentation that are unclear, or is the main issue that we do not have a full tutorial-like document?

    Regards,
    Terje

Reply
  • Hi,

    Thank you for your valuable feedback. While we strive to provide the documentation, guides and support needed for reaching your goals, we are aware that we have blind spots. Pointing out those blind spots is appreciated!

    Are there particular points in the "Adding support for a custom board" documentation that are unclear, or is the main issue that we do not have a full tutorial-like document?

    Regards,
    Terje

Children
  • I'm glad you feel it's valuable.

    To be clear, no, it's not that the documentation is unclear, it's that you don't provide any guidance on this aspect at all.

    This is the fundamental part of embedded development!

    You provide examples in the SDK but they are not thoroughly documented and commented. 

    The frequently cited advice to copy and paste an example amounts to little more than cargo cult programming. 

    As I posted elsewhere, you don't have any application notes. I offer this as an example of how it is done well:

    ww1.microchip.com/.../TB3215-Getting-Started-with-SPI-DS90003215.pdf

    If you delve into their documentation there are examples for each peripheral with schematic diagrams and code samples. The examples are minimal and clear and follow a consistent format.

    This layer of documentation is vital and is yet it is entirely absent from the Nordic canon.

  • Hi,

    Thanks, I think I understand a bit more what you are referring to now. From what I can understand, this is in part a difference in approach, and in part - as you write - lack of guidance on one or more given aspects.

    What we do have, is for each SoC the full hardware documentation for the peripherals, with registers, and in some instances also a couple of lines of code. See e.g. the SPI master documentation in the nRF52832 Product Specification.

    What we also do have, is documentation for the high-level libraries in the SDK, both general information about the libraries and API documentation. See e.g. the SPI transaction manager library documentation (with some example lines of code) and the SPI transaction manager API documentation. From what I understand, this is one point where better overall documentation including to-the-point code excerpts are lacking.

    Then there are the examples in the SDK. See e.g. the SPI Transaction Manager Example. Today these work as "quickly getting something up and running". But they are less suited for "figuring out how things work" / "build custom applications from", in particular due to lack of documentation providing a thorough understanding of how each component works.

    For some aspects of development we have tutorial like guides, including guides for some aspects related to the SDK. For some of the issues raised in this thread this could be a good place to add more guides.

    For some aspects of development we have Application Notes and White Papers, albeit predominantly hardware related. There seems to be some missing pieces for the software side of things.

    We are aware that we have little or no low-level application notes / guides / documentation for how to use peripherals/registers directly. Simply put, as is, we expect anyone interested in direct registry manipulation to read the product specifications and look at the low-level header files directly. We expect the vast majority of developers to use our higher level libraries and never need to manipulate registers directly. Therefore, the higher levels of the SDK is where we primarily put our documentation efforts.

    So in summary, I see that there are some missing connections on the higher SDK levels, regarding getting an understanding of how things fit together. I also get what you mean by "cargo culture programming", in particular when starting on a new project. This is definitely something I will bring up internally, and see what we can do to improve on. Any further feedback might be useful in that regard. If you have the slightest feeling or idea that I might have gotten something wrong here, then please let me know.

    Regards,
    Terje

  • To demonstrate, taking the example nRF5_SDK_15.3.0\examples\peripheral\gpiote

    Starting with a gpio pin initialisation:
    err_code = nrf_drv_gpiote_out_init(GPIO_OUTPUT_PIN_NUMBER, &config);

    So let's look at GPIO_OUTPUT_PIN_NUMBER:
    #define GPIO_OUTPUT_PIN_NUMBER BSP_LED_0 /**< Pin number for output. */
    We need to know that BSP_LED_0 is defined in the board file (eg: pca10040.h) but that this is actually determined by a macro in boards.h which has to be set in the common section of private configurations in the project options.
    #define BSP_LED_0 LED_1
    And:
    #define LED_1 17
    This is 3 layers of abstraction plus some fairly obscure menu setting configuration tweaks and the generation of a new header file that needs to reside in a specific sdk folder and to follow a specific format for a simple gpio pin definition. And we haven't begun to unpack the function yet.

    Then for the function nrf_drv_gpiote_out_init:
    err_code = nrf_drv_gpiote_out_init(GPIO_OUTPUT_PIN_NUMBER, &config);

    we need to know:
    nrf_drv_gpiote_out_config_t config = GPIOTE_CONFIG_OUT_TASK_TOGGLE(false);

    GPIOTE_CONFIG_OUT_TASK_TOGGLE is re-assigned by a macro in nrf_drv_gpiote.h:
    /** @brief Macro for forwarding the new implementation. */
    #define GPIOTE_CONFIG_OUT_TASK_TOGGLE NRFX_GPIOTE_CONFIG_OUT_TASK_TOGGLE

    which in turn is defined by another macro in nrf_drv_gpiote.h:
    /**@brief Macro for configuring a pin to use the GPIO OUT TASK to toggle the pin state.
    * @details The initial pin state must be provided. */
    #define NRFX_GPIOTE_CONFIG_OUT_TASK_TOGGLE(init_high) \
    { \
    .init_state = init_high ? NRF_GPIOTE_INITIAL_VALUE_HIGH : NRF_GPIOTE_INITIAL_VALUE_LOW, \
    .task_pin = true, \
    .action = NRF_GPIOTE_POLARITY_TOGGLE, \
    }

    Looking at NRF_GPIOTE_INITIAL_VALUE_HIGH for example, this is defined as type nrf_gpiote_outinit_t in nrf_gpiote by typedef enum as:
    NRF_GPIOTE_INITIAL_VALUE_HIGH = GPIOTE_CONFIG_OUTINIT_High

    In nrf_bitfields.h GPIOTE_CONFIG_OUTINIT_High we find that:
    #define GPIOTE_CONFIG_OUTINIT_High (1UL)

    Similarly, NRF_GPIOTE_INITIAL_VALUE_LOW resolves to:
    #define GPIOTE_CONFIG_OUTINIT_Low (0UL)

    Now for NRF_GPIOTE_POLARITY_TOGGLE, again defined as type nrf_gpiote_polarity_t in nrf_gpiote.h, we find:
    NRF_GPIOTE_POLARITY_TOGGLE = GPIOTE_CONFIG_POLARITY_Toggle

    where GPIOTE_CONFIG_POLARITY_Toggle is defined in nrf52.bitfields as:
    #define GPIOTE_CONFIG_POLARITY_Toggle (3UL)

    Now to look at the function call:
    nrf_drv_gpiote_out_init(GPIO_OUTPUT_PIN_NUMBER, &config);

    A macro re-definition in nrfdrive_gpiote.h:
    /** @brief Macro for forwarding the new implementation. */
    #define nrf_drv_gpiote_out_init nrfx_gpiote_out_init

    takes us to a function in nrfx_gpiote.c:
    nrfx_err_t nrfx_gpiote_out_init(nrfx_gpiote_pin_t pin,
    nrfx_gpiote_out_config_t const * p_config)

    To start let's look at NRFX_ASSERT(pin < NUMBER_OF_PINS):

    NUMBER_OF_PINS is defined as a macro in nrf_gpio.h:
    #define NUMBER_OF_PINS (P0_PIN_NUM)
    where P0_PIN_NUM is another macro in nrf52832_peripherls.h
    #define P0_PIN_NUM 32

    next NRFX_ASSERT(m_cb.state == NRFX_DRV_STATE_INITIALIZED);
    where NRFX_DRV_STATE_INITIALIZED is a typedef nrfx_drv_state_t in nrfx_common.h to 1

    then we have a check
    if (pin_in_use(pin))
    that refers to a struct m_cb of type gpiote_control_block_t in nrfdrive_gpiote.c

    if that passes we get to the task configuration that checks if we already set p_config->task_pin

    THEN we get to the channel assignment
    int8_t channel = channel_port_alloc(pin, NULL, true);

    that seems to be assigning the next available channel and we get to:
    nrf_gpiote_task_configure((uint32_t)channel, pin, p_config->action, p_config->init_state);

    that resolves to a static inline function definition in nrf_gpiote.h:
    __STATIC_INLINE void nrf_gpiote_task_configure(uint32_t idx, uint32_t pin,
    nrf_gpiote_polarity_t polarity,
    nrf_gpiote_outinit_t init_val)
    {
    NRF_GPIOTE->CONFIG[idx] &= ~(GPIOTE_CONFIG_PORT_PIN_Msk |
    GPIOTE_CONFIG_POLARITY_Msk |
    GPIOTE_CONFIG_OUTINIT_Msk);

    NRF_GPIOTE->CONFIG[idx] |= ((pin << GPIOTE_CONFIG_PSEL_Pos) & GPIOTE_CONFIG_PORT_PIN_Msk) |
    ((polarity << GPIOTE_CONFIG_POLARITY_Pos) & GPIOTE_CONFIG_POLARITY_Msk) |
    ((init_val << GPIOTE_CONFIG_OUTINIT_Pos) & GPIOTE_CONFIG_OUTINIT_Msk);
    }

    Finally we start to get to the bones of it and we can get some idea of how the function relates to the register settings.
    And then we get to the pin configuration:
    if (p_config->init_state == NRF_GPIOTE_INITIAL_VALUE_HIGH)
    which is about whether we want our gpio pin configured as a n output and set high or low by default.

    So nrf_gpio_pin_set(pin) resolves to another static inline in nrf_gpio.h:

    __STATIC_INLINE void nrf_gpio_pin_set(uint32_t pin_number)
    {
    NRF_GPIO_Type * reg = nrf_gpio_pin_port_decode(&pin_number);
    nrf_gpio_port_out_set(reg, 1UL << pin_number);
    }

    where nrf_gpio_pin_port_decode is another static inline returning a pointer of type NRF_GPIO_Type that does an assert to check if our pin is in the range for the port another macro in nrf_gpio.h
    #define NUMBER_OF_PINS (P0_PIN_NUM)
    where P0_PIN_NUM is another macro in nrf52832_peripherals.h
    #define P0_PIN_NUM 32

    nrf_gpio_port_out_set is another static inline in nrf_gpio.h that resolves to
    __STATIC_INLINE void nrf_gpio_port_out_set(NRF_GPIO_Type * p_reg, uint32_t set_mask)
    {
    p_reg->OUTSET = set_mask;
    }

    then we get to
    nrf_gpio_cfg_output(pin);

    that resolves to another static inline
    __STATIC_INLINE void nrf_gpio_cfg_output(uint32_t pin_number)
    {
    nrf_gpio_cfg(
    pin_number,
    NRF_GPIO_PIN_DIR_OUTPUT,
    NRF_GPIO_PIN_INPUT_DISCONNECT,
    NRF_GPIO_PIN_NOPULL,
    NRF_GPIO_PIN_S0S1,
    NRF_GPIO_PIN_NOSENSE);
    }
    where nrf_gpio_cfg is another static inline:
    __STATIC_INLINE void nrf_gpio_cfg(
    uint32_t pin_number,
    nrf_gpio_pin_dir_t dir,
    nrf_gpio_pin_input_t input,
    nrf_gpio_pin_pull_t pull,
    nrf_gpio_pin_drive_t drive,
    nrf_gpio_pin_sense_t sense);

    and
    pin_configured_set(pin);
    which is:
    __STATIC_INLINE void pin_configured_set(uint32_t pin)
    {
    nrf_bitmask_bit_set(pin, m_cb.configured_pins);
    }
    where nrf_bitmask_bit_set is defined in nrf_bitmask.h:
    __STATIC_INLINE void nrf_bitmask_bit_set(uint32_t bit, void * p_mask)
    {
    uint8_t * p_mask8 = (uint8_t *)p_mask;
    uint32_t byte_idx = BITMASK_BYTE_GET(bit);
    bit = BITMASK_RELBIT_GET(bit);
    p_mask8[byte_idx] |= (1 << bit);
    }

    and more macros in nrf_bitmask.h:
    #define BITMASK_BYTE_GET(abs_bit) ((abs_bit)/8)
    #define BITMASK_RELBIT_GET(abs_bit) ((abs_bit) & 0x00000007)


    So when you finally comprehend all of that you have done what amounts to:

    void pin_config(uint32_t channel_id, uint32_t pin, nrf_gpiote_polarity_t polarity, nrf_gpiote_outinit_t init_val)
    {
    //nrf_gpiote_task_configure
    NRF_GPIOTE->CONFIG[channel_id] &= ~(GPIOTE_CONFIG_PORT_PIN_Msk |
    GPIOTE_CONFIG_POLARITY_Msk |
    GPIOTE_CONFIG_OUTINIT_Msk);

    NRF_GPIOTE->CONFIG[channel_id] |= ((pin << GPIOTE_CONFIG_PSEL_Pos) & GPIOTE_CONFIG_PORT_PIN_Msk) |
    ((polarity << GPIOTE_CONFIG_POLARITY_Pos) & GPIOTE_CONFIG_POLARITY_Msk) |
    ((init_val << GPIOTE_CONFIG_OUTINIT_Pos) & GPIOTE_CONFIG_OUTINIT_Msk);

    NRF_GPIO_Type * p_reg = (NRF_GPIO_Type*) NRF_P0_BASE;

    //nrf_gpio_cfg_output
    p_reg->PIN_CNF[pin] = (NRF_GPIO_PIN_DIR_OUTPUT << GPIO_PIN_CNF_DIR_Pos)
    | (NRF_GPIO_PIN_INPUT_DISCONNECT << GPIO_PIN_CNF_INPUT_Pos)
    | (NRF_GPIO_PIN_NOPULL << GPIO_PIN_CNF_PULL_Pos)
    | (NRF_GPIO_PIN_S0S1 << GPIO_PIN_CNF_DRIVE_Pos)
    | (NRF_GPIO_PIN_NOSENSE << GPIO_PIN_CNF_SENSE_Pos);

    //nrf_gpio_pin_set
    p_reg->OUTSET = 1UL << pin;
    }

    And usage looks like:
    pin_config(1, PN_LED, GPIOTE_CONFIG_POLARITY_Toggle, 1);

    So which is easier to explain, to write, maintain and understand?

  • Hi,

    I am sorry for the long delay on this one.

    I agree that the number of layers, macros, files involved is likely to make the code that you refer to harder to maintain and fully understand.The question then becomes, for the user of the library, how deep one needs to go down the rabbit hole in order to use the API at the highest level. (Where by "higher" I mean further away from hardware.) Ideally, at the high level, the complexity involved for the end product developer should be no more, no less, than what is needed at that level of abstraction. Where this is not the case it may at least be possible to correct with support and better documentation.

    Regarding the NRFX indirection, this is in order for the same low-level library to be used by both SDKs, as part of a transition over to nRF Connect SDK.

    Regards,
    Terje

Related