Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
1 task done
hitecSmartHome opened this issue Sep 13, 2024 · 17 comments
Open
1 task done
Assignees
Labels
Type: Feature request Feature request for Arduino ESP32

Comments

@hitecSmartHome
Copy link

hitecSmartHome commented Sep 13, 2024

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

  • I confirm I have checked existing list of Feature requests and Contribution Guide.
@hitecSmartHome hitecSmartHome added the Type: Feature request Feature request for Arduino ESP32 label Sep 13, 2024
@SuGlider SuGlider self-assigned this Sep 13, 2024
@SuGlider
Copy link
Collaborator

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
Copy link
Author

Yes

@hitecSmartHome
Copy link
Author

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
Copy link
Contributor

TD-er commented Sep 13, 2024

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
Copy link
Author

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

@hitecSmartHome
Copy link
Author

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
Copy link
Author

Current flow

  • Write a packet
  • Do other things while the slave processes the packet ( do not wait for anything in any task )
  • Get a cb about a new response packet
  • Process the response packet
  • Wait for the busy uart
  • Write a packet

This would be better

  • Write a packet
  • Do other things while the slave processes the packet ( do not wait for anything in any task )
  • Get a cb about a new response packet
  • Process the response packet
  • Get a cb about the whole packet is transmitted ( rts pin is low from high ? )
  • Write a new packet as soon as everything is transmitted ( in the cb ) ( no blocking while loop while the uart is busy in any task )

@device111
Copy link

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
Copy link
Contributor

TD-er commented Sep 18, 2024

@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
Copy link

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

@hitecSmartHome
Copy link
Author

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
Copy link
Author

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
Copy link
Author

hitecSmartHome commented Sep 19, 2024

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
Copy link

whatdtech commented Sep 27, 2024

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
Copy link
Author

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
Copy link

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

@hitecSmartHome
Copy link
Author

There is none.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Type: Feature request Feature request for Arduino ESP32
Projects
None yet
Development

No branches or pull requests

5 participants