Тут некоторые представители сообщества, читающие мои предыдущие материалы, просили меня рассказать подробнее про микроконтроллеры, таймеры и прерывания. Выполняю их пожелание. Вероятно, кто-то скажет, что это не тема для данного сайта... Но это не совсем так. ER9x разрабатывалась обычными моделистами, kha делал прошивку для оранжа, Expert, VitGo... Ну да не о том речь.
Микроконтроллер – маленькая микросхемка на плате, которая выполняет возложенные на нее прошивкой функции – «черный ящик». Если мы хотим понимать, как работает тот или иной код прошивки, как заставить микроконтроллер делать то, что нам надо, а не то, что он хочет… Короче говоря, я предлагаю немного приоткрыть завесу и хотя бы в общих чертах уяснить, из чего состоит микроконтроллер и как все это работает.

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

Разговор об архитектуре микроконтроллера имеет смысл начинать с самого простого и необходимого элемента – регистра. По сути это оперативная память… одна единственная ячейка оперативной памяти… очень быстрой памяти. Условно регистр можно изобразить следующим образом:

    Я изобразил некий абстрактный восьмиразрядный регистр. Он имеет набор входов DI0:7 и набор выходов DO0:7. Так же у него есть специальный управляющий вход. Я обозначил его S. Работает он следующим образом: Первым делом на входах DI устанавливаем нужную комбинацию нулей и единиц – то двоичное число, которое мы хоти запомнить. Далее на вход S подаем одиночный импульс. По переднему (или по заднему – регистры бывают разные) фронту импульса регистр запоминает состояние входов и устанавливает точно такое же состояние на выходах. После этого сигнал со входа можно убрать, а на выходе он останется. Регистр запомнил переданное ему число.

 

    Я упомянул о фронтах импульса… Для тех, кто не знает, фронтом импульса называется переходный процесс, во время которого значение сигнала меняется с нуля на единицу или наоборот. В первом случае – это будет передний фронт, а во втором – задний.

 

    Итак, основные составляющие микроконтроллера:

  • АЛУ – арифметико-логическое устройство. Этот компонент отвечает за все вычисления. Как следует из названия, он выполняет все математические операции.
  • ОЗУ – оперативная память. Во время работы в ней размещаются значения всех переменных.
  • ПЗУ – постоянная память. В отличии от оперативной, данные в ней хранятся даже после отключения питания. Она используется для размещения кода программы и для хранения других пользовательских данных, которые должны быть доступны в разных сеансах работы. Под последним я понимаю интервал времени между включением и отключением питания.
  • АЦП – аналогово-цифровой преобразователь. Этот компонент служит для получения цифрового представления аналогового сигнала. Грубо говоря, он меряет напряжение на входе и выдает число на выходе.
  • Порты ввода-вывода – набор аппаратных средств, при помощи которых микроконтроллер взаимодействует с другими устройствами.
  • Таймеры/счетчики – специальные компоненты для подсчета времени

 

    Каждый из этих компонентов в микроконтроллере живет своей жизнью и не мешает остальным. Для взаимодействия друг с другом все они подключены к общей шине посредством выше описанных регистров. Именно эта шина и определяет разрядность микроконтроллера – количество одновременно пересылаемых битов между любыми двумя компонентами. Микроконтроллеры семейства Mega являются восьмиразрядными (или восьмибитными). Существуют так же микроконтроллеры 16-ти и 32-х разрядные…

 

    А вот синхронизация компонентов на шине решена до предела просто и гениально. Для постоянной работы этих компонентов, их надо обеспечить фронтами импульсов. И чтобы компоненты могли легко обмениваться информацией, фронты эти должны быть синхронны. Учитывая микроскопические размеры кристалла микроконтроллера самым естественным решением было сделать некий компонент, который всех и обеспечит этими импульсами – тактовый генератор. Генерация импульсов заданной частоты – его единственное предназначение. А от того, насколько высокую частоту он будет выдавать, зависит, как быстро остальные компоненты будут выполнять свои функции. Таким образом, частота тактового генератора является одной из основных характеристик микроконтроллера, определяющих его быстродействие.

 

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

 

    Прошивка – управляющая программа микроконтроллера – представляет собой последовательный набор команд. Ядро считывает эти команды из ПЗУ и выполняет со строгим соблюдением последовательности. Теоретически прошивку можно написать сразу в виде машинного кода, но на практике ни один нормальный человек этим заниматься не будет ввиду неоправданно высокой трудоемкости. Для разработки прошивки используют языки программирования. А для перевода программы с языка в машинный код служат компиляторы. Языки программирования условно делят на две категории: высокого и низкого уровня. Языки программирования низкого уровня максимально приближены к машинному коду. Типичный представитель – ассемблер. Писать на нем не удобно, но зато, в отличии от высокоуровневых языков, можно получить максимальный уровень оптимизации как по объему прошивки, так и по скорости выполнения.

 

    Языки же высокого уровня обладают худшими показателями оптимизации, но они более просты в использовании. Текст программы, написанной на таких языках, более читабелен. Более худшие параметры оптимизации связаны с тем, что одна команда такого языка интерпретируется сразу в несколько команд ассемблера. Компилятор при этом использует типовые шаблоны. В результате в итоговом машинном коде появляются избыточные операции. Однако, как показывает практика, того уровня, что дают компиляторы таких языков вполне достаточно для большинства задач. Наиболее известным языком этой группы является язык Си. Кстати, в свои предки его может зачислить большинство других языков: Java, Processing и другие… Язык, используемый в Arduino IDE, порожден от Processing и сохранил в себе большинство правил классического языка Си.

 

    Но о языке Си я предлагаю поговорить отдельно. Здесь же вернемся к архитектуре, а Си будем использовать для демонстрации.

 

    Как говорилось выше, программа исполняется последовательно по шагам, а рассматриваемые микроконтроллеры имеют всего одно ядро. Так что ни о какой многозадачности говорить не приходится. Однако это не значит, что микроконтроллер не может запараллелить некоторые процессы. Ранее я упоминал, что микроконтроллер состоит из ряда самодостаточных компонентов, которые управляются посредством регистров. Для примера возьмем АЦП. Оцифровка аналогового сигнала – операция достаточно ресурсоемкая. Чем выше требуемая точность, тем больше времени требуется для получения результата. Мы можем дать АЦП команду на преобразование и смиренно ждать результата, ничего больше не делая. А можем не тратить время попусту и продолжить выполнение других инструкций программы. Правда тут возникает другая проблема – нашей программе как-то надо узнать, что результат готов и его можно использовать. Вот для таких случаев в ядре предусмотрен специальный механизм обратной связи – прерывания. Кстати, реализуется он на базе все тех же регистров. Прерывания позволяют прервать выполнение основного кода программы – как бы поставить на паузу – и передать управление специальной функции-обработчику. По окончании выполнения инструкций этой функции, основная программа продолжит выполняться с того же места автоматически. От разработчика при этом требуется не так уж и много:

  • Во-первых, надо знать какие прерывания и в каких режимах может генерировать тот или иной компонент. На этот случай есть документация (datasheets).
  • Во-вторых, надо объявить функцию-обработчик и сопоставить ее с нужным прерыванием – определить вектор прерывания (ISR(XXX_vect)).
  • Ну и последнее – включить компоненту соответствующий режим.

 

О работе с АЦП, его регистрах, режимах и прерываниях можно почитать здесь (повторяться не буду).

 

    Я предлагаю подробно рассмотреть такие компоненты как таймеры/счетчики. Как следует из названия, компоненты предназначены для отслеживания интервалов времени. У микроконтроллеров семейства Mega как правило по несколько таймеров. Для примера возьмем микроконтроллер ATMega328, на котором построена Arduino Uno. Он обладает тремя таймерами. Каждый таймер работает от тактового генератора и подсчитывает импульсы независимо от остальных. Количество подсчитанных импульсов можно получить из регистра TCNT соответствующего таймера. Например, для таймера 1 к этому регистру можно обратиться по имени TCNT1. Допустим, что тактовый генератор выдает импульсы с частотой в 16 мегагерц(т.е. 16 миллионов импульсов в секунду). Не трудно посчитать, что между соответствующими фронтами соседних импульсов будет интервал времени 0,0625мкс. Если предположить, что значение регистра TCNT1 изменилось на 100, то это будет означать прохождение интервала в 6 с четвертью микросекунд.

 

    Каждый таймер имеет свою разрядность. Фактически речь идет о максимальном числе, которое может находиться в регистре TCNT. Так таймер 1 является 16-ти разрядным (максимальное число – 65535), а таймеры 0 и 2 – 8-ми разрядне (максимальное число – 255). Когда в регистре таймера находится максимальное число и с тактового генератора приходит следующий импульс, происходит переполнение таймера, и он сбрасывается в ноль. Этот момент мы можем отследить в программе используя прерывания TIMER0_OVF_vect, TIMER1_OVF_vect, TIMER2_OVF_vect для каждого таймера соответственно.

 

    Но таймеры могут подсчитывать не только импульсы с тактового генератора. Каждый таймер обладает своим набором делителей. Делитель фактически пропускает по одному импульсу на заданное значение. Задать делитель можно выставив соответствующие значения битов CSx0:2 в регистрах TCCRxB, где х – номер соответствующего таймера. Если, например, для таймера настроить делитель на 8, то при частоте тактового генератора в выше означенные 16мГц значение регистра TCNT будет изменяться на единицу каждые пол микросекунды.

 

    Так же каждый таймер обладает парой компараторов. Компаратор строится на базе регистра OCRxA (OCRxB). В последний записывается значение, при совпадении с которым регистра TCNT, срабатывает соответствующее прерывание TIMERx_COMPA_vect (TIMERx_COMPB_vect). При этом таймер можно настроить таким образом, чтобы при срабатывании прерывания компаратора происходил сброс на ноль. При этом частоту прерываний можно будет вычислить по формуле F(A) = Fclk/(N*(1+OCRxA)) , где Fclk – частота тактового генератора; N - коэф. делителя (1, 8, 64, 256 или 1024).


