Микропаскаль учебный курс. Часть 3

Почти всё то что мы рассматривали в учебном курсе ранее, справедливо не только для микропаскаля, но и для «большого» паскаля для ПК. Начиная с этой части так же будут рассматриваться некоторые специфические особенности паскаля для AVR. Но начнем мы с пары слов о пользовательских функциях и процедурах в микропаскале.  Функция или процедура это некоторый код который может быть вызван из любой части основной программы с какими либо параметрами (или без них). После того как процедура или функция отработала, управление возвращается основной программе. Между функциями и процедурами всего одно различие: Функция может возвращать какое либо значение, а процедура не может.  Функция выглядит следующим образом:

Function ИмяФункции(СписокПараметров):ТипВозвращаемогоЗначения;
begin
  result:=ВозвращаемоеЗначение;
end;

Имя функции любое не содержащее пробелов и спецсимволов. Именно по этому имени происходит обращение к функции, поэтому для улучшения читаемости кода рекомендуется давать осмысленные имена функциям и процедурам. Список параметров — список переменных которые передаются в функцию из вне. Тип возвращаемого значения — тип данных возвращаемого функцией значения. Функция должна обязательно возвратить значение, для этого нужно записать в переменную result возвращаемое значение. Если в теле функции нигде нет присвоения значения этой переменной, то проект даже не скомпилируется. Вместо result можно так же использовать имя самой функции. Отдельное внимание стоит уделить списку параметров. Существует два способа передать в функцию что-либо: С использованием параметров-значений или с использованием параметров-переменных. Рассмотрим оба варианта начиная с использования параметра-переменной:

program Funct;
var p:byte;

Function MyFunction(var a:byte):byte;
begin
  a:=a+10;
  result:=a;
end;

begin
   DDRA:=0xFF;
   p:=123;
   PORTA:=MyFunction(p);
end.

В данном случае в функцию передается переменная и функция может изменять её! Это значит что когда программа отработает, в переменной p уже будет не 123, а 133. Если же мы уберем слово var из описания списка передаваемых параметров, то в функцию передастся всего лишь её значение переменной. И что бы с ней не вытворяли внутри функции переменная p как была так и останется равной 123. Различие между параметрами-переменными и параметрами-значениям только в этом. Для процедур в микропаскале справедливо всё сказанное выше за исключением того что процедуры ничего не возвращают. Немного перепишем наш пример для использования в нем не функции а процедуры:

program Proc;
var p:byte;

Procedure MyProcedure(var a:byte);
begin
  a:=a+10;
end;

begin
   DDRA:=0xFF;
   p:=123;
   MyProcedure(p);
   PORTA:=p;
end.

Здесь у нас в процедуру передается переменная p которую изменяет тело процедуры прибавляя к её значению 10. В результате в PORTA будет выведено число 133.

Теперь можно перейти к прерываниям. Если кто не помнит что такое прерывание то напоминаю: Прерывание это некоторое событие вызываемое периферией контроллера (таймеры, ацп, EEPROM итд). Когда такое событие наступает, (таймер отсчитал определённый промежуток времени, ацп завершил измерение напряжения, EEPROM готова к работе итд) то основная программа на время перестаёт выполняться и управление передаётся к команде хранящейся в памяти по строго определённому адресу (Называется вектор прерывания и для каждого прерывания свой). Естественно возникает вопрос как объявить обработчик прерывания в микропаскале? Делается это следующим образом. Предположим, что хотим обрабатывать прерывание по переполнению от таймера 0 на микроконтроллере Atmega32. Открываем даташит и смотрем по какому адресу расположен вектор этого прерывания. В данном случае это адрес 0x16. Теперь пишем процедуру которая и будет обработчиком прерывания:

procedure Timer0_Interrupt(); iv 0x16;
  begin
    ///Тело обработчика прерывания
  end;

Название процедуры обработчика может быть произвольное. Лезть каждый раз в даташит чтобы узнать адрес вектора прерывания не совсем удобно. Поэтому у каждого вектора прерывания есть своё название на нормальном человеческом языке. В целях переносимости кода лучше всего использовать именно их т.к. у всех контроллеров они называются одинаково, а адреса векторов вполне могут быть разными. Для того чтоб посмотреть список названий векторов прерываний можно например открыть файл  ATmega32.mpas в папке \mikroPascal PRO for AVR\Defs\ . Там мы найдем следующее:

