Issues with UART controlling Isolated RS485/MODBUS

EarthAndy
Posts: 12
Joined: Fri Jan 19, 2024 6:53 pm

Issues with UART controlling Isolated RS485/MODBUS

Postby EarthAndy » Tue Jul 02, 2024 2:11 pm

I am currently working with a PCBA that has a esp32s2 chip that uses a UART channel to control a VFD over a simple MODBUS interface. The module is set up to use UART serial that is connected to a RS485 Field Isolator with RX, TX, and TX_enable. When my module sends a packet, it sets TX_enable in order for the isolator to pass the signal, and drops it to receive the response.

I am seeing an issue where when the module transmits a UART packet, it will hold the TX_enable for high too long and I miss the response. In my test systems I have two identical PCBAs running identical firmware connected to identical VFDs and one will show this issue for less than 5% of transceive events, and the other will fail on over 25% of packet comms, so there could be some hardware issues as well.
scope_3.png
scope_3.png (25.4 KiB) Viewed 1600 times
In this trace we are monitoring the TX line out of the esp32 on the yellow channel and the TX_enable out of the esp32 on the green channel. You can see that the TX_enable line is getting held for ~6ms past the end of the TX packet - and unfortunately the VFD response is just 2ms after the packet and it gets clobbered in the Field Isolator because the TX_enable line is still high.

The UART Initalization is pretty straight-forward (the code is basically from the examples). I'm using ASCII, N,8,1 @ 115.2k:

Code: Select all

//==============================================================================
// Initialize UART
//==============================================================================
void McuUartBasic_Esp32::init(uart_port_t uart_num, int tx_io_num, int rx_io_num, int rts_io_num, int cts_io_num, \
                                int baud_rate, uart_word_length_t data_bits, uart_parity_t parity, uart_stop_bits_t stop_bits, \
                                uart_hw_flowcontrol_t flow_ctrl,  uart_sclk_t source_clk ) {

    ESP_LOGI(LOG_Tag, "[%p|%d] Initializing UART#%d...", this, __LINE__, uart_num);
    
    _uart_num = uart_num;

    uart_config_t uart_config;
    memset( &uart_config, 0, sizeof(uart_config_t) );
    
    uart_config.baud_rate = baud_rate;
    uart_config.data_bits = data_bits;
    uart_config.parity    = parity;
    uart_config.stop_bits = stop_bits;
    uart_config.flow_ctrl = flow_ctrl;
    uart_config.source_clk = source_clk;

    // Configure UART parameters
    uart_param_config(_uart_num, &uart_config);

    // Set UART pins (TX, RX, RTS)
    uart_set_pin(_uart_num, tx_io_num, rx_io_num, rts_io_num, cts_io_num);

    // Install UART driver using an event queue here if needed
    uart_driver_install(_uart_num, MB_UART_RX_BUF_SIZE, 0, 0, NULL, 0);

    uart_set_mode(uart_num, UART_MODE_RS485_HALF_DUPLEX);

    ESP_LOGI(LOG_Tag, "[%p|%d] Initialized UART#%d!", this, __LINE__, _uart_num);

}

Code: Select all

#==============================================================================
# UART
#==============================================================================
add_compile_definitions( MODBUS_UART_CHANNEL=UART_NUM_1 )
add_compile_definitions( MODBUS_UART_BAUD=115200 )
add_compile_definitions( MODBUS_UART_WORD_LEN=UART_DATA_8_BITS )
add_compile_definitions( MODBUS_UART_PARITY=UART_PARITY_DISABLE )
add_compile_definitions( MODBUS_UART_STOP_BITS=UART_STOP_BITS_1 )
add_compile_definitions( MODBUS_UART_FLOW_CTRL=UART_HW_FLOWCTRL_DISABLE )
add_compile_definitions( MODBUS_UART_CLOCK_SRC=UART_SCLK_DEFAULT )

