How to determine if TX is complete in RawSerial/UnbufferedSerial?

Hi,

I am trying to interface with a RS485 transceiver in half duplex mode, and need to disable driver right after TX is complete then enable receiver.

Is there anyway to determine in mbed if TX is done? We can try to directly access the MCU registers or add some arbitrary delay to deal with this, but both ways seem defy the purpose of Mbed.

Please advise. Thanks.

Hello Li,

In addition to the public write funtion the UnbufferedSerial has also two private write functions inherited from it’s base class - SerialBase.

    /** Begin asynchronous write using 8bit buffer.
     *
     *  The write operation ends with any of the enabled events and invokes
     *  registered callback function (which can be empty to not receive callback at all).
     *  Events that are not enabled by event argument are simply ignored.
     *  Operation has to be ended explicitly by calling abort_write() when
     *  no events are enabled.
     *  This function locks the deep sleep until any event has occurred.
     *
     *  @param buffer   The buffer where received data will be stored
     *  @param length   The buffer length in bytes
     *  @param callback The event callback function
     *  @param event    The logical OR of TX events that should end operation
     *  @return Zero if new transaction was started, -1 if transaction is already on-going
     */
    int write(const uint8_t *buffer, int length, const event_callback_t &callback, int event = SERIAL_EVENT_TX_COMPLETE);

    /** Begin asynchronous write using 16bit buffer.
     *
     *  The write operation ends with any of the enabled events and invokes
     *  registered callback function (which can be empty to not receive callback at all).
     *  Events that are not enabled by event argument are simply ignored.
     *  Operation has to be ended explicitly by calling abort_write() when
     *  no events are enabled.
     *  This function locks the deep sleep until any event has occurred.
     *
     *  @param buffer   The buffer where received data will be stored
     *  @param length   The buffer length in bytes
     *  @param callback The event callback function
     *  @param event    The logical OR of TX events that should end operation
     *  @return Zero if new transaction was started, -1 if transaction is already on-going
     */
    int write(const uint16_t *buffer, int length, const event_callback_t &callback, int event = SERIAL_EVENT_TX_COMPLETE);

As you can see these functions call a callback function you can use to determine when TX is complete.

So my suggestion is:

  • Define a MyUnbufferedSerial class by copy&pasting the UnbufferedSerial and change inheritance from private to public:
class UnbufferedSerial:
    private SerialBase,
    public FileHandle,
    private NonCopyable<UnbufferedSerial> {
public:
...

to

class MyUnbufferedSerial:
    public SerialBase,
    public FileHandle,
    private NonCopyable<UnbufferedSerial> {
public:
...

This should make the SerialBase class write functions available in the MyUnbufferedSerial class.

  • Another option is to use a SerialBase object in your application program rather then an UnbufferedSerial one.

Best regards, Zoltan

Hello

I’m using a RS485 transceiver in half duplex mode too so I have to manually switch the driver from TX mode to RX mode just right after transmission of all bytes in write() function completes.

   int write(const uint8_t *buffer, int length, const event_callback_t &callback, int event = SERIAL_EVENT_TX_COMPLETE);

Could someone kindly show me how define the event_callback_t function on MBED-OS 6?

Thanks in advance
Alex

Hello Alex,

  • The RS485 class, like the UnbufferedSerial, is derived from the SerialBase. However, the inheritance in this case is public.
  • Added a new Digitalout data-member _de (Driver Enabled - DE). On the hardware side it is assumed that the mbed _de pin is connected to (both) the DE and the /RE pins of the RS485 driver.
  • Added new RS485::enable_receiver member function.
  • The RS485::write member function calls the SerialBase::write function to ensure that the RS485::enable_receiver callback is get called on the SERIAL_EVENT_TX_COMPLETE event.

Please keep in mind that the code below has not been tested!

RS485.h:

/* mbed Microcontroller Library
 * Copyright (c) 2019 ARM Limited
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#ifndef RS485_H
#define RS485_H

#include "platform/platform.h"

#if DEVICE_SERIAL || defined(DOXYGEN_ONLY)

#include <cstdarg>

#include "drivers/SerialBase.h"
#include "platform/FileHandle.h"
#include "platform/mbed_toolchain.h"
#include "platform/NonCopyable.h"
#include "drivers/DigitalOut.h"


namespace mbed {
/** \defgroup drivers-public-api-uart UART
 * \ingroup drivers-public-api
 */

/**
 * \defgroup drivers_RS485 RS485 class
 * \ingroup drivers-public-api-uart
 * @{
 */

/**
 * Class implementation for unbuffered I/O for an interrupt driven application
 * or one that needs to have more control.
 */
class RS485:
    public SerialBase,
    public FileHandle,
    private NonCopyable<RS485> {
public:
    /**
     * Create a serial port instance connected to the specified transmit and
     * receive pins, with the specified baud rate.
     *
     *  @param tx Transmit pin
     *  @param rx Receive pin
     *  @param baud The baud rate of the serial port (optional, defaults to MBED_CONF_PLATFORM_DEFAULT_SERIAL_BAUD_RATE)
     *
     *  @note
     *    Either tx or rx may be specified as NC if unused
     */
    RS485(
        PinName tx,
        PinName rx,
        PinName de, // Driver Enable (DE) pin connected to `not Receiver Enable` (/RE) pin
        int baud = MBED_CONF_PLATFORM_DEFAULT_SERIAL_BAUD_RATE
    );

    /** Write the contents of a buffer to a file
     *
     * Blocks until all data is written
     *
     *  @param buffer   The buffer to write from
     *  @param size     The number of bytes to write
     *  @return         The number of bytes written
     */
    ssize_t write(const void *buffer, size_t size);

    /** Read the contents of a file into a buffer
     *
     *  Blocks and reads exactly one character
     *
     *  @param buffer   The buffer to read in to
     *  @param size     The number of bytes to read
     *  @return         The number of bytes read
     */
    ssize_t read(void *buffer, size_t size);

    /** Move the file position to a given offset from from a given location
     *
     * Not valid for a device type FileHandle like RS485.
     * In case of RS485, returns ESPIPE
     *
     *  @param offset   The offset from whence to move to
     *  @param whence   The start of where to seek
     *      SEEK_SET to start from beginning of file,
     *      SEEK_CUR to start from current position in file,
     *      SEEK_END to start from end of file
     *  @return         The new offset of the file, negative error code on failure
     */
    off_t seek(off_t offset, int whence = SEEK_SET) override
    {
        return -ESPIPE;
    }

    /** Get the size of the file
     *
     *  @return         Size of the file in bytes
     */
    off_t size() override
    {
        return -EINVAL;
    }

    /** Check if the file in an interactive terminal device
     *
     *  @return         True if the file is a terminal
     *  @return         False if the file is not a terminal
     *  @return         Negative error code on failure
     */
    int isatty() override
    {
        return true;
    }

    /** Close a file
     *
     *  @return         0 on success, negative error code on failure
     */
    int close() override
    {
        return 0;
    }

    /** Enable or disable input
     *
     * Control enabling of device for input. This is primarily intended
     * for temporary power-saving; the overall ability of the device to operate
     * for input and/or output may be fixed at creation time, but this call can
     * allow input to be temporarily disabled to permit power saving without
     * losing device state.
     *
     *  @param enabled      true to enable input, false to disable.
     *
     *  @return             0 on success
     *  @return             Negative error code on failure
     */
    int enable_input(bool enabled) override;

    /** Enable or disable output
     *
     * Control enabling of device for output. This is primarily intended
     * for temporary power-saving; the overall ability of the device to operate
     * for input and/or output may be fixed at creation time, but this call can
     * allow output to be temporarily disabled to permit power saving without
     * losing device state.
     *
     *  @param enabled      true to enable output, false to disable.
     *
     *  @return             0 on success
     *  @return             Negative error code on failure
     */
    int enable_output(bool enabled) override;

    /** Check for poll event flags
     * Check the events listed in events to see if data can be read or written
     * without blocking.
     * Call is nonblocking - returns state of events.
     *
     * @param events        bitmask of poll events we're interested in - POLLIN/POLLOUT etc.
     *
     * @returns             bitmask of poll events that have occurred.
     */
    short poll(short events) const override;

    using SerialBase::attach;
    using SerialBase::baud;
    using SerialBase::format;
    using SerialBase::readable;
    using SerialBase::writeable;
    using SerialBase::IrqCnt;
    using SerialBase::RxIrq;
    using SerialBase::TxIrq;

    using SerialBase::write;    // make the private function available for the RS485 class


#if DEVICE_SERIAL_FC
    // For now use the base enum - but in future we may have extra options
    // such as XON/XOFF or manual GPIO RTSCTS.
    using SerialBase::Flow;
    // In C++11, we wouldn't need to also have using directives for each value
    using SerialBase::Disabled;
    using SerialBase::RTS;
    using SerialBase::CTS;
    using SerialBase::RTSCTS;

    /** Set the flow control type on the serial port
     *
     *  @param type the flow control type (Disabled, RTS, CTS, RTSCTS)
     *  @param flow1 the first flow control pin (RTS for RTS or RTSCTS, CTS for CTS)
     *  @param flow2 the second flow control pin (CTS for RTSCTS)
     */
    void set_flow_control(Flow type, PinName flow1 = NC, PinName flow2 = NC);
#endif // DEVICE_SERIAL_FC

private:
    DigitalOut  _de;

    void enable_receiver(int event) { _de = 0; } // disable driver (enable receiver)
};

} // namespace mbed

#endif // DEVICE_SERIAL || defined(DOXYGEN_ONLY)

#endif // RS485_H

RS485.cpp:

/* mbed Microcontroller Library
 * Copyright (c) 2006-2019 ARM Limited
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "RS485.h"

#if DEVICE_SERIAL

#include "platform/mbed_critical.h"

namespace mbed {

RS485::RS485(
    PinName tx,
    PinName rx,
    PinName de,
    int baud
) : SerialBase(tx, rx, baud), _de(de)
{
    _de = 0;    // disable driver (enable receiver)
}

ssize_t RS485::write(const void *buffer, size_t size)
{
    _de = 1; // enable driver (disable receiver)

    const unsigned char *buf = static_cast<const unsigned char *>(buffer);

    if (size == 0) {
        return 0;
    }

    bool lock_api = !core_util_in_critical_section();

    if (lock_api) {
        lock();
    }

    for (size_t i = 0; i < size; i++) {
        _base_putc(buf[i]);
    }

    if (lock_api) {
        unlock();
    }

    SerialBase::write((const uint8_t*)buffer, size, callback(this, &RS485::enable_receiver), SERIAL_EVENT_TX_COMPLETE);

    return size;
}

ssize_t RS485::read(void *buffer, size_t size)
{
    unsigned char *buf = static_cast<unsigned char *>(buffer);

    if (size == 0) {
        return 0;
    }

    lock();

    buf[0] = _base_getc();

    unlock();

    return 1;
}

short RS485::poll(short events) const
{
    short revents = 0;
    if (
        (events & POLLIN)
        && (const_cast <RS485 *>(this))->SerialBase::readable()
    ) {
        revents |= POLLIN;
    }
    if (
        (events & POLLOUT)
        && (const_cast <RS485 *>(this))->SerialBase::writeable()
    ) {
        revents |= POLLOUT;
    }
    return revents;
}

int RS485::enable_input(bool enabled)
{
    SerialBase::enable_input(enabled);

    return 0;
}

int RS485::enable_output(bool enabled)
{
    SerialBase::enable_output(enabled);

    return 0;
}

#if DEVICE_SERIAL_FC
void RS485::set_flow_control(Flow type, PinName flow1, PinName flow2)
{
    lock();
    SerialBase::set_flow_control(type, flow1, flow2);
    unlock();
}
#endif // DEVICE_SERIAL_FC

} // namespace mbed

#endif // #if DEVICE_SERIAL

main.cpp:

#include "mbed.h"
#include "RS485.h"

DigitalOut led1(LED1);
RS485      rs485(PB_6, PA_6, PC_7, 115200);    // Tx, Rx, DE, baud

char buf[] = "Hello, RS485.";

int main()
{
    while(true)
    {
        led1 = !led1;
        rs485.write(buf, sizeof(buf));
        ThisThread::sleep_for(500ms);
    }
}

Hello Zoltan

Sorry for the delay, I’ll give a try to your suggestion very soon.

Thank you very much!
Regards

-Alex