Перейти к основному содержимому

Переменные

Два входных значения, состояние программы и две команды приводам

Роботу мало получить сигнал — значение нужно сохранить, сравнить и передать следующей части программы. Переменная даёт данным имя и место хранения. Благодаря ей код говорит не «возьмите число из неизвестного места», а «проверьте расстояние», «увеличь счётчик» или «запомни, разрешено ли движение».

Коротко

Переменная в C++ имеет тип, имя и текущее значение. Тип определяет допустимые значения и операции, имя объясняет назначение, а присваивание обновляет состояние. Для неизменяемых настроек используют const, а для значений времени на Arduino часто выбирают unsigned long.

Зачем это нужно

В одном проходе loop() контроллер может прочитать левый и правый датчики, вычислить ошибку положения, выбрать скорость двух моторов и запомнить время предыдущего действия. Без переменных эти данные невозможно удобно связать.

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

Хорошие переменные помогают:

  • отличать измерение от порога;
  • видеть единицу прямо в имени;
  • хранить состояние между проходами loop();
  • изменять настройку в одном месте;
  • печатать внутренние данные в Serial;
  • находить несоответствие типов до испытания устройства.

Главная идея

Объявление сообщает компилятору: «создайте объект этого типа с таким именем». Инициализация сразу задаёт начальное значение:

int distanceCm = 42;
bool obstacleDetected = false;

Позже присваивание меняет уже существующий объект:

distanceCm = 18;
obstacleDetected = true;

Левая часть = — куда записать, правая — какое значение вычислить. Оператор = не означает математическое равенство. Для сравнения на равенство применяется ==.

ЧастьПримерЧто сообщает
типintкакие значения и операции ожидаются
имяdistanceCmкакую величину хранит объект
инициализатор42начальное значение
присваиваниеdistanceCm = 18;новое значение существующей переменной

Основные типы для первого скетча

C++ предоставляет целые, вещественные, логические и символьные типы. Их точный диапазон зависит от типа и платформы. На классической Arduino Uno тип int обычно 16-битный, а на многих современных Arduino-платформах — 32-битный. Если границы критичны для протокола или расчёта, не переносите предположение между платами: сверяйте документацию платформы и используйте целочисленные типы фиксированной ширины из <stdint.h>.

ТипПримерДиапазон на Arduino UNOПодходящее применениеОграничение
boolbool enabled = true;false или trueсостояние «да/нет»не хранит несколько режимов
bytebyte pwm = 120;от 0 до 255байт данных, короткое неотрицательное значениене хранит отрицательные числа
charchar command = 's';от -128 до 127один символ командыне равен строке "s"
intint distanceCm = 25;от -32768 до 32767небольшое целое со знакомна UNO уже, чем на многих новых платах
unsigned intunsigned int samples = 0;от 0 до 65535неотрицательный счётчик в подходящем диапазонепереполнение циклическое
longlong encoderPosition = -1200;от -2147483648 до 2147483647более широкий счётчик со знакомзанимает больше памяти, чем int
unsigned longunsigned long nowMs = millis();от 0 до 4294967295время millis() и интервалыпереполняется, но разность можно обработать корректно
floatfloat voltage = 4.82f;примерно от -3.4E38 до 3.4E38приближённая вещественная величинаоколо 6-7 значащих цифр
doubledouble k = 1.25;как float на Arduino UNOсовместимость с кодом, где ожидают doubleна UNO не даёт большей точности

Не выбирайте float только потому, что величина физическая. Расстояние в миллиметрах, напряжение в милливольтах и коэффициент в тысячных нередко удобно хранить целыми числами: единица явно задана, а поведение вычислений легче проверить.

Тип — это ещё и договор

bool motorEnabled говорит читателю, что ожидаются два состояния. unsigned long lastUpdateMs показывает неотрицательное значение времени и единицу. Компилятор проверяет часть договора, а осмысленное имя — человек.

Имена и единицы измерения

Имена x, a1 и value допустимы синтаксически, но плохо объясняют устройство. Сравните:

int value = 20;
int stopDistanceCm = 20;

Вторая строка сообщает назначение и единицу. Это предотвращает смешивание сантиметров с миллиметрами и сырого значения АЦП с рассчитанным напряжением.

Полезные формы:

  • leftSensorRaw — сырое значение левого датчика;
  • batteryVoltageMv — напряжение в милливольтах;
  • lastCommandMs — время команды по millis();
  • motorsEnabled — логическое состояние;
  • targetSpeedPercent — заданная относительная скорость.

Имена C++ чувствительны к регистру: distanceCm и distanceCM — разные идентификаторы. Не используйте зарезервированные слова языка и имена, конфликтующие с библиотеками.

const: настройка, которую не меняют

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

const int sensorPin = A0;
const int stopDistanceCm = 20;
const unsigned long reportIntervalMs = 500;

Попытка позже присвоить новое значение такому объекту приведёт к ошибке компиляции. Это полезнее комментария «не изменять»: ограничение проверяется инструментом.

const не означает, что величина физически неизменна. Порог — настройка программы, а показание датчика меняется:

const int sensorPin = A0;
int sensorRaw = 0;

void loop() {
sensorRaw = analogRead(sensorPin);
}
ДанныеМеняются во время работы?Подходящая форма
номер контактаобычно нетconst int sensorPin
порог реакциинет, если скетч его не перенастраиваетconst int threshold
текущее измерениедаобычная переменная
счётчик событийдаобычная переменная подходящего диапазона
факт разрешения движениядаbool

Область видимости и время жизни

Переменная, объявленная внутри функции или блока, доступна в ограниченной области:

void loop() {
const int sensorRaw = analogRead(A0);

if (sensorRaw > 700) {
const bool alarm = true;
Serial.println(alarm);
}
}

sensorRaw доступна до конца loop(), а alarm — только внутри блока if. Это снижает риск случайно использовать временное значение в другой части программы.

Глобальную переменную объявляют вне функций. Она существует всё время работы скетча и доступна последующим функциям файла:

unsigned long eventCount = 0;

void setup() {
Serial.begin(9600);
}

void loop() {
eventCount = eventCount + 1;
Serial.println(eventCount);
delay(1000);
}

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

Счётчик и обновление значения

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

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

Увеличить счётчик можно несколькими эквивалентными для обычной переменной способами:

pulseCount = pulseCount + 1;
pulseCount += 1;
++pulseCount;

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

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

unsigned long pulseCount = 0;

void setup() {
Serial.begin(9600);
}

void loop() {
const bool simulatedPulse = true;

if (simulatedPulse) {
++pulseCount;
}

Serial.println(pulseCount);
delay(500);
}

Переполнение и время

У целого типа конечный диапазон. Беззнаковое значение после максимума переходит к нулю по модульной арифметике. Для времени millis() это ожидаемое поведение, поэтому интервалы проверяют вычитанием:

unsigned long previousMs = 0;
const unsigned long intervalMs = 1000;

void loop() {
const unsigned long nowMs = millis();

if (nowMs - previousMs >= intervalMs) {
previousMs = nowMs;
Serial.println("tick");
}
}

Сравнение nowMs >= previousMs + intervalMs хуже переносит переход счётчика через максимум. Разность беззнаковых значений сохраняет смысл прошедшего интервала, если интервал выбран в корректной половине диапазона и код вызывается достаточно регулярно.

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

Пример: состояние робота по двум датчикам

const int leftSensorPin = A0;
const int rightSensorPin = A1;
const int darkThreshold = 600;

int leftSensorRaw = 0;
int rightSensorRaw = 0;
bool lineDetected = false;

void setup() {
Serial.begin(9600);
}

void loop() {
leftSensorRaw = analogRead(leftSensorPin);
rightSensorRaw = analogRead(rightSensorPin);
lineDetected =
leftSensorRaw > darkThreshold || rightSensorRaw > darkThreshold;

Serial.print("line_detected=");
Serial.println(lineDetected);
}

Названия разделяют контакты, сырые показания, настройку порога и итоговое логическое состояние. Следующий урок добавит ветви, которые выберут действие по lineDetected.

Что запомнить о переменных

  • Переменная объединяет тип, имя и изменяемое значение.
  • Инициализация задаёт начальное состояние; присваивание обновляет его.
  • Тип выбирают по смыслу, диапазону и платформе.
  • Единица в имени делает физические значения проверяемыми.
  • const защищает настройку от случайного присваивания.
  • Область видимости стоит делать как можно уже.
  • Время millis() хранят в unsigned long и сравнивают разностью.

Практика

  1. Объявите переменные для сырого показания датчика, расстояния в миллиметрах, состояния препятствия и времени последнего отчёта. Обоснуйте тип каждого объекта.
  2. Перепишите набор безымянных чисел как именованные const-настройки с единицами. Попробуйте изменить одну константу присваиванием и изучите сообщение компилятора.
  3. Создайте счётчик проходов loop() и печатайте его с ограниченной частотой. Наблюдайте рост и оцените, когда выбранный тип исчерпает диапазон.
  4. Составьте функциональную модель двух датчиков и двух команд моторам. Запишите переменные входов, состояния и выходов, не подключая моторы напрямую к контроллеру.

Проверьте себя

  1. Чем объявление отличается от присваивания?
  2. Почему = и == нельзя менять местами?
  3. Когда подходит тип bool?
  4. Почему диапазон int нельзя считать одинаковым на всех Arduino-платформах?
  5. Зачем добавлять единицу измерения в имя?
  6. Что запрещает const?
  7. Чем локальная переменная отличается от глобальной?
  8. Почему интервал millis() проверяют выражением now - previous?
Ориентиры для самопроверки

Сначала ответьте без подсказки. Ответ можно считать полным, если вы:

  • формулируете основную мысль своими словами;
  • называете важные условия, ограничения или меры безопасности;
  • для схемы, кода или расчёта показываете ход решения и ожидаемый результат.

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

Словарь статьи

ТерминЗначение
переменнаяименованный объект, хранящий значение определённого типа
типнабор допустимых значений и операций над ними
объявлениевведение имени и типа объекта
инициализациязадание начального значения при создании объекта
присваиваниезапись нового значения в существующий изменяемый объект
идентификаторимя переменной, функции или другого элемента программы
constограничение, запрещающее менять объект после инициализации
область видимостичасть программы, где имя доступно
переполнениевыход вычисления за диапазон представимого типа
счётчикпеременная, накапливающая число событий или шагов

Связанные темы

Источники