When writing code for an MK, it is more complicated than “blinking a light bulb”, the developer is faced with the limitations inherent in linear programming in the “super cycle plus interrupt” style. Interrupt handling requires quickness and brevity, which leads to the addition of flags to the code and bringing the project to the style “super cycle with interrupts and flags”.
If the complexity of the system increases, the number of interdependent flags grows exponentially, and the project quickly turns into a poorly readable and manageable macaroni code.
The use of real-time operating systems helps to get rid of the “macaroni code” and return the complex project on the MC to flexibility and control.
Several real-time cooperative operating systems have been developed and quite popular for AVR microcontrollers. However, all of them are written in C or Assembler and are not suitable for those who program MK in the BASCOM AVR environment, depriving them of such a useful tool for writing serious applications.
')
To remedy this shortcoming, I developed a simple RTOS for the BASCOM AVR programming environment, which I bring to the court of readers.

For many, the familiar MK programming style is the so-called.
super cycle The code consists of a set of functions, procedures and descriptors (constants, variables), possibly library ones, generally referred to as “background code”, as well as a large infinite loop enclosed in a
do-loop type structure. At start-up, the equipment of the MK itself and external devices are first initialized, constants and initial values of variables are set, and then control is transferred to this infinite supercycle.
The simplicity of the supercycle is obvious. Most of the tasks performed by the MC, because somehow cyclical. There are also disadvantages: if some device or signal requires an immediate response, the MC will provide it no sooner than the cycle turns around. If the duration of the signal is shorter than the cycle period, such a signal will be skipped.
In the example below, we want to check whether the button is
pressed :
do
Obviously, if “some code” works long enough, the MC may not notice a short press of a button.
Fortunately, the MC is equipped with an interrupt system that solves this problem: all critical signals can be “hung up” on interrupts and write for each handler. So the next level appears:
supercycle with interrupts .
The example below shows the structure of a program with a super-cycle and an interrupt that handles a button press:
on button button_isr
However, the use of interrupts creates a new problem: the interrupt handler code must be as fast and short as possible; inside the interrupt functional MK is limited. Since AVR MCs do not have a hierarchical interrupt system, another interrupt cannot occur inside the interrupt - they are hardware prohibited at this time. So the interrupt should be executed as quickly as possible, otherwise other interrupts (and possibly more important ones) will be skipped and not processed.
Interrupt MemorizationIn fact, being inside an interrupt, the MC is able to note the fact of another interruption in a special register, which allows it to be processed later. However, this interrupt cannot be processed immediately anyway.
Therefore, we cannot write something complicated in the interrupt handler, especially if this code should have delays - as long as the delay does not work, the MC will not return to the main program (super cycle) and will be deaf to other interrupts.
Because of this, inside the interrupt handler, you often only have to mark the fact of an event with a flag — your own for each event — and then check and process flags inside the super-cycle. This, of course, lengthens the time of reaction to an event, but at least we do not miss anything important.
Thus, the next level of complexity arises - a
supercycle with interrupts and flags .
The following code is similar:
on button button_isr
A considerable number of programs for MK and this is limited. However, such programs are usually still more or less simple. If you write something more complicated, the number of flags begins to grow like a snowball, and the code becomes more and more confused and unreadable. In addition, the example above does not solve the problem with delays. Of course, you can "hang up" a separate interrupt on the timer, and in it ... also control various flags. But this makes the program completely ugly, the number of interdependent flags grows exponentially, and even the developer himself can hardly figure out such a macaroni code. Attempting to find an error or modify the code often becomes equal in efforts to develop a new project.
How to solve the problem of "macaroni code" and make it more readable and manageable? The
operating system (OS) comes to the rescue. In it, the functional that should be implemented by the MC is divided into tasks that the OS manages.
Types of operating systems for MK
Operating systems for MCs can be divided into two large classes: OS with crowding out and cooperative OS. In any of these operating systems, tasks are managed by a special procedure called the
dispatcher . In an OS with
displacement, the dispatcher independently at any moment switches the execution from one task to another, allocating to each some number of machine time ticks (possibly different, depending on the priority of the task). Such an approach as a whole works remarkably, allowing you not to look at the content of the tasks at all: in the problem code you can write at least
1: goto 1
- and still the remaining tasks (including this one) will be carried out. However, preemptive OSs require a lot of resources (memory and processor cycles), since each switch must completely preserve the context of the task being disconnected and load the renewable context. The context here refers to the contents of the machine registers and the stack (BASCOM uses two stacks - hardware for return addresses of subroutines and software for passing arguments). Not only does such a load require multiple processor cycles, so the context of each task needs to be stored somewhere for as long as it does not work. In the "large" processors, originally focused on multitasking, these functions are often supported by hardware, and they have much more resources. In AVR MK there is no hardware support for multitasking (all you need to do "manually"), and the available memory is small. Therefore, the preemptive OS, although they exist, is not very suitable for simple MCs.
Another thing -
cooperative OS . Here the task itself controls the moment at which control is transferred to the dispatcher, allowing him to start other tasks for execution. Moreover, the tasks here are obliged to do it - otherwise the execution of the code is stalled. On the one hand, it seems that such an approach reduces the overall reliability: after all, if a task “hangs”, it will never call the dispatcher, and the whole system will stop responding. On the other hand, a linear code or a supercycle in this plan is no better than that, because they can hang exactly with the same risk.
However, the cooperative OS has an important advantage. Since here the programmer himself sets the switching moment, it cannot happen suddenly, for example, while a task is working with some resource or in the middle of calculating an arithmetic expression. Therefore, in a cooperative OS in most cases you can do without saving the context. This significantly saves processor time and memory, and therefore looks much more suitable for implementation on AVR MC.
Task switching in BASCOM AVR
To implement task switching in the BASCOM AVR environment, a task code, each of which is implemented as a normal procedure, must call the dispatcher in some place — also implemented as a normal procedure.
Imagine that we have two tasks, each of which, at some point in its code, calls the dispatcher.
sub task1() do
Suppose Task 1 was executed. Let's see what happens on the stack when it performs a “dispatcher call”:
return address to the main code (2 bytes)
top of the stack -> return address to Task 1 that called the dispatcher (2 bytes)
The top of the stack will point to the instruction address in Task 1, which follows the dispatcher's call (the
loop instruction in our example).
In the simplest case, the dispatcher's goal is to transfer control to Task 2. The question is how to do this? (suppose the dispatcher already knows the address of Task 2).
To do this, the dispatcher must pull out the return address to Task 1 from the stack (and somewhere to remember), and place the address of Task 2 on this stack, and then give the return command. The processor will retrieve the address placed there from the stack and, instead of returning to Task 1, will proceed to Task 2.
In turn, when Task 2 calls the dispatcher, we will also pull out the stack and save the address at which we can return to Task 2, and load the previously saved Task address 1 onto the stack. Give the
return command - and find ourselves at the continuation point of Task 1 .
As a result, we will have such a leapfrog:
Task 1 -> Dispatcher -> Task 2 -> Dispatcher -> Task 1 ....
Not bad! And it works. But, of course, for any OS that is suitable for practical use, this is not enough. After all, not always and not all tasks should work - for example, they can
expect something (the expiration of the delay time, the appearance of some signal, etc.). This means that tasks should have
status (WORKS, READY, EXPECT, and so on). In addition, it would be nice to assign tasks
priority . Then, if more than one task is ready for execution, the dispatcher will continue the task that has the highest priority.
AQUA RTOS
To implement the described idea, the cooperative OS AQUA RTOS was developed, providing the tasks with the necessary services and allowing to implement cooperative multitasking in the BASCOM AVR environment.
Important note regarding the procedure mode in BASCOM AVRBefore you begin the description of AUQA RTOS, it should be noted that the BASCOM AVR environment supports two types of addressing procedures. This is governed by the config option submode = new | old.
In the case of specifying the old option, the compiler, first, will compile all the code linearly, regardless of whether it is used somewhere or not, secondly, procedures without arguments, decorated in the sub name / end style will be perceived as procedures designed in the style of name: / return. This allows us to pass the address of the procedure as a label as an argument to another procedure by using the bylabel modifier. This also applies to procedures designed in the style in the sub name / end sub style (the name of the procedure must be passed as a label).
At the same time, the mode submode = old imposes some restrictions: task procedures should not contain arguments; the code of the files connected via $ include is included in the overall project linearly, therefore the bypass should be provided in the connected files - transition from beginning to end using a goto and a label.
Thus, in AQUA RTOS, the user must either use only the old task_name: / return procedure notation for tasks, or use the more common sub name / end sub, adding the modifier config submode = old to the beginning of his code, and the bypass to the included files goto label / include file code / label :.
Task Status AQUA RTOS
The following statuses are defined for tasks in AQUA RTOS:
OSTS_UNDEFINE OSTS_READY OSTS_RUN OSTS_DELAY OSTS_STOP OSTS_WAIT OSTS_PAUSE OSTS_RESTART
If the task is not yet initialized, it is assigned the status
OSTS_UNDEFINE .
After initialization, the task has the status
OSTS_STOP .
If the task
is ready for execution , it is assigned the status
OSTS_READY .
The currently running task has the status
OSTS_RUN .
From it it can go to the statuses
OSTS_STOP, OSTS_READY, OSTS_DELAY, OSTS_WAIT, OSTS_PAUSE .
The
OSTS_DELAY status has a task that performs the
delay .
The status of
OSTS_WAIT is assigned to tasks that are
waiting for a semaphore, event or message (more about them below).
What is the difference between
OSTS_STOP and
OSTS_PAUSED statuses ?
If for some reason the task receives the status
OSTS_STOP , then the subsequent resumption of the task (upon receipt of the status
OSTS_READY ) will be carried out from the point of its entry, i.e. from the very beginning. From the status of
OSTS_PAUSE, the task will continue to work in the place where it was suspended.
Task Status Management
Both the OS itself can manage tasks - automatically, and the user, by calling the OS services. There are several task management services (the names of all OS services start with the prefix
OS_ ):
OS_InitTask(task_label, task_prio) OS_Stop() OS_StopTask(task_label) OS_Pause() OS_PauseTask(task_label) OS_Resume() OS_ResumeTask(task_label) OS_Restart()
Each of them has two options:
OS_service and
OS_serviceTask (except for the
OS_InitTask service, which has only one option; the
OS_Init service initializes the OS itself).
What is the difference between
OS_service and
OS_serviceTask ? The first method acts on the task that caused it; the second allows you to specify a pointer to another task as an argument and, thus, to control another from one task.
About OS_ResumeAll task management services, except for OS_Resume and OS_ResumeTask, automatically call the task manager after processing. In contrast, OS_Resume * services only set the task status OSTS_READY. This status will be processed only when the dispatcher is explicitly called.
Priority and queue of tasks
As mentioned above, in a real system, some tasks may turn out to be more important, while others may be minor. Therefore, a useful feature of the OS is the ability to assign tasks priority. In this case, if there are several simultaneously
completed tasks, the OS will first select the task with the highest priority. If
all finished tasks have equal priority, the OS will put them on execution in a circle, in the order called “carousel” or round-robin.
In AQUA RTOS, priority is assigned to a task when it is
initialized through a call to the
OS_InitTask service, to which the address of the task is passed as the first argument, and the number from 1 to 15 is sent as the second argument.
A lower number means a higher priority . In the course of the OS, the change in the assigned task priority is not provided.
Delays
In each task, the delay is processed independently of other tasks.
Thus, while the OS fulfills the delay of one task, others can be executed.
For the organization of delays provided services
OS_Delay | OS_DelayTask . The argument is the number of milliseconds to which the execution of the task is
postponed . Since the dimension of the argument is
dword , the maximum delay is 4294967295 ms - or about 120 hours, which seems to be quite sufficient for most applications. After calling the delay service, the dispatcher is automatically called, who, while the delay is being processed, transfers control to other tasks.
Semaphores
Semaphores in AQUA RTOS are something like flags and variables accessible to tasks. They are of two types - binary and countable. The first have only two states: free and closed. The second ones are a byte counter (the counting semaphore service in the current version of AQUA RTOS is not implemented (I'm a lazy ass), so everything below applies only to binary semaphores).
The difference between a semaphore and a simple flag is that the task can be made to
wait for the release of the specified semaphore. In some ways, the use of semaphores really resembles a railroad: before reaching the semaphore, the composition (task) will check the semaphore, and if it is not open, it will wait for the enabling signal to go on. At this time, other trains (tasks) can continue to move (run).
In this case, all the black work is assigned to the dispatcher. As soon as the task is ordered to wait for the semaphore, control is automatically transferred to the dispatcher, and it can run other tasks — just until the specified semaphore is released. As soon as the state of the semaphore changes to
free , the dispatcher will assign the status to all waiting for this semaphore task (
OSTS_READY ), and they will be executed in turn and priority.
In total, AQUA RTOS provides 16 binary semaphores (this number can in principle be increased by changing the dimension of the variable in the task control block, since inside they are implemented as bit flags).
Binary semaphores work through the following services:
hBSem OS_CreateBSemaphore() OS_WaitBSemaphore(hBSem) OS_WaitBSemaphoreTask(task_label, hBSem) OS_BusyBSemaphore(hBSem) OS_FreeBSemaphore(hBSem)
Before using the semaphore you need
to create . This is done by calling the
OS_CreateBSemaphore service, which returns a unique byte identifier (handle) of the created
hBSem semaphore, or through a custom handler issues an
OSERR_BSEM_MAX_REACHED error, indicating that the maximum possible number of binary semaphores has been reached.
You can work with the received identifier by passing it as an argument to other semaphore services.
OS_WaitBSemaphore Service
| OS_WaitBSemaphoreTask puts the (current | specified) task to
wait for the hBSem semaphore to be released if the semaphore is busy, and then transfers control to the dispatcher so that it can run other tasks. If the semaphore is free, control transfer does not occur, and the task will simply continue execution.
The
OS_BusyBSemaphore and
OS_FreeBSemaphore services set the
hBSem semaphore to
busy or
free, respectively.
Destruction of semaphores in order to simplify the OS and reduce the amount of code is not provided. Thus, all created semaphores are static.
Developments
In addition to semaphores, tasks can be event driven. One task can be specified to
expect an event , and another task (as well as a background code) can
signal this event. At the same time, all tasks that were waiting for this event will receive the status
ready for execution (
OSTS_READY ) and will be delivered by the dispatcher for execution in turn and priority order.
What events can the task respond to? Well, for example:
- interrupt;
- the occurrence of an error;
- release of a resource (sometimes it is more convenient to use a semaphore for this);
- changing the state of the I / O line or pressing a key on the keyboard;
- receive or send a symbol via RS-232;
- transfer of information from one part of the application to another (see also messages).
The event system is implemented through the following services:
hEvent OS_CreateEvent() OS_WaitEvent(hEvent) OS_WaitEventTask(task_label, hEvent) OS_WaitEventTO(hEvent, dwTimeout) OS_SignalEvent(hEvent)
Before using the event you need
to create it . This is done by calling the
OS_CreateEvent function, which returns the unique byte identifier (handle) of the
hEvent event, or, via a custom handler, issues an
OSERR_EVENT_MAX_REACHED error, indicating that the number of events that can be created in the OS has been reached (maximum 255 different events).
To make a task wait for an
hEvent event, in its code you should call
OS_WaitEvent , passing the event handle as an argument. After calling this service, the control will be automatically transferred to the dispatcher.
Unlike the semaphore service, the event service has a waiting option with a
timeout . For this is the service
OS_WaitEventTO . The second argument here is the number of milliseconds that the task can wait for an event. If the specified time has elapsed, the task will receive the status
ready for execution as if the event has occurred, and will be supplied by the dispatcher to continue execution in the order of priority and priority. You can learn about what happened not an event, but a timeout, by checking the global flag
OS_TIMEOUT .
The task or background code can signal the occurrence of a specified event by calling the OS_SignalEvent service , to which the event handle is passed as an argument. In this case, all tasks waiting for this event, the OS will set the status ready for execution , so that they can continue execution in the order of priority and queue.Messages
The message system works as a whole similarly to the event system, however, it provides tasks with more opportunities and flexibility: it provides not only waiting for a message on a specified topic, but a way to transfer a message from one task to another - a number or a string.It is implemented through the following services: hTopic OS_CreateMessage() OS_WaitMessage(hTopic) OS_WaitMessageTask(task_label, hTopic) OS_WaitMessageTO(hTopic, dwTimeout) OS_SendMessage(hTopic, wMessage) word_ptr OS_GetMessage(hTopic) word_ptr OS_PeekMessage(hTopic) string OS_GetMessageString(hTopic) string OS_PeekMessageString(hTopic)
To use the messaging service, you must first create a message subject . This is done through the service OS_CreateMessage , which returns a handle to a byte threads hTopic , either through a custom handler throws an error OSERR_TOPIC_MAX_REACHED , talking about what has been achieved the maximum possible number of the messages, and no longer able to create.To tell the task to wait for a message on the topic of hTopic , in its code you should call OS_WaitMessage , passing the handle of the topic as an argument. After calling this service, the control will be automatically transferred to the task manager. Thus, this service puts the current task in the statewait for posts on hTopic .The idle timeout service OS_WaitMessageTO works in the same way as the OS_WaitEventTO service of the event system.For sending messages, the service OS_SendMessage is provided . The first argument is the topic handle to which the message will be transmitted, and the second argument is the word dimension argument . This can be either a standalone number or a pointer to a string , which, in turn, is already a message.To get a string pointer, it suffices to use the varptr function built into BASCOM , for example, like this: strMessage = "Hello, world!" OS_SendMessage hTopic, varptr (strMessage)
After resuming work after calling OS_WaitMessage , that is, when an expected message is received, the task can either receive a message with its subsequent automatic deletion, or just view the message - in this case it will not be deleted. For this are the last four services in the list. The first two return the number of the word dimension , which can be either an independent message, or serve as a pointer to the string that contains the message. At the same time OS_GetMessage automatically deletes the message, and OS_PeekMessage leaves it.If the task immediately needs a string, not a pointer, you can use the services OS_GetMessageString or OS_PeekMessageString which work similarly to the previous two, but return a string, not a pointer to it.Internal timer service
To work with the delays and timing AQUA RTOS uses a built-in IC hardware timer TIMER0 . Thus, the external code (background and tasks) should not use this timer. But usually it is not required, because The OS supplies tasks with all necessary means of working with time intervals. Timer resolution is 1 ms.Examples of working with AQUA RTOS
Initial settings
At the very beginning of the user code, you need to determine whether the code will be executed in the embedded simulator or on real hardware. Define the constant OS_SIM = TRUE | FALSE , which sets the simulation mode.In addition, in the OS code, edit the OS_MAX_TASK constant , which defines the maximum number of tasks supported by the OS. Than this number is less, the OS works faster (less overhead), and the less memory it consumes. Therefore, there is no need to specify there more tasks than you need. Do not forget to change this constant if the number of tasks has changed.OS Initialization
Before you start AQUA RTOS must be initialized. To do this, call the service OS_Init . This service adjusts the initial parameters of the OS. More importantly, it has an argument — the address of a custom error-handling procedure. She, in turn, also has an argument - an error code.This handler must be in the user code (at least in the form of a stub) - the OS sends error codes to it, and the user has no other way to catch them and process them accordingly. I strongly recommend that, at least during the development phase, not to stub out, but to include in this procedure any output of information about errors.So, the first step in working with AQUA RTOS is to add the OS initialization code and the error handler procedure to the user program: OS_Init my_err_trap
Task initialization
The second step is to initialize the tasks, specifying their names and priority: OS_InitTask task_1, 1 OS_InitTask task_2 , 1
Test tasks
We blink LEDs
So let's create a test application that can be loaded into a standard Arduino Nano V3 board. Create a folder in the folder with the OS file (for example, test), and create the following bas file there:
Connect the anodes of the LEDs to the D4 and D5 pins of the Arduino board (or to other pins by changing the corresponding definition lines in the code). Cathodes through limiting resistors 100 ... 500 Ohms connect to the GND bus . Compile and fill the firmware into the board. The LEDs will start switching asynchronously with a period of 2 and 0.66 s.Let's look at the code. So, first we initialize the hardware (we set the compiler options, the port mode and assign aliases), then the OS itself, and finally the tasks.Since the newly created tasks are in the “stopped” state, you need to give them the status “ready to be executed” (perhaps not all tasks in a real application — after all, some of them may, according to the developer’s plan, initially be in a stopped state, and run on performing only from other tasks, and not immediately at the start of the system; however, in this example, both tasks should immediately start working). Therefore, for each task, we call the OS_ResumeTask service .Now the tasks are ready for execution, but not yet executed. Who will launch them? Of course, the dispatcher! To do this, we must call it when you first start the system. Now, if everything is written correctly, the dispatcher will perform our tasks in turn, and we can finish the main part of the program with the end operator.Let's look at the tasks. It immediately catches the eye that each of them is framed as an endless do-loop loop . The second important property is that within such a cycle there must be at least one call to either the dispatcher or the OS service that automatically calls the dispatcher after itself — otherwise such a task will never give up control and other tasks will not be able to be performed. In our case, this is the OS_Delay delay service . As an argument, we gave him the number of milliseconds to which each task should be suspended.If you set the constant OS_SIM = TRUE at the beginning of the code and run the code not on a real chip, but in the simulator, you can see how the OS works.The dispatcher we called will see if the tasks are “ready for execution”, and arrange them in a queue according to priority. If the priority is the same, the dispatcher will “roll tasks on the carousel”, moving the task that has just been worked out to the very end of the queue.By selecting the task that should be executed (say, task_1 ), the dispatcher replaces the return address on the stack (initially it shows the end instruction in the main code) with the entry point address of the task task_1 , which the system recognizes during the task initialization, and executes the return command , which causes MK pull the return address from the stack and go to it - that is, start the execution of the task task_1 (operatordo in task_1 code ).Task task_1 , switching its LED, causes the OS_delay service , which, having performed the necessary actions, goes to the dispatcher.The dispatcher saves the address that was on the stack to the task_1 task control block (points to the instruction following the OS_delay call , i.e., the loop instruction ), and then, “turning the carousel” detects that it is now necessary to execute the task_2 task . He pushes the task_2 task address onto the stack (currently indicates the do statement in task_2 task code ) and executes the commandreturn , which forces MK to pull out the return address from the stack and go to it - that is, start the execution of the task task_2 .Task task_2 , switching its LED, causes the OS_delay service , which, having performed the necessary actions, goes to the dispatcher.The dispatcher saves the address that was on the stack to the task_1 task control block (points to the instruction following the OS_delay call , i.e., the loop instruction ), and then, “turning the carousel” detects that it is now necessary to execute the task_2 task . The difference from the initial state will be that now in the task control block task_1not the starting address of the task is stored, but the address of the point from which the transition to the dispatcher occurred. There (on the loop instruction in the task_1 task code ), and control will be transferred.The task task_1 will execute the loop instruction , and then the whole cycle “Task 1 - Manager - Task 2 - Manager” will repeat endlessly.We send messages
Now let's try sending messages from one task to another.
The result of running the program in the simulator will be the following output to the terminal window:task 1
task 2 is waiting messages…
task 1
task 1
task 1
task 1 is sending message to task 2
task 1
message recieved: Hello, task 2!
task 2 is waiting messages…
task 1
task 1
...
Notice the order in which the work and task switching takes place. As soon as Task 1 types task 1 , control is transferred to the dispatcher so that he can start the second task. Task 2 prints task 2 is waiting messages ... , then calls the service to wait for messages on the hTopic theme , and control is automatically transferred to the dispatcher, which again calls Task 1. Ta prints task 1 again and gives control to the dispatcher. However, since the dispatcher detects that Task 2 is now waiting for messages, it returns control of Task 1 to the incr instruction following the dispatcher's call.When the task_1_cnt counterin Task 1 will exceed the specified value, the task sends a message, but continues to be executed - executes the loop instruction and again prints task 1 . After that, she calls the dispatcher, who now detects that there is a message for Task 2, and transfers control to it. Further process is carried out cyclically.Event handling
The following code polls two buttons and switches the LEDs when the corresponding button is pressed:
An example of a real application under AQUA RTOS
Let's try to imagine what the program of a coffee vending machine could look like. The machine should show the presence of coffee options and selection in LEDs in the buttons; receive signals from the receiver of coins, prepare the ordered drink, issue change. In addition, the machine must control the internal equipment: for example, maintain the temperature of the water heater at 95 ... 97 ° C; transmit information about equipment malfunction and stock of ingredients and receive commands via remote access (for example, via a GSM modem), and also signal vandalism.Event driven approach
At first, it is not easy for a developer to switch from a familiar “super-cycle + flags + interrupt” scheme to an approach based on tasks and events. This requires highlighting the main tasks that the device must perform.Let's try to outline such tasks for our machine:- control and heater control - ControlHeater ()
- indication of the presence and selection of drinks - ShowGoods ()
- acceptance of coins / bills and their summation - AcceptMoney ()
- poll buttons - ScanKeys ()
- make change - MakeChange ()
- Beverage Vacation - ReleaseCoffee ()
- vandalism protection - Alarm ()
Let us estimate the importance of tasks and the frequency of their call.ControlHeater () is obviously important because we always need boiling water to make coffee. But it should not be done too often, because the heater is very inert and the water cools slowly. Enough to check the temperature once a minute. We give this task a priority of 5.ShowGoods () is not too important. The offer can change only after the release of the goods, if the stock of some ingredients is exhausted. Therefore, we give this task a priority of 8, and let it be executed when the machine starts and every time the goods are released.ScanKeys ()should have a high enough priority for the machine to respond quickly to pressing the buttons. We give this task a priority of 3, and we will execute it every 40 milliseconds.AcceptMoney () is also part of the user interface. We will give it the same priority as ScanKeys (), and we will execute it every 20 milliseconds.MakeChange () is executed only after the goods are released. We will associate it with ReleaseCoffee () and give priority 10.ReleaseCoffee () is needed only when the appropriate amount of money has been received and the drink selection button is pressed. For quick response, we give it priority 2.Since anti-vandal resistance is a rather important function of the automaton, the Alarm () taskYou can set the highest priority to 1, and activate it once a second to check tilt or tamper sensors.Thus, we will need seven tasks with different priorities. After the start, when the program reads the settings from the EEPROM and initializes the hardware, it is time to initialize the OS and start the tasks.
To work as part of an RTOS, each task must have a specific structure: it must have at least one dispatcher call (or an OS service that automatically transfers control to the dispatcher) - this is the only way to ensure cooperative multitasking.For example, ReleaseCoffee () might look something like this: sub ReleaseCoffee() do OS_WaitMessage bCoffeeSelection wItem = OS_GetMessage(bCoffeeSelection) Release wItem loop end sub
The ReleaseCoffee task in the infinite loop waits for a message on the topic bCoffeeSelection and does not do anything until it arrives (control is automatically returned to the dispatcher so that he can run other tasks). As soon as the message is sent, ReleaseCoffee () is ready for execution, and when this happens, the task receives the contents of the message (selected beverage code) wItem using the OS_GetMessage service and releases the goods to the customer. Since ReleaseCoffee () uses a message subsystem, a message must be created before running multitasking: dim bCoffeeSelection as byte bCoffeeSelection = OS_CreateMessage()
As mentioned above, ShowGoods () must be executed once during start-up and every time the goods are released. To associate it with the release procedure of ReleaseCoffee () , we use the event service. To do this, create an event before running multitasking: dim bGoodsReliased as byte bGoodsReliased = OS_CreateEvent()
And in the ReleaseCoffee () procedure, after the Release wItem line, add the bGoodsReliased event alarm : OS_SignalEvent bGoodsReliased
OS Initialization
In order to prepare the OS for work, we must initialize it, specifying the address of the error handler, which is in the user code. We do this using the OS_Init service : OS_Init Mailfuncion
In the user code, you need to add a handler - a procedure whose byte argument will be the error code: sub Mailfuncion (bCoffeeErr) print "Mailfunction! Error #: "; bCoffeeErr if isErrCritical (bCoffeeErr) = 1 then CallService(bCoffeeErr) end if end sub
This procedure prints the error code (or may display it in some other way: on the screen, via a GSM modem, etc.), and in case the error is critical, call the service department.Running tasks
We already remember that events, semaphores, etc. must be initialized before being used. In addition, the tasks themselves must be initialized using the OS_InitTask service before starting : OS_InitTask ControlHeater , 5 OS_InitTask ShowGoods , 8 OS_InitTask AcceptMoney , 3 OS_InitTask ScanKeys , 3 OS_InitTask MakeChange, 10 OS_InitTask ReleaseCoffee , 2 OS_InitTask Alarm , 1
Since the multitasking mode has not yet begun, the order in which tasks are started is irrelevant, and in any case does not depend on their priorities. At this point, all tasks are still in a stopped state. To prepare them for execution, we must use the OS_ResumeTask service to give them the “ready for execution” status: OS_ResumeTask ControlHeater OS_ResumeTask ShowGoods OS_ResumeTask AcceptMoney OS_ResumeTask ScanKeys OS_ResumeTask MakeChange OS_ResumeTask ReleaseCoffee OS_ResumeTask Alarm
As already mentioned, not all tasks must start when multitasking starts; some of them can be arbitrarily “stopped” at any time and receive readiness only under certain conditions. The OS_ResumeTask service can be called at any time from anywhere in the code (background or task) when multitasking is already running. The main thing is that the task to which it refers, has been previously initialized.Start multitasking
Now everything is ready to start multitasking. We do this by calling the dispatcher: OS_Sheduler
After that, we can safely put end in the program code — the OS now takes control of the further execution of the code.Let's look at the whole code:
Of course, it would be more correct to approach not with periodic polling of buttons and cash sensor, but using interrupts. In the handlers of these interrupts, we could use the sending of messages using the OS_SendMessage () service with the content equal to the number of the key pressed or the denomination of the entered coin / bill. I suggest the reader to modify the program independently. Thanks to the task-oriented approach and the service provided by the OS, this will require minimal code changes.AQUA RTOS source code
Source code version 1.05 is available for download at the link.P.S
Q: Why AQUA?A: Well, I did the controller of the aquarium, it is like a “smart home”, but not for people, but for fish. Full of all kinds of sensors, a real-time clock, relay and analogue power outputs, an on-screen menu, a flexible “event program”, and even a WiFi module. Intervals should be counted, buttons should be polled, sensors should be processed, an event program should be read from the EEPROM and executed, the screen should be updated, Wi-Fi should respond. Moreover, the controller should go to a multi-level menu for settings and programming. To do it on flags and interrupts is to get the very “macaroni code”, which cannot be sorted out or modified. That's why I decided that I needed an OS. Here it is AQUA.Q: Surely the code is full of logical errors and glitches?A: Surely. I, as I could, invented various tests and drove the OS on a variety of tasks, and even slapped a noticeable number of bugs, but this does not mean that all of them are complete. More than confident that they are still a lot hidden in the back streets of the code. Therefore, I will be very grateful if, instead of poking me in the bugs with a muzzle, you politely and tactfully point them out, and better tell me how you think they are best corrected. It will also be great if the project is further developed as a product of collective creativity. For example, someone will add a counting semaphore service (not forgotten? I'm a lazy ass) and suggest other improvements. In any case, I will be very grateful for the constructive contribution.