Интернет-лаборатория роботов ZiZiBOT.RU

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

г. Юрга,
ул.Ленинградская 38/83

+7 923-503-6074

fastArduino

Ускоряем программу под ArduinoIDE (Arduino NaNO, UNO, MEGA)

Скачать материалы к данной статье

 

 

  1. Если в программе много расчетов, рекомендую заменить числа с плавающей точкой целыми числами.
  2. Убрать приостановки программы функцией delay(). Использовать временные метки.
  3. Заменить чтение и запись в GPIO работой с портами ввода вывода.
  4. Уменьшить точность чтения аналогового сигнала функций analogRead(). https://github.com/avandalen/avdweb_AnalogReadFast/blob/master/avdweb_AnalogReadFast.h
  5. Не пользоваться функциями задерживающими выполнение программы , например, pulseIn().
  6. Не использовать Software Serial порты и прочие программные эмуляторы протоколов обмена.
  7. Отключить вывод данных в Serial порт.
  8. Где возможно, использовать 8-разрядные числа.
  9. Применить быстрый контроллер.


Микроконтроллер (ATMega328) Arduino Nano/Uno является довольно медленными и за один такт выполняет только одну 8-и битовую операцию. Этого достаточно в большинстве приложений для данных контроллеров. Если вы используете Arduino NANO с парой датчиков, для анализа обстановки пару раз в секунду, то задумываться об ускорении вашей программы не стоит. А вот если таких датчиков много, допустим пара десятков, подключенных через мультиплексор, с которых считывается аналоговый сигнал, затем сигнал анализируется, обрабатывается и передается на исполнительных механизм, то вы может столкнуться с тем, что ряд показаний датчиков будет теряться, система ошибается «виснет».

 

Давайте проверим насколько данные рекомендации эффективны


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

 

1. Пункт -  заменить числа с плавающей точкой целыми числами

 

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
}
 
void loop() {
  // Минимальное значение для 16 разрядного, -32768 а максимальное 32767
  // выход за эти пределы приведет к потере знаковых разрядов и ошибочным вычислениЯм
  uint32_t startTime;
  uint32_t stopTime;
  uint32_t floatTime;
  uint32_t intTime;
  float alfa_f = 0;
  startTime = micros(); //Запомимнаем микросекунды старта цикла
  for (int i = -32768; i < 32767; i++)
  {
    alfa_f = alfa_f + 10;
    alfa_f = alfa_f * 2;
    alfa_f = alfa_f / 3;
  }
  stopTime = micros(); //Запомимнаем микросекунды старта цикла
  floatTime = stopTime - startTime;
  Serial.print("float Operations ="); Serial.print(floatTime); Serial.println(" micro sec"); //
  Serial.println(alfa_f);
   
  //int8_t alfa_i = 0;
  //int alfa_i = 0;
  int32_t alfa_i = 0;
  startTime = micros(); //Запомимнаем микросекунды старта цикла
  for (int i = -32768; i < 32767; i++)
  {
    alfa_i = alfa_i + 10;
    alfa_i = alfa_i * 2;
    alfa_i = alfa_i / 3;
  }
  stopTime = micros(); //Запомимнаем микросекунды старта цикла
  intTime = stopTime - startTime;
  Serial.print("Integer Operations ="); Serial.print(intTime); Serial.println(" micro sec"); ////
  Serial.println(alfa_i);
  Serial.println("***************************");
  Serial.print("floatTime-intTime = "); Serial.print(floatTime - intTime); Serial.println(" micro sec");
  Serial.print("floatTime/intTime = "); Serial.print(floatTime / intTime); Serial.println(" How much");
  Serial.println("***************************");
  delay(1000);
}


 Тестирование примера 1 показывает, что при использовании типа int (целое 16 разрядное знаковое для 8битных плат) в расчетах вместо float (32 разрядное с плавающей точкой) дает ускорение более чем в 3 раза. Использование типа int32_t уже не столь эффективно, а вот использование in8_t , дает ускорение аж в 6 раз.
 
