RS-485 на отечественных микроконтроллерах от фирмы Миландр

kv4v5bguuuolgcpmka8u5oerocu.png

Несколько дней назад я имел неосторожность завуалированно пообещать запилить пост про Миландр… Ну что ж, попробуем.

Как вы, вероятно, уже знаете, существует российская компания Миландр, которая, среди прочего, выпускает микроконтроллеры на ядре ARM Cortex-M. Волею судеб я был вынужден с ними познакомиться достаточно плотно, и познал боль.

Небольшая часть этой боли, вызванная работой с RS-485, описана далее. Заранее прошу прощения, если слишком сильно разжевываю базовые понятия, но мне хотелось сделать эту статью доступной для понимания более широкой аудитории.
Так же заранее оговорюсь, что имел дело только с 1986ВЕ91 и 1986ВЕ1, о других уверенно говорить не могу.

TL; DR

Миландровскому UART«у не хватает прерывания «Transmit complete», костыль — «режим проверки по шлейфу», т.е. режим эха. Но с нюансами.


Вступление


Интерфейс RS-485 (так же известный как EIA-485, хотя я ни разу не слышал, чтобы его так называли в обиходе) — это асинхронный полудуплексный интерфейс с топологией «шина». Этот стандарт оговаривает только физику — т.е. уровни напряжения и временные диаграммы —, но не оговаривает протокол обмена, защиту от ошибок передачи, арбитраж и тому подобное.

По факту, RS-485 — это просто полудуплексный UART с повышенными уровнями напряжения по дифференциальной паре. Именно эта простота и обеспечивает популярность RS-485.
Чтобы превратить UART в RS-485 используются специальные микросхемы-преобразователи, такие как MAX485 или 5559ИН10АУ (от того же Миландра). Они работают почти «прозрачно» для программиста, которому остается только правильно выбирать режим работы микросхемы — прием или передача. Делается это с помощью ног nRE (not Receiver Output Enable) и DE (Driver Output Enable), которые, как правило, объединяются и управляются одной ногой микроконтроллера.

Поднятие этой ноги переключает микросхему на передачу, а опускание — на прием.
Соответственно, все, что требуется от программиста, это поднять эту ногу RE-DE, передать нужное количество байт, опустить ногу и ждать ответа. Звучит достаточно просто, правда?
Хе-хе.

Проблема


Эту ногу нужно опустить в тот момент, когда все передаваемые байты полностью переданы на линию. Как поймать этот момент? Для этого нужно отловить событие «Transmit complete» (передача завершена), которое генерирует блок UART’a в микроконтроллере. В большинстве своем события — это выставление бита в каком-нибудь регистре или запрос прерывания. Чтобы отловить выставление бита в регистре, регистр нужно опрашивать, т.е. использовать код, вроде этого:

while( MDR_UART1->FR & UART_FR_BUSY ) {;}


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

Прерывание в этом отношении гораздо удобнее, поскольку оно прилетает само по себе, асинхронно. В прерывании мы можем быстренько опустить RE-DE и всего делов.

Разумеется, если бы мы могли так сделать, никакой боли бы не было и этого поста бы тоже не было.

Дело в том, что в блоке UART, который Миландр ставит во все свои микроконтроллеры на Cortex-M (насколько мне известно), нет прерывания по событию «Передача завершена». Есть только флаг. И есть прерывание «Буфер передатчика пуст». И прерывание «байт принят», конечно же.

Еще есть

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

Проблема в том, что «Буфер передатчика пуст» — это совсем не то же самое, что «Передача завершена». Насколько я понимаю внутреннее устройства UART’a, событие «Буфер пуст» означает, что в буфере передатчика есть хотя бы одно свободное место. Даже в случае, если это место всего одно (т.е. буфер размером в один байт), это лишь означает, что последний передаваемый байт был скопирован во внутренний сдвиговый регистр, из которого этот байт будет выползать на линию, бит за битом.

Короче говоря, событие «буфер передатчика пуст», не означает, что все байты были переданы полностью. Если мы опустим RE-DE в этот момент, то мы «обрежем» нашу посылку.

Что же делать?

Ребус


1ojz7t3sscudqyv37j94zeawls0.png
Расшифровка:
«Прополка битовых полей» — это локальный мем из короткой, но наполненной болью темы на форуме Миландра — forum.milandr.ru/viewtopic.php? f=33&t=626.
Простейшее решение — это таки «пропалывать» (от английского «poll» — непрерывный опрос) флаг UART_FR_BUSY.


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

Если использовать какую-нибудь ОСРВ, то ради этой прополки приходится заводить целую отдельную задачу, будить ее в прерывании, ставить ей не самый низкий приоритет, морока, короче.

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

Время, которое мы можем себе позволить «передержать» ногу RE-DE, зависит, в основном, от скорости передачи (бодрейта) и от быстроты устройства, с которым мы общаемся по шине.
В моем случае скорость была относительно невелика (57600 бод), а устройство было достаточно резвым. И иногда так случалось, что у ответа терялся бит-другой.

В целом, не очень хорошее решение.

Таймер


Второй вариант, который приходит в голову — использовать аппаратный таймер. Тогда в прерывании «Буфер передатчика пуст» мы запускаем таймер с таймаутом, который равен времени передачи одного байта (это время легко вычисляется из бодрейта), а в прерывании от таймера — опускать ногу.

Хороший, надежный способ. Только таймер жалко; их у Миландров традиционно немного — две-три штуки.

Режим шлейфа


Если внимательно читать тех. описание на UART — например, для 1986ВЕ91Т — можно заметить вот этот очень короткий абзац:

Проверка по шлейфу
Проверка по шлейфу (замыкание выхода передатчика на вход приемника) выполняется путем установки в 1 бита LBE в регистре управления контроллером UARTCR.

Если же тех. описание не читать, то практически того же эффекта можно добиться, закоротив ноги RX и TX аппаратно.

Мысли вслух

Интересно, причем тут какой-то шлейф? Обычно такой режим называется «эхо», ну да ладно.


Идея состоит в следующем — перед передачей последнего байта в посылке, нужно активировать режим «проверки по шлейфу». Тогда можно получить прерывание по приему нашего собственного последнего байта в тот момент, когда он полностью вылезет на шину! Ну, почти.

На практике оказалось, что прерывание по приему срабатывает немножкораньше, чем должно, примерно на треть битового интервала. Я не знаю с чем это связано; возможно, в режиме проверки по шлейфу не происходит настоящего сэмплирования линии, может быть, режим шлейфа не учитывает последний стоп-бит. Не знаю. Как бы то ни было, мы не можем опустить RE-DE сразу по входу в это прерывание, потому что так мы «отрежем» от нашего последнего байта стоп-бит или часть стоп-бита.

Строго говоря, можем или не можем зависит от соотношения скорости работы интерфейса (т.е. длительности одного битового интервала) и частоты работы микроконтроллера, но я на 80 МГц тактовой частоты и с бодрейтом 57600 не мог.

Далее возможны варианты.

Если вы можете себе позволить опрашивать флаг UART_FR_BUSY в течение одного битового интервала — на деле даже чуть меньше, потому что вход в прерывание и предварительные проверки тоже отнимают время — то выход найден. Для скорости 57600 максимальное время опроса составит ~18 микросекунд (один битовый интервал), на практике — около 5 микросекунд.

Для тех, кому интересно, привожу код обработчика прерывания целиком.
void Handle :: irqHandler(void)
{
    UMBA_ASSERT( m_isInited );

    m_irqCounter++;

    // --------------------------------------------- Прием

    // do нужен только чтобы делать break
    do
    {
        if ( UART_GetITStatusMasked( m_mdrUart, UART_IT_RX ) != SET )
            break;

        // по-факту, прерывание сбрасывается при чтении байта, но это недокументированная фича
        UART_ClearITPendingBit( m_mdrUart, UART_IT_RX );

        uint8_t byte = UART_ReceiveData( m_mdrUart );

        // для 485 используется режим шлейфа, поэтому мы можем принимать эхо самих себя
        if( m_rs485Port != nullptr && m_echoBytesCounter > 0 )
        {
            // эхо нам не нужно
            m_echoBytesCounter--;

            if( m_echoBytesCounter == 0 )
            {
                // после последнего байта надо __подождать__,
                // потому что мы принимаем его эхо до того, как стоп-бит до конца вылезет на линию
                // из-за мажоритарной логики семплирования.
                // Если не ждать, то можно потерять около трети стоп-бита.

                // Время ожидания зависит от бодрейта, примерное время ожидания:

                // бодрейт | длительность бита, |  время ожидания, |
                //         |        мкс         |       мкс        |
                //         |                    |                  |
                // 9600    |      105           |       32         |
                // 57600   |       18           |       4,5        |
                // 921600  |        1           |        0         |
                //         |                    |                  |

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

                // блокирующе пропалываем бит
                while( m_mdrUart->FR & UART_FR_BUSY ) {;}

                // и только теперь можно выключать передатчик и режим шлейфа
                rs485TransmitDisable();

                // семафор, что передача завершена
                #ifdef UART_USE_FREERTOS
                    osSemaphoreGiveFromISR( m_transmitCompleteSem, NULL );
                #endif
            }

            break;
        }

        // если в приемнике нет места - байт теряется и выставляется флаг overrun
        #ifdef UART_USE_FREERTOS

            BaseType_t result = osQueueSendToBackFromISR( m_rxQueue, &byte, NULL );

            if( result == errQUEUE_FULL )
            {
                m_isRxOverrun = true;
            }

        #else

            if( m_rxBuffer.isFull() )
            {
                m_isRxOverrun = true;
            }
            else
            {
                m_rxBuffer.writeHead(byte);
            }

        #endif


    } while( 0 );

    // --------------------------------------------- Ошибки

    // Проверяем на ошибки - обязательно после приема!
    // К сожалению, функций SPL для этого нет
    m_error = m_mdrUart->RSR_ECR;

    if( m_error != error_none )
    {
        // Ошибки в регистре сбрасывается
        m_mdrUart->RSR_ECR = 0;
    }

    // --------------------------------------------- Передача

    if( UART_GetITStatusMasked( m_mdrUart, UART_IT_TX ) != SET )
        return;

    // предпоследний байт в 485 - включаем режим шлейфа
    if( m_txCount == m_txMsgSize - 1 && m_rs485Port != nullptr )
    {
        setEchoModeState( true );
        m_echoBytesCounter = 2;
    }
    // все отправлено
    else if( m_txCount == m_txMsgSize )
    {
        // явный сброс можно (и нужно) делать только для последнего байта
        UART_ClearITPendingBit( m_mdrUart, UART_IT_TX );
        m_pTxBuf = nullptr;

        return;
    }

    // Еще есть, что отправить
    UMBA_ASSERT( m_pTxBuf != nullptr );

    UART_SendData( m_mdrUart, m_pTxBuf[ m_txCount ] );
    m_txCount++;
}


Если вы можете себе позволить перемычку (в идеале — управляемую) между ногами RX и TX, то всё тоже хорошо.

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

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

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

И, конечно же, напомнить о существовании форка стандартной периферийной библиотеки, в котором, в отличие от официальной библиотеки, исправляются баги и есть поддержка gcc.

© Habrahabr.ru