Хабрахабр

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

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

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

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

TL; DR

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

Вступление

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

Именно эта простота и обеспечивает популярность RS-485.
Чтобы превратить UART в RS-485 используются специальные микросхемы-преобразователи, такие как MAX485 или 5559ИН10АУ (от того же Миландра). По факту, RS-485 — это просто полудуплексный UART с повышенными уровнями напряжения по дифференциальной паре. Делается это с помощью ног 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 в этот момент, то мы «обрежем» нашу посылку. Короче говоря, событие «буфер передатчика пуст», не означает, что все байты были переданы полностью.

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

Ребус

Расшифровка:

«Прополка битовых полей» — это локальный мем из короткой, но наполненной болью темы на форуме Миландра — 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 не мог. Строго говоря, можем или не можем зависит от соотношения скорости работы интерфейса (т.е.

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

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

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

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.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть