Porting openvela to the STM32F407 Development Board

[ English | 简体中文 ]

This guide provides a detailed walkthrough for porting the openvela operating system to the RoboMaster Type-C development board (hereinafter referred to as the "C-board"). The C-board's main controller chip (MCU) is the STM32F407IGH6. Since official support for the STM32F407IG series is already available in NuttX, this guide will focus on the adaptation process for the Board Support Package (BSP).

After completing this tutorial, you will be able to create a basic BSP for a new hardware platform, successfully run the openvela system, and control the onboard LED.

What You Will Learn

  • The core principles of openvela's board-level porting and its boot process.
  • How to implement the key C language interface functions required for board-level initialization.
  • How to configure GPIOs and write LED drivers for both system status and user applications.
  • How to integrate a new board into the openvela build system using Kconfig.
  • How to create a defconfig default configuration file for the new board.

Prerequisites

Before you begin, please ensure you have completed the following preparations:

  1. Get the Source Code: Refer to the Quick Start document to download the latest code.
  2. Understand the openvela architecture: It is recommended to read the openvela Architecture beforehand to understand its layered design.
  3. Review the boot process: Consult the System Boot Process document for a more detailed startup sequence and function call graph.

I. Porting Principles and Boot Process

Porting openvela essentially involves providing the operating system framework with a set of interfaces to interact with specific hardware. These interfaces form the Board Support Package (BSP), located in the nuttx/boards/ directory. This guide will walk you through implementing a minimal-functionality BSP to get openvela up and running on the C-board.

Key Boot Functions

During startup, openvela calls a series of BSP-provided functions in a specific order to complete hardware initialization. To successfully boot the system, you must implement the following key functions in your BSP:

Function Description
void stm32_boardinitialize(void) Called by the system startup code __start. This is the earliest board-level initialization function executed, used to configure the most basic hardware, such as clocks and the debug serial port.
void board_late_initialize(void) If CONFIG_BOARD_LATE_INITIALIZE is enabled, this function is called late in the OS initialization phase, typically for initializing drivers that depend on OS services.
int stm32_bringup(void) If CONFIG_BOARD_LATE_INITIALIZE is enabled, it's called by board_late_initialize(). Otherwise, it's called by board_app_initialize(arg). Driver initialization is implemented within stm32_bringup.
int board_app_initialize(uintptr_t arg) Triggered by the boardctl() interface via the BOARDIOC_INIT command. It is used to perform application-level or user-defined initializations.

Additionally, if your system is configured with CONFIG_ARCH_LEDS to display system status (e.g., boot-up, panic), you will also need to implement the following functions:

Function Description
void board_autoled_initialize(void) Initializes the LED pins used for system status indication, typically setting them as GPIO outputs.
void board_autoled_on(int led) Turns on a specific LED based on the system state (e.g., LED_STARTED).
void board_autoled_off(int led) Turns off a specific LED based on the system state (e.g., at the end of LED_PANIC).

Hardware Configuration: Clocks and Pins

In addition to implementing function interfaces, you also need to define macros in the board-level header file board.h to configure the STM32's clock tree, peripheral pin functions, and more. These macros will be used by chip-level driver code (e.g., stm32_rcc.c).

The following is a clock tree configuration example for the C-board. It shows how to use a 12MHz external high-speed crystal (HSE) and multiply it via the PLL to generate a 168MHz system clock (SYSCLK).

/* Clocking *****************************************************************/
/*
 * System Clock source           : PLL (HSE)
 * SYSCLK(Hz)                    : 168000000
 * HCLK(Hz)                      : 168000000
 * AHB Prescaler                 : 1
 * APB1 Prescaler                : 4
 * APB2 Prescaler                : 2
 * HSE Frequency(Hz)             : 12000000     (STM32_BOARD_XTAL)
 * PLLM                          : 12           (STM32_PLLCFG_PLLM)
 * PLLN                          : 336          (STM32_PLLCFG_PLLN)
 * PLLP                          : 2            (STM32_PLLCFG_PLLP)
 * PLLQ                          : 7            (STM32_PLLCFG_PLLQ)
 * Main regulator output voltage : Scale1 mode
 * Flash Latency(WS)             : 5
 * Prefetch Buffer               : ON
 * Instruction cache             : ON
 * Data cache                    : ON
 */

