Skip to content
Snippets Groups Projects
Select Git revision
  • c5dcfab76d773c4195893e2ce758aaebaf8887c0
  • master default protected
2 results

e7020e_2021_hw

Forked from Per Lindgren / e7020e_2021_hw
Source project has a limited visibility.

app

Examples and exercises for the Nucleo STM32F401re/STM32F11re devkits.

Dependencies

  • Rust 1.32, or later.

  • rust-std components (pre-compiled core crate) for the ARM Cortex-M targets. Run:

$ rustup target add thumbv7em-none-eabihf
  • For programming (flashing) and debugging

    • openocd (install using your package manager)
    • arm-none-eabi toolchain (install using your package manager). In the following we refer the arm-none-eabi-gdb as just gdb for brevity.
  • st-flash (for low level access to the MCU flash)

Examples

Hello

  1. Connect your devkit using USB. To check that it is found you can run:
$ lsusb
...
Bus 001 Device 004: ID 0483:374b STMicroelectronics ST-LINK/V2.1
...

(Bus/Device/ID may vary.)

  1. In a terminal in the app folder run:
$ cargo build --example hello
$ openocd -f openocd.cfg
...
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : clock speed 2000 kHz
Info : STLINK V2J20M4 (API v2) VID:PID 0483:374B
Info : Target voltage: 3.254773
Info : stm32f4x.cpu: hardware has 6 breakpoints, 4 watchpoints
Info : Listening on port 3333 for gdb connections

openocd should connect to your target using the stlink programmer (onboard your Nucleo devkit). You may need to hold the RESET button (black), while starting openocd. If that does not work, disconnect the USB cable, hold the RESET button, re-connect the USB, start openocd then let go of the button.

  1. In another terminal (in the same app folder) run:
$ arm-none-eabi-gdb target/thumbv7em-none-eabihf/debug/examples/hello -x openocd.gdb

This starts gdb with file being the hello (elf) binary, and runs the openocd.gdb script, which loads (flashes) the binary to the target (our devkit). The script connects to the openocd server, sets breakpoints at main (as well as some exception handlers, more on those later), enables semihosting, loads the binary and finally runs the first intsruction (stepi).

  1. You can now continue debugging of the program:
(gdb) c
Continuing.

Breakpoint 3, main () at examples/hello.rs:13
13          hprintln!("Hello, world!").unwrap();

The cortex-m-rt run-time initializes the system and your global variables (in this case there are none). After that it calls the [entry] function. Here you hit a breakpoint.

  1. You can contine debugging:
(gdb) c
Continuing.
halted: PC: 0x08000608

At this point, the openocd terminal should read:

Info : halted: PC: 0x08000608
Hello, world!

Your program is now stuck in an infinite loop (doing nothing).

  1. Press CTRL-c in the gdb terminal:
Program received signal SIGINT, Interrupt.
0x08000624 in main () at examples/hello.rs:14
14          loop {}
(gdb)

