Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Second round of changes to the MCU docs #1624

Merged
merged 42 commits into from
Sep 12, 2022
Merged
Changes from 31 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
a16f2be
Refer to this as sections not document
tronical Sep 12, 2022
28c9384
Rename the toolchain/HAL section to prerequisites
tronical Sep 12, 2022
49027f7
Rephrase the memory allocator paragraph
tronical Sep 12, 2022
e06ea2d
Take the person out of the section connecting sentence - it's simpler…
tronical Sep 12, 2022
5e56baf
create -> crate
tronical Sep 12, 2022
9495fc6
bare-meta -> bare metal
tronical Sep 12, 2022
354245b
Minor edit
tronical Sep 12, 2022
ffb7817
Polish compat-0.3.0 explanation
tronical Sep 12, 2022
7c3a183
Use but instead of which to emphasize the contrast between std and ba…
tronical Sep 12, 2022
425bfbf
Don't use future tense for what describes the present
tronical Sep 12, 2022
2001137
Simplify the unsafe-single-threaded guarantees
tronical Sep 12, 2022
b00011e
Fix reference to std feature
tronical Sep 12, 2022
db4c515
Fix `libm` feature description
tronical Sep 12, 2022
3192933
Re-introduce the HAL abbreviation
tronical Sep 12, 2022
7cdeb65
Fix the build.rs section order
tronical Sep 12, 2022
20ca2ad
Fix application structure intro
tronical Sep 12, 2022
cb1e761
Remove use of typical
tronical Sep 12, 2022
1fc9eaa
Use user interface consistently
tronical Sep 12, 2022
640ba85
In bare metal environments it is your responsibility to substitute th…
tronical Sep 12, 2022
b0ea94e
Shorten application structure bullet points
tronical Sep 12, 2022
969aa30
Connect the crate selection with the need of writing drivers better
tronical Sep 12, 2022
3122ff4
Clarify the purpose of forward input from peripherals to Slint
tronical Sep 12, 2022
e1a6ca0
Minor sentence polish
tronical Sep 12, 2022
1a3005d
Use contractions
tronical Sep 12, 2022
90247c5
an minimal -> a minimal
tronical Sep 12, 2022
ff5855e
Remove stray "in order to"
tronical Sep 12, 2022
f206d2f
since -> between
tronical Sep 12, 2022
7418994
Simplify the additional functionality the trait can be used to implement
tronical Sep 12, 2022
bbadf2f
Polish and simplify the renderer section a little bit
tronical Sep 12, 2022
988d8c7
Use "Rendering" consistently for both rendering section titles
tronical Sep 12, 2022
971bdbd
Attemt to simplify the remaining sections in one go
tronical Sep 12, 2022
bff6e9f
Fix typo
tronical Sep 12, 2022
4b47af5
The global memory allocator support is available *in* the rust version
tronical Sep 12, 2022
fe669bf
Emphasize that only the night version has what we need
tronical Sep 12, 2022
fe1f3ce
Fix missing plural
tronical Sep 12, 2022
0d55efd
Consistently use 0.3.0 instead of mixing 0.3 and 0.3.0
tronical Sep 12, 2022
817bade
Clarify that thread_local! as a macro is not available
tronical Sep 12, 2022
12fea26
drive -> operate
tronical Sep 12, 2022
6b6027a
Update api/rs/slint/mcu.md
tronical Sep 12, 2022
434274a
Update api/rs/slint/mcu.md
tronical Sep 12, 2022
63b99cb
Additional suggestions from Olivier
tronical Sep 12, 2022
94c4ad9
only available in the nightly version -> only possible with the night…
tronical Sep 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 54 additions & 60 deletions api/rs/slint/mcu.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
# Slint on Microcontrollers (MCU)

This document explains how to use Slint to develop a UI on a MCU in a bare metal environment.
The following scetions explain how to use Slint to develop a UI on a MCU in a bare metal environment.
ogoffart marked this conversation as resolved.
Show resolved Hide resolved

## Install Toolchain / Hardware Abstraction Layer (HAL)
## Prerequisites

Writing an application in Rust that runs on a MCU requires the following software components:
Writing an application in Rust that runs on a MCU requires several prerequisites:

* You need to install a Rust toolchain to cross-compile to the target architecture.
* You need to locate and select the correct HAL crates and drivers, and depend on them in your `Cargo.toml`.
* You need to install tools for flashing and debugging your code on the device.
* Install a Rust toolchain to cross-compile to the target architecture.
* Locate and select the correct Hardware Abstraction Layer (HAL) crates and drivers, and depend on them in your `Cargo.toml`.
* Install tools for flashing and debugging your code on the device.

