Как заставить Arduino надежно работать в качестве ведомого устройства SPI?

Я хочу, чтобы два Arduino Nanos взаимодействовали друг с другом с помощью SPI, в идеале со скоростью около 2 МГц или быстрее.

У меня есть два стандартных Nanos, работающих на частоте 16 МГц на 5 В.

  • Мастер использует SPI.transfer() для отправки массива по проводу.1 Для простоты устранения неполадок массив поочередно содержит кучу нулей или кучу из 255; но никогда оба.

  • Ведомое устройство просто возвращает десятичное число 66 для каждого байта. Это было выбрано потому, что его легко определить по логической трассировке.

  • Мастер проверяет ответы, и если есть какие-либо значения, отличные от 66, он выводит массив в UART.

Даже на скорости 125 Кбит, что является самой низкой доступной скоростью, я регулярно получаю поврежденные байты. Интересно, что в большинстве случаев (но не всегда!) поврежденный байт равен либо 0, либо 255 - другими словами, точно такой же, как байт, отправленный с главного сервера! Это кажется мне очень маловероятным совпадением.

Я проверил трафик с помощью логического анализатора, и кажется, что подчиненное устройство просто иногда решает не работать в соответствии с инструкциями; и вместо этого оно просто снова отправляет входные данные. Например, байт, выходящий на MISO, точно такой же, как байт, поступающий на MOSI. Но только иногда.

Что здесь происходит? Почему это не работает? И что я могу с этим поделать?


Мастер-скетч:

#define MOSI    12
#define SCK     13

#include <SPI.h>
#include "pins_arduino.h"
#include <avr/wdt.h>

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

    digitalWrite(SS, HIGH);
    pinMode(SS, OUTPUT);
    digitalWrite(SCK, HIGH);
    pinMode(SCK, OUTPUT);
    pinMode(MOSI, OUTPUT);

    SPI.setClockDivider(SPI_CLOCK_DIV128);      // 125 кГц
    SPI.begin();

    Serial.println("\n\nSTARTING...\n");
}

void do_transfer(byte attempt, byte payload) {

    // размер пакета 200
    byte buffer[200];

    // инициализировать пакет
    for (byte count = 0; count < sizeof(buffer); count++)
        buffer[count] = payload;

    // выполнить
    digitalWrite(SS, LOW);
    SPI.transfer(buffer, sizeof(buffer));
    digitalWrite(SS, HIGH);    // SS - вывод 10

    // подсчет результатов
    byte successes = 0, failures = 0;
    for (byte index = 0; index < sizeof(buffer); index++)
        if (buffer[index] == 66) successes++; else failures++;

    // печать результатов
    Serial.print("Attempt ");
    Serial.print(attempt);
    Serial.print(" with payload ");
    Serial.print(payload);
    Serial.print(": ");
    if (failures == 0)
        Serial.print("SUCCESS");
    else {
        Serial.print(successes);
        Serial.print(" correct bytes, ");
        Serial.print(failures);
        Serial.print(" incorrect bytes");
        for (byte index = 0; index < sizeof(buffer); index++) {
            if (index % 10 == 0)
                Serial.print("\n\t");
            if (buffer[index] == 66)
                Serial.print("-");
            else
                Serial.print(buffer[index]);
            if (index < sizeof(buffer) - 1)
                Serial.print("\t");
        }
    }
    Serial.println();
    Serial.println();
}

void loop(void) {
    delay(800);                                                         // дайте ведомому устройству время для готовности

    for (byte attempt = 1; attempt <= 10; attempt+=2) {
        do_transfer(attempt, 0x00);
        do_transfer(attempt+1, 0xFF);
        wdt_reset();
    }
    while(true);                                                        // завершение программы
}

Скетч ведомого:

#include "pins_arduino.h"

void setup(void) {
    pinMode(MISO, OUTPUT);          // MISO должен быть выходом
    SPCR |= _BV(SPE);               // включить SPI в подчиненном режиме
    SPCR |= _BV(SPIE);              // включить прерывания
    SPDR = 66;
}

ISR (SPI_STC_vect) { SPDR = 66; }   // просто отправляйте 66, пока коровы не вернутся домой

void loop (void) { }

Соединения: Следующие контакты на обоих Arduino соединены вместе:

  • SS (вывод 10)
  • МИСО (вывод 11)
  • MOSI (вывод 12)
  • SCK (вывод 13)
  • ВНД

1 Проблема также возникает, если я отправляю по одному байту за раз с помощью SPI.transfer(), если только (возможно) я не вставлю задержку в 1 мс между каждым байтом. Что, конечно, нелепо.

, 👍4

Обсуждение

Что происходит при меньших задержках между байтами (например, в микросекундном диапазоне)? Для меня это звучит так, как будто в случаях ошибок новый байт синхронизируется ведущим устройством до того, как ISR может быть запущен на ведомом устройстве (который заменяет последний полученный байт новым байтом ответа). Это может произойти, когда в данный момент запущен другой ISR. Вы также можете попытаться отключить прерывания Timer0 в подчиненном скетче, поскольку Timer0 используется для учета времени в ядре Arduino. Тогда такие функции, как millis (), micros () и delay(), больше не будут работать., @chrisl