II. Code Implementation Steps

This section will guide you through creating the necessary files and writing the code for this project.

1. Create the Code Directory Structure

First, create a new directory named stm32f407-robomaster inside nuttx/boards/arm/stm32/, and set up the following subdirectory and file structure.

Code Path: nuttx/boards/arm/stm32/stm32f407-robomaster/

stm32f407-robomaster
├── CMakeLists.txt
├── configs                           # defconfig configuration path
│   ├── led
│   │   └── defconfig                 # Default configuration for the LED example
│   └── nsh
│       └── defconfig                # Default configuration for NuttShell (NSH)
├── include
│   └── board.h                       # Board-level hardware configuration header
├── Kconfig                           # Board-level Kconfig file
├── scripts                           # Linker scripts
│   ├── ld.script                     # Linker script
│   └── Make.defs                     # Board-level Make definitions
└── src
    ├── CMakeLists.txt
    ├── Make.defs
    ├── stm32_appinit.c               # Implements board_app_initialize
    ├── stm32_autoleds.c              # Implements system status LED control
    ├── stm32_boot.c                  # Implements stm32_boardinitialize
    ├── stm32_bringup.c               # Implements driver initialization
    ├── stm32f407-robomaster.h        # Board-specific private header, defines GPIOs
    └── stm32_userleds.c              # Implements user-level LED driver interface

2. Implement Core Initialization Functions

In the src/ directory, you need to create and populate the following C files.

stm32_boot.c: Early Hardware Initialization

This file is responsible for implementing stm32_boardinitialize(), which configures essential hardware during the earliest stage of the system boot process.

#include <nuttx/config.h>
#include "stm32f407-robomaster.h" // Board-specific private definitions

void stm32_boardinitialize(void)
{
#ifdef CONFIG_ARCH_LEDS
  /* Initialize the system status LEDs if enabled */
  board_autoled_initialize();
#endif
}


#ifdef CONFIG_BOARD_LATE_INITIALIZE
void board_late_initialize(void)
{
  /* Perform board-specific late initialization */
  stm32_bringup();
}
#endif

stm32_bringup.c: Device Driver Initialization

The stm32_bringup() function in this file is responsible for initializing and registering the drivers for all onboard devices. In this example, we only register the user LED driver.

#include "stm32.h"

#ifdef CONFIG_USERLED
#  include <nuttx/leds/userled.h>
#endif

#include "stm32f407-robomaster.h"

int stm32_bringup(void)
{
  int ret = OK;

#ifdef CONFIG_USERLED
  /* Register the user LED driver to make it available at /dev/userleds */
  ret = userled_lower_initialize("/dev/userleds");
  if (ret < 0)
    {
      syslog(LOG_ERR, "ERROR: userled_lower_initialize() failed: %d\n", ret);
    }
#endif
  /* Add initialization for other drivers here, e.g., I2C, SPI, SDIO, etc. */
  return ret;
}

stm32_appinit.c: Application Initialization Bridge

This file mainly implements the int board_app_initialize(uintptr_t arg) function:

  • If CONFIG_BOARD_LATE_INITIALIZE is enabled, stm32_bringup() will be called by board_late_initialize().
  • If CONFIG_BOARD_LATE_INITIALIZE is not enabled, stm32_bringup() will be called by board_app_initialize(arg).
int board_app_initialize(uintptr_t arg)
{
#ifdef CONFIG_BOARD_LATE_INITIALIZE
  /* If late initialization is defined, bringup has already been called. No action needed here. */
  return OK;
#else
  /* Otherwise, call bringup here to initialize drivers. */
  return stm32_bringup();
#endif
}

