Knurling-rs changelog #1

Since the releases of defmt and probe-run there have been many improvements in the defmt ecosystem and work on new tools. We have been posting those updates on Twitter but in case you missed them this blog post is a summary of those updates.

Location information

Log messages now include the location of the log statement. The location is presented as:

  • the path to the function that contains the log statement, e.g. some_crate::some_module::some_function
  • the file path and line number of the log statement, e.g. src/module.rs:12.
fn main() -> ! {
    defmt::info!("main");
    lpc_app::hal::init();

    defmt::info!("exiting ..");
    lpc_app::exit()
}
$ cargo run --bin hello
0.000000 INFO  main
└─ hello::__cortex_m_rt_main @ src/bin/hello.rs:8
0.000001 INFO  HAL has been initialized
└─ lpc_app::hal::init @ src/hal.rs:3
0.000002 INFO  exiting ..
└─ hello::__cortex_m_rt_main @ src/bin/hello.rs:11
(..)

The syntax used for this information is understood by VS code and can be clicked to navigate to the log statement.

Navigating to the location of a log statement in VS Code

Improved backtraces

Backtraces now include inlined functions – they'll appear as extra frames – as well as location information.

// src/bin/panic.rs
fn main() -> ! {
    foo();
    nrf_app::exit()
}

#[inline(always)]
fn foo() {
    bar()
}

#[inline(always)]
fn bar() {
    panic!()
}
stack backtrace:
   0: HardFaultTrampoline
      <exception entry>
   1: rust_begin_unwind
        at (..)/panic-probe/src/lib.rs:74
   2: core::panicking::panic_fmt
        at (..)/src/libcore/panicking.rs:85
   3: core::panicking::panic
        at (..)/src/libcore/panicking.rs:50
   4: panic::bar
        at src/bin/panic.rs:19
   5: panic::foo
        at src/bin/panic.rs:14
   6: panic::__cortex_m_rt_main
        at src/bin/panic.rs:8
   7: main
        at src/bin/panic.rs:6
   8: ResetTrampoline
        at (..)/cortex-m-rt-0.6.13/src/lib.rs:547
   9: Reset
        at (..)/cortex-m-rt-0.6.13/src/lib.rs:550

Previous example executed from VS code

Probe selection

You can specify which probe probe-run will use via the --probe flag. The flag takes an argument of the form $vendor_id:$product_id:[$serial_number]. This flag is particularly useful if you have more that one development board connected to your PC / laptop.

Some examples:

  • --probe 1fc9:0132 will use the probe that identifies itself as a USB device with a vendor ID of 0x1fc9 and product ID of 0x0132
  • --probe 1fc9:0132:0F01502C will use the probe that identifies itself as a USB device with a vendor ID of 0x1fc9, product ID of 0x0132 and a serial number of 0F01502C

To dedicate a whole Cargo project to a particular development board you can specify the flag in .cargo/config.toml

# .cargo/config.toml
runner = "probe-run --chip LPC845 --probe 1fc9:0132:0F01502C --defmt"

On the other hand, if you want to run programs within the same Cargo project on different boards you can use the PROBE_RUN_PROBE environment variable.

$ # terminal 1 - runs on device #1
$ PROBE_RUN_PROBE=1fc9:0132:0F01502C cargo run --bin my-app
$ # terminal 2 (same directory) - runs on device #2
$ PROBE_RUN_PROBE=1fc9:0132:502C0F01 cargo run --bin my-app

Stack overflow protection

We have built a linker wrapper, flip-link, that adds zero cost stack overflow protection to your embedded program.

Function calls and local (stack) variables inside those functions use stack space. Embedded devices have limited amount of RAM allocated for the (function call) stack. When a large number of functions are invoked in a nested fashion (recursive functions are particularly problematic) the stack can overflow its allocated space.

Unless a hardware mechanism like the Memory Protection Unit (MPU) has been configured to catch stack overflows most ARM Cortex-M programs will corrupt their own memory when the call stack overflows. ARM Cortex-M0 devices don't ship with a MPU so that kind of stack overflow protection is not possible on those devices. Another approach to stack overflow protection are stack probes but this requires LLVM support; currently only non-bare-metal x86 targets have stack probe support.

The approach used by flip-link consists of flipping the standard memory layout of ARM Cortex-M programs. With this inverted memory layout, the stack hits the boundary of the RAM region when it "overflows" instead of corrupting memory. This boundary collision raises a hardware exception (usually the "hard fault" exception), which by default halts the program.

Embedded programs loaded by probe-run will make probe-run exit with non-zero exit code on stack overflows.

Running a program that overflows its stack with and without stack overflow protection. Without protection the program corrupts its memory; resulting in some log messages being lost.

Stack overflow detection

Even if you don't use flip-link, probe-run can still detect stack overflows.

probe-run reports potential stack overflows as warnings and certain stack overflows as errors.

The warning mechanism is implemented by filling a small unused section of RAM that's farthest away from the start stack with a known bit pattern. When the program finishes this section is checked; if it was modified then the program used "too much stack space"; this is reported as a potential stack overflow.

The 'certain detection' mechanism checks the value of the Stack Pointer (SP) when the program exits abnormally (reaches the hard fault exception). If its value is outside an acceptable range, e.g. inside the .bss or .data section or outside the RAM region, then a stack overflow error is reported.

Note that in both cases if flip-link is not used then probe-run will report the stack overflow after memory has been corrupted.

Unit testing

We demoed using cargo test to run tests on an embedded device in a previous blog post. Since then we have created a procedural macro called defmt-test* to make writing unit tests more ergonomic.

*defmt-test is currently in preview state and only available to our GitHub sponsors

With this proc-macro you can write unit tests as if you were using the built-in #[test] attribute.

// testsuite/tests/test.rs

#[defmt_test::tests]
mod tests {
    #[test]
    fn assert_true() {
        assert!(true)
    }

    #[test]
    fn assert_false() {
        assert!(false)
    }
}
$ cargo test -p testsuite
0.000000 INFO  running assert_true ..
0.000001 INFO  .. assert_true ok
0.000002 INFO  running assert_false ..
0.000003 ERROR panicked at 'assertion failed: false', tests/test.rs:15:9
stack backtrace:
(..)
error: test failed, to rerun pass `-p testsuite --test all`

$ echo $?
134

With this proc-macro you can also specify some initialization (#[init]) code that will run before the unit tests. The value returned by the #[init] will be shared by the unit tests. You can use #[init] to initialize the target device peripherals.

#[defmt_test::tests]
mod tests {
    // TMP = some I2C temperature sensor
    #[init]
    fn init() -> tmp::Tmp {
        let i2c = hal::I2c::init();
        tmp::Tmp::new(i2c)
    }

    #[test]
    fn check_deviceid_register(tmp: &mut tmp::Tmp) {
        assert_eq!(tmp.read_deviceid_register(), tmp::DEVICEID);
    }

    #[test]
    fn reasonable_temperature(tmp: &mut tmp::Tmp) {
        const MIN: i8 = 10;
        const MAX: i8 = 40;
        let temp = tmp.read_temperature();
        assert!(temp >= MIN, "too cold");
        assert!(temp <= MAX, "too hot");
    }
}

Sponsor this work

probe-run and panic-probe are Knurling projects and can be funded through GitHub sponsors. Sponsors get early access to the tools we are building. Thank you to all of the people already sponsoring our work through the Knurling project!