3. Create and wake a pendable function#

pw_async2: Cooperative async tasks for embedded

Currently, your vending machine immediately dispenses an item after receiving coins. In this step, you modify your app to let customers choose what to buy. Along the way you will learn how to correctly wake up a task that is waiting for input like this. You will also gain some experience implementing a Pend() function yourself.

Stub the Keypad class#

The hardware simulator sends you keypad events via async calls to key_press_isr(). The simulator sends integer values between 0 and 9 that indicate which keypad button was pressed. Your mission is to process these keypad events safely and to allow your task to wait for keypad numbers after receiving coins.

  1. Use the keypad in main.cc:

    • Declare a global keypad instance

    • Update key_press_isr() to handle keypad input events by invoking keypad.Press(key)

    • Pass a reference to the keypad when creating your task instance

    Hint
     1namespace {
     2
     3codelab::CoinSlot coin_slot;
     4codelab::Keypad keypad;
     5
     6}  // namespace
     7
     8// Interrupt handler function invoked when the user inserts a coin into the
     9// vending machine.
    10void coin_inserted_isr() { coin_slot.Deposit(); }
    11
    12// Interrupt handler function invoked when the user presses a key on the
    13// machine's keypad. Receives the value of the pressed key (0-9).
    14void key_press_isr(int key) { keypad.Press(key); }
    15
    16// Interrupt handler function invoked to simulate the item drop detector
    17// detecting confirmation that an item was successfully dispensed from the
    18// machine.
    19void item_drop_sensor_isr() {
    20  // In Step 5 you will uses this as part of a new Dispense task that runs
    21  // the dispenser motor until an item drops, or you time out on the vend
    22  // operation.
    23}
    24
    25int main() {
    26  pw::async2::Dispatcher dispatcher;
    27  codelab::HardwareInit(&dispatcher);
    28
    29  codelab::VendingMachineTask task(coin_slot, keypad);
    30  dispatcher.Post(task);
    31
    32  dispatcher.RunToCompletion();
    33
    34  return 0;
    35}
    
  2. Set up the keypad class in vending_machine.h (as well as a data member for tracking how many coins have been inserted):

    • Declare the stub Keypad class:

       1class Keypad {
       2 public:
       3  constexpr Keypad() : key_pressed_(kNone) {}
       4
       5  // Pends until a key has been pressed, returning the key number.
       6  //
       7  // May only be called by one task.
       8  pw::async2::Poll<int> Pend(pw::async2::Context& cx);
       9
      10  // Record a key press. Typically called from the keypad ISR.
      11  void Press(int key);
      12
      13 private:
      14  // A special internal value to indicate no keypad button has yet been
      15  // pressed.
      16  static constexpr int kNone = -1;
      17
      18  int key_pressed_;
      19};
      
    • Set up a private keypad_ member for VendingMachineTask

    • Add a Keypad& argument to the VendingMachineTask constructor parameters and use it to initialize the keypad_ member

    • Set up an unsigned coins_inserted_ data member in VendingMachineTask that tracks how many coins have been inserted and initialize coins_inserted_ to 0 in the constructor

    Hint
     1namespace codelab {
     2
     3class Keypad {
     4 public:
     5  constexpr Keypad() : key_pressed_(kNone) {}
     6
     7  // Pends until a key has been pressed, returning the key number.
     8  //
     9  // May only be called by one task.
    10  pw::async2::Poll<int> Pend(pw::async2::Context& cx);
    11
    12  // Record a key press. Typically called from the keypad ISR.
    13  void Press(int key);
    14
    15 private:
    16  // A special internal value to indicate no keypad button has yet been
    17  // pressed.
    18  static constexpr int kNone = -1;
    19
    20  int key_pressed_;
    21};
    22
    23// The main task that drives the vending machine.
    24class VendingMachineTask : public pw::async2::Task {
    25 public:
    26  VendingMachineTask(CoinSlot& coin_slot, Keypad& keypad)
    27      : pw::async2::Task(PW_ASYNC_TASK_NAME("VendingMachineTask")),
    28        coin_slot_(coin_slot),
    29        displayed_welcome_message_(false),
    30        keypad_(keypad),
    31        coins_inserted_(0) {}
    32
    33 private:
    34  // This is the core of the asynchronous task. The dispatcher calls this method
    35  // to give the task a chance to do work.
    36  pw::async2::Poll<> DoPend(pw::async2::Context& cx) override;
    37
    38  CoinSlot& coin_slot_;
    39  bool displayed_welcome_message_;
    40  Keypad& keypad_;
    41  unsigned coins_inserted_;
    42};
    43
    44}  // namespace codelab
    
  3. Create a stub implementation and integrate it (as well as the coin count data member) in vending_machine.cc:

    • Create a stub Keypad implementation:

      1pw::async2::Poll<int> Keypad::Pend(pw::async2::Context& cx) {
      2  return key_pressed_;
      3}
      4
      5void Keypad::Press(int key) {}
      

      Notice how the Pend member function just immediately returns the value of key_pressed_, which is only ever set to kNone (-1). We will fix that later.

    • Update VendingMachineTask::DoPend() to check if coins have already been inserted before it pends for coins (coin_slot_.Pend(cx))

    • Wait for keypad input by invoking keypad_.Pend(cx)) after coins have been inserted

    Hint
     1pw::async2::Poll<int> Keypad::Pend(pw::async2::Context& cx) {
     2  return key_pressed_;
     3}
     4
     5void Keypad::Press(int key) {}
     6
     7pw::async2::Poll<> VendingMachineTask::DoPend(pw::async2::Context& cx) {
     8  if (!displayed_welcome_message_) {
     9    PW_LOG_INFO("Welcome to the Pigweed Vending Machine!");
    10    PW_LOG_INFO("Please insert a coin.");
    11    displayed_welcome_message_ = true;
    12  }
    13
    14  if (coins_inserted_ == 0) {
    15    PW_TRY_READY_ASSIGN(unsigned coins, coin_slot_.Pend(cx));
    16    PW_LOG_INFO("Received %u coin%s.", coins, coins > 1 ? "s" : "");
    17    PW_LOG_INFO("Please press a keypad key.");
    18    coins_inserted_ += coins;
    19  }
    20
    21  PW_TRY_READY_ASSIGN(int key, keypad_.Pend(cx));
    22  PW_LOG_INFO("Keypad %d was pressed. Dispensing an item.", key);
    23
    24  return pw::async2::Ready();
    25}
    

Verify the stub implementation#

  1. Run the app:

    bazelisk run //pw_async2/codelab
    
  2. Press c Enter to insert a coin.

    You should see a log stating that -1 was pressed. This is expected since the KeyPad::Pend() stub implementation returns key_pressed_, which was initialized to kNone (-1).

    INF  Welcome to the Pigweed Vending Machine!
    INF  Please insert a coin.
    c
    INF  Received 1 coin.
    INF  Please press a keypad key.
    INF  Keypad -1 was pressed. Dispensing an item.
    

Handle keypad events#

Now, let’s update the Keypad implementation to actually handle key presses.

  1. Protect keypad data access in vending_machine.h:

    • Include the pw_sync/interrupt_spin_lock.h and pw_sync/lock_annotations.h headers

    • Add a pw::sync::InterruptSpinLock lock_ private member to Keypad

    • Guard key_pressed_ with the spin lock with PW_GUARDED_BY

    Since the keypad ISR is asynchronous, you’ll need to synchronize access to the stored event data. For this codelab, we use pw::sync::InterruptSpinLock which is safe to acquire from an ISR in production use. Alternatively you can use atomic operations.

    We use PW_GUARDED_BY to add a compile-time check to ensure that the protected key press data is only accessed when the spin lock is held.

    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#include "pw_sync/interrupt_spin_lock.h"
     6#include "pw_sync/lock_annotations.h"
     7
     8namespace codelab {
     9
    10class Keypad {
    11 public:
    12  constexpr Keypad() : key_pressed_(kNone) {}
    13
    14  // Pends until a key has been pressed, returning the key number.
    15  //
    16  // May only be called by one task.
    17  pw::async2::Poll<int> Pend(pw::async2::Context& cx);
    18
    19  // Record a key press. Typically called from the keypad ISR.
    20  void Press(int key);
    21
    22 private:
    23  // A special internal value to indicate no keypad button has yet been
    24  // pressed.
    25  static constexpr int kNone = -1;
    26
    27  pw::sync::InterruptSpinLock lock_;
    28  int key_pressed_ PW_GUARDED_BY(lock_);
    29};
    
  2. Implement keypress handling in vending_machine.cc:

    • In Keypad::Pend(), attempt to read the keypress data

    • In Keypad::Press(), store the keypress data

    Hint
     1pw::async2::Poll<int> Keypad::Pend(pw::async2::Context& cx) {
     2  std::lock_guard lock(lock_);
     3  int key = std::exchange(key_pressed_, kNone);
     4  if (key != kNone) {
     5    return key;
     6  }
     7  return pw::async2::Pending();
     8}
     9
    10void Keypad::Press(int key) {
    11  std::lock_guard lock(lock_);
    12  key_pressed_ = key;
    13}
    

    std::exchange ensures that the key_pressed_ data is read once by clearing it out to kNone (-1) after a read.

It’s so simple… what could go wrong?

Test the keypad implementation#

  1. Run the app:

    bazelisk run //pw_async2/codelab
    
  2. Press c Enter to insert a coin.

    INF  Welcome to the Pigweed Vending Machine!
    INF  Please insert a coin.
    c
    INF  Received 1 coin.
    INF  Please press a keypad key.
    
       ▄████▄      ██▀███      ▄▄▄           ██████     ██░ ██
      ▒██▀ ▀█     ▓██ ▒ ██▒   ▒████▄       ▒██    ▒    ▓██░ ██▒
      ▒▓█ 💥 ▄    ▓██ ░▄█ ▒   ▒██  ▀█▄     ░ ▓██▄      ▒██▀▀██░
      ▒▓▓▄ ▄██▒   ▒██▀▀█▄     ░██▄▄▄▄██      ▒   ██▒   ░▓█ ░██
      ▒ ▓███▀ ░   ░██▓ ▒██▒    ▓█   ▓██▒   ▒██████▒▒   ░▓█▒░██▓
      ░ ░▒ ▒  ░   ░ ▒▓ ░▒▓░    ▒▒   ▓▒█░   ▒ ▒▓▒ ▒ ░    ▒ ░░▒░▒
        ░  ▒        ░▒ ░ ▒░     ▒   ▒▒ ░   ░ ░▒  ░ ░    ▒ ░▒░ ░
      ░             ░░   ░      ░   ▒      ░  ░  ░      ░  ░░ ░
      ░ ░            ░              ░  ░         ░      ░  ░  ░
      ░
    
    pw_async2/dispatcher_base.cc:151: PW_CHECK() or PW_DCHECK() FAILED!
    
      FAILED ASSERTION
    
        !task->wakers_.empty()
    
      FILE & LINE
    
        pw_async2/dispatcher_base.cc:151
    
      FUNCTION
    
        NativeDispatcherBase::RunOneTaskResult pw::async2::NativeDispatcherBase::RunOneTask(Dispatcher &, Task *)
    
      MESSAGE
    
        Task 0x7ffd8ddc2f40 returned Pending() without registering a waker
    

    To prevent obviously incorrect usage, the pw_async2 module asserts if you return Pending() without actually storing a Waker, because it means your task has no way of being woken back up.

Store a waker#

In general, you should always store a Waker before returning Pending(). A waker is a lightweight object that allows you to tell the dispatcher to wake a task. When a Task::DoPend() call returns Pending(), the task is put to sleep so that the dispatcher doesn’t have to repeatedly poll the task. Without a waker, the task will sleep forever.

  1. Declare a waker in vending_machine.h:

    • Include the pw_async2/waker.h header

    • Add a pw::async2::Waker waker_ private member to the Keypad class

    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#include "pw_async2/waker.h"
     6#include "pw_sync/interrupt_spin_lock.h"
     7#include "pw_sync/lock_annotations.h"
     8
     9namespace codelab {
    10
    11class Keypad {
    12 public:
    13  constexpr Keypad() : key_pressed_(kNone) {}
    14
    15  // Pends until a key has been pressed, returning the key number.
    16  //
    17  // May only be called by one task.
    18  pw::async2::Poll<int> Pend(pw::async2::Context& cx);
    19
    20  // Record a key press. Typically called from the keypad ISR.
    21  void Press(int key);
    22
    23 private:
    24  // A special internal value to indicate no keypad button has yet been
    25  // pressed.
    26  static constexpr int kNone = -1;
    27
    28  pw::sync::InterruptSpinLock lock_;
    29  int key_pressed_ PW_GUARDED_BY(lock_);
    30  pw::async2::Waker waker_;  // No guard needed!
    31};
    
  2. Use the waker in vending_machine.cc:

    • In Keypad::Pend() store the waker before returning Pending():

      PW_ASYNC_STORE_WAKER(cx, waker_, "keypad press");
      

    The last argument should always be a meaningful string describing the wait reason. In the next section you’ll see how this string can help you debug issues.

    Hint
     1pw::async2::Poll<int> Keypad::Pend(pw::async2::Context& cx) {
     2  std::lock_guard lock(lock_);
     3  int key = std::exchange(key_pressed_, kNone);
     4  if (key != kNone) {
     5    return key;
     6  }
     7  PW_ASYNC_STORE_WAKER(cx, waker_, "keypad press");
     8  return pw::async2::Pending();
     9}
    10
    11void Keypad::Press(int key) {
    12  std::lock_guard lock(lock_);
    13  key_pressed_ = key;
    14}
    

Tip

See Setting up wakers for an overview of all of the different ways that you can set up wakers.

Forget to wake the task#

You’ve set up the waker but you’re not using it yet. Let’s see what happens.

  1. Run the app:

    bazelisk run //pw_async2/codelab
    
  2. Press c Enter to insert a coin.

  3. Press 1 Enter to select an item.

    You should see output like this:

    INF  Welcome to the Pigweed Vending Machine!
    INF  Please insert a coin.
    c
    INF  Received 1 coin.
    INF  Please press a keypad key.
    1
    

    Nothing happens, not even an assertion! This time there is no crash! Why??

    Answer

    The problem is that pw_async2 has no way of knowing when the task is ready to be woken up.

    The reason pw_async2 can detect forgetting to store the waker, on the other hand, is because it happens during a pw_async2-initiated call into your code, so there can be a postcondition check.

  4. Debug this issue by pressing d Enter:

    You should see output like this:

    d
    INF  pw::async2::Dispatcher
    INF  Woken tasks:
    INF  Sleeping tasks:
    INF    - VendingMachineTask:0x7ffeec48fd90 (1 wakers)
    INF      * Waker 1: keypad press
    

    This shows the state of all the tasks registered with the dispatcher. The last line is the wait reason string that you provided when you registered the waker. We can see that the vending machine task is still sleeping.

    Tip

    If you use pw_async2 in your own project, you can get this kind of debug information by calling LogRegisteredTasks.

    If you don’t see the reason messages, make sure that PW_ASYNC2_DEBUG_WAIT_REASON is not set to 0.

Wake the task#

  1. Fix the issue in vending_machine.cc:

    • Invoke the Wake() method on the Waker:

      1void Keypad::Press(int key) {
      2  std::lock_guard lock(lock_);
      3  key_pressed_ = key;
      4  std::move(waker_).Wake();
      5}
      

      By design, the Wake() call consumes the Waker. To invoke Wake() you must use std::move() to make it clear that the task can be only woken once through a given waker.

      Wakers are default-constructed in an empty state, and moving the value means the location that is moved from is reset to an empty state. If you invoke Wake() on an empty Waker, the call is a no-op.

  2. Verify the fix:

    bazelisk run //pw_async2/codelab
    
  3. Press c Enter to insert a coin.

  4. Press 1 Enter to select an item.

    You should see keypad input working correctly now:

    INF  Welcome to the Pigweed Vending Machine!
    INF  Please insert a coin.
    c
    INF  Received 1 coin.
    INF  Please press a keypad key.
    1
    INF  Keypad 1 was pressed. Dispensing an item.
    

Tip

You can also end up in a “task not waking up” state if you destroy or otherwise clear the Waker instance that pointed at the task to wake. LogRegisteredTasks() can also help here by pointing to a problem related to waking your task.

Next steps#

Continue to 4. Use a state machine to learn how to manage the rapidly increasing complexity of your code.

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;
codelab::Keypad keypad;

}  // 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) { keypad.Press(key); }

// 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, keypad);
  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<int> Keypad::Pend(pw::async2::Context& cx) {
  std::lock_guard lock(lock_);
  int key = std::exchange(key_pressed_, kNone);
  if (key != kNone) {
    return key;
  }
  PW_ASYNC_STORE_WAKER(cx, waker_, "keypad press");
  return pw::async2::Pending();
}

void Keypad::Press(int key) {
  std::lock_guard lock(lock_);
  key_pressed_ = key;
  std::move(waker_).Wake();
}

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;
  }

  if (coins_inserted_ == 0) {
    PW_TRY_READY_ASSIGN(unsigned coins, coin_slot_.Pend(cx));
    PW_LOG_INFO("Received %u coin%s.", coins, coins > 1 ? "s" : "");
    PW_LOG_INFO("Please press a keypad key.");
    coins_inserted_ += coins;
  }

  PW_TRY_READY_ASSIGN(int key, keypad_.Pend(cx));
  PW_LOG_INFO("Keypad %d was pressed. Dispensing an item.", key);

  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"
#include "pw_async2/waker.h"
#include "pw_sync/interrupt_spin_lock.h"
#include "pw_sync/lock_annotations.h"

namespace codelab {

class Keypad {
 public:
  constexpr Keypad() : key_pressed_(kNone) {}

  // Pends until a key has been pressed, returning the key number.
  //
  // May only be called by one task.
  pw::async2::Poll<int> Pend(pw::async2::Context& cx);

  // Record a key press. Typically called from the keypad ISR.
  void Press(int key);

 private:
  // A special internal value to indicate no keypad button has yet been
  // pressed.
  static constexpr int kNone = -1;

  pw::sync::InterruptSpinLock lock_;
  int key_pressed_ PW_GUARDED_BY(lock_);
  pw::async2::Waker waker_;  // No guard needed!
};

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

 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_;
  Keypad& keypad_;
  unsigned coins_inserted_;
};

}  // namespace codelab