3. Implement the LED Driver

openvela categorizes LEDs into two types: one for indicating system status (autoleds) and another for control by user applications (userleds).

stm32_autoleds.c: System Status LEDs

This file implements the board_autoled_* series of functions to control the on/off state of the LEDs. These are automatically called by the system kernel upon specific events (e.g., boot-up, assertion failure).

/****************************************************************************
 * Private Functions
 ****************************************************************************/

static inline void set_led(bool v)
{
  ledinfo("Turn LED %s\n", v? "on":"off");
  stm32_gpiowrite(GPIO_LEDR, v);
  stm32_gpiowrite(GPIO_LEDG, v);
  stm32_gpiowrite(GPIO_LEDB, v);
}

/****************************************************************************
 * Public Functions
 ****************************************************************************/
#ifdef CONFIG_ARCH_LEDS
void board_autoled_initialize(void)
{
  /* Configure all system status LED GPIOs as outputs */
  stm32_configgpio(GPIO_LEDR);
  stm32_configgpio(GPIO_LEDG);
  stm32_configgpio(GPIO_LEDB);
}

void board_autoled_on(int led)
{
  ledinfo("board_autoled_on(%d)\n", led);

  switch (led)
    {
    case LED_STARTED:      // OS boot complete
    case LED_HEAPALLOCATE: // Heap allocation failed
      /* As the board provides only one soft controllable LED, we simply
       * turn it on when the board boots.
       */

      set_led(true);
      break;

    case LED_PANIC:            // System Panic
      /* In a Panic state, the LED is typically controlled by upper-level
       * code to blink rapidly. */
      set_led(true);
      break;
    }
}

void board_autoled_off(int led)
{
  /* Implement the corresponding turn-off logic for board_autoled_on */
  switch (led)
    {
    case LED_PANIC:
      /* For panic state, the LED is blinking */
      set_led(false);
      break;
    }
}

#endif /* CONFIG_ARCH_LEDS */

stm32_userleds.c: User Application LEDs

This file provides the low-level hardware operation interfaces for the generic LED driver (located at drivers/leds/userled_lower.c). Applications can control these LEDs by accessing /dev/userleds through standard VFS interfaces like open(), write(), and ioctl().

We need to provide the following function interfaces to be called by userled_lower.c:

#include <nuttx/config.h>
#include "stm32f407-robomaster.h"

#ifndef CONFIG_ARCH_LEDS

/* Define the GPIO configurations for the onboard user LEDs */
static const uint32_t g_ledcfg[BOARD_NLEDS] =
{
  GPIO_LEDB,
  GPIO_LEDG,
  GPIO_LEDR
};

/****************************************************************************
 * Name: board_userled_initialize
 ****************************************************************************/

uint32_t board_userled_initialize(void)
{
  int i;

  /* Configure the GPIOs for the onboard user LEDs */
  for (i = 0; i < BOARD_NLEDS; i++)
    {
      stm32_configgpio(g_ledcfg[i]);
    }

  return BOARD_NLEDS;
}   
void board_userled(int led, bool ledon)
{
  /* Set the state of a single user LED */
  if ((unsigned)led < BOARD_NLEDS)
    {
      stm32_gpiowrite(g_ledcfg[led], ledon);
    }
}
  
void board_userled_all(uint32_t ledset)
{
  int i;

  /* Set the state of all user LEDs based on a bitmask */
  for (i = 0; i < BOARD_NLEDS; i++)
    {
      stm32_gpiowrite(g_ledcfg[i], (ledset & (1 << i)) != 0);
    }
}

Ultimately, the following LED driver interfaces are implemented in userled_upper.c:

static int     userled_open(FAR struct file *filep);
static int     userled_close(FAR struct file *filep);
static ssize_t userled_write(FAR struct file *filep, FAR const char *buffer,
                        size_t buflen);
