Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • pmrust/pmrust.pages.upb.ro
  • genan.omer/pmrust.pages.upb.ro
  • vladut.chintoiu/pmrust.pages.upb.ro
  • petru.nania/website-pmrust-fork
  • sofia.huzan/pmrust.pages.upb.ro
  • ionut.pruteanu2308/arcade-game
  • luana.militaru/pong-game
  • sebastian.scrob/project
  • matei.bejinaru/website-bejinaru-matei
  • adragomir2806/website-dragomir-alexandru
  • fatemehsadat/pmrust.pages.upb.ro
  • razvan.costea2205/pmrust.pages.upb.ro
  • darius_gabriel.iuga/pm-website
  • andrei.neagu1910/pmrust.pages.upb.ro
  • irina.chiorean/pmrust.pages.upb.ro
  • adrian_costin.lungu/pmrust.pages.upb.ro
  • andrei.salavastru/pmrust.pages.upb.ro
  • maria_elena.tudor/pmrust.pages.upb.ro
  • vlad.preda2503/electric-piano
  • delia_alexa.dragan/website-music-player
  • francisc.niss/automatic-guitar-tuner
  • mihnea.sandulache/pmrust.pages.upb.ro
  • dragos_andrei.rosu/pmrust.pages.upb.ro
  • armin.shafiei/the-tone-corrector
  • vladyslav.kiselar/pmrust.pages.upb.ro
  • carla_maria.rusu/pmrust.pages.upb.ro
  • razvan.beldie/pmrust.pages.upb.ro
27 results
Show changes
Commits on Source (26)
Showing
with 272 additions and 194 deletions
......@@ -11,7 +11,7 @@ drawings:
defaults:
foo: true
transition: slide-left
title: MA - 05 - Asynchronous Development
title: MA - 04 - Asynchronous Development
mdc: true
layout: cover
themeConfig:
......@@ -22,7 +22,7 @@ background:
---
# Asynchronous Development
Lecture 5
Lecture 4
---
---
......
......@@ -11,7 +11,7 @@ drawings:
defaults:
foo: true
transition: slide-left
title: MA - 03 - UART & SPI
title: MA - 05 - UART & SPI
mdc: true
layout: cover
themeConfig:
......@@ -22,7 +22,7 @@ background:
---
# UART & SPI
Lecture 6
Lecture 5
---
......
......@@ -11,7 +11,7 @@ drawings:
defaults:
foo: true
transition: slide-left
title: MA - 07 - I2C & USB 2.0
title: MA - 06 - I2C & USB 2.0
mdc: true
layout: cover
themeConfig:
......@@ -22,7 +22,7 @@ background:
---
# I2C & USB 2.0
Lecture 7
Lecture 6
---
......
......@@ -11,7 +11,7 @@ drawings:
defaults:
foo: true
transition: slide-left
title: MA - 03 - Exceptions and Interrupts
title: MA - 07 - Exceptions and Interrupts
mdc: true
layout: cover
themeConfig:
......@@ -22,7 +22,7 @@ background:
---
# Exceptions and Interrupts
Lecture 3
Lecture 7
---
......
......@@ -119,8 +119,8 @@ Embassy provides four types of channels synchronized using `Mutex`s
|-|-|
| [`Channel`](https://docs.embassy.dev/embassy-sync/git/default/channel/struct.Channel.html) | A Multiple Producer Multiple Consumer (MPMC) channel. Each message is only received by a single consumer. |
| [`PriorityChannel`](https://docs.embassy.dev/embassy-sync/git/default/priority_channel/struct.PriorityChannel.html) | A Multiple Producer Multiple Consumer (MPMC) channel. Each message is only received by a single consumer. Higher priority items are shifted to the front of the channel. |
| [`Signal`](https://docs.embassy.dev/embassy-sync/git/default/pubsub/struct.PubSubChannel.html) | Signalling latest value to a single consumer. |
| [`PubSubChannel`](https://docs.embassy.dev/embassy-sync/git/default/signal/struct.Signal.html) | A broadcast channel (publish-subscribe) channel. Each message is received by all consumers. |
| [`Signal`](https://docs.embassy.dev/embassy-sync/git/default/signal/struct.Signal.html) | Signalling latest value to a single consumer. |
| [`PubSubChannel`](https://docs.embassy.dev/embassy-sync/git/default/pubsub/struct.PubSubChannel.html) | A broadcast channel (publish-subscribe) channel. Each message is received by all consumers. |
---
---
......@@ -129,7 +129,7 @@ sends data from one task to another
[`Channel`](https://docs.embassy.dev/embassy-sync/git/default/channel/struct.Channel.html) - A Multiple Producer Multiple Consumer (MPMC) channel. Each message is only received by a single consumer.
[`Signal`](https://docs.embassy.dev/embassy-sync/git/default/pubsub/struct.PubSubChannel.html) - Signalling latest value to a single consumer.
[`Signal`](https://docs.embassy.dev/embassy-sync/git/default/signal/struct.Signal.html) - Signalling latest value to a single consumer.
```mermaid
flowchart LR
......@@ -166,7 +166,7 @@ flowchart LR
# PubSubChannel
sends data from one task to all receiver tasks
[`PubSubChannel`](https://docs.embassy.dev/embassy-sync/git/default/signal/struct.Signal.html) - A broadcast channel (publish-subscribe) channel. Each message is received by all consumers.
[`PubSubChannel`](https://docs.embassy.dev/embassy-sync/git/default/pubsub/struct.PubSubChannel.html) - A broadcast channel (publish-subscribe) channel. Each message is received by all consumers.
```mermaid
flowchart LR
......@@ -181,7 +181,7 @@ flowchart LR
# Channel Example
```rust{all|1|2|5,7,14,17-25|5,8-14}
```rust{1|2|5,7,14,17-25|5,8-14|all}
enum LedState { On, Off }
static CHANNEL: Channel<ThreadModeRawMutex, LedState, 64> = Channel::new();
......
......@@ -22,7 +22,7 @@ layout: two-cols
}
</style>
- MCUs are usually *single core*[^rp2040]
- MCUs are usually *single core*[^rp2350]
- Tasks in parallel require an OS[^interrupts]
- Tasks can be suspended at any time
- **Switching** the task is **expensive**
......@@ -54,7 +54,7 @@ sequenceDiagram
end
```
[^rp2040]: RP2040 is a dual core MCU, we use only one core
[^rp2350]: RP2350 is a dual core MCU, we use only one core
[^interrupts]: Running in an ISR is not considered a normal task
---
......
......@@ -9,10 +9,10 @@ of Embassy
# Bibliography
for this section
**Embassy Documentation**, *[Embassy executor](https://embassy.dev/book/dev/runtime.html)*
**Embassy Documentation**, *[Embassy executor](https://embassy.dev/book/#_embassy_executor)*
---
---
# Tasks
<div grid="~ cols-2 gap5">
......@@ -32,9 +32,9 @@ for this section
</div>
```rust {all|9-22|1-7|18-21|19|3-6|4|5}
```rust {9-22|1-7|18-21|19|3-6|4|5|all}
#[embassy_executor::task(pool_size = 2)]
async fn led_blink(mut led:Output<'static, PIN_X>) {
async fn led_blink(mut led: AnyPin) {
loop {
led.toogle();
Timer::after_secs(1).await;
......@@ -60,7 +60,7 @@ async fn main(spawner: Spawner) {
</div>
---
---
# Tasks can stop the executor
<div grid="~ cols-2 gap5">
......@@ -73,9 +73,9 @@ async fn main(spawner: Spawner) {
</div>
``` {all|5-8|3-9}
```rust {5-8|3-9|all}
#[embassy_executor::task]
async fn led_blink(mut led:Output<'static, PIN_X>) {
async fn led_blink(mut led: AnyPin) {
loop {
led.toogle();
// this does not execute anything
......@@ -114,6 +114,7 @@ async fn main(spawner: Spawner) {
---
layout: two-cols
---
## Priority Tasks
<style>
......@@ -139,7 +140,7 @@ unsafe fn SWI_IRQ_0() {
:: right ::
```rust {all|5,6,22|1,7-10|2,12-15|3,17-21}
```rust {5,6,22|1,7-10|2,12-15|3,17-21|all}
static EXECUTOR_HIGH: InterruptExecutor = InterruptExecutor::new();
static EXECUTOR_MED: InterruptExecutor = InterruptExecutor::new();
static EXECUTOR_LOW: StaticCell<Executor> = StaticCell::new();
......
......@@ -33,7 +33,7 @@ trait Future {
}
```
```rust {all|5,10|6|6,7|6|6,7|6|6,8|1,8}{lines: false}
```rust {5,10|6|6,7|6|6,7|6|6,8|1,8|all}{lines: false}
fn execute<F>(mut f: F) -> F::Output
where
F: Future
......@@ -74,7 +74,7 @@ sequenceDiagram
<div grid="~ cols-2 gap-5">
```rust {all|1-4|6-9|11-18}
```rust {1-4|6-9|11-18|all}
enum SleepStatus {
SetAlarm,
WaitForAlarm,
......@@ -97,7 +97,7 @@ impl Sleep {
<v-click>
```rust {all|1,20|1,2,20|4,19|5,18|6-10|11-17|11,12,13|11,14,15}{lines: false}
```rust {1,20|1,2,20|4,19|5,18|6-10|11-17|11,12,13|11,14,15|all}{lines: false}
impl Future for Sleep {
type Output = ();
......@@ -130,7 +130,7 @@ impl Future for Sleep {
<div grid="~ cols-2 gap-5">
```rust {all|1,20|1,2,20|4,19|5,18|6-10|11-17|11,12,13|11,14,15}{lines: false}
```rust {1,20|1,2,20|4,19|5,18|6-10|11-17|11,12,13|11,14,15|all}{lines: false}
impl Future for Sleep {
type Output = ();
......@@ -218,7 +218,7 @@ fn blink(led: Output<'static, PIN_X>) -> Blink {
<v-click>
```rust {4-23|5-22|5,6-10,22|5,11-17,22|12-14|5,11-17,22|12,14,15,16|5,18-21,22}{lines: false}
```rust {4-23|5-22|5,6-10,22|5,11-17,22|12-14|5,11-17,22|12,14,15,16|5,18-21,22|all}{lines: false}
impl Future for Blink {
type Output = ();
fn poll(&mut self) -> Poll<Self::Output> {
......@@ -257,7 +257,7 @@ impl Future for Blink {
- it does not know how to execute them
- executors are implemented into third party libraries
```rust {all|12|11,13,15|14}
```rust {12|11,13,15|14|all}
use engine::execute;
// Rust rewrites the function to a Future
......@@ -280,7 +280,7 @@ fn main() -> ! {
---
# Executor
```rust {all|1|4-16|5-12|14,15}
```rust {1|4-16|5-12|14,15|all}
static TASKS: [Option<impl Future>; N] = [None, N];
fn executor() {
......
......@@ -84,11 +84,12 @@ This is how a Rust application would look like
<div grid="~ cols-2 gap-4">
```rust{all|1|2|4|6|7,11|10|13-16}
```rust{all|1|2|4,5|7|8,12|11|14-17}
#![no_std]
#![no_main]
use cortex_m_rt::entry;
use core::panic::PanicInfo;
#[entry]
fn main() -> ! {
......@@ -126,6 +127,7 @@ This is how a Rust application would look like
use core::ptr::{read_volatile, write_volatile};
use cortex_m_rt::entry;
use core::panic::PanicInfo;
const GPIOX_CTRL: u32 = 0x4001_4004;
const GPIO_OE_SET: *mut u32= 0xd000_0024 as *mut u32;
......@@ -134,25 +136,25 @@ const GPIO_OUT_CLR:*mut u32= 0xd000_0018 as *mut u32;
#[panic_handler]
pub fn panic(_info: &PanicInfo) -> ! {
loop { }
loop { }
}
```
```rust {all}{startLine:18}
#[entry]
fn main() -> ! {
let gpio_ctrl = GPIOX_CTRL + 8 * pin as *mut u32;
unsafe {
write_volatile(gpio_ctrl, 5);
write_volatile(GPIO_OE_SET, 1 << pin);
let reg = match value {
0 => GPIO_OUT_CLR,
_ => GPIO_OUT_SET
};
write_volatile(reg, 1 << pin);
};
loop { }
let gpio_ctrl = (GPIOX_CTRL + 8 * pin) as *mut u32;
unsafe {
write_volatile(gpio_ctrl, 5);
write_volatile(GPIO_OE_SET, 1 << pin);
let reg = match value {
0 => GPIO_OUT_CLR,
_ => GPIO_OUT_SET
};
write_volatile(reg, 1 << pin);
};
loop { }
}
```
......
......@@ -212,7 +212,7 @@ read the number of elapsed μs since reset
#### Reading the time elapsed since restart
```rust{1,5|2,6|4,7,8|all}
```rust {1,5|2,6|4,7,8|all}
const TIMERLR: *const u32 = 0x400b_000c;
const TIMERHR: *const u32 = 0x400b_0008;
......@@ -228,7 +228,7 @@ The **reading order maters** as reading `TIMELR` latches the value in `TIMEHR` (
:: right ::
<div align="center">
<img src="./rp2350_timer_registers_1.png" class="rounded w-100">
<img src="./rp2350_timer_registers_1.png" class="rounded w-90">
</div>
---
......
......@@ -72,7 +72,7 @@ fn main() {
let age = 26;
println!("Hello, {}. You are {} years old", name, age);
// if the replacements are only variable, one can use the inline version
// if the replacements are only variables, one can use the inline version
println!("Hello, {name}. You are {age} years old");
}
```
......@@ -155,7 +155,7 @@ let y: u16 = 25;
| 32-bit | `i32` | `u32` | `int` / `Integer`[^java_unsigned] | `int` / `unsigned int` |
| 64-bit | `i64` | `u64` | `long` / `Long`[^java_unsigned] | `long long` / `unsigned long long` |
| 128-bit | `i128` | `u128` | N/A | N/A |
| arch | `isize` | `usize` | N/A | `int` / `unsigned int` |
| arch | `isize` | `usize` | N/A | `intptr_t` / `uintptr_t` |
**Floating Point** → Rust's floating point types are `f32` and `f64`, which are 32-bit and 64-bit in size, respectively. The default type is `f64` because on modern CPUs it is about the same speed as `f32` but is capable of more precision. All floating point types are **signed**.
......@@ -341,7 +341,7 @@ To format the `Debug` nicely use `{:#?}`.
### Tuple structures
Tuples are the same construct as structures, just that instead of using names for their field, they use numbers (indexes).
Tuples are the same as structures, just that instead of using names for their fields, they use numbers (indexes).
```rust
struct Color(i32, i32, i32);
......
......@@ -287,6 +287,7 @@ pin.set_high();
pin.set_low();
```
:::tip
While the device initialization is specific to every hardware device (the example uses the
......@@ -407,9 +408,15 @@ as they are connected to the lab board's debugger chip.
The board provides four single colored LEDs, red, green, blue and yellow. Each one of them
uses one pin for control. Each LED connector has one single hole on the board,
marked with `RED`, `GREEN', `BLUE` and `YELLOW` respectively. These are located in the **Connectors**
marked with `RED`, `GREEN`, `BLUE` and `YELLOW` respectively. These are located in the **Connectors**
section of the board.
:::warning
The LEDs are connected so they will light up if the pin is set to `Level::Low` and turn off if the pin is set to `Level::High`.
:::
The four switches that the lab board provides are signaled with labels
`SW4`, `SW5`, `SW6` and `SW7` in the connectors section.
......
website/lab/03/images/board_photoresistor.png

246 KiB

website/lab/03/images/board_rgb.png

433 KiB

website/lab/03/images/board_servo.png

942 KiB

website/lab/03/images/common_anode_common_cathode.png

90.6 KiB | W: 0px | H: 0px

website/lab/03/images/common_anode_common_cathode.png

227 KiB | W: 0px | H: 0px

website/lab/03/images/common_anode_common_cathode.png
website/lab/03/images/common_anode_common_cathode.png
website/lab/03/images/common_anode_common_cathode.png
website/lab/03/images/common_anode_common_cathode.png
  • 2-up
  • Swipe
  • Onion skin
website/lab/03/images/pwm_rp2040_pins.png

98 KiB | W: 0px | H: 0px

website/lab/03/images/pwm_rp2040_pins.png

51.7 KiB | W: 0px | H: 0px

website/lab/03/images/pwm_rp2040_pins.png
website/lab/03/images/pwm_rp2040_pins.png
website/lab/03/images/pwm_rp2040_pins.png
website/lab/03/images/pwm_rp2040_pins.png
  • 2-up
  • Swipe
  • Onion skin
website/lab/03/images/servo_motor.png

191 KiB

website/lab/03/images/servo_wires.png

176 KiB

---
description: Pulse Width Modulation and Analog to Digital Converters
slug: /lab/03
unlisted: true
---
# 03 - PWM & ADC
This lab will teach you the difference between digital and analog signals, how to simulate analog signals by using Pulse Width Modulation (PWM) and how to convert analog signals to digital ones using Analog-to-Digital Converters (ADC).
The purpose of this lab is to help you learn about different types of signals in electronics and how to work with them.
## Concepts
- Understanding digital vs. analog signals;
- Simulating analog signals using Pulse Width Modulation (PWM);
- Converting analog signals to digital using Analog-to-Digital Converters (ADC);
- Practical applications of PWM and ADC in circuit design.
- How to use the lab board's components.
## Resources
1. **Raspberry Pi Ltd**, *[RP2040 Datasheet](https://datasheets.raspberrypi.com/rp2040/rp2040-datasheet.pdf)*
- Chapter 2 - *System Description*
- Chapter 2.15 - *Clocks*
- Subchapter 2.15.1
- Subchapter 2.15.2
- Chapter 4 - *Peripherals*
- Chapter 4.5 - *PWM*
- Chapter 4.6 - *Timer*
- Chapter 4.9 - *ADC and Temperature Sensor*
- Subchapter 4.9.1
- Subchapter 4.9.2
- Subchapter 4.9.5
1. **Raspberry Pi Ltd**, *[RP2350 Datasheet](https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf)*
- Chapter 8 - *Clocks*
- Subchapter 8.1.1
- Subchapter 8.1.2
- Chapter 12 - *Peripherals*
- Chapter 12.4 - *ADC and Temperature Sensor*
- Subchapter 12.4.1
- Subchapter 12.4.3
- Subchapter 12.4.6
- Chapter 12.5 - *PWM*
- Chapter 12.8 - *System Timers*
2. **Paul Denisowski**, *[Understanding PWM](https://www.youtube.com/watch?v=nXFoVSN3u-E)*
## Timing
In embedded applications, keeping track of time is crucial. Even for the simple task of blinking a led at a certain time interval, we need a reference of time that is constant and precise.
In embedded applications, keeping track of time is crucial. Even for the simple task of blinking an LED at a certain time interval, we need a reference of time that is constant and precise.
### Clocks
......@@ -36,16 +41,15 @@ A clock is a piece of hardware that provides us with that reference. Its purpose
![ClockSignal](images/clock_signal.png)
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 negligeable. 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 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.
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 125MHz clock, so the crystal oscillator frequency of 12MHz is multiplied (this is done using a method called Phase-Locked Loop).
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.
![RPCrystal](images/rp_crystal.png)
### Counters
A counter is a piece of hardware logic that counts, as its name suggests. Every clock cycle, it increments the value of a register, until it overflows and starts anew.
A counter in electronics is a tool that tracks numbers, typically by adding or subtracting one each time the clock ticks. When it reaches its maximum (or minimum) value, it resets or wraps around. This reset is called an "overflow" (when counting up) or "underflow" (when counting down). Some counters can also switch between counting up and down based on control signals.
:::info
A regular counter on 8 bits would count up from 0 to 255, then loop back to 0 and continue counting.
......@@ -66,7 +70,7 @@ The way the counter works here is that it increments/decrements every clock cycl
#### SysTick
The ARM Cortex-M uses 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-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.
- `SYST_CVR` register - the value of the timer itself
- `SYST_RVR` register - the reset value
......@@ -76,16 +80,18 @@ The ARM Cortex-M uses the SysTick time counter to keep track of time. This count
### Timers
Until now, we have been able to blink a led at a certain time interval, by busy waiting a while between each led toggle. The technique we used so far was asking the processor to skip a clock cycle a number of times, or by calling the processor instruction `nop` (no operation) in a loop.
The simplest way to make a processor wait is to ask the processor to skip a clock cycle a number of times, or by calling the processor instruction `nop` (no operation) in a loop.
:::info
This method is not ideal, since the `nop` instruction stalls the processor and wastes valuable time that could otherwise be used to do other things in the meantime. To optimize this, we can use *alarms*.
:::
An **alarm** is a counter that triggers an interrupt every time it reaches a certain value. This way, an alarm can be set to trigger after a specific interval of time, and while it's waiting, the main program can continue executing instructions, and so it is not blocked. When the alarm reaches the chosen value, it goes off and triggers an interrupt that can then be handled in its specific ISR.
An **alarm** is a counter that triggers an interrupt every time it reaches a certain value. This way, an alarm can be set to trigger after a specific interval of time, and while the **alarm** hardware is *running in the background*, the main program can continue executing instructions, and so it is not blocked. When the alarm reaches the chosen value, it goes off and triggers an interrupt that can then be handled in its specific Interrupt Service Routine (ISR).
![Alarm](images/alarm.svg)
:::info
The RP2040 timer is fully monotomic, meaning it can never truly overflow. Its value is stored on 64 bits and increments every 1 microsecond, which means that the last value it can increment to before overflowing is 2<sup>64-1</sup>, which the equivalent of roughly 500,000 years. This timer allows 4 different alarms, which can be used independently (`TIMER_IRQ_0/1/2/3`).
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<sup>^64-1</sup>, 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.
:::
## Analog and Digital Signals
......@@ -99,7 +105,7 @@ The RP2040 timer is fully monotomic, meaning it can never truly overflow. Its va
![DigitalSignal](images/digital_signal.png)
## Pulse-Width Modulation (PWM)
Up to now, we learned to turn a led on and off, or in other words, set a led's intensity to 100% or 0%. What if we wanted to turn on the led only at 50% intensity? We only have a two-level digital value, 0 or 1, so technically a value of 0.5 is not possible. What we can do is simulate this analog signal, so that it *looks* like the led is at half intensity.
Up to now, we learned to turn an LED on and off, or in other words, set a LED's intensity to 100% or 0%. What if we wanted to turn on the LED only at 50% intensity? We only have a two-level digital value, 0 or 1, so technically a value of 0.5 is not possible. What we can do is simulate this analog signal, so that it *looks* like the LED is at half intensity.
**Pulse-Width Modulation** is a method of simulating an analog signal using a digital one, by varying the width of the generated square wave.
......@@ -123,7 +129,7 @@ duty\_cycle = \frac{time\_on}{period} \%
$$
For the RP2040, to generate this PWM signal, a *counter* is used. The PWM counter is controlled by these registers (`X` can be from 0-7, depending on the channel):
*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):
- `CHX_CTR` - the actual value of the counter
- `CHX_CC` - the value that the counter will compare to
......@@ -133,7 +139,7 @@ When `CHX_CTR` is reset, the value of the output signal is 1. The counter counts
![PWMRP2040](images/pwm_rp2040_example.png)
On RP2040, all GPIO pins support PWM. Every two pins share a PWM slice, and each one of them is on a separate channel.
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.
![RP2040PWMPins](images/pwm_rp2040_pins.png)
......@@ -172,13 +178,25 @@ For this lab, we will be using **common anode** RGB LEDs, which means that the P
#### How to wire an RGB LED
For **common cathode** RGB LEDs, we must tie each of the 3 color led legs to GPIO pins in series with a *resistance*, and connect the fourth pin to **GND**.
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.
For **common anode** RGB LEDs, we must also tie each of the 3 color led legs to GPIO pins in series with a *resistance*, but connect the fourth pin to **3V3** instead.
![RGBWiring](images/board_rgb.png)
:::danger
Do not forget to tie a resistance to each color pin of the RGB LED!
:::
#### How to wire a servo motor
A **servo motor** is a motor that can be controlled with PWM. It has an arm that can be rotated to a specific angle, depending on the PWM signal it receives.
A servo motor has three wires:
- **Power** - usually red, connected to a voltage source (5V)
- **Ground** - usually black, connected to the ground
- **Signal** - usually orange, connected to a PWM pin
![ServoMotorWiring](images/servo_wires.png)
The board provides the connectors for the servo motor. These connectors are labeled `GND`, `PWR`, and `SIG` in the connectors section.
![ServoMotor](images/board_servo.png)
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.
### PWM in Embassy-rs
......@@ -192,13 +210,15 @@ let peripherals = embassy_rp::init(Default::default());
In order to modify the PWM counter configurations, we need to create a `Config` for our PWM.
```rust
use embassy_rp::pwm::Config as ConfigPwm; // PWM config
// 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 = 0x8000; // in HEX, equals 32768 in decimal
// Set compare value (counter value at which the PWM signal will change from 1 to 0)
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;
```
......@@ -210,26 +230,129 @@ In this case, `config.compare_a` is half of `config.top`. This means that the du
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.
```rust
// Create PWM driver for pin 0
let mut pwm = Pwm::new_output_a( // output A
peripherals.PWM_CH0, // channel 0
peripherals.PIN_0, // pin 0
// 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()
);
```
:::warning
1. The code above is an example for pin 0. You need to modify the channel, output and pin depending on the PWM pin you choose to use!
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!
:::
If we decide to modify the value of `compare_a` or `compare_b`, we have to update the configuration for the PWM.
```rust
config.compare_a += 100; // modified value of `compare_a`
config.compare_b += 100; // modified value of `compare_b`
pwm.set_config(&config); // set the new configuration for PWM
```
### Controlling a Servo Motor Using PWM
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());
```
To control a servo motor using PWM, we need to calculate the **TOP value**, which determines the PWM period.
#### Calculating the TOP Value
Servos typically expect a **50 Hz** PWM signal, which corresponds to a **20 ms** period.
$$
top = \left( \frac{f_{clock}}{f_{pwm} \times divider} \right) - 1
$$
For example, with:
$$
\begin{aligned}
f_{clock} &= 150 \,MHz \\
f_{PWM} &= 50 \,Hz \\
divider &= 64
\end{aligned}
$$
We get:
$$
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**.
To convert these pulse widths into PWM compare values, we use:
$$
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).
Now, let's implement this in Rust:
```rust
// Configure PWM for servo control
let mut servo_config: PwmConfig = Default::default();
// Set the calculated TOP value for 50 Hz PWM
servo_config.top = 0xB71A;
// Set the clock divider to 64
servo_config.divider = 64_i32.to_fixed(); // Clock divider = 64
// 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;
```
After setting up the PWM configuration, we want to create a control loop that can dynamically adjust the servo's position.
```rust
// Initialize PWM for servo control
let mut servo = Pwm::new_output_a(
peripherals.PWM_SLICE1,
peripherals.PIN_2,
servo_config.clone()
);
// 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)
}
```
## Analog-to-Digital Converter (ADC)
Now we know how to represent an analog signal using digital signals. There are plenty of cases in which we need to know how to transform an analog signal into a digital one, for example a temperature reading, or the voice of a person. This means that we need to correctly represent a continuous wave of infinite values to a discrete wave of a finite set of values.
......@@ -268,7 +391,7 @@ In other words, we must sample at least twice per cycle.
- temperature sensor
- potentiometer
- photoresistor (what we will be using for this lab)
- photoresistor
A **photoresistor** (or photocell) is a sensor that measures the intensity of light around it. Its internal resistance varies depending on the light hitting its surface; therefore, the more light there is, the lower the resistance will be.
......@@ -276,7 +399,12 @@ A **photoresistor** (or photocell) is a sensor that measures the intensity of li
#### How to wire a photoresistor
To wire a photoresistor, we need to connect one leg to *GND* and the other leg to a voltage divider. Take a look at the [Electronics](/tutorial/electronics/index.md#voltage-divider) tutorial:
The photoresistor that the board provides is signaled with the label `PHOTORESISTOR` in the connectors section.
![PhotoresistorWiring](images/board_photoresistor.png)
:::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. Take a look at the [Electronics](/tutorial/electronics/index.md#voltage-divider) tutorial:
$$
V_{out} = V_{in} * \frac{R_{2}}{R_{1} + R_{2}};
$$
......@@ -290,6 +418,8 @@ In our case:
This way, the ADC pin measures the photoresistor's resistance, without the risk of a short-circuit.
![PhotoresistorWiring](images/photoresistor_wiring.png)
:::
### ADC in Embassy-rs
......@@ -325,129 +455,67 @@ Now, we need to initialize the ADC pin we will be using. The Raspberry Pi Pico h
```rust
// Initialize ADC pin
let mut adc_pin = Channel::new_pin(peripherals.PIN_X, Pull::None); // where X should be replaced with a pin number
// X should be replaced with a pin number
let mut adc_pin = Channel::new_pin(peripherals.PIN_X, Pull::None);
```
Once we have the ADC and pin set up, we can start reading values from the pin.
```rust
let level = adc.read(&mut adc_pin).await.unwrap(); // read a value from the pin
info!("Light sensor reading: {}", level); // print the value over serial
Timer::after_secs(1).await; // wait a bit before reading and printing another value
```
loop {
// read a value from the pin
let level = adc.read(&mut adc_pin).await.unwrap();
## Exercises
1. Connect an LED to pin GP2, a photo-resistor to ADC0 and an RGB LED to pins GP1, GP3, GP4. Use [KiCad](https://www.kicad.org/) to draw the schematics. (**1p**)
2. Take a look at `lab04_ex2` in the lab skeleton. It is a working example of lighting an LED using PWM at 50% intensity. The LED in the example is connected to GP0.
- Modify the provided example to light the LED of *your circuit* to 25% intensity. (**1p**)
- Increase the LED's intensity by 10% every second, until it reaches max intensity, when it stops. (**1p**)
3. Read the value of the photo-resistor and print it to the console. (**2p**)
// print the value over serial
info!("Light sensor reading: {}", level);
:::info
To see the console with messages from the Pico, use the flash command with an extra `-s` parameter.
```bash
elf2uf2-rs -s -d /target/thumbv6m-none-eabi/debug/<crate_name>
// wait a bit before reading and printing another value
Timer::after_secs(1).await;
}
```
:::
:::info
To be able to print messages to the console, we need to send messages over a simulated serial port to the computer. For this, we will use the USB driver provided by Embassy.
```rust
use embassy_rp::usb::{Driver, InterruptHandler};
use embassy_rp::{bind_interrupts, peripherals::USB};
use log::info;
// Use for the serial over USB driver
bind_interrupts!(struct Irqs {
USBCTRL_IRQ => InterruptHandler<USB>;
});
## Exercises
:::info
If you need to remember the layout of the board, check the [Lab Board](./02#the-lab-board) section from the previous lab.
// The task used by the serial port driver
// over USB
#[embassy_executor::task]
async fn logger_task(driver: Driver<'static, USB>) {
embassy_usb_logger::run!(1024, log::LevelFilter::Info, driver);
}
Remember that LEDs are wired so they light up on `Level::Low` and turn off on `Level::High` and buttons return `Level::Low` when pressed and `Level::High` when not pressed.
:::
#[embassy_executor::main]
async fn main(spawner: Spawner) {
let peripherals = embassy_rp::init(Default::default());
:::danger
Please make sure the lab professor verifies your circuit before it is powered up.
:::
// Start the serial port over USB driver
let driver = Driver::new(peripherals.USB, Irqs);
spawner.spawn(logger_task(driver)).unwrap();
1. Write a program using Embassy that adjusts the brightness of an LED connected to GPIO pin by changing the PWM duty cycle.
- Light up the LED at 25% intensity. (**1p**)
- Make the LED change intensity from 0% to 100% in 10% increments every 1 second. (**1p**)
:::info
You should use the lab skeleton provided in the [Lab Repository](https://github.com/UPB-PMRust/lab-2025) as a starting point for your implementation.
// ...
Embassy will reset all the peripherals when the `main` function exits, that means `PWM` and `ADC` will stop. Make sure the `main` function never exits so you can see how the circuit behaves.
:::
info!("message");
}
2. Write a program using Embassy to control the led intensity using a potentiometer. The potentiometer is connected to an ADC-capable GPIO pin. The LED should change intensity based on the potentiometer's position. (**2p**)
```
:::warning
Unlike the photoresistor, which requires an external resistor to form a voltage divider, a potentiometer already has an internal voltage divider. You only need to connect its three pins:
- One leg to VCC.
- The middle pin to an ADC-capable GPIO pin.
- One leg to GND.
(The potentiometer that the board provides is signaled with label `POTENTIOMETER` in the connectors section.)
:::
:::warning
Notice that the USB driver also uses an `InterruptHandler` import that could be confused with the `InterruptHandler` used by ADC. Make sure to use different naming conventions for each one, as described in the warning [here](#adc-in-embassy-rs).
:::
4. Depending on the value read from the photo-resistor, brighten or dim the led. The led should shine brighter when there is *less* light in the room. (**2p**)
:::tip
Use the serial console to debug your program!
:::
5. Make the RGB LED switch from red -> yellow -> blue every time the switch A is pressed. (**2p**)
3. Make the RGB LED switch from red -> yellow -> blue every time the button `SW4` is pressed. (**2p**)
:::note
The reason why we **can't** use GP1, GP2 and GP3 for the RGB LED, for example, is because GP2 and GP3 are both on PWM channel 1, therefore we can't independently control them with PWM.
`GP2`, `GP3`, and `GP4` can be used to control an RGB LED. Although `GP2` and `GP3` share the same PWM channel and therefore have the same frequency, each pin can have an **independent duty cycle**, allowing individual control of the brightness for each color.
:::
![Colors](images/colors.png)
6. Using the `SysTick` interrupt in *bare metal*, make the led blink at a 100ms delay. (**1p**)
:::tip
Setting up the `SysTick` counter:
```rust
const SYST_RVR: *mut u32 = 0xe000_e014 as *mut u32;
const SYST_CVR: *mut u32 = 0xe000_e018 as *mut u32;
const SYST_CSR: *mut u32 = 0xe000_e010 as *mut u32;
// fire systick every 5 seconds
let interval: u32 = 5_000_000;
unsafe {
write_volatile(SYST_RVR, interval);
write_volatile(SYST_CVR, 0);
// set fields `ENABLE` and `TICKINT`
write_volatile(SYST_CSR, 0b011);
// we need to write the whole register, single bit modifications is not possible with the SYST_CSR register
}
```
Registering the `SysTick` handler:
```rust
#[exception]
unsafe fn SysTick() {
/* systick fired */
}
```
:::
:::info
To safely share a bool value globally to keep track of the LED status, we need to use an [`AtomicBool`](https://doc.rust-lang.org/std/sync/atomic/struct.AtomicBool.html). This requires less code than using a normal `bool` and a `Mutex`.
4. Write a program using Embassy that measures light intensity using a photoresistor connected to ADC.
- Use `defmt` to display the measured light intensity. (**1p**)
- Make the RGB LED change color based on the light intensity. Red for low intensity, green for medium intensity, and blue for high intensity. (**1p**)
Creating a new static `AtomicBool`:
```rust
// imports
use core::sync::atomic::{AtomicBool, Ordering};
static atomic_bool: AtomicBool = AtomicBool::new(false);
```
5. Write a program using Embassy that moves a servo motor smoothly between 0° and 180°, then back to 0°, in a continuous loop. (**2p**)
Reading the value of an `AtomicBool`:
```rust
let atomic_bool_value = atomic_bool.load(Ordering::Relaxed);
```
Writing the value of an `AtomicBool`:
```rust
atomic_bool.store(true, Ordering::Relaxed);
```
:::