2. Call an async function#

pw_async2: Cooperative async tasks for embedded

The task that you created in the last step isn’t actually asynchronous yet. It runs from start to finish without stopping. Most real-world tasks need to wait for one reason or another. For example, one task may need to wait for a timer to expire. Another one might need to wait for a network packet to arrive. In the case of the vending machine app, your task needs to wait for a user to insert a coin.

In pw_async2, operations that can wait represented by futures.

Wait for a coin#

Your vending machine will use the CoinSlot class to read the number of coins that a customer inserts. We’ve already implemented this class for you. Update your VendingMachineTask to use CoinSlot now.

  1. Study the GetCoins() declaration in pw_async2/codelab/coin_slot.h.

    1  // Returns a future that resolves when coins are deposited.
    2  // May only be called by one task.
    3  CoinFuture GetCoins();
    

    Asynchronous operations like GetCoins() return some type of Future. A future is an object that represents the result of an asynchronous operation that may or may not be complete.

    Similar to a task’s DoPend(), futures have a Pend() method to progress them, which takes an async Context and returns a Poll of some value. When a task pends a future, it checks the return value to determine how to proceed:

    • If it’s Ready(value), the operation is complete, and the task can continue with the value.

    • If it’s Pending(), the operation is not yet complete. The task will generally stop and return Pending() itself, effectively “sleeping” until it is woken up.

    When a task is sleeping, it doesn’t consume any CPU cycles. The Dispatcher won’t poll it again until an external event wakes it up. This is the core of cooperative multitasking.

    Note

    If you peek into pw_async2/codelab/coin_slot.cc you’ll see that it internally “wakes” its future when coins are received. You’ll learn more about waking in the next step.

  2. Update the VendingMachineTask declaration in vending_machine.h to use CoinSlot:

    • Include the coin_slot.h header

    • Update the VendingMachineTask constructor to accept a coin slot reference (CoinSlot& coin_slot)

    • Add a coin_slot_ data member to VendingMachineTask

    • Add a coin_future_ member to store the future

    Hint
     1#include "coin_slot.h"
     2#include "pw_async2/context.h"
     3#include "pw_async2/poll.h"
     4#include "pw_async2/task.h"
     5
     6namespace codelab {
     7
     8// The main task that drives the vending machine.
     9class VendingMachineTask : public pw::async2::Task {
    10 public:
    11  VendingMachineTask(CoinSlot& coin_slot)
    12      : pw::async2::Task(PW_ASYNC_TASK_NAME("VendingMachineTask")),
    13        coin_slot_(coin_slot) {}
    14
    15 private:
    16  // This is the core of the asynchronous task. The dispatcher calls this method
    17  // to give the task a chance to do work.
    18  pw::async2::Poll<> DoPend(pw::async2::Context& cx) override;
    19
    20  CoinSlot& coin_slot_;
    21  CoinFuture coin_future_;
    22};
    23
    24}  // namespace codelab
    
  3. Update main.cc:

    • When creating your VendingMachineTask instance, pass the coin slot as an arg

    We’ve already provided a global CoinSlot instance (coin_slot) at the top of main.cc. You don’t need to create one.

    Hint
     1int main() {
     2  pw::async2::BasicDispatcher dispatcher;
     3  codelab::HardwareInit(&dispatcher);
     4
     5  codelab::VendingMachineTask task(coin_slot);
     6  dispatcher.Post(task);
     7
     8  dispatcher.RunToCompletion();
     9
    10  return 0;
    11}
    
  4. Update the DoPend() implementation in vending_machine.cc to interact with the coin slot:

    • Prompt the user to insert a coin:

      PW_LOG_INFO("Please insert a coin.");
      
    • Obtain a future from the CoinSlot into coin_future_.

    • Use coin_future_.Pend(cx) to wait for coin insertion

    • Handle the pending case of coin_future_.Pend(cx)

    • If coin_future_.Pend(cx) is ready, log the number of coins that were detected

    Recall that CoinFuture::Pend returns Poll<unsigned> indicating the status of the coin slot:

    • If Poll() returns Pending(), it means that no coin has been inserted yet. Your task cannot proceed without payment, so it must signal this to the dispatcher by returning Pending() itself. Futures like CoinFuture which wait for data will automatically wake your waiting task once that data becomes available.

    • If the Poll is Ready(), it means that coins have been inserted. The Poll object now contains the number of coins. Your task can get this value and proceed to the next step.

    Hint
     1pw::async2::Poll<> VendingMachineTask::DoPend(pw::async2::Context& cx) {
     2  PW_LOG_INFO("Welcome to the Pigweed Vending Machine!");
     3  PW_LOG_INFO("Please insert a coin.");
     4
     5  coin_future_ = coin_slot_.GetCoins();
     6
     7  pw::async2::Poll<unsigned> poll_result = coin_future_.Pend(cx);
     8  if (poll_result.IsPending()) {
     9    return pw::async2::Pending();
    10  }
    11  unsigned coins = poll_result.value();
    12  PW_LOG_INFO(
    13      "Received %u coin%s. Dispensing item.", coins, coins > 1 ? "s" : "");
    14  return pw::async2::Ready();
    15}
    16
    17}  // namespace codelab
    

Spot the issue#

There’s a problem with the current implementation…

  1. Run your vending machine app again:

    bazelisk run //pw_async2/codelab
    

    You should see the welcome message, followed by the prompt to insert coins.

    INF  Welcome to the Pigweed Vending Machine!
    INF  Please insert a coin.
    
  2. To simulate inserting a coin, type c Enter (type c and then type Enter):

    The hardware thread will call the coin slot’s Interrupt Service Routine (ISR), which wakes up your task. The dispatcher will run the task again, but instead of seeing the success message, you’ll see a duplicate welcome message followed by a crash!

    INF  Welcome to the Pigweed Vending Machine!
    INF  Please insert a coin.
    c
    INF  Welcome to the Pigweed Vending Machine!
    INF  Please insert a coin.
       ▄████▄      ██▀███      ▄▄▄           ██████     ██░ ██
      ▒██▀ ▀█     ▓██ ▒ ██▒   ▒████▄       ▒██    ▒    ▓██░ ██▒
      ▒▓█ 💥 ▄    ▓██ ░▄█ ▒   ▒██  ▀█▄     ░ ▓██▄      ▒██▀▀██░
      ▒▓▓▄ ▄██▒   ▒██▀▀█▄     ░██▄▄▄▄██      ▒   ██▒   ░▓█ ░██
      ▒ ▓███▀ ░   ░██▓ ▒██▒    ▓█   ▓██▒   ▒██████▒▒   ░▓█▒░██▓
      ░ ░▒ ▒  ░   ░ ▒▓ ░▒▓░    ▒▒   ▓▒█░   ▒ ▒▓▒ ▒ ░    ▒ ░░▒░▒
        ░  ▒        ░▒ ░ ▒░     ▒   ▒▒ ░   ░ ░▒  ░ ░    ▒ ░▒░ ░
      ░             ░░   ░      ░   ▒      ░  ░  ░      ░  ░░ ░
      ░ ░            ░              ░  ░         ░      ░  ░  ░
      ░
    
    pw_async2/codelab/coin_slot.cc:27: PW_CHECK() or PW_DCHECK() FAILED!
    
      FAILED ASSERTION
    
        current_future_ == nullptr
    
      FILE & LINE
    
        pw_async2/codelab/coin_slot.cc:27
    
      FUNCTION
    
        CoinFuture codelab::CoinSlot::GetCoins()
    
      MESSAGE
    
        Called GetCoins() while a CoinFuture is already active
    

    What happened?

    Answer

    When a task is suspended and resumed, its DoPend() method is called again from the beginning. The first time DoPend() ran, it obtained a future from GetCoins() and then returned Pending().

    When the coin was inserted, the task was woken up and the dispatcher called DoPend() again from the top. It printed the welcome message again, then tried to call GetCoins() a second time. However, CoinSlot enforces that only one future can be active at a time, so it asserted and crashed.

    This demonstrates a critical concept of asynchronous programming: tasks must manage their own state.

Manage the future state#

