aboutsummaryrefslogtreecommitdiff
path: root/examples
diff options
context:
space:
mode:
authorDario Nieuwenhuis <[email protected]>2024-08-13 16:34:46 +0000
committerGitHub <[email protected]>2024-08-13 16:34:46 +0000
commitc0d74e153e6fa406977ccad0ca64b8c90a65d76a (patch)
tree17c31fffa6afc93f8184681a895a65fcd1c500b8 /examples
parentafd910016cd185dd121a47c3d481e6148640f071 (diff)
parente05e5d33f0ab832b2ea1c48674c99b41581118be (diff)
Merge pull request #3216 from 1-rafael-1/rp-example-orchestrate-tasks
add example to rp: orchestrate multiple tasks
Diffstat (limited to 'examples')
-rw-r--r--examples/rp/src/bin/orchestrate_tasks.rs318
1 files changed, 318 insertions, 0 deletions
diff --git a/examples/rp/src/bin/orchestrate_tasks.rs b/examples/rp/src/bin/orchestrate_tasks.rs
new file mode 100644
index 000000000..0e21d5833
--- /dev/null
+++ b/examples/rp/src/bin/orchestrate_tasks.rs
@@ -0,0 +1,318 @@
1//! This example demonstrates some approaches to communicate between tasks in order to orchestrate the state of the system.
2//!
3//! We demonstrate how to:
4//! - use a channel to send messages between tasks, in this case here in order to have one task control the state of the system.
5//! - use a signal to terminate a task.
6//! - use command channels to send commands to another task.
7//! - use different ways to receive messages, from a straightforwar awaiting on one channel to a more complex awaiting on multiple futures.
8//!
9//! There are more patterns to orchestrate tasks, this is just one example.
10//!
11//! We will use these tasks to generate example "state information":
12//! - a task that generates random numbers in intervals of 60s
13//! - a task that generates random numbers in intervals of 30s
14//! - a task that generates random numbers in intervals of 90s
15//! - a task that notifies about being attached/disattached from usb power
16//! - a task that measures vsys voltage in intervals of 30s
17//! - a task that consumes the state information and reacts to it
18
19#![no_std]
20#![no_main]
21
22use assign_resources::assign_resources;
23use defmt::*;
24use embassy_executor::Spawner;
25use embassy_futures::select::{select, Either};
26use embassy_rp::adc::{Adc, Channel, Config, InterruptHandler};
27use embassy_rp::clocks::RoscRng;
28use embassy_rp::gpio::{Input, Pull};
29use embassy_rp::{bind_interrupts, peripherals};
30use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
31use embassy_sync::{channel, signal};
32use embassy_time::{Duration, Timer};
33use rand::RngCore;
34use {defmt_rtt as _, panic_probe as _};
35
36// This is just some preparation, see example `assign_resources.rs` for more information on this. We prep the rresources that we will be using in different tasks.
37// **Note**: This will not work with a board that has a wifi chip, because the wifi chip uses pins 24 and 29 for its own purposes. A way around this in software
38// is not trivial, at least if you intend to use wifi, too. Workaround is to wire from vsys and vbus pins to appropriate pins on the board through a voltage divider. Then use those pins.
39// For this example it will not matter much, the concept of what we are showing remains valid.
40assign_resources! {
41 vsys: Vsys {
42 adc: ADC,
43 pin_29: PIN_29,
44 },
45 vbus: Vbus {
46 pin_24: PIN_24,
47 },
48}
49
50bind_interrupts!(struct Irqs {
51 ADC_IRQ_FIFO => InterruptHandler;
52});
53
54/// This is the type of Events that we will send from the worker tasks to the orchestrating task.
55enum Events {
56 UsbPowered(bool),
57 VsysVoltage(f32),
58 FirstRandomSeed(u32),
59 SecondRandomSeed(u32),
60 ThirdRandomSeed(u32),
61 ResetFirstRandomSeed,
62}
63
64/// This is the type of Commands that we will send from the orchestrating task to the worker tasks.
65/// Note that we are lazy here and only have one command, you might want to have more.
66enum Commands {
67 /// This command will stop the appropriate worker task
68 Stop,
69}
70
71/// This is the state of the system, we will use this to orchestrate the system. This is a simple example, in a real world application this would be more complex.
72#[derive(Default, Debug, Clone, Format)]
73struct State {
74 usb_powered: bool,
75 vsys_voltage: f32,
76 first_random_seed: u32,
77 second_random_seed: u32,
78 third_random_seed: u32,
79 times_we_got_first_random_seed: u8,
80 maximum_times_we_want_first_random_seed: u8,
81}
82
83impl State {
84 fn new() -> Self {
85 Self {
86 usb_powered: false,
87 vsys_voltage: 0.0,
88 first_random_seed: 0,
89 second_random_seed: 0,
90 third_random_seed: 0,
91 times_we_got_first_random_seed: 0,
92 maximum_times_we_want_first_random_seed: 3,
93 }
94 }
95}
96
97/// Channel for the events that we want the orchestrator to react to, all state events are of the type Enum Events.
98/// We use a channel with an arbitrary size of 10, the precise size of the queue depends on your use case. This depends on how many events we
99/// expect to be generated in a given time frame and how fast the orchestrator can react to them. And then if we rather want the senders to wait for
100/// new slots in the queue or if we want the orchestrator to have a backlog of events to process. In this case here we expect to always be enough slots
101/// in the queue, so the worker tasks can in all nominal cases send their events and continue with their work without waiting.
102/// For the events we - in this case here - do not want to loose any events, so a channel is a good choice. See embassy_sync docs for other options.
103static EVENT_CHANNEL: channel::Channel<CriticalSectionRawMutex, Events, 10> = channel::Channel::new();
104
105/// Signal for stopping the first random signal task. We use a signal here, because we need no queue. It is suffiient to have one signal active.
106static STOP_FIRST_RANDOM_SIGNAL: signal::Signal<CriticalSectionRawMutex, Commands> = signal::Signal::new();
107
108/// Channel for the state that we want the consumer task to react to. We use a channel here, because we want to have a queue of state changes, although
109/// we want the queue to be of size 1, because we want to finish rwacting to the state change before the next one comes in. This is just a design choice
110/// and depends on your use case.
111static CONSUMER_CHANNEL: channel::Channel<CriticalSectionRawMutex, State, 1> = channel::Channel::new();
112
113// And now we can put all this into use
114
115/// This is the main task, that will not do very much besides spawning the other tasks. This is a design choice, you could do the
116/// orchestrating here. This is to show that we do not need a main loop here, the system will run indefinitely as long as at least one task is running.
117#[embassy_executor::main]
118async fn main(spawner: Spawner) {
119 // initialize the peripherals
120 let p = embassy_rp::init(Default::default());
121 // split the resources, for convenience - see above
122 let r = split_resources! {p};
123
124 // spawn the tasks
125 spawner.spawn(orchestrate(spawner)).unwrap();
126 spawner.spawn(random_60s(spawner)).unwrap();
127 spawner.spawn(random_90s(spawner)).unwrap();
128 spawner.spawn(usb_power(spawner, r.vbus)).unwrap();
129 spawner.spawn(vsys_voltage(spawner, r.vsys)).unwrap();
130 spawner.spawn(consumer(spawner)).unwrap();
131}
132
133/// This is the task handling the system state and orchestrating the other tasks. WEe can regard this as the "main loop" of the system.
134#[embassy_executor::task]
135async fn orchestrate(_spawner: Spawner) {
136 let mut state = State::new();
137
138 // we need to have a receiver for the events
139 let receiver = EVENT_CHANNEL.receiver();
140
141 // and we need a sender for the consumer task
142 let state_sender = CONSUMER_CHANNEL.sender();
143
144 loop {
145 // we await on the receiver, this will block until a new event is available
146 // as an alternative to this, we could also await on multiple channels, this would block until at least one of the channels has an event
147 // see the embassy_futures docs: https://docs.embassy.dev/embassy-futures/git/default/select/index.html
148 // The task random_30s does a select, if you want to have a look at that.
149 // Another reason to use select may also be that we want to have a timeout, so we can react to the absence of events within a time frame.
150 // We keep it simple here.
151 let event = receiver.receive().await;
152
153 // react to the events
154 match event {
155 Events::UsbPowered(usb_powered) => {
156 // update the state and/or react to the event here
157 state.usb_powered = usb_powered;
158 info!("Usb powered: {}", usb_powered);
159 }
160 Events::VsysVoltage(voltage) => {
161 // update the state and/or react to the event here
162 state.vsys_voltage = voltage;
163 info!("Vsys voltage: {}", voltage);
164 }
165 Events::FirstRandomSeed(seed) => {
166 // update the state and/or react to the event here
167 state.first_random_seed = seed;
168 // here we change some meta state, we count how many times we got the first random seed
169 state.times_we_got_first_random_seed += 1;
170 info!(
171 "First random seed: {}, and that was iteration {} of receiving this.",
172 seed, &state.times_we_got_first_random_seed
173 );
174 }
175 Events::SecondRandomSeed(seed) => {
176 // update the state and/or react to the event here
177 state.second_random_seed = seed;
178 info!("Second random seed: {}", seed);
179 }
180 Events::ThirdRandomSeed(seed) => {
181 // update the state and/or react to the event here
182 state.third_random_seed = seed;
183 info!("Third random seed: {}", seed);
184 }
185 Events::ResetFirstRandomSeed => {
186 // update the state and/or react to the event here
187 state.times_we_got_first_random_seed = 0;
188 state.first_random_seed = 0;
189 info!("Resetting the first random seed counter");
190 }
191 }
192 // we now have an altered state
193 // there is a crate for detecting field changes on crates.io (https://crates.io/crates/fieldset) that might be useful here
194 // for now we just keep it simple
195
196 // we send the state to the consumer task
197 // since the channel has a size of 1, this will block until the consumer task has received the state, which is what we want here in this example
198 // **Note:** It is bad design to send too much data between tasks, with no clear definition of what "too much" is. In this example we send the
199 // whole state, in a real world application you might want to send only the data, that is relevant to the consumer task AND only when it has changed.
200 // We keep it simple here.
201 state_sender.send(state.clone()).await;
202 }
203}
204
205/// This task will consume the state information and react to it. This is a simple example, in a real world application this would be more complex
206/// and we could have multiple consumer tasks, each reacting to different parts of the state.
207#[embassy_executor::task]
208async fn consumer(spawner: Spawner) {
209 // we need to have a receiver for the state
210 let receiver = CONSUMER_CHANNEL.receiver();
211 let sender = EVENT_CHANNEL.sender();
212 loop {
213 // we await on the receiver, this will block until a new state is available
214 let state = receiver.receive().await;
215 // react to the state, in this case here we just log it
216 info!("The consumer has reveived this state: {:?}", &state);
217
218 // here we react to the state, in this case here we want to start or stop the first random signal task depending on the state of the system
219 match state.times_we_got_first_random_seed {
220 max if max == state.maximum_times_we_want_first_random_seed => {
221 info!("Stopping the first random signal task");
222 // we send a command to the task
223 STOP_FIRST_RANDOM_SIGNAL.signal(Commands::Stop);
224 // we notify the orchestrator that we have sent the command
225 sender.send(Events::ResetFirstRandomSeed).await;
226 }
227 0 => {
228 // we start the task, which presents us with an interesting problem, because we may return here before the task has started
229 // here we just try and log if the task has started, in a real world application you might want to handle this more gracefully
230 info!("Starting the first random signal task");
231 match spawner.spawn(random_30s(spawner)) {
232 Ok(_) => info!("Successfully spawned random_30s task"),
233 Err(e) => info!("Failed to spawn random_30s task: {:?}", e),
234 }
235 }
236 _ => {}
237 }
238 }
239}
240
241/// This task will generate random numbers in intervals of 30s
242/// The task will terminate after it has received a command signal to stop, see the orchestrate task for that.
243/// Note that we are not spawning this task from main, as we will show how such a task can be spawned and closed dynamically.
244#[embassy_executor::task]
245async fn random_30s(_spawner: Spawner) {
246 let mut rng = RoscRng;
247 let sender = EVENT_CHANNEL.sender();
248 loop {
249 // we either await on the timer or the signal, whichever comes first.
250 let futures = select(Timer::after(Duration::from_secs(30)), STOP_FIRST_RANDOM_SIGNAL.wait()).await;
251 match futures {
252 Either::First(_) => {
253 // we received are operating on the timer
254 info!("30s are up, generating random number");
255 let random_number = rng.next_u32();
256 sender.send(Events::FirstRandomSeed(random_number)).await;
257 }
258 Either::Second(_) => {
259 // we received the signal to stop
260 info!("Received signal to stop, goodbye!");
261 break;
262 }
263 }
264 }
265}
266
267/// This task will generate random numbers in intervals of 60s
268#[embassy_executor::task]
269async fn random_60s(_spawner: Spawner) {
270 let mut rng = RoscRng;
271 let sender = EVENT_CHANNEL.sender();
272 loop {
273 Timer::after(Duration::from_secs(60)).await;
274 let random_number = rng.next_u32();
275 sender.send(Events::SecondRandomSeed(random_number)).await;
276 }
277}
278
279/// This task will generate random numbers in intervals of 90s
280#[embassy_executor::task]
281async fn random_90s(_spawner: Spawner) {
282 let mut rng = RoscRng;
283 let sender = EVENT_CHANNEL.sender();
284 loop {
285 Timer::after(Duration::from_secs(90)).await;
286 let random_number = rng.next_u32();
287 sender.send(Events::ThirdRandomSeed(random_number)).await;
288 }
289}
290
291/// This task will notify if we are connected to usb power
292#[embassy_executor::task]
293pub async fn usb_power(_spawner: Spawner, r: Vbus) {
294 let mut vbus_in = Input::new(r.pin_24, Pull::None);
295 let sender = EVENT_CHANNEL.sender();
296 loop {
297 sender.send(Events::UsbPowered(vbus_in.is_high())).await;
298 vbus_in.wait_for_any_edge().await;
299 }
300}
301
302/// This task will measure the vsys voltage in intervals of 30s
303#[embassy_executor::task]
304pub async fn vsys_voltage(_spawner: Spawner, r: Vsys) {
305 let mut adc = Adc::new(r.adc, Irqs, Config::default());
306 let vsys_in = r.pin_29;
307 let mut channel = Channel::new_pin(vsys_in, Pull::None);
308 let sender = EVENT_CHANNEL.sender();
309 loop {
310 // read the adc value
311 let adc_value = adc.read(&mut channel).await.unwrap();
312 // convert the adc value to voltage.
313 // 3.3 is the reference voltage, 3.0 is the factor for the inbuilt voltage divider and 4096 is the resolution of the adc
314 let voltage = (adc_value as f32) * 3.3 * 3.0 / 4096.0;
315 sender.send(Events::VsysVoltage(voltage)).await;
316 Timer::after(Duration::from_secs(30)).await;
317 }
318}