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.
Use the keypad in
main.cc
:Declare a global
keypad
instanceUpdate
key_press_isr()
to handle keypad input events by invokingkeypad.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}
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 forVendingMachineTask
Add a
Keypad&
argument to theVendingMachineTask
constructor parameters and use it to initialize thekeypad_
memberSet up an
unsigned coins_inserted_
data member inVendingMachineTask
that tracks how many coins have been inserted and initializecoins_inserted_
to0
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
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 ofkey_pressed_
, which is only ever set tokNone
(-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#
Run the app:
bazelisk run //pw_async2/codelab
Press c Enter to insert a coin.
You should see a log stating that
-1
was pressed. This is expected since theKeyPad::Pend()
stub implementation returnskey_pressed_
, which was initialized tokNone
(-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.
Protect keypad data access in
vending_machine.h
:Include the
pw_sync/interrupt_spin_lock.h
andpw_sync/lock_annotations.h
headersAdd a
pw::sync::InterruptSpinLock lock_
private member toKeypad
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};
Implement keypress handling in
vending_machine.cc
:In
Keypad::Pend()
, attempt to read the keypress dataIn
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 thekey_pressed_
data is read once by clearing it out tokNone
(-1
) after a read.
It’s so simple… what could go wrong?
Test the keypad implementation#
Run the app:
bazelisk run //pw_async2/codelab
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 returnPending()
without actually storing aWaker
, 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.
Declare a waker in
vending_machine.h
:Include the
pw_async2/waker.h
headerAdd a
pw::async2::Waker waker_
private member to theKeypad
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};
Use the waker in
vending_machine.cc
:In
Keypad::Pend()
store the waker before returningPending()
: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.
Run the app:
bazelisk run //pw_async2/codelab
Press c Enter to insert a coin.
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 apw_async2
-initiated call into your code, so there can be a postcondition check.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#
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 theWaker
. To invokeWake()
you must usestd::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 emptyWaker
, the call is a no-op.
Verify the fix:
bazelisk run //pw_async2/codelab
Press c Enter to insert a coin.
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