Реализация UART + RS-485

Тема в разделе "Микроконтроллеры AVR", создана пользователем Alex19, 18 дек 2015.

  1. Alex19

    Alex19 Гуру

    Данная реализация (не библиотека, пришлось потратить пару часов, чтобы убрать специфические функции моих проектов) была сделана давно, все думал довести до ума, но сейчас сконцентрировался на изучение Raspberry, руки не доходят до AVR. Впрочем даже в существующим виде она вполне работа способна и надежна, если учитывать ее особенности.

    Но так, как в смежной теме была поднята тема UART, решил опубликовать. Начнем..

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

    Сами пакеты представляют собой следующую последовательность ? (символ начала пакета) С (символ действия, в нашем случае команда) X (длина структуры, до 255) CN (номер команды) XS (сама структура с данными) CRC (контрольная сумма). Реализация на самая легкая учитывая саму структуру пакетов. И просто не годится для простой передачи данных (она тоже поддерживается данной библиотекой, но о ней чуть позже). Она будет полезна при передачи большого кол-ва критических данных.

    Код в прищепке, тестировалось Arduino IDE, UP (Atmega2560), Down (Atmega328p).

    Подробнее завтра.

    UPD.
    RS485 реализован в обе стороны, пример UPмастер который всегда инициализирует общение, пока мастер не послал команду или запрос, слейв (в примере Down) не начинает общение.

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

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

    Теперь о достоинствах и недостатках данной реализации.
    Начнем с меда.
    1. Работа на прерываниях.
    2. Кольцевой буфер.
    3. Передача структур.
    4. Проверка полученных данных.
    5. Работа с RS-485 в обе стороны.
    6. Относительно быстрая.

    Теперь деготь, его увы не мало, но все решаемо.
    1. Это не библиотека.
    Впрочем ее не сложно оформить в виде библиотеки.
    2. В примерах, поддержка лишь ATmega328P и ATmega2560.
    Легко решается:
    2.1. выкидываем FastIO.hи меняем на свою библиотеку или просто на работу с регистрами настройку и переключение пинов. Вы можете дополнить ее, если понравилась под нужный чип, в оригинальном FastIO.h -https://github.com/MarlinFirmware/Marlin/blob/RC/Marlin/fastio.h поддерживается куда больше чипов.
    2.2 Надо поменять настройку 2 пинов в setup.

    Код (C++):

      // Настройка дебага пина
      MC_SET_PIN_OUTPUT(LED_PIN);
      MC_WRITE_PIN(LED_PIN, LOW);

      //TODO: Проверять необходимо ли использование RS485.
      // Настройка пина управления RS485
      MC_SET_PIN_OUTPUT(RS485_PIN_MASTER);
     
    2.3 Изменить определение эти пинов в файле Config.h
    Код (C++):

    // Пин Arduino для RS485.
    #define RS485_PIN_MASTER   2

    // Пин Led для Arduino.
    #define LED_PIN           13
    2.4 В файле UART.cppпройтись поиском RS485_PIN_MASTER и заменить на свою работу с портами.

    После этого появится поддержка ATmega32U4, включая его софт Serial (софт Serial без прерываний).

    2.5 Дальше чуть сложнее, идем в Config.h и добавляем свой чип в этот список (к примеру тот же Atmega8).

    Код (C++):
    / Определяем тип ардуины.
    #if defined(__AVR_ATmega328P__)
    // Если ArduinoProMini.
    #define ARDUINO_PRO_MINI
    #endif
    #if defined(__AVR_ATmega32U4__)
    // Если ArduinoProMicro.
    #define ARDUINO_PRO_MICRO
    #endif
    #if defined(__AVR_ATmega2560__)
    // Если ArduinoMega.
    #define ARDUINO_MEGA_2560
    #endif
    #else
    #error This board not support!
    #endif
    К примеру так
    Код (C++):
    #if defined(__AVR_ATmega8__)
    // Если ATmega8.
    #define AVR_ATmega8
    #endif
    2.6 Открываем свой pdfна Ваш чип (взяли к примеру Atmega8), регистры совпадают (кроме одного нюанса номера UARTв регистре, у Atmega8 их нет). Идем в UART.cpp и внимательно смотрим проверки добавляя в варианты компиляции, нашу реализацию.

    И так у Atmega8 немного по другому называются прерывания USART_RXC_vect, USART_TXC_vect. И нет цифры UART в регистрах.

    Возьмем к примеру RX и изменим его для Atmega8.
    Код (C++):

    #if defined(AVR_ATmega8)
      ISR(USART_RXС_vect)  { StoreUARTInBuf(UDR, 0); }
    #endif
    #if defined(ARDUINO_PRO_MINI)
      ISR(USART_RX_vect)  { StoreUARTInBuf(UDR0, 0); }
    #endif
    #if defined(ARDUINO_PRO_MICRO)
      ISR(USART1_RX_vect)  { StoreUARTInBuf(UDR1, 1); }
    #endif
    #if defined(ARDUINO_MEGA_2560)
      ISR(USART0_RX_vect)  { StoreUARTInBuf(UDR0, 0); }
      ISR(USART1_RX_vect)  { StoreUARTInBuf(UDR1, 1); }
      ISR(USART2_RX_vect)  { StoreUARTInBuf(UDR2, 2); }
      ISR(USART3_RX_vect)  { StoreUARTInBuf(UDR3, 3); }
    #endif
    И так везде.

    Сделав для Atmega8, данный код должен подойти и к ATtiny2313 и другим имеющим одинаковые регистры управления.

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

    Решение.

    3.1. Идем в файл UART.cpp, находим - Прерывание по завершению отправки по каналу UART.
    Вокруг него вставляем
    Код (C++):

    #if defined(UART_USE_RS485)

    Тут обработчик прерывания

    #endif
     
    3.2. В файле UART.cppнаходим все регистры TXCIEn и так же добавляем проверку на UART_USE_RS485.

    4. Для контролеров у которых более 1 UARTформирует массивы под кольцевые буферы на все порты, не зависимо от того используются они или нет. Тут простого решения нет, как вариант создать массив в котором будут номера использованных UART, но это потребует вникнуть в логику программы.

    5. Использует 3 массив, в файле Protocol.cpp. И так же как и в четвертом пункте формирует массив на все порты. Можно попробовать такое же решение, как и в 4 пункте. Так же можно отказаться от него и записывать непосредственно в структуру, минус такого подхода в том, что контрольная сумма может не совпасть, а мы уже залили новые не верные данные.

    Чуть позже поясню ее работу и настройку.

    UPD2. На работе буду выкраивать время в промежутках.
    Теперь о особенностях работы.

    1. Здесь используется кольцевой буфер, поэтому в loop нельзя использовать, то что может остановить программу delay, pulseIn и т.д. Желательно делать Ваш код скоростным, в противном случае Вы можете потерять данные, они просто будут перезаписаны в кольцевом буфере.

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

    Сами файлы, начнем с простых.
    1. FastIO.h библиотека работы с портами. Взята из проекта Marlin (так же интересный проект, можно почерпнуть много интересного) от 3D принтера, урезанный мной. Общие для обоих примеров.
    2. Type.h в нем хранятся структуры для передачи данных, ни чего особенного тут нет. Общие для обоих примеров.
    3. Config.h остановимся немного подробнее на них. Общие для обоих проектов, кроме секции - Настройки UART (причина разные контролеры). Пример UP:
    Код (C++):
    // Порт UART.
    #define UART_PORT            1                  
    // Скорость порта UART.
    #define UART_PORT_SPEED            115200UL
    // Порт UART.
    #define UART_DEBUG_PORT                 0
    // Скорость порта UART.
    #define UART_DEBUG_SPEED                115200UL
    // Использование RS485.
    #define UART_USE_RS485
    // Какой порт используем под RS485.
    #define UART_RS485_PORT                 1
    // Отладка по UART
    #define UART_HARDWARE_DEBUG                        
    UART_PORT номер основного порта (нумерация AVR, UART0, UART1 и т.д.).
    UART_PORT_SPEED скорость основного порта.
    UART_DEBUG_PORT номер порта для дебага.
    UART_DEBUG_SPEED скорость порта дебага.
    UART_USE_RS485 использование RS485.
    UART_RS485_PORT номер порта на котором висит RS485, в данной реализации равен UART_PORT.
    UART_HARDWARE_DEBUG отладка по UART. Подключение дополнительных функций для отладки в файле Protocol.cpp.

    Пример Down:
    Код (C++):

    // Порт UART.
    #define UART_PORT                  0
    // Скорость порта UART.
    #define UART_PORT_SPEED            115200UL
    // Скорость порта UART.
    #define UART_DEBUG_SPEED                115200UL
    // Использование RS485.
    #define UART_USE_RS485
    // Какой порт используем под RS485.
    #define UART_RS485_PORT                 0
    // Отладка по UART
    #define UART_SOFTWARE_DEBUG
    Остановимся на тех что отличаются.
    Нет UART_DEBUG_PORT, пример Down написан под Atmega328p у него только 1 UART, поэтому нет и дебаг порта, который поддерживается данной реализацией.
    UART_DEBUG_SPEED скорость SoftwareSerial, вставил в код, чтобы проверить примеры.
    UART_SOFTWARE_DEBUG вставил в код, чтобы проверить примеры, загрузка библиотеки SoftwareSerial в .ino

    4. UART.cpp и UART.h общие для обоих примеров, код работы непосредственно с UART, практически полностью повторяют проект MultiWii, за исключением поддержки RS-485. В UART.cpp интересного не много, в UART.h есть секция настройки.
    Код (C++):
    // Определяем кол-во UART-тов в зависимости от платы
    #if defined(ARDUINO_MEGA_2560)
      // Если ArduinoMega
      #define UARTNumber 4
    #elif defined(ARDUINO_PRO_MICRO)
      // Если ArduinoProMicro
      #define UARTNumber 2
    #else
      // Иначе предполагается ArduinoProMini
      #define UARTNumber 1
    #endif

    // Размер буфера RX
    #define RXBufferSize 64

    // Размер буфера TX
    #define TXBufferSize 64
    Здесь определяются настройки размерности кольцевых буферов на прием и отправку.

    В файле UART.cpp идет их объявление на основании данных параметров.
    Код (C++):
    // UARTBufferRX - буфер RX для хранения данных
    static uint8_t UARTBufferRX[RXBufferSize][UARTNumber];
    и
    Код (C++):
    // UARTBufferTX - буфер X для хранения данных
    static uint8_t UARTBufferTX[TXBufferSize][UARTNumber];
    Тут мы видим ложку дегтя под номером 4. Если у Вас Atmega32u4 или Atmega2560, а нужен только 1 порт, массив все равно будет выделен под все порты, что потребует дополнительной оперативки.
     

    Вложения:

    • UART_UP.zip
      Размер файла:
      15,9 КБ
      Просмотров:
      467
    • UART_Down.zip
      Размер файла:
      15,4 КБ
      Просмотров:
      524
    Последнее редактирование: 18 дек 2015
    Radius, DrProg и ИгорьК нравится это.
  2. Alex19

    Alex19 Гуру

    5. Protocol.cpp и Protocol.h так же из примера MultiWii, но изменений больше.
    На этих файлах остановимся подробнее.

    Функция HeadCmdSend, формирует заголовок команды.
    Принимает 3 параметра.
    uint8_t err - в простой реализации, когда есть только команды, в нем нет смысла. А вот если у Вас есть сообщения об ошибках или мало 255 команд, можно менять символ действия (С (символ действия, в нашем случае команда)). Тем самым расширить кол-во команд, сделать разные пакеты. В данной реализации не используется.

    uint8_t s - размер структуры в байтах (X (длина структуры, до 255)).

    uint8_t c - номер команды (CN (номер команды)).

    Пример вызова
    Код (C++):
    HeadCmdSend(0, 1, 2);
    Функция SerializeStruct, передает саму структуру в буфер TX.
    Принимает 2 параметра.
    uint8_t *cb - сама структура.

    uint8_t siz - размер структуры в байтах.
    Пример вызова
    Код (C++):
    SerializeStruct((uint8_t*)&rovDataS, 1);
    где структура rovDataS
    Код (C++):
    typedef struct
    {
      uint8_t errore;
    } rovDataStruct;
    Функция TailCmdSend - сохраняет хвост команды в буфер TX, считаем контрольную сумму и отправляем данные.

    Пример отправки данных
    Код (C++):
    HeadCmdSend(0, 12, 1);

    SerializeStruct((uint8_t*)&ps2S, 12);

    TailCmdSend();
    Заполнение структуры ps2S, происходит обычным образом
    Код (C++):
    // Устанавливаем статус
    ps2S.statuswork = sendPS2Data;
    Все структуры объявлены как extern, сделано это для того, чтобы из любого модуля можно было добраться до данных структур. У меня просто принцип, получил запрос прерываний по UART после этого нет (они включены, но произойти не могут), затем спокойно заполняю данные, после чего отправляю. Это позволяет избежать проблем с атомарностью операций.

    Чтение, это просто постоянный вызов функции UARTGetData() в loop. Определение получены ли данные делаются на основании одного из полей структуры в примере поле statuswork структуры ps2Struct. После чего простая проверка
    Код (C++):
      // Пришли данные с джойстика.
      if (ps2S.statuswork == 1)
      {  
    После чего просто увеличиваю statuswork, чтобы вызов не произошел вторично. Тут можно использовать битовые поля и т.д.

    Функция UARTGetData, все что она делает это проверяет наличие данных в кольцевом буфере и переносит данные в 3 буфер InBuffer.

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

    Сам массив InBuffer
    Код (C++):
    // Буфер хранящий сами данные без служебной информации.
    #define InBufferSize 64
    static uint8_t InBuffer[InBufferSize][UARTNumber];
    static uint8_t IndexBuffer[UARTNumber];
    Тут мы видим 5 ложку дегтя, не зависимо от того сколько портов Вы используете, массив будет создан под все имеющиеся у контролера. Что потребует лишней оперативки.

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

    Вы можете воспользоватся данной функцией.
    Код (C++):
    // evaluate all other incoming serial data
    void evaluateOtherData(uint8_t sr)
    {
      switch (sr)
      {
      case 'A': // button A press
        toggle_telemetry(1);
        break;
      case 'B': // button B press
        toggle_telemetry(2);
        break;
      case 'C': // button C press
        toggle_telemetry(3);
        break;
      case 'D': // button D press
        toggle_telemetry(4);
        break;
      case 'a': // button A release
      case 'b': // button B release
      case 'c': // button C release
      case 'd': // button D release
        break;
      }
    }
     
    Вызывать ее нужно, в зависимости от порта или других предпочтений в UARTGetData, после и уходить от проверки пакетов. Выглядит это так
    Код (C++):

        while (cc--)
        {
          c = UARTRead(CurrentUARTPortRead);
         #ifdef SUPPRESS_ALL_SERIAL_MSP
            evaluateOtherData(c);
          #else
             Остальной код работы с пакетом
     
    И последнее, прежде чем перейти к данным по размеру и скорости работы. Отладка, параметр UART_HARDWARE_DEBUG, в зависимости от него будут подключаться функции для дебага (простой записи текста в порт). Данные функции были просто взяты из библиотеки Print.h Ардуино (Arduino 1.0.6\hardware\arduino\cores\arduino, старая привычка с AVR работать на 1.0.x, сейчас версии 1.6.x работают без проблем), немного изменены, лишь для того чтобы работали без классов.

    Примеры работы.
    Код (C++):
    // Вставляем в буфер.
    PrintDebug(4);
    // Отправляем.
    UARTSendData(UART_DEBUG_PORT);
    Не хотите лишний раз писать UARTSendData, тогда замените
    Код (C++):
    void WriteDebug(uint8_t c)
    {
      UARTSerialize(UART_DEBUG_PORT,c);
    }
    на
    Код (C++):
    void WriteDebug(uint8_t c)
    {
      UARTSerialize(UART_DEBUG_PORT,c);
      UARTSendData(UART_DEBUG_PORT);
    }
    И отправка будет выглядеть так
    Код (C++):
    PrintDebug(4);
    Так же можно отправлять данные без включения UART_HARDWARE_DEBUG, вот так.
    Код (C++):
    UARTSerialize(UART_DEBUG_PORT, '8');
    UARTSendData(UART_DEBUG_PORT);
    Но меньше возможностей, PrintDebug поддерживает DEC, BIN и не требуется заботится о конвертации данных в uint8_t.

    Ах да подключение RS-485, можно глянуть тут - http://adatum.ru/podklyuchenie-konvertera-rs-485-v-ttl-k-arduino.html.

    Зачем все это, во первых поделится реализацией (идеями), во вторых пока объясняешь другим, находишь замечания в реализации. Около года тому я считал данную реализацию совершенной:).

    Если у кого-то есть аналогичные решения но лучше, буду признателен если поделитесь. Пока я ковыряюсь c Node.js+socket.io+serial, закончу придется доводить до ума эту реализацию.

    Вечером скину параметры размер, скорость и фото этого безобразия. Вы можете проверить скачав файлы, просто закомментировать #if defined(DEBUG_SPEED_CYCLE) в .ino

    UPD.
    Исправление проверки скорости для примера UP, попала от другой реализации.
    Код (C++):
    #if defined(DEBUG_SPEED_CYCLE)
      timeCycle = GetDifferenceULong(timeCycleBegin, micros());

      if (timeCycle > timeCycleMax)
      {
        timeCycleMax = timeCycle;

        PrintDebug(timeCycleMax);
        PrintDebug('\n');
        UARTSendData(UART_DEBUG_PORT);
      }
    #endif
    Размер кода примера UP (Atmega2560).
    Максимальный все включено.
    Если выкинуть loop и setup, и сделать main (честный AVR). Оставив все включенным.
    Максимальное время цикла 0,108 миллисекунд.

    Для примера Down, софт сериал выключен.
    Если выкинуть loop и setup, и сделать main (честный AVR).
    Итог размер скетча для AVR не маленький, особенно для младших моделей. Память кушает не мало, особенно на AVR где много UART-ов.

    И фото сборка на макетке, аля мечта кота:).
    foto.jpg
    Моему всегда нравится мигающие огоньки, и всегда есть интрига будут ли работать соединения (поэтому сейчас храню в ящике из под молочных стаканчиков).
     
    Последнее редактирование: 18 дек 2015
    ИгорьК нравится это.