Отметим, что использовать в расчетах переменные длиной в 1 или 2 байта (int8_t, int16_t) следует только, если значения (даже внутри формулы) в расчетах не превышают пределы (-128/127, -32768/32767). Если возможны, даже промежуточные выходы получаемых в результате расчетов чисел за указанные пределы, то  результаты вычислений будут неверными.
 

2. Пункт - Убрать приостановки программы функцией delay(). Использовать временные метки

 

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
}
uint32_t goTime = 0;
void loop() {
  // Минимальное значение для 16 разрядного, -32768 а максимальное 32767
  // выход за эти пределы приведет к потере знаковых разрядов и ошибочным вычислениЯм
  if (goTime < millis())
  {
    goTime = millis() + 10000; //+10секунд
    uint32_t startTime;
    uint32_t stopTime;
    uint32_t floatTime;
    uint32_t intTime;
    float alfa_f = 0;
    startTime = micros(); //Запомимнаем микросекунды старта цикла
    for (int i = -32768; i < 32767; i++)
    {
      alfa_f = alfa_f + 10;
      alfa_f = alfa_f * 2;
      alfa_f = alfa_f / 3;
    }
    stopTime = micros(); //Запомимнаем микросекунды старта цикла
    floatTime = stopTime - startTime;
    Serial.print("float Operations ="); Serial.print(floatTime); Serial.println(" micro sec"); //
    Serial.println(alfa_f);
    
    //int8_t alfa_i = 0;
    //int alfa_i = 0;
    int32_t alfa_i = 0;
    startTime = micros(); //Запомимнаем микросекунды старта цикла
    for (int i = -32768; i < 32767; i++)
    {
      alfa_i = alfa_i + 10;
      alfa_i = alfa_i * 2;
      alfa_i = alfa_i / 3;
    }
    stopTime = micros(); //Запомимнаем микросекунды старта цикла
    intTime = stopTime - startTime;
    Serial.print("Integer Operations ="); Serial.print(intTime); Serial.println(" micro sec"); ////
    Serial.println(alfa_i);
    Serial.println("***************************");
    Serial.print("floatTime-intTime = "); Serial.print(floatTime - intTime); Serial.println(" micro sec");
    Serial.print("floatTime/intTime = "); Serial.print(floatTime / intTime); Serial.println(" How much");
    Serial.println("***************************");
  }
}


Допустим, в программе один раз в 10 секунд требуется выводить некоторое значение в порт. 
Возьмем в качестве примера расчет из первого пункта. Мы в качестве задержки вывода используем функцию delay(количество миллисекунд задержки), но во время данной задержки никакие расчеты производиться не могут (кроме обработки прерываний), и вычислительная способность программы падает, программа простаивает.
Сделаем так, чтобы наши расчеты выполнялись без задержки, но через 10 секунд. Для этого введем дополнительную глобальную переменную (описана не в функции, а в основной программе)  
uint32_t goTime = 0;   , 
в которой будем хранить время запуска следующего расчета 
if (goTime < millis()) {….}. 
В начале расчета записываем в текущее время, увеличенное на 10 секунд 
goTime = millis() + 10000; //+10секунд.
 
Но когда подобных блоков становиться много, программа запутывается. На помощь приходит использование статических переменных внутри функций.

 

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
}
void loop() { //*************************************  loop()
  // Минимальное значение для 16 разрядного, -32768 а максимальное 32767
  // выход за эти пределы приведет к потере знаковых разрядов и ошибочным вычислениЯм
  testSpeed();
}
bool testSpeed() //*************************************  testSpeed()
{
  static uint32_t goTime = 0;
  if (goTime < millis())
  {
    goTime = millis() + 10000; //+10секунд
    uint32_t startTime;
    uint32_t stopTime;
    uint32_t floatTime;
    uint32_t intTime;
    float alfa_f = 0;
    startTime = micros(); //Запомимнаем микросекунды старта цикла
    for (int i = -32768; i < 32767; i++)
    {
      alfa_f = alfa_f + 10;
      alfa_f = alfa_f * 2;
      alfa_f = alfa_f / 3;
    }
    stopTime = micros(); //Запомимнаем микросекунды старта цикла
    floatTime = stopTime - startTime;
    Serial.print("float Operations ="); Serial.print(floatTime); Serial.println(" micro sec"); //
    Serial.println(alfa_f);
    
    //int8_t alfa_i = 0;
    //int alfa_i = 0;
    int32_t alfa_i = 0;
    startTime = micros(); //Запомимнаем микросекунды старта цикла
    for (int i = -32768; i < 32767; i++)
    {
      alfa_i = alfa_i + 10;
      alfa_i = alfa_i * 2;
      alfa_i = alfa_i / 3;
    }
    stopTime = micros(); //Запомимнаем микросекунды старта цикла
    intTime = stopTime - startTime;
    Serial.print("Integer Operations ="); Serial.print(intTime); Serial.println(" micro sec"); ////
    Serial.println(alfa_i);
    Serial.println("***************************");
    Serial.print("floatTime-intTime = "); Serial.print(floatTime - intTime); Serial.println(" micro sec");
    Serial.print("floatTime/intTime = "); Serial.print(floatTime / intTime); Serial.println(" How much");
    Serial.println("***************************");
    return true;
  }
  return false;
}

 

Теперь создана функция 
bool testSpeed()
, она вызывается с частотой повторения функции loop() 
Но отрабатывает расчет  только в том случае, если со времени ее предыдущего вызова прошло 10 секунд.
Мы разгрузили основную функцию loop(), сделали код более читаемым и в то же время сохранили функционал программы.
 

3. Пункт - Заменить чтение и запись в GPIO работой с портами ввода вывода


(Заменить чтение и запись в GPIO работой с портами ввода вывода)
Отказаться от использования digitalWrite() digitalRead() в пользу прямого обращения к портам.
Но для этого потребуется умение пользоваться булевой алгеброй.
 
Все PINы Arduino, принадлежат определенному порту ввода вывода, всего портов для NANO и UNO на микроконтроллере Atmega328 три: PB, PD , PC.
Для операций с каждым из портов ввода-вывода используется три регистра: DDRx, PORTx, PINx 
DDRx — Data Direction Register. Регистр, определяющий направление,  см. команду pinMode(),  (1 – передача, 0 – приём).
PORTx — регистр, который содержит логические значения, выводимые на соответствующие GPIO, в случае, если они настроены на вывод.
PINx — регистр, который содержит логические значения, полученные при считывании данных из порта в случае, если они настроены на ввод.

Сначала оттестируем ввод данных с GPIO
 

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  pinMode(8,INPUT);
}
void loop() { //*************************************  loop()
  // Минимальное значение для 16 разрядного, -32768 а максимальное 32767
  // выход за эти пределы приведет к потере знаковых разрядов и ошибочным вычислениЯм
  testSpeed();
}
bool testSpeed() //*************************************  testSpeed()
{
  static uint32_t goTime = 0;
  if (goTime < millis())
  {
    goTime = millis() + 10000; //+10секунд
  uint8_t portB_in_data;
  int32_t summa = 0;
  uint32_t startTime;
  uint32_t stopTime;
  uint32_t digitalReadTime;
  uint32_t portTime;
  startTime = micros(); //Запомимнаем микросекунды старта цикла
  for (int i = -32768; i < 32767; i++)
  {
    portB_in_data = PINB & 0b00000001; //Считываем значение с порта B, накладываем маску для выделения только значения на GPIO D8 (PB0).
    summa = summa + portB_in_data;
  }
  stopTime = micros(); //Запомимнаем микросекунды старта цикла
  portTime = stopTime - startTime;
  Serial.print("port input Operations ="); Serial.print(portTime); Serial.println(" micro sec"); //
  Serial.println(summa);
startTime = micros(); //Запомимнаем микросекунды старта цикла
  for (int i = -32768; i < 32767; i++)
  {
    portB_in_data = digitalRead(8); //Считываем значение с GPIO D8 (PB0).
    summa = summa + portB_in_data;
  }
  stopTime = micros(); //Запомимнаем микросекунды старта цикла
  digitalReadTime = stopTime - startTime;
  Serial.print("digitalRead(8); Operations ="); Serial.print(digitalReadTime); Serial.println(" micro sec"); //
  Serial.println(summa);
  Serial.println("***************************");
  Serial.print("digitalReadTime-portTime = "); Serial.print(digitalReadTime - portTime); Serial.println(" micro sec");
  Serial.print("digitalReadTime/portTime = "); Serial.print(digitalReadTime / portTime); Serial.println(" How much");
  Serial.println("***************************");
    return true;
  }
  return false;
}