Тут я заготовил некоторую «рыбу» для использования таймеров на Arduino. Ее можно использовать для любой Arduino (в т.ч. Mega2560). Надо только учитывать, что на младших моделях таймеры 3, 4 и 5 отсутствуют.

 

Теперь стоит немного остановиться на портах ввода/вывода.

 

Все внешние выводы микроконтроллера выходят наружу через так называемые порты. Они обозначаются буквами латинского алфавита. По сути каждый порт – это опять таки набор регистров.

 

На примере порта D

  • DDRD – The Port D Data Direction Register (регистр направления передачи данных порта D)
  • PORTD – The Port D Data Register (регистр данных порта D)
  • PIND – The Port D Input Pins Address (адрес входных выводов порта D)

 

Наименования портов и их соотношение с соответствующими пинами Arduino можно найти на распиновках.

 

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

 

    Как-то я задумал сделать шагающего робота – этакого шестилапого паучка… При беглом подсчете оказалось, что в нем должно быть два десятка сервоприводов (18 штук в лапах – 3 на каждую; и еще пара на подвес камеры). И ими всеми надо управлять! Ну да алгоритмы движения паучка – это отдельная тема… А вот в качестве практики к выше написанному я хочу предложить Вам устройство для управления восемнадцатью сервоприводами с компьютера или посредством UART. Для изготовления можно прототипа можно использовать Arduino Uno и Sensor Shield или Arduino Nano и Expansion Board. Если переопределить порты, можно использовать и Arduino Mega. При этом, кстати, можно будет и количество сервоприводов увеличить в несколько раз.

 

    Протокол обмена данными по последовательному порту я взял от телеметрии FrSky. Немного модифицировал и приспособил для своих нужд. Это не так интересно, чтоб на этом останавливаться. ПО для компьютера было написано на C# в Visual Studio 2010. На нем я тоже останавливаться не буду. Просто предложу скачать исходники и готовый exe.

 

    Самое интересное тут в использовании таймеров и портов ввода/вывода. Первым делом все свободные контакты назначаем выходами:

//////// BANK A //////////

pinMode(2, OUTPUT);

pinMode(3, OUTPUT);

pinMode(4, OUTPUT);

pinMode(5, OUTPUT);

pinMode(6, OUTPUT);

pinMode(7, OUTPUT);

pinMode(8, OUTPUT);

pinMode(9, OUTPUT);

//////// BANK B //////////

pinMode(10, OUTPUT);

pinMode(11, OUTPUT);

pinMode(12, OUTPUT);

pinMode(13, OUTPUT);

pinMode(A0, OUTPUT);

pinMode(A1, OUTPUT);

pinMode(A2, OUTPUT);

pinMode(A3, OUTPUT);

//////// BANK C //////////

pinMode(A4, OUTPUT);

pinMode(A5, OUTPUT);


В файле device.h можно найти дефайнсы вида:

#define CH0_on PORTD |= (1<<2) //D2

#define CH0_off PORTD &= 0xFB //D2

 

    Как видно, в первом случае дефайнс подменяет команду включения пина, а во втором – выключения.

 

    Таким образом выходы настроили, управлять ими можем… Теперь самое время вспомнить о том, что сервоприводы управляются сигналом ШИМ с частотой в 50Гц и шириной импульса от 1000 до 2000мкс. Столь жесткая привязка ко времени просто обязывает использовать таймеры.

 

    Частота в 50Гц говорит о том, что импульсы сервоприводам должны формироваться каждые 20мс. Допустим, что мы разработали алгоритм, который отправит каждой серве по одному импульсу заданной продолжительности. Чтобы все работало правильно, нам придется перезапускать этот алгоритм каждые 20мс. Да будет так! Берем 8-ми разрядный таймер 2 и настраиваем его на частоту прерываний 100Гц.

// таймер 2 используем для формирования частоты 50Гц

void Timer2_Init()

{

TCCR2B = 0; //stop timer

TCCR2A = 0;

TCNT2 = 0; //setup

TCCR2A = 0;

TCCR2A = 1<<WGM21;

TCCR2B = (1<<CS20)|(1<<CS21)|(1<<CS22); // CLK/1024

TIMSK2 = 1<<OCIE2A;

OCR2A = 155;

}

 

    Делитель на 1024, компаратор на 155, сброс после совпадения… И собственно обработчик прерывания по совпадению:

ISR(TIMER2_COMPA_vect) {

timer2_cnt++;

if (timer2_cnt > 1)

{

timer2_cnt = 0;

AllBankReset();

}

}

 

    Как видно из кода, на каждое второе прерывание (100/2=50Гц) вызывается функция AllBankReset(), которая возвращает алгоритм формирования импульса каждой серве в начало.

 

    Теперь о алгоритме формирования импульсов. Для начала предположим, что у нас всего одна серва. Для формирования импульса ей надо сделать следующее:

  1. Подать на серву логическую единицу
  2. Задать срабатывание таймера 1 через интервал времени, равный необходимой ширине импульса.
  3. При срабатывании прерывания по совпадению А таймера 1 через заданный интервал времени подать на серву логический ноль.

Если этот алгоритм выполнять каждые 20мс, то серва будет работать.

 

    Теперь попробуем увеличить количество сервоприводов. Для этого будем формировать импульсы последовательно. Т.е. при срабатывании прерывания таймера 1 будем выставлять 0 для текущей сервы и 1 для следующей, а в регистр компаратора будем записывать значение для нового интервала.

Таким образом в 20мс мы сможем уложить 9 сервоприводов.

20мс = 20 000мкс

9*2000мкс(макс импульс для сервы) = 18 000мкс

 

    Оставшиеся 2000мкс пойдут на погрешности, связанные с временем на выполнение команд. Конечно на эти погрешности времени надо намного меньше, но у нас либо будет 2000мкс при 9-ти сервах, либо 0 при 10-ти…

 

    Но 9 сервоприводов – это далеко не 18. Тут стоит вспомнить, что у таймера 1 имеется два компаратора и каждый из них может генерировать прерывания независимо от другого. Скопируем код от прерывания по совпадению А для прерывания по совпадению В и изменим выводы на другие 9 серв. И вот наш микроконтроллер управляет уже 18 сервоприводами.

 

Теперь о некоторых тонкостях:

  1. Таймер 0 следует использовать только в крайнем случае, т.к. он же используется ядром для некоторых системных функций.
  2. Если уж взялись использовать таймер 0, то не стоит уж слишком сильно переопределять его настройки. Именно из этих соображений я не стал включать для него делитель. Для таймера 1 делитель можно было бы и включить, но пусть уж будет аналогичным таймеру 0.
  3. Для более равномерного распределения сигналов, я оставил на каждом компараторе таймера 1 по 8 сервоприводов, а на компаратор таймера 0 – оставшиеся 2.
  4. Я несколько усложнил алгоритм расчета интервала времени импульса для сервы, введя туда еще скорость изменения положения. Последняя представляется двумя параметрами – шагом и частотой увеличения значения на этот шаг.
В итоге получилась прошивка, которая позволяет превратить Arduino в шилд на подобии такого или такого.

Данное устройство не стоит воспринимать как конечное. Скорее это некое "how to" - рыба для включения в прошивку более серьезного девайса. Например, того же паучка...

Исходники прошивки ServoShield.

Приложения WinServo (+ исходники на C#).

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