====== 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~~