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.
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’sPend()
method is a pendable function that takes an async Context and returns aPoll
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 thevalue
.If it’s
Pending()
, the operation is not yet complete. The task will generally stop and returnPending()
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.Update the
VendingMachineTask
declaration invending_machine.h
to useCoinSlot
:Include the
coin_slot.h
headerUpdate the
VendingMachineTask
constructor to accept a coin slot reference (CoinSlot& coin_slot
)Add a
coin_slot_
data member toVendingMachineTask
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
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 ofmain.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}
Update the
DoPend()
implementation invending_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 insertionHandle 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
returnsPoll<unsigned>
indicating the status of the coin slot:If
Poll()
returnsPending()
, it means that no coin has been inserted yet. Your task cannot proceed without payment, so it must signal this to the dispatcher by returningPending()
itself. Pendable functions likeCoinSlot::Pend
which wait for data will automatically wake your waiting task once that data becomes available.If the
Poll
isReady()
, it means that coins have been inserted. ThePoll
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.
Refactor the
DoPend()
implementation invending_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.
If the function returns
Pending()
, the macro immediately returnsPending()
from the current function (yourDoPend
). This propagates the “sleeping” state up to the dispatcher.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 theawait
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…
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.
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 timeDoPend()
ran, it printed the welcome message and then returnedPending()
from inside thePW_TRY_READY_ASSIGN
macro. When the coin was inserted, the task was woken up and the dispatcher calledDoPend()
again from the top. It printed the welcome message a second time, and then when it calledcoin_slot_.Pend(cx)
, the coin was available, so it returnedReady()
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.
Add the boolean flag in
vending_machine.h
:Add a
displayed_welcome_message_
data member toVendingMachineTask
Initialize
displayed_welcome_message_
tofalse
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};
Update
vending_machine.cc
:Gate the welcome message and coin insertion prompt in
DoPend()
behind the boolean flagFlip 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.
Run the app again:
bazelisk run //pw_async2/codelab
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