Blog/content/posts/2023/arduino/start.md
2023-03-10 21:15:56 +03:00

25 KiB
Raw Permalink Blame History

title date draft tags
📟 Начало работы с Arduino 2023-02-24T16:46:09+03:00 true
arduino
tips

⚠️ Статья носит информационный характер и находится в процессе написания.
Есть вероятность, что через некоторое время, части текста не будет.

Цифровые и аналоговые пины и их назначение

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

  1. Power Pins — порты питания, режим их работы нельзя запрограммировать или изменить.
    Они выдают стабилизированное напряжение 5V или 3,3V,
    Vin выдает напряжение от источника питания,
    а GND — это заземление (общий минус)
  2. PWM Pins — порты с ШИМ модуляцией, которые можно запрограммировать,
    как цифровой выход/вход. Данные порты обозначены на плате знаком тильда (˜)
  3. Analog In — порты, принимающие аналоговый сигнал от датчиков, работают на вход.
    Данные порты тоже можно запрограммировать, как цифровой вход/выход.
    Данные пины не поддерживают ШИМ модуляцию.

Режим пинов назначается в процедуре void setup с помощью pinMode(), например:

void setup() {
    pinMode(10, OUTPUT); // объявляем пин 10 как выход
    pinMode(A2, OUTPUT); // объявляем пин A2 как выход

    pinMode(12, INPUT); // объявляем пин 12 как вход
    pinMode(A1, INPUT); // объявляем пин A1 как вход
}

Пояснения к коду:

  1. К выходу 10 и A2 можно подключить светодиод, который будет включаться и выключаться при вызове команды в программе.
  2. Пин 10 может использоваться для ШИМ сигнала, например, чтобы плавно включить светодиод, а пин A2 может выдавать только цифровой сигнал (0 или 1)
  3. К входу 12 и A1 можно подключить цифровой датчик и микроконтроллер будет проверять наличие сигнала на этих пинах (логический нуль или единицу)
  4. К входу A1 можно подключить аналоговый датчик, тогда микроконтроллер будет получать не только сигнал, но и узнавать характеристику сигнала.

Для справки:

Пины не случайно разделены на пины с ШИМ модуляцией (PWM Pins) и аналоговые.
PWM пины создают аналоговый сигнал, к ним подключают сервопривод, шаговый двигатель и другие устройства, где требуется подавать сигнал с разными характеристиками.
Аналоговые пины (Analog In) используются для подключения аналоговых датчиков, с них входящий сигнал преобразуется в цифровой с помощью встроенного АЦП.

Использование аналоговых пинов как цифровые

При подключении большого количества устройств к плате пинов общего назначения может не хватить.
Тогда в скетче можно указыть, что необходимо использовать аналоговые пины как цифровые.
Также можно использовать не буквенное, а цифровое обозначение выходов, т.е. A0 — это 14 пин, A1 — это 15 пин и т.д. (работает только на Uno или Nano).

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

void setup() {
    pinMode(A2, OUTPUT); // объявляем пин A2, как цифровой выход
    pinMode(16, OUTPUT); // объявляем пин 16, как цифровой выход
}

Назначение пинов SDA, SCL

Данные пины используются для приема/передачи информации по протоколу I2C. Например, при подключении жк дисплея с модулем I2C или GPS модуля. С помощью специальной библиотеки микроконтроллер может обмениваться информацией с подключенным периферийным устройством, поддерживающим данный протокол. На Ардуино Mega, в отличии от Uno и Nano, имеется целых три пары пинов SDA и SCL.

Назначение пинов TX, RX

Пины TX/RX также используются для коммуникации, но уже по протоколу UART.
На платах Uni и Nano пины TX/RX подключены параллельно USB разъему для связи с компьютером. Поэтому, если подключиться к данным портам устройство, например, Bluetooth модуль, то загрузить в Ардуино скетч не получится, так как плата автоматически переключится на чтение данных с устройства, а не с компьютера.

Функции void loop() и void setup()

Функции void loop() и void setup() — это первое с чем сталкивается любой, кто начинает знакомство с языком программирования микроконтроллеров Ардуино.

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

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

Если пользователь не начал прошивку, то контроллер начинает выполнять ранее загруженный скетч.

Оба цикла вызываются встроенной функцией main() из файла main.cpp.
При этом функция void setup() вызывается один раз, а и void loop() вызывается в цикле for бесконечное количество раз.

Если в скетче будут присутствовать более одной функции void setup() или void loop(), то при компиляции Aduino IDE выдаст ошибку: redefinition of void setup() или redefinition of void loop() соответственно.

Функция void setup()

void setup() {
    // код должен располагаться между фигурных скобок
}

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

Процедура void setup() вызывается один раз и её используют для назначения режима работы пинов или команд, которые необходимо выполнить только в момент загрузки программы.

void loop()

