Arduino Nano работает очень медленно, хотя расчеты просты и задержек нет.

arduino-nano performance ssd1306

Я работаю над крошечной игрой в пинг-понг на базе Arduino Nano. Его режимы: «человек против человека». и «Человек против компьютера»; (компьютер просто пытается удержать ракетку в том же положении Y, что и мяч). Есть 2 "вверх" кнопки, 2 «вниз»; кнопки, два 7-сегментных индикатора счета и 2 регистра сдвига, управляющие ими. Дисплей — SSD1306 (128x32 пикселей).

Я публикую здесь весь код, потому что не знаю, в чем проблема.

#include <Adafruit_SSD1306.h>

// Кнопки вверх/вниз
#define UP1 2  
#define DOWN1 3
#define UP2 4
#define DOWN2 5

// Регистры сдвига, управляющие индикаторами оценки
#define DATA1 6
#define CLOCK1 7
#define LATCH1 8
#define DATA2 9
#define CLOCK2 10
#define LATCH2 11

#define RANDOM_NUMBERS A0  // Этот порт ни к чему не подключен и используется для получения случайного начального значения
#define WRAP 118  // Текст на дисплее переносится на эту позицию

#define SSD1306_ADDRESS 0x3C  // I2C-адрес дисплея

Adafruit_SSD1306 ssd1306;

const byte numbers[10] = {  // Арабские числа для показателей оценки
  0b1111110, 0b0011000, 0b1101101, 0b0111101, 0b0011011, 0b0110111, 0b1110111, 0b0011100, 0b1111111, 0b0111111
};

short currentDirection = -1;
short directionState = -1;  // Порядковый номер следующего хода в периоде траектории мяча
short nextMove = -1;
short ballX = 63;
short ballY = 15;
short racket1 = 12;
short racket2 = 12;

short score1 = 0;
short score2 = 0;
bool gameOver = false;

bool playingWithComputer = false;

void setup() {
  pinMode(UP1, INPUT_PULLUP);
  pinMode(DOWN1, INPUT_PULLUP);
  pinMode(UP2, INPUT_PULLUP);
  pinMode(DOWN2, INPUT_PULLUP);
  pinMode(DATA1, OUTPUT);
  pinMode(CLOCK1, OUTPUT);
  pinMode(LATCH1, OUTPUT);
  pinMode(DATA2, OUTPUT);
  pinMode(CLOCK2, OUTPUT);
  pinMode(LATCH2, OUTPUT);
  
  randomSeed(analogRead(RANDOM_NUMBERS));

  updateScores();

  ssd1306.begin(SSD1306_SWITCHCAPVCC, SSD1306_ADDRESS);
  ssd1306.display();
  delay(2000);  // Задержка для отображения логотипа Adafruit
  ssd1306.clearDisplay();

  ssd1306.setTextSize(1);
  ssd1306.setTextColor(WHITE);
  ssd1306.setCursor(0, 0);  // Курсор должен находиться в этой позиции, чтобы правильно рисовать объекты

  printLongText(5, 0, "Press \"up\" to play with computer, otherwise press \"down\"");

  while (true) {
    if (!digitalRead(UP1) || !digitalRead(UP2)) {  // Оба "up" Кнопки (а также обе кнопки «вниз») обрабатываются одинаково, если только нет игры «человек против человека».
      playingWithComputer = true;
      break;
    }
    if (!digitalRead(DOWN1) || !digitalRead(DOWN2)) break;

    delay(5);  // Эта задержка здесь, чтобы избежать перегрузки процессора
  }

  ssd1306.clearDisplay();
  ssd1306.display();
  
  delay(100);  // Эта задержка предназначена для того, чтобы показать пользователю, что ему больше не нужно удерживать кнопку
}

