В прошлой статье мы разобрали устройство платы Arduino, научились подключать ее к компьютеру и загрузили свой первый скетч! Называется он Blink и отвечает за мигание встроенного светодиода в плату с определенной частотой. Если вы начинающий, и только начали изучать мир Arduino, то вам некоторые функции в коде могут быть непонятны. Поэтому давайте разберем все по порядку. Структура кода в Arduino IDE – начинаем!
Программный код состоит из двух обязательных частей – функций. Первая часть называется void setup() (в переводе на русский – установка) и в ней прописывается код, который отработает всего один раз. Во второй обязательной функции void loop() прописывается код, работающий в бесконечном цикле. Сюда прописывается то, что будет происходить циклично (то есть с определенной частотой в определенный промежуток времени). Еще перед этими двумя функциями, то есть в самом начале кода, иногда прописываются различные директивы или переменные, которые будут необходимы для дальнейшей работы кода. Они будут постоянны на протяжении всего кода (это различные библиотеки, введенные названия устройств и т.д.)
Если перенестись на минутку в реальный мир, то эту структуру легко можно представить на простых вещах. Например, вы включаете свет в комнате, когда темно. Для этого, вы нажали на выключатель и лампочка загорелась. Также и в коде: сначала мы задаем постоянные данные, которые нам необходимы, то есть прописываем, что существует некое устройство (лампочка), прописываем для нее низкой значение
Таким образом, структура кода будет всегда одна и та же, вы ее сейчас видите ниже (от нее уже и нужно будет отталкиваться в дальнейшем, в зависимости от того, что вы хотите сделать и запрограммировать)
В нашем первом коде Blink есть некая функция PinMode(), а также delay() и digitalWrite()
Разберемся с каждой из них в отдельности.
В нашем коде встроенный светодиод мигает с частотой в одну секунду. Он подключается к выводу 13. В прошлом статье мы уже говорили, что на плате Arduino имеются выводы для подключения различных устройств и периферии – они делятся на цифровые и аналоговые. По отдельности их мы разберём чуть позднее, а пока скажем, что у каждого вывода есть номер.
На плате Arduino Uno вверху расположены выводы, пронумерованные от 0 до 13. Это цифровые выводы, к ним подключают обычно различные светодиоды. Однако, вывод номер 13 отвечает за работу встроенного светодиода на плате и служит, чаще всего, для проверки ее работоспособности. Указав в скетче номер вывода, мы можем работать с устройством, которое подключено к нему.
Функция PinMode() служит для настройки этого самого вывода. Все выводы на плате могут работать, как входы, так и выходы. Потому пользователь это также должен учитывать. По умолчанию, все контакты являются входами, но для светодиода нужен выходной сигнал, так как на него идет рабочая команда и подается напряжение.
Функция PinMode(pin, mode); состоит из двух значений. Это pin — номер вывода (например у нас светодиод подключен к выводу 13) и mode — режим входа/выхода (INPUT/OUTPUT)
void loop()
<
digitalWrite(13, HIGH);
delay(1000);
digitalWrite(13, LOW);
delay(1000);
>
На этом наш второй урок по программированию на Arduino подходит к концу. Сегодня мы познакомились со структурой кода на примере Blink, узнали о новых функциях и как их использовать, а впереди нас ожидает еще много всего интересного и познавательного. Всем удачной компиляции и отличного настроения!
Данная статья — является собственностью интернет-магазина РОБОТОТЕХНИКА
Источник: robototehnika.ru
Как выполнять параллельные задачи (Threads) в программе для Arduino
Arduino для начинающих. Начало работы
Инструкция по созданию параллельных потоков в программе для Arduino
1 Схема подключения для демонстрации потоков в работе с Arduino
Вообще говоря, Arduino не поддерживает настоящее распараллеливание задач, или мультипоточность. Но можно при каждом повторении цикла loop() указать микроконтроллеру проверять, не наступило ли время выполнить некую дополнительную, фоновую задачу. При этом пользователю будет казаться, что несколько задач выполняются одновременно.
Например, давайте будем мигать светодиодом с заданной частотой и параллельно этому издавать нарастающие и затихающие подобно сирене звуки из пьезоизлучателя. И светодиод, и пьезоизлучатель мы уже не раз подключали к Arduino. Соберём схему, как показано на рисунке.
Если вы подключаете светодиод к цифровому выводу, отличному от «13», не забывайте о токоограничивающем резисторе примерно на 220 Ом.
2 Управление светодиодом и пьезоизлучателемс помощью оператора delay()
Напишем вот такой скетч и загрузим его в Ардуино.
const int soundPin = 3; /* объявляем переменную с номером пина, на который подключён пьезоэлемент */ const int ledPin = 13; // объявляем переменную с номером пина светодиода void setup() pinMode(soundPin, OUTPUT); // объявляем пин 3 как выход. pinMode(ledPin, OUTPUT); // объявляем пин 13 как выход. > void loop() // Управление звуком: tone(soundPin, 700); // издаём звук на частоте 700 Гц delay(200); tone(soundPin, 500); // на частоте 500 Гц delay(200); tone(soundPin, 300); // на частоте 300 Гц delay(200); tone(soundPin, 200); // на частоте 200 Гц delay(200); // Управление светодиодом: digitalWrite(ledPin, HIGH); // зажигаем delay(200); digitalWrite(ledPin, LOW); // гасим delay(200); >
После включения видно, что скетч выполняется не совсем так как нам нужно: пока полностью не отработает сирена, светодиод не мигнёт, а мы бы хотели, чтобы светодиод мигал во время звучания сирены. В чём же здесь проблема?
Дело в том, что обычным образом эту задачу не решить. Задачи выполняются микроконтроллером строго последовательно. Оператор delay() задерживает выполнение программы на указанный промежуток времени, и пока это время не истечёт, следующие команды программы не будут выполняться. Из-за этого мы не можем задать разную длительность выполнения для каждой задачи в цикле loop() программы. Поэтому нужно как-то сымитировать многозадачность.
3 Параллельные процессы без оператора «delay()»
Вариант, при котором Arduino будет выполнять задачи псевдо-параллельно, предложен разработчиками Ардуино. Суть метода в том, что при каждом повторении цикла loop() мы проверяем, настало ли время мигать светодиодом (выполнять фоновую задачу) или нет. И если настало, то инвертируем состояние светодиода. Это своеобразный вариант обхода оператора delay().
const int soundPin = 3; // переменная с номером пина пьезоэлемента const int ledPin = 13; // переменная с номером пина светодиода const long ledInterval = 200; // интервал мигания светодиодом, мсек. int ledState = LOW; // начальное состояние светодиода unsigned long previousMillis = 0; // храним время предыдущего срабатывания светодиода void setup() pinMode(soundPin, OUTPUT); // задаём пин 3 как выход. pinMode(ledPin, OUTPUT); // задаём пин 13 как выход. > void loop() // Управление звуком: tone(soundPin, 700); delay(200); tone(soundPin, 500); delay(200); tone(soundPin, 300); delay(200); tone(soundPin, 200); delay(200); // Мигание светодиодом: // время с момента включения Arduino, мсек: unsigned long currentMillis = millis(); // Если время мигать пришло, if (currentMillis — previousMillis >= ledInterval) < previousMillis = currentMillis; // то запоминаем текущее время if (ledState == LOW) < // и инвертируем состояние светодиода ledState = HIGH; >else < ledState = LOW; >digitalWrite(ledPin, ledState); // переключаем состояние светодиода > >
Существенным недостатком данного метода является то, что участок кода перед блоком управления светодиодом должен выполняться быстрее, чем интервал времени мигания светодиода «ledInterval». В противном случае мигание будет происходить реже, чем нужно, и эффекта параллельного выполнения задач мы не получим. В частности, в нашем скетче длительность изменения звука сирены составляет 200+200+200+200 = 800 мсек, а интервал мигания светодиодом мы задали 200 мсек. Но светодиод будет мигать с периодом 800 мсек, что в 4 раза больше того, что мы задали.
Вообще, если в коде используется оператор delay(), в таком случае трудно сымитировать псевдо-параллельность, поэтому желательно его избегать.
В данном случае нужно было бы для блока управления звуком сирены также проверять, пришло время или нет, а не использовать delay(). Но это бы увеличило количество кода и ухудшило читаемость программы.
4 Использование библиотеки ArduinoThreadдля создания параллельных потоков
Чтобы решить поставленную задачу, воспользуемся замечательной библиотекой ArduinoThread, которая позволяет с лёгкостью создавать псевдо-параллельные процессы. Она работает похожим образом, но позволяет не писать код по проверке времени – нужно выполнять задачу в этом цикле или не нужно. Благодаря этому сокращается объём кода и улучшается читаемость скетча. Давайте проверим библиотеку в действии.
Первым делом скачаем с официального сайта архив библиотеки и разархивируем его в директорию libraries/ среды разработки Arduino IDE. Затем переименуем папку ArduinoThread-master в ArduinoThread.
Схема подключений останется прежней. Изменится лишь код программы.
#include // подключение библиотеки ArduinoThread const int soundPin = 3; // переменная с номером пина пьезоэлемента const int ledPin = 13; // переменная с номером пина светодиода Thread ledThread = Thread(); // создаём поток управления светодиодом Thread soundThread = Thread(); // создаём поток управления сиреной void setup() pinMode(soundPin, OUTPUT); // объявляем пин 3 как выход. pinMode(ledPin, OUTPUT); // объявляем пин 13 как выход. ledThread.onRun(ledBlink); // назначаем потоку задачу ledThread.setInterval(1000); // задаём интервал срабатывания, мсек soundThread.onRun(sound); // назначаем потоку задачу soundThread.setInterval(20); // задаём интервал срабатывания, мсек > void loop() // Проверим, пришло ли время переключиться светодиоду: if (ledThread.shouldRun()) ledThread.run(); // запускаем поток // Проверим, пришло ли время сменить тональность сирены: if (soundThread.shouldRun()) soundThread.run(); // запускаем поток > // Поток светодиода: void ledBlink() static bool ledStatus = false; // состояние светодиода Вкл/Выкл ledStatus = !ledStatus; // инвертируем состояние digitalWrite(ledPin, ledStatus); // включаем/выключаем светодиод > // Поток сирены: void sound() static int ton = 100; // тональность звука, Гц tone(soundPin, ton); // включаем сирену на «ton» Гц if (ton else < // по достижении 500 Гц ton = 100; // сбрасываем тональность до 100 Гц >>
В программе мы создаём два потока – ledThread и soundThread, каждый выполняет свою операцию: один мигает светодиодом, второй управляет звуком сирены.
В каждой итерации цикла для каждого потока проверяем, пришло ли время его выполнения или нет. Если пришло – он запускается на исполнение с помощью метода run(). Главное – не использовать оператор delay(). В коде даны более подробные пояснения.
Загрузим код в память Ардуино, запустим. Теперь всё работает в точности так, как надо!
Последнее изменениеПятница, 01 Ноябрь 2019 19:29 Прочитано 87741 раз
Поблагодарить автора:
Поделиться
Например:
if (что-то) myThread.enabled = true;
>
else myThread.enabled = false;
>
Возможно стоит подумать в этом направлении.
Леонид Кукленко 26.01.2020 03:12 Комментировать Здравствуйте. Скопировал ваш пример — сам по себе он работает хорошо. Заменил рабочие части обоих новых loop на свои (по отдельности они работают отлично) и программа выводит в монитор порта только информацию из участка setup. Есть ли какие-то ограничения на использование каких либо функций в новых loop (delay используется один раз)? И если кто-то сталкивался, подскажите, пожалуйста, как это решить.
aave1 26.01.2020 16:17 Комментировать Леонид, возможно в вашем скетче есть какой-то участок кода, из которого программа никогда не выходит. Без самого кода трудно сказать точнее. Можете разместить здесь хотя бы псевдокод участка, который не срабатывает?
Леонид Кукленко 26.01.2020 17:55 Комментировать Здравствуйте, aave1. Вот один из void к примеру, Обычный вольтметр, в цикле идет усреднение за период времени. Остальные части как в примере
int Uphre = A0;
float Uanalog = 0;
float Udigital[Kolichestvo_izmereniy];
float Uavgdigital = 0;
int i = 0;
void Dannie()
for(i=0; i
Леонид Кукленко 26.01.2020 17:57 Комментировать Kolichestvo_izmereniy; i++) Uanalog = analogRead(Uphre);
Udigital[i] = Uanalog*5.0/1024.0;
Uavgdigital += Udigital[i];
delay(Vremya_izmereniya_min*60000/Kolichestvo_izmereniy);
>
Serial.println(Uavgdigital/Kolichestvo_izmereniy);
Uavgdigital = 0;
>
Леонид Кукленко 26.01.2020 23:32 Комментировать Извините за беспокойство, ошибка была в разделе setup, действительно цикл мешал, спасибо за наводку
aave1 27.01.2020 18:03 Комментировать Рад, что вы разобрались! Я ещё не успел вникнуть в вашу проблему.
Сергей 05.07.2020 04:39 Комментировать понятия потока тут недопустимо, даже таймеры грузят МК, это вам не СТМ32, у которого конвейер.
aave1 06.07.2020 19:43 Комментировать Сергей, конечно, это не поток в классическом понимании, но тем не менее. Даже библиотека называетсяThreads.
Олег 14.10.2020 07:31 Комментировать Друзья, с отцом сварили ворота для двора дома, в электронике соображаю хорошо а вот с программированием все плохо, подскажите мне скетч, который можно использовать в моём случае
1.есть ворота и один двигатель переменного тока на 220 вольт.
2.есть релюхи для реверса двигателя.
3.есть симисторы и оптотранзисторы для управления симисторами
4. есть ардуино UNO
5. есть 4 датчика холла
Антон4ик 24.01.2021 19:37 Комментировать всем привет!
Хочу сразу представиться — я дилетант-любитель, которому стало интересна тема с ардуино. Поставил себе задачу сделать умную теплицу для начала. Как обычно бывает, хочется чтобы она могла всё из одной коробочки. На первом этапе хочу создать одновременную работу реле от датчика температуры и реле от датчика температуры. Столкнулся потребностью в многозадачности на ардуино.
Почитав тут и на Гидхабе про эту библиотеку, я создал скетч (точнее на что меня хватило), но что-то не работает. может подскажите что не так в скетче? Или обратить внимание на железо? Буду очень благодарен в помощи. P.S. Скетч прилагается:
#include «DHT.h» // подключение библиотеки DHT
#include «Thread.h» // подключение библиотеки ArduinoThread
long vlaga;
Thread vlagaThread = Thread(); // создаём поток управления датчиком движения
long Temperatura;
Thread TemperaturaThread = Thread(); // создаём поток управления градусником
void setup()
<
Serial.begin(9600);
vlaga = map(analogRead(A0),0,1023,0,100);
void loop()
Serial.println((String(«показания») + String(map(analogRead(A0),0,1023,0,100)) + String(» градусы») + String(dht_4.readTemperature())));
// Проверим, пришло ли время переключиться по земле:
if (vlagaThread.shouldRun())
vlagaThread.run(); // запускаем поток
// Проверим, пришло ли время температуры воздуха:
if (TemperaturaThread.shouldRun())
TemperaturaThread.run(); // запускаем поток
>
// Поток датчика земли:
void vlagaZemli()
<
if (vlaga = 30) digitalWrite(5,HIGH);
>
else digitalWrite(5,LOW);
>
>
aave1 24.01.2021 21:00 Комментировать Антон4ик, добрый день! Попробуйте в функции vlagaZemli() заменить «=» на «==». А также в функции loop() уберите вывод в монитор порта «Serial.println», т.к. там ошибка, или замените на
Serial.println(«показания» + (String)map(analogRead(A0),0,1023,0,100) + » градусы» + (String)dht_4.readTemperature());
Anton4ik 31.01.2021 07:22 Комментировать aave1,
Я воспользовался Вашими советами и сделал как рекомендовали, но у меня так и не переключается реле при изменении значения теипературы.
если будет возможность, подскажите пожалуйста, что не так со скетчем? Буду очень благодарен
P.s. не рубите с плеча сырое дарование
#include «DHT.h»
#include «Thread.h» // подключение библиотеки ArduinoThread
#include «Adafruit_Sensor.h»
long vlaga;
Thread vlagaThread = Thread(); // создаём поток управления датчиком движения
long Temperatura;
Thread TemperaturaThread = Thread(); // создаём поток управления градусником
void setup()
Serial.begin(9600);
vlaga = map(analogRead(A0),0,1023,0,100);
void loop()
Serial.println((String(«показания») + String(map(analogRead(A0),0,1023,0,100)) + String(» градусы») + String(dht_4.readTemperature())));
// Проверим, пришло ли время переключиться по земле:
if (vlagaThread.shouldRun())
vlagaThread.run(); // запускаем поток
// Проверим, пришло ли время температуры воздуха:
if (TemperaturaThread.shouldRun())
TemperaturaThread.run(); // запускаем поток
>
// Поток датчика температуры:
void TemperaturaVozduha() <
if (Temperatura >= 27) digitalWrite(7,1);
>
else digitalWrite(7,0);
>
>
// Поток датчика земли:
void vlagaZemli() <
if (vlaga >= 50) digitalWrite(3,LOW);
>
else digitalWrite(3,HIGH);
>
>
aave1 01.02.2021 08:39 Комментировать Anton4ik! Тут есть несколько ошибок.
1) задавать pinMode() не надо, т.к. влага и температура это не пины (ножки на плате), а просто переменные, которые хранят измеренные значения.
3) Самое главное — вы не назначаете потокам задачу. Вы создаёте поток и запускаете, но он не знает, что делать. Чтобы указать ему задачу, нужно добавить vlagaThread.onRun(vlagaZemli). Аналогично нужно дать задачу и потоку измерения температуры.
MikiBot 09.12.2021 11:40 Комментировать Недостатки замечательной библиотеки ArduinoThread:
Если остановить поток (thread.enabled = false;),
а потом, в нужный момент запустить (thread.enabled = true;),
то он НЕ начнется сначала, может тут же и закончится,
что не соответствует вашим ожиданиям.
Время изменить тоже не позволяет.
aave1 09.12.2021 18:31 Комментировать Спасибо за дополнение!
Игорь 16.02.2022 08:20 Комментировать Добрый день!
Многопоточность со светодиодами и звуковыми излучателями, это хорошо и просто.
У меня вопрос, как осуществить при такой многопоточности сравнение сигналов низкого уровня на двух и более входных портах от кнопок, чтобы при этом отрабатывали выходные порты один, два или три?
AlexeyHolik 06.07.2022 12:51 Комментировать Я хотел сделать часы на Ардуино. Я попытался воспользоваться вашей библиотекой Thread. Нихрена не работает. И вообще сколько бы я не устанавливал разных библиотек для разных скетчей, никогда ничего не работает. Похоже, я слишком тупой.
aave1 06.07.2022 19:08 Комментировать Алексей, ну зачем же вы так. Возможно из-за большого количества библиотек и бессистемного подхода ничего не получается. Попробуйте взяться за какую-нибудь одну и довести дело до конца. Например, эта библиотека точно проверена.
Но ещё дело может быть в том, что сами библиотеки постоянно обновляются, и статья, написанная несколько месяцев назад, уже может содержать устаревшие данные. К сожалению, это так. Но в статьях описан сам принцип и подход к данным вопросам. Просто наберитесь терпения, и всё у вас получится!
Источник: soltau.ru
В Arduino по-хардкорному, часть 2. Продолжаем программировать для ATmega 2560 на примере сигнализации
В прошлой статье мы кратко прошлись по архитектуре и инструментам разработки для ATmega 2560, написали простенькую программку и даже прошили ее в контроллер. Но, как ты понимаешь, Hello world — это только цветочки, попробую угостить тебя ягодками. Сегодня в меню: прерывания, работа с EEPROM, работа с UART и дискретными входами.
warning
Редакция и автор не несут ответственности за возможный причиненный вред здоровью и имуществу при несоблюдении техники безопасности работы с электроприборами.
Постановка задачи
Месяц назад мы сделали маленький main, который заставлял весело моргать светодиод на плате. В этот раз мы пойдем дальше, наш проект будет соответствовать следующим требованиям:
- устройство должно иметь два режима индикации: режим ожидания и «аварийный»;
- устройство должно переходить в аварийный режим, если замкнулись определенные контакты на плате, и возвращаться обратно при их размыкании;
- пользователь должен иметь возможность настроить индикацию по своему вкусу.
Так как сухая постановка задачи скучна, мы придумаем жизненную ситуацию. Например, пока ты сидишь и смотришь кино в наушниках, в холодильник на кухне проникает неизвестный враг и похищает оттуда колбасу. Никогда не сталкивался с таким? А ведь это очень возможно, и нужно быть готовым ко всему заранее! Нужна сигнализация. Для ее реализации дверь холодильника оборудуй контактным или магнитным датчиком (например, ИО-102-16/2, но на кустарном уровне сгодятся и два провода витой пары, приклеенные скотчем так, чтобы при закрытой дверце холодильника они замыкались), Arduino положи в комнате на видном месте, заведи провода от датчика к Arduino по следующей схеме (какой конкретно провод куда подключать — значения не имеет):
- один провод на колодке DIGITAL на любой из контактов GND ;
- второй провод на колодке DIGITAL на контакт 43 .
План решения задачи
Чтобы отслеживать, замкнуты ли провода, необходимо периодически опрашивать состояние входного сигнала на микроконтроллере.
В задаче указано, что пользователь должен иметь возможность настраивать индикацию. Воспользуемся памятью EEPROM микроконтроллера и при запуске будем читать оттуда настройки. А для записи настроек напишем небольшой интерактивный терминал, к которому можно подключиться любой терминальной программой по порту RS-232.
Встает вопрос: где взять RS-232 на Arduino и на компьютере? Все очень просто, если ты не потерял схему платы Arduino. USB-разъем платы Arduino заведен на UART0 микроконтроллера через микросхему‑преобразователь USB — COM (на самом деле, как ты помнишь, там стоит ATmega16U, он‑то и играет роль этого преобразователя).
Индикацию из основного цикла программы придется убирать, потому что время обработки команд у нас непредсказуемо, а значит, мы будем иметь проблемы с выдерживанием времени моргания светодиодом.
info
В реальной жизни для анализа состояния входных сигналов используют аппаратные и программные фильтры, так как мир неидеален и при размыкании/замыкании контактов возникает так называемый дребезг контакта.
Решение
Периферия
Напомню, что ATmega 2560 является представителем SoC. Это означает, что на одном кристалле микроконтроллер содержит различную периферию. Перечислю, что пригодится для решения нашей задачи (полный перечень, как всегда, в документации):
- таймеры — основное их предназначение, как несложно догадаться, — отсчитывать время и совершать какие‑то действия по результатам этих измерений. Очень часто таймеры просят генерировать прерывание при отмеривании какого‑то кратного интервала времени для реализации часов или определения задержек;
- UART — основной для небольших и маленьких микроконтроллеров канал связи с внешним миром;
- GPIO — самый базовый класс периферии, позволяющий напрямую работать с ногами микроконтроллера.
Ввод/вывод
Настройка таймера
Начнем, как принято, с конца :). Как ты понял, раз из основного цикла мы убираем управление светодиодом, то куда‑то его надо вставить. Самое логичное — это повесить обработчик прерывания по таймеру и в нем отсчитывать время зажигания или гашения светодиода, а также собственно зажигать/гасить светодиод. Приведу небольшой кусочек кода, который инициализирует таймер на генерацию прерывания один раз в 1 мс:
TCCR1A = 0x00 ;
TCCR1B = ( 1 < < WGM12 ) | ( 1 < < CS11 ) | ( 1 < < CS10) ;
TCCR1C = 0x00;
TCNT1 = 0x0000;
OCR1A = 250;
OCR1B = 0x0000;
OCR1C = 0x0000;
TIMSK1 |= ( 1 < < OCIE1A) ;
Что тут происходит? Чтобы не заниматься неудобными пересчетами в программе, проще настроить таймер на срабатывание раз в 1 мс. Таймеры в ATmega — штука крутая и имеют кучу возможностей, о которых ты можешь почитать в мануале, я выбрал режим работы таймера по сравнению со сбросом ( 1
После настройки таймера выставляй флаг разрешения прерывания по сравнению Timer1 TIMSK1 |= ( 1
Теперь объявляй обработчик прерывания, выглядит это следующим образом:
ISR ( TIMER1_ COMPA_ vect )
// Вызываем пользовательский обработчик прерывания по таймеру
user_ timer_ ISR ( ) ;
Таблица векторов прерываний
Вектор прерывания — это не что иное, как адрес, на который микроконтроллер передает управление при возникновении прерывания. По этому адресу располагается инструкция перехода в функцию — обработчик прерывания или (не обязательно) инструкция возврата из прерывания. Зачастую все среды разработки предлагают заполнять по умолчанию таблицу векторов прерываний инструкцией возврата из прерывания, чтобы при ошибке во время разработки (ты включил прерывание или забыл / не планировал писать обработчик) выполнение программы вернулось в нормальное русло. Таблица векторов прерываний располагается по младшим адресам Flash микроконтроллера. По нулевому адресу располагается так называемый Reset-вектор, на этот вектор микроконтроллер передает управление при старте или при перезагрузке.
Для универсальности я сделал вызов внешней функции, в нее ты потом можешь вынести различные вкусности, которые хочешь обрабатывать раз в миллисекунду. Я же поставил моргание светодиодом и анализ состояния дискретного входа (то есть проверка на замыкание/размыкание контактного датчика).
// Моргаем светодиодом
blink_ led ( ) ;
// Анализируем состояние дискретных входов
process_ input ( ) ;
Обработка состояния дискретного входа
Для начала надо инициализировать GPIO микроконтроллера, сказав ему, что ты хочешь использовать определенные ноги как получатель входных сигналов. Делается это следующим образом:
// Включаем внутреннюю подтяжку к «1»
PORTL | = _BV ( PL6 ) ;
// Настраиваем пин PL6 на вход
Что такое внутренняя подтяжка? Переключив ногу на вход, ты сказал микроконтроллеру, что он должен снимать сигнал с этой ноги. Но что делать, если нога «болтается в воздухе», то есть контактный датчик разомкнут?
info
Существуют микроконтроллеры, позволяющие делать подтяжку к обоим значениям: или к 1, или к 0.
Давай определимся для начала, какое состояние будет считаться замкнутым. Я выбрал замыкание ноги на GND . Сейчас объясню почему.
Микроконтроллер понимает только два состояния дискретного входа: к ноге приложено напряжение (логическая единица) или нога замкнута на землю (логический ноль), все остальное есть неопределенность. То есть каждый раз, считывая состояние, ты не можешь точно сказать, какое же оно на самом деле, и микроконтроллер может посчитать его как за 0, так и за 1.
Для разрешения этой дилеммы используют подтяжки к 1 (через ограничительный резистор замыкают на линию питания) или к 0 (замыкают ногу на землю), то есть задается значение по умолчанию. Таким образом, если нога «висит в воздухе», то, зная, к какому значению у тебя подтяжка, ты можешь точно подтвердить факт «нога в воздухе» и получить вполне определенное значение.
В ATmega 2560 (как и во всем семействе микроконтроллеров mega) существует функция внутренней подтяжки к 1, то есть микроконтроллер сам разрешает ситуацию «нога в воздухе». Теперь ты знаешь ответ на вопрос, зачем использовать состояние «замкнуто» контактного датчика как замыкание на GND. Если нога «в воздухе», то ты прочитаешь 1, если нога замкнута на землю, то ты прочитаешь 0.
Чтение состояния дискретного входа осуществляется чтением регистра PINx и маскированием соответствующего бита:
uint8_ t cur_ state = ( PINL
Дальше анализируем состояние и в зависимости от него устанавливаем нужный режим:
// Изменилось, начинаем моргать в соответствии с новым режимом
if ( cur_ state == 0 )
ch_ blink_ mode ( wm_ alarm ) ;
ch_ blink_ mode ( wm_ normal ) ;
warning
При работе с микроконтроллером постарайся убрать все металлические предметы, чтобы предотвратить случайное короткое замыкание и выход платы из строя.
А поговорить?
Теперь давай займемся реализацией терминала, то есть организуем общение с микроконтроллером. Как я уже упоминал, использовать мы будем UART0 . UART0 является периферийным устройством, и для указания микроконтроллеру, что ты хочешь именно его, необходимо произвести некоторые манипуляции, то есть настроить скорость, проверку четности, длину слова.
Для облегчения жизни я сделал небольшой макрос (он стандартный, ничего инновационного) для настройки регистра скорости:
#define UART_CALC_BR( br ) ( ( uint16_t )( ( F_CPU / ( 16UL * ( br) ) ) — 1 ) )
Настроим сам USART0 .
uint16_ t br = UART_ CALC_ BR ( 9600 ) ;
// Настройка скорости обмена
UBRR0H = br > > 8 ;
UBRR0L = br
// 8 бит данных, 1 стоп-бит, без контроля четности
UCSR0C = ( 1 < < USBS0 ) | ( 1 < < UCSZ01 ) | ( 1 < < UCSZ00 ) ;
// Разрешить прием и передачу данных
UCSR0B = ( 1 < < TXEN0 ) | ( 1 < < RXEN0 ) | ( 1 < < RXCIE0 ) ;
Не подскажете, как пройти в библиотеку?
Разработчики стандартных библиотек для встраиваемых решений позаботились о приближении этих библиотек к стандарту С/С++. Ключевое слово тут «приближение». Реализация многих функций достаточно урезана ввиду ограниченности ресурсов микроконтроллеров, а некоторые закрыты заглушками для совместимости.
Так, функции по обработке строк ( sscanf , sprintf и подобные) достаточно требовательны к использованию стека. При наличии на борту 4 Кб оперативной памяти это достаточно критично. Поэтому, если ты решишь использовать ту или иную функцию, читай описание к ней не в стандартных мануалах, а в документации на конкретную библиотеку.
Теперь позаботимся об удобном обмене данными. Так как у нас будет реализация терминала, я решил, что удобнее всего использовать стандартные библиотечные функции printf и fgets . Для того чтобы эти функции заработали в том виде, как они задуманы, необходимо создать свой поток и реализовать функции отправки и приема байта:
// Объявляем поток ввода/ вывода, который будем использовать для перенаправления stdio
static FILE uart_ stream = FDEV_ SETUP_ STREAM ( uart_ putc, uart_ getc, _ FDEV_ SETUP_ RW ) ;
А также перенаправить stdio в наш поток
stdout = stdin =
Рассмотрим чуть ближе прием байта из порта:
int uart_ getc ( FILE * file )
// Ждем, пока появятся данные в FIFO, если там ничего нет
while ( FIFO_ IS_ EMPTY ( uart_ rx_ fifo ) ) ;
__ builtin_ avr_ cli ( ) ; // Запрещаем прерывания
ret = FIFO_ FRONT ( uart_ rx_ fifo ) ;
FIFO_ POP ( uart_ rx_ fifo ) ;
__ builtin_ avr_ sei ( ) ; // Разрешаем прерывания
return ret ;
Для того чтобы функция gets работала, приходится ждать, пока очередной байт поступит в FIFO. При извлечении байт из FIFO рекомендуется отключать прерывания, чтобы во время извлечения байта не произошло прерывание и один ресурс (FIFO) не начали использовать с двух сторон.
Займемся отправкой байта.
int uart_ putc ( char c, FILE * file )
__ builtin_ avr_ cli ( ) ; // Запрещаем прерывания
if ( ! FIFO_ IS_ FULL ( uart_ tx_ fifo ) ) <
// Если в буфере есть место, то добавляем туда байт
FIFO_ PUSH ( uart_ tx_ fifo, c ) ;
// и разрешаем прерывание по освобождению передатчика
UCSR0B | = ( 1 < < UDRIE0 ) ;
ret = -1; // Буфер переполнен
__builtin_avr_sei(); // Разрешаем прерывания
return ret;
Вот тут встречается интересный момент: включение прерывания по освобождению передатчика. Так как не очень‑то хочется вручную складывать байты в передатчик, то воспользуемся прерыванием по очистке передатчика. Повесим на это прерывание обработчик, который кладет в передатчик очередной байт из FIFO, если в FIFO что‑то есть:
ISR ( USART0_ UDRE_ vect )
if ( FIFO_ IS_ EMPTY ( uart_ tx_ fifo ) ) <
// Если данных в FIFO больше нет, то запрещаем это прерывание
// Иначе передаем следующий байт
char txbyte = FIFO_FRONT( uart_tx_fifo ) ;
FIFO_POP( uart_tx_fifo ) ;
UDR0 = txbyte;
Для завершения картины приведу код обработчика по приему байта:
ISR ( USART0_ RX_ vect )
unsigned char rxbyte = UDR0 ;
if ( ! FIFO_ IS_ FULL ( uart_ rx_ fifo ) ) <
FIFO_ PUSH ( uart_ rx_ fifo, rxbyte ) ;
Отладка
Так как мы ограничиваемся только самими Arduino и не покупаем железный отладчик, то реализованный класс работы с UART удобно использовать для отладки твоей программы. Просто в нужных местах вставляешь трейс‑вывод с помощью printf . Только не увлекайся и помни про пожирание стека и памяти библиотечными функциями. Для вывода значения регистров и значения переменных в нужных местах этого вполне достаточно.
Хранение настроек
Вот мы и подошли к самому, с моей точки зрения, любопытному. Настройки хотелось бы хранить даже после перезагрузки или отключения питания устройства. Для этой цели в микроконтроллере есть EEPROM. Для работы с этой памятью присутствует библиотека eeprom. h . Можно пойти альтернативным путем и реализовать запись/чтение самостоятельно, это несложно. Но если есть уже готовое решение, то предлагаю им и воспользоваться.
Итак, в арсенале имеются функции eeprom_read_byte , eeprom_write_byte , eeprom_read_block , eeprom_write_block . У EEPROM есть одна особенность — она бывает занята, поэтому разработчики библиотеки (и в этом я к ним присоединяюсь) рекомендуют вызывать eeprom_busy_wait или проверять готовность функцией eeprom_is_ready .
Так как софт для микроконтроллера — это вещь автономная, то настоятельно рекомендуется всячески защищать настройки контрольными блоками от случайных или несанкционированных изменений. В нашем примере я использую один контрольный байт на блок настроек, который сигнализирует о том, что данные мною были записаны. Соответственно, если этот байт равен определенному значению (в нашем случае это 0), то это означает, что данные в блоке верны. Для безопасности перед записью этот байт я перевожу в состояние 1, после окончания записи возвращаю это значение в 0″. Данные манипуляции нужны для того, чтобы во время записи при внезапной перезагрузке микроконтроллер не загрузил мусор из памяти, а взял настройки по умолчанию.
danger
Статическое электричество смертельно для микросхем, избегай работы с микроконтроллерами в синтетической и шерстяной одежде, по возможности используй заземляющие браслеты.
3, 2, 1, поехали!
Итак, теперь прошиваем контроллер, запускаем терминал
#minicom -D `ls / dev/ serial/ by-id/ *arduino*` -c on -b 9600
Arduino выводит сообщение о старте и дает приглашение:
Для терминала я реализовал следующий набор команд (help не реализовывал):
Источник: xakep.ru