Глава 13
Персональный компьютер вместо паяльника
Слово «программирование» в отношении МК имеет двоякий смысл. Во-первых, это собственно процесс написания программы, как и для любых других устройств, содержащих процессор, от холодильников до персональных компьютеров или узлов управления космическими аппаратами. Во-вторых, этим словом также называют процесс загрузки программы в память программ. Чтобы не путаться, мы будем подразумевать под «программированием» МК именно запись программ в память (ее еще называют «прошивкой»), а подготовительный этап будем называть «созданием программы».
Заметки на полях
Отмечу, что каждый, кто хочет всерьез научиться программировать (неважно для чего, хоть для Веба, хоть для микроконтроллеров), должен начинать с изучения фундаментального труда Д. Кнута [10]. К сожалению, Кнут не практикующий программист, а профессор Стэндфордского университета, и как все профессоры, начинает с анализа, доказательств теорем и тому подобной «чепухи», зачем-то очень нужной математикам. Читатель, конечно, понимает, что в этих высказываниях есть значительная доля шутки, но правда заключается в том, что математик потребует доказательства там, где инженеру достаточно заверения авторитета, что такая-то формула справедлива, а доказательства и обоснования со ссылками на древнегреческих авторов его только отвлекают от главного. И все же я очень советую изучить по крайней мере те главы первого тома, которые посвящены машинным языкам, и алгоритмы арифметических действий из второго тома (всего тома три, но я ссылаюсь только на два, так как третий посвящен исключительно алгоритмам сортировки, которые лежат за пределами любительской программистской практики).
Конечно, продираться через многочисленные математические изыски Кнута необязательна, если вы согласны ограничиться готовыми программными модулями, которые с течением времени научитесь править и подстраивать под свои нужды. Материал в этой книге не рассчитан на «крутых» профессионалов, и для его освоения и повторения приведенных здесь конструкций не надо обладать особыми знаниями. Но после ознакомления с работой Д. Кнута во многих вещах вам будет значительно проще разобраться.
Как программируются микроконтроллеры
В МК AVR встроенная память программ энергонезависимая, и называется «flash-памятью программ», так ее можно отличить от также энергонезависимой памяти для хранения пользовательских данных, которая именуется EEPROM. Если строго следовать терминологии, то flash — это, как вы знаете, разновидность EEPROM, но во всяком случае, встроенная память программ для AVR-контроллеров делается именно по flash-технологии, что позволяет значительно ускорить процесс программирования. Первоначально такое разделение на EEPROM и flash для, по сути дела, одинаковых по функционированию разделов памяти (и то и другое призвано хранить данные при отсутствии питания) приводило еще и к тому, что память программ допускала лишь около 1000 циклов перезаписи, в то время как EEPROM намного больше (порядка 100 000). Так было в семействе Classic и первоначальных выпусках семейств Tuny и Mega. И хотя на практике и того, и другого более чем достаточно, современные МК AVR допускают уже 10 000 циклов перезаписи для памяти программ.
Подробности
Дело в том, что модели из семейства Меда могут самопрограммироваться в процессе эксплуатации (загружать программу из внешних источников), и вот тогда тысячи циклов в теории может оказаться недостаточно. Если вас заинтересовала эта возможность, то имейте в виду, что для самопрограммирования все равно требуется первоначально занести в память программу-загрузчик. Употребляется этот способ тогда, когда контроллер должен выполнять существенно различные функции в зависимости от каких-то условий. В некотором роде это напоминает загрузку различных программ в обычных компьютерах (например, в карманных или в смартфонах). Здесь мы, конечно, разбирать эти подробности не будем.
С другой стороны, со временем разница между памятью программ и пользовательской EEPROM, совершенно очевидно, стирается: так, в семействе Tuny flash-память программируется побайтно, а не страницами, а в старших моделях семейства Mega, наоборот, EEPROM программируется страницами (правда, небольшого размера, всего в 4 байта). Для конечного пользователя все эти нюансы не имеют ровным счетом никакого значения.
Встроенная память программ имеет объем от 1 кбайта у ATtunyll до 128 кбайт у ATmega128 (как вы догадались, число в наименовании модели как раз и означает объем памяти программ). Если у пользователей системы Windows такие объемы вызовут лишь снисходительную улыбку, то советую им не «задаваться», поскольку Андрес Хейлсберг, автор Turbo Pascal, умудрился в первой версии этого продукта (1982) уложить в 33 280 байт интегрированную среду разработки, встроенный редактор и библиотеку времени выполнения. Так что возможностей AVR достаточно, чтобы построить неслабый персональный компьютер, который мог бы существенно превышать по возможностям модели IBM PC АТ на процессоре i286 (напомним, что последний выполнял инструкцию в среднем за 2–4 такта, a AVR — за 1–2). Правда, 1286 мог адресовать до 16 Мбайт динамического ОЗУ (у процессоров для ПК, напомним, нет встроенной памяти, кроме операционных регистров). Но для применений, которые требуют объемного программного кода, и AVR (кроме Tuny) могут выполнять программы из внешней памяти объемом до 4 Мбайт в отдельных моделях, причем скорость выборки команд зависит только от параметров ОЗУ и тактовой частоты AVR. Мы в данной книге эту возможность не рассматриваем — автору еще ни разу не удалось превысить лимит встроенной памяти выбранного МК.
Программаторы
Для того чтобы загрузить программу во flash-память, служат специальные программаторы, которые делятся на последовательные и параллельные (существуют еще стандартизированный согласно IEEE Std 1149.1-1990 интерфейс JTAG для отладки и тестирования микроконтроллерных устройств. С его помощью, например, выполняется всем известная «перешивка» готовых устройств. Но мы не будем здесь на этом останавливаться, т. к. в любительских конструкциях, да и во многих профессиональных, он никогда не употребляется).
Последовательные программаторы еще называют ISP (In-System Programmer, что переводится как «внутрисистемный программатор»), потому что они обычно не предполагают специального устройства для подсоединения и питания программируемой микросхемы, а заканчиваются обычным плоским кабелем с двухрядным штырьковым разъемом типа IDC или PLD (таким же, какие служат для подсоединения периферии к материнским платам ПК), который подсоединяется к специально предусмотренной штыревой части, располагаемой прямо на плате вашего устройства (подробнее см. главу 14). Питание на программатор при этом поступает от самой схемы.
Такой способ программирования очень удобен для отладки, т. к. МК находится в «родном» окружении и сразу после программирования автоматически начинает работать. Причем внешние элементы (с некоторыми оговорками) процессу программирования обычно не мешают. Далее я расскажу, как с помощью этого способа программирования превратить любую схему в отладочный модуль, что позволяет избежать необходимости приобретения дорогих универсальных модулей и обойтись без изучения фирменной среды программирования AVR Studio. Недостатки способа — дополнительная площадь, которая будет занята «лишними» элементами на плате, а также некоторое снижение помехоустойчивости (о том, какие дополнительные меры следует принять, я обязательно расскажу, когда мы будем рассматривать конкретные схемы). Зато вы всегда без лишних сложностей сможете поправить ошибку в программе и дополнить ее функциональность хоть через месяц, хоть через год» что особенно важно для любительских конструкций, которые, как правило, выпускаются в одном-двух экземплярах.
Подробности
Последовательное программирование AVR есть, по сути, использование стандартного последовательного интерфейса SPI в специфических целях, причем следует учитывать, что в некоторых моделях (ATmega64 и АТтеда128) выводы собственно SPI с выводами программирования не совпадают. Процесс последовательного программирования начинается с того, что на вывод SCK подается напряжение уровня «земли», после этого на Reset поступает короткий положительный импульс, и затем последний также оставляют в состоянии низкого уровня не менее, чем на 20 мс (можно и просто предварительно подключить эти выводы к низкому уровню, а затем включить питание контроллера). При этом микросхема переходит в режим ожидания команд программирования. Отсюда понятно, почему снижается помехоустойчивость: такое сочетание уровней вполне может возникнуть в процессе эксплуатации из-за наводок на выводы разъема программирования. У автора этих строк в контроллере, расположенном в цехе со множеством работающих механизмов, при срабатывании мощного пускателя в нескольких метрах от устройства стиралась память программ! Правда, исправить ситуацию оказалось довольно просто установкой дополнительных «подтягивающих» резисторов.
Последовательное программирование поддерживают не все контроллеры AVR, из семейства Tuny так можно программировать только ATtuny12 и ATtuny15. В отличие от последовательного, параллельный способ программирования применяется для кристалла вне схемы. Программаторы такого рода, как правило, в той или иной степени универсальны и подходят для множества различных программируемых чипов, начиная от микросхем ПЗУ (в том числе и компьютерной BIOS) и заканчивая большинством МК.
Например, показанный на рис. 13.1 популярный AutoProg поддерживает около 4000 типов микросхем. Такие программаторы, естественно, относительно дороги (не самый дорогой в своем классе AutoProg стоит почти 9000 руб.), что и понятно — представьте, сколько труда вложено в программное обеспечение с поддержкой такого количества чипов. Экономически целесообразны они для тех, кто много работает с различными семействами микросхем, а также при изготовлении серийной продукции с отлаженной программой, в которой разъемы ISP-систем будут только мешать. Для отладки универсальные программаторы неудобны, т. к. приходится часто вытаскивать и вставлять микросхему в панельку с риском ее повредить, а для некоторых типов корпусов это может быть вообще исключено, если нет соответствующего переходника. Если вы работаете только с одним типом микросхем, но хотите тиражировать отлаженное изделие без лишних разъемов на плате, то вам ничего не стоит изготовить отдельный программатор на основе ISP-системы. Правда, «оживить» чипы AVR, в которых по ошибке были неправильно запрограммированы fuse-биты, отвечающие за источник тактового сигнала (об этом см. в конце главы), можно только с помощью параллельного программатора.
Рис. 13.1. Параллельный программатор AutoProg
Надо сказать, что и тот и другой способ программирования не составляет никакого секрета и подробно излагается в описании любого AVR. Так что в принципе программатор может быть изготовлен самостоятельно как в виде аппаратного устройства, обычно подключаемого через COM-порт, так и в чисто программном виде, с подключением кристалла через LPT-порт. Причем Atmel даже приводит в своих рекомендациях (Application Notes, которые наши электронщики обычно ласково зовут «аппнотами», доступны на сайте Atmel по адресу: ) типовую схему простейшего ISP-программатора (см. «аппноту» AVR910, которая в PDF-формате содержит схему, а asm-файлы включают исходные коды «прошивки»). Программу для ПК, впрочем, придется писать самому, на основе описания процесса программирования (или доставать где-нибудь готовую, например, извлекать из AVR Studio), поскольку программатор лишь транслирует команды.
Конечно, изготовление такого программатора самостоятельно — это занятие очень «на любителя», потому что количество затраченных усилий отнюдь не соответствует результату. Программатор можно заказать в самой Atmel, хотя это и нецелесообразно по ряду причин, т. к. долго и дорого (зато поддержка AVR Studio, о которой далее, обеспечена). Дешевенькое «наколеночное» устройство такого типа можно приобрести рублей за 300 через Интернет, а цена «более фирменных» ISP-систем не выходит за пределы примерно 30 долл. Автор этих строк много лет пользуется программатором AS-2 фирмы Argussoft, который непосредственно подсоединяется к COM-порту (на рис. 13.2 он показан без соединительного кабеля).
Рис. 13.2. Программатор AS-2 фирмы Argussoft в разобранном виде
Есть и аналогичная модификация AS-3, которая работает с USB. Программатор позволяет «прошивать» прямо в системе многие микросхемы Atmel (не только AVR), причем интерфейс управляющей программы на русском языке очень нагляден, что, в частности, дополнительно предохраняет от неверной установки Fuse-битов (см. раздел в конце этой главы). Поддерживается пакетная работа (когда стирание, запись и проверки объединяются в одну операцию), а также имеется полезная функция перепрошивки самого программатора, если он вдруг начинает сбоить.
Рис. 13.3. Программа-загрузчик к программатору AS-2
Программирующий разъем — десятиконтактный игольчатый IDС или PLD (подробнее про наименования разъемов такого типа см. главу 14) и именно на него будут ориентированы все схемы, приведенные в этой книге. Это не значит, что я вас призываю покупать именно AS, разъем такого типа стал стандартом и им снабжены многие ISP, кроме упоминавшегося простейшего ISP от самой Atmel, где разъем минимальный— шестиконтактный (три линии собственно SPI, а также два питания и Reset). Впрочем, при необходимости всегда можно изготовить переходник, т. к. все последовательные программаторы работают по одним и тем же линиям (в десятиконтактном разъеме «лишние» контакты служат для разделения сигнальных линий «земляными», так же, как в соединительном кабеле IDE-интерфейса жестких дисков).
Внешний вид программирующего разъема PLD-10 на макетной плате и разводка его выводов показана на рис. 13.4. Обратите внимание, что отсчет выводов ведется не по кругу, как для микросхем, в нижнем ряду расположены все нечетные выводы, в верхнем — четные (это удобно, т. к. не приходится нарушать нумерацию у разъемов с различным количеством контактов, когда короткие могут получаться из длинных простым «отламыванием» лишней части). Естественно, при отсутствии чехла ответная часть может быть при-соединена двумя способами, потому первый вывод на плате следует помечать (на фото видна черная точка, проставленная фломастером). Названия выводов соответствуют таковым у контроллера.
Рис. 13.4. Пример расположения программирующего разъема PLD-10 на макетной плате и его разводка выводов
С или ассемблер ?
Ядро и система команд МК AVR с самого начала создавались в сотрудничестве с фирмой IAR Systems— производителем компиляторов для языков программирования C/C++. Поэтому структура контроллера максимально оптимизирована для того, чтобы можно было писать программы на языках высокого уровня. Так утверждает реклама, но верить подобным утверждениям стоит с некоторой оглядкой, т. к. все в конечном счете зависит от компилятора. Ведь задача перед ним стоит очень непростая: перевести строки на языке высокого уровня в команды контроллера, что для AVR ничуть не проще, чем для «настоящих» микропроцессоров, и потери тут неизбежны как в части компактности кода, так и времени его выполнения. Но это еще не самое главное.
Фирма IAR Systems в настоящее время предлагает серию пакетов Embedded Workbench для более чем двадцати типов МК различных фирм (под девизом «различные архитектуры — одно решение»). Здесь все рассчитано на то, чтобы человек, владеющий языком С, с минимальными потерями времени смог «пересесть» с одного на другой тип МК. В этом монструозном инструменте все было бы, возможно, и здорово, если бы не цена, которая измеряется в тысячах американских «президентов». Конечно, кое-кто в нашей стране может не придать этому значения (и многие не придают), но сначала стоит рассмотреть саму целесообразность такого подхода, а она вызывает обоснованные сомнения у многих специалистов.
Упомянем также, что IAR Systems — не единственный разработчик компиляторов с языка С для AVR. Есть куда более изящные и не столь «навороченные» инструменты (например, ICC for AVR от фирмы ImageCraft, CodeVisionAVR от HP Infotech), но в любом случае реальная цена их начинается от сотен евро. И это оправданно, поскольку рабочие инструменты должны быть качественными, а потому дешевыми быть не могут. Но для полноты картины стоит упомянуть и бесплатный WinAVR (AVRGCC, winavr.sourceforge.net), который создан на основе компилятора GNU GCC и, соответственно, распространяется по лицензии GPL (вместе с исходными кодами), обладающий всеми недостатками и достоинствами «свободных» продуктов, на чем мы здесь не будем останавливаться. Этот продукт поддерживается AVR Studio, о которой далее.
С другой стороны, фирменный ассемблер (в том числе и вариант, работающий в графическом режиме под Windows) абсолютно бесплатен. Даже если вы пойдете на поводу у тех, кто не придает никакого значения факту наличия у программных продуктов некоторой стоимости или рискнете обратиться к WinAVR, то вам необходимо еще принять во внимание следующие соображения. Язык С надо специально и отдельно учить. И в любом случае никто вас также и не освободит от подробного изучения аппаратной части МК. Для начинающего все это в совокупности может оказаться слишком сложным.
А что, спросите вы, язык ассемблера учить не надо? Практически не надо, потому что ассемблер— это, вообще говоря, не язык, а просто несколько правил оформления последовательности команд МК в текстовом представлении в «понятную» компилятору программу. Основные правила изложены в этой главе далее. Фирменное описание ассемблера для AVR занимает несколько страничек (сравните с фолиантами для изучения С) и входит в AVR Studio, отдельно его можно скачать с сайта Atmel. Функциональность команд наизусть заучивать отнюдь не требуется, т. к. даже профессионалы во время работы кладут перед собой открытый справочник по командам. Причем для каждого типа МК и правила написания, и мнемоника одинаковых по содержанию команд может быть различной, но в принципе все ассемблеры похожи и переход от одного к другому не настолько трудоемок, как это стараются представить разработчики «многокилобаксовых» компиляторов с языка С. Куда больше времени занимает изучение самой структуры контроллеров и работы их отдельных узлов.
То, что для ассемблера выучить действительно потребуется — это реализация некоторых типовых приемов программирования, таких как различные циклы с условными и безусловными переходами, арифметические функции, деление на локальные и глобальные переменные и т. п. Это и служит основным аргументом в пользу языков высокого уровня, где эти вещи уже сделаны за вас. Я бы выразился так: выбор между С и ассемблером зависит от вашей предыдущей подготовки. Если вы вообще никогда не сталкивались с программированием, но разбираетесь в электронике и функционировании узлов МК, то с ассемблером вам будет освоиться намного легче, ведь вы просто напрямую взаимодействуете с этими самыми узлами. Если же, наоборот, вы легко программируете на С, и зачем-то вам потребовалось освоить контроллеры, то выбирайте С, собственно, для этого упомянутые компиляторы и создавались.
Заметки на полях
«Программирование без GOTO» — лозунг знаменитого Дейкстры, классика процедурного программирования, к ассемблеру, к сожалению, абсолютно неприложим. Дейкстра имел в виду, что большое количество операторов перехода на метку очень сильно усложняет чтение программы и ведет к лишним ошибкам (подобные программы Дейкстра называл «спагетти» — из-за многочисленных линий переходов, сопровождающих графическое представление такой программы в виде блок-схемы). Но после компиляции все эти хорошо знакомые знатокам Pascal циклы while, do или repeat, until , равно как и оператор выбора (case), и даже — с некоторыми нюансами — обращение к процедурам, все равно превращаются в набор условных или безусловных переходов на определенные адреса в памяти программ. В ассемблере это приходится делать, что называется, «ручками». Разумеется, считать адреса (как при программировании в машинных кодах в начале 1950-х) вручную не нужно, в программе просто ставится метка, как и в языках высокого уровня (только в ассемблере еще проще, чем даже в каноническом Бейсике — в случае процедуры-подпрограммы метка заодно служит ее именем). Тем не менее сложности тут могут быть, и все зависит от склада вашего ума, некоторым (как автору этих строк) в хитросплетениях условных и безусловных переходов разобраться не составляет особенного труда, и иногда такое представление кажется даже более наглядным. Другие от этого «стонут и плачут», и им, конечно, следует обращаться к языку С.
При первоначальном изучении микроконтроллеров я бы решительно рекомендовал ассемблер. Как писал упоминавшийся в начале главы классик программирования Дональд Кнут, «каждый, кто всерьез интересуется компьютерами, должен рано или поздно изучить по крайней мере один машинный язык». Ассемблер — это первично, а все остальное вторично. Но автор этих строк вовсе не является упрямым снобом и прекрасно понимает, что сколько-нибудь крупные проекты (когда число строк кода начинает измеряться тысячами и более) на ассемблере будет создавать весьма затруднительно, по крайней мере, создавать «с нуля», не используя уже готовые модули. Но даже если вы в конце концов перейдете на С, советую эту книгу все же изучить до конца, чтобы познакомиться с AVR, а сам язык в приложении к AVR неплохо изложен в [4]Конструкция опубликована автором в журнале «Радио», 2004, № 9.
.
В этой книге вы встретите только ассемблерные программы. Мы ведь не будем писать для AVR операционные системы, и к тому же максимально попытаемся использовать готовые модули. А разобраться в работе этих модулей, когда они представлены в виде «голых» команд процессора, значительно проще. Не будем мы здесь и разбирать работу в AVR Studio — бесплатном пакете от Atmel, предназначенном для отладки программ (впрочем, программы на ассемблере в нем можно и создавать, а вот на С — только отлаживать, если нет дополнительного компилятора из перечисленных ранее). Вообще-то этот пакет в некоторых случаях оказывается очень целесообразен, особенно в совокупности с различными отладочными модулями (т. н. «китами», Kit’s, которые, в отличие от самого пакета, отнюдь не бесплатны). В нем удобно отлаживать сложные линейные (т. е. не использующие прерываний) процедуры, например, какое-нибудь деление четырехбайтных чисел. И если у вас хватит терпения и усидчивости, чтобы разобраться с еще одной «прослойкой» между вами и микросхемой, то вы ничего не потеряете. Но я не буду загромождать книгу не столь обязательными описаниями промежуточного софта, созданного исключительно для удобства, а покажу, как можно любую вашу схему превратить в отладочный модуль без лишних сложностей. Если вы заинтересовались AVR Studio, то вам сюда [3]Конструкция опубликована автором в журнале «Радио». 2002, № 8.
.
Заметки на полях
Автор этих строк — приверженец стиля разработки программ, который подозрительно напоминает широко известную в программистской среде «разработку, управляемую тестированием». Индустрия ПО приучает программистов к иному стилю: условно говоря, когда все создается «на бумаге». Сначала утверждается общий проект (для чего даже есть специальные люди — программные архитекторы), рисуются блок-схемы, разные программисты пишут отдельные куски кода (отлаживая их, конечно, но только «теоретически», без привязки к реальной рабочей среде), потом это объединяется в некий «проект», наконец, собирается альфа-версия и начинается тестирование готовой программы. Это привлекательный способ, особенно в случае разработки больших проектов, потому что он хорошо управляется в рамках стандартных бизнес-процессов. Именно в расчете на этот способ поточного производства программ создаются компиляторы со строжайшей проверкой типов данных и инструменты вроде AVR Studio, чтобы отловить мелкие ошибки по максимуму еще на начальной стадии. Но общие ошибки на уровне алгоритмов все равно отловить сразу не удается, и потом не удивляйтесь, откуда в конечном продукте низкоуровневые баги, которые следовало бы исправить еще на стадии блок-схемы. Хороший пример дает скандал с гибелью американской станции Mars Climate Orbiter, произошедшей из-за нестыковки в единицах измерения длины различных модулей бортовой навигационной программы.
Способ «разработки, управляемой тестированием» (его еще иногда называют «экстремальным программированием») намного сложнее в организации, потому что предполагает определенный уровень подготовки и заинтересованности исполнителей, и с большим трудом может быть формализован в терминах календарных планов. При этом способе любой (в идеале) кусок кода тестируется сразу после написания прямо в рабочей среде, и конечный проект собирается из таких уже по максимуму отлаженных фрагментов. Остается только отладить их взаимодействие, что также выполняется немедленно по ходу сборки. Когда вы начинаете какую-то программу с неизвестным ранее алгоритмом — создайте сначала отдельную вспомогательную программу, где фрагменты этого алгоритма можно детально проверить. Это значительно дольше, чем писать программу сразу (и что еще важнее — здесь очень трудно рассчитать время заранее), но позволяет сэкономить кучу времени на последующем доведении программы до ума. Именно для такой цели и удобна система, которую я опишу в главе 16 , когда вы имеете немедленную обратную связь от вашего устройства.
Обратите внимание на следующий момент: в этой концепции документация пишется не до, а после создания и отладки собственно программы. Это полезно вдвойне, поскольку документацию не приходится непрерывно править и в ней оказывается меньше ошибок, а программа при написании документации к ней лишний раз анализируется, и очень часто бывает, что на этой стадии в ней также обнаруживаются незамеченные ошибки и недоработки. Поэтому мой совет всем без исключения разработчикам таков: даже если вы делаете устройство в одном экземпляре и для собственного удовольствия, всегда потом составляйте детальное описание программы. Это пригодится не только для того, чтобы что-то «починить» или усовершенствовать спустя некоторое время, когда детали забудутся, но и для того, чтобы лучше «отловить» ошибки и добавить забытые, но необходимые «фичи». По аналогичным причинам текст программы обязательно следует сопровождать комментариями.
Обустройство ассемблера
Если у вас еще нет AVR-ассемблера (т. е. самой программы-компилятора), то, возможно, AVR Studio вам придется установить, т. к. это самый надежный способ добыть последнюю версию ассемблера. Скачайте AVR Studio с сайта Atmel (например, отсюда , к сожалению, последние версии пакета стали довольно «монструозными» — более 40 Мбайт), установите его, разыщите файл avrasm32.exe или более современный avrasm2.exe (он находится в папке…\Atmel\AVR Tools\AvrAssembler) и скопируйте его в подготовленную заранее другую папку. Оттуда же целиком целесообразно скопировать папку Appnotes, во-первых, из-за собственно «аппнот», которые представляют собой рекомендации по реализации отдельных функций и выгодно отличаются от представленных на сайте Atmel форматом ASM. По сути, это готовые модули для встраивания в вашу программу (на сайте они находятся в формате PDF, а исходные коды приходится скачивать отдельно). Учтите, что в них могут быть ошибки, о чем мы еще будем упоминать, и применять их надо с оглядкой.
Во-вторых, в этой папке находятся файлы макроопределений (с расширением inc) для всех моделей AVR, поддерживаемых данной версией ассемблера, даже для тех, что уже не выпускаются. Если не скопировать папку, их пришлось бы скачивать с сайта Atmel поодиночке, а для старых типов искать где-то на стороне. Эти файлы необходимы, иначе вы не сможете откомпилировать ни одной программы, поскольку сам ассемблер абсолютно не подозревает о существовании таких вещей, как PortA или регистр DDRC, а «знает» только числовые адреса соответствующих регистров. Соответствие между этими мнемоническими обозначениями и адресами и устанавливается с помощью inc-файлов, причем для разных моделей эти адреса могут различаться. Можно на всякий случай извлечь также описание AVR-ассемблера в формате СНМ (на английском, естественно). После копирования саму AVR Studio можно удалить.
Ранее существовал и довольно примитивный ассемблер под Windows (wavrasm.exe), но затем, видимо, в корпорации решили его не развивать, обойдясь AVR Studio. Так что нам, сермяжным, придется обустроить среду для создания программ самостоятельно — это делается один раз, и потом вы вообще сможете забыть про существование avrasm.exe. Для этого потребуется еще как минимум текстовый редактор. Можно обойтись Блокнотом или многочисленными его более функциональными заменителями, вроде Edit Plus (MS Word не подойдет решительно), но тогда нам придется отдельно каждый раз запускать процесс компиляции из командной строки. Обычно это делается с помощью FAR или других клонов Norton Commander, что неудобно и сильно замедляет процесс, Потому лучше использовать специальный редактор, который позволит запускать процесс компиляции прямо из своего окна, и к тому же имеет возможность «подсветки» синтаксиса. Так как специально для AVR редакторов никто не делает, они чаще всего по умолчанию «заточены» под Intel-ассемблер, но «подсветку» нередко можно настроить «под себя».
Заметки на полях
Редакторов для написания ассемблерного кода довольно много, как правило, они в той или иной степени «самодеятельные» и бесплатные (исключение — очень профессиональный и известный с давних времен, но платный MultiEdit). Тут важно только выбрать самый удобный, иначе можно попасть в ситуацию, когда будет еще хуже, чем с Блокнотом. Например, широко разрекламированный на множестве ресурсов ASMEdit (некоего Матвеева), первое, что у меня сделал — еще в процессе инсталляции «повесил» Windows 98 [11] до полной неработоспособности, а когда я все же «достучался» до исполняемого файла, то запустить его оказалось невозможным, т. к. окно свернулось в углу экрана в маленький квадратик и распахиваться не желало. Я разыскал более старую версию (ASMEdit 1.2), она установилась нормально (если не считать грамматических ошибок в инсталляторе), и тут выяснилось, что: а) настройка запуска компиляции из командной строки настолько сложна, что требует чуть ли не написания отдельной программы; б) настройка «подсветки» синтаксиса крайне примитивна. К тому же программа без спроса ассоциирует с собой расширение asm и «замусоривает» перечень ассоциаций еще полудюжиной расширений для неведомых целей, которые потом приходится вычищать вручную. Я это так подробно рассказываю потому, что у этого редактора есть одно удобное свойство: сообщения компилятора перенаправляются в окно редактора, как это было в wavrasm, и не требуется просматривать отдельные консольные окна. Если вам удастся «справиться» с ASMEdit, то вам сильно повезло.
Я перебрал в свое время несколько программ, но ни одна меня не устроила в такой степени, как творение некоего Анатолия Вознюка, которое я использую с 1999 года. Сам Анатолий знаменит также своим шедевром под названием Small CD Writer, давно скрывается инкогнито, сайт его больше не откликается, а программы не развиваются (в частности, Small CD Writer так и «не умеет» корректно писать на DVD). Но в разбираемом редакторе под названием ASM Editor (последняя версия 2.2), собственно, больше и развивать нечего. Доступен он на множестве «софтоотстойников» в Интернете* только не перепутайте его с упомянутым ASMEdit.
Для начала работы нам предварительно надо создать командный файл. Предположим, у вас файл avrasm32.exe находится в созданной вами папке c: \avrtools. Запустите Блокнот и введите в него следующий текст (соответственно измените путь, если папка другая): c: \avrtools\avrasm32 — fI %1.asm
Сохраните этот файл под именем, например, asm32.bat в той же папке, что и avrasm32.exe. Теперь запустите ASM Editor, выберите в меню пункт Service|Properties и в появившемся окне найдите вкладку Project, а в ней — строку Assemble ASM file (рис. 13.5). В эту строку, как и показано на рисунке, внесите путь к нашему bat-файлу. Больше ничего настраивать не требуется, остальные пункты можно оставить, как есть, или очистить — по желанию. Теперь по нажатию сочетания клавиш +, вне зависимости от того, где находится файл с редактируемой программой, он скомпилируется, и результат (т. н. hex-файл, о чем далее), если нет ошибок, окажется в той же папке, где и исходный текст.
Рис. 13.5. Настройка редактора ASM Editor
Одновременно с компиляцией отдельно возникает консольное (с белыми надписями на черном фоне) окно с сообщениями компилятора. Сразу смотрите на последние строки, где должна быть надпись «Assembly complete with no errors» («ассемблирование выполнено без ошибок»). Если такая надпись есть, ищите готовый hex-файл в папке с исходным текстом. В противном случае hex-файла не окажется (а предыдущий его вариант, если он был, будет стерт), а в этом окне появятся номера строк в исходном тексте с ошибками и примерным описанием проблемы в данной строке (например, сообщение «Undefined variable reference» — «ссылка на неопределенную переменную», означает, что у вас в данной строке идентификатор, который нигде не определен).
Кроме этого, при успешной компиляции в окне сообщений можно посмотреть точный размер скомпилированной программы в словах (words), для получения размера в байтах надо умножить это число на два. Отметим сразу, что объем hex-файла размеру программы соответствовать не будет (подробнее см. далее), так что узнать объем занимаемой памяти можно только отсюда.
Структура программы AVR
Как вы уже знаете, при работе МК последовательно выполняет команды программы, имеющейся в памяти. Программист может менять порядок выполнения команд, организуя циклы и различные переходы. Одно из самых мощных средств программирования — вызов подпрограмм или процедур (в Pascal подпрограммы делятся на процедуры и функции, а в языке С есть только функции, но это дела не меняет — в принципе это все одно и то же), т. е. кусков кода, которые могут использоваться неоднократно. Во всех ассемблерах вызов процедур предусмотрен обязательно.
Естественно, программу сначала нужно в память записать, причем записать совершенно определенным образом, так, чтобы МК «знал», откуда начинать при включении питания или после подачи импульса Reset. Это его «знание» в случае современных МК AVR также программируется, однако для простоты будем считать, что программа всегда начинает читаться с самой первой ячейки памяти программ, т. е. с нулевого адреса. (А вот для классического процессора семейства i86 это не так. По умолчанию он всегда начинал с команды, расположенной за 16 байт до конца первого мегабайта памяти, т. е. по адресу FFFFOh. К тому же, адрес этот можно изменять, но мы этим здесь заниматься не будем.) Исходя из этих обстоятельств, программа должна иметь определенную структуру.
По начальному (нулевому) адресу всегда располагается одна и та же команда безусловного перехода (по-английски jump — «прыжок»), которая записывается так:
rjmp RESET
Или так:
jmp RESET
Отметьте себе на память, что AVR-ассемблер, в отличие от канонического С, регистры букв не различает (одинаково правильной будет форма записи jmp, JMP и jmp, так же как Reset, RESET и reset).
Форма написания (jmp или rjmp) зависит от типа контроллера: если в нем объем памяти программ меньше или равен 8 кбайт, то всегда (и не только в этом случае) используется команда rjmp (relative jump, т. е. «относительный безусловный переход»). Она занимает в памяти два байта, как и практически все остальные команды AVR, о чем мы еще поговорим. Код самой команды в этих двух байтах занимает старшую тетраду старшего байта (т. е. четыре бита), остальные 12 бит представляют собой адрес, куда переходить. В данном случае компилятор подставит адрес команды, следующей сразу за меткой RESET, с которой и начнется собственно выполнение программы. Метка с этим именем, естественно, всегда должна присутствовать, но может быть расположена уже в любом другом удобном месте программы, за исключением еще нескольких первых адресов, о назначении которых далее. Метка, кстати, может называться и не RESET, а любым другим именем, просто так принято для удобства чтения: хотите найти в любой программе ее начало — ищите метку RESET.
Вернемся к форме записи команды. 12 бит адреса могут представлять 4096 различных адресов. Так как единицей объема памяти программ служит слово из двух байтов (а не отдельный байт), как раз из-за того, что любая команда занимает не менее чем два байта, то общий объем адресуемой таким образом памяти и составит 8 кбайт. А вот если памяти больше, то приходится использовать команду jmp (абсолютный безусловный переход). Она состоит из четырех байт, в которых адрес займет 22 бита, и потому с ее помощью можно адресовать до 4 М (8 Мбайт) памяти. Мы условимся, что старшие модели семейства Mega применять не будем, и потому ограничимся командой rjmp.
Подведем некоторый итог: мы уже узнали многое о структуре типовой программы для AVR. Она должна начинаться с безусловного перехода на метку RESET, а само начало этой программы будет располагаться сразу после этой метки где-то в другом месте программы. А почему так странно? Нельзя ли начать прямо сразу с нулевого адреса, строка за строкой, зачем нужны какие-то переходы? Можно, отвечают нам авторы описаний AVR. Простейшая программа может начинаться с нулевого адреса без переходов, но только в одном случае: если мы обязываемся отказаться от прерываний. А без них контроллер теряет 90 % своей функциональности. Конечно, можно придумать пример программы, которая бы делала что-нибудь полезное, но при этом работала совсем без прерываний. Но на практике без прерываний обходятся только тогда, когда штатных прерываний хватает (например, для отслеживания состояния большого количества кнопок), или когда без них программа получается компактнее (такие случаи мы еще встретим).
Обработка прерываний
AVR по умолчанию ожидает, что сразу после первой команды с адресом $0000 идет таблица т. н. векторов прерываний. Вектор — это просто отсылка по нужному адресу с помощью команды rjmp. Адрес обозначается меткой, может располагаться в любом месте программы, и содержит начало процедуры обработки прерывания. Первый вектор располагается по адресу $0001 (а для МК с памятью более 8 К — по адресу $0002, потому что по адресу $0001 находится вторая половина более длинной команды jmp), причем адрес этот означает номер двухбайтного слова в памяти, а не отдельного байта (оно в два раза меньше количества байт). Попутно заметим, адреса в SRAM (ОЗУ) именно байтовые, а не пословные (см. далее). На самом деле в режиме по умолчанию нам вообще не нужно думать про абсолютные адреса и их нумерацию: первая команда программы (rjmp RESET), которая еще называется вектором сброса (или вектором начальной загрузки), автоматически расположится по нулевому адресу, вторая — по адресу $0001 и т. д. Найдя какую-нибудь команду перехода по метке, компилятор автоматически подставит абсолютные адреса.
Порядок векторов и их количество в таблице жестко задано в соответствии с типом МК. Потому самое первое, что вы должны сделать, приступая к программированию, — открыть руководство по применению выбранного типа контроллеров и скопировать оттуда эту таблицу. Можно попытаться сделать это прямо через буфер обмена из PDF-описания или преобразовав его в другой формат, так меньше вероятность что-то пропустить, только придется потом удалить указанные там абсолютные адреса, стоящие в начале каждой строки. Вот как будет выглядеть заготовка начала программы для МК ATmega8 (листинг 13.1).
Листинг 13.1.
;=======прерывания=======
rjmp RESET ;Reset Handler
rjmp EXT_INT0 ;IRQ0 Handler
rjmp EXT_INT1 ;IRQ1 Handler
rjmp TIM2_COMP ;Timer2 Compare Handler
rjmp TIM2_OVF ;Timer2 Overflow Handler
rjmp TIM1_CAPT ;Timer1 Capture Handler
rjmp TIM1_COMPA ;Timer 1 CompareA Handler
rjmp TIM1_COMPB ;Timer1 CompareB Handler
rjmp TIM1_OVF ;Timer1 Overflow Handler
rjmp TIM0_OVF ;Timer0 Overflow Handler
rjmp SPI_STC ;SPI Transfer Complete Handler
rjmp USART_RXC ;USART RX Complete Handler
rjmp USART_UDRE ;UDR Empty Handler
rjmp USART_TXC ;USART TX Complete Handler
rjmp ADC ;ADC Conversion Complete Handler
rjmp EE_RDY ;EEPROM Ready Handler
rjmp ANA_COMP ;Analog Comparator Handler
rjmp TWSI ;Two-wire Serial Interface Handler
rjmp SPM_RDY ;Store Program Memory Ready Handler
;========================
Обратите внимание, что в ассемблерной программе каждый оператор занимает отдельную строку (в большинстве языков высокого уровня операторы можно записывать в одной строке, например, разделяя их знаками препинания; здесь это не допускается). Символы пробелов и табуляции могут встречаться в произвольном количестве, и они полностью игнорируются, за исключением двух случаев: нельзя разбивать идентификатор на части и, наоборот, нельзя соединять разные идентификаторы между собой; хотя бы один пробел, знак табуляции (или знак препинания, если это предусмотрено форматом команды) между наименованиями инструкций, переменных, регистров и т. п. должен быть.
Имена меток, конечно, можно присваивать произвольно. Как вы уже поняли, после точки с запятой идет комментарий — произвольная строка, на которую компилятор не обращает внимания. На новую строку комментарий не переходит: в случае длинного комментария в начале каждой следующей строки также надо ставить точку с запятой.
Но постойте, мы что, обязаны использовать все прерывания? Конечно, нет. Для неиспользуемых прерываний вместо команды rjmp <метка> следует поставить reti — выход из прерывания («return interrupt»). На самом деле здесь возможна и команда пор — пустая операция («по operation»). Я пробовал из любопытства, — все отлично работает. Но руководство рекомендует именно reti, т. к. при случайной инициализации прерывания оно все равно не будет выполняться, а команда пор в этом случае, очень вероятно, привела бы к «зависанию» программы либо выполнению совсем других, не предусмотренных нами, операций.
Лично я оформляю начало программы следующим образом: копирую таблицу в указанном ранее виде, а затем в начале каждой строки (кроме первой) автоматически меняю с помощью функции поиска-замены редактора «rjmp» на «reti; rjmp», вот так (старую команду я закомментировал):
rjmp RESET ;Reset Handler
reti; rjmp EXT_INT0 ;IRQ0 Handler
reti; rjmp EXT_INT1 ;IRQ1 Handler
…
Теперь заготовка начала программы готова: при необходимости в дальнейшем какого-нибудь прерывания, мы удаляем из нужной строки фрагмент reti;, а затем где-то в программе перед началом процедуры обработки этого прерывания ставим нужную метку, например:
rjmp RESET ;Reset Handler
rjmp EXT_INT0 ;IRQ0 Handler
…
EXT_INT0: ;процедура обработки прерывания INTO
…
reti ;окончание процедуры обработки прерывания INTO
Обратите внимание, что после метки в программе (но не внутри команды, которая на нее ссылается!) должно идти двоеточие, иначе компилятор не «поймет», что перед ним именно метка. Кроме того, меток не касается правило написания операторов в отдельных строках (потому что метка не оператор, а адрес), после метки и двоеточия в программе сразу может идти оператор.
Процедура RESET
Теперь обратимся к процедуре RESET, т. е. к истинному началу программы, что там должно быть? Когда контроллер доходит до команды вызова подпрограммы (call или rcall, см. далее), или в нем происходит прерывание, он должен сохранить состояние программного счетчика с тем, чтобы потом «знать», куда вернуться. Это происходит в специальной области памяти, которая называется стеком (stack). Потому в любой программе на AVR-ассемблере для семейств Mega и Classic первыми после метки reset должны идти следующие строки:
RESET:
ldi temp, low(RAMEND) ;загрузка указателя стека
out SPL, temp
ldi temp, high(RAMEND) ;загрузка указателя стека
out SPH, temp
Этими операторами вы указываете, где компилятору расположить программный стек, а именно в конце SRAM (что обозначается константой RAMEND, объявленной в соответствующем inc-файле, загляните). Служебные слова low и high означают, что от ramend нужно взять, соответственно, младший и старший байты. Для тех моделей, у которых объем SRAM не превышает 256 байт (из доступных в настоящее время это только модель 2313 во всех ее версиях), запись сокращается:
RESET:
ldi temp, RAMEND ;загрузка указателя стека
out SPL, temp
Заметки на полях
У многих представителей семейства Tuny (кроме, насколько я знаю, Tuny2313), в том числе тех, у которых объем SRAM также не превышает 256 байт, стек аппаратный, а не программный, потому там его расположение специально указывать не требуется (отметим, что в [2] в этой части допущена ошибка). По этой причине, в частности, для семейства Tuny не определены команды push и pop, которые для остальных моделей позволяют временно сохранять в стеке значение какого-либо регистра (а для некоторых процессоров, вроде i8086, в которых регистров общего назначения мало, это вообще одни из самых употребительных команд). Размер же аппаратного стека строго фиксирован, и в нем умещается только адрес возврата из подпрограммы или обработчика прерывания.
После задания стека желательно поставить следующие строки:
ldi temp,1<<ACD
out ACSR,temp ;выкл. аналог, компаратор
Почему желательно, а не обязательно? Потому что по умолчанию аналоговый компаратор всегда включен и, соответственно, расходует питание. Если вы его не используете, то зачем лишнее потребление? Правда, это практически не скажется на потреблении в нормальном режиме работы, т. к. доля компаратора здесь очень мала. Вставка этих строк становится критичной только в том случае, если применяются режимы энергосбережения. С другой стороны, если компаратор нужен (см. главу 14), то, конечно, эти строки следует исключить.
После всего этого в процедуре reset обычно идет секция инициализации, где разрешаются прерывания, устанавливаются состояния выводов портов, инициализируются начальные значения переменных и т. п. Примеры мы еще встретим в тексте этой книги неоднократно. Эта секция обязательно должна заканчиваться командой sei — общим разрешением прерываний, т. к. по умолчанию они запрещены.
Теперь вроде бы все готово к работе, но что будет делать контроллер в ожидании прерываний? Ведь основная функциональность большинства микропрограмм сосредоточена именно в их обработчиках, и в простейшем случае контроллер попросту ничего не должен делать, пока не придет сигнал очередного прерывания. Поэтому простейшая программа должна заканчиваться пустым циклом:
…
sei ;разрешаем прерывания
STOP:
rjmp STOP
На самом деле в этом цикле можно делать и что-то полезное, например, войти в один из режимов энергосбережения, или отслеживать изменение состояния какого-то вывода, или момент прихода байта через UART и т. п. — в дальнейшем мы увидим примеры подобных действий. Именно внутри такого цикла работают немногочисленные программы, вообще не использующие прерываний.
Определения переменных, констант и подключение внешних файлов
Определения играют в процессе написания ассемблерной программы важную роль, не менее важную, чем секция объявления переменных в программе на языке высокого уровня. Сравните две записи:
….
mov r16,r11
cpi r1l6,$11
…
и
…
mov temp,counter
cpi temp,max_value
…
И та, и другая запись верна, но вторая значительно более «читабельна», правда? Одно дело иметь программу, в которой фигурируют регистры (еще слава богу, что не их абсолютные адреса) и числа, совсем другое — работать с «говорящими» именами переменных и констант. Идентификатором temp (от слова «temporary» — временный) обычно обозначают рабочую переменную, которая не предназначена для долговременного хранения данных, counter — в переводе «счетчик», max_vaiue — «максимальное значение». Но если команды компилятор «знает» (мнемоника команд фиксирована и их смысл мы расшифруем немного далее), то как ему узнать, что мы имели имели в виду под этими самыми temp и counter?
Для этого служит секция определений имен переменных и констант, которую чаще всего располагают в самом начале текста программы, еще до векторов прерываний, или в отдельном внешнем файле, если определений много, и они очень загромождают текст. Именно такие макроопределения и содержат те самые файлы с расширением inc, которые мы копировали из AVR Studio, — для каждого контроллера свои. В результате чего мы можем писать mov GIMSK, temp вместо mov $3F,r16.
Для того чтобы компилятор нашел эти определения, inc-файл нужно подключить к нашей программе. Это делается с помощью директивы include, как в языке С, например (обратите внимание на точку перед include):
.include «2313def.inc»
В данном случае подключается файл с макроопределениями для контроллера AT90S2313. Подобной строкой должен начинаться текст любой программы. Причем файл должен соответствовать выбранному контроллеру, иначе программа может и не заработать: иногда на ошибку вам укажут при компиляции, иногда это выявится только во время исполнения.
Отметим, что имя файла здесь может быть абсолютно произвольным, — по директиве include компилятор, «не думая», разыщет файл с указанным именем (разумеется, он должен находиться в текущем каталоге, или для него должен быть указан полный путь), скопирует из него текст и вставит этот текст в том месте вашей программы, где расположена директива. И только потом начнет «разбираться». С директивами include надо быть аккуратным, если файл содержит команды, а не только определения, то вставлять его нужно уже не в начале текста, а после всех векторов прерываний, иначе выполнение программы начнется с него.
Но имени temp (не говоря уж о counter) вы в inc-файлах не найдете — это нестандартное определение и относится уже к нашей «епархии». Создать свои определения можно с помощью директив def (definitions) для переменных и equ (equvalent) для констант вот так:
.equ max_value = $11 ;максимальное значение = 17
.def temp = r16 ;регистр r16 есть переменная temp
.def counter = r05 ;регистр r05 есть переменная counter
Эти определения также обычно располагают в начале текста программы. Теперь компилятор разберется, что к чему в нашем примере ранее. Только учтите, что никаких проверок, кроме синтаксических, тут не выполняется, потому можно объявить два разных имени для одного регистра, и они будут восприниматься как синонимы:
.def temp = r16 ;регистр r16 есть переменная temp
.def counter = r16 ;регистр r16 есть переменная counter
Изменение temp будет автоматически означать изменение counter и наоборот. Иногда этим пользуются, если в разных частях программы один регистр применяется для разных по смыслу вещей (и вы можете встретить такие примеры в фирменных «аппнотах»). В общем случае к такой возможности следует прибегать лишь в исключительных случаях — слишком много ошибок можно наделать.
С помощью директивы equ, вообще говоря, можно определять довольно сложные выражения, но этим пользуются редко, гораздо чаще ее применяют для определения переменных, которые располагаются не в регистрах, а в области SRAM. Например, следующая последовательность директив и команд запишет содержимое регистра counter в SRAM по адресу $60 (для большинства моделей, кроме старших Mega, это первый свободный адрес после занятых адресами регистров):
.equ memory_address = $60
…
clr ZH
ldi ZL,memory_address
st Z,counter
В заключение темы еще раз напомним, что имена переменных (как и команд, и меток) нечувствительны к регистру букв, что еще один плюс для AVR-ассемблера по отношению к языку С, где они по умолчанию различаются, и у начинающих это приводит к множеству ошибок.
Система команд AVR
С некоторыми командами мы уже познакомились, но еще не знаем толком, что они означают. Теперь я постараюсь разобрать основные команды и их использование. Подчеркну, что речь идет только о части самых употребительных и концептуально важных команд, потому что всего команд для AVR от 90 до 133 (в зависимости от контроллера), и только их подробное описание, например в [2]Префикс «К» в названии отечественных микросхем, обозначающий их принадлежность к бытовому/коммерческому диапазону температур, мы будем в этой книге опускать, подробнее см. главу 8 .
, занимает почти 70 страниц, гораздо больше, чем описание собственно AVR-ассемблера. С теми командами, которые выпадут из рассмотрения здесь, мы познакомимся по ходу дела в дальнейшем. Выборочный перечень базовых команд с их описанием, достаточный для составления большинства законченных программ, вы также найдете в Приложении 4. Однако в любом случае при написании собственных программ вам будет необходимо иметь справочник по командам, в роли которого может выступать как [1, 2], так и англоязычный документ PDF с описанием конкретного контроллера, который легко добывается с сайта Atmel. Учтите, что в неофициальных пособиях могут быть ошибки, так в [1, 2] они встречаются в достаточном количестве, хотя в последних изданиях их, возможно, и исправили.
Формат команды
Прежде всего, заметим, что команды (инструкции, операции — мы будем употреблять эти слова как синонимы) контроллера записываются в определенном формате. Сначала идет собственно мнемоническое название команды, которое может состоять из двух (ld, st), трех (mov, ldi, sei, add) или четырех (breq, brne, sbiw) символов. Затем после пробела (любого их числа, допустимы также знаки табуляции), если это предусмотрено форматом команды, идут операнды: имена регистров или константы (а также выражения), над которыми производится операция (на самом деле это не имена, а адреса, но мы не будем забывать про включение inc-файлов и создание собственных определений, и оперировать только с именами, так будет гораздо меньше ошибок).
Если операндов два (больше не бывает), то они перечисляются через запятую (с любым числом пробелов до или после нее, или вообще без них). Причем тут действует важное правило: во всех ассемблерах второй операнд является первым по смыслу действия (т. е. источником), а первый — приемником (результатом). Например, команда ldi temp, 1 расшифровывается, как «загрузить единицу в регистр temp», а операция sub temp, counter вычитает counter из temp, а не наоборот, и результат помещает также в temp.
Заметки на полях
Такая запись (действие— приемник— источник) стандартна для всех ассемблеров, и называется прямой польской записью: когда сначала просят налить чай (действие), потом указывают, в какой стакан (первый операнд, он же результат), и только после этого сообщают, где находится чайник (второй операнд). В отличие от обычной инфиксной , когда сначала указывается стакан (первый операнд), в который нужно налить чай (действие), ах да, вот из этого чайника (второй операнд). Если кто-то пробовал пользоваться некоторыми старыми моделями настольных калькуляторов, то там использовалась обратная польская запись, когда сначала указываются оба операнда, а потом действие (стакан и чайник, затем указывается, что нужно налить из второго в первый). Это наиболее удобно для компьютерных устройств (потому что позволяет без лишних сложностей реализовать операции вычисления по формулам со скобками любой степени вложенности), но отнюдь не для человека, т. к. счет на таких калькуляторах требовал специальной тренировки. В то же время ассемблерный порядок операндов обычно не доставляет трудностей при обучении, и довольно логичен: сначала главный операнд, который будет изменяться, а потом тот, с помощью которого и производятся изменения.
Выходные файлы
Есть, конечно, команды, которые вообще не требуют операндов (как уже знакомые нам пор или sei), или используют всего один операнд (cir), но вне зависимости от этого каждая команда AVR будет занимать в памяти ровно два байта (кроме немногих команд, вроде упоминавшейся jmp, которые работают с операндами большого размера, оттого занимают четыре байта). Такое готовое представление — командное слово, или код операции (КОП) — и записывается в виде числа компилятором в выходной файл, имеющий расширение hex, который необходим программатору для дальнейшей записи в контроллер. Кроме HEX-файлов, есть еще и другие форматы записи готовых программ (самый известный — бинарный), но hex-формат для микроконтроллеров самый распространенный, и мы будем рассматривать только его.
Рассматриваемый НЕХ-формат придуман фирмой Intel (есть и другие «гексы»). Отличается такой формат тем, что содержит числа в текстовом представлении (в шестнадцатеричной записи). Поэтому в случае чего его можно даже править в обычном текстовом редакторе (что исключено для бинарных файлов, содержащих числа, а не символы). Кстати, точно в таком же формате осуществляется запись констант в EEPROM, если это требуется.
Рассмотрим НЕХ-формат подробнее. На рис. 13.6 представлен файл короткой программы, открытый в обычном Блокноте. На первый взгляд тут «сам черт ногу сломит», но на самом деле все достаточно просто, хотя чтение затрудняется тем, что строки не поделены на отдельные байты. Разбираться будет проще, если вы скопируете этот файл под другим именем и расставите в нем пробелы после каждой пары символов. Мы же рассмотрим данный файл как есть.
Рис. 13.6. Файл формата HEX в Блокноте
Основную часть файла занимают информационные строки, содержащие непосредственно КОП. Они состоят из ряда служебных полей и собственно данных. Каждая строка начинается двоеточием, после которой идет число байт в строке. Кроме первой и последней, везде стоит, как видите, число 10 (десятичное 16), т. е. в каждой строке будет ровно 16 информационных байт (исключая служебные). После количества байт идет два байта адреса памяти, указывающие куда писать (в первой строке 0000, во второй это будет, естественно, 0010, т. е. предыдущий адрес плюс 16, и т. д.). Наконец, после адреса идет еще один служебный байт, обозначающий тип данных, который в информационных строках равен 00 (а в первой и последней — 02 и 01, о чем далее). Только после этого начинаются собственно байты данных, которые означают соответствующие КОП, записанные пословно (КОП для AVR, напоминаю, занимают в основном два байта, и память в этих МК также организована пословно), причем так, что младший байт идет первым. Таким образом, запись в первой информационной строке «ЗАС0» в привычном нам «арабском» порядке, когда самый старший разряд располагается слева, должна выглядеть, как СОЗА (если помните, я в главе 7 упоминал об этой несуразице).
В первой строке служебный байт типа данных равен 02, и это означает, что данные в ней представляют сегмент памяти, с которого должна начинаться запись (в данном случае 0000). Заканчивается hex-файл всегда строкой «:00000001ff» — значение типа данных 01 означает конец записи, и данных больше не ожидается. А что означает FF?
Самым последним байтом в каждой строке идет контрольная сумма (дополнение до 2) для всех остальных байт строки, включая служебные. Алгоритм ее вычисления очень простой: нужно вычесть из числа 256 значения всех байтов строки (можно не обращая внимание на перенос), и взять младший байт результата. Соответственно, проверка целостности строки еще проще: нужно сложить значения всех байтов (включая контрольную сумму), и младший байт результата должен равняться нулю. Так, в первой строке число информационных байтов всего два (первое значение в строке), плюс служебный байт типа данных, равный также двум, итого контрольная сумма всегда равна 256 — 2–2 = 252 = $FC. В последней строке одни нули, кроме типа данных, равного 1, соответственно, контрольная сумма равна 256 — 1 = 255 = $FF.
Теперь попробуем немного расшифровать данные. Первое слово в первой информационной строке, как мы выяснили, равно $С03А. Если мы возьмем фирменное описание команд, то обнаружим, что значению старшей тетрады в КОП, равной $С (1100 в двоичной системе), соответствует команда rjmp, но ведь так и должно быть, мы же договорились, что любая программа начинается с безусловного перехода на метку RESET. Теперь очевидно, что остальные биты в этом значении ($03А) представляют абсолютный адрес в программе, где в ее тексте стояла метка reset. Попробуем его найти, для этого вспомним, что адреса отсчитываются по словам, а не по байтам, т. е. число $ЗА (58) надо умножить на 2 (получится 116 = $74), и искать в этой области. Разыщем строку с адресом $0070, отсчитаем три пары байт от начала данных, и найдем там фрагмент «F894», который в нормальной записи будет выглядеть, как $94F8, а это, как легко убедиться по справочнику, есть код команды cli, запрещающей прерывания (которая в начале программы лишняя, т. к. они все равно запрещены, но, видимо, поставлена на всякий случай). Следующая команда будет начинаться с байта $Е5, и первая тетрада в ней обозначает код команды ldi (1110 — проверьте!), а пятерка, очевидно, есть фрагмент адреса конца памяти (RAMEND), который в силу довольно сложного формата записи получается на самом деле равным $025F (см. значение младшего байта, равное $2F). Такое значение соответствует значению ramend, определенному в inc-файлах для МК с 512 байтами встроенного ОЗУ. Все, как и должно быть. Если мы обратим внимание опять на первую-вторую строки с данными, то увидим повторяющийся фрагмент «1895», который, как легко догадаться из материала предыдущего раздела, должен быть командой reti (если проверите по справочнику, то так оно и окажется).
Как видите, разобраться довольно сложно, но при некотором навыке и наличии под рукой таблицы двоичных кодов команд вполне возможно. Именно так работает программа, которая превращает код обратно в текст — дизассемблер (он входит в AVR Studio). А зачем это может понадобиться на практике? Дело в том, что в памяти программ часто хранят те константы, которые предположительно не будут изменяться в процессе эксплуатации, например, устанавливаемые по умолчанию значения какой-то величины. Но, разумеется, по истечении некоторого времени эти константы обязательно захочется изменить. И если у вас текст программы по каким-то причинам отсутствует (например, программа взята из публикации в журнале или скачана с радиолюбительского сайта), а загрузочный hex-файл имеется, то всегда можно «хакнуть» исходный код, и немного подправить его под свои нужды.
Команды перехода (передачи управления)
В языках высокого уровня была всего одна команда перехода на метку (GOTO), и то Дейкстра на нее «набросился». А в ассемблере AVR таких команд пруд пруди, целых 33 шутки! Зачем? На самом деле без доброй половины из них, если не больше, можно обойтись во всех жизненных случаях, т. к. они в значительной степени взаимозаменяемы. Разнообразие это, если угодно, дань памяти великому программисту — для повышения читаемости программ. Мы рассмотрим только ключевые команды из этого перечня.
С командами безусловного перехода rjmp и jmp мы уже познакомились достаточно подробно, так что сразу перейдем к вызову процедур rcall и call (официальный язык фирменных руководств Atmel предпочитает вместо процедуры упоминавшийся нами консервативный термин — подпрограмма, subroutine). Синтаксис у них точно такой же, как у команд безусловного перехода, и, по сути, это тот же самый переход по метке. И разница между этими двумя командами аналогичная: call работает в пределах 64 К адресов памяти (или до 8 М в соответствующем контроллере, поддерживающем такой объем адресного пространства), занимает 4 байта и выполняется за 4 цикла, a rcall годится только для МК с объемом памяти не более 8 кбайт, но занимает 2 байта и выполняется за 3 цикла. Мы в дальнейшем будем пользоваться только командой rcall.
Отличаются они от команд безусловного перехода тем, что здесь в момент перехода к процедуре контроллер автоматически сохраняет в стеке адрес текущей команды для того, чтобы потом знать, куда вернуться (потому длительность выполнения этих команд на такт больше, чем для просто перехода). А как МК «узнает», когда именно нужно возвращаться? Для этого каждая процедура-подпрограмма оформляется специальным образом: не отличаясь поначалу ничем от любого другого участка программного кода, обозначенного меткой, в месте возврата она должна содержать специальную команду — ret (от return — «возврат»). По этой команде МК извлекает из стека сохраненное содержимое счетчика команд и продолжает выполнение прерванной основной программы.
Заметим, что стек — одно из самых употребительных понятий в программировании. Наличие программного стека позволяет, например, организовать привычное для языков высокого уровня разделение переменных на локальные и глобальные. Во всех последующих программах в этой книге мы будем пользоваться только глобальными переменными, и при нехватке регистров общего назначения для них мы будем использовать ячейки SRAM. (Привычку употреблять преимущественно глобальные переменные автор даже перенес в свой стиль программирования на Delphi, как вы увидите из главы 18.) Однако в больших проектах локальные переменные зачастую бывает просто необходимы (например, когда одна и та же процедура вызывается в разных местах с исходными данными, хранящимися в различных регистрах). Механизм организации локальных переменных с помощью стека приведен в листинге 13.2 (он в точности такой же, как это происходит при компиляции в «настоящих» языках программирования).
Листинг 13.2:
push var_1 ;переменная var_1 в программе помещается в стек
push var_2 ;переменная var_2 в программе помещается в стек
rcall procedure ;вызывается процедура
pop var_2 ;после нее результат извлекается из стека
pop var_1 ;второй результат извлекается из стека
…
procedure: ;в процедуре извлекается локальная
pop var_loc2 ;переменная var_loc2 со значением var_2
pop var_loc1 ;и переменная var_loc1 со значением var_1
… ;расчеты, расчеты…
push var_loc1 ;результат — в стек
push var_loc2 ;результат — в стек
ret ;возврат из процедуры
В качестве переменных var_1 и var_2 возможны любые регистры, при этом действия внутри процедуры всегда будут совершаться с заданными регистрами var_loc. Обратите внимание, что когда таких переменных несколько, важно соблюдать правильный порядок их помещения в стек и извлечения оттуда, согласно принципу «первым вошел — последним вышел» (в программах на языках высокого уровня за порядком переменных в стеке следят специальные форматы вызова функций типа stdcall и подобные).
Аналогично происходит обработка прерываний, только специальной команды, как вы знаете, там нет, вызов производится обычным переходом rjmp или jmp, но поскольку он осуществляется с определенного адреса (там, где стоит вектор прерывания), то контроллер делает то же самое: сохраняет в стеке адрес командного счетчика, на котором выполнение основной программы было грубо нарушено, начинает выполнять прерывание и ожидает команды возврата, здесь она записывается как reti (return interrupt).
Еще одна важнейшая группа команд ветвления программы — команды перехода по состоянию отдельного бита в указанном регистре (sbrs, sbic и т. п.). Они очень удобны для организации процедур, аналогичных оператору выбора CASE в языках высокого уровня, но, к сожалению, обладают непривычной логикой: «пропустить следующую команду, если условие выполняется» и для новичка могут показаться слишком заумными. В качестве примера приведу довольно сложную по логике работы, но характерную для микроконтроллерной техники процедуру, в которой задача формулируется так: при наступлении некоторого условия мигать попеременно зеленым и красным светодиодами (СД).
Предположим, что условие мигания задается состоянием бита 3 в некоем рабочем регистре, который назовем регистром флагов — Flag (не путать с «официальным» регистром флагов SREG, о котором далее). Если бит 3 регистра Flag равен единице (установлен), надо мигать, если нет (сброшен) — оба СД погашены.
Красный СД подсоединен к выходу порта В номер 5, а зеленый — к выходу порта С номер 7 (разумеется, это могут быть любые другие выводы других портов).
Текущее состояние СД задается битом 4 в том же регистре флагов Flag.
Алгоритм работы такой программы на языке Pascal (листинг 13.3) описывается типичным вложенным оператором выбора.
Листинг 13.3
case <бит 3 per. Flag> of
0: <погасить оба СД>
1: case <бит 4 per. Flag> of
1: <устанавливаем Port С, вых. 7> ; //горим зеленым <сбрасываем
Port В, вых. 5>; //гасим красный
<сбрасываем бит 4 Flag>; { следующий раз горим красным }
0: устанавливаем Port В, вых. 5>; //горим красным
<сбрасываем Port С, вых. 7>; //гасим зеленый
<устанавливаем бит 4 Flag>; // следующий раз горим зеленым
end;
end;
Разобравшийся в алгоритме читатель уже, несомненно, задает вопрос — а как обеспечить цикличность? Для этого подобный код включают в обработчик события по таймеру с секундным, например, интервалом. Причем, что характерно, и в микроконтроллере, и в операционной системе Windows это происходит абсолютно одинаково: инициализируется системный таймер (в МК для этого надо разрешить соответствующее прерывание), задается интервал его срабатывания (в Windows это одна команда, в МК их несколько больше) и — вперед! Но с таймерами мы будем разбираться далее по ходу дела, а пока посмотрим, как тот же алгоритм реализовывается в МК (листинг 13.4).
Листинг 13.4
sbrs Flag,3 ;если флаг 3 стоит, будем мигать
rjmp dark ;иначе будем гасить
sbrs Flag,4 ;если флаг 4 стоит, будем гореть зеленым
rjmp set4 ;иначе красным
cbr Flag, 0Ь00010000 ;следующий раз горим красным
cbi PortB,5 ;гасим зеленый
sbi PortC,7 ;горим зеленым
rjmp continue ;все готово
set4: ;если флаг 4 не стоит, будем гореть красным
sbr Flag, 0Ь00010000 ;следующий раз горим зеленым
cbi PortC,7 ;гасим красный
sbi PortB,5 ;горим красным
rjmp continue ;все готово
dark:
cbi PortC,7 ;гасим оба
cbi PortB,5
continue:
…
С используемыми здесь командами установки и сброса отдельных бит (sbi, sbr и т. п.) мы плотнее познакомимся чуть позже, а сейчас задержимся на ключевой команде всего алгоритма — sbrs, что расшифровывается, как Skip if Bit in Register is Set («пропустить, если бит в регистре установлен»). Имеется в виду, что по состоянию бита (если он установлен в единицу) пропустить нужно следующую команду. В качестве последней обычно также выступает одна из команд ветвления, как здесь, но далеко не всегда. Удобно, например, организовывать выход из прерывания или подпрограммы по какому-то условию, если поставить следующей после sbrs команду reti или, соответственно, ret (примеры мы еще встретим).
Противоположная по логике процедура записывается, как sbrc (Skip if Bit in Register is Cleared — «пропустить, если бит в регистре очищен», т. е. равен нулю). Наконец, есть еще пара аналогичных команд — sbis и sbic, которые применяются, когда нужно отследить состояние бита в регистре ввода/вывода (I/O), а не в регистре общего назначения. Все эти команды мы будем активно применять в дальнейшем.
Наконец, в самой обширной группе команд ветвления имена начинаются с букв Ьг (от слова branch — «ветка»). Это команды условного перехода, которые считаются одними из самых главных в любой системе программирования, т. к. позволяют организовывать циклы — базовое понятие, программистских наук. По смыслу все эти команды сводятся к банальному if… then («если… то»). Мы будем пользоваться лишь частью этих команд, потому что они во многом взаимозаменяемы, и здесь подробно разберем только одну пару команд. Смысл остальных вам будет понятен по ходу дела.
Это наиболее часто употребляемая пара brne (Branch if Not Equal, «перейти, если не равно») и breq (Branch if Equal, «перейти, если равно»). Уже из смысла этих команд понятно, как они пишутся: после команды следует метка, на которую нужно перейти. Вопрос только такой: откуда здесь берется собственно условие? Для этого все команды ветвления обязательно употребляют в паре с одной из команд, устанавливающих флаг нуля Z в регистре флагов SREG. Обычно (и это наиболее наглядно) для этой цели служит команда ср (от compare— «сравнить»), которая сравнивает регистры, или cpi («сравнить с непосредственным значением»). Например, вот так будет вы глядеть простейший цикл, в котором переменная temp последовательно принимает значения от 1 до 10:
clr temp ;обнулить temp
back_loop:
inc temp ;увеличиваем temp на 1
<что-то делаем, необязательно с помощью temp >
cpi temp,10
brne back_loop
Обратите внимание: если надо, чтобы temp начинала с нулевого значения, то фрагмент «что-то делаем» следует вставить до команды inc, но тогда последним рабочим значением temp в цикле будет 9, а не 10, а после выхода из процедуры — все равно 10.
Другой нюанс заключается в том, что удобная команда сравнения с непосредственным числом cpi работает только для регистров с номерами от 16 до 31 (как и многие другие команды работы с непосредственными значениями, например, ldi). По этой причине рабочие переменные (temp, счетчики) всегда желательно выбирать из этой половины регистрового файла (в примерах в этой книге, как и в «аппнотах», кстати, обычно temp — это r16, хотя и не всегда). Положение осложняется тем, что регистры из старшей половины наиболее дефицитны: последние шесть из них объединены в пары для работы с памятью (см. далее) и некоторых других операций, r24 и r25 задействованы в команде adiw и т. п. Если переменных не хватает, то регистр из первой половины регистрового файла (допустим, это r15) в аналогичном цикле приходится использовать с парой команд:
ldi temp,10
ср r15,temp
Иногда проще построить декрементный цикл, в котором переменная уменьшается от заданного значения до нуля:
ldi temp,10 ;загружаем 10 в temp
back_loop:
dec temp ;уменьшаем temp на 1
<что-то делаем с помощью temp >
brne back_loop
Как видите, здесь вообще ничего сравнивать не требуется, потому что команда dec при достижении нуля сама установит флаг Z (то же относится к команде tst temp, которая эквивалентна команде cpi temp, 0). И если даже выбрать регистр из первой половины, то лишняя команда понадобится не в каждом цикле, а только один раз для загрузки предварительного значения:
ldi temp,10 ;загружаем 10 в temp
mov r15,temp ;загружаем 10 в r15 и далее его используем
;в команде dec
Один важный нюанс в работе всех команд перехода заключается в том, что они могут занимать непредсказуемое количество циклов (1 или 2), в зависимости от того, выполняется условие или нет. В AVR реализован конвейер команд, который «тупо» полагает, что следующей будет выполняться команда сразу после команды перехода. Естественно, если ветвление необходимо (в большинстве случаев), конвейер останавливается на один лишний такт, в течение которого происходит выборка адреса перехода. Однако этот недостаток с лихвой компенсируется тем, что за счет конвейера почти все остальные команды выполняются за один такт— недостижимая мечта для многих других типов контроллеров (например, в популярном до сих пор семействе 8051, выпущенном еще в начале 80-х, команда выполняется как минимум за 12 тактов, хотя некоторые современные клоны этого семейства могут работать и быстрее).
Арифметика и логика в интерпретации AVR
Арифметические операции для AVR на первый взгляд могут показаться реализованными довольно странно для пользователя, привыкшего к бытовому представлению об арифметике, но на самом деле получается очень стройная система.
Не вызывают никаких возражений только очевидные операции: add R1,R2 (сложить два регистра, записать результат в первый) и sub ri,r2 (вычесть второй из первого, записать результат в первый). Но если вдуматься, то вопросов возникает множество: а что будет, если сумма превышает 255? Или разность меньше нуля? Куда девать остатки? Оказывается, все продумано: для учета переноса есть специальные команды adc и sbc соответственно. Корректная операция сложения двух 16-разрядных чисел будет занимать две команды:
add RL1,RL2
adc RH1,RH2
Здесь RL1 и RL2 содержат младшие (low) байты слагаемых, a RH1 и RH2 — старшие (high). Если при первой операции результат превысит 255, то перенос запишется в специальный флаг переноса (в регистре флагов sreg обозначается буквой С) и учтется при второй операции. Аналогично выглядит операция вычитания.
Постойте, но мы же вовсе не хотели складывать 16-разрядные числа! Мы хотели всего лишь сделать так, чтобы в результате сложения 8-разрядных чисел получился правильный результат, пусть он займет два байта. На что нам тогда старший байт второго слагаемого, если его вообще в природе не существует? Конечно, можно сделать его фиктивным, загрузив в некий регистр нулевое значение, но это только кажется, что регистров у AVR много (аж 32 штуки), на самом деле они довольно быстро расходуются под переменные и разные другие надобности, и занимать целый регистр под фиктивную операцию, пусть даже на один раз, как-то некрасиво. Потому «экономная» операция сложения 8-разрядных чисел будет выглядеть таким образом:
add RL1,R2
brcc add_8
inc RH1
add_8:
…
Исходные слагаемые находятся в R1 и R2, а результат будет в RH1:RH2. Отметим, что в старшем разряде (RH1) в результате может оказаться только либо 0, либо 1, т. к. сумма двух восьмиразрядных чисел не может превысить число 510 (255 + 255), именно потому флаг переноса С представляет собой один-единственный бит в регистре флагов. В этой процедуре команда brcc представляет собой операцию условного перехода на метку add_8 по условию, что флаг переноса равен нулю (BRanch if Carry Cleared). Таким образом, если флаг не равен нулю, выполнится операция увеличения значения регистра RH1 на единицу inc RH1, в противном случае она будет пропущена.
Внимательный читатель, несомненно, уже заметил ошибку в программе: а чему равно значение RH1 до выполнения нашей процедуры? Вдруг оно совсем не ноль, и тогда нельзя говорить о корректном результате. Поэтому правильней было бы дополнить нашу процедуру еще одним оператором, который расположить раньше всех остальных: cir RH1 (т. е. очистить RH1).
Заметим, что во всех случаях процедура разрастается во времени: было две команды, стало четыре, причем тут имеется еще и неявное замедление, поскольку все представленные арифметические команды выполняются за один такт, а команда ветвления brcc может занимать такт (если С = 1), а может и два (если С = 0). Итого мы выиграли один регистр, а потеряли два или три такта. И так всегда: есть процедуры, оптимизированные по времени, есть — по количеству команд, а могут быть и по количеству занимаемых регистров. Идеала, как и везде в жизни, тут не добиться, приходится идти на компромисс.
Если вы посмотрите таблицу команд в Приложении 4, то можете обратить внимание, что из восьмибитовых арифметических операций с константой доступно только вычитание (SUBI, SUCI), напрашивающейся операции сложения с константой нет. Это не упущение автора при формировании выборочной таблицы, а действительная особенность системы команд AVR. Это можно обойти при желании вычитанием отрицательного числа, но на практике не очень-то требуется, потому что разработчики семейства AVR решили облегчить жизнь пользователям, добавив в перечень арифметических команд две очень удобные команды adiw и sbiw, которые, по сути, делают то же самое, что пары команд add/adc (sub/sbc), только за одну операцию, и притом с константой, а не с регистром. Единственный их недостаток— работают они только с определенными парами регистров: с четырьмя парами, начиная с r25-r24. Три старших пары (r26-27, r28-29 и r30-31) носят еще название X, Y и Z, и мы их будем «проходить» далее в этой главе, они задействованы в операциях обмена данными с SRAM. Но, к счастью, точно так же работает и пара r24-r25, которая более нигде не употребляется в объединенном качестве, и это очень удобно. Независимо от используемой пары, старшим считается регистр с большим номером, а операцию нужно проводить с младшим, при этом перенос учтется автоматически. Например, в результате выполнения последовательности команд
clr r25
ldi r24,100
adiw r24,200
в регистрах r25:r24 окажется записано число 300 (в r25 будет записано $01, что эквивалентно 256 для 16-разрядного числа, а в r24 окажется $2С = 44). Аналогично работает и процедура вычитания константы sbiw.
С арифметикой многоразрядных чисел мы познакомимся в главе 15, там же попробуем освоить умножение и деление. Заметим, что все эти операции корректно работают с числами со знаком (см. главу 7), но вы удивитесь, узнав, насколько редко такая задача возникает на практике. А с дробными числами (с «плавающей запятой») AVR работать напрямую не «умеют», и тут приходится хитрить, о чем мы подробнее поговорим в главе 15.
Кроме собственно арифметических, к этой группе команд еще иногда относят некоторые логические операции, например логический сдвиг, хотя чаще их помещают в группу инструкций для операций с битами. Самая простая такая операция — сдвиг всех разрядов регистра влево (lsl) или вправо (lsr) на одну позицию. Сдвиги приходится применять довольно часто, потому что, как вы знаете из главы 7, такая операция равносильна умножению (соответственно, делению) на 2. Для того чтобы разряды не терялись при сдвиге, предусмотрены разновидности этих операций: rol и rоr. Они учитывают все тот же флаг переноса С, и его можно перенести в другой регистр. Например, в результате выполнения последовательности команд
lsl R1
rol R2
регистр R1 будет умножен на 2, а старший его разряд (неважно, ноль он или единица) окажется в младшем разряде R2. Причем обратите внимание, что R2 при этом также умножается на 2, и, следовательно, его младший разряд без учета переноса всегда окажется равным нулю (четные числа в двоичной системе оканчиваются на 0).
Здесь же имеет смысл рассмотреть инструкции установки отдельных битов. Команды, устанавливающие значения битов в регистрах общего назначения (sbr и cbr) иногда относят к группе арифметических операций, а команды, устанавливающие биты в регистрах ввода/вывода (sbi и cbi) — к группе битовых операций. Правда, в некоторых пособиях их относят к одной группе операций с битами, что, конечно, логичней. Но почему такой разнобой, если они, по сути, делают одно и то же?
Механизм работы этих команд существенно различается. Очевиднее всего устроены sbi и cbi: так, уже знакомая нам команда sbi PortB,5 установит в единичное состояние разряд номер 5 порта В. Если этот разряд порта сконфигурирован на выход, то единица появится непосредственно на соответствующем выводе микросхемы (например, для микросхемы ATmega8 это вывод 19, для 8515 или 8535 любого семейства это вывод 6 и т. п.), если на вход, то эта операция управляет подключением «подтягивающего» резистора.
Гораздо сложнее устроены команды установки бит в регистрах общего назначения. Взгляните на фрагмент ранее приведенного кода, где вы встретите команду sbr Flag, 0b00010000, устанавливающую бит номер 4 в единицу. Почему так сложно, да еще и в двоичной системе? Дело в том, что команды эти на самом деле не устанавливают никаких битов, а просто осуществляют некую логическую операцию между значением регистра и заданным байтом, который в этом случае называют битовой маской. Указанная команда расшифровывается как Flag OR ob00010000. Если вы вернетесь к главе 7, то сообразите, что при такой операции четвертый бит (нумерация начинается с нуля) установится в единицу, какое бы значение он ни имел ранее, а все остальные останутся в старом состоянии. Понятно также, почему я предпочел использовать двоичное представление маски: так легче отсчитывать биты. Так, разумеется, можно за один раз установить хоть все биты в регистре — в этом отличие команды sbr от sbi, которая устанавливает только по одному биту.
Аналогично работает команда сброса бита cbr Flag, 0b000010000. Только для того, чтобы ее можно было записывать в точности в том же виде, что и sbr, логическая операция, которую она осуществляет, сложнее: Flag AND (NOT 0b000100000). Здесь сначала в маске инвертируются все биты (реально это производится вычитанием ее из числа $FF, т. е. нахождением дополнения до 1), а затем полученное число и значение регистра комбинируются операцией логического умножения (а не сложения, как в предыдущем случае). В результате четвертый бит обнулится обязательно, а все остальные останутся в неприкосновенности. Кстати, обнулять все биты некоего регистра R удобнее и нагляднее не командой cbr R, $FF, а с помощью инструкций clr R или ldi R,0.
Призываю вас об этой разнице между регистрами общего назначения и регистрами ввода/вывода не забывать. Я и сам до сих пор «попадаюсь» на том, что в случае необходимости обнуления бита 1 в рабочем регистре temp записываю cbr temp, 1 (аналогично верной команде cbi portB,1), хотя такая операция обнулит не первый, а нулевой бит в temp. А операция cbr R, 0 (как и операция sbr R, 0) вообще ничего не делает, и такая запись бессмысленна.
Команды переноса данных
Последнее, что мы рассмотрим в этом разделе — команды, которые употребляются для переноса данных из одной области памяти в другую (здесь память рассматривается в широком смысле этого слова, и в нее включаются также и регистры). Некоторые команды из этой группы нам уже знакомы — это ldi и mov. Первая загружает в регистр непосредственное число (но действует только для регистров, начиная с r16), а вторая — значение другого регистра. Отметим еще, что для ldi очень часто применяется форма записи, которую можно пояснить следующим практическим примером:
ldi temp, (1<<RXEN|1<<TXEN|1<<RXB8|1<<ТХВ8)
Смысл этого выражения в том, что мы устанавливаем в единицы для регистра temp только биты с указанными именами (естественно, последние должны быть где-то определены, в данном случае это сделано в inc-файлах). Обратите внимание, что в отличие от команды sbi, остальные биты будут одновременно обнулены, а не останутся при своих значениях. Такая форма очень удобна для задания поименованных битов в процессе инициализации служебных регистров (при использовании «законной» команды sbi нужные биты пришлось бы указывать в числах, и предварительно обнулять регистр). В данном случае мы хотим инициализировать последовательный порт UART, и приведенная форма записи означает, что устанавливается разрешение приема (RXEN) и передачи (TXEN), причем и для того и для другого устанавливается 8-битный режим (RXB8 и TXB8). Подробнее мы в этом разберемся в главе 16, когда будем «проходить» программирование UART, а пока заметим, что еще. не все доделали: установили биты в регистре temp, но он-то какое отношение имеет к последовательному порту?
Потому следующей по тексту программы командой мы обязаны перенести установленное значение в соответствующий регистр ввода/вывода, для которого, собственно, поименованные биты и имеют значение, называющийся UCR. Это делается традиционной для всех ассемблеров командой out:
out UCR,temp
Замечу, что указанные команды по отношению к UART справедливы для семейства Classic, для Mega это делается несколько сложнее (a Tuny, кроме модели 2313, вообще UART не имеют), но сейчас это неважно. В паре к команде out идет симметричная команда in (чтения данных из I/О-регистра).
Заметки на полях
Здесь уместно обратить внимание читателей на то, что в момент подобного рода установок надо тщательно следить за тем, чтобы значение рабочего регистра temp не могло измениться между командами его установки и переносом значения в I/O-регистр. А как это может случиться, спросите вы? Да запросто, — если разрешены прерывания, в обработчиках которых наверняка также будет присутствовать temp , то они могут «вклиниться» между командами. Пусть вероятность такого события крайне мала, и оно может не происходить годами, но программист обязан об этом помнить, и грамотно составленная программа во время таких установок прерывания запрещает. В секции RESET это обеспечивается тем, что по умолчанию прерывания запрещены, а команда на их общее разрешение ( sei ) ставится после всех установок. Куда меньше проблем вызывают сами обработчики, т. к. по умолчанию во время обработки прерывания другое, произошедшее позднее, ожидает своей очереди (автоматически прерывания разрешатся только на выходе из текущего обработчика по команде reti ). Но если вам зачем-то нужно разрешить критичным прерываниям «вклиниваться» во время обработки некритичных (для этого следует явно разрешить прерывания внутри обработчика данного прерывания командой sei ), то такая проблема снова возникает. Именно поэтому, в частности, не рекомендуется употреблять синонимы для имен рабочих регистров: слишком легко забыть, что temp и counter , к примеру, указывают на один регистр, и в разных процедурах, вложенных одна в другую, они будут произвольно менять свое значение. Если же все-таки запретить прерывания нельзя или не хочется (например, во время выполнения длинного цикла), то уберечься от ошибок можно, если в начале обработчика прерывания сохранять критичные регистры в стеке командой push , а в конце — извлекать их командой pop . Но тут также возникает немало возможностей наделать трудновылавливаемые ошибки в программе, поскольку отследить содержимое стека тоже не всегда просто.
Следующими по важности командами переноса данных будут команды загрузки и чтения SRAM— id и st. С этими командами связаны такие «жуткие» понятия, как «прямая» и «косвенная» адресации, а также «относительная косвенная адресация» и прочая подобная «лабуда» из арсенала разработчиков микросхем и теоретиков программирования. Эти понятия на практике абсолютно не требуются, и только затуманивают мозги, потому что зачастую относятся к совершенно разным командам и узлам кристалла (это единственный раздел в описаниях МК, который спокойно можно пропускать при чтении, все остальные изучать очень полезно). Мы постараемся от термина «адресация» вообще отказаться и разберем здесь три основных режима чтения/записи SRAM: простой, а также с преддекрементом и постинкрементом. Все три употребляются очень часто, хотя два последних режима работают не во всех типах AVR.
Во всех случаях в чтении и записи SRAM используются регистры X, Y и Z — т. е. пары r27:r26, r29:r28 и r31:r30 соответственно, которые по отдельности еще именуют ХН, XL, YH, YL, ZH, ZL в том же порядке (т. е. старшим в каждой паре служит регистр с большим номером). Если обмен данными производится между памятью и другим регистром общего назначения, то достаточно только одной из этих пар (любой), если же между областями памяти — целесообразно задействовать две. Независимо от того, какую из пар мы используем, чтение и запись происходят по идентичным схемам, меняются только имена регистров.
Покажем основной порядок действий при чтении из памяти для регистра Z (r31:r30). Чтение одной ячейки с заданным адресом Address, коррекция ее значения и последующая запись выполняются так:
ldi ZH,High(Address) ;старший байт адреса RAM
ldi ZL,Low(Address) ;младший байт адреса RAM
ld temp,Z ;читаем ячейку в temp
inc temp ;например, увеличиваем значение на 1
st Z,temp ;и снова записываем
Заметки на полях
При всех подобных манипуляциях нужно внимательно следить за двумя вещами: во-первых, за тем, чтобы не залезть в несуществующую область памяти (если объем SRAM составляет 512 байт, как в большинстве моделей, которые мы будем использовать, то ZH в данном примере может иметь значения только 0 или 1). Во-вторых, не забыть, что младшие адреса SRAM заняты регистрами (в большинстве моделей под это зарезервированы первые 96 адресов от $00 до $5F). И запись, например, по адресу $0000 (ZH=0, ZL=0) равносильна записи в регистр r0 . Во избежание коллизий я по возможности поступаю следующим образом: резервирую пару ZH, ZL только под запись/чтение в память, и устанавливаю с самого начала регистр ZH в единицу. Тогда при любом значении ZL мы будем «шарить» только в старших 256 байтах из 512, чего для любых практических нужд обычно достаточно (а если недостаточно, то, скорее всего, все равно придется задействовать внешнюю память), а случайно пересечься с регистрами нет никакой возможности. Обязательно нужно также помнить, что и последние адреса памяти также нельзя занимать: мы сами дали в начале программы команду задействовать их под стек.
Режимы с преддекрементом и постинкрементом удобны, когда нужно прочесть или записать в память целый фрагмент (эти команды недействительны для большинства МК семейства Tuny). Схема действий аналогичная, только при этом команды выглядят так:
st -Z,temp ;с преддекрементом, запись в ячейку с адресом Z-1, после выполнения команды Z = Z-1
st Z+,temp ;с постинкрементом, запись в ячейку с адресом Z, после выполнения команды Z = Z+1
Аналогично выглядят команды чтения:
ld temp, -Z ;с преддекрементом, чтение из ячейки с адресом Z-1, после выполнения команды Z = Z-1
ld temp,Z+ ;с постинкрементом, чтение из ячейки с адресом Z, после выполнения команды Z = Z+1
Вот как можно в цикле записать 16 ячеек памяти подряд одним и тем же значением из temp, начиная с нулевого адреса старших 256 байтов памяти:
ldi ZH,1
clr ZL
LoopW:
st Z+,temp ;сложили в память
cpi ZL,16 ; счетчик до 16
brne LoopW
Еще одна важная, хотя и нечасто употребляемая команда переноса данных — инструкция lpm, которая позволяет прочесть произвольный байт из памяти программ. Напомню, что архитектура у большинства разновидностей МК, в том числе и AVR, гарвардская, когда память программ отделена от памяти данных, и в память программ контроллер самостоятельно ничего писать не может (кроме случая самопрограммирования, но это экзотика). Потому хранение в памяти программ тех констант, которые никогда не будут изменяться, прямо рекомендуется разработчиками AVR, за много лет так и не сумевшими окончательно решить проблему безопасного хранения данных в EEPROM (о чем мы еще будем долго и много говорить).
Вот типичная задача такого рода: пусть контроллер осуществляет управление семисегментным индикатором в динамическом режиме, когда в каждом такте приходится выводить разные цифры. Выстраивать рисунки (битовые маски) этих цифр каждый раз замучаешься, и программа получится очень громоздкая, к тому же совершенно нечитаемая. Проще их «нарисовать» один раз и расположить по порядку (от 0 до 9) прямо в любом месте программы (удобно сразу после векторов прерываний). Дать понять компилятору, что это особая область памяти, которая его не касается, и должна быть перенесена без изменений, можно с помощью директивы db, а чтобы потом можно было найти эту область, ее следует пометить обычной меткой:
N_mask: ;маски цифр на семисегментном индикаторе
.db 0b000111111, 0b0000001110, 0b01011011, 0b01001111, 0b01100110, 0b01101101, 0b01111101, 0b00000111, 0b01111111, 0b01101111
Заметим, что таким образом можно формировать hex-файлы для предварительной записи констант в EEPROM, хотя мы будем пользоваться иным, более корректным методом (см. главу 15). Теперь можно в нужном месте использовать команду lpm следующим хитрым образом:
ldi ZH,High(N_mask*2) ;загружаем адрес начала маски
ldi ZL,Low(N_mask*2)
add ZL,5 ;адрес маски цифры «5»
lpm ;маска окажется в регистре r0
Надо сказать, что современные МК семейства Mega поддерживают и более простой формат команды lpm, аналогичный обычной id, но универсальности ради я привык к традиционному формату, который поддерживают все МК AVR. Здесь в регистр Z (и только Z!) заносится адрес начала массива констант, причем учитывается, что память программ имеет двухбайтную организацию, а в Z надо заносить побайтные адреса, отчего появляется множитель 2. По этому адресу, как мы договорились, располагается маска цифры «0». Для загрузки цифры «5» прибавляем к адресу это значение и вызываем команду lpm. Полученное значение маски окажется в самом первом регистре общего назначения го, так что при таких действиях его не следует занимать под какие-то переменные.
О Fuse-битах
Это «несчастье» свалилось на нашу голову с появлением семейств Tuny и Mega и привело к многочисленным проклятиям на голову фирмы Atmel со стороны армии любителей, которые стали один за другим «запарывать» кристаллы при программировании. Теперь уже все привыкли и обзавелись соответствующим софтом, а сначала было довольно трудно. Положение усугублялось тем, что в описании этих сущностей действовала извращенная логика: как мы знаем, любая чистая EEPROM (по принципу ее устройства) содержит единицы, и слово «запрограммированный» по отношению к такой ячейке означает, что в нее записали нули. Поэтому разработчики программатора AS-2 даже специально написали в окне программирования конфигурационных ячеек (такое название более правильное, чем fuse-бит, буквально означающее «предохранительный бит») памятку на этот счет (рис. 13.7).
Рис. 13.7. Окно типового состояния конфигурационных ячеек в нормальном режиме работы ATmega8535
На рис. 13.7 приведено безопасное рабочее состояние конфигурационных ячеек для ATmega8535, причем выпуклая кнопка означает единичное состояние ячейки, а нажатая — нулевое (и не путайтесь с этим самым «запрограммированным» состоянием!). Для разных моделей набор fuse-битов различный, но означают они одно и то же, потому мы разберем типовое их состояние на этом примере. Перед первым программированием нового кристалла просто один раз установите эти ячейки в нужное состояние.
Переписывать фирменные руководства я не буду, остановлюсь только на самых необходимых моментах. По умолчанию любая микросхема семейств Mega или Tuny запрограммирована на работу от внутренней RC-цепочки, за что разработчикам большое спасибо, иначе было бы невозможным первичное программирование по SPI, а только через параллельный программатор.
Для работы с обычным кварцем, присоединенным по типовой схеме, требуется установить все ячейки CKSEL0—3 в единицы, что согласно логике контроллера означает незапрограммированное их состояние. Это и ведет к критической ошибке. Решив при поверхностном чтении очень невнятно написанного, и к тому же по-английски, руководства, что установка всех единиц означает запрограммировать все ячейки, пользователь смело устанавливает их на самом деле в нули, отчего микросхема переходит в состояние работы от внешнего генератора и разбудить ее через SPI-интерфейс уже невозможно. Легче всего в этом случае переустановить fuse-биты с помощью параллельного программатора, либо за неимением такового, попробовать-таки подключить внешний генератор (его можно собрать по одной из схем из главы 9), как описано в руководстве.
Это самая крупная ошибка, которую можно допустить, но есть и помельче, правда, легче исправляемые. Ячейка SPIEN разрешает/запрещает последовательное программирование по SPI и должна оставаться в нулевом состоянии.
Ячейка S8535C (в других моделях она, соответственно, будет иметь другое название, или вовсе отсутствовать) очень важна, она определяет режим совместимости с семейством Classic (в данном случае с AT90S8535). Если ее установить в нулевое состояние, то МК семейства Mega (а также и единственный представитель Tuny — модель 2313) перейдет в режим совместимости, про все «навороты» можно забыть (кроме, конечно, самих конфигурационных ячеек), и без изменений использовать наработанные старые программы. В режиме совместимости следует учесть, что состояния МК нельзя перемешивать: если fuse-бит совместимости запрограммирован, то программа компилируется полностью, как для семейства Classic (в том числе с помощью соответствующего inc-файла), иначе она может не заработать. Например, AT90S8535 имеет 17 прерываний, a ATmega8535 — 21, и те же самые прерывания могут оказаться на других местах.
Еще одна важная ячейка — EESAVE, которая на рис. 13.7 установлена в единицу (режим по умолчанию), но ее целесообразно перевести в нулевое состояние, тогда при программировании памяти программ не будет стираться содержимое EEPROM. Ячейки SUT определяют длительность задержки сброса, и в большинстве случаев принципиального значения их состояние не имеет.
Наконец, для нас будет иметь значение состояние ячеек BODEN и BODLEVEL. Первая, будучи установлена в ноль, разрешает работу т. н. схемы BOD (Brown-out Detection), которая сбрасывает контроллер при снижении питания ниже допустимого порога. Ячейка BODLEVEL и определяет этот самый порог: при установленной в ноль ячейке он равен 4 В, при единице — 2,7 В. При питании 5 В надо выбирать первое значение, при 3,3 В — второе. Это предохраняет контроллер от «недопустимых операций» при выключении питания, но для обеспечения полной сохранности содержимого EEPROM таких мер оказывается недостаточно и приходится принимать дополнительные.
Ячейки, название которых начинается с BOOT, определяют режим начальной загрузки. Как я уже упоминал, в современных AVR можно изменять начальный адрес программы и расположение векторов прерываний. Эти ячейки, как и все остальные, следует оставить в исходном состоянии. В том числе это касается и битов защиты программы, которые на практике никакой защиты не дают, т. к. при необходимости легко обходятся. Зато неприятностей могут доставить массу, поскольку раз запрограммировав их, исправить что-то уже будет очень трудно, а для любителя почти невозможно.