Arm Mbed OS support forum

Implementation of asynchronous Serial driver on NUCLEO-H743ZI2

Hello everyone,

recently I’ve been trying to implement an asynchronous driver for the serial port on a NUCLEO-H743ZI2 board. To be precise, the overall goal was to create a basic I/O mechanism for Modbus bus interfacing some servoamplifiers. My very first attempt was to utilize the mbed::UnbufferedSerial class. However, it quickly turned out that the write() method is implemented in a polling-manner and so cannot be used inside the TX ISR set by the attach() method.

I decided to switch implementation of my driver to the mbed::SerialBase that next to the attach() API shares simple _base_getc()/_base_putc() wrappers around STM’s HAL layer accessing data registers of the U(S)ART peripheral. At the moment I’m struggling with my current implementation of the TX channel. My idea was to introduce a simple ISR that writes requested data to the U(S)ART’s TX buffer byte by byte until the whole data buffer is sent. At that point, the ISR releases the rtos::Semaphore that the calling thread is waiting on. The whole process is guarded by the mbed::Timeout set to the 1.5 of the time needed to transmit a single byte. This is an additional mechanism that would protect driver from sufferring from too late TX ISR’s execution due to occurence of the higher-priority interrupts (this is an edge case and would probably never happen taking into account the range of used baudrates and frequency that the MCU’s core is operating at, but you can’t never be too safe in system of high reliability). The current version of the code implementing the algorithm is following:

// Context of the ISR
struct {
    volatile uint8_t bytesSent = 0;             // Number of bytes sent
    const std::span<uint8_t> data{data};        // Data to be sent (data is an std::span passed directly to the I/O method)
    rtos::Semaphore sem{0,1};                   // Semaphore that the calling thread will wait on
    mbed::Timeout timeout;                      // Timeout timer
    mbed::Callback<void(void)> timeout_handler; // Timeout handler
} irq_context;

// Timeout handler (on timeout releases semaphore that the calling thread will wait on)
irq_context.timeout_handler = [&sem = irq_context.sem, this]() {
    attach(nullptr, mbed::SerialBase::TxIrq); // Disable transmission
    sem.release();                            // Wake up waiting thread
};

/* --------------------------------- Write frame --------------------------------- */

// Set communication direction to TX (if half-duplex communication used)
if(rxEnableN.is_connected())
    rxEnableN.write(1);

// Arm the timeout timer (@var charTimeout is equal to 3/2 of the transmission period of a single byte)
irq_context.timeout.attach(irq_context.timeout_handler, charTimeout);

// Write request to the slave (setup byte TX interrupt handler)
attach([&irq_context, this](){

    // Write the next byte to the Modbus 
    _base_putc(static_cast<int>(irq_context.data[irq_context.bytesSent++]));
    
    // Check if the last byte has been written
    if(irq_context.bytesSent == irq_context.data.size()) {

        // Unarm the timeout timer
        irq_context.timeout.detach();
        // If so, disable TX interrupts ...
        attach(nullptr, mbed::SerialBase::TxIrq);
        // ... and wake up the calling thread
        irq_context.sem.release();

    // Else, if more bytes shall be written
    } else
        // Refresh the timeout timer
        irq_context.timeout.attach(irq_context.timeout_handler, charTimeout);

}, mbed::SerialBase::TxIrq);

// Put calling thread into wait state until the end of transmission
irq_context.sem.acquire();

// Check whether transmission succeeded
if(irq_context.bytesSent != data.size())
    return StatusCode(Error::Transmission);

// If a half-duplex bus is used, wait for the last byte to be completely transmitted
// before disabling output driver of the RS-232/485 interface chip
if(rxEnableN.is_connected()) {

    /**
     * @note This step requires an ugly workaround using STM32 drivers directly,
     *    as Mbed does not provide interface for waiting for the transmission
     *    to be completed
     */

    // Get pointer to the UART handle (@var uart_handlers is defined int STM's API implemntation)
    UART_HandleTypeDef *huart = &uart_handlers[_serial.index];
    // Wait for TC flag to be set
    while(__HAL_UART_GET_FLAG(huart, UART_FLAG_TC) != SET);
     // Disable bus driver
    rxEnableN = 0;
}

My main question concerns above implementation of the ISR. The code compiles and runs without producing any Mbed core’s error but it writes thrash to the bus (confirmed with an external RS485<->USB converted and osciloscope measurements). I suspect that the problem is a usage of captured variables in the ISR’s context. I’m not sure what should and what should not be declared as volatile here. Short debugging confirmed that the irq_context.bytesSent is read as ‘0’ after waking up the waiting thread. However ISR is surely(ish) executed, as the time between hanging and waking up the thread is many times longer than the timeout period. I will appreciate every bit of information from anybody who can make this C+±style IRQ handling issue a little bit clearer to me :slight_smile:

On a side note i can put in that if there is some simpler method to implement such a mechanism in Mbed i will willingly see any tips in this field :wink: Altough STM32_H7 famile has a SERIAL_ASYNCH API implemented in it’s target source code, the DEVICE_SERIAL_ASYNCH trait is not defined in the target.json for these MCU’s (and after trying to compile Mbed with DEVICE_SERIAL_ASYNCH defined i can tell that it is surely for a reason)

Problem’s solved. For those who will search for the solution for a similar problem: the mistake was using the same name for the irq_context.data field of the context structure and the argument of the enclosing function. That way, the irq_context.data field was constructed (to be precise: copy-assigned) with itself resulting in irq_context.data.size() equals 0. It explained the behaviour of the transmission routine, that (as it turned out) sent exactly 256 characters before interrupting (this is a representation range of the uint8_t type. The mistake resulted from being unable to capture data argument of the enclosing directly by the TX ISR lambda expression as according to the current C++ standard “Local class cannot reference to a local variable declared in enclosing function”.

However the side-note question still holds. If someone knows a simpler or more robust way to implement asynchronous Serial driver for targets that does not implement DEVICE_SERIAL_ASYNCH API it would be nice to have it posted on the forum for future developers in need :slight_smile:

Having searched many sources about using volatile in the embedded context (especially object-oriented ISR implementations) I think there may be a need to clarify the issue in some organized form for newcomers. As the Mbed is probably one of the biggest communities of developers using modern C++ in embedded context (Dan Saks perfectly explained why this is not a trivial issue) I think the Mbed social channels may be good place to do so :slight_smile: I don’t feel educated enough in C++ meanders (yet) to deal with it, so here I leave my personal ‘request’ that may be upvoted by the community in some future. If I find myself more confident in the field, I will surely share my thoughts with You!

Hi

Yes, seems SERIAL_ASYNCH is not defined for MCU_STM32H7…
This can be improved, any help is welcomed :slight_smile:
Thx