AQUA RTOS real-time OS for MK AVR in BASCOM AVR environment

When writing for MK code more complicated than “blinking a light”, the developer is faced with the limitations inherent in linear programming in the style of “supercycle plus interrupts”. Processing interrupts requires speed and conciseness, which leads to adding flags to the code and making the project style “supercycle with interrupts and flags”.

If the complexity of the system grows, the number of interdependent flags grows exponentially, and the project quickly turns into a poorly readable and manageable “pasta code”.

The use of real-time operating systems helps to get rid of the "pasta code" and return flexibility and manageability to a complex MK project.
Several cooperative real-time operating systems have been developed and quite popular for AVR microcontrollers. However, they are all 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 correct this shortcoming, I developed a simple RTOS for the BASCOM AVR programming environment, which I bring to the attention of readers.

image

For many, the familiar MK programming style is the so-called supercycle . The code in this case consists of a set of functions, procedures, and descriptors (constants, variables), possibly library ones, generally called “background code”, as well as a large infinite loop enclosed in a do-loop construct. At start-up, the equipment of the MK itself and external devices is initialized first, the constants and initial values ​​of the 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 MK, because one way or another cyclical. The disadvantages are also evident: if some device or signal requires an immediate reaction, MK will provide it no sooner than the cycle turns around. If the signal duration is shorter than the cycle period, such a signal will be skipped.

In the example below, we want to check if the button is pressed :

do
    ' какой-то код
    if button = 1 then ' реакция на нажатие кнопки
    ' еще какой-то код
loop

Obviously, if "some code" works long enough, MK may not notice a short press of a button.

Fortunately, MK is equipped with an interrupt system that can solve this problem: all critical signals can be “hung” on interrupts and a handler can be written for each. So the next level appears: a supercycle with interruptions .
The example below shows the structure of the program with a supercycle and an interrupt that processes a button click:

on button button_isr ' назначаем обработчик кнопки
enable interrupts
' *** суперцикл ***
do
    ' какой-то код
loop
end
' обработчик кнопки
button_isr:
    ' делаем что-то при нажатии кнопки
return

However, using interrupts poses a new problem: the interrupt handler code should be as fast and shorter as possible; inside interruptions, MK functionality is limited. Since AVR MKs do not have a hierarchical interrupt system, another interrupt cannot happen inside an interrupt - they are hardware disabled 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 memory
In fact, being inside the interrupt, MK is able to note the fact of another interrupt in a special register, which allows it to be processed later. However, this interrupt cannot be processed immediately.

Therefore, we cannot write something complicated in the interrupt handler, especially if this code must have delays - because until the delay works, the MK will not return to the main program (supercycle) and will be deaf to other interrupts.

Because of this, inside the interrupt handler you often only have to flag the fact of the event with a flag - your own for each event - and then check and process the flags inside the supercycle. This, of course, lengthens the reaction time to the event, but at least we do not miss something important.

Thus, the next level of complexity arises - a supercycle with interrupts and flags .

The following code is shown:

on button button_isr ' назначаем обработчик кнопки
enable interrupts
' *** суперцикл ***
do
    ' какой-то код
    if button_flag = 1 then 
        ' реакция на нажатие кнопки
        button_flag = 0 ' не забудем сбросить флаг
    end if
    ' еще какой-то код
loop
end
' *** обработчик прерывания кнопки ***
button_isr:
    button_flag = 1
return

A considerable number of programs for MK are limited by this. However, such programs are usually still more or less simple. If you write something more complicated, then the number of flags starts to grow like a snowball, and the code becomes more and more confused and unreadable. In addition, in the example above, the problem with delays has not been resolved. Of course, you can "hang" 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 is growing exponentially, and even the developer himself can hardly figure out such a “pasta code” quite soon. Trying to find a mistake or modify the code often becomes equal in efforts to develop a new project.

How to solve the problem of "pasta code" and make it more readable and manageable? Comes to the rescueoperating system (OS). In it, the functionality that MK should implement is divided into tasks whose operation is controlled by the OS.

Types of operating systems for MK


Operating systems for MK can be divided into two large classes: OS with crowding out and cooperative OS. In any of these operating systems, tasks are controlled by a special procedure called a dispatcher . In an OS with crowding out, the dispatcher independently at any time switches execution from one task to another, allocating each a certain number of clock cycles (possibly different, depending on the priority of the task). This approach as a whole works great, allowing you not to look at the contents of the tasks at all: you can write at least the task code

