• Порты микроконтроллера AVR. Учебный курс

    29.01.2021

    Бит

    Чтение/Запись

    Исходное значение

    · Бит 7 – Разрешение всех прерываний. Для разрешения прерываний этот бит должен быть установлен в состояние 1. Управление разрешением конкретного прерывания выполняется регистром маски прерывания EIMSK и TIMSK. Если этот бит очищен (=0), то ни одно из прерываний не обрабатывается. Бит аппаратно очищается после возникновения прерывания и устанавливается для последующего разрешения прерывания командой RETI.
    · Бит 6 – Бит сохранения копии. Команды копирования бита BLD и BST используют этот бит как источник и приемник при операциях с битами. Командой BST бит регистра общего назначения копируется в бит T, командой BLD бит Т копируется в бит регистра общего назначения.
    · Бит 5 – Флаг полупереноса. Он указывает на перенос между тетрадами при выполнении ряда арифметических операций.
    · Бит 4 – Бит знака. Бит S имеет значение результата операции исключающее ИЛИ (N(+)V) над флагами отрицательного значения (N) и дополнения до двух флага переполнения (V).

    · Бит 3 – Дополнение до двух флага переполнения. Он поддерживает арифметику дополнения до двух.
    · Бит 2 – Флаг отрицательного значения. Этот флаг указывает на отрицательный результат ряда арифметических и логических операций.
    · Бит 1 – Флаг нулевого значения. Этот флаг указывает на нулевой результат ряда арифметических и логических операций.
    · Бит 0 – Флаг переноса. Этот флаг указывает на перенос при арифметических и логических операциях.

    Микроконтроллер AT90S8535 имеет 4 параллельных порта ввода/вывода A, B,C и D.
    Порт А является 8-разрядным двунаправленным портом. Взаимодействие с портом А осуществляется через три регистра в пространстве ввода/вывода памяти данных: регистр данных – PORTA, $1B($3B), регистр направления данных – DDRA, $1A($3A), регистр входных данных – PINA, $19($39). Регистр PINA обеспечивает только возможность чтения, а регистры PORTA и DDRA – возможность чтения и записи. Регистр PINA не является регистром в полном смысле этого слова. Обращение к нему обеспечивает чтение физического состояния каждого вывода порта. Порт А служит также для ввода аналоговых сигналов A/D.

    Регистр данных порта А – PORTA

    Бит

    Чтение/Запись

    Исходное значение

    Регистр направления данных порта А – DDRA

    Бит

    Чтение/Запись

    Исходное значение

    Регистр входных данных порта А – PINA

    Бит

    Чтение/Запись

    Исходное значение

    Порт В является 8-разрядным двунаправленным портом ввода/вывода. Также как и у порта А взаимодействие с портом В осуществляется через три регистра в пространстве ввода/вывода памяти данных: регистр данных – PORTB, $18($38), регистр направления данных – DDRB, $17($37) и регистр входных данных – PINB, $16($36). Регистр PINB обеспечивает возможность только чтения. Регистр PINB не является регистром в полном смысле этого слова. Обращение к нему обеспечивает чтение физического состояния каждого вывода порта. Выводы порта В могут выполнять альтернативные функции, указанные в табл. 2.1.

    Таблица 2.1. Альтернативные функции выводов порта В

    Вывод порта

    Альтернативная функция

    T0 – вход тактового сигнала таймера/счетчика 0

    T1 – вход тактового сигнала таймера/счетчика 1

    AIN0 – положительный вывод компаратора

    AIN1 – отрицательный вывод компаратора

    – вход выбора ведомого SPI

    MOSI – установка ведущий выход/ведомый вход SPI

    MISO – установка ведущий вход/ведомый выход SPI

    SCK – тактовый сигнал SPI

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

    Регистр данных порта B PORTB

    Бит

    Чтение/Запись

    Исходное значение

    Регистр направления данных порта B – DDRB

    Бит

    Чтение/Запись

    Исходное значение

    Регистр входных данных порта B – PINB

    Бит

    Чтение/Запись

    Исходное значение

    Порт С представляет собой 8-разрядный двунаправленный порт ввода/вывода. Также как у портов А и В взаимодействие с портом С осуществляется через три регистра в пространстве ввода/вывода памяти данных: регистр данных – PORTC, $15($35), регистр направления данных – DDRC, $14($34) и регистр входных данных – PINC, $13($33). Регистр PINC обеспечивает только возможность чтения, а регистры PORTC и DDRC – возможность чтения и записи. Регистр PINC не является регистром в полном смысле этого слова. Обращение к нему обеспечивает чтение физического состояния каждого вывода порта.
    У порта С только два вывода могут выполнять альтернативные функции: выводы PC6 и PC7 выполняют функции TOSC1 и TOSC2 таймера/счетчика 2.

    Регистр данных порта C PORTC

    Бит

    Чтение/Запись

    Исходное значение

    Регистр направления данных порта C – DDRC

    Бит

    Чтение/Запись

    Исходное значение

    Регистр входных данных порта C – PINC

    Бит

    Чтение/Запись

    Исходное значение

    Порт D является 8-разрядным двунаправленным портом ввода/вывода. Также как и у портов А, В и С взаимодействие с портом D осуществляется через три регистра в пространстве ввода/вывода памяти данных: регистр данных – PORTD, $12($32), регистр направления данных – DDRD, $11($31) и регистр входных данных – PIND, $10($30). Регистр PIND обеспечивает возможность чтения, а регистры PORTD и DDRD – возможность чтения и записи. Регистр PIND не является регистром в полном смысле этого слова. Обращение к нему обеспечивает чтение физического состояния каждого вывода порта.
    Выводы порта D могут выполнять альтернативные функции, указанные в табл. 2.2.

    Таблица 2.2. Альтернативные функции выводов порта D

    Вывод порта

    Альтернативная функция

    RxD – вход приемника UART

    TxD – выход передатчика UART

    INT0 – вход внешнего прерывания 0

    INT1 – вход внешнего прерывания 1

    OC1B – вывод сравнения выхода В таймера/счетчика 1

    OC1А – вывод сравнения выхода А таймера/счетчика 1

    ICP – вход триггера захвата таймера/счетчика 1

    OC2 – вывод сравнения выхода таймера/счетчика 2

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

    Регистр данных порта D PORTD

    Бит

    Чтение/Запись

    Исходное значение

    Регистр направления данных порта D DDRD

    Бит

    Чтение/Запись

    Исходное значение

    Регистр входных данных порта D PIND

    Бит

    Чтение/Запись

    Исходное значение

    Так как рассматриваемая работа первая, то для приобретения обучающимися навыков работы с лабораторным комплексом все обучающиеся сначала делают одинаковую работу. Со своих рабочих мест они вводят в ПЭВМ одну и ту же задачу вычитания из числа 5 числа 3, приведённую в п. 1.5.3.1. После компиляции программы она записывается в микроконтроллер рабочего места и демонстрируется её работа преподавателю.
    После такого знакомства с комплексом обучающийся приступает к выполнению индивидуального задания. При наличии времени преподаватель может усложнить индивидуальное задание.

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

    Наша первая программа будет управлять, по началу, одним из выводов микроконтроллера. Для того чтобы удостоверится в том, что программа работает, к управляемому выводу через токоограничивающий резистор мы подключим светодиод, анод которого соединен с выводом МК, а катод с минусом (общим проводом).

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

    Порты ввода-вывода микроконтроллера

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

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

    Для работы микроконтроллера, впрочем, как и любой другой микросхемы, необходимо напряжение. Во избежание ложных срабатываний МК нужно питать только стабильным напряжением от 4,5 В до 5,5 В. Этот диапазон напряжений строго регламентирован и приведен в даташите.

    Плюс («+») источника питания подсоединяется в 7-й ножке, обозначенной VCC. Минус («-») – к 8-й или 22-й ножке, которые имеют обозначение GND (GND – сокращенно от ground – «земля»).

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

    Микроконтроллер ATmega8 имеет три порта ввода-вывода: B, C и D. Порты могут быть полными и неполными. Полный порт состоит из восьми бит и соответственно имеет столько же одноименных выводов. У неполного порта меньше 8 бит, поэтому число выводов такого порта также менее восьми.

    У данного МК порты B и D полные. А порт C неполный и имеет семь бит. Еще раз обращаю внимание, что нумерация битов начинается с нуля, например PB0, PB1, PB2…

    Не удивляйтесь, что у ATmega8 отсутствует порт A. Другие МК, имеющие большее число выводов, могут содержать как порт A, так и порт E. У микроконтроллеров с меньшим числом выводов может быть только один порт и тот неполный.

    Знакомство с Atmel Studio 7

    Теперь перейдем к написанию кода программы. Наша первая программа будет устанавливать + 5 В на нулевом бите порта C PC0, т.е. на 23-м выводе микроконтроллера.

    Запускаем Atmel Studio.

    Сначала необходимо создать проект. Для этого в открывшемся окне кликаем по вкладке New Project .

    Также проект можно создать, кликнув по вкладке File . В выпавшем меню следует выбрать New и далее Project . Или нажать комбинацию клавиш Ctrl+Shift+N .

    В появившемся окне выбираем язык программирования C/C++ и кликаем по вкладке.


    Далее нам нужно задать имя и место на диске для нашего проекта. Назовём наш первый проект именем 1 в строке для ввода Name . Изменить место расположения файла можно кликнув по кнопке Browse напротив строки Location . Если оставить галочку возле Create directory for solution , то в выбранном месте автоматически создастся папка с именем проекта. В данной папке помимо проекта будут созданы и другие вспомогательные файлы, поэтому я рекомендую не убирать галочку.

    После того, как имя проекта и его место выбраны, нажимаем кнопку OK . Снова появляется окно. В нем нам нужно выбрать тип микроконтроллера. В нашем случае – это ATmega8 . Кликаем по вкладке Device Family . В выпавшем меню выбираем серию микроконтроллеров ATmega .

    С помощью прокрутки находим и выделяем микроконтроллер ATmega8 и наживаем кнопку OK .

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

    Рассмотрим все по порядку.

    Atmel Studio 7 | Первая программа

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

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

    * 1.c

    * Created: 07.08.2017 16:57:59

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

    /* — начало комментария

    */ — конец комментария

    Весь текс, который помещен между /* и */ полностью пропускается компилятором.

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

    // Здесь пишется однострочный комментарий

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

    Директива препроцессора

    Следующим элементом программы является строка

    #include

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

    По сути, можно было бы и не подключать файл io.h , а набрать его содержимое вручную, однако это очень неудобно.

    Знак # означает, что данная команда – это директива препроцессора. Дело в том, что прежде чем скомпилировать файл компилятору необходимо выполнит предварительную его обработку. Поэтому сначала выполняется некая подготовка файла к компиляции путем добавления некоторых инструкций, прописанных в файле io.h

    io – название файла, которое происходит от сокращения input/output – ввод/вывод.

    H – расширение файла, название его происходит от слова header – заголовок.

    Теперь все вместе. io.h – это заголовочный файл, в котором записана информация о настройках ввода-вывода микроконтроллера.

    Главная функция main

    Ниже нам встречается следующая строка:

    int main (void )

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

    В синтаксисе языка Си идентификатором функции служат круглые скобки

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

    int – это целочисленный тип данных. В данном случае функция работает с целыми числами: как положительными, так и отрицательными. Существуют и другие типы данных, например с плавающей запятой, символьные и др. Более подробно мы будем рассматривать типы данных по мере необходимости или в отдельной статье. Однако для функции main рекомендуется всегда использовать тип данных int, поскольку конструкция int main (void ) определена стандартом языка Си и распознается любым компилятором.

    Область действия функции определяется фигурными скобками

    . → тело функции

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

    В общем случае любая функция имеет следующую конструкцию:

    тип данных имя функции (агрумент )

    тело функции (код)

    Функция while

    Внутри функции main находится функция while :

    while (1)

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

    Теперь, когда мы рассмотрели основные элементы конструкции программы, давайте посмотрим целиком на базовый шаблон. Без комментариев он имеет следующий вид:

    #include

    int main (void )

    while (1)

    Программирование портов ввода-вывода микроконтроллера ATmega8

    Сейчас мы уже можем дополнить программу нужным нам кодом. Первым делом необходимо настроить нулевой бит порта C PC0 на выход.

    Мы уже знаем, что МК может, как принимать, так и выдавать сигнал, т.е. выводы (порты) его могут работать как входы и как выходы . Поэтому предварительно нужно настроить вывод МК на соответствующий режим. Для этого в микроконтроллере есть специальный регистр, который называется DDR – d irect d ata r egister – регистр направления данных.

    У каждого порта есть свой такой регистр. Например, регистр порта C называется DDRC , порта B – DDRB , порта D – DDRD .

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

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

    DDRC = 0b0000001;

    Данной командой в регистр DDRC записывается двоичное число равное десятичному 1. Префикс 0b идентифицирует данное число, как двоичное.

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

    Также можно записать в регистр шестнадцатеричное число:

    DDRC = 0x1;

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

    Давайте рассмотрим еще один пример. Допустим нам необходимо настроить нулевой, третий и седьмой биты порта B на выход, а остальные биты на вход. Для этого случая код имеет такой вид:

    DDRB = 0b10001001;

    Регистр микроконтроллера PORT

    После того, как мы настроили нулевой бит порта C PC0 на выход, нужно еще выполнить настройку, чтобы на данном выводе появилось напряжение +5 В. Для этого необходимо установить нулевой бит в регистре PORT . Если бит установлен в единицу , то на выводе будет +5 В (точнее говоря величина напряжения питания микроконтроллера, которая может находится в пределах 4,5…5,5 В для микроконтроллера ATmega8). Если бит установлен в ноль , — то на выводе будет напряжение, величина которого близка к нулю .

    Каждый порт имеет свой регистр: порт A – PORTA , порт B – PORTB , порт C – PORTC .

    И так, чтобы получить на выводе PC0 напряжение +5 В, необходимо записать такую команду:

    PORT = 0b0000001;

    Обратите внимание на то, что каждая команда заканчивается точкой с запятой.

    Таким образом, чтобы засветить светодиод, нам необходимы всего лишь две команды:

    DDRC = 0b0000001;

    PORTС = 0b0000001;

    Первой командой мы определяем вывод PC0 как вход, а второй устанавливаем на нем напряжение +5 В.

    Полный код программы выглядит так:

    #include

    int main (void )

    DDRC = 0b0000001;

    While (1)

    PORTC = 0b0000001;

    Здесь необходимо заметить следующее: команда DDRC = 0b0000001; выполнится всего один раз, а команда PORTC = 0b0000001; будет выполняться все время в цикле, поскольку она находится в теле функции while (1) . Но даже если мы вынесем команду за пределы функции и поместим ее после DDRC = 0b0000001; , светодиод и в этом случае будет светиться все время. Однако, разместив команду PORTC = 0b0000001 ; в теле while (1) , мы делаем явный акцент на том, что светодиод должен светится все время.

    Компиляция файла

    Теперь, когда код полностью готов, его нужно скомпилировать. Для этого необходимо нажать клавишу F7 или кликнуть по кнопке Build и в выпавшем меню выбрать Build Solution .

    Если ошибок в коде нет, то файл скомпилируется, а в нижней части экрана появится запись о том, что проект скомпилирована успешно: Build succeeded .

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

    Одним из самых важных аспектов программирования микроконтроллеров является работа с регистрами и портами. У микроконтроллеров серии AVR несколько регистров ввода/вывода и 32 рабочих регистра общего назначения. Программист не может непосредственно записать число в регистр ввода/вывода. Вместо этого он должен записать число в регистр общего назначения, а затем скопировать значение этого регистра в регистр ввода/вывода. Рабочие регистры обозначаются как R1, R2, ... , R31.

    Для упрощения написания программ очень удобно давать регистрам имена. Целесообразна давать имена, соответствующие хранимой информации. Например, если регистр R16 используется для хранения временной информации, то его можно назвать temp. Это делается следующим образом:

    Для того чтобы не "обзывать" регистры ввода/вывода и основные регистры микроконтроллер, достаточно в начале программы подключить заголовочный файл, соответствующий используемому микроконтроллеру. Т.е. не придётся давать имена регистрам портов, таймерам/счетчикам и т.п. Например, если программа предназначена для микроконтроллера AT90s8515:

    Include "8515def.inc"

    Для микроконтроллера AT90s1200 - самый первый микроконтроллер AVR, регистры ввод/вывода имеют номера от $0 до $3F (зависит от модели МК). Отдельно можно выделить регистры ввода/вывода PortB, PinB, PortD, PinD (они имеют буквенные обозначения после подключения 1200def.inc, а так их адреса $18, $16, $12, $10 - согласитесь очень непросто держать в голове цифровые константы, проще буквенные имена). У последних микроконтроллеров AVR портов намного больше, они называются A, B, C, D, E...

    Рассмотрим расположение выводов популярного микроконтроллера ATtiny2313. Ножки 2-9, 11 с именами PD0 - PD7 являются портом D, аналогично с портом B. Обратите внимание на то, что порт B - восьмибитный, а порт D - семибитный.

    Порты могут работать как входы и как выходы. Если порт работает как вход, то для того чтобы считать значения, необходимо обратиться к регистру PinB или PinD - смотря с какого порта производим считывание. Если на некоторых пинах высокие уровни, соответствующие лог. "1", то соответствующие биты в считанных значениях будут установлены в "1". Выводы способны выдерживать ток до 20 mA, но не стои забывать о суммарном токе всех ножек порта т.к. существуют ограничения. Если порт является выходом, то значения на линиях порта устанавливается путём записи соответствующего значения в регистр порта PortB или PortD. Для установления лог. "1" на выходе порта, следует установить соответствующий бит в регистре PortB или PortD.

    Самый важный момент работы с портом - это работа с регистром защелкой, отвечающей за работу линий порта на вход или на выход. Название этого регистра DDRx, где x - буква порта. Для того чтобы сделать ножки выходами, мы должны записать в соответствующие биты "1". Например, мы хотим сделать ножку PB7 порта B входом, а остальные ножки выходами, то для этого необходимо записать в регистр DDRB значение 0b01111111. Приставка 0b означает, что число записано в двоичном виде. При запуске регистры DDRx обнулены, т.е. все ножки являются входами. Рекомендуется неиспользуемые ножки в устройстве делать входами.

    Рассмотрим простую программку работающую с портом микроконтроллера:

    Include "8515def.inc" ; подключим файл с описаниями регистров.def temp =r16 rjmp RESET ; вектор перехода при сбросе RESET: ldi temp, 0b00000011 ; определим PC0 и PC1 как выходы out DDRC, temp ldi temp, 0b00000001 ; зажигаем светодиод на ножке PC0 out PORTC , temp in temp, PinC ; считываем уровни с порта C ... LOOP: ; основной цикл программы nop rjmp LOOP

    Вот ты читаешь сейчас это и думаешь — память, регистры, стек и прочее это хорошо. Но ведь это не пощупать, не увидеть. Разве что в симуляторе, но я и на дельфи с тем же условием могу накодить. Где мясо!!!

    В других курсах там, чуть ли не с первых строк, делают что то существенное — диодиком мигают и говорят, что это наш Hello World. А тут? Гыде???

    Да-да-да, я тебя понимаю. Более того, наверняка ты уже сбегал к конкурентам и помигал у них диодиком;)))) Ничего, простительно.

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

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

    Инструментарий
    Работа с портами, обычно, подразумевает работу с битами. Это поставить бит, сбросить бит, инвертировать бит. Да, конечно, в ассемблере есть удобные команды

    cbi/sbi, но работают они исключительно в малом адресном диапазоне (от 0 до 1F, поэтому давайте сначала напишем универсальные макросы, чтобы в будущем применять их и не парить мозг насчет адресного пространства.

    Макросы будут зваться:

    • SETB byte,bit,temp
    • CLRB byte,bit,temp
    • INVB byte,bit,temp,temp2

    Причем при работе с битами младших РВВ (0-1F адрес) то значение параметра TEMP можно и не указывать — он все равно подставляться не будет. За исключением команд инверсии — там промежуточные регистры полюбому нужны будут.

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

    • SETBM byte,bit
    • CLRBM byte,bit
    • INVBM byte,bit

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

    1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 < 0x20 SBI @0,@1 .else .if @0<0x40 PUSH R17 IN R17,@0 ORI R17,1<<@1 OUT @0,R17 POP R17 .else PUSH R17 LDS R17,@0 ORI R17,1<<@1 STS @0,R17 POP R17 .endif .endif .ENDM ;SET BIT with REG .MACRO SETB .if @0 < 0x20 ; Low IO SBI @0,@1 .else .if @0<0x40 ; High IO IN @2,@0 ORI @2,1<<@1 OUT @0,@2 .else ; Memory LDS @2,@0 ORI @2,1<<@1 STS @0,@2 .endif .endif .ENDM ;............................................................. ;Clear BIT with REG .MACRO CLRB .if @0 < 0x20 ; Low IO CBI @0,@1 .else .if @0<0x40 ; High IO IN @2,@0 ANDI @2,~(1<<@1) OUT @0,@2 .else ; Memory LDS @2,@0 ANDI @2,~(1<<@1) STS @0,@2 .endif .endif .ENDM ;Clear BIT with STACK .MACRO CLRBM .if @0 < 0x20 CBI @0,@1 .else .if @0<0x40 PUSH R17 IN R17,@0 ANDI R17,~(1<<@1) OUT @0,R17 POP R17 .else PUSH R17 LDS R17,@0 ANDI R17,~(1<<@1) STS @0,R17 POP R17 .endif .endif .ENDM ;............................................................. .MACRO INVB .if @0 < 0x40 IN @2,@0 LDI @3,1<<@1 EOR @3,@2 OUT @0,@3 .else LDS @2,@0 LDI @3,1<<@1 EOR @2,@3 STS @0,@2 .endif .ENDM .MACRO INVBM .if @0 < 0x40 PUSH R16 PUSH R17 IN R16,@0 LDI R17,1<<@1 EOR R17,R16 OUT @0,R17 POP R17 POP R16 .else PUSH R16 PUSH R17 LDS R16,@0 LDI R17,1<<@1 EOR R17,R16 STS @0,R17 POP R17 POP R16 .endif .ENDM ;= End macro.inc ========================================

    ;= Start macro.inc ======================================== ;SET BIT with stack .MACRO SETBM .if @0 < 0x20 SBI @0,@1 .else .if @0<0x40 PUSH R17 IN R17,@0 ORI R17,1<<@1 OUT @0,R17 POP R17 .else PUSH R17 LDS R17,@0 ORI R17,1<<@1 STS @0,R17 POP R17 .endif .endif .ENDM ;SET BIT with REG .MACRO SETB .if @0 < 0x20 ; Low IO SBI @0,@1 .else .if @0<0x40 ; High IO IN @2,@0 ORI @2,1<<@1 OUT @0,@2 .else ; Memory LDS @2,@0 ORI @2,1<<@1 STS @0,@2 .endif .endif .ENDM ;............................................................. ;Clear BIT with REG .MACRO CLRB .if @0 < 0x20 ; Low IO CBI @0,@1 .else .if @0<0x40 ; High IO IN @2,@0 ANDI @2,~(1<<@1) OUT @0,@2 .else ; Memory LDS @2,@0 ANDI @2,~(1<<@1) STS @0,@2 .endif .endif .ENDM ;Clear BIT with STACK .MACRO CLRBM .if @0 < 0x20 CBI @0,@1 .else .if @0<0x40 PUSH R17 IN R17,@0 ANDI R17,~(1<<@1) OUT @0,R17 POP R17 .else PUSH R17 LDS R17,@0 ANDI R17,~(1<<@1) STS @0,R17 POP R17 .endif .endif .ENDM ;............................................................. .MACRO INVB .if @0 < 0x40 IN @2,@0 LDI @3,1<<@1 EOR @3,@2 OUT @0,@3 .else LDS @2,@0 LDI @3,1<<@1 EOR @2,@3 STS @0,@2 .endif .ENDM .MACRO INVBM .if @0 < 0x40 PUSH R16 PUSH R17 IN R16,@0 LDI R17,1<<@1 EOR R17,R16 OUT @0,R17 POP R17 POP R16 .else PUSH R16 PUSH R17 LDS R16,@0 LDI R17,1<<@1 EOR R17,R16 STS @0,R17 POP R17 POP R16 .endif .ENDM ;= End macro.inc ========================================

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

    Но вернемся к коду,
    Мигнем уж светодиодиком то, наконец?

    Да не вопрос. На демоплате уже смонтированы светодиоды, почему бы их не заюзать? Они висят на выводах порта PD4,PD5, PD7. Надо только одеть джамперы.

    ; Internal Hardware Init ====================================== SETB DDRD,4,R16 ; DDRD.4 = 1 SETB DDRD,5,R16 ; DDRD.5 = 1 SETB DDRD,7,R16 ; DDRD.7 = 1 ; End Internal Hardware Init ===================================

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

    ; Main ========================================================= Main: SETB PORTD,4,R16 ; Зажгли LED1 SETB PORTD,7,R16 ; Зажгли LED3 JMP Main ; End Main =====================================================

    Компилиуем, можешь в трассировщике прогнать, сразу увидишь как меняются биты. Прошиваем… и после нажатия на RESET и выгрузки bootloader (если конечно у тебя ) увидишь такую картину:


    И для версии II


    Во! Тока это же скучно. Давай ка ими помигаем.

    Заменим всего лишь наши макрокоманды.

    ; Main ========================================================= Main: SETB PORTD,4,R16 ; Зажгли LED1 INVB PORTD,7,R16,R17 ; Инвертировали LED3 JMP Main ; End Main =====================================================

    Зажгли, прошили…

    А вот фиг — горят оба, но один чуть-чуть тусклей. На самом деле он мерцает, но очень очень быстро. Если ткнуть осциллографом в вывод PD7, то будет видно, что уровень меняется там с бешеной частотой:


    Что делать? Очевидно замедлить. Как? Самый простой способ, который практикуют в подавляющем большинстве обучалок и быстрых стартов — тупой задержкой. Т.е. получают код вида:

    ; Main ========================================================= Main: SETB PORTD,4,R16 ; Зажгли LED1 INVB PORTD,7,R16,R17 ; Инвертировали LED3 RCALL Delay JMP Main ; End Main ===================================================== ; Procedure ==================================================== .equ LowByte = 255 .equ MedByte = 255 .equ HighByte = 255 Delay: LDI R16,LowByte ; Грузим три байта LDI R17,MedByte ; Нашей выдержки LDI R18,HighByte loop: SUBI R16,1 ; Вычитаем 1 SBCI R17,0 ; Вычитаем только С SBCI R18,0 ; Вычитаем только С BRCC Loop ; Если нет переноса - переход RET ; End Procedure ================================================

    Прошили, запустили… О да, теперь мигание будет заметно.

    При параметрах 255.255.255 длительность выдержки на 8Мгц будет около 2.1 секунды. Можно увеличить разрядность задержки еще на несколько байт. Тогда можно хоть час зарядить.

    Но этот метод ущербен, сейчас покажу почему.

    Давай добавим кнопку. LED3 пусть мигает в инверсии. А мы сделаем так, что когда кнопка нажата у нас горит LED1, а когда отпущена горит LED2.

    В качестве кнопки возьмем тактовую A, подключим ее к порту PD6.


    Для версии II аналогично. Только кнопку возьмем с группы кнопок и соединим вывод PD6 контроллера с штырем COL1 на кнопочной панели.

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

    Проверка кнопки делается командой SBIC, но вначале ее надо инициализировать. Сделать DDR=0, PORT=1 — вход с подтяжкой.

    Добавляем в секцию инициализации (Internal Hardware Init) эти строчки:

    ; Main ========================================================= Main: SBIS PIND,6 ; Если кнопка нажата - переход RJMP BT_Push SETB PORTD,5 ; Зажгем LED2 CLRB PORTD,4 ; Погасим LED1 Next: INVB PORTD,7,R16,R17 ; Инвертировали LED3 RCALL Delay JMP Main BT_Push: SETB PORTD,4 ; Зажгем LED1 CLRB PORTD,5 ; Погасим LED2 RJMP Next ; End Main =====================================================

    Ну чо, работает. Кнопочка жмется — диодики меняются. Третий же бодро подмигивает. Но есть западло:

    Торомозит программка то! Я кнопочку нажал, а картинка не сменилась, нужно подождать, подержать… Почему? А это из-за нашего быдлокодинга.

    Помнишь я тебе говорил, что тупые задержки, в которых МК ничего не делает это адское зло? Вот! Теперь ты в этом убедился сам. Чтож, со злом надо бороться. Как? Ну эт я тоже уже говорил — делать непрерывный цикл с флажками. Хотя бы внести в нашу задержку полезную работу.

    Шарманка
    Сейчас я тебе покажу как можно сделать цифровую шарманку. Помнишь как она устроена?

    Там барабан с торчащими гвоздями и пружинки на разные тона. Гвозди вращаются, дергают пружинки — они звякают. Получается расколбасный музон. А что если нашу шарманку развернуть в ленту. Не правда ли гвозди похожи на единички? ;))))

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

    Совпало? Делаем «ДРЫНЬ!»

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

    Скажем, считает шарманка от 0 до 1000, а нам надо 10 раз мигнуть диодом. Не обязательно втыкать 10 обработчиков с разными значениями. Достаточно одного, но чтобы он ловил значение **10. Все, остальное нам не важно. И сработает он на 0010, 0110, 0210, 0310, 0410, 0510, 0610, 0710, 0810, 0910. Более частые интервалы также делятся как нам надо, достаточно влезть в другой разряд. Тут надо только не забыть отрезать нафиг старшие разряды, чтобы не мешались.

    Приступим. Вначале создадим наш счетчик в сегменте данных:

    LDS R16,CCNT LDS R17,CCNT+1 LDS R18,CCNT+2 LDS R19,CCNT+3

    Все, теперь в R16 самый младший байт нашего счетчика, а в R19 самый старший.

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

    По можно сделать так:

    LDI R20,1 ; Нам нужна единичка CLR R15 ; А еще нолик. ADD R16,R20 ; Прибавляем 1 если в регистре 255, то будет С ADC R17,R15 ; Прибавляем 0+С ADC R18,R15 ; Прибавляем 0+С ADC R19,R15 ; Прибавляем 0+С

    Пришлось потратить еще два регистра на хранение констант нашего сложения. Все от того, что AVR не умеет складывать регистры с непосредственным числом. Зато умеет вычитать.

    Я уже показывал, что R-(-1)=R+1, а ведь никто не запрещает нам этот же прием устроить и тут — сделать сложение через вычитание.

    1 2 3 4 SUBI R16,(-1) SBCI R17,(-1) SBCI R18,(-1) SBCI R19,(-1)

    SUBI R16,(-1) SBCI R17,(-1) SBCI R18,(-1) SBCI R19,(-1)

    Даст нам инкремент четырехбайтного числа R19:R18:R17:R16

    А теперь я вам покажу немного целочисленной магии
    Почему это будет работать? Смотри сам:

    SUBI R16,(-1) это, по факту R16 — 255 и почти при всех раскладах она нам даст нам заём из следующего разряда — С. А в регистре останется то число на которое больше.

    Т.е. смотри как работает эта математика, вспомним про число в доп кодах. Покажу на четырехрязрядном десятичном примере. У Нас есть ВСЕГО ЧЕТЫРЕ РАЗРЯДА, ни больше ни меньше. Отрицательное число это 0-1 так? Окей.

    1 С 0000-1=9999+C

    Т.е. мы как бы взяли как бы из пятиразрядной 1 С 0000 отняли 1, но разрядов то у нас всего четыре! Получили доп код 9999 и флаг заема С (сигнализирующий о том, что был заем)

    Т.е. в нашей целочисленной математике 9999=-1:) Проверить легко -1+1 = 0 Верно?

    9999+1 = 1 С 0000 Верно! :))) А 1 старшего разряда банально не влезла в разрядность и ушла в флаг переноса C, сигнализирующего еще и о переполнении.

    Оки, а теперь возьмем и сделаем R-(-1). Пусть R=4

    1 С 0004-9999 = 0005+С

    Вот так вот взяли и сложили через вычитание. Просто магия, да? ;)

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

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

    Флаг С не выскочит лишь когда у нас дотикает до 255 (9999 в десятичном примере), тогда будет 255-255 = 0 и вскочит лишь Z, но нам он не нужен.

    STS CCNT,R16 STS CCNT+1,R17 STS CCNT+2,R18 STS CCNT+3,R19

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

    ; Main ========================================================= Main: SETB PORTD,4 ; Зажгли LED1 INVB PORTD,7,R16,R17 ; Инвертировали LED3 Next: INCM CCNT JMP Main

    Запусти режим отладки и поставь точку останова (F9) на метку Main и загони курсор на первый брейкпоинт.

    0хB6(CCNT) 0х9F(CCNT+1) 0х04(CCNT+2) 0x00(CCNT+3)

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

    Как сравнивать? Да довольно просто. Все зависит от того что мы хотим получить. Если ОДНО событие за ВЕСЬ период глобального счетчика нашей шарманки, то тупо, побайтно. В этом случае у тебя диодик моргнет через секунду после старта, а дальше ты будешь ждать пол часа до переполнения всего четырехбайтного счетчика.

    Чтобы он моргал каждую секунду тебе надо при сравнении замаскировать старшие биты глобального счетчика, словно у него разрядность не 32 бита, а меньше (и переполняется он чаще).

    Младшие байты сравниваем как есть, а самый старший только до его максимального разряда, остальные надо отрезать.

    Т.е. самый старший разряд для этого случая это CCNT+2=0х04 если в двоичном представлении то 0х04 = 00000 100 так вот, счетчик у нас четырех разрядный, значит событие с маской

    00 04 9F B6 (00000000 00000 100 10011111 10110110)

    до переполнения возникнет дофига число раз. Видишь я нули жирным шрифтом выделил. Старший самый у мы вообще сравнивать не будем, а вот пред старший надо через AND по маске 00000111 продавить, чтобы отсечь старшие биты.

    Их еще надо заполнить до переполнения и обнуления счетчика. Но если мы их замаскируем то их дальнейшая судьба нас не волнует. Пусть хоть до второго пришествия тикает — нам плевать.

    LDS R16,CCNT ; Грузим числа в регистры LDS R17,CCNT+1 LDS R18,CCNT+2 ANDI R18,0x07 ; Накладываем маску CPI R16,0xB6 ; Сравниванем побайтно BRNE NoMatch CPI R17,0x9F BRNE NoMatch CPI R18,0x04 BRNE NoMatch ; Если совпало то делаем экшн Match: INVB PORTD,7,R16,R17 ; Инвертировали LED3 ; Не совпало - не делаем:) NoMatch: Next: INCM CCNT ; Проворачиваем шарманку JMP Main

    Во, загрузили теперь мигает. Никаких затупов нет, нигде ничего не подвисает, а главный цикл пролетает со свистом, только успевай барабан шарманки проворачивать:)

    Вот только мигает заметно медленней чем мы хотели. Не 1 секунда, а 8. Ну а что ты хотел — добавив процедуру сравнения мы удлиннили цикл еще на несколько команд. И теперь он выполняется не 25 тактов, а 36. Пересчитывай все циферки заново:)))))

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

    А если код будет еще больше, то ваще труба и погрешность накапливается с каждой итерацией!

    Зато, если добавить код обработки кнопок:

    ; Main ========================================================= Main: SBIS PIND,6 ; Если кнопка нажата - переход RJMP BT_Push SETB PORTD,5 ; Зажгем LED2 CLRB PORTD,4 ; Погасим LED1 Next: LDS R16,CCNT ; Грузим числа в регистры LDS R17,CCNT+1 LDS R18,CCNT+2 ANDI R18,0x07 CPI R16,0xB6 ; Сравниванем побайтно BRNE NoMatch CPI R17,0x9F BRNE NoMatch CPI R18,0x04 BRNE NoMatch ; Если совпало то делаем экшн Match: INVB PORTD,7,R16,R17 ; Инвертировали LED3 ; Не совпало - не делаем:) NoMatch: NOP INCM CCNT JMP Main BT_Push: SETB PORTD,4 ; Зажгем LED1 CLRB PORTD,5 ; Погасим LED2 RJMP Next ; End Main =====================================================

    То увидим, что от тормозов и следов не осталось. Кнопки моментально реагируют на нажатия, а диодик мигает сам по себе. Многозадачность! :)

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

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

    Можешь скачать и поглядеть , протрассировать его, чтобы увидеть как там все вертится.

    А для точных вычислений времени существуют таймеры. Но о них разговор отдельный.

    Похожие статьи