RS-485 on domestic microcontrollers from the company Milandr

    A few days ago I had the imprudence of a veiled promise to gash a post about Milander ... Well, let's try.

    As you probably already know, there is a Russian company Milandr, which, among other things, produces microcontrollers based on the ARM Cortex-M core. By the will of fate, I was forced to get acquainted with them quite tightly, and knew the pain .

    A small part of this pain caused by working with RS-485 is described below. I apologize in advance if I chew too much on basic concepts, but I wanted to make this article accessible to a wider audience.
    I also make a reservation in advance that I only dealt with 1986Е91 and 1986Е1, I can’t talk about others with confidence.

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


    The RS-485 interface (also known as EIA-485, although I have never heard of being called it in everyday use) is an asynchronous half duplex interface with a bus topology. This standard specifies only physics - i.e. voltage levels and timing diagrams - but does not specify the exchange protocol, protection against transmission errors, arbitration, and the like.

    In fact, RS-485 is just a half-duplex UART with elevated voltage levels over a differential pair. It is this simplicity that makes RS-485 so popular.
    To convert the UART to RS-485, special converter chips are used, such as the MAX485 or 5559IN10AU (from the same Milandra). They work almost “transparently” for a programmer, who can only choose the correct mode of operation of the chip - reception or transmission. This is done using the legs nRE (not Receiver Output Enable) and DE (Driver Output Enable), which, as a rule, are combined and controlled by one leg of the microcontroller.

    Raising this leg switches the microcircuit to the gear, and lowering - to the reception.
    Accordingly, all that is required of the programmer is to raise this leg to RE-DE, transfer the required number of bytes, lower the leg and wait for an answer. Sounds simple enough, right?


    This leg must be lowered at the moment when all transmitted bytes are completely transferred to the line. How to catch this moment? To do this, you need to catch the event "Transmit complete" (transmission is completed), which generates the UART block in the microcontroller. For the most part, events are the setting of a bit in a register or an interrupt request. To catch the setting of a bit in the register, the register must be interrogated, i.e. use code like this:

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

    This is if we can afford to completely stop the execution of the program until all the bytes have been transferred. As a rule, we can not afford it.

    Interruption in this respect is much more convenient, since it arrives by itself, asynchronously. In the interrupt, we can quickly drop RE-DE and the whole business.

    Of course, if we could do this, there would be no pain, and this fast would not exist either.

    The fact is that in the UART block, which Milander puts in all of his microcontrollers on Cortex-M (as far as I know), there is no interruption on the event “Transfer is completed”. There is only a flag. And there is an interrupt "Transmit buffer is empty." And the interruption "byte is accepted", of course.

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

    The problem is that the Transmitter Buffer is Empty is not at all the same as Transmit Complete. As far as I understand the internal structure of UART, the event “Buffer is empty” means that there is at least one free space in the transmitter buffer. Even if this place is only one (i.e., a buffer in the size of one byte), this only means that the last transmitted byte was copied into the internal shift register, from which this byte will crawl out onto the line, bit by bit.

    In short, the “transmitter buffer empty” event does not mean that all bytes have been transmitted completely. If we omit RE-DE at this moment, we will “cut off” our package.

    What to do?


    «Прополка битовых полей» — это локальный мем из короткой, но наполненной болью темы на форуме Миландра —
    Простейшее решение – это таки «пропалывать» (от английского «poll» — непрерывный опрос) флаг UART_FR_BUSY.

    Of course, this solution is not very pleasant. If we cannot check this flag blocking, then we have to check it periodically. To check it periodically, you have to fence a whole garden (especially if you want to write a portable module, and not just one-time solve this problem).

    If we use some kind of RTOS, then for the sake of this weeding we have to start a whole separate task, wake it up in interruption, set it not the lowest priority, it is shorter.

    But, it would seem, all right, we suffered once, then we use and rejoice. But no.
    Unfortunately, it is not enough for us to omit RE-DE strictly after all bytes have been transmitted to the end. We need to lower it not too late.. Because we are not alone on the bus. Most likely, some reply from another subscriber should come to our message. And if we omit RE-DE too late, we will not switch to receive mode and lose a few bits of the answer.

    The time that we can afford to “overdo” the leg of the RE-DE depends mainly on the speed of transmission (cheerfulness) and on the speed of the device with which we communicate via the bus.
    In my case, the speed was relatively small (57600 baud), and the device was quite frisky. And sometimes it happened that the answer lost a bit or two.

    Overall, not a good decision.


    The second option that comes to mind is to use a hardware timer. Then, in the “Transmit buffer is empty” interrupt, we start a timer with a timeout that is equal to the transmission time of one byte (this time is easily calculated from the bodrate), and in the interrupt from the timer, lower the leg.

    Good, reliable way. Only the timer pity; Milandrov traditionally has few of them - two or three pieces.

    Loop mode

    If you carefully read those. the description on the UART - for example, for 1986ВЕ91Т - you can notice this very short paragraph: If those. If the description is not read, then practically the same effect can be achieved by shortening the legs of the RX and TX hardware.

    Проверка по шлейфу

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

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

    The idea is as follows - before transferring the last byte in the package, you need to activate the "test by loop" mode. Then you can get an interrupt on the reception of our own last byte at the moment when it completely crawls onto the bus! Almost.

    In practice, it turned out that the receive interrupt is triggered a little bit earlier than it should, by about a third of the bit interval. I do not know what it is connected with; perhaps, in the test mode on the loop, the real line sampling does not occur, maybe the loop mode does not take into account the last stop bit. I do not know. Anyway, we cannot drop RE-DE right after entering this interrupt, because in this way we “cut off” the stop bit or part of the stop bit from our last byte.

    Strictly speaking, we can or cannot depend on the ratio of the speed of the interface (that is, the duration of one bit interval) and the frequency of the microcontroller, but I could not at 80 MHz clock frequency and with 57600 clock rates.

    Further options are possible.

    If you can afford to poll the UART_FR_BUSY flag for one bit interval - in fact, even a little less, because the entry to the interrupt and the preliminary checks also take time - then the output is found. For the speed of 57600, the maximum polling time will be ~ 18 microseconds (one bit interval), in practice - about 5 microseconds.

    For those who are interested, I give the entire code for the interrupt handler.
    void Handle :: irqHandler(void)
        UMBA_ASSERT( m_isInited );
        // --------------------------------------------- Прием// do нужен только чтобы делать breakdo
            if ( UART_GetITStatusMasked( m_mdrUart, UART_IT_RX ) != SET )
            // по-факту, прерывание сбрасывается при чтении байта, но это недокументированная фича
            UART_ClearITPendingBit( m_mdrUart, UART_IT_RX );
            uint8_t byte = UART_ReceiveData( m_mdrUart );
            // для 485 используется режим шлейфа, поэтому мы можем принимать эхо самих себяif( m_rs485Port != nullptr && m_echoBytesCounter > 0 )
                // эхо нам не нужно
                if( m_echoBytesCounter == 0 )
                    // после последнего байта надо __подождать__,// потому что мы принимаем его эхо до того, как стоп-бит до конца вылезет на линию// из-за мажоритарной логики семплирования.// Если не ждать, то можно потерять около трети стоп-бита.// Время ожидания зависит от бодрейта, примерное время ожидания:// бодрейт | длительность бита, |  время ожидания, |//         |        мкс         |       мкс        |//         |                    |                  |// 9600    |      105           |       32         |// 57600   |       18           |       4,5        |// 921600  |        1           |        0         |//         |                    |                  |// при использовании двух стоп бит и/или бита четности,// время прополки вроде как не меняется.// Видимо, пропалывается только треть последнего бита, не важно какого.// блокирующе пропалываем битwhile( m_mdrUart->FR & UART_FR_BUSY ) {;}
                    // и только теперь можно выключать передатчик и режим шлейфа
                    // семафор, что передача завершена#ifdef UART_USE_FREERTOS
                        osSemaphoreGiveFromISR( m_transmitCompleteSem, NULL );
            // если в приемнике нет места - байт теряется и выставляется флаг overrun#ifdef UART_USE_FREERTOS
                BaseType_t result = osQueueSendToBackFromISR( m_rxQueue, &byte, NULL );
                if( result == errQUEUE_FULL )
                    m_isRxOverrun = true;
            #elseif( m_rxBuffer.isFull() )
                    m_isRxOverrun = true;
        } 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 )
        // предпоследний байт в 485 - включаем режим шлейфаif( m_txCount == m_txMsgSize - 1 && m_rs485Port != nullptr )
            setEchoModeState( true );
            m_echoBytesCounter = 2;
        // все отправленоelseif( m_txCount == m_txMsgSize )
            // явный сброс можно (и нужно) делать только для последнего байта
            UART_ClearITPendingBit( m_mdrUart, UART_IT_TX );
            m_pTxBuf = nullptr;
        // Еще есть, что отправить
        UMBA_ASSERT( m_pTxBuf != nullptr );
        UART_SendData( m_mdrUart, m_pTxBuf[ m_txCount ] );

    If you can afford a jumper (ideally managed) between the legs of the RX and TX, then everything is fine too.

    Unfortunately, I can’t offer any other options today.

    I have it all. If anyone knows other ways to solve this problem, please share them in the comments.

    Also, taking this opportunity and changing the rules of Habr, I want to promote the site StartMilandr , which is a collection of articles about Milander microcontrollers. For unclear reasons, you can google it only by accident.

    And, of course, recall the existence of a fork of a standard peripheral library, in which, unlike the official library, bugs are fixed and there is gcc support.

    Also popular now: