(продолжение, начало см. Урок 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(). Функция также может и не принимать никаких аргументов, но пустые скобки после имени все равно приходится записывать.
Ну вот, для последующих уроков вполне достаточно. Хоть и кажется, что написал много, однако на самом деле - совсем чуть-чуть, так что остальные приемы буду пояснять в процессе следующих уроков.
Илья,здравствуйте! На Ваших уроках 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;
}
Знаете, Александр - скетч написан в точности так, как он себя и ведет (с ваших слов). Т.е. последовательность команд сначала крутит вперед, потом назад.
ОтветитьУдалитьНадо дописать таким образом, чтобы направление поворота зависело от переменной state, например - справьте команду первого поворота:
motor.step(600, (STATE == HIGH) ? FORWARD:BACKWARD, SINGLE);
... а второй motor.step уберите вообще.
Илья,благодарю за помощь!Все заработало.С уважением,Александр.
ОтветитьУдалитьИлья,здравствуйте!Пршу меня простить за назойливость,но без Вашей помощи не обойтись.Не могу разобраться,как добавить в скетч серву.Все время выдает ошибку,а знаний не хватает, чтобы исправить.Задача: отрабатывать поворот сервы на заданный угол паралельно с работой ШД.Т.е,при нажатии кнопки ШД делает определенное кол-во шагов,серва поворачивается на определенный угол.При повторном нажатии кнопки ШД и серва отрабатывают назад.Если не затруднит подскажите,пожалуйста,как добиться желаемого.Заранее благодарен,Александр.
ОтветитьУдалить#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;
}
Тут могу посоветовать использовать для управления сервой аппаратный ШИМ, об этом Урок 6.
ОтветитьУдалитьНо можно, конечно, и как в Вашем скетче, только неясно, зачем нужна функция servoPulse - она нигде не вызывается и в строке "Servo myservo((state == HIGH) ? (myAngle=0; myAngle<=180; myAngle++):(myAngle=180; myAngle>=0; myAngle--));" явная ошибка синтаксиса. Я бы просто написал analogWrite(servoPin,value). Только надо перевести угол в value, согласно спецификации сервы.
Илья,пытался "привязать серву"-запутался окончательно.Как вставить серву с углом поворота 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;
}
А что за серва такая хитрая - с регулируемой скоростью перемещения?
ОтветитьУдалитьСерва обычная.Это я не точно выразился.Имелось ввиду регулировать скорость перемещения в скетче.
ОтветитьУдалитьДопустим, поврот на 180 градусов за 10 секунд.И время можно изменить.
ОтветитьУдалитьУ обычной сервы скорость перемещения регулировать нельзя. Можно только позиционировать.
ОтветитьУдалитьЧтобы отвести ее на угол 180 градусов - в данном случае максимальный - надо сказать analogWrite(servoPin,255);, чтобы обратно - analogWrite(servoPin,0);.
Я бы написал так: analogWrite(servoPin, (state == HIGH) ? 255:0);.
Попробовал этот скетч в механике-серва не реагирует.Где моя ошибка?
ОтветитьУдалить#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;
}
Вы либо пользуйтесь своим "объектом для контроля сервы" myservo, либо analogWrite - видимо в этом причина.
ОтветитьУдалитьВ комментах тяжело отлаживаться - предлагаю писать через профиль письма.
Скажите как понимать выражение uint8_t. На многих сайтах написано что это unsigned int но как int может быть 8 битным - не понятно?
ОтветитьУдалитьИ почему в описании к прогаммированию на ардуино нет информации по этому ввопросу?
Чтобы понять, надо посмотреть определение:
Удалитьtypedef unsigned char uint8_t;
Но не советую использовать это в скетчах - пользуйтесь byte. Исчерпывающе про этот тип можно прочитать в стандарте ISO/IEC 9899:1999, раздел 7.18 "Integer types".
Я бы не был столь категоричен в утверждении, что define якобы экономит память. Например, если мы опишем строку через define, то в код она будет включена столько раз, сколько раз мы ее используем, а если как переменную, то только один раз.
ОтветитьУдалитьТут не всё так однозначно, #define действительно выполняет подстановку перед компиляцией, но смотреть надо на то, что получается в результате этой самой подстановки. Ардуино работает на гарвардской архитектуре, а значит память программ и данных - это аппаратно два различных устройства. Памяти данных обычно мало, и если есть задача использовать её экономно, а объявление переменной приводит к выделению лишней ячейки в памяти - это хуже, чем если просто использовать в коде константу - опять-таки, если эта константа помещается в память программ. Все эти "если" - допущения относительно того, как наш скетч компилируется в ассемблерные инструкции. И я еще могу предположить, что переменную можно разместить во внутреннем регистре, но числовую константу, которая по своей сути неизменна, пихать куда-то помимо памяти программ просто так компилятор не будет.
Удалить