===== FAQ по ОСРВ OSA =====
Здесь я собрал ответы на самые распространенные вопросы по OSA.
[[osa:articles:rtos_usage#Можно ли вызывать сервисы ожидания из функций, вызываемых задачами?|Можно ли вызывать сервисы ожидания из функций, вызываемых задачами?]]\\ 
[[osa:articles:rtos_usage#Ожидание события в цикле|Ожидание события в цикле]]\\ 
[[osa:articles:rtos_usage#Можно ли создать задачу по указателю на функцию?|Можно ли создать задачу по указателю на функцию?]]\\ 
[[osa:articles:rtos_usage#Что будет, если отсылать/принимать сообщения из неинициализированной очереди?|Что будет, если отсылать/принимать сообщения из неинициализированной очереди?]]\\ 
[[osa:articles:rtos_usage#Изменение типов сообщений|Изменение типов сообщений]]\\ 
[[osa:articles:rtos_usage#Если две задачи ожидают одного и того же события|Если две задачи ожидают одного и того же события]]\\ 
[[osa:articles:rtos_usage#Модификация тела сообщения до того, как оно будет принято|Модификация тела сообщения до того, как оно будет принято]]\\ 
[[osa:articles:rtos_usage#Использование таймеров вне задач|Использование таймеров вне задач]]\\ 
[[osa:articles:rtos_usage#Почему виснет OS_Delay?|Почему виснет OS_Delay?]]
----
==== Можно ли вызывать сервисы ожидания из функций, вызываемых задачами? ====
**Ответ: нет.**
Этот вопрос, пожалуй, задают чаще остальных. Такой подход, действительно, выглядит очень заманчивым, когда, например, в задачах часто вызывается сервис OS_Delay  с одним и тем же параметром:
    // В этом примере просто напрашивается вынос OS_Delay(10) в отдельную функцию.
    // Сам вызов сервиса занимает около 10 слов ROM, и его, конечно, хочется 
    // заменить одним вызовом.
    ...
    OS_Delay(10);
    ...
    if (...) OS_Delay(10);
    ...
    do {
        ...
        OS_Delay(10);
        ...
    } while (...);
    ...
или когда есть несколько задач, ожидающих одно и то же событие:
    ...
    // Ждем, когда освободится доступ в EEPROM
    OS_Bsem_Wait(BS_EEPROM_FREE);
    ...
К сожалению в ОСРВ OSA такое недопустимо. Дело в том, что при таком подходе произойдет путаница с адресами возврата в стеке. Рассмотрим на примере:
//-----------------------------------------------
void Delay10 (void)
{
    OS_Delay(10);
}
//-----------------------------------------------
void Bsem_Wait (void)
{
    OS_Bsem_Wait(BS_BINSEM);
}
//-----------------------------------------------
void TaskA (void)
{
    for (;;) 
    {
        Delay10();
        /*...*/
        Delay10();
        /*...*/
        Delay10();
        /*...*/
    }
}
//-----------------------------------------------
void TaskB (void)
{
    for (;;) 
    {
        Bsem_Wait();
        /*...*/
        Bsem_Wait();
        /*...*/
        Bsem_Wait();
        /*...*/
    }
}
//-----------------------------------------------
Итак, у нас есть две задачи: TaskA и TaskB. Обе вызывают разные функции, в каждой из которых есть сервис, содержащий код возврата в планировщик (в перечне сервисов все такие сервисы в примечании отмечены буквой "T"). Для простоты рассмотрим работу этого примера на PIC16. //(Примечание: планировщик в этих контроллерах передает управление задачам присвоением адреса задачи паре регистров PCLATH:PCL, а задачи возвращают управление планировщику, совершая переход на него по GOTO. Таким образом экономится стек, т.к. ни задачи, ни планировщик не вызываются через CALL)//. При выполнении программы возможна такая последовательность:
^ . ^  Действие  ^  Передача управления  ^  Стек  ^
| 1 | Планировщик запускает задачу **TaskA**  | PCLATH:PCL=TaskA | - |
| 2 | Задача **TaskA** вызывает функцию **Delay10()**| CALL Delay10. | **ret_addr_A**\\ - |
| 3 | Функция **Delay10** вызывает системный сервис **OS_Delay**, который, инициализировав задержку, передает управление планировщику | GOTO sched | **ret_addr_A**\\ - |
| 4 | Планировщик запускает задачу **TaskB** | PCLATH:PCL=TaskB | **ret_addr_A**\\ - |
| 5 | Задача **TaskB** вызывает функцию **Bsem_Wait()** | CALL Bsem_Wait | **ret_addr_B**\\ **ret_addr_A**\\ - |
| 6 | Функция **Bsem_Wait** вызывает системный сервис **OS_Bsem_Wait**, который передает управление планировщику | GOTO sched | **ret_addr_B**\\ **ret_addr_A**\\ - |
| 7 | Планировщик крутится вхолостую, пока идет задержка, запущенная в задаче **TaskA** и пока не установлен семафор, которого ожидает задача **TaskB** | |**ret_addr_B**\\ **ret_addr_A**\\ - |
| 8 | Задержка закончилась, плнировщик передает управление задаче **TaskA** в то же место, откуда был возврат в планировщик, а именно - в середину функиции **Delay10** | PCLATH:PCL=Delay10 |**ret_addr_B**\\ **ret_addr_A**\\ - |
| 9 | И теперь - кульминация: функция делает возврат, при котором из стека берется последний положенный туда адрес, а именно - **ret_addr_B** | RETURN | **ret_addr_A**\\ - |
Как видно, после 9-ой операции мы из функции **Delay10** вернемся в функцию-задачу **TaskB**, хотя должны были вернуться в **TaskA**.
//**Примечание.** В принципе, такой подход допускается, если у программиста есть уверенность в том, что в один момент времени только одна задача производит вызов такой функции. Однако, здесь надо быть крайне осторожным и хорошо понимать, что он делает. Поэтому, если Вы не уверены, что уследите за вызовами при дальнейшем росте программы, - не применяйте такой прием!//
([[osa:articles:rtos_usage#FAQ по ОСРВ OSA|Вернуться к списку FAQ]])
----
==== Ожидание события в цикле ====
Некоторые из присланных мне программ содержали однотипную ошибку, которую я бы хотел здесь обрисовать. Иногда бывает так, что в ходе ожидания какого-либо события требуется выполнять какое-то действие. Поэтому код этого ожидания некоторые писали без использования сервисов OS_xxx_Wait, заменяя их циклом do {...} while. Рассмотрим отвлеченный пример: пока ожидаем установки какого-то двоичного семафора, нам нужно сравнивать напряжения на двух входах АЦП и, в зависимости от результата сравнения, зажигать либо красный либо зеленый светодиод. 
Код такого ожидания выглядел так:
    do 
    {
        if (ADC_Read(0) > ADC_Read(1))    // Сравниваем напряжения на двух аналоговых входах
        {
            GREEN_LED = 1;
            RED_LED = 0;
        } else {
            GREEN_LED = 0;
            RED_LED = 1;
        }
        OS_Yield();                       // Возврат в планировщик
    } while (!OS_Bsem_Check(BS_START));
Ошибка такого подхода заключается в том, что задача, крутясь в таком цикле, является **всегда готовой** к выполнению. И если в программе есть задачи с более низким приоритетом, то они не смогут получить управление до тех пор, пока эта задача не дождется семафора. А если ожидаемый ей семафор должна установить как раз задача с более низким приоритетом, то программа просто зависнет в вечном ожидании. 
Как быть в таких случаях? Здесь есть несколько вариантов решения этой коллизии.
== 1. Вынести выполняемый при ожидании код в отдельную низкоприоритетную задачу ==
С точки зрения концепции ОСРВ такой способ самый правильный. В данном конкретном примере сравнение напряжений на входах АЦП и ожидание семафора - функционально разные действия и нет никакого смысла выполнять их одновременно.
OST_TASK_POINTER tp;
/******************************************************************************/
//  Отдельная задача для работы с АЦП и светодиодами
/******************************************************************************/
void Task_ADC_Leds (void)
{
   tp = OS_GetCurTask();
   for (;;) 
   {
        /*...*/
        /* Здесь сравниваем напряжения */
        /*...*/
        OS_Yield();                       // Возврат в планировщик
   }
}
/******************************************************************************/
//  Наша задача
/******************************************************************************/
void Task (void)
{
    for (;;)
    {
        /*...*/
        // Перед ожиданием создаем задачу сравнения напряжений
        OS_Task_Create(7, Task_ADC_Leds);
        
        // Ждем наш семафор
        OS_Bsem_Wait(BS_START);
        // Удаляем задачу сравнения напряжений
        OS_Task_Delete(tp);
        /*...*/
    }
}
Но при своей правильности этот подход не всегда оправдан, т.к. требует наличие свободного дескриптора на момент создания новой задачи, дополнительной глобальной переменной и времени на создание/удаление задачи.
== 2. Понизить приоритет на время ожидания ==
Понизив приоритет до минимального, мы исключаем, что какая-либо задача окажется блокированной. С точки зрения ресурсов контроллера я бы назвал такой подход оптимальным.
    static char prio;
    /*...*/
    prio = OS_Task_GetPriority(this_task);   // Запоминаем текущий приоритет задачи
    OS_Task_SetPriority(this_task, 7);       // Понижаем приоритет до минимального
    do {
        /*...*/
        /* Здесь сравниваем напряжения */
        /*...*/
        OS_Yield();
    } while (!OS_Bsem_Check(BS_START));
    OS_Task_SetPriority(this_task, prio);    // После цикла восстанавливаем сохраненный 
                                             // приоритет
//Примечание: рекомендуется понижать приоритет не до самого низкого (7-го), а до предпоследнего (6-го), т.к.низший приоритет удобно использовать для задачи SLEEP'а.//
== 3. Вставить в цикл небольшую задержку ==
Заменив OS_Yield() на OS_Delay(1), мы гарантировано на время одного системного тика ставим задачу в режим ожидания (время задержки можно увеличить, если одного тика мало):
    do {
        /*...*/
        /* Здесь сравниваем напряжения */
        /*...*/
        OS_Delay(1);
    } while (!OS_Bsem_Check(BS_START));
Недостатком такого способа будет увеличение периода сравнения напряжений. Возможно, в данном примере это не страшно, но в другом случае, если операции внутри цикла критичны ко времени, это может отрицательно сказаться на логике работы устройства.
([[osa:articles:rtos_usage#FAQ по ОСРВ OSA|Вернуться к списку FAQ]])
----
==== Можно ли создать задачу по указателю на функцию? ====
**Ответ: да**
В OSA есть два недокументированных сервиса, которые позволяют это сделать. Я их не стал описывать в общей документации, чтобы не вносить путаницы в логику работы задач. Тем не менее, такой способ создания задач может оказаться очень удобным в пользовательских приложениях, где адрес функции-задачи привязан, например, к пункту меню. Предположим, у нас определен тип структуры, содержащей название пункта меню и адрес задачи, которую нужно будет выполнять по этому пункту:
typedef struct 
{
    const char* strMenu;
    void  (*Func)(void);
} TMenuItem;
Далее в программе определен массив этих структур:
const TMenuItem UserMenu[] = {
    {"Load", Task_Load},
    {"Save", Task_Save},
    {"View", Task_View}
    {"Edit", Task_Edit}
};    
Все эти задачи описываются как обычно, например:
void Task_Load (void)
{
    for (;;) 
    {
        /*...*/
    }
}
Далее - одна тонкость. Чтобы компилятор правильно строил дерево вызовов подпрограмм, ему нужно указать, что функции, которые мы перечислили в массиве, являются задачами. Для этого в main() нужно для каждой такой функции вызвать сервис OS_Task_Reserve:
void main (void)
{
    /*...*/
    OS_Task_Reserve(Task_Load);
    OS_Task_Reserve(Task_Save);
    OS_Task_Reserve(Task_View);
    OS_Task_Reserve(Task_Edit);
    /*...*/
}
После этого компилятор будет знать, что эти функции косвенно вызываются из main, и правильно распределит локальные переменные этих функций.
Теперь в произвольном месте программы можно создавать задачи по указателю на функцию, пользуясь специальным сервисом OS_Task_Create**P**:
    LCD_Out(UserMenu[i].strMenu);        // Выводим на экран название функции
    OS_Task_CreateP(0, UserMenu[i].Func); // Создаем задачу с высшим приоритетом
([[osa:articles:rtos_usage#FAQ по ОСРВ OSA|Вернуться к списку FAQ]])
----
==== Что будет, если отсылать/принимать сообщения из неинициализированной очереди? ====
**Ответ: ничего хорошего**
Это относится не только к неинициализированной очереди, но и к неинициализированным: счетным семафорам, коротким сообщениям, указателям на сообщения. Неизвестно, что содержат в себе эти переменные на момент обращения к ним. Поэтому нужно всегда следить за тем, чтобы эти объекты ОС инициализировались **до** первого обращения к ним.
Рекомендую проанализировать работу следующего примера:
OST_QUEUE q;
OST_MSG   smsg;
OST_MSG   rmsg;
void Task1 (void)
{
    OS_Queue_Create(q);          // Создаем очередь (инициализируем)
    for (;;)
    {
        OS_Queue_Send(q, smsg);  // Отсылаем сообщение в очередь
    }
}
void Task2 (void)
{
    for (;;)
    {
        OS_Queue_Wait(q, rmsg);  // Ожидаем сообщение из очереди
    }
}
void main (void)
{
    OS_Init();
    OS_Task_Create(1, Task1);
    OS_Task_Create(0, Task2);
    OS_Run();
}
Обратите внимание на расстановку приоритетов: приоритет задачи Task2 выше, чем Task1. Это означает, что первой выполнится именно задача Task2, т.е. та, которая ожидает сообщение из очереди, а задача, инициализирующая очередь, запустится второй (если OS_Queue_Wait не вызовет сбой программы). При всей очевидности выхода из ситуации (а именно - правильной расстановке приоритетов), допустить такую ошибку очень просто. Для неприоритетного режима - вообще неизвестно, какая задача запустится первой. Поэтому очереди (и все остальные объекты, требующие инициализации) следует инициализировать так, чтобы на момент обращения к ним они гарантировано были инициализированны. Тривиальный способ - создавать их в функции main() до вызова сервиса OS_Run().
([[osa:articles:rtos_usage#FAQ по ОСРВ OSA|Вернуться к списку FAQ]])
----
==== Изменение типов сообщений ====
OSA имеет два вида сообщений: указатель на сообщение и короткое однобайтовое сообщение. Различаются они тем, что с помощью первого можно передавать любой объем информации, т.к. фактически передается только указатель на нее, а с помощью второго - только одно значение (по умолчанию это значения от 1 до 255). Учитывая архитектурные особенности PIC-контроллеров, программистам оставлена возможность изменять типы этих сообщений. 
Сначала поговорим о типе **указателя на сообщение**. По умолчанию указатель на сообщение имеет тип **void* **, т.е. указатель на область RAM-памяти. Учитывая, что ядро PIC-контроллера построены по гарвардской архитектуре (раздельные шины адреса для памяти данных и программы), указатели на данные в ОЗУ и указатели на константы, хранящиеся в программной памяти, - это разные вещи. Поэтому ОСРВ OSA предоставляет программисту возможность на этапе написания программы выбрать тип указателей на сообщения. Этот тип нужно указать в файл конфигурации osacfg.h:
#define OS_MSG_TYPE    const char *
в этом примере мы заменяем тип указателя на сообщение так, что сможем в программе обмениваться строковыми константами.
OST_MSG_CB  msg_cb;        // Дескриптор сообщения
const char * MenuStrings[] = {"Load", "Save", "Save as...", "Exit"};
void Task_Menu (void)
{
    for (;;)
    {
        for (i = 0; i < 4; i++) 
            OS_Msg_Send(msg_cb, MenuString[i]);
        /*...*/
    }
        
}
//Примечание: в программе может быть применен только один тип для указателей на сообщения, и он не может меняться в ходе выполнения программы. (Исключение составляют указатели в HT-PICC18, когда указан ключ компиляции -CP24.)//
Теперь об одной ошибке, вернее, о некоторой некорректности использования возможности замены типа указателя на сообщения. Несколько раз в программах видел такое:
typedef struct {
    char*   name;
    int     age;
    int     weight;
} TMyStruct;
#define OS_MSG_TYPE   TMyStruct *
Т.е. программист создает некую свою структуру и тип сообщения заменяет указателем на нее. Ошибки здесь, конечно, нет. Но концептуально такой подход довольно спорный. За год программист может написать одну программу, а может и 10, и 20. Каждая программа может быть индивидуальна, и данные, которыми будут обмениваться задачи, - тоже. Если в каждой программе подменять тип указателя на сообщение каким-то специфичным указателем вместо void*, то возникнет некоторая неразбериха, да и проблемы с переносом модулей. Если предполагается работать с указателями на эти структуры, расположенные в RAM-области памяти, то лучше оставить тип void*. Это же замечание касается указателей на структуры, расположенные в ROM-области памяти - их лучше определять как **const void* **.
Теперь два слова о изменении типа **короткого сообщения**. Для чего оно вообще сделано? Две причины: экономия RAM и повышение скорости. Довольно часто между программами нужно обмениваться незначительными объемами информации: "нажата кнопка 5", "переключиться в режим 3", "зажечь светодиод 12". Преимущества перед указателями на сообщения:
  * если бы мы использовали для передачи такой информации указатели на сообщения, нам бы пришлось тратить 2 байта ОЗУ (3 - для PIC18, 4 для PIC24 и dsPIC) на дескриптор сообщения и 1 байт на тело сообщения, куда будет указывать дескриптор, итого - 3 байта. При использовании короткого сообщения нам понадобится всего 1 байт. 
  * при приеме указателя на сообщение программе потребуется сперва сформировать указатель в FSR, а только потом уже получить доступ к информации. При приеме же короткого сообщения мы получаем информацию напрямую.
  * для указателей на сообщения нельзя менять содержимое отправленных данных, пока задача-приемник не получила сообщение. При использовании короткого сообщения это ограничение снимается, т.к. все сообщение помещается в дескриптор.
По умолчанию короткое сообщение имеет тип **unsigned char**. Этот тип может быть заменен на любой перечислимый тип (int, long, float, bit) заданием константы в файле osacfg.h:
#define OS_SMSG_TYPE   unsigned long
Такое, хоть и редко, но бывает нужно. 
У короткого сообщения есть две особенности, которые нужно учитывать при проектировании программы: 
  - это сообщение может иметь только перечислимый тип (нужно учитывать при замене типа); т.е. этот тип нельзя переопределить на структуру, поскольку запись значения в короткое сообщение и чтение из него выполняется оператором присваивания ( = );
  - через него нельзя передавать нулевое значение. Этим приходится платить за использование такого эффективного инструмента. Нулевое значение рассматривается системой как отсутствие сообщения.
([[osa:articles:rtos_usage#FAQ по ОСРВ OSA|Вернуться к списку FAQ]])
----
==== Если две задачи ожидают одного и того же события. ====
При написании программы с использованием ОСРВ OSA нужно учитывать одну особенность ее планировщика. При поиске лучшей готовой задачи для выполнения планировщик в цикле пробегается по всем дескрипторам, проверяет готовность задач и сравнивает их приоритеты. Управление получит готовая задача с высшим приоритетом. Если есть несколько задач с одинаковым приоритетом, то управление получит та, которая была рассмотрена планировщиком раньше. Дескрипторы задач хранятся в массиве, который планировщиком рассматривается как кольцевой. Каждый раз он начинает поиск готовой задачи со следующей после последней выполненной. Например, у нас 5 задач. Последней задачей выполнялась 3-я. Тогда порядок проверки задач при следующей работе планировщика будет таким: 4, 5, 1, 2, 3. 
Проблема в том, что если две задачи ждут одного и того же события, например, семафора, который устанавливается третьей задачей, то одна из задач никогда не получит управление. Рассмотрим пример:
/******************************************************************************/
//  Задача, устанавливающая семафор
/******************************************************************************/
void Task1 (void)
{
    for (;;) {
        OS_Bsem_Set(bsem);
        OS_Delay(10);
    }
}
/******************************************************************************/
//  Первая задача, ожидающая семафор
/******************************************************************************/
void Task2 (void)
{
    for (;;) {
        OS_Bsem_Wait(bsem);
        /*...*/
    }
}
/******************************************************************************/
//  Вторая задача, ожидающая семафор
/******************************************************************************/
void Task3 (void)
{
    for (;;) {
        OS_Bsem_Wait(bsem);
        /*...*/
    }
}
/******************************************************************************/
//  
/******************************************************************************/
void main (void)
{
    OS_Init();
    OS_Task_Create(1, Task1);   // Все задачи с равными приоритетами
    OS_Task_Create(1, Task2);
    OS_Task_Create(1, Task3);
    OS_Run();
}
Последовательность просмотра задач планировщиком будет такова: Task1, Task2, Task3, Task1, Task2, Task3, ... . Проблема в том, что Task2 и Task3 не получают управления до тех пор, пока не будет установлен двоичный семафор, а устанавливается он только в Task1. Поэтому всегда будет происходить одна и та же последовательность: 
  - **Task1** устанавливает семафор **bs**;
  - **Task2** и **Task3** становятся готовыми к выполнению;
  - после работы **Task1** планировщик начинает поиск задачи, начиная со следующей после последней выполненной, т.е. начиная с **Task2**;
  - планировщик рассматривает **Task2**, видит, что она готова; лучший приоритет пока не выбран, поэтому лучшим на данный момент планировщик будет считать 1 (приоритет задачи Task2). Планировщик ее запоминает как кандидата на запуск;
  - планировщик рассматривает **Task3**, видит, что она готова, но ее приоритет не лучше, чем текущий лучший, поэтому он эту задачу пропускает;
  - планировщик рассматривает **Task1**, видит, что она не готова к выполнению (она в задержке);
  - управление передается задаче **Task2**;
  - **Task2** сбрасывает семафор (сервис OS_Bsem_Wait делает это автоматически), отрабатывает какие-то свои действия и возвращается в планировщик;
  - планировщик начинает просмотр списка задач, начиная с **Task3**, но уже видит, что семафор сброшен, поэтому **Task3** опять ставится в ожидание;
  - следующей управление получит **Task1** по истечении времени задержки, а потом все начнется сначала.
Как тут быть? Единого способа решения нет. Можно ретранслировать семафор дальше, т.е. дождавшись его в Task2, сразу же установить его, чтобы и Task3 смогла получить управление. Но это не лучший вариант, т.к. неизвестно, сколько задач в цепочке, и на какой нужно останавливать установку семафора. Можно использовать счетный семафор, но опять же нужно знать, сколько задач его ожидают, чтобы установить в нем правильное число. На мой взгляд, самым удобным в таком случае будет использование флагов. В задаче-отправителе устанавливать все биты флага:
    /*...*/
    OS_Flag_Set_1(flag, 0xFF);
    /*...*/
а в задачах-приемниках ожидать только своего.
    /* В задаче Task2 */
    OS_Flag_Wait(flag, 0x01);
    OS_Flag_Set_0(flag, 0x01);
    /* В задаче Task3 */
    OS_Flag_Wait(flag, 0x02);
    OS_Flag_Set_0(flag, 0x02);
В общем, способы решения есть, а какой применять, - на усмотрение программиста. **Главное - надо помнить об этой особенности планировщика.**
([[osa:articles:rtos_usage#FAQ по ОСРВ OSA|Вернуться к списку FAQ]])
----
==== Модификация тела сообщения до того, как оно будет принято ====
В одной присланной мне программе была допущена такая ошибка: задача формировала тело сообщения, отправляла другой задаче указатель на него, а потом сразу же модифицировала для отправки следующего. Выглядело это так:
OST_MSG_CB msg_cb;
void Task (void)
{
    static char buf[3];
    for (;;) {
       // Формируем первое сообщение
       buf[0] = '1';
       buf[1] = '2';
       buf[2] = '3';
       OS_Msg_Send(msg_cb, buf);
       OS_Yield();
       // Формируем второе сообщение
       buf[0] = '5';
       buf[1] = '6';
       buf[2] = '7';
       OS_Msg_Send(msg_cb, buf);
       OS_Yield();
       
       /*...*/
   }
}
Не смотря на то, что после отправки сообщения выполняется OS_Yield, чтобы дать возможность адресату получить сообщение, оно может и не доставиться с первого раза (по любой причине: задача-приемник чем-то занята, или имеет низкий приоритет, или, наконец, находится в режиме паузы). Сервис OS_Msg_Send устроен так, что он не сможет отправить сообщение до тех пор, пока предыдущее не получено адресатом. Тем не менее, следует помнить, что этот сервис следит только за дескриптором сообщения, а не за областью памяти, где фактически расположено тело. 
Что произойдет в нашем примере, если после первого OS_Yield задача-адресат не успеет принять отправленное ей сообщение? Фактически ей отправляется только адрес области памяти, где располагается массив buf, в котором на момент отправки лежат значения '1', '2' и '3'. Итак, после выполнения OS_Yield сообщение не было принято. Планировщик возвращает управление задаче отправителю, и она продолжает свое выполнение с того места, откуда вышла, т.е. со следующей строки после OS_Yield. Здесь у нас происходит замена элементов буфера buf на '5', '6' и '7'. После этого задача Task пытается отправить очередное сообщение, но так как предыдущее еще не получено адресатом, то эта задача становится в ожидание, когда дескриптор освободится. 
По прошествии какого-то времени адресат получит сообщение и освободит дескриптор, но в теле сообщения будут уже новые значения (не "123", а "567"). Задача отправитель же, дождавшись освобождения дескриптора, отсылает следующее сообщение; фактически - это тот же указатель на тот же участок памяти. И задача-адресат второй раз подряд получит "567".
Как этого избежать? Всего-навсего перед формированием нового сообщения в том же буфере нужно проверять, было ли предыдущее сообщение доставлено.
       // Формируем первое сообщение
       buf[0] = '1';
       buf[1] = '2';
       buf[2] = '3';
       OS_Msg_Send(msg_cb, buf);
       OS_Cond_Wait(!OS_Msg_Check(msg_cb));   // Ставим задачу в режим ожидания до тех пор,
                                              // пока сообщение не будет принято
       // Формируем второе сообщение
       buf[0] = '5';
       buf[1] = '6';
       buf[2] = '7';
       OS_Msg_Send(msg_cb, buf);
       OS_Yield();
([[osa:articles:rtos_usage#FAQ по ОСРВ OSA|Вернуться к списку FAQ]])
----
==== Использование таймеров вне задач ====
Как статические, так и динамические таймеры могут использоваться в любом месте программы (за исключением сервисов Wait и Delay). Это позволяет:
**1. В любом месте программы ожидать какого-то условия с выходом по таймауту.**
char MyFunc1 (void)
{
    /*...*/
    OS_Stimer_Run(0, 10);                  // Запускаем статический таймер 0 
                                           // на отсчет 10 системных тиков
    while (!RB0 && !OS_Stimer_Check(0))    // Ожидаем установки RB0
        continue;                          
    /*...*/
}
**2. Формировать задержку внутри фоновых функций (не задач).**
void MyFunc2 (void)
{
    /*...*/
    OS_Stimer_Run(0, 10);                  // Запускаем статический таймер 0 
                                           // на отсчет 10 системных тиков
    while (!OS_Stimer_Check(0)) continue;  // Ожидаем конца счета
    /*...*/
}
**3. Выделять квант времени для работы какой-либо функции.**
char buffer[10];
void Task (void)
{
    static OST_DTIMER dt;
    OS_Dtimer_Create(dt);                   // Инициализируем таймер
    for (;;)
    {
        OS_Dtimer_Run(dt, 50);              // Выделяем для функции Receive квант 
                                            // времени в 50 тиков
        if (Receive(&dt) == 1)
        {    
            /*...*/ 
        }
        /*...*/
    }
}
char Receive (OST_DTIMER *dt)
{
    static bit b;
    do 
    {
        b = GetBit();                       // Принимаем бит данных
        ShiftBit(b);                        // Вдвигаем его в буфер
        if (CheckSumOK()) return 1;         // Проверяем контрольную сумму
    } while (!OS_Dtimer_Check(*dt));        // Висим в цикле, пока таймер не досчитает
    // Попали сюда, значит ничего не приняли за отведенное время
    return 0;
}
([[osa:articles:rtos_usage#FAQ по ОСРВ OSA|Вернуться к списку FAQ]])
~~UP~~
----
==== Почему виснет OS_Delay? ====
"Почему после вызова OS_Delay() задача подвисает, значение таймеров не изменяется, хотя в osacfg.h определена константа OS_ENABLE_TTIMERS?". Как ни странно, вопрос довольно частный.
**Ответ: счет таймеров выполняет сервис OS_Timer, который должен периодически вызываться.**
~~UP~~