Covering these is out of scope for this document, but we recommend reading the [Rust Embedded Book](https://docs.rust-embedded.org/book/),
We recommend reading the [Rust Embedded Book](https://docs.rust-embedded.org/book/),
as well as the curated list of [Awesome Embedded Rust](https://github.com/rust-embedded/awesome-embedded-rust) for a wide range of different
crates, tools and training materials. These resources should guide you through the initial setup and often come with "hello world" examples
to get started with your device.

In order to set a global allocator, required by Slint, you will need a **nightly** version of Rust. This is due to the fact that the support for using a custom global allocator in a bare metal
environment with `#![no_std]` has not been stabilized yet (see [#51540](https://github.com/rust-lang/rust/issues/51540) or
Slint requires a global memory allocator. That is available the nightly version of Rust, because the support for using a custom global
ogoffart marked this conversation as resolved.
Show resolved Hide resolved
allocator in a bare metal environment with `#![no_std]` has not been stabilized yet (see [#51540](https://github.com/rust-lang/rust/issues/51540) or
[#66741](https://github.com/rust-lang/rust/issues/66741) for tracking issues).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe change this paragraph to:

Slint requires a global memory allocator, only available in the nightly version of Rust at this time. We can support stable Rust once support for using a custom global
allocator in a bare metal environment with #![no_std] has been stabilized (see #51540 or
#66741 for tracking issues).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comments on a oudated version of the wording.
I think the current wording is good enough.
If you think it could be improved, feel free to change it.


In the following sections we assume that your setup is complete and you have a non-graphical skeleton Rust program running on your MCU.
The following sections assume that your setup is complete and you have a non-graphical skeleton Rust program running on your MCU.

## Changes to `Cargo.toml`

Expand All @@ -38,28 +38,21 @@ features = ["compat-0.3.0", "unsafe-single-threaded", "libm"]
slint-build = "0.3"
```

The default features of the `slint` create are tailored towards hosted environments and includes the "std" feature. In bare metal environments,
The default features of the `slint` crate are tailored towards hosted environments and includes the "std" feature. In bare metal environments,
you need to disable the default features.

Three features are selected:
In the snippet above, three features are selected:

* `compat-0.3.0`: You need to select this feature when disabling the default features. See [this blog post](https://slint-ui.com/blog/rust-adding-default-cargo-feature.html)
for a detailed explanation.
* `compat-0.3.0`: We select this feature when disabling the default features. For a detailed explanation see our blog post ["Adding default cargo features without breaking Semantic Versioning"](https://slint-ui.com/blog/rust-adding-default-cargo-feature.html).
* `unsafe-single-threaded`: Slint internally uses Rust's [`thread_local!`](https://doc.rust-lang.org/std/macro.thread_local.html) macro to store global data.
This feature is only available in the Rust Standard Library (std), which is not available in bare-metal environments. As a fallback, the `unsafe-single-threaded`
feature will change Slint to use unsafe static for storage. By setting this feature, you guarantee not to use Slint API from a thread other than the main thread,
or from interrupt handlers.
* `libm`: The Rust Standard Library (std) provides traits and functions for floating point arithmetic. Without `std`, this feature enables the use of the
[libm](https://crates.io/crates/libm) crate to substitute this functionality.
This feature is only available in the Rust Standard Library (std), but is not available in bare metal environments. As a fallback, the `unsafe-single-threaded`
feature changes Slint to use unsafe static for storage. This way, you guarantee to use Slint API only from the main thread, or from interrupt handlers.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This feature is only available in the Rust Standard Library (std), but is not available in bare metal environments. As a fallback, the `unsafe-single-threaded`
feature changes Slint to use unsafe static for storage. This way, you guarantee to use Slint API only from the main thread, or from interrupt handlers.
This macro is only available in the Rust Standard Library (std), not in bare metal environments. As a fallback, the `unsafe-single-threaded`
feature changes Slint to use unsafe static for storage. By selecting this feature, you must ensure that you only use
the Slint API from a single thread and not from interrupt handlers.

The sense of the sentence was changed: It is not possible to use slint from interrupt handlers

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remove the redundant "available" (well spotted).

Well spotted about the change of meaning!

* `libm`: We select this feature to enable the use of the [libm](https://crates.io/crates/libm) crate to provide traits and functions for floating point arithmetic.
They are typically provided by the Rust Standard Library (std), but that is not available in bare metal environments.

## Changes to `build.rs`

When targeting MCUs, you need a [build script](https://doc.rust-lang.org/cargo/reference/build-scripts.html) to compile the `.slint` files using the `slint-build` crate.
Use the `slint_build::EmbedResourcesKind::EmbedForSoftwareRenderer` configuration option to tell the slint compiler to embed the images and fonts in the binary
in a format that's suitable for the software based renderer we're going to use.

The following example of a `build.rs` script compiles the `main.slint` design file in the `ui/` sub-directory to Rust code and embeds the code as well as all
graphical assets needed into the program binary.
Next, write a build script to compile the `.slint` files to Rust code for embedding into the program binary, using the `slint-build` crate:

```rust,no_run
fn main() {
Expand All @@ -71,44 +64,47 @@ fn main() {
}
```

Use the `slint_build::EmbedResourcesKind::EmbedForSoftwareRenderer` configuration option to tell the Slint compiler to embed the images and fonts in the binary
in a format that's suitable for the software based renderer we're going to use.

## Application Structure

A graphical application in hosted environments is typically composed of at least three different tasks:
Typically, a graphical application in hosted environments is comprised of at least three different tasks:

* Receive user input from operation system APIs.
* React to the input by performing application specific computations.
* Render an updated user interface and present it on the screen using device-independent operating system APIs.
* Receives user input from operation system APIs.
* Reacts to the input by performing application specific computations.
* Renders an updated user interface and presents it on the screen using device-independent operating system APIs.

The operating system provides what is typically called an event loop to connect and schedule these tasks. Slint implements the
task of receiving user input and forwarding it to the user interface layer, as well as rendering the interface to the screen.
The operating system provides an event loop to connect and schedule these tasks. Slint implements the
task of receiving user input and forwarding it to the user interface layer, as well as rendering the user interface to the screen.

In bare metal environments it becomes your responsibility to substitute and connect functionality that is otherwise provided by the operating system:
In bare metal environments it is your responsibility to substitute and connect functionality that is otherwise provided by the operating system:

* You need to select crates that allow you to initialize the chips that drive peripherals, such as a touch input or display controller.
Sometimes it may be necessary for you to develop your own drivers.
* You need to drive the event loop yourself by querying peripherals for input, forwarding input into computational modules of your
* Select crates that allow you to initialize the chips that drive peripherals, such as a touch input or display controller.
If there are no crates, you may have to to develop your own drivers.
* Drive the event loop yourself by querying peripherals for input, forwarding that input into computational modules of your
application and instructing Slint to render the user interface.

In Slint the two primary APIs you need to use to accomplish these tasks the [`slint::platform::Platform`] trait as well as the [`slint::Window`] struct.
In the following sections we are going to cover how to use them and how they integrate into your event loop.
In Slint, the two primary APIs you need to use to accomplish these tasks are the [`slint::platform::Platform`] trait as well as the [`slint::Window`] struct.
In the following sections we're going to cover how to use them and how they integrate into your event loop.

### The `Platform` Trait

The [`slint::platform::Platform`] trait defines the interface between Slint and platform APIs typically provided by operating and windowing systems.

You need to provide an minimal implementation of this trait and call [`slint::platform::set_platform`] before constructing your Slint application.
You need to provide a minimal implementation of this trait and call [`slint::platform::set_platform`] before constructing your Slint application.

A minimal implementation needs to cover two functions:
This minimal implementation needs to cover two functions:

* `fn create_window_adapter(&self) -> Rc<dyn WindowAdapter + 'static>;`: Implement this function to return an implementation of the `WindowAdapter`
trait that will be associated with the Slint components you create. We provide a convenience struct [`slint::platform::software_renderer::MinimalSoftwareWindow`]
that implements this trait.
* `fn duration_since_start(&self) -> Duration`: In order for animations in `.slint` design files to change properties correctly, Slint needs to know
how much time has elapsed since two rendered frames. In a bare metal environment you need to provide a source of time. Often the HAL crate of your
* `fn duration_since_start(&self) -> Duration`: For animations in `.slint` design files to change properties correctly, Slint needs to know
how much time has elapsed between two rendered frames. In a bare metal environment you need to provide a source of time. Often the HAL crate of your
device provides a system timer API for this, which you can query in your impementation.

There are additional functions in the trait that you can implement, for example handling of debug output, a delegated event loop or an interface
to safely deliver events in multi-threaded environments.
There are additional functions in the trait that you can implement, for example to handle debug output, to delegate the event loop or to implement
the interface to safely deliver events in multi-threaded environments.

A typical minimal implementation of the [`Platform`] trait that uses the [`MinimalSoftwareWindow`] looks like this:

Expand Down Expand Up @@ -225,24 +221,22 @@ loop {

### The Renderer

In desktop and embedded environments, Slint typically uses operating system provided, often hardware-accelerated APIs to render the user interface.
In desktop and embedded environments, Slint typically uses operating system provided APIs to render the user interface. These APIs are often hardware accelerated.
In contrast, most MCUs don't have dedicated chips to render advanced graphics. Instead, the CPU is responsible for computing the colors of each
pixels on the screen. This is called software rendering, and Slint provides a software renderer for this task.
tronical marked this conversation as resolved.
Show resolved Hide resolved

In the previous example, we've instantiated a [`slint::platform::software_renderer::MinimalSoftwareWindow`]. This struct implements the
`slint::platform::WindowAdapter` trait and also holds an instance of a [`slint::platform::software_renderer::SoftwareRenderer`]. You can access it
through the [`draw_if_needed()`](MinimalSoftwareWindow::draw_if_needed) function.
`slint::platform::WindowAdapter` trait and also holds an instance of a [`slint::platform::software_renderer::SoftwareRenderer`]. You obtain access to it
through the callback parameter of the [`draw_if_needed()`](MinimalSoftwareWindow::draw_if_needed) function.

We provide two different ways of using the renderer, depending on the amount of RAM your MCU is equipped with and the kind of screen that is attached:
Depending on the amount of RAM your MCU is equipped with, and the kind of screen that is attached, you can choose between two different ways of using the renderer:

* Use the [`SoftwareRenderer::render()`] function if you have enough RAM to allocate one, or even two, copies of the entire screen (also known as
frame buffer).
* Use the [`SoftwareRenderer::render_by_line()`] function to render the entire user interface line by line and send each line of pixels to the screen,
typically via the SPI. This requires allocating at least enough RAM to store one single line of pixels.

With both methods you instruct Slint to render into a buffer, that either represents the entire screen or just a line. That buffer is a slice of
a type that implements the [`slint::platform::software_renderer::TargetPixel`] trait.

With both methods Slint renders into a provided buffer, which is a slice of a type that implements the [`slint::platform::software_renderer::TargetPixel`] trait.
For convenience, Slint provides an implementation for [`slint::Rgb8Pixel`] as well as [`slint::platform::software_renderer::Rgb565Pixel`].

#### Rendering into a Buffer
Expand Down Expand Up @@ -296,17 +290,17 @@ loop {

```

#### Render Line by Line
#### Rendering Line by Line

When rendering the user interface line by line, you need to implement the [`LineBufferProvider`] trait. It
defines a bi-directional interface between Slint and your code to send lines to your screen:
defines a bi-directional interface between Slint and your code to send lines to the screen:

* Through the associated `TargetPixel` type Slint knows how to create and manipulate pixels without having to know
the exact device-specific binary representation and operations for blending.
* Through the `process_line` function Slint notifies you when a line can be rendered and provides a callback that
you can invoke to fill a slice of pixels for the given line.
* The trait's associated `TargetPixel` type let's Slint know how to create and manipulate pixels. How exactly the pixels are
represented in your device and how they are blended remains your implementation detail.
* The trait's `process_line` function notifies you when a line can be rendered and provides a callback that you can invoke
to fill a slice of pixels for the given line.

The following example defines a `DisplayWrapper` struct that connects screen driver that implements the [`embedded_graphics`](https://lib.rs/embedded-graphics) traits
The following example defines a `DisplayWrapper` struct: It connects screen driver that implements the [`embedded_graphics`](https://lib.rs/embedded-graphics) traits
with Slint's `Rgb565Pixel` type to implement the `LineBufferProvider` trait. The pixels for one line are sent to the screen by calling
the [DrawTarget::fill_contiguous](https://docs.rs/embedded-graphics/0.7.1/embedded_graphics/draw_target/trait.DrawTarget.html) function.

Expand Down Expand Up @@ -379,17 +373,17 @@ loop {

```

Hint: In our experience, using the synchronous `DrawTarget::fill_contiguous` function is slow. If
Note: In our experience, using the synchronous `DrawTarget::fill_contiguous` function is slow. If
your device is capable of using DMA, you may be able to achieve better performance by using
two line buffers: One buffer to render into with the CPU, while the other buffer is transferred to
the screen using DMA asynchronously.

Hint: If you see wrong colors on your device, it may be necessary to invert the bits in the u16 pixel
Note: If you see wrong colors on your screen, it may be necessary to invert the bits in the u16 pixel
representation before sending them to the screen driver.

tronical marked this conversation as resolved.
Show resolved Hide resolved
## Example Implementations

Slint's own examples use a helper crate called `mcu-board-support` that provides implementations of
The examples that come with Slint use a helper crate called `mcu-board-support`. It provides implementations of
the `Platform` trait for some MCUs, along with support for touch input and system timers.

You can find the crate in our Git repository at:
Expand Down