void loop() {
  if (gameOver) {
    delay(1000);
    return;
  }

  if (nextMove < 0) {
    pickDirection();
    return;
  }

  short oldDirection = currentDirection;
  switch (getBounceType()) {
    case 0:
      verticalFlipDirection();
      ballY++;
      break;
    case 1:
      verticalFlipDirection();
      ballY--;
      break;
    case 2:
      if (ballY < (racket1 - 1) || ballY > (racket1 + 8)) {  // Определяет, долетел ли мяч до ракетки или нет (истина, если нет)
        score2++;

        // Мяч попадает в центр экрана после гола
        ballX = 63;
        ballY = 15;
        pickDirection();

        updateScores();
      }
      else {
        horizontalFlipDirection();
        ballX++;
      }

      break;
    case 3:
      if (ballY < (racket2 - 1) || ballY > (racket2 + 8)) {
        score1++;
        ballX = 63;
        ballY = 15;
        pickDirection();
        updateScores();
      }
      else {
        horizontalFlipDirection();
        ballX--;
      }

      break;
    case 4:
      if (racket1 > 1) {
        score2++;
        ballX = 63;
        ballY = 15;
        pickDirection();
        updateScores();
      }
      else {
        reverseDirection();  // Это происходит, когда мяч пробежал точно до угла своей зоны
        ballX++;
        ballY++;
      }

      break;
    case 5:
      if (racket2 > 1) {
        score1++;
        ballX = 63;
        ballY = 15;
        pickDirection();
        updateScores();
      }
      else {
        ballX--;
        ballY++;
        reverseDirection();
      }

      break;
    case 6:
      if (racket1 > 1) {
        score2++;
        ballX = 63;
        ballY = 15;
        pickDirection();
        updateScores();
      }
      else {
        reverseDirection();
        ballX++;
        ballY--;
      }

      break;
    case 7:
      if (racket2 > 1) {
        score1++;
        ballX = 63;
        ballY = 15;
        pickDirection();
        updateScores();
      }
      else {
        ballX--;
        ballY--;
        reverseDirection();
      }

      break;
  }
  if (oldDirection != currentDirection) directionState = 0;  // Сбрасываем состояние направления, если мяч отскочил
  pickMove();  // Следующий ход мог измениться, если мяч отскочил
  
  switch (nextMove) {
    case 0:
      ballY--; 
      break;
    case 1:
      ballX++;
      break;
    case 2:
      ballY++;
      break;
    case 3:
      ballX--;
      break;
  }
  directionState++;
  pickMove();

  ssd1306.clearDisplay();
  ssd1306.drawRect(ballX, ballY, 2, 2, WHITE);

  if (playingWithComputer) {
    if (!digitalRead(UP1) || !digitalRead(UP2)) {
      if (racket1 > 0) racket1--;
    }
    if (!digitalRead(DOWN1) || !digitalRead(DOWN2)) {
      if (racket1 < 24) racket1++;
    }

    //Это алгоритм, используемый компьютером — он просто пытается удерживать ракетку, управляемую компьютером, в том же положении Y, что и мяч.
    if ((racket2 + 3) > ballY) {
      if (racket2 > 0) racket2--;
    }
    if ((racket2 + 3) < ballY) {
      if (racket2 < 24) racket2++;
    }
  } else {
    if (!digitalRead(UP1)) {
      if (racket1 > 0) racket1--;
    }
    if (!digitalRead(DOWN1)) {
      if (racket1 < 24) racket1++;
    }
    if (!digitalRead(UP2)) {
      if (racket2 > 0) racket2--;
    }
    if (!digitalRead(DOWN2)) {
      if (racket2 < 24) racket2++;
    }
  }

  ssd1306.drawRect(0, racket1, 2, 8, WHITE);
  ssd1306.drawRect(126, racket2, 2, 8, WHITE);
  
  ssd1306.display();
}

// Эта функция выбирает случайное направление для мяча
void pickDirection() {
  currentDirection = random(0, 20);
  directionState = 0;
  pickMove();
}

// Эта функция определяет следующее движение мяча (вверх/вправо/вниз/влево) на основе его текущего направления и состояния направления
void pickMove() {
  short sector = getSector();  // Направления, ведущие вверх-вправо, находятся в секторе 0, те, которые идут вниз-вправо, находятся в секторе 1 и т. д.

  switch (currentDirection) {
    case 0:
    case 5:
    case 10:
    case 15:
      directionState = directionState % 5;  // Сбрасываем состояние направления, когда мяч достигает следующего периода своей траектории

      // В определенных состояниях направления мяч движется в своем основном направлении (например, при почти вертикальной траектории основное направление будет вверх)
      // В остальных состояниях уходит во вторичное направление, которое при добавлении к основному формирует реальную траекторию мяча
      // Секторы делятся на 2 части. Первая часть включает в себя 3 траектории, основной номер направления которых (вверх - 0, вправо - 1, вниз - 2, влево - 3) совпадает с номером сектора.
      // Второстепенные направления в первой части — это номер сектора плюс 1
      // Во второй части основное направление — (sector_number + 1) % 4, а вторичное — номер сектора
      // Поддерживается 20 траекторий (по 5 в каждом из 4 секторов). Они представлены в Ping-Pong.png.
      // Первые траектории в каждом секторе (а также вторые и т.д.) имеют одинаковую структуру, поэтому блок кода только один
      // для обработки первых траекторий всех секторов. Однако их первичные и вторичные направления различаются в каждом секторе.
      // Следующая строка определяет, должен ли мяч теперь двигаться в основном или второстепенном направлении, и на основе этого устанавливает nextMove
      nextMove = (directionState == 0 || directionState == 1 || directionState == 3) ? sector : (sector + 1) % 4;
      return;
    case 1:
    case 6:
    case 11:
    case 16:
      directionState = directionState % 4;
      nextMove = (directionState < 2) ? sector : (sector + 1) % 4;
      return;
    case 2:
    case 7:
    case 12:
    case 17:
      directionState = directionState % 4;
      nextMove = (directionState == 0 || directionState == 2) ? sector : (sector + 1) % 4;
      return;
    case 3:
    case 8:
    case 13:
    case 18:
      directionState = directionState % 4;
      nextMove = (directionState < 2) ? getSecondPartMove(sector) : sector;
      return;
    case 4:
    case 9:
    case 14:
    case 19:
      directionState = directionState % 5;
      nextMove = (directionState == 0 || directionState == 1 || directionState == 3) ? getSecondPartMove(sector) : sector;
      return;
  }
}

// Просто помощник для PickMove (определяет основное направление траекторий второй части в определенном секторе)
short getSecondPartMove(short sector) {
  return (sector + 1) % 4;
}

// Эта функция определяет текущий тип отскока (-1 - отскока в данный момент нет, 0 - отскок от верхней стены,
// 1 - отскок от нижней стены, 2 - лицом к стене первого игрока, 3 - лицом к стене второго игрока,
// 4 - лицом к левому верхнему углу, 5 - к правому верхнему углу, 6 - к левому нижнему, 7 - к правому нижнему
// При счете 4-7 мяч меняет траекторию
short getBounceType() {
  if (ballX <= 2 && ballY <= 0) return 4;  // Сделаны отступы ballX, чтобы мяч отскакивал от ракеток, а не от границ экрана
  if (ballX >= 124 && ballY <= 0) return 5;
  if (ballX <= 2 && ballY >= 30) return 6;
  if (ballX >= 124 && ballY >= 30) return 7;

  if (ballX <= 2) return 2;
  if (ballX >= 124) return 3;

  if (ballY <= 0) return 0;
  if (ballY >= 30) return 1;

  return -1;
}

// Эта функция переворачивает текущую траекторию мяча вертикально, чтобы он отскочил от верхней/нижней стены
// (исходная и перевернутая траектории имеют взаимосвязь, которая различается на разных участках исходной траектории, см. эту функцию)
void verticalFlipDirection() {
  short sector = getSector();
  
  switch (sector) {
    case 0:
    case 1:
      currentDirection = 9 - currentDirection;
      return;
    case 2:
    case 3:
      currentDirection = 10 + (9 - (currentDirection - 10));
      return;
  }

  directionState = 0;
}

// То же, что и предыдущая функция, но здесь перевод горизонтальный
// (отношения между исходной и перевернутой траекториями различны)
void horizontalFlipDirection() {
  short sector = getSector();

  switch (sector) {
    case 0:
    case 3:
      currentDirection = 19 - currentDirection;
      return;
    case 1:
    case 2:
      currentDirection = 5 + (9 - (currentDirection - 5));
      return;
  }

  directionState = 0;
}

// Эта функция меняет траекторию мяча
void reverseDirection() {
  currentDirection = (currentDirection + 10) % 20;
  directionState = 0;
}

// Эта функция определяет сектор текущей траектории
short getSector() {
  if (currentDirection < 5) return 0;
  else if (currentDirection < 10) return 1;
  else if (currentDirection < 15) return 2;
  else return 3;
}

