Skip to content

Commit

Permalink
Second round of changes to the MCU docs (#1624)
Browse files Browse the repository at this point in the history
  • Loading branch information
tronical authored Sep 12, 2022
1 parent 911515b commit f600181
Showing 1 changed file with 56 additions and 66 deletions.
122 changes: 56 additions & 66 deletions api/rs/slint/mcu.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
# 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 sections explain how to use Slint to develop a UI on a MCU in a bare metal environment.

## 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 currently only possible in the nightly version of Rust, because 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
[#66741](https://github.com/rust-lang/rust/issues/66741) for tracking issues).

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`

Start by adding a dependency to the `slint` and the `slint-build` crate to your `Cargo.toml`:
Start by adding a dependency to the `slint` and the `slint-build` crates to your `Cargo.toml`:

```toml
[dependencies]
Expand All @@ -35,31 +35,24 @@ default-features = false
features = ["compat-0.3.0", "unsafe-single-threaded", "libm"]

[build-dependencies]
slint-build = "0.3"
slint-build = "0.3.0"
```

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 macro is only available in the Rust Standard Library (std), but not 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 a single thread, and not from interrupt handlers.
* `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 operate 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,21 @@ loop {

### The Renderer

In desktop and embedded environments, Slint typically uses operating system provided, often hardware-accelerated APIs to render the user interface.
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.
In desktop and embedded environments, Slint typically uses operating system provided APIs to render the user interface using the GPU.
In contrast, most MCUs don't have GPUs. Instead, the all the rendering is done by sotfware on the CPU. This is called software rendering, and Slint provides a SoftwareRenderer for this task.

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 +289,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 +372,14 @@ 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
representation before sending them to the screen driver.

## 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

0 comments on commit f600181

Please sign in to comment.