mikroPascal for AVR. Особенности языка.

AVR

Предисловие

Поработав с mP поплотнее, я понял, что эта статья излишне длинная. На самом деле, о нем достаточно знать это.

Введение

При освоении нового компилятора уже знакомого языка, особенно под новую систему, возникает вопрос — а чем оно отличается от уже знакомого? Этот вопрос, применительно к компилятору mikroPascal for AVR (mP) фирмы mikroElektronika я и попробую осветить.


Возможности языка

Язык представляет собой вполне обычный паскаль, без поддержки ООП. Из типов данных поддерживаются только базовые, указатели, строки, массивы и записи (они же структуры). Перечислений, наборов и объединений нет, а жаль — они вполне полезны и на МК. Для указателей поддерживается указательная арифметика, хотя хитрые выражения с ее использованием компилятор часто отказывается компилировать. Также, из-за ограниченной памяти, отсутствует поддержка рекурсии (хотя все функции реентерабельны).
Примечание: под функциями я имею в виду и функции, и процедуры.

Заголовочные файлы МК

Для удобной работы со специальными регистрами необходим файл описания регистров и прочих ресурсов МК. Такие файлы расположены в каталоге <путь к компилятору>\\defs и подключаются автоматически ко всем файлам в проекте (примерно как модуль System в Delphi).
В этих файлах описаны:
  • Регистры (SREG, DDRB, etc)
  • Номера битов в битовых регистрах (SREG_I, DDB3, etc)
  • Отдельные биты в битовых регистрах (SREG_I_bit, DDB3_bit, etc)
  • Векторы прерываний (IVT_ADDR_RESET, IVT_ADDR_INT0, etc)
 

Литералы

Поддерживаются традиционные для паскаля целочисленные, дробные и строковые литералы. Из особенностей стоит отметить только префиксы «0x» для 16-ричных и "%" для двоичных литералов. Строковые литералы представлены в виде ASCIIZ, пустая строка состоит из одного терминирующего нуля.
$10 //10h = 16
0x2A //2Ah = 42
%01011001 //1011001b = 89
'hello' //[68 65 6C 6C 6F 00]
'' //[00]

Типы данных

Простые типы:


Строковый тип представлен помесью коротких строк и ASCIIZ, максимальная длина — 255 символов. Синтаксически соответсвует коротким строкам, индексация с нуля, неявно добавляется один элемент для хранения завершающего нуля. Пустая строка хранится как одиночный NULL. При работе со строками требуется определенная осторожность, чтобы не выйти за пределы и не затереть завершающий ноль.
var S: string[10]; //Строка длиной 10 символов плюс завершающий ноль
S := 'hello'; //Присвоение/копирование, в памяти представлено как [68 65 6C 6C 6F 00 ?? ?? ?? ?? ??]
S := S + 'world'; //Конкатенирование, в памяти [68 65 6C 6C 6F 77 6F 72 6C 64 00]

Составные типы представлены массивами и записями, со вполне типичным для паскаля синтаксисом. Многомерные массивы не поддерживаются, но могут быть объявлены как массивы массивов. Записи могут объявляться только как описания типов.
var A = array[10, 10] of byte; //Ошибка
var A = array[10] of array[10] of byte; //OK
type TDot = record X, Y: byte; end; //OK
var Dor = record X, Y: byte; end; //Ошибка
var Dot1, Dot2: TDot; Dot1 := Dot2; //Копирование записей

Указатели подробно рассмотрены ниже.

Память и переменные

У МК AVR имеется несколько адресных пространств: пространство РОН (регистров общего назначения, Register Space на схеме) — это регистры R0-R31, пространство ввода-вывода (регистры периферии, I/O Space), расширенное пространство ввода-вывода (то же самое для МК с богатой начинкой, у которых все регистры управления не влезли в стандартные 64 IO-регистра), ОЗУ (SRAM Space) и память программ, она же флэш. Вообще, кроме перечисленных есть еще EEPROM, но в mP к нему нет прямого доступа, только через библиотечные функции (встроенные или собственные).
Схема адресного пространства RAM
Как видно из схемы, все пространства, кроме флэша, отображаются в единое адресное пространство RAM. Для размещения данных доступны пространства РОН и ОЗУ.
В каком именно пространстве разместить переменную можно указать при помощи спецификаторов:

var
  MyRegisterVariable: Byte; rx;
  MyRAMVariable: Byte; data; //спецификатор data можно опустить, т.к. по умолчанию переменные создаются именно там

