17.07.2010

Уроки Wiring (4)

Урок 4. Немного о синтаксисе Wiring

(продолжение, начало см. Урок 3. Из чего состоит скетч?)

Чтобы освоить следующие уроки, придется позанудствовать  уделить немного внимания самому языку программирования Arduino - Wiring. На самом деле, он является надмножеством C++ для микроконтроллеров AVR: это такой "хитрый" язык, когда объекты использовать еще можно, а вот распределять динамическую память при помощи оператора new - уже нельзя. Мы рассмотрим объекты позже, когда будем учиться работать с библиотеками. Если вы уже знакомы с синтаксисом С/C++, можете смело забить пропустить этот урок и переходить к следующему.

Всем остальным хочу порекомендовать учебники по языкам: для C рекомендую Кернигана и Ритчи, а по C++ - Страуструпа или Шилдта. Сам по ним учился и гарантирую, что после прочтения этих книг вы как минимум получите грамотное и развернутое представление о языке. Я же, на свой страх и риск, попробую изложить необходимый на мой взгляд минимум.

Итак, из прошлого урока вы уже знаете про переменные и оператор присваивания:

val = a + b / c * d - 2;

Оператор разбит на две части знаком равенства. Справа - некое выражение: комбинация переменных, числовых констант и арифметических операций, подлежащая вычислению. Слева - имя переменной, которой будет присвоен результат вычислений (в данном случае ее имя "val"). Арифметические операции имеют приоритет и могут быть двуместными и одноместными. Вот стандартная таблица, которая должна быть выжжена каленым железом в голове каждого пишущего на C/C++ программиста (у меня она распечатана и висит на доске, на магнитике):



Не пугайтесь обилия вариантов, начинающему едва ли понадобится треть этой таблицы. Зато в ней наглядно иллюстрируется порядок, в котором будут происходить вычисления. Выражение a=6+4*2 даст в результате 14, и чтобы добиться изменения "естественного" порядка, надо использовать круглые скобки: a=(6+4)*2.

Переменная закрепляет за собой одну или несколько ячеек в оперативной памяти микроконтроллера - по выбору компилятора это может быть RAM или внутренний регистр. У переменной обязательно есть тип, определяющий значения, которые ей можно присваивать. Условно все типы можно поделить на целочисленные и с плавающей точкой. С последними надо осторожно: стоит один раз использовать  float или double, как подключится соответствующая библиотека  и размер скетча заметно увеличится.

Тип указывается при объявлении переменной, перед именем:


int a; //  от -32,768 до 32,767
byte b; // от 0 до 255
float с; // от -3.4028235 x 10^38 до 3.4028235 x 10^38


Объявление можно совмещать с присвоением:

double result = sqrl(15); // извлекаем квадратный корень

(только помните, что в отличие от присвоения, объявление делается один раз).

Если типы переменных в вычисляемом выражении различны, компилятор пытается автоматически преобразовывать их, повышая точность или расширяя диапазон. Но в некоторых случаях это не спасает и легко сделать ошибку, забыв, например, что деление бывает целочисленным. В примере ниже запись числовых констант "5" и "5.0" имеет  принципиальное значение:


float result1 = 5/2; // результат - 2.0
float result2 = 5.0/2.0; // результат - 2.5

В первом случае "5" и "2" будут распознаны как константы целочисленных типов, деление произойдет с отбрасыванием дробной части, несмотря на то, что результат будет присвоен переменной вещественного типа (так по-научному называют типы с плавающей точкой ;)

Чтобы сэкономить память, которой у Arduino так мало, старайтесь не плодить переменные почем зря. В качестве альтернативы можно воспользоваться оператором препроцессора #define, который перед компиляцией "подставляет" вместо одной группы символов другую. Например, так память расходуется:

int ledPin = 13;

... а так - нет:

#define ledPin 13  

но оба варианта допускают использование таким образом:

pinMode(ledPin, OUTPUT);

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

Перейдем к другим операторам (присваивание лишь один из доступного нам перечня действий в программе на Wiring). Для удобства, операторы объединяют в единый логический блок с помощью фигурных скобок "{" и "}". Во многих конструкциях ниже, вместо одного оператора можно  подставить несколько, если заключить их в фигурные скобки, друг от друга операторы отделяются при помощи точки с запятой - ";".

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

if (логическое выражение)
  <оператор, если условие истинно>
else
  <оператор, если условие ложно>

Логическое выражение - это подмножество рассмотренных выше арифметических, но оно дает в качестве результата логические значения ИСТИНА или ЛОЖЬ. В C/C++ лживое утверждение равно нулю, а истинное - любое значение, кроме нуля. Для получения результата используют специальные логические операторы <, >, <=, >=, ==, != (см. таблицу ранее). Строго говоря, результат логического выражения можно использовать не только в условном операторе, но и сохранить в переменной. Если хотите наглядно указать на логическую природу переменных в своей программе, можете использовать тип boolean:

boolean b = buttonPressed();
if (b) {
 // действия при нажатой кнопке
} else {
 // действия при отпущенной кнопке
}

Разновидностью оператора ветвления являются операторы циклов - конструкции, которые повторяют один и тот же блок операторов несколько раз подряд.

Если число итераций (повторений цикла) заранее неизвестно, используют цикл while, который имеет две разновидности:

while (логическое выражение) оператор;

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

while ( buttonPressed() ) delay(100);

... будет ждать, пока пользователь наконец не отпустит кнопку. 

В этом варианте цикл обрабатывается по схеме: сначала вычислить выражение, если оно истинно, в очередной раз выполнить тело цикла. Если пользователь не нажал на кнопку, то тело не выполнится ни одного раза - обычно это нормально, но бывают ситуации, когда тело надо хотя бы один раз выполнить. Тогда необходимо использовать вторую разновидность while:

do оператор while (логическое выражение);

... которая гарантирует хотя бы одно выполнение оператора, например:

do {
  c = beepUltrasonic(outPin);
} while (echoUltrasonic(inPin) > 30);

Этот пример отталкивается от легенды, что echoUltrasonic возвращает правильный результат только после beepUltrasonic. Да, можно было бы добавить первый вызов beepUltrasonic перед циклом, но так - гораздо понятнее и избегаем дублирования кода.

Наконец, если число повторений известно заранее, удобен оператор for:

for (инициализатор; логическое выражение; шаг) оператор;

В круглых скобках записано три части оператора:
  • инициализатор выполняется один раз, перед началом цикла;
  • логическое выражение проверяется перед очередным выполнением тела цикла - если оно ложно, цикл завершается;
  • шаг - действие, которое выполняется по завершении итерации цикла, и часто туда вписывают приращение счетчика на единицу.
Например, нам нужно перевести все цифровые пины Arduino в режим выхода. Мы знаем, что их всего 14, нумеруются от 0 до 13. Тогда мы можем написать такой цикл:

for (int i=0; i<14; i++) pinMode(i,OUTPUT);

В качестве счетчика выступает целочисленная переменная i, которую одновременно и объявляем, и инициализируем нулем. Дальше идет логическое выражение, определяющее выход из цикла, когда значение i станет больше тринадцати и - наконец - магическое "i++" означает "увеличить содержимое переменной i на единицу". Таким образом, pinMode иполнится 14 раз, что нам и хотелось с самого начала. Конечно, можно было бы записать этот цикл и через while:

int i = 0;
while (i<14) {
  pinMode(i,OUTPUT);
  i = i+1;
}

Напоследок о том, чего мы уже неявно коснулись: функции. Часто, реализуя очередной алгоритм, вы ощущаете, что есть повторяющиеся места или логически законченные фрагменты, которые можно без труда обособить в последовательность операторов. В этом случае можно не городить длиннющую простыню из операторов, а разбить их на функции - как это уже сделано со встроенными setup() и loop(). Как и переменную, функцию надо сначала объявить:

тип_возвращаемого_значения имя_функции(тип1 имя1, тип2 имя2, ... ) 
{
  <операторы>
  return возвращаемое значение;
}

Подобно переменной, функция имеет имя - набор латинских букв и цифр, но начинается обязательно с буквы. По своей сущности функция C/Wiring похожа на математическую даже по записи - сначала имя, далее в скобках - набор аргументов, вычисленное значение подставляется в вычисляемое выражение.

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

int sqr(int x)
{
  return x*x;
}


...


int z = sqr(a-c);

Кстати, уже известный нам оператор pinMode на самом деле вовсе и не оператор, а тоже -  функция,  входящая в состав ядра Arduino:

