blob: 0c68e50e715a07e526a4399396ea89aa54237579 [file]
.. _nuttx-initialization-sequence:
=============================
NuttX Initialization Sequence
=============================
Overview
========
This initialization sequence is really quite simple because the system runs
in single-thread mode up until the point the that is starts the application.
That means the initialization sequence is just a simple, straight-line
of function calls.
It is until just before starting the application that system goes to
multi-threaded mode and things can get more complex.
At the highest level, the NuttX initialization sequence can be
represented in three phases:
* **Phase A** - The hardware-specific power-on reset initialization,
* **Phase B** - NuttX RTOS initialization, and
* **Phase C** - Application Initialization.
Each of these will be discussed in more detail in the following sections.
Case example: STM32 F4
======================
In this discussion, we'll use the STM32 F4 MCU and its popular evaluation
board STM32F4Discovery as example but the explanation can be applied
to any supported architecture.
Here is the map of initialization function calls::
__start()-arch/arm/src/common/stm32/stm32_start_m3m4_v1.c
|
+--*Set stack limit
+--stm32_clockconfig()
+--stm32_fpuconfig()
+--stm32_lowsetup()
+--stm32_gpioinit()
+--showprogress('A')
+--
+--
+--stm32_boardinitialize()-boards/arm/stm32f4/stm32f4discovery/src/stm32_boot.c
| |
| +--stm32_spidev_initialize()-stm32_spi.c:ONLY CHIP SELECTS
| +--stm32_usbinitialize()-
| +--stm32_netinitialize()-
| +--board_autoled_initialize()-
|
nx_start()-sched/init/nx_start.c
|
+--*Initialize global data structures
+--*Initialize OS facilities
+--net_initialize()-net/net_initialize.c
| |
| +--net_lockinitialize()
| +--mld_initialize()
| +--can_initialize()
| +--netlink_initialize()
| +--tcp_initialize()
| +--udp_initialize()
| +--usrsock_initialize()
|
+--up_initialize()-arch/arm/src/common/up_initialize.c
| |
| +--arm_dmainitialize()
| +--Config basic /dev nodes
| +--arm_serialinit()
| +--Console Init
| +--Crypto Config
| +--arm_netinitialize()
| | |
| | +--stm32_spibus_initialize()
| |
| |
| +--arm_usbinitialize()
| +--L2 Cache Init
|
+--board_early_initialize()
+--g_nx_initstate = OSINIT_HARDWARE
+--shm_initialize()
+--lib_initialize()
+--binfmt_initialize()
+--Start SMP
+--syslog_initialize()
+--g_nx_initstate = OSINIT_OSREADY
+--DEBUGVERIFY(nx_bringup())-sched/init/nx_bringup.c
| |
| +--nx_pgworker()
| +--nx_workqueues()
| +--nx_create_initthread()-sched/init/nx_bringup.c
| |
| +----+different thread
| : :
| : nx_start_task()-sched/init/nx_bringup.c
| : :
| same+----+--nx_start_application()-sched/init/nx_bringup.c
| thread |
| +--board_late_initialize()-stm32_boot.c:BOARD_DEPENDANT
| | |
| | +--stm32_bringup()
| | |
| | +--stm32_i2ctool()
| | +--board_bmp180_initialize()
| | +--stm32_sdio_initialize()
| | +--stm32_usbhost_initialize()
| | +--stm32_pwm_setup()
| | +--stm32_can_setup()
| | +--etc
| |
| +--
|
|
|
|
|
+--kmm_givesemaphore()
+--up_idle()
Phase A - Power-On Reset Initialization
=======================================
The system begins execution when the processor is reset.
This usually at power-on, but all resets are basically the same whether
they occur because of power-on, pressing the reset button, or on a watchdog
timer expiration.
.. note:: The code that executes when the processor is reset is unique
to the particular CPU architecture and is not a common part
of NuttX.
The kind of things that must be done by the architecture-specific reset
handling includes:
* Putting the processor in its operational state. This may include things
like setting CPU modes; initializing co-processors, etc.
* Setting up clocking so that the software and peripherals operate as expected.
* Setting up the C stack pointer (and other processor registers).
* Initializing memory.
* Starting NuttX.
Memory Initialization
---------------------
In C implementations, there are two general classes of variable storage.
First there are the initialized variables.
For example, consider the global variable x:
.. code-block:: c
int x = 5;
The C code must be assured that after reset, the variable x has the value 5.
Initialized variable of this kind are retained in a special memory section
called data (or ``.data``).
Other variables are not initialized. Like the global variable y:
.. code-block:: c
int y;
But the C code will still expect y to have an initial value.
That initial value will be zero.
All uninitialized variables of this type need to have the value zero.
These uninitialized variables are retained in a section called bss
(or ``.bss``).
When we say that the reset handling logic initializes memory, we mean two things:
1. It provides the (initial) values of the initialized variables by copying
the values from FLASH into the ``.data`` section, and
2. It resets all of the uninitialized variables to zero.
It clears the ``.bss`` section.
Lets walk through reset sequence. This reset logic can be found in two files::
nuttx/arch/arm/src/stm32_vectors.S
nuttx/arch/arm/src/stm32_start.c
nuttx/arch/arm/src/stm32_vectors.S
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This file provides all of the STM32 exception vectors and power-on reset
is simply another exception vector.
There are few important things to note about this file.
``.section .vectors, ax``. This pseudo operation will place all of the
vectors into a special section call ``.vectors``.
On of the STM32 F4 linker scripts is located at
``nuttx/boards/arm/stm32f4/stm32f4discovery/scripts/ld.script``.
In that file, you can see that section ``.vectors`` is forced to lie
at the very beginning of FLASH memory.
The STM32 F4 can be configured to boot in different ways via strapping.
If it is strapped to boot from FLASH, then the STM32 FLASH memory will
be aliased to address ``0x0000 0000`` when the reset occurs.
That is the address of the power-up reset interrupt vector.
The first two 32-bit entries in the vector table represent the power-up
exception vector (which we know will be positioned at address
``0x0000 0000`` when the reset occurs). Those two entries are:
.. code-block:: c
.word IDLE_STACK /* Vector 0: Reset stack pointer */
.word __start /* Vector 1: Reset vector */
The Cortex-M family is unique in the way that is handles the reset vector.
Notice that there are two values: the stack pointer for the start-up thread
(the IDLE thread), and the entry point in the IDLE thread.
When the reset occurs, the the stack pointer is automatically set
to the first value and then the processor jumps to reset entry point
``__start`` specified in the second entry.
This means that the reset exception handling code can be implemented in C
rather than assembly language.
nuttx/arch/arm/src/stm32_start.c
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The reset vector ``__start`` lies in the file
``nuttx/arch/arm/src/common/stm32/stm32_start_m3m4_v1.c`` and does the real,
low-level architecture-specific initialization. This initialization includes:
1. ``stm32_clockconfig()`` - Initialize the PLLs and peripheral clocking
needed by the board.
2. ``stm32_fpuconfig()`` - If the STM32 F4's hardware floating point
is initialized, then configure the FPU and enable access to the FPU
co-processors.
3. ``stm32_lowsetup()`` - Enable the low-level UART. This is done very early
in initialization so that we can get serial debug output to the console
as soon as possible. If you are doing a board bring-up this
is very important.
4. ``stm32_gpioinit()`` - Perform any GPIO remapping that is needed
(this is a stub for the F4, but the F1 family requires this step).
5. ``showprogress('A')`` - This simply outputs the character ``A`` on the
serial console (only if ``CONFIG_DEBUG`` is enabled). If debug is enabled,
you will always see the letters ABDE output on the console. That output
all comes from this file.
6. Next the memory is initialized:
a. The ``.bss`` section is set to zero (Letter ``B`` is then output if
``CONFIG_DEBUG`` is enabled), then
b. The ``.data`` section is set to its initial values (The letter ``C``
is output if debug is enabled),
7. ``stm32_boardinitialize()`` - Board-specific logic is initialized by
calling this function. For the case of the STM32F4Discovery board,
this logic can be found at
``nuttx/boards/arm/stm32f4/stm32f4discovery/src/stm32_boot.c`` and does
the following operations:
a. ``stm32_spidev_initialize()`` - Initialize SPI chip selects
if SPI is enabled.
b. ``stm32_usbinitialize()`` - Initialize hardware USB devices if enabled.
c. ``stm32_netinitialize()`` - Initialize hardware network devices
if enabled.
d. ``board_autoled_initialize()`` - Configure on-board LEDs
if LED support has been selected.
8. When ``stm32_boardinitialize()`` returns to ``__start()``, the low-level,
architecture-specific initialization is complete.
Phase B - NuttX RTOS Initialization
===================================
``nx_start()``
--------------
This function resides in the file ``nuttx/sched/init/nx_start.c`` and
is the NuttX entry point.
It is called by ``__start()`` and performs the next phase of RTOS-specific
initialization before bringing up the application.
The operations performed by ``nx_start()`` are summarized below.
Note that many of these features can be disabled from the NuttX configuration
file and in that case those operations are not performed:
1. Initializes some NuttX global data structures,
2. Initializes the TCB for the IDLE (i.e, the thread that the initialization
is performed on),
3. ``nxsem_initialize()`` - Initialize the POSIX semaphore facilities.
This needs to be done first because almost all other OS features depend
on POSIX counting semaphores.
4. Memory organization - This includes heap configuration, memory manager,
paging, I/O buffers, etc.
5. ``task_initialize()`` - Initialize task data structures.
6. ``fs_initialize()`` - Initialize the file system (needed to support
device drivers).
7. ``irq_initialize()`` - Initialize the interrupt handler subsystem.
This initializes only data structures; CPU interrupts are still disabled.
8. ``wd_initialize()`` - Initialize the NuttX watchdog timer facility,
9. ``clock_initialize()`` - Initialize the system clock,
10. ``timer_initialize()`` - Initialize the POSIX timer facilities,
11. ``nxsig_initialize()`` - Initialize the POSIX signal facilities,
12. ``nxmq_initialize()`` - Initialize the POSIX message queue facilities,
13. ``pthread_initialize()`` - Initialize the POSIX pthread facilities,
14. ``net_initialize()`` - Initialize networking facilities,
.. note:: Up to this point, all of the initialization steps have only been
software initializations. Nothing has interacted with the hardware.
Rather, all of these steps simply prepared the environment so that
things like interrupts and threads can function properly.
The next phases depend upon that setup.
15. ``up_initialize()`` - The processor specific details of running
the operating system will be handled here. Such things as setting up
interrupt service routines and starting the clock are some of the things
that are different for each processor and hardware platform.
All ARM-based MCUs share a common ``up_initialize()`` implementation
provided at ``nuttx/arch/arm/src/common/up_initialize.c``. The operations
perform by this common ARM initialization will, however, call into
facilities provided by the particular ARM chip.
For the STM32 F4, those facilities would be provided by logic in files
as ``nuttx/arch/arm/src/stm32``. The common ARM initialization sequence is:
* ``up_color_intstack()`` - Colorize the interrupt stack.
* ``arm_addregion()`` - The basic heap was set up during processing by
``nx_start()``. However, if the board supports multiple, discontiguous
memory regions, any addition memory regions can be added to the heap
by this function. For the STM32 F4, ``up_addregion()`` is implemented
in ``nuttx/arch/arm/src/common/stm32/stm32_allocateheap_m3m4_v1.c``.
* ``arm_pminitialize()`` - If ``CONFIG_PM`` is defined, the function must
initialize the power management subsystem. This MCU-specific function
must be called very early in the initialization sequence before any other
device drivers are initialized (since they may attempt to register with
the power management subsystem). There is no implementation
of ``up_pminitialize()`` for any STM32 platform.
* ``arm_dmainitialize()`` - Initialize the DMA subsystem.
For the STM32 F4, this DMA initialization can be found in
``nuttx/arch/arm/src/common/stm32/stm32_dma_m3m4_v1_8ch.c`` (which includes
``nuttx/arch/arm/src/common/stm32/stm32_dma_m3m4_v1_8ch.c``).
* ``devnull_register()`` - Registers the standard ``/dev/null``.
* ``devrandom_register()`` - Registers the standard ``/dev/random``.
* ``devurandom_register()`` - Registers the standard ``/dev/urandom``.
* ``devzero_register()`` - Registers the standard ``/dev/zero``.
* ``loop_register()`` - Registers the standard ``/dev/loop``.
* ``note_register()`` - Registers the standard ``/dev/note``.
* ``arm_serialinit()`` - Initialize the **standard** serial driver
(found at ``nuttx/arch/arm/src/common/stm32/stm32_serial_m3m4_v1v2v3v4.c`` STM32 F4).
* ``arm_netinitialize()`` - Initialize the network. For the STM32 F4,
this function is in ``nuttx/arch/arm/src/common/stm32/stm32_eth_m3m4_v1.c``.
* ``arm_usbinitialize()`` - Initialize USB (host or device).
For the STM32 F4, this function is in
``nuttx/arch/arm/src/common/stm32/stm32_otgfsdev_m3m4_v1.c``.
* ``arm_l2ccinitialize()`` - Initialize the L2 cache if present
and selected.
* ``up_ledon(LED_IRQSENABLED)`` - Finally, ``up_initialize()``
illuminates board-specific LEDs to indicate the IRQs are now enabled.
16. ``board_early_initialize()`` - If ``CONFIG_BOARD_EARLY_INITIALIZE`` is
selected, then an additional initialization call will be performed in the
boot-up sequence to a function called ``board_early_initialize()``.
It will be called immediately after ``up_initialize()`` (and may be
thought of as a board-specific, extension of ``up_initialize()``)
and well ``before board_late_initialize()`` is called and the initial
application is started.
17. ``g_nx_initstate = OSINIT_HARDWARE`` - This signals that basic
hardware setup is complete.
18. ``shm_initialize()`` - Initialize shared memory support.
19. ``lib_initialize()`` - Initialize the C libraries. This is done last
because the libraries may depend on the above.
20. ``binfmt_initialize()`` - Initialize binary loader subsystem.
21. Start SMP support in multi-core MCUs. This is not the case
in STM32F4Discovery but relevant because here are created ``stdin``,
``stdout`` and ``stderr`` for each CPU's (even if there is only one)
IDLE task. All tasks subsequently created by the IDLE thread will inherit
these file descriptors.
22. ``syslog_initialize()`` - Late initialization of the system logging
device. Some SYSLOG channel must be initialized late in the initialization
sequence because it may depend on having IDLE task file structures setup.
23. ``nx_bringup()`` - Create the initial tasks. This will be described
in more detail below.
``nx_bringup()``
----------------
This function is called at the very end of the initialization sequence
in ``nx_start()``, just before entering the IDLE loop. It is located
in ``nuttx/sched/init/nx_bringup.c`` and it starts all of the required
threads and tasks needed to bring up the system.
This function performed the following specific operations:
* ``nx_pgworker()`` - Start the page fill worker kernel thread that will
resolve page faults. This should always be the first thread started because
it may have to resolve page faults in other threads. This is the task that
runs in order to satisfy page faults in processors that have an MMU and
in configurations where on-demand paging is enabled.
* ``nx_workqueues()`` - Start the worker thread. The worker thread may be used
to execute any processing deferred to the worker thread via APIs provided
in ``include/nuttx/wqueue.h``. The worker thread's primary function is
as the “bottom half” for extended device driver processing but can be used
for a variety of purposes like misc garbage clean-up.
* ``nx_create_initthread()`` - Once the operating system has been initialized,
this functions either directly calls ``nx_start_application()`` or creates
a thread for running it
* ``nx_start_application()`` - If set in the NuttX configuration,
this function calls ``board_late_initialize()``.
* ``board_late_initialize()`` is a last-minute, board-specific
initialization. Note that there was earlier, board-specific initialization
calls (to ``stm32_board_initialize()`` and
to ``board_early_initialize()``). The difference here is these first,
low-level initialization calls were made before the OS was completely
launched. ``board_late_initialize()``, on the other hand, is called
at the end after the OS has been initialized but before any application
tasks have been started.
``board_late_initialize()`` would be an ideal place to do board-specific
initialization steps that depend on having a fully initialized OS
such as memory allocations, initialization of complex device drivers,
mounting of file systems, etc.
* After that, ``nx_start_application()`` launches the application either
by creating a task for it or executing a program from a filesystem after
mounting it.
* In the case of creating a task for the application, its entry has
the name ``user_start()``. ``user_start()`` is provided by application
code and when it runs, it begins the application-specific phase of the
initialization sequence as described below.
.. note:: The default ``user_start()`` entry point can be changed to use
one of the named applications used by NSH. This is a start-up option
that is not often used and will not be discussed further here.
And finally enter the IDLE loop. After completing the initialization, the role
of the IDLE thread changes. It becomes the thread that executes only when
there is nothing else to do in the system (hence, the name IDLE thread).
IDLE Thread Activities
----------------------
As mention, the IDLE thread is the thread that executes only when there is
nothing else to do in the system. It has the lowest priority in the system.
It always has the priority 0. It is the only thread that is permitted to have
the priority 0. And it can never be blocked (otherwise, what would run then?).
As a result, the IDLE thread is always in the ``g_readytorun`` list and,
in fact, since that list is prioritized, can guaranteed to always be
the final entry at the tail of the ``g_readytorun`` list.
The IDLE is an an infinite loop. But this does not make it a “CPU hog”.
Since it is the lowest priority, the it can be suspended whenever anything
else needs to run.
The IDLE thread does two things in this infinite loop:
1. If the worker was not started (see ``nx_bringup()`` below), then the IDLE
thread will perform memory clean-up. Memory clean is required to handle
deferred memory deallocation. Memory allocations must be deferred when
the memory is freed in a context where the software does not have access
to the heap and, hence, cannot truly free the memory (such as
in an interrupt handler). In this case, the memory is simply put into
a list of freed memory and, eventually, cleaned up by the IDLE thread.
.. note:: The worker thread's primary function is as the “bottom half”
for extended device driver processing. If the worker thread
was started, then it will run at a higher priority than the
IDLE thread. In this case, the worker thread will take over
responsibility for cleaning up these deferred allocations.
2. Then the loop calls ``up_idle()``. The operations performed
by ``up_idle()`` are architecture- and board-specific. In general,
this is the location where CPU-specific reduced power operations
may be performed.
STM32 F4 IDLE thread
--------------------
The default STM32 F4 IDLE thread is located at
``nuttx/arch/arm/src/stm32_idle.c``.
This default version does very little:
1. It includes a example, “skeleton” function that illustrates that kinds
of things that you can do if ``CONFIG_PM`` is enabled (this example code
is not fully implemented in the default IDLE logic).
2. The it executes the Cortex-M thumb2 instruction wfi which causes the CPU
to sleep until the next interrupt occurs.
Phase C - Application Initialization
====================================
At the conclusion of the OS initialization phase in ``nx_start()``,
the user application is started by creating a new task
at the entry point ``user_start()``.
There must be exactly one entry point called ``user_start()``
in every application built on top of NuttX.
Any additional initialization performed in the ``user_start()`` function
is purely application dependent.
A Simple Hello World Application
--------------------------------
The simplest user application would be the “Hello, World!” example.
See ``apps/examples/hello``. Here is the whole example:
.. code-block:: c
int user_start(int argc, char *argv[])
{
printf("Hello, World!!\n");
return 0;
}
In this case, no additional application initialization is needed.
It just “says hello” and exits.
A Nutt Shell User Application/Command
-------------------------------------
The NuttShell (NSH) is a simple shell application that may be used with NuttX.
It is described here. It supports a variety of commands and is (very) loosely
based on the bash shell and the common utilities used
in Unix shell programming.
NSH is implemented as a library that can be found at ``nuttx-apps/nshlib``.
The NSH start-up sequence is very simple. As an example, the the code at
``apps/examples/nsh/nsh_main.c`` illustrates how to start NuttX.
It simple does the following:
1. If you have C++ static initializers, it will call your implementation
of ``up_cxxinitialize()`` which will, in turn, call those
static initializers.
2. This function then calls ``nsh_initialize()`` which initializes
the NSH library. ``nsh_initialize()`` is described in more detail below.
3. If the Telnet console is enabled, it calls ``nsh_telnetstart()`` which
resides in the NSH library. ``nsh_telnetstart()`` will start the Telnet
daemon that will listen for Telnet connections and start remote
NSH sessions.
4. If a local console is enabled (probably on a serial port), then
``nsh_consolemain()`` is called. ``nsh_consolemain()`` also resides
in the NSH library. ``nsh_consolemain()`` does not return so that
finished the entire NSH initialization sequence.
``nsh_initialize()``
^^^^^^^^^^^^^^^^^^^^
The NSH initialization function, ``nsh_initialize()``, be found in
``apps/nshlib/nsh_init.c``. It does only three things:
``nsh_romfsetc()`` if so configured, it executes an NSH start-up script that
can be found at ``/etc/init.d/rcS`` in the target file system.
``/etc`` is the location where a read-only, ROMFS file system is mounted by
``nsh_romfsetc()``. The ROMFS image is, itself, just built into the firmware.
By default, this rcS startup script contains the following logic:
.. code-block:: sh
# Create a RAMDISK and mount it at XXXRDMOUNTPOUNTXXX
mkrd -m XXXMKRDMINORXXX -s XXMKRDSECTORSIZEXXX XXMKRDBLOCKSXXX
mkfatfs /dev/ramXXXMKRDMINORXXX
mount -t vfat /dev/ramXXXMKRDMINORXXX XXXRDMOUNTPOUNTXXX
Where the ``XXXX*XXXX`` strings get replaced in the template
when the ROMFS image is created:
* ``XXXMKRDMINORXXX`` will become the RAM device minor number. Default: ``0``.
* ``XXMKRDSECTORSIZEXXX`` will become the RAM device sector size.
* ``XXMKRDBLOCKSXXX`` will become the number of sectors in the device.
* ``XXXRDMOUNTPOUNTXXX`` will become the configured mount point.
Default: ``/etc``.
This script will, then, create a RAMDISK, format a FAT file system
on the RAM disk, and then mount the FAT filesystem at a configured mountpoint.
This rcS template file can be found at ``apps/nshlib/rcS.template``.
The resulting ROMFS file system can be found in
``apps/nshlib/nsh_romfsimg.h``.
* ``boardctl()`` - Next any architecture-specific NSH initialization will be
performed (if any). The NSH initialization logic will all the non-standard
OS interface ``boardctl()`` like: ``(void)boardctl(BOARDIOC_INIT, 0);``.
The first argument, the command ``BOARDIOC_INIT``, indicates that the
``boardctl()`` is being requested to perform application-oriented
initialization. In response to this command, ``boardctl()`` will call the
board-specific implementation of ``board_app_initialize()``.
That function is not generally available to application level code in
all configurations but can always be accesses via ``boardctl()``.
* ``board_app_initialize()`` - For the STM32F4Discovery, this architecture
specific initialization can be found at
``boards/arm/stm32f4/stm32f4discovery/src/stm32_appinit.c``.
This it does things like:
1. Initialize SPI devices.
2. Initialize SDIO.
3. Mount any SD cards that may be inserted.
There are two possibilities then for application startup initialization:
1. In the application itself via ``boardctl(BOARDIOC_INIT, 0)``, OR
2. in the OS using ``board_late_initialize()``.
Each works well in certain contexts but there are also things that I don't
like about either possibility.
``boardctl()``
--------------
You can see this application-controlled initialization in, for example,
the NSH application in `nuttx-apps/nshlib``. This is seen as the call to
``boardctl(BOARDIOC_INIT, 0)``. This requires that the ``boardctl()``
interface be enabled with ``CONFIG_LIB_BOARDCTL``.
When ``CONFIG_LIB_BOARDCTL=y``, board-specific OS internal logic must provide
the interface ``board_app_initilize()``. ``boardctl(BOARDIOC_INIT, 0)`` is the
application level interface that provides the proper interface to
``board_app_initilize()``. All board-specific, driver-level initialization may
then be performed in ``board_app_initilize()`` under the control
of the application.
With ``CONFIG_LIB_BOARDCTL=y``, there are two levels of application-specific
initialization:
(a) an OS/kernel level application initialization that needs to be performed
before the application is started, and
(b) a user/application level initialization that can be performed after
the application has started.
``board_late_nitialize()``
--------------------------
``board_late_initialize()`` is the OS/kernel level application initialization
that is performed before the application has started. It usually works
quite well and is usually a good alternative for ``boardctl(BOARDIOC_INIT)``.
To avoid issues with trying to perform initialization on the IDLE thread,
``board_late_initialize()`` is called from an internal kernel thread.
The IDLE thread has limitations: It cannot wait for events so
it can only be used for simple, straight-line initialization logic.
That may be insufficient in some cases.
For this reason, ``board_late_initialize()`` must run on a kernel thread.
Consider the ``board_late_initialize()`` calling sequence:
``board_late_initialize()`` is called directly from ``do_app_start()`` in
``nuttx/sched/init/nx_bringup.c`` just before starting the application task.
``nx_create_initthread()`` is, in turn, called from the function
``nx_start_application()``. If ``CONFIG_BOARD_LATE_INITIALIZE`` is
not defined, then this is just a normal C function call.
But if ``CONFIG_BOARD_LATE_INITIALIZE`` is defined, then an intermediate,
trampoline kernel thread is started. That kernel thread executes
``do_app_start()`` and moves the initialization off of the IDLE thread.
This works great but is a little more complex than I would like.
``apps/platform``
-----------------
There is also special place for user/application initialization.
That is the ``nuttx-apps/platform`` platform directory. This directory
should be a mirror of the ``nuttx/configs`` directory.
There should be a board directory in ``nuttx-apps/platform`` for every
board that is in ``nuttx/configs``.