1:
goto 1

- and still the rest of the tasks (including this one) will be performed. However, preemptive OSs require a lot of resources (processor memory and clock cycles), since each switch should completely save the context of the task to be disabled and load the context of the resume. The context here refers to the contents of machine registers and the stack (BASCOM uses two stacks - the hardware one for return addresses of subprograms and the software one for passing arguments). Not only does such a load require a lot of processor cycles, but also the context of each task needs to be stored somewhere for a while until it works. In the "large" processors, initially oriented to multitasking, these functions are often supported in hardware, and they have much more resources. There is no multitasking hardware support in MK AVR (everything needs to be done “manually”), and available memory is small. Therefore, displacing OSs, although they exist, are not too suitable for simple MKs.

Another thing is cooperative OS . Here the task itself controls at what point to transfer control to the dispatcher, allowing him to start other tasks. Moreover, the tasks here are required to do this - otherwise the code execution will stall. On the one hand, it seems that this approach reduces overall reliability: 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 is no better in this respect - because they can freeze with exactly the same risk.

However, a cooperative OS has an important advantage. Since here the programmer sets the moment of switching himself, it cannot happen suddenly, for example, while the 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 maintaining the context. This significantly saves processor time and memory, and therefore looks much more suitable for implementation on MK AVR.

Task switching in BASCOM AVR


In order to implement task switching in the BASCOM AVR environment, the task code, each of which is implemented as a normal procedure, must in some place call the dispatcher - also implemented as a normal procedure.
Imagine that we have two tasks, each of which in some place of its code is called by the dispatcher.

sub task1()
    do
        'код Задачи 1
        'вызов диспетчера
    loop
end sub
' ----------------------------------
sub task2()
    do
        'код Задачи 2
        'вызов диспетчера
    loop
end sub

Suppose task 1 was executed. Let's see what happens on the stack when it executes 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 address of the instruction in Task 1, which follows the dispatcher call (the loop instruction in our example).

The goal of the dispatcher in the simplest case 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 should pull the address of return to Task 1 from the stack (and somewhere to remember), and put the address of Task 2 on this place on the stack, and then give the return command. The processor will extract the address placed there from the stack and, instead of returning to Task 1, will proceed to the execution of Task 2.

In turn, when Task 2 calls the dispatcher, we will also pull out the stack and save the address at which it will be possible to return to Task 2, and load the previously saved task 1 address onto the stack. Give the commandreturn - and we will find ourselves at the point of continuation of Problem 1.

As a result, we get such a mess:

Task 1 -> Dispatcher -> Task 2 -> Dispatcher -> Task 1 ....

Not bad! And it works. But, of course, this is not enough for an OS that is practically usable. 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.). So, the tasks should have a status (WORKS, READY, EXPECTED, etc.). In addition, it would be nice to have tasks assigned 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, which provides the necessary services to tasks and allows implementing cooperative multitasking in the BASCOM AVR environment.

Important Notice Regarding Procedure Mode in BASCOM AVR
Before starting the description of AUQA RTOS, it should be noted that the BASCOM AVR environment supports two types of addressing procedures. This is regulated by the config submode = new | old.
In the case of specifying the old option, the compiler, firstly, will compile all the code linearly, regardless of whether it is used somewhere or not, and secondly, procedures without arguments designed in the style of sub name / end sub will be perceived as procedures , styled 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 of the sub name / end sub style (you need to pass the name of the procedure as a label).
At the same time, the submode = old mode imposes some restrictions: task procedures must not contain arguments; the code of files connected via $ include is included in the general project linearly, therefore, bypass should be provided in the connected files - go from beginning to end using goto and a label.
Thus, in AQUA RTOS, the user must either use only the old procedure notation in the style of task_name: / return for tasks, or use the more common sub name / end sub, adding the modifier submode = old at the beginning of his code, and bypass in the included files goto label / include file code / label :.