The primary TX handler sets the TX_enable pin, sends the packet, then drops the TX_enable pin (I left in some commented out lines of some attempts to fix this issue:

Code: Select all

void DevModbus::_do_modbus_tx(void) {

    uint16_t _tx_len = strlen((char*)_tx_buffer);
    //uint8_t _timeout_ms = (uint8_t)(1000.0f * (float)_tx_len * 8.0f / 115200.0f) + 1;

    //ESP_LOGI(LOG_Tag, "_do_read03_tx_ascii()  _tx_len:%u   _timeout_ms:%u", _tx_len, _timeout_ms);

    if(_tx_en) _tx_en->set(1);

    _uart_channel->_do_tx( _tx_buffer, _tx_len, 20 / portTICK_PERIOD_MS, true );
    //_uart_channel->_do_tx( _tx_buffer, _tx_len, _timeout_ms / portTICK_PERIOD_MS, true );
    //_uart_channel->_do_tx( _tx_buffer, _tx_len, 0, true );

    if(_tx_en) _tx_en->set(0);

}
... and the _do_tx() function is pretty simple:

Code: Select all

//==============================================================================
// Basic UART Transmit
//==============================================================================
void McuUartBasic_Esp32::_do_tx(uint8_t* tx_buffer, size_t len, uint32_t timeout_tics, bool rx_flush ) {

    if(_uart_num == -1) {
        ESP_LOGI(LOG_Tag, "[%p|%d] Can't do_tx without initialized UART! [%d]", this, __LINE__, _uart_num);
        return;
    }

    if(rx_flush) uart_flush(_uart_num);

    uart_write_bytes(_uart_num, (const char*)tx_buffer, len);

    if(timeout_tics)
        uart_wait_tx_done(_uart_num, timeout_tics);
}
According to the API for uart_write_bytes:
Send data to the UART port from a given buffer and length,.

If the UART driver's parameter 'tx_buffer_size' is set to zero: This function will not return until all the data have been sent out, or at least pushed into TX FIFO.

Otherwise, if the 'tx_buffer_size' > 0, this function will return after copying all the data to tx ring buffer, UART ISR will then move data from the ring buffer to TX FIFO gradually.
It seems that the uart_write_bytes is blocking for much longer than it should, and I can't seem to track down any reason why it would be. As you can see in the intialization that I have not set up a TX buffer so another confirmation that I should expect the block during TX:
tx_buffer_size -- UART TX ring buffer size. If set to zero, driver will not use TX buffer, TX function will block task until all data have been sent out.
This functionality all lives in its own FreeRTOS task. I've tried to move the task between the cores, I've tried to increase the priority, I've tried to give it more stack (just reaching at this point), but nothing will help those transmit events where the TX_enable holds over.

Has anyone run into something like this before, and where should I look to start to address this?

Thanks in advance.

aliarifat794
Posts: 200
Joined: Sun Jun 23, 2024 6:18 pm

Re: Issues with UART controlling Isolated RS485/MODBUS

Postby aliarifat794 » Tue Jul 02, 2024 5:58 pm

Sounds like you are working with a customized PCB. Have you performed the basic PCB tests?

EarthAndy
Posts: 12
Joined: Fri Jan 19, 2024 6:53 pm

Re: Issues with UART controlling Isolated RS485/MODBUS

Postby EarthAndy » Tue Jul 02, 2024 6:15 pm

Yes. There is no other circuitry that would pull or hold the TX_en line high.

TX goes directly to the isolator TX_in.
RX goes directly to the isolator RX_out.
TX_en goes directly to the isloator TX_en.


Most of the Message packets are properly bracketed by the TX_en (high while the packet is being sent), but I'm seeing upwards of 25% of the messages having the lagging TX_en.

EarthAndy
Posts: 12
Joined: Fri Jan 19, 2024 6:53 pm

Re: Issues with UART controlling Isolated RS485/MODBUS

Postby EarthAndy » Wed Jul 03, 2024 7:38 pm

SOLVED!

I started going over the API docs in detail and found this:
The ESP32 UART controllers themselves do not support half-duplex communication as they cannot provide automatic control of the RTS pin connected to the RE/DE input of RS485 bus driver. However, half-duplex communication can be achieved via software control of the RTS pin by the UART driver. This can be enabled by selecting the UART_MODE_RS485_HALF_DUPLEX mode when calling uart_set_mode().

Once the host starts writing data to the TX FIFO buffer, the UART driver automatically asserts the RTS pin (logic 1); once the last bit of the data has been transmitted, the driver de-asserts the RTS pin (logic 0).
To use this mode, the software would have to disable the hardware flow control function. This mode works with all the used circuits shown below.
I was already setting up the UART driver to do exactly this, but then I was controlling the TX_en pin as a GPIO on top of that:

Code: Select all

    if(_tx_en) _tx_en->set(1);
    _uart_channel->_do_tx( _tx_buffer, _tx_len, 10 / portTICK_PERIOD_MS, true );
    if(_tx_en) _tx_en->set(0);
I disabled the _tx_en GPIO that gets set before and after the _do_tx which allowed the UART driver to control the RTS on its own.

I also tweaked the _do_tx() fn a bit:

Code: Select all

    if(!timeout_tics)
        uart_write_bytes(_uart_num, (const char*)tx_buffer, len);
    else {
        uart_tx_chars(_uart_num, (const char*)tx_buffer, len);
        uart_wait_tx_done(_uart_num, timeout_tics);
    }
This was likely unnecessary in my application, but a better implementation of the timeouts.


So, lesson learned, if you use UART_MODE_RS485_HALF_DUPLEX, the UART driver will control the RTS on its own, don't try to control it yourself or you'll probably end up with races and collisions.

Who is online

Users browsing this forum: Bing [Bot] and 215 guests