СПРАВОЧНЫЙ МАТЕРИАЛ Книга по работе с WinAVR и AVR Studio ПРЕДИСЛОВИЕ Последние годы заметен среди радиолюбителей резкий всплеск интереса к конструкциям на микроконтроллерах (МК). Обусловлено это их чрезвычайной универсальностью и гибкостью при более чем демократичных ценах. Наибольшей популярностью продолжают пользоваться 8-разрядные МК, среди которых выделяются модели семейства AVR фирмы Atmel. Но микроконтроллер без программы – это мертвый компонент, его функциональность даже меньше, чем у обычного инвертора. Процесс разработки программного обеспечения для микроконтроллера – это наиболее сложная часть конструирования, именно это вызывает наибольшие сложности у любителей. Не обладая достаточными навыками в программировании, они вынуждены заниматься лишь копированием чужих разработок. Совпадение целей и интересов у автора-разработчика и человека, решившего повторить конструкцию, происходит редко, и это порождает множество вопросов, криков о помощи на Интернет-форумах и, порой, отталкивает радиолюбителя от микроконтроллеров – дескать, легче сделать что-то на десятке микросхем жесткой логики, чем уговорить когото подкорректировать программу. Но зачем кого-то уговаривать? Перефразируя известное детское стихотворение: не надо ждать, не надо звать, а надо взять и… написать! Т.е. речь идет о собственноручном написании программ для микроконтроллеров. Это не так сложно, как кажется, во всяком случае, под силу каждому технически грамотному человеку, овладевшему компьютером и электроникой, пусть даже не цифровой. Как известно, программы разрабатываются на языках программирования, среди которых наибольшей популярностью пользуется Си. Благодаря основам, заложенным в этот язык, разработка программ для микроконтроллеров мало отличается от разработки программ для обычных компьютеров. Поэтому проще всего переключиться на микроконтроллеры смогут те, кто уже умеет хоть немного программировать для персонального компьютера. А все остальные желающие могут прочесть специальные книги, посвященные как самому языку Си, так и процессу алгоритмизации. Литературы на этот счет достаточно, равно как и литературы об аппаратном строении микроконтроллеров. А вот книг, которые целенаправленно рассматривали бы аспекты «сопряжения» языка программирования и микроконтроллера, явно недостаточно. Восполнить этот пробел хоть в какой-то мере – вот главная цель и задача этой книги. И при восполнении упомянутого пробела сделан упор на бесплатное программное обеспечение. Дело в том, что ценность имеющихся книг, рассказывающих о программных пакетах стоимостью в сотни и даже тысячи долларов, очень сомнительна. Любителей, способных выложить такие суммы, найдется в нашей стране исчезающе мало, и даже не всякие фирмы рискнут пойти на такие траты. Выходит, что подобные книги косвенно стимулируют пиратство. В то же время есть альтернативный, абсолютно бесплатный софт, по качеству вполне соперничающий с коммерческими пакетами. Его применение абсолютно законно, и более того – часто дает лучшие результаты, нежели иные коммерческие. Но бесплатные пакеты порой грешат недостаточно качественной документацией, а то и полным ее отсутствием, тем более на русском языке. Ликвидация этого недостатка – вторая задача данной книги. Кому может быть полезна эта книга? Прежде всего, тем, кто занимался «компьютерным» программированием и испытывает желание или необходимость приступить к разработке программ для микроконтроллеров, например, в связи со сменой работы. Книга будет полезна и студентам, изучающим микроконтроллеры и радиолюбителям, желающим освоить новую для себя стезю. Полезное для себя наверняка извлекут и профессионалы, желающие перейти на бесплатное программное обеспечение. Несомненно, полезной книга будет для всех, кто владеет английским языком в недостаточной мере, чтобы изучать фирменную документацию. Что можно найти в этой книге? Прежде всего, практические рекомендации по установке и использованию пакета разработки программ для микроконтроллеров AVR, распространяемому 58 Роман Абраш г. Новочеркасск E-mail: arv@radioliga.com бесплатно – WinAVR, а так же применению его в интеграции с бесплатной IDE фирмы Atmel – AVR Studio. Краткое описание основ языка Си, которое не может быть полноценным учебником, но должно послужить основой для первых шагов в программировании. Книга так же содержит подробный справочник по функциям, входящим в библиотеку AVR-LIBC, являющуюся частью комплекта WinAVR, руководство по разработке модулей на ассемблере для Си-программ. Достаточно подробно рассмотрен необходимый минимум команд компилятора GCC, являющегося «сердцем» WinAVR, а так же описаны некоторые утилиты сторонних разработчиков. В книге имеется ряд практических примеров программ и программных «заготовок» для решения задач, являющихся типовыми: обмен с компьютером по RS-232, организация динамической индикации, работа с ЖКИ и т.п. В книгу включен FAQ – ответы на часто задаваемые вопросы, составленный на основе Интернет-форумов и других источников. Чего вы не найдете в этой книге? Главное: вы не найдете сведений об архитектуре и периферии микроконтроллеров AVR – об этом написано множество книг и повторяться нет никакого смысла. Так же в книге не рассмотрен ряд утилит и средств, входящих в комплект WinAVR – в основном, это утилиты для программирования и отладки, а так же вспомогательные утилиты (binutils), работающие в консольном режиме. Сделано это вполне осознанно: в среде Windows более привычным и удобным является графический интерфейс, который для работы с WinAVR обеспечивает AVR Studio, делая тем самым ненужными консольные средства. Что потребуется для того, чтобы воспользоваться информацией из этой книги? Для работы будет необходим компьютер, желательно, подключенный к Интернет. Потребуется, скорее всего, кое-что из аппаратуры: рекомендуется один из комплектов разработчика, предлагаемых фирмой Atmel или другими фирмами. В крайнем случае, можно ограничиться наличием одного из микроконтроллеров AVR и самодельным программатором. Все необходимое для работы программное обеспечение (в том числе инсталляционные пакеты AVR Studio и WinAVR), рассматриваемые в книге примеры программ, а так же большое количество документации и свободно распространяемых любительских библиотек и разработок имеется на сайте [1]. РАЗРАБОТКА ПРОГРАММ Разработка программного обеспечения для микроконтроллеров AVR мало отличается от разработки любых иных программ, разве что от программиста требуется чуть более глубокие знания электроники – хотя бы на уровне понимания действия логических элементов и триггеров. Разумеется, чем лучше программист владеет электроникой, тем более качественные программы он сможет создать. Тесная связь «софта и железа» по сути требует, чтобы программист был электронщиком или же электронщик был программистом. В настоящее время обе ситуации имеют место. Существует три более-менее устоявшихся подхода к разработке микроконтроллерных устройств: - от схемы к программе - от программы к схеме - смешанный. Деление, конечно, условное. В первом случае электронщик разрабатывает схему, представляя микроконтроллер неким «черным ящиком», т.е. устройством, которое «как-то само» выполняет определенные действия, формируя выходные сигналы в зависимости от входных. После того, как схема готова, составляется задание программисту, в котором указывается, на каких выводах контроллер какие сигналы должен получать или формировать. Порой на этом этапе требуется корректировка схемы, т.к. далеко не любой вывод контроллера способен выполнить нужную функцию. По заданию программист пишет программу, после чего следует этап отладки «в железе», т.е. в реальной схеме. После этого этапа часто следуют корректировки как схемы, так и программы. В общем, процесс итеративно повторяется до достижения требуемого результата. Радиолюбитель – 01/2010 СПРАВОЧНЫЙ МАТЕРИАЛ При втором подходе программист получает задание на разработку программы первым. В процессе работы он сам выбирает, какой вывод МК будет выполнять то или иное действие (для чего, несомненно, он должен знать архитектуру выбранного контроллера). Когда, как минимум, сделано «распределение выводов», задание на разработку схемы получает и электронщик. Теперь уже он должен рисовать схему с учетом отсутствия свободы выбора выводов микросхемы-микроконтроллера. После того, как программа и схема готовы, следуют этапы отладки, которые не отличаются от предыдущего варианта. Наконец, при третьем подходе программист и электронщик – это одно и то же лицо, которое по ходу составления схемы пишет и программу. В этом случае этот специалист, зная все нюансы электроники и программирования, «по ходу пьесы» подстраивает одно под другое, т.е. добивается наиболее оптимального взаимодействия всех частей. Ведь не секрет, что желания программиста и электронщика часто взаимопротивоположны, и поиск компромиссов в первых двух рассмотренных вариантах чреват лишними препираниями, в то время как третий подход все трения исключает. Так как настоящая книга посвящена программным средствам, дальнейшее изложение будет вестись, как будто имеет место второй вариант, т.е. от программы к схеме – так, не обращая излишнего внимания на схемотехнику, проще разобраться в процессе программирования. При этом неизбежные обращения взгляда на схемотехнику будут происходить в предположении, что читатель обладает необходимыми знаниями и навыками в области электроники, т.е. он – электронщик, желающий стать программистом. Основы Си Эта глава посвящается введению в язык Си. Это не учебник, скорее – это упрощенный краткий справочник с пояснениями и примерами. К сожалению, последовательность изложения может показаться нелогичной: это объясняется тем, что невозможно совместить краткость и простоту с логичностью и последовательностью при рассмотрении такой сложной системы, как язык программирования. Поэтому часть упоминаемых терминов получает объяснение в последующем тексте, иногда даже в другом разделе. Общие сведения Назначение языка программирования – предоставить программисту средства для изложения алгоритма решения какой-либо задачи в форме, воспринимаемой компилятором (специальной программой, служащей для «перевода» с одного языка на другой, понятный конкретной аппаратной платформе, т.е. процессору или микроконтроллеру). Основу любого языка составляет алфавит, т.е. определенный набор допустимых символов, из которых затем составляются лексемы, т.е. элементы языка, уже означающие конкретные понятия. Любой язык программирования – это «письменный» язык, т.е. он существует в виде символов текста. Запись средствами языка алгоритма в файле называется исходным текстом программы (или просто программой). Алфавит языка Си составляют: 1. Буквы латинского алфавита 2. Цифры от 0 до 9 3. Специальные знаки: “ { } , | [ ] ( ) + - / % \ ; ‘ : ? < = > _ ! & # ~ ^ . * Пробел не является языковым элементом, он служит лишь для разделения отдельных лексем. Из элементов алфавита составляются лексемы: • Идентификаторы, называемые иногда символами • Зарезервированные (ключевые) служебные слова • Знаки операций • Разделители Идентификатор – это последовательность из символов латинского алфавита, десятичных цифр и символов подчеркивания, начинающаяся не с цифры. Прописные и строчные символы различаются, т.е. Run и RUN – разные идентификаторы. Длина идентификатора принципиально не ограничена, но анализируются только Радиолюбитель – 01/2010 первые 32 символа1 , т.е. два идентификатора с одинаковыми первыми 32-символами будут считаться одинаковыми, даже если в 33-ем символе они различаются. Разделитель – это символ или несколько слитно написанных символов, которые служат для отделения одной лексемы от другой. Об одном разделителе – пробеле – уже было сказано, однако более важным является точка с запятой. Так же важными разделителями являются скобки2 (обязательно в паре – открывающая и закрывающая). Разделителем является и пустая строка, т.е. символ перевода строки, а так же символ табуляции. Существует особый вид разделителя – комментарий. В Си определено два вида комментария: многострочный и однострочный. Многострочный комментарий начинается с пары символов «/*» и завершается парой «*/». Все символы, находящиеся между этими парами символов, игнорируются, т.е. не воспринимаются компилятором. Однострочный комментарий начинается с пары символов «//» и продолжается до конца текущей строки, т.е. все символы, следующие за этой парой до конца строки, игнорируются компилятором. Программа на языке Си представляет собой последовательность лексем и разделителей, записанных в текстовом файле. Если отдельную законченную мысль человек при письме завершает точкой, то в языке Си каждая законченная мысль (т.е. последовательность лексем, имеющая самостоятельное смысловое значение) завершается особым разделителем – точкой с запятой. Хотя нет никаких ограничений на способ записи программы, все же следует по мере возможности выделять каждую осмысленную запись программы, начиная ее с новой строки. Кроме этого есть и другие рекомендации – см. «О стиле программирования». Служебные слова Ключевое или служебное слово – это идентификатор, зарезервированный для особого использования. В Си стандартно определены следующие служебные слова3 : break enum long struct case extern public switch char float register typedef const for return union continue goto short unsigned do if signed void double inline sizeof volatile else int static while В сущности, этих служебных слов достаточно, чтобы реализовать любую программу. Часть из этих слов являются операторами (см. далее), часть используется в составе других конструкций, часть соответствует обозначению стандартных типов. Далее все служебные слова будут рассмотрены в соответствующем контексте по назначению. Типы данных Любой алгоритм, так или иначе, сводится к неким действиям над данными, будь то числа, буквы, звуки, сигналы и т.п. Так как язык программирования служит для реализации алгоритма, он имеет все необходимые средства для манипулирования данными. Для разделения их по видам, введено понятие тип данных. Так как тип не может существовать без данных, а данными в языке Си может быть очень много совершенно разных объектов, далее будем использовать термин объект для обозначения абстрактных данных. Тип – это идентификатор, обозначающий принадлежность объекта к некой категории, однозначно определяющей содержимое и набор действий, которые можно выполнить над ним. Например, все числа можно разделить на ряд категорий – целые, действительные, комплексные, отрицательные и т.п. Так как работа процессора и микроконтроллера есть процесс цифровой обработки данных, поэтому так или иначе все разнообразие данных сводится к числам. Даже буквы есть не что иное, как числа, 1 Значение может отличаться в зависимости от стандарта, которому соответствует компилятор. 2 Разделителями считаются любые скобки – круглые, фигурные и квадратные, однако не всегда и не все из них могут использоваться произвольно – существует ряд ограничений, которые будут упомянуты по мере изложения основ языка. 3 Список служебных слов может несколько отличаться в зависимости от конкретного компилятора (как правило в сторону расширения). 59 СПРАВОЧНЫЙ МАТЕРИАЛ сопоставленные им (грубо говоря, это номер буквы по порядку в алфавите). По этим причинам в языке Си основным видом данных являются числа, которые для удобства разделены на ряд стандартных типов4 : char – символ, то есть число, занимающее один байт (т.е. 8-битное число) int – целое число, занимающее 2 байта (т.е. 16-битное число) long – длинное целое число, занимающее 4 байта (т.е. 32-битное число) long long – двойное длинное целое число, занимающее 8 байт (т.е. 64-битное число) float – действительное число, т.е. число с плавающей точкой double – действительное число удвоенной точности При помощи дополнительных служебных слов определяются разновидности типов целых чисел: signed – обозначает, что целое число имеет знак (т.е. может принимать и отрицательные значения) unsigned – обозначает, что целое число не имеет знака, т.е. может быть только нулем или более short – обозначает, что фактический размер целого числа вдвое меньше Таким образом, получаются следующие типы: unsigned char – число от 0 до 255 signed char – число от -127 до 127 unsigned int – число от 0 до 65535 signed int – число от -32767 до 32764 unsigned long – число от 0 до 4294967295 signed long – число от -2147483648 до 2147483648 и т.д. Следует отметить, что слово signed, как правило, нет смысла использовать, так как по умолчанию все типы целых чисел имеют знак. Примечание: тип char стоит особняком – многие версии стандарта Си допускают, что этот тип по умолчанию не имеет знака, что является причиной многих ошибок. Обычно имеется возможность управлять компилятором, переключая тип char со знакового на беззнаковое по умолчанию. Но, чтобы никогда не зависеть от особенностей компилятора, рекомендуется всегда явно указывать signed или unsigned при использовании типа char. Так же ключевое слово short используется достаточно редко5 в программировании для микроконтроллеров. Программист имеет возможность определять новые типы на основе уже определенных, для этого служит ключевое слово typedef, например: typedef unsigned char byte – определение нового типа byte для обозначения однобайтного числа без знака. То есть сначала следует ключевое слово typedef, затем указывается уже определенный тип, а завершает все идентификатор нового типа. После такого определения программист может использовать byte наравне с другими типами, в том числе и для нового определения типа. Все рассмотренные типы считаются простыми типами. Кроме них, имеются и сложные, например, массивы (см. далее), структуры и объединения (см. далее) – они рассмотрены отдельно. Однако все сказанное относится и к ним, т.е. любой сложный тип может использоваться для определения нового типа пользователя. Константы Константа – это лексема языка, представляющая какое-либо фиксированное числовое или иное значение. Константы делятся на несколько групп: целочисленные, действительные, символьные и строковые. В зависимости от значения константы, компилятор относит ее к одному из известных типов данных, выделяя для нее соответствующее количество байтов памяти. Целочисленные константы представляют собой запись чисел в одной из допустимых систем счисления: десятичной, двоичной6 , 4 Приведены типы, являющиеся стандартными в версии Си для микроконтроллеров восьмеричной и шестнадцатеричной. Система счисления определяется компилятором следующим образом: если лексема, определенная как константа, начинается с символов «0b» – принимается, что это запись числа в двоичной системе; если лексема начинается с символов «0x» – принимается шестнадцатеричное представление числа. Восьмеричная система подразумевает, что число всегда начинается с нуля. Если же ни один из рассмотренных признаков не обнаружен – принимается десятичная система. Вот пример записи константы (числа 307) в разных системах счисления: 307 – десятичная запись 0b100110011 – двоичная запись 0463 – восьмеричная запись 0x133 – шестнадцатеричная запись. По значению константы компилятор определяет наименьший подходящий для нее тип данных. Однако часто необходимо задавать константы так, чтобы они заранее соответствовали конкретному типу, например, нужно определить константу long равную 2. Если просто написать 2, то компилятор посчитает, что для константы достаточно типа char, и будет его и использовать (далее будет показано, к чему это может привести). Чтобы принудительно указать размер константы, используются специальные суффиксы, приписываемые к константе справа: L – для обозначения long (можно и прописную букву «l» использовать) U – для обозначения unsigned (можно и прописную букву «u» использовать). Суффиксы могут использоваться по отдельности или вместе в любых комбинациях. Таким образом, ранее упомянутая константа должна быть записана в виде 2L или 2ul. Константы действительного типа определяются компилятором по наличию точки в записи лексемы или по наличию символа «e» (или «E»), отделяющего мантиссу7 от показателя степени числа 10. Вот пример констант этого типа: 66.2 .1 0. 2.14e-7 Количество байтов, выделенное компилятором для хранения константы, в этом случае определяется формой представления чисел с плавающей точкой, т.е. может быть разным для разных компиляторов. При помощи служебного слова sizeof можно получить константу, равную количеству байтов для хранения константы: sizeof(3) будет равно 1 (1 байт); sizeof(3L) будет равно 4 (4 байта), и т.д. Символьная константа – это разновидность целочисленной константы, служащей для определения кода алфавитно-цифрового символа. Для обозначения такой константы используется запись символа, окруженного апострофами: ′F′ ′5′ ′.′ Очевидно, что далеко не любой символ можно ввести таким образом. Снять проблему позволяет особый символ «обратная черта» (или back-slash): этот символ (называется escape-символ) позволяет ввести неотображаемые символы, символы «апостроф», «кавычки», «вопрос» и «обратная черта», а так же любой другой символ, указав его код. Определение символа при помощи escape называется escapeпоследовательностью8 : Escape-последовательность \a \b \f \n \r \t \v \\ \′′ \” \? \000 \0x00 Код символа 0x07 0x08 0x0c 0x0a 0x0d 0x09 0x0b 0x5c 0x27 0x22 0x3f 000 0x00 Наименование символа Звуковой сигнал «Забой» (возврат на символ) Перевод страницы Перевод строки Возврат каретки Горизонтальная табуляция Вертикальная табуляция Обратная черта Апостроф Кавычки Знак вопроса Любой восьмеричный код символа Любой шестнадцатеричный код символа AVR 5 Ключевое слово short имеет важную роль в системах, для которых тип int не является 16-битным числом (например, для PC-компьютеров). В этом случае тип char соответственно может быть 16-битным, и тогда для получения однобайтного типа необходимо использование short char. 6 Двоичные константы определены для GNU-расширений Си. 60 7 Подразумевается, что читатель знаком с показательной формой записи действиетльных чисел. 8 Escape-последовательности ведут свое начало с тех времен, когда работа велась через консольные текстовые терминалы – эти последовательности управляли положением «курсора» на терминале или печатающем устройстве. Радиолюбитель – 01/2010 СПРАВОЧНЫЙ МАТЕРИАЛ При вводе символов по их кодам в escape-последовательности следует помнить, что код не может быть больше 255 (в десятичном представлении). Наконец, строковая константа – это последовательность символов, заключенная в кавычки. Среди символов строковой константы могут быть и escape-последовательности. Если введены подряд несколько строковых констант, разделенных лишь пробелами, табуляциями, переводами строки или комментариями – они объединяются в одну строковую константу. Длинные строки можно переносить со строки на строку при помощи символа переноса – той же самой «обратной черты». Есть несколько особенностей слияния и переноса строк, связанных с обработкой пробелов: 1. Если происходит слияние строк – результирующая строка будет содержать последовательно все символы обеих строк (это очевидно). 2. Если строка переносится при помощи символа переноса, то в результирующую строку будет включены все пробелы от последнего символа до символа переноса и все пробелы от начала следующей строки до константы. Вот примеры, поясняющие сказанное: “ïðîñòàÿ ñòðîêîâàÿ êîíñòàíòà” ”ñëèÿíèå ñòðîê ” ”123” ”ïåðåíîñ ñòðîêè \ 123” ”åùå îäèí âàðèàíò\ ïåðåíîñà ñòðîêè” ”âàðèàíò\ ïåðåíîñà” Компилятор преобразует эти константы в следующие: “ïðîñòàÿ ñòðîêîâàÿ êîíñòàíòà” ”ñëèÿíèå ñòðîê 123” ”ïåðåíîñ ñòðîêè 123” ”åùå îäèí âàðèàíò ”âàðèàíòïåðåíîñà” ïåðåíîñà ñòðîêè” Важной особенностью строковых констант заключается в том, что компилятор всегда добавляет к явно введенным программистом символам еще один – символ завершения строки, код которого 0х00 (так называемый null-символ или завершающий ноль). Этот символ никак не виден в тексте программы, но обрабатывается компилятором и программой. Переменные В процессе работы любая программа не только использует константы, т.е. фиксированные значения, но и получает, изменяет и выводит данные различных типов, т.е. оперирует изменяющимися числами (как было сказано ранее, все данные, в конце концов, есть числа). Изменяемые величины хранятся в ОЗУ, каждое отдельное число в отдельных ячейках. Для обозначения таких ячеек введена особая лексема – переменная. Переменная – это идентификатор, обозначающий строго определенную область памяти, содержащую данные конкретного типа. Для определения любых переменных не допускается использовать идентификаторы, совпадающие с ключевыми словами языка или уже определенными ранее (например, введенных программистом типов). Из определения переменной следует ее неразрывная связь с одним из определенных типов, т.е. переменные могут быть типа char, int, float и т.д. В программе каждая переменная должна быть обязательно определена, т.е. описана. Описание переменной осуществляется по следующему шаблону9 : <тип> <список идентификаторов>; Здесь тип – это один из ранее определенных типов, а список идентификаторов – это один или более идентификаторов, разделенных запятыми. Завершать определение должна традиционная точка с запятой. Запятая – это особый разделитель, используемый для разделения элементов списка, т.е. таких элементов, которые должны восприниматься компилятором как некая неделимая группа. 9 Здесь и далее, если иное не оговорено, используется традиционный формат описания шаблонов выражений: в угловых скобках указываются обязательные элементы, в квадратных – опциональные, все прочие символы вне скобок являются обязательными. Радиолюбитель – 01/2010 unsigned long var; int a, b, c; char ch; Пример демонстрирует определение переменной var, имеющей тип «беззнаковое длинное целое», трех переменных a, b и с типа «целое число со знаком» и одну переменную ch типа «символ». Встретив определение переменных, компилятор резервирует необходимое количество ячеек памяти, которые затем будут использованы в программе по усмотрению программиста. Использование неописанных переменных в программе не допускается. По умолчанию каждой из описанных переменных сразу присваивается нулевое значение – говорят «переменная проинициализирована значением по умолчанию» (инициализация по умолчанию характерна не для всех переменных, о чем будет сказано далее). Далее будет показано, как можно инициализировать переменные другими значениями. Программист должен знать, что память для хранения переменных выделяется в порядке их описания, т.е. для вышеприведенного примера в ОЗУ сразу после 4-го байта переменной var будут следовать 2 байта переменной a и т.д. Порядок байтов, составляющих переменную, определяется платформой, для которой компилируется программа. Для микроконтроллеров AVR принято, что многобайтные переменные хранятся в памяти «от младшего байта к старшему», т.е. первым размещается младший байт (наименее значащие биты числа), а затем все более старшие, вплоть до последнего (наиболее значащие биты числа). При описании переменных могут дополнительно использоваться ключевые слова static, register, extern и volatile. Эти слова могут располагаться как до определения типа переменной, так и после него, однако обязательно до указания идентификатора. Если переменная объявлена с использованием слова register, это означает, что компилятор поместит ее в один из доступных регистров микроконтроллера. Не стоит надеяться, что регистровые переменные каким-то образом помогут создать более быстродействующую программу: для большинства случаев результат будет скорее обратный, хотя в некоторых случаях положительный эффект может быть. Ключевое слово volatile – очень важное. Дословно оно означает «изменяемая», что на первый взгляд излишне для переменной, само наименование которой означает изменение значения. Однако, дело тут в другом. Компилятор в процессе своей работы анализирует написанный программистом текст программы и старается убрать из него те части, которые явно не несут никакого смысла (подробнее об этом см. в главе «Об оптимизаторе программ»). В частности, компилятор может исключить все участки программы, которые обрабатывают переменные, не изменяющие (с точки зрения компилятора) значения. Разумеется, далеко не всегда это соответствует замыслам программиста. Ключевое слово volatile, использованное при объявлении переменной, укажет компилятору, что эта переменная все-таки изменяется, пусть даже и неизвестным компилятору способом. Можно считать, что volatile есть синоним слова «неприкосновенный», т.е. объявленную таким образом переменную нельзя трогать при оптимизации. Назначение остальных ключевых слов будет рассмотрено далее. Массивы Массив – это последовательно хранимый набор нескольких «пронумерованных» переменных одинакового типа, называемых элементами массива. Для определения массива используется следующий шаблон: <тип> <идентификатор>[<размер>]; Здесь тип – как и ранее, любой из определенных типов, обозначающий тип каждого элемента массива, идентификатор – это собственно имя переменной-массива, а размер (который может и отсутствовать) – это константа, ограничивающая количество элементов массива. Обратите внимание на то, что квадратные скобки должны присутствовать всегда, независимо от того, задается ли размер или нет – это признак массива. 61 СПРАВОЧНЫЙ МАТЕРИАЛ Из определения массива следует, что, во-первых, все элементы массива находятся в памяти друг за другом, а во-вторых, имеют одинаковый тип. Чем же это отличается от обычного последовательного определения нескольких однотипных переменных? int a[5]; // ìàññèâ a[] èç 5-è ýëåìåíòîâ òèïà int int a1, a2, a3, a4, a5; // 5 îòäåëüíûõ ïåðåìåííûõ òèïà int Отличие принципиальное. Определенные во второй строке переменные независимы друг от друга, для обращения к каждой из них программист обязан использовать уникальный идентификатор. У массива же идентификатор единственный, а для обращения к его элементам используется индекс, т.е. номер элемента: a[0] – первый элемент массива, a[4] – пятый элемент. Обратите внимание, что нумерация элементов массива начинается с нуля. Преимущество массивов перед отдельными переменными бросается в глаза на простом примере. Если представить себе строку символов в виде массива и в виде нескольких отдельных символьных переменных, сразу становится ясно, что работа со строкой уже из десятка символов становится неразумно сложной, в то время как каждый их элементов массива-строки может быть обработан одним и тем же способом. В качестве указателя размера массива при описании может использоваться любая целочисленная константа, а при обращении – константа или переменная соответствующего типа. В Си никак не контролируется допустимость индекса при обращении к элементу массива, т.е. обращение к a[12] будет воспринято компилятором, как безошибочное, однако результат в этом случае не предсказуем. Рассмотренный пример массива – одномерный, т.е. его можно представить в виде «линейки» с делениями – элементами. Однако нередко требуется оперировать массивами-«плоскостями», «кубами» и более многомерными10 «фигурами». Такая возможность предусмотрена в Си: многомерный массив описывается так же, как и одномерный, только количество квадратных скобок (с возможно указанным размером «измерения») соответствует числу измерений: обрабатывая список сотрудников, удобно объединить в одну группу фамилию, имя и отчество сотрудника, и хранить такие группы в массиве. Это, с одной стороны, позволило бы при подсчете общего количества сотрудников просто считать такие группы, но с другой стороны, в любой момент можно получить имя любого из сотрудников по его номеру. Для подобной группировки введено понятие структуры. Структура – это особая форма слияния нескольких переменных, называемых полями структуры, в единое целое. Для определения структуры используется ключевое слово struct. Шаблон определения структуры следующий: struct {<поле 1>; <поле 2>; … ; <поле N>;}; Здесь в фигурных скобках перечисляются поля структуры, которые есть ничто иное, как объявления переменных. Обратите внимание, на точки с запятой, завершающие определение каждого поля структуры. Структура – это разновидность типа, поэтому определение переменной типа структура осуществляется аналогично ранее рассмотренному объявлению простых переменных: struct {int a; long b; char c;} st; Этот пример показывает, как объявляется переменная st типа структура с полями а (типа int), b (типа long) и с (типа char). Обращение к переменным-полям структуры осуществляется через так называемую точечную нотацию, когда поле структуры отделяется от ее идентификатора точкой: st.a позволит обратиться к полю, а переменной st, а st.c – к полю ее полю с. Ничто не препятствует объявить массив таких структур: struct {int a; long b; char c;} st[5]; Этот пример определяет массив st, состоящий из 5-и элементов, каждый из которых имеет поля a, b и с соответствующих типов. Точечная нотация продолжает действовать и при обращении к элементам массива структур: st[1].a – поле a второго элемента массива, st[4].c – поле с последнего элемента. Так же ничто не препятствует определить структуру как тип, и использовать для определения новых переменных уже этот тип: int a1[5]; // 1-ìåðíûé ìàññèâ èç ýëåìåíòîâ int typedef struct { int a; long b; char c;} strc; int a2[][]; // 2-ìåðíûé ìàññèâ ýëåìåíòîâ int strc st; // ñ íåîïðåäåëåííûìè ðàçìåðàìè strc arr_st[5]; int a3[3][3][3]; // 3-ìåðíûé ìàññèâ ýëåìåíòîâ int // ñ îïðåäåëåííûìè ðàçìåðàìè Подобные записи «читаются» слева направо, например, третья строка примера читается так: массив из трех элементов, каждый из которых представляет собой массив из трех элементов, элементами которого являются массивы из трех элементов типа int. «Трехмерный массив» звучит куда проще, но это неудобное описание помогает понять, как именно следует обращаться к многомерному массиву: a3[0] – ýòî ìàññèâ äâóìåðíûé a3[0][0] – ýòî ìàññèâ îäíîìåðíûé a3[0][0][0]– ýòî ýëåìåíò ìàññèâà (íå ìàññèâ!) òèïà int Кстати, фактически многомерные массивы – это лишь форма сокращенной (и, возможно, более удобной) записи одномерного массива, размер которого определяется произведением размеров по его «измерениям». Т.е. рассмотренный массив a3 можно представить в виде одномерного массива с размером 27 элементов. Именно как одномерный и хранится в памяти многомерный массив, причем «развертка измерений» происходит по измерениям справа налево, т.е. самым первым в памяти будет элемент a3[0][0][0], затем a3[0][0][1], затем a3[0][0][2], затем a3[0][1][0] и т.д. Как и любые переменные, массивы в момент описания инициализируются значением по умолчанию. Кроме того, как и для любой переменной, для объявления массива возможно использование ключевых слов extern, static и volatile. Язык Си считается самым низкоуровневым языком среди высокоуровневых. Под этим следует понимать, что язык позволяет получить возможность практически так же манипулировать данными, как и ассемблер. Одной из таких «низкоуровневых» возможностей являются объединения. По виду описания объединение (определяемое ключевым словом union) очень похоже на структуру: union { int a; long b; char c;} uni; Однако, принципиальное отличие от структуры в том, что все поля объединения физически соответствуют одной и той же области памяти! Размер области памяти, выделяемой компилятором, определяется так, чтобы поместилось поле наибольшего размера, т.е. для вышеприведенного примера будет выделено 4 байта для переменной uni. При этом при обращении к uni.b будет задействована вся эта область, при обращении к uni.a - только первые 2 байта этой области, а uni.c позволит оперировать лишь первым байтом. То есть налицо обращение к одной и той области памяти, как к переменным разного типа. Объединение, так же как и структура, может использоваться в описании нового типа, быть элементом массива и полем структуры. Действует правило: все, что уже определено, может быть использовано. Разумеется, все равнее сказанное в отношении переменных, применимо и к структурам и объединениям. Литература, Интернет-ссылки 1. http://www.arv.radioliga.com/ Структуры и объединения Часто требуется объединить несколько переменных или типов данных (одинаковых или разных) в некую группу. Например, 10 Вспоминается шутка математиков: «представьте себе проекцию семимерного куба на пятимерную плоскость»… 62 Продолжение в №2/2010 Радиолюбитель – 01/2010 СПРАВОЧНЫЙ МАТЕРИАЛ Книга по работе с WinAVR и AVR Studio Продолжение. Начало в №1/2010 Указатели Указатель – это переменная особого рода: она содержит не само значение переменной, а адрес той области памяти, где искомое значение хранится. Указатель – это одна из наиболее мощных возможностей языка Си и, как любое мощное средство, указатель может быть как чрезвычайно удобным инструментом, так и источником больших проблем, если используется неумело. Для объявления указателя достаточно добавить к типу переменной символ «*»: unsigned int * iptr; char * arr[]; long * lptr; Объявленные таким образом переменные iptr, arr[] и lptr будут указателями: iptr – указатель на число типа int, arr – массив указателей на символ, lptr – указатель на число типа long. Чтобы обратиться к тем данным, на которые указывает переменная-указатель, следует предварить ее имя символом «*» (такая операция получила название «разыменование указателя»): iptr – адрес области памяти, хранящей число типа int *iptr – значение этого числа Очевидно, что использование указателей позволит получить доступ к любой произвольной области памяти. Именно в этом и кроется источник возможных проблем: стоит ошибиться в значении переменной-указателя, как последствия обращения к данным, на которые она указывает, становятся непредсказуемыми. Выражения Выражение – это запись алгоритма вычислений над операндами при помощи знаков математических, логических и прочих действий. Другими словами: выражение – есть формула вычислений, записанная средствами языка программирования. В определении выражения использован новый термин: операнд. Операнд – это выражение, над которым выполняется какое-либо действие. Как видите, операнд и выражение оказываются определенными друг через друга. Надеюсь, для образованного человека не составит труда представить себе, что же такое выражение на самом деле – для этого достаточно вспомнить, что такое математическое выражение. В языке Си определены два типа операндов (выражений): математические и логические. Математическое выражение состоит из операндов, эквивалентных числам, и знаков математических операций. Логическое выражение – это выражение отношений, т.е. ответов на вопросы «больше?», «меньше?», «равно?» и т.д. Аналогично математическим, существуют логические действия и соответствующие им знаки логических операций. Действия могут выполняться над двумя операндами или над одним операндом – унарные операции. В Си определены следующие унарные математические операции: Знак Операция – Унарный минус, признак изменения знака числа на противоположный + Унарный плюс, означает сохранение знака числа без изменения (принципиального смысла не имеет) ++ Знак инкремента, увеличение операнда на 1 -Знак декремента, уменьшение операнда на 1 ~ Побитовая инверсия числа Математических операций над двумя операндами больше: Знак Операция + Сумма операндов – Разность операндов * Произведение операндов / Частное операндов Радиолюбитель – 02/2010 Роман Абраш г. Новочеркасск E-mail: arv@radioliga.com % | & ^ Остаток от целочисленного деления операндов Побитовая операция ИЛИ над операндами Побитовая операция И над операндами Побитовая операция ИСКЛЮЧАЮЩЕЕ ИЛИ над операндами >> Побитовый сдвиг влево << Побитовый сдвиг вправо Об операциях побитового сдвига следует упомянуть, что эти операции сохраняют знак операнда, если операнд имеет знаковый тип. Унарная логическая операция всего одна: Знак Операция ! Логическое отрицание (НЕ) Логических операций над парой операндов больше: Знак Операция || Логическое ИЛИ && Логическое И > Больше < Меньше == Эквивалентно != Не эквивалентно <= Меньше или равно >= Больше или равно Важно понимать, что для логических выражений определено лишь два значения результата: ИСТИНА и ЛОЖЬ, причем истинным в Си считается любое не равное нулю значение, а ложным – значение ноль. Вот несколько примеров логических и арифметических выражений: 5 + 2 * 3 – àðèôìåòè÷åñêîå âûðàæåíèå, ýêâèâàëåíòíîå 11 (5 + 2) * 3 – àðèôìåòè÷åñêîå âûðàæåíèå, ýêâèâàëåíòíîå 21 5 + var * (3 + b) – àðèôìåòè÷åñêîå âûðàæåíèå ñ èñïîëüçîâàíèåì ïåðåìåííûõ (5 > 2) – ëîãè÷åñêîå âûðàæåíèå ñî çíà÷åíèåì ËÎÆÜ (2-7) >= -11 – ëîãè÷åñêîå âûðàæåíèå ñî çíà÷åíèåì ÈÑÒÈÍÀ (2-7) >= 0 – ëîãè÷åñêîå âûðàæåíèå ñî çíà÷åíèåì ËÎÆÜ Знаки арифметических и логических операций называются по-другому операторами, однако понятие оператор более широкое, и включает в себя не только какое либо одно действие, но и целую группу действий, выполняемых по определенным правилам. Особняком стоит операция, обозначаемая символом «?» – условная операция. Эта операция имеет достаточно сложную форму записи: <выражение1> ? <значение1> : <значение2> Операция позволяет выбрать в качестве своего результата одно из значений – значение1 или значение2 в зависимости от того, истинно или ложно выражение1. В качестве обоих значений могут использоваться так же выражения. Пример: x > 5 ? 4 : 8 – если x будет меньше или равно 5, то результат операции будет 8, а если x будет больше 5 – результат будет 4. Смысл в выражениях был бы невелик, если бы результат их вычислений было бы невозможно присваивать значениям переменных. Для присвоения значения переменной используется оператор присваивания, обозначаемый символом «=»: var = 5 + 2; // ïðèñâîèòü ïåðåìåííîé var çíà÷åíèå âûðàæåíèÿ 5 + 2, ò.å. 7 Возвращаясь к описанию переменных, следует заметить, что можно присваивать значение переменной сразу в момент ее описания: int var = 5; // îïèñàííàÿ ïåðåìåííàÿ var èìååò òèï int è çíà÷åíèå 5 В операторе присваивания левее знака «=» должно находиться так называемое леводопустимое выражение, т.е. либо переменная, либо такое выражение, результатом которого будет значение указателя, который покажет место в памяти для хранения результата. Правее знака равенства может находиться любое праводопустимое 57 СПРАВОЧНЫЙ МАТЕРИАЛ выражение, т.е. в сущности, любое выражение. Интересный момент возникает, если и слева и справа от знака равенства используется одна и та же переменная: S = S + 5; Такое выражение следует понимать так: «присвоить переменной S значение, равное сумме ее текущего значения и числа 5». Можно записать то же самое более коротким способом: S += 5; В данном примере использован двойной оператор «+=», называемый присваивание с суммированием. Значение этого оператора то же самое: увеличение значения переменной на число правее оператора. Кроме присваивания с суммированием допустимы аналогичные комбинации для присваивания с разностью, с умножением, делением или остатком от деления, записываемым соответственно так: «–=», «*=», «/=» или «%=». Любой такой оператор «разворачивается» аналогично рассмотренному: a *= 2 ðàâíîñèëüíî a = a * 2 // âàðèàíò 1 a /= 2 ðàâíîñèëüíî a = a / 2 ptr = &temp; a -= 2 ðàâíîñèëüíî a = a – 2 var = *ptr++; a %= 2 ðàâíîñèëüíî a = a % 2 // âàðèàíò 2 Несколько слов о присваивании значений переменным типа указатель. Как было сказано, указатель есть ни что иное, как адрес объекта в памяти, соответственно в качестве значения можно ему присваивать только адрес. Делается это при помощи унарного оператора взятия адреса & (не путать с побитовой операцией И !!!): int *ptr; int var; ptr = &var; В этом примере объявлен указатель ptr на переменную типа int и переменная var этого типа; далее происходит присваивание указателю значения адреса переменной var. Теперь операторы *ptr = 12; и var = 12; приведут к одинаковому результату. Иногда требуется обратиться к какому-либо участку памяти, который явно не является конкретной переменной, т.е. адрес участка известен, но соответствующего ему описанного в программе объекта нет. В этом случае следует использовать прием, названный явным приведением типа: ptr = (int *)0x1000; Пример показывает, как указателю ptr присваивается значение адреса 0x1000, т.е. происходит «обман» бдительного компилятора: ему подсовывают указание воспринимать число, как адрес переменной типа int, хотя не известно, что именно находится по этому адресу фактически. Приведение типов приходится делать всегда, когда типы операндов не соответствуют желаемым. Приоритеты операций Каждая операция имеет свой приоритет, который определяет порядок вычисления операции в выражении: если слева направо следуют подряд несколько операций одинакового приоритета, порядок их вычисления не определен, но результат вычисления гарантированно будет верным, в противном случае вычисляются прежде операции с наибольшим приоритетом, а затем – с меньшим. Порядок вычислений может быть изменен при помощи круглых скобок: выражение в скобках вычисляется всегда прежде остальных, т.е. имеет наивысший приоритет. В таблице перечислены все операции с указанием их приоритетов: Операция Приоритет ! ~ «унарные плюс и минус» ++ -- ? операция разименования указателя * 13 58 */% 12 +-( 11 >> и << 10 < <= > >= 9 == != 8 & 7 ^ 6 | 5 && 4 || 3 ? 2 = *= /= += -= &= ^= |= <<= >>= 1 Так как запомнить все приоритеты довольно непросто, желательно на первых порах (и не возбраняется вообще всегда) указывать порядок вычислений при помощи скобок. Использование операторов ++ и -- совместно с указателями приводит к необходимости помнить о порядке выполнения операций: ptr = &temp; var = (*ptr)++; Оба варианта дадут одно и то же значение var, но совсем разные значений для temp и ptr: в первом случае переменная temp не изменится, но увеличится на 1 значение указателя ptr; во втором случае неизменным останется значение указателя, но изменится содержимое переменной, на которую он указывает, т.е. temp увеличится на 1. Операторы ++ и -- с указателями работают особым образом: они изменяют значение указателя на величину, равную размеру типа элемента, на который указатель указывает. То есть если указатель объявлен как int *ptr, то ptr++ изменит значение указателя на 2, если long *ptr, то ptr++ увеличит указатель на 4 и т.д. Увеличение указателя на 1 происходит только для типа указателя (void *) («абстрактный указатель», просто адрес байта) или (char *). В любом выражении допустимо комбинировать логические и арифметические выражения, в этом случае следует считать, что логическому значению ЛОЖЬ соответствует арифметическое значение ноль, а значению ИСТИНА – арифметическое значение 1: var = (5 < 2) + 2; // ïåðåìåííîé var áóäåò ïðèñâîåíî çíà÷åíèå 2 var = (5 >= 2) + 2; // ïåðåìåííîé var áóäåò ïðèñâîåíî çíà÷åíèå 3 Особое внимание следует обращать на приоритет операций: var = (5 > 2) + 2; // var = 3 var = 5 > 2 + 2; // var = 0 Во втором выражении более высокий приоритет имеет арифметическая операция сложения, поэтому переменной var будет присвоено значение выражения 5 > 4, что ЛОЖНО, т.е. var будет равна 0. Наиболее простым и универсальным советом будет использование скобок везде, где необходимо точно гарантировать порядок вычислений – в этом случае можно считать (конечно, лишь условно!) все операторы равноприоритетными, а порядок вычислений определять лишь с помощью скобок. Возможно, это приведет к некоторой излишней «скобочности» программы, зато гарантирует, что планы программиста не разойдутся с мнением компилятора. Упомянутые ранее унарные операторы инкремента и декремента – одна из интересных особенностей языка Си. Эти операторы могут применяться только к операнду-переменной, и ни в коем случае не к операнду-константе. Но главное, они могут быть префиксными и постфиксными, т.е. записываться либо слева от переменной, либо справа: int var = 1; var++; // òåïåðü var = 2 --var; // òåïåðü var = 1 var--; // var = 0 Радиолюбитель – 02/2010 СПРАВОЧНЫЙ МАТЕРИАЛ Казалось бы, к чему два способа выполнить одно и то же? Все дело в том, как будет использован результат оператора при вычислении выражений. Вот пример: int var = 5; int b = var++ - 5; // var ñòàíîâèòñÿ ðàâíûì 6 int ñ = ++var – 5; // var ñòàíîâèòñÿ ðàâíûì 7 Переменная b окажется равной 0, а переменная с будет равна 2. Все дело в том, что при постфиксном операторе ++ сначала используется значение переменной в выражении, а потом выполняется оператор инкремента, а при префиксной записи – сначала выполняется оператор инкремента, а потом уже обновленное значение переменной используется при вычислении выражения. Операторы побитового сдвига << и >> выполняют перемещение битов операнда слева на количество разрядов, заданное выражением справа. Как уже было сказано, эти операторы сохраняют знак левого операнда, если операнд – число со знаком. Заполнение «освобождаемых» разрядов для беззнаковых чисел происходит нулями. Унарные логические операции, и операция побитовой инверсии имеют лишь один вариант вычисления. Но, к сожалению, на этом простые операторы заканчиваются, все прочие операторы имеют более сложную форму записи и, соответственно, выполняют более сложные действия, заслуживая отдельного рассмотрения. Тип результата выражения Выражения могут состоять из операндов различных типов, например, не возбраняется осуществлять суммирование переменных типа char и long, double и unsigned int и т.д. И результат этих вычислений так же может быть присвоен переменной любого типа. Как же ведет себя компилятор в этом случае? Рассмотрим следующий пример: int x1 = 20000, x2 = 15; long res; res = x1 * x2; Переменной res (длинное целое) присваивается результат произведения двух чисел (int), значения переменных укладываются в допустимые диапазоны, ожидается, что значение res будет 300000. Однако, это ошибочное предположение. Стандартом Си определено, что тип результата выражения определяется по набольшему типу входящего в него операнда, т.е. в данном случае результат x1 * x2 будет иметь тип int. Разумеется, произведение чисел вызовет переполнение результата, и все, что «не влезло» в размер, будет отброшено, т.е. в результате произведение будет равно -27680. Такой странный результат получился из-за несоответствия типа результата выражения и типа операндов. Чтобы получить верный результат, нужно использовать один из операндов типа long: res = (long)x1 * x2; // èëè òàê res = x1 * (long)x2; Такая запись показывает, что компилятор должен воспринимать операнд x1, как число типа long. Принудительное указание в круглых скобках нового типа для выражения называется явным преобразованием или приведением типа. Ошибкой будет попытка задать тип для результата всего выражения: res = (long) (x1 * x2); Такая запись ничем не отличается от первоначальной и даст неверный результат (т.к. в этой записи явно указано то преобразование, которое компилятор и так делает автоматически). Компилятор всегда приводит автоматически типы всех операндов в выражении к наибольшему типу, т.е. от меньшего типа к большему. Важно помнить, что если в выражении все операнды типа char, то они будут приведены к типу int, если нет явного преобразования типов. При помощи приведения типа программист может изменить его поведение, что чревато большими ошибками. Радиолюбитель – 02/2010 Операторы Оператор if Условный оператор if позволяет выполнить группу операторов лишь в случае, если какое-то логическое выражение имеет значение ИСТИНА. Дополнительно может быть определена группа операторов, выполняемых в случае, если заданное выражение имеет значение ЛОЖЬ. Оператор if может быть описан по одному из следующих шаблонов: 1. if (<проверка>) <оператор1>; 2. if (<проверка>) <оператор1>; else <оператор2>; 3. if (<проверка>) { <список операторов1 ;>} 4. if (<проверка>) { <список операторов1 ;>} else {список операторов2 ;} Здесь проверка – это логическое выражение, проверяемое на истинность, оператор1 – оператор, выполняемый только в случае истинности проверки, оператор2 выполняется только в случае ложности проверки. Под списком операторов подразумевается любая последовательность любых операторов, причем, как и одиночные, эти списки выполняются лишь в соответствующем случае в зависимости от результата проверки. Обратите внимание, что для 3 и 4 форм оператора if наличие завершающей оператор точки с запятой не является необходимым. В данном случае закрывающая фигурная скобка однозначно свидетельствует о завершении оператора, в то время как в списках каждый из операторов должен завершаться точкой с запятой (если, конечно, оператор в списке не ограничивается фигурными скобками). Шаблоны оператора if не накладывают никаких ограничений на то, какие операторы могут быть на месте оператра1 или оператора2 или же в соответствующих списках. Это означает, что среди них так же может использоваться оператор if. В этом случае может возникнуть проблема с определением принадлежности части оператора else: if (a < 2) if (c > 5) result = 5; else result = 0; В этом примере стоит задуматься, к какому из операторов if относится часть else? Язык Си однозначно вводит следующее правило: всякий else относится к ближайшему предыдущему if, не имеющему своего else. Таким образом, в вышеприведенном примере else относится к оператору if(c > 5). Очевидно, что запись оператора в одну строку не способствует улучшению восприятия программы. Благодаря тому, что язык Си допускает использование любых «пробельных» разделителей, гораздо более удачной следует считать следующую форму записи: if (a < 2) if (c > 5) result = 5; else result = 0; Благодаря использованию табуляций вышеприведенный пример даже визуально показывает принадлежность else соответствующему оператору if. Следует отметить, что пример показывает одну из проблем, характерную для оператора if – так называемую неполную проверку вариантов. Она заключается в следующем: если, например, переменная а равна 5, то переменная result оказывается с неопределенным значением. Возможно, она получит (или получила) нужное значение в другом операторе, возможно переменная a никогда не получит значение больше 1 (но тогда, к чему вообще проверка?!), однако если это не так – проблем не миновать. Итак, при использовании оператора if желательно не допускать двусмысленностей его поведения при различных результатах проверяемых выражений. Так же не лишним будет использование фигурных скобок для определения границ списка операторов, даже если фактически используется единственный оператор. Оператор switch Анализ разных значений переменной при помощи оператора if может вылиться в «многоэтажную» конструкцию типа такой: 59 СПРАВОЧНЫЙ МАТЕРИАЛ if (x == 0) x = 5; else if (x == 1) x = 12; else if (x == 3) x = 36; // è ò.ä. äî ïåðåáîðà âñåõ âàðèàíòîâ çíà÷åíèÿ õ Это очень неудобная конструкция. Заменить ее призван оператор switch, который записывается по следующему шаблону: switch (<выражение>){ case <значение1>: [операторы1]; case <значение2> : [операторы2]; // и т.д. до конца вариантов значений [default : [операторы по умолчанию]]; } Здесь выражение – это выражение, значения которого анализируются, значение1 – это один из вариантов анализируемого результата выражения, операторы1 - соответствующая этому варианту обработка, значение2 и операторы2 – это соответственно следующее анализируемое значение и соответствующая ему обработка и т.д. Операторы по умолчанию выполняются в том случае, если выражение имеет значение, не соответствующее ни одному из определенных в строке с ключевым словом case. Операторы по умолчанию и ключевое слово default могут отсутствовать. Результат анализируемого выражения обязательно должен быть одного из целочисленных типов, а в качестве значений case обязательно должны использоваться константы или выражения, вычисляемые на основе констант. Допускается использовать символы (в апострофах). Оператор действует следующим образом: вычисляется значение выражения, результат проверяется на совпадение с одним из указанных в строчках case. Если найдено совпадение, то, начиная с этого места и вплоть до конца оператора (т.е. до закрывающей фигурной скобки), выполняются все обозначенные операторы, в том числе и те, которые относятся к другим, «нижеследующим» за найденным значениям. Если совпадений не найдено, то выполнятся операторы по умолчанию, если они определены, в противном случае не будет выполнено никаких действий. Пример: int res; switch (x){ case 0 : res = 1; case 1 : res = 2; default : res = 3; } Если x в этом примере равен 5, то значение переменной res станет равно 3. Однако, если x будет равен 0, то res все равно окажется равным 3, т.к. поочередно выполнятся все операторы, начиная с того, что указан в строке case 0. Чтобы этого бессмысленного поведения не происходило, введен особый оператор break, который вызывает прекращение работы оператора switch досрочно, т.е. этот оператор является последним исполняемым оператором внутри switch. Таким образом, правильная запись желаемого алгоритма должна быть такой: int res; switch (x){ case 0 : res = 1; break; case 1 : res = 2; break; default : res = 3; } В этом случае для x равного 0, 1 и 5 будут получены соответственно значения res 1, 2 и 3. Для последнего варианта наличие «прекращающего» оператора не требуется, это очевидно. Оператор for При реализации многих алгоритмов бывает необходимо повторить некую последовательность действий несколько раз. Такое повторение получило наименование цикла, а оператор, для соответствующего действия – оператором цикла. В Си имеется несколько операторов цикла, один из которых – оператор for. Шаблон описания оператора следующий: 60 for ([список операторов1]; [условие продолжения]; [список операторов 2]) [тело цикла]; Здесь список операторов1 – перечень операторов, разделенных запятыми, которые должны быть выполнены перед началом цикла; условие продолжения – логическое выражение, истинное значение которого есть непременное условие очередной итерации цикла; список операторов2 – перечень операторов, разделенных запятыми, выполняемых после каждой итерации цикла; тело цикла – это один оператор или ограниченная фигурными скобками последовательность операторов, выполняемых на каждой итерации. Самое важное, что любая из этих частей оператора for может отсутствовать, о чем свидетельствуют квадратные скобки в шаблоне. Пример некоторых вариантов оператора цикла for: for (i = 0, s = 0; i < 10; s += i++); for (;;); for (;i < 10;); for (i = 0, j = 9, s = 0; i < 10; i++, j—) s += arr[i][j]; Первый цикл примера вычисляет сумму всех чисел от 0 до 9 и помещает результат в переменной s. Рассмотрим более подробно, как это происходит. Сначала выполняются операторы i=0 и s = 0, которые инициализируют (ранее объявленные) переменные i и s. Если в момент объявления эти переменные уже проинициализированы, эта часть оператора for может отсутствовать. Затем происходит проверка условия выполнения итерации цикла, т.е. проверка i < 10. Разумеется, это истинное выражение, и, значит, происходит выполнение тела цикла – очередная (первая) итерация. В рассматриваемом примере тело цикла отсутствует, поэтому просто выполняется завершающая часть, которая состоит из оператора s += i++, который одновременно выполняет 2 действия: накапливает в переменной s сумму значений переменной i, а затем увеличивает значение i на 1. После этих действий снова осуществляется проверка условия, затем очередная итерация и т.д. до тех пор, пока значение i не станет равным 10. В этот момент условие i < 10 станет ложным, и выполнение оператора for закончится. Продолжим рассмотрение примеров операторов for. Второй цикл, единожды начавшись, никогда не закончится, т.е. это бесконечный оператор, или бесконечный цикл. В этом случае отсутствие выражения проверки условия трактуется как истинное выражение, т.е. «нет требований к условию – значит, любые варианты подходят». Третий пример показывает оператор цикла, который может быть бесконечным, если переменная i < 10. Т.е. для того, чтобы этот цикл не смог «завесить» программу, необходимо, чтобы либо к началу цикла переменная i уже содержала значение 10 или более, либо каким-то образом во время выполнения цикла значение i должно быть изменено. Наконец, четвертый пример показывает, как вычислить сумму «правой» диагонали матрицы arr размером 10x10. Следует отметить, что допускается11 внутри списка операторов1 использовать определение переменных, однако, определенные таким образом переменные, считаются существующими только пока цикл выполняется, и «исчезают» после его завершения: for (int i=0; i < 10; i++) a += i; Оператор for имеет довольно много вариантов и возможностей, однако не стоит увлекаться предоставляемой гибкостью. Чем более простая форма оператора будет избрана, тем меньше вероятность ошибки программирования: // ïåðâûé ñïîñîá for (int i, j=9, s; i < 10; s += arr[i++][j—]); // âòîðîé ñïîñîá int i=0, s=0, î = 9; for (; i<10; i++) { s += arr[i][j]; j—; } 11 Такое допущение для компилятора WinAVR возможно лишь в том случае, если включен режим соответствия стандарту С99 с расширениями GNU. Радиолюбитель – 02/2010 СПРАВОЧНЫЙ МАТЕРИАЛ В этом примере приведены два варианта ранее рассмотренного подсчета суммы «правой» диагонали матрицы, однако первый способ требует гораздо больше умственных усилий для понимания написанного, чем второй. Оператор while Оператор for удобен, если требуется повторить какие-либо действия определенное число раз. Но иногда число повторений неизвестно, хотя известно условие завершения цикла (например, при решении уравнения методом последовательных приближений). В этом случае можно использовать оператор цикла по условию while. Шаблон этого оператора следующий: while (условие) [тело цикла]; Здесь условие – логическое выражение, истинное значение которого является условием продолжения цикла, тело цикла – это либо один оператор, либо ограниченная фигурными скобками последовательность операторов. Важная особенность while в том, что если к моменту его начала условие ложно – тело цикла не выполнится ни разу. Пример операторов while: while (1); while (PORT == 2) sum++; Первый пример демонстрирует бесконечный цикл, т.к. ненулевое значение равносильно истинному логическому выражению. Второй пример опрашивает значение переменной PORT и ведет подсчет в переменной sum числа таких опросов до тех пор, пока значение переменной равно двум12 . По сравнению с оператором for, цикл while допускает значительно меньше вариантов «оформления». Очевидно, что цикл for вполне в состоянии заменить цикл while: // ýêâèâàëåíò áåñêîíå÷íîãî öèêëà for (;;); // ýêâèâàëåíò îïðîñà è ïîäñ÷åòà ÷èñëà ýòèõ îïðîñîâ for (; PORT == 2; sum++); Тем не менее, рекомендуется использовать while там, где это оправдано логикой алгоритма, это позволит улучшить «читабельность» программы. Оператор do Как было показано, тело оператора while может быть не выполнено ни разу, если условие его продолжения сразу ложно. Но иногда требуется, чтобы при прочих равных условиях тело цикла всегда выполнялось хотя бы один раз. Для этих целей применяется оператор do, записываемый по следующему шаблону: do [тело цикла] while (условие); Здесь, как и ранее, тело цикла – это либо один оператор, либо заключенная в фигурные скобки последовательность операторов, а условие – это логическое выражение, определяющее условие продолжения итераций цикла. Как и ранее, тело цикла может отсутствовать. В остальном цикл do аналогичен циклу while. Оператор break Оператор break служит для досрочного завершения операторов цикла (любых) и оператора switch. Исполнение этого оператора равносильно переходу к оператору, следующему за «телом» упомянутых операторов. Пример: Оператор continue Оператор continue служит для досрочного начала очередной итерации цикла, т.е. он обеспечивает немедленный переход к проверке условия для циклов do или while, или к переходу к выполнению последней группы операторов, указанных в круглых скобках, для цикла for. Этот оператор служит для пропуска части операторов цикла, если на определенной итерации их выполнять не следует. Пример: int arr[10]; int found, sum; for (int i = 0; i < 10; i++){ if (arr[i] == 12){ found++; continue; } sum += arr[i]; } В этом примере осуществляется подсчет суммы sum всех значений массива arr, не равных 12, и одновременно подсчет в found количества элементов, равных 12. Оператор goto Последний оператор языка Си – оператор безусловного перехода goto. Шаблон оператора такой: goto <метка>; Метка – это идентификатор, при объявлении завершаемый двоеточием. Этот идентификатор служит для обозначения определенного места в программе, где метка объявлена (отождествляется с адресом исполняемого кода в памяти программ). При использовании в операторе goto метка указывается уже без двоеточия. Оператор goto вызывает безусловное продолжение исполнения программы с указанной метки. Пример использования меток и оператора goto: if (a < 5) goto m1; a = 0; goto m2; m1: a = 25; m2: Этот пример показывает реализацию следующего алгоритма: если значение переменной a меньше пяти, то присвоить переменной значение 25, в противном случае обнулить переменную a. Использование оператора goto в программах считается дурным тоном среди программистов. Как правило, программы с этими операторами более запутаны, труднее отлаживаются, таят больше потенциальных возможностей для ошибок. Метка может находиться почти в любом13 месте программы, и соответственно, оператор goto может заставить программу изменить нормальный ход непредсказуемым образом, если программист случайно забудет вовремя убрать или изменить нужную метку. Доказано, что любой алгоритм может быть реализован без использования goto, лишь с помощью других операторов языка Си. Например, только что рассмотренный алгоритм элементарно и гораздо красивее реализуется так: if (a < 5) a = 25; else int arr[10]; a = 0; int found=-1; for (int i = 0; i < 10; i++) if (arr[i] == 12) { found = i; break;} В этом примере осуществляется поиск в массиве arr элемента, равного числу 12. Как только такой элемент будет найден, цикл завершится досрочно, и значение i будет сохранено в переменной found. Если после завершения цикла значение found будет равно -1, это будет означать, что элементов 12 в массиве нет, в противном случае found будет равно индексу первого найденного элемента. 12 То, каким образом значение переменной PORT может измениться – не рассматривается в данном контексте, предполагается пока, что все-таки такое изменение возможно. Радиолюбитель – 02/2010 Продолжение в №3/2010 13 Об ограничениях на размещение меток см. главу «Структура программы». 61 СПРАВОЧНЫЙ МАТЕРИАЛ Книга по работе с WinAVR и AVR Studio Продолжение. Начало в №1-2/2010 Функции Как было сказано, язык Си предоставляет в распоряжение программиста небольшой, но достаточно мощный инструментарий в виде набора операторов, которые позволяют достаточно просто решать определенные наиболее часто требуемые задачи. Однако программист наверняка будет нуждаться и в других решениях «типовых» для его алгоритма задач. Т.е. наверняка в программе найдутся определенные участки, выполняющие конкретные этапы алгоритма и при этом повторяющиеся с минимальными изменениями во многих местах. Для уменьшения таких повторов, а заодно для качественного улучшения реализации алгоритма введено понятие функции. Функция – это особым образом оформленная последовательность операторов, заменяемая в тексте программы своим коротким эквивалентом, т.е. обращением к функции. Функция представляет собой как бы мини-программу по обработке небольшого количества данных. Данные, передаваемые ей на обработку, получили название параметров функции, а результат их обработки назван значением функции. Для возврата результата функции служит особый оператор return. Различают описание (определение) функции, реализацию и ее использование (иначе говоря – вызов, т.е. обращение к функции). Как и переменная, функция должна быть определена до первого обращения к ней. Шаблон определения функции следующий: <тип> <идентификатор>([список параметров]); Здесь тип – любой ранее определенный тип данных, идентификатор – это собственно «имя» описываемой функции, а список параметров – это разделенный запятыми список описаний «входных» переменных. Описание функции напоминает описание переменной, за исключением обязательно присутствующих круглых скобок (список параметров может отсутствовать). Под типом функции подразумевается тип возвращаемого ею значения. Реализация функции – это уже раскрытие в виде операторов алгоритма ее работы. Шаблон реализации функции следующий: <тип> <идентификатор>([список параметров]) { [список описаний переменных] [последовательность операторов] } Начало реализации аналогично определению, однако завершается не точкой с запятой, а ограниченным фигурными скобками телом функции. Тело функции состоит из необязательного объявления переменных, принадлежащих только этой функции, и необязательной последовательности операторов, реализующих алгоритм работы функции. Необязательность всех элементов лишь подчеркивает, что такое допустимо синтаксисом языка, но вовсе не здравым смыслом. Наконец, использование функции осуществляется по следующему шаблону: <идентификатор>([список значений]); Здесь идентификатор – тот же, что был задан при описании и реализации функции, а список значений – это список выражений, используемых для передачи в функцию параметров. Число выражений в этом списке обязательно должно совпадать с числом параметров в определении функции. Рассмотрим все вышесказанное более подробно на примере. Предположим, необходимо иметь функцию вычисления квадрата гипотенузы по значению катетов прямоугольного треугольника. Определимся с входными и выходными данными. Очевидно, что значения длин катетов – это входные, а квадрат гипотенузы – выходные данные. Ограничим диапазон значений входных данных типом int, тогда очевидно для выходного значения потребуется тип long. Опишем функцию q_hyp, которая получает на входе два 54 Роман Абраш г. Новочеркасск E-mail: arv@radioliga.com значения типа int, и возвращает результат типа long (воспользуемся шаблоном описания функции): long q_hyp (int katet1, int katet2); Далее необходимо выполнить реализацию функции: long q_hyp(int katet1, int katet2){ long result; result = katet1*katet1 + katet2*katet2; return result; } Следует обратить внимание на новый оператор return, имеющий очень простой шаблон: return [возвращаемое значение]; Этот оператор вызывает немедленное завершение работы функции с заданным возвращаемым значением, которым может быть любое выражение. В реализации функции использована дополнительная переменная result типа long. Эта переменная описана внутри тела функции и называется локальной переменной, т.е. доступной только внутри тела этой функции. Это означает, что вне тела функции переменная result не существует, в противовес глобальным переменным, описанным вне тела функции (в тексте программы), которые существуют и доступны как внутри функции, так и в остальной части программы. Функция может использовать любые глобальные переменные, но ее локальные переменные нигде, кроме ее тела, использовать нельзя. Как же можно использовать только что определенную функцию? Любым из следующих способов: int a = 2, b = 5; long r; r = q_hyp(2,5); // r áóäåò ðàâíî 29 r = q_hyp(a, 4); // r áóäåò ðàâíî 20 r = q_hyp(a, b); // r áóäåò ðàâíî 29 r = q_hyp(5 + a, b *2); // r áóäåò ðàâíî 149 q_hyp(a,b); // ðåçóëüòàò ôóíêöèè áóäåò ïðîèãíîðèðîâàí r = 10 * q_hyp(a, b); // r áóäåò ðàâíî 290 То есть обращение к функции эквивалентно использованию переменной соответствующего значения. Кстати, если результат функции не используется – это не является ошибкой. Самое важное: переменным, определенным в качестве входных, присваиваются значения, фактически указанные при обращении функции. Говорят, что формальным параметрам присваиваются фактические значения. Переменные-формальные параметры функции доступны в ее теле наравне с локальными, причем любые их изменения никак не отражаются на «внешней» программе. В только что рассмотренном примере r = q_hyp(a, b) никаким способом функция не может изменить значения переменных a и b, даже если бы в ее теле было написано katet1 = 100 – действует правило локальности переменной katet1. Теперь рассмотрим различные допустимые варианты реализации функций. Функция, которая не возвращает значения. Для такого случая существует особый вид типа – void (пусто). Этот тип означает буквально «нет значения». Для описанных как void функций недопустимо использование оператора return с указанием возвращаемого значения. Обычно функции без возвращаемого значения изменяют глобальные переменные. Пример: void func(int x); Функция без параметров. Такая функция может как возвращать значение, так и обойтись без этого. При ее описании, реализации и использовании просто указываются круглые скобки с ключевым словом void внутри. Пример: int func(void); Радиолюбитель – 03/2010 СПРАВОЧНЫЙ МАТЕРИАЛ Следует так же оговорить особенность оператора return. Как было сказано, он вызывает немедленное завершение функции, т.е. в следующем примере функция всегда будет возвращать значение 100 вне зависимости от значения переданных ей параметров: long q_hyp (int katet1, int katet2); Далее необходимо выполнить реализацию функции: long q_hyp(int katet1, int katet2){ long result; return 100; result = katet1*katet1 + katet2*katet2; return result; Этот пример объявляет переменную funcptr типа указатель на функцию, возвращающую результат типа int и имеющую 2 параметра типа int. При объявлении указателя на функцию запись выглядит слегка шиворот-навыворот, но так уж требует Си: сначала мы указываем тип результата функции, затем обязательно в круглых скобках идентификатор (не забудьте про звездочку – признак указателя), а затем, опять в круглых скобках, список типов параметров функции. Сделанная запись – не что иное, как объявление переменной с новым типом. Можно определить этот тип при помощи typedef, после чего определять указатели на функции станет легче: typedef int (*t_func)(int, int); t_func funcptr; } Если функция не возвращает значения, использование оператора return необязательно – в этом случае функция завершится после исполнения последнего оператора ее тела. Внутри функции могут быть определены метки для использования совместно с оператором goto. Это локальные метки, т.е. оператор goto может осуществить переход лишь на метку в пределах тела функции, и никуда более. Глобальных меток в Си не существует (см. раздел «setjmp.h – Нелокальные переходы goto» для знакомства со способом обойти это ограничение). Рекурсия Особый случай использования функции – это обращение к ней изнутри ее тела. Такой метод получил название рекурсивного вызова функции, или рекурсии. Существует большое количество алгоритмов, которые чрезвычайно сложно реализуются обычным, нерекурсивным образом, в то время как с использованием рекурсии решение получается простым и красивым. Среди них, например, различные алгоритмы сортировки, поиска и т.п. Рассмотрим пример рекурсивной функции для вычисления факториала числа: long long factorial (unsigned char N){ long long result; if (N != 0) result = N * factorial(N-1); else result = 1; return result; } Рекурсивный алгоритм подсчета факториала заключается в том, чтобы обращаться самому к себе, но со значением входного числа на 1 меньше текущего значения, пока входное значение не станет равно нулю. Возможно, это не самый удачный пример эффективного рекурсивного алгоритма, однако он хорошо раскрывает главную опасность рекурсии. Дело в том, что все локальные переменные размещаются в ОЗУ, причем при каждом новом обращении к функции происходит выделение для них новой области памяти. Чем «глубже» происходит погружение в рекурсивные вызовы, тем больше расходуется памяти. Это легко может привести к полному исчерпанию доступного пространства ОЗУ во время расчета, и узнать об этом будет чрезвычайно проблематично, ведь Си для микроконтроллеров не содержит никаких средств контроля подобных ситуаций. Поэтому, не смотря на всю привлекательность рекурсивных алгоритмов, использовать их можно лишь с большой осторожностью. Косвенное обращение к функции Рассмотренные способы обращения к функции являются примерами непосредственного ее вызова, точно так же, как ранее непосредственно использовались значения переменных. Но при помощи указателя можно было получить доступ к содержимому переменной и косвенным образом, аналогичный способ существует и для обращения к функции. Очевидно, реализация этого способа требует определения указателя на функцию. Делается это достаточно просто, путем объявления переменной соответствующего типа: int (*funcptr)(int, int); Радиолюбитель – 03/2010 Этот пример полностью аналогичен предыдущему. Обращение к функции по указателю происходит чуть иначе, чем обращение к переменной по указателю: в данном случае операция разыменования указателя не используется: funcptr = (t_func)0x1000; funcptr(3, 12); // âûçîâ ôóíêöèè ñ ïàðàìåòðàìè ïî àáñîëþòíîìó àäðåñó В этом примере можно было бы использовать и такое присваивание значения указателю: funcptr = (void *)0x1000; так как тип указателя (void *) совместим с любым иным типом указателя. Препроцессор Препроцессор – это отдельное средство предварительной обработки текста программы. Препроцессор изменяет текст программы автоматически перед началом работы компилятора. Эти изменения включают: - «слияние» строковых констант - удаление комментариев - замена escape-последовательностей на фактические коды символов - выполняются текстовые макро-подстановки (см. далее) - включение и исключение участков текста программы Директивы препроцессора Управление работой препроцессора осуществляется при помощи специальных ключевых слов, названных директивами препроцессора. Директива обязательно должна начинаться с символа «#» в первой позиции строки программы. Набор директив может быть весьма обширен, но обязательно реализуются директивы включения файлов, определения макроподстановок и условной компиляции. #include Директива включения файла в текст программы. Формат директивы может быть двух вариантов: #include <имя файла> или #include ”имя файла” Здесь в первом случае угловые скобки – неотъемлемый атрибут директивы. Имя файла – это имя любого текстового файла, возможно, с указанием пути к нему. Действие этой директивы заключается в том, что в место нее в текст программы помещается содержимое указанного файла без изменений, т.е. текст программы расширяется содержимым файла. Дальнейшая обработка препроцессором уже ведется над этим «расширенным» текстом. Никаких проверок содержимого файла не производится. Первый вариант директивы (с указанием файла в угловых скобках) заставляет препроцессор искать файлы в особой директории, так называемой директории стандартных библиотек. Второй вариант ограничивает поиск той директорией, где находится обрабатываемый файл, т.е. в директории пользователя. 55 СПРАВОЧНЫЙ МАТЕРИАЛ Наличие двух вариантов директивы позволяет программисту заменять системные библиотечные файлы собственными. Обычно этой директивой подключаются файлы с расширением .h – так называемые заголовочные файлы. В результате обработки препроцессором будет сформирован следующий текст (комментарии из текста удаляются): #define Директива определения макроподстановки (макроса). Директива может иметь один из двух форматов: #define <идентификатор> [значение] или #define <идентификатор>(псевдо-параметры) <тело> Здесь идентификатор – любой допустимый синтаксисом идентификатор, значение – любой набор символов. Псевдо-параметры – это список (через запятую) идентификаторов, используемых в качестве составной части при определении тела, представляющего собой так же любой набор символов. Первая форма директивы получила название директивы определения символа или простого макроса, а вторая – директива определения макроса-функции (из-за внешнего сходства с определением функции). Действие директивы в любом из форматов заключается в том, что препроцессор, встретив в тексте программы определенный директивой идентификатор, осуществляет подстановку вместо него соответствующего значения или тела, причем подстановка осуществляется именно путем ввода текстовых символов. Это, в частности, подразумевает, что в качестве идентификатора можно использовать даже ключевые слова языка Си! Рассмотрим ряд примеров. Пусть указаны следующие директивы: В первом операторе из-за наличия однострочного комментария в строке определения макроса будет удалена важная часть, а во втором получится сразу две ошибки: во-первых, макрос – это не функция, и в итоге оператор присваивания приобретает недопустимый вид, а во-вторых, умножение на 12 окажется отделенным точкой с запятой из тела макроса от остальной части оператора, что, естественно, так же недопустимо. В связи с этими особенностями макросов настоятельно рекомендуется придерживаться следующих правил: 1. В определении макроса, если это необходимо, использовать только комментарии вида /* … */ 2. Заключать в круглые скобки как все тело макроса, так и входящие в него псевдопараметры 3. Не завершать тело макроса точкой с запятой Примеры правильно определенных макросов: #define D1 7 #define D2 + 5 * #define D3 if А далее в тексте программы эти символы используются так: for (i = D1; i < D1 D2 3; i++) D3 (i == 5) break; После того, как препроцессор обработает указанные строки, будет сгенерирован следующий текст программы: for (i = 7; i < 7 + 5 * 3; i++) if (i == 5) break; То есть вместо символов D1, D2 и D3 соответственно будут подставлены их значения. Смысл полученного текста лежит на совести программиста. При объявлении макроса-функции перед тем, как осуществить замену символа в тексте программы, препроцессор осуществляет подстановку вместо псевдо-параметров их фактические значения, и лишь затем тело макроса подставляется в текст: #define foo(x) x + 5 int k = foo(45*x); // ïîñëå îáðàáîòêè ïðåïðîöåññîðîì çàìåíèòñÿ íà for (i = 0; i < 56 count = while(arr[i++] != ‘ ‘); * 12; #define LINE 56 /* ÷èñëîâóþ êîíñòàíòó ìîæíî áåç ñêîáîê */ #define cnt(x,y) ( (x) * (y) ) /* ñêîáêè ëèøíèìè íå áûâàþò! */ Если определяемый макрос не умещается на одной строке, допускается использовать символ переноса – обратная черта. Допускается повторно определять один и тот же макрос в разных местах программы, это сопровождается предупреждением компилятора, но, в сущности, безопасно. Переопределенный макрос начинает действовать для строк программы, находящихся ниже его определения, а выше действует старое определение. #undef Директива, которая отменяет описание макроса, ранее сделанное директивой #define. Формат директивы очень прост: #undef <идентификатор> С момента появления в тексте программы этой директивы, макрос, определяемый идентификатором, перестает существовать для препроцессора. Если после этого снова определить макрос с тем же идентификатором – компилятор не выдаст предупреждения. Директивы условной компиляции Группа директив препроцессора, позволяющих в зависимости от значения определенных символов (макросов) изменять содержимое текста программы: #if, #ifdef, #ifndef, #else, #elif и #endif. Эти директивы сложные, т.е. состоят из более чем одной строки. Начинаются всегда с одной из первых трех директив, завершаются всегда директивой #endif. Оставшиеся директивы #else и #elif – дополнительные, могут и отсутствовать. Эти директивы, как по синтаксису, так и по смыслу, очень близки к оператору if и служат для проверки определенного условия, и, в зависимости от истинности этой проверки, исключения указанных строк программы из процесса компиляции. Пояснить сказанное проще всего на примерах. #if (VER > 5) int k = 45*x + 5; Важно понимать, что любой макрос – это именно текстовая подстановка, т.е. в отличие от настоящей функции не происходит никакой передачи параметров, вызова и т.п. – именно поэтому псевдопараметр макроса не имеет типа. Из-за того, что подстановки осуществляются, начиная с первого символа (не пробельного) после идентификатора и до конца строки, следует быть осторожными с использованием однострочных комментариев и точки с запятой. Следующие примеры явно неверные: int x = 10; #endif В примере строка int x = 10 будет включена в текст программы только в том случае, если значение ранее описанного символа VER будет больше 5. Если же символ VER не определен вообще, или его значение менее 5 – строка будет удалена препроцессором. #if (VER > 5) int x = 10; #else #define LINE 56 // îïðåäåëÿåì êîíñòàíòó long x = 10; #define cnt(x, y) while(x[y++] != ‘ ‘); #endif for (i = 0; i < LINE; i ++) arr[i] = i; count = cnt(arr, i) * 12; 56 //ñòðîêè, êîòîðûå áóäóò îáðàáîòàíû òîëüêî â ñëó÷àå, åñëè VER > 5 В этом примере в текст программы будет введено определение переменной int x только для случаев, когда VER > 5, а в противном переменная x будет иметь тип long. (см. следующую страницу) Радиолюбитель – 03/2010 СПРАВОЧНЫЙ МАТЕРИАЛ #if (VER > 5) int x = 10; #elif (VER > 3) char x = 10; #else long x = 10; #endif Этот пример показывает, как изменяется тип переменной x в зависимости от значения VER: Значение VER Тип переменной x 6 и более int 4 или 5 char 3 и менее long Количество директив #elif может быть любым. Если не требуется анализировать значение символа, а достаточно лишь убедиться в том, что он определен, используется такой вариант: #ifdef VER int x = 10; #endif Если символ VER определен «выше по тексту» программы – определение переменной включается в текст программы, иначе – исключается. Директива #ifndef позволяет что-то включить в текст программы, если символ НЕ описан ранее. Обе директивы могут содержать блок #else для указания альтернативного включения текста. Не нужды упоминать, что указанные директивы могут «охватывать» любое количество строк программы, в том числе любые другие директивы препроцессора. Главное, о чем нельзя забывать: директивы препроцессора не могут использоваться для контроля содержимого переменных! То есть определить, существует или нет переменная var при помощи директивы #ifdef var можно, но будет ошибочной попытка проверить ее значение директивой #if var == 15. Структура программы Ранее была использована аналогия между разговорным языком и языком программирования, благодаря чему были введены понятия алфавита, лексем и т.п. Однако, «вызубрить» словарь и правила построения предложений еще не достаточно, чтобы на равных общаться с иностранцем, необходимо знать еще характерные идиомы, традиции и т.п. – все то, что создает атмосферу взаимопонимания. Аналогичная ситуация и в программировании: чтобы компилятор без проблем воспринял программу, следует придерживаться определенных правил в ее организации. Ряд этих правил относятся к категории «неписаных», т.е. формально не обязательных, но очень и очень желательных к применению. Кроме этого существуют строгие правила, которых, к счастью немного. Основные понятия Рассмотрение структуры программ будет осуществлено на основе наиболее сложного, всеобъемлющего случая. Менее сложные (и более близкие к реальным любительским проектам) получаются путем соответствующего упрощения, о чем по мере изложения будет упоминаться особо. Существует ряд устоявшихся терминов в среде программирования, которые будут использованы и в этой книге. Проект – совокупность всех файлов, необходимых для выполнения поставленной перед программистом задачи. Включает в себя файлы исходных текстов программ, документацию, промежуточные файлы, генерируемые компилятором, и главное: результат компиляции проекта – загружаемый файл. Загружаемый файл – результат работы компилятора, файл, содержимое которого готово для загрузки в микроконтроллер. Иногда используют неофициальный термин «прошивка». Исходный текст программы, как правило, состоит из более чем одного файла. Каждый отдельный файл, составляющий программу, Радиолюбитель – 03/2010 называют модулем. Модуль обычно строится из описаний переменных, типов, макросов, функций и т.п., сгруппированных по функциональному назначению. Например, программа может состоять из модуля работы с дисплеем, модуля обслуживания клавиатуры и т.д. Обязательным является наличие как минимум одного модуля – главного, который, как правило, имеет название, совпадающее с названием всего проекта. Главный модуль использует, в том числе, определения, сделанные в других модулях. Основное его отличие в том, что он обязательно должен содержать реализацию (описание не требуется) главной функции main() – это зарезервированное имя для главной функции. Реализация этой функции должна быть следующей: int main(void){ <òåëî ôóíêöèè> } Главная функция автоматически получает управление после начала работы программы, т.е. она вызывается самой первой. Завершение функции main() равносильно прекращению любой работы. Для микроконтроллеров несущественно значение, которое функция возвращает, поэтому в теле функции нет необходимости использовать оператор return. До реализации любой функции в любом модуле должны быть сделаны все необходимые объявления (описания) используемых в этой функции типов, констант, переменных и т.п. В частности, должны быть сделаны подключения файлов, в которых описаны функции из других модулей проекта. Такие файлы получили название заголовочных и традиционно имеют расширение .h (от англ. header – заголовок). Как правило, каждый модуль имеет свой заголовочный файл. В нем должны быть описаны все функции, переменные, типы, макросы и т.п., словом, все то, что должно быть доступно в главном модуле (или любом другом, если это требуется). Реализация функций выполняется в файле с исходным текстом модуля, который имеет традиционное расширение .c. Файл заголовка включается директивой #include в соответствующий модуль и, таким образом, в этом модуле становятся доступны все объявленные возможности. Деление содержимого исходного текста модуля – условное, т.е. не обязательное. Ничто не препятствует подключать в главный модуль сразу исходный текст вспомогательного, однако такой подход не одобряется. Может быть так же определен заголовочный файл для всего проекта в целом. Обычно в него помещают определения макросов и символов, которые используются всеми модулями проекта. Например, описав символ DEBUG, можно при помощи директив условной компиляции изменять логику работы любого из модулей во время отладки. Локальность и глобальность В проекте, состоящем из нескольких файлов, изобилующих определениями переменных, функций и т.п., необходимо четко ориентироваться в том, какие описанные элементы доступны в тех или иных файлах. Считается, что любое описание действительно только внутри блока, ограниченного фигурными скобками, где это определение реализовано. Для модуля аналогом фигурных скобок выступают начало и конец файла модуля. Любое объявление считается локальным по отношению к своему блоку. Таким образом, все объявления, сделанные в начале модуля, «видимы» и доступны для любых функций этого модуля. Такие объявления называют глобальными для данного модуля. Итак, каждое объявление может быть локальным и глобальным одновременно, все зависит от точки отсчета. Чтобы обеспечить видимость глобальных объявлений модуля из других модулей, необходимо вынести их в заголовочный файл, который подключить директивой #include в нужном модуле. Объявлять переменные в заголовочном файле – неверная практика, т.к. это может привести к многократному переобъявлению одних и тех же идентификаторов в различных модулях, т.е. всюду, где заголовочный файл будет подключен. Переменные и так по умолчанию считаются «видимыми» для других модулей, хотя для того, чтобы 57 СПРАВОЧНЫЙ МАТЕРИАЛ воспользоваться переменной из стороннего модуля, необходимо объявить ее внешней. Кроме видимости, стоит упомянуть еще о действительности значений глобальных переменных. Считается, что глобальная переменная сохраняет свое значение в пределах блока своего описания. То есть, если значение глобальной переменной Cnt будет изменено функцией foo(), то это значение будет действительно и для вызванной следом функции fee(). Внешние функции и переменные По отношению к текущему модулю все переменные и функции из других модулей считаются внешними, т.е. недоступными. Если необходимо использовать эту внешнюю переменную, ее нужно объявить в модуле с указанием ключевого слова extern: extern int Cnt; Пример показывает объявление внешней переменной Cnt. При компиляции такой программы компилятор зарезервирует адрес этой переменной, не определяя его конкретное значение, а компоновщик при сборке загружаемого файла будет осуществлять поиск во всех модулях объявление переменной Cnt и, если найдет, присвоит значение ее адресу. Таким образом, внешние переменные становятся доступны в модуле лишь после компоновки. Для обращения к функции из другого модуля в большинстве случаев не требуется указания служебного слова extern – достаточно простого наличия ее объявления в подключаемом заголовочном файле, действия компоновщика при этом аналогичны рассмотренным. Иное дело, если функция определена, но исходного текста соответствующего модуля нет. Такая ситуация возможна, если вместо исходного текста модуля имеется объектный файл. В этом случае объектный файл включается в обработку компоновщиком, и для объявленной функции использование ключевого слова extern является обязательным. Иными словами, слово extern означает, что адрес объекта должен быть найден не компилятором, а компоновщиком на основе содержимого объектных файлов, используемых для сборки проекта. Излишне говорить, что фактический тип внешней переменной или функции должен совпадать с объявленным, а для функций еще должны совпадать типы и количество параметров. Так как на этапе компоновки уже нет возможности точно сверить соответствие фактической реализации объявлению, несоблюдение этого правила чревато крупными проблемами. Обычно, в этом случае генерируется лишь предупреждение, а не ошибка. static-переменные Ранее уже говорилось о сохранении значений глобальных переменных. Однако очень часто требуется, чтобы локальная переменная внутри функции сохраняла бы свое значение от вызова к вызову. Например, многие алгоритмы генерации псевдослучайных чисел для получения нового числа используют значение, возвращенное в предыдущий раз. Возможность сохранять значение локальной переменной есть – для этого служит ключевое слово static (т.е. статическая), добавляемое к типу переменной при ее определении. Например: int func(void){ static int result = 0; return ++result; } При первом обращении к функции func() она вернет результат 1, при втором – 2, при третьем – 3 и т.д., т.е. будет возвращать порядковый номер обращений к ней. Для хранения этого номера используется локальная переменная result, объявленная статической. Инициализация статической переменной происходит лишь единственный раз, при первом обращении к функции, а все прочее время эта переменная сохраняет ранее присвоенное ей значение. Об оптимизаторе программ Рассмотренные средства языка Си позволяют строить программы произвольной сложности. Удобство языка высокого уровня 58 может создать ложное впечатление о «вседозволенности» или «безграничных возможностях» языка, т.е. может показаться, что любая программа будет очень качественной лишь благодаря средствам Си, а от программиста не требуется повышенных усилий при ее разработке, как, например, это необходимо при работе на ассемблере. Увы, использование языка высокого уровня вызывает неизбежное увеличение размера итогового программного кода, повышает требовательность программы к памяти и быстродействию микроконтроллера. Для систем на базе персональных компьютеров с сотнями мегабайт доступного ОЗУ и гигагерцами тактовой частоты процессора эти проблемы, возможно, и не особенно актуальны, но не для микроконтроллеров AVR, с их единицами килобайт ОЗУ (а то и десятками байт) и весьма скоромной производительностью ядра. Чтобы хоть как-то снизить остроту этой проблемы, все компиляторы Си содержат средства встроенной оптимизации генерируемого кода. В чем же заключается оптимизация и как она осуществляется компилятором? Обратимся к функции, рассмотренной в разделе «static-переменные». Если задуматься, то как бы эта функция не использовалась в программе, ее можно с успехом заменить обращением к переменной result с автоинкрементом (если бы эта переменная была не локальной, а глобальной), и тем самым исключить лишний код и расходы стековой памяти на реализацию обращений к функции. Но программист, предположим, не посчитал нужным так поступить, и тогда в дело вступает оптимизатор компилятора, который незаметно изменяет логику программы, не нарушая ее принципиально. То есть вместо обращения к функции func() компилятор встраивает код result++, переместив при этом переменную result из области локальных переменных к глобальным. Для программиста все остается, как было, но фактически программа уже немного не та… Другой способ оптимизации заключается, например, в том, что компилятор может добиться более высокой скорости выполнения цикла, заменив его на следующие друг за другом участки одинаковой функциональности, т.е. фактически аннулируя смысл оператора цикла. «Развернутый» цикл может быть больше по размеру кода, но часто оказывается оптимальнее по требованиям к ОЗУ и более быстродействующим. Практически всегда компилятор заменяет функцию, которая используется лишь однажды в программе на эквивалентные операторы, помещенные прямо в месте вызова функции. Так же у компилятора хватает интеллекта разобраться с неизменными значениями выражений и заменить несколько операторов сразу константой результата. Пример: define MAX 15 int sum, i = 10; while (i < MAX){ if (i == 9) break; i++; sum *= i; } Если проанализировать этот участок кода, то окажется, что несмотря на всю его сложность, значение переменной sum всегда будет равно нулю. Но это значение переменная и так получает по умолчанию при описании, следовательно, оператор цикла можно устранить из программы, никак не нарушив ее функционирования. Этот пример, конечно, из разряда казусов. Однако такие казусы случаются нередко, приводя, порой, к занимательным результатам: если представить, что значение sum используется далее в программе, то автоматически могут быть исключены и другие участки кода: if (sum > 10) x = x + sum; else x = x * sum; delta = (x + i * 12)/2; И в этом участке лишним окажется оператор if, более того, значение переменной x так же оказывается предопределенным, и Радиолюбитель – 03/2010 СПРАВОЧНЫЙ МАТЕРИАЛ компилятор может счесть ее вовсе лишней, исключив и другие участки, где эта переменная задействована. В общем, может оказаться, что написанная с большими усилиями программа не делает ничего… Конечно, это свидетельствует в первую очередь о недостаточно хорошо продуманном алгоритме, либо о грубых ошибках программирования – к сожалению, интеллекта никакого оптимизатора не хватит найти и исправить такие ошибки. С другой стороны, программист сам может предпринимать меры по оптимизации своей программы. Например, один из известных способов увеличения быстродействия программы (в ущерб ее размеру) заключается в принудительной замене вызовов функций их содержимым в каждом месте, где функция используется. Такой прием получил название встраиваемых в код функций, или (более традиционный термин) inline-функций. Ключевое слово inline, использованное при описании функции, указывает компилятору (а точнее – его оптимизатору) при первой же возможности заменить обращения к функции явной вставкой в текст соответствующих операторов. В этом случае функция превращается в некое подобие макроса-функции. Однако само по себе ключевое слово inline еще ничего не гарантирует – компилятор все равно поступит так, как посчитает наиболее оптимальным. Другая проблема, связанная с работой оптимизатора – это переменные, которые изменяют свое значение не в том модуле, где они описаны, а в другом, для которого они являются внешними. Например, в модуле interface.c, отвечающем за интерфейс с пользователем, определена переменная key для хранения кода нажатой клавиши, а значение в нее записывается в одной из функций модуля keyboard.c, отвечающего за опрос клавиатуры (в модуле keyboard.c переменная key описана как extern). В этом случае далеко не исключен (а точнее – закономерен) вариант, что при оптимизации модуля interface.c компилятор сочтет, что переменная key никогда не меняет своего значения, и исключит весь код, связанный с анализом ее значения. Естественно, получившаяся программа будет полностью неработоспособна. Подобное поведение компилятора с оптимизатором породило массу мифов о «неправильных» компиляторах или ошибках в них. Однако, ошибка тут лишь одна – программиста. Чтобы оптимизатор умерил свою прыть и не трогал переменные, проанализировать поведение которых ему не по силам, программист обязан объявлять их как изменяемые, т.е. volatile. В этом случае и сама переменная, и связанный с нею код не будут «испорчены» оптимизатором. Вариантов стратегии оптимизации программы, как правило, три: достижение минимума размера, достижение максимума скорости исполнения и универсальная, т.е. достижения оптимального соотношения размера и производительности. Обычно программисту предоставляются средства для более «тонкой» настройки оптимизатора под свои нужды. Кроме прочего, часто предлагаются средства отступить от стандарта Си или «обычного поведения» операторов и стандартных функций, чтобы выиграть в чем-то другом. С такими средствами программист должен быть особо осторожен, т.к. любое отступление от стандарта может повлечь проблемы. Однако грамотное использование всех средств позволяет достичь существенного выигрыша по всем статьям. Далее в разделе «Оптимизация» рассмотрены основные средства оптимизации, которыми располагает программист при работе с компилятором GCC для микроконтроллеров AVR О стиле программирования Эта глава является заключительной в кратком описании языка Си и посвящается важной части в работе программиста, которой зачастую пренебрегают. Речь пойдет о стиле программирования или, точнее, о стиле оформления программ. Большинство программистов-одиночек разрабатывают программы в твердой уверенности, что делают это «для себя», что никто никогда не заинтересуется в содержимом исходных текстов их программ, и потому нет смысла тратить время и силы на всякие «излишества» и «украшения». Это в корне неверное предположение! Известен афоризм «всякая хорошо работающая вещь – красива», который вполне применим и к программе. Ранее было показано, что язык Си допускает много разных способов14 в составлении выражений, в записи операторов и т.д. Очень часто такое изобилие «инструментария» воспринимается программистом как стимул писать витиеватые программы, изобилующие огромным количеством макросов, переопределяющих едва ли не все ключевые слова языка, многократно и часто без необходимости вложенными друг в друга операторами и т.п. Все это выдается за высокий класс программиста, но фактически является лишь одним из проявлений каких-то комплексов, ничего общего с квалификацией не имеющих. Качественная программа должна легко читаться, для чего порой комментариев в ней оказывается больше, чем собственно операторов. Кроме комментариев, существует и еще способ улучшить читаемость программного текста. Речь идет о так называемом «самодокументировании» программы. Одно время популярной была так называемая венгерская нотация, при которой любой идентификатор содержит в своем начале особым образом закодированный тип (например, все строки начинаются с символа s, указатели с ptr, целые числа – с i и т.п.), однако принципиальным можно считать лишь осмысленность любого идентификатора. Если программа обрабатывает строки, являющиеся именами и фамилиями людей, то более удачным следует признать такие определения: #define MAX_PERSONAL_COUNT 100 typedef struct{ char name[]; char gender[]; char age; } people_struct; people_struct personal_list[MAX_PERSONAL_COUNT]; чем более короткий, но эквивалентный по смыслу вариант struct { char a1[]; char a2[]; int a3; } arr[100]; Для человека, хоть немного владеющего английским, первый вариант расскажет сам за себя: первая строка определяет максимальное количество персон, далее следует определение нового типа-структуры с полями Имя, Пол и Возраст, далее объявляется массив списка персонала. Вторая же запись понятна лишь в программистском смысле, т.е. уловить связь массивов и полей структуры с реально осуществляемыми действиями невозможно. Спустя определенный срок программист, написавший программу в первом стиле (и потративший на 14 В этой книге изложены далеко не все особенности синтаксиса языка Си – с целью упрощения. Показан лишь необходимый минимум для начала осовоения. Радиолюбитель – 03/2010 59 СПРАВОЧНЫЙ МАТЕРИАЛ это немного больше времени), вспомнит все тонкости своей программы за 5 минут, в то время как второй (состряпавший свой вариант на пару часов быстрее) наверняка потратит не один день на это15… Для качественного оформления текста программы обязательно следует использовать выделение иерархически связанных участков при помощи отступов – это значительно улучшает восприятие заложенного в программу алгоритма. Фигурные скобки, ограничивающие тело функций и операторов, принято размешать одним из следующих способов: for (;;) for (;;){ { // óðîâåíü îïåðàòîðîâ öèêëà // óðîâåíü îïåðàòîðîâ öèêëà } } То есть в первом способе открывающая и закрывающая скобки оператора находятся на одном уровнем с самим оператором, но на разных строках, а тело сдвинуто вправо. Во втором случае открывающая скобка находится рядом с оператором, а закрывающая – на уровне оператора. Оба способа позволяют, проследив вертикальные уровни скобок, легко разобраться в уровне вложенности операторов: for(;;) for(;;){ { while (x){ if (x){ while (x) { a = x *2; if (x) }else{ { a = x + 2; a = x *2; } } } else } { a = x + 2; } } } Желательно, выбрав один раз приемлемый для себя стиль оформления программ, придерживаться его все время. Постепенно это станет привычкой и не будет восприниматься рутинной обязанностью. Очень желательно оформлять в виде функций повторяющиеся в разных местах программы участки. Как правило, если участок повторяется 2 или более раз – его стоит оформить в виде функции. Иногда очень удобно оформлять функции не по количеству повторов, а по смыслу выполняемых действий. Например, часто бывает удобно «набросать костяк» алгоритма, обозначив его характерные этапы функциями, которые затем реализовать: int main(void){ init_all(); // èíèöèàëèçàöèÿ âñåé ïåðèôåðèè while(1){ // ãëàâíûé öèêë display(); // îáíîâëåíèå äèñïëåÿ if(test_key() ){ // ïðîâåðêà íàæàòèÿ êëàâèàòóðû // åñëè êíîïêà íàæàòà – ïðîàíàëèçèðîâàòü è îáðàáîòàòü switch (get_key()){ case 0 : press_key0(); break case 1: press_key1(); break; } } } } В этом простом примере показана «заготовка» едва ли не любой программы для микроконтроллера, взаимодействующей с пользователем при помощи кнопок и дисплея. Составив такой скелет, программист может сосредоточиться на реализации уже придуманных функций get_key() – получение кода нажатой кнопки, display() – обновления содержимого дисплея и т.д. Такой порядок разработки программы можно назвать «от общего к частностям» (иногда используют термин нисходящее программирование). Соответственно, при реализации функций так же можно применить этот способ. Заботиться о количестве функций и о том, что многие из них используются лишь один раз, не стоит – оптимизатор все сделает лучшим образом. Программист должен думать, а процессор – работать. 15 Видимо, на этом основана поговорка программистов «легче сделать все с начала, чем разобраться в ранее написанном». Продолжение в №4/2010 60 Радиолюбитель – 03/2010 СПРАВОЧНЫЙ МАТЕРИАЛ Роман Абраш г. Новочеркасск E-mail: arv@radioliga.com Продолжение. Начало в №1-3/2010 WinAVR + AVR Studio Пакет WinAVR включает собственный редактор (Programmers Notepad) для написания исходных текстов программ и ряд утилит, облегчающих этот процесс. Однако значительно более удобным способом следует признать интеграцию WinAVR в бесплатную среду разработки программ для микроконтроллеров AVR фирмы Atmel – AVR Studio, самую свежую версию которой всегда можно скачать с сайта Atmel. Не смотря на все достоинства утилит, входящих в WinAVR, они имеют один весьма существенный недостаток: не поддерживают отладку непосредственно в IDE, и слабо интегрируются с аппаратными средствами разработки и отладки, называемых Starter Kit (стартовые комплекты). AVR Studio прекрасно работает со всеми аппаратными средствами Atmel, но поставляется лишь с поддержкой ассемблера. Объединение достоинств обоих пакетов позволяет достичь желаемого результата: разработка и интерактивная отладка с поддержкой всех средств Atmel на языке высокого уровня Си. Инсталляция Процесс инсталляции хотя и прост, но сопровождается большим количеством англоязычных сообщений, что вызывает затруднения у русскоязычных пользователей. По этой причине далее при всяком удобном случае будут приведены скриншоты окон, выводимых программами, с соответствующими пояснениями. Установка AVR Studio Установка AVR Studio осуществляется запуском соответствующего программного файла-инсталлятора16 – файл AVRStudio4.18SP2.exe [2], который в режиме мастера (Wizard) выполнит все необходимые действия, пользователю остается лишь внимательно следить за выводимыми сообщениями и давать необходимые ответы на задаваемые вопросы. Сразу после запуска инсталлятора вы видите окно следующего вида: Это просто экран приветствия, следует нажать кнопку «Next» для продолжения. Радиолюбитель – 04/2010 Книга по работе с WinAVR и AVR Studio Следующий экран такой: Традиционное лицензионное соглашение – необходимо отметить вариант «I accept…» (я принимаю соглашение) и нажать «Next» для продолжения. Теперь предлагается выбрать папку для установки программы. Рекомендуется избегать папок, в именах которых встречаются русские символы. Папка по умолчанию, как правило, удовлетворяет этим требованиям. Далее снова следует нажать «Next». На этом экране приводятся сведения об элементах, установка которых не обязательна. В приведенном примере предлагается установить USB драйвер для аппаратных средств отладки и программирования микроконтроллеров ICE40, ICE50, AVRISP mkll, JTAGICE и STK600. Если вы не имеете указанных аппаратных средств – можно отказаться от установки драйвера, тем более что при необходимости всегда можно «доустановить» его. Для некоторых опциональных компонентов может активироваться кнопка «Change», при помощи которой можно изменить состав выбранной компоненты (в рассматриваемой версии таких компонентов нет). 16 Следующий экран уведомляет, что все готово для начала инсталляции, которая начнется после нажатия кнопки «Next»: После нажатия «Next» начинается собственно установка, т.е. распаковка и копирование файлов IDE AVR Studio в выбранную папку. Процесс довольно долгий, сопровождается «индикацией»: Если решено отменить инсталляцию – можно нажать кнопку «Cancel», однако работоспособность программы не будет обеспечена. После завершения процесса выводится последнее окно следующего вида: Здесь сообщается об успешной установке AVR Studio, следует нажать кнопку «Finish» для завершения работы мастера установки. После этого в меню «Пуск» Windows появится пункт для запуска AVR Studio – программа установлена, и можно начинать работу с нею. Но пока что возможности работы с Си нет – AVR Studio поставляется только с двумя версиями компиляторов ассемблера. Если у вас есть желание освоить Си (а оно должно быть, если вы читаете эту книгу), следует приступить к инсталляции WinAVR. На момент верстки статьи последняя версия была датирована февралем 2010 г. 57 СПРАВОЧНЫЙ МАТЕРИАЛ Установка WinAVR Инсталлятор WinAVR для Windows последней (на момент верстки статьи) версии можно скачать по ссылке [3] . Файл инсталлятора имеет имя вида WinAVR-yyyymmddinstall.exe, где yyyy – год, mm – месяц и dd – день релиза. Инсталляция происходит немного приятнее, так как инсталлятор русифицирован. В начале предлагается выбрать язык, на котором инсталлятор будет с вами общаться: Рекомендуется выбрать «Russian» русский (если нет иных предпочтений, разумеется) и нажать «ОК». Дальнейшая процедура уже существенно более понятна: В следующем окне предлагается выбрать компоненты для установки. Единственное, без чего вы вполне можете обойтись – это редактор Programmers Notepad, остальные опции рекомендуется оставить включенными. После нажатия кнопки «Установить» начинается весьма долгая процедура инсталляции, сопровождаемая индикатором: Вариантов нет – следует дождаться завершения процесса инсталляции. Завершится инсталляция нажатием в появившемся окне кнопки «Готово». Сразу после этого откроется файл со сведениями о релизе – он на английском, можно смело его закрывать. На этом подготовительные операции следует считать завершенными – теперь все готово для работы. После окна приветствия предлагается согласиться с условиями лицензии – следует согласиться, разумеется. Затем предлагается указать папку для инсталляции. Обязательно следует выбрать папку, не содержащую в своем пути русских символов и пробелов17 ! Рекомендуется сократить предлагаемое по умолчанию имя до «C:\WinAVR». Работа Мастер проектов Запустите только что установленную AVR Studio, и на вашем дисплее появится интегрированная среда разработки программ от Atmel. При первом запуске сразу же появится окно мастера проектов: На рисунке показан вид мастера с некоторым числом проектов, с которыми уже велась работа – в вашем случае при первом запуске список проектов будет, конечно, пуст. Кнопка «New Project» в верхней части позволит создать новый проект, а расположенная рядом кнопка «Open» – открыть любой имеющийся. Если список предыдущих проектов (Recent projects) не пуст – можно выбрать любой из них и продолжить работу над ним, нажав кнопку «Load» (загрузить). Кнопка «Next >>» позволит изменить параметры выбранного проекта. Но в первый раз список предыдущих проектов будет пуст, и ничего иного, кроме как создать новый проект или открыть существующий, сделать не получится, разве что завершить работу мастера проектов кнопкой «Cancel». Мастер проектов автоматически стартует при каждом запуске AVR Studio. Если такое его поведение вас не устраивает – деактивируйте опцию «Show dialog at startup» (показать при запуске) – в следующий раз мастер не запустится автоматически. Нажмем «New Project», чтобы пройти все этапы создания проекта с нуля. На этом этапе мастер проектов предлагает выбрать тип проекта (Project type) - будет ли это программа на ассемблере (Atmel AVR Assembler), или же на Си (AVR GCC) 18 . Проекты на ассемблере нас не интересуют, поэтому работу мастера в этом режиме мы рассматривать не станем, а сразу выберем AVR GCC. Кроме этого следует указать имя проекта (Project name) – это будет особый файл, в котором будут сохранены все последующие настройки нашего проекта. Если отмечены опции «Create initial file» (создать файл главного модуля) и «Create folder» (создать папку проекта) – по мере вода имени проекта автоматически будут заполняться поля «Initial file» (файл главного модуля) и «Location» (местоположение). Расширения для файлов указывать не надо – они присваиваются автоматически. Разумеется, назначенные мастером имена файлов и папок вы всегда можете изменить вручную. Рекомендуется все же придерживаться следующего правила: имя папки для проекта должно совпадать с именем самого проекта так будет меньше путаницы в дальнейшей работе. Так же рекомендуется всегда размещать каждый новый проект в отдельной папке, а для всех проектов вообще выделить другую, «корневую» папку. 17 В последних версиях WinAVR жесткость этого требования уменьшена – пробелы допустимы, хотя если есть намерение пользоваться утилитами комплекта WinAVR для командной строки – следует его соблюдать. 18 Как уже было сказано, WinAVR – это всего лишь «сборка» утилит для разработки программ на Си, в которую включен мультиплатформенный компилятор AVR GCC. 58 Радиолюбитель – 04/2010 СПРАВОЧНЫЙ МАТЕРИАЛ После того, как имя проекта задано – активируется кнопка «Next >>», позволяя перейти к следующей странице мастера: Здесь предлагается выбрать отладочную платформу19 и используемый микроконтроллер (Select debug platform and device). В окне слева перечислены все доступные платформы для отладки (Debug platform), все, кроме тех, что содержат в своем названии слово «Simulator», требуют наличия аппаратных средств от Atmel. Если вы имеете одно из них – следует выбрать из списка его, а если нет – придется довольствоваться эмуляцией отладки, для чего следует выбрать AVR Simulator. Для некоторых платформ могут появляться дополнительные опции для их настройки. В этом случае активируется опция «Open platform option» (открыть настройки платформы), при активации которой вам будет предложено настроить специфические для выбранной платформы параметры после завершения работы мастера. Для каждой платформы эти параметры могут быть различны, и об этом следует читать в сопроводительной документации. В зависимости от выбранной платформы меняется содержимое окна выбора микроконтроллера (Device) – те модели, которые не поддерживаются выбранной системой, становятся недоступными для выбора (затеняются). Эмулятор (AVR Simulator)20 поддерживает практически все существующие микроконтроллеры, поэтому для начала следует пользоваться им. Выбрав платформу и модель микроконтроллера, вы можете завершить работу мастера нажатием кнопки «Finish», в результате чего будут сгенерированы и открыты необходимые файлы. На рисунке показан выбор модели Atmega8 – все дальнейшее рассмотрение среды AVR Studio будет сопровождаться скриншотами именно для этого случая (для других моделей могут быть незначительные отличия, связанные с различием в архитектуре микроконтроллеров). Рабочее пространство Когда работа мастера проектов завершена, внешний вид рабочего пространства AVR Studio становится таким: 19 Об отладке будет сказано в следующих главах. Более «продвинутый» AVR Simulator 2 по заявлению Atmel реализует новые (более адекватные реальности) алгоритмы моделирования периферии микроконтроллеров, однако на момент написания книги поддерживаел очень ограниченный ассортимент микроконтроллеров, что не позволяет рекомендовать его как основное средство эмуляции. 20 Радиолюбитель – 04/2010 Среда разработки – многооконная, т.е. все доступное пространство разделено на несколько областей-окон изменяемых размеров, каждая из которых предназначена для просмотра различной информации (на рисунке помечены номерами). В процессе работы число и расположение этих областей может изменяться в зависимости от текущего режима IDE, так же можно самостоятельно настроить рабочее пространство по своему усмотрению. В данной главе представлен краткий обзор всех областей и общих для всех особенностей управления рабочим пространством, далее каждая область будет рассмотрена отдельно более подробно. Центральная область c содержит окно редактирования исходного текста. В этой области каждый файл может либо быть в отдельном окне (как на рисунке), либо может заполнять всю область целиком (если нажать кнопку «Развернуть»). В нижней части d центральной области размещаются закладки-ярлычки для быстрого переключения между открытыми файлами. Область e – это окно проекта, в котором в виде древовидной структуры показано содержимое всего проекта. Корнем дерева является файл проекта «demo1», ветви дерева – это группы файлов, составляющих проект: · Source Files – исходные тексты модулей программы. Сюда входят все файлы с исходными текстами модулей, составляющих проект. · Header Files – заголовочные файлы, подключаемые пользователем. · External Dependencies – внешние зависимости, т.е. файлы, от которых зависят прочие файлы проекта. В этой группе обычно автоматически размещаются заголовочные файлы, подключенные внутри других заголовочных файлов, т.е. неявно задействованные в проекте. · Other Files – прочие файлы. В этой группе помещаются файлы, генерируемые компилятором в процессе работы, т.е. выходные файлы, а так же некоторые вспомогательные файлы, например, справочные сведения или документация. Область f – окно периферийных устройств микроконтроллера. Оно имеет название «I/O View» - просмотр ввода-вывода, т.к. любое периферийное устройство для программиста представлено в виде набора портов ввода-вывода (или регистров ввода-вывода). В этом окне в виде раскрывающихся списков перечислены все аппаратные устройства выбранного микроконтроллера – таймеры, порты, АЦП и т.п. Во время написания исходного текста программы это окно поможет ориентироваться в назначении битов тех или иных регистров управления периферией, но гораздо более важную роль оно играет при отладке. Окно g – это многофункциональное окно вспомогательной информации. По умолчанию оно включает 4 закладки, каждая из которых отвечает за свои сведения: · Build – информация о результатах работы компилятора, здесь выводятся сообщения о ходе компиляции программы. · Message – сообщения. В этой области можно видеть все сообщения IDE: от сведений о загрузке модулей программы до сообщений об ошибках компилятора. · Find in Files – область результатов поиска по файлам. · Breakpoints and Tracepoints – точки останова и трассировки. В этой области перечислены все точки остановки исполнения и трассировки программы, которые программист установил для отладки. Наконец, область h – это традиционная область панелей кнопок, которые облегчают выполнение многих команд. Выше находится строка меню. Все рассмотренные области, кроме c, могут менять свое расположение, размеры и даже отображаться в виде плавающих отдельных окошек. Внешний вид и расположение областей можно изменить при помощи вспомогательного меню, которое открывается при нажатии на кнопочку с треугольником в заголовке области (см. рисунок). Это вспомогательное меню содержит следующие пункты: · Floating – сделать область «плавающей» · Docking – «прилепить» область к одному из краев рабочего пространства · Tabbed Document – оформить область в виде закладки другой области · Auto Hide – прятать автоматически, т.е. в режимах, когда область не требуется, она будет автоматически убрана с экрана · Hide – спрятать, т.е. убрать область с экрана. Эта команда равносильна нажатию на кнопочку с крестиком в заголовке. Изменить местоположение области можно и более простым способом – просто ухватив ее за заголовок и потащив: рабочая область сразу приобретет примерно следующий вид (см. следующую стр.): 59 СПРАВОЧНЫЙ МАТЕРИАЛ На рисунке показан момент, когда область проекта «перетаскивается» в центр рабочего пространства, причем курсор попадает в крестообразную область-«маркер» (которая появляется в момент начала перетаскивания). Кроме крестовидного маркера в центре, появляются еще 4 квадратных маркера по краям рабочего пространства. Все это должно способствовать по замыслу разработчиков AVR Studio упрощению процесса размещения перетаскиваемой области в нужном месте. Когда при перетаскивании области курсор попадает в один из этих появившихся «маркеров», на рабочем пространстве голубым цветом подсвечивается зона, в которую «прилипнет» перетаскиваемое окно. К сожалению, описать словами этот процесс непросто, гораздо интереснее попробовать потаскать окошки самостоятельно, наблюдая за возникающими эффектами и достигаемыми результатами. В процессе перетаскивания окон обращайте внимание на подсвечиваемые голубым области – они могут показывать и вариант, когда окно станет новой закладкой в уже имеющемся окне. Если бросить окно вне одного из «маркеров» – окно останется плавающим. Если область «закрыть» – ничего страшного, при помощи команд главного меню любую область всегда можно снова вывести на дисплей (см. далее). Главное меню Рассмотрим кратко содержание главного меню AVR Studio, останавливаясь более подробно на заслуживающих того пунктах. Часть пунктов меню более подробно будут раскрыты далее, при описании соответствующих режимов работы. Для многих команд в меню назначены вспомогательные изображения. Эти же изображения могу находиться и на кнопках, размещенных в области кнопок, т.е. легко сориентироваться в командах: одинаковые изображения – одинаковые команды. В дальнейшем отдельного описания кнопок не будет. File - Файл Традиционное меню, содержит традиционные команды: · New File – Новый файл. Создает новый пустой текстовый документ 60 для редактирования. Тип файла задается при сохранении. · Open File – Открыть файл для редактирования. · Close – закрыть текущий редактируемый файл. · Save – сохранить текущий редактируемый файл · Save As – сохранить файл под другим именем · Save All – сохранить все открытые файлы · Print – напечатать текущий редактируемый файл · Print Setup – настроить режим печати · Exit – завершение работы с AVR Studio Project - управление проектами Если вы отказались от автоматического запуска при старте мастера проектов – это меню поможет вам выполнить все необходимые операции, как с мастером, так и без него. · Project Wizard – мастер проекта. Команда запускает рассмотренный ранее мастер проектов. · New Project – создание нового проекта. В сущности, команда запускает мастер проектов со второй его страницы, т.е. с момента ввода имен файлов. · Open Project – открыть проект, ранее сохраненный на диске. · Save Project – сохранить проект. Обратите внимание, что эта команда не сохраняет редактируемые файлы, составляющие проект – она сохраняет различные настройки проекта. · Close Project – закрыть текущий проект. Работа возможна лишь с единственным проектом, поэтому перед тем, как начать новый, следует закрыть текущий. Обычно автоматическое закрытие проекта происходит и без вызова этой команды – при открытии или создании нового. · Recent Projects – предыдущие проекты. Эта команда открывает список нескольких проектов, с которыми вы работали последнее время, позволяя вернуться к одному из них. По действию аналогична выбору предыдущего проекта на первой странице мастера проектов, но «помнит» не более 10 проектов (в порядке увеличения даты). · Configuration Options – параметры конфигурации проекта. Важная команда, позволяющая произвести тонкую настройку режима компиляции. Подробно рассматривается далее. Build – сборка проекта Подробно рассматривается далее. Edit – правка Меню содержит ряд традиционных команд, а также несколько дополнительных: · Undo – отменить изменение. · Redo – вернуть то, что было отменено командой Undo. · Cut, C o p y , Paste – стандартные команды вырезать, копировать и вставить. · Toggle Bookmark – закладка. Команда позволяет сделать закладку на строке, в которой сейчас находится курсор. Подробно работа с закладками рассматривается в главе «Редактирование исходных текстов». · Remove Bookmarks – удалить закладки. · Find – найти. Выполняет поиск текста в текущем редактируемом файле. · Find in Files – найти в файлах. Выполняет поиск текста по нескольким файлам, помещая результаты поиска в отдельное окно. Дополнительно о поиске упоминается в главе «Редактирование исходных текстов». · Replace – найти и заменить. Выполняет автоматический поиск и замену. · Next Error – перемещает курсор в строку, содержащую очередную ошибку. · Show Whitespace – показать пробелы. Команда позволяет обозначить точкой все пробелы в тексте программы. · Font and Color – команда позволяет настроить внешний вид редактора текста. Подробно рассмотрена в главе «Подсветка синтаксиса». View – вид Управляет видимостью вспомогательных областей-окон рабочего пространства. Большинство этих областей необходимы при отладке программ, и рассматриваются подробно далее, здесь же только кратко перечислены команды: · Toolbars – инструментальные панели. Открывает дополнительное меню (см. далее), позволяющее оперативно управлять видимостью различных панелей кнопок и т.п. · Status Bar – панель состояния. Команда управляет видимостью традиционного элемента окон программ Windows – нижняя строка состояния. · Disassembler – окно дизассемблированного кода программы. · Watch – окно просмотра переменных · Memory, Memory 2 и Memory 3 – три окна просмотра содержимого памяти микроконтроллера · Register – окно просмотра содержимого регистров микроконтроллера. Радиолюбитель – 04/2010 СПРАВОЧНЫЙ МАТЕРИАЛ П о д м е н ю Toolbars содержит следующие опции: · Standard Toolbar – панель стандартных кнопок · Edit – панель кнопок, соответствующих командам меню Edit · Debug – панель кнопок, соответствующая командам меню Debug · Debug Windows – окна с отладочной информацией · MDI Tabs – закладки многодокументного интерфейса. Если эта опция отмечена, то в области редактирования файлов будут присутствовать закладки быстрого переключения между файлами. Если опция не активна – закладки не будут видны. · AVRGCCPLUGIN и AVR GCC – опции, управляющие видимостью панелей и окон, внедренных в AVR Studio модулями WinAVR. Настоятельно рекомендуется не отключать эти опции. · STK500 – управление панелями аппаратного комплекса отладки STK500. Эта опция может отсутствовать, если STK500 не поддерживается. · TraceBar – панель трассировки · I/O – панель периферии · Processor – панель состояния ядра процессора · Build Output – панель (закладка) вывода результатов компиляции · Message Output – панель (закладка) вывода сообщений · Find Output – панель (закладка) вывода результатов поиска текста в файлах · Breakpoints and Tracepoints – панель (закладка) со списком точек остановки или трассировки Tools – инструменты Содержит ряд команд для работы с различными дополнительными утилитами или аппаратными средствами, поддерживаемыми AVR Studio. Большинство команд этого меню предназначено для работы с аппаратными средствами программирования и отладки микроконтроллеров, и, в случае отсутствия этих самых аппаратных средств, не функционируют. Реальный интерес представляют только следующие команды: · Customize – настройка по своему вкусу внешнего вида IDE, подробно рассматривается далее. · Options – параметры. Команда позволяет настроить параметры работы AVR Studio. Подробно рассматривается далее. · Show Key Assignments – настройка «горячих клавиш». Большинству команд меню уже назначены горячие клавиши, что хорошо видно на соответствующих рисунках. Вы можете самостоятельно настроить соответствие клавиш так, как вам удобно. · Plug-in Manager – менеджер плагинов. Плагин – это дополнительный модуль, расширяющий функциональность AVR Studio. Обычно плагины поставляются сторонними разработчиками аппаратного обеспечения и т.п. Например, поддержка WinAVR реализуется при помощи соответствующего плагина. Данная команда позволяет подключить или отключить имеющиеся плагины, изменив тем самым функционирование IDE. · Program AVR – программирование микроконтроллера AVR. Команда позволяет, не выходя из AVR Studio, выполнить программирование, т.е. «прошивку» микроконтроллера результатами компиляции программы. Для этого необходимо лишь наличие программатора, поддерживаемого AVR Studio. Debug – отладка Отладка в AVR Studio будет рассмотрена более детально в следующих главах, а так же, по мере возможности, при рассмотрении примеров. Windows – окна Стандартное меню для всех Windowsпрограмм. · Workspace – открывает дополнительное меню управления рабочим пространством (см. далее) · Split – разделить. Позволяет разделить текущее окно редактирования файла на 2 или 4 части, чтобы иметь возможность одновременно наблюдать участки текста, находящиеся на большом удалении друг от друга (например, начало и конец большого файла). Режим актуален для работы с дисплеями высокого разрешения и большого размера. · Cascade – расположить окна каскадом · Tile Horizontally и Tile Vertically – расположить окна мозаикой по горизонтали или вертикали · Arrange Icons – упорядочить значки свернутых окон · Windows – выбор текущего окна из списка (если открыто очень много файлов) Дополнительно в этом меню добавляются пункты, соответствующие всем открытым для редактирования файлам. Следует отметить, что работа с окнами становится неактуальной благодаря системе закладок – все окна распахиваются (максимизируются), а переключение между ними осуществляется при помощи ярлычков. П у н к т Workspace открывает следующие команды: · Save Workspace – сохранить рабочее пространство · Delete Workspace – удалить рабочее пространство · Default – включить рабочее пространство по умолчанию Эти команды позволяют запомнить расположение всех областей, чтобы потом воспользоваться им одной командой. Help – помощь Меню традиционное, но содержит ряд особых команд, сильно облегчающих работу с AVR Studio (если вы хорошо владеете английским языком). · AVR Tools User Guide – руководство пользователя по аппаратным устройствам фирмы Atmel. Позволяет ознакомиться с предлагаемым Atmel ассортиментом аппаратных средств поддержки разработчика, причем руководство весьма полное, вплоть до принципиальных схем некоторых устройств. · AVR Studio User Guide – англоязычный справочный файл по работе с AVR Studio. Надеюсь, читателям этой книги этот пункт меню не потребуется. · Check for Program Upgrade – проверить, не вышла ли более новая версия AVR Studio, доступная для загрузки с сайта Atmel. · Release Notes and Known Issues – открывает документ, в котором перечислены особенности и «недоработки» текущей версии AVR Studio. Для рассматриваемой версии 4.14 содержимое этого документа приведено в переводе в приложениях. · AVR GCC Plug-in Help – справка по настройке плагина WinAVR (все аспекты настройки рассматриваются в этой книге). · avr-libc Reference Manual – открывает справочник по стандартным функциям библиотеки avr-libc. Полноценное руководство по этим функциям имеется в этой книге (см. раздел «AVR-LIBC»). · About AVR Studio – выводит сведения о версии AVR Studio Продолжение в №5/2010 Ресурсы 2 http://www.atmel.com/dyn/resources/prod_documents/AVRStudio4.18SP2.exe 3. http://sourceforge.net/projects/winavr/files/WinAVR/20100110/WinAVR-20100110-install.exe/download Радиолюбитель – 04/2010 61 СПРАВОЧНЫЙ МАТЕРИАЛ Роман Абраш г. Новочеркасск E-mail: arv@radioliga.com Продолжение. Начало в №1-4/2010 Окно проекта Рассмотрим подробнее возможности, предоставляемые окном проекта. Если нажать правую кнопку мышки в этом окне, то появится всплывающее меню, содержимое которого зависит от того, над каким элементом окна произошло нажатие. При щелчке над корневым пунктом, т.е. над именем проекта, появляется следующее меню: Часть команд этого меню совпадает с командами главного меню File, Projects или Build, часть – уникальные команды. Например, команда Properties (свойства) позволяет узнать некоторые вспомогательные свойства проекта, выводя окно следующего вида: В этом окне приведены следующие сведения: Project File – полное имя файла проекта Project Directory – полный путь к папке проекта Last Saved – дата и время последнего сохранения проекта Active Configuration – имя текущей конфигурации Options Changed Since Last Build – показывает Yes, если с момента последней компиляции проекта были изменены настройки компилятора, если такого не было – показывает No. Output File – имя объектного файла, генерируемого компилятором при сборке проекта Last Build – дата и время последней полной перекомпиляции проекта (Unknown означает, что проект еще ни разу не компилировался) Output File Size – размер выходного файла Target Part – выбранная модель микроконтроллера 58 Книга по работе с WinAVR и AVR Studio Команда Add Existing File(s) (добавить существующие файлы) позволяет добавить к проекту уже созданный файл, имеющийся на диске. В зависимости от расширения файла он будет добавлен в соответствующую ветку дерева проекта. Команда Show File Paths (показать пути файлов) позволяет включить показ полных путей файлов в дереве проекта. Если осуществить щелчок правой кнопкой мышки над именем файла исходного текста (Source Files) в дереве проекта, всплывающее меню будет иного вида: Часть команд так же дублирует уже рассмотренные (или рассматриваемые в соответствующих разделах далее), но есть и новые: Remove File from Project позволяет удалить файл из состава проекта (сам файл с диска не удаляется). Rename File позволяет переименовать файл, а Delete File – удалить файл с диска. Наиболее важная команда – Edit Configuration Options (изменить параметры конфигурации проекта). Эта команда выполняет те же самые действия, что и команда Configuration Options в главном меню Project. Подробно рассматривается далее. Окно проекта служит не просто для перечисления файлов, входящих в проект. Выполнив двойной щелчок на любом файле в окне проекта, можно автоматически открыть его в редакторе, при этом нет нужды искать папку, где этот файл действительно хранится – все делается автоматически. Окно периферии Окно периферии – это весьма гибко настраиваемое окно отображения ресурсов встроенных периферийных устройств микроконтроллера. По умолчанию оно разделено на две части: в верхней перечислены наименования периферийных устройств, а в нижней – состояние регистров одного выбранного устройства. На рисунке показан вид окна, когда выбран аналоговый компаратор. Выбрать периферийное устройство можно или щелкнув на нем в окне, или выбрав из выпадающего списка в верхней части окна. В нижней части показаны регистры (столбец Name), управляющие аналоговым компаратором, с указанием адреса (столбец Address) регистра (без скобок указан адрес в области портов ввода-вывода, соответствующий регистру, а в скобках – адрес в пространстве ОЗУ). Кроме того, в столбце Value (значение) может быть показано текущее значение числа, хранимого в регистре (во время отладки), а в столбце Bits (биты) графически показаны состояния отдельных битов порта – черными квадратиками показаны единичные значения битов, пустыми (белыми) – нулевые, серым цветом показаны биты, не участвующие в управлении выбранным периферийным устройством. Толстая разделительная линия между верхней и нижней частями позволяет изменить соотношение размеров соответствующих областей. В верхней части панели находится несколько кнопок, которые позволяют изменить внешний вид содержимого окна. Самая левая вызывает появление списка вариантов внешнего вида: Module Split View (режим раздельного отображения) – это как раз только что рассмотренный вид. Flat Register View (вид плоских регистров) видоизменяет отображение панели следующим образом: То есть все регистры управления периферией указаны в порядке возрастания их адресов. При наведении курсора на наименование регистра появляется всплывающая подсказка о его назначении (это хорошо видно на рисунке). Tree View (древовидный режим отображения) представляется наиболее удобным: Радиолюбитель – 05/2010 СПРАВОЧНЫЙ МАТЕРИАЛ В этом режиме можно наблюдать одновременно несколько регистров управления разными периферийными устройствами, сворачивая и разворачивая нужные ветви по необходимости. Другие кнопки позволяют видоизменить отдельные нюансы отображения, причем для некоторых режимов одни могут быть недоступны. В окне периферии так же имеется всплывающее по нажатию правой кнопки мыши контекстное меню. В зависимости от места щелчка содержимое меню может немного отличаться, но в основном оно содержит следующие команды: Select All – выделить все. Позволяет выделить сразу все регистры в списке, чтобы потом одновременно воздействовать другими командами на них. Hexadecimal Display – шестнадцатеричный формат чисел. Если опция отключена – все числовые значения в окне (или для выбранного регистра) будут выводиться в десятичном формате. Expand Groups и Collapse Groups – развернуть или свернуть группу. Эта команда равносильна нажатию на кнопочке с плюсиком левее наименования регистра, т.е. раскрывает или скрывает подпункты в описании регистра или периферийного устройства. Export – позволяет экспортировать (т.е. сохранить) в виде текстового файла выбранные регистры. Show Tooltip – показывать всплывающие подсказки. Show Bitnumber – показывать номера битов. Если опция включена, то внутри квадратиков битового представления регистра будут указываться номера соответствующих битов. Font – позволяет задать шрифт для отображения содержимого окна. Default Font – сбрасывает шрифт к назначенному по умолчанию Reset Columns – сброс колонок. Команда позволяет сбросить режим отображения колонок к состоянию по умолчанию. Clear – очистить содержимое регистра Print – вывод содержимого окна на печать Help – справка об окне Columns – колонки (столбцы). Команда позволяет указать, какие колонки надо показывать в окне: Name – имя, наименование. Колонка, в которой показано название периферийного устройства. Address – адрес регистра. Value – значение регистра Bits – битовое представление содержимого регистра. Радиолюбитель – 05/2010 Примечательно, что ширину колонок можно менять, перетаскивая границы в заголовке колонок. Настройка интерфейса IDE Настройка IDE заключается в придании различным элементам интерфейса программы желаемого внешнего вида, а так же указания некоторых особенностей работы. Например, можно изменить пункты меню, горячие клавиши, выбрать шрифт для редактора, изображения для кнопок и т.п. Рассмотрим основные возможности настройки, предоставляемые командой меню «Tools» Customize, которая вызывает на дисплей следующее диалоговое окно: В окне «Toolbars» перечислены все панели кнопок, добавленные в интерфейс различными модулями программы, в том числе плагинами. Отмеченные галочкой панели – видны, не отмеченные – не видны. Кнопка Reset (сброс) позволяет сбросить к исходному состояние панели кнопок указанного модуля или плагина (например, если вы случайно удалили кнопку). Reset All (сбросить все) возвращает состояние всех панелей к исходному. При сбросе панелей требуется подтвердить свое намерение, для чего выводится окно с вопросом: «Все ваши изменения настроек будут потеряны! Вы действительно желаете осуществить сброс панели?». В этом окне 6 закладок: Commands – настройка панелей кнопок Toolbars – управление инструментальными панелями Tools – инструменты пользователя Keyboard – клавиатурные сочетания (горячие клавиши) Menu – настройка контекстного меню Options – прочие параметры Настройка панелей кнопок На закладке Commands диалога Customize имеется два окна: слева Categories (категории), справа Commands (команды). В левом окне перечислены все группы команд, которые AVR Studio в состоянии воспринимать, а справа раскрыто содержимое выбранной группы. На рисунке, например, показан набор возможных команд для категории «File» (работа с файлами). Чтобы добавить кнопку на панель кнопок, надо схватить в окне «Commands» соответствующий элемент и перетащить его на нужную панель кнопок. При выборе команды в нижней части диалога выводится краткая подсказка-описание этой команды (на английском). Чтобы удалить кнопку с панели, надо схватить ее и «сбросить» в любом месте, отличном от панели кнопок. Следует соблюдать осторожность с настройкой кнопок по своему усмотрению, т.к. порой непросто найти и вернуть кнопку, удаленную случайно. Состав панелей кнопок зависит от разных условий, например, от того, какие плагины подключены к AVR Studio. В нашем случае указаны следующие панели: AVRGCCPLUGIN – панель WinAVR Debug – панель кнопок для отладки программы Debug Windows – панель управления окнами отладки Edit – панель команд редактирования Menu Bar – главное меню (невозможно скрыть) ms – главная панель кнопок (файловые операции и т.п.) STK500 – панель работы с комплектом разработчика STK500 Tools – панель инструментов Trace – панель трассировки Инструменты пользователя При переходе на закладку «Tools» окно Customize приобретает такой вид: Управление инструментальными панелями При переходе на закладку Toolbars окно диалога Customize приобретает следующий вид (см. рисунок): 59 СПРАВОЧНЫЙ МАТЕРИАЛ Это предназначается для добавления пользователем собственных команд в главное меню (и, при желании – в виде кнопок на любую панель). Собственные команды предназначаются для запуска других программ, поэтому закладка Tools содержит: · Список имеющихся пользовательских команд – Menu contents · Кнопки для изменения порядка команд пользователя (вверху справа) · Поле указания имени запускаемого файла – Command · Поле списка параметров для запускаемого файла – Arguments · Рабочая директория для запускаемого файла – Initial Directory. В качестве запускаемого файла можно указать любой exe-файл или ярлык, указать Интернет-адрес или ввести любую иную системную команду. Если требуется, надо указать и список параметров для запуска. Рабочая директория по умолчанию используется та, что указана в качестве директории проекта (см. «Мастер проектов»). Добавление пользовательской команды начинается с нажатия кнопки (добавить). В окне Menu Contents сразу появляется строка для ввода наименования команды, которое будет видно в главном меню Tools. Удалить пользовательскую команду можно кнопкой (удалить), а изменить порядок (если команд несколько) – кнопками (выше) и (ниже). 6. Если введенная комбинация не назначена ни одной команде – в нижней части окна появится надпись Unassigned (не назначена), в противном случае будет выведено наименование команды, для которой введенная комбинация уже назначена. 7. Если введенная комбинация вас устраивает – нажмите кнопку Assign (назначить). После этого для соответствующей команды можно будет использовать введенную вами комбинацию. Обратите внимание, что можно назначить несколько комбинаций одной и той же команде! Если нужно удалить комбинацию клавиш, надо выполнить пункты с первого по 4 включительно, выбрать нужную комбинацию в окне Current Keys и нажать кнопку «Remove» (удалить). Кнопка Reset All (сбросить все) приведет состояние горячих клавиш для всех команд к исходному по умолчанию. Для выбора групп настроек используется окно слева, содержащее группы General (основные), Breakpoints (точки останова), Workspace (рабочее пространство) и Editor (редактор). Как это ни странно, но разработчики AVR Studio допустили раздвоение настроек из одной группы в разных командах меню и диалогах настройки, однако, как бы там ни было, рассмотрим варианты настроек этого окна. Назначение этой закладки – настройка всплывающего меню для области редактора текста21 . Прочие параметры При переходе на последнюю закладку «Options» диалога Customize окно приобретает следующий вид: 60 Настройка режимов AVR Studio Общие настройки AVR Studio осуществляются командой меню «Tools» Options, в результате чего открывается следующее окно: Настройка контекстного меню Закладка Menu диалога Customize имеет следующий вид: Клавиатурные сочетания (горячие клавиши) Закладка «Keyboard» имеет следующий вид: Последовательность изменения или назначения комбинации горячих клавиш команде следующая: 1. Выбирается категория команды в списке Category (аналогично закладке Commands) 2. Выбирается команда в категории из списка Commands (команды) 3. Выбирается вариант конфигурации из списка Set Accelerator for (установить горячую клавишу для конфигурации) 4. В окне Current Keys (текущая комбинация) можно увидеть текущее значение горячей комбинации клавиш для выбранной команды. 5. В окне Press New Shortcut Key (нажмите новую комбинацию клавиш) появится комбинация, которую вы нажмете (обязательно в сочетании с Ctrl и(или) Alt). · Включить или отключить вывод в подсказках горячих клавиш, назначенных кнопке – опция Show shortcut keys in Screen Tips · Включить или отключить большие изображения для кнопок – Large Icons Кнопка Visualizations предусмотрена разработчиками на перспективу – хотя при нажатии на нее открывается дополнительное окно, изменить с его помощью какиелибо настройки невозможно. На этой закладке можно изменить следующие опции: · Включить или отключить показ всплывающих подсказок при наведении курсора на кнопки команд – опция Show Screen Tips on toolbars 21 По неизвестным причинам, настройки, осуществляемые на данной закладке, никак не проявляются в работе. Возможно, это особенность текущей версии AVR Studio, т.е. возможность, заложенная на перспективу. General – Основные настройки К основным отнесены следующие опции: Hide Startup Wizard – скрыть мастер проектов при запуске. Опция позволяет отказаться от автоматического запуска мастера проектов при старте AVR Studio. Save project automatically on exit – сохранять проект автоматически при выходе. Активация этой опции позволит избежать потери информации при завершении работы AVR Studio. Auto open last project – автоматически загрузить последний проект. Если опция активна, то при старте AVR Studio будет автоматически загружен проект, с которым осуществлялась работа в последнем сеансе. Show current sourcecode – показывать текущую строку исходного текста Reset Desktop on restart – сбросить настройки рабочего пространства при перезапуске Step over when Autostepping – не входить в функции при пошаговом автовыполнении. Enable Splash Screen at Startup – показывать «заставку» при старте программы. Если опция активна, то при каждом запуске AVR Studio появляется окно-заставка следующего вида (см. рисунок на следующей странице): Радиолюбитель – 05/2010 СПРАВОЧНЫЙ МАТЕРИАЛ Filetabs (ярлыки файлов) – список вариантов оформления ярлыков-закладок для переключения между открытыми файлами. Имеются следующие варианты: Full path (полный путь), Filename only (только имя файла) и Full path outside projectfolder (полный путь, если файл вне папки проекта). Number of COM-ports to try – число СОМ-портов для проверки. Эта опция задает номер СОМ-порта, до которого (начиная с 1-го, т.е. с СОМ1) будут перебираться порты при попытках обнаружить подключенные аппаратные средства. Точки останова При выборе этой группы доступны следующие опции: Stop on breakpoint when Step Out, Step Into or Run to Cursor – останавливаться на точках при различных режимах пошаговой отладки. Stop on breakpoint when autostepping – останавливаться на точках при автопошаговом исполнении программы. Disable data breakpoints while resetting – запрещать точки остановки по изменению данных во время сброса. · Tile Horizontally – расположить окна горизонтальной мозаикой · Tile Vertically – расположить окна вертикальной мозаикой Можно настроить способ размещения окон, когда включается или выключается окно просмотра дизассемблированного текста (When toggling Disassembly window): · Show normal – показать обычным способом · Tile Horizontally – расположить окна горизонтальной мозаикой · Tile Vertically – расположить окна вертикальной мозаикой Можно изменить стиль оформления всего интерфейса программы (Visual Style), причем независимо от установленной ОС можно использовать характерный вид для одной из следующих версий Windows (чтобы изменения вступили в силу, надо перезапустить AVR Studio): · Windows XP · Windows 2005 · Windows default – элементы интерфейса будут отображаться так, как предусмотрено текущей версией Windows на компьютере пользователя. Кроме всего прочего, можно управлять «умным» перетаскиванием панелей и окон – опция Use smart docking. Если эта опция активна, то при перетаскивании панелей будут появляться маркеры и подсвечиваться области «прилипания» (см. предыдущий номер журнала). Наконец, опция Restore desktop position and size when restarting позволит при повторном запуске AVR Studio восстанавливать размер и положение окна программы, которое было в последнем сеансе работы. Редактор Группа настроек редактора включает в себя следующие опции: Рабочее пространство Эта группа опций содержит различные возможности настроек внешнего вида: Можно настроить способ размещения окна с документом при открывании файла (группа вариантов When Opening Files – когда открываются файлы): · Restore Last – восстановить последний вариант · Maximize – развернуть окно документа · Default – оставить по умолчанию Радиолюбитель – 05/2010 Подсветка синтаксиса Подсветка синтаксиса – это возможность редактора выделять ключевые слова языка программирования цветом, начертанием символа и т.п., делая, таким образом, текст программы более наглядным. Настройка этой возможности AVR Studio осуществляется командой меню «Edit» Font and Color. Открывающееся по этой команде окно содержит следующие средства для настройки подсветки синтаксиса: Список распознаваемых лексем и элементов языка. Определены следующие элементы: · Text – любой текст программы, не входящий в остальные группы · Text Selection – выделенная область текста · Number – числовая константа · Operator – математический или логический оператор, знак операции · Comment – комментарий · Keyword – ключевое слово языка · String – строковая константа Для каждого элемента можно задать шрифт (нажав на кнопку Choose Font – изменить шрифт), указать цвет текста и фона (Foreground и Background соответственно). Изменение цвета шрифта возможно, только если неактивны опции Automatic (автоматически) для соответствующего цвета. Шрифт допустим любой моноширинный, т.е. такой, в котором ширина всех символов одинакова. Кнопка Reset All позволит вернуть расцветку выделения синтаксиса к той, что установлена по умолчанию. Font Size – размер шрифта Tabwidth – ширина табуляции (в символах). Дополнительная опция Replace tabs with space позволяет вместо символа табуляции вставлять в текст программы при нажатии кнопки Tab соответствующего количества пробелов. Кнопка Restore Default позволяет вернуть настройки шрифта к заданным по умолчанию. Чтобы сделанные настройки вступили в силу, нужно перезапустить AVR Studio (или, как минимум, закрыть и заново открыть редактируемые файлы). Продолжение в №6/2010 61 СПРАВОЧНЫЙ МАТЕРИАЛ Роман Абраш г. Новочеркасск E-mail: arv@radioliga.com Продолжение. Начало в №1-5/2010 Редактирование исходных текстов AVR Studio для исходных текстов программ реализует достаточно широкий набор функций редактирования. Часть функций стандартна для всех Windows-программ, например, работа с буфером обмена (копирование, вставка и т.п.), есть ряд возможностей, специфичных для работы с исходными текстами программ. По умолчанию некоторые команды редактора доступны только через кнопки панели редактирования, некоторые – через команды меню и(или) горячие клавиши. Если некоторые команды вам удобнее выполнять при помощи горячих клавиш или, наоборот, при помощи мыши (через кнопки), следует перенастроить AVR Studio, как было рассказано в главе «Настройка интерфейса IDE». Список всех команд редактора и соответствующих горячих клавиш по умолчанию приведен в таблице 1. Редактор тесно интегрирован с отладчиком, поэтому содержит ряд команд, необходимых при отладке. Хотя этими командами можно пользоваться и во время ввода и редактирования текста программы, рассматриваются они в главе «Компиляция и отладка». В редакторе можно пользоваться всплывающим по нажатию правой кнопки мыши меню. Оно содержит достаточно много команд, часть из которых доступна только в режиме отладки (и рассматривается в соответствующей главе), другая часть команд доступна всегда, но имеет смысл только для режима отладки. Поиск и замена текста При нажатии Ctrl-F или выполнения команды меню «Edit» Find открывается окно поиска текста в текущем файле: Книга по работе с WinAVR и AVR Studio · Regular expression – введенный текст представляет собой регулярное выражение для поиска Дополнительно указывается направление поиска (Direction) – вверх (Up) или вниз (Down). Нажатие кнопки Find Next приведет к тому, что будет найдено и выделено следующее (по отношению к положению курсора) заданное вхождение искомого текста. Если в процессе поиска будет достигнут конец файла – поиск продолжится с начала, т.е. будет происходить по кругу в указанном направлении. Если заданного текста не найдено – выводится сообщение. Кнопка Mark All приведет к тому, что будут найдены все вхождения указанного текста в файле и каждая строка с этим текстом будет помечена закладкой. Поиск по регулярному выражению – это интересная опция, которая позволяет осуществлять поиск по весьма сложным алгоритмам. О формате регулярных выражений22 можно почитать, например, в [4]. Чтобы немного пролить свет на эту возможность, приведем ряд примеров регулярных выражений, которые можно использовать в своей работе: Кнопка Find Next, как и ранее, позволяет найти и выделить очередное вхождение текста, кнопка Replace – заменить найденный текст другим, а кнопка Replace All – найдет и заменит все вхождения в указанной области. Как и ранее, для поиска можно использовать регулярные выражения. Как было уже упомянуто, имеется возможность поиска текста в нескольких файлах, для чего служит команда меню «Edit» Find in Files. Таблица 1 Сочетание клавиш Действие Перемещение по тексту На символ влево На символ вправо На строку вверх На строку вниз Прокрутка текста на строку вверх Ctrl – Прокрутка текста на строку вниз Ctrl – В конец строки End Нужно найти Регулярное выражение для этого В начало строки Home На страницу вниз PgDn Любой элемент массива, адресуемый целочисленной константой \[\s*\d*\s*\] На страницу вверх PgUp В конец документа Ctrl – End Функцию, имя которой начинается с подчеркивания \b\s*_+\w*\s*\( В начало документа Ctrl – Home Приведенные примеры регулярных выражений достаточно «интеллектуальны». Например, первое выражение позволит найти текст «[1]», «[128]» и «[ 0 ]», но проигнорирует «[a]» или «[i++]». Второе выражение обнаружит функцию «_demo(void)», но проигнорирует «func_demo()». Поиск с заменой осуществляется при помощи команды меню «Edit» Replace. Окно для этой команды похоже на рассмотренное ранее: K началу следующего слова Ctrl – Kначалу предыдущего слова Ctrl – Вставка табуляции Tab Переход к предыдущей позиции табуляции Shift –Tab Операции с закладками Включить/отключить закладку в текущей строке Ctrl – F2 Переход к следующей закладке F2 Переход к предыдущей закладке Shift – F2 Удалить все закладки Ctrl – Shift – F2 Поиск и замена Поиск Ctrl – F или Alt – F9 Поиск следующего вхождения текста F3 Поиск предыдущего вхождения текста Shift – F3 Выделение и удаление В поле-списке Find what можно ввести (или выбрать из выпадающего списка один из вариантов предыдущего поиска) текст, который следует найти. Если в момент вызова команды поиска курсор находился в каком-либо слове, оно автоматически вводится в это поле. Далее следует задать режимы поиска: · Match whole words only – искать совпадение только целых слов · Match case – искать с учетом регистра символов 54 Небольшое отличие заключается в том, что имеется два поля-списка: первое Find what для поиска текста, второе Replace with – для указания текста, который будет заменять найденный. Кроме того, вместо направления поиска указывается диапазон поиска (Replace In): только внутри выделенного текста (Selection) или во всем файле (Whole file). Выделить все Ctrl – A Удалить слово правее курсора Ctrl – Del Удалить слово левее курсора Ctrl – BkSp Удалить строку Ctrl – L Выделить Shift + команды перемещения по тексту Отменить выделение Esc Дополнительно 22 Регулярные выражения вполне заслуживают отдельной книги. Отменить правку Alt – BkSp Отменить отмену правки Ctrl – Shift – BkSp Радиолюбитель – 06/2010 СПРАВОЧНЫЙ МАТЕРИАЛ Окно поиска в файлах более простое, содержит всего три поля для ввода: · Find what – текст, который надо найти · In files/filetype – шаблон просматриваемых файлов (например, «*.c» - исходные тексты программ на Си) · In folder – папка, в которой осуществляется просмотр указанных файлов. Опция Search only in files in project ограничивает поиск файлами, составляющими текущий проект, отключая указанную папку. При поиске в файлах нельзя использовать регулярные выражения. Введенный текст ищется с учетом регистра символов. Все найденные вхождения помещаются в окне вывода результатов поиска примерно в таком виде: В каждой строке списка указывается полный путь к файлу, в котором найден указанный текст, затем указывается номер строки с вхождением текста и сама строка. Двойной щелчок на строке в этом окне приведет к тому, что будет открыт нужный файл, и курсор будет установлен в нужную строку. В окне результатов поиска можно выполнить ряд других действий при помощи всплывающего по нажатию правой кнопки мыши контекстного меню: · Clear – очистить окно · Copy – скопировать выделенную строку в буфер обмена · Font – изменить шрифт для текста в окне · Default Font – установить шрифт по умолчанию · Help on Output – просмотреть справку об окне Опции Info (информация), Warning (предупреждения), Error (ошибки) и Timestamp (дата и время) позволяют указать, какого рода информация должна отображаться в этом окне. Компиляция и отладка Так как проект может состоять из нескольких модулей, различают компиляцию модуля и сборку (построение) проекта. Сборка осуществляется компоновщиком из уже откомпилированных объектных модулей, а сама компиляция исходных текстов в объектные файлы выполняется компилятором. Каждый модуль может быть откомпилирован отдельно. Сборка проекта вызывает автоматическую перекомпиляцию тех исходных файлов, которые изменились с момента предыдущей сборки. Если исходные тексты модуля не изменялись – при сборке используется ранее полученный объектный файл, что существенно ускоряет Радиолюбитель – 06/2010 процесс. Иногда требуется процесс очистки (Clean), т.е. удаления всех ранее созданных объектных файлов, чтобы заново перекомпилировать их все. Традиционно процесс компиляции и сборки проекта автоматизируется при помощи так называемого make-файла, в котором на особом скриптовом языке описаны все действия, которые нужно последовательно выполнить, в результате процесс напоминает исполнение команд в bat-файле. Однако, среда AVR Studio позволяет отказаться от использования этого подхода, предоставляя альтернативу гораздо более привычную для Windows – графический интерфейс. Фактически, генерация и исполнение make-файла осуществляется, однако она скрыта от пользователя. Параметры компиляции проекта Ранее уже упоминались команды в главном и контекстном меню проекта, выполняющие настройку параметров компиляции. Рассмотрим диалоговое окно, которое при этом открывается. Это окно настройки параметров компиляции и сборки проекта. В левой части его имеется область для выбора групп параметров в виде графических символов, а остальная часть служит для настройки соответствующих опций. Всего 5 групп параметров: · General – основные · Include Directories – подключаемые директории · Libraries – библиотеки · Memory Settings – настройки параметров памяти · Custom Options – параметры пользователя General – основные Рассмотрим опции в группе основных по порядку их размещения в окне – сверху вниз. Первой следует выбор текущей конфигурации проекта – Active Configuration. Вы можете выбрать из выпадающего списка наименование нужной конфигурации или, нажав на кнопку Edit Configurations, добавить новую или изменить существующие. Несколько слов о том, что такое конфигурация, которая уже упоминалась несколько раз ранее. Разрабатываемый проект может быть рассчитан на использование разных микроконтроллеров, или же на использование какой-либо разной внешней периферии, или предполагается, что проект будет портирован на другое семейство микроконтроллеров и т.п. В каждом конкретном случае наверняка будут иметься небольшие вариации исходных текстов всех модулей, параметров конфигурации и т.п. Программист стоит перед выбором: или создавать набор почти одинаковых файлов для каждого случая, или каким-то образом учесть разные варианты в одних и тех же файлах (способы для этого уже рассмотрены – см. «Директивы условной компиляции»). Но остается открытым вопрос, как упростить процесс выбора нужного варианта. Вот для этого как раз и существует так называемая конфигурация проекта, которая подразумевает наличие нескольких вариантов настроек для одного и того же проекта. По умолчанию всегда существует одна конфигурация – default (по умолчанию). Если требуется создать вариант проекта, предположим для разных типов микроконтроллеров – нужно создать нужное количество конфигураций (далее будет сказано, как). Для каждой конфигурации задается свой тип микроконтроллера, свои уникальные (если нужно) подключаемые файлы, определяются константы, макросы и т.п. Все это автоматически сохраняется при сохранении проекта. Когда потребуется – следует выбрать нужную конфигурацию, т.е. сделать ее текущей, и выполнить очистку и пересборку проекта. Вернемся к диалогу настроек. При нажатии кнопки Edit Configurations (редактирование конфигураций) открывается окно следующего вида: В нем перечислены все существующие конфигурации (в данном случае – единственная default). Выбрав нужную из списка, вы можете ее удалить кнопкой Delete или переименовать кнопкой Rename. Ниже списка имеется поле ввода названия новой конфигурации – если вы введете там ее имя, то активируется кнопка Add Configuration (добавить конфигурацию). Нажатие этой кнопки добавит вашу конфигурацию к списку. Не допускается задавать имя конфигурации с пробелами или с символами кириллицы. Все параметры проекта, рассматриваемые далее, относятся исключительно к выбранной конфигурации. Если их несколько – придется настроить все параметры отдельно для каждой. 55 СПРАВОЧНЫЙ МАТЕРИАЛ Следующей идет опция Use External Makefile (использовать внешний makeфайл). Если эта опция активирована, то будет использован указанный в соответствующем поле make-файл. Использование этой возможности подразумевает наличие этого самого make-файла, правильно создать который – целая наука. В подавляющем большинстве реальных практических случаев вам это не потребуется. Далее предлагается указать имя выходного файла проекта – Output File Name и директории, куда он будет помещен – Output File Directory. По умолчанию имя выходного файла совпадает с именем проекта (но с расширением elf), а имя директории – с именем выбранной конфигурации. Сама директория по умолчанию создается в папке проекта. Как правило, нужды изменять значение этих параметров не возникает. Далее следует группа важных параметров компиляции проекта. Device – выбор модели микроконтроллера. Из выпадающего списка вы можете выбрать любую поддерживаемую компилятором модель. Учтите, что выбранная здесь модель контроллера не обязательно должна совпадать с той, что была указана в момент создания проекта (см. «Мастер проектов»), однако во избежание недоразумений не следует допускать такого разнобоя23. Frequency – тактовая частота контроллера. Вы должны указать в герцах значение тактовой частоты, с которой предполагается использовать микроконтроллер. Этот параметр важен для правильной генерации кода некоторых библиотечных функций. В программе это значение всегда доступно в виде константы F_CPU (т.е. эту символьную константу можно использовать при написании программы). Рекомендуется всегда задавать этот параметр. Optimization – уровень оптимизации. Можно выбрать из выпадающего списка любой доступный вариант. По умолчанию устанавливается режим наиболее сильной оптимизации по размеру кода – Os. Unsigned Chars – опция, указывающая компилятору, что тип char должен рассматриваться как беззнаковый. В скобках приводится параметр командной строки компилятора для этой же цели. Следует с осторожностью пользоваться этой опцией! Unsigned Bitfields – беззнаковые битовые поля. Опция влияет на структуры типа битовое поле24. Pack Structure Members – упаковывать поля структур. Опция указывает компилятору использовать так называемый «упакованный» формат хранения в памяти данных полей структуры. В некоторых случаях это может вызвать несовместимость с исходными текстами, написанными для других компиляторов, хотя в большинстве случаев дает экономию памяти данных. 23 Мастер проекта задает модель для отладки, в то время как компилятор может генерировать код для любой другой модели. Разумеется, ожидать адекватного поведения отладчика при разных моделях контроллера не приходится. 24 Эта возможность языка Си не рассматривалась в кратком введении в язык. Принципиально, это никак не скажется на эффективности ваших программ. 56 Short Enums – короткие списки. Опция указывает компилятору, что следует использовать для хранения перечисляемых констант24 тип short int. Как правило, все эти 4 опции можно оставить по умолчанию (они активированы), и только в случае возникновения ошибок компиляции, связанных с ними, отключать. Завершают этот раздел параметров проекта три опции генерации компилятором вспомогательных файлов25. Create Hex File – создать Hex-файл. Если вы планируете использовать программатор для «прошивки» микроконтроллера вашей программой – включите эту опцию, т.к. формат Hex – де-факто стандартный формат данных для всех типов программаторов. Generate Map File – генерировать файл «карты памяти». Карта памяти позволит детально рассмотреть результат работы компилятора: выяснить, какие переменные в каких областях памяти находятся, сколько места занимают и т.п. Бывает очень полезно при «глубинной» отладке или оптимизации. Отключение этой опции ускоряет компиляцию. Generate List File – генерировать листинг. Листинг – это файл, в котором одновременно виден исходный текст на языке Си и его ассемблерный эквивалент, а так же машинные коды соответствующих инструкций и т.п. информация. Может быть полезен при отладке или при изучении работы компилятора. Отключение этой опции ускоряет компиляцию. Include Directories – подключаемые директории что компилятор использует первый найденный подходящий файл. Libraries – библиотеки Группа настроек библиотек служит для указания компоновщику использовать готовые библиотечные файлы (архивы) или объектные модули. Параметры по умолчанию подойдут в большинстве случаев. Однако, если необходимо, вы можете добавить пути поиска библиотек (аналогично рассмотренному способу в предыдущей главе), а так же указать конкретные варианты объектных модулей стандартных библиотек, которые следует использовать при сборке вашего проекта. Для этого в списке Available Link Objects (доступные для компоновки объекты) нужно выбрать требуемый файл и, нажав кнопку Add Library (добавить библиотеку), добавить его в список прикомпоновываемых объектов Link with These Objects. Кнопка Remove Object позволяет удалить файл из числа прикомпоновываемых, кнопки Move Up (переместить выше) и Move Down (переместить ниже) изменяют порядок файлов в списке. Наконец, кнопка Add Object позволит добавить файл, которого нет в левом списке, к списку справа. Memory Settings – настройки параметров памяти Эта группа опций представлена единственным (и по умолчанию пустым) списком директорий, в которых компилятор последовательно будет искать файлы, подключенные директивами #include. Список по умолчанию пуст неспроста – указывать дополнительные пути имеет смысл лишь в том случае, когда вы имеете пакеты библиотек или исходных файлов модулей сторонних разработчиков (или свои собственные универсальные «заготовки»). Добавить директорию можно при помощи кнопки , удалить – , а кнопками и можно изменить порядок просмотра директорий компилятором. Учтите, 25 Фактически, эти файлы генерируются дополнительными утилитами, входящими в комплект WinAVR, однако с точки зрания программиста это несущественно. Группа параметров памяти позволяет управлять пользовательскими секциями (см. далее «Секции памяти»). В верхней части окна приводятся сведения о памяти выбранного микроконтроллера (Part Information): Flash Size – объем памяти программ Sram Size – объем ОЗУ Eeprom Size – объем EEPROM Ниже располагается список заданных пользователем секций в виде таблицы, состоящий из трех столбцов: Memory Type (тип памяти), Name (имя секции) и Address (стартовый адрес секции). Добавить свою Радиолюбитель – 06/2010 СПРАВОЧНЫЙ МАТЕРИАЛ секцию вы можете, нажав кнопку Add, при это откроется следующее окно: Поля ввода в этом окне совпадают с наименованиями столбцов в таблице: из выпадающего списка можно выбрать тип памяти (память программ Flash, ОЗУ Sram или EEPROM), затем следует ввести имя секции и, в заключение, ее стартовый адрес. Учтите, что компоновщик считает, что секция ОЗУ начинается с адреса 0x800000, а секция EEPROM – с адреса 0x810000, т.е. к фактическому адресу в пространстве ОЗУ или EEPROM нужно прибавить соответствующее смещение. На добавленную секцию затем можно сослаться в программе, указав соответствующий атрибут. Удалить секцию из таблицы можно нажатием кнопки Remove. В нижней части окна имеется опция управления участком памяти, выделяемого для организации стека – Specify Initial Stack Address. Если активировать эту опцию, то можно задать адрес начала области ОЗУ, выделяемой под стек (дополнительно см. «Динамическое распределение памяти»). Custom Options – параметры пользователя Последняя группа параметров позволяет пользователю добавить опции к командной строке компилятора и компоновщика (Custom Compilation Options). Это дает максимальную гибкость в настройке параметров компиляции, однако требует достаточно высокой квалификации пользователя. Слева находится окно со списком всех файлов проекта и парой «глобальных» элементов – All files (все файлы) и Linker Options (параметры компоновщика). Справа окно со списком параметров командной строки, используемых для обработки выбранного в левом списке элемента. Т.е. если вы желаете, чтобы все файлы компилировались с одним и тем же набором параметров – выберите слева вариант All files. По умолчанию в списке справа перечислены все опции, которые были заданы на других страницах диалога настройки в «интерактивном режиме» – присмотритесь: вы увидите –funsigned-char, которая соответствует опции Unsigned Chars из первой группы, указание константы F_CPU и т.п. Удаление (кнопкой Remove) элемента списка приведет к соответствующему Радиолюбитель – 06/2010 изменению поведения компилятора и соответственно отразится в других разделах настроек. Некоторые опции не имеют интерактивных аналогов, но могут серьезно влиять на компиляцию. В главе «Опции командной строки» будет рассказано о параметрах командной строки, которые вы можете дополнительно добавить к этому списку. Делается это вводом соответствующей строки в поле под правым списком и нажатию кнопки Add (добавить). Если требуется изменить введенную ранее строку – надо выделить ее в списке и нажать кнопку Edit, она скопируется в поле редактирования (не забудьте затем снова нажать Add для возврата строки в список). В нижней части имеется группа опций External Tools, которая позволяет изменить инструментарий, необходимый для компиляции, указав другие программы компилятора avr-gcc и утилиты make. Категорически не рекомендуется изменять значения по умолчанию. Сборка проекта Рассмотрим меню «Build», ранее только упоминавшееся. Меню состоит из следующих пунктов: · Build (собрать) – выполняет сборку проекта, компилируя при необходимости обновленные исходные файлы модулей. · Rebuild All (пересобрать все) – выполняет принудительную перекомпиляцию всех файлов проекта с последующей сборкой. · Build and Run (собрать и запустить) – выполняет сборку проекта и включает режим отладки, т.е. запускает программу на исполнение. · Compile (компилировать) – выполняет компиляцию только одного текущего редактируемого файла (модуля) · Clean (очистить) – удаляет все результаты предыдущих компиляций всех модулей · Export Makefile – сохраняет в makeфайле все настройки компиляции для последующего использования компилятором в консольном режиме. Выполнение любой команды меню, кроме последнего, заключается в автоматически (и незаметно для пользователя) выполняющихся следующих этапах: 1. Компиляция при помощи консольного компилятора avr-gcc.exe одного или всех модулей с «перехватом» всей выводимой компилятором информации и выводом ее в окно информации на закладке «Build» (окно Output – см. «Рабочее пространство»). 2. При отсутствии ошибок на первом этапе (и при необходимости) – запуск компоновщика с аналогичным перехватом выводимой информации. 3. При отсутствии ошибок компоновки – запуск вспомогательных утилит для генерации различных выходных файлов с аналогичным «перенаправлением» выводимых ими сообщений. В итоге у пользователя создается впечатление, что AVR Studio сама выполняет компиляцию и т.п. действия. Запуск компилятора сопровождается выводимой командной строкой в окне информации на закладке «Build», отмечаемой зеленым кружочком. Если компиляция модуля прошла без ошибок, то в окне появляется сообщение «Build succeeded with 0 Warnings...» – компиляция успешна, 0 предупреждений. Разумеется, количество предупреждений может быть и ненулевым. В том случае, если выполнялась сборка проекта, дополнительно будет выведены сведения о полученном объектном модуле проекта, и окно будет иметь следующий вид: В приведенной на рисунке итоговой сводке указывается объем занятой памяти микроконтроллера attiny13: программа занимает 558 байт (что составляет 54,5% от всего имеющегося пространства), данные (секций «.data», «.bss» и «.noinit») занимают 5 байтов (что составляет 7,8% всей доступной памяти). Эти сведения помогут вам сделать выводы о ресурсоемкости вашего проекта. Ошибки компиляции Если на этапе компиляции обнаруживаются ошибки в тексте модуля, выводится соответственно иное сообщение «Build failed with 3 errors and 0 warnings...» – компиляция неудачна, 3 ошибки и 0 предупреждений (и снова – число ошибок и предупреждений может быть иным). Подобное сообщение обязательно предваряется указанным количеством сообщений об ошибках (помеченных красным кружочком) и предупреждениях (помеченных желтым кружочком), например, так: На рисунке показано, что при компиляции модуля «encoder.c» в функции этого модуля «enc_rotate» обнаружены следующие три ошибки (как правило, одна ошибка может порождать несколько следующих за ней): в строке 36 найден ранее не описанный символ EN_A_DN (остальные две ошибки – это собственно не ошибки, а 57 СПРАВОЧНЫЙ МАТЕРИАЛ продолжение описания первой ошибки26 ). После сообщений об ошибках следует строка-уведомление о том, что сборка проекта невозможна, т.к. есть ошибка. К сожалению, привести список всех сообщений об ошибках, распознаваемых компилятором, нет никакой возможности – он слишком обширен. Чтобы более-менее ориентироваться в них, следует хоть немного знать английский язык – фразы весьма примитивны, перевод их не вызывает сложностей буквально спустя две-три попытки. Процесс работы над проектом, как правило, обязательно сопровождается неоднократной компиляцией модуля с последующим исправлением ошибок. Чтобы этот процесс был более удобен – AVR Studio реализует удобный механизм: двойной щелчок мышкой на строке с сообщением об ошибке приводит к тому, что в редакторе откроется нужный файл (если он не был еще открыт), а курсор будет установлен в строку с ошибкой, причем сама строка будет дополнительно помечена синей стрелочкой – вам останется лишь исправить ошибку. Отдельно следует оговорить случай, когда после компиляции программы выясняется, что размер объектного модуля слишком велик, чтобы уместиться в доступной памяти микроконтроллера. Окно результатов компиляции при этом внешне выглядит, как при верной компиляции, что не должно вводить в заблуждение: Прекрасно видно, что программа «требует» наличия 201% памяти, что, разумеется, невозможно. В том, что это действительно ошибка, вы убедитесь в момент, когда попытаетесь приступить к отладке программы (см. «Средства виртуальной отладки»): вместо начала отладки произойдет переключение информационного окна на закладку «Message»: В отмеченной желтым кружком строке сказано, что «Содержимое объектного файла превышает размер программной памяти выбранного микроконтроллера», а следующая строка, отмеченная красным, указывает на ошибку загрузки этого объектного файла. Борьба с размером программы – сложный процесс. Иногда слишком большой 26 Особенность «перехвата» сообщений – каждая строка сообщения воспринимается как новая ошибка. 58 объем является следствием неудачно выбранного режима оптимизации, реже – следствием крайне неаккуратного программирования. Следует помнить, что очень требовательными к памяти программ являются любые алгоритмы с использованием чисел с плавающей точкой – только добавление в программу единственной операции деления с дробным результатом может увеличить объем программы более чем на 1,5 килобайта! Наконец, ошибка может быть и в выборе микроконтроллера с заведомо недостаточным объемом памяти. Предупреждения компилятора Предупреждения компилятора – это сообщения о найденных в тексте программы «подозрительных» или потенциально опасных мест, неоднозначностей, допускаемых синтаксисом языка и т.п. В некоторых случаях предупреждение – это завуалированная ошибка, но чаще это просто «очистка совести» компилятора за слишком широкие вольности, допускаемые языком Си. В окне информации предупреждения компилятора выводятся на закладке «Build», и отмечаются желтыми значками: Как и при ошибках, двойной щелчок на строке с предупреждением перенесет курсор в строку, где кроется причина этого сообщения. Опасны ли предупреждения? Скорее следует признать, что они таят в себе некоторую опасность именно за счет своей «сомнительности», т.е. компилятор «чувствует», что тут что-то не так, но назвать это ошибкой – «язык» у него не поворачивается (извините за столь одушевленную образность). Например, на рисунке самое первое предупреждение гласит «../ bp_c.c:75: warning: return type of “main” is not “int”» – тип функции main – не int. Опасно ли это? 100% нет – для микроконтроллеров совершенно без разницы, какое значение возвращает функция main. А несколько следующих предупреждений уже иного рода: «../ display.c:41: warning: large integer implicitly truncated to unsigned type» – большое число укорочено до размера unsigned int. Это уже должно насторожить: согласитесь, что если число 2L будет укорочено до int – это не страшно, но вот если 24765134 укоротить до int – это вряд ли может устроить. Поэтому надо внимательно проанализировать строки в программе, приведшие к предупреждениям. В частности, в случае на рисунке, предупреждения были вызваны строками типа char var = ~2. Почему так вышло? Все просто: по умолчанию результат любой операции в Си, если иное не оговорено, трактуется как int, а константа 2 по умолчанию трактуется компилятором, как char (см. «Константы»). Таким образом, операция инверсии ~2 из char получит int, т.е. число со знаком. Переменная var имеет тип char, т.е. меньший, чем int – чтобы значение «влезло» в переменную, оно укорачивается путем отбрасывания старшего байта – об этом компилятор и предупреждает. В этом конкретном случае никакой опасности нет, потому что поведение компилятора совпадает с намерением программиста получить просто инверсное представление байта 2, однако поручиться, что такое же предупреждение во всех случаях определенно безопасно, нельзя. По возможности следует стремиться к тому, чтобы предупреждений при компиляции не возникало. Средства виртуальной отладки Допустим, компиляция проекта прошла без ошибок и предупреждений. Означает ли это, что программа заработает в реальной схеме? Вовсе нет! Записанная без ошибок неверная инструкция к действию не может привести к успешному результату. То есть отсутствие синтаксических ошибок, проверяемых компилятором, вовсе не означает отсутствия логических или алгоритмических ошибок программиста. Отладка – это как раз процесс поиска логических и алгоритмических ошибок. AVR Studio предлагает программисту удобный способ проследить за тем, как его программа исполняется микроконтроллером, при этом программист может не только наблюдать за процессом, но и вмешиваться в него! Реализуется это следующим образом: AVR Studio эмулирует27 работу микроконтроллера, т.е. имитирует исполнение им программы. Благодаря тому, что мощность современных компьютеров очень велика, особых проблем проимитировать работу куда более слабого AVR не возникает. При этом AVR Studio заполняет «виртуальные» регистры и ячейки памяти значениями из программы, исполняя написанные программистом строки. Содержимое любой переменной в программе, и даже любого регистра или ячейки памяти, программист может в любой момент изменить, проимитировав любые варианты поступления в программу внешних данных. Такое «виртуальное» исполнение программы позволяет найти почти все отклонения поведения программы от задуманного. Рассмотрим меню «Debug», команды которого управляют всем процессом отладки. На рисунках (см. на следующей странице) показаны варианты меню «Debug» соответственно для выключенного режима отладки и для включенного. 27 Наряду с термином «эмуляция» применяется и термин «симуляция». Разделение между ними скорее надуманное: первый чаще применяют в тех случаях, когда используются аппаратные средства, имитирующие действия микроконтроллера или его узлов, второе – при использовании чисто программных методов имитации. В настоящей книге разницы между терминами не делается, т.к. в русском языке это слова-синонимы. Радиолюбитель – 06/2010 СПРАВОЧНЫЙ МАТЕРИАЛ · Start Debugging – запуск отладки. Команда переводит AVR Studio в режим эмуляции микроконтроллера, при этом могут автоматически появиться на дисплее новые окна и панели. · Stop Debugging – остановка отладки. Команда завершает режим отладки, возвращая вид интерфейса к исходному. · Run – исполнение. Команда запускает программу на исполнение. Обычно этой команде должны предшествовать команды установки точек останова, иначе возможности наблюдения за ходом работы программы практически не будет (во время исполнения содержимое отладочных окон не обновляется). · Break – приостановка (пауза) исполнения. Команда активируется только во время исполнения программы по команде Run и позволяет остановить программу принудительно. После остановки исполнения обновляется содержимое окон отладки. · Reset – сброс. Команда позволяет проимитировать поступление сигнала Reset на Радиолюбитель – 06/2010 микроконтроллер, т.е. вынуждает программу начаться заново. · Step Into – «шаг внутрь». Пошаговое исполнение программы с заходом в функции. · Step Over – «шаг поверх». Пошаговое исполнение программы, причем обращения к функциям исполняются, как «одна команда». · Step Out – «шаг наружу». Команда вызывает исполнение всех оставшихся строк (операторов) в функции до выхода из нее, т.е. остановка происходит в момент возврата значения функции. · Run to Cursor – «дойти до строки с курсором». Команда запускает программу на исполнение с текущего места и останавливает, как только исполнение дойдет до строки, в которой находился курсор в момент подачи команды. · Auto Step – автопошаговое исполнение. Программа начинает исполняться в режиме «автоматического исполнения» команд Step, подаваемых через определенные интервалы времени (можно регулировать). · Next Breakpoint – перейти к следующей точке останова. · New Breakpoint – подменю команд установки точек останова. Возможно три варианта точек остановки: Program Breakpoint – остановка по месту в программе, Data Breakpoint – остановка по изменению данных (переменных) в программе и Program Tracepoint – точка трассировки программы (не является точкой останова по своей сути). · Toggle Breakpoint – включение/выключение точки останова в строке программы · Remove all Breakpoints – удалить все точки останова из программы · Trace – подменю управления режимами трассировки программы (рассматривается в последующих главах). · Stack Monitor – команда зарезервирована · Show Next Statement – показать строку, которая должна исполняться. Команда позволяет быстро переместиться к очередной исполняющейся строке из любого места программы. · Quickwatch – добавление переменной в окно просмотра. Добавляется переменная, на которой установлен текстовый курсор в тексте программы. · Select Platform and Device – выбрать отладочную платформу и микроконтроллер. Позволяет изменить на время отладки настройки, сделанные при создании проекта. · Up/Download Memory – сохранить/загрузить область памяти. · AVR Simulator Options – настройки эмулятора AVR. В первую очередь рассмотрим последний пункт меню – настройку параметров эмулятора (данный пункт доступен только в режиме отладки), а остальные команды будут рассмотрены более подробно далее, в контексте различных способов и режимов отладки (см. следующую колонку): В левой части окна имеется древовидный список групп настроек эмулятора. Количество этих групп зависит от того, какой именно способ отладки был избран при создании проекта: мы рассматриваем программный симулятор, а для аппаратных средств отладки могут быть доступны и другие группы параметров. В базовом варианте доступно две группы: Device selection – выбор устройства и Stimuli and logging – стимуляция и протоколирование. В группе Device Selection можно выбрать тип эмулируемого микроконтроллера из раскрывающегося списка Device. Важно понимать, что обязательно нужно всегда указывать одинаковые типы и в настройках компилятора и здесь, иначе отладка будет некорректной или вообще невозможной. По умолчанию такое соответствие обеспечивается автоматически. Так же можно указать значение тактовой частоты контроллера при отладке: выбрать из списка Frequency или ввести значение вручную. Для корректной эмуляции нужно так же обеспечить совпадение с частотой, указанной при компиляции. Наконец, если программа содержит секцию загрузчика (Boot loader), можно и нужно указать адрес начала этой секции и указать режим поведения при сбросе микроконтроллера (отмеченная опция Enable Boot reset заставит после сброса исполняться функцию загрузчика). Если микроконтроллер имеет возможность подключения внешнего ОЗУ, становится доступной опция Enable external Memory, включающая поддержку эмулятором дополнительного внешнего ОЗУ. В правой части окна присутствует область, в которой кратко приведены основные характеристики выбранного микроконтроллера. Следует помнить, что максимальное значение тактовой частоты, указанное здесь (и даже в технической документации) не является ограничением для программной эмуляции – вы можете задать значение тактовой частоты хоть 100 МГц – процесс отладки от этого никак не изменится. Группа Stimuli and logging отвечает за режим имитации и протоколирования внешних сигналов на портах микроконтроллера и подробно рассматривается в главе «Имитация входных сигналов и наблюдение выходных». Ресурсы 4. http://www.rsdn.ru/article/alg/regular.xml Продолжение в №7/2010 59 СПРАВОЧНЫЙ МАТЕРИАЛ Роман Абраш г. Новочеркасск E-mail: arv@radioliga.com Продолжение. Начало в №1-6/2010 Наблюдение за ресурсами проекта Для наблюдения за содержимым всех ресурсов микроконтроллера и переменных в программе пользователя во время ее отладки AVR Studio предоставляет богатый набор средств. Прежде всего, это «Окно периферии», предоставляющее удобный интерфейс наблюдения и изменения состояния всех регистров управления встроенными периферийными устройствами микроконтроллера. Во время отладки состояние отдельных битов изображается закрашенными в черный цвет квадратиками; щелкнув на любом из них, программист может изменить состояние бита на противоположное. Это бывает необходимо в следующих случаях: - обнаружена ошибка в программе, заключающаяся в «инверсном» анализе какого-либо бита (т.е. надо проверять на равенство 1, а в программе ошибочно проверяется на 0 и т.п.). Конечно, можно остановить процесс отладки и, исправив ошибку, перекомпилировать программу, однако часто удобнее принудительно изменить бит в регистре «наоборот», чтобы «обмануть» неверную программу, заставив ее правильно отреагировать на ситуацию, чтобы продолжить отладку остальных участков кода; - AVR Studio не поддерживает корректную эмуляцию периферийного устройства контроллера. Например, невозможна эмуляция АЦП – значение в регистрах результата AVR Studio никогда самостоятельно не изменяет, и для того, чтобы проимитировать факт реального измерения, программист должен самостоятельно ввести значения в соответствующие регистры; - необходимо проимитировать поступление на порт микроконтроллера сигнала извне (от прочих элементов схемы). В этом случае нужно изменить значение соответствующего регистра PINx вручную. Кроме ручного способа имеется и «полуавтоматический», так называемое «стимулирование порта», которое рассматривается в следующей главе более подробно. Кроме окна периферии имеется и еще ряд окон и панелей, управляемых при помощи меню «View». Рассмотрим их подробно. Панель состояния процессора – Processor. По умолчанию (если расположение панелей не было изменено) в режиме отладки автоматически активируется в области, где находится и окно проекта: 52 Книга по работе с WinAVR и AVR Studio В этой панели отображаются как некоторые недоступные для принудительного изменения значения, так и доступные. Program Counter – программный счетчик, показывает значение PC, т.е. в сущности адрес очередной исполняемой команды. Stack Pointer – указатель стека, показывает адрес ОЗУ, хранящийся в регистре SP. X pointer, Y pointer и Z pointer – показывает значения указателей X, Y и Z. Рассмотренные три параметра недоступны для принудительного изменения в ходе отладки. Cycle Counter – счетчик машинных циклов, показывает число тактов, потраченных на исполнение всех команд с момента старта программы. Этот счетчик может быть сброшен в любое время 28. Frequency – тактовая частота, соответствует значению, заданному в настройках эмулятора (см. предыдущую главу). Stop Watch – время остановки. Это значение показывает время, прошедшее с момента начала исполнения программы (т.е. с момента старта отладки) до момента ее приостановки. Этот «секундомер» может быть сброшен пользователем в любой момент, что позволяет засекать время исполнения отдельных участков программы. SREG – содержимое регистра статуса ядра микроконтроллера, показывает состояние всех битов этого регистра, которые доступны для изменения пользователем. Далее следует группа регистрового файла микроконтроллера – Registers, в которой показано содержимое всех 32 регистров. Эти значения так же доступны для модификации в любое время. Для панели Processor доступно всплывающее меню: Hexadecimal Display – если отмечено, то все или только выбранное мышкой значение будет отображаться в шестнадцатеричном формате, в противном случае используется десятичный формат. Reset Stopwatch – сброс времени остановки (сброс «секундомера»). Reset Cycle Counter – сброс счетчика машинных тактов. Show Stopwatch in milliseconds – время остановки отображать в миллисекундах (по умолчанию счет в микросекундах). Font – изменить шрифт, используемый для вывода содержимого панели. Default Font – установить для панели шрифт по умолчанию. Help – вызов справки (на английском) о панели. 28 Здесь и далее под «любым временем» подразумевается любой момент остановленного исполнения программы. Во время автоматического или автоматического пошагового исполнения все средства изменения состояния регистров и портов недоступны. Окно-панель наблюдения за переменными Watch: Это окно содержит 4 закладки, в каждой из которых можно наблюдать и при необходимости модифицировать содержимое любых переменных (в том числе регистров микроконтроллера) во время отладки. Информация представлена в виде таблицы из 4-х столбцов: Name – имя переменной. Можно ввести имя переменной вручную, выполнив двойной щелчок в первой свободной строке. Двойной щелчок на имени уже имеющемся в окне позволяет изменить его, т.е. выбрать другую переменную для наблюдения. Value – значение переменной. Показывается числовое и, если возможно, символьное представление. Выполнив двойной щелчок в этом столбце, можно принудительно изменить значение переменной, введя любую допустимую в Си константу. Type – тип переменной. Location – адрес начала области памяти, выделенной для хранения переменной. В квадратных скобках указывается тип памяти (встроенное ОЗУ или внешнее). Кроме ручного ввода имени переменной, в окно Watch можно перетащить и бросить идентификатор переменной прямо из текста программы, т.е. выделить переменную, «схватить» ее и перенести в это окно. Есть и третий способ – установив курсор на интересующую переменную в тексте программы, щелкнуть правой кнопкой мыши и в появившемся меню (см. рисунок) выбрать команду Add Watch. Наконец, аналогичный результат достигается и при нажатии на кнопку на панели кнопок. Для окна Watch имеется всплывающее меню: D i s p l a y selected Value as Hex – отображать выбранное Радиолюбитель – 07/2010 СПРАВОЧНЫЙ МАТЕРИАЛ значение (необходимо предварительно выделить строку в таблице) в виде шестнадцатеричного числа. Display all Values as Hex – все значения показывать в шестнадцатеричном формате. Display Array Index as Hex – индексы массивов показывать в шестнадцатеричном формате. Add Item – добавить переменную Remove selected Item – удалить из окна выделенную строку (переменную) Remove all items – удалить из закладки все переменные Font и Default font – изменение шрифта для окна, как уже было сказано ранее. Help on Watch View – вызов справки об окне. К сожалению, просмотр массивов в окне Watch на протяжении многих версий AVR Studio сопровождается одним неудобством: содержимое массива не обновляется в момент остановки программы (на точке останова или принудительно), поэтому приходится «свернуть» и затем «развернуть « массив, чтобы увидеть его актуальное содержимое. Для сворачивания и разворачивания массивов в соответствующей строке окна будет находиться кнопочка с «плюсиком» или «минусом» соответственно. Окно просмотра содержимого памяти Memory 29: Если окно Watch позволяет наблюдать и модифицировать значения переменных, то данное окно позволяет аналогично оперировать содержимым любых ячеек памяти безотносительно к их распределению по переменным. В верхней части окна имеется ряд органов управления: · Список типов наблюдаемой памяти: Data (ОЗУ данных), EEPROM, I/O (область портов), Program (Flash память программ) и Register (область адресов регистрового файла). · Кнопка 8/16, позволяющая изменить разрядность отображаемых данных – 8 или 16 бит. · Кнопка abc, включающая или отключающая показ символьного представления содержимого. · Поле Address, задающее адрес первой отображаемой ячейки в окне. Если на очередном шаге отладки содержимое ячейки памяти изменилось – это выделяется красным цветом. 29 Как было упомянуто ранее, таких окон может быть до трех. Радиолюбитель – 07/2010 При помощи команд всплывающего меню можно гибко управлять как отображением, так и содержимым наблюдаемой области памяти: Hexadecimal и Decimal позволяют переключить формат вывода содержимого памяти. 1 Byte или 2 Byte переключают разрядность данных (как и кнопка 8/16). Byte address переключает режим вычисления адреса – побайтно (если отмечено) или по 16-битным словам. Add Data Breakpoint – команда установки точки остановки по изменению содержимого указанной ячейки памяти. Show tooltip – включает или отключает всплывающие подсказки. Вы можете изменить произвольно выбранную ячейку и посмотреть, как это скажется на работе вашей программы. Очень удобно при помощи этого окна определять глубину стека, необходимую для работы программы: запускаем программу на исполнение, ждем достаточное время для того, чтобы все ветви алгоритма отработали, а затем останавливаем программу и открываем окно просмотра ОЗУ. Будет хорошо видно, что в начале области памяти и в конце ячейки содержат какие-то значения – в начале область переменных, а в конце область, использованная стеком. Если между этими областями имеется достаточное количество пустых ячеек (содержат значение 0xFF) – все нормально, стек не затирает область переменных. Если между этими областями нет пустоты или всего две-три ячейки не заняты – это очень тревожный признак –скорее всего такая программа в реальности работать не будет из-за переполнения стека. В комплексе с окном Memory удобно использовать другую возможность – загрузку или сохранение содержимого области памяти из/ в файла, реализуемую командой Up/Download Memory из меню «Debug». В этом случае появляется окно следующего вида: Точно так же вы можете указать тип памяти – список Memory Type, задать адрес первой обрабатываемой ячейки Start Address, количество обрабатываемых ячеек Byte Count (при этом ориентируйтесь на подсказку выше – значения Start и Size, которые определяют границы выбранной области). В поле Hex File нужно указать имя файла, с которым будет осуществлена работа. Кнопка Load from File позволяет загрузить из указанного файла содержимое в выбранную область памяти, а кнопка Save to File выполняет обратную операцию – сохраняет указанную область в заданный файл. Формат файла – Intel HEX. Таким образом, реализуется достаточно удобный механизм работы с «загружаемыми» данными. Вернемся к окнам наблюдения: очередное из них – это окно просмотра регистрового файла Register. Оно дублирует содержимое регистров в панели Processor, а по функциональности соответствует только что рассмотренным окнам с той лишь разницей, что позволяет отображать информацию с большим разнообразием форматов, указываемых через всплывающее меню: В дополнению к шестнадцатеричному и десятичному форматам, здесь имеется возможность указать символьный (Ascii) или двоичный (Binary). Последнее окно, нередко необходимое для отладки, это окно дизассемблера (Disassembler). Это окно располагается обычно в основной области, т.е. там же, где и исходный текст. В нем выводится дизассемблированный код программы, т.е. восстановленный до команд ассемблера. При этом операторы Си так же показаны (что позволяет увидеть, какими ассемблерными командами реализован тот или иной оператор Си): Содержимое окна дизассемблера напоминает содержимое файла-листинга, за исключением того, что формируется не компилятором, а AVR Studio. Имитация входных сигналов и наблюдение выходных Микроконтроллер, не смотря на всю его многофункциональность, так или иначе взаимодействует с остальными элементами схемы конкретного устройства, т.е. должен реагировать на входные сигналы, формируя выходные. Процесс отладки часто требует именно контроля того, как программа отреагирует на поступающие сигналы. Если входных сигналов немного и алгоритм их поступления достаточно прост, то их 53 СПРАВОЧНЫЙ МАТЕРИАЛ вполне можно проимитировать, устанавливая нужные значения в нужные моменты времени непосредственно в соответствующих битах регистров PINx, как было сказано ранее. Но этот способ сильно усложняется, если число входных сигналов растет, и становится почти невозможным, если частота их поступления высока. Решение этой проблемы заключается в использовании так называемой «стимуляции» портов микроконтроллера, т.е. имитации поступления на них внешних сигналов. Реализуется это при помощи заранее подготовленного текстового файла с расширением «sti», в котором последовательно перечислены условные моменты (в машинных тактах работы микроконтроллера), когда состояние сигналов на порте меняется, и, разумеется, сами эти значения сигналов. То есть файл стимуляции имеет примерно следующее содержимое: На рисунке показано, что в начальный момент все сигналы, подаваемые на порт, имеют низкий логический уровень (00). В момент наступления 9-го машинного такта состояние сигналов меняется на 0xAB, а к 14-ому такту на 0xAC и т.д. Количество строк в файле ограничено значением 999999999. Для стимуляции используется отдельный режим отладки, настраиваемый группой параметров Stimuli and logging ранее рассматриваемого окна настроек эмулятора. Вы должны указать порт, который будет подвержен стимуляции, выбрав его из списка Port, а так же задать файл со стимулирующей последовательностью Input, после чего нажать кнопку Add Entry для добавления заданной стимуляции к списку операций Action List. Для каждого порта вы можете указать свой файл аналогичным способом, после чего нужные уровни поступят в нужные моменты времени на соответствующие «выводы портов» автоматически во время отладки, вам останется лишь следить за реакцией на это вашей программы. Кроме стимулирования существует обратная задача – протоколирование сигналов, формируемых микроконтроллером – Logging. В этом случае сигналы выбранного порта сохраняются в файл с расширением «log» точно в том же виде, как и при стимуляции. Выбор функции – стимуляция или протоколирование – осуществляется выбором соответствующей опции Function. 54 Если выбрано протоколирование порта, то файл можно и не задавать, если активировать опцию To screen. В этом случае по мере смены уровней на выбранном порту в окне Message будут выводиться соответствующие сообщения такого вида: Вывод этих сообщений не зависит от того, ведется ли запись протокола в файл или нет. Создание файлов стимуляции – довольно-таки утомительная процедура, особенно для длительных и сложных последовательностей. Облегчить ее можно, если использовать дополнительные утилиты сторонних разработчиков – см. главу «Дополнительные средства». СРЕДСТВА ПОДДЕРЖКИ АППАРАТНОЙ ОТЛАДКИ Под аппаратной отладкой понимаются средства, подключаемые к компьютеру через один из имеющихся интерфейсов, и обеспечивающих исполнение программы в реальном микроконтроллере, но под контролем среды AVR Studio. Это позволяет полностью исключить несоответствия программной эмуляции микроконтроллера. Яркий пример – рассмотренный способ стимуляции портов. Очевидно, что такая стимуляция осуществляется строго синхронно с работой микроконтроллера, т.к. моменты изменения уровней сигналов привязаны к числу машинных тактов. В реальных схемах моменты поступления внешних сигналов никак не связаны с работой микроконтроллера, и с точки зрения программы являются практически случайными. Кроме того, программная эмуляция попросту невозможна для ситуаций с большим периодом повторения – уже отладка процессов, длящихся десятки секунд, в режиме эмуляции становится утомительно долгой, что тогда говорить о процессах, длящихся минуты и часы! Так же практически невозможна эмуляция различных сложных и быстродействующих интерфейсов, например, CAN или USB, аналоговые устройства так же не эмулируются в принципе. Обзор средств Большинство средств аппаратной отладки разработано и поставляется на рынок самой фирмой Atmel, имеющиеся на рынке образцы сторонних производителей – лишь упрощенные версии фирменных устройств, либо их функциональные аналоги. Аппаратная отладка может осуществляться либо по стандартному интерфейсу JTAG, встроенному в некоторые типы микроконтроллеров, либо по интерфейсу Debug Wire, так же присутствующему во многих моделях микроконтроллеров. Необходимость аппаратной поддержки указанных интерфейсов накладывает ограничения на применимость средств – многие микроконтроллеры принципиально не могут работать с этими средствами. Имеются средства, которые облегчают процесс отладки, не заменяя эмуляцию, но дополняя ее – однако, это в сущности лишь макетные платы с готовым интерфейсом «визуализации» данных – либо в виде различных дисплеев, либо в виде возможности вывода информации из микроконтроллера в компьютер и отображения ее в окне терминальной программы. Все фирменные средства отладки (и, в том числе, программирования) микроконтроллеров приведены в справочном файле, открываемом по команде меню «Help» AVR Tools User Guide. В этом же файле приведены подробные инструкции по их использованию. В рамках данной статьи рассмотреть все средства достаточно подробно невозможно, поэтому ограничимся лишь их кратким перечнем с указанием основных особенностей. ICE50 и ICE40 – эмуляторы-отладчики, поддерживают почти все микроконтроллеры, обеспечивают полный функционал отладки, включая все виды точек останова, поддержку аналоговой периферии, сторожевого таймера, режимов «сна» микроконтроллера и т.п. Отличаются комплектностью и количеством поддерживаемых микроконтроллеров. Подключаются вместо реального микроконтроллера в схему пользователя и имитируют его работу. JTAGICE – эмулятор-отладчик, поддерживающий только микроконтроллеры со встроенным интерфейсом JTAG. Дополнительно обеспечивает возможность программирования микроконтроллеров. В отличие от ICE50, не эмулирует работу микроконтроллера, а подключается к имеющемуся микроконтроллеру на плате пользователя, т.е. обеспечивает наблюдение за работой конкретного экземпляра контроллера. ICE200 – несколько упрощенная версия ICE50 со слегка усеченным функционалом. AVR Dragon – отладочный комплекс в виде платы, на которой предусмотрена зона макетирования, т.е. в некоторых случаях непосредственно на плате этого устройства пользователь может собирать свои схемы. Обеспечивает поддержку отладки как по интерфейсу JTAG, так и Debug Wire, реализует все режимы программирования микроконтроллеров. Поддерживает все микроконтроллеры (часть – при помощи дополнительных средств). Особенности использования Как было сказано, аппаратные отладочные средства делятся на 2 типа: эмулирующие микроконтроллер и наблюдающие за микроконтроллером. Первый тип, не смотря на гибкость и широкий спектр поддержки контроллеров, не может быть на 100% полным аналогом, т.е. все равно в силу своей работы может иметь отклонения от поведения реальных кристаллов. Большинство таких отклонений известны и перечислены в соответствующих фирменных Радиолюбитель – 07/2010 СПРАВОЧНЫЙ МАТЕРИАЛ документах, однако полной гарантии в отсутствии новых нет. С другой стороны, «наблюдающие» через JTAG или Debug Wire отладчики категорически не подходят для отладки многих моделей контроллеров… Наконец, всем типам аппаратных средств все равно присуща одна главная проблема: допуская в любой момент приостановку исполнения программы, они тем самым нарушают «реальность» окружения микроконтроллера. Скажем, остановив контроллер, они не останавливают сигналы с датчиков, подключенных к нему. В этом случае программа исполняется все равно не в той среде, как при реальной работе устройства – и это следует учитывать при отладке. ПРОЦЕСС ОТЛАДКИ ПРОГРАММЫ Итак, рассмотрены практически все средства обеспечения отладки – от окна AVR Studio до внешних аппаратных отладчиков. Настала пора рассмотреть в деталях сам процесс отладки, т.е. как используются и взаимодействуют все рассмотренные средства. Начинается процесс отладки с нажатия кнопки или соответствующей горячей комбинации клавиш Ctrl-Shift-Alt-F5. Рабочее пространство AVR Studio при этом видоизменяется, подготавливаясь к процессу отладки. Если используются средства аппаратной поддержки – они инициализируются (их подключение должно быть сделано ранее). В окне с исходным текстом появляется желтая стрелка, указывающая на строку программы, готовую к исполнению: Теперь, в зависимости от намерений программиста, можно открыть любое из рассмотренных ранее окон для просмотра переменных, памяти, регистров и т.п. – на этом подготовительные операции завершены. Пошаговое исполнение программы После подготовки начинается, собственно, процедура отладки. Обычно она заключается в пошаговом исполнении программы, т.е. исполнению операторов одного за другим. Под пошаговым исполнением подразумевается то, что каждый оператор исполняется только после того, как программист даст на это команду – нажмет кнопку или (т.е. команды Step Into или Step Over – см. меню Debug – отладка). Содержимое окна немного изменится: Радиолюбитель – 07/2010 Как видите, указатель передвинулся на очередную строку программы (а сама строка выделилась). Дальнейшее нажатие клавиш F10 или F11 позволит последовательно исполнить и остальные операторы программы, наблюдая по ходу дела за изменениями, осуществляемыми ими над переменными. На рисунке показано содержимое регистра DDRB до исполнения оператора DDRB=255 и после: А окно программы при этом будет уже таким: Далее в программе следует оператор бесконечного цикла, в котором постоянно увеличивается на 1 содержимое PORTB, т.е. на выводах порта формируется возрастающая двоичная последовательность сигналов: Окно периферии Пошаговое исполнение может осуществляться и в том случае, когда открыто окно дизассемблера – в этом случае каждый шаг будет соответствовать одной ассемблерной команде. Следует отметить, что нормальная отладка возможна только при компиляции программы с отключенной оптимизацией (см. главу «Параметры компиляции проекта»). При включении оптимизации при отладке могут наблюдаться «чудеса»: то порядок исполнения строк программы не соответствует ожиданиям, то некоторые переменные недоступны для наблюдения в окне Watch, или же в какой-то строке программы невозможно поставить точку останова. Эти эффекты – следствие работы оптимизатора, который просто может выбросить за ненадобностью некоторые строки программы, изменить (не нарушая логику работы) последовательность выполнения операторов или удалить ненужные куски кода вообще. К сожалению, без оптимизации размер кода получается существенно больше, чем с оптимизацией, и для микроконтроллеров с малым объемом памяти никакой отладки вообще может не получиться. Тут придется идти на компромисс: либо отлаживать программу с «чудесами», стараясь уследить за тем, что она делает, либо собрать проект без оптимизации и отладить его на микроконтроллере, максимально близком к нужному, но с бОльшей памятью – так как многие контроллеры обладают сходной периферией, то погрешность такого метода минимальна. Автоматическое исполнение программы Кроме исполнения программы по шагам под контролем пользователя, имеются и режимы автоматического исполнения – как по шагам, так и в непрерывном режиме. Автоматическое исполнение по шагам заключается в том, что AVR Studio самостоятельно подает сама себе команды Step Into. Этот режим позволяет пронаблюдать, как программа исполняется – после каждого автоматического шага обновляются значения во всех окнах, и программист, наблюдая за этим процессом, может сделать какие-то выводы. Запускается автовыполнение по шагам кнопкой (Auto Step). Остановка этого процесса осуществляется командой Break (кнопка ). Необходимость обновления большого количества информации на дисплее делает этот режим достаточно медленным. Если необходимо отладить программу, содержащую большие участки уже проверенного кода или же длительные циклы, можно воспользоваться режимом автоматического исполнения, который включается командой Run (кнопка ). В этом случае вся информация во всех окнах и панелях «замораживается», в то время как программа «исполняется» на полной скорости. В случае использования аппаратных отладчиков происходит действительное исполнение программы, т.е. микроконтроллер работает на заданной тактовой частоте, а в случае эмуляции – «виртуальное» исполнение происходит на максимально возможной скорости эмуляции, обеспечиваемой мощностью компьютера. Остановить режим исполнения так же можно командой Break. Точки останова Кроме принудительной остановки исполнения программы, в котором не очень много пользы, имеется гораздо более удобный способ – указание точки останова (breakpoint). AVR Studio реализует 2 типа точек останова – программная (Program breakpoint) или по изменению данных (Data breakpoint). Программная точка останова просто помечает строку программы, дойдя до которой процесс автоисполнения будет остановлен, 55 СПРАВОЧНЫЙ МАТЕРИАЛ при этом сама строка еще не будет выполнена. Такие точки останова очень полезны при отладке долгих процессов, прерываний и т.п. В этом случае весь неинтересный для программиста код исполняется автоматически и достаточно быстро, а со строки, отмеченной точкой останова, отладка ведется по шагам. Остановка по изменению данных происходит лишь в том случае, когда программа изменит значение указанной переменной (или указанной области памяти). Этот режим очень полезен для поиска мест в программе, где происходит незапланированное изменение переменной. Например, в ходе отладки обнаруживается, что глобальная переменная tmp принимает значение, которое программист не предусматривал, в результате чего программа исполняется неверно. Если программа состоит из нескольких модулей, а каждый модуль – из сотен строк, то простым анализом исходного текста программы найти место этого изменения очень сложно, а если это связано с переменными-указателями, то может и вообще невозможно. В этом случае программист задает точку остановки по любому изменению переменной tmp и запускает программу на исполнение. Всякий раз, как только произойдет модификация содержимого переменной, автоисполнение будет прекращено на первом же операторе после модификации. Проанализировав это место в тексте программы и сопоставив при необходимости его с текущим содержимым других переменных, программист либо продолжает автоисполнение (если это место вне подозрений), либо приступает к исправлению найденной ошибки. Установка обычных точек остановки происходит простым нажатием кнопки (или командой Toggle Breakpoint), при этом строка, в которой находится курсор (текстовый, а не «мышиный»), отмечается красной точкой: Одновременно с этим в окне Breakpoints and Tracepoints появляется новая строка: В этом окне указано, что точка остановки установлена в модуле demo1.c в строке с номером 8, эта точка в настоящее время активирована (отмечена галочкой) и вызывает остановку всегда. Данное окно позволяет гибко управлять точками останова. Непосредственно в 56 нем можно удалить точку, выделив строку и нажав Del, или временно деактивировать, «сняв» галочку с нужной точки (в этом случае останова не будет происходить, хотя сама точка останется). Но гораздо больше возможностей предоставляет кнопка в этом окне (или команда Properties из всплывающего меню), которая открывает следующее окно: В этом окне можно настроить много параметров установленной программной точки останова. Начнем рассмотрение снизу вверх, т.к. внизу расположены общие для разных режимов опции. Во-первых, опция Enabled управляет активностью точки (галочка в списке). Во-вторых, имеется возможность не останавливать исполнение при проходе точки, а только обновить содержимое всех окон AVR Studio – за это отвечает опция Continue execution after the views have been updated. В-третьих, имеется возможность остановиться не сразу, а лишь после определенного количества проходов по точке (очень удобно при отладке циклов) – для этого следует указать в окне Break execution after значение, большее 1. Рядом с этим окном приводится для справки число проходов через точку к текущему моменту (на рисунке – 0 проходов). Теперь рассмотрим остальные опции сверху вниз. Bound to – указывает, к чему применяется точка. Варианты возможны такие: File – строка в файле модуля, Function – функция в модуле или Address – адрес машинного кода в программе. Переключения области точки изменяет вид остальной части окна, делая одни опции активными, а другие – недоступными. Если точка назначается строке в файле, то вы можете указать собственно имя файла в поле File и номер строки в нем – поле Line (но гораздо проще это сделать, как было сказано ранее – командой Toggle Breakpoint). Для точки на функции станет активным поле выбора функций программы – Function. Вы должны будете либо ввести имя функции в этом поле (в особом формате), либо, что удобнее, нажать кнопочку рядом с полем и выбрать функцию из списка (см. рисунок в следующей колонке). В этом окне в виде древовидной структуры показаны все функции проекта, достаточно выбрать нужную и нажать кнопку ОК. Наконец, если выбран конкретный адрес – активируется поле для его ввода. Следует учесть, что если окажется, что заданный адрес находится «внутри» какого-то оператора, то в окне текста программы никакой отметки соответствующей строки не будет, увидеть ее можно будет лишь в окне дизассемблера, однако остановка будет происходить все равно. Только вот с выделением строки, на которой произошла остановка, может возникнуть небольшая проблема: после оптимизации, как было сказано, не всегда имеется возможность однозначно определить, какому именно оператору Си соответствует конкретная ассемблерная команда. В этом случае после остановки может быть выделена строка с оператором, следующим за тем, внутри которого произошла остановка. Возможностей программных точек останова, как видите, достаточно немало, но возможностей остановки по изменению данных существенно больше! Установить точку остановки по изменению данных можно либо при помощи меню «Debug» (команда New Breakpoint – Data breakpoint), либо непосредственно в окне просмотра точек остановки Breakpoints and Tracepoints. Во втором случае для этого следует использовать кнопку или команду New из всплывающего меню. При любом способе открывается окно настройки параметров точки останова: Параметров, как видите, существенно больше, чем для программной точки. Самые нижние опции – точно такие же, как и ранее, а вот верхние следует рассмотреть подробно. Радиолюбитель – 07/2010 СПРАВОЧНЫЙ МАТЕРИАЛ Самый верхний элемент – список условий срабатывания точки останова – Break when (остановить, когда). В раскрывающемся списке перечислены все возможные варианты условий: · Location is accessed – осуществлен любой доступ к переменной · Location content is equal to a value – значение переменной совпадает с указанным значением · Location content is not equal to a value – значение переменной не равно указанному значению · Location content is greater than a value – значение переменной больше указанного значения · Location content is less than a value – значение переменной меньше указанного значения · Location content is greater than or equal to a value – значение переменной больше или равно указанному значению · Location content is less than or equal to a value – значение переменной меньше или равно указанному значению · Location content is within a range – значение переменной находится в указанном диапазоне · Location content is outside a range – значение переменной вне указанного диапазона · Bits of a location is equal to a value – определенные биты в переменной имеют заданные значения · Bits of a location is not equal to a value – определенные биты в переменной не совпадают с заданным значением Если для указанной переменной (в поле Location) выполняется выбранное условие – происходит срабатывание точки и автовыполнение программы останавливается (разумеется, лишь в том случае, если все другие условия этому не противоречат). В зависимости от того, какое именно условие выбрано, меняются остальные поля ввода значений. Так, например, для проверки битов в переменной, появляется поле ввода маски Bitmask (в котором надо отметить единичными значениями те биты, которые анализируются в переменной), а при проверке значения на попадание в диапазон – появляются поля ввода минимального и максимального значения. Value – это поле значения, с которым сравнивается указанная в Location переменная. Выбор переменной проще всего осуществить из списка, открывающегося по нажатию кнопки с многоточием рядом с полем Location – вид этого списка совпадает с ранее рассмотренным списком выбора функций, с той лишь разницей, что выбирать следует переменные (локальные или глобальные). Далее следуют поля и опции «тонкой» настройки режима контроля значения переменных. Поле Access type позволяет указать способ обращения к переменной, после которого осуществляется проверка условия. Есть три варианта: Read/Write (любое обращение к переменной), Read only (только Радиолюбитель – 07/2010 чтение) и Write only (только запись). В первом случае проверка происходит после любого обращения к переменной, во втором – только после считывания, в третьем – только после записи. Далее следует опция выбора способа контроля многобайтных переменных: MSB – только старший байт или Any byte – любой байт. Группа опций Custom data type (пользовательский тип данных) позволяет задать режим проверки переменных нестандартных типов. Активировав эту опцию, следует выбрать базовый тип переменной Base type и указать ее фактический размер в байтах Size. Это необходимо делать в тех случаях, если контролируются переменные не стандартных типов, а введенных пользователем. Группа опций Custom scope (область просмотра) позволяет указать область контролируемой памяти, задав начальный и конечный адреса – поля Start addr и End addr соответственно. Следует отметить, что в большинстве случаев пользователю не нужно производить настройку режимов контроля переменных, согласившись с параметрами по умолчанию. Возвращаясь немного назад, к окну просмотра содержимого памяти, следует сказать, что в его всплывающем меню имеется команда установки точки останова по изменению любой области памяти без привязки к конкретной переменной Add Data breakpoint. Пользоваться этой возможностью очень удобно, например, для контроля вершины стека программы. Когда происходит срабатывание точки остановки, она отмечается стрелочкой желтого цвета: Следует дополнительно отметить, что в окне Breakpoints and Tracepoints могут быть указаны (а в тексте программы – расставлены) не только точки остановки, но и точки трассировки (Tracepoints), для которых имеются соответствующие команды. Точка трассировки позволяет проследить момент «прохода» программы через указанную строку, т.е. позволяет получить в некотором смысле «протокол» исполнения программы. Однако эта возможность реализуется только при наличии средств аппаратной поддержки, в режиме «виртуальной» отладки недоступна и потому не рассматривается. Альтернативные средства отладки Не смотря на достаточно широкие возможности виртуального исполнения программы, часть проблем с их помощью решить невозможно без использования либо аппаратных средств, либо без натурных испытаний устройства. Частично эту проблему можно решить при помощи других средств отладки, наиболее интересным из которых следует признать программы симуляции электронных схем. К сожалению, все эти программы – исключительно коммерческие, т.е. далеко не бесплатные. Одна из очень удачных программ для этого – небезызвестный ISIS Proteus Professional фирмы Labcenter Electronics. Эта программа позволяет «нарисовать» принципиальную схему устройства с микроконтроллером, используя «интерактивные» элементы, а затем «загрузить» в микроконтроллер написанную программу и «подать питание» на схему. При этом программно моделируется поведение всех элементов – от микроконтроллера до резистора и транзистора, по возможности все происходящие в схеме изменения отображаются практически в реальном времени на дисплее – «загораются» светодиоды, «вращаются» моторы, динамики издают звуки и т.п. Кроме чисто визуальных средств можно использовать «виртуальные» инструменты – осциллограф, генератор сигналов и т.п. Эта программа позволяет не только увидеть внешние эффекты, демонстрирующие результат работы программы, но и так же, как и в AVR Studio, проводить отладку по шагам, просматривая содержимое переменных и памяти. К сожалению, изменить вручную содержимое переменных в этом случае невозможно. Начиная с версии AVR Studio 4.16 появилась возможность интеграции с установленным Proteus ISIS. Если протеус был уже установлен к моменту установки AVR Studio, то в списке платформ для отладки (см. главу «Мастер проектов» – раздел о выборе платформы отладки Select debug platform and device) появится платформа Proteus VSM Viewer. Если выбрать эту платформу, то при запуске отладки произойдет «внедрение» протеуса внутрь окна AVR Studio – при этом схему отлаживаемого устройства можно создать там, а отлаживать в студии. У этого гибрида масса достоинств – все плюсы отладки по точкам останова и просмотру/изменению переменных от студии и все плюсы точной имитации аналоговой периферии и схемы от протеуса. Недостаток только один – требования к памяти и мощности процессора компьютера. Так же порой эта связка может «упасть», т.е. обе программы завершаются по ошибке (редкое явление). Proteus был бы исключительно незаменимым средством для любого разработчика, если бы не был весьма дорогостоящим продуктом. Продолжение в №8/2010 57 СПРАВОЧНЫЙ МАТЕРИАЛ Книга по работе с WinAVR и AVR Studio Продолжение. Начало в №1-7/2010 ПРОБЛЕМЫ И ОГРАНИЧЕНИЯ Наибольшее количество проблем и ограничений имеет встроенный в AVR Studio эмулятор. Все они описаны в соответствующих сопроводительных файлах, но на английском языке, поэтому здесь приведены описания наиболее важных моментов. 1. Полностью или частично не эмулируются встроенные во все типы микроконтроллеров периферийные аналоговые устройства, а так же TWI и USI. 2. Полностью отсутствует поддержка эмуляции «теневых» регистров у всех микроконтроллеров. Это проявляется, в частности, в том, что в режимах Fast-PWM и Phase-correct PWM значение регистра OCR не обновляется при достижении счетчиком TCNT верхнего значения. 3. Не поддерживается эмуляция регистра ASSR, т.е. невозможна корректная отладка программ, использующих асинхронные режимы таймера. 4. Не для всех типов микроконтроллеров поддерживаются корректно обращения к 16-битным регистрам (в частности, не эмулируется «защелкивание» значения после чтения младшего байта). 5. Некоторые биты в регистрах, которые должны сбрасываться в ноль при записи 1, могут сбрасываться и при записи 0. 6. Эмулятор выводит сообщение всякий раз, когда осуществляется попытка выполнить инструкцию, не поддерживаемую выбранным микроконтроллером, что делает отладку практически невозможной. Отключить это невозможно, кроме как исправить программу пользователя. 7. В окне Watch значения массивов обновляются не всегда. Чтобы обновить значения массива, наблюдаемого в этом окне, необходимо выполнить «сворачивание» и последующее его «разворачивание». 8. Некоторые типы переменных не отображаются корректно (например, long long). 9. Не реализовано разделение секций кода для эмуляции режимов записи в память программ. 10. Поддерживается только один режим «сна» – Idle mode. 11. WDT поддерживается не для всех моделей микроконтроллеров. Для некоторых моделей корректные интервалы WDT реализовываются только для тактовой частоты 1 МГц. В некоторых случаях при истечении интервала WDT программа не останавливается на точке останова по вектору сброса. 12. Эмулятор допускает запись в регистры PINx, причем записанное значение сохраняется там. 13. Эмулятор некорректно обеспечивает работу с портами ввода-вывода, в которых физически недоступна часть битов – все 8 битов могут использоваться в программе. 14. Регистр UDR модуля USART (UART) не может быть модифицирован никаким способом «извне» – ни вручную пользователем, ни при помощи стимуляции портов. 15. Корректная работа с парой «совмещенных» регистров USRC и UBRRH возможна только в том случае, если запись в UBRRH осуществляется только после записи в UBRC. 16. Для всех микроконтроллеров не реализована эмуляция «удвоенной» скорости SPI. 17. Не реализовано отключение периферии при помощи регистра PRR – и «отключенная» периферия продолжает эмулироваться нормально. Все эти «нюансы» необходимо учитывать при отладке. Многие из них присущи только определенным типам микроконтроллеров – уточнить это можно, лишь обратившись к справочному файлу. Кроме этих, имеется ряд «глюков» самой AVR Studio – например, иной раз эта среда неожиданно выгружается без каких-либо сообщений. Повторный запуск позволяет продолжить работу, как ни в чем не бывало. К сожалению, автору неизвестны ни условия 56 Роман Абраш г. Новочеркасск E-mail: arv@radioliga.com возникновения этих ситуаций, ни методы борьбы с ними – все происходит необъяснимо случайно, но, к счастью, крайне редко. Для каждого аппаратного отладчика-эмулятора имеются свои отдельные ограничения и «нюансы» – они перечислены в соответствующей документации. Дополнительные средства Существует ряд бесплатных утилит, значительно облегчающих процесс написания и отладки программ для микроконтроллеров AVR. Практически все они сделаны энтузиастами, хотя имеются и фирменные. Поддержка LCD-индикаторов Фирма Atmel бесплатно предоставляет утилиту для «визуализации» работы с LCD дисплеями микроконтроллеров со встроенными драйверами ЖКИ. Это средство позволяет разработать собственный «виртуальный» индикатор, «подключить» его к микроконтроллеру (точнее, непосредственно к его драйверу) и проводить отладку, наблюдая за тем, как работает индикатор: Данное средство ориентировано на имитацию работы комплекта разработчика AVR Butterfly, однако может использоваться и без него. Чаще требуется применить графический ЖКИ с встроенным контроллером, которых в настоящее время выпускается большое количество. И основная проблема, с которой сталкивается программист в этом случае – это отсутствие готовых шрифтов для вывода на такие индикаторы текстовой информации. Решение в этом случае может заключаться в использовании генератора шрифтов LCD Font Generator: Эта утилита позволит легко создать оригинальный шрифт в виде массива констант, который затем уже можно использовать для вывода на ЖКИ. Программа ориентирована на другой компилятор Си – Code Vision CVAVR, однако получаемый код элементарно адаптируется и для WinAVR GCC. Программа создана программистом одной из арабских стран, поэтому в некоторых случаях пытается выводить текст арабской вязью, но, тем не менее, интерфейс очень прост и удобен. Генератор файлов симуляции внешних сигналов Утилита создания файлов стимуляции портов Stimuli Generator позволяет свести сложность создания этих файлов к минимуму. Она представляет собой графический редактор импульсных последовательностей. Интерфейс программы прост и нагляден (см. рисунок на следующей странице): Достаточно лишь задать в мегагерцах частоту микроконтроллера в поле MCU speed, установить шаг графика Display step и указать единицы измерения времени – микросекунды (us) или миллисекунды (ms). После этого можно «рисовать» Радиолюбитель – 08/2010 СПРАВОЧНЫЙ МАТЕРИАЛ мышкой произвольные импульсы. Скроллер Zoom позволит увеличить или уменьшить видимую область сигнала (т.е. он меняет масштаб по оси времени). После того, как сигналы нарисованы – надо сохранить их в файле, выполнив команду меню «File» Save или Save As (сохранить или сохранить с новым именем). Если необходимо – можно загрузить ранее созданный файл стимуляции командой Load и внести в него изменения. Мастер создания заготовок программ Очень мощное средство, позволяющее автоматически сгенерировать «скелет» программы в виде исходного текста на Си (и не только) – утилита AvrWiz. Интерфейс программы достаточно сложный, после первого запуска окно имеет следующий вид: Работа с утилитой заключается в том, что установкой тех или иных «галочек» активируется создание различных «заготовок» соответствующих функций и модулей программы. Помимо заготовки программы, утилита способна создать makeфайл, выполнить тестовую компиляцию созданной «заготовки» и кое-что еще. В верхней части окна программы имеются закладки, соответствующие разным возможностям. На закладке CPU задаются основные режимы работы, приводятся ссылки на сайт проекта и другая дополнительная информация. На закладке Generate указываются модули и функции, которые программа должна создать автоматически (см. рисунок в следующей колонке): Отметив опцию create AVR Studio project и нажав кнопку save, мы получаем уже подготовленный к загрузке в AVR Studio проект со всеми необходимыми файлами модулей, соответствующих отмеченным ниже возможностям: · 00_main – заготовка функции main() · 01_timer – заготовки функций настройки и обработки таймеров · 02_uart – заготовки функций для настройки и работы с модулем UART · 03_eeprom – заготовки функций для работы с EEPROM · 04_multitasking – заготовки функций поддержки «многозадачности» · 1wire – заготовки функций для работы с интерфейсом 1-Wire Радиолюбитель – 08/2010 · calculator – вспомогательные вычисления · delay/loop – заготовки функций программных задержек в виде циклов · fuses – вспомогательные функции для правильного выбора fuse-битов · HD44780U – заготовки функций поддержки ЖКИ на основе контроллера HD44780U · help – справочные сведения · i2c – заготовки функций поддержки интерфейса I2C · spi – заготовки функций работы с интерфейсом SPI · test – выполнение тестовой компиляции проекта-заготовки · twi – заготовки функций для работы с аппаратным модулем TWI Создание всех заготовок настраивается на соответствующих закладках, например, вот как выглядит закладка настройки таймеров: Вверху задается тип микроконтроллера, тактовая частота в мегагерцах и указывается язык, на котором необходимо создавать исходный текст (нас интересует Си – GCC). В настоящее время поддерживается еще и ассемблер. Ниже опциями указывается, необходима ли обработка прерываний (interrupt), надо ли создавать комментарии с подробным описанием кода (help). Затем выбирается номер таймера из числа доступных в выбранном микроконтроллере. Заключает все настройка самого режима работы таймера: можно либо ввести значение периода его переполнения (time) в машинных тактах (ticks) или интервалах времени, при этом прочие значения устанавливаются автоматически; либо указать конкретное значение предделителя (prescaler) и значение счетчика TCNT – тогда автоматически будет вычислено время его переполнения. В большом окне слева показан код, который будет соответствовать заданным настройкам – его можно скопировать и затем вставить в свою программу, а можно воспользоваться автогенерацией проекта, как было сказано ранее. Аналогично можно настроить и создать все прочие функции. 57 СПРАВОЧНЫЙ МАТЕРИАЛ ПРОГРАММИРОВАНИЕ МИКРОКОНТРОЛЛЕРОВ Итак, схема разработана и собрана, программа – написана и отлажена, и остается лишь вдохнуть жизнь в собранное устройство, т.е. записать в память микроконтроллера результат компиляции проекта. Этой цели служат специальные программаторы, т.е. устройства (и поддерживающие их программы), способные выполнить запись файла в микроконтроллер. В комплексе с AVR Studio наиболее удобно пользоваться средствами, предлагаемыми фирмой Atmel или другими, совместимыми с ними, т.к. они интегрируются в среду. Однако, часто не менее удобно, а порой и значительно удобнее использовать программаторы и программы сторонних разработчиков, которых великое множество, причем многие из них совершенно бесплатны. Плюсом этого варианта следует признать возможность самостоятельной сборки схемы программатора, в то время как фирменные средства придется покупать. Использование встроенных средств AVR Studio AVR Studio поддерживает следующие типы средств, способных осуществлять программирование микроконтроллеров: STK500, AVRISP, AVR Dragon и некоторые другие. Для всех этих средств используется единый интерфейс. Подключение соответствующего программатора осуществляется командой меню «Tools» Program AVR – Connect или Program AVR – Autoconnect. Первая команда позволяет указать вручную порт, к которому подключен программатор, а вторая автоматически перебирает все подходящие порты, осуществляя поиск подходящего устройства. Если автоматический поиск неудачен – открывается окно ручного подключения (то же самое, как и для первой команды): В левом списке Platform перечислены все поддерживаемые программаторы, а в правом – все порты, которые поддерживаются выбранным программатором. Auto – означает автопоиск порта. Выбрав платформу и порт, следует нажать кнопку Connect - произойдет подключение программатора. Чтобы поиск и подключение были успешными, следует заранее подать питание на программатор (если это предусмотрено). Если подключение успешно, то откроется окно программирования микроконтроллера (открыть это окно для ознако м л е н и я без возможнос ти реальной работы с программатором, т.е. без подключения его, м о ж н о кнопкой Disconnected Mode – отключенный режим): 30 Приведено окно для STK500 и совместимых с ним программаторов в «отключенном» режиме, для других типов программаторов содержимое некоторых закладок может отличаться. 58 Это окно по традиции содержит30 несколько закладок-страниц, а на каждой из них ряд опций и органов управления и настройки режимов работы программатора. Параметры программирования – Main На закладке Main (основные) позволяет указать тип микроконтроллера, который планируется программировать – верхний список в группе Device and Signature Bytes (устройство и сигнатурные байты). Кнопка Erase Device выполняет полное стирание всей памяти микроконтроллера, а кнопка Read Signature позволяет считать сигнатуру реально подключенного к программатору микроконтроллера и сравнить ее с правильной для выбранного типа. Если сигнатуры не совпадают – все функции программирования блокируются. Группа Programming Mode and Target Settings (режим программирования и настройки) позволяет указать режим программирования: ISP mode (последовательное внутрисхемное программирование) или PP/HVSP mode (параллельное или высоковольтное последовательное программирование). Разумеется, соответствующий режим должен поддерживаться программатором. Кнопка Settings позволяет настроить скорость (точнее – частоту) обмена информацией с микроконтроллером при последовательном внутрисхемном программировании. Нажатие этой кнопки открывает следующее окно: В этом окне из единственного открывающегося списка следует выбрать наиболее подходящую частоту обмена. Выбирать ее следует исходя из того, какой частотой тактируется прошиваемый микроконтроллер – частота для ISP-программирования должна быть минимум в 4 раза меньше тактовой частоты контроллера, несоблюдение этого правила может привести к невозможности программирования. Кнопка Read позволяет считать значение текущей частоты работы программатора, кнопка Write производит запись в программатор нового значения частоты. Функции программирования – Program Закладка Program имеет следующий вид: Здесь предоставлены все доступные функции программирования, рассмотрим их сверху вниз по порядку. Группа Device содержит к н о п к у полного стирания микроконтроллера E r a s e Device, а так же 2 опции, определяющие поведение программатора при выполнении остальных функций: · Erase device before flash programming – стирать контроллер перед записью программы Радиолюбитель – 08/2010 СПРАВОЧНЫЙ МАТЕРИАЛ · Verify device after programming – проверять запись после программирования Назначение этих опций очевидно. Далее имеются две очень похожих группы Flash и EEPROM, содержащих средства программирования соответственно памяти программ и данных микроконтроллера. Каждая из групп содержит одинаковые опции выбора источника данных: · Use Current Simulator/Emulator Memory – использовать текущее содержимое памяти эмулятора или симулятора · Input HEX File – входной HEX-файл Первая опция используется в случае использования аппаратного средства отладки, которое может выполнять функции программатора, вторая – во всех остальных случаях. Три кнопки Program (программирование), Verify (проверка) и Read (считывание) выполняют соответствующие функции. Считывание используется для записи в указанный файл содержимого из соответствующей области памяти. Последняя группа содержит средства работы с файлами в формате «elf». В отличие от формата HEX, файл этого формата может содержать одновременно данные для памяти программ и для данных, а так же битов защиты и конфигурации (см. далее), поэтому программирование обеих областей осуществляется сразу. В самой нижней области в текстовом формате выводятся сообщения о ходе выполнения соответствующих функций. Установка защиты – LockBits На данной закладке задаются биты защиты памяти от несанкционированного считывания. Описание вариантов приводятся в документации к микроконтроллеру. Единственное, о чем следует помнить, это то, что установка и считывание значений битов защиты возможна всегда, а снятие – только при полном стирании микроконтроллера. Установка fuse-битов – Fuses На этой закладке осуществляется конфигуриров а н и е встроенных аппаратных средств микроконтроллера при помощи так называемых fuse-битов. В большом верхнем окне перечислен список всех доступных для текущего режима программирования и выбранного микроконтроллера битов, а в нижнем представление этих же битов в виде, пригодном для записи в контроллер. Далее следуют три опции: · Auto read – автосчитывание значений fuse-битов. Если активировано, то при переходе на эту закладку состояния fuseбитов будут считаны из контроллера и показаны в окне. · Smart warning – «умные» предупреждения. Если активировано, то в случае задания опасных или несовместимых с другими значений fuse-битов, будут выведено соответствующее предупреждение. · Verify after programming – проверять после записи. К установке fuse-битов следует подходить с особой тщательностью, т.к. для режима последовательного внутрисхемного программирования существуют такие комбинации, программирование которых сделает невозможной любое последующее обращение к микроконтроллеру из программатора. В технической документации на каждый микроконтроллер fuse-биты подробно описаны, в рамках этой книги они не рассматриваются. Важно помнить, что в данном случае отмеченные галочкой fuse-биты соответствуют запрограммированным (т.е. активированным). Радиолюбитель – 08/2010 Дополнительные опции – Advanced На данной закладке прису тствуют дополнительные опции программирования. В частности, можно считать значения калибр о в оч н ы х б а й т о в встроенных RC-генераторов и записать их затем в соответствующую ячейку памяти программ или данных. Для чтения калибровочного байта следует выбрать соответствующий генератор из списка Calibrate for frequency и нажать кнопку Read – в окне Value будет показано считанное значение. Если указать в окне Address адрес ячейки памяти в памяти программ (активна опция Flash) или EEPROM (активна опция Eeprom), то после нажатия кнопки Write это значение будет записано в соответствующую ячейку. Параметры аппаратуры программатора – HW Settings (см. рисунок на следующей странице): На этой закладке присутствуют органы настройки напряжений, используемых программатором для различных режимов работы, а так же других параметров. Доступность этих регуляторов зависит от модели программатора. В группе Voltages имеется 3 регулятора напряжения: · VTarget – напряжение питания схемы с микроконтроллером · ARef – опорное напряжение для аналоговых цепей, формируемое аппаратурой (для программаторов совмещенных с отладчиком-эмулятором) 59 СПРАВОЧНЫЙ МАТЕРИАЛ · AREF1 – дополнительное опорное напряжение (см. документацию к аппаратному средству). Кнопка Read позволяет считать текущие значения нап р я же н и й из программатора/отладчика, а к н о п к а Write осуществляет запись новых значений. Регулятор Clock Generator позволяет изменить тактовую частоту контроллера программатора или отладчика. Наконец, кнопка Upgrade в группе Firmware Upgrade позволяет обновить прошивку программатора/отладчика. Сведения о версии – HW Info На этой закладке (рисунок не приводится) присутствуют две строки со сведениями о версии программного и аппаратного обеспечения используемого программатора/отладчика. Эти данные используются, если возникает необходимость в проверке наличия обновлений соответствующих средств. Автоматизация работы – Auto На данно й з а к л а д к е можно задать последовательность действий, выполняемых автоматически, что може т б ы т ь полезно при программировании серии микроконтроллеров. Для этого всего лишь необходимо отметить в списке те действия, которые необходимо выполнять, после чего нажать кнопку Start – отмеченные задачи будут выполнены последовательно сверху вниз. Если на каком-то этапе произойдет ошибка – дальнейшие действия не будут осуществлены. Опция Log to file (вести протокол работы в файле) используется, если необходимо формировать текстовый файл, содержащий сведения о ходе каждой операции. Если каждый раз файл следует перезаписывать, нужно активировать опцию Overwrite, а если следует дописывать к уже имеющемуся файлу – Append. Выбрать месторасположение и имя файла можно, нажав кнопку Browse. 60 Использование средств сторонних разработчиков Помимо встроенных в AVR Studio средств программирования, ориентированных на применение достаточно дорогого фирменного аппаратного обеспечения, существует большое количество бесплатных любительских средств, зачастую не менее функциональных. К сожалению, они не интегрируются в среду AVR Studio, а выполнены в виде отдельных программ. Аппаратное обеспечение для них обычно крайне простое, в некоторых случаях состоит всего из 4-6 деталей, но оказывается достаточным для многих случаев. PonyProg Наиболее известна программа PonyProg итальянского автора Клаудио Ланконелли. Программа использует несложные адаптеры, подключаемые к COM или LPT портам компьютера, и умеет работать практически со всеми микроконтроллерами AVR, а так же с большим количеством других микроконтроллеров и программируемых микросхем. Имеется версия программы с русифицированным интерфейсом, что является несомненным ее достоинством. PonyProg осуществляет только внутрисхемное последовательное программирование микроконтроллеров, режимы высоковольтного и параллельного программирования не поддерживаются. В настоящее время существует довольно большое количество недорогих «клонов» фирменных программаторов типа STK500, прибрести которые можно во многих интернет-магази′ нах. Однако, больший интерес для любителя может представлять клон, который можно изготовить самостоятельно – это, например, AvrUsb500, разработанный автором Petka (это интернет-ник). Об этом программаторе можно более подробно узнать в интернете по адресу: http://electronix.ru/forum/index.php?showtopic=68372. Этот клон фирменного комплекта разработчика STK500 является полностью совместимым с AVR Studio, которая опознает его как родной STK500. Следует только не забывать, что режимы «высоковольтного» и параллельного программирования этот клон (да и большинство других) не поддерживает. По ключевым словам «клон STK500» можно найти в интернете и другие варианты подобных программаторов, доступных для самостоятельной сборки. Продолжение в №9/2010 Радиолюбитель – 08/2010 СПРАВОЧНЫЙ МАТЕРИАЛ Книга по работе с WinAVR и AVR Studio Продолжение. Начало в №1-8/2010 МИГРАЦИЯ ПРОГРАММ В этой главе рассматриваются основные проблемы, связанные с миграцией проектов программ для микроконтроллеров, даются общие рекомендации по их преодолению. Под миграцией понимаются два процесса: 1. Имеется программа для одного компилятора, требуется адаптировать ее для другого. 2. Имеется программа для одного микроконтроллера, требуется адаптировать ее для другого. Первая ситуация имеет место в случае перехода программиста с одной системы программирования на другую – например, с платного компилятора CVAVR на бесплатный WinAVR. Несмотря на постоянно муссируемую тему о кроссплатформенности языка Си, о стандартах этого языка и т.п. еще никому не удавалось просто взять программу, написанную для одной версии компилятора, и перекомпилировать ее другим компилятором – всегда находятся какие-то препоны, требующие вмешательства. Связано это отчасти с тем, что каждый производитель программных средств самостоятельно решает, какому стандарту Си соответствовать, а в вопросах, не затрагиваемых стандартами, вообще царит полная анархия. Однако, влияние языка высокого уровня все-таки велико: усилия для миграции между различными компиляторами относитель′ Обычно бывает достаточно изменить имена некоторых но мало. функций и/или макросов, а так же имена каталогов и подключаемых файлов. Например, для упомянутого CVAVR константа в памяти программ определяется при помощи ключевого слова flash: flash unsigned int var; в то время как в WinAVR для этой цели служит макрос PROGMEM: PROGMEM unsigned int var; Несколько больше проблем возникает с теми местами программы, которые реализуют работу с такими «переменными». В WinAVR для чтения константы, описанной в памяти программ, используется специальный макрос, в то время как многие другие компиляторы выполняют необходимые операции автоматически. Сравните: // ó÷àñòîê ïðîãðàììû CVAVR flash unsigned int var = 12; for (int i = 0; i < var; i++) // àíàëîãè÷íûé ó÷àñòîê äëÿ WinAVR PROGMEM unsigned int var = 12; for (int i = 0; i < pgm_read_word(&var); i++) Разница, как видите, заметная, хотя принципиально переделка несложная. Аналогичная ситуация возникает и при работе с данными, сохраняемые в EEPROM: WinAVR для этих целей так же использует макросы-функции, а коммерческие компиляторы позволяют просто использовать значение «переменной», описанной в соответствующей памяти. Ситуация осложняется и другими «нюансами», связанными, например, с тем, как по умолчанию компилятор трактует тип char – как число со знаком или без. Этот, малозначительный на первый взгляд, факт может принести немало хлопот при отладке программы. Ответьте, какое значение получит переменная var после выполнения этого цикла: int var = 0; for (char i=250; i > 0; i++) var++; Увы, дать правильный ответ, не вспоминая об «истинном» типе char, невозможно: если компилятор считает его числом без знака, то var = 6, а если со знаком, то var будет равно 0. Радиолюбитель – 09/2010 Роман Абраш г. Новочеркасск E-mail: arv@radioliga.com «Мелочи», вроде способа описания функций-обработчиков прерываний можно даже и не упоминать: // Îáðàáîòêà ïðåðûâàíèÿ ïî ïåðåïîëíåíèþ òàéìåðà 0 â CVAVR interrupt [TIM0_OVF] void timer0_ovf_isr(void){ TCNT0=0x10; } // Îáðàáîòêà ïðåðûâàíèÿ ïî ïåðåïîëíåíèþ òàéìåðà 0 â WinAVR ISR (TIMER0_OVF_vect){ TCNT0=0x10; } Вывод, к сожалению, неутешительный: чтобы выполнить миграцию проекта с одного компилятора на другой, необходимо выполнить следующие шаги, проверяя результат пробной компиляцией проекта: 1. Проверить, имеются ли в наличии использованные в исходном тексте подключаемый файлы, доступны ли указанные для них каталоги. При необходимости заменить или изменить их на те, которые являются соответствующими аналогами для нового компилятора. Например, для WinAVR характерно использование #include <avr/io.h>, в то время как для многих других компиляторов используется #include <io.h>. 2. Найти и изменить имена всех функций исходного проекта, для которых существуют соответствующие аналоги в новом компиляторе. Например, функция задержки на несколько микросекунд для CVAVR delay_us() имеет аналог в WinAVR с идентификатором _delay_us(). 3. Найти и модифицировать в соответствии с особенностями нового компилятора все описания констант, переменных, макросов и функций, специфичных для старого компилятора. Например, ранее рассмотренные особенности, связанные с прерываниями, EEPROM и т.п. 4. Проверить (возможно, с использованием отладчика) корректность алгоритма программы, полученной после всех предыдущих этапов. В случае неверной работы обратить внимание на переменные типа char, режимы оптимизатора и т.п. Увы, для выполнения всех этих рекомендаций во многих случаях недостаточно знания особенностей только одного компилятора… Миграция программы между разными типами микроконтроллеров может потребоваться как по желанию разработчика, так и без, например, в случае снятия с производства устаревшей модели микроконтроллера. Когда фирма Atmel снимает модель контроллера с производства, то всегда предлагает ей адекватную (лучшую) модель. В этом случае публикуется специальный файл-рекомендация по миграции, в котором изложены все основные отличия между старой и новой моделями, и даются советы по адаптации программ. Если же разработчик принимает решение о замене микроконтроллера другим, это может быть связано с различными причинами, и проблем с миграцией возникает больше. Если исходный и новый микроконтроллеры имеют аналогичные наборы периферийных устройств, или новый имеет более развитую периферию – миграция практически не вызывает проблем. Максимум, что может потребоваться – это изменить идентификаторы, соответствующие регистрам управления периферией или битам этих регистров. Например, в некоторых моделях микроконтроллеров регистр управления АЦП имеет идентификатор ADCSRA, но в некоторых он видоизменен на ADCSR0A. Такие несоответствия устраняются элементарно – на каждое неверное описание компилятор отреагирует сообщением об ошибке. Больше проблем может принести ситуация, когда новый микроконтроллер имеет существенные отличия в периферии. В этом случае часто помогает лишь полная переделка всей программы. Например, если исходная программа была написана для микроконтроллера с тремя аппаратными таймерами, а требуется перенести ее на контроллер всего с двумя – это может быть очень серьезной 55 СПРАВОЧНЫЙ МАТЕРИАЛ проблемой! Не меньшей проблемой может быть разница в объеме памяти программ и/или ОЗУ. Дать осмысленные рекомендации для всех возможных вариантов не представляется возможным, но, как правило, без проблем переносятся программы между контроллерами одного семейства со сходными параметрами: с atmega8 на atmega88 или atmega48, с attiny13 на attiny25; почти всегда возможен перенос с «младшего» микроконтроллера на «старший»: с attiny25 на atmega48, с atmega8 на atmega16 и т.д. Перенос программ со «старшего» семейства контроллеров на «младшее» почти всегда невозможен или сильно затруднен: маловероятно, что программа для atmega16 будет адаптирована для attiny2313. Невозможен принципиально перенос программ, использующих уникальные особенности архитектуры отдельных микроконтроллеров, например программа, написанная для attiny15 (достаточно «слабого» микроконтроллера) и использующая его аппаратный синтезатор частоты для формирования ШИМ, никогда не сможет быть перенесена на гораздо более мощный микроконтроллер atmega16, в котором отсутствует такой синтезатор. СПРАВОЧНАЯ ИНФОРМАЦИЯ Информация для этого раздела собрана из различных источников, в том числе получена при переводе различной документации и сообщений на форумах, структурирована и снабжена авторскими комментариями и пояснениями. Большая часть информации о компиляторе WinAVR GCC и стандартных библиотечных функциях, являющихся его неотъемлемой частью, получена из оригинальной документации, входящей в его комплект. По материалам различных Интернет-форумов собраны наиболее часто задаваемые вопросы и приведены ответы на них. AVR-GCC Аббревиатура GCC расшифровывается как GNU Compiler Collection, что можно перевести как Коллекция Компиляторов по лицензии GNU, т.е. свободно распространяемое бесплатное программное обеспечение. Иначе говоря, GCC – это семейство бесплатных компиляторов. GCC поддерживает множество платформ, в том числе микроконтроллерных, практически все языки программирования (от ассемблера до Java), есть версии, работающие во всех известных операционных системах. В этой книге под GCC подразумевается только компилятор С для платформы AVR, запускаемый в операционной системе семейства Win32 – версия, получившая название avr-gcc. Особенности GCC для других платформ, операционных систем и языков могут упоминаться, но подробно не рассматриваются. Общие сведения о GCC для AVR GCC – это компилятор консольного типа, т.е. основная работа с ним осуществляется через ввод большого количества параметров командной строки. Далее будет рассмотрена часть параметров, специфичных для платформы AVR. GCC – универсальный компилятор, поддерживающий не только язык Си, но и ассемблер и расширения С++. Характерно, что компилятор самостоятельно определяет тип языка исходного текста (по расширению файла) и осуществляет «невидимый» запуск соответствующего модуля, являющегося дополнением к нему. Такими дополнениями может быть С или С++ препроцессор, компилятор ассемблера и т.п. Компилятор генерирует в результате своей работы либо ассемблерный файл, либо завершенный объектный файл. Каждый отдельный модуль компилируется в отдельный объектный файл. Для объединения объектных файлов в загружаемый образ или конечный объектный файл проекта служит компоновщик. Этот подход характерен для любых компиляторов, GCC не исключение. Традиционно для упрощения и автоматизации процесса компиляции и компоновки проекта, а так же для ряда вспомогательных задач, используется утилита make, для которой на специальном скриптовом языке описывается «программа» действий по сборке проекта. В комплект WinAVR она включена, однако здесь не рассматривается, т.к. большинство выполняемых ею функций реализуется в интерактивном виде при помощи графической среды разработки AVR Studio. 56 Особенности языка Си GCC полностью реализует требования стандартов, с которыми заявлена совместимость. Кроме этого в реализации С поддерживаются некоторые возможности, определенные стандартом только для С++. То есть программист может использовать некоторые возможности, не свойственные классическому С, достигая своих целей с меньшими затратами. Однако при этом следует помнить, что получаемые таким образом исходные тексты могут оказаться несовместимы с другими компиляторами, реализующими только стандартные возможности. Объявление и инициализация переменных Допускается использовать символ доллара «$» в именах переменных и функций. Однако это может сделать программу не совместимой с другими компиляторами. При описании переменных можно использовать присваивание им начальных значений не только в виде констант, но и вычисляемых на основе значений других переменных. То есть допускается такое определение: foo (float f, float g) { float beat_freqs[2] = { f-g, f+g }; /* ... */ } Интересной возможностью является объявление массивов с вычисляемым в процессе выполнения программы размером: FILE * concat_fopen (char *s1, char *s2, char *mode) { char str[strlen (s1) + strlen (s2) + 1]; strcpy (str, s1); strcat (str, s2); return fopen (str, mode); } Предусмотрена возможность выборочной инициализации элементов массива. Например, описывается массив из 10 элементов, в котором только 5-й и 10-й элементы должны быть проинициализированы определенными значениями, а остальные могут иметь значение по умолчанию (т.е. 0). Чтобы не писать лишнего, допускается использовать такую форму инициализации: int arr[10] = {[4]=15, [9]=25}; Это будет эквивалентно следующей традиционной записи: int arr[10] = {0,0,0,0,15,0,0,0,0,25}; Отдельно стоит отметить, что порядок выборочной инициализации может быть произвольным, т.е. сначала можно проинициализировать 7-й элемент массива, а затем первый: int arr[10] = {[6]=15, [0]=25}; Предусмотрен упрощенный вариант инициализации диапазона элементов массива одинаковыми значениями: int arr[10] = {[0 ... 4]=15}; // ïåðâûå 5 ýëåìåíòîâ èìåþò çíà÷åíèå 15 При инициализации структур так же можно осуществить выборочную инициализацию их полей: struct point { int x, y; }; point pt = {.y = 10 }; // ïîëå .x îñòàåòñÿ èíèöèàëèçèðîâàííûì ïî óìîë÷àíèþ Точечную нотацию для обращения к полям инициализируемой структуры можно использовать и в том случае, если инициализируется массив структур, причем делать это можно выборочно: struct point { int x, y; }; point ptarray[10] = {[3].y = 12, [4].x = 2}; Допускается определять структуры, не содержащие полей: struct empty { }; Радиолюбитель – 09/2010 СПРАВОЧНЫЙ МАТЕРИАЛ Разрешено определение объединения (union) без ключевого слова typedef: union foo { int i; double d; }; int x; double y; progmem Атрибут для переменной, указывающий на то, что переменная на самом деле есть константа, размещенная в памяти программ. Пример: char str[] __attribute__ ((progmem)) = “Ñòðîêà â ïàìÿòè ïðîãðàìì” union foo u; /* ... */ u = (union foo) x; // ðàâíîñèëüíî u.i = x u = (union foo) y; // ðàâíîñèëüíî u.d = y Расширения операторов Синтаксис некоторых операторов расширен по сравнению со стандартом Си. Например, в операторе switch можно использовать для case диапазон значений: switch(x){ deprecated Атрибут для переменной типа или функции для указания о том, что соответствующий объект не должен использоваться в текущем файле. Это может быть необходимо, если, например, переменная введена лишь для совместимости с другими версиями программы, или же в будущем должна быть удалена. Компилятор выводит предупреждение с указанием на строку программы в которой происходит обращение к переменной с этим атрибутом. Например, компилятор выведет предупреждение на вторую строку следующего кода: case 0 : // îáðàáîòêà îäíîãî çíà÷åíèÿ break; extern int old_var __attribute__ ((deprecated)); case 1 ... 5: // îáðàáîòêà äèàïàçîíà çíà÷åíèé int new_fn () { return old_var; } } Программист должен выделять троеточие пробелами, иначе это может быть интерпретировано неверно, если значения диапазона представляют собой числа. Допускается сокращенная запись оператора ?: x ? : y Результатом этого выражения будет x, если x не равно 0, и y в противном случае, т.е. сокращенная запись эквивалентна следующей: x ? x : y Пользоваться расширениями следует с осторожностью. Т.к. получаемый текст программы может быть несовместим с другими компиляторами. section Формат атрибута следующий: section(“имя секции”), где имя секции – имя любой определенной секции памяти. Атрибут указывает, что переменная должна размещаться в указанной секции. Обычно переменные размещаются в секциях .data или .bss. Программист может разместить переменную в любой иной секции, главное, чтобы эта секция была определена заранее. Пример: struct duart a __attribute__ ((section («DUART_A»))) = { 0 }; struct duart b __attribute__ ((section («DUART_B»))) = { 0 }; char stack[10000] __attribute__ ((section («STACK»))) = { 0 }; int init_data __attribute__ ((section («INITDATA»))) = 0; Данный атрибут применим к переменным и функциям. Атрибуты Атрибут – это особый символ, связанный с определенным объектом (переменной, функцией или типом) и определяющий особенности работы компилятора с этим объектом. Атрибут указывается при помощи специального ключевого слова __attribute__, за которым следует собственно список атрибутов, заключенный в двойные круглые скобки: __attribute__ ((список_атрибутов)) Список атрибутов может состоять из одного или нескольких идентификаторов атрибутов, часто в качестве таких идентификаторов используются макросы. В некоторых случаях используется макрос-подобная форма идентификатора, т.е. идентификатор с круглыми скобками, внутри которых указан какой-либо параметр. Несколько идентификаторов разделяются запятыми. Допустим и пустой список идентификаторов, такое назначение атрибута попросту игнорируется. Для различных объектов определены различные наборы допустимых атрибутов, хотя некоторые атрибуты могут назначаться и разным объектам, например, атрибуты для типов и переменных в своем большинстве не разделяются. packed Атрибут для переменной (или типа), определяющий, что переменной должно быть выбрано минимально возможное пространство памяти. Пример: int arr[10] __attribute__ ((packed)); unused Атрибут для переменной, указывающий, что переменная может быть неиспользована в тексте программы. Для определенных с таким атрибутом переменных GCC не выводит предупреждений. Пример: int some __attribute__ ((unused)); Радиолюбитель – 09/2010 alias Формат атрибута следующий: alias(“имя функции”), где имя функции – любое ранее определенное имя функции. Атрибут позволяет назначить новое имя (псевдоним) для функции. Следующий пример показывает, как при обращении к функции demo() будет происходить вызов функции __foo(): void __foo () { /* Do something. */; } void demo () __attribute__ ((weak, alias («__foo»))); weak Атрибут, ограничивающий видимость функции или ее псевдонима текущим модулем. always_inline Атрибут, указывающий, что функция всегда должна быть встраиваемой (inline). Обычно, пока не включена оптимизация, помеченные ключевым словом inline функции не будут таковыми. Более того, они будут включены в код лишь в том случае, если оптимизатор сочтет это необходимым в конкретном контексте программы. Указание же атрибута always_inline гарантирует, что функция всегда будет встроена в код. nacked Атрибут, указывающий компилятору, что для функции не требуется генерировать код пролога и эпилога, программист обязан самостоятельно выполнить необходимые действия. noreturn Атрибут, указывающий, что функция никогда не возвращает управление. Определено несколько функций, не возвращающих управление, например abort() или exit(). Эти функции «знакомы» компилятору. Но программист может определить и свои функции аналогичного поведения при помощи атрибута noreturn. В этом случае ком- 57 СПРАВОЧНЫЙ МАТЕРИАЛ пилятор генерирует максимально оптимальный код, в котором не производится формирование эпилога, т.е. восстановления стека, освобождения локально выделенной функцией памяти и собственно, инструкция RET, завершающей код любой функции. Примечание: функции, не возвращающие управление, не должны возвращать какое-либо значение, т.е. должны иметь тип void. Типы поддерживаемых GCC файлов Как было сказано ранее, GCC автоматически распознает тип содержимого файла по его расширению и выполняет соответствующие этому типу действия. Всего поддерживается более 40 типов файлов. В таблице 2 перечислены большинство из них с кратким описанием, жирным шрифтом выделены актуальные для программ на Си для AVR. Таблица 2 Расширение файла -с Компилировать файлы исходных текстов, но не выполнять их компоновку в объектный модуль. При этом генерируются объектные файлы для каждого файла с исходным текстом, результирующие файлы имеют расширение .o. Файлы, тип содержимого которых не определен по их расширению, игнорируются. -S Прекратить компиляцию на этапе получения ассемблерного текста. При этом для каждого файла с исходным текстом генерируется ассемблерный файл с расширением .s, генерация объектного файла не осуществляется. Файлы, тип содержимого которых не определен по их расширению, игнорируются. Описание .c Исходный текст на С, подлежащий обработке препроцессором .i Исходный текст на С, не подлежащий обработке препроцессором .ii Исходный текст на С++, не подлежащий обработке препроцессором .m Исходный текст на Objective-C, подлежащий обработке препроцессором .mi Исходный текст на Objective-C, не подлежащий обработке препроцессором .M, .mm Исходный текст на Objective-C++, подлежащий обработке препроцессором .mii Исходный текст на Objective-C++, не подлежащий обработке препроцессором .h Заголовочный файл для С, С++, Objective-C или Objective-C++ .cc, .cp, .cxx, .cpp, .CPP, c++, .C Исходный текст на С++, подлежащий обработке препроцессором .H, .hh, .hp, .hxx, hpp, .HPP, .h++, .tcc Заголовочный файл для С++ .F, .for, .FPP Исходный текст на фортране, не подлежащий обработке препроцессором .f90, .f95 Исходный текст на фортране-90/95, не подлежащий обработке препроцессором .F90, .F95 Исходный текст на фортране-90/95, подлежащий обработке препроцессором .ads, .adb Исходный текст модуля на языке Ада .s Исходный текст на ассемблере .S, .sx Исходный текст на ассемблере, подлежащий обработке препроцессором Опции командной строки Количество параметров командной строки для GCC вообще огромно и составляет значительно больше сотни! К счастью, многие из этих параметров не актуальны для платформы AVR. Часть опций подставляется автоматически при использовании AVR Studio (например, подключение библиотек, установка входного и выходного имени файла, указание путей поиска файлов и т.п.), поэтому здесь будут рассмотрены лишь те опции, которые программист должен при необходимости задавать самостоятельно. Опции, специфичные для расширения С++, не рассматриваются подробно, лишь упоминаются при необходимости. Важно, что параметры командной строки для GCC регистрозависимы, т.е. верхний и нижний регистр символов различается. Большое количество опций, начинающихся с –f или –W, имеют 2 формы: положительную и отрицательную, т.е. первая включает какой-то режим, а вторая – отключает его. Например, для «положительной» опции –ffoo ее «антипод» записывается с приставкой «no-», т.е. –fno-foo. Далее упоминается лишь одна из этих форм: та, которая не принята компилятором по умолчанию. Далее рассматриваются все опции, удовлетворяющие вышеперечисленным условиям и оговоркам, по функциональным группам. 58 Выходной формат Группа опций компилятора, определяющих его поведение. -E Прекратить компиляцию после завершения работы препроцессора. Генерация выходных файлов не происходит – результат направляется в поток стандартного вывода. Файлы, тип содержимого которых не определен по их расширению, игнорируются. Диалект С Группа опций, определяющих степень совместимости компилятора с различными версиями стандарта Си. -ansi Опция включает поддержку стандарта ANSI-C, для программ на Си это означает поддержку стандарта ISO C90. Опция отключает некоторые расширения GCC, например, поддержку ключевых слов asm или typeof, однако __asm__ и __typeof__ и другие аналогичные ключевые слова-синонимы продолжают функционировать. -std Опция позволяет выбрать поддерживаемый стандарт языка. Полный формат опции следующий: -std=<ключ>, где ключ – это один из указанных в таблице 3, выделенная жирным опция принимается по умолчанию, если опция –std не указана. Таблица 3 Kлюч Значение с89, iso9899:1990 ISO C90 (то же самое, что и опция –ansi iso9899:199409 ISO C90 в редакции с поправкой №1 c99, c9x, iso9899:1999, iso9899:199x ISO C99 (пока поддерживается не в полном объеме) gnu89 ISO C90 с расширениями (включая некоторые расширения С99) gnu99, gnu9x ISO C99 плюс расширения GNU. Эта опция станет умалчиваемой, когда С99 будет полностью поддерживаться GCC Предупреждения компилятора Группа опций компилятора, позволяющих управлять выводом предупреждений компилятора. Предупреждение обычно выводится в том случае, если в тексте имеется программная конструкция, не являющаяся ошибочной по синтаксису языка, но могущая привести к ошибке во время исполнения программы при определенном стечении обстоятельств. Многие особенности генерации предупреждений управляются при помощи опций-переключателей (например, начинающихся с – W). Для этих опций во многих случаях придется использовать «негативную» форму, чтобы отключить определенный вид предупреждений. GCC поддерживает более 100 опций управления предупреждениями компилятора. Формат книги не позволяет рассмотреть их все, поэтому рассмотрена только часть, по мнению автора, наиболее ходовых. Радиолюбитель – 09/2010 СПРАВОЧНЫЙ МАТЕРИАЛ -fsyntax-only Опция вынуждает компилятор контролировать только синтаксис, ничего более. -pedantic Опция включает проверку на соответствие всем расширениям выбранного стандарта. Не следует ожидать, что будет проведена проверка на соответствие всем требованиям стандарта – это неверно. Проверка происходит лишь на соответствие тем возможностям, которые выбранный стандарт требует проверять. -w Опция запрещает вывод всех сообщений об ошибках. Не рекомендуется использовать ее без необходимости. -Wno-import Опция блокирует вывод предупреждений о директиве #import -Wchar-subscripts Опция включает вывод предупреждения, если элемент массива имеет тип char. Это делается для того, чтобы предупредить распространенную ошибку программистов, связанную с тем, что они забывают о том, что во многих случаях тип char – это число со знаком. -Wcomment Опция включает вывод предупреждения всякий раз, когда внутри многострочного комментария, начинающегося с «/*» встречается снова символ начала такого комментария (т.е. снова «/*»); или когда в однострочном комментарии «//» встречается символ слияния строк «\» -Wformat Опция, включающая проверку соответствия строки-формата типам передаваемых переменных для вывода в функциях семейства printf() и scanf(). Опция имеет много дополнительных вариантов, которые могут использоваться для выключения некоторых предупреждений, если глобально включена опция –Wformat. -Wno-format-extra-args – выключает предупреждение, если в списке выводимых переменных больше значений, чем указано в строке формата. Стандарт Си подразумевает, что лишние значения будут отброшены. -Wno-format-zero-length – выключает предупреждение, если строка формата имеет нулевую длину. Стандарт Си допускает пустую строку формата. -Winit-self Опция включает вывод предупреждения, если переменная в момент описания инициализируется собственным значением, т.е. так: int some = some; Опция действует только в том случае, если включена опция – Wuninitialized, которая в свою очередь включается только при включенной оптимизации. -Wimplicit-int Опция вызывает вывод предупреждения, если для переменной не определен тип, т.е. происходит присвоение типа int по умолчанию. -Wimplicit-function-declaration Опция вызывает вывод предупреждения, если функция используется до того, как определена. Имеется сходная опция -Werror-implicit-function-declaration, которая вместо предупреждения в этом случае выводит сообщение об ошибке. Негативный вариант опции -Werror-implicit-functiondeclaration не поддерживается. -Wimplicit Опция, объединяющая действие двух опций сразу: -Werrorimplicit-function-declaration и -Wimplicit-int. Радиолюбитель – 09/2010 -Wmain Опция вызывает вывод предупреждения, если определение функции main() вызывает подозрения. Считается, что функция main() должна возвращать значение типа int и не иметь аргументов. Примечание: для платформы AVR данное требование к функции main() бессмысленно. -Wmissing-braces Опция вызывает вывод предупреждения, если инициализация многомерного массива осуществляется без должного количества фигурных скобок, например, в подобном случае: int a[2][2] = { 0, 1, 2, 3 }; // åñòü ïðåäóïðåæäåíèå int b[2][2] = { { 0, 1 }, { 2, 3 } }; // íåò ïðåäóïðåæäåíèÿ -Wparentheses Опция вызывает вывод предупреждения, если есть подозрение на отсутствие скобок или на неверное вложение операторов. Например, следующие операторы могут вызывать такое предупреждение: x<=y<=z if (a) if (b) foo (); else bar (); Первый пример эквивалентен (x<=y ? 1 : 0) <= z, однако есть подозрение, что программист надеялся получить иной результат. Во втором случае может оказаться, что программист надеялся, что else относится к верхнему оператору if(a), а не к if(b), как действительно считает компилятор. -Wsequence-point Опция вызывает вывод предупреждения, если встречается конструкция, допустимая синтаксически, но есть подозрение о том, что последовательность вычислений нарушена. Как известно, синтаксис Си допускает весьма витиеватые конструкции, чрезмерное увлечение которыми может создать неоднозначности, например: a = a++; a[n] = b[n++]; a[i++] = i; Примечание: всегда есть возможность реализовать тот же алгоритм без неоднозначных или запутанных конструкций. Наличие предупреждения – верный признак плохого стиля программирования. Ошибочно считать, что уровень программиста определяется его способностью писать код, с трудом разбираемый даже компилятором. -Wall Опция, активирующая все ранее рассмотренные опции генерации предупреждений (исключая те опции, которые отменяют определенные предупреждения). По умолчанию всегда используется при компиляции из AVR Studio, поэтому программист может добавлять лишь негативные опции для отключения части предупреждений. Оптимизация Опции управления оптимизацией позволяют выбрать те или иные возможности компилятора по уменьшению размера генерируемого кода и(или) уменьшению времени его исполнения. Как правило, требуется получить максимально быстрый код минимального размера. Гибкость GCC в плане оптимизации очень высока: он поддерживает более сотни различных опций. Как правило, необходимости в столь тонкой и тщательной настройки оптимизатора нет, тем более что есть ряд опций, оптимально объединяющих другие опции для достижения определенных целей. В эту группу включены так же опции, позволяющие осуществить дополнительную оптимизацию кода, но уже не за счет оптимизатора, а за счет осознанного желания программиста отказаться от каких-либо возможностей для этого. 59 СПРАВОЧНЫЙ МАТЕРИАЛ -O или –O1 Первый уровень оптимизации. Компилятор пытается предпринять действия, которые уменьшают объем кода и время его исполнения, но не требуют при этом длительного компилирования. -O2 Второй уровень оптимизации. Компилятор использует почти все средства оптимизации, кроме «развертывания» циклов и встраивания inline-функций. По сравнению с –O1 процесс компиляции длится существенно дольше, но генерируемый код обладает большей производительностью. -O3 Третий уровень оптимизации. Компилятор применяет все средства оптимизации для получения максимально быстрого кода, в том числе использует развертывание циклов и встраивание в код inline-функций. Размер генерируемого кода при этом увеличивается. -Os Оптимизация по размеру кода. Используются все средства оптимизации кроме тех, что приводят к увеличению размера кода. В большинстве случаев использование этой опции генерирует оптимальный код, т.е. компромиссно быстрый и компактный одновременно. Нередко этот код оказывается и самым быстрым. -O0 Оптимизация отключена. Компилятор не предпринимает никаких мер по уменьшению объема кода и по увеличению его производительности. -mno-interrupts Опция для генерации кода, не совместимого с системой обработки прерываний. Исключает из генерируемого кода запрещение прерываний при изменении указателя стека, уменьшая тем самым объем кода. В итоге в некоторых случаях возможно получение неработоспособного кода в программах с прерываниями. -mcall-prologues Опция, указывающая компилятору оформлять код пролога и эпилога функций в виде отдельных подпрограмм. Итоговый объем кода может при этом уменьшиться, но может возрасти время исполнения программы. -mno-tablejump Опция, запрещающая компилятору генерировать таблицы переходов. В некоторых случаях это позволяет уменьшить объем результирующего кода. -mint8 Опция уменьшения в 2 раза размера всех целых чисел. Таким образом, тип int и char будут однобайтными, long – 16-битным, а long long – 32-битным. Эта опция противоречит стандартам Си, однако позволяет получить меньший по объему код с лучшим быстродействием. Препроцессор Си Группа опций, управляющих работой препроцессора. -D Опция, позволяющая определить «внешний», т.е. не определенный внутри текста программы, макрос. Имеется две разновидности формата этой опции: -D <имя> - определяет <имя> как макрос со значением 1 -D <имя>=<значение> - определяет <имя> как макрос определенным значением. Например: -D debug -D clock=1000 Эти строки эквивалентны тому, как если бы в первых строках компилируемого текста программы находились бы следующие директивы: 60 #define debug 1 #define clock 1000 Примечание: имя макроса отделяется от опции минимум одним пробелом. -U Опция, отменяющая определение любого макроса: встроенного в GCC или определенного директивой –D. Формат опции следующий: -U <имя> - макрос <имя> перестает существовать для препроцессора. Примечание: имя макроса отделяется от опции минимум одним пробелом. Ассемблер Группа опций, которые позволяют указать параметры ассемблера, который вызывается GCC автоматически. -Wa Опция имеет следующий формат: -Wa,<параметр> Назначение опции – передать ассемблеру указанный после запятой параметр. Если этот параметр содержит запятые, происходит разбиение его на несколько параметров. То есть при помощи этой опции можно передать сразу несколько параметров, разделив их запятыми. -Xassembler Опция имеет следующий формат: -Xassembler <параметр> Назначение опции аналогично –Wa: передать параметр компилятору ассемблера. В отличие от –Wa эта опция не может передать несколько параметров, для каждой части составного параметра следует использовать отдельные опции –Xassembler. Примечание: параметр отделяется от опции минимум одним пробелом. Эта опция не анализируется GCC, что позволяет передавать ассемблеру нестандартные команды. Компоновщик Группа опций для управления процессом компоновки. -Wl Опция, позволяющая передать компоновщику список параметров точно так же, как –Wa. -Wl,-gc-sections Вариант использования опции –Wl для передачи компоновщику параметра -gc-sections. Этот параметр означает, что компоновщик должен удалить все секции памяти, к которым нет обращения из других секций. Это позволяет в некоторых случаях уменьшить объем результирующего кода. Платформа Группа опций, позволяющих настроить компилятор на поддержку определенной платформы. В настоящей книге рассматриваются только микроконтроллеры AVR в качестве платформы. -mcu Опция указания системы команд микроконтроллера. По умолчанию автоматически добавляется средой AVRStudio. Формат опции следующий: -mcu=<тип>, где тип – это обозначение системы команд. Определено 5 вариантов систем команд: avr1 – система команд микроконтроллеров «минимальной» конфигурации, не имеющих ОЗУ и потому поддерживаемых только ассемблером. К их числу относятся, например, AT90S1200, attiny12 и другие. avr2 – система команд «классических» AVR с объемом памяти программ до 8К. Этот набор используется по умолчанию, т.е. если опция –mcu отсутствует. В эту группу микроконтроллеров входят, например, at90s2313, at90s2323, attiny22 и др. Радиолюбитель – 09/2010 СПРАВОЧНЫЙ МАТЕРИАЛ avr3 –система команд классического ядра AVR с объемом памяти программ до 128К. avr4 – система команд «улучшенного» ядра AVR с объемом памяти программ до 8К. К этой группе относятся, например, контроллеры семейства mega: atmega8 и т.п. avr5 – система команд «улучшенного» ядра AVR с объемом памяти программ до 128К, например, atmega16, atmega128 и т.п. -mtiny-stack Опция указывает компилятору изменять только младший байт указателя стека, ограничивая тем самым его глубину. -minit-stack Опция позволяет назначить начальный адрес указателя стека. Формат опции следующий: -minit-stack=N, где N – значение указателя стека. N может быть числом или символом, по умолчанию используется символ __stack. Глобальные параметры компилятора Группа опций, оказывающих влияние на различные параметры компилятора, которые сложно отнести к ранее рассмотренным группам. -gdwarf-2 Для реализации отладки в AVR Studio необходимо использование опции генерации отладочной информации -gdwarf-2. GCC поддерживает кроме этой еще более 6 десятков опций управления отладочной информацией, однако их актуальность для платформы AVR и AVR Studio сомнительна, поэтому они не рассматриваются. -fshort-enums Оправляет способом организации перечисляемых типов. При использовании этой опции под переменную перечисляемого типа (enum) будет выделено минимально необходимое для хранения типа целое число. В некоторых случаях это позволяет уменьшить объем кода и требования к ОЗУ. Примечание: код, генерируемый в случае использования этой опции, не совместим с кодом, генерируемым по умолчанию. Это может вызвать проблемы при компоновке модулей, оттранслированных с этой опцией и без нее. -fshort-double Опция указывает, что тип double будет равнозначен типу float. Это может уменьшить объем кода и увеличить быстродействие программ, использующих сложные математические вычисления, однако в ущерб точности расчетов. -ffunction-sections Опция указывает компилятору выделять код каждой функции в отдельную секцию. Имя секции совпадает с именем функции. Самостоятельное использование этой опции не дает никакого эффекта, однако в совокупности с другими может дать существенный выигрыш в объеме кода. -fdata-sections Опция указывает компилятору выделять каждую переменную в отдельную секцию). Имя секции совпадает с именем переменной. Самостоятельное использование этой опции не дает никакого эффекта, однако в совокупности с другими может дать существенный выигрыш в объеме требуемой памяти (ОЗУ). Продолжение в №10/2010 Радиолюбитель – 09/2010 61 СПРАВОЧНЫЙ МАТЕРИАЛ Роман Абраш г. Новочеркасск E-mail: arv@radioliga.com Продолжение. Начало в №1-9/2010 AVR-LIBC ВВЕДЕНИЕ Комплект WinAVR содержит много различных средств для разработки программного обеспечения микроконтроллерных систем на основе микроконтроллеров AVR. Однако, сам по себе компилятор, каким бы хорошим и универсальным он ни был, не содержит главного: набора библиотечных функций. Существует некий набор библиотечных функций, ставший стандартным, т.е. набор, к которому привыкли все без исключения программисты Си, его так и называют стандартной библиотекой. Очевидно, для полноценной разработки ПО для микроконтроллеров AVR так же необходима такая стандартная библиотека. И она включена в состав WinAVR: это библиотека AVR-LIBC. Она представляет собой набор объектных модулей и соответствующих заголовочных файлов, обеспечивает большинство традиционно-стандартных функций, а так же ряд платформо-ориентированных расширений, т.е. функций, специфичных только для микроконтроллеров AVR. Стандартные функции порой имеют отклонения от привычного поведения в «компьютерном Си», что связано с ограничениями, накладываемыми архитектурой микроконтроллера. Программист должен быть осведомлен о таких отклонениях. Далее будут рассмотрены основные составляющие части библиотеки AVR-LIBC с необходимыми комментариями. Они разделены на три группы: Стандартные модули, AVR-специфичные модули и Вспомогательные модули. СТАНДАРТНЫЕ МОДУЛИ Стандартные модули – это модули, реализующие набор привычных функций, характерных для всех Си-программ вне зависимости от платформы. В этот раздел включен и ряд модулей, определяющих некоторые важные для GCC-компилятора константы, а так же модули, необходимые для всех прочих библиотечных модулей AVR-LIBC. Далее будет принят следующий формат изложения материала: - жирным шрифтом выделяется наименование раздела, как правило, совпадающего с соответствующим модулем библиотеки; - подчеркнутым шрифтом выделяется функция или макрос, опреджеляемый в рассматриваемом модуле; - после слова Определение приводится прототип функции, как он определен в соответствущем хидере. Далее следует подробное описание функции или макроса, иногда с пояснениями. Радиолюбитель – 10/2010 Книга по работе с WinAVR и AVR Studio alloca.h – Выделение памяти в стеке Определение: void * alloca(size_t size); Описание: функция выделяет size байт в области стека вызывающего модуля. Это временно выделяемое пространство автоматически освобождается, когда функция, вызывающая alloca(), возвращает управление в вызывающую ее функцию. alloca() реализована в виде макроса, который транслируется в inline-функцию __builtin_alloca(). Это означает, что нет возможности обращаться к функции по ее адресу или изменять ее поведение при связывании с другими библиотеками. Возвращаемое значение: функция возвращает указатель на начало выделенной области. Если происходит переполнение стека, поведение программы не определено. Предупреждение: избегайте использования функции внутри списка параметров функций. assert.h – Диагностика Определение: #include <assert.h> Описание: этот заголовочный файл определяет вспомогательные отладочные возможности. Так как в большинстве приложений нет стандартно заданного потока вывода для ошибок, генерирование печатаемых сообщений об ошибках по умолчанию отключено. Эти сообщения будут генерироваться только в том случае, если в приложении определен макрос __ASSERT_USE_STDERR прежде подключения файла <assert.h>. По умолчанию для остановки приложения должна использоваться только функция abort(). Применение: #define assert(<выражение>) Макрос assert() принимает в качестве параметра выражение, проверяет его на истинность и, если выражение ложно, вызывает функцию abort() для завершения приложения. При этом в стандартный поток вывода ошибок выводится соответствующее сообщение. Если выражение истинно, этот макрос не выполняет никаких действий. Все макросы assert() могут быть удалены из текста программы при компиляции с параметром –DNDEBUG. ctype.h – Символьные операции Тут определены различные функции над символами. Определение: #include <ctype.h> Описание: этот заголовочный файл подключает к проекту некоторые функции (см. далее детальное описание каждой) обработки символов. Функции разделены на две группы: классифицирующие символ и преобразующие символ. Классифицирующие функции следующие: isalnum() isalpha() isascii() isblank() iscntrl() isdigit() isgraph() islower() isprint() ispunct() isspace() isupper() isxdigit() Эти функции получают в качестве параметра символ, и возвращают истинное или ложное значение в зависимости от того, как этот символ проклассифицирован. Если параметр не является unsigned char – все функции возвращают FALSE. Преобразующие функции следующие: toascii() tolower() toupper() Эти функции так же получают в качестве параметра символ, а возвращают уже преобразованный символ. Функции модуля Обратите внимание на то, что параметр этих функций имеет тип int, хотя используется как символ, т.е. char. isalnum() Определение: int isalnum (int c) Описание: Проверяет, является ли аргумент алфавитно-цифровым символом. Результат эквивалентен (isalpha(с) || isdigit(c)) isalpha() Определение: int isalpha (int c) Описание: Проверяет, является ли символ алфавитным. Результат эквивалентен (isupper(c) || islower(c)) isascii() Определение: int isascii (int c) Описание: Проверяет, является ли параметр 7-битным ASCII-символом. isblank() Определение: int isblank (int c) Описание: Проверяет, является ли символ «пустым», т.е. является ли он пробелом или символом табуляции. iscntrl() Определение: int iscntrl (int c) 57 СПРАВОЧНЫЙ МАТЕРИАЛ Описание: Проверяет, является ли символ управляющим. преобразует в непредсказуемые, делая текст нечитаемым. isdigit() Определение: int isdigit (int c) Описание: Проверяет, является ли символ цифровым. tolower() Определение: int tolower (int c) Описание: Преобразует, если возможно, регистр символа к нижнему. isgraph() Определение: int isgraph (int c) Описание: Проверяет, является ли символ «псевдографическим». toupper() Определение: int toupper (int c) Описание: Преобразует, если возможно, регистр символа к верхнему. islower() Определение: int islower (int c) Описание: Проверяет, является ли «регистр» символа «нижним». errno.h – Системные ошибки Определение: #include <errno.h> Описание: многие библиотечные функции при возникновении ошибок устанавливают значение встроенной глобальной переменной errno. Подключение заголовочного файла errno.h вводит этим значениям символьные эквиваленты. Предупреждение: использование глобальной переменной errno в мультизадачных или многопоточных приложениях не является удачным. Проблема в том, что ее значение может быть изменено другим потоком в промежутке между ее установкой и началом анализа в исходном потоке. Примечание. Символьные константы, описанные в этом файле, не имеют практической ценности для начинающего разработчика, поэтому не приводятся – вы можете самостоятельно изучить их по содержимому указанного файла. isprint() Определение: int isprint (int c) Описание: Проверяет, является ли символ «печатаемым», т.е. любым, который можно отобразить на печатающем устройстве (включая пробел). ispunct() Определение: int ispunct (int c) Описание: Проверяет, является ли символ печатаемым, но при этом не принадлежит алфавитно-цифровым или «пустым». isspace() Определение: int isspace (int c) Описание: Проверяет, является ли символ символом-разделителем. В этой реализации AVR-LIBC в число этих символов входят: - пробел - пролистывание формы (form-feed, “\f”) - перевод строки (“\n”) - возврат каретки (“\r”) - горизонтальная табуляция (“\t”) - вертикальная табуляция (“\v”) isupper() Определение: int isupper (int c) Описание: Проверяет, является ли символ буквой в верхнем регистре. isxdigit() Определение: int isxdigit (int c) Описание: Проверяет, является ли символ допустимой шестнадцатеричной цифрой. toascii() Определение: int toascii (int c) Описание: Преобразует символ в 7-битный ASCII-символ путем очистки старших битов. Предупреждение: пользователи могут быть недовольны, если вы применяете эту функцию, т.к. многие символы (например, национальных алфавитов) эта функция 58 inttypes.h – Целочисленные преобразования Определение: #include <inttypes.h> Описание: этот заголовочный файл использует определения целых чисел stdint.h и расширяет их дополнительными вариантами, используемыми в работе компилятора. В настоящее время расширения включают 2 дополнительных целочисленных типа для «дальних указателей» (т.е. для указателей в коде, способных адресовать память более чем 64К), а так же все типы, поддерживаемые функциями форматированного вывода из stdio.h. Так как эти функции не реализуют полного комплекта спецификаций, определяемых ISO 9899:1999, включены только реализованные возможности. Идея подхода в том, что каждый тип, определенный в stdint.h, сопровождается макросами, позволяющими определить соответствующий объект форматирования для функций printf() или scanf(). Пример: #include <inttypes.h> uint8_t smallval; int32_t longval; ... printf(«The hexadecimal value of smallval is «PRIx8», the decimal value of longval is «PRId32 «.\n», smallval, longval); Дальние указатели для адресации более, чем 64К памяти typedef int32_t int_farptr_t – целое со знаком для хранения дальнего указателя. typedef uint32_t uint_farptr_t – целое беззнаковое для хранения дальнего указателя. Макросы для спецификатора форматов printf() и scanf() Для С++ эти макросы подключаются только в том случае, если определен символ __STDC_LIMIT_MACROS до подключения файла <inttypes.h>. Модуль содержит большое количество макросов для определения форматов ввода-вывода различных чисел. Т.к. эти макросы применяются в основном встроенными в библиотеку функциями, практическая их ценность для конечного пользователя невелика. Поэтому из рассмотрения в данном издании они опущены. math.h – Математика Определение: #include <math.h> Описание: этот заголовочный файл определяет некоторые базовые константы и математические операции. Функции, определенные этим файлом, требуют связывания с libm.a. Так же следует учитывать, что математические функции не генерируют исключений и не изменяют переменную errno. Для улучшения качества оптимизации GCC они реализованы в большинстве своем с атрибутом const. Определения модуля #define M_PI 3.141592653589793238462643 – число p #define M_SQRT2 1.4142135623730950488016887 – квадратный корень из двух #define NAN __builtin_nan(«») – константа определения «не числа» #define INFINITY __builtin_inf() – константа определения бесконечно большого значения Функции модуля cos() Определение: double cos (double x) Описание: возвращает косинус аргумента x (должен быть в радианах). fabs() Определение: double fabs (double x) Описание: возвращает модуль значения аргумента x. fmod() Определение: double fmod (double x, double y) Описание: возвращает остаток от деления x/y. modf() Определение: double modf (double x, double *iptr) Радиолюбитель – 10/2010 СПРАВОЧНЫЙ МАТЕРИАЛ Описание: функция разбивает аргумент x на целую и дробную части, каждая из которых имеет тот же знак, что и x. Функция возвращает дробную часть (со знаком), а целую часть (со знаком) помещает по указателю iptr. Примечание: если указатель iptr нулевой – возврат целой части пропускается. sin() Определение: double sin (double x) Описание: возвращает синус аргумента x (должен быть в радианах). sqrt() Определение: double sqrt (double x) Описание: возвращает не отрицательный квадратный корень аргумента x. tan() Определение: double tan (double x) Описание: возвращает тангенс аргумента x (должен быть в радианах). floor() Определение: double floor (double x) Описание: возвращает в виде числа с плавающей точкой ближайшее целое число, меньшее или равное аргументу x. ceil() Определение: double ceil (double x) Описание: возвращает в виде числа с плавающей точкой ближайшее целое число, большее или равное аргументу x. frexp() Определение: double frexp (double x, int *pexp) Описание: функция разбивает число x на два множителя: первый множитель – это число 0 или число в диапазоне (по модулю) от 0,5 до 1, – функция возвращает в качестве результата; второй множитель образуется как число 2 в степени pexp. Таким образом, если результат функции обозначить y, то x = y * 2pexp. Если x равно нулю – обе возвращаемые части равны нулю. Если x не является конечным числом – функция возвращает x, а pexp равно нулю. Если второй параметр функции NULL, то функция воспринимает это как указание не вычислять и не возвращать значение pexp. ldexp() Определение: double ldexp (double x, int exp) Описание: возвращает число x, умноженное на 2 в степени exp. exp() Определение: double exp (double x) Описание: возвращает е в степени x (экспоненциальная функция). Радиолюбитель – 10/2010 cosh() Определение: double cosh (double x) Описание: возвращает гиперболический косинус аргумента x. sinh() Определение: double sinh (double x) Описание: возвращает гиперболический синус аргумента x. tanh() Определение: double tanh (double x) Описание: возвращает гиперболический тангенс аргумента x. acos() Определение: double acos (double x) Описание: возвращает арккосинус аргумента x. Значение аргумента должно быть в пределах допустимых значений -1…+1. Возвращается значение в пределах -p/2…+p/2 радиан. asin() Определение: double asin (double x) Описание: возвращает арксинус аргумента x. Значение аргумента должно быть в пределах допустимых значений -1…+1. Возвращается значение в пределах -p/2…+p/2 радиан. atan() Определение: double atan (double x) Описание: возвращает арктангенс аргумента x. Возвращается значение в пределах -p/2…+p/2 радиан. atan2() Определение: double atan2 (double y, double x) Описание: возвращает арктангенс частного x/y. Возвращается значение в пределах -p…+p радиан. log() Определение: double log (double x) Описание: возвращает натуральный логарифм аргумента x. log10() Определение: double log10 (double x) Описание: возвращает десятичный логарифм аргумента x. pow() Определение: double pow (double x, double y) Описание: возвращает x, возведенное в степень y. isnan() Определение: int isnan (double x) Описание: возвращает 1, если аргумент x бесконечно малое значение, и 0 – в противном случае. isinf() Определение: int isinf (double x) Описание: возвращает -1, если аргумент x – «минус-бесконечность», 1, если аргумент «плюс-бесконечность» и 0 – в противном случае. square() Определение: double square (double x) Описание: возвращает x в квадрате. Примечание: это нестандартная для С функция. copysign() Определение: static double copysign (double x, double y) Описание: возвращает x, но со знаком от y. Эта функция работает даже если любой из аргументов (или оба) – бесконечность или пренебрежимо малые числа. fdim() Определение: double fdim (double x, double y) Описание: возвращает наибольшее значение из вариантов (x-y) или 0. Если хотя бы одно из чисел бесконечно мало – возвращается бесконечно малое число. fma() Определение: double fma (double x, double y, double z) Описание: функция «умножение с накоплением». Возвращает (x*y)+z, однако, в процессе вычислений промежуточный результат не округляется, что позволяет порой улучшить точность вычислений. fmax() Определение: double fmax (double x, double y) Описание: возвращает наибольший из аргументов. Если оба аргумента NAN - возвращается NAN, если один аргумент NAN, возвращается другой. fmin() Определение: double fmin (double x, double y) Описание: возвращает наименьший из аргументов. Если оба аргумента NAN - возвращается NAN, если один аргумент NAN, возвращается другой. signbit() Определение: int signbit (double x) Описание: возвращает 1, если знаковый бит у аргумента x установлен. Примечание: это не то же самое, что результат (x < 0.0), так как спецификация IEEE 754 допускает наличие знака у нуля! Значение (-0.0 < 0.0) ложно, но signbit(-0.0) возвратит ненулевое значение! 59 СПРАВОЧНЫЙ МАТЕРИАЛ trunс() Определение: double trunc (double x) Описание: возвращает округленное до ближайшего целого, не большего по модулю аргумента x, число. Врезка. Пример использования функций. #include <setjmp.h> jmp_buf env; // env – ïåðåìåííàÿ-ìåòêà int main (void){ if (setjmp (env)){ isfinite() Определение: static int isfinite (double x) Описание: возвращает ненулевое значение, если аргумент x – конечное число: не бесконечность (с любым знаком) и не NAN. hypot() Определение: double hypot (double x, double y) Описание: возвращает квадратный корень из суммы квадратов аргументов. Применение этой функции вместо ее эквивалента sqrt(x*x + y*y) более предпочтительно, так как обеспечивает лучшую точность. Исключается ошибка промежуточного округления, ошибки переполнения (при больших значениях аргументов) и «исчезновения» (при малых значениях) результата. round() Определение: double round (double x) Описание: возвращает округленное до ближайшего целого значение аргумента x, но при этом не учитывает правило «половины единицы». Переполнение не возникает. Примечание: если аргумент – целое число или бесконечность – оно и возвращается. Если аргумент NAN – возвращается NAN. lround() Определение: long lround (double x) Описание: возвращает целое число. В остальном аналогична round(), кроме того, что ошибка переполнения возможна. lrint() Определение: long lrint (double x) Описание: возвращает округленное до целого значение аргумента х с учетом «правила половины», т.е. 1,5 и 2,5 будут округлены до 2. Примечание: возвращаемое значение может быть LONG_MIN (т.е. 0x80000000), если аргумент – не конечное число или было переполнение. setjmp.h – Нелокальные переходы goto Подробное описание Когда в тексте программы применяется оператор goto, он позволяет осуществить переход только внутри функции, где он применен (т.е. локально). Чтобы осуществить переход в произвольное место программы, определены функции setjmp() и longjmp(). Их использование позволяет, например, реализовать более эффективно обработку ошибок и исключительных ситуаций при низкоуровневом программировании. 60 ... çäåñü äîëæíà îáðàáîòàòüñÿ îøèáêà ... } while (1){ ... îñíîâíîé öèêë, â êîòîðîì âûçûâàåòñÿ foo() ... } } ... void foo (void){ ... ÷òî-òî äåëàåòñÿ ... if (err){ // à åñëè ïðîèñõîäèò îøèáêà – ïðîèñõîäèò ïåðåõîä â ãëàâíóþ ôóíêöèþ longjmp (env, 1); } } Использование этих подходов может сделать программу сложной для понимания, а так же может привести к потере контекста глобальных регистровых переменных. Поэтому по возможности следует искать альтернативные пути. Пример использования функций см. на врезке: Функции модуля setjmp() Определение: int setjmp (jmp_buf jmpb) Параметры: jmp_buf jmpb – переменная сохранения контекста. Описание: функция сохраняет контекст стека и «окружения» в переменной jmpb для последующего использования в функции longjmp(). Сохраненный контекст теряется, если происходит возврат из функции, использовавшей setjmp(). Функция возвращает 0, если возврат произошел немедленно, и не 0, если возврат произошел при вызове longjmp() с использованием сохраненного контекста. longjmp() Определение: void longjmp (jmp_buf jmpb, int ret) __ATTR_NORETURN__ Параметры: jmp_buf jmpb – переменная сохранения контекста. int ret – значение, используемое для возврата. Описание: функция осуществляет нелокальный переход к сохраненному ранее контексту. Возврата из функции в традиционном понимании нет, нет и возвращаемого значения. Функция восстанавливает окружение (т.е. состояние среды) в том виде, как сохранено ранее в переменной jmpb, после чего осуществляет передачу управления так, как будто завершилась функция setjmp() в том месте, откуда была вызвана, но с возвратом значения ret. Примечаниe от автора Не смотря на некоторую гибкость возможностей, предлагаемых функциями модуля setjmp.h, настоятельно не рекомендуется применять их, особенно в случае, когда общая квалификация программиста невысока (т.е. начинающими). stdint.h – Стандартные целые числа Модуль вводит описания различных типов целых чисел, определяемых стандартом С99. Просто используйте для определения переменных разрядностью N бит (где N может быть 8, 16, 32 или 64) следующую форму записи типа: [u]intN_t. Например, uint8_t – эквивалент unsigned char; а int16_t – эквивалент signed int. Кроме того, вводится большое количество макросов и констант, которые можно использовать в своих программах. Типы целых чисел Следующие типы вводят (определяют) обозначения-синонимы типам целых чисел по размеру в битах. Любой из этих типов может быть представлен стандартным эквивалентом, однако вводимые типы имеют более короткую запись, что, несомненно, более удобно: int8_t – байт со знаком (8 бит) uint8_t – байт без знака (8 бит) int16_t – целое число со знаком (16 бит) uint16_t – целое число без знака (16 бит) int32_t – длинное целое число со знаком (32 бита) uint32_t – длинное целое число без знака (32 бита) int64_t – большое длинное число со знаком (64 бита) uint64_t – большое длинное число без знака (64 бита) Вводятся 2 типа для указателей: intptr_t – то же самое, что и int16_t, используется для хранения указателя uintptr_t – то же самое. Что и uint16_t, используется для хранения указателя Радиолюбитель – 10/2010 СПРАВОЧНЫЙ МАТЕРИАЛ Ряд типов для чисел, содержащих числа с числом битов не менее определенного: int_least8_t – для чисел со знаком не более чем из 8 битов uint_least8_t – для чисел без знака не более чем из 8 битов int_least16_t – для чисел со знаком не более чем из 16 бит uint_least16_t – для чисел без знака не более чем из 16 бит int_least32_t – для чисел со знаком не более чем из 32 битов uint_least32_t – для чисел без знака не более чем из 32 битов int_least64_t – для чисел со знаком не более чем из 64 битов uint_least64_t – для чисел без знака не более чем из 64 битов Определения типов для чисел заданной размерности. Обрабатывающихся с максимальной скоростью31 (расшифровка типов не приводится ввиду явной очевидности): int_fast8_t uint_fast8_t int_fast16_t uint_fast16_t uint_fast32_t int_fast64_t uint_fast32_t Определения типов целых чисел, способных хранить максимально возможные числа среди поддерживаемых знаковых или беззнаковых: intmax_t – наибольшее целое со знаком (аналог int64_t) uintmax_t – наибольшее целое без знака (аналог uint64_t) Константы В модуле определен ряд констант (макросов), определяющих предельные значения для чисел каждого типа. Данные константы для программ на С++ подключаются только в том случае, если определено __STDC_LIMIT_MACROS до подключения модуля stdint.h. INT8_MAX – максимальное число типа int8_t INT8_MIN – минимальное число типа int8_t UINT8_MAX – максимальное число типа uint8_t INT16_MAX – максимальное число типа int16_t INT16_MIN – минимальное число типа int16_t UINT16_MAX – максимальное число типа uint16_t INT32_MAX – максимальное число типа int32_t INT32_MIN – минимальное число типа int32_t UINT32_MAX – максимальное число типа uint32_t INT64_MAX – максимальное число типа int64_t INT64_MIN – минимальное число типа int64_t UINT64_MAX – максимальное число типа uint64_t Аналогичные константы предельных значений определены и для всех других типов чисел (приводятся без описания): INT_LEAST8_MAX INT_LEAST8_MIN UINT_LEAST8_MAX INT_LEAST16_MAX INT_LEAST16_MIN UINT_LEAST16_MAX INT_LEAST32_MAX INT_LEAST32_MIN UINT_LEAST32_MAX INT_LEAST64_MAX INT_LEAST64_MIN UINT_LEAST64_MAX INT_FAST8_MAX INT_FAST8_MIN UINT_FAST8_MAX INT_FAST16_MAX INT_FAST16_MIN UINT_FAST16_MAX INT_FAST32_MAX INT_FAST32_MIN UINT_FAST32_MAX INT_FAST64_MAX INT_FAST64_MIN UINT_FAST64_MAX 31 Это не более чем условность – на самом деле вводимые «скоростные» типы, как и все предыдущие, есть всего лишь синонимы ранее определенным, т.е. обрабатываются компилятором абсолютно с той же скоростью. Очевидно, эти типы введены для совместимости с GCC для других платформ. Радиолюбитель – 10/2010 INTPTR_MAX INTPTR_MIN UINTPTR_MAX INTMAX_MAX INTMAX_MIN UINTMAX_MAX Имеется так же ряд констант максимальных значений для специальных типов (эти определения для С++ так же требуют наличия определения __STDC_LIMIT_MACROS до подключения модуля stdint.h): PTRDIFF_MAX PTRDIFF_MIN SIG_ATOMIC_MAX SIG_ATOMIC_MIN SIZE_MAX Макросы задания числовых констант Модуль определяет ряд макросов-функций, позволяющих определять числовые константы заданного размера без использования «суффиксов» размерности: INT8_C(value) – константа 8-битная со знаком UINT8_C(value) – константа 8-битная без знака INT16_C(value) – константа 16-битная со знаком UINT16_C(value) – константа 16-битная без знака INT32_C(value) – константа 32-битная со знаком UINT32_C(value) – константа 32-битная без знака INT64_C(value) – константа 64-битная со знаком UINT64_C(value) – константа 64-битная без знака INTMAX_C(value) – константа наибольшего размера со знаком UINTMAX_C(value) – константа наибольшего размера без знака Использование этих макросов вместо простых чисел позволит избежать ошибок, связанных с определением размерности результата вычислений с константами. Продолжение в №11/2010 61 СПРАВОЧНЫЙ МАТЕРИАЛ Роман Абраш г. Новочеркасск E-mail: arv@radioliga.com Продолжение. Начало в №1-10/2010 stdio.h – Стандартный ввод-вывод В силу направленности avr-libc на микроконтроллеры, т.е. на среду без операционной системы и любых стандартных средств ввода-вывода, имеются большие отличия в реализации функций модуля по сравнению с требованиями стандарта. Реализован только необходимый минимум функций. Имеется ряд условностей и ограничений, связанных с аппаратной и архитектурной особенностью среды. Для максимальной компактности генерируемого кода многие возможности усечены или исключены полностью, например, касающиеся форматированного ввода-вывода функциями printf() и scanf(). Часть возможностей этих функций может быть включена или выключена путем задания определенных опций (параметров) компилятора, чтобы получать код минимального размера с необходимым минимумом функциональности. Стандартные потоки stdin, stdout и stderr поддерживаются «виртуально», т.к. нет никакой аппаратной их поддержки, нет и никакой инициализации этих потоков; т.к. нет реально существующих файлов вообще – нет и fopen() и т.п. Имеется функция fdevopen(), которая позволяет связать стандартные функции ввода-вывода с реализуемыми программистом средствами поддержки конкретной аппаратуры. В качестве альтернативы можно использовать макрос fdev_setup_stream(), при помощи которого производится инициализация средств ввода-вывода. В любом случае программист самостоятельно должен реализовать функцию ввода, вывода или обе только для одного символа (нет различий между символьным и двоичным потоками), которая затем и используется для работы со стандартными потоками. Более подробно об этом будет сказано далее, при рассмотрении соответствующих функций. Так же с целью получения наиболее компактного кода, имеется ряд функций, способных работать сразу со строковыми константами, определенными в сегменте кода программы. Реализация библиотеки, как было сказано ранее, не делает различий между текстовыми и двоичными операциями вводавывода, поэтому следует быть осторожными при использовании автоматических преобразований, реализуемых функциями ввода-вывода, например, автоматической вставки символа перевода строки. Этот символ будет нарушать нормальную двоичную последовательность, поэтому для двоичных потоков надо исключить любую «автоматику» преобразований. Если же после этого потребуется выводить текст в потоки, 56 Книга по работе с WinAVR и AVR Studio символы перевода строки и возврата каретки следует вставлять принудительно. Первое обращение к fdevopen() для чтения приведет к фактической связи с потоком stdin. Обращение же к fdevopen() на запись приведет к тому, что результирующий поток будет связан с stdin и stderr, т.е. фактически оба этих потока будут на самом деле одним (stdin и stderr в данном контексте – просто синонимы). Соответственно закрытие функцией fclose() любого из этих потоков фактически закроет и его синоним. Данный подход позволил существенно оптимизировать результирующий код: во-первых, за счет исключения дублирования структур данных потоков; во-вторых, нет необходимости в функции fprintf(); в-третьих, можно отказаться от передачи через стек лишнего параметра (указателя на используемый поток). В сущности, это оправдано особенностями архитектуры микроконтроллера. Имеется возможность связать с потоком дополнительные пользовательские данные при помощи fdev_set_udata(), которые затем могут быть извлечены в реализациях функций ввода-вывода символа при помощи fdev_get_udata() и обработаны, как требуется. Это, например, позволит реализовать одну функцию ввода-вывода, работающую с различными UART. В этом случае функции ввода-вывода сохраняют контекст различных UART в области данных пользователя между своими вызовами. По умолчанию fdevopen() подразумевает использование malloc(), что часто очень нежелательно в ограниченных ресурсах микроконтроллера. Поэтому в библиотеке реализован опциональный вариант fdevopen() без использования malloc(). Макрос fdev_setup_stream() позволяет настроить стандартные функции ввода-вывода для работы с подготовленным заранее буфером файла. Пример см. на врезке. Врезка. Пример: #include <stdio.h> // ïîäêëþ÷åíèå ìîäóëÿ ââîäà-âûâîäà static int uart_putchar(char c, FILE *stream); // îïðåäåëÿåìàÿ ïîëüçîâàòåëåì // ôóíêöèÿ âûâîäà ñèìâîëà // ïîäãîòîâêà ïðè ïîìîùè ìàêðîñà ñòðóêòóðû «ôàéëà» äëÿ ââîäà-âûâîäà static FILE mystdout = FDEV_SETUP_STREAM(uart_putchar, NULL, _FDEV_SETUP_WRITE); // ðåàëèçàöèÿ ôóíêöèè âûâîäà ñèìâîëà static int uart_putchar(char c, FILE *stream){ // ïðèíóäèòåëüíàÿ âñòàâêà ñèìâîëà «âîçâðàò êàðåòêè» ïîñëå ñèìâîëà // «ïåðåâîä ñòðîêè» äëÿ íîðìàëüíîãî îòîáðàæåíèÿ â òåðìèíàëüíîé ïðîãðàììå if (c == ’\n’) uart_putchar(’\r’, stream); loop_until_bit_is_set(UCSRA, UDRE); // îæèäàíèå ãîòîâíîñòè UART UDR = c; // âûâîä ñèìâîëà return 0; } // îñíîâíàÿ ôóíêöèÿ int main(void){ init_uart(); // èíèöèàëèçàöèÿ UART stdout = &mystdout; // ñâÿçûâàíèå ïîòîêà stdout ñ ïîäãîòîâëåííûì «ôàéëîì» printf(«Hello, world!\n»); // èñïîëüçîâàíèå ñòàíäàðòíîé ôóíêöèè âûâîäà return 0; } Приведенный пример выводит в стандартный поток, связанный с аппаратно-реализованным UART микроконтроллера, текстовое сообщение. В примере применена инициализация потока mystdout макросом FDEV_SETUP_STREAM() вместо макроса fdev_setup_stream(), поэтому вся инициализация потока происходит в момент инициализации переменных С-программы, т.е. совершенно прозрачно для пользователя. Если инициализированный таким образом поток более не нужен, он может быть разрушен макросом fdev_close(). Не требуется использование fclose(), т.к. это потребует от компоновщика подключения модуля stdlib.h – Стандартные возможности для «освобождения» памяти, что в данном случае совершенно лишнее. Определения модуля В модуле определены следующие константы и макросы: FILE FILE – определение структуры «файл», т.е. структуры, передаваемой в различные функции ввода-вывода в качестве идентификатора файла. stdin stdin – поток, используемый упрощенными (т.е. не требующими параметра stream) функциями ввода. stdout stdout – поток, используемый упрощенными (т.е. не требующими параметра stream) функциями вывода. Радиолюбитель – 11/2010 СПРАВОЧНЫЙ МАТЕРИАЛ stderr stderr – поток, используемый для вывода сообщений об ошибках. По умолчанию соответствует stdout. Если необходимо направить сообщения об ошибках в другой, отличный от stdout, поток, необходимо присвоить ему другое значение, возвращенное fdevopen(), не закрывая имеющееся (т.к. иначе это привело бы к закрытию stdout). EOF EOF – константа «конец файла». Это значение возвращается функциями ввода в качестве индикатора ошибки. Так как в текущей реализации для AVR не существует реального понятия «файл», иное использование этой константы – бессмысленно. fdev_set_udata() fdev_set_udata(stream, u) – макрос вставки пользовательских данных (указатель) u во внутреннюю структуру файла stream. Использование этих данных возможно в функциях get() и put(). fdev_get_udata() fdev_get_udata(stream) – макрос возврата пользовательских данных (указателя) из внутренней структуры файла stream. fdev_setup_stream() fdev_setup_stream(stream, put, get, rwflag) – макрос, настраивающий буфер для использования стандартными функциями ввода-вывода. Аргумент stream типа FILE должен быть заранее подготовлен пользователем, в отличие от аналогичной структуры, выделяемой fdevopen() динамически. Аргументы put и get аналогичны передаваемым в fdevopen(). Аргумент rwflag – это флаги-признаки режима открытия потока. Может принимать одно из трех значений: · _FDEV_SETUP_READ – для чтения · _FDEV_SETUP_WRITE – для записи · _FDEV_SETUP_RW – записи и чтения _FDEV_ERR _FDEV_ERR – константа ошибки чтения потока. Используется в функции чтения fdevopen(). _FDEV_EOF _FDEV_EOF – константа ошибки при попытке чтения за концом потока. Используется в функции чтения fdevopen(). FDEV_SETUP_STREAM() FDEV_SETUP_STREAM(put, get, rwflag) – макрос, аналогичный fdev_setup_stream(), только инициализирующий переменную потока путем присвоения ей «возвращаемого» значения макроса. Параметры макроса аналогичны параметрам fdev_setup_stream(). fdev_close() fdev_close() – макрос, освобождающий любые ресурсы, связанные с открытым потоком. В текущей реализации он не делает Радиолюбитель – 11/2010 ничего, но введен для совместимости с будущими реализациями. putc() putc(c, stream) – макрос «упрощенного» вывода в поток, полностью аналогичный (и являющийся синонимом) fputc(). putchar() putchar(c) – макрос вывода символа в поток stdout. getc() getc(stream) – макрос «упрощенного» ввода из поток, полностью аналогичный (и являющийся синонимом) fgetc(). getchar() getchar() – макрос, возвращающий символ, считанный из потока stdin. Спецификация строки формата для функций форматированного вывода Строка формата ввода-вывода состоит из последовательностей символов двух типов: обычных, которые копируются в поток непосредственно, и последовательностей символов-директив (или управляющих символов), которые в процессе обработки заменяются другими в соответствии с определенными правилами. Формат управляющей последовательности следующий (в [квадратных скобках] указаны элементы, которые могут отсутствовать, жирным выделены элементы-модификаторы, прочие символы – обязательные): %[префикс][ширина][.точность][суффикс]тип Префикс определяет общие характеристики преобразования формата. Список префиксов (может быть одновременно несколько): # – использовать альтернативный формат. Для типов c, d, i, и s игнорируется. Для типа o означает, что точность представления числа должна быть увеличена так, чтобы первый выводимый символ был нулем. Для типов x или X означает, что выводимое число будет предваряться символами “0x” или “0X” соответственно. 0 – выравнивание нулями. Означает, что если задана минимальная ширина представления числа, то все незначащие левые символы будут заменены нулями (вместо пробелов). Для типов d, i, o, u, i, x, и X данный модификатор игнорируется. - – флаг отрицательного значения. Означает, что выводимое значение выравнивается влево в пределах заданной ширины, дополняясь справа пробелами. Если применяются оба модификатора 0 и -, последний имеет приоритет. “ “ (пробел) – означает, что положительное число должно предваряться слева пробелом (т.е. резервирует место под возможный символ минуса для отрицательных чисел). Применяется для типов d или i. + – означает, что положительные числа всегда должны предваряться знаком плюс. Если использованы оба модификатора + и пробел, первый имеет приоритет. Ширина – это десятичное число, которое определяет минимальное количество символов, требуемое для представления числа. Если число может быть представлено меньшим количеством символов – оно дополняется слева или справа (в зависимости от префикса) пробелами (или нулями – см. префикс 0). Если для представления числа необходимо больше символов, то значение ширины игнорируется. Т.е. если указана ширина 3 и задан префикс 0, то число 1 будет выведено как “001”. Точность – это десятичное число, определяющее символов в представлении для достижения заданной точности. Для строковых аргументов определяет максимальную длину строки, для числовых – минимальное кол-во значащих цифр. Пару ширина-точность для чисел с плавающей точкой можно рассматривать как пару «общее кол-во знаков»-«кол-во знаков после точки». Суффикс – это символ h или l. h – игнорируется, а l означает, что аргумент имеет тип long, а не int. Тип – это символ, который определяет тип очередного элемента в списке выводимых аргументов. Определены следующие типы (должен указываться любой из перечисленных): diouxX – целое число. Аргумент рассматривается, как целое число, и выводится в десятичном со знаком(di), десятичном без знака (u), восьмеричном (о) или шестнадцатеричном (xX) формате. Тип Х означает, что шестнадцатеричные цифры-буквы должны выводиться в верхнем регистре (т.е. число 0x0a1b2c3d будет выведено как “A1B2C3D”). Если задана точность, то представление числа дополняется слева нулями для ее достижения. p – указатель. Аргумент трактуется, как void * и выводится, как обычное десятичное число в шестнадцатеричном формате, т.е. последовательность %р полностью аналогична управляющей последовательности %#x с – символ. Аргумент трактуется как unsigned char и выводится соответствующий символ. s – строка. Аргумент трактуется как указатель на строку. В поток выводятся все символы вплоть до завершающего строку символа NULL (который не выводится). Если задана точность – выводится не более указанного количества символов, при этом наличие символа NULL в строке не требуется. S – строка в сегменте кода. Аргумент трактуется как указатель на строку, размещенную в сегменте кода (см. тип s), в отличие от стандартного размещения в ОЗУ. % – символ %. Никаких преобразований не требуется, очередной аргумент так же не требуется – это просто вывод символа % в поток (т.е. управляющая последовательность %% предназначена для вывода в поток одного символа %). eE – экспоненциальная форма записи числа с плавающей точкой. Аргумент типа double при необходимости округляется и выводится в формате [-]d.ddde±dd, где d – 57 СПРАВОЧНЫЙ МАТЕРИАЛ цифра. Число цифр после точки определяется точностью (если не задана – принимается равной 6), если точность равна 0, то дробная часть не выводится и десятичная точка так же не выводится. Если тип Е, то в экспоненциальной форме используется этот символ (взамен е). Показатель степени – всегда 2 знака. fF – простая форма записи числа с плавающей точкой. Аргумент типа double при необходимости округляется и выводится в формате [-]ddd.ddd, где d – цифра. Число цифр после точки определяется точностью (если не задана – принимается равной 6), если точность равна 0, то дробная часть не выводится и десятичная точка так же не выводится. Если выводится точка, то минимум одна цифра всегда выводится перед ней. gG – версия fF или eE (соответственно с учетом регистра) с некоторыми особенностями. Число цифр после точки определяется точностью (если не задана – принимается равной 6), если точность равна 0, то она принимается равной 1. выбор между вариантами fF и eE осуществляется автоматически: форма fF используется, если число не более 1000000, или показатель степени 10 для числа меньше или равен значению точности. Примечания. 1. Ни при каких условиях не происходит усечения точности результата для вывода чисел: если заданная ширина недостаточна для представления числа – она игнорируется и выводится необходимое число знаков. 2. Для ширины и точности имеется ограничение – они не могут быть более 255. 3. Если использована не полная версия функции vprintf() при сборке проекта, а в списке аргументов присутствует число типа double, в потоке вывода на этом месте будет символ «?», т.е. краха не произойдет. 4. Так как суффикс h игнорируется (по стандарту он предназначен для принудительного приведения к типу char), эффективность кода повысится, если никогда его не использовать в строке формата. 5. Суффикс l приведет к прекращению вывода в поток. Т.к. текущая реализация не поддерживает тип long long. 6. Переменная ширина или точность не реализованы, поэтому символ * в поле ширины или точности приведет к прекращению вывода в поток. Спецификация строки формата для функций форматированного ввода Строка формата для функций ввода имеет строение, аналогичное строке формата вывода (см. Спецификация строки формата для функций форматированного вывода), но состоит из следующих частей: %[префикс][размер] [тип] Префикс – это одна или более из числа следующих последовательностей: * – означает, что преобразование должно быть выполнено, но результат должен 58 быть проигнорирован, т.е. не должен помещаться в элемент списка аргументов h – означает, что аргумент в списке имеет тип short int (а не int) hh – означает, что аргумент имеет тип char (а не int) l – означает, что аргумент имеет тип long int (а не int) для целочисленного ввода или что аргумент имеет тип double для ввода чисел с плавающей точкой. Размер – это десятичное число от 1 до 255, которое показывает, какое минимальное количество символов потока должно быть считано для интерпретации соответствующего значения. Тип – это один из следующих символов: % – означает, что должен быть введен символ процента, преобразование не осуществляется d – означает, что должно быть введено целое (возможно, со знаком) число в десятичном представлении, т.е. очередной аргумент в списке имеет тип int i – означает, что должно быть введено целое (возможно со знаком) число в десятичном, восьмеричном или шестнадцатеричном формате, аргумент имеет тип int. Формат числа определяется по первым символам: если число начинается с 0x или 0X принимается шестнадцатеричный формат, если число начинается с 0 (????) – восьмеричный, иначе принимается десятичный формат. o – означает, что вводится восьмеричное целое число (аргумент имеет тип unsigned int) u – означает, что вводится беззнаковое целое число, аргумент имеет тип unsigned int x – означает, что вводится число в шестнадцатеричном представлении, аргумент имеет тип unsigned int e или E, или f, или F, или g, или G – означает, что вводится число в формате с плавающей точкой, аргумент имеет тип float s – означает, что вводится последовательность символов, не содержащая разрывов, аргумент представляет собой указатель на буфер достаточной длины, чтобы вместить всю последовательность вместе с завершающим символом NULL. Ввод строки прекращается, как только встретится любой символ разрыва строки, например, пробел, табуляция и т.п. с – означает, что должна быть введена последовательность символов, количество которых определено полем размер. Аргумент, как и для s, должен указывать на буфер достаточной длины, однако завершающий NULL не вводится. Вводятся все символы, включая разделительные. Чтобы пропустить начальные пробелы используйте принудительно вставленный в строку формата пробел перед управляющей последовательностью. p – означает, что вводится значение указателя. Ожидается, что последовательность символов будет соответствовать той, что выводится в аналогичном случае функцией vfprintf(). n – не вводится ничего, вместо этого в аргумент (типа int) заносится количество обработанных к настоящему моменту символов потока. Никаких преобразований не выполняется. Это действие может быть пропущено при использовании префикса *. [множество] – означает, что должна быть введена последовательность, состоящая из символов, указанных между скобками []. Аргумент должен указывать на буфер достаточного объема. Чтобы вместить все символы, включая автоматически добавляемый NULL в конце. Множество формируется путем простого перечисления символов в виде строки, однако, есть особенности: 1. Если первый символ за открывающей скобкой «^» (птичка), то остальные символы образуют множество символов, исключаемых из числа вводимых. 2. В множество не включается символ закрывающей квадратной скобки. Чтобы все же добавить его в множество, следует поместить его сразу после открывающей скобкой или после «птички». В любом ином случае эта скобка обозначит конец множества. 3. Чтобы добавить в множество набор подряд идущих символов, следует указать первый символ, затем через дефис – последний. Примеры использования множеств: [[0-9] – ввод последовательности, состоящей из символов «0», «1», «2», «3», «4», «5», «6», «7», «8», «9» «[« [^0-9] – ввод последовательности из любых символов, кроме цифр [][] – ввод последовательности, состоящей из символов квадратных скобок Функции модуля fdevopen() Определение: FILE * fdevopen (int(*put)(char, FILE *), int(*get)(FILE *)) Параметры: int(*put)(char, FILE *) – указатель на функцию вывода символа в поток (если имеется намерение использовать создаваемый поток для вывода). Эта функция имеет два параметра: выводимый символ и указатель на открытый поток. Эта функция должна возвращать 0 в случае удачного вывода, и не ноль в случае ошибки. int(*get)(FILE *) – указатель на функцию для ввода символа (если имеется намерение использовать поток для ввода). Эта функция имеет один параметр – указатель на открытый поток, и должна возвратить введенный символ в случае успешного завершения. В случае попытки считать «за концом данных» функция должна вернуть _FDEV_EOF, в случае иных ошибок функция должна возвращать _FDEV_ERR. Возвращаемое значение: в случае успеха – указатель на структуру открытого потока в динамической памяти, иначе NULL. Описание: Эта функция заменяет fopen(). Она позволяет открыть поток для случая, когда необходимо реализовать собственную поддержку аппаратной части для ввода-вывода. В случае успешного завершения функция возвращает указатель на Радиолюбитель – 11/2010 СПРАВОЧНЫЙ МАТЕРИАЛ структуру FILE, которая затем может использоваться, как соответствующий поток в других функциях. Если недостаточно динамической памяти, или не определена ни одна из функций put и get (т.е. попытка открыть поток вообще без намерения его использовать) – функция завершается неудачно. Первый поток, открытый для ввода автоматически ассоциируется с потоком stdin, первый поток, открытый для вывода, автоматически ассоциируется с потоками stdout и stderr. Примечания: 1. Функция использует динамическое выделение памяти при помощи calloc() (и/ или malloc()). 2. Для совместимости с кодом, написанным для avr-libc версии 1.2 или более ранней, следует определить __STDIO_FDEVOPEN_COMPAT_12 до подключения модуля stdio.h. Это делается исключительно для обеспечения совместимости и упрощения миграции кода, в новых проектах не используйте это! fclose() Определение: int fclose (FILE *stream) Параметры: FILE *stream – указатель на ранее открытый поток Возвращаемое значение: всегда 0 Описание: функция закрывает поток, открытый функцией fdevopen() и тем самым делает невозможной любой ввод-вывод через него. Примечания: эта функция не должна использоваться для потоков, открытых с помощью fdev_setup_stream() или FDEV_SETUP_STREAM(). vfprintf() Определение: int vfprintf (FILE *stream, const char *fmt, va_list ap) Параметры: FILE *stream – указатель на открытый для вывода поток const char *fmt – указатель на строку формата va_list ap – указатель на список выводимых аргументов Возвращаемое значение: число выведенных в поток символов в случае успешного завершения или EOF в противном случае (это может быть, если поток не открыт для вывода). Описание: это базовая функция форматированного файлового вывода для всего семейства printf-функций. Она реализует символьный вывод в поток stream списка аргументов, заданного как ap в соответствии с форматом, заданным строкой fmt (см. Спецификация строки формата для функций форматированного вывода). Примечания: полная реализация всех возможностей форматирования требует больших затрат программной памяти. С целью минимизации ресурсов существует 3 варианта реализации функции vfprintf(), Радиолюбитель – 11/2010 выбираемых компоновщиком во время сборки проекта: - обычная (по умолчанию) – реализует все виды преобразований, кроме предназначенных для чисел с плавающей точкой; - усеченная – реализует только базовые преобразования форматов целых чисел и строк, кроме того, может применяться только префикс # (см. Спецификация строки формата для функций форматированного вывода); - полная – реализующая полную спецификацию форматов. Усеченная версия выбирается следующими параметрами компилятора: -Wl,-u,vfprintf -lprintf_min Полная версия выбирается следующими параметрами компилятора: -Wl,-u,vfprintf -lprintf_flt -lm vfprintf_P() Определение: int vfprintf_P (FILE *stream, const char *fmt, va_list ap) Параметры: FILE *stream – указатель на открытый для вывода поток const char *fmt – указатель на строку формата в сегменте кода va_list ap – указатель на список выводимых аргументов Возвращаемое значение: число выведенных в поток символов в случае успешного завершения или EOF в противном случае (это может быть, если поток не открыт для вывода). Описание: это версия функции vprintf(), которая использует строку формата, размещаемую не в ОЗУ, а в сегменте кода программы. fputc() Определение: int fputc (int c, FILE *stream) Параметры: int c – символ для вывода FILE *stream – открытый для вывода поток. Возвращаемое значение: в случае успеха возвращает выведенный символ, в противном случае возвращает EOF. Описание: Функция выводит в поток символ (1 байт, хотя параметр имеет тип int). Примечания: printf() Определение: int printf (const char *fmt,...) Описание: данная функция осуществляет форматированный вывод в поток stderr. Параметры и подробности см. в описании vfprintf(). printf_P() Определение: int printf_P (const char *fmt,...) Описание: данная функция осуществляет форматированный вывод в поток stderr, используя строку формата в сегменте кода. Параметры и подробности см. в описании vfprintf_P(). vprintf() Определение: int vprintf (const char *fmt, va_list ap) Описание: данная функция осуществляет форматированный вывод в поток stdout. Параметры и подробности см. в описании vfprintf(). sprintf() Определение: int sprintf (char *s, const char *fmt,...) Описание: данная функция осуществляет форматированный вывод в строку s (область памяти для строки-результата должна иметь достаточный размер!). Подробности об остальных параметрах функции и ее особенностях см. в описании vfprintf(). sprintf_P() Определение: int sprintf_P (char *s, const char *fmt,...) Описание: данная функция осуществляет форматированный вывод в строку s (область памяти для строки-результата должна иметь достаточный размер!), используя строку формата в сегменте кода. Подробности об остальных параметрах функции и ее особенностях см. в описании vfprintf_P(). snprintf() Определение: int snprintf (char *s, size_t n, const char *fmt,...) Описание: вариант функции sprintf() с контролем размера строки. Параметр n определяет максимальное количество символов (включая завершающий NULL), которое будет помещено в строку s. В остальном (по параметрам и поведению) полностью аналогична функции sprintf(). snprintf_P() Определение: int snprintf_P (char *s, size_t n, const char *fmt,...) Описание: вариант функции sprintf_P() с контролем размера строки. Параметр n определяет максимальное количество символов (включая завершающий NULL), которое будет помещено в строку s. В остальном (по параметрам и поведению) полностью аналогична функции sprintf_P(). vsprintf() Определение: int vsprintf (char *s, const char *fmt, va_list ap) Описание: аналог функции snprintf(), но со списком аргументов, передаваемых переменной ap. vsprintf_P() Определение: int vsprintf_P (char *s, const char *fmt, va_list ap) Описание: вариант vsprintf(), но для строки формата в сегменте кода. vsnprintf() Определение: int vsnprintf (char *s, size_t n, const char *fmt, va_list ap) 59 СПРАВОЧНЫЙ МАТЕРИАЛ Описание: вариант snprintf() с контролем количества выводимых символов, как в snprintf() и списком аргументов, передаваемых переменной ap. vsnprintf_P() Определение: int vsnprintf_P (char *s, size_t n, const char *fmt, va_list ap) Описание: вариант snprintf() с контролем количества выводимых символов, как в snprintf() и списком аргументов, передаваемых переменной ap. Строка формата размещается в сегменте кода. fprintf() Определение: int fprintf (FILE *stream, const char *fmt,...) Описание: функция осуществляет форматированный вывод в поток stream. См. подробности об особенностях и параметрах в vfprintf(). fprintf_P() Определение: int fprintf_P (FILE *stream, const char *fmt,...) Описание: функция осуществляет форматированный вывод в поток stream, причем строка формата размещается в сегменте кода. См. подробности об особенностях и параметрах в vfprintf(). fputs() Определение: int fputs (const char *str, FILE *stream) Параметры: const char *str – указатель на строку FILE *stream – указатель на открытый для вывода поток Возвращаемое значение: возвращает 0 в случае успешного завершения, в противном случае возвращает EOF. Описание: Функция выводит в поток stream строку str. Завершающий символ NULL не выводится. fputs_P() Определение: int fputs_P (const char *str, FILE *stream) Описание: вариант fputs() для вывода строки из сегмента кода. puts() Определение: int puts (const char *str) Параметры: const char *str – указатель на строку Возвращаемое значение: возвращает 0 в случае успешного завершения, в противном случае возвращает EOF. Описание: функция выводит в поток stdout строку str, дополняя ее символом перевода строки. puts_P() Определение: int puts_P (const char *str) const char *str – указатель на строку Возвращаемое значение: возвращает 0 в случае успешного завершения, в противном случае возвращает EOF. 60 Описание: функция выводит в поток stdout строку str из сегмента кода, дополняя ее символом перевода строки. fwrite() Определение: size_t fwrite (const void *ptr, size_t size, size_t nmemb, FILE *stream) Параметры: const void *ptr – указатель на первый выводимый элемент size_t size – размер выводимого элемента size_t nmemb – число выводимых элементов FILE *stream – указатель на поток, открытый для записи Возвращаемое значение: в случае успеха возвращает значение nmemb Описание: функция выводит в поток stream nmemb элементов, каждый из которых имеет размер size байт. Указатель ptr должен указывать на первый выводимый элемент. fgetc() Определение: int fgetc (FILE *stream) Параметры: FILE *stream – указатель на поток, открытый для чтения Возвращаемое значение: введенный символ или EOF в случае ошибки Описание: функция считывает символ (байт) из потока stream. В случае ошибки или попытки чтения «за концом потока» возвращается значение EOF, поэтому следует использовать функции feof() и ferror() для определения истинной причины ошибки. ungetc() Определение: int ungetc (int c, FILE *stream) Параметры: int c – символ FILE *stream – указатель на поток, открытый для чтения Возвращаемое значение: в случае успеха возвращает символ с, в противном случае возвращает EOF. Описание: функция «заталкивает» в поток, открытый для чтения, один символ с (преобразуемый в unsigned char), заставляя его стать следующим, считываемым при вводе. Функция возвращает этот самый символ, если не было ошибки. Примечания: если «заталкиваемый» символ равен EOF, функция завершается с ошибкой, а поток остается неизмененным. fgets() Определение: char * fgets (char *str, int size, FILE *stream) Параметры: char *str – указатель на строку-результат int size – количество вводимых байт FILE *stream – указатель на поток, открытый для чтения Возвращаемое значение: в случае успешного завершения возвращает указатель на строку-результат, в противном случае возвращает NULL. Описание: функция считывает из потока stream не более size-1 байтов, размещая их в строке-результате str. Считывание символов продолжается до тех пор, пока не встретится символ перевода строки (который в результат не заносится). Если не было ошибок, строка-результат завершается символом NULL, а функция возвращает указатель на строку-результат. В случае ошибок возвращается NULL, и в потоке устанавливается признак ошибки, чтобы можно было использовать функцию ferror(). gets() Определение: char * gets (char *str) Параметры: char *str – строка-результат Возвращаемое значение: в случае успешного завершения возвращает указатель на строку-результат, в противном случае возвращает NULL Описание: функция действует аналогично fgets(), только строка читается из потока stdin и количество считанных байт не контролируется. Примечания: отсутствие контроля количества вводимых символов налагает большую ответственность на программиста. fread() Определение: size_t fread (void *ptr, size_t size, size_t nmemb, FILE *stream) Параметры: void *ptr – указатель на буфер-приемник size_t size – размер каждого элемента size_t nmemb – количество элементов FILE *stream – указатель на поток. Открытый для чтения Возвращаемое значение: в случае успешного завершения возвращает значение nmemb Описание: функция считывает из потока stream nmemb элементов, каждый из которых имеет размер size байт и помещает их в буфер ptr. Примечания: буфер должен иметь достаточный объем! clearerr() Определение: void clearerr (FILE *stream) Параметры: FILE *stream – указатель на поток Возвращаемое значение: нет Описание: функция сбрасывает признаки любых ошибок потока stream. feof() Определение: int feof (FILE *stream) Параметры: FILE *stream – указатель на поток Возвращаемое значение: не нулевое значение, если достигнут конец потока stream Радиолюбитель – 11/2010 СПРАВОЧНЫЙ МАТЕРИАЛ Описание: функция тестирует состояние флага EOF для указанного потока. Примечания: функция не сбрасывает тестируемый флаг, для этого следует использовать clearerr(). ferror() Определение: int ferror (FILE *stream) Параметры: FILE *stream – указатель на поток Возвращаемое значение: не нулевое значение, если установлен флаг ошибки в потоке stream Описание: функция тестирует состояние флага ошибки для указанного потока. Примечания: функция не сбрасывает тестируемый флаг, для этого следует использовать clearerr(). vfscanf() Определение: int vfscanf (FILE *stream, const char *fmt, va_list ap) Параметры: FILE *stream – указатель на поток, открытый для чтения const char *fmt – указатель на строкуспецификатор формата va_list ap – список вводимых аргументов Возвращаемое значение: функция возвращает количество введенных элементов списка аргумента в случае успешного завершения или EOF в противном случае. Описание: это базовая функция форматированного ввода для всего семейства scanf-функций. Функция считывает символы из потока stream и преобразовывает их в соответствии с заданным форматом fmt, помещая результирующие значения в аргументы из списка ap. Все символы, не попадающие под спецификацию управляющей последовательности строки формата (см. Спецификация строки формата для функций форматированного ввода) считаются текстом и проверяются на совпадение с соответствующими символами строки формата непосредственно. При этом пробел в строке формата считается совпадающим с любым символом «пустого места» в потоке, прочие же должны совпадать абсолютно. Если обнаруживается несовпадение символов, либо достигается конец потока – функция завершается по ошибке. Функция возвращает количество считанных элементов в списке аргументов, Радиолюбитель – 11/2010 причем 0 так же допустим. Ноль возвращается, если поступавшие символы не могли быть интерпретированы в соответствии с заданным форматом. Функция возвращает EOF только в том случае, если конец потока был достигнут прежде, чем завершено последнее преобразование формата. Примечания: полная реализация всех возможностей форматирования ввода требует больших затрат программной памяти. С целью минимизации ресурсов существует 3 варианта реализации функции vfscanf(), выбираемых компоновщиком во время сборки проекта: - обычная (по умолчанию) – реализует все виды преобразований, кроме предназначенных для чисел с плавающей точкой; - усеченная – реализует только базовые преобразования форматов целых чисел и строк, кроме того, исключается управляющая последовательность %[ (см. Спецификация строки формата для функций форматированного ввода); - полная – реализующая полную спецификацию форматов, кроме того, снимающая ограничение на размер в 255 символов (размер ограничивается 65535). Усеченная версия выбирается следующими параметрами компилятора: -Wl,-u,vfscanf -lscanf_min –lm Полная версия выбирается следующими параметрами компилятора: -Wl,-u,vfscanf -lscanf_flt -lm vfscanf_P() Определение: int vfscanf_P (FILE *stream, const char *fmt, va_list ap) Описание: вариант функции vfscanf(), использующий строку формата в сегменте кода. fscanf() Определение: int fscanf (FILE *stream, const char *fmt,...) Описание: вариант функции vfscanf(), использующий прямо указанный список вводимых аргументов. fscanf_P() Определение: int fscanf_P (FILE *stream, const char *fmt,...) Описание: вариант функции fscanf(), использующий строку формата в сегменте кода. scanf() Определение: int scanf (const char *fmt,...) Описание: вариант fscanf() для ввода из потока stdin. scanf_P() Определение: int scanf_P (const char *fmt,...) Описание: вариант scanf(), использующий строку формата в сегменте кода. vscanf() Определение: int vscanf (const char *fmt, va_list ap) Описание: вариант vfscanf() для ввода из потока stdin. sscanf() Определение: int sscanf (const char *buf, const char *fmt,...) Параметры: const char *buf – буфер для обработки const char *fmt – строка формата ... – список вводимых аргументов Описание: функция полностью аналогичная vfscanf(), с той лишь разницей, что вместо чтения из потока берутся символы из буфера buf. sscanf_P() Определение: int sscanf_P (const char *buf, const char *fmt,...) Описание: вариант sscanf(), использующий строку формата в сегменте кода. fflush() Определение: int fflush (FILE *stream) Параметры: FILE *stream – указатель на поток Возвращаемое значение: Описание: вызывает немедленный «сброс» буферов чтения-записи для указанного потока stream. Примечание: эта функция введена только для совместимости, т.к. никакой буферизации ввода-вывода не реализовано. Функция не делает ничего Продолжение в №12/2010 61 СПРАВОЧНЫЙ МАТЕРИАЛ Роман Абраш г. Новочеркасск E-mail: arv@radioliga.com Продолжение. Начало в №1-11/2010 stdlib.h – Стандартные возможности Этот модуль определяет базовые макросы и функции, требуемые по стандарту ISO С, а также некоторые специфичные для AVR расширения стандарта. Определения модуля В модуле определены следующие константы: RANDOM_MAX – наибольшее случайное число, генерируемое функцией random() DTOSTR_ALWAYS_SIGN – флаг для функции dtostre() DTOSTR_PLUS_SIGN – флаг для функции dtostre() DTOSTR_UPPERCASE – флаг для функции dtostre() RAND_MAX – наибольшее случайное число, генерируемое функцией rand() Введены описания следующих структур и типов: div_t – возвращаемое значение функции div() ldiv_t – возвращаемое значение функции ldiv() typedef int(*) __compar_fn_t (const void *, const void *) – определение указателя на функцию, используемую при сортировке qsort(), введен лишь для удобства. Эта функция в качестве параметров получает два указателя, сравнивает объекты, на которые эти указатели указывают, и возвращает число меньше нуля, ноль или число больше нуля соответственно для случаев, когда первый объект меньше, равен или больше второго. Обе структуры div_t и ldiv_t содержат по 2 поля данных: quot – частное и rem – остаток, только для первой структуры оба этих поля имеют тип int, а для второй – long. Кроме того, определены глобальные переменные, позволяющие произвести «тонкую настройку» поведения менеджера динамической памяти (см. Динамическое распределение памяти). size_t __malloc_margin char * __malloc_heap_start char * __malloc_heap_end Стандартные функции abort() Определение: void abort (void) Параметры: нет. Возвращаемое значение: нет. Описание: функция вызывает аварийное завершение программы. В реализации для AVR это приводит к запрещению прерываний и вызову функции exit(1) или, что 52 Книга по работе с WinAVR и AVR Studio то же самое, переход к бесконечному пустому циклу. Это равносильно фактическому прекращению работы микроконтроллера. Примечания: состояние портов микроконтроллера и прочей периферии не изменяется. abs() Определение: int abs (int i) Описание: функция возвращает абсолютное значение аргумента i, т.е. его модуль. Примечание: данная функция – встроенная в GCC. labs() Определение: long labs (long i) Описание: функция возвращает абсолютное значение аргумента i, т.е. его модуль. Примечание: данная функция – встроенная в GCC. bsearch() Определение: void * bsearch (const void *key, const void *base, size_t nmemb, size_t size, int(*compar)(const void *, const void *)) Параметры: const void *key – указатель на ключевой элемент поиска const void *base – указатель на начало области поиска size_t nmemb – количество просматриваемых элементов size_t size – размер каждого элемента int(*compar)(const void *, const void *) – указатель на функцию сравнения элементов Возвращаемое значение: функция возвращает указатель на найденный элемент либо NULL, если ничего не найдено. Если имеется несколько одинаковых элементов, то будет возвращен указатель на один из них (какой именно – не определено стандартом32). Описание: функция выполняет поиск элемента key среди последовательности из nmemb подобных элементов base, каждый из которых имеет размер size. Поиск осуществляется путем сравнения key с каждым из элементов в последовательности при помощи функции compare(), которая возвращает число меньше нуля, равное нулю или большее нуля, соответствующие результатам сравнения МЕНЬШЕ, ЭКВИВАЛЕНТНО или БОЛЬШЕ. div() Определение: div_t div (int num, int denom) 32 мент. Скорее всего это будет первый встреченный эле- Описание: функция целочисленного деления целых чисел. Возвращает результат в виде заполненных полей структуры div_t (см. Определения модуля). ldiv() Определение: ldiv_t ldiv (long num, long denom) Описание: функция целочисленного деления длинных целых чисел. Возвращает результат в виде заполненных полей структуры ldiv_t (см. Определения модуля). qsort() Определение: void qsort (void *base, size_t nmemb, size_t size, __compar_fn_t compar) Параметры: void *base – указатель на исходный массив элементов size_t nmemb – количество элементов в массиве size_t size – размер каждого элемента __compar_fn_t compare – указатель на функцию сравнения элементов (см. Определения модуля) Возвращаемое значение: нет. Описание: функция выполняет «быструю» сортировку элементов в массиве base. Для сравнения элементов используется функция compar(), которая возвращает число меньше нуля, равное нулю или большее нуля, соответствующие результатам сравнения МЕНЬШЕ, ЭКВИВАЛЕНТНО или БОЛЬШЕ. Количество элементов определяется значением nmemb, размер каждого элемента равен size. strtol() Определение: long strtol (const char *nptr, char **endptr, int base) Параметры: const char *nptr – указатель на строку char **endptr – указатель на указатель на первый необработанный символ в строке int base – основание для преобразования Возвращаемое значение: результат преобразования строки в число. Описание: функция осуществляет анализ символов строки nptr и их преобразование в длинное целое число, используя base, как основание системы счисления. Строка может начинаться с любого числа символов-разделителей, которые определяются функцией isspace(), после которых допустим один символ «–» или «+». base может принимать значения от 2 до 36 включительно или особое значение – ноль. В зависимости от значения base в строке nptr допустимы разные символы. Для base==16 число может начинаться с символов ”0x”. Если base==0, то происходит автоматическое распознавание системы счисления по первым символам Радиолюбитель – 11/2010 СПРАВОЧНЫЙ МАТЕРИАЛ числа: если обнаружена последовательность ”0x”, то принимается base=16, если число начинается с нуля – принимается base=8 (т.е. восьмеричная система), а в противном случае принимается base=10 (т.е. десятичная система). Для систем счисления с основанием больше 10 в качестве допустимых символов-цифр принимаются буквы латинского алфавита (независимо от регистра) – аналогично шестнадцатеричной системе. Анализ строки прекращается, как только будет встречен первый символ, не попадающий в число допустимых для принятой системы счисления, при этом, если endptr не NULL, по его адресу заносится адрес этого символа. Если в строке не встретилось вообще никакой цифры, в endptr записывается исходное значение nptr. Если при преобразовании не возникло ошибок переполнения, функция возвращает результат преобразования в виде длинного целого числа со знаком. Если было переполнение, то в переменную errno записывается значение ERANGE (см. errno.h – Системные ошибки)и возвращается LONG_MIN или LONG_MAX соответственно «направлению» переполнения. strtoul() Определение: unsigned long strtoul (const char *nptr, char **endptr, int base) Описание: функция полностью аналогична strtol(), кроме того, что возвращаемое значение – беззнаковое длинное (соответственно, символ «–» в строке недопустим). atol() Определение: long atol (const char *s) Параметры: const char *s – указатель на строку Описание: функция преобразует строку s (точнее, первые значащие символы строки) в длинное целое со знаком число, которое и возвращает. В отличие от strtol() не производится контроль переполнения (errno не устанавливается и результат функции неопределен), зато данная функция требует значительно меньше памяти (как по коду, так и по ОЗУ) и работает гораздо быстрее. Примечания: в документации не указано, поддерживает ли эта функция автоматическое определение системы счисления, или требует только десятичной? atoi() Определение: int atoi (const char *s) Описание: функция во всем, кроме типа возвращаемого значения, аналогична atol(). exit() Определение: void exit (int status) Параметры: int status – код завершения Радиолюбитель – 11/2010 Возвращаемое значение: нет. Описание: функция вызывает завершение программы. Так как в случае с микроконтроллером нет никакой внешней среды для программы, возвращаемое значение попросту игнорируется, а действие функции заключается в глобальном запрещении прерываний и переходу к бесконечному пустому циклу. В контексте С++ перед этим еще происходит вызов глобального деструктора. malloc() Определение: void * malloc (size_t size) Параметры: size_t size – размер запрашиваемой области памяти Возвращаемое значение: указатель на выделенную область или NULL в случае невозможности выделения. Описание: функция выделяет (резервирует) область памяти запрошенного размера и возвращает указатель на эту область, если выделение произошло успешно. Примечания: функция не производит инициализацию выделенной памяти какимлибо значением. Дополнительно см. Динамическое распределение памяти. free() Определение: void free (void *ptr) Параметры: void *ptr – указатель на область памяти Возвращаемое значение: нет Описание: функция освобождает память, на которую указывает указатель ptr, и тем самым делает ее доступной для последующего выделения. Если ptr == NULL, функция ничего не делает. Примечания: Дополнительно см. Динамическое распределение памяти. calloc() Определение: void * calloc (size_t nele, size_t size) Параметры: size_t nele – число элементов size_t size – размер элемента Возвращаемое значение: указатель на выделенную память или NULL, если выделение невозможно Описание: функция выделяет (резервирует) область памяти для размещения nele элементов по size байт каждый. Полностью аналогична malloc(), кроме того, что выделенная память заполняется нулями. Примечания: Дополнительно см. Динамическое распределение памяти. realloc() Определение: void * realloc (void *ptr, size_t size) Параметры: void *ptr – указатель на «расширяемую» область памяти size_t size – новый размер для области Возвращаемое значение: указатель на новую область или NULL, если изменение размера невозможно. Описание: функция изменяет размер области памяти, ранее выделенной для ptr, до размера size. Возвращаемое значение не обязательно будет совпадать с исходным значением ptr, однако содержимое области памяти будет сохранено (насколько это возможно для нового размера области). Если изменение размера области невозможно, то значение и содержимое ptr не меняется. strtod() Определение: double strtod (const char *nptr, char **endptr) Параметры: const char *nptr – указатель на строку char **endptr – указатель на указатель на первый необработанный символ строки Возвращаемое значение: результат преобразования строки в число с плавающей точкой. Описание: аналогично strtol(), функция осуществляет анализ символов строки nptr (игнорируя начальные пробелы) и преобразование их в число с плавающей точкой. Подразумевается, что число представлено в виде общепринятой записи, в т.ч. с мантиссой, т.е. допустим первый символ «+» или «–», затем десятичные цифры, возможно с разделительной точкой, затем, возможно символ «е» или «Е», после которого должно следовать число (возможно со знаком) без точки. Функция возвращает результат, если не было переполнения в результате преобразования. Если же переполнение было, то переменная errno устанавливается в ERANGE (см. errno.h – Системные ошибки) и возвращается значение «бесконечность» с соответствующим знаком. Если endptr не NULL, то туда записывается указатель на первый символ в строке, который не обработан (см. strtol()). atof() Определение: double atof (const char *nptr) Описание: функция полностью эквивалентна strtod() rand() Определение: int rand (void) Возвращаемое значение: псевдослучайное число Описание: функция возвращает псевдослучайное число в пределах от 0 до RAND_MAX. Это число генерируется по специальному алгоритму, формирующему конечную последовательность псевдослучайных чисел, по достижению конца которой числа начинают повторяться в той же последовательности. Функция srand() осуществляет «настройку» алгоритма так, что можно получать одинаковые или разные последовательности псевдослучайных чисел, при каждом вызове rand() автоматически предыдущее псевдослучайное значение используется для srand() (это происходит внутри реализации функции). 53 СПРАВОЧНЫЙ МАТЕРИАЛ Примечания: Данная функция обеспечивает достаточно «короткую» последовательность чисел и введена для совместимости со стандартом. (См. более продвинутые варианты random() и srandom(), обладающие лучшим качеством «случайности» генерируемых чисел.) srand() Определение: void srand (unsigned int seed) Параметры: unsigned int seed – коэффициент генератора случайных чисел Возвращаемое значение: нет. Описание: функция выполняет настройку генератора псевдослучайной последовательности чисел, используя коэффициент seed в качестве условно-стартового значения. Все это следует понимать лишь как то, что после вызова srand(5) и srand(10) функция rand() будет возвращать принципиально разные последовательности чисел, однако два независимых устройства после srand(5) будут получать абсолютно одинаковые последовательности чисел при помощи rand(). Примечания: по умолчанию генератор псевдослучайной последовательности настроен. Как будто был вызов srand(1). rand_r() Определение: int rand_r (unsigned long *ctx) Параметры: unsigned long *ctx – указатель на переменную для сохранения контекста Возвращаемое значение: псевдослучайное число. Описание: эта функция – реентерабельный вариант rand(). Для сохранения внутренних «настроек» генератора псевдослучайной последовательности она использует не глобальную переменную модуля, а локальную переменную пользовательской программы, на которую указывает ctx, таким образом возможен рекурсивный вызов функции, и при этом сохраняется «случайность» возвращаемых значений. dtostre() Определение: char * dtostre (double val, char *s, unsigned char prec, unsigned char flags) Параметры: double val – преобразуемое число char *s – указатель на строку-результат unsigned char prec – точность представления unsigned char flags – флаги-признаки преобразования Возвращаемое значение: указатель на строку-результат Описание: функция осуществляет преобразование val в его символьное представление, помещаемое в s (должно быть заранее выделено достаточно места). Формат представления числа [-]d.ddde±dd, где количество цифр после точки определяется значением prec. Если prec не равно нулю, то перед точкой всегда одна цифра, 54 а если prc==0, то точка не выводится. Показатель степени всегда содержит 2 знака. Параметр flags может принимать одно или более (объединяемых по ИЛИ ) следующих значений: DTOSTRE_UPPERCASE – символ e будет заменен на E DTOSTRE_ALWAYS_SIGN – если число положительное, всегда будет добавлен пробел перед его представлением DTOSTRE_PLUS_SIGN – всегда будет выведен знак перед представлением, в т.ч. «+» для положительных Примечания: эта функция не входит в библиотеку libc.a, и поэтому должна использоваться лишь совместно с подключением к линкеру математической библиотеки libm.a директивой компилятора –lm. dtostrf() Определение: char * dtostrf (double val, signed char width, unsigned char prec, char *s) Параметры: double val – преобразуемое число signed char width – ширина результирующего представления unsigned char prec – точность представления char *s – указатель на строку-результат Возвращаемое значение: указатель на строку-результат. Описание: функция осуществляет преобразование числа в формате с плавающей точкой val в его символьное представление, помещаемое в строку s (достаточное пространство должно быть обеспечено заранее). Результат форматируется в «простой» форме с плавающей точкой, т.е. [-]d.dd, причем width показывает, сколько символов должно занимать представление результата (включая десятичную точку и возможный минус для отрицательных чисел), а prec определяет число знаков после точки. width – это число со знаком, отрицательное значение означает, что результат должен быть выровнен «влево». Примечания: эта функция не входит в библиотеку libc.a, и поэтому должна использоваться лишь совместно с подключением к линкеру математической библиотеки libm.a директивой компилятора –lm. Дополнительные функции itoa() Определение: char * itoa (int val, char *s, int radix) Параметры: int val – преобразуемое число char *s – указатель на строку-результат int radix – основание системы счисления Возвращаемое значение: указатель на строку-результат. Описание: функция преобразует целое число val в его символьное представление, помещаемое в s, в заданной системе счисления по основанию radix. Основание radix может принимать значения от 2 до 36 включительно, для цифр, «вес» которых более 9 используются символы латинского алфавита, начиная с “a”. Если число отрицательное, то символ «минус» выводится только для десятичной системы счисления (т.е. если radix==10). Примечания: см. «обратную» функцию atoi() ltoa() Определение: char * ltoa (long int val, char *s, int radix) Параметры: int val – преобразуемое число char *s – указатель на строку-результат int radix – основание системы счисления Возвращаемое значение: указатель на строку-результат. Описание: функция полностью аналогична itoa(), за исключением того, что val – длинное целое число. Примечания: чем меньшее значение radix используется, тем большее пространство должно быть предусмотрено для хранения результата s, в противном случае возможен выход за пределы массива с непредсказуемыми последствиями. utoa() Определение: char * utoa (unsigned int val, char *s, int radix) Описание: функция полностью аналогична itoa(), за исключением того, что val – целое число без знака. ultoa() Определение: char * ultoa (unsigned long int val, char *s, int radix) Описание: функция полностью аналогична itoa(), за исключением того, что val – длинное целое число без знака. random() Определение: long random (void) Параметры: нет Возвращаемое значение: псевдослучайное число Описание: функция возвращает псевдослучайное число в пределах от 0 до RANDOM_MAX. Кроме того, что тип возвращаемого числа – длинное целое, различий в функционировании по сравнению с rand() нет никаких. Примечания: благодаря большей разрядности возвращаемого числа данная функция образует более «случайную» последовательность, по сравнению с rand(), и ее применение совместно с srandom() и random_r() более предпочтительно. srandom() Определение: void srandom (unsigned long seed) Описание: за исключением того, что данная функция выполняет «настройку» улучшенного алгоритма для работы функции random(), и имеет аргумент seed типа длинное целое число, действие функции полностью аналогично стандартной функции srand(). Радиолюбитель – 11/2010 СПРАВОЧНЫЙ МАТЕРИАЛ random_r() Определение: long random_r (unsigned long *ctx) Описание: за исключением того, что данная функция возвращает значение и использует аргумент ctx типа длинное целое число, действие функции полностью аналогично стандартной функции rand_r(). string.h – Строки Модуль реализует набор функций поддержки различных действий над строками, оканчивающимися символом NULL (так называемые ASCIIZ-строки). Модуль предназначен для работы со строками, находящимися в ОЗУ. Если используются строки, хранимые в сегменте кода, следует использовать функции, определенные в модуле avr/pgmspace.h – Поддержка обращения к сегменту кода AVR. Модуль определяет макрос _FFS(x), который по смыслу полностью аналогичен функции ffs(). Этот макрос вычисляется на этапе компиляции, что порождает быстрый и компактный код, но в качестве своего аргумента должен принимать только константные значения. Применение этого макроса с аргументом-переменной не рекомендуется, так как генерируемый код может быть объемным и/или неверным в принципе. Функции модуля ffs() Определение: int ffs(int val) Параметры: int val – анализируемое число Возвращаемое значение: номер бита. Описание: функция возвращает номер младшего значащего бита в анализируемом числе val. Биты нумеруются с 1-го, которому соответствует младший разряд числа. Если нет ни одного установленного бита в числе, функция возвращает ноль. Примечания: для вычисления константных значений более предпочтительно применение макроса _FFS() Параметры: void *dest – указатель на область-приемник const void *src – указатель на областьисточник int val – значение для поиска size_t len – ограничитель количества копируемых байт Возвращаемое значение: указатель на следующий за найденным в области src байт или NULL, если не найдено. Описание: функция выполняет копирование из области src в область dest не более len байт последовательно, прекращая копирование, если очередной байт равен val. Параметры: void *dest – указатель на область-приемник const void *src – указатель на областьисточник size_t len – ограничитель количества копируемых байт Возвращаемое значение: указатель на область-приемник. Описание: функция копирует len байт из области-источника в область-приемник. Примечания: области dest и src не должны пересекаться! Для правильного копирования пересекающихся областей следует использовать memmove() memchr() Определение: void * memchr(const void *src, int val, size_t len) Параметры: const void *src – указатель на начало области поиска int val – искомое значение size_t len – длина области Возвращаемое значение: указатель на найденный байт или NULL, если не найдено. Описание: функция осуществляет поиск байта val (обрабатываемого как unsigned char) в области памяти, начинающейся с адреса src и содержащей len байт. Поиск прекращается на первом встреченном байте, значение которого совпадает с val. memmem() Определение: void * memmem(const void *s1, size_t len1, const void *s2, size_t len2) Параметры: const void *s1 – область поиска size_t len1 – длина области поиска const void *s2 – «искомая» область size_t len2 – длина «искомой» области Возвращаемое значение: указатель на начало первого вхождения области s2 внутри s1 или NULL, если не найдено. Описание: функция ищет первое полное вхождение последовательности s2 из len2 байт внутри области s1 из len1 байт. Если len2==0, функция возвращает указатель на s1. ffsll() Определение: int ffsll(long long val) Описание: действие функции полностью аналогично ffs(), за исключением того, что анализируемое число val имеет тип большое длинное целое. memcmp() Определение: int memcmp(const void *s1, const void *s2, size_t len) Параметры: const void *s1 – указатель на первую область памяти const void *s2 – указатель на вторую область памяти size_t len – длина областей Возвращаемое значение: число меньше нуля, равное нулю или большее нуля в случае, если первая область соответственно меньше, равна или больше второй по содержимому. Описание: функция сравнивает len первых байтов в областях s1 и s2, возвращая результат сравнения. Примечания: 1. Сравнение байтов ведется как беззнаковых (unsigned char). 2. Если сравниваются области, заполненные 16-битными числами (или более) результат сравнения может быть некорректным. 3. Эта функция не совместима с опцией компилятора –mint8, однако если важна проверка только на равенство, она может корректно использоваться и с этой опцией. memccpy() Определение: void * memccpy(void *dest, const void *src, int val, size_t len) memcpy() Определение: void * memcpy(void *dest, const void *src, size_t len) ffsl() Определение: int ffsl(long val) Описание: действие функции полностью аналогично ffs(), за исключением того, что анализируемое число val имеет тип длинное целое. Радиолюбитель – 11/2010 memmove() Определение: void * memmove(void *, const void *, size_t) Параметры: void *dest – указатель на область-приемник const void *src – указатель на областьисточник size_t len – ограничитель количества копируемых байт Возвращаемое значение: указатель на область-приемник. Описание: функция копирует len байт из области-источника в область-приемник. Причем эти области могут пересекаться. memrchr() Определение: void * memrchr(const void *src, int val, size_t len) Параметры: const void *src – указатель на начало области поиска int val – искомое значение size_t len – длина области Возвращаемое значение: указатель на найденный байт или NULL, если не найдено. Примечания: функция, выполняющая поиск байта, как и memchr(), только в обратном направлении, т.е. от конца области src к ее началу. memset() Определение: void * memset(void *dest, int val, size_t len) 55 СПРАВОЧНЫЙ МАТЕРИАЛ Параметры: const void *dest – указатель на начало области int val – значение для записи size_t len – длина области Возвращаемое значение: dest Описание: функция осуществляет заполнение len первых байтов области dest значением val, рассматриваемым как байт. strcasecmp() Определение: int strcasecmp(const char *s1, const char *s2) Параметры: const char *s1 – указатель на первую строку const char *s2 – указатель на вторую строку Возвращаемое значение: число меньше нуля, равное нулю или большее нуля в случае, если первая строка соответственно меньше, равна или больше второй по содержимому. Описание: функция производит сравнение двух ASCIIZ-строк без учета регистра символов. Сравнение ведется с учетом фактической длины s1 и s2: если одна из строк совпадает по символам со второй, но короче – она считается меньше другой. strcasestr() Определение: char * strcasestr(const char *s1, const char *s2) Параметры: const char *s1 – указатель на первую строку const char *s2 – указатель на вторую строку Возвращаемое значение: указатель на начало первого вхождения строки s2 в строке s1 или NULL, если не найдено. Описание: функция ищет подстроку s2 внутри строки s1 без учета регистра символов. Если s2 равна s1 или наоборот, пуста, то возвращается s1. Примечания: см. также strstr() strcat() Определение: char * strcat(char *dest, const char *src) Параметры: char *dest – указатель на строку-приемник const char *src – указатель на строкуисточник Возвращаемое значение: указатель на строку-приемник. Описание: функция осуществляет конкатенацию двух строк, присоединяя к dest строку src. Строки не должны пересекаться и в dest должно быть предусмотрено достаточное пространство. strchr() Определение: char * strchr(const char *str, int val) Параметры: const char *str – указатель на строку int val – искомый символ 56 Возвращаемое значение: указатель на найденный символ в строке или NULL, если не найдено. Описание: функция ищет первый символ val в строке str, возвращая указатель на него. Не смотря на тип переменной val, функция не может использоваться для поиска мультибайтных символов, т.е. «символ» в данном случае означает «байт». strchrnul() Определение: char * strchrnul(const char *str, int val) Параметры: const char *str – указатель на строку int val – искомый символ Возвращаемое значение: указатель на найденный символ в строке или на пустую строку, если не найдено. Описание: функция, как и strchr(), осуществляет поиск символа val в строке str, однако всегда возвращает не NULL-указатель: если символ не найден, возвращается указатель на символ NULL, завершающий str. strcmp() Определение: int strcmp(const char *s1, const char *s2) Параметры: const char *s1 – указатель на первую строку const char *s2 – указатель на вторую строку Возвращаемое значение: число меньше нуля, равное нулю или большее нуля в случае, если первая строка соответственно меньше, равна или больше второй по содержимому. Описание: функция производит сравнение двух ASCIIZ-строк. Сравнение ведется с учетом фактической длины s1 и s2: если одна из строк совпадает по символам со второй, но короче – она считается меньше другой. Примечания: см. так же функцию strcasecmp() strcpy() Определение: char * strcpy(char *dest, const char *src) Параметры: char *dest – указатель на строку-приемник const char *src – указатель на строкуисточник Возвращаемое значение: указатель на строку-приемник. Описание: функция копирует в буфер dest строку src (включая завершающий NULL). Примечания: если буфер dest не содержит достаточного пространства для размещения целиком строки src (что бывает, если программист глуп или просто ленив, и не смог проверить размеры строк перед копированием)33, может произойти выход за допустимые границы области памяти (переполнение буфера) – любимая техника хакеров. 33 В скобках приведен дословный перевод соответствующего текста из оригинальной документации WinAVR. strcspn() Определение: size_t strcspn(const char *s, const char *reject) Параметры: const char *s – указатель на строку const char *reject – указатель на строку-множество Возвращаемое значение: число символов. Описание: функция вычисляет количество подряд идущих первых символов в строке s, не совпадающих ни с одним из символов строки reject (завершающий ноль не рассматривается как часть строки). strlcat() Определение: size_t strlcat(char *dest, const char *src, size_t siz) Параметры: Возвращаемое значение: Описание: Примечания: strlcpy() Определение: size_t strlcpy(char *dest, const char *src, size_t siz) Параметры: Возвращаемое значение: Описание: Примечания: strlen() Определение: size_t strlen(const char *str) Параметры: const char *str – указатель на строку Возвращаемое значение: количество символов в строке. Описание: функция вычисляет длину строки. Завершающий ноль не участвует в подсчете. strlwr() Определение: char * strlwr(char *s) Описание: переводит «буквенные» символы строки s в нижний регистр. Возвращает s. strncasecmp() Определение: int strncasecmp(const char *s1, const char *s2, size_t len) Описание: функция полностью аналогична strcasecmp(), за исключением того, что сравниваются только len первых байтов строки s1. strncat() Определение: char * strncat(char *dest, const char *src, size_t len) Описание: функция полностью аналогична strcat(), за исключением того, что будут присоединены только len первых байтов строки src. strncmp() Определение: Радиолюбитель – 11/2010 СПРАВОЧНЫЙ МАТЕРИАЛ int strncmp(const char *s1, const char *s2, size_t len) Описание: функция полностью аналогична strcpy(), за исключением того, что сравниваются только len первых байтов строки s1. strncpy() Определение: char * strncpy(char *dest, const char *src, size_t len) Параметры: char *dest – указатель на строку-приемник const char *src – указатель на строкуисточник size_t len – количество копируемых байт Возвращаемое значение: указатель на строку-приемник. Описание: функция осуществляет копирование не более len байтов из строки src в строку dest. Если среди len первых байтов src не будет встречен NULL, то dest не будет иметь завершающего нуля. Если же ноль встретится раньше, чем скопируются len байтов, остаток будет дополнен нулями. strnlen() Определение: size_t strnlen(const char *str, size_t len) Параметры: const char *str – указатель на строку size_t len – ограничитель Возвращаемое значение: длина строки, но не более len Описание: функция возвращает количество символов в строке str (не включая завершающий ноль), если это количество меньше len, или значение len в противном случае. strpbrk() Определение: char *strpbrk(const char *s, const char *accept) Параметры: const char *s – указатель на строку const char *accept – указатель на строку-множество Возвращаемое значение: указатель на первый встреченный символ в строке s или NULL, если не найдено. Описание: функция ищет в строке s первый символ, совпадающий с любым символом из строки accept (завершающий ноль не входит в число проверяемых). Если оба или любой из параметров пусты, возвращается NULL. strrchr() Определение: char *strrchr(const char *str, int val) Параметры: const char *str – указатель на строку int val – искомый символ Возвращаемое значение: указатель на найденный символ или NULL, если не найдено. Описание: функция ищет последний символ в строке str, равный val, или, что то же самое, первый символ val от конца стро- Радиолюбитель – 11/2010 ки. Как и strchr(), эта функция не может использоваться для поиска многобайтных символов, т.е. val рассматривается как байт. strrev() Определение: char *strrev(char *str) Параметры: char *str – указатель на строку Возвращаемое значение: str Описание: функция изменяет порядок следования символов в строке str на противоположный (т.е. “ABCD” превращается в “DCBA”). strsep() Определение: char *strsep(char **sp, const char *delim) Параметры: char **sp – указатель на указатель на строку const char *delim – указатель на строкуразделитель Возвращаемое значение: указатель на исходное значение *sp Описание: функция разбивает строку на отдельные элементы. Функция ищет первое вхождение любого из символов delim (в том числе и завершающий ноль) в строке, на которую указывает sp, и заменяет этот символ нулем, а в sp запоминается указатель на следующий за найденным символ. Примечания: если сравнить значение символа, на который указывает sp после завершения функции, с нулем, то можно выяснить, остались ли в исходной строке еще другие символы, отличные от символов-разделителей, или нет. strspn() Определение: size_t strspn(const char *s, const char *accept) Параметры: const char *s – указатель на строку const char *accept – указатель на строку-множество Возвращаемое значение: количество символов. Описание: функция определяет, сколько первых подряд идущих символов в строке s совпадает с любым из символов строки accept, и возвращает это количество. char *strtok_r(char *str, const char *delim, char **last) Параметры: char *str – указатель на строку const char *delim – указатель на строкуразделитель char **last – указатель на указатель на строку-остаток Возвращаемое значение: NULL, если вся строка разобрана, или указатель очередной найденный элемент. Описание: функция осуществляет разбор (парсинг) строки str по отдельным элементам, разделенным символами из строки delim. Парсинг строки ведется следующим образом. Первый вызов strtok_r() обязательно происходит с параметром str, указывающим на разбираемую строку, все последующие вызовы делаются с str=NULL. Переменнаяуказатель last должна оставаться не изменой во всем процессе парсинга одной и той же строки, а delim может меняться от вызова к вызову. При каждом вызове функция ищет в разбираемой строке str очередное вхождение любого из символов строки delim, заменяет его на ноль и возвращает указатель на начало выделенного таким образом элемента, а указатель на следующий символ сохраняется в переменной last до следующего вызова функции. Если возвращенное значение не NULL, можно продолжить парсинг строки следующим вызовом функции. Примечания: переменная для хранения указателя last – это локальная переменная, определенная пользователем, она используется для обеспечения реентерабельности функции strtok_r() (т.е. эта функция может вызываться из рекурсивной функции в программе). strupr() Определение: char *strupr(char *) Описание: переводит «буквенные» символы строки s в верхний регистр. Возвращает s. strstr() Определение: char *strstr(const char *s1, const char *s2) Параметры: const char *s1 – указатель на первую строку const char *s2 – указатель на вторую строку Возвращаемое значение: указатель на начало первого вхождения строки s2 в строке s1 или NULL, если не найдено. Описание: функция ищет подстроку s2 внутри строки s1. Если s2 равна s1 или наоборот, пуста, то возвращается s1. strtok_r() Определение: Продолжение в №1/2011 57 СПРАВОЧНЫЙ МАТЕРИАЛ Роман Абраш г. Новочеркасск E-mail: arv@radioliga.com Продолжение. Начало в №1-12/2010 AVR-СПЕЦИФИЧНЫЕ МОДУЛИ К специфичным для AVR относятся модули, реализующие ряд функций, выполнение которых возможно исключительно на платформе AVR-микроконтроллеров, т.е. аппаратно-зависимые функции. В основном эти функции реализуют поддержку особенностей архитектуры микроконтроллеров, например, встроенную EEPROM или возможность записи в память программ. avr/boot.h – Поддержка загрузчиков AVR Модуль определяет ряд макросов, упрощающих разработку программ пользователя, реализующих алгоритм самозагрузки программы (boot-loading). Это возможно не для любого микроконтроллера AVR. Макросы модуля позволяют работать со всей областью памяти программ микроконтроллеров. Макросы не используют глобальное запрещение прерываний, это оставлено на совести программиста. Пример функции самозаписи памяти программ см. на врезке. Определения модуля BOOTLOADER_SECTION BOOTLOADER_SECTION – модификатор атрибутов переменной или функции в Книга по работе с WinAVR и AVR Studio программе пользователя. Наличие этого макроса дает указание компоновщику разместить соответствующий объект в строго определенной области памяти, соответственно требованиям используемого микроконтроллера. Пример: BOOTLOADER_SECTION void func1(void); - функция func1() будет размещена в области памяти микроконтроллера, отведенной для загрузчика. GET_LOW_FUSE_BITS GET_LOW_FUSE_BITS – константа, задающая адрес чтения младшего байта fuseбитов микроконтроллера для применения в boot_lock_fuse_bits_get() GET_LOCK_BITS GET_LOCK_BITS – константа, задающая адрес чтения байта lock-битов микроконтроллера для применения в boot_lock_fuse_bits_get() GET_EXTENDED_FUSE_BITS GET_EXTENDED_FUSE_BITS – константа, задающая адрес чтения байта расширенных fuse-битов микроконтроллера для применения в boot_lock_fuse_bits_get() GET_HIGH_FUSE_BITS GET_HIGH_FUSE_BITS – константа, задающая адрес чтения старшего байта fuse-битов микроконтроллера для применения в boot_lock_fuse_bits_get() Врезка. Пример функции самозаписи памяти программ. #include <inttypes.h> #include <avr/interrupt.h> #include <avr/pgmspace.h> void boot_program_page (uint32_t page, uint8_t *buf){ uint16_t i; uint8_t sreg; // запрещаем прерывания sreg = SREG; cli(); eeprom_busy_wait(); // ожидаем, завершения предыдущей записи boot_page_erase (page);// стираем указанную страницу boot_spm_busy_wait(); // ожидаем завершения стирания // цикл заполнения страницы памяти for (i=0; i<SPM_PAGESIZE; i+=2){ // заполняем словами «в обратном порядке» uint16_t w = *buf++; w += (*buf++) << 8; boot_page_fill (page + i, w); } boot_page_write (page);// записываем заполненную страницу boot_spm_busy_wait(); // ожидаем завершения записи // разрешение перехода к RWWсекции памяти. Это нужно, если // после самозаписи должен быть осуществлен переход к основной программе boot_rww_enable(); // восстановление состояния прерываний, как было при входе в функцию SREG = sreg; } 54 Макросы-функции, определенные в модуле Так как большинство макросов выполнены в виде макросов-функций, далее они будут рассмотрены без упоминания того, что это не функции на самом деле. boot_spm_interrupt_enable() boot_spm_interrupt_enable() Описание: разрешает прерывания после исполнения инструкции записи в память программ. boot_spm_interrupt_disable() boot_spm_interrupt_disable() Описание: запрещает прерывания после исполнения инструкции записи в память программ. boot_is_spm_interrupt() boot_is_spm_interrupt() Описание: проверяет, разрешены ли прерывания после исполнения инструкции записи в память программ. boot_rww_busy() boot_rww_busy() Описание: проверяет, занята ли секция RWW памяти программ. boot_spm_busy() boot_spm_busy() Описание: проверяет, завершена ли инструкция записи в память программ. boot_spm_busy_wait() boot_spm_busy_wait() Описание: ожидает, пока не завершится инструкция записи в память программ. boot_lock_fuse_bits_get() boot_lock_fuse_bits_get(address) Описание: возвращает считанный байт fuse-битов или lock-битов микроконтроллера в зависимости от значения address, которое может принимать одно из следующих значений: GET_LOW_FUSE_BITS, GET_EXTENDED_FUSE_BITS, GET_HIGH_FUSE_BITS или GET_LOCK_BITS. Примечание: возвращается физически реальное значение соответствующего байта, т.е. запрограммированные биты являются нулями, а незапрограммированные биты – единицами. boot_signature_byte_get() boot_signature_byte_get(addr) Описание: возвращает байт из области сигнатуры микроконтроллера. Для микроконтроллеров, поддерживающих калибровочные байты встроенных RC-генераторов, этот макрос так же может возвращать и эти значения. Значение addr может находиться в пределах от 0 до 0x1F – см. документацию на примененный микроконтроллер. Радиолюбитель – 01/2011 СПРАВОЧНЫЙ МАТЕРИАЛ boot_page_fill() boot_page_fill(address, data) Описание: записывает во временный буфер страницы по указанному адресу address заданные данные data. Примечание: address – это адрес байта, data – это 16-битное значение (слово). AVR адресуют память побайтно, но за один раз записывают слово! Правильное использование макроса заключается в том, чтобы от обращения к обращению увеличивать значение address на 2, а в data помещать сразу по 2 байта, как единое слово. boot_page_erase() boot_page_erase(address) Описание: выполняет стирание страницы памяти программ. Примечание: address – это адрес байта, а не слова. boot_page_write() boot_page_write(address) Описание: выполняет запись буфера в указанную address страницу памяти программ. Примечание: address – это адрес байта, а не слова. boot_rww_enable() boot_rww_enable() Описание: включает доступ к RWWсекции памяти программ. boot_lock_bits_set() boot_lock_bits_set(lock_bits) Описание: устанавливает биты защиты загрузчика (BLB-биты). Примечания: 1. Параметр lock_bits в данном макросе должен содержать единицы в тех позициях, которые должны быть запрограммированы, т.е. записаны в ноль. 2. Макрос устанавливает только BLBбиты защиты. 3. Как и любые биты защиты, единожды установленные BLB-биты могут быть стерты только при полном стирании памяти микроконтроллера. boot_page_fill_safe() boot_page_fill_safe(address, data) Описание: выполняет то же действие, что и boot_page_fill(), но перед этим дожидается завершения ранее начатой операции самопрограммирования. boot_page_erase_safe() boot_page_erase_safe(address) Описание: выполняет то же действие, что и boot_page_erase(), но перед этим дожидается завершения ранее начатой операции самопрограммирования. boot_page_write_safe() boot_page_write_safe(address) Описание: выполняет то же действие, что и boot_page_write(), но перед этим дожидается завершения ранее начатой операции самопрограммирования. Радиолюбитель – 01/2011 boot_rww_enable_safe() boot_rww_enable_safe() Описание: выполняет то же действие, что и boot_rww_enable(), но перед этим дожидается завершения ранее начатой операции самопрограммирования. eeprom_is_ready() eeprom_is_ready() Описание: макрос, возвращающий не нулевое значение, если EEPROM готова для очередного обращения, 0 – в противном случае. boot_lock_bits_set_safe() boot_lock_bits_set_safe(lock_bits) Описание: выполняет то же действие, что и boot_lock_bits_set(), но перед этим дожидается завершения ранее начатой операции самопрограммирования. eeprom_busy_wait() eeprom_busy_wait() Описание: макрос, выполняющий цикл ожидания завершения операции записи в EEPROM. avr/eeprom.h – Поддержка EEPROM AVR Этот модуль содержит ряд простых функций и макросов, реализующих основные операции для работы со встроенным EEPROM микроконтроллеров AVR. Все функции действуют по принципу «ожидания», т.е. достаточно длительные. Если требуется реализация работы по прерываниям для максимально эффективного кода, она должна быть реализована пользователем самостоятельно. Все функции выполняют проверку готовности EEPROM для очередного обращения, но они не контролируют состояние автомата записи в память программ (т.е. самопрограммирования). Если ваша программа содержит код самопрограммирования, вы должны самостоятельно реализовать проверку завершения любой операции самопрограммирования перед обращением к EEPROM. Так как функции используют фиксированные регистры для работы, они не реентерабельны, т.е. если эти функции вызываются и из обычного цикла программы и из обработчиков прерываний, следует запрещать глобально прерывания перед обращениям к этим функциям из основной программы. Определения модуля Модуль вводит ряд определений (констант и макросов), часть из которых позволяет упростить совместимость исходного текста со средой разработки IAR. _EEPUT() _EEPUT(addr, val) Описание: макрос, осуществляющий запись байта val в EEPROM по адреса addr. Введен для совместимости с IAR. _EEGET() _EEGET(var, addr) Описание: макрос, осуществляющий чтение байта из EEPROM по адресу addr. Введен для совместимости с IAR. EEMEM EEMEM Описание: атрибут, указывающий на то, что переменная должна размещаться в EEPROM. Пример использования: EEMEM int k; - переменная k должна быть размещена в EEPROM. Функции модуля Большинство функций реализованы как static inline-функции или макросы-функции. eeprom_read_byte() uint8_t eeprom_read_byte (const uint8_t *p) Параметры: const uint8_t *p – указатель на байт в EEPROM Возвращаемое значение: считанный из EEPROM байт. Описание: функция выполняет считывание байта по указанному адресу из EEPROM. eeprom_read_word() uint16_t eeprom_read_word (const uint16_t *p) Параметры: const uint16_t *p – указатель на слово в EEPROM Возвращаемое значение: считанное из EEPROM слово. Описание: функция выполняет считывание 16-битного значения из EEPROM. eeprom_read_dword() uint32_t eeprom_read_dword (const uint32_t *p) Параметры: const uint32_t *p – указатель на двойное слово в EEPROM Возвращаемое значение: считанное из EEPROM двойное слово. Описание: функция выполняет считывание 32-битного числа из EEPROM. eeprom_read_block() void eeprom_read_block (void *dst, const void *src, size_t n) Параметры: void *dst – указатель на начало области в ОЗУ const void *src – указатель на начало области EEPROM size_t n – количество байт Возвращаемое значение: нет. Описание: функция выполняет считывание n байтов из EEPROM по адресу src и размещает их в ОЗУ, начиная с адреса dest. eeprom_write_byte() void eeprom_write_byte (uint8_t *p, uint8_t value) Параметры: uint8_t *p – указатель на ячейку в EEPROM 55 СПРАВОЧНЫЙ МАТЕРИАЛ uint8_t value – записываемый байт Возвращаемое значение: нет. Описание: функция выполняет запись байта value в EEPROM по адресу p. Врезка. Пример использования. #include <avr/io.h> FUSES ={ eeprom_write_word() void eeprom_write_word (uint16_t *p, uint16_t value) uint16_t *p – указатель на ячейку в EEPROM uint16_t value – записываемое слово Возвращаемое значение: нет. Описание: функция выполняет запись 16-битного числа value в EEPROM по адресу p. eeprom_write_dword() void eeprom_write_dword (uint32_t *p, uint32_t value) uint32 *p – указатель на ячейку в EEPROM uint32 value – записываемое двойное слово Возвращаемое значение: нет. Описание: функция выполняет запись 32-битного числа value в EEPROM по адресу p. eeprom_write_block() void eeprom_write_block (const void *src, void *dst, size_t n) Параметры: void *src – указатель на начало области ОЗУ void *dst – указатель на начало области в EEPROM size_t n – количество байт Возвращаемое значение: нет. Описание: функция осуществляет запись n байт из области ОЗУ src в область EEPROM dest. Следует признать, что модуль eeprom.h от релиза к релизу WinAVR претерпевает существенные модификации, в частности, неоднократно менялись местами параметры функций блочного чтениязаписи, что, разумеется, порождает определенные сложности с совместимостью ранее разработанного кода и новой версии компилятора. Поэтому рекомендуется тщательно изучить документацию на свежий релиз WinAVR, т.к. в нем могут быть сюрпризы. Из числа приятных сюрпризов, появившихся с версии WinAVR 20100110, можно назвать появление «бережных» функций записи EEPROM eeprom_update_xxxx(), где xxxx – это суффикс byte, word, dword или block. Параметры этих функций такие же, как у соответствующих eeprom_write_xxxx(), а отличие в том, что новые функции перед тем, как записать новое значение в ячейку EEPROM, проверяют ее текущее значение и, если оба значения одинаковы, то запись в ячейку не производится. Так как ресурс записей EEPROM AVR ограничен 100000 записей, использование новых функций позволяет увеличить живучесть EEPROM без принятия каких-то особых мер. Рекомендуется использовать эти функции всегда. 56 .low = LFUSE_DEFAULT, .high = (FUSE_BOOTSZ0 & FUSE_EESAVE & FUSE_SPIEN & FUSE_JTAGEN), .extended = EFUSE_DEFAULT, }; int main(void){ return 0; } avr/fuse.h – Поддержка fuse-битов AVR Модуль позволяет разместить в особой секции ELF-файла информацию о состоянии fuse-битов, которую может извлекать программное обеспечение программатора для установки fuse-битов при программировании микроконтроллера. Размещение этой информации в ELF-файле осуществляется при компоновке программы. К сожалению, в HEX-формате сведения о состоянии fuse-битов размещены быть не могут. Данный модуль – универсальный, т.е. ориентирован на весь спектр поддерживаемых микроконтроллеров. Необходимо обязательно подключать к проекту модуль io.h, в котором определяются глобальные параметры, используемые затем в модуле fuse.h. Сам модуль fuse.h подключается в этом случае автоматически, поэтому нужды в явном его подключении нет. В зависимости от типа примененного микроконтроллера fuse-биты могут располагаться в младшем, старшем или расширенном fuse-байте. Для каждого отдельно взятого fuse-бита вводятся константы-маски, соответствующие наименованию fuse-бита, например, FUSE_SPIEN, FUSE_RSTDSBL и т.д.34 . Если необходимо установить несколько fuse-битов в одном байте, нужно объединять их при помощи битовой операции AND, т.е. так: (FUSE_SPIEN & FUSE_RSTDISBL & ….). Для каждого из fuse-байтов конкретного микроконтроллера определены значения fuse-битов по умолчанию, которые следует применять, если изменять их не требуется: LFUSE_DEFAULT для младшего байта, HFUSE_DEFAULT для старшего и EFUSE_DEFAULT для расширенного. Модуль определяет всего одну глобальную переменную-структуру FUSES, к заданию начальных значений для которой и сводится вся работа с fuse-битами. Данная переменная в зависимости от микроконтроллера может быть разного типа: если микроконтроллер содержит всего один fuse-байт, то эта переменная является просто переменной типа int8_t; если микроконтроллер имеет 2 fuse-байта, то FUSES представляет собой структуру с 2-я полями low и high соответственно типа int8_t каждое; для микроконтроллеров с тремя fuse-байтами FUSES представляет собой структуру с тремя полями типа int8_t – low, high и extended. Наконец, если МК содержит более 3-х fuseбайтов, FUSES определена как массив байтов соответствующей длины. Следует учитывать, что компоновщик использует только значения переменной FUSES, которые были указаны на этапе начальной инициализации, и игнорирует все изменения, которые могли быть сделаны в ходе выполнения программы. Пример использования см. на врезке. Важно, что всегда необходимо инициализировать все поля структуры FUSES. Суть рассматриваемого модуля заключена в том, что получаемый в результате компиляции elf-файл содержит абсолютно всю информацию, необходимую для правильного программирования микроконтроллера. Остается только решить вопрос, какое именно программное обеспечение в состоянии воспользоваться этой информацией. В какой-то мере эту задачу может решить утилита avrdude, входящая в состав WinAVR, однако, только в связке с другой утилитой avr-objcopy. Avrdude – это универсальный программатор, поддерживающий огромное количество схем для прошивки микроконтроллеров AVR, а утилита avr-objcopy позволяет извлекать из elf-файла информационные блоки о flash, eeprom и fuse-битах (из соответствующих секций) и сохранять их в виде hex-файлов, которые уже могут использоваться avrdude для прошивки. К сожалению, обе эти утилиты имеют немалое количество опций запуска, поэтому в рамках данного изложения не рассматриваются (это планируется сделать позже). avr/interrupt.h – Прерывания Общие сведения При обработке прерываний происходит конфликт между декларируемой Си независимостью от аппаратуры, и жесткой привязке векторов прерываний к этой самой аппаратуре. Таким образом, любая реализация обработки прерываний на Си становится уникальной для конкретной среды программирования. В WinAVR принято, что каждому определенному вектору прерывания сопоставлена особым образом поименованная функция-обработчик. Определение в 34 Т.к. fuse-биты отличаются в зависимости от модели микроконтроллера, следует ознакомиться с их наименованием по соответствующей документации к выбранному контроллеру; в настоящей документации они не приводятся. Радиолюбитель – 01/2011 СПРАВОЧНЫЙ МАТЕРИАЛ программе функции, названной в соответствии с определенными соглашениями, приводит к тому, что компилятор генерирует для нее нестандартный код пролога и эпилога, в частности, обязательно сохраняет SREG в начале и восстанавливает в конце, а так же использует для возврата из функции команду RETI. Кроме того, в таблице векторов прерываний для каждой определенной пользователем функции-обработчика помещается команда перехода к соответствующей функции. Далее кратко перечислены основные характерные особенности реализации обработчиков прерываний WinAVR. Подробности следует искать в описаниях соответствующих макросов. Детали реализации этого подхода остаются за кадром, т.к. программисту достаточно определить при помощи макроса ISR() функцию – и она будет назначена (и соответственно оформлена) обработчиком определенного прерывания. Особенность аппаратного строения AVR заключается в том, что во время работы одного обработчика прерываний все прочие прерывания запрещены. Такая ситуация в некоторых случаях может быть нежелательной, для чего можно (естественно, осознавая последствия, к которым это может привести) разрешить прерывания принудительно внутри функции-обработчика, используя макрос sei(). Но можно поступить и проще, указав для макроса ISR() параметр ISR_NOBLOCK, что заставит компилятор поместить соответствующую инструкцию глобального разрешения прерываний сразу после пролога функции (т.е. инициализирующего участка кода). При разработке программ не исключена ситуация, когда случайно будет сгенерировано прерывание, для которого нет назначенного обработчика (ситуация «баг программы»). По умолчанию компилятор генерирует код, который в этом случае приведет к переходу на адрес 0х0000, т.е. поведение кода равносильно сбросу микроконтроллера. Однако такое поведение можно изменить, определив особый обработчик «неверного прерывания», который автоматически назначается все «незанятым» векторам прерываний. Делается это при помощи указания для ISR() «плохого» вектора BADISR_vect. Такой обработчик будет использован для всех векторов прерываний, для которых не указаны явно их обработчики. Так же часто может возникать ситуация, когда один и тот же обработчик должен использоваться для нескольких прерываний. Можно, конечно, определить несколько абсолютно идентичных функций, однако, можно и сэкономить память, указав для ISR() параметр ISR_ALIASOF(). Еще одной занимательной ситуацией, связанной с прерываниями, является пробуждение микроконтроллера по прерыванию из «спящего» режима. В этом случае обработчик прерывания вызывается, однако, часто в нем никаких полезных действий выполняться не должно. Для таких случаев предусмотрен макрос EMPTY_INTERRUPT(), позволяющий Радиолюбитель – 01/2011 определить обработчик, который не делает ничего. Наконец, во многих случаях может оказаться, что неизбежно генерируемый компилятором для обработчика прерывания код пролога и эпилога оказывается слишком велик. Было бы удобно в таком случае использовать обработчик, реализованный на ассемблере, либо попытаться обойтись средствами Си для получения более оптимального кода. Сделать это можно при помощи параметра ISR_NAKED, указываемого для ISR(). В этом случае компилятор не генерирует вообще никакого пролога и эпилога, поэтому программист сам должен обеспечить сохранение контекста программы (в частности, содержимого SREG), и, главное, использовать макрос reti() для выхода из обработчика. Для каждого микроконтроллера определены свои константы для указания соответствующего вектора прерывания. Эти константы содержат в своем составе суффиксc _vect. Для совместимости с предыдущими версиями WinAVR сохранена и возможность определять обработчик прерывания при помощи «сигналов», т.е. констант, имеющих «префикс» SIG_ (префиксы и суффиксы соответствующих векторов определены в заголовочных файлах для каждого типа микроконтроллеров). Рекомендуется использовать «новую» форму _vect. Макросы и определения sei() sei() – глобальное разрешение прерываний cli() cli() – глобальное запрещение прерываний reti() reti() – макрос принудительно помещает в код программы инструкцию RETI, обеспечивая возврат из прерывания. ISR() ISR(vector, attributes) – макрос для определения обработчика прерываний. Первый параметр vector – это одна из констант, определяющих номер вектора прерывания. Реальные константы определены в соответствующем заголовочном файле модели микроконтроллера (оканчиваются на суффикс _vect, например, ADC_vect). Кроме этого можно использовать константу BADISR_vect для задания обработчика «по умолчанию». Второй параметр attributes – необязательный, определяет опциональные варианты реализации соответствующего обработчика. Допустимо использовать несколько атрибутов из числа следующих (атрибуты разделяются запятыми): ISR_BLOCK – атрибут по умолчанию, означает запрет прерываний, пока не отработает текущий обработчик ISR_NOBLOCK – разрешает глобально прерывания сразу после пролога обработчика ISR_NAKED – указывает, что вся реализация кода обработчика делается программистом (нет никакого пролога и эпилога) ISR_ALIASOF(v) – позволяет назначить текущему обработчику несколько векторов (см. врезку для ISR_ALIASOF). SIGNAL() SIGNAL(vector) – макрос определения обработчика, сохраненный исключительно для совместимости со старым кодом. Не рекомендуется использование в новых разработках. В качестве параметра vector должен получать константу с префиксом SIG_ из заголовочного файла конкретного микроконтроллера (например, SIG_ADC). EMPTY_INTERRUPT() EMPTY_INTERRUPT(vector) – макрос определения «пустого» обработчика, т.е. состоящего только из команды RETI. В качестве параметра принимаются те же значения, что и для ISR(). ISR_ALIAS() ISR_ALIAS(vector, target_vector) – макрос, связывающий два вектора в один обработчик. Не рекомендуется использование в новых разработках. В качестве параметра vector и target_vector используется любая из констант, как и для ISR(), причем параметр target_vector к моменту вызова макроса должен быть уже определен (см. врезку ISR_ALIAS): ISR_ALIASOF() ISR_ALIASOF(target_vector) – макрос, используемый в качестве опционального параметра ISR() для того, чтобы связать обработчик, определенный ISR(), еще и с вектором target_vector (см. врезку ISR_ALIASOF). Врезка ISR_ALIAS // определяем обработчик внешнего запроса прерывания №0 ISR(INT0_vect){ PORTB = 42; } // назначаем этот обработчик и для запроса №1 ISR_ALIAS(INT1_vect, INT0_vect); Врезка ISR_ALIASOF // определяем обработчик внешнего запроса прерывания №0 и одновременно №1 ISR(INT0_vect, ISR_ALIASOF(INT1_vect)){ PORTB = 42; } 57 СПРАВОЧНЫЙ МАТЕРИАЛ BADISR_vect BADISR_vect – константа «псевдовектора», обозначающая любой явно не заданный вектор. По умолчанию соответствует переходу на начало программной памяти, вызывая переинициализацию всего кода при поступлении «необрабатываемого» прерывания. ISR_BLOCK ISR_BLOCK – опциональный атрибут, используемый в ISR(), и обозначающий, что прерывания будут запрещены в течение работы всего обработчика прерывания. Если этот атрибут не задан явно (т.е. используется ISR() без атрибутов вообще), прерывания все равно будут запрещены. ISR_NOBLOCK ISR_NOBLOCK – опциональный атрибут, используемый в ISR(), и обозначающий, что прерывания будут разрешены в течение работы всего обработчика прерывания. Это позволит быстрее отреагировать на ожидающие в очереди другие запросы прерываний, однако увеличивает требования к объему стека. ISR_NAKED ISR_NAKED – опциональный атрибут, используемый в ISR(), и обозначающий, что компилятор не должен добавлять пролог и эпилог к функции. В этом случае программист обязан сам выполнить все необходимые меры, включая выход из обработчика макросом reti(). avr/io.h – Специфичные для AVR функции ввода-вывода Этот заголовочный файл по существу реализует автоматическое подключение одного конкретного файла, соответствующего выбранному микроконтроллеру. Во всяком случае, для определения констант и макросов портов ввода-вывода и т.п. периферии программист более не должен применять никаких усилий, кроме подключения этого файла к проекту и задания типа микроконтроллера. Фактически происходит подключение целого ряда заголовочных файлов, в которых определены константы и макросы для обращения к различной периферии микроконтроллера (см. например avr/sfr_defs.h – Регистры специальных функций AVR). Рассмотрение всех этих определений опущено, т.к. подразумевается, что программист, работающий с AVR, вполне информирован о его периферии. Из прочего стоит обратить внимание лишь на следующие константы: RAMEND – соответствует адресу последней существующей ячейки ОЗУ, т.е. в сущности, определяет размер ОЗУ выбранного микроконтроллера. XRAMEND – соответствует адресу последней существующей ячейки внешнего или дополнительно адресуемого ОЗУ (если его поддержка имеется в микроконтроллере). E2END – соответствует адресу последней ячейки встроенного EEPROM 58 FLASHEND – соответствует адресу последней ячейки памяти программ, т.е. фактически определяет размер сегмента кода. SPM_PAGESIZE – соответствует размеру страницы для операций самопрограммирования (т.е. записи из программы в сегмент кода). avr/pgmspace.h – Поддержка обращения к сегменту кода AVR В этом модуле определяются ряд функций и макросов, позволяющих обращаться к данным, хранящимся в памяти программ, т.е. сегменте кода. Для этого необходимо, чтобы микроконтроллер поддерживал инструкции LPM или ELPM. Функции этого модуля – попытка облегчить процесс адаптации программ для IAR, однако полной совместимости нет по принципиальным особенностям GCC. Если вы работаете со строками, которые постоянно размещены в ОЗУ, вы можете использовать функции для работы с ними из модуля string.h – Строки, но для неизменных строк более удачным будет размещение их в сегменте кода. В этом случае вы можете работать с ними при помощи функций этого модуля, которые совпадают по имени с аналогичными из string.h, но имеют суффикс _Р – признак того, что строка находится в программной памяти (такой принцип так же использовался и в модуле stdio.h – Стандартный ввод-вывод). Однако следует учитывать, что функции из настоящего модуля могут работать только со строками, размещенными в первых 64К адресного пространства микроконтроллера, т.к. не используют инструкцию ELPM. Это не страшно в обычных случаях. Но может вызвать проблемы, если имеется много текстовых констант или же используется самозагрузка кода. При написании программы вы не должны беспокоиться о месте размещения строк, т.к. компоновщик автоматически разместит все данные в программной памяти сразу после векторов прерывания. Так же следует помнить о том, что все используемые для обращения к сегменту кода указатели представляют себя адреса байтов (хотя программная адресация у AVR – 16-битная). Определения модуля Модуль определяет ряд констант, атрибутов и макросов, облегчающих разработку программ, работающих с данными в сегменте кода. PROGMEM PROGMEM – макрос, устанавливающий атрибут, указывающий, что переменная на самом деле представляет собой константу в сегменте кода PSTR() PSTR(s) – макрос, используемый для определения указателя на строку s, размещенную в сегменте кода PGM_P PGM_P – макрос, определяющий указатель на символ в сегменте кода PGM_VOID_P PGM_VOID_P – макрос, определяющий указатель на объект в сегменте кода pgm_read_byte_near() pgm_read_byte_near(address_short) – макрос, возвращающий значение байта, считанного из программной памяти по адресу address_short (в первых 64К). pgm_read_word_near() pgm_read_word_near(address_short) – макрос, возвращающий значение 16-битного числа, считанного из программной памяти по адресу address_short (в первых 64К). pgm_read_dword_near() pgm_read_dword_near(address_short) – макрос, возвращающий значение 32-битного числа, считанного из программной памяти по адресу address_short (в первых 64К). pgm_read_byte_far() pgm_read_byte_far(address_long) – макрос, возвращающий значение байта, считанного из программной памяти по адресу address_long (32 бита, нет ограничения в 64К). pgm_read_word_far() pgm_read_word_far(address_long) – макрос, возвращающий значение 16-битного числа, считанного из программной памяти по адресу address_long (32 бита, нет ограничения в 64К). pgm_read_dword_far() pgm_read_dword_far(address_long) – макрос, возвращающий значение 32-битного числа, считанного из программной памяти по адресу address_long (32 бита, нет ограничения в 64К). pgm_read_byte() pgm_read_byte(address_short) – аналог pgm_read_byte_near() pgm_read_word() pgm_read_word(address_short) – аналог pgm_read_word_near() pgm_read_dword() pgm_read_dword(address_short) – аналог pgm_read_dword_near() Типы, вводимые модулем Модуль вводит ряд типов, которые просто облегчают определение констант в сегменте кода. По смыслу все эти типы аналогичны определенным в inttypes.h – Целочисленные преобразования, только содержат префикс prog_, означающий, что соответствующий объект размещается в памяти программ. В связи с этим расшифровка значения типов не приводится. prog_void prog_char prog_uchar prog_int8_t prog_uint8_t prog_int16_t Радиолюбитель – 01/2011 СПРАВОЧНЫЙ МАТЕРИАЛ prog_uint16_t prog_int32_t prog_uint32_t prog_int64_t prog_uint64_t Функции модуля Так как все функции не более чем аналоги string.h – Строки, вместо подробного описания будут указаны ссылки на функции для работы со строками в ОЗУ. Единственное, на что должен обратить программист, это типы передаваемых в функции параметров и возвращаемых значений: как правило, все указатели в функциях этого модуля – это указатели на объекты в сегменте кода. strcmp_P() int strcmp_P (const char *s1, PGM_P s2) Функция аналогична strcmp(), за исключением того, что s2 находится в программной памяти. strcpy_P() char * strcpy_P (char *s1, PGM_P s2) Функция аналогична strcpy(), за исключением того, что s2 находится в программной памяти. strpbrk_P() char * strpbrk_P (const char *s, PGM_P accept) Функция аналогична strpbrk(), за исключением того, что accept находится в программной памяти strrchr_P() PGM_P strrchr_P (PGM_P s, int val) Функция аналогична strrchr(), за исключением того, что s находится в программной памяти strcspn_P() size_t strcspn_P (const char *s, PGM_P reject) Функция аналогична strcspn(), за исключением того, что reject находится в программной памяти. strsep_P() char * strsep_P (char **sp, PGM_P delim) Функция аналогична strsep(), за исключением того, что delim находится в программной памяти memchr_P() PGM_VOID_P memchr_P (PGM_VOID_P s, int val, size_t len) Функция ищет в области программной памяти s символ, возвращая указатель на него или NULL, если не найдено. Подробности см. в memrchr() strlcat_P() size_t strlcat_P (char *dest, PGM_P src, size_t siz) Функция аналогична strlcat(), за исключением того, что src находится в программной памяти strspn_P() size_t strspn_P (const char *s, PGM_P accept) Функция аналогична strspn(), за исключением того, что accept находится в программной памяти memcmp_P() int memcmp_P (const void *s1, PGM_VOID_P s2, size_t len) Функция сравнивает область ОЗУ s1 и памяти программ s2. Подробности см. в memcmp() strlcpy_P() size_t strlcpy_P (char *dest, PGM_P src, size_t siz) Функция аналогична strlcpy(), за исключением того, что src находится в программной памяти strstr_P() char * strstr_P (const char *s1, PGM_P s2) Функция аналогична strstr(), за исключением того, что s2 находится в программной памяти memcpy_P() void * memcpy_P (void *dest, PGM_VOID_P src, size_t len) Функция копирует в область ОЗу dest содержимое области памяти программ src. Подробности см. в memcpy() strlen_P() size_t strlen_P (PGM_P s) Функция аналогична strlen(), за исключением того, что s находится в программной памяти memrchr_P() PGM_VOID_P memrchr_P (PGM_VOID_P s, int val, size_t len) Функция ищет в области программной памяти s символ. Подробности см. в memrchr() и memchr_P() strcasecmp_P() int strcasecmp_P (const char *s1, PGM_P s2) Функция сравнивает строку s1 в ОЗУ со строкой в памяти программ s2 без учета регистра символов. См. strcasecmp() strcat_P() char * strcat_P (char *dest, PGM_P src) Функция осуществляет конкатенацию строки src из памяти программ к строке dest в ОЗУ. См. strcat() strchr_P() PGM_P strchr_P (PGM_P s, int val) Функция ищет символ в строке s в программной памяти. См. strchr() strchrnul_P() PGM_P strchrnul_P (PGM_P s, int val) Функция, аналогичная strchr_P(), за исключением того, что всегда возвращает неNULL указатель: если символ не найден, возвращается указатель на завершающий ноль строки s Радиолюбитель – 01/2011 strncasecmp_P() int strncasecmp_P (const char *s1, PGM_P s2, size_t n) Функция аналогична strncasecmp(), за исключением того, что s2 находится в программной памяти memmem_P() void * memmem_P (const void *s1, size_t len1, PGM_VOID_P s2, size_t len2) Функция аналогична memmem(), за исключением того, что s2 находится в программной памяти strcasestr_P() char * strcasestr_P (const char *s1, PGM_P s2) Функция аналогична strcasestr(), за исключением того, что s2 находится в программной памяти strncat_P() char * strncat_P (char *dest, PGM_P src, size_t len) Функция аналогична strncat(), за исключением того, что src находится в программной памяти strncmp_P() int strncmp_P (const char *s1, PGM_P s2, size_t n) Функция аналогична strncmp(), за исключением того, что s2 находится в программной памяти strncpy_P() char * strncpy_P (char *dest, PGM_P src, size_t n) Функция аналогична strncpy(), за исключением того, что src находится в программной памяти strnlen_P() size_t strnlen_P (PGM_P src, size_t len) Функция аналогична strnlen(), за исключением того, что src находится в программной памяти Продолжение в №2/2011 59 СПРАВОЧНЫЙ МАТЕРИАЛ Роман Абраш г. Новочеркасск E-mail: arv@radioliga.com Продолжение. Начало в №1-12/2010; №1/2011 avr/power.h – Поддержка PRR Некоторые модели микроконтроллеров AVR имеют встроенный регистр PRR (Power Reduction Register – регистр снижения мощности) или несколько таких регистров PRRn. С их помощью имеется возможность отключить часть неиспользуемой программой периферии для снижения потребляемой микроконтроллером мощности. Этот модуль определяет рад макросов, облегчающих использование PRR. Однако, нормальное их функционирование возможно только для тех микроконтроллеров, которые действительно содержат этот регистр (регистры) и соответствующую периферию; кроме того, иногда одна и та же периферия отличается в наименовании (например, USART и USART0). Здесь перечислены все соответствующие макросы, однако о наличие поддержки ими выбранного программистом микроконтроллера следует справляться в документации. При попытке использовать неподдерживаемый микроконтроллер будут возникать ошибки компиляции. power_adc_enable() – включение встроенного АЦП (не путать35 с битом ADEN в соответствующем регистре) power_adc_disable() – выключение АЦП power_lcd_enable() – включение модуля ЖКИ-драйвера power_lcd_disable() – выключение модуля ЖКИ-драйвера power_psc0_enable() – включение модуля Power Stage Controller 0 power_psc0_disable() – выключение модуля Power Stage Controller 0 power_psc1_enable() – включение модуля Power Stage Controller 1 power_psc1_disable() – выключение модуля Power Stage Controller 1 power_psc2_enable() – включение модуля Power Stage Controller 2 power_psc2_disable() – выключение модуля Power Stage Controller 2 power_spi_enable() – включение модуля SPI power_spi_disable() – выключение модуля SPI power_timer0_enable() – включение таймера 0 power_timer0_disable() – выключение таймера 0 power_timer1_enable() – включение таймера 1 power_timer1_disable() – выключение таймера 1 power_timer2_enable() – включение таймера 2 35 Данное примечание относится и к другим макросам – они обеспечивают именно отключение/включение питания соответствующей периферии при помощи регистров PRR. 58 Книга по работе с WinAVR и AVR Studio power_timer2_disable() – выключение таймера 2 power_timer3_enable() – включение таймера 3 power_timer3_disable() – выключение таймера 3 power_timer4_enable() – включение таймера 4 power_timer4_disable() – выключение таймера 4 power_timer5_enable() – включение таймера 5 power_timer5_disable() – выключение таймера 5 power_twi_enable() – включение модуля TWI power_twi_disable() – выключение модуля TWI power_usart_enable() – включение модуля USART power_usart_disable() – выключение модуля USART power_usart0_enable() – включение модуля USART0 power_usart0_disable() – выключение модуля USART0 power_usart1_enable() – включение модуля USART1 power_usart1_disable() – выключение модуля USART1 power_usart2_enable() – включение модуля USART2 power_usart2_disable() – выключение модуля USART2 power_usart3_enable() – включение модуля USART3 power_usart3_disable() – выключение модуля USART3 power_usb_enable() – включение модуля USB power_usb_disable() – выключение модуля USB power_usi_enable() – включение модуля USI power_usi_disable() – выключение модуля USI power_vadc_enable() – включение модуля VADC power_vadc_disable() – выключение модуля VADC power_all_enable() – включить все модули power_all_disable() – выключить все модули Некоторые модели AVR имеют регистр CLKPR предварительного деления тактовой частоты, при помощи которого так же можно снижать потребляемую мощность. Для работы с этим регистром введен особый тип: typedef enum{ clock_div_1 = 0, clock_div_2 = 1, clock_div_4 = 2, clock_div_8 = 3, clock_div_16 = 4, clock_div_32 = 5, clock_div_64 = 6, clock_div_128 = 7, clock_div_256 = 8 } clock_div_t; и 2 макроса: clock_prescale_set(x) – установка значения делителя частоты (х – одно из значений clock_div_t) clock_prescale_get() – получить значение текущего делителя (возвращаемый тип результата – clock_div_t) avr/sfr_defs.h – Регистры специальных функций AVR Данный модуль определяет символьные имена для всех регистров специальных функций, а так же вводит ряд макросов, упрощающих типичные действия над битовыми значениями этих регистров. Модуль подключается автоматически, если подключен модуль avr/io.h – Специфичные для AVR функции ввода-вывода, поэтому нет необходимости подключать его явно. Макросы Все макросы определены, как макросы-функции. _BV() _BV(bit) – макрос, преобразующий номер бита в битовую маску. Данная команда используется для работы с битами SFR, выполняя следующее действие: (1<<(bit)). bit_is_set() bit_is_set(sfr, bit) – макрос, возвращающий логическое значение ИСТИНА, если бит bit в указанном регистре sfr установлен. Используется для проверки состояния флагов. bit_is_clear() bit_is_clear(sfr, bit) – макрос, возвращающий логическое значение ИСТИНА, если бит bit в регистре sfr не установлен. Используется для проверки состояния флагов. loop_until_bit_is_set() loop_until_bit_is_set(sfr, bit) – макрос, выполняющий цикл ожидания (путем непрерывного опроса соответствующего sfr) до тех пор, пока бит bit в регистре sfr не будет установлен. loop_until_bit_is_clear() loop_until_bit_is_clear(sfr, bit) – макрос, выполняющий цикл ожидания (путем непрерывного опроса соответствующего sfr) до тех пор, пока бит bit в регистре sfr не будет сброшен. avr/sleep.h – Управление энергосберегающими режимами и режимами «сна» В этом модуле определен ряд макросов и функций, реализующих общий интерфейс управления режимами «сна». Радиолюбитель – 02/2011 СПРАВОЧНЫЙ МАТЕРИАЛ Использование инструкции SLEEP позволяет снизить общее энергопотребление в некоторых применениях. Микроконтроллеры AVR реализуют несколько различных вариантов «сна», для которых в модуле определены следующие константы (о поддержке выбранным контроллером соответствующего режима следует справиться в документации к контроллеру, не все константы определены для любого контроллера): SLEEP_MODE_IDLE – режим «бездействия» Idle SLEEP_MODE_PWR_DOWN – режим «отключения» Power Down SLEEP_MODE_PWR_SAVE – режим «экономии» Power Save SLEEP_MODE_ADC – режим «снижения шума АЦП» ADC Noise Reduction SLEEP_MODE_STANDBY – режим «ожидания» SLEEP_MODE_EXT_STANDBY – расширенный режим «ожидания» Практически управление режимом энергосбережения сводится к вызову макроса set_sleep_mode() для указания нужного режима и последующего включения этого режима макросом sleep_mode(). Следует помнить, что если нет необходимости остановить работу микроконтроллера вплоть до аппаратного сброса, необходимо разрешить прерывания до выполнения этих макросов. В некоторых случаях может потребоваться большая гибкость в управлении режимами экономии, для чего предусмотрены отдельные функции sleep_enable(), sleep_disable() и sleep_cpu(). Функции и макросы модуля set_sleep_mode() set_sleep_mode(mode) – макрос, подготавливающий микроконтроллер к заданному режиму сна. Параметр mode – одна из определенных ранее констант режима. Этот макрос эквивалентен переходу в режим Idle, т.е. остановки тактового генератора и отключения периферии, если это требуется режимом, не происходит. Для полноценного включения режима требуется второй макрос sleep_mode() sleep_mode() sleep_mode() – макрос, переводящий микроконтроллер в режим сна. Его реализация такова, что программист вправе считать, что «возврат» из макроса произойдет уже после «пробуждения» контроллера. sleep_enable() void sleep_enable(void) – функция, устанавливающая бит разрешения режима сна (SE-бит) sleep_disable() void sleep_disable(void) – функция, сбрасывающая бит разрешения режима сна (SEбит) sleep_cpu() void sleep_cpu(void) – функция, «усыпляющая» микроконтроллер, т.е. эквивалентная команде SLEEP Радиолюбитель – 02/2011 avr/version.h – Контроль версии avr-libc Модуль определяет несколько констант, которые могут использоваться в программе для проверки совместимости исходного текста с используемой версией avr-libc. __AVR_LIBC_VERSION_STRING__ – строчное представление версии библиотеки, например, ″1.6.2″ __AVR_LIBC_VERSION__ – версия библиотеки в виде длинного целого числа без знака, например, 10602UL __AVR_LIBC_DATE_STRING__ – строчное представление даты релиза библиотеки (совпадает с релизом WinAVR), например, ″20080403″ __AVR_LIBC_DATE__ - дата релиза библиотеки в виде длинного целого числа без знака, например, 20080403UL __AVR_LIBC_MAJOR__ – версия библиотеки, например, 1 __AVR_LIBC_MINOR__ – «подверсия» библиотеки, например, 6 __AVR_LIBC_REVISION__ – номер релиза подверсии, например, 2 avr/wdt.h – Поддержка WDT AVR Модуль реализует необходимый и достаточный минимум макросов, при помощи которых реализуется взаимодействие со сторожевым таймером (WDT) любого микроконтроллера AVR. Для работы с WDT требуется выполнение определенной четко регламентированной процедуры, которую макросы реализуют автоматически. В частности, в нужных случаях на время происходит глобальное запрещение прерываний. Программист может не беспокоиться о дополнительных действиях, используя предлагаемые макросы. Единственное, о чем следует помнить, это особенность, присущая новым моделям микроконтроллеров (например, ATMega88 и т.п.): после аппаратного сброса WDT продолжает работать, причем с минимальным предделителем (обычно, период WDT составляет 15 мс). Поэтому следует выключать его (если нужно) как можно раньше. При этом может потребоваться особый подход для того, чтобы определить причину аппаратного сброса. Рекомендуется придерживаться следующего подхода (см. врезку): В приведенном примере для сохранения значения MCUSR использована переменная mcusr_mirror, которая не получает значение 0 по умолчанию (характерное для Си-программ) – секция «.noinit». А функция, которая осуществляет инициализацию этой переменной содержимым MCUSR и одновременно запрещает работу WDT, get_mcusr() размещается в секции автоматической инициализации пользователя «.init3», т.е. будет выполнена сразу после инициализации стека. Функция не имеет пролога и эпилога, поэтому ее код будет помещен в соответствующей секции как inline. Дополнительно о секциях памяти и порядке их инициализации см. в главе Секции памяти. Макросы и константы модуля В модуле определен ряд констант для задания периода переполнения WDT. Так как некоторые модели микроконтроллеров могут иметь и другие значения программируемых периодов, реальное количество констант может быть другим (нетрудно выяснить, взглянув в содержимое файла wdh.h). Из наименования константы очевидно ее смысловое значение, поэтому детальное описание не приводится. WDTO_15MS WDTO_30MS WDTO_60MS WDTO_120MS WDTO_250MS WDTO_500MS WDTO_1S WDTO_2S WDTO_4S WDTO_8S Так же в модуле определены три макроса типа функция. wdt_enable() wdt_enable(value) – макрос, включающий WDT с периодом переполнения, заданным значением value, которое должно быть одной из рассмотренных ранее констант WDTO_xxxS. wdt_reset() wdt_reset() – макрос, осуществляющий сброс счетчика WDT, эквивалент инструкции WDR. #include <stdint.h> #include <avr/wdt.h> // объявление «неинициализируемой» переменной uint8_t mcusr_mirror _attribute_ ((section (І.noinitІ))); // объявление функции «запоминания» значения MCUSR void get_mcusr(void) \ __attribute__((naked)) \ /* без пролога и эпилога */ __attribute__((section(І.init3І))); /* в секции автоматической инициализации пользователя */ void get_mcusr(void){ mcusr_mirror = MCUSR; MCUSR = 0; wdt_disable(); } 59 СПРАВОЧНЫЙ МАТЕРИАЛ wdt_disable() wdt_disable() – макрос, выключающий WDT. Если WDT включен постоянно (т.е. так задано fuse-битами), этот макрос, естественно, не выполняет своей функции. ВСПОМОГАТЕЛЬНЫЕ МОДУЛИ К вспомогательным модулям отнесены те из входящих в AVR-LIBC, которые служат для упрощения некоторых действий, часто необходимых в системах на базе микроконтроллеров AVR. Это сервисные функции и функции, разработанные по инициативе сообщества программистов (GCC и WinAVR – это открытый проект, любой доброволец имеет возможность пополнить их функциональность, если она одобрена остальным сообществом). util/atomic.h – Атомарные и неатомарные участки кода Модуль реализует средства управления «атомарностью»36 участков кода. Данный модуль использует расширения СИ, вводимые стандартом ISO C99, и поэтому может использоваться только с опцией компилятора –std=с99 или –std=gnu99. Определения модуля ATOMIC_BLOCK() ATOMIC_BLOCK(type) – макрос, служащий для определения блока кода, ограниченного фигурными скобками, как исполняющегося атомарно. В качестве параметра type должна использоваться константа ATOMIC_FORCEON или ATOMIC_RESTORESTATE. Пример использования макроса: ATOMIC_BLOCK(ATOMIC_FORCEON){ ctr_copy = ctr; } NONATOMIC_BLOCK() NONATOMIC_BLOCK(type) – макрос, служащий для определения блока кода, ограниченного фигурными скобками, как исполняющегося «не атомарно» (см. ATOMIC_BLOCK()). Это может потребоваться, если внутри большого атомарного блока есть участки, не требующие атомарности. ATOMIC_RESTORESTATE ATOMIC_RESTORESTATE – константа для использования в качестве параметра макроса ATOMIC_BLOCK(). Указывает, что в начале блока состояние SREG должно быть запомнено, а в конце блока – восстановлено в прежнем виде. Это гарантирует, что состояние флага глобального разрешения прерываний после завершения атомарного блока будет таким же, как и перед его 36 Полноценное определение термина «атомарный» не приводится, в данном контексте можно считать, что это означает исполнение одного или более операторов Си как одной команды, т.е. без возможности прерывания посередине участка. Не смотря на кажущуюся странность этого термина в применении к одному оператору, это так: оператор long tmp = 0; фактически будет оттранслирован в 5 ассемблерных инструкций, исполняемых последовательно; между ними вполне возможно возникновение прерывания, что будет соответствовать прерыванию «в середине» оператора. 60 началом (внутри атомарного блока прерывания, разумеется, запрещены). ATOMIC_FORCEON ATOMIC_FORCEON – константа для использования в качестве параметра макроса ATOMIC_BLOCK(). Указывает, что в конце атомарного блока прерывания просто должны быть принудительно разрешены. Использование этого параметра позволяет несколько сократить объем кода и повысить скорость исполнения атомарного блока, однако программист должен быть уверен, что глобальное разрешение прерываний после атомарного блока – это действительно то, что он хочет. NONATOMIC_RESTORESTATE NONATOMIC_RESTORESTATE – константа для использования в качестве параметра макроса NONATOMIC_BLOCK(). Указывает на то, что перед началом не-атомного блока должно быть сохранено значение SREG, а после его завершения – восстановлено. Это гарантирует, что блок действительно будет выполнен не атомарно, при этом состояние флага глобального разрешения прерываний после блока будет таким же, как и перед ним (внутри не-атомарного блока прерывания, естественно, разрешены). NONATOMIC_FORCEOFF NONATOMIC_FORCEOFF – константа для использования в качестве параметра макроса NONATOMIC_BLOCK(). Указывает на то, что по завершению не-атомарного блока прерывания просто должны быть принудительно запрещены. Использование этого параметра позволяет несколько сократить объем кода и повысить скорость исполнения не-атомарного блока, однако программист должен быть уверен, что глобальное запрещение прерываний после не-атомарного блока – это действительно то, что он хочет. Важной особенностью ATOMIC-блоков кода является то, что внутри могут быть использованы любые операторы Си, в том числе break и return, при этом состояние аппарата прерываний будет корректно востановлено при исполнении этих операторов. util/crc16.h – Вычисления CRC Модуль содержит определения нескольких inline-функций для быстрого вычисления циклических контрольных сумм (CRC) по широкоизвестным полиномам. Алгоритм использования этих функций для контроля целостности блока данных (основное назначение CRC) в общем случае следующий: - вводится переменная, например, C_R_C (ей должно быть присвоено строго определенное начальное значение) - последовательно для каждого из значений проверяемого блока вызывается соответствующая функция, которая модифицирует значение переменной C_R_C - после того, как все данные «обсчитаны» сравнение полученного значения C_R_C с заранее известным (правильным) значением CRC или нулем, если правильное значение CRC входило в состав обрабатываемого блока данных. Функции модуля _crc16_update() uint16_t _crc16_update (uint16_t crc, uint8_t data) Параметры: uint16_t crc – текущее значение CRC uint8_t data – байт данных Возвращаемое значение: новое значение CRC Описание: функция подсчета 16-разрядной контрольной суммы по полиному, типичному для дисковых накопителей x16 + x15 + x2 + 1 (0xА001). Для верного подсчета контрольной суммы начальное значение crc должно быть 0xFFFF. _crc_xmodem_update() uint16_t _crc_xmodem_update (uint16_t crc, uint8_t data) Параметры: uint16_t crc – текущее значение CRC uint8_t data – байт данных Возвращаемое значение: новое значение CRC Описание: функция подсчета 16-разрядной контрольной суммы по полиному, требуемому протоколом Xmodem x16 + x12 + x5 + 1 (0x1021). Для верного подсчета контрольной суммы начальное значение crc должно быть 0. _crc_ccitt_update() uint16_t _crc_ccitt_update (uint16_t crc, uint8_t data) Параметры: uint16_t crc – текущее значение CRC uint8_t data – байт данных Возвращаемое значение: новое значение CRC Описание: функция подсчета 16-разрядной контрольной суммы по полиному, требуемому стандартом CCITT (используется в протоколах PPP и IrDA) x16 + x12 + x5 + 1 (0x8408). Для верного подсчета контрольной суммы начальное значение crc должно быть 0xFFFF. Примечание: хотя полином выглядит точно так же, как и для Xmodem, существует принципиальная разница в алгоритме вычисления CRC, связанная с порядком поступления битов на обработку (старшиймладший). _crc_ibutton_update() uint8_t _crc_ibutton_update (uint8_t crc, uint8_t data) Параметры: uint8_t crc – текущее значение CRC uint8_t data – байт данных Возвращаемое значение: новое значение CRC Описание: функция подсчета 8-разрядной контрольной суммы по полиному, используемому в приборах iButton37 x8 + x5 + x4 + 1 (0x8C). Для верного подсчета контрольной суммы начальное значение crc должно быть 0. 37 Зарегистрированная торговая марка Maxim-Dallas. Радиолюбитель – 02/2011 СПРАВОЧНЫЙ МАТЕРИАЛ util/delay.h – Реализация задержек программными циклами Модуль определяет функции, являющиеся всего лишь более удобной «оберткой» базовым функциям задержек программными циклами (см. util/delay_basic.h – Базовые задержки программными циклами). Для нормального функционирования функций необходимо, чтобы было определено значение константы F_CPU, равное тактовой частоте микроконтроллера в герцах, до подключения модуля. Возможно определить эту константу внутри make-файла или, что то же самое, непосредственно в параметрах компилятора. Функции задержек реализованы следующим образом. По значению задержки, полученному как параметр функции (в виде константы) на этапе компиляции вычисляется количество пустых программных циклов, которые при заданной тактовой частоте нужно выполнить для получения этой задержки. В итоге компилятор строит код, который и выполняет эту задачу. Чтобы это было возможно, необходимо, чтобы вычисления над константами с плавающей точкой были проведены на этапе компиляции, т.е. необходимо, чтобы была включена оптимизация при компиляции. Если проект скомпилирован при отключенной оптимизации – задержки, обеспечиваемые функциями, не гарантируются. Таким образом, для использования модуля delay.h необходимо выполнить 2 условия: 1. Объявить F_CPU, равное тактовой частоте контроллера в герцах до подключения модуля 2. Включить оптимизацию (любого уровня) при компиляции. Примечание: функции реализуют задержки путем программных циклов, которые могут быть могут быть прерваны запросом прерывания. Разумеется, в этом случае длительность задержки окажется больше заданного значения, и чем чаще происходят прерывания и чем сложнее (т.е. длительнее) обработчики прерываний, тем эта погрешность выше. Функции модуля _delay_us() void _delay_us(double us) Описание: функция осуществляет задержку в us микросекунд. Максимально возможное значение задержки 768 / F_CPU микросекунд (где F_CPU в мегагерцах). Программист не обязан следить за тем, чтобы параметр us находился в допустимых пределах – если его значение окажется больше, чем допустимо, вызов функции автоматически будет перенаправлен в функцию _delay_ms() (никаких уведомлений об этом не осуществляется). _delay_ms() void _delay_ms(double ms) Описание: функция осуществляет задержку в ms миллисекунд. Максимально возможное значение задержки 262.14 / Радиолюбитель – 02/2011 F_CPU миллисекунд (где F_CPU в мегагерцах). Программист не обязан следить за тем, чтобы параметр ms находился в допустимых пределах – если его значение окажется больше, чем допустимо, произойдет автоматическое снижение точности выдержки интервала (никаких уведомлений об этом не происходит), таким образом, функция сможет реализовывать задержки вплоть до 65535 секунд. util/delay_basic.h – Базовые задержки программными циклами Модуль определяет две базовых inlineфункции задержек программным циклом известной длительности, которые предназначены для получения очень коротких и точных задержек, и активно используются функциями _delay_us() и _delay_ms(). Функции не выполняют запрещения прерываний на время своей работы. Поэтому следует учитывать, что это может повлиять на точность формируемых задержек. Для организации длительных задержек более эффективно использование аппаратных таймеров. Реализуется это несколько необычным способом. Перед подключением модуля следует определить две константы: F_CPU (тактовая частота микроконтроллера в герцах) и BAUD (желаемая скорость работы UART в бодах). Так же можно дополнительно определить значение BAUD_TOL, которое задает допустимую погрешность скорости в процентах (по умолчанию 2%). После определения этих констант следует подключить модуль setbaud.h, т.е. например, так: #include <avr/io.h> #define F_CPU 4000000 // указали тактовую частоту контроллера static void uart_9600(void){ #define BAUD 9600 #include <util/setbaud.h> UBRRH = UBRRH_VALUE; UBRRL = UBRRL_VALUE; #if USE_2X UCSRA |= (1 << U2X); #else UCSRA &= ~(1 << U2X); Функции модуля #endif } _delay_loop_1() void _delay_loop_1 (uint8_t count) Описание: функция обеспечивает исполнение count раз (от 1 до 256) наикратчайшего цикла со счетчиком (т.е. одна итерация цикла длится 3 машинных такта). Дополнительно тратится время на инициализацию задействованного счетного регистра. Примечание: для организации 256 повторений цикла следует передать в функцию параметр ноль. static void uart_38400(void){ #undef BAUD // отменим определение, чтобы избежать предупреждения компилятора #define BAUD 38400 #include <util/setbaud.h> UBRRH = UBRRH_VALUE; UBRRL = UBRRL_VALUE; #if USE_2X UCSRA |= (1 << U2X); #else UCSRA &= ~(1 << U2X); #endif _delay_loop_2() void _delay_loop_2 (uint16_t count) Описание: функция обеспечивает исполнение count раз (от 1 до 65536) наикратчайшего цикла со счетчиком (т.е. одна итерация цикла длится 4 машинных такта). Дополнительно тратится время на инициализацию задействованного счетного регистра. Примечание: для организации 65536 повторений цикла следует передать в функцию параметр ноль. util/parity.h – Генерация битов четности Модуль определяет единственный макрос, реализующий оптимизированную ассемблерную функцию вычисления бита четности для байта данных (может использоваться при обмене по UART). parity_even_bit() parity_even_bit(val) – макрос, возвращающий 1, если число единиц a байте val нечетное. util/setbaud.h – Упрощение вычисления скоростей UART Модуль облегчает расчет значений регистров управления аппаратного UART. } В приведенном примере показано определение двух функций, которые настраивают UART на скорости 9600 и 38400 бод соответственно. Делается это путем переопределения вышеупомянутых констант и многократного подключения модуля setbaud.h. В самом модуле на основании заданных констант производится расчет значений других констант, которые затем используются для инициализации UBRR и UCSRA. Таким образом, модуль использует ряд «входных» определений, формируя рад «выходных»: UBRR_VALUE – значение для записи в UBRR UBRRL_VALUE – значение для записи в UBRRL UBRRH_VALUE – значение для записи в UBRRH USE_2X – логический флаг, определяющий необходимость установки бита U2X в регистре UCSRA. Продолжение в №3/2011 61 СПРАВОЧНЫЙ МАТЕРИАЛ Книга по работе с WinAVR и AVR Studio Продолжение. Начало в №1-12/2010; №1-2/2011 РАСПРЕДЕЛЕНИЕ ПАМЯТИ AVR-GCC Компилятор GCC, адаптированный под архитектуру AVR, использует свои собственные соглашения о том, как распределяется память во время компиляции программы и во время ее исполнения. Знание этих особенностей позволит программисту более тонко влиять на ход своей работы, добиваясь желаемого результата, порой недостижимого без этих знаний. Секции памяти При компиляции программы происходит распределение результатов работы компилятора по различным областям памяти микроконтроллера, называемыми секциями (некий аналог традиционно используемым сегментам памяти). Компилятор размещает данные и исполняемые коды по этим секциям, а уже компоновщик затем собирает эти секции в блоки, которые в итоге предназначаются для программирования во flash-память программ микроконтроллера, EEPROM данных и т.п. Каждая секция должна иметь символьное имя. Существует ряд предопределенных секций, и может быть создано любое количество пользовательских секций. Каждая секция характеризуется областью адресов конкретного типа памяти, внутри которой размещается ее содержимое. В общих чертах суть использования секций можно пояснить следующим примером. Предположим, необходимо разместить какую-либо функцию в строго определенной области памяти программ (это часто бывает необходимо для микроконтроллерных систем, например для загрузчиков – см. avr/boot.h – Поддержка загрузчиков AVR). Для решения этой задачи необходимо определить пользовательскую секцию, начинающуюся с нужного адреса, а затем в тексте программы для нужной функции указать атрибут section(«.user»), где «.user» – это заданное имя секции. В результате компоновщик разместит функцию как раз начиная с начала секции памяти, что и требовалось. Разумеется, аналогичным образом можно определить секции в областях EEPROM или области ОЗУ, «привязав» таким образом переменные к конкретным адресам. Далее рассмотрены основные стандартные секции памяти, поддерживаемые компилятором. .text Основная секция для сегмента кода. В нее помещаются все пользовательские функции, т.е. то, что определено в тексте программы (отсюда, очевидно, и название секции). .data Секция статически проинициализированных переменных пользователя. Если программист использует определения типа char str[] = “Это пример”; long counter = 12345; то эти переменные размещаются в секции .data. Имеется возможность указать адрес начала этой секции принудительно при помощи опций компилятора -Wl,-Tdata,<addr>, где addr – желаемый адрес начала секции (естественно, угловые скобки не нужны). Следует помнить лишь о том, что компоновщик автоматически отнимает38 от значения addr число 0x800000, т.е. если нужно начать секцию .data с адреса 0x1100, следует использовать addr=0x801100. .bss Секция глобальных и статических переменных, которые не инициализируются значениями, указанными пользователем, т.е. получают значение 0 по умолчанию. 38 Так реализовано, очевидно, для совместимости с GCC, хотя реально для AVR в этом нет никакого смысла. 58 Роман Абраш г. Новочеркасск E-mail: arv@radioliga.com .noinit Эта секция – часть секции .bss, и содержит вообще никак не инициализируемые переменные. Попытка указать атрибут размещения в этой секции для переменной, содержащей значение по умолчанию, вызовет ошибку компиляции. Имеется возможность указать адрес начала этой секции принудительно при помощи опций компилятора -Wl,-sectionstart=.noinit=<addr>, где addr – желаемый адрес начала секции (естественно, угловые скобки не нужны). По отношению к значению addr действуют те же условности, что и для секции .data. .init0 … .init9 Определено 10 секций начальной инициализации, являющихся частью секции .text. Номер секции определяет ее очередность, т.е. код в секции .init1 однозначно будет выполнен раньше кода из секции .init2, по после кода секции .init0. Важно представлять себе, что в этих секциях не используются вызовы функций, т.е. используется «плоский» код. Таким образом, функция, объявляемая в любой из этих секций, не должна использовать оператор возврата, т.к. не будет вызвана. Иначе говоря, все функции в этих секциях должны быть объявлены с атрибутом NACKED, и не должны вызываться из основной программы. .init0 – самая первая секция инициализации. Код, объявленный в этой секции, будет исполнен немедленно после аппаратного сброса. .init1, .init3, .init5, .init7 и .init8 – не используются по умолчанию, программист может определять в них свой код (помня о порядке их исполнения). .init2 – в этой секции происходит инициализация указателя стека и очистка регистра r0 (этот регистр используется, как вспомогательный в других инициализирующих секциях, нельзя менять его содержимое, т.к. это нарушит работу других секций). .init4 – для микроконтроллеров с объемом ПЗУ программ более 64К в этой секции находится код, инициализирующий содержимое секции .data, т.е. копирующий в ОЗУ данные из памяти программ. Для всех прочих микроконтроллеров в этой секции находится код, обнуляющий секцию .bss. .init6 – не используется в С-программах, но для С++ в этой секции реализуются конструкторы классов. .init9 – это по существу переход к началу функции main(). Примечание. Остается не раскрытым вопрос о том, в какой именно секции происходит инициализация секции .data для микроконтроллеров с объемом памяти менее 64К. Известно, что этот инициализирующий код является частью секции .text. Так же очень важно помнить, что стек инициализируется только в секции init2, поэтому код, размещаемый в «предыдущих» секциях не может использовать обращения к функциям. .fini9 … .fini0 Определено 10 завершающих секций, так же являющихся частью секции .text. Эти секции содержат код (ограничения те же, что и для .init0 … .init9), последовательно (в порядке убывания номера секции) исполняющийся при завершении программы. .fini9 – соответствует началу исполнения функции exit(). По умолчанию компилятором не используется. .fini6 – в С-программах не используется, а в С++ в этой секции размещаются деструкторы. .fini0 – содержит запрет прерываний и бесконечный цикл, означающий остановку программы. .fini8, .fini7, .fini5, .fini4, .fini3, .fini2 и .fini1 – по умолчанию компилятором не используются, программист может размещать в них свой код. .bootloader Секция кода загрузчика. Адрес начала и размер этой секции должен соответствовать заданным fuse-битами параметрам микроконтроллера (обычно задается в опциях проекта и автоматически настраивается компоновщиком). Радиолюбитель – 03/2011 СПРАВОЧНЫЙ МАТЕРИАЛ .eeprom Секция данных EEPROM. Обычно нет необходимости работать непосредственно с этой секцией, т.к. все необходимые действия по размещению данных в EEPROM успешно осуществляются более простыми способами (см. avr/eeprom.h – Поддержка EEPROM AVR), хотя реализация этих способов все равно заключается в указании соответствующего атрибута, т.е. указания секции размещения объекта, только это скрыто от программиста. Динамическое распределение памяти Компилятор AVR-GCC поддерживает микроконтроллеры, имеющие довольно мало ОЗУ (минимально допустимое поддерживаемое количество ОЗУ – 128 байт)39, в то время как основой самого языка Си является активная работа с ОЗУ, которое необходимо для организации стека, динамического выделения памяти, хранения локальных переменных и т.п. Кроме этого, в AVR нет никакой аппаратной поддержки какоголибо управления памятью, например, для контроля «пересечения» упомянутых областей ОЗУ (как это реализовано, например, для PC). Традиционно компилятор размещает секцию .data с самого начала доступной области ОЗУ, после чего следует секция .bss. Так называемая «куча» (heap) или область динамически распределяемой памяти будет следовать сразу за .bss. Стек начинается с вершины (т.е. последней доступной ячейки памяти) и движется в сторону уменьшения адресов. При таком распределении есть гарантия, что динамически выделяемые области никогда не пересекутся со статически распределенной памятью (если, конечно, не было ошибок программиста), но нет гарантии, что область стека не пересечется с динамически распределенными областями памяти данных. Это возможно, например, при рекурсивном обращении к функциям, если функции имеют большое количество локальных переменных или если область кучи сильно фрагментирована. Рисунок поясняет типичное распределение ОЗУ40. Для устройств на микроконтроллерах подобный подход непрост, т.к. нужно обеспечить минимальный размер кода, реализующего динамическое выделение памяти и контроль «границ» при обеспечении минимальной фрагментации ОЗУ, и в то же время высокое быстродействие, т.к. микроконтроллеры все еще очень медленные по сравнению с «большими» компьютерами. Программная реализация этих требований стремится к их решению, предлагая при этом некоторые средства и указывая направления для их оптимизации. Представляется очевидным, что при наличии внешней памяти следует разместить «кучу» в ней – это однозначно позволит избежать проблем со стеком, который всегда должен быть во встроенном ОЗУ. Такой перенос не должен зависеть от места размещения .data и .bss. Разумеется, обращение к внешнему ОЗУ более медленное, нежели ко внутреннему, поэтому окончательное решение о целесообразности такого переноса следует принимать осознанно. Кроме того, существуют ранее упомянутые (см. stdlib.h – Стандартные возможности) «настроечные» глобальные переменные, которые позволяют отрегулировать поведение функции malloc(). Инициализация этих переменных должна осуществляться до первого обращения к функции malloc(), причем следует помнить, что реализация некоторых других функций использует обращение к malloc() (см. stdio.h – Стандартный ввод-вывод), т.е. надо быть уверенным, что инициализация действительно происходит раньше первого обращения. Переменные __malloc_heap_start и __malloc_heap_end могут использоваться для ограничения области действия функции malloc(). По умолчанию эти переменные инициализируются компоновщиком значениями так, что __malloc_heap_start указывает сразу на первую ячейку после .bss, а __malloc_heap_end устанавливается в 0, символизируя, что malloc() может выделять память вплоть до указателя стека. Для перемещения динамически распределяемой области во внешнюю память переменная __malloc_heap_end должна быть соответственно откорректирована. Это может быть сделано либо во время исполнения программы путем записи в переменную, либо на этапе компоновки с использованием символа __heap_end. Вот, например, как можно переместить целиком .data, .bss и динамическую область во внешнее ОЗУ при помощи директивы компилятора (следует помнить об особенности адресации секций при компоновке, которая была рассмотрена в предыдущем разделе): -Wl,-Tdata=0x801100,-defsym=__heap_end=0x80ffff В результате распределение памяти будет таким, как показано на рисунке: То есть во встроенном ОЗУ останется только область стека, а все остальные области окажутся во внешнем ОЗУ. Динамически распределяемая область будет простираться вплоть до адреса 0xFFFF. Другой вариант – когда требуется разместить во внешнем ОЗУ только динамическую область, оставив во встроенном стек и секции .data и .bss. В этом случае можно использовать что-то подобное такой директиве компилятора: -Wl,-defsym=__heap_start=0x802000,-defsym=__heap_end=0x803fff Распределение памяти в этом случае будет следующим (обратите внимание, что тут для примера специально сделано несплошное распределение областей, т.е. имеются «дыры», недоступные для динамического распределения памяти): 39 В документации к avr-libc указана именно эта цифра – минимум 128 байт ОЗУ, однако это не мешает компилятору генерировать корректный код для микроконтроллеров с ОЗУ в 64 байта (например, для attiny13). Разумеется, программист должен прилагать определенные усилиия для «экономии» ОЗУ в этом случае. Более того, с некоторыми усилиями WinAVR способен генерировать код для микроконтроллеров вообще без ОЗУ, например, для attiny15! Но это уже из области «виртуозного» программирования. 40 Все рисунки этого раздела демонстрируют распределение адресов памяти, характерное для микроконтроллера Atmega128. Радиолюбитель – 03/2011 59 СПРАВОЧНЫЙ МАТЕРИАЛ Подобное распределение с «дырами» может быть необходимо, например, в случае, когда внешнее ОЗУ физически не присутствует в определенных областях адресного пространства – ситуация вполне обычная для микроконтроллерных систем. Примечание: разумеется, работа с внешним ОЗУ требует определенных действий по настройке аппаратной поддержки этой возможности в AVR, которые не рассматриваются в данном руководстве (изложены в соответствующих разделах документации к микроконтроллерам, поддерживающих работу с внешним ОЗУ). Если переменная __malloc_heap_end содержит ноль, то функция malloc() будет пытаться выделить память так, чтобы не пересечь значение указателя стека, причем будет дополнительно зарезервировано __malloc_margin байтов. Т.е. между последним выделенным байтом функцией malloc() и текущим указателем стека всегда гарантируется пространство не менее __malloc_margin байтов. В этом случае программист должен быть уверен, что различные вызовы функций и(или) прерываний не приведут к изменению указателя стека более, чем на эту величину, иначе не исключена возможность пересечения с динамически распределяемой областью памяти. По умолчанию __malloc_margin содержит значение 32. Для динамического выделения памяти используется подход, близкий к тому, который был реализован в MS DOS: связный список свободных областей. Для организации списка используется по 4 дополнительных байта, которые не являются доступными программе пользователя, но предшествуют адресу выделенной по запросу области. В этих байтах хранится признак занятости или свободности блока, ссылки на следующий и(или) предыдущий блок. Таким образом, при запросе 4 байтов фактически выделяется 8, но первые 4 используются менеджером динамической памяти, а на вторые 4 возвращается указатель. Если программа пользователя изменит содержимое первых 4-х байтов указанного промежутка, то связный список будет разрушен и работа менеджера динамической памяти станет непредсказуема. Другое следствие данного подхода: если осуществляется многократный запрос небольших участков памяти, это может быстро привести к нехватке свободного ОЗУ, за счет того, что к каждому запрошенному участку фактически прибавляется 4 дополнительных байта. При каждом обращении к malloc() менеджер динамической памяти осуществляет просмотр списка свободных участков памяти, стремясь найти подходящий (с размером равным или большим запрошенному). Если элемент найден и его размер равен запрошенному – просто возвращается указатель на него. Если же размер найденного элемента больше требуемого, происходит разбиение его на две части: первая есть запрошенный блок, а вторая добавляется в список свободных. Если подходящего по размеру элемента не найдено, происходит попытка увеличить размер кучи, т.е. в зависимости от значения __malloc_heap_end происходит либо проверка на пересечение со стеком, либо на границу «кучи», после чего размер последнего элемента в списке изменяется. Если изменение (увеличение) «кучи» невозможно, malloc() возвращает NULL. При вызове функции free() участок памяти просто добавляется в список свободных, при этом менеджер памяти всегда стремится объединить два подряд идущих свободных участка памяти в один, уменьшая тем самым количество элементов в списке и делая возможным выделение большего участка (т.е. происходит оптимизация «кучи», в определенном смысле «дефрагментация»). Подобная оптимизация происходит и при попытке изменить размер ранее выделенной области, т.е. при вызове функции realloc(). Очевидно, что все эти операции достаточно долгие, т.к. связаны с дополнительными затратами на просмотр корректировку списка элементов памяти. Данные в сегменте кода Любая программа на Си содержит большое количество констант, которые, в частности, используются для инициализации переменных. В силу особенностей работы компилятора получается так, что константы оказываются частью ассемблерных инструкций. Это кажется нормальным, если константа и соответствующая ей переменная имеет тип char или int, но для более «длинных» переменных это создает проблему: получается, что 60 одни и те же данные размещаются как в памяти программ, так и в ОЗУ (куда они «перекочевывают» на этапе инициализации программы). Особенно актуальна эта проблема для определений типа этого: const char str[] = ‘Пример 1’; const int arr[] = {1,2,3,4,5,6,7,8,9}; В этих случаях будет сгенерирован автоматически исполняющийся код, который будет заносить символы строки в соответствующие ячейки ОЗУ, а так же числа 1,2,3 и т.д. в другие ячейки ОЗУ. Очевидно, это приведет к совершенно нерациональному использованию ОЗУ, которого обычно достаточно немного в микроконтроллерах. Выход из этой ситуации – хранение констант подобного рода в памяти программ, объем которой, как правило, существенно больше ОЗУ. Однако есть сложность, накладываемая гарвардской архитектурой AVR – ОЗУ и память программ размещаются в разных непересекающихся адресных пространствах. Дело в том, что язык Си ориентирован на архитектуру фон-Неймана, т.е. разработан для случая, когда и данные и коды программы находятся в едином адресном пространстве. Поэтому требуются особые подходы, чтобы обеспечить хранение констант в памяти программ. Во многих компиляторах для этого используются отступления от стандарта в виде новых ключевых слов, опций компилятора и т.п. Комплект WinAVR достигает этих же целей иначе. В AVR-GCC имеется специальное ключевое слово __attribute__, которое позволяет определить различные дополнительные требования или условия при объявлении переменных, констант, функций и т.п. Это ключевое слово сопровождается двойными скобками, внутри которых указываются соответствующие атрибуты. В частности, для указания компоновщику и компилятору того, что описываемая переменная или константа должна размещаться в памяти программ, используется атрибут progmem. В файле pgmspace.h (см. avr/pgmspace.h – Поддержка обращения к сегменту кода AVR) определен макрос PROGMEM, который упрощает процедуру указания этого атрибута, а так же определен ряд макросов для обращения к описанным константам. Возможно, это покажется не самым удобным способом, однако, введение нестандартного поведения в GCC – слишком сложная процедура… Многие считают, что ключевого слова const достаточно для того, чтобы объявить константу в памяти программ, однако, это далеко не так. Это противоречит самому смыслу ключевого слова const, которое лишь сообщает компилятору, что данные не должны модифицироваться. Например, const часто используется в определении списка параметров функции, обозначая, что внутри функции содержимое этого параметра не должно модифицироваться. Теперь о том, как же правильно осуществить хранение и обращение к константам в памяти программ. Предположим, в программе имеется следующее объявление: unsigned char mydata[11][10] = { {0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09}, {0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,0x13}, {0x14,0x15,0x16,0x17,0x18,0x19,0x1A,0x1B,0x1C,0x1D}, {0x1E,0x1F,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27}, {0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0x30,0x31}, {0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B}, {0x3C,0x3D,0x3E,0x3F,0x40,0x41,0x42,0x43,0x44,0x45}, {0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F}, {0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59}, {0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,0x60,0x61,0x62,0x63}, {0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x6B,0x6C,0x6D} }; Это какая-то таблица, которая будет использоваться только в качестве источника каких-то масштабных коэффициентов, т.е. содержимое массива будет неизменно. Работа с массивом будет осуществляться, например, так: some = mydata[i][j]; Радиолюбитель – 03/2011 СПРАВОЧНЫЙ МАТЕРИАЛ Можно поместить этот массив в память программ: #include <avr/pgmspace.h> unsigned char mydata[11][10] PROGMEM = { {0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09}, pgm_read_byte() байт за байтом, а можно воспользоваться специальными функциями, ориентированными на работу с такими строками (см. avr/pgmspace.h – Поддержка обращения к сегменту кода AVR). Вот как, например, может быть реализован вывод вышеописанного массива строк: void foo(void){ {0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,0x13}, char buffer[10]; {0x14,0x15,0x16,0x17,0x18,0x19,0x1A,0x1B,0x1C,0x1D}, for (unsigned char i = 0; i < 5; i++) { {0x1E,0x1F,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27}, strcpy_P(buffer, (PGM_P)pgm_read_word(&(string_table[i]))); {0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0x30,0x31}, {0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B}, // тут выводим строку {0x3C,0x3D,0x3E,0x3F,0x40,0x41,0x42,0x43,0x44,0x45}, } {0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F}, {0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59}, {0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,0x60,0x61,0x62,0x63}, {0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x6B,0x6C,0x6D} }; После компиляции можно убедиться, что содержимое массива действительно размещено в памяти программ. Однако ранее описанная конструкция some = mydata[i][j] теперь не будет работать правильно! Компилятор по-прежнему ведет поиск значений в ОЗУ. Чтобы получить верное значение из теперешнего массива mydata, необходимо использовать макрос pgm_read_byte(), передав ему в качестве параметра адрес нужного элемента массива: some = pgm_read_byte(&(mydata[i][j])); Если данные имеют другую размерность – следует использовать соответствующие макросы. Теперь рассмотрим случай с хранением строк в памяти программ. char* string_table[] PROGMEM = { “String 1”, “String 2”, “String 3”, “String 4”, “String 5” }; Будет ли верным такое определение? И да, и нет – смотря что требовалось. Если ожидалось, что в память программ попадут строки «String 1», «String 2», и т.п. – это ошибочное определение. А вот если ожидалось, что массив с адресами начала строк должен быть размещен в памяти программ – то да, это действительно верно. Но, скорее всего, ожидалось первое. Дело в том, что атрибут в GCC действует лишь на определение переменной, но никак не на ее значение, т.е. к переменной string_table атрибут progmem будет применен, а собственно к строкам – нет. Чтобы действительно разместить строки в памяти программ, надо назначить соответствующий атрибут каждой строке отдельно: char char char char char string_1[] string_2[] string_3[] string_4[] string_5[] PROGMEM PROGMEM PROGMEM PROGMEM PROGMEM = = = = = “String “String “String “String “String return; } В данном примере происходит вызов функции strcpy_P(), которая копирует в ОЗУ строку из памяти программ, адрес которой считывается из массива string_table, находящегося так же в памяти программ. Примечание. Использование макросов для обращения к памяти программ естественно вызывает генерацию компилятором некоторого дополнительного кода, т.е. вызывает прирост объема кода программы и времени ее исполнения. Эти накладные расходы, как правило, невелики по сравнению с экономией ОЗУ, однако программист должен помнить об этом, ибо при необходимости может учесть это в программе для получения более оптимального кода, например, выделив все обращения к памяти программ в одну функцию. Всегда полезно смотреть на листинг программы. EEPROM Работа с EEPROM (см. avr/eeprom.h – Поддержка EEPROM AVR), входящим в состав практически всех микроконтроллеров AVR, осуществляется по принципам, сходным с обращением к памяти программ (см. Данные в сегменте кода): то есть просто объявляется проинициализированная при необходимости переменная с соответствующим атрибутом (точнее, используется макрос EEMEM для этого). Если переменная проинициализирована, то компилятор поместит соответствующие данные в отдельном файле, который затем можно использовать для программирования EEPROM микроконтроллера. #include <avr/eeprom.h> unsigned char mydata[11][10] EEMEM = { {0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09}, {0x0A,0x0B,0x0C,0x0D,0x0E,0x0F,0x10,0x11,0x12,0x13}, {0x14,0x15,0x16,0x17,0x18,0x19,0x1A,0x1B,0x1C,0x1D}, {0x1E,0x1F,0x20,0x21,0x22,0x23,0x24,0x25,0x26,0x27}, {0x28,0x29,0x2A,0x2B,0x2C,0x2D,0x2E,0x2F,0x30,0x31}, {0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x3A,0x3B}, {0x3C,0x3D,0x3E,0x3F,0x40,0x41,0x42,0x43,0x44,0x45}, {0x46,0x47,0x48,0x49,0x4A,0x4B,0x4C,0x4D,0x4E,0x4F}, 1”; 2”; 3”; 4”; 5”; {0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57,0x58,0x59}, {0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,0x60,0x61,0x62,0x63}, {0x64,0x65,0x66,0x67,0x68,0x69,0x6A,0x6B,0x6C,0x6D} }; а затем воспользоваться макросом для определения массива строк в памяти программ: PGM_P string_table[] PROGMEM = { string_1, string_2, string_3, string_4, string_5 }; В результате будет получено желаемое: в памяти программ будет определен массив указателей на строки, которые так же находятся в памяти программ. Использование такого массива, как и строк в программной памяти вообще, может происходить различным образом: можно обращаться к строкам при помощи макроса Радиолюбитель – 03/2011 char foo(char i; char j){ return eeprom_read_byte(&(mydata[i][j])); } Для обращения к соответствующим переменным так же используются макросы-функции. К сожалению, в состав AVR-LIBC не включены функции для работы со строками, размещенными в EEPROM, однако, программист может реализовать их самостоятельно. В отличие от констант в памяти программ, переменные в EEPROM – это действительно переменные, т.е. их значение может быть изменено в процессе работы программы. Для изменения значений таких переменных используются соответствующие макросы. Запись в EEPROM – относительно долгий процесс, о чем должен помнить программист. Продолжение в №4/2011 61 СПРАВОЧНЫЙ МАТЕРИАЛ Книга по работе с WinAVR и AVR Studio Продолжение. Начало в №1-12/2010; №1-3/2011 ASSEMBLER Есть несколько причин использовать ассемблер при разработке программ. Среди них можно выделить основные: · Применение моделей МК, не имеющих достаточного количества ОЗУ · Получение максимально быстродействующих участков кода · Реализация возможностей, которые нельзя или сложно выполнить стандартными средствами Си. За исключением первого варианта, все эти задачи могут быть решены при помощи ассемблерных вставок в Си-программе. Хотя GCC ориентирован на разработку программ на языках С и С++, все же имеется поддержка ассемблерных вставок и файлов, целиком написанных на ассемблере. Нет необходимости использовать отдельный компилятор ассемблера, достаточно, оформив соответственно ассемблерный исходный текст, вызвать Си-компилятор GCC, который в свою очередь в нужный момент вызовет препроцессор, ассемблер и компоновщик, сгенерировав необходимый набор выходных файлов. Это позволяет сделать процесс разработки на ассемблере неотличимым от привычного для Си-программиста. Как было сказано ранее, в проект могут входить файлы исходных текстов на Си и ассемблере. Для ассемблерных файлов принято расширение «s», и тут следует обратить внимание на одну важную особенность: если расширение файла имеет нижний регистр, то файл будет компилироваться непосредственно, а если расширение будет в верхнем регистре, т.е. «S», произойдет обязательный вызов препроцессора перед компиляцией. Различие регистра происходит даже для файловых систем, не чувствительных к регистру имен файлов (в том числе для Windows). При необходимости можно принудительно указать необходимость запуска препроцессора при помощи опции компилятора: -x assembler-with-cpp Исходные тексты полностью на ассемблере GCC использует расширенный синтаксис ассемблера по сравнению с тем, что встроен в AVR Studio. Это потому, что GAS (GNU Assembler) генерирует перемещаемый объектный код. Прежде всего, поддерживаются в полной мере все директивы Си-препроцессора, включая условную компиляцию, макросы-функции и т.п. Кроме того, поддерживаются псевдооператоры (директивы), унаследованные от GAS. Полная документация по этому диалекту ассемблера заслуживает отдельной книги. Здесь же перечислены только наиболее важные и востребованные возможности (особенности). 1. Комментарии в Си-стиле. Для комментариев в ассемблерных текстах можно использовать не только традиционную точку с запятой (однострочный комментарий), но и пару сочетаний символов «/*» и «*/» (многострочный комментарий). Так же как комментарий воспринимается любая строка, начинающаяся с символа «#», если после этого символа присутствует хотя бы один пробел (табуляция). 2. Псевдооператор .byte для выделения ячейки ОЗУ (1 байт). 3. Псевдооператор .ascii для определения строки символов (не завершающихся нулем автоматически). 4. Псевдооператор .asciiz для определения строки символов, оканчивающейся нулем (добавляется автоматически). 5. Директива .data для переключения на секцию памяти .data. 6. Директива .text для переключения на секцию памяти .text. 58 Роман Абраш г. Новочеркасск E-mail: arv@radioliga.com 7. Директива .section, при помощи которой задается выбор нужной секции памяти. 8. Директива .set для определения константы (эквивалент директивы .equ). 9. Директива .global для определения public-символа, т.е. символа, видимого в других модулях программы. 10. Локальные и глобальные метки. После директивы .global могут использоваться локальные метки, представляющие собой число с двоеточием. Директива .global делает предыдущие локальные метки невидимыми для текущей функции, т.е. вновь могут быть определены метки 0:, 1: и т.д. Для перехода к локальным меткам нужно использовать суффикс «b» или «f» для обозначения направления перехода – назад (backward, вверх по тексту) или вперед (forward, вниз по тексту) соответственно. Глобальная метка должна начинаться с нецифрового символа. 11. Директива .extern для определения внешнего символа, т.е. символа, определенного в другом модуле программы. Поддерживается и стандартная директива .org, однако ее применение бессмысленно, т.к. для перемещаемого объектного кода расположение объектов в памяти формирует компоновщик, выбирая необходимые адреса автоматически. Имеются и дополнительные операторы ассемблера: lo8(w) – возвращает младшие 8 бит 16-битного числа w hi8(w) – возвращает старшие 8 бит 16-битного числа w pm(addr) – позволяет получить адрес константы в памяти программ в формате, принятом для доступа (как известно, адресация памяти программ в AVR ведется по 16-битным словам, а доступ – по адресу байта). Пример небольшой программы на ассемблере для микроконтроллера AT90S1200, которая формирует 100 кГц меандр на выводе PD6 (используется кварц 10,7 МГц): #include <avr/io.h> ; примечание 1 work = 16 ; примечание 2 tmp = 17 inttmp = 19 intsav = 0 SQUARE = PD6 ; примечание 3 ; примечание 4: tmconst= 10700000 / 200000 fuzz= 8 ; 100 кГц => 200000 фронтов в сек. ; число тактов на вызов прерывания .section .text .global main ; примечание 5 main: rcall ioinit rjmp 1b 1: ; примечание 6 .global TIMER0_OVF_vect ; примечание 7 TIMER0_OVF_vect: 1: ldi inttmp, 256 tmconst + fuzz out _SFR_IO_ADDR(TCNT0), inttmp ; примечание 8 in intsav, _SFR_IO_ADDR(SREG) ; примечание 9 sbic _SFR_IO_ADDR(PORTD), SQUARE rjmp 1f sbi _SFR_IO_ADDR(PORTD), SQUARE rjmp 2f cbi _SFR_IO_ADDR(PORTD), SQUARE out _SFR_IO_ADDR(SREG), intsav 2: reti ioinit: Радиолюбитель – 04/2011 СПРАВОЧНЫЙ МАТЕРИАЛ sbi _SFR_IO_ADDR(DDRD), SQUARE ldi work, _BV(TOIE0) out _SFR_IO_ADDR(TIMSK), work ldi work, _BV(CS00) out _SFR_IO_ADDR(TCCR0), work ldi work, 256 tmconst out _SFR_IO_ADDR(TCNT0), work ; tmr0: CK/1 sei ret .global __vector_default ; примечание 10 __vector_default: reti .end Примечания: 1. Подключение того же самого заголовочного файла, что и для программы на Си. Следует помнить, что не любой заголовочный файл может быть подключен к ассемблерному тексту. 2. Определение локальной константы может быть выполнено так же при помощи характерной для Си директивы препроцессора #define work 16. 3. Использование константы, определенной в подключенном заголовочном файле. 4. При вычислениях констант ассемблер использует разрядность чисел, характерной для хост-платформы (т.е. для Windows это будет 32 разряда), в отличие от Си, который использует по умолчанию тип int. Чтобы получить меандр 100 кГц, необходимо переключать уровень PD6 200000 раз в секунду, что весьма критично ко времени исполнения. Необходимо учитывать затраты процессорного времени на генерацию прерывания и возврат из него, для чего и используется корректирующая константа fuzz (8 – т.е. 4 такта на прерывание, 2 такта на переход по вектору прерывания и 2 такта для переключения PD6). 5. Любая внешняя функция должна быть объявлена как .global. Использование стандартного имени main приводит к тому, что компоновщик будет использовать его в качестве точки входа в программу (как для Си main()). 6. Основной цикл программы – просто бесконечный пустой цикл. Вся работа ведется по прерываниям от таймера. Обратите внимание на то, как осуществлен переход к локальной метке (использование суффикса «b»). 7. Функция обработчика прерывания должна иметь имя из числа определенных констант для векторов прерывания (см. avr/ interrupt.h – Прерывания) – это позволит компоновщику разместить ее в нужном месте таблицы векторов. Необходимо помнить, что ни ассемблер, ни компоновщик не могут проверить корректность обработчика (например, наличие команды RETI для возврата) – это должен реализовать программист. 8. Как было показано ранее (см. avr/sfr_defs.h – Регистры специальных функций AVR), для получения адреса регистра ввода-вывода необходимо использовать макрос _SFR_IO_ADDR() (AT90S1200 не имеет ОЗУ, поэтому обращение к регистру ввода-вывода как к ячейке памяти невозможно, т.е. необходимо использовать команды IN/OUT). 9. При разработке обработчиков прерывания следует помнить, что контекст основной программы не должен изменяться во время прерывания. В данном примере не предпринято мер по сохранению контекста, т.к. основной цикл ничего не делает, но в реальных ситуациях следует позаботиться о том, чтобы не изменить содержимое регистров, используемых компилятором в основной программе, а так же регистра состояния SREG. Для сохранения контекста можно применять стек или ячейки ОЗУ. 10. Как было показано ранее (см. avr/interrupt.h – Прерывания), для всех неиспользуемых векторов прерывания Радиолюбитель – 04/2011 может использоваться адрес «обработчика по умолчанию» __vector_default. Этот символ должен быть объявлен .global, тогда компоновщик заполнит всю таблицу незадействованных векторов его адресом (в противном случае таблица будет заполнена переходами к адресу 0x0000). Разумеется, этот обработчик должен обеспечивать нормальный возврат из прерывания – команду RETI. Ассемблерные вставки в Си-программе Ассемблерные вставки используются в программах Си, если для решения поставленной задачи достаточно буквально нескольких команд ассемблера в критически важном участке программы и переписывание всей программы нецелесообразно. Особенность ассемблерных вставок в том, что для них применяется особый синтаксис ассемблера, позволяющий компилятору самостоятельно подбирать, например, регистры для операндов команд. Таким образом, компилятор «знает», какие ресурсы задействованы в ассемблерной вставке и использует это знание для наиболее оптимального построения предыдущего и последующего (относительно вставки) Си-кода. Особенностью ассемблерных вставок является то, что внутри них реализуется автоматически доступ к переменным, определенным в тексте Си-программы. Оператор asm() Для ассемблерной вставки используется ключевое слово (оператор) asm, синтаксис которого проще всего рассмотреть на примере (считывание порта D): asm(«in %0, %1» : «=r» (value) : «I» (_SFR_IO_ADDR(PORTD)) ); Внутри скобок оператора asm находятся строка, состоящая из трех частей, разделенных двоеточиями: «in %0, %1» – это первая часть, инструкция ассемблера. Она обязательно помещается в двойные кавычки. Состоит из обычной мнемоники ассемблерной команды, операнды которой имеют особый вид: номер операнда, предваряемый символом процента (операнды нумеруются с нуля). Вместо обозначения операндов по номеру с процентом, можно использовать поименованные операнды. В этом случае вместо %0 нужно использовать имя операнда в квадратных скобках, а последующие части «тела» оператора asm так же предварять этим именем (в этом случае они могут следовать в другом порядке, т.к. назначение операнда определяется уже не порядковым номером, а именем): asm volatile («in [x1], [x2]» : [x1] «=r» (value) : [x2] «I» (_SFR_IO_ADDR(PORTD)) ); «=r» (value) – вторая часть, соответствующая операнду %0 – список выходных параметров инструкции ассемблера. Эта часть состоит из размещенного в кавычках условного обозначения параметра результата и (через пробел) в скобках наименование переменной для его сохранения. «I» (_SFR_IO_ADDR(PORTD)) – третья часть, соответствующая операнду %1 – список входных параметров инструкции. Как и предыдущая часть, эта состоит так же из указанного в кавычках условного обозначения параметра и в скобках – его значения. Такая запись может показаться несколько избыточной и странной, однако пока продолжим знакомиться с особенностями синтаксиса ассемблерных вставок, вскоре вопрос о смысле этой «избыточности» будет снят. В операторе asm может быть еще четвертая часть (в примере она отсутствует) – список регистров, от содержимого которых оператор зависит или которые зависят от него. Дело в том, что фактически оператор asm может быть оттранслирован компилятором в несколько иные инструкции ассемблера, причем их количество может быть отлично от указанных программистом. В частности, после компиляции вышеприведенного примера результирующий код будет следующим (выдержка из S-файла, генерируемого GCC): lds r24,value /* #APP */ in r24, 12 /* #NOAPP */ sts value,r24 59 СПРАВОЧНЫЙ МАТЕРИАЛ Комментарии вставлены компилятором, чтобы проинформировать ассемблер о том, какая инструкция введена программистом. Т.е. фактически одна команда ассемблера оттранслировалась в три. Регистр r24 был выбран компилятором (компилятор мог выбрать и любой иной регистр по своему усмотрению). Команда загрузки r24 и сохранения его результата в переменой value – так же добавлены компилятором. Очевидно, что первая команда не имеет смысла, если включена оптимизация, она будет удалена оптимизатором. Более того, если выяснится, что значение value более нигде в функции не используется – оптимизатором будет удалены все эти команды! Чтобы исключить воздействие оптимизатора на ассемблерную вставку, необходимо использовать ключевое слово volatile: asm volatile («in %0, %1» : «=r» (value) : «I» (_SFR_IO_ADDR(PORTD)) ); Обратите внимание на то, что компилятор автоматически добавляет «пролог» и «эпилог» к ассемблерному тексту вставки: пролог служит для загрузки в регистры значения из входных переменных, а эпилог реализует сохранение результата в переменных. Пролог добавляется всегда перед написанными программистом инструкциями, эпилог – всегда после. С этой особенностью связан ряд тонкостей, которые рассмотрены далее. Все части оператора могут быть на разных строках: asm ( «in %0, %1» : «=r» (value) : «I» (_SFR_IO_ADDR(PORTD)) ); В некоторых случаях вторая и последующие части оператора asm не требуются. Не смотря на то, что можно просто их опустить вместе с двоеточиями, более хорошей практикой будет сохранение «пустых» мест: Кроме того, в одном операторе asm может быть перечислено сразу несколько команд ассемблера: asm volatile ( “nop\n\t” “nop\n\t” “nop\n\t” : : ); В этом случае каждая команда ассемблера может начинаться с новой строки, как и принято в ассемблере. Желательно принудительно указывать наличие символов перевода строки и табуляции “\n\t”, иначе генерируемый компилятором ассемблерный код будет нечитаемым. Вторая и последующие части оператора asm относятся ко всему оператору, т.е. в случае многострочного оператора они должны быть указаны только один раз в самом конце. Особое внимание следует обратить на то, что для сохранения команд NOP в коде программы при включенной оптимизации всегда необходимо использовать volatile. В операторе asm можно использовать все инструкции, как и в обычном ассемблере, в том числе метки. Эти метки можно использовать для инструкций переходов: Особенности использования меток рассмотрены далее. Кроме прочего, можно использовать символьные имена для особых регистров: __SREG__ – регистр состояния SREG __SP_L__ – младший байт указателя стека __SP_H__ – старший байт указателя стека __tmp_reg__ – вспомогательный регистр r0 __zero_reg__ – регистр r1 – всегда обнулен Регистр __tmp_reg__ может использоваться без необходимости сохранения его содержимого, а __zero_reg__ должен сохранять свое значение всегда (т.е. не может использоваться в качестве операнда-приемника результата). asm(“nop” : : ); Продолжение в №5/2011 60 Радиолюбитель – 04/2011 СПРАВОЧНЫЙ МАТЕРИАЛ Книга по работе с WinAVR и AVR Studio Продолжение. Начало в №1-12/2010; №1-4/2011 Операнды оператора asm() Параметры оператора asm, используемые в качестве операндов инструкций ассемблера, обозначаются, как было сказано ранее, определенными символами, помещаемыми в двойные кавычки (см. таблицу 4). В таблице 4 перечислены все допустимые символы для обозначения параметров. Обнаружив такой символ, компилятор самостоятельно подставит вместо него соответствующее значение из числа допустимых. При этом могут быть странные на первый взгляд ошибки, если символ выбран программистом неверно. Например, программист применил для операнда-приемника инструкции ORI символ “r”. Компилятор может выбрать для этого любой регистр из числа имеющихся в его распоряжении в текущий момент. При этом может быть выбран и регистр, например, r3, что приведет к ошибке, т.к. для инструкции ORI допустимо применять только регистры из старшей половины. Компилятор может выбрать и «правильный» регистр, и это может привести к сомнениям: почему ранее работавшая без ошибок ассемблерная вставка вдруг стала вызывать ошибку. Поэтому правильным выбором для ORI будет символ “d”. В таблице 5 приведены все инструкции ассемблера, требующие операндов, с указанием подходящих им символов. Однако, эти требования недостаточно строгие, т.к. не ограничивают, например, диапазон номеров битов от 0 до 7 и т.п. Любой символ может предваряться символом-модификатором: = – обозначает, что операнд только для записи. Используется для операндов результата. + – обозначает, что операнд для записи и чтения. & – обозначает, что для вывода должен использоваться новый регистр. Используемые для результата операнды всегда должны быть только для записи и иметь вид «леводопустимого выражения». Компилятор не проверяет совместимость типов операнда и присваемого ему значения. Таблица 4 Символ Допустимые значения Что обозначает Роман Абраш г. Новочеркасск E-mail: arv@radioliga.com Входные операнды должны быть определены как доступные только для чтения (без символа-модификатора). Однако, в случае, когда единственный операнд используется и как входное и как выходное значение, существует способ обойти это ограничение: необходимо вместо символа операнда использовать номер соответствующего операнда в инструкции: asm volatile(«swap %0» : «=r» (value) : «0» (value)); Пример показывает, как переменная value используется в качестве входного значения и в нее же помещается результат ассемблерной команды, меняющей тетрады байта. “0” – это номер операнда команды SWAP. Такая запись указывает компилятору использовать для операнда-приемника результата то же самое значение, что было выбрано для операнда-источника. Однако следует знать, что компилятор может выбрать одно и то же значение и для источника и для приемника, даже если это явно не указано. Обычно это не опасно, но может быть фатальным, если значение операнда-результата модифицируется другими ассемблерными инструкциями до выполнения сохранения результата оператора asm. asm volatile(«in %0,%1» «\n\t» «out %1, %2» «\n\t» : «=&r» (input) : «I» (_SFR_IO_ADDR(port)), «r» (output) ); Таблица 5 Инструкция Символы операндов Инструкция Символы операндов adc r, r add adiw w, I and r, r r, r andi d, M asr r bclr I bld r, I brbc I, label brbs I, label r, I bset I bst a Старшие регистры без указателей r16 … r23 cbi I, I cbr d, I b Базовые регистровые пары – указатели y, z com r cp r, r d Старшие регистры r16 … r31 cpc r, r cpi d, M e Регистровые пары – указатели x, y, z cpse r, r dec r r, r q Указатель стека SPH:SPL elpm t, z eor r Любой регистр r0 … r31 in r, I inc r t Вспомогательный регистр r0 ld r, e ldd r, b w Специальный старший регистр r24, r26, r28, r30 ldi d, M lds r, label x Регистровая пара X x (r27:r26) lpm t, z lsl r y Регистровая пара Y y (r29:r28) lsr r mov r, r z Регистровая пара Z z (r31:r30) movw r, r mul r, r G Kонстанта в формате с плавающей точкой 0.0 neg r or r, r I 6-битовое положительное число (константа) 0 … 63 ori d, M out I, r J 6- битовое отрицательное число (константа) -63 … 0 pop r push r K Целочисленная константа 2 rol r ror r L Целочисленная константа 0 sbc r, r sbci d, M l Младшие регистры r0 … r15 sbi I, I sbic I, I M Однобайтная константа 0 … 255 sbiw w, I sbr d, M N Целочисленная константа -1 sbrc r, I sbrs r, I O Целочисленная константа 8, 16, 24 ser d st e, r P Целочисленная константа 1 std b, r sts label, r sub r, r subi d, M swap r Q Адрес памяти по указателю Y или Z со смещением R Целочисленная константа Радиолюбитель – 05/2011 -6 to 5 57 СПРАВОЧНЫЙ МАТЕРИАЛ В этом примере сначала осуществляется считывание порта port, а затем другое значение в этот же самый порт выводится. Компилятор вполне может выбрать для ввода и вывода один и тот же регистр, например, r5, при этом значение r5, считанное первой командой еще до того, как будет записано эпилогом в переменную output, будет изменено значением, загруженным из переменной input для вывода во второй команде. Чтобы этого избежать, применен модификатор &, который указывает компилятору выбрать для операнда источника обязательно другой регистр, т.е. не совпадающий ни с одним из выбранных для других операндов “r”. Нередка ситуация, когда ассемблерная вставка должна манипулировать числами более одного байта. В этом случае возникает проблема с обращением к нескольким байтам, составляющим один операнд. Для обозначения регистров, в которых хранится значение переменной, используются дополнительные символы – заглавные латинские символы «A», «B», «C» и т.д. Такая запись указывает компилятору выбрать очередной регистр для хранения очередного байта, составляющего переменную. Младший байт соответствует символу «А», более старшие последовательно назначаются для «В», «С» и т.д. Программист может использовать соответственно «%A0» для обращения к младшему байту первого операнда, «%A1» – младшему байту второго операнда и т.д. В этом примере производится перестановка байтов в 16-битной переменной value: asm volatile(«mov __tmp_reg__, %A0» «\n\t» «mov %A0, %B0» «\n\t» «mov %B0, __tmp_reg__» «\n\t» : «=r» (value) : «0» (value) ); То есть вопреки всему ранее сказанному надо использовать нижний регистр для указания младшего байта операнда! Это проблема реализации компилятора GCC текущей версии40. Зависимости оператора asm() Как было сказано, последним элементом оператора asm может быть список зависимостей. Этот список может отсутствовать вместе с отделяющим его двоеточием. Однако если внутри ассемблерной вставки программист использует значения регистров, не указанных в списках операндов, он обязан уведомить компилятор об этом. В этом случае в код пролога будет добавлены команды сохранения перечисленных в этом списке регистров, а в коде эпилога эти значения будут восстановлены в прежнем виде. Например, при помощи следующего кода осуществляется атомарное увеличение 8-битной переменной: asm volatile( «cli» «ld r24, %a0» «inc r24» «st %a0, r24» «sei» : : «e» (ptr) : «r24» ); «\n\t» «\n\t» «\n\t» «\n\t» «\n\t» Примечательно в этом примере то, что используется указатель для обращения к переменной, иначе сохранение значения произошло бы только в коде эпилога, который будет добавлен после команды разрешения прерывания, т.е. это уже нарушило бы условие атомарности ассемблерной вставки. А с использованием указателя переменная обновляется до разрешения прерываний. Более правильным было бы в этом случае использовать __tmp_reg__ вместо r24: Перестановка байтов 32-битного переменной value: asm volatile(«mov __tmp_reg__, %A0» «mov %A0, %D0» «mov %D0, __tmp_reg__» «mov __tmp_reg__, %B0» «mov %B0, %C0» «mov %C0, __tmp_reg__» : «=r» (value) : «0» (value) ); «\n\t» «\n\t» «\n\t» «\n\t» «\n\t» «\n\t» Для обозначения операнда, используемого и как источник, и как приемник результата, разумно использовать модификатор «+»: asm volatile(«mov __tmp_reg__, %A0» «mov %A0, %D0» «mov %D0, __tmp_reg__» «mov __tmp_reg__, %B0» «mov %B0, %C0» «mov %C0, __tmp_reg__» : «+r» (value) ); «\n\t» «\n\t» «\n\t» «\n\t» «\n\t» «\n\t» asm volatile( «cli» «\n\t» «ld __tmp_reg__, %a0» «inc __tmp_reg__» «st %a0, __tmp_reg__» «sei» «\n\t» : : «e» (ptr) ); «\n\t» «\n\t» «\n\t» Вышеприведенные примеры имеют одну неприятную особенность: их нельзя использовать в участках программы, где прерывания уже запрещены, т.к. эти вставки принудительно разрешают прерывания. Казалось бы, эту проблему легко решить при помощи локальной переменной: { uint8_t s; asm volatile( «in %0, __SREG__» «cli» «ld __tmp_reg__, %a1» «inc __tmp_reg__» «st %a1, __tmp_reg__» «out __SREG__, %0» : «=&r» (s) : «e» (ptr) ); Непредвиденная проблема может возникнуть еще и в случае, если осуществляется работа с указателем, т.е. регистровой парой. Предположим, программист использует следующее определения операнда: «\n\t» «\n\t» «\n\t» «\n\t» «\n\t» «\n\t» } «e» (ptr) Допустим, компилятор избрал для хранения этого операнда регистровую пару Z, т.е. %A0 соответствует ZL (r30), а %B0 – ZH (r31). Однако компилятор вызовет ошибку, если программист использует обращение к этой паре так: ld r24, Z Чтобы компилятор сгенерировал правильный код, необходимо использовать только такую конструкцию: ld К сожалению, это не так, хотя и выглядит правильно. Ассемблерный код модифицирует переменную, на которую указывает указатель ptr. Но загрузка значения указателя в регистровую пару осуществляется в коде пролога, который выполняется еще до запрещения прерываний, т.е. вполне значение указателя может быть изменено на этом этапе. Разумеется, в этом случае результат работы ассемблерной вставки непредсказуем. Более того, при оптимизации значение указателя вообще может оказаться не в ОЗУ, а в регистрах. Самое меньшее, что можно в этом случае сделать, это применить специальный элемент списка зависимостей вставки memory: r24, %a0 41 58 На момент написания книги это соответствует версии GCC 4.3.хх Радиолюбитель – 05/2011 СПРАВОЧНЫЙ МАТЕРИАЛ «dec %1» «\n\t» «brne L_dl1%=» «\n\t» : «=&w» (cnt) : «r» (ms), «r» (delay_count) ); { uint8_t s; asm volatile( «in %0, __SREG__» «cli» «ld __tmp_reg__, %a1» «inc __tmp_reg__» «st %a1, __tmp_reg__» «out __SREG__, %0» : «=&r» (s) : «e» (ptr) : «memory» ); «\n\t» «\n\t» «\n\t» «\n\t» «\n\t» «\n\t» } Это укажет компилятору учесть при оптимизации тот факт, что ассемблерная вставка использует память, которую нельзя модифицировать. Но наиболее простой и удобный способ все-таки состоит в том, чтобы использовать volatile при определении указателя ptr: volatile uint_t *ptr; Это обеспечит правильное поведение как самой ассемблерной вставки, так и оптимизатора в ее отношении. Примечание. Случаи действительной необходимости в указании списка зависимостей достаточно редки. Почти всегда есть возможность избежать их, обеспечив компилятору больше свободы в оптимизации кода. Макросы на ассемблере Для многократного использования ассемблерных вставок в различных проектах имеет смысл разместить их в заголовочном файле в виде макросов. AVR-LIBC содержит немалое количество таких файлов, найти которые можно в директории avr/include. Однако такое использование ассемблерных вставок может вызвать предупреждения компилятора для режима соответствия ANSII-стандарту. Чтобы избежать предупреждений, достаточно писать __asm__ вместо asm и __volatile__ вместо volatile – это полные синонимы. Более заметная проблема ассемблерных вставок в виде макросов связана с использованием меток. Макрос – это ведь просто подстановка текста, поэтому в случае использования обычных меток в ассемблерных вставках возможно появление одинаковых меток в различных участках программы, что недопустимо. Для избежания этого следует использовать особый синтаксис меток для ассемблерных вставок: в определении метки используется сочетание «%=», которое на этапе компиляции заменяется на некий уникальный номер, таким образом, гарантируя уникальность всех меток. Вот, например, как реализован один из макросов в iomacros.h: #define loop_until_bit_is_clear(port,bit) \ __asm__ __volatile__ ( \ «L_%=: « «sbic %0, %1» «\n\t» \ «rjmp L_%=» \ : /* no outputs */ \ : «I» (_SFR_IO_ADDR(port)), «I» (bit) ) Метка L_%= будет заменена на что-то типа L_1234, причем это будет гарантированно уникальная метка. Функции на ассемблере Макрос с ассемблерной вставкой будет приводить к появлению одного и того же кода каждый раз при использовании макроса. Для многих критичных к размеру памяти приложений это неприемлемо. В этом случае логичнее реализовать С-функцию целиком на ассемблере. void delay(uint8_t ms) { uint16_t cnt; asm volatile ( «\n» «L_dl1%=:» «mov %A0, %A2» «mov %B0, %B2» «L_dl2%=:» «sbiw %A0, 1»«\n\t» «brne L_dl2%=» Радиолюбитель – 05/2011 } Пример демонстрирует реализацию функции задержки программным циклом на заданное количество миллисекунд. В функции используется глобальная переменная delay_count, которая должна содержать значение тактовой частоты, деленное на 4000, причем это значение должно быть присвоено этой переменной до обращений к функции. Как было показано ранее, функция использует локальную переменную для сохранения значения используемых регистров. Следующий пример показывает, как реализуется возврат значения из ассемблерной функции: uint16_t inw(uint8_t port) { uint16_t result; asm volatile ( «in %A0,%1» «\n\t» «in %B0,(%1) + 1» : «=r» (result) : «I» (_SFR_IO_ADDR(port)) ); return result; } Функция inw возвращает 16-разрядное число, считанное из пары «смежных» портов ввода-вывода. port определяет «младший» порт. Соглашения об именах Си и ассемблера По умолчанию GCC использует одинаковые имена переменных и функций для Си и для ассемблера. Однако программист может изменить эту ситуацию при помощи особой формы оператора asm: unsigned long value asm(«clock») = 3686400; Это определение вынудит компилятор использовать имя clock вместо его значения. Такая «подмена» имеет смысл только для глобальных (статических) или внешних переменных, так как локальные переменные не имеют символьных имен для ассемблера. Кроме того, локальные переменные могут быть помещены в регистры. Программист может назначить локальной переменной определенный регистр: void Count(void) { register unsigned char counter asm(«r3»); ... какието действия ... asm volatile(«clr r3»); ... какието действия ... } В этом примере показано, как заставить компилятор поместить локальную переменную counter в регистр r3. Однако, это не «вечное» закрепление: если оптимизатор компилятора сочтет, что значение переменной более не требуется сохранять, назначение регистра r3 может быть переопределено. Если программист закрепляет слишком много регистров за переменными, компилятор может исчерпать ресурсы регистров для компиляции остального кода. Кроме этого, принудительное назначение регистра переменной чаще приводит к ухудшению эффективности кода вместо ожидаемой его оптимизации. Существует и способ изменить имя функции: extern long Calc(void) asm («CALCULATE»); «\n\t» «\n\t» «\n» «\n\t» «\n\t» Вышеприведенный пример заставит компилятор при обращении к функции Calc() генерировать инструкцию вызова функции CALCULATE. Окончание следует. 59