High-level vs. Low-level Programming
Introduction
High-level vs. Low-Level programming
The programs for embedded systems are usually developed at a relatively low level. The lowest level would be the Assembly language. The main advantage of low-level programming is that the resulting program is usually more optimized and memory efficient. It also offers direct control and access to hardware instructions and memory. The big disadvantage of low-level programming is its lack of portability, meaning that source code is specific to a given device and must be changed or adapted from one device to another. The source code is also usually much more difficult to read, reuse and maintain. This results generally in low productivity.
The disadvantages mentioned are a motivation for using a high-level programming language such a C++, together with high-level libraries for accessing device components. Today’s compilers achieve a level of optimization that makes programming in C++ a good choice for embedded systems as well. In addition, build tools are also efficient enough for making the development process possible using C++. Reuse of code and availability of existing libraries is also an important advantage of such programming languages.
What you’ll build
In this codelab, you’re going to program a Blinky program both by using a low-level API and by using the Mbed OS API.
What you’ll learn
- How to develop the Blinky program using a low-level API.
- How to develop the Blinky program using the Mbed OS API.
- The advantages of using a high-level programming API.
What you’ll need
- Mbed Studio for developing and debugging C++ code snippets.
- Basic C++ knowldege is required for this codelab.
The Blinky main program
The main program for blinking a led is a very simple program implementing the super-loop model (infinite loop). Within the loop, the program simply performs two operations:
- The first method toggles the led value.
- The second method makes the program sleep for the time of the blinking interval.
The program structure is shown below:
// necessary includes
int main()
{
// Blinking rate
// definition may depend on implementation
// performs some initialization
while (true) {
// toggle the led
// sleeps for the blinking interval time
}
}
Implementation using a Low-Level API
We now implement the blinking behavior using the C++ language, but with an approach that is the closest possible to a specific hardware platform, addressing almost directly GPIO with addresses and pointers.
In the code below, we first initialize the HAL library and configure the output pin that is connected to the LED. On the target device, LED1 corresponds to pin 12 that is available on port I of the microcontroller. For initializing the LED, the clock and the corresponding GPIO must be initialized. This code is essentially the same code as one would write for all STM32H7XX platforms and it corresponds to the code available in the device specific libraries of Mbed OS. Porting to other STM32 platforms requires a number of modifications and porting to other platforms requires a full rewriting of the program.
The code for the low level Blinky program is
// direct access to low level STM32H7XX
#include "stm32h7xx_hal.h"
#include "stm32h7xx_hal_gpio.h"
/* Common Error codes */
#define BSP_ERROR_NONE 0
/* Definitions for leds */
#define LED1_GPIO_PORT GPIOI
#define LED1_PIN GPIO_PIN_12
#define LED2_GPIO_PORT GPIOI
#define LED2_PIN GPIO_PIN_13
#define LED3_GPIO_PORT GPIOI
#define LED3_PIN GPIO_PIN_14
#define LED4_GPIO_PORT GPIOI
#define LED4_PIN GPIO_PIN_15
#define LEDx_GPIO_CLK_ENABLE() __HAL_RCC_GPIOI_CLK_ENABLE()
typedef enum
{
LED1 = 0U,
LED_GREEN = LED1,
LED2 = 1U,
LED_ORANGE = LED2,
LED3 = 2U,
LED_RED = LED3,
LED4 = 3U,
LED_BLUE = LED4,
LEDn
} Led_TypeDef;
static GPIO_TypeDef* LED_PORT[LEDn] = { LED1_GPIO_PORT,
LED2_GPIO_PORT,
LED3_GPIO_PORT,
LED4_GPIO_PORT};
static const uint32_t LED_PIN[LEDn] = { LED1_PIN,
LED2_PIN,
LED3_PIN,
LED4_PIN};
int32_t BSP_LED_Init(Led_TypeDef Led)
{
int32_t ret = BSP_ERROR_NONE;
GPIO_InitTypeDef GPIO_InitStruct;
/* Enable the GPIO_LED clock */
LEDx_GPIO_CLK_ENABLE();
/* Configure the GPIO_LED pin */
GPIO_InitStruct.Pin = LED_PIN[Led];
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
HAL_GPIO_Init(LED_PORT[Led], &GPIO_InitStruct);
/* By default, turn off LED */
HAL_GPIO_WritePin(LED_PORT[Led], LED_PIN[Led], GPIO_PIN_SET);
return ret;
}
/**
* @brief Turns selected LED On.
* @param Led LED to be set on
* This parameter can be one of the following values:
* @arg LED1
* @arg LED2
* @arg LED3
* @arg LED4
* @retval BSP status
*/
int32_t BSP_LED_On(Led_TypeDef Led)
{
int32_t ret = BSP_ERROR_NONE;
HAL_GPIO_WritePin (LED_PORT [Led], (uint16_t)LED_PIN [Led], GPIO_PIN_RESET);
return ret;
}
/**
* @brief Turns selected LED Off.
* @param Led LED to be set off
* This parameter can be one of the following values:
* @arg LED1
* @arg LED2
* @arg LED3
* @arg LED4
* @retval BSP status
*/
int32_t BSP_LED_Off(Led_TypeDef Led)
{
int32_t ret = BSP_ERROR_NONE;
HAL_GPIO_WritePin (LED_PORT [Led], (uint16_t)LED_PIN [Led], GPIO_PIN_SET);
return ret;
}
int main()
{
/* STM32H7xx HAL library initialization:
- Systick timer is configured by default as source of time base, but user
can eventually implement his proper time base source (a general purpose
timer for example or other time source), keeping in mind that Time base
duration should be kept 1ms since PPP_TIMEOUT_VALUEs are defined and
handled in milliseconds basis.
- Set NVIC Group Priority to 4
- Low Level Initialization
*/
HAL_Init();
// initialize the led
Led_TypeDef led = LED3;
BSP_LED_Init(led);
// toggle in a loop
bool on = true;
while (true) {
if (on) {
BSP_LED_On(led);
}
else {
BSP_LED_Off(led);
}
on = !on;
// busy wait for the blinking interval time
// using a rough estimation...
// and dependent on the CPU speed.
for (uint64_t i = 0; i < 50000000; i++) {
asm("nop");
}
}
return 0;
}
Read the comments in the code carefully for understanding this low level implementation. This code is more efficient than the one using the Mbed OS API, since the number of abstraction layers is reduced. However, as mentioned above, it is not portable, since symbol definitions and addresses are device specific.
Implementation using the Mbed API
The Mbed OS API provides the actual programmer a RTOS framework but it also provides an API for
- I/O abstractions
- Memory management
- File system
- Networking support
Also the Mbed OS API offers probably the easiest and most understandable way
to program any Arm Cortex-M MCU, because it is object-oriented using C++ and
it thus encapsulates a lot of functionalities in easy-to-use APIs. An example is
given here for the Blinky program. In this program, the DigitalOut
class is
used. The class declaration/definition is shown
here
, as an example of a full C++-based abstraction of a digital output pin. It is
important to note that the class includes operator declarations for assignment
and read. This allows us to write the blinking mechanism in a single line of
code as shown below.
The Blinky program written using the Mbed OS API, more specifically the
DigitalOut
class is shown below:
#include "mbed.h"
int main()
{
// constant definition
static constexpr std::chrono::milliseconds kBlinkingRate = 500ms;
// Initialise the digital pin LED1 as an output
DigitalOut led(LED1);
while (true) {
// toggle the led state
led = !led;
// sleep for the blinking rate interval
ThisThread::sleep_for(kBlinkingRate);
}
}
In this code, the declaration of led1
constructs and assigns an output pin to
LED1
, with the correct ports and addresses. The
assignment led = !led
toggles the LED value by using the assignment and int()
operators of the DigitalOut class. In this code, the use of addresses is
completely encapsulated and invisible to the programmer. The code is also fully
portable and can be used for any compatible target device that contains a
LED1`
hardware.
For a better understanding of the code above, compile it and debug it on your
target device. For this purpose, start a debug session. You should stop
automatically on the first statement (declaration of led1
). Step into each
statement and observe how the I/O functionalities are encapsulated in the
various classes. Be aware that the debugging capabilities of Keil Studio Cloud
or Mbed Studio are still limited - setting a breakpoint may not always work
properly for instance.
Mbed OS API and CMSIS
For making the abstraction used in the Blinky program makes use of the Cortex Microcontroller Software Interface Standard (CMSIS). CMSIS is a vendor-independent hardware abstraction layer for the Cortex-M processor series. This layer provides a standardized software interface, such as library functions, which help control the processor more easily. It significantly improves the portability across different Cortex-M processors and Cortex-M based microcontrollers. This is illustrated in the two figures below:
Standardization by CMSIS includes:
- Functions for accessing/configuring the interrupt system, the system control block and the system tick timer.
- Access to special registers.
- Access to special instructions.
- Initialization functions.
The advantages of using CMSIS are:
- An easier porting of application code from one Cortex-M based microcontroller to another.
- An easier code reuse from one application to another, running on different Cortex-M based microcontrollers.
- A better compatibility when integrating third-party software components using the same standard.
- A better code density and smaller memory footprint by benefiting from the optimization made in the CMSIS source code.
As a conclusion to this codelab, we show below the CMSIS component architecture, including CMSIS-CORE: