-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathORTOS_tutorial.ino
More file actions
267 lines (216 loc) · 9.16 KB
/
ORTOS_tutorial.ino
File metadata and controls
267 lines (216 loc) · 9.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
#include <ObjectiveRTOS.h>
void setup()
{
configure_timer();
Serial.begin(115200);
pinMode(13, OUTPUT);
}
void loop()
{
//nothing to do
}
// ======================================================
// CONFIGURATION
// ======================================================
/*
In order for the scheduler to work, it must be called cyclically at a fixed time interval.
This time interval is called a tick. For this example, it is assumed that one tick lasts 1 ms.
*/
// Therefore, an interrupt handler will be defined that triggers the scheduling process at a fixed time interval.
ISR(TIMER2_COMPA_vect)
{
ORTOS_next_tick();
}
// Configuration of timer 2, which will cyclically generate interrupts every 1 ms. This function is called in setup().
void configure_timer()
{
cli();
TCCR2A = (1 << WGM21);
TCCR2B = (1 << CS22);
OCR2A = 249;
TIMSK2 = (1 << OCIE2A);
sei();
}
/*
Declaration that within one second there are 1000 scheduler ticks.
This information is used by macros that convert time into scheduler ticks,
such as SECONDS(), MILLISECONDS(), MICROSECONDS(), so that time can be
correctly converted into the number of ticks executed by the scheduler.
*/
#define SCHEDULER_TICKS_PER_SECOND 1000
// ======================================================
// EXAMPLES OF TASK TYPES
// ======================================================
// Example 1 - using rerun_task() – the most memory-efficient approach:
/*
The process of interrupting and switching to another task is very resource-intensive,
because it requires saving the execution context of the current task
(register contents and stack). For the AVR ATmega328p microcontroller,
2 KB of RAM is a very small amount, so it is worth saving every possible byte of memory.
The simplest approach is to eliminate saving the task context as much as possible.
This is the idea behind the object-oriented design of this library.
All local variables of the function are packed and moved into a class.
As a result, exactly as much memory is reserved as the task actually needs,
and the scheduler does not have to guess which registers contain local variables.
*/
/*
In this example, a clock will be implemented that counts the number of seconds
since the microcontroller was started. To create a new task object,
the base class must inherit from the runnable class.
The abstract runnable class serves as an interface for the scheduler,
allowing it to call the run() method.
*/
class cyclic_task_example : public runnable
{
public:
int time;
// Constructor
cyclic_task_example()
{
/*
The constructor must first initialize its variables and only then
request the scheduler to start the task by calling start_task().
Otherwise, the scheduler might call run() before the constructor finishes.
*/
time = 0;
/*
start_task() must always be called at the end of the constructor.
This function takes a delay parameter that specifies after how many ticks
the scheduler should run the run() method.
If this function is not used in the constructor, the created object
will never start working automatically.
*/
start_task();
}
// This task will count the time in seconds from the microcontroller startup
void run()
{
Serial.print(F("TASK 1: Current time: "));
Serial.println(time++);
/*
rerun_task() informs the scheduler that the task should be executed again.
The parameter specifies after how much time the run() method should be called again.
*/
rerun_task(SECONDS(1));
/*
Calling rerun_task() stops further execution of the task.
The task will now wait to be restarted by the scheduler,
where it will begin execution from the beginning.
*/
}
};
// Creating an instance of the class is enough to start the task
cyclic_task_example task1;
// Example 2 - using delay_task() – more convenient.
/*
The first example showed how to use rerun_task() to create cyclic tasks.
Unfortunately, this approach is not always convenient, especially for tasks
that require sequential execution. For this reason, the delay_task() function
was also implemented. This function saves the entire task context (local variables, etc.)
as well as the location in the program where the task was paused.
After the delay expires, execution resumes from that exact location.
While the task is suspended, at least 22 bytes of memory are reserved,
plus optionally a stack of the size used by the task.
*/
/*
It is also possible to create tasks using the ready-made task class.
To create such a task, a pointer to a function must be passed to the constructor,
along with an optional delay before the first execution.
*/
void sequential_task_example()
{
Serial.print(F("TASK 2: "));
delay_task(MILLISECONDS(100));
Serial.print(F("Hello"));
delay_task(MILLISECONDS(100));
Serial.print(F(" World"));
delay_task(MILLISECONDS(100));
Serial.print(F("!!"));
delay_task(MILLISECONDS(100));
Serial.print(F("!\n"));
delay_task(MILLISECONDS(500));
delay_task(SECONDS(2));
// Example 3 - dynamic task creation
/*
This example shows how to create a task using the new operator.
This can be useful if task A wants to delegate part of its work to task B.
For example, task B may be responsible for performing a time-consuming measurement,
while task A is responsible for continuously controlling another device.
*/
Serial.println(F("TASK 2: Generating a task using lambda..."));
delay_task(MILLISECONDS(100));
/*
In this example, a lambda is used to create the task, but it could also be
a function or an object inheriting from the runnable class.
In this version of the library, lambdas with captures are not supported yet,
but this will most likely be added in the future.
*/
// Task 2 invokes the new operator.
// It is not necessary to store the pointer returned by new, because in this example task 2 will not refer to it later.
new task([]()
{
Serial.println(F("TASK 3: The task was generated successfully"));
digitalWrite(13, HIGH);
delay_task(MILLISECONDS(500));
for(int i = 0; i < 10; i++)
{
Serial.print(F("TASK 3: "));
Serial.print(i);
Serial.println(F("/10"));
delay_task(MILLISECONDS(20));
}
digitalWrite(13, LOW);
Serial.println(F("TASK 3: The task has been completed successfully"));
/*
When a task finishes its work, it has several options:
- it can do nothing and remain in memory to be accessed by another task
- otherwise, the task should delete itself
During execution, the scheduler always provides a pointer
to the currently running task, called current_task.
Therefore, to free the task memory, it is sufficient to write:
*/
delete current_task;
}); // End of lambda.
// Task 2 continues its execution independently of what is happening in the lambda.
rerun_task(SECONDS(2));
}
task task2(sequential_task_example, MILLISECONDS(200));
/*
It should be remembered that the scheduler never preempts a task automatically
and never switches tasks on its own. It is the responsibility of the task to decide
when the scheduler may execute another task. A task runs until one of the following occurs:
- delay_task() is called
- rerun_task() is called
- end_task() is called
- the function simply ends
Only then can the scheduler execute other available tasks during the delay period.
Using while(1){} without any of the above functions is not allowed, as it will completely
block the execution of other tasks. If necessary, use while(1) { delay_task(1); }
Why this solution?
It saves CPU time that would otherwise be spent managing access to shared resources.
Therefore, mutexes or semaphores are not required, but tasks must use delay functions.
This approach is much more memory-efficient, because the delay function suspends
the task exactly at a point where no computation is being performed. If the scheduler
were preemptive, it would not know what the function was doing at a given moment
and would have to store all register information, which would require a minimum
of 37 bytes per task. Without preemption, only the registers required by the ABI convention
need to be saved, which requires at least 22 bytes per task.
*/
// ======================================================
// Other functions and variables not used in the examples
// ======================================================
/*
Every task that is to be executed is stored in the task queue.
By convention:
- a task in the queue is considered unfinished
- a task not in the queue is considered finished
The function bool is_task_finished(runnable* task) is provided,
which returns whether a task has already finished or not:
- true – if the task is finished
- false – if the task is still waiting to be executed
There is also a time_stamp() function that marks the time
from which delay_task() or rerun_task() should be calculated.
By default, time is counted from the moment the task object is created
or from the last call to one of these functions.
It is also possible to read the current time from the tick_counter variable.
*/