AQUA RTOS task statuses


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 has not yet been 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, she can go to the statuses OSTS_STOP, OSTS_READY, OSTS_DELAY, OSTS_WAIT, OSTS_PAUSE .
The status OSTS_DELAY has a task fulfilling a delay .
The OSTS_WAIT status 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 of OSTS_STOP , then the subsequent resumption of the task (upon receipt of the status of OSTS_READY ) will be carried out from its entry point, 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 automatically manage the tasks, as well as the user, by calling the OS services. There are several task management services (the names of all OS services begin with the OS_ prefix ):

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 itself that caused it; the second allows you to set as an argument a pointer to another task and, thus, to manage another from one task.

About OS_Resume
All task management services, except OS_Resume and OS_ResumeTask, automatically call the task manager after processing. In contrast, OS_Resume * services only set the task status to OSTS_READY. This status will be processed only when the dispatcher is explicitly called.

Priority and task queue


As mentioned above, in a real system, some tasks may be more important, while others may be secondary. Therefore, a useful feature of the OS is the ability to assign tasks priority. In this case, if there are several ready-made tasks at the same time , the OS will first select the task with the highest priority. If all ready-made tasks have equal priority, the OS will put them to execution in a circle, in an order called a "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 , which receives the address of the task as the first argument and a number from 1 to 15 as the second argument. A lower number means a higher priority. During the operation of the OS, a change in the priority assigned to the task is not provided.

Delays


In each task, the delay is processed independently of other tasks.
Thus, while the OS is working out 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 for which the task is delayed . Since the dimension of the argument is dword , the maximum delay is 4294967295 ms, or about 120 hours, which seems to be sufficient for most applications. After calling the delay service, the dispatcher is automatically called, which transfers control to other tasks for the duration of the delay.

Semaphores


Semaphores in AQUA RTOS are something like flags and variables available 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 service of counting semaphores in the current version of AQUA RTOS is not implemented (I'm a lazy ass), so everything said 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: upon reaching the semaphore, the composition (task) will check the semaphore, and if it is not open, it will wait for the enabling signal to appear to go further. 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 told to wait for the semaphore, control is automatically transferred to the dispatcher, and he can start other tasks - exactly until the specified semaphore is freed. As soon as the state of the semaphore changes to free , the dispatcher assigns all tasks that were waiting for this semaphore the status ready ( OSTS_READY ), and they will be executed in the order of priority 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 unit, because 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 a semaphore must be created . This is done by calling the OS_CreateBSemaphore service , which returns the unique byte identifier (handle) of the created hBSem semaphore , or through the user-defined handler throws 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.

Service OS_WaitBSemaphore | OS_WaitBSemaphoreTask puts the (current | specified) task in a state to wait for the release of the hBSem semaphoreif this 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 simply continues.

Services OS_BusyBSemaphore and OS_FreeBSemaphore set semaphore hBSem a state busy or free , respectively.

The destruction of semaphores in order to simplify the OS and reduce the amount of code is not provided. Thus, all created semaphores are static.

Events


In addition to semaphores, tasks can be event driven. One task can be instructed to expect a certain event , and another task (as well as the background code) may 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 set by the dispatcher for execution in the order of priority and priority.

What events can the task respond to? Well, for example:
  • interrupt;
  • occurrence of an error;
  • release of the resource (sometimes it is more convenient to use a semaphore for this);
  • changing the status of the I / O line or pressing a key on the keyboard;
  • receiving or sending a character via RS-232;
  • information transfer 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 an event, you need to create it . This is done by calling the OS_CreateEvent function , which returns a unique byte identifier (handle) for the hEvent event , or throws an OSERR_EVENT_MAX_REACHED error through the user-defined handler , indicating that the limit on the number of events that can be generated in the OS has been reached (maximum 255 different events).

To make a task wait for an hEvent event , call OS_WaitEvent in its code , passing the event handle as an argument. After calling this service, control will be automatically transferred to the dispatcher.

Unlike the semaphore service, the event service provides an option to wait for an event withtimeout . To do this, use the OS_WaitEventTO service . The second argument here you can specify the number of milliseconds that the task can expect the event. If the specified time has expired, the task will receive the status ready for execution as if the event had occurred, and will be set by the dispatcher to continue execution in the order of priority and priority. The task can find out that it was not an event, but a timeout, that the task could check by checking the OS_TIMEOUT global flag .

The task or background code can signal the occurrence of a given event by calling the OS_SignalEvent service , which receives the event handle as an argument. In this case, all tasks waiting for this event, the OS will set the statusready for execution , so that they can continue to execute in order of priority and priority.

Messages


The message system works in general similarly to the event system, but it provides tasks with more options and flexibility: it provides not only the expectation of a message on a specified topic, but the way the message itself is transmitted from one task to another - a number or a string.
This 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 OS_CreateMessage service , which returns the byte handle of the hTopic topic , or through the user-defined handler, throws an error OSERR_TOPIC_MAX_REACHED , indicating that the maximum possible number of message topics has been reached, and it will not work out anymore.

To tell the task to wait for a message on the hTopic topic , call OS_WaitMessage in its code , passing the handle of the topic as an argument. After calling this service, control will be automatically transferred to the task manager. Thus, this service puts the current task in a statewait for a hTopic message .

The wait service with the OS_WaitMessageTO timeout works similarly to the OS_WaitEventTO service of the event system.

For sending messages, the OS_SendMessage service is provided . The first argument is the handle to the topic to which the message will be transmitted, and the second is the word dimension argument . This can be either an independent number or a pointer to a string , which, in turn, is already a message.

To get a line pointer, just use the varptr function built into BASCOM , for example, like this:

strMessage = "Hello, world!"
OS_SendMessage hTopic, varptr (strMessage)

Resuming work after calling OS_WaitMessage , that is, when the expected message is received, the task can either receive the message with its subsequent automatic destruction, or just view the message - in this case it will not be destroyed. To do this, use the last four services in the list. The first two return a number of dimension word , which can either be an independent message, or serve as a pointer to the string that contains the message. In this case, 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 . Therefore, external code (background and tasks) should not use this timer. But usually this is not required, because The OS supplies tasks with all the necessary tools for working with time intervals. The timer resolution is 1 ms.

Examples of working with AQUA RTOS


Initial settings


At the very beginning of user code, you need to determine whether the code will be executed in the built-in 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 determines the maximum number of tasks supported by the OS. The lower this number, the faster the OS works (less overhead), and the less memory it consumes. Therefore, you should not indicate there more tasks than you need. Do not forget to change this constant if the number of tasks has changed.

OS initialization


Before starting, AQUA RTOS must be initialized. To do this, call the OS_Init service . This service configures the initial OS settings. More importantly, it has an argument — the address of the user-defined error handling routine. 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 at the development stage not to put a stub, but to include any output of error information in this procedure.

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 
    '...
    '...
    '...
sub my_err_trap(err_code as byte)
    print err_code 
end sub

Task initialization


The second step is to initialize the tasks by specifying their names and priority:

OS_InitTask task_1, 1
OS_InitTask task_2 , 1
'...
OS_InitTask task_N , 1

Test tasks


LED flashing


So, let's create a test application that can be downloaded to a standard Arduino Nano V3 board. Create a folder in the folder with the OS file (for example, test), and there create the following bas-file:

' начальные установки компилятора
config submode = old 
$include "..\aquaRTOS_1.05.bas"
$regfile = "m328pdef.dat"
$crystal = 16000000
$hwstack = 48
$swstack = 48
$framesize = 64
' объявление процедур
declare sub my_err_trap (byval err_code as byte)
declare sub task_1()
declare sub task_2()
' назначим светодиодам порты и режим работы
led_1 alias portd.4
led_2 alias portd.5
config portd.4 = output
config portd.5 = output
' *** начало кода приложения ***
' инициализируем ОС
OS_Init my_err_trap 
' инициализируем задачи
OS_InitTask task_1, 1
OS_InitTask task_2 , 1
' изначально все задачи имеют статус «остановлена» (OSTS_STOP)
' чтобы задачи начали работать, им нужно задать статус 
' «готова к выполнению» (OSTS_READY) вызовом сервиса OS_ResumeTask
OS_ResumeTask task_1
OS_ResumeTask task_2
' осталось запустить ОС вызовом диспетчера
OS_Sheduler
end
' *** задачи ***
sub task_1 ()
    do
        toogle led_1 ' переключим светодиод 1
        OS_delay 1000 ' приостановить на 1000 мс
    loop
end sub
sub task_2 ()
    do
        toogle led_2 ' переключим светодиод 2
        OS_delay 333 ' приостановить на 333 мс
    loop
end sub
' ****************************************************
' обработчик ошибок
sub my_err_trap(err_code as byte)
    print "OS Error: "; err_code 
end sub

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). Connect the cathodes via 100 ... 500 Ohm terminating resistors to the GND bus . Compile and upload the firmware to the board. LEDs will begin to switch asynchronously with a period of 2 and 0.66 s.

