espressif / arduino-esp32

Arduino core for the ESP32
GNU Lesser General Public License v2.1
13.42k stars 7.37k forks source link

Register a callback when the last byte is written on UART or when the RTS pin become low #10343

Open hitecSmartHome opened 2 weeks ago

hitecSmartHome commented 2 weeks ago

Related area

UART

Hardware specification

UART

Is your feature request related to a problem?

Currently ( with arduino v3 and idf v5 ) I can register callback functions for onReceive and onReceiveError. When I receive a packet I immidiately want to write the next. It is not possible because the UART is still busy. I had to implement a logic to wait for the UART to become available before I can write.

A cb function would be good if the UART is available again.

Describe the solution you'd like

// Pass a cb just like in the case of onReceive
Serial1.onAvailable([](){
    // UART is avaiable again. Can write the next packet.
});

Describe alternatives you've considered

Before I want to write I call this wrapper function

while( isUartBusy() ){}

This is the implementation

bool Modbus::isUartBusy(){
    return uart_wait_tx_done(UART_NUM_1, 0) == ESP_ERR_TIMEOUT;
}

This is not ideal because it freezes the task which wants to write.

Additional context

Here is how I use the UART right now.

void Modbus::init() {
    Serial1.begin(MBUS_BAUD, SERIAL_8N1, MBUS_RX, MBUS_TX);
    Serial1.setPins(-1, -1, -1, MBUS_RTS);
    Serial1.setMode(UART_MODE_RS485_HALF_DUPLEX);
    Serial1.setRxTimeout(MBUS_RX_TIMEOUT);

    Serial1.onReceive(
        std::bind(&Modbus::handlePacket, this),
        PACKET_TRIGGER_ONLY_ON_TIMEOUT
    );
    Serial1.onReceiveError(std::bind(&Modbus::handleReceiveError, this, std::placeholders::_1));
}

I have checked existing list of Feature requests and the Contribution Guide

SuGlider commented 2 weeks ago

Humm... so the feature request is about using UART in Half Duplex mode. While receiving, no writing. While writing, no receiving.

You want a callback for when all data has left UART FIFO?

hitecSmartHome commented 2 weeks ago

Yes

hitecSmartHome commented 2 weeks ago

Currently when I get a cb about the data receive, the app immediately wants to write the next packet. It cant write since the uart is still busy. I have to wait until the uart is available so I can write the next packet. This results in a busy while waiting loop on one of the task. If there would be a cb about the rts pin I could write the next packet in that cb because that would signal to the app that the uart is available

TD-er commented 2 weeks ago

You can also look into hardware RS485 support of the ESP32. This should then toggle some DE/RE pin and you could then register a callback on change of this pin (or rise/fall) to handle the next data.

... or do the same on the RTS pin (as suggested in the issue title)

hitecSmartHome commented 2 weeks ago

Oh that is a good idea. Thank you very much will definitely try this

hitecSmartHome commented 2 weeks ago

Well. This is not good because I literally can't do anything in an ISR routin. Also I can't bind any class method and can't use any std::function.

While this could work

void IRAM_ATTR packetWriteDone(){

}
attachInterrupt(MBUS_RTS, packetWriteDone, FALLING);

It would be even slower than the current implementation. I must set a flag inside the interrupt and watch that flag in the loop. By the time any task reaches the flag check code in it's loop the busy while loop approach could write three packets

hitecSmartHome commented 2 weeks ago

Current flow

This would be better

device111 commented 2 weeks ago

It looks like you want implement the Modbus Protocol. It gaves a lot of working librarys for this. i.e.: https://github.com/yaacov/ArduinoModbusSlave

Have you tried Serial.flush()? It does similar like "uart_wait_tx_done", without a timeout.

Why you don't use Serial.available() for the incoming Bytes? Serial.onReceive() is always a blocking Function. And you can use the RTS Pin normaly before and after sending the packet. You don't must set the Serial to Half-Duplex.

TD-er commented 2 weeks ago

@device111 If you need to implement RS485, then the ESP32-xx all do have hardware support for toggling the DE/RE pin and even have collision detection in hardware. No need to make it complex.

device111 commented 2 weeks ago

Ok, you can use it for auto-toggle the DE Pin, but the rest of the implementation must affect to this. :wink:

hitecSmartHome commented 2 weeks ago

Yes, I have implemented the modbus protocoll but not the traditional so I can't use any library for this. How is the onReceive blocking? It loops in the UART task and calls my callback when the whole packet is received. It does not block anything. I have configured the hardware uart manager just like you said. Look at the init function

void Modbus::init() {
    Serial1.begin(MBUS_BAUD, SERIAL_8N1, MBUS_RX, MBUS_TX);
    Serial1.setPins(-1, -1, -1, MBUS_RTS);
    Serial1.setMode(UART_MODE_RS485_HALF_DUPLEX);
    Serial1.setRxTimeout(MBUS_RX_TIMEOUT);

    Serial1.onReceive(
        std::bind(&Modbus::handlePacket, this),
        PACKET_TRIGGER_ONLY_ON_TIMEOUT
    );
    Serial1.onReceiveError(std::bind(&Modbus::handleReceiveError, this, std::placeholders::_1));
}