Ускорение работы при использовании прямого обращения по чтению к регистру в 5 ПЯТЬ раз!
Теперь тестируем запись. Получаем разницу в скорости выполнения 6 ШЕСТЬ раз.

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  pinMode(8,OUTPUT);
}
void loop() { //*************************************  loop()
  // Минимальное значение для 16 разрядного, -32768 а максимальное 32767
  // выход за эти пределы приведет к потере знаковых разрядов и ошибочным вычислениЯм
  testSpeed();
}
bool testSpeed() //*************************************  testSpeed()
{
  static uint32_t goTime = 0;
  if (goTime < millis())
  {
    goTime = millis() + 10000; //+10секунд
  uint8_t portB_in_data;
  int32_t summa = 0;
  uint32_t startTime;
  uint32_t stopTime;
  uint32_t digitalWriteTime;
  uint32_t portTime;
  const uint8_t mask_D8 = 0b11111110; //маска для удаления значение в младше разряде регистра PB (D8)
  startTime = micros(); //Запомимнаем микросекунды старта цикла
  for (int i = -32768; i < 32767; i++)
  {
    portB_in_data = PORTB & mask_D8; //Выделили значение в порту без младшего разряда
    PORTB = portB_in_data & 0b00000001; //Добавляем 1 в младший разряд (D8)
  }
  stopTime = micros(); //Запомимнаем микросекунды старта цикла
  portTime = stopTime - startTime;
  Serial.print("port Write Operations ="); Serial.print(portTime); Serial.println(" micro sec"); //
  startTime = micros(); //Запомимнаем микросекунды старта цикла
  for (int i = -32768; i < 32767; i++)
  {
    digitalWrite(8,HIGH); //Устанавливаем высокий потенциал на GPIO D8 (PB0).
  }
  stopTime = micros(); //Запомимнаем микросекунды старта цикла
  digitalWriteTime = stopTime - startTime;
  Serial.print("digitalWrite(8,HIGH); Operations ="); Serial.print(digitalWriteTime); Serial.println(" micro sec"); //
  Serial.println("***************************");
  Serial.print("digitalWriteTime-portTime = "); Serial.print(digitalWriteTime - portTime); Serial.println(" micro sec");
  Serial.print("digitalWriteTime/portTime = "); Serial.print(digitalWriteTime / portTime); Serial.println(" How much");
  Serial.println("***************************");
    return true;
  }
  return false;
}


 
Вывод, если ваша программа совершает много операций ввода вывода, то использование прямого обращения к портам, ЕСЛИ ВАШЕЙ ПРОГРАММЕ НЕ ХВАТАЕТ СКОРОСТИ!!!!   - Хорошее решение!

4. Пункт - Уменьшить точность чтения аналогового сигнала функций analogRead(). https://github.com/avandalen/avdweb_AnalogReadFast/blob/master/avdweb_AnalogReadFast.h


Уменьшение точности чтения аналогового сигнала функций analogRead().

https://github.com/avandalen/avdweb_AnalogReadFast/blob/master/avdweb_AnalogReadFast.h
Для примера мы использовали сравнение скорости аналогового чтения в стандартном режиме и с уменьшенной точностью. Разница в скорости выполнения также 6 раз.

