Передача данных по RS-485, синхронизация ардуин

Тема в разделе "Проводная и беспроводная связь", создана пользователем Alex19, 4 янв 2015.

  1. Alex19

    Alex19 Гуру

    Добрый вечер.
    Ковыряюсь со своим Rov-ом.

    Есть 2 ардуины, передача данных осуществляется через RS-485, провод 200 метров. Одна ардуина отправляет, другая принимает, все работает.

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

    Постараюсь подробно пояснить, передача по RS-485. Вверху пульт, пока эту роль выполняет PS2 joystick он передает команды ардуине, она в свою очередь передает на сам Rov. В Rov-е стоит ардуина, которая принимает данные, она же передает данные телеметрии на пульт сверху.

    Есть очень большая вероятность потери данных, к примеру, с пульта была отправлена команда, перевел RS-485 в режим отправки, следовательно приемную ардуину нужно перевести в режим приема данных, а как узнать когда?

    Единственный шанс синхронизировать, а вот как пока не знаю, подскажите куда копать.

    Скорость передачи 115200.
    Команда от пульта не более 32 байт, пакет с данными телеметрии не более 64 байт, взято с запасом. Кол-во команд от пульта, не более 10-20 в секунду, пакетов с данными телеметрии на пульт сверху 10-20 в секунду.

    Другими словами можно договорится, о промежутках времени на прием и передачу. Но как увы не представляю.
     
  2. ANV

    ANV Гуру

    Делайте тогда чтобы мастером был пульт.
    Логика такая:
    - инициатор передачи всегда пульт
    - ROV всегда отвечает на команду пульта. В случае команды отвечает квитанцией "команда принята" или "ошибка CRC". Когда надо получить данные, пульт шлет "передать данные", в ответ квитанция и если команда верна, то еще и данные
     
    Alex19 нравится это.
  3. Alex19

    Alex19 Гуру

    Спасибо за ответ.
    Но суть Вашего предложения, увы не понял. Возможно из-за не понимания самого RS-485.

    Не понимаю, как ответит Rov если линия на пульте переведена на отправку, а Rov на прием. Rov должен переведен на отправку, а пульт на прием.

    Насколько я знаю, прежде чем получить данные, необходимо перевести ножку к которой подключен модуль RS-485 в нужный режим (прием или отправка) на каждой плате.

    Извините, что не уточнил, про плату RS-485, предположил, что это понятно из контекста. Ведь у ардуины нет RS-485.

    Когда данные получены, проверить их корректность, не составляет труда, будь-то собственной проверкой CRC или встроенной.

    UPD. С большой долей вероятности Вы правы, учитывая Ваш опыт и я просто не правильно понимаю RS-485. Сейчас буду изучать

    UPD2. Так и есть Вы оказались правы, сам RS-485 не регламентирует протокол передачи данных, пользовался им как обычным UART, поэтому не понимал Вас.

    Вы предлагаете использовать протоколы обмена мастер - слайв к примеру Modbus.
    Буду дальше разбираться.

    Большое спасибо!
     
    Последнее редактирование: 5 янв 2015
  4. ANV

    ANV Гуру

    По логике "владения" шиной
    - в состоянии покоя линию никто не держит, пусть висит на failsafe резисторах
    - мастер перед передачей делает enable драйверу и передает сообщение
    - после передачи сообщения делает disable
    - в это время приемник получает сообщение и выжидает intermessage gap (время после приема последнего байта, после которого сообщение пультом считается переданным, а шина отпущена)
    - ROV занимает шину, передает сообщение и отпускает шину.

    Собственно все.
     
    Alex19 нравится это.
  5. Alex19

    Alex19 Гуру

    Попробую сделать именно так, а тянуть дополнительные библиотеки, ой как не хочется.

    Большое спасибо!
     
  6. ANV

    ANV Гуру

    Еще пара моментов:
    1. Приемник никогда не должен верить передатчику.
    Это означает что не надо использовать блокирующие функции и проверять целостность данных.
    Т.е. не должно быть в коде "принять 8 байт" - если придет больше, то в буфере к следующему приему будет лишний байт, если меньше, то подвиснет до таймаута или приклеит первый байт от следующего пакета.
    2. Если начнутся глюки по 485, то это могут быть помехи от видеолинка. Попробовать их побороть можно намотав диффпару 485 несколько витков на ферритовое кольцо. Кольцо надо сделать как на стороне пульта, так и на стороне ROV и расположить его как можно ближе к драйверу 485.
     
    Alex19 нравится это.
  7. Unixon

    Unixon Оракул Модератор

    Если поставить еще по одному 485 драйверу с каждого конца и разделить функции приема и передачи между каналами, получится 422 интерфейс, который сразу в полнодуплексном режиме будет работать по тем же дифф. парам.
     
    Megakoteyka и Alex19 нравится это.
  8. Alex19

    Alex19 Гуру

    Спасибо, надо почитать, не слышал о таком.

    Главное, чтобы кол-во проводов осталось 2.
    В идеале 1 или даже 0:), но это мечты на таких расстояниях.

    UPD. Как я понял Вы предлагаете использовать 2 UART и на каждый повесить свой RS-485, один настроить на прием, другой на отправку. Увы из-за размеров могу использовать Arduino Nano, Mini, но там только 1 UART.

    И проводов становится 4, увы не подходит.
     
    Последнее редактирование: 5 янв 2015
  9. ANV

    ANV Гуру

    UART нужен один, проводов 4
     
  10. Unixon

    Unixon Оракул Модератор

    Эм... нет, тут я не аккуратновыразился. Провода добавятся (еще +2), конечно, но прием и передача будут по дифф. линии, так же как у полудуплексного 485.

    Нет, UART один. Но RX на один 485, а TX на другой.
     
  11. Alex19

    Alex19 Гуру

    Это сильно облегчает программирование.

    Но увы 4 провода на передачу данных, для меня не позволительная роскошь, пока их просто нет и не предвидеться:(.

    UPD. Код передачи данных решил взять от MultiWii, когда-то разбирал его. Чтобы отчасти не изобретать велосипед. Там уже передача данных по UART сделана на прерываниях, кольцевой буфер. В прерываниях буду взводить состояние прием или передача.

    Не сразу увидел.
    Спасибо, за то что делитесь опытом.

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

    С камерами еще не проводил серьезных тестов, пока просто проверил работает или нет, на простенькой, которая была.

    Код получается не простой для меня, но главное есть понимание.

    UPD. Передачу по принципу MultiWii сделал, кому любопытно можно посмотреть тут. А вот с RS-485 не получилось, проблема в одном из модулей RS-485 только принимает но не отправляет. Придут другие, вернусь к данной проблеме.
     
    Последнее редактирование: 30 янв 2015
  12. Alex19

    Alex19 Гуру

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

    Логика работы
    В Setup делаю предварительную настройку
    Код (Text):

      // Настраиваем таймер 2 режим Normal.
      // Регистр TCCR2A - Регистр управления A. Настройка режима таймера/счетчика Т2.
      // Биты WGM21 (1) , WGM20 (0) регистра TCCR2A устанавливают режим работы таймера/счетчика T2.
      // Регистр TCCR2B - Регистр управления B.
      // Биты CS22, CS21, CS20 (Clock Select) - определяют источник тактовой частоты для таймера/счетчика Т2 и задают коэффициент предделителя.
      // Регистр TCNT2 - Счетный регистр таймера/счетчика T2.
      // Регистр TIFR2 - Регистр флагов прерываний для таймера/счетчика T2.
      // Бит TOV2 - Бит автоматически очищается при запуске обработчика прерывания по переполнению счетного регистра. Также это можно сделать программно, записав 1 в соответствующий флаг.
      // Регистр TIMSK2 - Конфигурационный регистр разрешения прерываний таймера/счетчика T2.
      // Бит TOIE2 - Бит TOIE2 0 запрещает прерывание по событию переполнение, 1 - разрешает.
      TCCR2A = 0;  // Устанавливаем регистр TCCR2A в 0
      TCCR2B = 0;  // Устанавливаем регистр TCCR2B в 0
      TCCR2A = (0<<WGM21)|(0<<WGM20);    // Устанавливаем режим работы Normal таймера/счетчика T2
      TCNT2  = 130;                      // Задаем начальное значение таймера/счетчика T2
      TIFR2 = (1<<TOV2);                 // Сброс флага TOV2 в регистре TIFR2
      TIMSK2 |= (1 << TOIE2);            // Разрешаем прерывание по переполнению
    Разумеется начальное значение для этой строчки TCNT2 = 130; лучше вынести в глобальную переменную (если в программе планируется изменять данное значение) или константу, к примеру
    Код (Text):

    volatile uint8_t T2BeginValue = 130;
    TCNT2  = T2BeginValue;
     
    или
    Код (Text):

    #define T2BeginValue 130
    TCNT2  = T2BeginValue;
     
    В прерывании по опустошению буфера UART, после того как последний бит отправлен и мы отключаем это прерывание, запускаем таймер
    Код (Text):
     
            // Регистр TCNT2 - Счетный регистр таймера/счетчика T2.
            TCNT2 = 130;              // Задаем начальное значение таймера/счетчика T2
            // Регистр TCCR2B - Регистр управления B.
            // Биты CS22, CS21, CS20 (Clock Select) - определяют источник тактовой частоты для таймера/счетчика Т2 и задают коэффициент предделителя.
            TCCR2B |= (1 << CS22);    // Задаем значение тактовой частоты (Частота МК/64, период тактовой частоты таймера равен 0,004мс) таймера/счетчика T2
    Привожу код прерывания целиком, лишь для наглядности, пока не привел все к подобающему стилю и не усыпал комментариями
    Код (Text):
    /// <summary>
    // Прерывание по опустошению буффера UART
    /// </summary>
    #if defined(ArduinoProMini) || defined(ArduinoMega)
      #if defined(ArduinoProMini)
      ISR(USART_UDRE_vect)
      {
      #endif
      #if defined(ArduinoMega)
      ISR(USART0_UDRE_vect)
      {
      #endif
        uint8_t t = UARTTailTX[0];
        if (UARTHeadTX[0] != t)
        {
          if (++t >= TXBufferSize)
          {
            t = 0;
          }
          UDR0 = UARTBufferTX[t][0];
          UARTTailTX[0] = t;
        }
        if (t == UARTHeadTX[0])
        {
          UCSR0B &= ~(1<<UDRIE0);
         
          #if defined(Alex) // Времянка для тестов
            // Регистр TCNT2 - Счетный регистр таймера/счетчика T2.
            TCNT2 = 130;              // Задаем начальное значение таймера/счетчика T2
            // Регистр TCCR2B - Регистр управления B.
            // Биты CS22, CS21, CS20 (Clock Select) - определяют источник тактовой частоты для таймера/счетчика Т2 и задают коэффициент предделителя.
            TCCR2B |= (1 << CS22);    // Задаем значение тактовой частоты (Частота МК/64, период тактовой частоты таймера равен 0,004мс) таймера/счетчика T2
          #endif
        }
      }
    #endif
    #if defined(ArduinoMega) || defined(ArduinoProMicro)
      ISR(USART1_UDRE_vect)
      {
        uint8_t t = UARTTailTX[1];
        if (UARTHeadTX[1] != t)
        {
          if (++t >= TXBufferSize)
          {
            t = 0;
          }
          UDR1 = UARTBufferTX[t][1];
          UARTTailTX[1] = t;
        }
        if (t == UARTHeadTX[1])
        {
          UCSR1B &= ~(1<<UDRIE1);
        }
      }
    #endif
    #if defined(ArduinoMega)
      ISR(USART2_UDRE_vect)
      {
        uint8_t t = UARTTailTX[2];
        if (UARTHeadTX[2] != t)
        {
          if (++t >= TXBufferSize)
          {
            t = 0;
          }
          UDR2 = UARTBufferTX[t][2];
          UARTTailTX[2] = t;
        }
        if (t == UARTHeadTX[2])
        {
          UCSR2B &= ~(1<<UDRIE2);
        }
      }
      ISR(USART3_UDRE_vect)
      {
        uint8_t t = UARTTailTX[3];
        if (UARTHeadTX[3] != t)
        {
          if (++t >= TXBufferSize)
          {
            t = 0;
          }
          UDR3 = UARTBufferTX[t][3];
          UARTTailTX[3] = t;
        }
        if (t == UARTHeadTX[3])
        {
          UCSR3B &= ~(1<<UDRIE3);
        }
      }
    #endif
     
    Перед отправкой данный в UART, подаем на пин управления моду RS-485 - 1 и включаем прерывание по опустошению буфера UART
    Код (Text):
    WritePin(RS485PinMaster, HIGH);
    WritePin - это аналог digitalWrite, но через регистры позаимствован из проекта 3D принтера Marlin.

    Весь код, для наглядности
    Код (Text):
    /// <summary>
    // Отправляем данные по UART, включаем прерывание по опустошению буфера UART
    /// </summary>
    /// <param name="port">Номер порта</param>
    void UARTSendData(uint8_t port)
    {
      // UDRIE - Разрешение прерывания при очистке регистра данных UART. Если данный разряд установлен в «1», то при установке флага UDRE в регистра UCSRA  генерируется прерывание.
      #if defined(Alex)
        WritePin(RS485PinMaster, HIGH);
      #endif
     
      #if defined(ArduinoProMini)
        UCSR0B |= (1<<UDRIE0);
      #endif
      #if defined(ArduinoProMicro)
        switch (port)
        {
          case 0:
            while(UARTHeadTX[0] != UARTTailTX[0])
            {
              if (++UARTTailTX[0] >= TXBufferSize)
              {
                UARTTailTX[0] = 0;
              }
              USB_Send(USB_CDC_TX,UARTBufferTX[UARTTailTX[0]],1);
            }
            break;
          case 1: UCSR1B |= (1<<UDRIE1); break;
        }
      #endif
      #if defined(ArduinoMega)
        switch (port)
        {
          case 0: UCSR0B |= (1<<UDRIE0); break;
          case 1: UCSR1B |= (1<<UDRIE1); break;
          case 2: UCSR2B |= (1<<UDRIE2); break;
          case 3: UCSR3B |= (1<<UDRIE3); break;
        }
      #endif
    }
    Само прерывание
    Код (Text):
    // Прерывание таймера/счетчика T2 по переполнению
    ISR(TIMER2_OVF_vect)
    {
      WritePin(RS485PinMaster, LOW);
     
      // Регистр TCCR2B - Регистр управления B.
      // Биты CS22, CS21, CS20 (Clock Select) - определяют источник тактовой частоты для таймера/счетчика Т2 и задают коэффициент предделителя.
      TCCR2B = 0;      // Таймер остановлен
    }
     
    Если, допустил где-то неточности, надеюсь более опытные участники меня поправят.
    Еще раз спасибо всем, кто откликнулся.