static int     userled_ioctl(FAR struct file *filep, int cmd,
                         unsigned long arg);

4. Define Hardware Macros

Define hardware-related macros in src/stm32f407-robomaster.h and include/board.h.

stm32f407-robomaster.h: Board-Specific Private Definitions

This file defines the GPIO pins for onboard peripherals and other private configurations.

/* Configuration ************************************************************/

/* LED.  User LEDR: the red LED is a user LED connected to board LED D12
 * corresponding to MCU I/O PH10.
 *       User LEDG: the green LED is a user LED connected to board LED D12
 * corresponding to MCU I/O PH11.
 *       User LEDB: the blue LED is a user LED connected to board LED D12
 * corresponding to MCU I/O PH13.
 *
 * - When the I/O is HIGH value, the LED is on.
 * - When the I/O is LOW, the LED is off.
 */

#define GPIO_LEDB \
(GPIO_PORTH | GPIO_PIN10 | GPIO_OUTPUT_SET | GPIO_OUTPUT | GPIO_PULLUP | \
 GPIO_SPEED_50MHz)

#define GPIO_LEDG \
(GPIO_PORTH | GPIO_PIN11 | GPIO_OUTPUT_SET | GPIO_OUTPUT | GPIO_PULLUP | \
 GPIO_SPEED_50MHz)
 
#define GPIO_LEDR \
(GPIO_PORTH | GPIO_PIN12 | GPIO_OUTPUT_SET | GPIO_OUTPUT | GPIO_PULLUP | \
 GPIO_SPEED_50MHz)

 /* Buttons
 *
 * B1 USER: the user button is connected to the I/O PA0 of the STM32
 * microcontroller.
 */

#define MIN_IRQBUTTON   BUTTON_USER
#define MAX_IRQBUTTON   BUTTON_USER
#define NUM_IRQBUTTONS  (BUTTON_USER + 1)

#define GPIO_BTN_USER \
  (GPIO_INPUT |GPIO_PULLUP |GPIO_EXTI | GPIO_PORTA | GPIO_PIN0)

include/board.h: Public Board-Level Definitions

This file contains board-level definitions shared by the NuttX kernel and applications, such as clock configurations.

#ifndef __BOARDS_ARM_STM32_STM32F407_ROBOMASTER_INCLUDE_BOARD_H
#define __BOARDS_ARM_STM32_STM32F407_ROBOMASTER_INCLUDE_BOARD_H

/* Clock configuration macros */
#define STM32_BOARD_XTAL        12000000ul
#define STM32_PLLCFG_PLLM       STM32_PLLCFG_PLLM_DIV(12)
#define STM32_PLLCFG_PLLN       STM32_PLLCFG_PLLN_MUL(336)
#define STM32_PLLCFG_PLLP       STM32_PLLCFG_PLLP_DIV(2)
#define STM32_PLLCFG_PLLQ       STM32_PLLCFG_PLLQ_DIV(7)

/* ... Other public definitions ... */

#endif /* __BOARDS_ARM_STM32_STM32F407_ROBOMASTER_INCLUDE_BOARD_H */

III. Integrating into the Build System

After writing the code, you need to modify the build system configuration so that openvela can recognize and compile your new BSP.

1. Kconfig Integration