#include "avdweb_AnalogReadFast.h"
void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
}
void loop() { //*************************************  loop()
  // Минимальное значение для 16 разрядного, -32768 а максимальное 32767
  // выход за эти пределы приведет к потере знаковых разрядов и ошибочным вычислениЯм
  testSpeed();
}
bool testSpeed() //*************************************  testSpeed()
{
  static uint32_t goTime = 0;
  if (goTime < millis())
  {
    goTime = millis() + 10000; //+10секунд
  uint8_t portB_in_data;
  int32_t summa = 0;
  uint32_t startTime;
  uint32_t stopTime;
  uint32_t analogReadTime;
  uint32_t fastAnalogReadTime;
 // analogReadResolution(12);  //только для продвинутых плат
  startTime = micros(); //Запомимнаем микросекунды старта цикла
  for (int i = -32768; i < 32767; i++)
  {
    summa=summa+analogRead(A0);
  }
  stopTime = micros(); //Запомимнаем микросекунды старта цикла
  analogReadTime = stopTime - startTime;
  Serial.print(summa);  Serial.print(" analogRead() Operations ="); Serial.print(analogReadTime); Serial.println(" micro sec"); //
 // analogReadResolution(10);  //только для продвинутых плат
  startTime = micros(); //Запомимнаем микросекунды старта цикла
  for (int i = -32768; i < 32767; i++)
  {
    summa=summa+analogReadFast(A0);
  }
  stopTime = micros(); //Запомимнаем микросекунды старта цикла
  fastAnalogReadTime = stopTime - startTime;
  Serial.print(summa); Serial.print(" fast analogRead() Operations ="); Serial.print(fastAnalogReadTime); Serial.println(" micro sec"); //
  Serial.println("============================"); 
  Serial.print("analogReadTime-fastAnalogReadTime = "); Serial.print(analogReadTime - fastAnalogReadTime); Serial.println(" micro sec");
  Serial.print("analogReadTime/fastAnalogReadTime = "); Serial.print(analogReadTime / fastAnalogReadTime); Serial.println(" How much");
  Serial.println("***************************");
    return true;
  }
  return false;
}


7. Пункт убрать все лишние операции вывода данных в Serial порт

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

 

 

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
}
void loop() { //*************************************  loop()
  // Минимальное значение для 16 разрядного, -32768 а максимальное 32767
  // выход за эти пределы приведет к потере знаковых разрядов и ошибочным вычислениЯм
  testSpeed();
}
bool testSpeed() //*************************************  testSpeed()
{
  static uint32_t goTime = 0;
  if (goTime < millis())
  {
    goTime = millis() + 10000; //+10секунд
  uint8_t portB_in_data;
  int32_t summa = 0;
  uint32_t startTime;
  uint32_t stopTime;
  uint32_t analogReadTime;
  uint32_t fastAnalogReadTime;
 // analogReadResolution(12);  //только для продвинутых плат
  startTime = micros(); //Запомимнаем микросекунды старта цикла
  for (int i = 0; i < 10000; i++)
  {
    summa=summa+1;
    Serial.println(summa);
  }
  stopTime = micros(); //Запомимнаем микросекунды старта цикла
  analogReadTime = stopTime - startTime;
  Serial.print(summa);  Serial.print(" Serial.print() Operations ="); Serial.print(analogReadTime); Serial.println(" micro sec"); //
 // analogReadResolution(10);  //только для продвинутых плат
  startTime = micros(); //Запомимнаем микросекунды старта цикла
  for (int i = 0; i < 10000; i++)
  {
    summa=summa+1;
  }
  stopTime = micros(); //Запомимнаем микросекунды старта цикла
  fastAnalogReadTime = stopTime - startTime;
  Serial.print(summa); Serial.print(" Not Serial.print() Operations ="); Serial.print(fastAnalogReadTime); Serial.println(" micro sec"); //
  Serial.println("============================"); 
  Serial.print("analogReadTime-fastAnalogReadTime = "); Serial.print(analogReadTime - fastAnalogReadTime); Serial.println(" micro sec");
  Serial.print("analogReadTime/fastAnalogReadTime = "); Serial.print(analogReadTime / fastAnalogReadTime); Serial.println(" How much");
  Serial.println("***************************");
    return true;
  }
  return false;
}

Разница в скорости просто огромная и это в 1250133 раза!!!! Дело в том, что когда буфер отправки порта заполнен, контроллер ждет его освобождения т.е. передачи данных, а скорость передачи несмотря на 115200 бит в секунду мала в сравнении со скоростью расчетов.

 

ОСТАВИТЬ КОММЕНТАРИЙ

Форма авторизации

ВОЙТИ С ПОМОЩЬЮ:
ИЛИ Авторизация на сайте:

или



X

Написать сообщение:

Укажите свой номер телефона или e-mail для обратной связи
- e-mail
ИЛИ
- номер телефона

Текст сообщения: