====== OSA : Учебник. Урок 2 - Локальные переменные ======
* **[[osa:tutorial:intro|К оглавлению]]**
===== Тема =====
Разбирая этот урок, мы убедимся в том, что переменные внутри функций-задач нужно объявлять как static. На простом примере будет показано, к чему может привести пренебрежение этим правилом.
===== Проект =====
Создадим проект, следуя инструкциям, описанным в [[osa:tutorial:tutor1|первом уроке]], только создадим его в папке "c:\tutor\t2" и назовем файл "tutor2.c".
===== Описание задач =====
Текст нашей второй программы будет очень похож на текст первой: те же две задачи, то же переключение с помощью сервиса ##[[osa:ref:allservices:OS_Yield|OS_Yield]]##().
char m_cTest1;
char m_cTest2;
void Task_T1 (void)
{
char cTemp1;
for (;;)
{
cTemp1 = 1; // Значение этой переменной будет потеряно
// после передачи управления системе.
OS_Yield();
m_cTest1 = cTemp1;
}
}
void Task_T2 (void)
{
char cTemp2;
for (;;)
{
cTemp2 = 2; // Значение этой переменной будет потеряно
// после передачи управления системе.
OS_Yield();
m_cTest2 = cTemp2;
}
}
Переменные m_cTest1 и m_cTest2 сделаны глобальными для того, чтобы их было удобнее отслеживать при отладке в симуляторе. В каждой задаче объявлено по одной локальной переменной. Цель данного урока - продемонстрировать, что значения локальных переменных могут быть потеряны (затерты) после передачи управления ядру операционной системы.
Допишем к программе функцию main(), которая:
* вызовет сервис инициализации ОС;
* объявит системе, что Task_T1 и Task_T2 являются функциями-задачами;
* запустит планировщик на выполнение.
void main (void)
{
OS_Init();
OS_Task_Create(3, Task_T1);
OS_Task_Create(3, Task_T2);
OS_Run();
}
~~UP~~
===== Полный текст =====
Итак, полный текст нашей программы теперь будет выглядеть так:
#include
//******************************************************************************
// Глобальные переменные
//******************************************************************************
char m_cTest1;
char m_cTest2;
//******************************************************************************
// Функции-задачи
//******************************************************************************
void Task_T1 (void)
{
char cTemp1;
m_cTest1 = 0;
for (;;)
{
cTemp1 = 1; // Значение этой переменной будет потеряно
// после передачи управления системе.
OS_Yield();
m_cTest1 = cTemp1;
}
}
//------------------------------------------------------------------------------
void Task_T2 (void)
{
char cTemp2;
m_cTest2 = 0;
for (;;)
{
cTemp2 = 2; // Значение этой переменной будет потеряно
// после передачи управления системе.
OS_Yield();
m_cTest2 = cTemp2;
}
}
//******************************************************************************
// main
//******************************************************************************
void main (void)
{
OS_Init(); // Инициализация переменных системы
OS_Task_Create(3, Task_T1); // Добавление задач в список
OS_Task_Create(3, Task_T2);
OS_Run(); // Запуск планировщика
}
//******************************************************************************
// end of file
//******************************************************************************
~~UP~~
===== Как работает программа =====
Порядок переключения между задачами идентичен порядку, описанному в первом уроке. Разница только в содержимом функций-задач: если в первом уроке мы при каждом запуске задачи увеличивали переменную-счетчик, то теперь мы некоей тестовой переменной (m_cTest1 или m_cTest2) присваиваем значение локальной переменной (cTemp1 или cTemp2, соответственно).
~~UP~~
===== Запуск в симуляторе =====
==== Работа программы ====
Включим симулятор через меню "Debugger\Select Tool\MPLAB SIM". Соберем проект по Ctrl+F10. Для наблюдения за состоянием переменных откроем два окна: Watch и Local (оба окна открываются через меню "View"). В окно Watch добавляем две глобальные переменные m_cTest1 и m_cTest2. Установим точки останова так, как показано на рисунке ниже:
{{osa:tutorial:tutor_t2_breakpoints.png}}
Теперь нажимаем F9 (Run) и попадаем на первую точку останова в задаче Task_T1, где локальной переменной cTemp1 присваивается значение "1".
Нажимаем F8 (Step Over) и убеждаемся, что локальная переменная cTemp1 приняла значение "1" (ее значение будет отображаться в окне Local).
Итак, в данный момент курсор симулятора установлен на строке, содержащей вызов ##[[osa:ref:allservices:OS_Yield|OS_Yield]]##() из задачи Task_T1. Следующей строкой у нас присваивание m_cTest1 = cTemp1, а сама переменная cTemp1 у нас имеет значение "1". Мы подошли к главному моменту урока. Как было описано в параграфе [[osa:tutorial:intro#Особенности отладки|"Введение. Особенности отладки."]], сервисы, переключающие контекст нужно выполнять командой Run (F9), предварительно установив точку останова на следующей за сервисом строке. У нас точка останова уже стоит, так что смело давим F9 (Run).
Сервис ##[[osa:ref:allservices:OS_Yield|OS_Yield]]##() выполнился, и курсор симулятора теперь стоит на строке присваивания m_cTest1 = cTemp1. Но Обратим внимание на самый важный момент: **значение переменной cTemp1 изменилось и стало равно "2"**.
Разберемся, что с ней произошло. Для начала разберемся с тем, где размещаются локальные переменные.
==== Локальные переменные в MCC18 и MCC30 ====
В этих двух компиляторах локальные переменные создаются в стеке, причем в MCC18 стек эмулируется программно с помощью указателей FSR1 и FSR2, а в MCC30 используется общий стек, он адресуется регистрами WREG14 и WREG15.
Рассмотрим, как выделяется память под локальные переменные в этих компиляторах. На этапе компиляции производится подсчет, какой объем памяти требуется под локальные переменные для каждой функции. В начало каждой функции, содержащей локальные переменные, компилятором автоматически вставляется код, который увеличивает указатель стека на значение, соответствующее объему локальных переменных функции. Например, если в функции используются 3 локальных переменных типа unsigned int, то указатель стека будет увеличен на 6.
Рассмотрим рисунок:
{{osa:tutorial:tutor_t2_stack_mcc.png}}
На рисунке приведен порядок изменения указателей стека для MCC18 и MCC30. Теперь обращение к локальным переменным внутри функции будет производиться через указатель фрейма FSR2 для MCC18 или WREG14 для MCC30. Если из функции func() будет вызвана другая функция func2(), локальные переменные которой занимают 4 байта, то стек будет выглядеть так:
{{osa:tutorial:tutor_t2_stack2_mcc.png}}
Как видно, локальные переменные функций не пересекаются, когда одна вызывает другую. Более того, при такой организации возможна рекурсия.
Теперь важный момент: если одна функция по очереди вызывает две другие, то при попадании в каждую из них значение регистров указателей стека будет одинаковым. Т.е. если бы функция func1() после вызова func2() вызывала бы еще некую func3():
void func1 (void)
{
...
func2();
...
func3();
...
}
, то для локальных переменных func3() использовалась бы область памяти, начинающаяся с указателя FSR2, т.е. та же самая область, которая была занята под локальные переменные функции func2().
Функция func1 по очереди вызывает func2 и func3. При входе в func2 происходит следующее:
- ##FSR2 -> [FSR1++]## - сохранение текущего значения FSR2
- ##FSR2 = FSR1## - FSR2 становится указателем фрейма локальных переменных
- ##FSR1 += x## - указаетль стека увеличивается на количество байт, занимаемых под локальные переменные функции func2.
После того, как функция func2() отработает, перед выполнением return производятся следующие опреации:
- ##FSR1 -= x## - восстанавливаем прежнее знанчение укаателя стека
- ##FSR2 = [--FSR1]## - восстанавливаем указатель фрейма
Таким образом, к моменту вызова func3 значения регистров FSR1 и FSR2 те же, что и перед вызовом func2. Очевидно, что при входе в функцию func3 под ее локальные переменные будут заняты те же ячейки памяти, что были заняты и под func2().
==== Локальные переменные в PICC и PICC18 ====
Стратегия распределения памяти под локальные переменные в этих компиляторах несколько отличается от стратегий MCC. На этапе линковки строится граф вызовов, который содержит информацию о том, какие функции из каких функций вызываются и сколько каждая функция требует памяти под свои локальные переменные. Такой граф может выглядеть, например, так (в квадратных скобках указан объем памяти под локальные переменные):
{{osa:tutorial:tutor_t2_graph_picc.png}}
Далее линкер, опираясь на эту информацию, строит все возможные цепочки вызовов от верхушки графа до концевого узла (в нашем случае их 7), и для каждой цепочки строится своя схема выделения локальных переменных. Обратим внимание на функцию func4(), которая встречается в двух цепочках, причем, количество элементов в этих цепях различное. Это также учитывается линкером при распределении памяти под локальные переменные. Рассмотрим для примера три цепочки:
* Task_T1 -> func1
* Task_T2 -> func3 -> func4
* Task_T3 -> func4
Для первой цепочки никаких коллизий нет и локальные переменные разных уровней графа вызовов будут следовать непрерывно друг за другом. А вот вторая и третья цепочки графа имеют две общие вершины: main() и func4().
{{osa:tutorial:tutor_t2_picc_local.png}}
Не вдаваясь в подробности, сосредоточим внимание на том, что под локальные переменные функций, вызываемых из функции main(), выделяется одна и та же область памяти (она может быть разного объема, но начинается для всех с одного и того же адреса).
//**Примечание.** Из-за такой стратегии размещения локальных переменных PICC и PICC18 не позволяют делать рекурсивные вызовы.//
~~UP~~
==== . ====
Вернемся к нашему примеру. В ОСРВ OSA все функции задачи вызываются (хоть и не напрямую) планировщиком ##[[osa:ref:allservices:OS_Run|OS_Run]]##, который расположен в функции main(). Следовательно, вне зависимости от стратегии выделения памяти под локальные переменные, получается так, что локальные переменные всех функций-задач будут начинаться по одному и тому же адресу. Для MCC при вызове из main() мы в любую задачу попадаем с одними и теми же значениями регистров-указателей стека; для PICC линкер, построив граф вызовов, разместит все задачи на одном уровне после main().
Итак, что же происходит с переменной cTemp1 при выполнении ##[[osa:ref:allservices:OS_Yield|OS_Yield]]##()? Сперва производится возврат в планировщик, который, в свою очередь, принимает решение, что нужно запускать задачу Task_T2. Получив управление Task_T2 своей локальной переменной cTemp2, **которая оказывается расположенной в той же области памяти (в той же ячейке), что и локальная переменная cTemp1 из задачи Task_T1**, присваивает значение "2". Т.к. cTemp1 и cTemp2 имеют один и тот же адрес, то при записи в любую из этих переменных произойдет запись и во вторую. Далее Task_T2 вызывает сервис ##[[osa:ref:allservices:OS_Yield|OS_Yield]]##, который возвращает управление планировщику, а планировщик передает управление задаче Task_T1 на строчку, следующую за вызовом ##[[osa:ref:allservices:OS_Yield|OS_Yield]]##(), т.е. на присваивание m_cTest1 = cTemp1. При этом, как мы уже поняли, значение переменной cTemp1 изменилось задачей Task_T2.
~~UP~~
==== Эксперимент 1 ====
Перепишем задачу Task_T1 так:
void Task_T1 (void)
{
static char s_cTemp1;
m_cTest1 = 0;
for (;;)
{
s_cTemp1 = 1;
OS_Yield();
m_cTest1 = s_cTemp1;
}
}
Установим брейкпоинты на тех же местах и попробуем выполнить программу в симуляторе. Дойдя до строки присваивания m_cTest1 = s_cTemp1, мы можем убедиться, что значение переменной s_cTemp1 осталось неизменным после того, как отработала задача Task_T2. Все дело в квалификаторе ##static##, стоящим перед объявлением локальной переменной. Этот квалификатор говорит компилятору, что переменная должна сохранять свое значение после выхода из функции до следующего в нее входа. Эта переменная не будет размещаться в стеке (для MCC) или в области локальных переменных (для PICC), она будет помещена в отдельную область, где за ней на все время выполнения программы закрепится одна ячейка памяти.
~~UP~~
===== Заключение =====
В данном уроке мы рассмотрели важное свойство локальных переменных: их время жизни ограничено с момента входа в задачу до момента возврата в планировщик. Локальные переменные можно применять только в пределах одного сеанса работы задачи, иначе последствия непредсказуемы. Вот типичная ошибка:
void Task (void)
{
char i; // Ошибка здесь. Эта переменная должна быть объявлена
// как static
for (;;)
{
i = 20;
while (--i) OS_Yield();
...
}
}
Мы можем подвиснуть в этом цикле навсегда, а можем выйти из него на первом же шаге, в зависимости от того, как область памяти, занимаемая авто-переменной i, используется другими задачами.
Компилятор не предполагает одновременной работы нескольких функций, расположенных на одном уровне в графе вызовов, поэтому, управляя операционной системой как надстройкой над компилятором, программист должен сообщать ему, какие переменные не должны пересекаться в памяти программы, объявляя их с квалификатором static.
Не лишним будет на первых порах работы с ОСРВ все локальные переменные внутри задачи объявлять как статические.
~~UP~~