You have now compiled and debugged a minimal Rust hello example. gdb is a very useful tool so lookup some tutorials/docs (e.g., https://sourceware.org/gdb/onlinedocs/gdb/), a Cheat Sheet can be found at https://darkdust.net/files/GDB%20Cheat%20Sheet.pdf.

ITM

The hello example uses the semihosting interface to emit the trace information (appearing in the openocd terminal). The drawback is that semihosting is incredibly slow as it involves a lot of machinery to process each character. (Essentially, it writes a character to a given position in memory, runs a dedicated break instruction, openocd detecects the break, reads the character at the given postition in memory and emits the character to the console.)

A better approach is to use de bultin in ITM (Instrumentation Trace Macrocell), designed to more efficently implement tracing. The onboard stlink programmer can put up to 4 characters into an ITM package, and transmit that to the host (openocd). openocd can process the incoming data and send it to a file or FIFO queue. The ITM package stream needs to be decoded (header + data). To this end we use the itmdump tool.

In a separate terminal:

$ mkfifo /tmp/itm.log
$ itmdump -f /tmp/itm.log -F

Now you can compile and run the itm application using the same steps as the hello programg. In the itmdump console you should now have the trace output.

$ mkfifo /tmp/itm.log
$ itmdump -f /tmp/itm.log -F
Hello, world!

Under the hood there is much less overhead, the serial transfer rate is set to 2MBit in between the ITM (inside of the MCU) and stlink programmer (onboard the Nucleo devkit). So in theory we can transmit some 200kByte/s data over ITM. However, we are limited by the USB interonnection and openocd to recieve and forward packages.

The stlink programmer, buffers packages but has limited buffer space. Hence in practise, you should keep tracing to short messages, else the buffer will overflow (and the programmer might crash). See trouble shooting section if you run into trouble.

Trouble shooting

Working with embedded targets involves a lot of tooling, and many things can go wrong.

openocd fails to connect

If you end up with a program that puts the MCU in a bad state, even a reset might not help you. In that case you can erase the flash memory. st-flash connects to the target directly (bypassing gdb and openocd) and hence more likely to get access to the target even if its in a bad state.

$ st-flash erase

gdb fails to connect

openocd acts as a gdb server, while gdb is a gdb client. By default they connect over port :3333 (: indicates that the port is on the localhost, not a remote connection). In cases you might have another gdb connection blocking the port.

$ ps -all
F S   UID   PID  PPID  C PRI  NI ADDR SZ WCHAN  TTY          TIME CMD
0 S  1000  5659  8712  0  80   0 -  6139 -      pts/1    00:00:28 openocd
0 S  1000  7549 16215  0  80   0 - 25930 se_sys pts/4    00:00:00 arm-none-eabi-g
...

In this case you can try killing gdb by:

$ kill -9 7549

or even

$ killall -9 arm-none-eabi-g

Notice, the process name is truncated for some reason...

If this did not help you can check if some other client has aquired the port, and kill the intruder accordingly.

$ lsof -i :3333
COMMAND    PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
openocd   5659  pln   12u  IPv4 387143      0t0  TCP localhost:dec-notes (LISTEN)
openocd   5659  pln   13u  IPv4 439988      0t0  TCP localhost:dec-notes->localhost:59560 (ESTABLISHED)
arm-none- 7825  pln   14u  IPv4 442734      0t0  TCP localhost:59560->localhost:dec-notes (ESTABLISHED)
$ kill -9 7825

itmdump no tracing or faulty output

There can be a number of reasons ITM tracing fails.

  • The openocd.gdb script enables ITM tracing assuming the /tmp/itm.log and itmdump has been correctly setup before gdb is launched (and the script run). So the first thing is to check that you follow the sequence suggested above.

  • openocd.gdbsets enables ITM tracing by:

# 16000000 must match the core clock frequency
monitor tpiu config internal /tmp/itm.log uart off 16000000
monitor itm port 0 on

The transfer speed (baud rate) is automatically negotiated, however you can set it explicitly (maximum 2000000).

monitor tpiu config internal /tmp/itm.log uart off 16000000 2000000

You may try a lower value.

  • The stm32f401re/stm32f411re defaults to 16000000 (16MHz) as the core clock frequency, based on an internal oscillator. If your application sets another core clock frequency the openocd.gdb script (tpiu setting) must be changed accordingly.

  • openocd implements a number of events which might be called by gdb, e.g.:

(gdb) monitor reset init
adapter speed: 2000 kHz
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08001298 msp: 0x20018000, semihosting
adapter speed: 8000 kHz

This invokes the init event, which sets the core clock to 64MHz. If you intend to run the MCU at 64MHz (using this approach), ITM will not work unless the tpiu setting matches 64MHz.

If you on the other hand want to use monitor reset init but not having the core clock set to 64MHz, you can use a custom stlink.cfg (instead of the one shipped with openocd). The original looks like this:

...
$_TARGETNAME configure -event reset-init {
	# Configure PLL to boost clock to HSI x 4 (64 MHz)
	mww 0x40023804 0x08012008   ;# RCC_PLLCFGR 16 Mhz /8 (M) * 128 (N) /4(P)
	mww 0x40023C00 0x00000102   ;# FLASH_ACR = PRFTBE | 2(Latency)
	mmw 0x40023800 0x01000000 0 ;# RCC_CR |= PLLON
	sleep 10                    ;# Wait for PLL to lock
	mmw 0x40023808 0x00001000 0 ;# RCC_CFGR |= RCC_CFGR_PPRE1_DIV2
	mmw 0x40023808 0x00000002 0 ;# RCC_CFGR |= RCC_CFGR_SW_PLL

	# Boost JTAG frequency
	adapter_khz 8000
}

The clock configuration can be commented out:

...
$_TARGETNAME configure -event reset-init {
	# # Configure PLL to boost clock to HSI x 4 (64 MHz)
	# mww 0x40023804 0x08012008   ;# RCC_PLLCFGR 16 Mhz /8 (M) * 128 (N) /4(P)
	# mww 0x40023C00 0x00000102   ;# FLASH_ACR = PRFTBE | 2(Latency)
	# mmw 0x40023800 0x01000000 0 ;# RCC_CR |= PLLON
	# sleep 10                    ;# Wait for PLL to lock
	# mmw 0x40023808 0x00001000 0 ;# RCC_CFGR |= RCC_CFGR_PPRE1_DIV2
	# mmw 0x40023808 0x00000002 0 ;# RCC_CFGR |= RCC_CFGR_SW_PLL

	# Boost JTAG frequency
	adapter_khz 8000
}

You can start openocd to use these (local) settings by:

$ openocd -f stlink.cfg -f stm32f4x.cfg

A possible advantege of monitor reset init is that the adapter speed is set to 8MHz, which at least in theory gives better transfer rate between openocd and the stlink programmer (default is 2MBit). I'm not sure the improvement is noticable.

  • ITM buffer overflow

In case the ITM buffer is saturated, ITM tracing stops working (and might be hard to recover). In such case:

  1. correct and recompile the program,

  2. erase the flash (using st-flash),

  3. power cycle the Nucleo (disconnect-and-re-connect),

  4. remove/re-make fifo, and finally re-start openocd/gdb.

This ensures 1) the program will not yet again overflow the ITM buffer, 2) the faulty program is gone (and not restarted accidently on a RESET), 3) the programmer firmware is restarted and does not carry any persistent state, notice a RESET applies only to the target, not the programmer, so if the programmer crashes it needs to be power cycled), 4) the FIFO /tmp/itm.log, openocd and gdb will have fresh states.

Visual Studio Code

It is possible to run gdb from within the vscode. vscode is highly configurable, (keyboard shortcuts, keymaps, plugins etc.). Using the default setup, and the cortex-debug plugin you can:

  • CTRL+m compile all examples (cargo build --examples). Cargo is smart and just re-compiles what is changed.

  • CTRL+d enter debug mode to choose a binary. (itm 64MHz (debug))

  • F5 to start. It will open the cortex_m_rt/src/lib.rs file, which contains the startup code. From there you can continue F5 again.

  • F6 to break. The program will now be in the infinite loop.

  • You can view the ITM trace in the OUTPUT tab, choose the dropdown SWO: ITM [port 0, type console]. It should now display:

[2019-01-02T21:35:26.457Z]   Hello, world!
  • SHIFT-F5 shuts down the debugger.

You may step, view the current context variables, add watches, inspect the call stack, add breakpoints, inspect peripherals and registers. Read more in the documentation for the plugin.

Caveats

Visual Studio Code is not an "IDE", its a text editor with plugin support, with an API somewhat limiting what can be done from within a plugin (in comparison to Eclipse, IntelliJ...) regarding panel layouts etc. E.g., as far as I know you cannot view the adapter output (openocd) at the same time as the ITM trace, they are both under the OUTPUT tab. Moreover, each time you re-start a debug session, you need to re-select the SWO: Name [port 0, type console] to view the ITM output. There are some hax around this:

  • Never shut down the debug session. Instead use the DEBUG CONSOLE (CTRL+SHIFT+Y) to get to the gdb console. This is not the full gdb interactive shell with some limitations (no tab completion e.g.). Make sure the MCU is stopped (F6). The console should show something like:
Program
 received signal SIGINT, Interrupt.
0x0800056a in main () at examples/itm.rs:31
31	    loop {}
  • Now you can edit an re-compile your program, e.g. changing the text:

iprintln!(stim, "Hello, again!");

  • In the DEBUG CONSOLE, write load press ENTER write monitor reset init press ENTER.
load
{"token":97,"outOfBandRecord":[{"isStream":false,"type":"status","asyncClass":"download","output":[]}]}
`/home/pln/rust/app/target/thumbv7em-none-eabihf/debug/examples/itm' has changed; re-reading symbols.
Loading section .vector_table, size 0x400 lma 0x8000000
Loading section .text, size 0x10c8 lma 0x8000400
Loading section .rodata, size 0x2a8 lma 0x80014d0
Start address 0x8001298, load size 6000
Transfer rate: 9 KB/sec, 2000 bytes/write.

mon reset init
{"token":147,"outOfBandRecord":[],"resultRecords":{"resultClass":"done","results":[]}}
adapter speed: 2000 kHz
target halted due to debug-request, current mode: Thread 
xPSR: 0x01000000 pc: 0x08001298 msp: 0x20018000
adapter speed: 8000 kHz
  • The newly compiled binary is now loaded and you can continue (F5). Switching to the OUTPUT window now preserves the ITM view and displays both traces:
[2019-01-02T21:43:27.988Z]   Hello, world!
[2019-01-02T22:07:29.090Z]   Hello, again!
  • Using the gdb terminal (DEBUG CONSOLE) from within vscode is somewhat instable/experimental. E.g., CTRL+c does not break the target (use F6, or write interrupt). The contiune command, indeed continues execution (and the control bar changes mode, but you cannot break using neither F6 nor interrupt). So it seems that the state of the cortex-debug plugin is not correctly updated. Moreover setting breakpoints from the gdb terminal indeed informs gdb about the breakpoint, but the state in vscode is not updated, so be aware.

launch configurations

The first three lauch configurations in the .vscode/launch.json file:

 {
            "type": "cortex-debug",
            "request": "launch",
            "servertype": "openocd",
            "name": "itm 64Mhz (debug)",
            "executable": "./target/thumbv7em-none-eabihf/debug/examples/itm",
            "configFiles": [
                "interface/stlink.cfg",
                "target/stm32f4x.cfg"
            ],
            "postLaunchCommands": [
                "monitor reset init"
            ],
            "swoConfig": {
                "enabled": true,
                "cpuFrequency": 64000000,
                "swoFrequency": 2000000,
                "source": "probe",
                "decoders": [
                    {
                        "type": "console",
                        "label": "ITM",
                        "port": 0
                    }
                ]
            },
            "cwd": "${workspaceRoot}"
        },
        {
            "type": "cortex-debug",
            "request": "launch",
            "servertype": "openocd",
            "name": "hello 16Mhz (debug)",
            "executable": "./target/thumbv7em-none-eabihf/debug/examples/hello",
            "configFiles": [
                "interface/stlink.cfg",
                "target/stm32f4x.cfg"
            ],
            "postLaunchCommands": [
                "monitor arm semihosting enable"
            ],
            "cwd": "${workspaceRoot}"
        },

      {
            "type": "cortex-debug",
            "request": "launch",
            "servertype": "openocd",
            "name": "itm 16Mhz (debug)",
            "executable": "./target/thumbv7em-none-eabihf/debug/examples/itm",
            // uses local config files
            "configFiles": [
                "./stlink.cfg",
                "./stm32f4x.cfg"
            ],
            "postLaunchCommands": [
                "monitor reset init"
            ],
            "swoConfig": {
                "enabled": true,
                "cpuFrequency": 16000000,
                "swoFrequency": 2000000,
                "source": "probe",
                "decoders": [
                    {
                        "type": "console",
                        "label": "ITM",
                        "port": 0
                    }
                ]
            },
            "cwd": "${workspaceRoot}"
        },

We see some similarities to the openocd.gdb file, we don't need to explicitly connect to the target (that is automatic). Also launching openocd is automatic (for good and bad, its re-started each time). postLaunchCommands allows arbitrary commands to be executed by gdb once the session is up. E.g. in the hello case we enable semihosting, while in the itm case we run monitor reset init to get the MCU in 64MHz (first example) or 16MHz (third example), before running the application (continue). Notice the first example uses the "stock" openocd configuration files, while the third example uses our local configuration files (that does not change the core frequency).

Advanced usage

There are numerous ways to automate gdb. Scripts can be run by the gdb command source (so for short). Scripting common tasks like setting breakpoints, dumping some memory region etc. can be really helpful.

License

This template is licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Code of Conduct

Contribution to this crate is organized under the terms of the Rust Code of Conduct, the maintainer of this crate, the Cortex-M team, promises to intervene to uphold that code of conduct.