Прежде чем читать эту статью, рекомендуем вам прочитать статью: Микроконтроллеры
Что подтянуть в си
В наших проектах мы будем использовать язык программирования Си. Поэтому, вам необходимо очень хорошо его знать. Прежде чем читать данную статью, рекомендуем предварительно подтянуть знания по этому языку программирования. В данном разделе мы рассмотрим наиболее важные для программирования микроконтроллеров элементы языка Си. Их надо знать на пятёрку!
Основные типы данных, приведение типов, числовые константы
Константы
Ни одна программа на Си не обходится без констант. Константа может быть определена как переменная с модификатором const, а может просто быть указана в тексте программа.
const int i = 10; //констант i, равная 10 #define I 10 //константа I определенная через#define int b; b *= 2; //константа 2, сразу указанная втексте
Лучше использовать вместо const, методы #define. Это позволяет задавать вам константы при сборке программы, используя make в сложных проектах.
Когда вы используете числовые константы не забудьте добавить модификатор типа этой константы.
10 //константа типа int !!!! (2 или 1 байт) 10L //константа типа long (4 байта) 10U //беззнаковая константа 10F //число float 10D //число double
Если этого не сделать, то компилятор может ошибиться, и не привести её к нужному типу. Например, 1<< 31 = 0, а вот 1L << 31 = 2 в 32 степени. Программируя микроконтроллеры, вы всегда должны помнить о типе каждой константы, каждой переменной. Это очень важно!
Переменные
Когда вы пишете программу для МК, то у вас обязательно будут какие-то переменные. Очень большое значение имеет тип этих переменных. Вы должны точно знать сколько бит занимает тот или иной тип. К сожалению в Си типы определены не очень строго, например, тип int может занимать 2 байта или 4 байта. Как правило, это зависит от компилятора или выбранной среды программирования. Поэтому когда вы начинаете работать в новой среде, обязательно уточните в документации, сколько байт занимает каждый тип данных.
Во многих библиотеках на Си для микроконтроллеров используются специальные типы, обозначающие точное количество занимаемых бит. Например: uint8_t — этот типа, без знаковое (u — unsigned) целое число (int — integer) длиной 8 бит. Если такие типы определены, лучше использовать их, чем стандартные типы int, char и т. д.
Си часто использует неявное приведение типов переменных, однако, программируя микроконтроллеры, вам лучше про это забыть сразу. Обязательно используйте явное приведение типов - это позволит вам избежать большого количества ошибок.
u8 i = 255; u16 b; b = i * 2; //не явное приведение типа, bбудет равно 254!!!! а не 500 b = (u16)i * 2 //явное приведение типа b = 500
При неявном приведении типов, результат может отличаться от ожидаемого. Проблема заключается в том, что компилятор, может сначала провести все операции, с более маленьким типом, а потом уже привести результат к большему типу. В этом случае старшие биты будут потеряны.
В МК всегда не хватает памяти, всегда идёт гонка за производительностью. Поэтому, большинство ваших переменных, должно иметь тот тип, с которым комфортно работает микроконтроллер. Например, для 32-х битных МК, это u32, для 8-ми битных u8. Это очень сильно влияет на производительность вашей программы.
Битовые операции
Для экономии памяти, а также из-за архитектурных особенностей МК, при программировании, вы очень часть будет работать с битами. Вам точно будут необходимы следующие битовые операции:
-
i << 10 - сдвинуть значение переменной i на 10 бит влево. Если представить значение переменной i в двоичном коде, то данная операция сдвигает все биты влево, на пустое место становится 0, старшие биты теряются. Пример: 0b10000110 << 2 = 0b00011000.
-
i >> 10 — сдвинуть значение переменной на 10 бит вправо.
-
i & 0b111 — логическое умножение, работает оно как обычное умножение, только применяется к каждому биту. Данная операция очень часто используется, для выделения из переменной значения одного бита в условиях. Например, i & 0b100, даст нам 0b100, если третий бит равен 1 и 0 в других случаях, а значит условие if (i & 0b100) сработает, когда третий бит выставлен в 1. Можно сказать, что данная операция является наложением маски на значение переменной.
-
i | 0b001 - логическое сложение. Работает как обычное сложение, только для каждого бита, при этом 1 | 1 = 1, а не 2. Данная операция используется для установки нужных бит в значении в единицу, не затрагивая других бит. Например, i | 0b100, выставит третий бит в единицу.
-
~ i - логическое отрицание, данная операция меняет все 1 на 0. Очень часть используется вместе с логическим умножением, для снятия нужного бита. Например, i & (~ 0b100) обнулит третий бит в переменной i.
В некоторых микроконтроллерах (например, STM32), есть специальные служебные регистры, установка в которых, определённого бита в 1, снимает бит в нужном регистре. Если есть такие особенности, то предпочтительно использовать именно эти служебные регистры, а не обычные.
Для удобной работы с битами, в Си есть понятие битовые структуры. Например:
typedef struct GPIOx{ u32:0 PIN0; u32:1 PIN1; ... u32:31 PIN31; } GPIOA;
Такая структура позволит вам удобно обращаться к битам, через их символьное описание. GPIOA-> PIN0 = 1;
Для экономии места, очень удобно, для флажков использовать биты, тогда в одном байте можно разместить 8 флажков, однако, для ускорения работы, лучше использовать родные типы для данного микроконтролера.
Операции сдвига очень часто используются для сборки из байтов длинных переменных. Например, у нас есть 2 байта, надо из них получить число u16:
(u16) i = (a << 8) | b;
Вызов функций по указателю
Очень удобно использовать возможность Си хранить указатель на функцию в переменной. Это позволяет делать универсальные обработчики событий. В переменную помещаем указатель на нужную функцию, и по окончанию какого -то события вызываем эту функцию. Такой приём часто используется в обработчиках прерываний. На Си это выглядит так.
typedef void(*PFN_Callback_t)(void); //создаем типуказатель на функцию void * PcallBack; //создаем переменную указательна функцию PcallBack = (void*)(&func1); //записываем впеременную нужную нам функцию ((PFN_Callback_t)PcallBack)(); //вызываем функцию
Разбираем данные на байты
В Си есть функции преобразования типов. Очень удобно с их помощью работать с различными данными. Например, любую структуру можно представить в виде массива байт. Это часто используется, чтобы передать структуру по какому-то протоколу или записать её в EEPROM. Предварительно, вы должны оценить размер структуры в байтах, для этого в Си есть функция sizeOf(), в которую вы передаёте нужный тип. Просто сложить размеры входящих переменных в структуру нельзя, компилятор для оптимизации может использовать выравнивание переменных и добавить пустые байты. Самым правильным будет в отладчике (или в симуляторе) посмотреть размер структуры с помощью функции sizeof() и дальше исходить из этого.
Разберём подробнее как можно «разобрать» данные на байты.
u8 i; u16 b; u32 f;
Введём три переменные. В любой момент вы можете обращаться к ним как к массиву.
(u8 *) (&b) [0] = 1; (u8 *) (&b) [1] = 2; sendUart( (u8 *) (&f)[0]); sendUart( (u8 *) (&f)[1]); sendUart( (u8 *) (&f)[2]); sendUart( (u8 *) (&f)[3]); (u8 *) (&f) [0] = readUart(); (u8 *) (&f) [1] = readUart(); (u8 *) (&f) [2] = readUart(); (u8 *) (&f) [3] = readUart();
Приводим адрес переменной b к указателю на массив байт. И обращаемся к первому и второму байту. Посылаем по UART 32-ух битное число побайтно и потом собираем его обратно.
Функции, inline
Для того, чтобы вызвать какую либо функцию, процессор должен сохранить в стеке текущее состояние регистров, записать параметры функции в регистры, потом осуществить вызов. Это очень долго. Поэтому, в программировании микроконтроллеров, очень часто используется модификатор inline. Он означает, что весь кусок функции копируется в код программы без вызова функции. При этом конечно увеличивается размер программы, зато уменьшается скорость выполнения. Если компилятор поддерживает такую возможность, то обязательно пользуйтесь ей. Если же её нет, то применяйте #define. Это конечно менее удобно, но работает аналогично.
inline void store_char(unsigned char c, ring_buffer *rx_buffer) { int i = (unsigned int)(rx_buffer->head + 1) % RX_BUFFER_SIZE; if (i != rx_buffer->tail) { rx_buffer->buffer[rx_buffer->head] = c; rx_buffer->head = i; } }
Обычно, это какие-то небольшие функции, которые должны выполняться быстро, например, использоваться в прерываниях.
Целочисленная математика и тип float
В микроконтроллерах используется в основном целочисленная математика. Есть отдельные МК, которые имеют FPU (float point) модуль для проведения расчетов с дробными числами типов float и double. Если ваш МК не имеет такого модуля, то вам стоит избегать данных типов в вашей программе. Выполнение операций с данными типами на МК без модуля FPU будет выполняться очень долго. Например, среднее время операции умножения дробных чисел на STM8 занимает 2000 тактов!!!
При работе с целочисленной математикой, вы должны всегда следить за размерностью результата, а также быть осторожнее с операциями деления. Прежде чем делить результат на что-то, для повышения точности, вы должны сначала провести все операции умножения. Например, результат будет разный, если (2 / 100 * 100) и (2 * 100 / 100). В первом случае вы получите 0, потому что, 2 меньше 100 и первая операция деления даст 0. А во втором случае получите единицу, потому что 200 / 100 = 1. Все операции деления в целочисленной математике не имеют остатка!
Если вам нужно приводить к общему виду какие либо единицы измерения, то делать это необходимо в самом конце, когда выводим информацию пользователю, особенно это касается операций с суммированием, иначе вы потеряете точность. Для повышения точности используйте умножение на большое число, а потом поделите на него. Этот приём очень часто используется в программах на МК. При этом, для умножения лучше использовать числа степени 2, а не десятки и тысячи, так как операции умножения на степень 2, это простой сдвиг битов, выполняется гораздо быстрее чем умножение на 100.
Допустим, нам надо посчитать ток, которое потребляет устройство. У нас есть датчик тока. Каждую секунду мы знаем мгновенный ток. Чтобы посчитать ток, потраченный за минуту, надо каждую секунду делить его на 60 и сложить полученный результат. Но, в целочисленной математике, вам надо сложить все результаты, а потом поделить на 60. То есть, правильнее будет сделать так:
//вводим две переменные! Для секунд идля тока! u32 tok; u32 sec; //каждую секунду добавляем ТОК, иничего не делим. sec++; tok+= gettok(); //когда надо выдать результат, тутбудем делить sendLCD( tok / sec); {\ccc]
Все операции деления надо применятьв самом конце. Соответственно, при такомподходе надо следить за возможнымпереполнением значения переменных.
В микроконтроллерах операции математического округления (до большего числа, например) занимают много времени.Поэтому, проще будет не делать такого округления. Лучше повысить точность и вывести больше знаков, но последний знак не округлять. То есть, если вы хотели бы показывать градусы с точностью до десятых. То лучше вывести до сотых, но без округления. В основном, все программы так и работают. Чтобы работать с максимальной точностью в операциях сравнения не используйте деление, а лучше наоборот умножьте на делитель минимальное число и сравнивайте уже большие числа сразу.
Основная модель программирования МК
Если вы в любой среде программирования микроконтроллеров создадите пустойпроект, то увидите примерно такую программу:
#define … //секция 1 int main() { //секция 2 - инициализация while (1) { //секция 3 - основной цикл }; }
Это не случайное сходство для различных сред программирования МК. В данной мини программе заложена основная модель программирования всех микроконтроллеров. Любой микроконтроллер работает после включения в бесконечном цикле. И вы должны это запомнить. Любая программа для МК никогда не заканчивается. После подачи напряжения, инициализации и старта, МК работает непрерывно, выполняя бесконечно Основной цикл программы.
При включении МК проводится инициализации всех его параметров, а потом вы выбираете, как будете писать основную программу.
Существует две методики программирования микроконтроллеров:
-
используется только основной цикл — polling (опрос)
-
используются прерывания — interrupt
Конечно, на практике, обычно комбинируют эти два подхода, но выделим их как отдельные методики, потому что, одну и туже задачу, можно решить каждым из методов. Вы должны понимать, чем одна методика лучше другой. В каких случаях лучше использовать прерывания, а когда задействовать основной цикл.
Инициализация
Любая программа начинается с инициализации, микроконтроллеры - не исключение. Первым делом необходимо инициализировать состояние всех внешних портов, настроить периферию, задать параметры тактирования микроконтроллера, произвести прочие настройки.
Данный код выполняется один раз, при включении микроконтроллера. Далее микроконтроллер переходит в основной цикл.
Стадия инициализации длится очень быстро — несколько микросекунд, но по меркам схемотехники, это очень долго. Такого времени вполне достаточно, чтобы сгорели внешние транзисторы или другие детали. Когда проектируете своё устройство, имейте это ввиду. Все важные узлы схемы должны работать при полном отсутствии микроконтроллера! Например, если у вас H — мост управления двигателем, то обязательно должны быть резисторы подтяжки на транзисторах, которые обеспечат нужный уровень без работающего микроконтроллера. Если вы управляете пищалкой, то транзистор должен иметь подтяжку к GND, чтобы без МК транзистор не остался в неопределённом состоянии.
Если вам важно энергопотребление (например, при питании от аккумулятора), то необходимо позаботится об этом на стадии инициализации. Если вы не используете какую либо периферию, то необходимо отключить ее, через специальные регистры. Если у вас остались свободные выводы у МК, необходимо перевести их в Push Pull режим и перевести в выдачу Low сигнала, они не должны оставаться в неопределённом состоянии. Если вам не нужна быстрая обработка данных, надо снизить частоту работы ядра. Все это делается один раз на этапе инициализации.
Если вы используете спящий режим, то при включении лучше настроить всю периферию, выводы на минимальное потребление и уйти в спящий режим. Это позволит вам упросить схему зарядки аккумулятора и заряжать его в спящем режиме, когда потребление тока очень мало. Если так не сделать, то при включении МК будет пытаться сделать какие-то энергоёмкие действия, что при недостаточном заряде, будет вызывать перезагрузку МК.
На этапе инициализации можно проиграть какие-то звуки, вывести какое-то приветствие на экран, как-то показать, что прибор включился. Тут самое место для считывания из EEPROM всяких настроек работы программы.
Как правило, во всех МК есть возможность определить, по какой причине произошла перезагрузка. В этом месте можно проверить — были ли ошибки, недостаточное питание, что явилось причиной перезагрузки, зафиксировать это для дальнейшего разбора.
Если вы делаете схему, в которой выход из строя каких-либо компонентов может привести к полной не работоспособности, позаботьтесь о том, как при включении вы можете проверить корректную работу этих компонент и максимально спасти прибор. Обязательно запрограммируйте эти проверки, и не выполняйте программу дальше, в случае обнаружения неработоспособности.
Основной цикл (polling)
После того как все модули МК инициализированы, настройки прочитаны, начинается собственно ваша программа.
Микроконтроллеры сделаны для того, чтобы автоматизировать какие-то процессы, занимая как можно меньше места, и делая это как можно быстрее, экономичнее. Для лучшего понимания методик программирования МК, рассмотрим некий просто прибор — электронный термометр. Примерно так будет работать его программа в основном цикле.
-
Прочитаем данные с датчика температуры
-
Произведём необходимые вычисления, чтобы привести эти данные к читаемому виду
-
Выведем их на экран
-
Перейдём к п.1
На примере этой программы вы видите, что микроконтроллер работает непрерывно, в вечном цикле, выполняя одни и те же действия. Вроде бы, тут нет ничего сложного, однако на практике, возникает много нюансов в такой методике программирования.
Микроконтроллер всегда работает в среде реального времени, он взаимодействует с устройствами, датчиками, которые живут своей жизнью, параллельно и независимо от МК. Программируя МК, вы всегда должны об этом помнить. Ваша задача грамотно разделить эти потоки информации, правильно реагировать на различные события. Каждая ваша операция занимает какое процессорное время, но обычно микроконтроллер работает очень быстро, и думать об этом приходится редко. Вы больше должны думать о внешних устройствах — датчиках, экранах, пищалках — вот они работают медленно, и с этим в первую очередь надо работать. Основное время микроконтроллер тратит на ожидание ответа от внешних устройств.
В данном разделе мы не будем рассматривать то, как МК получает данные от датчика, по какому-то протоколу с использованием периферии, или читая данные с вывода МК. Это будет описано позже. Здесь важно, чтобы вы поняли, как необходимо разделять потоки данных в вашей программе.
Основной цикл должен быть написан максимально без задержек, без использования функций типа Delay(), с чётким разделением всех потоков по временным интервалам. Применительно к нашему термометру, подумаем, как часто нам надо считывать данные с датчика температуры? Если процедура чтения данных будет непрерывно вызываться в основном цикле, то это, при работает МК на частоте 16Мгц, даст нам более 1 млн опросов датчика в секунду. Надо ли нам столько раз его опрашивать, сможет ли датчик выдавать информацию так часто. Об это вы должны думать каждый раз когда пишите какую либо процедуру опроса в основном цикле. То есть первое, что необходимо, это определить частоту, с которой должен выполняться тот или иной кусок программы. Основной цикл должен состоять из большого количества простых условий, вы все время должны проверять различные флажки — состояние выводов, состояние битов в регистрах периферии, значения переменных, эти операции выполняются очень быстро и большую часть времени работы МК, он должен проверять эти флажки. И только в момент, когда это необходимо предпринимать какие-то действия, которые могут выполняться долгое время.
Теперь уточним нашу задачу. Пусть датчик температуры выдаёт данные 10 раз в секунду. Наша программа изменится следующим образом.
if (timedatchik==0) { timedatchik = 100;//ms temp = getdatadatchik(); temp *= 100; } if (temp != temoold) refreshlcd();
Мы будем засекать время 100мс, чтобы получить частоту 10 раз в секунду, и будет опрашивать датчик только 10 раз в секунду. Выводить на экран температуру нам тоже незачем каждый цикл, поэтому и вывод информации необходим только тогда, когда поступили новые данные.
А теперь немного отвлечёмся от нашего примера и рассмотрим конструкции, которые наиболее часто используются в основном цикле.
а) Выполнение программы только при изменении состояния датчика.
while(1) { kn = getdata(); if (knold != kn) { …. } knold = kn; }
Создаем две переменные knold и kn, записываем новые данные в kn, сравниваем значение старое с текущим, в конце цикла запоминаем текущее значение kn в переменную knold. Таким образом, мы можем отследить ситуацию изменения значения датчика.
б) выполнение программы с заданной периодичностью:
if (timedel == 0) { ... timdel = 10; }
Переменная timdel уменьшается каждую миллисекунду или секунду, когда она равна нулю, то делаем что-то и опять начинаем новый отсчет времени. Так можно программировать работу по временным интервалам.
в) выполнение программы по установленному флажку:
if (flagset) { … flagset=0; }
Проверяем установлен ли флажок, если да, то делаем что нам нужно, и сбрасываем флаг. Сам флаг устанавливается в другом месте программы.
г) простой автомат
switch (status){ case 10: ... break; case 20: .. break; case 30: .. break; default: }
Теперь мы можем описать схему работы в основном цикле МК.
-
С нужной периодичностью получаем данные с датчиков
-
В случае изменения данных датчиков, проводим нужные вычисления
-
При необходимости производим управляющее действие, что-то выводим на экран, воспроизводим музыку и т. д.
-
Периодически производим какое-то действие, например, мигаем светодиодом
Как вы могли заметить, при написании программы в основном цикле, мы не можем гарантировать, что на следующем цикле будет ли выполнено какое-либо действие (может придти время для другого длительного события). Поэтому необходимо все операции, которые нельзя разрывать во времени, выполнять сразу, с нужными задержками. Например, если нам надо получить данные от датчика по протоколу I2C, то необходимо выполнять всю процедуру обмена в одной команде. Делается это примерно так.
void getdatadatchik() { sendbyteI2C(10); while (bytesending); sendbyteI2C(10); while (bytesending); while (bytereceive==0); temp = I2Creg->data; }
Мы останавливаем основной цикл, пока не получим все данные. Бесконечные задержки while служат для того, чтобы дождаться выставленного флажка в какой-то регистре. И дождаться этого необходимо именно сейчас, а не наследующем цикле. Потому что, мы можем узнать о том, что байт получен слишком поздно, когда выйдет таймаут, и датчик уже перестанет передавать данные. В итоге, так или иначе, в основном цикле начинают возникать задержки, которые тормозят основную программу. Ваша задача так расписать периоды и частоту вызова тех или иных операций, чтобы одно не мешало другому. Далее мы рассмотрим, как в этом помогают прерывания.
Прерывания
Прерывание (англ. interrupt) — сигнал от программного или аппаратного обеспечения, сообщающий процессору о наступлении какого-либо события, требующего немедленного внимания. Мы не будем вдаваться в тонкости работы механизма прерываний. С точки зрения программирования МК на Си, нам будет достаточно понимания того факта, что в каждом микроконтроллере есть возможность, при наступлении определённого события приостановить выполнение основной программы и передать его в специальную функцию, обработчик данного прерывания. Набор событий зависит от конкретного микроконтроллера, они подробно описаны в datasheets. Рассмотрим, как можно использовать прерывания в вашей программе.
Для лучшего понимания, решим простую задачу — есть кнопка, при её нажатии надо зажечь светодиод. Если мы будем эту задачу решать методом опроса (polling), то мы получим примерно такую программу
knold = 1; while (1) { kn = PORTB->PIN1;//считали состояние кнопки if ((kn == 0) && knold) { PORTB->PIN2 = 1;//зажгли светодиод } knold = kn; }
В основном цикле мы все время опрашиваем состояние вывода PIN1, и как только там станет 0 (кнопка нажата), то зажигаем светодиод. Основной минус в таком подходе, что в основном цикле мы все время опрашиваем состояние кнопки, а так как в основном цикле, могут попадаться и длительные операции, то мы можем пропустить нажатие кнопки! Если МК занят в этом время, например, выводом на экран какой-то информации. Прерывания как раз и предназначены для того, чтобы убрать этот минус.
Прерывания обрабатываются постоянно, независимо от загрузки процессора, за это отвечает отдельный модуль, и пропустить событие не получится. Самое интересное, что некоторые виды прерываний, обрабатываются даже в спящем режиме, и позволяют не тратить энергию МК для постоянного опроса ножек. Однако, прерывания тоже занимают ресурсы процессора, и управление ими не простая задача.
Обработка прерываний построена по принципу очереди. Все прерывания распределены по приоритетам производителем МК, а также есть возможность программно поменять его для каждого прерывания. Вам обязательно надо понять этот механизм и научится им пользоваться. Для этого представим следующую ситуацию, в один и тот же момент времени, сработало прерывание по изменению состояния входа PIN1 — пр1 (приоритет 1), прерывание о готовности данных ADC — пр2 (приоритет 2), прерывание таймера TIM1 — пр3 (приоритет 2). Как же МК поступит в этом случае?
Во первых, МК отсортирует все эти прерывания по приоритету, далее по времени срабатывания и далее просто по внутреннему номеру прерывания — пр1,пр2,пр3. После этого он поставит их всех в очередь, и передаст управление функции, обработчику прерывания пр1. Когда его обработка закончится, то следующему в очереди, и так, пока не обработает все прерывания. У каждого прерывания в очереди есть бит отложенного прерывания (pending bit ), прерывание удаляется из очереди, только когда этот бит будет снят. Как правило, снять этот бит вы должны в обработчике прерывания самостоятельно. Это сделано специально, для того, чтобы МК был уверен в том, что прерывание обработано. Также, такая схема позволяет обрабатывать в одном обработчике несколько различных видов прерываний с разными отложенными битами. Во многих МК есть возможность обрабатывать вложенные прерывания, как правило это включается программно. В этом случае, другое прерывание более высокого уровня может прерывать обрабатываемое. Механизмы реализации тут могут быть разные, более точно необходимо читать в datasheet. Если вам необходимо заняться тонкой настройкой приоритета прерывания, то представьте, что обработчики каждого прерывания выполняется очень долго, и вам надо выяснить остановка какого прерывания может стать для него критической. Приоритет такого прерывания надо сделать самым высоким. И так далее.
Теперь вернёмся к нашей задаче про кнопку. С использованием прерываний программа будет выглядеть так.
void INT1(void) { // обработчик прерывания if (PORTB->PIN1==0) { kn = 1; } } void main(void) { while (1) { if (kn) { PORTB->PIN2 = 1;//зажгли светодиод kn=0; } } }
Функция INT1 назначена на обработку прерывания по изменению значения порта «B». В обработчике прерывания мы проверяем, что кнопка нажата и выставляем флажок. Уже в основном цикле, мы проверяем, что флажок установлен и делаем нужное действие. Можно ли зажечь светодиод не в основном цикле, а сразу в обработчике прерывания? Конечно да, но вы должны понимать, что обработчик прерывания должен выполняться максимально быстро, чтобы не задерживать другие прерывания. Поэтому, там должны быть только простые операции, все сложные вычисления лучше оставить на основной цикл. Внутри прерывания можно смело управлять выводами, читать или записывать переменные, производить не сложные операции. Внутри прерываний желательно не использовать вызовы других функций, нельзя использовать функции зависящие от прерываний (например, Delay).
Прерывания — очень удобный механизм, как правило его используют вместе с основным циклом. Если вы пишите код, на границе возможностей МК по скорости, то вы должны обязательно прочитать в datasheet сколько тактов МК тратит на вход в прерывание — это может быть от 8 до 16 тактов! Как видите - не мгновенно. Также надо понимать, что вести отладку прерываний в реальном времени, не просто, пока отладчик остановится, могут измениться состояния регистров. Поэтому, если вам необходимо понимать сколько времени прошло от события до его обработки, то необходимо использовать debug pin — выводы выделенные для отладки. Самое быстрое, что вы можете сделать — это поменять состояние вывода. И вот это событие, уже необходимо анализировать с помощью осциллографа или логического анализатора — внешнего устройства.
Мы рекомендуем, там где это возможно, использовать прерывания. Это делает вашу программу более простой и понятной, разгружает процессор и позволяет делать код многопоточным.
Спящий режим
Энергопотребление вашего устройства в первую очередь зависит от микроконтроллера. Он является мозгом всем системы и легко может отключить от питания любую периферию. В итоге схема, когда МК все время подключён к питанию и управляет периферией, все чаще используется в приборах. Это позволяет отказаться от выключателей, включать и выключать устройство долгим нажатием на кнопку. Специально для экономии энергии, МК имеют различные режимы питания. Один из способов — это понижение частоты работы МК, второй — полная остановка МК (или его части). Это и есть спящий режим.
Конкретные настройки спящих режимов сильно зависят от выбранного МК и отличаются от серии к серии. Однако, практически все МК, имеют следующие режимы сна:
-
Полная остановка — halt режим, наименьшее энергопотребление, как правило работают только внешние прерывания, именно они и будят МК, например, нажатие кнопки.
-
Остановка с активным таймером пробуждения — active halt, дополнительно работает таймер, который может будить МК через определённые интервалы времени. Такой режим очень подходит, например, для МК, обслуживающего датчик. Прочитали датчик, передали сообщение и опять уснули. При этом, надо понимать, что МК живёт в мире микросекунд. Если он поработает 10 миллисекунд, а спать будет 900 миллисекунд, то потратит одну сотую часть от энергии, постоянной работы. При этом каждую секунду он будет на связи.
-
Остановка с работающей периферией — wait режим, позволяет спать ядру, пока периферия работает. Например, дали задание измерить напряжение, и спать. Результат готов, проснулись, обработали.
Как правило, спящий режим активируется программно. Если вы делаете устройство на аккумуляторе или батарейкам, то вам обязательно необходимо изучить какие режимы энергопотребления имеет ваш выбранный МК. И обязательно надо использовать их в программе.
С точки зрения программирования на языке Си, все довольно просто. Вы должны записать в определённые регистры, что будет работать при остановке ядра, а потом вызвать в коде команду halt() (или аналогичную). На этом месте вашей программы МК уснёт. После выхода из сна, программа продолжится со следующей инструкции после команды halt(). Это очень естественно и понятно, и легко программировать. Дополнительно, в некоторых МК (stm8 и другие), есть возможность работы постоянно в спящем режиме, отвлекаясь только на прерывания, не возвращаясь к основной программе, после обработки прерывания. Это очень удобно. Например, уснули, проснёмся когда 10 раз сработает прерывание от кнопки. Для этого в обработчике прерывания необходимо установить флажок для продолжения сна, пока счётчик не достигнет 10.
Управляем выводами GPIO
GPIO - это самый простой, и одновременно самый важный модуль МК. В основном вся работа с ним сводится к двум операциям:
-
при включении МК надо инициализировать параметры каждого вывода (конечно это можно делать не только при включении МК, но и в процессе работы программы)
-
при работе надо, или прочитать один бит из регистра отвечающего за порт МК, или записать бит
Обычно все выводы МК сгруппированы в порты, и практически все они доступны программно (за исключением специальных выводов — VDD, GND, RESET — иногда и он доступен). Если вы составляете схему самостоятельно, обязательно прочитайте Datasheet на каждый используемый вывод. Во многих МК, не все выводы одинаковые, и некоторые из них имеют большое количество ограничений. Например, STM8S003, выводы I2C, SDA SCL нельзя перевести в HIGH, они работают только как Open Drain вывод. Таким образом, на них нельзя повесить кнопку без внешнего резистора. Обычно вывод, на который подключается внешний кварц, тоже имеет ограничения. Тоже самое касается RESET. Ограничение может иметь вывод программирования МК.
Настройки выводов у каждого МК свои. Но есть общие правила. Каждый вывод может:
-
быть обычным выводом OUTPUT — то есть выдавать 0 или 1 по вашему требованию, иметь PULL UP резисторы, быть выходом Open Drain, быть в неопределённом состоянии
-
быть входом INPUT — то есть иметь возможность программно прочитать состояние выхода, обычно в этом случае используется триггер Шмитта.
-
иметь альтернативную функцию — служить выводом какой либо периферии, например, UART RX, SPI MOSI и т. д.
Настройка выводов производится через специальные регистры. За каждый вывод обычно отвечает один бит. Для упрощения работы с МК, чтобы не помнить все выводы, производители разрабатывают специальные библиотеки, в которых, или делаются специальные дефайны (define) для удобства, или функции для настройки вывода. С ними программирование вывода становится простым, а программа читаемой. Вот как, например, настраивается вывод при использовании библиотеки от компании Nuvoton.
GPIO_SetMode(P4, BIT2, GPIO_PMD_OPEN_DRAIN);
а обращение к выводам может быть такое
P43=1; //порт 4, 3 нога в HIGH P42=0; //порт 4, 2 нога в LOW
Если вы разрабатываете программу, то для работы с выводами очень удобно использовать команду #define это позволяет давать выводам имена согласно вашей схеме. Также это позволяет быстро проводить настройку выводов под другую трассировку схемы.
#define BTON PORTB->10 = 1; #define BTOFF PORTB->10 = 0; main { BTON while (1) { if( flag ) BTOFF } }
Прочитать состояние вывода на Си тоже просто. Надо прочитать состояние нужного бита в регистре, отвечающем за порт. Самая быстрая битовая операция (с точки зрения количества тактов) на МК — это установка в единицу одного бита — логическое сложение, поэтому во многих МК, есть специальные регистры, в которых надо установить бит в 1, чтобы задать на выводе состояние 0. Это сильно ускоряет операции с выводами.
Каждый вывод МК имеет ограничения на максимальную частоту меандра. Например, в STM8 максимальная частота меандра на выводе может быть 2 или 10 Мгц. Чем выше частота, тем больше энергопотребление! Поэтому, не следует выбирать максимальную частоту там, где это не надо. В STM32, чтобы получить частоту выше 50Мгц, сделаны специальные ячейки для подзарядки выводов, которые тоже потребляют энергию и их надо специально включать. Также надо иметь ввиду, если частота ножек будет больше 50Мгц, то потребуются специальные решения при трассировке платы, с меньшими частотами проблем не должно возникать. Как правило, программно получить такой меандр на выводе не получится, в основном это возможно только при использовании периферии, например таймеров.
МК имеет на борту большое количество различной периферии. Но производитель не знает, какая периферия будет вам нужна. Поэтому, он оставляет это на ваш выбор. Каждый вывод МК ценный и может служить обычным выводом. Но если вам нужна периферия, то можно задействовать определённые выводы под периферию. В последних моделях МК, периферию можно подключить практически любой вывод МК, на младших, она закреплена на фиксированных выводах. Конечно, это очень удобно, когда можно использовать любые выводы, это сильно упрощает трассировку платы. Поэтому, выбирая МК под проект, имейте это ввиду. Иногда производитель предлагает выбрать из нескольких вариантов выводов для одной периферии. Будьте очень внимательны при таком выборе выводов, может оказаться так, что назначая на определённый вывод UART RX, вы теряете какую-то другую периферию. На текущий момент существуют специальные визуальные конструкторы (см. в конце статьи), которые позволяют упростить этот процесс, практически на все серии МК. Обязательно используйте их, но не доверяйте на 100 процентов, проверяйте по datasheet. Тут лучше 10 раз проверить, один отрезать.
Используя вывод для какой то периферии, производитель, как правило, оставляет возможность настроить сам вывод как вам надо (по любому варианту вывода). Например, ножка UART TX, может иметь режим Open Drain (или PULL UP), тогда 1 в TX будет означать неопределённое состояние ножки, а вовсе не HIGH! Это очень удобно использовать при согласовании уровней. Open drain режим можно использовать для управления выводом с подтяжкой к другому напряжению, отличному от питания МК. Например, МК питается от 3в, а Open drain будет управлять 5В или 1.8В.
Отдельно стоит сказать про выводы, толерантные к напряжению 5В. Например, если МК имеет напряжение питания 3.3В и есть необходимость взаимодействовать с датчиком или другой микросхемой, которая имеет напряжение питания 5В. Различное напряжение питания элементов схемы порождает проблемы согласования уровней, что требует схемного решения. Производители МК специально, для упрощения таких решений, делают часть выводов толерантными к 5В. Это говорит о том, что МК не «сгорит», если подать на эти ножки 5В, вместо 3.3В. В случае HIGH состояния вывода у МК на выводе будет около 3.3В. Такое напряжение воспринимается пятивольтовыми датчиками как логическая единица. Это все позволяет не делать схемных решений по согласованию уровней. Будьте ВНИМАТЕЛЬНЫ — не все выводы толерантны к 5В, а только некоторые. Внимательно читайте Datasheet.
Кнопки
Кнопки - самый часто используемый элемент в приборах. Ничего сложного в их обработке нет. Как правило кнопка подключается одним выводом на GND, а другим к выводу МК. Таким образом, при нажатии, вывод соединяется с GND. Чтобы обработать такой вариант кнопки, необходимо вывод установить в режим работы INPUT PULLUP. То есть, когда кнопка не нажата на выводе должна быть логическая единица, из-за резистора подтяжки к VDD. Если кнопка подключена длинными проводами, то внутренней подтяжки может не хватить, надо использовать внешний резистр.
Обрабатывать кнопки можно как в основном цикле, так и с помощью прерываний. В начале разберём как это делать с помощью прерываний. В разных МК, прерывания на выводах работают по разному. Если есть такая возможность, лучше настроить срабатывание прерывания по падающему сигналу FALL (то есть в момент когда HIGH меняется на LOW), если такое не возможно, то по изменению сигнала PIN CHANGE (в этом случае прерывание будет срабатывать и при нажатии и при отпускании кнопки). Как правило, обработчик прерывания один на весь порт, и если у вас подключено несколько кнопок к выводам одного порта, то в обработчике прерывания надо проверить, какой вывод равен нулю, чтобы узнать какая кнопка сработала. Далее все просто, устанавливаем флажок, что кнопка нажата, и в основном цикле или сразу делаем нужное действие. Код на Си будет выглядеть примерно так.
void INT1(void) { if (PORTB->PIN1 == 0) { kn = 1; } } main() { while (1) { if (kn) { kn = 0; } } }
Чтение порта сбрасывает Pending бит данного прерывания. В случае обработки кнопки в основном цикле, нужна будет переменная knold, и программа будет выглядеть примерно так.
main() { while(1) { kn = PORTB->PIN1; if (kn==0 && knold) { //... } knold = kn; } }
Однако, если вы сделаете первый или второй вариант кода, то работать это будет не совсем верно. Проблема в дребезге контактов. При замыкании контактов, в кнопке или выключателе, контакты замыкаются не мгновенно, в момент замыкания гибки контакты начинают вибрировать, возникают помехи, и микроконтроллер видит не одно нажатие кнопки, а сразу несколько. Обычно это происходит не дольше 50 миллисекунд. Таким образом, чтобы корректно обработать нажатие кнопки, необходимо в случае использования прерываний отключить прерывание на данном выводе на 50 миллисекунд, а потом включить обратно. При использовании основного цикла, опрос кнопок надо делать каждые 50 мс. А ещё лучше опрашивать кнопки в прерывании таймера, каждые 50 миллисекунд, так будет решена проблема дребезга и вы не пропустите нажатие кнопки. Дополнительно можно убедится, что это не наводки от помех, и после задержки ещё раз проверить состояние кнопки, и если она все ещё нажата, то это не помехи, кнопку можно считать нажатой. Ещё хороший метод, несколько раз в цикле прочитать состояние кнопки, и если все разы состояние кнопки одинаковое, то считаем что кнопка нажата.
Если вы только начинаете работать с МК, то обязательно поработайте с кнопками. Типичные задачи, которые вы должны уметь решать.
-
По нажатию кнопки зажечь светодиод, по следующему нажатию выключить
-
По нажатию зажечь, по длительному нажатию выключить (3 секунды)
-
По двойному нажатию зажечь и тройному нажатию выключить (в течение 2 секунды 2 нажатия или 3)
Для отслеживания двойного и долгого нажатия нужна переменная таймер, для учёта времени, прошедшего с начала нажатия. Для двойного нажатия также нужен счётчик. Логика простая. Например, выделяем время 2 секунды. Кнопка нажата, счётчик плюс один. Если прошло две секунды, то обнулили счётчик. Если счётчик равен 2, то зафиксировано двойное нажатие. Для долгого нажатия, запускаем таймер после нажатия, и если до его окончания кнопку отпустили, то сбрасываем все и опять ждём нажатия.
Стоит отметить, что кроме программной защиты от дребезга контактов есть схемные решения, самое простое - наличие параллельно выводам кнопки конденсатора на 100nf. Кнопки очень удобно использовать для вывода МК из спящего режима, для этого достаточно настроить обработку прерываний по нажатию кнопки.
Таймеры
Таймеры в МК — это вторая самая часто используемая периферия. Вам обязательно нужно изучить, и попробовать на практике, как с ней работать. В разных МК таймеры могут работать по разному, поэтому обязательно читаем datasheet перед начало работы с ними. Мы рассмотрим общие моменты использования таймеров. Чтобы было понятнее, будет рассматривать настройки таймера на примере реальных задач. Названия регистров приведены на примере STM8, у других производителей могут быть выбраны другие названия, но сути это не меняет.
Для начала, разберём самый простой вариант использования таймера — счётчик миллисекунд. Нам необходимо, чтобы переменная milis содержала количество миллисекунд, прошедшее с запуска таймера. Так как, мы считаем миллисекунды, то тип переменной сделаем unsigned long, чтобы быстро не наступило переполнение. Вся работа с таймерами ведётся с помощью прерываний, потому что, очень сложно поймать путём опроса состояния таймера момент перехода таймера через 0, хотя это тоже возможно. Разберем как решается данная задача с помощью прерываний.
Первым делом, необходимо произвести настройки таймера. Потом, запустить его, и далее в обработчике прерывания останется только добавлять единицу к нашей переменной. Так как нам надо учитывать миллисекунды, то необходимо настроить таймер так, чтобы прерывание вызывалось каждую миллисекунду. Как мы ранее описывали в статье про микроконтроллеры, таймер — это простой счётчик, который с каждым тактом МК уменьшается (или увеличивается) на единицу. Основные его параметры — предделитель PSCR (pre scaler register) и максимальное значение счетчика ARR (auto reload register), которое ограничено разрядностью таймера. Необходимо, используя эти два параметра и настроить период срабатывания прерывания. Допустим частота МК 16 МГц и таймер работает на этой же частоте (таймер может работать на частоте отличной от частоты МК). Чтобы прерывание срабатывало каждую миллисекунду, необходимо сделать так, чтобы таймер досчитал до 16 000 и вызывал прерывание. Для этого достаточно установить ARR таймера в 16 000. Для такого большого числа нужно 16 бит. Если таймер имеет такую разрядность, то достаточно в ARR записать (16000 — 1), обычно таймер начинает считать с нуля. Однако, такое не всегда возможно, разрядность ограничена производителем МК. На помощь приходит предделитель. По сути, в этом случае получается два связанных таймера. Сначала, каждый такт, предделитель уменьшается на единицу, и только когда он дойдет до 0, увеличивается счетчик таймера CNT (counter), когда его значение дойдет до ARR, то сработает прерывание. Например, если предделитель будет равен 15 (16-1), то в ARR надо записать уже не 16 000, а 16 000 / 16 = 1000. А если предделитель равен 160, то уже 100 надо записать в ARR. Таким образом, можно пользоваться таймерами и с низкой разрядностью. Естественно, такой таймер не позволит учитывать другой интервал времени, например, каждые 999 миллисекунд. На Cи таймер программируется очень просто, для предделителя и ARR, есть специальные регистры, надо в них записать нужные значения и все.
TIM1_PSCR = 160; //предделитель TIM1_ARR = 100; // максимальное значениетаймера
Для запуска таймера необходимо в специальном регистре установить в 1 бит. Дополнительно, может понадобиться включить питание на периферию таймера TIM1. Если нам необходимо, то надо включить обработку прерываний таймера — установить в единицу нужные биты в регистрах. Если вы работаете с библиотекой, то там могут быть готовые функции, которые сразу настраивают таймер. Например, на stm8 это выглядит так:
TIM1_TimeBaseInit(TIM1_PRESCALER_16, 1000); TIM1_ClearFlag(TIM1_FLAG_UPDATE); TIM1_ITConfig(TIM4_IT_UPDATE, ENABLE); TIM1_Cmd(ENABLE);
Первая команда настраивает таймер, задает предделитель и значение таймера. Следующая команда сбрасывает таймер на 0, а следующая запускает обработку прерываний. Последняя - стартует сам таймер. Все, осталось обработать прерывание.
INTERRUPT_HANDLER(TIM4_UPD_OVF_IRQHandler, 23) { milis++; /* Cleat Interrupt Pending bit */ TIM4_ClearITPendingBit(TIM4_IT_UPDATE); }
После обработки события, в прерывании необходимо очистить бит отложенного прерывания, иначе обработчик будет вызываться постоянно. Здесь стоит отметить, что если у вас, например, 8-ми битный МК, а разрядность таймера 16-ти битная, то регистр ARR состоит из двух регистров — обычно _High и _Low. Запись в эти регистры должна быть в определённой последовательности, сначала _High, а потом _Low (читайте это в datasheet). На си это выглядит примерно так:
TIM1->ARRH = (uint8_t)(TIM1_Period >> 8); TIM1->ARRL = (uint8_t)(TIM1_Period);
Мы познакомились с основными регистрами таймера — PSCR и ARR. Что еще можно делать с помощью таймеров? Таймеры могут генерировать ШИМ (PWM) сигнал. Для этого вводится ещё один регистр — сравнения и захвата CCR (capture and compare). Он имеет такую же разрядность как и счетчик таймера CNT. В данный регистр мы можем записать значение, и когда таймер дойдёт до него, то будет вызвано дополнительное прерывание. Таким образом, у нас будет уже два прерывания — когда таймер достигнет до данного значения и когда дойдет до ARR. Если в одном прерывании подать на некий вывод HIGH, а в другом LOW, то получится ШИМ сигнал. Значение CCR будет регулировать скважность ШИМ сингала.
Таймеры могут самостоятельно управлять выводами, без обработчиков прерывания. Это позволяет организовать «железный» ШИМ, без участия ядра МК. К сожалению, не все выводы можно задействовать для управления, обычно они жёстко определены. Это создаёт определённые неудобства при трассировке платы. С использованием же прерываний, можно использовать любой вывод.
Дополнительно, производители МК добавляют в каждом таймере несколько каналов. По сути, это отдельные, таймеры, но связанные общим предделителем и максимальным значением. Значение сравнения CRR у каждого канала может быть своё. Такая схема позволяет на железном уровне синхронно работать всем каналам таймера. В этом случае лучше использовать прямое управление выводами. Самое частое использование эта модель находит в управлении моторами. Специальный бит в регистре настроек таймера позволяет синхронно менять все настройки каналов таймера. Работает это обычно очень просто, вы записываете новые значения CRR и других параметров таймера, но реально таймер не применяет их, пока вы не установите в единицу этот специальный бит. И в этом момент, сразу все значения всех каналов будут обновлены. Также, можно одновременно запускать все каналы. Для управления моторами существует много настроек, которые предназначены для защиты, быстрое выключение всех таймеров по специальному выводу, блокирование настроек таймеров, для запрета случайного изменения, все их необходимо читать в datasheet для конкретного МК.
Таймеры могут помогать декодировать сигналы. Например, вам необходимо посчитать сколько микросекунд прошло между импульсами на определённом выводе МК. Даже если вы будете делать это с помощью прерываний, вы не сможете точно посчитать время из-за работы других прерываний и задержек по входу в прерывание. Если нужно точное значение, то здесь поможет только таймер. При таком использовании, разрядность таймера определяет точность посчитанного времени. По сути одна единица счётчика (с учетом предделителя) — это минимальная единица времени, которую вы сможете учесть. Настраивается тут опять же все просто. С помощью специального регистра, вы переводите таймер в режим учета входа (обычно вывод опять же предопределён), настраиваете ARR таймера, настраиваете уровень сигнала, который вы ждёте и запускаете таймер. В итоге, когда произойдёт нужное событие, будет вызвано прерывание, и в регистре сравнения CRR будет лежать значение, равное времени интервала. Вам остаётся только его перевести в нужную единицу времени, в зависимости от настроек таймера. Где это ещё может понадобиться: декодирования сигнала пульта ИК управления телевизором, декодирование ШИМ сигнала на входе (например, считывание команды с передатчика радиоуправления), декодирование 1-wire сигнала и другие варианты управления по одному проводу.
Таймеры - довольно сложная периферия, имеющая очень широкие возможности. Они также могут:
-
управлять другими таймерами (по срабатыванию одного таймера, запускается другой таймер, особенно это удобно, при декодировании сигнала)
-
управлять другой периферией (считывание ADC синхронно с таймером, с определённый момент, синхронное копирование данных через DMA)
-
генерирование одного импульса нужной длины
-
управление моторами с учетом dead time (время задержки между включением выключением синхронных выводов)
-
управление моторами с получением сигналов от энкодера положения вала
Описать все это в данной статье не возможно. В каждом конкретном случае, производитель приводит примеры использования, необходимо ориентироваться на них и datasheet. В основном все это реализуется записью определённых параметров в регистры настроек таймера. Чтобы изучить все это, лучше всего поработать с таймерами на практике, в конкретных приборах.
Учёт временных интервалов
Практически ни одна программа для МК не обходится без учёта временных событий. Например, каждые 500 миллисекунд считываем показания датчика, через 3 секунды после нажатия кнопки выключить светодиод, пищать каждую секунду, мигать светодиодом каждые 5 минут и т. д. Вы должны легко уметь программировать обработку таких событий. Есть несколько вариантов как это можно реализовать. Все они основаны на таймере, например, миллисекундном. Итак, у нас есть настроенный таймер и обработчик прерывания, который срабатывает каждую миллисекунду, как это настроить мы писали выше. Рассмотрим как можно запрограммировать учет таких событий.
Пусть нам надо каждые 500 миллисекунд включать и выключать светодиод. Необходимо создать переменную timeled, в которую записать значение 500. Далее в обработчике прерывания, каждый раз уменьшать значение этой переменной на единицу. Когда она дойдет до нуля, в основном цикле можно сделать нужное действие и опять записать туда 500. Все повторится сначала.
TIMINT() { if (timeled) timeled--; } main() { while(1) { if (timeled==0) { //делаем что нужно.. timeled = 500; } } }
Конечно, если это задача, помигать светодиодом, то можно это сделать прямо в прерывании. Если же это что-то более массивное, например, обмен с датчиком, то лучше делать все в основном цикле. Напомним ещё раз - обработчики прерывания должны быть очень небольшие и быстрые. Заметьте, что мы используем не увеличивающуюся переменную, а уменьшающуюся. Так у вас не будет проблем с переполнением.
Второй метод, более продвинутый, использовать возможность Си хранить в переменных указатели на функции. Вы можете создать массив, состоящий из элементов структуры, хранящей значение таймера, новое значение таймера, и указатель на функцию. Далее в обработчике прерывания для всех элементов массива уменьшаете значение на единицу. В основном цикле, когда дошли до нуля, вызываете функцию из структуры таймера. Запускать такие таймеры очень удобно. Назначаете на нужный номер таймера функцию и время срабатывания, и она регулярно будет вызываться. Набор функций можно менять динамически. Вот как будет выглядеть программа.
typedef struct { timer_res_t msec; timer_res_t start; void* pCallback; } Vtimer_t,*Pvtimer; TIMINT() { u8 i; for (i = 0; i < VTIMER_NUM; i++) { if (sVtimer[i].msec) sVtimer[i].msec--; } } main() { while(1) { for (i = 0; i < VTIMER_NUM; i++) { if (sVtimer[i].msec == 0 && sVtimer[i].pcallback != 0) { ((PFN_Callback_t)sVtimer[i].pCallback)(); sVtimer[i].msec = sVtimer[i].start; } } } }
Если нужен какой-то ряд событий по временным интервалам, то в основном цикле вы можете использовать убывающие сравнения со значением переменной.
if (timled > 300) { //выполняется от 500 до 300 } else if (timeled > 200) { //выполняется от 300 до 200 } else if(timeled) //от 200 до 0 }else { timeled = 500;//перезапуска таймер }
Если таймеры служат для учета какого то события, которое имеет много настроек, то лучше его включить в структуру вместе со всеми значениями, так код становится более читаемым. Также не забывайте использовать дефайны на значения таймеров по умолчанию, это позволит вам их быстро менять.
Работа с основной периферией
Микроконтроллеры имеют много различной периферии на борту. Это необходимо для разгрузки основного процессора от выполнения рутинных задач. Всю периферию мы не сможем разобрать в данной статье, мы разберём только наиболее часто используемые модули. Но, в принципе, можно сказать, что механизмы работы с любой периферией общие. Производитель МК под каждую периферию выделяет ряд регистров, через которые и ведётся работа.
Периферия - это независимые от основного процессора модули, которые используют общий ресурс — память, и могут иметь доступ к шине данных. Механизм работы с любой периферией асинхронный, вы даёте задание периферии, она кладёт результат в специальный регистр или забирает оттуда данные, и оповещает вас о своей работе через другой регистр, как правило выставляя флажки — биты, и вызывая прерывания.
Для корректной работы любой периферии, вы должны вовремя обрабатывать ответы модуля. Если вы пропустите нужное событие, или отреагируете на него некорректно - это может привести к возникновению различных непредвиденных ситуаций, в том числе до полного зависания периферии до следующего выключения прибора. Важно внимательно читать datasheet на модули, и действовать согласно описанию. Производители МК очень часто пишут дополнения к datasheet - ERRATA, и там указывают об ошибках в периферии. Если что-то пошло не так, ищите дополнения к описанию, смотрите примеры от производителя, читайте форумы.
ADC
Данная периферия позволяет оцифровать значение напряжения на определённом выводе МК, чтобы потом с ним можно было работать в программе. МК может иметь на борту несколько модулей ADC, каждый модуль может иметь несколько каналов. Основное отличие каналов от независимых модулей ADC, заключается в невозможности одновременной оцифровки значений на выводах разных каналов одного ADC. Каждый канал подключён к одному модулю ADC через мультиплексор, и чтобы опросить все каналы, МК должен последовательно подключать каждый вывод через мультиплексор к самому ADC.
Механизм работы с ADC очень простой. Вы даёте команду, записывая единицу в специальный бит регистра настроек ADC, на старт измерения, и через какое-то время ADC выдаёт результат в регистр ответа, и выставляет в единицу бит готовности, и если настроено, вызывает прерывание. Далее вы обрабатываете результат. Все это можно делать в основном цикле. Тогда необходимо ждать ответ в цикле, проверяя бит готовности и потом считать полученные данные.
ADC1.CR1 |= ADCSTART; while ((ADC1.SR & ADCREADY)==0); rez = ADC1.DATA;
Названия регистров, конечно, могут отличаться у каждого МК, они свои, общего стандарта тут нет. Как долго производит вычисления модуль ADC? Обычно МК содержат не очень быстрые и точные ADC, в среднем одно измерение занимает от 1 до 10 микросекунд и имеет разрядность 10-12 бит. Если вам нужен более быстрый или более точный ADC, то надо подбирать специальный МК или смотреть в сторону внешних специализированных ADC. Там скорость может доходить до 1 наносекунды и точность до 24 бит.
Мы рассмотрели самый простой случай работы с ADC — считывание результата с одного канала. Практически все МК умеют работать с каналами в последовательном режиме и управлять последовательностью опроса выводов. В этом случае, вы задаёте какие выводы надо опрашивать и в каком порядке (обычно прямой или обратный порядок по нумерации выводов ADC1 … ADC10 или ADC10...ADC1) путём записи в регистры настроек. Запускаете ADC, и по готовности каждого результата забираете его в регистре данных. Тут опять же есть разные варианты настроек, МК может начать следующее измерение после того, как вы заберёте предыдущее или работать на скорость, не успели забрать, потеряли данные. Удобнее всего в этом режиме работать через прерывания. Если вам неважна скорость обработки данных, то самый правильный вариант, собирать с помощью прерывания сразу все нужные каналы в один массив данных и потом уже в основном цикле работать с ними. ADC могут работать в непрерывном режиме и единичном. В непрерывном, после одного измерения сразу начинается другое.
Вы должны понимать, что любая работа с ADC требует применения фильтров, это может быть решено схематически — добавляется RC цепочка, или программно, тогда используется цифровой фильтр по специальному алгоритму. Самый простой цифровой фильтр, например, скользящее среднее последних 10 значений. Без использования фильтров возможны выбросы пиков значений, что приведёт к неверной работе вашей программы. Если некуда спешить, то можно сразу получить 5-10 значений и выбрать из них среднее прямо в основном цикле.
Практически все ADC имеют зависимость точности и скорости обработки данных. То есть, чем более высокую точность вы требуете, тем медленнее вы получите результат. Иногда точность можно настроить так как вам необходимо. Также стоит понимать, так как каналы ADC зависимые и идут через мультиплексор, то при быстром переключении канала, на максимальной скорости, может быть считано не верное значение, наведённое предыдущим каналом. Производитель об этом, как правило, предупреждает. Поэтому если вы работаете на границе точности и скорости ADC, то внимательно читайте datasheet и дополнения к нему. В этом случае дополнительно используются схемные решения, ограничивающие ток на выводе или дополнительные ёмкости.
Если нужна синхронная работа ADC с какими-то событиями, то тут необходимо использовать таймеры. Они могут давать сигнал о начале измерения. Правда это могут не все таймеры, поэтому внимательно читаем datasheet. Если у МК есть модуль DMA, мы его рассмотрим далее, то очень удобно с его помощью, вообще без прерываний, перемещать результаты ADC сразу в массив в памяти. В этом случае вы прозрачно работаете с массивом готовых данных и программа сильно упрощается. Конечно цифровой фильтр в таком случае применить сложнее.
ADC потребляет много энергии. Если он вам не нужен, не забывайте отключать его (в принципе это относится ко всей ненужной периферии). При включении ADC обычно нужное некоторое время, чтобы он начал работать. Также необходима специальная последовательность действий для его калибровки. Все это указано в datasheet.
К данной периферии также можно отнести компаратор и функцию Analog Watchdog. Эти функции позволяют генерировать прерывание при попадании напряжения на выводе в определённый заданный вами интервал.
UART
UART — универсальный асинхронный приемопередающий модуль. Обычно он может работать и в синхронном режиме тоже, и по сути является универсальным синхронно асинхронным интерфейсом. Возможности этой периферии очень широки. Кроме классического последовательного порта она также может декодировать 1-wire протокол, IRDA (инфракрасный приёмник) декодирование, работа со SmartCard протоколом, мульти процессорный обмен данный с использование адресов.
Рассмотрим самый простой, и наиболее часто встречающийся вариант — классический асинхронный последовательный порт. С точки зрения программирования данного модуля все довольно просто. Как правило есть два регистра данных — полученные данные и передаваемые данные, в некоторых МК это один регистр, через который данные передаются в независимые регистры, недоступные программисту. Перед началом работы необходимо провести инициализацию периферии. Настроить параметры порта, скорость обмена (сейчас есть варианты автоопределения скорости при получении данных), формат пакета (8-9 бит, наличие аппаратного контроля чётности и т. д.). Когда все настроено, остаётся только получать и передавать данные. Например, если мы хотим передать данные, то записываем их в регистр передачи данных и выставляем флажок начала передачи. Ждём выставления флажка, что данные переданы, тогда можно передавать следующие данные, и так пока не передадим все данные. Если мы работаем в основном цикле, то просто в цикле ожидаем нужного статуса и передаём очередной пакет данных. При работе с прерывании, функция отправки выглядит примерно так.
UART_TX_INT() { data = TxBuffer1[IncrementVar_TxCounter1()]; UART1->DR = data; if (GetVar_TxCounter1() == GetVar_NbrOfDataToTransfer1()) { /* Disable the UART1 Transmit interrupt */ UART1_ITConfig(UART1_IT_TXE, DISABLE); } } UART_RX_INT() { data = UART1->DR; RxBuffer1[IncrementVar_RxCounter1()] = data if (GetVar_RxCounter1() == GetVar_NbrOfDataToRead1()) { /* Disable the UART1 Receive interrupt */ UART1_ITConfig(UART1_IT_RXNE_OR, DISABLE); } }
Прерывание на передачу данных вызывается каждый раз, когда освобождён буфер передачи данных, в обработчике мы должны просто положить очередную порцию данных в регистр данных, если же это все данные — то необходимо отключить данный вид прерывания. При получении данных, тоже самое, прерывание будет вызвано когда данные будут доступны для чтения, получаем их, если это конец, то отключаем прерывания. У вас может возникнуть вопрос - пока мы будем записывать данные в регистр, линия будет ждать пакета данных и нарушится непрерывная передача? Этого не произойдёт, потому что, на самом деле МК имеет еще один регистр данных (буфер), которые реально передаются, поэтому на самом деле, как только мы положим данные в регистр данных DR, они сначала скопируются в другой внутренний регистр и начнётся передача данных, и когда передача ещё идёт, уже сработает прерывание и мы положим новую порцию данных раньше чем та была отправлена. Так организуется непрерывный поток данных. В более мощных МК, данная периферия может иметь большой буфер в несколько десятков байт для передачи и получения данных, это позволяет работать с очень большими скоростями непрерывных данных.
В данном примере вы заметили, что используется такое понятие как буфер. Обычно UART используется для обмена с компьютером или другими системами, и как правило обмен происходит на уровне строк, а не байтов. Поэтому обычно используется внутренний буфер для удобной передачи целой строки за раз. Таким образом, достаточно положить строку данных в буфер и включить прерывания, дать сигнал о начале передачи, все данные будут переданы и прерывания выключатся. Очень удобно. Тоже самое при получении. Даём команду и при наполнении буфера или получении байта окончания строки - получаем флажок и разбираемся с полученными данными в буфере.
Следует сказать что в Си есть очень удобная функция printf и scanf для формирования строк. Обычно производители МК в своих библиотеках позволяют перенаправить эти функции на UART периферию. В этом случае можно в удобном формате передавать данные на компьютер с вашего устройства через COM порт. Например, в STM8, достаточно оформить следующие функции и поток будет перенаправлен на UART периферию.
PUTCHAR_PROTOTYPE { /* Write a character to the UART1 */ UART1_SendData8(c); /* Loop until the end of transmission */ while (UART1_GetFlagStatus(UART1_FLAG_TXE) == RESET); return (c); } GETCHAR_PROTOTYPE { char c = 0; /* Loop until the Read data register flag is SET */ while (UART1_GetFlagStatus(UART1_FLAG_RXNE) == RESET); c = UART1_ReceiveData8(); return (c); }
UART очень удобно использовать для настройки и программирования вашего устройства по bluetooth или wifi. В этом случае после установки соединения вы просто передаете данные в определённом вами формате по UART. В обработчике прерывания ждёте начала сообщения, наполняете буфер и передаёте сигнал готовности, далее в основном цикле обрабатываете данные и производите нужные настройки.
Многие МК позволяют загружать прошивку по данному протоколу, загрузчик (bootloader) встроенный на заводе, использует данную периферию. Если такая возможность есть, используйте эту возможность в схемах для прошивки по bluetooth или wifi.
Остальные возможности UART используются реже, их можно посмотреть на примерах к вашему МК. При работе с любой периферией, связанной с выводами МК, не забывайте настраивать сами выводы на нужный режим, это не всегда делается автоматически.
SPI
Это самый простой протокол обмена данными, который к тому же, один из самых быстрых из межмикросхемных протоколов. Он предполагает наличие одной линии данных для передачи, одной для чтения, и одной линии тактового генератора. В настоящее время существует модификация QUAD SPI, имеющая 4 вывода данных, работающие в асинхронном режиме, и один тактовый вывод, имеющая очень большую пропускную способность. Если вы работаете с флеш картами, то лучше выбирать МК с данной модификацией, иначе скорость чтения/записи будет низкой.
Следует сразу отметить, что данная периферия может работать как в режиме ведущего (master) так и в режиме ведомого (slave). В случае ведущего, МК генерирует тактовый сигнал, а в режиме ведомого, генерирует сигнал другое внешнее устройство, например, другой МК. Также существует синхронный режим получения данных вместе с передачей по трем линиям, или обмен по двум линиям в двух стороннем режиме.
Наиболее часто данный протокол используется для обмена с датчиками. В этом случае МК является ведущим, датчик ведомым, и обычно используется синхронный режим по трем линиям. В этом случае передача данных происходит вместе с получением данных. Когда датчик передает данные, то, как правило, МК получает эти данные, а передавать может нули или следующую команду, в зависимости от настроек датчика. В такой синхронной передаче важно понимать работу флажков или прерываний. Как только вы положили данные в регистр данных, начинается копирование их во внутренний буфер передачи данных, и через какое то время МК уже может класть следующие данные, однако транзакция еще не завершена, и при работе в синхронном режиме, надо дождаться получения данных и только потом передавать следующие. Без прерываний, это работает не сложно, просто кладем байт, ждем когда получен байт и на этом конец транзакции.
//ждем окончания передачи когдаможно класть следующий байт while (SPI_GetFlagStatus(SPI_FLAG_TXE)== RESET) { } /* Write one byte in the SPI Transmit Data Register */ SPI_SendData(TxBuffer2[TxCounter]); /* Wait the byte is entirely received by SPI */ while (SPI_GetFlagStatus(SPI_FLAG_RXNE) == RESET) { } /* Store the received byte in the RxBuffer2 */ RxBuffer2[RxCounter++] = SPI_ReceiveData();
В случае с прерываниями, по сути, для МК это будет как бы асинхронная передача, свой буфер на прием и свой буфер на передачу. Буфер передачи будет на шаг впереди. Кладем второй байт, получаем первый, кладем третий, получаем второй и так пока не получим\передадим обе очереди.
Остальные режимы отличаются только параметрами регистров настройки. Настроек как всегда много. Можно задавать какой уровень является тактовым, что делать на линии во время простоя и т. д.
Отметим, что SPI протокол легко эмулируется обычными выводами (особенно на передачу данных), и с ним не сложно работать и без периферии, но с ней конечно удобнее. Также этот протокол практически всегда сразу работает и не имеет проблем в настройке, устойчив к различной скорости, не имеет такой сложной обработки исключительных ситуаций как наш следующий протокол I2C. Если есть возможность выбирать датчики, лучше брать на SPI протоколе, он гораздо удобнее и быстрее чем I2C, и менее капризный чем UART.
Отметим, что в данном протоколе используется также вывод CS — выбор датчика, с которым мы будем общаться. Он необходим даже в случае одного единственного датчика, для получения начального состояния. При включении питания все выводы МК находятся в неопределённом состоянии и датчик может зафиксировать случайные сигналы. Обмен с датчиком производится только при низком состоянии вывода CS.
I2C
Данный протокол широко используется в датчиках, микросхемах внешней памяти. Поэтому им обязательно надо научиться пользоваться. Если у вас есть готовый драйвер обмена по этому протоколу, то проблем нет, однако обычно производитель МК в библиотеках даёт только удобный доступ к регистрам периферии I2C, а полный драйвер надо писать самостоятельно или искать в сети. Полный драйвер написать сложно, он должен уметь обрабатывать все исключительные ситуации, и как правило в сети есть частичные драйвера (они конечно тоже работают, но могут иметь не корректную обработку редких случаев). Поэтому данную периферию очень часто ругают, что реализована она дескать криво и косо производителем МК. Отчасти и это верно, потому что, этот модуль обычно содержит и железные ошибки. Наиболее простой вариант использования данной периферии, когда МК является ведущим, а датчики ведомым. Он обычно работает отлично и отлажен. Ситуация наоборот, работает хуже, а самая сложная — смена ведущего в процессе работы, ещё хуже. Если у вас последний случай, то готовьтесь самостоятельно отлаживать данный протокол и дописывать драйвер. Вам обязательно понадобится внешний логический анализатор. Мы рассмотрим только частый случай, когда МК ведущий. Также, сразу отметим, что реализовывать программный вариант данного протокола это худший случай, выбирайте МК, имеющий данную периферию, если она вам нужна в проекте.
I2C — этот тот случай, когда работать с периферией лучше через прерывания. Однако надо иметь ввиду, что в этом случае прерывания должны иметь очень высокий приоритет! Почему? Сейчас вы это поймёте, но для начала разберёмся как производится обмен по данному протоколу.
Обмен производится по двум линиям — тактовый сигнал CLOCK (SCL) и данные DATA (SDA). Как обычно SCL задает ведущий, а вот данные в этом протоколе могут ходить в обе стороны, протокол двухсторонний. Протокол позволяется общаться сразу с несколькими устройствами находящимися на одной шине. Для этого у каждого ведомого устройства должен быть свой уникальный адрес, ведущему он не нужен. Классическая реализация использует 7 бит под адрес и 1 бит для определения направления данных — чтение (от ведомого к ведущему) или запись (от ведущего к ведомому) (существует расширенная версия 10 бит под адрес). В режиме простоя на обоих линиях должна быть логическая единица. Обмен начинается с того, что мастер формирует сигнал Старт (S), формируя ноль на линии SCL. Передача заканчивается сигналом STOP (P), переход от нуля к единице на линии SCL. Переда началом обмена, мастер обязательно должен проверить, что линия не занята — на SCL логическая единица. Биты считываются при высоком состоянии линии SCL, изменяются при низком состоянии SCL. Самая большая хитрость протокола состоит в 9-м бите — ACK\NACK.
Этот
бит подтверждает, что данные получены
или переданы. Некорректная обработка
этого бита может привести к зависанию
линии. Давайте посмотрим на примере,
как происходит обмен по данному протоколу.
Возьмём datasheet на датчик температуры
NCT75, в нем приведены диаграммы сигналов,
которые помогут понять, что происходит
при обмене по данному протоколу.
Как видно на картинке, начинается обмен с сигнала старт на линии. Далее МК передаёт адрес датчика и последний бит R\W (запись или чтение) — в данном случае запись, низкий уровень. После передачи адреса, если датчик с таким адресом есть на линии, то он отвечает сигналом ACK (низкий уровень), если нет ответа, то сигнал будет NACK (высокий уровень) и МК должен закончить передачу, подав сигнал стоп. Далее МК передаёт адрес регистра указателя с которым дальше он будет работать. Датчик опять отвечает ACK, что означается, что данные получены. МК завершает передачу сигналом стоп.
Если надо записать один байт в какой то регистр, то сначала надо выбрать адрес регистра, а потом не передавая сигнал стоп, сразу же передать записываемый байт, и завершить сигналом стоп. Таким образом, вроде все легко и понятно. Алгоритм простой: проверили линию, если линия свободна, подали сигнал старт, далее передали адрес и указали, что будем делать — записывать или читать. Далее получили ответ от датчика, что все хорошо. Передали записываемые данные и завершили передачу данных. Если делать это все руками, в основном цикле, то особенных проблем нет (в этом просто случае). Но если работает периферия I2C, то у нас получается асинхронный обмен с этой периферией. И если не успеть отреагировать на ответные сигналы ACK\NACK или не успеть передать их на линию, то вся система разрушится. По какой причине можно не успеть? Допустим мы работаем по прерываниями. МК передал адрес и получил ACK, пора передавать сам байт данных. Вы должны его положить в специальный регистр. МК вас оповестит и вызовет прерывание, в котором вы и должны это сделать. Но если у прерывания низкий приоритет, то его может прервать другое прерывание и вы продолжите чуть позже.. весь пакет может нарушится. В итоге, если вы пользуетесь прерываниями, то приоритет у I2C должен быть высоким. Особенно, если вы работаете на высоких скоростях (есть возможность работать на скоростях до 3МГц).
Теперь, рассмотрим этот процесс поподробнее со стороны периферии МК. Так как периферия работает асинхронно, то почти во всех МК, можно считать, что она работает через буфер. Разберём, на примере STM8, как работает передача данных.
На
данной схеме события, которые отслеживает
МК отмечены как EVx (на все эти события
может быть назначено прерывание).
Генерируем старт, записываем бит в
специальный регистр. EV5 — в статус
регистре выставляется флажок, что старт
сгенерирован. Это событие нужно для
того, чтобы вы положили байт с данными
(адрес) в регистр данных. Далее периферия
сама его отправит и проверит, что получен
ACK. EV6 — адрес корректно отправлен, если
он не было получен, то этого события не
будет. EV8_1 — Регистр данных пустой,
можно записывать данные, записываем
туда данные. EV8 — данные начали
передаваться, они лежат в буфере, а вот
регистр данных пустой — можно записать
следующий байт, хотя передача первого
ещё идёт. Как вы видите мы работает
наперёд, когда идёт передача первого
байта, уже записываем второй байт, и
т. д. Это позволяет МК работать на
быстрой скорости непрерывно. EV8_2 - в
конце мы проверяем, что байт передан и
получен, и генерируем сигнал стоп. Уже
видно возможные ошибки, если не обработать
корректно, что нет ответа при передаче
адреса от датчика, то можно ждать ответа
бесконечно. Если байт прошёл с ошибкой,
то надо начать сначала или повторить
байт. Все эти ситуации надо обрабатывать,
а это усложняет разработку драйвера.
Конечно на практике, это все можно не
обрабатывать, если у вас один два датчика,
то в ошибок скорее всего не будет. Но
вот если, например, в квадрокоптере по
I2C идёт общение с главным датчиком -
акселерометром, то из-за не обработанной
ошибки, он просто упадёт. В проекте
Paparazzi UAV, разработчики написали полноценный
драйвер, можете оценить его сложность.
С получением данных ещё сложнее. Проблема в том, что МК должен подтверждать полученные данные сигналом ACK, а последний байт NACK и потом стоп. Посмотрим диаграмму.
Начало
тут такое же. Передаём старт, потом
адрес. EV6 — говорит, что адрес передан,
а далее, EV7 — Rx буфер заполнен, можно
читать данные. Опять же, МК работает на
перед. Пока мы читаем данные DATA1, он уже
получает данные DATA2, поэтому, для того
чтобы корректно завершить получение
данных, необходимо действовать строго
по datasheet. А именно, в момент EV7_2, не считывая
данные из регистра данных, необходимо
дождаться статуса получения следующего
байта в буфер, BTF — передача байта
завершена. В этом случае, DATAN-2 будет в
DR регистре, DATAN-1 уже в буфере, и вот тут
надо уже установить признак NACK, так как
он будет относится к следующему байту
— то есть последнему! Потом надо прочитать
DR, там DATAN-2, и уже выставить статус STOP,
он будет относится опять же к следующему
байту, последнему, и после этого прочитать
DR опять, там DATAN-1. Остаётся получить
последний байт после статуса EV7. Выполнять
последние команды необходимо с
отключёнными прерываниями, чтобы никто
не нарушил данный процесс. Если же
посылка у нас всего из двух байт, то
ситуация меняется, и действовать надо
раньше, сразу после получения подтверждения
передачи адреса. А если мы читаем один
байт, то NACK надо выставлять до очистки
статуса получения адреса, а стоп
выставлять сразу после очистки статуса
получения адреса.
Вот так запутано работает данная периферия в режиме без прерываний. С прерываниями же все проще, достаточно перед получением последнего байта установить NACK и STOP в обработчике прерывания. Но прерывания должны иметь максимальный приоритет.
Дополнительно стоит отметить, что, есть ещё событие RESTART, вместо STOP и следом START, и некоторые датчики ждут полного завершения и старта заново, а некоторые сразу ждут RESTART между посылками.
С драйвером мы разобрались. Теперь осталось немного примеров, как работать с датчиками. Обычно все датчики работают по I2C по общему принципу. Сначала мы передаём в режиме записи номер регистра из которого необходимо прочитать данные, а потом в режиме чтения читаем нужное количество байт. С I2C удобно работать сразу в пакетном режиме. Создаём буфер (массив), и создаём процедуру, в которую передаём указатель на этот буфер, количество байт на запись, количество байт на чтение. Первый байт в буфере всегда адрес, второй байт — номер регистра, а дальше полученные данные. С такой процедурой общение по I2C становится гораздо удобнее. Так как протокол общения по I2C не очень быстрый (100-400кГц), то удобно также наш буфер снабдить функцией обработчиком данных, и как только они будут получены, то будет вызвана данная процедура. Примерно так выглядит обмен в этом случае.
buf[0] = addr; buf[1] = 0x45; readi2c(buff,1,2, &func);//передать 1 байт,прочитать 2 байта и вызвать функциюfunc…
Мы даём задание прочитать 2 байта по указателю 0x45 и вызвать функцию func, если все успешно. Далее в функции просто обрабатываем нужные данные.
EEPROM
Данный вид памяти используется для хранения энергонезависимых настроек и параметров вашей программы. Размер этой области не очень большой, но и хранить нам много не надо. Работа с EEPROM ведётся как с обычными переменными, по сути надо просто записать или прочитать данные из определённого адреса памяти. Производители МК специально защищают данную область памяти, делая её доступной только после определённых команд, чтобы случайно не стереть её по ошибке.
Таким образом, вы сначала должны разблокировать доступ к области памяти, далее записываете данные и опять блокируете. Обычно стирание памяти производится медленно, а запись быстрее. Поэтому производители МК специально делают две команды - стереть данные в памяти и записать данные в память. Также может быть одна объединённая команда, запись сразу со стиранием. При этом стёртая ячейка, как правило, содержит все 1, а не 0!
Разные МК имеют различные режимы записи в этот вид памяти. Практически у всех есть побайтный режим, может быть режим записи сразу двух байт, четырех, стирание может производится только постранично. Это все необходимо читать в datasheets.
В самом начале, мы повторяли, как можно любые данные (например, структуру данных) прочитать побайтно. Здесь, как раз, это может пригодится, чтобы запись или прочитать всю структуру данных нужной длины. Если в проекте у вас будет активная работа с данным видом памяти, то сделайте себе функции на запись и чтение нужного количества байт по указанному адресу.
Watchdog timer
Если делаете устройство, которое должно работать без вашего присутствия длительное время, то вам обязательно будет нужна данная периферия. Она необходима для дополнительного контроля за тем, что ваше устройство не зависло. Работает эта периферия очень просто. По сути это обычный таймер, который перезагружает МК, если дойдет до заданного числа. Вы включаете данный таймер, задаете его длительность, далее вам необходимо в любом месте программы (обычно в основном цикле), обнулять таймер до того, как он дойдет до заданного числа. Этим вы гарантируете, что МК работает как надо. Если что- то пошло не так, то вы не обнулите таймер и он вызовет перезагрузку МК.
Часто, еще бывает второй вид охранного таймера — оконный таймер. Его необходимо обновлять в строго определённый интервал времени, не слишком рано и не слишком поздно — то есть, в заданное окно времени. Такая схема позволяет отследить ситуацию, что какая-то часть программы выполняется слишком быстро или слишком долго, что говорит о сбое.
DMA
DMA — direct memory access — прямой доступ к памяти. Это очень полезная периферия. Она имеет очень много различных настроек. Здесь мы разберём только основные принципы её использования.
Данная периферия позволяет копировать данные из одной области памяти в другую, минуя центральный процессор. Вы даёте задание, скопировать один байт из такой-то ячейки памяти в другую. Совершенно бесполезная функция, можно же просто написать на Си пару команд. Однако ключевым здесь является, то, что процессор при это свободен, то есть копирование производится другим модулем, а процессор в это время выполняет свою работу. Давайте посмотрим на простом примере, как это можно использовать.
Допустим, нам необходимо воспользоваться ADC, и постоянно измерять напряжение на каком то выводе. Как это сделать мы рассматривали выше. Мы можем перевести ADC в непрерывный режим, включить прерывания и в обработчике прерывания написать:
volt = ADCDATA;
В переменную volt кладем данные из регистра ADC. Но, как вы понимаете, такая конструкция отвлекает от важных дел наш центральный процессор. Он должен прервать основную программу и выполнить эту одну команду. Вот, чтобы этого не делать, и нужна периферия DMA. Достаточно ей дать задание - копировать из области памяти регистра ADCDATA в область памяти переменной volt, по наступлению события — ADCREADY (ADC посчитал результат). Например, на STM8L это будет выглядеть так:
#define ADC1_DR_ADDRESS ((uint16_t)0x5344) #define BUFFER_SIZE ((uint8_t) 0x01) #define BUFFER_ADDRESS ((uint16_t)(&volt)) SYSCFG_REMAPDMAChannelConfig(REMAP_DMA1Channel_ADC1ToChannel0);//присоединяем событие окончания ADC кDMA каналу 0 DMA_Init(DMA1_Channel0, BUFFER_ADDRESS, //какой каналADC и куда ADC1_DR_ADDRESS, //откуд BUFFER_SIZE,//размер буфера DMA_DIR_PeripheralToMemory,//из другойпериферии в память DMA_Mode_Normal, //нормальный режим илимассив DMA_MemoryIncMode_Inc, DMA_Priority_High, DMA_MemoryDataSize_HalfWord); //включаем канал0 DMA_Cmd(DMA1_Channel0, ENABLE); //если нужно включаем прерывание //DMA_ITConfig(DMA1_Channel0, DMA_ITx_TC, ENABLE); //включаем DMA периферию DMA_GlobalCmd(ENABLE);
Один раз производим настройку, задаём откуда, куда, сколько байт, какими партиями копировать данные (по байту или 2 байту и т. д.) и запускаем в работу периферию. Теперь, когда ADC закончит вычисления, то он даст команду DMA и она скопирует данные в нашу переменную. Нам же останется просто работать с переменной volt, там будет всегда свежее значение данных.
Конечно, для такого простого случая DMA не нужен. Но возможности этого модуля гораздо шире. Он может работать с массивами данных, кольцевыми буферами, а это уже гораздо интереснее. Например, можно считывать данные последовательно с 4 каналов ADC и помещать их в массив. Можно запускать работу DMA по таймеру. Можно просто копировать большие объёмы данных без нагрузки на процессор. Можно перекладывать данные из одной периферии в другую, например, с UART в SPI. Можно получать данные с UART сразу в буфер. Можно передать массив данных сразу на UART или SPI.
Стоит отметить, что несмотря на то, что периферия не загружает процессор, у нее есть узкое место — и это сама память, к ней постоянно обращается процессор, и это не даёт быстро работать DMA. В современных МК, производители вводят несколько независимых областей оперативной памяти, и тогда, можно так построить программу, чтобы процессор не мешал работе DMA.
Генераторы кода
Современные микроконтроллеры становятся все сложнее. Количество периферии на борту растёт. Программировать их все сложнее и сложнее. Для облегчения процесса входа в новые версии МК, производители пишут специальные программы, которые могут сами писать код. Конечно не всю программу за вас, а только код инициализации периферии. Дополнительно эти программы имеют визуальный редактор выводов выбранного МК. Пользоваться ими очень удобно. Если вам не нужен код, то обязательно проверьте по этим программам правильно ли вы выбрали выводы. Как правило они корректно учитывают все возможные альтернативные комбинации выводов периферии.
Такие программы сейчас есть практически у всех линеек МК. Рассмотрим кратко основные.
STMCube
STM8CubeMX — это программа от компании ST предназначенная для конфигурирования STM8 серии МК. К сожалению, код она писать не умеет, но может посчитать энергопотребление вашей схемы. Также поможет рассчитать схему тактирования CPU и периферии. Однозначна полезная программа.
STM32CubeMX — серия для STM32. Этот продукт уже гораздо более серьёзный. По сути это не только конфигуратор, генератор кода, но ещё и набор библиотек для разных серий МК. Правда, к этим библиотекам до сих пор есть много нареканий, с точки зрения ошибок и производительности, но это очень удобный механизм. По сути вы получаете сразу готовый проект, со всеми библиотеками, с настроенной периферией, с обработчиками прерываний и т. д. Остаётся только писать свой код.
MPLab IDE
Микроконтроллеры компании Microchip можно настраивать сразу в среде разработки MPLab IDE. Среда отхватает серию PIC и dsPIC. Настройка производится на уровне конфигурирования библиотек. Ранее было отдельное решение MPLab VDI для визуальной настройки, но оно больше не поддерживается.
Существует онлайн верия IDE MPLab Express по данному адресу. Там же есть MPLab Code Configurator.
Все очень наглядно и удобно. Также можно добавить наличие большого количества доступных для разработки библиотек.
ATMEL Start
Компания ATMEL имеет он лайн конфигуратор доступный по этому адресу http://start.atmel.com/. Тут даже есть готовые примеры проектов. Генерация кода, готовый проект. Визуальный выбор периферии. Все, вплоть до покупки самого МК.
Если вы только начинаете работать с МК, то попробуйте этот конфигуратор.
Кроссплатформенная разработка
Каждый вид МК имеет свою периферию и свои регистры. Поэтому даже среди одного класса МК — STM32 — очень сложно обеспечить переносимость кода. В интернет есть специальные библиотеки, которые стараются учесть все различия и позволить писать общий код. Это очень важно для проектов под разнообразное железо.
Для серии ARM-Cortex M3-M0-M4 есть очень хорошая библиотека LibOpenCM3 для работы с периферией под GCC, распространяемая бесплатно. Она очень часто используется в Open-Source проектах. Поддержка большого количества ARM микроконтроллеров — STM32, NXP LPC1000, EXM32, Atmel SAM3U и другие. Библиотека хорошо документирована и позволяет писать кросс платформенные проекты. На ней написано очень много проектов, откуда смело можно брать код.
Для серии ATMEGA — существует проект Arduino, под который написано очень много готовых библиотек, работа со всевозможными датчиками, управление внешней памятью, чего там только нет. Arduino может работать на разных процессорах и код будет общий.
Учитывая, то что эти библиотеки и примеры написаны на Си, то их всегда можно скопировать с одного микроконтроллера на другой.
Если вы собрались делать проект под разные устройства, то стоит обратить внимание на эти библиотеки.
Также стоит отметить, что некую кроссплатформенность дают RTOS (операционные системы для микроконтроллеров реального времени). Но принципы программирования под них совершенно другие и в данной статье мы их не будем рассматривать.
Вообще же стоит рассчитывать на то, что вы пишите уникальный софт под конкретный микроконтроллер. Поэтому надо хорошо подумать, когда вы выбираете МК — хватит ли его на все ваши задачи. И не бойтесь использовать различные МК, выбирайте тот, который вам нужен, а не тот, который вы знаете.