Creating a Custom Zephyr Board for the i.MX RT685

Showing results for 
Search instead for 
Did you mean: 

Creating a Custom Zephyr Board for the i.MX RT685

NXP Pro Support
NXP Pro Support
0 0 3,686

I have been both vocal and enthusiastic about the Zephyr Real Time Operation System (RTOS) and   recently presented my thoughts on some of the “why”.    The i.MX RT 685 now has full platform support in the latest LTS 2 version of Zephyr.     I thought it would be instructive to write an article detailing bring-up of a custom board, the RT685 SuperMonkey.   The goal of the SuperMonkey design was to demonstrate a minimal configuration of an RT685 using a QSPI flash device.    I wanted to demonstrate something a little bit different than what was on the EVK as the flash memory interface is highly configurable.    Along those same lines, I want to show what a bringing up Zephyr would look like on a custom RT685 board.     The flexibility of its flashless architecture means that there are some additional considerations to get the board up and running.

I really like bare minimum examples as they are the best place to start when learning a new tool.   Once one understands the bare minimum configuration, it is easier to pull in more complicated code from other examples.   Often the simplest example will show all the structural elements so one can infer how a system works.   This makes adding functionality easier to approach and there are just enough breadcrumbs to discover more sophisticated functionality. The example provided here is illustrate just that;  A minimal example that will bring up enough to show you that the system is working.  In the case of a Zephyr application on the RT685, I want to show that the serial shell is functional.    I will be inserting tidbits here and there that are a bit beyond the scope of a board port as they will leave breadcrumbs to some unique Zephyr features and workflow.

A bit about Zephyr boards


When performing application development with Zephyr, the concept of a “board” is central.   A powerful feature of Zephyr is to be able to retarget an application to diverse hardware.      Moving code between say a LPC55S69 to an i.MX RT685 can be straightforward as use of the built in APIs will allow quite a bit of functionality to be reused with minimal changes.   When using Zephyr, you can still write all the custom bare metal, raw register access code you want.  You are not limited to the Zephyr API.  However, using the common APIs for UART, I2C, etc. will make the portability easier.  This will reduce the total surface area of code that will need to change between different boards.   The board abstraction is core component of this process so your application code can run on multiple platforms.

So, what exactly is a Zephyr board?   Loosely speaking, it is a folder with  

  • A device tree overlay that brings in a specific SoC and peripheral configurations
  • Kconfig settings for the OS that are important for the board
  • .c code to do things like pin muxes, etc.   
  • Documentation

Basically, everything needed to sit on top of the OS code to get the system up and running in some configuration. An important point to note is that one can use a “built-in” boards to do quite a bit of work before even considering a “custom” board definition.      The device tree system allows application code override/overlay behaviors to “modify” and existing board.     So, you don’t always need a dedicated Zephyr “board” when using your own hardware.   The good news is that there are a lot of boards to use as reference

For the case of the RT685, there is a nuance with external flash setup that requires the use of a custom board.   For most MCUs with internal flash, the built in boards and do 99% of what you need to get started.

Getting Setup for Custom Board Development


Setting up a west manifest


In this article I will be weaving in some other Zephyr concepts that I have found useful.      There are several workflows for managing Zephyr projects.   The RTOS itself is available here:

When 1st getting started,  it is 100% OK to follow the instructions and start hacking on projects in the /samples directory.   However, once I know I will be doing some real work on a project, I like to organize my project as a west manifest:

There is quite a bit of documentation around manifests which can be overwhelming.  In the simplest case, simply think of it as a way of organizing git repositories for your project with some automation.       With a simple manifest, you can specify the repository for your application code and the repository for Zephyr.   West can then be used to fetch everything and get it setup for you. There is a bit of setup time at the beginning but pays dividends down the road.     I like my application code to exist “out of tree” from the Zephyr repo.   The west manifest automates the setup process.  

As an example, checkout the west.yml from here:

This is a minimal manifest that will allow west to initialize a folder and fetch the Zephyr source code.     As projects get more complicated, you can add other dependencies at different revisions levels, etc.  I find this useful for maintaining “internal repositories” that have not be upstreamed into the mainline Zephyr codebase or for anything that I want to keep private while I develop.  You could even specify the repository of your own Zephyr fork that is completely under your control.   The system is quite flexible. In fact, if you do a bit of digging, you will find a west.yml in the main zephyr repo which is a manifest of all the “stuff” that is pulled in. 

The key take-away once the manifest is created, getting your project development environment setup is just two commands (west init, west update) which makes life much easier over the long term. When you have multiple developers and multiple machines.  Feel free to use the rt_super_z repository as starting point.   This repo has the west manifest and a simple hello world project structure that I will be using for my board port.

Seeding your custom board using the RT685 EVK


It is important to note that board porting is well documented here:

You could use that documentation and start from scratch but a copy/paste from a known working configuration that is “close” is a good strategy.          In the Zephyr source tree, you can find the i.MX RT boards under zephyr/boards/arm.


Note that I use Visual Studio Code as an editor when working with Zephyr.   I find it very easy to navigate the source tree when doing driver/board development.  The next step I took was to *copy* the mimxrt685_evk folder into my application project under hello_world/boards/arm.    For initial development of the board port, the hello world test will house my board direction.  I also followed the existing naming conventions to create an “rt_super” board.



It takes just a few minutes to go through the .yaml, defconfig, etc. to get your custom naming in place.   I first focused on remapping text that was MIMXRT685_EVK to RT_SUPER

By default,   Zephyr will look in its default boards directory when you kick off a build.     To change this behavior, simply edit the CMakeLists.txt in your application folder and add this:



This change where Zephyr will look for your board when kicking of a fresh build with “west build”.   This directory could be anywhere but for now I will keep it local to this test project.    I also like to fix the board selection for my project, so I don’t have to specify it on the command line during development of the board package.   Add this to your application CMakeLists.txt to fix the board for the application:



At this point you should be able to kick of a build and Zephyr will use the rt_super board in your local application folder.      Keep in mind that the goal here was to simply copy files and rename items in the .yaml and kconfig files so we have a clean starting point with our “rt_super” board.    None of the internal configuration of the hardware as actually changed at this point.    We essentially have another board that will target the MIMXRT685-EVK just under a different name.   You could have also kept using the default Zephyr boards folder for development.  I chose to put it in my application repository to make it a bit easier to work between a few computers.

Notable Customizations for the SuperMonkey


QSPI and Boot Header


The RT685 represents a bit trickier case with board porting versus a traditional MCU with built in flash memory.   There is a little more work to get Zephyr configured for the particular flash on our custom hardware.    If your design kept the same memory configuration as the EVK (OctalSPI on FlexSPI Port B),  then additional setup is not necessary. Flashless microcontrollers are both a blessing and a curse in this regard.  The flexibility allows for better application customization but requires some more work up front to get your system setup for development

After power up or reset, the RT685 first executes a bootloader that is built into ROM. The RT685 ROM bootloader will attempt to access an external device on FlexSPI Port A or B (depending on the state ISP[2:0] pins) using a 1-bit IO mode.     A special data structure is expected at a 0x400 offset from base of the of external flash memory.  This boot header provides additional configuration details to the bootloader about the attached flash device.  The ROM code will the reconfigure the interface per the boot header configuration.    If the boot header does not exist or is invalid, the ROM will sit and wait for commands over USB or a UART interface.

During a Zephyr build, the boot header needs to get baked in so the generated binary can run properly after reset.      Using the mimxrt685_evk board as a guide, we can modify the boot header for our custom board.      Learning how to navigate the Zephyr source tree is an important aspect to being successful in development zephyr boards and drivers.

Inside of the zephyr/boards/arm/mimxrt685_evk folder is a file Kconfig.board.   



Here we are defining a new configuration option for the board that selects a different option NXP_IMX_RT6XX_BOOT_HEADER.    The boot header is not actually located in the Zephyr source tree, rather in another NXP repository that gets pulled in when you first setup your Zephyr workspace.   Note that this happens automatically when setting up Zephyr initially with west init and west update.    One reason I really like using a west manifest as described previously is that all these dependencies coming in automagically.


In your Zephyr workspace is a modules folder that contains external vendor libraries.   If you dig inside you can find the NXP HAL.     Open modules\hal\nxp\mcux\boards\CMakelists.txt to can see how the configuration option NXP_IMX_RT6XX_BOOT_HEADER is used.



The file flash_config.c contains a data structure with an attribute for the linker to place it in the correct location in flash.     For our custom board, we will need to have our own copy of this file with the custom boot header.      You can place your own version in the custom board directory.



You can read more about the boot header I used here.  To get the new added file included in the build,  you can add a line to the CMakeLists.txt in the custom board folder:



Here we set BOOT_HEADER_ENABLE as it is used in flash_config.c   Note that we are safe to add flash_config.c here.   The version in the NXP HAL directory will not be used as our custom Kconfig.board and Kconfig.defconfig uses alternate the naming for the board symbols.  The flash_config.c in the NXL HALwill not be added to the build as the mimxrt685 board symbols will no longer be enabled. At this point we are setup to our custom boot header.    This step is probably the most complicated which is the result of the “flashless” nature of the RT685 and its boot rom.   


Board Device Tree Overlay


RTC Disable


After getting the boot header configured,  there were a couple modifications needed in the board device tree overlay.      For the most part you can leave this identical to the EVK.    Even if there are peripherals you don’t plan on using,  you can leave the elements be.     There are a couple changes however needed to get the SuperMonkey board to boot.

I chose to not populate a 32.768kHz Crystal on my board.



I found that some of the low level RT600 SoC init code would try to start it up causing boot issues.     To turn this off, I simply had to disable the driver in the board level overlay:




Flash Configuration