// Эта функция печатает текст на дисплее, перенося его в определенную позицию X
void printLongText(short cursorX, short cursorY, String text) {
  ssd1306.setCursor(cursorX, cursorY);
  
  int16_t x, y;
  uint16_t w, h;

  short lastSpace = -1;
  short i;

  // Добавляем символ за символом в текущую строку до тех пор, пока не будет достигнута позиция переноса
  // Параллельно находим последний пробел перед переносом (он будет там, где строка прерывается)
  for (i = 0; i < text.length(); i++) {
    if (text.charAt(i) == ' ') lastSpace = i;

    ssd1306.getTextBounds(text.substring(0, i + 1), 0, 0, &x, &y, &w, &h);

    if (x + w > WRAP && lastSpace >= 0) {
      ssd1306.println(text.substring(0, lastSpace + 1));  // Достигнута позиция переноса; разрыв текущей строки по индексу последнего пробела в ней и ее печать
      text = text.substring(lastSpace + 1);

      i = -1;  // Теперь то же самое со следующей строкой
      lastSpace = -1;
      ssd1306.setCursor(cursorX, ssd1306.getCursorY());
    }
  }
  ssd1306.println(text);  // печатаем оставшуюся часть текста

  ssd1306.display();

  ssd1306.setCursor(0, 0);  // Курсор должен находиться в этой позиции, чтобы правильно рисовать объекты
}

// Эта функция отображает баллы по соответствующим индикаторам и проверяет, закончилась ли игра
// (т.е. у одного из игроков результат 7)
void updateScores() {
  digitalWrite(LATCH1, false);
  shiftOut(DATA1, CLOCK1, MSBFIRST, numbers[score1]);  // Отправляем необходимую маску в сдвиговые регистры
  digitalWrite(LATCH1, true);

  digitalWrite(LATCH2, false);
  shiftOut(DATA2, CLOCK2, MSBFIRST, numbers[score2]);
  digitalWrite(LATCH2, true);

  if (score1 >= 7) {
    ssd1306.clearDisplay();
    printLongText(10, 12, "Player 1 wins!");
    ssd1306.display();

    gameOver = true;
  }
  if (score2 >= 7) {
    ssd1306.clearDisplay();
    printLongText(10, 12, "Player 2 wins!");
    ssd1306.display();

    gameOver = true;
  }
}

Посмотрите, как медленно движется мяч, хотя итерация loop этого не делает. не включает никаких задержек и не требует сложных вычислений. Преобразование в GIF не изменило скорость.

Моя схема выглядит так (это всего лишь тест, без кнопок, только подтягивания и без индикаторов оценок).

schematic

Ответы здесь не помогли. Изменение тактовой частоты I2C на 400 кГц тоже не помогло.

Итак, такая медлительность вызвана (не очень высокой) сложностью моего кода? Или это какой-то глюк? Пожалуйста, не предлагайте использовать библиотеки вместо моей собственной логики перемещения и подпрыгивания.

, 👍3

Обсуждение

Пробовали ли вы проверить, что именно занимает так много времени на каждой итерации loop(), используя millis() или micros()? Я предполагаю, что код отображения занимает довольно много времени. Вы также можете попробовать не очищать дисплей на каждой итерации, а рисовать поверх предыдущих прямоугольников новые черные прямоугольники. Может работать быстрее, @chrisl

Я воспроизвел эту проблему, так как у меня случайно оказался один из этих дисплеев. Комментирование очистки экрана не помогло., @Nick Gammon

Код занимает около 167 мс на итерацию цикла., @Nick Gammon

Хороший вопрос, кстати. Вы хорошо описали проблему, показали схему, показали код, описали то, что уже пробовали, и подробно объяснили, в чем проблема. Плюс у вас было видео проблемы. В целом отличная работа!, @Nick Gammon