После выполнения цикла setup, программа переходит в цикл loop, который будет повторяться до тех пор, пока на плату подано питание.

Если цикл содержит одну команду, то она будет выполняться тысячи раз в секунду.

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

Функция loop*() имеет следующую конструкцию:

void loop() {
    // основной код программы располагается здесь
}

Таким образом, если необходимо при запуске программы включить светодиод один раз (для индикации работы устройства) на микроконтроллере Arduino Nano, то команду лучше написать в цикле void setup().

Если в программе необходимо выполнять какое-то действие постоянно, например, получать данные с ультразвукового дальномера HC-SR04, то команду следует располагать в цикле void loop().

Функция pinMode()

Функция pinMode() устанавливает режим работы заданного пина, как входа или выхода.

Цифровой пин может находиться в двух состояниях. В режиме входа пин считывает напряжение от 0 до 5 Вольт, а в режиме выхода выдавать на пине такое же напряжение.

Режим работы пина микроконтроллера выбирается при помощи функции pinMode(pin, mode), где pin это номер пина, а mode это режим.

pinMode OUTPUT

OUTPUT (порт работает как выход) — пин становится управляемым источником питания с максимальным током 40 мА.

В зависимости от команды digitalWrite() пин принимает значение единицы или нуля.
Пример: pinMode(10, OUTPUT).

Функция digitalWrite() и analogWrite()

Цифровой пин может генерировать цифровой сигнал с помощью команды digitalWrite(), т.е. выдавать напряжение 5 Вольт.

Цифровой сигнал может иметь два значения — 0 или 1 (0 Вольт или 5 Вольт).

Если в программе используется команда analogWrite() для ШИМ портов платы, то микроконтроллер может генерировать сигнал PWM на портах — создавать имитацию аналогового сигнала.

void setup() {
    pinMode(10, OUTPUT);
}

void loop() {
    digitalWrite(10, HIGH);
    delay(250);
    digitalWrite(10, LOW);
    delay(250);
}

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

pinMode INPUT

INPUT (порт работает как вход) — пин в этом режиме считывает данные с аналоговых и цифровых датчиков, состояния кнопок. Порт находится в высокоимпедансном состоянии, т.е. у пина высокое сопротивление.
Пример: pinMode(10, INPUT).

Функция digitalRead() и analogRead()

Arduino может определить наличие напряжения на пине через функцию digitalRead(), которая возвращает 0 (LOW) или 1 (HIGH).

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

Используя функцию analogRead(), можно прочитать напряжение с аналогового датчика, функция возвращает число от 0 до 1023.

void setup() {
    pinMode(10, INPUT);
    Serial.begin(9600);
}

void loop() {
    int data = digitalRead(10);
    Serial.println(data);
    delay(250);
}

Нельзя подавать на вход микроконтроллера напряжение выше напряжения питания платы.

Кроме того, для аналоговых выводов можно использовать команды digitalRead() и digitalWrite().
В этом случае аналоговые порты будут считывать (digitalRead) или выдавать (digitalWrite) цифровой, а не аналоговый сигнал.

pinMode INPUT_PULLUP

INPUT_PULLUP (порт работает как вход) но к пину подключается резистор в 20 кОм.

В этом режиме при подключении кнопки к Ардуино можно не использовать внешние подтягивающие резисторы.
Пример: pinMode(10, INPUT_PULLUP).

Директива #define

Директива #define позволяет задавать имена значениям (константам), которые делают скетч более понятным.

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

Рассмотрим на примерах с описанием правильные варианты использования директивы #define.

Константы, которые определены через директиву #define, не занимают места в памяти микроконтроллера, так как значение подставляет значения вместо имен при компиляции скетча.

Работу директивы можно сравнить с операцией НАЙТИ и ЗАМЕНИТЬ в текстовом редакторе.

При компиляции скетча компилятор находит в программе часть кода <что меняем> и заменяет ее на кусок кода <на что меняем>.

Синтаксис директивы:

#define <что меняем> <на что меняем>

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

Обрати внимание:

В конце строчки не ставится точка с запятой и знак равенства, как это делается при объявлении переменной.

#define RED 11 // присваиваем имя RED для пина 11
#define GRN 12 // присваиваем имя GRN для пина 12
#define BLU 13 // присваиваем имя BLU для пина 13
 
void setup() {
    pinMode(RED, OUTPUT); // используем PIN 11 для вывода
    pinMode(GRN, OUTPUT); // используем PIN 12 для вывода
    pinMode(BLU, OUTPUT); // используем PIN 13 для вывода
}

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

А программа автоматически будет заменять имена RED, GRN, BLU на соответствующие значения при компиляции.

Директивы #ifdef, #ifndef и #endif в скетче

Инструкция #ifdef проверят, было ли встречено в программе данное определение ранее, если было, то ставится блок кода с последующей строки и до #endif.

