Skip to content

Commit

Permalink
finish up spinning and blinking
Browse files Browse the repository at this point in the history
  • Loading branch information
AdinAck committed Aug 19, 2024
1 parent ab7af4d commit 3d523cf
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ weight: 1

Making an LED blink is rather straight forward:

```c
```cpp
const unsigned int LED{17}; // define a constant for the LED pin

void setup() {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
94 changes: 93 additions & 1 deletion content/assignments/spinning-and-blinking/arduino/spinning.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,101 @@
title: Spinning
type: docs
prev: assignments/spinning-and-blinking/arduino/blinking
next: assignments/spinning-and-blinking/submission
weight: 3
---

It's time to get our hands dirty and really change this firmware. We're going to make a motor spin, but to do that, firmware won't be enough.

Pick up a **Motion Shield** and any complementary components/equipment.
## Preparation

Pick up a **Motion Shield** and:
- 9v battery and connector
- DC motor
- Jumper
- Wires
- Tiny screwdriver

![](images/motion-pinout.png)

This ^ is the pinout diagram for the motion shield. It shows you how GPIO from the DevBoard are allocated.

We want to spin a motor, so we're going to use one of the motor output ports at the bottom.

## How do motors work?

The standard method of driving DC motors is with a structure called a **Full Bridge**:

![](images/full-bridge.png)

There are two output stages that can assert a voltage of either **GND** or **VS**.

If stage **A** is `HIGH` and stage **B** is `LOW`, current would flow through the motor
from `+` to `-` and the motor would exert a torque $\vec \tau$.

If stage **A** is `LOW` and stage **B** is `HIGH`, current would flow through the motor
from `-` to `+` and the motor would exert a torque $-\vec \tau$.

So let's try it. Looking at the bottom of the pinout diagram, we can see which GPIO
correspond to each motor port.

Solder some wires onto your motor and plug them into a motor port. Also, place a jumper
on the enable port for the motor channel you are using.

Let's change our blinky code to configure two more output pins:

```cpp
const unsigned int LED{17};
// add these
const unsigned int MTR_HI{?};
const unsigned int MTR_LO{?};

void setup() {
pinMode(LED, OUTPUT);
// and these
pinMode(MTR_HI, OUTPUT);
pinMode(MTR_LO, OUTPUT);

// configure pins to spin the motor in a direction
digitalWrite(MTR_HI, HIGH);
digitalWrite(MTR_LO, LOW);
}
```

Run this, and watch the motor spin!

You can reverse the direction by inverting the levels of these pins. Try it!

## Greater Granularity

You don't *always* want to exert maximum torque in either direction, but rather some
*proportion* of the maximum torque.

To achieve this, we can use **P**ulse **W**idth **M**odulation (PWM).

Rather than holding the output stages steady at `HIGH` or `LOW`, we can rapidly
change the state, targeting an on *proportion*.

Imagine we hold stage **B** `LOW`, and over intervals of `1us`, pull stage **A** `HIGH` for `200ns`
and `LOW` for the remaining `800ns`. *On average*, it would look as though the voltage on the switch
node of stage A were $V_s \cdot \frac{200ns}{1us}=0.2V_s$, which roughly corresponds to 1/5th
the maximum torque.

Arduino let's us do this with the `analogWrite` function.

`analogWrite` accepts values in the range `0..=255` (unsigned 8bit integer).

The following is the equivalent of our previous test but with `analogWrite`:

```cpp
analogWrite(MTR_HI, 255);
analogWrite(MTR_LO, 0);
```
Run this and watch the motor spin!
Swap these numbers and watch it spin the other way!
## Challenge
Make the motor smoothly oscillate back and forth between spinning forward and backward.
34 changes: 5 additions & 29 deletions content/assignments/spinning-and-blinking/rust/blinking.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,17 @@ use esp_hal::{
peripherals::Peripherals,
prelude::*,
system::SystemControl,
timer::{timg::TimerGroup, ErasedTimer, OneShotTimer},
timer::{timg::TimerGroup},
};
use esp_println::println;
use static_cell::StaticCell;
```

This looks like a lot, but these are just crates we are importing to use in our
program. We will see *exactly* which crates we use and why as we explore the rest
of this file.

```rust
#[main]
#[esp_hal_embassy::main]
async fn main(_spawner: Spawner) {
// ...
}
Expand Down Expand Up @@ -102,40 +101,17 @@ esp_println::logger::init_logger_from_env();
Next up we enable logging via the ESP32s JTAG[^8] controller.

```rust
let timg0 = TimerGroup::new(peripherals.TIMG0, &clocks, None);
let timg0 = TimerGroup::new(peripherals.TIMG0, &clocks);
```

Then we initialize the 0th timer group (which we can use to provide Embassy with a means for
keeping time).

```rust
let timer = {
static TIMER: StaticCell<[OneShotTimer<ErasedTimer>; 1]> = StaticCell::new();

TIMER.init([OneShotTimer::new(timg0.timer0.into())])
};
```

This section is a little more complicated, but the next line...

```rust
esp_hal_embassy::init(&clocks, timer);
esp_hal_embassy::init(&clocks, timg0.timer0);
```

...makes it a little more clear.

Embassy needs a `OneShotTimer<'static, T>` to provide monotonics[^10]. The `'static` indicates
that this timer must exist for the entire duration of the program. `let` bindings always exist
until the scope they were bound in ends. To create a `static` lifetime, the variable must be
defined as `static`. But `static` values must be `const` (i.e. known at compile time and constant).

So how do we take a runtime value (the timer) and coerce it into a `static` lifetime?

Well we know that even though the timer doesn't exist for the *entire* duration of the program
(it didn't exist before we initialized it at runtime), it *will* exist for the *rest* of the program. With this knowledge, we effectively satisfy the `static`
requirement because the scope of use of Embassy is a subset of the lifetime of the timer.

We use the `StaticCell` structure to accomplish the lifetime coersion.
Embassy needs a `impl TimerCollection` (some time that implements the trait `TimerCollection`) to provide monotonics[^10].

{{< callout type="info" >}}
We understand that this may be uncharted territory for many of you. We implore you to do your own
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions content/assignments/spinning-and-blinking/rust/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,21 @@ The resulting file structure should look something like:
You can learn more about this structure [here](https://embassy.dev/book/#_project_structure).
{{< /callout >}}

Open the `Cargo.toml` file and append the following:

```toml
[patch.crates-io]
esp-hal = { git = "https://github.com/esp-rs/esp-hal/", rev = "f95ab0def50130a9d7da0ba0101c921e239ecdb5" }
esp-hal-embassy = { git = "https://github.com/esp-rs/esp-hal/", rev = "f95ab0def50130a9d7da0ba0101c921e239ecdb5" }
esp-backtrace = { git = "https://github.com/esp-rs/esp-hal/", rev = "f95ab0def50130a9d7da0ba0101c921e239ecdb5" }
esp-println = { git = "https://github.com/esp-rs/esp-hal/", rev = "f95ab0def50130a9d7da0ba0101c921e239ecdb5" }
```

{{< callout type="warning" >}}
The ESP32 Rust HAL is *highly* volatile right now, so as a precautionary measure,
we are locking the version of the esp related crates with a dependency patch.
{{< /callout >}}

## Checkpoint

Let's make sure all we've done so far is working properly.
Expand Down
189 changes: 188 additions & 1 deletion content/assignments/spinning-and-blinking/rust/spinning.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,196 @@
title: Spinning
type: docs
prev: assignments/spinning-and-blinking/rust/blinking
next: assignments/spinning-and-blinking/submission
weight: 3
math: true
---

It's time to get our hands dirty and really change this firmware. We're going to make a motor spin, but to do that, firmware won't be enough.

Pick up a **Motion Shield** and any complementary components/equipment.
## Preparation

Pick up a **Motion Shield** and:
- 9v battery and connector
- DC motor
- Jumper
- Wires
- Tiny screwdriver

![](images/motion-pinout.png)

This ^ is the pinout diagram for the motion shield. It shows you how GPIO from the DevBoard are allocated.

We want to spin a motor, so we're going to use one of the motor output ports at the bottom.

## How do motors work?

The standard method of driving DC motors is with a structure called a **Full Bridge**:

![](images/full-bridge.png)

There are two output stages that can assert a voltage of either **GND** or **VS**.

If stage **A** is `HIGH` and stage **B** is `LOW`, current would flow through the motor
from `+` to `-` and the motor would exert a torque $\vec \tau$.

If stage **A** is `LOW` and stage **B** is `HIGH`, current would flow through the motor
from `-` to `+` and the motor would exert a torque $-\vec \tau$.

So let's try it. Looking at the bottom of the pinout diagram, we can see which GPIO
correspond to each motor port.

Solder some wires onto your motor and plug them into a motor port. Also, place a jumper
on the enable port for the motor channel you are using.

Let's change our blinky code to configure two more output pins:

```rust
let io = Io::new(peripherals.GPIO, peripherals.IO_MUX);

let mut led = Output::new(io.pins.gpio17, Level::Low);
// add these
let motor_hi = Output::new(io.pins.gpio?, Level::High /* set default pin configuration to HIGH */);
let motor_lo = Output::new(io.pins.gpio?, Level::Low);
```

Run this, and watch the motor spin!

You can reverse the direction by inverting the levels of these pins. Try it!

## Greater Granularity

You don't *always* want to exert maximum torque in either direction, but rather some
*proportion* of the maximum torque.

To achieve this, we can use **P**ulse **W**idth **M**odulation (PWM).

Rather than holding the output stages steady at `HIGH` or `LOW`, we can rapidly
change the state, targeting an on *proportion*.

Imagine we hold stage **B** `LOW`, and over intervals of `1us`, pull stage **A** `HIGH` for `200ns`
and `LOW` for the remaining `800ns`. *On average*, it would look as though the voltage on the switch
node of stage A were $V_s \cdot \frac{200ns}{1us}=0.2V_s$, which roughly corresponds to 1/5th
the maximum torque.

ESP32s actually have a peripheral specifically for generating PWM control signals for motors, the
peripheral is appropriately named `MCPWM`.

Let's configure it!

First up, let's add a new crate `fugit`, which provides us with some convenient
integer trait extensions for providing units of time. We'll use this later.

```sh
cargo add fugit
```

Then import the extension trait[^1] at the top of the file:

```rust
use fugit::RateExtU32;
```

Go back to where we defined the motor pins and rewrite it like this:

```rust
let motor_hi_pin = io.pins.gpio13;
let motor_lo_pin = io.pins.gpio14;
```

Rather than being standard outputs, we'll make these pins available to the MCPWM peripheral.

Now let's set up the MCPWM peripheral for use.

The MCPWM peripheral's clock is sourced from the `crypto_pwm_clock`, we can verify
its speed:

```rust
println!("src clock: {}", clocks.crypto_pwm_clock);
```

You should see:

```
src clock: 160000000 Hz
```

since the default system clock configuration is `160MHz` and the `crypto_pwm_clock`
default prescaler is `Div1`.

> It's important that we check these things because we are going to try to achieve
> a target PWM frequency, which is going to be sourced from the peripheral's
> source clock.
Now, we configure our peripheral (the MCPWM) clock:

```rust
let clock_cfg = PeripheralClockConfig::with_frequency(&clocks, 32.MHz()).unwrap();
```

> If we hadn't imported the fugit integer extension trait, we would have encountered
> error code [E0599](https://doc.rust-lang.org/error_codes/E0599.html) from our usage
> of `.MHz()` because Rust can only find trait methods if the trait is in scope.
This function solves for the appropriate prescaler to achieve a frequency of `32MHz`.
(In this case, `Div5`)

> You could just specify the prescaler by using `with_prescaler` and avoid unnecessary runtime fallibility!
Now let's initialize the peripheral with this clock configuration:

```rust
let mut mcpwm = McPwm::new(peripherals.MCPWM0, clock_cfg);
```

Then attach a timer to an operator:

```rust
mcpwm.operator0.set_timer(&mcpwm.timer0);
```

MCPWM's operators control two pins (convenient!) each and require a timer to fascilitate operation.

```rust
let (mut motor_hi, mut motor_lo) = mcpwm.operator0.with_pins(
motor_hi_pin,
PwmPinConfig::UP_ACTIVE_HIGH,
motor_lo_pin,
PwmPinConfig::UP_ACTIVE_HIGH,
);
```

And now we create a PWM configuration from our clock configuration:

```rust
let timer_clock_cfg = clock_cfg
.timer_clock_with_frequency(u8::MAX as u16, PwmWorkingMode::Increase, 20.kHz())
.unwrap();
```

`u8::MAX as u16` sets the maximum duty cycle value. You can change this to whatever you want.
The greater this number, the higher the resolution. I decided an 8bit unsigned integer
is a reasonable duty cycle space.

We can use this configuration to initiate PWM operation:

```rust
mcpwm.timer0.start(timer_clock_cfg);
```

And finally, configure the duty cycles. Let's spin in a direction at full torque:

```rust
motor_hi.set_timestamp(255);
motor_lo.set_timestamp(0);
```

Run this and watch the motor spin!

Swap these numbers and watch it spin the other way!

## Challenge

Make the motor smoothly oscillate back and forth between spinning forward and backward.

[^1]: An extension trait is a trait that extends the capability of primitive types.
Loading

0 comments on commit 3d523cf

Please sign in to comment.