Что касается вашего редактирования по поводу SPI, разница в скорости будет значительной. Согласно [моей странице о протоколах](http://www.gammon.com.au/forum/?id=10918) SPI может быть в 30 раз быстрее, чем I2C (в зависимости от тактовой частоты I2C), поэтому я предлагаю получить версия этой платы с SPI действительно обеспечит значительно более высокую частоту обновления. Затем вам, возможно, придется переработать способ отображения результатов, но [немного некорректный SPI](http://www.gammon.com.au/forum/?id=10892&reply=6#reply6) может быть вариантом для оценок. Оценки вряд ли нуждаются в таком быстром обновлении, и в любом случае данных будет намного меньше., @Nick Gammon

@NickGammon Но в своем вопросе я отметил, что изменение тактовой частоты I2C на 400 кГц (что в 4 раза быстрее обычной скорости) вообще не повлияло на ситуацию. Я не думаю, что использование SPI даст такое существенное улучшение., @SNBS

Да, но я где-то читал, что используемая вами библиотека все равно меняет тактовую частоту I2C на 400 кГц, поэтому ваше изменение не повлияет. Вот почему вы не увидели никаких улучшений. Но переход на SPI даст вам улучшение в 10 раз (поскольку тактовая частота I2C составляет 400 кГц, а не 100 кГц, согласно моим данным на связанной странице)., @Nick Gammon

Re «_Как жаль, что парень, опубликовавший этот пост, не объяснил, как он добился такой высокой частоты кадров!_»: он упоминает, что использует интерфейс SPI., @Edgar Bonet

@EdgarBonet Когда я это писал, я не знал, что SPI быстрее, чем I2C. *"Я не думаю, что интерфейс связи, I2C в моем случае и SPI в его случае, имеет большое значение."*, @SNBS


1 ответ


Лучший ответ:

10

Я вложил немного времени в ваш код. В качестве совета для вас и других, кто может захотеть использовать тайм-код, вот простой способ сделать это...

Класс таймера

Определите класс таймера следующим образом:

class timer
  {
  unsigned long start;
  const char sReason [50];

  public:

    // конструктор запоминает время создания
    timer (const char * s)
      {
      strncpy (sReason, s, sizeof (sReason) - 1);
      start = micros ();
      Serial.print (F("Start:  "));
      Serial.println (sReason);
      };

    // деструктор получает текущее время и отображает разницу
    ~timer ()
      {
      unsigned long t = micros ();
      Serial.print (F("Finish: "));
      Serial.print (sReason);
      Serial.print (F(" = "));
      Serial.print (t - start);
      Serial.println (F(" µs"));
      }
  };    // таймер окончания класса

Как использовать класс таймера

Если вы хотите, чтобы время "петли" тогда просто поместите это в начало:

void loop() {
  timer t ("Loop");
  ... другой код...
}  // конец цикла

Конструктор запускает отсчет времени, а деструктор (при выходе из loop) определяет разницу и отображает ее.

Чтобы синхронизировать определенный код, создайте "блок" вот так:

  {    // начало блока
  timer t ("Draw ball");
  ssd1306.clearDisplay();
  ssd1306.drawRect(ballX, ballY, 2, 2, WHITE);
  }    // конец блока

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

Естественно, чтобы все это работало, вам нужно выполнить Serial.begin (115200); в setup.


Результаты хронометража

Я добавил два вышеупомянутых вызова синхронизации, а также для рисования ракеток и обновления дисплея:

  {
  timer t ("Draw rackets");
  ssd1306.drawRect(0, racket1, 2, 8, WHITE);
  ssd1306.drawRect(126, racket2, 2, 8, WHITE);
  }

  {
  timer t ("ssd1306.display");
  ssd1306.display();
  }

Результаты, которые я получил:

Start:  Loop
Start:  Draw ball
Finish: Draw ball = 1812 µs
Start:  Draw rackets
Finish: Draw rackets = 1956 µs
Start:  ssd1306.display
Finish: ssd1306.display = 168944 µs
Finish: Loop = 178516 µs

Очевидно, что обновление дисплея занимает все время (каждый раз 169 мс, что означает, что вы будете выполнять около 5 циклов в секунду).

В соответствии с предложением на форуме Adafruit я изменил ваш конструктор на:

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 32
#define OLED_RESET -1
Adafruit_SSD1306 ssd1306(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

Теперь время:

Start:  Loop
Start:  Draw ball
Finish: Draw ball = 1780 µs
Start:  Draw rackets
Finish: Draw rackets = 1956 µs
Start:  ssd1306.display
Finish: ssd1306.display = 21192 µs
Finish: Loop = 30648 µs

Итак, это около 33 кадров в секунду, а не 5 кадров в секунду. Большое улучшение!

,

Спасибо вам за вашу работу! Сделал это, мяч теперь движется довольно быстро, что и было моей целью., @SNBS

Гораздо более высокая частота кадров может быть достигнута с помощью I2C за счет обновления только тех частей буфера дисплея, которые фактически изменились с момента предыдущего кадра. SSD1306 поддерживает это, но, увы, библиотека Adafruit\_SSD1306 этого не делает., @Edgar Bonet

@EdgarBonet Верно, особенно с чем-то простым, например, с понгом., @Nick Gammon