В примере проверяется был ли ранее в #define определен признак отладки, если да, то код (вывод сообщения на монитор порта) будет выполнен, если признак не определен, то сообщение на мониторе выводиться не будет.

#ifdef DEBUG
   Serial.println("Debug message: ...");
#endif

Инструкция #ifndef проверят, было ли встречено в программе данное определение ранее и если не было, то вставится блок кода с последующей строки и до #endif.

В следующем простом примере объявляется новая константа, если только ранее её не объявляли в скетче.

Если дефайн с таким именем уже использовался, то программа проигнорирует строчки внутри конструкции #ifndef … #endif.

#ifndef RED
    #define RED 11
#endif

Замена функций с помощью #define

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

Это более сложный, но интересный вариант использования #define, который позволяет создать много разных упрощающих инструкций в скетче.

Например, можно в первом примере заменить функцию pinMode() на конструкцию с заданными параметрами.

#define out(pin) pinMode(pin, OUTPUT)
 
void setup() {
    out(11);
    out(12);
    out(13);
}

В примере функция pinMode() закодирована одним словом out.

Теперь, где в скетче встретится слово out, компилятор подставит строку pinMode(pin, OUTPUT) с заданным параметром pin.

Таким же образом можно заменить команды digitalWrite() и delay().

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

#define out(pin) pinMode(pin, OUTPUT)
#define on(pin, del) digitalWrite(pin, HIGH); delay(del)
#define off(pin, del) digitalWrite(pin, LOW); delay(del)
 
void setup() {
    out(11);
    out(12);
    out(13);
}

void loop() {
    on(11, 500);
    off(11, 500);
    on(12, 500);
    off(12, 500);
    on(13, 500);
    off(13, 500);
}

define или const, что выбрать?

Иногда бывает не удобно применять директиву #define для создания констант, в этом случае используют ключевое слово const.

В отличие от глобальных переменных, значение const должно быть определено сразу при объявлении константы.

Необходимо помнить, что при использовании #define имена следует делать максимально уникальными, чтобы не было совпадений с командами из подключаемых библиотек.

const int RED = 11; // присваиваем имя RED для пина 11
const int GRN = 12; // присваиваем имя GRN для пина 12
const int BLU = 13; // присваиваем имя BLU для пина 13
 
void setup() {
    pinMode(RED, OUTPUT); // используем PIN 11 для вывода
    pinMode(GRN, OUTPUT); // используем PIN 12 для вывода
    pinMode(BLU, OUTPUT); // используем PIN 13 для вывода
}

Для справки:

Использование #define или const не дает никаких преимуществ, с точки зрения экономии объема памяти микроконтроллера.

Задержки, delay() и millis()

delayMicroseconds()

Функция delayMicroseconds() останавливает выполнение программы на заданное количество микросекунд (в 1 секунде 1 000 000 микросекунд).

При необходимости задержки в программе более чем на несколько тысяч микросекунд рекомендуется использовать delay().

delay()

Функция delay() останавливает выполнение программы на заданное количество миллисекунд (в 1 секунде 1 000 миллисекунд).

Во время задержки программы с помощью функции delay(), не могут быть считаны подключенные к плате датчики или произведены другие операции, например, запись в EEPROM данных.

В качестве альтернативы следует использовать функцию millis().

millis()

Функция millis() возвращает количество прошедших миллисекунд с момента начала выполнения программы.

Счетчик времени сбрасывается на ноль при переполнении значения unsigned long (приблизительно через 50 дней).

Функция millis() позволяет сделать многозадачность в ардуинке, так как выполнение программы не останавливается и можно выполнять параллельно другие операции в скетче.

unsigned long timer = 0;
bool led_state = HIGH;

void setup() {
    pinMode(LED_BUILTIN, OUTPUT);
    Serial.begin(9600);
}
 
void loop() {
    if (millis() - timer > 200) {
      led_state =! led_state;
      digitalWrite(LED_BUILTIN, led_state);

      timer = millis();
    }

    // выводим количество миллисекунд прошедших с момента начала программы
    Serial.print("Time: ");
    Serial.println(timer);
}

tone()

// FIXIT: https://xn--18-6kcdusowgbt1a4b.xn--p1ai/tone-arduino/

map()

// FIXIT: https://xn--18-6kcdusowgbt1a4b.xn--p1ai/map-arduino/

random()

// FIXIT: https://xn--18-6kcdusowgbt1a4b.xn--p1ai/random-arduino/

Serial Monitor

// FIXIT: https://xn--18-6kcdusowgbt1a4b.xn--p1ai/%d0%bc%d0%be%d0%bd%d0%b8%d1%82%d0%be%d1%80-%d0%bf%d0%be%d1%80%d1%82%d0%b0-%d0%b0%d1%80%d0%b4%d1%83%d0%b8%d0%bd%d0%be/

Используемые материалы:

  1. Уроки по Arduino | робототехника18.рф