const IVT_ADDR_RESET            = 0x0000;
const IVT_ADDR_INT0             = 0x0002;
const IVT_ADDR_INT1             = 0x0004;
const IVT_ADDR_INT2             = 0x0006;
const IVT_ADDR_TIMER2_COMP      = 0x0008;
const IVT_ADDR_TIMER2_OVF       = 0x000A;
const IVT_ADDR_TIMER1_CAPT      = 0x000C;
const IVT_ADDR_TIMER1_COMPA     = 0x000E;
const IVT_ADDR_TIMER1_COMPB     = 0x0010;
const IVT_ADDR_TIMER1_OVF       = 0x0012;
const IVT_ADDR_TIMER0_COMP      = 0x0014;
const IVT_ADDR_TIMER0_OVF       = 0x0016;
const IVT_ADDR_SPI__STC         = 0x0018;
const IVT_ADDR_USART__RXC       = 0x001A;
const IVT_ADDR_USART__UDRE      = 0x001C;
const IVT_ADDR_USART__TXC       = 0x001E;
const IVT_ADDR_ADC              = 0x0020;
const IVT_ADDR_EE_RDY           = 0x0022;
const IVT_ADDR_ANA_COMP         = 0x0024;
const IVT_ADDR_TWI              = 0x0026;
const IVT_ADDR_SPM_RDY          = 0x0028;

Это и есть ни что иное как таблица векторов прерываний для Atmega32. Ну разумеется что лезть в файл каждый раз тоже ни чуть не проще чем в даташит, поэтому когда пишем procedure Timer0_Interrupt(); iv ставим пробел после iv и жмем комбинацию CTRL + Пробел и пишем IVT после этого редактор кода сам предложит дописать имя вектора прерывания. Разумеется для работы с прерываниями надо их сначала разрешить. Чтоб их разрешить нужно установить бит I в регистре SREG. Работать с отдельными битами регистра в микропаскале очень просто. Например чтоб установить бит I (седьмой по счёту) нужно написать SREG.B7:=1 или что еще понятней SREG_I_bit := 1;. Для того чтоб получше себе представить как работать с прерываниями вот небольшой пример (мигание светодиодом):

program Blinker;

procedure Timer0_Interrupt(); iv IVT_ADDR_TIMER0_OVF;
  begin
    PORTA:=NOT PORTA; ///Инвертируем состояние порта
  end;

begin
 SREG_I_bit := 1; //Разрешаем прерывания
 TCCR0.CS00:=1;   //Включаем таймер0
 TCCR0.CS02:=1;   //с делителем на 1024
 TIMSK.TOIE0:=1;  //Разрешаем прерывание по переполнению
 DDRA:=0xFF;      //Порт А выход
end.

А сейчас когда с прерываниями более менее разобрались, стоит поговорить об ассемблерных вставках. В микропаскале есть возможность вставлять фрагменты ассемблерного кода в любое место программы. Делается это что-бы например сократить время выполнения особо критичных кусков кода или их размер. Ассемблерные вставки оформляются следующим образом:

program Blinker;
begin
 DDRA:=0xFF;  //Порт на выход

 while(TRUE) do   ///Бесконечный цикл
  Begin
      asm
        ldi R16,0xFF
        out PORTA,R16    //Выводим 0xFF в порт
      end;
      delay_ms(500);     // Ждем
      asm
        ldi R16,0x00
        out PORTA,R16     //Выводим 0x00 в порт
      end;
      delay_ms(500);      // Ждем
  end;
end.

Однако ассемблерных вставок стоит избегать, и использовать их в случае крайней необходимости, т.к. это очень сильно влияет на переносимость кода с одного микроконтроллера на другой. Кстати если требуется вставить в код пустую команду NOP то сделать это можно без использования всяких вставок (просто пишем NOP; и всё).  Иногда может возникнуть необходимость использовать в ассемблерной вставке какую либо переменную. Компилятор располагает все переменные по умолчанию в оперативной памяти, и адрес любой из них можно выяснить в окне Statistics на вкладке Variables. Если для удобства хочется чтобы компилятор разметил переменную в регистре, то нужно явно сказать ему об этом при объявлении переменной:

var i:byte; rx;

Но и тут есть некоторые недостатки. Размещать переменные в регистрах он начинает с регистра R2. А как известно регистры R0-R15 в AVR являются немного ущербными (с ними не работают некоторые команды например ldi) . Заставить компилятор хранить переменную в конкретном регистре мне не удалось. Кстати нужно быть аккуратным при использовании регистров в ассемблерных вставках, мало ли чего в них положил компилятор для своих нужд. Лучше вообще предварительно запихивать их значения в стек, а потом когда регистр нам не нужен восстанавливать значение из стека.

P.S. В следующей части пойдёт рассказ о работе с периферией контроллера (АЦП, ЕЕПРОМ, UART итд)
P.P.S. Я сейчас активно пишу научный труд под названием диплом, именно поэтому с обновлениями возникают задержки. Но это временное явление :-)

Микропаскаль учебный курс. Часть 3: 32 комментария

  1. О себе: Фанат microP
    Про версии проги и компиляторы вставь в главы это важно!
    У меня прога для проца написанная на microP 2.6 весила
    967 байт (Tiny13) после перекомпиляции по 4.60 стала весить
    842. 15% свободного места на Tiny13 поменяв версию компилятора = я в шоке.

    Если народ ищет таблетку для 4.6 то таблетка от 2.6 подходит прекрасно.
    Дерзайте продукт отличнейший: Отладчик несравним не с чем — я умудрился на цикле с внутренними вычислениями и подциклами атомные часы на Delay_us(x) написать.

    1. У отладчика есть один небольшой баг. Иногда возникает ситуация когда нельзя во время отладки выбрать переменную в окне Watch Values. Этот глюк тянется из версии в версию.

      1. Да это я тоже заметил — есть способ борьбы с ним, точнее обхода его: Копирую текст, Делаю новый проект вставляю перекомпиливаю — обычно помогает, хз почему это как танец с бубном :)

  2. Прочитал Все. Автору огромное спасибо!

    Есть вопросец: в предпоследних сторочках описаний «program Funct;» и «program Proc;» не нужны ли «end;»?

    Если и дальше пойдет так же понятно, — от С откажусь к Е.М. :)

  3. Мне кажется лучше рассматривать именно особенности программирования для AVR на микропаскале. Function ИмяФункции(СписокПараметров):ТипВозвращаемогоЗначения; такое описание можно найти в любом справочнике по паскалю, и нет смысла это описывать так как это пришло из обычного паскаля

  4. Автору огромное спасибо!
    только начал изучать контроллеры и обрадовался когда нашел эти статьи, т.к. паскаль всегда нравился больше си.
    буду ждать следующих частей!

  5. Замечательный курс!!!
    Уже распечатал 0..3 части.
    У меня вопрос от начинающего — как организовать моргание диодов между пинами одного порта?
    И ещё — как обратиться к определённой ножке порта А и считать её значение?

    1. Моргать диодами на одном порте можно так:
      While(1) do
      begin
      PORTB.0:=1; // Зажигаем диод
      delay_ms(1000); // задержка
      PORTB.0:=0; //Выключаем диод
      delay_ms(1000);
      PORTB.1:=1; // теперь другой диод
      delay_ms(1000); // задержка
      PORTB.1:=0; //Выключаем диод
      delay_ms(1000); // задержка
      end;

      считать значение с определенной ноги можно так:
      pinval:=PINA.0;

  6. у меня возникли некоторые вопросы по использованию таймера:

    procedure Timer0_Interrupt(); iv IVT_ADDR_TIMER0_OVF;
    begin
    PORTA:=NOT PORTA; ///Инвертируем состояние порта
    end;
    begin
    SREG_I_bit := 1; //Разрешаем прерывания
    TCCR0.CS00:=1; //Включаем таймер0
    TCCR0.CS02:=1; //с делителем на 1024

    //что здесь означает делитель? это както определяет время срабатывания таймера?

    TIMSK.TOIE0:=1; //Разрешаем прерывание по переполнению

    //если я правильно понял, то по переполнению это фактически прерывание через както выставленный в предидущей строке промежуток времени. тогда какие еще есть прерывания у таймера и как их настраивать?

    DDRA:=0xFF; //Порт А выход
    end.

    1. Чтоб таймер начал тикать его нужно включить битами CSх2, CSх1 и CSх0. (икс — номер таймера) От комбинаций этих бит зависит как частот наш таймер будет тикать. Например при комбинации:
      CSх2=0 CSх1=0 и CSх0=1 наш таймер будет тикать очень быстро, с частотой тактирования контроллера. А если к примеру установить биты так: CSх2=1 CSх1=0 CSх0=1 то таймер будет тикать с частотой тактрования делённой на 1024. Все эти комбинации можно посмотреть в даташите на контроллер в разделе про таймеры. (те комбинации что я написал выше справедливы для атмеги32)

      Прерывание по переполнению возникает в тот момент когда значение в счётном регистре достигло своего максимума и таймер совершает еще один тик, который сбрасывает счётный регистр в ноль и запускает прерывание. Еще бывает прерывание по совпадению когда счётный регистр сравнивается с регистром сравнения и если значения в них совпали то возникает прерывание.

  7. выходит можно задать не любое значение времени через которое будет срабатывать таймер а только частота кварца, деленая на 1 8 64 256 или 1024.

    допустим если в этом примере подключить к порту светодиод то при частоте тактирования 1 мгц с делением в таймере на 1024, светодиод будет мигать 1*10^6/1024=976 раз в секунду?

    еще мне не свовсем понятна разница в прерывании по переполнению и сравнению. получается что вроде и там и там он отсчитывает импульсы кварца пока не насчитает 1 8 64 256 или 1024?

    1. Можно задавать любое. ни кто тебе не запрещает что-то записывать в счётный регистр. Запишешь например туда 200 так понятно что он быстрей до переполнения дотикает чем если бы он был обнулён.

      в расчёте ошибка, надо еще на 256 разделить так как таймер переполняется через каждые 256 тиков.

      Тамер тикает до 255 (если он 8-ми битный или до 65535 если 16-ти битный). Делитель просто заставляет его тикать пореже. если использовать прерывание по переполнению так как я писал выше, то разницы между ним и прерыванием по совпадению особой нет…

  8. Распечатал статьи, в первый же вечер написал несколько простеньких прог, прошил — все моргает и радует глаз. Спасибо огромное!!!

    Микропаскаль очень понравился.
    Справочные PDF с сайта просто шикарные!!!

    Много лет пытаюсь влюбиться в Си! Неа! Неполучается. а без любви не могу :)
    Жду с нетерпением продолжения пионерских ( в смысле первопроходских :) статей!

  9. Сделал из всех статей один PDF файл, очень хорошо и детально все описано. Хочу заняться микроконтроллерами AVR и обязательно на mikroPascal, жду с нетерпением продолжения. Спасибо вам большое за этот материал, для начинающих это просто клад.

    1. Скорее всего их не будет. микропаскаль говно. не используйте его пожалуйста. Хотя возможно в более новых версиях (старше пятой) компилятора многие баги пофиксят и он реабилитируется в моих глазах… Подвел он меня и очень сильно. Два раза. и оба раза в коммерческих проектах. пришлось подстраиваться под баги компилятора и писать отвратительные костыли чтоб оно хоть как то заработало…

        1. Программа просто не работает.
          Но стоит в начале добавить абсолютно не нужную инициализацию UARTa как всё начинает работать… Это один из багов.
          Баги часто вылезают на больших программах в маленьких вроде все работает

          1. Может проблема в самих камнях и обязательности инициализации некоторых устройств типа WD ?
            Подобное было у меня при переезде программы с 8515 на 32U4 — чистый ассемблер, а дело в железе — без принудительной инициализации WD программа на 32U4 не работала.

      1. А Вы пробовали паскаль AVRco? Может он надежнее и лучше, чем микропаскаль? Все таки больше тысячи евро против 199 долларов и знаменитое немецкое качество. Я вот не силен в больших проектах, пишу я-ля светодиодики-релюшки всякие.. Очень интересует ваше мнение! Их бесплатных версий мне вполне хватило-бы.

  10. а в паскале есть чтото вроде того что в си называется прототипом функции? тоесть чтобы функцию можно было использовать в функции, описаной выше?

  11. Паскаль — хорошее дело. Я с него начинал свой путь программера. Только не с него конкретно, а с его дедушки «Алгол — 60». Теперь пришлось под натиском обстоятельств перейти на это птичий язык, C.
    Перешел бы с удовольствием на микропаскаль, но поддержка программаторов микропаскалем мне не ясна. Если он не поддерживает связь с фирменной штукой: AVRATJTAGICE-MKII, то грош ему цена. Что скажите?

Добавить комментарий