Курс лекций - Микропроцессоры

         

Алфавит языка


Символы исходной программы представляют собой подмножество таблиц символов ASCII для DOS и ANSI для WINDOWS. В исходном тексте программы, написанном на языке программирования PL/M-51 допустимо использование следующих символов:

символы интервала, буквы, знаки цифры.

Символы интервала определяют один или несколько пробелов в предложении исходного модуля. К этим символам относятся "пробел" и " табуляция".

В качестве букв воспринимаются латинские буквы верхнего и нижнего регистра:

A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z.

Ниже приведен перечень цифр:

0, 1, 2, 3, 4, 5, 6, 7, 8, 9.

Наименования знаков и их обозначение приведено в таблице 1:



Наименование

Обозначение

Номер

#

Знак денежной единицы

$

Апостроф

'

круглая скобка левая

(

круглая скобка правая

)

Звездочка

*

Плюс

+

Запятая

,

Минус

-

Точка

.

дробная черта

/

Двоеточие

:

Точка с запятой

;

Меньше

<

Равно

=

больше

>

вопросительный знак

?

коммерческое эт

@

Знаки, комбинации знаков (<>, >=, <= ), а также символы интервала являются разделителями конструкций языка. До и после знака - разделителя в любой конструкции языка могут быть вставлены символы интервала.

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

Из символов формируются и .



Идентификаторы


это символическое обозначение объекта программы. В качестве идентификатора может быть использована любая последовательность букв и цифр. При этом в качестве буквы может быть использована любая буква латинского алфавита, а также вопросительный знак (?) и знак "нижнее подчеркивание" ( _ ). Идентификатор может начинаться только с буквы! Это позволяет отличать его от . В идентификаторах, язык программирования ASM-51 различает буквы верхнего и нижнего регистров.

Количество символов в идентификаторе ограничено длиной строки (255 символов). Транслятор различает идентификаторы по первым 31 символам.

Примеры идентификаторов:

ADD5, FFFFH, ?, ALFA_1.

В языке программирования ASM-51 имеются три категории идентификаторов:

; ; .

Числа


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

B двоичное число (разрешённые цифры 0,1) Q\O восьмеричное число (разрешённые цифры 0,1,2,3,4,5,6,7) [D] десятичное число (разрешённые цифры 0,1,2,3,4,5,6,7,8,9) H шестнадцатеричное число (разрешённые цифры 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F)

Для десятичного числа суффикс может отсутствовать. Количество символов в числе ограничено размером строки, однако значение числа определяется по модулю 2**16 (т.е. диапазон значений числа находится в пределах от 0 до 65535).

Примеры записи чисел:

011101b, 1011100B, 735Q, 456o, 256 , 0fah, 0CBH

Число всегда начинается с цифры. Это необходимо для того, чтобы отличать шестнадцатиричное число от идентификатора.

ADCH - идентификатор
0ADCH - число

Часто бывает удобно выполнить некоторые вычисления для того, чтобы получить число. Язык программирования ASM-51 позволяет выполнять беззнаковые операции над числами. В таких выражениях допустимо использовать арифметические операции:

+ суммирование - вычитание * умножение / деление mod вычисление остатка от целочисленного деления В языке программирования ASМ-51 также определена одноместная операция '-'. Для нее требуется один операнд, которому она предшествует. Для изменения порядка выполнения операций можно воспользоваться скобками. Кроме арифметических операций в выражениях допустимо использование логических операций: not побитовая инверсия операнда and логическое "и" or логическое "или" xor "исключающее или" (суммирование по модулю два)

и функций выделения старшего HIGH и младшего LOW байта шестнадцатиразрядного числа.

Пример использования выражений для определения числовой константы:

Часто число используется для представления символов. В этом случае для определения числа можно воспользоваться литеральной константой. Литеральная константа заключается в апострофы:

'a', 'W'

Для записи фраз в памяти программ можно воспользоваться литеральными строками:

В этом случае каждый символ заменяется отдельным байтом и запоминается в ПЗУ памяти программ.

[ ] [ ] [ ]



Ключевые слова


Ключевое слово является определяющей частью оператора языка ассемблера. Значения ключевых слов языка ассемблера АSМ-51 не могут быть изменены или переопределены в программном модуле каким-либо образом. Ключевому слову не может быть назначено имя- синоним. Ключевые слова могут быть написаны буквами как верхнего, так и нижнего регистров.

В языке АSМ-51 имеются следующие категории ключевых слов:

Инструкции по форме записи совпадают с мнемоническими обозначениями команд микроконтроллеров семейства MCS-51 и совместно с операндами, составляют . Список инструкций:

, , , , , CALL, , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , , .

Директивы совместно с вспомогательными словами определяют действия в программе, которые должны быть выполнены ассемблером в процессе преобразования исходного текста программы в объектный код. В языке программирования ASM51 используются:

Директивы: BIT, , CODE, , DATA, , DBIT, DS, , , END, , , IDATA, , NAME, , , RSEG, SEGMENT, , , XDATA, .

Вспомогательные слова: AT, BIT, BITADDRESSABLE, CODE, DATA, IDATA, INBLOCK, INPAGE, NUMBER, PAGE, UNIT, XDATA.

Операции выполняются ассемблером в процессе вычисления выражений на этапе трансляции исходного текста программы для определения конкретного числа, которое используется в команде. Перечень операций, использующихся языком программирования ASM-51:

AND, EQ, GE, GT, HIGH, LE, LOW, LT, MOD, NE, NOT, OR, SHL, SHR, XOR.

Встроенные имена


Встроенные имена присвоены адресам регистров специальных функций, адресам флагов специальных функций AR0-AR7, рабочим регистрам R0-R7 текущего банка регистров, а также аккумулятору A и флагу переноса C.

Имя

Регистр

A

Аккумулятор

R0-R7

8-разрядный рабочий регистр текущего банка рабочих регистров

DPTR

16-разрядный регистр-указатель данных

PC

16-разрядный счетчик команд

C

флаг переноса

AB

регистровая пара, состоящая из аккумулятора A (старшая часть) и регистра B (младшая часть)



Определяемые имена


Определяемые имена объявляются пользователем. В языке программирования ASM-51 имеются следующие категории определяемых идентификаторов:

метки, внутренние и внешние переменные адресного типа, внутренние и внешние переменные числового типа, имена сегментов, названия программных модулей.

Язык программирования ассемблер всегда включает


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

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

Директива equ позволяет назначать имена переменных и констант. Теперь можно назначить переменной адрес в одном месте и пользоваться идентификатором переменной во всей программе. Правда за использование идентификатора именно в качестве переменной отвечает программист, тем не менее, если в процессе написания программы потребуется изменить адрес переменной, это можно сделать в одном месте программы, а не просматривать всю программу, разбираясь является ли в данной конкретной команде число 10 константой, адресом ячейки ли количеством повторов в цикле. Все необходимые изменения сделает сам транслятор. Пример назначения переменных приведён на примере, показанном на рисунке 1.



Рисунок 1. Назначение переменных при помощи директивы equ.

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

При помощи директивы equ можно назначать не только переменные, но и константы. Как уже говорилось ранее, будет ли использован идентификатор как переменная или как константа зависит от и видов , которые использует программист. Один раз назначенный идентификатор уже не может быть изменён и при повторной попытке назначения точно такого же имени идентификатора будет выдано сообщение об ошибке.



Если требуется в различных местах программы назначать одному и тому же идентификатору различные числа, то нужно пользоваться директивой set. Использование этой директивы полностью идентично использованию директивы equ, поэтому иллюстрироваться примером не будет.

Константы, назначаемые директивой equ, могут быть использованы только в одной команде. Достаточно часто требуется работа с таблицей констант, такой как таблица перекодировки, таблицы элементарных функций или синдромы помехоустойчивых кодов. Такие константы используются не на этапе трансляции, а хранятся в микроконтроллера. Для занесения констант в память программ микроконтроллера используются директивы db и dw.

Директива db используется для занесения в память программ однобайтных констант. Пример использования директивы db приведён на рисунке 2.



Рисунок 2. Назначение констант при помощи директивы db.

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

В директиве db можно задавать сразу несколько констант, разделённых запятыми. Можно одновременно использовать все , но обычно имеет смысл снабдить каждую константу комментарием, как это сделано в предыдущем примере. Так программа становится более понятной и легче искать допущенную ошибку.

Эта же директива позволяет легко записывать надписи, которые в дальнейшем потребуется высвечивать на встроенном дисплее или экране дисплея универсального компьютера, подключённого к разрабатываемому устройству через какой либо интерфейс. Пример использования директивы db для занесения надписей в память программ микроконтроллера приведён на рисунке 3.



Рисунок 3. Применение директивы db для занесения надписей в память программ микроконтроллера.

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


Пример листинга фрагмента программы приведён на рисунке 4.



Рисунок 4. Применение директивы dw.

На рисунке 4 приведён фрагмент листинга программы для того, чтобы можно было проследить какие байты заносятся в память программ микроконтроллера. В самой правой колонке листинга приведены адреса в которые будут занесены числа, являющиеся операндами директивы dw. В следующей колонке приведены двухбайтовые числа, которые будут заноситься в память программ микроконтроллера. Обратите внимание, что несмотря на то, что первые два операнда состоят только из одной цифры, в память микроконтроллера заносятся четыре шестнадцатеричных цифры (двухбайтовое число).

При трансляции исходного текста программ предполагается, что первая команда расположена по нулевому адресу. Адрес последующих команд зависит от длины и количества предыдущих команд. Пример листинга начального участка программы приведён на рисунке 5.



Рисунок 5. Пример листинга программы.

Иногда требуется расположить команду по определённому адресу. Наиболее часто это требуется при использовании прерываний, когда первая команда должна быть расположена точно на . Это можно сделать используя команду для заполнения промежутков между векторами прерывания, но лучше воспользоваться директивой ORG.

Директива org предназначена для записи в счетчик адреса сегмента значения своего операнда. То есть при помощи этой директивы можно разместить команду (или данные) в памяти микроконтроллера по любому адресу. Пример использования директивы ORG для размещения подпрограмм обработки прерываний на векторах прерываний показан на рисунке 6.



Рисунок 6. Пример использования директивы ORG.

Необходимо отметить, что при использовании этой директивы возможна ситуация, когда программист приказывает транслятору разместить новый код программы по уже написанному месту, поэтому использование этой директивы допустимо только в крайних случаях. Обычно это использование векторов прерываний.

Директива using При использовании прерываний критичным является время, занимаемое программой, обработчиком прерываний.Это время можно значительно сократить, выделив для обработки прерываний отдельный Выделить отдельный банк регистров можно при помощи директивы USING. Номер банка используемых регистров указывается в директиве в качестве операнда. Пример использования директивы USING для подпрограммы обслуживания прерываний от приведён на рисунке 7.



Рисунок 7. Пример использования директивы USING.

Остальные директивы предназначены для управления сегментами, и поэтому будут рассмотрены позднее.

[] [] []


Использование сегментов в языке программирования ассемблер


Как только встал вопрос о транслировании программы по частям возникает вопрос как с этими частями работать. Справедливости ради необходимо отметить, что даже когда мы не задумываемся о сегментах, в программе присутствует два сегмента: память программ и память данных. Если внимательно присмотреться к программе, то можно обнаружить, что кроме команд в памяти программ хранятся константы, то есть в памяти программ располагаются по крайней мере два сегмента: программа и данные. Чередование программы и данных может привести к нежелательным последствиям. Вследствие каких либо причин случайно данные могут быть выполнены в качестве программы или наоборот программа может быть воспринята и обработана как данные.

Рисунок 1. Разбиение памяти программ и памяти данных на сегменты.

Перечисленные выше причины приводят к тому, что желательно явным образом выделить по крайней мере три сегмента:

программу; переменные; константы.

Наиболее простой способ определения сегментов это использование абсолютных сегментов памяти. При этом способе распределение памяти ведётся вручную точно также, как это делалось при использовании директивы . В этом случае начальный адрес сегмента жёстко задаётся программистом и он же следит за тем, чтобы сегменты не перекрывались друг с другом в . Использование абсолютных сегментов позволяет более гибко работать с памятью данных, так как теперь байтовые переменные в памяти данных могут быть назначены при помощи директивы резервирования памяти DS, а битовые переменные при помощи директивы резервирования битов DBIT.

Для определения абсолютных сегментов памяти используются директивы:

- абсолютный сегмент в области - абсолютный сегмент в области - абсолютный сегмент в области - абсолютный сегмент в области - абсолютный сегмент в области

Директива BSEG позволяет определить абсолютный сегмент во внутренней памяти данных с битовой адресацией по определённому адресу. Эта директива не назначает имени сегменту, то есть объединение сегментов из различных невозможно.
Для определения конкретного начального адреса сегмента применяется атрибут AT. Если атрибут AT не используется, то начальный адрес сегмента предполагается равным нулю. Использование битовых переменных позволяет значительно экономить внутреннюю память программ микроконтроллера. Пример использования директивы BSEG для объявления битовых переменных приведён на рисунке 2.



Рисунок 2. Пример использования директивы BSEG для объявления битовых переменных.

Директива CSEG позволяет определить абсолютный сегмент в памяти программ по определённому адресу. Эта директива не назначает имени сегменту, то есть объединение сегментов из различных невозможно. Для определения конкретного начального адреса сегмента применяется атрибут AT. Если атрибут AT не используется, то начальный адрес сегмента предполагается равным нулю. Пример использования директивы CSEG для размещения подпрограммы обслуживания прерывания от таймера 0 приведён на рисунке 3.



Рисунок 3. Пример использования директивы CSEG для размещения подпрограммы обслуживания прерывания.

Директива DSEG позволяет определить абсолютный сегмент во внутренней памяти данных по определённому адресу. Предполагается, что к этому сегменту будут обращаться . Эта директива не назначает имени сегменту, то есть объединение сегментов из различных невозможно. Для определения конкретного начального адреса сегмента применяется атрибут AT. Если атрибут AT не используется, то начальный адрес сегмента предполагается равным нулю. Пример использования директивы DSEG для объявления байтовых переменных приведён на рисунке 4.



Рисунок 4. Пример использования директивы DSEG для объявления байтовых переменных.

В приведённом примере предполагается, что он связан с примером, приведённом на рисунке 2. То есть команды, изменяющие битовые переменные RejInd, RejPriem или Flag одновременно будут изменять содержимое переменной Rejim, и наоборот команды работающие с переменной Rejim одновременно изменяют содержимое флагов RejInd, RejPriem или Flag. Такое объявление переменных позволяет написать наиболее эффективную программу управления контроллером и подключенными к нему устройствами.



Директива ISEG позволяет определить абсолютный сегмент во по определённому адресу. Напомню, что внутренняя память с косвенной адресацией в два раза больше памяти с прямой адресацией. Эта директива не назначает имени сегменту, то есть объединение сегментов из различных невозможно. Для определения конкретного начального адреса сегмента применяется атрибут AT. Если атрибут AT не используется, то начальный адрес сегмента предполагается равным нулю. Пример использования директивы ISEG для объявления байтовых переменных приведён на рисунке 5.



Рисунок 5. Пример использования директивы ISEG для объявления байтовых переменных.

Директива XSEG позволяет определить абсолютный сегмент во внешней памяти данных по определённому адресу. Эта директива не назначает имени сегменту, то есть объединение сегментов из различных невозможно. Для определения конкретного начального адреса сегмента применяется атрибут AT. Если атрибут AT не используется, то начальный адрес сегмента предполагается равным нулю. До недавнего времени использование внешней памяти не имело смысла, так как это значительно увеличивало габариты и цену устройства. Однако в последнее время ряд фирм стал размещать на кристалле значительные объёмы ОЗУ, доступ к которому осуществляется как к внешней памяти. Так как использование этой директивы не отличается от использования директивы DSEG, то отдельный пример приводиться не будет.

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

Еще одна ситуация, когда нас не интересует начальный адрес сегмента – это программные модули. Как уже говорилось ранее, в программные модули обычно выносятся подпрограммы. Естественно, что где конкретно будут находиться эти подпрограммы в адресном пространстве микроконтроллера, нас тоже мало интересует.



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

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

 data      –  размещает сегмент во внутренней памяти данных с прямой адресацией;  idata     –  размещает сегмент во внутренней памяти данных с косвенной адресацией;  bit         –  размещает сегмент во внутренней памяти данных с битовой адресацией;  xdata     –  размещает сегмент во внешней памяти данных;  code     –  размещает сегмент в памяти программ; После определения имени сегмента можно использовать этот сегмент при помощи директивы rseg. Использование сегмента зависит от области памяти, для которой он предназначен. Если это память данных, то в сегменте объявляются байтовые или битовые переменные. Если это память программ, то в сегменте размещаются константы или участки кода программы. Пример использования директив segment и  rseg для объявления битовых переменных приведен на рисунке 8.31.

_data segment idata

  public VershSteka

;Определение переменных----------------------------

rseg _data

buferKlav:  ds 8

VershSteka:

  End

Рис. 8.31. Пример использования директив segment и rseg для объявления байтовых переменных

В этом примере объявлена строка buferKlav, состоящая из восьми байтовых переменных. Кроме того, в данном примере объявлена переменная VershSteka, соответствующая последней ячейке памяти, используемой для хранения переменных. Переменная VershSteka может быть использована для начальной инициализации указателя стека для того, чтобы отвести под стек максимально доступное количество ячеек внутренней памяти.Это необходимо для того, чтобы избежать переполнения стека при вложенном вызове подпрограмм.

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

Еще один пример использования директив segment и  rseg приведен на рисунке 8.32. В этом примере директива  segment используется для объявления сегмента битовых переменных.

_bits segment bit

  public knIzm,strVv

;Определение битовых переменных -------------------------------------

  rseg _bits

knIzm: dbit 1

strVv: dbit 1

  end

Рис. 8.32. Пример использования директив segment и rseg для объявления битовых переменных

[ ] [ ]


Язык программирования АSМ51 поддерживает модульное


Язык программирования АSМ51 поддерживает модульное написание программ. Графическое изображение процесса написания программы на языке программирования ASM-51 приведено на рисунке 1.



Рисунок 1. Процесс написания программы на языке программирования ASM-51.

Файл, в котором хранится программа, написанная на языке АSМ51 (исходный текст программы), называется исходным модулем. Для исходного текста программы принято использовать расширения файла: asm, a51, srs, s51. Исходный текст программы можно написать, используя любой текстовый редактор.

Получить можно, указав имя исходного модуля программы в качестве программы-транслятора в DOS строке или строке командного файла:

asm51.exe modul.asm

Получить программы можно, указав все имена объектных модулей программы в качестве программы редактора связей в DOS строке или строке командного файла:

rl51.exe main.obj, modul1.obj, modul2.obj

Имя исполняемого модуля программы по умолчанию совпадает с именем первого объектного файла в списке параметров строки запуска редактора связей. Исполняемый модуль программы записывается в файл без расширения.

Большинство программаторов не может работать с объектным форматом исполняемого модуля программы, поэтому для загрузки машинного кода в процессор необходимо преобразовать объектный формат исполняемого модуля в общепринятый для программаторов гексадецимальный формат. При преобразовании форматов вся отладочная информация теряется. Машинный код процессора в гексадецимальном формате называется загрузочным модулем.

Загрузочный модуль программы можно получить при помощи программы-преобразователя программы oh.exe, передав ей в качестве имя файла исполняемого модуля программы:

oh.exe main


Многомодульные программы


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

Программа-транслятор работает со всем исходным текстом целиком, ведь она соединяет все файлы перед трансляцией вместе. Поэтому время трансляции исходного текста программы остаётся значительным (и даже возрастает). В то же самое время программа никогда не переписывается целиком. Обычно изменяется только небольшой участок программы. При назначении переменных их количество ограничено программой-транслятором и может быть исчерпано при написании программы. Различные программисты, участвующие в создании программного продукта могут назначать одинаковые имена для своих переменных и при попытке соединения файлов в единую программу обычно возникают проблемы.

Все эти проблемы могут быть решены при раздельной трансляции программы. То есть было бы неплохо уметь транслировать каждый файл с исходным текстом программы отдельно и соединять затем готовые оттранслированные участки программы.

На первый взгляд раздельная трансляция не должна вызывать каких либо проблем. Однако это не так. При компиляции исходного текста программы транслятор составляет таблицу ссылок на константы, переменные и команды. Если при втором просмотре исходного текста программы, во время которого формируется объектный модуль, транслятор не обнаружит имени переменной или метки в своей таблице, то будет сформировано сообщение об ошибке и объектный модуль будет стёрт с диска компьютера.

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

EXTRN DATA (BufInd, ERR)
EXTRN CODE (Podprogr) Для того, чтобы редактор связей мог осуществить связывание модулей в единую программу, переменные и метки, объявленные по крайней мере в одном из модулей как EXTRN, в другом модуле должны быть объявлены как доступные для всех модулей при помощи директивы PUBLIC . Пример использования директивы PUBLIC на языке программирования ASM-51:

PUBLIC BufInd, Parametr
PUBLIC Podprogr, ?Podprogr?Byte

Использование нескольких модулей при написании программы увеличивает скорость трансляции и, в конечном итоге, скорость написания программы. Однако объявления переменных и имён подпрограмм внешних модулей загромождают исходный текст модуля. Кроме того, при использовании чужих модулей трудно объявить переменные и подпрограммы без ошибок. Поэтому обычно объявления переменных, констант и предварительные объявления подпрограмм хранят во включаемых файлах, которые называются файлами-заголовками. Правилом хорошего тона считается при разработке программного модуля сразу же написать файл-заголовок для этого модуля, который может быть использован программистами, работающими с Вашим программным модулем.

Для объединения нескольких модулей в исполняемую программу имена всех модулей передаются в редактор связей rl51.exe в качестве параметров при запуске этой программы. Пример вызова редактора связей из командной строки DOS для объединения трёх модулей:

rl51.exe progr.obj, modul1.obj, modul2.obj

В результате работы редактора связей в этом примере будет создан исполняемый модуль с именем progr. Формат записи информации в этом файле остаётся прежним - объектный. Это позволяет объединять модули по частям, то есть при желании можно из нескольких мелких модулей получить один более крупный.

В настоящее время часто пользуются интегрированными средами программирования, такими как Franclin или keil C, в состав которых входят язык программирования ASM?51. В этих программах создание строки вызова редактора связей производится автоматически при настройке программного проекта, а вызов этой строки производится при трансляции программного проекта.Настройка программного проекта происходит при подключении к проекту новых программных модулей и при изменении свойств программного проекта, таких как разрешение или запрет создания карты памяти программы, выбор папки для хранения выходных файлов, разрешение или запрет помещения в выходной файл отладочной информации разрешение или запрет создания загрузочного HEX файла.

[ ] [ ] [ ]


Отладка программ


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

Рисунок 2. Пример системы отладки программного обеспечения для микроконтроллеров.



Передача переменных-параметров в подпрограмму


В приведённом выше примере байт передаётся в подпрограмму через глобальную переменную G_Per. Однако программа будет эффективнее при использовании подпрограммы с параметрами. Мы знаем, что параметр подпрограммы - это локальная переменная. В этом случае могут значительно снизиться требования к памяти данных. Для размещения локальных переменных лучше всего использовать внутренние регистры процессора. На языке ASM51 для передачи параметра размерностью один байт обычно используется аккумулятор как показано в примере программы, приведённом на рисунке 2.

Рисунок 2. Пример подпрограммы - процедуры с передачей байта через аккумулятор.

Вызов такой подпрограммы на языке программирования С выглядел бы следующим образом:

Если в подпрограмму нужно передать двухбайтовое значение, то в качестве параметра подпрограммы используется пара регистров (обычно регистры R6-старший байт и R7-младший байт). Пример программы, передающей в подпрограмму двухбайтовое число, написанной на языке программирования ASM-51 приведён на рисунке 3.

Рисунок 3. Пример подпрограммы - процедуры с передачей двухбайтного числа через регистры R7 и R6.

Если в подпрограмму нужно передать четырёхбайтовое значение (это требуется для переменной, соответствующей типу long или float), то используются регистры R4...R7 (регистр R4 - старший байт):

Регистры R0 и R1 обычно используются в качестве указателей обрабатываемых переменных таких как строки или массивы. Если требуется, чтобы подпрограмма обработала значительный объём данных, как например:

то эти данные можно передать через параметр - указатель. В качестве указателя при обращении к внешней памяти данных или к памяти программ обычно используется регистр-указатель данных DPTR. Пример передачи в качестве параметра строки, написанный на языке программирования ASM-51 приведён на рисунке 4:

Рисунок 4. Пример подпрограммы - процедуры с передачей адреса строки через регистр DPTR.

При обращении к массивам или структурам, расположенным во внутренней памяти данных в качестве указателя адреса используется регистр R0 или R1. Пример передачи в подпрограмму массива в качестве параметра, написанный на языке программирования ASM-51 приведён на рисунке 5:

Рисунок 5. Пример подпрограммы - процедуры с передачей адреса массива через регистр R0.



Часто требуется передавать результат вычислений


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



Как видно из приведённого примера, использование подпрограмм-функций значительно увеличивает наглядность программ и приближает запись на языке программирования к записи математического выражения. На языке программирования ASM51 этот же вызов подпрограммы-функции выглядит следующим образом:

В этом примере подпрограмма вычисления синуса перед выполнением оператора возврата в основную программу должна поместить результат вычисления синуса в аккумулятор.

Подпрограмма - функция может возвращать и многобайтовые переменные, используя регистр - указатель R0 или R1. Однако для копирования этих переменных потребуется ещё одна подпрограмма копирования.


выносятся отдельно от основного


на языке программирования ASM- 51 выносятся отдельно от основного текста программы. Обычно подпрограммы размещают после основного текста программы для того, чтобы случайно не передать управление подпрограмме не оператором вызова подпрограммы. Это может произойти из-за того, что ассемблер назначает адреса операторам в порядке их написания.

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


Как известно, подпрограммы обработки прерываний


Как известно, подпрограммы обработки прерываний вызываются аппаратурой, поэтому эти подпрограммы не могут иметь параметров. Кроме того эти подпрограммы не могут быть подпрограммами-функциями. При возвращении из подпрограммы обработки прерывания должны быть разрешены дальнейшие прерывания, поэтому возврат из подпрограммы обработки прерывания может быть осуществлён только командой .

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



Рисунок 6. Пример подпрограммы обработки прерывания.

В приведённом примере директива CSEG AT 0BH использована для того, чтобы разместить подпрограмму обработки прерывания на вектор прерывания от таймера 0.

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



Рисунок 7. Пример подпрограммы обработки прерывания.

Если подпрограмма обработки прерывания использует несколько регистров, то на сохранение регистров в стеке и на восстановление их из стека тратится достаточно много времени. Микроконтроллеры семейства MCS-51 предлагают возможность использовать для подпрограмм прерываний отдельный . В языке программирования ASM-51 то, что программа использует не нулевой банк регистров отображается при помощи директивы USING как это показано в примере на рисунке 8.



Рисунок 8. Пример подпрограммы обработки прерывания с использованием второго банка регистров.

[] [] []


Подпрограмма процедура вызывается командами процессора


Подпрограмма процедура вызывается командами процессора и . В языке программирования ASM51 допустимо использования директивы CALL эта директива подбирает наиболее подходящую к данному случаю по размеру команду.

Пример подпрограммы управления:

Рисунок 1. Пример подпрограммы - процедуры.


Применение позволяет увеличить скорость написания


Применение позволяет увеличить скорость написания программ и облегчить отладку написанной программы.  Языки программирования C, PASCAL, PL/M разрабатывались на основе принципов структурного программирования, поэтому в состав этих языков программирования входят структурные операторы. Тем не менее структурное программирование возможно и на языках программирования низкого уровня в том числе и на языке программирования ASM-51, где не предусмотрено структурных операторов,

При написании программы с использование методов структурного программирования эта программа может быть оттранслирована и выполнена на любом этапе написания программы, при этом можно отследить все алгоритмические действия программы, написанные к этому времени. При использовании методов структурного программирования процесс написания программы не отличается от процесса создания алгоритма. Более того, эти этапы создания программы можно объединить.

Для реализации методов структурного программирования огромное значение имеет использование "говорящих меток", когда метки обозначаются не просто M0, M1 и т.д., а в названии метки отображается действие, выполняемое программой. Для людей, не владеющих иностранным языком ограничение в использовании для назначения меток букв только латинского алфавита создаёт определённые трудности. Тем не менее и латинскими буквами можно писать русские слова! При этом для обозначения действия может потребоваться несколько слов, использование же пробелов внутри метки недопустимо! Выйти из такой ситуации можно двумя способами:

использование специальных символов-разделителей; начинать каждое новое слово внутри метки с буквы верхнего регистра В качестве символов-разделителей можно использовать символы подчёркивания '_' и вопроса '?' Примеры назначения говорящих меток:

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

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

Наиболее распространённый структурный оператор называется . Любая задача может быть разбита на несколько подзадач. Выполнение подзадач лучше оформить как подпрограмму, в названии которой можно (и нужно) отразить подзадачу, которую должна решать эта подпрограмма. Например:

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

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



Рисунок 1. Пример использования подпрограмм для структурирования программы, написанной на языке программирования ASM-51.

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



Рисунок 2. Пример использования команд условного перехода для реализации условного оператора с одним плечом.

Полная схема  реализуется на языке ассемблер более сложным образом. Для реализации такого оператора потребуется уже две команды микроконтроллера. Для исключения выполнения второго плеча условного оператора потребуется команда безусловного перехода.


Пример реализации условного оператора приведён на рисунке 3.



Рисунок 3. Пример реализации условного оператора на языке программирования ASM-51.

Третий структурный оператор - это Такой оператор легко реализуется на языке программирования ассемблер при помощи команды условного или безусловного перехода. Отличие от условного оператора заключается в том, что передача управления осуществляется не вперёд, а назад. На языках программирования высокого уровня такой оператор входит в состав языка (оператор do..while в языке программирования C или оператор repeat..until в языке программирования PASCAL). На языке программирования ассемблер для реализации этого оператора можно воспользоваться любой условной операцией. Однако для реализации оператора цикла в системе команд микроконтроллера MCS-51 предусмотрена специальная команда, выполняющая сразу два алгоритмических действия - . Пример использования этой команды для реализации оператора цикла приведён на рисунке 4.



Рисунок 4. Пример оператор цикла с проверкой условия после тела цикла на языке программирования ASM-51.

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



Рисунок 5. Пример оператор цикла с проверкой условия до тела цикла на языке программирования ASM-51.

[] [] []


Исходный текст программы представляет собой


Исходный текст программы представляет собой последовательность языка, сгруппированных в сегменты и оформленных в виде файла.

Оператор - это базовая конструкция языка программирования, определяющая действия в программе. В языке программирования ASM-51 в одной строке может быть записан только один оператор! Максимальный размер строки - 255 символов. Признаком конца оператора является символ "возврат каретки".

Оператор состоит из трех полей:

<поле метки> <поле операции> <поле комментария>,

Любое из полей, в том числе и все поля, могут отсутствовать. Оператор, в котором все поля отсутствуют, называется пустым оператором. Он используется для увеличения наглядности программы.

Пример оператора, записанного на языке программирования ASM-51:

Поле метки используется для записи меток. Метки используются для организации условных и безусловных переходов, а также для объявления переменных и констант. Признаком конца поля метки является символ "двоеточие" (:). Однако язык программирования ASM-51, в виде исключения, допускает использовать как признак конца поля метки.

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

Пример использования оператора, содержащего только метку:

Поле операции используется для записи языка или , которые состоят из мнемонического обозначения и одного или нескольких операндов. В качестве операндов могут использоваться адреса ячеек памяти, обозначения регистров или метки операторов. Операнды отделяются друг от друга запятыми. Вместе с запятыми для увеличения читаемости программы допускается использование символов интервала.

Поле комментария начинается с символа "точка с запятой" (;). Это поле используется для записи пояснений к программе. Оператор, в котором присутствует только поле , используется для увеличения наглядности программы.

начинается с символа (;) и может содержать любые ASCII символы. Примеры комментариев:

;---------------------------------------------------------------------------------------- ; ПОДПРОГРАММА ВЫЧИСЛЕНИЯ ФУНКЦИИ ;---------------------------------------------------------------------------------------- ; X + Y * Z [] [] []


Целые типы данных


Для определения данных целого типа используются различные ключевые слова, которые определяют диапазон значений и размер области памяти, выделяемой под переменные (табл. 6).

Таблица 6

Тип Размер памяти в битах Размер памяти в байтах Диапазон значений
bit 1 от 0 до 1
char 8 1 от -128 до 127
unsigned shar 8 1 oт 0 до 255
int, short 16 2 от -32768 до 32767
long 32 4 от -2 147 483 648 до 2 147 483 647
unsigned int, unsigned short 16 2 от 0 до 65535
unsigned long 32 4 от 0 до 4 294 967 295
sbit 1   0 или 1
sfr 8 1 oт 0 до 255
sfr16 16 2 от 0 до 65535

Отметим, что ключевые слова signed и unsigned необязательны. Они указывают, как интерпретируется нулевой бит объявляемой переменной, т.е., если указано ключевое слово unsigned, то нулевой бит интерпретируется как часть числа, в противном случае нулевой бит интерпретируется как знаковый. В случае отсутствия ключевого слова unsigned целая переменная считается знаковой. В том случае, если спецификатор типа состоит из ключевого типа signed или unsigned и далее следует идентификатор переменной, то она будет рассматриваться как переменная типа int. Например:

unsigned int n; //Беззнаковое шестнадцатиразрядное число n unsigned int b; int c; //подразумевается signed int c unsigned d; //подразумевается unsigned int d signed f; //подразумевается signed int f

Отметим, что модификатор типа char используется для представления одиночного символа или для объявления строковых литералов. Значением объекта типа char является код (размером 1 байт), соответствующий представляемому символу.

Отметим также, что восьмеричные и шестнадцатеричные константы также могут иметь модификатор unsigned. Это достигается указанием суффикса u или U после константы, константа без этого префикса считается знаковой.

Например:

0xA8C //int signed 01786l //long signed 0xF7u //int unsigned

Числа с плавающей запятой


Для переменных, представляющих используется модификатор типа float. Модификатор double тоже допустим в языке программирования C51, но он не приводит к увеличению точности результата.

Величина с модификатором типа float занимает 4 байта. Из них 1 байт отводится для знака, 8 бит для избыточной экспоненты и 23 бита для мантиссы. Отметим, что старший бит мантиссы всегда равен 1, поэтому он не заполняется, в связи с этим диапазон значений переменной с плавающей точкой равен от ±1.175494E-38 до ±3.402823E+38.

Пример объявления переменной с плавающей запятой:

float f, a, b;

Идентификаторы


Идентификаторы в языке программирования C-51 используются для определения имени переменной, подпрограммы, символической константы или метки оператора. Длина идентификатора может достигать 255 символов, но транслятор различает идентификаторы только по первым 31 символам.

Возникает вопрос - а зачем тогда нужен такой длинный идентификатор? Ответ: для создания подпрограммы или переменной, которое может состоять из нескольких слов. Например:

ProchitatPort(); //Прочитать порт VklychitIndikator(); //Включить индикатор

В приведенном примере подпрограмма ProchitatPort выполняет действия необходимые для чтения порта, а подпрограмма VklychitIndikator выполняет действия, необходимые для зажигания индикатора. Естественно, что намного легче прочитать действие подпрограммы непосредственно из имени подпрограммы, чем лазить каждый раз в алгоритм программы или искать исходный текст подпрограммы для того чтобы в очередной раз разобраться - что же она выполняет? Для этого при объявлении имени подпрограммы можно потратить количество символов и большее чем 31!

То же самое можно сказать и про имена переменных. Например:

sbit ReleVklPitanija = 0x80; //К нулевому выводу порта P0 подключено реле включения питания sbit svDiod = 0x81; //К первому выводу порта P0 подключен светодиод sbit DatTemperat = 0x82; //Ко второму выводу порта P0 подключен датчик температуры

В приведённом примере каждой ножке микроконтроллера назначается переменная с именем, отображающим устройство, подключённое к этой ножке. В результате при чтении программы не потребуется каждый раз обращаться к принципиальной схеме устройства каждый раз, как только производится операция записи или чтения переменной, связанной с портами микроконтроллера. (Разбираться с принципиальной схемой занятие не менее "увлекательное" по сравнению с поиском неизвестной и неизвестно что выполняющей подпрограммы)

В качестве может быть использована любая последовательность строчных или прописных букв латинского алфавита и цифр, а также символов подчёркивания '_'.
Идентификатор может начинаться только с буквы или символа '_', но ни в коем случае с цифры. Это позволяет программе-транслятору различать идентификаторы и числовые константы. Строчные и прописные буквы в идентификаторе различаются. Например: идентификаторы abc и ABC, A128B и a128b воспринимаются как разные.

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

Идентификатор не должен совпадать с ключевыми словами, с зарезервированными словами и именами функций из библиотеки компилятора языка С. Следует обратить особое внимание на использование символа подчеркивание (_) в качестве первого символа идентификатора, поскольку идентификаторы, построенные таким образом, могут совпадать с именами системных функций или переменных, в результате чего они станут недоступными. Следует отметить, что никто не запрещает объявлять идентификатор, совпадающий с именами функций из библиотек компилятора языка С. Однако после объявления такого идентификатора Вы не сможете обратиться к функции с таким же именем никаким образом.

Примеры правильных идентификаторов:

A XYR_56 OpredKonfigPriem Byte_Prinjat SvdiodGorit

Инициализация данных


В языке программирования C-51, как и в других версиях языка C при объявлении переменной ей можно присвоить начальное значение, присоединяя инициатор к описателю. При этом во время запуска Вашей программы в ячейки памяти, соответствующие этим переменным будут записаны начальные значения. Только после этого выполнение программы будет передано подпрограмме main();.

Инициатор переменной начинается со знака "=" и может быть записан в следующих форматах:

Формат 1: = инициатор;
Формат 2: = { список - инициаторов };

Формат 1 используется при инициализации переменных основных типов и указателей, а формат 2 - при инициализации составных объектов.

Примеры присваивания первоначальных значений простым переменным:

char tol = 'N'; //Переменная tol инициализируется символом 'N'. const long megabyte = (1024*1024);

Немодифицируемой переменной megabyte присваивается значение константного выражения, после чего эта переменная не может быть изменена. Отмечу, что для микроконтроллеров семейства MCS-51 внутренняя память является дефицитным ресурсом, поэтому использовать ее для хранения констант нерационально. Лучше объявить переменную с спецификатором типа памяти code.

static int b[2][2] = {1,2,3,4};

Инициализируется двухмерный массив b целых величин, элементам массива присваиваются значения из списка. Эта же инициализация может быть выполнена следующим образом:

static int b[2][2] = { { 1,2 }, { 3,4 } };

При инициализации массива можно опустить одну или несколько размерностей

static int b[3] = { { 1,2 }, { 3,4 } };

Если при инициализации указано меньше значений для строк, то оставшиеся элементы инициализируются 0, т.е. при описании

static int b[2][2] = { { 1,2 }, { 3 } };

элементы первой строки получат значения 1 и 2, а второй 3 и 0.

При инициализации составных объектов, нужно внимательно следить за использованием скобок и списков инициализаторов.

Примеры:

struct complex {float real; float imag; }comp[2][3]={{{1,1},{2,3},{ 4, 5}}, {{6,7},{8,9},{10,11}} };

В данном примере инициализируется массив структур comp из двух строк и трех столбцов, где каждая структура состоит из двух элементов real и imag.


struct complex comp2 [2][3] = { {1,1},{2,3},{4,5},{6,7},{8,9},{10,11} }; В этом примере компилятор интерпретирует рассматриваемые фигурные скобки следующим образом:

первая левая фигурная скобка - начало составного инициатора для массива comp2; вторая левая фигурная скобка - начало инициализации первой строки массива comp2[0]. Значения 1,1 присваиваются двум элементам первой структуры; первая правая скобка (после 1) указывает компилятору, что список инициаторов для строки массива окончен, и элементы оставшихся структур в строке comp[0] автоматически инициализируются нулем; аналогично список {2,3} инициализирует первую структуру в строке comp[1], а оставшиеся структуры массива обращаются в нули; на следующий список инициализаторов {4,5} компилятор будет сообщать о возможной ошибке так как строка 3 в массиве comp2 отсутствует. При инициализации объединения задается значение первого элемента объединения в соответствии с его типом.

Пример:

union tab {unsigned char name[10]; int tab1; }pers={'A','H','T','O','H'}; Инициализируется переменная pers.name, и так как это массив, для его инициализации требуется список значений в фигурных скобках. Первые пять элементов массива инициализируются значениями из списка, остальные нулями.

Инициализацию массива символов можно выполнить при помощи литеральной строки.

char stroka[ ] = "привет"; Инициализируется массив символов из 7 элементов, последним элементом (седьмым) будет символ '\0', которым завершаются все литеральные строки.

В случае, если задается размер массива, а литеральная строка длиннее, чем размер массива, то лишние символы отбрасываются. Следующее объявление инициализирует переменную stroka как массив, состоящий из семи элементов.

char stroka[5]="привет"; В переменную stroka попадают первые пять элементов литерала, а символы 'т' и '\0' отбрасываются. Если строка короче размерности массива, то оставшиеся элементы массива заполняются нулями. Отметим, что инициализация переменной типа tab может иметь следующий вид:

union tab pers1="Антон"; и, таким образом, в символьный массив попадут символы:

'А','Н','Т','О','Н','\0',

а в остальные элементы будут записаны нули.

* Для тех читателей что вышли на эту страницу по поиску прошу обратить внимание, что здесь рассматривается не стандартный язык программирования С, а язык, адаптированный к микроконтроллерам . Имеются отличия!

[ ]


Исполняемые операторы


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



Использование подпрограмм в языке программирования С-51*


В отличие от других языков программирования высокого уровня в языке С нет деления на основную программу, и . В этом языке вся программа строится только из функций. Мощность языка С во многом определяется легкостью и гибкостью объявления и реализации функций.

Функция - это совокупность объявлений и операторов, обычно предназначенная для решения определенной задачи. Термин функция в языке программирования С эквивалентен понятию . Действия, выполняемые основной программой в других языках программирования такие как очистка внутреннего ОЗУ и присваивание начального значения переменным, выполняются автоматически при включении питания. После завершения этих действий вызывается подпрограмма с именем main. Каждая функция должна иметь имя, которое используется для ее объявления, определения и вызова.

В любой программе, написанной на языке программирования С-51 должна быть функция с именем main (главная функция), именно с этой функции, в каком бы месте программы она не находилась, начинается выполнение программы. Обратите внимание, что на языке программирования С-51 пишутся программы для микроконтроллеров. Поэтому эти программы не должны завершаться пока включено питание микроконтроллера. Это значит, что в функции main обязательно должен быть бесконечный цикл. В противном случае при выходе из этой функции управление будет передано случайному адресу памяти программ. Это может привести к непредсказуемым результатам, вплоть до выхода микроконтроллерной системы из строя.

При вызове функции ей при помощи аргументов (формальных параметров) могут быть переданы некоторые значения (фактические параметры), используемые во время выполнения функции. Функция может возвращать некоторое, но только одно, значение. Это возвращаемое значение и есть результат выполнения функции, который при выполнении программы подставляется в выражение, из которого производился вызов функции.

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

С использованием функций в языке С-51 связаны три понятия - определение функции (описание действий, выполняемых подпрограммой-функцией), объявление функции (задание формы обращения к функции) и вызов функции.



это язык программирования общего назначения,


Язык программирования C – это язык программирования общего назначения, предназначенный для написания программ, эффективных по исполняемому коду с элементами структурного программирования и богатым набором операторов. Язык программирования C практически не имеет ограничений, что позволяет использовать язык программирования C для эффективного решения широкого круга задач. Однако при написании программ для микроконтроллеров необходимо учитывать особенности построения аппаратуры этих микросхем.

В состав языка программирования С-51 введён ряд изменений, отображающих особенности построения памяти микроконтроллеров семейства MCS-51. Кроме того, эти изменения позволяют непосредственно обращаться к встроенным портам, таймерам и другим устройствам микроконтроллеров указанного семейства. Особенности микроконтроллеров этого семейства в основном отображаются через описания переменных.

Язык программирования С-51 удовлетворяет стандарту ANSI-C. Этот язык программирования предназначен для получения компактных и быстродействующих программ, предназначенных для микроконтроллеров семейства MCS-51. Язык С-51 обеспечивает гибкость программирования на широко известном языке программирования C, при скорости работы и компактности, сравнимой с программами, написанными на языке программирования ассемблер.

Так как язык программирования C не имеет собственных средств ввода и вывода, то он обращается к соответствующим функциям операционных систем. В языке программирования С51 вместо этого имеется возможность изменять библиотечные функции, и тем самым обращаться к конкретным ячейкам памяти микроконтроллера, для которого пишется программа.

Компилятор c51.exe - это программное средство, которое исходный текст, написанный на языке программирования C51 в перемещаемые объектные модули. Эти модули затем могут объединяться с другими модулями, написанными на языках программирования C51, PLM-51 или ASM-51. Компилятор выводит на экран дисплея или в файлы листингов сообщения об ошибках и вспомогательную информацию, которая может быть использована при отладке и разработке программ.

Компилятор c51.exe может быть установлен на компьютерах серии IBM или совместимых с ними в операционной системе DOS 3.Х и выше и использоваться для генерации команд микроконтроллеров семейства MCS-51.


Категории типов данных


Ключевые слова для определения основных типов данных

Целые типы данных: Типы данных с плавающей запятой: bit float sbit char int short long signed unsigned sfr sfr16

Переменная любого типа может быть объявлена как неизменяемая. Это достигается добавлением ключевого слова const к спецификатору типа. Объекты с типом const представляют собой данные, используемые только для чтения, т.е. этой переменной не может быть присвоено новое значение. Отметим, что если после слова const отсутствует спецификатор типа, то подразумевается спецификатор типа int. Если ключевое слово const стоит перед объявлением составных типов (массив, структура, смесь, перечисление), то это приводит к тому, что каждый элемент также должен являться немодифицируемым, т.е. значение ему может быть присвоено только один раз.

Примеры использования ключевого слова const:

const float A=2.128E-2; const B=286; //подразумевается const int B=286

Отметим, что переменные с спецификатором класса памяти размещаются во внутреннем ОЗУ. Неизменяемость контролируется только на этапе трансляции. Для размещения переменной в ПЗУ лучше воспользоваться спецификатором типа памяти code



Ключевые слова


- это зарезервированные слова, которые используются для построения операторов языка.

Список ключевых слов:

alien _at_ auto bdata bit break case char code compact continue data default do double else enum extern far float for idata if int interrupt large long pdata _priority_ reentrant register return sbit sfr sfr16 signed sizeof short small struct switch typedef _task_ union unsigned void volatile while

Ключевые слова не могут быть использованы в качестве идентификаторов.



Константы


Константы предназначены для введения чисел в состав выражений операторов языка программирования C. В отличие от идентификаторов, всегда начинающихся с буквы, константы всегда начинаются с цифры. В языке программирования С-51 разделяют четыре типа констант:

целые и константы, константы с , и .

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

Десятичная константа состоит из одной или нескольких десятичных цифр, причем первая цифра не может быть нулем (иначе число будет воспринято как восьмеричное).

Восьмеричная константа состоит из обязательного нуля и одной или нескольких восьмеричных цифр (среди цифр должны отсутствовать цифры восемь и девять, так как эти цифры не входят в восьмеричную систему счисления). Если константа содержит цифру, недопустимую в восьмеричной системе счисления, то константа считается ошибочной.

Шестнадцатеричная константа начинается с обязательной последовательности символов 0х или 0Х и содержит одну или несколько шестнадцатеричных цифр (0,1,2,3,4,5,6,7,8,9,A,B,C,D,E,F)

Примеры целых констант: Десятичная Восьмеричная Шестнадцатеричная константа константа константа 16 020 0x10 127 0117 0x2B 240 0360 0XF0

Если требуется сформировать отрицательную целую константу, то используют знак "-" перед записью константы (который будет называться унарным минусом). Например: -0x2A, -088, -16.

Каждой целой константе присваивается тип, определяющий преобразования, которые должны быть выполнены, если константа используется в выражениях. Тип константы определяется следующим образом:


- десятичные константы рассматриваются как , и им присваивается тип int (целая) или long (длинная целая) в соответствии со значением константы. Если константа меньше 32768, то ей присваивается тип int в противном случае long.

- восьмеричным и шестнадцатеричным константам присваивается тип int, unsigned int (беззнаковая целая), long или unsigned long в зависимости от значения константы согласно табл 5.

Таблица 5

Диапазон шестнадцатеричных константДиапазон восьмеричных константТип
0x0 - 0x7FFF 0 - 077777 int
0X8000 - 0XFFFF 0100000 - 0177777 unsigned int
0X10000 - 0X7FFFFFFF 0200000 - 017777777777 long
0X80000000 - 0XFFFFFFFF 020000000000 - 037777777777 unsigned long
Иногда требуется с самого начала интерпретировать константу как длинное целое число. Для того чтобы любую целую константу определить типом long, достаточно в конце константы поставить букву "l" или "L". Пример:

5l, 6l, 128L, 0105L, OX2A11L.

Примеры синтаксически недопустимых целочисленных констант:

12AF - шестнадцатеричная константа не имеет символов 0x в начале константы, поэтому по умолчанию для нее принимается десятичная система счисления, но тогда в ней присутствуют недопустимые символы.

0x2ADG - символ G недопустим при записи шестнадцатеричных чисел.

Константа с плавающей запятой - это десятичное число, представленное в виде действительного числа с десятичной запятой и порядком числа. Формат записи константы с плавающей запятой:

[ цифры ].[ цифры ] [ Е|e [+|-] цифры ].

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

115.75, 1.5Е-2, -0.025, .075, -0.85Е2

- представляется ASCII или ANSI символом, заключенном в апострофы. тоже может быть использована в символьных константах. При этом она рассматривается как одиночный символ. Значением символьной константы является числовой код символа.


Примеры символьных констант:

' '- пробел , 'Q'- буква Q , '\n' - символ новой строки , '\\' - обратная дробная черта , '\v' - вертикальная табуляция . Символьные константы имеют тип int и при преобразовании типов дополняются знаком. Символьные константы используются обычно при управлении микроконтроллерным устройством от клавиатуры. Пример использования символьной константы на языке программирования C-51 приведён ниже:

if(NajKn=='p') VklUstr(); В этом примере если в переменной NajKn содержится код, соответствующий букве "p", то будет выполнена подпрограмма VklUstr.

Строковые константы. Если символьные константы используются обычно при вводе информации с клавиатуры, то при отображении информационных сообщений обычно используются целые строки символов. В строке допускается использование пробелов.

Строковая константа (литерал или литеральная строка) - последовательность символов (включая строковые и прописные буквы русского и латинского а также цифры) заключенные в кавычки ("). Например: "Школа N 35", "город Тамбов", "YZPT КОД".

Отметим, что все управляющие символы, кавычка ("), обратная дробная черта (\) и символ новой строки в литеральной строке и в символьной константе представляются соответствующими управляющими последовательностями. Каждая управляющая последовательность представляет собой один символ. Например, при печати литерала "Школа \n N 35" его часть "Школа" будет напечатана на одной строке, а вторая часть "N 35" на следующей строке.

Символы литеральной строки обычно хранятся в , но могут храниться и в . В конец каждой литеральной строки компилятором добавляется нулевой символ, который можно записать как: "\0". Именно этот символ и является признаком конца строки.

Литеральная строка рассматривается как массив символов (char[ ]). Отметим важную особенность: число элементов массива равно числу символов в строке плюс 1, так как нулевой символ (символ конца строки) также является элементом массива.


Все литеральные строки рассматриваются компилятором как различные объекты. Одна литеральная строка может выводиться на дисплей как несколько строк. Такие строки разделяются при помощи обратной дробной черты и символа возврата каретки \n. На одной строке исходного текста программы можно записать только одну литеральную строку. Если необходимо продолжить написание одной и той же литеральной строки на следующей строке исходного текста программы, то в конце строки исходного текста можно поставить обратную строку. Например исходный текст:

"строка неопределенной \ длины" полностью идентичен литеральной строке:

"строка неопределенной длины".

Однако более удобно для объединения литеральных строк использовать символ (символы) пробела. Если в программе встречаются два или более литерала, разделенные только пробелами или символами табуляции, то они будут рассматриваться как одна литеральная строка. Этот принцип можно использовать для формирования литералов, занимающих более одной строки.

* Для тех читателей что вышли на эту страницу по поиску прошу обратить внимание, что здесь рассматривается не стандартный язык программирования С, а язык, адаптированный к микроконтроллерам . Имеются отличия!

[ ]


Лексические единицы, разделители и использование пробелов*


Наименьшей единицей операторов C-51 является лексическая единица. Каждая из лексических единиц относится к одному из классов:

; ; простые ограничители (все , кроме _, являются простыми ограничителями); составные ограничители (они образуются посредством определенных комбинаций двух спецсимволов, а именно: !=,+=,-=,*=,<<,>>, <=, >=, /*, */,//); ; ;

В большинстве случаев вполне очевидно, где заканчивается одна лексическая единица и начинается следующая. Например, в операторе присваивания:

X=AP*(FT-3)/A;

X, AP, FT, A - являются идентификаторами переменных;
3 - числовой константой;
все прочие символы - простыми ограничителями.

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



Нетипизированные указатели


Нетипизированные указатели объявляются точно так же, как указатели в стандартном языке программирования C. Для того, чтобы не зависеть от типа памяти, в которой может быть размещена переменная, для нетипизированных указателей выделяется 3 байта. В первом байте указывается вид памяти переменной, во втором байте - старший байт адреса, в третьем - младший байт адреса переменной. Нетипизированные указатели могут быть использованы для обращения к любым переменным независимо от типа памяти микроконтроллера. Именно поэтому многие библиотечные функции языка программирования C51 используют указатели этого типа, при этом им совершенно неважно, в какой именно области памяти размещаются переменные. Приведем листинг, в котором отображаются особенности трансляции нетипизированных указателей:

stmt level source 1 char *c_ptr; /* char ptr */ 2 int *i_ptr; /* int ptr */ 3 long *l_ptr; /* long ptr */ 4 5 void main (void) 6 { 7 1 char data dj; /*переменные во внутренней памяти данных data */ 8 1 int data dk; 9 1 long data dl; 10 1 11 1 char xdata xj; /*переменные во внешней памяти данных xdata */ 12 1 int xdata xk; 13 1 long xdata xl; 14 1 15 1 char code cj = 9; /*переменные в памяти программ code */ 16 1 int code ck = 357; 17 1 long code cl = 123456789; 18 1 19 1 /*настроим указатели на внутреннюю память данных data */ 20 1 c_ptr = &dj; 21 1 i_ptr = &dk; 22 1 l_ptr = &dl; 23 1 /*настроим указатели на внешнюю память данных xdata */ 24 1 c_ptr = &xj; 25 1 i_ptr = &xk; 26 1 l_ptr = &xl; 27 1 /*настроим указатели на память программ code */ 28 1 c_ptr = &cj; 29 1 i_ptr = &ck; 30 1 l_ptr = &cl; 31 1 } ASSEMBLY LISTING OF GENERATED OBJECT CODE ; FUNCTION main (BEGIN) ; SOURCE LINE # 5 ; SOURCE LINE # 6 ; SOURCE LINE # 20 0000 750000 R MOV c_ptr,#00H 0003 750000 R MOV c_ptr+01H,#HIGH dj 0006 750000 R MOV c_ptr+02H,#LOW dj ; SOURCE LINE # 21 0009 750000 R MOV i_ptr,#00H 000C 750000 R MOV i_ptr+01H,#HIGH dk 000F 750000 R MOV i_ptr+02H,#LOW dk ; SOURCE LINE # 22 0012 750000 R MOV l_ptr,#00H 0015 750000 R MOV l_ptr+01H,#HIGH dl 0018 750000 R MOV l_ptr+02H,#LOW dl ; SOURCE LINE # 24 001B 750001 R MOV c_ptr,#01H 001E 750000 R MOV c_ptr+01H,#HIGH xj 0021 750000 R MOV c_ptr+02H,#LOW xj ; SOURCE LINE # 25 0024 750001 R MOV i_ptr,#01H 0027 750000 R MOV i_ptr+01H,#HIGH xk 002A 750000 R MOV i_ptr+02H,#LOW xk ; SOURCE LINE # 26 002D 750001 R MOV l_ptr,#01H 0030 750000 R MOV l_ptr+01H,#HIGH xl 0033 750000 R MOV l_ptr+02H,#LOW xl ; SOURCE LINE # 28 0036 7500FF R MOV c_ptr,#0FFH 0039 750000 R MOV c_ptr+01H,#HIGH cj 003C 750000 R MOV c_ptr+02H,#LOW cj ; SOURCE LINE # 29 003F 7500FF R MOV i_ptr,#0FFH 0042 750000 R MOV i_ptr+01H,#HIGH ck 0045 750000 R MOV i_ptr+02H,#LOW ck ; SOURCE LINE # 30 0048 7500FF R MOV l_ptr,#0FFH 004B 750000 R MOV l_ptr+01H,#HIGH cl 004E 750000 R MOV l_ptr+02H,#LOW cl ; SOURCE LINE # 31 0051 22 RET ; FUNCTION main (END)

Объединения (смеси)


Главной особенностью объединения является то, что для каждого из объявленных элементов этого объединения выделяется одна и та же область памяти, т.е. они перекрываются. Хотя доступ к этой области памяти возможен с использованием любого из элементов, элемент для этой цели должен выбираться так, чтобы полученный результат не был бессмысленным.

Объединение применяется для следующих целей:

использования одного и той же области памяти для размещения переменных различного типа; интерпретации представления переменной одного типа, как несколько переменных другого типа.

Объединение по описанию подобно структуре. Тип объединения может задаваться в следующем виде:

union { описание элемента 1; ... описание элемента n; };

Доступ к элементам объединения осуществляется тем же способом, что и к структурам.

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

Второй вариант можно проиллюстрировать следующим образом. Например, требуется передать число плавающего типа. Однако последовательный порт может передавать или принимать только однобайтовые числа. В этом случае можно воспользоваться объединением:

union {float Koeff; //Интерпретация объединения как переменной плавающего типа char byte[4];//Интерпретация объединения как массива } bufer; //Объявление переменной bufer

Объединение bufer позволяет последовательному порту получить отдельный доступ ко всем байтам числа bufer.Koeff начиная от младшего байта bufer.byte[0], и заканчивая старшим байтом bufer.byte[3]. В программе затем можно пользоваться загруженным числом как числом с плавающей запятой.

[ ]



это группа элементов одинакового типа


Массивы - это группа элементов одинакового типа (float, char, int и т.п.). Из объявления массива компилятор должен получить информацию о типе элементов массива и их количестве. Объявление массива имеет два формата:

спецификатор-типа описатель [константное-выражение];
спецификатор-типа описатель [ ];

Описатель - это идентификатор массива.

Спецификатор-типа задает тип элементов объявляемого массива. Элементами массива не могут быть функции и элементы типа void.

Константное-выражение в квадратных скобках задает количество элементов массива. Константное- выражение при объявлении массива может быть опущено в следующих случаях:

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

Каждое константное-выражение в квадратных скобках определяет число элементов по данному измерению массива, так что объявление двухмерного массива содержит два константных-выражения, трехмерного - три и т.д. Отметим, что в языке СИ первый элемент массива имеет индекс равный 0.

Примеры:

int a[2][3]; /* представлено в виде матрицы a[0][0] a[0][1] a[0][2] a[1][0] a[1][1] a[1][2] */ double b[10]; /* вектор из 10 элементов имеющих тип double */ int w[3][3] = { { 2, 3, 4 }, { 3, 4, 8 }, { 1, 0, 9 } }; В последнем примере объявлен массив w[3][3]. Списки, выделенные в фигурные скобки, соответствуют строкам массива, в случае отсутствия скобок инициализация будет выполнена неправильно.

В языке СИ можно использовать сечения массива, как и в других языках высокого уровня (PL1 и т.п. ), однако на использование сечений накладывается ряд ограничений. Сечения формируются вследствие опускания одной или нескольких пар квадратных скобок.
Пары квадратных скобок можно отбрасывать только справа налево и строго последовательно. Сечения массивов используются при организации вычислительного процесса в функциях языка СИ, разрабатываемых пользователем.

Примеры:

int s[2][3]; Если при обращении к некоторой функции написать s[0], то будет передаваться нулевая строка массива s.

int b[2][3][4]; При обращении к массиву b можно написать, например, b[1][2] и будет передаваться вектор из четырех элементов, а обращение b[1] даст двухмерный массив размером 3 на 4. Нельзя написать b[2][4], подразумевая, что передаваться будет вектор, потому что это не соответствует ограничению наложенному на использование сечений массива.

Для работы с символьными строками в языке программирования С используются массивы символов, например:

char str[] = "объявление массива символов"; Следует учитывать, что в символьной строке находится на один элемент больше, так как последним элементом строки должен быть '\0'. В этом примере использовано неявное задание длины массива символов. Это стало возможным так как массиву сразу присваивается конкретное значение. При программировании микроконтроллеров семейства MCS-51 такое задание массива может привести к неоправданному расходу , поэтому лучше воспользоваться размещением строки в :

char code str[] = "объявление массива символов";


Объявление новых типов переменных


В языке программирования C-51 имеется возможность заранее объявить тип переменной, а затем воспользоваться им при объявлении переменных. Использование заранее объявленного типа позволяет при объявлении переменной сократить его длину, избежать ошибок при объявлении переменных в разных местах программы и добиться полной идентичности объявляемых переменных.

Объявить новый тип переменной можно двумя способами. Первый способ – указать имя типа при объявлении структуры, объединения или перечисления, а затем использовать это имя в объявлении переменных и функций. Второй – использовать для объявления типа ключевое слово typedef.

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

Отметим, что любой тип может быть объявлен с использованием ключевого слова typedef, включая типы указателей, функций или массивов. Имя с ключевым словом typedef для типов указателя, структуры, объединения может быть объявлено прежде чем эти типы будут определенны, но в пределах видимости объявителя.

Примеры объявления и использования новых типов:

typedef float (* MATH)( ); // MATH - новое имя типа, представляющее указатель на функцию, возвращающую значения типа float typedef char FIO[40] // FIO - массив из сорока символов MATH cos; // cos указатель на функцию, возвращающую значения типа  double // Можно провести эквивалентное объявление float (* cos)( ); FIO person; //Переменная person - массив из сорока символов // Это эквивалентно объявлению char person[40];

При объявлении переменных и типов здесь были использованы имена типов (MATH FIO). Помимо объявления переменных, имена типов могут еще использоваться в трех случаях: в списке формальных параметров при объявлении функций, в операциях приведения типов и в операции sizeof .



Объявление переменных в языке программирования C-51*


В языке программирования C51 любая переменная должна быть объявлена до первого использования этой переменной. Как уже говорилось ранее, этот язык программирования предназначен для написания программ для микроконтроллеров семейства MCS-51, поэтому в составе языка должна отображаться внутренняя структура этого семейства микроконтроллеров. Эти особенности отражены во введении новых типов данных. В остальном язык программирования C-51 не отличается от стандарта ANSI.

Объявление переменной в языке программирования C51 представляется в следующем виде:

[спецификатор класса памяти] спецификатор типа [спецификатор типа памяти] описатель [=инициатор] [,описатель [= инициатор] ]...

Описатель - идентификатор простой переменной либо более сложная конструкция с квадратными скобками, круглыми скобками или звездочкой (набором звездочек).

Спецификатор типа - одно или несколько ключевых слов, определяющие тип объявляемой переменной. В языке СИ имеется стандартный набор типов данных, используя который можно сконструировать новые (уникальные) типы данных.

Инициатор - задает начальное значение или список начальных значений, которые (которое) присваивается переменной при объявлении.

Спецификатор класса памяти - определяется одним из четырех ключевых слов языка С: auto, extern, register, static, и указывает, каким образом будет распределяться память под объявляемую переменную, с одной стороны, а с другой, область видимости этой переменной, т.е., из каких частей программы можно к ней обратиться.

Спецификатор типа памяти - определяется одним из шести ключевых слов языка С-51: code, data, idata, bdata, xdata, pdata и указывает, в какой области будет размещена переменная.



Объявление указателей в языке программирования C-51*


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

Для инициализации указателя (записи начального адреса переменной) можно использовать идентификатор переменной, при этом в качестве идентификатора может выступать имя переменной, массива, структуры, литеральной строки.

При объявлении переменной - указателя, необходимо определить тип объекта данных, адрес которых будет содержать переменная, и имя указателя с предшествующей звездочкой (или группой звездочек). Формат объявления указателя:

спецификатор-типа [ модификатор ] *описатель.

Спецификатор-типа задает тип объекта и может быть любого основного типа, структуры или смеси (об этих типах будет сказано ниже). Задавая вместо спецификатора-типа ключевое слово void, можно отсрочить определение типа, на который ссылается указатель. Переменная, объявляемая как указатель на тип void, может быть использована для ссылки на объект любого типа. Однако для того, чтобы можно было выполнить арифметические и логические операции над указателями или над объектами, на которые они указывают, необходимо при выполнении каждой операции явно определить тип объектов. Такие определения типов могут быть выполнены с помощью операции приведения типов.

Примеры объявления указателей на различные типы переменных:

unsigned int * ptr; /* переменная ptr представляет собой указатель на ) переменную*/ float * x; /* переменная х указывает на */ char *buffer ; /*объявляется указатель с именем buffer который указывает на */

Теперь для того, чтобы начать работать с этими указателями достаточно их инициализировать. Например:

ptr=&A; //Присвоить адрес переменной A *ptr=2+2;//Работаем с переменной A a=&B; *ptr=3*4;//А теперь работаем с переменной B

В качестве модификаторов при объявлении указателя могут выступать ключевые слова const, data, idata, xdata, code.
Ключевое слово const указывает, что указатель не может быть изменен в программе. Размер переменной, объявленной как указатель, зависит от модификатора и используемого вида памяти, для которой будет компилироваться программа. Указатели на различные типы данных не обязательно должны иметь одинаковую длину.

Для модификации размера указателя можно использовать ключевые слова data, idata, xdata, code.

Задавая вместо спецификатора типа, ключевое слово void, можно отсрочить определение типа, на который ссылается указатель. Переменная, объявляемая как указатель на тип void, может быть использована для ссылки на объект любого типа. Однако для того, чтобы можно было выполнить арифметические и логические операции над указателями или над объектами, на которые они указывают, необходимо при выполнении каждой операции явно определить тип объектов. Такие определения типов могут быть выполнены с помощью операции приведения типов.

float nomer; void *addres; addres = &nomer; (float *)addres ++; /* Переменная addres объявлена как указатель на объект любого типа. Поэтому ей можно присвоить адрес любого объекта (& - операция вычисления адреса). Однако, как было отмечено выше, ни одна арифметическая операция не может быть выполнена над указателем, пока не будет явно определен тип данных, на которые он указывает. Это можно сделать, используя операцию приведения типа (float *) для преобразования типа указателя addres к типу float. Затем оператор ++ отдаёт приказ перейти к следующему адресу.*/ В качестве модификаторов при объявлении указателя могут выступать ключевые слова const, data, idata, xdata, code. Ключевое слово const указывает, что указатель не может быть изменен в программе.

Вследствие уникальности архитектуры контроллера 8051 и его производных компилятор С51 поддерживает 2 вида указателей: память-зависимые и нетипизированные.


Оператор безусловного перехода goto


Оператор goto изменяет порядок выполнения программы при помощи передачи управления на оператор, метка которого указана в операторе goto. Оператор goto записывается в следующем виде:

goto имя-метки; ... имя-метки: оператор;

Оператор goto передает управление на оператор, помеченный меткой имя-метки. Помеченный оператор должен находиться в той же функции, что и оператор goto, а используемая метка должна быть уникальной, т.е. одно имя-метки не может быть использовано для разных операторов программы. Имя-метки - это идентификатор.

Любой оператор в составном операторе может иметь свою метку. Используя оператор goto, можно передавать управление внутрь составного оператора. Но нужно быть осторожным при входе в составной оператор, содержащий объявления переменных с инициализацией, так как объявления располагаются перед выполняемыми операторами и значения объявленных переменных при таком переходе будут не определены.

Использование операторов goto является неизбежным при некоторых ситуациях, однако в большинстве случаев там, где должна быть предусмотрена передача управления, более предпочтительным является использование итеративного оператора while, do while, DO CASE, if или вызова процедуры. Неограниченное использование операторов goto в программе приводит к тому, что программу становится трудно понимать, модифицировать и сопровождать. Реальная практика использования языка C-51 показывает, что в большинстве случаев программные модули могут не содержать оператор goto без ухудшения их эффективности.



Оператор break


Оператор break обеспечивает прекращение выполнения самого внутреннего из объединяющих его операторов , , ,. После выполнения оператора break управление передается оператору, следующему за прерванным оператором цикла или выбора.



Оператор цикла for


Оператор for - это наиболее общий способ организации цикла. Оператор цикла for записывается в следующей форме:

for ( выражение 1 ; выражение 2 ; выражение 3 ) тело цикла;

Выражение 1 обычно используется для установления начального значения переменных, управляющих циклом. Выражение 2 - это выражение, определяющее условие, при котором тело цикла будет выполняться. Выражение 3 определяет изменение переменных, управляющих циклом после каждого выполнения тела цикла. В качестве тела цикла может служить любой исполняемый оператор языка C-51, в том числе и составной оператор. Внутри составного оператора может быть заключено любое количество исполняемых операторов.

Схема выполнения оператора for:

Вычисляется выражение 1. Вычисляется выражение 2. Если значения выражения 2 отлично от нуля (истина), выполняется тело цикла, вычисляется выражение 3 и осуществляется переход к пункту 2, если выражение 2 равно нулю (ложь), то управление передается на оператор, следующий за оператором for.

Обратите внимание, что проверка условия всегда выполняется в начале цикла. Это значит, что тело цикла может ни разу не выполниться, если условие выполнения сразу будет ложным.

Пример использования оператора for:

for(i=1;i<10;i++) //от i равного 1 до 10 с шагом 1 выполнить b=i*i;

В этом примере вычисляются квадраты чисел от 1 до 9.

Некоторые варианты использования оператора for повышают его гибкость за счет возможности использования нескольких переменных, управляющих циклом.

Пример:

int main() {int top,bot; char string[100],temp; for(top=0,bot=100;top<bot;top++,bot--) {temp=string[top]; string[bot]=temp; } return 0; }

В этом примере, реализующем запись строки символов в обратном порядке, для управления циклом используются две переменные top и bot. Отметим, что на месте выражение 1 и выражение 3 здесь используются несколько выражений, записанных через запятую, и выполняемых последовательно.

Так как согласно синтаксису языка Си оператор может быть пустым, тело оператора for также может быть пустым. Такая форма оператора может быть использована для организации поиска.

Пример:

for(i=0;t[i]<10;i++);

В данном примере переменная цикла i принимает значение номера первого элемента массива t, значение которого больше 10.

При написании программ для микроконтроллеров достаточно часто требуется реализовать бесконечный цикл. В этом случае оператор цикла for принимает следующий вид:

for(;;) //Постоянно {Knop=P2; //опрашивать порт P2 ObrabSobyt(); //и обрабатывать нажатие кнопок }

Оператор цикла for - это универсальный оператор, поэтому его реализация в машинных командах достаточно сложна. Для написания программ часто достаточно более простых операторов цикла. Использование таких операторов приводит к более компактным и быстродействующим программам.



Оператор цикла с проверкой условия до тела цикла while


Оператор цикла while называется циклом с предусловием и имеет следующий формат:

while (выражение) тело цикла;

Оператор while содержит условную операцию (такую же, как в операторе if), и вызывает исполнение операторов в этом блоке до тех пор, пока условие верно. Проверка условия производится до выполнения тела цикла, поэтому оператор тела цикла может быть не выполнен ни разу. В качестве выражения допускается использовать любое выражение языка Си, а в качестве тела любой оператор, в том числе пустой или составной. Схема выполнения оператора while следующая:

Вычисляется выражение. Если выражение ложно, то выполнение оператора while заканчивается и выполняется следующий по порядку оператор. Если выражение истинно, то выполняется тело цикла (которое может быть составным оператором) Процесс повторяется с пункта 1.

Оператор while может быть полезен для ожидания срабатывания какого-либо устройства микроконтроллера, например таймера:

... while(!TF0); //Программа ожидает переполнения таймера T0 TF0=0; TL0=time; //Настроить таймер T0 TН0=time>>8; //на очередной интервал времени ...

В следующем примере оператор while используется для пошагового прохождения по элементам массива Table до тех пор, пока очередной элемент не превысит значение скалярной переменной с именем Level (уровень англ.):

i = 0; while(table(i)<=Level)i++;

В приведённом примере Table - это предварительно объявленный массив. Переменные Level и i тоже должны быть предварительно объявлены.

Переменной i первоначально присваивается значение 0, затем она используется как индекс для массива Table. Так как i увеличивается при каждом проходе цикла while, то каждый раз, когда выполняется оператор внутри блока while, с переменной Level сравнивается следующий элемент массива Table. Когда найден элемент, превышающий значение переменной Level, то условие в операторе while больше неверно, выполнение блока не повторяется и управление передается следующему за циклом while оператору. С этого момента переменная i является индексом первого элемента массива Table, который превышает значение переменной Level.



Оператор цикла с проверкой условия после тела цикла do while


Оператор цикла while называется циклом с постусловием и имеет следующий формат:

do тело цикла while (выражение);

Оператор do while содержит условную операцию (такую же, как в операторе if), и вызывает исполнение операторов в этом блоке до тех пор, пока условие верно. Проверка условия производится после выполнения тела цикла, поэтому оператор тела цикла будет выполнен хотя бы один раз. В качестве выражения допускается использовать любое выражение языка Си, а в качестве тела любой оператор, в том числе пустой или составной. Схема выполнения оператора do while следующая:

Выполняется тело цикла (которое может быть составным оператором) Вычисляется выражение. Если выражение ложно, то выполнение оператора do while заканчивается и выполняется следующий по порядку оператор. Если выражение истинно, то выполнение оператора продолжается с пункта 1.

Чтобы прервать выполнение цикла до того, как условие станет ложным, можно использовать оператор break.

Оператор do while в большинстве случаев соответствует одной машинной команде, поэтому может быть использован для написания эффективных по коду и быстродействию программ. Например:

i=10 do тело цикла; while(--i<0);

Однако, конкретно в реализации компилятора C-51, намного эффективнее следующая конструкция:

for(i=10;i>0;i--) тело цикла;

Операторы while и do while могут быть вложенными.

Пример использования вложенных операторов цикла:

int i,j,k; ... i=0;j=0;k=0; do{i++;j--; while (a[k]<i)k++; } while(i<30&&j<-30);

Оператор continue


Оператор continue, как и оператор break, используется только внутри операторов цикла, но в отличие от него выполнение цикла не прерывается, а начинается следующая итерация цикла. Формат записи оператора:

continue;

Пример:

int main() {int a,b; for(a=1,b=0;a<100;b+=a,a++) {if(b%2)continue; ... /* обработка четных сумм */ } return 0; }

Когда сумма чисел от 1 до а становится нечетной, оператор continue передает управление на очередную итерацию цикла for, не выполняя операторы обработки четных сумм.

Оператор continue, как и оператор break, прерывает самый внутренний из объемлющих его циклов.



Оператор присваивания


Оператор присваивания записывается в виде:

=;

Выражение вычисляется, и полученное значение присваивается переменной. Например:

P0=2; //Установить начальные потенциалы на ножках второго порта микроконтроллера a=cos(b*5); //Этот оператор присваивания осуществляет вызов подпрограммы-функции.

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

sum+=3; //Оператор эквивалентен оператору sum=sum+3; Umensh-=5; //Оператор эквивалентен оператору Umensh=Umensh-5; a*=10; //Оператор эквивалентен оператору a=a*5; mask&=0x10;//Оператор эквивалентен оператору mask=mask&5; Обычно используется для записи нулей в определённые биты переменной

Оператор возвращения из подпрограммы return


Оператор return завершает выполнение функции, в которой он задан, и возвращает управление в вызывающую функцию, в точку, непосредственно следующую за вызовом. Формат оператора:

return [выражение] ;

Значение выражения, если оно задано, возвращается в вызывающую функцию в качестве значения вызываемой функции. Если выражение опущено, то возвращаемое значение не определено. Выражение может быть заключено в круглые скобки, хотя их наличие не обязательно.

Если в какой-либо функции отсутствует оператор return, то передача управления в вызывающую функцию происходит после выполнения последнего оператора вызываемой функции. При этом возвращаемое значение не определено. Если функция не должна иметь возвращаемого значения, то ее нужно объявлять с типом void.

Таким образом, использование оператора return необходимо либо для немедленного выхода из функции, либо для передачи возвращаемого значения.

Пример:

int sum(int a,int b) {return(a+b); }

Функция sum имеет два формальных параметра a и b типа int, и возвращает значение типа int, о чем говорит описатель, стоящий перед именем функции. Возвращаемое оператором return значение равно сумме фактических параметров.

Пример:

void prov (int a, double b) {float c; if(a<3) return; else if(b>10) return; else {c=a+b; if((2*c-b)==11)return; } }

В этом примере оператор return используется для выхода из функции в случае выполнения одного из проверяемых условий.



Оператор выбора switch


Оператор switch предназначен для организации выбора из множества различных вариантов. Формат оператора следующий:

switch ( выражение ) {[объявление] ... [ case константное-выражение1]: [ список-операторов1] [ case константное-выражение2]: [ список-операторов2] ... ... [ default: [ список операторов ]] }

Выражение, следующее за ключевым словом switch в круглых скобках, может быть любым выражением, допустимыми в языке СИ, значение которого должно быть целым. Отметим, что можно использовать явное приведение к целому типу.

Значение этого выражения является ключевым для выбора из нескольких вариантов. Тело оператора smitch состоит из нескольких операторов, помеченных ключевым словом case с последующим константным выражением.

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

Все константные выражения в операторе switch должны быть уникальны. Кроме операторов, помеченных ключевым словом case, может быть, но обязательно один, фрагмент помеченный ключевым словом default.

Список операторов может быть пустым, либо содержать один или более операторов. Причем в операторе switch не требуется заключать последовательность операторов в фигурные скобки.

Отметим также, что в операторе switch можно использовать свои локальные переменные, объявления которых находятся перед первым ключевым словом case, однако в объявлениях не должна использоваться инициализация.

Схема выполнения оператора switch следующая:

вычисляется выражение в круглых скобках; вычисленные значения последовательно сравниваются с константными выражениями, следующими за ключевыми словами case; если одно из константных выражений совпадает со значением выражения, то управление передается на оператор, помеченный соответствующим ключевым словом case; если ни одно из константных выражений не равно выражению, то управление передается на оператор, помеченный ключевым словом default, а в случае его отсутствия управление передается на следующий после switch оператор.

Отметим интересную особенность использования оператора switch: конструкция со словом default может быть не последней в теле оператора switch.
Ключевые слова case и default в теле оператора switch существенны только при начальной проверке, когда определяется начальная точка выполнения тела оператора switch. Все операторы, между начальным оператором и концом тела, выполняются вне зависимости от ключевых слов, если только какой-то из операторов не передаст управления из тела оператора switch. Таким образом, программист должен сам позаботится о выходе из case, если это необходимо. Чаще всего для этого используется оператор break.

Для того, чтобы выполнить одни и те же действия для различных значений выражения, можно пометить один и тот же оператор несколькими ключевыми словами case.

Если значение выражения равно 0, то выполняется только первый оператор присваивания, и 0 будет присвоен переменной red (красный англ). Если значение выражения равно 1, то будет выполнен только второй оператор присваивания и переменной blue (голубой англ) будет присвоен 0. Значения выражения 2 и 3 вызовет присваивание 0 переменным green (зеленый англ) и gray (серый англ) соответственно.

Следует отметить, что с точки зрения программиста этот оператор выглядит очень эффектно. Однако при рассмотрении машинного кода получается довольно страшная конструкция с использованием таблиц переходов. Использование нескольких условных операторов if несмотря на то, что они не очень красиво выглядят в исходном тексте программы, приводит к короткому и быстродействующему машинному коду программы.


Оператор выражение


Любое выражение, которое заканчивается точкой с запятой, является оператором.

Выполнение оператора выражение заключается в вычислении выражения. Полученное значение выражения никак не используется, поэтому, как правило, такие выражения вызывают побочные эффекты. Заметим, что вызвать подпрограмму-процедуру, можно только при помощи оператора выражения.

Примеры записи операторов-выражений:

++ i; //Этот оператор представляет выражение, которое увеличивает значение переменной i на единицу. a(x,y); //Этот оператор представляет выражение состоящее из вызова подпрограммы-процедуры.

используется два типа операторов:


В языке программирования C- 51 используется два типа операторов: операторы объявления и выполняемые операторы. Все операторы C-51 заканчиваются точкой с запятой.


Операторы объявления


Объявление является неисполняемым оператором, который объявляет некоторый объект или набор объектов, связывает с ним один или несколько идентификаторов и, если это необходимо, распределяет .

В объявлении могут быть объявлены три типа объектов: переменные, метки и функции. В программе для каждого имени допустимо только одно объявление. Метка полностью объявлена, если она стоит перед выполняемым оператором и заканчивается с двоеточием ':'. Например:

vfg: svGorit(); //Вызов подпрограммы с именем svGorit

Переменные, константы, литералы и подпрограммы должны быть объявлены раньше, чем они будут использоваться в исполняемом операторе. Объявление переменной заключается в задании типа этой переменной и ее имени. Тип переменной записывается перед именем переменной. Например:

char Rejim; //Переменная, хранящая номер режима размером в один байт int Schet; //Переменная, использующаяся как счётчик размером в два байта

В языке программирования C-51 нет необходимости знать конкретный адрес переменной, достаточно обратиться к ней по имени Rejim или Schet.

Функция объявляется точно так же как и переменные. То есть перед именем подпрограммы-функции записывается тип переменной, которую возвращает подпрограмма после своего завершения. Так как в подпрограмму в свою очередь тоже могут передаваться переменные, которые называются параметрами подпрограммы, то эти переменные записываются в скобках после имени подпрограммы-функции. Перед каждой переменной обязательно указывается ее тип. Например:

char PrinjatByte(void); //Подпрограмма, предназначенная для приёма одного байта.

Так как подпрограмме приёма обычно не требуется никаких дополнительных переменных, то вместо переменной-параметра подпрограммы указано слово void, обозначающее, что у подпрограммы-функции нет параметров.



Определение подпрограмм


Определение функции состоит из заголовка и тела. Определение функции записывается в следующем виде:

[спецификатор класса памяти] [спецификатор типа] имя функции ([список формальных параметров]) //Заголовок функции { //тело функции }

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

Тело функции - это составной оператор, содержащий операторы, определяющие действие функции. Тело функции начинается с фигурной скобки '{' и состоит из объявления переменных и исполняемых операторов. Именно эти операторы, входящие в тело функции, и определяют действие функции. Завершается тело функции закрывающей фигурной скобкой '}'.

Пример определения функции:

bit SostKnIzm(void)//Заголовок функции {//---------------начало тела функции-------------- bit tmp=0; if(P0!=0xff)tmp=1; return tmp; }//---------------конец тела функции----------------- //================== Вызывающая подпрограмма ========== ... if(SostKnIzm()) //Вызов подпрограммы SostKnIzm DecodSostKn();

В приведенном примере показано как при помощи функции, возвращающей битовую переменную можно повысить наглядность исходного текста программы. Оператор if(SostKnIzm()) DecodSostKn(); практически не требует комментариев. Имя функции SostKnIzm показывает что контролирует эта функция.

Необязательный спецификатор класса памяти задает класс памяти функции, который может быть static или extern.

При использовании спецификатора класса памяти static функция становится невидимой из других файлов программного проекта, то есть информация об этой функции не помещается в объектный файл. Использование спецификатора класса памяти static может быть полезно для того, чтобы имя этой функции могло быть использовано в других файлах программного проекта для реализации совершенно других задач. Если функция, объявленная с спецификатором класса памяти static, ни разу не вызывалась в данном файле, то она вообще не транслируется компилятором и не занимает места в программной памяти микроконтроллера, а программа-компилятор языка программирования С-51 выдает предупреждение об этом.
Это свойство может быть полезным при отладке программ и программных модулей.

Использование спецификатора класса памяти extern используется для связывания подпрограмм, находящихся в других модулях (и возможно написанных на других языках программирования) с вызовом подпрограммы из данного программного модуля. В основном использование этого спецификатора класса памяти эквивалентно предварительному объявлению функции, которое будет рассмотрено далее. Если спецификатор класса памяти функции не указан, то подразумевается класс памяти extern, то есть по умолчанию все подпрограммы считаются глобальными и доступными из всех файлов программного проекта.

Спецификатор типа функции задает тип возвращаемого значения и может задавать любой тип. Если спецификатор типа не задан, то по умолчанию предполагается, что функция возвращает значение типа int. Функция не может возвращать массив или функцию, но может возвращать указатель на любой тип, в том числе и на массив и на функцию. Тип возвращаемого значения, задаваемый в определении функции, должен соответствовать типу в объявлении этой функции.

Функция возвращает значение если ее выполнение заканчивается оператором return, содержащим некоторое выражение. Указанное выражение вычисляется, преобразуется, если необходимо, к типу возвращаемого значения и возвращается в точку вызова функции в качестве результата. Пример определения подпрограммы-функции:

#include <reg51.h> //Подключить описания внутренних регистров микроконтроллера char getkey () //Заголовок функции, возвращающей байт, принятый по последовательному порту {while (!RI); //Если последовательный порт принял байт, RI = 0; //то подготовиться к приёму следующего байта return (SBUF); //и передать принятый байт в вызывающую подпрограмму. } В операторе return возвращаемое значение может записываться как в скобках, так и без них. Если функция определена как функция, возвращающая некоторое значение (подпрограмма-функция), а в операторе return при выходе из нее отсутствует выражение, то это может привести к непредсказуемым результатам.



Для функций, не использующих возвращаемое значение (подпрограмм-процедур), должен быть использован тип void, указывающий на отсутствие возвращаемого значения. Если оператор return не содержит выражения или выполнение функции завершается после выполнения последнего ее оператора (без выполнения оператора return), то возвращаемое значение не определено.

void putchar (char c) //Заголовок функции, передающей один байт через последовательный порт {while (!TI); //Если передатчик последовательного порта готов к передаче байта SBUF = c; //то занести в буфер передатчика последовательного порта байт TI = 0; //и начать передачу } Все переменные, объявленные в теле функции без указания класса памяти, имеют класс памяти auto, т.е. они являются локальными. Так как глубина стека в процессорах семейства MCS-51 ограничена 256 байтами, то при вызове функций аргументам назначаются конкретные адреса во внутренней памяти микроконтроллера и производится их инициализация. Управление передается первому оператору тела функции и начинается выполнение функции, которое продолжается до тех пор, пока не встретится оператор return или последний оператор тела функции. Управление при этом возвращается в точку, следующую за точкой вызова, а локальные переменные становятся недоступными. При выходе из функции значения этих переменных теряются, так как при вызове других функций эти же ячейки памяти распределяются для их локальных переменных.

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


Отладка программ


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

внутрисхемным эмулятором встроенным программным отладчиком внешним программным отладчиком отлаживаемым устройством с записанным в память программ двоичным кодом программы

Внутрисхемный эмулятор с отображением переменных языка программирования на дисплее компьютера оказывает значительную помощь при отладке программ непосредственно на разрабатываемой аппаратуре. Этот метод отладки предоставляет наиболее удобную среду, когда можно непосредственно в отлаживаемом устройстве останавливать программу, контролировать выполнение программы непосредственно по исходному тексту программы, состояние внешних портов и внутренних переменных, как входящих в состав микросхемы, так и объявленных при написании исходного текста программы. Необходимое для отладки программ оборудование показано на рисунке 2.

Рисунок 2. Пример системы отладки программного обеспечения для микроконтроллеров.

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

включение информации о типе переменных для проверки типов при связывании модулей. Эта же информация используется внутрисхемным эмулятором. Исключение информации о переменных пользователя может использоваться для создания прототипов или для уменьшения размера объектного модуля; включение или исключение таблиц символьной информации; конфигурация вызовов подпрограмм для обеспечения связывания с модулями, написанными на языке программирования ASM-51; определение желаемого содержания и формата выходного листинга программы. Распечатка промежуточных кодов на языке ассемблер после компилирования программ, написанных на языке программирования PLM-51. Включение или исключение листингов отдельных блоков исходного текста.

[ ]



Память зависимые указатели


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

char data *str; /*указатель на строку во внутренней памяти данных data */ int xdata *numtab; /*указатель на целую во внешней памяти данных xdata */ long code *powtab; /*указатель на длинную целую в памяти программ code */

Поскольку модель памяти определяется во время компиляции, типизированным указателям не нужен байт, в котором указывается тип памяти микроконтроллера. Поэтому программа с использованием типизированных указателей будет короче и будет выполняться быстрее по сравнению с программой, использующей нетипизированные указатели. Типизированные указатели могут иметь размер в 1 байт (указатели на память idata, data, bdata, и pdata) или в 2 байта (указатели на память code и xdata).



Параметры подпрограмм


Список формальных параметров - это последовательность объявлений , разделенная запятыми. Формальные параметры - это переменные, используемые внутри тела функции и получающие значение при вызове функции путем копирования в них значений соответствующих.

Пример определения функции с одним параметром:

int rus (unsigned char r) //Заголовок функции {//---------------начало тела функции-------------- if (r>='А' && c<=' ') return 1; else return 0; }//---------------конец тела функции-----------------

В данном примере определена функция с именем rus, имеющая один параметр с именем r и типом unsigned char. Функция возвращает целое значение, равное 1, если параметр функции является буквой русского алфавита, или 0 в противном случае.

Если функция не использует параметров, то наличие круглых скобок обязательно, а вместо списка параметров рекомендуется указать слово void. Например:

void main(void) {P0=0; //Зажигание светодиода while(1); //Бесконечный цикл }

Порядок и типы должны быть одинаковыми в определении функции и во всех ее объявлениях. Поэтому желательно объявление функции поместить в отдельный файл, который затем можно включить в исходные тексты программных модулей при помощи директивы #include. Типы фактических параметров при вызове функции должны быть совместимы с типами соответствующих формальных параметров. Тип формального параметра может быть любым основным типом, , , , или . Если тип формального параметра не указан, то этому параметру присваивается тип int.

Для можно задавать класс памяти register, при этом для величин типа int спецификатор типа можно опустить. Однако все известные мне компиляторы языка программирования С-51 игнорируют этот спецификатор, так как расположение параметров в памяти микроконтроллера оптимизируется с точки зрения использования минимального количества необходимой внутренней памяти.

По умолчанию компиляторы стараются передавать параметры в функцию через регистры, тем самым максимально сохраняя внутреннюю память микроконтроллеров.
Номера регистров, используемые для передачи параметров в функцию в зависимости от типа аргументов приведены в таблице 1
Таблица 1

Номер аргумента char, однобайтовый указатель int, двухбайтовый указатель long,float Нетипизированные указатели
1 R7 R6,R7 R4 - R7 R1 - R3
2 R5 R4,R5 R4 - R7 R1 - R3
3 R3 R2,R3 R1 - R3
Поскольку при вызове функции значения фактических параметров копируются в локальные переменные, в теле функции нельзя изменить значения переменных в вызывающей функции. Например нужно поменять местами значения переменных x и y:
/* Неправильное использование параметров функции */ void change (int x, int y) {int k=x; x=y; y=k; } В данной функции значения локальных переменных x и y, являющихся формальными параметрами, меняются местами, но поскольку эти переменные существуют только внутри функции change, значения фактических параметров, используемых при вызове функции, останутся неизменными.
Однако, если в качестве параметра функции использовать указатель на переменную, то можно изменить значение переменной, адрес которой будет содержаться в указателе. Для того чтобы менялись местами значения фактических аргументов можно использовать функцию приведенную в следующем примере:
/* Правильное использование параметров функции */ void change (int *x, int *y) {int k=*x; *x=*y; *y=k; } При вызове такой функции в качестве фактических параметров должны быть использованы не значения переменных, а их адреса:
change (&a,&b);

Переменные перечислимого типа


Переменная, которая может принимать значение из некоторого списка значений, называется переменной перечислимого типа или перечислением.

Использование такого вида переменной эквивалентно применению целой знаковой переменной char или int. Это означает, что для переменной перечислимого вида будет выделен один или два байта в зависимости от максимального значения используемых этой переменной констант. В отличие от переменных целого типа, переменные перечислимого типа позволяют вместо безликих чисел использовать имена констант, которые более понятны и легче запоминаются человеком.

Например, вместо использования чисел 1,2,3,4,5,6,7 можно использовать названия дней недели: Poned, Vtorn, Sreda, Chetv, Pjatn, Subb, Voskr. При этом каждой константе будет соответствовать свое конкретное число. Однако использование имен констант приведет к более понятной программе. Более того, транслятор сам позволяет отслеживать правильность использования констант и при попытке использования константы, не входящей в объявленный заранее список, выдает сообщение об ошибке.

Переменные enum типа могут использоваться в индексных выражениях и как операнды в арифметических операциях и в операциях отношения. Например:

if(rab_ned == SUB) dejstvie = rabota [rab_ned];

При объявлении перечисления определяется тип переменной перечисления и определяется список именованных констант, называемый списком перечисления. Значением каждого имени этого списка является целое число. Объявление перечислимой переменной начинается с ключевого слова enum и может быть представлено в двух форматах:

enum [имя типа перечисления] {список констант} имя1 [,имя2 ...]; enum имя перечисления описатель [,описатель..];

В первом формате имена и значения констант задаются в списке констант. Необязательное имя типа объявляемой переменной - это идентификатор, который представляет собой тип переменной, соответствующий списку констант. За списком констант записывается имя одной или нескольких переменных.

Список констант содержит одну или несколько конструкций вида:


идентификатор [= константное выражение]

Каждый идентификатор - это имя константы. Все идентификаторы в списке enum должны быть уникальными. В случае если константе явным образом не задается число, то первому идентификатору присваивается значение 0, следующему идентификатору - значение 1 и т.д.

Пример объявления переменной rab_ned и типа переменных, совместимых с этой переменной, week выглядит следующим образом:

enum week {SUB = 0, /* константе SUB присвоено значение 0 */ VOS = 0, /* константе VOS присвоено значение 0 */ POND, /* константе POND присвоено значение 1 */ VTOR, /* константе VTOR присвоено значение 2 */ SRED, /* константе SRED присвоено значение 3 */ HETV, /* константе HETV присвоено значение 4 */ PJAT /* константе PJAT присвоено значение 5 */ } rab_ned; Идентификатор, связанный с константным выражением, принимает значение, задаваемое этим константным выражением. Результат вычисления константного выражения должен иметь тип int и может быть как положительным, так и отрицательным. Следующему идентификатору в списке, если этот идентификатор не имеет своего константного выражения, присваивается значение, равное константному выражению предыдущего идентификатора плюс 1. Использование констант должно подчиняться следующим правилам:

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

enum week rab1; К переменной перечислимого типа можно обращаться при помощи указателей. При этом необходимо заранее определить тип переменной, на которую будет ссылаться указатель.


Это может быть сделано, как описывалось выше или при помощи оператора typedef. Например:

Typedef enum {SUB = 0, /* константе SUB присвоено значение 0 */ VOS = 0, /* константе VOS присвоено значение 0 */ POND, /* константе POND присвоено значение 1 */ VTOR, /* константе VTOR присвоено значение 2 */ SRED, /* константе SRED присвоено значение 3 */ HETV, /* константе HETV присвоено значение 4 */ PJAT /* константе PJAT присвоено значение 5 */ } week; Этот оператор не объявляет переменную, а только определяет тип переменной, отличающийся от стандартного. В дальнейшем этот тип может быть использован для объявления переменных и указателей на переменные.

* Для тех читателей что вышли на эту страницу по поиску прошу обратить внимание, что здесь рассматривается не стандартный язык программирования С, а язык, адаптированный к микроконтроллерам . Имеются отличия!

[ ]


Подпрограммы обработки прерываний


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

interrupt N;

где N-любое десятичное число от 0 до 31.

Число N определяет номер обрабатываемого прерывания. При этом номер 0 соответствует внешнему прерыванию от ножки INT0, номер 1 соответствует прерыванию от таймера 0, номер 2 соответствует внешнему прерыванию от ножки INT1 и так далее. Пример подпрограммы-обработчика прерывания от таймера 0:

void IntTim0(void) interrupt 2 {TH0=25; TL0=32; //Задать новый интервал времени таймера T0 TF=0; //Сбросить флаг таймера T0 для разрешения следующего прерывания от данного таймера }

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

void IntTim0(void) interrupt 2 using 1 {TH0=25; TL0=32; //Задать новый интервал времени таймера T0 TF=0; //Сбросить флаг таймера T0 для разрешения следующего прерывания от данного таймера }

* Для читателей, которые вышли на эту страницу по поиску, прошу обратить внимание, что здесь рассматривается не стандартный язык программирования С, а язык, адаптированный к микроконтроллерам . Имеются отличия!

[ ]



Поля битов


Элементом структуры может быть битовое поле, обеспечивающее доступ к отдельным битам памяти. Вне структур битовые поля объявлять нельзя. Нельзя также организовывать массивы битовых полей и нельзя применять к полям операцию определения адреса. В общем случае тип структуры с битовым полем задается в следующем виде:

struct { unsigned идентификатор 1 : длина-поля 1; unsigned идентификатор 2 : длина-поля 2; }

длина-поля задается целым выражением или константой. Эта константа определяет число битов, отведенное соответствующему полю. Поле нулевой длинны обозначает выравнивание на границу следующего слова.

Пример:

struct {unsigned R: 1;//Флаг приёма байта unsigned T: 1;//Флаг передачи байта unsigned Cmd:5;//Поле команды unsigned St: 1;//Поле статуса } Cntr;

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

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

Cntr.Cmd=30;

Предварительное объявление подпрограмм


В языке С-51 нет требования, чтобы определение функции обязательно предшествовало ее вызову. Определения используемых функций могут следовать за определением функции main, перед ним, или находится в другом файле. Однако для того, чтобы компилятор мог осуществить проверку соответствия типов передаваемых фактических параметров типам формальных параметров и, если необходимо, выполнил соответствующие преобразования, до вызова функции нужно поместить объявление (прототип) функции.

Если объявление функции не задано, то по умолчанию строится прототип функции на основе анализа первого вызова функции. Тип возвращаемого значения создаваемого прототипа int, а список типов и числа параметров функции формируется на основании типов и числа фактических параметров используемых при данном вызове. Однако такой прототип не всегда согласуется с последующим определением функции. При размещении функции в другом файле или после оператора ее вызова рекомендуется задавать прототип функции. Это позволит компилятору либо выдавать диагностические сообщения, при неправильном использовании функции, либо правильным образом преобразовывать типы аргументов при её вызове.

Прототип - это явное объявление функции, которое предшествует определению функции. Тип возвращаемого значения при объявлении функции должен соответствовать типу возвращаемого значения в определении функции. Если прототип задан с классом памяти static, то и определение функции должно иметь класс памяти static. Объявление (прототип) функции имеет следующий формат:

[спецификатор класса памяти] [спецификатор типа] имя функции ([список формальных параметров]);

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

Для функции, определенной в предыдущем параграфе, прототип может быть записан в следующем виде:

int rus (unsigned char r);

При этом в объявлении функции имена формальных параметров могут быть опущены:

int rus (unsigned char);

Применение


Язык программирования C-51 и его библиотеки являются частью интегрированного набора средств разработки программного обеспечения для микроконтроллеров семейства MCS-51. Язык программирования C51 поддерживает модульное написание программ. Процесс разработки программ на языке программирования C-51 показан на рисунке 1.

Рисунок 1. Процесс написания программы на языке программирования С-51.

При разработке программного обеспечения выполняются следующие этапы:

постановка задачи (полное определение решаемой проблемы); разработка принципиальной схемы и выбор необходимого программного обеспечения; разработка системного программного обеспечения. Этот важный шаг состоит из нескольких этапов, включающих: описание последовательности выполняемых каждым блоком задач, выбор языка программирования и используемых алгоритмов; написание текста программы и подготовка к трансляции при помощи любого текстового редактора; компиляция программы; исправление синтаксических ошибок, выявленных компилятором, в текстовом редакторе с последующей перетрансляцией; создание и сохранение библиотек часто используемых объектных модулей при помощи программы lib51.ехе; связывание полученных перемещаемых объектных модулей в абсолютный модуль и размещение переменных в памяти микроконтроллера при помощи редактора связей bl51.exe; создание программы, записываемой в ПЗУ микроконтроллера (загружаемый модуль) в hex формате, при помощи программы oh.exe; проверка полученной программы при помощи символьного отладчика или других программных или аппаратных средств.

Файл, в котором хранится программа, написанная на языке C51 (исходный текст программы), называется исходным модулем. Для исходного текста программы принято использовать расширения файла: '*.c'. Исходный текст программы можно написать, используя любой текстовый редактор, однако намного удобнее воспользоваться интегрированной средой программирования, подобной . В интегрированную среду программирования кроме текстового редактора обычно входят отладчик программ, менеджер проектов и средства запуска программ-трансляторов.


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



Рисунок 2. Кнопка трансляции исходного текста файла в интегрированной среде программирования keil-c.

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

c51.exe modul.c

В этом примере в результате трансляции исходного текста программы, содержащегося в файле modul.c будет получен объектный модуль, который будет записан в файл с именем modul.obj. Как показано на рисунке 1, объектный модуль не может быть загружен в память программ микроконтроллера. В память микроконтроллера загружается исполняемый модуль.

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

bl51.exe main.obj, modul1.obj, modul2.obj

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

В интегрированной среде программирования процесс получения исполняемого модуля не сложнее предыдущего варианта. Для трансляции всего программного проекта достаточно нажать на соответствующую кнопку, как это показано на рисунке 3.



Рисунок 3. Кнопка получения исполняемого и загрузочного модулей в интегрированной среде программирования keil-c.



Большинство программаторов, предназначенных для записи информации в память программ микроконтроллеров, не может работать с объектным форматом исполняемого модуля программы, поэтому для загрузки машинного кода в процессор необходимо преобразовать объектный формат исполняемого модуля в общепринятый для программаторов HEX-формат. При преобразовании форматов вся отладочная информация теряется. Машинный код процессора в HEX-формате называется загрузочным модулем.

Загрузочный модуль программы можно получить при помощи программы-преобразователя программы oh.exe, передав ей в качестве имя файла исполняемого модуля программы, например:

oh.exe main

В результате выполнения этой командной строки будет получен загрузочный модуль программы, который будет записан в файл с именем main.hex.

В интегрированной среде программирования загрузочный файл получается автоматически при выполнении трансляции программного проекта, так как интегрированная среда программирования сама выполняет перечисленные выше действия в соответствии с настройками программного проекта.


Приоритеты выполнения операций


В языке С-51 операции с высшими приоритетами вычисляются первыми. Наивысшим приоритетом является приоритет равный 1. Приоритеты и порядок операций приведены в табл. 1. Порядок вычисления выражения следующий: сначала выполняются операторы в круглых скобках, в них от старшего приоритета к младшему, а среди равнозначных операторов - слева направо.

Таблица 1

Приоритет Знак операции Типы операции Порядок выполнения
2 () [] . -> Выражение Слева направо
1 - ~ ! * & ++ -- sizeof приведение типов Унарные Справа налево
3 * / % Мультипликативные Слева направо
4 + - Аддитивные
5 << >> Сдвиг
6 < > <= >= Отношение
7 == != Отношение (равенство)
8 & Поразрядное И
9 ^ Поразрядное исключающее ИЛИ
10 | Поразрядное ИЛИ
11 && Логическое И
12 Логическое ИЛИ
13 ? : Условная
14 = *= /= %= += -= &= |= >>= <<= ^= Простое и составное присваивание Справа налево
15 , Последовательное вычисление Слева направо

[ ]



Пустой оператор


Пустой оператор состоит только из точки с запятой. У пустого оператора может быть метка. Пустой оператор не содержит ничего, кроме пробелов и комментариев. При выполнении этого оператора ничего не происходит. Он обычно используется в следующих случаях:

в операторах цикла do, for, while при использовании их в качестве элементов задержки, в операторе if в строках, когда место оператора не требуется, но по синтаксису требуется хотя бы один оператор; при необходимости пометить структурный оператор.

Синтаксис языка СИ требует, чтобы после метки обязательно следовал оператор. Фигурная же скобка оператором не является. Поэтому, если надо передать управление на фигурную скобку, необходимо использовать пустой оператор.

Пример:

int main ( ) { ... { if (...) goto a; /* переход на скобку */ { ... } a:; } return 0; }

[ ]



Рекурсивный вызов подпрограмм


В стандартном языке программирования С все функции могут быть вызваны сами из себя или использоваться различными программными потоками одновременно. Для этого все локальные переменные располагаются в . В микроконтроллерах семейства MCS-51 ресурсы ограничены, поэтому в языке программирования С-51 для функций по умолчанию располагаются не в стеке, а непосредственно во внутренней памяти микроконтроллера. Если же подпрограмма должна вызываться рекурсивно, то ее необходимо объявить как программу с повторным вызовом (reentrant):

return_type funcname ([args]) reentrant

Классический пример рекурсии - это математическое определение факториала n!:

n! = 1 при n=0; n*(n-1)! при n>1 .

Функция, вычисляющая факториал, будет иметь следующий вид:

long fakt(int n) reentrant {return ((n==1)?1:n*fakt(n-1)); }

в любом другом текстовом файле,


Как и в любом другом текстовом файле, в исходном тексте программы, написанной на языке программирования С51, используются ASCII или ANSI символы.  Множество символов, используемых в языке программирования С-51 можно разделить на пять групп.

1. Символы, используемые для образования ключевых слов и идентификаторов (табл.1). В эту группу входят прописные и строчные буквы английского алфавита, а также символ подчеркивания. Следует отметить, что язык программирования С-51 различает прописные и строчные буквы. Например, идентификаторы start и Start будут считаться различными идентификаторами.

Таблица 1

Прописные буквы латинского алфавита A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
Строчные буквы латинского алфавита a b c d e f g h i j k l m n o p q r s t u v w x y z
Символ подчеркивания _
Арабские цифры 0 1 2 3 4 5 6 7 8 9
2. Прописные и строчные буквы русского алфавита (табл.2).

Таблица 2

Прописные буквы русского алфавита А Б В Г Д Е Ж З И К Л М Н О П Р С Т У Ф Х Ц Ч Ш Щ Ы Ь Э Ю Я
Строчные буквы русского алфавита а б в г д е ж з и к л м н о п р с т у ф х ц ч ш щ ъ ы ь э ю я
3. Знаки нумерации и специальные символы (табл. 3). Эти символы используются для организации процесса вычислений, а также для передачи компилятору определенного набора инструкций.

Таблица 2

Символ Наименование Символ Наименование
, запятая ) круглая скобка правая
. точка ( круглая скобка левая
; точка с запятой } фигурная скобка правая
: двоеточие { фигурная скобка левая
? вопросительный знак < меньше
' апостроф > больше
! восклицательный знак [ квадратная скобка
| вертикальная черта ] квадратная скобка
/ дробная черта # номер
\ обратная черта % процент
~ тильда & амперсанд
* звездочка ^ исключающее ИЛИ
+ плюс = равно
- минус " кавычки
4. Управляющие и разделительные символы. К этой группе символов относятся: пробел, символы табуляции, перевода строки, возврата каретки, символы новой страницы и новой строки.
Эти символы отделяют друг от друга лексические единицы языка, к которым относятся ключевые слова, константы, идентификаторы и т.д. Последовательность разделительных символов (например, последовательность пробелов) рассматривается компилятором как один символ.

5. Управляющие последовательности, т.е. специальные символьные комбинации, используемые в функциях ввода и вывода информации. Управляющая последовательность начинается с использования обратной косой черты (\), за которой следует комбинация латинских букв и цифр. Список управляющих последовательностей приведён в таблице 4.

Таблица 4

Управляющая последовательность Наименование Шестнадцатеричный код
\a Звонок 007
\b Возврат на шаг 008
\t Горизонтальная табуляция 009
\n Переход на новую строку 00A
\v Вертикальная табуляция 00B
\r Возврат каретки 00D
\f Новая страница 00C
\" Кавычки 022
\' Апостроф 027
\0 Ноль-символ 000
\\ Обратная дробная черта 05C
\OOO Восьмеричный код ASCII или ANSI символа  
\xHHH Шестнадцатеричный код ASCII или ANSI символа HHH
Управляющие последовательности \OOO и \xHHH (здесь O обозначает восьмеричную цифру; H обозначает шестнадцатеричную цифру) позволяет представить символ из кодовой таблицы ASCII или ANSI как последовательность восьмеричных или шестнадцатеричных цифр соответственно. Например символ возврата каретки может быть представлен следующими способами:

\r - управляющая последовательность,

\015 - восьмеричный код символа возврата каретки,

\x00D - шестнадцатеричный код символа возврата каретки.

Следует отметить, что в строковых константах всегда обязательно задавать все три цифры в управляющей последовательности. Например отдельную управляющую последовательность \n (переход на новую строку) можно представить как \010 или \xA, но в строковых константах необходимо задавать все три цифры, в противном случае символ или символы следующие за управляющей последовательностью будут рассматриваться как ее недостающая часть.


Например:

"ABCDE\x009FGH" данная строковая команда будет напечатана с использованием определенных функций языка СИ, как два отдельных слова ABCDE и FGH, разделенные табуляцией, в этом случае если указать неполную управляющую строку "ABCDE\x09FGH",то при печати появится строка ABCDEЯGH, так как компилятор воспримет последовательность \x09F как символ "Я".

Отметим тот факт, что, если обратная дробная черта предшествует символу не являющемуся управляющей последовательностью (т.е. не включенному в табл.4) и не являющемуся цифрой, то эта черта игнорируется, а сам символ представляется как литеральный. Например:

символ \h представляется символом h в строковой или символьной константе.

Кроме определения управляющей последовательности, символ обратной дробной черты (\) используется также как символ продолжения. Если за (\) следует символ возврата каретки, то оба символа игнорируются, а следующая строка является продолжением предыдущей. Это свойство может быть использовано для записи длинных строк. Например:

printf("Это очень длинная \ строка") Компилятор С-51 выдает сообщение об ошибке, если в тексте исходной программы встречается символ, отличающийся от символов, перечисленных выше.

[ ]


в программе является частью, по


Язык программирования С-51 является структурно-модульным языком. Каждый оператор в программе является частью, по крайней мере, одного модуля. Каждая программа, написанная на языке программирования С-51 состоит из одного или более модулей. Каждый модуль записывается в отдельном файле и компилируется отдельно.

В помещаются операторы, составляющие программу. Эти операторы объявляют константы или переменные и выполняют необходимые действия. Операторы, выполняющие действия, обязательно должны быть помещены в подпрограммы. Исполнение программы всегда начинается с подпрограммы с именем main (Т.е. в простейшем случае достаточно написать только эту подпрограмму).

начинается с заголовка подпрограммы, в который входит тип возвращаемой переменной, имя подпрограммы и круглых скобок, внутри которых объявляются переменные-параметры подпрограммы. В подпрограмме main (а также во всех подпрограммах, где не нужно возвращать переменные) вместо типа переменной указывается слово void. Исполняемые операторы заключаются в фигурные скобки.

Все переменные и константы, которые будут использоваться в программе, обязательно должны быть объявлены до подпрограммы, где они будут использованы первый раз.

При написании программы для микроконтроллеров всегда необходимо видеть перед глазами принципиальную схему устройства, для которого пишется программа, так как схема и программа тесно связаны между собой и дополняют друг друга. Для иллюстрации простейшей программы, написанной на языке программирования C-51 воспользуемся схемой, приведённой на рисунке 1.



Рисунок 1. Пример простейшей схемы устройства, построенной с использованием микроконтроллера.

Для примера заставим гореть светодиод VD1. Этот светодиод будет светиться только тогда, когда через него будет протекать ток. Для этого на шестом выводе порта P0 должен присутствовать нулевой потенциал. Запишем его первой же командой нашей программы:

#include<reg51.h> void main(void) {P0=0; //Зажигание светодиода while(1); //Бесконечный цикл } Эта программа содержит только один исполняемый оператор.
Это оператор присваивания 'P0=0;'. Следующий оператор 'while(1);' обеспечивает зацикливание программы. Зацикливание программы сделано для того, чтобы микроконтроллер не выполнял больше никаких действий. В противном случае микроконтроллер перейдёт к следующей ячейке памяти и будет выполнять команды, которые мы не записывали.

Обратите внимание, что язык программирования знает где находится порт P0. Эта информация содержится в строке #include<reg51.h>.

Для того, чтобы получить более полное представление о структуре программ, написанных на языке программирования С-51, приведём пример исходного текста программы с использованием подпрограмм.

#include<reg51.h> void svGorit(void) {P0=0; //Зажигание светодиода } void main(void) {svGorit(); //Вызов подпрограммы с именем svGorit while(1); //Бесконечный цикл } В приведённом примере использование подпрограммы никаких преимуществ не даёт, но в более сложных программах использование "говорящих" имён переменных может приблизить исходный текст программы к алгоритму, и, тем самым, сделать программу более понятной. Это в свою очередь значительно уменьшит время отладки программы.

[ ]


Структурный оператор {}


Существует два основных способа использования структурного оператора:

Структурный оператор может рассматриваться в качестве отдельного оператора языка С-51 и использоваться в программе везде, где может встречаться отдельный исполняемый оператор. Это используется в операторах , , , switch of и ; Структурный оператор ограничивает область действия локальных переменных.

Каждый оператор внутри структурного оператора может являться любым оператором языка C-51, в том числе и объявлением, при условии, что все объявления внутри структурного оператора должны быть выполнены до первого исполняемого оператора.

Структурный оператор начинается с открывающей скобки '{' и записывается в следующем виде:

{<operator-1>; //Здесь могут быть объявления переменных <operator-2>; ... <operator-n>; }

Заметим, что в конце составного оператора точка с запятой не ставится. Пример использования структурного оператора:

if(Wes<Min) /*Условная операция*/ {incr=incr*2; /*Структурный оператор*/ Schetch=Schetch+1; /*Содержит два оператора*/ }

Структурные операторы могут вкладываться друг в друга:

{<operator-1>; <operator-2>; {<operator-A>; <operator-B> <operator-C>; } <operator-3>; <operator-4>; }

Пример использования объявлений внутри вложенного структурного оператора:

int main () {int q,b; float t,d; ... if(...) {int e,g; float f,q; ... } ... return (0); }

Переменные e,g,f,q будут уничтожены после выполнения составного оператора. Отметим, что переменная q является локальной в составном операторе, т.е. она никоим образом не связана с переменной q объявленной в начале функции main с типом int. Отметим также, что выражение стоящее после return может быть заключено в круглые скобки, хотя наличие последних необязательно.



Структуры


Работа с массивами облегчает понимание и написание программы, когда для обозначения похожих элементов используется один идентификатор. Однако в ряде случаев приходится обрабатывать разнородные элементы, описывающие один объект. В этом случае вместо массива используется структура.

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

struct { список описаний}

В структуре обязательно должен быть указан хотя бы один компонент. Компоненты структуры называются полями структуры. Объявление полей производится в следующем виде:

тип-данных описатель;

где тип-данных указывает тип структуры для объектов, определяемых в описателях. В простейшей форме описатели представляют собой идентификаторы переменных или массивов.

Пример объявления структур:

struct//Описание типа структуры------------------------- {char tzvet,//Цвет точки int x, //Координата X y; //Координата Y }//------------------------------------------------------ tochka1, tochka2, //Переменные, обозначающие точки дисплея simv[7][9]; //Переменная, содержащая рисунок символа struct {int year; //Поле структуры, в котором хранится год char moth, //Поле структуры, в котором хранится месяц day; //Поле структуры, в котором хранится день }//------------------------------------------------------- date1, date2;//Переменные, обозначающие две различных даты

Переменные tochka1, tochka2 объявляются как структуры, каждая из которых состоит из трех полей tzvet, х и у. Переменная simv объявляется как двумерный массив, состоящий из 63 переменных, описывающих точку дисплея. Во втором объявлении каждая из двух переменных date1, date2 состоит из трех компонентов year, moth, day.

Существует и другой способ связывания имени переменной с типом структуры, он основан на использовании отдельного объявления типа структуры. Тип структуры описывается следующим образом:

struct тип-структуры { список описаний; };

где тип-структуры является идентификатором типа объявляемой структуры и может быть использован для последующего объявления структур данного вида (то есть содержащих точно такие же поля) в форме:

struct тип-структуры список-идентификаторов;

В приведенном ниже примере идентификатор student описывается как тип структуры:

struct student {char name[25];//Имя и фамилия студента int id, //Номер в журнале age; //Возраст char usp; //успеваемость };

Пример:

struct student st1[23];//объявление массива переменных типа студент

Доступ к компонентам структуры осуществляется с помощью указания имени структуры и следующего через точку имени выделенного компонента, например:

st[1].name="Иванов"; st[1].id=2; st[1].age=23;

Условный оператор


Оператор if обеспечивает условное выполнение операторов. Он записывается в следующей форме:

if(<>) <operator-1>; [else <operator-2>;]

При этом ключевое слово else со следующим за ним исполняемым оператором представляют собой необязательную часть условного оператора. Если результат вычисления выражения равен 1 (истина), то выполняется operator-1. Если результат вычисления выражения равен 0 (ложь), то выполняется operator-2. Если выражение ложно и отсутствует оператор-2, то выполняется оператор, следующий за условным. Пример записи условного оператора:

if(Wes<Min) /*Условная операция*/ Schetch=Schetch+1; /*Плечо 1*/ else Schetch=0; /*Плечо 2*/

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

if(P1.5)fnKn5Naj(); //В этом примере предполагается, что к пятой ножке порта P1 подключена кнопка с надписью "5" if(Kn5Naj)fnKn5Naj(); //Этот пример эквивалентен предыдущему, но ножке P1.5 поставлена в соответствие переменная Kn5Naj if(PrinjatByte())DecodCmd(); //Предполагается, что функция PrinjatByte возвращает значение '1', если байт принят

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



и операндов, результатом которой является


Выражением называется комбинация знаков операций и операндов, результатом которой является определенное значение. Знаки операций определяют действия, которые должны быть выполнены над операндами. Каждый операнд в выражении в свою очередь может быть выражением. Значение выражения зависит от расположения знаков операций и круглых скобок в выражении, а также от приоритета выполнения операций. Примеры выражений:

А+В A*(B+C)-(D-E)/F Выражение в языке программирования С-51 состоит из операндов, которые комбинируются при помощи различных или операций, а также . Над переменными-указателями возможно проведение операций.

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

Если в качестве операнда используется константа, то ему соответствует значение и тип представляющей его константы. Целая константа может быть типа int, long, unsigned int, unsigned long, в зависимости от ее значения и от формы записи. Символьная константа имеет тип int. Константа с плавающей точкой всегда имеет тип float.

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

a=(int)b+(int)c; //Переменные a и b могут быть восьмиразрядными. Преобразование типов нужно чтобы избежать переполнения s=sin((float)a/15)); //Если не преобразовать тип переменной a, то деление будет целочисленным и результат деления будет равен нулю. В выражениях в качестве операндов могут использоваться подвыражения. Подвыражение - это обычное выражение, заключенное в скобки. Подвыражения могут использоваться для группировки частей выражения, точно так же, как и в обычной алгебраической записи.
Использование подвыражений позволяет сократить количество операторов в программе, а значит и объем программы (но не объём ), но затрудняет отладку этой программы.

В языке программирования C-51 используются арифметические операции, результат которых зависит от :

+ суммирование - вычитание * умножение / деление % вычисление остатка от целочисленного деления Примеры выражений, использующие арифметические операции:

А+В А+В-С A*T+F/D A*(B+C)-(D-E)/F В языке программирования C-51 также определено несколько одноместных арифметических операций:

'-' изменение знака операнда на противоположное значение '+' знак плюс не влияет на значение операнда ++ увеличение значения операнда на единицу -- уменьшение значения операнда на единицу Для одноместной операции требуется один операнд, которому она предшествует. Например:

P3 = -5; //Присвоить порту P3 значение числа -5 a = -b; //Присвоить переменной a отрицательное значение переменной b с=++a + 2; //Увеличить значение переменной a на 1 и прибавить 2 с=a++ + 2; //Прибавить к переменной a 2, присвоить это значение переменной c и только после этого увеличить значение переменной a на 1. Над операндами можно осуществлять логические операции:

'&&' логическое "и" '&' побитовое логическое "и" '' логическое "или" '|' побитовое логическое "или" '^' "исключающее или" (суммирование по модулю два) Здесь следует объяснить различие между логическими и побитовыми логическими операциями. Дело в том, что в стандартном языке ANSI C не существует битовых переменных. Для хранения битовых значений истина '1' и ложь '0' используются стандартные целые типы переменных. В простейшем случае это байт. При этом все значения переменной, отличающиеся от 0 считаются 1. Например, пусть в переменной a хранится число 5, а в переменной b - число 6. Тогда:

a|b=7 //00000101 or 00000110 = 00000111 ab=1 //(00000101)->1 (00000110)->1; (1 or 1= 1), Результат равен 00000001 a&b=4 //00000101 and 00000110 = 00000100 a&&b=1 //(00000101)->1 (00000110)->1; (1 and 1= 1), Результат равен 00000001 a^b=3 //00000101 xor 00000110 = 00000011 В языке программирования C-51 также определено несколько одноместных логических операций:



'!' инверсия операнда '~' побитовая инверсия операнда Например, пусть в переменной a хранится число 5. Тогда:

a=~a; //~00000101 = 11111010 = 250 a=~a; //~11111010 = 00000101 = 5 a=!a; //(00000101)->1; ~1 = 0 a=!a; // ~0 = 1 В условном операторе и операторах цикла используются операции отношения:

< меньше > больше <= меньше или равно >= больше или равно == равно != не равно Если указанное отношение между операндами верно, то результат равен 1, иначе 0. Например, если d=7, то:

(d > 5) результат будет 1 (истина) (d = 4) результат будет 0 (ложь) Над переменными-указателями возможно проведение адресных операций.

'*' операция косвенной адресации '&' вычисление адреса переменной Операция косвенной адресации '*' осуществляет доступ к переменной при помощи указателя. Результатом операции является значение переменной, на которую указывает операнд. Типом результата является тип переменной, адресуемой указателем. При работе с указателями необходимо быть предельно осторожными, так как, если указатель содержит недопустимый адрес, то результат операции чтения или записи будет непредсказуем и может привести к выходу проектируемого устройства из строя.

Операция вычисления адреса переменной '&' дает адрес ячейки памяти своего операнда. Операндом может быть любой идентификатор. Имя функции или массива также может быть операндом операции вычисления адреса переменной, хотя в этом случае знак операции вычисления адреса переменной является лишним, так как имена массивов и функций являются и так являются адресами. Операция вычисления адреса переменной не может применятся к элементам структуры, являющимися полями битов, и к объектам с классом памяти register.

Пример использования адресных операций при работе с указателем:

int t, f=0, *adress; adress = &t /* переменной adress, объявленной как указатель, присваивается адрес переменной t */ *adress =f; /* переменной находящейся по адресу, содержащемуся в переменной adress, присваивается значение переменной f, т.е. 0, что эквивалентно t=f; т.е.t=0; */ Может возникнуть вопрос: а зачем тогда нужны указатели, если можно прекрасно обойтись обычным присваиванием переменной? Использование указателей очень удобно при написании подпрограмм. Ведь одни и те же действия подпрограмма должна выполнять над различными участками памяти. Например при выводе информации через :

void PutNadp(char code *c)//Объявлен указатель c на символ в памяти программ {do{while(!TI); //Подождать готовности последовательного порта TI=0; SBUF=*c++; //Передать очередной символ }while(*c!=0); //Если передан последний символ строки } //то выйти из подпрограммы ... PutNadp("привет!"); //Вывод одной строки ... PutNadp("Вася!"); //Вывод второй строки

Вызов подпрограмм


Вызов функции имеет следующий формат:

имя функции ([список выражений]);

Список выражений представляет собой список фактических параметров, передаваемых в функцию. Этот список может быть и пустым, если в определении функции отсутствуют параметры, но наличие круглых скобок обязательно. Примеры вызова функции (подпрограммы-процедуры):

DecodSostKn(); VypFunc();

Функция, если она возвращает какое-либо значение (подпрограмма-функция), может быть вызвана и в составе выражения, например:

y=sin(x); //sin - это имя подпрограммы-функции if(rus(c))SvDiod=Gorit; //rus - это имя подпрограммы-функции

Выполнение вызова функции происходит следующим образом:

Вычисляются выражения в списке выражений и подвергаются обычным арифметическим преобразованиям. Затем тип полученного фактического аргумента сравнивается с типом соответствующего формального параметра. Если они не совпадают, то либо производится преобразование типов, либо формируется сообщение об ошибке. Число выражений в списке выражений должно совпадать с числом формальных параметров. Если в прототипе функции указано, что ей не требуются параметры, а при вызове они указаны, формируется сообщение об ошибке. Происходит присваивание значений фактических параметров соответствующим формальным параметрам. Управление передается на первый оператор функции.

Поскольку имя функции является адресом начала тела функции, то в качестве обращения к функции может быть использовано не только имя функции но и значение указателя на функцию. Это значит что функция может быть вызвана через указатель на функцию. Пример объявления указателя на функцию:

int (*fun)(int x, int *y);

Здесь объявлена переменная fun как указатель на функцию с двумя параметрами: типа int и указателем на int. Сама функция должна возвращать значение типа int. Круглые скобки, содержащие имя указателя fun и признак указателя *, обязательны, иначе запись

int *fun (int x,int *y);

будет интерпретироваться как объявление функции fun возвращающей указатель на int.

Вызов функции возможен только после инициализации значения указателя:


float (*funPtr)(int x, int y); float fun2(int k, int l); ... funPtr=fun2; /* инициализация указателя на функцию */ (*funPtr)(2,7); /* обращение к функции */ В рассмотренном примере указатель на функцию funPtr описан как указатель на функцию с двумя параметрами, возвращающую значение типа double, и также описана функция fun2. В противном случае, т.е. когда указателю на функцию присваивается функция описанная иначе чем указатель, произойдет ошибка.

Рассмотрим пример использования указателя на функцию в качестве параметра функции вычисляющей производную от функции cos(x).

Пример:

float proiz(float x,float dx,float(*f)(float x)); float fun(float z); int main() {float x; /* точка вычисления производной */ float dx; /* приращение */ float z; /* значение производной */ scanf("%f,%f",&x,&dx); /* ввод значений x и dx */ z=proiz(x,dx,fun); /* вызов функции */ printf("%f",z); /* печать значения производной */ } float proiz(float x,float dx,float (*f)(float z))/* функция вычисляющая производную */ {float xk,xk1; xk=fun(x); xk1=fun(x+dx); return (xk1/xk-1e0)*xk/dx; } float fun( float z) /* функция от которой вычисляется производная */ {return (cos(z)); } Для вычисления производной от какой-либо другой функции можно изменить тело функции fun или использовать при вызове функции proiz имя другой функции. В частности, для вычисления производной от функции cos(x) можно вызвать функцию proiz в форме

z=proiz(x,dx,cos); а для вычисления производной от функции sin(x) в форме

z=proiz(x,dx,sin);