Creating an new Embedded Rust Projects for NXP LPC55S69

In “Getting Started with Rust on NXP LPC55S69-EVK” I demonstrated how easy it is to run a ‘blinky’ with Rust. I used the Embassy framework.

rust on LPC55S69
rust on LPC55S69

In this article, I show how one can create a standalone Rust project for an embedded target.

Outline

The Embassy Book at https://embassy.dev/book/index.html#_starting_a_new_project explains the steps how to create a new Rust project. It is using an STM32 as reference, with ‘adopt as needed for your target’. To make it easier for Rust on NXP, here are the steps to do the same for the NXP LPC55S69. So this can help you too if you are targeting a different MCU than the STM32 as well.

Prerequisite: you should have Embassy installed. You also need a working blinky on your board. See Getting Started with Rust on NXP LPC55S69-EVK. And we are creating our new project from the Embassy example in embassy\examples\lpc55s69.

Creating New Project

Create a new project with:

$ cargo new lpc
$ cd lpc

This creates the next structure:

lpc
| .gitignore
| Cargo.toml
\---src
main.rs

Blinky Code

Copy the example (blinky) code and replace the content in main.rs: Here for the LPC:

 //! This example has been made with the LPCXpresso55S69 board in mind, which has a built-in LED on PIO1_6. #![no_std] #![no_main] use cortex_m::asm::nop; use defmt::*; use embassy_executor::Spawner; use embassy_nxp::gpio::{Level, Output}; use {defmt_rtt as _, panic_halt as _}; #[embassy_executor::main] async fn main(_spawner: Spawner) { let p = embassy_nxp::init(Default::default()); let mut led = Output::new(p.PIO1_6, Level::Low); loop { info!("led off!"); led.set_high(); for _ in 0..200_000 { nop(); } info!("led on!"); led.set_low(); for _ in 0..200_000 { nop(); } } } 

.cargo/config.toml

Copy the example .cargo/config.toml into the new project. It hast the following content:

[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip LPC55S69JBD100"

[build]
target = "thumbv8m.main-none-eabihf"

[env]
DEFMT_LOG = "debug"

In above file we already have the correct compiler for the Cortex-M33 (thumbv8m) plus the correct chip (LPC55S69JBD100) for probe-rs.

Our new project looks now like this:

lpc
| .gitignore
| Cargo.toml
\---src
main.rs

.cargo/config.toml

We need to tell cargo which toolchain to run with cargo build or cargo run. For this we can copy the .cargo/config.toml from the example project:

[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip LPC55S69JBD100"

[build]
target = "thumbv8m.main-none-eabihf"

[env]
DEFMT_LOG = "debug"

With this, the project looks like this:

lpc
| .gitignore
| Cargo.toml
+---.cargo
| config.toml

\---src
main.rs

Cargo.toml

In Cargo.toml we have to describe the dependencies and crates used. Copy the Cargo.toml from the example and overwrite the existing one.

This is how it looks right now:

[package]
edition = "2024"
name = "embassy-nxp-lpc55s69-examples"
version = "0.1.0"
license = "MIT OR Apache-2.0"

publish = false

[dependencies]
embassy-nxp = { version = "0.1.0", path = "../../embassy-nxp", features = ["lpc55-core0", "rt", "defmt", "time-driver-rtc"] }
embassy-executor = { version = "0.9.0", path = "../../embassy-executor", features = ["arch-cortex-m", "executor-thread", "executor-interrupt"] }
embassy-sync = { version = "0.7.2", path = "../../embassy-sync", features = ["defmt"] }
embassy-time = { version = "0.5.0", path = "../../embassy-time", features = ["defmt", "tick-hz-32_768"] }
panic-halt = "1.0.0"
cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] }
cortex-m-rt = { version = "0.7.0"}
defmt = "1.0.1"
defmt-rtt = "1.0.0"
panic-probe = { version = "1.0.0", features = ["print-defmt"] }
panic-semihosting = "0.6.0"

[profile.release]
debug = 2

[package.metadata.embassy]
build = [
{ target = "thumbv8m.main-none-eabihf", artifact-dir = "out/examples/lpc55s69" }
]

First, change its name to whatever you like:

name = "blinky"

