Appearance
This is NOT intended as a full guide on how to program the STM32H7.
Possible future posts will introduce more complex examples like handling real time audio, reading from multiplexed ADCs, using the QSPI flash and the SDRAM.
Requirements
- An STM32H7 board, like the Daisy Seed
- The
nightly
version of the Rust compiler - The target for Cortex-M7
thumbv7em-none-eabihf
- A serial debugger like the STLink-V3Mini (optional)
sh
rustup +nightly target add thumbv7em-none-eabihf
So let's write the toolchain details in rust-toolchain.toml
at the project root.
toml
[toolchain]
channel = "nightly"
targets = ["thumbv7em-none-eabihf"]
Project Setup
We will be using the rtic
framework, so let's start with writing down the main.rs
file.
rust
#![no_std]
#![no_main]
use panic_halt as _;
use stm32h7xx_hal::stm32;
#[rtic::app(device = stm32, peripherals = true)]
mod app {
#[shared]
struct Shared {}
#[local]
struct Local {}
#[init]
fn init(cx: init::Context) -> (Shared, Local, init::Monotonics) {
(Shared {}, Local {}, init::Monotonics())
}
#[idle]
fn idle(_cx: idle::Context) -> ! {
loop {
cortex_m::asm::nop();
}
}
}
This is a minimal application that effectively does nothing. The Shared
and Local
structs are used to pass data to different interrupt driven tasks (that we will add in a minute), more on tasks on the rtic documentation.
Making an LED blink
Let's make sure that everything works as intended first. Let's make an LED blink.
The init
function is where most of the code will be. First, it configures the clocks to the desired frequencies:
rust
let device = cx.device;
let mut core = cx.core;
// initialize the clocks
let mut ccdr = {
let pwr = device.PWR.constrain();
let vos = pwr.freeze();
device
.RCC
.constrain()
.use_hse(HSE_CLOCK_HZ)
.sys_ck(SYS_CLOCK_HZ)
.pclk1(PCLK_HZ)
// pll 1
.pll1_strategy(PllConfigStrategy::Iterative)
.pll1_p_ck(PLL1_P_HZ)
// pll 3
.pll3_strategy(PllConfigStrategy::Iterative)
.pll3_p_ck(PLL3_P_HZ)
.freeze(vos, &device.SYSCFG)
};
ccdr.clocks.hsi48_ck().expect("Needed for USB");
ccdr.peripheral.kernel_usb_clk_mux(UsbClkSel::Hsi48);
core.SCB.enable_icache();
Then, it will configure the timer (TIM2
). This timer's interrupt will be used as trigger for the task that will handle the LED blinking. By configuring with an interval of 500ms, it will trigger the task once every 500ms.
rust
let mut tim2 = device.TIM2.timer(
MilliSeconds::from_ticks(500).into_rate(),
ccdr.peripheral.TIM2,
&ccdr.clocks,
);
tim2.listen(Event::TimeOut);
And then it will configure the pin C7
to control the on-board LED. The resource will then be passed to the task triggered by the timer to set the pin high or low, so that the LED will turn on or off.
rust
let gpioc = device.GPIOC.split(ccdr.peripheral.GPIOC);
let led = gpioc.pc7.into_push_pull_output();
Lastly, init
needs to return the shared and local resources, plus the Monotonics
.
rust
(
Shared {},
Local {
led,
timer: tim2,
led_is_on: false,
},
init::Monotonics(),
)
Now to the actual blinking task.
We need to annotate this function with a #[task]
attribute. This will tell rtic
to trigger this task using TIM2
, and that this task will consume the local resources timer, led
and led_is_on
.
The task will:
- Take the resources from
cx.local
: Here it uses a destructuring pattern for convenience, it's not necessary though. - Clear the interrupt request: Effectively marking this interrupt request as "handled", and waiting for the next.
- Turn on or off the LED: The
led_is_on
local variable will store the state of the LED, we could also useled.is_set_high()
to check. - Update the state: Again, we could check the state based on the
gpioc::PC7
peripheral, here we just flip a boolean.
rust
#[task(binds = TIM2, local = [timer, led, led_is_on])]
fn blink(cx: blink::Context) {
// take the resources
let blink::LocalResources {
timer,
led,
led_is_on,
} = cx.local;
// clear the interrupt request
timer.clear_irq();
if *led_is_on {
led.set_high();
} else {
led.set_low();
}
// flip the state
*led_is_on = !(*led_is_on);
}
Flashing the board
Before we can flash the board, we need to add a Linker Script to our project. A linker script is available from the stm32h7xx-hal
repo
txt
MEMORY
{
FLASH (RX) : ORIGIN = 0x08000000, LENGTH = 128K
DTCMRAM (RWX) : ORIGIN = 0x20000000, LENGTH = 128K
SRAM (RWX) : ORIGIN = 0x24000000, LENGTH = 512K
RAM_D2 (RWX) : ORIGIN = 0x30000000, LENGTH = 288K
RAM_D3 (RWX) : ORIGIN = 0x38000000, LENGTH = 64K
ITCMRAM (RWX) : ORIGIN = 0x00000000, LENGTH = 64K
SDRAM (RWX) : ORIGIN = 0xc0000000, LENGTH = 64M
QSPIFLASH (RX) : ORIGIN = 0x90000000, LENGTH = 8M
}
/* stm32h7xx-hal uses a PROVIDE that expects RAM symbol to exist */
REGION_ALIAS(RAM, DTCMRAM);
SECTIONS
{
.sram1_bss (NOLOAD) :
{
. = ALIGN(4);
_ssram1_bss = .;
PROVIDE(__sram1_bss_start__ = _sram1_bss);
*(.sram1_bss)
*(.sram1_bss*)
. = ALIGN(4);
_esram1_bss = .;
PROVIDE(__sram1_bss_end__ = _esram1_bss);
} > RAM_D2
.sdram_bss (NOLOAD) :
{
. = ALIGN(4);
_ssdram_bss = .;
PROVIDE(__sdram_bss_start = _ssdram_bss);
*(.sdram_bss)
*(.sdram_bss*)
. = ALIGN(4);
_esdram_bss = .;
PROVIDE(__sdram_bss_end = _esdram_bss);
} > SDRAM
}
In case you're using probe-rs
to flash your device, you should add the following dependencies to your Cargo.toml
:
toml
defmt = "0.3.8"
defmt-rtt = "0.4.1"
panic-probe = { version = "0.3.2", features = ["print-defmt"] }
And to make the compiler aware of the new dependencies, import them like this:
rust
use defmt_rtt as _;
use panic_probe as _;
Additionally, to make things easier and not have to run multiple commands to build, flash and debug, let's configure cargo.
Create a file .cargo/config.toml
in your project and add the following:
toml
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
runner = "probe-rs run --chip STM32H750IBKx"
rustflags = [
"-C", "link-arg=-Tlink.x",
"-C", "link-arg=-Tdefmt.x",
"-C", "link-arg=--nmagic",
]
[build]
target = "thumbv7em-none-eabihf"
This tells cargo to use probe-rs run --chip STM32H750IBKx <path-to-executable>
when we invoke cargo run
.
Once this is done, you can flash the board using probe-rs
or your debugger of choice.
Full code
rust
#![no_std]
#![no_main]
use defmt_rtt as _;
use panic_probe as _;
use stm32h7xx_hal::{
gpio::{gpioc, GpioExt, Output, PushPull},
pac::TIM2,
pwr::PwrExt,
rcc::{PllConfigStrategy, RccExt},
stm32,
time::{Hertz, MegaHertz, MilliSeconds},
timer::{Event, Timer, TimerExt},
};
const HSE_CLOCK_HZ: Hertz = MegaHertz::from_raw(16).convert();
const SYS_CLOCK_HZ: Hertz = MegaHertz::from_raw(480).convert();
const PCLK_HZ: Hertz = Hertz::from_raw(SYS_CLOCK_HZ.raw() / 4);
const PLL1_P_HZ: Hertz = SYS_CLOCK_HZ;
const PLL3_P_HZ: Hertz = Hertz::from_raw(48_000 * 256);
#[rtic::app(device = stm32, peripherals = true)]
mod app {
use super::*;
#[shared]
struct Shared {}
#[local]
struct Local {
led: gpioc::PC7<Output<PushPull>>,
timer: Timer<TIM2>,
led_is_on: bool,
}
#[init]
fn init(cx: init::Context) -> (Shared, Local, init::Monotonics) {
let device = cx.device;
// initialize the clocks
let device = cx.device;
let mut core = cx.core;
// initialize the clocks
let mut ccdr = {
let pwr = device.PWR.constrain();
let vos = pwr.freeze();
device
.RCC
.constrain()
.use_hse(HSE_CLOCK_HZ)
.sys_ck(SYS_CLOCK_HZ)
.pclk1(PCLK_HZ)
// pll 1
.pll1_strategy(PllConfigStrategy::Iterative)
.pll1_p_ck(PLL1_P_HZ)
// pll 3
.pll3_strategy(PllConfigStrategy::Iterative)
.pll3_p_ck(PLL3_P_HZ)
.freeze(vos, &device.SYSCFG)
};
ccdr.clocks.hsi48_ck().expect("Needed for USB");
ccdr.peripheral.kernel_usb_clk_mux(UsbClkSel::Hsi48);
core.SCB.enable_icache();
// setup timer2 with an interval of 500ms
let mut tim2 = device.TIM2.timer(
MilliSeconds::from_ticks(500).into_rate(),
ccdr.peripheral.TIM2,
&ccdr.clocks,
);
tim2.listen(Event::TimeOut);
// take pin C7 and set it as output
let gpioc = device.GPIOC.split(ccdr.peripheral.GPIOC);
let led = gpioc.pc7.into_push_pull_output();
(
Shared {},
Local {
led,
timer: tim2,
led_is_on: false,
},
init::Monotonics(),
)
}
#[idle]
fn idle(_: idle::Context) -> ! {
loop {
cortex_m::asm::nop();
}
}
#[task(binds = TIM2, local = [timer, led, led_is_on])]
fn blink(cx: blink::Context) {
// take the resources
let blink::LocalResources {
timer,
led,
led_is_on,
} = cx.local;
// clear the interrupt request
timer.clear_irq();
if *led_is_on {
led.set_high();
} else {
led.set_low();
}
// flip the state
*led_is_on = !(*led_is_on);
}
}