Because a task can be suspended and resumed at any Pending() return, you need a way to remember where you left off. All futures have an is_pendable() method that can help us determining whether the future has been initialized and can be polled.

  1. Update vending_machine.cc:

    • Gate the welcome message and GetCoins() call in DoPend() so they only run if !coin_future_.is_pendable().

    Hint
    1pw::async2::Poll<> VendingMachineTask::DoPend(pw::async2::Context& cx) {
    2  if (!coin_future_.is_pendable()) {
    3    PW_LOG_INFO("Welcome to the Pigweed Vending Machine!");
    4    PW_LOG_INFO("Please insert a coin.");
    5    coin_future_ = coin_slot_.GetCoins();
    6  }
    

Verify the fix#

With this guard in place, the vending machine task will only obtain a future once and no longer crash.

  1. Run the app again:

    bazelisk run //pw_async2/codelab
    
  2. Simulate inserting a coin by pressing c Enter.

    INF  Welcome to the Pigweed Vending Machine!
    INF  Please insert a coin.
    c
    INF  Received 1 coin. Dispensing item.
    

Reduce boilerplate#

The pattern of polling a future and returning Pending() if it’s not ready is common in pw_async2. To reduce this boilerplate, pw_async2 provides the PW_TRY_READY_ASSIGN macro to simplify writing clean async code.

  1. Refactor the DoPend() implementation in vending_machine.cc:

    • Replace the code that you wrote in the last step with a PW_TRY_READY_ASSIGN implementation that handles both the ready and pending scenarios.

      1. If the future returns Pending(), the macro immediately returns Pending() from the current function (your DoPend). This propagates the “sleeping” state up to the dispatcher.

      2. If the future returns Ready(some_value), the macro unwraps the value and assigns it to a variable you specify. The task then continues executing.

    For those familiar with async/await in other languages like Rust or Python, this macro serves a similar purpose to the await keyword. It’s the point at which your task can be suspended.

    Hint
     1pw::async2::Poll<> VendingMachineTask::DoPend(pw::async2::Context& cx) {
     2  if (!coin_future_.is_pendable()) {
     3    PW_LOG_INFO("Welcome to the Pigweed Vending Machine!");
     4    PW_LOG_INFO("Please insert a coin.");
     5    coin_future_ = coin_slot_.GetCoins();
     6  }
     7
     8  PW_TRY_READY_ASSIGN(unsigned coins, coin_future_.Pend(cx));
     9  PW_LOG_INFO(
    10      "Received %u coin%s. Dispensing item.", coins, coins > 1 ? "s" : "");
    11  return pw::async2::Ready();
    12}
    13
    14}  // namespace codelab
    

Next steps#

You’ve now implemented a task that waits for an asynchronous event and correctly manages its state. In 3. Create and wake a Future, you’ll learn how to write your own futures.

Checkpoint#

At this point, your code should look similar to the files below.

#include "coin_slot.h"
#include "hardware.h"
#include "pw_async2/basic_dispatcher.h"
#include "vending_machine.h"

namespace {

codelab::CoinSlot coin_slot;

}  // namespace

// Interrupt handler function invoked when the user inserts a coin into the
// vending machine.
void coin_inserted_isr() { coin_slot.Deposit(); }

// Interrupt handler function invoked when the user presses a key on the
// machine's keypad. Receives the value of the pressed key (0-9).
void key_press_isr(int key) {
  // In Step 3, implement your keypad handler here.
}

// Interrupt handler function invoked to simulate the item drop detector
// detecting confirmation that an item was successfully dispensed from the
// machine.
void item_drop_sensor_isr() {
  // In Step 5 you will uses this as part of a new Dispense task that runs
  // the dispenser motor until an item drops, or you time out on the vend
  // operation.
}

int main() {
  pw::async2::BasicDispatcher dispatcher;
  codelab::HardwareInit(&dispatcher);

  codelab::VendingMachineTask task(coin_slot);
  dispatcher.Post(task);

  dispatcher.RunToCompletion();

  return 0;
}
#include "vending_machine.h"

#include "pw_async2/try.h"
#include "pw_log/log.h"

namespace codelab {

pw::async2::Poll<> VendingMachineTask::DoPend(pw::async2::Context& cx) {
  if (!coin_future_.is_pendable()) {
    PW_LOG_INFO("Welcome to the Pigweed Vending Machine!");
    PW_LOG_INFO("Please insert a coin.");
    coin_future_ = coin_slot_.GetCoins();
  }

  PW_TRY_READY_ASSIGN(unsigned coins, coin_future_.Pend(cx));
  PW_LOG_INFO(
      "Received %u coin%s. Dispensing item.", coins, coins > 1 ? "s" : "");
  return pw::async2::Ready();
}

}  // namespace codelab
#pragma once

#include "coin_slot.h"
#include "pw_async2/context.h"
#include "pw_async2/poll.h"
#include "pw_async2/task.h"

namespace codelab {

// The main task that drives the vending machine.
class VendingMachineTask : public pw::async2::Task {
 public:
  VendingMachineTask(CoinSlot& coin_slot)
      : pw::async2::Task(PW_ASYNC_TASK_NAME("VendingMachineTask")),
        coin_slot_(coin_slot) {}

 private:
  // This is the core of the asynchronous task. The dispatcher calls this method
  // to give the task a chance to do work.
  pw::async2::Poll<> DoPend(pw::async2::Context& cx) override;

  CoinSlot& coin_slot_;
  CoinFuture coin_future_;
};

}  // namespace codelab