@chrisl ваше предложение отключить таймер сработало. В основном. Но хотя сейчас он работает до 250 кГц, я получаю много ошибок на частоте 500 кГц. Многие байты MISO задерживаются на один бит., @Infinity Computers

Я получаю ошибочные '33' байта, возвращаемые при отправке нулей, и я получаю ошибочные '161' и '194' байты, возвращаемые при отправке 255. Это кажется очень специфичным. Есть какие-нибудь идеи?, @Infinity Computers

Честно говоря, я сомневаюсь, что у вас получится намного быстрее. Узким местом, очевидно, является то, что вы не можете записать буфер достаточно быстро. Нано просто слишком медленный. Другие пользователи этого сайта, несомненно, могут дать вам точную оценку необходимого времени для прерывания, подсчитав необходимые циклы., @chrisl

О конкретных значениях ошибок: В настоящее время я не уверен, является ли регистр данных SPI буферизованным. Если нет, то возможно, что ваш код записывает этот регистр, и в середине этого аппаратное обеспечение SPI уже отправляет половину записанных данных. Не уверен в этом, @chrisl

В техническом описании говорится: "когда SPI настроен как подчиненный, SPI гарантированно будет работать только при fosc / 4 или ниже". Так что я не думаю, что проблема в этом. Кроме того, согласно спецификации, регистр SPI имеет "двойную буферизацию в направлении приема"., @Infinity Computers


1 ответ


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

3

Ваша проблема здесь - это выбор времени. Я попробовал вашу настройку и обнаружил, что действительно получаю случайные ошибки на частоте 125 кГц. Отключение таймера 0 (как было предложено Chrisl) исправило это:

  power_timer0_disable();

Но хотя сейчас он работает до 250 кГц, я получаю много ошибок на частоте 500 кГц.

Хорошо, время между тактовыми импульсами теперь составляет 2 мкс, однако, как я описываю здесь, время, необходимое для выполнения ISR, такого как SPI_STC_vect, составляет 2,625 мкс, поэтому он не может справляться с необходимостью помещать туда данные каждые 2 с.

На самом деле у вас есть немного больше времени, чем 2 мкс, потому что критическое время - это время между окончанием первого байта (когда вызывается ISR) и временем до начала второго байта, когда мастер начнет выборку вашего ответа.

Смотрите скриншот ниже с указанием критического времени, выделенного желтым цветом.

SPI timing screenshot

Это время составляет 2,45 мкс, а ISR занимает 2,625 мкс. Теперь кое-что из этого - эпилог ISR, который на самом деле не будет иметь большого значения, но вы можете видеть, что дела обстоят туго. Трудно точно сказать, в какой момент процессор начинает вызывать ISR после поступления последнего бита, и длина инструкции, которую он выполняет в данный момент, может быть влияющим фактором.

Я заставил ваши скетчи работать на частоте 250 кГц со следующими поправками к ведомому устройству:

#include "pins_arduino.h"
#include <avr/power.h>
#include <avr/sleep.h>

void setup(void) {
  pinMode(MISO, OUTPUT);          // MISO должен быть выходом
  SPCR |= _BV(SPE);               // включить SPI в подчиненном режиме
  SPCR |= _BV(SPIE);              // включить прерывания
  SPDR = 66;
  power_timer0_disable(); 
  set_sleep_mode (SLEEP_MODE_IDLE);
}

ISR (SPI_STC_vect) { SPDR = 66; }   // просто отправляйте 66, пока коровы не вернутся домой

void loop (void) 
 {
 sleep_mode ();
 }

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

Как и ожидалось (по вышеуказанным причинам), он не работал на частоте 500 кГц.

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

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


Но в техническом описании указано, что скорости Fosc / 4 возможны в подчиненном режиме, который в данном случае будет составлять 4 МГц. Было бы это достижимо, если бы я удалил ISR и просто использовал цикл занятости?

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

char SPI_SlaveReceive(void)
{
/* Дождитесь завершения приема */
while(!(SPSR & (1<<SPIF)))
;
/* Вернуть регистр данных */
return SPDR;
}

Независимо от того, что они утверждают, то, что вы намереваетесь, должно быть физически возможным. Таким образом, с Fosc / 4, предполагая тактовую частоту 16 МГц, у вас есть 2 мкс для поступления байта (250 нс * 8). Таким образом, аппаратное обеспечение вполне может получить один байт за это время, и у вас может быть достаточно тактовых циклов, чтобы заметить его поступление и поместить его куда-нибудь в память (хотя это может быть затруднительно), но у вас не будет времени отправлять другой ответ после каждого входящего байта, потому что у вас есть только на это уходит около 250 нс, и цикл, который обнаруживает прерывание, вероятно, израсходует большую часть или все это.

Немного повозившись, я заставил его работать на частоте 250 кГц, где целью было просто, чтобы ведомое устройство возвращало некоторые данные (в конце концов, я предполагаю, что это то, что должно делать ведомое устройство).

