Одна из частых проблем начинающего робототехника — написание программ.
Вы можете, конечно, очень быстро изучить основы программирования, но только в одном случае, если вы проигнорируете такие аспекты, как использование заметок в каталоге, структурирование данных и, что естественно, саму работу микроконтроллера.
Мы не говорим о том, что быстрое обучение — это плохо. Напротив, это очень даже хорошо. Ведь благодаря быстрому обучению, вы сможете создать свою первую конструкцию и программу к ней, и тем самым понять, хотите ли вы заниматься робототехникой в дальнейшем. Но если вы точно знаете, что хотите связать свою жизнь с робототехникой и программированием, то вам точно стоит изучить определенный объем теоретического материала.
Вот почему мы решили написать статью, содержащую информацию об устройстве и работе микроконтроллера и обо всем, что с ним связано. Поскольку 8-битные процессоры Atmel из семейства AVR на сегодняшний день являются самыми популярными, мы на них и остановимся. Точнее на ATmega16.
Datasheet (таблицу данных) Atmega16 можно скачать с сайта производителя.
Упрощенная схема работы микроконтроллера
Чтобы микроконтроллер мог выполнять свою задачу, ему необходимо несколько основных компонентов. Самым важным из них является процессор (Central Processing Unit, сокращенно CPU). Он отвечает за выполнение написанной нами программы. Память, которая различается по емкости, скорости доступа и сохранению данных, также является важным аспектом.
Другими необходимыми элементами являются периферийные устройства. Чаще всего используются порты ввода-вывода. Связь между вышеупомянутыми элементами можно показать на простой схеме:
Как видите, путь слева ведет только в одну сторону — от процессора к целевому чипу. Это адресная шина. Справа находится шина данных, по которой данные могут передаваться в обоих направлениях.
Размер шины данных сильно влияет на скорость работы. |
В AVR он 8 битный поэтому, например, число int, равное 16 битам, необходимо транспортировать дважды. Персональные компьютеры обычно имеют 32-битную или 64-битную систему. Поэтому неудивительно, что ваш компьютер может выполнять те же задачи намного быстрее.
Связь между процессором и другими системами с помощью адресной шины и шины данных очень проста. Сначала процессор выбирает необходимый в данный момент чип через адресную шину, а затем байты перемещаются к процессору или от процессора по шине. Каждый чип имеет уникальный адрес, поэтому, когда используется одно устройство, остальные не используют шину данных. Также стоит упомянуть о системе тактовых сигналов. Она отвечает за генерацию импульсов постоянной частоты.
Тактовый сигнал достигает ЦП и периферийных устройств, и все операции выполняются в его части. Это второй важный фактор, влияющий на быстродействие микроконтроллера. |
Теперь перейдем к более подробному обсуждению отдельных компонентов.
Процессор (ЦП)
Процессор — это цифровая последовательная синхронизированная система. Слово цифровой означает, что процессор различает только низкое и высокое напряжение. Последовательный означает, что каждое новое состояние зависит от текущего входа и предыдущего состояния. А синхронизированный означает, что он работает в ритме тактового сигнала.
Задача процессора состоит в выполнении программы, сохраненной во FLASH-памяти. Программа разделена на команды, которые последовательно передаются и выполняются в CPU.
Центральный блок состоит из нескольких более мелких компонентов. 8-битные регистры предназначены для хранения данных, информации о состоянии, необходимых в данный момент.
К мелким компонентам относятся:
- Указатель инструкции (IP) — сохраняет в памяти адрес, с которого должна быть получена следующая команда.
- Регистр инструкций — хранит код текущей инструкции.
- Указатель стека (SP) — указывает на текущую вершину стека (о стеке будет рассказано позже).
- Регистр состояния — хранит флаги (управляющие биты), необходимые для работы программы. Большинство флагов предназначены для арифметических операций и используются только при программировании на ассемблере. Также есть глобальный флаг прерывания.
- Регистры общего назначения — в семействе AVR таких регистров 32, они называются R0, R1 и т.д. до R31. Это собственная оперативная память процессора. Операции с этими регистрами выполняются быстрее, чем с данными из ОЗУ или периферийных систем. Фактически, большинство операций состоит из чтения данных в одном из этих регистров, выполнения запрошенных операций и отправки их обратно по шине данных.
Часть процессора, отвечающая за выполнение вычислений, — это ALU (Arithmetic Logic Unit). С его помощью вы можете выполнять арифметические, логические и битовые операции с числами, хранящимися в регистрах общего назначения. Результаты этих операций также влияют на флаги в регистре состояния.
Флэш-память
Флэш-память еще называется программной памятью. Это память с наибольшей емкостью, в основном для хранения данных. С помощью нее информация не удаляется при отключении питания. Как видно на структурной схеме микроконтроллера, стрелка между FLASH-памятью и шиной данных направлена только в одном направлении. Это связано с тем, что ЦП не может записывать информацию непосредственно на нее. По этой причине использование этого типа памяти весьма ограничено. Помимо программы, она также может содержать таблицы констант и раздел загрузчика, который позволяет загружать новую программу во FLASH без использования программатора.
Программная память разделена на ячейки с определенной емкостью в зависимости от используемого микроконтроллера. Наверное, никого не удивит то, что в AVR это 8 бит. Каждой ячейке назначается шестнадцатеричный адрес. Ниже представлена карта памяти ATmega16:
Как видите, память разделена на разделы приложения и загрузчика. Размер раздела загрузчика определяется битами. После сброса, процессор последовательно считывает команды. В зависимости от настройки битов она может запускаться с начала раздела приложения или с начала загрузчика.
Мы не будем здесь разбираться с загрузчиками, потому что это тема для отдельной статьи. Кроме того, в даташите вы можете найти некоторую информацию с примерами кода. В любом случае программу можно спокойно запускать, если в разделе загрузчика ничего нет.
RAM (оперативная память)
ОЗУ, в отличие от FLASH, хранит информацию только во время работы чипа. Очищается после каждого сброса. В свою очередь, доступ к нему намного быстрее и нет ограничений на допустимые записи. Поэтому ОЗУ идеально подходит для хранения переменных.
Кроме того, ОЗУ используется как аппаратный стек. Принцип работы стека прост — на него можно накинуть больше байт данных, а потом удалить. Последний брошенный байт находится на самом верху стека, поэтому он будет привязан первым. Если мы бросим в стек еще один байт, он превзойдет предыдущий и будет первым. Стек обычно находится в конце памяти и переходит к началу. Следовательно, если мы неправильно напишем программу, значения, помещаемые в стек, в конечном итоге перезапишут другие данные в памяти.
Адресное пространство (то есть набор всех адресов, которые соответствуют какой-либо ячейке памяти) SRAM (статическое ОЗУ) в AVR также содержит адреса регистров R0-R31 и регистров управления периферийных схем. Ниже представлена карта оперативной памяти ATmega16:
Мы хотели бы указать здесь, что флэш-память и оперативная память полностью независимы друг от друга и имеют отдельные адреса, которые никак не связаны друг с другом. По такому принципу сделаны многие микроконтроллеры.
Периферийные системы
Периферийные системы — это все те полезные компоненты, которыми мы пользуемся при программировании наших роботов. Чаще всего используются стандартные параллельные порты ввода-вывода, принимающие цифровые сигналы от PIN-кодов.
Есть много других схем, которые могут быть альтернативным вводом-выводом этих портов. К ним относятся таймеры, компараторы, внешние генераторы прерываний и многое другое.
Каждая периферийная система имеет регистры конфигурации, которые определяют ее работу. Например, для стандартного порта в ATmega у нас есть три регистра:
- DDRx — определяет направление потока данных,
- PORTx — форсирует состояние выхода,
- PINx — читает статус входа.
Описание использования периферийных систем и режимов их работы — тема для целой серии статей. К счастью, есть таблицы с названиями отдельных битов, режимами работы в зависимости от настроек этих битов и подробными описаниями конфигурации.
Создание и реализация программы
Как мы уже упоминали в обсуждении программной памяти, процессор, после сброса в каждом цикле, считывает инструкцию, на которую обращает внимание указатель инструкции (IP), и выполняет ее. Во время выполнения значение IP изменяется, чтобы можно было прочитать следующую инструкцию. Значение IP не всегда нужно увеличивать на 1, поэтому программа не всегда будет выполняться в одном и том же порядке. Можно делать скачки, то есть циклы.
Программа для микроконтроллера должна состоять из части, которая инициализирует периферийные схемы, а затем переходит к бесконечному циклу. Если такой цикл не существует, процессор, после достижения последнего адреса во FLASH-памяти, возвращается к началу и повторно инициализируется. Этот процесс происходит, даже если вся память не заполнена, потому что для незапрограммированных ячеек, по умолчанию установлено значение 0xFF (255 в шестнадцатеричном формате или только единицы в двоичном формате). Это соответствует коду инструкции, которые затем выполняются для каждой инструкции до завершения памяти. Конечно, в такой программе нет смысла.
Кстати, некоторые путают машинный код с языком ассемблера. |
Машинный код и язык ассемблера — это разные вещи. Машинный код имеет двоичную форму и разделен на команды. В семействе AVR каждая инструкция имеет 16 битов и содержит код операции, режим адресации и значения или адреса аргументов.
Машинный код очень трудно читать людям. А вот язык ассемблера состоит из инструкций и аргументов, состоящих из нескольких букв. Немного попрактиковавшись, вы сможете прочитать и понять его без каких-либо проблем, потому что названия инструкций представляют собой английские сокращения, например, MOV — двигаться. Одна инструкция на языке ассемблера обычно запускается за один такт и соответствует одной инструкции машинного кода.
Каждое семейство процессоров имеет собственный машинный код и язык ассемблера. |
Список команд ассемблера AVR можно найти в примечании в сводке по набору инструкций. Благодаря содержащейся в ней таблице, вы можете узнать о команде на ассемблере, типах аргументов, кратком описании и количестве тактов, необходимых для ее выполнения. Также можно проверить, как долго выполняются функции.
Если мы напишем нашу программу на языке более высокого уровня, например, на C, процесс создания исполняемого файла, то есть компиляция, пройдет в несколько этапов.
После оптимизации, компилятор пишет программу на языке ассемблера, а затем переключается на машинный код.
Прерывания
Прерывания предназначены для того, чтобы вы могли остановить выполнение основного цикла программы и перейти к обработке какого-либо срочного события. Они активируются флажком в регистре статуса. Если этот флаг установлен в 1, периферийные устройства могут работать в режиме прерывания. Некоторые из них могут генерировать несколько разных прерываний. Например, коммуникационные интерфейсы имеют возможность генерировать прерывания при отправке и получении байта.
Когда процессор получает запрос прерывания, он переходит к специальному адресу памяти (вектору прерывания), где находятся инструкции по обработке каждого прерывания, и выбирает соответствующие. |
Таблицу, показывающую, какой адрес в векторе соответствует какому прерыванию, конечно можно найти в примечании к каталогу. Перед переходом к прерыванию, в стек записывается IP-регистр, чтобы вы могли вернуться к основной программе в том же месте, но позже. Важно, чтобы указатель стека указывал на то же место в памяти, что и в начале прерывания, прежде чем возвращаться к основному циклу.
Очевидным преимуществом работы в режиме прерывания является немедленное получение важных данных измерений или быстрая связь. Второе преимущество — улучшение основного цикла программы. Больше нет необходимости каждый раз проверять состояние данной периферийной системы. Про нее можно полностью забыть в основном цикле и обработать только с помощью прерывания.
Поскольку прерывания мешают выполнению основной программы, очевидно, что их обработка должна быть как можно более короткой. Это оставляет больше времени для работы с программой. Новички часто не знают об этом и помещают в обработчик прерывания длинные циклы или функции задержки. В общем, функции задержки плохо работают с прерываниями, даже если они находятся в основном цикле. Причина в том, что они используют циклы и операцию ассемблера, без операции, для заданного количества циклов в основном цикле программы. Таким образом, нетрудно догадаться, что чем чаще этот цикл прерывается, тем больше время задержки будет отличаться от установленного. Конечно, для коротких промежутков времени, порядка нескольких микросекунд, разница не должна отрицательно влиять на программу, но для более длительных задержек различия могут быть уже значительными.
Мы можем определить продолжительность прерывания, используя код языка ассемблера. Мы также можем оценить, сколько раз будет срабатывать прерывание. Благодаря этому мы можем правильно скорректировать время задержки. Конечно, точно в цель мы никогда не попадем, но такая процедура может помочь.
При написании программы прерывания на C, мы часто попадаем в ловушку компилятора и оптимизации. |
Что ж, все переменные разных типов, которые мы обрабатываем во время компиляции, в любом случае преобразуются в одну и ту же форму нуля или единицы, а затем помещаются в памяти. Это могут быть регистры общего назначения, ОЗУ или стек. Если же они больше не нужны, вместо них можно написать что-нибудь другое.
Компилятор, на основе анализа кода, определяет, нужны ли по-прежнему данные или нет, и может не знать, что глобальная переменная, также должна использоваться в прерывании. В результате переменная, предназначенная для изменения своего значения в прерывании, часто остается неизменной.
Рецепт состоит в том, чтобы добавить ключевое слово volatile при инициализации глобальной переменной. |
Типы переменных
Наверняка каждый, кто написал какую-либо программу, знает основные типы переменных. Однако, наверное, не всех интересовало, как они хранятся в памяти и какие действия с ними можно выполнять. Поэтому здесь мы остановились на наиболее популярных типах.
Char — переменная, представляющая один символ с клавиатуры. Он занимает в памяти 1 байт (8 бит). Несмотря на то, что новички часто приравнивают этот тип только к буквам, на самом деле его также можно рассматривать как число. Беззнаковый символ может быть от 0 до 255 (то есть от 2 ^ 8 до 1), а символ может быть от -128 до 127.
Вы можете выполнять с ней математические операции, независимо от того, использовали ли вы его ранее как число или знак. например, ‘a’ + 1 = ‘b’.
Вы можете найти числовой эквивалент каждого символа в таблице ASCII.
Int (целое число) — это один из наиболее часто используемых типов переменных. Ints обычно занимает 16 бит памяти и может принимать значения от 0 до 65535 (2 ^ 16-1) в беззнаковом режиме или от -32768 до 32767 со знаком.
Обычно нам не нужно работать с такими большими числами, и нам подходят 8-битные переменные. Следовательно, если мы знаем, что нам не понадобятся числа больше 8 бит, написание такой переменной как int — это просто пустая трата места в памяти.
Float (число с плавающей запятой) — это тип, хранящий дробные числа, занимает в памяти 4 байта (32 бита). В то время как в предыдущих двух типах двоичные данные напрямую преобразовывались в десятичные числа, здесь ситуация иная.
Значение числа с плавающей запятой рассчитывается так:
Где S (знак) соответствует знаку и имеет значение 1 или -1, а E (показатель степени) — это показатель степени, до которой повышается основание системы счисления, над которой мы в настоящее время работаем.
Конечно, компьютеры работают в двоичной системе, поэтому число 2. M — мантисса, которая, в нашем случае, является числом от 1 до 2. Числа с плавающей запятой намного точнее, чем обычные типы. К сожалению, чем большие значения они принимают, тем они менее точны. Это связано с их экспоненциальным характером и присвоением разным числам с одинаковым значением одного и того же представления в памяти.
Очень важно помнить, что арифметика с плавающей запятой отличается от классической арифметики. |
Операции с плавающей запятой сложнее, и они также работают с 32-битными числами. Их использование намного более интенсивно загружает процессор и отрицательно сказывается на скорости выполнения программы. Поэтому перед использованием числа с плавающей запятой стоит задуматься, действительно ли вам нужна такая точность. Возможно, было бы лучше работать с целыми числами и использовать какой-нибудь математический трюк, например, масштабирование значения, чтобы избавиться от дроби, или сохранение целой части в одной переменной и дробной части в другой.
Хотя с нашей точки зрения, все эти типы радикально различаются, для процессора они всегда представляют собой просто последовательность нулей и единиц. Вот почему с числами или символами типа int вы можете выполнять операции с равномерным битовым сдвигом. Вы также можете заставить компилятор читать один тип как другой. В этом суть операции приведения к C, с которой у многих возникают проблемы. Однако в языке ассемблера каждая переменная — это просто двоичные данные в памяти, и программист сам решает, как с ними обращаться.
Математические операции
Как вы могли заметить, у ЦП есть только несколько основных функций, таких как:
- сложение,
- вычитание,
- умножение,
- побитовые операции.
Первое, что бросается в глаза — это отсутствие деления. Да, даже такая тривиальная математическая операция — настоящий вызов для микроконтроллера. Процессор обращается с этим так же, как человек, имеющий в своем распоряжении лист бумаги. Алгоритм очень близок к обычному письменному делению! Однако это может занять несколько десятков тактов, а для этого требуется много регистров процессора.
Однако есть одно исключение — деление на два. |
В двоичном формате эта операция фактически сдвигает число влево или вправо на заданное количество разрядов. Точно так же, как в десятичной системе счисления, где умножение / деление на степень десяти сводится к смещению десятичной точки. Итак, деление — это еще одна вещь, которую следует использовать в умеренных количествах.
Поскольку так много проблем вызвано простым делением для микроконтроллеров, страшно думать о более сложных операциях, таких как тригонометрические, логарифмические функции, производные или интегралы. Вот здесь и пригодятся математические уловки. Точнее, приближение функций, т.е. приближение их значений на заданных интервалах. Что касается периодических функций, то есть тригонометрических функций, вы также можете использовать массивы вычисленных значений, а затем масштабировать их.
Аппроксимация функции многочленами основана на наблюдении, что для каждой функции можно найти многочлен, который находится с ней на определенном интервале. Чем выше степень полинома, тем точнее приближение.
Конечно, было бы хорошо выполнить операции, связанные с вычислением такого многочлена, раньше, например, в Matlab, а в программе использовать готовые коэффициенты.
Когда дело доходит до аппроксимации интеграции и дифференцирования, на различных форумах есть множество примеров, связанных с реализацией алгоритма PID. Там для интегрирования используется метод прямоугольников. Другими методами аппроксимации интегрирования являются метод трапеций и метод Ньютона-Рафсона. С другой стороны, дифференцирование обычно проводится как коэффициент разности.
Существует множество алгоритмов аппроксимации функций. Однако они требуют хороших математических знаний, включая тригонометрические функции, полиномы, интегральное и дифференциальное исчисление.
Вывод
Статья, как мы думаем, получилась очень обширной и затрагивает множество различных вопросов. Мы понимаем, что, наверное, мало кому это понадобится сразу. Тем не менее, такую информацию стоит изучить.
При написании данной статьи, мы использовали знания, полученные на занятиях в университете и опыт из собственных практик, а также использовали общедоступные интернет-источники.
С Уважением, МониторБанк