The SuperMonkey uses a QSPI device for code storage.  I had to patch the flexspi node in the board device tree overlay to indicate the new flash driver.   As a reference,  I actually used the device tree overlay from the i.MXRT1064 EVK board,   This board uses the same QSPI flash as the SuperMonkey.     Since the RT600 and RT1060 share the same FlexSPI IP,  we can reuse the drives and device tree configuration.     The device tree approach used by Zephyr starts paying dividends quite quickly when one needs to stitch together complicated system and one can reuse existing functionality.



I did patch the flash size and partition sizes to be more appropriate for my smaller capacity QSPI flash.

Board Kconfig.defconfig


There is one last config file that needs patched for our particular flash configuration on the SuperMonkey.   Kconfig.defconfig inside of the board folder has a bunch of additional Kconfig options added you your project.   There is a FLASH_SIZE configuration setting which defaults to a value pulled from the device tree.    Using the syntax from the MIMXRT685 EVK, I modified it to pull from our modified device tree with QSPI.    The Kconfig and device tree systems are very powerful and allows data to be pulled in at build time to provide configuration information to the build system and application code.  



Keep in mind that at the end of the build process, the Kconfig files and device tree overlays turn into header files for your application to use to determine how the system is configured.   Kconfig options can be driven from the device tree.

Pin Multiplexing


NXP provided board ports add a file pinmux.c to the build system.    The purpose of this code is to provide a function that will setup the pin mux on the device to route desired functions to the physical pins on the IC.   Before we take a look at the actual function that does the work, I wanted to point out a macro located at the bottom of the file.



Zephyr is unique in that it provides macros to statically define functionality in a very “clean” manner.   One of those macros is “SYS_INIT”.   This macro adds functions to a list that are call during kernel initialization.   In this case,  rt_super_pinmux_init is called before the kernel is initialized.        The advantage of this approach is that can insert additional behaviors at different points in the boot process without hacking the core kernel code.    This also cleans up your “main” routine as you can specify  “APPLICATION”  in the macro to have functions called right before main().  I find that this approach makes libraries and modules much easier to develop leaving the application code tidy.



In this pin mux implementation, you can see there is logic to conditionally include pin mux setup functions.    This provides an example of using both Kconfig macros and device tree access macros.    I mentioned before that both KConfig and device tree overlay files ultimately get translated into header files with macros that you can use to control your application code.       Feel free to use as much or as little functionality as you want in the pin mux initialization code.      It is a good idea to get any UART pins setup in this stage as you will be able to see printk, logging and shell output from the boot process.      If you want, you can also do pin mux configuration in your application code but keep in mind you have the option to get pins initialized very early in the boot process.

I do want to point out that there are some important changes coming to pin muxing in Zephyr:

Some platforms, such as the Nordic NRF families,  have a great deal of flexibility in pin assignments where any digital function can be routed to any pin.    In this case,  NRF based device trees overlays can specify IO pins when peripherals nodes are instantiated.    This makes modifying boards very simple with the device tree overlay mechanism.   There is work in progress on a new pinctrl API that will allows similar behavior (within the limits of the particular chip) such that pin assignments can be performed in the device tree overlay.    Keep a lookout for this feature in future release.  For now you will need to perform pin muxing in the board initialization functions or in your application code.



This file is used to define “runners” that will allow programming with the west flash.    Feel free to look at other board examples to set this up.  In my case, I liked to program and debug with Segger Ozone and a J-Link instead of using the command line flash mechanism.    You can see my RT600 setup here:

When a build is complete, you can use zephyr.elf in build/zephyr folder of your application.      With this approach you can getting fully feature source level debugging of your Zephyr application.   Ozone even offers the ability to be “thread aware”.

Final Results


Given the additional steps needed for RT600 board port, I wanted to test with a simple project that turns on the shell.  This would give me a workable base to expand the  board port.        My hello_world program turns on the shell and enables a “monkey” command.


 Ascii art sourced from

We now have the fundamental elements in place for the this board port.   I hope you found this information in getting the RT685, or any other part, setup with a custom board in Zephyr.  One you get acclimated to the Zephyr workflow, it isn’t too complicated to built new applications, drivers and boards quickly.   The RT685 requires a few extra steps to get the boot header in place but once you have the RTOS building,  you are one your way to making some cool stuff.  The RT685 has a large amount of internal SRAM (4.5MB) RT685 as well a secondary DSP core (a topic for another day) and cool DSP peripherals (the PowerQuad) so it makes a great playground for Zephyr applications.

You can find SuperMonkey board port here:

In case you missed it here are the links to all of the other  RT600/SuperMonkey articles. 

Crossing Over With the i.MX RT600 Part 1 of 2

Crossing Over With the i.MX RT600 Part 2 of 2

i.MX RT685 Hardware Design 1 of 3 - Power and Package

i.MX RT 685 Hardware Design 2 of 3 - Flash Memory and Boot Configuration

i.MX RT 685 Hardware Design 3 of 3 - The SuperMonkey Design

i.MX RT685 SuperMonkey QSPI Bringup with MCUXpresso and Segger J-Link

Tags (2)