This uses UART_MODE_RS485_HALF_DUPLEX and I have also configured the pins

  Serial1.begin(MBUS_BAUD, SERIAL_8N1, MBUS_RX, MBUS_TX);
  Serial1.setPins(-1, -1, -1, MBUS_RTS);

so I don't really have to do any pin toggling.

In my application, when I got a packet, I immidiately send the next packet. ( it is a master on the bus ) When this happens, sometimes the bus is busy and the write won't happen. That is why I implemented this function

bool Modbus::isUartBusy(){
    return uart_wait_tx_done(UART_NUM_1, 0) == ESP_ERR_TIMEOUT;
}

So it waits for the TX done. But I would prefer a callback style like onReceive about the transmit done event so I don't have to implement any busy loop.

hitecSmartHome commented 2 weeks ago

This basically looks like this in a simple form

void Modbus::init() {
    Serial1.begin(MBUS_BAUD, SERIAL_8N1, MBUS_RX, MBUS_TX);
    Serial1.setPins(-1, -1, -1, MBUS_RTS);
    Serial1.setMode(UART_MODE_RS485_HALF_DUPLEX);
    Serial1.setRxTimeout(MBUS_RX_TIMEOUT);

    Serial1.onReceive(
        std::bind(&Modbus::handlePacket, this),
        PACKET_TRIGGER_ONLY_ON_TIMEOUT
    );
    Serial1.onReceiveError(std::bind(&Modbus::handleReceiveError, this, std::placeholders::_1));
}

void Modbus::handlePacket() {
    // Check how many bytes are available
    int available = Serial1.available();
    // Check if it would overflow our buffer
    if (available >= MAX_MBUS_DATA_LENGTH) {
        ESP_LOGE(MBUS_DEBUG_TAG, "Packet is too big: %d bytes. Can't process it.", available);
        lastPacketError = BUFF_OVERFLOW;
        callErrorCb();
        return;
    }
    // Get all the data
    uint8_t rawPacket[available];
    int readBytes = Serial1.readBytes(rawPacket, available);
    // Check CRC and other error bytes.
    if (!isPacketValid(rawPacket, readBytes)) {
        ESP_LOGE(MBUS_DEBUG_TAG, "Invalid packet. Can't process it.");
        printRawPacket(rawPacket, readBytes);
        callErrorCb();
        return;
    }
    // Parse the packet if it was a scan packet.
    parseScanPacket(rawPacket, readBytes);
    // Call the packet callback if it wasn't a scan and we have a valid callback
    if (packetCallback && !isScanning) {
        packetCallback(rawPacket, readBytes);
    }
}

After these checks are done, the modbus calls the packetCallback. This immidiately sends the next packet to the next slave but the write has to wait because there are cases when the bus is still busy. I need continous communication between the slaves and the master. It can not be interrupted or waited on. I have to write the next packet as soon as I can. The onTxDone or onAvailable callback would simplify this because I could send the next packet as soon as the bus is free.

hitecSmartHome commented 2 weeks ago

Something like this would be really good

void Modbus::init() {
    Serial1.begin(MBUS_BAUD, SERIAL_8N1, MBUS_RX, MBUS_TX);
    Serial1.setPins(-1, -1, -1, MBUS_RTS);
    Serial1.setMode(UART_MODE_RS485_HALF_DUPLEX);
    Serial1.setRxTimeout(MBUS_RX_TIMEOUT);

    Serial1.onReceive(
        std::bind(&Modbus::handlePacket, this),
        PACKET_TRIGGER_ONLY_ON_TIMEOUT
    );
    Serial1.onReceiveError(std::bind(&Modbus::handleReceiveError, this, std::placeholders::_1));
    Serial1.onTransmitDone(std::bind(&Modbus::handleTransmitDone, this, std::placeholders::_1));
}

I could send the next packet inside handleTransmitDone because at this point it is sure that the bus is free.

Currently: got packet -> parse packet -> wait for uart free -> send next packet

Ideally: got packet -> parse packet got uart free -> send next packet

If the UART becomes free while a task parses the packet it would be event better because we don't have to wait for parsing the packet. We could send the next packet immidiately regardless of the latest data.

whatdtech commented 6 days ago

Hi @hitecSmartHome did you find any solution for this?. I'm actually trying to adapt profibus stack for esp32 based on this project github. I need to adapt USART data register empty and uart receive interrupts for esp32.

hitecSmartHome commented 6 days ago

Well, as I said, I'm doing it like this now:

Serial1.onReceive([](){
 // Got a whole packet. That callback means the uart triggered a byte timeout.
 // Read the whole UART buffer into my own.
 int available = Serial1.available();
 uint8_t rawPacket[available];
 int readBytes = Serial1.readBytes(rawPacket, available);
 // Do some checks on the packet like CRC and things like that...
 if( isPacketValid() ){
     // If the packet was valid, process it.
     processValidPacket();
 }
 // Write the next packet but wait for uart busy in `writeNextPacket`
 writeNextPacket();
},PACKET_TRIGGER_ONLY_ON_TIMEOUT);
whatdtech commented 6 days ago

@hitecSmartHome uart receive interrupt is ok, but what about uart data register empty interrupt?

hitecSmartHome commented 3 days ago

There is none.