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.
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 ofFuture. 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 aPend()method to progress them, which takes an async Context and returns aPollof 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 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
Dispatcherwon’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.
Update the
VendingMachineTaskdeclaration invending_machine.hto useCoinSlot:Include the
coin_slot.hheaderUpdate the
VendingMachineTaskconstructor to accept a coin slot reference (CoinSlot& coin_slot)Add a
coin_slot_data member toVendingMachineTaskAdd 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
Update
main.cc:When creating your
VendingMachineTaskinstance, pass the coin slot as an arg
We’ve already provided a global
CoinSlotinstance (coin_slot) at the top ofmain.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}
Update the
DoPend()implementation invending_machine.ccto interact with the coin slot:Prompt the user to insert a coin:
PW_LOG_INFO("Please insert a coin.");
Obtain a future from the
CoinSlotintocoin_future_.Use
coin_future_.Pend(cx)to wait for coin insertionHandle 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::PendreturnsPoll<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. Futures likeCoinFuturewhich wait for data will automatically wake your waiting task once that data becomes available.If the
PollisReady(), it means that coins have been inserted. ThePollobject 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…
Run your vending machine app again:
bazelisk run //pw_async2/codelabYou 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, 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 activeWhat happened?
Answer
When a task is suspended and resumed, its
DoPend()method is called again from the beginning. The first timeDoPend()ran, it obtained a future fromGetCoins()and then returnedPending().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 callGetCoins()a second time. However,CoinSlotenforces 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.
Update
vending_machine.cc:Gate the welcome message and
GetCoins()call inDoPend()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.
Run the app again:
bazelisk run //pw_async2/codelabSimulate 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.
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.
1. If the future returns
Pending(), the macro immediately returnsPending()from the current function (yourDoPend). 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/awaitin other languages like Rust or Python, this macro serves a similar purpose to theawaitkeyword. 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