Article

Knurling toolset v0.3 has been released!

Published on 9 min read
Open Source Project
Knurling icon
Knurling
A tool set to develop embedded applications faster.
Details ❤️ Sponsor

    Knurling-rs is Ferrous Systems' push to sustainably build better tooling for developing and debugging Rust software for embedded systems. Knurling-rs includes a suite of tools that make it easier to develop, log, debug, and test your embedded Rust libraries and applications!

    Knurling-rs is supported by our sponsors on GitHub. If you're interested in seeing more projects like this, consider becoming a sponsor today!

    The Knurling team is delighted to bring you version 0.3.0 of the Knurling toolset. Read on to learn what you can do with Knurling tools and what's new in version 0.3.0.

    What are Knurling tools all about?

    The goal of our tools and libraries is to make embedded Rust development, specifically the development of microcontroller programs, as ergonomic as regular Rust development. That means that if you write a program like this:

    fn main() -> ! {
        println!("Hello Knurling!");
        // ..
    }
    

    Then you should be able to cargo run this program and have it executed on a microcontroller.

    $ cargo run --bin hello
    (HOST) INFO  flashing program (6.36 KiB)
    (HOST) INFO  success!
    ─────────────────────────────────────────────────────────────
    Hello Knurling!
    └─ hello::main @ src/bin/hello.rs:8
    

    Knurling tools and libraries enable this kind of development.

    Apart from program flashing and basic I/O, Knurling also provides:

    • logging functionality;
    fn main() -> ! {
        error!("A");
        warn!("B");
        info!("C");
        debug!("D");
        trace!("E");
        app::exit()
    }
    
    
    $ DEFMT_LOG=info cargo run --bin log
    ERROR A
    └─ log::main @ src/bin/log.rs:8
    WARN  B
    └─ log::main @ src/bin/log.rs:9
    INFO  C
    └─ log::main @ src/bin/log.rs:10
    
    • (call stack) backtraces;
    fn main() -> ! {
        panic!("It's going down")
    }
    
    ERROR panicked at 'It's going down'
    └─ panic::main @ src/bin/panic.rs:8
    ─────────────────────────────────────────────────────────────
    stack backtrace:
       0: HardFaultTrampoline
          <exception entry>
       1: lib::inline::__udf
            at ./asm/inline.rs:172:5
       2: __udf
            at ./asm/lib.rs:49:17
       3: cortex_m::asm::udf
            at [cortex-m-0.7.3]/src/asm.rs:43:5
       4: _defmt_panic
            at src/lib.rs:13:5
       5: defmt::export::panic
            at [defmt-0.3.0]/src/export/mod.rs:125:14
       6: main
            at src/bin/panic.rs:8:5
       7: Reset
    
    • unit testing on the embedded hardware;
    #[test]
    fn assert_works() {
        assert!(true)
    }
    
    #[test]
    fn assert_fails() {
        assert_eq!(24, 42, "FIXME")
    }
    
    $ cargo test
    (HOST) INFO  flashing program (6.80 KiB)
    (HOST) INFO  success!
    ─────────────────────────────────────────────────────────────
    (1/2) running `assert_works`...
    └─ test::tests::__defmt_test_entry @ tests/test.rs:13
    (2/2) running `assert_fails`...
    └─ test::tests::__defmt_test_entry @ tests/test.rs:18
    ERROR panicked at 'assertion failed: `(left == right)`: FIXME'
    diff < left / right >
    <24
    >42
    └─ test::tests::assert_fails @ tests/test.rs:19
    ─────────────────────────────────────────────────────────────
    stack backtrace:
    (..)
    

    All this implemented in a way with small footprint in mind so it can run on small devices – think 8 MHz CPU and 16 KB RAM – without taking valuable resources away from the application.

    What's new in version 0.3?

    There have been lot of changes since the last 0.2.x release of the knurling crates. As you may guess from the change in the minor version number, those changes include user-facing breaking changes so you'll need to change things here and there when upgrading.

    This blog post will highlight the main new features and point out breaking changes. For instructions on how to upgrade your codebase from 0.2.x to 0.3.0 check out our migration guide!

    New log filter

    You already caught a glimpse of this feature in the previous section! The logging framework, implemented in the defmt library, now uses an environment variable, named DEFMT_LOG, for its filtering mechanism – it used Cargo features in v0.2.x but those have been removed. If you have used the RUST_LOG environment variable, as implemented in the env_logger crate, then you'll feel at home because the DEFMT_LOG user interface is exactly the same.

    // src/bin/levels.rs
    fn main() -> ! {
        defmt::info!("info");
        defmt::trace!("trace");
        defmt::warn!("warn");
        defmt::debug!("debug");
        defmt::error!("error");
    
        app::exit()
    }
    
    $ # default = only ERROR level logs
    $ cargo r --bin levels
    ERROR error
    └─ levels::main @ src/bin/levels.rs:12
    
    $ # enable the INFO and higher severity levels
    $ DEFMT_LOG=info cargo r --bin levels
    INFO  info
    └─ levels::main @ src/bin/levels.rs:8
    WARN  warn
    └─ levels::main @ src/bin/levels.rs:10
    ERROR error
    └─ levels::main @ src/bin/levels.rs:12
    

    With the log filter mechanism implemented in v0.2.x you could only filter out or select logs from entire crates. With the new DEFMT_LOG mechanism you can filter with module-level granularity.

    // src/bin/filter.rs (crate-name = filter)
    fn main() -> ! {
        defmt::info!("main");
        module::g();
        // ..
    }
    
    mod module {
        mod inner {
            pub fn f() {
                defmt::info!("module::inner::f");
            }
        }
    
        pub fn g() {
            defmt::info!("module::g");
            inner::f();
        }
    }
    
    $ # only include the `module`
    $ DEFMT_LOG=filter::module cargo r --bin filter
    INFO  module::g
    └─ filter::module::g @ src/bin/filter.rs:21
    INFO  module::inner::f
    └─ filter::module::inner::f @ src/bin/filter.rs:16
    
    $ # include `module` but omit `module::inner`
    $ DEFMT_LOG=filter::module,filter::module::inner=off cargo r --bin filter
    INFO  module::g
    └─ filter::module::g @ src/bin/filter.rs:21
    

    An interesting difference between the defmt log filter and env_logger is that logging is removed at compile time, whereas env_logger logging code is always kept around and a runtime check is performed to decide whether to emit logs or not. So, disabling the logs with DEFMT_LOG=off will make your application smaller (binary size). The downside is that changing DEFMT_LOG requires a recompilation of all crates that use defmt – note that the recompilation happens automatically on the next cargo build when you change DEFMT_LOG.

    $ DEFMT_LOG=trace cargo b --release --bin levels
    $ size target/*/release/levels
       text    data     bss     dec     hex filename
       6460      48    1032    7540    1d74 levels
    
    $ DEFMT_LOG=off cargo b --release --bin levels
    $ size target/*/release/levels
       text    data     bss     dec     hex filename
       2016      48    1032    3096     c18 levels
    

    4 KB savings in Flash after disabling all logging.

    println!

    This feature is related to the log filter. In addition to the five existing logging macros, v0.3.0 includes a println! macro. Unlike the logging macros, println! cannot be filtered out with DEFMT_LOG so it's always included in the output of probe-run.

    // src/bin/hello.rs
    fn main() -> ! {
        defmt::println!("Hello Knurling!");
        // ..
    }
    
    $ DEFMT_LOG=off cargo run --bin hello
    Hello, world!
    └─ hello::main @ src/bin/hello.rs:8
    

    Leaner and more robust defmt

    Upon upgrading from defmt v0.2.x to defmt v0.3.0 you may see a noticeable reduction in the binary size of your application. Thanks to the work done by community member @Dirbaio, the encoding internally done by defmt is now much easier to optimize for size (e.g. opt-level=z) by the compiler.

    // src/bin/panic.rs
    fn main() -> ! {
        defmt::panic!()
    }
    
    $ # opt-level = 'z'
    $ cargo b --release --bin panic
    
    $ # defmt v0.2.3
    $ size target/*/release/panic
       text    data     bss     dec     hex filename
       5912      48    1032    6992    1b50 panic
    
    $ # defmt v0.3.0
    $ size target/*/release/panic
       text    data     bss     dec     hex filename
       5444      48    1032    6524    197c panic
    

    Dirbaio also added a rzCOBS (Reverse-Zerocompressing-COBS) encoding option, which is now the default encoding, to defmt. rzCOBS encodes data in frames so decoders like probe-run are now able to detect errors in the encoded data and skip corrupted frames. It's unlikely that you'll run into corrupted data when using probe-run as USB is rather reliable but if you are using defmt on top of a serial interface (e.g. RS232 or RS485) then the added reliability of using frames will come in handy.

    A more flexible write!

    This is another contribution by Dirbaio!

    The write! macro is used to manually implement the defmt::Format trait, which specifies how a type is formatted when used with defmt macros. In v0.2.x it was not possible to invoke the write! macro more than once within a single Format implementation so using it inside a for loop would yield a compiler error.

    struct Braces<'a>(&'a [i32]);
    
    impl Format for Braces<'_> {
        fn format(&self, f: defmt::Formatter) {
            defmt::write!(f, "{{ ");
            let mut is_first = true;
            for item in self.0 {
                if !is_first {
                    defmt::write!(f, ", ");
                }
                defmt::write!(f, "{}", item);
                is_first = false;
            }
            defmt::write!(f, " }}");
        }
    }
    
    defmt::println!("{}", Braces(&[0, 1])); // -> { 0, 1 }
    

    This restriction has been removed in v0.3.0 so the above code now works 🎉.

    #[ignore] tests

    defmt-test is a no-std test harness that supports running tests on resource constrained embedded devices, e.g. microcontrollers. It provides the core functionality found in the "standard" test harness, the test crate: #[test] functions, which may return a Result; #[should_error] (instead of #[should_panic]); etc. but was missing support for the #[ignore] in the v0.2.x series. Release v0.3.0 includes support for the #[ignore] attribute.

    As it says on the tin, an ignored test will be compiled but not executed. It will still be reported in the output of the test harness and accounted for in the test count.

    #[defmt_test::tests]
    mod tests {
        #[test]
        fn f() {
            assert!(true);
        }
    
        #[test]
        #[ignore]
        fn g() {
            assert!(false);
        }
    }
    
    $ cargo t
    (1/2) running `f`...
    (2/2) ignoring `g`...
    all tests passed!
    

    probe-run CLI

    probe-run saw a breaking change in its command line interface around the configuration of backtraces. v0.2.x has two flags to control backtraces: --force-backtrace and --max-backtrace-len. These have been replaced with flags: --backtrace and --backtrace-limit.

    --backtrace follows the convention of having 3 auto, on and off options. on and off are self-explanatory; auto means print a backtrace if the application ended abnormally: either due to panic, a 'hardware fault' or a stack overflow.

    --backtrace-limit controls how many frames are included in the backtrace. The default is to include 50 frames and --backtrace-limit=0 disables the limit, which is another well-established convention.

    Outro

    That covers the new features and breaking changes in the v0.3.0 release!

    If you have haven't tried out the Knurling tools and libraries before, check out the book! If you are migrating from v0.2.x to v0.3, check out the migration guide!

    As usual, we want to thank all our of sponsors for sustaining Knurling's maintenance and development. We also want to give a special thank to the many collaborators that have reported bugs and brought up ideas for new features; they have also made this v0.3 release possible.

    Sponsor this work

    Knurling-rs is mainly funded through GitHub sponsors. Sponsors get early access to the tools we are building and help us to support and grow the knurling tools and courses. Thank you to all of the people already sponsoring our work through the Knurling project!