Blog/content/posts/2023/arduino/start.md

501 lines
25 KiB
Markdown
Raw Normal View History

2023-02-24 20:13:21 +03:00
---
title: "📟 Начало работы с Arduino"
date: 2023-02-24T16:46:09+03:00
draft: true
tags: [arduino, tips]
---
⚠️ Статья носит информационный характер и находится в процессе написания.
Есть вероятность, что через некоторое время, части текста не будет.
## Цифровые и аналоговые пины и их назначение
Все пины можно разделить на несколько видов,
различие будет только в **количестве** данных выводов на различных платах.
1. **Power Pins** — порты питания, режим их работы нельзя запрограммировать или изменить.
Они выдают стабилизированное напряжение 5V или 3,3V,
2023-03-10 21:15:56 +03:00
`Vin` выдает напряжение от источника питания,
а `GND` — это заземление (общий минус)
2023-02-24 20:13:21 +03:00
2. **PWM Pins** — порты с ШИМ модуляцией, которые можно запрограммировать,
как цифровой выход/вход. Данные порты обозначены на плате знаком тильда (˜)
3. **Analog In** — порты, принимающие аналоговый сигнал от датчиков, работают на вход.
Данные порты тоже можно запрограммировать, как цифровой вход/выход.
Данные пины не поддерживают ШИМ модуляцию.
Режим пинов назначается в процедуре `void setup` с помощью `pinMode()`, например:
```c
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).
В следующем примере две строчки имеют одинаковое значение.
```c
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()`
```c
void setup() {
// код должен располагаться между фигурных скобок
}
```
Фигурные скобки указывают, где начало и конец цикла,
поэтому все команды должны располагаться между ними.
Процедура `void setup()` вызывается один раз
и её используют для назначения режима работы пинов или команд,
которые необходимо выполнить только в момент загрузки программы.
### `void loop()`
После выполнения цикла `setup`, программа переходит в цикл `loop`,
который будет повторяться до тех пор, пока на плату подано питание.
Если цикл содержит одну команду, то она будет выполняться тысячи раз в секунду.
Если написать скетч для мигания светодиодом на Arduino,
то необходимо добавлять в код задержку для выполнения программы,
иначе мигания светодиода не будет заметно.
Функция `loop*()` имеет следующую конструкцию:
```c
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** на портах —
создавать имитацию аналогового сигнала.
```c
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`.
```c
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`,
не занимают места в памяти микроконтроллера,
так как значение подставляет значения вместо имен при компиляции скетча.
Работу директивы можно сравнить с операцией `НАЙТИ` и `ЗАМЕНИТЬ` в текстовом редакторе.
При компиляции скетча компилятор находит в программе часть кода `<что меняем>`
и заменяет ее на кусок кода `<на что меняем>`.
**Синтаксис директивы:**
```c
#define <что меняем> <на что меняем>
```
При использовании директивы следует избегать использования имени другой переменной,
константы или команды ардуино, иначе оно оно будет заменено при компиляции.
**Обрати внимание:**
В конце строчки не ставится точка с запятой и знак равенства,
как это делается при объявлении переменной.
```c
#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` определен признак отладки,
если да, то код (вывод сообщения на монитор порта) будет выполнен,
если признак не определен, то сообщение на мониторе выводиться не будет.
```c
#ifdef DEBUG
Serial.println("Debug message: ...");
#endif
```
Инструкция `#ifndef` проверят, было ли встречено в программе данное определение ранее
и если не было, то вставится блок кода с последующей строки и до `#endif`.
В следующем простом примере объявляется новая константа,
если только ранее её не объявляли в скетче.
Если дефайн с таким именем уже использовался,
то программа проигнорирует строчки внутри конструкции `#ifndef … #endif`.
```c
#ifndef RED
#define RED 11
#endif
```
### Замена функций с помощью `#define`
Кроме использования дефайн в программе для объявления констант,
можно заменять целые фрагменты кода с помощью директивы `#define`.
Это более сложный, но интересный вариант использования `#define`,
который позволяет создать много разных упрощающих инструкций в скетче.
Например, можно в первом примере заменить функцию `pinMode()`
на конструкцию с заданными параметрами.
```c
#define out(pin) pinMode(pin, OUTPUT)
void setup() {
out(11);
out(12);
out(13);
}
```
В примере функция `pinMode()` закодирована одним словом `out`.
Теперь, где в скетче встретится слово `out`,
компилятор подставит строку `pinMode(pin, OUTPUT)` с заданным параметром `pin`.
Таким же образом можно заменить команды `digitalWrite()` и `delay()`.
Используя RGB светодиод или три обычных светодиода с ардуинкой
можно проверить работу следующего примера скетча с директивой дефайн.
```c
#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` имена следует делать
максимально уникальными, чтобы не было совпадений с командами из подключаемых библиотек.
```c
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()` позволяет сделать многозадачность в ардуинке,
так как выполнение программы не останавливается
и можно выполнять параллельно другие операции в скетче.
```c
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()`
```text
// FIXIT: https://xn--18-6kcdusowgbt1a4b.xn--p1ai/tone-arduino/
```
## `map()`
```text
// FIXIT: https://xn--18-6kcdusowgbt1a4b.xn--p1ai/map-arduino/
```
## `random()`
```text
// FIXIT: https://xn--18-6kcdusowgbt1a4b.xn--p1ai/random-arduino/
```
## Serial Monitor
```text
// 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](https://xn--18-6kcdusowgbt1a4b.xn--p1ai/%d0%bf%d0%b8%d0%bd%d1%8b-%d0%b0%d1%80%d0%b4%d1%83%d0%b8%d0%bd%d0%be/) | робототехника18.рф