Kconfig is used to manage the configuration of the kernel and applications. You need to perform the following three steps to integrate the new board.

  1. Define the board-level option in nuttx/boards/Kconfig:

    Add a new config entry to display your development board in the configuration menu.

    config ARCH_BOARD_STM32F407_RM
        bool "STM32F407IGH6 ARM Development Board"    
        depends on ARCH_CHIP_STM32F407IG
        select ARCH_HAVE_LEDS
        select ARCH_HAVE_BUTTONS
        select ARCH_HAVE_IRQBUTTONS
        ---help---
            A configuration for the STM32F407 RoboMaster development board.
    
  2. Set the default board name in nuttx/boards/Kconfig:

    When your board is selected, this sets the ARCH_BOARD variable to your BSP directory name automatically.

    config ARCH_BOARD
        string
        # ... 其他板的 default 设置 ...
        default "stm32f407-robomaster"      if ARCH_BOARD_STM32F407_RM  
    
  3. Load the board-level Kconfig file in nuttx/boards/Kconfig:

    Ensure that when your board is selected, its Kconfig file from the BSP directory is loaded.

    if ARCH_BOARD_STM32F407_RM
    source "boards/arm/stm32/stm32f407-robomaster/Kconfig"
    endif
    
  4. Add board-specific options in the stm32f407-robomaster/Kconfig file.

    Here we demonstrate an empty framework:

    #
    # For a description of the syntax of this configuration file,
    # see the file kconfig-language.txt in the NuttX tools repository.
    #
    
    if ARCH_BOARD_STM32F407_RM
    
    endif # ARCH_BOARD_STM32F407_RM
    

2. Create the Default Configuration (defconfig)

A defconfig file is a minimal set of system configurations that provides users with an out-of-the-box starting point. You should provide a defconfig for each core feature set (e.g., NSH, specific examples).

The defconfig file in the nuttx/boards/arm/stm32/stm32f407-robomaster/configs/nsh/ directory is as follows:

#
# This file is autogenerated: PLEASE DO NOT EDIT IT.
#
# You can use "make menuconfig" to make any modifications to the installed .config file.
# You can then do "make savedefconfig" to generate a new defconfig file that includes your
# modifications.
#
# CONFIG_ARCH_FPU is not set
# CONFIG_DISABLE_OS_API is not set
# CONFIG_NSH_ARGCAT is not set
# CONFIG_NSH_CMDOPT_HEXDUMP is not set
# CONFIG_NSH_DISABLE_IFCONFIG is not set
# CONFIG_NSH_DISABLE_PS is not set
# CONFIG_STM32_SYSCFG is not set
CONFIG_ARCH="arm"
CONFIG_ARCH_BOARD="stm32f407-robomaster"
CONFIG_ARCH_BOARD_STM32F407_RM=y
CONFIG_ARCH_CHIP="stm32"
CONFIG_ARCH_CHIP_STM32=y
CONFIG_ARCH_CHIP_STM32F407IG=y
CONFIG_ARCH_INTERRUPTSTACK=2048
CONFIG_ARCH_STACKDUMP=y
CONFIG_BOARD_LOOPSPERMSEC=8499
CONFIG_HAVE_CXX=y
CONFIG_INIT_ENTRYPOINT="nsh_main"
CONFIG_INTELHEX_BINARY=y
CONFIG_LINE_MAX=64
CONFIG_NSH_FILEIOSIZE=512
CONFIG_NSH_READLINE=y
CONFIG_PREALLOC_TIMERS=4
CONFIG_RAM_SIZE=114688
CONFIG_RAM_START=0x20000000
CONFIG_RAW_BINARY=y
CONFIG_RR_INTERVAL=200
CONFIG_SCHED_WAITPID=y
CONFIG_START_DAY=6
CONFIG_START_MONTH=6
CONFIG_START_YEAR=2020
CONFIG_STM32_FLASH_CONFIG_G=y
CONFIG_STM32_JTAG_SW_ENABLE=y
CONFIG_STM32_USART6=y
CONFIG_SYSTEM_NSH=y
CONFIG_TASK_NAME_SIZE=0
CONFIG_USART6_SERIAL_CONSOLE=y

Note: For more information on using Kconfig and defconfig, please refer to the Kconfig Usage Guide.

IV. Summary

At this point, you have completed the core steps for porting openvela to the RoboMaster C-type development board. You have created a complete BSP that includes boot logic, driver implementations, and build system integration. Do not forget to provide the correct linker script ld.script and Make.defs files in the scripts/ directory.

Once all files are in place, you can compile and flash the firmware.

V. Next Steps

After successfully porting openvela to the C-board, you can try running an application to verify your work: