Занимательная микроэлектроника — страница 70 из 117

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-биты с помощью параллельного программатора, либо за неимением такового, попробовать-таки подключить внешний генератор (его