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 are called pendable functions.

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 Pend() declaration in pw_async2/codelab/coin_slot.h.

    1  // Pends until the coins have been deposited. Returns the number of coins
    2  // received.
    3  //
    4  // May only be called by one task.
    5  pw::async2::Poll<unsigned> Pend(pw::async2::Context& context);
    

    Similar to DoPend(), the coin slot’s Pend() method is a pendable function that takes an async Context and returns a Poll of some value. When a task calls a pendable function, 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 the Pend() implementation uses something called a waker. You’ll learn more about wakers 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

    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};
    22
    23}  // 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::Dispatcher 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.");
      
    • Use coin_slot_.Pend(cx) to wait for coin insertion

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

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

    Recall that CoinSlot::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. Pendable functions like CoinSlot::Pend 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  pw::async2::Poll<unsigned> poll_result = coin_slot_.Pend(cx);
     5  if (poll_result.IsPending()) {
     6    return pw::async2::Pending();
     7  }
     8  unsigned coins = poll_result.value();
     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
    

Reduce boilerplate#

The pattern of polling a pendable function 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 function 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 function 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  PW_LOG_INFO("Welcome to the Pigweed Vending Machine!");
     3  PW_LOG_INFO("Please insert a coin.");
     4  PW_TRY_READY_ASSIGN(unsigned coins, coin_slot_.Pend(cx));
     5  PW_LOG_INFO(
     6      "Received %u coin%s. Dispensing item.", coins, coins > 1 ? "s" : "");
     7  return pw::async2::Ready();
     8}
     9
    10}  // 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, and you’ll see… an unexpected result:

    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.
    INF  Received 1 coin. Dispensing item.
    

    The welcome message was printed twice! Why?

    Answer

    When a task is suspended and resumed, its DoPend() method is called again from the beginning. The first time DoPend() ran, it printed the welcome message and then returned Pending() from inside the PW_TRY_READY_ASSIGN macro. When the coin was inserted, the task was woken up and the dispatcher called DoPend() again from the top. It printed the welcome message a second time, and then when it called coin_slot_.Pend(cx), the coin was available, so it returned Ready() and the task completed.

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

Manage the welcome state#

Because a task can be suspended and resumed at any Pending() return, you need a way to remember where you left off. For simple cases like this, a boolean flag is sufficient.

  1. Add the boolean flag in vending_machine.h:

    • Add a displayed_welcome_message_ data member to VendingMachineTask

    • Initialize displayed_welcome_message_ to false in the constructor

    Hint
     1class VendingMachineTask : public pw::async2::Task {
     2 public:
     3  VendingMachineTask(CoinSlot& coin_slot)
     4      : pw::async2::Task(PW_ASYNC_TASK_NAME("VendingMachineTask")),
     5        coin_slot_(coin_slot),
     6        displayed_welcome_message_(false) {}
     7
     8 private:
     9  // This is the core of the asynchronous task. The dispatcher calls this method
    10  // to give the task a chance to do work.
    11  pw::async2::Poll<> DoPend(pw::async2::Context& cx) override;
    12
    13  CoinSlot& coin_slot_;
    14  bool displayed_welcome_message_;
    15};
    
  2. Update vending_machine.cc:

    • Gate the welcome message and coin insertion prompt in DoPend() behind the boolean flag

    • Flip the flag after the welcome message and prompt have been printed

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

Verify the fix#

The welcome message should no longer get duplicated.

  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.
    

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 pendable function, you’ll learn how to write your own pendable functions.

Checkpoint#

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

#include "coin_slot.h"
#include "hardware.h"
#include "pw_async2/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::Dispatcher 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 (!displayed_welcome_message_) {
    PW_LOG_INFO("Welcome to the Pigweed Vending Machine!");
    PW_LOG_INFO("Please insert a coin.");
    displayed_welcome_message_ = true;
  }

  PW_TRY_READY_ASSIGN(unsigned coins, coin_slot_.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),
        displayed_welcome_message_(false) {}

 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_;
  bool displayed_welcome_message_;
};

}  // namespace codelab