;
});
```
diff --git a/docs/embassy/index.md b/docs/embassy/index.md
index d031d63..9a46f3b 100644
--- a/docs/embassy/index.md
+++ b/docs/embassy/index.md
@@ -57,13 +57,11 @@ udevadm trigger # to ensure the new rules are applied to already added devices.
This is a top view of the board we will use for this workshop:
-
+
-The schematics can be found [here](https://gitlab.cs.pub.ro/pmrust/pm-ma-pcb). Feel free to consult them!
+### STM32 Nucleo-U545RE-Q
-### Raspberry Pi Pico 2 W
-
-The *"brains"* of the board is represented by the **Raspberry Pi Pico 2 W**, a compact microcontroller with dual-core **Arm Cortex-M33** and optional dual-core **RISC-V Hazard3**, running at up to **150 MHz**. It features **520 KB SRAM**, **4 MB flash storage**, and built-in Wi-Fi (802.11n) & Bluetooth 5.2 via the **CYW43439** chip. It includes a 40-pin GPIO header with **26 digital I/O pins**, **4 analog inputs** and security enhancements include **Arm TrustZone** and **OTP memory**. This makes it ideal for IoT and embedded applications.
+The *"brains"* of the board is represented by the **STM32 Nucleo-U545RE-Q**, a compact microcontroller with based on **Arm Cortex-M33**, running at up to **160 MHz**. It features **272 KB SRAM**, **512 KB flash storage**. It includes **51 GPIO pins**, **4 I²C buses**, **3 SPI channels**, **2 UARTs**, **USB 2.0 FS**, and **CAN (FDCAN)**, on the analog side, it integrates a **12‑bit ADC**, a **14‑bit ADC**, and dual **12‑bit DACs**, complemented by advanced math and signal‑processing functions and security enhancements include **Arm TrustZone** and **OTP memory**. This makes it ideal for IoT and embedded applications.
## Documentation
@@ -77,24 +75,33 @@ General-Purpose Input/Output, or GPIO, is an essential part of embedded systems
GPIO pins can be used as outputs (LEDs, motors, buzzers) or as inputs (buttons, sensors).
-The RP2040 and RP2350 have three peripherals that control the GPIO pins:
+The STM32U545 controls the GPIO pins through several GPIO Ports (PortA, PortB, PortC, PortD, PortE, etc.). Using GPIOs implies configuring the:
+1. *MODER* - connects the outer GPIO pin to one of the:
+ * internal `GPIO` peripheral as `input`
+ * internal `GPIO` peripheral as `output`
+ * one of the alternate function peripheral (further selected by the *Alternate Function Multiplexer*)
+ * the `ADC` peripheral
+2. *Electrical Configuration* - manages the physical pin on the outside of the chip. It decides the electrical behavior of the pad, such as whether it has a pull‑up or pull‑down resistor, whether it drives strongly or weakly, and whether it is push‑pull or open‑drain.
+3. *Alternate Function Multiplexer* - connects the internal peripherals to the outside pins. Each pin can be switched to different functions. For example, a single pin can be used as:
+ * a `UART` transmit line
+ * an `I2C` data line
+ * an `SPI` clock or data line
+ * a `TIM` timer channel
+ * a `PWM` timer channel
+ * ...
+4. *GPIO Input/Output* - this is the simple digital interface that developers use when they want to read or write a pin directly in software. It’s what you use to toggle an LED or read a button.
+
+
-1. *Pads* - control the actual physical pin or pad that the processor has outside. They control the electrical parameters, like maximum current or pull up and pull down resistors
-2. *IO Bank0* - connects and multiplexes the peripheral's pins to the output pads. Several peripherals use the same output pad to communicate with the exterior. For example, in the image below, `GPIO0` can be used either for:
- * `SIO` - the `GPIO` function
- * `SPI_RX` - the receive pin for the `SPI` peripheral
- * `I2C0_SDA` - the data pin for the `I2C0` peripheral
- * `UART0_TX` - the transmit pin for the `UART0` (serial port 0) peripheral
-3. *SIO* - that controls the interior MCU's pins. This is the peripheral that developers use to read and write the value of the pins.
+:::note
+When you look at an STM32 Nucleo-U545RE-Q development board, you'll notice two sets of male pin headers. These are designed to give you two different ways to access the microcontroller's (MCU) pins.
-
+- **ST Morpho Headers**: These are the two long, outer rows of pins (e.g., CN7 and CN10). They are a superset designed to expose all the I/O pins of the STM32 microcontroller for complete access and flexibility.
-Every pin of the MCU can perform multiple functions. Several peripherals need to use input and output pins.
-It is the role of the *IO Bank0* to multiplex and connect the peripherals to the pins.
+- **Arduino Headers**: These are the four inner headers (e.g., CN5, CN6, CN8, CN9). They are a subset of the ST morpho pins. Their layout is standardized to match the Arduino Uno R3, allowing you to easily connect and use existing Arduino shields.
-
-
-
+In short, every Arduino pin is also available on the ST morpho headers, but the morpho headers provide access to many more pins that are not part of the Arduino layout.
+:::
### Timing
@@ -106,11 +113,13 @@ A clock is a piece of hardware that provides us with that reference. Its purpose

-The most precise type of clock is the crystal oscillator (XOSC). The reason why it is so accurate is because it uses the crystal's natural vibration frequency to create the clock signal. This clock is usually external to the processor itself, but the processor also has an internal clock (ROSC) that is less accurate and that can be used in cases where small variations of clock pulses are negligible. When using the USB protocol, for instance, a more stable clock signal is required, therefore the XOSC is necessary. The crystal oscillator on the Raspberry Pi Pico board has a frequency of 12MHz.
+The most precise type of clock on the STM32 Nucleo-U545RE-Q is the High-Speed External oscillator (HSE). The reason why it is so accurate is because it uses the crystal’s natural vibration frequency to generate the clock signal. This oscillator is usually external to the microcontroller itself, requiring a crystal or external clock source connected to the HSE pins.
+
+The processor also provides several internal RC oscillators (HSI, MSI, LSI), which are less accurate because they rely on resistor-capacitor timing rather than quartz resonance. These internal clocks are useful when small variations in clock pulses are acceptable, or when minimizing external components is important.
-This clock signal is just a reference, and most of the time we need to adjust it to our needs. This is done by either multiplying or dividing the clock, or in other words, elevating or lowering the frequency of the clock. For example, the RP2040 itself runs on a 133MHz clock, so the crystal oscillator frequency of 12MHz is multiplied (this is done using a method called Phase-Locked Loop). Similarly, the RP2350, the successor to the RP2040, also uses a PLL to adjust the 12MHz reference clock but supports a higher clock speed of 150MHz. This increased clock speed, along with more precise frequency control and improved power management, makes the RP2350 more versatile for both high-performance and energy-efficient applications.
+The STM32 Nucleo-U545RE-Q does not have a built-in crystal frequency. Instead, the board designer selects an appropriate crystal (commonly 8 MHz, 16 MHz, or 24 MHz) to drive the HSE, depending on the application requirements.
-
+
#### Counters
@@ -134,7 +143,7 @@ The way the counter works here is that it increments/decrements every clock cycl
#### SysTick
-The ARM Cortex-M0 used by RP2040 and the ARM Cortex-M33 used by RP2350 both use the SysTick time counter to keep track of time. This counter is decremented every microsecond, and when it reaches 0, it triggers an exception and then resets.
+The ARM Cortex-M33 used by STM32U545 both use the SysTick time counter to keep track of time. This counter is decremented every microsecond, and when it reaches 0, it triggers an exception and then resets.
* `SYST_CVR` register - the value of the timer itself
* `SYST_RVR` register - the reset value
@@ -153,9 +162,11 @@ An **alarm** is a counter that triggers an interrupt every time it reaches a cer

:::info
-The **RP2350** timer and the **RP2040's** are fully monotonic, meaning they can never truly overflow. Their value is stored on 64 bits and incremented every 1 microsecond, ensuring precise and consistent timekeeping. This means the last value they can increment to before overflowing is 2^64-1, which is equivalent to roughly 500,000 years. RP2040 and RP2350 support 4 different alarms (TIMERx_IRQ_0/1/2/3), which can be used independently, allowing for multiple timed events or tasks to be managed simultaneously.
+The **RP2350** timer and the **RP2040's** are fully monotonic, meaning they can never truly overflow. Their value is stored on 64 bits and incremented every 1 microsecond, ensuring precise and consistent timekeeping. This means the last value they can increment to before overflowing is 264‑1, which is equivalent to roughly 500,000 years. RP2040 and RP2350 support 4 different alarms (TIMERx_IRQ_0/1/2/3), which can be used independently, allowing for multiple timed events or tasks to be managed simultaneously.
-RP2350 provides two timer peripherals, while RP2040 provides only one.
+The **RP2350** provides two timer peripherals, while the **RP2040** provides only one.
+
+The **STM32U5**, by contrast, does not use a single 64‑bit monotonic timer. Instead, it provides a set of general‑purpose 16‑bit and 32‑bit timers (TIMx) as well as advanced timers for motor control and low‑power timers for energy‑sensitive applications. These timers can be configured with prescalers and auto‑reload registers to generate periodic interrupts, PWM signals, input capture, or output compare events. Unlike the RP2040 and RP2350’s unified 64‑bit microsecond counter, STM32 timers can overflow depending on their bit‑width (e.g., a 16‑bit timer rolls over after 65,535 counts). To achieve long‑duration or monotonic timekeeping, software typically chains timers or uses the SysTick timer in combination with interrupts.
:::
### Analog and Digital Signals
@@ -194,22 +205,33 @@ duty\_cycle = \frac{time\_on}{period} \%
$$
-*Counters* are used by the RP2350 and RP2040 to generate the PWM signals. The PWM counters are controlled by these registers (`X` can be from 0-7, depending on the channel):
+On the STM32U545, PWM generation is handled by the timer peripherals (`TIMx`), where each channel uses a counter (`TIMx_CNT`), a compare register (`TIMx_CCRy`), and an auto‑reload register (`TIMx_ARR`) to define the duty cycle and period.
-* `CHX_CTR` - the actual value of the counter
-* `CHX_CC` - the value that the counter will compare to
-* `CHX_TOP` - the value at which the counter will reset (or *wrap*)
+- `TIMx_CNT` - the actual value of the counter
+- `TIMx_CCRy` - the compare value for channel y (this sets the duty cycle)
+- `TIMx_ARR` - the auto‑reload value, the maximum count before the counter resets (this sets the period)
-When `CHX_CTR` is reset, the value of the output signal is 1. The counter counts up until it reaches `CHX_CC`, after which the value of the output signal becomes 0. The counter continues to count until it reaches `CHX_TOP`, and then the signal becomes 1 again. This way, by choosing the value of `CHX_CC`, we set the duty cycle of the PWM signal.
+When `TIMx_CNT` is reset (0), the output signal is set to 1 (active). The counter counts up until it reaches `TIMx_CCRy`, after which the output signal becomes 0 (inactive). The counter continues to count until it reaches `TIMx_ARR`, then it resets to 0 and the signal becomes 1 again.

-On RP2350 and RP2040, all GPIO pins support PWM. Every two pins share a PWM slice, and each one of them is on a separate channel.
+On STM32U545, PWM signals are generated by the timer peripherals (TIMx). Each timer provides multiple channels, and each channel can be mapped to specific GPIO pins through the alternate function system.
+
+| Arduino connector | STM32 pin |PWMs |
+|-----------|----------|----------|
+| `D3` |`PB3` |`TIM2_CH2` (Timer 2 PWM Channel 2)|
+| `D5` | `PB4`|`TIM3_CH1` (Timer 3 PWM Channel 1) |
+| `D6` |`PB10`|`TIM2_CH3` (Timer 2 PWM Channel 3) |
+| `D9` |`PC6`|`TIM3_CH1` (Timer 3 PWM Channel 1) |
+| `D10` |`PC9`|`TIM3_CH4` (Timer 3 PWM Channel 4) |
+| `D11` |`PA7`|`TIM3_CH2`(Timer 3 PWM Channel 2) |
-
+
:::info
-This means that in order to use a pin as PWM, we need to know what channel it's on, and which output it uses (A or B).
+
+You can find more details about the pins in the [user manual](https://www.st.com/resource/en/user_manual/um3062-stm32u3u5-nucleo64-boards-mb1841-stmicroelectronics.pdf) of the board(page 33)
+
:::
#### Examples of hardware controlled through PWM
@@ -244,9 +266,9 @@ For this lab, we will be using **common anode** RGB LEDs, which means that the P
#### How to wire an RGB LED
-The RGB LED that the board provides is signaled with labels `RGB_B` (blue), `RGB_G` (green) and `RGB_R` (red) in the connectors section.
+The RGB LED that the board provides is signaled with labels `RGB_BLUE` (blue), `RGB_GREEN` (green) and `RGB_RED` (red) in the connectors section.
-
+
#### How to wire a servo motor
@@ -261,163 +283,158 @@ A servo motor has three wires:
The board provides the connectors for the servo motor. These connectors are labeled `GND`, `PWR`, and `SIG` in the connectors section.
-
+
-To wire the servo motor to the Raspberry Pi Pico 2, first connect the servo motor to the board using the `GND`, `PWR`, and `SIG` connectors. Then, a jumper wire is needed to connect the `SERVOS` connector on the board to a PWM pin on the Raspberry Pi Pico 2.
+
+To wire the servo motor to the STM32 Nucleo-U545RE-Q, note that the servo’s `PWR`, `GND`, and `SIG` lines are already connected to the lab board. The only additional step is to place a jumper wire from the SIG connector of the the lab board to a suitable PWM-capable pin on the STM32 Nucleo-U545RE-Q.
#### PWM in Embassy-rs
First, we need a reference to all peripherals, as usual.
+
```rust
// Initialize peripherals
-let peripherals = embassy_rp::init(Default::default());
+let peripherals = embassy_stm32::init(Default::default());
```
-In order to modify the PWM counter configurations, we need to create a `Config` for our PWM.
-
```rust
-// PWM config
-use embassy_rp::pwm::Config as ConfigPwm;
-
-// Create config for PWM slice
-let mut config: ConfigPwm = Default::default();
-// Set top value (value at which PWM counter will reset)
-config.top = 0x9088; // in HEX, equals 37000 in decimal
-// Set compare value (counter value at which the
-// PWM signal will change from 1 to 0)
-config.compare_a = config.top / 2;
-```
+use embassy_stm32::timer::simple_pwm::{PwmPin, SimplePwm};
+use embassy_stm32::peripherals::TIM2;
+use embassy_stm32::timer::Ch1;
-In the example above:
- - `top` is the field from `Config` that will define the value at which the counter will reset back to 0
- - `compare_a` is the field from `Config` that will define the value at which the PWM signal will switch from 1 to 0
-In this case, `config.compare_a` is half of `config.top`. This means that the duty cycle of the generated PWM signal will be 50%, or, in other words, that the PWM signal will switch from 1 to 0 halfway through each period.
+ // Configure PA0 as TIM2_CH1 PWM output
+ let led_pwm_pin: PwmPin<'_, TIM2, Ch1> = PwmPin::new(p.PA0, OutputType::PushPull);
+```
-To select the pin that we want to use for PWM, we need to create a new PWM driver that uses the correct channel and output for our pin.
+ - `PA0` → the physical GPIO pin you want to use.
+ - `TIM2` → the timer peripheral that will generate the PWM signal.
+ - `Ch1` → the channel of that timer (channel 1).
+ - `PwmPin::new_ch1` → ties PA0 to TIM2 channel 1’s output.
+ - `OutputType::PushPull` → configures the electrical drive mode of the pin.
```rust
-// Create a PWM driver for pin 3
-let mut pwm = Pwm::new_output_b( // Output B
- peripherals.PWM_SLICE1, // Channel 1
- peripherals.PIN_3, // Pin 3 (modify this as needed)
- config.clone()
+let mut pwm = SimplePwm::new(
+ p.TIM2, // Timer 2 peripheral
+ Some(led_pwm_pin), // Channel 1 output (PA0)
+ None, // Channel 2 not used
+ None, // Channel 3 not used
+ None, // Channel 4 not used
+ khz(1), // PWM frequency = 1 kHz
+ Default::default(), // Default configuration
);
```
:::warning
-1. The code above is an example for pin 3. You need to modify the channel, output and pin depending on the PWM pin you choose to use!
-2. The value of `compare_a` or `compare_b` must be changed depending on the desired duty cycle!
-
+The code above is an example for PA0 with TIM2_CH1. You need to modify the timer, channel, and pin depending on which PWM‑capable pin you want to use!
:::
-If we decide to modify the value of `compare_a` or `compare_b`, we have to update the configuration for the PWM.
+If we decide to modify the duty cycle of the PWM, we can update it directly on the channel:
```rust
-config.compare_b += 100; // modified value of `compare_b`
-pwm.set_config(&config); // set the new configuration for PWM
+let mut ch1 = pwm.ch1(); // Get handle for channel 1
+ch1.enable();
+
+// Set duty cycle to 50%
+ch1.set_duty_cycle_percent(50);
+
+// Later, change duty cycle to 10%
+ch1.set_duty_cycle_percent(10);
```
#### Controlling a Servo Motor Using PWM
-Just like controlling other hardware through PWM, we start by initializing the peripherals:
+Just like controlling other hardware through PWM, we start by initializing the peripherals.
-```rust
-// Initialize the RP2350 peripherals
-let peripherals = embassy_rp::init(Default::default());
-```
+Servos typically expect a 50 Hz PWM signal, which corresponds to a 20 ms period.
-To control a servo motor using PWM, we need to calculate the **TOP value**, which determines the PWM period.
+Servos interpret PWM signals based on the pulse width rather than just frequency:
-#### Calculating the TOP Value
+- `Period`: The total time for one PWM cycle, which is 20 ms (50 Hz).
+- `Minimum Pulse Width`: Typically **0.5 ms**, which corresponds to a servo position of **0 degrees**.
+- `Maximum Pulse Width`: Typically **2.5 ms**, which corresponds to a servo position of **180 degrees**.
-Servos typically expect a **50 Hz** PWM signal, which corresponds to a **20 ms** period.
+```rust
+// Initialize peripherals
+let peripherals = embassy_stm32::init(Default::default());
+```
-$$
+At 50 Hz, the PWM period is:
+$$
-top = \left( \frac{f_{clock}}{f_{pwm} \times divider} \right) - 1
+T = 20ms
$$
-For example, with:
-
-$$
-\begin{aligned}
-f_{clock} &= 150 \,MHz \\
-f_{PWM} &= 50 \,Hz \\
-divider &= 64
-\end{aligned}
+For a given pulse width $$pulse_{width}$$ the duty cycle percentage is:
$$
-We get:
+duty_{cycle} = \left( \frac{pulse_{width} \times 100}{T} \right)
$$
-top = \left( \frac{150\,000\,000}{50 \times 64} \right) - 1 = 46\,874
-$$
-
-which is `0xB71A` in hexadecimal.
-
-The **clock divider** is used to slow down the high-frequency system clock so that it can generate a usable PWM signal. The RP2350's system clock runs at 150 MHz, which is too fast for direct PWM control of a servo. By setting the divider to 64, we effectively slow down the clock.
-Servos interpret PWM signals based on the pulse width rather than just frequency:
-
-* `Period`: The total time for one PWM cycle, which is 20 ms (50 Hz).
-* `Minimum Pulse Width`: Typically **0.5 ms**, which corresponds to a servo position of **0 degrees**.
-* `Maximum Pulse Width`: Typically **2.5 ms**, which corresponds to a servo position of **180 degrees**.
+The function `set_duty_cycle_fraction(num, den)` expects a fraction representing the duty cycle.
-To convert these pulse widths into PWM compare values, we use:
+For example, to generate a $$pulse_{width}$$ of 2.5ms,
$$
-compare = \left( \frac{pulse_{width} \times top}{T} \right)
-$$
-
-where:
-* $$pulse_{width}$$ is the desired pulse width in microseconds.
-* $$top$$ is the previously calculated counter value (46,874).
-* $$T$$ is the total period in microseconds (20,000 μs for 50 Hz).
+duty_{cycle} = \left( \frac{2.5 \times 1000}{20} \right) ‰ = 125 ‰
-Now, let's implement this in Rust:
+$$
-```rust
-// Configure PWM for servo control
-let mut servo_config: PwmConfig = Default::default();
+That means `set_duty_cycle_fraction(125, 1000)`
-// Set the calculated TOP value for 50 Hz PWM
-servo_config.top = 0xB71A;
+- For $$pulse_{width}$$ = 0.5ms:
+$$
-// Set the clock divider to 64
-servo_config.divider = 64_i32.to_fixed(); // Clock divider = 64
+duty_{cycle} = \left( \frac{0.5 \times 1000}{20} \right) ‰ = 25 ‰
-// Servo timing constants
-const PERIOD_US: usize = 20_000; // 20 ms period for 50 Hz
-const MIN_PULSE_US: usize = 500; // 0.5 ms pulse for 0 degrees
-const MAX_PULSE_US: usize = 2500; // 2.5 ms pulse for 180 degrees
+$$
-// Calculate the PWM compare values for minimum and maximum pulse widths
-let min_pulse = (MIN_PULSE_US * servo_config.top as usize) / PERIOD_US;
-let max_pulse = (MAX_PULSE_US * servo_config.top as usize) / PERIOD_US;
-```
+That means `set_duty_cycle_fraction(25, 1000)`
-After setting up the PWM configuration, we want to create a control loop that can dynamically adjust the servo's position.
+:::note
+We use ‰ for accuracy, as the PWM works with integer numbers and % is not accurate enough.
+:::
+Now, let's implement it in Rust:
```rust
-// Initialize PWM for servo control
-let mut servo = Pwm::new_output_a(
- peripherals.PWM_SLICE1,
- peripherals.PIN_2,
- servo_config.clone()
+// Set the PWM pin
+let servo_pin = PwmPin::new_ch1(p.PA0, OutputType::PushPull);
+
+// 50 Hz PWM (20 ms period)
+let mut pwm = SimplePwm::new(
+ p.TIM2,
+ Some(servo_pin),
+ None,
+ None,
+ None,
+ hz(50),
+ Default::default(),
);
-// Main loop to move the servo back and forth
-loop {
- // Move servo to maximum position (180 degrees)
- // Set compare value for max pulse width
- // Update PWM configuration
- // Wait 1 second
- // Then move the servo to minimum position (0 degrees)
-}
+// Enable the PWM channel
+ let mut ch1 = pwm.ch1();
+ ch1.enable();
+
+const MIN_PERIOD_US: u32 = 500;
+const MAX_PERIOD_US: u32 = 2500;
+const PERIOD_US: u32 = 20000;
+
+let min_value = (MIN_PERIOD_US * 1000) / PERIOD_US;
+let max_value = (MAX_PERIOD_US * 1000) / PERIOD_US;
+
+ // Main loop to move the servo back and forth
+ loop {
+ // At 50 Hz, T = 20 ms. A 2.5 ms pulse means 2.5/20 = 12.5% duty cycle.
+ set_duty_cycle_fraction(max_value as u16, 1000); // sets the servo to set servo to ~180°.
+ // Wait 1 second before moving back
+ // A 0.5 ms pulse means 0.5/20 = 2.5% duty cycle.
+ set_duty_cycle_fraction(min_value as u16, 1000); // sets the servo to set servo to ~0°.
+ // Wait 1 second before repeating
+ }
```
### Analog-to-Digital Converter (ADC)
@@ -445,7 +462,7 @@ The [Nyquist-Shannon sampling theorem](https://en.wikipedia.org/wiki/Nyquist%E2%
For an analog signal to be represented without loss of information, the conversion needs to satisfy the following formula:
$$
-sampling_f >= 2 \times max_{f}
+sampling_f > 2 \times max_{f}
$$
The analog signal needs to be sampled at a frequency greater than twice the *maximum frequency* of the signal.
@@ -467,7 +484,7 @@ A **photoresistor** (or photocell) is a sensor that measures the intensity of li
The photoresistor that the board provides is signaled with the label `PHOTORESISTOR` in the connectors section.
-
+
:::info
To wire a photoresistor on your board at home, you need to connect one leg to *GND* and the other leg to a voltage divider.
@@ -489,55 +506,58 @@ This way, the ADC pin measures the photoresistor's resistance, without the risk
#### ADC in Embassy-rs
-On the RP2350, ADC uses an interrupt called `ADC_IRQ_FIFO` to signal whenever a new sample has been added to the ADCs' FIFO. This new sample will be stored inside a FIFO. In the Embassy library, this interrupt is already implemented, so all we need to do is bind it and use it in our ADC variable.
+ On the STM32, the ADC peripheral does not use a FIFO interrupt like the RP2. Instead, each ADC (ADC1, ADC2, etc.) can be configured directly, and Embassy provides a driver that handles resolution, averaging, and sample time for you. All we need to do is create the ADC driver, configure it, and then bind it to the pin we want to read from.
-```rust
-// Bind the `ADC_IRQ_FIFO` interrupt to the Embassy's ADC handler
-bind_interrupts!(struct Irqs {
- ADC_IRQ_FIFO => InterruptHandler;
-});
+| Arduino connector | STM32 pin |ADC|
+|-----------|----------|----------|
+| `A0` |`PA0` |`ADC1_IN5`(ADC1 Channel 5) |
+| `A1` | `PA1`|`ADC1_IN6` (ADC1 Channel 6) |
+| `A2` |`PA4`|`ADC1_IN9` (ADC1 Channel 9) |
+| `A3` |`PB0`|`ADC1_IN15` (ADC1 Channel 15) |
+| `A4` |`PC1`|`ADC1_IN2` (ADC1 Channel 2) |
+| `A5` |`PC0`|`ADC1_IN1` (ADC1 Channel 1)|
-// ---- fn main() ----
-// Initialize peripherals
-let peripherals = embassy_rp::init(Default::default());
+ ```rust
+ // ---- fn main() ----
-// Create ADC driver
-let mut adc = Adc::new(peripherals.ADC, Irqs, Config::default());
-```
+let p = embassy_stm32::init(Default::default());
-:::warning
-If we are using PWM and ADC in the same code, we will have two different `Config` imports with the same name. In order to avoid compilation errors, we need to separate the PWM config import from the ADC one. To do this, we can import the two `Config`s with different names.
+// Create ADC driver on ADCx
+let mut adc = adc::Adc::new(p.ADCx);
-```rust
-use embassy_rp::adc::Config as ConfigAdc; // ADC config
+// Configure resolution, averaging, and sample time
+adc.set_resolution(adc::Resolution::BITS14);
+adc.set_averaging(adc::Averaging::Samples1024);
+adc.set_sample_time(adc::SampleTime::CYCLES160_5);
-// ---- fn main() ----
-let mut adc = Adc::new(peripherals.ADC, Irqs, ConfigAdc::default());
-```
-
-:::
+const MAX_VALUE: u32 = adc::resolution_to_max_count(adc::Resolution::BITS14);
+ ```
-Now, we need to initialize the ADC pin we will be using. The Raspberry Pi Pico has 3 pins that support ADC: `ADC0`, `ADC1`, and `ADC2`.
+ :::warning
+When reading the ADC, it is important to distinguish between the maximum digital count and the maximum input voltage. The function
```rust
-// Initialize ADC pin
-// X should be replaced with a pin number
-let mut adc_pin = Channel::new_pin(peripherals.PIN_X, Pull::None);
+const MAX_VALUE: u32 = adc::resolution_to_max_count(adc::Resolution::BITS14);
```
+returns the largest integer code that the ADC can produce for a 14‑bit conversion.
-Once we have the ADC and pin set up, we can start reading values from the pin.
+- For a 14‑bit ADC, the digital output spans from 0 to `2^14 - 1 = 16383`.
+- `MAX_VALUE` will therefore hold the value `16383`.
+- This is not the maximum voltage that the ADC can measure. The maximum voltage is determined by the reference voltage (e.g., 3.3 V), while `MAX_VALUE` simply represents the highest possible digital code.
+:::
+Once we have the ADC and pin set up, we can start reading values from the pin. Here’s a simple loop that reads the raw ADC value, converts it to a voltage, and prints it:
```rust
loop {
- // read a value from the pin
- let level = adc.read(&mut adc_pin).await.unwrap();
+ // Read a raw ADC value (blocking read)
+ let level: u16 = adc.blocking_read(&mut adc_pin);
+ let voltage = 3.3f32 * level as f32 / MAX_VALUE as f32;
- // print the value over serial
- info!("Light sensor reading: {}", level);
+ info!("Light sensor reading: {}, voltage: {}", level, voltage);
- // wait a bit before reading and printing another value
- Timer::after_secs(1).await;
+ // Wait a bit before reading again
+ embassy_time::Timer::after_secs(1).await;
}
```
@@ -604,53 +624,40 @@ For 10-bit addresses, the controller first issues a specific sequence of bits. T
These are the I2C imports we will be using. We will use the functions provided by the `embedded_hal` crate, since these are standard and used by most frameworks.
```rust
-use embassy_rp::i2c::{I2c, InterruptHandler as I2CInterruptHandler, Config as I2cConfig};
-use embedded_hal_async::i2c::{Error, I2c as _};
-use embassy_rp::peripherals::I2C0;
+use embassy_stm32::i2c::I2c;
+use embassy_stm32::{bind_interrupts, i2c, peripherals};
```
-:::info
-The reason why we use `I2c as _` from `embedded_hal_async` is that in order to use the `embedded_hal` trait methods, the trait needs to be imported.
-:::
-
We start by initializing the peripherals.
```rust
-let peripherals = embassy_rp::init(Default::default());
+let peripherals = embassy_stm32::init(Default::default());
```
-Next, we declare the pins we will be using for the SDA and SCL lines. We can find which pins of the Raspberry Pi Pico have these functions by looking at the pinout.
+Next, we declare the pins we will be using for the SDA and SCL lines. We can find which pins of the STM32 Nucleo-U545RE-Q have these functions by looking at the pinout.
```rust
-let sda = peripherals.PIN_X;
-let scl = peripherals.PIN_Y;
+let sda = peripherals.PXn;
+let scl = peripherals.PYm;
```
We then initialize our I2C instance, using the pins we defined earlier and a default configuration. It's recommended to use the asynchronous version, since it won't block the executor.
```rust
-let mut i2c = I2c::new_async(peripherals.I2C0, scl, sda, Irqs, I2cConfig::default());
+let mut i2c = I2c::new(peripherals.I2C1, sda, scl, Irqs, p.GPDMA1_CH0, p.GPDMA1_CH1, Default::default());
```
-The first argument of the `new` function is the I2C channel that will be used. There are two I2C channels on the Raspberry Pi Pico, and depending on which pins we decided to use, we can see which channel they are on by looking at the pinout.
+The first argument of the `new` function is the I2C channel that will be used. There are multiple I2C channels on the STM32 Nucleo-U545RE-Q, and depending on which pins we decided to use, we can see which channel they are on by looking at the pinout.
The `Irqs` variable refers to the interrupt that the I2C driver will use when handling transfers. We also need to bind this interrupt, which depends on the I2C channel we are working with.
```rust
bind_interrupts!(struct Irqs {
- I2C0_IRQ => I2CInterruptHandler;
+ I2C1_EV => i2c::EventInterruptHandler;
+ I2C1_ER => i2c::ErrorInterruptHandler;
});
```
-:::warning
-Since multiple `Config`s and `InterruptHandler`s can exist in one file, in the code examples above, `I2cConfig` and `I2CInterruptHandler` are renamed imports:
-
-```rust
-use embassy_rp::i2c::{I2c, InterruptHandler as I2CInterruptHandler, Config as I2cConfig};
-```
-
-:::
-
#### Reading from a target
To read from a target, we will be using the `read_async` function of the I2C driver.
@@ -663,7 +670,7 @@ The function takes 2 parameters:
The following example reads two bytes from the target of address `0x44`.
```rust
-const TARGET_ADDR: u16 = 0x44;
+const TARGET_ADDRESS: u16 = 0x44;
let mut rx_buf = [0x00u8; 2];
i2c.read(TARGET_ADDR, &mut rx_buf).await.unwrap();
```
@@ -703,7 +710,7 @@ As this is an embedded workshop, you will need to make several connections using
Before you start with writing code, make sure you register your team in this [spreadsheet](https://docs.google.com/spreadsheets/d/1wVNxWiOeCw_OBxBlQvEoAUpePjKxQL9dRNmtNhi3-aE/edit?usp=sharing).
-A skeleton application, which periodically blinks an external LED, connected to the `GP4` pin can be found in this [repository](https://github.com/UPB-RustWorkshop-Students/embassy-mar-2025). It should already include everything you will need for today's workshop, in regards of external dependencies. Go ahead and clone it locally. In order to run it, you will need to actually connect the LED to the specified pin, using one of the provided male-to-male cables. For the LED, you can use any of the exposed LED's labeled `RED`, `GREEN`, `BLUE` and `YELLOW`. Then you can run the following command:
+A skeleton application, which prints "Hello" can be found in this [repository](https://github.com/UPB-RustWorkshop/embedded-nov-2025). It should already include everything you will need for today's workshop, in regards of external dependencies. Go ahead and clone it locally. In order to run it, you will need to actually connect the LED to the specified pin, using one of the provided male-to-male cables. For the LED, you can use any of the exposed LED's labeled `RED`, `GREEN`, `BLUE` and `YELLOW`. Then you can run the following command:
```shell
cargo run --bin demo
@@ -715,7 +722,7 @@ Throughout this section, you will find multiple `HINT`s, please try to solve the
### Hi, I am new here!
-For your first task, you will need to print your team's name in terminal using the [defmt](https://crates.io/crates/defmt) whenever you press any of the four buttons labeled `SW4`, `SW5`, `SW6` and `SW7`.
+For your first task, you will need to print your team's name in terminal using the [defmt](https://crates.io/crates/defmt) whenever you press any of the four buttons labeled `S1`, `S2`, `S3` and `S4`.
#### Baby steps
@@ -725,7 +732,7 @@ You should begin by printing something in terminal. Take a look at the [defmt bo
#### Use the button
-Try to incorporate one button into your application. You will need to `await` a button press, and then print the message in a continuous loop. Check the Embassy's [`Input`](https://docs.embassy.dev/embassy-rp/git/rp235xb/gpio/struct.Input.html) documentation. Try to use one of the asynchronous functions, as it will be easier to extend it. Make sure you don't forget to wire the button.
+Try to incorporate one button into your application. You will need to `await` a button press, and then print the message in a continuous loop. Check the Embassy's [`ExtiInput`](https://docs.embassy.dev/embassy-stm32/git/stm32u545re/exti/struct.ExtiInput.html) documentation. Try to use one of the asynchronous functions, as it will be easier to extend it. Make sure you don't forget to wire the button.
[`HINT`](./hints.md#hi-i-am-new-here---hint-02)
@@ -741,7 +748,7 @@ This task requires singing one song of your desire, using the **active buzzer**
#### Configure the PWM
-You will need to a PWM output pin. In order to do that, you will need to decide what pin to use, and figure out what PWM Channel it is using. In order to do that, you will need to consult the [datasheet](https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf). The Embassy's PWM api can be consulted [here](https://docs.embassy.dev/embassy-rp/git/rp235xb/pwm/index.html).
+You will need to a PWM output pin. In order to do that, you will need to decide what pin to use, and figure out what PWM Channel it is using. In order to do that, you will need to consult the [datasheet](https://www.st.com/resource/en/datasheet/stm32u545ce.pdf). The Embassy's PWM api can be consulted [here](https://docs.embassy.dev/embassy-stm32/git/stm32u545re/lptim/pwm/struct.Pwm.html).
[`HINT`](./hints.md#sing-your-own-tune---hint-01)
@@ -780,7 +787,7 @@ const SONG: [(Option, i8); LEN] = {
A song is composed of either notes or pauses, which is why we chose to use the `Option` enum, and we chose to represent its length as fractions of the whole note (meaning that a `(None, 4)` represents a pause of a quarter of a note and a `(Some(Note::A2), 2)` is a half note A2). We omitted the dotted notes, and to represent them, we will use the negative counterparts of the notes. Therefore, `(Some(Note::C2), -2)` will be treated as a dotted C2 half note.
-In the `music.rs` module, you will find already defined an 8 note array that represents an octave. You will need to connect the buzzer to a pin, that you will configure as PWM output. Then, you need to iterate over the array and figure out the duration of the note based on the above mentioned convention and "play" it, by configuring the `top` register to generate a signal of matching the note's frequency, and a **duty cycle of 50%**. In order to mimic a real instrument, for the last 10% of the note duration we will turn off the buzzer.
+In the `music.rs` module, you will find already defined an 8 note array that represents an octave. You will need to connect the buzzer to a pin, that you will configure as PWM output. Then, you need to iterate over the array and figure out the duration of the note based on the above mentioned convention and "play" it, by configuring the timer's frequency in order to get a signal of matching the note's frequency, and a **duty cycle of 50%**. In order to mimic a real instrument, for the last 10% of the note duration we will turn off the buzzer.
Even though there are no *pause* notes, handling them now will help you later.
@@ -798,19 +805,19 @@ RGB LEDs are made of 3 separate LEDs (one for each color channel) that are contr

-The one on the board is a **common anode**, and because of that the LEDs are off when the signal is high, and are at maximum intensity when the signal is low. Keep that in mind when writing your code, and for a clearer picture, consult the [schematics](https://gitlab.cs.pub.ro/pmrust/pm-ma-pcb).
+The one on the board is a **common anode**, and because of that the LEDs are off when the signal is high, and are at maximum intensity when the signal is low.
#### Disco party
-Start by making the LED fade from blue to red. Remember what you learned at the previous exercise about configuring a PWM channel and **do not forget to wire the LED** using the sockets labeled `RGB_R`, `RGB_G` and `RGB_B`.
+Start by making the LED fade from blue to red. Remember what you learned at the previous exercise about configuring a PWM channel and **do not forget to wire the LED** using the sockets labeled `RGB_RED`, `RGB_GREEN` and `RGB_BLUE`.
[`HINT`](./hints.md#the-temperature-is-rising---hint-01)
#### The temperature is rising
-To read the temperature, we wil use the **BMP280** temperature and pressure sensor. You can find its datasheet [here](https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp280-ds001.pdf), and we highly encourage you to open it. The interaction with the sensor will be handled by the `bmp280` module. You will need to explore it a bit, in order to figure out what is it's api. The module exposes two submodules, `i2c` and `spi`, depending on the underlying communication protocol you want to use. For this workshop, we will use **I2C**, because it only requires you to connect the `BMP_SCL` and `BMP_SDA` to two I2C capable pins. The fastest way to determine two pins like so is to check the Raspberry Pi Pico 2 W [pinout](https://datasheets.raspberrypi.com/picow/pico-2-w-pinout.pdf).
+To read the temperature, we wil use the **BMP280** temperature and pressure sensor. You can find its datasheet [here](https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bmp280-ds001.pdf), and we highly encourage you to open it. The interaction with the sensor will be handled by the `bmp280` module. You will need to explore it a bit, in order to figure out what is it's api. The module exposes two submodules, `i2c` and `spi`, depending on the underlying communication protocol you want to use. For this workshop, we will use **I2C**, because it only requires you to connect the `BMP_SCL` and `BMP_SDA` to two I2C capable pins.
-Instantiate the `I2C` bus providing the two pins `SCA` and `SDL` and the respective peripheral (`I2C0` or `I2C1`) and a default config. Then instantiate the `BMP280` and set the control options, (power mode and pressure and temperature oversampling rate). You will also need to bind the interrupt for the I2C peripheral (`I2CX_IRQ`, where `X` is the peripheral number), using the `bind_interrupt!` macro. You can find an example in its [documentation](https://docs.embassy.dev/embassy-rp/git/rp235xb/macro.bind_interrupts.html).
+Instantiate the `I2C` bus providing the two pins `SCA` and `SDL` and the respective peripheral (`I2C0` or `I2C1`) and a default config. Then instantiate the `BMP280` and set the control options, (power mode and pressure and temperature oversampling rate). You will also need to bind the interrupt for the I2C peripheral (`I2CX_EV` and `I2CX_ER`, where `X` is the peripheral number), using the `bind_interrupt!` macro. You can find an example in its [documentation](https://docs.embassy.dev/embassy-stm32/git/stm32u545re/macro.bind_interrupts.html).
[`HINT`](./hints.md#the-temperature-is-rising---hint-02)