Next, we have to remove the dependencies to the local embassy repository and replace/patch it with crate patch entries.

In the [dependencies], there are references to

path = "../../embassy-nxp", 

Remove them all. The result then looks like this:

[dependencies]
embassy-nxp = { version = "0.1.0", features = ["lpc55-core0", "rt", "defmt", "time-driver-rtc"] }
embassy-executor = { version = "0.9.0", features = ["arch-cortex-m", "executor-thread", "executor-interrupt"] }
embassy-sync = { version = "0.7.2", features = ["defmt"] }
embassy-time = { version = "0.5.0", features = ["defmt", "tick-hz-32_768"] }
panic-halt = "1.0.0"
cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] }
cortex-m-rt = { version = "0.7.0"}
defmt = "1.0.1"
defmt-rtt = "1.0.0"
panic-probe = { version = "1.0.0", features = ["print-defmt"] }
panic-semihosting = "0.6.0"

[patch.crates.io]

Next, we need to find the git hash key for the embassy crates. It has to match the versions we are using in the [dependencies]! If we are using the mainline/HEAD version we can get it from git with the next command:

$ git ls-remote https://github.com/embassy-rs/embassy.git HEAD

Which gives the key in the next line below, at the time of writing this article:

88de32c168216c98259380f76edc54ec49e1a81a HEAD

With this information, add a [patch.crates.io] section to the Cargo.toml, for all the embassy items:

[patch.crates-io]
embassy-nxp = { git = "https://github.com/embassy-rs/embassy", rev = "88de32c168216c98259380f76edc54ec49e1a81a" }
embassy-executor = { git = "https://github.com/embassy-rs/embassy", rev = "88de32c168216c98259380f76edc54ec49e1a81a" }
embassy-sync = { git = "https://github.com/embassy-rs/embassy", rev = "88de32c168216c98259380f76edc54ec49e1a81a" }
embassy-time = { git = "https://github.com/embassy-rs/embassy", rev = "88de32c168216c98259380f76edc54ec49e1a81a" }

package.metadata.embassy

The original Cargo.toml has the next entry:

[package.metadata.embassy]
build = [
{ target = "thumbv8m.main-none-eabihf", artifact-dir = "out/examples/lpc55s69" }
]

Remove that section and entry, as this is only for the example build.

That’s it for the Cargo.toml.

Final Cargo.toml

The result looks like this:

[package]
edition = "2024"
name = "blinky"
version = "0.1.0"
license = "MIT OR Apache-2.0"

[dependencies]
embassy-nxp = { version = "0.1.0", features = ["lpc55-core0", "rt", "defmt", "time-driver-rtc"] }
embassy-executor = { version = "0.9.0", features = ["arch-cortex-m", "executor-thread", "executor-interrupt"] }
embassy-sync = { version = "0.7.2", features = ["defmt"] }
embassy-time = { version = "0.5.0", features = ["defmt", "tick-hz-32_768"] }
panic-halt = "1.0.0"
cortex-m = { version = "0.7.6", features = ["inline-asm", "critical-section-single-core"] }
cortex-m-rt = { version = "0.7.0"}
defmt = "1.0.1"
defmt-rtt = "1.0.0"
panic-probe = { version = "1.0.0", features = ["print-defmt"] }
panic-semihosting = "0.6.0"

[patch.crates-io]
embassy-nxp = { git = "https://github.com/embassy-rs/embassy", rev = "88de32c168216c98259380f76edc54ec49e1a81a" }
embassy-executor = { git = "https://github.com/embassy-rs/embassy", rev = "88de32c168216c98259380f76edc54ec49e1a81a" }
embassy-sync = { git = "https://github.com/embassy-rs/embassy", rev = "88de32c168216c98259380f76edc54ec49e1a81a" }
embassy-time = { git = "https://github.com/embassy-rs/embassy", rev = "88de32c168216c98259380f76edc54ec49e1a81a" }

[profile.release]
debug = 2

Rust Toolchain

We need to cargo what toolchain to use. Create a file rust-toolchain.toml in the project root:

lpc
| .gitignore
| Cargo.toml
| rust-toolchain.toml
+---.cargo
| config.toml
\---src
main.rs

You can use the one from embassy/rust-toolchain.toml as base and remove everything not needed. In my case the target is a Cortex-M33, so I keep the thumbv8m toolchain:

[toolchain]
channel = "1.90"
components = [ "rust-src", "rustfmt", "llvm-tools"]
targets = ["thumbv8m.main-none-eabihf"]

build.rs

Cargo needs a custom build script. In our example this is build.rs.

 //! This build script copies the `memory.x` file from the crate root into //! a directory where the linker can always find it at build time. //! For many projects this is optional, as the linker always searches the //! project root directory -- wherever `Cargo.toml` is. However, if you //! are using a workspace or have a more complicated build setup, this //! build script becomes required. Additionally, by requesting that //! Cargo re-run the build script whenever `memory.x` is changed, //! updating `memory.x` ensures a rebuild of the application with the //! new memory settings. use std::env; use std::fs::File; use std::io::Write; use std::path::PathBuf; fn main() { // Put `memory.x` in our output directory and ensure it's // on the linker search path. let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap()); File::create(out.join("memory.x")) .unwrap() .write_all(include_bytes!("memory.x")) .unwrap(); println!("cargo:rustc-link-search={}", out.display()); // By default, Cargo will re-run a build script whenever // any file in the project changes. By specifying `memory.x` // here, we ensure the build script is only re-run when // `memory.x` is changed. println!("cargo:rerun-if-changed=memory.x"); println!("cargo:rustc-link-arg-bins=--nmagic"); println!("cargo:rustc-link-arg-bins=-Tlink.x"); println!("cargo:rustc-link-arg-bins=-Tdefmt.x"); } 

Copy it to the new project:

lpc
| .gitignore
| build.rs
| Cargo.toml
| rust-toolchain.toml
+---.cargo
| config.toml
\---src
main.rs

memory.x

As mentioned in above build.rs, there is the memory.x file needed for the linker:

/* File originally from lpc55-hal repo: https://github.com/lpc55/lpc55-hal/blob/main/memory.x */ 
MEMORY
{
FLASH : ORIGIN = 0x00000000, LENGTH = 512K

/* for use with standard link.x */
RAM : ORIGIN = 0x20000000, LENGTH = 256K

/* would be used with proper link.x */
/* needs changes to r0 (initialization code) */
/* SRAM0 : ORIGIN = 0x20000000, LENGTH = 64K */
/* SRAM1 : ORIGIN = 0x20010000, LENGTH = 64K */
/* SRAM2 : ORIGIN = 0x20020000, LENGTH = 64K */
/* SRAM3 : ORIGIN = 0x20030000, LENGTH = 64K */

/* CASPER SRAM regions */
/* SRAMX0: ORIGIN = 0x1400_0000, LENGTH = 4K /1* to 0x1400_0FFF *1/ */
/* SRAMX1: ORIGIN = 0x1400_4000, LENGTH = 4K /1* to 0x1400_4FFF *1/ */

/* USB1 SRAM regin */
/* USB1_SRAM : ORIGIN = 0x40100000, LENGTH = 16K */

/* To define our own USB RAM section in one regular */
/* RAM, probably easiest to shorten length of RAM */
/* above, and use this freed RAM section */

}

Copy it to the new project:

lpc
| .gitignore
| build.rs
| Cargo.toml
| memory.x
| rust-toolchain.toml
+---.cargo
| config.toml
\---src
main.rs

Build it

Now we should have everything in place. Use

$ cargo build

to build it:

$ cargo build
Updating crates.io index
Compiling proc-macro2 v1.0.101
Compiling unicode-ident v1.0.19
Compiling quote v1.0.41
Compiling semver-parser v0.7.0
Compiling cortex-m v0.7.7
Compiling defmt v1.0.1
Compiling cortex-m-rt v0.7.5
Compiling thiserror v2.0.17
Compiling nb v1.1.0
Compiling critical-section v1.2.0
Compiling void v1.0.2
Compiling cfg_aliases v0.2.1
Compiling vcell v0.1.3
Compiling defmt-macros v1.0.1
Compiling bitfield v0.13.2
Compiling bitflags v1.3.2
Compiling nb v0.1.3
Compiling volatile-register v0.2.2
Compiling nxp-pac v0.1.0 (https://github.com/i509VCB/nxp-pac?rev=b736e3038254d593024aaa1a5a7b1f95a5728538#b736e303)
Compiling strsim v0.11.1
Compiling litrs v0.4.2
Compiling ident_case v1.0.1
Compiling semver v0.9.0
Compiling byteorder v1.5.0
Compiling embedded-hal v0.2.7
Compiling autocfg v1.5.0
Compiling heapless v0.8.0
Compiling embassy-time-driver v0.2.1 (https://github.com/embassy-rs/embassy?rev=88de32c168216c98259380f76edc54ec49e1a81a#88de32c1)
Compiling fnv v1.0.7
Compiling rustc_version v0.2.3
Compiling hash32 v0.3.1
Compiling embassy-time-queue-utils v0.3.0 (https://github.com/embassy-rs/embassy?rev=88de32c168216c98259380f76edc54ec49e1a81a#88de32c1)
Compiling embedded-io-async v0.6.1
Compiling stable_deref_trait v1.2.1
Compiling cortex-m-semihosting v0.5.0
Compiling embassy-executor-timer-queue v0.1.0 (https://github.com/embassy-rs/embassy?rev=88de32c168216c98259380f76edc54ec49e1a81a#88de32c1)
Compiling futures-core v0.3.31
Compiling embassy-sync v0.7.2 (https://github.com/embassy-rs/embassy?rev=88de32c168216c98259380f76edc54ec49e1a81a#88de32c1)
Compiling embedded-io v0.6.1
Compiling bare-metal v0.2.5
Compiling document-features v0.2.11
Compiling embassy-hal-internal v0.3.0 (https://github.com/embassy-rs/embassy?rev=88de32c168216c98259380f76edc54ec49e1a81a#88de32c1)
Compiling num-traits v0.2.19
Compiling cfg-if v1.0.4
Compiling embedded-hal-async v1.0.0
Compiling panic-probe v1.0.0
Compiling embassy-executor v0.9.1 (https://github.com/embassy-rs/embassy?rev=88de32c168216c98259380f76edc54ec49e1a81a#88de32c1)
Compiling embedded-hal v1.0.0
Compiling defmt-rtt v1.1.0
Compiling futures-sink v0.3.31
Compiling embassy-futures v0.1.2 (https://github.com/embassy-rs/embassy?rev=88de32c168216c98259380f76edc54ec49e1a81a#88de32c1)
Compiling blinky v0.1.0 (C:\tmp\rust\lpc)
Compiling cordyceps v0.3.4
Compiling panic-halt v1.0.0
Compiling syn v2.0.107
Compiling proc-macro-error-attr2 v2.0.0
Compiling panic-semihosting v0.6.0
Compiling proc-macro-error2 v2.0.1
Compiling darling_core v0.20.11
Compiling thiserror-impl v2.0.17
Compiling cortex-m-rt-macros v0.7.5
Compiling darling_macro v0.20.11
Compiling darling v0.20.11
Compiling defmt-parser v1.0.0
Compiling embassy-executor-macros v0.7.0 (https://github.com/embassy-rs/embassy?rev=88de32c168216c98259380f76edc54ec49e1a81a#88de32c1)
Compiling embassy-time v0.5.0 (https://github.com/embassy-rs/embassy?rev=88de32c168216c98259380f76edc54ec49e1a81a#88de32c1)
Compiling embassy-nxp v0.1.0 (https://github.com/embassy-rs/embassy?rev=88de32c168216c98259380f76edc54ec49e1a81a#88de32c1)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 19.68s

Congratulations! We have now an embassy Rust standalone project as a starting point.

Summary

Transforming an embassy example project to a standalone project the first time looks complex. But it is a good learning exercise to understand cargo and the build system. And at the end, it is not that difficult, once the steps are understood.

You can find the project used in this article on GitHub.

What would be ideal is something like https://github.com/lulf/embassy-template or https://github.com/adinack/cargo-embassy: Unfortunately they only support a limited set of targets. A tool is needed to generate new projects for the missing devices. Until then, I hope the steps explained in this article can be used.

Happy rusting 🙂

Links

What do you think?

This site uses Akismet to reduce spam. Learn how your comment data is processed.