2. Hello World

1. Create a new workspace

Open STM32CubeIDE and create a new workspace.

2. Create a new project

Go to File > New > STM32 Project and select the board you are using. In this case, I am using the Nucleo-F446RE board. Click Next. Create an empty project and click Next. Give the project a name and click Finish.

3. Disable FPU

Now, try building the starter project, you might get the error:

../Src/main.c:22:4: warning: #warning "FPU is not initialized, but the project is compiling for an FPU. Please initialize the FPU before use." [-Wcpp]

To disable compiling for FPU, go to Project > Properties > C/C++ Build > Settings > MCU Settings and for the drop down menus:

  • Floating-point unit: None
  • Floating-point ABI: Software implementation

disable_fpu

Now rebuild the project and it should give you no warnings.


4. Download the code to the board

I always find the word "download" a bit confusing. I think it should be "upload" instead. Anyway, to download the code to the board, connect the board to the computer via USB. We can run the code in so-called debug mode, which allows us to set breakpoints and step through the code. To do this, go to Run > Debug Configurations.... Remember to enable Serial Wire Viewer (SWV) in the debug configuration. Click Debug and the code should start running. A window should pop up asking you to select a perspective, and the indicator light on the board should be blinking. Since the starter code is just an infinite loop, there is nothing else to do here. Click the red square to stop the code.


5. Debugging on STM32 Boards

Before writing any code, let's first delve into the debugging process for STM32 boards, using STM32F446 Nucleo 64 as our example. Debugging is an essential aspect of embedded system development, and understanding the underlying hardware and protocols can make the process much smoother.

The Debugging Chain

When you connect an STM32 board to your computer for debugging, you're essentially establishing a chain that looks something like this:

USB port (Computer) ⟶ ST-Link/V2 Debug Circuit ⟶ SWO Pin ⟶ ARM Cortex-M4 Processor

Debugging Components Explained

Let's get into the specifics of the different elements in the debugging chain and their roles:

USB Connection to ST-Link/V2

The USB connection functions as the initial bridge between your computer and the board. The ST-Link/V2 chip onboard acts as an intermediary, converting USB data packets into debugging and programming signals compatible with the microcontroller.

ST-Link/V2 Debug Circuit

This component is crucial for in-circuit debugging and programming of STM8 and STM32 microcontrollers. It essentially serves two main functions: 1. Debugging: It allows you to set breakpoints, step through code, and examine variables, just like you would in a software IDE. 2. Flashing: It writes your compiled code into the microcontroller's flash memory.

The circuit internally uses debugging protocols like SWD (Serial Wire Debug) or sometimes JTAG, although SWD is more common for STM32 boards.

SWO Pin and ARM Cortex-M4 Processor

The SWO (Serial Wire Output) pin serves as a channel for real-time trace capabilities, allowing you to collect run-time information without halting the processor. Inside the Cortex-M4 processor, SWO is connected to specific debug modules: - ITM (Instrumentation Trace Macrocell): This is used mainly for application-driven event tracing. You can use it to redirect standard output functions like printf to the debugger. - SWD (Serial Wire Debug): This is a 2-wire protocol for debugging; there's a Data line and a Clock line. It's a simpler, more modern alternative to JTAG and requires fewer pins.

ITM and SWD Deep Dive

  • ITM: Provides a memory-mapped register interface through which your application can write logging or event data. The data can then be viewed in the debugger in real-time.
  • SWD: Unlike JTAG, SWD uses only two pins to provide the same level of control and visibility into the system. This is advantageous in systems where pin count is a concern.

JTAG vs. SWD

While both are debugging protocols, SWD is generally preferred over JTAG for STM32 systems because it achieves the same functionalities but with fewer pins. JTAG uses at least 4 pins, whereas SWD needs only 2 plus a common ground.

Debugging in Embedded Systems vs. General Software Engineering

In general software engineering, debugging is often done with high-level languages and environments that abstract away most of the hardware. In embedded systems, debugging involves a deeper interaction with the hardware. You'll often need to concern yourself with things like memory-mapped I/O, register values, and even individual CPU instructions. The debuggers used in embedded systems, therefore, often provide functionality specific to hardware-level debugging, such as examining peripheral registers or capturing real-time trace data.

Leveraging printf in Debugging

The ITM unit inside the Cortex-M4 core has a feature where you can redirect the printf statements to output to the debugger. This is achieved by setting up the ITM to act as a "sink" for the standard output stream (stdout). This can be particularly helpful for real-time monitoring without halting the processor.

In STM32CubeIDE, you can see that there a source file named syscalls.c in the Src folder. This file contains a function named __io_putchar that is called whenever printf is used:

extern int __io_putchar(int ch) __attribute__((weak));

The default implementation of this function is to do nothing, but we can override (because of the weak attribute) it to redirect the output to the debugger. We will do this in the next section.


6. Writing the code

Note: This method only works for Cortex-M3 and -M4 processors. If you are using M0, you need to use semihosting.

To override the __io_putchar function, let's create two new files: imt_debug.c and itm_debug.h. In itm_debug.h, we'll define a function that redirects the output to the debugger:

#ifndef __IMG_DEBUG_
#define __IMG_DEBUG_

void __io_putchar(int ch);


#endif // __IMG_DEBUG_

In itm_debug.c, we'll implement the function:

#include "imt_debug.h"
#include <stdint.h>


//Debug Exception and Monitor Control Register base address
#define DEMCR                   *((volatile uint32_t*) 0xE000EDFCU )

/* ITM register addresses */
#define ITM_STIMULUS_PORT0      *((volatile uint32_t*) 0xE0000000 )
#define ITM_TRACE_EN            *((volatile uint32_t*) 0xE0000E00 )

void __io_putchar(int ch)
{

    //Enable TRCENA
    DEMCR |= ( 1 << 24);

    //enable stimulus port 0
    ITM_TRACE_EN |= ( 1 << 0);

    // read FIFO status in bit [0]:
    while(!(ITM_STIMULUS_PORT0 & 1));

    //Write to ITM stimulus port0
    ITM_STIMULUS_PORT0 = ch;
}

Let's not worry about the details of the implementation for now. The important thing is that we're writing to the ITM stimulus port 0, which is the port that is connected to the debugger. Now, let's go back to main.c:

#include <stdio.h>
#include "imt_debug.h"

int main(void)
{

    while(1)
    {
        printf("Hello World\r\n");
    }
}

Build and run the code in the debug mode (rememeber to enable SWV in the debug configuration). In the debug perspective, go to Window > Show View > SWV > SWV ITM Data Console. This will open a new window at the bottom of the screen. Click on the setting icon and enable port 0:

swv_itm_data_console_enable_port0

Click "Ok". Now, click the red circle to start recording. And then click the green triangle to start the code. You should see the following output:

hello_world_result