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:
(..)
- and a Cargo project template that bundles all the libraries and tools together to get you easily started
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!