void pinMode(uint8_t pin, uint8_t mode)
{
  uint8_t bit = digitalPinToBitMask(pin);
  uint8_t port = digitalPinToPort(pin);
  volatile uint8_t *reg;
  
  if (port == NOT_A_PIN) return;


  // JWS: can I let the optimizer do this?
  reg = portModeRegister(port);


  if (mode == INPUT) *reg &= ~bit;
  else *reg |= bit;
}

Специальный неопределенный тип void означает "ничего". Функция такого типа ничего не обязана возвращать и может обойтись оператором return без значения - именно такого типа функции setup() и loop(). Функция также может и не принимать никаких аргументов, но пустые скобки после имени все равно приходится записывать.

Ну вот, для последующих уроков вполне достаточно. Хоть и кажется, что написал много, однако на самом деле - совсем чуть-чуть, так что остальные приемы буду пояснять в процессе следующих уроков.

16 комментариев:

  1. Илья,здравствуйте! На Ваших уроках http://mk90.blogspot.com/2010/06/wiring-4.html пытаюсь овладеть навыками програмирования.Если это уместно,подскажите пожалуйста, где у меня ошибка в скетче.Задача: нажатие кнопки-горит светодиод и ШД делает определенное кол-во оборотов и останавливается.При повторном нажатии кнопки-светодиод гаснет и ШД отрабатывает движение назад.
    У меня получается при нажатии кнопки ШД отрабатывает вперед и назад сразу.Где ошибка? Заранее благодарен.Александр.


    #include
    int inPin = 2; // the number of the input pin
    int outPin = 9; // the number of the output pin
    AF_Stepper motor(200, 2);

    int state = HIGH; // the current state of the output pin
    int reading; // the current reading from the input pin
    int previous = LOW; // the previous reading from the input pin

    // the follow variables are long's because the time, measured in miliseconds,
    // will quickly become a bigger number than can be stored in an int.
    long time = 0; // the last time the output pin was toggled
    long debounce = 200; // the debounce time, increase if the output flickers

    void setup()
    {
    pinMode(inPin, INPUT);
    pinMode(outPin, OUTPUT);
    motor.setSpeed(100); // 100 оборотов в минуту
    }

    void loop()
    {
    reading = digitalRead(inPin);

    // if the input just went from LOW and HIGH and we've waited long enough
    // to ignore any noise on the circuit, toggle the output pin and remember
    // the time
    if (reading == HIGH && previous == LOW && millis() - time > debounce) {
    motor.step(600, FORWARD, SINGLE); //3 оборота
    if (state == HIGH)
    state = LOW;
    else
    state = HIGH;
    motor.step(600, BACKWARD, SINGLE); //3 оборота

    time = millis();
    }

    digitalWrite(outPin, state);

    previous = reading;
    }

    ОтветитьУдалить
  2. Знаете, Александр - скетч написан в точности так, как он себя и ведет (с ваших слов). Т.е. последовательность команд сначала крутит вперед, потом назад.

    Надо дописать таким образом, чтобы направление поворота зависело от переменной state, например - справьте команду первого поворота:

    motor.step(600, (STATE == HIGH) ? FORWARD:BACKWARD, SINGLE);

    ... а второй motor.step уберите вообще.

    ОтветитьУдалить
  3. Илья,благодарю за помощь!Все заработало.С уважением,Александр.

    ОтветитьУдалить
  4. Илья,здравствуйте!Пршу меня простить за назойливость,но без Вашей помощи не обойтись.Не могу разобраться,как добавить в скетч серву.Все время выдает ошибку,а знаний не хватает, чтобы исправить.Задача: отрабатывать поворот сервы на заданный угол паралельно с работой ШД.Т.е,при нажатии кнопки ШД делает определенное кол-во шагов,серва поворачивается на определенный угол.При повторном нажатии кнопки ШД и серва отрабатывают назад.Если не затруднит подскажите,пожалуйста,как добиться желаемого.Заранее благодарен,Александр.

    #include
    #include "Servo.h"
    Servo myservo;

    int servoPin = 10; // порт подключения сервы
    int myAngle; // будет хранить угол поворота
    int pulseWidth; // длительность импульса
    void servoPulse(int servoPin, int myAngle)
    {
    pulseWidth = (myAngle * 11) + 500; // конвертируем угол в микросекунды
    digitalWrite(servoPin, HIGH); // устанавливаем серве высокий уровень
    delayMicroseconds(pulseWidth); // ждём
    digitalWrite(servoPin, LOW); // устанавливаем низкий уровень
    delay(20); //
    }
    int inPin = 2; // контакт, к которому подключена кнопка
    int outPin = 9; // контакт, к которому подключен светодиод
    AF_Stepper motor(200, 2);//Создаем объект для двигателя на 2 канале (M3 и M4)

    int state = HIGH; // the current state of the output pin
    int reading; // the current reading from the input pin
    int previous = LOW; // the previous reading from the input pin

    // the follow variables are long's because the time, measured in miliseconds,
    // will quickly become a bigger number than can be stored in an int.
    long time = 0; // the last time the output pin was toggled
    long debounce = 200; // the debounce time, increase if the output flickers

    void setup()
    {
    pinMode(inPin, INPUT);
    pinMode(outPin, OUTPUT);
    motor.setSpeed(100); // 100 оборотов в минуту
    pinMode(servoPin, OUTPUT);// конфигурируем пин сервы, как выход
    }

    void loop()
    {
    reading = digitalRead(inPin);

    // if the input just went from LOW and HIGH and we've waited long enough
    // to ignore any noise on the circuit, toggle the output pin and remember
    // the time
    if (reading == HIGH && previous == LOW && millis() - time > debounce) {
    motor.step(860, (state == HIGH) ? FORWARD:BACKWARD, SINGLE);
    Servo myservo((state == HIGH) ? (myAngle=0; myAngle<=180; myAngle++):(myAngle=180; myAngle>=0; myAngle--));

    if (state == HIGH)
    state = LOW;
    else
    state = HIGH;
    time = millis();
    }
    digitalWrite(outPin, state);
    previous = reading;
    }

    ОтветитьУдалить
  5. Тут могу посоветовать использовать для управления сервой аппаратный ШИМ, об этом Урок 6.

    Но можно, конечно, и как в Вашем скетче, только неясно, зачем нужна функция servoPulse - она нигде не вызывается и в строке "Servo myservo((state == HIGH) ? (myAngle=0; myAngle<=180; myAngle++):(myAngle=180; myAngle>=0; myAngle--));" явная ошибка синтаксиса. Я бы просто написал analogWrite(servoPin,value). Только надо перевести угол в value, согласно спецификации сервы.

    ОтветитьУдалить
  6. Илья,пытался "привязать серву"-запутался окончательно.Как вставить серву с углом поворота 180 гр.и с регулируемой скоростью перемещения в нижеследующий скетч,чтобы выполнялось условие? 1действие-Кнопка нажата,ШД делает определенное кол-во шагов вперед и останавливается,серва поворачивается на 180 гр.и останавливается.
    2действие-Кнопка нажата,серва отрабатывает поворот на 180 гр. назад и останавливается, ШД отрабатывает движение назад и останавливается.
    Очень важна последовательность действий.
    Буду благодарен.Александр.


    #include
    #include "Servo.h"
    Servo myservo;

    int servoPin = 10; // порт подключения сервы


    int inPin = 2; // контакт, к которому подключена кнопка
    int outPin = 9; // контакт, к которому подключен светодиод
    AF_Stepper motor(200, 2);//Создаем объект для двигателя на 2 канале (M3 и M4)

    int state = HIGH; // the current state of the output pin
    int reading; // the current reading from the input pin
    int previous = LOW; // the previous reading from the input pin

    // the follow variables are long's because the time, measured in miliseconds,
    // will quickly become a bigger number than can be stored in an int.
    long time = 0; // the last time the output pin was toggled
    long debounce = 200; // the debounce time, increase if the output flickers

    void setup()
    {
    pinMode(inPin, INPUT);
    pinMode(outPin, OUTPUT);
    motor.setSpeed(100); // 100 оборотов в минуту

    }

    void loop()
    {
    reading = digitalRead(inPin);

    // if the input just went from LOW and HIGH and we've waited long enough
    // to ignore any noise on the circuit, toggle the output pin and remember
    // the time
    if (reading == HIGH && previous == LOW && millis() - time > debounce) {
    motor.step(860, (state == HIGH) ? FORWARD:BACKWARD, SINGLE);


    if (state == HIGH)
    state = LOW;
    else
    state = HIGH;
    time = millis();
    }
    digitalWrite(outPin, state);
    previous = reading;
    }

    ОтветитьУдалить
  7. А что за серва такая хитрая - с регулируемой скоростью перемещения?

    ОтветитьУдалить
  8. Серва обычная.Это я не точно выразился.Имелось ввиду регулировать скорость перемещения в скетче.

    ОтветитьУдалить
  9. Допустим, поврот на 180 градусов за 10 секунд.И время можно изменить.

    ОтветитьУдалить
  10. У обычной сервы скорость перемещения регулировать нельзя. Можно только позиционировать.

    Чтобы отвести ее на угол 180 градусов - в данном случае максимальный - надо сказать analogWrite(servoPin,255);, чтобы обратно - analogWrite(servoPin,0);.

    Я бы написал так: analogWrite(servoPin, (state == HIGH) ? 255:0);.

    ОтветитьУдалить
  11. Попробовал этот скетч в механике-серва не реагирует.Где моя ошибка?
    #include
    #include
    Servo myservo; // создаём объект для контроля сервы


    int servoPin = 10; // порт подключения сервы
    int pos = 0; // переменная для хранения позиции сервы


    int inPin = 2; // контакт, к которому подключена кнопка
    int outPin = 9; // контакт, к которому подключен светодиод
    AF_Stepper motor(200, 2);//Создаем объект для двигателя на 2 канале (M3 и M4)

    int state = HIGH; // the current state of the output pin
    int reading; // the current reading from the input pin
    int previous = LOW; // the previous reading from the input pin

    long time = 0; // the last time the output pin was toggled
    long debounce = 200; // the debounce time, increase if the output flickers

    void setup()
    {
    pinMode(inPin, INPUT);
    pinMode(outPin, OUTPUT);
    motor.setSpeed(100); // 100 оборотов в минуту
    myservo.attach(10); // серва подключена к 10-му пину

    }

    void loop()
    {
    reading = digitalRead(inPin);


    if (reading == HIGH && previous == LOW && millis() - time > debounce) {
    motor.step(860, (state == HIGH) ? FORWARD:BACKWARD, SINGLE);
    analogWrite(servoPin, (state == HIGH) ? 255:0);

    if (state == HIGH)
    state = LOW;
    else
    state = HIGH;
    time = millis();
    }
    digitalWrite(outPin, state);
    previous = reading;
    }

    ОтветитьУдалить
  12. Вы либо пользуйтесь своим "объектом для контроля сервы" myservo, либо analogWrite - видимо в этом причина.

    В комментах тяжело отлаживаться - предлагаю писать через профиль письма.

    ОтветитьУдалить
  13. Скажите как понимать выражение uint8_t. На многих сайтах написано что это unsigned int но как int может быть 8 битным - не понятно?
    И почему в описании к прогаммированию на ардуино нет информации по этому ввопросу?

    ОтветитьУдалить
    Ответы
    1. Чтобы понять, надо посмотреть определение:

      typedef unsigned char uint8_t;

      Но не советую использовать это в скетчах - пользуйтесь byte. Исчерпывающе про этот тип можно прочитать в стандарте ISO/IEC 9899:1999, раздел 7.18 "Integer types".

      Удалить
  14. Я бы не был столь категоричен в утверждении, что define якобы экономит память. Например, если мы опишем строку через define, то в код она будет включена столько раз, сколько раз мы ее используем, а если как переменную, то только один раз.

    ОтветитьУдалить
    Ответы
    1. Тут не всё так однозначно, #define действительно выполняет подстановку перед компиляцией, но смотреть надо на то, что получается в результате этой самой подстановки. Ардуино работает на гарвардской архитектуре, а значит память программ и данных - это аппаратно два различных устройства. Памяти данных обычно мало, и если есть задача использовать её экономно, а объявление переменной приводит к выделению лишней ячейки в памяти - это хуже, чем если просто использовать в коде константу - опять-таки, если эта константа помещается в память программ. Все эти "если" - допущения относительно того, как наш скетч компилируется в ассемблерные инструкции. И я еще могу предположить, что переменную можно разместить во внутреннем регистре, но числовую константу, которая по своей сути неизменна, пихать куда-то помимо памяти программ просто так компилятор не будет.

      Удалить