Мастер

#define MOSI    12
#define SCK     13

#include <SPI.h>
#include "pins_arduino.h"
#include <avr/wdt.h>

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

    digitalWrite(SS, HIGH);
    pinMode(SS, OUTPUT);
    digitalWrite(SS, HIGH);

    digitalWrite(SCK, HIGH);
    pinMode(SCK, OUTPUT);
    pinMode(MOSI, OUTPUT);

    SPI.begin();
    SPI.setClockDivider(SPI_CLOCK_DIV64);     

    Serial.println("\n\nSTARTING...\n");
    
}

void do_transfer(byte attempt, byte payload) {

    // размер пакета 200
    byte buffer[200];

    // выполнить
    digitalWrite(SS, LOW);
    delay (1);    // дайте ведомому устройству время для реагирования
    SPI.transfer(buffer, sizeof(buffer));
    digitalWrite(SS, HIGH);    // SS - вывод 10

    // подсчет результатов
    byte successes = 0, failures = 0;
    for (byte index = 0; index < sizeof(buffer); index++)
        if (buffer[index] == index) successes++; else failures++;

    // печать результатов
    Serial.print("Attempt ");
    Serial.print(attempt);
    Serial.print(" with payload ");
    Serial.print(payload);
    Serial.print(": ");
    if (failures == 0)
        Serial.print("SUCCESS");
    else {
        Serial.print(successes);
        Serial.print(" correct bytes, ");
        Serial.print(failures);
        Serial.print(" incorrect bytes");
        for (byte index = 0; index < sizeof(buffer); index++) {
            if (index % 10 == 0)
                Serial.print("\n\t");
            if (buffer[index] == index)
                Serial.print("-");
            else
                Serial.print(buffer[index]);
            if (index < sizeof(buffer) - 1)
                Serial.print("\t");
        }
    }
    Serial.println();
    Serial.println();
}

void loop(void) {
    delay(800);                                                         // дайте ведомому устройству время для готовности

    for (byte attempt = 1; attempt <= 10; attempt+=2) {
        do_transfer(attempt, 0x00);
        do_transfer(attempt+1, 0xFF);
        wdt_reset();
    }
    while(true);                                                        // завершение программы
}

Подчинение

#include "pins_arduino.h"
#include <avr/power.h>
#include <avr/sleep.h>

const int TEST_SIZE = 200;

volatile byte my_data [TEST_SIZE];

volatile byte dummy;

ISR (PCINT0_vect)
 {
 // обработайте прерывание смены вывода для D8 на D13 здесь

  // игнорировать переход к ВЫСОКОМУ
  if (digitalRead (SS) == HIGH)
    return;
    
  SPDR = 0;
  
  for (byte i = 0; i < TEST_SIZE; i++)
    {
    SPDR = my_data [i];
    while(!(SPSR & (1<<SPIF)))
      {
      // если SS поднялся высоко, сдавайся
      if (PINB & (1 << 2))
        return;
      }
    } // конец цикла for

    
 }  // конец PCINT0_vect

void setup(void) {
  pinMode(MISO, OUTPUT);          // MISO должен быть выходом
  SPCR |= _BV(SPE);               // включить SPI в подчиненном режиме
  power_timer0_disable();
  for (int i = 0; i < TEST_SIZE; i++)
    my_data [i] = i;

  // прерывание смены контактов для D10
  PCMSK0 |= bit (PCINT2);  // требуется вывод 10
  PCIFR  |= bit (PCIF0);   // очистить все незавершенные прерывания
  PCICR  |= bit (PCIE0);   // включить прерывания смены контактов для D8 на D13

  SPDR = 0;

  set_sleep_mode (SLEEP_MODE_IDLE);

}


void loop (void) 
  {
  sleep_mode ();
  }

В следующем тактовом делителе (SPI_CLOCK_DIV32) Я начал время от времени получать ошибки. В конце концов, Fclk / 32 составляет всего 2 мкс на тактовый импульс и 16 мкс на байт, и необходимые вещи во внутреннем цикле должны занимать столько времени — я не рассчитывал время отдельных команд, вы можете это сделать. :)

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

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

,

Я понимаю. Но в техническом описании указано, что скорости Fosc / 4 возможны в подчиненном режиме, который в данном случае будет составлять 4 МГц. Было бы это достижимо, если бы я удалил ISR и просто использовал цикл занятости?, @Infinity Computers

См. Расширенный ответ выше., @Nick Gammon

Да, похоже, это правильный путь. Я внес несколько изменений в код и увеличил частоту кристалла до 20 МГц, и мне удалось достичь скорости 1,5 Мбит / с - хотя, по общему признанию, без какой-либо фактической обработки, даже без сохранения значения. Так что, вероятно, 1 Мбит / с будет достижим. Спасибо за помощь!, @Infinity Computers

Честно говоря, я никогда не думал о том, чтобы поместить busyloop * внутрь * прерывания. Это отличная идея., @Infinity Computers