const
  MyFlashConstant: dword = $EDB88320; code;

Спецификаторы:
  • code: память программ. Здесь можно располагать только константы.
  • data: ОЗУ. По умолчанию все переменные располагаются в ней.
  • rx: РОН. К этой области памяти наиболее быстрый доступ, но она ограничена, поэтому при нехватке регистров переменная со спецификатором rx может оказаться в ОЗУ.
  • io: регистры ввода-вывода. Нужны, как правило, для объявления регистров. Например: DDRA: byte; absolute 0x3A; io; sfr;
  • sfr: Используется в сочетании с спецификаторами rx, io, data для доступа к регистрам специального назначения.
  • register: то же, что и rx. Про отличие — ниже.
У спецификаторов sfr и register есть одна особенность. Компилятор mP компилирует не непосредственно в машинный код, а в ассемблерный листинг, который затем транслируется ассемблером в машинный код. Поскольку в ассемблере все имена переменных глобальны, то для различения переменных с одинаковыми именами, но в разных модулях компилятор их переименовывает. Так, например, переменная RecvCount, объявленная в модуле UART, в ассемблерном листинге будет называться UART_RecvCount. Но переменные, объявленные со спецификаторами sfr и register не переименовываются. В результате, для ассемблера одноименные переменные в разных модулях будут одной переменной.

Кроме спецификаторов, к переменным можно применять квалификаторы, изменяющие поведение компилятора при работе с ними. mP поддерживает следущие квалификаторы:
  • near: имеет смысл только в сочетании с спецификатором code, указывает компилятору поместить данные в ближнем сегменте флэша (т.е. в первых 64 КБ, на мк где флэша более 64 КБ).
  • far: аналогично near, но данные будут размещены в дальнем сегменте, т.е. вне первых 64 КБ флэша.
  • external: указывает, что переменная объявлена где-то в другом модуле. Позволяет двум и более модулям иметь общую переменную. Также применим к функциям, хотя и редко нужен.
  • volatile: указывает, что переменная может внезапно поменяться, независимо от программы. Запрещает компилятору оптимизировать работу с ней (выкидывать лишние, на его взгляд, присвоения или кэшировать в РОН прочитаное оттуда значение). В частности, именно с этим квалификатором объявлены регистры для чтения данных извне — PIN*, UDR и т.д.
  • absolute <address>: указывает компилятору разместить переменную строго по указанному адресу. Например: DDRA: byte; absolute 0x3A; io; sfr;
  • at <variable>: позволяет создать алиас на переменную (т.е. присвоить одной и той же переменной несколько разных имен). Например: var LEDPort: Byte at PORTD;
Кучу примеров по применению всех этих спецификаторов и квалификаторов можно посмотреть в заголовочных файлах микроконтроллеров, в папке <путь к компилятору>\\defs. В принципе, многие из них только там и нужны :).

Для програмистов же из всего этого чаще всего нужны несколько возможностей.
  1. Запихнуть часто используемую переменную в РОН, чтобы программа быстрее работала
    
    var i: Byte; rx;
    for i:=0 to 10 do Something(i);
    

  2. Объявить алиасы для используемого GPIO. Часто используется встроенными библиотеками для работы с железом (например, LCD).
    
    //В подключаемом модуле
    var
      F51DataDDR: Byte; external; sfr;
      F51DataPort: Byte; external; sfr;
      F51DataPin: Byte; external; sfr; volatile;
      F51CtrlDDR: Byte; external; sfr;
      F51CtrlPort: Byte; external; sfr;
      F51CtrlPin: Byte; external; sfr; volatile;
    
    procedure DataPortOut;
    begin
      F51DataPort:=$00;
      F51DataDDR:=$FF;
    end;
    
    //В главном модуле привяжем к конкретным регистрам
    var
      F51DataDDR: Byte at DDRA; sfr;
      F51DataPort: Byte at PORTA; sfr;
      F51DataPin: Byte at PINA; sfr;
      F51CtrlDDR: Byte at DDRB; sfr;
      F51CtrlPort: Byte at PORTB; sfr;
      F51CtrlPin: Byte at PINB; sfr;
    


Существует аналог absolute для функций и констант во флэше, позволяющий располагать их по указанному адресу (внимание: адрес указывается в словах, а не байтах):

const
  BootHello: string = 'Hello'; org 0x1E90;

procedure Bootloader; org 0x1E00;
begin

end;

В ранних версиях mP эта же директива использовалась для объявления обработчиков прерываний, в более новых вместо нее рекомендуется использовать директиву iv.
Для случаев, когда необходимо всю программу (т.е. все функции и флэш-константы) разместить выше определенного адреса существует директива orgall:

begin
  orgall(0x200); // Все функции и константы в программе будут размещены по адресам выше 0x200
  ...
end.


Указатели и указательная арифметика

В mP указатели являются типизированными (тип Pointer отсутствует) и могут указывать на данные в любом из доступных адресных пространств, однако расположены могуть быть только в пространстве RAM (т.е. rx + io + data).

var ptr1: ^const byte;                   // ptr1 расположен в ОЗУ и указывает на байт в пространстве памяти программ
var ptr2: ^const ^volatile sfr byte; rx; // ptr2 расположен в пространстве rx (РОН), указывает на указатель в пространстве памяти программ, указывающий на volatile байт в пространстве sfr
var ptr3: ^data byte; code;              // ошибка, указатели нельзя размещать в пространстве памяти программ
const ptr4: ^byte;        // ptr4 расположен в ОЗУ, указывает на байт в памяти программ. Несмотря на const сам указатель не константен - это старый синтаксис объявления указателей на константы


Из указательной арифметики доступны следущие операции:
  • присвоение одного указателя другому
  • сравнение указателей друг с другом и с нулем
  • сложение/вычитание указателя и целого числа
  • вычитание двух указателей
Сложение и вычитание указателя и целого числа аналогично Си — т.е. указатель увеличивается/уменьшается на размер данных, на которые указывает, умноженный на число. При вычитании указателей в результате получается расстояние между ними, также в размерах данных, на которые указатели указывают.

Указатели на функции


 type TMyFunctionType = function (param1, param2: byte; param3: word) : word;  // Сперва объявляем процедурный тип
 var MyFuncPtr: ^TMyFunctionType;                                                  // Указатель на функцию

//использование
 MyFuncPrt:=@Func1;
 X:=MyFuncPtr^(1, 2, 3);

При вызове функций по указателю следует сообщить об этом линкеру, вызовом встроенной функции SetFuncCall в вызывающей функции:

procedure CalledByPtr1;
...

procedure CalledByPtr2;
...

procedure CallerByPtr;
var
  F: ^TMyFuncPtr;
begin
  SetFuncCall(CalledByPtr1, CalledByPtr2);
  F:=@CalledByPtr1;
  F^();
  F:=@CalledByPtr2;
  F^();
end;

Это гарантирует, что линкер не выкинет вызываемые по указателю функции, сочтя их неиспользуемыми.

Работа с битами

Приятной особенностью компилятора mP является поддержка работы с отдельными битами 8-битных переменных.
Доступ к отдельным битам: Variable.<номер бита> или Variable.B<номер бита>.

var i, j: Byte;
j:=3;
for i:=0 to 15 do
begin
  PORTB.3:=i.0; //выводим биты переменной i на пины со светодиодами пинборда
  PORTD.B4:=i.1;
  PORTD.PORTD5:=i.B2;
  PORTD7_bit:=i.j; //Можно даже брать номер бита из переменной
  Delay_ms(200);
end;

Объявление однобитовой переменной (флага, например):

var SomeFlag: bit;

Объявление бита в переменной:

var   PORTD7_bit : sbit at PORTD.B7; // седьмой бит переменной PORTD


//В подключаемом модуле
var
  Abit: sbit; external; sfr; //Определяемый извне бит в регистре специального назначения
  Bbit: sbit; external; //Определяемый извне бит в памяти

//В главном модуле
var
  B: Byte;
  Abit: sbit at PORTB.3;
  Bbit: sbit at B.0;


Ограничения битовых переменных:
  • Нельзя создавать указатели на них
  • Нельзя создавать массивы битовых переменных
  • Записи не могут содержать битовые переменные
  • Битовые переменные нельзя инициализировать

Обработчики прерываний

Для объявления обработчика прерывания используются директивы iv и org:

procedure INT0_ISR; iv IVT_ADDR_INT0; //Рекомендуемый вариант
begin

end;

procedure INT1_ISR; org 0x600 iv IVT_ADDR_INT1; //Можно указать, по какому адресу разместить тело обработчика, 0x600 в данном случае
begin

end;

procedure RESET_ISR; org AVT_ADDR_RESET; //Устаревший вариант объявления обработчика прерывания
begin

end;


Встроенные функции

Компилятор поддерживает несколько встроенных функций, преимущественно инлайновых. Среди них задержки (Delay_us, Delay_ms, etc), константы (Clock_kHz, Clock_MHz, etc), стандартные функции паскаля (Hi, Lo, Ord, Chr, Inc, Dec, etc), работа с битами (SetBit, ClearBit, TestBit) и другие. Подробней — в справке, раздел AVR Specifics\Built-In Routines.
Также есть не упомянутая в хелпе функция NOP, компилируемая в одноименную команду процессора. Полезна для малых задержек или замены пустого оператора, не поддерживаемого компилятором:

while MyFlag=0 do; //фиг там, хотя стандарт языка допускает
while MyFlag=0 do NOP; //ожидание флага

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

Ассемблерные вставки

При необходимости вставить код на ассемблере можно применить блок asm… end:

procedure Init;
begin
  asm cli end;
  CalibrateRC;
  InitPWM;
  LEDInit;
  uInit;
  F51Init;
  asm sei end;
end;


//Кусок кода из проекта "ИК дистанционное управление" dcoder'а
{Прерывание от INT0. тута мы записываем в память значение счётчика}
procedure Int0_req; org 0x2;
begin
 XL := TCNT1L; //Дёргаем в регистры значение таймера
 XH := TCNT1H; //Сначала младший байт, потом старший.
 asm
  CLR ZH
  LDS ZL, _mem_count  //Загружаем в Z адрес из памяти
  ST Z+, XL           //Пихаем младший таймер
  ST Z+, XH           //Пихаем старший таймер
  STS _mem_count, ZL  //Закидываем адрес обратно в память.
 end;
 PSR10_bit := 1; //Скидываем прескаллер
 TCNT1H:=0;      //Обнуляем таймер
 TCNT1L:=0;      //Сначала старший, потом младший.
 //TCCR1B := 2;    //Пускаем таймер тикать снова!
end;

Ассемблерные вставки позволяют использовать не поддерживаемые компилятором возможности (например команды cli/sei) или оптимизировать критичный участок вручную. Но не стоит забывать, что это негативно отражается на портируемости. Например, упомянутый выше проект «ИК дистанционное управление» (в оригинале под тини2313) под ATmega16 компилируется (после комментирования работы с EEPROM), но нифига не работает.

На этом, пожалуй, все. Суховато, но уж как умею :) Зато, при знании языка, этого вполне достаточно для работы с mP.

P.S. Как обычно, отложил на потом и забыл. Кусочек из хелпа, примечания разработчиков касательно особенностей языка.
  • Directive absolute in Rx memory space guarantees only that defined variable will be overlapped with the given memory address. (честно говоря, я это не понял)
  • Адреса всех регистров указываются в пространстве RAM. Пространство RAM — непрерывный регион памяти, включающий все пространства оперативной памяти AVR (пространство RAM = пространство РОН + пространство ВВ + пространство ОЗУ). При использовании инструкций, предназначенных для работы с пространством ВВ в ассемблерных вставках, доступ к регистрам ВВ должен осуществляться по их адресам в пространстве RAM.
  • Регистры общего назначения, не используемые компилятором, можно использовать для своих переменных. Размер доступного для них пространства rx не фиксирован и зависит от потребностей проекта в памяти. Компилятор использует не менее 7 РОН.
  • Literal strings are stored according to the destination (Flash or RAM). If stored in RAM, they are linked as a global and always exist by default. There is an option for storing literal string on the function frame. (тоже мало что понял)
  • Доступная ОЗУ делится на статическую и динамическую. Статическая растет вверх (от нижних адресов к верхним). Динамическая растет вниз (от верхних адресов к нижним). Комментарий: статическая память используется для хранения глобальных переменных, которые существуют все время работы программы, динамическая — это стек, помимо адресов возврата и сохранения регистров там выделяется память под локальные переменные, что позволяет экономить память.
  • В случае большой глубины вызовов (активное использование перекрестных вызовов, рекурсий и т.д.) не гарантировано, что динамической памяти хватит для работы программы. В этом случае динамическая память перекроется со статической, переписав ее.

  • +3
  • 09 марта 2011, 03:25
  • Vga

Комментарии (26)

RSS свернуть / развернуть
Эх блин опередил ты меня :-)
Я тоже учебный курс по микропаскалю собрался делать.
0
Дак кто мешает :))) Кооперируйтесь и дополняйте друг друга =)
0
Еще кстати пару топиков осветить забыл. Строки и указательную арифметику.
0
  • avatar
  • Vga
  • 09 марта 2011, 17:30
Ну так статью всегда можно подредактировать, действуй.
0
Ну на самом деле это была напоминалка самому себе)
0
А вот мне интересно, можно ли битовую переменную, например, в регистре ввода/вывода передать в функцию/процедуру и какой при этом машинный код будет генерироваться?
0
14 365 Parameter cannot be of sbit or bit type 2051Prog.mpas
Довольно ожидаемо)
0
А какже внешняя «библиотечная» периферия конфигурируется? Использует пердопределённые имена переменных?
0
В библиотеке объявлены глобальные переменные-алиасы на регистры, вида
LCD_RS_PIN: sbit; external; //пример абстрактный

При ее использовании их все нужно доопределить:
LCD_RS_PIN: sbit at PIND.5;

Довольно забавный способ, без оверхеда, но не очень гибкий.
0
Ты еще про отсутствие контроля границ массивов (и строк до кучи) не написал. Причем у них это не баг, а фича, как я понимаю.
0
Что в хелпе нашел, то и написал) А что там с контролем и почему это фича?
0
Кипипастер, блин. Фича – потому, что это тянется, судя по раздраженным вопрлям в инете, хз сколько версий. Ты можешь создать массив (строку) из 10 элементов, а потом записать в них, к примеру, 11. Или 20. Или 100. Компилятор это НЕ ОТСЛЕЖИВАЕТ.
0
Неправда, я хелп не копипастил, а переписыал в более сжатом и (на мой взгляд) удобном виде)
Компилятор это НЕ ОТСЛЕЖИВАЕТ.
Ну, в принципе, большую часть таких операций нужно отслеживать в рантайме. Ну и этот пункт скорее для другой статьи. Той, на которую я ссылаюсь во введении)
0
Например: при конверсионных операциях длина результатов известна. Переменные явно объявлены. Что с тобой сделает Pascal традиционного вида? Правильно: обматерит. В случае mP – песенка «все хорошо, прекрасная маркиза». Т.е. фактически они отсекли то, за что мы все Паскаль и любим: строгая типизация и контроль. Понятное дело, что все это было сделано исключительно во благо и для оного :) Вот только нас об этом ПРЕДУПРЕДИТЬ НАПРОЧЬ ЗАБЫЛИ.
0
Честно говоря, качество компиляторов микроэлектроники (впрочем, я щупал только микропаскаль) сильно не впечатляет)
Последняя версия mP for 8051 (если, конечно, они не сподобились выпустить что-то новее, чем 2.25 для них) вообще непригодна для использования.
Т.е. фактически они отсекли то, за что мы все Паскаль и любим: строгая типизация и контроль.
Примерно поэтому я предпочитаю для МК использовать хорошие компиляторы С вместо этих поделок >_<
0
Ты так и не посмотрел в архив, судя по всему.
0
для 8051 — mP/mC/mB Pro 3.5
0
Прогресс. Надеюсь, оно не будет так глючно, как mPAVR 3.5.
0
Сравни.
0
Сравнивал последние версии, в коде никаких изменений, добавляют новые контроллеры и небольшие изменения в библиотеках (mikroPascal PIC и ARM).
+1
Понятно
0
mPAVR в последних версиях сравнительно прилично работает — по крайней мере не генерит косой код. А вот 8051 они плохо поддерживают — на тот момент, когда я ставил mPAVR 5.60 для 8051 был только mP 2.2 (или 2.25, или около того) пятилетней давности и вот он генерил вообще нерабочий код.
0
В целом соглашусь, вполне себе работает (если работает). Даже громоздкость не проблема – еслть же вполне доступные кристаллы и на 128К, и на 256К флеша и с тоже немаленьким ОЗУ.

Проблема тут, ИМХО, в плохой (не сказать хуже) документированности продуктов. Как, например, с этим горбылем-нюансом касательно границ массивов (строк).
0
Ну, документация лично меня вполне устраивает, отдельно радует то, что в таком объеме ее легко скурить полностью. Вот качество продукта мне не по нраву. Держу mP для мелких некритичных проектов — типа помигать диодиком на CM7.
0
Я же не про ее объем, а про ее качество ворчу. Не учтены важные нюансы. Это, конечно, не Turbo Pascal 5.5 в коробке с толстенными томами, но уж прописать свои тонкие нюансы они вроде обязаны были. Продают же.

Ладно, хрен с ним.
0
Как-то так, в общем:
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.