Let's look at the code. So, first we initialize the equipment (we set the compiler options, the mode of ports and assign aliases), then the OS itself, and finally the tasks.

Since the tasks just created are in the “stopped” state, you need to give them the status “ready for execution” (perhaps not all tasks in a real application - after all, some of them, according to the developer's intention, can initially be in a stopped state and run on execution 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 completed. Who will launch them? Of course, the dispatcher! To do this, we must call it at the first start of the system. Now, if everything is written correctly, the dispatcher will perform our tasks one by one, and we can end the main part of the program with the end statement.

Let's look at the tasks. It is immediately evident that each of them is framed as an endless do-loop . The second important property - inside 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 indicated to him the number of milliseconds for which each task should be paused.

If you set the constant OS_SIM = TRUE at the beginning of the code and run the code not on the real chip, but in the simulator, then you can trace how the OS works.

The dispatcher we called will see if the tasks with the status are “ready for execution” and line them up according to priority. If the priority is the same, then the dispatcher will "roll tasks on the carousel", moving the newly completed task to the very end of the queue.

Having selected the task to be executed (for example, task_1 ), the dispatcher replaces the return address (initially, he points to the end instruction in the main code) with the address of the task_1 entry point , which the system recognizes during the initialization of the task, and executes the return command , which forces MK to pull the return address from the stack and go to it - that is, start the execution of task_1 task (operatordo in task_1 code ).

The task task_1 , switching its LED, calls the OS_delay service , which, having completed the necessary actions, goes to the dispatcher.

The dispatcher saves the address that was on the stack to the task_1 task control unit (points to the instruction following the OS_delay call , that is, the loop instruction ), and then, “turning the carousel”, discovers that task_2 must now be completed . It pushes the task_2 task address onto the stack (it currently points to the do statement in the task_2 task code ) and executes the return command, which causes the MK to pull the return address from the stack and go to it - that is, start the execution of task_2 .

Task task_2 , switching its LED, calls the OS_delay service , which, after performing the necessary actions, goes to the dispatcher.

The dispatcher saves the address that was on the stack to the task_1 task control unit (points to the instruction following the OS_delay call , that is, the loop instruction ), and then, “turning the carousel”, discovers that task_2 must now be completed . The difference from the initial state will be that now in the task_1 task control blocknot the start address of the task is stored, but the address of the point from which the transition to the dispatcher occurred. There (to the loop statement in the task_1 task code ), and control will be transferred.

Task task_1 will execute the loop statement , and then the entire cycle "Task 1 - Dispatcher - Task 2 - Dispatcher" will be repeated endlessly.

Send messages


Now let's try sending messages from one task to another.

' начальные установки компилятора
config submode = old 
$include "..\aquaRTOS_1.05.bas"
$regfile = "m328pdef.dat"
$crystal = 16000000
$hwstack = 48
$swstack = 48
$framesize = 64
' объявление процедур
declare sub my_err_trap (byval err_code as byte)
declare sub task_1()
declare sub task_2()
const OS_SIM = TURE ' будем запускать этот код в симуляторе
' объявление переменных
dim hTopic as byte ' переменная для темы сообщений
dim task_1_cnt as byte ' счетчик для задачи 1
dim strMessage as string * 16 ' сообщение
' *** начало кода приложения ***
OS_CreateMessage hTopic
OS_Init my_err_trap
OS_InitTask task_1 , 1
OS_InitTask task_2 , 1
OS_ResumeTask task_1
OS_ResumeTask task_2
OS_Sheduler
end
' *** задачи ***
sub task_1()
    do
        print "task 1"
        OS_Sheduler
        incr task_1_cnt ' увеличим счетчик на 1
        if task_1_cnt > 3 then
            print "task 1 is sending message to task 2"
            strMessage = "Hello, task 2!"
            ' посылаем сообщение задаче 2
            OS_SendMessage hTopic , varptr(strMessage)         
            task_1_cnt = 0
        end if
    loop
end sub
sub task_2()
    do
        print "task 2 is waiting messages..."
        ' будем ждать сообщений от задачи 1
        OS_WaitMessage hTopic
        print "message recieved: " ; OS_GetMessageString (hTopic)
    loop
end sub
' ****************************************************
' обработчик ошибок
sub my_err_trap(err_code as byte)
    print "OS Error: "; err_code 
end sub

The result of running the program in the simulator will be the following output in 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


Pay attention to the order in which work and task switching occur. As soon as Task 1 prints 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 message waiting service on the hTopic topic , and control is automatically transferred to the dispatcher, which again calls Task 1. It 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 to Task 1 on the incr statement following the dispatcher call.
When the counter task_1_cntin Task 1, it will exceed the specified value, the task sends a message, but continues to execute - executes the loop statement and prints task 1 again . After that, she calls the dispatcher, who now discovers that there is a message for Task 2, and transfers control to her. Next, the process is performed cyclically.

Event handling


The following code polls two buttons and switches the LEDs when you press the corresponding button:

' начальные установки компилятора
config submode = old
$include "..\aquaRTOS_1.05.bas"
$regfile = "m328pdef.dat"
$crystal = 16000000
$hwstack = 48
$swstack = 48
$framesize = 64
' объявление процедур
declare sub my_err_trap (byval err_code as byte)
declare sub task_scankeys()
declare sub task_led_1()
declare sub task_led_2()
' зададим светодиодам порты и режим их работы
led_1 alias portd.4
led_2 alias portd.5
config portd.4 = output
config portd.5 = output
button_1 alias pind.6
button_2 alias pind.7
config portd.6 = input
config portd.7 = input
' объявление переменных
dim eventButton_1 as byte
dim eventButton_2 as byte
' *** начало кода приложения ***
eventButton_1 = OS_CreateEvent  ' создадим по событию на каждую кнопку
eventButton_2 = OS_CreateEvent
OS_Init my_err_trap
OS_InitTask task_scankeys , 1
OS_InitTask task_led_1 , 1
OS_InitTask task_led_2 , 1
OS_ResumeTask task_scankeys
OS_ResumeTask task_led_1
OS_ResumeTask task_led_2
OS_Sheduler
end
' *** задачи ***
sub task_scankeys()
   do
      debounce button_1 , 0 , btn_1_click , sub
      debounce button_2 , 0 , btn_2_click , sub
      OS_Sheduler
   loop
btn_1_click:
    OS_SignalEvent eventButton_1
return
btn_2_click:
    OS_SignalEvent eventButton_2
    return
end sub
sub task_led_1()
    do
        OS_WaitEvent eventButton_1
        toggle led_1
    loop
end sub
sub task_led_2()
    do
        OS_WaitEvent eventButton_2
        toggle led_2
    loop
end sub
' ****************************************************
' обработчик ошибок
sub my_err_trap(err_code as byte)
    print "OS Error: "; err_code
end sub

A real application example under AQUA RTOS



Let's try to imagine how a coffee vending machine program might look like. The machine should indicate the presence of coffee options and the choice of LEDs in the buttons; receive signals from the coin receiver, 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 data on equipment malfunctions and stock of ingredients and receive commands through remote access (for example, via a GSM modem), as well as signal vandalism.

Event Driven Approach


At first, it can be difficult for a developer to switch from the usual “supercycle + flags + interrupts” scheme to an approach based on tasks and events. This requires highlighting the basic tasks that the device should perform.
Let's try to outline such tasks for our machine:

  • heater control and management - ControlHeater ()
  • indication of availability and choice of drinks - ShowGoods ()
  • acceptance of coins / bills and their summation - AcceptMoney ()
  • polling buttons - ScanKeys ()
  • change delivery - MakeChange ()
  • drink leave - ReleaseCoffee ()
  • vandalism protection - Alarm ()

Let us estimate the degree of importance of the 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 performed too often, because the heater has a high inertia, and the water cools slowly. It is enough to check the temperature once a minute.
Give priority to this task 5. ShowGoods () is not too important. The offer may change only after the release of the goods, if the supply of any ingredients is exhausted. Therefore, we will give this task a priority of 8, and let it be executed when the machine starts up and every time the goods are released.
ScanKeys ()must have a high enough priority for the machine to respond quickly to button presses. Give this task priority 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 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 accepted and the drink selection button is pressed. For quick response, we give it priority 2.
Since vandal resistance is a rather important function of the machine, the Alarm () taskyou can set the highest priority - 1, and activate once a second to check the tilt or tamper sensors.

Thus, we will need seven tasks with different priorities. After the start, when the program read the settings from EEPROM and initialized the equipment, it is time to initialize the OS and start the tasks.

' объявление процедур задач
declare sub ControlHeater()
declare sub ShowGoods() 
declare sub AcceptMoney()
declare sub ScanKeys()
declare sub MakeChange ()
declare sub ReleaseCoffee()
declare sub Alarm()

To work as part of RTOS, each task must have a certain structure: it must have at least one call to the dispatcher (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 an infinite loop expects a message on the topic bCoffeeSelection and does nothing until it arrives (control is automatically returned to the dispatcher so that he can start other tasks). As soon as the message is sent, ReleaseCoffee () becomes ready for execution, and when this happens, the task receives the message content (code of the selected drink) wItem using the OS_GetMessage service and releases the goods to the customer. Since ReleaseCoffee () uses the message subsystem, a message must be created before starting multitasking:

dim bCoffeeSelection as byte
bCoffeeSelection = OS_CreateMessage()

As mentioned above, ShowGoods () must be executed once at start-up and every time when goods are released. To associate it with the release procedure ReleaseCoffee () , we use the event service. To do this, create an event before starting multitasking:

dim bGoodsReliased as byte
bGoodsReliased = OS_CreateEvent()

And in the ReleaseCoffee () procedure, after the line Release wItem we add an alarm about the bGoodsReliased event :

OS_SignalEvent bGoodsReliased

OS initialization


In order to prepare the OS for work, we must initialize it, indicating the address of the error handler, which is in the user code. We do this using the OS_Init service :

OS_Init Mailfuncion

In user code, you need to add a handler - a procedure whose byte argument is the error code:

sub Mailfuncion (bCoffeeErr)
    print "Mailfunction! Error #: "; bCoffeeErr
    if isErrCritical (bCoffeeErr) = 1 then 
        CallService(bCoffeeErr)
    end if
end sub

This procedure prints an error code (or can display it in some other way: on the screen, via a GSM modem, etc.), and in case the error is critical, it calls the service department.

Task launch


We already remember that events, semaphores, etc. must be initialized before being used. In addition, the tasks themselves must be initialized with 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 the tasks start is insignificant, 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 set them the status “ready for execution”:

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 necessarily start when multitasking is launched; some of them may at any time remain in a “stopped” state 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 is pre-initialized.

Multitasking start


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 care of the further execution of the code.

Let's look at the whole code:

' начальные установки компилятора
config submode = old
$include "..\aquaRTOS_1.05.bas"
$include "coffee_hardware.bas" ' файл с процедурами управления оборудованием
' процедуры в этом файле имеют префикс Coffee_
$regfile = "m328pdef.dat" ' Arduino Nano v3
$crystal = 16000000
$hwstack = 48
$swstack = 48
$framesize = 64
Coffee_InitHardware ' инициализация оборудования автомата
' объявление процедур
declare sub Mailfuncion (byval bCoffeeErr as byte) ' обработчик ошибок
declare sub ControlHeater () ' управление водонагревателем
declare sub ShowGoods () ' показать наличие товара
declare sub AcceptMoney () ' прием наличных
declare sub ScanKeys () ' опрос кнопок
declare sub MakeChange () ' выдача сдачи после отпуска товара
declare sub ReleaseCoffee () ' отпуск товара
declare sub Alarm () ' обеспечение безопасности автомата
' проведем начальные настройки оборудования
Coffee_InitHardware ()
' объявление переменных
dim wMoney as long ' счетчик введенных денег
dim wGoods as long ' номер товара
' *** начало кода приложения ***
' инициализация ОС
OS_Init Mailfuncion 
' создадим тему сообщения о выборе напитка
dim bCoffeeSelection as byte
bCoffeeSelection = OS_CreateMessage() 
' создадим событие отпуска товара
dim bGoodsReliased as byte
bGoodsReliased = OS_CreateEvent() 
' инициализация задач
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
' подготовка задач к выполнению
OS_ResumeTask ControlHeater 
OS_ResumeTask ShowGoods
OS_ResumeTask AcceptMoney 
OS_ResumeTask ScanKeys
OS_ResumeTask MakeChange
OS_ResumeTask ReleaseCoffee
OS_ResumeTask Alarm
' запуск ОС
OS_Sheduler
end
' *** код задач ***
' -----------------------------------
sub ControlHeater()
    do
        select case GetWaterTemp()
            case is > 97 
                Coffee_HeaterOff ' выключить нагреватель
            case is < 95
                Coffee_HeaterOn ' включить нагреватель
            case is < 5
                CallServce (WARNING_WATER_FROZEN) ' угроза замерзания 
        end select
        OS_Delay 60000 ' ждать 1 минуту
    loop
end sub
' -----------------------------------
sub ShowGoods()
    do
        LEDS = Coffee_GetDrinkSupplies() ' установить состояние порта D,
        ' к которому подключены светодиоды индикации наличия товаров и
        ' ассоциирована переменная LEDS 
         OS_WaitEvent bGoodsReliased ' ожидать события "отпуск товара"
    loop
end sub
' -----------------------------------
sub AcceptMoney()
    do
        wMoney = wMoney + ReadMoneyAcceptor()
        OS_Delay 20
    loop
end sub
' -----------------------------------
sub ScanKeys()
    do
        wGoods = ButtonPressed()
        if wMoney >= GostOf(wGoods) then
            OS_SendMessage bCoffeeSelection, wGoods
            ' отправляет сообщение на тему bCoffeeSelection, которое
            ' содержит код выбранного товара
        end if
        OS_Delay 40
    loop
end sub
' -----------------------------------
sub MakeChange()
    do
        OS_WaitEvent bGoodsReliased ' ожидать события "отпуск товара"
        Refund wMoney
    loop
end sub
' -----------------------------------
sub ReleaseCoffee()
    do
        OS_WaitMessage bCoffeeSelection 'ждать сообщения bCoffeeSelection
        wItem = OS_GetMessage(bCoffeeSelection) ' прочесть сообщение
        Release wItem ' отпустить выбранный товар
        wMoney = wMoney – CostOf (wItem) ' уменьшить на цену товара
        OS_SignalEvent bGoodsReliased ' просигналить об этом задачам
        ' обратите внимание, что это событие могут ждать две задачи:
        ' MakeChange и ShowGoods
        ' обе они, получив сообщение, становятся готовыми к исполнению 
    loop
end sub
' -----------------------------------
sub Alarm() 
    do
        OS_Delay 1000
        if Hijack() = 1 then 
            CallPolice()
        end if
    loop
end sub 
' -----------------------------------
' *** обработчик ошибок ОС ***
sub Mailfuncion (bCoffeeErr)
    print "Mailfunction! Error #: "; bCoffeeErr
    if isErrCritical (bCoffeeErr) = 1 then 
        CallService()
    end if
end sub

Of course, an approach would be more correct, not with periodic polling of buttons and a cash sensor, but with the use of interrupts. In the handlers of these interrupts, we could use the sending of messages using the OS_SendMessage () service with contents equal to the number of the pressed key or the value of the entered coin / bill. I invite the reader to modify the program on their own. 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 of version 1.05 is available for download here

P.S


Q: Why AQUA?
A: Well, I did the aquarium controller, it’s like a “smart home”, not just for people, but for fish. Full of all sorts of sensors, a real-time clock, relay and analog power outputs, an on-screen menu, a flexible "event program" and even a WiFi module. The intervals should be counted, the buttons should be polled, the sensors should be processed, the event program should be read from EEPROM and executed, the screen should be updated, the Wi-Fi should respond. Moreover, the controller must go to a multi-level menu for settings and programming. To do on flags and interrupts is just to get the very “pasta code”, which is neither understood nor modified. Therefore, I decided that I needed an OS. Here she is AQUA.

Q: Surely the code is full of logical errors and glitches?
A: Surely. I, as I could, came up with a variety of tests and drove the OS on a variety of tasks, and even slammed a noticeable number of bugs, but this does not mean that everything is complete. I’m more than sure that there are still a lot of them in the back streets of the code. Therefore, I will be very grateful if, instead of poking me in the bugs in the face, you politely and tactfully point to them, and better tell me how you think it is better to fix them. It will also be great if the project is further developed as a product of collective creativity. For example, someone will add a service for counting semaphores (didn’t forget? - I'm a lazy ass) and offer other improvements. In any case, I will be very grateful for the constructive contribution.

Also popular now: